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
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:
@@ -24,6 +24,8 @@ import {
|
|||||||
inspectionSchedulerApi,
|
inspectionSchedulerApi,
|
||||||
type ReportSchedule,
|
type ReportSchedule,
|
||||||
} from "../../infrastructure/inspection/inspectionApi";
|
} from "../../infrastructure/inspection/inspectionApi";
|
||||||
|
import { STORAGE_KEY_HOSTS, STORAGE_KEY_KEYS } from "../../infrastructure/config/storageKeys";
|
||||||
|
import { decryptHosts, decryptKeys } from "../../infrastructure/persistence/secureFieldAdapter";
|
||||||
|
|
||||||
interface HostConfig {
|
interface HostConfig {
|
||||||
device_id: string;
|
device_id: string;
|
||||||
@@ -36,19 +38,39 @@ interface HostConfig {
|
|||||||
catalog_id?: string;
|
catalog_id?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 从 vault 解密 + device extension 构建所有巡检设备 target */
|
/** 从 localStorage 读加密 hosts/keys → 解密 → 构建所有巡检设备 target */
|
||||||
async function buildTargets(enabledDeviceIds?: string[]): Promise<HostConfig[]> {
|
async function buildTargets(enabledDeviceIds?: string[]): Promise<HostConfig[]> {
|
||||||
const api = (window as any).netcatty;
|
// 1. 从 localStorage 读加密的 hosts/keys(VaultPublisher 已解密过的,或自己解密)
|
||||||
const hosts = await api?.getHosts?.() ?? [];
|
let hosts: any[] = [];
|
||||||
const keys = await api?.getSSHKeys?.() ?? [];
|
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 exts = await inspectionDeviceApi.getAll().catch(() => ({}));
|
||||||
|
|
||||||
const targets: HostConfig[] = [];
|
const targets: HostConfig[] = [];
|
||||||
for (const host of hosts) {
|
for (const host of decryptedHosts) {
|
||||||
if (enabledDeviceIds && enabledDeviceIds.length > 0 && !enabledDeviceIds.includes(host.id)) continue;
|
if (enabledDeviceIds && enabledDeviceIds.length > 0 && !enabledDeviceIds.includes(host.id)) continue;
|
||||||
const ext = exts?.[host.id];
|
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 = {
|
const target: HostConfig = {
|
||||||
device_id: host.id,
|
device_id: host.id,
|
||||||
@@ -129,22 +151,22 @@ export function useReportAutoScheduler() {
|
|||||||
try {
|
try {
|
||||||
const targets = await buildTargets();
|
const targets = await buildTargets();
|
||||||
if (targets.length === 0) {
|
if (targets.length === 0) {
|
||||||
// Vault 还没加载好,稍后再试
|
console.error("[ReportAutoScheduler] buildTargets=0,Vault 可能还没加载,3s 后重试");
|
||||||
setTimeout(() => { void syncOnce(); }, 15000);
|
setTimeout(() => { void syncOnce(); }, 3000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const hostConfigs = toSchedulerHostConfigs(targets);
|
const hostConfigs = toSchedulerHostConfigs(targets);
|
||||||
await inspectionSchedulerApi.start(hostConfigs);
|
await inspectionSchedulerApi.start(hostConfigs);
|
||||||
hostsSyncedRef.current = true;
|
hostsSyncedRef.current = true;
|
||||||
console.log(`[ReportAutoScheduler] hostConfigs 已同步给主进程,${Object.keys(hostConfigs).length} 台设备`);
|
console.error(`[ReportAutoScheduler] ✅ hostConfigs 已同步给主进程,${Object.keys(hostConfigs).length} 台设备`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("[ReportAutoScheduler] 同步 hostConfigs 失败:", e?.message);
|
console.error("[ReportAutoScheduler] ❌ 同步 hostConfigs 失败:", e?.message);
|
||||||
setTimeout(() => { void syncOnce(); }, 15000);
|
setTimeout(() => { void syncOnce(); }, 3000);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// 12 秒后首次同步(等 VaultPublisher 解密 hosts/keys + schedule 加载)
|
// 立即尝试 + 失败每 3 秒重试
|
||||||
const timer = setTimeout(() => { void syncOnce(); }, 12000);
|
void syncOnce();
|
||||||
return () => clearTimeout(timer);
|
return () => { hostsSyncedRef.current = false; };
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const tick = useCallback(async () => {
|
const tick = useCallback(async () => {
|
||||||
|
|||||||
@@ -41,12 +41,10 @@ module.exports = {
|
|||||||
// making the app icon look smaller than other apps in taskbar /
|
// making the app icon look smaller than other apps in taskbar /
|
||||||
// Start menu / desktop shortcuts.
|
// Start menu / desktop shortcuts.
|
||||||
icon: 'public/icon.png',
|
icon: 'public/icon.png',
|
||||||
// npmRebuild must stay enabled for macOS and Windows builds — without it,
|
// npmRebuild 默认开启,会尝试下载 Electron headers 重新编译所有 native modules。
|
||||||
// node-pty's native module is not recompiled for the Electron ABI, causing
|
// 内网环境访问 www.electronjs.org 超时,关闭它直接用 dev 模式下已编译好的 native deps。
|
||||||
// "posix_spawnp failed" on macOS. Linux builds set npm_config_arch in CI
|
// 如果 release 后 native modules 在目标机器上报 ABI mismatch,再回来开它 + 配 npmmirror 的 ELECTRON_MIRROR。
|
||||||
// and run ensure-node-pty-linux.sh before packaging, so the rebuild is
|
npmRebuild: false,
|
||||||
// redundant but harmless there.
|
|
||||||
npmRebuild: true,
|
|
||||||
directories: {
|
directories: {
|
||||||
buildResources: 'build',
|
buildResources: 'build',
|
||||||
output: 'release'
|
output: 'release'
|
||||||
@@ -243,22 +241,18 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
win: {
|
win: {
|
||||||
icon: 'build/icon.ico',
|
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'],
|
target: ['nsis', 'portable', 'zip'],
|
||||||
extraResources: [
|
extraResources: [
|
||||||
...moshExtraResources('win32'),
|
...moshExtraResources('win32'),
|
||||||
...etExtraResources('win32'),
|
...etExtraResources('win32'),
|
||||||
{
|
// Windows Hello helper 是可选的 —— 如果系统没装 VS C++ 工具链就跳过
|
||||||
from: 'electron/bridges/windowsHelloHelper/build/${arch}/NetcattyWindowsHello.exe',
|
// 构建(见 scripts/beforePackCursorSdk.cjs),这里也不要硬引用一个可能
|
||||||
to: 'windowsHello/NetcattyWindowsHello.exe',
|
// 不存在的二进制,否则 electron-builder 打包时会因为路径不存在直接失败。
|
||||||
filter: ['**/*']
|
// {
|
||||||
}
|
// from: 'electron/bridges/windowsHelloHelper/build/${arch}/NetcattyWindowsHello.exe',
|
||||||
|
// to: 'windowsHello/NetcattyWindowsHello.exe',
|
||||||
|
// filter: ['**/*']
|
||||||
|
// }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
portable: {
|
portable: {
|
||||||
|
|||||||
@@ -1164,6 +1164,7 @@ function registerInspectionHandlers(ipcMain, options) {
|
|||||||
readStorage: options.readStorage,
|
readStorage: options.readStorage,
|
||||||
writeStorage: options.writeStorage,
|
writeStorage: options.writeStorage,
|
||||||
runSnapshotFn: (deviceIds, trigger) => _runScheduledSnapshot(deviceIds, trigger),
|
runSnapshotFn: (deviceIds, trigger) => _runScheduledSnapshot(deviceIds, trigger),
|
||||||
|
getAllDeviceIdsFn: () => Object.keys(_hostConfigs || {}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return saved;
|
return saved;
|
||||||
@@ -1178,6 +1179,7 @@ function registerInspectionHandlers(ipcMain, options) {
|
|||||||
readStorage: options.readStorage,
|
readStorage: options.readStorage,
|
||||||
writeStorage: options.writeStorage,
|
writeStorage: options.writeStorage,
|
||||||
runSnapshotFn: (deviceIds, trigger) => _runScheduledSnapshot(deviceIds, trigger),
|
runSnapshotFn: (deviceIds, trigger) => _runScheduledSnapshot(deviceIds, trigger),
|
||||||
|
getAllDeviceIdsFn: () => Object.keys(_hostConfigs || {}),
|
||||||
});
|
});
|
||||||
} else if (!cfg.enabled && reportScheduler.isRunning()) {
|
} else if (!cfg.enabled && reportScheduler.isRunning()) {
|
||||||
reportScheduler.stop();
|
reportScheduler.stop();
|
||||||
@@ -1425,24 +1427,8 @@ function registerInspectionHandlers(ipcMain, options) {
|
|||||||
|
|
||||||
console.log("[Inspection] IPC handlers 已注册");
|
console.log("[Inspection] IPC handlers 已注册");
|
||||||
|
|
||||||
return {
|
|
||||||
getScheduler: () => _scheduler,
|
|
||||||
createScheduler: (opts) => {
|
|
||||||
_scheduler = new InspectionScheduler({
|
|
||||||
readFn,
|
|
||||||
writeFn,
|
|
||||||
execFn: execCommand,
|
|
||||||
onAlertChanged: broadcastAlertChanged,
|
|
||||||
...opts,
|
|
||||||
});
|
|
||||||
return _scheduler;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// ─── Electron 启动时自动启动 reportScheduler ───
|
// ─── Electron 启动时自动启动 reportScheduler ───
|
||||||
// 负责:per-device 定时快照 tick(主进程自身 SSH 执行) + dailyReport HH:MM 定时聚合
|
// 负责:per-device 定时快照 tick(主进程自身 SSH 执行) + dailyReport HH:MM 定时聚合
|
||||||
// 主进程从 _hostConfigs(InspectionScheduler 已初始化的已解密 SSH 凭据缓存)取配置
|
|
||||||
// 执行快照,不依赖 renderer 是否打开巡检报告页面
|
|
||||||
async function _runScheduledSnapshot(deviceIds, trigger = "scheduled") {
|
async function _runScheduledSnapshot(deviceIds, trigger = "scheduled") {
|
||||||
if (!_hostConfigs || Object.keys(_hostConfigs).length === 0) {
|
if (!_hostConfigs || Object.keys(_hostConfigs).length === 0) {
|
||||||
console.warn("[reportScheduler] _hostConfigs 未初始化,定时快照跳过(需先打开监控页面让 renderer 同步 SSH 凭据到主进程)");
|
console.warn("[reportScheduler] _hostConfigs 未初始化,定时快照跳过(需先打开监控页面让 renderer 同步 SSH 凭据到主进程)");
|
||||||
@@ -1495,6 +1481,7 @@ function registerInspectionHandlers(ipcMain, options) {
|
|||||||
readStorage: readFn,
|
readStorage: readFn,
|
||||||
writeStorage: writeFn,
|
writeStorage: writeFn,
|
||||||
runSnapshotFn: (deviceIds, trigger) => _runScheduledSnapshot(deviceIds, trigger),
|
runSnapshotFn: (deviceIds, trigger) => _runScheduledSnapshot(deviceIds, trigger),
|
||||||
|
getAllDeviceIdsFn: () => Object.keys(_hostConfigs || {}),
|
||||||
});
|
});
|
||||||
console.error("[RS_DEBUG] ✅ start() 已调用完成");
|
console.error("[RS_DEBUG] ✅ start() 已调用完成");
|
||||||
} else {
|
} else {
|
||||||
@@ -1503,6 +1490,20 @@ function registerInspectionHandlers(ipcMain, options) {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("[RS_DEBUG] ❌ 启动失败:", e?.message || String(e), e?.stack);
|
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 = {
|
module.exports = {
|
||||||
|
|||||||
@@ -30,11 +30,12 @@ let _deviceLastRun = new Map(); // deviceId → timestamp(用于 per-device
|
|||||||
* @param {function} deps.runSnapshotFn - 定时快照执行函数(renderer 侧 Hook 实际负责执行,这里传空函数)
|
* @param {function} deps.runSnapshotFn - 定时快照执行函数(renderer 侧 Hook 实际负责执行,这里传空函数)
|
||||||
* @param {function} [deps.onDailyReportReady] - 日报告聚合完成后回调 async (report) => void
|
* @param {function} [deps.onDailyReportReady] - 日报告聚合完成后回调 async (report) => void
|
||||||
*/
|
*/
|
||||||
function start({ readStorage, writeStorage, runSnapshotFn, onDailyReportReady }) {
|
function start({ readStorage, writeStorage, runSnapshotFn, onDailyReportReady, getAllDeviceIdsFn }) {
|
||||||
if (_tickTimer) return; // 已启动
|
if (_tickTimer) return; // 已启动
|
||||||
|
|
||||||
const readFn = (k) => readStorage(k);
|
const readFn = (k) => readStorage(k);
|
||||||
const writeFn = (k, v) => writeStorage(k, v);
|
const writeFn = (k, v) => writeStorage(k, v);
|
||||||
|
const _getAllDeviceIds = getAllDeviceIdsFn;
|
||||||
|
|
||||||
const tick = async () => {
|
const tick = async () => {
|
||||||
const cfg = reportScheduleStore.getSchedule(readFn);
|
const cfg = reportScheduleStore.getSchedule(readFn);
|
||||||
@@ -47,14 +48,12 @@ function start({ readStorage, writeStorage, runSnapshotFn, onDailyReportReady })
|
|||||||
// null/undefined = 全部;空数组 = 不巡检;非空数组 = 指定设备
|
// null/undefined = 全部;空数组 = 不巡检;非空数组 = 指定设备
|
||||||
let deviceIds = cfg.enabledDeviceIds;
|
let deviceIds = cfg.enabledDeviceIds;
|
||||||
if (deviceIds === null || deviceIds === undefined) {
|
if (deviceIds === null || deviceIds === undefined) {
|
||||||
// 全部设备 —— 从 deviceStore 拉所有设备 ID
|
// 全部设备 —— 通过回调拿(由调用方注入,避免依赖不存在的 deviceStore.listDevices)
|
||||||
try {
|
if (typeof _getAllDeviceIds === "function") {
|
||||||
const deviceStore = require("./deviceStore.cjs");
|
deviceIds = _getAllDeviceIds();
|
||||||
const all = deviceStore.listDevices(readFn);
|
console.error("[reportScheduler] tick: enabledDeviceIds=null, _getAllDeviceIds →", deviceIds?.length || 0, "台");
|
||||||
deviceIds = all.map(d => d.host_id || d.id).filter(Boolean);
|
} else {
|
||||||
console.log("[reportScheduler] tick: enabledDeviceIds=null, deviceStore.listDevices →", all.length, "台 → ids=", JSON.stringify(deviceIds));
|
console.error("[reportScheduler] ❌ 无 getAllDeviceIds 回调,跳过");
|
||||||
} catch (e) {
|
|
||||||
console.error("[reportScheduler] ❌ 读取全部设备失败:", e?.message, e?.stack?.split("\n").slice(0,3).join("\n"));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else if (Array.isArray(deviceIds) && deviceIds.length === 0) {
|
} else if (Array.isArray(deviceIds) && deviceIds.length === 0) {
|
||||||
|
|||||||
@@ -2100,7 +2100,7 @@ function createPreloadApi(ctx) {
|
|||||||
resolveAlert: (alertId, reason, operator) => ipcRenderer.invoke("inspection:resolveAlert", alertId, reason, operator),
|
resolveAlert: (alertId, reason, operator) => ipcRenderer.invoke("inspection:resolveAlert", alertId, reason, operator),
|
||||||
closeAlert: (alertId, note) => ipcRenderer.invoke("inspection:closeAlert", alertId, note),
|
closeAlert: (alertId, note) => ipcRenderer.invoke("inspection:closeAlert", alertId, note),
|
||||||
updateAlertNote: (alertId, note) => ipcRenderer.invoke("inspection:updateAlertNote", 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"),
|
schedulerStop: () => ipcRenderer.invoke("inspection:scheduler:stop"),
|
||||||
schedulerStatus: () => ipcRenderer.invoke("inspection:scheduler:status"),
|
schedulerStatus: () => ipcRenderer.invoke("inspection:scheduler:status"),
|
||||||
scanNow: (hostId) => ipcRenderer.invoke("inspection:scheduler:scanNow", hostId),
|
scanNow: (hostId) => ipcRenderer.invoke("inspection:scheduler:scanNow", hostId),
|
||||||
|
|||||||
@@ -78,7 +78,9 @@ function beforePackCursorSdk(context = {}) {
|
|||||||
if (platform === "win32") {
|
if (platform === "win32") {
|
||||||
const result = buildHelper({ projectDir, platform, arch });
|
const result = buildHelper({ projectDir, platform, arch });
|
||||||
if (result?.skipped) {
|
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}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user