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
308 lines
10 KiB
JavaScript
308 lines
10 KiB
JavaScript
"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,
|
||
};
|