mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-08-03 02:51:17 +08:00
feat(xai): add Grok account management UI with four-locale strings
Add XaiOAuthSection (device-code login, account list, default selection) backed by the generalized useManagedAuth hook, wire it into the Auth Center and both Claude provider forms, and expose model fetching for signed-in accounts. - Accounts requiring re-auth stay visible in the account selector as disabled items with an expired badge instead of silently vanishing. - Auth status refetches periodically so a revoked refresh token surfaces without reloading the panel. - All strings ship in zh/en/ja/zh-TW; a locale coverage test pins every required key in all four locales.
This commit is contained in:
@@ -39,6 +39,7 @@ import { Switch } from "@/components/ui/switch";
|
||||
import { BasicFormFields } from "./BasicFormFields";
|
||||
import { CodexOAuthSection } from "./CodexOAuthSection";
|
||||
import { CopilotAuthSection } from "./CopilotAuthSection";
|
||||
import { XaiOAuthSection } from "./XaiOAuthSection";
|
||||
import { ApiKeySection } from "./shared/ApiKeySection";
|
||||
import { EndpointField } from "./shared/EndpointField";
|
||||
import { ModelDropdown } from "./shared/ModelDropdown";
|
||||
@@ -68,6 +69,7 @@ import {
|
||||
type ClaudeDesktopDefaultRoute,
|
||||
} from "@/lib/api/providers";
|
||||
import { resolveManagedAccountId } from "@/lib/authBinding";
|
||||
import { useCopilotAuth, useCodexOauth, useXaiOauth } from "./hooks";
|
||||
|
||||
export type ClaudeDesktopProviderFormValues = ProviderFormData & {
|
||||
presetId?: string;
|
||||
@@ -280,6 +282,9 @@ export function ClaudeDesktopProviderForm({
|
||||
const [selectedCodexAccountId, setSelectedCodexAccountId] = useState<
|
||||
string | null
|
||||
>(() => resolveManagedAccountId(initialData?.meta, "codex_oauth"));
|
||||
const [selectedXaiAccountId, setSelectedXaiAccountId] = useState<
|
||||
string | null
|
||||
>(() => resolveManagedAccountId(initialData?.meta, "xai_oauth"));
|
||||
const [codexFastMode, setCodexFastMode] = useState<boolean>(
|
||||
() => initialData?.meta?.codexFastMode ?? false,
|
||||
);
|
||||
@@ -377,13 +382,24 @@ export function ClaudeDesktopProviderForm({
|
||||
);
|
||||
const activeProviderType =
|
||||
activePreset?.providerType ?? initialData?.meta?.providerType;
|
||||
const { isAuthenticated: isCopilotAuthenticated, accounts: copilotAccounts } =
|
||||
useCopilotAuth();
|
||||
const {
|
||||
isAuthenticated: isCodexOauthAuthenticated,
|
||||
accounts: codexOauthAccounts,
|
||||
} = useCodexOauth();
|
||||
const {
|
||||
isAuthenticated: isXaiOauthAuthenticated,
|
||||
accounts: xaiOauthAccounts,
|
||||
} = useXaiOauth();
|
||||
const isOfficial =
|
||||
initialData?.category === "official" ||
|
||||
activePreset?.category === "official";
|
||||
const usesManagedOAuth =
|
||||
activePreset?.requiresOAuth === true ||
|
||||
activeProviderType === "github_copilot" ||
|
||||
activeProviderType === "codex_oauth";
|
||||
activeProviderType === "codex_oauth" ||
|
||||
activeProviderType === "xai_oauth";
|
||||
|
||||
// API Key 获取/邀请链接(与 Claude Code 表单同款,见 ClaudeFormFields)
|
||||
const apiKeyLinkCategory = activePreset?.category ?? initialData?.category;
|
||||
@@ -568,7 +584,7 @@ export function ClaudeDesktopProviderForm({
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!baseUrl.trim()) {
|
||||
if (!baseUrl.trim() && !usesManagedOAuth) {
|
||||
toast.error(
|
||||
t("providerForm.fetchModelsNeedEndpoint", {
|
||||
defaultValue: "请先填写接口地址",
|
||||
@@ -576,6 +592,61 @@ export function ClaudeDesktopProviderForm({
|
||||
);
|
||||
return;
|
||||
}
|
||||
const selectedAccountIsUsable = (
|
||||
accountId: string | null,
|
||||
accounts: Array<{ id: string; requires_reauth: boolean }>,
|
||||
) =>
|
||||
accountId === null ||
|
||||
accounts.some(
|
||||
(account) => account.id === accountId && !account.requires_reauth,
|
||||
);
|
||||
const managedAuthState =
|
||||
activeProviderType === "github_copilot"
|
||||
? {
|
||||
authenticated: isCopilotAuthenticated,
|
||||
accountId: selectedGitHubAccountId,
|
||||
accounts: copilotAccounts,
|
||||
loginMessage: t("copilot.loginRequired", {
|
||||
defaultValue: "请先登录 GitHub Copilot",
|
||||
}),
|
||||
}
|
||||
: activeProviderType === "codex_oauth"
|
||||
? {
|
||||
authenticated: isCodexOauthAuthenticated,
|
||||
accountId: selectedCodexAccountId,
|
||||
accounts: codexOauthAccounts,
|
||||
loginMessage: t("codexOauth.loginRequired", {
|
||||
defaultValue: "请先登录 ChatGPT 账号",
|
||||
}),
|
||||
}
|
||||
: activeProviderType === "xai_oauth"
|
||||
? {
|
||||
authenticated: isXaiOauthAuthenticated,
|
||||
accountId: selectedXaiAccountId,
|
||||
accounts: xaiOauthAccounts,
|
||||
loginMessage: t("xaiOauth.loginRequired", {
|
||||
defaultValue: "请先登录 xAI 账号",
|
||||
}),
|
||||
}
|
||||
: null;
|
||||
if (managedAuthState && !managedAuthState.authenticated) {
|
||||
toast.error(managedAuthState.loginMessage);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
managedAuthState &&
|
||||
!selectedAccountIsUsable(
|
||||
managedAuthState.accountId,
|
||||
managedAuthState.accounts,
|
||||
)
|
||||
) {
|
||||
toast.error(
|
||||
t("managedAuth.selectedAccountUnavailable", {
|
||||
defaultValue: "已绑定账号不存在或需要重新登录,请重新选择账号",
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!usesManagedOAuth && !apiKey.trim()) {
|
||||
toast.error(
|
||||
t("providerForm.fetchModelsNeedApiKey", {
|
||||
@@ -594,7 +665,8 @@ export function ClaudeDesktopProviderForm({
|
||||
}))
|
||||
.filter((route) => route.route || route.model);
|
||||
|
||||
if (mode === "proxy") {
|
||||
const effectiveMode = activeProviderType === "xai_oauth" ? "proxy" : mode;
|
||||
if (effectiveMode === "proxy") {
|
||||
// 固定四档(Sonnet / Opus / Fable / Haiku),route_id 由 UI 生成、恒合法,
|
||||
// 因此只要求至少填一个实际请求模型;留空档继承第一个已填档(Sonnet 优先),
|
||||
// 对齐 Claude Code 的兜底,保证落库四档齐全、子 agent 不会找不到模型。
|
||||
@@ -654,9 +726,11 @@ export function ClaudeDesktopProviderForm({
|
||||
Record<string, ClaudeDesktopModelRoute>
|
||||
>((acc, route) => {
|
||||
acc[route.route] = {
|
||||
model: mode === "direct" ? route.route : route.model || route.route,
|
||||
model:
|
||||
effectiveMode === "direct" ? route.route : route.model || route.route,
|
||||
labelOverride:
|
||||
route.labelOverride || (mode === "proxy" ? route.model : undefined),
|
||||
route.labelOverride ||
|
||||
(effectiveMode === "proxy" ? route.model : undefined),
|
||||
supports1m: route.supports1m || undefined,
|
||||
};
|
||||
return acc;
|
||||
@@ -664,8 +738,13 @@ export function ClaudeDesktopProviderForm({
|
||||
|
||||
const meta: ProviderMeta = {
|
||||
...(initialData?.meta ?? {}),
|
||||
claudeDesktopMode: mode,
|
||||
apiFormat: mode === "proxy" ? apiFormat : "anthropic",
|
||||
claudeDesktopMode: effectiveMode,
|
||||
apiFormat:
|
||||
activeProviderType === "xai_oauth"
|
||||
? "openai_responses"
|
||||
: effectiveMode === "proxy"
|
||||
? apiFormat
|
||||
: "anthropic",
|
||||
};
|
||||
|
||||
meta.claudeDesktopModelRoutes = routeMap;
|
||||
@@ -683,7 +762,13 @@ export function ClaudeDesktopProviderForm({
|
||||
authProvider: "codex_oauth",
|
||||
accountId: selectedCodexAccountId ?? undefined,
|
||||
}
|
||||
: undefined;
|
||||
: activeProviderType === "xai_oauth"
|
||||
? {
|
||||
source: "managed_account",
|
||||
authProvider: "xai_oauth",
|
||||
accountId: selectedXaiAccountId ?? undefined,
|
||||
}
|
||||
: undefined;
|
||||
meta.codexFastMode =
|
||||
activeProviderType === "codex_oauth" ? codexFastMode : undefined;
|
||||
|
||||
@@ -773,13 +858,18 @@ export function ClaudeDesktopProviderForm({
|
||||
selectedAccountId={selectedGitHubAccountId}
|
||||
onAccountSelect={setSelectedGitHubAccountId}
|
||||
/>
|
||||
) : (
|
||||
) : activeProviderType === "codex_oauth" ? (
|
||||
<CodexOAuthSection
|
||||
selectedAccountId={selectedCodexAccountId}
|
||||
onAccountSelect={setSelectedCodexAccountId}
|
||||
fastModeEnabled={codexFastMode}
|
||||
onFastModeChange={setCodexFastMode}
|
||||
/>
|
||||
) : (
|
||||
<XaiOAuthSection
|
||||
selectedAccountId={selectedXaiAccountId}
|
||||
onAccountSelect={setSelectedXaiAccountId}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
@@ -835,6 +925,7 @@ export function ClaudeDesktopProviderForm({
|
||||
<Switch
|
||||
checked={needsModelMapping}
|
||||
onCheckedChange={handleModelMappingChange}
|
||||
disabled={activeProviderType === "xai_oauth"}
|
||||
aria-label={t("claudeDesktop.modelMappingToggle", {
|
||||
defaultValue: "需要模型映射",
|
||||
})}
|
||||
@@ -844,44 +935,49 @@ export function ClaudeDesktopProviderForm({
|
||||
|
||||
{needsModelMapping && (
|
||||
<div className="space-y-4 rounded-lg border border-border-default p-4">
|
||||
<div className="space-y-2">
|
||||
<Label>
|
||||
{t("providerForm.apiFormat", { defaultValue: "API 格式" })}
|
||||
</Label>
|
||||
<Select
|
||||
value={apiFormat}
|
||||
onValueChange={(value) =>
|
||||
setApiFormat(value as ClaudeApiFormat)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="anthropic">
|
||||
{t("providerForm.apiFormatAnthropic", {
|
||||
defaultValue: "Anthropic Messages (原生)",
|
||||
})}
|
||||
</SelectItem>
|
||||
<SelectItem value="openai_chat">
|
||||
{t("providerForm.apiFormatOpenAIChat", {
|
||||
defaultValue: "OpenAI Chat Completions (需开启路由)",
|
||||
})}
|
||||
</SelectItem>
|
||||
<SelectItem value="openai_responses">
|
||||
{t("providerForm.apiFormatOpenAIResponses", {
|
||||
defaultValue: "OpenAI Responses API (需开启路由)",
|
||||
})}
|
||||
</SelectItem>
|
||||
<SelectItem value="gemini_native">
|
||||
{t("providerForm.apiFormatGeminiNative", {
|
||||
defaultValue:
|
||||
"Gemini Native generateContent (需开启路由)",
|
||||
})}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{activeProviderType !== "xai_oauth" && (
|
||||
<div className="space-y-2">
|
||||
<Label>
|
||||
{t("providerForm.apiFormat", {
|
||||
defaultValue: "API 格式",
|
||||
})}
|
||||
</Label>
|
||||
<Select
|
||||
value={apiFormat}
|
||||
onValueChange={(value) =>
|
||||
setApiFormat(value as ClaudeApiFormat)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="anthropic">
|
||||
{t("providerForm.apiFormatAnthropic", {
|
||||
defaultValue: "Anthropic Messages (原生)",
|
||||
})}
|
||||
</SelectItem>
|
||||
<SelectItem value="openai_chat">
|
||||
{t("providerForm.apiFormatOpenAIChat", {
|
||||
defaultValue:
|
||||
"OpenAI Chat Completions (需开启路由)",
|
||||
})}
|
||||
</SelectItem>
|
||||
<SelectItem value="openai_responses">
|
||||
{t("providerForm.apiFormatOpenAIResponses", {
|
||||
defaultValue: "OpenAI Responses API (需开启路由)",
|
||||
})}
|
||||
</SelectItem>
|
||||
<SelectItem value="gemini_native">
|
||||
{t("providerForm.apiFormatGeminiNative", {
|
||||
defaultValue:
|
||||
"Gemini Native generateContent (需开启路由)",
|
||||
})}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-1 border-t border-border-default pt-4">
|
||||
|
||||
Reference in New Issue
Block a user