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:
Jason
2026-07-19 00:20:17 +08:00
parent 615c99c62b
commit e9317f476e
16 changed files with 936 additions and 68 deletions
@@ -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">