diff --git a/src/components/AppSwitcher.tsx b/src/components/AppSwitcher.tsx index c09cc46..5c21cd5 100644 --- a/src/components/AppSwitcher.tsx +++ b/src/components/AppSwitcher.tsx @@ -26,8 +26,8 @@ export function AppSwitcher({ activeApp, onSwitch }: AppSwitcherProps) { diff --git a/src/components/JsonEditor.tsx b/src/components/JsonEditor.tsx index bdd1c16..65868e3 100644 --- a/src/components/JsonEditor.tsx +++ b/src/components/JsonEditor.tsx @@ -62,7 +62,7 @@ const JsonEditor: React.FC = ({ return diagnostics; }), - [showValidation] + [showValidation], ); useEffect(() => { diff --git a/src/components/ProviderForm.tsx b/src/components/ProviderForm.tsx index ed04869..9045e78 100644 --- a/src/components/ProviderForm.tsx +++ b/src/components/ProviderForm.tsx @@ -128,25 +128,26 @@ const ProviderForm: React.FC = ({ const [settingsConfigError, setSettingsConfigError] = useState(""); // 用于跟踪是否正在通过通用配置更新 const isUpdatingFromCommonConfig = useRef(false); - + // Codex 通用配置状态 const [useCodexCommonConfig, setUseCodexCommonConfig] = useState(false); - const [codexCommonConfigSnippet, setCodexCommonConfigSnippetState] = useState(() => { - if (typeof window === "undefined") { - return DEFAULT_CODEX_COMMON_CONFIG_SNIPPET; - } - try { - const stored = window.localStorage.getItem( - CODEX_COMMON_CONFIG_STORAGE_KEY, - ); - if (stored && stored.trim()) { - return stored; + const [codexCommonConfigSnippet, setCodexCommonConfigSnippetState] = + useState(() => { + if (typeof window === "undefined") { + return DEFAULT_CODEX_COMMON_CONFIG_SNIPPET; } - } catch { - // ignore localStorage 读取失败 - } - return DEFAULT_CODEX_COMMON_CONFIG_SNIPPET; - }); + try { + const stored = window.localStorage.getItem( + CODEX_COMMON_CONFIG_STORAGE_KEY, + ); + if (stored && stored.trim()) { + return stored; + } + } catch { + // ignore localStorage 读取失败 + } + return DEFAULT_CODEX_COMMON_CONFIG_SNIPPET; + }); const [codexCommonConfigError, setCodexCommonConfigError] = useState(""); const isUpdatingFromCodexCommonConfig = useRef(false); // -1 表示自定义,null 表示未选择,>= 0 表示预设索引 @@ -217,7 +218,11 @@ const ProviderForm: React.FC = ({ useEffect(() => { if (initialData) { if (!isCodex) { - const configString = JSON.stringify(initialData.settingsConfig, null, 2); + const configString = JSON.stringify( + initialData.settingsConfig, + null, + 2, + ); const hasCommon = hasCommonConfigSnippet( configString, commonConfigSnippet, @@ -235,7 +240,9 @@ const ProviderForm: React.FC = ({ }; if (config.env) { setClaudeModel(config.env.ANTHROPIC_MODEL || ""); - setClaudeSmallFastModel(config.env.ANTHROPIC_SMALL_FAST_MODEL || ""); + setClaudeSmallFastModel( + config.env.ANTHROPIC_SMALL_FAST_MODEL || "", + ); setBaseUrl(config.env.ANTHROPIC_BASE_URL || ""); // 初始化基础 URL // 初始化 Kimi 模型选择 @@ -254,7 +261,13 @@ const ProviderForm: React.FC = ({ setUseCodexCommonConfig(hasCommon); } } - }, [initialData, commonConfigSnippet, codexCommonConfigSnippet, isCodex, codexConfig]); + }, [ + initialData, + commonConfigSnippet, + codexCommonConfigSnippet, + isCodex, + codexConfig, + ]); // 当选择预设变化时,同步类别 useEffect(() => { @@ -497,7 +510,7 @@ const ProviderForm: React.FC = ({ isUpdatingFromCommonConfig.current = false; }, 0); } - + // 保存通用配置到 localStorage if (!validationError && typeof window !== "undefined") { try { @@ -529,10 +542,7 @@ const ProviderForm: React.FC = ({ setBaseUrl(""); // 清空基础 URL // 同步通用配置状态 - const hasCommon = hasCommonConfigSnippet( - configString, - commonConfigSnippet, - ); + const hasCommon = hasCommonConfigSnippet(configString, commonConfigSnippet); setUseCommonConfig(hasCommon); setCommonConfigError(""); @@ -643,10 +653,7 @@ const ProviderForm: React.FC = ({ updateSettingsConfigValue(configString); // 同步通用配置开关 - const hasCommon = hasCommonConfigSnippet( - configString, - commonConfigSnippet, - ); + const hasCommon = hasCommonConfigSnippet(configString, commonConfigSnippet); setUseCommonConfig(hasCommon); }; @@ -681,11 +688,12 @@ const ProviderForm: React.FC = ({ // Codex: 处理通用配置开关 const handleCodexCommonConfigToggle = (checked: boolean) => { - const { updatedConfig, error: snippetError } = updateTomlCommonConfigSnippet( - codexConfig, - codexCommonConfigSnippet, - checked, - ); + const { updatedConfig, error: snippetError } = + updateTomlCommonConfigSnippet( + codexConfig, + codexCommonConfigSnippet, + checked, + ); if (snippetError) { setCodexCommonConfigError(snippetError); @@ -753,10 +761,7 @@ const ProviderForm: React.FC = ({ // 保存 Codex 通用配置到 localStorage if (typeof window !== "undefined") { try { - window.localStorage.setItem( - CODEX_COMMON_CONFIG_STORAGE_KEY, - value, - ); + window.localStorage.setItem(CODEX_COMMON_CONFIG_STORAGE_KEY, value); } catch { // ignore localStorage 写入失败 } @@ -1177,7 +1182,9 @@ const ProviderForm: React.FC = ({ useCommonConfig={useCodexCommonConfig} onCommonConfigToggle={handleCodexCommonConfigToggle} commonConfigSnippet={codexCommonConfigSnippet} - onCommonConfigSnippetChange={handleCodexCommonConfigSnippetChange} + onCommonConfigSnippetChange={ + handleCodexCommonConfigSnippetChange + } commonConfigError={codexCommonConfigError} authError={codexAuthError} /> diff --git a/src/components/ProviderForm/ClaudeConfigEditor.tsx b/src/components/ProviderForm/ClaudeConfigEditor.tsx index 2b75e6d..03aa98b 100644 --- a/src/components/ProviderForm/ClaudeConfigEditor.tsx +++ b/src/components/ProviderForm/ClaudeConfigEditor.tsx @@ -60,7 +60,7 @@ const ClaudeConfigEditor: React.FC = ({ // 支持按下 ESC 关闭弹窗 useEffect(() => { if (!isCommonConfigModalOpen) return; - + const onKeyDown = (e: KeyboardEvent) => { if (e.key === "Escape") { e.preventDefault(); @@ -120,15 +120,13 @@ const ClaudeConfigEditor: React.FC = ({ rows={12} /> {configError && ( - - {configError} - + {configError} )} 完整的 Claude Code settings.json 配置内容 {isCommonConfigModalOpen && ( - { if (e.target === e.currentTarget) closeModal(); @@ -136,7 +134,7 @@ const ClaudeConfigEditor: React.FC = ({ > {/* Backdrop - 统一背景样式 */} - + {/* Modal - 统一窗口样式 */} {/* Header - 统一标题栏样式 */} @@ -153,7 +151,7 @@ const ClaudeConfigEditor: React.FC = ({ - + {/* Content - 统一内容区域样式 */} @@ -171,7 +169,7 @@ const ClaudeConfigEditor: React.FC = ({ )} - + {/* Footer - 统一底部按钮样式 */} = ({ const [vscodeError, setVscodeError] = useState(""); const [vscodeSuccess, setVscodeSuccess] = useState(""); const [isWritingVscode, setIsWritingVscode] = useState(false); + const lastAppliedBaseUrlRef = useRef(null); useEffect(() => { if (commonConfigError && !isCommonConfigModalOpen) { @@ -49,6 +50,61 @@ const CodexConfigEditor: React.FC = ({ return () => window.clearTimeout(timer); }, [vscodeSuccess]); + const ensureVscodeApiAvailable = () => { + if (typeof window === "undefined" || !window.api?.writeVscodeSettings) { + setVscodeError("当前环境暂不支持写入 VS Code 配置"); + return false; + } + return true; + }; + + const applyVscodeConfig = async ( + baseUrl: string, + successMessage = "已写入 VS Code 配置", + ) => { + if (!ensureVscodeApiAvailable()) { + return false; + } + + setIsWritingVscode(true); + try { + const success = await window.api.writeVscodeSettings(baseUrl); + if (success) { + setVscodeSuccess(successMessage); + lastAppliedBaseUrlRef.current = baseUrl; + return true; + } + setVscodeError("写入 VS Code 配置失败,请稍后重试"); + } catch (error) { + setVscodeError(`写入 VS Code 配置失败: ${String(error)}`); + } finally { + setIsWritingVscode(false); + } + return false; + }; + + const removeVscodeConfig = async () => { + if (!ensureVscodeApiAvailable()) { + return false; + } + + setIsWritingVscode(true); + try { + const success = await window.api.writeVscodeSettings(); + if (success) { + setVscodeSuccess("已移除 VS Code 配置"); + lastAppliedBaseUrlRef.current = null; + return true; + } + setVscodeError("移除 VS Code 配置失败,请稍后重试"); + } catch (error) { + setVscodeError(`移除 VS Code 配置失败: ${String(error)}`); + } finally { + setIsWritingVscode(false); + } + return false; + }; + const handleVscodeConfigToggle = async (checked: boolean) => { if (isWritingVscode) return; @@ -56,12 +112,6 @@ const CodexConfigEditor: React.FC = ({ setVscodeError(""); setVscodeSuccess(""); - if (typeof window === "undefined" || !window.api?.writeVscodeSettings) { - setVscodeError("当前环境暂不支持写入 VS Code 配置"); - setWriteVscodeConfig(!checked); - return; - } - if (checked) { const trimmed = configValue.trim(); if (!trimmed) { @@ -77,46 +127,70 @@ const CodexConfigEditor: React.FC = ({ return; } + const success = await applyVscodeConfig(baseUrl); + if (!success) { + setWriteVscodeConfig(false); + } + return; + } + + const success = await removeVscodeConfig(); + if (!success) { + setWriteVscodeConfig(true); + } + }; + + useEffect(() => { + if (!writeVscodeConfig || isWritingVscode) { + return; + } + + const trimmed = configValue.trim(); + if (!trimmed) { + return; + } + + const baseUrl = extractBaseUrlFromToml(trimmed); + if (!baseUrl) { + setVscodeError("未在 config.toml 中找到 base_url 字段"); + setWriteVscodeConfig(false); + return; + } + + if (lastAppliedBaseUrlRef.current === baseUrl) { + return; + } + + const sync = async () => { + // 直接调用 API 而不依赖 applyVscodeConfig 函数,避免闭包问题 + if (typeof window === "undefined" || !window.api?.writeVscodeSettings) { + setVscodeError("当前环境暂不支持写入 VS Code 配置"); + return; + } + setIsWritingVscode(true); try { const success = await window.api.writeVscodeSettings(baseUrl); if (success) { - setVscodeSuccess("已写入 VS Code 配置"); + setVscodeSuccess("已更新 VS Code 配置"); + lastAppliedBaseUrlRef.current = baseUrl; } else { setVscodeError("写入 VS Code 配置失败,请稍后重试"); - setWriteVscodeConfig(false); } } catch (error) { setVscodeError(`写入 VS Code 配置失败: ${String(error)}`); - setWriteVscodeConfig(false); } finally { setIsWritingVscode(false); } + }; - return; - } - - setIsWritingVscode(true); - try { - const success = await window.api.writeVscodeSettings(); - if (success) { - setVscodeSuccess("已移除 VS Code 配置"); - } else { - setVscodeError("移除 VS Code 配置失败,请稍后重试"); - setWriteVscodeConfig(true); - } - } catch (error) { - setVscodeError(`移除 VS Code 配置失败: ${String(error)}`); - setWriteVscodeConfig(true); - } finally { - setIsWritingVscode(false); - } - }; + sync(); + }, [configValue, writeVscodeConfig, isWritingVscode]); // 支持按下 ESC 关闭弹窗 useEffect(() => { if (!isCommonConfigModalOpen) return; - + const onKeyDown = (e: KeyboardEvent) => { if (e.key === "Escape") { e.preventDefault(); @@ -162,9 +236,7 @@ const CodexConfigEditor: React.FC = ({ data-enable-grammarly="false" /> {authError && ( - - {authError} - + {authError} )} Codex auth.json 配置内容 @@ -179,7 +251,7 @@ const CodexConfigEditor: React.FC = ({ > config.toml (TOML) - + {/* 右侧对齐的双列布局 - 使用flex而非grid以更好控制宽度 */} {/* 左列:VS Code 配置 */} @@ -201,13 +273,16 @@ const CodexConfigEditor: React.FC = ({ )} {vscodeError && ( - + {vscodeError} )} - + {/* 右列:通用配置 - 不设置宽度,让内容自然收缩 */} @@ -227,14 +302,17 @@ const CodexConfigEditor: React.FC = ({ 编辑通用配置 {commonConfigError && !isCommonConfigModalOpen && ( - + {commonConfigError} )} - + = ({ {isCommonConfigModalOpen && ( - { if (e.target === e.currentTarget) closeModal(); @@ -266,7 +344,7 @@ const CodexConfigEditor: React.FC = ({ > {/* Backdrop - 统一背景样式 */} - + {/* Modal - 统一窗口样式 */} {/* Header - 统一标题栏样式 */} @@ -283,7 +361,7 @@ const CodexConfigEditor: React.FC = ({ - + {/* Content - 统一内容区域样式 */} @@ -312,7 +390,7 @@ const CodexConfigEditor: React.FC = ({ )} - + {/* Footer - 统一底部按钮样式 */} { } return ""; -}; \ No newline at end of file +}; diff --git a/src/utils/providerConfigUtils.ts b/src/utils/providerConfigUtils.ts index e50b845..4c05c70 100644 --- a/src/utils/providerConfigUtils.ts +++ b/src/utils/providerConfigUtils.ts @@ -22,7 +22,10 @@ const deepMerge = ( return target; }; -const deepRemove = (target: Record, source: Record) => { +const deepRemove = ( + target: Record, + source: Record, +) => { Object.entries(source).forEach(([key, value]) => { if (!(key in target)) return; @@ -59,7 +62,7 @@ const isSubset = (target: any, source: any): boolean => { const deepClone = (obj: T): T => { if (obj === null || typeof obj !== "object") return obj; if (obj instanceof Date) return new Date(obj.getTime()) as T; - if (obj instanceof Array) return obj.map(item => deepClone(item)) as T; + if (obj instanceof Array) return obj.map((item) => deepClone(item)) as T; if (obj instanceof Object) { const clonedObj = {} as T; for (const key in obj) { @@ -78,7 +81,10 @@ export interface UpdateCommonConfigResult { } // 验证JSON配置格式 -export const validateJsonConfig = (value: string, fieldName: string = "配置"): string => { +export const validateJsonConfig = ( + value: string, + fieldName: string = "配置", +): string => { if (!value.trim()) { return ""; } @@ -123,7 +129,7 @@ export const updateCommonConfigSnippet = ( error: snippetError, }; } - + const snippet = JSON.parse(snippetString) as Record; if (enabled) { @@ -247,23 +253,23 @@ export const updateTomlCommonConfigSnippet = ( const removeTomlCommonConfig = (tomlString: string): string => { const startIdx = tomlString.indexOf(COMMON_CONFIG_MARKER_START); const endIdx = tomlString.indexOf(COMMON_CONFIG_MARKER_END); - + if (startIdx === -1 || endIdx === -1) { return tomlString; } - + // 找到标记前的换行符(如果有) let realStartIdx = startIdx; - if (startIdx > 0 && tomlString[startIdx - 1] === '\n') { + if (startIdx > 0 && tomlString[startIdx - 1] === "\n") { realStartIdx = startIdx - 1; } - + // 找到标记后的换行符(如果有) let realEndIdx = endIdx + COMMON_CONFIG_MARKER_END.length; - if (realEndIdx < tomlString.length && tomlString[realEndIdx] === '\n') { + if (realEndIdx < tomlString.length && tomlString[realEndIdx] === "\n") { realEndIdx = realEndIdx + 1; } - + return tomlString.slice(0, realStartIdx) + tomlString.slice(realEndIdx); }; @@ -273,19 +279,19 @@ export const hasTomlCommonConfigSnippet = ( snippetString: string, ): boolean => { if (!snippetString.trim()) return false; - + const startIdx = tomlString.indexOf(COMMON_CONFIG_MARKER_START); const endIdx = tomlString.indexOf(COMMON_CONFIG_MARKER_END); - + if (startIdx === -1 || endIdx === -1 || startIdx >= endIdx) { return false; } - + // 提取标记之间的内容 const existingSnippet = tomlString .slice(startIdx + COMMON_CONFIG_MARKER_START.length, endIdx) .trim(); - + return existingSnippet === snippetString.trim(); };
- {configError} -
{configError}
完整的 Claude Code settings.json 配置内容
@@ -171,7 +169,7 @@ const ClaudeConfigEditor: React.FC = ({
- {authError} -
{authError}
Codex auth.json 配置内容 @@ -179,7 +251,7 @@ const CodexConfigEditor: React.FC = ({ > config.toml (TOML) - + {/* 右侧对齐的双列布局 - 使用flex而非grid以更好控制宽度 */} {/* 左列:VS Code 配置 */} @@ -201,13 +273,16 @@ const CodexConfigEditor: React.FC = ({ )} {vscodeError && ( - + {vscodeError} )}
+
{vscodeError}
{commonConfigError}
@@ -312,7 +390,7 @@ const CodexConfigEditor: React.FC = ({