From eb2b8632ded614bf991d7d36abfa38b513ad8c2d Mon Sep 17 00:00:00 2001 From: shaw Date: Wed, 15 Jul 2026 22:05:59 +0800 Subject: [PATCH] =?UTF-8?q?fix(frontend):=20=E4=BF=AE=E5=A4=8D=20Grok=20OA?= =?UTF-8?q?uth=20=E5=BB=BA=E5=8F=B7=E7=BC=BA=E4=B8=8A=E6=B8=B8=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E5=85=A5=E5=8F=A3=E4=B8=8E=20JSON=20=E5=AF=BC?= =?UTF-8?q?=E5=85=A5=E6=B8=B2=E6=9F=93=E5=B4=A9=E6=BA=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CreateAccountModal 为 Grok OAuth 新增自定义上游地址与请求头覆写区块, 经统一校验注入授权码兑换/RT 批量/SSO 批量三条创建路径(授权码路径 在兑换前校验,避免烧掉一次性 code) - JSON 导入示例文案内嵌花括号被 vue-i18n 消息编译器当占位符解析, 渲染时抛 Invalid token in placeholder 炸掉面板:示例移出 i18n 硬编码; 同类修复 settings 的死键 claudeOAuthSystemPromptBlocksPlaceholder(删) 与 Hint 中被吞掉的 {billing_header}(字面量转义) - 新增 localesMessageCompile 守卫测试:预编译中英全部文案,拦截非法占位符 - 面板打开后 scrollIntoView + 聚焦,避免在弹窗滚动容器视野外展开 - 删除「填入模板」按钮及三平台模板常量(产品决策:无意义) - 抽取 HeaderOverrideEditor 共享组件,消除 Create/Edit/BulkEdit 三处重复 --- .../account/BulkEditAccountModal.vue | 108 +-------- .../components/account/CreateAccountModal.vue | 222 +++++++++++------- .../components/account/EditAccountModal.vue | 97 +------- .../account/HeaderOverrideEditor.vue | 84 +++++++ .../account/HeaderOverrideJsonTools.vue | 21 +- .../__tests__/CreateAccountModal.grok.spec.ts | 12 + .../__tests__/credentialsBuilder.spec.ts | 34 --- .../components/account/credentialsBuilder.ts | 40 ---- .../__tests__/localesMessageCompile.spec.ts | 41 ++++ .../src/i18n/locales/en/admin/accounts.ts | 2 - .../src/i18n/locales/en/admin/settings.ts | 3 +- .../src/i18n/locales/zh/admin/accounts.ts | 2 - .../src/i18n/locales/zh/admin/settings.ts | 3 +- 13 files changed, 298 insertions(+), 371 deletions(-) create mode 100644 frontend/src/components/account/HeaderOverrideEditor.vue create mode 100644 frontend/src/i18n/__tests__/localesMessageCompile.spec.ts diff --git a/frontend/src/components/account/BulkEditAccountModal.vue b/frontend/src/components/account/BulkEditAccountModal.vue index 6734031950..f6d941ea4a 100644 --- a/frontend/src/components/account/BulkEditAccountModal.vue +++ b/frontend/src/components/account/BulkEditAccountModal.vue @@ -538,75 +538,10 @@ {{ t('admin.accounts.headerOverride.bulkReplaceHint') }}

-
-
- - - -
-
- - - -
- - -
- -

- {{ t('admin.accounts.headerOverride.emptyValueHint') }} -

+

