"use client"; import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Activity, Calendar, ChevronDown, ChevronRight, Download, FileText, ListChecks, Loader2, Play, RefreshCw, Server, Settings2, Terminal, Trash2, Wifi, WifiOff, } from "lucide-react"; import { Button } from "../ui/button"; import { Card } from "../ui/card"; import { ScrollArea } from "../ui/scroll-area"; import { Switch } from "../ui/switch"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs"; import { toast } from "../ui/toast"; import { inspectionDailyReportApi, inspectionReportApi, inspectionReportScheduleApi, inspectionSnapshotExportApi, type DailyReport, type ReportDeviceTarget, type ReportSchedule, type ReportSnapshot, } from "../../infrastructure/inspection/inspectionApi"; import { summarizeSnapshot } from "../../infrastructure/inspection/reportSummary"; import { STORAGE_KEY_HOSTS, STORAGE_KEY_KEYS, } from "../../infrastructure/config/storageKeys"; import { decryptHosts, decryptKeys, } from "../../infrastructure/persistence/secureFieldAdapter"; // ─── 类型 ────────────────────────────────────── // Host / SSHKey 是 secureFieldAdapter 的真实类型,这里用 any 兜底避免额外 import type HostAny = any; type KeyAny = any; // ─── 工具 ────────────────────────────────────── const CATALOG_LABEL: Record = { linux_server: "Linux 服务器", huawei_h3c_switch: "华为/H3C 交换机", cisco_switch: "Cisco IOS 交换机", huawei_firewall: "华为防火墙", fortinet_firewall: "飞塔防火墙", unknown: "未知设备", }; function formatTime(ts: number) { const d = new Date(ts); const pad = (n: number) => n.toString().padStart(2, "0"); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`; } function formatDuration(ms: number) { if (ms < 1000) return `${ms}ms`; if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`; return `${Math.floor(ms / 60000)}m ${Math.floor((ms % 60000) / 1000)}s`; } /** 从 vault 解密 + device extension 拿到所有设备的 SSH target * enabledDeviceIds 为空/undefined = 全部设备;有值则只返回列表内的 * 返回值第二个元素是完整 host 列表(用于 SchedulePanel 渲染设备多选) * * 远程浏览器模式下:从 HTTP /api/vault/hosts 读 hosts(已脱敏), * 从 /api/devices 读 catalog_id。targets 不带真实凭据(远程跑不了 SSH)。 */ async function buildAllDeviceTargets(enabledDeviceIds?: string[]): Promise<{ targets: ReportDeviceTarget[]; allHosts: Array<{ id: string; hostname: string; catalog_id?: string; label: string }>; }> { const IS_REMOTE = typeof window !== "undefined" && !!(window as any).__REMOTE_TOKEN__; const REMOTE_TOKEN = (window as any).__REMOTE_TOKEN__ || ""; const targets: ReportDeviceTarget[] = []; const allHosts: Array<{ id: string; hostname: string; catalog_id?: string; label: string }> = []; let hosts: any[] = []; let extensions: Record = {}; if (IS_REMOTE) { // ── 远程浏览器:从 HTTP API 读 ── try { const url = `/api/vault/hosts?token=${encodeURIComponent(REMOTE_TOKEN)}`; const res = await fetch(url); const data = await res.json(); hosts = Array.isArray(data?.data) ? data.data : []; } catch (_) {} // catalog_id 从 /api/devices 读 try { const url = `/api/devices?token=${encodeURIComponent(REMOTE_TOKEN)}`; const res = await fetch(url); const data = await res.json(); for (const d of (data?.devices || [])) { if (d?.host_id) extensions[d.host_id] = { catalog_id: d.catalog_id || d.catalog_type }; } } catch (_) {} } else { // ── Electron 本地:从 localStorage 读 ── try { const raw = localStorage.getItem(STORAGE_KEY_HOSTS); if (raw) hosts = await decryptHosts(JSON.parse(raw)); } catch (_) {} try { const raw = localStorage.getItem("netcatty_inspection_device_extension_v1"); if (raw) extensions = JSON.parse(raw); } catch (_) {} } for (const host of hosts) { const ext = extensions[host.id]; const catalogId = ext?.catalog_id || host.catalog_id || undefined; const label = host.label || host.hostname; allHosts.push({ id: host.id, hostname: host.hostname, catalog_id: catalogId, label }); // 过滤:enabledDeviceIds 有值且当前不在列表里 → 跳过 if (enabledDeviceIds && enabledDeviceIds.length > 0 && !enabledDeviceIds.includes(host.id)) continue; targets.push({ device_id: host.id, hostname: host.hostname, port: host.port || 22, username: host.username || "", password: host.password, privateKeyContent: host.privateKeyContent || undefined, passphrase: host.passphrase, catalog_id: catalogId, }); } return { targets, allHosts }; } // ─── 主组件 ──────────────────────────────────── export function ReportPage() { const [snapshots, setSnapshots] = useState([]); const [loading, setLoading] = useState(false); const [running, setRunning] = useState(false); const [expandedSnap, setExpandedSnap] = useState(null); const [expandedDevice, setExpandedDevice] = useState(null); // 日运行报告 const [dailyReports, setDailyReports] = useState([]); const [dailyLoading, setDailyLoading] = useState(false); const [generating, setGenerating] = useState(false); // 调度配置 const [schedule, setSchedule] = useState(null); const [scheduleOpen, setScheduleOpen] = useState(false); // 所有 vault 设备(用于 SchedulePanel 渲染多选列表) const [allHosts, setAllHosts] = useState>([]); // 定时巡检跟踪:记录上次执行时间(手动 + 自动都更新) const lastRunAtRef = useRef(0); // AI 汇总 const [aiSummaries, setAiSummaries] = useState>({}); const [analyzingIds, setAnalyzingIds] = useState>(new Set()); const [aiErrors, setAiErrors] = useState>({}); /** AI 状态计算: * 优先从快照 JSON 读(持久化/主进程写入,页面切换不丢): * ai_summary_status === "running" → analyzing * ai_summary_status === "done" + ai_summary 存在 → done * ai_summary_status === "failed" → failed * 其次本地内存状态(本次页面内手动触发时立刻标记): * analyzingIds.has → analyzing * aiErrors 里有 → failed * 否则 idle */ const getAiState = useCallback((snapId: string) => { const fromSnap = snapshots.find(s => s.id === snapId); const status = fromSnap?.ai_summary_status; if (status === "running") return "analyzing" as const; if (status === "done" && fromSnap.ai_summary) return "done" as const; if (status === "failed") return "failed" as const; // 本地内存状态(兜底) if (analyzingIds.has(snapId)) return "analyzing" as const; if (aiSummaries[snapId]) return "done" as const; if (aiErrors[snapId]) return "failed" as const; return "idle" as const; }, [snapshots, analyzingIds, aiSummaries, aiErrors]); /** 从快照或本地 state 取 AI 汇总内容 */ const getAiSummaryFor = useCallback((snapId: string): any | undefined => { const fromSnap = snapshots.find(s => s.id === snapId)?.ai_summary; if (fromSnap) return fromSnap; return aiSummaries[snapId]; }, [snapshots, aiSummaries]); const loadSnapshots = useCallback(async () => { setLoading(true); try { const data = await inspectionReportApi.listSnapshots(50); setSnapshots(data || []); } catch (e) { console.error("[ReportPage] listSnapshots failed:", e); toast.error("加载快照失败"); } finally { setLoading(false); } }, []); const handleAiAnalyzeSnapshot = useCallback(async (snapId: string) => { const snap = snapshots.find(s => s.id === snapId); if (!snap) return; // 标记本地 analyzing(立即显示 spinner) setAnalyzingIds(prev => new Set(prev).add(snapId)); setAiErrors(prev => { const next = { ...prev }; delete next[snapId]; return next; }); try { // 先同步 providers 到主进程(主进程 AI 执行需要解密的 API key) const api = (window as any).netcatty; try { const providersRaw = localStorage.getItem("netcatty_ai_providers_v1"); if (providersRaw && api?.aiSyncProviders) { const providers = JSON.parse(providersRaw); await api.aiSyncProviders(providers); } } catch (_) {} // 主进程独立执行 —— 页面切换不影响 await inspectionReportApi.runAiSummary(snapId); // 主进程 setImmediate 异步执行,这里已经返回;靠事件/loadSnapshots 刷新状态 // 立即 load 一次,如果主进程刚好跑完则直接显示结果 setTimeout(() => { void loadSnapshots(); }, 800); } catch (e: any) { // IPC 层报错(provider 未配置等) setAiErrors(prev => ({ ...prev, [snapId]: e?.message || String(e) })); setAnalyzingIds(prev => { const s = new Set(prev); s.delete(snapId); return s; }); toast.error("AI 分析失败:" + (e?.message || String(e))); } }, [snapshots, loadSnapshots]); const loadDailyReports = useCallback(async () => { setDailyLoading(true); try { const data = await inspectionDailyReportApi.list(30); setDailyReports(data || []); } catch (e) { console.error("[ReportPage] listDailyReports failed:", e); } finally { setDailyLoading(false); } }, []); const loadSchedule = useCallback(async () => { try { const cfg = await inspectionReportScheduleApi.get(); setSchedule(cfg); } catch (e) { console.error("[ReportPage] getReportSchedule failed:", e); } }, []); useEffect(() => { void loadSnapshots(); void loadDailyReports(); void loadSchedule(); // 加载所有 vault 设备(用于 SchedulePanel 多选) buildAllDeviceTargets().then(r => setAllHosts(r.allHosts)).catch(() => {}); // 查询主进程快照执行状态(页面切换回来时主进程可能还在跑) void inspectionReportApi.isSnapshotRunning().then(r => { if (r?.running) setRunning(true); }).catch(() => {}); }, [loadSnapshots, loadDailyReports, loadSchedule]); // 订阅主进程快照完成广播 —— 页面切换后回来也能收到通知自动刷新 useEffect(() => { const api = (window as any).netcatty; if (!api?.on || typeof api.on !== "function") return; const handler = (_data: any) => { void loadSnapshots(); }; api.on("inspection:reportSnapshotCompleted", handler); return () => { try { api.off?.("inspection:reportSnapshotCompleted", handler); } catch {} }; }, [loadSnapshots]); // 订阅主进程巡检执行状态(running / done)—— 控制"立即巡检"按钮显示 useEffect(() => { const api = (window as any).netcatty; if (!api?.on || typeof api.on !== "function") return; const handler = ({ running }: any) => { setRunning(!!running); if (!running) void loadSnapshots(); }; api.on("inspection:reportSnapshotStatusChanged", handler); return () => { try { api.off?.("inspection:reportSnapshotStatusChanged", handler); } catch {} }; }, [loadSnapshots]); // ─── 定时巡检 ─── // 【已废弃】2026-10-15 移除 —— 定时巡检已迁移到主进程 reportScheduler 独立执行 // 不再依赖用户打开巡检报告页面(此组件 mount),关闭页面也会定时跑 // 之前这里 + useReportAutoScheduler + 主进程三路并行,每次触发产生重复快照 // 手动触发巡检请直接点击页面上的"立即巡检"按钮(handleRunSnapshot) // 订阅主进程 AI 汇总状态广播 —— 主进程独立执行,页面切换也不影响 useEffect(() => { const api = (window as any).netcatty; if (!api?.on || typeof api.on !== "function") return; const handler = ({ snapshotId, status, error }: any) => { // 清除本地 analyzing 标记(主进程状态已进 JSON) setAnalyzingIds(prev => { const s = new Set(prev); s.delete(snapshotId); return s; }); if (status === "failed" && error) { setAiErrors(prev => ({ ...prev, [snapshotId]: error })); } else if (status === "done") { setAiErrors(prev => { const n = { ...prev }; delete n[snapshotId]; return n; }); setAiSummaries(prev => { const n = { ...prev }; delete n[snapshotId]; return n; }); } // 刷新快照列表(从 JSON 读 ai_summary_status + ai_summary) void loadSnapshots(); }; api.on("inspection:reportAiStatusChanged", handler); return () => { try { api.off?.("inspection:reportAiStatusChanged", handler); } catch {} }; }, [loadSnapshots]); // 切页面回来:只刷新快照列表(不自动跑 AI,AI 需用户手动点击触发) useEffect(() => { void loadSnapshots(); }, [loadSnapshots, snapshots.length]); // 24h 统计 const stats24h = useMemo(() => { const cutoff = Date.now() - 24 * 60 * 60 * 1000; const recent = snapshots.filter(s => s.timestamp >= cutoff); let totalCmds = 0, okCmds = 0; for (const snap of recent) { for (const dev of snap.devices) { for (const cmd of dev.commands) { totalCmds++; if (cmd.ok) okCmds++; } } } return { snapCount: recent.length, deviceChecks: recent.reduce((a, s) => a + s.devices.length, 0), totalCmds, okCmds, failCmds: totalCmds - okCmds, failRate: totalCmds > 0 ? ((totalCmds - okCmds) / totalCmds * 100).toFixed(1) : "0", }; }, [snapshots]); const handleRunSnapshot = useCallback(async (trigger: "manual" | "scheduled" = "manual") => { // 记录执行时间(手动 + 定时共用,避免重复触发) lastRunAtRef.current = Date.now(); setRunning(true); try { // 先同步 AI providers 到主进程(快照生成后主进程 setImmediate 自动跑 AI 汇总需要) const api = (window as any).netcatty; try { const providersRaw = localStorage.getItem("netcatty_ai_providers_v1"); if (providersRaw && api?.aiSyncProviders) { await api.aiSyncProviders(JSON.parse(providersRaw)); } } catch (_) {} // 按调度配置过滤设备 // null/undefined = 全部;[] = 一个都不选(直接拦截) const filter = schedule?.enabledDeviceIds; if (filter && filter.length === 0) { toast.error("配置当前为「不巡检任何设备」,请先在 ⚙️ 报告设置中选择设备"); return; } const { targets } = await buildAllDeviceTargets(filter ?? undefined); if (targets.length === 0) { toast.error("没有可选设备:Vault 为空或配置过滤后无匹配"); return; } if (trigger === "manual") { toast.info(`正在巡检 ${targets.length} 台设备${filter ? "(已按配置过滤)" : "(全部)"}...`); } else { console.log(`[ReportPage] 定时巡检触发,目标 ${targets.length} 台${filter ? "(已按配置过滤)" : "(全部)"}`); } const result = await inspectionReportApi.runSnapshot(targets, trigger); if (result.ok && result.snapshot) { toast.success(`快照完成(${formatDuration(result.snapshot.duration_ms)})`); await loadSnapshots(); setExpandedSnap(result.snapshot.id); // AI 汇总需运维手动点击触发(不自动跑,节省 token) } else { toast.error("快照执行失败:" + (result.error || "未知错误")); } } catch (e: any) { console.error("[ReportPage] runSnapshot failed:", e); toast.error("快照失败:" + (e?.message || String(e))); } finally { setRunning(false); } }, [loadSnapshots, schedule]); const handleDelete = useCallback(async (id: string) => { try { await inspectionReportApi.deleteSnapshot(id); toast.success("已删除"); await loadSnapshots(); } catch { toast.error("删除失败"); } }, [loadSnapshots]); const handleClearAll = useCallback(async () => { if (!confirm("确定清空所有快照?此操作不可恢复。")) return; try { await inspectionReportApi.clearSnapshots(); await loadSnapshots(); toast.success("已清空"); } catch { toast.error("清空失败"); } }, [loadSnapshots]); const IS_REMOTE = typeof window !== "undefined" && !!(window as any).__REMOTE_TOKEN__; const handlePreviewSnapshotPdf = useCallback(async (snapshotId: string) => { if (IS_REMOTE) { const token = (window as any).__REMOTE_TOKEN__ || ""; window.open(`/api/report-preview?mode=snapshot&id=${encodeURIComponent(snapshotId)}&token=${encodeURIComponent(token)}`, "_blank"); return; } try { const result = await inspectionReportApi.previewSnapshotPdf(snapshotId); if (!result.ok) toast.error("预览失败:" + (result.error || "未知错误")); } catch (e: any) { toast.error("预览失败:" + (e?.message || String(e))); } }, []); /** 下载 = 让用户选路径(showSaveDialog)+ 存文件(不自动打开) */ const handleDownloadSnapshotPdf = useCallback(async (snapshotId: string) => { try { const snap = snapshots.find(s => s.id === snapshotId); if (!snap) { console.warn("[DownloadPDF] 找不到快照", snapshotId); return; } const api = (window as any).netcatty; const defaultName = `巡检快照_${formatTime(snap.timestamp).replace(/[: ]/g, "_")}.pdf`; console.log("[DownloadPDF] 开始 defaultName=", defaultName, "netcatty=", !!api, "showSaveDialog=", !!api?.showSaveDialog); let customPath: string | undefined; if (api?.showSaveDialog) { const r = await api.showSaveDialog(defaultName, [{ name: "PDF", extensions: ["pdf"] }]); console.log("[DownloadPDF] showSaveDialog 返回=", r); if (!r?.canceled && r?.filePath) customPath = r.filePath; } else { console.warn("[DownloadPDF] api.showSaveDialog 不可用"); } if (!customPath) { console.warn("[DownloadPDF] customPath 为空,取消下载"); return; } console.log("[DownloadPDF] 调用 exportSnapshotPdf snapshotId=", snapshotId, "customPath=", customPath); const res = await inspectionReportApi.exportSnapshotPdf({ snapshotId, aiSummary: snap.ai_summary, customPath, }); console.log("[DownloadPDF] exportSnapshotPdf 返回=", res); if (res.ok && res.filePath) toast.success(`已保存:${res.filePath}`); else toast.error("保存失败:" + (res.error || "未知错误")); } catch (e: any) { console.error("[DownloadPDF] 异常:", e); toast.error("保存失败:" + (e?.message || String(e))); } }, [snapshots]); const handlePreviewDailyPdf = useCallback(async (reportId: string) => { if (IS_REMOTE) { const token = (window as any).__REMOTE_TOKEN__ || ""; window.open(`/api/report-preview?mode=daily&id=${encodeURIComponent(reportId)}&token=${encodeURIComponent(token)}`, "_blank"); return; } try { const result = await inspectionDailyReportApi.previewPdf(reportId); if (!result.ok) toast.error("预览失败:" + (result.error || "未知错误")); } catch (e: any) { toast.error("预览失败:" + (e?.message || String(e))); } }, []); const handleGenerateDailyReport = useCallback(async (mode: "provisional" | "finalized" = "provisional") => { setGenerating(true); try { const result = await inspectionDailyReportApi.generate({ mode, trigger: "manual" }); if (result.ok && result.report) { toast.success(`日报告已生成(${result.report.date})`); await loadDailyReports(); } else { toast.error("生成失败:" + (result.error || "未知错误")); } } catch (e: any) { toast.error("生成失败:" + (e?.message || String(e))); } finally { setGenerating(false); } }, [loadDailyReports]); const handleDeleteDailyReport = useCallback(async (id: string) => { try { await inspectionDailyReportApi.delete(id); await loadDailyReports(); toast.success("已删除"); } catch { toast.error("删除失败"); } }, [loadDailyReports]); const handleSaveSchedule = useCallback(async (updated: Partial) => { try { const saved = await inspectionReportScheduleApi.save(updated); setSchedule(saved); toast.success("配置已保存"); } catch (e) { toast.error("保存失败"); } }, []); return (
{/* Header */}

