#!/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); }