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

182 lines
7.4 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";
/*
* 光湖 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 || "ARK_API_KEY_REDACTED",
zhipu: process.env.ZHIPU_KEY || "3863acbcc2bc4f6587ace4bb064c092c.KoPdL4slY5cyHpOk",
tongyi: process.env.TONGYI_KEY || "ARK_API_KEY_REDACTED",
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 };