Initial commit: NetMesh smart ops platform with 3D topology, inspection reports, RCA and AI ops assistant
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

This commit is contained in:
2026-10-15 13:53:04 +08:00
commit 2f9f8da48e
3299 changed files with 926841 additions and 0 deletions

View File

@@ -0,0 +1,329 @@
"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: [] };
}