feat: 巡检增强 / 拓扑大屏 / 远程 HTTP / portable 绿色打包
Some checks failed
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled

巡检模块:巡检调度器改为轮询驱动,新增每日报告存储、告警规则独立
Store、设备 Store 解耦,_executeSnapshot 补齐 DEBUG 声明,巡检中心
与告警中心页增强。

拓扑大屏:MonitoringTopologyView 扩展分组/布局控制,
useMonitoringData 重构数据流。

远程 HTTP:新增 electron/remoteHttpServer.cjs,preload/api.cjs 暴露
IPC,registerBridges 注册 netcatty:http:open 桥。

portable 绿色打包:
- electron-builder.config.cjs 恢复原始白名单(61MB app.asar),
  收集器自动追踪运行时依赖;
- portableData.cjs 重写为 .portable 标记检测;
- scripts/migratePortableLocalStorage.cjs 离线脚本把 Local Storage
  的 http://localhost:5173 origin 批量迁移为 app://netcatty;
- scripts/scanWorkspaceDeps.cjs 扫描 workspace 运行时依赖。
This commit is contained in:
2026-10-19 21:11:57 +08:00
committed by trae-solo-agent
parent 336c3afd47
commit 2e71e03d71
34 changed files with 3012 additions and 230 deletions

View File

