diff --git a/.github/brain/bingshuo-issues-index.json b/.github/brain/bingshuo-issues-index.json index cb79a6cc..25ece0c5 100644 --- a/.github/brain/bingshuo-issues-index.json +++ b/.github/brain/bingshuo-issues-index.json @@ -1,7 +1,7 @@ { "version": "1.0", "description": "冰朔主控问题索引库 — 记录已知问题、根因与排查路由", - "updated_at": "2026-03-10T12:37:06.767Z", + "updated_at": "2026-03-10T13:11:12.874Z", "issues": [ { "id": "BS-001", diff --git a/.github/brain/bingshuo-master-brain.md b/.github/brain/bingshuo-master-brain.md index e088296b..1c565e58 100644 --- a/.github/brain/bingshuo-master-brain.md +++ b/.github/brain/bingshuo-master-brain.md @@ -1,7 +1,7 @@ # 冰朔主控神经系统 · 核心主控大脑 v1.0 > 本文件为冰朔主控神经系统的总控脑文件。 -> 最后编译时间:2026-03-10T12:37:06.767Z +> 最后编译时间:2026-03-10T13:11:12.875Z --- @@ -85,7 +85,7 @@ > 本区块由 master-brain-compiler 自动编译。 -- **编译时间**:2026-03-10T12:37:06.767Z +- **编译时间**:2026-03-10T13:11:12.875Z - **脑文件规则版本**:v3.0 - **脑文件完整性**:✅ 完整 diff --git a/.github/brain/bingshuo-system-health.json b/.github/brain/bingshuo-system-health.json index 8d288a72..661160d4 100644 --- a/.github/brain/bingshuo-system-health.json +++ b/.github/brain/bingshuo-system-health.json @@ -1,7 +1,7 @@ { "version": "1.0", "description": "冰朔主控系统健康状态", - "updated_at": "2026-03-10T12:37:06.766Z", + "updated_at": "2026-03-10T13:11:12.874Z", "health": { "brain_consistency": { "status": "yellow", diff --git a/.github/brain/repo-map.json b/.github/brain/repo-map.json index a192f0c5..8ff7495d 100644 --- a/.github/brain/repo-map.json +++ b/.github/brain/repo-map.json @@ -1,7 +1,7 @@ { "description": "铸渊图书馆目录 · Library Catalog for 铸渊 (Zhùyuān)", "version": "2.0", - "generated_at": "2026-03-10T12:37:16.388Z", + "generated_at": "2026-03-10T13:11:27.110Z", "generated_by": "scripts/generate-repo-map.js", "repo": "qinfendebingshuo/guanghulab", "stats": { diff --git a/.github/brain/repo-snapshot.md b/.github/brain/repo-snapshot.md index 60c49ad2..1bd3bc2e 100644 --- a/.github/brain/repo-snapshot.md +++ b/.github/brain/repo-snapshot.md @@ -1,5 +1,5 @@ # 铸渊图书馆快照 · Repo Snapshot -> 生成于 2026-03-10 20:37 CST · 每次 push 自动更新 · 铸渊唤醒时优先读取此文件 +> 生成于 2026-03-10 21:11 CST · 每次 push 自动更新 · 铸渊唤醒时优先读取此文件 --- @@ -13,7 +13,7 @@ | 脚本 | 26 个执行脚本 | | 开发者节点 | 8 人 | | HLI 接口覆盖率 | 7/21 (33%) | -| 快照生成时间 | 2026-03-10 20:37 CST | +| 快照生成时间 | 2026-03-10 21:11 CST | --- diff --git a/README.md b/README.md index 083a0ed8..e0b25b98 100644 --- a/README.md +++ b/README.md @@ -78,29 +78,30 @@ | 时间 | 检查项 | 状态 | |------|--------|------| -| 03-09 17:29 | 🔧 系统更新: `.github/` | 铸渊 (Copilot) | -| 03-09 17:29 | 🔧 系统更新: `scripts/` | 铸渊 (Copilot) | -| 03-09 17:15 | 🔧 系统更新: `docs/` | 铸渊 (Copilot) | -| 03-09 17:15 | 🔧 系统更新: `persona-brain-db/` | 铸渊 (Copilot) | +| 03-10 20:38 | 🔧 系统更新: `.github/` | 铸渊 (ZhùYuān) | +| 03-10 20:37 | ❌ 📢 更新系统公告区 · 失败 | 冰朔 | +| 03-10 20:37 | 🔧 系统更新: `docs/` | 铸渊 (ZhùYuān) | +| 03-10 19:15 | 🔧 系统更新: `scripts/` | 铸渊 (Copilot) | +| 03-10 16:56 | ✅ 每日巡检 ✅ 通过 | 冰朔 | +| 03-10 08:00 | 🧠 核心大脑升级 v3.0 · 壳-核分离架构 · 前端壳(UI/IO) + 后端脑(prompt/routing/memory/context) · BRAIN域4接口上线 · 覆盖率 7/21 (33.3%) | 铸渊(冰朔指令) | +| 03-10 08:00 | ⚠️ 铸渊 PSP 巡检完成 · 发现 3 个问题 · 自动修复 0 项 | 铸渊PSP巡检 | +| 03-10 08:00 | 🔵 CI 构建 失败 | GitHub Actions | | 03-09 16:56 | ✅ 每日巡检 ✅ 通过 | 冰朔 | | 03-09 08:00 | ✅ 铸渊 PSP 巡检通过 · 全部检查项 ✅ | 铸渊PSP巡检 | -| 03-09 08:00 | 🔍 每日自检完成 · 知识库4条 · 缺失文件4个 | 铸渊自检 | -| 03-08 16:41 | ✅ 每日巡检 ✅ 通过 | 冰朔 | | 03-08 08:00 | ✅ 铸渊 PSP 巡检通过 · 全部检查项 ✅ | 铸渊PSP巡检 | -| 03-08 08:00 | 🔍 每日自检完成 · 知识库4条 · 缺失文件4个 | 铸渊自检 | -| 03-07 16:41 | ✅ 每日巡检 ✅ 通过 | 冰朔 | | 03-07 08:00 | ⚠️ 铸渊 PSP 巡检完成 · 发现 1 个问题 · 自动修复 0 项 | 铸渊PSP巡检 | -| 03-07 08:00 | 🔍 每日自检完成 · 知识库4条 · 缺失文件4个 | 铸渊自检 | -| 03-06 16:51 | ✅ 每日巡检 ✅ 通过 | 冰朔 | -| 03-06 08:00 | 🔍 每日自检完成 · 知识库4条 · 缺失文件4个 | 铸渊自检 | +| 03-06 00:07 | ✅ CI 构建 通过 | 冰朔 | +| 03-05 20:32 | 🚀 广播分发系统 + 唤醒协议全部激活 | 冰朔 | ### 🤖 铸渊自动提醒 -> 🟢 **今日无需冰朔手动干预** · 系统一切正常 +> 🔴 **需要冰朔手动干预!** > -> 🗓️ 2026-03-09 · 铸渊自动检测 +> - ❌ 📢 更新系统公告区 · 失败 +> +> 🗓️ 2026-03-10 · 铸渊已发送邮件提醒 --- @@ -114,24 +115,10 @@ | 时间 | 合作者 | 模块 | 状态 | |------|--------|------|------| -| 03-09 17:15 | 🛠️ 页页 | `backend-integration/` | 📦 上传成功 | -| 03-09 17:15 | 铸渊 (Copilot) | `backend/` | 📦 上传成功 | -| 03-09 17:15 | 铸渊 (Copilot) | `cloud-drive/` | 📦 上传成功 | -| 03-09 17:15 | 🤖 之之 | `dingtalk-bot/` | 📦 上传成功 | -| 03-09 17:15 | 铸渊 (Copilot) | `frontend/` | 📦 上传成功 | -| 03-09 17:15 | 🐱 肥猫 | `m01-login/` | 📦 上传成功 | -| 03-09 17:15 | 🐱 肥猫 | `m03-personality/` | 📦 上传成功 | -| 03-09 17:15 | 🌸 花尔 | `m05-user-center/` | 📦 上传成功 | -| 03-09 17:15 | 🍊 桔子 | `m06-ticket/` | 📦 上传成功 | -| 03-09 17:15 | 🎨 燕樊 | `m07-dialogue-ui/` | 📦 上传成功 | -| 03-09 17:15 | 🎨 燕樊 | `m10-cloud/` | 📦 上传成功 | -| 03-09 17:15 | 🍊 桔子 | `m11-module/` | 📦 上传成功 | -| 03-09 17:15 | 🍓 小草莓 | `m12-kanban/` | 📦 上传成功 | -| 03-09 17:15 | 🎨 燕樊 | `m15-cloud-drive/` | 📦 上传成功 | -| 03-09 17:15 | 铸渊 (Copilot) | `m18-health-check/` | 📦 上传成功 | -| 03-09 17:15 | 🌟 Awen | `notification/` | 📦 上传成功 | -| 03-09 17:15 | 🍓 小草莓 | `status-board/` | 📦 上传成功 | -| 03-09 17:15 | 🍊 桔子 | `ticket-system/` | 📦 上传成功 | +| 03-10 20:49 | zhizhi200271 | `—/` | ✅ 上传成功 | +| 03-10 20:40 | Copilot | `—/` | 🔵 已更新 | +| 03-10 20:37 | 冰朔 | `—/` | ✅ 上传成功 | +| 03-10 20:31 | 🛠️ 页页 | `backend-integration/` | 📦 上传成功 | ### 🤖 铸渊自动提醒 · 合作者 @@ -142,7 +129,7 @@ > **🎨 燕樊(DEV-003):** > - ⚠️ `m15-cloud-drive/` 缺少 README.md > -> 🗓️ 2026-03-09 · 铸渊已发送邮件提醒 +> 🗓️ 2026-03-10 · 铸渊已发送邮件提醒 --- diff --git a/persona-studio/backend/routes/apikey.js b/persona-studio/backend/routes/apikey.js index ebfcb434..56d6b8b3 100644 --- a/persona-studio/backend/routes/apikey.js +++ b/persona-studio/backend/routes/apikey.js @@ -81,12 +81,16 @@ function fetchModels(apiBase, apiKey, timeoutMs) { }); req.on('error', (err) => { - reject(new Error('API Base 不可访问: ' + err.message)); + const wrapped = new Error('API Base 不可访问: ' + err.message); + wrapped.code = err.code; + reject(wrapped); }); req.on('timeout', () => { req.destroy(); - reject(new Error('API Base 不可访问(请求超时)')); + const err = new Error('API Base 不可访问(请求超时)'); + err.code = 'ETIMEDOUT'; + reject(err); }); req.end(); @@ -169,6 +173,19 @@ router.post('/detect-models', async (req, res) => { }); } + // 校验 URL 格式 + try { + const testBase = api_base.replace(/\/+$/, ''); + const testPath = testBase.endsWith('/v1') ? testBase + '/models' : testBase + '/v1/models'; + new URL(testPath); + } catch (_e) { + return res.status(400).json({ + error: true, + code: 'INVALID_API_BASE', + message: 'API Base URL 格式无效' + }); + } + if (!api_key || typeof api_key !== 'string') { return res.status(400).json({ error: true, @@ -216,10 +233,23 @@ router.post('/detect-models', async (req, res) => { cached: false }); } catch (err) { + const errMsg = err.message || '模型检测失败'; + const errCode = err.code || ''; + let code = 'DETECT_FAILED'; + + // 区分 DNS / 网络 / 超时错误(优先使用 Node.js 错误码) + if (errCode === 'ENOTFOUND' || errCode === 'EAI_AGAIN' || /ENOTFOUND|getaddrinfo/.test(errMsg)) { + code = 'DNS_ERROR'; + } else if (errCode === 'ECONNREFUSED' || errCode === 'ECONNRESET' || errCode === 'EHOSTUNREACH' || errCode === 'ENETUNREACH' || /ECONNREFUSED|ECONNRESET|EHOSTUNREACH|ENETUNREACH|socket hang up/.test(errMsg)) { + code = 'NETWORK_ERROR'; + } else if (errCode === 'ETIMEDOUT' || errCode === 'ESOCKETTIMEDOUT' || /timeout|ETIMEDOUT/.test(errMsg)) { + code = 'TIMEOUT'; + } + res.status(502).json({ error: true, - code: 'DETECT_FAILED', - message: err.message || '模型检测失败' + code: code, + message: errMsg }); } }); diff --git a/persona-studio/backend/server.js b/persona-studio/backend/server.js index 03186e7f..a4899137 100644 --- a/persona-studio/backend/server.js +++ b/persona-studio/backend/server.js @@ -33,6 +33,16 @@ app.get('/api/ps/health', (_req, res) => { }); }); +// 兼容 /api/health 路径 +app.get('/api/health', (_req, res) => { + res.json({ + status: 'ok', + service: 'persona-studio', + version: '1.0.0', + timestamp: new Date().toISOString() + }); +}); + // ── 根路由 ── app.get('/', (_req, res) => { res.json({ diff --git a/persona-studio/frontend/chat.js b/persona-studio/frontend/chat.js index c1c55e03..a365219e 100644 --- a/persona-studio/frontend/chat.js +++ b/persona-studio/frontend/chat.js @@ -105,26 +105,9 @@ async function sendMessage() { sendBtn.disabled = true; try { - let data; - if (LOGIN_MODE === 'apikey') { - // API Key 模式:通过后端代理调用用户的 API - const apiMessages = conversationHistory.slice(-20).map(function (msg) { - return { role: msg.role === 'assistant' ? 'assistant' : 'user', content: msg.content }; - }); - - const res = await fetch(API_BASE + '/api/ps/apikey/chat', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - api_base: USER_API_BASE, - api_key: USER_API_KEY, - model: SELECTED_MODEL, - messages: apiMessages - }) - }); - - data = await res.json(); + // API Key 模式:浏览器直连用户 API(无需后端代理) + await streamApiKeyReply(text); } else { // 开发编号模式:使用原有后端接口 const res = await fetch(API_BASE + '/api/ps/chat/message', { @@ -137,17 +120,17 @@ async function sendMessage() { }) }); - data = await res.json(); - } + var data = await res.json(); - if (data.reply) { - appendMessage('persona', data.reply); - conversationHistory.push({ role: 'assistant', content: data.reply }); - } + 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'; + if (data.build_ready) { + buildReady = true; + document.getElementById('buildBtn').style.display = 'inline-flex'; + } } } catch (_err) { appendMessage('system', '消息发送失败,请稍后再试'); @@ -157,6 +140,105 @@ async function sendMessage() { input.focus(); } +/* ---- API Key 流式响应(浏览器直连) ---- */ +async function streamApiKeyReply(text) { + var apiMessages = conversationHistory.slice(-20).map(function (msg) { + return { role: msg.role === 'assistant' ? 'assistant' : 'user', content: msg.content }; + }); + + var chatUrl = USER_API_BASE + '/chat/completions'; + var reqBody = { + model: SELECTED_MODEL, + messages: apiMessages, + stream: true, + max_tokens: 2000, + temperature: 0.8 + }; + + var res = await fetch(chatUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + USER_API_KEY + }, + body: JSON.stringify(reqBody) + }); + + if (!res.ok) { + var errText = '请求失败 (HTTP ' + res.status + ')'; + try { + var errData = await res.json(); + errText = errData.error?.message || errText; + } catch (_e) { /* ignore parse error */ } + appendMessage('system', '⚠️ ' + errText); + return; + } + + // 流式读取响应 + var streamEl = appendStreamMessage(); + 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 + && parsed.choices[0].delta.content; + if (delta) { + full += delta; + streamEl.textContent = full; + } + } catch (_e) { /* ignore parse error */ } + } + } + + // 如果没有流式内容,尝试非流式解析 + if (!full && buf) { + try { + var jsonRes = JSON.parse(buf); + if (jsonRes.choices && jsonRes.choices[0] && jsonRes.choices[0].message) { + full = jsonRes.choices[0].message.content; + streamEl.textContent = full; + } + } catch (_e) { /* ignore */ } + } + + if (full) { + conversationHistory.push({ role: 'assistant', content: full }); + } else { + streamEl.textContent = '(未收到有效回复)'; + } +} + +/* ---- 创建流式消息气泡 ---- */ +function appendStreamMessage() { + var chatBody = document.getElementById('chatBody'); + var msgDiv = document.createElement('div'); + msgDiv.className = 'message message-persona'; + var contentEl = document.createElement('div'); + contentEl.className = 'msg-content'; + contentEl.textContent = '▋'; + msgDiv.innerHTML = '🧠'; + msgDiv.appendChild(contentEl); + chatBody.appendChild(msgDiv); + chatBody.scrollTop = chatBody.scrollHeight; + return contentEl; +} + /* ---- Key Handler ---- */ function handleKeyDown(e) { if (e.key === 'Enter' && !e.shiftKey) { diff --git a/persona-studio/frontend/index.html b/persona-studio/frontend/index.html index 0fe20170..eb8c080d 100644 --- a/persona-studio/frontend/index.html +++ b/persona-studio/frontend/index.html @@ -43,7 +43,7 @@ type="text" id="apiBaseInput" class="apikey-input" - placeholder="API Base URL(如 https://api.openai.com)" + placeholder="API Base URL(可留空自动探测,如 https://api.openai.com/v1)" autocomplete="off" /> const DEV_ID_RE = /^EXP-\d{3,}$/; + const PROBE_TIMEOUT_MS = 8000; const API_BASE = getApiBase(); + /* ---- 已知 API 端点列表(浏览器直连探测) ---- */ + const KNOWN_ENDPOINTS = [ + { label: '云雾 AI', base: 'https://api.yunwu.ai/v1' }, + { label: 'OpenAI', base: 'https://api.openai.com/v1' }, + { label: 'Google Gemini', base: 'https://generativelanguage.googleapis.com/v1beta/openai' }, + { label: 'DeepSeek', base: 'https://api.deepseek.com/v1' }, + { label: 'Moonshot', base: 'https://api.moonshot.cn/v1' }, + { label: '智谱 AI', base: 'https://open.bigmodel.cn/api/paas/v4' }, + ]; + function getApiBase() { if (location.hostname === 'localhost' || location.hostname === '127.0.0.1') { return 'http://localhost:3721'; @@ -84,6 +95,18 @@ return 'https://guanghulab.com'; } + /** + * 规范化 API Base URL + * 确保末尾无斜杠,并包含 /v1 路径 + */ + function normalizeApiBase(base) { + var normalized = base.replace(/\/+$/, ''); + if (!normalized.match(/\/v\d+(\/|$)/) && !normalized.endsWith('/openai')) { + normalized += '/v1'; + } + return normalized; + } + /* ---- 开发编号登录 ---- */ async function handleLogin(e) { e.preventDefault(); @@ -136,24 +159,18 @@ return false; } - /* ---- API Key 模型检测 ---- */ + /* ---- API Key 模型检测(浏览器直连,无需后端代理) ---- */ async function handleDetectModels() { - const apiBase = document.getElementById('apiBaseInput').value.trim(); - const apiKey = document.getElementById('apiKeyInput').value.trim(); - const errorEl = document.getElementById('errorMsg'); - const statusEl = document.getElementById('detectStatus'); - const modelContainer = document.getElementById('modelListContainer'); - const btn = document.getElementById('detectBtn'); + var apiBase = document.getElementById('apiBaseInput').value.trim(); + var apiKey = document.getElementById('apiKeyInput').value.trim(); + var errorEl = document.getElementById('errorMsg'); + var statusEl = document.getElementById('detectStatus'); + var modelContainer = document.getElementById('modelListContainer'); + var btn = document.getElementById('detectBtn'); errorEl.style.display = 'none'; modelContainer.style.display = 'none'; - if (!apiBase) { - errorEl.textContent = '请输入 API Base URL'; - errorEl.style.display = 'block'; - return; - } - if (!apiKey) { errorEl.textContent = '请输入 API Key'; errorEl.style.display = 'block'; @@ -161,59 +178,67 @@ } btn.disabled = true; - btn.textContent = '正在检测可用模型…'; - statusEl.textContent = '正在检测可用模型...'; + btn.textContent = '⏳ 正在检测可用模型…'; statusEl.className = 'detect-status detect-loading'; statusEl.style.display = 'block'; - try { - const res = await fetch(API_BASE + '/api/ps/apikey/detect-models', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ api_base: apiBase, api_key: apiKey }) - }); - - const data = await res.json(); - - if (!res.ok || data.error) { - statusEl.textContent = data.message || '未检测到模型,请检查 API Key'; - statusEl.className = 'detect-status detect-error'; - btn.disabled = false; - btn.textContent = '🔍 检测可用模型'; - return; - } - - const models = data.models || []; - if (models.length === 0) { - statusEl.textContent = '未检测到可用模型'; - statusEl.className = 'detect-status detect-error'; - btn.disabled = false; - btn.textContent = '🔍 检测可用模型'; - return; - } - - statusEl.textContent = '检测到 ' + models.length + ' 个模型,请选择一个模型进入对话'; - statusEl.className = 'detect-status detect-success'; - - renderModelList(models, apiBase, apiKey); - modelContainer.style.display = 'block'; - - btn.disabled = false; - btn.textContent = '🔍 重新检测'; - } catch (fetchErr) { - var errMsg = '检测失败'; - if (fetchErr instanceof TypeError) { - errMsg = '服务器代理未部署或不可达,请联系管理员'; - } else if (fetchErr.name === 'AbortError') { - errMsg = '请求超时,请检查网络连接'; - } else { - errMsg = '网络错误: ' + (fetchErr.message || '请检查网络连接'); - } - statusEl.textContent = errMsg; - statusEl.className = 'detect-status detect-error'; - btn.disabled = false; - btn.textContent = '🔍 检测可用模型'; + /* 构建探测列表:用户端点优先,然后已知端点 */ + var candidates = []; + if (apiBase) { + var normalizedUserBase = normalizeApiBase(apiBase); + candidates.push({ label: apiBase, base: normalizedUserBase }); } + for (var i = 0; i < KNOWN_ENDPOINTS.length; i++) { + var ep = KNOWN_ENDPOINTS[i]; + if (!apiBase || ep.base !== normalizedUserBase) { + candidates.push(ep); + } + } + + var matched = false; + for (var ci = 0; ci < candidates.length; ci++) { + var candidate = candidates[ci]; + statusEl.textContent = '🔍 探测 ' + candidate.label + '…'; + + try { + var ctrl = new AbortController(); + var tid = setTimeout(function () { ctrl.abort(); }, PROBE_TIMEOUT_MS); + + var res = await fetch(candidate.base + '/models', { + headers: { 'Authorization': 'Bearer ' + apiKey }, + signal: ctrl.signal + }); + clearTimeout(tid); + + if (res.ok) { + var data = await res.json(); + var models = (data.data || []).map(function (m) { return m.id; }).filter(Boolean).sort(); + + if (models.length === 0) continue; + + /* 回填端点地址 */ + document.getElementById('apiBaseInput').value = candidate.base; + + statusEl.textContent = '✅ 检测成功(' + candidate.label + ')· 发现 ' + models.length + ' 个可用模型'; + statusEl.className = 'detect-status detect-success'; + + renderModelList(models, candidate.base, apiKey); + modelContainer.style.display = 'block'; + matched = true; + break; + } + } catch (probeErr) { + /* 单个端点探测失败,继续下一个 */ + } + } + + if (!matched) { + statusEl.textContent = '❌ 未能检测到可用模型,请检查端点地址和密钥是否正确'; + statusEl.className = 'detect-status detect-error'; + } + + btn.disabled = false; + btn.textContent = '🔍 检测可用模型'; } /* ---- 渲染模型列表 ---- */