Files
SenMeshNet/electron/bridges/inspection/initialResponseGenerator.cjs

318 lines
11 KiB
JavaScript
Raw Permalink Normal View History

"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,
};