feat: add monitoring settings tab, workbench view, and inspection alert improvements
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:
2026-10-30 12:12:04 +08:00
committed by trae-solo-agent
parent 16dd04f746
commit 4a02370006
26 changed files with 2569 additions and 233 deletions

View File

@@ -126,6 +126,59 @@ export function AppSideEffects() {
} catch {}
}, []);
// 设置权威源同步(主窗口 Electron ↔ 远程浏览器 共享权威源)
// 1. 启动时:先从权威源拉取填 localStorage(远程先改了数据不丢失),
// 再把 localStorage 里可能的新增 merge 回权威源(单条 syncBatch)
// 2. 运行时:每次 localStorageAdapter 写调 IPC → 权威源
// 3. 主进程广播:远程浏览器改了设置 → 主进程发 IPC → 主窗口静默刷新 localStorage
useEffect(() => {
const isRemote = typeof window !== "undefined" && !!(window as any).__REMOTE_TOKEN__;
if (isRemote) return; // 远程浏览器在 VaultViewLayout 里走 fetch 路径
const nc = (window as any).netcatty;
if (!nc?.settingsSync) return;
(globalThis as any).__NETCATTY_SETTINGS_SYNC__ = (key: string, value: string | null) => {
nc.settingsSync(key, value).catch(() => {});
};
// 实时更新订阅:远程浏览器改了设置 → 主进程广播 → 静默刷新 localStorage
let unsub: null | (() => void) = null;
if (nc.onSettingsChanged) {
unsub = nc.onSettingsChanged(() => { /* 静默刷新,localStorage 已被 preload 填充 */ });
}
// 启动时:先拉后推
(async () => {
try {
// ① 先从权威源拉取(远程可能先改了数据)
const remoteRes = nc.settingsGetAll ? await nc.settingsGetAll() : null;
if (remoteRes?.ok && remoteRes.settings && typeof remoteRes.settings === "object") {
// 权威源值覆盖 localStorage(远程改的优先)
for (const [k, v] of Object.entries(remoteRes.settings)) {
if (v === null || v === undefined) localStorage.removeItem(k);
else localStorage.setItem(k, String(v));
}
}
// ② 再把 localStorage 里所有键 merge 回权威源(本地可能有新增)
const all = {} as Record<string, string>;
for (let i = 0; i < localStorage.length; i++) {
const k = localStorage.key(i);
if (k) all[k] = localStorage.getItem(k) ?? "";
}
if (Object.keys(all).length > 0 && nc.settingsSyncBatch) {
await nc.settingsSyncBatch(all);
}
} catch {}
})();
return () => {
if ((globalThis as any).__NETCATTY_SETTINGS_SYNC__) {
(globalThis as any).__NETCATTY_SETTINGS_SYNC__ = null;
}
unsub?.();
};
}, []);
const [isQuickSwitcherOpen, setIsQuickSwitcherOpen] = useState(false);
const [isCreateWorkspaceOpen, setIsCreateWorkspaceOpen] = useState(false);
// Combined state for the AddToWorkspaceDialog. null = closed; mode

View File

