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:
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,
|
||||
};
|
||||
Reference in New Issue
Block a user