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
+102 -8
View File
@@ -107,6 +107,7 @@ import {
useHermesFormState,
useCopilotAuth,
useCodexOauth,
useXaiOauth,
} from "./hooks";
import { ConfirmDialog } from "@/components/ConfirmDialog";
import { useSettingsQuery } from "@/lib/query";
@@ -514,10 +515,19 @@ function ProviderFormFull({
);
// Copilot OAuth 认证状态(仅 Claude 应用需要)
const { isAuthenticated: isCopilotAuthenticated } = useCopilotAuth();
const { isAuthenticated: isCopilotAuthenticated, accounts: copilotAccounts } =
useCopilotAuth();
// Codex OAuth 认证状态(ChatGPT Plus/Pro 反代)
const { isAuthenticated: isCodexOauthAuthenticated } = useCodexOauth();
const {
isAuthenticated: isCodexOauthAuthenticated,
accounts: codexOauthAccounts,
} = useCodexOauth();
const {
isAuthenticated: isXaiOauthAuthenticated,
accounts: xaiOauthAccounts,
} = useXaiOauth();
// 选中的 GitHub 账号 ID(多账号支持)
const [selectedGitHubAccountId, setSelectedGitHubAccountId] = useState<
@@ -528,6 +538,9 @@ function ProviderFormFull({
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,
);
@@ -1146,6 +1159,9 @@ function ProviderFormFull({
const isCodexOauthProvider =
templatePreset?.providerType === "codex_oauth" ||
initialData?.meta?.providerType === "codex_oauth";
const isXaiOauthProvider =
templatePreset?.providerType === "xai_oauth" ||
initialData?.meta?.providerType === "xai_oauth";
if (isCopilotProvider && !isCopilotAuthenticated) {
toast.error(
t("copilot.loginRequired", {
@@ -1162,6 +1178,56 @@ function ProviderFormFull({
);
return;
}
if (isXaiOauthProvider && !isXaiOauthAuthenticated) {
toast.error(
t("xaiOauth.loginRequired", {
defaultValue: "请先登录 xAI 账号",
}),
);
return;
}
const selectedAccountIsUsable = (
accountId: string | null,
accounts: Array<{ id: string; requires_reauth: boolean }>,
) =>
accountId === null ||
accounts.some(
(account) => account.id === accountId && !account.requires_reauth,
);
if (
isCopilotProvider &&
!selectedAccountIsUsable(selectedGitHubAccountId, copilotAccounts)
) {
toast.error(
t("managedAuth.selectedAccountUnavailable", {
defaultValue: "已绑定账号不存在,请重新选择账号",
}),
);
return;
}
if (
isCodexOauthProvider &&
!selectedAccountIsUsable(selectedCodexAccountId, codexOauthAccounts)
) {
toast.error(
t("managedAuth.selectedAccountUnavailable", {
defaultValue: "已绑定账号不存在,请重新选择账号",
}),
);
return;
}
if (
isXaiOauthProvider &&
!selectedAccountIsUsable(selectedXaiAccountId, xaiOauthAccounts)
) {
toast.error(
t("managedAuth.selectedAccountNeedsReauth", {
defaultValue: "已绑定 xAI 账号不存在或需要重新登录",
}),
);
return;
}
// OMO Other Fields JSONB 类(格式错了保存下去数据就坏了)
if (
@@ -1198,14 +1264,19 @@ function ProviderFormFull({
// cloud_provider(如 Bedrock)通过模板变量处理认证,跳过通用校验
if (category !== "official" && category !== "cloud_provider") {
if (appId === "claude") {
if (!isCodexOauthProvider && !baseUrl.trim()) {
if (!isCodexOauthProvider && !isXaiOauthProvider && !baseUrl.trim()) {
issues.push(
t("providerForm.endpointRequired", {
defaultValue: "非官方供应商请填写 API 端点",
}),
);
}
if (!isCopilotProvider && !isCodexOauthProvider && !apiKey.trim()) {
if (
!isCopilotProvider &&
!isCodexOauthProvider &&
!isXaiOauthProvider &&
!apiKey.trim()
) {
issues.push(
t("providerForm.apiKeyRequired", {
defaultValue: "非官方供应商请填写 API Key",
@@ -1278,6 +1349,9 @@ function ProviderFormFull({
const isCodexOauthProvider =
templatePreset?.providerType === "codex_oauth" ||
initialData?.meta?.providerType === "codex_oauth";
const isXaiOauthProvider =
templatePreset?.providerType === "xai_oauth" ||
initialData?.meta?.providerType === "xai_oauth";
let settingsConfig: string;
@@ -1483,7 +1557,13 @@ function ProviderFormFull({
authProvider: "codex_oauth",
accountId: selectedCodexAccountId ?? undefined,
}
: undefined,
: isXaiOauthProvider
? {
source: "managed_account",
authProvider: "xai_oauth",
accountId: selectedXaiAccountId ?? undefined,
}
: undefined,
// GitHub Copilot 多账号:保存关联的账号 ID
githubAccountId:
isCopilotProvider && selectedGitHubAccountId
@@ -1519,7 +1599,9 @@ function ProviderFormFull({
: undefined,
apiFormat:
appId === "claude" && category !== "official"
? localApiFormat
? isXaiOauthProvider
? "openai_responses"
: localApiFormat
: appId === "codex" && category !== "official"
? localCodexApiFormat
: undefined,
@@ -1552,7 +1634,10 @@ function ProviderFormFull({
? Number(localCodexMaxOutputTokens)
: undefined,
isFullUrl:
supportsFullUrl && category !== "official" && localIsFullUrl
supportsFullUrl &&
category !== "official" &&
!isXaiOauthProvider &&
localIsFullUrl
? true
: undefined,
};
@@ -2109,13 +2194,19 @@ function ProviderFormFull({
templatePreset?.providerType === "codex_oauth" ||
initialData?.meta?.providerType === "codex_oauth"
}
isXaiOauthPreset={
templatePreset?.providerType === "xai_oauth" ||
initialData?.meta?.providerType === "xai_oauth"
}
usesOAuth={
templatePreset?.requiresOAuth === true ||
templatePreset?.providerType === "github_copilot" ||
initialData?.meta?.providerType === "github_copilot" ||
baseUrl.includes("githubcopilot.com") ||
templatePreset?.providerType === "codex_oauth" ||
initialData?.meta?.providerType === "codex_oauth"
initialData?.meta?.providerType === "codex_oauth" ||
templatePreset?.providerType === "xai_oauth" ||
initialData?.meta?.providerType === "xai_oauth"
}
isCopilotAuthenticated={isCopilotAuthenticated}
selectedGitHubAccountId={selectedGitHubAccountId}
@@ -2125,6 +2216,9 @@ function ProviderFormFull({
onCodexAccountSelect={setSelectedCodexAccountId}
codexFastMode={codexFastMode}
onCodexFastModeChange={setCodexFastMode}
isXaiOauthAuthenticated={isXaiOauthAuthenticated}
selectedXaiAccountId={selectedXaiAccountId}
onXaiAccountSelect={setSelectedXaiAccountId}
templateValueEntries={templateValueEntries}
templateValues={templateValues}
templatePresetName={templatePreset?.name || ""}