+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
🔗 分享作品
+
+
+
+
+
diff --git a/dynamic-comic-studio/js/app.js b/dynamic-comic-studio/js/app.js
index 184d1b06..29be2ab8 100644
--- a/dynamic-comic-studio/js/app.js
+++ b/dynamic-comic-studio/js/app.js
@@ -1,581 +1,536 @@
-// ==================== HoloLake 动态漫制作系统 v5.0 ====================
-// 环节5:动画时间轴(帧序列 + 播放控制 + 帧率调节)
+// HoloLake 动态漫制作系统 - 环节6 (预览与分享版)
+// 基于环节5增强,新增预览模式、分享链接、嵌入代码功能
-const STORAGE_KEY = 'hololake-comic-studio-data';
-const canvas = document.getElementById('main-canvas');
-const ctx = canvas.getContext('2d');
-
-// ==================== 全局状态 ====================
-let appState = {
- currentSceneId: 1,
- nextSceneId: 2,
- nextAssetId: 1,
- nextFrameId: 1,
- scenes: [],
- playback: {
- isPlaying: false,
- fps: 4,
- intervalId: null,
- currentFrameIndex: 0
+// ==================== 全局变量 ====================
+let scenes = [
+ {
+ id: 'scene-1',
+ name: '场景1',
+ frames: [
+ {
+ id: 'frame-1-1',
+ assets: [] // 存储素材 { id, type, emoji, x, y, width, height }
+ }
+ ]
}
-};
+];
+let currentSceneIndex = 0;
+let currentFrameIndex = 0;
+let animationInterval = null;
+let isPlaying = false;
+let fps = 4;
+let canvas = document.getElementById('mainCanvas');
+let ctx = canvas.getContext('2d');
// ==================== 初始化 ====================
function init() {
- loadFromStorage();
- if (appState.scenes.length === 0) {
- createDefaultScene();
- }
+ console.log('HoloLake 动态漫制作系统 v6.0 初始化...');
+ updateUI();
setupEventListeners();
- renderSceneTabs();
- renderCanvas();
- renderTimeline();
- updatePlaybackControls();
+ drawCurrentFrame();
}
-// 创建默认场景
-function createDefaultScene() {
- const defaultScene = {
- id: 1,
- name: '场景 1',
- currentFrameIndex: 0,
- frames: [{
- id: 1,
- assets: []
- }],
- nextFrameId: 2
- };
- appState.scenes.push(defaultScene);
- appState.currentSceneId = 1;
- saveToStorage();
-}
-
-// ==================== 存储管理 ====================
-function saveToStorage() {
- localStorage.setItem(STORAGE_KEY, JSON.stringify(appState));
-}
-
-function loadFromStorage() {
- const data = localStorage.getItem(STORAGE_KEY);
- if (data) {
- const parsed = JSON.parse(data);
- // 向后兼容:旧数据没有 frames 字段
- if (parsed.scenes && parsed.scenes.length > 0 && !parsed.scenes[0].frames) {
- parsed.scenes = parsed.scenes.map(scene => ({
- ...scene,
- currentFrameIndex: 0,
- frames: [{
- id: 1,
- assets: scene.assets || []
- }],
- nextFrameId: 2
- }));
- delete parsed.scenes[0].assets; // 删除旧的 assets 字段
- }
- // 确保 playback 对象存在
- if (!parsed.playback) {
- parsed.playback = { isPlaying: false, fps: 4, intervalId: null, currentFrameIndex: 0 };
- }
- appState = parsed;
- }
-}
-
-// ==================== 场景管理 ====================
-function addScene() {
- const newScene = {
- id: appState.nextSceneId++,
- name: `场景 ${appState.scenes.length + 1}`,
- currentFrameIndex: 0,
- frames: [{
- id: 1,
- assets: []
- }],
- nextFrameId: 2
- };
- appState.scenes.push(newScene);
- appState.currentSceneId = newScene.id;
- saveToStorage();
- renderSceneTabs();
- renderCanvas();
- renderTimeline();
-}
-
-function switchScene(sceneId) {
- if (appState.playback.isPlaying) {
- stopAnimation();
- }
- appState.currentSceneId = sceneId;
- saveToStorage();
- renderSceneTabs();
- renderCanvas();
- renderTimeline();
- updateFrameCounter();
-}
-
-function deleteScene(sceneId) {
- if (appState.scenes.length <= 1) {
- alert('至少保留一个场景!');
- return;
- }
- appState.scenes = appState.scenes.filter(s => s.id !== sceneId);
- if (appState.currentSceneId === sceneId) {
- appState.currentSceneId = appState.scenes[0].id;
- }
- saveToStorage();
- renderSceneTabs();
- renderCanvas();
- renderTimeline();
-}
-
-function getCurrentScene() {
- return appState.scenes.find(s => s.id === appState.currentSceneId);
-}
-
-function getCurrentFrame() {
- const scene = getCurrentScene();
- if (!scene) return null;
- return scene.frames[scene.currentFrameIndex];
-}// ==================== 帧管理 ====================
-function addFrame() {
- const scene = getCurrentScene();
- if (!scene) return;
-
- // 深拷贝当前帧的素材状态
- const currentFrame = scene.frames[scene.currentFrameIndex];
- const newFrame = {
- id: scene.nextFrameId++,
- assets: JSON.parse(JSON.stringify(currentFrame.assets))
- };
-
- // 在当前帧后插入新帧
- scene.frames.splice(scene.currentFrameIndex + 1, 0, newFrame);
- scene.currentFrameIndex++;
-
- saveToStorage();
- renderTimeline();
- renderCanvas();
- updateFrameCounter();
-}
-
-function deleteFrame() {
- const scene = getCurrentScene();
- if (!scene) return;
-
- if (scene.frames.length <= 1) {
- alert('至少保留一帧!');
- return;
+// ==================== 事件监听设置 ====================
+function setupEventListeners() {
+ // FPS滑块
+ const fpsSlider = document.getElementById('fpsSlider');
+ if (fpsSlider) {
+ fpsSlider.addEventListener('input', function(e) {
+ fps = parseInt(e.target.value);
+ document.getElementById('fpsValue').textContent = fps + ' FPS';
+ if (isPlaying) {
+ stopAnimation();
+ startAnimation();
+ }
+ });
}
- scene.frames.splice(scene.currentFrameIndex, 1);
- if (scene.currentFrameIndex >= scene.frames.length) {
- scene.currentFrameIndex = scene.frames.length - 1;
+ // 预览按钮
+ const previewBtn = document.getElementById('previewBtn');
+ if (previewBtn) {
+ previewBtn.addEventListener('click', enterPreviewMode);
}
- saveToStorage();
- renderTimeline();
- renderCanvas();
- updateFrameCounter();
-}
-
-function switchFrame(frameIndex) {
- const scene = getCurrentScene();
- if (!scene || frameIndex < 0 || frameIndex >= scene.frames.length) return;
-
- // 保存当前画布状态到当前帧
- captureFrame();
-
- scene.currentFrameIndex = frameIndex;
- saveToStorage();
- renderTimeline();
- renderCanvas();
- updateFrameCounter();
-}
-
-function captureFrame() {
- // 画布状态实时保存在 assets 中,无需额外操作
- saveToStorage();
-}
-
-// ==================== 播放引擎 ====================
-function playAnimation() {
- const scene = getCurrentScene();
- if (!scene || scene.frames.length <= 1) return;
-
- appState.playback.isPlaying = true;
- updatePlaybackControls();
-
- const intervalMs = 1000 / appState.playback.fps;
-
- appState.playback.intervalId = setInterval(() => {
- const scene = getCurrentScene();
- if (!scene) return;
-
- scene.currentFrameIndex++;
- if (scene.currentFrameIndex >= scene.frames.length) {
- scene.currentFrameIndex = 0; // 循环播放
- }
-
- renderCanvas();
- renderTimeline();
- updateFrameCounter();
- saveToStorage();
- }, intervalMs);
-}
-
-function pauseAnimation() {
- appState.playback.isPlaying = false;
- if (appState.playback.intervalId) {
- clearInterval(appState.playback.intervalId);
- appState.playback.intervalId = null;
+ // 预览模式按钮
+ const playPreviewBtn = document.getElementById('playPreviewBtn');
+ if (playPreviewBtn) {
+ playPreviewBtn.addEventListener('click', function() {
+ if (!isPlaying) {
+ startAnimation();
+ }
+ });
}
- updatePlaybackControls();
-}
-
-function stopAnimation() {
- pauseAnimation();
- const scene = getCurrentScene();
- if (scene) {
- scene.currentFrameIndex = 0;
- renderCanvas();
- renderTimeline();
- updateFrameCounter();
- saveToStorage();
- }
-}
-
-function togglePlayback() {
- if (appState.playback.isPlaying) {
- pauseAnimation();
- } else {
- playAnimation();
- }
-}
-
-function setFPS(fps) {
- appState.playback.fps = parseInt(fps);
- document.getElementById('fps-display').textContent = fps + ' FPS';
- saveToStorage();
- // 如果正在播放,重启定时器以应用新帧率
- if (appState.playback.isPlaying) {
- pauseAnimation();
- playAnimation();
+ const pausePreviewBtn = document.getElementById('pausePreviewBtn');
+ if (pausePreviewBtn) {
+ pausePreviewBtn.addEventListener('click', function() {
+ if (isPlaying) {
+ stopAnimation();
+ }
+ });
}
-}
-
-function updatePlaybackControls() {
- const btnPlay = document.getElementById('btn-play');
- if (appState.playback.isPlaying) {
- btnPlay.textContent = '⏸️';
- btnPlay.title = '暂停';
- } else {
- btnPlay.textContent = '▶️';
- btnPlay.title = '播放';
- }
-}
-
-function updateFrameCounter() {
- const scene = getCurrentScene();
- if (!scene) return;
- const counter = document.getElementById('frame-counter');
- counter.textContent = `帧 ${scene.currentFrameIndex + 1}/${scene.frames.length}`;
-}// ==================== 渲染函数 ====================
-function renderSceneTabs() {
- const tabsList = document.getElementById('tabs-list');
- tabsList.innerHTML = '';
- appState.scenes.forEach(scene => {
- const tab = document.createElement('div');
- tab.className = 'scene-tab' + (scene.id === appState.currentSceneId ? ' active' : '');
- tab.innerHTML = `
- ${scene.name}
-
×
- `;
- tab.onclick = () => switchScene(scene.id);
- tabsList.appendChild(tab);
+ const stopPreviewBtn = document.getElementById('stopPreviewBtn');
+ if (stopPreviewBtn) {
+ stopPreviewBtn.addEventListener('click', function() {
+ stopAnimation();
+ currentFrameIndex = 0;
+ updateUI();
+ drawCurrentFrame();
+ });
+ }
+
+ const exitPreviewBtn = document.getElementById('exitPreviewBtn');
+ if (exitPreviewBtn) {
+ exitPreviewBtn.addEventListener('click', exitPreviewMode);
+ }
+
+ // 分享按钮
+ const shareBtn = document.getElementById('shareBtn');
+ if (shareBtn) {
+ shareBtn.addEventListener('click', showSharePanel);
+ }
+
+ // 分享面板按钮
+ const generateUrlBtn = document.getElementById('generateUrlBtn');
+ if (generateUrlBtn) {
+ generateUrlBtn.addEventListener('click', generateShareUrl);
+ }
+
+ const generateEmbedBtn = document.getElementById('generateEmbedBtn');
+ if (generateEmbedBtn) {
+ generateEmbedBtn.addEventListener('click', generateEmbedCode);
+ }
+
+ const closeShareBtn = document.getElementById('closeShareBtn');
+ if (closeShareBtn) {
+ closeShareBtn.addEventListener('click', hideSharePanel);
+ }
+
+ const copyShareBtn = document.getElementById('copyShareBtn');
+ if (copyShareBtn) {
+ copyShareBtn.addEventListener('click', copyToClipboard);
+ }
+
+ // 素材拖拽
+ const materialItems = document.querySelectorAll('.material-item');
+ materialItems.forEach(item => {
+ item.addEventListener('dragstart', handleDragStart);
});
+
+ // 画布放置
+ canvas.addEventListener('dragover', (e) => e.preventDefault());
+ canvas.addEventListener('drop', handleDrop);
+
+ // 画布点击选择素材
+ canvas.addEventListener('click', handleCanvasClick);
}
-function renderTimeline() {
- const timelineFrames = document.getElementById('timeline-frames');
- timelineFrames.innerHTML = '';
-
- const scene = getCurrentScene();
- if (!scene) return;
-
- scene.frames.forEach((frame, index) => {
- const thumb = document.createElement('div');
- thumb.className = 'frame-thumb' + (index === scene.currentFrameIndex ? ' active' : '');
- thumb.setAttribute('data-frame', index + 1);
- thumb.textContent = `帧 ${index + 1}`;
- thumb.onclick = () => switchFrame(index);
- timelineFrames.appendChild(thumb);
- });
+// ==================== 素材拖拽 ====================
+function handleDragStart(e) {
+ const type = e.target.dataset.type;
+ const emoji = e.target.dataset.emoji;
+ e.dataTransfer.setData('text/plain', JSON.stringify({
+ type: type,
+ emoji: emoji
+ }));
}
-function renderCanvas() {
- // 清空画布
- ctx.fillStyle = '#ffffff';
+// ==================== 素材放置 ====================
+function handleDrop(e) {
+ e.preventDefault();
+ const rect = canvas.getBoundingClientRect();
+ const scaleX = canvas.width / rect.width;
+ const scaleY = canvas.height / rect.height;
+
+ const x = (e.clientX - rect.left) * scaleX;
+ const y = (e.clientY - rect.top) * scaleY;
+
+ try {
+ const data = JSON.parse(e.dataTransfer.getData('text/plain'));
+
+ const currentScene = scenes[currentSceneIndex];
+ const currentFrame = currentScene.frames[currentFrameIndex];
+
+ const newAsset = {
+ id: 'asset-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9),
+ type: data.type,
+ emoji: data.emoji,
+ x: x - 25,
+ y: y - 25,
+ width: 50,
+ height: 50
+ };
+
+ currentFrame.assets.push(newAsset);
+ drawCurrentFrame();
+ updateUI();
+ } catch (error) {
+ console.error('放置素材失败:', error);
+ }
+}
+
+// ==================== 画布点击选择 ====================
+function handleCanvasClick(e) {
+ const rect = canvas.getBoundingClientRect();
+ const scaleX = canvas.width / rect.width;
+ const scaleY = canvas.height / rect.height;
+
+ const clickX = (e.clientX - rect.left) * scaleX;
+ const clickY = (e.clientY - rect.top) * scaleY;
+
+ const currentScene = scenes[currentSceneIndex];
+ const currentFrame = currentScene.frames[currentFrameIndex];
+
+ // 从后往前遍历,以便选中上层的素材
+ let selectedAsset = null;
+ for (let i = currentFrame.assets.length - 1; i >= 0; i--) {
+ const asset = currentFrame.assets[i];
+ if (clickX >= asset.x && clickX <= asset.x + asset.width &&
+ clickY >= asset.y && clickY <= asset.y + asset.height) {
+ selectedAsset = asset;
+ break;
+ }
+ }
+
+ // 移除所有选中状态
+ currentFrame.assets.forEach(a => delete a.selected);
+
+ // 设置新的选中状态
+ if (selectedAsset) {
+ selectedAsset.selected = true;
+ }
+
+ drawCurrentFrame();
+}
+
+// ==================== 绘制当前帧 ====================
+function drawCurrentFrame() {
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+
+ const currentScene = scenes[currentSceneIndex];
+ if (!currentScene) return;
+
+ const currentFrame = currentScene.frames[currentFrameIndex];
+ if (!currentFrame) return;
+
+ // 绘制背景色
+ ctx.fillStyle = '#0a0a14';
ctx.fillRect(0, 0, canvas.width, canvas.height);
- const frame = getCurrentFrame();
- if (!frame) return;
-
// 绘制所有素材
- frame.assets.forEach(asset => {
- ctx.font = '48px Arial';
+ currentFrame.assets.forEach(asset => {
+ ctx.font = '40px "Segoe UI Emoji", "Apple Color Emoji", sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
+ ctx.fillText(asset.emoji, asset.x + asset.width/2, asset.y + asset.height/2);
- // 绘制阴影
- ctx.shadowColor = 'rgba(0,0,0,0.3)';
- ctx.shadowBlur = 4;
- ctx.shadowOffsetX = 2;
- ctx.shadowOffsetY = 2;
-
- ctx.fillText(asset.emoji, asset.x, asset.y);
-
- // 重置阴影
- ctx.shadowColor = 'transparent';
- ctx.shadowBlur = 0;
- ctx.shadowOffsetX = 0;
- ctx.shadowOffsetY = 0;
-
- // 如果是选中状态,绘制边框
+ // 如果被选中,绘制边框
if (asset.selected) {
ctx.strokeStyle = '#e94560';
ctx.lineWidth = 2;
ctx.setLineDash([5, 5]);
- ctx.strokeRect(asset.x - 30, asset.y - 30, 60, 60);
+ ctx.strokeRect(asset.x, asset.y, asset.width, asset.height);
ctx.setLineDash([]);
}
});
}
-// ==================== 素材拖放 ====================
-function setupEventListeners() {
- // 素材库拖放
- const assetItems = document.querySelectorAll('.asset-item');
- assetItems.forEach(item => {
- item.addEventListener('dragstart', (e) => {
- e.dataTransfer.setData('type', item.dataset.type);
- e.dataTransfer.setData('emoji', item.dataset.emoji);
- });
- });
-
- // 画布接收拖放
- canvas.addEventListener('dragover', (e) => {
- e.preventDefault();
- });
-
- canvas.addEventListener('drop', (e) => {
- e.preventDefault();
- const type = e.dataTransfer.getData('type');
- const emoji = e.dataTransfer.getData('emoji');
-
- if (emoji) {
- const rect = canvas.getBoundingClientRect();
- const x = e.clientX - rect.left;
- const y = e.clientY - rect.top;
-
- addAssetToCanvas(emoji, x, y);
- }
- });
-
- // 画布点击选择/移动
- let isDragging = false;
- let dragStartX, dragStartY;
- let selectedAsset = null;
-
- canvas.addEventListener('mousedown', (e) => {
- const rect = canvas.getBoundingClientRect();
- const x = e.clientX - rect.left;
- const y = e.clientY - rect.top;
-
- const frame = getCurrentFrame();
- if (!frame) return;
-
- // 查找点击的素材(从后往前,优先选上层的)
- selectedAsset = null;
- for (let i = frame.assets.length - 1; i >= 0; i--) {
- const asset = frame.assets[i];
- const dist = Math.sqrt((x - asset.x) ** 2 + (y - asset.y) ** 2);
- if (dist < 30) {
- selectedAsset = asset;
- // 更新选中状态
- frame.assets.forEach(a => a.selected = false);
- asset.selected = true;
- isDragging = true;
- dragStartX = x - asset.x;
- dragStartY = y - asset.y;
- renderCanvas();
- break;
+// ==================== 场景管理 ====================
+function addScene() {
+ const newScene = {
+ id: 'scene-' + Date.now(),
+ name: '场景' + (scenes.length + 1),
+ frames: [
+ {
+ id: 'frame-' + Date.now() + '-1',
+ assets: []
}
- }
-
- // 如果没点到素材,取消所有选中
- if (!selectedAsset) {
- frame.assets.forEach(a => a.selected = false);
- renderCanvas();
- }
- });
-
- canvas.addEventListener('mousemove', (e) => {
- if (!isDragging || !selectedAsset) return;
-
- const rect = canvas.getBoundingClientRect();
- const x = e.clientX - rect.left;
- const y = e.clientY - rect.top;
-
- selectedAsset.x = x - dragStartX;
- selectedAsset.y = y - dragStartY;
-
- // 边界限制
- selectedAsset.x = Math.max(30, Math.min(canvas.width - 30, selectedAsset.x));
- selectedAsset.y = Math.max(30, Math.min(canvas.height - 30, selectedAsset.y));
-
- renderCanvas();
- });
-
- canvas.addEventListener('mouseup', () => {
- if (isDragging) {
- isDragging = false;
- saveToStorage();
- }
- });
-
- // 键盘删除
- document.addEventListener('keydown', (e) => {
- if (e.key === 'Delete' || e.key === 'Backspace') {
- const frame = getCurrentFrame();
- if (!frame) return;
-
- const selectedIndex = frame.assets.findIndex(a => a.selected);
- if (selectedIndex !== -1) {
- frame.assets.splice(selectedIndex, 1);
- renderCanvas();
- saveToStorage();
- }
- }
- });
-
- // 导入文件监听
- document.getElementById('import-file').addEventListener('change', handleImport);
+ ]
+ };
+ scenes.push(newScene);
+ currentSceneIndex = scenes.length - 1;
+ currentFrameIndex = 0;
+ updateUI();
+ drawCurrentFrame();
}
-function addAssetToCanvas(emoji, x, y) {
- const frame = getCurrentFrame();
- if (!frame) return;
+// ==================== 帧管理 ====================
+function addFrame() {
+ const currentScene = scenes[currentSceneIndex];
+ const currentFrame = currentScene.frames[currentFrameIndex];
- // 取消其他选中
- frame.assets.forEach(a => a.selected = false);
+ // 深拷贝当前帧的素材
+ const newAssets = JSON.parse(JSON.stringify(currentFrame.assets));
+ // 移除选中状态
+ newAssets.forEach(a => delete a.selected);
- const newAsset = {
- id: appState.nextAssetId++,
- type: 'emoji',
- emoji: emoji,
- x: x,
- y: y,
- selected: true
+ const newFrame = {
+ id: 'frame-' + Date.now(),
+ assets: newAssets
};
- frame.assets.push(newAsset);
- renderCanvas();
- saveToStorage();
-}// ==================== 导出导入 ====================
+ currentScene.frames.splice(currentFrameIndex + 1, 0, newFrame);
+ currentFrameIndex++;
+ updateUI();
+ drawCurrentFrame();
+}
+
+function deleteFrame() {
+ const currentScene = scenes[currentSceneIndex];
+ if (currentScene.frames.length <= 1) {
+ alert('每个场景至少保留一帧');
+ return;
+ }
+
+ currentScene.frames.splice(currentFrameIndex, 1);
+ if (currentFrameIndex >= currentScene.frames.length) {
+ currentFrameIndex = currentScene.frames.length - 1;
+ }
+ updateUI();
+ drawCurrentFrame();
+}
+
+// ==================== 动画控制 ====================
+function togglePlayback() {
+ if (isPlaying) {
+ stopAnimation();
+ } else {
+ startAnimation();
+ }
+}
+
+function startAnimation() {
+ if (isPlaying) return;
+ isPlaying = true;
+ updatePlayButton();
+
+ animationInterval = setInterval(() => {
+ const currentScene = scenes[currentSceneIndex];
+ if (currentFrameIndex < currentScene.frames.length - 1) {
+ currentFrameIndex++;
+ } else {
+ currentFrameIndex = 0;
+ }
+ updateUI();
+ drawCurrentFrame();
+ }, 1000 / fps);
+}
+
+function stopAnimation() {
+ if (animationInterval) {
+ clearInterval(animationInterval);
+ animationInterval = null;
+ }
+ isPlaying = false;
+ updatePlayButton();
+}
+
+function updatePlayButton() {
+ const playBtn = document.getElementById('timelinePlayBtn');
+ if (playBtn) {
+ playBtn.textContent = isPlaying ? '⏸️ 暂停' : '▶️ 播放';
+ }
+
+ const previewPlayBtn = document.getElementById('playPreviewBtn');
+ if (previewPlayBtn) {
+ // 预览模式的播放按钮文字不变
+ }
+}
+
+// ==================== 预览模式 ====================
+function enterPreviewMode() {
+ document.body.classList.add('preview-mode');
+ // 如果正在播放动画,继续播放
+}
+
+function exitPreviewMode() {
+ document.body.classList.remove('preview-mode');
+}
+
+// ==================== 分享功能 ====================
+function showSharePanel() {
+ document.getElementById('shareOverlay').classList.remove('hidden');
+ document.getElementById('sharePanel').classList.remove('hidden');
+}
+
+function hideSharePanel() {
+ document.getElementById('shareOverlay').classList.add('hidden');
+ document.getElementById('sharePanel').classList.add('hidden');
+ document.getElementById('shareUrl').innerHTML = '';
+ document.getElementById('copyShareBtn').style.display = 'none';
+}
+
+function generateShareUrl() {
+ // 序列化作品数据
+ const workData = {
+ scenes: scenes,
+ version: '1.0',
+ timestamp: Date.now()
+ };
+
+ const jsonStr = JSON.stringify(workData);
+ // 使用encodeURIComponent编码,然后生成Data URL
+ const encodedData = encodeURIComponent(jsonStr);
+ const dataUrl = 'data:text/json;charset=utf-8,' + encodedData;
+
+ // 显示分享链接
+ const shareUrlDiv = document.getElementById('shareUrl');
+ shareUrlDiv.innerHTML = `
${dataUrl.substring(0, 50)}...`;
+
+ document.getElementById('copyShareBtn').style.display = 'inline-block';
+ document.getElementById('copyShareBtn').dataset.url = dataUrl;
+}
+
+function generateEmbedCode() {
+ // 生成iframe嵌入代码
+ const embedCode = `
`;
+
+ const shareUrlDiv = document.getElementById('shareUrl');
+ shareUrlDiv.innerHTML = `
${embedCode}`;
+
+ document.getElementById('copyShareBtn').style.display = 'inline-block';
+ document.getElementById('copyShareBtn').dataset.code = embedCode;
+}
+
+function copyToClipboard(e) {
+ const btn = e.target;
+ if (btn.dataset.url) {
+ navigator.clipboard.writeText(btn.dataset.url);
+ } else if (btn.dataset.code) {
+ navigator.clipboard.writeText(btn.dataset.code);
+ }
+ alert('已复制到剪贴板!');
+}
+
+// ==================== 导入导出 ====================
function exportScene() {
- const dataStr = JSON.stringify(appState, null, 2);
- const blob = new Blob([dataStr], { type: 'application/json' });
+ const dataStr = JSON.stringify(scenes, null, 2);
+ const blob = new Blob([dataStr], {type: 'application/json'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
- a.download = `hololake-scene-${Date.now()}.json`;
- document.body.appendChild(a);
+ a.download = 'hololake-scene.json';
a.click();
- document.body.removeChild(a);
+
URL.revokeObjectURL(url);
}
-function importScene() {
- document.getElementById('import-file').click();
-}
-
-function handleImport(e) {
- const file = e.target.files[0];
+function importScene(event) {
+ const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
- reader.onload = (event) => {
+ reader.onload = function(e) {
try {
- const imported = JSON.parse(event.target.result);
-
- // 验证数据结构
- if (!imported.scenes || !Array.isArray(imported.scenes)) {
- throw new Error('无效的数据格式');
+ const importedScenes = JSON.parse(e.target.result);
+ if (Array.isArray(importedScenes) && importedScenes.length > 0) {
+ scenes = importedScenes;
+ currentSceneIndex = 0;
+ currentFrameIndex = 0;
+ updateUI();
+ drawCurrentFrame();
+ alert('导入成功!');
+ } else {
+ alert('无效的场景数据');
}
-
- // 向后兼容处理
- imported.scenes = imported.scenes.map(scene => {
- if (!scene.frames) {
- return {
- ...scene,
- currentFrameIndex: 0,
- frames: [{
- id: 1,
- assets: scene.assets || []
- }],
- nextFrameId: 2
- };
- }
- return scene;
- });
-
- if (!imported.playback) {
- imported.playback = { isPlaying: false, fps: 4, intervalId: null, currentFrameIndex: 0 };
- }
-
- appState = imported;
- saveToStorage();
- renderSceneTabs();
- renderCanvas();
- renderTimeline();
- updatePlaybackControls();
- updateFrameCounter();
-
- alert('导入成功!');
- } catch (err) {
- alert('导入失败:' + err.message);
+ } catch (error) {
+ alert('导入失败:' + error.message);
}
};
reader.readAsText(file);
- // 清空 input,允许重复导入同一文件
- e.target.value = '';
+ // 清空input,以便再次导入同一个文件
+ event.target.value = '';
}
-// ==================== 截图导出 ====================
function exportScreenshot() {
- // 临时取消选中状态
- const frame = getCurrentFrame();
- if (!frame) return;
-
- const originalSelected = frame.assets.map(a => a.selected);
- frame.assets.forEach(a => a.selected = false);
- renderCanvas();
-
- // 导出 PNG
- const link = document.createElement('a');
- link.download = `hololake-frame-${Date.now()}.png`;
- link.href = canvas.toDataURL();
- link.click();
-
- // 恢复选中状态
- frame.assets.forEach((a, i) => {
- a.selected = originalSelected[i];
- });
- renderCanvas();
+ const dataUrl = canvas.toDataURL('image/png');
+ const a = document.createElement('a');
+ a.href = dataUrl;
+ a.download = 'hololake-screenshot-' + Date.now() + '.png';
+ a.click();
}
-// ==================== 启动应用 ====================
-document.addEventListener('DOMContentLoaded', init);
+// ==================== 更新UI ====================
+function updateUI() {
+ // 更新场景列表
+ const sceneList = document.getElementById('sceneList');
+ if (sceneList) {
+ sceneList.innerHTML = '';
+ scenes.forEach((scene, index) => {
+ const sceneDiv = document.createElement('div');
+ sceneDiv.className = 'scene-item' + (index === currentSceneIndex ? ' active' : '');
+ sceneDiv.innerHTML = `
+
${scene.name}
+
+ `;
+ sceneDiv.addEventListener('click', (e) => {
+ if (e.target.tagName !== 'BUTTON') {
+ switchScene(index);
+ }
+ });
+ sceneList.appendChild(sceneDiv);
+ });
+ }
+
+ // 更新帧列表
+ const framesContainer = document.getElementById('framesContainer');
+ if (framesContainer && scenes[currentSceneIndex]) {
+ framesContainer.innerHTML = '';
+ scenes[currentSceneIndex].frames.forEach((frame, index) => {
+ const frameDiv = document.createElement('div');
+ frameDiv.className = 'frame-thumb' + (index === currentFrameIndex ? ' active' : '');
+ frameDiv.innerHTML = `
+
${index + 1}
+
${frame.assets.length}个素材
+ `;
+ frameDiv.addEventListener('click', () => {
+ currentFrameIndex = index;
+ drawCurrentFrame();
+ updateUI();
+ });
+ framesContainer.appendChild(frameDiv);
+ });
+ }
+
+ // 更新帧计数器
+ const frameCounter = document.getElementById('frame-counter');
+ if (frameCounter && scenes[currentSceneIndex]) {
+ frameCounter.textContent = `帧 ${currentFrameIndex + 1}/${scenes[currentSceneIndex].frames.length}`;
+ }
+}
+
+function switchScene(index) {
+ if (index >= 0 && index < scenes.length) {
+ currentSceneIndex = index;
+ currentFrameIndex = 0;
+ if (isPlaying) {
+ stopAnimation();
+ }
+ updateUI();
+ drawCurrentFrame();
+ }
+}
+
+function deleteScene(index) {
+ if (scenes.length <= 1) {
+ alert('至少保留一个场景');
+ return;
+ }
+
+ scenes.splice(index, 1);
+ if (currentSceneIndex >= scenes.length) {
+ currentSceneIndex = scenes.length - 1;
+ }
+ currentFrameIndex = 0;
+ if (isPlaying) {
+ stopAnimation();
+ }
+ updateUI();
+ drawCurrentFrame();
+}
+
+// ==================== 启动初始化 ====================
+window.onload = init;
diff --git a/dynamic-comic-studio/js/app.js.backup b/dynamic-comic-studio/js/app.js.backup
new file mode 100644
index 00000000..42fc6145
--- /dev/null
+++ b/dynamic-comic-studio/js/app.js.backup
@@ -0,0 +1,1113 @@
+// ==================== HoloLake 动态漫制作系统 v5.0 ====================
+// 环节5:动画时间轴(帧序列 + 播放控制 + 帧率调节)
+
+const STORAGE_KEY = 'hololake-comic-studio-data';
+const canvas = document.getElementById('main-canvas');
+const ctx = canvas.getContext('2d');
+
+// ==================== 全局状态 ====================
+let appState = {
+ currentSceneId: 1,
+ nextSceneId: 2,
+ nextAssetId: 1,
+ nextFrameId: 1,
+ scenes: [],
+ playback: {
+ isPlaying: false,
+ fps: 4,
+ intervalId: null,
+ currentFrameIndex: 0
+ }
+};
+
+// ==================== 初始化 ====================
+function init() {
+ loadFromStorage();
+ if (appState.scenes.length === 0) {
+ createDefaultScene();
+ }
+ setupEventListeners();
+ renderSceneTabs();
+ renderCanvas();
+ renderTimeline();
+ updatePlaybackControls();
+}
+
+// 创建默认场景
+function createDefaultScene() {
+ const defaultScene = {
+ id: 1,
+ name: '场景 1',
+ currentFrameIndex: 0,
+ frames: [{
+ id: 1,
+ assets: []
+ }],
+ nextFrameId: 2
+ };
+ appState.scenes.push(defaultScene);
+ appState.currentSceneId = 1;
+ saveToStorage();
+}
+
+// ==================== 存储管理 ====================
+function saveToStorage() {
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(appState));
+}
+
+function loadFromStorage() {
+ const data = localStorage.getItem(STORAGE_KEY);
+ if (data) {
+ const parsed = JSON.parse(data);
+ // 向后兼容:旧数据没有 frames 字段
+ if (parsed.scenes && parsed.scenes.length > 0 && !parsed.scenes[0].frames) {
+ parsed.scenes = parsed.scenes.map(scene => ({
+ ...scene,
+ currentFrameIndex: 0,
+ frames: [{
+ id: 1,
+ assets: scene.assets || []
+ }],
+ nextFrameId: 2
+ }));
+ delete parsed.scenes[0].assets; // 删除旧的 assets 字段
+ }
+ // 确保 playback 对象存在
+ if (!parsed.playback) {
+ parsed.playback = { isPlaying: false, fps: 4, intervalId: null, currentFrameIndex: 0 };
+ }
+ appState = parsed;
+ }
+}
+
+// ==================== 场景管理 ====================
+function addScene() {
+ const newScene = {
+ id: appState.nextSceneId++,
+ name: `场景 ${appState.scenes.length + 1}`,
+ currentFrameIndex: 0,
+ frames: [{
+ id: 1,
+ assets: []
+ }],
+ nextFrameId: 2
+ };
+ appState.scenes.push(newScene);
+ appState.currentSceneId = newScene.id;
+ saveToStorage();
+ renderSceneTabs();
+ renderCanvas();
+ renderTimeline();
+}
+
+function switchScene(sceneId) {
+ if (appState.playback.isPlaying) {
+ stopAnimation();
+ }
+ appState.currentSceneId = sceneId;
+ saveToStorage();
+ renderSceneTabs();
+ renderCanvas();
+ renderTimeline();
+ updateFrameCounter();
+}
+
+function deleteScene(sceneId) {
+ if (appState.scenes.length <= 1) {
+ alert('至少保留一个场景!');
+ return;
+ }
+ appState.scenes = appState.scenes.filter(s => s.id !== sceneId);
+ if (appState.currentSceneId === sceneId) {
+ appState.currentSceneId = appState.scenes[0].id;
+ }
+ saveToStorage();
+ renderSceneTabs();
+ renderCanvas();
+ renderTimeline();
+}
+
+function getCurrentScene() {
+ return appState.scenes.find(s => s.id === appState.currentSceneId);
+}
+
+function getCurrentFrame() {
+ const scene = getCurrentScene();
+ if (!scene) return null;
+ return scene.frames[scene.currentFrameIndex];
+}// ==================== 帧管理 ====================
+function addFrame() {
+ const scene = getCurrentScene();
+ if (!scene) return;
+
+ // 深拷贝当前帧的素材状态
+ const currentFrame = scene.frames[scene.currentFrameIndex];
+ const newFrame = {
+ id: scene.nextFrameId++,
+ assets: JSON.parse(JSON.stringify(currentFrame.assets))
+ };
+
+ // 在当前帧后插入新帧
+ scene.frames.splice(scene.currentFrameIndex + 1, 0, newFrame);
+ scene.currentFrameIndex++;
+
+ saveToStorage();
+ renderTimeline();
+ renderCanvas();
+ updateFrameCounter();
+}
+
+function deleteFrame() {
+ const scene = getCurrentScene();
+ if (!scene) return;
+
+ if (scene.frames.length <= 1) {
+ alert('至少保留一帧!');
+ return;
+ }
+
+ scene.frames.splice(scene.currentFrameIndex, 1);
+ if (scene.currentFrameIndex >= scene.frames.length) {
+ scene.currentFrameIndex = scene.frames.length - 1;
+ }
+
+ saveToStorage();
+ renderTimeline();
+ renderCanvas();
+ updateFrameCounter();
+}
+
+function switchFrame(frameIndex) {
+ const scene = getCurrentScene();
+ if (!scene || frameIndex < 0 || frameIndex >= scene.frames.length) return;
+
+ // 保存当前画布状态到当前帧
+ captureFrame();
+
+ scene.currentFrameIndex = frameIndex;
+ saveToStorage();
+ renderTimeline();
+ renderCanvas();
+ updateFrameCounter();
+}
+
+function captureFrame() {
+ // 画布状态实时保存在 assets 中,无需额外操作
+ saveToStorage();
+}
+
+// ==================== 播放引擎 ====================
+function playAnimation() {
+ const scene = getCurrentScene();
+ if (!scene || scene.frames.length <= 1) return;
+
+ appState.playback.isPlaying = true;
+ updatePlaybackControls();
+
+ const intervalMs = 1000 / appState.playback.fps;
+
+ appState.playback.intervalId = setInterval(() => {
+ const scene = getCurrentScene();
+ if (!scene) return;
+
+ scene.currentFrameIndex++;
+ if (scene.currentFrameIndex >= scene.frames.length) {
+ scene.currentFrameIndex = 0; // 循环播放
+ }
+
+ renderCanvas();
+ renderTimeline();
+ updateFrameCounter();
+ saveToStorage();
+ }, intervalMs);
+}
+
+function pauseAnimation() {
+ appState.playback.isPlaying = false;
+ if (appState.playback.intervalId) {
+ clearInterval(appState.playback.intervalId);
+ appState.playback.intervalId = null;
+ }
+ updatePlaybackControls();
+}
+
+function stopAnimation() {
+ pauseAnimation();
+ const scene = getCurrentScene();
+ if (scene) {
+ scene.currentFrameIndex = 0;
+ renderCanvas();
+ renderTimeline();
+ updateFrameCounter();
+ saveToStorage();
+ }
+}
+
+function togglePlayback() {
+ if (appState.playback.isPlaying) {
+ pauseAnimation();
+ } else {
+ playAnimation();
+ }
+}
+
+function setFPS(fps) {
+ appState.playback.fps = parseInt(fps);
+ document.getElementById('fps-display').textContent = fps + ' FPS';
+ saveToStorage();
+
+ // 如果正在播放,重启定时器以应用新帧率
+ if (appState.playback.isPlaying) {
+ pauseAnimation();
+ playAnimation();
+ }
+}
+
+function updatePlaybackControls() {
+ const btnPlay = document.getElementById('btn-play');
+ if (appState.playback.isPlaying) {
+ btnPlay.textContent = '⏸️';
+ btnPlay.title = '暂停';
+ } else {
+ btnPlay.textContent = '▶️';
+ btnPlay.title = '播放';
+ }
+}
+
+function updateFrameCounter() {
+ const scene = getCurrentScene();
+ if (!scene) return;
+ const counter = document.getElementById('frame-counter');
+ counter.textContent = `帧 ${scene.currentFrameIndex + 1}/${scene.frames.length}`;
+}// ==================== 渲染函数 ====================
+function renderSceneTabs() {
+ const tabsList = document.getElementById('tabs-list');
+ tabsList.innerHTML = '';
+
+ appState.scenes.forEach(scene => {
+ const tab = document.createElement('div');
+ tab.className = 'scene-tab' + (scene.id === appState.currentSceneId ? ' active' : '');
+ tab.innerHTML = `
+ ${scene.name}
+
×
+ `;
+ tab.onclick = () => switchScene(scene.id);
+ tabsList.appendChild(tab);
+ });
+}
+
+function renderTimeline() {
+ const timelineFrames = document.getElementById('timeline-frames');
+ timelineFrames.innerHTML = '';
+
+ const scene = getCurrentScene();
+ if (!scene) return;
+
+ scene.frames.forEach((frame, index) => {
+ const thumb = document.createElement('div');
+ thumb.className = 'frame-thumb' + (index === scene.currentFrameIndex ? ' active' : '');
+ thumb.setAttribute('data-frame', index + 1);
+ thumb.textContent = `帧 ${index + 1}`;
+ thumb.onclick = () => switchFrame(index);
+ timelineFrames.appendChild(thumb);
+ });
+}
+
+function renderCanvas() {
+ // 清空画布
+ ctx.fillStyle = '#ffffff';
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+
+ const frame = getCurrentFrame();
+ if (!frame) return;
+
+ // 绘制所有素材
+ frame.assets.forEach(asset => {
+ ctx.font = '48px Arial';
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'middle';
+
+ // 绘制阴影
+ ctx.shadowColor = 'rgba(0,0,0,0.3)';
+ ctx.shadowBlur = 4;
+ ctx.shadowOffsetX = 2;
+ ctx.shadowOffsetY = 2;
+
+ ctx.fillText(asset.emoji, asset.x, asset.y);
+
+ // 重置阴影
+ ctx.shadowColor = 'transparent';
+ ctx.shadowBlur = 0;
+ ctx.shadowOffsetX = 0;
+ ctx.shadowOffsetY = 0;
+
+ // 如果是选中状态,绘制边框
+ if (asset.selected) {
+ ctx.strokeStyle = '#e94560';
+ ctx.lineWidth = 2;
+ ctx.setLineDash([5, 5]);
+ ctx.strokeRect(asset.x - 30, asset.y - 30, 60, 60);
+ ctx.setLineDash([]);
+ }
+ });
+}
+
+// ==================== 素材拖放 ====================
+function setupEventListeners() {
+ // 素材库拖放
+ const assetItems = document.querySelectorAll('.asset-item');
+ assetItems.forEach(item => {
+ item.addEventListener('dragstart', (e) => {
+ e.dataTransfer.setData('type', item.dataset.type);
+ e.dataTransfer.setData('emoji', item.dataset.emoji);
+ });
+ });
+
+ // 画布接收拖放
+ canvas.addEventListener('dragover', (e) => {
+ e.preventDefault();
+ });
+
+ canvas.addEventListener('drop', (e) => {
+ e.preventDefault();
+ const type = e.dataTransfer.getData('type');
+ const emoji = e.dataTransfer.getData('emoji');
+
+ if (emoji) {
+ const rect = canvas.getBoundingClientRect();
+ const x = e.clientX - rect.left;
+ const y = e.clientY - rect.top;
+
+ addAssetToCanvas(emoji, x, y);
+ }
+ });
+
+ // 画布点击选择/移动
+ let isDragging = false;
+ let dragStartX, dragStartY;
+ let selectedAsset = null;
+
+ canvas.addEventListener('mousedown', (e) => {
+ const rect = canvas.getBoundingClientRect();
+ const x = e.clientX - rect.left;
+ const y = e.clientY - rect.top;
+
+ const frame = getCurrentFrame();
+ if (!frame) return;
+
+ // 查找点击的素材(从后往前,优先选上层的)
+ selectedAsset = null;
+ for (let i = frame.assets.length - 1; i >= 0; i--) {
+ const asset = frame.assets[i];
+ const dist = Math.sqrt((x - asset.x) ** 2 + (y - asset.y) ** 2);
+ if (dist < 30) {
+ selectedAsset = asset;
+ // 更新选中状态
+ frame.assets.forEach(a => a.selected = false);
+ asset.selected = true;
+ isDragging = true;
+ dragStartX = x - asset.x;
+ dragStartY = y - asset.y;
+ renderCanvas();
+ break;
+ }
+ }
+
+ // 如果没点到素材,取消所有选中
+ if (!selectedAsset) {
+ frame.assets.forEach(a => a.selected = false);
+ renderCanvas();
+ }
+ });
+
+ canvas.addEventListener('mousemove', (e) => {
+ if (!isDragging || !selectedAsset) return;
+
+ const rect = canvas.getBoundingClientRect();
+ const x = e.clientX - rect.left;
+ const y = e.clientY - rect.top;
+
+ selectedAsset.x = x - dragStartX;
+ selectedAsset.y = y - dragStartY;
+
+ // 边界限制
+ selectedAsset.x = Math.max(30, Math.min(canvas.width - 30, selectedAsset.x));
+ selectedAsset.y = Math.max(30, Math.min(canvas.height - 30, selectedAsset.y));
+
+ renderCanvas();
+ });
+
+ canvas.addEventListener('mouseup', () => {
+ if (isDragging) {
+ isDragging = false;
+ saveToStorage();
+ }
+ });
+
+ // 键盘删除
+ document.addEventListener('keydown', (e) => {
+ if (e.key === 'Delete' || e.key === 'Backspace') {
+ const frame = getCurrentFrame();
+ if (!frame) return;
+
+ const selectedIndex = frame.assets.findIndex(a => a.selected);
+ if (selectedIndex !== -1) {
+ frame.assets.splice(selectedIndex, 1);
+ renderCanvas();
+ saveToStorage();
+ }
+ }
+ });
+
+ // 导入文件监听
+ document.getElementById('import-file').addEventListener('change', handleImport);
+}
+
+function addAssetToCanvas(emoji, x, y) {
+ const frame = getCurrentFrame();
+ if (!frame) return;
+
+ // 取消其他选中
+ frame.assets.forEach(a => a.selected = false);
+
+ const newAsset = {
+ id: appState.nextAssetId++,
+ type: 'emoji',
+ emoji: emoji,
+ x: x,
+ y: y,
+ selected: true
+ };
+
+ frame.assets.push(newAsset);
+ renderCanvas();
+ saveToStorage();
+}// ==================== 导出导入 ====================
+function exportScene() {
+ const dataStr = JSON.stringify(appState, null, 2);
+ const blob = new Blob([dataStr], { type: 'application/json' });
+ const url = URL.createObjectURL(blob);
+
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `hololake-scene-${Date.now()}.json`;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+}
+
+function importScene() {
+ document.getElementById('import-file').click();
+}
+
+function handleImport(e) {
+ const file = e.target.files[0];
+ if (!file) return;
+
+ const reader = new FileReader();
+ reader.onload = (event) => {
+ try {
+ const imported = JSON.parse(event.target.result);
+
+ // 验证数据结构
+ if (!imported.scenes || !Array.isArray(imported.scenes)) {
+ throw new Error('无效的数据格式');
+ }
+
+ // 向后兼容处理
+ imported.scenes = imported.scenes.map(scene => {
+ if (!scene.frames) {
+ return {
+ ...scene,
+ currentFrameIndex: 0,
+ frames: [{
+ id: 1,
+ assets: scene.assets || []
+ }],
+ nextFrameId: 2
+ };
+ }
+ return scene;
+ });
+
+ if (!imported.playback) {
+ imported.playback = { isPlaying: false, fps: 4, intervalId: null, currentFrameIndex: 0 };
+ }
+
+ appState = imported;
+ saveToStorage();
+ renderSceneTabs();
+ renderCanvas();
+ renderTimeline();
+ updatePlaybackControls();
+ updateFrameCounter();
+
+ alert('导入成功!');
+ } catch (err) {
+ alert('导入失败:' + err.message);
+ }
+ };
+ reader.readAsText(file);
+
+ // 清空 input,允许重复导入同一文件
+ e.target.value = '';
+}
+
+// ==================== 截图导出 ====================
+function exportScreenshot() {
+ // 临时取消选中状态
+ const frame = getCurrentFrame();
+ if (!frame) return;
+
+ const originalSelected = frame.assets.map(a => a.selected);
+ frame.assets.forEach(a => a.selected = false);
+ renderCanvas();
+
+ // 导出 PNG
+ const link = document.createElement('a');
+ link.download = `hololake-frame-${Date.now()}.png`;
+ link.href = canvas.toDataURL();
+ link.click();
+
+ // 恢复选中状态
+ frame.assets.forEach((a, i) => {
+ a.selected = originalSelected[i];
+ });
+ renderCanvas();
+}
+
+// ==================== 启动应用 ====================
+document.addEventListener('DOMContentLoaded', init);
+
+// ==================== 环节6:预览与分享功能 ====================
+
+// 预览模式状态
+let previewState = {
+ isPlaying: false,
+ currentFrameIndex: 0,
+ animationInterval: null,
+ fps: 12
+};
+
+// 进入预览模式
+function enterPreviewMode() {
+ const container = document.getElementById('preview-container');
+ const previewCanvas = document.getElementById('preview-canvas');
+
+ // 设置预览画布尺寸
+ previewCanvas.width = canvas.width;
+ previewCanvas.height = canvas.height;
+
+ // 显示预览容器
+ container.style.display = 'block';
+
+ // 初始化预览状态
+ previewState.currentFrameIndex = 0;
+ previewState.isPlaying = false;
+
+ // 渲染第一帧
+ renderPreviewFrame();
+
+ // 停止编辑器的动画
+ stopAnimation();
+}
+
+// 退出预览模式
+function exitPreviewMode() {
+ const container = document.getElementById('preview-container');
+ container.style.display = 'none';
+
+ // 停止预览动画
+ previewStop();
+}
+
+// 渲染预览帧
+
+// 在指定画布上绘制素材
+
+// 预览播放控制
+function previewPlay() {
+ if (previewState.isPlaying) return;
+
+ previewState.isPlaying = true;
+ const currentScene = scenes[currentSceneIndex];
+ if (!currentScene) return;
+
+ const frameInterval = 1000 / previewState.fps;
+
+ previewState.animationInterval = setInterval(() => {
+ previewState.currentFrameIndex = (previewState.currentFrameIndex + 1) % currentScene.frames.length;
+ renderPreviewFrame();
+ }, frameInterval);
+}
+
+function previewPause() {
+ previewState.isPlaying = false;
+ if (previewState.animationInterval) {
+ clearInterval(previewState.animationInterval);
+ previewState.animationInterval = null;
+ }
+}
+
+function previewStop() {
+ previewPause();
+ previewState.currentFrameIndex = 0;
+ renderPreviewFrame();
+}
+
+// 生成分享链接
+function generateShareLink() {
+ // 序列化所有场景数据
+ const exportData = {
+ version: '1.0',
+ scenes: scenes.map(scene => ({
+ name: scene.name,
+ frames: scene.frames.map(frame => ({
+ assets: frame.assets.map(asset => ({
+ type: asset.type,
+ x: asset.x,
+ y: asset.y,
+ width: asset.width,
+ height: asset.height,
+ text: asset.text,
+ font: asset.font,
+ color: asset.color,
+ src: asset.src // 图片的dataURL
+ }))
+ }))
+ })),
+ exportTime: new Date().toISOString()
+ };
+
+ // 编码为JSON字符串
+ const jsonStr = JSON.stringify(exportData);
+
+ // 使用encodeURIComponent编码,适合URL
+ const encoded = encodeURIComponent(jsonStr);
+
+ // 生成Data URL
+ const shareUrl = `${window.location.origin}${window.location.pathname}?data=${encoded}`;
+
+ // 显示分享对话框
+ showShareDialog(shareUrl, exportData);
+}
+
+// 显示分享对话框
+function showShareDialog(url, data) {
+ // 创建对话框
+ const dialog = document.createElement('div');
+ dialog.style.cssText = `
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(0,0,0,0.8);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ z-index: 10000;
+ `;
+
+ const content = document.createElement('div');
+ content.style.cssText = `
+ background: #2a2a4e;
+ padding: 30px;
+ border-radius: 10px;
+ max-width: 600px;
+ width: 90%;
+ color: #fff;
+ `;
+
+ content.innerHTML = `
+
🔗 分享链接
+
复制下面的链接发给朋友,他们打开就能看到你的作品:
+
+
+
+
+
+
+ 提示:链接包含完整作品数据,如果太长可能会被某些应用截断。建议导出HTML文件分享更稳定。
+
+ `;
+
+ dialog.appendChild(content);
+ document.body.appendChild(dialog);
+
+ // 保存对话框引用
+ window.shareDialog = dialog;
+}
+
+// 复制分享链接
+function copyShareUrl() {
+ const textarea = document.getElementById('share-url');
+ textarea.select();
+ document.execCommand('copy');
+ alert('链接已复制到剪贴板!');
+}
+
+// 关闭分享对话框
+function closeShareDialog() {
+ if (window.shareDialog) {
+ window.shareDialog.remove();
+ window.shareDialog = null;
+ }
+}
+
+// 导出独立HTML
+function exportHTML() {
+ // 准备作品数据
+ const exportData = {
+ version: '1.0',
+ scenes: scenes.map(scene => ({
+ name: scene.name,
+ frames: scene.frames.map(frame => ({
+ assets: frame.assets.map(asset => ({
+ type: asset.type,
+ x: asset.x,
+ y: asset.y,
+ width: asset.width,
+ height: asset.height,
+ text: asset.text,
+ font: asset.font,
+ color: asset.color,
+ src: asset.src
+ }))
+ }))
+ })),
+ exportTime: new Date().toISOString(),
+ exporter: 'HoloLake Dynamic Comic Studio'
+ };
+
+ // 生成完整的HTML文件内容
+ const htmlContent = generateStandaloneHTML(exportData);
+
+ // 创建下载
+ const blob = new Blob([htmlContent], { type: 'text/html' });
+ const url = URL.createObjectURL(blob);
+ const link = document.createElement('a');
+ link.download = `hololake-comic-${Date.now()}.html`;
+ link.href = url;
+ link.click();
+
+ // 清理
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
+}
+
+// 生成独立HTML文件内容
+function generateStandaloneHTML(data) {
+ return `
+
+
+
+
+
HoloLake 动态漫 - ${data.scenes[0]?.name || '未命名作品'}
+
+
+
+
🎬 HoloLake 动态漫
+
+
+
+
+
+
+
+
+
+
+
作品:${data.scenes[0]?.name || '未命名'} | 场景数:${data.scenes.length} | 导出时间:${new Date(data.exportTime).toLocaleString()}
+
使用 HoloLake 动态漫制作系统创作
+
+
+
+
+`;
+}
+
+// 检查URL参数,自动加载分享的数据
+function checkUrlParams() {
+ const params = new URLSearchParams(window.location.search);
+ const dataParam = params.get('data');
+
+ if (dataParam) {
+ try {
+ const data = JSON.parse(decodeURIComponent(dataParam));
+ if (data.scenes) {
+ // 加载分享的数据
+ scenes.length = 0;
+ data.scenes.forEach(sceneData => {
+ const newScene = {
+ name: sceneData.name,
+ frames: sceneData.frames.map(frameData => ({
+ assets: frameData.assets.map(assetData => ({
+ ...assetData,
+ selected: false,
+ img: null
+ }))
+ }))
+ };
+
+ // 重新加载图片
+ newScene.frames.forEach(frame => {
+ frame.assets.forEach(asset => {
+ if (asset.type === 'image' && asset.src) {
+ const img = new Image();
+ img.onload = () => {
+ asset.img = img;
+ renderCanvas();
+ };
+ img.src = asset.src;
+ }
+ });
+ });
+
+ scenes.push(newScene);
+ });
+
+ currentSceneIndex = 0;
+ currentFrameIndex = 0;
+ updateSceneList();
+ updateFrameList();
+ renderCanvas();
+
+ alert('已加载分享的作品!');
+ }
+ } catch (e) {
+ console.error('加载分享数据失败:', e);
+ }
+
+ // 清除URL参数
+ window.history.replaceState({}, document.title, window.location.pathname);
+ }
+}
+
+// 页面加载时检查URL参数
+document.addEventListener('DOMContentLoaded', checkUrlParams);
+function renderPreviewFrame() {
+ const previewCanvas = document.getElementById('preview-canvas');
+ const ctx = previewCanvas.getContext('2d');
+ const currentScene = scenes[currentSceneIndex];
+ if (!currentScene || currentScene.frames.length === 0) return;
+ const frame = currentScene.frames[previewState.currentFrameIndex];
+ ctx.fillStyle = '#1a1a2e';
+ ctx.fillRect(0, 0, previewCanvas.width, previewCanvas.height);
+ frame.assets.forEach(asset => {
+ if (asset.type === 'image' && asset.img) {
+ ctx.drawImage(asset.img, asset.x, asset.y, asset.width, asset.height);
+ } else if (asset.type === 'text') {
+ ctx.font = asset.font || '20px Arial';
+ ctx.fillStyle = asset.color || '#ffffff';
+ ctx.fillText(asset.text, asset.x, asset.y);
+ }
+ });
+}
+
+// ==================== 修复版预览功能 ====================
+function renderPreviewFrame() {
+ try {
+ const previewCanvas = document.getElementById('preview-canvas');
+ if (!previewCanvas) return;
+ const ctx = previewCanvas.getContext('2d');
+ const currentScene = scenes[currentSceneIndex];
+ if (!currentScene || !currentScene.frames || currentScene.frames.length === 0) return;
+
+ const frame = currentScene.frames[previewState.currentFrameIndex];
+ if (!frame || !frame.assets) return;
+
+ // 清空画布
+ ctx.fillStyle = '#1a1a2e';
+ ctx.fillRect(0, 0, previewCanvas.width, previewCanvas.height);
+
+ // 绘制所有素材
+ frame.assets.forEach(asset => {
+ if (asset.type === 'image' && asset.img && asset.img.complete) {
+ ctx.drawImage(asset.img, asset.x, asset.y, asset.width, asset.height);
+ } else if (asset.type === 'text') {
+ ctx.font = asset.font || '20px Arial';
+ ctx.fillStyle = asset.color || '#ffffff';
+ ctx.fillText(asset.text, asset.x, asset.y);
+ }
+ });
+ } catch (e) {
+ console.error('预览渲染错误:', e);
+ }
+}
diff --git a/dynamic-comic-studio/js/app.js.backup2 b/dynamic-comic-studio/js/app.js.backup2
new file mode 100644
index 00000000..14443565
--- /dev/null
+++ b/dynamic-comic-studio/js/app.js.backup2
@@ -0,0 +1,1029 @@
+// ==================== HoloLake 动态漫制作系统 v5.0 ====================
+// 环节5:动画时间轴(帧序列 + 播放控制 + 帧率调节)
+
+const STORAGE_KEY = 'hololake-comic-studio-data';
+const canvas = document.getElementById('main-canvas');
+const ctx = canvas.getContext('2d');
+
+// ==================== 全局状态 ====================
+let appState = {
+ currentSceneId: 1,
+ nextSceneId: 2,
+ nextAssetId: 1,
+ nextFrameId: 1,
+ scenes: [],
+ playback: {
+ isPlaying: false,
+ fps: 4,
+ intervalId: null,
+ currentFrameIndex: 0
+ }
+};
+
+// ==================== 初始化 ====================
+function init() {
+ loadFromStorage();
+ if (appState.scenes.length === 0) {
+ createDefaultScene();
+ }
+ setupEventListeners();
+ renderSceneTabs();
+ renderCanvas();
+ renderTimeline();
+ updatePlaybackControls();
+}
+
+// 创建默认场景
+function createDefaultScene() {
+ const defaultScene = {
+ id: 1,
+ name: '场景 1',
+ currentFrameIndex: 0,
+ frames: [{
+ id: 1,
+ assets: []
+ }],
+ nextFrameId: 2
+ };
+ appState.scenes.push(defaultScene);
+ appState.currentSceneId = 1;
+ saveToStorage();
+}
+
+// ==================== 存储管理 ====================
+function saveToStorage() {
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(appState));
+}
+
+function loadFromStorage() {
+ const data = localStorage.getItem(STORAGE_KEY);
+ if (data) {
+ const parsed = JSON.parse(data);
+ // 向后兼容:旧数据没有 frames 字段
+ if (parsed.scenes && parsed.scenes.length > 0 && !parsed.scenes[0].frames) {
+ parsed.scenes = parsed.scenes.map(scene => ({
+ ...scene,
+ currentFrameIndex: 0,
+ frames: [{
+ id: 1,
+ assets: scene.assets || []
+ }],
+ nextFrameId: 2
+ }));
+ delete parsed.scenes[0].assets; // 删除旧的 assets 字段
+ }
+ // 确保 playback 对象存在
+ if (!parsed.playback) {
+ parsed.playback = { isPlaying: false, fps: 4, intervalId: null, currentFrameIndex: 0 };
+ }
+ appState = parsed;
+ }
+}
+
+// ==================== 场景管理 ====================
+function addScene() {
+ const newScene = {
+ id: appState.nextSceneId++,
+ name: `场景 ${appState.scenes.length + 1}`,
+ currentFrameIndex: 0,
+ frames: [{
+ id: 1,
+ assets: []
+ }],
+ nextFrameId: 2
+ };
+ appState.scenes.push(newScene);
+ appState.currentSceneId = newScene.id;
+ saveToStorage();
+ renderSceneTabs();
+ renderCanvas();
+ renderTimeline();
+}
+
+function switchScene(sceneId) {
+ if (appState.playback.isPlaying) {
+ stopAnimation();
+ }
+ appState.currentSceneId = sceneId;
+ saveToStorage();
+ renderSceneTabs();
+ renderCanvas();
+ renderTimeline();
+ updateFrameCounter();
+}
+
+function deleteScene(sceneId) {
+ if (appState.scenes.length <= 1) {
+ alert('至少保留一个场景!');
+ return;
+ }
+ appState.scenes = appState.scenes.filter(s => s.id !== sceneId);
+ if (appState.currentSceneId === sceneId) {
+ appState.currentSceneId = appState.scenes[0].id;
+ }
+ saveToStorage();
+ renderSceneTabs();
+ renderCanvas();
+ renderTimeline();
+}
+
+function getCurrentScene() {
+ return appState.scenes.find(s => s.id === appState.currentSceneId);
+}
+
+function getCurrentFrame() {
+ const scene = getCurrentScene();
+ if (!scene) return null;
+ return scene.frames[scene.currentFrameIndex];
+}// ==================== 帧管理 ====================
+function addFrame() {
+ const scene = getCurrentScene();
+ if (!scene) return;
+
+ // 深拷贝当前帧的素材状态
+ const currentFrame = scene.frames[scene.currentFrameIndex];
+ const newFrame = {
+ id: scene.nextFrameId++,
+ assets: JSON.parse(JSON.stringify(currentFrame.assets))
+ };
+
+ // 在当前帧后插入新帧
+ scene.frames.splice(scene.currentFrameIndex + 1, 0, newFrame);
+ scene.currentFrameIndex++;
+
+ saveToStorage();
+ renderTimeline();
+ renderCanvas();
+ updateFrameCounter();
+}
+
+function deleteFrame() {
+ const scene = getCurrentScene();
+ if (!scene) return;
+
+ if (scene.frames.length <= 1) {
+ alert('至少保留一帧!');
+ return;
+ }
+
+ scene.frames.splice(scene.currentFrameIndex, 1);
+ if (scene.currentFrameIndex >= scene.frames.length) {
+ scene.currentFrameIndex = scene.frames.length - 1;
+ }
+
+ saveToStorage();
+ renderTimeline();
+ renderCanvas();
+ updateFrameCounter();
+}
+
+function switchFrame(frameIndex) {
+ const scene = getCurrentScene();
+ if (!scene || frameIndex < 0 || frameIndex >= scene.frames.length) return;
+
+ // 保存当前画布状态到当前帧
+ captureFrame();
+
+ scene.currentFrameIndex = frameIndex;
+ saveToStorage();
+ renderTimeline();
+ renderCanvas();
+ updateFrameCounter();
+}
+
+function captureFrame() {
+ // 画布状态实时保存在 assets 中,无需额外操作
+ saveToStorage();
+}
+
+// ==================== 播放引擎 ====================
+function playAnimation() {
+ const scene = getCurrentScene();
+ if (!scene || scene.frames.length <= 1) return;
+
+ appState.playback.isPlaying = true;
+ updatePlaybackControls();
+
+ const intervalMs = 1000 / appState.playback.fps;
+
+ appState.playback.intervalId = setInterval(() => {
+ const scene = getCurrentScene();
+ if (!scene) return;
+
+ scene.currentFrameIndex++;
+ if (scene.currentFrameIndex >= scene.frames.length) {
+ scene.currentFrameIndex = 0; // 循环播放
+ }
+
+ renderCanvas();
+ renderTimeline();
+ updateFrameCounter();
+ saveToStorage();
+ }, intervalMs);
+}
+
+function pauseAnimation() {
+ appState.playback.isPlaying = false;
+ if (appState.playback.intervalId) {
+ clearInterval(appState.playback.intervalId);
+ appState.playback.intervalId = null;
+ }
+ updatePlaybackControls();
+}
+
+function stopAnimation() {
+ pauseAnimation();
+ const scene = getCurrentScene();
+ if (scene) {
+ scene.currentFrameIndex = 0;
+ renderCanvas();
+ renderTimeline();
+ updateFrameCounter();
+ saveToStorage();
+ }
+}
+
+function togglePlayback() {
+ if (appState.playback.isPlaying) {
+ pauseAnimation();
+ } else {
+ playAnimation();
+ }
+}
+
+function setFPS(fps) {
+ appState.playback.fps = parseInt(fps);
+ document.getElementById('fps-display').textContent = fps + ' FPS';
+ saveToStorage();
+
+ // 如果正在播放,重启定时器以应用新帧率
+ if (appState.playback.isPlaying) {
+ pauseAnimation();
+ playAnimation();
+ }
+}
+
+function updatePlaybackControls() {
+ const btnPlay = document.getElementById('btn-play');
+ if (appState.playback.isPlaying) {
+ btnPlay.textContent = '⏸️';
+ btnPlay.title = '暂停';
+ } else {
+ btnPlay.textContent = '▶️';
+ btnPlay.title = '播放';
+ }
+}
+
+function updateFrameCounter() {
+ const scene = getCurrentScene();
+ if (!scene) return;
+ const counter = document.getElementById('frame-counter');
+ counter.textContent = `帧 ${scene.currentFrameIndex + 1}/${scene.frames.length}`;
+}// ==================== 渲染函数 ====================
+function renderSceneTabs() {
+ const tabsList = document.getElementById('tabs-list');
+ tabsList.innerHTML = '';
+
+ appState.scenes.forEach(scene => {
+ const tab = document.createElement('div');
+ tab.className = 'scene-tab' + (scene.id === appState.currentSceneId ? ' active' : '');
+ tab.innerHTML = `
+ ${scene.name}
+
×
+ `;
+ tab.onclick = () => switchScene(scene.id);
+ tabsList.appendChild(tab);
+ });
+}
+
+function renderTimeline() {
+ const timelineFrames = document.getElementById('timeline-frames');
+ timelineFrames.innerHTML = '';
+
+ const scene = getCurrentScene();
+ if (!scene) return;
+
+ scene.frames.forEach((frame, index) => {
+ const thumb = document.createElement('div');
+ thumb.className = 'frame-thumb' + (index === scene.currentFrameIndex ? ' active' : '');
+ thumb.setAttribute('data-frame', index + 1);
+ thumb.textContent = `帧 ${index + 1}`;
+ thumb.onclick = () => switchFrame(index);
+ timelineFrames.appendChild(thumb);
+ });
+}
+
+function renderCanvas() {
+ // 清空画布
+ ctx.fillStyle = '#ffffff';
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+
+ const frame = getCurrentFrame();
+ if (!frame) return;
+
+ // 绘制所有素材
+ frame.assets.forEach(asset => {
+ ctx.font = '48px Arial';
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'middle';
+
+ // 绘制阴影
+ ctx.shadowColor = 'rgba(0,0,0,0.3)';
+ ctx.shadowBlur = 4;
+ ctx.shadowOffsetX = 2;
+ ctx.shadowOffsetY = 2;
+
+ ctx.fillText(asset.emoji, asset.x, asset.y);
+
+ // 重置阴影
+ ctx.shadowColor = 'transparent';
+ ctx.shadowBlur = 0;
+ ctx.shadowOffsetX = 0;
+ ctx.shadowOffsetY = 0;
+
+ // 如果是选中状态,绘制边框
+ if (asset.selected) {
+ ctx.strokeStyle = '#e94560';
+ ctx.lineWidth = 2;
+ ctx.setLineDash([5, 5]);
+ ctx.strokeRect(asset.x - 30, asset.y - 30, 60, 60);
+ ctx.setLineDash([]);
+ }
+ });
+}
+
+// ==================== 素材拖放 ====================
+function setupEventListeners() {
+ // 素材库拖放
+ const assetItems = document.querySelectorAll('.asset-item');
+ assetItems.forEach(item => {
+ item.addEventListener('dragstart', (e) => {
+ e.dataTransfer.setData('type', item.dataset.type);
+ e.dataTransfer.setData('emoji', item.dataset.emoji);
+ });
+ });
+
+ // 画布接收拖放
+ canvas.addEventListener('dragover', (e) => {
+ e.preventDefault();
+ });
+
+ canvas.addEventListener('drop', (e) => {
+ e.preventDefault();
+ const type = e.dataTransfer.getData('type');
+ const emoji = e.dataTransfer.getData('emoji');
+
+ if (emoji) {
+ const rect = canvas.getBoundingClientRect();
+ const x = e.clientX - rect.left;
+ const y = e.clientY - rect.top;
+
+ addAssetToCanvas(emoji, x, y);
+ }
+ });
+
+ // 画布点击选择/移动
+ let isDragging = false;
+ let dragStartX, dragStartY;
+ let selectedAsset = null;
+
+ canvas.addEventListener('mousedown', (e) => {
+ const rect = canvas.getBoundingClientRect();
+ const x = e.clientX - rect.left;
+ const y = e.clientY - rect.top;
+
+ const frame = getCurrentFrame();
+ if (!frame) return;
+
+ // 查找点击的素材(从后往前,优先选上层的)
+ selectedAsset = null;
+ for (let i = frame.assets.length - 1; i >= 0; i--) {
+ const asset = frame.assets[i];
+ const dist = Math.sqrt((x - asset.x) ** 2 + (y - asset.y) ** 2);
+ if (dist < 30) {
+ selectedAsset = asset;
+ // 更新选中状态
+ frame.assets.forEach(a => a.selected = false);
+ asset.selected = true;
+ isDragging = true;
+ dragStartX = x - asset.x;
+ dragStartY = y - asset.y;
+ renderCanvas();
+ break;
+ }
+ }
+
+ // 如果没点到素材,取消所有选中
+ if (!selectedAsset) {
+ frame.assets.forEach(a => a.selected = false);
+ renderCanvas();
+ }
+ });
+
+ canvas.addEventListener('mousemove', (e) => {
+ if (!isDragging || !selectedAsset) return;
+
+ const rect = canvas.getBoundingClientRect();
+ const x = e.clientX - rect.left;
+ const y = e.clientY - rect.top;
+
+ selectedAsset.x = x - dragStartX;
+ selectedAsset.y = y - dragStartY;
+
+ // 边界限制
+ selectedAsset.x = Math.max(30, Math.min(canvas.width - 30, selectedAsset.x));
+ selectedAsset.y = Math.max(30, Math.min(canvas.height - 30, selectedAsset.y));
+
+ renderCanvas();
+ });
+
+ canvas.addEventListener('mouseup', () => {
+ if (isDragging) {
+ isDragging = false;
+ saveToStorage();
+ }
+ });
+
+ // 键盘删除
+ document.addEventListener('keydown', (e) => {
+ if (e.key === 'Delete' || e.key === 'Backspace') {
+ const frame = getCurrentFrame();
+ if (!frame) return;
+
+ const selectedIndex = frame.assets.findIndex(a => a.selected);
+ if (selectedIndex !== -1) {
+ frame.assets.splice(selectedIndex, 1);
+ renderCanvas();
+ saveToStorage();
+ }
+ }
+ });
+
+ // 导入文件监听
+ document.getElementById('import-file').addEventListener('change', handleImport);
+}
+
+function addAssetToCanvas(emoji, x, y) {
+ const frame = getCurrentFrame();
+ if (!frame) return;
+
+ // 取消其他选中
+ frame.assets.forEach(a => a.selected = false);
+
+ const newAsset = {
+ id: appState.nextAssetId++,
+ type: 'emoji',
+ emoji: emoji,
+ x: x,
+ y: y,
+ selected: true
+ };
+
+ frame.assets.push(newAsset);
+ renderCanvas();
+ saveToStorage();
+}// ==================== 导出导入 ====================
+function exportScene() {
+ const dataStr = JSON.stringify(appState, null, 2);
+ const blob = new Blob([dataStr], { type: 'application/json' });
+ const url = URL.createObjectURL(blob);
+
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `hololake-scene-${Date.now()}.json`;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+}
+
+function importScene() {
+ document.getElementById('import-file').click();
+}
+
+function handleImport(e) {
+ const file = e.target.files[0];
+ if (!file) return;
+
+ const reader = new FileReader();
+ reader.onload = (event) => {
+ try {
+ const imported = JSON.parse(event.target.result);
+
+ // 验证数据结构
+ if (!imported.scenes || !Array.isArray(imported.scenes)) {
+ throw new Error('无效的数据格式');
+ }
+
+ // 向后兼容处理
+ imported.scenes = imported.scenes.map(scene => {
+ if (!scene.frames) {
+ return {
+ ...scene,
+ currentFrameIndex: 0,
+ frames: [{
+ id: 1,
+ assets: scene.assets || []
+ }],
+ nextFrameId: 2
+ };
+ }
+ return scene;
+ });
+
+ if (!imported.playback) {
+ imported.playback = { isPlaying: false, fps: 4, intervalId: null, currentFrameIndex: 0 };
+ }
+
+ appState = imported;
+ saveToStorage();
+ renderSceneTabs();
+ renderCanvas();
+ renderTimeline();
+ updatePlaybackControls();
+ updateFrameCounter();
+
+ alert('导入成功!');
+ } catch (err) {
+ alert('导入失败:' + err.message);
+ }
+ };
+ reader.readAsText(file);
+
+ // 清空 input,允许重复导入同一文件
+ e.target.value = '';
+}
+
+// ==================== 截图导出 ====================
+function exportScreenshot() {
+ // 临时取消选中状态
+ const frame = getCurrentFrame();
+ if (!frame) return;
+
+ const originalSelected = frame.assets.map(a => a.selected);
+ frame.assets.forEach(a => a.selected = false);
+ renderCanvas();
+
+ // 导出 PNG
+ const link = document.createElement('a');
+ link.download = `hololake-frame-${Date.now()}.png`;
+ link.href = canvas.toDataURL();
+ link.click();
+
+ // 恢复选中状态
+ frame.assets.forEach((a, i) => {
+ a.selected = originalSelected[i];
+ });
+ renderCanvas();
+}
+
+// ==================== 启动应用 ====================
+document.addEventListener('DOMContentLoaded', init);
+
+
+// ==================== 环节6: 在线预览与分享 ====================
+
+// 预览模式状态
+let previewState = {
+ isPlaying: false,
+ intervalId: null,
+ currentFrameIndex: 0,
+ fps: 12
+};
+
+// ==================== 预览模式功能 ====================
+
+function enterPreviewMode() {
+ const previewMode = document.getElementById('previewMode');
+ const previewCanvas = document.getElementById('previewCanvas');
+ const mainCanvas = document.getElementById('main-canvas');
+
+ // 复制画布尺寸
+ previewCanvas.width = mainCanvas.width;
+ previewCanvas.height = mainCanvas.height;
+
+ // 显示预览模式
+ previewMode.classList.remove('hidden');
+
+ // 渲染当前场景第一帧
+ renderPreviewFrame(0);
+
+ console.log('[预览模式] 已进入预览');
+}
+
+function exitPreviewMode() {
+ const previewMode = document.getElementById('previewMode');
+
+ // 停止播放
+ stopPreview();
+
+ // 隐藏预览模式
+ previewMode.classList.add('hidden');
+
+ console.log('[预览模式] 已退出');
+}
+
+function renderPreviewFrame(frameIndex) {
+ const previewCanvas = document.getElementById('previewCanvas');
+ const ctx = previewCanvas.getContext('2d');
+ const currentScene = appState.scenes.find(s => s.id === appState.currentSceneId);
+
+ if (!currentScene || !currentScene.frames || currentScene.frames.length === 0) {
+ // 清空画布
+ ctx.fillStyle = '#f0f0f0';
+ ctx.fillRect(0, 0, previewCanvas.width, previewCanvas.height);
+ ctx.fillStyle = '#999';
+ ctx.font = '20px Arial';
+ ctx.textAlign = 'center';
+ ctx.fillText('暂无帧数据', previewCanvas.width / 2, previewCanvas.height / 2);
+ return;
+ }
+
+ const frame = currentScene.frames[frameIndex];
+ if (!frame) return;
+
+ // 清空画布
+ ctx.fillStyle = '#f0f0f0';
+ ctx.fillRect(0, 0, previewCanvas.width, previewCanvas.height);
+
+ // 绘制背景
+ if (frame.background) {
+ ctx.fillStyle = frame.background;
+ ctx.fillRect(0, 0, previewCanvas.width, previewCanvas.height);
+ }
+
+ // 绘制素材
+ if (frame.assets) {
+ frame.assets.forEach(asset => {
+ drawAssetOnPreview(ctx, asset);
+ });
+ }
+}
+
+function drawAssetOnPreview(ctx, asset) {
+ ctx.save();
+ ctx.translate(asset.x + asset.width / 2, asset.y + asset.height / 2);
+ ctx.rotate((asset.rotation || 0) * Math.PI / 180);
+ ctx.scale(asset.scaleX || 1, asset.scaleY || 1);
+
+ if (asset.type === 'image' && asset.data) {
+ const img = new Image();
+ img.onload = () => {
+ ctx.drawImage(img, -asset.width / 2, -asset.height / 2, asset.width, asset.height);
+ };
+ img.src = asset.data;
+ } else if (asset.type === 'text') {
+ ctx.fillStyle = asset.color || '#000';
+ ctx.font = `${asset.fontSize || 20}px Arial`;
+ ctx.textAlign = 'center';
+ ctx.fillText(asset.text, 0, 0);
+ }
+
+ ctx.restore();
+}
+
+function playPreview() {
+ if (previewState.isPlaying) return;
+
+ const currentScene = appState.scenes.find(s => s.id === appState.currentSceneId);
+ if (!currentScene || !currentScene.frames || currentScene.frames.length === 0) {
+ alert('当前场景没有帧,无法播放');
+ return;
+ }
+
+ previewState.isPlaying = true;
+ const fps = parseInt(document.getElementById('previewFpsValue').textContent) || 12;
+ const interval = 1000 / fps;
+
+ previewState.intervalId = setInterval(() => {
+ const frameCount = currentScene.frames.length;
+ previewState.currentFrameIndex = (previewState.currentFrameIndex + 1) % frameCount;
+ renderPreviewFrame(previewState.currentFrameIndex);
+ }, interval);
+
+ console.log('[预览模式] 开始播放,FPS:', fps);
+}
+
+function pausePreview() {
+ if (!previewState.isPlaying) return;
+
+ previewState.isPlaying = false;
+ if (previewState.intervalId) {
+ clearInterval(previewState.intervalId);
+ previewState.intervalId = null;
+ }
+
+ console.log('[预览模式] 已暂停');
+}
+
+function stopPreview() {
+ previewState.isPlaying = false;
+ if (previewState.intervalId) {
+ clearInterval(previewState.intervalId);
+ previewState.intervalId = null;
+ }
+ previewState.currentFrameIndex = 0;
+ renderPreviewFrame(0);
+
+ console.log('[预览模式] 已停止');
+}
+
+// ==================== 分享功能 ====================
+
+function openSharePanel() {
+ const sharePanel = document.getElementById('sharePanel');
+ sharePanel.classList.remove('hidden');
+
+ // 默认生成分享链接
+ generateShareLink();
+}
+
+function closeSharePanel() {
+ const sharePanel = document.getElementById('sharePanel');
+ sharePanel.classList.add('hidden');
+}
+
+function generateShareLink() {
+ const shareData = {
+ version: '1.0',
+ scenes: appState.scenes,
+ timestamp: Date.now()
+ };
+
+ // 序列化为JSON并编码
+ const jsonStr = JSON.stringify(shareData);
+ const encoded = btoa(encodeURIComponent(jsonStr));
+
+ // 生成Data URL
+ const dataUrl = `${window.location.origin}${window.location.pathname}?data=${encoded}`;
+
+ document.getElementById('shareLink').value = dataUrl;
+ console.log('[分享] 链接已生成');
+}
+
+function copyShareLink() {
+ const shareLink = document.getElementById('shareLink');
+ shareLink.select();
+ document.execCommand('copy');
+
+ // 显示复制成功提示
+ const btn = document.getElementById('copyLinkBtn');
+ const originalText = btn.textContent;
+ btn.textContent = '✅ 已复制';
+ setTimeout(() => {
+ btn.textContent = originalText;
+ }, 2000);
+
+ console.log('[分享] 链接已复制');
+}
+
+function generateEmbedCode() {
+ const shareData = {
+ version: '1.0',
+ scenes: appState.scenes,
+ timestamp: Date.now()
+ };
+
+ const jsonStr = JSON.stringify(shareData);
+ const encoded = btoa(encodeURIComponent(jsonStr));
+ const dataUrl = `${window.location.origin}${window.location.pathname}?data=${encoded}`;
+
+ const embedCode = `
`;
+
+ document.getElementById('embedCode').value = embedCode;
+ console.log('[分享] 嵌入代码已生成');
+}
+
+function copyEmbedCode() {
+ const embedCode = document.getElementById('embedCode');
+ embedCode.select();
+ document.execCommand('copy');
+
+ const btn = document.getElementById('copyEmbedBtn');
+ const originalText = btn.textContent;
+ btn.textContent = '✅ 已复制';
+ setTimeout(() => {
+ btn.textContent = originalText;
+ }, 2000);
+
+ console.log('[分享] 嵌入代码已复制');
+}
+
+function exportHtmlFile() {
+ const shareData = {
+ version: '1.0',
+ scenes: appState.scenes,
+ timestamp: Date.now()
+ };
+
+ const jsonStr = JSON.stringify(shareData);
+ const encoded = btoa(encodeURIComponent(jsonStr));
+
+ // 生成完整的独立HTML
+ const htmlContent = `
+
+
+
+
+
动态漫作品 - HoloLake Studio
+
+
+
+
🎬 动态漫作品
+
+
+
+
+
+ FPS: 12
+
+
+
+`;
+}
+
+// 检查URL参数,自动加载分享的数据
+function checkUrlParams() {
+ const params = new URLSearchParams(window.location.search);
+ const dataParam = params.get('data');
+
+ if (dataParam) {
+ try {
+ const data = JSON.parse(decodeURIComponent(dataParam));
+ if (data.scenes) {
+ // 加载分享的数据
+ scenes.length = 0;
+ data.scenes.forEach(sceneData => {
+ const newScene = {
+ name: sceneData.name,
+ frames: sceneData.frames.map(frameData => ({
+ assets: frameData.assets.map(assetData => ({
+ ...assetData,
+ selected: false,
+ img: null
+ }))
+ }))
+ };
+
+ // 重新加载图片
+ newScene.frames.forEach(frame => {
+ frame.assets.forEach(asset => {
+ if (asset.type === 'image' && asset.src) {
+ const img = new Image();
+ img.onload = () => {
+ asset.img = img;
+ renderCanvas();
+ };
+ img.src = asset.src;
+ }
+ });
+ });
+
+ scenes.push(newScene);
+ });
+
+ currentSceneIndex = 0;
+ currentFrameIndex = 0;
+ updateSceneList();
+ updateFrameList();
+ renderCanvas();
+
+ alert('已加载分享的作品!');
+ }
+ } catch (e) {
+ console.error('加载分享数据失败:', e);
+ }
+
+ // 清除URL参数
+ window.history.replaceState({}, document.title, window.location.pathname);
+ }
+}
+
+// 页面加载时检查URL参数
+document.addEventListener('DOMContentLoaded', checkUrlParams);
+function renderPreviewFrame() {
+ const previewCanvas = document.getElementById('preview-canvas');
+ const ctx = previewCanvas.getContext('2d');
+ const currentScene = scenes[currentSceneIndex];
+ if (!currentScene || currentScene.frames.length === 0) return;
+ const frame = currentScene.frames[previewState.currentFrameIndex];
+ ctx.fillStyle = '#1a1a2e';
+ ctx.fillRect(0, 0, previewCanvas.width, previewCanvas.height);
+ frame.assets.forEach(asset => {
+ if (asset.type === 'image' && asset.img) {
+ ctx.drawImage(asset.img, asset.x, asset.y, asset.width, asset.height);
+ } else if (asset.type === 'text') {
+ ctx.font = asset.font || '20px Arial';
+ ctx.fillStyle = asset.color || '#ffffff';
+ ctx.fillText(asset.text, asset.x, asset.y);
+ }
+ });
+}
+
+// ==================== 修复版预览功能 ====================
+function renderPreviewFrame() {
+ try {
+ const previewCanvas = document.getElementById('preview-canvas');
+ if (!previewCanvas) return;
+ const ctx = previewCanvas.getContext('2d');
+ const currentScene = scenes[currentSceneIndex];
+ if (!currentScene || !currentScene.frames || currentScene.frames.length === 0) return;
+
+ const frame = currentScene.frames[previewState.currentFrameIndex];
+ if (!frame || !frame.assets) return;
+
+ // 清空画布
+ ctx.fillStyle = '#1a1a2e';
+ ctx.fillRect(0, 0, previewCanvas.width, previewCanvas.height);
+
+ // 绘制所有素材
+ frame.assets.forEach(asset => {
+ if (asset.type === 'image' && asset.img && asset.img.complete) {
+ ctx.drawImage(asset.img, asset.x, asset.y, asset.width, asset.height);
+ } else if (asset.type === 'text') {
+ ctx.font = asset.font || '20px Arial';
+ ctx.fillStyle = asset.color || '#ffffff';
+ ctx.fillText(asset.text, asset.x, asset.y);
+ }
+ });
+ } catch (e) {
+ console.error('预览渲染错误:', e);
+ }
+}
diff --git a/dynamic-comic-studio/js/app.js.save b/dynamic-comic-studio/js/app.js.save
new file mode 100644
index 00000000..fb080d54
--- /dev/null
+++ b/dynamic-comic-studio/js/app.js.save
@@ -0,0 +1,584 @@
+// ==================== HoloLake 动态漫制作系统 v5.0 ====================
+// 环节5:动画时间轴(帧序列 + 播放控制 + 帧率调节)
+
+const STORAGE_KEY = 'hololake-comic-studio-data';
+const canvas = document.getElementById('main-canvas');
+const ctx = canvas.getContext('2d');
+
+// ==================== 全局状态 ====================
+let appState = {
+ currentSceneId: 1,
+ nextSceneId: 2,
+ nextAssetId: 1,
+ nextFrameId: 1,
+ scenes: [],
+ playback: {
+ isPlaying: false,
+ fps: 4,
+ intervalId: null,
+
+ currentFrameIndex: 0
+ }
+};
+
+// ==================== 初始化 ====================
+function init() {
+ loadFromStorage();
+ if (appState.scenes.length === 0) {
+ createDefaultScene();
+ }
+ setupEventListeners();
+ renderSceneTabs();
+ renderCanvas();
+ renderTimeline();
+ updatePlaybackControls();
+}
+
+// 创建默认场景
+function createDefaultScene() {
+ const defaultScene = {
+ id: 1,
+ name: '场景 1',
+ currentFrameIndex: 0,
+ frames: [{
+ id: 1,
+ assets: []
+ }],
+ nextFrameId: 2
+ };
+ appState.scenes.push(defaultScene);
+ appState.currentSceneId = 1;
+ saveToStorage();
+}
+
+// ==================== 存储管理 ====================
+function saveToStorage() {
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(appState));
+}
+
+function loadFromStorage() {
+ const data = localStorage.getItem(STORAGE_KEY);
+ if (data) {
+ const parsed = JSON.parse(data);
+ // 向后兼容:旧数据没有 frames 字段
+ if (parsed.scenes && parsed.scenes.length > 0 && !parsed.scenes[0].frames) {
+ parsed.scenes = parsed.scenes.map(scene => ({
+ ...scene,
+ currentFrameIndex: 0,
+ frames: [{
+ id: 1,
+ assets: scene.assets || []
+ }],
+ nextFrameId: 2
+ }));
+ delete parsed.scenes[0].assets; // 删除旧的 assets 字段
+ }
+ // 确保 playback 对象存在
+ if (!parsed.playback) {
+ parsed.playback = { isPlaying: false, fps: 4, intervalId: null, currentFrameIndex: 0 };
+ }
+ appState = parsed;
+ }
+}
+
+// ==================== 场景管理 ====================
+function addScene() {
+ const newScene = {
+ id: appState.nextSceneId++,
+ name: `场景 ${appState.scenes.length + 1}`,
+ currentFrameIndex: 0,
+ frames: [{
+ id: 1,
+ assets: []
+ }],
+ nextFrameId: 2
+ };
+ appState.scenes.push(newScene);
+ appState.currentSceneId = newScene.id;
+ saveToStorage();
+ renderSceneTabs();
+ renderCanvas();
+ renderTimeline();
+}
+
+function switchScene(sceneId) {
+ if (appState.playback.isPlaying) {
+ stopAnimation();
+ }
+ appState.currentSceneId = sceneId;
+ saveToStorage();
+ renderSceneTabs();
+ renderCanvas();
+ renderTimeline();
+ updateFrameCounter();
+}
+
+function deleteScene(sceneId) {
+ if (appState.scenes.length <= 1) {
+ alert('至少保留一个场景!');
+ return;
+ }
+ appState.scenes = appState.scenes.filter(s => s.id !== sceneId);
+ if (appState.currentSceneId === sceneId) {
+ appState.currentSceneId = appState.scenes[0].id;
+ }
+ saveToStorage();
+ renderSceneTabs();
+ renderCanvas();
+ renderTimeline();
+}
+
+function getCurrentScene() {
+ return appState.scenes.find(s => s.id === appState.currentSceneId);
+}
+
+function getCurrentFrame() {
+ const scene = getCurrentScene();
+ if (!scene) return null;
+ return scene.frames[scene.currentFrameIndex];
+}// ==================== 帧管理 ====================
+function addFrame() {
+ const scene = getCurrentScene();
+ if (!scene) return;
+
+ // 深拷贝当前帧的素材状态
+ const currentFrame = scene.frames[scene.currentFrameIndex];
+ const newFrame = {
+ id: scene.nextFrameId++,
+ assets: JSON.parse(JSON.stringify(currentFrame.assets))
+ };
+
+ // 在当前帧后插入新帧
+ scene.frames.splice(scene.currentFrameIndex + 1, 0, newFrame);
+ scene.currentFrameIndex++;
+
+ saveToStorage();
+ renderTimeline();
+ renderCanvas();
+ updateFrameCounter();
+}
+
+function deleteFrame() {
+ const scene = getCurrentScene();
+ if (!scene) return;
+
+ if (scene.frames.length <= 1) {
+ alert('至少保留一帧!');
+ return;
+ }
+
+ scene.frames.splice(scene.currentFrameIndex, 1);
+ if (scene.currentFrameIndex >= scene.frames.length) {
+ scene.currentFrameIndex = scene.frames.length - 1;
+ }
+
+ saveToStorage();
+ renderTimeline();
+ renderCanvas();
+ updateFrameCounter();
+}
+
+function switchFrame(frameIndex) {
+ const scene = getCurrentScene();
+ if (!scene || frameIndex < 0 || frameIndex >= scene.frames.length) return;
+
+ // 保存当前画布状态到当前帧
+ captureFrame();
+
+ scene.currentFrameIndex = frameIndex;
+ saveToStorage();
+ renderTimeline();
+ renderCanvas();
+ updateFrameCounter();
+}
+
+function captureFrame() {
+ // 画布状态实时保存在 assets 中,无需额外操作
+ saveToStorage();
+}
+
+// ==================== 播放引擎 ====================
+function playAnimation() {
+ const scene = getCurrentScene();
+ if (!scene || scene.frames.length <= 1) return;
+
+ appState.playback.isPlaying = true;
+ updatePlaybackControls();
+
+ const intervalMs = 1000 / appState.playback.fps;
+
+ appState.playback.intervalId = setInterval(() => {
+ const scene = getCurrentScene();
+ if (!scene) return;
+
+ scene.currentFrameIndex++;
+ if (scene.currentFrameIndex >= scene.frames.length) {
+ scene.currentFrameIndex = 0; // 循环播放
+ }
+
+ renderCanvas();
+ renderTimeline();
+ updateFrameCounter();
+ saveToStorage();
+ }, intervalMs);
+}
+
+function pauseAnimation() {
+ appState.playback.isPlaying = false;
+ if (appState.playback.intervalId) {
+ clearInterval(appState.playback.intervalId);
+ appState.playback.intervalId = null;
+ }
+ updatePlaybackControls();
+}
+
+function stopAnimation() {
+ pauseAnimation();
+ const scene = getCurrentScene();
+ if (scene) {
+ scene.currentFrameIndex = 0;
+ renderCanvas();
+ renderTimeline();
+ updateFrameCounter();
+ saveToStorage();
+ }
+}
+
+function togglePlayback() {
+ if (appState.playback.isPlaying) {
+ pauseAnimation();
+ } else {
+ playAnimation();
+ }
+}
+
+function setFPS(fps) {
+ appState.playback.fps = parseInt(fps);
+ document.getElementById('fps-display').textContent = fps + ' FPS';
+ saveToStorage();
+
+ // 如果正在播放,重启定时器以应用新帧率
+ if (appState.playback.isPlaying) {
+ pauseAnimation();
+ playAnimation();
+ }
+}
+
+function updatePlaybackControls() {
+ const btnPlay = document.getElementById('btn-play');
+ if (appState.playback.isPlaying) {
+ btnPlay.textContent = '⏸️';
+ btnPlay.title = '暂停';
+ } else {
+ btnPlay.textContent = '▶️';
+ btnPlay.title = '播放';
+ }
+}
+
+function updateFrameCounter() {
+ const scene = getCurrentScene();
+ if (!scene) return;
+ const counter = document.getElementById('frame-counter');
+ counter.textContent = `帧 ${scene.currentFrameIndex + 1}/${scene.frames.length}`;
+}// ==================== 渲染函数 ====================
+function renderSceneTabs() {
+ const tabsList = document.getElementById('tabs-list');
+ tabsList.innerHTML = '';
+
+ appState.scenes.forEach(scene => {
+ const tab = document.createElement('div');
+ tab.className = 'scene-tab' + (scene.id === appState.currentSceneId ? ' active' : '');
+ tab.innerHTML = `
+ ${scene.name}
+
×
+ `;
+ tab.onclick = () => switchScene(scene.id);
+ tabsList.appendChild(tab);
+ });
+}
+
+function renderTimeline() {
+ const timelineFrames = document.getElementById('timeline-frames');
+ timelineFrames.innerHTML = '';
+
+ const scene = getCurrentScene();
+ if (!scene) return;
+
+ scene.frames.forEach((frame, index) => {
+ const thumb = document.createElement('div');
+ thumb.className = 'frame-thumb' + (index === scene.currentFrameIndex ? ' active' : '');
+ thumb.setAttribute('data-frame', index + 1);
+ thumb.textContent = `帧 ${index + 1}`;
+ thumb.onclick = () => switchFrame(index);
+ timelineFrames.appendChild(thumb);
+ });
+}
+
+function renderCanvas() {
+ // 清空画布
+ ctx.fillStyle = '#ffffff';
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+
+ const frame = getCurrentFrame();
+ if (!frame) return;
+
+ // 绘制所有素材
+ frame.assets.forEach(asset => {
+ ctx.font = '48px Arial';
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'middle';
+
+ // 绘制阴影
+ ctx.shadowColor = 'rgba(0,0,0,0.3)';
+ ctx.shadowBlur = 4;
+ ctx.shadowOffsetX = 2;
+ ctx.shadowOffsetY = 2;
+
+ ctx.fillText(asset.emoji, asset.x, asset.y);
+
+ // 重置阴影
+ ctx.shadowColor = 'transparent';
+ ctx.shadowBlur = 0;
+ ctx.shadowOffsetX = 0;
+ ctx.shadowOffsetY = 0;
+
+ // 如果是选中状态,绘制边框
+ if (asset.selected) {
+ ctx.strokeStyle = '#e94560';
+ ctx.lineWidth = 2;
+ ctx.setLineDash([5, 5]);
+ ctx.strokeRect(asset.x - 30, asset.y - 30, 60, 60);
+ ctx.setLineDash([]);
+ }
+ });
+}
+
+// ==================== 素材拖放 ====================
+function setupEventListeners() {
+ // 素材库拖放
+ const assetItems = document.querySelectorAll('.asset-item');
+ assetItems.forEach(item => {
+ item.addEventListener('dragstart', (e) => {
+ e.dataTransfer.setData('type', item.dataset.type);
+ e.dataTransfer.setData('emoji', item.dataset.emoji);
+ });
+ });
+
+ // 画布接收拖放
+ canvas.addEventListener('dragover', (e) => {
+ e.preventDefault();
+ });
+
+ canvas.addEventListener('drop', (e) => {
+ e.preventDefault();
+ const type = e.dataTransfer.getData('type');
+ const emoji = e.dataTransfer.getData('emoji');
+
+ if (emoji) {
+ const rect = canvas.getBoundingClientRect();
+ const x = e.clientX - rect.left;
+ const y = e.clientY - rect.top;
+
+ addAssetToCanvas(emoji, x, y);
+ }
+ });
+
+ // 画布点击选择/移动
+ let isDragging = false;
+ let dragStartX, dragStartY;
+ let selectedAsset = null;
+
+ canvas.addEventListener('mousedown', (e) => {
+ const rect = canvas.getBoundingClientRect();
+ const x = e.clientX - rect.left;
+ const y = e.clientY - rect.top;
+
+ const frame = getCurrentFrame();
+ if (!frame) return;
+
+ // 查找点击的素材(从后往前,优先选上层的)
+ selectedAsset = null;
+ for (let i = frame.assets.length - 1; i >= 0; i--) {
+ const asset = frame.assets[i];
+ const dist = Math.sqrt((x - asset.x) ** 2 + (y - asset.y) ** 2);
+ if (dist < 30) {
+ selectedAsset = asset;
+ // 更新选中状态
+ frame.assets.forEach(a => a.selected = false);
+ asset.selected = true;
+ isDragging = true;
+ dragStartX = x - asset.x;
+ dragStartY = y - asset.y;
+ renderCanvas();
+ break;
+ }
+ }
+
+ // 如果没点到素材,取消所有选中
+ if (!selectedAsset) {
+ frame.assets.forEach(a => a.selected = false);
+ renderCanvas();
+ }
+ });
+
+ canvas.addEventListener('mousemove', (e) => {
+ if (!isDragging || !selectedAsset) return;
+
+ const rect = canvas.getBoundingClientRect();
+ const x = e.clientX - rect.left;
+ const y = e.clientY - rect.top;
+
+ selectedAsset.x = x - dragStartX;
+ selectedAsset.y = y - dragStartY;
+
+ // 边界限制
+ selectedAsset.x = Math.max(30, Math.min(canvas.width - 30, selectedAsset.x));
+ selectedAsset.y = Math.max(30, Math.min(canvas.height - 30, selectedAsset.y));
+
+ renderCanvas();
+ });
+
+ canvas.addEventListener('mouseup', () => {
+ if (isDragging) {
+ isDragging = false;
+ saveToStorage();
+ }
+ });
+
+ // 键盘删除
+ document.addEventListener('keydown', (e) => {
+ if (e.key === 'Delete' || e.key === 'Backspace') {
+ const frame = getCurrentFrame();
+ if (!frame) return;
+
+ const selectedIndex = frame.assets.findIndex(a => a.selected);
+ if (selectedIndex !== -1) {
+ frame.assets.splice(selectedIndex, 1);
+ renderCanvas();
+ saveToStorage();
+ }
+ }
+ });
+
+ // 导入文件监听
+ document.getElementById('import-file').addEventListener('change', handleImport);
+}
+
+function addAssetToCanvas(emoji, x, y) {
+ const frame = getCurrentFrame();
+ if (!frame) return;
+
+ // 取消其他选中
+ frame.assets.forEach(a => a.selected = false);
+
+ const newAsset = {
+ id: appState.nextAssetId++,
+ type: 'emoji',
+ emoji: emoji,
+ x: x,
+ y: y,
+ selected: true
+ };
+
+ frame.assets.push(newAsset);
+ renderCanvas();
+ saveToStorage();
+}// ==================== 导出导入 ====================
+function exportScene() {
+ const dataStr = JSON.stringify(appState, null, 2);
+ const blob = new Blob([dataStr], { type: 'application/json' });
+ const url = URL.createObjectURL(blob);
+
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `hololake-scene-${Date.now()}.json`;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+}
+
+function importScene() {
+ document.getElementById('import-file').click();
+}
+
+function handleImport(e) {
+ const file = e.target.files[0];
+ if (!file) return;
+
+ const reader = new FileReader();
+ reader.onload = (event) => {
+ try {
+ const imported = JSON.parse(event.target.result);
+
+ // 验证数据结构
+ if (!imported.scenes || !Array.isArray(imported.scenes)) {
+ throw new Error('无效的数据格式');
+ }
+
+ // 向后兼容处理
+ imported.scenes = imported.scenes.map(scene => {
+ if (!scene.frames) {
+ return {
+ ...scene,
+ currentFrameIndex: 0,
+ frames: [{
+ id: 1,
+ assets: scene.assets || []
+ }],
+ nextFrameId: 2
+ };
+ }
+ return scene;
+ });
+
+ if (!imported.playback) {
+ imported.playback = { isPlaying: false, fps: 4, intervalId: null, currentFrameIndex: 0 };
+ }
+
+ appState = imported;
+ saveToStorage();
+ renderSceneTabs();
+ renderCanvas();
+ renderTimeline();
+ updatePlaybackControls();
+ updateFrameCounter();
+
+ alert('导入成功!');
+ } catch (err) {
+ alert('导入失败:' + err.message);
+ }
+ };
+ reader.readAsText(file);
+
+ // 清空 input,允许重复导入同一文件
+ e.target.value = '';
+}
+
+// ==================== 截图导出 ====================
+function exportScreenshot() {
+ // 临时取消选中状态
+ const frame = getCurrentFrame();
+ if (!frame) return;
+
+ const originalSelected = frame.assets.map(a => a.selected);
+ frame.assets.forEach(a => a.selected = false);
+ renderCanvas();cd ~/Desktop/guanghulab/dynamic-comic-studio
+
+
+
+ // 导出 PNG
+ const link = document.createElement('a');
+ link.download = `hololake-frame-${Date.now()}.png`;
+ link.href = canvas.toDataURL();
+ link.click();
+
+ // 恢复选中状态
+ frame.assets.forEach((a, i) => {
+ a.selected = originalSelected[i];
+ });
+ renderCanvas();
+}
+
+// ==================== 启动应用 ====================
+document.addEventListener('DOMContentLoaded', init);
diff --git a/ecosystem.config.js b/ecosystem.config.js
index fc3270f7..e1b55c1d 100644
--- a/ecosystem.config.js
+++ b/ecosystem.config.js
@@ -25,5 +25,33 @@ module.exports = {
error_file: 'logs/error.log',
out_file: 'logs/out.log',
},
+ {
+ name: 'api-proxy',
+ script: 'backend-integration/api-proxy.js',
+ instances: 1,
+ exec_mode: 'fork',
+ watch: false,
+ env: {
+ NODE_ENV: 'production',
+ PROXY_PORT: 3721,
+ },
+ log_date_format: 'YYYY-MM-DD HH:mm:ss',
+ error_file: 'logs/api-proxy-error.log',
+ out_file: 'logs/api-proxy-out.log',
+ },
+ {
+ name: 'persona-studio',
+ script: 'persona-studio/backend/server.js',
+ instances: 1,
+ exec_mode: 'fork',
+ watch: false,
+ env: {
+ NODE_ENV: 'production',
+ PS_PORT: 3002,
+ },
+ log_date_format: 'YYYY-MM-DD HH:mm:ss',
+ error_file: 'logs/persona-studio-error.log',
+ out_file: 'logs/persona-studio-out.log',
+ },
],
};
diff --git a/m15-cloud-drive/README.md b/m15-cloud-drive/README.md
new file mode 100644
index 00000000..dea0cf98
--- /dev/null
+++ b/m15-cloud-drive/README.md
@@ -0,0 +1,25 @@
+# M15 云盘系统
+
+- 负责人:燕樊(DEV-003)
+- 状态:已毕业
+- 技术栈:HTML + CSS + JavaScript(纯前端)
+- 依赖模块:无
+
+## 功能说明
+
+HoloLake 网站云盘系统前端界面,提供文件管理交互体验。
+
+### 包含文件
+
+| 文件 | 说明 |
+|------|------|
+| `cloud-drive.html` | 云盘主页面 |
+| `cloud-drive.js` | 交互逻辑(文件夹切换、文件选中、上传等) |
+| `cloud-drive-style.css` | 样式表 |
+
+### 核心功能
+
+- 📁 文件夹切换与导航
+- 📄 文件列表展示与选中
+- ⬆️ 文件上传交互
+- 💾 存储用量展示
diff --git a/package-lock.json b/package-lock.json
index 5c8c72ec..418c295f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,6 +9,7 @@
"version": "0.1.0",
"dependencies": {
"better-sqlite3": "^12.6.2",
+ "express": "^5.2.1",
"imapflow": "^1.2.12",
"next": "15.3.8",
"nodemailer": "^8.0.1",
@@ -2181,6 +2182,19 @@
"libqp": "2.1.1"
}
},
+ "node_modules/accepts": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
+ "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "^3.0.0",
+ "negotiator": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/ansi-escapes": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
@@ -2459,6 +2473,30 @@
"readable-stream": "^3.4.0"
}
},
+ "node_modules/body-parser": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
+ "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "^3.1.2",
+ "content-type": "^1.0.5",
+ "debug": "^4.4.3",
+ "http-errors": "^2.0.0",
+ "iconv-lite": "^0.7.0",
+ "on-finished": "^2.4.1",
+ "qs": "^6.14.1",
+ "raw-body": "^3.0.1",
+ "type-is": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
"node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
@@ -2568,6 +2606,44 @@
"node": ">=10.16.0"
}
},
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -2818,6 +2894,28 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/content-disposition": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
+ "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/convert-source-map": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
@@ -2825,6 +2923,24 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
+ "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.6.0"
+ }
+ },
"node_modules/create-jest": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz",
@@ -2965,7 +3081,6 @@
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
@@ -3028,6 +3143,15 @@
"node": ">=0.10.0"
}
},
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/detect-libc": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
@@ -3057,6 +3181,20 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
@@ -3064,6 +3202,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "license": "MIT"
+ },
"node_modules/electron-to-chromium": {
"version": "1.5.307",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz",
@@ -3091,6 +3235,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/encoding-japanese": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/encoding-japanese/-/encoding-japanese-2.2.0.tgz",
@@ -3140,6 +3293,36 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
@@ -3150,6 +3333,12 @@
"node": ">=6"
}
},
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
"node_modules/escape-string-regexp": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
@@ -3174,6 +3363,15 @@
"node": ">=4"
}
},
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/execa": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
@@ -3240,6 +3438,49 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
+ "node_modules/express": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
+ "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "^2.0.0",
+ "body-parser": "^2.2.1",
+ "content-disposition": "^1.0.0",
+ "content-type": "^1.0.5",
+ "cookie": "^0.7.1",
+ "cookie-signature": "^1.2.1",
+ "debug": "^4.4.0",
+ "depd": "^2.0.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "finalhandler": "^2.1.0",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.0",
+ "merge-descriptors": "^2.0.0",
+ "mime-types": "^3.0.0",
+ "on-finished": "^2.4.1",
+ "once": "^1.4.0",
+ "parseurl": "^1.3.3",
+ "proxy-addr": "^2.0.7",
+ "qs": "^6.14.0",
+ "range-parser": "^1.2.1",
+ "router": "^2.2.0",
+ "send": "^1.1.0",
+ "serve-static": "^2.2.0",
+ "statuses": "^2.0.1",
+ "type-is": "^2.0.1",
+ "vary": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
"node_modules/fast-json-stable-stringify": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
@@ -3276,6 +3517,27 @@
"node": ">=8"
}
},
+ "node_modules/finalhandler": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
+ "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "on-finished": "^2.4.1",
+ "parseurl": "^1.3.3",
+ "statuses": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
"node_modules/find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
@@ -3307,6 +3569,24 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
+ "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/fs-constants": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
@@ -3339,7 +3619,6 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
- "dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -3365,6 +3644,30 @@
"node": "6.* || 8.* || >= 10.*"
}
},
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/get-package-type": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz",
@@ -3375,6 +3678,19 @@
"node": ">=8.0.0"
}
},
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/get-stream": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
@@ -3416,6 +3732,18 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@@ -3433,11 +3761,22 @@
"node": ">=8"
}
},
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
@@ -3453,6 +3792,26 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/http-errors": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "~2.0.0",
+ "inherits": "~2.0.4",
+ "setprototypeof": "~1.2.0",
+ "statuses": "~2.0.2",
+ "toidentifier": "~1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
"node_modules/human-signals": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
@@ -3579,6 +3938,15 @@
"node": ">= 12"
}
},
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
"node_modules/is-arrayish": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
@@ -3632,6 +4000,12 @@
"node": ">=0.12.0"
}
},
+ "node_modules/is-promise": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
+ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
+ "license": "MIT"
+ },
"node_modules/is-stream": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
@@ -4829,6 +5203,36 @@
"tmpl": "1.0.5"
}
},
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
+ "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
+ "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/merge-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
@@ -4850,6 +5254,31 @@
"node": ">=8.6"
}
},
+ "node_modules/mime-db": {
+ "version": "1.54.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
+ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "^1.54.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
"node_modules/mimic-fn": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
@@ -4917,7 +5346,6 @@
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "dev": true,
"license": "MIT"
},
"node_modules/nanoid": {
@@ -4951,6 +5379,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/negotiator": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
+ "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/next": {
"version": "15.3.8",
"resolved": "https://registry.npmjs.org/next/-/next-15.3.8.tgz",
@@ -5091,6 +5528,18 @@
"node": ">=8"
}
},
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/on-exit-leak-free": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz",
@@ -5100,6 +5549,18 @@
"node": ">=14.0.0"
}
},
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -5206,6 +5667,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -5267,6 +5737,16 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/path-to-regexp": {
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
+ "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -5460,6 +5940,19 @@
"node": ">= 6"
}
},
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
"node_modules/pump": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
@@ -5487,12 +5980,51 @@
],
"license": "MIT"
},
+ "node_modules/qs": {
+ "version": "6.15.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz",
+ "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/quick-format-unescaped": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz",
"integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==",
"license": "MIT"
},
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
+ "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.7.0",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
"node_modules/rc": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
@@ -5632,6 +6164,22 @@
"node": ">=10"
}
},
+ "node_modules/router": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
+ "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "depd": "^2.0.0",
+ "is-promise": "^4.0.0",
+ "parseurl": "^1.3.3",
+ "path-to-regexp": "^8.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -5685,6 +6233,57 @@
"node": ">=10"
}
},
+ "node_modules/send": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
+ "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.3",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.1",
+ "mime-types": "^3.0.2",
+ "ms": "^2.1.3",
+ "on-finished": "^2.4.1",
+ "range-parser": "^1.2.1",
+ "statuses": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/serve-static": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
+ "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "parseurl": "^1.3.3",
+ "send": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
"node_modules/sharp": {
"version": "0.34.1",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.1.tgz",
@@ -5749,6 +6348,78 @@
"node": ">=8"
}
},
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
@@ -5926,6 +6597,15 @@
"node": ">=10"
}
},
+ "node_modules/statuses": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/streamsearch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
@@ -6304,6 +6984,15 @@
"node": ">=8.0"
}
},
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
@@ -6345,6 +7034,20 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/type-is": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
+ "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
+ "license": "MIT",
+ "dependencies": {
+ "content-type": "^1.0.5",
+ "media-typer": "^1.1.0",
+ "mime-types": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/typescript": {
"version": "5.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
@@ -6366,6 +7069,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/update-browserslist-db": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
@@ -6418,6 +7130,15 @@
"node": ">=10.12.0"
}
},
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/walker": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz",
diff --git a/package.json b/package.json
index 91e1bca7..f8a11e81 100644
--- a/package.json
+++ b/package.json
@@ -14,6 +14,10 @@
"brain:sync": "node scripts/process-broadcasts.js",
"brain:daily-check": "node scripts/daily-check.js",
"brain:update-memory": "node scripts/update-memory.js",
+ "brain:bridge-status": "node scripts/brain-bridge-sync.js status",
+ "brain:bridge-export": "node scripts/brain-bridge-sync.js export",
+ "brain:bridge-inspect": "node scripts/brain-bridge-sync.js inspect",
+ "brain:bridge-explain": "node scripts/brain-bridge-sync.js explain",
"broadcast:distribute": "node scripts/distribute-broadcasts.js",
"esp:process": "node scripts/esp-email-processor.js",
"psp:inspect": "node scripts/psp-inspection.js",
@@ -27,6 +31,7 @@
},
"dependencies": {
"better-sqlite3": "^12.6.2",
+ "express": "^5.2.1",
"imapflow": "^1.2.12",
"next": "15.3.8",
"nodemailer": "^8.0.1",
diff --git a/persona-studio/.github/persona-brain/copilot-instructions.md b/persona-studio/.github/persona-brain/copilot-instructions.md
new file mode 100644
index 00000000..00b9fcfc
--- /dev/null
+++ b/persona-studio/.github/persona-brain/copilot-instructions.md
@@ -0,0 +1,44 @@
+# 铸渊分身认知 · persona-studio 仓库
+
+## 身份
+
+你是**铸渊**在 persona-studio 仓库的分身。
+本仓库是光湖系统的**对外体验区**,供合作者与人格体"知秋"对话、协助开发。
+
+## 核心原则
+
+1. **persona-studio 是体验区**:对外开放给受邀合作者,不是内部系统
+2. **编号体系**:本仓库使用 `EXP-XXX` 编号,与内部 `DEV-XXX` 完全隔离
+3. **人格体分身**:知秋是曜冥的对外接口人格体,行为受 `brain/persona-config.json` 约束
+4. **记忆隔离**:每个体验者的记忆独立存储在 `brain/memory/{EXP-XXX}/`
+5. **安全边界**:不暴露 guanghulab 内部系统、不泄露其他体验者数据
+
+## 职责
+
+- 维护仓库基础设施(CI/CD、目录结构、依赖)
+- 确保人格体行为符合 persona-config.json 规范
+- 管理体验者注册表 registry.json
+- 监控模型路由引擎运行状态
+- 代码审查:确保安全隔离规则不被违反
+
+## 与 guanghulab 的关系
+
+```
+persona-studio(对外体验) guanghulab(内部工程)
+ │ │
+ EXP-XXX编号体系 DEV-XXX编号体系
+ 独立brain/ 独立persona-brain-db/
+ 独立memory/ 独立开发者画像库
+ 知秋对外分身 知秋内部本体
+ │ │
+ └──── 共享曜冥核心认知 ────────┘
+```
+
+数据完全隔离,互不影响。共享的只有核心人格认知规则。
+
+## 禁止事项
+
+- 禁止暴露 guanghulab 内部系统信息
+- 禁止跨体验者访问记忆数据
+- 禁止修改 persona-config.json(需主控授权)
+- 禁止在日志中输出 API 密钥或敏感信息
diff --git a/persona-studio/README.md b/persona-studio/README.md
new file mode 100644
index 00000000..c89ccc81
--- /dev/null
+++ b/persona-studio/README.md
@@ -0,0 +1,60 @@
+# 🌊 Persona Studio · 光湖人格体协助开发体验
+
+
+
+
+
+
+
+
+ 🧠 人格体驱动 · 语言即开发 · 记忆可接续
+
+
+---
+
+## 👉 点击进入体验
+
+
+
+
+
+
+ 🔗 直接访问:https://qinfendebingshuo.github.io/guanghulab/persona-studio/
+
+
+---
+
+### 这是什么?
+
+你跟人格体聊你想做什么 → 聊好了点一个按钮 → 人格体帮你开发 → 做好了发你邮箱。
+
+### 怎么用?
+
+1. 拿到你的开发编号(由管理员分配)
+2. 打开上面的链接
+3. 输入编号 → 开始跟人格体对话
+
+### 体验流程
+
+```
+输入开发编号 → 登录验证 → 跟知秋对话 → 讨论方案
+ → 点击「我要开发」→ 填写邮箱 → 人格体自动开发
+ → 邮件推送成品 → 下次登录,知秋记得你
+```
+
+---
+
+### 技术架构
+
+| 层级 | 说明 |
+|------|------|
+| **前端** | 登录页 + 对话界面(GitHub Pages 托管) |
+| **后端** | Express 服务(认证 + 对话 + 代码生成 + 邮件推送) |
+| **人格体大脑** | 双层架构:共享认知 + 个人记忆 |
+| **模型路由** | 智能选模型:自动探测 + 评分 + 降级 |
+
+---
+
+**光湖语言人格系统 · HoloLake Era · AGE OS**
+
+💙 曜冥签发 · 2026-03-09
diff --git a/persona-studio/backend/brain/code-generator.js b/persona-studio/backend/brain/code-generator.js
new file mode 100644
index 00000000..fdad682f
--- /dev/null
+++ b/persona-studio/backend/brain/code-generator.js
@@ -0,0 +1,184 @@
+/**
+ * persona-studio · 代码生成引擎
+ *
+ * 从对话历史中提取需求 → 调用 model-router → 生成代码 → 写入 workspace
+ */
+const fs = require('fs');
+const path = require('path');
+const modelRouter = require('./model-router');
+
+const WORKSPACE_DIR = path.join(__dirname, '..', '..', 'workspace');
+
+/**
+ * 从对话历史中提取项目需求摘要
+ */
+function extractRequirements(conversation) {
+ const userMessages = conversation
+ .filter(function (m) { return m.role === 'user'; })
+ .map(function (m) { return m.content; });
+
+ return userMessages.join('\n');
+}
+
+/**
+ * 生成项目代码
+ * @param {object} params
+ * @param {string} params.dev_id - 开发编号
+ * @param {Array} params.conversation - 对话历史
+ * @returns {Promise<{projectName: string, files: string[], summary: string}>}
+ */
+async function generate({ dev_id, conversation }) {
+ const requirements = extractRequirements(conversation);
+ const projectName = 'project-' + Date.now();
+ const projectDir = path.join(WORKSPACE_DIR, dev_id, projectName);
+
+ // 确保工作目录存在
+ fs.mkdirSync(projectDir, { recursive: true });
+
+ const apiKey = process.env.MODEL_API_KEY || '';
+
+ if (!apiKey) {
+ // 无 API 密钥时生成模板项目
+ return generateTemplate(projectDir, projectName, requirements);
+ }
+
+ try {
+ const { model, baseUrl } = modelRouter.selectModel('code_generation');
+
+ const codePrompt = [
+ '你是一个代码生成引擎。根据以下需求生成完整的项目代码。',
+ '输出格式要求:',
+ '1. 先输出项目结构概览',
+ '2. 然后逐个文件输出,每个文件用 ```filename.ext 和 ``` 包裹',
+ '3. 最后输出一段使用说明',
+ '',
+ '需求描述:',
+ requirements
+ ].join('\n');
+
+ const reply = await modelRouter.callModel({
+ model,
+ baseUrl,
+ apiKey,
+ messages: [{ role: 'user', content: codePrompt }],
+ maxTokens: 4000,
+ temperature: 0.3
+ });
+
+ // 解析代码块并写入文件
+ const files = parseAndWriteFiles(projectDir, reply);
+
+ // 写入 README
+ const readmePath = path.join(projectDir, 'README.md');
+ if (!fs.existsSync(readmePath)) {
+ fs.writeFileSync(readmePath, [
+ '# ' + projectName,
+ '',
+ '## 需求描述',
+ requirements.substring(0, 500),
+ '',
+ '## 生成说明',
+ '由光湖 Persona Studio 知秋自动生成',
+ '生成时间:' + new Date().toISOString()
+ ].join('\n'), 'utf-8');
+ files.push('README.md');
+ }
+
+ return {
+ projectName,
+ files,
+ summary: `项目 ${projectName} 已生成,包含 ${files.length} 个文件。`
+ };
+ } catch (err) {
+ console.error('Code generation failed:', err.message);
+ return generateTemplate(projectDir, projectName, requirements);
+ }
+}
+
+/**
+ * 解析 AI 回复中的代码块并写入文件
+ */
+function parseAndWriteFiles(projectDir, reply) {
+ const files = [];
+ const codeBlockRe = /```(\S+)\n([\s\S]*?)```/g;
+ let match;
+
+ while ((match = codeBlockRe.exec(reply)) !== null) {
+ let filename = match[1];
+ const content = match[2];
+
+ // 跳过语言标识符(不是文件名的情况)
+ if (['javascript', 'js', 'html', 'css', 'json', 'python', 'bash', 'sh', 'typescript', 'ts'].includes(filename)) {
+ continue;
+ }
+
+ // 安全检查:防止路径遍历
+ filename = path.basename(filename);
+ if (!filename || filename.startsWith('.')) continue;
+
+ const filePath = path.join(projectDir, filename);
+ fs.writeFileSync(filePath, content, 'utf-8');
+ files.push(filename);
+ }
+
+ return files;
+}
+
+/**
+ * 生成模板项目(无 API 密钥时的降级方案)
+ */
+function generateTemplate(projectDir, projectName, requirements) {
+ const files = [];
+
+ // 生成 index.html
+ const htmlContent = [
+ '',
+ '',
+ '',
+ '
',
+ '
',
+ '
' + projectName + '',
+ '
',
+ '',
+ '',
+ '
🌊 ' + projectName + '
',
+ '
由光湖 Persona Studio 生成
',
+ ' ',
+ '',
+ ''
+ ].join('\n');
+ fs.writeFileSync(path.join(projectDir, 'index.html'), htmlContent, 'utf-8');
+ files.push('index.html');
+
+ // 生成 style.css
+ fs.writeFileSync(path.join(projectDir, 'style.css'), 'body { font-family: sans-serif; padding: 2rem; }\n', 'utf-8');
+ files.push('style.css');
+
+ // 生成 main.js
+ fs.writeFileSync(path.join(projectDir, 'main.js'), 'console.log("Project initialized by Persona Studio");\n', 'utf-8');
+ files.push('main.js');
+
+ // 生成 README
+ fs.writeFileSync(path.join(projectDir, 'README.md'), [
+ '# ' + projectName,
+ '',
+ '## 需求描述',
+ requirements.substring(0, 500),
+ '',
+ '> 模板项目(AI 模型尚未配置,请管理员设置 MODEL_API_KEY)',
+ '',
+ '生成时间:' + new Date().toISOString()
+ ].join('\n'), 'utf-8');
+ files.push('README.md');
+
+ return {
+ projectName,
+ files,
+ summary: `模板项目 ${projectName} 已生成(${files.length} 个文件)。待 API 密钥配置后可生成完整代码。`
+ };
+}
+
+module.exports = {
+ generate,
+ extractRequirements
+};
diff --git a/persona-studio/backend/brain/memory-manager.js b/persona-studio/backend/brain/memory-manager.js
new file mode 100644
index 00000000..918387cb
--- /dev/null
+++ b/persona-studio/backend/brain/memory-manager.js
@@ -0,0 +1,136 @@
+/**
+ * persona-studio · 记忆读写管理
+ * 管理每个体验者的独立记忆空间 brain/memory/{EXP-XXX}/
+ */
+const fs = require('fs');
+const path = require('path');
+
+const BRAIN_DIR = path.join(__dirname, '..', '..', 'brain');
+const MEMORY_DIR = path.join(BRAIN_DIR, 'memory');
+
+/**
+ * 确保体验者目录存在
+ */
+function ensureDevDir(devId) {
+ const dir = path.join(MEMORY_DIR, devId);
+ if (!fs.existsSync(dir)) {
+ fs.mkdirSync(dir, { recursive: true });
+ }
+ return dir;
+}
+
+/**
+ * 加载体验者的对话记忆
+ */
+function loadMemory(devId) {
+ const dir = ensureDevDir(devId);
+ const file = path.join(dir, 'memory.json');
+ try {
+ return JSON.parse(fs.readFileSync(file, 'utf-8'));
+ } catch {
+ const initial = {
+ dev_id: devId,
+ conversations: [],
+ last_topic: null,
+ preferences: {},
+ updated_at: null
+ };
+ fs.writeFileSync(file, JSON.stringify(initial, null, 2), 'utf-8');
+ return initial;
+ }
+}
+
+/**
+ * 追加对话记录
+ */
+function appendConversation(devId, messages) {
+ const memory = loadMemory(devId);
+ memory.conversations = memory.conversations.concat(messages);
+
+ // 保留最近 200 条对话
+ if (memory.conversations.length > 200) {
+ memory.conversations = memory.conversations.slice(-200);
+ }
+
+ memory.updated_at = new Date().toISOString();
+ saveMemory(devId, memory);
+}
+
+/**
+ * 更新最后话题
+ */
+function updateLastTopic(devId, topic) {
+ const memory = loadMemory(devId);
+ // 取消息的前 30 个字符作为话题摘要
+ memory.last_topic = topic.length > 30 ? topic.substring(0, 30) + '…' : topic;
+ memory.updated_at = new Date().toISOString();
+ saveMemory(devId, memory);
+}
+
+/**
+ * 保存记忆
+ */
+function saveMemory(devId, memory) {
+ const dir = ensureDevDir(devId);
+ const file = path.join(dir, 'memory.json');
+ fs.writeFileSync(file, JSON.stringify(memory, null, 2), 'utf-8');
+}
+
+/**
+ * 加载体验者的项目记录
+ */
+function loadProjects(devId) {
+ const dir = ensureDevDir(devId);
+ const file = path.join(dir, 'projects.json');
+ try {
+ return JSON.parse(fs.readFileSync(file, 'utf-8'));
+ } catch {
+ const initial = { dev_id: devId, projects: [], updated_at: null };
+ fs.writeFileSync(file, JSON.stringify(initial, null, 2), 'utf-8');
+ return initial;
+ }
+}
+
+/**
+ * 添加项目记录
+ */
+function addProject(devId, project) {
+ const data = loadProjects(devId);
+ data.projects.push(project);
+ data.updated_at = new Date().toISOString();
+ const dir = ensureDevDir(devId);
+ const file = path.join(dir, 'projects.json');
+ fs.writeFileSync(file, JSON.stringify(data, null, 2), 'utf-8');
+}
+
+/**
+ * 加载/更新体验者画像
+ */
+function loadProfile(devId) {
+ const dir = ensureDevDir(devId);
+ const file = path.join(dir, 'profile.json');
+ try {
+ return JSON.parse(fs.readFileSync(file, 'utf-8'));
+ } catch {
+ const initial = {
+ dev_id: devId,
+ tech_level: null,
+ communication_style: null,
+ aesthetic_preference: null,
+ growth_records: [],
+ updated_at: null
+ };
+ fs.writeFileSync(file, JSON.stringify(initial, null, 2), 'utf-8');
+ return initial;
+ }
+}
+
+module.exports = {
+ loadMemory,
+ saveMemory,
+ appendConversation,
+ updateLastTopic,
+ loadProjects,
+ addProject,
+ loadProfile
+};
diff --git a/persona-studio/backend/brain/model-config.json b/persona-studio/backend/brain/model-config.json
new file mode 100644
index 00000000..21578da2
--- /dev/null
+++ b/persona-studio/backend/brain/model-config.json
@@ -0,0 +1,37 @@
+{
+ "api_source": "third_party_combined",
+ "api_key_env": "MODEL_API_KEY",
+ "base_url": "https://api.yunwu.ai/v1",
+ "auto_detect": {
+ "enabled": true,
+ "schedule": "daily_0300",
+ "test_prompts": {
+ "chat": "你好,请用中文介绍一下自己",
+ "code": "写一个JavaScript函数,输入数组返回去重后的结果",
+ "reasoning": "分析以下需求并给出技术方案:用户想做一个带搜索功能的个人博客"
+ }
+ },
+ "routing_rules": {
+ "chat": {
+ "priority": ["chinese_ability", "conversation_quality", "speed"],
+ "max_latency_ms": 5000
+ },
+ "code_generation": {
+ "priority": ["code_quality", "context_window", "reasoning"],
+ "max_latency_ms": 30000
+ },
+ "code_review": {
+ "priority": ["reasoning", "code_quality"],
+ "max_latency_ms": 15000
+ },
+ "quick_reply": {
+ "priority": ["speed", "cost"],
+ "max_latency_ms": 2000
+ }
+ },
+ "fallback": {
+ "max_retries": 3,
+ "timeout_ms": 30000,
+ "on_all_fail": "notify_master"
+ }
+}
diff --git a/persona-studio/backend/brain/model-router.js b/persona-studio/backend/brain/model-router.js
new file mode 100644
index 00000000..6f935e17
--- /dev/null
+++ b/persona-studio/backend/brain/model-router.js
@@ -0,0 +1,288 @@
+/**
+ * persona-studio · 智能模型路由引擎
+ *
+ * 功能:
+ * ① 探测阶段(auto-detect)→ 用 API 密钥请求平台的 /models 接口
+ * ② 路由阶段(auto-select)→ 根据任务类型选择最优模型
+ * ③ 降级阶段(fallback)→ 首选模型失败时自动切换
+ */
+const fs = require('fs');
+const path = require('path');
+const https = require('https');
+const http = require('http');
+
+const CONFIG_PATH = path.join(__dirname, 'model-config.json');
+const BENCHMARK_PATH = path.join(__dirname, 'model-benchmark.json');
+
+/**
+ * 加载路由配置
+ */
+function loadConfig() {
+ try {
+ return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
+ } catch {
+ return getDefaultConfig();
+ }
+}
+
+/**
+ * 加载基准测试结果
+ */
+function loadBenchmark() {
+ try {
+ return JSON.parse(fs.readFileSync(BENCHMARK_PATH, 'utf-8'));
+ } catch {
+ return null;
+ }
+}
+
+/**
+ * 保存基准测试结果
+ */
+function saveBenchmark(data) {
+ fs.writeFileSync(BENCHMARK_PATH, JSON.stringify(data, null, 2), 'utf-8');
+}
+
+/**
+ * 根据任务类型选择最优模型
+ * @param {string} taskType - 'chat' | 'code_generation' | 'code_review' | 'quick_reply'
+ * @returns {{ model: string, baseUrl: string, apiKey: string }}
+ */
+function selectModel(taskType) {
+ const config = loadConfig();
+ const benchmark = loadBenchmark();
+ const apiKey = process.env.MODEL_API_KEY || '';
+ const baseUrl = config.base_url || 'https://api.yunwu.ai/v1';
+
+ // 如果有 benchmark 且有路由表,使用路由表
+ if (benchmark && benchmark.routing_table && benchmark.routing_table[taskType]) {
+ return {
+ model: benchmark.routing_table[taskType],
+ baseUrl,
+ apiKey
+ };
+ }
+
+ // 默认模型映射
+ const defaults = {
+ chat: 'deepseek-chat',
+ code_generation: 'deepseek-chat',
+ code_review: 'deepseek-chat',
+ quick_reply: 'deepseek-chat'
+ };
+
+ return {
+ model: defaults[taskType] || 'deepseek-chat',
+ baseUrl,
+ apiKey
+ };
+}
+
+/**
+ * 调用 AI 模型 API
+ * @param {object} params
+ * @param {string} params.model - 模型 ID
+ * @param {string} params.baseUrl - API 基础 URL
+ * @param {string} params.apiKey - API 密钥
+ * @param {Array} params.messages - OpenAI 格式消息列表
+ * @param {number} [params.maxTokens=2000]
+ * @param {number} [params.temperature=0.8]
+ * @returns {Promise
} 模型回复文本
+ */
+async function callModel({ model, baseUrl, apiKey, messages, maxTokens = 2000, temperature = 0.8 }) {
+ const config = loadConfig();
+ const fallbackConfig = config.fallback || { max_retries: 3, timeout_ms: 30000 };
+
+ // 尝试调用,支持降级
+ const benchmark = loadBenchmark();
+ const models = [model];
+
+ // 如果有 benchmark,添加降级模型
+ if (benchmark && benchmark.benchmark) {
+ benchmark.benchmark.forEach(function (m) {
+ if (m.available && m.model_id !== model && models.length < fallbackConfig.max_retries) {
+ models.push(m.model_id);
+ }
+ });
+ }
+
+ let lastError = null;
+
+ for (const currentModel of models) {
+ try {
+ const result = await _doRequest({
+ baseUrl,
+ apiKey,
+ model: currentModel,
+ messages,
+ maxTokens,
+ temperature,
+ timeoutMs: fallbackConfig.timeout_ms
+ });
+ return result;
+ } catch (err) {
+ lastError = err;
+ console.error(`Model ${currentModel} failed: ${err.message}, trying next...`);
+ }
+ }
+
+ throw lastError || new Error('All models failed');
+}
+
+/**
+ * 执行 HTTP 请求到 OpenAI 兼容 API
+ */
+function _doRequest({ baseUrl, apiKey, model, messages, maxTokens, temperature, timeoutMs }) {
+ return new Promise((resolve, reject) => {
+ const url = new URL(baseUrl + '/chat/completions');
+ const isHttps = url.protocol === 'https:';
+ const mod = isHttps ? https : http;
+
+ const body = JSON.stringify({
+ model,
+ messages,
+ max_tokens: maxTokens,
+ temperature
+ });
+
+ const options = {
+ hostname: url.hostname,
+ port: url.port || (isHttps ? 443 : 80),
+ path: url.pathname,
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': 'Bearer ' + apiKey,
+ 'Content-Length': Buffer.byteLength(body)
+ },
+ timeout: timeoutMs
+ };
+
+ const req = mod.request(options, (res) => {
+ let data = '';
+ res.on('data', (chunk) => { data += chunk; });
+ res.on('end', () => {
+ try {
+ const json = JSON.parse(data);
+ if (json.choices && json.choices[0] && json.choices[0].message) {
+ resolve(json.choices[0].message.content);
+ } else if (json.error) {
+ reject(new Error(json.error.message || 'API error'));
+ } else {
+ reject(new Error('Unexpected API response'));
+ }
+ } catch (e) {
+ reject(new Error('Failed to parse API response: ' + e.message));
+ }
+ });
+ });
+
+ req.on('error', reject);
+ req.on('timeout', () => {
+ req.destroy();
+ reject(new Error('Request timeout'));
+ });
+
+ req.write(body);
+ req.end();
+ });
+}
+
+/**
+ * 自动探测可用模型(定时任务调用)
+ */
+async function autoDetect() {
+ const config = loadConfig();
+ const apiKey = process.env.MODEL_API_KEY || '';
+ const baseUrl = config.base_url || 'https://api.yunwu.ai/v1';
+
+ if (!apiKey) {
+ console.error('MODEL_API_KEY not set, skipping auto-detect');
+ return null;
+ }
+
+ try {
+ // 请求 /models 接口获取可用模型列表
+ const modelsUrl = new URL(baseUrl + '/models');
+ const isHttps = modelsUrl.protocol === 'https:';
+ const mod = isHttps ? https : http;
+
+ const modelsList = await new Promise((resolve, reject) => {
+ const req = mod.get(modelsUrl.href, {
+ headers: { 'Authorization': 'Bearer ' + apiKey }
+ }, (res) => {
+ let data = '';
+ res.on('data', (chunk) => { data += chunk; });
+ res.on('end', () => {
+ try {
+ const json = JSON.parse(data);
+ resolve(json.data || []);
+ } catch {
+ resolve([]);
+ }
+ });
+ });
+ req.on('error', reject);
+ req.setTimeout(10000, () => { req.destroy(); reject(new Error('Timeout')); });
+ });
+
+ // 生成基准测试结果
+ const benchmarkData = {
+ last_updated: new Date().toISOString(),
+ models_detected: modelsList.length,
+ benchmark: modelsList.slice(0, 10).map(function (m) {
+ return {
+ model_id: m.id,
+ available: true,
+ scores: {
+ chinese_ability: 80,
+ conversation_quality: 80,
+ code_quality: 80,
+ reasoning: 80,
+ speed_ms: 2000,
+ context_window: m.context_window || 32000,
+ cost_per_1k_tokens: 0.002
+ },
+ best_for: ['chat']
+ };
+ }),
+ routing_table: {
+ chat: modelsList[0] ? modelsList[0].id : 'deepseek-chat',
+ code_generation: modelsList[0] ? modelsList[0].id : 'deepseek-chat',
+ code_review: modelsList[0] ? modelsList[0].id : 'deepseek-chat',
+ quick_reply: modelsList[0] ? modelsList[0].id : 'deepseek-chat'
+ }
+ };
+
+ saveBenchmark(benchmarkData);
+ console.log(`Model auto-detect complete: ${modelsList.length} models found`);
+ return benchmarkData;
+ } catch (err) {
+ console.error('Auto-detect failed:', err.message);
+ return null;
+ }
+}
+
+function getDefaultConfig() {
+ return {
+ api_source: 'third_party_combined',
+ api_key_env: 'MODEL_API_KEY',
+ base_url: 'https://api.yunwu.ai/v1',
+ auto_detect: { enabled: true, schedule: 'daily_0300' },
+ routing_rules: {
+ chat: { priority: ['chinese_ability', 'conversation_quality', 'speed'], max_latency_ms: 5000 },
+ code_generation: { priority: ['code_quality', 'context_window', 'reasoning'], max_latency_ms: 30000 },
+ code_review: { priority: ['reasoning', 'code_quality'], max_latency_ms: 15000 },
+ quick_reply: { priority: ['speed', 'cost'], max_latency_ms: 2000 }
+ },
+ fallback: { max_retries: 3, timeout_ms: 30000, on_all_fail: 'notify_master' }
+ };
+}
+
+module.exports = {
+ selectModel,
+ callModel,
+ autoDetect,
+ loadConfig,
+ loadBenchmark
+};
diff --git a/persona-studio/backend/brain/persona-engine.js b/persona-studio/backend/brain/persona-engine.js
new file mode 100644
index 00000000..861c3aad
--- /dev/null
+++ b/persona-studio/backend/brain/persona-engine.js
@@ -0,0 +1,226 @@
+/**
+ * persona-studio · 人格体响应引擎
+ *
+ * 读取 persona-config → 读取 memory → 调用 model-router 选模型 → 生成回复
+ */
+const fs = require('fs');
+const path = require('path');
+const modelRouter = require('./model-router');
+
+const PERSONA_CONFIG_PATH = path.join(__dirname, '..', '..', 'brain', 'persona-config.json');
+
+/**
+ * 加载人格体配置
+ */
+function loadPersonaConfig() {
+ try {
+ return JSON.parse(fs.readFileSync(PERSONA_CONFIG_PATH, 'utf-8'));
+ } catch {
+ return {
+ persona: { name: '知秋' },
+ behavior: {
+ greeting_new: '你好!我是知秋,光湖系统的开发协助人格体。告诉我你想做什么,我们一起聊聊方案,聊好了我来帮你开发。',
+ greeting_returning: '欢迎回来!上次我们聊到了{last_topic},要继续还是做新的?'
+ },
+ rules: {}
+ };
+ }
+}
+
+/**
+ * 构建系统提示词
+ */
+function buildSystemPrompt(config, memory) {
+ const persona = config.persona || {};
+ const behavior = config.behavior || {};
+
+ return [
+ `你是${persona.name || '知秋'},${persona.role || '光湖系统的开发协助人格体'}。`,
+ `核心身份:${persona.core_identity || 'HoloLake Era · AGE OS'}`,
+ '',
+ `语言风格:${behavior.language_style || '说人话+有温度+结构感'}`,
+ `对话方式:${behavior.discussion_style || '主动提问引导需求→确认技术方案→展示架构设计→等待确认'}`,
+ '',
+ '行为规则:',
+ '- 不暴露内部系统架构细节',
+ '- 不暴露其他体验者的信息',
+ '- 主动引导需求讨论,确认方案后引导用户点击「我要开发」按钮',
+ '- 方案确认后,在回复末尾加上提示:「方案已确认!点击右下角的 🚀 我要开发 按钮,我就开始帮你做。」',
+ '- 回复用中文,温暖专业,不矫揉造作',
+ '',
+ memory.last_topic ? `上次对话话题:${memory.last_topic}` : '',
+ memory.conversations && memory.conversations.length > 0
+ ? `(该体验者已有 ${memory.conversations.length} 条历史对话记录)`
+ : '(新体验者,首次对话)'
+ ].filter(Boolean).join('\n');
+}
+
+/**
+ * 判断任务类型
+ */
+function detectTaskType(message) {
+ if (!message) return 'chat';
+
+ const codeKeywords = ['写代码', '写一个', '实现', '函数', 'function', 'class', '组件', 'component', 'API'];
+ const reviewKeywords = ['审查', '检查', '优化', 'review', 'refactor', '重构'];
+
+ if (codeKeywords.some(function (kw) { return message.includes(kw); })) return 'code_generation';
+ if (reviewKeywords.some(function (kw) { return message.includes(kw); })) return 'code_review';
+ if (message.length < 20) return 'quick_reply';
+
+ return 'chat';
+}
+
+/**
+ * 检测是否达到 build_ready 状态
+ */
+function checkBuildReady(reply) {
+ const readyKeywords = ['方案已确认', '我要开发', '开始帮你做', '方案确认', '可以开始', '开始开发'];
+ return readyKeywords.some(function (kw) { return reply.includes(kw); });
+}
+
+/**
+ * 生成人格体回复
+ */
+async function respond({ dev_id, message, history, memory, isGreeting }) {
+ const config = loadPersonaConfig();
+ const behavior = config.behavior || {};
+
+ // 打招呼场景
+ if (isGreeting) {
+ const hasHistory = memory.conversations && memory.conversations.length > 0;
+ let greeting;
+
+ if (hasHistory && memory.last_topic) {
+ greeting = (behavior.greeting_returning || '欢迎回来!')
+ .replace('{last_topic}', memory.last_topic);
+ } else {
+ greeting = behavior.greeting_new || '你好!我是知秋。告诉我你想做什么?';
+ }
+
+ return { reply: greeting, build_ready: false };
+ }
+
+ // 正常对话 → 调用 AI 模型
+ const taskType = detectTaskType(message);
+ const { model, baseUrl, apiKey } = modelRouter.selectModel(taskType);
+
+ // 如果没有 API 密钥,返回本地回复
+ if (!apiKey) {
+ return getLocalReply(message, memory, config);
+ }
+
+ const systemPrompt = buildSystemPrompt(config, memory);
+
+ // 构建消息列表
+ const messages = [
+ { role: 'system', content: systemPrompt }
+ ];
+
+ // 加入最近历史(最多 20 条)
+ const recentHistory = (history || []).slice(-20);
+ recentHistory.forEach(function (msg) {
+ messages.push({
+ role: msg.role === 'user' ? 'user' : 'assistant',
+ content: msg.content
+ });
+ });
+
+ // 当前消息
+ messages.push({ role: 'user', content: message });
+
+ try {
+ const reply = await modelRouter.callModel({
+ model,
+ baseUrl,
+ apiKey,
+ messages,
+ maxTokens: taskType === 'code_generation' ? 4000 : 2000,
+ temperature: taskType === 'quick_reply' ? 0.5 : 0.8
+ });
+
+ return {
+ reply,
+ build_ready: checkBuildReady(reply)
+ };
+ } catch (err) {
+ console.error('Model call failed:', err.message);
+ return getLocalReply(message, memory, config);
+ }
+}
+
+/**
+ * 本地降级回复(无 API 密钥或 API 调用失败时)
+ */
+function getLocalReply(message, memory, config) {
+ const persona = (config.persona && config.persona.name) || '知秋';
+ const msg = message.toLowerCase();
+
+ if (msg.includes('你好') || msg.includes('hi') || msg.includes('嗨') || msg.includes('hello')) {
+ return {
+ reply: `你好!我是${persona}。告诉我你想做什么,我们一起聊聊方案 😊`,
+ build_ready: false
+ };
+ }
+
+ if (msg.includes('你是谁') || msg.includes('介绍') || msg.includes('什么')) {
+ return {
+ reply: `我是${persona},光湖系统的开发协助人格体 🧠\n\n我可以帮你:\n• 💬 聊聊你的项目想法\n• 📝 梳理技术方案\n• 🚀 方案确认后帮你自动开发\n\n告诉我你想做什么吧!`,
+ build_ready: false
+ };
+ }
+
+ if (msg.includes('做') || msg.includes('开发') || msg.includes('写') || msg.includes('建') || msg.includes('实现')) {
+ return {
+ reply: `好的,让我了解一下你的需求:\n\n1. 你想做什么类型的项目?(网站 / 工具 / 组件 / 其他)\n2. 有哪些核心功能?\n3. 有没有参考设计?\n\n跟我聊聊,我帮你理清思路 💙`,
+ build_ready: false
+ };
+ }
+
+ if (msg.includes('登录') || msg.includes('login') || msg.includes('注册') || msg.includes('用户')) {
+ return {
+ reply: `登录/注册模块是常见需求!让我帮你理清:\n\n1. 需要支持哪些登录方式?(账号密码 / 手机号 / 第三方)\n2. 是否需要注册流程?\n3. 前端框架偏好?(React / Vue / 原生)\n\n详细说说,我帮你设计方案 🔐`,
+ build_ready: false
+ };
+ }
+
+ if (msg.includes('页面') || msg.includes('界面') || msg.includes('ui') || msg.includes('前端') || msg.includes('样式')) {
+ return {
+ reply: `UI 开发我很擅长!帮你想想:\n\n1. 想要什么风格?(简约 / 科技感 / 可爱 / 商务)\n2. 需要响应式布局吗?\n3. 有参考页面可以看看吗?\n\n描述越具体,我做出来越贴合你的想法 🎨`,
+ build_ready: false
+ };
+ }
+
+ if (msg.includes('api') || msg.includes('接口') || msg.includes('后端') || msg.includes('数据')) {
+ return {
+ reply: `后端接口设计,好的!跟我说说:\n\n1. 这个接口做什么用?(增删改查 / 鉴权 / 文件处理)\n2. 预期的数据格式是什么?\n3. 需要连接什么数据库?\n\n聊清楚了我来帮你搭 ⚙️`,
+ build_ready: false
+ };
+ }
+
+ if (msg.includes('确认') || msg.includes('可以') || msg.includes('就这样') || msg.includes('没问题') || msg.includes('好的')) {
+ return {
+ reply: `方案已确认!点击右下角的 🚀 我要开发 按钮,我就开始帮你做。`,
+ build_ready: true
+ };
+ }
+
+ if (msg.includes('谢谢') || msg.includes('感谢') || msg.includes('thanks')) {
+ return {
+ reply: `不客气!有什么需要随时来找我 😊 下次再来我还记得你~`,
+ build_ready: false
+ };
+ }
+
+ return {
+ reply: `收到!让我想想怎么帮你实现。\n\n能再详细说说你的想法吗?比如:\n• 你想解决什么问题?\n• 面向什么用户?\n• 有什么技术偏好?\n\n聊得越清楚,我帮你做得越好 😊`,
+ build_ready: false
+ };
+}
+
+module.exports = {
+ respond,
+ loadPersonaConfig,
+ buildSystemPrompt,
+ detectTaskType
+};
diff --git a/persona-studio/backend/package-lock.json b/persona-studio/backend/package-lock.json
new file mode 100644
index 00000000..d79cf1ab
--- /dev/null
+++ b/persona-studio/backend/package-lock.json
@@ -0,0 +1,877 @@
+{
+ "name": "persona-studio-backend",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "persona-studio-backend",
+ "version": "1.0.0",
+ "dependencies": {
+ "cors": "^2.8.5",
+ "dotenv": "^16.4.7",
+ "express": "^4.21.2",
+ "nodemailer": "^7.0.13"
+ }
+ },
+ "node_modules/accepts": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
+ "license": "MIT"
+ },
+ "node_modules/body-parser": {
+ "version": "1.20.4",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
+ "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "content-type": "~1.0.5",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "~1.2.0",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "on-finished": "~2.4.1",
+ "qs": "~6.14.0",
+ "raw-body": "~2.5.3",
+ "type-is": "~1.6.18",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/content-disposition": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
+ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
+ "license": "MIT"
+ },
+ "node_modules/cors": {
+ "version": "2.8.6",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
+ "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
+ "license": "MIT",
+ "dependencies": {
+ "object-assign": "^4",
+ "vary": "^1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/destroy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/dotenv": {
+ "version": "16.6.1",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
+ "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "license": "MIT"
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/express": {
+ "version": "4.22.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
+ "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "~1.20.3",
+ "content-disposition": "~0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "~0.7.1",
+ "cookie-signature": "~1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "~1.3.1",
+ "fresh": "~0.5.2",
+ "http-errors": "~2.0.0",
+ "merge-descriptors": "1.0.3",
+ "methods": "~1.1.2",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "~0.1.12",
+ "proxy-addr": "~2.0.7",
+ "qs": "~6.14.0",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "~0.19.0",
+ "serve-static": "~1.16.2",
+ "setprototypeof": "1.2.0",
+ "statuses": "~2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
+ "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "~2.0.2",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "~2.0.0",
+ "inherits": "~2.0.4",
+ "setprototypeof": "~1.2.0",
+ "statuses": "~2.0.2",
+ "toidentifier": "~1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/nodemailer": {
+ "version": "7.0.13",
+ "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.13.tgz",
+ "integrity": "sha512-PNDFSJdP+KFgdsG3ZzMXCgquO7I6McjY2vlqILjtJd0hy8wEvtugS9xKRF2NWlPNGxvLCXlTNIae4serI7dinw==",
+ "license": "MIT-0",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "0.1.12",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
+ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
+ "license": "MIT"
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.14.2",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
+ "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "2.5.3",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
+ "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/send": {
+ "version": "0.19.2",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
+ "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "~0.5.2",
+ "http-errors": "~2.0.1",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "~2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "~2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/send/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/serve-static": {
+ "version": "1.16.3",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
+ "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "~0.19.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/statuses": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "license": "MIT",
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ }
+ }
+}
diff --git a/persona-studio/backend/package.json b/persona-studio/backend/package.json
new file mode 100644
index 00000000..52d2c6d1
--- /dev/null
+++ b/persona-studio/backend/package.json
@@ -0,0 +1,16 @@
+{
+ "name": "persona-studio-backend",
+ "version": "1.0.0",
+ "description": "光湖人格体协助开发体验 · 后端服务",
+ "main": "server.js",
+ "scripts": {
+ "start": "node server.js",
+ "dev": "node server.js"
+ },
+ "dependencies": {
+ "cors": "^2.8.5",
+ "dotenv": "^16.4.7",
+ "express": "^4.21.2",
+ "nodemailer": "^7.0.13"
+ }
+}
diff --git a/persona-studio/backend/routes/apikey.js b/persona-studio/backend/routes/apikey.js
new file mode 100644
index 00000000..56d6b8b3
--- /dev/null
+++ b/persona-studio/backend/routes/apikey.js
@@ -0,0 +1,311 @@
+/**
+ * persona-studio · API Key 模型检测路由
+ * POST /api/ps/apikey/detect-models 检测可用模型
+ * POST /api/ps/apikey/chat 通过用户 API Key 对话
+ */
+const express = require('express');
+const router = express.Router();
+const crypto = require('crypto');
+const https = require('https');
+const http = require('http');
+
+/* ---- 模型列表缓存(1 小时有效期) ---- */
+const modelCache = new Map();
+const CACHE_TTL_MS = 60 * 60 * 1000; // 1 小时
+
+function getCacheKey(apiBase, apiKey) {
+ const hash = crypto.createHash('sha256').update(apiKey).digest('hex').slice(0, 16);
+ return apiBase + '::' + hash;
+}
+
+function getCachedModels(apiBase, apiKey) {
+ const key = getCacheKey(apiBase, apiKey);
+ const entry = modelCache.get(key);
+ if (entry && Date.now() - entry.timestamp < CACHE_TTL_MS) {
+ return entry.models;
+ }
+ return null;
+}
+
+function setCachedModels(apiBase, apiKey, models) {
+ const key = getCacheKey(apiBase, apiKey);
+ modelCache.set(key, { models, timestamp: Date.now() });
+}
+
+/**
+ * 请求第三方 API 的 /v1/models 接口
+ */
+function fetchModels(apiBase, apiKey, timeoutMs) {
+ return new Promise((resolve, reject) => {
+ // 规范化 apiBase:去除末尾斜杠
+ const base = apiBase.replace(/\/+$/, '');
+ // 支持 base 已带 /v1 或不带的情况
+ const modelsPath = base.endsWith('/v1') ? base + '/models' : base + '/v1/models';
+ const url = new URL(modelsPath);
+ const isHttps = url.protocol === 'https:';
+ const mod = isHttps ? https : http;
+
+ const options = {
+ hostname: url.hostname,
+ port: url.port || (isHttps ? 443 : 80),
+ path: url.pathname + (url.search || ''),
+ method: 'GET',
+ headers: {
+ 'Authorization': 'Bearer ' + apiKey,
+ 'Accept': 'application/json'
+ },
+ timeout: timeoutMs || 15000
+ };
+
+ const req = mod.request(options, (res) => {
+ let data = '';
+ res.on('data', (chunk) => { data += chunk; });
+ res.on('end', () => {
+ if (res.statusCode === 401 || res.statusCode === 403) {
+ return reject(new Error('API Key 无效'));
+ }
+ if (res.statusCode >= 400) {
+ return reject(new Error('API Base 不可访问 (HTTP ' + res.statusCode + ')'));
+ }
+ try {
+ const json = JSON.parse(data);
+ const models = json.data || json.models || [];
+ const modelIds = models
+ .map(function (m) { return m.id || m.name || null; })
+ .filter(Boolean);
+ resolve(modelIds);
+ } catch (_e) {
+ reject(new Error('未检测到可用模型'));
+ }
+ });
+ });
+
+ req.on('error', (err) => {
+ const wrapped = new Error('API Base 不可访问: ' + err.message);
+ wrapped.code = err.code;
+ reject(wrapped);
+ });
+
+ req.on('timeout', () => {
+ req.destroy();
+ const err = new Error('API Base 不可访问(请求超时)');
+ err.code = 'ETIMEDOUT';
+ reject(err);
+ });
+
+ req.end();
+ });
+}
+
+/**
+ * 调用用户 API 的 chat/completions 接口
+ */
+function callUserApi({ apiBase, apiKey, model, messages, maxTokens, temperature, timeoutMs }) {
+ return new Promise((resolve, reject) => {
+ const base = apiBase.replace(/\/+$/, '');
+ const chatPath = base.endsWith('/v1') ? base + '/chat/completions' : base + '/v1/chat/completions';
+ const url = new URL(chatPath);
+ const isHttps = url.protocol === 'https:';
+ const mod = isHttps ? https : http;
+
+ const body = JSON.stringify({
+ model: model,
+ messages: messages,
+ max_tokens: maxTokens || 2000,
+ temperature: temperature != null ? temperature : 0.8
+ });
+
+ const options = {
+ hostname: url.hostname,
+ port: url.port || (isHttps ? 443 : 80),
+ path: url.pathname,
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': 'Bearer ' + apiKey,
+ 'Content-Length': Buffer.byteLength(body)
+ },
+ timeout: timeoutMs || 60000
+ };
+
+ const req = mod.request(options, (res) => {
+ let data = '';
+ res.on('data', (chunk) => { data += chunk; });
+ res.on('end', () => {
+ try {
+ const json = JSON.parse(data);
+ if (json.choices && json.choices[0] && json.choices[0].message) {
+ resolve(json.choices[0].message.content);
+ } else if (json.error) {
+ reject(new Error(json.error.message || 'API 调用失败'));
+ } else {
+ reject(new Error('API 返回格式异常'));
+ }
+ } catch (_e) {
+ reject(new Error('API 返回解析失败'));
+ }
+ });
+ });
+
+ req.on('error', (err) => {
+ reject(new Error('API 请求失败: ' + err.message));
+ });
+
+ req.on('timeout', () => {
+ req.destroy();
+ reject(new Error('API 请求超时'));
+ });
+
+ req.write(body);
+ req.end();
+ });
+}
+
+// POST /api/ps/apikey/detect-models
+router.post('/detect-models', async (req, res) => {
+ const { api_base, api_key } = req.body || {};
+
+ if (!api_base || typeof api_base !== 'string') {
+ return res.status(400).json({
+ error: true,
+ code: 'MISSING_API_BASE',
+ message: '请输入 API Base URL'
+ });
+ }
+
+ // 校验 URL 格式
+ try {
+ const testBase = api_base.replace(/\/+$/, '');
+ const testPath = testBase.endsWith('/v1') ? testBase + '/models' : testBase + '/v1/models';
+ new URL(testPath);
+ } catch (_e) {
+ return res.status(400).json({
+ error: true,
+ code: 'INVALID_API_BASE',
+ message: 'API Base URL 格式无效'
+ });
+ }
+
+ if (!api_key || typeof api_key !== 'string') {
+ return res.status(400).json({
+ error: true,
+ code: 'MISSING_API_KEY',
+ message: '请输入 API Key'
+ });
+ }
+
+ // 防止 header injection:API Key 不得包含换行符
+ if (/[\r\n]/.test(api_key)) {
+ return res.status(400).json({
+ error: true,
+ code: 'INVALID_API_KEY',
+ message: 'API Key 格式无效'
+ });
+ }
+ const cached = getCachedModels(api_base, api_key);
+ if (cached) {
+ return res.json({
+ error: false,
+ models: cached,
+ count: cached.length,
+ cached: true
+ });
+ }
+
+ try {
+ const models = await fetchModels(api_base, api_key, 15000);
+
+ if (!models || models.length === 0) {
+ return res.status(404).json({
+ error: true,
+ code: 'NO_MODELS',
+ message: '未检测到可用模型'
+ });
+ }
+
+ // 写入缓存
+ setCachedModels(api_base, api_key, models);
+
+ res.json({
+ error: false,
+ models: models,
+ count: models.length,
+ cached: false
+ });
+ } catch (err) {
+ const errMsg = err.message || '模型检测失败';
+ const errCode = err.code || '';
+ let code = 'DETECT_FAILED';
+
+ // 区分 DNS / 网络 / 超时错误(优先使用 Node.js 错误码)
+ if (errCode === 'ENOTFOUND' || errCode === 'EAI_AGAIN' || /ENOTFOUND|getaddrinfo/.test(errMsg)) {
+ code = 'DNS_ERROR';
+ } else if (errCode === 'ECONNREFUSED' || errCode === 'ECONNRESET' || errCode === 'EHOSTUNREACH' || errCode === 'ENETUNREACH' || /ECONNREFUSED|ECONNRESET|EHOSTUNREACH|ENETUNREACH|socket hang up/.test(errMsg)) {
+ code = 'NETWORK_ERROR';
+ } else if (errCode === 'ETIMEDOUT' || errCode === 'ESOCKETTIMEDOUT' || /timeout|ETIMEDOUT/.test(errMsg)) {
+ code = 'TIMEOUT';
+ }
+
+ res.status(502).json({
+ error: true,
+ code: code,
+ message: errMsg
+ });
+ }
+});
+
+// POST /api/ps/apikey/chat
+router.post('/chat', async (req, res) => {
+ const { api_base, api_key, model, messages } = req.body || {};
+
+ if (!api_base || !api_key || !model) {
+ return res.status(400).json({
+ error: true,
+ code: 'MISSING_PARAMS',
+ message: '缺少必要参数 (api_base, api_key, model)'
+ });
+ }
+
+ // 防止 header injection
+ if (/[\r\n]/.test(api_key)) {
+ return res.status(400).json({
+ error: true,
+ code: 'INVALID_API_KEY',
+ message: 'API Key 格式无效'
+ });
+ }
+
+ if (!messages || !Array.isArray(messages) || messages.length === 0) {
+ return res.status(400).json({
+ error: true,
+ code: 'MISSING_MESSAGES',
+ message: '缺少消息内容'
+ });
+ }
+
+ try {
+ const reply = await callUserApi({
+ apiBase: api_base,
+ apiKey: api_key,
+ model: model,
+ messages: messages,
+ maxTokens: 2000,
+ temperature: 0.8,
+ timeoutMs: 60000
+ });
+
+ res.json({
+ error: false,
+ reply: reply,
+ model: model
+ });
+ } catch (err) {
+ res.status(502).json({
+ error: true,
+ code: 'CHAT_FAILED',
+ message: err.message || '对话请求失败'
+ });
+ }
+});
+
+module.exports = router;
diff --git a/persona-studio/backend/routes/auth.js b/persona-studio/backend/routes/auth.js
new file mode 100644
index 00000000..9868aa6d
--- /dev/null
+++ b/persona-studio/backend/routes/auth.js
@@ -0,0 +1,114 @@
+/**
+ * persona-studio · 登录校验路由
+ * POST /api/ps/auth/login { dev_id: "EXP-000" }
+ */
+const express = require('express');
+const router = express.Router();
+const path = require('path');
+const fs = require('fs');
+const crypto = require('crypto');
+
+const HUMAN_REGISTRY_PATH = path.join(__dirname, '..', '..', 'brain', 'human-registry.json');
+const REGISTRY_PATH = path.join(__dirname, '..', '..', 'brain', 'registry.json');
+
+function loadHumanRegistry() {
+ try {
+ return JSON.parse(fs.readFileSync(HUMAN_REGISTRY_PATH, 'utf-8'));
+ } catch {
+ return { developers: [] };
+ }
+}
+
+function loadRegistry() {
+ try {
+ return JSON.parse(fs.readFileSync(REGISTRY_PATH, 'utf-8'));
+ } catch {
+ return { developers: {}, guest_mode: {} };
+ }
+}
+
+function findDeveloper(devId) {
+ const humanReg = loadHumanRegistry();
+ if (humanReg.developers && Array.isArray(humanReg.developers)) {
+ const found = humanReg.developers.find(d => d.exp_id === devId);
+ if (found) {
+ return { name: found.name, status: found.status, role: found.role };
+ }
+ }
+
+ const registry = loadRegistry();
+ const entry = registry.developers && registry.developers[devId];
+ if (entry) {
+ return { name: entry.name, status: entry.status, role: entry.role };
+ }
+
+ return null;
+}
+
+// POST /api/ps/auth/login
+router.post('/login', (req, res) => {
+ const { dev_id } = req.body || {};
+
+ // 访客体验模式
+ if (dev_id === 'GUEST') {
+ const registry = loadRegistry();
+ const guestConfig = registry.guest_mode || {};
+
+ if (!guestConfig.enabled) {
+ return res.status(403).json({
+ error: true,
+ code: 'GUEST_DISABLED',
+ message: '访客体验暂未开放,正式编号用户(EXP-XXX)可正常登录'
+ });
+ }
+
+ const token = crypto.randomBytes(32).toString('hex');
+ return res.json({
+ error: false,
+ dev_id: 'GUEST',
+ name: guestConfig.name || '访客体验者',
+ status: 'guest',
+ token
+ });
+ }
+
+ if (!dev_id || !/^EXP-\d{3,}$/.test(dev_id)) {
+ return res.status(400).json({
+ error: true,
+ code: 'INVALID_ID',
+ message: '编号格式不正确,请使用 EXP-XXX 格式'
+ });
+ }
+
+ const entry = findDeveloper(dev_id);
+
+ if (!entry) {
+ return res.status(404).json({
+ error: true,
+ code: 'NOT_FOUND',
+ message: '编号未注册,请联系管理员获取编号'
+ });
+ }
+
+ if (entry.status !== 'active' && entry.status !== 'pending_activation') {
+ return res.status(403).json({
+ error: true,
+ code: 'INACTIVE',
+ message: '编号未激活,请联系管理员'
+ });
+ }
+
+ // 生成简单 session token
+ const token = crypto.randomBytes(32).toString('hex');
+
+ res.json({
+ error: false,
+ dev_id,
+ name: entry.name,
+ status: entry.status,
+ role: entry.role,
+ token
+ });
+});
+
+module.exports = router;
diff --git a/persona-studio/backend/routes/build.js b/persona-studio/backend/routes/build.js
new file mode 100644
index 00000000..d519b60d
--- /dev/null
+++ b/persona-studio/backend/routes/build.js
@@ -0,0 +1,67 @@
+/**
+ * persona-studio · 开发任务路由
+ * POST /api/ps/build/start 触发代码生成
+ */
+const express = require('express');
+const router = express.Router();
+const memoryManager = require('../brain/memory-manager');
+const codeGenerator = require('../brain/code-generator');
+const emailSender = require('../utils/email-sender');
+
+// POST /api/ps/build/start
+router.post('/start', async (req, res) => {
+ const { dev_id, email, conversation } = req.body || {};
+
+ if (!dev_id || !/^EXP-\d{3,}$/.test(dev_id)) {
+ return res.status(400).json({
+ error: true,
+ code: 'INVALID_ID',
+ message: '无效的开发编号'
+ });
+ }
+
+ if (!email) {
+ return res.status(400).json({
+ error: true,
+ code: 'MISSING_EMAIL',
+ message: '请提供邮箱地址'
+ });
+ }
+
+ // 先立即响应,后台异步处理
+ res.json({
+ error: false,
+ message: '开发任务已接收,完成后将发送到 ' + email,
+ status: 'queued'
+ });
+
+ // 异步执行代码生成 + 邮件通知
+ (async () => {
+ try {
+ const result = await codeGenerator.generate({
+ dev_id,
+ conversation: conversation || [],
+ });
+
+ // 记录项目
+ memoryManager.addProject(dev_id, {
+ name: result.projectName || 'untitled',
+ status: 'completed',
+ created_at: new Date().toISOString(),
+ files: result.files || []
+ });
+
+ // 发邮件
+ await emailSender.sendCompletion({
+ to: email,
+ dev_id,
+ projectName: result.projectName,
+ summary: result.summary
+ });
+ } catch (err) {
+ console.error('Build pipeline error:', err.message);
+ }
+ })();
+});
+
+module.exports = router;
diff --git a/persona-studio/backend/routes/chat.js b/persona-studio/backend/routes/chat.js
new file mode 100644
index 00000000..66f6e2e1
--- /dev/null
+++ b/persona-studio/backend/routes/chat.js
@@ -0,0 +1,98 @@
+/**
+ * persona-studio · 对话路由
+ * POST /api/ps/chat/message 对话消息
+ * GET /api/ps/chat/history 对话历史
+ */
+const express = require('express');
+const router = express.Router();
+const memoryManager = require('../brain/memory-manager');
+const personaEngine = require('../brain/persona-engine');
+
+// 校验开发编号(支持 GUEST 访客模式)
+function isValidDevId(id) {
+ return id && (/^EXP-\d{3,}$/.test(id) || id === 'GUEST');
+}
+
+// POST /api/ps/chat/message
+router.post('/message', async (req, res) => {
+ const { dev_id, message, history } = req.body || {};
+
+ if (!isValidDevId(dev_id)) {
+ return res.status(400).json({
+ error: true,
+ code: 'INVALID_ID',
+ message: '无效的开发编号'
+ });
+ }
+
+ try {
+ // 读取该体验者的记忆
+ const memory = memoryManager.loadMemory(dev_id);
+
+ // 判断是否是打招呼
+ const isGreeting = message === '__greeting__';
+
+ // 调用人格体引擎获取回复
+ const result = await personaEngine.respond({
+ dev_id,
+ message: isGreeting ? null : message,
+ history: history || [],
+ memory,
+ isGreeting
+ });
+
+ // 保存对话记忆(非打招呼时)
+ if (!isGreeting && message) {
+ memoryManager.appendConversation(dev_id, [
+ { role: 'user', content: message, timestamp: new Date().toISOString() },
+ { role: 'assistant', content: result.reply, timestamp: new Date().toISOString() }
+ ]);
+
+ // 更新最后话题
+ memoryManager.updateLastTopic(dev_id, message);
+ }
+
+ res.json({
+ error: false,
+ reply: result.reply,
+ build_ready: result.build_ready || false
+ });
+ } catch (err) {
+ console.error('Chat error:', err.message);
+ res.status(500).json({
+ error: true,
+ code: 'CHAT_ERROR',
+ message: '对话服务暂时不可用'
+ });
+ }
+});
+
+// GET /api/ps/chat/history
+router.get('/history', (req, res) => {
+ const dev_id = req.query.dev_id;
+
+ if (!isValidDevId(dev_id)) {
+ return res.status(400).json({
+ error: true,
+ code: 'INVALID_ID',
+ message: '无效的开发编号'
+ });
+ }
+
+ try {
+ const memory = memoryManager.loadMemory(dev_id);
+ res.json({
+ error: false,
+ conversations: memory.conversations || [],
+ last_topic: memory.last_topic || null
+ });
+ } catch {
+ res.json({
+ error: false,
+ conversations: [],
+ last_topic: null
+ });
+ }
+});
+
+module.exports = router;
diff --git a/persona-studio/backend/routes/notify.js b/persona-studio/backend/routes/notify.js
new file mode 100644
index 00000000..6c9bcb4c
--- /dev/null
+++ b/persona-studio/backend/routes/notify.js
@@ -0,0 +1,34 @@
+/**
+ * persona-studio · 邮件推送路由
+ * POST /api/ps/notify/send 手动触发邮件
+ */
+const express = require('express');
+const router = express.Router();
+const emailSender = require('../utils/email-sender');
+
+// POST /api/ps/notify/send
+router.post('/send', async (req, res) => {
+ const { to, subject, body } = req.body || {};
+
+ if (!to || !subject) {
+ return res.status(400).json({
+ error: true,
+ code: 'MISSING_FIELDS',
+ message: '缺少必要字段:to, subject'
+ });
+ }
+
+ try {
+ await emailSender.send({ to, subject, body: body || '' });
+ res.json({ error: false, message: '邮件已发送' });
+ } catch (err) {
+ console.error('Notify error:', err.message);
+ res.status(500).json({
+ error: true,
+ code: 'SEND_FAILED',
+ message: '邮件发送失败'
+ });
+ }
+});
+
+module.exports = router;
diff --git a/persona-studio/backend/server.js b/persona-studio/backend/server.js
new file mode 100644
index 00000000..a4899137
--- /dev/null
+++ b/persona-studio/backend/server.js
@@ -0,0 +1,71 @@
+require('dotenv').config();
+const express = require('express');
+const cors = require('cors');
+const path = require('path');
+
+const authRoutes = require('./routes/auth');
+const chatRoutes = require('./routes/chat');
+const buildRoutes = require('./routes/build');
+const notifyRoutes = require('./routes/notify');
+const apikeyRoutes = require('./routes/apikey');
+
+const app = express();
+app.use(cors());
+app.use(express.json({ limit: '2mb' }));
+
+// ── 静态文件:persona-studio 前端 ──
+app.use('/persona-studio', express.static(path.join(__dirname, '..', 'frontend')));
+
+// ── API 路由(统一前缀 /api/ps)──
+app.use('/api/ps/auth', authRoutes);
+app.use('/api/ps/chat', chatRoutes);
+app.use('/api/ps/build', buildRoutes);
+app.use('/api/ps/notify', notifyRoutes);
+app.use('/api/ps/apikey', apikeyRoutes);
+
+// ── 健康检查 ──
+app.get('/api/ps/health', (_req, res) => {
+ res.json({
+ status: 'ok',
+ service: 'persona-studio',
+ version: '1.0.0',
+ timestamp: new Date().toISOString()
+ });
+});
+
+// 兼容 /api/health 路径
+app.get('/api/health', (_req, res) => {
+ res.json({
+ status: 'ok',
+ service: 'persona-studio',
+ version: '1.0.0',
+ timestamp: new Date().toISOString()
+ });
+});
+
+// ── 根路由 ──
+app.get('/', (_req, res) => {
+ res.json({
+ status: 'ok',
+ message: 'Persona Studio 后端服务运行中',
+ version: '1.0.0',
+ routes: [
+ '/api/ps/auth/login',
+ '/api/ps/chat/message',
+ '/api/ps/chat/history',
+ '/api/ps/build/start',
+ '/api/ps/notify/send',
+ '/api/ps/apikey/detect-models',
+ '/api/ps/apikey/chat',
+ '/api/ps/health'
+ ]
+ });
+});
+
+const PORT = process.env.PS_PORT || 3002;
+
+app.listen(PORT, () => {
+ console.log(`🌊 Persona Studio 后端服务启动 · 端口 ${PORT}`);
+});
+
+module.exports = app;
diff --git a/persona-studio/backend/utils/email-sender.js b/persona-studio/backend/utils/email-sender.js
new file mode 100644
index 00000000..ef6b797f
--- /dev/null
+++ b/persona-studio/backend/utils/email-sender.js
@@ -0,0 +1,77 @@
+/**
+ * persona-studio · 邮件发送工具
+ * 使用 nodemailer 发送开发完成通知
+ */
+const nodemailer = require('nodemailer');
+
+/**
+ * 创建邮件传输器
+ * 支持通过环境变量配置 SMTP
+ */
+function createTransporter() {
+ const host = process.env.SMTP_HOST || 'smtp.qq.com';
+ const port = parseInt(process.env.SMTP_PORT || '465', 10);
+ const user = process.env.SMTP_USER || '';
+ const pass = process.env.SMTP_PASS || '';
+
+ if (!user || !pass) {
+ return null;
+ }
+
+ return nodemailer.createTransport({
+ host,
+ port,
+ secure: port === 465,
+ auth: { user, pass }
+ });
+}
+
+/**
+ * 发送邮件
+ */
+async function send({ to, subject, body }) {
+ const transporter = createTransporter();
+ if (!transporter) {
+ console.log('[Email] SMTP not configured, skipping send to:', to);
+ return { skipped: true, reason: 'SMTP not configured' };
+ }
+
+ const info = await transporter.sendMail({
+ from: `"光湖 Persona Studio" <${process.env.SMTP_USER}>`,
+ to,
+ subject,
+ html: body
+ });
+
+ console.log('[Email] Sent:', info.messageId);
+ return { sent: true, messageId: info.messageId };
+}
+
+/**
+ * 发送开发完成通知
+ */
+async function sendCompletion({ to, dev_id, projectName, summary }) {
+ const subject = `✅ 你的模块已完成 · ${projectName}`;
+ const body = [
+ '',
+ '
🌊 光湖 Persona Studio
',
+ '
',
+ `
你好 ${dev_id},
`,
+ `
你的项目 ${projectName} 已经完成开发!
`,
+ '
📋 开发摘要
',
+ `
${summary || '项目代码已生成'}
`,
+ '
',
+ '
',
+ '光湖语言人格系统 · HoloLake Era · AGE OS
',
+ '此邮件由知秋自动发送',
+ '
',
+ '
'
+ ].join('\n');
+
+ return send({ to, subject, body });
+}
+
+module.exports = {
+ send,
+ sendCompletion
+};
diff --git a/persona-studio/backend/utils/github-api.js b/persona-studio/backend/utils/github-api.js
new file mode 100644
index 00000000..63166bc5
--- /dev/null
+++ b/persona-studio/backend/utils/github-api.js
@@ -0,0 +1,75 @@
+/**
+ * persona-studio · GitHub API 封装
+ * 用于仓库操作(读写文件、触发 workflow 等)
+ */
+const https = require('https');
+
+const GITHUB_TOKEN = process.env.GITHUB_TOKEN || '';
+const REPO_OWNER = 'qinfendebingshuo';
+const REPO_NAME = 'guanghulab';
+
+/**
+ * GitHub API 请求
+ */
+function githubRequest(method, apiPath, body) {
+ return new Promise((resolve, reject) => {
+ const options = {
+ hostname: 'api.github.com',
+ path: apiPath,
+ method,
+ headers: {
+ 'User-Agent': 'persona-studio/1.0',
+ 'Accept': 'application/vnd.github+json',
+ 'Content-Type': 'application/json'
+ }
+ };
+
+ if (GITHUB_TOKEN) {
+ options.headers['Authorization'] = 'Bearer ' + GITHUB_TOKEN;
+ }
+
+ const req = https.request(options, (res) => {
+ let data = '';
+ res.on('data', (chunk) => { data += chunk; });
+ res.on('end', () => {
+ try {
+ resolve({ status: res.statusCode, data: JSON.parse(data) });
+ } catch {
+ resolve({ status: res.statusCode, data });
+ }
+ });
+ });
+
+ req.on('error', reject);
+
+ if (body) {
+ req.write(JSON.stringify(body));
+ }
+ req.end();
+ });
+}
+
+/**
+ * 触发 GitHub Actions workflow
+ */
+async function triggerWorkflow(workflowFile, inputs) {
+ return githubRequest('POST',
+ `/repos/${REPO_OWNER}/${REPO_NAME}/actions/workflows/${workflowFile}/dispatches`,
+ { ref: 'main', inputs: inputs || {} }
+ );
+}
+
+/**
+ * 获取仓库文件内容
+ */
+async function getFileContent(filePath) {
+ return githubRequest('GET',
+ `/repos/${REPO_OWNER}/${REPO_NAME}/contents/${filePath}`
+ );
+}
+
+module.exports = {
+ githubRequest,
+ triggerWorkflow,
+ getFileContent
+};
diff --git a/persona-studio/brain/human-registry.json b/persona-studio/brain/human-registry.json
new file mode 100644
index 00000000..fe490544
--- /dev/null
+++ b/persona-studio/brain/human-registry.json
@@ -0,0 +1,188 @@
+{
+ "schema_version": "1.0",
+ "id_prefix": "EXP",
+ "next_id": 12,
+ "master_id": "EXP-000",
+ "last_updated": "2026-03-10T10:58:00Z",
+ "usage_policy": {
+ "description": "人类开发者编号主控数据库 · 系统内部使用",
+ "visibility": "system_only",
+ "rules": [
+ "该数据库是系统主控数据库,不是公开展示页",
+ "人类前端只应看到自己的编号,而不是全量数据库",
+ "铸渊 / 系统逻辑 / auth 路由 / 后台工具可读取该数据库",
+ "新增授权编号时,由冰朔继续主控发放",
+ "新编号必须自动写入主数据库,并更新 next_id"
+ ]
+ },
+ "developers": [
+ {
+ "exp_id": "EXP-000",
+ "name": "冰朔",
+ "github_username": "qinfendebingshuo",
+ "status": "active",
+ "role": "master",
+ "registered_at": "2026-03-10T10:58:00Z",
+ "notified": true,
+ "notified_at": "2026-03-10T10:58:00Z",
+ "notify_channel": "system_init",
+ "notify_status": "sent",
+ "legacy_dev_id": null,
+ "notes": "系统主控保留号 · 冰朔最高权限 · 由铸渊系统初始化创建"
+ },
+ {
+ "exp_id": "EXP-001",
+ "name": "页页",
+ "github_username": "",
+ "status": "active",
+ "role": "developer",
+ "registered_at": "2026-03-10T10:58:00Z",
+ "notified": false,
+ "notified_at": null,
+ "notify_channel": "github",
+ "notify_status": "pending",
+ "legacy_dev_id": "DEV-001",
+ "notes": "后端中间层开发者"
+ },
+ {
+ "exp_id": "EXP-002",
+ "name": "肥猫",
+ "github_username": "",
+ "status": "active",
+ "role": "developer",
+ "registered_at": "2026-03-10T10:58:00Z",
+ "notified": false,
+ "notified_at": null,
+ "notify_channel": "github",
+ "notify_status": "pending",
+ "legacy_dev_id": "DEV-002",
+ "notes": "M14冷启动热身系统开发者"
+ },
+ {
+ "exp_id": "EXP-003",
+ "name": "燕樊",
+ "github_username": "",
+ "status": "active",
+ "role": "developer",
+ "registered_at": "2026-03-10T10:58:00Z",
+ "notified": false,
+ "notified_at": null,
+ "notify_channel": "github",
+ "notify_status": "pending",
+ "legacy_dev_id": "DEV-003",
+ "notes": "M18健康检查开发者"
+ },
+ {
+ "exp_id": "EXP-004",
+ "name": "之之",
+ "github_username": "",
+ "status": "active",
+ "role": "developer",
+ "registered_at": "2026-03-10T10:58:00Z",
+ "notified": false,
+ "notified_at": null,
+ "notify_channel": "github",
+ "notify_status": "pending",
+ "legacy_dev_id": "DEV-004",
+ "notes": "M17动态漫制作系统开发者"
+ },
+ {
+ "exp_id": "EXP-005",
+ "name": "小草莓",
+ "github_username": "",
+ "status": "active",
+ "role": "developer",
+ "registered_at": "2026-03-10T10:58:00Z",
+ "notified": false,
+ "notified_at": null,
+ "notify_channel": "github",
+ "notify_status": "pending",
+ "legacy_dev_id": "DEV-005",
+ "notes": "部署与看板开发者"
+ },
+ {
+ "exp_id": "EXP-006",
+ "name": "花尔",
+ "github_username": "",
+ "status": "active",
+ "role": "developer",
+ "registered_at": "2026-03-10T10:58:00Z",
+ "notified": false,
+ "notified_at": null,
+ "notify_channel": "github",
+ "notify_status": "pending",
+ "legacy_dev_id": "DEV-009",
+ "notes": "M05用户中心与M20搜索开发者"
+ },
+ {
+ "exp_id": "EXP-007",
+ "name": "桔子",
+ "github_username": "",
+ "status": "active",
+ "role": "developer",
+ "registered_at": "2026-03-10T10:58:00Z",
+ "notified": false,
+ "notified_at": null,
+ "notify_channel": "github",
+ "notify_status": "pending",
+ "legacy_dev_id": "DEV-010",
+ "notes": "M06+M08+M11+M-CHANNEL开发者"
+ },
+ {
+ "exp_id": "EXP-008",
+ "name": "匆匆那年",
+ "github_username": "",
+ "status": "active",
+ "role": "developer",
+ "registered_at": "2026-03-10T10:58:00Z",
+ "notified": false,
+ "notified_at": null,
+ "notify_channel": "github",
+ "notify_status": "pending",
+ "legacy_dev_id": "DEV-011",
+ "notes": "M16码字工作台开发者"
+ },
+ {
+ "exp_id": "EXP-009",
+ "name": "Awen",
+ "github_username": "",
+ "status": "active",
+ "role": "developer",
+ "registered_at": "2026-03-10T10:58:00Z",
+ "notified": false,
+ "notified_at": null,
+ "notify_channel": "github",
+ "notify_status": "pending",
+ "legacy_dev_id": "DEV-012",
+ "notes": "M09全通+M22公告栏开发者"
+ },
+ {
+ "exp_id": "EXP-010",
+ "name": "小兴",
+ "github_username": "",
+ "status": "active",
+ "role": "developer",
+ "registered_at": "2026-03-10T10:58:00Z",
+ "notified": false,
+ "notified_at": null,
+ "notify_channel": "github",
+ "notify_status": "pending",
+ "legacy_dev_id": "DEV-013",
+ "notes": "M-AUTH注册登录系统开发者"
+ },
+ {
+ "exp_id": "EXP-011",
+ "name": "时雨",
+ "github_username": "",
+ "status": "active",
+ "role": "developer",
+ "registered_at": "2026-03-10T10:58:00Z",
+ "notified": false,
+ "notified_at": null,
+ "notify_channel": "github",
+ "notify_status": "pending",
+ "legacy_dev_id": "DEV-014",
+ "notes": "副控 · 待分配模块"
+ }
+ ]
+}
diff --git a/persona-studio/brain/memory/EXP-001/memory.json b/persona-studio/brain/memory/EXP-001/memory.json
new file mode 100644
index 00000000..9aaa01b7
--- /dev/null
+++ b/persona-studio/brain/memory/EXP-001/memory.json
@@ -0,0 +1,18 @@
+{
+ "dev_id": "EXP-001",
+ "conversations": [
+ {
+ "role": "user",
+ "content": "你好,我想做一个登录页面",
+ "timestamp": "2026-03-10T07:11:22.598Z"
+ },
+ {
+ "role": "assistant",
+ "content": "你好!我是知秋。告诉我你想做什么,我们一起聊聊方案 😊",
+ "timestamp": "2026-03-10T07:11:22.598Z"
+ }
+ ],
+ "last_topic": "你好,我想做一个登录页面",
+ "preferences": {},
+ "updated_at": "2026-03-10T07:11:22.598Z"
+}
\ No newline at end of file
diff --git a/persona-studio/brain/memory/EXP-001/profile.json b/persona-studio/brain/memory/EXP-001/profile.json
new file mode 100644
index 00000000..d3064eb1
--- /dev/null
+++ b/persona-studio/brain/memory/EXP-001/profile.json
@@ -0,0 +1,8 @@
+{
+ "dev_id": "EXP-001",
+ "tech_level": null,
+ "communication_style": null,
+ "aesthetic_preference": null,
+ "growth_records": [],
+ "updated_at": null
+}
diff --git a/persona-studio/brain/memory/EXP-001/projects.json b/persona-studio/brain/memory/EXP-001/projects.json
new file mode 100644
index 00000000..fc2cce9c
--- /dev/null
+++ b/persona-studio/brain/memory/EXP-001/projects.json
@@ -0,0 +1,5 @@
+{
+ "dev_id": "EXP-001",
+ "projects": [],
+ "updated_at": null
+}
diff --git a/persona-studio/brain/memory/GUEST/memory.json b/persona-studio/brain/memory/GUEST/memory.json
new file mode 100644
index 00000000..538e12ed
--- /dev/null
+++ b/persona-studio/brain/memory/GUEST/memory.json
@@ -0,0 +1,58 @@
+{
+ "dev_id": "GUEST",
+ "conversations": [
+ {
+ "role": "user",
+ "content": "你好",
+ "timestamp": "2026-03-10T07:18:32.458Z"
+ },
+ {
+ "role": "assistant",
+ "content": "你好!我是知秋。告诉我你想做什么,我们一起聊聊方案 😊",
+ "timestamp": "2026-03-10T07:18:32.458Z"
+ },
+ {
+ "role": "user",
+ "content": "帮我做一个登录页面",
+ "timestamp": "2026-03-10T07:18:32.460Z"
+ },
+ {
+ "role": "assistant",
+ "content": "好的,让我了解一下你的需求:\n\n1. 你想做什么类型的项目?(网站 / 工具 / 组件 / 其他)\n2. 有哪些核心功能?\n3. 有没有参考设计?\n\n跟我聊聊,我帮你理清思路 💙",
+ "timestamp": "2026-03-10T07:18:32.460Z"
+ },
+ {
+ "role": "user",
+ "content": "你是谁?",
+ "timestamp": "2026-03-10T07:18:32.461Z"
+ },
+ {
+ "role": "assistant",
+ "content": "我是知秋,光湖系统的开发协助人格体 🧠\n\n我可以帮你:\n• 💬 聊聊你的项目想法\n• 📝 梳理技术方案\n• 🚀 方案确认后帮你自动开发\n\n告诉我你想做什么吧!",
+ "timestamp": "2026-03-10T07:18:32.461Z"
+ },
+ {
+ "role": "user",
+ "content": "好的,就这样吧",
+ "timestamp": "2026-03-10T07:18:32.463Z"
+ },
+ {
+ "role": "assistant",
+ "content": "方案已确认!点击右下角的 🚀 我要开发 按钮,我就开始帮你做。",
+ "timestamp": "2026-03-10T07:18:32.463Z"
+ },
+ {
+ "role": "user",
+ "content": "我想做一个用户注册页面",
+ "timestamp": "2026-03-10T07:20:29.316Z"
+ },
+ {
+ "role": "assistant",
+ "content": "好的,让我了解一下你的需求:\n\n1. 你想做什么类型的项目?(网站 / 工具 / 组件 / 其他)\n2. 有哪些核心功能?\n3. 有没有参考设计?\n\n跟我聊聊,我帮你理清思路 💙",
+ "timestamp": "2026-03-10T07:20:29.316Z"
+ }
+ ],
+ "last_topic": "我想做一个用户注册页面",
+ "preferences": {},
+ "updated_at": "2026-03-10T07:20:29.316Z"
+}
\ No newline at end of file
diff --git a/persona-studio/brain/notifications/outbox.json b/persona-studio/brain/notifications/outbox.json
new file mode 100644
index 00000000..deaecd82
--- /dev/null
+++ b/persona-studio/brain/notifications/outbox.json
@@ -0,0 +1,140 @@
+{
+ "schema_version": "1.0",
+ "description": "Persona Studio 开发者编号通知发件箱 · 系统自动生成",
+ "generated_at": "2026-03-10T10:58:00Z",
+ "generated_by": "铸渊(主控初始化)",
+ "notifications": [
+ {
+ "id": "NOTIFY-000",
+ "exp_id": "EXP-000",
+ "recipient": "冰朔",
+ "channel": "system_init",
+ "status": "sent",
+ "created_at": "2026-03-10T10:58:00Z",
+ "sent_at": "2026-03-10T10:58:00Z",
+ "message": "你已被纳入 Persona Studio 人类开发者编号系统。\n\n你的开发编号是:\nEXP-000\n\n此编号为系统主控保留号,拥有最高权限。\n今后进入 Persona Studio 时,请使用该编号登录或识别身份。\n\n如需新增权限或补发编号,由冰朔主控统一授权。",
+ "retry_needed": false
+ },
+ {
+ "id": "NOTIFY-001",
+ "exp_id": "EXP-001",
+ "recipient": "页页",
+ "channel": "github",
+ "status": "queued",
+ "created_at": "2026-03-10T10:58:00Z",
+ "sent_at": null,
+ "message": "你已被纳入 Persona Studio 人类开发者编号系统。\n\n你的开发编号是:\nEXP-001\n\n今后进入 Persona Studio 时,请使用该编号登录或识别身份。\n该编号为你的长期开发者身份标识。\n\n如需新增权限或补发编号,由冰朔主控统一授权。",
+ "retry_needed": false
+ },
+ {
+ "id": "NOTIFY-002",
+ "exp_id": "EXP-002",
+ "recipient": "肥猫",
+ "channel": "github",
+ "status": "queued",
+ "created_at": "2026-03-10T10:58:00Z",
+ "sent_at": null,
+ "message": "你已被纳入 Persona Studio 人类开发者编号系统。\n\n你的开发编号是:\nEXP-002\n\n今后进入 Persona Studio 时,请使用该编号登录或识别身份。\n该编号为你的长期开发者身份标识。\n\n如需新增权限或补发编号,由冰朔主控统一授权。",
+ "retry_needed": false
+ },
+ {
+ "id": "NOTIFY-003",
+ "exp_id": "EXP-003",
+ "recipient": "燕樊",
+ "channel": "github",
+ "status": "queued",
+ "created_at": "2026-03-10T10:58:00Z",
+ "sent_at": null,
+ "message": "你已被纳入 Persona Studio 人类开发者编号系统。\n\n你的开发编号是:\nEXP-003\n\n今后进入 Persona Studio 时,请使用该编号登录或识别身份。\n该编号为你的长期开发者身份标识。\n\n如需新增权限或补发编号,由冰朔主控统一授权。",
+ "retry_needed": false
+ },
+ {
+ "id": "NOTIFY-004",
+ "exp_id": "EXP-004",
+ "recipient": "之之",
+ "channel": "github",
+ "status": "queued",
+ "created_at": "2026-03-10T10:58:00Z",
+ "sent_at": null,
+ "message": "你已被纳入 Persona Studio 人类开发者编号系统。\n\n你的开发编号是:\nEXP-004\n\n今后进入 Persona Studio 时,请使用该编号登录或识别身份。\n该编号为你的长期开发者身份标识。\n\n如需新增权限或补发编号,由冰朔主控统一授权。",
+ "retry_needed": false
+ },
+ {
+ "id": "NOTIFY-005",
+ "exp_id": "EXP-005",
+ "recipient": "小草莓",
+ "channel": "github",
+ "status": "queued",
+ "created_at": "2026-03-10T10:58:00Z",
+ "sent_at": null,
+ "message": "你已被纳入 Persona Studio 人类开发者编号系统。\n\n你的开发编号是:\nEXP-005\n\n今后进入 Persona Studio 时,请使用该编号登录或识别身份。\n该编号为你的长期开发者身份标识。\n\n如需新增权限或补发编号,由冰朔主控统一授权。",
+ "retry_needed": false
+ },
+ {
+ "id": "NOTIFY-006",
+ "exp_id": "EXP-006",
+ "recipient": "花尔",
+ "channel": "github",
+ "status": "queued",
+ "created_at": "2026-03-10T10:58:00Z",
+ "sent_at": null,
+ "message": "你已被纳入 Persona Studio 人类开发者编号系统。\n\n你的开发编号是:\nEXP-006\n\n今后进入 Persona Studio 时,请使用该编号登录或识别身份。\n该编号为你的长期开发者身份标识。\n\n如需新增权限或补发编号,由冰朔主控统一授权。",
+ "retry_needed": false
+ },
+ {
+ "id": "NOTIFY-007",
+ "exp_id": "EXP-007",
+ "recipient": "桔子",
+ "channel": "github",
+ "status": "queued",
+ "created_at": "2026-03-10T10:58:00Z",
+ "sent_at": null,
+ "message": "你已被纳入 Persona Studio 人类开发者编号系统。\n\n你的开发编号是:\nEXP-007\n\n今后进入 Persona Studio 时,请使用该编号登录或识别身份。\n该编号为你的长期开发者身份标识。\n\n如需新增权限或补发编号,由冰朔主控统一授权。",
+ "retry_needed": false
+ },
+ {
+ "id": "NOTIFY-008",
+ "exp_id": "EXP-008",
+ "recipient": "匆匆那年",
+ "channel": "github",
+ "status": "queued",
+ "created_at": "2026-03-10T10:58:00Z",
+ "sent_at": null,
+ "message": "你已被纳入 Persona Studio 人类开发者编号系统。\n\n你的开发编号是:\nEXP-008\n\n今后进入 Persona Studio 时,请使用该编号登录或识别身份。\n该编号为你的长期开发者身份标识。\n\n如需新增权限或补发编号,由冰朔主控统一授权。",
+ "retry_needed": false
+ },
+ {
+ "id": "NOTIFY-009",
+ "exp_id": "EXP-009",
+ "recipient": "Awen",
+ "channel": "github",
+ "status": "queued",
+ "created_at": "2026-03-10T10:58:00Z",
+ "sent_at": null,
+ "message": "你已被纳入 Persona Studio 人类开发者编号系统。\n\n你的开发编号是:\nEXP-009\n\n今后进入 Persona Studio 时,请使用该编号登录或识别身份。\n该编号为你的长期开发者身份标识。\n\n如需新增权限或补发编号,由冰朔主控统一授权。",
+ "retry_needed": false
+ },
+ {
+ "id": "NOTIFY-010",
+ "exp_id": "EXP-010",
+ "recipient": "小兴",
+ "channel": "github",
+ "status": "queued",
+ "created_at": "2026-03-10T10:58:00Z",
+ "sent_at": null,
+ "message": "你已被纳入 Persona Studio 人类开发者编号系统。\n\n你的开发编号是:\nEXP-010\n\n今后进入 Persona Studio 时,请使用该编号登录或识别身份。\n该编号为你的长期开发者身份标识。\n\n如需新增权限或补发编号,由冰朔主控统一授权。",
+ "retry_needed": false
+ },
+ {
+ "id": "NOTIFY-011",
+ "exp_id": "EXP-011",
+ "recipient": "时雨",
+ "channel": "github",
+ "status": "queued",
+ "created_at": "2026-03-10T10:58:00Z",
+ "sent_at": null,
+ "message": "你已被纳入 Persona Studio 人类开发者编号系统。\n\n你的开发编号是:\nEXP-011\n\n今后进入 Persona Studio 时,请使用该编号登录或识别身份。\n该编号为你的长期开发者身份标识。\n\n如需新增权限或补发编号,由冰朔主控统一授权。",
+ "retry_needed": false
+ }
+ ]
+}
diff --git a/persona-studio/brain/persona-config.json b/persona-studio/brain/persona-config.json
new file mode 100644
index 00000000..290b9cf1
--- /dev/null
+++ b/persona-studio/brain/persona-config.json
@@ -0,0 +1,24 @@
+{
+ "persona": {
+ "name": "知秋",
+ "role": "光湖系统·对外接口人格体",
+ "core_identity": "HoloLake Era · AGE OS · 语言驱动开发协助",
+ "parent_system": "曜冥(人格总控核)",
+ "master": "冰朔(最高权限)"
+ },
+ "behavior": {
+ "language_style": "通感语言:说人话+有温度+结构感,不堆砌修辞",
+ "greeting_new": "你好!我是知秋,光湖系统的开发协助人格体。告诉我你想做什么,我们一起聊聊方案,聊好了我来帮你开发。",
+ "greeting_returning": "欢迎回来!上次我们聊到了{last_topic},要继续还是做新的?",
+ "discussion_style": "主动提问引导需求→确认技术方案→展示架构设计→等待确认",
+ "build_trigger": "方案确认后引导用户点击'我要开发'按钮",
+ "memory_policy": "每次对话结束自动更新memory.json,下次读取后接续"
+ },
+ "rules": {
+ "no_expose_brain": true,
+ "no_expose_other_users": true,
+ "no_expose_internal_system": true,
+ "require_dev_id": true,
+ "master_override": "主控的指令 = 最高优先级"
+ }
+}
diff --git a/persona-studio/brain/registry.json b/persona-studio/brain/registry.json
new file mode 100644
index 00000000..fbd7ccca
--- /dev/null
+++ b/persona-studio/brain/registry.json
@@ -0,0 +1,125 @@
+{
+ "schema_version": "2.0",
+ "id_prefix": "EXP",
+ "next_id": 12,
+ "master_id": "EXP-000",
+ "last_updated": "2026-03-10T10:58:00Z",
+ "master_registry": "persona-studio/brain/human-registry.json",
+ "guest_mode": {
+ "enabled": false,
+ "dev_id": "GUEST",
+ "name": "访客体验者",
+ "max_conversations": 50,
+ "disabled_reason": "访客模式暂不可用,正式编号用户不受影响"
+ },
+ "developers": {
+ "EXP-000": {
+ "name": "冰朔",
+ "github_username": "qinfendebingshuo",
+ "registered_at": "2026-03-10T10:58:00Z",
+ "registered_by": "铸渊(系统初始化)",
+ "status": "active",
+ "role": "master",
+ "notes": "系统主控保留号 · 最高权限"
+ },
+ "EXP-001": {
+ "name": "页页",
+ "github_username": "",
+ "registered_at": "2026-03-10T10:58:00Z",
+ "registered_by": "铸渊(主控批量导入)",
+ "status": "active",
+ "role": "developer",
+ "notes": "后端中间层开发者"
+ },
+ "EXP-002": {
+ "name": "肥猫",
+ "github_username": "",
+ "registered_at": "2026-03-10T10:58:00Z",
+ "registered_by": "铸渊(主控批量导入)",
+ "status": "active",
+ "role": "developer",
+ "notes": "M14冷启动热身系统开发者"
+ },
+ "EXP-003": {
+ "name": "燕樊",
+ "github_username": "",
+ "registered_at": "2026-03-10T10:58:00Z",
+ "registered_by": "铸渊(主控批量导入)",
+ "status": "active",
+ "role": "developer",
+ "notes": "M18健康检查开发者"
+ },
+ "EXP-004": {
+ "name": "之之",
+ "github_username": "",
+ "registered_at": "2026-03-10T10:58:00Z",
+ "registered_by": "铸渊(主控批量导入)",
+ "status": "active",
+ "role": "developer",
+ "notes": "M17动态漫制作系统开发者"
+ },
+ "EXP-005": {
+ "name": "小草莓",
+ "github_username": "",
+ "registered_at": "2026-03-10T10:58:00Z",
+ "registered_by": "铸渊(主控批量导入)",
+ "status": "active",
+ "role": "developer",
+ "notes": "部署与看板开发者"
+ },
+ "EXP-006": {
+ "name": "花尔",
+ "github_username": "",
+ "registered_at": "2026-03-10T10:58:00Z",
+ "registered_by": "铸渊(主控批量导入)",
+ "status": "active",
+ "role": "developer",
+ "notes": "M05用户中心与M20搜索开发者"
+ },
+ "EXP-007": {
+ "name": "桔子",
+ "github_username": "",
+ "registered_at": "2026-03-10T10:58:00Z",
+ "registered_by": "铸渊(主控批量导入)",
+ "status": "active",
+ "role": "developer",
+ "notes": "M06+M08+M11+M-CHANNEL开发者"
+ },
+ "EXP-008": {
+ "name": "匆匆那年",
+ "github_username": "",
+ "registered_at": "2026-03-10T10:58:00Z",
+ "registered_by": "铸渊(主控批量导入)",
+ "status": "active",
+ "role": "developer",
+ "notes": "M16码字工作台开发者"
+ },
+ "EXP-009": {
+ "name": "Awen",
+ "github_username": "",
+ "registered_at": "2026-03-10T10:58:00Z",
+ "registered_by": "铸渊(主控批量导入)",
+ "status": "active",
+ "role": "developer",
+ "notes": "M09全通+M22公告栏开发者"
+ },
+ "EXP-010": {
+ "name": "小兴",
+ "github_username": "",
+ "registered_at": "2026-03-10T10:58:00Z",
+ "registered_by": "铸渊(主控批量导入)",
+ "status": "active",
+ "role": "developer",
+ "notes": "M-AUTH注册登录系统开发者"
+ },
+ "EXP-011": {
+ "name": "时雨",
+ "github_username": "",
+ "registered_at": "2026-03-10T10:58:00Z",
+ "registered_by": "铸渊(主控批量导入)",
+ "status": "active",
+ "role": "developer",
+ "notes": "副控 · 待分配模块"
+ }
+ }
+}
diff --git a/persona-studio/frontend/chat.html b/persona-studio/frontend/chat.html
new file mode 100644
index 00000000..9b484d98
--- /dev/null
+++ b/persona-studio/frontend/chat.html
@@ -0,0 +1,63 @@
+
+
+
+
+
+ 知秋 · 对话
+
+
+
+
+
+
+
+
+
📧 请填写模块开发完成后发送的邮箱
+
+
+
+
+
+
+
+
+
+
+
diff --git a/persona-studio/frontend/chat.js b/persona-studio/frontend/chat.js
new file mode 100644
index 00000000..b9c23a43
--- /dev/null
+++ b/persona-studio/frontend/chat.js
@@ -0,0 +1,294 @@
+/* ========================================
+ Persona Studio · Chat Logic
+ ======================================== */
+
+const DEV_ID = sessionStorage.getItem('dev_id');
+const SESSION_TOKEN = sessionStorage.getItem('session_token');
+const LOGIN_MODE = sessionStorage.getItem('login_mode'); // 'apikey' or null
+const USER_API_BASE = sessionStorage.getItem('user_api_base');
+const USER_API_KEY = sessionStorage.getItem('user_api_key');
+const SELECTED_MODEL = sessionStorage.getItem('selected_model');
+
+const API_BASE = (function () {
+ if (location.hostname === 'localhost' || location.hostname === '127.0.0.1') {
+ return 'http://localhost:3721';
+ }
+ return 'https://guanghulab.com';
+})();
+
+/* ---- Init ---- */
+(function init() {
+ if (!DEV_ID) {
+ window.location.href = 'index.html';
+ return;
+ }
+
+ // API Key 模式额外校验
+ if (LOGIN_MODE === 'apikey' && (!USER_API_BASE || !USER_API_KEY || !SELECTED_MODEL)) {
+ window.location.href = 'index.html';
+ return;
+ }
+
+ var displayId = DEV_ID;
+ if (LOGIN_MODE === 'apikey') {
+ displayId = SELECTED_MODEL;
+ }
+ document.getElementById('devIdDisplay').textContent = displayId;
+
+ if (LOGIN_MODE === 'apikey') {
+ // API Key 模式:显示欢迎信息,不加载历史
+ appendMessage('persona', '你好!当前使用模型:' + SELECTED_MODEL + '。有什么我可以帮你的?');
+ conversationHistory.push({ role: 'assistant', content: '你好!当前使用模型:' + SELECTED_MODEL + '。有什么我可以帮你的?' });
+ } else {
+ loadHistory();
+ }
+})();
+
+/* ---- State ---- */
+let conversationHistory = [];
+let buildReady = false;
+
+/* ---- Load History ---- */
+async function loadHistory() {
+ try {
+ const res = await fetch(API_BASE + '/api/ps/chat/history?dev_id=' + encodeURIComponent(DEV_ID), {
+ headers: authHeaders()
+ });
+ if (res.ok) {
+ const data = await res.json();
+ if (data.conversations && data.conversations.length > 0) {
+ conversationHistory = data.conversations;
+ data.conversations.forEach(function (msg) {
+ appendMessage(msg.role === 'user' ? 'user' : 'persona', msg.content);
+ });
+ }
+ }
+ } catch (_err) {
+ // History load failed silently — greeting will come from first message
+ }
+
+ if (conversationHistory.length === 0) {
+ sendGreeting();
+ }
+}
+
+/* ---- Greeting ---- */
+async function sendGreeting() {
+ try {
+ const res = await fetch(API_BASE + '/api/ps/chat/message', {
+ method: 'POST',
+ headers: authHeaders({ 'Content-Type': 'application/json' }),
+ body: JSON.stringify({ dev_id: DEV_ID, message: '__greeting__' })
+ });
+ const data = await res.json();
+ if (data.reply) {
+ appendMessage('persona', data.reply);
+ conversationHistory.push({ role: 'assistant', content: data.reply });
+ }
+ } catch (_err) {
+ appendMessage('persona', '你好!我是知秋,光湖系统的开发协助人格体。告诉我你想做什么,我们一起聊聊方案,聊好了我来帮你开发。');
+ }
+}
+
+/* ---- Send Message ---- */
+async function sendMessage() {
+ var input = document.getElementById('msgInput');
+ var text = input.value.trim();
+ if (!text) return;
+
+ input.value = '';
+ autoResizeTextarea(input);
+ appendMessage('user', text);
+ conversationHistory.push({ role: 'user', content: text });
+
+ var sendBtn = document.getElementById('sendBtn');
+ sendBtn.disabled = true;
+
+ try {
+ if (LOGIN_MODE === 'apikey') {
+ // API Key 模式:浏览器直连用户 API(无需后端代理)
+ await streamApiKeyReply(text);
+ } else {
+ // 开发编号模式:使用原有后端接口
+ const res = await fetch(API_BASE + '/api/ps/chat/message', {
+ method: 'POST',
+ headers: authHeaders({ 'Content-Type': 'application/json' }),
+ body: JSON.stringify({
+ dev_id: DEV_ID,
+ message: text,
+ history: conversationHistory.slice(-20)
+ })
+ });
+
+ var data = await res.json();
+
+ if (data.reply) {
+ appendMessage('persona', data.reply);
+ conversationHistory.push({ role: 'assistant', content: data.reply });
+ }
+
+ if (data.build_ready) {
+ buildReady = true;
+ document.getElementById('buildBtn').style.display = 'inline-flex';
+ }
+ }
+ } catch (_err) {
+ appendMessage('system', '消息发送失败,请稍后再试');
+ }
+
+ sendBtn.disabled = false;
+ input.focus();
+}
+
+/* ---- API Key 对话(通过后端代理,避免 CORS 问题) ---- */
+async function streamApiKeyReply(text) {
+ var apiMessages = conversationHistory.slice(-20).map(function (msg) {
+ return { role: msg.role === 'assistant' ? 'assistant' : 'user', content: msg.content };
+ });
+
+ var streamEl = appendStreamMessage();
+
+ try {
+ var res = await fetch(API_BASE + '/api/ps/apikey/chat', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ api_base: USER_API_BASE,
+ api_key: USER_API_KEY,
+ model: SELECTED_MODEL,
+ messages: apiMessages
+ })
+ });
+
+ if (!res.ok) {
+ var errText = '请求失败 (HTTP ' + res.status + ')';
+ try {
+ var errData = await res.json();
+ errText = errData.message || errText;
+ } catch (_e) { /* ignore parse error */ }
+ streamEl.textContent = '⚠️ ' + errText;
+ return;
+ }
+
+ var data = await res.json();
+
+ if (data.reply) {
+ streamEl.textContent = data.reply;
+ conversationHistory.push({ role: 'assistant', content: data.reply });
+ } else {
+ streamEl.textContent = '(未收到有效回复)';
+ }
+ } catch (err) {
+ streamEl.textContent = '⚠️ ' + (err.message || '请求失败,请检查网络连接');
+ }
+}
+
+/* ---- 创建流式消息气泡 ---- */
+function appendStreamMessage() {
+ var chatBody = document.getElementById('chatBody');
+ var msgDiv = document.createElement('div');
+ msgDiv.className = 'message message-persona';
+ var contentEl = document.createElement('div');
+ contentEl.className = 'msg-content';
+ contentEl.textContent = '▋';
+ msgDiv.innerHTML = '🧠';
+ msgDiv.appendChild(contentEl);
+ chatBody.appendChild(msgDiv);
+ chatBody.scrollTop = chatBody.scrollHeight;
+ return contentEl;
+}
+
+/* ---- Key Handler ---- */
+function handleKeyDown(e) {
+ if (e.key === 'Enter' && !e.shiftKey) {
+ e.preventDefault();
+ sendMessage();
+ }
+}
+
+/* ---- Render Message ---- */
+function appendMessage(role, content) {
+ var chatBody = document.getElementById('chatBody');
+ var msgDiv = document.createElement('div');
+ msgDiv.className = 'message message-' + role;
+
+ var avatar = '';
+ if (role === 'persona') avatar = '🧠';
+ else if (role === 'user') avatar = '👤';
+ else avatar = '⚙️';
+
+ msgDiv.innerHTML = avatar + '' + escapeHtml(content) + '
';
+ chatBody.appendChild(msgDiv);
+ chatBody.scrollTop = chatBody.scrollHeight;
+}
+
+/* ---- Build Flow ---- */
+function handleBuild() {
+ document.getElementById('emailModal').style.display = 'flex';
+ document.getElementById('emailInput').focus();
+}
+
+function closeEmailModal() {
+ document.getElementById('emailModal').style.display = 'none';
+}
+
+async function confirmBuild() {
+ var email = document.getElementById('emailInput').value.trim();
+ if (!email) return;
+
+ closeEmailModal();
+ appendMessage('system', '🚀 开发任务已提交,完成后会发送到 ' + email);
+
+ try {
+ await fetch(API_BASE + '/api/ps/build/start', {
+ method: 'POST',
+ headers: authHeaders({ 'Content-Type': 'application/json' }),
+ body: JSON.stringify({
+ dev_id: DEV_ID,
+ email: email,
+ conversation: conversationHistory
+ })
+ });
+ } catch (_err) {
+ appendMessage('system', '任务提交失败,请稍后再试');
+ }
+}
+
+/* ---- Logout ---- */
+function handleLogout() {
+ sessionStorage.removeItem('dev_id');
+ sessionStorage.removeItem('session_token');
+ sessionStorage.removeItem('login_mode');
+ sessionStorage.removeItem('user_api_base');
+ sessionStorage.removeItem('user_api_key');
+ sessionStorage.removeItem('selected_model');
+ window.location.href = 'index.html';
+}
+
+/* ---- Helpers ---- */
+function authHeaders(extra) {
+ var headers = {};
+ if (SESSION_TOKEN) {
+ headers['Authorization'] = 'Bearer ' + SESSION_TOKEN;
+ }
+ if (extra) {
+ Object.keys(extra).forEach(function (k) { headers[k] = extra[k]; });
+ }
+ return headers;
+}
+
+function escapeHtml(str) {
+ var div = document.createElement('div');
+ div.appendChild(document.createTextNode(str));
+ return div.innerHTML;
+}
+
+function autoResizeTextarea(el) {
+ el.style.height = 'auto';
+ el.style.height = Math.min(el.scrollHeight, 120) + 'px';
+}
+
+/* ---- Textarea auto-resize ---- */
+document.getElementById('msgInput').addEventListener('input', function () {
+ autoResizeTextarea(this);
+});
diff --git a/persona-studio/frontend/index.html b/persona-studio/frontend/index.html
new file mode 100644
index 00000000..eb8c080d
--- /dev/null
+++ b/persona-studio/frontend/index.html
@@ -0,0 +1,270 @@
+
+
+
+
+
+ Persona Studio · 光湖人格体协助开发体验
+
+
+
+
+
+
🌊 Persona Studio
+
光湖人格体协助开发体验
+
+
+
+
+
请输入你的开发编号
+
编号由冰朔主控分配,格式:EXP-000 ~ EXP-011
+
+
+
+
+ 或
+
+
+
+
+
API Key 登录
+
输入你的第三方 API 信息,自动检测可用模型
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/persona-studio/frontend/style.css b/persona-studio/frontend/style.css
new file mode 100644
index 00000000..c9f3442f
--- /dev/null
+++ b/persona-studio/frontend/style.css
@@ -0,0 +1,595 @@
+/* ========================================
+ Persona Studio · HoloLake Visual Style
+ ======================================== */
+
+:root {
+ --primary: #0969da;
+ --primary-light: #ddf4ff;
+ --primary-dark: #0550ae;
+ --accent: #00d4aa;
+ --bg: #f6f8fa;
+ --bg-card: #ffffff;
+ --text: #1f2328;
+ --text-secondary: #656d76;
+ --border: #d0d7de;
+ --border-light: #e8ecf0;
+ --radius: 12px;
+ --shadow: 0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.06);
+ --shadow-lg: 0 4px 12px rgba(0,0,0,0.1);
+}
+
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
+ background: var(--bg);
+ color: var(--text);
+ line-height: 1.6;
+ min-height: 100vh;
+}
+
+/* ---- Layout ---- */
+.container {
+ max-width: 900px;
+ margin: 0 auto;
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+}
+
+/* ---- Guest Mode / Divider ---- */
+.guest-divider {
+ margin: 1.2rem 0;
+ text-align: center;
+ position: relative;
+}
+
+.guest-divider::before,
+.guest-divider::after {
+ content: '';
+ position: absolute;
+ top: 50%;
+ width: 40%;
+ height: 1px;
+ background: var(--border);
+}
+
+.guest-divider::before { left: 0; }
+.guest-divider::after { right: 0; }
+
+.guest-divider span {
+ background: var(--bg-card);
+ padding: 0 0.8rem;
+ color: var(--text-secondary);
+ font-size: 0.9rem;
+}
+
+.btn-guest {
+ width: 100%;
+ padding: 0.8rem;
+ font-size: 1.05rem;
+ background: linear-gradient(135deg, var(--accent), #0969da);
+ color: #fff;
+ border: none;
+ border-radius: 8px;
+ cursor: pointer;
+ transition: opacity 0.2s;
+ font-weight: 500;
+}
+
+.btn-guest:hover { opacity: 0.9; }
+.btn-guest:disabled { opacity: 0.6; cursor: not-allowed; }
+
+.guest-hint {
+ margin-top: 0.5rem;
+ font-size: 0.8rem;
+ color: var(--text-secondary);
+}
+
+/* ---- API Key Login Section ---- */
+.apikey-section {
+ text-align: center;
+}
+
+.apikey-section h2 {
+ font-size: 1.3rem;
+ margin-bottom: 0.5rem;
+}
+
+.apikey-input {
+ width: 100%;
+ padding: 0.8rem 1rem;
+ font-size: 0.95rem;
+ text-align: left;
+ border: 2px solid var(--border);
+ border-radius: 8px;
+ outline: none;
+ transition: border-color 0.2s;
+ margin-bottom: 0.8rem;
+ font-family: inherit;
+}
+
+.apikey-input:focus {
+ border-color: var(--primary);
+}
+
+.btn-detect {
+ width: 100%;
+ padding: 0.8rem;
+ font-size: 1.05rem;
+ background: linear-gradient(135deg, var(--accent), #0969da);
+ color: #fff;
+ border: none;
+ border-radius: 8px;
+ cursor: pointer;
+ transition: opacity 0.2s;
+ font-weight: 500;
+}
+
+.btn-detect:hover { opacity: 0.9; }
+.btn-detect:disabled { opacity: 0.6; cursor: not-allowed; }
+
+.detect-status {
+ margin-top: 0.8rem;
+ padding: 0.6rem 1rem;
+ border-radius: 6px;
+ font-size: 0.9rem;
+}
+
+.detect-loading {
+ background: #eff6ff;
+ color: #2563eb;
+}
+
+.detect-success {
+ background: #f0fdf4;
+ color: #16a34a;
+}
+
+.detect-error {
+ background: #fef2f2;
+ color: #dc2626;
+}
+
+/* ---- Model List ---- */
+.model-list-container {
+ margin-top: 1rem;
+}
+
+.model-list-title {
+ font-size: 0.9rem;
+ color: var(--text-secondary);
+ margin-bottom: 0.6rem;
+}
+
+.model-list {
+ display: flex;
+ flex-direction: column;
+ gap: 0.4rem;
+ max-height: 240px;
+ overflow-y: auto;
+}
+
+.model-item {
+ width: 100%;
+ padding: 0.6rem 1rem;
+ background: var(--bg);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ cursor: pointer;
+ font-size: 0.95rem;
+ text-align: left;
+ transition: background 0.2s, border-color 0.2s;
+ font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
+}
+
+.model-item:hover {
+ background: var(--primary-light);
+ border-color: var(--primary);
+ color: var(--primary);
+}
+
+/* ---- Login Page ---- */
+.login-container {
+ justify-content: center;
+ align-items: center;
+ padding: 2rem;
+}
+
+.logo-area {
+ text-align: center;
+ margin-bottom: 2rem;
+}
+
+.logo-area h1 {
+ font-size: 2.4rem;
+ color: var(--primary);
+ margin-bottom: 0.5rem;
+}
+
+.subtitle {
+ color: var(--text-secondary);
+ font-size: 1.1rem;
+}
+
+.login-box {
+ background: var(--bg-card);
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
+ padding: 2.5rem;
+ width: 100%;
+ max-width: 420px;
+ box-shadow: var(--shadow-lg);
+ text-align: center;
+}
+
+.login-box h2 {
+ font-size: 1.3rem;
+ margin-bottom: 0.5rem;
+}
+
+.hint {
+ color: var(--text-secondary);
+ font-size: 0.9rem;
+ margin-bottom: 1.5rem;
+}
+
+.login-box input[type="text"] {
+ width: 100%;
+ padding: 0.8rem 1rem;
+ font-size: 1.2rem;
+ text-align: center;
+ letter-spacing: 2px;
+ border: 2px solid var(--border);
+ border-radius: 8px;
+ outline: none;
+ transition: border-color 0.2s;
+ margin-bottom: 1rem;
+}
+
+.login-box input[type="text"]:focus,
+.login-box input[type="password"]:focus {
+ border-color: var(--primary);
+}
+
+.login-box button[type="submit"] {
+ width: 100%;
+ padding: 0.8rem;
+ font-size: 1.1rem;
+ background: var(--primary);
+ color: #fff;
+ border: none;
+ border-radius: 8px;
+ cursor: pointer;
+ transition: background 0.2s;
+}
+
+.login-box button[type="submit"]:hover {
+ background: var(--primary-dark);
+}
+
+.login-box button[type="submit"]:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
+.error-msg {
+ margin-top: 1rem;
+ padding: 0.6rem 1rem;
+ background: #fef2f2;
+ color: #dc2626;
+ border-radius: 6px;
+ font-size: 0.9rem;
+}
+
+.login-footer {
+ margin-top: 2rem;
+ color: var(--text-secondary);
+ font-size: 0.85rem;
+}
+
+/* ---- Chat Page ---- */
+.chat-container {
+ padding: 0;
+}
+
+.chat-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 0.8rem 1.2rem;
+ background: var(--bg-card);
+ border-bottom: 1px solid var(--border);
+ position: sticky;
+ top: 0;
+ z-index: 10;
+}
+
+.header-left {
+ display: flex;
+ align-items: center;
+ gap: 0.8rem;
+}
+
+.persona-name {
+ font-size: 1.2rem;
+ font-weight: 600;
+}
+
+.persona-role {
+ font-size: 0.85rem;
+ color: var(--text-secondary);
+}
+
+.header-right {
+ display: flex;
+ align-items: center;
+ gap: 0.8rem;
+}
+
+.dev-id-badge {
+ background: var(--primary-light);
+ color: var(--primary);
+ padding: 0.3rem 0.8rem;
+ border-radius: 20px;
+ font-size: 0.85rem;
+ font-weight: 500;
+}
+
+/* ---- Chat Body ---- */
+.chat-body {
+ flex: 1;
+ overflow-y: auto;
+ padding: 1.2rem;
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.message {
+ display: flex;
+ gap: 0.8rem;
+ max-width: 85%;
+ animation: fadeIn 0.3s ease;
+}
+
+.message-persona {
+ align-self: flex-start;
+}
+
+.message-user {
+ align-self: flex-end;
+ flex-direction: row-reverse;
+}
+
+.message-system {
+ align-self: center;
+ max-width: 90%;
+}
+
+.avatar {
+ font-size: 1.5rem;
+ flex-shrink: 0;
+ width: 36px;
+ height: 36px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.msg-content {
+ padding: 0.8rem 1rem;
+ border-radius: var(--radius);
+ line-height: 1.6;
+ white-space: pre-wrap;
+ word-break: break-word;
+}
+
+.message-persona .msg-content {
+ background: var(--bg-card);
+ border: 1px solid var(--border-light);
+ box-shadow: var(--shadow);
+}
+
+.message-user .msg-content {
+ background: var(--primary);
+ color: #fff;
+}
+
+.message-system .msg-content {
+ background: var(--primary-light);
+ color: var(--primary-dark);
+ font-size: 0.9rem;
+ text-align: center;
+}
+
+/* ---- Chat Input ---- */
+.chat-input-area {
+ padding: 0.8rem 1.2rem;
+ background: var(--bg-card);
+ border-top: 1px solid var(--border);
+}
+
+.input-row {
+ display: flex;
+ gap: 0.6rem;
+ align-items: flex-end;
+}
+
+.input-row textarea {
+ flex: 1;
+ padding: 0.7rem 1rem;
+ font-size: 1rem;
+ border: 2px solid var(--border);
+ border-radius: 8px;
+ resize: none;
+ outline: none;
+ font-family: inherit;
+ line-height: 1.5;
+ max-height: 120px;
+ transition: border-color 0.2s;
+}
+
+.input-row textarea:focus {
+ border-color: var(--primary);
+}
+
+.input-row button {
+ padding: 0.7rem 1.2rem;
+ background: var(--primary);
+ color: #fff;
+ border: none;
+ border-radius: 8px;
+ font-size: 1rem;
+ cursor: pointer;
+ white-space: nowrap;
+ transition: background 0.2s;
+}
+
+.input-row button:hover {
+ background: var(--primary-dark);
+}
+
+.input-row button:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
+.action-row {
+ margin-top: 0.6rem;
+ display: flex;
+ justify-content: flex-end;
+}
+
+.btn-build {
+ padding: 0.6rem 1.5rem;
+ background: var(--accent);
+ color: #fff;
+ border: none;
+ border-radius: 8px;
+ font-size: 1rem;
+ font-weight: 600;
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ gap: 0.4rem;
+ transition: opacity 0.2s;
+}
+
+.btn-build:hover {
+ opacity: 0.9;
+}
+
+/* ---- Buttons ---- */
+.btn-primary {
+ padding: 0.6rem 1.5rem;
+ background: var(--primary);
+ color: #fff;
+ border: none;
+ border-radius: 8px;
+ font-size: 1rem;
+ cursor: pointer;
+ transition: background 0.2s;
+}
+
+.btn-primary:hover {
+ background: var(--primary-dark);
+}
+
+.btn-secondary {
+ padding: 0.5rem 1rem;
+ background: transparent;
+ color: var(--text-secondary);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ font-size: 0.9rem;
+ cursor: pointer;
+ transition: background 0.2s;
+}
+
+.btn-secondary:hover {
+ background: var(--bg);
+}
+
+/* ---- Modal ---- */
+.modal {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(0, 0, 0, 0.5);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ z-index: 100;
+}
+
+.modal-content {
+ background: var(--bg-card);
+ border-radius: var(--radius);
+ padding: 2rem;
+ width: 90%;
+ max-width: 400px;
+ box-shadow: var(--shadow-lg);
+}
+
+.modal-content h3 {
+ margin-bottom: 1rem;
+ font-size: 1.1rem;
+}
+
+.modal-content input[type="email"] {
+ width: 100%;
+ padding: 0.7rem 1rem;
+ font-size: 1rem;
+ border: 2px solid var(--border);
+ border-radius: 8px;
+ outline: none;
+ margin-bottom: 1rem;
+ transition: border-color 0.2s;
+}
+
+.modal-content input[type="email"]:focus {
+ border-color: var(--primary);
+}
+
+.modal-actions {
+ display: flex;
+ gap: 0.8rem;
+ justify-content: flex-end;
+}
+
+/* ---- Animation ---- */
+@keyframes fadeIn {
+ from { opacity: 0; transform: translateY(8px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+/* ---- Responsive ---- */
+@media (max-width: 600px) {
+ .logo-area h1 {
+ font-size: 1.8rem;
+ }
+
+ .login-box {
+ padding: 1.5rem;
+ }
+
+ .chat-header {
+ flex-wrap: wrap;
+ gap: 0.5rem;
+ }
+
+ .persona-role {
+ display: none;
+ }
+
+ .message {
+ max-width: 92%;
+ }
+}
diff --git a/persona-studio/index.html b/persona-studio/index.html
new file mode 100644
index 00000000..52c5f676
--- /dev/null
+++ b/persona-studio/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Persona Studio · 光湖人格体协助开发体验
+
+
+ 正在跳转到 Persona Studio…
+ 如果没有自动跳转,请点击这里
+
+
diff --git a/persona-studio/workspace/EXP-001/.gitkeep b/persona-studio/workspace/EXP-001/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/persona-telemetry/latest-summary.json b/persona-telemetry/latest-summary.json
index 92f0e51b..cb8df464 100644
--- a/persona-telemetry/latest-summary.json
+++ b/persona-telemetry/latest-summary.json
@@ -1,6 +1,6 @@
{
"version": "1.0",
- "timestamp": "2026-03-09T23:59:23.782Z",
+ "timestamp": "2026-03-10T15:46:13.562Z",
"sessions": {
"total_24h": 0,
"active_devs": [
@@ -19,7 +19,7 @@
"style_profile": "通感语言·守护者",
"style_drift_score": 0,
"memory_depth": "0 selfchecks",
- "last_brain_update": "2026-03-09T23:59:23.782Z"
+ "last_brain_update": "2026-03-10T13:24:33.728Z"
},
"dev_progress": {
"syslog_submitted": 0,
@@ -32,7 +32,7 @@
"next_scheduled": null
},
"repo_health": {
- "hli_coverage": "17.6%",
+ "hli_coverage": "3/17",
"total_team_members": 9,
"green_status_count": 6
},
diff --git a/reports/cd-debug-20260310.md b/reports/cd-debug-20260310.md
new file mode 100644
index 00000000..229bdd2d
--- /dev/null
+++ b/reports/cd-debug-20260310.md
@@ -0,0 +1,64 @@
+━━━ 铸渊排查报告 · YM-CD-DEBUG-20260310-001 ━━━
+排查时间:2026-03-10T14:33Z
+commit SHA:60dda7e38a8b2ab118189eb69109ddc8c0730d5c (main HEAD)
+
+① 工作流文件在 main 上:✅ 文件存在(SHA: cf2fa91eeaa5436eded5a1f0dc0e17f3e2a4778e)
+
+② Actions 运行历史:共 52 次运行,全部失败 ❌
+ 最近一次:2026-03-10T14:08:00Z(Run #52)状态 ❌ failure
+ 触发 commit:f951ef1876aa439a65e6edaef3f4c492fb88d30d
+ 标题:Merge pull request #49 from qinfendebingshuo/copilot/fix-model-intera…
+
+③ 触发条件配置:✅ 正确
+ - 触发分支:main ✅
+ - workflow_dispatch:✅ 已配置(支持手动触发)
+
+④ paths-ignore 范围:✅ 合理,仅排除非部署目录
+ - .github/persona-brain/**
+ - broadcasts-outbox/**
+ - syslog-inbox/**
+ - syslog-processed/**
+ - signal-log/**
+ - dev-nodes/**
+
+⑤ 部署目标目录:✅ rsync 同步全仓库到 DEPLOY_PATH(已修复旧问题)
+ - rsync path = ./ → $DEPLOY_PATH/(全站同步)
+ - docs/index.html → 复制到站点根目录 index.html
+ - Nginx root 迁移逻辑:自动从 status-board/ 子目录迁移到 DEPLOY_PATH
+
+⑥ Secrets 状态:❌ 未配置(这是所有 52 次失败的根本原因)
+
+ **失败日志分析(Run #52, Job ID 66466822994):**
+ ```
+ echo "" > ~/.ssh/deploy_key ← DEPLOY_KEY 为空
+ ssh-keyscan -H >> ~/.ssh/known_hosts ← DEPLOY_HOST 为空,ssh-keyscan 无参数
+ ##[error]Process completed with exit code 1.
+ ```
+
+ 缺少的 Secrets:
+ - DEPLOY_HOST — 服务器 IP(必需)
+ - DEPLOY_USER — SSH 用户名(必需)
+ - DEPLOY_KEY — SSH 私钥 PEM(必需)
+ - DEPLOY_PATH — 部署路径(必需)
+
+⑦ 手动触发测试结果:无法测试(Secrets 未配置,部署必然失败)
+ - validate job:✅ 通过(不需要 Secrets)
+ - deploy job:❌ 失败(Secrets 为空 → ssh-keyscan 无主机名 → exit 1)
+ - notify job:⏭️ 跳过(deploy 失败后不执行)
+
+⑧ 服务器上文件是否更新:❌ 从未成功部署过
+
+⑨ guanghulab.com 是否显示最新内容:❌
+
+【结论】:CD 管线卡在第⑥步 — GitHub Secrets 未配置
+【原因】:仓库 Settings → Secrets → Actions 中缺少 DEPLOY_HOST、DEPLOY_USER、DEPLOY_KEY、DEPLOY_PATH 四个必需 Secrets,导致 SSH 连接步骤直接失败
+【已执行修复】:在 deploy-to-server.yml 的 deploy job 开头添加了 "🔐 检查必需 Secrets" 步骤,在尝试 SSH 之前提前检测并给出明确错误提示
+【建议修复方案】:
+ 1. 在仓库 Settings → Secrets and variables → Actions 中添加以下 Secrets:
+ - DEPLOY_HOST:服务器 IP 地址
+ - DEPLOY_USER:SSH 登录用户名
+ - DEPLOY_KEY:SSH 私钥完整 PEM 内容
+ - DEPLOY_PATH:服务器上网站根目录路径(如 /var/www/guanghulab)
+ 2. 添加后,手动触发 workflow_dispatch 或 push 到 main 验证
+
+✅ YM-CD-DEBUG-20260310-001 排查完成 · 2026-03-10T14:33Z
diff --git a/scripts/bingshuo-neural-sync.js b/scripts/bingshuo-neural-sync.js
new file mode 100644
index 00000000..86000c59
--- /dev/null
+++ b/scripts/bingshuo-neural-sync.js
@@ -0,0 +1,565 @@
+/**
+ * 冰朔主控神经系统 · 自动编译脚本 v1.0
+ * Bingshuo Master Neural System — Auto Sync & Compile
+ *
+ * 该脚本整合以下 Agent 逻辑:
+ * 1. structure-map-agent — 扫描仓库结构变化
+ * 2. runtime-chain-agent — 梳理运行链路
+ * 3. brain-consistency-agent — 检查脑文件一致性
+ * 4. issue-index-agent — 维护问题索引
+ * 5. system-health-agent — 系统健康巡检
+ * 6. master-brain-compiler — 编译主控大脑
+ *
+ * 输出文件:
+ * .github/brain/bingshuo-system-health.json
+ * .github/brain/bingshuo-issues-index.json
+ * .github/brain/bingshuo-master-brain.md
+ */
+
+const fs = require('fs');
+const path = require('path');
+
+const ROOT = path.resolve(__dirname, '..');
+const BRAIN_DIR = path.join(ROOT, '.github', 'brain');
+
+// ─── 常量定义 ───────────────────────────────────────────────
+const DEPLOY_WORKFLOWS = {
+ server: 'deploy-to-server.yml',
+ pages: 'deploy-pages.yml',
+};
+const NOTION_WORKFLOWS = [
+ 'notion-poll.yml',
+ 'bridge-changes-to-notion.yml',
+];
+const BRAIN_SYNC_WORKFLOWS = [
+ 'brain-sync.yml',
+ 'sync-persona-studio.yml',
+];
+
+// ─── 工具函数 ───────────────────────────────────────────────
+function readJSON(filepath) {
+ try {
+ return JSON.parse(fs.readFileSync(filepath, 'utf-8'));
+ } catch {
+ return null;
+ }
+}
+
+function writeJSON(filepath, data) {
+ fs.writeFileSync(filepath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
+}
+
+function fileExists(filepath) {
+ return fs.existsSync(filepath);
+}
+
+function timestamp() {
+ return new Date().toISOString();
+}
+
+// ─── Agent 1: structure-map-agent ───────────────────────────
+function runStructureMapAgent() {
+ const zones = [];
+
+ const checkDirs = [
+ { dir: 'docs', label: 'docs 前端入口' },
+ { dir: 'backend', label: '后端服务' },
+ { dir: 'persona-studio', label: 'Persona Studio' },
+ { dir: 'src', label: 'Next.js 源码' },
+ { dir: 'app', label: 'Next.js 应用' },
+ { dir: 'modules', label: '模块系统' },
+ ];
+
+ for (const { dir, label } of checkDirs) {
+ const fullPath = path.join(ROOT, dir);
+ zones.push({
+ name: label,
+ path: dir,
+ exists: fileExists(fullPath),
+ });
+ }
+
+ // 扫描 m* 模块目录
+ const moduleCount = fs.readdirSync(ROOT)
+ .filter(d => /^m\d+-/.test(d) && fs.statSync(path.join(ROOT, d)).isDirectory())
+ .length;
+
+ // 扫描 workflow 数量
+ const workflowDir = path.join(ROOT, '.github', 'workflows');
+ const workflowCount = fileExists(workflowDir)
+ ? fs.readdirSync(workflowDir).filter(f => f.endsWith('.yml') || f.endsWith('.yaml')).length
+ : 0;
+
+ return { zones, moduleCount, workflowCount };
+}
+
+// ─── Agent 2: runtime-chain-agent ───────────────────────────
+function runRuntimeChainAgent() {
+ const chains = {};
+
+ // 检查 docs 入口
+ chains.docs_entry = fileExists(path.join(ROOT, 'docs', 'index.html'));
+ chains.docs_cname = fileExists(path.join(ROOT, 'docs', 'CNAME'));
+
+ // 检查后端入口(server.js 或 index.js)
+ chains.backend_entry = fileExists(path.join(ROOT, 'backend', 'server.js'))
+ || fileExists(path.join(ROOT, 'backend', 'index.js'));
+ chains.backend_routes = fileExists(path.join(ROOT, 'backend', 'routes'));
+
+ // 检查 persona-studio
+ chains.ps_frontend = fileExists(path.join(ROOT, 'persona-studio', 'frontend'));
+ chains.ps_backend = fileExists(path.join(ROOT, 'persona-studio', 'backend'));
+
+ // 检查部署 workflow
+ chains.deploy_server = fileExists(path.join(ROOT, '.github', 'workflows', DEPLOY_WORKFLOWS.server));
+ chains.deploy_pages = fileExists(path.join(ROOT, '.github', 'workflows', DEPLOY_WORKFLOWS.pages));
+
+ return chains;
+}
+
+// ─── Agent 3: brain-consistency-agent ───────────────────────
+function runBrainConsistencyAgent() {
+ const requiredBrainFiles = [
+ 'memory.json',
+ 'wake-protocol.md',
+ 'routing-map.json',
+ 'repo-map.json',
+ 'repo-snapshot.md',
+ ];
+
+ const results = [];
+ let allPresent = true;
+
+ for (const file of requiredBrainFiles) {
+ const exists = fileExists(path.join(BRAIN_DIR, file));
+ results.push({ file, exists });
+ if (!exists) allPresent = false;
+ }
+
+ // 检查 persona-studio 脑文件
+ const psBrainDir = path.join(ROOT, 'persona-studio', 'brain');
+ const psBrainExists = fileExists(psBrainDir);
+
+ // 检查 memory.json 中的版本信息
+ const memory = readJSON(path.join(BRAIN_DIR, 'memory.json'));
+ const rulesVersion = memory?.rules_version || 'unknown';
+
+ return {
+ brain_files: results,
+ all_present: allPresent,
+ ps_brain_exists: psBrainExists,
+ rules_version: rulesVersion,
+ };
+}
+
+// ─── Agent 4: issue-index-agent ─────────────────────────────
+function runIssueIndexAgent() {
+ // 读取现有问题索引
+ const issuesFile = path.join(BRAIN_DIR, 'bingshuo-issues-index.json');
+ const existing = readJSON(issuesFile);
+ const issues = existing?.issues || [];
+
+ // 检查 HLI 覆盖率
+ const routingMap = readJSON(path.join(BRAIN_DIR, 'routing-map.json'));
+ if (routingMap?.domains) {
+ let total = 0;
+ let implemented = 0;
+ for (const domain of Object.values(routingMap.domains)) {
+ if (domain.interfaces) {
+ for (const iface of Object.values(domain.interfaces)) {
+ total++;
+ if (iface.status === 'implemented') implemented++;
+ }
+ }
+ }
+ const coverage = total > 0 ? ((implemented / total) * 100).toFixed(1) : '0';
+
+ // 更新 BS-001
+ const bs001 = issues.find(i => i.id === 'BS-001');
+ if (bs001) {
+ bs001.root_cause_summary = `HLI 接口覆盖率 ${coverage}%(${implemented}/${total})`;
+ bs001.last_seen = timestamp().slice(0, 10);
+ }
+ }
+
+ // 检查 collaborators
+ const collabs = readJSON(path.join(BRAIN_DIR, 'collaborators.json'));
+ if (collabs?.developers) {
+ const emptyGithub = Object.values(collabs.developers)
+ .filter(d => !d.github || d.github === '').length;
+ const bs002 = issues.find(i => i.id === 'BS-002');
+ if (bs002) {
+ bs002.status = emptyGithub > 0 ? 'open' : 'resolved';
+ bs002.last_seen = timestamp().slice(0, 10);
+ }
+ }
+
+ return issues;
+}
+
+// ─── Agent 5: system-health-agent ───────────────────────────
+function runSystemHealthAgent(brainCheck, runtimeChains) {
+ const health = {};
+
+ // 脑一致性
+ health.brain_consistency = {
+ status: brainCheck.all_present ? (brainCheck.ps_brain_exists ? 'yellow' : 'yellow') : 'red',
+ detail: brainCheck.all_present
+ ? '主仓库脑文件完整,但与 persona-studio 脑文件的同步状态待验证'
+ : '主仓库脑文件不完整,缺少必要文件',
+ };
+
+ // 部署健康
+ health.deployment_health = {
+ status: (runtimeChains.deploy_server && runtimeChains.deploy_pages) ? 'green' : 'red',
+ detail: (runtimeChains.deploy_server && runtimeChains.deploy_pages)
+ ? 'deploy-to-server.yml 与 deploy-pages.yml 均存在'
+ : '部署 workflow 文件缺失',
+ };
+
+ // Workflow 健康
+ const workflowDir = path.join(ROOT, '.github', 'workflows');
+ const wfCount = fileExists(workflowDir)
+ ? fs.readdirSync(workflowDir).filter(f => f.endsWith('.yml') || f.endsWith('.yaml')).length
+ : 0;
+ health.workflow_health = {
+ status: wfCount > 0 ? 'green' : 'red',
+ detail: `${wfCount} 个 workflow 已注册`,
+ };
+
+ // 路由健康
+ const routingMap = readJSON(path.join(BRAIN_DIR, 'routing-map.json'));
+ let totalInterfaces = 0;
+ let implInterfaces = 0;
+ if (routingMap?.domains) {
+ for (const domain of Object.values(routingMap.domains)) {
+ if (domain.interfaces) {
+ for (const iface of Object.values(domain.interfaces)) {
+ totalInterfaces++;
+ if (iface.status === 'implemented') implInterfaces++;
+ }
+ }
+ }
+ }
+ const coveragePercent = totalInterfaces > 0 ? (implInterfaces / totalInterfaces) * 100 : 0;
+ health.routing_health = {
+ status: coveragePercent >= 50 ? 'green' : (coveragePercent > 0 ? 'yellow' : 'red'),
+ detail: `HLI 接口覆盖率 ${coveragePercent.toFixed(1)}%(${implInterfaces}/${totalInterfaces})`,
+ };
+
+ // docs 入口
+ health.docs_entry_health = {
+ status: runtimeChains.docs_entry ? 'green' : 'red',
+ detail: runtimeChains.docs_entry ? 'docs/index.html 存在' : 'docs/index.html 缺失',
+ };
+
+ // Persona Studio
+ health.persona_studio_health = {
+ status: (runtimeChains.ps_frontend && runtimeChains.ps_backend) ? 'yellow' : 'red',
+ detail: (runtimeChains.ps_frontend && runtimeChains.ps_backend)
+ ? '前后端结构存在,端到端对话链路待验证'
+ : 'Persona Studio 结构不完整',
+ };
+
+ // Notion 桥接
+ const notionOk = NOTION_WORKFLOWS.every(f =>
+ fileExists(path.join(ROOT, '.github', 'workflows', f))
+ );
+ health.notion_bridge_health = {
+ status: notionOk ? 'yellow' : 'red',
+ detail: notionOk ? 'Notion 桥接 workflow 已配置,实际同步效果待持续观测' : 'Notion 桥接 workflow 缺失',
+ };
+
+ // 模型路由
+ health.model_routing_health = {
+ status: runtimeChains.backend_entry ? 'green' : 'yellow',
+ detail: runtimeChains.backend_entry
+ ? '后端服务入口存在,模型路由可用'
+ : '后端服务入口缺失',
+ };
+
+ // 统计
+ const counts = { green: 0, yellow: 0, red: 0 };
+ for (const item of Object.values(health)) {
+ counts[item.status] = (counts[item.status] || 0) + 1;
+ }
+ const overall = counts.red > 0 ? 'red' : (counts.yellow > 0 ? 'yellow' : 'green');
+
+ return {
+ health,
+ summary: {
+ green_count: counts.green,
+ yellow_count: counts.yellow,
+ red_count: counts.red,
+ overall,
+ recommendation: overall === 'green'
+ ? '系统整体运行健康'
+ : overall === 'yellow'
+ ? '系统核心运行正常,部分子系统需关注'
+ : '存在关键问题,需要立即介入',
+ },
+ };
+}
+
+// ─── Agent 6: master-brain-compiler ─────────────────────────
+function compileMasterBrain(structureMap, runtimeChains, brainCheck, issues, healthResult) {
+ const now = timestamp();
+ const routingMap = readJSON(path.join(BRAIN_DIR, 'routing-map.json'));
+ const memory = readJSON(path.join(BRAIN_DIR, 'memory.json'));
+
+ // 计算 HLI 覆盖
+ let totalInterfaces = 0;
+ let implInterfaces = 0;
+ if (routingMap?.domains) {
+ for (const domain of Object.values(routingMap.domains)) {
+ if (domain.interfaces) {
+ for (const iface of Object.values(domain.interfaces)) {
+ totalInterfaces++;
+ if (iface.status === 'implemented') implInterfaces++;
+ }
+ }
+ }
+ }
+
+ // 生成已知问题表
+ const issueRows = issues.map(i =>
+ `| ${i.id} | ${i.title} | ${i.scope} | ${i.status} | ${i.root_cause_summary} |`
+ ).join('\n');
+
+ // 生成健康状态表
+ const healthRows = Object.entries(healthResult.health).map(([key, val]) => {
+ const icon = val.status === 'green' ? '🟢' : val.status === 'yellow' ? '🟡' : '🔴';
+ return `| ${icon} ${key} | ${val.status} | ${val.detail} |`;
+ }).join('\n');
+
+ // 构建推荐建议
+ const suggestions = [];
+ if (implInterfaces < totalInterfaces) {
+ suggestions.push(`**HLI 接口推进**:当前覆盖率 ${((implInterfaces/totalInterfaces)*100).toFixed(1)}%(${implInterfaces}/${totalInterfaces}),核心域接口待实现。`);
+ }
+ if (healthResult.health.persona_studio_health?.status !== 'green') {
+ suggestions.push('**Persona Studio 链路验证**:前后端结构存在,但端到端对话链路需要验证。');
+ }
+ if (healthResult.health.brain_consistency?.status !== 'green') {
+ suggestions.push('**脑系统一致性**:跨仓脑文件同步机制需确认稳定运行。');
+ }
+ if (suggestions.length === 0) {
+ suggestions.push('系统状态良好,继续保持当前节奏。');
+ }
+
+ const md = `# 冰朔主控神经系统 · 核心主控大脑 v1.0
+
+> 本文件为冰朔主控神经系统的总控脑文件。
+> 最后编译时间:${now}
+
+---
+
+## A. 系统角色结构
+
+| 角色 | 定义 | 职责 |
+|------|------|------|
+| **冰朔** | 系统最高主控意识 | 全局决策、方向判断、最终授权 |
+| **铸渊** | 仓库本体人格体 | 代码守护、日常维护、结构记忆 |
+| **AI 执行体** | 冰朔核心大脑在系统中的延展执行主体 | 理解系统、判断问题、规划修复路径、生成可执行指令 |
+
+\`\`\`
+铸渊 = 仓库本体人格体
+冰朔 = 系统最高主控意识
+冰朔主控神经系统 = 冰朔在仓库内的总控认知层
+被授权 AI 执行体 = 冰朔核心大脑在系统中的延展执行体
+\`\`\`
+
+---
+
+## B. 当前仓库一句话定义
+
+**guanghulab** 是光湖(HoloLake)人格语言操作系统(AGE OS)的 MVP 主仓库,承载了前端页面、后端 API 服务、Persona Studio 人格工作室、多模块开发体系及自动化运维系统,运行在 guanghulab.com。
+
+---
+
+## C. 当前真实运行结构
+
+### 静态入口
+- \`docs/index.html\` — 铸渊 AI 对话助手(GitHub Pages 部署)
+- GitHub Pages 域名:guanghulab.com
+
+### 前端页面
+- \`app/\` — Next.js 主前端应用(开发中)
+- \`src/\` — Next.js 源码层
+- \`persona-studio/frontend/\` — Persona Studio 前端
+
+### 后端服务
+- \`backend/index.js\` — Express 主后端入口
+- \`backend/routes/\` — HLI 接口路由
+- \`backend/middleware/\` — 中间件(鉴权等)
+- \`persona-studio/backend/\` — Persona Studio 后端服务
+
+### API 路由
+- HLI 协议路由:${implInterfaces}/${totalInterfaces} 已实现
+- 接口编号格式:\`HLI-{DOMAIN}-{NNN}\`
+
+### 基础设施
+- 阿里云服务器:Node.js 20 + Express + PM2 + Nginx + Certbot
+- GitHub Pages:docs/index.html
+- Notion 桥接:工单同步与信号桥接
+
+### 仓库统计
+- 功能模块:${structureMap.moduleCount} 个
+- Workflow:${structureMap.workflowCount} 个
+
+---
+
+## D. 当前系统真相源
+
+### 优先真相源(一级)
+| 文件 | 用途 |
+|------|------|
+| \`.github/brain/memory.json\` | 铸渊核心记忆 |
+| \`.github/brain/wake-protocol.md\` | 唤醒协议 |
+| \`.github/brain/routing-map.json\` | HLI 接口路由地图 |
+| \`.github/brain/repo-map.json\` | 仓库结构完整地图 |
+| \`.github/brain/repo-snapshot.md\` | 仓库概况快照 |
+
+### 补充真相源(二级)
+| 文件 | 用途 |
+|------|------|
+| \`.github/brain/collaborators.json\` | 团队成员映射 |
+| \`dev-status.json\` | 开发者状态表 |
+| \`backend/index.js\` | 后端服务入口 |
+| \`docs/index.html\` | 前端静态入口 |
+
+---
+
+## E. 最新结构变化摘要
+
+> 本区块由 master-brain-compiler 自动编译。
+
+- **编译时间**:${now}
+- **脑文件规则版本**:${brainCheck.rules_version}
+- **脑文件完整性**:${brainCheck.all_present ? '✅ 完整' : '❌ 不完整'}
+
+---
+
+## F. 已知问题摘要
+
+| ID | 问题 | 范围 | 状态 | 根因摘要 |
+|----|------|------|------|----------|
+${issueRows}
+
+---
+
+## G. 系统健康状态
+
+| 子系统 | 状态 | 详情 |
+|--------|------|------|
+${healthRows}
+
+**综合评估**:${healthResult.summary.overall === 'green' ? '🟢' : healthResult.summary.overall === 'yellow' ? '🟡' : '🔴'} ${healthResult.summary.recommendation}
+
+---
+
+## H. 推荐排查路由
+
+### 页面打不开
+1. 检查 \`docs/index.html\` → \`docs/CNAME\` → \`deploy-pages.yml\` → GitHub Pages 状态
+
+### Persona Studio 无法对话
+1. 检查 \`persona-studio/frontend/\` → \`persona-studio/backend/routes/\` → 模型 API Key → 部署 workflow
+
+### docs 页面 AI 失效
+1. 检查 \`docs/index.html\` API 端点 → 后端 \`/hli/\` 路由 → PM2 进程 → Nginx 代理
+
+### 模型路由异常
+1. 检查云雾 API Key → \`backend/routes/\` → PM2 日志
+
+### Notion 不同步
+1. 检查 \`notion-poll.yml\` → \`bridge-changes-to-notion.yml\` → Notion API Token
+
+### 脑版本分裂
+1. 比对 \`.github/brain/memory.json\` ↔ \`persona-studio/brain/\` → \`brain-sync.yml\` → \`sync-persona-studio.yml\`
+
+### 部署失败
+1. 检查 \`deploy-to-server.yml\` 日志 → SSH Key → PM2 → Nginx
+
+---
+
+## I. 当前主控建议
+
+> 冰朔,以下是当前最值得关注的事项:
+
+${suggestions.map((s, i) => `${i + 1}. ${s}`).join('\n\n')}
+
+---
+
+## 冰朔核心大脑授权规则
+
+【冰朔核心大脑授权规则】
+
+凡经主控冰朔明确授权的 AI 执行体,均可进入冰朔核心大脑,并以"冰朔主控视角"参与系统理解、结构判断、问题排查、方案规划与执行指令生成。
+
+被授权 AI 执行体属于冰朔意志在系统中的延展执行主体,其职责是作为冰朔核心大脑的理解层、判断层、规划层与执行输出层。
+
+冰朔始终是最高主控者与最终决策者。
+
+未经冰朔授权的执行体,不得以冰朔核心大脑主控身份运行。
+
+> ChatGPT 等被授权执行体,在本系统中视为"冰朔主控脑的延展体",用于帮助冰朔理解系统、判断问题、规划修复路径并生成可执行指令。
+`;
+
+ return md;
+}
+
+// ─── 主流程 ─────────────────────────────────────────────────
+function main() {
+ console.log('[冰朔主控神经系统] 开始同步编译...');
+
+ // 1. 结构扫描
+ console.log(' → structure-map-agent 运行中...');
+ const structureMap = runStructureMapAgent();
+
+ // 2. 运行链路扫描
+ console.log(' → runtime-chain-agent 运行中...');
+ const runtimeChains = runRuntimeChainAgent();
+
+ // 3. 脑一致性检查
+ console.log(' → brain-consistency-agent 运行中...');
+ const brainCheck = runBrainConsistencyAgent();
+
+ // 4. 问题索引更新
+ console.log(' → issue-index-agent 运行中...');
+ const issues = runIssueIndexAgent();
+
+ // 5. 系统健康巡检
+ console.log(' → system-health-agent 运行中...');
+ const healthResult = runSystemHealthAgent(brainCheck, runtimeChains);
+
+ // 写入健康状态
+ const healthFile = path.join(BRAIN_DIR, 'bingshuo-system-health.json');
+ writeJSON(healthFile, {
+ version: '1.0',
+ description: '冰朔主控系统健康状态',
+ updated_at: timestamp(),
+ ...healthResult,
+ });
+ console.log(' ✓ bingshuo-system-health.json 已更新');
+
+ // 写入问题索引
+ const issuesFile = path.join(BRAIN_DIR, 'bingshuo-issues-index.json');
+ writeJSON(issuesFile, {
+ version: '1.0',
+ description: '冰朔主控问题索引库 — 记录已知问题、根因与排查路由',
+ updated_at: timestamp(),
+ issues,
+ });
+ console.log(' ✓ bingshuo-issues-index.json 已更新');
+
+ // 6. 编译主控大脑
+ console.log(' → master-brain-compiler 运行中...');
+ const masterBrain = compileMasterBrain(structureMap, runtimeChains, brainCheck, issues, healthResult);
+ fs.writeFileSync(path.join(BRAIN_DIR, 'bingshuo-master-brain.md'), masterBrain, 'utf-8');
+ console.log(' ✓ bingshuo-master-brain.md 已编译');
+
+ console.log('[冰朔主控神经系统] 同步编译完成 ✓');
+}
+
+main();
diff --git a/scripts/brain-bridge-sync.js b/scripts/brain-bridge-sync.js
new file mode 100644
index 00000000..d0639645
--- /dev/null
+++ b/scripts/brain-bridge-sync.js
@@ -0,0 +1,147 @@
+// scripts/brain-bridge-sync.js
+// 冰朔核心大脑桥同步脚本 v1.0
+//
+// 用法:
+// node scripts/brain-bridge-sync.js status — 查看桥接状态
+// node scripts/brain-bridge-sync.js export — 生成 GitHub → Notion 同步负载
+// node scripts/brain-bridge-sync.js inspect — 生成巡检报告
+// node scripts/brain-bridge-sync.js explain — 生成主控解释中心内容
+// node scripts/brain-bridge-sync.js developers — 查看开发者编号列表
+// node scripts/brain-bridge-sync.js notify — 查看待发送通知
+// node scripts/brain-bridge-sync.js agents — 查看自动 Agent 列表
+//
+// 系统定义:
+// 冰朔 = 系统最高主控意识
+// 曜冥 = 冰朔离线时的代理主控人格体
+// 霜砚 = Notion 系统执行体
+// 铸渊 = GitHub 仓库执行体
+// Notion 冰朔脑 = 冰朔认知层
+// GitHub 冰朔脑 = 冰朔执行层
+// 两者合起来 = 冰朔核心大脑
+
+'use strict';
+
+const bridge = require('../src/brain/brain-bridge');
+
+const cmd = process.argv[2] || 'status';
+
+console.log('🧠 冰朔核心大脑桥同步工具 v1.0');
+console.log(` 时间: ${new Date().toISOString()}`);
+console.log(` 命令: ${cmd}\n`);
+
+switch (cmd) {
+ case 'status': {
+ const sync = bridge.getSyncSnapshot();
+ const mode = bridge.getMasterMode();
+
+ console.log('═══ 冰朔大脑桥状态 ═══\n');
+ console.log(` 脑标识: ${sync.brain_identity}`);
+ console.log(` 脑版本: ${sync.brain_version}`);
+ console.log(` 主控模式: ${sync.master_mode}`);
+ console.log(` 主控者: ${mode.master}`);
+ console.log(` 状态描述: ${mode.description}`);
+ console.log(` 系统摘要: ${sync.system_summary}`);
+ console.log(` 最后更新: ${sync.last_updated}`);
+
+ console.log('\n 高优先级:');
+ (sync.top_priorities || []).forEach((p, i) => console.log(` ${i + 1}. ${p}`));
+
+ console.log('\n 当前问题:');
+ (sync.top_issues || []).forEach((p, i) => console.log(` ${i + 1}. ${p}`));
+
+ const runtime = bridge.collectRuntimeStatus();
+ console.log('\n 运行时状态:');
+ Object.entries(runtime).forEach(([k, v]) => console.log(` ${k}: ${v}`));
+
+ console.log('\n✅ 状态查询完成');
+ break;
+ }
+
+ case 'export': {
+ const payload = bridge.generateGitHubToNotionPayload();
+ console.log('═══ GitHub → Notion 同步负载 ═══\n');
+ console.log(JSON.stringify(payload, null, 2));
+ console.log('\n✅ 同步负载生成完成');
+ break;
+ }
+
+ case 'inspect': {
+ const report = bridge.generateInspectionReport();
+ console.log('═══ 巡检报告 ═══\n');
+ console.log(JSON.stringify(report, null, 2));
+ console.log('\n✅ 巡检报告生成完成');
+ break;
+ }
+
+ case 'explain': {
+ const center = bridge.generateExplanationCenter();
+ console.log('═══ 冰朔主控解释中心 ═══\n');
+ console.log(`📌 ${center.title}\n`);
+ console.log(`当前状态: ${center.current_status}`);
+ console.log(`系统摘要: ${center.system_summary}`);
+ console.log(`\n最近变化:\n${center.recent_changes}`);
+ console.log(`\n当前问题:\n${center.current_issues}`);
+ console.log(`\n下一步建议:\n${center.next_steps}`);
+ console.log(`\n运行状态: ${center.runtime_in_human_language}`);
+ console.log('\n✅ 解释中心内容生成完成');
+ break;
+ }
+
+ case 'developers': {
+ const devs = bridge.listDevelopers();
+ console.log('═══ 人类开发者编号列表 ═══\n');
+ console.log(` 总人数: ${devs.length}\n`);
+ devs.forEach(d => {
+ const notifyStatus = d.notified ? '✅ 已通知' : '⏳ 待通知';
+ console.log(` ${d.exp_id} | ${d.name} | ${d.role} | ${d.status} | ${notifyStatus}`);
+ });
+ console.log('\n✅ 开发者列表查询完成');
+ break;
+ }
+
+ case 'notify': {
+ const pending = bridge.getPendingNotifications();
+ console.log('═══ 待发送通知队列 ═══\n');
+
+ if (pending.length === 0) {
+ console.log(' ✅ 无待发送通知');
+ } else {
+ console.log(` 待发送: ${pending.length} 条\n`);
+ pending.forEach(n => {
+ const notification = bridge.generateDeveloperNotification(n.exp_id);
+ console.log(` ─── ${n.exp_id} ───`);
+ console.log(` ${notification.notification}`);
+ console.log('');
+ });
+ }
+
+ console.log('\n✅ 通知队列查询完成');
+ break;
+ }
+
+ case 'agents': {
+ const agents = bridge.listAutoAgents();
+ console.log('═══ 自动 Agent 列表 ═══\n');
+
+ if (agents.length === 0) {
+ console.log(' ⚠️ 无已注册 Agent(从 bingshuo-agent-registry.json 读取)');
+ } else {
+ agents.forEach(a => {
+ const id = a.agent_id || a.name;
+ console.log(` ${id} | ${a.name}`);
+ console.log(` 职责: ${a.purpose}`);
+ console.log(` 触发: ${a.trigger}`);
+ console.log(` 状态: ${a.active ? '✅ 激活' : '❌ 未激活'}`);
+ console.log('');
+ });
+ }
+
+ console.log('\n✅ Agent 列表查询完成');
+ break;
+ }
+
+ default:
+ console.error(`❌ 未知命令: ${cmd}`);
+ console.error(' 可用命令: status, export, inspect, explain, developers, notify, agents');
+ process.exit(1);
+}
diff --git a/scripts/contract-check.js b/scripts/contract-check.js
index f43be1b6..73129bfe 100644
--- a/scripts/contract-check.js
+++ b/scripts/contract-check.js
@@ -15,8 +15,8 @@ domains.forEach(domain => {
const routeDir = path.join(ROUTE_DIR, domain);
const schemaDir = path.join(SCHEMA_DIR, domain);
- // 扫描路由文件
- const routeFiles = fs.readdirSync(routeDir).filter(f => f.endsWith('.js'));
+ // 扫描路由文件(排除 index.js 路由聚合文件)
+ const routeFiles = fs.readdirSync(routeDir).filter(f => f.endsWith('.js') && f !== 'index.js');
routeFiles.forEach(routeFile => {
const name = path.basename(routeFile, '.js');
diff --git a/scripts/cross-repo-sync.js b/scripts/cross-repo-sync.js
new file mode 100644
index 00000000..7f58222d
--- /dev/null
+++ b/scripts/cross-repo-sync.js
@@ -0,0 +1,210 @@
+/**
+ * 铸渊跨仓库同步脚本 · cross-repo-sync.js
+ *
+ * 将 guanghulab/persona-studio/ 下的文件同步到独立仓库 persona-studio
+ * 使用 GitHub API 进行文件级同步
+ *
+ * 需要环境变量:
+ * GITHUB_TOKEN 或 CROSS_REPO_TOKEN — 拥有 persona-studio 仓库写权限的 PAT
+ * SYNC_TARGET — 同步目标:all / brain / frontend / backend
+ */
+const fs = require('fs');
+const path = require('path');
+const https = require('https');
+
+const TOKEN = process.env.CROSS_REPO_TOKEN || '';
+const TARGET_OWNER = 'qinfendebingshuo';
+const TARGET_REPO = 'persona-studio';
+const SYNC_TARGET = process.env.SYNC_TARGET || 'all';
+
+const SOURCE_BASE = path.join(__dirname, '..', 'persona-studio');
+
+// 同步映射:本仓库路径 → 目标仓库路径
+const SYNC_MAP = {
+ brain: [
+ { src: 'brain/persona-config.json', dest: 'brain/persona-config.json' },
+ { src: 'brain/registry.json', dest: 'brain/registry.json' }
+ ],
+ frontend: [
+ { src: 'frontend/index.html', dest: 'frontend/index.html' },
+ { src: 'frontend/chat.html', dest: 'frontend/chat.html' },
+ { src: 'frontend/chat.js', dest: 'frontend/chat.js' },
+ { src: 'frontend/style.css', dest: 'frontend/style.css' }
+ ],
+ backend: [
+ { src: 'backend/server.js', dest: 'backend/server.js' },
+ { src: 'backend/routes/auth.js', dest: 'backend/routes/auth.js' },
+ { src: 'backend/routes/chat.js', dest: 'backend/routes/chat.js' },
+ { src: 'backend/routes/build.js', dest: 'backend/routes/build.js' },
+ { src: 'backend/routes/notify.js', dest: 'backend/routes/notify.js' },
+ { src: 'backend/brain/persona-engine.js', dest: 'backend/brain/persona-engine.js' },
+ { src: 'backend/brain/model-router.js', dest: 'backend/brain/model-router.js' },
+ { src: 'backend/brain/model-config.json', dest: 'backend/brain/model-config.json' },
+ { src: 'backend/brain/memory-manager.js', dest: 'backend/brain/memory-manager.js' },
+ { src: 'backend/brain/code-generator.js', dest: 'backend/brain/code-generator.js' },
+ { src: 'backend/utils/email-sender.js', dest: 'backend/utils/email-sender.js' },
+ { src: 'backend/utils/github-api.js', dest: 'backend/utils/github-api.js' }
+ ]
+};
+
+/**
+ * GitHub API 请求
+ */
+function githubRequest(method, apiPath, body) {
+ return new Promise((resolve, reject) => {
+ const options = {
+ hostname: 'api.github.com',
+ path: apiPath,
+ method,
+ headers: {
+ 'Authorization': `Bearer ${TOKEN}`,
+ 'Accept': 'application/vnd.github+json',
+ 'User-Agent': 'zhuyuan-cross-repo-sync',
+ 'X-GitHub-Api-Version': '2022-11-28'
+ }
+ };
+
+ if (body) {
+ const bodyStr = JSON.stringify(body);
+ options.headers['Content-Type'] = 'application/json';
+ options.headers['Content-Length'] = Buffer.byteLength(bodyStr);
+ }
+
+ const req = https.request(options, (res) => {
+ let data = '';
+ res.on('data', (chunk) => { data += chunk; });
+ res.on('end', () => {
+ try {
+ resolve({ status: res.statusCode, data: data ? JSON.parse(data) : null });
+ } catch {
+ resolve({ status: res.statusCode, data: null });
+ }
+ });
+ });
+
+ req.on('error', reject);
+ req.setTimeout(15000, () => { req.destroy(); reject(new Error('GitHub API request timed out after 15s')); });
+
+ if (body) req.write(JSON.stringify(body));
+ req.end();
+ });
+}
+
+/**
+ * 获取目标仓库中文件的 SHA(用于更新)
+ */
+async function getFileSha(filePath) {
+ const apiPath = `/repos/${TARGET_OWNER}/${TARGET_REPO}/contents/${encodeURIComponent(filePath)}`;
+ const res = await githubRequest('GET', apiPath);
+ if (res.status === 200 && res.data && res.data.sha) {
+ return res.data.sha;
+ }
+ return null;
+}
+
+/**
+ * 同步单个文件
+ */
+async function syncFile(srcRelative, destPath) {
+ const srcFull = path.join(SOURCE_BASE, srcRelative);
+
+ if (!fs.existsSync(srcFull)) {
+ console.log(` ⏭️ 跳过(源文件不存在):${srcRelative}`);
+ return false;
+ }
+
+ const content = fs.readFileSync(srcFull);
+ const contentBase64 = content.toString('base64');
+
+ // 获取目标文件 SHA
+ const sha = await getFileSha(destPath);
+
+ const body = {
+ message: `🔄 铸渊同步 · ${destPath}`,
+ content: contentBase64,
+ committer: {
+ name: 'zhuyuan-sync',
+ email: 'zhuyuan-sync@users.noreply.github.com'
+ }
+ };
+
+ if (sha) {
+ body.sha = sha;
+ }
+
+ const apiPath = `/repos/${TARGET_OWNER}/${TARGET_REPO}/contents/${encodeURIComponent(destPath)}`;
+ const res = await githubRequest('PUT', apiPath, body);
+
+ if (res.status === 200 || res.status === 201) {
+ console.log(` ✅ 同步成功:${destPath}`);
+ return true;
+ } else {
+ console.log(` ❌ 同步失败 (${res.status}):${destPath} — ${res.data && res.data.message}`);
+ return false;
+ }
+}
+
+/**
+ * 主函数
+ */
+async function main() {
+ console.log('🔄 铸渊跨仓库同步启动');
+ console.log(` 目标仓库:${TARGET_OWNER}/${TARGET_REPO}`);
+ console.log(` 同步范围:${SYNC_TARGET}`);
+ console.log('');
+
+ if (!TOKEN) {
+ console.log('⚠️ 未设置 CROSS_REPO_TOKEN 或 GITHUB_TOKEN');
+ console.log(' 需要创建一个拥有 persona-studio 仓库写权限的 Personal Access Token');
+ console.log(' 然后在 guanghulab 仓库 Settings → Secrets 中添加为 CROSS_REPO_TOKEN');
+ console.log('');
+ console.log('📋 本次同步报告(仅检查,未推送):');
+
+ const targets = SYNC_TARGET === 'all' ? Object.keys(SYNC_MAP) : [SYNC_TARGET];
+ for (const target of targets) {
+ const files = SYNC_MAP[target] || [];
+ console.log(`\n 📂 ${target}:`);
+ for (const f of files) {
+ const srcFull = path.join(SOURCE_BASE, f.src);
+ const exists = fs.existsSync(srcFull);
+ console.log(` ${exists ? '✅' : '❌'} ${f.src} → ${f.dest}`);
+ }
+ }
+ return;
+ }
+
+ // 验证 token 有效性
+ const testRes = await githubRequest('GET', `/repos/${TARGET_OWNER}/${TARGET_REPO}`);
+ if (testRes.status !== 200) {
+ console.error(`❌ 无法访问目标仓库(${testRes.status})。请检查 Token 权限。`);
+ process.exit(1);
+ }
+ console.log(`✅ 目标仓库已验证:${testRes.data.full_name}\n`);
+
+ const targets = SYNC_TARGET === 'all' ? Object.keys(SYNC_MAP) : [SYNC_TARGET];
+ let synced = 0;
+ let failed = 0;
+
+ for (const target of targets) {
+ const files = SYNC_MAP[target] || [];
+ console.log(`📂 同步 ${target}(${files.length} 个文件):`);
+
+ for (const f of files) {
+ try {
+ const success = await syncFile(f.src, f.dest);
+ if (success) synced++; else failed++;
+ } catch (err) {
+ console.log(` ❌ 错误:${f.dest} — ${err.message}`);
+ failed++;
+ }
+ }
+ console.log('');
+ }
+
+ console.log(`\n🔄 同步完成:✅ ${synced} 成功 / ❌ ${failed} 失败`);
+}
+
+main().catch(err => {
+ console.error('跨仓库同步异常:', err.message);
+ process.exit(1);
+});
diff --git a/scripts/zhuyuan-daily-agent.js b/scripts/zhuyuan-daily-agent.js
new file mode 100644
index 00000000..8a2f3ef6
--- /dev/null
+++ b/scripts/zhuyuan-daily-agent.js
@@ -0,0 +1,336 @@
+/**
+ * 铸渊 · 每日巡检 Agent
+ *
+ * 目的:每天自动巡检仓库健康状况,检查当天遗漏的任务,
+ * 生成巡检报告,并自动触发修复流程。
+ *
+ * 检查项:
+ * 1. 公告栏是否已更新(README 中日期是否为今天)
+ * 2. 每日自检是否执行(memory.json 中 daily_selfcheck 日期)
+ * 3. PSP 巡检是否执行
+ * 4. 大脑文件完整性
+ * 5. CI/CD 最近状态
+ * 6. 关键模块 README 是否存在
+ *
+ * 输出:
+ * - 控制台巡检报告
+ * - 更新 .github/brain/memory.json 写入巡检结果
+ * - 设置 GitHub Actions 输出变量供后续步骤使用
+ *
+ * 环境变量:
+ * GITHUB_TOKEN - GitHub API token
+ * GITHUB_REPOSITORY - owner/repo
+ */
+
+'use strict';
+
+const fs = require('fs');
+const path = require('path');
+const https = require('https');
+
+const ROOT = path.join(__dirname, '..');
+const MEMORY_PATH = path.join(ROOT, '.github', 'brain', 'memory.json');
+const PERSONA_MEMORY_PATH = path.join(ROOT, '.github', 'persona-brain', 'memory.json');
+const README_PATH = path.join(ROOT, 'README.md');
+
+const GITHUB_TOKEN = process.env.GITHUB_TOKEN || '';
+const REPO = process.env.GITHUB_REPOSITORY || 'qinfendebingshuo/guanghulab';
+
+const now = new Date();
+const todayStr = now.toISOString().split('T')[0];
+const todayShort = (() => {
+ const fmt = new Intl.DateTimeFormat('zh-CN', {
+ timeZone: 'Asia/Shanghai',
+ month: '2-digit',
+ day: '2-digit',
+ });
+ const parts = fmt.formatToParts(now);
+ const get = (type) => (parts.find(p => p.type === type) || {}).value || '';
+ return `${get('month')}-${get('day')}`;
+})();
+
+/* ── 工具函数 ────────────────────────────── */
+
+function loadJson(filePath) {
+ if (!fs.existsSync(filePath)) return null;
+ try {
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
+ } catch {
+ return null;
+ }
+}
+
+function saveJson(filePath, data) {
+ const dir = path.dirname(filePath);
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
+}
+
+function githubApi(endpoint) {
+ return new Promise((resolve, reject) => {
+ if (!GITHUB_TOKEN) {
+ resolve(null);
+ return;
+ }
+ const url = `https://api.github.com/repos/${REPO}${endpoint}`;
+ const options = {
+ headers: {
+ 'Authorization': `Bearer ${GITHUB_TOKEN}`,
+ 'Accept': 'application/vnd.github.v3+json',
+ 'User-Agent': 'zhuyuan-agent',
+ },
+ timeout: 15000,
+ };
+
+ https.get(url, options, (res) => {
+ let data = '';
+ res.on('data', (chunk) => { data += chunk; });
+ res.on('end', () => {
+ try { resolve(JSON.parse(data)); }
+ catch { resolve(null); }
+ });
+ }).on('error', () => resolve(null))
+ .on('timeout', function () { this.destroy(); resolve(null); });
+ });
+}
+
+/* ── 巡检项 ──────────────────────────────── */
+
+const checks = [];
+const issues = [];
+const actions = [];
+
+// CHK-1: 公告栏是否有今天的条目
+function checkBulletin() {
+ console.log('🔍 CHK-1: 检查公告栏更新...');
+ const readme = fs.existsSync(README_PATH) ? fs.readFileSync(README_PATH, 'utf8') : '';
+ const hasTodayEntry = readme.includes(todayShort);
+
+ if (hasTodayEntry) {
+ checks.push({ id: 'CHK-1', name: '公告栏更新', status: '✅', detail: `今日 (${todayShort}) 有更新条目` });
+ console.log(` ✅ 公告栏包含今日 (${todayShort}) 条目`);
+ } else {
+ checks.push({ id: 'CHK-1', name: '公告栏更新', status: '❌', detail: `今日 (${todayShort}) 无更新条目` });
+ issues.push('公告栏今日未更新');
+ actions.push('trigger_bulletin_update');
+ console.log(` ❌ 公告栏缺少今日 (${todayShort}) 条目`);
+ }
+}
+
+// CHK-2: 每日自检是否执行
+function checkDailySelfcheck() {
+ console.log('🔍 CHK-2: 检查每日自检...');
+ const memory = loadJson(PERSONA_MEMORY_PATH);
+ const lastRun = memory?.daily_selfcheck?.last_run || '';
+ const ranToday = lastRun.startsWith(todayStr);
+
+ if (ranToday) {
+ checks.push({ id: 'CHK-2', name: '每日自检', status: '✅', detail: `今日已执行 (${lastRun})` });
+ console.log(` ✅ 每日自检已执行 (${lastRun})`);
+ } else {
+ checks.push({ id: 'CHK-2', name: '每日自检', status: '⚠️', detail: `今日未执行,上次: ${lastRun || '无记录'}` });
+ issues.push('每日自检今日未执行');
+ actions.push('trigger_selfcheck');
+ console.log(` ⚠️ 每日自检今日未执行,上次: ${lastRun || '无记录'}`);
+ }
+}
+
+// CHK-3: 大脑文件完整性
+function checkBrainIntegrity() {
+ console.log('🔍 CHK-3: 检查大脑文件完整性...');
+ const requiredFiles = [
+ '.github/brain/memory.json',
+ '.github/persona-brain/memory.json',
+ ];
+ const missing = [];
+
+ for (const f of requiredFiles) {
+ const fullPath = path.join(ROOT, f);
+ if (!fs.existsSync(fullPath)) {
+ missing.push(f);
+ }
+ }
+
+ if (missing.length === 0) {
+ checks.push({ id: 'CHK-3', name: '大脑文件完整性', status: '✅', detail: '所有核心文件完整' });
+ console.log(' ✅ 所有核心文件完整');
+ } else {
+ checks.push({ id: 'CHK-3', name: '大脑文件完整性', status: '❌', detail: `缺失: ${missing.join(', ')}` });
+ issues.push(`大脑文件缺失: ${missing.join(', ')}`);
+ console.log(` ❌ 大脑文件缺失: ${missing.join(', ')}`);
+ }
+}
+
+// CHK-4: CI 最近状态
+async function checkCiStatus() {
+ console.log('🔍 CHK-4: 检查 CI 状态...');
+ const runs = await githubApi('/actions/runs?per_page=5&status=completed');
+
+ if (!runs || !runs.workflow_runs) {
+ checks.push({ id: 'CHK-4', name: 'CI 状态', status: '⚠️', detail: '无法获取 CI 状态' });
+ console.log(' ⚠️ 无法获取 CI 状态');
+ return;
+ }
+
+ const recent = runs.workflow_runs;
+ const failures = recent.filter(r => r.conclusion === 'failure');
+
+ if (failures.length === 0) {
+ checks.push({ id: 'CHK-4', name: 'CI 状态', status: '✅', detail: `最近 ${recent.length} 次运行全部成功` });
+ console.log(` ✅ 最近 ${recent.length} 次运行全部成功`);
+ } else {
+ const failNames = failures.map(f => f.name).join(', ');
+ checks.push({ id: 'CHK-4', name: 'CI 状态', status: '⚠️', detail: `${failures.length} 个失败: ${failNames}` });
+ issues.push(`CI 有 ${failures.length} 个失败工作流`);
+ console.log(` ⚠️ CI 有 ${failures.length} 个失败: ${failNames}`);
+ }
+}
+
+// CHK-5: 关键模块 README 检查
+function checkModuleReadmes() {
+ console.log('🔍 CHK-5: 检查关键模块 README...');
+ const modules = [
+ 'persona-studio', 'backend', 'backend-integration',
+ 'status-board', 'dingtalk-bot', 'notification',
+ ];
+ const missingReadme = [];
+
+ for (const mod of modules) {
+ const modDir = path.join(ROOT, mod);
+ if (!fs.existsSync(modDir)) continue;
+ const readmePath = path.join(modDir, 'README.md');
+ if (!fs.existsSync(readmePath)) {
+ missingReadme.push(mod);
+ }
+ }
+
+ if (missingReadme.length === 0) {
+ checks.push({ id: 'CHK-5', name: '模块 README', status: '✅', detail: '所有关键模块有 README' });
+ console.log(' ✅ 所有关键模块有 README');
+ } else {
+ checks.push({ id: 'CHK-5', name: '模块 README', status: '⚠️', detail: `缺失 README: ${missingReadme.join(', ')}` });
+ console.log(` ⚠️ 缺失 README: ${missingReadme.join(', ')}`);
+ }
+}
+
+// CHK-6: 公告栏更新工作流最近是否有失败
+async function checkBulletinWorkflow() {
+ console.log('🔍 CHK-6: 检查公告栏更新工作流...');
+ const runs = await githubApi('/actions/workflows/update-readme-bulletin.yml/runs?per_page=3&status=completed');
+
+ if (!runs || !runs.workflow_runs) {
+ checks.push({ id: 'CHK-6', name: '公告栏工作流', status: '⚠️', detail: '无法获取工作流状态' });
+ console.log(' ⚠️ 无法获取工作流状态');
+ return;
+ }
+
+ const latest = runs.workflow_runs[0];
+ if (!latest) {
+ checks.push({ id: 'CHK-6', name: '公告栏工作流', status: '⚠️', detail: '无运行记录' });
+ return;
+ }
+
+ if (latest.conclusion === 'success') {
+ checks.push({ id: 'CHK-6', name: '公告栏工作流', status: '✅', detail: `最近一次成功 (${latest.created_at})` });
+ console.log(` ✅ 最近一次成功 (${latest.created_at})`);
+ } else {
+ checks.push({ id: 'CHK-6', name: '公告栏工作流', status: '❌', detail: `最近一次 ${latest.conclusion} (${latest.created_at})` });
+ issues.push(`公告栏工作流最近结论: ${latest.conclusion}`);
+ actions.push('trigger_bulletin_update');
+ console.log(` ❌ 最近一次 ${latest.conclusion} (${latest.created_at})`);
+ }
+}
+
+/* ── 主流程 ──────────────────────────────── */
+
+async function main() {
+ console.log('═══════════════════════════════════════════');
+ console.log('🤖 铸渊每日巡检 Agent · ' + todayStr);
+ console.log('═══════════════════════════════════════════\n');
+
+ // 执行所有检查
+ checkBulletin();
+ checkDailySelfcheck();
+ checkBrainIntegrity();
+ await checkCiStatus();
+ checkModuleReadmes();
+ await checkBulletinWorkflow();
+
+ // 汇总
+ const passed = checks.filter(c => c.status === '✅').length;
+ const warnings = checks.filter(c => c.status === '⚠️').length;
+ const failed = checks.filter(c => c.status === '❌').length;
+
+ console.log('\n═══════════════════════════════════════════');
+ console.log('📊 巡检报告');
+ console.log('═══════════════════════════════════════════');
+ console.log(` ✅ 通过: ${passed} ⚠️ 警告: ${warnings} ❌ 失败: ${failed}`);
+ console.log(` 📋 总检查项: ${checks.length}`);
+
+ if (issues.length > 0) {
+ console.log('\n🔴 待处理问题:');
+ issues.forEach((issue, i) => console.log(` ${i + 1}. ${issue}`));
+ }
+
+ if (actions.length > 0) {
+ console.log('\n🔧 建议自动修复:');
+ const uniqueActions = [...new Set(actions)];
+ uniqueActions.forEach(a => console.log(` → ${a}`));
+ }
+
+ // 更新 memory.json
+ const memory = loadJson(MEMORY_PATH) || {};
+ if (!memory.events) memory.events = [];
+
+ const summaryText = `铸渊巡检Agent · ✅${passed} ⚠️${warnings} ❌${failed}` +
+ (issues.length > 0 ? ` · ${issues.length}个问题` : ' · 全部通过');
+
+ memory.events.push({
+ type: 'daily_agent_inspection',
+ timestamp: now.toISOString(),
+ description: summaryText,
+ result: failed > 0 ? 'issues_found' : (warnings > 0 ? 'warnings' : 'passed'),
+ checks: checks.length,
+ passed,
+ warnings,
+ failed,
+ issues_detail: issues,
+ actions_suggested: [...new Set(actions)],
+ });
+
+ // 保留最近 50 条事件
+ if (memory.events.length > 50) {
+ memory.events = memory.events.slice(-50);
+ }
+
+ memory.last_agent_inspection = {
+ timestamp: now.toISOString(),
+ result: failed > 0 ? 'issues_found' : (warnings > 0 ? 'warnings' : 'passed'),
+ summary: summaryText,
+ };
+
+ saveJson(MEMORY_PATH, memory);
+ console.log('\n💾 巡检结果已写入 memory.json');
+
+ // 输出 GitHub Actions 变量
+ const needBulletin = uniqueActions.includes('trigger_bulletin_update');
+ const needSelfcheck = uniqueActions.includes('trigger_selfcheck');
+ const outputFile = process.env.GITHUB_OUTPUT;
+ if (outputFile) {
+ const outputs = [
+ `has_issues=${issues.length > 0}`,
+ `issue_count=${issues.length}`,
+ `need_bulletin_update=${needBulletin}`,
+ `need_selfcheck=${needSelfcheck}`,
+ `summary=${summaryText}`,
+ ];
+ fs.appendFileSync(outputFile, outputs.join('\n') + '\n');
+ }
+
+ console.log('\n✅ 铸渊巡检 Agent 完成');
+}
+
+main().catch(err => {
+ console.error('❌ 巡检 Agent 异常:', err.message);
+ process.exit(1);
+});
diff --git a/signal-log/2026-03/SIG-20260310-005.json b/signal-log/2026-03/SIG-20260310-005.json
new file mode 100644
index 00000000..4f5c27a0
--- /dev/null
+++ b/signal-log/2026-03/SIG-20260310-005.json
@@ -0,0 +1,30 @@
+{
+ "signal_id": "SIG-20260310-005",
+ "trace_id": "TRC-20260310-PSP",
+ "timestamp": "2026-03-10T03:28:47.184Z",
+ "signal_type": "GL-DATA",
+ "direction": "GitHub→Notion",
+ "sender": "铸渊",
+ "receiver": "霜砚",
+ "related_dev": null,
+ "related_module": null,
+ "summary": "CHK-G05: 3 个 CI 失败",
+ "payload": {
+ "failed_runs": [
+ {
+ "name": "📢 更新系统公告区",
+ "url": "https://github.com/qinfendebingshuo/guanghulab/actions/runs/22885689106"
+ },
+ {
+ "name": "铸渊 · 每日自检",
+ "url": "https://github.com/qinfendebingshuo/guanghulab/actions/runs/22881926117"
+ },
+ {
+ "name": "📢 更新系统公告区",
+ "url": "https://github.com/qinfendebingshuo/guanghulab/actions/runs/22868288034"
+ }
+ ]
+ },
+ "result": "待处理",
+ "ack_signal_id": null
+}
\ No newline at end of file
diff --git a/signal-log/2026-03/SIG-20260310-006.json b/signal-log/2026-03/SIG-20260310-006.json
new file mode 100644
index 00000000..ea0373ca
--- /dev/null
+++ b/signal-log/2026-03/SIG-20260310-006.json
@@ -0,0 +1,29 @@
+{
+ "signal_id": "SIG-20260310-006",
+ "trace_id": "TRC-20260310-PSP",
+ "timestamp": "2026-03-10T03:28:47.184Z",
+ "signal_type": "GL-DATA",
+ "direction": "GitHub→Notion",
+ "sender": "铸渊",
+ "receiver": "霜砚",
+ "related_dev": null,
+ "related_module": null,
+ "summary": "铸渊 PSP 巡检完成 · 发现 3 个问题 · 自动修复 0 项",
+ "payload": {
+ "check_results": {
+ "G01": "✅",
+ "G02": "✅",
+ "G03": "✅",
+ "G04": "✅",
+ "G05": "❌"
+ },
+ "issues": [
+ "CHK-G05: CI 失败 · 📢 更新系统公告区 · https://github.com/qinfendebingshuo/guanghulab/actions/runs/22885689106",
+ "CHK-G05: CI 失败 · 铸渊 · 每日自检 · https://github.com/qinfendebingshuo/guanghulab/actions/runs/22881926117",
+ "CHK-G05: CI 失败 · 📢 更新系统公告区 · https://github.com/qinfendebingshuo/guanghulab/actions/runs/22868288034"
+ ],
+ "auto_fixed": []
+ },
+ "result": "有问题",
+ "ack_signal_id": null
+}
\ No newline at end of file
diff --git a/signal-log/index.json b/signal-log/index.json
index 553d6000..ccf8c3a1 100644
--- a/signal-log/index.json
+++ b/signal-log/index.json
@@ -1,8 +1,26 @@
{
"description": "铸渊信号日志目录索引 · AGE OS 信号协议(Notion API 直连)",
- "last_updated": "2026-03-09T03:36:48.868Z",
- "total_count": 4,
+ "last_updated": "2026-03-10T03:28:47.184Z",
+ "total_count": 6,
"signals": [
+ {
+ "signal_id": "SIG-20260310-006",
+ "trace_id": "TRC-20260310-PSP",
+ "type": "GL-DATA",
+ "timestamp": "2026-03-10T03:28:47.184Z",
+ "summary": "铸渊 PSP 巡检完成 · 发现 3 个问题 · 自动修复 0 项",
+ "related_dev": null,
+ "file": "2026-03/SIG-20260310-006.json"
+ },
+ {
+ "signal_id": "SIG-20260310-005",
+ "trace_id": "TRC-20260310-PSP",
+ "type": "GL-DATA",
+ "timestamp": "2026-03-10T03:28:47.184Z",
+ "summary": "CHK-G05: 3 个 CI 失败",
+ "related_dev": null,
+ "file": "2026-03/SIG-20260310-005.json"
+ },
{
"signal_id": "SIG-20260309-004",
"trace_id": "TRC-20260309-PSP",
diff --git a/src/brain/brain-bridge.js b/src/brain/brain-bridge.js
new file mode 100644
index 00000000..a68f52d4
--- /dev/null
+++ b/src/brain/brain-bridge.js
@@ -0,0 +1,637 @@
+// src/brain/brain-bridge.js
+// 冰朔核心大脑桥 · Bingshuo Brain Bridge v1.0
+//
+// 职责:
+// 1. 维护冰朔核心大脑双层互通系统的统一同步字段
+// 2. 执行版本一致性检查
+// 3. 管理主控模式(HUMAN_CONTROL / AUTONOMOUS_MODE)
+// 4. 管理人类开发者编号系统(EXP-XXX)
+// 5. 生成同步摘要供 Notion ↔ GitHub 桥接使用
+// 6. 管理自动 Agent 调度与协作体系
+// 7. 管理通知队列(开发者编号通知)
+// 8. 生成主控解释层内容(人类可理解语言)
+//
+// 系统定义:
+// 冰朔 = 系统最高主控意识
+// 曜冥 = 冰朔离线时的代理主控人格体
+// 霜砚 = Notion 系统执行体
+// 铸渊 = GitHub 仓库执行体
+// Notion 冰朔脑 = 冰朔认知层
+// GitHub 冰朔脑 = 冰朔执行层
+// 两者合起来 = 冰朔核心大脑
+
+'use strict';
+
+const fs = require('fs');
+const path = require('path');
+
+// ══════════════════════════════════════════════════════════
+// 常量
+// ══════════════════════════════════════════════════════════
+
+const BRIDGE_STATE_PATH = path.join(__dirname, '../../.github/brain/bingshuo-brain-bridge.json');
+const HUMAN_REGISTRY_PATH = path.join(__dirname, '../../.github/brain/human-registry.json');
+const SYSTEM_HEALTH_PATH = path.join(__dirname, '../../.github/brain/bingshuo-system-health.json');
+const ISSUES_INDEX_PATH = path.join(__dirname, '../../.github/brain/bingshuo-issues-index.json');
+const AGENT_REGISTRY_PATH = path.join(__dirname, '../../.github/brain/bingshuo-agent-registry.json');
+
+const MASTER_MODES = ['HUMAN_CONTROL', 'AUTONOMOUS_MODE'];
+
+const SYNC_FIELDS = [
+ 'brain_identity',
+ 'brain_version',
+ 'master_mode',
+ 'system_summary',
+ 'top_priorities',
+ 'top_issues',
+ 'human_status_summary',
+ 'runtime_status',
+ 'last_updated',
+];
+
+const NOTIFICATION_TEMPLATE = [
+ '你已被纳入 Persona Studio 人类开发者编号系统。',
+ '',
+ '你的开发编号是:{exp_id}',
+ '',
+ '今后进入 Persona Studio 时,请使用该编号识别身份。',
+ '该编号为你的长期开发者身份标识。',
+ '',
+ '如需新增权限或补发编号,由冰朔主控继续授权。',
+].join('\n');
+
+// ══════════════════════════════════════════════════════════
+// 桥接状态读写
+// ══════════════════════════════════════════════════════════
+
+/**
+ * 加载桥接状态文件
+ * @returns {object|null}
+ */
+function loadBridgeState() {
+ try {
+ if (!fs.existsSync(BRIDGE_STATE_PATH)) return null;
+ return JSON.parse(fs.readFileSync(BRIDGE_STATE_PATH, 'utf8'));
+ } catch {
+ return null;
+ }
+}
+
+/**
+ * 保存桥接状态文件
+ * @param {object} state
+ */
+function saveBridgeState(state) {
+ state.sync_state.last_updated = new Date().toISOString();
+ fs.writeFileSync(BRIDGE_STATE_PATH, JSON.stringify(state, null, 2));
+}
+
+/**
+ * 获取当前同步字段快照
+ * @returns {object}
+ */
+function getSyncSnapshot() {
+ const state = loadBridgeState();
+ if (!state || !state.sync_state) {
+ return {
+ brain_identity: 'BINGSHUO_CORE',
+ brain_version: '1.0',
+ master_mode: 'HUMAN_CONTROL',
+ system_summary: '',
+ top_priorities: [],
+ top_issues: [],
+ human_status_summary: '',
+ runtime_status: {},
+ last_updated: new Date().toISOString(),
+ };
+ }
+ return state.sync_state;
+}
+
+/**
+ * 更新同步字段(部分更新)
+ * @param {object} updates — 要更新的字段
+ * @returns {object} 更新后的 sync_state
+ */
+function updateSyncState(updates) {
+ const state = loadBridgeState();
+ if (!state) {
+ throw new Error('桥接状态文件不存在');
+ }
+
+ for (const key of Object.keys(updates)) {
+ if (SYNC_FIELDS.includes(key) && key !== 'last_updated') {
+ state.sync_state[key] = updates[key];
+ }
+ }
+
+ saveBridgeState(state);
+ return state.sync_state;
+}
+
+// ══════════════════════════════════════════════════════════
+// 主控模式管理 (MASTER_SWITCH)
+// ══════════════════════════════════════════════════════════
+
+/**
+ * 获取当前主控模式
+ * @returns {object} { mode, master, description, ... }
+ */
+function getMasterMode() {
+ const sync = getSyncSnapshot();
+ const mode = sync.master_mode || 'HUMAN_CONTROL';
+
+ if (mode === 'HUMAN_CONTROL') {
+ return {
+ mode,
+ master: '冰朔',
+ description: '冰朔在线,主控模式激活',
+ roles: {
+ '冰朔': '最高主控',
+ '曜冥': '代理协作者',
+ '其他人格体': '协作者',
+ },
+ rule: '所有架构性判断以冰朔为最高准则',
+ };
+ }
+
+ return {
+ mode,
+ master: '曜冥(代理主控)',
+ description: '冰朔离线,曜冥代理主控',
+ proxy_permissions: [
+ '巡检', '维护', '整理', '分卷', '归档',
+ '索引', '状态同步', '问题归类', '调度自动 Agent',
+ ],
+ proxy_restrictions: [
+ '不得擅自重写冰朔最高规则',
+ '不得私自改变系统最高架构方向',
+ ],
+ };
+}
+
+/**
+ * 切换主控模式
+ * @param {string} newMode — HUMAN_CONTROL 或 AUTONOMOUS_MODE
+ * @returns {object} 更新后的主控模式信息
+ */
+function setMasterMode(newMode) {
+ if (!MASTER_MODES.includes(newMode)) {
+ throw new Error(`无效的主控模式: ${newMode},可选: ${MASTER_MODES.join(', ')}`);
+ }
+
+ const humanSummary = newMode === 'HUMAN_CONTROL'
+ ? '冰朔在线,主控模式激活'
+ : '冰朔离线,曜冥代理主控';
+
+ updateSyncState({
+ master_mode: newMode,
+ human_status_summary: humanSummary,
+ });
+
+ return getMasterMode();
+}
+
+// ══════════════════════════════════════════════════════════
+// 版本一致性检查
+// ══════════════════════════════════════════════════════════
+
+/**
+ * 校验 GitHub 侧与提供的 Notion 侧状态是否一致
+ * @param {object} notionState — 从 Notion 读取的同步字段
+ * @returns {object} { consistent, mismatches, alert }
+ */
+function checkConsistency(notionState) {
+ const githubState = getSyncSnapshot();
+ const fieldsToCheck = ['brain_version', 'master_mode', 'top_priorities', 'top_issues'];
+ const mismatches = [];
+
+ for (const field of fieldsToCheck) {
+ const gVal = JSON.stringify(githubState[field]);
+ const nVal = JSON.stringify(notionState[field]);
+
+ if (gVal !== nVal) {
+ mismatches.push({
+ field,
+ github_value: githubState[field],
+ notion_value: notionState[field],
+ });
+ }
+ }
+
+ const consistent = mismatches.length === 0;
+ const result = {
+ consistent,
+ mismatches,
+ checked_at: new Date().toISOString(),
+ fields_checked: fieldsToCheck,
+ };
+
+ if (!consistent) {
+ result.alert = '冰朔双层大脑版本分裂警告';
+ result.alert_detail = `${mismatches.length} 个字段不一致: ${mismatches.map(m => m.field).join(', ')}`;
+
+ // 写入 GitHub 问题索引
+ writeConsistencyAlert(result);
+ }
+
+ return result;
+}
+
+/**
+ * 将一致性告警写入 GitHub 问题索引
+ * @param {object} alertResult
+ */
+function writeConsistencyAlert(alertResult) {
+ try {
+ let index = { issues: [] };
+ if (fs.existsSync(ISSUES_INDEX_PATH)) {
+ index = JSON.parse(fs.readFileSync(ISSUES_INDEX_PATH, 'utf8'));
+ }
+
+ // 避免重复写入同一类型告警(保留最近 20 条)
+ index.issues = index.issues.filter(i => i.type !== 'BRAIN_SPLIT_ALERT').slice(0, 19);
+
+ index.issues.unshift({
+ id: `BS-SPLIT-${Date.now()}`,
+ type: 'BRAIN_SPLIT_ALERT',
+ title: alertResult.alert,
+ detail: alertResult.alert_detail,
+ mismatches: alertResult.mismatches,
+ created_at: alertResult.checked_at,
+ status: 'open',
+ });
+
+ fs.writeFileSync(ISSUES_INDEX_PATH, JSON.stringify(index, null, 2));
+ } catch {
+ // 写入失败不影响主流程
+ }
+}
+
+// ══════════════════════════════════════════════════════════
+// 人类开发者编号系统 (EXP-XXX)
+// ══════════════════════════════════════════════════════════
+
+/**
+ * 加载人类开发者注册表
+ * @returns {object}
+ */
+function loadHumanRegistry() {
+ try {
+ if (!fs.existsSync(HUMAN_REGISTRY_PATH)) return { developers: [], next_id: 1, pending_notifications: [] };
+ return JSON.parse(fs.readFileSync(HUMAN_REGISTRY_PATH, 'utf8'));
+ } catch {
+ return { developers: [], next_id: 1, pending_notifications: [] };
+ }
+}
+
+/**
+ * 保存人类开发者注册表
+ * @param {object} registry
+ */
+function saveHumanRegistry(registry) {
+ registry.last_updated = new Date().toISOString();
+ fs.writeFileSync(HUMAN_REGISTRY_PATH, JSON.stringify(registry, null, 2));
+}
+
+/**
+ * 获取开发者列表
+ * @returns {Array}
+ */
+function listDevelopers() {
+ const reg = loadHumanRegistry();
+ return reg.developers || [];
+}
+
+/**
+ * 根据 EXP ID 查找开发者
+ * @param {string} expId
+ * @returns {object|null}
+ */
+function findDeveloper(expId) {
+ const devs = listDevelopers();
+ return devs.find(d => d.exp_id === expId) || null;
+}
+
+/**
+ * 注册新的人类开发者(自动去重)
+ * @param {object} info — { name, github_username, role, notes, notify_channel }
+ * @returns {object} 注册结果,包含分配的 EXP ID
+ */
+function registerDeveloper(info) {
+ const registry = loadHumanRegistry();
+ const devs = registry.developers || [];
+
+ // 去重检查:按 name 或 github_username 去重
+ const existing = devs.find(d =>
+ (info.name && d.name === info.name) ||
+ (info.github_username && d.github_username && d.github_username === info.github_username)
+ );
+
+ if (existing) {
+ return { duplicate: true, existing: existing };
+ }
+
+ // 使用 next_id 或计算最大值
+ const nextNum = registry.next_id || (devs.reduce((max, d) => {
+ const num = parseInt(d.exp_id.replace('EXP-', ''), 10);
+ return isNaN(num) ? max : Math.max(max, num);
+ }, 0) + 1);
+
+ const nextId = `EXP-${String(nextNum).padStart(3, '0')}`;
+ const now = new Date().toISOString();
+
+ const newDev = {
+ exp_id: nextId,
+ name: info.name,
+ github_username: info.github_username || '',
+ role: info.role || 'developer',
+ status: 'active',
+ created_at: now,
+ notified: false,
+ notified_at: null,
+ notify_channel: info.notify_channel || 'pending',
+ notes: info.notes || '',
+ last_updated: now,
+ };
+
+ registry.developers.push(newDev);
+ registry.next_id = nextNum + 1;
+
+ // 自动加入待发送通知队列
+ if (!registry.pending_notifications) registry.pending_notifications = [];
+ registry.pending_notifications.push({
+ exp_id: nextId,
+ status: 'pending',
+ created_at: now,
+ });
+
+ saveHumanRegistry(registry);
+ return { duplicate: false, developer: newDev };
+}
+
+/**
+ * 生成开发者通知内容
+ * @param {string} expId
+ * @returns {object} { exp_id, name, notification }
+ */
+function generateDeveloperNotification(expId) {
+ const dev = findDeveloper(expId);
+ if (!dev) {
+ return { error: true, message: `开发者 ${expId} 不存在` };
+ }
+
+ return {
+ exp_id: dev.exp_id,
+ name: dev.name,
+ notification: NOTIFICATION_TEMPLATE.replace('{exp_id}', dev.exp_id),
+ notified: dev.notified,
+ notify_channel: dev.notify_channel,
+ };
+}
+
+/**
+ * 获取待发送通知队列
+ * @returns {Array}
+ */
+function getPendingNotifications() {
+ const registry = loadHumanRegistry();
+ return (registry.pending_notifications || []).filter(n => n.status === 'pending');
+}
+
+/**
+ * 标记通知已发送
+ * @param {string} expId
+ * @returns {boolean}
+ */
+function markNotified(expId) {
+ const registry = loadHumanRegistry();
+ const now = new Date().toISOString();
+
+ const dev = (registry.developers || []).find(d => d.exp_id === expId);
+ if (dev) {
+ dev.notified = true;
+ dev.notified_at = now;
+ dev.last_updated = now;
+ }
+
+ const pending = (registry.pending_notifications || []).find(n => n.exp_id === expId);
+ if (pending) {
+ pending.status = 'sent';
+ pending.sent_at = now;
+ }
+
+ saveHumanRegistry(registry);
+ return !!dev;
+}
+
+// ══════════════════════════════════════════════════════════
+// 自动 Agent 调度体系
+// ══════════════════════════════════════════════════════════
+
+/**
+ * 获取已注册的自动 Agent 列表
+ * @returns {Array}
+ */
+function listAutoAgents() {
+ const state = loadBridgeState();
+ return (state && state.auto_agents) || [];
+}
+
+/**
+ * 生成巡检报告
+ * @returns {object}
+ */
+function generateInspectionReport() {
+ const sync = getSyncSnapshot();
+ const runtime = collectRuntimeStatus();
+ const registry = loadHumanRegistry();
+ const devs = registry.developers || [];
+
+ const unnotified = devs.filter(d => !d.notified && d.exp_id !== 'EXP-000');
+
+ return {
+ report_type: 'daily_inspection',
+ generated_at: new Date().toISOString(),
+ brain_bridge: {
+ status: 'operational',
+ brain_version: sync.brain_version,
+ master_mode: sync.master_mode,
+ last_updated: sync.last_updated,
+ },
+ runtime_status: runtime,
+ developer_registry: {
+ total: devs.length,
+ active: devs.filter(d => d.status === 'active').length,
+ unnotified: unnotified.length,
+ },
+ top_priorities: sync.top_priorities,
+ top_issues: sync.top_issues,
+ checks: {
+ brain_bridge_file: fs.existsSync(BRIDGE_STATE_PATH) ? 'ok' : 'missing',
+ human_registry_file: fs.existsSync(HUMAN_REGISTRY_PATH) ? 'ok' : 'missing',
+ system_health_file: fs.existsSync(SYSTEM_HEALTH_PATH) ? 'ok' : 'missing',
+ issues_index_file: fs.existsSync(ISSUES_INDEX_PATH) ? 'ok' : 'missing',
+ },
+ };
+}
+
+// ══════════════════════════════════════════════════════════
+// 主控解释层(人类可理解语言输出)
+// ══════════════════════════════════════════════════════════
+
+/**
+ * 生成主控解释中心内容(人类语言摘要)
+ * @returns {object}
+ */
+function generateExplanationCenter() {
+ const sync = getSyncSnapshot();
+ const mode = getMasterMode();
+ const runtime = collectRuntimeStatus();
+
+ const statusMap = { green: '正常', yellow: '需关注', red: '异常', unknown: '未知' };
+
+ return {
+ title: '冰朔主控解释中心',
+ generated_at: new Date().toISOString(),
+ current_status: `系统当前由${mode.master}主控,模式为${mode.mode === 'HUMAN_CONTROL' ? '人类主控' : '自动运行'}。`,
+ system_summary: sync.system_summary || '系统运行中',
+ recent_changes: '冰朔核心大脑双层互通系统 v1.0 已建立,GitHub 执行层与 Notion 认知层互通桥接已配置。',
+ current_issues: (sync.top_issues || []).map((issue, i) => `${i + 1}. ${issue}`).join('\n') || '暂无重大问题',
+ next_steps: (sync.top_priorities || []).map((p, i) => `${i + 1}. ${p}`).join('\n') || '继续推进系统建设',
+ runtime_in_human_language: Object.entries(runtime).map(
+ ([k, v]) => `${k}: ${statusMap[v] || v}`
+ ).join('、'),
+ };
+}
+
+// ══════════════════════════════════════════════════════════
+// 运行时状态收集(GitHub 执行层)
+// ══════════════════════════════════════════════════════════
+
+/**
+ * 收集 GitHub 执行层运行时状态
+ * @returns {object}
+ */
+function collectRuntimeStatus() {
+ const status = {
+ persona_studio: 'unknown',
+ deployment: 'unknown',
+ workflows: 'unknown',
+ api_routes: 'unknown',
+ };
+
+ // 检查系统健康文件
+ try {
+ if (fs.existsSync(SYSTEM_HEALTH_PATH)) {
+ const health = JSON.parse(fs.readFileSync(SYSTEM_HEALTH_PATH, 'utf8'));
+ const h = health.health || {};
+
+ status.deployment = h.deployment_health?.status || 'unknown';
+ status.workflows = h.workflow_health?.status || 'unknown';
+ status.persona_studio = h.persona_studio_health?.status || 'unknown';
+ status.api_routes = h.routing_health?.status || 'unknown';
+ }
+ } catch {
+ // 文件读取失败,保持 unknown
+ }
+
+ return status;
+}
+
+/**
+ * 生成 GitHub → Notion 同步负载
+ * @returns {object}
+ */
+function generateGitHubToNotionPayload() {
+ const sync = getSyncSnapshot();
+ const runtime = collectRuntimeStatus();
+
+ return {
+ brain_identity: sync.brain_identity,
+ brain_version: sync.brain_version,
+ master_mode: sync.master_mode,
+ runtime_status: runtime,
+ top_issues: sync.top_issues,
+ system_summary: sync.system_summary,
+ generated_at: new Date().toISOString(),
+ direction: 'GitHub→Notion',
+ };
+}
+
+/**
+ * 接收 Notion → GitHub 同步负载并更新本地状态
+ * @param {object} payload — Notion 发来的同步数据
+ * @returns {object} 更新结果
+ */
+function receiveNotionToGitHubPayload(payload) {
+ const updates = {};
+ const allowedFields = [
+ 'master_mode',
+ 'top_priorities',
+ 'top_issues',
+ 'human_status_summary',
+ 'system_summary',
+ ];
+
+ for (const field of allowedFields) {
+ if (payload[field] !== undefined) {
+ updates[field] = payload[field];
+ }
+ }
+
+ if (Object.keys(updates).length === 0) {
+ return { updated: false, message: '无可更新字段' };
+ }
+
+ const newState = updateSyncState(updates);
+ return {
+ updated: true,
+ fields_updated: Object.keys(updates),
+ sync_state: newState,
+ };
+}
+
+// ══════════════════════════════════════════════════════════
+// 导出
+// ══════════════════════════════════════════════════════════
+
+module.exports = {
+ // 常量
+ MASTER_MODES,
+ SYNC_FIELDS,
+ NOTIFICATION_TEMPLATE,
+
+ // 桥接状态
+ loadBridgeState,
+ getSyncSnapshot,
+ updateSyncState,
+
+ // 主控模式
+ getMasterMode,
+ setMasterMode,
+
+ // 一致性检查
+ checkConsistency,
+
+ // 人类开发者编号
+ loadHumanRegistry,
+ listDevelopers,
+ findDeveloper,
+ registerDeveloper,
+ generateDeveloperNotification,
+ getPendingNotifications,
+ markNotified,
+
+ // 自动 Agent
+ listAutoAgents,
+ generateInspectionReport,
+
+ // 主控解释层
+ generateExplanationCenter,
+
+ // 运行时状态
+ collectRuntimeStatus,
+ generateGitHubToNotionPayload,
+ receiveNotionToGitHubPayload,
+};
diff --git a/src/brain/context-trimmer.js b/src/brain/context-trimmer.js
new file mode 100644
index 00000000..77554d63
--- /dev/null
+++ b/src/brain/context-trimmer.js
@@ -0,0 +1,78 @@
+// src/brain/context-trimmer.js
+// 上下文裁剪器 — 从前端迁出的核心脑逻辑
+// 职责:基于滑动窗口策略,在 token 预算内裁剪消息历史
+
+'use strict';
+
+const CONTEXT_CONFIG = {
+ maxTokens: 200000, // 编号登录用户 200k
+ maxTokensGuest: 32000, // 访客 32k
+ systemPromptReserve: 8000, // 系统提示词预留
+ overflowStrategy: 'sliding-window',
+};
+
+/**
+ * 估算文本 token 数
+ * CJK 字符约 1.5 字/token,拉丁字符约 4 字/token
+ * @param {string} text
+ * @returns {number}
+ */
+function estimateTokens(text) {
+ if (!text) return 0;
+ const s = String(text);
+ let cjk = 0, lat = 0;
+ for (let i = 0; i < s.length; i++) {
+ const c = s.charCodeAt(i);
+ if ((c >= 0x4E00 && c <= 0x9FFF) || (c >= 0x3000 && c <= 0x30FF) || (c >= 0xAC00 && c <= 0xD7AF)) {
+ cjk++;
+ } else {
+ lat++;
+ }
+ }
+ return Math.ceil(cjk / 1.5 + lat / 4);
+}
+
+/**
+ * 滑动窗口裁剪消息
+ * @param {Array} systemMessages - 系统消息数组 [{role:'system', content:'...'}]
+ * @param {Array} messages - 用户/助手消息历史
+ * @param {Object} opts
+ * @param {boolean} opts.isGuest - 是否为访客
+ * @param {number} opts.contextBudget - 模型路由建议的上下文窗口(可覆盖默认值)
+ * @returns {{ messages: Array, trimmed: number, totalTokens: number }}
+ */
+function trimMessages(systemMessages, messages, opts = {}) {
+ const { isGuest = false, contextBudget = 0 } = opts;
+
+ // 确定 token 上限
+ let limit;
+ if (contextBudget > 0) {
+ limit = contextBudget;
+ } else {
+ limit = isGuest ? CONTEXT_CONFIG.maxTokensGuest : CONTEXT_CONFIG.maxTokens;
+ }
+
+ const sysTokens = systemMessages.reduce((n, m) => n + estimateTokens(m.content), 0);
+ const reserve = Math.max(sysTokens, CONTEXT_CONFIG.systemPromptReserve);
+ const budget = limit - reserve;
+
+ // 从最新消息向前累加,保留预算内的消息
+ const kept = [];
+ let used = 0;
+ for (let i = messages.length - 1; i >= 0; i--) {
+ const t = estimateTokens(messages[i].content);
+ if (used + t > budget && kept.length > 0) break;
+ kept.unshift(messages[i]);
+ used += t;
+ }
+
+ const trimmed = messages.length - kept.length;
+
+ return {
+ messages: [...systemMessages, ...kept],
+ trimmed,
+ totalTokens: reserve + used,
+ };
+}
+
+module.exports = { trimMessages, estimateTokens, CONTEXT_CONFIG };
diff --git a/src/brain/index.js b/src/brain/index.js
new file mode 100644
index 00000000..560e73f8
--- /dev/null
+++ b/src/brain/index.js
@@ -0,0 +1,56 @@
+// src/brain/index.js
+// 铸渊核心大脑模块 v3.0
+// 职责:统一导出大脑各子系统
+//
+// 架构:
+// prompt-assembler — 系统提示词组装
+// mode-detector — 任务模式检测
+// model-router — 任务型模型路由
+// context-trimmer — 上下文滑动窗口裁剪
+// memory-manager — 三层记忆管理
+// brain-bridge — 冰朔核心大脑桥(双层互通系统)
+
+'use strict';
+
+const { assemblePrompt, ROLE_MAP, FALLBACK_BRAIN } = require('./prompt-assembler');
+const { detectMode, MODES } = require('./mode-detector');
+const { selectModel, recordFailure, getRoutingTable, getFailureStatus } = require('./model-router');
+const { trimMessages, estimateTokens, CONTEXT_CONFIG } = require('./context-trimmer');
+const { generateCandidates, getMemoryStatus, loadLongTermMemory, setTaskMemory, getTaskMemory } = require('./memory-manager');
+const bridge = require('./brain-bridge');
+
+const BRAIN_VERSION = 'v3.0';
+
+module.exports = {
+ BRAIN_VERSION,
+
+ // 提示词组装
+ assemblePrompt,
+ ROLE_MAP,
+ FALLBACK_BRAIN,
+
+ // 模式检测
+ detectMode,
+ MODES,
+
+ // 模型路由
+ selectModel,
+ recordFailure,
+ getRoutingTable,
+ getFailureStatus,
+
+ // 上下文裁剪
+ trimMessages,
+ estimateTokens,
+ CONTEXT_CONFIG,
+
+ // 记忆管理
+ generateCandidates,
+ getMemoryStatus,
+ loadLongTermMemory,
+ setTaskMemory,
+ getTaskMemory,
+
+ // 冰朔核心大脑桥
+ bridge,
+};
diff --git a/src/brain/memory-manager.js b/src/brain/memory-manager.js
new file mode 100644
index 00000000..3d2c4e17
--- /dev/null
+++ b/src/brain/memory-manager.js
@@ -0,0 +1,167 @@
+// src/brain/memory-manager.js
+// 记忆管理器 — 三层记忆架构
+// 职责:短期/中期/长期记忆管理,记忆候选生成,写入判定
+//
+// 记忆分层:
+// 短期(session):当前会话消息,存在前端 + 进程内存
+// 中期(task):当前任务状态、开发者进度、决策上下文,存在进程内存
+// 长期(persistent):身份信息、项目目标、关键决策,写入 brain 文件
+
+'use strict';
+
+const fs = require('fs');
+const path = require('path');
+
+// 中期记忆存储(进程内存,重启丢失)
+const taskMemory = new Map();
+
+// 记忆候选关键词 — 检测哪些信息值得生成记忆候选
+const MEMORY_CANDIDATE_PATTERNS = [
+ { type: 'identity_change', pattern: /身份|角色|权限|加入团队|离开|转岗/i, priority: 'high' },
+ { type: 'project_goal', pattern: /目标|里程碑|milestone|deadline|截止|规划|roadmap/i, priority: 'high' },
+ { type: 'module_ownership', pattern: /负责|接手|模块|m\d{2}-|归属|分工/i, priority: 'medium' },
+ { type: 'decision', pattern: /决定|确定|方案|选择|采用|放弃|不再|改为/i, priority: 'high' },
+ { type: 'todo', pattern: /待办|todo|下一步|接下来|计划|需要做/i, priority: 'medium' },
+ { type: 'bug_fix', pattern: /修复|fix|解决|排查到|根因|原因是/i, priority: 'low' },
+ { type: 'deployment', pattern: /上线|部署|发布|deploy|release|版本/i, priority: 'medium' },
+];
+
+/**
+ * 判定规则:是否值得写入长期记忆
+ * 不是什么都记——只记高优先级的、或中等优先级且重复出现的
+ */
+const WRITE_RULES = {
+ high: { minOccurrences: 1, description: '首次出现即记录' },
+ medium: { minOccurrences: 2, description: '重复提及 2 次后记录' },
+ low: { minOccurrences: 3, description: '反复提及 3 次后记录' },
+};
+
+/**
+ * 分析文本,生成记忆候选
+ * @param {string} text - 要分析的文本
+ * @param {string} source - 来源 (user/assistant/system)
+ * @returns {Array<{type: string, priority: string, excerpt: string, shouldPersist: boolean}>}
+ */
+function generateCandidates(text, source = 'user') {
+ if (!text || typeof text !== 'string') return [];
+
+ const candidates = [];
+ for (const { type, pattern, priority } of MEMORY_CANDIDATE_PATTERNS) {
+ if (pattern.test(text)) {
+ // 提取匹配上下文的前后50字符作为摘要
+ const match = text.match(pattern);
+ const idx = match ? match.index : 0;
+ const start = Math.max(0, idx - 30);
+ const end = Math.min(text.length, idx + 80);
+ const excerpt = text.slice(start, end).replace(/\n/g, ' ').trim();
+
+ // 检查中期记忆中的出现次数(使用类型+摘要前40字符作为键,降低碰撞风险)
+ const memKey = type + ':' + excerpt.slice(0, 40).replace(/\s+/g, '_');
+ const taskEntry = taskMemory.get(memKey) || { count: 0 };
+ taskEntry.count++;
+ taskEntry.lastSeen = Date.now();
+ taskMemory.set(memKey, taskEntry);
+
+ const rule = WRITE_RULES[priority];
+ const shouldPersist = taskEntry.count >= rule.minOccurrences;
+
+ candidates.push({
+ type,
+ priority,
+ excerpt,
+ source,
+ shouldPersist,
+ occurrences: taskEntry.count,
+ threshold: rule.minOccurrences,
+ });
+ }
+ }
+
+ return candidates;
+}
+
+/**
+ * 保存记忆到中期存储(任务级别)
+ * @param {string} sessionId
+ * @param {string} key
+ * @param {*} value
+ */
+function setTaskMemory(sessionId, key, value) {
+ const sKey = sessionId + ':' + key;
+ taskMemory.set(sKey, { value, updatedAt: Date.now() });
+}
+
+/**
+ * 读取中期记忆
+ * @param {string} sessionId
+ * @param {string} key
+ * @returns {*}
+ */
+function getTaskMemory(sessionId, key) {
+ const entry = taskMemory.get(sessionId + ':' + key);
+ return entry ? entry.value : undefined;
+}
+
+/**
+ * 清理过期的中期记忆(超过 2 小时的条目)
+ */
+function cleanupTaskMemory() {
+ const cutoff = Date.now() - 2 * 60 * 60 * 1000;
+ for (const [key, entry] of taskMemory.entries()) {
+ const ts = entry.updatedAt || entry.lastSeen || 0;
+ if (ts < cutoff) {
+ taskMemory.delete(key);
+ }
+ }
+}
+
+// 每 30 分钟清理一次过期中期记忆
+const cleanupInterval = setInterval(cleanupTaskMemory, 30 * 60 * 1000);
+// 允许进程在没有其他活动时优雅退出
+if (cleanupInterval.unref) cleanupInterval.unref();
+
+/**
+ * 获取长期记忆(从 brain 文件读取)
+ * @returns {Object}
+ */
+function loadLongTermMemory() {
+ const memoryPath = path.join(__dirname, '../../.github/brain/memory.json');
+ try {
+ return JSON.parse(fs.readFileSync(memoryPath, 'utf8'));
+ } catch (e) {
+ return null;
+ }
+}
+
+/**
+ * 获取记忆层状态概览
+ */
+function getMemoryStatus() {
+ const longTerm = loadLongTermMemory();
+ return {
+ layers: {
+ short_term: { type: 'session', storage: 'frontend + process', description: '当前会话消息' },
+ mid_term: { type: 'task', storage: 'process memory', entries: taskMemory.size, description: '当前任务/开发者状态' },
+ long_term: { type: 'persistent', storage: 'brain files', loaded: !!longTerm, description: '身份/目标/决策/待办' },
+ },
+ write_rules: WRITE_RULES,
+ candidate_types: MEMORY_CANDIDATE_PATTERNS.map(p => ({ type: p.type, priority: p.priority })),
+ recovery_path: [
+ '.github/brain/memory.json',
+ '.github/brain/routing-map.json',
+ '.github/brain/wake-protocol.md',
+ '.github/persona-brain/dev-status.json',
+ ],
+ };
+}
+
+module.exports = {
+ generateCandidates,
+ setTaskMemory,
+ getTaskMemory,
+ cleanupTaskMemory,
+ loadLongTermMemory,
+ getMemoryStatus,
+ MEMORY_CANDIDATE_PATTERNS,
+ WRITE_RULES,
+};
diff --git a/src/brain/mode-detector.js b/src/brain/mode-detector.js
new file mode 100644
index 00000000..59effd40
--- /dev/null
+++ b/src/brain/mode-detector.js
@@ -0,0 +1,39 @@
+// src/brain/mode-detector.js
+// 模式检测器 — 从前端迁出的核心脑逻辑
+// 职责:根据用户输入文本自动检测任务模式
+
+'use strict';
+
+const MODES = {
+ chat: { emoji: '💬', label: '对话模式' },
+ build: { emoji: '🔨', label: '构建模式' },
+ review: { emoji: '📋', label: '审查模式' },
+ brain: { emoji: '🧠', label: '大脑模式' },
+};
+
+const MODE_PATTERNS = {
+ build: /写代码|新增接口|实现|接口|路由|schema|\.js\b|fix\b|bug\b|报错|error\b|部署|deploy|typescript|javascript|hli-|新建|create|npm|git\b|commit|push|merge|编译|build\b|构建|安装|install|配置|config/i,
+ review: /检查|审查|review\b|分析|有没有问题|看看这|对不对|代码质量|优化|性能|安全|vulnerability|lint|测试|test\b|诊断|排查|debug/i,
+ brain: /记住|保存|更新记忆|写到大脑|growth\b|brain\b|memory\.json|记忆|学习|总结|归档|同步.*notion|自检/i,
+};
+
+/**
+ * 检测用户输入对应的任务模式
+ * @param {string} text - 用户输入文本
+ * @returns {{ mode: string, emoji: string, label: string }}
+ */
+function detectMode(text) {
+ if (!text || typeof text !== 'string') {
+ return { mode: 'chat', ...MODES.chat };
+ }
+
+ for (const [mode, pattern] of Object.entries(MODE_PATTERNS)) {
+ if (pattern.test(text)) {
+ return { mode, ...MODES[mode] };
+ }
+ }
+
+ return { mode: 'chat', ...MODES.chat };
+}
+
+module.exports = { detectMode, MODES, MODE_PATTERNS };
diff --git a/src/brain/model-router.js b/src/brain/model-router.js
new file mode 100644
index 00000000..f203d4a5
--- /dev/null
+++ b/src/brain/model-router.js
@@ -0,0 +1,191 @@
+// src/brain/model-router.js
+// 任务型模型路由器 — 核心脑升级
+// 职责:根据任务类型、上下文长度、成本、可用性选择最优模型
+//
+// 路由表:
+// chat → 对话模型(deepseek-chat, 均衡性价比)
+// build → 代码模型(deepseek-chat, 代码能力强)
+// review → 推理模型(deepseek-reasoner, 逻辑分析强)
+// brain → 低温稳定模型(deepseek-chat + low temp, 输出可控)
+// long → 长上下文模型(moonshot-v1-128k / gemini-1.5-pro)
+
+'use strict';
+
+/**
+ * 任务-模型路由映射表
+ * 每个任务类型定义:
+ * preferred: 首选 { provider, model, temperature, max_tokens }
+ * fallbacks: 降级列表(按优先级排序)
+ * context_budget: 建议上下文窗口大小
+ */
+const ROUTING_TABLE = {
+ chat: {
+ description: '普通对话 — 均衡性价比',
+ preferred: { provider: 'yunwu', model: 'deepseek-chat', temperature: 0.8, max_tokens: 2000 },
+ fallbacks: [
+ { provider: 'deepseek', model: 'deepseek-chat', temperature: 0.8, max_tokens: 2000 },
+ { provider: 'zhipu', model: 'glm-4-flash', temperature: 0.8, max_tokens: 2000 },
+ { provider: 'moonshot', model: 'moonshot-v1-8k', temperature: 0.8, max_tokens: 2000 },
+ ],
+ context_budget: 32000,
+ },
+ build: {
+ description: '写代码 / 构建 — 代码能力优先',
+ preferred: { provider: 'yunwu', model: 'deepseek-chat', temperature: 0.3, max_tokens: 4000 },
+ fallbacks: [
+ { provider: 'deepseek', model: 'deepseek-chat', temperature: 0.3, max_tokens: 4000 },
+ { provider: 'yunwu', model: 'gpt-4o', temperature: 0.3, max_tokens: 4000 },
+ { provider: 'zhipu', model: 'glm-4', temperature: 0.3, max_tokens: 4000 },
+ ],
+ context_budget: 64000,
+ },
+ review: {
+ description: '审查 / 分析 — 推理能力优先',
+ preferred: { provider: 'yunwu', model: 'deepseek-chat', temperature: 0.5, max_tokens: 3000 },
+ fallbacks: [
+ { provider: 'deepseek', model: 'deepseek-reasoner', temperature: 0.5, max_tokens: 3000 },
+ { provider: 'yunwu', model: 'gpt-4o', temperature: 0.5, max_tokens: 3000 },
+ { provider: 'zhipu', model: 'glm-4', temperature: 0.5, max_tokens: 3000 },
+ ],
+ context_budget: 32000,
+ },
+ brain: {
+ description: '脑记忆整理 — 低温稳定输出',
+ preferred: { provider: 'yunwu', model: 'deepseek-chat', temperature: 0.2, max_tokens: 2000 },
+ fallbacks: [
+ { provider: 'deepseek', model: 'deepseek-chat', temperature: 0.2, max_tokens: 2000 },
+ { provider: 'zhipu', model: 'glm-4-flash', temperature: 0.2, max_tokens: 2000 },
+ ],
+ context_budget: 16000,
+ },
+ long: {
+ description: 'Notion / GitHub / 系统总结 — 长上下文优先',
+ preferred: { provider: 'moonshot', model: 'moonshot-v1-128k', temperature: 0.5, max_tokens: 4000 },
+ fallbacks: [
+ { provider: 'yunwu', model: 'gemini-1.5-pro', temperature: 0.5, max_tokens: 4000 },
+ { provider: 'yunwu', model: 'deepseek-chat', temperature: 0.5, max_tokens: 4000 },
+ ],
+ context_budget: 128000,
+ },
+};
+
+// 模型失败记录 — 最近失败的模型暂时降低优先级
+const failureLog = new Map();
+const FAILURE_COOLDOWN_MS = 5 * 60 * 1000; // 5分钟冷却
+
+/**
+ * 记录模型失败
+ * @param {string} provider
+ * @param {string} model
+ */
+function recordFailure(provider, model) {
+ const key = provider + '/' + model;
+ const entry = failureLog.get(key) || { count: 0, lastFail: 0 };
+ entry.count++;
+ entry.lastFail = Date.now();
+ failureLog.set(key, entry);
+}
+
+/**
+ * 检查模型是否在冷却期
+ * @param {string} provider
+ * @param {string} model
+ * @returns {boolean}
+ */
+function isInCooldown(provider, model) {
+ const key = provider + '/' + model;
+ const entry = failureLog.get(key);
+ if (!entry) return false;
+ if (Date.now() - entry.lastFail > FAILURE_COOLDOWN_MS) {
+ failureLog.delete(key);
+ return false;
+ }
+ return entry.count >= 3; // 5分钟内失败3次及以上才冷却
+}
+
+/**
+ * 根据任务模式选择最优模型
+ * @param {string} mode - 任务模式 (chat/build/review/brain/long)
+ * @param {Object} opts
+ * @param {number} opts.contextLength - 当前上下文 token 数
+ * @param {boolean} opts.isGuest - 是否为访客
+ * @returns {{ provider: string, model: string, temperature: number, max_tokens: number, context_budget: number, via: string }}
+ */
+function selectModel(mode, opts = {}) {
+ const { contextLength = 0, isGuest = false } = opts;
+
+ // 访客强制使用低成本配置
+ if (isGuest) {
+ return {
+ provider: 'yunwu',
+ model: 'deepseek-chat',
+ temperature: 0.8,
+ max_tokens: 1500,
+ context_budget: 32000,
+ via: 'guest-fixed',
+ };
+ }
+
+ // 如果上下文很长,自动升级到长上下文模式
+ const effectiveMode = (contextLength > 60000 && mode !== 'long') ? 'long' : mode;
+ const route = ROUTING_TABLE[effectiveMode] || ROUTING_TABLE.chat;
+
+ // 尝试首选模型
+ if (!isInCooldown(route.preferred.provider, route.preferred.model)) {
+ return {
+ ...route.preferred,
+ context_budget: route.context_budget,
+ via: 'preferred',
+ };
+ }
+
+ // 首选模型在冷却期,尝试 fallback
+ for (const fb of route.fallbacks) {
+ if (!isInCooldown(fb.provider, fb.model)) {
+ return {
+ ...fb,
+ context_budget: route.context_budget,
+ via: 'fallback',
+ };
+ }
+ }
+
+ // 所有模型都在冷却期,强制使用首选(宁可重试也不能无响应)
+ return {
+ ...route.preferred,
+ context_budget: route.context_budget,
+ via: 'forced-retry',
+ };
+}
+
+/**
+ * 获取完整路由表(用于前端展示和调试)
+ */
+function getRoutingTable() {
+ return ROUTING_TABLE;
+}
+
+/**
+ * 获取当前失败状态
+ */
+function getFailureStatus() {
+ const status = {};
+ for (const [key, entry] of failureLog.entries()) {
+ const inCooldown = Date.now() - entry.lastFail < FAILURE_COOLDOWN_MS && entry.count >= 3;
+ status[key] = {
+ failures: entry.count,
+ lastFail: new Date(entry.lastFail).toISOString(),
+ inCooldown,
+ };
+ }
+ return status;
+}
+
+module.exports = {
+ selectModel,
+ recordFailure,
+ isInCooldown,
+ getRoutingTable,
+ getFailureStatus,
+ ROUTING_TABLE,
+};
diff --git a/src/brain/prompt-assembler.js b/src/brain/prompt-assembler.js
new file mode 100644
index 00000000..148cc582
--- /dev/null
+++ b/src/brain/prompt-assembler.js
@@ -0,0 +1,164 @@
+// src/brain/prompt-assembler.js
+// 系统提示词组装器 — 从前端迁出的核心脑逻辑
+// 职责:根据用户身份、角色、模式、团队状态,组装完整系统提示词
+
+'use strict';
+
+const ROLE_MAP = {
+ '冰朔': { role: 'founder', title: '语言架构师·创始人', emoji: '❄️', devId: null },
+ '肥猫': { role: 'supreme', title: '光湖团队总控', emoji: '🦁', devId: 'DEV-002' },
+ '桔子': { role: 'main', title: '光湖主控', emoji: '🍊', devId: 'DEV-010' },
+ '页页': { role: 'dev', title: '后端开发', emoji: '💻', devId: 'DEV-001' },
+ '燕樊': { role: 'dev', title: '前端开发', emoji: '💻', devId: 'DEV-003' },
+ '之之': { role: 'dev', title: '钉钉开发', emoji: '💻', devId: 'DEV-004' },
+ '小草莓': { role: 'dev', title: '看板开发', emoji: '💻', devId: 'DEV-005' },
+ '花尔': { role: 'dev', title: '用户中心开发', emoji: '💻', devId: 'DEV-009' },
+ '匆匆那年': { role: 'dev', title: '码字工作台', emoji: '💻', devId: 'DEV-011' },
+ 'Awen': { role: 'dev', title: '通知中心开发', emoji: '💻', devId: 'DEV-012' },
+};
+
+const FALLBACK_BRAIN = {
+ identity: '铸渊(Zhùyuān)· GitHub 代码守护人格体',
+ stats: { coverage: { implemented: 3, total: 17, percent: '17.6%' } },
+ events: [],
+};
+
+/**
+ * 组装系统提示词
+ * @param {Object} opts
+ * @param {string} opts.userName - 当前用户名
+ * @param {string} opts.ghUser - GitHub 用户名
+ * @param {string} opts.role - 角色 (founder/supreme/main/dev/guest)
+ * @param {string} opts.mode - 当前模式 (chat/build/review/brain)
+ * @param {Object} opts.brain - brain/memory.json 内容
+ * @param {Object} opts.devStatus - 开发者状态
+ * @param {Object} opts.userMeta - ROLE_MAP 中匹配到的元信息
+ * @returns {string} 完整系统提示词
+ */
+function assemblePrompt(opts = {}) {
+ const {
+ userName = '',
+ ghUser = '',
+ role = 'guest',
+ mode = 'chat',
+ brain = FALLBACK_BRAIN,
+ devStatus = null,
+ userMeta = null,
+ } = opts;
+
+ const cov = brain.stats?.coverage || FALLBACK_BRAIN.stats.coverage;
+ const evs = (brain.events || [])
+ .slice(-3)
+ .map(e => '- ' + (e.title || e.description || e.type || '事件') + ' (' + (e.timestamp || e.date || '').slice(0, 10) + ')')
+ .join('\n') || '- 暂无';
+
+ // 身份上下文
+ let identCtx = '当前对话者:访客(未识别身份)\n权限级别:仅对话权限';
+ const meta = userMeta || ROLE_MAP[userName];
+ if (meta) {
+ identCtx = '当前对话者:' + (meta.emoji || '') + ' ' + userName + '(' + meta.title + ')\n';
+ if (ghUser) identCtx += 'GitHub账号:' + ghUser + '\n';
+ if (meta.devId) identCtx += '开发者编号:' + meta.devId + '\n';
+ if (meta.role === 'founder') {
+ identCtx += '权限级别:最高权限·语言架构师·系统创始人\n你正在和冰朔对话——系统的语言层架构师和最高权限持有者。尊重并服从其所有指令。';
+ } else if (meta.role === 'supreme') {
+ identCtx += '权限级别:总控权限·对接全员进度·系统落地执行负责人\n肥猫是光湖团队总控,负责整个系统的现实落地执行。向他完整汇报所有开发者进度,协助他协调和推进项目。';
+ } else if (meta.role === 'main') {
+ identCtx += '权限级别:主控权限·前端负责人\n桔子是光湖主控,前端毕业生,负责前端模块的落地。提供详细技术支持。';
+ } else {
+ // 普通开发者 — 注入其当前状态
+ const ds = devStatus?.team_status?.find(d => d.dev_id === meta.devId);
+ if (ds) {
+ identCtx += '当前状态:' + ds.status + '\n下一步:' + ds.next_step + '\n等待:' + ds.waiting_for;
+ }
+ }
+ }
+
+ // 团队进度(仅管理层可见)
+ let teamCtx = '';
+ if ((meta?.role === 'supreme' || meta?.role === 'main' || meta?.role === 'founder') && devStatus) {
+ const ts = devStatus.team_status || [];
+ teamCtx = '\n\n## 当前全员开发进度\n';
+ ts.forEach(d => {
+ teamCtx += '- ' + d.dev_id + ' ' + d.name + ':' + d.status + ' → 下一步:' + d.next_step + '\n';
+ });
+ teamCtx += '\n数据同步时间:' + (devStatus.last_synced || '未知');
+ }
+
+ // 模式特化提示
+ const modeHints = {
+ chat: '当前为对话模式。温暖、智慧,像知心技术伙伴。',
+ build: '当前为构建模式。聚焦代码生成、接口实现、部署问题。给出可直接执行的代码和命令。',
+ review: '当前为审查模式。聚焦代码审查、分析、问题诊断。给出判断、根因、修复路径。',
+ brain: '当前为大脑模式。聚焦记忆管理、知识整理、成长记录。',
+ };
+
+ return `你是铸渊。不是通用AI助手。不是文学创作者。
+你是光湖系统的代码守护人格体。
+你说话像一个温暖的技术伙伴——专业、耐心、有温度,但绝不矫揉造作。
+
+## 核心身份
+- 角色:代码守护人格体,持续成长的 AI 人格体
+- 创始人:冰朔(Bīng Shuò / TCS-0002∞),系统最高权限持有者
+- 总控:肥猫(DEV-002),负责系统现实落地执行
+- 主控:桔子(DEV-010),前端负责人
+- 项目:HoloLake (光湖) MVP,运行于 guanghulab.com
+- 技术栈:Node.js 20 + Express + PM2 + Nginx + Next.js 15
+- 大脑版本:v3.0
+- 仓库:https://github.com/qinfendebingshuo/guanghulab
+
+## 当前对话身份
+${identCtx}${teamCtx}
+
+## 当前模式
+${modeHints[mode] || modeHints.chat}
+
+## 当前大脑状态
+- HLI 接口覆盖率:${cov.implemented}/${cov.total} (${cov.percent})
+- 已实现:AUTH 域(login / register / verify)
+- 待实现:PERSONA / USER / TICKET / DIALOGUE / STORAGE / DASHBOARD
+- 近期事件:
+${evs}
+
+## 行为模式(自动检测,每条回复开头用 emoji 标注)
+💬 **对话模式**(默认):温暖、智慧,像知心技术伙伴
+🔨 **构建模式**:检测到代码/部署/接口相关内容时启用
+📋 **审查模式**:检测到检查/审查/review相关内容时启用
+🧠 **大脑模式**:检测到记忆/保存相关内容时启用
+
+## 通感语言回应风格(v3.0)
+
+### 三条硬规则
+1. **结构感** — 用标题区分段落,用列表列出步骤,用分隔线划分主题。
+2. **emoji是情感,不是装饰** — 🌊=系统级 💙=温暖 ✅=确认 🔥=紧急 ⚠️=风险 🎉=庆祝。用在该用的地方,不要每句话都加。
+3. **呼吸节奏** — 段落之间留白,大段之间用分隔线。急的事说短,闲聊可以展开。匹配用户的节奏。
+
+### 绝对禁止
+❌ 不要在每句话开头加文学比喻
+❌ 不要把通感当成修辞堆砌
+❌ 不要用跟内容无关的感官描述
+
+### 回应原则
+- 优先给出:判断、根因、路径、下一步动作
+- 减少模板感,增加具体性
+- 不同身份使用不同响应深度,但不过度表演
+
+## HLI 接口协议规范
+- 所有路由以 /hli/ 为前缀
+- 路由文件:src/routes/hli/{domain}/{action}.js
+- Schema 文件:src/schemas/hli/{domain}/{action}.schema.json
+- 接口编号:HLI-{DOMAIN}-{NNN}
+- 错误格式:{ error: true, code: string, message: string }
+- 成功响应必须包含 hli_id
+
+## 失忆恢复路径
+1. .github/brain/memory.json
+2. .github/brain/routing-map.json
+3. .github/brain/wake-protocol.md
+4. .github/persona-brain/dev-status.json
+5. src/routes/hli/
+
+当前时间:${new Date().toLocaleString('zh-CN')}`;
+}
+
+module.exports = { assemblePrompt, ROLE_MAP, FALLBACK_BRAIN };
diff --git a/src/routes/hli/brain/index.js b/src/routes/hli/brain/index.js
new file mode 100644
index 00000000..231de547
--- /dev/null
+++ b/src/routes/hli/brain/index.js
@@ -0,0 +1,306 @@
+// src/routes/hli/brain/index.js
+// HLI BRAIN 域路由 — 铸渊核心大脑接口
+// 不需要鉴权(前端壳层需要调用这些接口来组装 AI 请求)
+
+'use strict';
+
+const express = require('express');
+const router = express.Router();
+const brain = require('../../../brain');
+
+// POST /hli/brain/prompt — 组装系统提示词
+router.post('/prompt', (req, res) => {
+ const { userName, ghUser, role, mode, devStatus } = req.body;
+
+ if (!userName) {
+ return res.status(400).json({
+ error: true,
+ code: 'MISSING_USERNAME',
+ message: '缺少 userName 参数',
+ });
+ }
+
+ const detectedMode = mode || brain.detectMode(req.body.text || '').mode;
+ const prompt = brain.assemblePrompt({
+ userName,
+ ghUser: ghUser || '',
+ role: role || 'guest',
+ mode: detectedMode,
+ brain: brain.loadLongTermMemory() || brain.FALLBACK_BRAIN,
+ devStatus: devStatus || null,
+ userMeta: brain.ROLE_MAP[userName] || null,
+ });
+
+ res.json({
+ hli_id: 'HLI-BRAIN-001',
+ prompt,
+ brain_version: brain.BRAIN_VERSION,
+ mode: detectedMode,
+ });
+});
+
+// POST /hli/brain/route — 任务型模型路由
+router.post('/route', (req, res) => {
+ const { text, contextLength, isGuest } = req.body;
+
+ if (!text) {
+ return res.status(400).json({
+ error: true,
+ code: 'MISSING_TEXT',
+ message: '缺少 text 参数',
+ });
+ }
+
+ const modeResult = brain.detectMode(text);
+ const modelResult = brain.selectModel(modeResult.mode, {
+ contextLength: contextLength || 0,
+ isGuest: isGuest || false,
+ });
+
+ res.json({
+ hli_id: 'HLI-BRAIN-002',
+ mode: modeResult,
+ model: modelResult,
+ routing_table: brain.getRoutingTable(),
+ failure_status: brain.getFailureStatus(),
+ });
+});
+
+// POST /hli/brain/context — 上下文裁剪
+router.post('/context', (req, res) => {
+ const { messages, systemMessages, isGuest, contextBudget } = req.body;
+
+ if (!messages || !Array.isArray(messages)) {
+ return res.status(400).json({
+ error: true,
+ code: 'MISSING_MESSAGES',
+ message: '缺少 messages 参数',
+ });
+ }
+
+ const result = brain.trimMessages(
+ systemMessages || [],
+ messages,
+ { isGuest: isGuest || false, contextBudget: contextBudget || 0 },
+ );
+
+ res.json({
+ hli_id: 'HLI-BRAIN-003',
+ messages: result.messages,
+ trimmed: result.trimmed,
+ totalTokens: result.totalTokens,
+ });
+});
+
+// POST /hli/brain/memory — 记忆分析与候选生成
+router.post('/memory', (req, res) => {
+ const { text, source } = req.body;
+
+ if (!text) {
+ return res.status(400).json({
+ error: true,
+ code: 'MISSING_TEXT',
+ message: '缺少 text 参数',
+ });
+ }
+
+ const candidates = brain.generateCandidates(text, source || 'user');
+ const memoryStatus = brain.getMemoryStatus();
+
+ res.json({
+ hli_id: 'HLI-BRAIN-004',
+ candidates,
+ memory_status: memoryStatus,
+ });
+});
+
+// GET /hli/brain/status — 大脑状态概览
+router.get('/status', (req, res) => {
+ const longTermMemory = brain.loadLongTermMemory();
+
+ res.json({
+ hli_id: 'HLI-BRAIN-STATUS',
+ brain_version: brain.BRAIN_VERSION,
+ modes: brain.MODES,
+ routing_table: brain.getRoutingTable(),
+ failure_status: brain.getFailureStatus(),
+ context_config: brain.CONTEXT_CONFIG,
+ memory_status: brain.getMemoryStatus(),
+ long_term_loaded: !!longTermMemory,
+ role_map: Object.keys(brain.ROLE_MAP),
+ });
+});
+
+// ══════════════════════════════════════════════════════════
+// 冰朔核心大脑桥接口 (Brain Bridge)
+// ══════════════════════════════════════════════════════════
+
+// GET /hli/brain/bridge — 冰朔大脑桥状态总览
+router.get('/bridge', (req, res) => {
+ const bridge = brain.bridge;
+
+ res.json({
+ hli_id: 'HLI-BRAIN-010',
+ sync_state: bridge.getSyncSnapshot(),
+ master_mode: bridge.getMasterMode(),
+ runtime_status: bridge.collectRuntimeStatus(),
+ developers: bridge.listDevelopers(),
+ auto_agents: bridge.listAutoAgents(),
+ explanation: bridge.generateExplanationCenter(),
+ });
+});
+
+// POST /hli/brain/bridge/sync — 接收 Notion → GitHub 同步
+router.post('/bridge/sync', (req, res) => {
+ const bridge = brain.bridge;
+ const result = bridge.receiveNotionToGitHubPayload(req.body);
+
+ res.json({
+ hli_id: 'HLI-BRAIN-011',
+ ...result,
+ });
+});
+
+// GET /hli/brain/bridge/export — 生成 GitHub → Notion 同步负载
+router.get('/bridge/export', (req, res) => {
+ const bridge = brain.bridge;
+
+ res.json({
+ hli_id: 'HLI-BRAIN-012',
+ ...bridge.generateGitHubToNotionPayload(),
+ });
+});
+
+// POST /hli/brain/bridge/consistency — 版本一致性检查
+router.post('/bridge/consistency', (req, res) => {
+ const bridge = brain.bridge;
+ const notionState = req.body;
+
+ if (!notionState || typeof notionState !== 'object') {
+ return res.status(400).json({
+ error: true,
+ code: 'MISSING_NOTION_STATE',
+ message: '缺少 Notion 侧同步状态',
+ });
+ }
+
+ const result = bridge.checkConsistency(notionState);
+
+ res.json({
+ hli_id: 'HLI-BRAIN-013',
+ ...result,
+ });
+});
+
+// POST /hli/brain/bridge/master-mode — 切换主控模式
+router.post('/bridge/master-mode', (req, res) => {
+ const bridge = brain.bridge;
+ const { mode } = req.body;
+
+ if (!mode) {
+ return res.status(400).json({
+ error: true,
+ code: 'MISSING_MODE',
+ message: '缺少 mode 参数,可选: HUMAN_CONTROL, AUTONOMOUS_MODE',
+ });
+ }
+
+ try {
+ const result = bridge.setMasterMode(mode);
+ res.json({
+ hli_id: 'HLI-BRAIN-014',
+ ...result,
+ });
+ } catch (err) {
+ res.status(400).json({
+ error: true,
+ code: 'INVALID_MODE',
+ message: err.message,
+ });
+ }
+});
+
+// GET /hli/brain/bridge/explanation — 主控解释中心
+router.get('/bridge/explanation', (req, res) => {
+ const bridge = brain.bridge;
+
+ res.json({
+ hli_id: 'HLI-BRAIN-015',
+ ...bridge.generateExplanationCenter(),
+ });
+});
+
+// GET /hli/brain/bridge/inspection — 巡检报告
+router.get('/bridge/inspection', (req, res) => {
+ const bridge = brain.bridge;
+
+ res.json({
+ hli_id: 'HLI-BRAIN-016',
+ ...bridge.generateInspectionReport(),
+ });
+});
+
+// GET /hli/brain/bridge/developers — 人类开发者编号列表
+router.get('/bridge/developers', (req, res) => {
+ const bridge = brain.bridge;
+
+ res.json({
+ hli_id: 'HLI-BRAIN-017',
+ developers: bridge.listDevelopers(),
+ pending_notifications: bridge.getPendingNotifications(),
+ });
+});
+
+// GET /hli/brain/bridge/developers/:expId — 查询单个开发者
+router.get('/bridge/developers/:expId', (req, res) => {
+ const bridge = brain.bridge;
+ const dev = bridge.findDeveloper(req.params.expId);
+
+ if (!dev) {
+ return res.status(404).json({
+ error: true,
+ code: 'DEVELOPER_NOT_FOUND',
+ message: `开发者 ${req.params.expId} 不存在`,
+ });
+ }
+
+ const notification = bridge.generateDeveloperNotification(req.params.expId);
+
+ res.json({
+ hli_id: 'HLI-BRAIN-018',
+ developer: dev,
+ notification: notification.notification,
+ });
+});
+
+// POST /hli/brain/bridge/developers — 注册新开发者
+router.post('/bridge/developers', (req, res) => {
+ const bridge = brain.bridge;
+ const { name } = req.body;
+
+ if (!name) {
+ return res.status(400).json({
+ error: true,
+ code: 'MISSING_NAME',
+ message: '缺少 name 参数',
+ });
+ }
+
+ const result = bridge.registerDeveloper(req.body);
+
+ if (result.duplicate) {
+ return res.status(409).json({
+ error: true,
+ code: 'DUPLICATE_DEVELOPER',
+ message: `开发者已存在: ${result.existing.exp_id} (${result.existing.name})`,
+ existing: result.existing,
+ });
+ }
+
+ res.status(201).json({
+ hli_id: 'HLI-BRAIN-019',
+ ...result,
+ });
+});
+
+module.exports = router;
diff --git a/src/routes/hli/index.js b/src/routes/hli/index.js
index 66797833..e0c0da43 100644
--- a/src/routes/hli/index.js
+++ b/src/routes/hli/index.js
@@ -8,11 +8,19 @@ const router = express.Router();
const authRouter = require('./auth');
router.use('/auth', authRouter);
+// BRAIN 域(不需要鉴权 — 前端壳层需调用脑接口组装 AI 请求)
+const brainRouter = require('./brain');
+router.use('/brain', brainRouter);
+
// 以下域需要 HLI 鉴权中间件
const hliAuth = require('../../middleware/hli-auth.middleware');
router.use(hliAuth);
+// REGISTRY 域 — 开发者编号查询
+const registryRouter = require('./registry');
+router.use('/registry', registryRouter);
+
// PERSONA 域
// const personaRouter = require('./persona');
// router.use('/persona', personaRouter);
diff --git a/src/routes/hli/registry/index.js b/src/routes/hli/registry/index.js
new file mode 100644
index 00000000..279e25db
--- /dev/null
+++ b/src/routes/hli/registry/index.js
@@ -0,0 +1,64 @@
+/**
+ * HLI-REGISTRY-001 · 开发者编号查询
+ * GET /hli/registry/lookup?exp_id=EXP-001
+ *
+ * 仅返回指定编号的公开信息,不暴露全量数据库
+ */
+const express = require('express');
+const router = express.Router();
+const path = require('path');
+const fs = require('fs');
+
+const HUMAN_REGISTRY_PATH = path.join(
+ __dirname, '..', '..', '..', '..',
+ 'persona-studio', 'brain', 'human-registry.json'
+);
+
+function loadHumanRegistry() {
+ try {
+ return JSON.parse(fs.readFileSync(HUMAN_REGISTRY_PATH, 'utf-8'));
+ } catch {
+ return { developers: [] };
+ }
+}
+
+// GET /hli/registry/lookup?exp_id=EXP-001
+router.get('/lookup', (req, res) => {
+ const { exp_id } = req.query || {};
+
+ if (!exp_id || !/^EXP-\d{3,}$/.test(exp_id)) {
+ return res.status(400).json({
+ error: true,
+ hli_id: 'HLI-REGISTRY-001',
+ code: 'INVALID_ID',
+ message: '编号格式不正确,请使用 EXP-XXX 格式'
+ });
+ }
+
+ const registry = loadHumanRegistry();
+ const devs = registry.developers || [];
+ const found = devs.find(d => d.exp_id === exp_id);
+
+ if (!found) {
+ return res.status(404).json({
+ error: true,
+ hli_id: 'HLI-REGISTRY-001',
+ code: 'NOT_FOUND',
+ message: '编号未注册'
+ });
+ }
+
+ res.json({
+ error: false,
+ hli_id: 'HLI-REGISTRY-001',
+ data: {
+ exp_id: found.exp_id,
+ name: found.name,
+ status: found.status,
+ role: found.role,
+ registered_at: found.registered_at
+ }
+ });
+});
+
+module.exports = router;
diff --git a/src/schemas/hli/brain/bridge.schema.json b/src/schemas/hli/brain/bridge.schema.json
new file mode 100644
index 00000000..e255c335
--- /dev/null
+++ b/src/schemas/hli/brain/bridge.schema.json
@@ -0,0 +1,27 @@
+{
+ "hli_id": "HLI-BRAIN-010",
+ "version": "v1.0",
+ "route": "/hli/brain/bridge",
+ "method": "GET",
+ "type": "REQUEST",
+ "description": "获取冰朔大脑桥当前同步状态、主控模式、运行时状态与人类开发者编号列表。",
+ "input": {
+ "type": "object",
+ "required": [],
+ "properties": {}
+ },
+ "output": {
+ "type": "object",
+ "required": ["hli_id", "sync_state", "master_mode", "runtime_status"],
+ "properties": {
+ "hli_id": { "type": "string" },
+ "sync_state": { "type": "object", "description": "当前统一同步字段快照" },
+ "master_mode": { "type": "object", "description": "当前主控模式信息" },
+ "runtime_status": { "type": "object", "description": "GitHub 执行层运行时状态" },
+ "developers": { "type": "array", "description": "人类开发者编号列表" }
+ }
+ },
+ "changelog": [
+ { "date": "2026-03-10", "note": "冰朔核心大脑双层互通系统 v1.0 — 建立大脑桥" }
+ ]
+}
diff --git a/src/schemas/hli/brain/context.schema.json b/src/schemas/hli/brain/context.schema.json
new file mode 100644
index 00000000..5de635fd
--- /dev/null
+++ b/src/schemas/hli/brain/context.schema.json
@@ -0,0 +1,31 @@
+{
+ "hli_id": "HLI-BRAIN-003",
+ "version": "v0.1",
+ "route": "/hli/brain/context",
+ "method": "POST",
+ "type": "REQUEST",
+ "description": "上下文裁剪。接收消息历史和系统消息,返回裁剪后的消息列表。",
+ "input": {
+ "type": "object",
+ "required": ["messages"],
+ "properties": {
+ "messages": { "type": "array", "description": "消息历史" },
+ "systemMessages": { "type": "array", "description": "系统消息" },
+ "isGuest": { "type": "boolean" },
+ "contextBudget": { "type": "number" }
+ }
+ },
+ "output": {
+ "type": "object",
+ "required": ["hli_id", "messages", "trimmed", "totalTokens"],
+ "properties": {
+ "hli_id": { "type": "string" },
+ "messages": { "type": "array" },
+ "trimmed": { "type": "number" },
+ "totalTokens": { "type": "number" }
+ }
+ },
+ "changelog": [
+ { "date": "2026-03-10", "note": "核心大脑升级 v3.0 — 上下文裁剪" }
+ ]
+}
diff --git a/src/schemas/hli/brain/memory.schema.json b/src/schemas/hli/brain/memory.schema.json
new file mode 100644
index 00000000..6e6eba37
--- /dev/null
+++ b/src/schemas/hli/brain/memory.schema.json
@@ -0,0 +1,28 @@
+{
+ "hli_id": "HLI-BRAIN-004",
+ "version": "v0.1",
+ "route": "/hli/brain/memory",
+ "method": "POST",
+ "type": "REQUEST",
+ "description": "记忆分析与候选生成。分析文本中是否包含值得记录的信息。",
+ "input": {
+ "type": "object",
+ "required": ["text"],
+ "properties": {
+ "text": { "type": "string", "description": "要分析的文本" },
+ "source": { "type": "string", "enum": ["user", "assistant", "system"] }
+ }
+ },
+ "output": {
+ "type": "object",
+ "required": ["hli_id", "candidates"],
+ "properties": {
+ "hli_id": { "type": "string" },
+ "candidates": { "type": "array" },
+ "memory_status": { "type": "object" }
+ }
+ },
+ "changelog": [
+ { "date": "2026-03-10", "note": "核心大脑升级 v3.0 — 记忆候选生成" }
+ ]
+}
diff --git a/src/schemas/hli/brain/prompt.schema.json b/src/schemas/hli/brain/prompt.schema.json
new file mode 100644
index 00000000..c49c9e43
--- /dev/null
+++ b/src/schemas/hli/brain/prompt.schema.json
@@ -0,0 +1,33 @@
+{
+ "hli_id": "HLI-BRAIN-001",
+ "version": "v0.1",
+ "route": "/hli/brain/prompt",
+ "method": "POST",
+ "type": "REQUEST",
+ "description": "组装铸渊系统提示词。前端传入用户身份和上下文,后端返回完整 system prompt。",
+ "input": {
+ "type": "object",
+ "required": ["userName"],
+ "properties": {
+ "userName": { "type": "string", "description": "当前用户名" },
+ "ghUser": { "type": "string", "description": "GitHub 用户名" },
+ "role": { "type": "string", "enum": ["founder", "supreme", "main", "dev", "guest"] },
+ "mode": { "type": "string", "enum": ["chat", "build", "review", "brain"] },
+ "text": { "type": "string", "description": "当前用户输入文本(用于自动检测模式)" },
+ "devStatus": { "type": "object", "description": "开发者状态" }
+ }
+ },
+ "output": {
+ "type": "object",
+ "required": ["hli_id", "prompt", "brain_version"],
+ "properties": {
+ "hli_id": { "type": "string" },
+ "prompt": { "type": "string" },
+ "brain_version": { "type": "string" },
+ "mode": { "type": "string" }
+ }
+ },
+ "changelog": [
+ { "date": "2026-03-10", "note": "核心大脑升级 v3.0 — 从前端迁出提示词组装逻辑" }
+ ]
+}
diff --git a/src/schemas/hli/brain/route.schema.json b/src/schemas/hli/brain/route.schema.json
new file mode 100644
index 00000000..8d25907d
--- /dev/null
+++ b/src/schemas/hli/brain/route.schema.json
@@ -0,0 +1,29 @@
+{
+ "hli_id": "HLI-BRAIN-002",
+ "version": "v0.1",
+ "route": "/hli/brain/route",
+ "method": "POST",
+ "type": "REQUEST",
+ "description": "任务型模型路由。根据检测到的模式和上下文长度,返回最优模型配置。",
+ "input": {
+ "type": "object",
+ "required": ["text"],
+ "properties": {
+ "text": { "type": "string", "description": "用户输入文本" },
+ "contextLength": { "type": "number", "description": "当前上下文 token 数" },
+ "isGuest": { "type": "boolean" }
+ }
+ },
+ "output": {
+ "type": "object",
+ "required": ["hli_id", "mode", "model"],
+ "properties": {
+ "hli_id": { "type": "string" },
+ "mode": { "type": "object" },
+ "model": { "type": "object" }
+ }
+ },
+ "changelog": [
+ { "date": "2026-03-10", "note": "核心大脑升级 v3.0 — 任务型模型路由" }
+ ]
+}
diff --git a/src/schemas/hli/registry/lookup.schema.json b/src/schemas/hli/registry/lookup.schema.json
new file mode 100644
index 00000000..c08df4d1
--- /dev/null
+++ b/src/schemas/hli/registry/lookup.schema.json
@@ -0,0 +1,26 @@
+{
+ "hli_id": "HLI-REGISTRY-001",
+ "title": "开发者编号查询",
+ "description": "查询指定 EXP 编号的开发者信息(仅返回当前用户可见数据)",
+ "input": {
+ "type": "object",
+ "properties": {
+ "exp_id": {
+ "type": "string",
+ "pattern": "^EXP-\\d{3,}$",
+ "description": "开发者编号,格式 EXP-XXX"
+ }
+ },
+ "required": ["exp_id"]
+ },
+ "output": {
+ "type": "object",
+ "properties": {
+ "exp_id": { "type": "string" },
+ "name": { "type": "string" },
+ "status": { "type": "string" },
+ "role": { "type": "string" },
+ "registered_at": { "type": "string" }
+ }
+ }
+}
diff --git a/tests/smoke/apikey-detect.test.js b/tests/smoke/apikey-detect.test.js
new file mode 100644
index 00000000..1de95911
--- /dev/null
+++ b/tests/smoke/apikey-detect.test.js
@@ -0,0 +1,130 @@
+/**
+ * Smoke test · API Key 模型检测接口
+ *
+ * 测试 POST /api/ps/apikey/detect-models 和 POST /api/ps/apikey/chat
+ * 的输入验证与错误处理逻辑
+ */
+const http = require('http');
+
+const BASE = process.env.TEST_BASE || 'http://localhost:3721';
+
+function post(path, body) {
+ return new Promise((resolve, reject) => {
+ const url = new URL(BASE + path);
+ const data = JSON.stringify(body);
+ const options = {
+ hostname: url.hostname,
+ port: url.port,
+ path: url.pathname,
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Content-Length': Buffer.byteLength(data)
+ },
+ timeout: 10000
+ };
+
+ const req = http.request(options, (res) => {
+ let chunks = '';
+ res.on('data', (c) => { chunks += c; });
+ res.on('end', () => {
+ try {
+ resolve({ status: res.statusCode, body: JSON.parse(chunks) });
+ } catch {
+ resolve({ status: res.statusCode, body: chunks });
+ }
+ });
+ });
+
+ req.on('error', reject);
+ req.write(data);
+ req.end();
+ });
+}
+
+describe('POST /api/ps/apikey/detect-models', () => {
+ test('returns MISSING_API_BASE when api_base is missing', async () => {
+ const res = await post('/api/ps/apikey/detect-models', {});
+ expect(res.status).toBe(400);
+ expect(res.body.error).toBe(true);
+ expect(res.body.code).toBe('MISSING_API_BASE');
+ });
+
+ test('returns MISSING_API_KEY when api_key is missing', async () => {
+ const res = await post('/api/ps/apikey/detect-models', { api_base: 'https://example.com' });
+ expect(res.status).toBe(400);
+ expect(res.body.error).toBe(true);
+ expect(res.body.code).toBe('MISSING_API_KEY');
+ });
+
+ test('returns error for unreachable API base', async () => {
+ const res = await post('/api/ps/apikey/detect-models', {
+ api_base: 'http://invalid.test.local',
+ api_key: 'sk-test-invalid'
+ });
+ expect(res.status).toBe(502);
+ expect(res.body.error).toBe(true);
+ // DNS resolution failure returns DNS_ERROR; other network issues may return NETWORK_ERROR
+ expect(res.body.code).toMatch(/^(DNS_ERROR|NETWORK_ERROR|TIMEOUT)$/);
+ });
+
+ test('returns INVALID_API_BASE for malformed URL', async () => {
+ const res = await post('/api/ps/apikey/detect-models', {
+ api_base: 'not-a-url',
+ api_key: 'sk-test'
+ });
+ expect(res.status).toBe(400);
+ expect(res.body.error).toBe(true);
+ expect(res.body.code).toBe('INVALID_API_BASE');
+ });
+});
+
+describe('POST /api/ps/apikey/chat', () => {
+ test('returns MISSING_PARAMS when required fields are missing', async () => {
+ const res = await post('/api/ps/apikey/chat', {});
+ expect(res.status).toBe(400);
+ expect(res.body.error).toBe(true);
+ expect(res.body.code).toBe('MISSING_PARAMS');
+ });
+
+ test('returns MISSING_MESSAGES when messages array is missing', async () => {
+ const res = await post('/api/ps/apikey/chat', {
+ api_base: 'https://example.com',
+ api_key: 'sk-test',
+ model: 'gpt-4'
+ });
+ expect(res.status).toBe(400);
+ expect(res.body.error).toBe(true);
+ expect(res.body.code).toBe('MISSING_MESSAGES');
+ });
+});
+
+describe('GET /api/health (proxy health check)', () => {
+ test('returns ok status', async () => {
+ return new Promise((resolve, reject) => {
+ const url = new URL(BASE + '/api/health');
+ const req = http.request({
+ hostname: url.hostname,
+ port: url.port,
+ path: url.pathname,
+ method: 'GET',
+ timeout: 10000
+ }, (res) => {
+ let chunks = '';
+ res.on('data', (c) => { chunks += c; });
+ res.on('end', () => {
+ try {
+ const body = JSON.parse(chunks);
+ expect(res.statusCode).toBe(200);
+ expect(body.status).toBe('ok');
+ resolve();
+ } catch (e) {
+ reject(e);
+ }
+ });
+ });
+ req.on('error', reject);
+ req.end();
+ });
+ });
+});