/** * useMonitoringData - 运行监控大屏数据 hook * * 数据源(全部来自 NetMesh 真实 API): * - hosts: props 传入(vault 里的设备列表) * - inspection.getLatestAll() → 每台设备最新巡检结果(含 metrics:cpu/memory/online) * - inspectionAlertApi.getActive() → 活动告警列表 * - inspectionAlertApi.getStats() → 告警统计 * - inspectionRuleApi.list() → 巡检规则(用于任务状态统计) */ import { useEffect, useState, useCallback, useRef, useMemo } from 'react'; import type { Host } from '../../domain/models'; import { inspectionResultApi, inspectionAlertApi, inspectionAlertRuleApi, inspectionSchedulerApi, inspectionDeviceApi, type AlertRecord, type InspectionResult, type AlertLevel, type AlertStats, } from '../../infrastructure/inspection/inspectionApi'; // ===== 对外类型 ===== export interface MonitoringAlert { id: string; deviceId: string; deviceName: string; level: 'critical' | 'major' | 'warning' | 'minor' | 'info'; message: string; time: string; metric?: string; metricValue?: string; threshold?: string; } export interface TrendPoint { date: string; // MM-DD count: number; } export interface TaskStats { running: number; success: number; failed: number; pending: number; } export interface TaskDevice { deviceId: string; deviceName: string; ip: string; status: 'running' | 'success' | 'failed' | 'pending'; lastScanAt?: number; durationMs?: number; errorMessage?: string; interval?: number; // 巡检间隔(秒) } interface UseMonitoringDataResult { // 指标缓存 cpuMap: Record; memMap: Record; onlineMap: Record; timestampMap: Record; // 每台设备最新巡检时间戳 // 告警 alerts: MonitoringAlert[]; alarmTrend: TrendPoint[]; alertStats: AlertStats | null; // 巡检任务 taskStats: TaskStats; taskDevices: TaskDevice[]; schedulerRunning: boolean; // 告警规则 & 派生阈值 rules: any[]; thresholds: { cpuCritical: number; cpuWarning: number; memCritical: number; memWarning: number; diskCritical: number; diskWarning: number; }; // 操作 refresh: () => Promise; loading: boolean; // 浏览器远程访问环境下实际使用的 hosts(Electron 环境 = props.hosts) effectiveHosts: Host[]; } // ===== localStorage 持久化(降级用,API 不可用时显示缓存) ===== const LS_CPU = 'netmesh:monitoring:cpuMap'; const LS_MEM = 'netmesh:monitoring:memMap'; const LS_ALERTS = 'netmesh:monitoring:alerts'; const LS_TREND = 'netmesh:monitoring:trend'; function readLS(key: string, fallback: T): T { try { const raw = localStorage.getItem(key); if (!raw) return fallback; return JSON.parse(raw) as T; } catch { return fallback; } } function writeLS(key: string, value: unknown) { try { localStorage.setItem(key, JSON.stringify(value)); } catch { /* 忽略 */ } } // 将 NetMesh AlertLevel 映射到监控页面的 5 级 function mapLevel(l: AlertLevel): MonitoringAlert['level'] { if (l === 'critical') return 'critical'; if (l === 'warning') return 'warning'; return 'info'; } // 格式化告警时间:今天 → HH:mm,昨天 → 昨天 HH:mm,更早 → MM-DD HH:mm function formatAlertTime(ts: number): string { const d = new Date(ts); const now = new Date(); const isToday = d.toDateString() === now.toDateString(); const yesterday = new Date(now); yesterday.setDate(yesterday.getDate() - 1); const isYesterday = d.toDateString() === yesterday.toDateString(); const timeStr = d.toLocaleTimeString('zh-CN', { hour12: false, hour: '2-digit', minute: '2-digit' }); if (isToday) return timeStr; if (isYesterday) return `昨天 ${timeStr}`; const ds = `${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; return `${ds} ${timeStr}`; } // 从巡检结果 metrics 里提取 cpu 使用率(数字百分比) function extractCpu(metrics: Record): number { if (!metrics) return 0; // NetMesh 巡检标准字段 if (typeof metrics.cpu_usage === 'number' && !isNaN(metrics.cpu_usage)) return metrics.cpu_usage; if (typeof metrics.cpu_usage === 'string') { const n = parseFloat(metrics.cpu_usage); if (!isNaN(n)) return n; } // 网络设备 if (metrics.cpuUtilization !== undefined) { const v = Number(metrics.cpuUtilization); if (!isNaN(v)) return v; } if (metrics.displayCpu?.cpuUsage !== undefined) { const v = Number(metrics.displayCpu.cpuUsage); if (!isNaN(v)) return v; } // 遍历所有字段找数值型 CPU 字段 for (const key of Object.keys(metrics)) { const kl = key.toLowerCase(); if (kl.includes('cpu') && (kl.includes('usage') || kl.includes('util') || kl.includes('percent'))) { const v = metrics[key]; if (typeof v === 'number' && !isNaN(v)) return v; if (typeof v === 'string') { const n = parseFloat(v); if (!isNaN(n)) return n; } } } return 0; } // 从巡检结果 metrics 里提取内存使用率 function extractMem(metrics: Record): number { if (!metrics) return 0; // NetMesh 巡检标准字段 if (typeof metrics.memory_usage === 'number' && !isNaN(metrics.memory_usage)) return metrics.memory_usage; if (typeof metrics.memory_usage === 'string') { const n = parseFloat(metrics.memory_usage); if (!isNaN(n)) return n; } // 网络设备 if (metrics.displayMemory?.memPercent !== undefined) { const v = Number(metrics.displayMemory.memPercent); if (!isNaN(v)) return v; } if (metrics.memoryPercent !== undefined) { const v = Number(metrics.memoryPercent); if (!isNaN(v)) return v; } if (metrics.mem_percent !== undefined) { const v = Number(metrics.mem_percent); if (!isNaN(v)) return v; } // 遍历所有字段找数值型内存字段 for (const key of Object.keys(metrics)) { const kl = key.toLowerCase(); if ((kl.includes('mem') || kl.includes('memory')) && (kl.includes('usage') || kl.includes('util') || kl.includes('percent'))) { const v = metrics[key]; if (typeof v === 'number' && !isNaN(v)) return v; if (typeof v === 'string') { const n = parseFloat(v); if (!isNaN(n)) return n; } } } // 如果有 mem_used 和 mem_total,计算百分比 const used = metrics.mem_used ?? metrics.memory_used ?? metrics.used_memory; const total = metrics.mem_total ?? metrics.memory_total ?? metrics.total_memory; if (typeof used === 'number' && typeof total === 'number' && total > 0) { return Math.min(100, Math.round((used / total) * 100)); } return 0; } // 生成过去 N 天的空趋势骨架 function buildTrendSkeleton(days = 14): TrendPoint[] { const today = new Date(); const out: TrendPoint[] = []; for (let i = days - 1; i >= 0; i--) { const d = new Date(today); d.setDate(d.getDate() - i); const ds = `${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; out.push({ date: ds, count: 0 }); } return out; } // 把告警数组按天统计到趋势 function buildTrendFromAlerts(allAlerts: AlertRecord[], days = 14): TrendPoint[] { const trend = buildTrendSkeleton(days); const map = new Map(trend.map((t) => [t.date, 0])); const today = new Date(); const earliest = new Date(today); earliest.setDate(earliest.getDate() - days + 1); earliest.setHours(0, 0, 0, 0); for (const a of allAlerts) { const t = new Date(a.triggered_at); if (t < earliest) continue; const ds = `${String(t.getMonth() + 1).padStart(2, '0')}-${String(t.getDate()).padStart(2, '0')}`; if (map.has(ds)) { map.set(ds, (map.get(ds) || 0) + 1); } } return trend.map((t) => ({ ...t, count: map.get(t.date) || 0 })); } // ===== Hook ===== // 工具函数:从 Host + keys 构造 SSH 连接配置(用于调度器执行巡检) function buildSshConfig(host: Host, keys: any[]): Record { const config: Record = { host: host.hostname, port: host.port || 22, username: host.username, label: host.label || host.hostname, }; if (host.password) { config.password = host.password; } // 私钥优先用 identityId 匹配 keys 数组 const keyId = host.identityId || host.identityFileId; if (keyId) { const key = keys.find((k) => k.id === keyId); if (key?.privateKeyContent) { config.privateKey = key.privateKeyContent; if (key.passphrase) config.passphrase = key.passphrase; } } return config; } export function useMonitoringData(hosts: Host[], keys: any[] = []): UseMonitoringDataResult { const [cpuMap, setCpuMap] = useState>(() => readLS(LS_CPU, {})); const [memMap, setMemMap] = useState>(() => readLS(LS_MEM, {})); const [onlineMap, setOnlineMap] = useState>({}); const [timestampMap, setTimestampMap] = useState>({}); const [alerts, setAlerts] = useState(() => { const cached = readLS(LS_ALERTS, []); return cached; }); // 初始 alertStats 也从缓存告警计算,保证和列表初始状态一致 const [alertStats, setAlertStats] = useState(() => { const cached = readLS(LS_ALERTS, []); if (cached.length === 0) return null; let critical = 0, warning = 0, info = 0; for (const a of cached) { if (a.level === 'critical') critical++; else if (a.level === 'warning') warning++; else info++; } return { total: cached.length, active: cached.length, active_critical: critical, active_warning: warning, active_info: info, acknowledged: 0, resolved: 0, closed: 0, }; }); const [alarmTrend, setAlarmTrend] = useState(() => { const cached = readLS(LS_TREND, []); return cached.length > 0 ? cached : buildTrendSkeleton(14); }); const [taskStats, setTaskStats] = useState({ running: 0, success: 0, failed: 0, pending: 0, }); const [taskDevices, setTaskDevices] = useState([]); const [schedulerRunning, setSchedulerRunning] = useState(false); const [rules, setRules] = useState([]); const [loading, setLoading] = useState(false); const loadingRef = useRef(false); // 用 ref 存最新的 hosts/keys,避免闭包陷阱 const hostsRef = useRef(hosts); const keysRef = useRef(keys); hostsRef.current = hosts; keysRef.current = keys; // 浏览器远程访问环境:从 /api/devices 构建虚拟 hosts 列表(Electron vault 不可用) const [remoteHosts, setRemoteHosts] = useState([]); 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() 正在执行中,跳过'); return; } loadingRef.current = true; if (!silent) setLoading(true); try { // ===== 1. 巡检最新结果 → CPU / 内存 / 在线状态 / 成功失败统计 ===== let latestResults: InspectionResult[] = []; try { const results: InspectionResult[] = await inspectionResultApi.getLatestAll(); latestResults = results; const newCpu: Record = {}; const newMem: Record = {}; const newOnline: Record = {}; const newTimestamps: Record = {}; let successCount = 0; let failedCount = 0; for (const r of results) { const cpu = extractCpu(r.metrics); const mem = extractMem(r.metrics); newCpu[r.device_id] = cpu; newMem[r.device_id] = mem; newOnline[r.device_id] = r.online !== false; newTimestamps[r.device_id] = r.timestamp; if (r.status === 'success') successCount++; else failedCount++; // 调试:打印每台设备的 metrics 结构,方便排查字段 console.log(`[monitoring] device ${r.device_id}: cpu=${cpu}, mem=${mem}, metrics keys=`, Object.keys(r.metrics || {})); } console.log(`[monitoring] loaded ${results.length} results: cpuMap=`, Object.keys(newCpu).length, 'memMap=', Object.keys(newMem).length); setCpuMap((prev) => { const merged = { ...prev, ...newCpu }; writeLS(LS_CPU, merged); return merged; }); setMemMap((prev) => { const merged = { ...prev, ...newMem }; writeLS(LS_MEM, merged); return merged; }); setOnlineMap((prev) => ({ ...prev, ...newOnline })); setTimestampMap((prev) => ({ ...prev, ...newTimestamps })); setTaskStats((prev) => ({ ...prev, success: successCount, failed: failedCount })); } catch (e) { console.log('[monitoring] getLatestAll failed, using cached:', e); } // ===== 2. 活动告警(统一数据源:列表、概览、计数都从 active 列表里统计)===== try { const active: AlertRecord[] = await inspectionAlertApi.getActive(); console.log('[monitoring] getActive 返回:', active.length, '条告警'); const mapped: MonitoringAlert[] = active.map((a) => ({ id: a.id, deviceId: a.device_id, deviceName: a.device_name, level: mapLevel(a.level), message: a.message || a.rule_name, time: formatAlertTime(a.triggered_at), metric: a.metric, metricValue: a.metric_value, threshold: a.threshold, })); setAlerts(mapped); writeLS(LS_ALERTS, mapped); // 统一从 active 列表统计告警计数(和列表、概览保持完全一致) let activeCritical = 0; let activeWarning = 0; let activeInfo = 0; for (const a of active) { if (a.level === 'critical') activeCritical++; else if (a.level === 'warning') activeWarning++; else activeInfo++; } setAlertStats({ total: active.length, // 用活动告警总数,不是历史总告警 active: active.length, active_critical: activeCritical, active_warning: activeWarning, active_info: activeInfo, acknowledged: 0, resolved: 0, closed: 0, }); // 趋势:拉最近 200 条历史告警算 14 天趋势 try { const all = await inspectionAlertApi.list({ limit: 200 }); const trend = buildTrendFromAlerts(all, 14); // 浅比较:数据没变化就不 setState,避免触发无意义重渲 setAlarmTrend((prev) => { if (prev.length === trend.length && prev.every((p, i) => p.date === trend[i].date && p.count === trend[i].count)) return prev; return trend; }); writeLS(LS_TREND, trend); } catch { /* 忽略趋势拉取失败 */ } } catch (e) { console.log('[monitoring] alerts failed:', e); } // ===== 3. 调度器状态 + 设备扩展 + 告警规则 → 任务统计 + 任务设备列表 + thresholds ===== try { const [schedulerStatus, enabledDevices, allExtensions, rulesResult] = await Promise.all([ inspectionSchedulerApi.status(), inspectionDeviceApi.getEnabled(), inspectionDeviceApi.getAll(), inspectionAlertRuleApi.list().catch(() => []), ]); setRules(rulesResult || []); setSchedulerRunning(schedulerStatus.running); const running = schedulerStatus.active_scans || 0; // 构建结果 map const resultMap = new Map(latestResults.map((r) => [r.device_id, r])); // 构建所有设备的任务状态列表(过滤掉没有 hostname 的无效设备) const allExtList = Object.values(allExtensions || {}); const taskDeviceList: TaskDevice[] = allExtList .map((ext) => { const host = hostsRef.current.find((h) => h.id === ext.host_id); const result = resultMap.get(ext.host_id); const isEnabled = ext.inspection_enabled; // 没有 hostname 的设备(配置不完整),过滤掉 if (!host?.hostname) return null; let status: TaskDevice['status']; if (isEnabled) { // 启用了定时巡检 if (result) { if (result.status === 'success') status = 'success'; else status = 'failed'; } else { // 启用了但还没巡检过 → 归为成功(表示已配置) status = 'success'; } } else { // 未启用定时巡检 → 待执行 status = 'pending'; } return { deviceId: ext.host_id, deviceName: host?.label || host?.hostname || ext.host_id, ip: host?.hostname || '', status, lastScanAt: result?.timestamp, durationMs: result?.duration_ms, errorMessage: result?.error_message || undefined, interval: ext.inspection_interval || undefined, inspectionEnabled: isEnabled, }; }) .filter((d): d is TaskDevice => d !== null); // 统计:执行中=启用巡检的设备数,成功/失败是其中的子集,待执行=未启用的 const enabledTaskDevices = taskDeviceList.filter((d) => d.inspectionEnabled); const runningCount = enabledTaskDevices.length; const successCount = enabledTaskDevices.filter((d) => d.status === 'success').length; const failedCount = enabledTaskDevices.filter((d) => d.status === 'failed').length; const pendingCount = taskDeviceList.filter((d) => !d.inspectionEnabled).length; console.log('[monitoring] 任务统计:', { running: runningCount, success: successCount, failed: failedCount, pending: pendingCount, total: taskDeviceList.length }); console.log('[monitoring] 启用巡检设备数:', enabledTaskDevices.length, '全部扩展设备数:', allExtList.length, '巡检结果数:', latestResults.length); setTaskDevices(taskDeviceList); setTaskStats({ running: runningCount, success: successCount, failed: failedCount, pending: pendingCount, }); // 自动启动调度器 if (enabledDevices.length === 0) { console.log('[monitoring] 没有启用巡检的设备'); } else { console.log('[monitoring] 条件满足,准备启动调度器'); try { const hostConfigs: Record> = {}; 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 中未找到 host_id:', hostId, ',跳过'); continue; } if (!host.username) { console.log('[monitoring] 设备', host.label || host.hostname || hostId, '缺少 username,跳过'); continue; } 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] 没有可巡检的设备(vault 中匹配不到或凭据不完整)'); } else { console.log('[monitoring] 启动调度器,匹配到', Object.keys(hostConfigs).length, '台设备(running=' + schedulerStatus.running + ')'); // 不管 running 与否都传 hostConfigs 过去让主进程更新, // 主进程 execFn 通过模块级 _hostConfigs 引用动态读最新值 await inspectionSchedulerApi.start(hostConfigs); setSchedulerRunning(true); } } catch (e) { console.log('[monitoring] 自动启动调度器失败:', e); } } } catch (e) { console.log('[monitoring] scheduler status failed:', e); } } finally { loadingRef.current = false; if (!silent) setLoading(false); } }, []); // 初始加载 + 定期刷新(每 30 秒) useEffect(() => { refresh(false); const id = setInterval(() => refresh(true), 30000); return () => clearInterval(id); }, [refresh]); // 事件驱动:告警变化立即刷新(静默刷新,不显示 loading 避免闪动) useEffect(() => { const api = (window as any).netcatty?.inspection; if (!api?.onAlertChanged) return; // 前端侧 500ms debounce let timer: ReturnType | null = null; const handler = () => { if (timer) clearTimeout(timer); timer = setTimeout(() => refresh(true), 500); }; const unsub = api.onAlertChanged(handler); return () => { if (timer) clearTimeout(timer); try { unsub?.(); } catch (_) {} }; }, [refresh]); // 规则变化 → 立刻 refresh(让 thresholds 同步) useEffect(() => { const api = (window as any).netcatty?.inspection; if (!api?.onRuleChanged) return; const handler = () => refresh(true); const unsub = api.onRuleChanged(handler); return () => { try { unsub?.(); } catch (_) {} }; }, [refresh]); // hosts 变化时,把在线状态默认设为 true(没巡检数据前) useEffect(() => { setOnlineMap((prev) => { const next: Record = { ...prev }; let changed = false; for (const h of effectiveHosts) { if (next[h.id] === undefined) { next[h.id] = true; // 假设在线,等巡检结果覆盖 changed = true; } } return changed ? next : prev; }); }, [effectiveHosts]); // 从告警规则派生阈值(和 alertRuleStore 默认规则对齐) const thresholds = useMemo(() => { let cpuCritical = 90, cpuWarning = 80; let memCritical = 85, memWarning = 75; let diskCritical = 90, diskWarning = 80; for (const r of rules) { if (!r.enabled) continue; // 排除 scope_type='catalog' 的设备专属规则(会覆盖全局通用规则) if (r.scope_type && r.scope_type !== 'all') continue; const t = Number(r.threshold); if (Number.isNaN(t)) continue; if (r.metric === 'cpu_usage') { if (r.level === 'critical') cpuCritical = t; else if (r.level === 'warning') cpuWarning = t; } else if (r.metric === 'memory_usage') { if (r.level === 'critical') memCritical = t; else if (r.level === 'warning') memWarning = t; } else if (r.metric === 'disk_usage') { if (r.level === 'critical') diskCritical = t; else if (r.level === 'warning') diskWarning = t; } } return { cpuCritical, cpuWarning, memCritical, memWarning, diskCritical, diskWarning }; }, [rules]); return { cpuMap, memMap, onlineMap, timestampMap, alerts, alarmTrend, alertStats, taskStats, taskDevices, schedulerRunning, rules, thresholds, refresh, loading, effectiveHosts, }; }