@@ -1364,6 +1364,80 @@ export const useVaultState = () => {
let cancelled = false;
const init = async () => {
try {
// ─── 远程浏览器环境:从 HTTP API 获取所有 Vault 数据,跳过 Electron localStorage 加密路径
const isRemote = typeof window !== "undefined" && !!(window as any).__REMOTE_TOKEN__;
if (isRemote) {
try {
const token = (window as any).__REMOTE_TOKEN__ || "";
const base = (p: string) => `${p}?token=${encodeURIComponent(token)}`;
const apiBase = `/api/vault`;
// 并行 fetch 所有 vault 数据
const results = await Promise.allSettled([
fetch(base("/api/devices")).then(r => r.json()),
fetch(base(`${apiBase}/keys`)).then(r => r.json()),
fetch(base(`${apiBase}/proxies`)).then(r => r.json()),
fetch(base(`${apiBase}/port-forwards`)).then(r => r.json()),
fetch(base(`${apiBase}/snippets`)).then(r => r.json()),
fetch(base(`${apiBase}/notes`)).then(r => r.json()),
fetch(base(`${apiBase}/known-hosts`)).then(r => r.json()),
fetch(base(`${apiBase}/connection-logs`)).then(r => r.json()),
fetch(base(`${apiBase}/groups`)).then(r => r.json()),
fetch(base(`${apiBase}/identities`)).then(r => r.json()),
]);
const getOk = (idx: number, fallback: any = null) =>
results[idx]?.status === "fulfilled" && (results[idx]?.value as any)?.ok
? (results[idx]?.value as any).data ?? (results[idx]?.value as any).devices ?? fallback
: fallback;
// hosts
const devicesData = getOk(0, []);
if (Array.isArray(devicesData)) {
const remoteHosts: Host[] = devicesData.map((d: any) => {
const catalogId = d.catalog_id || d.type || "";
return {
id: d.host_id || crypto.randomUUID(),
label: d.label || d.hostname || d.host_id,
hostname: d.hostname || "",
port: 22, username: "", tags: [], os: "linux",
deviceType: /switch|firewall|router/i.test(catalogId) ? "network" : "general",
distro: catalogId, distroMode: "auto",
} as any;
});
hostsRef.current = remoteHosts;
setHosts(remoteHosts);
}
// keys(已被 remoteHttpServer strip 敏感字段)
const keysData = getOk(1, []);
if (Array.isArray(keysData)) setKeys(keysData);
// proxies
const proxiesData = getOk(2, []);
if (Array.isArray(proxiesData)) setProxyProfiles(proxiesData);
// snippets
const snData = getOk(4, []);
if (Array.isArray(snData)) setSnippets(snData);
// notes(可能是数组或对象)
const notesData = getOk(5, null);
if (notesData) setNotes(Array.isArray(notesData) ? notesData : (notesData.notes || []));
// known hosts
const khData = getOk(6, []);
if (Array.isArray(khData)) setKnownHosts(khData);
// connection logs
const logData = getOk(7, []);
if (Array.isArray(logData)) setConnectionLogs(logData);
// identities
const idData = getOk(9, []);
if (Array.isArray(idData)) setIdentities(idData);
} catch (e) {
console.warn("[useVaultState] 远程 vault fetch 失败:", e);
}
if (!cancelled) {
setIsInitialized(true);
setVaultInitialized(true);
}
return; // 远程环境到此为止,跳过所有 Electron IPC 加载
}
await withVaultImportLock("vault", async () => {
recoverPluginImporterTransaction(localStorageAdapter, PLUGIN_IMPORT_TRANSACTION_KEYS);
});
@@ -1401,7 +1475,56 @@ export const useVaultState = () => {
hostsWritePendingRef.current = writePromise;
}
} else {
updateHosts(INITIAL_HOSTS);
// 本地 Local Storage 为空(可能是 origin 隔离导致的打包版环境)
// 尝试从主进程 inspectionStorage 的 _vault_hosts 恢复
let restored = false;
const nc = (window as any).netcatty;
if (nc?.inspection?.getVaultHosts) {
try {
const vaultHosts = await nc.inspection.getVaultHosts();
if (Array.isArray(vaultHosts) && vaultHosts.length > 0) {
// 主进程 inspectionStorage 现在已存完整字段(含凭证),直接复制
const migrated: Host[] = vaultHosts.map((v: any) => ({
id: v.id,
label: v.label || "",
hostname: v.hostname || "",
port: v.port || 22,
username: v.username || "",
password: v.password || undefined,
identityFileId: v.identityFileId || undefined,
privateKey: v.privateKey || "",
passphrase: v.passphrase || "",
tags: v.tags || [],
os: v.os || "linux",
type: v.type || "",
distro: v.distro || "",
group: v.group || "",
}));
if (cancelled) return;
const sanitized = normalizeVaultOrder(
migrated.map((h) => sanitizeHost(h)),
);
hostsRef.current = sanitized;
setHosts(sanitized);
// 加密写回 localStorage,后续启动就正常了
const ver = ++hostsWriteVersion.current;
const encryptPromise = encryptHosts(sanitized);
hostsEncryptPendingRef.current = encryptPromise.then(() => undefined);
const writePromise = encryptPromise.then(async (enc) => {
if (ver !== hostsWriteVersion.current) return;
return commitEncryptedHostsUnderVaultLock(ver, sanitized, enc);
});
hostsWritePendingRef.current = writePromise;
restored = true;
console.info("[useVaultState] 从 inspectionStorage 恢复 hosts:", sanitized.length, "台");
}
} catch (e) {
console.warn("[useVaultState] 从 inspectionStorage 恢复失败:", e);
}
}
if (!restored) {
updateHosts(INITIAL_HOSTS);
}
}
// Read keys fresh here (not before the hosts await) so we don't apply
@@ -2133,6 +2256,23 @@ export const useVaultState = () => {
[importData],
);
// ─── Electron 本地:把所有 vault 数据同步到 inspection-data.json(远程 HTTP API 读)
// 仅在 Electron 本地生效(远程浏览器不走 sync,直接 fetch)
useEffect(() => {
if (typeof window === "undefined") return;
if ((window as any).__REMOTE_TOKEN__) return; // 远程浏览器不走
const nc = (window as any).netcatty;
if (!nc?.inspection?.syncVaultData) return;
try { nc.inspection.syncVaultData("hosts", hosts).catch(() => {}); } catch {}
try { nc.inspection.syncVaultData("keys", keys).catch(() => {}); } catch {}
try { nc.inspection.syncVaultData("identities", identities).catch(() => {}); } catch {}
try { nc.inspection.syncVaultData("proxies", proxyProfiles).catch(() => {}); } catch {}
try { nc.inspection.syncVaultData("snippets", snippets).catch(() => {}); } catch {}
try { nc.inspection.syncVaultData("notes", notes).catch(() => {}); } catch {}
try { nc.inspection.syncVaultData("known_hosts", knownHosts).catch(() => {}); } catch {}
try { nc.inspection.syncVaultData("connection_logs", connectionLogs).catch(() => {}); } catch {}
}, [hosts, keys, identities, proxyProfiles, snippets, notes, knownHosts, connectionLogs]);
return {
isInitialized,
hosts,