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

@@ -90,6 +90,9 @@ interface UseMonitoringDataResult {
// 操作
refresh: () => Promise<void>;
loading: boolean;
// 浏览器远程访问环境下实际使用的 hosts(Electron 环境 = props.hosts)
effectiveHosts: Host[];
}
// ===== localStorage 持久化(降级用,API 不可用时显示缓存) =====
@@ -321,6 +324,61 @@ export function useMonitoringData(hosts: Host[], keys: any[] = []): UseMonitorin
hostsRef.current = hosts;
keysRef.current = keys;
// 浏览器远程访问环境:从 /api/devices 构建虚拟 hosts 列表(Electron vault 不可用)
const [remoteHosts, setRemoteHosts] = useState<Host[]>([]);
const IS_REMOTE = typeof window !== "undefined" && !!(window as any).__REMOTE_TOKEN__;
const REMOTE_TOKEN = (window as any).__REMOTE_TOKEN__ || "";
// Electron 本地:同步 vault hosts 给主进程(供远程浏览器 API 读)
useEffect(() => {
if (IS_REMOTE) return; // 远程浏览器不触发
const nc = (window as any).netcatty;
if (!nc?.inspection) return;
if (hosts.length === 0) return;
console.log('[useMonitoringData] syncing vault hosts to main process:', hosts.length);
nc.inspection.saveVaultHosts(hosts).then((r: any) => {
console.log('[useMonitoringData] saveVaultHosts result:', r);
}).catch((e: any) => {
console.warn('[useMonitoringData] saveVaultHosts error:', e);
});
}, [hosts, IS_REMOTE]);
useEffect(() => {
if (!IS_REMOTE) return;
console.log('[monitoring] REMOTE ENV - props.hosts.length=', hosts.length, 'remoteHosts.length=', remoteHosts.length);
if (hosts.length > 0) {
console.warn('[monitoring] REMOTE ENV 但 props.hosts 有值!跳过 remote hosts 加载,hosts[0]=', hosts[0]);
return;
}
const loadRemoteHosts = async () => {
try {
const res = await fetch(`/api/devices?token=${encodeURIComponent(REMOTE_TOKEN)}`);
const data = await res.json();
const list: Host[] = (data.devices || []).map((d: any) => ({
id: d.host_id,
label: d.label || d.name || d.hostname || d.host_id,
hostname: d.hostname || d.name || "",
port: 22,
username: "",
distro: d.distro || d.catalog_type || d.catalog_id || "",
type: d.type || "",
catalogId: d.catalog_id || "",
tags: [],
os: "linux" as const,
group: "",
}));
setRemoteHosts(list);
hostsRef.current = list;
} catch (e) {
console.warn("[monitoring] remote hosts load failed:", e);
}
};
void loadRemoteHosts();
}, [IS_REMOTE, REMOTE_TOKEN, hosts.length]);
// 实际使用的 hosts:Electron 用 props,浏览器用远程加载的
const effectiveHosts = IS_REMOTE && hosts.length === 0 ? remoteHosts : hosts;
const refresh = useCallback(async (silent = false) => {
if (loadingRef.current) {
console.log('[monitoring] refresh() 正在执行中,跳过');
@@ -505,33 +563,33 @@ export function useMonitoringData(hosts: Host[], keys: any[] = []): UseMonitorin
// 自动启动调度器
if (enabledDevices.length === 0) {
console.log('[monitoring] 没有启用巡检的设备');
} else if (schedulerStatus.running) {
console.log('[monitoring] 调度器已在运行中');
} else {
console.log('[monitoring] 条件满足,准备启动调度器');
console.log('[monitoring] 条件满足,准备启动调度器');
try {
const hostConfigs: Record<string, Record<string, unknown>> = {};
for (const dev of enabledDevices) {
const hostname = dev.hostname;
if (!hostname) {
console.log('[monitoring] 设备', dev.host_id, '没有 hostname,跳过');
continue;
}
const host = hostsRef.current.find((h) => h.hostname === hostname);
const allExt = allExtensions || {};
for (const [hostId, ext] of Object.entries(allExt)) {
if (!ext || !ext.inspection_enabled) continue;
const host = hostsRef.current.find((h) => h.id === hostId);
if (!host) {
console.log('[monitoring] vault 中未找到 hostname:', hostname, ',跳过');
console.log('[monitoring] vault 中未找到 host_id:', hostId, ',跳过');
continue;
}
if (!host.username) {
console.log('[monitoring] 设备', hostname, '缺少 username,跳过');
console.log('[monitoring] 设备', host.label || host.hostname || hostId, '缺少 username,跳过');
continue;
}
hostConfigs[dev.host_id] = buildSshConfig(host, keysRef.current);
hostConfigs[hostId] = buildSshConfig(host, keysRef.current);
}
// 诊断:打印 hostConfigs 样本看 label 有没有正确传递
const sample = Object.entries(hostConfigs).slice(0, 3).map(([id, c]) => ({ id, label: c.label, host: c.host, username: c.username }));
console.log('[monitoring] hostConfigs 样本:', JSON.stringify(sample));
if (Object.keys(hostConfigs).length === 0) {
console.log('[monitoring] 没有可巡检的设备(hostname 未写入或配置不完整)');
console.log('[monitoring] 没有可巡检的设备(vault 中匹配不到或凭据不完整)');
} else {
console.log('[monitoring] 启动调度器,匹配到', Object.keys(hostConfigs).length, '台设备');
console.log('[monitoring] 启动调度器,匹配到', Object.keys(hostConfigs).length, '台设备(running=' + schedulerStatus.running + ')');
// 不管 running 与否都传 hostConfigs 过去让主进程更新,
// 主进程 execFn 通过模块级 _hostConfigs 引用动态读最新值
await inspectionSchedulerApi.start(hostConfigs);
setSchedulerRunning(true);
}
@@ -586,7 +644,7 @@ export function useMonitoringData(hosts: Host[], keys: any[] = []): UseMonitorin
setOnlineMap((prev) => {
const next: Record<string, boolean> = { ...prev };
let changed = false;
for (const h of hosts) {
for (const h of effectiveHosts) {
if (next[h.id] === undefined) {
next[h.id] = true; // 假设在线,等巡检结果覆盖
changed = true;
@@ -594,7 +652,7 @@ export function useMonitoringData(hosts: Host[], keys: any[] = []): UseMonitorin
}
return changed ? next : prev;
});
}, [hosts]);
}, [effectiveHosts]);
// 从告警规则派生阈值(和 alertRuleStore 默认规则对齐)
const thresholds = useMemo(() => {
@@ -636,5 +694,6 @@ export function useMonitoringData(hosts: Host[], keys: any[] = []): UseMonitorin
thresholds,
refresh,
loading,
effectiveHosts,
};
}