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