2026-10-19 21:11:57 +08:00
|
|
|
|
"use strict";
|
2026-10-15 13:53:04 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 巡检报告快照存储
|
|
|
|
|
|
*
|
|
|
|
|
|
* 每次 runReportSnapshot 生成一条快照:
|
|
|
|
|
|
*
|
|
|
|
|
|
* {
|
|
|
|
|
|
* id, // "snap_<ts>_<rand>"
|
|
|
|
|
|
* timestamp, // 快照生成时间
|
|
|
|
|
|
* duration_ms, // 整体耗时
|
|
|
|
|
|
* trigger, // "manual" | "scheduled"
|
|
|
|
|
|
* devices: [ // 每台设备的报告
|
|
|
|
|
|
* {
|
|
|
|
|
|
* device_id,
|
|
|
|
|
|
* hostname,
|
|
|
|
|
|
* catalog_id,
|
|
|
|
|
|
* ok, // 设备上所有命令都 ok = true
|
|
|
|
|
|
* duration_ms,
|
|
|
|
|
|
* commands: [ // 每一条命令的执行结果
|
|
|
|
|
|
* { cmd, purpose, ok, duration_ms, stdout, stderr }
|
|
|
|
|
|
* ]
|
|
|
|
|
|
* }
|
|
|
|
|
|
* ]
|
|
|
|
|
|
* }
|
|
|
|
|
|
*
|
|
|
|
|
|
* 为避免 JSON 无限膨胀,MAX_SNAPSHOTS 条整体保留,每条设备 stdout 截断 MAX_CMD_STDOUT 字符。
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
const { STORAGE_KEY_REPORT_SNAPSHOTS } = require("./storageKeys.cjs");
|
|
|
|
|
|
|
|
|
|
|
|
const MAX_SNAPSHOTS = 200; // 保留最近 200 次快照
|
|
|
|
|
|
const MAX_CMD_STDOUT = 4000; // 单条命令 stdout 保留前 4000 字符(display logbuffer 可能 10k+)
|
|
|
|
|
|
|
|
|
|
|
|
let _snapshots = null;
|
|
|
|
|
|
|
|
|
|
|
|
function _safeParse(text, def) {
|
2026-10-19 21:11:57 +08:00
|
|
|
|
if (text === null || text === undefined) return def;
|
|
|
|
|
|
if (typeof text === "object") return text;
|
2026-10-15 13:53:04 +08:00
|
|
|
|
try { return JSON.parse(text); }
|
2026-10-19 21:11:57 +08:00
|
|
|
|
catch { return def; }
|
2026-10-15 13:53:04 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function _ensureLoaded(readFn) {
|
|
|
|
|
|
if (_snapshots === null) {
|
|
|
|
|
|
const raw = readFn(STORAGE_KEY_REPORT_SNAPSHOTS);
|
|
|
|
|
|
_snapshots = raw ? _safeParse(raw, []) : [];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function _save(writeFn) {
|
|
|
|
|
|
if (_snapshots.length > MAX_SNAPSHOTS) {
|
|
|
|
|
|
_snapshots = _snapshots.slice(-MAX_SNAPSHOTS);
|
|
|
|
|
|
}
|
|
|
|
|
|
writeFn(STORAGE_KEY_REPORT_SNAPSHOTS, JSON.stringify(_snapshots));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function _truncate(text, max = MAX_CMD_STDOUT) {
|
|
|
|
|
|
if (!text) return "";
|
|
|
|
|
|
if (text.length <= max) return text;
|
|
|
|
|
|
return text.slice(0, max) + `\n... [truncated ${text.length - max} chars]`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------- 增删改查 ----------
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 新增一条快照。snapshot.devices[].commands[].stdout 会自动截断。
|
|
|
|
|
|
*/
|
|
|
|
|
|
function addSnapshot(readFn, writeFn, snapshot) {
|
|
|
|
|
|
_ensureLoaded(readFn);
|
|
|
|
|
|
|
|
|
|
|
|
// 自动截断 stdout
|
|
|
|
|
|
const cleanDevices = (snapshot.devices || []).map(d => ({
|
|
|
|
|
|
...d,
|
|
|
|
|
|
commands: (d.commands || []).map(c => ({
|
|
|
|
|
|
...c,
|
|
|
|
|
|
stdout: _truncate(c.stdout || ""),
|
|
|
|
|
|
stderr: _truncate(c.stderr || "", 500),
|
|
|
|
|
|
})),
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
const record = {
|
|
|
|
|
|
id: snapshot.id || `snap_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
|
|
|
|
timestamp: snapshot.timestamp || Date.now(),
|
|
|
|
|
|
duration_ms: snapshot.duration_ms || 0,
|
|
|
|
|
|
trigger: snapshot.trigger || "manual",
|
|
|
|
|
|
devices: cleanDevices,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
_snapshots.push(record);
|
|
|
|
|
|
_save(writeFn);
|
|
|
|
|
|
return { ...record };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 列出快照(新的在前)
|
|
|
|
|
|
*/
|
|
|
|
|
|
function listSnapshots(readFn, limit = 50) {
|
|
|
|
|
|
_ensureLoaded(readFn);
|
|
|
|
|
|
return [..._snapshots].reverse().slice(0, limit);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 按 id 查单条快照
|
|
|
|
|
|
*/
|
|
|
|
|
|
function getSnapshot(readFn, id) {
|
|
|
|
|
|
_ensureLoaded(readFn);
|
|
|
|
|
|
return _snapshots.find(s => s.id === id) || null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 删除一条快照
|
|
|
|
|
|
*/
|
|
|
|
|
|
function deleteSnapshot(readFn, writeFn, id) {
|
|
|
|
|
|
_ensureLoaded(readFn);
|
|
|
|
|
|
const before = _snapshots.length;
|
|
|
|
|
|
_snapshots = _snapshots.filter(s => s.id !== id);
|
|
|
|
|
|
if (_snapshots.length < before) {
|
|
|
|
|
|
_save(writeFn);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 清空所有快照
|
|
|
|
|
|
*/
|
|
|
|
|
|
function clearAll(readFn, writeFn) {
|
|
|
|
|
|
_ensureLoaded(readFn);
|
|
|
|
|
|
_snapshots = [];
|
|
|
|
|
|
_save(writeFn);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 统计概览(用于日运行报告聚合)
|
|
|
|
|
|
*/
|
|
|
|
|
|
function summarize(readFn, sinceTs) {
|
|
|
|
|
|
_ensureLoaded(readFn);
|
|
|
|
|
|
const snaps = _snapshots.filter(s => s.timestamp >= sinceTs);
|
|
|
|
|
|
|
|
|
|
|
|
let totalDevices = 0;
|
|
|
|
|
|
let totalCmds = 0;
|
|
|
|
|
|
let okCmds = 0;
|
|
|
|
|
|
let failedCmds = 0;
|
|
|
|
|
|
const failedDevices = new Set();
|
|
|
|
|
|
|
|
|
|
|
|
for (const snap of snaps) {
|
|
|
|
|
|
for (const dev of snap.devices || []) {
|
|
|
|
|
|
totalDevices++;
|
|
|
|
|
|
for (const cmd of dev.commands || []) {
|
|
|
|
|
|
totalCmds++;
|
|
|
|
|
|
if (cmd.ok) okCmds++;
|
|
|
|
|
|
else { failedCmds++; failedDevices.add(dev.device_id); }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
snapshot_count: snaps.length,
|
|
|
|
|
|
total_device_checks: totalDevices,
|
|
|
|
|
|
total_commands: totalCmds,
|
|
|
|
|
|
ok_commands: okCmds,
|
|
|
|
|
|
failed_commands: failedCmds,
|
|
|
|
|
|
failed_device_ids: Array.from(failedDevices),
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 标记快照 AI 分析状态(running 主进程在跑、done 成功、failed 失败)
|
|
|
|
|
|
*/
|
|
|
|
|
|
function setSnapshotAiStatus(readFn, writeFn, snapshotId, status, error) {
|
|
|
|
|
|
_ensureLoaded(readFn);
|
|
|
|
|
|
const snap = _snapshots.find(s => s.id === snapshotId);
|
|
|
|
|
|
if (!snap) return false;
|
|
|
|
|
|
snap.ai_summary_status = status; // "running" | "done" | "failed" | undefined
|
|
|
|
|
|
if (error) snap.ai_summary_error = error;
|
|
|
|
|
|
else delete snap.ai_summary_error;
|
|
|
|
|
|
if (status === "running") {
|
|
|
|
|
|
delete snap.ai_summary;
|
|
|
|
|
|
delete snap.ai_summary_updated_at;
|
|
|
|
|
|
}
|
|
|
|
|
|
_save(writeFn);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 更新快照的 AI 汇总(AI 成功分析后存回,status 自动设为 done)
|
|
|
|
|
|
*/
|
|
|
|
|
|
function updateSnapshotAiSummary(readFn, writeFn, snapshotId, aiSummary, error) {
|
|
|
|
|
|
_ensureLoaded(readFn);
|
|
|
|
|
|
const snap = _snapshots.find(s => s.id === snapshotId);
|
|
|
|
|
|
if (!snap) return false;
|
|
|
|
|
|
if (error) {
|
|
|
|
|
|
snap.ai_summary_status = "failed";
|
|
|
|
|
|
snap.ai_summary_error = error;
|
|
|
|
|
|
delete snap.ai_summary;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
snap.ai_summary_status = "done";
|
|
|
|
|
|
snap.ai_summary = aiSummary;
|
|
|
|
|
|
snap.ai_summary_updated_at = Date.now();
|
|
|
|
|
|
delete snap.ai_summary_error;
|
|
|
|
|
|
}
|
|
|
|
|
|
_save(writeFn);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 清空快照的 AI 汇总(用于重试前清除旧结果)
|
|
|
|
|
|
*/
|
|
|
|
|
|
function clearSnapshotAiSummary(readFn, writeFn, snapshotId) {
|
|
|
|
|
|
_ensureLoaded(readFn);
|
|
|
|
|
|
const snap = _snapshots.find(s => s.id === snapshotId);
|
|
|
|
|
|
if (!snap) return false;
|
|
|
|
|
|
delete snap.ai_summary;
|
|
|
|
|
|
delete snap.ai_summary_updated_at;
|
|
|
|
|
|
_save(writeFn);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-19 21:11:57 +08:00
|
|
|
|
function _resetForTest() { _snapshots = null; }
|
|
|
|
|
|
|
2026-10-15 13:53:04 +08:00
|
|
|
|
module.exports = {
|
|
|
|
|
|
MAX_SNAPSHOTS,
|
|
|
|
|
|
MAX_CMD_STDOUT,
|
|
|
|
|
|
addSnapshot,
|
|
|
|
|
|
listSnapshots,
|
|
|
|
|
|
getSnapshot,
|
|
|
|
|
|
deleteSnapshot,
|
|
|
|
|
|
clearAll,
|
|
|
|
|
|
summarize,
|
|
|
|
|
|
setSnapshotAiStatus,
|
|
|
|
|
|
updateSnapshotAiSummary,
|
|
|
|
|
|
clearSnapshotAiSummary,
|
2026-10-19 21:11:57 +08:00
|
|
|
|
_resetForTest,
|
2026-10-15 13:53:04 +08:00
|
|
|
|
};
|