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
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:
307
electron/bridges/inspection/planGenerator.cjs
Normal file
307
electron/bridges/inspection/planGenerator.cjs
Normal 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,
|
||||
};
|
||||
Reference in New Issue
Block a user