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:
@@ -52,22 +52,44 @@ export function useAppLockRuntime(
|
||||
'getAppLockRuntimeState' | 'onAppLockRuntimeStateChanged'
|
||||
> | null | undefined,
|
||||
) {
|
||||
const [runtimeState, setRuntimeState] = useState<RuntimeAppLockState>(DEFAULT_RUNTIME_APP_LOCK_STATE);
|
||||
// 浏览器远程访问环境下(无 Electron bridge),直接 unlocked
|
||||
const BROWSER_ONLY = typeof window !== 'undefined' && !!(window as any).__REMOTE_TOKEN__;
|
||||
const [runtimeState, setRuntimeState] = useState<RuntimeAppLockState>(
|
||||
BROWSER_ONLY
|
||||
? { initialized: true, locked: false, reason: null, version: 0, lastLockedAt: null, lastUnlockedAt: null, lastActivityAt: null }
|
||||
: DEFAULT_RUNTIME_APP_LOCK_STATE,
|
||||
);
|
||||
const refreshRuntimeState = useCallback(async () => {
|
||||
// 浏览器远程访问:直接返回 unlocked
|
||||
if (BROWSER_ONLY) {
|
||||
const unlocked: RuntimeAppLockState = {
|
||||
initialized: true,
|
||||
locked: false,
|
||||
reason: null,
|
||||
version: 0,
|
||||
lastLockedAt: null,
|
||||
lastUnlockedAt: null,
|
||||
lastActivityAt: null,
|
||||
};
|
||||
setRuntimeState((current) => selectPreferredRuntimeAppLockState(current, unlocked));
|
||||
return unlocked;
|
||||
}
|
||||
const nextState = await bridge?.getAppLockRuntimeState?.();
|
||||
const normalized = normalizeRuntimeAppLockState(nextState);
|
||||
setRuntimeState((current) => selectPreferredRuntimeAppLockState(current, normalized));
|
||||
return normalized;
|
||||
}, [bridge]);
|
||||
}, [bridge, BROWSER_ONLY]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
const unsubscribe = bridge?.onAppLockRuntimeStateChanged?.((nextState) => {
|
||||
if (cancelled) return;
|
||||
const normalized = normalizeRuntimeAppLockState(nextState);
|
||||
setRuntimeState((current) => selectPreferredRuntimeAppLockState(current, normalized));
|
||||
}) ?? (() => {});
|
||||
const unsubscribe = BROWSER_ONLY
|
||||
? () => {}
|
||||
: (bridge?.onAppLockRuntimeStateChanged?.((nextState) => {
|
||||
if (cancelled) return;
|
||||
const normalized = normalizeRuntimeAppLockState(nextState);
|
||||
setRuntimeState((current) => selectPreferredRuntimeAppLockState(current, normalized));
|
||||
}) ?? (() => {}));
|
||||
|
||||
void refreshRuntimeState().catch(() => {
|
||||
if (cancelled) return;
|
||||
@@ -81,7 +103,7 @@ export function useAppLockRuntime(
|
||||
cancelled = true;
|
||||
unsubscribe();
|
||||
};
|
||||
}, [bridge, refreshRuntimeState]);
|
||||
}, [bridge, refreshRuntimeState, BROWSER_ONLY]);
|
||||
|
||||
return {
|
||||
runtimeState,
|
||||
|
||||
@@ -146,6 +146,12 @@ export function useReportAutoScheduler() {
|
||||
|
||||
/** mount 后延迟同步一次 hostConfigs 给主进程,让主进程 reportScheduler 能独立执行定时快照 */
|
||||
useEffect(() => {
|
||||
// 远程浏览器环境:vault localStorage 加密数据读不到,无法解密 hosts,跳过定时巡检报告逻辑
|
||||
const IS_REMOTE = typeof window !== "undefined" && !!(window as any).__REMOTE_TOKEN__;
|
||||
if (IS_REMOTE) {
|
||||
console.info("[ReportAutoScheduler] 远程访问环境,跳过巡检报告自动调度");
|
||||
return;
|
||||
}
|
||||
const syncOnce = async () => {
|
||||
if (hostsSyncedRef.current) return;
|
||||
try {
|
||||
|
||||
@@ -85,6 +85,8 @@ import {
|
||||
STORAGE_KEY_SHOW_TAB_NUMBER_BADGES,
|
||||
STORAGE_KEY_DISABLE_TERMINAL_FONT_ZOOM,
|
||||
STORAGE_KEY_PRODUCT_NAME,
|
||||
STORAGE_KEY_MONITORING_DASHBOARD_TITLE,
|
||||
STORAGE_KEY_REPORT_PRODUCT_NAME,
|
||||
} from '../../infrastructure/config/storageKeys';
|
||||
import { DEFAULT_UI_LOCALE, resolveSupportedLocale } from '../../infrastructure/config/i18n';
|
||||
import {
|
||||
@@ -273,10 +275,67 @@ export const useSettingsState = (options: { enableSettingsSync?: boolean; enable
|
||||
const stored = readStoredString(STORAGE_KEY_UI_FONT_FAMILY);
|
||||
return stored && isValidUiFontId(stored) ? stored : DEFAULT_UI_FONT_ID;
|
||||
});
|
||||
|
||||
// 产品名称配置(用于标题栏、侧边栏 Logo 等)
|
||||
const [productName, setProductName] = useState<string>(() => {
|
||||
return localStorageAdapter.readString(STORAGE_KEY_PRODUCT_NAME) || '云启智能运维平台';
|
||||
});
|
||||
|
||||
// 远程浏览器:启动时从 /api/dashboard-title 拉取产品名系列配置,写入 localStorage
|
||||
useEffect(() => {
|
||||
const IS_REMOTE = typeof window !== "undefined" && !!(window as any).__REMOTE_TOKEN__;
|
||||
if (!IS_REMOTE) return;
|
||||
const token = (window as any).__REMOTE_TOKEN__ || "";
|
||||
fetch(`/api/dashboard-title?token=${encodeURIComponent(token)}`)
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
let changed = false;
|
||||
if (d?.productName) {
|
||||
localStorageAdapter.writeString(STORAGE_KEY_PRODUCT_NAME, d.productName);
|
||||
setProductName(d.productName);
|
||||
changed = true;
|
||||
}
|
||||
if (d?.dashboardTitle) {
|
||||
localStorageAdapter.writeString(STORAGE_KEY_MONITORING_DASHBOARD_TITLE, d.dashboardTitle);
|
||||
changed = true;
|
||||
}
|
||||
if (d?.reportProductName !== undefined) {
|
||||
localStorageAdapter.writeString(STORAGE_KEY_REPORT_PRODUCT_NAME, d.reportProductName);
|
||||
changed = true;
|
||||
}
|
||||
if (changed) {
|
||||
try { window.dispatchEvent(new CustomEvent('productNameChanged')); } catch {}
|
||||
try { window.dispatchEvent(new CustomEvent('monitoringTitleChanged')); } catch {}
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// Electron 本地:启动时从主进程读取产品名系列配置 → 恢复到 Local Storage
|
||||
// 解决绿色便携版首次启动 Local Storage 空时显示旧默认值的问题
|
||||
useEffect(() => {
|
||||
const IS_REMOTE = typeof window !== "undefined" && !!(window as any).__REMOTE_TOKEN__;
|
||||
if (IS_REMOTE) return; // 远程浏览器有上面的 effect 处理
|
||||
const nc = (window as any).netcatty;
|
||||
if (!nc?.inspection) return;
|
||||
|
||||
const localProductName = localStorageAdapter.readString(STORAGE_KEY_PRODUCT_NAME);
|
||||
const needsRestore = !localProductName; // Local Storage 空才需要恢复
|
||||
|
||||
if (needsRestore) {
|
||||
nc.inspection.getProductName?.().then((v: string) => {
|
||||
if (v && v.length > 0) {
|
||||
localStorageAdapter.writeString(STORAGE_KEY_PRODUCT_NAME, v);
|
||||
setProductName(v);
|
||||
console.info('[useSettingsState] restored productName from main:', v);
|
||||
}
|
||||
}).catch(() => {});
|
||||
}
|
||||
// dashboardTitle 和 reportProductName 在 SettingsApplicationTab 里单独恢复
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const [syncConfig, setSyncConfig] = useState<SyncConfig | null>(() => localStorageAdapter.read<SyncConfig>(STORAGE_KEY_SYNC));
|
||||
const [terminalThemeId, setTerminalThemeId] = useState<string>(() => localStorageAdapter.readString(STORAGE_KEY_TERM_THEME) || DEFAULT_TERMINAL_THEME);
|
||||
const [followAppTerminalTheme, setFollowAppTerminalThemeState] = useState<boolean>(() => {
|
||||
@@ -1171,6 +1230,11 @@ export const useSettingsState = (options: { enableSettingsSync?: boolean; enable
|
||||
// Persist product name
|
||||
useLayoutEffect(() => {
|
||||
localStorageAdapter.writeString(STORAGE_KEY_PRODUCT_NAME, productName);
|
||||
// 同步到主进程 inspection-data.json,远程浏览器 API 可读
|
||||
try {
|
||||
const nc = (window as any).netcatty;
|
||||
nc?.inspection?.saveProductName?.(productName).catch(() => {});
|
||||
} catch {}
|
||||
if (persistMountedRef.current) {
|
||||
notifySettingsChanged(STORAGE_KEY_PRODUCT_NAME, productName);
|
||||
// 更新窗口标题栏
|
||||
|
||||
@@ -1364,6 +1364,80 @@ export const useVaultState = () => {
|
||||
let cancelled = false;
|
||||
const init = async () => {
|
||||
try {
|
||||
// ─── 远程浏览器环境:从 HTTP API 获取所有 Vault 数据,跳过 Electron localStorage 加密路径
|
||||
const isRemote = typeof window !== "undefined" && !!(window as any).__REMOTE_TOKEN__;
|
||||
if (isRemote) {
|
||||
try {
|
||||
const token = (window as any).__REMOTE_TOKEN__ || "";
|
||||
const base = (p: string) => `${p}?token=${encodeURIComponent(token)}`;
|
||||
const apiBase = `/api/vault`;
|
||||
|
||||
// 并行 fetch 所有 vault 数据
|
||||
const results = await Promise.allSettled([
|
||||
fetch(base("/api/devices")).then(r => r.json()),
|
||||
fetch(base(`${apiBase}/keys`)).then(r => r.json()),
|
||||
fetch(base(`${apiBase}/proxies`)).then(r => r.json()),
|
||||
fetch(base(`${apiBase}/port-forwards`)).then(r => r.json()),
|
||||
fetch(base(`${apiBase}/snippets`)).then(r => r.json()),
|
||||
fetch(base(`${apiBase}/notes`)).then(r => r.json()),
|
||||
fetch(base(`${apiBase}/known-hosts`)).then(r => r.json()),
|
||||
fetch(base(`${apiBase}/connection-logs`)).then(r => r.json()),
|
||||
fetch(base(`${apiBase}/groups`)).then(r => r.json()),
|
||||
fetch(base(`${apiBase}/identities`)).then(r => r.json()),
|
||||
]);
|
||||
const getOk = (idx: number, fallback: any = null) =>
|
||||
results[idx]?.status === "fulfilled" && (results[idx]?.value as any)?.ok
|
||||
? (results[idx]?.value as any).data ?? (results[idx]?.value as any).devices ?? fallback
|
||||
: fallback;
|
||||
|
||||
// hosts
|
||||
const devicesData = getOk(0, []);
|
||||
if (Array.isArray(devicesData)) {
|
||||
const remoteHosts: Host[] = devicesData.map((d: any) => {
|
||||
const catalogId = d.catalog_id || d.type || "";
|
||||
return {
|
||||
id: d.host_id || crypto.randomUUID(),
|
||||
label: d.label || d.hostname || d.host_id,
|
||||
hostname: d.hostname || "",
|
||||
port: 22, username: "", tags: [], os: "linux",
|
||||
deviceType: /switch|firewall|router/i.test(catalogId) ? "network" : "general",
|
||||
distro: catalogId, distroMode: "auto",
|
||||
} as any;
|
||||
});
|
||||
hostsRef.current = remoteHosts;
|
||||
setHosts(remoteHosts);
|
||||
}
|
||||
// keys(已被 remoteHttpServer strip 敏感字段)
|
||||
const keysData = getOk(1, []);
|
||||
if (Array.isArray(keysData)) setKeys(keysData);
|
||||
// proxies
|
||||
const proxiesData = getOk(2, []);
|
||||
if (Array.isArray(proxiesData)) setProxyProfiles(proxiesData);
|
||||
// snippets
|
||||
const snData = getOk(4, []);
|
||||
if (Array.isArray(snData)) setSnippets(snData);
|
||||
// notes(可能是数组或对象)
|
||||
const notesData = getOk(5, null);
|
||||
if (notesData) setNotes(Array.isArray(notesData) ? notesData : (notesData.notes || []));
|
||||
// known hosts
|
||||
const khData = getOk(6, []);
|
||||
if (Array.isArray(khData)) setKnownHosts(khData);
|
||||
// connection logs
|
||||
const logData = getOk(7, []);
|
||||
if (Array.isArray(logData)) setConnectionLogs(logData);
|
||||
// identities
|
||||
const idData = getOk(9, []);
|
||||
if (Array.isArray(idData)) setIdentities(idData);
|
||||
} catch (e) {
|
||||
console.warn("[useVaultState] 远程 vault fetch 失败:", e);
|
||||
}
|
||||
if (!cancelled) {
|
||||
setIsInitialized(true);
|
||||
setVaultInitialized(true);
|
||||
}
|
||||
return; // 远程环境到此为止,跳过所有 Electron IPC 加载
|
||||
}
|
||||
|
||||
await withVaultImportLock("vault", async () => {
|
||||
recoverPluginImporterTransaction(localStorageAdapter, PLUGIN_IMPORT_TRANSACTION_KEYS);
|
||||
});
|
||||
@@ -1401,7 +1475,56 @@ export const useVaultState = () => {
|
||||
hostsWritePendingRef.current = writePromise;
|
||||
}
|
||||
} else {
|
||||
updateHosts(INITIAL_HOSTS);
|
||||
// 本地 Local Storage 为空(可能是 origin 隔离导致的打包版环境)
|
||||
// 尝试从主进程 inspectionStorage 的 _vault_hosts 恢复
|
||||
let restored = false;
|
||||
const nc = (window as any).netcatty;
|
||||
if (nc?.inspection?.getVaultHosts) {
|
||||
try {
|
||||
const vaultHosts = await nc.inspection.getVaultHosts();
|
||||
if (Array.isArray(vaultHosts) && vaultHosts.length > 0) {
|
||||
// 主进程 inspectionStorage 现在已存完整字段(含凭证),直接复制
|
||||
const migrated: Host[] = vaultHosts.map((v: any) => ({
|
||||
id: v.id,
|
||||
label: v.label || "",
|
||||
hostname: v.hostname || "",
|
||||
port: v.port || 22,
|
||||
username: v.username || "",
|
||||
password: v.password || undefined,
|
||||
identityFileId: v.identityFileId || undefined,
|
||||
privateKey: v.privateKey || "",
|
||||
passphrase: v.passphrase || "",
|
||||
tags: v.tags || [],
|
||||
os: v.os || "linux",
|
||||
type: v.type || "",
|
||||
distro: v.distro || "",
|
||||
group: v.group || "",
|
||||
}));
|
||||
if (cancelled) return;
|
||||
const sanitized = normalizeVaultOrder(
|
||||
migrated.map((h) => sanitizeHost(h)),
|
||||
);
|
||||
hostsRef.current = sanitized;
|
||||
setHosts(sanitized);
|
||||
// 加密写回 localStorage,后续启动就正常了
|
||||
const ver = ++hostsWriteVersion.current;
|
||||
const encryptPromise = encryptHosts(sanitized);
|
||||
hostsEncryptPendingRef.current = encryptPromise.then(() => undefined);
|
||||
const writePromise = encryptPromise.then(async (enc) => {
|
||||
if (ver !== hostsWriteVersion.current) return;
|
||||
return commitEncryptedHostsUnderVaultLock(ver, sanitized, enc);
|
||||
});
|
||||
hostsWritePendingRef.current = writePromise;
|
||||
restored = true;
|
||||
console.info("[useVaultState] 从 inspectionStorage 恢复 hosts:", sanitized.length, "台");
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("[useVaultState] 从 inspectionStorage 恢复失败:", e);
|
||||
}
|
||||
}
|
||||
if (!restored) {
|
||||
updateHosts(INITIAL_HOSTS);
|
||||
}
|
||||
}
|
||||
|
||||
// Read keys fresh here (not before the hosts await) so we don't apply
|
||||
@@ -2133,6 +2256,23 @@ export const useVaultState = () => {
|
||||
[importData],
|
||||
);
|
||||
|
||||
// ─── Electron 本地:把所有 vault 数据同步到 inspection-data.json(远程 HTTP API 读)
|
||||
// 仅在 Electron 本地生效(远程浏览器不走 sync,直接 fetch)
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
if ((window as any).__REMOTE_TOKEN__) return; // 远程浏览器不走
|
||||
const nc = (window as any).netcatty;
|
||||
if (!nc?.inspection?.syncVaultData) return;
|
||||
try { nc.inspection.syncVaultData("hosts", hosts).catch(() => {}); } catch {}
|
||||
try { nc.inspection.syncVaultData("keys", keys).catch(() => {}); } catch {}
|
||||
try { nc.inspection.syncVaultData("identities", identities).catch(() => {}); } catch {}
|
||||
try { nc.inspection.syncVaultData("proxies", proxyProfiles).catch(() => {}); } catch {}
|
||||
try { nc.inspection.syncVaultData("snippets", snippets).catch(() => {}); } catch {}
|
||||
try { nc.inspection.syncVaultData("notes", notes).catch(() => {}); } catch {}
|
||||
try { nc.inspection.syncVaultData("known_hosts", knownHosts).catch(() => {}); } catch {}
|
||||
try { nc.inspection.syncVaultData("connection_logs", connectionLogs).catch(() => {}); } catch {}
|
||||
}, [hosts, keys, identities, proxyProfiles, snippets, notes, knownHosts, connectionLogs]);
|
||||
|
||||
return {
|
||||
isInitialized,
|
||||
hosts,
|
||||
|
||||
Reference in New Issue
Block a user