"backend/ent/vscode:/vscode.git/clone" did not exist on "31fe017888e8cc60718391393ca4381b1a513ca3"
Commit 882518c1 authored by Wang Lvyuan's avatar Wang Lvyuan
Browse files

fix(frontend): allow clearing model restriction in bulk edit

parent 0772d925
...@@ -1056,26 +1056,23 @@ const buildUpdatePayload = (): Record<string, unknown> | null => { ...@@ -1056,26 +1056,23 @@ const buildUpdatePayload = (): Record<string, unknown> | null => {
} }
if (enableModelRestriction.value) { if (enableModelRestriction.value) {
const modelMapping = buildModelMappingObject()
// 统一使用 model_mapping 字段 // 统一使用 model_mapping 字段
if (modelRestrictionMode.value === 'whitelist') { if (modelRestrictionMode.value === 'whitelist') {
if (allowedModels.value.length > 0) {
// 白名单模式:将模型转换为 model_mapping 格式(key=value) // 白名单模式:将模型转换为 model_mapping 格式(key=value)
// 空白名单表示“支持所有模型”,需显式发送空对象以覆盖已有限制。
const mapping: Record<string, string> = {} const mapping: Record<string, string> = {}
for (const m of allowedModels.value) { for (const m of allowedModels.value) {
mapping[m] = m mapping[m] = m
} }
credentials.model_mapping = mapping credentials.model_mapping = mapping
credentialsChanged = true credentialsChanged = true
}
} else { } else {
if (modelMapping) { // 映射模式下空配置同样表示“支持所有模型”。
credentials.model_mapping = modelMapping const modelMapping = buildModelMappingObject()
credentials.model_mapping = modelMapping ?? {}
credentialsChanged = true credentialsChanged = true
} }
} }
}
if (enableCustomErrorCodes.value) { if (enableCustomErrorCodes.value) {
credentials.custom_error_codes_enabled = true credentials.custom_error_codes_enabled = true
......
import { describe, expect, it, vi } from 'vitest' import { describe, expect, it, vi, beforeEach } from 'vitest'
import { mount } from '@vue/test-utils' import { flushPromises, mount } from '@vue/test-utils'
import BulkEditAccountModal from '../BulkEditAccountModal.vue' import BulkEditAccountModal from '../BulkEditAccountModal.vue'
import ModelWhitelistSelector from '../ModelWhitelistSelector.vue'
import { adminAPI } from '@/api/admin'
vi.mock('@/stores/app', () => ({ vi.mock('@/stores/app', () => ({
useAppStore: () => ({ useAppStore: () => ({
...@@ -13,7 +15,8 @@ vi.mock('@/stores/app', () => ({ ...@@ -13,7 +15,8 @@ vi.mock('@/stores/app', () => ({
vi.mock('@/api/admin', () => ({ vi.mock('@/api/admin', () => ({
adminAPI: { adminAPI: {
accounts: { accounts: {
bulkEdit: vi.fn() bulkUpdate: vi.fn(),
checkMixedChannelRisk: vi.fn()
} }
} }
})) }))
...@@ -32,18 +35,21 @@ vi.mock('vue-i18n', async () => { ...@@ -32,18 +35,21 @@ vi.mock('vue-i18n', async () => {
} }
}) })
function mountModal() { function mountModal(extraProps: Record<string, unknown> = {}) {
return mount(BulkEditAccountModal, { return mount(BulkEditAccountModal, {
props: { props: {
show: true, show: true,
accountIds: [1, 2], accountIds: [1, 2],
selectedPlatforms: ['antigravity'], selectedPlatforms: ['antigravity'],
selectedTypes: ['apikey'],
proxies: [], proxies: [],
groups: [] groups: [],
...extraProps
} as any, } as any,
global: { global: {
stubs: { stubs: {
BaseDialog: { template: '<div><slot /><slot name="footer" /></div>' }, BaseDialog: { template: '<div><slot /><slot name="footer" /></div>' },
ConfirmDialog: true,
Select: true, Select: true,
ProxySelector: true, ProxySelector: true,
GroupSelector: true, GroupSelector: true,
...@@ -54,12 +60,30 @@ function mountModal() { ...@@ -54,12 +60,30 @@ function mountModal() {
} }
describe('BulkEditAccountModal', () => { describe('BulkEditAccountModal', () => {
it('antigravity 白名单包含 Gemini 图片模型且过滤掉普通 GPT 模型', () => { beforeEach(() => {
vi.mocked(adminAPI.accounts.bulkUpdate).mockReset()
vi.mocked(adminAPI.accounts.checkMixedChannelRisk).mockReset()
vi.mocked(adminAPI.accounts.bulkUpdate).mockResolvedValue({
success: 2,
failed: 0,
results: []
} as any)
vi.mocked(adminAPI.accounts.checkMixedChannelRisk).mockResolvedValue({
has_risk: false
} as any)
})
it('antigravity 白名单包含 Gemini 图片模型且过滤掉普通 GPT 模型', async () => {
const wrapper = mountModal() const wrapper = mountModal()
const selector = wrapper.findComponent(ModelWhitelistSelector)
expect(selector.exists()).toBe(true)
expect(wrapper.text()).toContain('Gemini 3.1 Flash Image') await selector.find('div.cursor-pointer').trigger('click')
expect(wrapper.text()).toContain('Gemini 3 Pro Image (Legacy)')
expect(wrapper.text()).not.toContain('GPT-5.3 Codex') expect(wrapper.text()).toContain('gemini-3.1-flash-image')
expect(wrapper.text()).toContain('gemini-2.5-flash-image')
expect(wrapper.text()).not.toContain('gpt-5.3-codex')
}) })
it('antigravity 映射预设包含图片映射并过滤 OpenAI 预设', async () => { it('antigravity 映射预设包含图片映射并过滤 OpenAI 预设', async () => {
...@@ -69,8 +93,26 @@ describe('BulkEditAccountModal', () => { ...@@ -69,8 +93,26 @@ describe('BulkEditAccountModal', () => {
expect(mappingTab).toBeTruthy() expect(mappingTab).toBeTruthy()
await mappingTab!.trigger('click') await mappingTab!.trigger('click')
expect(wrapper.text()).toContain('Gemini 3.1 Image') expect(wrapper.text()).toContain('3.1-Flash-Image透传')
expect(wrapper.text()).toContain('G3 Image→3.1') expect(wrapper.text()).toContain('3-Pro-Image→3.1')
expect(wrapper.text()).not.toContain('GPT-5.3 Codex') expect(wrapper.text()).not.toContain('GPT-5.3 Codex Spark')
})
it('仅勾选模型限制且白名单留空时,应提交空 model_mapping 以支持所有模型', async () => {
const wrapper = mountModal({
selectedPlatforms: ['anthropic'],
selectedTypes: ['apikey']
})
await wrapper.get('#bulk-edit-model-restriction-enabled').setValue(true)
await wrapper.get('#bulk-edit-account-form').trigger('submit.prevent')
await flushPromises()
expect(adminAPI.accounts.bulkUpdate).toHaveBeenCalledTimes(1)
expect(adminAPI.accounts.bulkUpdate).toHaveBeenCalledWith([1, 2], {
credentials: {
model_mapping: {}
}
})
}) })
}) })
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment