Files
SenMeshNet/application/state/useInspectionScheduler.ts
zhaolei 2f9f8da48e
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
Initial commit: NetMesh smart ops platform with 3D topology, inspection reports, RCA and AI ops assistant
2026-10-15 13:53:04 +08:00

318 lines
8.8 KiB
TypeScript
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.
/**
* 巡检调度器 Hook(前端侧)
*
* 为什么调度器在前端?因为 vault 主机数据在渲染进程的 localStorage 里,
* 主进程拿不到完整的主机连接信息(密码/私钥)。
*
* 调度器职责:
* - 每 30 秒检查一次哪些设备到期需要巡检
* - 按并发限制依次调用主进程的 inspection:runInspection 执行
* - 维护运行状态、上次执行时间
*/
import { useRef, useCallback, useEffect, useState, useMemo } from "react";
import { inspectionExecApi, inspectionDeviceApi } from "../../infrastructure/inspection/inspectionApi";
import type { Host } from "../../domain/models";
import type { SSHKey } from "../../domain/models/key";
import type { DeviceExtension } from "../../infrastructure/inspection/inspectionApi";
const POLL_INTERVAL_MS = 30 * 1000; // 30 秒轮询
const DEFAULT_CONCURRENCY = 3; // 默认并发数
const SYS_DEFAULT_INTERVAL = 5; // 系统默认周期(分钟)
const TIER_DEFAULT_INTERVAL: Record<string, number> = {
core: 5,
aggregation: 10,
access: 15,
};
export interface SchedulerStatus {
running: boolean;
totalDevices: number;
activeDevices: number;
lastScanAt: number | null;
runningCount: number;
}
interface LastRunInfo {
[hostId: string]: number; // timestamp
}
/**
* 计算设备的有效巡检周期(分钟)
* 优先级:设备自定义 > 分级默认 > 系统默认
*/
function getEffectiveInterval(ext: DeviceExtension | null): number {
if (!ext || !ext.inspection_enabled) return 0;
if (ext.inspection_interval && ext.inspection_interval > 0) {
return ext.inspection_interval;
}
if (ext.tier && TIER_DEFAULT_INTERVAL[ext.tier]) {
return TIER_DEFAULT_INTERVAL[ext.tier];
}
return SYS_DEFAULT_INTERVAL;
}
/**
* 从 Host + keys 拼出巡检需要的连接配置
*/
function buildHostConfig(host: Host, keys: SSHKey[]): {
id: string;
label: string;
hostname: string;
port: number;
username: string;
password?: string;
privateKeyContent?: string;
passphrase?: string;
} {
const config: any = {
id: host.id,
label: host.label,
hostname: host.hostname,
port: host.port || 22,
username: host.username,
};
// 密码
if (host.password) {
config.password = host.password;
}
// 私钥(通过 identityFileId 关联)
if (host.identityFileId) {
const key = keys.find((k) => k.id === host.identityFileId);
if (key && key.privateKey) {
config.privateKeyContent = key.privateKey;
if (key.passphrase) {
config.passphrase = key.passphrase;
}
}
}
return config;
}
export function useInspectionScheduler(hosts: Host[], keys: SSHKey[]) {
const [status, setStatus] = useState<SchedulerStatus>({
running: false,
totalDevices: 0,
activeDevices: 0,
lastScanAt: null,
runningCount: 0,
});
const timerRef = useRef<NodeJS.Timeout | null>(null);
const lastRunRef = useRef<LastRunInfo>({});
const runningRef = useRef(false);
const runningCountRef = useRef(0);
const hostsRef = useRef(hosts);
const keysRef = useRef(keys);
// 实时更新 hosts/keys 引用
useEffect(() => {
hostsRef.current = hosts;
}, [hosts]);
useEffect(() => {
keysRef.current = keys;
}, [keys]);
/**
* 执行一次单设备巡检
*/
const runDeviceInspection = useCallback(
async (hostId: string, catalogId?: string) => {
const host = hostsRef.current.find((h) => h.id === hostId);
if (!host) {
console.warn(`[InspectionScheduler] 未找到主机: ${hostId}`);
return null;
}
const hostConfig = buildHostConfig(host, keysRef.current);
const actualCatalogId = catalogId || "linux_server";
runningCountRef.current += 1;
setStatus((s) => ({ ...s, runningCount: runningCountRef.current }));
try {
const result = await inspectionExecApi.runInspection(
hostId,
hostConfig,
actualCatalogId
);
lastRunRef.current[hostId] = Date.now();
return result;
} catch (e: any) {
console.error(`[InspectionScheduler] 巡检失败 ${hostId}:`, e.message);
return { success: false, error: e.message };
} finally {
runningCountRef.current = Math.max(0, runningCountRef.current - 1);
setStatus((s) => ({ ...s, runningCount: runningCountRef.current }));
}
},
[]
);
/**
* 主循环:检查到期设备并执行
*/
const scanLoop = useCallback(async (hosts?: typeof hosts) => {
// 如果还没启动,自动启动调度器
if (!runningRef.current) {
runningRef.current = true;
setStatus((s) => ({ ...s, running: true }));
console.log("[InspectionScheduler] 调度器自动启动");
}
// 使用传入的 hosts 参数,或者用 ref
const currentHosts = hosts ?? hostsRef.current;
// hosts 还没加载,跳过
if (!currentHosts || currentHosts.length === 0) {
console.log('[InspectionScheduler] hosts 还未加载,当前 hosts:', currentHosts?.length);
return;
}
console.log('[InspectionScheduler] 开始巡检,设备数:', currentHosts.length);
const allExts = await inspectionDeviceApi.getAll();
const extsMap = allExts || {}; // Record<string, DeviceExtension>
// 调试:统计启用了巡检的设备数量
let enabledCount = 0;
for (const host of currentHosts) {
const ext = extsMap[host.id];
if (ext?.inspection_enabled) enabledCount++;
}
console.log('[InspectionScheduler] 扩展配置数量:', Object.keys(extsMap).length, '| 启用巡检设备数:', enabledCount);
const now = Date.now();
const dueDevices: { hostId: string; catalogId: string }[] = [];
for (const host of currentHosts) {
const ext = extsMap[host.id];
if (!ext || !ext.inspection_enabled) continue;
const intervalMin = getEffectiveInterval(ext);
if (intervalMin <= 0) {
console.log('[InspectionScheduler] 跳过(周期为0):', host.label, '| intervalMin:', intervalMin);
continue;
}
const lastRun = lastRunRef.current[host.id] || ext.last_scan_at || 0;
const intervalMs = intervalMin * 60 * 1000;
const elapsed = now - lastRun;
const needRun = elapsed >= intervalMs;
console.log('[InspectionScheduler] 设备:', host.label, '| 周期:', intervalMin, 'min | 距上次:', Math.round(elapsed/60000), 'min | 需要巡检:', needRun);
if (needRun) {
dueDevices.push({
hostId: host.id,
catalogId: ext.catalog_id || "linux_server",
});
}
}
console.log('[InspectionScheduler] 到期设备数:', dueDevices.length);
if (dueDevices.length === 0) {
setStatus((s) => ({ ...s, lastScanAt: now }));
return;
}
// 并发执行(简单的并发控制)
const queue = [...dueDevices];
const workers: Promise<any>[] = [];
const processQueue = async () => {
while (queue.length > 0) {
const device = queue.shift();
if (!device) break;
await runDeviceInspection(device.hostId, device.catalogId);
}
};
for (let i = 0; i < Math.min(DEFAULT_CONCURRENCY, dueDevices.length); i++) {
workers.push(processQueue());
}
await Promise.all(workers);
setStatus((s) => ({ ...s, lastScanAt: now }));
}, [runDeviceInspection]);
/**
* 启动调度器
*/
const start = useCallback(() => {
if (runningRef.current) return;
runningRef.current = true;
// 立即执行一次
scanLoop();
// 定时轮询
timerRef.current = setInterval(() => {
scanLoop();
}, POLL_INTERVAL_MS);
setStatus((s) => ({ ...s, running: true }));
console.log("[InspectionScheduler] 已启动");
}, [scanLoop]);
/**
* 停止调度器
*/
const stop = useCallback(() => {
runningRef.current = false;
if (timerRef.current) {
clearInterval(timerRef.current);
timerRef.current = null;
}
setStatus((s) => ({ ...s, running: false }));
console.log("[InspectionScheduler] 已停止");
}, []);
/**
* 立即巡检指定设备
*/
const scanNow = useCallback(
async (hostId: string, catalogId?: string) => {
return await runDeviceInspection(hostId, catalogId);
},
[runDeviceInspection]
);
// 组件卸载时清理
useEffect(() => {
return () => {
if (timerRef.current) {
clearInterval(timerRef.current);
}
runningRef.current = false;
};
}, []);
// 计算状态
useEffect(() => {
const activeCount = hosts.filter((h) => {
// 这里没法同步拿到 extensions,先按 hosts 总数显示
return true;
}).length;
setStatus((s) => ({
...s,
totalDevices: hosts.length,
activeDevices: activeCount,
}));
}, [hosts]);
return useMemo(
() => ({
status,
start,
stop,
scanNow,
scanLoop,
}),
[status, start, stop, scanNow, scanLoop]
);
}