From 21c9eaa44d2a6d8200daca0f6b6745c3549f8ccf Mon Sep 17 00:00:00 2001 From: bruce Date: Wed, 27 May 2026 22:14:34 +0800 Subject: [PATCH] =?UTF-8?q?feat(frontend):=20=E6=96=B0=E5=A2=9E=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E5=B9=B3=E5=8F=B0=E7=AE=A1=E7=90=86=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E5=B9=B6=E6=8E=A5=E5=85=A5RAG=E6=A8=A1=E5=9E=8B=E9=85=8D?= =?UTF-8?q?=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/api/__tests__/modelProvider.spec.ts | 62 ++++ frontend/src/api/modelEnums.ts | 52 ++++ frontend/src/api/modelProvider.ts | 141 +++++++++ frontend/src/layouts/AdminLayout.vue | 34 ++- frontend/src/pages/rag/RagStoresPage.vue | 162 +++++++++- .../pages/rag/__tests__/RagStoresPage.spec.ts | 73 +++++ .../src/pages/system/ModelCallLogsPage.vue | 160 ++++++++++ .../src/pages/system/ModelConfigsPage.vue | 243 +++++++++++++++ .../src/pages/system/ModelProvidersPage.vue | 278 ++++++++++++++++++ .../src/pages/system/ModelRouteRulesPage.vue | 245 +++++++++++++++ frontend/src/router/__tests__/router.spec.ts | 4 + frontend/src/router/index.ts | 52 ++++ 12 files changed, 1489 insertions(+), 17 deletions(-) create mode 100644 frontend/src/api/__tests__/modelProvider.spec.ts create mode 100644 frontend/src/api/modelEnums.ts create mode 100644 frontend/src/api/modelProvider.ts create mode 100644 frontend/src/pages/system/ModelCallLogsPage.vue create mode 100644 frontend/src/pages/system/ModelConfigsPage.vue create mode 100644 frontend/src/pages/system/ModelProvidersPage.vue create mode 100644 frontend/src/pages/system/ModelRouteRulesPage.vue diff --git a/frontend/src/api/__tests__/modelProvider.spec.ts b/frontend/src/api/__tests__/modelProvider.spec.ts new file mode 100644 index 0000000..b430d90 --- /dev/null +++ b/frontend/src/api/__tests__/modelProvider.spec.ts @@ -0,0 +1,62 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { + checkModelProviderHealth, + deleteModelConfig, + deleteModelProvider, + deleteModelRouteRule, + getRagStoreModelConfig, + queryModelCallLogs, + queryModelConfigs, + queryModelProviders, + queryModelRouteRules, + rebuildRagStoreIndex, + saveModelConfig, + saveModelProvider, + saveModelRouteRule, + saveRagStoreModelConfig, +} from '../modelProvider'; +import { get, post } from '../request'; + +vi.mock('../request', () => ({ + get: vi.fn(), + post: vi.fn(), +})); + +describe('model provider api', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('calls model platform endpoints with expected params', () => { + queryModelProviders(); + saveModelProvider({ providerCode: 'OPENAI_MAIN' } as never); + deleteModelProvider('1'); + checkModelProviderHealth('1'); + queryModelConfigs(); + saveModelConfig({ modelCode: 'EMB_1' } as never); + deleteModelConfig('2'); + queryModelRouteRules(); + saveModelRouteRule({ routeCode: 'RAG_GLOBAL' } as never); + deleteModelRouteRule('3'); + queryModelCallLogs({ taskType: 'RAG_EMBEDDING' }); + getRagStoreModelConfig('10'); + saveRagStoreModelConfig({ storeId: '10', embeddingModelId: '2' } as never); + rebuildRagStoreIndex('10'); + + expect(post).toHaveBeenCalledWith('/model/providers/query'); + expect(post).toHaveBeenCalledWith('/model/providers/save', { providerCode: 'OPENAI_MAIN' }); + expect(post).toHaveBeenCalledWith('/model/providers/delete', undefined, { params: { id: '1' } }); + expect(post).toHaveBeenCalledWith('/model/providers/checkHealth', undefined, { params: { id: '1' } }); + expect(post).toHaveBeenCalledWith('/model/configs/query'); + expect(post).toHaveBeenCalledWith('/model/configs/save', { modelCode: 'EMB_1' }); + expect(post).toHaveBeenCalledWith('/model/configs/delete', undefined, { params: { id: '2' } }); + expect(post).toHaveBeenCalledWith('/model/routes/query'); + expect(post).toHaveBeenCalledWith('/model/routes/save', { routeCode: 'RAG_GLOBAL' }); + expect(post).toHaveBeenCalledWith('/model/routes/delete', undefined, { params: { id: '3' } }); + expect(post).toHaveBeenCalledWith('/model/call-logs/query', { taskType: 'RAG_EMBEDDING' }); + expect(get).toHaveBeenCalledWith('/rag/store/modelConfig', { params: { storeId: '10' } }); + expect(post).toHaveBeenCalledWith('/rag/store/modelConfig/save', { storeId: '10', embeddingModelId: '2' }); + expect(post).toHaveBeenCalledWith('/rag/store/rebuildIndex', undefined, { params: { storeId: '10' } }); + }); +}); diff --git a/frontend/src/api/modelEnums.ts b/frontend/src/api/modelEnums.ts new file mode 100644 index 0000000..68d0166 --- /dev/null +++ b/frontend/src/api/modelEnums.ts @@ -0,0 +1,52 @@ +import { listForManagement, type SysEnum } from './sysEnums'; + +export interface EnumOption { + label: string; + value: string; +} + +const enumCache = new Map(); + +function buildCacheKey(catalog: string, type: string) { + return `${catalog}::${type}`; +} + +function mapOption(item: SysEnum): EnumOption { + const value = item.strvalue && item.strvalue.trim() + ? item.strvalue + : item.name; + return { + label: item.name, + value, + }; +} + +export async function loadEnumOptions(catalog: string, type: string, forceRefresh = false) { + const cacheKey = buildCacheKey(catalog, type); + if (!forceRefresh && enumCache.has(cacheKey)) { + return enumCache.get(cacheKey) ?? []; + } + const response = await listForManagement({ catalog, type, keyword: '' }); + const options = (response.data ?? []).map(mapOption); + enumCache.set(cacheKey, options); + return options; +} + +export async function loadModelProviderEnumOptions(forceRefresh = false) { + const catalog = 'model_provider'; + const types = [ + 'provider_type', + 'protocol_type', + 'auth_type', + 'model_type', + 'task_type', + 'route_strategy', + 'health_status', + 'call_status', + 'match_scope', + ]; + const entries = await Promise.all( + types.map(async (type) => [type, await loadEnumOptions(catalog, type, forceRefresh)] as const), + ); + return Object.fromEntries(entries) as Record; +} diff --git a/frontend/src/api/modelProvider.ts b/frontend/src/api/modelProvider.ts new file mode 100644 index 0000000..128f89a --- /dev/null +++ b/frontend/src/api/modelProvider.ts @@ -0,0 +1,141 @@ +import { get, post } from './request'; + +export interface ModelProvider { + id?: string; + providerCode: string; + providerName: string; + providerType: string; + protocolType: string; + baseUrl: string; + authType: string; + secretRef?: string; + hasApiKey?: boolean; + timeoutMs: number; + priority: number; + enabled: boolean; + healthStatus?: string; + remark?: string; +} + +export interface ModelConfig { + id?: string; + providerId: string; + modelCode: string; + modelName: string; + upstreamModel: string; + modelType: string; + embeddingDimension?: number; + localModel: boolean; + defaultModel: boolean; + optionsJson?: string; + enabled: boolean; + remark?: string; +} + +export interface ModelRouteRule { + id?: string; + routeCode: string; + routeName: string; + taskType: string; + matchScope: string; + scopeId?: string; + primaryModelId: string; + fallbackModelIdsJson?: string; + routeStrategy: string; + maxLatencyMs?: number; + enabled: boolean; + remark?: string; +} + +export interface ModelCallLog { + id?: string; + requestId: string; + providerId?: string; + modelId?: string; + taskType: string; + bizType?: string; + bizId?: string; + callType: string; + status: string; + durationMs?: number; + errorCode?: string; + errorMessage?: string; +} + +export interface ModelCallLogQueryRequest { + taskType?: string; + providerId?: string; + modelId?: string; + status?: string; + bizType?: string; +} + +export interface RagStoreModelConfig { + id?: string; + storeId: string; + embeddingModelId: string; + embeddingDimension: number; + chunkStrategy?: number; + chunkSize?: number; + chunkOverlap?: number; + delimiter?: string; + active?: boolean; + indexVersion?: number; + remark?: string; +} + +export function queryModelProviders() { + return post('/model/providers/query'); +} + +export function saveModelProvider(data: Partial & { id?: string }) { + return post('/model/providers/save', data); +} + +export function deleteModelProvider(id: string) { + return post('/model/providers/delete', undefined, { params: { id } }); +} + +export function checkModelProviderHealth(id: string) { + return post('/model/providers/checkHealth', undefined, { params: { id } }); +} + +export function queryModelConfigs() { + return post('/model/configs/query'); +} + +export function saveModelConfig(data: Partial & { id?: string }) { + return post('/model/configs/save', data); +} + +export function deleteModelConfig(id: string) { + return post('/model/configs/delete', undefined, { params: { id } }); +} + +export function queryModelRouteRules() { + return post('/model/routes/query'); +} + +export function saveModelRouteRule(data: Partial & { id?: string }) { + return post('/model/routes/save', data); +} + +export function deleteModelRouteRule(id: string) { + return post('/model/routes/delete', undefined, { params: { id } }); +} + +export function queryModelCallLogs(query?: ModelCallLogQueryRequest) { + return post('/model/call-logs/query', query); +} + +export function getRagStoreModelConfig(storeId: string) { + return get('/rag/store/modelConfig', { params: { storeId } }); +} + +export function saveRagStoreModelConfig(data: Partial & { storeId: string }) { + return post('/rag/store/modelConfig/save', data); +} + +export function rebuildRagStoreIndex(storeId: string) { + return post('/rag/store/rebuildIndex', undefined, { params: { storeId } }); +} diff --git a/frontend/src/layouts/AdminLayout.vue b/frontend/src/layouts/AdminLayout.vue index fa5ebed..be658f7 100644 --- a/frontend/src/layouts/AdminLayout.vue +++ b/frontend/src/layouts/AdminLayout.vue @@ -6,10 +6,18 @@ import { Grid, Histogram, List, + Setting, } from '@element-plus/icons-vue'; -const menuItems = [ +const systemMenuItems = [ { path: '/system/enums', label: '系统枚举', icon: Grid }, + { path: '/system/model/providers', label: '模型服务商', icon: Setting }, + { path: '/system/model/configs', label: '模型配置', icon: Setting }, + { path: '/system/model/routes', label: '路由规则', icon: Setting }, + { path: '/system/model/call-logs', label: '调用日志', icon: Setting }, +]; + +const ragMenuItems = [ { path: '/rag/stores', label: '知识库', icon: Collection }, { path: '/rag/workbench', label: 'RAG工作台', icon: Histogram }, { path: '/rag/documents', label: '知识文档', icon: Document }, @@ -28,12 +36,24 @@ const menuItems = [ - - - - - {{ item.label }} - + + + + + + + {{ item.label }} + + + + + + + + + {{ item.label }} + + diff --git a/frontend/src/pages/rag/RagStoresPage.vue b/frontend/src/pages/rag/RagStoresPage.vue index cebd45d..827ebc8 100644 --- a/frontend/src/pages/rag/RagStoresPage.vue +++ b/frontend/src/pages/rag/RagStoresPage.vue @@ -3,6 +3,14 @@ import { CirclePlus, Delete, Edit, FolderAdd, Refresh, Search, UploadFilled } fr import { ElMessage, ElMessageBox } from 'element-plus'; import { computed, onMounted, reactive, ref } from 'vue'; import { useRouter } from 'vue-router'; +import { loadModelProviderEnumOptions, type EnumOption } from '@/api/modelEnums'; +import { + getRagStoreModelConfig, + queryModelConfigs, + rebuildRagStoreIndex, + saveRagStoreModelConfig, + type ModelConfig, +} from '@/api/modelProvider'; import { deleteRagStore, @@ -28,6 +36,20 @@ const activeStoreId = ref(null); const activeStore = ref(null); const pageOverview = ref(null); const activeStoreDocumentOverview = ref(null); +const embeddingModels = ref([]); +const ragConfigLoading = ref(false); +const ragConfigSaving = ref(false); +const ragConfig = reactive({ + id: '', + embeddingModelId: '', + embeddingDimension: 1024, + chunkStrategy: null as number | null, + chunkSize: null as number | null, + chunkOverlap: null as number | null, + delimiter: '', + remark: '', +}); +const chunkStrategyOptions = ref([]); const queryForm = reactive({ storeName: '', @@ -91,6 +113,8 @@ async function loadStores(preferredStoreId?: string | null) { activeStoreId.value = null; activeStore.value = null; activeStoreDocumentOverview.value = null; + ragConfig.id = ''; + ragConfig.embeddingModelId = ''; return; } @@ -123,6 +147,7 @@ async function selectStore(storeId: string) { ]); activeStore.value = storeResponse.data ?? null; activeStoreDocumentOverview.value = documentOverviewResponse.data ?? null; + await loadRagModelConfig(storeId); } finally { detailLoading.value = false; } @@ -223,10 +248,6 @@ async function removeStore() { await loadStores(); } -function showFutureMessage(actionName: string) { - ElMessage.info(`${actionName} 会在下一批接口里补齐`); -} - function openBatchUploadDialog() { if (!activeStore.value?.id) { ElMessage.warning('请选择知识库'); @@ -252,13 +273,85 @@ async function refreshAfterUpload() { ]); } +async function loadRagModelConfig(storeId: string) { + ragConfigLoading.value = true; + try { + const response = await getRagStoreModelConfig(storeId); + const data = response.data; + ragConfig.id = data?.id ?? ''; + ragConfig.embeddingModelId = data?.embeddingModelId ?? ''; + ragConfig.embeddingDimension = data?.embeddingDimension ?? 1024; + ragConfig.chunkStrategy = data?.chunkStrategy ?? null; + ragConfig.chunkSize = data?.chunkSize ?? null; + ragConfig.chunkOverlap = data?.chunkOverlap ?? null; + ragConfig.delimiter = data?.delimiter ?? ''; + ragConfig.remark = data?.remark ?? ''; + } finally { + ragConfigLoading.value = false; + } +} + +async function saveStoreModelConfig() { + if (!activeStoreId.value) { + return; + } + if (!ragConfig.embeddingModelId) { + ElMessage.warning('请选择 Embedding 模型'); + return; + } + ragConfigSaving.value = true; + try { + await saveRagStoreModelConfig({ + id: ragConfig.id || undefined, + storeId: activeStoreId.value, + embeddingModelId: ragConfig.embeddingModelId, + embeddingDimension: ragConfig.embeddingDimension, + chunkStrategy: ragConfig.chunkStrategy ?? undefined, + chunkSize: ragConfig.chunkSize ?? undefined, + chunkOverlap: ragConfig.chunkOverlap ?? undefined, + delimiter: ragConfig.delimiter || undefined, + remark: ragConfig.remark || undefined, + }); + ElMessage.success('知识库模型配置已保存'); + await loadRagModelConfig(activeStoreId.value); + } finally { + ragConfigSaving.value = false; + } +} + +async function triggerRebuildIndex() { + if (!activeStoreId.value) { + return; + } + await rebuildRagStoreIndex(activeStoreId.value); + ElMessage.success('已触发重建索引'); +} + +function syncEmbeddingDimensionFromModel() { + const model = embeddingModels.value.find((item) => item.id === ragConfig.embeddingModelId); + if (!model) { + return; + } + if (model.embeddingDimension) { + ragConfig.embeddingDimension = model.embeddingDimension; + } +} + function getStatusTagType(status?: string | null) { return status === '启用' ? 'success' : 'info'; } onMounted(() => { - loadOverview(); - loadStores(); + Promise.all([ + loadOverview(), + loadStores(), + queryModelConfigs().then((response) => { + embeddingModels.value = (response.data ?? []).filter((item) => item.modelType === 'EMBEDDING'); + }), + loadModelProviderEnumOptions().then((enums) => { + chunkStrategyOptions.value = enums.chunk_strategy ?? []; + }), + ]); }); @@ -351,7 +444,7 @@ onMounted(() => { > 批量导入文件 - 重建索引 + 重建索引 删除 @@ -415,12 +508,56 @@ onMounted(() => { -
+

检索配置

- 下一批接口补充 + + 保存配置 + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
-
@@ -721,6 +858,11 @@ onMounted(() => { padding: 12px 0; } +.rag-config-form :deep(.el-select), +.rag-config-form :deep(.el-input-number) { + width: 100%; +} + @media (max-width: 1280px) { .overview-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); diff --git a/frontend/src/pages/rag/__tests__/RagStoresPage.spec.ts b/frontend/src/pages/rag/__tests__/RagStoresPage.spec.ts index cd9fed3..0cba4d5 100644 --- a/frontend/src/pages/rag/__tests__/RagStoresPage.spec.ts +++ b/frontend/src/pages/rag/__tests__/RagStoresPage.spec.ts @@ -10,6 +10,12 @@ import { queryRagStores, saveRagStore, } from '@/api/ragStores'; +import { + getRagStoreModelConfig, + queryModelConfigs, + rebuildRagStoreIndex, + saveRagStoreModelConfig, +} from '@/api/modelProvider'; const routerPush = vi.hoisted(() => vi.fn()); @@ -24,6 +30,52 @@ vi.mock('@/api/ragDocuments', () => ({ batchUploadRagDocuments: vi.fn(() => Promise.resolve({ resultcode: '0', message: null, data: [] })), })); +vi.mock('@/api/modelEnums', () => ({ + loadModelProviderEnumOptions: vi.fn(() => + Promise.resolve({ + chunk_strategy: [ + { label: '固定长度', value: '1' }, + ], + }), + ), +})); + +vi.mock('@/api/modelProvider', () => ({ + queryModelConfigs: vi.fn(() => + Promise.resolve({ + resultcode: '0', + message: null, + data: [ + { + id: '88', + providerId: '1', + modelCode: 'TEXT_EMBED_3_LARGE', + modelName: 'text-embedding-3-large', + modelType: 'EMBEDDING', + embeddingDimension: 1024, + localModel: false, + defaultModel: true, + enabled: true, + }, + ], + }), + ), + getRagStoreModelConfig: vi.fn(() => + Promise.resolve({ + resultcode: '0', + message: null, + data: { + id: '9', + storeId: '1', + embeddingModelId: '88', + embeddingDimension: 1024, + }, + }), + ), + saveRagStoreModelConfig: vi.fn(() => Promise.resolve({ resultcode: '0', message: null, data: true })), + rebuildRagStoreIndex: vi.fn(() => Promise.resolve({ resultcode: '0', message: null, data: true })), +})); + vi.mock('@/api/ragStores', () => ({ getRagStoreOverview: vi.fn(() => Promise.resolve({ @@ -142,6 +194,8 @@ describe('RagStoresPage', () => { expect(queryRagStores).toHaveBeenCalled(); expect(getRagStoreById).toHaveBeenCalledWith('1'); expect(getRagStoreDocumentOverview).toHaveBeenCalledWith('1'); + expect(queryModelConfigs).toHaveBeenCalled(); + expect(getRagStoreModelConfig).toHaveBeenCalledWith('1'); }); it('filters stores by name and updates detail when a store is selected', async () => { @@ -225,4 +279,23 @@ describe('RagStoresPage', () => { expect(wrapper.text()).toContain('拖拽文件到此处'); expect(wrapper.text()).toContain('产品制度库'); }); + + it('saves store model config and triggers rebuild index', async () => { + const wrapper = mount(RagStoresPage, { + global: { + plugins: [ElementPlus], + }, + }); + + await flushPromises(); + const saveButtons = wrapper.findAll('button').filter((button) => button.text().includes('保存配置')); + await saveButtons[0]?.trigger('click'); + await flushPromises(); + expect(saveRagStoreModelConfig).toHaveBeenCalled(); + + const rebuildButtons = wrapper.findAll('button').filter((button) => button.text().includes('重建索引')); + await rebuildButtons[0]?.trigger('click'); + await flushPromises(); + expect(rebuildRagStoreIndex).toHaveBeenCalledWith('1'); + }); }); diff --git a/frontend/src/pages/system/ModelCallLogsPage.vue b/frontend/src/pages/system/ModelCallLogsPage.vue new file mode 100644 index 0000000..fcfa45c --- /dev/null +++ b/frontend/src/pages/system/ModelCallLogsPage.vue @@ -0,0 +1,160 @@ + + + + + diff --git a/frontend/src/pages/system/ModelConfigsPage.vue b/frontend/src/pages/system/ModelConfigsPage.vue new file mode 100644 index 0000000..bfc001c --- /dev/null +++ b/frontend/src/pages/system/ModelConfigsPage.vue @@ -0,0 +1,243 @@ + + + + + diff --git a/frontend/src/pages/system/ModelProvidersPage.vue b/frontend/src/pages/system/ModelProvidersPage.vue new file mode 100644 index 0000000..f7b4408 --- /dev/null +++ b/frontend/src/pages/system/ModelProvidersPage.vue @@ -0,0 +1,278 @@ + + + + + diff --git a/frontend/src/pages/system/ModelRouteRulesPage.vue b/frontend/src/pages/system/ModelRouteRulesPage.vue new file mode 100644 index 0000000..cae0257 --- /dev/null +++ b/frontend/src/pages/system/ModelRouteRulesPage.vue @@ -0,0 +1,245 @@ + + + + + diff --git a/frontend/src/router/__tests__/router.spec.ts b/frontend/src/router/__tests__/router.spec.ts index f49dfce..a2abaa7 100644 --- a/frontend/src/router/__tests__/router.spec.ts +++ b/frontend/src/router/__tests__/router.spec.ts @@ -12,6 +12,10 @@ describe('router', () => { expect(paths).toContain('/rag/documents'); expect(paths).toContain('/rag/tasks/chunk'); expect(paths).toContain('/system/enums'); + expect(paths).toContain('/system/model/providers'); + expect(paths).toContain('/system/model/configs'); + expect(paths).toContain('/system/model/routes'); + expect(paths).toContain('/system/model/call-logs'); expect(paths).toContain('/:pathMatch(.*)*'); }); }); diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index e15fb90..ae75795 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -7,6 +7,10 @@ import RagStoresPage from '@/pages/rag/RagStoresPage.vue'; import RagChunkTasksPage from '@/pages/rag/tasks/RagChunkTasksPage.vue'; import RagWorkbenchPage from '@/pages/rag/workbench/RagWorkbenchPage.vue'; import SystemEnumsPage from '@/pages/system/SystemEnumsPage.vue'; +import ModelProvidersPage from '@/pages/system/ModelProvidersPage.vue'; +import ModelConfigsPage from '@/pages/system/ModelConfigsPage.vue'; +import ModelRouteRulesPage from '@/pages/system/ModelRouteRulesPage.vue'; +import ModelCallLogsPage from '@/pages/system/ModelCallLogsPage.vue'; import AdminLayout from '@/layouts/AdminLayout.vue'; export const routes: RouteRecordRaw[] = [ @@ -20,6 +24,30 @@ export const routes: RouteRecordRaw[] = [ component: SystemEnumsPage, meta: { title: '系统枚举' }, }, + { + path: '/system/model/providers', + name: 'system-model-providers', + component: ModelProvidersPage, + meta: { title: '模型服务商' }, + }, + { + path: '/system/model/configs', + name: 'system-model-configs', + component: ModelConfigsPage, + meta: { title: '模型配置' }, + }, + { + path: '/system/model/routes', + name: 'system-model-routes', + component: ModelRouteRulesPage, + meta: { title: '路由规则' }, + }, + { + path: '/system/model/call-logs', + name: 'system-model-call-logs', + component: ModelCallLogsPage, + meta: { title: '调用日志' }, + }, { path: '/rag/stores', name: 'rag-stores', @@ -67,6 +95,30 @@ const routerRoutes: RouteRecordRaw[] = [ component: SystemEnumsPage, meta: { title: '系统枚举' }, }, + { + path: 'system/model/providers', + name: 'system-model-providers', + component: ModelProvidersPage, + meta: { title: '模型服务商' }, + }, + { + path: 'system/model/configs', + name: 'system-model-configs', + component: ModelConfigsPage, + meta: { title: '模型配置' }, + }, + { + path: 'system/model/routes', + name: 'system-model-routes', + component: ModelRouteRulesPage, + meta: { title: '路由规则' }, + }, + { + path: 'system/model/call-logs', + name: 'system-model-call-logs', + component: ModelCallLogsPage, + meta: { title: '调用日志' }, + }, { path: 'rag/stores', name: 'rag-stores',