Files
SenMeshNet/electron/bridges/inspection/reportStore.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

236 lines
5.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use strict";
/**
* 巡检报告快照存储
*
* 每次 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) {
if (text === null || text === undefined) return def;
if (typeof text === "object") return text;
try { return JSON.parse(text); }
catch { return def; }
}
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;
}
function _resetForTest() { _snapshots = null; }
module.exports = {
MAX_SNAPSHOTS,
MAX_CMD_STDOUT,
addSnapshot,
listSnapshots,
getSnapshot,
deleteSnapshot,
clearAll,
summarize,
setSnapshotAiStatus,
updateSnapshotAiSummary,
clearSnapshotAiSummary,
_resetForTest,
};