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
+101
View File
@@ -0,0 +1,101 @@
import { invoke } from "@tauri-apps/api/core";
export type ManagedAuthProvider = "github_copilot";
export interface ManagedAuthAccount {
id: string;
provider: ManagedAuthProvider;
login: string;
avatar_url: string | null;
authenticated_at: number;
is_default: boolean;
}
export interface ManagedAuthStatus {
provider: ManagedAuthProvider;
authenticated: boolean;
default_account_id: string | null;
migration_error?: string | null;
accounts: ManagedAuthAccount[];
}
export interface ManagedAuthDeviceCodeResponse {
provider: ManagedAuthProvider;
device_code: string;
user_code: string;
verification_uri: string;
expires_in: number;
interval: number;
}
export async function authStartLogin(
authProvider: ManagedAuthProvider,
): Promise<ManagedAuthDeviceCodeResponse> {
return invoke<ManagedAuthDeviceCodeResponse>("auth_start_login", {
authProvider,
});
}
export async function authPollForAccount(
authProvider: ManagedAuthProvider,
deviceCode: string,
): Promise<ManagedAuthAccount | null> {
return invoke<ManagedAuthAccount | null>("auth_poll_for_account", {
authProvider,
deviceCode,
});
}
export async function authListAccounts(
authProvider: ManagedAuthProvider,
): Promise<ManagedAuthAccount[]> {
return invoke<ManagedAuthAccount[]>("auth_list_accounts", {
authProvider,
});
}
export async function authGetStatus(
authProvider: ManagedAuthProvider,
): Promise<ManagedAuthStatus> {
return invoke<ManagedAuthStatus>("auth_get_status", {
authProvider,
});
}
export async function authRemoveAccount(
authProvider: ManagedAuthProvider,
accountId: string,
): Promise<void> {
return invoke("auth_remove_account", {
authProvider,
accountId,
});
}
export async function authSetDefaultAccount(
authProvider: ManagedAuthProvider,
accountId: string,
): Promise<void> {
return invoke("auth_set_default_account", {
authProvider,
accountId,
});
}
export async function authLogout(
authProvider: ManagedAuthProvider,
): Promise<void> {
return invoke("auth_logout", {
authProvider,
});
}
export const authApi = {
authStartLogin,
authPollForAccount,
authListAccounts,
authGetStatus,
authRemoveAccount,
authSetDefaultAccount,
authLogout,
};
+256
View File
@@ -0,0 +1,256 @@
/**
* GitHub Copilot OAuth API
*
* 提供 GitHub Copilot OAuth 设备码流程相关的 API 函数。
* 支持多账号管理。
*/
import { invoke } from "@tauri-apps/api/core";
/**
* GitHub 设备码响应
*/
export interface CopilotDeviceCodeResponse {
device_code: string;
user_code: string;
verification_uri: string;
expires_in: number;
interval: number;
}
/**
* GitHub 账号信息(公开信息)
*/
export interface GitHubAccount {
/** GitHub 用户 ID(唯一标识) */
id: string;
/** GitHub 用户名 */
login: string;
/** 头像 URL */
avatar_url: string | null;
/** 认证时间戳(Unix 秒) */
authenticated_at: number;
}
/**
* Copilot 认证状态(多账号版本)
*/
export interface CopilotAuthStatus {
/** 是否已认证(有任意账号)- 向后兼容 */
authenticated: boolean;
/** 默认账号 ID */
default_account_id: string | null;
/** 旧认证数据迁移失败时的状态消息 */
migration_error?: string | null;
/** 第一个账号的用户名 - 向后兼容 */
username: string | null;
/** Copilot Token 过期时间 - 向后兼容 */
expires_at: number | null;
/** 所有已认证账号列表 */
accounts: GitHubAccount[];
}
/**
* 启动 GitHub OAuth 设备码流程
*
* @returns 设备码响应,包含用户码和验证 URL
*/
export async function copilotStartDeviceFlow(): Promise<CopilotDeviceCodeResponse> {
return invoke<CopilotDeviceCodeResponse>("copilot_start_device_flow");
}
/**
* 轮询 OAuth Token
*
* 使用设备码轮询 GitHub,等待用户完成授权。
*
* @param deviceCode - 设备码
* @returns true 表示认证成功,false 表示仍在等待用户授权
*/
export async function copilotPollForAuth(deviceCode: string): Promise<boolean> {
return invoke<boolean>("copilot_poll_for_auth", {
deviceCode,
});
}
/**
* 获取 Copilot 认证状态
*
* @returns 认证状态,包含是否已认证、用户名和过期时间
*/
export async function copilotGetAuthStatus(): Promise<CopilotAuthStatus> {
return invoke<CopilotAuthStatus>("copilot_get_auth_status");
}
/**
* 注销 Copilot 认证
*/
export async function copilotLogout(): Promise<void> {
return invoke("copilot_logout");
}
/**
* 检查是否已认证
*
* @returns true 表示已认证
*/
export async function copilotIsAuthenticated(): Promise<boolean> {
return invoke<boolean>("copilot_is_authenticated");
}
/**
* Copilot 可用模型
*/
export interface CopilotModel {
id: string;
name: string;
vendor: string;
model_picker_enabled: boolean;
}
/**
* 获取有效的 Copilot Token
*
* 内部使用,用于代理请求。
*
* @returns Copilot Token
*/
export async function copilotGetToken(): Promise<string> {
return invoke<string>("copilot_get_token");
}
/**
* 获取 Copilot 可用模型列表
*
* @returns 可用模型列表
*/
export async function copilotGetModels(): Promise<CopilotModel[]> {
return invoke<CopilotModel[]>("copilot_get_models");
}
/**
* 配额详情
*/
export interface QuotaDetail {
entitlement: number;
remaining: number;
percent_remaining: number;
unlimited: boolean;
}
/**
* 配额快照
*/
export interface QuotaSnapshots {
chat: QuotaDetail;
completions: QuotaDetail;
premium_interactions: QuotaDetail;
}
/**
* Copilot 使用量响应
*/
export interface CopilotUsageResponse {
copilot_plan: string;
quota_reset_date: string;
quota_snapshots: QuotaSnapshots;
}
/**
* 获取 Copilot 使用量信息
*
* @returns 使用量信息,包含计划类型、重置日期和配额快照
*/
export async function copilotGetUsage(): Promise<CopilotUsageResponse> {
return invoke<CopilotUsageResponse>("copilot_get_usage");
}
// ==================== 多账号管理 API ====================
/**
* 列出所有已认证的 GitHub 账号
*
* @returns 账号列表
*/
export async function copilotListAccounts(): Promise<GitHubAccount[]> {
return invoke<GitHubAccount[]>("copilot_list_accounts");
}
/**
* 轮询 OAuth Token(多账号版本)
*
* 使用设备码轮询 GitHub,等待用户完成授权。
* 授权成功后返回新添加的账号信息。
*
* @param deviceCode - 设备码
* @returns 新添加的账号信息,如果仍在等待则返回 null
*/
export async function copilotPollForAccount(
deviceCode: string,
): Promise<GitHubAccount | null> {
return invoke<GitHubAccount | null>("copilot_poll_for_account", {
deviceCode,
});
}
/**
* 移除指定的 GitHub 账号
*
* @param accountId - GitHub 用户 ID
*/
export async function copilotRemoveAccount(accountId: string): Promise<void> {
return invoke("copilot_remove_account", { accountId });
}
/**
* 设置默认 GitHub 账号
*
* @param accountId - GitHub 用户 ID
*/
export async function copilotSetDefaultAccount(
accountId: string,
): Promise<void> {
return invoke("copilot_set_default_account", { accountId });
}
/**
* 获取指定账号的有效 Copilot Token
*
* 内部使用,用于代理请求。
*
* @param accountId - GitHub 用户 ID
* @returns Copilot Token
*/
export async function copilotGetTokenForAccount(
accountId: string,
): Promise<string> {
return invoke<string>("copilot_get_token_for_account", { accountId });
}
/**
* 获取指定账号的 Copilot 可用模型列表
*
* @param accountId - GitHub 用户 ID
* @returns 可用模型列表
*/
export async function copilotGetModelsForAccount(
accountId: string,
): Promise<CopilotModel[]> {
return invoke<CopilotModel[]>("copilot_get_models_for_account", {
accountId,
});
}
/**
* 获取指定账号的 Copilot 使用量信息
*
* @param accountId - GitHub 用户 ID
* @returns 使用量信息
*/
export async function copilotGetUsageForAccount(
accountId: string,
): Promise<CopilotUsageResponse> {
return invoke<CopilotUsageResponse>("copilot_get_usage_for_account", {
accountId,
});
}
+13
View File
@@ -12,5 +12,18 @@ export { openclawApi } from "./openclaw";
export { sessionsApi } from "./sessions";
export { workspaceApi } from "./workspace";
export * as configApi from "./config";
export * as authApi from "./auth";
export * as copilotApi from "./copilot";
export type { ProviderSwitchEvent } from "./providers";
export type { Prompt } from "./prompts";
export type {
CopilotDeviceCodeResponse,
CopilotAuthStatus,
GitHubAccount,
} from "./copilot";
export type {
ManagedAuthProvider,
ManagedAuthAccount,
ManagedAuthStatus,
ManagedAuthDeviceCodeResponse,
} from "./auth";
-12
View File
@@ -86,18 +86,6 @@ export const settingsApi = {
return await invoke("clear_claude_onboarding_skip");
},
async applyToolSearchPatch(): Promise<
Array<{ path: string; success: boolean; error?: string }>
> {
return await invoke("apply_toolsearch_patch");
},
async restoreToolSearchPatch(): Promise<
Array<{ path: string; success: boolean; error?: string }>
> {
return await invoke("restore_toolsearch_patch");
},
async saveFileDialog(defaultName: string): Promise<string | null> {
return await invoke("save_file_dialog", { defaultName });
},
+2 -1
View File
@@ -12,6 +12,7 @@ import type {
} from "@/types/usage";
import type { UsageResult } from "@/types";
import type { AppId } from "./types";
import type { TemplateType } from "@/config/constants";
export const usageApi = {
// Provider usage script methods
@@ -28,7 +29,7 @@ export const usageApi = {
baseUrl?: string,
accessToken?: string,
userId?: string,
templateType?: "custom" | "general" | "newapi",
templateType?: TemplateType,
): Promise<UsageResult> => {
return invoke("testUsageScript", {
providerId,
+21
View File
@@ -0,0 +1,21 @@
import type { ProviderMeta } from "@/types";
export function resolveManagedAccountId(
meta: ProviderMeta | undefined,
authProvider: string,
): string | null {
const binding = meta?.authBinding;
if (
binding?.source === "managed_account" &&
binding.authProvider === authProvider
) {
return binding.accountId ?? null;
}
if (authProvider === "github_copilot") {
return meta?.githubAccountId ?? null;
}
return null;
}