"use strict"; /** * 巡检报告 PDF 导出(正式文档版) * * 复用 Electron 原生 BrowserWindow + webContents.printToPDF, * 不引入额外 npm 依赖。A4 纸张、支持黑白打印、无 emoji。 * * 两个模板: * - renderSnapshotHtml(snap) → 单快照巡检报告(7 章节 + 健康评分) * - renderDailyReportHtml(report) → 日运行聚合报告(6 章节 + 健康评分) * * 独立存储路径:app.getPath('userData')/inspection-reports/{YYYY-MM-DD}/ */ const path = require("path"); const fs = require("fs"); // ─── 延迟 require ───────────────────────────────────── function _getElectron() { try { return require("electron"); } catch (e) { throw new Error("reportExport 只能在 Electron 主进程中使用"); } } async function _printHtmlToPdf(html) { const { BrowserWindow } = _getElectron(); const win = new BrowserWindow({ show: false, webPreferences: { backgroundThrottling: false }, }); try { await new Promise((resolve, reject) => { const ok = win.webContents.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(html)}`); if (!ok) return reject(new Error("loadURL 返回 false")); win.webContents.once("did-finish-load", resolve); win.webContents.once("did-fail-load", (_e, code, msg) => reject(new Error(`did-fail-load ${code}: ${msg}`))); setTimeout(() => reject(new Error("print loadURL 超时")), 15000); }); return await win.webContents.printToPDF({ landscape: false, printBackground: true, marginType: "default", pageSize: "A4", }); } finally { if (!win.isDestroyed()) win.destroy(); } } function _ensureDir(app) { const dir = path.join(app.getPath("userData"), "inspection-reports"); fs.mkdirSync(dir, { recursive: true }); return dir; } function _dateDir(app, ts) { const d = new Date(ts); const pad = (n) => n.toString().padStart(2, "0"); const name = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; const base = _ensureDir(app); const dir = path.join(base, name); fs.mkdirSync(dir, { recursive: true }); return dir; } // ─── 通用 helper ────────────────────────────────────── const DEFAULT_SYSTEM_NAME = "NetMesh 智能巡检系统"; const PADDING = (n) => n.toString().padStart(2, "0"); function _fmtDateTime(ts) { const d = new Date(ts); return `${d.getFullYear()}-${PADDING(d.getMonth() + 1)}-${PADDING(d.getDate())} ${PADDING(d.getHours())}:${PADDING(d.getMinutes())}:${PADDING(d.getSeconds())}`; } function _fmtDate(ts) { const d = new Date(ts); return `${d.getFullYear()}-${PADDING(d.getMonth() + 1)}-${PADDING(d.getDate())}`; } /** severity 枚举 → 扣分 + 风险等级文字标签 */ const SEVERITY_MAP = { critical: { deduct: 12, label: "【紧急】★★★★★", level: "紧急" }, warning: { deduct: 6, label: "【高优】★★★★☆", level: "高优" }, info: { deduct: 3, label: "【中优】★★★☆☆", level: "中优" }, low: { deduct: 1, label: "【低优】★★☆☆☆", level: "低优" }, }; function _severityInfo(sev) { return SEVERITY_MAP[sev] || SEVERITY_MAP.info; } function _severityLabel(sev) { return _severityInfo(sev).label; } /** 健康分数 → 等级文本 */ function _healthLevelText(score) { if (score >= 90) return "[优秀]"; if (score >= 80) return "[健康]"; if (score >= 70) return "[良好]"; if (score >= 60) return "[一般]"; if (score >= 40) return "[较差]"; return "[严重]"; } function _severityColor(sev) { if (sev === "critical") return "#b91c1c"; if (sev === "warning") return "#d97706"; if (sev === "info") return "#2563eb"; return "#6b7280"; } // ─── 健康评分算法 ──────────────────────────────────── /** 单次巡检快照的健康评分(满分 100,最低 0) */ // ─── 健康评分算法(多维度加权) ───────────────────── // 满分 100,四维度加权: // Reachability 40% 设备可及性 —— 在线设备占比 // Coverage 25% 巡检覆盖率 —— 命令成功率(仅在线设备) // Risk 25% 隐患评级 —— AI hidden_risks 加权,上限 20 // Efficiency 10% 执行效率 —— 超时/时长异常 // // 设计思路: // - 比例扣分而非线性叠加:设备越多,单台离线扣分越小 // - 离线设备不参与覆盖率计算(本来就连不上) // - Risk 维度有上限,防止 AI 隐患把总分打崩 // - Efficiency 权重偏低,因耗时不直接反映健康度 function calcSnapshotHealthScore(snap, aiSummary) { const devices = snap?.devices || []; const total = devices.length; if (total === 0) { return { score: 0, levelText: _healthLevelText(0), breakdown: "无设备数据" }; } // ── 维度 1: 设备可及性(满分 40)───────────────── const online = devices.filter(d => d.ok).length; const offline = total - online; const reachabilityScore = Math.round(40 * (online / total)); // ── 维度 2: 巡检覆盖率(满分 25)───────────────── let totalCmds = 0, okCmds = 0; for (const d of devices) { if (!d.ok) continue; // 离线跳过 const cmds = d.commands || []; for (const c of cmds) { totalCmds++; // command not found / 未识别 是框架 fallback,不算失败 if (!c.ok && /command not found|未识别|not recognized/i.test(c.stderr || c.stdout || "")) continue; if (c.ok) okCmds++; } } const coverageRatio = totalCmds === 0 ? 1 : okCmds / totalCmds; const coverageScore = Math.round(25 * coverageRatio); // ── 维度 3: 隐患评级(满分 25,上限扣 20)─────── const risks = aiSummary?.hidden_risks || []; let riskPenalty = 0, riskCritical = 0, riskWarning = 0, riskInfo = 0; for (const r of risks) { if (!r || !r.severity) continue; const sev = r.severity.toLowerCase(); if (sev === "critical") { riskPenalty += 5; riskCritical++; } else if (sev === "warning" || sev === "high") { riskPenalty += 3; riskWarning++; } else { riskPenalty += 1; riskInfo++; } } riskPenalty = Math.min(riskPenalty, 20); // 最多扣到只剩 5 分 const riskScore = 25 - riskPenalty; // ── 维度 4: 执行效率(满分 10)─────────────────── let efficiencyScore = 10; const durationMs = snap?.duration_ms || 0; const avgMsPerDevice = total > 0 ? durationMs / total : 0; if (avgMsPerDevice > 15000) { const penalty = Math.min(6, Math.floor((avgMsPerDevice - 15000) / 10000)); efficiencyScore = 10 - penalty; } const timeoutDevices = devices.filter(d => d.ok && (d.commands || []).some(c => !c.ok && /ETIMEDOUT|timeout/i.test(c.stderr || c.stdout || "")) ).length; if (timeoutDevices > 0) { efficiencyScore = Math.max(4, efficiencyScore - Math.min(3, timeoutDevices)); } // ── 最终得分(四维度求和)─────────────────────── const score = Math.max(0, reachabilityScore + coverageScore + riskScore + efficiencyScore); const breakdown = [ `设备可及性: ${reachabilityScore}/40(在线 ${online}/${total})`, `巡检覆盖率: ${coverageScore}/25(命令成功率 ${(coverageRatio * 100).toFixed(1)}%)`, `隐患评级: ${riskScore}/25(隐患 ${risks.length} 条: 严重 ${riskCritical}, 警告 ${riskWarning}, 提示 ${riskInfo})`, `执行效率: ${efficiencyScore}/10(平均 ${(avgMsPerDevice / 1000).toFixed(1)}s/台${timeoutDevices > 0 ? `, ${timeoutDevices} 台超时` : ""})`, ].join("\n"); return { score, levelText: _healthLevelText(score), breakdown }; } /** 日报告健康评分(基于每个快照的分数,取算术平均 + max/min) */ function calcDailyHealthScore(snapScores) { if (!snapScores || snapScores.length === 0) { return { score: 0, levelText: _healthLevelText(0), max: 0, min: 0 }; } const sum = snapScores.reduce((a, b) => a + b, 0); const score = Math.round(sum / snapScores.length); return { score, levelText: _healthLevelText(score), max: Math.max(...snapScores), min: Math.min(...snapScores), }; } // ─── 报告编号 ──────────────────────────────────────── function _snapReportId(snap) { // INS-SNAP-YYYYMMDD-xxx(用快照 timestamp 生成日期,序号用 snap id 末尾数字) const d = new Date(snap.timestamp); const datePart = `${d.getFullYear()}${PADDING(d.getMonth() + 1)}${PADDING(d.getDate())}`; // 从 snap.id 提取一个稳定序号,如 snap_xxx_0matlx → 取数字或字母序列 const seq = (snap.id.match(/(\d+)/) || [, "001"])[1].slice(-3).padStart(3, "0"); return `INS-SNAP-${datePart}-${seq}`; } function _dailyReportId(report) { return `INS-DAILY-${(report.date || "").replace(/-/g, "")}-${report.id?.slice(-3) || "001"}`; } // ─── 正式 CSS(A4 打印、无 emoji 依赖) ──────────────── const FORMAL_CSS = ` @page { margin: 2.54cm 2.5cm; @top-center { content: string(docHeader); font-size: 10px; color: #6b7280; } @bottom-center { content: "第 " counter(page) " 页 / 共 " counter(pages) " 页"; font-size: 10px; color: #6b7280; } } html, body { font-family: "SimSun", "宋体", "Microsoft YaHei", sans-serif; font-size: 12px; color: #1f2937; line-height: 1.55; } h1 { font-size: 20px; font-weight: 700; text-align: center; margin: 0 0 16px; padding-bottom: 10px; border-bottom: 2px solid #374151; string-set: docHeader content(); } h2 { font-size: 15px; font-weight: 700; margin: 22px 0 10px; padding-left: 10px; border-left: 4px solid #374151; color: #111827; } h3 { font-size: 13px; font-weight: 700; margin: 14px 0 6px; color: #1f2937; } .report-meta { display: flex; flex-wrap: wrap; gap: 10px 24px; font-size: 12px; color: #374151; margin-bottom: 14px; padding: 10px 14px; border: 1px solid #d1d5db; border-radius: 4px; background: #f9fafb; } .report-meta .meta-item { white-space: nowrap; } .report-meta .meta-label { color: #6b7280; margin-right: 4px; } /* 健康评分块 */ .score-block { border: 2px solid #1f2937; border-radius: 6px; padding: 14px 18px; margin: 16px 0; background: #fafafa; page-break-inside: avoid; } .score-block h2 { margin-top: 0; border-left-color: #1f2937; } .score-table { width: 100%; border-collapse: collapse; margin-top: 6px; } .score-table td { border: 1px solid #d1d5db; padding: 6px 10px; font-size: 12px; } .score-table td:first-child { color: #6b7280; background: #f3f4f6; width: 30%; font-weight: 500; } .score-table td.score-num { font-size: 18px; font-weight: 700; color: #b91c1c; } table { border-collapse: collapse; width: 100%; margin: 6px 0; font-size: 11.5px; } th, td { border: 1px solid #9ca3af; padding: 5px 8px; text-align: left; vertical-align: top; line-height: 1.45; } th { background: #e5e7eb; font-weight: 700; font-size: 11.5px; color: #111827; } tr:nth-child(even) td { background: #fafafa; } tr:hover td { background: #f3f4f6; } /* 风险等级文字标签 */ .sev-critical { color: #b91c1c; font-weight: 700; } .sev-warning { color: #d97706; font-weight: 600; } .sev-info { color: #2563eb; font-weight: 600; } .sev-low { color: #6b7280; } /* 表格内状态列小标签 */ .tag { display: inline-block; padding: 1px 6px; border-radius: 2px; font-size: 11px; border: 1px solid currentColor; } .tag-red { color: #b91c1c; border-color: #b91c1c; } .tag-amber { color: #b45309; border-color: #b45309; } .tag-blue { color: #1d4ed8; border-color: #1d4ed8; } .tag-green { color: #15803d; border-color: #15803d; } .tag-gray { color: #6b7280; border-color: #6b7280; } .cmd-fail-block { background: #fef2f2; border: 1px solid #fecaca; border-radius: 4px; padding: 8px 12px; margin: 8px 0; font-size: 11px; page-break-inside: avoid; } .cmd-fail-block .cmd-name { font-family: Consolas, "Courier New", monospace; color: #b91c1c; font-weight: 600; margin-bottom: 4px; } .cmd-fail-block .cmd-output { background: #1f2937; color: #e5e7eb; padding: 6px 10px; border-radius: 3px; font-family: Consolas, "Courier New", monospace; font-size: 10.5px; white-space: pre-wrap; word-break: break-all; max-height: 180px; overflow: hidden; } .section-break { page-break-before: always; } .no-print { display: none; } /* 运维处置建议 */ .rec-block { margin: 6px 0 12px; padding: 8px 12px; border-left: 3px solid #374151; background: #f9fafb; } .rec-block h4 { margin: 0 0 6px; font-size: 12px; font-weight: 700; } .rec-block ol { margin: 0; padding-left: 20px; font-size: 11.5px; line-height: 1.75; } /* 目录 */ .toc { border: 1px solid #d1d5db; background: #fafafa; border-radius: 4px; padding: 12px 20px; margin: 16px 0 18px; } .toc ol { margin: 0; padding-left: 20px; line-height: 1.9; font-size: 12px; } /* 离线设备标记 */ .offline-device { color: #b91c1c; font-weight: 600; } `; // ─── 快照模板 ──────────────────────────────────────── function renderSnapshotHtml(snap, aiSummary, systemName) { const _sys = systemName || DEFAULT_SYSTEM_NAME; const scoreInfo = calcSnapshotHealthScore(snap, aiSummary); // 快照报告总结拆成 3~4 条 bullet const snapshotSummaryBullets = (() => { const bullets = []; const totalDevs = snap.devices.length; const offlineDevs = snap.devices.filter(d => !d.ok).length; const onlineDevs = totalDevs - offlineDevs; const totalCmds = snap.devices.reduce((a, d) => a + d.commands.length, 0); const failCmds = snap.devices.reduce((a, d) => a + d.commands.filter(c => !c.ok).length, 0); bullets.push(`本次快照共巡检 ${totalDevs} 台设备,在线 ${onlineDevs} 台,离线 ${offlineDevs} 台;命令总执行 ${totalCmds} 条,失败 ${failCmds} 条`); // 从 hidden_risks 聚合 severity const severityCount = (aiSummary?.hidden_risks || []).reduce((a, r) => { const s = r.severity || "info"; a[s] = (a[s] || 0) + 1; return a; }, {}); const offlineCnt = (aiSummary?.offline_devices || []).length; const riskParts = []; if (offlineCnt) riskParts.push(`离线设备 ${offlineCnt} 台`); if (severityCount.critical) riskParts.push(`紧急风险 ${severityCount.critical} 起`); if (severityCount.warning) riskParts.push(`高优风险 ${severityCount.warning} 起`); if (severityCount.info) riskParts.push(`中低优风险 ${(severityCount.info || 0) + (severityCount.low || 0)} 起`); bullets.push(riskParts.length > 0 ? `风险统计:${riskParts.join(";")}` : "本次快照未检测到风险隐患"); // 从 AI device_status 里找异常设备 const abnormal = (aiSummary?.device_status || []).filter(d => !d.running_ok).map(d => d.device); if (abnormal.length > 0) { bullets.push(`运行异常设备共 ${abnormal.length} 台:${abnormal.slice(0, 5).join("、")}${abnormal.length > 5 ? ` 等 ${abnormal.length} 台` : ""}`); } else { bullets.push("所有在线设备运行状态正常"); } // 从 AI recommendations 里提炼 Top 建议 const recs = (aiSummary?.recommendations || []).slice(0, 2); if (recs.length > 0) { bullets.push(`首要处置建议:${recs.join(";")}`); } else if (aiSummary?.health_summary) { bullets.push(`${aiSummary.health_summary}`); } else { bullets.push("AI 汇总尚未完成,详细分析请稍候"); } return bullets; })(); const scoreHtml = `

