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
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:
289
electron/bridges/inspection/dailyReportStore.cjs
Normal file
289
electron/bridges/inspection/dailyReportStore.cjs
Normal file
@@ -0,0 +1,289 @@
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* 日运行报告存储 + 聚合
|
||||
*
|
||||
* 每次生成日报告一条:
|
||||
* {
|
||||
* id, // "daily_<date>_<mode>"
|
||||
* 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) {
|
||||
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);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
MAX_DAILY_REPORTS,
|
||||
buildDailyReport,
|
||||
addDailyReport,
|
||||
listDailyReports,
|
||||
deleteDailyReport,
|
||||
clearAll,
|
||||
_dayStart,
|
||||
_dayEnd,
|
||||
_dateStr,
|
||||
};
|
||||
Reference in New Issue
Block a user