"use strict"; /** * 处置执行引擎(remediationEngine) * * 设计原则(D24): * - 不直接 import harness 的 capabilityTools * - 通过 aiBridge.requestToolCall(alert_id, command_id, params) 让 Catty Agent 在它自己的 session 上下文里执行 * - 凭据、审计、权限全部走 Catty Agent 现有链路 * * 执行权限分级(D17): * - auto → AI 自动执行,值班员可随时暂停 * - confirm → 弹确认框,值班员点允许后执行 * - manual_only → 不调 AI,只展示 checklist,值班员手动完成后勾标记 */ const { STEP_EXECUTION, REMEDIATION_STATUS } = require("./types.cjs"); // ---------- 内存运行态(处置进行中)---------- const _running = new Map(); // alert_id → { status, steps: [], paused, promise } /** * 启动处置 * @param {object} alert - alertRecord(必须已匹配 matched_plan_snapshot) * @param {object} options - { onStepComplete, onComplete, onError } * @returns {Promise} 处置结果 */ async function startRemediation(alert, options = {}) { if (!alert?.matched_plan_snapshot) { throw new Error("告警未匹配预案,无法启动处置"); } const snapshot = alert.matched_plan_snapshot; const steps = snapshot.remediation?.steps || []; if (steps.length === 0) { throw new Error("预案没有处置步骤"); } // 初始化运行态 const runState = { alert_id: alert.id, status: REMEDIATION_STATUS.RUNNING, paused: false, started_at: Date.now(), auto_index: 0, // 正在执行的 auto 步骤索引 steps: steps.map((s) => ({ ...s, status: "pending", started_at: null, finished_at: null, stdout_preview: null, error: null, confirmed_by: null, confirmed_at: null, })), }; _running.set(alert.id, runState); try { // 顺序执行所有步骤(D10:串行,不做并发) for (let i = 0; i < runState.steps.length; i++) { const step = runState.steps[i]; // 暂停检查 if (runState.paused) { runState.status = REMEDIATION_STATUS.PAUSED; runState.paused_at = Date.now(); options.onPause?.(runState); // 等待恢复 await _waitForResume(runState); runState.status = REMEDIATION_STATUS.RUNNING; runState.paused_at = null; } const stepResult = await _executeStep(alert, snapshot, step, runState, options); runState.steps[i] = stepResult; options.onStepComplete?.(stepResult, i, runState); if (stepResult.status === "failed") { // on_failure 处理 const onFailure = step.on_failure || "stop"; if (onFailure === "escalate") { runState.status = REMEDIATION_STATUS.PARTIAL; break; } if (onFailure.startsWith("retry:")) { const retryCount = parseInt(onFailure.split(":")[1], 10) || 1; // retry 简化:标记 failed 但继续下一步 continue; } // 默认 stop runState.status = REMEDIATION_STATUS.FAILED; break; } } if (runState.status === REMEDIATION_STATUS.RUNNING) { runState.status = REMEDIATION_STATUS.COMPLETED; runState.completed_at = Date.now(); } options.onComplete?.(runState); return { ...runState }; } catch (err) { runState.status = REMEDIATION_STATUS.FAILED; runState.error = err.message; options.onError?.(err, runState); throw err; } finally { _running.delete(alert.id); } } async function _executeStep(alert, snapshot, step, runState, options) { step.status = "running"; step.started_at = Date.now(); try { // manual_only:只标记完成,不调 AI if (step.execution === STEP_EXECUTION.MANUAL_ONLY) { step.status = "pending_manual"; step.finished_at = null; return step; } // confirm:等待值班员确认 if (step.execution === STEP_EXECUTION.CONFIRM) { const confirmed = await _requestConfirmation(alert.id, step, options); if (!confirmed) { step.status = "skipped"; step.error = "值班员拒绝执行"; step.finished_at = Date.now(); return step; } step.confirmed_by = options.operator || "system"; step.confirmed_at = Date.now(); } // auto 或 confirm 已确认 → 通过 aiBridge 发工具调用 const result = await _callAiTool(alert, snapshot, step); step.status = result.ok ? "success" : "failed"; step.stdout_preview = (result.stdout || "").slice(0, 500); step.exit_code = result.exitCode; step.error = result.ok ? null : (result.stderr || result.error || null); step.finished_at = Date.now(); return step; } catch (err) { step.status = "failed"; step.error = err.message; step.finished_at = Date.now(); return step; } } /** * 通过 aiBridge 发工具调用(D24:薄封装,让 Catty Agent 执行) */ async function _callAiTool(alert, snapshot, step) { try { // aiBridge.requestToolCall 薄封装(在 aiBridge 扩展中实现) const bridge = require("../aiBridge.cjs"); if (typeof bridge.requestToolCall === "function") { return await bridge.requestToolCall({ alert_id: alert.id, device_id: alert.device_id, command_id: step.command_id, params: step.command_params || {}, timeout_sec: step.timeout_sec || 30, }); } } catch (e) { console.warn("[remediationEngine] aiBridge.requestToolCall 不可用,降级返回模拟结果:", e.message); } // 降级:直接 SSH 执行(如果 aiBridge 不可用) return await _fallbackSshExecute(alert, step); } async function _fallbackSshExecute(alert, step) { try { const { execSshCommand } = require("../inspection/inspectionExec.cjs"); // 从 alert 构造 SSH config(简化路径,实际需要从 vault 解密凭据) const deviceInfo = alert.device_profile || {}; if (!deviceInfo.hostname || !deviceInfo.username) { return { ok: false, stdout: "", stderr: "无有效 SSH 凭据(aiBridge 不可用)", exitCode: -1 }; } const sshConfig = { host: deviceInfo.hostname, port: deviceInfo.port || 22, username: deviceInfo.username, password: deviceInfo.password, privateKey: deviceInfo.privateKey, passphrase: deviceInfo.passphrase, }; // 注意:command_id → command 映射需要 commandCatalog const { getCommand } = require("./commandCatalogStore.cjs"); const cmdInfo = getCommand(() => null, step.command_id); const command = cmdInfo?.command || `echo "fallback: ${step.command_id}"`; const stdout = await execSshCommand(sshConfig, command, { connectTimeoutMs: 15000, execTimeoutMs: 30000 }); return { ok: true, stdout, stderr: "", exitCode: 0 }; } catch (err) { return { ok: false, stdout: "", stderr: err.message, exitCode: -1 }; } } function _requestConfirmation(alertId, step, options) { return new Promise((resolve) => { options.onConfirmRequest?.(alertId, step, (result) => resolve(!!result)); // 如果调用方没有提供 onConfirmRequest,默认跳过(测试模式) if (!options.onConfirmRequest) resolve(true); }); } function _waitForResume(runState) { return new Promise((resolve) => { const check = () => { if (!runState.paused) resolve(); else setTimeout(check, 200); }; check(); }); } // ---------- 暂停 / 恢复 ---------- function pause(alertId) { const state = _running.get(alertId); if (state) { state.paused = true; state.status = REMEDIATION_STATUS.PAUSED; state.paused_at = Date.now(); } return state ? { ok: true } : { ok: false, error: "处置不在运行中" }; } function resume(alertId) { const state = _running.get(alertId); if (state) { state.paused = false; state.status = REMEDIATION_STATUS.RUNNING; } return state ? { ok: true } : { ok: false, error: "处置不在运行中" }; } function cancel(alertId) { const state = _running.get(alertId); if (state) { state.paused = true; state.status = REMEDIATION_STATUS.CANCELLED; _running.delete(alertId); } return state ? { ok: true } : { ok: false, error: "处置不在运行中" }; } function getStatus(alertId) { const state = _running.get(alertId); return state ? { ...state } : null; } // ---------- 自动关闭检查(D25)---------- /** * 处置完成后检查 auto_close_after_remediation_recovery * 只有 remediation.status === "completed" 且指标恢复正常时才自动关告警 */ function checkAutoClose(alert, remediation) { if (!remediation?.auto_close_after_recovery) return false; if (remediation.status !== REMEDIATION_STATUS.COMPLETED) return false; // 简化:实际需要巡检复查指标是否恢复 // 这里只标记为"待复查",由 AlertStateMonitor 定时检查 return false; // 交由 AlertStateMonitor 处理 } module.exports = { startRemediation, pause, resume, cancel, getStatus, checkAutoClose, };