Files
SenMeshNet/electron/bridges/inspection/remediationEngine.cjs

284 lines
8.9 KiB
JavaScript
Raw Permalink Normal View History

"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<object>} 处置结果
*/
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,
};