mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-29 18:05:37 +08:00
fix(copilot): 修复 GitHub Copilot 认证和代理问题 (#1854)
* fix(copilot): 修复 GitHub Copilot 400 认证错误 问题:使用 GitHub Copilot provider 时报错 400 bad request 根因:与 copilot-api 项目对比发现多处差异 修复内容: - 更新版本号 0.26.7 到 0.38.2 - 更新 API 版本 2025-04-01 到 2025-10-01 - 添加缺失的关键 headers - 修正 openai-intent 值 - 添加动态 API endpoint 支持 - 同步更新 stream_check.rs headers Closes #1777 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: flush stream after write_all in hyper_client proxy Add explicit flush() calls after write_all() for TLS stream, plain TCP stream, and CONNECT tunnel requests to ensure buffered data is sent immediately, preventing connection hangs in Copilot auth header flow. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * 修复登录时的剪切板在mac与linux端可能没复制验证码 * fix: flush stream after write_all in hyper_client proxy Add explicit flush() calls after write_all() for TLS stream, plain TCP stream, and CONNECT tunnel requests to ensure buffered data is sent immediately, preventing connection hangs in Copilot auth header flow. * 修复登录时的剪切板在mac与linux端可能没复制验证码 * 1、修复不同类型的个人商业等不同类型的copilot账号问题 2、将验证码复制改为异步操作 * fix: address PR review comments for Copilot auth │ │ │ │ - Fix clipboard blocking by using spawn_blocking for arboard ops │ │ - Implement dynamic endpoint routing for enterprise Copilot users │ │ - Add api_endpoints cache cleanup in remove_account() and clear_auth() │ │ - Change API endpoint log level from info to debug │ │ - Fix clear_auth() to continue cleanup even if file deletion fails │ │ - Add 9 unit tests for Copilot detection and api_endpoints cachin * style: fix cargo fmt formatting * Fix Copilot dynamic endpoint handling * fix: restore clear_auth() memory-first cleanup order and fix cache leaks - Restore clear_auth() to clean memory state before deleting the storage file. The previous order (file deletion first) caused a regression where users could get stuck in a "cannot log out" state if file removal failed. - Add missing copilot_models.clear() in clear_auth() — this cache was cleaned in remove_account() but never in the full clear path. - Add endpoint_locks cleanup in both remove_account() and clear_auth() to prevent minor in-process memory leaks. - Update test to assert the correct behavior: memory should be cleaned even when file deletion fails. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: 周梦泽 <mengze.zhou@dafeng-tech.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Jason <farion1231@gmail.com>
This commit is contained in:
@@ -747,7 +747,7 @@ impl RequestForwarder {
|
||||
adapter: &dyn ProviderAdapter,
|
||||
) -> Result<(ProxyResponse, Option<String>), ProxyError> {
|
||||
// 使用适配器提取 base_url
|
||||
let base_url = adapter.extract_base_url(provider)?;
|
||||
let mut base_url = adapter.extract_base_url(provider)?;
|
||||
|
||||
let is_full_url = provider
|
||||
.meta
|
||||
@@ -770,6 +770,36 @@ impl RequestForwarder {
|
||||
.and_then(|m| m.provider_type.as_deref())
|
||||
== Some("github_copilot")
|
||||
|| base_url.contains("githubcopilot.com");
|
||||
|
||||
// GitHub Copilot 动态 endpoint 路由
|
||||
// 从 CopilotAuthManager 获取缓存的 API endpoint(支持企业版等非默认 endpoint)
|
||||
if is_copilot && !is_full_url {
|
||||
if let Some(app_handle) = &self.app_handle {
|
||||
let copilot_state = app_handle.state::<CopilotAuthState>();
|
||||
let copilot_auth = copilot_state.0.read().await;
|
||||
|
||||
// 从 provider.meta 获取关联的 GitHub 账号 ID
|
||||
let account_id = provider
|
||||
.meta
|
||||
.as_ref()
|
||||
.and_then(|m| m.managed_account_id_for("github_copilot"));
|
||||
|
||||
let dynamic_endpoint = match &account_id {
|
||||
Some(id) => copilot_auth.get_api_endpoint(id).await,
|
||||
None => copilot_auth.get_default_api_endpoint().await,
|
||||
};
|
||||
|
||||
// 只在动态 endpoint 与当前 base_url 不同时替换
|
||||
if dynamic_endpoint != base_url {
|
||||
log::debug!(
|
||||
"[Copilot] 使用动态 API endpoint: {} (原: {})",
|
||||
dynamic_endpoint,
|
||||
base_url
|
||||
);
|
||||
base_url = dynamic_endpoint;
|
||||
}
|
||||
}
|
||||
}
|
||||
let resolved_claude_api_format = if adapter.name() == "Claude" {
|
||||
Some(
|
||||
self.resolve_claude_api_format(provider, &mapped_body, is_copilot)
|
||||
@@ -893,6 +923,12 @@ impl RequestForwarder {
|
||||
"copilot-integration-id",
|
||||
"x-github-api-version",
|
||||
"openai-intent",
|
||||
// 新增 headers
|
||||
"x-initiator",
|
||||
"x-interaction-type",
|
||||
"x-vscode-user-agent-library-version",
|
||||
"x-request-id",
|
||||
"x-agent-task-id",
|
||||
]
|
||||
} else {
|
||||
&[]
|
||||
@@ -1663,4 +1699,107 @@ mod tests {
|
||||
&headers
|
||||
));
|
||||
}
|
||||
|
||||
// ==================== Copilot 动态 endpoint 路由相关测试 ====================
|
||||
|
||||
/// 验证 is_copilot 检测逻辑:通过 provider_type 判断
|
||||
#[test]
|
||||
fn copilot_detection_via_provider_type() {
|
||||
use crate::provider::{Provider, ProviderMeta};
|
||||
|
||||
let provider = Provider {
|
||||
id: "test".to_string(),
|
||||
name: "Test Copilot".to_string(),
|
||||
settings_config: serde_json::json!({}),
|
||||
website_url: None,
|
||||
category: None,
|
||||
created_at: None,
|
||||
sort_index: None,
|
||||
notes: None,
|
||||
meta: Some(ProviderMeta {
|
||||
provider_type: Some("github_copilot".to_string()),
|
||||
..Default::default()
|
||||
}),
|
||||
icon: None,
|
||||
icon_color: None,
|
||||
in_failover_queue: false,
|
||||
};
|
||||
|
||||
let is_copilot = provider
|
||||
.meta
|
||||
.as_ref()
|
||||
.and_then(|m| m.provider_type.as_deref())
|
||||
== Some("github_copilot");
|
||||
|
||||
assert!(is_copilot, "应该通过 provider_type 检测为 Copilot");
|
||||
}
|
||||
|
||||
/// 验证 is_copilot 检测逻辑:通过 base_url 判断
|
||||
#[test]
|
||||
fn copilot_detection_via_base_url() {
|
||||
let base_url = "https://api.githubcopilot.com";
|
||||
let is_copilot = base_url.contains("githubcopilot.com");
|
||||
assert!(is_copilot, "应该通过 base_url 检测为 Copilot");
|
||||
|
||||
let non_copilot_url = "https://api.anthropic.com";
|
||||
let is_not_copilot = non_copilot_url.contains("githubcopilot.com");
|
||||
assert!(!is_not_copilot, "非 Copilot URL 不应被检测为 Copilot");
|
||||
}
|
||||
|
||||
/// 验证企业版 endpoint(不包含 githubcopilot.com)场景下 is_copilot 仍然正确
|
||||
#[test]
|
||||
fn copilot_detection_for_enterprise_endpoint() {
|
||||
use crate::provider::{Provider, ProviderMeta};
|
||||
|
||||
// 企业版场景:provider_type 是 github_copilot,但 base_url 可能是企业内部域名
|
||||
let provider = Provider {
|
||||
id: "enterprise".to_string(),
|
||||
name: "Enterprise Copilot".to_string(),
|
||||
settings_config: serde_json::json!({}),
|
||||
website_url: None,
|
||||
category: None,
|
||||
created_at: None,
|
||||
sort_index: None,
|
||||
notes: None,
|
||||
meta: Some(ProviderMeta {
|
||||
provider_type: Some("github_copilot".to_string()),
|
||||
..Default::default()
|
||||
}),
|
||||
icon: None,
|
||||
icon_color: None,
|
||||
in_failover_queue: false,
|
||||
};
|
||||
|
||||
let enterprise_base_url = "https://copilot-api.corp.example.com";
|
||||
|
||||
// is_copilot 应该通过 provider_type 检测成功,即使 base_url 不包含 githubcopilot.com
|
||||
let is_copilot = provider
|
||||
.meta
|
||||
.as_ref()
|
||||
.and_then(|m| m.provider_type.as_deref())
|
||||
== Some("github_copilot")
|
||||
|| enterprise_base_url.contains("githubcopilot.com");
|
||||
|
||||
assert!(
|
||||
is_copilot,
|
||||
"企业版 Copilot 应该通过 provider_type 被正确检测"
|
||||
);
|
||||
}
|
||||
|
||||
/// 验证动态 endpoint 替换条件
|
||||
#[test]
|
||||
fn dynamic_endpoint_replacement_conditions() {
|
||||
// 条件:is_copilot && !is_full_url
|
||||
let test_cases = [
|
||||
(true, false, true, "Copilot + 非 full_url 应该替换"),
|
||||
(true, true, false, "Copilot + full_url 不应替换"),
|
||||
(false, false, false, "非 Copilot 不应替换"),
|
||||
(false, true, false, "非 Copilot + full_url 不应替换"),
|
||||
];
|
||||
|
||||
for (is_copilot, is_full_url, should_replace, desc) in test_cases {
|
||||
let will_replace = is_copilot && !is_full_url;
|
||||
assert_eq!(will_replace, should_replace, "{desc}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user