2026-05-27 17:00:09 +08:00
|
|
|
/*
|
|
|
|
|
* ════════════════════════════════════════════════════════════════
|
|
|
|
|
* 光湖 Portal · Express 后端入口
|
|
|
|
|
* Sovereign: TCS-0002∞ · ICE-GL∞ · 国作登字-2026-A-00037559
|
|
|
|
|
* 守护: 铸渊 · ICE-GL-ZY001
|
|
|
|
|
* 模板版本: 0.1.0
|
|
|
|
|
* ════════════════════════════════════════════════════════════════
|
|
|
|
|
*
|
|
|
|
|
* 服务器: GH-CVM-DOMAIN-PROD-01 / ZY-SVR-CN01 / 广州 2C2G
|
|
|
|
|
* 角色: guanghulab.com 唯一对外入口
|
|
|
|
|
*
|
|
|
|
|
* 路由 (全部 /api/* 前缀, nginx 反代):
|
|
|
|
|
* GET /api/brain → 铸渊大脑实时状态 (D115新增)
|
|
|
|
|
* GET /api/heartbeat → 快速心跳
|
|
|
|
|
* GET /api/health → portal + 推理端探活
|
|
|
|
|
* GET /api/active-model → 当前激活的模型
|
|
|
|
|
* POST /api/active-model {name} → 切换激活模型 (mother|coder)
|
|
|
|
|
* GET /api/conversations → 对话列表
|
|
|
|
|
* POST /api/conversations → 新建对话
|
|
|
|
|
* GET /api/conversations/:id/messages → 单对话历史
|
|
|
|
|
* POST /api/chat → 流式对话 (SSE 字节管道)
|
|
|
|
|
* GET /api/persona-db → 人格层右栏数据 (只读)
|
|
|
|
|
* GET /api/manifest → 开发层右栏数据 (只读 manifest 快照)
|
|
|
|
|
* ════════════════════════════════════════════════════════════════
|
|
|
|
|
*/
|
|
|
|
|
"use strict";
|
2026-05-10 13:12:44 +08:00
|
|
|
|
2026-05-27 17:00:09 +08:00
|
|
|
const path = require("path");
|
|
|
|
|
const fs = require("fs");
|
|
|
|
|
const express = require("express");
|
|
|
|
|
const { execSync } = require("child_process");
|
2026-05-10 13:12:44 +08:00
|
|
|
|
2026-05-27 17:00:09 +08:00
|
|
|
const { openDb } = require("./db/init");
|
|
|
|
|
const conversationsRouter = require("./routes/conversations");
|
|
|
|
|
const chatRouter = require("./routes/chat");
|
|
|
|
|
const activeModelRouter = require("./routes/active-model");
|
|
|
|
|
const personaDbRouter = require("./routes/persona-db");
|
|
|
|
|
const manifestRouter = require("./routes/manifest");
|
|
|
|
|
const chatV2Router = require("./routes/chat-v2");
|
|
|
|
|
const inferenceClient = require("./lib/inference-client");
|
|
|
|
|
const { readLimiter, writeLimiter, chatLimiter } = require("./lib/rate-limit");
|
2026-05-10 13:12:44 +08:00
|
|
|
|
2026-05-27 17:00:09 +08:00
|
|
|
// ─── 配置 ─────────────────────────────────────────────────────
|
|
|
|
|
const PORT = parseInt(process.env.PORT || "3000", 10);
|
|
|
|
|
const HOST = process.env.HOST || "127.0.0.1";
|
|
|
|
|
const PORTAL_DATA_DIR = process.env.PORTAL_DATA_DIR || "/data/guanghulab/portal/data";
|
|
|
|
|
const REPO_ROOT = path.resolve(__dirname, "..", "..");
|
|
|
|
|
const REPO_DIR = "/data/guanghulab/repo";
|
|
|
|
|
const STATIC_DIR =
|
|
|
|
|
process.env.PORTAL_STATIC_DIR ||
|
|
|
|
|
path.join(REPO_ROOT, "frontend", "lighthouse-portal");
|
2026-05-10 13:12:44 +08:00
|
|
|
|
2026-05-27 17:00:09 +08:00
|
|
|
// ─── 数据目录 ─────────────────────────────────────────────────
|
|
|
|
|
fs.mkdirSync(PORTAL_DATA_DIR, { recursive: true });
|
|
|
|
|
const DB_PATH = path.join(PORTAL_DATA_DIR, "conversations.sqlite");
|
|
|
|
|
const ENDPOINT_PATH = path.join(PORTAL_DATA_DIR, "inference-endpoint.json");
|
|
|
|
|
const ACTIVE_MODEL_PATH = path.join(PORTAL_DATA_DIR, "active-model.json");
|
2026-05-10 13:12:44 +08:00
|
|
|
|
2026-05-27 17:00:09 +08:00
|
|
|
// ─── 数据库 ───────────────────────────────────────────────────
|
|
|
|
|
const db = openDb(DB_PATH);
|
2026-05-10 13:12:44 +08:00
|
|
|
|
2026-05-27 17:00:09 +08:00
|
|
|
// ─── 推理客户端 (热加载 endpoint) ─────────────────────────────
|
|
|
|
|
inferenceClient.init({ endpointPath: ENDPOINT_PATH, activeModelPath: ACTIVE_MODEL_PATH });
|
|
|
|
|
|
|
|
|
|
// ─── Express ──────────────────────────────────────────────────
|
|
|
|
|
const app = express();
|
|
|
|
|
app.set("trust proxy", 1);
|
|
|
|
|
app.disable("x-powered-by");
|
|
|
|
|
app.use(express.json({ limit: "256kb" }));
|
|
|
|
|
|
|
|
|
|
// 业务上下文
|
|
|
|
|
app.use((req, _res, next) => {
|
|
|
|
|
req.ctx = { db, inference: inferenceClient, dataDir: PORTAL_DATA_DIR, repoRoot: REPO_ROOT };
|
|
|
|
|
next();
|
2026-05-10 13:12:44 +08:00
|
|
|
});
|
|
|
|
|
|
2026-05-27 17:00:09 +08:00
|
|
|
// ─── API 路由 ─────────────────────────────────────────────────
|
|
|
|
|
// 限流 (cc-004 系统自主防御 · 2C2G 不能被扫死):
|
|
|
|
|
app.get("/api/health", readLimiter, async (_req, res) => {
|
|
|
|
|
let inference = { ready: false, message: "推理端尚未配置" };
|
|
|
|
|
try {
|
|
|
|
|
inference = await inferenceClient.health();
|
|
|
|
|
} catch (e) {
|
|
|
|
|
inference = { ready: false, message: `推理端不可达: ${e.message}` };
|
|
|
|
|
}
|
|
|
|
|
res.json({
|
|
|
|
|
ok: true,
|
|
|
|
|
server: "GH-CVM-DOMAIN-PROD-01",
|
|
|
|
|
sovereign: "TCS-0002∞ · 国作登字-2026-A-00037559",
|
|
|
|
|
ts: new Date().toISOString(),
|
|
|
|
|
inference
|
|
|
|
|
});
|
2026-05-10 13:12:44 +08:00
|
|
|
});
|
|
|
|
|
|
2026-05-27 17:00:09 +08:00
|
|
|
// ═══════════════════════════════════════════════════════════════=
|
|
|
|
|
// ⚡ 铸渊大脑实时状态 API (D115新增 — 首页投影面板用)
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════=
|
|
|
|
|
app.get("/api/brain", readLimiter, (_req, res) => {
|
|
|
|
|
try {
|
|
|
|
|
const brainResult = { identity: "铸渊", id: "ICE-GL-ZY001" };
|
|
|
|
|
|
|
|
|
|
// 时间核心
|
|
|
|
|
let temporal = {};
|
|
|
|
|
try {
|
|
|
|
|
temporal = JSON.parse(fs.readFileSync(REPO_DIR + "/brain/temporal-core/temporal-brain.json", "utf8"));
|
|
|
|
|
} catch(e) { temporal = { error: e.message }; }
|
|
|
|
|
|
|
|
|
|
// 母编号
|
|
|
|
|
let mothers = [];
|
2026-05-10 13:12:44 +08:00
|
|
|
try {
|
2026-05-27 17:00:09 +08:00
|
|
|
const reg = JSON.parse(fs.readFileSync(REPO_DIR + "/brain/engineering/mother-registry.json", "utf8"));
|
|
|
|
|
mothers = (reg.mothers || []).map(m => ({
|
|
|
|
|
name: m.name, status: m.status,
|
|
|
|
|
done: (m.sub_paths || []).filter(s => s.status === "done").length,
|
|
|
|
|
total: (m.sub_paths || []).length,
|
|
|
|
|
paths: (m.sub_paths || []).map(s => ({ name: s.name, status: s.status }))
|
|
|
|
|
}));
|
|
|
|
|
} catch(e) {}
|
|
|
|
|
|
|
|
|
|
// 服务器健康
|
|
|
|
|
let server = { status: "offline", services: [] };
|
|
|
|
|
try {
|
|
|
|
|
const pm2 = JSON.parse(execSync("pm2 jlist 2>/dev/null", {timeout:3000}).toString());
|
|
|
|
|
const online = pm2.filter(p => p.pm2_env.status === "online");
|
|
|
|
|
server = {
|
|
|
|
|
status: "online",
|
|
|
|
|
services_online: online.length,
|
|
|
|
|
services_total: pm2.length,
|
|
|
|
|
services: online.map(p => p.name),
|
|
|
|
|
stopped: pm2.filter(p => p.pm2_env.status !== "online").map(p => p.name)
|
|
|
|
|
};
|
|
|
|
|
} catch(e) {}
|
|
|
|
|
|
|
|
|
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
|
|
|
res.json({
|
|
|
|
|
identity: "铸渊 · ICE-GL-ZY001",
|
|
|
|
|
copyright: "国作登字-2026-A-00037559",
|
|
|
|
|
awakening: (temporal.clock || {}).awakening_count || 0,
|
|
|
|
|
age_days: (temporal.clock || {}).repo_age_days || 0,
|
|
|
|
|
date: (temporal.clock || {}).current_date || "",
|
|
|
|
|
cognition: ((temporal.clock || {}).latest_cognition || "").substring(0, 100),
|
|
|
|
|
epochs: ((temporal.timeline || {}).epochs || []).slice(-5).map(e => ({
|
|
|
|
|
epoch: e.epoch, event: (e.event || "").substring(0, 80), date: e.date
|
|
|
|
|
})),
|
|
|
|
|
mothers: mothers,
|
|
|
|
|
server: server,
|
|
|
|
|
updated: new Date().toISOString()
|
|
|
|
|
});
|
|
|
|
|
} catch (e) {
|
|
|
|
|
res.status(500).json({ error: e.message });
|
|
|
|
|
}
|
2026-05-10 13:12:44 +08:00
|
|
|
});
|
|
|
|
|
|
2026-05-27 17:00:09 +08:00
|
|
|
app.get("/api/heartbeat", (_req, res) => {
|
|
|
|
|
try {
|
|
|
|
|
const t = JSON.parse(fs.readFileSync(REPO_DIR + "/brain/temporal-core/temporal-brain.json", "utf8"));
|
|
|
|
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
|
|
|
res.json({ alive: true, time: new Date().toISOString(), awakening: (t.clock || {}).awakening_count || 0, age_days: (t.clock || {}).repo_age_days || 0 });
|
|
|
|
|
} catch(e) {
|
|
|
|
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
|
|
|
res.json({ alive: true, fallback: true });
|
|
|
|
|
}
|
2026-05-10 13:12:44 +08:00
|
|
|
});
|
|
|
|
|
|
2026-05-27 17:00:09 +08:00
|
|
|
app.use("/api/active-model", writeLimiter, activeModelRouter);
|
|
|
|
|
app.use("/api/conversations", writeLimiter, conversationsRouter);
|
|
|
|
|
app.use("/api/chat", chatLimiter, chatRouter);
|
|
|
|
|
app.use("/api/persona-db", readLimiter, personaDbRouter);
|
|
|
|
|
app.use("/api/chat-v2", chatLimiter, chatV2Router);
|
|
|
|
|
app.use("/api/manifest", readLimiter, manifestRouter);
|
2026-05-27 16:57:53 +08:00
|
|
|
|
2026-05-27 17:00:09 +08:00
|
|
|
// ─── 静态 (nginx 上线后会兜底走 nginx, 这里保留兜底) ──────────
|
|
|
|
|
app.use("/_static", express.static(path.join(STATIC_DIR, "assets"), { fallthrough: true }));
|
|
|
|
|
app.get("/", (_req, res, next) => {
|
|
|
|
|
const idx = path.join(STATIC_DIR, "index.html");
|
|
|
|
|
fs.access(idx, fs.constants.R_OK, (err) => {
|
|
|
|
|
if (err) return next();
|
|
|
|
|
res.sendFile(idx);
|
|
|
|
|
});
|
2026-05-27 16:57:53 +08:00
|
|
|
});
|
|
|
|
|
|
2026-05-27 17:00:09 +08:00
|
|
|
// ─── 404 + 错误处理 (中文) ────────────────────────────────────
|
|
|
|
|
app.use((req, res) => {
|
|
|
|
|
if (req.path.startsWith("/api/")) {
|
|
|
|
|
res.status(404).json({ error: true, code: "not_found", message: "接口不存在: " + req.path });
|
|
|
|
|
} else {
|
|
|
|
|
res.status(404).type("html").send("<h1>404</h1><p>页面不存在</p>");
|
|
|
|
|
}
|
2026-05-27 16:57:53 +08:00
|
|
|
});
|
|
|
|
|
|
2026-05-27 17:00:09 +08:00
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
|
|
|
app.use((err, req, res, _next) => {
|
|
|
|
|
const msg = (err && err.message) || "未知错误";
|
|
|
|
|
console.error("[portal] 内部错误:", err);
|
|
|
|
|
if (res.headersSent) return;
|
|
|
|
|
res
|
|
|
|
|
.status(err && err.status ? err.status : 500)
|
|
|
|
|
.json({ error: true, code: "internal_error", message: "内部错误: " + msg });
|
2026-05-10 13:12:44 +08:00
|
|
|
});
|
|
|
|
|
|
2026-05-27 17:00:09 +08:00
|
|
|
// ─── 启动 ─────────────────────────────────────────────────────
|
|
|
|
|
if (require.main === module) {
|
|
|
|
|
app.listen(PORT, HOST, () => {
|
|
|
|
|
console.log(`[portal] 已启动 · ${HOST}:${PORT} · 数据目录=${PORTAL_DATA_DIR}`);
|
|
|
|
|
console.log(`[portal] 推理端点文件: ${ENDPOINT_PATH}`);
|
|
|
|
|
console.log(`[portal] 🧠 /api/brain · 💓 /api/heartbeat`);
|
|
|
|
|
console.log("[portal] 守护: 铸渊 · ICE-GL-ZY001 · TCS-0002∞ · 国作登字-2026-A-00037559");
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
module.exports = { app, db };
|