巡检报告

每小时巡检所有设备,每台 15-20 条命令,记录完整 stdout

{snapshots.length > 0 && ( )}
{/* Stats row */}
} label="24h 快照" value={stats24h.snapCount} /> } label="设备巡检次数" value={stats24h.deviceChecks} /> } label="命令成功" value={`${stats24h.okCmds} / ${stats24h.totalCmds}`} /> } label="失败率" value={`${stats24h.failRate}%`} accent={Number(stats24h.failRate) > 5 ? "red" : "green"} />
{/* 设置面板(可折叠) */} {scheduleOpen && schedule && (
{ await inspectionReportScheduleApi.toggleScheduler(enabled); await loadSchedule(); }} />
)} {/* Tabs */}
巡检快照 日运行报告
{/* Snapshots */} {loading ? (
加载中...
) : snapshots.length === 0 ? ( void handleRunSnapshot()} /> ) : (
{snapshots.map(snap => ( setExpandedSnap(prev => (prev === snap.id ? null : snap.id))} expandedDevice={expandedDevice} onDeviceToggle={id => setExpandedDevice(prev => (prev === id ? null : id))} onDelete={() => void handleDelete(snap.id)} onPreviewPdf={() => void handlePreviewSnapshotPdf(snap.id)} onAiAnalyze={() => void handleAiAnalyzeSnapshot(snap.id)} aiSummary={getAiSummaryFor(snap.id)} aiState={getAiState(snap.id)} aiError={aiErrors[snap.id]} /> ))}
)}
{/* Daily Report */} {/* Daily actions */}

