mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-06-16 13:34:04 +08:00
Consolidate managed account entry points
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
||||
ProviderForm,
|
||||
type ProviderFormValues,
|
||||
} from "@/components/providers/forms/ProviderForm";
|
||||
import { AuthSettingsPanel } from "@/components/providers/AuthSettingsPanel";
|
||||
import { UniversalProviderFormModal } from "@/components/universal/UniversalProviderFormModal";
|
||||
import { UniversalProviderPanel } from "@/components/universal";
|
||||
import { providerPresets } from "@/config/claudeProviderPresets";
|
||||
@@ -21,6 +22,7 @@ import { claudeDesktopProviderPresets } from "@/config/claudeDesktopProviderPres
|
||||
import { extractCodexBaseUrl } from "@/utils/providerConfigUtils";
|
||||
import type { OpenClawSuggestedDefaults } from "@/config/openclawProviderPresets";
|
||||
import type { UniversalProviderPreset } from "@/config/universalProviderPresets";
|
||||
import type { ManagedAuthProvider } from "@/lib/api";
|
||||
|
||||
interface AddProviderDialogProps {
|
||||
open: boolean;
|
||||
@@ -55,6 +57,21 @@ export function AddProviderDialog({
|
||||
const [selectedUniversalPreset, setSelectedUniversalPreset] =
|
||||
useState<UniversalProviderPreset | null>(null);
|
||||
const [isFormSubmitting, setIsFormSubmitting] = useState(false);
|
||||
const [authSettingsTarget, setAuthSettingsTarget] =
|
||||
useState<ManagedAuthProvider | null>(null);
|
||||
|
||||
const closeDialog = useCallback(() => {
|
||||
setAuthSettingsTarget(null);
|
||||
onOpenChange(false);
|
||||
}, [onOpenChange]);
|
||||
|
||||
const handlePanelClose = useCallback(() => {
|
||||
if (authSettingsTarget) {
|
||||
setAuthSettingsTarget(null);
|
||||
return;
|
||||
}
|
||||
closeDialog();
|
||||
}, [authSettingsTarget, closeDialog]);
|
||||
|
||||
const handleUniversalProviderSave = useCallback(
|
||||
async (provider: UniversalProvider) => {
|
||||
@@ -272,9 +289,9 @@ export function AddProviderDialog({
|
||||
}
|
||||
|
||||
await onSubmit(providerData);
|
||||
onOpenChange(false);
|
||||
closeDialog();
|
||||
},
|
||||
[appId, onSubmit, onOpenChange],
|
||||
[appId, onSubmit, closeDialog],
|
||||
);
|
||||
|
||||
const footer =
|
||||
@@ -282,7 +299,7 @@ export function AddProviderDialog({
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
onClick={closeDialog}
|
||||
className="border-border/20 hover:bg-accent hover:text-accent-foreground"
|
||||
>
|
||||
{t("common.cancel")}
|
||||
@@ -301,7 +318,7 @@ export function AddProviderDialog({
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
onClick={closeDialog}
|
||||
className="border-border/20 hover:bg-accent hover:text-accent-foreground"
|
||||
>
|
||||
{t("common.cancel")}
|
||||
@@ -320,7 +337,7 @@ export function AddProviderDialog({
|
||||
<FullScreenPanel
|
||||
isOpen={open}
|
||||
title={t("provider.addNewProvider")}
|
||||
onClose={() => onOpenChange(false)}
|
||||
onClose={handlePanelClose}
|
||||
footer={footer}
|
||||
>
|
||||
{showUniversalTab ? (
|
||||
@@ -342,7 +359,8 @@ export function AddProviderDialog({
|
||||
appId={appId}
|
||||
submitLabel={t("common.add")}
|
||||
onSubmit={handleSubmit}
|
||||
onCancel={() => onOpenChange(false)}
|
||||
onCancel={closeDialog}
|
||||
onManageAuthAccounts={setAuthSettingsTarget}
|
||||
onSubmittingChange={setIsFormSubmitting}
|
||||
showButtons={false}
|
||||
/>
|
||||
@@ -358,7 +376,8 @@ export function AddProviderDialog({
|
||||
appId={appId}
|
||||
submitLabel={t("common.add")}
|
||||
onSubmit={handleSubmit}
|
||||
onCancel={() => onOpenChange(false)}
|
||||
onCancel={closeDialog}
|
||||
onManageAuthAccounts={setAuthSettingsTarget}
|
||||
onSubmittingChange={setIsFormSubmitting}
|
||||
showButtons={false}
|
||||
/>
|
||||
@@ -372,6 +391,11 @@ export function AddProviderDialog({
|
||||
initialPreset={selectedUniversalPreset}
|
||||
/>
|
||||
)}
|
||||
|
||||
<AuthSettingsPanel
|
||||
target={authSettingsTarget}
|
||||
onClose={() => setAuthSettingsTarget(null)}
|
||||
/>
|
||||
</FullScreenPanel>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FullScreenPanel } from "@/components/common/FullScreenPanel";
|
||||
import { AuthCenterPanel } from "@/components/settings/AuthCenterPanel";
|
||||
import type { ManagedAuthProvider } from "@/lib/api";
|
||||
|
||||
interface AuthSettingsPanelProps {
|
||||
target: ManagedAuthProvider | null;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function AuthSettingsPanel({ target, onClose }: AuthSettingsPanelProps) {
|
||||
const { t } = useTranslation();
|
||||
const isOpen = target !== null;
|
||||
|
||||
return (
|
||||
<FullScreenPanel
|
||||
isOpen={isOpen}
|
||||
title={t("settings.tabAuth", { defaultValue: "认证" })}
|
||||
onClose={onClose}
|
||||
>
|
||||
{target ? <AuthCenterPanel authScrollTarget={target} /> : null}
|
||||
</FullScreenPanel>
|
||||
);
|
||||
}
|
||||
@@ -8,7 +8,14 @@ import {
|
||||
ProviderForm,
|
||||
type ProviderFormValues,
|
||||
} from "@/components/providers/forms/ProviderForm";
|
||||
import { openclawApi, providersApi, vscodeApi, type AppId } from "@/lib/api";
|
||||
import { AuthSettingsPanel } from "@/components/providers/AuthSettingsPanel";
|
||||
import {
|
||||
openclawApi,
|
||||
providersApi,
|
||||
vscodeApi,
|
||||
type AppId,
|
||||
type ManagedAuthProvider,
|
||||
} from "@/lib/api";
|
||||
|
||||
interface EditProviderDialogProps {
|
||||
open: boolean;
|
||||
@@ -32,6 +39,8 @@ export function EditProviderDialog({
|
||||
}: EditProviderDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
const [isFormSubmitting, setIsFormSubmitting] = useState(false);
|
||||
const [authSettingsTarget, setAuthSettingsTarget] =
|
||||
useState<ManagedAuthProvider | null>(null);
|
||||
|
||||
// 默认使用传入的 provider.settingsConfig,若当前编辑对象是"当前生效供应商",则尝试读取实时配置替换初始值
|
||||
const [liveSettings, setLiveSettings] = useState<Record<
|
||||
@@ -42,6 +51,19 @@ export function EditProviderDialog({
|
||||
// 使用 ref 标记是否已经加载过,防止重复读取覆盖用户编辑
|
||||
const [hasLoadedLive, setHasLoadedLive] = useState(false);
|
||||
|
||||
const closeDialog = useCallback(() => {
|
||||
setAuthSettingsTarget(null);
|
||||
onOpenChange(false);
|
||||
}, [onOpenChange]);
|
||||
|
||||
const handlePanelClose = useCallback(() => {
|
||||
if (authSettingsTarget) {
|
||||
setAuthSettingsTarget(null);
|
||||
return;
|
||||
}
|
||||
closeDialog();
|
||||
}, [authSettingsTarget, closeDialog]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
const load = async () => {
|
||||
@@ -212,9 +234,9 @@ export function EditProviderDialog({
|
||||
provider: updatedProvider,
|
||||
originalId: provider.id,
|
||||
});
|
||||
onOpenChange(false);
|
||||
closeDialog();
|
||||
},
|
||||
[appId, onSubmit, onOpenChange, provider],
|
||||
[appId, onSubmit, closeDialog, provider],
|
||||
);
|
||||
|
||||
if (!provider || !initialData) {
|
||||
@@ -225,7 +247,7 @@ export function EditProviderDialog({
|
||||
<FullScreenPanel
|
||||
isOpen={open}
|
||||
title={t("provider.editProvider")}
|
||||
onClose={() => onOpenChange(false)}
|
||||
onClose={handlePanelClose}
|
||||
footer={
|
||||
<Button
|
||||
type="submit"
|
||||
@@ -243,12 +265,17 @@ export function EditProviderDialog({
|
||||
providerId={provider.id}
|
||||
submitLabel={t("common.save")}
|
||||
onSubmit={handleSubmit}
|
||||
onCancel={() => onOpenChange(false)}
|
||||
onCancel={closeDialog}
|
||||
onManageAuthAccounts={setAuthSettingsTarget}
|
||||
onSubmittingChange={setIsFormSubmitting}
|
||||
initialData={initialData}
|
||||
showButtons={false}
|
||||
isProxyTakeover={isProxyTakeover}
|
||||
/>
|
||||
<AuthSettingsPanel
|
||||
target={authSettingsTarget}
|
||||
onClose={() => setAuthSettingsTarget(null)}
|
||||
/>
|
||||
</FullScreenPanel>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@ import {
|
||||
type ClaudeDesktopDefaultRoute,
|
||||
} from "@/lib/api/providers";
|
||||
import { resolveManagedAccountId } from "@/lib/authBinding";
|
||||
import type { ManagedAuthProvider } from "@/lib/api";
|
||||
|
||||
export type ClaudeDesktopProviderFormValues = ProviderFormData & {
|
||||
presetId?: string;
|
||||
@@ -100,6 +101,7 @@ export interface ClaudeDesktopProviderFormProps {
|
||||
iconColor?: string;
|
||||
};
|
||||
showButtons?: boolean;
|
||||
onManageAuthAccounts?: (target: ManagedAuthProvider) => void;
|
||||
}
|
||||
|
||||
type RouteRow = {
|
||||
@@ -248,6 +250,7 @@ export function ClaudeDesktopProviderForm({
|
||||
onSubmittingChange,
|
||||
initialData,
|
||||
showButtons = true,
|
||||
onManageAuthAccounts,
|
||||
}: ClaudeDesktopProviderFormProps) {
|
||||
const { t } = useTranslation();
|
||||
const initialMode = initialData?.meta?.claudeDesktopMode ?? "direct";
|
||||
@@ -749,13 +752,25 @@ export function ClaudeDesktopProviderForm({
|
||||
<div className="rounded-lg border border-border-default bg-muted/20 p-3">
|
||||
{activeProviderType === "github_copilot" ? (
|
||||
<CopilotAuthSection
|
||||
mode="select"
|
||||
selectedAccountId={selectedGitHubAccountId}
|
||||
onAccountSelect={setSelectedGitHubAccountId}
|
||||
onManageAccounts={
|
||||
onManageAuthAccounts
|
||||
? () => onManageAuthAccounts("github_copilot")
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<CodexOAuthSection
|
||||
mode="select"
|
||||
selectedAccountId={selectedCodexAccountId}
|
||||
onAccountSelect={setSelectedCodexAccountId}
|
||||
onManageAccounts={
|
||||
onManageAuthAccounts
|
||||
? () => onManageAuthAccounts("codex_oauth")
|
||||
: undefined
|
||||
}
|
||||
fastModeEnabled={codexFastMode}
|
||||
onFastModeChange={setCodexFastMode}
|
||||
/>
|
||||
|
||||
@@ -52,6 +52,7 @@ import type {
|
||||
ClaudeApiFormat,
|
||||
ClaudeApiKeyField,
|
||||
} from "@/types";
|
||||
import type { ManagedAuthProvider } from "@/lib/api";
|
||||
import {
|
||||
hasClaudeOneMMarker,
|
||||
setClaudeOneMMarker,
|
||||
@@ -87,6 +88,8 @@ interface ClaudeFormFieldsProps {
|
||||
selectedGitHubAccountId?: string | null;
|
||||
/** GitHub 账号选择回调(多账号支持) */
|
||||
onGitHubAccountSelect?: (accountId: string | null) => void;
|
||||
/** 打开托管账号管理入口 */
|
||||
onManageAuthAccounts?: (target: ManagedAuthProvider) => void;
|
||||
|
||||
// Codex OAuth (ChatGPT Plus/Pro)
|
||||
isCodexOauthPreset?: boolean;
|
||||
@@ -155,6 +158,7 @@ export function ClaudeFormFields({
|
||||
isCopilotAuthenticated,
|
||||
selectedGitHubAccountId,
|
||||
onGitHubAccountSelect,
|
||||
onManageAuthAccounts,
|
||||
isCodexOauthPreset,
|
||||
isCodexOauthAuthenticated,
|
||||
selectedCodexAccountId,
|
||||
@@ -554,16 +558,28 @@ export function ClaudeFormFields({
|
||||
{/* GitHub Copilot OAuth 认证 */}
|
||||
{isCopilotPreset && (
|
||||
<CopilotAuthSection
|
||||
mode="select"
|
||||
selectedAccountId={selectedGitHubAccountId}
|
||||
onAccountSelect={onGitHubAccountSelect}
|
||||
onManageAccounts={
|
||||
onManageAuthAccounts
|
||||
? () => onManageAuthAccounts("github_copilot")
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Codex OAuth 认证 (ChatGPT Plus/Pro) */}
|
||||
{isCodexOauthPreset && (
|
||||
<CodexOAuthSection
|
||||
mode="select"
|
||||
selectedAccountId={selectedCodexAccountId}
|
||||
onAccountSelect={onCodexAccountSelect}
|
||||
onManageAccounts={
|
||||
onManageAuthAccounts
|
||||
? () => onManageAuthAccounts("codex_oauth")
|
||||
: undefined
|
||||
}
|
||||
fastModeEnabled={codexFastMode}
|
||||
onFastModeChange={onCodexFastModeChange}
|
||||
/>
|
||||
|
||||
@@ -32,6 +32,7 @@ import type {
|
||||
CodexChatReasoning,
|
||||
ProviderCategory,
|
||||
} from "@/types";
|
||||
import type { ManagedAuthProvider } from "@/lib/api";
|
||||
|
||||
interface EndpointCandidate {
|
||||
url: string;
|
||||
@@ -50,6 +51,7 @@ interface CodexFormFieldsProps {
|
||||
isCodexOauthPreset?: boolean;
|
||||
selectedCodexAccountId?: string | null;
|
||||
onCodexAccountSelect?: (accountId: string | null) => void;
|
||||
onManageAuthAccounts?: (target: ManagedAuthProvider) => void;
|
||||
codexOauthNoneOptionLabel?: string;
|
||||
|
||||
// Base URL
|
||||
@@ -119,6 +121,7 @@ export function CodexFormFields({
|
||||
isCodexOauthPreset = false,
|
||||
selectedCodexAccountId,
|
||||
onCodexAccountSelect,
|
||||
onManageAuthAccounts,
|
||||
codexOauthNoneOptionLabel,
|
||||
shouldShowSpeedTest,
|
||||
codexBaseUrl,
|
||||
@@ -296,8 +299,14 @@ export function CodexFormFields({
|
||||
{/* Codex OAuth 账号选择 */}
|
||||
{isCodexOauthPreset && (
|
||||
<CodexOAuthSection
|
||||
mode="select"
|
||||
selectedAccountId={selectedCodexAccountId}
|
||||
onAccountSelect={onCodexAccountSelect}
|
||||
onManageAccounts={
|
||||
onManageAuthAccounts
|
||||
? () => onManageAuthAccounts("codex_oauth")
|
||||
: undefined
|
||||
}
|
||||
noneOptionLabel={codexOauthNoneOptionLabel}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -21,16 +21,21 @@ import {
|
||||
X,
|
||||
Sparkles,
|
||||
User,
|
||||
Settings2,
|
||||
} from "lucide-react";
|
||||
import { useCodexOauth } from "./hooks/useCodexOauth";
|
||||
import { copyText } from "@/lib/clipboard";
|
||||
|
||||
interface CodexOAuthSectionProps {
|
||||
className?: string;
|
||||
/** select 模式只展示账号选择和管理入口;manage 模式展示完整账号管理 */
|
||||
mode?: "manage" | "select";
|
||||
/** 当前选中的 ChatGPT 账号 ID */
|
||||
selectedAccountId?: string | null;
|
||||
/** 账号选择回调 */
|
||||
onAccountSelect?: (accountId: string | null) => void;
|
||||
/** 打开账号管理入口 */
|
||||
onManageAccounts?: () => void;
|
||||
/** 空选择项文案;默认表示使用托管认证的默认账号 */
|
||||
noneOptionLabel?: string;
|
||||
/** 是否开启 Codex FAST mode */
|
||||
@@ -47,8 +52,10 @@ interface CodexOAuthSectionProps {
|
||||
*/
|
||||
export const CodexOAuthSection: React.FC<CodexOAuthSectionProps> = ({
|
||||
className,
|
||||
mode = "manage",
|
||||
selectedAccountId,
|
||||
onAccountSelect,
|
||||
onManageAccounts,
|
||||
noneOptionLabel,
|
||||
fastModeEnabled = false,
|
||||
onFastModeChange,
|
||||
@@ -59,6 +66,7 @@ export const CodexOAuthSection: React.FC<CodexOAuthSectionProps> = ({
|
||||
const {
|
||||
accounts,
|
||||
defaultAccountId,
|
||||
isLoadingStatus,
|
||||
hasAnyAccount,
|
||||
pollingState,
|
||||
deviceCode,
|
||||
@@ -86,6 +94,21 @@ export const CodexOAuthSection: React.FC<CodexOAuthSectionProps> = ({
|
||||
onAccountSelect?.(value === "none" ? null : value);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (
|
||||
mode !== "select" ||
|
||||
!selectedAccountId ||
|
||||
!onAccountSelect ||
|
||||
isLoadingStatus
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!accounts.some((account) => account.id === selectedAccountId)) {
|
||||
onAccountSelect(null);
|
||||
}
|
||||
}, [accounts, isLoadingStatus, mode, onAccountSelect, selectedAccountId]);
|
||||
|
||||
const handleRemoveAccount = (accountId: string, e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
@@ -95,63 +118,87 @@ export const CodexOAuthSection: React.FC<CodexOAuthSectionProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const accountSelect = onAccountSelect &&
|
||||
(mode === "select" || hasAnyAccount || noneOptionLabel) && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm text-muted-foreground">
|
||||
{mode === "select"
|
||||
? t("codexOauth.chatgptAccount", "ChatGPT 账号")
|
||||
: t("codexOauth.selectAccount", "选择账号")}
|
||||
</Label>
|
||||
<Select
|
||||
value={selectedAccountId || "none"}
|
||||
onValueChange={handleAccountSelect}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue
|
||||
placeholder={t(
|
||||
"codexOauth.selectAccountPlaceholder",
|
||||
"选择一个 ChatGPT 账号",
|
||||
)}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">
|
||||
<span className="text-muted-foreground">
|
||||
{noneOptionLabel ??
|
||||
t("codexOauth.useDefaultAccount", "使用默认账号")}
|
||||
</span>
|
||||
</SelectItem>
|
||||
{accounts.map((account) => (
|
||||
<SelectItem key={account.id} value={account.id}>
|
||||
<div className="flex items-center gap-2">
|
||||
<User className="h-4 w-4 text-muted-foreground" />
|
||||
<span>{account.login}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={`space-y-4 ${className || ""}`}>
|
||||
{/* 认证状态标题 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<Label>{t("codexOauth.authStatus", "认证状态")}</Label>
|
||||
<Badge
|
||||
variant={hasAnyAccount ? "default" : "secondary"}
|
||||
className={hasAnyAccount ? "bg-green-500 hover:bg-green-600" : ""}
|
||||
>
|
||||
{hasAnyAccount
|
||||
? t("codexOauth.accountCount", {
|
||||
count: accounts.length,
|
||||
defaultValue: `${accounts.length} 个账号`,
|
||||
})
|
||||
: t("codexOauth.notAuthenticated", "未认证")}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* 账号选择器 */}
|
||||
{onAccountSelect && (hasAnyAccount || noneOptionLabel) && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm text-muted-foreground">
|
||||
{t("codexOauth.selectAccount", "选择账号")}
|
||||
</Label>
|
||||
<Select
|
||||
value={selectedAccountId || "none"}
|
||||
onValueChange={handleAccountSelect}
|
||||
{mode === "manage" && (
|
||||
<div className="flex items-center justify-between">
|
||||
<Label>{t("codexOauth.authStatus", "认证状态")}</Label>
|
||||
<Badge
|
||||
variant={hasAnyAccount ? "default" : "secondary"}
|
||||
className={hasAnyAccount ? "bg-green-500 hover:bg-green-600" : ""}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue
|
||||
placeholder={t(
|
||||
"codexOauth.selectAccountPlaceholder",
|
||||
"选择一个 ChatGPT 账号",
|
||||
)}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">
|
||||
<span className="text-muted-foreground">
|
||||
{noneOptionLabel ??
|
||||
t("codexOauth.useDefaultAccount", "使用默认账号")}
|
||||
</span>
|
||||
</SelectItem>
|
||||
{accounts.map((account) => (
|
||||
<SelectItem key={account.id} value={account.id}>
|
||||
<div className="flex items-center gap-2">
|
||||
<User className="h-4 w-4 text-muted-foreground" />
|
||||
<span>{account.login}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{hasAnyAccount
|
||||
? t("codexOauth.accountCount", {
|
||||
count: accounts.length,
|
||||
defaultValue: `${accounts.length} 个账号`,
|
||||
})
|
||||
: t("codexOauth.notAuthenticated", "未认证")}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{onFastModeChange && (
|
||||
{/* 账号选择器 */}
|
||||
{mode === "select" && accountSelect ? (
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-end">
|
||||
<div className="min-w-0 flex-1">{accountSelect}</div>
|
||||
{onManageAccounts && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={onManageAccounts}
|
||||
className="h-9 shrink-0"
|
||||
>
|
||||
<Settings2 className="h-4 w-4" />
|
||||
{t("codexOauth.manageAccounts", "管理账号")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
accountSelect
|
||||
)}
|
||||
|
||||
{mode === "manage" && onFastModeChange && (
|
||||
<div className="flex items-center justify-between rounded-md border bg-muted/30 p-3">
|
||||
<div className="space-y-1 pr-4">
|
||||
<Label className="text-sm font-medium">
|
||||
@@ -173,7 +220,7 @@ export const CodexOAuthSection: React.FC<CodexOAuthSectionProps> = ({
|
||||
)}
|
||||
|
||||
{/* 已登录账号列表 */}
|
||||
{hasAnyAccount && (
|
||||
{mode === "manage" && hasAnyAccount && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm text-muted-foreground">
|
||||
{t("codexOauth.loggedInAccounts", "已登录账号")}
|
||||
@@ -230,7 +277,7 @@ export const CodexOAuthSection: React.FC<CodexOAuthSectionProps> = ({
|
||||
)}
|
||||
|
||||
{/* 未认证 - 登录按钮 */}
|
||||
{!hasAnyAccount && pollingState === "idle" && (
|
||||
{mode === "manage" && !hasAnyAccount && pollingState === "idle" && (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={addAccount}
|
||||
@@ -243,7 +290,7 @@ export const CodexOAuthSection: React.FC<CodexOAuthSectionProps> = ({
|
||||
)}
|
||||
|
||||
{/* 已有账号 - 添加更多按钮 */}
|
||||
{hasAnyAccount && pollingState === "idle" && (
|
||||
{mode === "manage" && hasAnyAccount && pollingState === "idle" && (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={addAccount}
|
||||
@@ -257,7 +304,7 @@ export const CodexOAuthSection: React.FC<CodexOAuthSectionProps> = ({
|
||||
)}
|
||||
|
||||
{/* 轮询中状态 */}
|
||||
{isPolling && deviceCode && (
|
||||
{mode === "manage" && isPolling && deviceCode && (
|
||||
<div className="space-y-3 p-4 rounded-lg border border-border bg-muted/50">
|
||||
<div className="flex items-center justify-center gap-2 text-sm text-muted-foreground">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
@@ -314,7 +361,7 @@ export const CodexOAuthSection: React.FC<CodexOAuthSectionProps> = ({
|
||||
)}
|
||||
|
||||
{/* 错误状态 */}
|
||||
{pollingState === "error" && error && (
|
||||
{mode === "manage" && pollingState === "error" && error && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-red-500">{error}</p>
|
||||
<div className="flex gap-2">
|
||||
@@ -339,7 +386,7 @@ export const CodexOAuthSection: React.FC<CodexOAuthSectionProps> = ({
|
||||
)}
|
||||
|
||||
{/* 注销所有账号 */}
|
||||
{hasAnyAccount && accounts.length > 1 && (
|
||||
{mode === "manage" && hasAnyAccount && accounts.length > 1 && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
Plus,
|
||||
X,
|
||||
User,
|
||||
Settings2,
|
||||
} from "lucide-react";
|
||||
import { useCopilotAuth } from "./hooks/useCopilotAuth";
|
||||
import { copyText } from "@/lib/clipboard";
|
||||
@@ -28,10 +29,14 @@ import type { GitHubAccount } from "@/lib/api";
|
||||
|
||||
interface CopilotAuthSectionProps {
|
||||
className?: string;
|
||||
/** select 模式只展示账号选择和管理入口;manage 模式展示完整账号管理 */
|
||||
mode?: "manage" | "select";
|
||||
/** 当前选中的 GitHub 账号 ID */
|
||||
selectedAccountId?: string | null;
|
||||
/** 账号选择回调 */
|
||||
onAccountSelect?: (accountId: string | null) => void;
|
||||
/** 打开账号管理入口 */
|
||||
onManageAccounts?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -41,8 +46,10 @@ interface CopilotAuthSectionProps {
|
||||
*/
|
||||
export const CopilotAuthSection: React.FC<CopilotAuthSectionProps> = ({
|
||||
className,
|
||||
mode = "manage",
|
||||
selectedAccountId,
|
||||
onAccountSelect,
|
||||
onManageAccounts,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [copied, setCopied] = React.useState(false);
|
||||
@@ -64,6 +71,7 @@ export const CopilotAuthSection: React.FC<CopilotAuthSectionProps> = ({
|
||||
accounts,
|
||||
defaultAccountId,
|
||||
migrationError,
|
||||
isLoadingStatus,
|
||||
hasAnyAccount,
|
||||
pollingState,
|
||||
deviceCode,
|
||||
@@ -93,6 +101,21 @@ export const CopilotAuthSection: React.FC<CopilotAuthSectionProps> = ({
|
||||
onAccountSelect?.(value === "none" ? null : value);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (
|
||||
mode !== "select" ||
|
||||
!selectedAccountId ||
|
||||
!onAccountSelect ||
|
||||
isLoadingStatus
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!accounts.some((account) => account.id === selectedAccountId)) {
|
||||
onAccountSelect(null);
|
||||
}
|
||||
}, [accounts, isLoadingStatus, mode, onAccountSelect, selectedAccountId]);
|
||||
|
||||
// 处理移除账号
|
||||
const handleRemoveAccount = (accountId: string, e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
@@ -109,60 +132,103 @@ export const CopilotAuthSection: React.FC<CopilotAuthSectionProps> = ({
|
||||
return <CopilotAccountAvatar account={account} />;
|
||||
};
|
||||
|
||||
const accountSelect = onAccountSelect &&
|
||||
(mode === "select" || hasAnyAccount) && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm text-muted-foreground">
|
||||
{mode === "select"
|
||||
? t("copilot.githubAccount", "GitHub 账号")
|
||||
: t("copilot.selectAccount", "选择账号")}
|
||||
</Label>
|
||||
<Select
|
||||
value={selectedAccountId || "none"}
|
||||
onValueChange={handleAccountSelect}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue
|
||||
placeholder={t(
|
||||
"copilot.selectAccountPlaceholder",
|
||||
"选择一个 GitHub 账号",
|
||||
)}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">
|
||||
<span className="text-muted-foreground">
|
||||
{t("copilot.useDefaultAccount", "使用默认账号")}
|
||||
</span>
|
||||
</SelectItem>
|
||||
{accounts.map((account) => (
|
||||
<SelectItem key={account.id} value={account.id}>
|
||||
<div className="flex items-center gap-2">
|
||||
{renderAvatar(account)}
|
||||
<span>{account.login}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={`space-y-4 ${className || ""}`}>
|
||||
{/* 认证状态标题 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<Label>{t("copilot.authStatus", "GitHub Copilot 认证")}</Label>
|
||||
<Badge
|
||||
variant={hasAnyAccount ? "default" : "secondary"}
|
||||
className={hasAnyAccount ? "bg-green-500 hover:bg-green-600" : ""}
|
||||
>
|
||||
{hasAnyAccount
|
||||
? t("copilot.accountCount", {
|
||||
count: accounts.length,
|
||||
defaultValue: `${accounts.length} 个账号`,
|
||||
})
|
||||
: t("copilot.notAuthenticated", "未认证")}
|
||||
</Badge>
|
||||
</div>
|
||||
{mode === "manage" && (
|
||||
<div className="flex items-center justify-between">
|
||||
<Label>{t("copilot.authStatus", "GitHub Copilot 认证")}</Label>
|
||||
<Badge
|
||||
variant={hasAnyAccount ? "default" : "secondary"}
|
||||
className={hasAnyAccount ? "bg-green-500 hover:bg-green-600" : ""}
|
||||
>
|
||||
{hasAnyAccount
|
||||
? t("copilot.accountCount", {
|
||||
count: accounts.length,
|
||||
defaultValue: `${accounts.length} 个账号`,
|
||||
})
|
||||
: t("copilot.notAuthenticated", "未认证")}
|
||||
</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>
|
||||
{mode === "manage" && (
|
||||
<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 && (
|
||||
{mode === "manage" && migrationError && (
|
||||
<p className="text-sm text-amber-600 dark:text-amber-400">
|
||||
{t("copilot.migrationFailed", {
|
||||
error: migrationError,
|
||||
@@ -172,44 +238,27 @@ export const CopilotAuthSection: React.FC<CopilotAuthSectionProps> = ({
|
||||
)}
|
||||
|
||||
{/* 账号选择器(有账号时显示) */}
|
||||
{hasAnyAccount && onAccountSelect && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm text-muted-foreground">
|
||||
{t("copilot.selectAccount", "选择账号")}
|
||||
</Label>
|
||||
<Select
|
||||
value={selectedAccountId || "none"}
|
||||
onValueChange={handleAccountSelect}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue
|
||||
placeholder={t(
|
||||
"copilot.selectAccountPlaceholder",
|
||||
"选择一个 GitHub 账号",
|
||||
)}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">
|
||||
<span className="text-muted-foreground">
|
||||
{t("copilot.useDefaultAccount", "使用默认账号")}
|
||||
</span>
|
||||
</SelectItem>
|
||||
{accounts.map((account) => (
|
||||
<SelectItem key={account.id} value={account.id}>
|
||||
<div className="flex items-center gap-2">
|
||||
{renderAvatar(account)}
|
||||
<span>{account.login}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{mode === "select" && accountSelect ? (
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-end">
|
||||
<div className="min-w-0 flex-1">{accountSelect}</div>
|
||||
{onManageAccounts && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={onManageAccounts}
|
||||
className="h-9 shrink-0"
|
||||
>
|
||||
<Settings2 className="h-4 w-4" />
|
||||
{t("copilot.manageAccounts", "管理账号")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
accountSelect
|
||||
)}
|
||||
|
||||
{/* 已登录账号列表 */}
|
||||
{hasAnyAccount && (
|
||||
{mode === "manage" && hasAnyAccount && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm text-muted-foreground">
|
||||
{t("copilot.loggedInAccounts", "已登录账号")}
|
||||
@@ -272,7 +321,7 @@ export const CopilotAuthSection: React.FC<CopilotAuthSectionProps> = ({
|
||||
)}
|
||||
|
||||
{/* 未认证状态 - 登录按钮 */}
|
||||
{!hasAnyAccount && pollingState === "idle" && (
|
||||
{mode === "manage" && !hasAnyAccount && pollingState === "idle" && (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={addAccount}
|
||||
@@ -286,7 +335,7 @@ export const CopilotAuthSection: React.FC<CopilotAuthSectionProps> = ({
|
||||
)}
|
||||
|
||||
{/* 已有账号 - 添加更多账号按钮 */}
|
||||
{hasAnyAccount && pollingState === "idle" && (
|
||||
{mode === "manage" && hasAnyAccount && pollingState === "idle" && (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={addAccount}
|
||||
@@ -303,7 +352,7 @@ export const CopilotAuthSection: React.FC<CopilotAuthSectionProps> = ({
|
||||
)}
|
||||
|
||||
{/* 轮询中状态 */}
|
||||
{isPolling && deviceCode && (
|
||||
{mode === "manage" && isPolling && deviceCode && (
|
||||
<div className="space-y-3 p-4 rounded-lg border border-border bg-muted/50">
|
||||
<div className="flex items-center justify-center gap-2 text-sm text-muted-foreground">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
@@ -363,7 +412,7 @@ export const CopilotAuthSection: React.FC<CopilotAuthSectionProps> = ({
|
||||
)}
|
||||
|
||||
{/* 错误状态 */}
|
||||
{pollingState === "error" && error && (
|
||||
{mode === "manage" && pollingState === "error" && error && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-red-500">{error}</p>
|
||||
<div className="flex gap-2">
|
||||
@@ -388,7 +437,7 @@ export const CopilotAuthSection: React.FC<CopilotAuthSectionProps> = ({
|
||||
)}
|
||||
|
||||
{/* 注销所有账号按钮 */}
|
||||
{hasAnyAccount && accounts.length > 1 && (
|
||||
{mode === "manage" && hasAnyAccount && accounts.length > 1 && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
|
||||
@@ -8,7 +8,12 @@ import { Button } from "@/components/ui/button";
|
||||
import { Form, FormField, FormItem, FormMessage } from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { providerSchema, type ProviderFormData } from "@/lib/schemas/provider";
|
||||
import { providersApi, settingsApi, type AppId } from "@/lib/api";
|
||||
import {
|
||||
providersApi,
|
||||
settingsApi,
|
||||
type AppId,
|
||||
type ManagedAuthProvider,
|
||||
} from "@/lib/api";
|
||||
import type {
|
||||
ProviderCategory,
|
||||
ProviderMeta,
|
||||
@@ -228,6 +233,7 @@ export interface ProviderFormProps {
|
||||
onCancel: () => void;
|
||||
onUniversalPresetSelect?: (preset: UniversalProviderPreset) => void;
|
||||
onManageUniversalProviders?: () => void;
|
||||
onManageAuthAccounts?: (target: ManagedAuthProvider) => void;
|
||||
onSubmittingChange?: (isSubmitting: boolean) => void;
|
||||
initialData?: {
|
||||
name?: string;
|
||||
@@ -259,6 +265,7 @@ function ProviderFormFull({
|
||||
onCancel,
|
||||
onUniversalPresetSelect,
|
||||
onManageUniversalProviders,
|
||||
onManageAuthAccounts,
|
||||
onSubmittingChange,
|
||||
initialData,
|
||||
showButtons = true,
|
||||
@@ -655,8 +662,7 @@ function ProviderFormFull({
|
||||
const selectedPresetEntry = useMemo(
|
||||
() =>
|
||||
selectedPresetId && selectedPresetId !== "custom"
|
||||
? (presetEntries.find((entry) => entry.id === selectedPresetId) ??
|
||||
null)
|
||||
? (presetEntries.find((entry) => entry.id === selectedPresetId) ?? null)
|
||||
: null,
|
||||
[presetEntries, selectedPresetId],
|
||||
);
|
||||
@@ -1162,7 +1168,11 @@ function ProviderFormFull({
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (!isCopilotProvider && !isClaudeCodexOauthProvider && !apiKey.trim()) {
|
||||
if (
|
||||
!isCopilotProvider &&
|
||||
!isClaudeCodexOauthProvider &&
|
||||
!apiKey.trim()
|
||||
) {
|
||||
issues.push(
|
||||
t("providerForm.apiKeyRequired", {
|
||||
defaultValue: "非官方供应商请填写 API Key",
|
||||
@@ -1423,7 +1433,7 @@ function ProviderFormFull({
|
||||
authProvider: "codex_oauth",
|
||||
accountId: selectedCodexAccountId ?? undefined,
|
||||
}
|
||||
: undefined,
|
||||
: undefined,
|
||||
// GitHub Copilot 多账号:保存关联的账号 ID
|
||||
githubAccountId:
|
||||
isCopilotProvider && selectedGitHubAccountId
|
||||
@@ -2022,6 +2032,7 @@ function ProviderFormFull({
|
||||
isCopilotAuthenticated={isCopilotAuthenticated}
|
||||
selectedGitHubAccountId={selectedGitHubAccountId}
|
||||
onGitHubAccountSelect={setSelectedGitHubAccountId}
|
||||
onManageAuthAccounts={onManageAuthAccounts}
|
||||
isCodexOauthAuthenticated={isCodexOauthAuthenticated}
|
||||
selectedCodexAccountId={selectedCodexAccountId}
|
||||
onCodexAccountSelect={setSelectedCodexAccountId}
|
||||
@@ -2074,6 +2085,7 @@ function ProviderFormFull({
|
||||
isCodexOauthPreset={isCodexOfficialProvider}
|
||||
selectedCodexAccountId={selectedCodexAccountId}
|
||||
onCodexAccountSelect={setSelectedCodexAccountId}
|
||||
onManageAuthAccounts={onManageAuthAccounts}
|
||||
codexOauthNoneOptionLabel="暂不绑定托管账号"
|
||||
shouldShowSpeedTest={shouldShowSpeedTest}
|
||||
codexBaseUrl={codexBaseUrl}
|
||||
|
||||
@@ -1,12 +1,42 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { Github, ShieldCheck } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { CodexIcon } from "@/components/BrandIcons";
|
||||
import { CopilotAuthSection } from "@/components/providers/forms/CopilotAuthSection";
|
||||
import { CodexOAuthSection } from "@/components/providers/forms/CodexOAuthSection";
|
||||
import type { ManagedAuthProvider } from "@/lib/api";
|
||||
|
||||
export function AuthCenterPanel() {
|
||||
interface AuthCenterPanelProps {
|
||||
authScrollTarget?: ManagedAuthProvider | null;
|
||||
}
|
||||
|
||||
export function AuthCenterPanel({ authScrollTarget }: AuthCenterPanelProps) {
|
||||
const { t } = useTranslation();
|
||||
const copilotSectionRef = useRef<HTMLElement | null>(null);
|
||||
const codexOauthSectionRef = useRef<HTMLElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!authScrollTarget) return;
|
||||
|
||||
const sectionRef =
|
||||
authScrollTarget === "github_copilot"
|
||||
? copilotSectionRef
|
||||
: codexOauthSectionRef;
|
||||
|
||||
const frame = requestAnimationFrame(() => {
|
||||
const prefersReducedMotion = window.matchMedia(
|
||||
"(prefers-reduced-motion: reduce)",
|
||||
).matches;
|
||||
|
||||
sectionRef.current?.scrollIntoView({
|
||||
behavior: prefersReducedMotion ? "auto" : "smooth",
|
||||
block: "start",
|
||||
});
|
||||
});
|
||||
|
||||
return () => cancelAnimationFrame(frame);
|
||||
}, [authScrollTarget]);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@@ -34,7 +64,10 @@ export function AuthCenterPanel() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="rounded-xl border border-border/60 bg-card/60 p-6">
|
||||
<section
|
||||
ref={copilotSectionRef}
|
||||
className="scroll-mt-4 rounded-xl border border-border/60 bg-card/60 p-6"
|
||||
>
|
||||
<div className="mb-4 flex items-center gap-3">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-muted">
|
||||
<Github className="h-5 w-5" />
|
||||
@@ -52,7 +85,10 @@ export function AuthCenterPanel() {
|
||||
<CopilotAuthSection />
|
||||
</section>
|
||||
|
||||
<section className="rounded-xl border border-border/60 bg-card/60 p-6">
|
||||
<section
|
||||
ref={codexOauthSectionRef}
|
||||
className="scroll-mt-4 rounded-xl border border-border/60 bg-card/60 p-6"
|
||||
>
|
||||
<div className="mb-4 flex items-center gap-3">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-muted">
|
||||
<CodexIcon size={20} />
|
||||
|
||||
Reference in New Issue
Block a user