Files
SenMeshNet/components/monitoring/AlertAIPanel.tsx
zhaolei a5744e720b
Some checks failed
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
feat(plan-management): implement alert-remediation-plan system v1.3
- 8 new store modules + matcher + initialResponse + aggregation + remediation + AlertStateMonitor + planGenerator

- alert chain auto-integrated after runInspection: freeze snapshot + generate response + real-time agg + auto-start monitor

- AlertAIPanel component + MonitoringDetailPanel expandable alert list

- PlanManagementPage: full page with list/edit/AI generate/templates/commands
2026-10-30 16:55:55 +08:00

380 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React from 'react';
import { Zap, AlertCircle, CheckCircle2, AlertTriangle, Clock, Bot, RefreshCw, ChevronDown, ChevronUp, Wrench, ShieldAlert, Wifi, Activity, Info } from 'lucide-react';
import type { InitialResponse, DevicePlan, RemediationRun } from '../../infrastructure/inspection/inspectionApi';
// ====================================================================
// 告警 AI 面板(响应包 + 预案 + 处置)
// ====================================================================
interface Props {
alertId: string;
matchedPlan?: DevicePlan | null;
initialResponse?: InitialResponse | null;
remediation?: RemediationRun | null;
onRegenerateResponse?: () => void;
onStartRemediation?: () => void;
onPauseRemediation?: () => void;
onResumeRemediation?: () => void;
onCancelRemediation?: () => void;
isRemediationRunning?: boolean;
}
export default function AlertAIPanel({
alertId,
matchedPlan,
initialResponse,
remediation,
onRegenerateResponse,
onStartRemediation,
onPauseRemediation,
onResumeRemediation,
onCancelRemediation,
}: Props) {
return (
<div className="mt-2 space-y-2">
{/* 预案匹配 */}
{matchedPlan && (
<PlanMatchedBadge plan={matchedPlan} />
)}
{/* AI 初始响应包 */}
{initialResponse ? (
<ResponsePanel
response={initialResponse}
onRegenerate={onRegenerateResponse}
/>
) : matchedPlan ? (
<div className="p-2 rounded border border-dashed border-slate-600/50 text-xs text-muted-foreground flex items-center gap-1.5"
style={{ background: 'rgba(15,23,42,0.4)' }}>
<Clock className="w-3 h-3 animate-pulse" />
AI 正在生成初始响应包…
</div>
) : null}
{/* 处置执行区 */}
{matchedPlan?.remediation?.steps?.length ? (
<RemediationPanel
alertId={alertId}
remediation={remediation}
planSteps={matchedPlan.remediation.steps}
onStart={onStartRemediation}
onPause={onPauseRemediation}
onResume={onResumeRemediation}
onCancel={onCancelRemediation}
/>
) : null}
</div>
);
}
// --------------------------------------------------------------------
// 预案匹配徽章
// --------------------------------------------------------------------
function PlanMatchedBadge({ plan }: { plan: DevicePlan }) {
const levelColors: Record<string, string> = {
critical: 'bg-red-500/15 text-red-400 border-red-500/30',
warning: 'bg-amber-500/15 text-amber-400 border-amber-500/30',
info: 'bg-sky-500/15 text-sky-400 border-sky-500/30',
};
return (
<div className={`p-2 rounded border ${levelColors[plan.level] || levelColors.info} flex items-center gap-2 text-xs`}
style={{ background: 'rgba(15,23,42,0.5)' }}>
<Wrench className="w-3.5 h-3.5 flex-shrink-0" />
<div className="flex-1 min-w-0">
<div className="font-medium truncate">
匹配预案:{plan.description || plan.id}
</div>
{plan.escalation_fallback?.auto_escalate_minutes ? (
<div className="text-[11px] opacity-75">
挂起 {plan.escalation_fallback.auto_escalate_minutes} 分钟自动升级
</div>
) : null}
</div>
</div>
);
}
// --------------------------------------------------------------------
// AI 初始响应包面板(可折叠)
// --------------------------------------------------------------------
function ResponsePanel({
response,
onRegenerate,
}: {
response: InitialResponse;
onRegenerate?: () => void;
}) {
const [expanded, setExpanded] = React.useState(true);
const [causesExpanded, setCausesExpanded] = React.useState(true);
const statusColor = response.status?.includes('fallback') ? 'text-amber-400'
: response.status?.includes('partial') ? 'text-amber-400'
: 'text-emerald-400';
return (
<div className="rounded border border-cyan-500/20 overflow-hidden"
style={{ background: 'rgba(15,23,42,0.5)' }}>
{/* Header */}
<div
className="px-2.5 py-1.5 flex items-center gap-2 cursor-pointer hover:bg-cyan-500/5"
onClick={() => setExpanded(!expanded)}
>
<Bot className="w-3.5 h-3.5 text-cyan-400 flex-shrink-0" />
<span className="text-xs font-medium text-cyan-400">AI 初始响应包</span>
<span className={`text-[10px] ${statusColor} flex-1 text-right`}>
{response.generator === 'fallback' ? '降级' : 'AI'} · {response.status?.replace(/_/g, ' ')}
</span>
{onRegenerate && (
<button
onClick={(e) => { e.stopPropagation(); onRegenerate(); }}
className="p-0.5 hover:bg-slate-600/50 rounded"
title="重新生成"
>
<RefreshCw className="w-3 h-3 text-muted-foreground" />
</button>
)}
{expanded ? <ChevronUp className="w-3 h-3 text-muted-foreground" /> : <ChevronDown className="w-3 h-3 text-muted-foreground" />}
</div>
{expanded && (
<div className="px-2.5 py-2 border-t border-slate-600/30 space-y-2.5">
{/* 快速排查清单 */}
{response.quick_checklist?.length > 0 && (
<div>
<div className="text-[11px] text-muted-foreground mb-1 flex items-center gap-1">
<CheckCircle2 className="w-3 h-3 text-emerald-400" /> 快速排查清单
</div>
<div className="space-y-1">
{response.quick_checklist.map((item, i) => (
<div key={i} className="flex items-start gap-1.5 text-xs">
<span className="w-4 h-4 flex items-center justify-center text-[10px] bg-cyan-500/15 text-cyan-400 rounded flex-shrink-0 mt-0.5">
{item.priority || i + 1}
</span>
<div className="flex-1">
<div className="text-foreground">{item.action}</div>
{item.purpose && <div className="text-[11px] text-muted-foreground">{item.purpose}</div>}
</div>
</div>
))}
</div>
</div>
)}
{/* 可能原因(仅 AI 生成有) */}
{response.possible_causes?.length > 0 && (
<div>
<div
className="text-[11px] text-muted-foreground mb-1 flex items-center gap-1 cursor-pointer hover:text-foreground"
onClick={() => setCausesExpanded(!causesExpanded)}
>
<AlertTriangle className="w-3 h-3 text-amber-400" /> 可能原因
{causesExpanded ? <ChevronUp className="w-2.5 h-2.5" /> : <ChevronDown className="w-2.5 h-2.5" />}
</div>
{causesExpanded && (
<div className="space-y-1">
{response.possible_causes.map((cause, i) => (
<div key={i} className="flex items-start gap-1.5 text-xs">
<ProbabilityBadge p={cause.probability} />
<div className="flex-1">
<span className="text-foreground">{cause.label}</span>
{cause.hint && <span className="text-muted-foreground ml-1">· {cause.hint}</span>}
</div>
</div>
))}
</div>
)}
</div>
)}
{/* 通知状态 */}
{(response.auto_notifications_sent?.length > 0 || response.pending_notifications?.length > 0) && (
<div className="pt-1 border-t border-slate-600/20">
<div className="text-[11px] text-muted-foreground mb-1 flex items-center gap-1">
<Wifi className="w-3 h-3" /> 通知状态
</div>
<div className="flex flex-wrap gap-1">
{response.auto_notifications_sent.map((n, i) => (
<span key={i} className="text-[10px] px-1.5 py-0.5 rounded bg-emerald-500/15 text-emerald-400">
{n.channel} ✓
</span>
))}
{response.pending_notifications.map((n: any, i: number) => (
<span key={`p-${i}`} className="text-[10px] px-1.5 py-0.5 rounded bg-amber-500/15 text-amber-400">
{n.channel || 'pending'} · 待外部
</span>
))}
</div>
</div>
)}
{/* Warnings */}
{response.warnings?.length > 0 && (
<div className="pt-1 border-t border-slate-600/20">
{response.warnings.map((w, i) => (
<div key={i} className="text-[11px] text-amber-400 flex items-start gap-1 mt-0.5">
<AlertCircle className="w-3 h-3 flex-shrink-0 mt-0.5" /> {w}
</div>
))}
</div>
)}
{/* 生成时间 */}
<div className="text-[10px] text-muted-foreground text-right">
{new Date(response.generated_at).toLocaleString()}
</div>
</div>
)}
</div>
);
}
function ProbabilityBadge({ p }: { p: string }) {
const map: Record<string, { cls: string; label: string }> = {
high: { cls: 'bg-red-500/20 text-red-400 border-red-500/40', label: '高' },
medium: { cls: 'bg-amber-500/20 text-amber-400 border-amber-500/40', label: '中' },
low: { cls: 'bg-slate-500/20 text-slate-400 border-slate-500/40', label: '低' },
};
const cfg = map[p] || map.low;
return (
<span className={`text-[9px] px-1 rounded border ${cfg.cls} flex-shrink-0 mt-0.5`}>{cfg.label}</span>
);
}
// --------------------------------------------------------------------
// 处置执行面板
// --------------------------------------------------------------------
function RemediationPanel({
alertId: _alertId,
remediation,
planSteps,
onStart,
onPause,
onResume,
onCancel,
}: {
alertId: string;
remediation?: RemediationRun | null;
planSteps: any[];
onStart?: () => void;
onPause?: () => void;
onResume?: () => void;
onCancel?: () => void;
}) {
const statusMap: Record<string, { cls: string; label: string; icon: React.ReactNode }> = {
pending: { cls: 'text-slate-400', label: '待处置', icon: <Clock className="w-3 h-3" /> },
running: { cls: 'text-sky-400', label: '执行中', icon: <Loader2 className="w-3 h-3 animate-spin" /> },
completed: { cls: 'text-emerald-400', label: '完成', icon: <CheckCircle2 className="w-3 h-3" /> },
partial: { cls: 'text-amber-400', label: '部分完成', icon: <AlertTriangle className="w-3 h-3" /> },
failed: { cls: 'text-red-400', label: '失败', icon: <XCircle className="w-3 h-3" /> },
cancelled: { cls: 'text-slate-400', label: '已取消', icon: <XCircle className="w-3 h-3" /> },
paused: { cls: 'text-amber-400', label: '已暂停', icon: <Pause className="w-3 h-3" /> },
};
const runStatus = remediation?.status || 'pending';
const cfg = statusMap[runStatus] || statusMap.pending;
const steps = remediation?.steps?.length ? remediation.steps : planSteps.map((s: any, i: number) => ({
order: s.order || i + 1,
command_id: s.command_id,
execution: s.execution,
status: 'pending',
}));
return (
<div className="rounded border border-violet-500/20 overflow-hidden"
style={{ background: 'rgba(15,23,42,0.5)' }}>
{/* Header */}
<div className="px-2.5 py-1.5 flex items-center gap-2 border-b border-slate-600/30">
<Wrench className="w-3.5 h-3.5 text-violet-400" />
<span className="text-xs font-medium text-violet-400">处置执行</span>
<span className={`text-[10px] flex items-center gap-1 ${cfg.cls} ml-auto`}>
{cfg.icon} {cfg.label}
</span>
</div>
{/* Steps */}
<div className="px-2.5 py-1.5 max-h-40 overflow-y-auto">
<div className="space-y-1">
{steps.map((step: any, i: number) => (
<StepRow key={i} step={step} />
))}
</div>
</div>
{/* Actions */}
<div className="px-2.5 py-1.5 border-t border-slate-600/30 flex items-center gap-1">
{(runStatus === 'pending') && onStart && (
<button
onClick={onStart}
className="text-[11px] px-2 py-1 rounded bg-violet-500/20 text-violet-400 hover:bg-violet-500/30 flex items-center gap-1"
>
<Zap className="w-3 h-3" /> 开始处置
</button>
)}
{runStatus === 'running' && onPause && (
<button
onClick={onPause}
className="text-[11px] px-2 py-1 rounded bg-amber-500/20 text-amber-400 hover:bg-amber-500/30 flex items-center gap-1"
>
<Pause className="w-3 h-3" /> 暂停
</button>
)}
{runStatus === 'paused' && onResume && (
<button
onClick={onResume}
className="text-[11px] px-2 py-1 rounded bg-sky-500/20 text-sky-400 hover:bg-sky-500/30 flex items-center gap-1"
>
<Zap className="w-3 h-3" /> 继续
</button>
)}
{(runStatus === 'running' || runStatus === 'paused') && onCancel && (
<button
onClick={onCancel}
className="text-[11px] px-2 py-1 rounded bg-red-500/20 text-red-400 hover:bg-red-500/30 flex items-center gap-1"
>
<XCircle className="w-3 h-3" /> 取消
</button>
)}
</div>
</div>
);
}
function StepRow({ step }: { step: any }) {
const execBadge: Record<string, { cls: string; label: string }> = {
auto: { cls: 'bg-emerald-500/15 text-emerald-400', label: '自动' },
confirm: { cls: 'bg-amber-500/15 text-amber-400', label: '确认' },
manual_only: { cls: 'bg-slate-500/15 text-slate-400', label: '手动' },
};
const stepStatusMap: Record<string, { dot: string; icon: React.ReactNode; cls: string }> = {
pending: { dot: 'bg-slate-500', icon: <Clock className="w-3 h-3" />, cls: 'text-slate-400' },
running: { dot: 'bg-sky-500 animate-pulse', icon: <Loader2 className="w-3 h-3 animate-spin" />, cls: 'text-sky-400' },
success: { dot: 'bg-emerald-500', icon: <CheckCircle2 className="w-3 h-3" />, cls: 'text-emerald-400' },
failed: { dot: 'bg-red-500', icon: <XCircle className="w-3 h-3" />, cls: 'text-red-400' },
skipped: { dot: 'bg-slate-500', icon: <Info className="w-3 h-3" />, cls: 'text-slate-400' },
pending_manual: { dot: 'bg-amber-500', icon: <ShieldAlert className="w-3 h-3" />, cls: 'text-amber-400' },
};
const ss = stepStatusMap[step.status || 'pending'] || stepStatusMap.pending;
const eb = execBadge[step.execution] || execBadge.auto;
return (
<div className="flex items-center gap-1.5 text-xs">
<div className={`w-1.5 h-1.5 rounded-full ${ss.dot} flex-shrink-0`} />
<span className={`flex-shrink-0 ${ss.cls}`}>{ss.icon}</span>
<span className="font-mono text-foreground/80 truncate flex-1">
#{step.order || '?'} {step.command_id || 'checklist'}
</span>
<span className={`text-[9px] px-1 rounded flex-shrink-0 ${eb.cls}`}>{eb.label}</span>
{step.error && <span className="text-[10px] text-red-400 flex-shrink-0">✗</span>}
</div>
);
}
// 懒加载这些避免循环 import
import { Loader2, XCircle, Pause } from 'lucide-react';