refactor: 重构巡检报告调度器,移除设备依赖并优化重试逻辑
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

- 重构调度器,改为注入获取设备ID的回调,移除硬依赖的deviceStore
- 调整前端设备配置同步逻辑,改为直接读取解密后的本地存储
- 修改重试间隔,从15秒改为3秒,首次同步改为立即执行
- 调整日志级别,优化错误信息输出
This commit is contained in:
2026-10-15 14:15:16 +08:00
parent 2f9f8da48e
commit 2561001dca
6 changed files with 77 additions and 59 deletions

View File

@@ -24,6 +24,8 @@ import {
inspectionSchedulerApi,
type ReportSchedule,
} from "../../infrastructure/inspection/inspectionApi";
import { STORAGE_KEY_HOSTS, STORAGE_KEY_KEYS } from "../../infrastructure/config/storageKeys";
import { decryptHosts, decryptKeys } from "../../infrastructure/persistence/secureFieldAdapter";
interface HostConfig {
device_id: string;
@@ -36,19 +38,39 @@ interface HostConfig {
catalog_id?: string;
}
/** 从 vault 解密 + device extension 构建所有巡检设备 target */
/** 从 localStorage 读加密 hosts/keys → 解密 → 构建所有巡检设备 target */
async function buildTargets(enabledDeviceIds?: string[]): Promise<HostConfig[]> {
const api = (window as any).netcatty;
const hosts = await api?.getHosts?.() ?? [];
const keys = await api?.getSSHKeys?.() ?? [];
// 1. 从 localStorage 读加密的 hosts/keys(VaultPublisher 已解密过的,或自己解密)
let hosts: any[] = [];
let keys: any[] = [];
try {
const rawHosts = localStorage.getItem(STORAGE_KEY_HOSTS);
if (rawHosts) hosts = JSON.parse(rawHosts);
} catch (_) {}
try {
const rawKeys = localStorage.getItem(STORAGE_KEY_KEYS);
if (rawKeys) keys = JSON.parse(rawKeys);
} catch (_) {}
// 2. 解密
let decryptedHosts: any[] = [];
let decryptedKeys: any[] = [];
try { decryptedHosts = await decryptHosts(hosts); } catch (_) { decryptedHosts = hosts; }
try { decryptedKeys = await decryptKeys(keys); } catch (_) { decryptedKeys = keys; }
if (decryptedHosts.length === 0) {
console.error("[ReportAutoScheduler] localStorage 里没有 hosts!hosts=", hosts.length);
return [];
}
// 3. 读 device extension(catalog_id 等)
const exts = await inspectionDeviceApi.getAll().catch(() => ({}));
const targets: HostConfig[] = [];
for (const host of hosts) {
for (const host of decryptedHosts) {
if (enabledDeviceIds && enabledDeviceIds.length > 0 && !enabledDeviceIds.includes(host.id)) continue;
const ext = exts?.[host.id];
const key = keys.find((k: any) => k.id === host.identityFileId);
const key = decryptedKeys.find((k: any) => k.id === host.identityFileId);
const target: HostConfig = {
device_id: host.id,
@@ -129,22 +151,22 @@ export function useReportAutoScheduler() {
try {
const targets = await buildTargets();
if (targets.length === 0) {
// Vault 还没加载好,稍后再试
setTimeout(() => { void syncOnce(); }, 15000);
console.error("[ReportAutoScheduler] buildTargets=0,Vault 可能还没加载,3s 后重试");
setTimeout(() => { void syncOnce(); }, 3000);
return;
}
const hostConfigs = toSchedulerHostConfigs(targets);
await inspectionSchedulerApi.start(hostConfigs);
hostsSyncedRef.current = true;
console.log(`[ReportAutoScheduler] hostConfigs 已同步给主进程,${Object.keys(hostConfigs).length} 台设备`);
console.error(`[ReportAutoScheduler] ✅ hostConfigs 已同步给主进程,${Object.keys(hostConfigs).length} 台设备`);
} catch (e) {
console.warn("[ReportAutoScheduler] 同步 hostConfigs 失败:", e?.message);
setTimeout(() => { void syncOnce(); }, 15000);
console.error("[ReportAutoScheduler] ❌ 同步 hostConfigs 失败:", e?.message);
setTimeout(() => { void syncOnce(); }, 3000);
}
};
// 12 秒后首次同步(等 VaultPublisher 解密 hosts/keys + schedule 加载)
const timer = setTimeout(() => { void syncOnce(); }, 12000);
return () => clearTimeout(timer);
// 立即尝试 + 失败每 3 秒重试
void syncOnce();
return () => { hostsSyncedRef.current = false; };
}, []);
const tick = useCallback(async () => {

View File

@@ -41,12 +41,10 @@ module.exports = {
// making the app icon look smaller than other apps in taskbar /
// Start menu / desktop shortcuts.
icon: 'public/icon.png',
// npmRebuild must stay enabled for macOS and Windows builds — without it,
// node-pty's native module is not recompiled for the Electron ABI, causing
// "posix_spawnp failed" on macOS. Linux builds set npm_config_arch in CI
// and run ensure-node-pty-linux.sh before packaging, so the rebuild is
// redundant but harmless there.
npmRebuild: true,
// npmRebuild 默认开启,会尝试下载 Electron headers 重新编译所有 native modules。
// 内网环境访问 www.electronjs.org 超时,关闭它直接用 dev 模式下已编译好的 native deps。
// 如果 release 后 native modules 在目标机器上报 ABI mismatch,再回来开它 + 配 npmmirror 的 ELECTRON_MIRROR。
npmRebuild: false,
directories: {
buildResources: 'build',
output: 'release'
@@ -243,22 +241,18 @@ module.exports = {
},
win: {
icon: 'build/icon.ico',
// Do not hard-code arch here. pack:win-x64 / pack:win pass --x64/--arm64,
// and electron-builder unions config arch with the CLI set. Hard-coding
// ['x64', 'arm64'] made the official x64 CI job also emit win-arm64 and
// a universal win.exe whose 32-bit NSIS stub can mis-detect on ARM
// Windows, install to Program Files (x86), and leave Netcatty.exe
// missing (#2570). Keep official releases on pack:win-x64 until win32
// arm64 bundled mosh/et + native rebuilds are ready.
target: ['nsis', 'portable', 'zip'],
extraResources: [
...moshExtraResources('win32'),
...etExtraResources('win32'),
{
from: 'electron/bridges/windowsHelloHelper/build/${arch}/NetcattyWindowsHello.exe',
to: 'windowsHello/NetcattyWindowsHello.exe',
filter: ['**/*']
}
// Windows Hello helper 是可选的 —— 如果系统没装 VS C++ 工具链就跳过
// 构建(见 scripts/beforePackCursorSdk.cjs),这里也不要硬引用一个可能
// 不存在的二进制,否则 electron-builder 打包时会因为路径不存在直接失败。
// {
// from: 'electron/bridges/windowsHelloHelper/build/${arch}/NetcattyWindowsHello.exe',
// to: 'windowsHello/NetcattyWindowsHello.exe',
// filter: ['**/*']
// }
]
},
portable: {

View File

@@ -1164,6 +1164,7 @@ function registerInspectionHandlers(ipcMain, options) {
readStorage: options.readStorage,
writeStorage: options.writeStorage,
runSnapshotFn: (deviceIds, trigger) => _runScheduledSnapshot(deviceIds, trigger),
getAllDeviceIdsFn: () => Object.keys(_hostConfigs || {}),
});
}
return saved;
@@ -1178,6 +1179,7 @@ function registerInspectionHandlers(ipcMain, options) {
readStorage: options.readStorage,
writeStorage: options.writeStorage,
runSnapshotFn: (deviceIds, trigger) => _runScheduledSnapshot(deviceIds, trigger),
getAllDeviceIdsFn: () => Object.keys(_hostConfigs || {}),
});
} else if (!cfg.enabled && reportScheduler.isRunning()) {
reportScheduler.stop();
@@ -1425,24 +1427,8 @@ function registerInspectionHandlers(ipcMain, options) {
console.log("[Inspection] IPC handlers 已注册");
return {
getScheduler: () => _scheduler,
createScheduler: (opts) => {
_scheduler = new InspectionScheduler({
readFn,
writeFn,
execFn: execCommand,
onAlertChanged: broadcastAlertChanged,
...opts,
});
return _scheduler;
},
};
// ─── Electron 启动时自动启动 reportScheduler ───
// 负责:per-device 定时快照 tick(主进程自身 SSH 执行) + dailyReport HH:MM 定时聚合
// 主进程从 _hostConfigs(InspectionScheduler 已初始化的已解密 SSH 凭据缓存)取配置
// 执行快照,不依赖 renderer 是否打开巡检报告页面
async function _runScheduledSnapshot(deviceIds, trigger = "scheduled") {
if (!_hostConfigs || Object.keys(_hostConfigs).length === 0) {
console.warn("[reportScheduler] _hostConfigs 未初始化,定时快照跳过(需先打开监控页面让 renderer 同步 SSH 凭据到主进程)");
@@ -1495,6 +1481,7 @@ function registerInspectionHandlers(ipcMain, options) {
readStorage: readFn,
writeStorage: writeFn,
runSnapshotFn: (deviceIds, trigger) => _runScheduledSnapshot(deviceIds, trigger),
getAllDeviceIdsFn: () => Object.keys(_hostConfigs || {}),
});
console.error("[RS_DEBUG] ✅ start() 已调用完成");
} else {
@@ -1503,6 +1490,20 @@ function registerInspectionHandlers(ipcMain, options) {
} catch (e) {
console.error("[RS_DEBUG] ❌ 启动失败:", e?.message || String(e), e?.stack);
}
return {
getScheduler: () => _scheduler,
createScheduler: (opts) => {
_scheduler = new InspectionScheduler({
readFn,
writeFn,
execFn: execCommand,
onAlertChanged: broadcastAlertChanged,
...opts,
});
return _scheduler;
},
};
}
module.exports = {

View File

@@ -30,11 +30,12 @@ let _deviceLastRun = new Map(); // deviceId → timestamp(用于 per-device
* @param {function} deps.runSnapshotFn - 定时快照执行函数(renderer 侧 Hook 实际负责执行,这里传空函数)
* @param {function} [deps.onDailyReportReady] - 日报告聚合完成后回调 async (report) => void
*/
function start({ readStorage, writeStorage, runSnapshotFn, onDailyReportReady }) {
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);
@@ -47,14 +48,12 @@ function start({ readStorage, writeStorage, runSnapshotFn, onDailyReportReady })
// null/undefined = 全部;空数组 = 不巡检;非空数组 = 指定设备
let deviceIds = cfg.enabledDeviceIds;
if (deviceIds === null || deviceIds === undefined) {
// 全部设备 —— 从 deviceStore 拉所有设备 ID
try {
const deviceStore = require("./deviceStore.cjs");
const all = deviceStore.listDevices(readFn);
deviceIds = all.map(d => d.host_id || d.id).filter(Boolean);
console.log("[reportScheduler] tick: enabledDeviceIds=null, deviceStore.listDevices →", all.length, "台 → ids=", JSON.stringify(deviceIds));
} catch (e) {
console.error("[reportScheduler] ❌ 读取全部设备失败:", e?.message, e?.stack?.split("\n").slice(0,3).join("\n"));
// 全部设备 —— 通过回调拿(由调用方注入,避免依赖不存在的 deviceStore.listDevices)
if (typeof _getAllDeviceIds === "function") {
deviceIds = _getAllDeviceIds();
console.error("[reportScheduler] tick: enabledDeviceIds=null, _getAllDeviceIds →", deviceIds?.length || 0, "台");
} else {
console.error("[reportScheduler] ❌ 无 getAllDeviceIds 回调,跳过");
return;
}
} else if (Array.isArray(deviceIds) && deviceIds.length === 0) {

View File

@@ -2100,7 +2100,7 @@ function createPreloadApi(ctx) {
resolveAlert: (alertId, reason, operator) => ipcRenderer.invoke("inspection:resolveAlert", alertId, reason, operator),
closeAlert: (alertId, note) => ipcRenderer.invoke("inspection:closeAlert", alertId, note),
updateAlertNote: (alertId, note) => ipcRenderer.invoke("inspection:updateAlertNote", alertId, note),
schedulerStart: () => ipcRenderer.invoke("inspection:scheduler:start"),
schedulerStart: (...args) => ipcRenderer.invoke("inspection:scheduler:start", ...args),
schedulerStop: () => ipcRenderer.invoke("inspection:scheduler:stop"),
schedulerStatus: () => ipcRenderer.invoke("inspection:scheduler:status"),
scanNow: (hostId) => ipcRenderer.invoke("inspection:scheduler:scanNow", hostId),

View File

@@ -78,7 +78,9 @@ function beforePackCursorSdk(context = {}) {
if (platform === "win32") {
const result = buildHelper({ projectDir, platform, arch });
if (result?.skipped) {
throw new Error(`Windows Hello helper was not built: ${result.reason || "unknown"}`);
console.warn(`[beforePackCursorSdk] Windows Hello helper skipped: ${result.reason || "unknown"} (non-fatal, continuing build)`);
} else if (result?.outputPath) {
console.log(`[beforePackCursorSdk] Windows Hello helper built: ${result.outputPath}`);
}
}
}