diff --git a/.github/workflows/deploy-to-server.yml b/.github/workflows/deploy-to-server.yml
index 81c92290..d094fd71 100644
--- a/.github/workflows/deploy-to-server.yml
+++ b/.github/workflows/deploy-to-server.yml
@@ -538,6 +538,19 @@ jobs:
check_module '光湖集成壳' 'app'
check_module 'Persona Studio' 'persona-studio'
check_module '后端服务' 'backend'
+ check_module '用户中心' 'm05-user-center'
+ check_module '对话界面' 'm07-dialogue-ui'
+ check_module '云服务' 'm10-cloud'
+ check_module '模块管理' 'm11-module'
+ check_module '云盘' 'm15-cloud-drive'
+ check_module '登录' 'm01-login'
+ check_module '健康检查' 'm18-health-check'
+ check_module '冷启动' 'coldstart'
+ check_module '仪表盘' 'dashboard'
+ check_module '门户' 'portal'
+ check_module '通知' 'notification'
+ check_module '帮助中心' 'help-center'
+ check_module '前端' 'frontend'
echo ''
printf \"\$RESULTS\"
@@ -559,6 +572,59 @@ jobs:
"
echo "✅ 冒烟测试完成"
+ - name: 👤 人类使用侧验证
+ continue-on-error: true
+ run: |
+ echo "👤 人类使用侧验证(核心!系统测跑通 ≠ 人类测跑通)..."
+ echo "等待 10 秒让服务启动..."
+ sleep 10
+
+ HUMAN_CHECK_PASSED=0
+ HUMAN_CHECK_FAILED=0
+
+ # 验证主站可访问
+ HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 15 \
+ "http://${{ secrets.DEPLOY_HOST }}" 2>/dev/null || echo "000")
+ if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "301" ] || [ "$HTTP_CODE" = "302" ]; then
+ echo "✅ 主站 HTTP 响应: $HTTP_CODE"
+ HUMAN_CHECK_PASSED=$((HUMAN_CHECK_PASSED+1))
+ else
+ echo "❌ 主站 HTTP 响应: $HTTP_CODE(人类无法访问)"
+ HUMAN_CHECK_FAILED=$((HUMAN_CHECK_FAILED+1))
+ fi
+
+ # 验证 Persona Studio 可访问
+ PS_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 15 \
+ "http://${{ secrets.DEPLOY_HOST }}/persona-studio/" 2>/dev/null || echo "000")
+ if [ "$PS_CODE" = "200" ] || [ "$PS_CODE" = "301" ] || [ "$PS_CODE" = "302" ]; then
+ echo "✅ Persona Studio HTTP 响应: $PS_CODE"
+ HUMAN_CHECK_PASSED=$((HUMAN_CHECK_PASSED+1))
+ else
+ echo "⚠️ Persona Studio HTTP 响应: $PS_CODE"
+ HUMAN_CHECK_FAILED=$((HUMAN_CHECK_FAILED+1))
+ fi
+
+ # 验证后端 API 健康
+ API_CODE=$(ssh -i ~/.ssh/deploy_key \
+ "${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}" \
+ "curl -s -o /dev/null -w '%{http_code}' --max-time 5 http://localhost:3002/api/ps/health 2>/dev/null || echo '000'" \
+ 2>/dev/null || echo "000")
+ if [ "$API_CODE" = "200" ]; then
+ echo "✅ Persona Studio API 健康: $API_CODE"
+ HUMAN_CHECK_PASSED=$((HUMAN_CHECK_PASSED+1))
+ else
+ echo "⚠️ Persona Studio API 响应: $API_CODE"
+ HUMAN_CHECK_FAILED=$((HUMAN_CHECK_FAILED+1))
+ fi
+
+ echo ""
+ echo "━━━ 人类使用侧验证结果:${HUMAN_CHECK_PASSED} 通过 / ${HUMAN_CHECK_FAILED} 异常 ━━━"
+
+ echo "### 👤 人类使用侧验证" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "- 通过: ${HUMAN_CHECK_PASSED}" >> $GITHUB_STEP_SUMMARY
+ echo "- 异常: ${HUMAN_CHECK_FAILED}" >> $GITHUB_STEP_SUMMARY
+
- name: 📝 部署汇总
run: |
echo "### 🚀 全站部署完成" >> $GITHUB_STEP_SUMMARY
@@ -567,6 +633,7 @@ jobs:
echo "- 目标: \`${{ secrets.DEPLOY_HOST }}:${{ secrets.DEPLOY_PATH }}\`" >> $GITHUB_STEP_SUMMARY
echo "- 提交: \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY
echo "- 时间: $(TZ=Asia/Shanghai date '+%Y-%m-%d %H:%M CST')" >> $GITHUB_STEP_SUMMARY
+ echo "- 人类使用侧验证: 已执行" >> $GITHUB_STEP_SUMMARY
# ═══════════════════════════════════════════════════════════════
# Job 3: Notion 部署通知
diff --git a/app/index.html b/app/index.html
new file mode 100644
index 00000000..9e1ffd32
--- /dev/null
+++ b/app/index.html
@@ -0,0 +1,71 @@
+
+
+
+
+
+ 光湖集成壳 · HoloLake
+
+
+
+
+
🌊
+
光湖集成壳
+
🚧 此模块正在开发中
+
光湖集成壳是平台的核心应用外壳,负责统一加载与协调各功能模块的壳-核分离运行。即将完成。
+
← 返回主页
+
+
+
+
diff --git a/coldstart/index.html b/coldstart/index.html
new file mode 100644
index 00000000..3e4e5043
--- /dev/null
+++ b/coldstart/index.html
@@ -0,0 +1,71 @@
+
+
+
+
+
+ 冷启动 · HoloLake
+
+
+
+
+
🚀
+
冷启动
+
🚧 此模块正在开发中
+
冷启动模块负责系统的初始化引导流程,确保各服务按正确顺序启动并完成自检。即将就绪。
+
← 返回主页
+
+
+
+
diff --git a/data/users.json b/data/users.json
new file mode 100644
index 00000000..6143c301
--- /dev/null
+++ b/data/users.json
@@ -0,0 +1,39 @@
+{
+ "version": "1.0",
+ "description": "guanghulab 用户数据库 · 微信登录 + 开发者编号绑定",
+ "created_at": "2026-03-25T02:16:00Z",
+ "created_by": "ZY-HUMANSIDE-FIX-2026-0325-002",
+ "users": {},
+ "dev_id_map": {},
+ "roles": {
+ "admin": {
+ "permissions": ["chat", "dev", "log", "status", "admin", "system"],
+ "description": "系统管理员 · 全部功能 + 系统管理"
+ },
+ "developer": {
+ "permissions": ["chat", "dev", "log", "status"],
+ "description": "开发者 · 聊天 + 开发 + 日志 + 状态查看"
+ },
+ "visitor": {
+ "permissions": ["chat", "status"],
+ "description": "访客 · 聊天 + 状态查看(只读)"
+ }
+ },
+ "auth_methods": {
+ "wechat": {
+ "enabled": false,
+ "status": "pending_appid",
+ "note": "等待冰朔提供微信开放平台 AppID/AppSecret"
+ },
+ "dev_id": {
+ "enabled": true,
+ "status": "active",
+ "note": "开发者编号直接登录 · 服务器端验证"
+ },
+ "guest": {
+ "enabled": true,
+ "status": "active",
+ "note": "访客模式 · 无需登录"
+ }
+ }
+}
diff --git a/deploy/nginx/hololake.conf b/deploy/nginx/hololake.conf
new file mode 100644
index 00000000..37f9fc40
--- /dev/null
+++ b/deploy/nginx/hololake.conf
@@ -0,0 +1,139 @@
+# /etc/nginx/conf.d/hololake.conf
+# ═══════════════════════════════════════════════
+# guanghulab.com 统一入口 · Nginx 配置模板
+# 🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001
+# 📜 Copyright: 国作登字-2026-A-00037559
+# ═══════════════════════════════════════════════
+#
+# 使用方法:
+# 1. 复制到服务器: scp deploy/nginx/hololake.conf root@8.155.62.235:/etc/nginx/conf.d/
+# 2. 如果使用 Let's Encrypt: certbot --nginx -d guanghulab.com -d www.guanghulab.com
+# 3. 验证: nginx -t && systemctl reload nginx
+#
+# 端口映射:
+# 3721 → API 代理(AI 聊天,SSE 流式)
+# 3002 → Persona Studio 后端
+# 3000 → Express 主后端
+# 3001 → HLI 中间层
+# 8080 → WebSocket(状态看板实时推送)
+
+# HTTP → HTTPS 重定向(启用 HTTPS 后取消注释)
+# server {
+# listen 80;
+# server_name guanghulab.com www.guanghulab.com;
+# return 301 https://$host$request_uri;
+# }
+
+server {
+ listen 80;
+ # listen 443 ssl; # 启用 HTTPS 后取消注释
+ server_name guanghulab.com www.guanghulab.com;
+
+ # Let's Encrypt 证书(certbot 自动填充,启用 HTTPS 后取消注释)
+ # ssl_certificate /etc/letsencrypt/live/guanghulab.com/fullchain.pem;
+ # ssl_certificate_key /etc/letsencrypt/live/guanghulab.com/privkey.pem;
+
+ root /var/www/guanghulab;
+ index index.html;
+
+ # ── 前端静态文件(主站首页 = docs/index.html,已复制到根目录)──
+ location / {
+ try_files $uri $uri/ /index.html;
+ }
+
+ # ── AI 聊天 API 代理 → api-proxy.js 端口 3721(SSE 流式支持)──
+ location /api/chat {
+ proxy_pass http://127.0.0.1:3721/api/chat;
+ 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_set_header Connection "";
+ proxy_buffering off;
+ proxy_cache off;
+ chunked_transfer_encoding on;
+ proxy_read_timeout 120s;
+ proxy_send_timeout 60s;
+ }
+
+ # ── 模型列表 API ──
+ location /api/models {
+ proxy_pass http://127.0.0.1:3721/api/models;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ }
+
+ # ── Persona Studio API → 后端端口 3002 ──
+ location /api/ps/ {
+ add_header Access-Control-Allow-Origin * always;
+ add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always;
+ add_header Access-Control-Allow-Headers "Content-Type, Authorization, X-API-Key" always;
+ if ($request_method = OPTIONS) {
+ return 204;
+ }
+ 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 120s;
+ }
+
+ # ── 后端健康检查 ──
+ location /api/health {
+ proxy_pass http://127.0.0.1:3721/api/health;
+ 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/;
+ 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_cache_bypass $http_upgrade;
+ }
+
+ # ── Persona Studio WebSocket → 预览进度推送 ──
+ location /ws/preview {
+ proxy_pass http://127.0.0.1:3002;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "Upgrade";
+ 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_read_timeout 86400;
+ }
+
+ # ── WebSocket → 看板实时推送端口 8080 ──
+ location /ws {
+ proxy_pass http://127.0.0.1:8080;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "Upgrade";
+ 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_read_timeout 86400;
+ }
+
+ # ── 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;
+ }
+
+ # ── 错误页面 ──
+ error_page 404 /404.html;
+ error_page 500 502 503 504 /50x.html;
+}
diff --git a/frontend/index.html b/frontend/index.html
new file mode 100644
index 00000000..5e5a0491
--- /dev/null
+++ b/frontend/index.html
@@ -0,0 +1,71 @@
+
+
+
+
+
+ 前端 · HoloLake
+
+
+
+
+
🎨
+
前端
+
🚧 此模块正在开发中
+
前端模块正在构建光湖平台的用户界面与交互体验,采用现代化组件架构。开发进行中。
+
← 返回主页
+
+
+
+
diff --git a/help-center/index.html b/help-center/index.html
new file mode 100644
index 00000000..7582e2a4
--- /dev/null
+++ b/help-center/index.html
@@ -0,0 +1,71 @@
+
+
+
+
+
+ 帮助中心 · HoloLake
+
+
+
+
+
❓
+
帮助中心
+
🚧 此模块正在开发中
+
帮助中心将提供完善的文档、常见问题解答与使用指南,助您快速上手光湖平台。即将开放。
+
← 返回主页
+
+
+
+
diff --git a/m01-login/index.html b/m01-login/index.html
new file mode 100644
index 00000000..d298eccb
--- /dev/null
+++ b/m01-login/index.html
@@ -0,0 +1,71 @@
+
+
+
+
+
+ 登录 · HoloLake
+
+
+
+
+
🔐
+
登录
+
🚧 此模块正在开发中
+
登录模块将提供安全的身份认证与授权服务,支持多因素认证与统一身份管理。正在开发中。
+
← 返回主页
+
+
+
+
diff --git a/m10-cloud/index.html b/m10-cloud/index.html
new file mode 100644
index 00000000..e928a6f3
--- /dev/null
+++ b/m10-cloud/index.html
@@ -0,0 +1,71 @@
+
+
+
+
+
+ 云服务 · HoloLake
+
+
+
+
+
☁️
+
云服务
+
🚧 此模块正在开发中
+
云服务模块将提供光湖平台的云端基础设施管理能力,包括资源调度、服务编排与弹性伸缩。敬请期待。
+
← 返回主页
+
+
+
+
diff --git a/m15-cloud-drive/index.html b/m15-cloud-drive/index.html
new file mode 100644
index 00000000..e5796842
--- /dev/null
+++ b/m15-cloud-drive/index.html
@@ -0,0 +1,71 @@
+
+
+
+
+
+ 云盘 · HoloLake
+
+
+
+
+
💾
+
云盘
+
🚧 此模块正在开发中
+
云盘模块将提供安全可靠的文件存储与分享功能,支持多端同步、版本管理与协作共享。即将上线。
+
← 返回主页
+
+
+
+
diff --git a/m18-health-check/index.html b/m18-health-check/index.html
new file mode 100644
index 00000000..fd824376
--- /dev/null
+++ b/m18-health-check/index.html
@@ -0,0 +1,71 @@
+
+
+
+
+
+ 健康检查 · HoloLake
+
+
+
+
+
❤️
+
健康检查
+
🚧 此模块正在开发中
+
健康检查模块将实时监控系统各服务的运行状态,提供故障预警与自动恢复能力。感谢您的耐心等待。
+
← 返回主页
+
+
+
+
diff --git a/notification/index.html b/notification/index.html
new file mode 100644
index 00000000..a07e8073
--- /dev/null
+++ b/notification/index.html
@@ -0,0 +1,71 @@
+
+
+
+
+
+ 通知 · HoloLake
+
+
+
+
+
🔔
+
通知
+
🚧 此模块正在开发中
+
通知模块将提供多渠道消息推送能力,支持站内信、邮件、钉钉与飞书等通知方式。敬请期待。
+
← 返回主页
+
+
+
+
diff --git a/persona-studio/backend/routes/auth.js b/persona-studio/backend/routes/auth.js
index 9868aa6d..b31810a4 100644
--- a/persona-studio/backend/routes/auth.js
+++ b/persona-studio/backend/routes/auth.js
@@ -1,6 +1,11 @@
/**
* persona-studio · 登录校验路由
* POST /api/ps/auth/login { dev_id: "EXP-000" }
+ * GET /api/ps/auth/session { token }
+ * POST /api/ps/auth/wechat/callback { code } — 微信登录回调(待配置)
+ * POST /api/ps/auth/bind { openid, dev_id } — 绑定开发者编号(待配置)
+ *
+ * 📜 Copyright: 国作登字-2026-A-00037559
*/
const express = require('express');
const router = express.Router();
@@ -10,6 +15,21 @@ const crypto = require('crypto');
const HUMAN_REGISTRY_PATH = path.join(__dirname, '..', '..', 'brain', 'human-registry.json');
const REGISTRY_PATH = path.join(__dirname, '..', '..', 'brain', 'registry.json');
+const USERS_DB_PATH = path.join(__dirname, '..', '..', '..', 'data', 'users.json');
+
+// In-memory session store (server restart clears sessions)
+const sessions = new Map();
+const SESSION_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
+
+// Periodic session cleanup (every hour)
+setInterval(() => {
+ const now = Date.now();
+ for (const [token, session] of sessions) {
+ if (now - new Date(session.created_at).getTime() > SESSION_TTL_MS) {
+ sessions.delete(token);
+ }
+ }
+}, 60 * 60 * 1000);
function loadHumanRegistry() {
try {
@@ -27,6 +47,14 @@ function loadRegistry() {
}
}
+function loadUsersDB() {
+ try {
+ return JSON.parse(fs.readFileSync(USERS_DB_PATH, 'utf-8'));
+ } catch {
+ return { users: {}, dev_id_map: {} };
+ }
+}
+
function findDeveloper(devId) {
const humanReg = loadHumanRegistry();
if (humanReg.developers && Array.isArray(humanReg.developers)) {
@@ -63,12 +91,21 @@ router.post('/login', (req, res) => {
}
const token = crypto.randomBytes(32).toString('hex');
+ sessions.set(token, {
+ dev_id: 'GUEST',
+ name: guestConfig.name || '访客体验者',
+ role: 'visitor',
+ created_at: new Date().toISOString()
+ });
+
return res.json({
error: false,
dev_id: 'GUEST',
name: guestConfig.name || '访客体验者',
status: 'guest',
- token
+ role: 'visitor',
+ token,
+ auth_method: 'guest'
});
}
@@ -98,16 +135,91 @@ router.post('/login', (req, res) => {
});
}
- // 生成简单 session token
+ // 生成 session token
const token = crypto.randomBytes(32).toString('hex');
+ sessions.set(token, {
+ dev_id,
+ name: entry.name,
+ role: entry.role || 'developer',
+ created_at: new Date().toISOString()
+ });
res.json({
error: false,
dev_id,
name: entry.name,
status: entry.status,
- role: entry.role,
- token
+ role: entry.role || 'developer',
+ token,
+ auth_method: 'dev_id'
+ });
+});
+
+// GET /api/ps/auth/session — 验证 session token
+router.get('/session', (req, res) => {
+ const token = req.headers['x-session-token'] || req.query.token;
+
+ if (!token) {
+ return res.status(401).json({
+ error: true,
+ code: 'NO_TOKEN',
+ message: '未提供 session token'
+ });
+ }
+
+ const session = sessions.get(token);
+ if (!session) {
+ return res.status(401).json({
+ error: true,
+ code: 'INVALID_TOKEN',
+ message: 'Session 已过期或无效'
+ });
+ }
+
+ res.json({
+ error: false,
+ dev_id: session.dev_id,
+ name: session.name,
+ role: session.role
+ });
+});
+
+// POST /api/ps/auth/wechat/callback — 微信登录回调(预留接口)
+router.post('/wechat/callback', (req, res) => {
+ // 微信登录需要冰朔提供 AppID/AppSecret 后才能启用
+ // 当前返回提示信息
+ res.status(501).json({
+ error: true,
+ code: 'WECHAT_NOT_CONFIGURED',
+ message: '微信登录尚未配置。需要冰朔提供微信开放平台 AppID/AppSecret。当前请使用开发者编号登录。'
+ });
+});
+
+// POST /api/ps/auth/bind — 绑定微信 openid 和开发者编号(预留接口)
+router.post('/bind', (req, res) => {
+ res.status(501).json({
+ error: true,
+ code: 'BIND_NOT_CONFIGURED',
+ message: '绑定功能尚未启用。需要先完成微信登录配置。'
+ });
+});
+
+// GET /api/ps/auth/methods — 获取可用登录方式
+router.get('/methods', (_req, res) => {
+ const usersDB = loadUsersDB();
+ const methods = usersDB.auth_methods || {
+ dev_id: { enabled: true },
+ guest: { enabled: true },
+ wechat: { enabled: false }
+ };
+
+ res.json({
+ error: false,
+ methods: {
+ dev_id: { enabled: methods.dev_id ? methods.dev_id.enabled : true },
+ guest: { enabled: methods.guest ? methods.guest.enabled : true },
+ wechat: { enabled: methods.wechat ? methods.wechat.enabled : false }
+ }
});
});
diff --git a/persona-studio/backend/routes/proxy.js b/persona-studio/backend/routes/proxy.js
new file mode 100644
index 00000000..258156d8
--- /dev/null
+++ b/persona-studio/backend/routes/proxy.js
@@ -0,0 +1,288 @@
+/**
+ * persona-studio · 服务器端 AI 模型代理路由
+ *
+ * 目的:消除用户手动输入 API 密钥的需求
+ * 所有模型调用由服务器端代理完成,API 密钥仅存在于服务器 .env
+ *
+ * 路由前缀:/api/ps/proxy
+ *
+ * 📜 Copyright: 国作登字-2026-A-00037559
+ * 🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001
+ */
+const express = require('express');
+const router = express.Router();
+const https = require('https');
+const http = require('http');
+
+// 服务器端配置的模型 API
+const MODEL_API_KEY = process.env.MODEL_API_KEY || process.env.YUNWU_API_KEY || '';
+const MODEL_API_BASE = process.env.MODEL_API_BASE || 'https://api.yunwu.ai/v1';
+const DEFAULT_MODEL = process.env.DEFAULT_MODEL || 'gemini-2.0-flash';
+
+if (!MODEL_API_KEY) {
+ console.warn('[Proxy] ⚠️ MODEL_API_KEY / YUNWU_API_KEY 未配置,服务器代理模式不可用。用户需使用自有 API 密钥。');
+}
+
+/**
+ * 检查服务器端代理是否可用
+ * GET /api/ps/proxy/status
+ */
+router.get('/status', (_req, res) => {
+ const available = !!MODEL_API_KEY;
+ res.json({
+ error: false,
+ proxy_available: available,
+ model: available ? DEFAULT_MODEL : null,
+ api_base: available ? MODEL_API_BASE : null,
+ message: available
+ ? '服务器端代理可用 · 无需 API 密钥'
+ : '服务器端代理未配置 · 请使用自有 API 密钥'
+ });
+});
+
+/**
+ * 获取服务器端可用模型列表
+ * GET /api/ps/proxy/models
+ */
+router.get('/models', async (_req, res) => {
+ if (!MODEL_API_KEY) {
+ return res.status(503).json({
+ error: true,
+ code: 'PROXY_NOT_CONFIGURED',
+ message: '服务器端 API 密钥未配置'
+ });
+ }
+
+ try {
+ const data = await callModelAPI('GET', '/models', null);
+ const models = (data.data || []).map(m => m.id).filter(Boolean).sort();
+ res.json({ error: false, models, api_base: MODEL_API_BASE });
+ } catch (err) {
+ res.status(502).json({
+ error: true,
+ code: 'MODEL_API_ERROR',
+ message: '无法获取模型列表: ' + (err.message || 'Unknown error')
+ });
+ }
+});
+
+/**
+ * 服务器端代理聊天
+ * POST /api/ps/proxy/chat
+ * Body: { messages: [...], model?: string, max_tokens?: number }
+ */
+router.post('/chat', async (req, res) => {
+ if (!MODEL_API_KEY) {
+ return res.status(503).json({
+ error: true,
+ code: 'PROXY_NOT_CONFIGURED',
+ message: '服务器端 API 密钥未配置,请使用自有 API 密钥'
+ });
+ }
+
+ const { messages, model, max_tokens } = req.body || {};
+
+ if (!messages || !Array.isArray(messages) || messages.length === 0) {
+ return res.status(400).json({
+ error: true,
+ code: 'INVALID_MESSAGES',
+ message: '消息列表不能为空'
+ });
+ }
+
+ // Validate message format
+ for (const msg of messages) {
+ if (!msg.role || !msg.content) {
+ return res.status(400).json({
+ error: true,
+ code: 'INVALID_MESSAGE_FORMAT',
+ message: '每条消息必须包含 role 和 content 字段'
+ });
+ }
+ if (!['system', 'user', 'assistant'].includes(msg.role)) {
+ return res.status(400).json({
+ error: true,
+ code: 'INVALID_ROLE',
+ message: '消息 role 必须是 system/user/assistant'
+ });
+ }
+ }
+
+ try {
+ const data = await callModelAPI('POST', '/chat/completions', {
+ model: model || DEFAULT_MODEL,
+ messages,
+ max_tokens: Math.min(max_tokens || 4000, 8000),
+ temperature: 0.3
+ });
+
+ if (data.choices && data.choices[0] && data.choices[0].message) {
+ res.json({
+ error: false,
+ reply: data.choices[0].message.content,
+ model: data.model || model || DEFAULT_MODEL,
+ usage: data.usage || null
+ });
+ } else if (data.error) {
+ res.status(502).json({
+ error: true,
+ code: 'MODEL_ERROR',
+ message: data.error.message || '模型返回错误'
+ });
+ } else {
+ res.status(502).json({
+ error: true,
+ code: 'UNEXPECTED_RESPONSE',
+ message: '模型返回了意外格式的响应'
+ });
+ }
+ } catch (err) {
+ res.status(502).json({
+ error: true,
+ code: 'PROXY_ERROR',
+ message: '服务器代理请求失败: ' + (err.message || 'Unknown error')
+ });
+ }
+});
+
+/**
+ * 服务器端代理流式聊天(SSE)
+ * POST /api/ps/proxy/chat/stream
+ * Body: { messages: [...], model?: string, max_tokens?: number }
+ */
+router.post('/chat/stream', async (req, res) => {
+ if (!MODEL_API_KEY) {
+ return res.status(503).json({
+ error: true,
+ code: 'PROXY_NOT_CONFIGURED',
+ message: '服务器端 API 密钥未配置'
+ });
+ }
+
+ const { messages, model, max_tokens } = req.body || {};
+
+ if (!messages || !Array.isArray(messages) || messages.length === 0) {
+ return res.status(400).json({
+ error: true,
+ code: 'INVALID_MESSAGES',
+ message: '消息列表不能为空'
+ });
+ }
+
+ // Set SSE headers
+ res.setHeader('Content-Type', 'text/event-stream');
+ res.setHeader('Cache-Control', 'no-cache');
+ res.setHeader('Connection', 'keep-alive');
+ res.setHeader('X-Accel-Buffering', 'no');
+
+ try {
+ const base = MODEL_API_BASE.replace(/\/+$/, '');
+ const chatUrl = new URL(base + '/chat/completions');
+ const isHttps = chatUrl.protocol === 'https:';
+ const mod = isHttps ? https : http;
+
+ const body = JSON.stringify({
+ model: model || DEFAULT_MODEL,
+ messages,
+ max_tokens: Math.min(max_tokens || 4000, 8000),
+ temperature: 0.3,
+ stream: true
+ });
+
+ const options = {
+ hostname: chatUrl.hostname,
+ port: chatUrl.port || (isHttps ? 443 : 80),
+ path: chatUrl.pathname,
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': 'Bearer ' + MODEL_API_KEY,
+ 'Content-Length': Buffer.byteLength(body)
+ },
+ timeout: 120000
+ };
+
+ const proxyReq = mod.request(options, (proxyRes) => {
+ proxyRes.on('data', (chunk) => {
+ res.write(chunk);
+ });
+ proxyRes.on('end', () => {
+ res.end();
+ });
+ });
+
+ proxyReq.on('error', (err) => {
+ res.write('data: ' + JSON.stringify({ error: true, message: err.message }) + '\n\n');
+ res.end();
+ });
+
+ proxyReq.on('timeout', () => {
+ proxyReq.destroy();
+ res.write('data: ' + JSON.stringify({ error: true, message: 'Request timeout' }) + '\n\n');
+ res.end();
+ });
+
+ req.on('close', () => {
+ proxyReq.destroy();
+ });
+
+ proxyReq.write(body);
+ proxyReq.end();
+ } catch (err) {
+ res.write('data: ' + JSON.stringify({ error: true, message: err.message }) + '\n\n');
+ res.end();
+ }
+});
+
+/**
+ * Helper: Call model API (non-streaming)
+ */
+function callModelAPI(method, endpoint, body) {
+ return new Promise((resolve, reject) => {
+ const base = MODEL_API_BASE.replace(/\/+$/, '');
+ const url = new URL(base + endpoint);
+ const isHttps = url.protocol === 'https:';
+ const mod = isHttps ? https : http;
+
+ const bodyStr = body ? JSON.stringify(body) : null;
+
+ const options = {
+ hostname: url.hostname,
+ port: url.port || (isHttps ? 443 : 80),
+ path: url.pathname,
+ method,
+ headers: {
+ 'Authorization': 'Bearer ' + MODEL_API_KEY,
+ 'Content-Type': 'application/json'
+ },
+ timeout: 60000
+ };
+
+ if (bodyStr) {
+ options.headers['Content-Length'] = Buffer.byteLength(bodyStr);
+ }
+
+ const req = mod.request(options, (res) => {
+ let data = '';
+ res.on('data', (chunk) => { data += chunk; });
+ res.on('end', () => {
+ try {
+ resolve(JSON.parse(data));
+ } catch (_e) {
+ reject(new Error('Failed to parse API response'));
+ }
+ });
+ });
+
+ req.on('error', reject);
+ req.on('timeout', () => {
+ req.destroy();
+ reject(new Error('Request timeout'));
+ });
+
+ if (bodyStr) req.write(bodyStr);
+ req.end();
+ });
+}
+
+module.exports = router;
diff --git a/persona-studio/backend/server.js b/persona-studio/backend/server.js
index ae561b8f..0414ffdb 100644
--- a/persona-studio/backend/server.js
+++ b/persona-studio/backend/server.js
@@ -10,6 +10,7 @@ const buildRoutes = require('./routes/build');
const notifyRoutes = require('./routes/notify');
const apikeyRoutes = require('./routes/apikey');
const previewRoutes = require('./routes/preview');
+const proxyRoutes = require('./routes/proxy');
const app = express();
app.use(cors({
@@ -34,6 +35,7 @@ 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.use('/api/ps/proxy', proxyRoutes);
// ── 健康检查 ──
// ── 健康检查 ──app.use('/api/ps/preview', previewRoutes);
@@ -73,6 +75,10 @@ app.get('/', (_req, res) => {
'/api/ps/notify/send',
'/api/ps/apikey/detect-models',
'/api/ps/apikey/chat',
+ '/api/ps/proxy/status',
+ '/api/ps/proxy/models',
+ '/api/ps/proxy/chat',
+ '/api/ps/proxy/chat/stream',
'/api/ps/preview/:devId/:project',
'/api/ps/health'
]
diff --git a/persona-studio/frontend/chat.js b/persona-studio/frontend/chat.js
index b4a38bf7..541884ea 100644
--- a/persona-studio/frontend/chat.js
+++ b/persona-studio/frontend/chat.js
@@ -10,6 +10,7 @@ const LOGIN_MODE = sessionStorage.getItem('login_mode'); // 'developer' or 'gues
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 USE_SERVER_PROXY = sessionStorage.getItem('use_server_proxy') === 'true';
const API_BASE = (function () {
if (location.hostname === 'localhost' || location.hostname === '127.0.0.1') {
@@ -389,8 +390,13 @@ async function sendMessage() {
sendBtn.disabled = true;
try {
- // All modes now use API Key for real AI — ZhuYuan is awake
- await streamApiKeyReply(fullText);
+ if (USE_SERVER_PROXY) {
+ // 服务器代理模式:无需 API 密钥
+ await streamServerProxyReply(fullText);
+ } else {
+ // All modes now use API Key for real AI — ZhuYuan is awake
+ await streamApiKeyReply(fullText);
+ }
} catch (_err) {
appendMessage('system', '消息发送失败,请检查网络连接后再试');
}
@@ -404,6 +410,118 @@ async function sendMessage() {
input.focus();
}
+/* ---- 服务器代理模式对话(SSE 流式) ---- */
+async function streamServerProxyReply(text) {
+ var apiMessages = [ZHUYUAN_SYSTEM_PROMPT];
+ var contextPrompt = buildContextPrompt();
+ if (contextPrompt) {
+ apiMessages.push(contextPrompt);
+ }
+
+ var recentHistory = conversationHistory.slice(-20).map(function (msg) {
+ return { role: msg.role === 'assistant' ? 'assistant' : 'user', content: msg.content };
+ });
+ apiMessages = apiMessages.concat(recentHistory);
+
+ var streamEl = appendStreamMessage();
+
+ try {
+ var res = await fetch(API_BASE + '/api/ps/proxy/chat/stream', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ messages: apiMessages,
+ model: SELECTED_MODEL || undefined,
+ max_tokens: 2000
+ })
+ });
+
+ 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;
+ }
+
+ // SSE 流式读取
+ var full = '';
+ var reader = res.body.getReader();
+ var decoder = new TextDecoder();
+ var buf = '';
+
+ while (true) {
+ var chunk = await reader.read();
+ if (chunk.done) break;
+ buf += decoder.decode(chunk.value, { stream: true });
+ var lines = buf.split('\n');
+ buf = lines.pop();
+ for (var i = 0; i < lines.length; i++) {
+ var line = lines[i];
+ if (!line.startsWith('data: ')) continue;
+ var d = line.slice(6);
+ if (d === '[DONE]') continue;
+ try {
+ var parsed = JSON.parse(d);
+ var delta = parsed.choices && parsed.choices[0] && parsed.choices[0].delta;
+ var content = delta && delta.content;
+ if (content) {
+ full += content;
+ streamEl.textContent = full + '▋';
+ var chatBody = document.getElementById('chatBody');
+ chatBody.scrollTop = chatBody.scrollHeight;
+ }
+ } catch (_parseErr) { /* ignore malformed SSE line */ }
+ }
+ }
+
+ streamEl.textContent = full || '(未收到有效回复)';
+ if (full) {
+ conversationHistory.push({ role: 'assistant', content: full });
+
+ var readyKeywords = ['方案已确认', '我要开发', '开始帮你做', '方案确认', '可以开始', '开始开发'];
+ if (readyKeywords.some(function (kw) { return full.includes(kw); })) {
+ buildReady = true;
+ document.getElementById('buildBtn').style.display = 'inline-flex';
+ }
+ }
+ } catch (err) {
+ // 流式失败,降级到非流式代理
+ try {
+ var proxyRes = await fetch(API_BASE + '/api/ps/proxy/chat', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ messages: apiMessages,
+ model: SELECTED_MODEL || undefined,
+ max_tokens: 2000
+ })
+ });
+
+ if (proxyRes.ok) {
+ var data = await proxyRes.json();
+ if (data.reply) {
+ streamEl.textContent = data.reply;
+ conversationHistory.push({ role: 'assistant', content: data.reply });
+ } else {
+ streamEl.textContent = '(未收到有效回复)';
+ }
+ } else {
+ var proxyErrText = '请求失败 (HTTP ' + proxyRes.status + ')';
+ try {
+ var proxyErrData = await proxyRes.json();
+ proxyErrText = proxyErrData.message || proxyErrText;
+ } catch (_e) { /* ignore */ }
+ streamEl.textContent = '⚠️ ' + proxyErrText;
+ }
+ } catch (proxyErr) {
+ streamEl.textContent = '⚠️ ' + (proxyErr.message || '请求失败,请检查网络连接');
+ }
+ }
+}
+
/* ---- API Key 对话(浏览器直连 SSE 流式) ---- */
async function streamApiKeyReply(text) {
// Build messages with ZhuYuan system prompt + context
diff --git a/persona-studio/frontend/index.html b/persona-studio/frontend/index.html
index d9299f13..f95df727 100644
--- a/persona-studio/frontend/index.html
+++ b/persona-studio/frontend/index.html
@@ -77,38 +77,59 @@
-
🔑 输入 API 密钥
-
输入你的第三方 AI 模型 API Key,系统将自动检测可用模型,选择后即可唤醒铸渊
+
🔑 选择对话方式
-
-
-
-