Files
SenMeshNet/electron/bridges/inspection/reportScheduler.cjs
zhaolei 336c3afd47
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
Star History / update (push) Has been cancelled
build: 内网环境下跳过 Windows node-pty 补丁构建并关闭 GitHub 发布
调整 electron-builder 配置,Windows 平台仅打包 zip 格式,注释掉 NSIS 和 portable 目标;
注释掉 Windows Hello helper 资源配置以避免路径不存在导致的打包失败;
将发布配置设为 null 以关闭 GitHub 发布请求;
修改 afterPack 脚本,跳过 Windows 下 node-pty ConPTY 补丁的重建和复制,因内网无 MSVC 工具链无法完成该操作。
2026-10-15 15:30:31 +08:00

159 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 自动处理第一次(不立即触发,避免重启爆发)
console.log("[reportScheduler] 已启动,间隔", (cfg.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,
};