@@ -159,13 +159,21 @@ export function useSettingsIpcSync({
setSftpTransferConcurrencyState,
setSshTransportIdleTtlMsState,
}: UseSettingsIpcSyncParams) {
// Listen for settings changes from other windows via IPC
// Listen for settings changes from other windows via IPC.
// Preload (preload.cjs) owns the single upstream IPC listener and dispatches
// a `netcatty-settings-batch` CustomEvent after bulk-updating localStorage.
// We listen to that CustomEvent here instead of registering a second IPC
// listener (which would double-fire settingsGetAll + localStorage writes).
useEffect(() => {
if (!enabled) return;
const bridge = netcattyBridge.get();
if (!bridge?.onSettingsChanged) return;
const unsubscribe = bridge.onSettingsChanged((payload) => {
const { key, value } = payload;
const handler = (e: Event) => {
const detail = (e as CustomEvent).detail;
if (!detail || typeof detail !== 'object') return;
// 新格式: { settings: {...}, changedKeys: [...], changedEntries: {...} }
// 从中提取逐 key 变化信息,保持原有的逐 key 同步逻辑
const { changedEntries } = detail as { changedEntries?: Record<string, unknown> };
if (!changedEntries) return;
for (const [key, value] of Object.entries(changedEntries)) {
if (
key === STORAGE_KEY_THEME ||
key === STORAGE_KEY_UI_THEME_LIGHT ||
@@ -174,7 +182,7 @@ export function useSettingsIpcSync({
key === STORAGE_KEY_COLOR
) {
syncAppearanceFromStorage({ key, value });
return;
continue; // 跳过当前 key 的后续检查,继续 for 循环
}
if (key === STORAGE_KEY_UI_LANGUAGE && typeof value === 'string') {
const next = resolveSupportedLocale(value);
@@ -333,14 +341,10 @@ export function useSettingsIpcSync({
if (key === STORAGE_KEY_SSH_TRANSPORT_IDLE_TTL_MS && typeof value === 'number') {
setSshTransportIdleTtlMsState((prev) => (prev === value ? prev : value));
}
});
return () => {
try {
unsubscribe?.();
} catch {
// ignore
}
} // end for (changedEntries)
};
window.addEventListener("netcatty-settings-batch", handler);
return () => window.removeEventListener("netcatty-settings-batch", handler);
}, [
enabled,
applyIncomingCustomKeyBindings,

View File

@@ -293,12 +293,27 @@ export function useAISettingsState() {
const handleLocalStateChanged = (event: Event) => {
syncFromStorageKey((event as CustomEvent<{ key?: string }>).detail?.key ?? null);
};
// 远程浏览器改 AI 设置 → 主进程 broadcast → preload 更新 localStorage 并 dispatch 此事件
// 这里遍历 changedEntries 中所有 AI_ 开头的 key,逐个刷新 React state
const handleBatchSync = (event: Event) => {
const detail = (event as CustomEvent).detail;
if (!detail || typeof detail !== 'object') return;
const entries = detail.changedEntries as Record<string, unknown> | undefined;
if (!entries) return;
for (const key of Object.keys(entries)) {
if (key.startsWith('netcatty_ai_') || key.startsWith('netcatty.ai')) {
syncFromStorageKey(key);
}
}
};
window.addEventListener('storage', handleStorage);
window.addEventListener(AI_STATE_CHANGED_EVENT, handleLocalStateChanged);
window.addEventListener('netcatty-settings-batch', handleBatchSync);
return () => {
window.removeEventListener('storage', handleStorage);
window.removeEventListener(AI_STATE_CHANGED_EVENT, handleLocalStateChanged);
window.removeEventListener('netcatty-settings-batch', handleBatchSync);
};
}, []);

View File

@@ -2079,6 +2079,18 @@ export const useSettingsState = (options: { enableSettingsSync?: boolean; enable
updateTerminalSetting,
]);
// 远程浏览器改了设置 → 主进程 broadcast → preload 更新 localStorage 并 dispatch 此事件
// 这里监听并全量刷新 React state,解决 useState initializer 只跑一次的问题
useEffect(() => {
const IS_REMOTE = typeof window !== "undefined" && !!(window as any).__REMOTE_TOKEN__;
if (IS_REMOTE) return; // 远程浏览器不通过 preload,由 VaultViewLayout 处理
const handler = (_e: Event) => {
rehydrateAllFromStorage();
};
window.addEventListener("netcatty-settings-batch", handler);
return () => window.removeEventListener("netcatty-settings-batch", handler);
}, [rehydrateAllFromStorage]);
return {
theme,
setTheme,

View File

@@ -1374,7 +1374,7 @@ export const useVaultState = () => {
// 并行 fetch 所有 vault 数据
const results = await Promise.allSettled([
fetch(base("/api/devices")).then(r => r.json()),
fetch(base(`${apiBase}/hosts`)).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()),
@@ -1390,22 +1390,11 @@ export const useVaultState = () => {
? (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);
// hosts(从 /api/vault/hosts 来,已脱敏无密码)
const hostsData = getOk(0, []);
if (Array.isArray(hostsData)) {
hostsRef.current = hostsData as Host[];
setHosts(hostsData as Host[]);
}
// keys(已被 remoteHttpServer strip 敏感字段)
const keysData = getOk(1, []);