Skip to content

Commit adbedd4

Browse files
committed
Add bulk OpenAI OAuth WS mode editing
1 parent bda7c39 commit adbedd4

2 files changed

Lines changed: 125 additions & 4 deletions

File tree

frontend/src/components/account/BulkEditAccountModal.vue

Lines changed: 81 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -599,6 +599,43 @@
599599
</div>
600600
</div>
601601

602+
<!-- OpenAI OAuth WS mode -->
603+
<div v-if="allOpenAIOAuth" class="border-t border-gray-200 pt-4 dark:border-dark-600">
604+
<div class="mb-3 flex items-center justify-between">
605+
<label
606+
id="bulk-edit-openai-ws-mode-label"
607+
class="input-label mb-0"
608+
for="bulk-edit-openai-ws-mode-enabled"
609+
>
610+
{{ t('admin.accounts.openai.wsMode') }}
611+
</label>
612+
<input
613+
v-model="enableOpenAIWSMode"
614+
id="bulk-edit-openai-ws-mode-enabled"
615+
type="checkbox"
616+
aria-controls="bulk-edit-openai-ws-mode"
617+
class="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
618+
/>
619+
</div>
620+
<div
621+
id="bulk-edit-openai-ws-mode"
622+
:class="!enableOpenAIWSMode && 'pointer-events-none opacity-50'"
623+
>
624+
<p class="mb-3 text-xs text-gray-500 dark:text-gray-400">
625+
{{ t('admin.accounts.openai.wsModeDesc') }}
626+
</p>
627+
<p class="mb-3 text-xs text-gray-500 dark:text-gray-400">
628+
{{ t(openAIWSModeConcurrencyHintKey) }}
629+
</p>
630+
<Select
631+
v-model="openaiOAuthResponsesWebSocketV2Mode"
632+
data-testid="bulk-edit-openai-ws-mode-select"
633+
:options="openAIWSModeOptions"
634+
aria-labelledby="bulk-edit-openai-ws-mode-label"
635+
/>
636+
</div>
637+
</div>
638+
602639
<!-- RPM Limit (仅全部为 Anthropic OAuth/SetupToken 时显示) -->
603640
<div v-if="allAnthropicOAuthOrSetupToken" class="border-t border-gray-200 pt-4 dark:border-dark-600">
604641
<div class="mb-3 flex items-center justify-between">
@@ -821,6 +858,13 @@ import {
821858
buildModelMappingObject as buildModelMappingPayload,
822859
getPresetMappingsByPlatform
823860
} from '@/composables/useModelWhitelist'
861+
import {
862+
OPENAI_WS_MODE_OFF,
863+
OPENAI_WS_MODE_PASSTHROUGH,
864+
isOpenAIWSModeEnabled,
865+
resolveOpenAIWSModeConcurrencyHintKey
866+
} from '@/utils/openaiWsMode'
867+
import type { OpenAIWSMode } from '@/utils/openaiWsMode'
824868
825869
interface Props {
826870
show: boolean
@@ -843,6 +887,15 @@ const appStore = useAppStore()
843887
// Platform awareness
844888
const isMixedPlatform = computed(() => props.selectedPlatforms.length > 1)
845889
890+
const allOpenAIOAuth = computed(() => {
891+
return (
892+
props.selectedPlatforms.length === 1 &&
893+
props.selectedPlatforms[0] === 'openai' &&
894+
props.selectedTypes.length > 0 &&
895+
props.selectedTypes.every(t => t === 'oauth')
896+
)
897+
})
898+
846899
// 是否全部为 Anthropic OAuth/SetupToken(RPM 配置仅在此条件下显示)
847900
const allAnthropicOAuthOrSetupToken = computed(() => {
848901
return (
@@ -886,6 +939,7 @@ const enablePriority = ref(false)
886939
const enableRateMultiplier = ref(false)
887940
const enableStatus = ref(false)
888941
const enableGroups = ref(false)
942+
const enableOpenAIWSMode = ref(false)
889943
const enableRpmLimit = ref(false)
890944
891945
// State - field values
@@ -907,6 +961,7 @@ const priority = ref(1)
907961
const rateMultiplier = ref(1)
908962
const status = ref<'active' | 'inactive'>('active')
909963
const groupIds = ref<number[]>([])
964+
const openaiOAuthResponsesWebSocketV2Mode = ref<OpenAIWSMode>(OPENAI_WS_MODE_OFF)
910965
const rpmLimitEnabled = ref(false)
911966
const bulkBaseRpm = ref<number | null>(null)
912967
const bulkRpmStrategy = ref<'tiered' | 'sticky_exempt'>('tiered')
@@ -933,6 +988,13 @@ const statusOptions = computed(() => [
933988
{ value: 'active', label: t('common.active') },
934989
{ value: 'inactive', label: t('common.inactive') }
935990
])
991+
const openAIWSModeOptions = computed(() => [
992+
{ value: OPENAI_WS_MODE_OFF, label: t('admin.accounts.openai.wsModeOff') },
993+
{ value: OPENAI_WS_MODE_PASSTHROUGH, label: t('admin.accounts.openai.wsModePassthrough') }
994+
])
995+
const openAIWSModeConcurrencyHintKey = computed(() =>
996+
resolveOpenAIWSModeConcurrencyHintKey(openaiOAuthResponsesWebSocketV2Mode.value)
997+
)
936998
937999
// Model mapping helpers
9381000
const addModelMapping = () => {
@@ -1015,6 +1077,12 @@ const buildUpdatePayload = (): Record<string, unknown> | null => {
10151077
const updates: Record<string, unknown> = {}
10161078
const credentials: Record<string, unknown> = {}
10171079
let credentialsChanged = false
1080+
const ensureExtra = (): Record<string, unknown> => {
1081+
if (!updates.extra) {
1082+
updates.extra = {}
1083+
}
1084+
return updates.extra as Record<string, unknown>
1085+
}
10181086
10191087
if (enableProxy.value) {
10201088
// 后端期望 proxy_id: 0 表示清除代理,而不是 null
@@ -1089,9 +1157,17 @@ const buildUpdatePayload = (): Record<string, unknown> | null => {
10891157
updates.credentials = credentials
10901158
}
10911159
1160+
if (enableOpenAIWSMode.value) {
1161+
const extra = ensureExtra()
1162+
extra.openai_oauth_responses_websockets_v2_mode = openaiOAuthResponsesWebSocketV2Mode.value
1163+
extra.openai_oauth_responses_websockets_v2_enabled = isOpenAIWSModeEnabled(
1164+
openaiOAuthResponsesWebSocketV2Mode.value
1165+
)
1166+
}
1167+
10921168
// RPM limit settings (写入 extra 字段)
10931169
if (enableRpmLimit.value) {
1094-
const extra: Record<string, unknown> = {}
1170+
const extra = ensureExtra()
10951171
if (rpmLimitEnabled.value && bulkBaseRpm.value != null && bulkBaseRpm.value > 0) {
10961172
extra.base_rpm = bulkBaseRpm.value
10971173
extra.rpm_strategy = bulkRpmStrategy.value
@@ -1111,8 +1187,7 @@ const buildUpdatePayload = (): Record<string, unknown> | null => {
11111187
11121188
// UMQ mode(独立于 RPM 保存)
11131189
if (userMsgQueueMode.value !== null) {
1114-
if (!updates.extra) updates.extra = {}
1115-
const umqExtra = updates.extra as Record<string, unknown>
1190+
const umqExtra = ensureExtra()
11161191
umqExtra.user_msg_queue_mode = userMsgQueueMode.value // '' = 清除账号级覆盖
11171192
umqExtra.user_msg_queue_enabled = false // 清理旧字段(JSONB merge)
11181193
}
@@ -1178,6 +1253,7 @@ const handleSubmit = async () => {
11781253
enableRateMultiplier.value ||
11791254
enableStatus.value ||
11801255
enableGroups.value ||
1256+
enableOpenAIWSMode.value ||
11811257
enableRpmLimit.value ||
11821258
userMsgQueueMode.value !== null
11831259
@@ -1269,6 +1345,7 @@ watch(
12691345
enableRateMultiplier.value = false
12701346
enableStatus.value = false
12711347
enableGroups.value = false
1348+
enableOpenAIWSMode.value = false
12721349
enableRpmLimit.value = false
12731350
12741351
// Reset all values
@@ -1286,6 +1363,7 @@ watch(
12861363
rateMultiplier.value = 1
12871364
status.value = 'active'
12881365
groupIds.value = []
1366+
openaiOAuthResponsesWebSocketV2Mode.value = OPENAI_WS_MODE_OFF
12891367
rpmLimitEnabled.value = false
12901368
bulkBaseRpm.value = null
12911369
bulkRpmStrategy.value = 'tiered'

frontend/src/components/account/__tests__/BulkEditAccountModal.spec.ts

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,21 @@ function mountModal(extraProps: Record<string, unknown> = {}) {
5050
stubs: {
5151
BaseDialog: { template: '<div><slot /><slot name="footer" /></div>' },
5252
ConfirmDialog: true,
53-
Select: true,
53+
Select: {
54+
props: ['modelValue', 'options'],
55+
emits: ['update:modelValue'],
56+
template: `
57+
<select
58+
v-bind="$attrs"
59+
:value="modelValue"
60+
@change="$emit('update:modelValue', $event.target.value)"
61+
>
62+
<option v-for="option in options" :key="option.value" :value="option.value">
63+
{{ option.label }}
64+
</option>
65+
</select>
66+
`
67+
},
5468
ProxySelector: true,
5569
GroupSelector: true,
5670
Icon: true
@@ -115,4 +129,33 @@ describe('BulkEditAccountModal', () => {
115129
}
116130
})
117131
})
132+
133+
it('OpenAI OAuth 批量编辑应提交 OAuth 专属 WS mode 字段', async () => {
134+
const wrapper = mountModal({
135+
selectedPlatforms: ['openai'],
136+
selectedTypes: ['oauth']
137+
})
138+
139+
await wrapper.get('#bulk-edit-openai-ws-mode-enabled').setValue(true)
140+
await wrapper.get('[data-testid="bulk-edit-openai-ws-mode-select"]').setValue('passthrough')
141+
await wrapper.get('#bulk-edit-account-form').trigger('submit.prevent')
142+
await flushPromises()
143+
144+
expect(adminAPI.accounts.bulkUpdate).toHaveBeenCalledTimes(1)
145+
expect(adminAPI.accounts.bulkUpdate).toHaveBeenCalledWith([1, 2], {
146+
extra: {
147+
openai_oauth_responses_websockets_v2_mode: 'passthrough',
148+
openai_oauth_responses_websockets_v2_enabled: true
149+
}
150+
})
151+
})
152+
153+
it('OpenAI API Key 批量编辑不显示 WS mode 入口', () => {
154+
const wrapper = mountModal({
155+
selectedPlatforms: ['openai'],
156+
selectedTypes: ['apikey']
157+
})
158+
159+
expect(wrapper.find('#bulk-edit-openai-ws-mode-enabled').exists()).toBe(false)
160+
})
118161
})

0 commit comments

Comments
 (0)