Merge pull request #2116 from KnowSky404/fix/openai-bulk-edit-compact-config

fix: add OpenAI compact bulk edit fields
This commit is contained in:
Wesley Liddick
2026-05-04 00:14:46 +08:00
committed by GitHub
2 changed files with 184 additions and 5 deletions

View File

@@ -779,6 +779,110 @@
</div>
</div>
<!-- OpenAI Compact mode -->
<div v-if="allOpenAIPassthroughCapable" class="border-t border-gray-200 pt-4 dark:border-dark-600">
<div class="mb-3 flex items-center justify-between">
<div class="flex-1 pr-4">
<label
id="bulk-edit-openai-compact-mode-label"
class="input-label mb-0"
for="bulk-edit-openai-compact-mode-enabled"
>
{{ t('admin.accounts.openai.compactMode') }}
</label>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
{{ t('admin.accounts.openai.compactModeDesc') }}
</p>
</div>
<input
v-model="enableOpenAICompactMode"
id="bulk-edit-openai-compact-mode-enabled"
type="checkbox"
aria-controls="bulk-edit-openai-compact-mode"
class="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
/>
</div>
<div
id="bulk-edit-openai-compact-mode"
:class="!enableOpenAICompactMode && 'pointer-events-none opacity-50'"
>
<Select
v-model="openAICompactMode"
data-testid="bulk-edit-openai-compact-mode-select"
:options="openAICompactModeOptions"
aria-labelledby="bulk-edit-openai-compact-mode-label"
/>
</div>
</div>
<!-- OpenAI Compact model mapping -->
<div v-if="allOpenAIPassthroughCapable" class="border-t border-gray-200 pt-4 dark:border-dark-600">
<div class="mb-3 flex items-center justify-between">
<div class="flex-1 pr-4">
<label
id="bulk-edit-openai-compact-model-mapping-label"
class="input-label mb-0"
for="bulk-edit-openai-compact-model-mapping-enabled"
>
{{ t('admin.accounts.openai.compactModelMapping') }}
</label>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
{{ t('admin.accounts.openai.compactModelMappingDesc') }}
</p>
</div>
<input
v-model="enableOpenAICompactModelMapping"
id="bulk-edit-openai-compact-model-mapping-enabled"
type="checkbox"
aria-controls="bulk-edit-openai-compact-model-mapping"
class="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
/>
</div>
<div
id="bulk-edit-openai-compact-model-mapping"
:class="!enableOpenAICompactModelMapping && 'pointer-events-none opacity-50'"
>
<div v-if="openAICompactModelMappings.length > 0" class="mb-3 space-y-2">
<div
v-for="(mapping, index) in openAICompactModelMappings"
:key="index"
class="flex items-center gap-2"
>
<input
v-model="mapping.from"
type="text"
class="input flex-1"
:placeholder="t('admin.accounts.fromModel')"
data-testid="bulk-edit-openai-compact-model-mapping-input"
/>
<span class="text-gray-400"></span>
<input
v-model="mapping.to"
type="text"
class="input flex-1"
:placeholder="t('admin.accounts.toModel')"
data-testid="bulk-edit-openai-compact-model-mapping-input"
/>
<button
type="button"
class="rounded-lg p-2 text-red-500 transition-colors hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-900/20"
@click="removeOpenAICompactModelMapping(index)"
>
<Icon name="trash" size="sm" />
</button>
</div>
</div>
<button
type="button"
class="mb-3 w-full rounded-lg border-2 border-dashed border-gray-300 px-4 py-2 text-gray-600 transition-colors hover:border-gray-400 hover:text-gray-700 dark:border-dark-500 dark:text-gray-400 dark:hover:border-dark-400 dark:hover:text-gray-300"
data-testid="bulk-edit-openai-compact-model-mapping-add"
@click="addOpenAICompactModelMapping"
>
+ {{ t('admin.accounts.addMapping') }}
</button>
</div>
</div>
<!-- RPM Limit (仅全部为 Anthropic OAuth/SetupToken 时显示) -->
<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">
@@ -989,7 +1093,7 @@ import { ref, watch, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useAppStore } from '@/stores/app'
import { adminAPI } from '@/api/admin'
import type { Proxy as ProxyConfig, AdminGroup, AccountPlatform, AccountType } from '@/types'
import type { Proxy as ProxyConfig, AdminGroup, AccountPlatform, AccountType, OpenAICompactMode } from '@/types'
import BaseDialog from '@/components/common/BaseDialog.vue'
import ConfirmDialog from '@/components/common/ConfirmDialog.vue'
import Select from '@/components/common/Select.vue'
@@ -1115,6 +1219,8 @@ const enableOpenAIPassthrough = ref(false)
const enableOpenAIWSMode = ref(false)
const enableOpenAIAPIKeyWSMode = ref(false)
const enableCodexCLIOnly = ref(false)
const enableOpenAICompactMode = ref(false)
const enableOpenAICompactModelMapping = ref(false)
const enableRpmLimit = ref(false)
// State - field values
@@ -1140,6 +1246,8 @@ const openaiPassthroughEnabled = ref(false)
const openaiOAuthResponsesWebSocketV2Mode = ref<OpenAIWSMode>(OPENAI_WS_MODE_OFF)
const openaiAPIKeyResponsesWebSocketV2Mode = ref<OpenAIWSMode>(OPENAI_WS_MODE_OFF)
const codexCLIOnlyEnabled = ref(false)
const openAICompactMode = ref<OpenAICompactMode>('auto')
const openAICompactModelMappings = ref<ModelMapping[]>([])
const rpmLimitEnabled = ref(false)
const bulkBaseRpm = ref<number | null>(null)
const bulkRpmStrategy = ref<'tiered' | 'sticky_exempt'>('tiered')
@@ -1178,6 +1286,11 @@ const openAIWSModeOptions = computed(() => [
{ value: OPENAI_WS_MODE_CTX_POOL, label: t('admin.accounts.openai.wsModeCtxPool') },
{ value: OPENAI_WS_MODE_PASSTHROUGH, label: t('admin.accounts.openai.wsModePassthrough') }
])
const openAICompactModeOptions = computed(() => [
{ value: 'auto', label: t('admin.accounts.openai.compactModeAuto') },
{ value: 'force_on', label: t('admin.accounts.openai.compactModeForceOn') },
{ value: 'force_off', label: t('admin.accounts.openai.compactModeForceOff') }
])
const openAIWSModeConcurrencyHintKey = computed(() =>
resolveOpenAIWSModeConcurrencyHintKey(openaiOAuthResponsesWebSocketV2Mode.value)
)
@@ -1194,6 +1307,14 @@ const removeModelMapping = (index: number) => {
modelMappings.value.splice(index, 1)
}
const addOpenAICompactModelMapping = () => {
openAICompactModelMappings.value.push({ from: '', to: '' })
}
const removeOpenAICompactModelMapping = (index: number) => {
openAICompactModelMappings.value.splice(index, 1)
}
const addPresetMapping = (from: string, to: string) => {
const exists = modelMappings.value.some((m) => m.from === from)
if (exists) {
@@ -1262,6 +1383,10 @@ const buildModelMappingObject = (): Record<string, string> | null => {
)
}
const buildOpenAICompactModelMapping = (): Record<string, string> | null => {
return buildModelMappingPayload('mapping', [], openAICompactModelMappings.value)
}
const buildUpdatePayload = (): Record<string, unknown> | null => {
const updates: Record<string, unknown> = {}
const credentials: Record<string, unknown> = {}
@@ -1350,10 +1475,6 @@ const buildUpdatePayload = (): Record<string, unknown> | null => {
credentialsChanged = true
}
if (credentialsChanged) {
updates.credentials = credentials
}
if (enableOpenAIWSMode.value) {
const extra = ensureExtra()
extra.openai_oauth_responses_websockets_v2_mode = openaiOAuthResponsesWebSocketV2Mode.value
@@ -1375,6 +1496,16 @@ const buildUpdatePayload = (): Record<string, unknown> | null => {
extra.codex_cli_only = codexCLIOnlyEnabled.value
}
if (enableOpenAICompactMode.value) {
const extra = ensureExtra()
extra.openai_compact_mode = openAICompactMode.value
}
if (enableOpenAICompactModelMapping.value) {
credentials.compact_model_mapping = buildOpenAICompactModelMapping() ?? {}
credentialsChanged = true
}
// RPM limit settings (写入 extra 字段)
if (enableRpmLimit.value) {
const extra = ensureExtra()
@@ -1402,6 +1533,10 @@ const buildUpdatePayload = (): Record<string, unknown> | null => {
umqExtra.user_msg_queue_enabled = false // 清理旧字段JSONB merge
}
if (credentialsChanged) {
updates.credentials = credentials
}
return Object.keys(updates).length > 0 ? updates : null
}
@@ -1467,6 +1602,8 @@ const handleSubmit = async () => {
enableOpenAIWSMode.value ||
enableOpenAIAPIKeyWSMode.value ||
enableCodexCLIOnly.value ||
enableOpenAICompactMode.value ||
enableOpenAICompactModelMapping.value ||
enableRpmLimit.value ||
userMsgQueueMode.value !== null
@@ -1567,6 +1704,8 @@ watch(
enableOpenAIWSMode.value = false
enableOpenAIAPIKeyWSMode.value = false
enableCodexCLIOnly.value = false
enableOpenAICompactMode.value = false
enableOpenAICompactModelMapping.value = false
enableRpmLimit.value = false
// Reset all values
@@ -1588,6 +1727,8 @@ watch(
openaiOAuthResponsesWebSocketV2Mode.value = OPENAI_WS_MODE_OFF
openaiAPIKeyResponsesWebSocketV2Mode.value = OPENAI_WS_MODE_OFF
codexCLIOnlyEnabled.value = false
openAICompactMode.value = 'auto'
openAICompactModelMappings.value = []
rpmLimitEnabled.value = false
bulkBaseRpm.value = null
bulkRpmStrategy.value = 'tiered'

View File

@@ -217,6 +217,44 @@ describe('BulkEditAccountModal', () => {
})
})
it('筛选 OpenAI 账号批量编辑应提交 Compact 模式和专属模型映射', async () => {
const wrapper = mountModal({
accountIds: [],
selectedPlatforms: [],
selectedTypes: [],
target: {
mode: 'filtered',
filters: { platform: 'openai' },
previewCount: 12,
selectedPlatforms: ['openai'],
selectedTypes: ['oauth', 'apikey']
}
})
await wrapper.get('#bulk-edit-openai-compact-mode-enabled').setValue(true)
await wrapper.get('[data-testid="bulk-edit-openai-compact-mode-select"]').setValue('force_on')
await wrapper.get('#bulk-edit-openai-compact-model-mapping-enabled').setValue(true)
await wrapper.get('[data-testid="bulk-edit-openai-compact-model-mapping-add"]').trigger('click')
const inputs = wrapper.findAll('[data-testid="bulk-edit-openai-compact-model-mapping-input"]')
await inputs[0].setValue('gpt-5.4')
await inputs[1].setValue('gpt-5.4-openai-compact')
await wrapper.get('#bulk-edit-account-form').trigger('submit.prevent')
await flushPromises()
expect(adminAPI.accounts.bulkUpdate).toHaveBeenCalledTimes(1)
expect(adminAPI.accounts.bulkUpdate).toHaveBeenCalledWith({
filters: { platform: 'openai' },
extra: {
openai_compact_mode: 'force_on'
},
credentials: {
compact_model_mapping: {
'gpt-5.4': 'gpt-5.4-openai-compact'
}
}
})
})
it('OpenAI 账号批量编辑可关闭自动透传', async () => {
const wrapper = mountModal({
selectedPlatforms: ['openai'],