feat(models): add external catalog proxy selection

This commit is contained in:
elky
2026-07-31 09:32:25 +08:00
parent 12057db476
commit beb003b7ad
34 changed files with 2286 additions and 176 deletions
+41 -2
View File
@@ -2,23 +2,62 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
const apiMocks = vi.hoisted(() => ({
get: vi.fn(),
put: vi.fn(),
delete: vi.fn(),
}))
vi.mock('@/api/client', () => ({
default: { get: apiMocks.get, delete: apiMocks.delete },
default: { get: apiMocks.get, put: apiMocks.put, delete: apiMocks.delete },
}))
import { clearModelsDevCache, getModelsDevList, refreshModelsDevList } from '@/api/models-dev'
import {
clearModelsDevCache,
getExternalModelsAccessConfig,
getModelsDevList,
refreshModelsDevList,
updateExternalModelsAccessConfig,
} from '@/api/models-dev'
beforeEach(() => {
clearModelsDevCache()
localStorage.clear()
apiMocks.get.mockReset()
apiMocks.put.mockReset()
apiMocks.delete.mockReset()
apiMocks.delete.mockResolvedValue({ data: { cleared: true } })
})
describe('external models access config', () => {
it('reads the configured proxy node', async () => {
apiMocks.get.mockResolvedValue({ data: { proxy_node_id: 'proxy-1' } })
await expect(getExternalModelsAccessConfig()).resolves.toEqual({
proxy_node_id: 'proxy-1',
})
expect(apiMocks.get).toHaveBeenCalledWith('/api/admin/models/external/config')
})
it.each([
['a proxy node', 'proxy-1'],
['direct access', null],
] as const)('updates %s and clears the browser cache', async (_label, proxyNodeId) => {
localStorage.setItem('models_dev_cache', JSON.stringify({ timestamp: Date.now(), data: {} }))
apiMocks.put.mockResolvedValue({
data: { proxy_node_id: proxyNodeId, cache_cleared: true },
})
await expect(updateExternalModelsAccessConfig(proxyNodeId)).resolves.toEqual({
proxy_node_id: proxyNodeId,
cache_cleared: true,
})
expect(apiMocks.put).toHaveBeenCalledWith(
'/api/admin/models/external/config',
{ proxy_node_id: proxyNodeId },
)
expect(localStorage.getItem('models_dev_cache')).toBeNull()
})
})
describe('getModelsDevList', () => {
it('uses current modalities and experimental mode pricing while keeping legacy fallbacks', async () => {
apiMocks.get.mockResolvedValue({
+31
View File
@@ -73,6 +73,14 @@ export interface ModelsDevProvider {
export type ModelsDevData = Record<string, ModelsDevProvider>
export interface ExternalModelsAccessConfig {
proxy_node_id: string | null
}
export interface ExternalModelsAccessConfigUpdate extends ExternalModelsAccessConfig {
cache_cleared: boolean
}
// 扁平化的模型列表项(用于搜索和选择)
export interface ModelsDevModelItem {
providerId: string
@@ -165,6 +173,29 @@ export async function getModelsDevData(): Promise<ModelsDevData> {
return data
}
/**
* 获取 models.dev 目录的出站访问配置。
*/
export async function getExternalModelsAccessConfig(): Promise<ExternalModelsAccessConfig> {
const response = await api.get<ExternalModelsAccessConfig>('/api/admin/models/external/config')
return response.data
}
/**
* 更新 models.dev 目录的代理节点。null 表示直连。
* 后端会清理共享目录缓存;前端同时清理内存和 localStorage 缓存。
*/
export async function updateExternalModelsAccessConfig(
proxyNodeId: string | null,
): Promise<ExternalModelsAccessConfigUpdate> {
const response = await api.put<ExternalModelsAccessConfigUpdate>(
'/api/admin/models/external/config',
{ proxy_node_id: proxyNodeId },
)
clearModelsDevCache()
return response.data
}
// 模型列表缓存(避免重复转换)
let modelsListCache: ModelsDevModelItem[] | null = null
let modelsListCacheTimestamp: number | null = null
+12 -2
View File
@@ -269,8 +269,18 @@ export const proxyNodesApi = {
return response.data
},
async deleteProxyNode(nodeId: string): Promise<{ message: string; node_id: string; cleared_system_proxy: boolean }> {
const response = await apiClient.delete<{ message: string; node_id: string; cleared_system_proxy: boolean }>(`/api/admin/proxy-nodes/${nodeId}`)
async deleteProxyNode(nodeId: string): Promise<{
message: string
node_id: string
cleared_system_proxy: boolean
cleared_external_models_proxy: boolean
}> {
const response = await apiClient.delete<{
message: string
node_id: string
cleared_system_proxy: boolean
cleared_external_models_proxy: boolean
}>(`/api/admin/proxy-nodes/${nodeId}`)
return response.data
},
@@ -0,0 +1,116 @@
<template>
<div
data-testid="external-models-access-control"
>
<Popover
:open="popoverOpen"
@update:open="popoverOpen = $event"
>
<PopoverTrigger as-child>
<Button
variant="ghost"
size="icon"
class="h-8 w-8"
:class="proxyNodeId ? 'text-blue-500' : ''"
:disabled="loading || !configLoaded"
:title="t('models.externalCatalog.accessTitle')"
:aria-label="t('models.externalCatalog.accessTitle')"
data-testid="external-models-access-trigger"
>
<Globe class="w-3.5 h-3.5" />
</Button>
</PopoverTrigger>
<PopoverContent
class="w-72 p-3"
side="bottom"
align="end"
>
<div class="space-y-2">
<div class="flex items-center justify-between">
<span class="text-xs font-medium">
{{ t('models.externalCatalog.accessTitle') }}
</span>
<Button
v-if="proxyNodeId"
variant="ghost"
size="sm"
class="h-6 px-2 text-[10px] text-muted-foreground"
:disabled="loading"
data-testid="external-models-access-clear"
@click="updateAccessConfig(null)"
>
{{ legacyT('清除') }}
</Button>
</div>
<ProxyNodeSelect
:model-value="proxyNodeId || ''"
:disabled="loading || !configLoaded"
:trigger-aria-label="t('models.externalCatalog.accessTitle')"
trigger-class="h-8"
data-testid="external-models-access-select"
@update:model-value="updateAccessConfig"
/>
<p class="text-[10px] text-muted-foreground">
{{ t(proxyNodeId
? 'models.externalCatalog.proxyEnabled'
: 'models.externalCatalog.direct') }}
</p>
</div>
</PopoverContent>
</Popover>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { Globe } from 'lucide-vue-next'
import {
getExternalModelsAccessConfig,
updateExternalModelsAccessConfig,
} from '@/api/models-dev'
import { Button, Popover, PopoverContent, PopoverTrigger } from '@/components/ui'
import ProxyNodeSelect from '@/features/providers/components/ProxyNodeSelect.vue'
import { useToast } from '@/composables/useToast'
import { useI18n } from '@/i18n'
import { parseApiError } from '@/utils/errorParser'
const { success, error: showError } = useToast()
const { t, legacyT } = useI18n()
const proxyNodeId = ref<string | null>(null)
const popoverOpen = ref(false)
const configLoaded = ref(false)
const loading = ref(true)
async function loadAccessConfig() {
loading.value = true
try {
const config = await getExternalModelsAccessConfig()
proxyNodeId.value = config.proxy_node_id
configLoaded.value = true
} catch (err: unknown) {
showError(parseApiError(err, t('models.externalCatalog.loadFailed')))
} finally {
loading.value = false
}
}
async function updateAccessConfig(nextProxyNodeId: string | null) {
if (!configLoaded.value || loading.value) return
if (nextProxyNodeId === proxyNodeId.value) return
loading.value = true
try {
const result = await updateExternalModelsAccessConfig(nextProxyNodeId)
proxyNodeId.value = result.proxy_node_id
popoverOpen.value = false
success(t('models.externalCatalog.saved'))
} catch (err: unknown) {
showError(parseApiError(err, t('models.externalCatalog.saveFailed')))
} finally {
loading.value = false
}
}
onMounted(loadAccessConfig)
</script>
@@ -0,0 +1,222 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { createApp, nextTick } from 'vue'
const modelsDevMocks = vi.hoisted(() => ({
getExternalModelsAccessConfig: vi.fn(),
updateExternalModelsAccessConfig: vi.fn(),
}))
const toastMocks = vi.hoisted(() => ({
success: vi.fn(),
error: vi.fn(),
}))
vi.mock('@/api/models-dev', () => modelsDevMocks)
vi.mock('@/composables/useToast', () => ({
useToast: () => toastMocks,
}))
vi.mock('@/components/ui', async () => {
const { defineComponent, h } = await import('vue')
const passthrough = (name: string) => defineComponent({
name,
inheritAttrs: false,
setup(_, { attrs, slots }) {
return () => h('div', attrs, slots.default?.())
},
})
return {
Button: defineComponent({
name: 'ButtonStub',
inheritAttrs: false,
props: {
disabled: { type: Boolean, default: false },
},
setup(props, { attrs, slots }) {
return () => h('button', {
...attrs,
disabled: props.disabled,
type: 'button',
}, slots.default?.())
},
}),
Popover: defineComponent({
name: 'PopoverStub',
inheritAttrs: false,
props: {
open: { type: Boolean, default: false },
},
emits: ['update:open'],
setup(props, { attrs, slots }) {
return () => h('div', {
...attrs,
'data-popover-open': String(props.open),
}, slots.default?.())
},
}),
PopoverTrigger: passthrough('PopoverTriggerStub'),
PopoverContent: passthrough('PopoverContentStub'),
}
})
vi.mock('lucide-vue-next', async () => {
const { defineComponent, h } = await import('vue')
return {
Globe: defineComponent({
name: 'GlobeStub',
setup() {
return () => h('span', { 'data-testid': 'external-models-access-globe' })
},
}),
}
})
vi.mock('@/features/providers/components/ProxyNodeSelect.vue', async () => {
const { defineComponent, h } = await import('vue')
return {
default: defineComponent({
name: 'ProxyNodeSelectStub',
inheritAttrs: false,
props: {
modelValue: { type: String, required: true },
disabled: { type: Boolean, default: false },
},
emits: ['update:modelValue'],
setup(props, { attrs, emit }) {
return () => h('div', {
...attrs,
'data-model-value': props.modelValue,
'data-disabled': String(props.disabled),
}, [
h('button', {
type: 'button',
'data-testid': 'select-proxy-node',
onClick: () => emit('update:modelValue', 'proxy-1'),
}),
])
},
}),
}
})
import ExternalModelsAccessControl from '@/features/models/components/ExternalModelsAccessControl.vue'
import { createI18n, setI18nLocale } from '@/i18n'
interface MountedControl {
app: ReturnType<typeof createApp>
root: HTMLDivElement
}
const mountedControls: MountedControl[] = []
function mountControl(): MountedControl {
const root = document.createElement('div')
document.body.appendChild(root)
const app = createApp(ExternalModelsAccessControl)
app.use(createI18n())
app.mount(root)
const mounted = { app, root }
mountedControls.push(mounted)
return mounted
}
async function flushAsyncState() {
await Promise.resolve()
await Promise.resolve()
await nextTick()
}
beforeEach(() => {
setI18nLocale('zh-CN')
modelsDevMocks.getExternalModelsAccessConfig.mockReset()
modelsDevMocks.getExternalModelsAccessConfig.mockResolvedValue({ proxy_node_id: null })
modelsDevMocks.updateExternalModelsAccessConfig.mockReset()
modelsDevMocks.updateExternalModelsAccessConfig.mockImplementation(
async (proxyNodeId: string | null) => ({
proxy_node_id: proxyNodeId,
cache_cleared: true,
}),
)
toastMocks.success.mockReset()
toastMocks.error.mockReset()
})
afterEach(() => {
for (const mounted of mountedControls.splice(0)) {
mounted.app.unmount()
mounted.root.remove()
}
})
describe('ExternalModelsAccessControl', () => {
it('uses the provider-style globe button as the header control', async () => {
const { root } = mountControl()
await flushAsyncState()
const trigger = root.querySelector('[data-testid="external-models-access-trigger"]')
expect(trigger?.tagName).toBe('BUTTON')
expect(trigger?.classList.contains('h-8')).toBe(true)
expect(trigger?.classList.contains('w-8')).toBe(true)
expect(root.querySelector('[data-testid="external-models-access-globe"]')).not.toBeNull()
expect(trigger?.getAttribute('aria-label')).toBe('外部模型目录代理节点')
})
it('loads the saved node into the shared proxy selector', async () => {
modelsDevMocks.getExternalModelsAccessConfig.mockResolvedValue({
proxy_node_id: 'proxy-1',
})
const { root } = mountControl()
await flushAsyncState()
const select = root.querySelector('[data-testid="external-models-access-select"]')
expect(select?.getAttribute('data-model-value')).toBe('proxy-1')
expect(select?.getAttribute('data-disabled')).toBe('false')
expect(root.querySelector('[data-testid="external-models-access-trigger"]')
?.classList.contains('text-blue-500')).toBe(true)
expect(root.querySelector('[data-testid="external-models-access-clear"]')).not.toBeNull()
})
it('immediately saves a selected proxy node', async () => {
const { root } = mountControl()
await flushAsyncState()
;(root.querySelector('[data-testid="select-proxy-node"]') as HTMLButtonElement).click()
await flushAsyncState()
expect(modelsDevMocks.updateExternalModelsAccessConfig).toHaveBeenCalledWith('proxy-1')
expect(toastMocks.success).toHaveBeenCalledWith('外部模型目录代理节点已保存')
expect(root.querySelector('[data-testid="external-models-access-select"]')
?.getAttribute('data-model-value')).toBe('proxy-1')
})
it('clears the selected proxy back to direct access', async () => {
modelsDevMocks.getExternalModelsAccessConfig.mockResolvedValue({
proxy_node_id: 'proxy-1',
})
const { root } = mountControl()
await flushAsyncState()
;(root.querySelector('[data-testid="external-models-access-clear"]') as HTMLButtonElement).click()
await flushAsyncState()
expect(modelsDevMocks.updateExternalModelsAccessConfig).toHaveBeenCalledWith(null)
expect(root.querySelector('[data-testid="external-models-access-select"]')
?.getAttribute('data-model-value')).toBe('')
expect(root.querySelector('[data-testid="external-models-access-clear"]')).toBeNull()
})
it('keeps the globe trigger and selector disabled when config loading fails', async () => {
modelsDevMocks.getExternalModelsAccessConfig.mockRejectedValue(new Error('load failed'))
const { root } = mountControl()
await flushAsyncState()
expect(root.querySelector('[data-testid="external-models-access-select"]')
?.getAttribute('data-disabled')).toBe('true')
expect((root.querySelector('[data-testid="external-models-access-trigger"]') as HTMLButtonElement)
.disabled).toBe(true)
expect(toastMocks.error).toHaveBeenCalledOnce()
})
})
@@ -1,4 +1,5 @@
export { default as GlobalModelFormDialog } from './GlobalModelFormDialog.vue'
export { default as ExternalModelsAccessControl } from './ExternalModelsAccessControl.vue'
export { default as ModelDetailDrawer } from './ModelDetailDrawer.vue'
export { default as TieredPricingEditor } from './TieredPricingEditor.vue'
export { default as ModelMappingsTab } from './ModelMappingsTab.vue'
@@ -2,10 +2,13 @@
<div class="space-y-1.5">
<Select
:model-value="modelValue"
:disabled="proxyNodesStore.loading || nodeOptions.length === 0"
:disabled="disabled || proxyNodesStore.loading || nodeOptions.length === 0"
@update:model-value="(v: string) => $emit('update:modelValue', v)"
>
<SelectTrigger :class="triggerClass">
<SelectTrigger
:class="triggerClass"
:aria-label="triggerAriaLabel"
>
<SelectValue
:placeholder="proxyNodesStore.loading
? legacyT('加载节点列表中...')
@@ -40,10 +43,16 @@ import { useProxyNodesStore } from '@/stores/proxy-nodes'
import { useI18n } from '@/i18n'
import { formatRegion } from '@/utils/region'
const props = defineProps<{
const props = withDefaults(defineProps<{
modelValue: string
triggerClass?: string
}>()
triggerAriaLabel?: string
disabled?: boolean
}>(), {
triggerClass: '',
triggerAriaLabel: undefined,
disabled: false,
})
defineEmits<{
'update:modelValue': [value: string]
@@ -1,4 +1,4 @@
import { describe, expect, it, vi } from 'vitest'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createApp, defineComponent, h } from 'vue'
import ProxyNodeSelect from '@/features/providers/components/ProxyNodeSelect.vue'
@@ -19,34 +19,81 @@ vi.mock('@/components/ui', async () => {
const { defineComponent, h } = await import('vue')
const passthrough = (name: string) => defineComponent({
name,
setup(_, { slots }) {
return () => h('div', slots.default?.())
inheritAttrs: false,
setup(_, { attrs, slots }) {
return () => h('div', attrs, slots.default?.())
},
})
return {
Select: passthrough('SelectStub'),
Select: defineComponent({
name: 'SelectStub',
props: {
disabled: { type: Boolean, default: false },
},
setup(props, { slots }) {
return () => h('div', {
'data-testid': 'proxy-select',
'data-disabled': String(props.disabled),
}, slots.default?.())
},
}),
SelectTrigger: passthrough('SelectTriggerStub'),
SelectValue: passthrough('SelectValueStub'),
SelectContent: passthrough('SelectContentStub'),
SelectItem: passthrough('SelectItemStub'),
SelectItem: defineComponent({
name: 'SelectItemStub',
props: {
value: { type: String, required: true },
},
setup(props, { slots }) {
return () => h('div', { 'data-select-value': props.value }, slots.default?.())
},
}),
}
})
function mountSelect(props: Record<string, unknown> = {}) {
const root = document.createElement('div')
const app = createApp(defineComponent({
setup() {
return () => h(ProxyNodeSelect, { modelValue: '', ...props })
},
}))
app.use(createI18n())
app.mount(root)
return { app, root }
}
beforeEach(() => {
proxyNodesStore.ensureLoaded.mockClear()
})
describe('ProxyNodeSelect', () => {
it('loads proxy nodes when mounted', () => {
const root = document.createElement('div')
const app = createApp(defineComponent({
setup() {
return () => h(ProxyNodeSelect, { modelValue: '' })
},
}))
app.use(createI18n())
app.mount(root)
const { app } = mountSelect()
expect(proxyNodesStore.ensureLoaded).toHaveBeenCalledTimes(1)
app.unmount()
})
})
it('stays disabled when there are no proxy nodes', () => {
const { app, root } = mountSelect()
expect(root.querySelector('[data-testid="proxy-select"]')
?.getAttribute('data-disabled')).toBe('true')
app.unmount()
})
it('applies an accessible label to the select trigger', () => {
const { app, root } = mountSelect({
triggerAriaLabel: 'External model catalog access',
})
expect(root.querySelector('[aria-label="External model catalog access"]')).not.toBeNull()
app.unmount()
})
})
+14
View File
@@ -286,6 +286,12 @@ export const messages = {
'nav.userManagement': '用户管理',
'nav.providers': '提供商',
'nav.modelManagement': '模型管理',
'models.externalCatalog.accessTitle': '外部模型目录代理节点',
'models.externalCatalog.proxyEnabled': '当前通过所选代理节点访问 models.dev',
'models.externalCatalog.direct': '未设置,直接访问 models.dev',
'models.externalCatalog.saved': '外部模型目录代理节点已保存',
'models.externalCatalog.saveFailed': '保存外部模型目录代理节点失败',
'models.externalCatalog.loadFailed': '加载外部模型目录代理节点失败',
'nav.routing': '调度策略',
'nav.pool': '号池管理',
'nav.standaloneKeys': '独立密钥',
@@ -599,6 +605,12 @@ export const messages = {
'nav.userManagement': 'Users',
'nav.providers': 'Providers',
'nav.modelManagement': 'Models',
'models.externalCatalog.accessTitle': 'External model catalog proxy',
'models.externalCatalog.proxyEnabled': 'Accessing models.dev through the selected proxy node',
'models.externalCatalog.direct': 'No proxy configured; accessing models.dev directly',
'models.externalCatalog.saved': 'External model catalog proxy saved',
'models.externalCatalog.saveFailed': 'Failed to save external model catalog proxy',
'models.externalCatalog.loadFailed': 'Failed to load external model catalog proxy',
'nav.routing': 'Routing',
'nav.pool': 'Pool',
'nav.standaloneKeys': 'Standalone keys',
@@ -752,6 +764,8 @@ const legacyExactEnglishMessages: Record<string, string> = {
'代理节点已更新': 'Proxy node updated',
'代理节点已删除': 'Proxy node deleted',
'代理节点已删除,系统默认代理已自动清除': 'Proxy node deleted. The system default proxy was cleared automatically.',
'代理节点已删除,外部模型目录代理已自动清除': 'Proxy node deleted. The external model catalog proxy was cleared automatically.',
'代理节点已删除,系统默认代理和外部模型目录代理已自动清除': 'Proxy node deleted. The system default proxy and external model catalog proxy were cleared automatically.',
'代理节点安装命令已生成': 'Proxy node install command generated',
'安装命令已复制到剪贴板': 'Install command copied to clipboard',
'生成代理节点安装命令失败': 'Failed to generate proxy node install command',
@@ -36,6 +36,7 @@
>
<ListChecks class="w-3.5 h-3.5" />
</Button>
<ExternalModelsAccessControl />
<Button
variant="ghost"
size="icon"
@@ -691,6 +692,7 @@ import {
} from 'lucide-vue-next'
import ModelDetailDrawer from '@/features/models/components/ModelDetailDrawer.vue'
import GlobalModelFormDialog from '@/features/models/components/GlobalModelFormDialog.vue'
import ExternalModelsAccessControl from '@/features/models/components/ExternalModelsAccessControl.vue'
import ProviderModelFormDialog from '@/features/providers/components/ProviderModelFormDialog.vue'
import type { Model } from '@/api/endpoints'
import { useToast } from '@/composables/useToast'
+9 -5
View File
@@ -103,6 +103,7 @@ import { useToast } from '@/composables/useToast'
import { useConfirm } from '@/composables/useConfirm'
import { useClipboard } from '@/composables/useClipboard'
import { useI18n } from '@/i18n'
import { clearModelsDevCache } from '@/api/models-dev'
import {
proxyNodesApi,
type ProxyNode,
@@ -117,6 +118,7 @@ import {
import { Card } from '@/components/ui'
import { parseApiError } from '@/utils/errorParser'
import { parseBatchProxyNodeInput } from './proxy-node-batch'
import { proxyNodeDeleteSuccessMessage } from './proxy-node-delete-feedback'
import ProxyNodeBatchUpgradeDialog from './components/ProxyNodeBatchUpgradeDialog.vue'
import ProxyNodeEventsDialog from './components/ProxyNodeEventsDialog.vue'
import ProxyNodeFormDialog from './components/ProxyNodeFormDialog.vue'
@@ -597,12 +599,14 @@ async function handleDelete(node: ProxyNode) {
try {
const result = await proxyNodesApi.deleteProxyNode(node.id)
await store.fetchNodes()
if (result.cleared_system_proxy) {
success(legacyT('代理节点已删除,系统默认代理已自动清除'))
} else {
success(legacyT('代理节点已删除'))
if (result.cleared_external_models_proxy) {
clearModelsDevCache()
}
await store.fetchNodes()
success(legacyT(proxyNodeDeleteSuccessMessage(
result.cleared_system_proxy,
result.cleared_external_models_proxy,
)))
} catch (err: unknown) {
toastError(parseApiError(err, legacyT('删除失败')))
}
@@ -0,0 +1,36 @@
import { describe, expect, it } from 'vitest'
import { proxyNodeDeleteSuccessMessage } from '../proxy-node-delete-feedback'
describe('proxyNodeDeleteSuccessMessage', () => {
it.each([
{
clearedSystemProxy: true,
clearedExternalModelsProxy: true,
expected: '代理节点已删除,系统默认代理和外部模型目录代理已自动清除',
},
{
clearedSystemProxy: true,
clearedExternalModelsProxy: false,
expected: '代理节点已删除,系统默认代理已自动清除',
},
{
clearedSystemProxy: false,
clearedExternalModelsProxy: true,
expected: '代理节点已删除,外部模型目录代理已自动清除',
},
{
clearedSystemProxy: false,
clearedExternalModelsProxy: false,
expected: '代理节点已删除',
},
])(
'returns the coordinated message for system=$clearedSystemProxy and external=$clearedExternalModelsProxy',
({ clearedSystemProxy, clearedExternalModelsProxy, expected }) => {
expect(proxyNodeDeleteSuccessMessage(
clearedSystemProxy,
clearedExternalModelsProxy,
)).toBe(expected)
},
)
})
@@ -0,0 +1,15 @@
export function proxyNodeDeleteSuccessMessage(
clearedSystemProxy: boolean,
clearedExternalModelsProxy: boolean,
): string {
if (clearedSystemProxy && clearedExternalModelsProxy) {
return '代理节点已删除,系统默认代理和外部模型目录代理已自动清除'
}
if (clearedSystemProxy) {
return '代理节点已删除,系统默认代理已自动清除'
}
if (clearedExternalModelsProxy) {
return '代理节点已删除,外部模型目录代理已自动清除'
}
return '代理节点已删除'
}