日报告聚合当天所有巡检快照数据,随时可手动生成当日快照汇总

{dailyReports.length === 0 ? ( void handleGenerateDailyReport("provisional")} /> ) : (
{dailyReports.map(report => ( void handleDeleteDailyReport(report.id)} onPreviewPdf={() => void handlePreviewDailyPdf(report.id)} /> ))}
)}
); } export default ReportPage; // ─── 子组件 ──────────────────────────────────── function StatCard({ icon, label, value, accent }: { icon: React.ReactNode; label: string; value: React.ReactNode; accent?: "red" | "green"; }) { return (
{icon} {label}
{value}
); } function EmptyState({ onRun }: { onRun: () => void }) { return (

暂无巡检报告

点击"立即巡检"手动触发一次,或等待调度器自动执行

); } function SnapshotCard({ snap, expanded, onToggle, expandedDevice, onDeviceToggle, onDelete, onPreviewPdf, onAiAnalyze, aiSummary, aiState, aiError, }: { snap: ReportSnapshot; expanded: boolean; onToggle: () => void; expandedDevice: string | null; onDeviceToggle: (id: string) => void; onDelete: () => void; onPreviewPdf: () => void; onAiAnalyze: () => void; aiSummary?: any; aiState: "idle" | "analyzing" | "done" | "failed"; aiError?: string; }) { const okDevices = snap.devices.filter(d => d.ok).length; const totalCmds = snap.devices.reduce((a, d) => a + d.commands.length, 0); const okCmds = snap.devices.reduce((a, d) => a + d.commands.filter(c => c.ok).length, 0); // AI 状态 pill 颜色 const aiPill = { idle: { cls: "bg-muted text-muted-foreground", icon: "·", label: "AI 未分析" }, analyzing: { cls: "bg-purple-500/15 text-purple-600", icon: "", label: "AI 分析中..." }, done: { cls: "bg-green-500/15 text-green-600", icon: "✓", label: "AI 已分析" }, failed: { cls: "bg-red-500/15 text-red-600", icon: "✗", label: "AI 失败" }, }[aiState]; return ( {/* Header:点击展开 */}
{expanded ? : }
{formatTime(snap.timestamp)} {snap.trigger === "scheduled" ? "定时" : "手动"}
{snap.devices.length} 台 {okDevices}/{snap.devices.length} 正常 {okCmds}/{totalCmds} 命令成功 {formatDuration(snap.duration_ms)} {/* AI 状态 pill —— 放在 header 右侧作为整体状态指示 */} {/* done 状态:header 右侧直接给 PDF 预览按钮 —— 不用展开卡片就能点(PDF 预览窗口自带下载功能) */} {aiState === "done" && ( )}
{/* 展开区 */} {expanded && (
{/* ═══ AI 汇总区块(第一优先级,默认展开) ═══ */} {/* ═══ 原始命令详情(可折叠,排在下面) ═══ */}
📋 原始命令详情({snap.devices.length} 台设备)
{snap.devices.map(dev => ( onDeviceToggle(dev.device_id)} /> ))}
)}
); } function aiLabelHint(state: "idle" | "analyzing" | "done" | "failed") { switch (state) { case "idle": return "点击开始 AI 分析"; case "analyzing": return "AI 正在分析快照..."; case "done": return "AI 汇总已完成"; case "failed": return "AI 分析失败,点击重试"; } } /** AI 汇总区块组件 —— 四种状态渲染不同内容 */ function AiSummaryBlock({ aiState, aiSummary, aiError, onAiAnalyze, onPreviewPdf, }: { aiState: "idle" | "analyzing" | "done" | "failed"; aiSummary?: any; aiError?: string; onAiAnalyze: () => void; onPreviewPdf: () => void; }) { // 等待状态 if (aiState === "analyzing") { return (
AI 正在分析巡检数据...

观察温度、CPU、风扇、NTP、端口状态等细节,识别隐藏风险

); } // 未分析(idle) if (aiState === "idle") { return (
🤖还没 AI 分析,点击生成智能汇总
); } // 分析失败 if (aiState === "failed") { return (
✗ AI 分析失败

