From 2d64d8c61965fec6e55d232371e404f92f1b396e Mon Sep 17 00:00:00 2001 From: codeasier <48463012+codeasier@users.noreply.github.com> Date: Thu, 11 Jun 2026 22:21:42 +0800 Subject: [PATCH] fix(proxy): preserve Codex OAuth auth token on takeover (#3789) * fix(proxy): preserve Codex OAuth auth token on takeover * style(proxy): format Codex OAuth takeover fix * fix(proxy): unconditionally inject AUTH_TOKEN placeholder for codex takeover The preserve-if-exists condition left #3784 unfixed on three paths: hot-switch passes the provider's settings (presets carry no ANTHROPIC_AUTH_TOKEN key), fresh installs never had the key, and live configs already stripped by older releases stay stripped. - Fold the bool parameter into the policy enum as ManagedAccount { keep_auth_token } so every construction site declares intent - Decide via !is_github_copilot() within the managed branch so URL-only codex providers (no provider_type meta) are covered, matching the predicate family used for policy selection - Inject the placeholder unconditionally instead of only when the key pre-exists; Copilot behavior is unchanged (API_KEY only) - Pin the previously uncovered cases with tests: codex without a pre-existing key, URL-only codex, and Copilot removing a stale AUTH_TOKEN --------- Co-authored-by: codeasier Co-authored-by: Jason --- src-tauri/src/services/proxy.rs | 121 +++++++++++++++++++++++++++++++- 1 file changed, 118 insertions(+), 3 deletions(-) diff --git a/src-tauri/src/services/proxy.rs b/src-tauri/src/services/proxy.rs index 3423eed0b..d77c7169e 100644 --- a/src-tauri/src/services/proxy.rs +++ b/src-tauri/src/services/proxy.rs @@ -48,7 +48,7 @@ const CLAUDE_ONE_M_MARKER_FOR_CLIENT: &str = "[1M]"; #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum ClaudeTakeoverAuthPolicy { PreserveExistingOrAuthToken, - ManagedAccount, + ManagedAccount { keep_auth_token: bool }, } #[derive(Clone)] @@ -90,7 +90,12 @@ impl ProxyService { provider: &Provider, ) { let auth_policy = if provider.uses_managed_account_auth() { - ClaudeTakeoverAuthPolicy::ManagedAccount + // Codex 系(含仅凭 base_url 识别、无 provider_type meta 的)必须保留 + // ANTHROPIC_AUTH_TOKEN 占位符:Claude Code 缺该键会弹登录提示(#3784)。 + // Copilot 维持仅 API_KEY 占位,避免与 /login 管理的 key 冲突(#1049)。 + ClaudeTakeoverAuthPolicy::ManagedAccount { + keep_auth_token: !provider.is_github_copilot(), + } } else { ClaudeTakeoverAuthPolicy::PreserveExistingOrAuthToken }; @@ -180,7 +185,7 @@ impl ProxyService { ); } } - ClaudeTakeoverAuthPolicy::ManagedAccount => { + ClaudeTakeoverAuthPolicy::ManagedAccount { keep_auth_token } => { for key in token_keys { env.remove(key); } @@ -188,6 +193,14 @@ impl ProxyService { "ANTHROPIC_API_KEY".to_string(), json!(PROXY_TOKEN_PLACEHOLDER), ); + if keep_auth_token { + // 无条件注入而非"已存在才保留":热切换路径传入的是 provider + // settings(预设不含该键),且旧版接管已把存量用户 live 中的键删光。 + env.insert( + "ANTHROPIC_AUTH_TOKEN".to_string(), + json!(PROXY_TOKEN_PLACEHOLDER), + ); + } } } } @@ -2945,6 +2958,108 @@ mod tests { assert_env_str(env, "ANTHROPIC_DEFAULT_OPUS_MODEL", Some("claude-opus-4-8")); assert_env_str(env, "ANTHROPIC_DEFAULT_OPUS_MODEL_NAME", Some("gpt-5.4")); assert_env_str(env, "ANTHROPIC_API_KEY", Some(PROXY_TOKEN_PLACEHOLDER)); + assert_env_str(env, "ANTHROPIC_AUTH_TOKEN", Some(PROXY_TOKEN_PLACEHOLDER)); + } + + #[test] + fn managed_account_claude_takeover_codex_injects_auth_token_without_preexisting_key() { + let mut provider = Provider::with_id( + "codex".to_string(), + "Codex".to_string(), + json!({ + "env": { + "ANTHROPIC_BASE_URL": "https://chatgpt.com/backend-api/codex" + } + }), + None, + ); + provider.meta = Some(ProviderMeta { + provider_type: Some("codex_oauth".to_string()), + ..Default::default() + }); + + // 全新安装/热切换形态:传入的 env 没有任何 token 键。 + let mut live_config = provider.settings_config.clone(); + ProxyService::apply_claude_takeover_fields_for_provider( + &mut live_config, + "http://127.0.0.1:15721", + &provider, + ); + + let env = live_config + .get("env") + .and_then(|value| value.as_object()) + .expect("env should exist"); + assert_env_str(env, "ANTHROPIC_API_KEY", Some(PROXY_TOKEN_PLACEHOLDER)); + assert_env_str(env, "ANTHROPIC_AUTH_TOKEN", Some(PROXY_TOKEN_PLACEHOLDER)); + } + + #[test] + fn managed_account_claude_takeover_codex_by_base_url_keeps_auth_token() { + // 无 provider_type meta、仅凭 base_url 识别为受管 codex 的供应商, + // 也必须保留 AUTH_TOKEN 占位符(与策略选择共用同一判定族)。 + let provider = Provider::with_id( + "codex-url-only".to_string(), + "Codex (URL only)".to_string(), + json!({ + "env": { + "ANTHROPIC_BASE_URL": "https://chatgpt.com/backend-api/codex" + } + }), + None, + ); + assert!(provider.uses_managed_account_auth()); + assert!(!provider.is_codex_oauth()); + + let mut live_config = provider.settings_config.clone(); + ProxyService::apply_claude_takeover_fields_for_provider( + &mut live_config, + "http://127.0.0.1:15721", + &provider, + ); + + let env = live_config + .get("env") + .and_then(|value| value.as_object()) + .expect("env should exist"); + assert_env_str(env, "ANTHROPIC_API_KEY", Some(PROXY_TOKEN_PLACEHOLDER)); + assert_env_str(env, "ANTHROPIC_AUTH_TOKEN", Some(PROXY_TOKEN_PLACEHOLDER)); + } + + #[test] + fn managed_account_claude_takeover_copilot_removes_stale_auth_token() { + let mut provider = Provider::with_id( + "copilot".to_string(), + "GitHub Copilot".to_string(), + json!({ + "env": { + "ANTHROPIC_BASE_URL": "https://api.githubcopilot.com" + } + }), + None, + ); + provider.meta = Some(ProviderMeta { + provider_type: Some("github_copilot".to_string()), + ..Default::default() + }); + + let mut live_config = json!({ + "env": { + "ANTHROPIC_BASE_URL": "https://stale.example.com", + "ANTHROPIC_AUTH_TOKEN": "stale-token" + } + }); + ProxyService::apply_claude_takeover_fields_for_provider( + &mut live_config, + "http://127.0.0.1:15721", + &provider, + ); + + let env = live_config + .get("env") + .and_then(|value| value.as_object()) + .expect("env should exist"); + assert_env_str(env, "ANTHROPIC_API_KEY", Some(PROXY_TOKEN_PLACEHOLDER)); assert_env_str(env, "ANTHROPIC_AUTH_TOKEN", None); }