feat(copilot): add GitHub Enterprise Server support (#2175)

* feat(copilot): add GitHub Enterprise Server support

* fix(copilot): address GHES PR review findings (P1 + 2×P2)

- P1: Use composite account ID (domain:user_id) for GHES to prevent
  cross-instance ID collisions; github.com keeps plain numeric ID for
  backward compatibilit
- P2-a: Use get_api_endpoint() for model list URL with automatic
  fallback to static URL when dynamic endpoint resolution fails
- P2-b: Add normalize_github_domain() as backend SSOT for domain
  normalization (lowercase, strip protocol/path/query, reject userinfo)

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

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
hotelbe
2026-04-19 20:29:46 +08:00
committed by GitHub
parent 9871d3d1eb
commit 87635e7fc6
12 changed files with 441 additions and 68 deletions
+9 -2
View File
@@ -18,6 +18,7 @@ pub struct ManagedAuthAccount {
pub avatar_url: Option<String>,
pub authenticated_at: i64,
pub is_default: bool,
pub github_domain: String,
}
#[derive(Debug, Clone, serde::Serialize)]
@@ -59,6 +60,7 @@ fn map_account(
login: account.login,
avatar_url: account.avatar_url,
authenticated_at: account.authenticated_at,
github_domain: account.github_domain,
}
}
@@ -79,6 +81,7 @@ fn map_device_code_response(
#[tauri::command(rename_all = "camelCase")]
pub async fn auth_start_login(
auth_provider: String,
github_domain: Option<String>,
copilot_state: State<'_, CopilotAuthState>,
codex_state: State<'_, CodexOAuthState>,
) -> Result<ManagedAuthDeviceCodeResponse, String> {
@@ -87,7 +90,7 @@ pub async fn auth_start_login(
AUTH_PROVIDER_GITHUB_COPILOT => {
let auth_manager = copilot_state.0.read().await;
let response = auth_manager
.start_device_flow()
.start_device_flow(github_domain.as_deref())
.await
.map_err(|e| e.to_string())?;
Ok(map_device_code_response(auth_provider, response))
@@ -108,6 +111,7 @@ pub async fn auth_start_login(
pub async fn auth_poll_for_account(
auth_provider: String,
device_code: String,
github_domain: Option<String>,
copilot_state: State<'_, CopilotAuthState>,
codex_state: State<'_, CodexOAuthState>,
) -> Result<Option<ManagedAuthAccount>, String> {
@@ -115,7 +119,10 @@ pub async fn auth_poll_for_account(
match auth_provider {
AUTH_PROVIDER_GITHUB_COPILOT => {
let auth_manager = copilot_state.0.write().await;
match auth_manager.poll_for_token(&device_code).await {
match auth_manager
.poll_for_token(&device_code, github_domain.as_deref())
.await
{
Ok(account) => {
let default_account_id = auth_manager.get_status().await.default_account_id;
Ok(account.map(|account| {
+12 -3
View File
@@ -20,11 +20,12 @@ pub struct CopilotAuthState(pub Arc<RwLock<CopilotAuthManager>>);
/// 返回设备码和用户码,用于 OAuth 认证
#[tauri::command]
pub async fn copilot_start_device_flow(
github_domain: Option<String>,
state: State<'_, CopilotAuthState>,
) -> Result<GitHubDeviceCodeResponse, String> {
let auth_manager = state.0.read().await;
auth_manager
.start_device_flow()
.start_device_flow(github_domain.as_deref())
.await
.map_err(|e| e.to_string())
}
@@ -36,10 +37,14 @@ pub async fn copilot_start_device_flow(
#[tauri::command(rename_all = "camelCase")]
pub async fn copilot_poll_for_auth(
device_code: String,
github_domain: Option<String>,
state: State<'_, CopilotAuthState>,
) -> Result<bool, String> {
let auth_manager = state.0.write().await;
match auth_manager.poll_for_token(&device_code).await {
match auth_manager
.poll_for_token(&device_code, github_domain.as_deref())
.await
{
Ok(Some(_account)) => {
log::info!("[CopilotAuth] 用户已授权");
Ok(true)
@@ -61,10 +66,14 @@ pub async fn copilot_poll_for_auth(
#[tauri::command(rename_all = "camelCase")]
pub async fn copilot_poll_for_account(
device_code: String,
github_domain: Option<String>,
state: State<'_, CopilotAuthState>,
) -> Result<Option<GitHubAccount>, String> {
let auth_manager = state.0.write().await;
match auth_manager.poll_for_token(&device_code).await {
match auth_manager
.poll_for_token(&device_code, github_domain.as_deref())
.await
{
Ok(account) => Ok(account),
Err(crate::proxy::providers::copilot_auth::CopilotAuthError::AuthorizationPending) => {
Ok(None)