import { AlertTriangle, CheckCircle, XCircle, Clock, MessageSquare, RefreshCw, Filter, Info, AlertOctagon, Bell, ChevronRight, Bot, Loader2, Sparkles, Terminal, Lightbulb, Zap, } from "lucide-react"; import React, { useEffect, useState, useCallback, useRef } from "react"; import { Button } from "../ui/button"; import { Badge } from "../ui/badge"; import { Input } from "../ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "../ui/select"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, } from "../ui/dialog"; import { Label } from "../ui/label"; import { Textarea } from "../ui/textarea"; import { inspectionAlertApi, type AlertRecord, type AlertLevel, type AlertRecordStatus, type AlertStats, } from "../../infrastructure/inspection/inspectionApi"; import { toast } from "../ui/toast"; import { runRCA, type RunRCAResult } from "../../infrastructure/ai/rcaAgent/rcaRunner"; const LEVEL_COLORS: Record = { critical: { bg: "bg-red-500/10", text: "text-red-500", border: "border-red-500/30", dot: "bg-red-500", }, warning: { bg: "bg-orange-500/10", text: "text-orange-500", border: "border-orange-500/30", dot: "bg-orange-500", }, info: { bg: "bg-blue-500/10", text: "text-blue-500", border: "border-blue-500/30", dot: "bg-blue-500", }, }; const LEVEL_LABELS: Record = { critical: "严重", warning: "警告", info: "提示", }; const STATUS_LABELS: Record = { active: "待处理", acknowledged: "已确认", resolved: "已恢复", closed: "已关闭", }; const STATUS_COLORS: Record = { active: "text-red-500", acknowledged: "text-orange-500", resolved: "text-green-500", closed: "text-muted-foreground", }; export function AlertCenterPage() { const [alerts, setAlerts] = useState([]); const [stats, setStats] = useState(null); const [loading, setLoading] = useState(true); const [selectedAlert, setSelectedAlert] = useState(null); const [detailOpen, setDetailOpen] = useState(false); const [noteDialogOpen, setNoteDialogOpen] = useState(false); const [noteText, setNoteText] = useState(""); // 防御:清洗告警里可能残留的 [object Object](老数据修复前写入的坏值) const _clean = (v: any): string => { if (v === null || v === undefined) return ""; const s = typeof v === "string" ? v : String(v); // 已经包含 [object Object] 片段 → 尝试 JSON.parse 救回来,失败就显示 "—" if (s.includes("[object Object]")) { // 可能是 JSON.parse 后还能救 try { const parsed = JSON.parse(s.replace(/\[object Object\]/g, '"..."')); return JSON.stringify(parsed).slice(0, 300); } catch { return s.replace(/\[object Object\]/g, "—").replace(/, —/g, " —"); } } return s; }; // RCA 状态 const [rcaLoading, setRcaLoading] = useState(false); const [rcaResult, setRcaResult] = useState(null); // 筛选 const [filterLevel, setFilterLevel] = useState("all"); const [filterStatus, setFilterStatus] = useState("all"); const [searchText, setSearchText] = useState(""); const [activeTab, setActiveTab] = useState<"active" | "all">("active"); const isFirstLoadRef = useRef(true); const loadData = useCallback(async (silent = false) => { if (!silent) setLoading(true); try { const [statsData, alertsData] = await Promise.all([ inspectionAlertApi.getStats(), activeTab === "active" ? inspectionAlertApi.getActive() : inspectionAlertApi.list({ limit: 2000 }), ]); setStats(statsData); setAlerts(alertsData); } catch (e: any) { console.error("[AlertCenter] 加载失败:", e); } finally { if (!silent) setLoading(false); isFirstLoadRef.current = false; } }, [activeTab]); useEffect(() => { loadData(false); }, [loadData]); // 订阅主进程告警变化事件,自动刷新(静默刷新,不显示 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(() => loadData(true), 500); }; const unsub = api.onAlertChanged(handler); return () => { if (timer) clearTimeout(timer); try { unsub?.(); } catch (_) {} }; }, [loadData]); // 筛选后的数据 const filteredAlerts = (alerts ?? []).filter((a) => { if (filterLevel !== "all" && a.level !== filterLevel) return false; if (filterStatus !== "all" && a.status !== filterStatus) return false; if (searchText) { const s = searchText.toLowerCase(); if ( !a.device_name.toLowerCase().includes(s) && !a.rule_name.toLowerCase().includes(s) && !a.message.toLowerCase().includes(s) ) return false; } return true; }); const handleViewDetail = (alert: AlertRecord) => { setSelectedAlert(alert); // 自动恢复:如果告警 note 里有 [RCA_JSON] 标记,解析并填充 rcaResult if (alert.note?.startsWith("[RCA_JSON]")) { try { const parsed = JSON.parse(alert.note.slice("[RCA_JSON]".length)); if (parsed.__rca_json && parsed.summary) { setRcaResult({ success: true, rca: { summary: parsed.summary, confidence: parsed.confidence ?? "medium", rootCauses: parsed.rootCauses || [], evidence: parsed.evidence || [], recommendations: parsed.recommendations || [], }, error: undefined, } as any); } else { setRcaResult(null); } } catch { setRcaResult(null); } } else { setRcaResult(null); } setDetailOpen(true); }; /** 触发 AI 根因分析(RCA)— 同步阻塞直到结果返回,完成后持久化到告警记录 note 字段 */ const triggerRCA = async (alert: AlertRecord) => { setRcaLoading(true); setRcaResult(null); try { toast.info("AI 正在分析故障根因...(最多 3 分钟)"); const result = await runRCA(alert); setRcaResult(result); if (result.success && result.rca) { toast.success("AI 分析完成"); // 持久化 RCA 结果到告警记录 note 字段,加 [RCA_JSON] 前缀标记 try { const rcaPayload = JSON.stringify({ __rca_json: true, generated_at: Date.now(), summary: result.rca.summary, confidence: result.rca.confidence, rootCauses: result.rca.rootCauses, evidence: result.rca.evidence, recommendations: result.rca.recommendations, }); await inspectionAlertApi.updateNote(alert.id, `[RCA_JSON]${rcaPayload}`); // 同时更新本地 selectedAlert 的 note,这样详情弹窗里也能看到 setSelectedAlert(prev => prev ? { ...prev, note: `[RCA_JSON]${rcaPayload}` } : prev); } catch (persistErr) { console.warn("RCA 结果持久化失败:", persistErr); // 持久化失败不影响 UI 展示 } } else { toast.error("AI 分析失败:" + (result.error || "输出解析失败")); } } catch (e: any) { toast.error("AI 分析异常:" + (e?.message || String(e))); } finally { setRcaLoading(false); } }; const handleAcknowledge = async () => { if (!selectedAlert) return; try { await inspectionAlertApi.acknowledge(selectedAlert.id, "operator"); toast.success("已确认告警"); setDetailOpen(false); loadData(); } catch (e: any) { toast.error("操作失败"); } }; const handleResolve = async () => { if (!selectedAlert) return; try { await inspectionAlertApi.resolve(selectedAlert.id, "manual", "operator"); toast.success("已恢复告警"); setDetailOpen(false); loadData(); } catch (e: any) { toast.error("操作失败"); } }; const handleClose = () => { if (!selectedAlert) return; setNoteText(""); setNoteDialogOpen(true); }; const handleCloseConfirm = async () => { if (!selectedAlert) return; try { await inspectionAlertApi.close(selectedAlert.id, noteText); toast.success("已关闭告警"); setNoteDialogOpen(false); setDetailOpen(false); loadData(); } catch (e: any) { toast.error("操作失败"); } }; const formatTime = (ts: number) => { return new Date(ts).toLocaleString(); }; const getTimeAgo = (ts: number) => { const diff = Date.now() - ts; const mins = Math.floor(diff / 60000); if (mins < 1) return "刚刚"; if (mins < 60) return `${mins} 分钟前`; const hours = Math.floor(mins / 60); if (hours < 24) return `${hours} 小时前`; const days = Math.floor(hours / 24); return `${days} 天前`; }; return (
{/* 顶部统计 */}

