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

160 lines
6.1 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";
/**
* 巡检报告定时调度器
*
* 和 inspectionScheduler.cjs **物理隔离**:
* - 独立 tick 循环(1min)
* - 独立配置存储(reportScheduleStore)
* - 独立触发路径(调 index.cjs 里 runReportSnapshot 的实现函数)
* - 独立日报告聚合(dailyReportStore)
*
* 防止重蹈「两个调度器竞争同一巡检任务」的覆辙。
*/
const reportScheduleStore = require("./reportScheduleStore.cjs");
const reportStore = require("./reportStore.cjs");
const dailyReportStore = require("./dailyReportStore.cjs");
let _tickTimer = null;
let _dailyTimer = null;
let _lastDailyTriggerKey = null; // 用于 HH:MM 日报告每日只触发一次
/**
* 启动 reportScheduler
*
* @param {object} deps
* @param {function} deps.readStorage
* @param {function} deps.writeStorage
* @param {function} deps.runSnapshotFn - 定时快照执行函数(renderer 侧 Hook 实际负责执行,这里传空函数)
* @param {function} [deps.onDailyReportReady] - 日报告聚合完成后回调 async (report) => void
*/
function start({ readStorage, writeStorage, runSnapshotFn, onDailyReportReady, getAllDeviceIdsFn }) {
if (_tickTimer) return; // 已启动
const readFn = (k) => readStorage(k);
const writeFn = (k, v) => writeStorage(k, v);
const _getAllDeviceIds = getAllDeviceIdsFn;
const tick = async () => {
const cfg = reportScheduleStore.getSchedule(readFn);
if (!cfg.enabled) return;
const now = Date.now();
const intervalMs = (cfg.defaultIntervalHours || 1) * 3600 * 1000;
const lastRun = cfg.lastRunAt || 0;
const sinceLast = now - lastRun;
// 还没到间隔就跳过
if (lastRun > 0 && sinceLast < intervalMs) {
const remainingMin = ((intervalMs - sinceLast) / 60000).toFixed(1);
console.log(`[reportScheduler] tick: 未到间隔,还需 ${remainingMin} 分钟`);
return;
}
// null/undefined = 全部;空数组 = 不巡检;非空数组 = 指定设备
let deviceIds = cfg.enabledDeviceIds;
if (deviceIds === null || deviceIds === undefined) {
// 全部设备 —— 通过回调拿(由调用方注入,避免依赖不存在的 deviceStore.listDevices)
if (typeof _getAllDeviceIds === "function") {
deviceIds = _getAllDeviceIds();
} else {
console.error("[reportScheduler] ❌ 无 getAllDeviceIds 回调,跳过");
return;
}
} else if (Array.isArray(deviceIds) && deviceIds.length === 0) {
console.log("[reportScheduler] tick: enabledDeviceIds=[], 跳过(不巡检任何设备)");
return;
}
// 过滤掉 isDeviceEnabled=false 的(disabled 状态)
const enabledDeviceIds = (deviceIds || []).filter(id =>
reportScheduleStore.isDeviceEnabled(readFn, id)
);
if (enabledDeviceIds.length === 0) {
console.log("[reportScheduler] tick: 0 台设备启用,跳过");
return;
}
console.log(`[reportScheduler] tick: 到点,${enabledDeviceIds.length} 台设备,interval=${cfg.defaultIntervalHours}h,距上次 ${(sinceLast/60000).toFixed(1)} 分钟`);
try {
await runSnapshotFn(enabledDeviceIds, "scheduled");
// 持久化 lastRunAt —— 重启 Electron 也知道上次什么时候跑的
const merged = { ...cfg, lastRunAt: now };
reportScheduleStore.saveSchedule(writeFn, merged);
console.log("[reportScheduler] ✅ 快照完成,lastRunAt 已持久化");
} catch (e) {
console.error("[reportScheduler] runSnapshot 失败:", e?.message || String(e));
}
};
// 启动后等 60s tick 自动处理第一次(不立即触发,避免重启爆发)
const curCfg = reportScheduleStore.getSchedule(readFn);
console.log("[reportScheduler] 已启动,间隔", (curCfg.defaultIntervalHours || 1), "h,下次 tick 检查在 60s 后");
// 每 60 秒 tick 一次
_tickTimer = setInterval(() => { tick().catch(e => console.error("[reportScheduler] tick 异常:", e)); }, 60 * 1000);
_tickTimer.unref?.();
// 每秒检查是否到 dailyReportTime(HH:MM 精确匹配,每日只触发一次)
const dailyCheck = () => {
const cfg = reportScheduleStore.getSchedule(readFn);
if (!cfg.enabled) return;
const now = new Date();
const p = (n) => n.toString().padStart(2, "0");
const current = `${p(now.getHours())}:${p(now.getMinutes())}`;
const key = `${now.getFullYear()}-${p(now.getMonth() + 1)}-${p(now.getDate())}`;
if (current === cfg.dailyReportTime && _lastDailyTriggerKey !== key) {
_lastDailyTriggerKey = key;
console.log("[reportScheduler] 触发日运行报告:", key);
triggerDailyReport(readFn, writeFn, { mode: "finalized", trigger: "scheduled" })
.then(report => {
if (onDailyReportReady) {
onDailyReportReady(report).catch(e =>
console.error("[reportScheduler] onDailyReportReady 异常:", e?.message || String(e))
);
}
})
.catch(e => console.error("[reportScheduler] 日报告生成异常:", e));
}
};
_dailyTimer = setInterval(dailyCheck, 30 * 1000);
_dailyTimer.unref?.();
}
function stop() {
if (_tickTimer) { clearInterval(_tickTimer); _tickTimer = null; }
if (_dailyTimer) { clearInterval(_dailyTimer); _dailyTimer = null; }
console.log("[reportScheduler] 已停止");
}
function isRunning() {
return !!_tickTimer;
}
/**
* 手动触发日运行报告(立即执行,不等到 HH:MM)
* mode: "provisional" — 取当天 0:00 ~ 此刻
* "finalized" — 取当天 0:00 ~ 23:59
*/
async function triggerDailyReport(readFn, writeFn, { mode = "provisional", trigger = "manual" } = {}) {
const now = Date.now();
const dayStart = dailyReportStore._dayStart(now);
const dayEnd = mode === "finalized" ? dailyReportStore._dayEnd(now) : now;
const allSnapshots = reportStore.listSnapshots(readFn, 1000);
const report = dailyReportStore.buildDailyReport(allSnapshots, dayStart, dayEnd, { mode, trigger });
dailyReportStore.addDailyReport(readFn, writeFn, report);
console.log(`[reportScheduler] 日报告 ${report.id} 已生成 (mode=${mode})`);
return report;
}
module.exports = {
start,
stop,
isRunning,
triggerDailyReport,
};