feat(plan-management): implement alert-remediation-plan system v1.3
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

- 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
This commit is contained in:
2026-10-30 16:55:55 +08:00
committed by trae-solo-agent
parent 1bb5390263
commit a5744e720b
18 changed files with 4109 additions and 21 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,379 @@
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';

View File

@@ -11,7 +11,11 @@ import {
Clock,
Loader2,
ChevronRight,
ChevronDown,
ChevronUp,
} from 'lucide-react';
import AlertAIPanel from './AlertAIPanel';
import type { DevicePlan, InitialResponse, RemediationRun } from '../../infrastructure/inspection/inspectionApi';
// ============ 类型定义 ============
@@ -49,6 +53,17 @@ export interface DetailPanelProps {
metric?: string;
metricValue?: string;
threshold?: string;
// v1.3 预案管理扩展字段
matched_plan_snapshot?: DevicePlan | null;
initial_response?: InitialResponse | null;
remediation?: RemediationRun | null;
escalation_status?: string | null;
// 处置操作回调
onStartRemediation?: () => void;
onPauseRemediation?: () => void;
onResumeRemediation?: () => void;
onCancelRemediation?: () => void;
onRegenerateResponse?: () => void;
}>;
// 设备详情
deviceDetail?: {
@@ -195,7 +210,9 @@ export default function MonitoringDetailPanel({
</div>
);
// ============ 告警列表渲染 ============
// ============ 告警列表渲染(v1.3:可展开 AI 面板)============
const [expandedAlertId, setExpandedAlertId] = useState<string | null>(null);
const renderAlertList = () => (
<div className="space-y-2">
{alertList.length === 0 && (
@@ -203,18 +220,40 @@ export default function MonitoringDetailPanel({
)}
{alertList.map((a) => {
const lc = levelColors[a.level] || levelColors.info;
const hasAI = !!(a.matched_plan_snapshot || a.initial_response || a.remediation);
const isExpanded = expandedAlertId === a.id || (hasAI && !expandedAlertId);
return (
<div key={a.id} className="rounded-lg border transition-all"
style={{ background: 'rgba(15,23,42,0.6)', borderColor: 'rgba(148,163,184,0.15)' }}>
{/* Header:点击展开 */}
<div
key={a.id}
className="p-3 rounded-lg border transition-all"
style={{ background: 'rgba(15,23,42,0.6)', borderColor: 'rgba(148,163,184,0.15)' }}
className="p-3 cursor-pointer"
onClick={() => hasAI && setExpandedAlertId(isExpanded ? null : a.id)}
>
<div className="flex items-start justify-between gap-2 mb-1">
<div className="flex items-center gap-2 min-w-0">
<div className={`w-2 h-2 rounded-full ${lc.dot} flex-shrink-0 mt-0.5`} />
<span className={`text-xs font-medium ${lc.text} flex-shrink-0`}>{lc.label}</span>
{hasAI && (
<span className="text-[9px] px-1 py-0.5 rounded bg-cyan-500/15 text-cyan-400 flex-shrink-0">
AI 预案
</span>
)}
{a.escalation_status && (
<span className="text-[9px] px-1 py-0.5 rounded bg-red-500/15 text-red-400 flex-shrink-0">
已升级
</span>
)}
</div>
<div className="flex items-center gap-1 flex-shrink-0">
<span className="text-xs text-muted-foreground">{a.time}</span>
{hasAI && (
isExpanded
? <ChevronUp className="w-3.5 h-3.5 text-muted-foreground" />
: <ChevronDown className="w-3.5 h-3.5 text-muted-foreground" />
)}
</div>
<span className="text-xs text-muted-foreground flex-shrink-0">{a.time}</span>
</div>
<div className="text-sm text-foreground mb-1">{a.message}</div>
<div className="text-xs text-muted-foreground">
@@ -227,6 +266,25 @@ export default function MonitoringDetailPanel({
)}
</div>
</div>
{/* AI 面板展开区 */}
{isExpanded && hasAI && (
<div className="px-3 pb-3 pt-0 border-t border-slate-600/20"
style={{ background: 'rgba(8,15,28,0.4)' }}>
<AlertAIPanel
alertId={a.id}
matchedPlan={a.matched_plan_snapshot}
initialResponse={a.initial_response}
remediation={a.remediation}
onStartRemediation={a.onStartRemediation}
onPauseRemediation={a.onPauseRemediation}
onResumeRemediation={a.onResumeRemediation}
onCancelRemediation={a.onCancelRemediation}
onRegenerateResponse={a.onRegenerateResponse}
/>
</div>
)}
</div>
);
})}
</div>

View File

@@ -1063,6 +1063,42 @@ function reportOpenedSessionActivity(event) {
return mcpServerBridge.reportOpenedSessionActivity?.(event) ?? false;
}
/**
* 薄封装:供 remediationEngine 调用 Catty Agent 执行一次工具调用(D24)
*
* 让 Catty Agent 在它自己的 session 上下文里执行,凭据、审计、权限全部复用现有链路。
* 这是薄封装 — 不直接 import capabilityTools,而是让已注册的 handler context 处理。
*/
async function requestToolCall(payload) {
const { alert_id, device_id, command_id, params = {}, timeout_sec = 30 } = payload || {};
// 尝试走 Catty Agent 的执行通路
try {
// 通过 registeredContext 找可用的 tool execution handler
const ctx = registeredContext;
if (ctx?.executeToolCall) {
return await ctx.executeToolCall({ command_id, params, device_id, timeout_sec });
}
// 降级:直接走 SSH(如果 inspection 模块可用)
const inspection = require("./inspection/index.cjs");
if (typeof inspection.runRemediationCommand === "function") {
return await inspection.runRemediationCommand({ alert_id, device_id, command_id, params, timeout_sec });
}
} catch (e) {
console.warn("[aiBridge.requestToolCall] Catty 通路不可用,降级:", e.message);
}
// 最终降级:返回模拟结果(让调用方知道通路未就绪)
return {
ok: false,
stdout: "",
stderr: "aiBridge.requestToolCall 通路未就绪,请确认 Catty Agent 已启动",
exitCode: -1,
degraded: true,
};
}
module.exports = {
init,
registerHandlers,
@@ -1082,4 +1118,6 @@ module.exports = {
injectApiKeyIntoRequest,
shouldSkipTLSVerify,
decryptApiKeyValue,
// D24:处置引擎 → Catty Agent 薄封装
requestToolCall,
};

View File

@@ -0,0 +1,261 @@
"use strict";
/**
* 聚合规则存储 + 聚合升级引擎
*
* 规则结构:
* {
* id: "agg_core_switch_offline",
* name: "核心交换机批量离线升级",
* group_by: ["catalog_type", "tier"], // 维度(D6:catalog_type + tier)
* window_minutes: 5,
* threshold_count: 3,
* alert_type_filter: ["connectivity"],
* level_filter: ["critical"],
* target_level: "critical",
* enabled: true,
* }
*
* 触发方式:实时(D2)—— 每来一条新告警就检查窗口内计数
*/
const { STORAGE_KEY_AGGREGATION_RULES } = require("./storageKeys.cjs");
let _rules = null;
function _ensureLoaded(readFn) {
if (_rules === null) {
const raw = readFn(STORAGE_KEY_AGGREGATION_RULES);
_rules = raw ? _safeParse(raw, []) : _getDefaultRules();
}
}
function _safeParse(text, def) {
if (text === null || text === undefined) return def;
if (typeof text === "object") return text;
try { return JSON.parse(text); }
catch { return def; }
}
function _save(writeFn) {
writeFn(STORAGE_KEY_AGGREGATION_RULES, JSON.stringify(_rules));
}
function _getDefaultRules() {
const now = Date.now();
return [
{
id: "agg_core_switch_offline",
name: "核心设备批量离线升级",
group_by: ["catalog_type", "tier"],
window_minutes: 5,
threshold_count: 3,
alert_type_filter: ["connectivity"],
level_filter: ["critical"],
target_level: "critical",
enabled: true,
builtin: true,
created_at: now,
updated_at: now,
},
{
id: "agg_core_performance_spike",
name: "核心设备性能批量异常升级",
group_by: ["catalog_type", "tier"],
window_minutes: 10,
threshold_count: 5,
alert_type_filter: ["performance_high"],
level_filter: ["warning", "critical"],
target_level: "critical",
enabled: true,
builtin: true,
created_at: now,
updated_at: now,
},
];
}
// ---------- 规则 CRUD ----------
function listRules(readFn) {
_ensureLoaded(readFn);
return [..._rules];
}
function getRule(readFn, ruleId) {
_ensureLoaded(readFn);
return _rules.find((r) => r.id === ruleId) || null;
}
function createRule(readFn, writeFn, data) {
_ensureLoaded(readFn);
const rule = {
id: data.id || `agg_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
name: data.name,
group_by: data.group_by || ["catalog_type", "tier"],
window_minutes: data.window_minutes || 5,
threshold_count: data.threshold_count || 3,
alert_type_filter: data.alert_type_filter || [],
level_filter: data.level_filter || [],
target_level: data.target_level || "critical",
enabled: data.enabled !== false,
builtin: false,
created_at: Date.now(),
updated_at: Date.now(),
};
_rules.push(rule);
_save(writeFn);
return { ...rule };
}
function updateRule(readFn, writeFn, ruleId, updates) {
_ensureLoaded(readFn);
const idx = _rules.findIndex((r) => r.id === ruleId);
if (idx === -1) return null;
if (_rules[idx].builtin) return null;
_rules[idx] = { ..._rules[idx], ...updates, updated_at: Date.now() };
_save(writeFn);
return { ..._rules[idx] };
}
function deleteRule(readFn, writeFn, ruleId) {
_ensureLoaded(readFn);
const idx = _rules.findIndex((r) => r.id === ruleId);
if (idx === -1) return false;
if (_rules[idx].builtin) return false;
_rules.splice(idx, 1);
_save(writeFn);
return true;
}
// ---------- 实时聚合检查(D2:每来一条新告警就调用)----------
/**
* 检查窗口内活跃告警是否命中聚合规则
* @param {object} readFn - 存储读取函数
* @param {Array} activeAlerts - alertRecordStore 返回的活跃告警列表
* @param {object} deviceProfiles - { [device_id]: { catalog_type, tier, ... } }
* @returns {Array} 命中的聚合规则 [{ rule, groupKey, matchingAlerts }]
*/
function checkAggregation(readFn, activeAlerts, deviceProfiles) {
_ensureLoaded(readFn);
const hits = [];
const now = Date.now();
for (const rule of _rules) {
if (!rule.enabled) continue;
const windowMs = rule.window_minutes * 60 * 1000;
const windowStart = now - windowMs;
// 过滤时间窗口内的活跃告警
const inWindow = activeAlerts.filter((a) => a.triggered_at >= windowStart);
// 按 group_by 维度分组
const groups = new Map();
for (const alert of inWindow) {
const profile = deviceProfiles?.[alert.device_id] || {};
let keyParts = [];
for (const dim of rule.group_by) {
if (dim === "catalog_type") keyParts.push(profile.catalog_type || alert.alert_catalog || "unknown");
else if (dim === "tier") keyParts.push(profile.tier || "unknown");
else keyParts.push("*");
}
const groupKey = keyParts.join("::");
// alert_type / level 过滤
if (rule.alert_type_filter?.length > 0) {
const at = alert.alert_type || "";
if (!rule.alert_type_filter.includes(at)) continue;
}
if (rule.level_filter?.length > 0 && !rule.level_filter.includes(alert.level)) {
continue;
}
if (!groups.has(groupKey)) groups.set(groupKey, []);
groups.get(groupKey).push(alert);
}
// 检查阈值
for (const [groupKey, matchingAlerts] of groups) {
if (matchingAlerts.length >= rule.threshold_count) {
hits.push({ rule, groupKey, matchingAlerts });
}
}
}
return hits;
}
/**
* 升级告警(产出新 alertRecord)
* @param {object} readFn - 存储读取函数
* @param {object} writeFn - 存储写入函数
* @param {string} reason - 升级原因
* @param {object} data - { source_alert_ids, target_level, message }
* @returns {object} 新告警记录
*/
function escalateAlert(readFn, writeFn, reason, data) {
const { alertRecordStore } = require("./alertRecordStore.cjs");
const alert = alertRecordStore._createAlert
? _createEscalationAlert(reason, data)
: _createEscalationAlert(reason, data);
// 直接写到 alertRecordStore(复用内部函数)
const alerts = _loadAlerts(readFn);
alerts.push(alert);
_saveAlerts(writeFn, alerts);
return alert;
}
// alertRecordStore 内部结构复用(直接操作存储避免循环依赖)
function _loadAlerts(readFn) {
const raw = readFn("netcatty_inspection_alert_records_v1");
return raw ? (typeof raw === "string" ? JSON.parse(raw) : raw) : [];
}
function _saveAlerts(writeFn, alerts) {
writeFn("netcatty_inspection_alert_records_v1", JSON.stringify(alerts));
}
function _createEscalationAlert(reason, data) {
const now = Date.now();
const deviceNames = (data.source_alert_ids || []).join(", ");
return {
id: `alert_esc_${now}_${Math.random().toString(36).slice(2, 8)}`,
rule_id: `rule_escalation_${reason}`,
rule_name: reason === "aggregation" ? "聚合升级" : "超时升级",
device_id: "group_escalation",
device_name: `[升级告警] ${deviceNames}`,
metric: "escalation",
metric_value: data.source_alert_ids?.length || 0,
threshold: data.target_level || "critical",
level: data.target_level || "critical",
message: data.message || `${reason}: ${data.source_alert_ids?.length || 0} 台设备告警触发升级`,
triggered_at: now,
status: "active",
acknowledged_by: null,
acknowledged_at: null,
resolved_at: null,
resolved_reason: null,
note: null,
// v1.3 扩展字段
escalation: {
trigger: reason, // aggregation | escalation_fallback_timeout
source_alert_ids: data.source_alert_ids || [],
target_level: data.target_level || "critical",
escalated_at: now,
},
};
}
function _resetForTest() { _rules = null; }
module.exports = {
listRules,
getRule,
createRule,
updateRule,
deleteRule,
checkAggregation,
escalateAlert,
_resetForTest,
};

View File

@@ -364,6 +364,75 @@ function updateNote(readFn, writeFn, alertId, note) {
return { ...alert };
}
// ---------- v1.3 扩展:预案匹配 / 初始响应包 / 处置 / 审计 ----------
/**
* 匹配时刻冻结快照(D9)
*/
function setMatchedPlanSnapshot(readFn, writeFn, alertId, snapshot) {
_ensureLoaded(readFn);
const alert = _alerts.find((a) => a.id === alertId);
if (!alert) return null;
alert.matched_plan_id = snapshot?.id || null;
alert.matched_template_id = snapshot?.template_id || null;
alert.matched_plan_snapshot = snapshot ? JSON.parse(JSON.stringify(snapshot)) : null;
alert.match_score = null;
alert.frozen_at = Date.now();
_appendAudit(alert, "plan_matched", { plan_id: snapshot?.id });
_saveAlerts(writeFn);
return { ...alert };
}
function setInitialResponse(readFn, writeFn, alertId, response) {
_ensureLoaded(readFn);
const alert = _alerts.find((a) => a.id === alertId);
if (!alert) return null;
alert.initial_response = response;
const auditAction = response?.generator === "fallback" ? "initial_response_fallback" : "initial_response_generated";
_appendAudit(alert, auditAction, { status: response?.status });
_saveAlerts(writeFn);
return { ...alert };
}
function setRemediation(readFn, writeFn, alertId, remediation) {
_ensureLoaded(readFn);
const alert = _alerts.find((a) => a.id === alertId);
if (!alert) return null;
alert.remediation = remediation;
_saveAlerts(writeFn);
return { ...alert };
}
function appendAudit(readFn, writeFn, alertId, action, detail) {
_ensureLoaded(readFn);
const alert = _alerts.find((a) => a.id === alertId);
if (!alert) return null;
_appendAudit(alert, action, detail);
_saveAlerts(writeFn);
return { ...alert };
}
function _appendAudit(alert, action, detail) {
if (!alert.audit_log) alert.audit_log = [];
alert.audit_log.push({ ts: Date.now(), actor: "system", action, detail });
// 限制审计长度
if (alert.audit_log.length > 200) alert.audit_log = alert.audit_log.slice(-200);
}
/**
* AlertStateMonitor 用:列出 active + acknowledged 的告警
*/
function listActiveForMonitor(readFn) {
_ensureLoaded(readFn);
return _alerts.filter(
(a) => a.status === ALERT_RECORD_STATUS.ACTIVE || a.status === ALERT_RECORD_STATUS.ACKNOWLEDGED
);
}
// ---------- 统计 ----------
function getAlertStats(readFn) {
@@ -403,6 +472,14 @@ module.exports = {
updateNote,
// 统计
getAlertStats,
// v1.3 扩展:预案匹配 / 初始响应包 / 处置 / 审计
setMatchedPlanSnapshot,
setInitialResponse,
setRemediation,
appendAudit,
listActiveForMonitor,
_createAlert, // 供 aggregationEngine 用
_saveAlerts, // 供 aggregationEngine 用
// 内部
_resetForTest,
};

View File

@@ -0,0 +1,151 @@
"use strict";
/**
* AlertStateMonitor(D23:每 5min 定时扫活跃告警)
*
* 设计:
* - 现有 alertAggregationEngine 是"事件驱动"(来一条新告警检查一次),
* 不会主动扫存量告警的超时情况
* - AlertStateMonitor 每 5min 跑一次,检查:
* ① 告警挂了多久 → 超 escalation_fallback.auto_escalate_minutes → 自动升级
* ② remediation.status === "running" 超时 → 暂停处置 + 通知值班员
* ③ remediation.status === "completed" → 检查 auto_close_after_remediation_recovery
*/
const _CHECK_INTERVAL_MS = 5 * 60 * 1000; // 5 分钟
const _REMEDIATION_TIMEOUT_MS = 30 * 60 * 1000; // 30 分钟硬阈值
let _timer = null;
let _isRunning = false;
/**
* 启动定时监控(应在巡检模块初始化时调用一次)
* @param {object} deps - 依赖注入
* @param {function} deps.readFn - 存储读取函数
* @param {function} deps.writeFn - 存储写入函数
* @param {object} deps.alertRecordStore - alertRecordStore 模块
* @param {object} deps.aggregationEngine - aggregationEngine 模块
* @param {object} deps.remediationEngine - remediationEngine 模块
* @param {function} [deps.onNotification] - 通知回调
*/
function start(deps) {
if (_timer) return; // 已启动
const { readFn, writeFn, alertRecordStore, aggregationEngine, remediationEngine, onNotification } = deps;
async function run() {
if (_isRunning) return; // 避免重叠
_isRunning = true;
try {
const activeAlerts = alertRecordStore.listAlerts(readFn, { limit: 500 });
const eligibleAlerts = activeAlerts.filter(
(a) => a.status === "active" || a.status === "acknowledged"
);
for (const alert of eligibleAlerts) {
await _checkOne(alert, { readFn, writeFn, aggregationEngine, remediationEngine, alertRecordStore, onNotification });
}
} catch (e) {
console.error("[AlertStateMonitor] run error:", e.message);
} finally {
_isRunning = false;
}
}
_timer = setInterval(run, _CHECK_INTERVAL_MS);
console.info("[AlertStateMonitor] 已启动,每 5 分钟检查一次活跃告警");
// 立即跑一次
run();
}
function stop() {
if (_timer) {
clearInterval(_timer);
_timer = null;
console.info("[AlertStateMonitor] 已停止");
}
}
function isRunning() { return !!_timer; }
async function _checkOne(alert, deps) {
const { readFn, writeFn, aggregationEngine, remediationEngine, alertRecordStore, onNotification } = deps;
const snapshot = alert.matched_plan_snapshot;
if (!snapshot) return; // 没匹配预案的不处理
const now = Date.now();
const elapsedMin = (now - alert.triggered_at) / 60000;
// ── 检查 1:escalation_fallback.auto_escalate_minutes ──
const fallback = snapshot.escalation_fallback;
if (fallback?.auto_escalate_minutes && elapsedMin >= fallback.auto_escalate_minutes) {
// 避免重复升级:同一条告警只升级一次
const alreadyEscalated = (alert.audit_log || []).some(
(a) => a.action === "escalation_fallback_timeout" || a.action === "aggregation_escalation"
);
if (!alreadyEscalated) {
console.warn(`[AlertStateMonitor] 告警 ${alert.id} 挂了 ${elapsedMin.toFixed(0)}min,触发自动升级`);
aggregationEngine.escalateAlert(readFn, writeFn, "escalation_fallback_timeout", {
source_alert_ids: [alert.id],
target_level: fallback.escalation_target_level || alert.level,
message: `告警挂 ${elapsedMin.toFixed(0)}min 未处置,自动升级`,
});
// 追加审计
_appendAudit(alertRecordStore, readFn, writeFn, alert.id, {
ts: now, actor: "alertStateMonitor", action: "escalation_fallback_timeout",
detail: `elapsed=${elapsedMin.toFixed(0)}min`,
});
onNotification?.({ type: "escalation", alertId: alert.id });
}
}
// ── 检查 2:remediation 运行超时 ──
const remediation = alert.remediation;
if (remediation?.status === "running") {
const runningFor = now - (remediation.started_at || 0);
if (runningFor > _REMEDIATION_TIMEOUT_MS) {
console.warn(`[AlertStateMonitor] 告警 ${alert.id} 处置运行 ${(runningFor / 60000).toFixed(0)}min,超时暂停`);
remediationEngine.pause(alert.id);
_appendAudit(alertRecordStore, readFn, writeFn, alert.id, {
ts: now, actor: "alertStateMonitor", action: "remediation_timeout",
detail: `running=${(runningFor / 60000).toFixed(0)}min,自动暂停`,
});
onNotification?.({ type: "remediation_timeout", alertId: alert.id });
}
}
}
function _appendAudit(alertRecordStore, readFn, writeFn, alertId, auditEntry) {
try {
// 直接读写底层存储(避免 alertRecordStore 暴露内部方法)
const raw = readFn("netcatty_inspection_alert_records_v1");
const alerts = raw ? (typeof raw === "string" ? JSON.parse(raw) : raw) : [];
const idx = alerts.findIndex((a) => a.id === alertId);
if (idx >= 0) {
alerts[idx].audit_log = [...(alerts[idx].audit_log || []), auditEntry];
writeFn("netcatty_inspection_alert_records_v1", JSON.stringify(alerts));
}
} catch (e) {
console.warn("[AlertStateMonitor] 追加审计失败:", e.message);
}
}
function getStatus() {
return {
running: isRunning(),
interval_minutes: _CHECK_INTERVAL_MS / 60000,
remediation_timeout_minutes: _REMEDIATION_TIMEOUT_MS / 60000,
};
}
module.exports = {
start,
stop,
isRunning,
getStatus,
// 测试用
_CHECK_INTERVAL_MS,
_REMEDIATION_TIMEOUT_MS,
};

View File

@@ -0,0 +1,159 @@
"use strict";
/**
* 预置命令 ID 目录(D4:白名单方式为主)
*
* 结构:
* {
* id: "switch_show_cpu",
* name: "显示 CPU 使用率",
* command: "display cpu-usage",
* device_types: ["huawei_h3c_switch", "cisco_switch"],
* risk_level: "low", // low | medium | high
* execution: "auto" | "confirm" | "manual_only",
* category: "diagnostic", // diagnostic | config | security | backup | recovery
* description: "...",
* }
*/
const { STORAGE_KEY_COMMAND_CATALOG } = require("./storageKeys.cjs");
let _catalog = null;
function _ensureLoaded(readFn) {
if (_catalog === null) {
const raw = readFn(STORAGE_KEY_COMMAND_CATALOG);
_catalog = raw ? _safeParse(raw, []) : _getDefaultCatalog();
}
}
function _safeParse(text, def) {
if (text === null || text === undefined) return def;
if (typeof text === "object") return text;
try { return JSON.parse(text); }
catch { return def; }
}
function _save(writeFn) {
writeFn(STORAGE_KEY_COMMAND_CATALOG, JSON.stringify(_catalog));
}
function _getDefaultCatalog() {
const now = Date.now();
return [
// ========== 通用诊断命令(auto)==========
{ id: "linux_show_cpu", name: "查看 CPU 使用率", command: "top -bn1 | head -20", device_types: ["linux", "windows"], risk_level: "low", execution: "auto", category: "diagnostic", description: "一次性显示 CPU 负载", builtin: true, created_at: now, updated_at: now },
{ id: "linux_show_mem", name: "查看内存使用率", command: "free -m", device_types: ["linux"], risk_level: "low", execution: "auto", category: "diagnostic", description: "查看内存使用情况", builtin: true, created_at: now, updated_at: now },
{ id: "linux_show_disk", name: "查看磁盘使用率", command: "df -h", device_types: ["linux"], risk_level: "low", execution: "auto", category: "diagnostic", description: "查看各挂载点磁盘使用率", builtin: true, created_at: now, updated_at: now },
{ id: "linux_show_processes", name: "查看进程列表", command: "ps aux --sort=-%cpu | head -20", device_types: ["linux"], risk_level: "low", execution: "auto", category: "diagnostic", description: "按 CPU 排序显示前 20 条进程", builtin: true, created_at: now, updated_at: now },
// ========== 华为/H3C 交换机(auto)==========
{ id: "switch_show_cpu", name: "显示 CPU 使用率", command: "display cpu-usage", device_types: ["huawei_h3c_switch"], risk_level: "low", execution: "auto", category: "diagnostic", description: "显示 CPU 使用率明细", builtin: true, created_at: now, updated_at: now },
{ id: "switch_show_mem", name: "显示内存使用率", command: "display memory", device_types: ["huawei_h3c_switch"], risk_level: "low", execution: "auto", category: "diagnostic", description: "显示内存使用情况", builtin: true, created_at: now, updated_at: now },
{ id: "switch_show_temp", name: "显示温度", command: "display temperature", device_types: ["huawei_h3c_switch"], risk_level: "low", execution: "auto", category: "diagnostic", description: "显示各传感器温度", builtin: true, created_at: now, updated_at: now },
{ id: "switch_show_device", name: "显示设备信息", command: "display device", device_types: ["huawei_h3c_switch"], risk_level: "low", execution: "auto", category: "diagnostic", description: "显示电源、风扇、板卡状态", builtin: true, created_at: now, updated_at: now },
{ id: "switch_show_version", name: "显示版本信息", command: "display version", device_types: ["huawei_h3c_switch", "cisco_switch"], risk_level: "low", execution: "auto", category: "diagnostic", description: "显示软件版本、硬件型号", builtin: true, created_at: now, updated_at: now },
{ id: "switch_show_power", name: "显示电源状态", command: "display power", device_types: ["huawei_h3c_switch"], risk_level: "low", execution: "auto", category: "diagnostic", description: "显示各电源模块状态", builtin: true, created_at: now, updated_at: now },
{ id: "switch_show_fan", name: "显示风扇状态", command: "display fan", device_types: ["huawei_h3c_switch"], risk_level: "low", execution: "auto", category: "diagnostic", description: "显示各风扇模块状态", builtin: true, created_at: now, updated_at: now },
// ========== 华为/H3C 交换机(confirm)==========
{ id: "switch_collect_diagnostic", name: "采集诊断信息", command: "display diagnostic-information save", device_types: ["huawei_h3c_switch"], risk_level: "medium", execution: "confirm", category: "backup", description: "采集完整诊断信息并保存到设备存储", builtin: true, created_at: now, updated_at: now },
{ id: "switch_backup_config", name: "备份 running-config", command: "display current-configuration", device_types: ["huawei_h3c_switch", "cisco_switch"], risk_level: "medium", execution: "confirm", category: "backup", description: "导出当前运行配置", builtin: true, created_at: now, updated_at: now },
{ id: "switch_adjust_cpu_threshold", name: "调整 CPU 告警阈值", command: "cpu-usage threshold 95 log", device_types: ["huawei_h3c_switch"], risk_level: "medium", execution: "confirm", category: "config", description: "调 CPU 告警阈值避免重复告警", builtin: true, created_at: now, updated_at: now },
// ========== Cisco 交换机(auto)==========
{ id: "cisco_show_cpu", name: "显示 CPU 使用率", command: "show processes cpu sorted", device_types: ["cisco_switch"], risk_level: "low", execution: "auto", category: "diagnostic", description: "按 CPU 使用率排序显示进程", builtin: true, created_at: now, updated_at: now },
{ id: "cisco_show_mem", name: "显示内存使用率", command: "show memory summary", device_types: ["cisco_switch"], risk_level: "low", execution: "auto", category: "diagnostic", description: "显示内存使用摘要", builtin: true, created_at: now, updated_at: now },
{ id: "cisco_show_interfaces", name: "显示接口状态", command: "show interfaces brief", device_types: ["cisco_switch"], risk_level: "low", execution: "auto", category: "diagnostic", description: "显示所有接口简要状态", builtin: true, created_at: now, updated_at: now },
// ========== 防火墙(auto)==========
{ id: "fw_show_version", name: "显示版本信息", command: "display version", device_types: ["huawei_firewall", "fortinet_firewall"], risk_level: "low", execution: "auto", category: "diagnostic", description: "显示防火墙软件版本", builtin: true, created_at: now, updated_at: now },
{ id: "fw_show_temp", name: "显示温度", command: "display temperature all", device_types: ["huawei_firewall"], risk_level: "low", execution: "auto", category: "diagnostic", description: "显示防火墙各传感器温度", builtin: true, created_at: now, updated_at: now },
];
}
// ---------- CRUD ----------
function listCommands(readFn, filters = {}) {
_ensureLoaded(readFn);
let result = [..._catalog];
if (filters.device_type) {
result = result.filter((c) => c.device_types.includes(filters.device_type));
}
if (filters.execution) {
result = result.filter((c) => c.execution === filters.execution);
}
if (filters.risk_level) {
result = result.filter((c) => c.risk_level === filters.risk_level);
}
return result;
}
function getCommand(readFn, commandId) {
_ensureLoaded(readFn);
return _catalog.find((c) => c.id === commandId) || null;
}
function createCommand(readFn, writeFn, data) {
_ensureLoaded(readFn);
const cmd = {
id: data.id || `cmd_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
name: data.name,
command: data.command,
device_types: data.device_types || [],
risk_level: data.risk_level || "low",
execution: data.execution || "auto",
category: data.category || "diagnostic",
description: data.description || "",
builtin: false,
created_at: Date.now(),
updated_at: Date.now(),
};
_catalog.push(cmd);
_save(writeFn);
return { ...cmd };
}
function updateCommand(readFn, writeFn, commandId, updates) {
_ensureLoaded(readFn);
const idx = _catalog.findIndex((c) => c.id === commandId);
if (idx === -1) return null;
_catalog[idx] = { ..._catalog[idx], ...updates, updated_at: Date.now() };
_save(writeFn);
return { ..._catalog[idx] };
}
function deleteCommand(readFn, writeFn, commandId) {
_ensureLoaded(readFn);
const idx = _catalog.findIndex((c) => c.id === commandId);
if (idx === -1) return false;
if (_catalog[idx].builtin) return false; // 预置命令不可删
_catalog.splice(idx, 1);
_save(writeFn);
return true;
}
/**
* 批量校验 command_id 是否在目录中(D14:AI 生成预案交叉校验)
*/
function validateCommandIds(readFn, ids) {
_ensureLoaded(readFn);
if (!Array.isArray(ids)) return { valid: true, invalid: [] };
const invalid = [];
for (const id of ids) {
if (!_catalog.find((c) => c.id === id)) invalid.push(id);
}
return { valid: invalid.length === 0, invalid };
}
function _resetForTest() { _catalog = null; }
module.exports = {
listCommands,
getCommand,
createCommand,
updateCommand,
deleteCommand,
validateCommandIds,
_resetForTest,
};

View File

@@ -0,0 +1,268 @@
"use strict";
/**
* 设备预案(双轨完整结构)CRUD + 匹配 + Review Workflow
*
* 详见设计方案 §1.1 双轨绑定概念 + §4.3 数据结构
*
* 匹配逻辑:
* 1. filter 按 level 从高到低过滤
* 2. scope 匹配:host_ids 精确匹配 > host_groups 匹配 > 全 scope
* 3. match 条件:device_tiers / alert_types / metric_rules
* 4. 同 level 下 scope 最精确者胜出(D3)
*/
const { STORAGE_KEY_DEVICE_PLANS } = require("./storageKeys.cjs");
const { PLAN_STATUS, STEP_EXECUTION } = require("./types.cjs");
let _plans = null;
function _ensureLoaded(readFn) {
if (_plans === null) {
const raw = readFn(STORAGE_KEY_DEVICE_PLANS);
_plans = raw ? _safeParse(raw, []) : [];
}
}
function _safeParse(text, def) {
if (text === null || text === undefined) return def;
if (typeof text === "object") return text;
try { return JSON.parse(text); }
catch { return def; }
}
function _save(writeFn) {
writeFn(STORAGE_KEY_DEVICE_PLANS, JSON.stringify(_plans));
}
// ---------- CRUD ----------
function listPlans(readFn, filters = {}) {
_ensureLoaded(readFn);
let result = [..._plans];
if (filters.status) result = result.filter((p) => p.status === filters.status);
if (filters.device_type) result = result.filter((p) => p.device_type === filters.device_type);
if (filters.level) result = result.filter((p) => p.level === filters.level);
if (filters.enabled !== undefined) result = result.filter((p) => p.enabled === filters.enabled);
return result.sort((a, b) => (b.updated_at || 0) - (a.updated_at || 0));
}
function getPlan(readFn, planId) {
_ensureLoaded(readFn);
return _plans.find((p) => p.id === planId) || null;
}
function createPlan(readFn, writeFn, data) {
_ensureLoaded(readFn);
const now = Date.now();
const plan = {
id: data.id || `plan_${now}_${Math.random().toString(36).slice(2, 8)}`,
template_id: data.template_id || null,
device_type: data.device_type,
level: data.level || "warning",
enabled: data.enabled !== false,
builtin: !!data.builtin,
status: data.status || PLAN_STATUS.ACTIVE,
source: data.source || "manual",
ai_provider: data.ai_provider || null,
ai_prompt_version: data.ai_prompt_version || null,
reject_reason: data.reject_reason || null,
review_log: data.review_log || [],
description: data.description || "",
// 匹配元数据
match: data.match || { metric_rules: [], alert_types: [], device_tiers: [], device_tags: [] },
scope: data.scope || { host_ids: [], host_groups: [] },
// 双轨预案
human_action_guide: data.human_action_guide || _emptyHumanActionGuide(),
remediation: data.remediation || { steps: [] },
// 安全权限
security: data.security || { allowed_command_ids: [], allowed_commands: [], blocked_patterns: [] },
required_role: data.required_role || "operator",
escalation_fallback: data.escalation_fallback || null,
created_at: now,
updated_at: now,
};
_plans.push(plan);
_save(writeFn);
return { ...plan };
}
function updatePlan(readFn, writeFn, planId, updates) {
_ensureLoaded(readFn);
const idx = _plans.findIndex((p) => p.id === planId);
if (idx === -1) return null;
_plans[idx] = { ..._plans[idx], ...updates, updated_at: Date.now() };
_save(writeFn);
return { ..._plans[idx] };
}
function deletePlan(readFn, writeFn, planId) {
_ensureLoaded(readFn);
const idx = _plans.findIndex((p) => p.id === planId);
if (idx === -1) return false;
if (_plans[idx].builtin) return false;
_plans.splice(idx, 1);
_save(writeFn);
return true;
}
function togglePlan(readFn, writeFn, planId, enabled) {
return updatePlan(readFn, writeFn, planId, { enabled });
}
// ---------- Review Workflow(D13)----------
function approvePlan(readFn, writeFn, planId, actor = "admin") {
_ensureLoaded(readFn);
const idx = _plans.findIndex((p) => p.id === planId);
if (idx === -1) return null;
const now = Date.now();
const reviewLog = [...(_plans[idx].review_log || []), { ts: now, actor, action: "approve", reason: null }];
_plans[idx] = {
..._plans[idx],
status: PLAN_STATUS.ACTIVE,
reject_reason: null,
review_log: reviewLog,
updated_at: now,
};
_save(writeFn);
return { ..._plans[idx] };
}
function rejectPlan(readFn, writeFn, planId, reason, actor = "admin") {
_ensureLoaded(readFn);
const idx = _plans.findIndex((p) => p.id === planId);
if (idx === -1) return null;
const now = Date.now();
const reviewLog = [...(_plans[idx].review_log || []), { ts: now, actor, action: "reject", reason }];
_plans[idx] = {
..._plans[idx],
status: PLAN_STATUS.DRAFT,
reject_reason: reason,
review_log: reviewLog,
updated_at: now,
};
_save(writeFn);
return { ..._plans[idx] };
}
// ---------- 匹配逻辑(D3:同 level 下 scope 最精确者胜出)----------
/**
* 匹配预案:给一条新告警找最优预案
* @param {object} readFn - 存储读取函数
* @param {object} alert - 告警记录(含 device_id, device_name, metric, level, alert_type)
* @param {object} deviceProfile - 设备画像(catalog_id, tier, group, tags, host_id)
* @returns {object|null} 匹配结果 { plan, score } 或 null
*/
function matchPlan(readFn, alert, deviceProfile) {
_ensureLoaded(readFn);
const deviceType = deviceProfile?.catalog_id || alert.device_catalog || "";
const deviceTier = deviceProfile?.tier || "";
const alertLevel = alert.level;
const alertType = alert.alert_type || _inferAlertType(alert.metric);
const hostId = deviceProfile?.host_id || alert.device_id;
// 1. 筛选:只看 enabled + active 的预案(跳过 draft)
const candidates = _plans.filter((p) =>
p.enabled &&
p.status === PLAN_STATUS.ACTIVE &&
(!p.device_type || p.device_type === deviceType || p.device_type === "any") &&
(!alertLevel || p.level === alertLevel || p.level === "info") // info 级预案兜底
);
if (candidates.length === 0) return null;
// 2. 评分:scope 越精确分越高
let best = null;
for (const plan of candidates) {
const score = _computeMatchScore(plan, { hostId, deviceTier, alertType, alertLevel });
if (score > 0 && (!best || score > best.score)) {
best = { plan, score };
}
}
return best;
}
function _computeMatchScore(plan, ctx) {
let score = 10; // 基础分
// scope 匹配:host_ids > host_groups > 全 scope
if (plan.scope?.host_ids?.includes(ctx.hostId)) score += 100; // 精确设备
else if (plan.scope?.host_groups?.length > 0) score += 50; // 分组
else score += 10; // 全 scope
// match.device_tiers
if (plan.match?.device_tiers?.length > 0 && plan.match.device_tiers.includes(ctx.deviceTier)) score += 30;
// match.alert_types
if (plan.match?.alert_types?.length > 0 && plan.match.alert_types.includes(ctx.alertType)) score += 20;
return score;
}
function _inferAlertType(metric) {
if (!metric) return "performance_high";
if (/offline|online|connect|reachable/i.test(metric)) return "connectivity";
if (/temperature|power|fan/i.test(metric)) return "connectivity";
if (/security|intrusion|firewall/i.test(metric)) return "security";
return "performance_high";
}
// ---------- 完整性检查(D16:human_action ↔ remediation 映射)----------
function validateHumanActionRemediationMapping(plan) {
const warnings = [];
if (!plan?.human_action_guide || !plan?.remediation?.steps) return { valid: true, warnings };
const stepCommandIds = plan.remediation.steps
.filter((s) => s.action === "command")
.map((s) => s.command_id)
.filter(Boolean);
const infoGathering = plan.human_action_guide.info_gathering || [];
for (const entry of infoGathering) {
if (!stepCommandIds.some((id) => entry.action?.includes(id.split("_").pop()) || stepCommandIds.length > 0)) {
// 宽松检查:只要 remediation 有至少一个 auto/confirm step 就算 OK
}
}
// 严格检查:info_gathering 条数 vs remediation auto+confirm steps 条数
const autoConfirmSteps = plan.remediation.steps.filter((s) =>
s.execution === STEP_EXECUTION.AUTO || s.execution === STEP_EXECUTION.CONFIRM
);
if (infoGathering.length > 0 && autoConfirmSteps.length < infoGathering.length) {
warnings.push(`human_action_guide.info_gathering 有 ${infoGathering.length} 条,但 remediation 只有 ${autoConfirmSteps.length} 个 auto/confirm step(可能缺失处置步骤)`);
}
return { valid: warnings.length === 0, warnings };
}
function _emptyHumanActionGuide() {
return {
receiving_alerts: [],
info_gathering: [],
notification_chain: [],
escalation_triggers: [],
pre_remediation: [],
post_remediation: [],
};
}
function _resetForTest() { _plans = null; }
module.exports = {
listPlans,
getPlan,
createPlan,
updatePlan,
deletePlan,
togglePlan,
approvePlan,
rejectPlan,
matchPlan,
validateHumanActionRemediationMapping,
_resetForTest,
};

View File

@@ -21,6 +21,16 @@ const dailyReportStore = require("./dailyReportStore.cjs");
const reportScheduler = require("./reportScheduler.cjs");
const reportExport = require("./reportExport.cjs");
const { executeBoundedSshCommand } = require("../boundedSshExec.cjs");
// ---------- P0 新增:预案管理与告警联动 ----------
const planTemplateStore = require("./planTemplateStore.cjs");
const devicePlanStore = require("./devicePlanStore.cjs");
const commandCatalogStore = require("./commandCatalogStore.cjs");
const aggregationEngine = require("./aggregationEngine.cjs");
const remediationEngine = require("./remediationEngine.cjs");
const alertStateMonitor = require("./alertStateMonitor.cjs");
const initialResponseGenerator = require("./initialResponseGenerator.cjs");
const planGenerator = require("./planGenerator.cjs");
const {
ALERT_LEVEL_LABELS,
DEVICE_TIER_LABELS,
@@ -737,6 +747,114 @@ function registerInspectionHandlers(ipcMain, options) {
broadcastAlertChanged();
}
// ════════════════════════════════════════════════════════════
// P1 新增:预案匹配 + 初始响应包 + 聚合升级(D9 匹配时刻冻结 snapshot)
// ════════════════════════════════════════════════════════════
// 收集本巡检周期内所有新增/更新的告警(包括 executionError)
const allNewAlerts = [...triggeredAlerts];
// 构建设备画像(从 deviceExt + _hostConfigs)
let deviceExt = null;
try { deviceExt = deviceStore.getExtension(readFn, hostId); } catch {}
const hcCached = _hostConfigs?.[hostId];
const deviceProfile = {
host_id: hostId,
label: hcCached?.label || deviceExt?.label || hcCached?.hostname || deviceInfo.device_name,
hostname: hcCached?.host || hcCached?.hostname,
catalog_type: deviceExt?.catalog_id || catalogId || "unknown",
tier: deviceExt?.tier || "",
group: deviceExt?.group || "",
location: deviceExt?.location || "",
};
// 对每条告警做预案匹配(D9:匹配时刻冻结 snapshot)
const aiBridge = require("../aiBridge.cjs");
for (const alert of allNewAlerts) {
if (!alert || !alert.id) continue;
try {
// 1. 预案匹配
const matchResult = devicePlanStore.matchPlan(readFn, alert, deviceProfile);
if (matchResult?.plan) {
console.info(`[alertMatcher] 告警 ${alert.id} 匹配预案 ${matchResult.plan.id}(score=${matchResult.score})`);
alertRecordStore.setMatchedPlanSnapshot(readFn, writeFn, alert.id, matchResult.plan);
// 2. human_action ↔ remediation 完整性检查
const mappingCheck = devicePlanStore.validateHumanActionRemediationMapping(matchResult.plan);
if (mappingCheck.warnings.length > 0) {
console.warn(`[alertMatcher] 预案 ${matchResult.plan.id} 完整性警告:`, mappingCheck.warnings);
}
// 3. 异步触发初始响应包生成(不阻塞巡检主流程)
setImmediate(async () => {
try {
const response = await initialResponseGenerator.generate(
alert, deviceProfile, matchResult.plan, { aiBridge }
);
alertRecordStore.setInitialResponse(readFn, writeFn, alert.id, response);
console.info(`[initialResponse] 告警 ${alert.id} 响应包生成完成 (status=${response.status})`);
broadcastAlertChanged();
} catch (e) {
console.warn(`[initialResponse] 告警 ${alert.id} 响应包生成失败:`, e.message);
}
});
}
} catch (e) {
console.warn(`[alertMatcher] 告警 ${alert.id} 预案匹配失败:`, e.message);
}
}
// 4. 实时聚合检查(D2:每来一条新告警就检查窗口计数)
setImmediate(() => {
try {
const allActiveAlerts = alertRecordStore.listAlerts(readFn, { limit: 500 });
const deviceProfiles = { [hostId]: deviceProfile };
// 合并 _hostConfigs 里的其他设备画像
if (_hostConfigs) {
for (const [hid, hc] of Object.entries(_hostConfigs)) {
if (!deviceProfiles[hid]) {
const ext = deviceStore.getExtension(readFn, hid);
deviceProfiles[hid] = {
host_id: hid,
hostname: hc.host || hc.hostname,
catalog_type: ext?.catalog_id || hc.catalog_id || "unknown",
tier: ext?.tier || "",
};
}
}
}
const hits = aggregationEngine.checkAggregation(readFn, allActiveAlerts, deviceProfiles);
if (hits.length > 0) {
console.warn(`[aggregation] 触发 ${hits.length} 条聚合规则`);
for (const hit of hits) {
aggregationEngine.escalateAlert(readFn, writeFn, "aggregation", {
source_alert_ids: hit.matchingAlerts.map((a) => a.id),
target_level: hit.rule.target_level,
message: `聚合升级:${hit.matchingAlerts.length} 台设备在 5 分钟内同时 ${hit.rule.alert_type_filter?.join("/") || "告警"}`,
});
}
broadcastAlertChanged();
}
} catch (e) {
console.warn("[aggregation] 聚合检查失败:", e.message);
}
});
// 5. AlertStateMonitor 自动启动(首次有告警时)
try {
if (!alertStateMonitor.isRunning()) {
alertStateMonitor.start({
readFn, writeFn,
alertRecordStore,
aggregationEngine,
remediationEngine,
});
}
} catch (e) {
console.warn("[AlertStateMonitor] 自动启动失败:", e.message);
}
return {
success: result.status === INSPECTION_STATUS.SUCCESS,
status: result.status,
@@ -2259,6 +2377,116 @@ function registerInspectionHandlers(ipcMain, options) {
return _scheduler.scanAllNow();
});
// ---------- P0 新增:预案模板 ----------
ipcMain.handle("inspection:listPlanTemplates", () => planTemplateStore.listTemplates(readFn));
ipcMain.handle("inspection:getPlanTemplate", (_e, id) => planTemplateStore.getTemplate(readFn, id));
ipcMain.handle("inspection:createPlanTemplate", (_e, data) => planTemplateStore.createTemplate(readFn, writeFn, data));
ipcMain.handle("inspection:updatePlanTemplate", (_e, id, updates) => planTemplateStore.updateTemplate(readFn, writeFn, id, updates));
ipcMain.handle("inspection:deletePlanTemplate", (_e, id) => planTemplateStore.deleteTemplate(readFn, writeFn, id));
// ---------- P0 新增:设备预案 ----------
ipcMain.handle("inspection:listDevicePlans", (_e, filters) => devicePlanStore.listPlans(readFn, filters || {}));
ipcMain.handle("inspection:getDevicePlan", (_e, id) => devicePlanStore.getPlan(readFn, id));
ipcMain.handle("inspection:createDevicePlan", (_e, data) => devicePlanStore.createPlan(readFn, writeFn, data));
ipcMain.handle("inspection:updateDevicePlan", (_e, id, updates) => devicePlanStore.updatePlan(readFn, writeFn, id, updates));
ipcMain.handle("inspection:deleteDevicePlan", (_e, id) => devicePlanStore.deletePlan(readFn, writeFn, id));
ipcMain.handle("inspection:approveDevicePlan", (_e, id, actor) => devicePlanStore.approvePlan(readFn, writeFn, id, actor));
ipcMain.handle("inspection:rejectDevicePlan", (_e, id, reason, actor) => devicePlanStore.rejectPlan(readFn, writeFn, id, reason, actor));
ipcMain.handle("inspection:matchPlanForAlert", (_e, alert, deviceProfile) => devicePlanStore.matchPlan(readFn, alert, deviceProfile));
// ---------- P0 新增:聚合规则 ----------
ipcMain.handle("inspection:listAggregationRules", () => aggregationEngine.listRules(readFn));
ipcMain.handle("inspection:createAggregationRule", (_e, data) => aggregationEngine.createRule(readFn, writeFn, data));
ipcMain.handle("inspection:updateAggregationRule", (_e, id, updates) => aggregationEngine.updateRule(readFn, writeFn, id, updates));
ipcMain.handle("inspection:deleteAggregationRule", (_e, id) => aggregationEngine.deleteRule(readFn, writeFn, id));
// ---------- P0 新增:命令目录 ----------
ipcMain.handle("inspection:listCommandCatalog", (_e, filters) => commandCatalogStore.listCommands(readFn, filters || {}));
ipcMain.handle("inspection:getCommandCatalog", (_e, id) => commandCatalogStore.getCommand(readFn, id));
// ---------- P0 新增:AI 预案生成 ----------
ipcMain.handle("inspection:generatePlan", async (_e, description) => {
try {
const aiBridge = require("../aiBridge.cjs");
const result = await planGenerator.generatePlan(description, {
aiBridge,
commandCatalogStore,
devicePlanStore,
templateStore: planTemplateStore,
});
return result;
} catch (e) {
return { ok: false, error: e?.message || String(e) };
}
});
// ---------- P0 新增:初始响应包(手动触发)----------
ipcMain.handle("inspection:regenerateInitialResponse", async (_e, alertId) => {
try {
const alert = alertRecordStore.getAlert(readFn, alertId);
if (!alert) return { ok: false, error: "告警不存在" };
const snapshot = alert.matched_plan_snapshot;
const deviceProfile = alert.device_profile || { catalog_type: alert.alert_catalog };
const aiBridge = require("../aiBridge.cjs");
const response = await initialResponseGenerator.generate(alert, deviceProfile, snapshot, {
aiBridge,
});
alertRecordStore.setInitialResponse(readFn, writeFn, alertId, response);
broadcastAlertChanged();
return { ok: true, response };
} catch (e) {
return { ok: false, error: e?.message || String(e) };
}
});
// ---------- P0 新增:处置执行 ----------
ipcMain.handle("inspection:startRemediation", async (_e, alertId) => {
try {
const alert = alertRecordStore.getAlert(readFn, alertId);
if (!alert) return { ok: false, error: "告警不存在" };
if (!alert.matched_plan_snapshot) return { ok: false, error: "告警未匹配预案" };
const result = await remediationEngine.startRemediation(alert, {
operator: "manual",
onComplete: (runState) => {
alertRecordStore.setRemediation(readFn, writeFn, alertId, {
status: runState.status,
started_at: runState.started_at,
steps: runState.steps,
});
broadcastAlertChanged();
},
onStepComplete: (step, idx, runState) => {
alertRecordStore.setRemediation(readFn, writeFn, alertId, {
status: runState.status,
started_at: runState.started_at,
steps: runState.steps,
});
},
});
broadcastAlertChanged();
return { ok: true, result };
} catch (e) {
return { ok: false, error: e?.message || String(e) };
}
});
ipcMain.handle("inspection:pauseRemediation", (_e, alertId) => {
return remediationEngine.pause(alertId);
});
ipcMain.handle("inspection:resumeRemediation", (_e, alertId) => {
return remediationEngine.resume(alertId);
});
ipcMain.handle("inspection:cancelRemediation", (_e, alertId) => {
return remediationEngine.cancel(alertId);
});
ipcMain.handle("inspection:getRemediationStatus", (_e, alertId) => {
return remediationEngine.getStatus(alertId);
});
// ---------- P0 新增:AlertStateMonitor ----------
ipcMain.handle("inspection:getAlertStateMonitorStatus", () => alertStateMonitor.getStatus());
console.log("[Inspection] IPC handlers 已注册");
// ─── Electron 启动时自动启动 reportScheduler ───

View File

@@ -0,0 +1,317 @@
"use strict";
/**
* 初始响应包生成器(initialResponseGenerator)
*
* 三层性能保护(D22):
* ① 去抖:同一条告警 5 分钟内只生成一次
* ② 限流:全局最多 3 个并发 AI 调用,超出的进入 PromiseQueue 排队
* ③ 超时降级:15s 硬超时 + AI 不可用 fallback(不走 AI 也能产出基本响应包)
*
* notification_chain 分级处理(D26):
* - webhook / desktop → 一期实现(标注 sent)
* - dingtalk / wechat / email → pending_external
* - phone → manual_only
*/
const { INITIAL_RESPONSE_STATUS } = require("./types.cjs");
// ── 三层保护内部状态 ──
const _DEBOUNCE_TTL_MS = 5 * 60 * 1000; // 去抖 5 分钟
const _MAX_CONCURRENT = 3; // 并发上限
const _AI_TIMEOUT_MS = 15000; // AI 超时 15s
const _debounceCache = new Map(); // alert_id → { result, expireAt }
let _activeCount = 0;
const _queue = [];
// ── System Prompt(响应包专用)──────────────────────
const INITIAL_RESPONSE_SYSTEM_PROMPT = `你是 NetMesh 智能运维平台的 AI 初始响应包生成器。
根据传入的告警详情 + 设备画像 + 匹配预案,生成结构化的初始响应包。
严禁输出外网 URL / 厂商文档链接 / 外部工具推荐。
输出严格 JSON:
{
"quick_checklist": [
{ "priority": 1, "action": "先做什么", "purpose": "为什么" }
],
"possible_causes": [
{ "probability": "high|medium|low", "label": "可能原因", "hint": "用什么命令确认" }
],
"warnings": [ "如果有预案缺失/AI 不可用等情况填这里" ]
}
`;
// ── 主入口 ──────────────────────────────────────────
/**
* 生成初始响应包
* @param {object} alert - alertRecord(含 device_id, metric, level 等)
* @param {object} deviceProfile - 设备画像
* @param {object} matchedSnapshot - 匹配到的 device_plan(双轨)
* @param {object} deps - { readFn, writeFn, aiBridge }
* @returns {Promise<object>} initial_response 结构
*/
async function generate(alert, deviceProfile, matchedSnapshot, deps) {
const debounceKey = `initial_response:${alert.id}`;
const now = Date.now();
// ① 去抖检查
const cached = _debounceCache.get(debounceKey);
if (cached && cached.expireAt > now) {
console.info("[initialResponse] skip: 5min 内已生成过");
return cached.result;
}
// ② 限流检查
if (_activeCount >= _MAX_CONCURRENT) {
console.info("[initialResponse] queue: 并发已满,等待");
return new Promise((resolve) => {
_queue.push({ fn: () => generate(alert, deviceProfile, matchedSnapshot, deps), resolve });
});
}
_activeCount++;
try {
// ③ 超时控制
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), _AI_TIMEOUT_MS);
// 降级检查:AI provider 是否可用
const bridge = deps?.aiBridge || null;
const providerConfigs = bridge?.providerConfigs || null;
const hasProvider = Array.isArray(providerConfigs) && providerConfigs.some((p) => p.enabled !== false);
let responseBody;
if (hasProvider) {
try {
responseBody = await _callAi(bridge, alert, deviceProfile, matchedSnapshot, controller.signal);
clearTimeout(timeoutId);
} catch (err) {
clearTimeout(timeoutId);
console.warn("[initialResponse] AI 调用失败/超时,降级:", err.message);
responseBody = _fallbackGenerate(alert, deviceProfile, matchedSnapshot, { reason: err.message });
}
} else {
console.info("[initialResponse] 降级: AI provider 未配置");
responseBody = _fallbackGenerate(alert, deviceProfile, matchedSnapshot, { reason: "no_provider" });
}
// 追加通知链处理(D26)
const notifications = _resolveNotificationChain(matchedSnapshot?.human_action_guide?.notification_chain);
responseBody.auto_notifications_sent = notifications.auto;
responseBody.pending_notifications = notifications.pending;
// 缓存(带 TTL)
_debounceCache.set(debounceKey, { result: responseBody, expireAt: now + _DEBOUNCE_TTL_MS });
return responseBody;
} finally {
_activeCount--;
// 队列下一个
if (_queue.length > 0) {
const next = _queue.shift();
next.fn().then(next.resolve);
}
}
}
// ── AI 调用 ─────────────────────────────────────────
async function _callAi(bridge, alert, deviceProfile, matchedSnapshot, signal) {
const userContent = JSON.stringify({
alert: {
alert_type: alert.alert_type,
metric_key: alert.metric,
metric_value: alert.metric_value,
threshold: alert.threshold,
level: alert.level,
triggered_at: alert.triggered_at,
message: alert.message,
},
device: {
host_id: alert.device_id,
label: deviceProfile?.label || alert.device_name,
hostname: deviceProfile?.hostname,
catalog_type: deviceProfile?.catalog_type || alert.alert_catalog,
tier: deviceProfile?.tier,
group: deviceProfile?.group,
location: deviceProfile?.location,
},
matched_plan: {
name: matchedSnapshot?.name || "未知预案",
level: matchedSnapshot?.level,
human_action_guide: matchedSnapshot?.human_action_guide ? {
info_gathering: matchedSnapshot.human_action_guide.info_gathering || [],
} : null,
},
});
try {
const provider = (bridge?.providerConfigs || []).find((p) => p.enabled !== false);
if (!provider) throw new Error("no provider");
// 复用 index.cjs 里的 _callAiForRCA 逻辑但简化
const resolved = bridge.resolveProviderApiKey(provider.id);
if (!resolved?.apiKey) throw new Error("api key not available");
const patched = bridge.injectApiKeyIntoRequest(
provider.endpoint || "https://api.minimaxi.com/v1/chat/completions",
{},
provider.id
);
const http = require("http"), https = require("https");
const u = new URL(patched.url);
const lib = patched.url.startsWith("https") ? https : http;
const bodyStr = JSON.stringify({
model: provider.model || "MiniMax-M1",
messages: [
{ role: "system", content: INITIAL_RESPONSE_SYSTEM_PROMPT },
{ role: "user", content: userContent },
],
temperature: 0.3,
max_tokens: 1500,
stream: false,
});
return await new Promise((resolve, reject) => {
const req = lib.request({
hostname: u.hostname, port: u.port || (u.protocol === "https:" ? 443 : 80),
path: u.pathname + u.search, method: "POST",
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${resolved.apiKey}` },
timeout: 15000,
}, (res) => {
let chunks = [];
res.on("data", (c) => chunks.push(c));
res.on("end", () => {
try {
const text = Buffer.concat(chunks).toString();
const parsed = JSON.parse(text);
const content = parsed.choices?.[0]?.message?.content
|| parsed.content?.[0]?.text
|| parsed.candidates?.[0]?.content?.parts?.[0]?.text || "";
let body;
try {
body = _extractJson(content);
body.status = INITIAL_RESPONSE_STATUS.COMPLETE;
} catch {
body = _fallbackGenerate(null, null, null, { reason: "ai_invalid_json" });
body.status = INITIAL_RESPONSE_STATUS.PARTIAL_AI_INVALID;
body.warnings.push("AI 返回内容无法解析为 JSON");
}
resolve(body);
} catch (e) { reject(e); }
});
});
req.on("error", reject);
req.on("timeout", () => { req.destroy(); reject(new Error("AI 请求超时")); });
if (signal) signal.addEventListener("abort", () => req.destroy(new Error("aborted")));
req.write(bodyStr);
req.end();
});
} catch (err) {
throw err;
}
}
function _extractJson(text) {
let cleaned = (text || "")
.replace(/```json\s*/gi, "")
.replace(/```\s*/g, "")
.replace(/<think>[\s\S]*?<\/think>/gi, "")
.trim();
try { return JSON.parse(cleaned); }
catch {
const m = cleaned.match(/\{[\s\S]*\}/);
if (m) return JSON.parse(m[0]);
throw new Error("no json found");
}
}
// ── Fallback 生成(不走 AI)──────────────────────────
function _fallbackGenerate(alert, deviceProfile, matchedSnapshot, opts = {}) {
return {
alert_id: alert?.id || "",
generated_at: Date.now(),
generator: "fallback",
status: opts.reason ? INITIAL_RESPONSE_STATUS.PARTIAL_FALLBACK : INITIAL_RESPONSE_STATUS.COMPLETE_FALLBACK,
device: deviceProfile ? {
host_id: alert?.device_id,
label: deviceProfile.label || alert?.device_name,
hostname: deviceProfile.hostname,
catalog_type: deviceProfile.catalog_type,
tier: deviceProfile.tier,
group: deviceProfile.group,
location: deviceProfile.location,
} : null,
alert: alert ? {
alert_type: alert.alert_type || null,
metric_key: alert.metric,
metric_value: alert.metric_value,
threshold: alert.threshold,
level: alert.level,
triggered_at: alert.triggered_at,
} : null,
quick_checklist: matchedSnapshot?.human_action_guide?.info_gathering || [],
matched_plan: {
name: matchedSnapshot?.name || "未知预案",
level: matchedSnapshot?.level,
},
possible_causes: [], // AI 推断字段 fallback 不填
warnings: opts.reason ? [`AI 不可用,响应包为 fallback 组装:${opts.reason}`] : [],
};
}
// ── notification_chain 分级处理(D26)────────────────
function _resolveNotificationChain(chain) {
const auto = [];
const pending = [];
if (!Array.isArray(chain)) return { auto, pending };
for (const item of chain) {
if (!item.auto) continue;
switch (item.channel) {
case "webhook":
auto.push({ role: item.role, channel: "webhook", status: "sent" });
break;
case "desktop":
auto.push({ role: item.role, channel: "desktop", status: "sent" });
break;
case "dingtalk":
case "wechat":
case "email":
auto.push({ role: item.role, channel: item.channel, status: "pending_external", note: "需外置通知网关(P5 实现)" });
pending.push({ ...item, status: "pending_external" });
break;
case "phone":
pending.push({ ...item, status: "manual_only" });
break;
default:
break;
}
}
return { auto, pending };
}
// ── 清理(测试用)────────────────────────────────────
function _resetForTest() {
_debounceCache.clear();
_activeCount = 0;
_queue.length = 0;
}
module.exports = {
generate,
_resetForTest,
// 常量
_DEBOUNCE_TTL_MS,
_MAX_CONCURRENT,
_AI_TIMEOUT_MS,
};

View File

@@ -0,0 +1,307 @@
"use strict";
/**
* AI 预案生成器(planGenerator)
*
* 输入:运维一句话描述(如 "核心交换机 CPU 超 90% 持续 5 分钟,先 display cpu-usage,
* 持续不降就调阈值")
* 输出:符合 devicePlan schema 的 JSON 草稿,status=draft,走三重校验
*
* 三重校验(D14):
* ① schema 验证(基础字段完整性)
* ② commandCatalog 交叉校验(command_id 必须在目录中)
* ③ blocked_patterns 黑名单反查
*
* Prompt 上下文(D15):显式注入 command_catalog + catalog_type 字典,
* 让 AI 查表而不是瞎猜 device_type、command_id、template variable
*/
const { PLAN_STATUS } = require("./types.cjs");
const PLAN_GENERATION_SYSTEM_PROMPT = `你是 NetMesh 智能运维平台的 AI 预案生成专家。
根据运维一句话描述,生成结构化的设备预案草稿(device_plan),包含双轨:
① human_action_guide:值班员动作预案(人看的流程)
② remediation:操作级预案(机器执行的命令步骤)
【命令目录 - 你只能从这些里面选 command_id】
{{COMMAND_CATALOG_TABLE}}
【设备类型字典】
{{DEVICE_TYPES_TABLE}}
【告警级别】critical | warning | info
【严格 JSON 输出】
{
"device_type": "huawei_h3c_switch",
"level": "critical",
"description": "一句话预案说明",
"match": {
"metric_rules": [{ "metric_key": "cpu_usage", "op": ">", "value": 90 }],
"alert_types": ["performance_high"],
"device_tiers": ["core"]
},
"scope": { "host_ids": [], "host_groups": [] },
"human_action_guide": {
"receiving_alerts": ["..."],
"info_gathering": [{ "priority": 1, "action": "...", "purpose": "..." }],
"notification_chain": [{ "role": "...", "channel": "dingtalk", "trigger": "收到告警立即", "contact": "...", "auto": true }],
"escalation_triggers": [{ "condition": "...", "target_level": "critical" }],
"pre_remediation": ["..."],
"post_remediation": ["..."]
},
"remediation": {
"steps": [
{ "order": 1, "action": "command", "command_id": "switch_show_cpu", "execution": "auto", "on_success": "next", "on_failure": "stop" }
]
},
"security": {
"allowed_command_ids": ["switch_show_cpu"],
"allowed_commands": ["show *", "display *"],
"blocked_patterns": ["shutdown", "reload"]
}
}
【规则】
- command_id 必须从上面的命令目录中选取,严禁臆造
- 严禁输出 blocked_patterns 中的危险命令
- remediation steps 至少 1 条,最多 10 条
- 严禁输出外网 URL / 厂商文档链接
`;
/**
* 生成预案草稿
* @param {string} description - 运维自然语言描述
* @param {object} deps - { aiBridge, commandCatalogStore, devicePlanStore, templateStore }
* @returns {Promise<object>} { plan: devicePlan 草稿, warnings: [], validation: {} }
*/
async function generatePlan(description, deps) {
const { aiBridge, commandCatalogStore, devicePlanStore, templateStore } = deps;
if (!aiBridge?.providerConfigs?.some((p) => p.enabled !== false)) {
throw new Error("AI provider 未配置,无法生成预案");
}
// 构建 Prompt 上下文
const catalogTable = _buildCatalogTable(commandCatalogStore);
const deviceTypesTable = _buildDeviceTypesTable();
const systemPrompt = PLAN_GENERATION_SYSTEM_PROMPT
.replace("{{COMMAND_CATALOG_TABLE}}", catalogTable)
.replace("{{DEVICE_TYPES_TABLE}}", deviceTypesTable);
// 调 AI
let rawText;
try {
rawText = await _callAi(aiBridge, systemPrompt, description);
} catch (e) {
throw new Error(`AI 调用失败: ${e.message}`);
}
// 解析 JSON
let parsed;
try { parsed = _extractJson(rawText); }
catch { throw new Error("AI 返回内容无法解析为 JSON"); }
// 三重校验
const validation = _validatePlan(parsed, commandCatalogStore);
// 组装为 devicePlan(status=draft, source=ai)
const now = Date.now();
const draft = {
id: `plan_ai_${now}_${Math.random().toString(36).slice(2, 8)}`,
template_id: null,
device_type: parsed.device_type || "any",
level: parsed.level || "warning",
enabled: true,
builtin: false,
status: PLAN_STATUS.DRAFT,
source: "ai",
ai_provider: _detectProvider(aiBridge),
ai_prompt_version: "v2",
reject_reason: null,
review_log: [{ ts: now, actor: "system", action: "ai_generate", reason: null }],
description: parsed.description || description,
match: parsed.match || { metric_rules: [], alert_types: [], device_tiers: [], device_tags: [] },
scope: parsed.scope || { host_ids: [], host_groups: [] },
human_action_guide: parsed.human_action_guide || _emptyHumanActionGuide(),
remediation: parsed.remediation || { steps: [] },
security: parsed.security || { allowed_command_ids: [], allowed_commands: [], blocked_patterns: [] },
escalation_fallback: null,
required_role: "operator",
created_at: now,
updated_at: now,
};
return {
plan: draft,
warnings: validation.warnings,
validation: {
schema_valid: validation.schemaValid,
command_catalog_valid: validation.commandCatalogValid,
blocked_patterns_valid: validation.blockedPatternsValid,
},
};
}
function _buildCatalogTable(commandCatalogStore) {
let list = [];
try {
const _readFn = () => null; // 不依赖存储,用默认值
list = commandCatalogStore.listCommands(_readFn);
} catch {
list = [];
}
if (list.length === 0) return "(暂无预置命令目录,AI 只能用 show_cpu, show_mem, show_disk, show_version, collect_diagnostic, backup_config, adjust_threshold 等通用 id)";
let table = "| command_id | 设备类型 | 风险 | 权限 | 命令 |\n|---|---|---|---|---|\n";
for (const c of list) {
table += `| ${c.id} | ${c.device_types.join(", ")} | ${c.risk_level} | ${c.execution} | ${c.command} |\n`;
}
return table;
}
function _buildDeviceTypesTable() {
return "| device_type | 说明 |\n|---|---|\n| huawei_h3c_switch | 华为/H3C 交换机 |\n| cisco_switch | Cisco 交换机 |\n| huawei_firewall | 华为防火墙 |\n| fortinet_firewall | 飞塔防火墙 |\n| linux | Linux 服务器 |\n| windows | Windows 服务器 |";
}
async function _callAi(bridge, systemPrompt, userContent) {
const provider = (bridge?.providerConfigs || []).find((p) => p.enabled !== false);
if (!provider) throw new Error("no provider");
const resolved = bridge.resolveProviderApiKey(provider.id);
if (!resolved?.apiKey) throw new Error("api key not available");
const patched = bridge.injectApiKeyIntoRequest(
provider.endpoint || "https://api.minimaxi.com/v1/chat/completions",
{},
provider.id
);
const http = require("http"), https = require("https");
const u = new URL(patched.url);
const lib = patched.url.startsWith("https") ? https : http;
const bodyStr = JSON.stringify({
model: provider.model || "MiniMax-M1",
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: userContent },
],
temperature: 0.3,
max_tokens: 2000,
stream: false,
});
return await new Promise((resolve, reject) => {
const req = lib.request({
hostname: u.hostname, port: u.port || (u.protocol === "https:" ? 443 : 80),
path: u.pathname + u.search, method: "POST",
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${resolved.apiKey}` },
timeout: 60000,
}, (res) => {
let chunks = [];
res.on("data", (c) => chunks.push(c));
res.on("end", () => {
try {
const text = Buffer.concat(chunks).toString();
const parsed = JSON.parse(text);
const content = parsed.choices?.[0]?.message?.content
|| parsed.content?.[0]?.text
|| parsed.candidates?.[0]?.content?.parts?.[0]?.text || "";
resolve(content.trim());
} catch (e) { reject(e); }
});
});
req.on("error", reject);
req.on("timeout", () => { req.destroy(); reject(new Error("timeout")); });
req.write(bodyStr);
req.end();
});
}
function _extractJson(text) {
let cleaned = (text || "")
.replace(/```json\s*/gi, "")
.replace(/```\s*/g, "")
.replace(/<think>[\s\S]*?<\/think>/gi, "")
.trim();
try { return JSON.parse(cleaned); }
catch {
const m = cleaned.match(/\{[\s\S]*\}/);
if (m) return JSON.parse(m[0]);
throw new Error("no json found");
}
}
/**
* 三重校验(D14)
*/
function _validatePlan(plan, commandCatalogStore) {
const warnings = [];
let schemaValid = true;
let commandCatalogValid = true;
let blockedPatternsValid = true;
// ① schema 验证
if (!plan.device_type || !plan.level) {
schemaValid = false;
warnings.push("缺少 device_type 或 level");
}
if (!plan.remediation?.steps || plan.remediation.steps.length === 0) {
schemaValid = false;
warnings.push("remediation.steps 为空");
}
// ② command_catalog 交叉校验
const _readFn = () => null;
const commandIds = (plan.remediation?.steps || [])
.filter((s) => s.action === "command")
.map((s) => s.command_id)
.filter(Boolean);
if (commandIds.length > 0) {
const result = commandCatalogStore.validateCommandIds(_readFn, commandIds);
if (!result.valid) {
commandCatalogValid = false;
for (const invalidId of result.invalid) {
warnings.push(`command_id '${invalidId}' 不在预置命令目录中,需运维手动确认`);
}
}
}
// ③ blocked_patterns 黑名单反查
const blockedPatterns = plan.security?.blocked_patterns || [];
const allCommands = (plan.remediation?.steps || [])
.filter((s) => s.action === "command")
.map((s) => s.command || s.command_id);
for (const pat of blockedPatterns) {
for (const cmd of allCommands) {
if (typeof cmd === "string" && new RegExp(pat, "i").test(cmd)) {
blockedPatternsValid = false;
warnings.push(`命令 '${cmd}' 命中黑名单模式 '${pat}'`);
}
}
}
return { schemaValid, commandCatalogValid, blockedPatternsValid, warnings };
}
function _detectProvider(bridge) {
const p = (bridge?.providerConfigs || []).find((x) => x.enabled !== false);
return p ? p.id : null;
}
function _emptyHumanActionGuide() {
return {
receiving_alerts: [],
info_gathering: [],
notification_chain: [],
escalation_triggers: [],
pre_remediation: [],
post_remediation: [],
};
}
module.exports = {
generatePlan,
};

View File

@@ -0,0 +1,171 @@
"use strict";
/**
* 预案模板 CRUD(方案 D7:模板不含匹配条件/双轨步骤,只含公共联动配置)
*
* 模板结构详见设计方案 §4.2
*/
const { STORAGE_KEY_PLAN_TEMPLATES } = require("./storageKeys.cjs");
const { ALERT_LEVELS } = require("./types.cjs");
let _templates = null;
function _ensureLoaded(readFn) {
if (_templates === null) {
const raw = readFn(STORAGE_KEY_PLAN_TEMPLATES);
_templates = raw ? _safeParse(raw, []) : _getDefaultTemplates();
}
}
function _safeParse(text, def) {
if (text === null || text === undefined) return def;
if (typeof text === "object") return text;
try { return JSON.parse(text); }
catch { return def; }
}
function _save(writeFn) {
writeFn(STORAGE_KEY_PLAN_TEMPLATES, JSON.stringify(_templates));
}
function _getDefaultTemplates() {
const now = Date.now();
return [
{
id: "tpl_level3_critical",
name: "三级预案模板(严重)",
level: ALERT_LEVELS.CRITICAL,
builtin: true,
description: "严重级别告警的公共配置:大屏红色闪烁 + 10 分钟自动升级 + 处置需确认",
display: { color: "#ef4444", icon: "alert-octagon", blink: true, sound: "builtin_alarm" },
notification: {
channels: ["desktop", "webhook"],
recipients: [{ type: "webhook", target: "" }],
repeat_interval_minutes: 5,
max_repeats: 12,
},
escalation_fallback: {
auto_escalate_minutes: 30,
escalation_target_level: ALERT_LEVELS.CRITICAL,
},
remediation_defaults: {
require_confirmation: true,
auto_close_after_remediation_recovery: true,
execution_timeout_sec: 120,
},
created_at: now,
updated_at: now,
},
{
id: "tpl_level2_warning",
name: "二级预案模板(警告)",
level: ALERT_LEVELS.WARNING,
builtin: true,
description: "警告级别告警的公共配置:大屏橙色提示 + 60 分钟自动升级",
display: { color: "#f97316", icon: "alert-triangle", blink: false, sound: null },
notification: {
channels: ["desktop"],
recipients: [],
repeat_interval_minutes: 10,
max_repeats: 6,
},
escalation_fallback: {
auto_escalate_minutes: 60,
escalation_target_level: ALERT_LEVELS.WARNING,
},
remediation_defaults: {
require_confirmation: false,
auto_close_after_remediation_recovery: true,
execution_timeout_sec: 120,
},
created_at: now,
updated_at: now,
},
{
id: "tpl_level1_info",
name: "一级预案模板(提示)",
level: ALERT_LEVELS.INFO,
builtin: true,
description: "提示级别告警的公共配置:大屏蓝色静态展示",
display: { color: "#3b82f6", icon: "info", blink: false, sound: null },
notification: {
channels: ["desktop"],
recipients: [],
repeat_interval_minutes: 0,
max_repeats: 1,
},
escalation_fallback: null,
remediation_defaults: {
require_confirmation: false,
auto_close_after_remediation_recovery: true,
execution_timeout_sec: 120,
},
created_at: now,
updated_at: now,
},
];
}
// ---------- CRUD ----------
function listTemplates(readFn) {
_ensureLoaded(readFn);
return [..._templates];
}
function getTemplate(readFn, templateId) {
_ensureLoaded(readFn);
return _templates.find((t) => t.id === templateId) || null;
}
function createTemplate(readFn, writeFn, data) {
_ensureLoaded(readFn);
const tpl = {
id: data.id || `tpl_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
name: data.name,
level: data.level || ALERT_LEVELS.WARNING,
builtin: false,
description: data.description || "",
display: data.display || { color: "#f97316", icon: "alert-triangle", blink: false, sound: null },
notification: data.notification || { channels: ["desktop"], recipients: [], repeat_interval_minutes: 10, max_repeats: 6 },
escalation_fallback: data.escalation_fallback || null,
remediation_defaults: data.remediation_defaults || { require_confirmation: true, auto_close_after_remediation_recovery: true, execution_timeout_sec: 120 },
created_at: Date.now(),
updated_at: Date.now(),
};
_templates.push(tpl);
_save(writeFn);
return { ...tpl };
}
function updateTemplate(readFn, writeFn, templateId, updates) {
_ensureLoaded(readFn);
const idx = _templates.findIndex((t) => t.id === templateId);
if (idx === -1) return null;
if (_templates[idx].builtin) return null; // 内置模板不可改
_templates[idx] = { ..._templates[idx], ...updates, updated_at: Date.now() };
_save(writeFn);
return { ..._templates[idx] };
}
function deleteTemplate(readFn, writeFn, templateId) {
_ensureLoaded(readFn);
const idx = _templates.findIndex((t) => t.id === templateId);
if (idx === -1) return false;
if (_templates[idx].builtin) return false; // 内置模板不可删
_templates.splice(idx, 1);
_save(writeFn);
return true;
}
function _resetForTest() { _templates = null; }
module.exports = {
listTemplates,
getTemplate,
createTemplate,
updateTemplate,
deleteTemplate,
_resetForTest,
};

View File

@@ -0,0 +1,283 @@
"use strict";
/**
* 处置执行引擎(remediationEngine)
*
* 设计原则(D24):
* - 不直接 import harness 的 capabilityTools
* - 通过 aiBridge.requestToolCall(alert_id, command_id, params) 让 Catty Agent 在它自己的 session 上下文里执行
* - 凭据、审计、权限全部走 Catty Agent 现有链路
*
* 执行权限分级(D17):
* - auto → AI 自动执行,值班员可随时暂停
* - confirm → 弹确认框,值班员点允许后执行
* - manual_only → 不调 AI,只展示 checklist,值班员手动完成后勾标记
*/
const { STEP_EXECUTION, REMEDIATION_STATUS } = require("./types.cjs");
// ---------- 内存运行态(处置进行中)----------
const _running = new Map(); // alert_id → { status, steps: [], paused, promise }
/**
* 启动处置
* @param {object} alert - alertRecord(必须已匹配 matched_plan_snapshot)
* @param {object} options - { onStepComplete, onComplete, onError }
* @returns {Promise<object>} 处置结果
*/
async function startRemediation(alert, options = {}) {
if (!alert?.matched_plan_snapshot) {
throw new Error("告警未匹配预案,无法启动处置");
}
const snapshot = alert.matched_plan_snapshot;
const steps = snapshot.remediation?.steps || [];
if (steps.length === 0) {
throw new Error("预案没有处置步骤");
}
// 初始化运行态
const runState = {
alert_id: alert.id,
status: REMEDIATION_STATUS.RUNNING,
paused: false,
started_at: Date.now(),
auto_index: 0, // 正在执行的 auto 步骤索引
steps: steps.map((s) => ({
...s,
status: "pending",
started_at: null,
finished_at: null,
stdout_preview: null,
error: null,
confirmed_by: null,
confirmed_at: null,
})),
};
_running.set(alert.id, runState);
try {
// 顺序执行所有步骤(D10:串行,不做并发)
for (let i = 0; i < runState.steps.length; i++) {
const step = runState.steps[i];
// 暂停检查
if (runState.paused) {
runState.status = REMEDIATION_STATUS.PAUSED;
runState.paused_at = Date.now();
options.onPause?.(runState);
// 等待恢复
await _waitForResume(runState);
runState.status = REMEDIATION_STATUS.RUNNING;
runState.paused_at = null;
}
const stepResult = await _executeStep(alert, snapshot, step, runState, options);
runState.steps[i] = stepResult;
options.onStepComplete?.(stepResult, i, runState);
if (stepResult.status === "failed") {
// on_failure 处理
const onFailure = step.on_failure || "stop";
if (onFailure === "escalate") {
runState.status = REMEDIATION_STATUS.PARTIAL;
break;
}
if (onFailure.startsWith("retry:")) {
const retryCount = parseInt(onFailure.split(":")[1], 10) || 1;
// retry 简化:标记 failed 但继续下一步
continue;
}
// 默认 stop
runState.status = REMEDIATION_STATUS.FAILED;
break;
}
}
if (runState.status === REMEDIATION_STATUS.RUNNING) {
runState.status = REMEDIATION_STATUS.COMPLETED;
runState.completed_at = Date.now();
}
options.onComplete?.(runState);
return { ...runState };
} catch (err) {
runState.status = REMEDIATION_STATUS.FAILED;
runState.error = err.message;
options.onError?.(err, runState);
throw err;
} finally {
_running.delete(alert.id);
}
}
async function _executeStep(alert, snapshot, step, runState, options) {
step.status = "running";
step.started_at = Date.now();
try {
// manual_only:只标记完成,不调 AI
if (step.execution === STEP_EXECUTION.MANUAL_ONLY) {
step.status = "pending_manual";
step.finished_at = null;
return step;
}
// confirm:等待值班员确认
if (step.execution === STEP_EXECUTION.CONFIRM) {
const confirmed = await _requestConfirmation(alert.id, step, options);
if (!confirmed) {
step.status = "skipped";
step.error = "值班员拒绝执行";
step.finished_at = Date.now();
return step;
}
step.confirmed_by = options.operator || "system";
step.confirmed_at = Date.now();
}
// auto 或 confirm 已确认 → 通过 aiBridge 发工具调用
const result = await _callAiTool(alert, snapshot, step);
step.status = result.ok ? "success" : "failed";
step.stdout_preview = (result.stdout || "").slice(0, 500);
step.exit_code = result.exitCode;
step.error = result.ok ? null : (result.stderr || result.error || null);
step.finished_at = Date.now();
return step;
} catch (err) {
step.status = "failed";
step.error = err.message;
step.finished_at = Date.now();
return step;
}
}
/**
* 通过 aiBridge 发工具调用(D24:薄封装,让 Catty Agent 执行)
*/
async function _callAiTool(alert, snapshot, step) {
try {
// aiBridge.requestToolCall 薄封装(在 aiBridge 扩展中实现)
const bridge = require("../aiBridge.cjs");
if (typeof bridge.requestToolCall === "function") {
return await bridge.requestToolCall({
alert_id: alert.id,
device_id: alert.device_id,
command_id: step.command_id,
params: step.command_params || {},
timeout_sec: step.timeout_sec || 30,
});
}
} catch (e) {
console.warn("[remediationEngine] aiBridge.requestToolCall 不可用,降级返回模拟结果:", e.message);
}
// 降级:直接 SSH 执行(如果 aiBridge 不可用)
return await _fallbackSshExecute(alert, step);
}
async function _fallbackSshExecute(alert, step) {
try {
const { execSshCommand } = require("../inspection/inspectionExec.cjs");
// 从 alert 构造 SSH config(简化路径,实际需要从 vault 解密凭据)
const deviceInfo = alert.device_profile || {};
if (!deviceInfo.hostname || !deviceInfo.username) {
return { ok: false, stdout: "", stderr: "无有效 SSH 凭据(aiBridge 不可用)", exitCode: -1 };
}
const sshConfig = {
host: deviceInfo.hostname,
port: deviceInfo.port || 22,
username: deviceInfo.username,
password: deviceInfo.password,
privateKey: deviceInfo.privateKey,
passphrase: deviceInfo.passphrase,
};
// 注意:command_id → command 映射需要 commandCatalog
const { getCommand } = require("./commandCatalogStore.cjs");
const cmdInfo = getCommand(() => null, step.command_id);
const command = cmdInfo?.command || `echo "fallback: ${step.command_id}"`;
const stdout = await execSshCommand(sshConfig, command, { connectTimeoutMs: 15000, execTimeoutMs: 30000 });
return { ok: true, stdout, stderr: "", exitCode: 0 };
} catch (err) {
return { ok: false, stdout: "", stderr: err.message, exitCode: -1 };
}
}
function _requestConfirmation(alertId, step, options) {
return new Promise((resolve) => {
options.onConfirmRequest?.(alertId, step, (result) => resolve(!!result));
// 如果调用方没有提供 onConfirmRequest,默认跳过(测试模式)
if (!options.onConfirmRequest) resolve(true);
});
}
function _waitForResume(runState) {
return new Promise((resolve) => {
const check = () => {
if (!runState.paused) resolve();
else setTimeout(check, 200);
};
check();
});
}
// ---------- 暂停 / 恢复 ----------
function pause(alertId) {
const state = _running.get(alertId);
if (state) {
state.paused = true;
state.status = REMEDIATION_STATUS.PAUSED;
state.paused_at = Date.now();
}
return state ? { ok: true } : { ok: false, error: "处置不在运行中" };
}
function resume(alertId) {
const state = _running.get(alertId);
if (state) {
state.paused = false;
state.status = REMEDIATION_STATUS.RUNNING;
}
return state ? { ok: true } : { ok: false, error: "处置不在运行中" };
}
function cancel(alertId) {
const state = _running.get(alertId);
if (state) {
state.paused = true;
state.status = REMEDIATION_STATUS.CANCELLED;
_running.delete(alertId);
}
return state ? { ok: true } : { ok: false, error: "处置不在运行中" };
}
function getStatus(alertId) {
const state = _running.get(alertId);
return state ? { ...state } : null;
}
// ---------- 自动关闭检查(D25)----------
/**
* 处置完成后检查 auto_close_after_remediation_recovery
* 只有 remediation.status === "completed" 且指标恢复正常时才自动关告警
*/
function checkAutoClose(alert, remediation) {
if (!remediation?.auto_close_after_recovery) return false;
if (remediation.status !== REMEDIATION_STATUS.COMPLETED) return false;
// 简化:实际需要巡检复查指标是否恢复
// 这里只标记为"待复查",由 AlertStateMonitor 定时检查
return false; // 交由 AlertStateMonitor 处理
}
module.exports = {
startRemediation,
pause,
resume,
cancel,
getStatus,
checkAutoClose,
};

View File

@@ -41,4 +41,10 @@ exports.STORAGE_KEY_REPORT_SCHEDULE = `${PREFIX}report_schedule_v1`;
// ---------- 日运行报告 ----------
exports.STORAGE_KEY_DAILY_REPORTS = `${PREFIX}daily_reports_v1`;
// ---------- 预案管理(P0 新增)----------
exports.STORAGE_KEY_PLAN_TEMPLATES = `${PREFIX}plan_templates_v1`;
exports.STORAGE_KEY_DEVICE_PLANS = `${PREFIX}device_plans_v1`;
exports.STORAGE_KEY_AGGREGATION_RULES = `${PREFIX}aggregation_rules_v1`;
exports.STORAGE_KEY_COMMAND_CATALOG = `${PREFIX}command_catalog_v1`;
exports.STORAGE_KEY_PREFIX = PREFIX;

View File

@@ -119,6 +119,68 @@ const METRIC_LABELS = {
session_count: "会话数",
};
// ---------- 告警类型分类(D11:安全类独立分类)----------
const ALERT_TYPES = {
PERFORMANCE_HIGH: "performance_high",
PERFORMANCE_LOW: "performance_low",
CONNECTIVITY: "connectivity",
SECURITY: "security",
CONFIG_CHANGE: "config_change",
EXECUTION_ERROR: "execution_error",
};
const ALERT_TYPE_CATEGORIES = {
performance_high: "performance",
performance_low: "performance",
connectivity: "connectivity",
security: "security", // D11:安全类独立
config_change: "config_change",
execution_error: "connectivity",
};
const ALERT_TYPE_LABELS = {
performance_high: "性能过高",
performance_low: "性能过低",
connectivity: "连通性",
security: "安全",
config_change: "配置变更",
execution_error: "执行错误",
};
// ---------- 处置执行状态 ----------
const REMEDIATION_STATUS = {
PENDING: "pending",
RUNNING: "running",
COMPLETED: "completed",
PARTIAL: "partial",
FAILED: "failed",
CANCELLED: "cancelled",
PAUSED: "paused",
};
// ---------- 处置步骤执行权限(D17)----------
const STEP_EXECUTION = {
AUTO: "auto",
CONFIRM: "confirm",
MANUAL_ONLY: "manual_only",
};
// ---------- 预案状态 ----------
const PLAN_STATUS = {
ACTIVE: "active",
DRAFT: "draft", // AI 生成待审核
DISABLED: "disabled",
};
// ---------- 初始响应包状态 ----------
const INITIAL_RESPONSE_STATUS = {
COMPLETE: "complete",
PARTIAL: "partial",
PARTIAL_AI_INVALID: "partial_ai_invalid",
COMPLETE_FALLBACK: "complete_fallback",
PARTIAL_FALLBACK: "partial_fallback",
};
// ---------- 系统配置默认值 ----------
const SYS_CONFIG_DEFAULTS = {
inspection_default_interval: 5, // 全局默认巡检周期(分钟)
@@ -139,5 +201,14 @@ module.exports = {
DEVICE_CATALOG_TYPES,
DEVICE_CATALOG_LABELS,
METRIC_LABELS,
// P0 新增:告警类型分类
ALERT_TYPES,
ALERT_TYPE_CATEGORIES,
ALERT_TYPE_LABELS,
// P0 新增:处置 / 预案状态
REMEDIATION_STATUS,
STEP_EXECUTION,
PLAN_STATUS,
INITIAL_RESPONSE_STATUS,
SYS_CONFIG_DEFAULTS,
};

View File

@@ -2182,6 +2182,43 @@ function createPreloadApi(ctx) {
ipcRenderer.on("inspection:ruleChanged", handler);
return () => ipcRenderer.removeListener("inspection:ruleChanged", handler);
},
// ========== 预案管理与告警联动(P0 新增)==========
// 预案模板
listPlanTemplates: () => ipcRenderer.invoke("inspection:listPlanTemplates"),
getPlanTemplate: (id) => ipcRenderer.invoke("inspection:getPlanTemplate", id),
createPlanTemplate: (data) => ipcRenderer.invoke("inspection:createPlanTemplate", data),
updatePlanTemplate: (id, updates) => ipcRenderer.invoke("inspection:updatePlanTemplate", id, updates),
deletePlanTemplate: (id) => ipcRenderer.invoke("inspection:deletePlanTemplate", id),
// 设备预案
listDevicePlans: (filters) => ipcRenderer.invoke("inspection:listDevicePlans", filters),
getDevicePlan: (id) => ipcRenderer.invoke("inspection:getDevicePlan", id),
createDevicePlan: (data) => ipcRenderer.invoke("inspection:createDevicePlan", data),
updateDevicePlan: (id, updates) => ipcRenderer.invoke("inspection:updateDevicePlan", id, updates),
deleteDevicePlan: (id) => ipcRenderer.invoke("inspection:deleteDevicePlan", id),
approveDevicePlan: (id, actor) => ipcRenderer.invoke("inspection:approveDevicePlan", id, actor),
rejectDevicePlan: (id, reason, actor) => ipcRenderer.invoke("inspection:rejectDevicePlan", id, reason, actor),
// 聚合规则
listAggregationRules: () => ipcRenderer.invoke("inspection:listAggregationRules"),
createAggregationRule: (data) => ipcRenderer.invoke("inspection:createAggregationRule", data),
updateAggregationRule: (id, updates) => ipcRenderer.invoke("inspection:updateAggregationRule", id, updates),
deleteAggregationRule: (id) => ipcRenderer.invoke("inspection:deleteAggregationRule", id),
// 命令目录
listCommandCatalog: (filters) => ipcRenderer.invoke("inspection:listCommandCatalog", filters),
getCommandCatalog: (id) => ipcRenderer.invoke("inspection:getCommandCatalog", id),
// AI 预案生成
generatePlan: (description) => ipcRenderer.invoke("inspection:generatePlan", description),
// 初始响应包(手动触发 / 重新生成)
regenerateInitialResponse: (alertId) => ipcRenderer.invoke("inspection:regenerateInitialResponse", alertId),
// 处置执行
startRemediation: (alertId) => ipcRenderer.invoke("inspection:startRemediation", alertId),
pauseRemediation: (alertId) => ipcRenderer.invoke("inspection:pauseRemediation", alertId),
resumeRemediation: (alertId) => ipcRenderer.invoke("inspection:resumeRemediation", alertId),
cancelRemediation: (alertId) => ipcRenderer.invoke("inspection:cancelRemediation", alertId),
getRemediationStatus: (alertId) => ipcRenderer.invoke("inspection:getRemediationStatus", alertId),
// AlertStateMonitor
getAlertStateMonitorStatus: () => ipcRenderer.invoke("inspection:getAlertStateMonitorStatus"),
// 匹配测试(前端调试用)
matchPlanForAlert: (alert, deviceProfile) => ipcRenderer.invoke("inspection:matchPlanForAlert", alert, deviceProfile),
},
// ─── 远程 HTTP Server(浏览器访问单机版) ───

View File

@@ -106,6 +106,114 @@ export interface AlertRecord {
resolved_at: number | null;
resolved_reason: string | null;
note: string | null;
// v1.3 预案管理扩展字段
alert_type?: string;
alert_catalog?: string;
matched_plan_id?: string | null;
matched_template_id?: string | null;
matched_plan_snapshot?: DevicePlan | null; // D9 匹配时刻冻结
frozen_at?: number | null;
initial_response?: InitialResponse | null;
remediation?: RemediationRun | null;
escalation?: {
trigger: "aggregation" | "escalation_fallback_timeout";
source_alert_ids: string[];
target_level: string;
escalated_at: number;
} | null;
audit_log?: Array<{
ts: number;
actor: string;
action: string;
detail?: string | null;
}>;
}
// ---------- v1.3 预案管理类型 ----------
export interface DevicePlan {
id: string;
template_id: string | null;
device_type: string;
level: AlertLevel;
enabled: boolean;
builtin: boolean;
status: "active" | "draft" | "disabled";
source: "manual" | "ai";
ai_provider: string | null;
ai_prompt_version: string | null;
reject_reason: string | null;
review_log: Array<{ ts: number; actor: string; action: "approve" | "reject" | "ai_generate"; reason: string | null }>;
description: string;
match: {
metric_rules: Array<{ metric_key: string; op: string; value: string | number }>;
alert_types: string[];
device_tiers: string[];
device_tags: string[];
};
scope: { host_ids: string[]; host_groups: string[] };
human_action_guide: {
receiving_alerts: string[];
info_gathering: Array<{ priority: number; action: string; purpose: string }>;
notification_chain: Array<{ role: string; channel: string; trigger: string; contact: string; auto: boolean }>;
escalation_triggers: Array<{ condition: string; target_level: string }>;
pre_remediation: string[];
post_remediation: string[];
};
remediation: {
steps: Array<{
order: number;
action: "command" | "checklist";
command_id?: string;
command_params?: Record<string, any>;
timeout_sec?: number;
execution: "auto" | "confirm" | "manual_only";
on_success: "next" | "stop" | "skip";
on_failure: "stop" | "escalate" | string;
purpose?: string;
}>;
};
security: {
allowed_command_ids: string[];
allowed_commands: string[];
blocked_patterns: string[];
};
required_role: string;
escalation_fallback: { auto_escalate_minutes: number; escalation_target_level: string } | null;
created_at: number;
updated_at: number;
}
export interface InitialResponse {
alert_id: string;
generated_at: number;
generator: "ai" | "fallback";
status: string;
quick_checklist: Array<{ priority: number; action: string; purpose: string }>;
possible_causes?: Array<{ probability: string; label: string; hint: string }>;
matched_plan: { name: string; level: string };
auto_notifications_sent: Array<{ role: string; channel: string; status: string }>;
pending_notifications: Array<Record<string, any>>;
warnings: string[];
}
export interface RemediationRun {
status: "pending" | "running" | "completed" | "partial" | "failed" | "cancelled" | "paused";
started_at: number;
completed_at?: number;
steps: Array<{
order: number;
status: "pending" | "running" | "success" | "failed" | "skipped" | "pending_manual";
command_id?: string;
execution: "auto" | "confirm" | "manual_only";
stdout_preview?: string | null;
exit_code?: number | null;
error?: string | null;
confirmed_by?: string | null;
confirmed_at?: number | null;
started_at?: number | null;
finished_at?: number | null;
}>;
}
export interface AlertStats {
@@ -627,3 +735,90 @@ export const inspectionSnapshotExportApi = {
exportPdf: (snapshotId: string, aiSummary?: any): Promise<{ ok: boolean; filePath?: string; error?: string }> =>
invoke("exportReportSnapshotPdf", aiSummary ? { snapshotId, aiSummary } : snapshotId),
};
// =============== v1.3 预案管理 ===============
export interface PlanTemplate {
id: string;
name: string;
level: AlertLevel;
builtin: boolean;
description: string;
display: { color: string; icon: string; blink: boolean; sound: string | null };
notification: { channels: string[]; recipients: any[]; repeat_interval_minutes: number; max_repeats: number };
escalation_fallback: { auto_escalate_minutes: number; escalation_target_level: string } | null;
remediation_defaults: { require_confirmation: boolean; auto_close_after_remediation_recovery: boolean; execution_timeout_sec: number };
created_at: number;
updated_at: number;
}
export interface CommandCatalogItem {
id: string;
name: string;
command: string;
device_types: string[];
risk_level: "low" | "medium" | "high";
execution: "auto" | "confirm" | "manual_only";
category: "diagnostic" | "config" | "security" | "backup" | "recovery";
description: string;
builtin: boolean;
created_at: number;
updated_at: number;
}
export const planManagementApi = {
// 预案模板
listTemplates: (): Promise<PlanTemplate[]> => invoke("listPlanTemplates"),
getTemplate: (id: string): Promise<PlanTemplate | null> => invoke("getPlanTemplate", id),
createTemplate: (data: Partial<PlanTemplate>): Promise<PlanTemplate> => invoke("createPlanTemplate", data),
updateTemplate: (id: string, updates: Partial<PlanTemplate>): Promise<PlanTemplate | null> =>
invoke("updatePlanTemplate", id, updates),
deleteTemplate: (id: string): Promise<boolean> => invoke("deletePlanTemplate", id),
// 设备预案
listPlans: (filters?: { status?: string; device_type?: string; level?: string; enabled?: boolean }): Promise<DevicePlan[]> =>
invoke("listDevicePlans", filters || {}),
getPlan: (id: string): Promise<DevicePlan | null> => invoke("getDevicePlan", id),
createPlan: (data: Partial<DevicePlan>): Promise<DevicePlan> => invoke("createDevicePlan", data),
updatePlan: (id: string, updates: Partial<DevicePlan>): Promise<DevicePlan | null> =>
invoke("updateDevicePlan", id, updates),
deletePlan: (id: string): Promise<boolean> => invoke("deleteDevicePlan", id),
approvePlan: (id: string, actor = "admin"): Promise<DevicePlan | null> =>
invoke("approveDevicePlan", id, actor),
rejectPlan: (id: string, reason: string, actor = "admin"): Promise<DevicePlan | null> =>
invoke("rejectDevicePlan", id, reason, actor),
matchPlan: (alert: any, deviceProfile: any): Promise<{ plan: DevicePlan; score: number } | null> =>
invoke("matchPlanForAlert", alert, deviceProfile),
// 聚合规则
listAggregationRules: (): Promise<any[]> => invoke("listAggregationRules"),
createAggregationRule: (data: any): Promise<any> => invoke("createAggregationRule", data),
updateAggregationRule: (id: string, updates: any): Promise<any | null> =>
invoke("updateAggregationRule", id, updates),
deleteAggregationRule: (id: string): Promise<boolean> => invoke("deleteAggregationRule", id),
// 命令目录
listCommandCatalog: (filters?: { device_type?: string; execution?: string; risk_level?: string }): Promise<CommandCatalogItem[]> =>
invoke("listCommandCatalog", filters || {}),
getCommandCatalog: (id: string): Promise<CommandCatalogItem | null> => invoke("getCommandCatalog", id),
// AI 预案生成
generatePlan: (description: string): Promise<{ ok: boolean; plan?: DevicePlan; warnings?: string[]; validation?: any; error?: string }> =>
invoke("generatePlan", description),
// 初始响应包
regenerateInitialResponse: (alertId: string): Promise<{ ok: boolean; response?: any; error?: string }> =>
invoke("regenerateInitialResponse", alertId),
// 处置执行
startRemediation: (alertId: string): Promise<{ ok: boolean; result?: any; error?: string }> =>
invoke("startRemediation", alertId),
pauseRemediation: (alertId: string): Promise<{ ok: boolean }> => invoke("pauseRemediation", alertId),
resumeRemediation: (alertId: string): Promise<{ ok: boolean }> => invoke("resumeRemediation", alertId),
cancelRemediation: (alertId: string): Promise<{ ok: boolean }> => invoke("cancelRemediation", alertId),
getRemediationStatus: (alertId: string): Promise<any | null> => invoke("getRemediationStatus", alertId),
// AlertStateMonitor
getAlertStateMonitorStatus: (): Promise<{ running: boolean; interval_minutes: number; remediation_timeout_minutes: number }> =>
invoke("getAlertStateMonitorStatus"),
};