feat(plan-management): implement alert-remediation-plan system v1.3
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
This commit is contained in:
2026-10-30 16:55:55 +08:00
committed by trae-solo-agent
parent 1bb5390263
commit a5744e720b
18 changed files with 4109 additions and 21 deletions

View File

@@ -0,0 +1,307 @@
"use strict";
/**
* AI 预案生成器(planGenerator)
*
* 输入:运维一句话描述(如 "核心交换机 CPU 超 90% 持续 5 分钟,先 display cpu-usage,
* 持续不降就调阈值")
* 输出:符合 devicePlan schema 的 JSON 草稿,status=draft,走三重校验
*
* 三重校验(D14):
* ① schema 验证(基础字段完整性)
* ② commandCatalog 交叉校验(command_id 必须在目录中)
* ③ blocked_patterns 黑名单反查
*
* Prompt 上下文(D15):显式注入 command_catalog + catalog_type 字典,
* 让 AI 查表而不是瞎猜 device_type、command_id、template variable
*/
const { PLAN_STATUS } = require("./types.cjs");
const PLAN_GENERATION_SYSTEM_PROMPT = `你是 NetMesh 智能运维平台的 AI 预案生成专家。
根据运维一句话描述,生成结构化的设备预案草稿(device_plan),包含双轨:
① human_action_guide:值班员动作预案(人看的流程)
② remediation:操作级预案(机器执行的命令步骤)
【命令目录 - 你只能从这些里面选 command_id】
{{COMMAND_CATALOG_TABLE}}
【设备类型字典】
{{DEVICE_TYPES_TABLE}}
【告警级别】critical | warning | info
【严格 JSON 输出】
{
"device_type": "huawei_h3c_switch",
"level": "critical",
"description": "一句话预案说明",
"match": {
"metric_rules": [{ "metric_key": "cpu_usage", "op": ">", "value": 90 }],
"alert_types": ["performance_high"],
"device_tiers": ["core"]
},
"scope": { "host_ids": [], "host_groups": [] },
"human_action_guide": {
"receiving_alerts": ["..."],
"info_gathering": [{ "priority": 1, "action": "...", "purpose": "..." }],
"notification_chain": [{ "role": "...", "channel": "dingtalk", "trigger": "收到告警立即", "contact": "...", "auto": true }],
"escalation_triggers": [{ "condition": "...", "target_level": "critical" }],
"pre_remediation": ["..."],
"post_remediation": ["..."]
},
"remediation": {
"steps": [
{ "order": 1, "action": "command", "command_id": "switch_show_cpu", "execution": "auto", "on_success": "next", "on_failure": "stop" }
]
},
"security": {
"allowed_command_ids": ["switch_show_cpu"],
"allowed_commands": ["show *", "display *"],
"blocked_patterns": ["shutdown", "reload"]
}
}
【规则】
- command_id 必须从上面的命令目录中选取,严禁臆造
- 严禁输出 blocked_patterns 中的危险命令
- remediation steps 至少 1 条,最多 10 条
- 严禁输出外网 URL / 厂商文档链接
`;
/**
* 生成预案草稿
* @param {string} description - 运维自然语言描述
* @param {object} deps - { aiBridge, commandCatalogStore, devicePlanStore, templateStore }
* @returns {Promise<object>} { plan: devicePlan 草稿, warnings: [], validation: {} }
*/
async function generatePlan(description, deps) {
const { aiBridge, commandCatalogStore, devicePlanStore, templateStore } = deps;
if (!aiBridge?.providerConfigs?.some((p) => p.enabled !== false)) {
throw new Error("AI provider 未配置,无法生成预案");
}
// 构建 Prompt 上下文
const catalogTable = _buildCatalogTable(commandCatalogStore);
const deviceTypesTable = _buildDeviceTypesTable();
const systemPrompt = PLAN_GENERATION_SYSTEM_PROMPT
.replace("{{COMMAND_CATALOG_TABLE}}", catalogTable)
.replace("{{DEVICE_TYPES_TABLE}}", deviceTypesTable);
// 调 AI
let rawText;
try {
rawText = await _callAi(aiBridge, systemPrompt, description);
} catch (e) {
throw new Error(`AI 调用失败: ${e.message}`);
}
// 解析 JSON
let parsed;
try { parsed = _extractJson(rawText); }
catch { throw new Error("AI 返回内容无法解析为 JSON"); }
// 三重校验
const validation = _validatePlan(parsed, commandCatalogStore);
// 组装为 devicePlan(status=draft, source=ai)
const now = Date.now();
const draft = {
id: `plan_ai_${now}_${Math.random().toString(36).slice(2, 8)}`,
template_id: null,
device_type: parsed.device_type || "any",
level: parsed.level || "warning",
enabled: true,
builtin: false,
status: PLAN_STATUS.DRAFT,
source: "ai",
ai_provider: _detectProvider(aiBridge),
ai_prompt_version: "v2",
reject_reason: null,
review_log: [{ ts: now, actor: "system", action: "ai_generate", reason: null }],
description: parsed.description || description,
match: parsed.match || { metric_rules: [], alert_types: [], device_tiers: [], device_tags: [] },
scope: parsed.scope || { host_ids: [], host_groups: [] },
human_action_guide: parsed.human_action_guide || _emptyHumanActionGuide(),
remediation: parsed.remediation || { steps: [] },
security: parsed.security || { allowed_command_ids: [], allowed_commands: [], blocked_patterns: [] },
escalation_fallback: null,
required_role: "operator",
created_at: now,
updated_at: now,
};
return {
plan: draft,
warnings: validation.warnings,
validation: {
schema_valid: validation.schemaValid,
command_catalog_valid: validation.commandCatalogValid,
blocked_patterns_valid: validation.blockedPatternsValid,
},
};
}
function _buildCatalogTable(commandCatalogStore) {
let list = [];
try {
const _readFn = () => null; // 不依赖存储,用默认值
list = commandCatalogStore.listCommands(_readFn);
} catch {
list = [];
}
if (list.length === 0) return "(暂无预置命令目录,AI 只能用 show_cpu, show_mem, show_disk, show_version, collect_diagnostic, backup_config, adjust_threshold 等通用 id)";
let table = "| command_id | 设备类型 | 风险 | 权限 | 命令 |\n|---|---|---|---|---|\n";
for (const c of list) {
table += `| ${c.id} | ${c.device_types.join(", ")} | ${c.risk_level} | ${c.execution} | ${c.command} |\n`;
}
return table;
}
function _buildDeviceTypesTable() {
return "| device_type | 说明 |\n|---|---|\n| huawei_h3c_switch | 华为/H3C 交换机 |\n| cisco_switch | Cisco 交换机 |\n| huawei_firewall | 华为防火墙 |\n| fortinet_firewall | 飞塔防火墙 |\n| linux | Linux 服务器 |\n| windows | Windows 服务器 |";
}
async function _callAi(bridge, systemPrompt, userContent) {
const provider = (bridge?.providerConfigs || []).find((p) => p.enabled !== false);
if (!provider) throw new Error("no provider");
const resolved = bridge.resolveProviderApiKey(provider.id);
if (!resolved?.apiKey) throw new Error("api key not available");
const patched = bridge.injectApiKeyIntoRequest(
provider.endpoint || "https://api.minimaxi.com/v1/chat/completions",
{},
provider.id
);
const http = require("http"), https = require("https");
const u = new URL(patched.url);
const lib = patched.url.startsWith("https") ? https : http;
const bodyStr = JSON.stringify({
model: provider.model || "MiniMax-M1",
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: userContent },
],
temperature: 0.3,
max_tokens: 2000,
stream: false,
});
return await new Promise((resolve, reject) => {
const req = lib.request({
hostname: u.hostname, port: u.port || (u.protocol === "https:" ? 443 : 80),
path: u.pathname + u.search, method: "POST",
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${resolved.apiKey}` },
timeout: 60000,
}, (res) => {
let chunks = [];
res.on("data", (c) => chunks.push(c));
res.on("end", () => {
try {
const text = Buffer.concat(chunks).toString();
const parsed = JSON.parse(text);
const content = parsed.choices?.[0]?.message?.content
|| parsed.content?.[0]?.text
|| parsed.candidates?.[0]?.content?.parts?.[0]?.text || "";
resolve(content.trim());
} catch (e) { reject(e); }
});
});
req.on("error", reject);
req.on("timeout", () => { req.destroy(); reject(new Error("timeout")); });
req.write(bodyStr);
req.end();
});
}
function _extractJson(text) {
let cleaned = (text || "")
.replace(/```json\s*/gi, "")
.replace(/```\s*/g, "")
.replace(/<think>[\s\S]*?<\/think>/gi, "")
.trim();
try { return JSON.parse(cleaned); }
catch {
const m = cleaned.match(/\{[\s\S]*\}/);
if (m) return JSON.parse(m[0]);
throw new Error("no json found");
}
}
/**
* 三重校验(D14)
*/
function _validatePlan(plan, commandCatalogStore) {
const warnings = [];
let schemaValid = true;
let commandCatalogValid = true;
let blockedPatternsValid = true;
// ① schema 验证
if (!plan.device_type || !plan.level) {
schemaValid = false;
warnings.push("缺少 device_type 或 level");
}
if (!plan.remediation?.steps || plan.remediation.steps.length === 0) {
schemaValid = false;
warnings.push("remediation.steps 为空");
}
// ② command_catalog 交叉校验
const _readFn = () => null;
const commandIds = (plan.remediation?.steps || [])
.filter((s) => s.action === "command")
.map((s) => s.command_id)
.filter(Boolean);
if (commandIds.length > 0) {
const result = commandCatalogStore.validateCommandIds(_readFn, commandIds);
if (!result.valid) {
commandCatalogValid = false;
for (const invalidId of result.invalid) {
warnings.push(`command_id '${invalidId}' 不在预置命令目录中,需运维手动确认`);
}
}
}
// ③ blocked_patterns 黑名单反查
const blockedPatterns = plan.security?.blocked_patterns || [];
const allCommands = (plan.remediation?.steps || [])
.filter((s) => s.action === "command")
.map((s) => s.command || s.command_id);
for (const pat of blockedPatterns) {
for (const cmd of allCommands) {
if (typeof cmd === "string" && new RegExp(pat, "i").test(cmd)) {
blockedPatternsValid = false;
warnings.push(`命令 '${cmd}' 命中黑名单模式 '${pat}'`);
}
}
}
return { schemaValid, commandCatalogValid, blockedPatternsValid, warnings };
}
function _detectProvider(bridge) {
const p = (bridge?.providerConfigs || []).find((x) => x.enabled !== false);
return p ? p.id : null;
}
function _emptyHumanActionGuide() {
return {
receiving_alerts: [],
info_gathering: [],
notification_chain: [],
escalation_triggers: [],
pre_remediation: [],
post_remediation: [],
};
}
module.exports = {
generatePlan,
};