"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"; interface HostConfig { device_id: string; hostname: string; username: string; port?: number; password?: string; privateKeyContent?: string; passphrase?: string; catalog_id?: string; } /** 从 vault 解密 + device extension 构建所有巡检设备 target */ async function buildTargets(enabledDeviceIds?: string[]): Promise { const api = (window as any).netcatty; const hosts = await api?.getHosts?.() ?? []; const keys = await api?.getSSHKeys?.() ?? []; const exts = await inspectionDeviceApi.getAll().catch(() => ({})); const targets: HostConfig[] = []; for (const host of hosts) { if (enabledDeviceIds && enabledDeviceIds.length > 0 && !enabledDeviceIds.includes(host.id)) continue; const ext = exts?.[host.id]; const key = keys.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(() => { const syncOnce = async () => { if (hostsSyncedRef.current) return; try { const targets = await buildTargets(); if (targets.length === 0) { // Vault 还没加载好,稍后再试 setTimeout(() => { void syncOnce(); }, 15000); return; } const hostConfigs = toSchedulerHostConfigs(targets); await inspectionSchedulerApi.start(hostConfigs); hostsSyncedRef.current = true; console.log(`[ReportAutoScheduler] hostConfigs 已同步给主进程,${Object.keys(hostConfigs).length} 台设备`); } catch (e) { console.warn("[ReportAutoScheduler] 同步 hostConfigs 失败:", e?.message); setTimeout(() => { void syncOnce(); }, 15000); } }; // 12 秒后首次同步(等 VaultPublisher 解密 hosts/keys + schedule 加载) const timer = setTimeout(() => { void syncOnce(); }, 12000); return () => clearTimeout(timer); }, []); 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; } }, []); useEffect(() => { // 临时验证:mount 后 10s 先执行一次 tick const initial = setTimeout(() => { void tick(); }, 10000); // 临时验证:每 30 秒 tick 一次 const timer = setInterval(() => { void tick(); }, 30 * 1000); return () => { clearInterval(timer); clearTimeout(initial); }; }, [tick]); }