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