"use client"; /** * 巡检报告 AI 汇总 * * 复用 RCA runner 的 AI provider 读取(localStorage netcatty_ai_providers_v1) * 通过 window.netcatty.aiFetch bridge 发 HTTP 请求到用户配置的 LLM endpoint。 * * Prompt 四段式输出,JSON 结构化: * { health_summary, anomalies[], trend_notes[], recommendations[] } */ type ProviderInfo = { providerId: string; apiKey: string; baseUrl: string; model: string; style: "openai" | "google" | "anthropic"; }; const AI_PROVIDERS_KEY = "netcatty_ai_providers_v1"; const AI_ACTIVE_PROVIDER_KEY = "netcatty_ai_active_provider_v1"; async function resolveProvider(): Promise { let providers: any[] | null = null; let activeId: string | null = null; try { const raw = localStorage.getItem(AI_PROVIDERS_KEY); if (raw) providers = JSON.parse(raw); activeId = localStorage.getItem(AI_ACTIVE_PROVIDER_KEY); } catch (_) {} if (!Array.isArray(providers) || providers.length === 0) return null; let primary: any = null; if (activeId) primary = providers.find((p) => p.id === activeId); if (!primary) primary = providers.find((p) => p.enabled !== false); if (!primary?.apiKey) return null; // 同步 providers 到主进程(RCA 路径也需要) const api = (window as any).netcatty; if (api?.aiSyncProviders) { try { await api.aiSyncProviders(providers); } catch (_) {} } const lower = (primary.baseURL || primary.providerId || "").toLowerCase(); const style = /anthropic|claude/.test(lower) ? "anthropic" : /google|gemini/.test(lower) ? "google" : "openai"; const suffix = style === "anthropic" ? "/v1/messages" : style === "google" ? "/v1beta/models" : "/chat/completions"; return { providerId: primary.id || "default", apiKey: primary.apiKey, baseUrl: (primary.baseURL || "").replace(/\/$/, "") + suffix, model: primary.defaultModel || "gpt-4o-mini", style, }; } async function aiChatCompletion( provider: ProviderInfo, systemPrompt: string, userContent: string, ): Promise { const api = (window as any).netcatty; if (!api?.aiFetch) throw new Error("aiFetch bridge unavailable"); const b = (window as any).netcatty; if (b?.aiAllowlistAddHost) { try { await b.aiAllowlistAddHost(provider.baseUrl); } catch (_) {} } let url = provider.baseUrl; const body: any = { model: provider.model, temperature: 0.3, }; // 用 placeholder — 主进程 injectApiKeyIntoRequest 会根据 providerId 解密注入真实 key const headers: Record = { "Content-Type": "application/json", "Authorization": "Bearer __IPC_SECURED__", }; if (provider.style === "google") { url = `${provider.baseUrl}/${provider.model}:generateContent`; delete headers.Authorization; headers["x-goog-api-key"] = "__IPC_SECURED__"; body.contents = [{ role: "user", parts: [{ text: `${systemPrompt}\n\n${userContent}` }], }]; } else if (provider.style === "anthropic") { headers["x-api-key"] = "__IPC_SECURED__"; headers["anthropic-version"] = "2023-06-01"; delete headers.Authorization; body.model = provider.model; body.system = systemPrompt; body.messages = [{ role: "user", content: userContent }]; body.max_tokens = 1024; } else { body.messages = [ { role: "system", content: systemPrompt }, { role: "user", content: userContent }, ]; } // timeoutMs=60s — prompt 已压缩(只给失败+resource/log 输出),正常 30s 内返回 const resp: any = await api.aiFetch(url, "POST", headers, JSON.stringify(body), provider.providerId, true, false, false, 60000); if (!resp?.ok) { const msg = resp?.error ? `${resp.error} (HTTP ${resp.status})` : `HTTP ${resp?.status || "?"}`; throw new Error(`AI 调用失败: ${msg}`); } const data = resp.data ? JSON.parse(resp.data) : {}; if (provider.style === "google") { return data?.candidates?.[0]?.content?.parts?.[0]?.text || ""; } else if (provider.style === "anthropic") { return data?.content?.[0]?.text || ""; } return data?.choices?.[0]?.message?.content || ""; } function extractJson(text: string): any { // 去掉 ```json ... ``` 包裹 let t = text.trim(); t = t.replace(/^```json\s*/i, "").replace(/\s*```\s*$/, ""); // 找最外层 { ... } const m = t.match(/\{[\s\S]*\}/); if (!m) return null; try { return JSON.parse(m[0]); } catch { return null; } } // ─── Prompt ──────────────────────────────────── const SYSTEM_PROMPT = `你是一名资深运维工程师,负责汇总服务器/交换机/防火墙的巡检报告。 数据中提供了每台设备执行各类巡检命令的原始输出,请观察这些细节数据,识别隐藏风险和运行状态。 输出 JSON: { "health_summary": "一句话整体评估(X 台在线 / Y 台离线,命令成功率 %,关键发现一句话)", "offline_devices": [ { "device": "hostname", "reason": "SSH连接超时|认证失败" } ], "device_status": [ { "device": "hostname", "catalog": "huawei_h3c_switch", "running_ok": true, "highlights": ["关键观察1", "关键观察2"], "issues": ["失败的命令或隐藏风险"] } ], "hidden_risks": [ { "device": "hostname", "observation": "具体数值和异常点", "severity": "critical|warning|info" } ], "recommendations": ["按优先级排序的处置建议,必须具体到设备和操作"] } 分析要点: - 观察温度数值:正常交换机 40-55°C,超过 60°C 值得关注 - 观察 CPU/内存使用率:单台持续 >75% 要标 warning - 观察风扇转速:异常偏高(如 4500 RPM 但正常 3000)可能预示轴承磨损 - 观察端口状态:CRC error、input error 非零 = 物理链路问题 - 观察 NTP:stratum 变化、peer 切换频繁 = 时钟源不稳定 - 观察 display logbuffer:找出 WARNING/CRITICAL 级别的日志条目作为隐藏风险 - vrrp/stp 角色异常(非预期的 Master/Backup 切换、指定 STP 阻塞端口)= 网络拓扑变更 - Linux 服务器的 load average / /proc/meminfo / ip link 状态也要关注 - Linux journalctl -p warning 输出的是 warning 以上日志,逐条解读 hidden_risks 用于放那些"命令没失败但数值异常"的情况(如 display temperature 成功但读到 65°C) device_status 用于列出每台设备的简要运行概览(亮点 + 问题) 严格输出合法 JSON,不要额外解释`; /** * 按命令类型分层保留 stdout: * 基础信息/资源指标 → 完整保留(短,是核心) * 网络状态 → 前 800 字 * 日志类 → 最后 30 行(tail) * 失败命令 → 完整保留错误输出 */ function _classifyCmd(cmdName: string): "base" | "resource" | "network" | "log" { const n = cmdName.toLowerCase(); if (/version|device|hostname|uptime|uname/.test(n)) return "base"; if (/memory|cpu|temperature|fan|power|free -m|vmstat|top -bn1|journalctl -p/.test(n)) return "resource"; if (/interface|arp|vlan|stp|vrrp|ntp|route|neigh|ip -br|ip link|ip addr/.test(n)) return "network"; if (/logbuffer|journalctl|dmesg|messages|syslog|show log/.test(n)) return "log"; return "network"; // 未知默认中等长度 } function _truncateStdout(cmdName: string, stdout: string): string { if (!stdout) return ""; const cls = _classifyCmd(cmdName); if (cls === "log") { // 日志取最后 30 行 const lines = stdout.split("\n"); if (lines.length > 30) return lines.slice(-30).join("\n"); return stdout; } if (cls === "network") { return stdout.slice(0, 800); } // base / resource 完整保留(一般 < 30 行) return stdout.slice(0, 2000); // 兜底防极端 } function buildSnapshotUserContext(snapshot: any): string { const offlineDevices: any[] = []; const onlineDevices: any[] = []; for (const d of snapshot.devices) { const allFail = d.commands.every((c: any) => !c.ok); const isConnectionErr = d.commands.every((c: any) => /Connection refused|ETIMEDOUT|ECONNREFUSED|connect failed|authentication failed|SSH.*failed/i.test(c.stderr || c.stdout || "") ); const isOffline = !d.ok || (allFail && isConnectionErr); if (isOffline) { const reason = d.commands[0]?.stderr?.slice(0, 200) || d.commands[0]?.stdout?.slice(0, 200) || "SSH连接失败"; offlineDevices.push({ device: d.hostname, reason }); } else { // 在线设备:成功设备只给概览,只给失败命令 + 资源/日志关键命令的截断 stdout const detailCmds: any[] = []; let hasFail = false; for (const c of d.commands) { if (!c.ok) { hasFail = true; detailCmds.push({ cmd: c.cmd, purpose: c.purpose, status: "fail", output: (c.stdout || c.stderr || "").slice(0, 600), }); continue; } // 成功命令:只保留 resource(CPU/温度/风扇)和 log 类的截断输出 const cls = _classifyCmd(c.cmd); if (cls === "resource") { detailCmds.push({ cmd: c.cmd, purpose: c.purpose, status: "ok", output: (c.stdout || "").slice(0, 400), }); } else if (cls === "log") { detailCmds.push({ cmd: c.cmd, purpose: c.purpose, status: "ok", output: _truncateStdout(c.cmd, c.stdout || ""), }); } // base / network 类成功命令 → **完全不塞** } onlineDevices.push({ device: d.hostname, catalog: d.catalog_id, status: hasFail ? "partial_fail" : "ok", command_total: d.commands.length, command_failed: d.commands.filter((c: any) => !c.ok).length, commands: detailCmds, // 只有失败命令 + resource/log 成功命令 }); } } // 总体统计 const totalOnline = onlineDevices.length; const totalOffline = offlineDevices.length; const totalCmds = onlineDevices.reduce((a, d) => a + (d as any).command_total, 0); const failCmds = onlineDevices.reduce((a, d) => a + (d as any).command_failed, 0); return JSON.stringify({ timestamp: new Date(snapshot.timestamp).toISOString(), trigger: snapshot.trigger, duration_ms: snapshot.duration_ms, offline_count: totalOffline, online_count: totalOnline, total_commands: totalCmds, failed_commands: failCmds, offline_devices: offlineDevices, online_devices: onlineDevices, }, null, 2); } function buildDailyUserContext(report: any): string { const s = report.summary; const failDevices = Object.entries(s.deviceSuccessRate) .filter(([, r]) => (r as number) < 0.95) .map(([id, r]) => ({ device: id, successRate: r })); return JSON.stringify({ date: report.date, mode: report.mode, snapshotCount: s.snapshotCount, deviceCount: s.deviceCount, totalCmds: s.totalCmds, failRate: s.failRate, failingDevices: failDevices, topFailedCommands: s.topFailedCommands.slice(0, 10), }, null, 2); } // ─── 主入口 ──────────────────────────────────── export async function summarizeSnapshot(snapshot: any): Promise { const provider = await resolveProvider(); if (!provider) { throw new Error("AI provider 未配置,请在设置→AI 页面添加 provider"); } const content = buildSnapshotUserContext(snapshot); console.log("[ReportSummary] summarizeSnapshot:", { providerId: provider.providerId, model: provider.model, devices: snapshot?.devices?.length, contentLen: content.length, }); const t0 = Date.now(); const raw = await aiChatCompletion(provider, SYSTEM_PROMPT, content); console.log("[ReportSummary] AI 返回,耗时:", (Date.now() - t0) / 1000, "s,返回长度:", raw.length); return extractJson(raw) || { health_summary: raw, anomalies: [], trend_notes: [], recommendations: [] }; } export async function summarizeDailyReport(report: any): Promise { const provider = await resolveProvider(); if (!provider) { throw new Error("AI provider 未配置,请在设置→AI 页面添加 provider"); } const content = buildDailyUserContext(report); const raw = await aiChatCompletion(provider, SYSTEM_PROMPT, content); return extractJson(raw) || { health_summary: raw, anomalies: [], trend_notes: [], recommendations: [] }; }