Files
SenMeshNet/electron/bridges/inspection/devicePlanStore.cjs
zhaolei a5744e720b
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
feat(plan-management): implement alert-remediation-plan system v1.3
- 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
2026-10-30 16:55:55 +08:00

269 lines
8.9 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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,
};