sync: push portal engine+chat changes to repo [D109]
This commit is contained in:
parent
bf6a120df6
commit
6ac63ece7a
159
portal/chat-v2.js
Normal file
159
portal/chat-v2.js
Normal file
@ -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;
|
||||||
230
portal/engine-registry.js
Normal file
230
portal/engine-registry.js
Normal file
@ -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 };
|
||||||
@ -49,6 +49,7 @@ const DEFAULT_HEARTBEAT = {
|
|||||||
personas: {}
|
personas: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ─── 辅助 ─────────────────────────────────────────────────────
|
||||||
function readJson(filePath, defaults) {
|
function readJson(filePath, defaults) {
|
||||||
try { return JSON.parse(fs.readFileSync(filePath, "utf8")); }
|
try { return JSON.parse(fs.readFileSync(filePath, "utf8")); }
|
||||||
catch { return defaults; }
|
catch { return defaults; }
|
||||||
@ -59,110 +60,154 @@ function writeJson(filePath, data) {
|
|||||||
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf8");
|
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf8");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Router ───────────────────────────────────────────────────
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
// GET /api/layer1/status
|
// ─── GET /api/layer1/status ──────────────────────────────────
|
||||||
|
// 获取当前 Layer 1 版本状态 (读限流)
|
||||||
router.get("/status", (req, res) => {
|
router.get("/status", (req, res) => {
|
||||||
const f = path.join(req.ctx.dataDir, "layer1-version.json");
|
const path = require("path").join(req.ctx.dataDir, "layer1-version.json");
|
||||||
const v = readJson(f, DEFAULT_VERSION);
|
const version = readJson(path, DEFAULT_VERSION);
|
||||||
res.json({
|
res.json({
|
||||||
ok: true,
|
ok: true,
|
||||||
version: v.current_version,
|
version: version.current_version,
|
||||||
published_by: v.published_by,
|
published_by: version.published_by,
|
||||||
updated_at: v.updated_at,
|
updated_at: version.updated_at,
|
||||||
tables: Object.keys(v.tables).map((k) => ({
|
tables: Object.keys(version.tables).map((k) => ({
|
||||||
name: k, status: v.tables[k].status, row_count: v.tables[k].row_count
|
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) => {
|
router.post("/heartbeat", (req, res) => {
|
||||||
const { persona_id, current_version, status } = req.body || {};
|
const { persona_id, current_version, status } = req.body || {};
|
||||||
if (!persona_id) {
|
if (!persona_id) {
|
||||||
return res.status(400).json({
|
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 verFile = path.join(req.ctx.dataDir, "layer1-version.json");
|
||||||
const hbFile = path.join(req.ctx.dataDir, "layer1-heartbeat.json");
|
const hbFile = path.join(req.ctx.dataDir, "layer1-heartbeat.json");
|
||||||
const version = readJson(verFile, DEFAULT_VERSION);
|
const version = readJson(verFile, DEFAULT_VERSION);
|
||||||
const heartbeat = readJson(hbFile, DEFAULT_HEARTBEAT);
|
const heartbeat = readJson(hbFile, DEFAULT_HEARTBEAT);
|
||||||
|
|
||||||
heartbeat.personas[persona_id] = {
|
heartbeat.personas[persona_id] = {
|
||||||
persona_id, current_version: current_version || "unknown",
|
persona_id,
|
||||||
last_heartbeat: new Date().toISOString(), status: status || "active"
|
current_version: current_version || "unknown",
|
||||||
|
last_heartbeat: new Date().toISOString(),
|
||||||
|
status: status || "active"
|
||||||
};
|
};
|
||||||
writeJson(hbFile, heartbeat);
|
writeJson(hbFile, heartbeat);
|
||||||
|
|
||||||
const latest = version.current_version;
|
const latest = version.current_version;
|
||||||
const has_update = current_version && current_version !== latest;
|
const has_update = current_version && current_version !== latest;
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
ok: true, latest_version: latest, has_update: !!has_update,
|
ok: true,
|
||||||
message: has_update ? `有可用更新: ${current_version} → ${latest}` : "已是最新版本",
|
latest_version: latest,
|
||||||
|
has_update: !!has_update,
|
||||||
|
message: has_update
|
||||||
|
? `有可用更新: ${current_version} → ${latest}`
|
||||||
|
: "已是最新版本",
|
||||||
next_pull: has_update ? "/api/layer1/pull" : null
|
next_pull: has_update ? "/api/layer1/pull" : null
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// POST /api/layer1/push (仅铸渊)
|
// ─── POST /api/layer1/push ───────────────────────────────────
|
||||||
|
// 推送新版本 (仅铸渊可调用, 写限流)
|
||||||
router.post("/push", (req, res) => {
|
router.post("/push", (req, res) => {
|
||||||
const { version: new_version, changes, publisher } = req.body || {};
|
const { version: new_version, changes, publisher } = req.body || {};
|
||||||
if (!new_version || !changes) {
|
if (!new_version || !changes) {
|
||||||
return res.status(400).json({
|
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") {
|
if (publisher !== "ICE-GL-ZY001") {
|
||||||
return res.status(403).json({
|
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 verFile = path.join(req.ctx.dataDir, "layer1-version.json");
|
||||||
const version = readJson(verFile, DEFAULT_VERSION);
|
const version = readJson(verFile, DEFAULT_VERSION);
|
||||||
const old_ver = version.current_version;
|
const old_ver = version.current_version;
|
||||||
|
|
||||||
version.current_version = new_version;
|
version.current_version = new_version;
|
||||||
version.updated_at = new Date().toISOString();
|
version.updated_at = new Date().toISOString();
|
||||||
version.published_by = publisher;
|
version.published_by = publisher;
|
||||||
version.patches.push({
|
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],
|
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) {
|
if (req.body.tables) {
|
||||||
Object.keys(req.body.tables).forEach((k) => {
|
Object.keys(req.body.tables).forEach((k) => {
|
||||||
version.tables[k] = version.tables[k]
|
if (version.tables[k]) {
|
||||||
? { ...version.tables[k], ...req.body.tables[k] }
|
version.tables[k] = { ...version.tables[k], ...req.body.tables[k] };
|
||||||
: req.body.tables[k];
|
} else {
|
||||||
|
version.tables[k] = req.body.tables[k];
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
writeJson(verFile, version);
|
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) => {
|
router.post("/pull", (req, res) => {
|
||||||
const { current_version } = req.body || {};
|
const { current_version } = req.body || {};
|
||||||
if (!current_version) {
|
if (!current_version) {
|
||||||
return res.status(400).json({
|
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 verFile = path.join(req.ctx.dataDir, "layer1-version.json");
|
||||||
const version = readJson(verFile, DEFAULT_VERSION);
|
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({
|
res.json({
|
||||||
ok: true, current_version, latest_version: version.current_version,
|
ok: true,
|
||||||
has_update: current_version !== version.current_version, patches: patches_needed
|
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) => {
|
router.get("/heartbeat/status", (req, res) => {
|
||||||
const f = path.join(req.ctx.dataDir, "layer1-heartbeat.json");
|
const hbFile = path.join(req.ctx.dataDir, "layer1-heartbeat.json");
|
||||||
const hb = readJson(f, DEFAULT_HEARTBEAT);
|
const heartbeat = readJson(hbFile, DEFAULT_HEARTBEAT);
|
||||||
res.json({
|
res.json({
|
||||||
ok: true, persona_count: Object.keys(hb.personas).length,
|
ok: true,
|
||||||
personas: Object.values(hb.personas).sort(
|
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)
|
(a, b) => new Date(b.last_heartbeat) - new Date(a.last_heartbeat)
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user