feat(copilot): add GitHub Copilot reverse proxy support (#930)

* refactor(toolsearch): replace binary patch with ENABLE_TOOL_SEARCH env var toggle

- Remove toolsearch_patch.rs binary patching mechanism (~590 lines)
  - Delete `toolsearch_patch.rs` and `commands/toolsearch.rs`
  - Remove auto-patch startup logic and command registration from lib.rs
  - Remove `tool_search_bypass` field from settings.rs
  - Remove frontend settings ToggleRow, useSettings hook sync logic, and API methods
  - Clean up zh/en/ja i18n keys (notifications + settings)

- Add ENABLE_TOOL_SEARCH toggle to Claude provider form
  - Add checkbox in CommonConfigEditor.tsx (alongside teammates toggle)
  - When enabled, writes `"env": { "ENABLE_TOOL_SEARCH": "true" }`
  - When disabled, removes the key; takes effect on provider switch
  - Add zh/en/ja i18n key: `claudeConfig.enableToolSearch`

Claude Code 2.1.76+ natively supports this env var, eliminating the need for binary patching.

* feat(claude): add effortLevel high toggle to provider form

- Add "high-effort thinking" checkbox to Claude provider config form
- When checked, writes `"effortLevel": "high"`; when unchecked, removes the field
- Add zh/en/ja i18n translations

* refactor(claude): remove deprecated alwaysThinking toggle

- Claude Code now enables extended thinking by default; alwaysThinkingEnabled is a no-op
- Thinking control is now handled via effortLevel (added in prior commit)
- Remove state, switch case, and checkbox UI from CommonConfigEditor
- Clean up alwaysThinking i18n keys across zh/en/ja locales

* feat(opencode): add setCacheKey: true to all provider presets

- Add setCacheKey: true to options in all 33 regular presets
- Add setCacheKey: true to OPENCODE_DEFAULT_CONFIG for custom providers
- Exclude 2 OMO presets (Oh My OpenCode / Slim) which have their own config mechanism

Closes #1523

* fix(codex): resolve 1M context window toggle causing MCP editor flicker

- Add localValueRef to short-circuit duplicate CodeMirror updateListener callbacks,
  breaking the React state → CodeMirror → stale onChange → React state feedback loop
- Use localValueRef.current in handleContextWindowToggle and handleCompactLimitChange
  to avoid stale closure reads
- Change compact limit input from type="number" to type="text" with inputMode="numeric"
  to remove unnecessary spinner buttons

* feat(codex): add 1M context window toggle utilities and i18n keys

- Add extractCodexTopLevelInt, setCodexTopLevelInt, removeCodexTopLevelField
  TOML helpers in providerConfigUtils.ts
- Add i18n keys for contextWindow1M, autoCompactLimit in zh/en/ja locales

* feat(claude): collapse model mapping fields by default

- Wrap 5 model mapping inputs in a Collapsible, collapsed by default
- Auto-expand when any model value is present (including preset-filled)
- Show hint text when collapsed explaining most users need no config
- Add zh/en/ja i18n keys for toggle label and collapsed hint
- Use variant={null} to avoid ghost button hover style clash in dark mode

* feat(claude): merge advanced fields into single collapsible section

- Merge API format, auth field, and model mapping into a unified "Advanced Options" collapsible
- Extend smart-expand logic to detect non-default values across all advanced fields
- Preserve model mapping sub-header and hint with a separator line
- Update zh/en/ja i18n keys (advancedOptionsToggle, advancedOptionsHint, modelMappingLabel, modelMappingHint)

* feat(copilot): add GitHub Copilot reverse proxy support

Add GitHub Copilot as a Claude provider variant with OAuth device code
authentication and Anthropic ↔ OpenAI format transformation.

Backend:
- Add CopilotAuthManager for GitHub OAuth device code flow
- Implement Copilot token auto-refresh (60s before expiry)
- Persist GitHub token to ~/.cc-switch/copilot_auth.json
- Add ProviderType::GitHubCopilot and AuthStrategy::GitHubCopilot
- Modify forwarder to use /chat/completions for Copilot
- Add Copilot-specific headers (Editor-Version, Editor-Plugin-Version)

Frontend:
- Add CopilotAuthSection component for OAuth UI
- Add useCopilotAuth hook for OAuth state management
- Auto-copy user code to clipboard and open browser
- Use 8-second polling interval to avoid GitHub rate limits
- Skip API Key validation for Copilot providers
- Add GitHub Copilot preset with claude-sonnet-4 model

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>

* fix(copilot): remove is_expired() calls from tests

Remove references to deleted is_expired() method in test code.
Only is_expiring_soon() is needed for token refresh logic.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>

* feat(copilot): add real-time model listing from Copilot API

- Add fetch_models() to CopilotAuthManager calling GET /models endpoint
- Add copilot_get_models Tauri command
- Add copilotGetModels() frontend API wrapper
- Modify ClaudeFormFields to show model dropdown for Copilot providers
  - Fetches available models on component mount when isCopilotPreset
  - Groups models by vendor (Anthropic, OpenAI, Google, etc.)
  - Input + dropdown button combo allows both manual entry and selection
  - Non-Copilot providers keep original plain Input behavior

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(copilot): add usage query integration

- Add Copilot usage API integration (fetch_usage method)
- Add copilot_get_usage Tauri command
- Add GitHub Copilot template in usage query modal
- Unify naming: copilot → github_copilot
- Add constants management (TEMPLATE_TYPES, PROVIDER_TYPES)
- Improve error handling with detailed error messages
- Add database migration (v5 → v6) for template type update
- Add i18n translations (zh, en, ja)
- Improve type safety with TemplateType
- Apply code formatting (cargo fmt, prettier)

* 修复github 登录和注销问题 ,模型选择问题

* feat(copilot): add multi-account support for GitHub Copilot

- Add multi-account storage structure with v1 to v2 migration
- Add per-account token caching and auto-refresh
- Add new Tauri commands for account management
- Integrate account selection in Proxy forwarder
- Add account selection UI in CopilotAuthSection
- Save githubAccountId to ProviderMeta
- Add i18n translations for multi-account features (zh/en/ja)

* 修复用量查询Reset字段出现多余字符

* refactor(auth-binding): introduce generic provider auth binding primitives

- add shared authBinding types in Rust and TypeScript while keeping githubAccountId as a compatibility field\n- resolve Copilot token, models, and usage through provider-bound account lookup instead of only the implicit default account\n- fix the Unix build regression in settings.rs by restoring std::io::Write for write_all()\n- remove the accidental .github ignore entry and drop leftover Copilot form debug logs\n- keep the first migration step non-breaking by writing both authBinding and the legacy githubAccountId field from the form

* refactor(auth-service): add managed auth command surface and explicit default account state

- introduce generic managed auth commands and frontend auth API wrappers for provider-scoped login, status, account listing, removal, logout, and default-account selection\n- store an explicit Copilot default_account_id instead of relying on HashMap iteration order, and use it consistently for fallback token/model/usage resolution\n- sort managed accounts deterministically and surface default-account state to the UI\n- refactor the Copilot form hook to wrap a generic useManagedAuth implementation while preserving the existing component contract\n- add default-account controls to the Copilot auth section and extend Copilot auth status serialization/tests for the new state

* feat(auth-center): add a dedicated settings entrypoint for managed OAuth accounts

- add an Auth Center tab to Settings so managed OAuth accounts are no longer hidden inside individual provider forms\n- introduce a first AuthCenterPanel that hosts GitHub Copilot account management as the initial managed auth provider\n- keep the provider form experience intact while establishing a global account-management surface for future providers such as OpenAI\n- validate that the new settings tab works cleanly with the generic managed auth hook and existing Copilot account controls

* feat(add-provider): expose managed OAuth sources alongside universal providers

- add an OAuth tab to the Add Provider flow so managed auth sources sit beside app-specific and universal providers\n- reuse the new Auth Center panel inside the dialog, keeping account management discoverable during provider creation\n- make the dialog footer adapt to the OAuth tab so account setup does not pretend to create a provider directly\n- align the add-provider UX with the new architecture where OAuth accounts are global assets and providers bind to them later

* fix(auth-reliability): harden managed auth persistence and refresh behavior

- replace direct Copilot auth store writes with private temp-file writes and atomic rename semantics, and document the local token storage limitation\n- add per-account refresh locks plus a double-check path so concurrent requests do not stampede GitHub token refresh\n- surface legacy migration failures through auth status, expose them in the UI, and add translated copy for the new account-state labels\n- stop writing the legacy githubAccountId field from the provider form while keeping compatibility reads in place\n- add logout error recovery and Copilot model-load toasts so auth failures are no longer silently swallowed

* refactor(copilot-detection): prefer provider type before URL fallbacks

- update forwarder endpoint rewriting to treat providerType as the primary GitHub Copilot signal\n- keep githubcopilot.com string matching only as a compatibility fallback for older provider records without providerType\n- reduce one more path where Copilot behavior depended purely on URL heuristics

* fix(copilot-auth): add cancel button to error state in CopilotAuthSection

- 错误状态下仅有"重试"按钮,用户无法退出(如不可恢复的 403 未订阅错误)
- 新增"取消"按钮,复用已有的 cancelAuth 逻辑重置为 idle 状态

* 修复打包后github账号头像显示异常

* 修复github copilot 来源的模型测试报错

* feat(copilot-preset): add default model presets for GitHub Copilot

- 补充 Copilot 预设的默认模型配置,用户选完预设即可直接使用
- ANTHROPIC_MODEL: claude-opus-4.6
- ANTHROPIC_DEFAULT_HAIKU_MODEL: claude-haiku-4.5
- ANTHROPIC_DEFAULT_SONNET_MODEL: claude-sonnet-4.6
- ANTHROPIC_DEFAULT_OPUS_MODEL: claude-opus-4.6

---------

Co-authored-by: Jason <farion1231@gmail.com>
Co-authored-by: 周梦泽 <mengze.zhou@dafeng-tech.com>
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
Zhou Mengze
2026-03-17 23:57:58 +08:00
committed by GitHub
parent 36bbdc36f5
commit 8ccfbd36d6
50 changed files with 4555 additions and 1062 deletions
+182
View File
@@ -0,0 +1,182 @@
use tauri::State;
use crate::commands::copilot::CopilotAuthState;
use crate::proxy::providers::copilot_auth::{GitHubAccount, GitHubDeviceCodeResponse};
const AUTH_PROVIDER_GITHUB_COPILOT: &str = "github_copilot";
#[derive(Debug, Clone, serde::Serialize)]
pub struct ManagedAuthAccount {
pub id: String,
pub provider: String,
pub login: String,
pub avatar_url: Option<String>,
pub authenticated_at: i64,
pub is_default: bool,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct ManagedAuthStatus {
pub provider: String,
pub authenticated: bool,
pub default_account_id: Option<String>,
pub migration_error: Option<String>,
pub accounts: Vec<ManagedAuthAccount>,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct ManagedAuthDeviceCodeResponse {
pub provider: String,
pub device_code: String,
pub user_code: String,
pub verification_uri: String,
pub expires_in: u64,
pub interval: u64,
}
fn ensure_auth_provider(auth_provider: &str) -> Result<&str, String> {
match auth_provider {
AUTH_PROVIDER_GITHUB_COPILOT => Ok(AUTH_PROVIDER_GITHUB_COPILOT),
_ => Err(format!("Unsupported auth provider: {auth_provider}")),
}
}
fn map_account(
provider: &str,
account: GitHubAccount,
default_account_id: Option<&str>,
) -> ManagedAuthAccount {
ManagedAuthAccount {
is_default: default_account_id == Some(account.id.as_str()),
id: account.id,
provider: provider.to_string(),
login: account.login,
avatar_url: account.avatar_url,
authenticated_at: account.authenticated_at,
}
}
fn map_device_code_response(
provider: &str,
response: GitHubDeviceCodeResponse,
) -> ManagedAuthDeviceCodeResponse {
ManagedAuthDeviceCodeResponse {
provider: provider.to_string(),
device_code: response.device_code,
user_code: response.user_code,
verification_uri: response.verification_uri,
expires_in: response.expires_in,
interval: response.interval,
}
}
#[tauri::command(rename_all = "camelCase")]
pub async fn auth_start_login(
auth_provider: String,
state: State<'_, CopilotAuthState>,
) -> Result<ManagedAuthDeviceCodeResponse, String> {
let auth_provider = ensure_auth_provider(&auth_provider)?;
let auth_manager = state.0.read().await;
let response = auth_manager
.start_device_flow()
.await
.map_err(|e| e.to_string())?;
Ok(map_device_code_response(auth_provider, response))
}
#[tauri::command(rename_all = "camelCase")]
pub async fn auth_poll_for_account(
auth_provider: String,
device_code: String,
state: State<'_, CopilotAuthState>,
) -> Result<Option<ManagedAuthAccount>, String> {
let auth_provider = ensure_auth_provider(&auth_provider)?;
let auth_manager = state.0.write().await;
match auth_manager.poll_for_token(&device_code).await {
Ok(account) => {
let default_account_id = auth_manager.get_status().await.default_account_id;
Ok(account
.map(|account| map_account(auth_provider, account, default_account_id.as_deref())))
}
Err(crate::proxy::providers::copilot_auth::CopilotAuthError::AuthorizationPending) => {
Ok(None)
}
Err(e) => Err(e.to_string()),
}
}
#[tauri::command(rename_all = "camelCase")]
pub async fn auth_list_accounts(
auth_provider: String,
state: State<'_, CopilotAuthState>,
) -> Result<Vec<ManagedAuthAccount>, String> {
let auth_provider = ensure_auth_provider(&auth_provider)?;
let auth_manager = state.0.read().await;
let status = auth_manager.get_status().await;
let default_account_id = status.default_account_id.clone();
Ok(status
.accounts
.into_iter()
.map(|account| map_account(auth_provider, account, default_account_id.as_deref()))
.collect())
}
#[tauri::command(rename_all = "camelCase")]
pub async fn auth_get_status(
auth_provider: String,
state: State<'_, CopilotAuthState>,
) -> Result<ManagedAuthStatus, String> {
let auth_provider = ensure_auth_provider(&auth_provider)?;
let auth_manager = state.0.read().await;
let status = auth_manager.get_status().await;
let default_account_id = status.default_account_id.clone();
Ok(ManagedAuthStatus {
provider: auth_provider.to_string(),
authenticated: status.authenticated,
default_account_id: default_account_id.clone(),
migration_error: status.migration_error,
accounts: status
.accounts
.into_iter()
.map(|account| map_account(auth_provider, account, default_account_id.as_deref()))
.collect(),
})
}
#[tauri::command(rename_all = "camelCase")]
pub async fn auth_remove_account(
auth_provider: String,
account_id: String,
state: State<'_, CopilotAuthState>,
) -> Result<(), String> {
ensure_auth_provider(&auth_provider)?;
let auth_manager = state.0.write().await;
auth_manager
.remove_account(&account_id)
.await
.map_err(|e| e.to_string())
}
#[tauri::command(rename_all = "camelCase")]
pub async fn auth_set_default_account(
auth_provider: String,
account_id: String,
state: State<'_, CopilotAuthState>,
) -> Result<(), String> {
ensure_auth_provider(&auth_provider)?;
let auth_manager = state.0.write().await;
auth_manager
.set_default_account(&account_id)
.await
.map_err(|e| e.to_string())
}
#[tauri::command(rename_all = "camelCase")]
pub async fn auth_logout(
auth_provider: String,
state: State<'_, CopilotAuthState>,
) -> Result<(), String> {
ensure_auth_provider(&auth_provider)?;
let auth_manager = state.0.write().await;
auth_manager.clear_auth().await.map_err(|e| e.to_string())
}
+212
View File
@@ -0,0 +1,212 @@
//! GitHub Copilot Tauri Commands
//!
//! 提供 Copilot OAuth 认证相关的 Tauri 命令,支持多账号管理。
use crate::proxy::providers::copilot_auth::{
CopilotAuthManager, CopilotAuthStatus, CopilotModel, CopilotUsageResponse, GitHubAccount,
GitHubDeviceCodeResponse,
};
use std::sync::Arc;
use tauri::State;
use tokio::sync::RwLock;
/// Copilot 认证状态
pub struct CopilotAuthState(pub Arc<RwLock<CopilotAuthManager>>);
// ==================== 设备码流程 ====================
/// 启动设备码流程
///
/// 返回设备码和用户码,用于 OAuth 认证
#[tauri::command]
pub async fn copilot_start_device_flow(
state: State<'_, CopilotAuthState>,
) -> Result<GitHubDeviceCodeResponse, String> {
let auth_manager = state.0.read().await;
auth_manager
.start_device_flow()
.await
.map_err(|e| e.to_string())
}
/// 轮询 OAuth Token(向后兼容)
///
/// 使用设备码轮询 GitHub,等待用户完成授权
/// 返回 true 表示授权成功,false 表示等待中
#[tauri::command(rename_all = "camelCase")]
pub async fn copilot_poll_for_auth(
device_code: String,
state: State<'_, CopilotAuthState>,
) -> Result<bool, String> {
let auth_manager = state.0.write().await;
match auth_manager.poll_for_token(&device_code).await {
Ok(Some(_account)) => {
log::info!("[CopilotAuth] 用户已授权");
Ok(true)
}
Ok(None) => Ok(false),
Err(crate::proxy::providers::copilot_auth::CopilotAuthError::AuthorizationPending) => {
Ok(false)
}
Err(e) => {
log::error!("[CopilotAuth] 轮询失败: {}", e);
Err(e.to_string())
}
}
}
/// 轮询 OAuth Token(多账号版本)
///
/// 返回新添加的账号信息,如果授权成功
#[tauri::command(rename_all = "camelCase")]
pub async fn copilot_poll_for_account(
device_code: String,
state: State<'_, CopilotAuthState>,
) -> Result<Option<GitHubAccount>, String> {
let auth_manager = state.0.write().await;
match auth_manager.poll_for_token(&device_code).await {
Ok(account) => Ok(account),
Err(crate::proxy::providers::copilot_auth::CopilotAuthError::AuthorizationPending) => {
Ok(None)
}
Err(e) => {
log::error!("[CopilotAuth] 轮询失败: {}", e);
Err(e.to_string())
}
}
}
// ==================== 多账号管理 ====================
/// 列出所有已认证的账号
#[tauri::command]
pub async fn copilot_list_accounts(
state: State<'_, CopilotAuthState>,
) -> Result<Vec<GitHubAccount>, String> {
let auth_manager = state.0.read().await;
Ok(auth_manager.list_accounts().await)
}
/// 移除指定账号
#[tauri::command(rename_all = "camelCase")]
pub async fn copilot_remove_account(
account_id: String,
state: State<'_, CopilotAuthState>,
) -> Result<(), String> {
let auth_manager = state.0.write().await;
auth_manager
.remove_account(&account_id)
.await
.map_err(|e| e.to_string())
}
/// 设置默认账号
#[tauri::command(rename_all = "camelCase")]
pub async fn copilot_set_default_account(
account_id: String,
state: State<'_, CopilotAuthState>,
) -> Result<(), String> {
let auth_manager = state.0.write().await;
auth_manager
.set_default_account(&account_id)
.await
.map_err(|e| e.to_string())
}
// ==================== 状态查询 ====================
/// 获取认证状态(包含所有账号)
#[tauri::command]
pub async fn copilot_get_auth_status(
state: State<'_, CopilotAuthState>,
) -> Result<CopilotAuthStatus, String> {
let auth_manager = state.0.read().await;
Ok(auth_manager.get_status().await)
}
/// 检查是否已认证(有任意账号)
#[tauri::command]
pub async fn copilot_is_authenticated(state: State<'_, CopilotAuthState>) -> Result<bool, String> {
let auth_manager = state.0.read().await;
Ok(auth_manager.is_authenticated().await)
}
/// 注销所有 Copilot 认证
#[tauri::command]
pub async fn copilot_logout(state: State<'_, CopilotAuthState>) -> Result<(), String> {
let auth_manager = state.0.write().await;
auth_manager.clear_auth().await.map_err(|e| e.to_string())
}
// ==================== Token 获取 ====================
/// 获取有效的 Copilot Token(向后兼容:使用第一个账号)
///
/// 内部使用,用于代理请求
#[tauri::command]
pub async fn copilot_get_token(state: State<'_, CopilotAuthState>) -> Result<String, String> {
let auth_manager = state.0.read().await;
auth_manager
.get_valid_token()
.await
.map_err(|e| e.to_string())
}
/// 获取指定账号的有效 Copilot Token
#[tauri::command(rename_all = "camelCase")]
pub async fn copilot_get_token_for_account(
account_id: String,
state: State<'_, CopilotAuthState>,
) -> Result<String, String> {
let auth_manager = state.0.read().await;
auth_manager
.get_valid_token_for_account(&account_id)
.await
.map_err(|e| e.to_string())
}
// ==================== 模型和使用量 ====================
/// 获取 Copilot 可用模型列表(向后兼容:使用第一个账号)
#[tauri::command]
pub async fn copilot_get_models(
state: State<'_, CopilotAuthState>,
) -> Result<Vec<CopilotModel>, String> {
let auth_manager = state.0.read().await;
auth_manager.fetch_models().await.map_err(|e| e.to_string())
}
/// 获取指定账号的 Copilot 可用模型列表
#[tauri::command(rename_all = "camelCase")]
pub async fn copilot_get_models_for_account(
account_id: String,
state: State<'_, CopilotAuthState>,
) -> Result<Vec<CopilotModel>, String> {
let auth_manager = state.0.read().await;
auth_manager
.fetch_models_for_account(&account_id)
.await
.map_err(|e| e.to_string())
}
/// 获取 Copilot 使用量信息(向后兼容:使用第一个账号)
#[tauri::command]
pub async fn copilot_get_usage(
state: State<'_, CopilotAuthState>,
) -> Result<CopilotUsageResponse, String> {
let auth_manager = state.0.read().await;
auth_manager.fetch_usage().await.map_err(|e| e.to_string())
}
/// 获取指定账号的 Copilot 使用量信息
#[tauri::command(rename_all = "camelCase")]
pub async fn copilot_get_usage_for_account(
account_id: String,
state: State<'_, CopilotAuthState>,
) -> Result<CopilotUsageResponse, String> {
let auth_manager = state.0.read().await;
auth_manager
.fetch_usage_for_account(&account_id)
.await
.map_err(|e| e.to_string())
}
+6 -2
View File
@@ -1,6 +1,8 @@
#![allow(non_snake_case)]
mod auth;
mod config;
mod copilot;
mod deeplink;
mod env;
mod failover;
@@ -19,12 +21,14 @@ mod settings;
pub mod skill;
mod stream_check;
mod sync_support;
mod toolsearch;
mod usage;
mod webdav_sync;
mod workspace;
pub use auth::*;
pub use config::*;
pub use copilot::*;
pub use deeplink::*;
pub use env::*;
pub use failover::*;
@@ -42,7 +46,7 @@ pub use session_manager::*;
pub use settings::*;
pub use skill::*;
pub use stream_check::*;
pub use toolsearch::*;
pub use usage::*;
pub use webdav_sync::*;
pub use workspace::*;
+61
View File
@@ -2,6 +2,7 @@ use indexmap::IndexMap;
use tauri::State;
use crate::app_config::AppType;
use crate::commands::copilot::CopilotAuthState;
use crate::error::AppError;
use crate::provider::Provider;
use crate::services::{
@@ -10,6 +11,11 @@ use crate::services::{
use crate::store::AppState;
use std::str::FromStr;
// 常量定义
const TEMPLATE_TYPE_GITHUB_COPILOT: &str = "github_copilot";
const COPILOT_UNIT_PREMIUM: &str = "requests";
/// 获取所有供应商
#[tauri::command]
pub fn get_providers(
state: State<'_, AppState>,
@@ -142,10 +148,65 @@ pub fn import_default_config(state: State<'_, AppState>, app: String) -> Result<
#[tauri::command]
pub async fn queryProviderUsage(
state: State<'_, AppState>,
copilot_state: State<'_, CopilotAuthState>,
#[allow(non_snake_case)] providerId: String, // 使用 camelCase 匹配前端
app: String,
) -> Result<crate::provider::UsageResult, String> {
let app_type = AppType::from_str(&app).map_err(|e| e.to_string())?;
// 检查是否为 GitHub Copilot 模板类型,并解析绑定账号
let (is_copilot_template, copilot_account_id) = {
let providers = state
.db
.get_all_providers(app_type.as_str())
.map_err(|e| format!("Failed to get providers: {}", e))?;
let provider = providers.get(&providerId);
let is_copilot = provider
.and_then(|p| p.meta.as_ref())
.and_then(|m| m.usage_script.as_ref())
.and_then(|s| s.template_type.as_ref())
.map(|t| t == TEMPLATE_TYPE_GITHUB_COPILOT)
.unwrap_or(false);
let account_id = provider
.and_then(|p| p.meta.as_ref())
.and_then(|m| m.managed_account_id_for(TEMPLATE_TYPE_GITHUB_COPILOT));
(is_copilot, account_id)
};
if is_copilot_template {
// 使用 Copilot 专用 API
let auth_manager = copilot_state.0.read().await;
let usage = match copilot_account_id.as_deref() {
Some(account_id) => auth_manager
.fetch_usage_for_account(account_id)
.await
.map_err(|e| format!("Failed to fetch Copilot usage: {}", e))?,
None => auth_manager
.fetch_usage()
.await
.map_err(|e| format!("Failed to fetch Copilot usage: {}", e))?,
};
let premium = &usage.quota_snapshots.premium_interactions;
let used = premium.entitlement - premium.remaining;
return Ok(crate::provider::UsageResult {
success: true,
data: Some(vec![crate::provider::UsageData {
plan_name: Some(usage.copilot_plan),
remaining: Some(premium.remaining as f64),
total: Some(premium.entitlement as f64),
used: Some(used as f64),
unit: Some(COPILOT_UNIT_PREMIUM.to_string()),
is_valid: Some(true),
invalid_message: None,
extra: Some(format!("Reset: {}", usage.quota_reset_date)),
}]),
error: None,
});
}
ProviderService::query_usage(state.inner(), app_type, &providerId)
.await
.map_err(|e| e.to_string())
+63 -13
View File
@@ -1,6 +1,7 @@
//! 流式健康检查命令
use crate::app_config::AppType;
use crate::commands::copilot::CopilotAuthState;
use crate::error::AppError;
use crate::services::stream_check::{
HealthStatus, StreamCheckConfig, StreamCheckResult, StreamCheckService,
@@ -13,6 +14,7 @@ use tauri::State;
#[tauri::command]
pub async fn stream_check_provider(
state: State<'_, AppState>,
copilot_state: State<'_, CopilotAuthState>,
app_type: AppType,
provider_id: String,
) -> Result<StreamCheckResult, AppError> {
@@ -23,7 +25,9 @@ pub async fn stream_check_provider(
.get(&provider_id)
.ok_or_else(|| AppError::Message(format!("供应商 {provider_id} 不存在")))?;
let result = StreamCheckService::check_with_retry(&app_type, provider, &config).await?;
let auth_override = resolve_copilot_auth_override(provider, &copilot_state).await?;
let result =
StreamCheckService::check_with_retry(&app_type, provider, &config, auth_override).await?;
// 记录日志
let _ =
@@ -38,6 +42,7 @@ pub async fn stream_check_provider(
#[tauri::command]
pub async fn stream_check_all_providers(
state: State<'_, AppState>,
copilot_state: State<'_, CopilotAuthState>,
app_type: AppType,
proxy_targets_only: bool,
) -> Result<Vec<(String, StreamCheckResult)>, AppError> {
@@ -67,18 +72,20 @@ pub async fn stream_check_all_providers(
}
}
let result = StreamCheckService::check_with_retry(&app_type, &provider, &config)
.await
.unwrap_or_else(|e| StreamCheckResult {
status: HealthStatus::Failed,
success: false,
message: e.to_string(),
response_time_ms: None,
http_status: None,
model_used: String::new(),
tested_at: chrono::Utc::now().timestamp(),
retry_count: 0,
});
let auth_override = resolve_copilot_auth_override(&provider, &copilot_state).await?;
let result =
StreamCheckService::check_with_retry(&app_type, &provider, &config, auth_override)
.await
.unwrap_or_else(|e| StreamCheckResult {
status: HealthStatus::Failed,
success: false,
message: e.to_string(),
response_time_ms: None,
http_status: None,
model_used: String::new(),
tested_at: chrono::Utc::now().timestamp(),
retry_count: 0,
});
let _ = state
.db
@@ -104,3 +111,46 @@ pub fn save_stream_check_config(
) -> Result<(), AppError> {
state.db.save_stream_check_config(&config)
}
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);
if !is_copilot {
return Ok(None);
}
let auth_manager = copilot_state.0.read().await;
let account_id = provider
.meta
.as_ref()
.and_then(|meta| meta.github_account_id.clone());
let token = match account_id.as_deref() {
Some(id) => auth_manager
.get_valid_token_for_account(id)
.await
.map_err(|e| AppError::Message(format!("GitHub Copilot 认证失败: {e}")))?,
None => auth_manager
.get_valid_token()
.await
.map_err(|e| AppError::Message(format!("GitHub Copilot 认证失败: {e}")))?,
};
Ok(Some(crate::proxy::providers::AuthInfo::new(
token,
crate::proxy::providers::AuthStrategy::GitHubCopilot,
)))
}
-21
View File
@@ -1,21 +0,0 @@
#![allow(non_snake_case)]
/// Check Tool Search patch status for the active Claude Code installation
#[tauri::command]
pub async fn check_toolsearch_status() -> Result<crate::toolsearch_patch::ToolSearchStatus, String>
{
crate::toolsearch_patch::check_toolsearch_status().map_err(|e| e.to_string())
}
/// Apply Tool Search patch (bypass domain restriction) to the active installation
#[tauri::command]
pub async fn apply_toolsearch_patch() -> Result<Vec<crate::toolsearch_patch::PatchResult>, String> {
crate::toolsearch_patch::apply_toolsearch_patch().map_err(|e| e.to_string())
}
/// Restore Tool Search patch (re-enable domain restriction) for the active installation
#[tauri::command]
pub async fn restore_toolsearch_patch() -> Result<Vec<crate::toolsearch_patch::PatchResult>, String>
{
crate::toolsearch_patch::restore_toolsearch_patch().map_err(|e| e.to_string())
}