巡检健康综合评分

综合得分${scoreInfo.score}
健康等级${scoreInfo.levelText}
报告总结:
`; const offlineDevices = snap.devices.filter(d => !d.ok); const onlineDevices = snap.devices.filter(d => d.ok); const totalCmds = snap.devices.reduce((a, d) => a + d.commands.length, 0); const okCmds = snap.devices.reduce((a, d) => a + d.commands.filter(c => c.ok).length, 0); const failCmds = totalCmds - okCmds; const failRate = totalCmds > 0 ? ((failCmds / totalCmds) * 100).toFixed(1) : "0.0"; const tsStr = _fmtDateTime(snap.timestamp); const dateStr = _fmtDate(snap.timestamp); const execDur = ((snap.duration_ms || 0) / 1000).toFixed(1); const reportId = _snapReportId(snap); const metaHtml = `
报告编号:${reportId} 执行时间:${tsStr} 触发方式:${snap.trigger === "scheduled" ? "定时触发" : "手动触发"} 巡检耗时:${execDur} s 设备总数:${snap.devices.length} 台 在线设备:${onlineDevices.length} 台 离线设备:${offlineDevices.length} 台 命令总执行:${totalCmds} 条 命令成功率:${okCmds}/${totalCmds} (${((okCmds / Math.max(totalCmds, 1)) * 100).toFixed(1)}%) 编制系统:${_sys} 编制日期:${dateStr} 保密级别:内部运维资料,请勿外传
`; // ═══ 目录 ═══ const tocHtml = `
目录
  1. 巡检总览
  2. 离线设备清单
  3. 风险隐患汇总
  4. 设备运行概览
  5. 运维处置建议
  6. 异常设备详情
  7. 附录 — 本次巡检命令集合
