Files
zcode-tooling/zcode-env-sync/scripts/status.mjs
T

132 lines
6.3 KiB
JavaScript

#!/usr/bin/env node
/**
* /sync-status:对比本机与远端最新快照差异(只读,不改本机)。
* 用法: node status.mjs
*/
import fs from "node:fs";
import path from "node:path";
import {
CLI_CONFIG, readJson, officialPlugins, enabledPlugins,
customPlugins, hashDir, secretTargets, secretsManifest,
ensureRepo, latestSnapshotDir, latestSnapshotName, readSnapshotManifest,
extractModelSecrets, modelKeyManifest, listSnapshots, humanSize,
} from "./sync-core.mjs";
function fail(e) { console.error(`状态检查失败:${e?.message ?? e}`); process.exitCode = 1; }
/**
* --list:列出远端全部快照。
* sync-import 的文档让用户"先 /sync-status 看远端有哪些(快照名)",
* 但这里原本只有"本机 vs 最新一个"的对比,列不出来 —— 补上这条。
*/
function listAll() {
ensureRepo();
const all = listSnapshots();
if (!all.length) { console.log("# 远端尚无快照\n- 先 /sync-export 推第一版"); return; }
const latest = latestSnapshotName();
const out = [`# 远端快照(${all.length} 个,合计 ${humanSize(all.reduce((a, s) => a + s.bytes, 0))})`];
for (const s of all) {
const mark = s.name === latest ? "*" : " ";
out.push(`${mark} ${s.name}`);
out.push(` ${s.host} @ ${s.createdAt ?? "?"} ${humanSize(s.bytes)}`
+ (s.valid ? "" : " ⚠ manifest 损坏"));
if (s.valid) {
out.push(` 官方启用 ${s.enabledCount} / 自研 ${s.custom.length}${s.custom.length ? `(${s.custom.join(", ")})` : ""}`
+ ` / MCP ${s.mcpCount} / 密钥 ${s.secretCount}`
+ (s.modelProviderCount ? ` / 模型 provider ${s.modelProviderCount}` : ""));
}
}
out.push(`- 标 * 的是 latest.json 指向的快照(/sync-import 不带参数时用的就是它)`);
out.push(`- 导入指定版本:/sync-import <上面任意快照名>`);
console.log(out.join("\n"));
}
try {
if (process.argv.includes("--list")) { listAll(); process.exit(0); }
ensureRepo();
const snapDir = latestSnapshotDir();
if (!snapDir) {
console.log("# 远端尚无快照\n- 先 /sync-export 推第一版");
process.exit(0);
}
const m = readSnapshotManifest(snapDir);
const out = [`# 本机 vs ${path.basename(snapDir)}`, `- 快照来源:${m.host} @ ${m.createdAt}`];
// 官方启用差异
const localEn = new Set(enabledPlugins());
const snapEn = new Set(m.enabled ?? []);
const miss = [...snapEn].filter((e) => !localEn.has(e));
const extra = [...localEn].filter((e) => !snapEn.has(e));
out.push(`- 官方插件:快照启用 ${(m.enabled ?? []).length} 项,本机启用 ${localEn.size} 项,缺 ${miss.length}(${miss.join(", ") || "—"}),多 ${extra.length}(${extra.join(", ") || "—"})`);
// 自研插件差异(按 hash)
const localCp = new Map(customPlugins().map((c) => [c.name, hashDir(c.dir)]));
const snapCp = new Map((m.custom ?? []).map((c) => [c.name, c.hash]));
const diffCp = [];
for (const [name, h] of snapCp) {
if (!localCp.has(name)) diffCp.push(`${name}(快照有/本机无)`);
else if (localCp.get(name) !== h) diffCp.push(`${name}(版本不同)`);
}
for (const name of localCp.keys()) if (!snapCp.has(name)) diffCp.push(`${name}(本机有/快照无)`);
out.push(`- 自研插件:${diffCp.length ? `差异 ${diffCp.length}:` + diffCp.join("; ") : "一致"}`);
// MCP server 名差异
const cfg = readJson(CLI_CONFIG, {});
const localMcp = new Set(Object.keys(cfg?.mcp?.servers ?? {}));
const snapMcp = new Set(m.mcpServerNames ?? []);
const mMiss = [...snapMcp].filter((s) => !localMcp.has(s));
const mExtra = [...localMcp].filter((s) => !snapMcp.has(s));
out.push(`- MCP:${mMiss.length || mExtra.length ? `缺 ${mMiss.join(", ") || "—"};多 ${mExtra.join(", ") || "—"}` : "server 名一致"}`);
// 密钥文件差异(只比清单 hash,不读值)
const localSec = new Map(secretsManifest(secretTargets()).map((f) => [f.rel, f.sha256]));
const snapSec = new Map((m.secretFiles ?? []).map((f) => [f.rel, f.sha256]));
const sDiff = [];
for (const [rel, h] of snapSec) {
if (!localSec.has(rel)) sDiff.push(`${rel}(快照有/本机无)`);
else if (localSec.get(rel) !== h) sDiff.push(`${rel}(内容不同)`);
}
for (const rel of localSec.keys()) if (!snapSec.has(rel)) sDiff.push(`${rel}(本机有/快照无)`);
out.push(`- 密钥束:${sDiff.length ? `差异 ${sDiff.length}:` + sDiff.join("; ") : "清单一致"}`);
// 模型 provider 差异(按 id 比对,只比结构和模型数,不读任何密钥值)
if ((m.modelProviders ?? []).length) {
let localProvs = [];
try {
const { summarizeProviders, V2_CONFIG } = await import("./sync-core.mjs");
localProvs = summarizeProviders(readJson(V2_CONFIG, {}));
} catch { /* 读不到就当本机没有 */ }
const snapP = new Map(m.modelProviders.map((p) => [p.id, p]));
const locP = new Map(localProvs.map((p) => [p.id, p]));
const pd = [];
for (const [id, sp] of snapP) {
const lp = locP.get(id);
if (!lp) pd.push(`${id}(快照有/本机无)`);
else if (JSON.stringify(lp.models) !== JSON.stringify(sp.models)) pd.push(`${id}(模型不同)`);
}
for (const id of locP.keys()) if (!snapP.has(id)) pd.push(`${id}(本机有/快照无)`);
const needKey = m.modelProvidersWithSecrets ?? [];
out.push(`- 模型 provider:${pd.length ? `差异 ${pd.length}:` + pd.join("; ") : `${snapP.size} 个一致`}`
+ (needKey.length ? `;带密钥 ${needKey.length} 个` : ""));
// 模型密钥清单对比(只比哈希,不读不打值)
if ((m.modelKeys ?? []).length) {
let localKeys = [];
try {
const { V2_CONFIG } = await import("./sync-core.mjs");
localKeys = modelKeyManifest(extractModelSecrets(readJson(V2_CONFIG, {})));
} catch { /* 读不到就当本机没有 */ }
const snapK = new Map((m.modelKeys ?? []).map((k) => [k.rel, k.sha256]));
const locK = new Map(localKeys.map((k) => [k.rel, k.sha256]));
const kd = [];
for (const [rel, h] of snapK) {
if (!locK.has(rel)) kd.push(`${rel}(快照有/本机无)`);
else if (locK.get(rel) !== h) kd.push(`${rel}(值不同)`);
}
for (const rel of locK.keys()) if (!snapK.has(rel)) kd.push(`${rel}(本机有/快照无)`);
out.push(`- 模型密钥:${kd.length ? `差异 ${kd.length}:` + kd.join("; ") : `${locK.size} 个一致`}`);
}
}
console.log(out.join("\n"));
} catch (e) { fail(e); }