Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
7537dce0
Unverified
Commit
7537dce0
authored
Mar 24, 2026
by
Wesley Liddick
Committed by
GitHub
Mar 24, 2026
Browse files
Merge pull request #1230 from LvyuanW/bulk-openai-oauth-ws-mode-pr
Add bulk OpenAI OAuth WS mode editing
parents
5f41b747
adbedd48
Changes
2
Hide whitespace changes
Inline
Side-by-side
frontend/src/components/account/BulkEditAccountModal.vue
View file @
7537dce0
...
@@ -599,6 +599,43 @@
...
@@ -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 {
...
@@ -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()
...
@@ -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)
...
@@ -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)
...
@@ -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(() => [
...
@@ -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 => {
...
@@ -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 => {
...
@@ -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 => {
...
@@ -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 () => {
...
@@ -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(
...
@@ -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(
...
@@ -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
'
...
...
frontend/src/components/account/__tests__/BulkEditAccountModal.spec.ts
View file @
7537dce0
...
@@ -50,7 +50,21 @@ function mountModal(extraProps: Record<string, unknown> = {}) {
...
@@ -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', () => {
...
@@ -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
)
})
})
})
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment