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