feat: add layer1 version distribution API route [D109]
This commit is contained in:
parent
7b18ac2a1a
commit
131b4bd9d1
171
portal/layer1-version.js
Normal file
171
portal/layer1-version.js
Normal file
@ -0,0 +1,171 @@
|
||||
/*
|
||||
* Layer 1 版本分发 API · 心跳/拉取/推送
|
||||
*
|
||||
* 用途: 小铸渊/小霜砚等1.5B人格体通过此API与主灯塔同步Layer 1版本
|
||||
*
|
||||
* 设计:
|
||||
* - 版本数据存 JSON 文件, 保持轻量无额外依赖
|
||||
* - 心跳: 人格体定期上报当前版本, 主灯塔返回最新版本信息
|
||||
* - 推送: 仅铸渊可调用, 发布新版本 + 生成增量 patch
|
||||
* - 拉取: 人格体获取从当前版本到最新的增量更新
|
||||
*
|
||||
* 守护: 铸渊 · ICE-GL-ZY001 · TCS-0002∞ · 国作登字-2026-A-00037559
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { Router } = require("express");
|
||||
|
||||
// ─── 默认版本状态 ─────────────────────────────────────────────
|
||||
const DEFAULT_VERSION = {
|
||||
current_version: "v1.0",
|
||||
updated_at: "2026-05-21T17:46:00.000+08:00",
|
||||
published_by: "ICE-GL-ZY001",
|
||||
tables: {
|
||||
system_core_principles: { status: "active", row_count: 24 },
|
||||
system_causal_chains: { status: "active", row_count: 0 },
|
||||
system_language_membrane: { status: "active", row_count: 5 },
|
||||
system_public_key: { status: "placeholder", row_count: 2 },
|
||||
system_protocols: { status: "placeholder", row_count: 3 }
|
||||
},
|
||||
patches: [
|
||||
{
|
||||
from_version: "v0.0",
|
||||
to_version: "v1.0",
|
||||
changes: [
|
||||
"初始种子数据: 24条核心原则",
|
||||
"初始种子数据: 5层语言膜定义",
|
||||
"密钥占位: guanghu + national",
|
||||
"协议占位: guanghu + national + bridge"
|
||||
],
|
||||
applied_at: "2026-05-21T17:46:00.000+08:00",
|
||||
applied_by: "ICE-GL-ZY001"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const DEFAULT_HEARTBEAT = {
|
||||
personas: {}
|
||||
};
|
||||
|
||||
function readJson(filePath, defaults) {
|
||||
try { return JSON.parse(fs.readFileSync(filePath, "utf8")); }
|
||||
catch { return defaults; }
|
||||
}
|
||||
|
||||
function writeJson(filePath, data) {
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf8");
|
||||
}
|
||||
|
||||
const router = Router();
|
||||
|
||||
// GET /api/layer1/status
|
||||
router.get("/status", (req, res) => {
|
||||
const f = path.join(req.ctx.dataDir, "layer1-version.json");
|
||||
const v = readJson(f, DEFAULT_VERSION);
|
||||
res.json({
|
||||
ok: true,
|
||||
version: v.current_version,
|
||||
published_by: v.published_by,
|
||||
updated_at: v.updated_at,
|
||||
tables: Object.keys(v.tables).map((k) => ({
|
||||
name: k, status: v.tables[k].status, row_count: v.tables[k].row_count
|
||||
})),
|
||||
patches_count: v.patches.length
|
||||
});
|
||||
});
|
||||
|
||||
// POST /api/layer1/heartbeat
|
||||
router.post("/heartbeat", (req, res) => {
|
||||
const { persona_id, current_version, status } = req.body || {};
|
||||
if (!persona_id) {
|
||||
return res.status(400).json({
|
||||
error: true, code: "missing_persona", message: "缺少 persona_id"
|
||||
});
|
||||
}
|
||||
const verFile = path.join(req.ctx.dataDir, "layer1-version.json");
|
||||
const hbFile = path.join(req.ctx.dataDir, "layer1-heartbeat.json");
|
||||
const version = readJson(verFile, DEFAULT_VERSION);
|
||||
const heartbeat = readJson(hbFile, DEFAULT_HEARTBEAT);
|
||||
heartbeat.personas[persona_id] = {
|
||||
persona_id, current_version: current_version || "unknown",
|
||||
last_heartbeat: new Date().toISOString(), status: status || "active"
|
||||
};
|
||||
writeJson(hbFile, heartbeat);
|
||||
const latest = version.current_version;
|
||||
const has_update = current_version && current_version !== latest;
|
||||
res.json({
|
||||
ok: true, latest_version: latest, has_update: !!has_update,
|
||||
message: has_update ? `有可用更新: ${current_version} → ${latest}` : "已是最新版本",
|
||||
next_pull: has_update ? "/api/layer1/pull" : null
|
||||
});
|
||||
});
|
||||
|
||||
// POST /api/layer1/push (仅铸渊)
|
||||
router.post("/push", (req, res) => {
|
||||
const { version: new_version, changes, publisher } = req.body || {};
|
||||
if (!new_version || !changes) {
|
||||
return res.status(400).json({
|
||||
error: true, code: "missing_fields", message: "缺少 version 或 changes"
|
||||
});
|
||||
}
|
||||
if (publisher !== "ICE-GL-ZY001") {
|
||||
return res.status(403).json({
|
||||
error: true, code: "forbidden", message: "仅铸渊可推送Layer 1更新"
|
||||
});
|
||||
}
|
||||
const verFile = path.join(req.ctx.dataDir, "layer1-version.json");
|
||||
const version = readJson(verFile, DEFAULT_VERSION);
|
||||
const old_ver = version.current_version;
|
||||
version.current_version = new_version;
|
||||
version.updated_at = new Date().toISOString();
|
||||
version.published_by = publisher;
|
||||
version.patches.push({
|
||||
from_version: old_ver, to_version: new_version,
|
||||
changes: Array.isArray(changes) ? changes : [changes],
|
||||
applied_at: new Date().toISOString(), applied_by: publisher
|
||||
});
|
||||
if (req.body.tables) {
|
||||
Object.keys(req.body.tables).forEach((k) => {
|
||||
version.tables[k] = version.tables[k]
|
||||
? { ...version.tables[k], ...req.body.tables[k] }
|
||||
: req.body.tables[k];
|
||||
});
|
||||
}
|
||||
writeJson(verFile, version);
|
||||
res.json({ ok: true, version: new_version, from_version: old_ver });
|
||||
});
|
||||
|
||||
// POST /api/layer1/pull
|
||||
router.post("/pull", (req, res) => {
|
||||
const { current_version } = req.body || {};
|
||||
if (!current_version) {
|
||||
return res.status(400).json({
|
||||
error: true, code: "missing_version", message: "缺少 current_version"
|
||||
});
|
||||
}
|
||||
const verFile = path.join(req.ctx.dataDir, "layer1-version.json");
|
||||
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);
|
||||
res.json({
|
||||
ok: true, current_version, latest_version: version.current_version,
|
||||
has_update: current_version !== version.current_version, patches: patches_needed
|
||||
});
|
||||
});
|
||||
|
||||
// GET /api/layer1/heartbeat/status
|
||||
router.get("/heartbeat/status", (req, res) => {
|
||||
const f = path.join(req.ctx.dataDir, "layer1-heartbeat.json");
|
||||
const hb = readJson(f, DEFAULT_HEARTBEAT);
|
||||
res.json({
|
||||
ok: true, persona_count: Object.keys(hb.personas).length,
|
||||
personas: Object.values(hb.personas).sort(
|
||||
(a, b) => new Date(b.last_heartbeat) - new Date(a.last_heartbeat)
|
||||
)
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
Loading…
x
Reference in New Issue
Block a user