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