"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, };