mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-27 08:14:33 +08:00
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:
@@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -45,6 +46,19 @@ export const CopilotAuthSection: React.FC<CopilotAuthSectionProps> = ({
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [copied, setCopied] = React.useState(false);
|
||||
const [deploymentType, setDeploymentType] = React.useState<
|
||||
"github.com" | "enterprise"
|
||||
>("github.com");
|
||||
const [enterpriseDomain, setEnterpriseDomain] = React.useState("");
|
||||
|
||||
// 根据部署类型计算实际的 GitHub 域名
|
||||
const effectiveGithubDomain =
|
||||
deploymentType === "enterprise" && enterpriseDomain.trim()
|
||||
? enterpriseDomain
|
||||
.trim()
|
||||
.replace(/^https?:\/\//, "")
|
||||
.replace(/\/$/, "")
|
||||
: undefined;
|
||||
|
||||
const {
|
||||
accounts,
|
||||
@@ -63,7 +77,7 @@ export const CopilotAuthSection: React.FC<CopilotAuthSectionProps> = ({
|
||||
setDefaultAccount,
|
||||
cancelAuth,
|
||||
logout,
|
||||
} = useCopilotAuth();
|
||||
} = useCopilotAuth(effectiveGithubDomain);
|
||||
|
||||
// 复制用户码
|
||||
const copyUserCode = async () => {
|
||||
@@ -113,6 +127,41 @@ export const CopilotAuthSection: React.FC<CopilotAuthSectionProps> = ({
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* GitHub 部署类型选择 */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm text-muted-foreground">
|
||||
{t("copilot.deploymentType", "GitHub 部署类型")}
|
||||
</Label>
|
||||
<Select
|
||||
value={deploymentType}
|
||||
onValueChange={(v) =>
|
||||
setDeploymentType(v as "github.com" | "enterprise")
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="github.com">
|
||||
{t("copilot.deploymentGitHubCom", "GitHub.com")}
|
||||
</SelectItem>
|
||||
<SelectItem value="enterprise">
|
||||
{t("copilot.deploymentEnterprise", "GitHub Enterprise Server")}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{deploymentType === "enterprise" && (
|
||||
<Input
|
||||
placeholder={t(
|
||||
"copilot.enterpriseDomainPlaceholder",
|
||||
"例如:company.ghe.com",
|
||||
)}
|
||||
value={enterpriseDomain}
|
||||
onChange={(e) => setEnterpriseDomain(e.target.value)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{migrationError && (
|
||||
<p className="text-sm text-amber-600 dark:text-amber-400">
|
||||
{t("copilot.migrationFailed", {
|
||||
@@ -179,6 +228,12 @@ export const CopilotAuthSection: React.FC<CopilotAuthSectionProps> = ({
|
||||
{t("copilot.defaultAccount", "默认")}
|
||||
</Badge>
|
||||
)}
|
||||
{account.github_domain &&
|
||||
account.github_domain !== "github.com" && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{account.github_domain}
|
||||
</Badge>
|
||||
)}
|
||||
{selectedAccountId === account.id && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{t("copilot.selected", "已选中")}
|
||||
@@ -223,6 +278,7 @@ export const CopilotAuthSection: React.FC<CopilotAuthSectionProps> = ({
|
||||
onClick={addAccount}
|
||||
className="w-full"
|
||||
variant="outline"
|
||||
disabled={deploymentType === "enterprise" && !enterpriseDomain.trim()}
|
||||
>
|
||||
<Github className="mr-2 h-4 w-4" />
|
||||
{t("copilot.loginWithGitHub", "使用 GitHub 登录")}
|
||||
@@ -236,7 +292,10 @@ export const CopilotAuthSection: React.FC<CopilotAuthSectionProps> = ({
|
||||
onClick={addAccount}
|
||||
className="w-full"
|
||||
variant="outline"
|
||||
disabled={isAddingAccount}
|
||||
disabled={
|
||||
isAddingAccount ||
|
||||
(deploymentType === "enterprise" && !enterpriseDomain.trim())
|
||||
}
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
{t("copilot.addAnotherAccount", "添加其他账号")}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { GitHubAccount } from "@/lib/api";
|
||||
import { useManagedAuth } from "./useManagedAuth";
|
||||
|
||||
export function useCopilotAuth() {
|
||||
const managedAuth = useManagedAuth("github_copilot");
|
||||
export function useCopilotAuth(githubDomain?: string) {
|
||||
const managedAuth = useManagedAuth("github_copilot", githubDomain);
|
||||
const defaultAccount =
|
||||
managedAuth.accounts.find(
|
||||
(account) => account.id === managedAuth.defaultAccountId,
|
||||
|
||||
@@ -10,7 +10,10 @@ import type {
|
||||
|
||||
type PollingState = "idle" | "polling" | "success" | "error";
|
||||
|
||||
export function useManagedAuth(authProvider: ManagedAuthProvider) {
|
||||
export function useManagedAuth(
|
||||
authProvider: ManagedAuthProvider,
|
||||
githubDomain?: string,
|
||||
) {
|
||||
const queryClient = useQueryClient();
|
||||
const queryKey = ["managed-auth-status", authProvider];
|
||||
|
||||
@@ -52,7 +55,7 @@ export function useManagedAuth(authProvider: ManagedAuthProvider) {
|
||||
}, [stopPolling]);
|
||||
|
||||
const startLoginMutation = useMutation({
|
||||
mutationFn: () => authApi.authStartLogin(authProvider),
|
||||
mutationFn: () => authApi.authStartLogin(authProvider, githubDomain),
|
||||
onSuccess: async (response) => {
|
||||
setDeviceCode(response);
|
||||
setPollingState("polling");
|
||||
@@ -87,6 +90,7 @@ export function useManagedAuth(authProvider: ManagedAuthProvider) {
|
||||
const newAccount = await authApi.authPollForAccount(
|
||||
authProvider,
|
||||
response.device_code,
|
||||
githubDomain,
|
||||
);
|
||||
if (newAccount) {
|
||||
stopPolling();
|
||||
|
||||
@@ -889,7 +889,11 @@
|
||||
"retry": "Retry",
|
||||
"copyCode": "Copy code",
|
||||
"migrationFailed": "Legacy auth migration failed: {{error}}",
|
||||
"loadModelsFailed": "Failed to load Copilot models"
|
||||
"loadModelsFailed": "Failed to load Copilot models",
|
||||
"deploymentType": "GitHub Deployment Type",
|
||||
"deploymentGitHubCom": "GitHub.com",
|
||||
"deploymentEnterprise": "GitHub Enterprise Server",
|
||||
"enterpriseDomainPlaceholder": "e.g. company.ghe.com"
|
||||
},
|
||||
"codexOauth": {
|
||||
"authStatus": "Auth status",
|
||||
|
||||
@@ -889,7 +889,11 @@
|
||||
"retry": "再試行",
|
||||
"copyCode": "コードをコピー",
|
||||
"migrationFailed": "旧認証データの移行に失敗しました: {{error}}",
|
||||
"loadModelsFailed": "Copilot モデル一覧の読み込みに失敗しました"
|
||||
"loadModelsFailed": "Copilot モデル一覧の読み込みに失敗しました",
|
||||
"deploymentType": "GitHub デプロイメントタイプ",
|
||||
"deploymentGitHubCom": "GitHub.com",
|
||||
"deploymentEnterprise": "GitHub Enterprise Server",
|
||||
"enterpriseDomainPlaceholder": "例: company.ghe.com"
|
||||
},
|
||||
"codexOauth": {
|
||||
"authStatus": "認証状態",
|
||||
|
||||
@@ -890,7 +890,11 @@
|
||||
"retry": "重试",
|
||||
"copyCode": "复制代码",
|
||||
"migrationFailed": "旧认证数据迁移失败:{{error}}",
|
||||
"loadModelsFailed": "加载 Copilot 模型列表失败"
|
||||
"loadModelsFailed": "加载 Copilot 模型列表失败",
|
||||
"deploymentType": "GitHub 部署类型",
|
||||
"deploymentGitHubCom": "GitHub.com",
|
||||
"deploymentEnterprise": "GitHub Enterprise Server",
|
||||
"enterpriseDomainPlaceholder": "例如:company.ghe.com"
|
||||
},
|
||||
"codexOauth": {
|
||||
"authStatus": "认证状态",
|
||||
|
||||
@@ -9,6 +9,7 @@ export interface ManagedAuthAccount {
|
||||
avatar_url: string | null;
|
||||
authenticated_at: number;
|
||||
is_default: boolean;
|
||||
github_domain: string;
|
||||
}
|
||||
|
||||
export interface ManagedAuthStatus {
|
||||
@@ -30,19 +31,23 @@ export interface ManagedAuthDeviceCodeResponse {
|
||||
|
||||
export async function authStartLogin(
|
||||
authProvider: ManagedAuthProvider,
|
||||
githubDomain?: string,
|
||||
): Promise<ManagedAuthDeviceCodeResponse> {
|
||||
return invoke<ManagedAuthDeviceCodeResponse>("auth_start_login", {
|
||||
authProvider,
|
||||
githubDomain: githubDomain || null,
|
||||
});
|
||||
}
|
||||
|
||||
export async function authPollForAccount(
|
||||
authProvider: ManagedAuthProvider,
|
||||
deviceCode: string,
|
||||
githubDomain?: string,
|
||||
): Promise<ManagedAuthAccount | null> {
|
||||
return invoke<ManagedAuthAccount | null>("auth_poll_for_account", {
|
||||
authProvider,
|
||||
deviceCode,
|
||||
githubDomain: githubDomain || null,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,8 @@ export interface GitHubAccount {
|
||||
avatar_url: string | null;
|
||||
/** 认证时间戳(Unix 秒) */
|
||||
authenticated_at: number;
|
||||
/** GitHub 域名(github.com 或 GHES 域名) */
|
||||
github_domain: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user