mirror of
https://gitee.com/wanwujie/sub2api
synced 2026-04-03 06:52:13 +08:00
fix: 修复批量编辑账号时模型白名单显示与实际不一致的问题 #982
修复批量编辑账号时,UI 显示的是 plain 模型名(如 GPT-5),但实际落库的是 dated 模型名的问题。 核心改动: 1. 批量编辑白名单不再使用 BulkEditAccountModal.vue 中手写的过期模型列表 - 移除了 allModels 和 presetMappings 的硬编码列表(共 200+ 行) - 直接复用 ModelWhitelistSelector.vue 组件 2. ModelWhitelistSelector 组件支持多平台联合过滤 - 新增 platforms 属性支持传入多个平台 - 添加 normalizedPlatforms 计算属性统一处理单平台和多平台场景 - availableOptions 根据选中的多个平台动态联合过滤模型列表 - fillRelated 功能支持一次性填充多个平台的相关模型 3. 模型映射预设改为动态生成 - filteredPresets 改用 getPresetMappingsByPlatform 从统一模型源按平台动态生成 - 不再依赖弹窗中的手写预设列表 现在的行为: - UI 显示什么模型,勾选什么模型,传给后端的就是什么模型 - 彻底解决了批量编辑链路上"显示与实际不一致"的问题 - 模型列表和映射预设始终与系统定义保持同步
This commit is contained in:
@@ -164,27 +164,10 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Model Checkbox List -->
|
||||
<div class="mb-3 grid grid-cols-2 gap-2">
|
||||
<label
|
||||
v-for="model in filteredModels"
|
||||
:key="model.value"
|
||||
class="flex cursor-pointer items-center rounded-lg border p-3 transition-all hover:bg-gray-50 dark:border-dark-600 dark:hover:bg-dark-700"
|
||||
:class="
|
||||
allowedModels.includes(model.value)
|
||||
? 'border-primary-500 bg-primary-50 dark:bg-primary-900/20'
|
||||
: 'border-gray-200'
|
||||
"
|
||||
>
|
||||
<input
|
||||
v-model="allowedModels"
|
||||
type="checkbox"
|
||||
:value="model.value"
|
||||
class="mr-2 rounded border-gray-300 text-primary-600 focus:ring-primary-500"
|
||||
/>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">{{ model.label }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<ModelWhitelistSelector
|
||||
v-model="allowedModels"
|
||||
:platforms="selectedPlatforms"
|
||||
/>
|
||||
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ 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<string, string[]> = {
|
||||
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<string, ReturnType<typeof getPresetMappingsByPlatform>[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' },
|
||||
|
||||
@@ -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<string>()
|
||||
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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user