告警中心

监控设备巡检产生的告警

{/* 统计卡片 */}
严重告警
{stats?.active_critical ?? 0}
警告
{stats?.active_warning ?? 0}
提示
{stats?.active_info ?? 0}
已恢复
{stats?.resolved ?? 0}
{/* 筛选栏 */}
{activeTab === "all" && ( )} setSearchText(e.target.value)} className="w-48" />
{/* 告警列表 */}
{loading ? (
加载中...
) : filteredAlerts.length === 0 ? (

暂无告警记录

{activeTab === "active" ? "所有设备运行正常" : "还没有产生过告警"}

) : (
{filteredAlerts.map((alert) => { const colors = LEVEL_COLORS[alert.level]; return (
handleViewDetail(alert)} className="flex items-center gap-4 px-6 py-3 hover:bg-accent/30 cursor-pointer transition-colors" > {/* 级别指示点 */}
{/* 主要内容 */}
{alert.device_name} {LEVEL_LABELS[alert.level]} {STATUS_LABELS[alert.status]}
{alert.rule_name} — {alert.message}
{/* 时间 */}
{getTimeAgo(alert.triggered_at)}
{formatTime(alert.triggered_at)}
{/* 快速 AI 根因分析(阻止冒泡避免触发详情 Dialog) */} {(() => { const hasRca = alert.note?.startsWith("[RCA_JSON]"); if (hasRca) { return ( { e.stopPropagation(); handleViewDetail(alert); }} > 已分析 ); } return ( ); })()}
); })}
)}
{/* 详情弹窗 */} {selectedAlert && ( <>
告警详情
设备名称
{selectedAlert.device_name}
告警级别
{LEVEL_LABELS[selectedAlert.level]}
规则名称
{selectedAlert.rule_name}
告警消息
{_clean(selectedAlert.message)}
当前值
{_clean(selectedAlert.metric_value)}
阈值
{_clean(selectedAlert.threshold)}
触发时间
{formatTime(selectedAlert.triggered_at)}
当前状态
{STATUS_LABELS[selectedAlert.status]}
{/* 备注(过滤掉 [RCA_JSON] 结构化数据段,只展示人工备注) */} {(() => { const rawNote = selectedAlert.note || ""; const humanNote = rawNote .replace(/\[RCA_JSON\][\s\S]*?(?=\n\[|\n---|\n$|$)/g, "") .replace(/\[RCA_JSON\][\s\S]*$/g, "") .trim(); if (!humanNote) return null; return (
备注
{humanNote}
); })()} {/* AI 根因分析结果卡片 */} {rcaLoading && (
AI 正在分析故障根因...
主动连接设备执行诊断命令,可能需要 1-3 分钟
)} {rcaResult && !rcaLoading && (
{/* Header */}
AI 根因分析结果 {rcaResult.source === "ai" ? "AI 驱动" : "规则兜底"}
{rcaResult.rca && ( = 80 ? "border-green-500/50 bg-green-500/10 text-green-400" : rcaResult.rca.confidence >= 50 ? "border-yellow-500/50 bg-yellow-500/10 text-yellow-400" : "border-red-500/50 bg-red-500/10 text-red-400" } > 置信度 {rcaResult.rca.confidence}% )}
{rcaResult.success && rcaResult.rca ? ( <> {/* Summary card */}
{rcaResult.rca.summary}
{/* Root causes */}
根因分析
    {rcaResult.rca.rootCauses.map((c, i) => (
  • {i + 1}. {c}
  • ))}
{/* Evidence — 成功/失败区分 */}
关键证据 {(rcaResult.rca.evidence ?? []).filter((e: any) => !e.summary.includes("失败")).length} 成功 · {(rcaResult.rca.evidence ?? []).filter((e: any) => e.summary.includes("失败")).length} 失败
{(rcaResult.rca.evidence ?? []).map((e: any, i: number) => { const isFailed = e.summary.includes("失败") || e.summary.includes("error") || e.summary.includes("错误"); const raw = e.keyFindings?.[0] || ""; const shortOutput = raw.length > 200 ? raw.slice(0, 200) + "…" : raw; return (
{e.tool} {e.summary} {isFailed && ( 执行失败 )} {raw && (
{shortOutput}
                                    
)}
); })} {rcaResult.rca.evidence.length === 0 && (
无命令证据(可能设备离线或诊断阶段被跳过)
)}
{/* Recommendations */}
处置建议
{rcaResult.rca.recommendations.map((r, i) => (
{r.priority === "immediate" ? "🔥 立即执行" : r.priority === "short_term" ? "⚡ 短期建议" : "📋 长期优化"} 风险 {r.risk === "low" ? "低" : r.risk === "medium" ? "中" : "高"}
{r.action}
{r.command && (
{r.command}
)} {r.note && (
💡 {r.note}
)}
))}
) : (
AI 分析失败: {rcaResult.error || "未知错误"}
)} {/* Footer stats */}
耗时 {(rcaResult.durationMs / 1000).toFixed(1)}s {(rcaResult.toolInvocations ?? []).filter((i: any) => i.ok).length} / {(rcaResult.toolInvocations ?? []).length} 工具调用成功
)}
{selectedAlert && ( )}
{selectedAlert.status === "active" && ( )} {(selectedAlert.status === "active" || selectedAlert.status === "acknowledged") && ( )} {selectedAlert.status !== "closed" && ( )}
)}
{/* 关闭备注弹窗 */} 关闭告警