Initial commit: NetMesh smart ops platform with 3D topology, inspection reports, RCA and AI ops assistant
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
This commit is contained in:
199
application/state/useReportAutoScheduler.ts
Normal file
199
application/state/useReportAutoScheduler.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* 巡检报告定时快照(全局 Hook)
|
||||
*
|
||||
* 挂载到 App.tsx 顶层 → 不管用户在哪个 Tab 都能定时跑快照,关页面(巡检报告 Tab)也不停。
|
||||
* 只有彻底关闭 Electron 应用才会停。
|
||||
*
|
||||
* 双路执行:
|
||||
* - 主进程 reportScheduler(setInterval 1min)独立执行定时快照 + 日报告聚合
|
||||
* - renderer 侧(本 Hook)负责:1) 把 vault 解密的 SSH 凭据同步给主进程;2) 作为兜底在 renderer 侧也执行 runSnapshot
|
||||
*
|
||||
* 为什么双路:
|
||||
* - 主进程 scheduler 不依赖 renderer 是否打开监控页面 / Vault / 巡检报告页面
|
||||
* - 但主进程没法解密 vault,必须由 renderer 在启动后(VaultPublisher 已解密)
|
||||
* 调 inspectionSchedulerApi.start(hostConfigs) 把凭据同步过去
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import {
|
||||
inspectionReportApi,
|
||||
inspectionReportScheduleApi,
|
||||
inspectionDeviceApi,
|
||||
inspectionSchedulerApi,
|
||||
type ReportSchedule,
|
||||
} from "../../infrastructure/inspection/inspectionApi";
|
||||
|
||||
interface HostConfig {
|
||||
device_id: string;
|
||||
hostname: string;
|
||||
username: string;
|
||||
port?: number;
|
||||
password?: string;
|
||||
privateKeyContent?: string;
|
||||
passphrase?: string;
|
||||
catalog_id?: string;
|
||||
}
|
||||
|
||||
/** 从 vault 解密 + device extension 构建所有巡检设备 target */
|
||||
async function buildTargets(enabledDeviceIds?: string[]): Promise<HostConfig[]> {
|
||||
const api = (window as any).netcatty;
|
||||
const hosts = await api?.getHosts?.() ?? [];
|
||||
const keys = await api?.getSSHKeys?.() ?? [];
|
||||
|
||||
const exts = await inspectionDeviceApi.getAll().catch(() => ({}));
|
||||
|
||||
const targets: HostConfig[] = [];
|
||||
for (const host of hosts) {
|
||||
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 target: HostConfig = {
|
||||
device_id: host.id,
|
||||
hostname: host.hostname,
|
||||
username: host.username,
|
||||
port: host.port || 22,
|
||||
catalog_id: ext?.catalog_id,
|
||||
};
|
||||
if (host.password) target.password = host.password;
|
||||
if (key?.privateKeyContent || key?.privateKey) {
|
||||
target.privateKeyContent = key.privateKeyContent || key.privateKey;
|
||||
if (key.passphrase) target.passphrase = key.passphrase;
|
||||
}
|
||||
targets.push(target);
|
||||
}
|
||||
return targets;
|
||||
}
|
||||
|
||||
/** 把 HostConfig[] 转成主进程 inspection:scheduler:start 需要的 { host_id → SSH config } 格式 */
|
||||
function toSchedulerHostConfigs(targets: HostConfig[]): Record<string, Record<string, unknown>> {
|
||||
const out: Record<string, Record<string, unknown>> = {};
|
||||
for (const t of targets) {
|
||||
out[t.device_id] = {
|
||||
host: t.hostname,
|
||||
hostname: t.hostname,
|
||||
port: t.port || 22,
|
||||
username: t.username,
|
||||
password: t.password,
|
||||
privateKey: t.privateKeyContent,
|
||||
privateKeyContent: t.privateKeyContent,
|
||||
passphrase: t.passphrase,
|
||||
catalog_id: t.catalog_id,
|
||||
};
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 同步 AI providers 到主进程(主进程 AI 汇总需要) */
|
||||
async function syncAiProviders() {
|
||||
try {
|
||||
const api = (window as any).netcatty;
|
||||
const providersRaw = localStorage.getItem("netcatty_ai_providers_v1");
|
||||
if (providersRaw && api?.aiSyncProviders) {
|
||||
await api.aiSyncProviders(JSON.parse(providersRaw));
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
export function useReportAutoScheduler() {
|
||||
const lastRunAtRef = useRef<number>(0);
|
||||
const runningRef = useRef(false);
|
||||
const scheduleRef = useRef<ReportSchedule | null>(null);
|
||||
const hostsSyncedRef = useRef(false);
|
||||
|
||||
// 加载 schedule
|
||||
useEffect(() => {
|
||||
void inspectionReportScheduleApi.get().then(cfg => { scheduleRef.current = cfg; }).catch(() => {});
|
||||
}, []);
|
||||
|
||||
// 监听 schedule 变更(用户在设置面板修改配置后自动生效)
|
||||
useEffect(() => {
|
||||
const api = (window as any).netcatty;
|
||||
if (!api?.on || typeof api.on !== "function") return;
|
||||
const handler = (_data: any) => {
|
||||
void inspectionReportScheduleApi.get().then(cfg => {
|
||||
scheduleRef.current = cfg;
|
||||
lastRunAtRef.current = 0;
|
||||
}).catch(() => {});
|
||||
};
|
||||
api.on("inspection:reportScheduleChanged", handler);
|
||||
return () => { try { api.off?.("inspection:reportScheduleChanged", handler); } catch {} };
|
||||
}, []);
|
||||
|
||||
/** mount 后延迟同步一次 hostConfigs 给主进程,让主进程 reportScheduler 能独立执行定时快照 */
|
||||
useEffect(() => {
|
||||
const syncOnce = async () => {
|
||||
if (hostsSyncedRef.current) return;
|
||||
try {
|
||||
const targets = await buildTargets();
|
||||
if (targets.length === 0) {
|
||||
// Vault 还没加载好,稍后再试
|
||||
setTimeout(() => { void syncOnce(); }, 15000);
|
||||
return;
|
||||
}
|
||||
const hostConfigs = toSchedulerHostConfigs(targets);
|
||||
await inspectionSchedulerApi.start(hostConfigs);
|
||||
hostsSyncedRef.current = true;
|
||||
console.log(`[ReportAutoScheduler] hostConfigs 已同步给主进程,${Object.keys(hostConfigs).length} 台设备`);
|
||||
} catch (e) {
|
||||
console.warn("[ReportAutoScheduler] 同步 hostConfigs 失败:", e?.message);
|
||||
setTimeout(() => { void syncOnce(); }, 15000);
|
||||
}
|
||||
};
|
||||
// 12 秒后首次同步(等 VaultPublisher 解密 hosts/keys + schedule 加载)
|
||||
const timer = setTimeout(() => { void syncOnce(); }, 12000);
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
const tick = useCallback(async () => {
|
||||
if (runningRef.current) return;
|
||||
const cfg = scheduleRef.current;
|
||||
// 临时验证:忽略 enabled 检查
|
||||
// if (!cfg?.enabled) return;
|
||||
if (cfg.enabledDeviceIds && cfg.enabledDeviceIds.length === 0) return;
|
||||
|
||||
const intervalMs = 2 * 60 * 1000; // 临时验证:强制 2 分钟
|
||||
const now = Date.now();
|
||||
|
||||
// 临时验证:首次立即执行,不等快照时间
|
||||
if (lastRunAtRef.current !== 0 && now - lastRunAtRef.current < intervalMs) return;
|
||||
|
||||
runningRef.current = true;
|
||||
lastRunAtRef.current = now;
|
||||
try {
|
||||
await syncAiProviders();
|
||||
const targets = await buildTargets(cfg.enabledDeviceIds ?? undefined);
|
||||
if (targets.length === 0) {
|
||||
console.warn("[ReportAutoScheduler] 无有效设备,跳过");
|
||||
return;
|
||||
}
|
||||
console.error("[ReportAutoScheduler] ✅ tick 触发,设备数:", targets.length, "间隔:", cfg.defaultIntervalHours, "h");
|
||||
// 先同步 hostConfigs 给主进程(让主进程 reportScheduler 也能跑)
|
||||
if (hostsSyncedRef.current || !hostsSyncedRef.current) { // 每次都同步
|
||||
try {
|
||||
await inspectionSchedulerApi.start(toSchedulerHostConfigs(targets));
|
||||
hostsSyncedRef.current = true;
|
||||
console.error("[ReportAutoScheduler] ✅ hostConfigs 已同步");
|
||||
} catch (e) {
|
||||
console.error("[ReportAutoScheduler] ❌ 同步 hostConfigs 失败:", e?.message);
|
||||
}
|
||||
}
|
||||
await inspectionReportApi.runSnapshot(targets, "scheduled");
|
||||
console.error("[ReportAutoScheduler] ✅ runSnapshot 完成");
|
||||
} catch (e) {
|
||||
console.error("[ReportAutoScheduler] ❌ tick 执行失败:", e);
|
||||
} finally {
|
||||
runningRef.current = false;
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// 临时验证:mount 后 10s 先执行一次 tick
|
||||
const initial = setTimeout(() => { void tick(); }, 10000);
|
||||
// 临时验证:每 30 秒 tick 一次
|
||||
const timer = setInterval(() => { void tick(); }, 30 * 1000);
|
||||
return () => { clearInterval(timer); clearTimeout(initial); };
|
||||
}, [tick]);
|
||||
}
|
||||
Reference in New Issue
Block a user