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:
317
application/state/useInspectionScheduler.ts
Normal file
317
application/state/useInspectionScheduler.ts
Normal file
@@ -0,0 +1,317 @@
|
||||
/**
|
||||
* 巡检调度器 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]
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user