Files
SenMeshNet/electron/bridges/inspection/remediationEngine.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

284 lines
8.9 KiB
JavaScript
Raw 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";
/**
* 处置执行引擎(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,
};