{{ t('admin.accounts.headerOverride.bulkDisableHint') }} @@ -1277,17 +1212,15 @@ import { buildModelMappingObject as buildModelMappingPayload, getPresetMappingsByPlatform } from '@/composables/useModelWhitelist' -import HeaderOverrideJsonTools from '@/components/account/HeaderOverrideJsonTools.vue' +import HeaderOverrideEditor from '@/components/account/HeaderOverrideEditor.vue' import { buildHeaderOverridesObject, - getHeaderOverrideTemplate, isHeaderOverrideCapable, validateHeaderOverrideRows, HEADER_OVERRIDE_ENABLED_CREDENTIAL_KEY, HEADER_OVERRIDES_CREDENTIAL_KEY, type HeaderOverrideRow } from '@/components/account/credentialsBuilder' -import { createStableObjectKeyResolver } from '@/utils/stableObjectKey' import { OPENAI_WS_MODE_CTX_POOL, OPENAI_WS_MODE_OFF, @@ -1436,37 +1369,6 @@ const customErrorCodeInput = ref(null) const interceptWarmupRequests = ref(false) const headerOverrideEnabled = ref(false) const headerOverrideRows = ref([]) -const getHeaderOverrideRowKey = createStableObjectKeyResolver('bulk-header-override-row') - -const addHeaderOverrideRow = () => { - headerOverrideRows.value.push({ name: '', value: '' }) -} - -const removeHeaderOverrideRow = (index: number) => { - headerOverrideRows.value.splice(index, 1) -} - -// 模板仅在所选账号平台唯一时可用:混合 anthropic+openai 选择无法确定用哪套模板, -// 误填会把另一平台的专有头写进所有所选账号 -const headerOverrideTemplatePlatform = computed(() => { - return targetSelectedPlatforms.value.length === 1 ? targetSelectedPlatforms.value[0] : null -}) - -// 模板按钮:填入所选平台的标准客户端请求头名称(值留空),跳过已存在的同名行 -const fillHeaderOverrideTemplate = () => { - const platform = headerOverrideTemplatePlatform.value - if (!platform) return - const existing = new Set( - headerOverrideRows.value.map((row) => row.name.trim().toLowerCase()).filter(Boolean) - ) - const rows = headerOverrideRows.value.filter((row) => row.name.trim() || row.value.trim()) - for (const row of getHeaderOverrideTemplate(platform)) { - if (!existing.has(row.name)) { - rows.push(row) - } - } - headerOverrideRows.value = rows -} const proxyId = ref(null) const concurrency = ref(1) const loadFactor = ref(null) diff --git a/frontend/src/components/account/CreateAccountModal.vue b/frontend/src/components/account/CreateAccountModal.vue index 6132a1f1e1..5c21f95fd3 100644 --- a/frontend/src/components/account/CreateAccountModal.vue +++ b/frontend/src/components/account/CreateAccountModal.vue @@ -1533,74 +1533,10 @@

-
-
- - - -
-
- - - -
- - -
- -

- {{ t('admin.accounts.headerOverride.emptyValueHint') }} -

+ @@ -1968,6 +1904,90 @@ /> + +
+
+
+ +

+ {{ t('admin.accounts.grokCustomBaseUrl.hint') }} +

+
+ +
+
+ +
+
+ + +
+
+
+ +

+ {{ t('admin.accounts.headerOverride.hint') }} +

+
+ +
+ +
+
+

+ + {{ t('admin.accounts.headerOverride.info') }} +

+
+ + +
+
+
(null) const headerOverrideEnabled = ref(false) const headerOverrideRows = ref([]) -const addHeaderOverrideRow = () => { - headerOverrideRows.value.push({ name: '', value: '' }) -} +// Grok OAuth:自定义上游地址(base_url 仅改写转发端点,OAuth 授权/刷新不受影响) +const grokOAuthCustomBaseUrlEnabled = ref(false) +const grokOAuthBaseUrl = ref('') -const removeHeaderOverrideRow = (index: number) => { - headerOverrideRows.value.splice(index, 1) -} - -// 模板按钮:填入标准客户端请求头名称(值留空),跳过已存在的同名行 -const fillHeaderOverrideTemplate = () => { - const existing = new Set( - headerOverrideRows.value.map((row) => row.name.trim().toLowerCase()).filter(Boolean) - ) - const rows = headerOverrideRows.value.filter((row) => row.name.trim() || row.value.trim()) - for (const row of getHeaderOverrideTemplate(form.platform)) { - if (!existing.has(row.name)) { - rows.push(row) +// Grok OAuth 三条创建路径(授权码/RT 批量/SSO 批量)共用的前置校验。 +// 授权码路径必须在兑换 code 之前调用,避免校验失败时白白消耗一次性授权码。 +const validateGrokOAuthUpstreamConfig = (): boolean => { + if (grokOAuthCustomBaseUrlEnabled.value) { + const trimmed = grokOAuthBaseUrl.value.trim() + if (!trimmed) { + appStore.showError(t('admin.accounts.grokCustomBaseUrl.required')) + return false + } + if (!/^https?:\/\//i.test(trimmed)) { + appStore.showError(t('admin.accounts.grokCustomBaseUrl.invalid')) + return false } } - headerOverrideRows.value = rows + if (headerOverrideEnabled.value) { + const headerError = validateHeaderOverrideRows(headerOverrideRows.value) + if (headerError) { + appStore.showError(t(`admin.accounts.headerOverride.${headerError}`)) + return false + } + } + return true +} + +// 把已通过校验的自定义上游地址与请求头覆写写入 credentials +const applyGrokOAuthUpstreamConfig = (credentials: Record) => { + if (grokOAuthCustomBaseUrlEnabled.value) { + credentials.base_url = grokOAuthBaseUrl.value.trim() + } + applyHeaderOverride(credentials, headerOverrideEnabled.value, headerOverrideRows.value, 'create') } const interceptWarmupRequests = ref(false) const autoPauseOnExpired = ref(true) @@ -3780,7 +3813,6 @@ const vertexServiceAccountDragActive = ref(false) const tempUnschedEnabled = ref(false) const tempUnschedRules = ref([]) const getModelMappingKey = createStableObjectKeyResolver('create-model-mapping') -const getHeaderOverrideRowKey = createStableObjectKeyResolver('create-header-override-row') const getOpenAICompactModelMappingKey = createStableObjectKeyResolver('create-openai-compact-model-mapping') const getAntigravityModelMappingKey = createStableObjectKeyResolver('create-antigravity-model-mapping') const getTempUnschedRuleKey = createStableObjectKeyResolver('create-temp-unsched-rule') @@ -4184,10 +4216,12 @@ watch( anthropicAPIKeyAuthScheme.value = 'x_api_key' webSearchEmulationMode.value = 'default' } - // 请求头覆写为平台相关配置(模板/常用头集合不同),切换平台时清空, - // 避免上一平台的模板行被提交到新平台账号 + // 请求头覆写为平台相关配置(常用头集合不同),切换平台时清空, + // 避免上一平台的配置行被提交到新平台账号 headerOverrideEnabled.value = false headerOverrideRows.value = [] + grokOAuthCustomBaseUrlEnabled.value = false + grokOAuthBaseUrl.value = '' // Reset OAuth states oauth.resetState() openaiOAuth.resetState() @@ -4579,6 +4613,8 @@ const resetForm = () => { customErrorCodeInput.value = null headerOverrideEnabled.value = false headerOverrideRows.value = [] + grokOAuthCustomBaseUrlEnabled.value = false + grokOAuthBaseUrl.value = '' interceptWarmupRequests.value = false autoPauseOnExpired.value = true openaiPassthroughEnabled.value = false @@ -5194,6 +5230,7 @@ const handleGrokValidateRT = async (refreshTokenInput: string) => { grokOAuth.error.value = t('admin.accounts.oauth.grok.pleaseEnterRefreshToken') return } + if (!validateGrokOAuthUpstreamConfig()) return grokOAuth.loading.value = true grokOAuth.error.value = '' @@ -5214,6 +5251,7 @@ const handleGrokValidateRT = async (refreshTokenInput: string) => { } const credentials = grokOAuth.buildCredentials(tokenInfo) + applyGrokOAuthUpstreamConfig(credentials) const extra = grokOAuth.buildExtraInfo(tokenInfo) const accountName = refreshTokens.length > 1 ? `${form.name || tokenInfo.email || 'Grok OAuth Account'} #${i + 1}` : (form.name || tokenInfo.email || 'Grok OAuth Account') @@ -5277,11 +5315,13 @@ const handleGrokImportSSO = async (ssoInput: string) => { .map((token) => token.trim()) .filter((token) => token) if (ssoTokens.length === 0) return + if (!validateGrokOAuthUpstreamConfig()) return grokOAuth.loading.value = true grokOAuth.error.value = '' const credentials: Record = {} + applyGrokOAuthUpstreamConfig(credentials) const modelMapping = buildModelMappingObject(modelRestrictionMode.value, allowedModels.value, modelMappings.value) if (modelMapping) { credentials.model_mapping = modelMapping @@ -5904,6 +5944,7 @@ const handleAntigravityExchange = async (authCode: string) => { // Grok OAuth 授权码兑换 const handleGrokExchange = async (authCode: string) => { if (!authCode.trim() || !grokOAuth.sessionId.value) return + if (!validateGrokOAuthUpstreamConfig()) return grokOAuth.loading.value = true grokOAuth.error.value = '' @@ -5926,6 +5967,7 @@ const handleGrokExchange = async (authCode: string) => { if (!tokenInfo) return const credentials = grokOAuth.buildCredentials(tokenInfo) + applyGrokOAuthUpstreamConfig(credentials) const extra = grokOAuth.buildExtraInfo(tokenInfo) await createAccountAndFinish('grok', 'oauth', credentials, extra) } catch (error: any) { diff --git a/frontend/src/components/account/EditAccountModal.vue b/frontend/src/components/account/EditAccountModal.vue index 12dd469c65..418fb62d88 100644 --- a/frontend/src/components/account/EditAccountModal.vue +++ b/frontend/src/components/account/EditAccountModal.vue @@ -497,74 +497,10 @@

-
-
- - - -
-
- - - -
- - -
- -

- {{ t('admin.accounts.headerOverride.emptyValueHint') }} -

+ @@ -2630,7 +2566,7 @@ import ProxyAdBanner from '@/components/common/ProxyAdBanner.vue' import GroupSelector from '@/components/common/GroupSelector.vue' import ModelWhitelistSelector from '@/components/account/ModelWhitelistSelector.vue' import QuotaLimitCard from '@/components/account/QuotaLimitCard.vue' -import HeaderOverrideJsonTools from '@/components/account/HeaderOverrideJsonTools.vue' +import HeaderOverrideEditor from '@/components/account/HeaderOverrideEditor.vue' import { applyAntigravityProjectID, applyHeaderOverride, @@ -2638,7 +2574,6 @@ import { applyPlanType, buildPlanTypeOptions, readPlanType, - getHeaderOverrideTemplate, isCustomGrokBaseUrl, isHeaderOverrideCapable, splitHeaderOverridesObject, @@ -2788,27 +2723,6 @@ const headerOverrideCapable = computed( const grokOAuthCustomBaseUrlEnabled = ref(false) const grokOAuthBaseUrl = ref('') -const addHeaderOverrideRow = () => { - headerOverrideRows.value.push({ name: '', value: '' }) -} - -const removeHeaderOverrideRow = (index: number) => { - headerOverrideRows.value.splice(index, 1) -} - -// 模板按钮:填入标准客户端请求头名称(值留空),跳过已存在的同名行 -const fillHeaderOverrideTemplate = () => { - const existing = new Set( - headerOverrideRows.value.map((row) => row.name.trim().toLowerCase()).filter(Boolean) - ) - const rows = headerOverrideRows.value.filter((row) => row.name.trim() || row.value.trim()) - for (const row of getHeaderOverrideTemplate(props.account?.platform || '')) { - if (!existing.has(row.name)) { - rows.push(row) - } - } - headerOverrideRows.value = rows -} const interceptWarmupRequests = ref(false) const autoPauseOnExpired = ref(false) const autoPause5hThreshold = ref(null) @@ -2825,7 +2739,6 @@ const isSyncingAntigravityUpstream = ref(false) const tempUnschedEnabled = ref(false) const tempUnschedRules = ref([]) const getModelMappingKey = createStableObjectKeyResolver('edit-model-mapping') -const getHeaderOverrideRowKey = createStableObjectKeyResolver('edit-header-override-row') const getOpenAICompactModelMappingKey = createStableObjectKeyResolver('edit-openai-compact-model-mapping') const getAntigravityModelMappingKey = createStableObjectKeyResolver('edit-antigravity-model-mapping') const getTempUnschedRuleKey = createStableObjectKeyResolver('edit-temp-unsched-rule') diff --git a/frontend/src/components/account/HeaderOverrideEditor.vue b/frontend/src/components/account/HeaderOverrideEditor.vue new file mode 100644 index 0000000000..d84bbf6192 --- /dev/null +++ b/frontend/src/components/account/HeaderOverrideEditor.vue @@ -0,0 +1,84 @@ + + + diff --git a/frontend/src/components/account/HeaderOverrideJsonTools.vue b/frontend/src/components/account/HeaderOverrideJsonTools.vue index 04391a62a3..f2afad2860 100644 --- a/frontend/src/components/account/HeaderOverrideJsonTools.vue +++ b/frontend/src/components/account/HeaderOverrideJsonTools.vue @@ -15,12 +15,13 @@ {{ t('admin.accounts.headerOverride.copyJson') }} -
+