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
巡检模块:巡检调度器改为轮询驱动,新增每日报告存储、告警规则独立 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 运行时依赖。
236 lines
5.9 KiB
JavaScript
236 lines
5.9 KiB
JavaScript
"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,
|
||
};
|