From 73d72651b48cf52edf4d6c75cb2cbb2c83b33bf0 Mon Sep 17 00:00:00 2001 From: Wang Lvyuan <74089601+LvyuanW@users.noreply.github.com> Date: Mon, 23 Mar 2026 17:17:42 +0800 Subject: [PATCH] feat: support bulk OpenAI passthrough toggle --- .../account/BulkEditAccountModal.vue | 437 +++++++++++------- .../__tests__/BulkEditAccountModal.spec.ts | 75 ++- 2 files changed, 339 insertions(+), 173 deletions(-) diff --git a/frontend/src/components/account/BulkEditAccountModal.vue b/frontend/src/components/account/BulkEditAccountModal.vue index baecd6f6..68dc4fcc 100644 --- a/frontend/src/components/account/BulkEditAccountModal.vue +++ b/frontend/src/components/account/BulkEditAccountModal.vue @@ -31,6 +31,57 @@

+ +
+
+
+ +

+ {{ t('admin.accounts.openai.oauthPassthroughDesc') }} +

+
+ +
+
+ +
+
+
@@ -89,100 +140,30 @@ role="group" aria-labelledby="bulk-edit-model-restriction-label" > - -
- - -
- - -
-
-

- - - - {{ t('admin.accounts.selectAllowedModels') }} -

-
- - - -

- {{ t('admin.accounts.selectedModels', { count: allowedModels.length }) }} - {{ - t('admin.accounts.supportsAllModels') - }} +

+

+ {{ t('admin.accounts.openai.modelRestrictionDisabledByPassthrough') }}

- -
-
-

+

