mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-25 13:45:03 +08:00
fix(proxy): inject only ANTHROPIC_API_KEY for managed-account Claude takeover
- Provider: add uses_managed_account_auth / is_github_copilot helpers to identify managed-account providers (GitHub Copilot / Codex OAuth) - ProxyService: choose auth policy by provider type when taking over Claude Live config. Managed accounts drop token env keys and write only the ANTHROPIC_API_KEY placeholder; other providers keep the existing ANTHROPIC_AUTH_TOKEN fallback behavior - Forwarder: add outbound guard that refuses to send the PROXY_MANAGED placeholder upstream to *.githubcopilot.com and chatgpt.com /backend-api/codex - Add unit tests covering detection, injection, and the outbound guard
This commit is contained in:
+176
-16
@@ -45,6 +45,12 @@ const CLAUDE_TAKEOVER_OPUS_MODEL: &str = "claude-opus-4-7";
|
||||
// 写给 Claude Code 时沿用文档示例的大写形式;解析侧大小写不敏感。
|
||||
const CLAUDE_ONE_M_MARKER_FOR_CLIENT: &str = "[1M]";
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum ClaudeTakeoverAuthPolicy {
|
||||
PreserveExistingOrAuthToken,
|
||||
ManagedAccount,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ProxyService {
|
||||
db: Arc<Database>,
|
||||
@@ -69,7 +75,34 @@ impl ProxyService {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn apply_claude_takeover_fields(config: &mut Value, proxy_url: &str) {
|
||||
Self::apply_claude_takeover_fields_with_policy(
|
||||
config,
|
||||
proxy_url,
|
||||
ClaudeTakeoverAuthPolicy::PreserveExistingOrAuthToken,
|
||||
);
|
||||
}
|
||||
|
||||
fn apply_claude_takeover_fields_for_provider(
|
||||
config: &mut Value,
|
||||
proxy_url: &str,
|
||||
provider: &Provider,
|
||||
) {
|
||||
let auth_policy = if provider.uses_managed_account_auth() {
|
||||
ClaudeTakeoverAuthPolicy::ManagedAccount
|
||||
} else {
|
||||
ClaudeTakeoverAuthPolicy::PreserveExistingOrAuthToken
|
||||
};
|
||||
|
||||
Self::apply_claude_takeover_fields_with_policy(config, proxy_url, auth_policy);
|
||||
}
|
||||
|
||||
fn apply_claude_takeover_fields_with_policy(
|
||||
config: &mut Value,
|
||||
proxy_url: &str,
|
||||
auth_policy: ClaudeTakeoverAuthPolicy,
|
||||
) {
|
||||
// 必须在 remove/insert 前 snapshot:避免读到自己刚写入的接管别名。
|
||||
let takeover_model_fields = Self::build_claude_takeover_model_fields(config);
|
||||
|
||||
@@ -105,19 +138,32 @@ impl ProxyService {
|
||||
"OPENAI_API_KEY",
|
||||
];
|
||||
|
||||
let mut replaced_any = false;
|
||||
for key in token_keys {
|
||||
if env.contains_key(key) {
|
||||
env.insert(key.to_string(), json!(PROXY_TOKEN_PLACEHOLDER));
|
||||
replaced_any = true;
|
||||
}
|
||||
}
|
||||
match auth_policy {
|
||||
ClaudeTakeoverAuthPolicy::PreserveExistingOrAuthToken => {
|
||||
let mut replaced_any = false;
|
||||
for key in token_keys {
|
||||
if env.contains_key(key) {
|
||||
env.insert(key.to_string(), json!(PROXY_TOKEN_PLACEHOLDER));
|
||||
replaced_any = true;
|
||||
}
|
||||
}
|
||||
|
||||
if !replaced_any {
|
||||
env.insert(
|
||||
"ANTHROPIC_AUTH_TOKEN".to_string(),
|
||||
json!(PROXY_TOKEN_PLACEHOLDER),
|
||||
);
|
||||
if !replaced_any {
|
||||
env.insert(
|
||||
"ANTHROPIC_AUTH_TOKEN".to_string(),
|
||||
json!(PROXY_TOKEN_PLACEHOLDER),
|
||||
);
|
||||
}
|
||||
}
|
||||
ClaudeTakeoverAuthPolicy::ManagedAccount => {
|
||||
for key in token_keys {
|
||||
env.remove(key);
|
||||
}
|
||||
env.insert(
|
||||
"ANTHROPIC_API_KEY".to_string(),
|
||||
json!(PROXY_TOKEN_PLACEHOLDER),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,11 +274,32 @@ impl ProxyService {
|
||||
.map_err(|e| format!("构建 claude 有效配置失败: {e}"))?;
|
||||
let (proxy_url, _) = self.build_proxy_urls().await?;
|
||||
|
||||
Self::apply_claude_takeover_fields(&mut effective_settings, &proxy_url);
|
||||
Self::apply_claude_takeover_fields_for_provider(
|
||||
&mut effective_settings,
|
||||
&proxy_url,
|
||||
provider,
|
||||
);
|
||||
self.write_claude_live(&effective_settings)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_current_provider_for_app(&self, app_type: &AppType) -> Result<Option<Provider>, String> {
|
||||
let Some(current_id) = crate::settings::get_effective_current_provider(&self.db, app_type)
|
||||
.map_err(|e| format!("获取 {app_type:?} 当前供应商失败: {e}"))?
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
self.db
|
||||
.get_provider_by_id(¤t_id, app_type.as_str())
|
||||
.map_err(|e| format!("读取 {app_type:?} 当前供应商失败: {e}"))
|
||||
}
|
||||
|
||||
fn require_current_provider_for_app(&self, app_type: &AppType) -> Result<Provider, String> {
|
||||
self.get_current_provider_for_app(app_type)?
|
||||
.ok_or_else(|| format!("{app_type:?} 当前供应商不存在,无法接管 Live 配置"))
|
||||
}
|
||||
|
||||
/// 设置 AppHandle(在应用初始化时调用)
|
||||
pub fn set_app_handle(&self, handle: tauri::AppHandle) {
|
||||
futures::executor::block_on(async {
|
||||
@@ -1007,7 +1074,12 @@ impl ProxyService {
|
||||
|
||||
// Claude: 修改 ANTHROPIC_BASE_URL,使用占位符替代真实 Token(代理会注入真实 Token)
|
||||
if let Ok(mut live_config) = self.read_claude_live() {
|
||||
Self::apply_claude_takeover_fields(&mut live_config, &proxy_url);
|
||||
let claude_provider = self.require_current_provider_for_app(&AppType::Claude)?;
|
||||
Self::apply_claude_takeover_fields_for_provider(
|
||||
&mut live_config,
|
||||
&proxy_url,
|
||||
&claude_provider,
|
||||
);
|
||||
self.write_claude_live(&live_config)?;
|
||||
log::info!("Claude Live 配置已接管,代理地址: {proxy_url}");
|
||||
}
|
||||
@@ -1057,7 +1129,12 @@ impl ProxyService {
|
||||
match app_type {
|
||||
AppType::Claude => {
|
||||
let mut live_config = self.read_claude_live()?;
|
||||
Self::apply_claude_takeover_fields(&mut live_config, &proxy_url);
|
||||
let claude_provider = self.require_current_provider_for_app(&AppType::Claude)?;
|
||||
Self::apply_claude_takeover_fields_for_provider(
|
||||
&mut live_config,
|
||||
&proxy_url,
|
||||
&claude_provider,
|
||||
);
|
||||
self.write_claude_live(&live_config)?;
|
||||
log::info!("Claude Live 配置已接管,代理地址: {proxy_url}");
|
||||
}
|
||||
@@ -1107,7 +1184,23 @@ impl ProxyService {
|
||||
match app_type {
|
||||
AppType::Claude => {
|
||||
if let Ok(mut live_config) = self.read_claude_live() {
|
||||
Self::apply_claude_takeover_fields(&mut live_config, &proxy_url);
|
||||
let claude_provider = self
|
||||
.get_current_provider_for_app(&AppType::Claude)
|
||||
.ok()
|
||||
.flatten();
|
||||
if let Some(provider) = claude_provider.as_ref() {
|
||||
Self::apply_claude_takeover_fields_for_provider(
|
||||
&mut live_config,
|
||||
&proxy_url,
|
||||
provider,
|
||||
);
|
||||
} else {
|
||||
Self::apply_claude_takeover_fields_with_policy(
|
||||
&mut live_config,
|
||||
&proxy_url,
|
||||
ClaudeTakeoverAuthPolicy::PreserveExistingOrAuthToken,
|
||||
);
|
||||
}
|
||||
let _ = self.write_claude_live(&live_config);
|
||||
}
|
||||
}
|
||||
@@ -2083,6 +2176,73 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn managed_account_claude_takeover_uses_api_key_placeholder() {
|
||||
let mut provider = Provider::with_id(
|
||||
"copilot".to_string(),
|
||||
"GitHub Copilot".to_string(),
|
||||
json!({
|
||||
"env": {
|
||||
"ANTHROPIC_BASE_URL": "https://api.githubcopilot.com",
|
||||
"ANTHROPIC_MODEL": "claude-haiku-4.5"
|
||||
}
|
||||
}),
|
||||
None,
|
||||
);
|
||||
provider.meta = Some(ProviderMeta {
|
||||
provider_type: Some("github_copilot".to_string()),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let mut live_config = provider.settings_config.clone();
|
||||
ProxyService::apply_claude_takeover_fields_for_provider(
|
||||
&mut live_config,
|
||||
"http://127.0.0.1:15721",
|
||||
&provider,
|
||||
);
|
||||
|
||||
let env = live_config
|
||||
.get("env")
|
||||
.and_then(|value| value.as_object())
|
||||
.expect("env should exist");
|
||||
assert_eq!(
|
||||
env.get("ANTHROPIC_API_KEY")
|
||||
.and_then(|value| value.as_str()),
|
||||
Some(PROXY_TOKEN_PLACEHOLDER)
|
||||
);
|
||||
assert!(
|
||||
env.get("ANTHROPIC_AUTH_TOKEN").is_none(),
|
||||
"managed OAuth providers should avoid Claude Auth Token login semantics"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normal_claude_takeover_without_token_keeps_auth_token_fallback() {
|
||||
let mut live_config = json!({
|
||||
"env": {
|
||||
"ANTHROPIC_BASE_URL": "https://api.example.com",
|
||||
"ANTHROPIC_MODEL": "claude-haiku-4.5"
|
||||
}
|
||||
});
|
||||
|
||||
ProxyService::apply_claude_takeover_fields(&mut live_config, "http://127.0.0.1:15721");
|
||||
|
||||
assert_eq!(
|
||||
live_config
|
||||
.get("env")
|
||||
.and_then(|env| env.get("ANTHROPIC_AUTH_TOKEN"))
|
||||
.and_then(|value| value.as_str()),
|
||||
Some(PROXY_TOKEN_PLACEHOLDER)
|
||||
);
|
||||
assert!(
|
||||
live_config
|
||||
.get("env")
|
||||
.and_then(|env| env.get("ANTHROPIC_API_KEY"))
|
||||
.is_none(),
|
||||
"non-managed providers should retain the legacy fallback behavior"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_toml_base_url_updates_active_model_provider_base_url() {
|
||||
let input = r#"
|
||||
|
||||
Reference in New Issue
Block a user