"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} 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(/[\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, };