{aiError || "未知错误"}

); } // 成功(done)—— 完整 AI 汇总 + PDF 按钮 return (
{/* 顶部标题 + PDF 按钮 */}
🤖 AI 智能分析
{/* 健康摘要 */}
{aiSummary?.health_summary}
{/* 离线 */} {aiSummary?.offline_devices?.length > 0 && (
🔴 离线设备({aiSummary.offline_devices.length})
    {aiSummary.offline_devices.map((o: any, i: number) => (
  • {o.device} — {o.reason}
  • ))}
)} {/* 隐藏风险 */} {aiSummary?.hidden_risks?.length > 0 && (
⚠️ 隐藏风险({aiSummary.hidden_risks.length})
    {aiSummary.hidden_risks.map((h: any, i: number) => (
  • {h.device} — {h.observation} {h.severity}
  • ))}
)} {/* 设备运行概览 */} {aiSummary?.device_status?.length > 0 && (
📋 设备运行概览
{aiSummary.device_status.map((d: any, i: number) => (
{d.device} {d.running_ok ? "正常" : "异常"}
{d.highlights?.length > 0 && (
观察:{d.highlights.join(";")}
)} {d.issues?.length > 0 && (
问题:{d.issues.join(";")}
)}
))}
)} {/* 处置建议 */} {aiSummary?.recommendations?.length > 0 && (
💡 处置建议
    {aiSummary.recommendations.map((r: string, i: number) => (
  1. {r}
  2. ))}
)}
); } function DeviceBlock({ dev, expanded, onToggle }: { dev: ReportSnapshot["devices"][number]; expanded: boolean; onToggle: () => void; }) { const okCmds = dev.commands.filter(c => c.ok).length; const failCmds = dev.commands.length - okCmds; return (
{expanded ? : } {dev.ok ? : } {dev.hostname} {CATALOG_LABEL[dev.catalog_id] || dev.catalog_id} {okCmds}/{dev.commands.length} {failCmds > 0 && ({failCmds} 失败)}
{expanded && (
{dev.commands.map((cmd, idx) => (
{cmd.ok ? : } {cmd.cmd} {cmd.purpose} · {formatDuration(cmd.duration_ms)} {(cmd.stdout || cmd.stderr) && (
                    {cmd.stdout || cmd.stderr}
                  
)}
))}
)}
); } // ─── 日运行报告(Phase 1 简化) ───────────────── function DailyReportView({ snapshots }: { snapshots: ReportSnapshot[] }) { if (snapshots.length === 0) { return (

