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:
@@ -34,6 +34,22 @@ pub async fn open_external(app: AppHandle, url: String) -> Result<bool, String>
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn copy_text_to_clipboard(text: String) -> Result<bool, String> {
|
||||
// Use spawn_blocking to avoid blocking the async runtime
|
||||
// Clipboard access can block on some platforms and may have thread/loop constraints
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let mut clipboard =
|
||||
arboard::Clipboard::new().map_err(|e| format!("访问系统剪贴板失败: {e}"))?;
|
||||
clipboard
|
||||
.set_text(text)
|
||||
.map_err(|e| format!("写入系统剪贴板失败: {e}"))?;
|
||||
Ok(true)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| format!("剪贴板任务执行失败: {e}"))?
|
||||
}
|
||||
|
||||
/// 检查更新
|
||||
#[tauri::command]
|
||||
pub async fn check_for_updates(handle: AppHandle) -> Result<bool, String> {
|
||||
|
||||
@@ -26,6 +26,7 @@ pub async fn stream_check_provider(
|
||||
.ok_or_else(|| AppError::Message(format!("供应商 {provider_id} 不存在")))?;
|
||||
|
||||
let auth_override = resolve_copilot_auth_override(provider, &copilot_state).await?;
|
||||
let base_url_override = resolve_copilot_base_url_override(provider, &copilot_state).await?;
|
||||
let claude_api_format_override = resolve_claude_api_format_override(
|
||||
&app_type,
|
||||
provider,
|
||||
@@ -39,6 +40,7 @@ pub async fn stream_check_provider(
|
||||
provider,
|
||||
&config,
|
||||
auth_override,
|
||||
base_url_override,
|
||||
claude_api_format_override,
|
||||
)
|
||||
.await?;
|
||||
@@ -87,6 +89,8 @@ pub async fn stream_check_all_providers(
|
||||
}
|
||||
|
||||
let auth_override = resolve_copilot_auth_override(&provider, &copilot_state).await?;
|
||||
let base_url_override =
|
||||
resolve_copilot_base_url_override(&provider, &copilot_state).await?;
|
||||
let claude_api_format_override = resolve_claude_api_format_override(
|
||||
&app_type,
|
||||
&provider,
|
||||
@@ -108,6 +112,7 @@ pub async fn stream_check_all_providers(
|
||||
&provider,
|
||||
&config,
|
||||
auth_override,
|
||||
base_url_override,
|
||||
claude_api_format_override,
|
||||
)
|
||||
.await
|
||||
@@ -151,17 +156,7 @@ async fn resolve_copilot_auth_override(
|
||||
provider: &crate::provider::Provider,
|
||||
copilot_state: &State<'_, CopilotAuthState>,
|
||||
) -> Result<Option<crate::proxy::providers::AuthInfo>, AppError> {
|
||||
let is_copilot = provider
|
||||
.meta
|
||||
.as_ref()
|
||||
.and_then(|meta| meta.provider_type.as_deref())
|
||||
== Some("github_copilot")
|
||||
|| provider
|
||||
.settings_config
|
||||
.pointer("/env/ANTHROPIC_BASE_URL")
|
||||
.and_then(|value| value.as_str())
|
||||
.map(|url| url.contains("githubcopilot.com"))
|
||||
.unwrap_or(false);
|
||||
let is_copilot = is_copilot_provider(provider);
|
||||
|
||||
if !is_copilot {
|
||||
return Ok(None);
|
||||
@@ -171,7 +166,7 @@ async fn resolve_copilot_auth_override(
|
||||
let account_id = provider
|
||||
.meta
|
||||
.as_ref()
|
||||
.and_then(|meta| meta.github_account_id.clone());
|
||||
.and_then(|meta| meta.managed_account_id_for("github_copilot"));
|
||||
|
||||
let token = match account_id.as_deref() {
|
||||
Some(id) => auth_manager
|
||||
@@ -190,6 +185,49 @@ async fn resolve_copilot_auth_override(
|
||||
)))
|
||||
}
|
||||
|
||||
async fn resolve_copilot_base_url_override(
|
||||
provider: &crate::provider::Provider,
|
||||
copilot_state: &State<'_, CopilotAuthState>,
|
||||
) -> Result<Option<String>, AppError> {
|
||||
let is_copilot = is_copilot_provider(provider);
|
||||
let is_full_url = provider
|
||||
.meta
|
||||
.as_ref()
|
||||
.and_then(|meta| meta.is_full_url)
|
||||
.unwrap_or(false);
|
||||
|
||||
if !is_copilot || is_full_url {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let auth_manager = copilot_state.0.read().await;
|
||||
let account_id = provider
|
||||
.meta
|
||||
.as_ref()
|
||||
.and_then(|meta| meta.managed_account_id_for("github_copilot"));
|
||||
|
||||
let endpoint = match account_id.as_deref() {
|
||||
Some(id) => auth_manager.get_api_endpoint(id).await,
|
||||
None => auth_manager.get_default_api_endpoint().await,
|
||||
};
|
||||
|
||||
Ok(Some(endpoint))
|
||||
}
|
||||
|
||||
fn is_copilot_provider(provider: &crate::provider::Provider) -> bool {
|
||||
provider
|
||||
.meta
|
||||
.as_ref()
|
||||
.and_then(|meta| meta.provider_type.as_deref())
|
||||
== Some("github_copilot")
|
||||
|| provider
|
||||
.settings_config
|
||||
.pointer("/env/ANTHROPIC_BASE_URL")
|
||||
.and_then(|value| value.as_str())
|
||||
.map(|url| url.contains("githubcopilot.com"))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
async fn resolve_claude_api_format_override(
|
||||
app_type: &AppType,
|
||||
provider: &crate::provider::Provider,
|
||||
@@ -237,3 +275,80 @@ async fn resolve_claude_api_format_override(
|
||||
|
||||
Ok(Some(api_format.to_string()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::is_copilot_provider;
|
||||
use crate::provider::{Provider, ProviderMeta};
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn copilot_provider_detection_accepts_provider_type_or_base_url() {
|
||||
let typed_provider = Provider {
|
||||
id: "p1".to_string(),
|
||||
name: "typed".to_string(),
|
||||
settings_config: 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,
|
||||
};
|
||||
assert!(is_copilot_provider(&typed_provider));
|
||||
|
||||
let url_provider = Provider {
|
||||
id: "p2".to_string(),
|
||||
name: "url".to_string(),
|
||||
settings_config: json!({
|
||||
"env": {
|
||||
"ANTHROPIC_BASE_URL": "https://api.githubcopilot.com"
|
||||
}
|
||||
}),
|
||||
website_url: None,
|
||||
category: None,
|
||||
created_at: None,
|
||||
sort_index: None,
|
||||
notes: None,
|
||||
meta: None,
|
||||
icon: None,
|
||||
icon_color: None,
|
||||
in_failover_queue: false,
|
||||
};
|
||||
assert!(is_copilot_provider(&url_provider));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copilot_full_url_metadata_is_available_for_override_guard() {
|
||||
let provider = Provider {
|
||||
id: "p3".to_string(),
|
||||
name: "relay".to_string(),
|
||||
settings_config: json!({}),
|
||||
website_url: None,
|
||||
category: None,
|
||||
created_at: None,
|
||||
sort_index: None,
|
||||
notes: None,
|
||||
meta: Some(ProviderMeta {
|
||||
provider_type: Some("github_copilot".to_string()),
|
||||
is_full_url: Some(true),
|
||||
..Default::default()
|
||||
}),
|
||||
icon: None,
|
||||
icon_color: None,
|
||||
in_failover_queue: false,
|
||||
};
|
||||
|
||||
assert!(is_copilot_provider(&provider));
|
||||
assert_eq!(
|
||||
provider.meta.as_ref().and_then(|meta| meta.is_full_url),
|
||||
Some(true)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user