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
- 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
284 lines
8.9 KiB
JavaScript
284 lines
8.9 KiB
JavaScript
"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,
|
||
};
|