暂无数据

至少跑一次巡检快照后,这里会展示 24 小时汇总统计

); } const cutoff = Date.now() - 24 * 60 * 60 * 1000; const recent = snapshots.filter(s => s.timestamp >= cutoff); const deviceMap = new Map(); for (const snap of recent) { for (const dev of snap.devices) { const cur = deviceMap.get(dev.device_id) || { hostname: dev.hostname, catalog_id: dev.catalog_id, totalCmds: 0, okCmds: 0, failSnapshots: 0, }; cur.totalCmds += dev.commands.length; cur.okCmds += dev.commands.filter(c => c.ok).length; if (!dev.ok) cur.failSnapshots++; deviceMap.set(dev.device_id, cur); } } const devices = Array.from(deviceMap.entries()) .map(([id, d]) => ({ id, ...d })) .sort((a, b) => b.totalCmds - a.totalCmds); const cmdMap = new Map(); for (const snap of recent) { for (const dev of snap.devices) { for (const cmd of dev.commands) { const cur = cmdMap.get(cmd.cmd) || { total: 0, ok: 0 }; cur.total++; if (cmd.ok) cur.ok++; cmdMap.set(cmd.cmd, cur); } } } const cmds = Array.from(cmdMap.entries()) .map(([cmd, v]) => ({ cmd, ...v, failRate: v.total - v.ok })) .sort((a, b) => b.failRate - a.failRate); const sumCmds = Array.from(deviceMap.values()).reduce((a, d) => a + d.totalCmds, 0); const sumOk = Array.from(deviceMap.values()).reduce((a, d) => a + d.okCmds, 0); return (
{/* 概览 */}

过去 24 小时汇总

0 ? ((sumOk / sumCmds) * 100).toFixed(1) : "0"}%`} />
{/* 设备列表 */}

设备巡检统计

{devices.map(d => (
{d.failSnapshots > 0 ? : } {d.hostname} {CATALOG_LABEL[d.catalog_id] || d.catalog_id} 成功率 {d.totalCmds > 0 ? ((d.okCmds / d.totalCmds) * 100).toFixed(1) : "0"}% ({d.okCmds}/{d.totalCmds}) {d.failSnapshots > 0 && ( {d.failSnapshots} 次失败 )}
))}
{/* 失败命令 Top */} {cmds.filter(c => c.failRate > 0).length > 0 && (

命令失败 Top

{cmds.filter(c => c.failRate > 0).slice(0, 10).map(c => (
{c.cmd} {c.ok}/{c.total} 成功 {((c.total - c.ok) / c.total * 100).toFixed(0)}% 失败
))}
)}
); } function MetricItem({ label, value }: { label: string; value: React.ReactNode }) { return (
{label}
{value}
); } // ─── 日运行报告(消费 DailyReport[] 类型) ────────── function EmptyDailyReport({ onGenerate }: { onGenerate: () => void }) { return (

暂无日运行报告

至少跑一次巡检快照后,点击"生成今日报告"即可出当日汇总

); } function DailyReportCard({ report, onDelete, onPreviewPdf }: { report: DailyReport; onDelete: () => void; onPreviewPdf: () => void; }) { const [expanded, setExpanded] = useState(false); const s = report.summary; const isFinalized = report.mode === "finalized"; return (
setExpanded(v => !v)}> {expanded ? : }
{report.date} {isFinalized ? "最终版" : "当前快照"} {report.trigger === "scheduled" ? "定时" : "手动"}
{s.snapshotCount} 次快照 · {s.deviceCount} 台设备 · {s.totalCmds} 条命令
成功率 {s.totalCmds > 0 ? `${s.okCmds}/${s.totalCmds}` : "0"} 5 ? "text-red-500" : "text-green-500"}> 失败 {s.failRate}%
{expanded && (
{/* 设备成功率 */}

设备成功率

{Object.entries(s.deviceSuccessRate).map(([devId, rate]) => (
{rate >= 0.98 ? : rate >= 0.8 ? : } {devId.slice(0, 12)} {(rate * 100).toFixed(0)}%
))}
{/* Top 失败命令 */} {s.topFailedCommands.length > 0 && (

失败命令 Top {s.topFailedCommands.length > 10 ? `10 / ${s.topFailedCommands.length}` : s.topFailedCommands.length}

{s.topFailedCommands.slice(0, 10).map((f, idx) => (
#{idx + 1} {f.cmd} {f.device_id} × {f.count}
))}
)}
)}
); } // ─── 调度设置面板 ─────────────────────────────────── function SchedulePanel({ schedule, allHosts, onSave, onToggleScheduler }: { schedule: ReportSchedule; allHosts: Array<{ id: string; hostname: string; catalog_id?: string; label: string }>; onSave: (updated: Partial) => void; onToggleScheduler: (enabled: boolean) => void; }) { const [local, setLocal] = useState(schedule); const [saving, setSaving] = useState(false); // dirty 保护:用户在面板里编辑过 → 不要被外部 props.schedule 变化静默覆盖 // (比如主进程广播 schedule 变更、其他组件触发 loadSchedule 等) const dirtyRef = useRef(false); useEffect(() => { if (!dirtyRef.current) setLocal(schedule); }, [schedule]); const handleSave = async () => { setSaving(true); await onSave(local); dirtyRef.current = false; setSaving(false); }; /** 编辑点统一入口:改 local + 标记 dirty,防止外部 props 覆盖未保存的本地编辑 */ const edit = useCallback((updater: ReportSchedule | ((p: ReportSchedule) => ReportSchedule)) => { dirtyRef.current = true; setLocal(updater); }, []); // 设备多选 helpers // enabledDeviceIds 约定:null/undefined = 全部,[] = 一个都不选,[id1,id2] = 指定 const selectedAll = !local.enabledDeviceIds; // null / undefined → 全部 const toggleAllDevices = (selectAll: boolean) => { edit(p => ({ ...p, enabledDeviceIds: selectAll ? null : [] })); }; const toggleDevice = (deviceId: string) => { edit(p => { const cur = p.enabledDeviceIds; if (!cur) { // 当前是"全部" —— 取消这一台变成"除了这台" return { ...p, enabledDeviceIds: allHosts.filter(h => h.id !== deviceId).map(h => h.id) }; } const included = cur.includes(deviceId); const next = included ? cur.filter(id => id !== deviceId) : [...cur, deviceId]; return { ...p, enabledDeviceIds: next }; }); }; const CATALOG_LABEL: Record = { linux_server: "Linux", huawei_h3c_switch: "华为/H3C", cisco_switch: "Cisco", huawei_firewall: "华为FW", fortinet_firewall: "飞塔FW", windows_server: "Windows", }; const enabledCount = selectedAll ? allHosts.length : (local.enabledDeviceIds?.length || 0); return (

巡检报告调度配置

{/* 全局开关 */}
自动调度
按周期自动巡检
{ edit(p => ({ ...p, enabled: v })); // onToggleScheduler 直接写主进程,成功后清 dirty(async) Promise.resolve(onToggleScheduler(v)).then(() => { dirtyRef.current = false; }); }} />
{/* 巡检周期 */}
默认巡检周期
edit(p => ({ ...p, defaultIntervalHours: Number(e.target.value) || 1 }))} /> 小时
{/* 日报告时间 */}
日报告触发时间
edit(p => ({ ...p, dailyReportTime: e.target.value }))} />
{/* AI 汇总 */}
AI 汇总分析
手动触发快照时生成 AI 四段式汇总
edit(p => ({ ...p, aiSummaryEnabled: v }))} />
{/* 设备选择 */}
巡检设备选择 已选 {enabledCount}/{allHosts.length} 台{selectedAll ? "(空 = 全部)" : ""}
{allHosts.length === 0 ? (
Vault 里还没设备,请先去 Vault 添加主机
) : allHosts.map(h => { const checked = selectedAll ? true : (local.enabledDeviceIds?.includes(h.id) ?? false); return ( ); })}
); }