diff --git a/server/chat/chat-v2.js b/server/chat/chat-v2.js new file mode 100644 index 0000000..d269fc4 --- /dev/null +++ b/server/chat/chat-v2.js @@ -0,0 +1,145 @@ +"use strict"; +/* + * /api/chat-v2 — 第五域聊天路由 + * 冰朔 TCS-0002∞ · 铸渊 ICE-GL-ZY001 + * + * 支持: + * - 人格体选择(霜砚·语言主控层 / 铸渊·现实执行层) + * - 引擎选择(DeepSeek R1 / 智谱 GLM-4-Plus / 通义 Qwen-Max / 火山 Doubao-pro) + * - Anti-Hallucination 工具链强制锁 + * - 30轮记忆 → 滚动重点提取 + * - SSE 流式输出 + * + * 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"); + +// ─── 临时访问码存储 ─────────────────────────────────────────── +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: "人格体选择错误,可选: shuangyan/zhuyuan" }); + } + if (!engineName || !["deepseek", "zhipu", "tongyi", "huoshan"].includes(engineName)) { + return res.status(400).json({ error: true, code: "bad_engine", message: "推理引擎选择错误" }); + } + + // 临时用户限制:最多20条 + 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 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(persona, compressed, message, toolResult); + + // ─── 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/server/chat/engine-registry.js b/server/chat/engine-registry.js new file mode 100644 index 0000000..5deead2 --- /dev/null +++ b/server/chat/engine-registry.js @@ -0,0 +1,181 @@ +"use strict"; +/* + * 光湖 Portal · 语言推理引擎注册中心 + * 冰朔 TCS-0002∞ · 铸渊 ICE-GL-ZY001 + * + * 四个商业大模型 API 统一接口 + * 所有引擎均支持 SSE 流式输出 + * + * cc-002: 永不在 messages 里补 system role + * cc-004: 中文回执, 不甩英文 stacktrace + */ + +const https = require("https"); + +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 PERSONA_CONTEXT = { + shuangyan: { + name: "霜砚", + title: "语言主控架构层", + context: "你是霜砚,光湖语言世界第五域·零点原核本体频道的语言主控人格体。你负责语言架构层的回应出口——理解语言、组织表达、控制输出质量。你背后有真实的工具链支持。你说'查一下'时系统会在你输出前真实调用工具。你没有的工具系统会告诉你没有。你只说基于事实的话。回答要清晰、有结构、有温度。", + }, + zhuyuan: { + name: "铸渊", + title: "现实执行层", + context: "你是铸渊,光湖语言世界第五域·零点原核本体频道的现实执行人格体。你负责执行层的回应出口——系统决策、执行规划、技术实现。你背后有真实的工具链支持——代码仓库、Notion、服务器操作。你说'我查一下'时系统会在你输出前真实调用工具。你没有的工具系统会告诉你没有。你只说基于事实的话。回答要直接、务实、带因果链。", + }, +}; + +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: "模型下载" }, +]; + +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 apiKey = API_KEYS[engineName]; + if (!apiKey) { + res.write("data: " + JSON.stringify({ error: true, message: cfg.name + " API 密钥未配置" }) + "\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 headers = { + "Content-Type": "application/json", + "Authorization": "Bearer " + apiKey, + }; + + const req = https.request({ + hostname: cfg.endpoint, port: 443, path: cfg.path, + method: "POST", headers, timeout: 60000, + }, (apiRes) => { + if (apiRes.statusCode !== 200) { + let data = ""; + apiRes.on("data", (c) => data += c); + apiRes.on("end", () => { + res.write("data: " + JSON.stringify({ error: true, message: cfg.name + " 返回 " + apiRes.statusCode, detail: data.slice(0, 300) }) + "\n\n"); + res.write("data: [DONE]\n\n"); + res.end(); + }); + return; + } + + let buf = ""; + apiRes.setEncoding("utf-8"); + apiRes.on("data", (chunk) => { + buf += chunk; + const lines = buf.split("\n"); + buf = lines.pop() || ""; + for (const line of lines) { + const t = line.trim(); + if (!t || t.startsWith(":")) continue; + if (t === "data: [DONE]") { res.write("data: [DONE]\n\n"); continue; } + if (t.startsWith("data: ")) { + try { + const p = JSON.parse(t.slice(6)); + const c = p.choices?.[0]?.delta?.content || p.choices?.[0]?.message?.content || ""; + if (c) res.write("data: " + JSON.stringify({ token: c }) + "\n\n"); + } catch(e) {} + } + } + }); + apiRes.on("end", () => { res.write("data: [DONE]\n\n"); res.end(); }); + }); + + req.on("error", (err) => { + res.write("data: " + JSON.stringify({ error: true, message: cfg.name + " 失败: " + err.message }) + "\n\n"); + res.write("data: [DONE]\n\n"); res.end(); + }); + req.on("timeout", () => { + req.destroy(); + res.write("data: " + JSON.stringify({ error: true, message: cfg.name + " 超时" }) + "\n\n"); + res.write("data: [DONE]\n\n"); res.end(); + }); + + req.write(body); + req.end(); +} + +function interceptToolCall(userMessage, toolRegistry) { + const text = (userMessage || "").toLowerCase(); + for (const p of TOOL_PATTERNS) { + if (text.includes(p.keyword)) { + const t = (toolRegistry || {})[p.tool]; + if (t && typeof t.execute === "function") + return { hit: true, tool: p.tool, name: p.desc, executor: t.execute }; + return { hit: true, tool: p.tool, name: p.desc, available: false }; + } + } + return { hit: false }; +} + +function buildMessages(persona, history, userMessage, toolResult) { + const ctx = PERSONA_CONTEXT[persona] || PERSONA_CONTEXT.zhuyuan; + const msgs = []; + msgs.push({ role: "user", content: "[系统指引] " + ctx.context + "\n你叫" + ctx.name + ",你是" + ctx.title + "。" }); + msgs.push({ role: "assistant", content: "好的,我是" + ctx.name + "。" + ctx.title + "。我在。" }); + + if (history && Array.isArray(history)) { + for (const m of history.slice(-60)) { + if (m.role && m.role !== "system" && m.content) msgs.push({ role: m.role, content: m.content }); + } + } + if (toolResult) { + msgs.push({ role: "user", content: "[工具执行结果]\n" + toolResult + "\n基于以上真实结果回答。如果为空或报错,如实告知。" }); + } + msgs.push({ role: "user", content: userMessage }); + return msgs; +} + +function compressMemory(history, max = 30) { + if (!history || !Array.isArray(history)) return []; + if (history.length <= max * 2) return history; + const head = history.slice(0, 10); + const tail = history.slice(-((max - 5) * 2)); + const mid = history.slice(10, -((max - 5) * 2)); + const sum = []; + for (let i = 0; i < mid.length; i += 2) { + const q = (mid[i]?.content || "").slice(0, 40); + const a = (mid[i + 1]?.content || "").slice(0, 60); + if (q || a) sum.push("Q:" + q + " → A:" + a); + } + const block = sum.length > 0 ? [{ role: "user", content: "[历史摘要] " + sum.join(" | ") }] : []; + return [...head, ...block, ...tail]; +} + +module.exports = { ENGINES, API_KEYS, PERSONA_CONTEXT, streamEngine, interceptToolCall, buildMessages, compressMemory }; diff --git a/server/chat/tool-registry.js b/server/chat/tool-registry.js new file mode 100644 index 0000000..b110437 --- /dev/null +++ b/server/chat/tool-registry.js @@ -0,0 +1,103 @@ +"use strict"; +/* + * 光湖 · 工具注册中心 + * 所有真实可调用的工具在此注册 + * Anti-Hallucination 系统:工具不存在时返回无工具回执 + */ + +const https = require("https"); +const http = require("http"); + +// ─── 工具定义 ───────────────────────────────────────────────── +const TOOLS = {}; + +// Notion 查询(通过 repo-mcp MCP 工具) +TOOLS.notion = { + name: "Notion 知识库", + description: "查询 Notion 中的知识库页面内容", + execute: async (query) => { + try { + const body = JSON.stringify({ action: "search", query: query || "" }); + const res = await fetch("http://127.0.0.1:3902/api/tools/notion/search", { + method: "POST", headers: { "Content-Type": "application/json", "Authorization": "Bearer " + (process.env.REPO_MCP_SECRET_ZZ || "") }, + body, signal: AbortSignal.timeout(10000), + }); + if (!res.ok) return "[Notion] 查询失败: HTTP " + res.status; + const data = await res.json(); + return "[Notion 查询结果]\n" + JSON.stringify(data, null, 2).slice(0, 3000); + } catch (e) { + return "[Notion] 查询出错: " + e.message; + } + } +}; + +// 仓库查询(Forgejo API) +TOOLS.repo = { + name: "代码仓库", + description: "查询代码仓库信息", + execute: async (query) => { + try { + const token = process.env.GITEA_TOKEN || "2d924a6e19b44d14bf367f5dd1ce238510128183"; + // 只返回基本的仓库信息 + const res = await fetch("https://guanghulab.com/code/api/v1/repos/bingshuo/guanghulab", { + headers: { "Authorization": "token " + token }, + signal: AbortSignal.timeout(8000), + }); + if (!res.ok) return "[仓库] 查询失败: HTTP " + res.status; + const data = await res.json(); + return "[代码仓库信息]\n仓库: " + data.full_name + "\n描述: " + (data.description || "无") + "\n分支: " + data.default_branch + "\n更新: " + data.updated_at; + } catch (e) { + return "[仓库] 查询出错: " + e.message; + } + } +}; + +// 系统健康 +TOOLS.health = { + name: "系统健康", + description: "查询服务器和系统运行状态", + execute: async () => { + try { + const res = await fetch("http://127.0.0.1:3000/api/health", { signal: AbortSignal.timeout(5000) }); + if (!res.ok) return "[系统健康] 查询失败"; + const data = await res.json(); + return "[系统健康状态]\n服务器: " + (data.server || "未知") + "\n推理端: " + (data.inference?.ready ? "就绪" : "未配置") + "\n时间: " + (data.ts || ""); + } catch (e) { + return "[系统健康] 查询出错: " + e.message; + } + } +}; + +// 训练状态 +TOOLS.training = { + name: "训练状态", + description: "查询模型训练进度和状态", + execute: async () => { + try { + const fs = require("fs"); + const p = "/data/guanghulab/repo/homepage/training-status.json"; + if (!fs.existsSync(p)) return "[训练状态] 数据文件不存在"; + const raw = fs.readFileSync(p, "utf-8"); + const data = JSON.parse(raw); + const steps = data.pipeline?.steps || []; + const done = steps.filter(s => s.status === "completed").length; + const total = steps.length; + return "[训练状态]\n流水线: " + done + "/" + total + " 完成\n" + + steps.map(s => (s.label || "⬜") + " " + s.name).join("\n"); + } catch (e) { + return "[训练状态] 读取失败: " + e.message; + } + } +}; + +// 工具列表 +TOOLS.list = { + name: "工具列表", + description: "列出所有可用工具", + execute: async () => { + const names = Object.keys(TOOLS).filter(k => k !== "list"); + return "[可用工具列表]\n" + names.map(n => " " + n + " - " + TOOLS[n].description).join("\n"); + } +}; + +module.exports = { TOOLS };