"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} |
本次快照于 ${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} 秒 |
本次巡检所有设备均在线。
`; 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 `以下设备 SSH 会话建立失败,未采集任何设备内部数据;不再重复罗列单条命令失败记录。
| 设备 | 故障分类 | 故障描述 |
|---|
本次巡检未检测到需要关注的风险隐患。
`; } // 按 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 => `${r.device}按风险优先级从高到低排序
| 风险等级 | 设备 | 风险简述 |
|---|
本次巡检无在线设备。
`; 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}仅展示 SSH 登录成功设备;"正常 / 存在风险" 依据 AI 智能分析判定,无 AI 汇总时依据命令执行结果。
| 设备 | 设备类型 | 综合状态 | 关键观察 |
|---|
其余 ${onlineDevices.length - 50} 台在线设备省略展示
` : ""}`; })(); // ─── 5. 运维处置建议 ─── const recHtml = (() => { if (!aiSummary?.recommendations || aiSummary.recommendations.length === 0) { return `本次巡检未生成处置建议。
`; } // 把 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 ? "" : `本次巡检在线设备所有命令执行成功,无异常。
`; } const deviceBlocks = abnormal.map(d => { const failCmds = d.commands.filter(c => !c.ok); const cmdTable = `| 命令 | 用途 | 异常说明 |
|---|---|---|
| ${c.cmd} | ${c.purpose || "-"} | ${(c.stderr || c.stdout || "命令执行失败").slice(0, 100)} |
${d.hostname}(${d.catalog_id || "-"})命令成功 ${d.commands.filter(c => c.ok).length}/${d.commands.length},失败 ${failCmds.length} 条
${cmdTable} ${failOutputs} `; }).join(""); return `仅展示 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) => `| # | 命令 |
|---|
| 当日综合得分 | ${dailyScoreInfo.score} |
| 健康等级 | ${dailyScoreInfo.levelText} |
| 快照最高 / 最低 | ${dailyScoreInfo.max} / ${dailyScoreInfo.min} |
暂无快照数据。
`; } // 如果有真实快照分数(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}| 快照时间 | AI 健康摘要 | 快照得分 |
|---|
当日无异常事件。
`; } // 聚合:同设备 + 同类型 → 合并,统计出现次数,保留首次和末次时间,汇总事件简述 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.device}同一设备同类事件做统计,保留首次、末次发生时间与发生次数,减少重复行。
| 事件等级 | 设备 | 事件类型 | 次数 | 首次 | 末次 | 事件简述 |
|---|
当日快照尚未完成 AI 分析,暂无风险汇总。
`; } const rows = timelineRisks.map(r => `${r.device}| 风险等级 | 设备 | 风险简述 |
|---|
当日未检测到需要关注的风险。
`; 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 => `${r.device}跨快照聚合当日全部巡检快照的 AI 分析结果,按优先级排序。
| 风险等级 | 设备 | 风险简述 |
|---|
暂无设备数据。
`; 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)}| 设备 | 巡检成功率 | 综合状态 |
|---|
当日无命令失败。
`; const rows = fails.slice(0, 20).map((f, i) => `${_hostFor(f.device_id)}| # | 命令 | 涉及设备 | 失败次数 |
|---|
当日系统运行正常,无需要处置的风险。
`; } // 分组 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 `按紧急、高优、中优、低优分组输出,供运维团队参考。
${block("【紧急】★★★★★ 需要立即处置", "sev-critical", criticalRecs)} ${block("【高优】★★★★☆ 尽快处理", "sev-warning", highRecs)} ${block("【中优】★★★☆☆ 择机维护", "sev-info", midRecs)} ${block("【低优】★★☆☆☆ 持续关注", "sev-low", lowRecs)}`; })(); return `