"use strict"; /** * 日运行报告存储 + 聚合 * * 每次生成日报告一条: * { * id, // "daily__" * date, // "2026-10-13" * mode, // "provisional" | "finalized" * timestamp, // 生成时间 * trigger, // "manual" | "scheduled" * period: { start_ts, end_ts }, // 覆盖的数据时间范围 * summary: { * snapshotCount, * deviceCount, * totalCmds, okCmds, failCmds, * failRate, * deviceSuccessRate: { host_a: 0.98, ... }, * topFailedCommands: [ { cmd, device_id, count } ] * }, * aiSummary: { ... } | null // AI 四段式汇总,可选 * } * * 独立文件,不与 snapshots 混存。 */ const { STORAGE_KEY_DAILY_REPORTS } = require("./storageKeys.cjs"); const MAX_DAILY_REPORTS = 120; // 保留最近约 4 个月(每天 1 条) function _safeParse(raw, def) { if (raw === null || raw === undefined) return def; if (typeof raw === "object") return raw; try { return JSON.parse(raw); } catch { return def; } } let _reports = null; function _ensureLoaded(readFn) { if (_reports === null) { const raw = readFn(STORAGE_KEY_DAILY_REPORTS); _reports = raw ? _safeParse(raw, []) : []; } } function _save(writeFn) { if (_reports.length > MAX_DAILY_REPORTS) { _reports = _reports.slice(-MAX_DAILY_REPORTS); } writeFn(STORAGE_KEY_DAILY_REPORTS, JSON.stringify(_reports, null, 2)); } function _dayStart(date) { const d = new Date(date); d.setHours(0, 0, 0, 0); return d.getTime(); } function _dayEnd(date) { const d = new Date(date); d.setHours(23, 59, 59, 999); return d.getTime(); } function _dateStr(ts) { const d = new Date(ts); const p = (n) => n.toString().padStart(2, "0"); return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`; } /** * 从一组 snapshots 聚合出一份日运行报告数据 * * @param {Array} snapshots - 快照数组 * @param {number} startTs - 数据时间范围起点 * @param {number} endTs - 数据时间范围终点 */ function buildDailyReport(snapshots, startTs, endTs, { mode, trigger }) { const filtered = snapshots.filter(s => s.timestamp >= startTs && s.timestamp <= endTs); const deviceMap = new Map(); // hostId -> { okCmds, totalCmds, okCount, totalCount } const cmdFailureMap = new Map(); // "host|cmd" -> count let totalCmds = 0, okCmds = 0; for (const snap of filtered) { for (const dev of snap.devices) { const prev = deviceMap.get(dev.device_id) || { okCmds: 0, totalCmds: 0 }; for (const cmd of dev.commands) { totalCmds++; if (cmd.ok) { okCmds++; prev.okCmds++; } prev.totalCmds++; if (!cmd.ok) { const key = `${dev.device_id}|${cmd.cmd}`; cmdFailureMap.set(key, (cmdFailureMap.get(key) || 0) + 1); } } deviceMap.set(dev.device_id, prev); } } // 设备成功率 const deviceSuccessRate = {}; for (const [devId, v] of deviceMap) { deviceSuccessRate[devId] = v.totalCmds > 0 ? Number((v.okCmds / v.totalCmds).toFixed(3)) : 0; } // Top 失败命令(最多 20 条) const topFailedCommands = Array.from(cmdFailureMap.entries()) .sort((a, b) => b[1] - a[1]) .slice(0, 20) .map(([key, count]) => { const [device_id, cmd] = key.split("|"); return { device_id, cmd, count }; }); const dateStr = _dateStr(endTs); // ═══ 新增:从快照 AI 汇总构建故障时间线 ═══ // 只有 ai_summary_status === "done" 的快照才贡献 timeline 事件 const timeline = []; const perSnapshotHighlights = []; // 每个快照的健康摘要(按时间序) for (const snap of filtered) { const ts = snap.timestamp; const ai = snap.ai_summary; const status = snap.ai_summary_status; const timeStr = new Date(ts).toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" }); const dateTimeStr = new Date(ts).toLocaleString("zh-CN", { hour: "2-digit", minute: "2-digit", month: "2-digit", day: "2-digit" }); if (ai && status === "done") { // 每个快照的健康摘要(时间线串起来的基础) perSnapshotHighlights.push({ timestamp: ts, time: timeStr, snapshot_id: snap.id, health_summary: ai.health_summary || "—", }); // 离线设备 → timeline event for (const dev of (ai.offline_devices || [])) { timeline.push({ timestamp: ts, time: timeStr, snapshot_id: snap.id, type: "offline", severity: "critical", device: dev.device || dev.hostname || dev, detail: dev.reason || "SSH 连接失败", }); } // 隐藏风险 → timeline event for (const risk of (ai.hidden_risks || [])) { timeline.push({ timestamp: ts, time: timeStr, snapshot_id: snap.id, type: "risk", severity: risk.severity || "warning", device: risk.device || "未知设备", detail: risk.observation || risk.message || String(risk), }); } // 命令异常(在 anomalies 数组里)→ timeline event for (const a of (ai.anomalies || [])) { timeline.push({ timestamp: ts, time: timeStr, snapshot_id: snap.id, type: "anomaly", severity: "warning", device: a.device || "未知设备", detail: a.cmd || a.message || String(a), }); } } else { // 无 AI 汇总的快照(可能 AI 失败/未触发),也记录基础事实 const offlineDevs = snap.devices.filter(d => !d.ok); if (offlineDevs.length > 0) { for (const d of offlineDevs) { timeline.push({ timestamp: ts, time: timeStr, snapshot_id: snap.id, type: "offline", severity: "critical", device: d.hostname, detail: "本次巡检整体失败", }); } } } } // 按时间升序排(时间线要串起来) timeline.sort((a, b) => a.timestamp - b.timestamp); // 设备状态矩阵(哪些设备在今天的哪些时间点出问题) const deviceStatusMatrix = {}; // hostId → [{ time, snapshot_id, status, note }] for (const snap of filtered) { const ts = snap.timestamp; const timeStr = new Date(ts).toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" }); for (const dev of snap.devices) { if (!deviceStatusMatrix[dev.device_id]) { deviceStatusMatrix[dev.device_id] = []; } let status = "ok"; let note = ""; if (!dev.ok) { status = "offline"; note = "SSH 连接失败"; } else { const failCmds = dev.commands.filter(c => !c.ok); if (failCmds.length > 0) { status = "partial"; note = `${failCmds.length} 条命令失败`; } } deviceStatusMatrix[dev.device_id].push({ time: timeStr, snapshot_id: snap.id, status, note, }); } } const report = { id: `daily_${dateStr}_${mode}`, date: dateStr, mode, timestamp: Date.now(), trigger, period: { start_ts: startTs, end_ts: endTs }, summary: { snapshotCount: filtered.length, deviceCount: deviceMap.size, totalCmds, okCmds, failCmds: totalCmds - okCmds, failRate: totalCmds > 0 ? Number(((totalCmds - okCmds) / totalCmds * 100).toFixed(1)) : 0, deviceSuccessRate, topFailedCommands, }, timeline, // ═══ 新增:故障时间线 ═══ perSnapshotHighlights, // ═══ 新增:每个快照的健康摘要(按时间)═══ deviceStatusMatrix, // ═══ 新增:设备×时间点状态矩阵 ═══ aiSummary: null, // 日报告自己的 AI 汇总(可选生成) }; return report; } /** * 保存一份日报告到存储,同一 date+mode 存在则覆盖 */ function addDailyReport(readFn, writeFn, report) { _ensureLoaded(readFn); const idx = _reports.findIndex(r => r.date === report.date && r.mode === report.mode); if (idx >= 0) { _reports[idx] = report; } else { _reports.push(report); } _reports.sort((a, b) => b.timestamp - a.timestamp); _save(writeFn); return report; } function listDailyReports(readFn, limit = 30) { _ensureLoaded(readFn); return _reports.slice(0, limit); } function deleteDailyReport(readFn, writeFn, id) { _ensureLoaded(readFn); const idx = _reports.findIndex(r => r.id === id); if (idx < 0) return false; _reports.splice(idx, 1); _save(writeFn); return true; } function clearAll(readFn, writeFn) { _reports = []; _save(writeFn); } function _resetForTest() { _reports = null; } module.exports = { MAX_DAILY_REPORTS, buildDailyReport, addDailyReport, listDailyReports, deleteDailyReport, clearAll, _dayStart, _dayEnd, _dateStr, _resetForTest, };