85 lines
2.8 KiB
JavaScript
85 lines
2.8 KiB
JavaScript
|
|
/*
|
||
|
|
* /api/manifest — 现实开发层右栏: 模块注册表 (只读)
|
||
|
|
* 守护: 铸渊 · ICE-GL-ZY001
|
||
|
|
*
|
||
|
|
* 数据源:
|
||
|
|
* 1. 优先 PORTAL_DATA_DIR/manifest-snapshot.json (部署时由 CI 打包注入)
|
||
|
|
* 2. 回退 repo 内 .github/brain/architecture/function-manifest.json
|
||
|
|
*
|
||
|
|
* baton-004 自检 3:
|
||
|
|
* "右栏要展示模块注册表. 数据从哪儿来? 直接从 GitHub API 拉?"
|
||
|
|
* 答: 不从 GitHub API. 国内 2C2G 访问 GitHub API 不稳, 且会暴露主权.
|
||
|
|
* 直接读本地快照. PR-6 forgejo 装好后, 这一份会从 forgejo 拉.
|
||
|
|
*/
|
||
|
|
"use strict";
|
||
|
|
|
||
|
|
const express = require("express");
|
||
|
|
const fs = require("fs");
|
||
|
|
const path = require("path");
|
||
|
|
|
||
|
|
const router = express.Router();
|
||
|
|
|
||
|
|
let cache = null;
|
||
|
|
let cacheTs = 0;
|
||
|
|
const CACHE_MS = 30 * 1000;
|
||
|
|
|
||
|
|
function loadManifest(dataDir, repoRoot) {
|
||
|
|
// 1. 部署快照
|
||
|
|
const snap = path.join(dataDir, "manifest-snapshot.json");
|
||
|
|
let raw = null;
|
||
|
|
if (fs.existsSync(snap)) {
|
||
|
|
try { raw = JSON.parse(fs.readFileSync(snap, "utf8")); } catch (_) {}
|
||
|
|
}
|
||
|
|
if (!raw) {
|
||
|
|
// 2. 仓库内 manifest
|
||
|
|
const repoManifest = path.join(repoRoot, ".github", "brain", "architecture", "function-manifest.json");
|
||
|
|
if (fs.existsSync(repoManifest)) {
|
||
|
|
try { raw = JSON.parse(fs.readFileSync(repoManifest, "utf8")); } catch (_) {}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (!raw) return { items: [] };
|
||
|
|
|
||
|
|
const items = [];
|
||
|
|
const fns = Array.isArray(raw.functions) ? raw.functions : [];
|
||
|
|
for (const fn of fns) {
|
||
|
|
if (!fn || typeof fn !== "object") continue;
|
||
|
|
const tags = [];
|
||
|
|
if (fn.id) tags.push(fn.id);
|
||
|
|
if (fn.host_server) tags.push(fn.host_server);
|
||
|
|
if (fn.status) tags.push(fn.status);
|
||
|
|
if (fn.keep_decision === "keep") tags.push("活跃");
|
||
|
|
items.push({
|
||
|
|
title: fn.display_name_zh || fn.id || "(未命名)",
|
||
|
|
tags,
|
||
|
|
summary:
|
||
|
|
(fn.description || fn.role || fn.notes || "").toString().replace(/\s+/g, " ").slice(0, 240)
|
||
|
|
});
|
||
|
|
}
|
||
|
|
// 服务器/宿主也作为模块注册表的一部分展示 (开发层关心物理拓扑)
|
||
|
|
const servers = Array.isArray(raw.servers) ? raw.servers : [];
|
||
|
|
for (const s of servers) {
|
||
|
|
if (!s || typeof s !== "object") continue;
|
||
|
|
const tags = [];
|
||
|
|
if (s.id) tags.push(s.id);
|
||
|
|
if (s.spec) tags.push(s.spec);
|
||
|
|
if (s.status) tags.push(s.status);
|
||
|
|
items.push({
|
||
|
|
title: s.display_name_zh || s.id || "(服务器)",
|
||
|
|
tags,
|
||
|
|
summary: (s.role || s.notes || "").toString().replace(/\s+/g, " ").slice(0, 240)
|
||
|
|
});
|
||
|
|
}
|
||
|
|
return { items, version: raw._version || raw.version || null };
|
||
|
|
}
|
||
|
|
|
||
|
|
router.get("/", (req, res) => {
|
||
|
|
const now = Date.now();
|
||
|
|
if (!cache || now - cacheTs > CACHE_MS) {
|
||
|
|
cache = loadManifest(req.ctx.dataDir, req.ctx.repoRoot);
|
||
|
|
cacheTs = now;
|
||
|
|
}
|
||
|
|
res.json({ source: "function-manifest", ...(cache || { items: [] }) });
|
||
|
|
});
|
||
|
|
|
||
|
|
module.exports = router;
|