From 2683af57cb64e64d6d47875303aa950deee56041 Mon Sep 17 00:00:00 2001 From: Jason Date: Sat, 30 May 2026 14:13:58 +0800 Subject: [PATCH] Add Codex auth preservation setting --- src-tauri/src/codex_config.rs | 10 ++- src-tauri/src/services/proxy.rs | 90 +++++++++++++++++++ src-tauri/src/settings.rs | 14 +++ src/components/settings/CodexAuthSettings.tsx | 35 ++++++++ src/components/settings/SettingsPage.tsx | 5 ++ src/hooks/useSettingsForm.ts | 5 ++ 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 + src/lib/schemas/settings.ts | 1 + src/types.ts | 2 + 12 files changed, 171 insertions(+), 3 deletions(-) create mode 100644 src/components/settings/CodexAuthSettings.tsx diff --git a/src-tauri/src/codex_config.rs b/src-tauri/src/codex_config.rs index ea06e7cfe..20ecc1606 100644 --- a/src-tauri/src/codex_config.rs +++ b/src-tauri/src/codex_config.rs @@ -788,9 +788,9 @@ pub fn read_codex_live_settings() -> Result { /// Route a Codex live write between full auth+config or config-only. /// -/// Official providers with usable login material own `auth.json`; everyone -/// else only touches `config.toml` so the user's ChatGPT login cache survives -/// third-party switches. +/// Official providers with usable login material own `auth.json`. Third-party +/// providers only touch `config.toml` when the compatibility setting is enabled +/// so the user's ChatGPT login cache survives provider switches. pub fn write_codex_live_for_provider( category: Option<&str>, auth: &Value, @@ -798,6 +798,10 @@ pub fn write_codex_live_for_provider( ) -> Result<(), AppError> { if category == Some("official") && codex_auth_has_login_material(auth) { write_codex_live_atomic(auth, config_text) + } else if category != Some("official") + && !crate::settings::preserve_codex_official_auth_on_switch() + { + write_codex_live_atomic(auth, config_text) } else { let live_config = prepare_codex_provider_live_config(auth, config_text.unwrap_or(""))?; write_codex_live_config_atomic(Some(&live_config)) diff --git a/src-tauri/src/services/proxy.rs b/src-tauri/src/services/proxy.rs index b7286179f..cefa5b107 100644 --- a/src-tauri/src/services/proxy.rs +++ b/src-tauri/src/services/proxy.rs @@ -2637,6 +2637,96 @@ wire_api = "responses" ); } + #[test] + #[serial] + fn codex_custom_provider_live_write_can_overwrite_auth_when_preserve_disabled() { + let _home = TempHome::new(); + crate::settings::reload_settings().expect("reload settings"); + crate::settings::update_settings(crate::settings::AppSettings { + preserve_codex_official_auth_on_switch: false, + ..Default::default() + }) + .expect("disable Codex official auth preservation"); + + let db = Arc::new(Database::memory().expect("init db")); + let service = ProxyService::new(db); + let oauth_auth = json!({ + "auth_mode": "chatgpt", + "tokens": { + "id_token": "oauth-id", + "access_token": "oauth-access" + } + }); + crate::codex_config::write_codex_live_atomic( + &oauth_auth, + Some( + r#"model_provider = "openai" +model = "gpt-5-codex" +"#, + ), + ) + .expect("seed live OAuth auth"); + + let mut provider = Provider::with_id( + "rightcode".to_string(), + "RightCode".to_string(), + json!({ + "auth": { + "OPENAI_API_KEY": "rightcode-key" + }, + "config": r#"model_provider = "rightcode" +model = "gpt-5-codex" + +[model_providers.rightcode] +name = "RightCode" +base_url = "https://rightcode.example/v1" +wire_api = "responses" +"# + }), + None, + ); + provider.category = Some("custom".to_string()); + let takeover_auth = json!({ + "OPENAI_API_KEY": PROXY_TOKEN_PLACEHOLDER + }); + let takeover_settings = json!({ + "auth": takeover_auth, + "config": r#"model_provider = "rightcode" +model = "gpt-5-codex" + +[model_providers.rightcode] +name = "RightCode" +base_url = "http://127.0.0.1:15721/v1" +wire_api = "responses" +"# + }); + + service + .write_codex_live_for_provider(&takeover_settings, Some(&provider)) + .expect("write provider-driven Codex live config"); + + let live_auth: Value = + crate::config::read_json_file(&crate::codex_config::get_codex_auth_path()) + .expect("read live auth"); + assert_eq!( + live_auth, + json!({ + "OPENAI_API_KEY": PROXY_TOKEN_PLACEHOLDER + }), + "disabled preservation should let third-party switches overwrite auth.json" + ); + + let live_config = std::fs::read_to_string(crate::codex_config::get_codex_config_path()) + .expect("read live config"); + assert!( + !live_config.contains("experimental_bearer_token"), + "provider token should stay in auth.json when preservation is disabled" + ); + + crate::settings::update_settings(crate::settings::AppSettings::default()) + .expect("reset settings"); + } + #[test] fn update_toml_base_url_updates_active_model_provider_base_url() { let input = r#" diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs index fa853031e..4f2a90945 100644 --- a/src-tauri/src/settings.rs +++ b/src-tauri/src/settings.rs @@ -253,6 +253,9 @@ pub struct AppSettings { /// Whether to show the failover toggle independently on the main page #[serde(default)] pub enable_failover_toggle: bool, + /// Keep Codex ChatGPT login material in auth.json when switching to third-party providers + #[serde(default = "default_true")] + pub preserve_codex_official_auth_on_switch: bool, /// User has confirmed the failover toggle first-run notice #[serde(default, skip_serializing_if = "Option::is_none")] pub failover_confirmed: Option, @@ -366,6 +369,7 @@ impl Default for AppSettings { usage_confirmed: None, stream_check_confirmed: None, enable_failover_toggle: false, + preserve_codex_official_auth_on_switch: true, failover_confirmed: None, first_run_notice_confirmed: None, common_config_confirmed: None, @@ -699,6 +703,16 @@ pub fn get_hermes_override_dir() -> Option { .map(|p| resolve_override_path(p)) } +pub fn preserve_codex_official_auth_on_switch() -> bool { + settings_store() + .read() + .unwrap_or_else(|e| { + log::warn!("设置锁已毒化,使用恢复值: {e}"); + e.into_inner() + }) + .preserve_codex_official_auth_on_switch +} + // ===== 当前供应商管理函数 ===== /// 获取指定应用类型的当前供应商 ID(从本地 settings 读取) diff --git a/src/components/settings/CodexAuthSettings.tsx b/src/components/settings/CodexAuthSettings.tsx new file mode 100644 index 000000000..047fd7318 --- /dev/null +++ b/src/components/settings/CodexAuthSettings.tsx @@ -0,0 +1,35 @@ +import { useTranslation } from "react-i18next"; +import { KeyRound } from "lucide-react"; +import type { SettingsFormState } from "@/hooks/useSettings"; +import { ToggleRow } from "@/components/ui/toggle-row"; + +interface CodexAuthSettingsProps { + settings: SettingsFormState; + onChange: (updates: Partial) => void; +} + +export function CodexAuthSettings({ + settings, + onChange, +}: CodexAuthSettingsProps) { + const { t } = useTranslation(); + + return ( +
+
+ +

{t("settings.codexAuth")}

+
+ + } + title={t("settings.preserveCodexOfficialAuthOnSwitch")} + description={t("settings.preserveCodexOfficialAuthOnSwitchDescription")} + checked={settings.preserveCodexOfficialAuthOnSwitch ?? true} + onCheckedChange={(value) => + onChange({ preserveCodexOfficialAuthOnSwitch: value }) + } + /> +
+ ); +} diff --git a/src/components/settings/SettingsPage.tsx b/src/components/settings/SettingsPage.tsx index 5e48fbaed..bfc69d316 100644 --- a/src/components/settings/SettingsPage.tsx +++ b/src/components/settings/SettingsPage.tsx @@ -44,6 +44,7 @@ import { ModelTestConfigPanel } from "@/components/usage/ModelTestConfigPanel"; import { UsageDashboard } from "@/components/usage/UsageDashboard"; import { LogConfigPanel } from "@/components/settings/LogConfigPanel"; import { AuthCenterPanel } from "@/components/settings/AuthCenterPanel"; +import { CodexAuthSettings } from "@/components/settings/CodexAuthSettings"; import { useInstalledSkills } from "@/hooks/useSkills"; import { useSettings } from "@/hooks/useSettings"; import { useImportExport } from "@/hooks/useImportExport"; @@ -245,6 +246,10 @@ export function SettingsPage({ settings={settings} onChange={handleAutoSave} /> + diff --git a/src/hooks/useSettingsForm.ts b/src/hooks/useSettingsForm.ts index de625ffc1..3a73fd9f2 100644 --- a/src/hooks/useSettingsForm.ts +++ b/src/hooks/useSettingsForm.ts @@ -116,6 +116,8 @@ export function useSettingsForm(): UseSettingsFormResult { data.enableClaudePluginIntegration ?? false, silentStartup: data.silentStartup ?? false, skipClaudeOnboarding: data.skipClaudeOnboarding ?? false, + preserveCodexOfficialAuthOnSwitch: + data.preserveCodexOfficialAuthOnSwitch ?? true, claudeConfigDir: sanitizeDir(data.claudeConfigDir), codexConfigDir: sanitizeDir(data.codexConfigDir), geminiConfigDir: sanitizeDir(data.geminiConfigDir), @@ -140,6 +142,7 @@ export function useSettingsForm(): UseSettingsFormResult { useAppWindowControls: false, enableClaudePluginIntegration: false, skipClaudeOnboarding: false, + preserveCodexOfficialAuthOnSwitch: true, language: readPersistedLanguage(), } as SettingsFormState); @@ -177,6 +180,8 @@ export function useSettingsForm(): UseSettingsFormResult { serverData.enableClaudePluginIntegration ?? false, silentStartup: serverData.silentStartup ?? false, skipClaudeOnboarding: serverData.skipClaudeOnboarding ?? false, + preserveCodexOfficialAuthOnSwitch: + serverData.preserveCodexOfficialAuthOnSwitch ?? true, claudeConfigDir: sanitizeDir(serverData.claudeConfigDir), codexConfigDir: sanitizeDir(serverData.codexConfigDir), geminiConfigDir: sanitizeDir(serverData.geminiConfigDir), diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 1ea3d07f6..8aac25c91 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -561,6 +561,9 @@ "enableClaudePluginIntegrationDescription": "When enabled, the VS Code Claude Code extension provider will switch with this app", "skipClaudeOnboarding": "Skip Claude Code first-run confirmation", "skipClaudeOnboardingDescription": "When enabled, Claude Code will skip the first-run confirmation", + "codexAuth": "Codex Authentication", + "preserveCodexOfficialAuthOnSwitch": "Keep official login when switching third-party providers", + "preserveCodexOfficialAuthOnSwitchDescription": "When enabled, third-party Codex switches update only config.toml and keep the ChatGPT login in auth.json. When disabled, auth.json is overwritten with the active provider auth.", "appVisibility": { "title": "Homepage Display", "description": "Choose which apps to show on the homepage", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 3608ddcb1..1d156e347 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -561,6 +561,9 @@ "enableClaudePluginIntegrationDescription": "オンにすると VS Code の Claude Code 拡張のプロバイダーも同期します", "skipClaudeOnboarding": "Claude Code の初回確認をスキップ", "skipClaudeOnboardingDescription": "オンにすると Claude Code の初回インストール確認をスキップします", + "codexAuth": "Codex 認証", + "preserveCodexOfficialAuthOnSwitch": "サードパーティ切替時に公式ログインを保持", + "preserveCodexOfficialAuthOnSwitchDescription": "オンにするとサードパーティ Codex への切替では config.toml のみ更新し、auth.json の ChatGPT ログインを保持します。オフにすると auth.json を現在のプロバイダー認証で上書きします。", "appVisibility": { "title": "ホームページ表示", "description": "ホームページに表示するアプリを選択", diff --git a/src/i18n/locales/zh-TW.json b/src/i18n/locales/zh-TW.json index 2c1f928e6..178721b1d 100644 --- a/src/i18n/locales/zh-TW.json +++ b/src/i18n/locales/zh-TW.json @@ -561,6 +561,9 @@ "enableClaudePluginIntegrationDescription": "開啟後 Vscode Claude Code 外掛程式的供應商將隨本軟體切換", "skipClaudeOnboarding": "跳過 Claude Code 初次安裝確認", "skipClaudeOnboardingDescription": "開啟後跳過 Claude Code 初次安裝確認", + "codexAuth": "Codex 認證", + "preserveCodexOfficialAuthOnSwitch": "切換第三方時保留官方登入", + "preserveCodexOfficialAuthOnSwitchDescription": "開啟後切換第三方 Codex 供應商只更新 config.toml,並保留 auth.json 中的 ChatGPT 登入狀態;關閉後會用目前供應商 auth 覆寫 auth.json。", "appVisibility": { "title": "主頁面顯示", "description": "選擇在主頁面顯示的應用程式", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index d7c114b5f..70096cadc 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -561,6 +561,9 @@ "enableClaudePluginIntegrationDescription": "开启后 Vscode Claude Code 插件的供应商将随本软件切换", "skipClaudeOnboarding": "跳过 Claude Code 初次安装确认", "skipClaudeOnboardingDescription": "开启后跳过 Claude Code 初次安装确认", + "codexAuth": "Codex 认证", + "preserveCodexOfficialAuthOnSwitch": "切换第三方时保留官方登录", + "preserveCodexOfficialAuthOnSwitchDescription": "开启后切换第三方 Codex 供应商只更新 config.toml,并保留 auth.json 中的 ChatGPT 登录态;关闭后会用当前供应商 auth 覆盖 auth.json。", "appVisibility": { "title": "主页面显示", "description": "选择在主页面显示的应用", diff --git a/src/lib/schemas/settings.ts b/src/lib/schemas/settings.ts index 774d3eced..33ee0eb66 100644 --- a/src/lib/schemas/settings.ts +++ b/src/lib/schemas/settings.ts @@ -15,6 +15,7 @@ export const settingsSchema = z.object({ skipClaudeOnboarding: z.boolean().optional(), launchOnStartup: z.boolean().optional(), enableLocalProxy: z.boolean().optional(), + preserveCodexOfficialAuthOnSwitch: z.boolean().optional(), language: z.enum(["en", "zh", "zh-TW", "ja"]).optional(), // 设备级目录覆盖 diff --git a/src/types.ts b/src/types.ts index 9b943beca..6d75b402d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -333,6 +333,8 @@ export interface Settings { streamCheckConfirmed?: boolean; // Whether to show the failover toggle independently on the main page enableFailoverToggle?: boolean; + // Preserve Codex ChatGPT login in auth.json when switching third-party providers + preserveCodexOfficialAuthOnSwitch?: boolean; // User has confirmed the failover toggle first-run notice failoverConfirmed?: boolean; // User has confirmed the first-run welcome notice