diff --git a/src/App.tsx b/src/App.tsx index 866567a..9025499 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -13,9 +13,12 @@ import { buttonStyles } from "./lib/styles"; import { useDarkMode } from "./hooks/useDarkMode"; import { extractErrorMessage } from "./utils/errorUtils"; import { applyProviderToVSCode } from "./utils/vscodeSettings"; +import { getCodexBaseUrl } from "./utils/providerConfigUtils"; +import { useVSCodeAutoSync } from "./hooks/useVSCodeAutoSync"; function App() { const { isDarkMode, toggleDarkMode } = useDarkMode(); + const { isAutoSyncEnabled } = useVSCodeAutoSync(); const [activeApp, setActiveApp] = useState("claude"); const [providers, setProviders] = useState>({}); const [currentProviderId, setCurrentProviderId] = useState(""); @@ -77,7 +80,7 @@ function App() { }; }, []); - // 监听托盘切换事件 + // 监听托盘切换事件(包括菜单切换) useEffect(() => { let unlisten: (() => void) | null = null; @@ -92,6 +95,11 @@ function App() { if (data.appType === activeApp) { await loadProviders(); } + + // 若为 Codex 且开启自动同步,则静默同步到 VS Code(覆盖) + if (data.appType === "codex" && isAutoSyncEnabled) { + await syncCodexToVSCode(data.providerId, true); + } }); } catch (error) { console.error("设置供应商切换监听器失败:", error); @@ -106,7 +114,7 @@ function App() { unlisten(); } }; - }, [activeApp]); // 依赖activeApp,切换应用时重新设置监听器 + }, [activeApp, isAutoSyncEnabled]); // 依赖自动同步状态,确保拿到最新开关 const loadProviders = async () => { const loadedProviders = await window.api.getProviders(activeApp); @@ -176,11 +184,17 @@ function App() { }; // 同步Codex供应商到VS Code设置 - const syncCodexToVSCode = async (providerId: string) => { + const syncCodexToVSCode = async (providerId: string, silent = false) => { try { const status = await window.api.getVSCodeSettingsStatus(); if (!status.exists) { - showNotification("未找到 VS Code 用户设置文件 (settings.json)", "error", 3000); + if (!silent) { + showNotification( + "未找到 VS Code 用户设置文件 (settings.json)", + "error", + 3000, + ); + } return; } @@ -188,30 +202,42 @@ function App() { const provider = providers[providerId]; const isOfficial = provider?.category === "official"; - // 非官方供应商需要解析 base_url + // 非官方供应商需要解析 base_url(使用公共工具函数) let baseUrl: string | undefined = undefined; if (!isOfficial) { - const text = typeof provider?.settingsConfig?.config === "string" ? provider.settingsConfig.config : ""; - const baseUrlMatch = text.match(/base_url\s*=\s*(['"])([^'"]+)\1/); - if (!baseUrlMatch || !baseUrlMatch[2]) { - showNotification("当前配置缺少 base_url,无法写入 VS Code", "error", 4000); + const parsed = getCodexBaseUrl(provider); + if (!parsed) { + if (!silent) { + showNotification( + "当前配置缺少 base_url,无法写入 VS Code", + "error", + 4000, + ); + } return; } - baseUrl = baseUrlMatch[2]; + baseUrl = parsed; } - const updatedSettings = applyProviderToVSCode(raw, { baseUrl, isOfficial }); + const updatedSettings = applyProviderToVSCode(raw, { + baseUrl, + isOfficial, + }); if (updatedSettings !== raw) { await window.api.writeVSCodeSettings(updatedSettings); - showNotification("已同步到 VS Code", "success", 1500); + if (!silent) { + showNotification("已同步到 VS Code", "success", 1500); + } } - + // 触发providers重新加载,以更新VS Code按钮状态 await loadProviders(); } catch (error: any) { console.error("同步到VS Code失败:", error); - const errorMessage = error?.message || "同步 VS Code 失败"; - showNotification(errorMessage, "error", 5000); + if (!silent) { + const errorMessage = error?.message || "同步 VS Code 失败"; + showNotification(errorMessage, "error", 5000); + } } }; @@ -229,9 +255,9 @@ function App() { // 更新托盘菜单 await window.api.updateTrayMenu(); - // Codex: 切换供应商后自动同步到 VS Code - if (activeApp === "codex") { - await syncCodexToVSCode(id); + // Codex: 切换供应商后,只在自动同步启用时同步到 VS Code + if (activeApp === "codex" && isAutoSyncEnabled) { + await syncCodexToVSCode(id, true); // silent模式,不显示通知 } } else { showNotification("切换失败,请检查配置", "error"); diff --git a/src/components/ProviderList.tsx b/src/components/ProviderList.tsx index d08cd6b..6ca6607 100644 --- a/src/components/ProviderList.tsx +++ b/src/components/ProviderList.tsx @@ -3,7 +3,13 @@ import { Provider } from "../types"; import { Play, Edit3, Trash2, CheckCircle2, Users } from "lucide-react"; import { buttonStyles, cardStyles, badgeStyles, cn } from "../lib/styles"; import { AppType } from "../lib/tauri-api"; -import { applyProviderToVSCode, detectApplied, normalizeBaseUrl } from "../utils/vscodeSettings"; +import { + applyProviderToVSCode, + detectApplied, + normalizeBaseUrl, +} from "../utils/vscodeSettings"; +import { getCodexBaseUrl } from "../utils/providerConfigUtils"; +import { useVSCodeAutoSync } from "../hooks/useVSCodeAutoSync"; // 不再在列表中显示分类徽章,避免造成困惑 interface ProviderListProps { @@ -13,7 +19,11 @@ interface ProviderListProps { onDelete: (id: string) => void; onEdit: (id: string) => void; appType?: AppType; - onNotify?: (message: string, type: "success" | "error", duration?: number) => void; + onNotify?: ( + message: string, + type: "success" | "error", + duration?: number, + ) => void; } const ProviderList: React.FC = ({ @@ -53,22 +63,11 @@ const ProviderList: React.FC = ({ } }; - // 解析 Codex 配置中的 base_url(仅用于 VS Code 写入) - const getCodexBaseUrl = (provider: Provider): string | undefined => { - try { - const cfg = provider.settingsConfig; - const text = typeof cfg?.config === "string" ? cfg.config : ""; - if (!text) return undefined; - // 支持单/双引号 - const m = text.match(/base_url\s*=\s*(['"])([^'\"]+)\1/); - return m && m[2] ? m[2] : undefined; - } catch { - return undefined; - } - }; + // 解析 Codex 配置中的 base_url(已提取到公共工具) - // VS Code 按钮:仅在 Codex + 当前供应商显示;按钮文案根据是否“已应用”变化 + // VS Code 按钮:仅在 Codex + 当前供应商显示;按钮文案根据是否"已应用"变化 const [vscodeAppliedFor, setVscodeAppliedFor] = useState(null); + const { enableAutoSync, disableAutoSync } = useVSCodeAutoSync(); // 当当前供应商或 appType 变化时,尝试读取 VS Code settings 并检测状态 useEffect(() => { @@ -91,7 +90,8 @@ const ProviderList: React.FC = ({ if (current && current.category !== "official") { const base = getCodexBaseUrl(current); if (detected.apiBase && base) { - applied = normalizeBaseUrl(detected.apiBase) === normalizeBaseUrl(base); + applied = + normalizeBaseUrl(detected.apiBase) === normalizeBaseUrl(base); } } setVscodeAppliedFor(applied ? currentProviderId : null); @@ -106,7 +106,11 @@ const ProviderList: React.FC = ({ try { const status = await window.api.getVSCodeSettingsStatus(); if (!status.exists) { - onNotify?.("未找到 VS Code 用户设置文件 (settings.json)", "error", 3000); + onNotify?.( + "未找到 VS Code 用户设置文件 (settings.json)", + "error", + 3000, + ); return; } @@ -129,15 +133,19 @@ const ProviderList: React.FC = ({ // 幂等:没有变化也提示成功 onNotify?.("已应用到 VS Code", "success", 1500); setVscodeAppliedFor(provider.id); + // 用户手动应用时,启用自动同步 + enableAutoSync(); return; } await window.api.writeVSCodeSettings(next); onNotify?.("已应用到 VS Code", "success", 1500); setVscodeAppliedFor(provider.id); + // 用户手动应用时,启用自动同步 + enableAutoSync(); } catch (e: any) { console.error(e); - const msg = (e && e.message) ? e.message : "应用到 VS Code 失败"; + const msg = e && e.message ? e.message : "应用到 VS Code 失败"; onNotify?.(msg, "error", 5000); } }; @@ -146,22 +154,33 @@ const ProviderList: React.FC = ({ try { const status = await window.api.getVSCodeSettingsStatus(); if (!status.exists) { - onNotify?.("未找到 VS Code 用户设置文件 (settings.json)", "error", 3000); + onNotify?.( + "未找到 VS Code 用户设置文件 (settings.json)", + "error", + 3000, + ); return; } const raw = await window.api.readVSCodeSettings(); - const next = applyProviderToVSCode(raw, { baseUrl: undefined, isOfficial: true }); + const next = applyProviderToVSCode(raw, { + baseUrl: undefined, + isOfficial: true, + }); if (next === raw) { onNotify?.("已从 VS Code 移除", "success", 1500); setVscodeAppliedFor(null); + // 用户手动移除时,禁用自动同步 + disableAutoSync(); return; } await window.api.writeVSCodeSettings(next); onNotify?.("已从 VS Code 移除", "success", 1500); setVscodeAppliedFor(null); + // 用户手动移除时,禁用自动同步 + disableAutoSync(); } catch (e: any) { console.error(e); - const msg = (e && e.message) ? e.message : "移除失败"; + const msg = e && e.message ? e.message : "移除失败"; onNotify?.(msg, "error", 5000); } }; @@ -221,10 +240,12 @@ const ProviderList: React.FC = ({ {provider.name} {/* 分类徽章已移除 */} -
+
当前使用
@@ -254,29 +275,32 @@ const ProviderList: React.FC = ({
- {appType === "codex" && provider.category !== "official" && ( - - )} + {appType === "codex" && + provider.category !== "official" && ( + + )}
*/} + {/* VS Code 自动同步设置 */} +
+

+ Codex 设置 +

+ +
+ {/* 配置文件位置 */}

diff --git a/src/hooks/useVSCodeAutoSync.ts b/src/hooks/useVSCodeAutoSync.ts new file mode 100644 index 0000000..42564a4 --- /dev/null +++ b/src/hooks/useVSCodeAutoSync.ts @@ -0,0 +1,93 @@ +import { useState, useEffect, useCallback } from "react"; + +const VSCODE_AUTO_SYNC_KEY = "vscode-auto-sync-enabled"; +const VSCODE_AUTO_SYNC_EVENT = "vscode-auto-sync-changed"; + +export function useVSCodeAutoSync() { + const [isAutoSyncEnabled, setIsAutoSyncEnabled] = useState(false); + + // 从 localStorage 读取初始状态 + useEffect(() => { + try { + const saved = localStorage.getItem(VSCODE_AUTO_SYNC_KEY); + if (saved !== null) { + setIsAutoSyncEnabled(saved === "true"); + } + } catch (error) { + console.error("读取自动同步状态失败:", error); + } + }, []); + + // 订阅同窗口的自定义事件,以及跨窗口的 storage 事件,实现全局同步 + useEffect(() => { + const onCustom = (e: Event) => { + try { + const detail = (e as CustomEvent).detail as { enabled?: boolean } | undefined; + if (detail && typeof detail.enabled === "boolean") { + setIsAutoSyncEnabled(detail.enabled); + } else { + // 兜底:从 localStorage 读取 + const saved = localStorage.getItem(VSCODE_AUTO_SYNC_KEY); + if (saved !== null) setIsAutoSyncEnabled(saved === "true"); + } + } catch { + // 忽略 + } + }; + const onStorage = (e: StorageEvent) => { + if (e.key === VSCODE_AUTO_SYNC_KEY) { + setIsAutoSyncEnabled(e.newValue === "true"); + } + }; + window.addEventListener(VSCODE_AUTO_SYNC_EVENT, onCustom as EventListener); + window.addEventListener("storage", onStorage); + return () => { + window.removeEventListener(VSCODE_AUTO_SYNC_EVENT, onCustom as EventListener); + window.removeEventListener("storage", onStorage); + }; + }, []); + + // 启用自动同步 + const enableAutoSync = useCallback(() => { + try { + localStorage.setItem(VSCODE_AUTO_SYNC_KEY, "true"); + setIsAutoSyncEnabled(true); + // 通知同窗口其他订阅者 + window.dispatchEvent( + new CustomEvent(VSCODE_AUTO_SYNC_EVENT, { detail: { enabled: true } }), + ); + } catch (error) { + console.error("保存自动同步状态失败:", error); + } + }, []); + + // 禁用自动同步 + const disableAutoSync = useCallback(() => { + try { + localStorage.setItem(VSCODE_AUTO_SYNC_KEY, "false"); + setIsAutoSyncEnabled(false); + // 通知同窗口其他订阅者 + window.dispatchEvent( + new CustomEvent(VSCODE_AUTO_SYNC_EVENT, { detail: { enabled: false } }), + ); + } catch (error) { + console.error("保存自动同步状态失败:", error); + } + }, []); + + // 切换自动同步状态 + const toggleAutoSync = useCallback(() => { + if (isAutoSyncEnabled) { + disableAutoSync(); + } else { + enableAutoSync(); + } + }, [isAutoSyncEnabled, enableAutoSync, disableAutoSync]); + + return { + isAutoSyncEnabled, + enableAutoSync, + disableAutoSync, + toggleAutoSync, + }; +} diff --git a/src/lib/tauri-api.ts b/src/lib/tauri-api.ts index 5ef7b30..e14d517 100644 --- a/src/lib/tauri-api.ts +++ b/src/lib/tauri-api.ts @@ -244,7 +244,11 @@ export const tauriAPI = { }, // VS Code: 获取 settings.json 状态 - getVSCodeSettingsStatus: async (): Promise<{ exists: boolean; path: string; error?: string }> => { + getVSCodeSettingsStatus: async (): Promise<{ + exists: boolean; + path: string; + error?: string; + }> => { try { return await invoke("get_vscode_settings_status"); } catch (error) { diff --git a/src/utils/providerConfigUtils.ts b/src/utils/providerConfigUtils.ts index 930969f..7b2d3f1 100644 --- a/src/utils/providerConfigUtils.ts +++ b/src/utils/providerConfigUtils.ts @@ -287,3 +287,34 @@ export const hasTomlCommonConfigSnippet = ( normalizeWhitespace(snippetString), ); }; + +// ========== Codex base_url utils ========== + +// 从 Codex 的 TOML 配置文本中提取 base_url(支持单/双引号) +export const extractCodexBaseUrl = ( + configText: string | undefined | null, +): string | undefined => { + try { + const text = typeof configText === "string" ? configText : ""; + if (!text) return undefined; + const m = text.match(/base_url\s*=\s*(['"])([^'\"]+)\1/); + return m && m[2] ? m[2] : undefined; + } catch { + return undefined; + } +}; + +// 从 Provider 对象中提取 Codex base_url(当 settingsConfig.config 为 TOML 字符串时) +export const getCodexBaseUrl = ( + provider: { settingsConfig?: Record } | undefined | null, +): string | undefined => { + try { + const text = + typeof provider?.settingsConfig?.config === "string" + ? (provider as any).settingsConfig.config + : ""; + return extractCodexBaseUrl(text); + } catch { + return undefined; + } +}; diff --git a/src/utils/vscodeSettings.ts b/src/utils/vscodeSettings.ts index af33657..09ed9b4 100644 --- a/src/utils/vscodeSettings.ts +++ b/src/utils/vscodeSettings.ts @@ -37,7 +37,10 @@ export function removeManagedKeys(content: string): string { let out = content; // 删除 chatgpt.apiBase try { - out = applyEdits(out, modify(out, ["chatgpt.apiBase"], undefined, { formattingOptions: fmt })); + out = applyEdits( + out, + modify(out, ["chatgpt.apiBase"], undefined, { formattingOptions: fmt }), + ); } catch { // 忽略删除失败 } @@ -69,8 +72,16 @@ export function removeManagedKeys(content: string): string { try { const data = parse(out) as any; const cfg = data?.["chatgpt.config"]; - if (cfg && typeof cfg === "object" && !Array.isArray(cfg) && Object.keys(cfg).length === 0) { - out = applyEdits(out, modify(out, ["chatgpt.config"], undefined, { formattingOptions: fmt })); + if ( + cfg && + typeof cfg === "object" && + !Array.isArray(cfg) && + Object.keys(cfg).length === 0 + ) { + out = applyEdits( + out, + modify(out, ["chatgpt.config"], undefined, { formattingOptions: fmt }), + ); } } catch { // 忽略解析失败,保持已删除的键 @@ -97,7 +108,10 @@ export function applyProviderToVSCode( }; out = JSON.stringify(obj, null, 2) + "\n"; } else { - out = applyEdits(out, modify(out, ["chatgpt.apiBase"], apiBase, { formattingOptions: fmt })); + out = applyEdits( + out, + modify(out, ["chatgpt.apiBase"], apiBase, { formattingOptions: fmt }), + ); out = applyEdits( out, modify(out, ["chatgpt.config", "preferred_auth_method"], "apikey", {