`; // ─── 1. 巡检总览 ─── const overviewHtml = `

1. 巡检总览

本次快照于 ${tsStr} 触发,共对 ${snap.devices.length} 台网络设备执行批量 SSH 巡检; ${onlineDevices.length} 台设备 SSH 登录成功,${offlineDevices.length} 台设备 SSH 会话建立失败标记为离线; 在线设备共执行命令 ${totalCmds} 条,成功 ${okCmds} 条,失败 ${failCmds} 条,命令成功率 ${((okCmds / Math.max(totalCmds, 1)) * 100).toFixed(1)}%。

${aiSummary?.health_summary ? `

AI 健康摘要:${aiSummary.health_summary}

` : "

本次快照尚未完成 AI 汇总分析

"}
项目数值
设备总数${snap.devices.length} 台
在线 / 离线${onlineDevices.length} / ${offlineDevices.length}
命令成功 / 失败${okCmds} / ${failCmds}
命令失败率${failRate}%
巡检总耗时${execDur} 秒
`; // ─── 2. 离线设备清单 ─── const offlineHtml = (() => { if (offlineDevices.length === 0) return `

2. 离线设备清单

本次巡检所有设备均在线。

`; const rows = offlineDevices.map(d => { // 离线设备:从 stderr/stdout 提取故障分类 const firstErr = (d.commands[0]?.stderr || d.commands[0]?.stdout || "").trim(); let category = "SSH 会话建立失败"; let desc = firstErr || "SSH 连接超时或认证失败"; if (/Timed out while waiting for handshake|ETIMEDOUT/i.test(firstErr)) { category = "SSH 握手超时"; desc = "网络不可达或设备 SSH 服务未正常监听"; } else if (/Invalid identification string|protocol version mismatch/i.test(firstErr)) { category = "SSH 协议协商失败"; desc = "SSH 版本、加密算法协商失败,核对设备 SSH 配置与固件版本"; } else if (/connection is closed by SSH server/i.test(firstErr)) { category = "SSH 服务侧关闭连接"; desc = "会话被设备主动关闭,请检查设备 SSH 服务状态"; } else if (/authentication failed|permission denied/i.test(firstErr)) { category = "SSH 认证失败"; desc = "用户名 / 密码 / 密钥不匹配"; } return `${d.hostname}${category}${desc}`; }).join(""); return `

2. 离线设备清单

以下设备 SSH 会话建立失败,未采集任何设备内部数据;不再重复罗列单条命令失败记录。

${rows}
设备故障分类故障描述
`; })(); // ─── 3. 风险隐患汇总 ─── const riskHtml = (() => { const risks = aiSummary?.hidden_risks; if (!risks || risks.length === 0) { return `

3. 风险隐患汇总

本次巡检未检测到需要关注的风险隐患。

`; } // 按 severity 从高到低排序 const sevOrder = { critical: 0, warning: 1, info: 2, low: 3 }; const sorted = [...risks].sort((a, b) => (sevOrder[a.severity] ?? 99) - (sevOrder[b.severity] ?? 99)); const rows = sorted.map(r => ` ${_severityLabel(r.severity)} ${r.device} ${(r.observation || "").slice(0, 120)} `).join(""); return `

3. 风险隐患汇总

按风险优先级从高到低排序

${rows}
风险等级设备风险简述
`; })(); // ─── 4. 设备运行概览(仅在线设备) ─── const overviewDevicesHtml = (() => { if (onlineDevices.length === 0) return `

4. 设备运行概览

本次巡检无在线设备。

`; const statusFromAI = {}; for (const ds of (aiSummary?.device_status || [])) statusFromAI[ds.device] = ds; const rows = onlineDevices.slice(0, 50).map(d => { const ai = statusFromAI[d.hostname]; const failCmds = d.commands.filter(c => !c.ok).length; const aiStatus = ai ? (ai.running_ok ? '正常' : '存在风险') : (failCmds === 0 ? '正常' : '存在异常'); const highlights = ai ? [...(ai.highlights || []), ...(ai.issues || [])].join(";") : (failCmds > 0 ? `${failCmds} 条命令失败` : "全部命令成功"); return ` ${d.hostname} ${d.catalog_id || "-"} ${aiStatus} ${highlights.slice(0, 160)} `; }).join(""); return `

4. 设备运行概览

仅展示 SSH 登录成功设备;"正常 / 存在风险" 依据 AI 智能分析判定,无 AI 汇总时依据命令执行结果。

${rows}
设备设备类型综合状态关键观察
${onlineDevices.length > 50 ? `

其余 ${onlineDevices.length - 50} 台在线设备省略展示

` : ""}`; })(); // ─── 5. 运维处置建议 ─── const recHtml = (() => { if (!aiSummary?.recommendations || aiSummary.recommendations.length === 0) { return `

5. 运维处置建议

本次巡检未生成处置建议。

`; } // 把 recommendations 按严重程度分组(简单启发式:用关键词匹配) const criticalRecs = []; const highRecs = []; const midRecs = []; const lowRecs = []; for (const r of aiSummary.recommendations) { if (/(立即|紧急|尽快|critical|马上|重启|更换|故障|掉电|离线)/i.test(r)) criticalRecs.push(r); else if (/(重要|尽快|排查|修复|异常|暴力|crash|panic)/i.test(r)) highRecs.push(r); else if (/(建议|关注|定期|监控|持续|计划)/i.test(r)) lowRecs.push(r); else midRecs.push(r); } const block = (title, sevClass, items) => items.length === 0 ? "" : `

${title}

    ${items.map(i => `
  1. ${i}
  2. `).join("")}
`; return `

5. 运维处置建议

${block("【紧急】★★★★★ 需要立即处置", "sev-critical", criticalRecs)} ${block("【高优】★★★★☆ 尽快处理", "sev-warning", highRecs)} ${block("【中优】★★★☆☆ 择机维护", "sev-info", midRecs)} ${block("【低优】★★☆☆☆ 持续关注", "sev-low", lowRecs)}`; })(); // ─── 6. 异常设备详情 ─── const detailHtml = (() => { const abnormal = onlineDevices.filter(d => d.commands.some(c => !c.ok)); if (abnormal.length === 0) { return `

6. 异常设备详情

本次巡检在线设备所有命令执行成功,无异常。

`; } const deviceBlocks = abnormal.map(d => { const failCmds = d.commands.filter(c => !c.ok); const cmdTable = ` ${failCmds.map(c => ` `).join("")}
命令用途异常说明
${c.cmd} ${c.purpose || "-"} ${(c.stderr || c.stdout || "命令执行失败").slice(0, 100)}
`; const failOutputs = failCmds.map(c => `
${c.cmd}${c.purpose ? " — " + c.purpose : ""}
${(c.stdout || c.stderr || "").slice(0, 2000)}
`).join(""); return `

设备:${d.hostname}(${d.catalog_id || "-"})

命令成功 ${d.commands.filter(c => c.ok).length}/${d.commands.length},失败 ${failCmds.length} 条

${cmdTable} ${failOutputs}
`; }).join(""); return `

6. 异常设备详情

仅展示 SSH 登录成功、但部分巡检命令执行异常的设备;离线设备已在第 2 节列出、不再重复。

${deviceBlocks}`; })(); // ─── 7. 附录:巡检命令集合 ─── const appendixHtml = (() => { const cmdSet = new Set(); for (const d of snap.devices) { for (const c of d.commands) cmdSet.add(c.cmd); } const cmdList = Array.from(cmdSet); const rows = cmdList.map((c, i) => `${i + 1}${c}`).join(""); return `

7. 附录 — 本次巡检命令集合

${rows}
#命令
`; })(); return ` 网络设备巡检快照报告 ${reportId}

${_sys}

${tsStr} 网络设备巡检快照报告
${scoreHtml} ${tocHtml} ${overviewHtml} ${offlineHtml} ${riskHtml} ${overviewDevicesHtml} ${recHtml} ${detailHtml} ${appendixHtml} `; } // ─── 日运行报告模板 ────────────────────────────────── function renderDailyReportHtml(report, allSnapshots, systemName) { const _sys = systemName || DEFAULT_SYSTEM_NAME; // 从快照构建 device_id → hostname 映射(用于把 UUID 替换成实际设备名) const deviceIdToHost = new Map(); if (allSnapshots) { for (const snap of allSnapshots) { for (const d of snap.devices || []) { if (d.device_id && d.hostname && !deviceIdToHost.has(d.device_id)) { deviceIdToHost.set(d.device_id, d.hostname); } } } } const _hostFor = (id) => deviceIdToHost.get(id) || id; // 如果没传入快照列表就用 report 本身的 timeline/perSnapshotHighlights 计算 let snapScores = []; if (allSnapshots && allSnapshots.length > 0) { // 从快照列表计算每个快照的分数(日期匹配) for (const snap of allSnapshots) { const d = _fmtDate(snap.timestamp); if (d === report.date) { const ai = snap.ai_summary_status === "done" ? snap.ai_summary : null; snapScores.push(calcSnapshotHealthScore(snap, ai).score); } } } const dailyScoreInfo = calcDailyHealthScore(snapScores); const s = report.summary || {}; const tsStr = _fmtDateTime(report.timestamp); const dailyReportId = _dailyReportId(report); // ═══ 元信息 ═══ const metaHtml = `
报告编号:${dailyReportId} 报告日期:${report.date} 生成时间:${tsStr} 报告模式:${report.mode === "finalized" ? "最终版" : " provisional 中间版"} 快照次数:${s.snapshotCount || 0} 次 涉及设备:${s.deviceCount || 0} 台 总命令执行:${s.totalCmds || 0} 条 整体失败率:${s.failRate ?? 0}% 编制系统:${_sys} 保密级别:内部运维资料
`; // ═══ 健康评分块 ═══ // 日报告总结拆成 3~4 条 bullet const dailySummaryBullets = (() => { const snapCount = s.snapshotCount || 0; const devCount = s.deviceCount || 0; const totalCmds = s.totalCmds || 0; const failCmds = s.failCmds || 0; const failRate = s.failRate ?? 0; // 从快照里聚合离线情况 let totalOffline = 0; let maxOfflinePerSnap = 0; for (const snap of (allSnapshots || [])) { const off = (snap.devices || []).filter(d => !d.ok).length; totalOffline += off; if (off > maxOfflinePerSnap) maxOfflinePerSnap = off; } // timeline 事件聚合 const severityCount = (report.timeline || []).reduce((a, e) => { a[e.severity || "info"] = (a[e.severity || "info"] || 0) + 1; return a; }, {}); const totalRisk = severityCount.critical + severityCount.warning + severityCount.info + (severityCount.low || 0); const bullets = []; bullets.push(`本次共执行 ${snapCount} 次巡检快照,覆盖 ${devCount} 台设备,总命令执行 ${totalCmds} 条,命令成功率 ${snapCount === 0 ? 100 : Math.round((1 - failRate / 100) * 100)}%`); bullets.push(`全部快照累计离线设备 ${totalOffline} 台次,单次最多离线 ${maxOfflinePerSnap} 台`); bullets.push(`跨快照共检出风险事件 ${totalRisk} 起:紧急 ${severityCount.critical || 0} 起,高优 ${severityCount.warning || 0} 起,中优 ${(severityCount.info || 0) + (severityCount.low || 0)} 起`); // 第四点:从 timeline 里提炼 TOP 风险设备 const topRisks = (report.timeline || []) .filter(e => e.severity === "critical" || e.severity === "warning") .reduce((m, e) => { m[e.device] = (m[e.device] || 0) + 1; return m; }, {}); const topDevs = Object.entries(topRisks) .sort((a, b) => b[1] - a[1]) .slice(0, 3) .map(([d]) => d); if (topDevs.length > 0) { bullets.push(`需重点关注设备:${topDevs.join("、")},在当日巡检中反复出现高优先级问题`); } else { bullets.push("未检测到反复出现高优/紧急问题的设备,整体趋势平稳"); } return bullets; })(); const scoreHtml = `

当日巡检健康综合评分

当日综合得分${dailyScoreInfo.score}
健康等级${dailyScoreInfo.levelText}
快照最高 / 最低${dailyScoreInfo.max} / ${dailyScoreInfo.min}
报告总结:
`; // ═══ 目录 ═══ const tocHtml = `
目录
  1. 快照历史总览
  2. 故障事件时间线(聚合视图)
  3. 风险事件汇总
  4. 设备巡检成功率统计
  5. 命令失败 TOP 统计
  6. 综合运维结论与处置建议
`; // ─── 1. 快照历史总览 ─── const overviewHtml = (() => { const highlights = report.perSnapshotHighlights || []; if (highlights.length === 0 && snapScores.length === 0) { return `

1. 快照历史总览

暂无快照数据。

`; } // 如果有真实快照分数(snapScores 数组),把每个快照的分数对齐到 time const scoreMap = {}; if (allSnapshots) { const perSnapScores = {}; for (const snap of allSnapshots) { const ai = snap.ai_summary_status === "done" ? snap.ai_summary : null; perSnapScores[snap.id] = calcSnapshotHealthScore(snap, ai).score; } // 用 timeline 或 perSnapshotHighlights 关联回快照 for (const h of highlights) { if (h.snapshot_id) scoreMap[h.time] = perSnapScores[h.snapshot_id] ?? null; } } // 还有 timeline 事件数可以补充到这张表里 const rows = highlights.length > 0 ? highlights.map(h => ` ${h.time} ${h.health_summary ? h.health_summary.slice(0, 100) : "—"} ${scoreMap[h.time] ?? "—"} `).join("") : `暂无 AI 汇总快照`; return `

1. 快照历史总览

${rows}
快照时间AI 健康摘要快照得分
`; })(); // ─── 2. 故障事件时间线(聚合视图) ─── const timelineHtml = (() => { const timeline = report.timeline || []; if (timeline.length === 0) { return `

2. 故障事件时间线

当日无异常事件。

`; } // 聚合:同设备 + 同类型 → 合并,统计出现次数,保留首次和末次时间,汇总事件简述 const groups = {}; for (const ev of timeline) { const key = `${ev.device}|${ev.type}`; if (!groups[key]) { groups[key] = { ...ev, count: 1, firstTime: ev.time, lastTime: ev.time, details: ev.detail ? [ev.detail] : [], }; } else { groups[key].count++; groups[key].lastTime = ev.time; if (ev.detail && !groups[key].details.includes(ev.detail)) { groups[key].details.push(ev.detail); } // severity 升级:critical 覆盖 warning 覆盖 info if (ev.severity === "critical") groups[key].severity = "critical"; else if (ev.severity === "warning" && groups[key].severity !== "critical") groups[key].severity = "warning"; } } const sorted = Object.values(groups).sort((a, b) => { const order = { critical: 0, warning: 1, info: 2 }; return (order[a.severity] ?? 99) - (order[b.severity] ?? 99); }); const rows = sorted.map(g => { const sevLabel = _severityLabel(g.severity || "info"); const typeMap = { offline: "设备离线", risk: "风险隐患", anomaly: "异常" }; // 简述:取前 3 条不同 detail,超过则加"等";离线类直接用第一条 detail const detailList = g.details && g.details.length > 0 ? g.details.slice(0, 3).map((d, i) => `${i + 1}. ${d}`).join("
") + (g.details.length > 3 ? `
...(另有 ${g.details.length - 3} 条)` : "") : (g.detail || ""); return ` ${sevLabel} ${g.device} ${typeMap[g.type] || g.type || "-"} ${g.count} 次 ${g.firstTime} ${g.lastTime} ${detailList} `; }).join(""); return `

2. 故障事件时间线(聚合视图)

同一设备同类事件做统计,保留首次、末次发生时间与发生次数,减少重复行。

${rows}
事件等级 设备 事件类型 次数 首次 末次 事件简述
`; })(); // ─── 3. 风险事件汇总(跨快照聚合) ─── const riskHtml = (() => { if (!allSnapshots || allSnapshots.length === 0) { // 没真实快照数据就用 timeline 事件 const timelineRisks = (report.timeline || []).filter(e => e.type === "risk" || e.type === "anomaly"); if (timelineRisks.length === 0) { return `

3. 风险事件汇总

当日快照尚未完成 AI 分析,暂无风险汇总。

`; } const rows = timelineRisks.map(r => ` ${_severityLabel(r.severity || "info")} ${r.device} ${(r.detail || "").slice(0, 100)} `).join(""); return `

3. 风险事件汇总

${rows}
风险等级设备风险简述
`; } // 真实快照数据:从所有快照的 hidden_risks 聚合(同设备+同 severity 合并) const agg = new Map(); for (const snap of allSnapshots) { if (snap.ai_summary_status !== "done") continue; const risks = snap.ai_summary?.hidden_risks || []; for (const r of risks) { const key = `${r.device}|${r.severity || "info"}`; const prev = agg.get(key); if (!prev) { agg.set(key, { device: r.device, severity: r.severity || "info", observations: [r.observation] }); } else { if (!prev.observations.includes(r.observation)) prev.observations.push(r.observation); } } } if (agg.size === 0) return `

3. 风险事件汇总

当日未检测到需要关注的风险。

`; const order = { critical: 0, warning: 1, info: 2, low: 3 }; const rows = [...agg.values()].sort((a, b) => (order[a.severity] ?? 99) - (order[b.severity] ?? 99)).map(r => ` ${_severityLabel(r.severity)} ${r.device} ${(r.observations || []).join(";").slice(0, 150)} `).join(""); return `

3. 风险事件汇总

跨快照聚合当日全部巡检快照的 AI 分析结果,按优先级排序。

${rows}
风险等级设备风险简述
`; })(); // ─── 4. 设备巡检成功率 ─── const deviceSuccessHtml = (() => { const rate = s.deviceSuccessRate || {}; const entries = Object.entries(rate); if (entries.length === 0) return `

4. 设备巡检成功率统计

暂无设备数据。

`; const rows = entries.sort((a, b) => a[1] - b[1]).slice(0, 50).map(([id, r]) => { const pct = (r * 100).toFixed(1); const tag = r >= 0.98 ? '在线-正常' : r >= 0.8 ? '在线-有风险' : r === 0 ? '离线' : '巡检异常'; return `${_hostFor(id)}${pct}%${tag}`; }).join(""); return `

4. 设备巡检成功率统计

${rows}
设备巡检成功率综合状态
`; })(); // ─── 5. 命令失败 TOP ─── const topFailHtml = (() => { const fails = s.topFailedCommands || []; if (fails.length === 0) return `

5. 命令失败 TOP 统计

当日无命令失败。

`; const rows = fails.slice(0, 20).map((f, i) => ` ${i + 1} ${f.cmd} ${_hostFor(f.device_id)} ${f.count} 次 `).join(""); return `

5. 命令失败 TOP 统计

${rows}
#命令涉及设备失败次数
`; })(); // ─── 6. 处置建议 ─── const recHtml = (() => { const block = (title, sevClass, items) => items.length === 0 ? "" : `

${title}

    ${items.map(i => `
  1. ${i}
  2. `).join("")}
`; // 优先用 AI 生成的 recommendations(如果有) let recs = report.aiSummary?.recommendations; // 没有 AI 汇总 → 从 timeline + 各快照程序化合成建议 if (!recs || recs.length === 0) { recs = []; // 1. timeline 里的 critical 事件 → 紧急建议 const criticalEvents = (report.timeline || []).filter(e => e.severity === "critical"); const criticalByDevice = new Map(); for (const e of criticalEvents) { const prev = criticalByDevice.get(e.device) || []; if (!prev.includes(e.type)) prev.push(e.type); criticalByDevice.set(e.device, prev); } for (const [dev, types] of criticalByDevice) { recs.push(`设备 ${dev} 发生 ${types.join("、")} 共 ${criticalEvents.filter(e => e.device === dev).length} 次,需立即排查处置`); } // 2. timeline 里的 warning 事件 → 高优建议 const warningEvents = (report.timeline || []).filter(e => e.severity === "warning"); const warnByDevice = new Map(); for (const e of warningEvents) { const prev = warnByDevice.get(e.device) || []; if (!prev.includes(e.type)) prev.push(e.type); warnByDevice.set(e.device, prev); } for (const [dev, types] of warnByDevice) { recs.push(`设备 ${dev} 存在 ${types.join("、")},建议尽快排查处理`); } // 3. 离线设备 → 处置建议 const offlineDevs = (report.timeline || []).filter(e => e.type === "offline"); const offlineHosts = [...new Set(offlineDevs.map(e => e.device))]; if (offlineHosts.length > 0) { recs.push(`离线设备(${offlineHosts.length} 台):${offlineHosts.join("、")} — 检查网络连通性与设备供电,排查 SSH 服务状态`); } // 4. 设备成功率低于 80% → 持续关注 const lowRate = Object.entries(report.summary?.deviceSuccessRate || {}) .filter(([, v]) => v < 0.8) .map(([dev]) => dev); if (lowRate.length > 0) { recs.push(`命令成功率偏低的设备(${lowRate.length} 台):${lowRate.join("、")} — 检查命令兼容性与设备固件版本`); } // 5. topFailedCommands 跨快照高频失败 → 建议排查 const topFails = (report.summary?.topFailedCommands || []).slice(0, 3); if (topFails.length > 0) { for (const f of topFails) { recs.push(`命令 "${f.cmd}" 在 ${f.count} 次巡检中失败 — 确认设备固件是否支持该命令`); } } } if (recs.length === 0) { return `

6. 综合运维结论与处置建议

当日系统运行正常,无需要处置的风险。

`; } // 分组 const criticalRecs = recs.filter(r => /立即|紧急|尽快|重启|更换|故障|掉电|critical|排查处置|离线设备/i.test(r)); const highRecs = recs.filter(r => /重要|尽快|排查|修复|异常|暴力|尽快排查/i.test(r)); const lowRecs = recs.filter(r => /建议|关注|定期|监控|计划|成功率偏低|命令.*失败/i.test(r)); const midRecs = recs.filter(r => !criticalRecs.includes(r) && !highRecs.includes(r) && !lowRecs.includes(r)); return `

6. 综合运维结论与处置建议

按紧急、高优、中优、低优分组输出,供运维团队参考。

${block("【紧急】★★★★★ 需要立即处置", "sev-critical", criticalRecs)} ${block("【高优】★★★★☆ 尽快处理", "sev-warning", highRecs)} ${block("【中优】★★★☆☆ 择机维护", "sev-info", midRecs)} ${block("【低优】★★☆☆☆ 持续关注", "sev-low", lowRecs)}`; })(); return ` 网络设备日运行巡检报告 ${dailyReportId}

${_sys}

${_fmtDate(report.timestamp || Date.now())} 网络设备巡检日报告
${scoreHtml} ${tocHtml} ${overviewHtml} ${timelineHtml} ${riskHtml} ${deviceSuccessHtml} ${topFailHtml} ${recHtml} `; } // ─── 主入口 ─────────────────────────────────────── async function exportSnapshotPdf(app, snapshot, aiSummary, customPath, systemName) { const filePath = customPath || (() => { const dir = _dateDir(app, snapshot.timestamp); const fileName = `snap_${snapshot.id}_${Date.now()}.pdf`; return path.join(dir, fileName); })(); const html = renderSnapshotHtml(snapshot, aiSummary, systemName); const pdf = await _printHtmlToPdf(html); fs.writeFileSync(filePath, pdf); console.log(`[reportExport] 快照 PDF: ${filePath}${aiSummary ? " (含AI汇总)" : ""}`); return filePath; } async function exportDailyReportPdf(app, report, customPath, allSnapshots, systemName) { const filePath = customPath || (() => { const dir = _dateDir(app, report.timestamp || Date.now()); const fileName = `daily_${report.id}_${Date.now()}.pdf`; return path.join(dir, fileName); })(); const html = renderDailyReportHtml(report, allSnapshots, systemName); const pdf = await _printHtmlToPdf(html); fs.writeFileSync(filePath, pdf); console.log(`[reportExport] 日报告 PDF: ${filePath}`); return filePath; } module.exports = { renderSnapshotHtml, renderDailyReportHtml, calcSnapshotHealthScore, calcDailyHealthScore, exportSnapshotPdf, exportDailyReportPdf, /** 内嵌 BrowserWindow 预览(temp 目录 + 自动清理) */ async previewSnapshotPdf(app, snapshot, aiSummary, systemName) { const { BrowserWindow } = _getElectron(); const tmpDir = app.getPath("temp"); const filePath = path.join(tmpDir, `netcatty-preview-snap_${Date.now()}.pdf`); const html = renderSnapshotHtml(snapshot, aiSummary, systemName); const pdf = await _printHtmlToPdf(html); fs.writeFileSync(filePath, pdf); const win = new BrowserWindow({ width: 1100, height: 800, show: true, autoHideMenuBar: true, title: `巡检快照预览 · ${_snapReportId(snapshot)}`, }); win.setMenuBarVisibility(false); const urlPath = filePath.replace(/\\/g, "/"); const url = `file://${urlPath.startsWith("/") ? "" : "/"}${urlPath}`; win.loadURL(url); win.once("closed", () => { try { fs.unlinkSync(filePath); } catch (_) {} }); return { ok: true, filePath }; }, async previewDailyReportPdf(app, report, allSnapshots, systemName) { const { BrowserWindow } = _getElectron(); const tmpDir = app.getPath("temp"); const filePath = path.join(tmpDir, `netcatty-preview-daily_${Date.now()}.pdf`); const html = renderDailyReportHtml(report, allSnapshots, systemName); const pdf = await _printHtmlToPdf(html); fs.writeFileSync(filePath, pdf); const win = new BrowserWindow({ width: 1100, height: 800, show: true, autoHideMenuBar: true, title: `日运行报告预览 · ${report.date}`, }); win.setMenuBarVisibility(false); const urlPath = filePath.replace(/\\/g, "/"); const url = `file://${urlPath.startsWith("/") ? "" : "/"}${urlPath}`; win.loadURL(url); win.once("closed", () => { try { fs.unlinkSync(filePath); } catch (_) {} }); return { ok: true, filePath }; }, };