Merge pull request #1230 from LvyuanW/bulk-openai-oauth-ws-mode-pr
Add bulk OpenAI OAuth WS mode editing
This commit is contained in:
@@ -599,6 +599,43 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- OpenAI OAuth WS mode -->
|
||||||
|
<div v-if="allOpenAIOAuth" class="border-t border-gray-200 pt-4 dark:border-dark-600">
|
||||||
|
<div class="mb-3 flex items-center justify-between">
|
||||||
|
<label
|
||||||
|
id="bulk-edit-openai-ws-mode-label"
|
||||||
|
class="input-label mb-0"
|
||||||
|
for="bulk-edit-openai-ws-mode-enabled"
|
||||||
|
>
|
||||||
|
{{ t('admin.accounts.openai.wsMode') }}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
v-model="enableOpenAIWSMode"
|
||||||
|
id="bulk-edit-openai-ws-mode-enabled"
|
||||||
|
type="checkbox"
|
||||||
|
aria-controls="bulk-edit-openai-ws-mode"
|
||||||
|
class="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
id="bulk-edit-openai-ws-mode"
|
||||||
|
:class="!enableOpenAIWSMode && 'pointer-events-none opacity-50'"
|
||||||
|
>
|
||||||
|
<p class="mb-3 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{{ t('admin.accounts.openai.wsModeDesc') }}
|
||||||
|
</p>
|
||||||
|
<p class="mb-3 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{{ t(openAIWSModeConcurrencyHintKey) }}
|
||||||
|
</p>
|
||||||
|
<Select
|
||||||
|
v-model="openaiOAuthResponsesWebSocketV2Mode"
|
||||||
|
data-testid="bulk-edit-openai-ws-mode-select"
|
||||||
|
:options="openAIWSModeOptions"
|
||||||
|
aria-labelledby="bulk-edit-openai-ws-mode-label"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- RPM Limit (仅全部为 Anthropic OAuth/SetupToken 时显示) -->
|
<!-- RPM Limit (仅全部为 Anthropic OAuth/SetupToken 时显示) -->
|
||||||
<div v-if="allAnthropicOAuthOrSetupToken" class="border-t border-gray-200 pt-4 dark:border-dark-600">
|
<div v-if="allAnthropicOAuthOrSetupToken" class="border-t border-gray-200 pt-4 dark:border-dark-600">
|
||||||
<div class="mb-3 flex items-center justify-between">
|
<div class="mb-3 flex items-center justify-between">
|
||||||
@@ -821,6 +858,13 @@ import {
|
|||||||
buildModelMappingObject as buildModelMappingPayload,
|
buildModelMappingObject as buildModelMappingPayload,
|
||||||
getPresetMappingsByPlatform
|
getPresetMappingsByPlatform
|
||||||
} from '@/composables/useModelWhitelist'
|
} from '@/composables/useModelWhitelist'
|
||||||
|
import {
|
||||||
|
OPENAI_WS_MODE_OFF,
|
||||||
|
OPENAI_WS_MODE_PASSTHROUGH,
|
||||||
|
isOpenAIWSModeEnabled,
|
||||||
|
resolveOpenAIWSModeConcurrencyHintKey
|
||||||
|
} from '@/utils/openaiWsMode'
|
||||||
|
import type { OpenAIWSMode } from '@/utils/openaiWsMode'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
show: boolean
|
show: boolean
|
||||||
@@ -843,6 +887,15 @@ const appStore = useAppStore()
|
|||||||
// Platform awareness
|
// Platform awareness
|
||||||
const isMixedPlatform = computed(() => props.selectedPlatforms.length > 1)
|
const isMixedPlatform = computed(() => props.selectedPlatforms.length > 1)
|
||||||
|
|
||||||
|
const allOpenAIOAuth = computed(() => {
|
||||||
|
return (
|
||||||
|
props.selectedPlatforms.length === 1 &&
|
||||||
|
props.selectedPlatforms[0] === 'openai' &&
|
||||||
|
props.selectedTypes.length > 0 &&
|
||||||
|
props.selectedTypes.every(t => t === 'oauth')
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
// 是否全部为 Anthropic OAuth/SetupToken(RPM 配置仅在此条件下显示)
|
// 是否全部为 Anthropic OAuth/SetupToken(RPM 配置仅在此条件下显示)
|
||||||
const allAnthropicOAuthOrSetupToken = computed(() => {
|
const allAnthropicOAuthOrSetupToken = computed(() => {
|
||||||
return (
|
return (
|
||||||
@@ -886,6 +939,7 @@ const enablePriority = ref(false)
|
|||||||
const enableRateMultiplier = ref(false)
|
const enableRateMultiplier = ref(false)
|
||||||
const enableStatus = ref(false)
|
const enableStatus = ref(false)
|
||||||
const enableGroups = ref(false)
|
const enableGroups = ref(false)
|
||||||
|
const enableOpenAIWSMode = ref(false)
|
||||||
const enableRpmLimit = ref(false)
|
const enableRpmLimit = ref(false)
|
||||||
|
|
||||||
// State - field values
|
// State - field values
|
||||||
@@ -907,6 +961,7 @@ const priority = ref(1)
|
|||||||
const rateMultiplier = ref(1)
|
const rateMultiplier = ref(1)
|
||||||
const status = ref<'active' | 'inactive'>('active')
|
const status = ref<'active' | 'inactive'>('active')
|
||||||
const groupIds = ref<number[]>([])
|
const groupIds = ref<number[]>([])
|
||||||
|
const openaiOAuthResponsesWebSocketV2Mode = ref<OpenAIWSMode>(OPENAI_WS_MODE_OFF)
|
||||||
const rpmLimitEnabled = ref(false)
|
const rpmLimitEnabled = ref(false)
|
||||||
const bulkBaseRpm = ref<number | null>(null)
|
const bulkBaseRpm = ref<number | null>(null)
|
||||||
const bulkRpmStrategy = ref<'tiered' | 'sticky_exempt'>('tiered')
|
const bulkRpmStrategy = ref<'tiered' | 'sticky_exempt'>('tiered')
|
||||||
@@ -933,6 +988,13 @@ const statusOptions = computed(() => [
|
|||||||
{ value: 'active', label: t('common.active') },
|
{ value: 'active', label: t('common.active') },
|
||||||
{ value: 'inactive', label: t('common.inactive') }
|
{ value: 'inactive', label: t('common.inactive') }
|
||||||
])
|
])
|
||||||
|
const openAIWSModeOptions = computed(() => [
|
||||||
|
{ value: OPENAI_WS_MODE_OFF, label: t('admin.accounts.openai.wsModeOff') },
|
||||||
|
{ value: OPENAI_WS_MODE_PASSTHROUGH, label: t('admin.accounts.openai.wsModePassthrough') }
|
||||||
|
])
|
||||||
|
const openAIWSModeConcurrencyHintKey = computed(() =>
|
||||||
|
resolveOpenAIWSModeConcurrencyHintKey(openaiOAuthResponsesWebSocketV2Mode.value)
|
||||||
|
)
|
||||||
|
|
||||||
// Model mapping helpers
|
// Model mapping helpers
|
||||||
const addModelMapping = () => {
|
const addModelMapping = () => {
|
||||||
@@ -1015,6 +1077,12 @@ const buildUpdatePayload = (): Record<string, unknown> | null => {
|
|||||||
const updates: Record<string, unknown> = {}
|
const updates: Record<string, unknown> = {}
|
||||||
const credentials: Record<string, unknown> = {}
|
const credentials: Record<string, unknown> = {}
|
||||||
let credentialsChanged = false
|
let credentialsChanged = false
|
||||||
|
const ensureExtra = (): Record<string, unknown> => {
|
||||||
|
if (!updates.extra) {
|
||||||
|
updates.extra = {}
|
||||||
|
}
|
||||||
|
return updates.extra as Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
if (enableProxy.value) {
|
if (enableProxy.value) {
|
||||||
// 后端期望 proxy_id: 0 表示清除代理,而不是 null
|
// 后端期望 proxy_id: 0 表示清除代理,而不是 null
|
||||||
@@ -1089,9 +1157,17 @@ const buildUpdatePayload = (): Record<string, unknown> | null => {
|
|||||||
updates.credentials = credentials
|
updates.credentials = credentials
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (enableOpenAIWSMode.value) {
|
||||||
|
const extra = ensureExtra()
|
||||||
|
extra.openai_oauth_responses_websockets_v2_mode = openaiOAuthResponsesWebSocketV2Mode.value
|
||||||
|
extra.openai_oauth_responses_websockets_v2_enabled = isOpenAIWSModeEnabled(
|
||||||
|
openaiOAuthResponsesWebSocketV2Mode.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// RPM limit settings (写入 extra 字段)
|
// RPM limit settings (写入 extra 字段)
|
||||||
if (enableRpmLimit.value) {
|
if (enableRpmLimit.value) {
|
||||||
const extra: Record<string, unknown> = {}
|
const extra = ensureExtra()
|
||||||
if (rpmLimitEnabled.value && bulkBaseRpm.value != null && bulkBaseRpm.value > 0) {
|
if (rpmLimitEnabled.value && bulkBaseRpm.value != null && bulkBaseRpm.value > 0) {
|
||||||
extra.base_rpm = bulkBaseRpm.value
|
extra.base_rpm = bulkBaseRpm.value
|
||||||
extra.rpm_strategy = bulkRpmStrategy.value
|
extra.rpm_strategy = bulkRpmStrategy.value
|
||||||
@@ -1111,8 +1187,7 @@ const buildUpdatePayload = (): Record<string, unknown> | null => {
|
|||||||
|
|
||||||
// UMQ mode(独立于 RPM 保存)
|
// UMQ mode(独立于 RPM 保存)
|
||||||
if (userMsgQueueMode.value !== null) {
|
if (userMsgQueueMode.value !== null) {
|
||||||
if (!updates.extra) updates.extra = {}
|
const umqExtra = ensureExtra()
|
||||||
const umqExtra = updates.extra as Record<string, unknown>
|
|
||||||
umqExtra.user_msg_queue_mode = userMsgQueueMode.value // '' = 清除账号级覆盖
|
umqExtra.user_msg_queue_mode = userMsgQueueMode.value // '' = 清除账号级覆盖
|
||||||
umqExtra.user_msg_queue_enabled = false // 清理旧字段(JSONB merge)
|
umqExtra.user_msg_queue_enabled = false // 清理旧字段(JSONB merge)
|
||||||
}
|
}
|
||||||
@@ -1178,6 +1253,7 @@ const handleSubmit = async () => {
|
|||||||
enableRateMultiplier.value ||
|
enableRateMultiplier.value ||
|
||||||
enableStatus.value ||
|
enableStatus.value ||
|
||||||
enableGroups.value ||
|
enableGroups.value ||
|
||||||
|
enableOpenAIWSMode.value ||
|
||||||
enableRpmLimit.value ||
|
enableRpmLimit.value ||
|
||||||
userMsgQueueMode.value !== null
|
userMsgQueueMode.value !== null
|
||||||
|
|
||||||
@@ -1269,6 +1345,7 @@ watch(
|
|||||||
enableRateMultiplier.value = false
|
enableRateMultiplier.value = false
|
||||||
enableStatus.value = false
|
enableStatus.value = false
|
||||||
enableGroups.value = false
|
enableGroups.value = false
|
||||||
|
enableOpenAIWSMode.value = false
|
||||||
enableRpmLimit.value = false
|
enableRpmLimit.value = false
|
||||||
|
|
||||||
// Reset all values
|
// Reset all values
|
||||||
@@ -1286,6 +1363,7 @@ watch(
|
|||||||
rateMultiplier.value = 1
|
rateMultiplier.value = 1
|
||||||
status.value = 'active'
|
status.value = 'active'
|
||||||
groupIds.value = []
|
groupIds.value = []
|
||||||
|
openaiOAuthResponsesWebSocketV2Mode.value = OPENAI_WS_MODE_OFF
|
||||||
rpmLimitEnabled.value = false
|
rpmLimitEnabled.value = false
|
||||||
bulkBaseRpm.value = null
|
bulkBaseRpm.value = null
|
||||||
bulkRpmStrategy.value = 'tiered'
|
bulkRpmStrategy.value = 'tiered'
|
||||||
|
|||||||
@@ -50,7 +50,21 @@ function mountModal(extraProps: Record<string, unknown> = {}) {
|
|||||||
stubs: {
|
stubs: {
|
||||||
BaseDialog: { template: '<div><slot /><slot name="footer" /></div>' },
|
BaseDialog: { template: '<div><slot /><slot name="footer" /></div>' },
|
||||||
ConfirmDialog: true,
|
ConfirmDialog: true,
|
||||||
Select: true,
|
Select: {
|
||||||
|
props: ['modelValue', 'options'],
|
||||||
|
emits: ['update:modelValue'],
|
||||||
|
template: `
|
||||||
|
<select
|
||||||
|
v-bind="$attrs"
|
||||||
|
:value="modelValue"
|
||||||
|
@change="$emit('update:modelValue', $event.target.value)"
|
||||||
|
>
|
||||||
|
<option v-for="option in options" :key="option.value" :value="option.value">
|
||||||
|
{{ option.label }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
`
|
||||||
|
},
|
||||||
ProxySelector: true,
|
ProxySelector: true,
|
||||||
GroupSelector: true,
|
GroupSelector: true,
|
||||||
Icon: true
|
Icon: true
|
||||||
@@ -115,4 +129,33 @@ describe('BulkEditAccountModal', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('OpenAI OAuth 批量编辑应提交 OAuth 专属 WS mode 字段', async () => {
|
||||||
|
const wrapper = mountModal({
|
||||||
|
selectedPlatforms: ['openai'],
|
||||||
|
selectedTypes: ['oauth']
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.get('#bulk-edit-openai-ws-mode-enabled').setValue(true)
|
||||||
|
await wrapper.get('[data-testid="bulk-edit-openai-ws-mode-select"]').setValue('passthrough')
|
||||||
|
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_oauth_responses_websockets_v2_mode: 'passthrough',
|
||||||
|
openai_oauth_responses_websockets_v2_enabled: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('OpenAI API Key 批量编辑不显示 WS mode 入口', () => {
|
||||||
|
const wrapper = mountModal({
|
||||||
|
selectedPlatforms: ['openai'],
|
||||||
|
selectedTypes: ['apikey']
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(wrapper.find('#bulk-edit-openai-ws-mode-enabled').exists()).toBe(false)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user