diff --git a/src-tauri/src/proxy/forwarder.rs b/src-tauri/src/proxy/forwarder.rs index 742ebdb4..a36b31a5 100644 --- a/src-tauri/src/proxy/forwarder.rs +++ b/src-tauri/src/proxy/forwarder.rs @@ -20,23 +20,17 @@ use tokio::sync::RwLock; /// Headers 黑名单 - 不透传到上游的 Headers /// -/// 参考 Claude Code Hub 设计,过滤以下类别: -/// 1. 认证类(会被覆盖) -/// 2. 连接类(由 HTTP 客户端管理) -/// 3. 代理转发类 -/// 4. CDN/云服务商特定头 -/// 5. 请求追踪类 -/// 6. 浏览器特定头(可能被上游检测) +/// 精简版黑名单,只过滤必须覆盖或可能导致问题的 header +/// 参考成功透传的请求,保留更多原始 header /// /// 注意:客户端 IP 类(x-forwarded-for, x-real-ip)默认透传 const HEADER_BLACKLIST: &[&str] = &[ // 认证类(会被覆盖) "authorization", "x-api-key", - // 连接类 + // 连接类(由 HTTP 客户端管理) "host", "content-length", - "connection", "transfer-encoding", // 编码类(会被覆盖为 identity) "accept-encoding", @@ -68,16 +62,9 @@ const HEADER_BLACKLIST: &[&str] = &[ "x-b3-sampled", "traceparent", "tracestate", - // 浏览器特定头(可能被上游检测为非 CLI 请求) - "sec-fetch-mode", - "sec-fetch-site", - "sec-fetch-dest", - "sec-ch-ua", - "sec-ch-ua-mobile", - "sec-ch-ua-platform", - "accept-language", - // anthropic-beta 单独处理,避免重复 + // anthropic 特定头单独处理,避免重复 "anthropic-beta", + "anthropic-version", // 客户端 IP 单独处理(默认透传) "x-forwarded-for", "x-real-ip", @@ -555,14 +542,30 @@ impl RequestForwarder { } } - // 处理 anthropic-beta Header(透传) - // 参考 Claude Code Hub 的实现,直接透传客户端的 beta 标记 - if let Some(beta) = headers.get("anthropic-beta") { - if let Ok(beta_str) = beta.to_str() { - request = request.header("anthropic-beta", beta_str); - passed_headers.push(("anthropic-beta".to_string(), beta_str.to_string())); - log::info!("[{}] 透传 anthropic-beta: {}", adapter.name(), beta_str); - } + // 处理 anthropic-beta Header(仅 Claude) + // 关键:确保包含 claude-code-20250219 标记,这是上游服务验证请求来源的依据 + // 如果客户端发送的 beta 标记中没有包含 claude-code-20250219,需要补充 + if adapter.name() == "Claude" { + const CLAUDE_CODE_BETA: &str = "claude-code-20250219"; + let beta_value = if let Some(beta) = headers.get("anthropic-beta") { + if let Ok(beta_str) = beta.to_str() { + // 检查是否已包含 claude-code-20250219 + if beta_str.contains(CLAUDE_CODE_BETA) { + beta_str.to_string() + } else { + // 补充 claude-code-20250219 + format!("{CLAUDE_CODE_BETA},{beta_str}") + } + } else { + CLAUDE_CODE_BETA.to_string() + } + } else { + // 如果客户端没有发送,使用默认值 + CLAUDE_CODE_BETA.to_string() + }; + request = request.header("anthropic-beta", &beta_value); + passed_headers.push(("anthropic-beta".to_string(), beta_value.clone())); + log::info!("[{}] 设置 anthropic-beta: {}", adapter.name(), beta_value); } // 客户端 IP 透传(默认开启) @@ -612,19 +615,20 @@ impl RequestForwarder { ); } - // anthropic-version 透传:优先使用客户端的版本号 - // 参考 Claude Code Hub:透传客户端值而非固定版本 - if let Some(version) = headers.get("anthropic-version") { - if let Ok(version_str) = version.to_str() { - // 覆盖适配器设置的默认版本 - request = request.header("anthropic-version", version_str); - passed_headers.push(("anthropic-version".to_string(), version_str.to_string())); - log::info!( - "[{}] 透传 anthropic-version: {}", - adapter.name(), - version_str - ); - } + // anthropic-version 统一处理(仅 Claude):优先使用客户端的版本号,否则使用默认值 + // 注意:只设置一次,避免重复 + if adapter.name() == "Claude" { + let version_str = headers + .get("anthropic-version") + .and_then(|v| v.to_str().ok()) + .unwrap_or("2023-06-01"); + request = request.header("anthropic-version", version_str); + passed_headers.push(("anthropic-version".to_string(), version_str.to_string())); + log::info!( + "[{}] 设置 anthropic-version: {}", + adapter.name(), + version_str + ); } // ========== 最终发送的 Headers 日志 ========== diff --git a/src-tauri/src/proxy/providers/claude.rs b/src-tauri/src/proxy/providers/claude.rs index a0dfd87e..87a0d85c 100644 --- a/src-tauri/src/proxy/providers/claude.rs +++ b/src-tauri/src/proxy/providers/claude.rs @@ -217,28 +217,37 @@ impl ProviderAdapter for ClaudeAdapter { // 现在 OpenRouter 已推出 Claude Code 兼容接口,因此默认直接透传 endpoint。 // 如需回退旧逻辑,可在 forwarder 中根据 needs_transform 改写 endpoint。 - format!( + let base = format!( "{}/{}", base_url.trim_end_matches('/'), endpoint.trim_start_matches('/') - ) + ); + + // 为 /v1/messages 端点添加 ?beta=true 参数 + // 这是某些上游服务(如 DuckCoding)验证请求来源的关键参数 + if endpoint.contains("/v1/messages") && !endpoint.contains("?") { + format!("{base}?beta=true") + } else { + base + } } fn add_auth_headers(&self, request: RequestBuilder, auth: &AuthInfo) -> RequestBuilder { + // 注意:anthropic-version 由 forwarder.rs 统一处理(透传客户端值或设置默认值) + // 这里不再设置 anthropic-version,避免 header 重复 match auth.strategy { - // Anthropic 官方: Authorization Bearer + x-api-key + anthropic-version + // Anthropic 官方: Authorization Bearer + x-api-key AuthStrategy::Anthropic => request .header("Authorization", format!("Bearer {}", auth.api_key)) - .header("x-api-key", &auth.api_key) - .header("anthropic-version", "2023-06-01"), + .header("x-api-key", &auth.api_key), // ClaudeAuth 中转服务: 仅 Bearer,无 x-api-key - AuthStrategy::ClaudeAuth => request - .header("Authorization", format!("Bearer {}", auth.api_key)) - .header("anthropic-version", "2023-06-01"), + AuthStrategy::ClaudeAuth => { + request.header("Authorization", format!("Bearer {}", auth.api_key)) + } // OpenRouter: Bearer - AuthStrategy::Bearer => request - .header("Authorization", format!("Bearer {}", auth.api_key)) - .header("anthropic-version", "2023-06-01"), + AuthStrategy::Bearer => { + request.header("Authorization", format!("Bearer {}", auth.api_key)) + } _ => request, } } @@ -416,15 +425,33 @@ mod tests { #[test] fn test_build_url_anthropic() { let adapter = ClaudeAdapter::new(); + // /v1/messages 端点会自动添加 ?beta=true 参数 let url = adapter.build_url("https://api.anthropic.com", "/v1/messages"); - assert_eq!(url, "https://api.anthropic.com/v1/messages"); + assert_eq!(url, "https://api.anthropic.com/v1/messages?beta=true"); } #[test] fn test_build_url_openrouter() { let adapter = ClaudeAdapter::new(); + // /v1/messages 端点会自动添加 ?beta=true 参数 let url = adapter.build_url("https://openrouter.ai/api", "/v1/messages"); - assert_eq!(url, "https://openrouter.ai/api/v1/messages"); + assert_eq!(url, "https://openrouter.ai/api/v1/messages?beta=true"); + } + + #[test] + fn test_build_url_no_beta_for_other_endpoints() { + let adapter = ClaudeAdapter::new(); + // 非 /v1/messages 端点不添加 ?beta=true + let url = adapter.build_url("https://api.anthropic.com", "/v1/complete"); + assert_eq!(url, "https://api.anthropic.com/v1/complete"); + } + + #[test] + fn test_build_url_preserve_existing_query() { + let adapter = ClaudeAdapter::new(); + // 已有查询参数时不重复添加 + let url = adapter.build_url("https://api.anthropic.com", "/v1/messages?foo=bar"); + assert_eq!(url, "https://api.anthropic.com/v1/messages?foo=bar"); } #[test] diff --git a/src-tauri/src/services/provider/mod.rs b/src-tauri/src/services/provider/mod.rs index c80dfab9..a7df4073 100644 --- a/src-tauri/src/services/provider/mod.rs +++ b/src-tauri/src/services/provider/mod.rs @@ -374,7 +374,7 @@ impl ProviderService { let providers = state.db.get_all_providers(app_type.as_str())?; let provider = providers .get(¤t_id) - .ok_or_else(|| AppError::Message(format!("Provider {} not found", current_id)))?; + .ok_or_else(|| AppError::Message(format!("Provider {current_id} not found")))?; match app_type { AppType::Claude => Self::extract_claude_common_config(&provider.settings_config), diff --git a/src-tauri/src/services/skill.rs b/src-tauri/src/services/skill.rs index e681efbc..1729eda5 100644 --- a/src-tauri/src/services/skill.rs +++ b/src-tauri/src/services/skill.rs @@ -323,7 +323,7 @@ impl SkillService { // 获取 skill 信息 let skill = db .get_installed_skill(id)? - .ok_or_else(|| anyhow!("Skill not found: {}", id))?; + .ok_or_else(|| anyhow!("Skill not found: {id}"))?; // 从所有应用目录删除 for app in [AppType::Claude, AppType::Codex, AppType::Gemini] { @@ -353,7 +353,7 @@ impl SkillService { // 获取当前 skill let mut skill = db .get_installed_skill(id)? - .ok_or_else(|| anyhow!("Skill not found: {}", id))?; + .ok_or_else(|| anyhow!("Skill not found: {id}"))?; // 更新状态 skill.apps.set_enabled_for(app, enabled); @@ -521,7 +521,7 @@ impl SkillService { // 创建记录 let skill = InstalledSkill { - id: format!("local:{}", dir_name), + id: format!("local:{dir_name}"), name, description, directory: dir_name, @@ -551,7 +551,7 @@ impl SkillService { let source = ssot_dir.join(directory); if !source.exists() { - return Err(anyhow!("Skill 不存在于 SSOT: {}", directory)); + return Err(anyhow!("Skill 不存在于 SSOT: {directory}")); } let app_dir = Self::get_app_skills_dir(app)?; @@ -566,7 +566,7 @@ impl SkillService { Self::copy_dir_recursive(&source, &dest)?; - log::debug!("Skill {} 已复制到 {:?}", directory, app); + log::debug!("Skill {directory} 已复制到 {app:?}"); Ok(()) } @@ -578,7 +578,7 @@ impl SkillService { if skill_path.exists() { fs::remove_dir_all(&skill_path)?; - log::debug!("Skill {} 已从 {:?} 删除", directory, app); + log::debug!("Skill {directory} 已从 {app:?} 删除"); } Ok(()) @@ -1044,7 +1044,7 @@ pub fn migrate_skills_to_ssot(db: &Arc) -> Result { }; let skill = InstalledSkill { - id: format!("local:{}", directory), + id: format!("local:{directory}"), name, description, directory, @@ -1060,7 +1060,7 @@ pub fn migrate_skills_to_ssot(db: &Arc) -> Result { count += 1; } - log::info!("Skills 迁移完成,共 {} 个", count); + log::info!("Skills 迁移完成,共 {count} 个"); Ok(count) }