From a04e72a2674bbf8066705801f8778fdee1a4b8f3 Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 31 May 2026 21:00:50 +0800 Subject: [PATCH] Fix Codex edit dialog masking live OAuth during proxy takeover The reported "OAuth access token disappears when enabling Codex proxy takeover" was a display artifact, not data loss: auth.json on disk kept the OAuth login the whole time. During takeover the edit dialog falls back to the stored provider config (so it does not surface the proxy placeholder), which for a third-party provider shows that provider's own key instead of the live auth.json, making the OAuth token look gone. Thread an isProxyTakeover flag from App through ProviderForm into the Codex editor and show an explicit notice plus storage-aware auth/config hints clarifying that the form displays the stored provider config while the live config is temporarily managed by the proxy. Drop the proxy-running condition so the notice shows whenever takeover is active, even with the proxy stopped. Add a regression test asserting the dialog does not read live settings during takeover and renders the database config. i18n synced across en/zh/ja/zh-TW. --- src/App.tsx | 2 +- .../providers/EditProviderDialog.tsx | 1 + .../providers/forms/CodexConfigEditor.tsx | 18 +++++++ .../providers/forms/CodexConfigSections.tsx | 16 +++++- .../providers/forms/ProviderForm.tsx | 3 ++ src/i18n/locales/en.json | 3 ++ src/i18n/locales/ja.json | 3 ++ src/i18n/locales/zh-TW.json | 3 ++ src/i18n/locales/zh.json | 3 ++ tests/components/EditProviderDialog.test.tsx | 49 +++++++++++++++++++ 10 files changed, 98 insertions(+), 3 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index d1497ffce..33aebf00e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1533,7 +1533,7 @@ function App() { }} onSubmit={handleEditProvider} appId={activeApp} - isProxyTakeover={isProxyRunning && isCurrentAppTakeoverActive} + isProxyTakeover={isCurrentAppTakeoverActive} /> {effectiveUsageProvider && ( diff --git a/src/components/providers/EditProviderDialog.tsx b/src/components/providers/EditProviderDialog.tsx index 2536eb9d6..688e2db6f 100644 --- a/src/components/providers/EditProviderDialog.tsx +++ b/src/components/providers/EditProviderDialog.tsx @@ -247,6 +247,7 @@ export function EditProviderDialog({ onSubmittingChange={setIsFormSubmitting} initialData={initialData} showButtons={false} + isProxyTakeover={isProxyTakeover} /> ); diff --git a/src/components/providers/forms/CodexConfigEditor.tsx b/src/components/providers/forms/CodexConfigEditor.tsx index a7a375292..8e23ae32b 100644 --- a/src/components/providers/forms/CodexConfigEditor.tsx +++ b/src/components/providers/forms/CodexConfigEditor.tsx @@ -1,6 +1,9 @@ import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Info } from "lucide-react"; import { CodexAuthSection, CodexConfigSection } from "./CodexConfigSections"; import { CodexCommonConfigModal } from "./CodexCommonConfigModal"; +import { Alert, AlertDescription } from "@/components/ui/alert"; interface CodexConfigEditorProps { authValue: string; @@ -11,6 +14,8 @@ interface CodexConfigEditorProps { showRemoteCompaction?: boolean; + isProxyTakeover?: boolean; + onAuthChange: (value: string) => void; onConfigChange: (value: string) => void; @@ -43,6 +48,7 @@ const CodexConfigEditor: React.FC = ({ configValue, providerName, showRemoteCompaction, + isProxyTakeover = false, onAuthChange, onConfigChange, onAuthBlur, @@ -57,6 +63,7 @@ const CodexConfigEditor: React.FC = ({ onExtract, isExtracting, }) => { + const { t } = useTranslation(); const [isCommonConfigModalOpen, setIsCommonConfigModalOpen] = useState(false); const handleCloseCommonConfigModal = () => { @@ -66,12 +73,22 @@ const CodexConfigEditor: React.FC = ({ return (
+ {isProxyTakeover && ( + + + + {t("codexConfig.proxyTakeoverStorageNotice")} + + + )} + {/* Auth JSON Section */} {/* Config TOML Section */} @@ -85,6 +102,7 @@ const CodexConfigEditor: React.FC = ({ onEditCommonConfig={() => setIsCommonConfigModalOpen(true)} commonConfigError={commonConfigError} configError={configError} + isProxyTakeover={isProxyTakeover} /> {/* Common Config Modal */} diff --git a/src/components/providers/forms/CodexConfigSections.tsx b/src/components/providers/forms/CodexConfigSections.tsx index ddfeada52..275eaa106 100644 --- a/src/components/providers/forms/CodexConfigSections.tsx +++ b/src/components/providers/forms/CodexConfigSections.tsx @@ -29,6 +29,7 @@ interface CodexAuthSectionProps { onChange: (value: string) => void; onBlur?: () => void; error?: string; + isProxyTakeover?: boolean; } /** @@ -39,6 +40,7 @@ export const CodexAuthSection: React.FC = ({ onChange, onBlur, error, + isProxyTakeover = false, }) => { const { t } = useTranslation(); const [isDarkMode, setIsDarkMode] = useState(false); @@ -90,7 +92,11 @@ export const CodexAuthSection: React.FC = ({ {!error && (

- {t("codexConfig.authJsonHint")} + {t( + isProxyTakeover + ? "codexConfig.authJsonStorageHint" + : "codexConfig.authJsonHint", + )}

)}
@@ -107,6 +113,7 @@ interface CodexConfigSectionProps { onEditCommonConfig: () => void; commonConfigError?: string; configError?: string; + isProxyTakeover?: boolean; } /** @@ -122,6 +129,7 @@ export const CodexConfigSection: React.FC = ({ onEditCommonConfig, commonConfigError, configError, + isProxyTakeover = false, }) => { const { t } = useTranslation(); const [isDarkMode, setIsDarkMode] = useState(false); @@ -367,7 +375,11 @@ export const CodexConfigSection: React.FC = ({ {!configError && (

- {t("codexConfig.configTomlHint")} + {t( + isProxyTakeover + ? "codexConfig.configTomlStorageHint" + : "codexConfig.configTomlHint", + )}

)} diff --git a/src/components/providers/forms/ProviderForm.tsx b/src/components/providers/forms/ProviderForm.tsx index c22572fdb..191b067ee 100644 --- a/src/components/providers/forms/ProviderForm.tsx +++ b/src/components/providers/forms/ProviderForm.tsx @@ -230,6 +230,7 @@ export interface ProviderFormProps { iconColor?: string; }; showButtons?: boolean; + isProxyTakeover?: boolean; } export function ProviderForm(props: ProviderFormProps) { @@ -251,6 +252,7 @@ function ProviderFormFull({ onSubmittingChange, initialData, showButtons = true, + isProxyTakeover = false, }: ProviderFormProps) { if (appId === "claude-desktop") { throw new Error("ProviderFormFull should not receive claude-desktop"); @@ -2165,6 +2167,7 @@ function ProviderFormFull({ configValue={codexConfig} providerName={form.watch("name")} showRemoteCompaction={category !== "official"} + isProxyTakeover={isProxyTakeover} onAuthChange={setCodexAuth} onConfigChange={handleCodexConfigChange} useCommonConfig={useCodexCommonConfigFlag} diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index f4499ce70..ec1e3d3b1 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -1104,8 +1104,11 @@ "authJson": "auth.json (JSON) *", "authJsonPlaceholder": "{\n \"OPENAI_API_KEY\": \"sk-your-api-key-here\"\n}", "authJsonHint": "Codex auth.json configuration content", + "authJsonStorageHint": "Proxy takeover is showing provider-stored auth, not the live ~/.codex/auth.json; OAuth access tokens remain in the live auth.json.", "configToml": "config.toml (TOML)", "configTomlHint": "Codex config.toml configuration content", + "configTomlStorageHint": "Proxy takeover is showing the provider upstream config; the live config.toml is temporarily managed with the local proxy URL and PROXY_MANAGED.", + "proxyTakeoverStorageNotice": "This app is under proxy takeover. You are editing the provider's stored config; the live config is temporarily managed by the proxy, so this form does not need to show 127.0.0.1 or PROXY_MANAGED.", "writeCommonConfig": "Write Common Config", "editCommonConfig": "Edit Common Config", "editCommonConfigTitle": "Edit Codex Common Config Snippet", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 92cbc4f6d..d73694696 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -1104,8 +1104,11 @@ "authJson": "auth.json (JSON) *", "authJsonPlaceholder": "{\n \"OPENAI_API_KEY\": \"sk-your-api-key-here\"\n}", "authJsonHint": "Codex の auth.json 設定内容", + "authJsonStorageHint": "プロキシ引き継ぎ中は、live の ~/.codex/auth.json ではなくプロバイダー保存 auth を表示しています。OAuth アクセストークンは live auth.json に保持されます。", "configToml": "config.toml (TOML)", "configTomlHint": "Codex の config.toml 設定内容", + "configTomlStorageHint": "プロキシ引き継ぎ中はプロバイダーの upstream config を表示しています。live config.toml は一時的にローカルプロキシ URL と PROXY_MANAGED で管理されます。", + "proxyTakeoverStorageNotice": "このアプリはプロキシ引き継ぎ中です。ここで編集しているのはプロバイダーの保存設定で、実際の live 設定は一時的にプロキシが管理します。このフォームに 127.0.0.1 や PROXY_MANAGED が表示される必要はありません。", "writeCommonConfig": "共通設定を書き込む", "editCommonConfig": "共通設定を編集", "editCommonConfigTitle": "Codex 共通設定スニペットを編集", diff --git a/src/i18n/locales/zh-TW.json b/src/i18n/locales/zh-TW.json index 7d2410c38..071f30724 100644 --- a/src/i18n/locales/zh-TW.json +++ b/src/i18n/locales/zh-TW.json @@ -1076,8 +1076,11 @@ "authJson": "auth.json (JSON) *", "authJsonPlaceholder": "{\n \"OPENAI_API_KEY\": \"sk-your-api-key-here\"\n}", "authJsonHint": "Codex auth.json 設定內容", + "authJsonStorageHint": "代理接管中顯示的是供應商儲存 auth,不是即時 ~/.codex/auth.json;OAuth Access Token 會保留在 live auth.json 中。", "configToml": "config.toml (TOML)", "configTomlHint": "Codex config.toml 設定內容", + "configTomlStorageHint": "代理接管中顯示的是供應商上游 config;即時 config.toml 會由代理暫時寫為本機位址和 PROXY_MANAGED。", + "proxyTakeoverStorageNotice": "目前應用處於代理接管狀態。這裡編輯的是供應商儲存配置,真實 live 配置會由代理暫時接管;儲存不需要這裡顯示 127.0.0.1 或 PROXY_MANAGED。", "writeCommonConfig": "寫入通用設定", "editCommonConfig": "編輯通用設定", "editCommonConfigTitle": "編輯 Codex 通用設定片段", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index 0afb08854..b38ec6a43 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -1104,8 +1104,11 @@ "authJson": "auth.json (JSON) *", "authJsonPlaceholder": "{\n \"OPENAI_API_KEY\": \"sk-your-api-key-here\"\n}", "authJsonHint": "Codex auth.json 配置内容", + "authJsonStorageHint": "代理接管中显示的是供应商存储 auth,不是实时 ~/.codex/auth.json;OAuth Access Token 会保留在 live auth.json 中。", "configToml": "config.toml (TOML)", "configTomlHint": "Codex config.toml 配置内容", + "configTomlStorageHint": "代理接管中显示的是供应商上游 config;实时 config.toml 会由代理临时写为本地地址和 PROXY_MANAGED。", + "proxyTakeoverStorageNotice": "当前应用处于代理接管状态。这里编辑的是供应商存储配置,真实 live 配置会由代理临时接管;保存不会要求这里显示 127.0.0.1 或 PROXY_MANAGED。", "writeCommonConfig": "写入通用配置", "editCommonConfig": "编辑通用配置", "editCommonConfigTitle": "编辑 Codex 通用配置片段", diff --git a/tests/components/EditProviderDialog.test.tsx b/tests/components/EditProviderDialog.test.tsx index b4b35a29d..43a5fcb58 100644 --- a/tests/components/EditProviderDialog.test.tsx +++ b/tests/components/EditProviderDialog.test.tsx @@ -42,6 +42,7 @@ vi.mock("@/components/providers/forms/ProviderForm", () => ({ ProviderForm: ({ initialData, onSubmit, + isProxyTakeover, }: { initialData: { name?: string; @@ -61,6 +62,7 @@ vi.mock("@/components/providers/forms/ProviderForm", () => ({ icon?: string; iconColor?: string; }) => void; + isProxyTakeover?: boolean; }) => (
({ {JSON.stringify(initialData.settingsConfig ?? {})} + + {isProxyTakeover ? "true" : "false"} +
), })); @@ -153,4 +158,48 @@ describe("EditProviderDialog", () => { modelCatalog: dbModelCatalog, }); }); + + it("代理接管中编辑 Codex 供应商时展示数据库配置而不是读取 live 代理配置", async () => { + const provider: Provider = { + id: "deepseek", + name: "DeepSeek", + category: "custom", + settingsConfig: { + auth: { + OPENAI_API_KEY: "db-key", + }, + config: + 'model_provider = "custom"\n[model_providers.custom]\nbase_url = "https://api.deepseek.com/v1"\n', + }, + }; + + apiMocks.getCurrent.mockResolvedValue(provider.id); + apiMocks.getLiveProviderSettings.mockResolvedValue({ + auth: { + OPENAI_API_KEY: "PROXY_MANAGED", + }, + config: + 'model_provider = "custom"\n[model_providers.custom]\nbase_url = "http://127.0.0.1:15721/v1"\nexperimental_bearer_token = "PROXY_MANAGED"\n', + }); + + render( + , + ); + + await waitFor(() => { + expect(screen.getByTestId("is-proxy-takeover").textContent).toBe("true"); + }); + + expect(apiMocks.getLiveProviderSettings).not.toHaveBeenCalled(); + expect( + JSON.parse(screen.getByTestId("settings-config").textContent ?? "{}"), + ).toEqual(provider.settingsConfig); + }); });