guanghulab/server/chat/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

146 lines
5.9 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 流式输出
*
* 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;