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
262 lines
7.7 KiB
JavaScript
262 lines
7.7 KiB
JavaScript
"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,
|
||
};
|