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
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:
@@ -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,
|
||||
};
|
||||
|
||||
261
electron/bridges/inspection/aggregationEngine.cjs
Normal file
261
electron/bridges/inspection/aggregationEngine.cjs
Normal 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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
151
electron/bridges/inspection/alertStateMonitor.cjs
Normal file
151
electron/bridges/inspection/alertStateMonitor.cjs
Normal 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,
|
||||
};
|
||||
159
electron/bridges/inspection/commandCatalogStore.cjs
Normal file
159
electron/bridges/inspection/commandCatalogStore.cjs
Normal 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,
|
||||
};
|
||||
268
electron/bridges/inspection/devicePlanStore.cjs
Normal file
268
electron/bridges/inspection/devicePlanStore.cjs
Normal 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,
|
||||
};
|
||||
@@ -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 ───
|
||||
|
||||
317
electron/bridges/inspection/initialResponseGenerator.cjs
Normal file
317
electron/bridges/inspection/initialResponseGenerator.cjs
Normal 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,
|
||||
};
|
||||
307
electron/bridges/inspection/planGenerator.cjs
Normal file
307
electron/bridges/inspection/planGenerator.cjs
Normal 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,
|
||||
};
|
||||
171
electron/bridges/inspection/planTemplateStore.cjs
Normal file
171
electron/bridges/inspection/planTemplateStore.cjs
Normal 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,
|
||||
};
|
||||
283
electron/bridges/inspection/remediationEngine.cjs
Normal file
283
electron/bridges/inspection/remediationEngine.cjs
Normal 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,
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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(浏览器访问单机版) ───
|
||||
|
||||
Reference in New Issue
Block a user