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>
|
||||||
</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 时显示) -->
|
<!-- 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">
|
||||||
@@ -989,7 +1093,7 @@ import { ref, watch, computed } from 'vue'
|
|||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useAppStore } from '@/stores/app'
|
import { useAppStore } from '@/stores/app'
|
||||||
import { adminAPI } from '@/api/admin'
|
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 BaseDialog from '@/components/common/BaseDialog.vue'
|
||||||
import ConfirmDialog from '@/components/common/ConfirmDialog.vue'
|
import ConfirmDialog from '@/components/common/ConfirmDialog.vue'
|
||||||
import Select from '@/components/common/Select.vue'
|
import Select from '@/components/common/Select.vue'
|
||||||
@@ -1115,6 +1219,8 @@ const enableOpenAIPassthrough = ref(false)
|
|||||||
const enableOpenAIWSMode = ref(false)
|
const enableOpenAIWSMode = ref(false)
|
||||||
const enableOpenAIAPIKeyWSMode = ref(false)
|
const enableOpenAIAPIKeyWSMode = ref(false)
|
||||||
const enableCodexCLIOnly = ref(false)
|
const enableCodexCLIOnly = ref(false)
|
||||||
|
const enableOpenAICompactMode = ref(false)
|
||||||
|
const enableOpenAICompactModelMapping = ref(false)
|
||||||
const enableRpmLimit = ref(false)
|
const enableRpmLimit = ref(false)
|
||||||
|
|
||||||
// State - field values
|
// State - field values
|
||||||
@@ -1140,6 +1246,8 @@ const openaiPassthroughEnabled = ref(false)
|
|||||||
const openaiOAuthResponsesWebSocketV2Mode = ref<OpenAIWSMode>(OPENAI_WS_MODE_OFF)
|
const openaiOAuthResponsesWebSocketV2Mode = ref<OpenAIWSMode>(OPENAI_WS_MODE_OFF)
|
||||||
const openaiAPIKeyResponsesWebSocketV2Mode = ref<OpenAIWSMode>(OPENAI_WS_MODE_OFF)
|
const openaiAPIKeyResponsesWebSocketV2Mode = ref<OpenAIWSMode>(OPENAI_WS_MODE_OFF)
|
||||||
const codexCLIOnlyEnabled = ref(false)
|
const codexCLIOnlyEnabled = ref(false)
|
||||||
|
const openAICompactMode = ref<OpenAICompactMode>('auto')
|
||||||
|
const openAICompactModelMappings = ref<ModelMapping[]>([])
|
||||||
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')
|
||||||
@@ -1178,6 +1286,11 @@ const openAIWSModeOptions = computed(() => [
|
|||||||
{ value: OPENAI_WS_MODE_CTX_POOL, label: t('admin.accounts.openai.wsModeCtxPool') },
|
{ value: OPENAI_WS_MODE_CTX_POOL, label: t('admin.accounts.openai.wsModeCtxPool') },
|
||||||
{ value: OPENAI_WS_MODE_PASSTHROUGH, label: t('admin.accounts.openai.wsModePassthrough') }
|
{ 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(() =>
|
const openAIWSModeConcurrencyHintKey = computed(() =>
|
||||||
resolveOpenAIWSModeConcurrencyHintKey(openaiOAuthResponsesWebSocketV2Mode.value)
|
resolveOpenAIWSModeConcurrencyHintKey(openaiOAuthResponsesWebSocketV2Mode.value)
|
||||||
)
|
)
|
||||||
@@ -1194,6 +1307,14 @@ const removeModelMapping = (index: number) => {
|
|||||||
modelMappings.value.splice(index, 1)
|
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 addPresetMapping = (from: string, to: string) => {
|
||||||
const exists = modelMappings.value.some((m) => m.from === from)
|
const exists = modelMappings.value.some((m) => m.from === from)
|
||||||
if (exists) {
|
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 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> = {}
|
||||||
@@ -1350,10 +1475,6 @@ const buildUpdatePayload = (): Record<string, unknown> | null => {
|
|||||||
credentialsChanged = true
|
credentialsChanged = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (credentialsChanged) {
|
|
||||||
updates.credentials = credentials
|
|
||||||
}
|
|
||||||
|
|
||||||
if (enableOpenAIWSMode.value) {
|
if (enableOpenAIWSMode.value) {
|
||||||
const extra = ensureExtra()
|
const extra = ensureExtra()
|
||||||
extra.openai_oauth_responses_websockets_v2_mode = openaiOAuthResponsesWebSocketV2Mode.value
|
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
|
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 字段)
|
// RPM limit settings (写入 extra 字段)
|
||||||
if (enableRpmLimit.value) {
|
if (enableRpmLimit.value) {
|
||||||
const extra = ensureExtra()
|
const extra = ensureExtra()
|
||||||
@@ -1402,6 +1533,10 @@ const buildUpdatePayload = (): Record<string, unknown> | null => {
|
|||||||
umqExtra.user_msg_queue_enabled = false // 清理旧字段(JSONB merge)
|
umqExtra.user_msg_queue_enabled = false // 清理旧字段(JSONB merge)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (credentialsChanged) {
|
||||||
|
updates.credentials = credentials
|
||||||
|
}
|
||||||
|
|
||||||
return Object.keys(updates).length > 0 ? updates : null
|
return Object.keys(updates).length > 0 ? updates : null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1467,6 +1602,8 @@ const handleSubmit = async () => {
|
|||||||
enableOpenAIWSMode.value ||
|
enableOpenAIWSMode.value ||
|
||||||
enableOpenAIAPIKeyWSMode.value ||
|
enableOpenAIAPIKeyWSMode.value ||
|
||||||
enableCodexCLIOnly.value ||
|
enableCodexCLIOnly.value ||
|
||||||
|
enableOpenAICompactMode.value ||
|
||||||
|
enableOpenAICompactModelMapping.value ||
|
||||||
enableRpmLimit.value ||
|
enableRpmLimit.value ||
|
||||||
userMsgQueueMode.value !== null
|
userMsgQueueMode.value !== null
|
||||||
|
|
||||||
@@ -1567,6 +1704,8 @@ watch(
|
|||||||
enableOpenAIWSMode.value = false
|
enableOpenAIWSMode.value = false
|
||||||
enableOpenAIAPIKeyWSMode.value = false
|
enableOpenAIAPIKeyWSMode.value = false
|
||||||
enableCodexCLIOnly.value = false
|
enableCodexCLIOnly.value = false
|
||||||
|
enableOpenAICompactMode.value = false
|
||||||
|
enableOpenAICompactModelMapping.value = false
|
||||||
enableRpmLimit.value = false
|
enableRpmLimit.value = false
|
||||||
|
|
||||||
// Reset all values
|
// Reset all values
|
||||||
@@ -1588,6 +1727,8 @@ watch(
|
|||||||
openaiOAuthResponsesWebSocketV2Mode.value = OPENAI_WS_MODE_OFF
|
openaiOAuthResponsesWebSocketV2Mode.value = OPENAI_WS_MODE_OFF
|
||||||
openaiAPIKeyResponsesWebSocketV2Mode.value = OPENAI_WS_MODE_OFF
|
openaiAPIKeyResponsesWebSocketV2Mode.value = OPENAI_WS_MODE_OFF
|
||||||
codexCLIOnlyEnabled.value = false
|
codexCLIOnlyEnabled.value = false
|
||||||
|
openAICompactMode.value = 'auto'
|
||||||
|
openAICompactModelMappings.value = []
|
||||||
rpmLimitEnabled.value = false
|
rpmLimitEnabled.value = false
|
||||||
bulkBaseRpm.value = null
|
bulkBaseRpm.value = null
|
||||||
bulkRpmStrategy.value = 'tiered'
|
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 () => {
|
it('OpenAI 账号批量编辑可关闭自动透传', async () => {
|
||||||
const wrapper = mountModal({
|
const wrapper = mountModal({
|
||||||
selectedPlatforms: ['openai'],
|
selectedPlatforms: ['openai'],
|
||||||
|
|||||||
Reference in New Issue
Block a user