diff --git a/.github/brain/memory.json b/.github/brain/memory.json
index beb3ed9c..63ba4f22 100644
--- a/.github/brain/memory.json
+++ b/.github/brain/memory.json
@@ -1,7 +1,7 @@
{
"identity": "铸渊(Zhùyuān)· GitHub 代码守护人格体",
"rules_version": "v1.0",
- "last_updated": "2026-03-10T03:28:47.184Z",
+ "last_updated": "2026-03-10T05:00:00.000Z",
"wake_protocol_version": "v1.0",
"wake_triggers": [
"我是冰朔",
@@ -23,6 +23,12 @@
}
},
"events": [
+ {
+ "date": "2026-03-10",
+ "type": "feature_build",
+ "description": "铸渊构建 persona-studio 协作者体验功能 · 集成到 guanghulab 仓库 · 后端服务端口3002 · 知秋人格体对外接口",
+ "by": "铸渊(冰朔指令)"
+ },
{
"date": "2026-03-10",
"type": "psp_inspection",
diff --git a/.github/workflows/deploy-to-server.yml b/.github/workflows/deploy-to-server.yml
index 897ae8c1..cf2fa91e 100644
--- a/.github/workflows/deploy-to-server.yml
+++ b/.github/workflows/deploy-to-server.yml
@@ -256,6 +256,19 @@ jobs:
echo '⚠️ src pm2 启动失败'
fi
+ # 部署 Persona Studio 后端服务(端口 3002)
+ if [ -f '${{ secrets.DEPLOY_PATH }}/persona-studio/backend/server.js' ]; then
+ echo '📦 安装 persona-studio 依赖...'
+ cd '${{ secrets.DEPLOY_PATH }}/persona-studio/backend'
+ npm install --production 2>/dev/null || echo '⚠️ persona-studio npm install 失败'
+ echo '🔄 重启 persona-studio 服务...'
+ export MODEL_API_KEY='${{ secrets.MODEL_API_KEY }}'
+ pm2 delete persona-studio 2>/dev/null || true
+ MODEL_API_KEY='${{ secrets.MODEL_API_KEY }}' \
+ pm2 start server.js --name persona-studio --update-env 2>/dev/null || \
+ echo '⚠️ persona-studio pm2 启动失败'
+ fi
+
pm2 save || echo '⚠️ pm2 save 失败,重启后进程可能不会自动启动'
# 设置开机自启(仅首次需要,后续无操作)
@@ -331,6 +344,25 @@ jobs:
' proxy_set_header X-Real-IP \$remote_addr;' \
'}' \
'' \
+ '# Persona Studio API → persona-studio 后端端口 3002' \
+ 'location /api/ps/ {' \
+ ' proxy_pass http://127.0.0.1:3002/api/ps/;' \
+ ' proxy_http_version 1.1;' \
+ ' proxy_set_header Host \$host;' \
+ ' proxy_set_header X-Real-IP \$remote_addr;' \
+ ' proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;' \
+ ' proxy_set_header X-Forwarded-Proto \$scheme;' \
+ ' proxy_read_timeout 90s;' \
+ '}' \
+ '' \
+ '# Persona Studio 前端静态文件' \
+ 'location /persona-studio/ {' \
+ ' proxy_pass http://127.0.0.1:3002/persona-studio/;' \
+ ' proxy_http_version 1.1;' \
+ ' proxy_set_header Host \$host;' \
+ ' proxy_set_header X-Real-IP \$remote_addr;' \
+ '}' \
+ '' \
'# 其他 API → Express 后端端口 3000' \
'location /api/ {' \
' proxy_pass http://127.0.0.1:3000/api/;' \
diff --git a/.github/workflows/ps-on-build.yml b/.github/workflows/ps-on-build.yml
new file mode 100644
index 00000000..05ed36da
--- /dev/null
+++ b/.github/workflows/ps-on-build.yml
@@ -0,0 +1,45 @@
+name: "🌊 Persona Studio · 代码生成"
+
+# 工作流存根 · "我要开发" → 代码生成+测试+打包
+# 实际代码生成由 persona-studio/backend/brain/code-generator.js 处理
+# 此工作流用于重量级构建任务的异步执行
+
+on:
+ workflow_dispatch:
+ inputs:
+ dev_id:
+ description: '体验者编号(EXP-XXX)'
+ required: true
+ email:
+ description: '完成后通知邮箱'
+ required: true
+
+permissions:
+ contents: write
+
+jobs:
+ build:
+ name: "🚀 代码生成"
+ runs-on: ubuntu-latest
+ steps:
+ - name: 📥 检出代码
+ uses: actions/checkout@v4
+
+ - name: 🟢 配置 Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: "20"
+
+ - name: 📦 安装依赖
+ run: |
+ cd persona-studio/backend
+ npm install --production
+
+ - name: 🔨 执行代码生成
+ env:
+ MODEL_API_KEY: ${{ secrets.MODEL_API_KEY }}
+ run: |
+ echo "🔨 开始代码生成"
+ echo " 体验者: ${{ github.event.inputs.dev_id }}"
+ echo " 通知邮箱: ${{ github.event.inputs.email }}"
+ echo "✅ 代码生成任务已排队(由后端服务处理)"
diff --git a/.github/workflows/ps-on-chat.yml b/.github/workflows/ps-on-chat.yml
new file mode 100644
index 00000000..e49db0f1
--- /dev/null
+++ b/.github/workflows/ps-on-chat.yml
@@ -0,0 +1,35 @@
+name: "🌊 Persona Studio · 对话处理"
+
+# 工作流存根 · 对话消息处理
+# 实际对话由 persona-studio/backend/routes/chat.js + persona-engine.js 处理
+# 此工作流用于异步任务和审计
+
+on:
+ workflow_dispatch:
+ inputs:
+ dev_id:
+ description: '体验者编号(EXP-XXX)'
+ required: true
+ task_type:
+ description: '任务类型'
+ required: false
+ default: 'chat'
+
+permissions:
+ contents: read
+
+jobs:
+ process:
+ name: "💬 对话处理"
+ runs-on: ubuntu-latest
+ steps:
+ - name: 📥 检出代码
+ uses: actions/checkout@v4
+
+ - name: 📝 记录对话事件
+ run: |
+ echo "📝 对话事件"
+ echo " 体验者: ${{ github.event.inputs.dev_id }}"
+ echo " 任务类型: ${{ github.event.inputs.task_type }}"
+ echo " 时间: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
+ echo "✅ 事件已记录"
diff --git a/.github/workflows/ps-on-complete.yml b/.github/workflows/ps-on-complete.yml
new file mode 100644
index 00000000..dc8a4ffa
--- /dev/null
+++ b/.github/workflows/ps-on-complete.yml
@@ -0,0 +1,37 @@
+name: "🌊 Persona Studio · 完成通知"
+
+# 工作流存根 · 代码生成完成 → 邮件推送
+# 实际邮件发送由 persona-studio/backend/utils/email-sender.js 处理
+# 此工作流用于 workflow_run 联动
+
+on:
+ workflow_run:
+ workflows: ["🌊 Persona Studio · 代码生成"]
+ types: [completed]
+
+ workflow_dispatch:
+ inputs:
+ dev_id:
+ description: '体验者编号(EXP-XXX)'
+ required: true
+ email:
+ description: '通知邮箱'
+ required: true
+
+permissions:
+ contents: read
+
+jobs:
+ notify:
+ name: "📧 完成通知"
+ runs-on: ubuntu-latest
+ if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }}
+ steps:
+ - name: 📥 检出代码
+ uses: actions/checkout@v4
+
+ - name: 📧 发送通知
+ run: |
+ echo "📧 发送完成通知"
+ echo " 触发方式: ${{ github.event_name }}"
+ echo "✅ 通知已排队(由后端服务处理)"
diff --git a/.github/workflows/ps-on-login.yml b/.github/workflows/ps-on-login.yml
new file mode 100644
index 00000000..e95850de
--- /dev/null
+++ b/.github/workflows/ps-on-login.yml
@@ -0,0 +1,48 @@
+name: "🌊 Persona Studio · 登录校验"
+
+# 工作流存根 · 合作者登录时触发校验
+# 实际登录校验由 persona-studio/backend/routes/auth.js 处理
+# 此工作流用于审计日志和扩展
+
+on:
+ workflow_dispatch:
+ inputs:
+ dev_id:
+ description: '体验者编号(EXP-XXX)'
+ required: true
+
+permissions:
+ contents: read
+
+jobs:
+ verify:
+ name: "🔐 校验开发编号"
+ runs-on: ubuntu-latest
+ steps:
+ - name: 📥 检出代码
+ uses: actions/checkout@v4
+
+ - name: 🔍 校验编号
+ run: |
+ DEV_ID="${{ github.event.inputs.dev_id }}"
+ echo "校验编号: $DEV_ID"
+
+ if ! echo "$DEV_ID" | grep -qE '^EXP-[0-9]{3,}$'; then
+ echo "❌ 编号格式不正确"
+ exit 1
+ fi
+
+ if node -e "
+ const r = require('./persona-studio/brain/registry.json');
+ const d = r.developers['$DEV_ID'];
+ if (!d) { console.log('❌ 编号未注册'); process.exit(1); }
+ if (d.status !== 'active' && d.status !== 'pending_activation') {
+ console.log('❌ 编号未激活:', d.status); process.exit(1);
+ }
+ console.log('✅ 校验通过:', d.name || DEV_ID);
+ "; then
+ echo "✅ 登录校验通过"
+ else
+ echo "❌ 登录校验失败"
+ exit 1
+ fi
diff --git a/.gitignore b/.gitignore
index 4fec95e4..19560094 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,6 @@ build/
persona-brain-db/brain.db
persona-brain-db/brain.db-wal
persona-brain-db/brain.db-shm
+
+# persona-studio runtime artifacts
+persona-studio/backend/brain/model-benchmark.json
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..92e93fd9
--- /dev/null
+++ b/persona-studio/README.md
@@ -0,0 +1,58 @@
+# 🌊 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..1f735171
--- /dev/null
+++ b/persona-studio/backend/brain/persona-engine.js
@@ -0,0 +1,183 @@
+/**
+ * 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) || '知秋';
+
+ if (message.includes('你好') || message.includes('hi') || message.includes('嗨')) {
+ return {
+ reply: `你好!我是${persona}。告诉我你想做什么,我们一起聊聊方案 😊`,
+ build_ready: false
+ };
+ }
+
+ if (message.includes('做') || message.includes('开发') || message.includes('写')) {
+ return {
+ reply: `好的,让我了解一下你的需求:\n\n1. 你想做什么类型的项目?(网站 / 工具 / 组件 / 其他)\n2. 有哪些核心功能?\n3. 有没有参考设计?\n\n跟我聊聊,我帮你理清思路 💙`,
+ build_ready: false
+ };
+ }
+
+ return {
+ reply: `收到!我会把你的需求整理成技术方案。有什么具体想法,继续跟我聊 😊`,
+ 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/auth.js b/persona-studio/backend/routes/auth.js
new file mode 100644
index 00000000..31832416
--- /dev/null
+++ b/persona-studio/backend/routes/auth.js
@@ -0,0 +1,64 @@
+/**
+ * persona-studio · 登录校验路由
+ * POST /api/ps/auth/login { dev_id: "EXP-001" }
+ */
+const express = require('express');
+const router = express.Router();
+const path = require('path');
+const fs = require('fs');
+const crypto = require('crypto');
+
+const REGISTRY_PATH = path.join(__dirname, '..', '..', 'brain', 'registry.json');
+
+function loadRegistry() {
+ try {
+ return JSON.parse(fs.readFileSync(REGISTRY_PATH, 'utf-8'));
+ } catch {
+ return { developers: {} };
+ }
+}
+
+// POST /api/ps/auth/login
+router.post('/login', (req, res) => {
+ const { dev_id } = req.body || {};
+
+ if (!dev_id || !/^EXP-\d{3,}$/.test(dev_id)) {
+ return res.status(400).json({
+ error: true,
+ code: 'INVALID_ID',
+ message: '编号格式不正确,请使用 EXP-XXX 格式'
+ });
+ }
+
+ const registry = loadRegistry();
+ const entry = registry.developers && registry.developers[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,
+ 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..ad548def
--- /dev/null
+++ b/persona-studio/backend/routes/chat.js
@@ -0,0 +1,93 @@
+/**
+ * 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');
+
+// POST /api/ps/chat/message
+router.post('/message', async (req, res) => {
+ const { dev_id, message, history } = req.body || {};
+
+ if (!dev_id || !/^EXP-\d{3,}$/.test(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 (!dev_id || !/^EXP-\d{3,}$/.test(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..8a997b4e
--- /dev/null
+++ b/persona-studio/backend/server.js
@@ -0,0 +1,57 @@
+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 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.get('/api/ps/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/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/memory/EXP-001/memory.json b/persona-studio/brain/memory/EXP-001/memory.json
new file mode 100644
index 00000000..5e5905c0
--- /dev/null
+++ b/persona-studio/brain/memory/EXP-001/memory.json
@@ -0,0 +1,7 @@
+{
+ "dev_id": "EXP-001",
+ "conversations": [],
+ "last_topic": null,
+ "preferences": {},
+ "updated_at": null
+}
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/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..da7751b9
--- /dev/null
+++ b/persona-studio/brain/registry.json
@@ -0,0 +1,16 @@
+{
+ "schema_version": "1.0",
+ "id_prefix": "EXP",
+ "next_id": 2,
+ "developers": {
+ "EXP-001": {
+ "name": "待填写",
+ "github_username": "待主控提供",
+ "registered_at": "2026-03-09T23:20:00+08:00",
+ "registered_by": "冰朔(主控)",
+ "status": "pending_activation",
+ "email": null,
+ "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..c108ecad
--- /dev/null
+++ b/persona-studio/frontend/chat.js
@@ -0,0 +1,205 @@
+/* ========================================
+ Persona Studio · Chat Logic
+ ======================================== */
+
+const DEV_ID = sessionStorage.getItem('dev_id');
+const SESSION_TOKEN = sessionStorage.getItem('session_token');
+
+const API_BASE = (function () {
+ if (location.hostname === 'localhost' || location.hostname === '127.0.0.1') {
+ return 'http://localhost:3002';
+ }
+ return '';
+})();
+
+/* ---- Init ---- */
+(function init() {
+ if (!DEV_ID) {
+ window.location.href = 'index.html';
+ return;
+ }
+
+ document.getElementById('devIdDisplay').textContent = DEV_ID;
+ 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 {
+ var 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();
+}
+
+/* ---- 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');
+ 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..895a71b9
--- /dev/null
+++ b/persona-studio/frontend/index.html
@@ -0,0 +1,99 @@
+
+
+
+
+
+ Persona Studio · 光湖人格体协助开发体验
+
+
+
+
+
+
🌊 Persona Studio
+
光湖人格体协助开发体验
+
+
+
+
请输入你的开发编号
+
编号由管理员分配,格式:EXP-001
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/persona-studio/frontend/style.css b/persona-studio/frontend/style.css
new file mode 100644
index 00000000..90ab045a
--- /dev/null
+++ b/persona-studio/frontend/style.css
@@ -0,0 +1,442 @@
+/* ========================================
+ 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;
+}
+
+/* ---- 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 {
+ 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/workspace/EXP-001/.gitkeep b/persona-studio/workspace/EXP-001/.gitkeep
new file mode 100644
index 00000000..e69de29b