"use client"; /** * 巡检报告定时快照(全局 Hook) * * 挂载到 App.tsx 顶层 → 不管用户在哪个 Tab 都能定时跑快照,关页面(巡检报告 Tab)也不停。 * 只有彻底关闭 Electron 应用才会停。 * * 双路执行: * - 主进程 reportScheduler(setInterval 1min)独立执行定时快照 + 日报告聚合 * - renderer 侧(本 Hook)负责:1) 把 vault 解密的 SSH 凭据同步给主进程;2) 作为兜底在 renderer 侧也执行 runSnapshot * * 为什么双路: * - 主进程 scheduler 不依赖 renderer 是否打开监控页面 / Vault / 巡检报告页面 * - 但主进程没法解密 vault,必须由 renderer 在启动后(VaultPublisher 已解密) * 调 inspectionSchedulerApi.start(hostConfigs) 把凭据同步过去 */ import { useCallback, useEffect, useRef } from "react"; import { inspectionReportApi, inspectionReportScheduleApi, inspectionDeviceApi, inspectionSchedulerApi, type ReportSchedule, } from "../../infrastructure/inspection/inspectionApi"; import { STORAGE_KEY_HOSTS, STORAGE_KEY_KEYS } from "../../infrastructure/config/storageKeys"; import { decryptHosts, decryptKeys } from "../../infrastructure/persistence/secureFieldAdapter"; interface HostConfig { device_id: string; hostname: string; username: string; port?: number; password?: string; privateKeyContent?: string; passphrase?: string; catalog_id?: string; } /** 从 localStorage 读加密 hosts/keys → 解密 → 构建所有巡检设备 target */ async function buildTargets(enabledDeviceIds?: string[]): Promise { // 1. 从 localStorage 读加密的 hosts/keys(VaultPublisher 已解密过的,或自己解密) let hosts: any[] = []; let keys: any[] = []; try { const rawHosts = localStorage.getItem(STORAGE_KEY_HOSTS); if (rawHosts) hosts = JSON.parse(rawHosts); } catch (_) {} try { const rawKeys = localStorage.getItem(STORAGE_KEY_KEYS); if (rawKeys) keys = JSON.parse(rawKeys); } catch (_) {} // 2. 解密 let decryptedHosts: any[] = []; let decryptedKeys: any[] = []; try { decryptedHosts = await decryptHosts(hosts); } catch (_) { decryptedHosts = hosts; } try { decryptedKeys = await decryptKeys(keys); } catch (_) { decryptedKeys = keys; } if (decryptedHosts.length === 0) { console.error("[ReportAutoScheduler] localStorage 里没有 hosts!hosts=", hosts.length); return []; } // 3. 读 device extension(catalog_id 等) const exts = await inspectionDeviceApi.getAll().catch(() => ({})); const targets: HostConfig[] = []; for (const host of decryptedHosts) { if (enabledDeviceIds && enabledDeviceIds.length > 0 && !enabledDeviceIds.includes(host.id)) continue; const ext = exts?.[host.id]; const key = decryptedKeys.find((k: any) => k.id === host.identityFileId); const target: HostConfig = { device_id: host.id, hostname: host.hostname, username: host.username, port: host.port || 22, catalog_id: ext?.catalog_id, }; if (host.password) target.password = host.password; if (key?.privateKeyContent || key?.privateKey) { target.privateKeyContent = key.privateKeyContent || key.privateKey; if (key.passphrase) target.passphrase = key.passphrase; } targets.push(target); } return targets; } /** 把 HostConfig[] 转成主进程 inspection:scheduler:start 需要的 { host_id → SSH config } 格式 */ function toSchedulerHostConfigs(targets: HostConfig[]): Record> { const out: Record> = {}; for (const t of targets) { out[t.device_id] = { host: t.hostname, hostname: t.hostname, port: t.port || 22, username: t.username, password: t.password, privateKey: t.privateKeyContent, privateKeyContent: t.privateKeyContent, passphrase: t.passphrase, catalog_id: t.catalog_id, }; } return out; } /** 同步 AI providers 到主进程(主进程 AI 汇总需要) */ async function syncAiProviders() { try { const api = (window as any).netcatty; const providersRaw = localStorage.getItem("netcatty_ai_providers_v1"); if (providersRaw && api?.aiSyncProviders) { await api.aiSyncProviders(JSON.parse(providersRaw)); } } catch (_) {} } export function useReportAutoScheduler() { const lastRunAtRef = useRef(0); const runningRef = useRef(false); const scheduleRef = useRef(null); const hostsSyncedRef = useRef(false); // 加载 schedule useEffect(() => { void inspectionReportScheduleApi.get().then(cfg => { scheduleRef.current = cfg; }).catch(() => {}); }, []); // 监听 schedule 变更(用户在设置面板修改配置后自动生效) useEffect(() => { const api = (window as any).netcatty; if (!api?.on || typeof api.on !== "function") return; const handler = (_data: any) => { void inspectionReportScheduleApi.get().then(cfg => { scheduleRef.current = cfg; lastRunAtRef.current = 0; }).catch(() => {}); }; api.on("inspection:reportScheduleChanged", handler); return () => { try { api.off?.("inspection:reportScheduleChanged", handler); } catch {} }; }, []); /** mount 后延迟同步一次 hostConfigs 给主进程,让主进程 reportScheduler 能独立执行定时快照 */ useEffect(() => { // 远程浏览器环境:vault localStorage 加密数据读不到,无法解密 hosts,跳过定时巡检报告逻辑 const IS_REMOTE = typeof window !== "undefined" && !!(window as any).__REMOTE_TOKEN__; if (IS_REMOTE) { console.info("[ReportAutoScheduler] 远程访问环境,跳过巡检报告自动调度"); return; } const syncOnce = async () => { if (hostsSyncedRef.current) return; try { const targets = await buildTargets(); if (targets.length === 0) { console.error("[ReportAutoScheduler] buildTargets=0,Vault 可能还没加载,3s 后重试"); setTimeout(() => { void syncOnce(); }, 3000); return; } const hostConfigs = toSchedulerHostConfigs(targets); await inspectionSchedulerApi.start(hostConfigs); hostsSyncedRef.current = true; console.error(`[ReportAutoScheduler] ✅ hostConfigs 已同步给主进程,${Object.keys(hostConfigs).length} 台设备`); } catch (e) { console.error("[ReportAutoScheduler] ❌ 同步 hostConfigs 失败:", e?.message); setTimeout(() => { void syncOnce(); }, 3000); } }; // 立即尝试 + 失败每 3 秒重试 void syncOnce(); return () => { hostsSyncedRef.current = false; }; }, []); const tick = useCallback(async () => { if (runningRef.current) return; const cfg = scheduleRef.current; // 临时验证:忽略 enabled 检查 // if (!cfg?.enabled) return; if (cfg.enabledDeviceIds && cfg.enabledDeviceIds.length === 0) return; const intervalMs = 2 * 60 * 1000; // 临时验证:强制 2 分钟 const now = Date.now(); // 临时验证:首次立即执行,不等快照时间 if (lastRunAtRef.current !== 0 && now - lastRunAtRef.current < intervalMs) return; runningRef.current = true; lastRunAtRef.current = now; try { await syncAiProviders(); const targets = await buildTargets(cfg.enabledDeviceIds ?? undefined); if (targets.length === 0) { console.warn("[ReportAutoScheduler] 无有效设备,跳过"); return; } console.error("[ReportAutoScheduler] ✅ tick 触发,设备数:", targets.length, "间隔:", cfg.defaultIntervalHours, "h"); // 先同步 hostConfigs 给主进程(让主进程 reportScheduler 也能跑) if (hostsSyncedRef.current || !hostsSyncedRef.current) { // 每次都同步 try { await inspectionSchedulerApi.start(toSchedulerHostConfigs(targets)); hostsSyncedRef.current = true; console.error("[ReportAutoScheduler] ✅ hostConfigs 已同步"); } catch (e) { console.error("[ReportAutoScheduler] ❌ 同步 hostConfigs 失败:", e?.message); } } await inspectionReportApi.runSnapshot(targets, "scheduled"); console.error("[ReportAutoScheduler] ✅ runSnapshot 完成"); } catch (e) { console.error("[ReportAutoScheduler] ❌ tick 执行失败:", e); } finally { runningRef.current = false; } }, []); /** renderer 侧已不再跑定时 tick —— 主进程 reportScheduler 独立执行定时快照 * 本 Hook 只负责:mount 后把 vault 解密的 SSH 凭据同步给主进程 * 之前 renderer 和主进程双路并行,每次触发产生 2 条快照,已修复。 * tick 函数保留但不被任何定时器调用,runSnapshot 路径已彻底注释。 */ void tick; }