From fb8996d19c8be352d6ce99573153b0f801c9d021 Mon Sep 17 00:00:00 2001 From: wugeer <1284057728@qq.com> Date: Sat, 7 Mar 2026 22:53:44 +0800 Subject: [PATCH] fix:Add a new vendor page, API endpoint, and model name. (#1155) * fix:Add a new vendor page, API endpoint, and model name. Fix the bug where, after entering characters, line breaks cannot be fully deleted. * fix: add missing i18n key codexConfig.modelNameHint for zh/en/ja --------- Co-authored-by: Jason --- .../providers/forms/CodexFormFields.tsx | 10 +++- .../providers/forms/hooks/useBaseUrlState.ts | 8 +-- .../forms/hooks/useCodexConfigState.ts | 20 ++----- src/i18n/locales/en.json | 3 +- src/i18n/locales/ja.json | 3 +- src/i18n/locales/zh.json | 3 +- src/utils/providerConfigUtils.ts | 33 ++++++++--- tests/utils/providerConfigUtils.codex.test.ts | 55 +++++++++++++++++++ 8 files changed, 101 insertions(+), 34 deletions(-) create mode 100644 tests/utils/providerConfigUtils.codex.test.ts diff --git a/src/components/providers/forms/CodexFormFields.tsx b/src/components/providers/forms/CodexFormFields.tsx index c527b991f..99d8aa7be 100644 --- a/src/components/providers/forms/CodexFormFields.tsx +++ b/src/components/providers/forms/CodexFormFields.tsx @@ -117,9 +117,13 @@ export function CodexFormFields({ className="w-full px-3 py-2 border border-border-default bg-background text-foreground rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:focus:ring-blue-400/20 transition-colors" />

- {t("codexConfig.modelNameHint", { - defaultValue: "指定使用的模型,将自动更新到 config.toml 中", - })} + {modelName.trim() + ? t("codexConfig.modelNameHint", { + defaultValue: "指定使用的模型,将自动更新到 config.toml 中", + }) + : t("providerForm.modelHint", { + defaultValue: "💡 留空将使用供应商的默认模型", + })}

)} diff --git a/src/components/providers/forms/hooks/useBaseUrlState.ts b/src/components/providers/forms/hooks/useBaseUrlState.ts index 037dfeb3b..2944dfdf3 100644 --- a/src/components/providers/forms/hooks/useBaseUrlState.ts +++ b/src/components/providers/forms/hooks/useBaseUrlState.ts @@ -60,10 +60,8 @@ export function useBaseUrlState({ if (!codexConfig) return; const extracted = extractCodexBaseUrl(codexConfig) || ""; - if (extracted !== codexBaseUrl) { - setCodexBaseUrl(extracted); - } - }, [appType, category, codexConfig, codexBaseUrl]); + setCodexBaseUrl((prev) => (prev === extracted ? prev : extracted)); + }, [appType, category, codexConfig]); // 从Claude配置同步到 state(Gemini) useEffect(() => { @@ -116,7 +114,7 @@ export function useBaseUrlState({ const sanitized = url.trim(); setCodexBaseUrl(sanitized); - if (!sanitized || !onCodexConfigChange) { + if (!onCodexConfigChange) { return; } diff --git a/src/components/providers/forms/hooks/useCodexConfigState.ts b/src/components/providers/forms/hooks/useCodexConfigState.ts index a4271c067..1747d6c12 100644 --- a/src/components/providers/forms/hooks/useCodexConfigState.ts +++ b/src/components/providers/forms/hooks/useCodexConfigState.ts @@ -74,10 +74,8 @@ export function useCodexConfigState({ initialData }: UseCodexConfigStateProps) { return; } const extracted = extractCodexBaseUrl(codexConfig) || ""; - if (extracted !== codexBaseUrl) { - setCodexBaseUrl(extracted); - } - }, [codexConfig, codexBaseUrl]); + setCodexBaseUrl((prev) => (prev === extracted ? prev : extracted)); + }, [codexConfig]); // 与 TOML 配置保持模型名称同步 useEffect(() => { @@ -85,10 +83,8 @@ export function useCodexConfigState({ initialData }: UseCodexConfigStateProps) { return; } const extracted = extractCodexModelName(codexConfig) || ""; - if (extracted !== codexModelName) { - setCodexModelName(extracted); - } - }, [codexConfig, codexModelName]); + setCodexModelName((prev) => (prev === extracted ? prev : extracted)); + }, [codexConfig]); // 获取 API Key(从 auth JSON) const getCodexAuthApiKey = useCallback((authString: string): string => { @@ -165,10 +161,6 @@ export function useCodexConfigState({ initialData }: UseCodexConfigStateProps) { const sanitized = url.trim(); setCodexBaseUrl(sanitized); - if (!sanitized) { - return; - } - isUpdatingCodexBaseUrlRef.current = true; setCodexConfig((prev) => setCodexBaseUrlInConfig(prev, sanitized)); setTimeout(() => { @@ -184,10 +176,6 @@ export function useCodexConfigState({ initialData }: UseCodexConfigStateProps) { const trimmed = modelName.trim(); setCodexModelName(trimmed); - if (!trimmed) { - return; - } - isUpdatingCodexModelNameRef.current = true; setCodexConfig((prev) => setCodexModelNameInConfig(prev, trimmed)); setTimeout(() => { diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 91d736a05..b434a149a 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -775,7 +775,8 @@ "extractFromCurrent": "Extract from Editor", "extractNoCommonConfig": "No common config available to extract from editor", "extractFailed": "Extract failed: {{error}}", - "saveFailed": "Save failed: {{error}}" + "saveFailed": "Save failed: {{error}}", + "modelNameHint": "Specify the model to use, will be auto-updated in config.toml" }, "geminiConfig": { "envFile": "Environment Variables (.env)", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index c995cd0fc..bd765e12c 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -775,7 +775,8 @@ "extractFromCurrent": "編集内容から抽出", "extractNoCommonConfig": "編集内容から抽出できる共通設定がありません", "extractFailed": "抽出に失敗しました: {{error}}", - "saveFailed": "保存に失敗しました: {{error}}" + "saveFailed": "保存に失敗しました: {{error}}", + "modelNameHint": "使用するモデルを指定します。config.toml に自動更新されます" }, "geminiConfig": { "envFile": "環境変数 (.env)", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index c9a9ad8a2..9cb2e0d40 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -775,7 +775,8 @@ "extractFromCurrent": "从编辑内容提取", "extractNoCommonConfig": "当前编辑内容没有可提取的通用配置", "extractFailed": "提取失败: {{error}}", - "saveFailed": "保存失败: {{error}}" + "saveFailed": "保存失败: {{error}}", + "modelNameHint": "指定使用的模型,将自动更新到 config.toml 中" }, "geminiConfig": { "envFile": "环境变量 (.env)", diff --git a/src/utils/providerConfigUtils.ts b/src/utils/providerConfigUtils.ts index fc4b46844..ba685f076 100644 --- a/src/utils/providerConfigUtils.ts +++ b/src/utils/providerConfigUtils.ts @@ -447,12 +447,23 @@ export const setCodexBaseUrl = ( baseUrl: string, ): string => { const trimmed = baseUrl.trim(); - if (!trimmed) { - return configText; - } // 归一化原文本中的引号(既能匹配,也能输出稳定格式) const normalizedText = normalizeQuotes(configText); + // 允许清空:当 baseUrl 为空时,移除 base_url 行 + if (!trimmed) { + if (!normalizedText) return normalizedText; + const next = normalizedText + .split("\n") + .filter((line) => !/^\s*base_url\s*=/.test(line)) + .join("\n") + // 避免移除后留下过多空行 + .replace(/\n{3,}/g, "\n\n") + // 避免开头出现空行 + .replace(/^\n+/, ""); + return next; + } + const normalizedUrl = trimmed.replace(/\s+/g, ""); const replacementLine = `base_url = "${normalizedUrl}"`; const pattern = /base_url\s*=\s*(["'])([^"']+)\1/; @@ -494,13 +505,21 @@ export const setCodexModelName = ( modelName: string, ): string => { const trimmed = modelName.trim(); - if (!trimmed) { - return configText; - } - // 归一化原文本中的引号(既能匹配,也能输出稳定格式) const normalizedText = normalizeQuotes(configText); + // 允许清空:当 modelName 为空时,移除 model 行 + if (!trimmed) { + if (!normalizedText) return normalizedText; + const next = normalizedText + .split("\n") + .filter((line) => !/^\s*model\s*=/.test(line)) + .join("\n") + .replace(/\n{3,}/g, "\n\n") + .replace(/^\n+/, ""); + return next; + } + const replacementLine = `model = "${trimmed}"`; const pattern = /^model\s*=\s*["']([^"']+)["']/m; diff --git a/tests/utils/providerConfigUtils.codex.test.ts b/tests/utils/providerConfigUtils.codex.test.ts new file mode 100644 index 000000000..13dfa2ae7 --- /dev/null +++ b/tests/utils/providerConfigUtils.codex.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, it } from "vitest"; +import { + extractCodexBaseUrl, + extractCodexModelName, + setCodexBaseUrl, + setCodexModelName, +} from "@/utils/providerConfigUtils"; + +describe("Codex TOML utils", () => { + it("removes base_url line when set to empty", () => { + const input = [ + 'model_provider = "openai"', + 'base_url = "https://api.example.com/v1"', + 'model = "gpt-5-codex"', + "", + ].join("\n"); + + const output = setCodexBaseUrl(input, ""); + + expect(output).not.toMatch(/^\s*base_url\s*=/m); + expect(extractCodexBaseUrl(output)).toBeUndefined(); + expect(extractCodexModelName(output)).toBe("gpt-5-codex"); + }); + + it("removes model line when set to empty", () => { + const input = [ + 'model_provider = "openai"', + 'base_url = "https://api.example.com/v1"', + 'model = "gpt-5-codex"', + "", + ].join("\n"); + + const output = setCodexModelName(input, ""); + + expect(output).not.toMatch(/^\s*model\s*=/m); + expect(extractCodexModelName(output)).toBeUndefined(); + expect(extractCodexBaseUrl(output)).toBe("https://api.example.com/v1"); + }); + + it("updates existing values when non-empty", () => { + const input = [ + 'model_provider = "openai"', + "base_url = 'https://old.example/v1'", + 'model = "old-model"', + "", + ].join("\n"); + + const output1 = setCodexBaseUrl(input, " https://new.example/v1 \n"); + expect(extractCodexBaseUrl(output1)).toBe("https://new.example/v1"); + + const output2 = setCodexModelName(output1, " new-model \n"); + expect(extractCodexModelName(output2)).toBe("new-model"); + }); +}); +