feat: 巡检增强 / 拓扑大屏 / 远程 HTTP / portable 绿色打包
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
巡检模块:巡检调度器改为轮询驱动,新增每日报告存储、告警规则独立 Store、设备 Store 解耦,_executeSnapshot 补齐 DEBUG 声明,巡检中心 与告警中心页增强。 拓扑大屏:MonitoringTopologyView 扩展分组/布局控制, useMonitoringData 重构数据流。 远程 HTTP:新增 electron/remoteHttpServer.cjs,preload/api.cjs 暴露 IPC,registerBridges 注册 netcatty:http:open 桥。 portable 绿色打包: - electron-builder.config.cjs 恢复原始白名单(61MB app.asar), 收集器自动追踪运行时依赖; - portableData.cjs 重写为 .portable 标记检测; - scripts/migratePortableLocalStorage.cjs 离线脚本把 Local Storage 的 http://localhost:5173 origin 批量迁移为 app://netcatty; - scripts/scanWorkspaceDeps.cjs 扫描 workspace 运行时依赖。
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/**
|
||||
* 运行监控 - 3D 拓扑大屏
|
||||
*
|
||||
* 移植自 ITYUNWEI TopologyView.vue
|
||||
@@ -328,7 +328,12 @@ function formatRelativeTime(timestamp: number): string {
|
||||
|
||||
// 扩展 Host 的设备类型信息
|
||||
function classifyHostType(host: Host): string {
|
||||
const distro = (host.distro || '').toLowerCase();
|
||||
// 优先用直接的 type 字段(remote API 会返回 switch/server/firewall/router)
|
||||
const directType = (host as any).type;
|
||||
if (directType && ['server', 'switch', 'router', 'firewall', 'storage', 'workstation'].includes(directType)) {
|
||||
return directType;
|
||||
}
|
||||
const distro = (host.distro || (host as any).catalog || (host as any).catalog_id || '').toLowerCase();
|
||||
const name = (host.name || '').toLowerCase();
|
||||
const label = (host.label || '').toLowerCase();
|
||||
const combined = `${distro} ${name} ${label}`;
|
||||
@@ -476,24 +481,94 @@ export function MonitoringTopologyView({ hosts, keys }: Props) {
|
||||
const [currentTime, setCurrentTime] = useState(new Date());
|
||||
const [legend, setLegend] = useState(true);
|
||||
const [showConnectionLegend, setShowConnectionLegend] = useState(true);
|
||||
const [monitoringTitle, setMonitoringTitle] = useState<string>(() =>
|
||||
localStorage.getItem(STORAGE_KEY_MONITORING_DASHBOARD_TITLE) || '云启Net智能巡检监测系统'
|
||||
);
|
||||
const [monitoringTitle, setMonitoringTitle] = useState<string>(() => {
|
||||
// 先给默认值,useEffect 里远程异步加载(时序更安全)
|
||||
if (typeof window !== "undefined") {
|
||||
const saved = localStorage.getItem(STORAGE_KEY_MONITORING_DASHBOARD_TITLE);
|
||||
if (saved) return saved;
|
||||
}
|
||||
return '云启Net智能巡检监测系统';
|
||||
});
|
||||
|
||||
// 监听 localStorage 中大屏标题变化(设置页面修改后实时生效)
|
||||
// 远程环境:异步加载标题 / 位置 / 连接线(useState init 阶段 token 可能还没注入)
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
const token = (window as any).__REMOTE_TOKEN__;
|
||||
if (!token) return; // 不是远程环境或 token 未就绪
|
||||
|
||||
const fetchJSON = (url: string): Promise<any> =>
|
||||
fetch(url).then(r => r.json()).catch(() => null);
|
||||
|
||||
// 1. 标题
|
||||
fetchJSON(`/api/dashboard-title?token=${encodeURIComponent(token)}`)
|
||||
.then(json => { if (json?.ok && json.title) setMonitoringTitle(json.title); });
|
||||
|
||||
// 2. 拓扑位置
|
||||
fetchJSON(`/api/topology-positions?token=${encodeURIComponent(token)}`)
|
||||
.then(json => {
|
||||
if (json?.ok && json.positions && Object.keys(json.positions).length > 0) {
|
||||
setSavedPositions(json.positions);
|
||||
}
|
||||
});
|
||||
|
||||
// 3. 连接线
|
||||
fetchJSON(`/api/topology-links?token=${encodeURIComponent(token)}`)
|
||||
.then(json => { if (json?.ok && Array.isArray(json.links)) setCustomLinks(json.links); });
|
||||
}, []);
|
||||
|
||||
// Electron 环境:监听 localStorage 变化 + mount 时同步 hosts/title 给主进程
|
||||
useEffect(() => {
|
||||
const nc = (window as any).netcatty;
|
||||
console.log('[MonitoringTopologyView sync] nc exists:', !!nc, 'nc.inspection exists:', !!nc?.inspection, 'hosts.length:', hosts?.length);
|
||||
if (!nc?.inspection) {
|
||||
console.log('[MonitoringTopologyView sync] skip — not Electron env');
|
||||
return;
|
||||
}
|
||||
|
||||
// mount 时一次性同步 vault hosts + 标题 + 拓扑位置 + 拓扑连线
|
||||
if (hosts?.length > 0) {
|
||||
console.log('[MonitoringTopologyView sync] saving vault hosts:', hosts.length);
|
||||
nc.inspection.saveVaultHosts(hosts).then((r: any) => console.log('saveVaultHosts result:', r)).catch((e: any) => console.warn('saveVaultHosts error:', e));
|
||||
}
|
||||
const savedTitle = localStorage.getItem(STORAGE_KEY_MONITORING_DASHBOARD_TITLE);
|
||||
console.log('[MonitoringTopologyView sync] savedTitle:', savedTitle);
|
||||
if (savedTitle) nc.inspection.saveDashboardTitle(savedTitle).catch(() => {});
|
||||
const savedPos = localStorage.getItem('monitoring-topology-positions');
|
||||
if (savedPos) nc.inspection.saveTopologyPositions(JSON.parse(savedPos)).catch(() => {});
|
||||
const savedLinks = localStorage.getItem('monitoring-topology-links');
|
||||
console.log('[MonitoringTopologyView sync] savedLinks exists:', !!savedLinks);
|
||||
if (savedLinks) {
|
||||
try { nc.inspection.saveTopologyLinks(JSON.parse(savedLinks)).catch(() => {}); } catch {}
|
||||
}
|
||||
if (!savedLinks && nc.inspection.getTopologyLinks) {
|
||||
nc.inspection.getTopologyLinks().then((linksFromMain: any) => {
|
||||
if (Array.isArray(linksFromMain) && linksFromMain.length > 0) {
|
||||
console.info('[MonitoringTopologyView] restore links from main:', linksFromMain.length);
|
||||
localStorage.setItem('monitoring-topology-links', JSON.stringify(linksFromMain));
|
||||
try { setCustomLinks(linksFromMain); } catch {}
|
||||
}
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
const syncToMain = (value: string) => {
|
||||
if (value) nc.inspection.saveDashboardTitle(value).catch(() => {});
|
||||
};
|
||||
const handler = (e: StorageEvent) => {
|
||||
if (e.key === STORAGE_KEY_MONITORING_DASHBOARD_TITLE && e.newValue !== null) {
|
||||
setMonitoringTitle(e.newValue || '云启Net智能巡检监测系统');
|
||||
syncToMain(e.newValue);
|
||||
}
|
||||
};
|
||||
// 同页 localStorage 变化不走 storage 事件,直接在同页用 CustomEvent 广播
|
||||
const localHandler = () => {
|
||||
const v = localStorage.getItem(STORAGE_KEY_MONITORING_DASHBOARD_TITLE);
|
||||
setMonitoringTitle(v || '云启Net智能巡检监测系统');
|
||||
syncToMain(v || '');
|
||||
};
|
||||
window.addEventListener('storage', handler);
|
||||
window.addEventListener('monitoringTitleChanged' as any, localHandler);
|
||||
// mount 时同步一次当前标题
|
||||
const initVal = localStorage.getItem(STORAGE_KEY_MONITORING_DASHBOARD_TITLE);
|
||||
if (initVal) syncToMain(initVal);
|
||||
return () => {
|
||||
window.removeEventListener('storage', handler);
|
||||
window.removeEventListener('monitoringTitleChanged' as any, localHandler);
|
||||
@@ -843,14 +918,12 @@ export function MonitoringTopologyView({ hosts, keys }: Props) {
|
||||
});
|
||||
const [pendingLink, setPendingLink] = useState<{ sourceId: string; targetId: string } | null>(null);
|
||||
|
||||
// 用户创建的连线(从 localStorage 加载)
|
||||
// 用户创建的连线 —— 默认空数组,远程/本地各自从 useEffect 加载
|
||||
const [customLinks, setCustomLinks] = useState<Link[]>(() => {
|
||||
try {
|
||||
const saved = localStorage.getItem('monitoring-topology-links');
|
||||
return saved ? JSON.parse(saved) : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
} catch { return []; }
|
||||
});
|
||||
|
||||
// 连线模式使用 ref 避免闭包问题
|
||||
@@ -884,6 +957,13 @@ export function MonitoringTopologyView({ hosts, keys }: Props) {
|
||||
localStorage.setItem('monitoring-topology-links', JSON.stringify(customLinks));
|
||||
}, [customLinks]);
|
||||
|
||||
// 连线变化时同步到主进程 inspection-data.json(供远程浏览器 API 读取)
|
||||
useEffect(() => {
|
||||
const nc = (window as any).netcatty;
|
||||
if (!nc?.inspection) return;
|
||||
nc.inspection.saveTopologyLinks(customLinks).catch(() => {});
|
||||
}, [customLinks]);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem('monitoring-topology-excluded-links', JSON.stringify(excludedAutoLinks));
|
||||
}, [excludedAutoLinks]);
|
||||
@@ -902,10 +982,16 @@ export function MonitoringTopologyView({ hosts, keys }: Props) {
|
||||
exitFullscreen();
|
||||
}
|
||||
};
|
||||
// 浏览器原生全屏变化(ESC 退出),同步 React 状态
|
||||
const onFsChange = () => {
|
||||
if (!document.fullscreenElement && isFullscreen) {
|
||||
setIsFullscreen(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (isFullscreen) {
|
||||
document.addEventListener('keydown', onKeyDown);
|
||||
// body 加 class,配合 CSS 让 monitoring-page 固定全屏
|
||||
document.addEventListener('fullscreenchange', onFsChange);
|
||||
document.body.classList.add('monitoring-fullscreen-active');
|
||||
// Electron 环境下同时让窗口也全屏
|
||||
const nc = (window as any).netcatty;
|
||||
@@ -916,6 +1002,7 @@ export function MonitoringTopologyView({ hosts, keys }: Props) {
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', onKeyDown);
|
||||
document.removeEventListener('fullscreenchange', onFsChange);
|
||||
document.body.classList.remove('monitoring-fullscreen-active');
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -923,6 +1010,12 @@ export function MonitoringTopologyView({ hosts, keys }: Props) {
|
||||
|
||||
const enterFullscreen = useCallback(() => {
|
||||
setIsFullscreen(true);
|
||||
// 浏览器远程访问环境:用原生 Fullscreen API 让 body 全屏
|
||||
if (typeof (document.documentElement as any).requestFullscreen === 'function') {
|
||||
(document.documentElement as any).requestFullscreen().catch(() => {});
|
||||
} else if (typeof (document.body as any).webkitRequestFullscreen === 'function') {
|
||||
(document.body as any).webkitRequestFullscreen();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const exitFullscreen = useCallback(() => {
|
||||
@@ -931,6 +1024,12 @@ export function MonitoringTopologyView({ hosts, keys }: Props) {
|
||||
if (nc?.windowFullscreen) {
|
||||
nc.windowFullscreen(false).catch(() => {});
|
||||
}
|
||||
// 浏览器环境退出原生全屏
|
||||
if (document.fullscreenElement && typeof document.exitFullscreen === 'function') {
|
||||
document.exitFullscreen().catch(() => {});
|
||||
} else if (typeof (document as any).webkitExitFullscreen === 'function') {
|
||||
(document as any).webkitExitFullscreen();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const toggleFullscreen = useCallback(() => {
|
||||
@@ -966,7 +1065,7 @@ export function MonitoringTopologyView({ hosts, keys }: Props) {
|
||||
}
|
||||
}
|
||||
|
||||
return hosts.map((h) => {
|
||||
return monitoring.effectiveHosts.map((h) => {
|
||||
const type = classifyHostType(h);
|
||||
const online = monitoring.onlineMap[h.id] ?? true;
|
||||
const cpuUsage = monitoring.cpuMap[h.id] ?? 0;
|
||||
@@ -996,9 +1095,14 @@ export function MonitoringTopologyView({ hosts, keys }: Props) {
|
||||
status = 'warning';
|
||||
}
|
||||
|
||||
// label 里可能已经包含 IP(如 "外网-192.168.0.221"),去重显示
|
||||
let rawName = h.label || h.id;
|
||||
const ipRegex = /(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/;
|
||||
const name = rawName.replace(ipRegex, '').trim().replace(/[-_\s]+$|^[-_\s]+/g, '') || rawName;
|
||||
|
||||
return {
|
||||
id: h.id,
|
||||
name: h.label || h.name || h.id,
|
||||
name,
|
||||
ip: h.hostname,
|
||||
type,
|
||||
status,
|
||||
@@ -1011,7 +1115,7 @@ export function MonitoringTopologyView({ hosts, keys }: Props) {
|
||||
lastUpdate,
|
||||
};
|
||||
});
|
||||
}, [hosts, monitoring.cpuMap, monitoring.memMap, monitoring.onlineMap, monitoring.timestampMap, monitoring.alerts, monitoring.thresholds]);
|
||||
}, [monitoring.effectiveHosts, monitoring.cpuMap, monitoring.memMap, monitoring.onlineMap, monitoring.timestampMap, monitoring.alerts, monitoring.thresholds]);
|
||||
|
||||
// 合并自动生成链路和用户自定义链路,并过滤掉源/目标已不存在的僵尸连线
|
||||
const links = useMemo(() => {
|
||||
@@ -1706,7 +1810,11 @@ export function MonitoringTopologyView({ hosts, keys }: Props) {
|
||||
const data: Record<string, { x: number; y: number; z: number }> = saved ? JSON.parse(saved) : {};
|
||||
data[devId] = { x: p.x, y: clampedY, z: p.z };
|
||||
localStorage.setItem('monitoring-topology-positions', JSON.stringify(data));
|
||||
console.log('[持久化] 位置已保存:', devId, { x: p.x, y: clampedY, z: p.z });
|
||||
// Electron 环境同步给主进程,让远程浏览器也能读到
|
||||
const nc = (window as any).netcatty;
|
||||
if (nc?.inspection?.saveTopologyPositions) {
|
||||
nc.inspection.saveTopologyPositions(data).catch(() => {});
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
};
|
||||
|
||||
@@ -2183,6 +2291,22 @@ export function MonitoringTopologyView({ hosts, keys }: Props) {
|
||||
const positions: Record<string, THREE.Vector3> = {};
|
||||
|
||||
const savedPositions = (() => {
|
||||
const IS_REMOTE = typeof window !== "undefined" && !!(window as any).__REMOTE_TOKEN__;
|
||||
if (IS_REMOTE) {
|
||||
try {
|
||||
const token = (window as any).__REMOTE_TOKEN__ || "";
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open("GET", `/api/topology-positions?token=${encodeURIComponent(token)}`, false);
|
||||
xhr.send();
|
||||
if (xhr.status === 200) {
|
||||
const json = JSON.parse(xhr.responseText);
|
||||
if (json?.ok && json.positions && Object.keys(json.positions).length > 0) {
|
||||
return json.positions;
|
||||
}
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
// 本地 Electron:从 localStorage 读
|
||||
try {
|
||||
const saved = localStorage.getItem('monitoring-topology-positions');
|
||||
return saved ? JSON.parse(saved) : null;
|
||||
@@ -2237,6 +2361,20 @@ export function MonitoringTopologyView({ hosts, keys }: Props) {
|
||||
|
||||
positionsRef.current = positions;
|
||||
|
||||
// Electron 环境:启动时自动把 localStorage 里的拓扑位置同步给主进程,供远程浏览器读取
|
||||
{
|
||||
const IS_ELECTRON = typeof window !== "undefined" && !!(window as any).netcatty;
|
||||
if (IS_ELECTRON) {
|
||||
try {
|
||||
const saved = localStorage.getItem('monitoring-topology-positions');
|
||||
if (saved) {
|
||||
const nc = (window as any).netcatty;
|
||||
nc.inspection?.saveTopologyPositions?.(JSON.parse(saved)).catch(() => {});
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
}
|
||||
|
||||
// 创建设备节点
|
||||
const nodes: THREE.Group[] = [];
|
||||
for (const device of devices) {
|
||||
@@ -3383,6 +3521,7 @@ export function MonitoringTopologyView({ hosts, keys }: Props) {
|
||||
setCustomLinks((prev) => [...prev, newLink]);
|
||||
console.log('[连线] 创建链路:', pendingLink.sourceId, '->', pendingLink.targetId, type);
|
||||
setPendingLink(null);
|
||||
setLinkMode(null); // 退出连线模式,恢复 cursor 为 grab
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
|
||||
@@ -90,6 +90,9 @@ interface UseMonitoringDataResult {
|
||||
// 操作
|
||||
refresh: () => Promise<void>;
|
||||
loading: boolean;
|
||||
|
||||
// 浏览器远程访问环境下实际使用的 hosts(Electron 环境 = props.hosts)
|
||||
effectiveHosts: Host[];
|
||||
}
|
||||
|
||||
// ===== localStorage 持久化(降级用,API 不可用时显示缓存) =====
|
||||
@@ -321,6 +324,61 @@ export function useMonitoringData(hosts: Host[], keys: any[] = []): UseMonitorin
|
||||
hostsRef.current = hosts;
|
||||
keysRef.current = keys;
|
||||
|
||||
// 浏览器远程访问环境:从 /api/devices 构建虚拟 hosts 列表(Electron vault 不可用)
|
||||
const [remoteHosts, setRemoteHosts] = useState<Host[]>([]);
|
||||
const IS_REMOTE = typeof window !== "undefined" && !!(window as any).__REMOTE_TOKEN__;
|
||||
const REMOTE_TOKEN = (window as any).__REMOTE_TOKEN__ || "";
|
||||
|
||||
// Electron 本地:同步 vault hosts 给主进程(供远程浏览器 API 读)
|
||||
useEffect(() => {
|
||||
if (IS_REMOTE) return; // 远程浏览器不触发
|
||||
const nc = (window as any).netcatty;
|
||||
if (!nc?.inspection) return;
|
||||
if (hosts.length === 0) return;
|
||||
console.log('[useMonitoringData] syncing vault hosts to main process:', hosts.length);
|
||||
nc.inspection.saveVaultHosts(hosts).then((r: any) => {
|
||||
console.log('[useMonitoringData] saveVaultHosts result:', r);
|
||||
}).catch((e: any) => {
|
||||
console.warn('[useMonitoringData] saveVaultHosts error:', e);
|
||||
});
|
||||
}, [hosts, IS_REMOTE]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!IS_REMOTE) return;
|
||||
console.log('[monitoring] REMOTE ENV - props.hosts.length=', hosts.length, 'remoteHosts.length=', remoteHosts.length);
|
||||
if (hosts.length > 0) {
|
||||
console.warn('[monitoring] REMOTE ENV 但 props.hosts 有值!跳过 remote hosts 加载,hosts[0]=', hosts[0]);
|
||||
return;
|
||||
}
|
||||
const loadRemoteHosts = async () => {
|
||||
try {
|
||||
const res = await fetch(`/api/devices?token=${encodeURIComponent(REMOTE_TOKEN)}`);
|
||||
const data = await res.json();
|
||||
const list: Host[] = (data.devices || []).map((d: any) => ({
|
||||
id: d.host_id,
|
||||
label: d.label || d.name || d.hostname || d.host_id,
|
||||
hostname: d.hostname || d.name || "",
|
||||
port: 22,
|
||||
username: "",
|
||||
distro: d.distro || d.catalog_type || d.catalog_id || "",
|
||||
type: d.type || "",
|
||||
catalogId: d.catalog_id || "",
|
||||
tags: [],
|
||||
os: "linux" as const,
|
||||
group: "",
|
||||
}));
|
||||
setRemoteHosts(list);
|
||||
hostsRef.current = list;
|
||||
} catch (e) {
|
||||
console.warn("[monitoring] remote hosts load failed:", e);
|
||||
}
|
||||
};
|
||||
void loadRemoteHosts();
|
||||
}, [IS_REMOTE, REMOTE_TOKEN, hosts.length]);
|
||||
|
||||
// 实际使用的 hosts:Electron 用 props,浏览器用远程加载的
|
||||
const effectiveHosts = IS_REMOTE && hosts.length === 0 ? remoteHosts : hosts;
|
||||
|
||||
const refresh = useCallback(async (silent = false) => {
|
||||
if (loadingRef.current) {
|
||||
console.log('[monitoring] refresh() 正在执行中,跳过');
|
||||
@@ -505,33 +563,33 @@ export function useMonitoringData(hosts: Host[], keys: any[] = []): UseMonitorin
|
||||
// 自动启动调度器
|
||||
if (enabledDevices.length === 0) {
|
||||
console.log('[monitoring] 没有启用巡检的设备');
|
||||
} else if (schedulerStatus.running) {
|
||||
console.log('[monitoring] 调度器已在运行中');
|
||||
} else {
|
||||
console.log('[monitoring] 条件满足,准备启动调度器');
|
||||
console.log('[monitoring] 条件满足,准备启动调度器');
|
||||
try {
|
||||
const hostConfigs: Record<string, Record<string, unknown>> = {};
|
||||
for (const dev of enabledDevices) {
|
||||
const hostname = dev.hostname;
|
||||
if (!hostname) {
|
||||
console.log('[monitoring] 设备', dev.host_id, '没有 hostname,跳过');
|
||||
continue;
|
||||
}
|
||||
const host = hostsRef.current.find((h) => h.hostname === hostname);
|
||||
const allExt = allExtensions || {};
|
||||
for (const [hostId, ext] of Object.entries(allExt)) {
|
||||
if (!ext || !ext.inspection_enabled) continue;
|
||||
const host = hostsRef.current.find((h) => h.id === hostId);
|
||||
if (!host) {
|
||||
console.log('[monitoring] vault 中未找到 hostname:', hostname, ',跳过');
|
||||
console.log('[monitoring] vault 中未找到 host_id:', hostId, ',跳过');
|
||||
continue;
|
||||
}
|
||||
if (!host.username) {
|
||||
console.log('[monitoring] 设备', hostname, '缺少 username,跳过');
|
||||
console.log('[monitoring] 设备', host.label || host.hostname || hostId, '缺少 username,跳过');
|
||||
continue;
|
||||
}
|
||||
hostConfigs[dev.host_id] = buildSshConfig(host, keysRef.current);
|
||||
hostConfigs[hostId] = buildSshConfig(host, keysRef.current);
|
||||
}
|
||||
// 诊断:打印 hostConfigs 样本看 label 有没有正确传递
|
||||
const sample = Object.entries(hostConfigs).slice(0, 3).map(([id, c]) => ({ id, label: c.label, host: c.host, username: c.username }));
|
||||
console.log('[monitoring] hostConfigs 样本:', JSON.stringify(sample));
|
||||
if (Object.keys(hostConfigs).length === 0) {
|
||||
console.log('[monitoring] 没有可巡检的设备(hostname 未写入或配置不完整)');
|
||||
console.log('[monitoring] 没有可巡检的设备(vault 中匹配不到或凭据不完整)');
|
||||
} else {
|
||||
console.log('[monitoring] 启动调度器,匹配到', Object.keys(hostConfigs).length, '台设备');
|
||||
console.log('[monitoring] 启动调度器,匹配到', Object.keys(hostConfigs).length, '台设备(running=' + schedulerStatus.running + ')');
|
||||
// 不管 running 与否都传 hostConfigs 过去让主进程更新,
|
||||
// 主进程 execFn 通过模块级 _hostConfigs 引用动态读最新值
|
||||
await inspectionSchedulerApi.start(hostConfigs);
|
||||
setSchedulerRunning(true);
|
||||
}
|
||||
@@ -586,7 +644,7 @@ export function useMonitoringData(hosts: Host[], keys: any[] = []): UseMonitorin
|
||||
setOnlineMap((prev) => {
|
||||
const next: Record<string, boolean> = { ...prev };
|
||||
let changed = false;
|
||||
for (const h of hosts) {
|
||||
for (const h of effectiveHosts) {
|
||||
if (next[h.id] === undefined) {
|
||||
next[h.id] = true; // 假设在线,等巡检结果覆盖
|
||||
changed = true;
|
||||
@@ -594,7 +652,7 @@ export function useMonitoringData(hosts: Host[], keys: any[] = []): UseMonitorin
|
||||
}
|
||||
return changed ? next : prev;
|
||||
});
|
||||
}, [hosts]);
|
||||
}, [effectiveHosts]);
|
||||
|
||||
// 从告警规则派生阈值(和 alertRuleStore 默认规则对齐)
|
||||
const thresholds = useMemo(() => {
|
||||
@@ -636,5 +694,6 @@ export function useMonitoringData(hosts: Host[], keys: any[] = []): UseMonitorin
|
||||
thresholds,
|
||||
refresh,
|
||||
loading,
|
||||
effectiveHosts,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user