mirror of
https://gitee.com/wanwujie/sub2api
synced 2026-05-04 21:20:51 +08:00
Merge pull request #2116 from KnowSky404/fix/openai-bulk-edit-compact-config
fix: add OpenAI compact bulk edit fields
This commit is contained in:
@@ -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'
|
||||
|
||||
@@ -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'],
|
||||
|
||||
Reference in New Issue
Block a user