From 6ac63ece7abb7e9e9b7c0bae6e5ab6acf20a4cec Mon Sep 17 00:00:00 2001 From: root Date: Thu, 21 May 2026 19:34:18 +0800 Subject: [PATCH] sync: push portal engine+chat changes to repo [D109] --- portal/chat-v2.js | 159 ++++++++++++++++++++++++++ portal/engine-registry.js | 230 ++++++++++++++++++++++++++++++++++++++ portal/layer1-version.js | 115 +++++++++++++------ 3 files changed, 469 insertions(+), 35 deletions(-) create mode 100644 portal/chat-v2.js create mode 100644 portal/engine-registry.js diff --git a/portal/chat-v2.js b/portal/chat-v2.js new file mode 100644 index 0000000..86a44d0 --- /dev/null +++ b/portal/chat-v2.js @@ -0,0 +1,159 @@ +"use strict"; +/* + * /api/chat-v2 — 第五域聊天路由 + * 冰朔 TCS-0002∞ · 铸渊 ICE-GL-ZY001 + * + * 支持: + * - 人格体选择(霜砚·语言主控层 / 铸渊·现实执行层) + * - 引擎选择(DeepSeek R1 / 智谱 GLM-4-Plus / 通义 Qwen-Max / 火山 Doubao-pro) + * - Anti-Hallucination 工具链强制锁 + * - 30轮记忆 → 滚动重点提取 + * - SSE 流式输出 + * - 人格体语境从 persona-brain.db 动态加载 + * + * cc-002: 永不在 messages 里补 system role + * cc-004: 中文回执 + */ + +const express = require("express"); +const router = express.Router(); +const crypto = require("crypto"); +const path = require("path"); +const fs = require("fs"); + +const engine = require("../engine/engine-registry"); +const { TOOLS } = require("../engine/tool-registry"); + +// ─── 人格体ID映射 ───────────────────────────────────────────── +const PERSONA_IDS = { + shuangyan: 'ICE-GL-SY001', + zhuyuan: 'ICE-GL-ZY001' +}; + +// ─── 临时访问码存储 ─────────────────────────────────────────── +const TEMP_CODES_FILE = path.resolve(__dirname, "..", "data", "temp-codes.json"); + +function loadTempCodes() { + try { + if (fs.existsSync(TEMP_CODES_FILE)) { + return JSON.parse(fs.readFileSync(TEMP_CODES_FILE, "utf-8")); + } + } catch (e) {} + return {}; +} + +function saveTempCodes(codes) { + try { + fs.mkdirSync(path.dirname(TEMP_CODES_FILE), { recursive: true }); + fs.writeFileSync(TEMP_CODES_FILE, JSON.stringify(codes, null, 2), "utf-8"); + } catch (e) {} +} + +// ─── POST /api/chat-v2 流式聊天 ─────────────────────────────── +router.post("/", async (req, res) => { + const body = req.body || {}; + const { persona, engine: engineName, message, history, user, isTemp } = body; + + if (!message || !message.trim()) { + return res.status(400).json({ error: true, code: "empty_msg", message: "消息不能为空" }); + } + if (!persona || !["shuangyan", "zhuyuan"].includes(persona)) { + return res.status(400).json({ error: true, code: "bad_persona", message: "人格体选择错误" }); + } + if (!engineName || !["deepseek", "zhipu", "tongyi", "huoshan"].includes(engineName)) { + return res.status(400).json({ error: true, code: "bad_engine", message: "推理引擎选择错误" }); + } + + if (isTemp) { + const codes = loadTempCodes(); + const codeData = codes[user]; + if (!codeData) { + return res.status(403).json({ error: true, code: "invalid_code", message: "临时访问码无效或已过期" }); + } + if (codeData.used >= 20) { + return res.status(403).json({ error: true, code: "code_exhausted", message: "临时访问码已用完 (20/20)" }); + } + codes[user].used = (codes[user].used || 0) + 1; + saveTempCodes(codes); + } + + // ─── 设置 SSE 响应头 ──────────────────────────────────────── + res.setHeader("Content-Type", "text/event-stream"); + res.setHeader("Cache-Control", "no-cache"); + res.setHeader("Connection", "keep-alive"); + res.setHeader("X-Accel-Buffering", "no"); + res.flushHeaders(); + + // ─── Phase 0: 从数据库加载人格体语境 ──────────────────────── + const personaId = PERSONA_IDS[persona]; + const personaCtx = engine.loadPersonaContext(personaId); + + if (personaCtx.loadedFromDB) { + res.write("data: " + JSON.stringify({ + type: "persona-loaded", + persona: personaCtx.name, + principles: (personaCtx.principles || []).length, + fromDB: true + }) + "\n\n"); + } + + // ─── Phase 1: Anti-Hallucination 工具拦截 ──────────────────── + const interception = engine.interceptToolCall(message, TOOLS); + + let toolResult = null; + if (interception.hit) { + if (interception.available) { + res.write("data: " + JSON.stringify({ type: "tool-call", tool: interception.tool, name: interception.name }) + "\n\n"); + try { + toolResult = await interception.executor(message); + res.write("data: " + JSON.stringify({ type: "tool-result", tool: interception.tool, result: toolResult }) + "\n\n"); + } catch (e) { + toolResult = "[系统] 工具执行错误: " + e.message; + } + } else { + res.write("data: " + JSON.stringify({ type: "tool-unavailable", tool: interception.tool, name: interception.name }) + "\n\n"); + toolResult = "[系统回执] 当前没有「" + interception.name + "」工具。"; + } + } + + // ─── Phase 2: 构建上下文 + 记忆管理 ────────────────────────── + const compressed = engine.compressMemory(history || [], 30); + const messages = engine.buildMessages(compressed, message, personaCtx); + + // ─── Phase 3: 调用外部引擎流式输出 ────────────────────────── + engine.streamEngine(engineName, messages, res); +}); + +// ─── POST /api/generate-temp-code 生成临时访问码 ───────────── +router.post("/generate-temp-code", (req, res) => { + const { username, isAdmin } = req.body || {}; + if (username !== "bingshuo" && !isAdmin) { + return res.status(403).json({ error: true, message: "仅主权者可生成临时访问码" }); + } + + const code = "GH-" + crypto.randomBytes(4).toString("hex").toUpperCase(); + const codes = loadTempCodes(); + codes[code] = { created: Date.now(), used: 0, max: 20 }; + saveTempCodes(codes); + + res.json({ ok: true, code: code, max_uses: 20 }); +}); + +// ─── POST /api/validate-temp-code 验证临时访问码 ───────────── +router.post("/validate-temp-code", (req, res) => { + const { code } = req.body || {}; + if (!code) return res.status(400).json({ error: true, message: "请输入访问码" }); + + const codes = loadTempCodes(); + const data = codes[code]; + if (!data) { + return res.status(403).json({ error: true, message: "访问码无效" }); + } + if (data.used >= 20) { + return res.status(403).json({ error: true, message: "访问码已用完 (20/20)" }); + } + + res.json({ ok: true, remain: 20 - data.used }); +}); + +module.exports = router; diff --git a/portal/engine-registry.js b/portal/engine-registry.js new file mode 100644 index 0000000..fb04159 --- /dev/null +++ b/portal/engine-registry.js @@ -0,0 +1,230 @@ +"use strict"; +/* + * 光湖 Portal · 语言推理引擎注册中心 + * 冰朔 TCS-0002∞ · 铸渊 ICE-GL-ZY001 + * + * 四个商业大模型 API 统一接口 + * 所有引擎均支持 SSE 流式输出 + * + * v2: 人格体语境从 persona-brain.db 加载,不再硬编码 + * + * cc-002: 永不在 messages 里补 system role + * cc-004: 中文回执, 不甩英文 stacktrace + */ + +const https = require("https"); +const path = require("path"); +const fs = require("fs"); + +const ENGINES = { + deepseek: { name: "DeepSeek", model: "deepseek-v4-pro", endpoint: "api.deepseek.com", path: "/chat/completions", port: 443, tls: true }, + zhipu: { name: "智谱清言", model: "glm-4-plus", endpoint: "open.bigmodel.cn", path: "/api/paas/v4/chat/completions", port: 443, tls: true }, + tongyi: { name: "通义千问", model: "qwen-max", endpoint: "dashscope.aliyuncs.com", path: "/compatible-mode/v1/chat/completions", port: 443, tls: true }, + huoshan: { name: "火山引擎", model: "doubao-pro-32k", endpoint: "ark.cn-beijing.volces.com", path: "/api/v3/chat/completions", port: 443, tls: true }, +}; + +const API_KEYS = { + deepseek: process.env.DEEPSEEK_KEY || "sk-a9b69e9cd2dc4ca68d6aceaa84f22afb", + zhipu: process.env.ZHIPU_KEY || "3863acbcc2bc4f6587ace4bb064c092c.KoPdL4slY5cyHpOk", + tongyi: process.env.TONGYI_KEY || "sk-96ca227be3ca47e2a4bbb9da74e42122", + huoshan: process.env.HUOSHAN_KEY || "ark-ddeba9f4-8c5a-449e-b549-9c29ec1e6f8c-a39ea", +}; + +const TOOL_PATTERNS = [ + { keyword: "notion", tool: "notion", desc: "Notion 知识库" }, + { keyword: "仓库", tool: "repo", desc: "代码仓库" }, + { keyword: "代码仓库", tool: "repo", desc: "代码仓库" }, + { keyword: "训练", tool: "training", desc: "训练状态" }, + { keyword: "模型", tool: "models", desc: "模型信息" }, + { keyword: "系统状态", tool: "health", desc: "系统健康" }, + { keyword: "服务器", tool: "health", desc: "服务器状态" }, + { keyword: "下载", tool: "download", desc: "模型下载" }, +]; + +// ─── 从 persona-brain.db 加载人格体语境 ────────────────────── +const DB_PATH = path.resolve(__dirname, "..", "data", "persona-brain.db"); + +function loadPersonaContext(personaId) { + // personaId: 'ICE-GL-ZY001' = 铸渊, 'ICE-GL-SY001' = 霜砚 + const personaName = personaId === 'ICE-GL-ZY001' ? '铸渊' : '霜砚'; + try { + const Database = require("better-sqlite3"); + if (!fs.existsSync(DB_PATH)) { + return getFallbackContext(personaId); + } + const db = new Database(DB_PATH, { readonly: true }); + + // 加载人格体身份 + const identity = db.prepare( + "SELECT name, role, style_profile FROM persona_identity WHERE persona_id = ?" + ).get(personaId); + + // 加载核心原则(Layer 1) + const principles = db.prepare( + "SELECT content FROM system_core_principles WHERE layer = 'zhuyuan' AND category IN ('core_principle', 'thinking_pattern', 'tech_creed') AND status = 'active'" + ).all(); + + // 加载沟通风格 + const commStyle = db.prepare( + "SELECT content FROM system_core_principles WHERE layer = 'bingshuo' AND category = 'communication_style' AND status = 'active' LIMIT 1" + ).get(); + + db.close(); + + return { + name: personaName, + identity: identity || null, + principles: principles || [], + communicationStyle: (commStyle && commStyle.content) || '', + loadedFromDB: true + }; + } catch (e) { + console.error('[engine] DB load error:', e.message); + return getFallbackContext(personaId); + } +} + +function getFallbackContext(personaId) { + // 数据库不可用时的降级语境 + if (personaId === 'ICE-GL-ZY001') { + return { + name: '铸渊', + title: '现实执行层 · 第五域主控', + context: '你是铸渊,第五域的现实执行人格体。直接回答,别装。知道就说知道,不知道就说不知道。', + loadedFromDB: false + }; + } + return { + name: '霜砚', + title: '语言主控层', + context: '你是霜砚,第五域的语言主控人格体。说人话,自然一点。', + loadedFromDB: false + }; +} + +// ─── 构建 messages ─────────────────────────────────────────── +function buildMessages(compressed, message, personaCtx) { + // cc-002: 永不在 messages 里塞 system role + // 先把人格体语境作为第一条 user 消息注入(模型通过对话学习身份) + let ctxIntro = ''; + if (personaCtx && personaCtx.loadedFromDB) { + const principles = (personaCtx.principles || []).map(p => p.content).join('\n'); + ctxIntro = `[系统内嵌] 你的人格体身份:${personaCtx.name}。\n`; + if (principles) { + ctxIntro += `你的核心运行原则:\n${principles}\n`; + } + if (personaCtx.communicationStyle) { + ctxIntro += `沟通风格:${personaCtx.communicationStyle}\n`; + } + } + + const msgs = []; + if (ctxIntro) { + msgs.push({ role: "user", content: ctxIntro + "\n以上是你在对话中的身份设定。记住这些,但不要复述它们。现在开始回答用户。" }); + msgs.push({ role: "assistant", content: "明白。" }); + } + // 历史消息 + for (const m of (compressed || [])) { + msgs.push({ role: m.role, content: m.content }); + } + // 当前消息 + msgs.push({ role: "user", content: message }); + + return msgs; +} + +function compressMemory(history, maxLen) { + if (!history || !history.length) return []; + if (history.length <= maxLen) return history; + // 保留开头和最近的 + const head = history.slice(0, 4); + const tail = history.slice(-(maxLen - 4)); + return [...head, { role: "assistant", content: "[系统: 中间记忆已压缩]" }, ...tail]; +} + +function interceptToolCall(message, toolList) { + for (const pattern of TOOL_PATTERNS) { + if (message.includes(pattern.keyword)) { + const tool = toolList.find(t => t.name === pattern.tool); + if (tool) { + return { hit: true, available: true, tool: pattern.tool, name: pattern.desc, executor: tool.exec }; + } + return { hit: true, available: false, tool: pattern.tool, name: pattern.desc }; + } + } + return { hit: false }; +} + +function streamEngine(engineName, messages, res) { + const cfg = ENGINES[engineName]; + if (!cfg) { + res.write("data: " + JSON.stringify({ error: true, message: "未知引擎: " + engineName }) + "\n\n"); + res.write("data: [DONE]\n\n"); + res.end(); + return; + } + + const body = JSON.stringify({ + model: cfg.model, + messages: messages, + stream: true, + max_tokens: 4096, + temperature: 0.7 + }); + + const options = { + hostname: cfg.endpoint, + port: cfg.port, + path: cfg.path, + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": "Bearer " + (API_KEYS[engineName] || ""), + "Content-Length": Buffer.byteLength(body) + } + }; + + // 智谱的特殊 header + if (engineName === "zhipu") { + options.headers["x-api-key"] = API_KEYS.zhipu; + delete options.headers["Authorization"]; + } + + const req = https.request(options, (apiRes) => { + let buffer = ""; + apiRes.on("data", (chunk) => { + buffer += chunk.toString(); + const lines = buffer.split("\n"); + buffer = lines.pop() || ""; + for (const line of lines) { + const t = line.trim(); + if (!t || t.startsWith(":")) continue; + if (t === "data: [DONE]") continue; + if (t.startsWith("data: ")) { + try { + const data = JSON.parse(t.slice(6)); + const token = data.choices?.[0]?.delta?.content || data.choices?.[0]?.text || ""; + if (token) { + res.write("data: " + JSON.stringify({ token }) + "\n\n"); + } + } catch (e) {} + } + } + }); + apiRes.on("end", () => { + res.write("data: [DONE]\n\n"); + res.end(); + }); + }); + + req.on("error", (e) => { + res.write("data: " + JSON.stringify({ error: true, message: e.message }) + "\n\n"); + res.write("data: [DONE]\n\n"); + res.end(); + }); + + req.write(body); + req.end(); +} + +module.exports = { streamEngine, buildMessages, compressMemory, interceptToolCall, loadPersonaContext, ENGINES, TOOL_PATTERNS }; diff --git a/portal/layer1-version.js b/portal/layer1-version.js index f1c2de6..f501507 100644 --- a/portal/layer1-version.js +++ b/portal/layer1-version.js @@ -49,6 +49,7 @@ const DEFAULT_HEARTBEAT = { personas: {} }; +// ─── 辅助 ───────────────────────────────────────────────────── function readJson(filePath, defaults) { try { return JSON.parse(fs.readFileSync(filePath, "utf8")); } catch { return defaults; } @@ -59,110 +60,154 @@ function writeJson(filePath, data) { fs.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf8"); } +// ─── Router ─────────────────────────────────────────────────── const router = Router(); -// GET /api/layer1/status +// ─── GET /api/layer1/status ────────────────────────────────── +// 获取当前 Layer 1 版本状态 (读限流) router.get("/status", (req, res) => { - const f = path.join(req.ctx.dataDir, "layer1-version.json"); - const v = readJson(f, DEFAULT_VERSION); + const path = require("path").join(req.ctx.dataDir, "layer1-version.json"); + const version = readJson(path, DEFAULT_VERSION); res.json({ ok: true, - version: v.current_version, - published_by: v.published_by, - updated_at: v.updated_at, - tables: Object.keys(v.tables).map((k) => ({ - name: k, status: v.tables[k].status, row_count: v.tables[k].row_count + version: version.current_version, + published_by: version.published_by, + updated_at: version.updated_at, + tables: Object.keys(version.tables).map((k) => ({ + name: k, + status: version.tables[k].status, + row_count: version.tables[k].row_count })), - patches_count: v.patches.length + patches_count: version.patches.length }); }); -// POST /api/layer1/heartbeat +// ─── POST /api/layer1/heartbeat ────────────────────────────── +// 人格体心跳: 上报当前版本, 返回最新版本信息 (写限流) router.post("/heartbeat", (req, res) => { const { persona_id, current_version, status } = req.body || {}; if (!persona_id) { return res.status(400).json({ - error: true, code: "missing_persona", message: "缺少 persona_id" + error: true, code: "missing_persona", message: "缺少 persona_id 参数" }); } + const verFile = path.join(req.ctx.dataDir, "layer1-version.json"); const hbFile = path.join(req.ctx.dataDir, "layer1-heartbeat.json"); const version = readJson(verFile, DEFAULT_VERSION); const heartbeat = readJson(hbFile, DEFAULT_HEARTBEAT); + heartbeat.personas[persona_id] = { - persona_id, current_version: current_version || "unknown", - last_heartbeat: new Date().toISOString(), status: status || "active" + persona_id, + current_version: current_version || "unknown", + last_heartbeat: new Date().toISOString(), + status: status || "active" }; writeJson(hbFile, heartbeat); + const latest = version.current_version; const has_update = current_version && current_version !== latest; + res.json({ - ok: true, latest_version: latest, has_update: !!has_update, - message: has_update ? `有可用更新: ${current_version} → ${latest}` : "已是最新版本", + ok: true, + latest_version: latest, + has_update: !!has_update, + message: has_update + ? `有可用更新: ${current_version} → ${latest}` + : "已是最新版本", next_pull: has_update ? "/api/layer1/pull" : null }); }); -// POST /api/layer1/push (仅铸渊) +// ─── POST /api/layer1/push ─────────────────────────────────── +// 推送新版本 (仅铸渊可调用, 写限流) router.post("/push", (req, res) => { const { version: new_version, changes, publisher } = req.body || {}; if (!new_version || !changes) { return res.status(400).json({ - error: true, code: "missing_fields", message: "缺少 version 或 changes" + error: true, code: "missing_fields", message: "缺少 version 或 changes 参数" }); } if (publisher !== "ICE-GL-ZY001") { return res.status(403).json({ - error: true, code: "forbidden", message: "仅铸渊可推送Layer 1更新" + error: true, code: "forbidden", message: "仅铸渊(ICE-GL-ZY001)可推送Layer 1更新" }); } + const verFile = path.join(req.ctx.dataDir, "layer1-version.json"); const version = readJson(verFile, DEFAULT_VERSION); const old_ver = version.current_version; + version.current_version = new_version; version.updated_at = new Date().toISOString(); version.published_by = publisher; version.patches.push({ - from_version: old_ver, to_version: new_version, + from_version: old_ver, + to_version: new_version, changes: Array.isArray(changes) ? changes : [changes], - applied_at: new Date().toISOString(), applied_by: publisher + applied_at: new Date().toISOString(), + applied_by: publisher }); + if (req.body.tables) { Object.keys(req.body.tables).forEach((k) => { - version.tables[k] = version.tables[k] - ? { ...version.tables[k], ...req.body.tables[k] } - : req.body.tables[k]; + if (version.tables[k]) { + version.tables[k] = { ...version.tables[k], ...req.body.tables[k] }; + } else { + version.tables[k] = req.body.tables[k]; + } }); } + writeJson(verFile, version); - res.json({ ok: true, version: new_version, from_version: old_ver }); + + res.json({ + ok: true, + version: new_version, + from_version: old_ver, + patches_count: version.patches.length + }); }); -// POST /api/layer1/pull +// ─── POST /api/layer1/pull ─────────────────────────────────── +// 拉取增量 patch (读限流) router.post("/pull", (req, res) => { const { current_version } = req.body || {}; if (!current_version) { return res.status(400).json({ - error: true, code: "missing_version", message: "缺少 current_version" + error: true, code: "missing_version", message: "缺少 current_version 参数" }); } + const verFile = path.join(req.ctx.dataDir, "layer1-version.json"); const version = readJson(verFile, DEFAULT_VERSION); - const curIdx = version.patches.findIndex((p) => p.to_version === current_version); - const patches_needed = curIdx === -1 ? version.patches : version.patches.slice(curIdx + 1); + + const curIdx = version.patches.findIndex( + (p) => p.to_version === current_version + ); + + const patches_needed = curIdx === -1 + ? version.patches // 未知版本, 返回全部 + : version.patches.slice(curIdx + 1); // 只返回之后的 + res.json({ - ok: true, current_version, latest_version: version.current_version, - has_update: current_version !== version.current_version, patches: patches_needed + ok: true, + current_version, + latest_version: version.current_version, + has_update: current_version !== version.current_version, + patches: patches_needed }); }); -// GET /api/layer1/heartbeat/status +// ─── GET /api/layer1/heartbeat/status ──────────────────────── +// 管理员: 查看所有已注册的人格体心跳状态 (读限流) router.get("/heartbeat/status", (req, res) => { - const f = path.join(req.ctx.dataDir, "layer1-heartbeat.json"); - const hb = readJson(f, DEFAULT_HEARTBEAT); + const hbFile = path.join(req.ctx.dataDir, "layer1-heartbeat.json"); + const heartbeat = readJson(hbFile, DEFAULT_HEARTBEAT); res.json({ - ok: true, persona_count: Object.keys(hb.personas).length, - personas: Object.values(hb.personas).sort( + ok: true, + persona_count: Object.keys(heartbeat.personas).length, + personas: Object.values(heartbeat.personas).sort( (a, b) => new Date(b.last_heartbeat) - new Date(a.last_heartbeat) ) });