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

巡检模块:巡检调度器改为轮询驱动,新增每日报告存储、告警规则独立
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:
2026-10-19 21:11:57 +08:00
committed by trae-solo-agent
parent 336c3afd47
commit 2e71e03d71
34 changed files with 3012 additions and 230 deletions

View File

@@ -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}