From 0ff20ed78dc88e6f914641e527cc4f5928540ff8 Mon Sep 17 00:00:00 2001 From: lichengzhe Date: Mon, 23 Feb 2026 01:01:19 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=A0=B9=E6=8D=AE=20token=20=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E9=80=89=E6=8B=A9=E6=AD=A3=E7=A1=AE=E7=9A=84=20Claude?= =?UTF-8?q?=20=E8=AE=A4=E8=AF=81=E7=8E=AF=E5=A2=83=E5=8F=98=E9=87=8F=20(#3?= =?UTF-8?q?2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题: cc-switch 将所有 Claude API 凭证统一存为 ANTHROPIC_AUTH_TOKEN, 但 Claude Code 对两种认证方式使用不同的 HTTP header: - ANTHROPIC_AUTH_TOKEN → Authorization: Bearer(OAuth token) - ANTHROPIC_API_KEY → x-api-key(标准 API key) 当用户使用标准 API key(sk-ant-api...)时,被错误地存为 ANTHROPIC_AUTH_TOKEN,Claude Code 会将其作为 Bearer token 发送, 导致服务端返回 401。 修复: - 新增 claude_auth_env_keys() 根据 token 前缀自动选择 env var: sk-ant-oat... → ANTHROPIC_AUTH_TOKEN(OAuth) 其他 → ANTHROPIC_API_KEY(标准 API key) - 新增 get_claude_token_from_env() 统一读取逻辑,兼容两种 key - 更新所有写入端(TUI form / CLI interactive / deeplink import) - 更新所有读取端使用统一 helper,兼容已有数据 Closes #32 Co-Authored-By: Claude Opus 4.6 --- src-tauri/src/cli/commands/provider.rs | 5 +---- src-tauri/src/cli/commands/provider_input.rs | 11 ++++++----- src-tauri/src/cli/interactive/provider.rs | 5 +---- src-tauri/src/cli/tui/form.rs | 12 +++++++----- src-tauri/src/cli/tui/ui.rs | 5 +---- src-tauri/src/deeplink/provider.rs | 9 ++++----- src-tauri/src/services/provider/mod.rs | 19 +++++++++++++++++++ src-tauri/src/services/provider/usage.rs | 4 +--- src-tauri/tests/deeplink_import.rs | 2 +- 9 files changed, 41 insertions(+), 31 deletions(-) diff --git a/src-tauri/src/cli/commands/provider.rs b/src-tauri/src/cli/commands/provider.rs index 06448f0..c4df6da 100644 --- a/src-tauri/src/cli/commands/provider.rs +++ b/src-tauri/src/cli/commands/provider.rs @@ -604,10 +604,7 @@ fn extract_claude_config(settings_config: &serde_json::Value) -> ClaudeConfig { if let Some(env) = env { ClaudeConfig { - api_key: env - .get("ANTHROPIC_AUTH_TOKEN") - .or_else(|| env.get("ANTHROPIC_API_KEY")) - .and_then(|v| v.as_str()) + api_key: crate::services::provider::get_claude_token_from_env(env) .map(|s| mask_api_key(s)), base_url: env .get("ANTHROPIC_BASE_URL") diff --git a/src-tauri/src/cli/commands/provider_input.rs b/src-tauri/src/cli/commands/provider_input.rs index a6e39ce..5e787c8 100644 --- a/src-tauri/src/cli/commands/provider_input.rs +++ b/src-tauri/src/cli/commands/provider_input.rs @@ -324,8 +324,8 @@ fn prompt_claude_config(current: Option<&Value>) -> Result { let api_key = if let Some(current_key) = current .and_then(|v| v.get("env")) - .and_then(|e| e.get("ANTHROPIC_AUTH_TOKEN")) - .and_then(|k| k.as_str()) + .and_then(|v| v.as_object()) + .and_then(crate::services::provider::get_claude_token_from_env) .filter(|s| !s.is_empty()) { // 编辑模式:显示完整 API Key 供编辑 @@ -369,8 +369,9 @@ fn prompt_claude_config(current: Option<&Value>) -> Result { .prompt() .map_err(|e| AppError::Message(texts::input_failed_error(&e.to_string())))?; + let (key, _) = crate::services::provider::claude_auth_env_keys(api_key.trim()); let mut env = serde_json::Map::new(); - env.insert("ANTHROPIC_AUTH_TOKEN".to_string(), json!(api_key.trim())); + env.insert(key.to_string(), json!(api_key.trim())); env.insert("ANTHROPIC_BASE_URL".to_string(), json!(base_url.trim())); if config_models { @@ -754,8 +755,8 @@ pub fn display_provider_summary(provider: &Provider, app_type: &AppType) { println!("\n{}", texts::core_config_label().bright_cyan()); match app_type { AppType::Claude => { - if let Some(env) = provider.settings_config.get("env") { - if let Some(api_key) = env.get("ANTHROPIC_AUTH_TOKEN").and_then(|v| v.as_str()) { + if let Some(env) = provider.settings_config.get("env").and_then(|v| v.as_object()) { + if let Some(api_key) = crate::services::provider::get_claude_token_from_env(env) { println!( " {}: {}", texts::api_key_display_label(), diff --git a/src-tauri/src/cli/interactive/provider.rs b/src-tauri/src/cli/interactive/provider.rs index 98eb57e..54bd8fb 100644 --- a/src-tauri/src/cli/interactive/provider.rs +++ b/src-tauri/src/cli/interactive/provider.rs @@ -725,10 +725,7 @@ fn extract_claude_config(settings_config: &serde_json::Value) -> ClaudeConfig { if let Some(env) = env { ClaudeConfig { - api_key: env - .get("ANTHROPIC_AUTH_TOKEN") - .or_else(|| env.get("ANTHROPIC_API_KEY")) - .and_then(|v| v.as_str()) + api_key: crate::services::provider::get_claude_token_from_env(env) .map(|s| mask_api_key(s)), base_url: env .get("ANTHROPIC_BASE_URL") diff --git a/src-tauri/src/cli/tui/form.rs b/src-tauri/src/cli/tui/form.rs index e7662cc..231a9dc 100644 --- a/src-tauri/src/cli/tui/form.rs +++ b/src-tauri/src/cli/tui/form.rs @@ -402,7 +402,7 @@ impl ProviderAddFormState { .get("env") .and_then(|v| v.as_object()) { - if let Some(token) = env.get("ANTHROPIC_AUTH_TOKEN").and_then(|v| v.as_str()) { + if let Some(token) = crate::services::provider::get_claude_token_from_env(env) { form.claude_api_key.set(token); } if let Some(url) = env.get("ANTHROPIC_BASE_URL").and_then(|v| v.as_str()) { @@ -883,7 +883,9 @@ impl ProviderAddFormState { let env_obj = env_value .as_object_mut() .expect("env must be a JSON object"); - set_or_remove_trimmed(env_obj, "ANTHROPIC_AUTH_TOKEN", &self.claude_api_key.value); + let (key, old_key) = crate::services::provider::claude_auth_env_keys(&self.claude_api_key.value); + set_or_remove_trimmed(env_obj, key, &self.claude_api_key.value); + env_obj.remove(old_key); set_or_remove_trimmed(env_obj, "ANTHROPIC_BASE_URL", &self.claude_base_url.value); if self.claude_model_config_touched { set_or_remove_trimmed(env_obj, "ANTHROPIC_MODEL", &self.claude_model.value); @@ -2108,7 +2110,7 @@ mod tests { assert_eq!(provider["id"], "p1"); assert_eq!(provider["name"], "Provider One"); assert_eq!( - provider["settingsConfig"]["env"]["ANTHROPIC_AUTH_TOKEN"], + provider["settingsConfig"]["env"]["ANTHROPIC_API_KEY"], "token" ); assert_eq!( @@ -2544,7 +2546,7 @@ requires_openai_auth = true settings["env"]["ANTHROPIC_BASE_URL"], "https://provider.example", "provider field should override common snippet value" ); - assert_eq!(settings["env"]["ANTHROPIC_AUTH_TOKEN"], "sk-provider"); + assert_eq!(settings["env"]["ANTHROPIC_API_KEY"], "sk-provider"); } #[test] @@ -2704,7 +2706,7 @@ requires_openai_auth = true "common env keys should be removed" ); assert_eq!( - env.get("ANTHROPIC_AUTH_TOKEN") + env.get("ANTHROPIC_API_KEY") .and_then(|value| value.as_str()), Some("sk-provider"), "provider-specific env keys should be preserved" diff --git a/src-tauri/src/cli/tui/ui.rs b/src-tauri/src/cli/tui/ui.rs index bbfd3c8..f2d45e9 100644 --- a/src-tauri/src/cli/tui/ui.rs +++ b/src-tauri/src/cli/tui/ui.rs @@ -2717,10 +2717,7 @@ fn render_provider_detail( .get("env") .and_then(|v| v.as_object()) { - let api_key = env - .get("ANTHROPIC_AUTH_TOKEN") - .or_else(|| env.get("ANTHROPIC_API_KEY")) - .and_then(|v| v.as_str()) + let api_key = crate::services::provider::get_claude_token_from_env(env) .map(mask_api_key) .unwrap_or_else(|| texts::tui_na().to_string()); let base_url = env diff --git a/src-tauri/src/deeplink/provider.rs b/src-tauri/src/deeplink/provider.rs index 7951b7e..9ded48e 100644 --- a/src-tauri/src/deeplink/provider.rs +++ b/src-tauri/src/deeplink/provider.rs @@ -219,10 +219,9 @@ fn build_provider_meta(request: &DeepLinkImportRequest) -> Result serde_json::Value { let mut env = serde_json::Map::new(); - env.insert( - "ANTHROPIC_AUTH_TOKEN".to_string(), - json!(request.api_key.clone().unwrap_or_default()), - ); + let token = request.api_key.clone().unwrap_or_default(); + let (key, _) = crate::services::provider::claude_auth_env_keys(&token); + env.insert(key.to_string(), json!(token)); env.insert( "ANTHROPIC_BASE_URL".to_string(), json!(get_primary_endpoint(request)), @@ -358,7 +357,7 @@ fn merge_claude_config( })?; if request.api_key.as_ref().is_none_or(|s| s.is_empty()) { - if let Some(token) = env.get("ANTHROPIC_AUTH_TOKEN").and_then(|v| v.as_str()) { + if let Some(token) = crate::services::provider::get_claude_token_from_env(env) { request.api_key = Some(token.to_string()); } } diff --git a/src-tauri/src/services/provider/mod.rs b/src-tauri/src/services/provider/mod.rs index cf9d4e3..58f6565 100644 --- a/src-tauri/src/services/provider/mod.rs +++ b/src-tauri/src/services/provider/mod.rs @@ -20,6 +20,25 @@ use crate::store::AppState; use gemini_auth::GeminiAuthType; use live::LiveSnapshot; +/// 根据 token 前缀判断 Claude 认证环境变量名。 +/// OAuth token (`sk-ant-oat...`) → `ANTHROPIC_AUTH_TOKEN`(Bearer header), +/// 标准 API key → `ANTHROPIC_API_KEY`(x-api-key header)。 +/// 返回 `(应使用的 key, 应清除的旧 key)`。 +pub fn claude_auth_env_keys(token: &str) -> (&'static str, &'static str) { + if token.trim().starts_with("sk-ant-oat") { + ("ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_API_KEY") + } else { + ("ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN") + } +} + +/// 从 env 对象中读取 Claude API key,兼容两种 key 名。 +pub fn get_claude_token_from_env(env: &serde_json::Map) -> Option<&str> { + env.get("ANTHROPIC_AUTH_TOKEN") + .or_else(|| env.get("ANTHROPIC_API_KEY")) + .and_then(|v| v.as_str()) +} + /// 供应商相关业务逻辑 pub struct ProviderService; diff --git a/src-tauri/src/services/provider/usage.rs b/src-tauri/src/services/provider/usage.rs index 72d9470..56204ff 100644 --- a/src-tauri/src/services/provider/usage.rs +++ b/src-tauri/src/services/provider/usage.rs @@ -202,9 +202,7 @@ impl ProviderService { ) })?; - env.get("ANTHROPIC_AUTH_TOKEN") - .or_else(|| env.get("ANTHROPIC_API_KEY")) - .and_then(|v| v.as_str()) + super::get_claude_token_from_env(env) .ok_or_else(|| { AppError::localized( "provider.claude.api_key.missing", diff --git a/src-tauri/tests/deeplink_import.rs b/src-tauri/tests/deeplink_import.rs index b1ac401..bb5d8a8 100644 --- a/src-tauri/tests/deeplink_import.rs +++ b/src-tauri/tests/deeplink_import.rs @@ -39,7 +39,7 @@ fn deeplink_import_claude_provider_persists_to_config() { assert_eq!(provider.website_url.as_deref(), request.homepage.as_deref()); let auth_token = provider .settings_config - .pointer("/env/ANTHROPIC_AUTH_TOKEN") + .pointer("/env/ANTHROPIC_API_KEY") .and_then(|v| v.as_str()); let base_url = provider .settings_config