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:
317
electron/bridges/inspection/initialResponseGenerator.cjs
Normal file
317
electron/bridges/inspection/initialResponseGenerator.cjs
Normal file
@@ -0,0 +1,317 @@
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* 初始响应包生成器(initialResponseGenerator)
|
||||
*
|
||||
* 三层性能保护(D22):
|
||||
* ① 去抖:同一条告警 5 分钟内只生成一次
|
||||
* ② 限流:全局最多 3 个并发 AI 调用,超出的进入 PromiseQueue 排队
|
||||
* ③ 超时降级:15s 硬超时 + AI 不可用 fallback(不走 AI 也能产出基本响应包)
|
||||
*
|
||||
* notification_chain 分级处理(D26):
|
||||
* - webhook / desktop → 一期实现(标注 sent)
|
||||
* - dingtalk / wechat / email → pending_external
|
||||
* - phone → manual_only
|
||||
*/
|
||||
|
||||
const { INITIAL_RESPONSE_STATUS } = require("./types.cjs");
|
||||
|
||||
// ── 三层保护内部状态 ──
|
||||
const _DEBOUNCE_TTL_MS = 5 * 60 * 1000; // 去抖 5 分钟
|
||||
const _MAX_CONCURRENT = 3; // 并发上限
|
||||
const _AI_TIMEOUT_MS = 15000; // AI 超时 15s
|
||||
|
||||
const _debounceCache = new Map(); // alert_id → { result, expireAt }
|
||||
let _activeCount = 0;
|
||||
const _queue = [];
|
||||
|
||||
// ── System Prompt(响应包专用)──────────────────────
|
||||
|
||||
const INITIAL_RESPONSE_SYSTEM_PROMPT = `你是 NetMesh 智能运维平台的 AI 初始响应包生成器。
|
||||
|
||||
根据传入的告警详情 + 设备画像 + 匹配预案,生成结构化的初始响应包。
|
||||
严禁输出外网 URL / 厂商文档链接 / 外部工具推荐。
|
||||
|
||||
输出严格 JSON:
|
||||
{
|
||||
"quick_checklist": [
|
||||
{ "priority": 1, "action": "先做什么", "purpose": "为什么" }
|
||||
],
|
||||
"possible_causes": [
|
||||
{ "probability": "high|medium|low", "label": "可能原因", "hint": "用什么命令确认" }
|
||||
],
|
||||
"warnings": [ "如果有预案缺失/AI 不可用等情况填这里" ]
|
||||
}
|
||||
`;
|
||||
|
||||
// ── 主入口 ──────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* 生成初始响应包
|
||||
* @param {object} alert - alertRecord(含 device_id, metric, level 等)
|
||||
* @param {object} deviceProfile - 设备画像
|
||||
* @param {object} matchedSnapshot - 匹配到的 device_plan(双轨)
|
||||
* @param {object} deps - { readFn, writeFn, aiBridge }
|
||||
* @returns {Promise<object>} initial_response 结构
|
||||
*/
|
||||
async function generate(alert, deviceProfile, matchedSnapshot, deps) {
|
||||
const debounceKey = `initial_response:${alert.id}`;
|
||||
const now = Date.now();
|
||||
|
||||
// ① 去抖检查
|
||||
const cached = _debounceCache.get(debounceKey);
|
||||
if (cached && cached.expireAt > now) {
|
||||
console.info("[initialResponse] skip: 5min 内已生成过");
|
||||
return cached.result;
|
||||
}
|
||||
|
||||
// ② 限流检查
|
||||
if (_activeCount >= _MAX_CONCURRENT) {
|
||||
console.info("[initialResponse] queue: 并发已满,等待");
|
||||
return new Promise((resolve) => {
|
||||
_queue.push({ fn: () => generate(alert, deviceProfile, matchedSnapshot, deps), resolve });
|
||||
});
|
||||
}
|
||||
_activeCount++;
|
||||
|
||||
try {
|
||||
// ③ 超时控制
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), _AI_TIMEOUT_MS);
|
||||
|
||||
// 降级检查:AI provider 是否可用
|
||||
const bridge = deps?.aiBridge || null;
|
||||
const providerConfigs = bridge?.providerConfigs || null;
|
||||
const hasProvider = Array.isArray(providerConfigs) && providerConfigs.some((p) => p.enabled !== false);
|
||||
|
||||
let responseBody;
|
||||
if (hasProvider) {
|
||||
try {
|
||||
responseBody = await _callAi(bridge, alert, deviceProfile, matchedSnapshot, controller.signal);
|
||||
clearTimeout(timeoutId);
|
||||
} catch (err) {
|
||||
clearTimeout(timeoutId);
|
||||
console.warn("[initialResponse] AI 调用失败/超时,降级:", err.message);
|
||||
responseBody = _fallbackGenerate(alert, deviceProfile, matchedSnapshot, { reason: err.message });
|
||||
}
|
||||
} else {
|
||||
console.info("[initialResponse] 降级: AI provider 未配置");
|
||||
responseBody = _fallbackGenerate(alert, deviceProfile, matchedSnapshot, { reason: "no_provider" });
|
||||
}
|
||||
|
||||
// 追加通知链处理(D26)
|
||||
const notifications = _resolveNotificationChain(matchedSnapshot?.human_action_guide?.notification_chain);
|
||||
responseBody.auto_notifications_sent = notifications.auto;
|
||||
responseBody.pending_notifications = notifications.pending;
|
||||
|
||||
// 缓存(带 TTL)
|
||||
_debounceCache.set(debounceKey, { result: responseBody, expireAt: now + _DEBOUNCE_TTL_MS });
|
||||
return responseBody;
|
||||
} finally {
|
||||
_activeCount--;
|
||||
// 队列下一个
|
||||
if (_queue.length > 0) {
|
||||
const next = _queue.shift();
|
||||
next.fn().then(next.resolve);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── AI 调用 ─────────────────────────────────────────
|
||||
|
||||
async function _callAi(bridge, alert, deviceProfile, matchedSnapshot, signal) {
|
||||
const userContent = JSON.stringify({
|
||||
alert: {
|
||||
alert_type: alert.alert_type,
|
||||
metric_key: alert.metric,
|
||||
metric_value: alert.metric_value,
|
||||
threshold: alert.threshold,
|
||||
level: alert.level,
|
||||
triggered_at: alert.triggered_at,
|
||||
message: alert.message,
|
||||
},
|
||||
device: {
|
||||
host_id: alert.device_id,
|
||||
label: deviceProfile?.label || alert.device_name,
|
||||
hostname: deviceProfile?.hostname,
|
||||
catalog_type: deviceProfile?.catalog_type || alert.alert_catalog,
|
||||
tier: deviceProfile?.tier,
|
||||
group: deviceProfile?.group,
|
||||
location: deviceProfile?.location,
|
||||
},
|
||||
matched_plan: {
|
||||
name: matchedSnapshot?.name || "未知预案",
|
||||
level: matchedSnapshot?.level,
|
||||
human_action_guide: matchedSnapshot?.human_action_guide ? {
|
||||
info_gathering: matchedSnapshot.human_action_guide.info_gathering || [],
|
||||
} : null,
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
const provider = (bridge?.providerConfigs || []).find((p) => p.enabled !== false);
|
||||
if (!provider) throw new Error("no provider");
|
||||
|
||||
// 复用 index.cjs 里的 _callAiForRCA 逻辑但简化
|
||||
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: INITIAL_RESPONSE_SYSTEM_PROMPT },
|
||||
{ role: "user", content: userContent },
|
||||
],
|
||||
temperature: 0.3,
|
||||
max_tokens: 1500,
|
||||
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: 15000,
|
||||
}, (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 || "";
|
||||
|
||||
let body;
|
||||
try {
|
||||
body = _extractJson(content);
|
||||
body.status = INITIAL_RESPONSE_STATUS.COMPLETE;
|
||||
} catch {
|
||||
body = _fallbackGenerate(null, null, null, { reason: "ai_invalid_json" });
|
||||
body.status = INITIAL_RESPONSE_STATUS.PARTIAL_AI_INVALID;
|
||||
body.warnings.push("AI 返回内容无法解析为 JSON");
|
||||
}
|
||||
resolve(body);
|
||||
} catch (e) { reject(e); }
|
||||
});
|
||||
});
|
||||
req.on("error", reject);
|
||||
req.on("timeout", () => { req.destroy(); reject(new Error("AI 请求超时")); });
|
||||
if (signal) signal.addEventListener("abort", () => req.destroy(new Error("aborted")));
|
||||
req.write(bodyStr);
|
||||
req.end();
|
||||
});
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
// ── Fallback 生成(不走 AI)──────────────────────────
|
||||
|
||||
function _fallbackGenerate(alert, deviceProfile, matchedSnapshot, opts = {}) {
|
||||
return {
|
||||
alert_id: alert?.id || "",
|
||||
generated_at: Date.now(),
|
||||
generator: "fallback",
|
||||
status: opts.reason ? INITIAL_RESPONSE_STATUS.PARTIAL_FALLBACK : INITIAL_RESPONSE_STATUS.COMPLETE_FALLBACK,
|
||||
device: deviceProfile ? {
|
||||
host_id: alert?.device_id,
|
||||
label: deviceProfile.label || alert?.device_name,
|
||||
hostname: deviceProfile.hostname,
|
||||
catalog_type: deviceProfile.catalog_type,
|
||||
tier: deviceProfile.tier,
|
||||
group: deviceProfile.group,
|
||||
location: deviceProfile.location,
|
||||
} : null,
|
||||
alert: alert ? {
|
||||
alert_type: alert.alert_type || null,
|
||||
metric_key: alert.metric,
|
||||
metric_value: alert.metric_value,
|
||||
threshold: alert.threshold,
|
||||
level: alert.level,
|
||||
triggered_at: alert.triggered_at,
|
||||
} : null,
|
||||
quick_checklist: matchedSnapshot?.human_action_guide?.info_gathering || [],
|
||||
matched_plan: {
|
||||
name: matchedSnapshot?.name || "未知预案",
|
||||
level: matchedSnapshot?.level,
|
||||
},
|
||||
possible_causes: [], // AI 推断字段 fallback 不填
|
||||
warnings: opts.reason ? [`AI 不可用,响应包为 fallback 组装:${opts.reason}`] : [],
|
||||
};
|
||||
}
|
||||
|
||||
// ── notification_chain 分级处理(D26)────────────────
|
||||
|
||||
function _resolveNotificationChain(chain) {
|
||||
const auto = [];
|
||||
const pending = [];
|
||||
if (!Array.isArray(chain)) return { auto, pending };
|
||||
|
||||
for (const item of chain) {
|
||||
if (!item.auto) continue;
|
||||
switch (item.channel) {
|
||||
case "webhook":
|
||||
auto.push({ role: item.role, channel: "webhook", status: "sent" });
|
||||
break;
|
||||
case "desktop":
|
||||
auto.push({ role: item.role, channel: "desktop", status: "sent" });
|
||||
break;
|
||||
case "dingtalk":
|
||||
case "wechat":
|
||||
case "email":
|
||||
auto.push({ role: item.role, channel: item.channel, status: "pending_external", note: "需外置通知网关(P5 实现)" });
|
||||
pending.push({ ...item, status: "pending_external" });
|
||||
break;
|
||||
case "phone":
|
||||
pending.push({ ...item, status: "manual_only" });
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return { auto, pending };
|
||||
}
|
||||
|
||||
// ── 清理(测试用)────────────────────────────────────
|
||||
|
||||
function _resetForTest() {
|
||||
_debounceCache.clear();
|
||||
_activeCount = 0;
|
||||
_queue.length = 0;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
generate,
|
||||
_resetForTest,
|
||||
// 常量
|
||||
_DEBOUNCE_TTL_MS,
|
||||
_MAX_CONCURRENT,
|
||||
_AI_TIMEOUT_MS,
|
||||
};
|
||||
Reference in New Issue
Block a user