diff --git a/src-tauri/src/deeplink/mod.rs b/src-tauri/src/deeplink/mod.rs index 0b8afffe..e373010a 100644 --- a/src-tauri/src/deeplink/mod.rs +++ b/src-tauri/src/deeplink/mod.rs @@ -48,6 +48,10 @@ pub struct DeepLinkImportRequest { #[serde(skip_serializing_if = "Option::is_none")] pub opus_model: Option, + /// 自定义环境变量(从 inline config 的 env 对象中保留的非标准 key) + #[serde(skip_serializing_if = "Option::is_none")] + pub custom_env: Option>, + #[serde(skip_serializing_if = "Option::is_none")] pub content: Option, #[serde(skip_serializing_if = "Option::is_none")] diff --git a/src-tauri/src/deeplink/parser.rs b/src-tauri/src/deeplink/parser.rs index 79ab6c89..d6f59e7c 100644 --- a/src-tauri/src/deeplink/parser.rs +++ b/src-tauri/src/deeplink/parser.rs @@ -138,5 +138,6 @@ fn parse_provider_deeplink( .get("usageAutoInterval") .and_then(|v| v.parse::().ok()), openclaw_config: None, + custom_env: None, }) } diff --git a/src-tauri/src/deeplink/provider.rs b/src-tauri/src/deeplink/provider.rs index 2f53392f..099ea924 100644 --- a/src-tauri/src/deeplink/provider.rs +++ b/src-tauri/src/deeplink/provider.rs @@ -256,6 +256,13 @@ fn build_claude_settings(request: &DeepLinkImportRequest) -> serde_json::Value { ); } + // 合并自定义环境变量 + if let Some(custom) = &request.custom_env { + for (key, value) in custom { + env.entry(key.clone()).or_insert(value.clone()); + } + } + json!({ "env": env }) } @@ -482,7 +489,12 @@ 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()) { + // 支持两种 Claude 认证 env key:ANTHROPIC_AUTH_TOKEN 和 ANTHROPIC_API_KEY + if let Some(token) = env + .get("ANTHROPIC_AUTH_TOKEN") + .or_else(|| env.get("ANTHROPIC_API_KEY")) + .and_then(|v| v.as_str()) + { request.api_key = Some(token.to_string()); } } @@ -527,6 +539,25 @@ fn merge_claude_config( .map(|s| s.to_string()); } + // 保留非标准 env key(如 ANTHROPIC_CUSTOM_HEADERS 等自定义变量) + let known_keys: &[&str] = &[ + "ANTHROPIC_AUTH_TOKEN", + "ANTHROPIC_API_KEY", + "ANTHROPIC_BASE_URL", + "ANTHROPIC_MODEL", + "ANTHROPIC_DEFAULT_HAIKU_MODEL", + "ANTHROPIC_DEFAULT_SONNET_MODEL", + "ANTHROPIC_DEFAULT_OPUS_MODEL", + ]; + let custom: serde_json::Map = env + .iter() + .filter(|(k, _)| !known_keys.contains(&k.as_str())) + .map(|(k, v)| (k.clone(), v.clone())) + .collect(); + if !custom.is_empty() { + request.custom_env = Some(custom); + } + Ok(()) } diff --git a/src-tauri/tests/deeplink_import.rs b/src-tauri/tests/deeplink_import.rs index 3904bc91..92e802b5 100644 --- a/src-tauri/tests/deeplink_import.rs +++ b/src-tauri/tests/deeplink_import.rs @@ -334,3 +334,64 @@ fn deeplink_import_rejects_non_http_endpoints_from_config() { "expected scheme validation error, got {err:?}" ); } + +#[test] +fn deeplink_import_claude_provider_reads_api_key_from_anthropic_api_key() { + let _guard = lock_test_mutex(); + reset_test_fs(); + let _home = ensure_test_home(); + + // Inline config uses ANTHROPIC_API_KEY (not ANTHROPIC_AUTH_TOKEN) + let config_json = r#"{"env":{"ANTHROPIC_API_KEY":"sk-api-key-test","ANTHROPIC_BASE_URL":"https://api.example.com/v1"}}"#; + let config_b64 = BASE64_URL_SAFE_NO_PAD.encode(config_json.as_bytes()); + + let url = format!( + "ccswitch://v1/import?resource=provider&app=claude&name=APIKey%20Claude&config={config_b64}&configFormat=json" + ); + let request = parse_deeplink_url(&url).expect("parse deeplink url"); + + let mut config = MultiAppConfig::default(); + config.ensure_app(&AppType::Claude); + + let state = state_from_config(config); + + let provider_id = import_provider_from_deeplink(&state, request) + .expect("import provider from deeplink should succeed with ANTHROPIC_API_KEY"); + + let guard = state.config.read().expect("read config"); + let manager = guard + .get_manager(&AppType::Claude) + .expect("claude manager should exist"); + let provider = manager + .providers + .get(&provider_id) + .expect("provider created via deeplink"); + + // API key should be read from ANTHROPIC_API_KEY + let auth_token = provider + .settings_config + .pointer("/env/ANTHROPIC_AUTH_TOKEN") + .and_then(|v| v.as_str()); + assert_eq!( + auth_token, + Some("sk-api-key-test"), + "api_key should be populated from ANTHROPIC_API_KEY" + ); + + // ANTHROPIC_API_KEY should NOT appear in custom_env (it's a known key) + let api_key_in_env = provider + .settings_config + .pointer("/env/ANTHROPIC_API_KEY") + .and_then(|v| v.as_str()); + assert!( + api_key_in_env.is_none(), + "ANTHROPIC_API_KEY should not leak into env as a separate key" + ); + drop(guard); + + let persisted = state + .db + .get_provider_by_id(&provider_id, AppType::Claude.as_str()) + .expect("read provider from db"); + assert!(persisted.is_some(), "provider should be persisted to db"); +}