Files
SenMeshNet/infrastructure/inspection/reportSummary.ts
zhaolei 2f9f8da48e
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
Initial commit: NetMesh smart ops platform with 3D topology, inspection reports, RCA and AI ops assistant
2026-10-15 13:53:04 +08:00

330 lines
12 KiB
TypeScript
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 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<ProviderInfo | null> {
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<string> {
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<string, string> = {
"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<any> {
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<any> {
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: [] };
}