@@ -821,7 +883,6 @@ import { buildModelMappingObject as buildModelMappingPayload, getPresetMappingsByPlatform } from '@/composables/useModelWhitelist' - interface Props { show: boolean accountIds: number[] @@ -843,6 +904,15 @@ const appStore = useAppStore() // Platform awareness const isMixedPlatform = computed(() => props.selectedPlatforms.length > 1) +const allOpenAIPassthroughCapable = computed(() => { + return ( + props.selectedPlatforms.length === 1 && + props.selectedPlatforms[0] === 'openai' && + props.selectedTypes.length > 0 && + props.selectedTypes.every(t => t === 'oauth' || t === 'apikey') + ) +}) + // 是否全部为 Anthropic OAuth/SetupToken(RPM 配置仅在此条件下显示) const allAnthropicOAuthOrSetupToken = computed(() => { return ( @@ -886,6 +956,7 @@ const enablePriority = ref(false) const enableRateMultiplier = ref(false) const enableStatus = ref(false) const enableGroups = ref(false) +const enableOpenAIPassthrough = ref(false) const enableRpmLimit = ref(false) // State - field values @@ -907,6 +978,7 @@ const priority = ref(1) const rateMultiplier = ref(1) const status = ref<'active' | 'inactive'>('active') const groupIds = ref([]) +const openaiPassthroughEnabled = ref(false) const rpmLimitEnabled = ref(false) const bulkBaseRpm = ref(null) const bulkRpmStrategy = ref<'tiered' | 'sticky_exempt'>('tiered') @@ -933,6 +1005,11 @@ const statusOptions = computed(() => [ { value: 'active', label: t('common.active') }, { value: 'inactive', label: t('common.inactive') } ]) +const isOpenAIModelRestrictionDisabled = computed(() => + allOpenAIPassthroughCapable.value && + enableOpenAIPassthrough.value && + openaiPassthroughEnabled.value +) // Model mapping helpers const addModelMapping = () => { @@ -1015,6 +1092,12 @@ const buildUpdatePayload = (): Record | null => { const updates: Record = {} const credentials: Record = {} let credentialsChanged = false + const ensureExtra = (): Record => { + if (!updates.extra) { + updates.extra = {} + } + return updates.extra as Record + } if (enableProxy.value) { // 后端期望 proxy_id: 0 表示清除代理,而不是 null @@ -1055,7 +1138,15 @@ const buildUpdatePayload = (): Record | null => { } } - if (enableModelRestriction.value) { + if (enableOpenAIPassthrough.value) { + const extra = ensureExtra() + extra.openai_passthrough = openaiPassthroughEnabled.value + if (!openaiPassthroughEnabled.value) { + extra.openai_oauth_passthrough = false + } + } + + if (enableModelRestriction.value && !isOpenAIModelRestrictionDisabled.value) { // 统一使用 model_mapping 字段 if (modelRestrictionMode.value === 'whitelist') { // 白名单模式:将模型转换为 model_mapping 格式(key=value) @@ -1091,7 +1182,7 @@ const buildUpdatePayload = (): Record | null => { // RPM limit settings (写入 extra 字段) if (enableRpmLimit.value) { - const extra: Record = {} + const extra = ensureExtra() if (rpmLimitEnabled.value && bulkBaseRpm.value != null && bulkBaseRpm.value > 0) { extra.base_rpm = bulkBaseRpm.value extra.rpm_strategy = bulkRpmStrategy.value @@ -1111,8 +1202,7 @@ const buildUpdatePayload = (): Record | null => { // UMQ mode(独立于 RPM 保存) if (userMsgQueueMode.value !== null) { - if (!updates.extra) updates.extra = {} - const umqExtra = updates.extra as Record + const umqExtra = ensureExtra() umqExtra.user_msg_queue_mode = userMsgQueueMode.value // '' = 清除账号级覆盖 umqExtra.user_msg_queue_enabled = false // 清理旧字段(JSONB merge) } @@ -1168,6 +1258,7 @@ const handleSubmit = async () => { const hasAnyFieldEnabled = enableBaseUrl.value || + enableOpenAIPassthrough.value || enableModelRestriction.value || enableCustomErrorCodes.value || enableInterceptWarmup.value || @@ -1269,10 +1360,12 @@ watch( enableRateMultiplier.value = false enableStatus.value = false enableGroups.value = false + enableOpenAIPassthrough.value = false enableRpmLimit.value = false // Reset all values baseUrl.value = '' + openaiPassthroughEnabled.value = false modelRestrictionMode.value = 'whitelist' allowedModels.value = [] modelMappings.value = [] diff --git a/frontend/src/components/account/__tests__/BulkEditAccountModal.spec.ts b/frontend/src/components/account/__tests__/BulkEditAccountModal.spec.ts index 3598ff11..6458359e 100644 --- a/frontend/src/components/account/__tests__/BulkEditAccountModal.spec.ts +++ b/frontend/src/components/account/__tests__/BulkEditAccountModal.spec.ts @@ -50,7 +50,21 @@ function mountModal(extraProps: Record = {}) { stubs: { BaseDialog: { template: '
' }, ConfirmDialog: true, - Select: true, + Select: { + props: ['modelValue', 'options'], + emits: ['update:modelValue'], + template: ` + + ` + }, ProxySelector: true, GroupSelector: true, Icon: true @@ -115,4 +129,63 @@ describe('BulkEditAccountModal', () => { } }) }) + + it('OpenAI 账号批量编辑可开启自动透传', async () => { + const wrapper = mountModal({ + selectedPlatforms: ['openai'], + selectedTypes: ['oauth'] + }) + + await wrapper.get('#bulk-edit-openai-passthrough-enabled').setValue(true) + await wrapper.get('#bulk-edit-openai-passthrough-toggle').trigger('click') + await wrapper.get('#bulk-edit-account-form').trigger('submit.prevent') + await flushPromises() + + expect(adminAPI.accounts.bulkUpdate).toHaveBeenCalledTimes(1) + expect(adminAPI.accounts.bulkUpdate).toHaveBeenCalledWith([1, 2], { + extra: { + openai_passthrough: true + } + }) + }) + + it('OpenAI 账号批量编辑可关闭自动透传', async () => { + const wrapper = mountModal({ + selectedPlatforms: ['openai'], + selectedTypes: ['apikey'] + }) + + await wrapper.get('#bulk-edit-openai-passthrough-enabled').setValue(true) + await wrapper.get('#bulk-edit-account-form').trigger('submit.prevent') + await flushPromises() + + expect(adminAPI.accounts.bulkUpdate).toHaveBeenCalledTimes(1) + expect(adminAPI.accounts.bulkUpdate).toHaveBeenCalledWith([1, 2], { + extra: { + openai_passthrough: false, + openai_oauth_passthrough: false + } + }) + }) + + it('开启 OpenAI 自动透传时不再同时提交模型限制', async () => { + const wrapper = mountModal({ + selectedPlatforms: ['openai'], + selectedTypes: ['oauth'] + }) + + await wrapper.get('#bulk-edit-openai-passthrough-enabled').setValue(true) + await wrapper.get('#bulk-edit-openai-passthrough-toggle').trigger('click') + await wrapper.get('#bulk-edit-model-restriction-enabled').setValue(true) + await wrapper.get('#bulk-edit-account-form').trigger('submit.prevent') + await flushPromises() + + expect(adminAPI.accounts.bulkUpdate).toHaveBeenCalledTimes(1) + expect(adminAPI.accounts.bulkUpdate).toHaveBeenCalledWith([1, 2], { + extra: { + openai_passthrough: true + } + }) + expect(wrapper.text()).toContain('admin.accounts.openai.modelRestrictionDisabledByPassthrough') + }) })