From 9fea71a70cd3c658bb80571cb82f28e8e80fd906 Mon Sep 17 00:00:00 2001 From: fawney19 Date: Mon, 12 Jan 2026 23:28:37 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20=E6=96=B0=E5=A2=9E=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E8=AF=B7=E6=B1=82=E9=93=BE=E8=B7=AF=E9=A2=84=E8=A7=88?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加后端 API 获取 GlobalModel 的请求链路信息 (/api/admin/models/global/{id}/routing) - 新增 RoutingTab 组件展示模型的请求链路树状结构 - 支持全局 Key 优先和提供商优先两种模式的可视化 - 重构批量模型管理对话框为单列勾选模式 - 修复 errorParser.ts 字符串拼接格式问题 - 若干 Vue 模板格式优化 --- frontend/src/api/endpoints/global-models.ts | 22 + frontend/src/api/endpoints/types.ts | 80 ++ frontend/src/api/global-models.ts | 1 + .../src/components/ui/collapsible-trigger.vue | 5 +- .../models/components/ModelDetailDrawer.vue | 330 +----- .../features/models/components/RoutingTab.vue | 1014 +++++++++++++++++ .../components/BatchAssignModelsDialog.vue | 20 +- .../components/KeyAllowedModelsDialog.vue | 5 +- .../components/KeyAllowedModelsEditDialog.vue | 51 +- .../components/ProviderDetailDrawer.vue | 10 +- .../components/ProviderFormDialog.vue | 12 +- frontend/src/utils/errorParser.ts | 6 +- frontend/src/views/admin/ModelManagement.vue | 605 ++++------ src/api/admin/models/__init__.py | 2 + src/api/admin/models/routing.py | 388 +++++++ 15 files changed, 1897 insertions(+), 654 deletions(-) create mode 100644 frontend/src/features/models/components/RoutingTab.vue create mode 100644 src/api/admin/models/routing.py diff --git a/frontend/src/api/endpoints/global-models.ts b/frontend/src/api/endpoints/global-models.ts index d437ff3..ba0d4bf 100644 --- a/frontend/src/api/endpoints/global-models.ts +++ b/frontend/src/api/endpoints/global-models.ts @@ -6,6 +6,16 @@ import type { GlobalModelWithStats, GlobalModelListResponse, ModelCatalogProviderDetail, + ModelRoutingPreviewResponse, +} from './types' + +// 重新导出路由相关类型供外部使用 +export type { + RoutingKeyInfo, + RoutingEndpointInfo, + RoutingModelMapping, + RoutingProviderInfo, + ModelRoutingPreviewResponse, } from './types' /** @@ -97,3 +107,15 @@ export async function getGlobalModelProviders(globalModelId: string): Promise<{ ) return response.data } + +/** + * 获取 GlobalModel 的请求链路预览 + */ +export async function getGlobalModelRoutingPreview( + globalModelId: string +): Promise { + const response = await client.get( + `/api/admin/models/global/${globalModelId}/routing` + ) + return response.data +} diff --git a/frontend/src/api/endpoints/types.ts b/frontend/src/api/endpoints/types.ts index 0200809..c318766 100644 --- a/frontend/src/api/endpoints/types.ts +++ b/frontend/src/api/endpoints/types.ts @@ -622,3 +622,83 @@ export interface ImportFromUpstreamResponse { success: ImportFromUpstreamSuccessItem[] errors: ImportFromUpstreamErrorItem[] } + +// ========== 路由预览相关类型 ========== + +/** + * Key 路由信息 + */ +export interface RoutingKeyInfo { + id: string + name: string + masked_key: string + internal_priority: number + global_priority?: number | null + rpm_limit?: number | null + is_adaptive: boolean + effective_rpm?: number | null + cache_ttl_minutes: number + health_score: number + is_active: boolean + api_formats: string[] + circuit_breaker_open: boolean + circuit_breaker_formats: string[] +} + +/** + * Endpoint 路由信息 + */ +export interface RoutingEndpointInfo { + id: string + api_format: string + base_url: string + custom_path?: string | null + is_active: boolean + keys: RoutingKeyInfo[] + total_keys: number + active_keys: number +} + +/** + * 模型名称映射信息 + */ +export interface RoutingModelMapping { + name: string + priority: number + api_formats?: string[] | null +} + +/** + * Provider 路由信息 + */ +export interface RoutingProviderInfo { + id: string + name: string + model_id: string + provider_priority: number + billing_type?: string | null + monthly_quota_usd?: number | null + monthly_used_usd?: number | null + is_active: boolean + provider_model_name: string + model_mappings: RoutingModelMapping[] + model_is_active: boolean + endpoints: RoutingEndpointInfo[] + total_endpoints: number + active_endpoints: number +} + +/** + * 模型请求链路预览响应 + */ +export interface ModelRoutingPreviewResponse { + global_model_id: string + global_model_name: string + display_name: string + is_active: boolean + providers: RoutingProviderInfo[] + total_providers: number + active_providers: number + scheduling_mode: string + priority_mode: string +} diff --git a/frontend/src/api/global-models.ts b/frontend/src/api/global-models.ts index 42884a9..1c92a3c 100644 --- a/frontend/src/api/global-models.ts +++ b/frontend/src/api/global-models.ts @@ -21,4 +21,5 @@ export { deleteGlobalModel, batchAssignToProviders, getGlobalModelProviders, + getGlobalModelRoutingPreview, } from './endpoints/global-models' diff --git a/frontend/src/components/ui/collapsible-trigger.vue b/frontend/src/components/ui/collapsible-trigger.vue index b12d852..27fca42 100644 --- a/frontend/src/components/ui/collapsible-trigger.vue +++ b/frontend/src/components/ui/collapsible-trigger.vue @@ -5,7 +5,10 @@ const props = defineProps() diff --git a/frontend/src/features/models/components/ModelDetailDrawer.vue b/frontend/src/features/models/components/ModelDetailDrawer.vue index abd5f6a..666e555 100644 --- a/frontend/src/features/models/components/ModelDetailDrawer.vue +++ b/frontend/src/features/models/components/ModelDetailDrawer.vue @@ -95,14 +95,14 @@ type="button" class="flex-1 px-2 sm:px-4 py-2 text-xs sm:text-sm font-medium rounded-md transition-all duration-200" :class="[ - detailTab === 'providers' + detailTab === 'routing' ? 'bg-primary text-primary-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground hover:bg-background/50' ]" - @click="detailTab = 'providers'" + @click="detailTab = 'routing'" > - - 提供商 + + 链路 @@ -407,269 +407,17 @@ - -
- - -
-
-
-

