Files
NetMesh/application/state/useInspectionScheduler.ts
zhaolei c64e00d835 feat: 新增设备巡检模块实现完整的巡检告警功能
新增设备巡检模块核心功能,包括:
- 巡检数据存储与调度系统
- 华为/H3C交换机及Linux服务器采集器
- 设备扩展配置与告警规则管理
- 主机详情页巡检配置入口
- 产品名称自定义设置功能
- 网络设备SSH exec通道兼容处理
2026-10-09 10:37:48 +08:00

318 lines
8.8 KiB
TypeScript
Raw Permalink 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]
);
}