From 19d3ecc76f355fbac5026bacdac6aac54716c018 Mon Sep 17 00:00:00 2001
From: IanShaw027
Date: Sun, 15 Mar 2026 16:57:29 +0800
Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=89=B9=E9=87=8F?=
=?UTF-8?q?=E7=BC=96=E8=BE=91=E8=B4=A6=E5=8F=B7=E6=97=B6=E6=A8=A1=E5=9E=8B?=
=?UTF-8?q?=E7=99=BD=E5=90=8D=E5=8D=95=E6=98=BE=E7=A4=BA=E4=B8=8E=E5=AE=9E?=
=?UTF-8?q?=E9=99=85=E4=B8=8D=E4=B8=80=E8=87=B4=E7=9A=84=E9=97=AE=E9=A2=98?=
=?UTF-8?q?=20#982?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
修复批量编辑账号时,UI 显示的是 plain 模型名(如 GPT-5),但实际落库的是 dated 模型名的问题。
核心改动:
1. 批量编辑白名单不再使用 BulkEditAccountModal.vue 中手写的过期模型列表
- 移除了 allModels 和 presetMappings 的硬编码列表(共 200+ 行)
- 直接复用 ModelWhitelistSelector.vue 组件
2. ModelWhitelistSelector 组件支持多平台联合过滤
- 新增 platforms 属性支持传入多个平台
- 添加 normalizedPlatforms 计算属性统一处理单平台和多平台场景
- availableOptions 根据选中的多个平台动态联合过滤模型列表
- fillRelated 功能支持一次性填充多个平台的相关模型
3. 模型映射预设改为动态生成
- filteredPresets 改用 getPresetMappingsByPlatform 从统一模型源按平台动态生成
- 不再依赖弹窗中的手写预设列表
现在的行为:
- UI 显示什么模型,勾选什么模型,传给后端的就是什么模型
- 彻底解决了批量编辑链路上"显示与实际不一致"的问题
- 模型列表和映射预设始终与系统定义保持同步
---
.../account/BulkEditAccountModal.vue | 261 ++----------------
.../account/ModelWhitelistSelector.vue | 43 ++-
2 files changed, 58 insertions(+), 246 deletions(-)
diff --git a/frontend/src/components/account/BulkEditAccountModal.vue b/frontend/src/components/account/BulkEditAccountModal.vue
index c6e08684..64524d51 100644
--- a/frontend/src/components/account/BulkEditAccountModal.vue
+++ b/frontend/src/components/account/BulkEditAccountModal.vue
@@ -164,27 +164,10 @@
-
-
-
-
+
{{ t('admin.accounts.selectedModels', { count: allowedModels.length }) }}
@@ -832,8 +815,12 @@ import ConfirmDialog from '@/components/common/ConfirmDialog.vue'
import Select from '@/components/common/Select.vue'
import ProxySelector from '@/components/common/ProxySelector.vue'
import GroupSelector from '@/components/common/GroupSelector.vue'
+import ModelWhitelistSelector from '@/components/account/ModelWhitelistSelector.vue'
import Icon from '@/components/icons/Icon.vue'
-import { buildModelMappingObject as buildModelMappingPayload } from '@/composables/useModelWhitelist'
+import {
+ buildModelMappingObject as buildModelMappingPayload,
+ getPresetMappingsByPlatform
+} from '@/composables/useModelWhitelist'
interface Props {
show: boolean
@@ -865,26 +852,20 @@ const allAnthropicOAuthOrSetupToken = computed(() => {
)
})
-const platformModelPrefix: Record = {
- anthropic: ['claude-'],
- antigravity: ['claude-', 'gemini-', 'gpt-oss-', 'tab_'],
- openai: ['gpt-'],
- gemini: ['gemini-'],
- sora: []
-}
-
-const filteredModels = computed(() => {
- if (props.selectedPlatforms.length === 0) return allModels
- const prefixes = [...new Set(props.selectedPlatforms.flatMap(p => platformModelPrefix[p] || []))]
- if (prefixes.length === 0) return allModels
- return allModels.filter(m => prefixes.some(prefix => m.value.startsWith(prefix)))
-})
-
const filteredPresets = computed(() => {
- if (props.selectedPlatforms.length === 0) return presetMappings
- const prefixes = [...new Set(props.selectedPlatforms.flatMap(p => platformModelPrefix[p] || []))]
- if (prefixes.length === 0) return presetMappings
- return presetMappings.filter(m => prefixes.some(prefix => m.from.startsWith(prefix)))
+ if (props.selectedPlatforms.length === 0) return []
+
+ const dedupedPresets = new Map[number]>()
+ for (const platform of props.selectedPlatforms) {
+ for (const preset of getPresetMappingsByPlatform(platform)) {
+ const key = `${preset.from}=>${preset.to}`
+ if (!dedupedPresets.has(key)) {
+ dedupedPresets.set(key, preset)
+ }
+ }
+ }
+
+ return Array.from(dedupedPresets.values())
})
// Model mapping type
@@ -937,204 +918,6 @@ const umqModeOptions = computed(() => [
{ value: 'serialize', label: t('admin.accounts.quotaControl.rpmLimit.umqModeSerialize') },
])
-// All models list (combined Anthropic + OpenAI + Gemini)
-const allModels = [
- { value: 'claude-opus-4-6', label: 'Claude Opus 4.6' },
- { value: 'claude-sonnet-4-6', label: 'Claude Sonnet 4.6' },
- { value: 'claude-opus-4-5-20251101', label: 'Claude Opus 4.5' },
- { value: 'claude-sonnet-4-20250514', label: 'Claude Sonnet 4' },
- { value: 'claude-sonnet-4-5-20250929', label: 'Claude Sonnet 4.5' },
- { value: 'claude-3-5-haiku-20241022', label: 'Claude 3.5 Haiku' },
- { value: 'claude-haiku-4-5-20251001', label: 'Claude Haiku 4.5' },
- { value: 'claude-3-opus-20240229', label: 'Claude 3 Opus' },
- { value: 'claude-3-5-sonnet-20241022', label: 'Claude 3.5 Sonnet' },
- { value: 'claude-3-haiku-20240307', label: 'Claude 3 Haiku' },
- { value: 'gpt-5.3-codex', label: 'GPT-5.3 Codex' },
- { value: 'gpt-5.3-codex-spark', label: 'GPT-5.3 Codex Spark' },
- { value: 'gpt-5.4', label: 'GPT-5.4' },
- { value: 'gpt-5.2-2025-12-11', label: 'GPT-5.2' },
- { value: 'gpt-5.2-codex', label: 'GPT-5.2 Codex' },
- { value: 'gpt-5.1-codex-max', label: 'GPT-5.1 Codex Max' },
- { value: 'gpt-5.1-codex', label: 'GPT-5.1 Codex' },
- { value: 'gpt-5.1-2025-11-13', label: 'GPT-5.1' },
- { value: 'gpt-5.1-codex-mini', label: 'GPT-5.1 Codex Mini' },
- { value: 'gpt-5-2025-08-07', label: 'GPT-5' },
- { value: 'gemini-3.1-flash-image', label: 'Gemini 3.1 Flash Image' },
- { value: 'gemini-2.5-flash-image', label: 'Gemini 2.5 Flash Image' },
- { value: 'gemini-2.0-flash', label: 'Gemini 2.0 Flash' },
- { value: 'gemini-2.5-flash', label: 'Gemini 2.5 Flash' },
- { value: 'gemini-2.5-pro', label: 'Gemini 2.5 Pro' },
- { value: 'gemini-3-pro-image', label: 'Gemini 3 Pro Image (Legacy)' },
- { value: 'gemini-3-flash-preview', label: 'Gemini 3 Flash Preview' },
- { value: 'gemini-3-pro-preview', label: 'Gemini 3 Pro Preview' }
-]
-
-// Preset mappings (combined Anthropic + OpenAI + Gemini)
-const presetMappings = [
- {
- label: 'Sonnet 4',
- from: 'claude-sonnet-4-20250514',
- to: 'claude-sonnet-4-20250514',
- color: 'bg-blue-100 text-blue-700 hover:bg-blue-200 dark:bg-blue-900/30 dark:text-blue-400'
- },
- {
- label: 'Sonnet 4.5',
- from: 'claude-sonnet-4-5-20250929',
- to: 'claude-sonnet-4-5-20250929',
- color:
- 'bg-indigo-100 text-indigo-700 hover:bg-indigo-200 dark:bg-indigo-900/30 dark:text-indigo-400'
- },
- {
- label: 'Opus 4.5',
- from: 'claude-opus-4-5-20251101',
- to: 'claude-opus-4-5-20251101',
- color:
- 'bg-purple-100 text-purple-700 hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-400'
- },
- {
- label: 'Opus 4.6',
- from: 'claude-opus-4-6',
- to: 'claude-opus-4-6-thinking',
- color:
- 'bg-purple-100 text-purple-700 hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-400'
- },
- {
- label: 'Opus 4.6-thinking',
- from: 'claude-opus-4-6-thinking',
- to: 'claude-opus-4-6-thinking',
- color:
- 'bg-purple-100 text-purple-700 hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-400'
- },
- {
- label: 'Sonnet 4.6',
- from: 'claude-sonnet-4-6',
- to: 'claude-sonnet-4-6',
- color:
- 'bg-purple-100 text-purple-700 hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-400'
- },
- {
- label: 'Sonnet4→4.6',
- from: 'claude-sonnet-4-20250514',
- to: 'claude-sonnet-4-6',
- color: 'bg-sky-100 text-sky-700 hover:bg-sky-200 dark:bg-sky-900/30 dark:text-sky-400'
- },
- {
- label: 'Sonnet4.5→4.6',
- from: 'claude-sonnet-4-5-20250929',
- to: 'claude-sonnet-4-6',
- color: 'bg-cyan-100 text-cyan-700 hover:bg-cyan-200 dark:bg-cyan-900/30 dark:text-cyan-400'
- },
- {
- label: 'Sonnet3.5→4.6',
- from: 'claude-3-5-sonnet-20241022',
- to: 'claude-sonnet-4-6',
- color: 'bg-teal-100 text-teal-700 hover:bg-teal-200 dark:bg-teal-900/30 dark:text-teal-400'
- },
- {
- label: 'Opus4.5→4.6',
- from: 'claude-opus-4-5-20251101',
- to: 'claude-opus-4-6-thinking',
- color:
- 'bg-violet-100 text-violet-700 hover:bg-violet-200 dark:bg-violet-900/30 dark:text-violet-400'
- },
- {
- label: 'Opus->Sonnet',
- from: 'claude-opus-4-5-20251101',
- to: 'claude-sonnet-4-5-20250929',
- color: 'bg-amber-100 text-amber-700 hover:bg-amber-200 dark:bg-amber-900/30 dark:text-amber-400'
- },
- {
- label: 'Gemini 2.5 Image',
- from: 'gemini-2.5-flash-image',
- to: 'gemini-2.5-flash-image',
- color: 'bg-sky-100 text-sky-700 hover:bg-sky-200 dark:bg-sky-900/30 dark:text-sky-400'
- },
- {
- label: 'Gemini 3.1 Image',
- from: 'gemini-3.1-flash-image',
- to: 'gemini-3.1-flash-image',
- color: 'bg-sky-100 text-sky-700 hover:bg-sky-200 dark:bg-sky-900/30 dark:text-sky-400'
- },
- {
- label: 'G3 Image→3.1',
- from: 'gemini-3-pro-image',
- to: 'gemini-3.1-flash-image',
- color: 'bg-sky-100 text-sky-700 hover:bg-sky-200 dark:bg-sky-900/30 dark:text-sky-400'
- },
- {
- label: 'GPT-5.3 Codex',
- from: 'gpt-5.3-codex',
- to: 'gpt-5.3-codex',
- color: 'bg-emerald-100 text-emerald-700 hover:bg-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-400'
- },
- {
- label: 'GPT-5.3 Spark',
- from: 'gpt-5.3-codex-spark',
- to: 'gpt-5.3-codex-spark',
- color: 'bg-emerald-100 text-emerald-700 hover:bg-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-400'
- },
- {
- label: 'GPT-5.4',
- from: 'gpt-5.4',
- to: 'gpt-5.4',
- color: 'bg-rose-100 text-rose-700 hover:bg-rose-200 dark:bg-rose-900/30 dark:text-rose-400'
- },
- {
- label: '5.2→5.3',
- from: 'gpt-5.2-codex',
- to: 'gpt-5.3-codex',
- color: 'bg-lime-100 text-lime-700 hover:bg-lime-200 dark:bg-lime-900/30 dark:text-lime-400'
- },
- {
- label: 'GPT-5.2',
- from: 'gpt-5.2-2025-12-11',
- to: 'gpt-5.2-2025-12-11',
- color: 'bg-green-100 text-green-700 hover:bg-green-200 dark:bg-green-900/30 dark:text-green-400'
- },
- {
- label: 'GPT-5.2 Codex',
- from: 'gpt-5.2-codex',
- to: 'gpt-5.2-codex',
- color: 'bg-blue-100 text-blue-700 hover:bg-blue-200 dark:bg-blue-900/30 dark:text-blue-400'
- },
- {
- label: 'Max->Codex',
- from: 'gpt-5.1-codex-max',
- to: 'gpt-5.1-codex',
- color: 'bg-pink-100 text-pink-700 hover:bg-pink-200 dark:bg-pink-900/30 dark:text-pink-400'
- },
- {
- label: '3-Pro-Preview→3.1-Pro-High',
- from: 'gemini-3-pro-preview',
- to: 'gemini-3.1-pro-high',
- color: 'bg-amber-100 text-amber-700 hover:bg-amber-200 dark:bg-amber-900/30 dark:text-amber-400'
- },
- {
- label: '3-Pro-High→3.1-Pro-High',
- from: 'gemini-3-pro-high',
- to: 'gemini-3.1-pro-high',
- color: 'bg-orange-100 text-orange-700 hover:bg-orange-200 dark:bg-orange-900/30 dark:text-orange-400'
- },
- {
- label: '3-Pro-Low→3.1-Pro-Low',
- from: 'gemini-3-pro-low',
- to: 'gemini-3.1-pro-low',
- color: 'bg-yellow-100 text-yellow-700 hover:bg-yellow-200 dark:bg-yellow-900/30 dark:text-yellow-400'
- },
- {
- label: '3-Flash透传',
- from: 'gemini-3-flash',
- to: 'gemini-3-flash',
- color: 'bg-lime-100 text-lime-700 hover:bg-lime-200 dark:bg-lime-900/30 dark:text-lime-400'
- },
- {
- label: '2.5-Flash-Lite透传',
- from: 'gemini-2.5-flash-lite',
- to: 'gemini-2.5-flash-lite',
- color: 'bg-green-100 text-green-700 hover:bg-green-200 dark:bg-green-900/30 dark:text-green-400'
- }
-]
-
// Common HTTP error codes
const commonErrorCodes = [
{ value: 401, label: 'Unauthorized' },
diff --git a/frontend/src/components/account/ModelWhitelistSelector.vue b/frontend/src/components/account/ModelWhitelistSelector.vue
index 16ffa225..ebce3740 100644
--- a/frontend/src/components/account/ModelWhitelistSelector.vue
+++ b/frontend/src/components/account/ModelWhitelistSelector.vue
@@ -131,7 +131,8 @@ const { t } = useI18n()
const props = defineProps<{
modelValue: string[]
- platform: string
+ platform?: string
+ platforms?: string[]
}>()
const emit = defineEmits<{
@@ -144,11 +145,36 @@ const showDropdown = ref(false)
const searchQuery = ref('')
const customModel = ref('')
const isComposing = ref(false)
+const normalizedPlatforms = computed(() => {
+ const rawPlatforms =
+ props.platforms && props.platforms.length > 0
+ ? props.platforms
+ : props.platform
+ ? [props.platform]
+ : []
+
+ return Array.from(
+ new Set(
+ rawPlatforms
+ .map(platform => platform?.trim())
+ .filter((platform): platform is string => Boolean(platform))
+ )
+ )
+})
+
const availableOptions = computed(() => {
- if (props.platform === 'sora') {
- return getModelsByPlatform('sora').map(m => ({ value: m, label: m }))
+ if (normalizedPlatforms.value.length === 0) {
+ return allModels
}
- return allModels
+
+ const allowedModels = new Set()
+ for (const platform of normalizedPlatforms.value) {
+ for (const model of getModelsByPlatform(platform)) {
+ allowedModels.add(model)
+ }
+ }
+
+ return allModels.filter(model => allowedModels.has(model.value))
})
const filteredModels = computed(() => {
@@ -192,10 +218,13 @@ const handleEnter = () => {
}
const fillRelated = () => {
- const models = getModelsByPlatform(props.platform)
const newModels = [...props.modelValue]
- for (const model of models) {
- if (!newModels.includes(model)) newModels.push(model)
+ for (const platform of normalizedPlatforms.value) {
+ for (const model of getModelsByPlatform(platform)) {
+ if (!newModels.includes(model)) {
+ newModels.push(model)
+ }
+ }
}
emit('update:modelValue', newModels)
}