- 关联提供商列表 -

-
-
- - -
-
-
- - -
- -
- - - -
- - -

- 暂无关联提供商 -

- -
-
+ +
+
@@ -688,12 +436,8 @@ import { Zap, Image, Building2, - Plus, Edit, - Trash2, Power, - Loader2, - RefreshCw, Copy, Layers, BarChart3 @@ -711,14 +455,15 @@ import TableBody from '@/components/ui/table-body.vue' import TableRow from '@/components/ui/table-row.vue' import TableHead from '@/components/ui/table-head.vue' import TableCell from '@/components/ui/table-cell.vue' +import RoutingTab from './RoutingTab.vue' // 使用外部类型定义 import type { GlobalModelResponse } from '@/api/global-models' import type { TieredPricingConfig, PricingTier } from '@/api/endpoints/types' import type { CapabilityDefinition } from '@/api/endpoints' +import type { RoutingProviderInfo } from '@/api/global-models' const props = withDefaults(defineProps(), { - loadingProviders: false, hasBlockingDialogOpen: false, }) const emit = defineEmits<{ @@ -729,7 +474,6 @@ const emit = defineEmits<{ 'editProvider': [provider: any] 'deleteProvider': [provider: any] 'toggleProviderStatus': [provider: any] - 'refreshProviders': [] }>() const { success: showSuccess, error: showError } = useToast() const { copyToClipboard } = useClipboard() @@ -737,12 +481,48 @@ const { copyToClipboard } = useClipboard() interface Props { model: GlobalModelResponse | null open: boolean - providers: any[] - loadingProviders?: boolean hasBlockingDialogOpen?: boolean capabilities?: CapabilityDefinition[] } +// RoutingTab 引用 +const routingTabRef = ref | null>(null) + +// 将 RoutingProviderInfo 转换为父组件期望的格式 +function convertRoutingProviderToLegacyFormat(provider: RoutingProviderInfo) { + return { + id: provider.id, + model_id: provider.model_id, + name: provider.name, + is_active: provider.model_is_active + } +} + +// 处理从 RoutingTab 来的编辑事件 +function handleEditProviderFromRouting(provider: RoutingProviderInfo) { + emit('editProvider', convertRoutingProviderToLegacyFormat(provider)) +} + +// 处理从 RoutingTab 来的状态切换事件 +function handleToggleProviderFromRouting(provider: RoutingProviderInfo) { + emit('toggleProviderStatus', convertRoutingProviderToLegacyFormat(provider)) +} + +// 处理从 RoutingTab 来的删除事件 +function handleDeleteProviderFromRouting(provider: RoutingProviderInfo) { + emit('deleteProvider', convertRoutingProviderToLegacyFormat(provider)) +} + +// 刷新路由数据 +function refreshRoutingData() { + routingTabRef.value?.loadRoutingData?.() +} + +// 暴露刷新方法给父组件 +defineExpose({ + refreshRoutingData +}) + // 根据能力名称获取显示名称 function getCapabilityDisplayName(capName: string): string { const cap = props.capabilities?.find(c => c.name === capName) diff --git a/frontend/src/features/models/components/RoutingTab.vue b/frontend/src/features/models/components/RoutingTab.vue new file mode 100644 index 0000000..ce948f4 --- /dev/null +++ b/frontend/src/features/models/components/RoutingTab.vue @@ -0,0 +1,1014 @@ + + + + + diff --git a/frontend/src/features/providers/components/BatchAssignModelsDialog.vue b/frontend/src/features/providers/components/BatchAssignModelsDialog.vue index 3fd230d..38f3f34 100644 --- a/frontend/src/features/providers/components/BatchAssignModelsDialog.vue +++ b/frontend/src/features/providers/components/BatchAssignModelsDialog.vue @@ -107,8 +107,12 @@ />
-

{{ model.display_name }}

-

{{ model.name }}

+

+ {{ model.display_name }} +

+

+ {{ model.name }} +

@@ -159,8 +163,12 @@ />
-

{{ model.id }}

-

{{ model.owned_by || model.id }}

+

+ {{ model.id }} +

+

+ {{ model.owned_by || model.id }} +

@@ -172,7 +180,9 @@ class="flex flex-col items-center justify-center py-12 text-muted-foreground" > -

{{ searchQuery ? '无匹配结果' : '暂无可用模型' }}

+

+ {{ searchQuery ? '无匹配结果' : '暂无可用模型' }} +

-

+
diff --git a/frontend/src/features/providers/components/KeyAllowedModelsEditDialog.vue b/frontend/src/features/providers/components/KeyAllowedModelsEditDialog.vue index 7db2cea..a25e8ea 100644 --- a/frontend/src/features/providers/components/KeyAllowedModelsEditDialog.vue +++ b/frontend/src/features/providers/components/KeyAllowedModelsEditDialog.vue @@ -131,7 +131,10 @@ class="w-4 h-4 border rounded flex items-center justify-center shrink-0" :class="selectedModels.includes(model) ? 'bg-primary border-primary' : ''" > - +
{{ model }}
@@ -174,18 +177,28 @@ class="w-4 h-4 border rounded flex items-center justify-center shrink-0" :class="selectedModels.includes(model.name) ? 'bg-primary border-primary' : ''" > - +
-

{{ model.display_name }}

-

{{ model.name }}

+

+ {{ model.display_name }} +

+

+ {{ model.name }} +

-
+
- +
{{ model.id }}
@@ -235,8 +251,15 @@ class="flex flex-col items-center justify-center py-12 text-muted-foreground" > -

{{ searchQuery ? '无匹配结果' : '暂无可选模型' }}

-

点击闪电按钮从上游获取模型

+

+ {{ searchQuery ? '无匹配结果' : '暂无可选模型' }} +

+

+ 点击闪电按钮从上游获取模型 +

@@ -250,10 +273,18 @@ {{ hasChanges ? '有未保存的更改' : '' }}

- - +
diff --git a/frontend/src/features/providers/components/ProviderDetailDrawer.vue b/frontend/src/features/providers/components/ProviderDetailDrawer.vue index 20f66ff..0883669 100644 --- a/frontend/src/features/providers/components/ProviderDetailDrawer.vue +++ b/frontend/src/features/providers/components/ProviderDetailDrawer.vue @@ -83,7 +83,10 @@
-