Files
SenMeshNet/electron/bridges/inspection/dailyReportStore.cjs
zhaolei 2e71e03d71
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
feat: 巡检增强 / 拓扑大屏 / 远程 HTTP / portable 绿色打包
巡检模块:巡检调度器改为轮询驱动,新增每日报告存储、告警规则独立
Store、设备 Store 解耦,_executeSnapshot 补齐 DEBUG 声明,巡检中心
与告警中心页增强。

拓扑大屏:MonitoringTopologyView 扩展分组/布局控制,
useMonitoringData 重构数据流。

远程 HTTP:新增 electron/remoteHttpServer.cjs,preload/api.cjs 暴露
IPC,registerBridges 注册 netcatty:http:open 桥。

portable 绿色打包:
- electron-builder.config.cjs 恢复原始白名单(61MB app.asar),
  收集器自动追踪运行时依赖;
- portableData.cjs 重写为 .portable 标记检测;
- scripts/migratePortableLocalStorage.cjs 离线脚本把 Local Storage
  的 http://localhost:5173 origin 批量迁移为 app://netcatty;
- scripts/scanWorkspaceDeps.cjs 扫描 workspace 运行时依赖。
2026-10-19 21:11:57 +08:00

295 lines
8.8 KiB
JavaScript

"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) {
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,
};