guanghulab/portal/chat-v2.js
Guanghu Domestic Migration d1e47f4565
Some checks are pending
自动更新代码和重启 / update-and-restart (push) Waiting to run
CI检查 + 自动部署 / check (push) Waiting to run
CI检查 + 自动部署 / deploy (push) Blocked by required conditions
重启聊天服务 / restart (push) Waiting to run
chore: import sanitized domestic snapshot for REPO-002
Source snapshot: ca48d3ddf926d79aa138306164169baf764bb829
2026-07-17 15:54:41 +08:00

160 lines
6.3 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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;