Files
SenMeshNet/electron/bridges/inspection/initialResponseGenerator.cjs
zhaolei a5744e720b
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
feat(plan-management): implement alert-remediation-plan system v1.3
- 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
2026-10-30 16:55:55 +08:00

318 lines
11 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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,
};