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 - 统一底部按钮样式 */} = ({ // 支持按下 ESC 关闭弹窗 useEffect(() => { if (!isCommonConfigModalOpen) return; - + const onKeyDown = (e: KeyboardEvent) => { if (e.key === "Escape") { e.preventDefault(); @@ -97,9 +97,7 @@ const CodexConfigEditor: React.FC = ({ data-enable-grammarly="false" /> {authError && ( - - {authError} - + {authError} )} Codex auth.json 配置内容 @@ -161,7 +159,7 @@ const CodexConfigEditor: React.FC = ({ {isCommonConfigModalOpen && ( - { if (e.target === e.currentTarget) closeModal(); @@ -169,7 +167,7 @@ const CodexConfigEditor: React.FC = ({ > {/* Backdrop - 统一背景样式 */} - + {/* Modal - 统一窗口样式 */} {/* Header - 统一标题栏样式 */} @@ -186,7 +184,7 @@ const CodexConfigEditor: React.FC = ({ - + {/* Content - 统一内容区域样式 */} @@ -194,7 +192,9 @@ const CodexConfigEditor: React.FC = ({ handleCommonConfigSnippetChange(e.target.value)} + onChange={(e) => + handleCommonConfigSnippetChange(e.target.value) + } placeholder={`# Common Codex config # Add your common TOML configuration here`} rows={12} @@ -215,7 +215,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 aa5e284..930969f 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) { @@ -205,14 +211,14 @@ export const setApiKeyInConfig = ( // ========== TOML Config Utilities ========== -const COMMON_CONFIG_MARKER_START = "# === COMMON CONFIG START ==="; -const COMMON_CONFIG_MARKER_END = "# === COMMON CONFIG END ==="; - export interface UpdateTomlCommonConfigResult { updatedConfig: string; error?: string; } +// 保存之前的通用配置片段,用于替换操作 +let previousCommonSnippet = ""; + // 将通用配置片段写入/移除 TOML 配置 export const updateTomlCommonConfigSnippet = ( tomlString: string, @@ -220,71 +226,64 @@ export const updateTomlCommonConfigSnippet = ( enabled: boolean, ): UpdateTomlCommonConfigResult => { if (!snippetString.trim()) { - // 如果片段为空,移除已存在的通用配置部分 - const cleaned = removeTomlCommonConfig(tomlString); + // 如果片段为空,直接返回原始配置 return { - updatedConfig: cleaned, + updatedConfig: tomlString, }; } if (enabled) { // 添加通用配置 - const withoutOld = removeTomlCommonConfig(tomlString); - const commonSection = `\n${COMMON_CONFIG_MARKER_START}\n${snippetString}\n${COMMON_CONFIG_MARKER_END}\n`; + // 先移除旧的通用配置(如果有) + let updatedConfig = tomlString; + if (previousCommonSnippet && tomlString.includes(previousCommonSnippet)) { + updatedConfig = tomlString.replace(previousCommonSnippet, ""); + } + + // 在文件末尾添加新的通用配置 + // 确保有适当的换行 + const needsNewline = updatedConfig && !updatedConfig.endsWith("\n"); + updatedConfig = + updatedConfig + (needsNewline ? "\n\n" : "\n") + snippetString; + + // 保存当前通用配置片段 + previousCommonSnippet = snippetString; + return { - updatedConfig: withoutOld + commonSection, + updatedConfig: updatedConfig.trim() + "\n", }; } else { // 移除通用配置 - const cleaned = removeTomlCommonConfig(tomlString); + if (tomlString.includes(snippetString)) { + const updatedConfig = tomlString.replace(snippetString, ""); + // 清理多余的空行 + const cleaned = updatedConfig.replace(/\n{3,}/g, "\n\n").trim(); + + // 清空保存的状态 + previousCommonSnippet = ""; + + return { + updatedConfig: cleaned ? cleaned + "\n" : "", + }; + } return { - updatedConfig: cleaned, + updatedConfig: tomlString, }; } }; -// 从 TOML 中移除通用配置部分 -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') { - realStartIdx = startIdx - 1; - } - - // 找到标记后的换行符(如果有) - let realEndIdx = endIdx + COMMON_CONFIG_MARKER_END.length; - if (realEndIdx < tomlString.length && tomlString[realEndIdx] === '\n') { - realEndIdx = realEndIdx + 1; - } - - return tomlString.slice(0, realStartIdx) + tomlString.slice(realEndIdx); -}; - // 检查 TOML 配置是否已包含通用配置片段 export const hasTomlCommonConfigSnippet = ( tomlString: string, 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(); + + // 简单检查配置是否包含片段内容 + // 去除空白字符后比较,避免格式差异影响 + const normalizeWhitespace = (str: string) => str.replace(/\s+/g, " ").trim(); + + return normalizeWhitespace(tomlString).includes( + normalizeWhitespace(snippetString), + ); };
- {configError} -
{configError}
完整的 Claude Code settings.json 配置内容
@@ -171,7 +169,7 @@ const ClaudeConfigEditor: React.FC = ({
- {authError} -
{authError}
Codex auth.json 配置内容 @@ -161,7 +159,7 @@ const CodexConfigEditor: React.FC = ({
@@ -194,7 +192,9 @@ const CodexConfigEditor: React.FC = ({