mirror of
https://github.com/fawney19/Aether.git
synced 2026-01-03 00:02:28 +08:00
refactor(frontend): refactor model management with aliases, remove mappings UI
This commit is contained in:
@@ -1,121 +0,0 @@
|
|||||||
/**
|
|
||||||
* 模型别名管理 API
|
|
||||||
*/
|
|
||||||
|
|
||||||
import client from '../client'
|
|
||||||
import type { ModelMapping, ModelMappingCreate, ModelMappingUpdate } from './types'
|
|
||||||
|
|
||||||
export interface ModelAlias {
|
|
||||||
id: string
|
|
||||||
alias: string
|
|
||||||
global_model_id: string
|
|
||||||
global_model_name: string | null
|
|
||||||
global_model_display_name: string | null
|
|
||||||
provider_id: string | null
|
|
||||||
provider_name: string | null
|
|
||||||
scope: 'global' | 'provider'
|
|
||||||
mapping_type: 'alias' | 'mapping'
|
|
||||||
is_active: boolean
|
|
||||||
created_at: string
|
|
||||||
updated_at: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CreateModelAliasRequest {
|
|
||||||
alias: string
|
|
||||||
global_model_id: string
|
|
||||||
provider_id?: string | null
|
|
||||||
mapping_type?: 'alias' | 'mapping'
|
|
||||||
is_active?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UpdateModelAliasRequest {
|
|
||||||
alias?: string
|
|
||||||
global_model_id?: string
|
|
||||||
provider_id?: string | null
|
|
||||||
mapping_type?: 'alias' | 'mapping'
|
|
||||||
is_active?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
function transformMapping(mapping: ModelMapping): ModelAlias {
|
|
||||||
return {
|
|
||||||
id: mapping.id,
|
|
||||||
alias: mapping.source_model,
|
|
||||||
global_model_id: mapping.target_global_model_id,
|
|
||||||
global_model_name: mapping.target_global_model_name,
|
|
||||||
global_model_display_name: mapping.target_global_model_display_name,
|
|
||||||
provider_id: mapping.provider_id ?? null,
|
|
||||||
provider_name: mapping.provider_name ?? null,
|
|
||||||
scope: mapping.scope,
|
|
||||||
mapping_type: mapping.mapping_type || 'alias',
|
|
||||||
is_active: mapping.is_active,
|
|
||||||
created_at: mapping.created_at,
|
|
||||||
updated_at: mapping.updated_at
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取别名列表
|
|
||||||
*/
|
|
||||||
export async function getAliases(params?: {
|
|
||||||
provider_id?: string
|
|
||||||
global_model_id?: string
|
|
||||||
is_active?: boolean
|
|
||||||
skip?: number
|
|
||||||
limit?: number
|
|
||||||
}): Promise<ModelAlias[]> {
|
|
||||||
const response = await client.get('/api/admin/models/mappings', {
|
|
||||||
params: {
|
|
||||||
provider_id: params?.provider_id,
|
|
||||||
target_global_model_id: params?.global_model_id,
|
|
||||||
is_active: params?.is_active,
|
|
||||||
skip: params?.skip,
|
|
||||||
limit: params?.limit
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return (response.data as ModelMapping[]).map(transformMapping)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取单个别名
|
|
||||||
*/
|
|
||||||
export async function getAlias(id: string): Promise<ModelAlias> {
|
|
||||||
const response = await client.get(`/api/admin/models/mappings/${id}`)
|
|
||||||
return transformMapping(response.data)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建别名
|
|
||||||
*/
|
|
||||||
export async function createAlias(data: CreateModelAliasRequest): Promise<ModelAlias> {
|
|
||||||
const payload: ModelMappingCreate = {
|
|
||||||
source_model: data.alias,
|
|
||||||
target_global_model_id: data.global_model_id,
|
|
||||||
provider_id: data.provider_id ?? null,
|
|
||||||
mapping_type: data.mapping_type ?? 'alias',
|
|
||||||
is_active: data.is_active ?? true
|
|
||||||
}
|
|
||||||
const response = await client.post('/api/admin/models/mappings', payload)
|
|
||||||
return transformMapping(response.data)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新别名
|
|
||||||
*/
|
|
||||||
export async function updateAlias(id: string, data: UpdateModelAliasRequest): Promise<ModelAlias> {
|
|
||||||
const payload: ModelMappingUpdate = {
|
|
||||||
source_model: data.alias,
|
|
||||||
target_global_model_id: data.global_model_id,
|
|
||||||
provider_id: data.provider_id ?? null,
|
|
||||||
mapping_type: data.mapping_type,
|
|
||||||
is_active: data.is_active
|
|
||||||
}
|
|
||||||
const response = await client.patch(`/api/admin/models/mappings/${id}`, payload)
|
|
||||||
return transformMapping(response.data)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 删除别名
|
|
||||||
*/
|
|
||||||
export async function deleteAlias(id: string): Promise<void> {
|
|
||||||
await client.delete(`/api/admin/models/mappings/${id}`)
|
|
||||||
}
|
|
||||||
@@ -4,6 +4,5 @@ export * from './endpoints'
|
|||||||
export * from './keys'
|
export * from './keys'
|
||||||
export * from './health'
|
export * from './health'
|
||||||
export * from './models'
|
export * from './models'
|
||||||
export * from './aliases'
|
|
||||||
export * from './adaptive'
|
export * from './adaptive'
|
||||||
export * from './global-models'
|
export * from './global-models'
|
||||||
|
|||||||
@@ -5,9 +5,6 @@ import type {
|
|||||||
ModelUpdate,
|
ModelUpdate,
|
||||||
ModelCatalogResponse,
|
ModelCatalogResponse,
|
||||||
ProviderAvailableSourceModelsResponse,
|
ProviderAvailableSourceModelsResponse,
|
||||||
UpdateModelMappingRequest,
|
|
||||||
UpdateModelMappingResponse,
|
|
||||||
DeleteModelMappingResponse
|
|
||||||
} from './types'
|
} from './types'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -99,27 +96,6 @@ export async function getProviderAvailableSourceModels(
|
|||||||
return response.data
|
return response.data
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新目录中的模型映射
|
|
||||||
*/
|
|
||||||
export async function updateCatalogMapping(
|
|
||||||
mappingId: string,
|
|
||||||
data: UpdateModelMappingRequest
|
|
||||||
): Promise<UpdateModelMappingResponse> {
|
|
||||||
const response = await client.put(`/api/admin/models/catalog/mappings/${mappingId}`, data)
|
|
||||||
return response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 删除目录中的模型映射
|
|
||||||
*/
|
|
||||||
export async function deleteCatalogMapping(
|
|
||||||
mappingId: string
|
|
||||||
): Promise<DeleteModelMappingResponse> {
|
|
||||||
const response = await client.delete(`/api/admin/models/catalog/mappings/${mappingId}`)
|
|
||||||
return response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 批量为 Provider 关联 GlobalModels
|
* 批量为 Provider 关联 GlobalModels
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -211,11 +211,17 @@ export interface ConcurrencyStatus {
|
|||||||
key_max_concurrent?: number
|
key_max_concurrent?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ProviderModelAlias {
|
||||||
|
name: string
|
||||||
|
priority: number // 优先级(数字越小优先级越高)
|
||||||
|
}
|
||||||
|
|
||||||
export interface Model {
|
export interface Model {
|
||||||
id: string
|
id: string
|
||||||
provider_id: string
|
provider_id: string
|
||||||
global_model_id?: string // 关联的 GlobalModel ID
|
global_model_id?: string // 关联的 GlobalModel ID
|
||||||
provider_model_name: string // Provider 侧的模型名称(原 name)
|
provider_model_name: string // Provider 侧的主模型名称
|
||||||
|
provider_model_aliases?: ProviderModelAlias[] | null // 模型名称别名列表(带优先级)
|
||||||
// 原始配置值(可能为空,为空时使用 GlobalModel 默认值)
|
// 原始配置值(可能为空,为空时使用 GlobalModel 默认值)
|
||||||
price_per_request?: number | null // 按次计费价格
|
price_per_request?: number | null // 按次计费价格
|
||||||
tiered_pricing?: TieredPricingConfig | null // 阶梯计费配置
|
tiered_pricing?: TieredPricingConfig | null // 阶梯计费配置
|
||||||
@@ -244,7 +250,8 @@ export interface Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ModelCreate {
|
export interface ModelCreate {
|
||||||
provider_model_name: string // Provider 侧的模型名称(原 name)
|
provider_model_name: string // Provider 侧的主模型名称
|
||||||
|
provider_model_aliases?: ProviderModelAlias[] // 模型名称别名列表(带优先级)
|
||||||
global_model_id: string // 关联的 GlobalModel ID(必填)
|
global_model_id: string // 关联的 GlobalModel ID(必填)
|
||||||
// 计费配置(可选,为空时使用 GlobalModel 默认值)
|
// 计费配置(可选,为空时使用 GlobalModel 默认值)
|
||||||
price_per_request?: number // 按次计费价格
|
price_per_request?: number // 按次计费价格
|
||||||
@@ -261,6 +268,7 @@ export interface ModelCreate {
|
|||||||
|
|
||||||
export interface ModelUpdate {
|
export interface ModelUpdate {
|
||||||
provider_model_name?: string
|
provider_model_name?: string
|
||||||
|
provider_model_aliases?: ProviderModelAlias[] | null // 模型名称别名列表(带优先级)
|
||||||
global_model_id?: string
|
global_model_id?: string
|
||||||
price_per_request?: number | null // 按次计费价格(null 表示清空/使用默认值)
|
price_per_request?: number | null // 按次计费价格(null 表示清空/使用默认值)
|
||||||
tiered_pricing?: TieredPricingConfig | null // 阶梯计费配置
|
tiered_pricing?: TieredPricingConfig | null // 阶梯计费配置
|
||||||
@@ -273,21 +281,6 @@ export interface ModelUpdate {
|
|||||||
is_available?: boolean
|
is_available?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ModelMapping {
|
|
||||||
id: string
|
|
||||||
source_model: string // 别名/源模型名
|
|
||||||
target_global_model_id: string // 目标 GlobalModel ID
|
|
||||||
target_global_model_name: string | null
|
|
||||||
target_global_model_display_name: string | null
|
|
||||||
provider_id: string | null
|
|
||||||
provider_name: string | null
|
|
||||||
scope: 'global' | 'provider'
|
|
||||||
mapping_type: 'alias' | 'mapping'
|
|
||||||
is_active: boolean
|
|
||||||
created_at: string
|
|
||||||
updated_at: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ModelCapabilities {
|
export interface ModelCapabilities {
|
||||||
supports_vision: boolean
|
supports_vision: boolean
|
||||||
supports_function_calling: boolean
|
supports_function_calling: boolean
|
||||||
@@ -335,7 +328,6 @@ export interface ModelCatalogItem {
|
|||||||
global_model_name: string // GlobalModel.name(原 source_model)
|
global_model_name: string // GlobalModel.name(原 source_model)
|
||||||
display_name: string // GlobalModel.display_name
|
display_name: string // GlobalModel.display_name
|
||||||
description?: string | null // GlobalModel.description
|
description?: string | null // GlobalModel.description
|
||||||
aliases: string[] // 所有指向该 GlobalModel 的别名列表
|
|
||||||
providers: ModelCatalogProviderDetail[] // 支持该模型的 Provider 列表
|
providers: ModelCatalogProviderDetail[] // 支持该模型的 Provider 列表
|
||||||
price_range: ModelPriceRange // 价格区间
|
price_range: ModelPriceRange // 价格区间
|
||||||
total_providers: number
|
total_providers: number
|
||||||
@@ -351,8 +343,6 @@ export interface ProviderAvailableSourceModel {
|
|||||||
global_model_name: string // GlobalModel.name(原 source_model)
|
global_model_name: string // GlobalModel.name(原 source_model)
|
||||||
display_name: string // GlobalModel.display_name
|
display_name: string // GlobalModel.display_name
|
||||||
provider_model_name: string // Model.provider_model_name(Provider 侧的模型名)
|
provider_model_name: string // Model.provider_model_name(Provider 侧的模型名)
|
||||||
has_alias: boolean // 是否有别名指向该 GlobalModel
|
|
||||||
aliases: string[] // 别名列表
|
|
||||||
model_id?: string | null // Model.id
|
model_id?: string | null // Model.id
|
||||||
price: ProviderModelPriceInfo
|
price: ProviderModelPriceInfo
|
||||||
capabilities: ModelCapabilities
|
capabilities: ModelCapabilities
|
||||||
@@ -371,65 +361,6 @@ export interface BatchAssignProviderConfig {
|
|||||||
model_id?: string
|
model_id?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BatchAssignModelMappingRequest {
|
|
||||||
global_model_id: string // 要分配的 GlobalModel ID(原 source_model)
|
|
||||||
providers: BatchAssignProviderConfig[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BatchAssignProviderResult {
|
|
||||||
provider_id: string
|
|
||||||
mapping_id?: string | null
|
|
||||||
created_model: boolean
|
|
||||||
model_id?: string | null
|
|
||||||
updated: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BatchAssignError {
|
|
||||||
provider_id: string
|
|
||||||
error: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BatchAssignModelMappingResponse {
|
|
||||||
success: boolean
|
|
||||||
created_mappings: BatchAssignProviderResult[]
|
|
||||||
errors: BatchAssignError[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ModelMappingCreate {
|
|
||||||
source_model: string // 源模型名或别名
|
|
||||||
target_global_model_id: string // 目标 GlobalModel ID
|
|
||||||
provider_id?: string | null
|
|
||||||
mapping_type?: 'alias' | 'mapping'
|
|
||||||
is_active?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ModelMappingUpdate {
|
|
||||||
source_model?: string // 源模型名或别名
|
|
||||||
target_global_model_id?: string // 目标 GlobalModel ID
|
|
||||||
provider_id?: string | null
|
|
||||||
mapping_type?: 'alias' | 'mapping'
|
|
||||||
is_active?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UpdateModelMappingRequest {
|
|
||||||
source_model?: string
|
|
||||||
target_global_model_id?: string
|
|
||||||
provider_id?: string | null
|
|
||||||
mapping_type?: 'alias' | 'mapping'
|
|
||||||
is_active?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UpdateModelMappingResponse {
|
|
||||||
success: boolean
|
|
||||||
mapping_id: string
|
|
||||||
message?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DeleteModelMappingResponse {
|
|
||||||
success: boolean
|
|
||||||
message?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AdaptiveStatsResponse {
|
export interface AdaptiveStatsResponse {
|
||||||
adaptive_mode: boolean
|
adaptive_mode: boolean
|
||||||
current_limit: number | null
|
current_limit: number | null
|
||||||
|
|||||||
@@ -1,384 +0,0 @@
|
|||||||
<template>
|
|
||||||
<Dialog
|
|
||||||
:model-value="open"
|
|
||||||
:title="dialogTitle"
|
|
||||||
:description="dialogDescription"
|
|
||||||
:icon="dialogIcon"
|
|
||||||
size="md"
|
|
||||||
@update:model-value="handleDialogUpdate"
|
|
||||||
>
|
|
||||||
<form
|
|
||||||
class="space-y-4"
|
|
||||||
@submit.prevent="handleSubmit"
|
|
||||||
>
|
|
||||||
<!-- 模式选择(仅创建时显示) -->
|
|
||||||
<div
|
|
||||||
v-if="!isEditMode"
|
|
||||||
class="space-y-2"
|
|
||||||
>
|
|
||||||
<Label>创建类型 *</Label>
|
|
||||||
<div class="grid grid-cols-2 gap-3">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="p-3 rounded-lg border-2 text-left transition-all"
|
|
||||||
:class="[
|
|
||||||
form.mapping_type === 'alias'
|
|
||||||
? 'border-primary bg-primary/5'
|
|
||||||
: 'border-border hover:border-primary/50'
|
|
||||||
]"
|
|
||||||
@click="form.mapping_type = 'alias'"
|
|
||||||
>
|
|
||||||
<div class="font-medium text-sm">
|
|
||||||
别名
|
|
||||||
</div>
|
|
||||||
<div class="text-xs text-muted-foreground mt-1">
|
|
||||||
名称简写,按目标模型计费
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="p-3 rounded-lg border-2 text-left transition-all"
|
|
||||||
:class="[
|
|
||||||
form.mapping_type === 'mapping'
|
|
||||||
? 'border-primary bg-primary/5'
|
|
||||||
: 'border-border hover:border-primary/50'
|
|
||||||
]"
|
|
||||||
@click="form.mapping_type = 'mapping'"
|
|
||||||
>
|
|
||||||
<div class="font-medium text-sm">
|
|
||||||
映射
|
|
||||||
</div>
|
|
||||||
<div class="text-xs text-muted-foreground mt-1">
|
|
||||||
模型降级,按源模型计费
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 模式说明 -->
|
|
||||||
<div class="rounded-lg border border-border bg-muted/50 p-3 text-sm">
|
|
||||||
<p class="text-foreground font-medium mb-1">
|
|
||||||
{{ form.mapping_type === 'alias' ? '别名模式' : '映射模式' }}
|
|
||||||
</p>
|
|
||||||
<p class="text-muted-foreground text-xs">
|
|
||||||
{{ form.mapping_type === 'alias'
|
|
||||||
? '用户请求此别名时,会路由到目标模型,并按目标模型价格计费。'
|
|
||||||
: '将源模型的请求转发到目标模型处理,按源模型价格计费。' }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Provider 选择/作用范围 -->
|
|
||||||
<div
|
|
||||||
v-if="showProviderSelect"
|
|
||||||
class="space-y-2"
|
|
||||||
>
|
|
||||||
<Label>作用范围</Label>
|
|
||||||
<!-- 固定 Provider 时显示只读 -->
|
|
||||||
<div
|
|
||||||
v-if="fixedProvider"
|
|
||||||
class="px-3 py-2 border rounded-md bg-muted/50 text-sm"
|
|
||||||
>
|
|
||||||
仅 {{ fixedProvider.display_name || fixedProvider.name }}
|
|
||||||
</div>
|
|
||||||
<!-- 否则显示可选择的下拉 -->
|
|
||||||
<Select
|
|
||||||
v-else
|
|
||||||
v-model:open="providerSelectOpen"
|
|
||||||
:model-value="form.provider_id || 'global'"
|
|
||||||
@update:model-value="handleProviderChange"
|
|
||||||
>
|
|
||||||
<SelectTrigger class="w-full">
|
|
||||||
<SelectValue placeholder="选择作用范围" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="global">
|
|
||||||
全局(所有 Provider)
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem
|
|
||||||
v-for="p in providers"
|
|
||||||
:key="p.id"
|
|
||||||
:value="p.id"
|
|
||||||
>
|
|
||||||
仅 {{ p.display_name || p.name }}
|
|
||||||
</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 别名模式:别名名称 -->
|
|
||||||
<div
|
|
||||||
v-if="form.mapping_type === 'alias'"
|
|
||||||
class="space-y-2"
|
|
||||||
>
|
|
||||||
<Label for="alias-name">别名名称 *</Label>
|
|
||||||
<Input
|
|
||||||
id="alias-name"
|
|
||||||
v-model="form.alias"
|
|
||||||
placeholder="如:sonnet, opus"
|
|
||||||
:disabled="isEditMode"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<p class="text-xs text-muted-foreground">
|
|
||||||
{{ isEditMode ? '创建后不可修改' : '用户将使用此名称请求模型' }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 映射模式:选择源模型 -->
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="space-y-2"
|
|
||||||
>
|
|
||||||
<Label>源模型 (用户请求的模型) *</Label>
|
|
||||||
<Select
|
|
||||||
v-model:open="sourceModelSelectOpen"
|
|
||||||
:model-value="form.alias"
|
|
||||||
:disabled="isEditMode"
|
|
||||||
@update:model-value="form.alias = $event"
|
|
||||||
>
|
|
||||||
<SelectTrigger
|
|
||||||
class="w-full"
|
|
||||||
:class="{ 'opacity-50': isEditMode }"
|
|
||||||
>
|
|
||||||
<SelectValue placeholder="请选择源模型" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem
|
|
||||||
v-for="model in availableSourceModels"
|
|
||||||
:key="model.id"
|
|
||||||
:value="model.name"
|
|
||||||
>
|
|
||||||
{{ model.display_name }} ({{ model.name }})
|
|
||||||
</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<p class="text-xs text-muted-foreground">
|
|
||||||
{{ isEditMode ? '创建后不可修改' : '选择要被映射的源模型,计费将按此模型价格' }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 目标模型选择 -->
|
|
||||||
<div class="space-y-2">
|
|
||||||
<Label>
|
|
||||||
{{ form.mapping_type === 'alias' ? '目标模型 *' : '目标模型 (实际处理请求) *' }}
|
|
||||||
</Label>
|
|
||||||
<!-- 固定目标模型时显示只读信息 -->
|
|
||||||
<div
|
|
||||||
v-if="fixedTargetModel"
|
|
||||||
class="px-3 py-2 border rounded-md bg-muted/50"
|
|
||||||
>
|
|
||||||
<span class="font-medium">{{ fixedTargetModel.display_name }}</span>
|
|
||||||
<span class="text-muted-foreground ml-1">({{ fixedTargetModel.name }})</span>
|
|
||||||
</div>
|
|
||||||
<!-- 否则显示下拉选择 -->
|
|
||||||
<Select
|
|
||||||
v-else
|
|
||||||
v-model:open="targetModelSelectOpen"
|
|
||||||
:model-value="form.global_model_id"
|
|
||||||
@update:model-value="form.global_model_id = $event"
|
|
||||||
>
|
|
||||||
<SelectTrigger class="w-full">
|
|
||||||
<SelectValue placeholder="请选择模型" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem
|
|
||||||
v-for="model in availableTargetModels"
|
|
||||||
:key="model.id"
|
|
||||||
:value="model.id"
|
|
||||||
>
|
|
||||||
{{ model.display_name }} ({{ model.name }})
|
|
||||||
</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<template #footer>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
@click="handleCancel"
|
|
||||||
>
|
|
||||||
取消
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
:disabled="submitting"
|
|
||||||
@click="handleSubmit"
|
|
||||||
>
|
|
||||||
<Loader2
|
|
||||||
v-if="submitting"
|
|
||||||
class="w-4 h-4 mr-2 animate-spin"
|
|
||||||
/>
|
|
||||||
{{ isEditMode ? '保存' : '创建' }}
|
|
||||||
</Button>
|
|
||||||
</template>
|
|
||||||
</Dialog>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, computed } from 'vue'
|
|
||||||
import { Loader2, Tag, SquarePen } from 'lucide-vue-next'
|
|
||||||
import { Dialog, Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@/components/ui'
|
|
||||||
import Button from '@/components/ui/button.vue'
|
|
||||||
import Input from '@/components/ui/input.vue'
|
|
||||||
import Label from '@/components/ui/label.vue'
|
|
||||||
import { useToast } from '@/composables/useToast'
|
|
||||||
import { useFormDialog } from '@/composables/useFormDialog'
|
|
||||||
import type { ModelAlias, CreateModelAliasRequest, UpdateModelAliasRequest } from '@/api/endpoints/aliases'
|
|
||||||
import type { GlobalModelResponse } from '@/api/global-models'
|
|
||||||
|
|
||||||
export interface ProviderOption {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
display_name?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AliasFormData {
|
|
||||||
alias: string
|
|
||||||
global_model_id: string
|
|
||||||
provider_id: string | null
|
|
||||||
mapping_type: 'alias' | 'mapping'
|
|
||||||
is_active: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
|
||||||
open: boolean
|
|
||||||
editingAlias?: ModelAlias | null
|
|
||||||
globalModels: GlobalModelResponse[]
|
|
||||||
providers?: ProviderOption[]
|
|
||||||
fixedTargetModel?: GlobalModelResponse | null // 用于从模型详情抽屉打开时固定目标模型
|
|
||||||
fixedProvider?: ProviderOption | null // 用于 Provider 特定别名固定 Provider
|
|
||||||
showProviderSelect?: boolean // 是否显示 Provider 选择(默认 true)
|
|
||||||
}>(), {
|
|
||||||
editingAlias: null,
|
|
||||||
providers: () => [],
|
|
||||||
fixedTargetModel: null,
|
|
||||||
fixedProvider: null,
|
|
||||||
showProviderSelect: true
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
'update:open': [value: boolean]
|
|
||||||
'submit': [data: CreateModelAliasRequest | UpdateModelAliasRequest, isEdit: boolean]
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const { error: showError } = useToast()
|
|
||||||
|
|
||||||
// 状态
|
|
||||||
const submitting = ref(false)
|
|
||||||
const providerSelectOpen = ref(false)
|
|
||||||
const sourceModelSelectOpen = ref(false)
|
|
||||||
const targetModelSelectOpen = ref(false)
|
|
||||||
const form = ref<AliasFormData>({
|
|
||||||
alias: '',
|
|
||||||
global_model_id: '',
|
|
||||||
provider_id: null,
|
|
||||||
mapping_type: 'alias',
|
|
||||||
is_active: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
// 处理 Provider 选择变化
|
|
||||||
function handleProviderChange(value: string) {
|
|
||||||
form.value.provider_id = value === 'global' ? null : value
|
|
||||||
}
|
|
||||||
|
|
||||||
// 重置表单
|
|
||||||
function resetForm() {
|
|
||||||
form.value = {
|
|
||||||
alias: '',
|
|
||||||
global_model_id: props.fixedTargetModel?.id || '',
|
|
||||||
provider_id: props.fixedProvider?.id || null,
|
|
||||||
mapping_type: 'alias',
|
|
||||||
is_active: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载别名数据(编辑模式)
|
|
||||||
function loadAliasData() {
|
|
||||||
if (!props.editingAlias) return
|
|
||||||
form.value = {
|
|
||||||
alias: props.editingAlias.alias,
|
|
||||||
global_model_id: props.editingAlias.global_model_id,
|
|
||||||
provider_id: props.editingAlias.provider_id,
|
|
||||||
mapping_type: props.editingAlias.mapping_type || 'alias',
|
|
||||||
is_active: props.editingAlias.is_active,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用 useFormDialog 统一处理对话框逻辑
|
|
||||||
const { isEditMode, handleDialogUpdate, handleCancel } = useFormDialog({
|
|
||||||
isOpen: () => props.open,
|
|
||||||
entity: () => props.editingAlias,
|
|
||||||
isLoading: submitting,
|
|
||||||
onClose: () => emit('update:open', false),
|
|
||||||
loadData: loadAliasData,
|
|
||||||
resetForm,
|
|
||||||
})
|
|
||||||
|
|
||||||
// 对话框标题
|
|
||||||
const dialogTitle = computed(() => {
|
|
||||||
if (isEditMode.value) {
|
|
||||||
return form.value.mapping_type === 'mapping' ? '编辑映射' : '编辑别名'
|
|
||||||
}
|
|
||||||
if (props.fixedProvider) {
|
|
||||||
return `创建 ${props.fixedProvider.display_name || props.fixedProvider.name} 特定别名/映射`
|
|
||||||
}
|
|
||||||
return '创建别名/映射'
|
|
||||||
})
|
|
||||||
|
|
||||||
// 对话框描述
|
|
||||||
const dialogDescription = computed(() => {
|
|
||||||
if (isEditMode.value) {
|
|
||||||
return form.value.mapping_type === 'mapping' ? '修改模型映射配置' : '修改别名设置'
|
|
||||||
}
|
|
||||||
return '为模型创建别名或映射规则'
|
|
||||||
})
|
|
||||||
|
|
||||||
// 对话框图标
|
|
||||||
const dialogIcon = computed(() => isEditMode.value ? SquarePen : Tag)
|
|
||||||
|
|
||||||
// 映射模式下可选的源模型(排除已选择的目标模型)
|
|
||||||
const availableSourceModels = computed(() => {
|
|
||||||
return props.globalModels.filter(m => m.id !== form.value.global_model_id)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 可选的目标模型(映射模式下排除已选择的源模型)
|
|
||||||
const availableTargetModels = computed(() => {
|
|
||||||
if (form.value.mapping_type === 'mapping' && form.value.alias) {
|
|
||||||
// 找到源模型对应的 GlobalModel
|
|
||||||
const sourceModel = props.globalModels.find(m => m.name === form.value.alias)
|
|
||||||
if (sourceModel) {
|
|
||||||
return props.globalModels.filter(m => m.id !== sourceModel.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return props.globalModels
|
|
||||||
})
|
|
||||||
|
|
||||||
// 提交表单
|
|
||||||
async function handleSubmit() {
|
|
||||||
if (!form.value.alias) {
|
|
||||||
showError(form.value.mapping_type === 'alias' ? '请输入别名名称' : '请选择源模型', '错误')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const targetModelId = props.fixedTargetModel?.id || form.value.global_model_id
|
|
||||||
if (!targetModelId) {
|
|
||||||
showError('请选择目标模型', '错误')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
submitting.value = true
|
|
||||||
try {
|
|
||||||
const data: CreateModelAliasRequest | UpdateModelAliasRequest = {
|
|
||||||
alias: form.value.alias,
|
|
||||||
global_model_id: targetModelId,
|
|
||||||
provider_id: props.fixedProvider?.id || form.value.provider_id,
|
|
||||||
mapping_type: form.value.mapping_type,
|
|
||||||
is_active: form.value.is_active,
|
|
||||||
}
|
|
||||||
|
|
||||||
emit('submit', data, !!props.editingAlias)
|
|
||||||
} finally {
|
|
||||||
submitting.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -104,19 +104,6 @@
|
|||||||
<span class="hidden sm:inline">关联提供商</span>
|
<span class="hidden sm:inline">关联提供商</span>
|
||||||
<span class="sm:hidden">提供商</span>
|
<span class="sm:hidden">提供商</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
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 === 'aliases'
|
|
||||||
? 'bg-primary text-primary-foreground shadow-sm'
|
|
||||||
: 'text-muted-foreground hover:text-foreground hover:bg-background/50'
|
|
||||||
]"
|
|
||||||
@click="detailTab = 'aliases'"
|
|
||||||
>
|
|
||||||
<span class="hidden sm:inline">别名/映射</span>
|
|
||||||
<span class="sm:hidden">别名</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tab 内容 -->
|
<!-- Tab 内容 -->
|
||||||
@@ -684,236 +671,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tab 3: 别名 -->
|
|
||||||
<div v-show="detailTab === 'aliases'">
|
|
||||||
<Card class="overflow-hidden">
|
|
||||||
<!-- 标题栏 -->
|
|
||||||
<div class="px-4 py-3 border-b border-border/60">
|
|
||||||
<div class="flex items-center justify-between gap-4">
|
|
||||||
<div>
|
|
||||||
<h4 class="text-sm font-semibold">
|
|
||||||
别名与映射
|
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
class="h-8 w-8"
|
|
||||||
title="添加别名/映射"
|
|
||||||
@click="$emit('addAlias')"
|
|
||||||
>
|
|
||||||
<Plus class="w-3.5 h-3.5" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
class="h-8 w-8"
|
|
||||||
title="刷新"
|
|
||||||
@click="$emit('refreshAliases')"
|
|
||||||
>
|
|
||||||
<RefreshCw
|
|
||||||
class="w-3.5 h-3.5"
|
|
||||||
:class="loadingAliases ? 'animate-spin' : ''"
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 表格内容 -->
|
|
||||||
<div
|
|
||||||
v-if="loadingAliases"
|
|
||||||
class="flex items-center justify-center py-12"
|
|
||||||
>
|
|
||||||
<Loader2 class="w-6 h-6 animate-spin text-primary" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<template v-else-if="aliases.length > 0">
|
|
||||||
<!-- 桌面端表格 -->
|
|
||||||
<Table class="hidden sm:table">
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow class="border-b border-border/60 hover:bg-transparent">
|
|
||||||
<TableHead class="h-10 font-semibold">
|
|
||||||
别名
|
|
||||||
</TableHead>
|
|
||||||
<TableHead class="w-[80px] h-10 font-semibold">
|
|
||||||
类型
|
|
||||||
</TableHead>
|
|
||||||
<TableHead class="w-[100px] h-10 font-semibold">
|
|
||||||
作用域
|
|
||||||
</TableHead>
|
|
||||||
<TableHead class="w-[100px] h-10 font-semibold text-center">
|
|
||||||
操作
|
|
||||||
</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
<TableRow
|
|
||||||
v-for="alias in aliases"
|
|
||||||
:key="alias.id"
|
|
||||||
class="border-b border-border/40 hover:bg-muted/30 transition-colors"
|
|
||||||
>
|
|
||||||
<TableCell class="py-3">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span
|
|
||||||
class="w-2 h-2 rounded-full shrink-0"
|
|
||||||
:class="alias.is_active ? 'bg-green-500' : 'bg-gray-300'"
|
|
||||||
:title="alias.is_active ? '活跃' : '停用'"
|
|
||||||
/>
|
|
||||||
<code class="text-sm font-medium bg-muted px-1.5 py-0.5 rounded">{{ alias.alias }}</code>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell class="py-3">
|
|
||||||
<Badge
|
|
||||||
variant="secondary"
|
|
||||||
class="text-xs"
|
|
||||||
>
|
|
||||||
{{ alias.mapping_type === 'mapping' ? '映射' : '别名' }}
|
|
||||||
</Badge>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell class="py-3">
|
|
||||||
<Badge
|
|
||||||
v-if="alias.provider_id"
|
|
||||||
variant="outline"
|
|
||||||
class="text-xs truncate max-w-[90px]"
|
|
||||||
:title="alias.provider_name || 'Provider'"
|
|
||||||
>
|
|
||||||
{{ alias.provider_name || 'Provider' }}
|
|
||||||
</Badge>
|
|
||||||
<Badge
|
|
||||||
v-else
|
|
||||||
variant="default"
|
|
||||||
class="text-xs"
|
|
||||||
>
|
|
||||||
全局
|
|
||||||
</Badge>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell class="py-3 text-center">
|
|
||||||
<div class="flex items-center justify-center gap-0.5">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
class="h-7 w-7"
|
|
||||||
title="编辑"
|
|
||||||
@click="$emit('editAlias', alias)"
|
|
||||||
>
|
|
||||||
<Edit class="w-3.5 h-3.5" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
class="h-7 w-7"
|
|
||||||
:title="alias.is_active ? '停用' : '启用'"
|
|
||||||
@click="$emit('toggleAliasStatus', alias)"
|
|
||||||
>
|
|
||||||
<Power class="w-3.5 h-3.5" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
class="h-7 w-7"
|
|
||||||
title="删除"
|
|
||||||
@click="$emit('deleteAlias', alias)"
|
|
||||||
>
|
|
||||||
<Trash2 class="w-3.5 h-3.5" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
|
|
||||||
<!-- 移动端卡片列表 -->
|
|
||||||
<div class="sm:hidden divide-y divide-border/40">
|
|
||||||
<div
|
|
||||||
v-for="alias in aliases"
|
|
||||||
:key="alias.id"
|
|
||||||
class="p-4 space-y-2"
|
|
||||||
>
|
|
||||||
<div class="flex items-start justify-between gap-3">
|
|
||||||
<div class="flex items-center gap-2 min-w-0 flex-1">
|
|
||||||
<span
|
|
||||||
class="w-2 h-2 rounded-full shrink-0"
|
|
||||||
:class="alias.is_active ? 'bg-green-500' : 'bg-gray-300'"
|
|
||||||
/>
|
|
||||||
<code class="text-sm font-medium bg-muted px-1.5 py-0.5 rounded truncate">{{ alias.alias }}</code>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-1 shrink-0">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
class="h-7 w-7"
|
|
||||||
@click="$emit('editAlias', alias)"
|
|
||||||
>
|
|
||||||
<Edit class="w-3.5 h-3.5" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
class="h-7 w-7"
|
|
||||||
@click="$emit('toggleAliasStatus', alias)"
|
|
||||||
>
|
|
||||||
<Power class="w-3.5 h-3.5" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
class="h-7 w-7"
|
|
||||||
@click="$emit('deleteAlias', alias)"
|
|
||||||
>
|
|
||||||
<Trash2 class="w-3.5 h-3.5" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<Badge
|
|
||||||
variant="secondary"
|
|
||||||
class="text-xs"
|
|
||||||
>
|
|
||||||
{{ alias.mapping_type === 'mapping' ? '映射' : '别名' }}
|
|
||||||
</Badge>
|
|
||||||
<Badge
|
|
||||||
v-if="alias.provider_id"
|
|
||||||
variant="outline"
|
|
||||||
class="text-xs truncate max-w-[120px]"
|
|
||||||
>
|
|
||||||
{{ alias.provider_name || 'Provider' }}
|
|
||||||
</Badge>
|
|
||||||
<Badge
|
|
||||||
v-else
|
|
||||||
variant="default"
|
|
||||||
class="text-xs"
|
|
||||||
>
|
|
||||||
全局
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="text-center py-12"
|
|
||||||
>
|
|
||||||
<!-- 空状态 -->
|
|
||||||
<Tag class="w-12 h-12 mx-auto text-muted-foreground/30 mb-3" />
|
|
||||||
<p class="text-sm text-muted-foreground">
|
|
||||||
暂无别名或映射
|
|
||||||
</p>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
class="mt-4"
|
|
||||||
@click="$emit('addAlias')"
|
|
||||||
>
|
|
||||||
<Plus class="w-4 h-4 mr-1" />
|
|
||||||
添加别名/映射
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
@@ -931,7 +688,6 @@ import {
|
|||||||
Zap,
|
Zap,
|
||||||
Image,
|
Image,
|
||||||
Building2,
|
Building2,
|
||||||
Tag,
|
|
||||||
Plus,
|
Plus,
|
||||||
Edit,
|
Edit,
|
||||||
Trash2,
|
Trash2,
|
||||||
@@ -955,13 +711,11 @@ import TableCell from '@/components/ui/table-cell.vue'
|
|||||||
|
|
||||||
// 使用外部类型定义
|
// 使用外部类型定义
|
||||||
import type { GlobalModelResponse } from '@/api/global-models'
|
import type { GlobalModelResponse } from '@/api/global-models'
|
||||||
import type { ModelAlias } from '@/api/endpoints/aliases'
|
|
||||||
import type { TieredPricingConfig, PricingTier } from '@/api/endpoints/types'
|
import type { TieredPricingConfig, PricingTier } from '@/api/endpoints/types'
|
||||||
import type { CapabilityDefinition } from '@/api/endpoints'
|
import type { CapabilityDefinition } from '@/api/endpoints'
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
loadingProviders: false,
|
loadingProviders: false,
|
||||||
loadingAliases: false,
|
|
||||||
hasBlockingDialogOpen: false,
|
hasBlockingDialogOpen: false,
|
||||||
})
|
})
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -973,11 +727,6 @@ const emit = defineEmits<{
|
|||||||
'deleteProvider': [provider: any]
|
'deleteProvider': [provider: any]
|
||||||
'toggleProviderStatus': [provider: any]
|
'toggleProviderStatus': [provider: any]
|
||||||
'refreshProviders': []
|
'refreshProviders': []
|
||||||
'addAlias': []
|
|
||||||
'editAlias': [alias: ModelAlias]
|
|
||||||
'toggleAliasStatus': [alias: ModelAlias]
|
|
||||||
'deleteAlias': [alias: ModelAlias]
|
|
||||||
'refreshAliases': []
|
|
||||||
}>()
|
}>()
|
||||||
const { success: showSuccess, error: showError } = useToast()
|
const { success: showSuccess, error: showError } = useToast()
|
||||||
|
|
||||||
@@ -985,9 +734,7 @@ interface Props {
|
|||||||
model: GlobalModelResponse | null
|
model: GlobalModelResponse | null
|
||||||
open: boolean
|
open: boolean
|
||||||
providers: any[]
|
providers: any[]
|
||||||
aliases: ModelAlias[]
|
|
||||||
loadingProviders?: boolean
|
loadingProviders?: boolean
|
||||||
loadingAliases?: boolean
|
|
||||||
hasBlockingDialogOpen?: boolean
|
hasBlockingDialogOpen?: boolean
|
||||||
capabilities?: CapabilityDefinition[]
|
capabilities?: CapabilityDefinition[]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
export { default as GlobalModelFormDialog } from './GlobalModelFormDialog.vue'
|
export { default as GlobalModelFormDialog } from './GlobalModelFormDialog.vue'
|
||||||
export { default as AliasDialog } from './AliasDialog.vue'
|
|
||||||
export { default as ModelDetailDrawer } from './ModelDetailDrawer.vue'
|
export { default as ModelDetailDrawer } from './ModelDetailDrawer.vue'
|
||||||
export { default as TieredPricingEditor } from './TieredPricingEditor.vue'
|
export { default as TieredPricingEditor } from './TieredPricingEditor.vue'
|
||||||
|
|||||||
@@ -526,14 +526,7 @@
|
|||||||
@edit-model="handleEditModel"
|
@edit-model="handleEditModel"
|
||||||
@delete-model="handleDeleteModel"
|
@delete-model="handleDeleteModel"
|
||||||
@batch-assign="handleBatchAssign"
|
@batch-assign="handleBatchAssign"
|
||||||
/>
|
@manage-alias="handleManageAlias"
|
||||||
|
|
||||||
<!-- 模型映射 -->
|
|
||||||
<MappingsTab
|
|
||||||
v-if="provider"
|
|
||||||
:key="`mappings-${provider.id}`"
|
|
||||||
:provider="provider"
|
|
||||||
@refresh="handleRelatedDataRefresh"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -636,6 +629,16 @@
|
|||||||
@update:open="batchAssignDialogOpen = $event"
|
@update:open="batchAssignDialogOpen = $event"
|
||||||
@changed="handleBatchAssignChanged"
|
@changed="handleBatchAssignChanged"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- 模型别名管理对话框 -->
|
||||||
|
<ModelAliasDialog
|
||||||
|
v-if="open && provider"
|
||||||
|
:open="aliasDialogOpen"
|
||||||
|
:provider-id="provider.id"
|
||||||
|
:model="aliasEditingModel"
|
||||||
|
@update:open="aliasDialogOpen = $event"
|
||||||
|
@saved="handleAliasSaved"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -663,9 +666,9 @@ import { getProvider, getProviderEndpoints } from '@/api/endpoints'
|
|||||||
import {
|
import {
|
||||||
KeyFormDialog,
|
KeyFormDialog,
|
||||||
KeyAllowedModelsDialog,
|
KeyAllowedModelsDialog,
|
||||||
MappingsTab,
|
|
||||||
ModelsTab,
|
ModelsTab,
|
||||||
BatchAssignModelsDialog
|
BatchAssignModelsDialog,
|
||||||
|
ModelAliasDialog
|
||||||
} from '@/features/providers/components'
|
} from '@/features/providers/components'
|
||||||
import EndpointFormDialog from '@/features/providers/components/EndpointFormDialog.vue'
|
import EndpointFormDialog from '@/features/providers/components/EndpointFormDialog.vue'
|
||||||
import ProviderModelFormDialog from '@/features/providers/components/ProviderModelFormDialog.vue'
|
import ProviderModelFormDialog from '@/features/providers/components/ProviderModelFormDialog.vue'
|
||||||
@@ -734,6 +737,10 @@ const deleteModelConfirmOpen = ref(false)
|
|||||||
const modelToDelete = ref<Model | null>(null)
|
const modelToDelete = ref<Model | null>(null)
|
||||||
const batchAssignDialogOpen = ref(false)
|
const batchAssignDialogOpen = ref(false)
|
||||||
|
|
||||||
|
// 别名管理相关状态
|
||||||
|
const aliasDialogOpen = ref(false)
|
||||||
|
const aliasEditingModel = ref<Model | null>(null)
|
||||||
|
|
||||||
// 拖动排序相关状态
|
// 拖动排序相关状态
|
||||||
const dragState = ref({
|
const dragState = ref({
|
||||||
isDragging: false,
|
isDragging: false,
|
||||||
@@ -755,7 +762,8 @@ const hasBlockingDialogOpen = computed(() =>
|
|||||||
deleteKeyConfirmOpen.value ||
|
deleteKeyConfirmOpen.value ||
|
||||||
modelFormDialogOpen.value ||
|
modelFormDialogOpen.value ||
|
||||||
deleteModelConfirmOpen.value ||
|
deleteModelConfirmOpen.value ||
|
||||||
batchAssignDialogOpen.value
|
batchAssignDialogOpen.value ||
|
||||||
|
aliasDialogOpen.value
|
||||||
)
|
)
|
||||||
|
|
||||||
// 监听 providerId 变化
|
// 监听 providerId 变化
|
||||||
@@ -784,6 +792,7 @@ watch(() => props.open, (newOpen) => {
|
|||||||
keyAllowedModelsDialogOpen.value = false
|
keyAllowedModelsDialogOpen.value = false
|
||||||
deleteKeyConfirmOpen.value = false
|
deleteKeyConfirmOpen.value = false
|
||||||
batchAssignDialogOpen.value = false
|
batchAssignDialogOpen.value = false
|
||||||
|
aliasDialogOpen.value = false
|
||||||
|
|
||||||
// 重置临时数据
|
// 重置临时数据
|
||||||
endpointToEdit.value = null
|
endpointToEdit.value = null
|
||||||
@@ -1021,6 +1030,19 @@ async function handleBatchAssignChanged() {
|
|||||||
emit('refresh')
|
emit('refresh')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理管理别名 - 打开别名对话框
|
||||||
|
function handleManageAlias(model: Model) {
|
||||||
|
aliasEditingModel.value = model
|
||||||
|
aliasDialogOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理别名保存完成
|
||||||
|
async function handleAliasSaved() {
|
||||||
|
aliasEditingModel.value = null
|
||||||
|
await loadProvider()
|
||||||
|
emit('refresh')
|
||||||
|
}
|
||||||
|
|
||||||
// 处理模型保存完成
|
// 处理模型保存完成
|
||||||
async function handleModelSaved() {
|
async function handleModelSaved() {
|
||||||
editingModel.value = null
|
editingModel.value = null
|
||||||
|
|||||||
@@ -7,6 +7,6 @@ export { default as ProviderModelFormDialog } from './ProviderModelFormDialog.vu
|
|||||||
export { default as ProviderDetailDrawer } from './ProviderDetailDrawer.vue'
|
export { default as ProviderDetailDrawer } from './ProviderDetailDrawer.vue'
|
||||||
export { default as EndpointHealthTimeline } from './EndpointHealthTimeline.vue'
|
export { default as EndpointHealthTimeline } from './EndpointHealthTimeline.vue'
|
||||||
export { default as BatchAssignModelsDialog } from './BatchAssignModelsDialog.vue'
|
export { default as BatchAssignModelsDialog } from './BatchAssignModelsDialog.vue'
|
||||||
|
export { default as ModelAliasDialog } from './ModelAliasDialog.vue'
|
||||||
|
|
||||||
export { default as MappingsTab } from './provider-tabs/MappingsTab.vue'
|
|
||||||
export { default as ModelsTab } from './provider-tabs/ModelsTab.vue'
|
export { default as ModelsTab } from './provider-tabs/ModelsTab.vue'
|
||||||
|
|||||||
@@ -1,310 +0,0 @@
|
|||||||
<template>
|
|
||||||
<Card class="overflow-hidden">
|
|
||||||
<!-- 标题头部 -->
|
|
||||||
<div class="p-4 border-b border-border/60">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<h3 class="text-sm font-semibold leading-none">
|
|
||||||
别名与映射管理
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
v-if="!hideAddButton"
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
class="h-8"
|
|
||||||
@click="openCreateDialog"
|
|
||||||
>
|
|
||||||
<Plus class="w-3.5 h-3.5 mr-1.5" />
|
|
||||||
创建别名/映射
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 加载状态 -->
|
|
||||||
<div
|
|
||||||
v-if="loading"
|
|
||||||
class="flex items-center justify-center py-12"
|
|
||||||
>
|
|
||||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 别名列表 -->
|
|
||||||
<div
|
|
||||||
v-else-if="mappings.length > 0"
|
|
||||||
class="overflow-x-auto"
|
|
||||||
>
|
|
||||||
<table class="w-full text-sm">
|
|
||||||
<thead class="bg-muted/50 text-xs uppercase tracking-wide text-muted-foreground">
|
|
||||||
<tr>
|
|
||||||
<th class="text-left px-4 py-3 font-semibold">
|
|
||||||
名称
|
|
||||||
</th>
|
|
||||||
<th class="text-left px-4 py-3 font-semibold w-24">
|
|
||||||
类型
|
|
||||||
</th>
|
|
||||||
<th class="text-left px-4 py-3 font-semibold">
|
|
||||||
指向模型
|
|
||||||
</th>
|
|
||||||
<th
|
|
||||||
v-if="!hideAddButton"
|
|
||||||
class="px-4 py-3 font-semibold w-28 text-center"
|
|
||||||
>
|
|
||||||
操作
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr
|
|
||||||
v-for="mapping in mappings"
|
|
||||||
:key="mapping.id"
|
|
||||||
class="border-b border-border/40 last:border-b-0 hover:bg-muted/30 transition-colors"
|
|
||||||
>
|
|
||||||
<td class="px-4 py-3">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<!-- 状态指示灯 -->
|
|
||||||
<span
|
|
||||||
class="w-2 h-2 rounded-full shrink-0"
|
|
||||||
:class="mapping.is_active ? 'bg-green-500' : 'bg-gray-300'"
|
|
||||||
:title="mapping.is_active ? '活跃' : '停用'"
|
|
||||||
/>
|
|
||||||
<span class="font-mono">{{ mapping.alias }}</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="px-4 py-3">
|
|
||||||
<Badge
|
|
||||||
variant="secondary"
|
|
||||||
class="text-xs"
|
|
||||||
>
|
|
||||||
{{ mapping.mapping_type === 'mapping' ? '映射' : '别名' }}
|
|
||||||
</Badge>
|
|
||||||
</td>
|
|
||||||
<td class="px-4 py-3">
|
|
||||||
{{ mapping.global_model_display_name || mapping.global_model_name }}
|
|
||||||
</td>
|
|
||||||
<td
|
|
||||||
v-if="!hideAddButton"
|
|
||||||
class="px-4 py-3"
|
|
||||||
>
|
|
||||||
<div class="flex justify-center gap-1.5">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
class="h-8 w-8"
|
|
||||||
title="编辑"
|
|
||||||
@click="openEditDialog(mapping)"
|
|
||||||
>
|
|
||||||
<Edit class="w-3.5 h-3.5" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
class="h-8 w-8"
|
|
||||||
:disabled="togglingId === mapping.id"
|
|
||||||
:title="mapping.is_active ? '点击停用' : '点击启用'"
|
|
||||||
@click="toggleActive(mapping)"
|
|
||||||
>
|
|
||||||
<Power class="w-3.5 h-3.5" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
class="h-8 w-8 text-destructive hover:text-destructive"
|
|
||||||
title="删除"
|
|
||||||
@click="confirmDelete(mapping)"
|
|
||||||
>
|
|
||||||
<Trash2 class="w-3.5 h-3.5" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 空状态 -->
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="p-8 text-center text-muted-foreground"
|
|
||||||
>
|
|
||||||
<ArrowLeftRight class="w-12 h-12 mx-auto mb-3 opacity-50" />
|
|
||||||
<p class="text-sm">
|
|
||||||
暂无特定别名/映射
|
|
||||||
</p>
|
|
||||||
<p class="text-xs mt-1">
|
|
||||||
点击上方按钮添加
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<!-- 使用共享的 AliasDialog 组件 -->
|
|
||||||
<AliasDialog
|
|
||||||
:open="dialogOpen"
|
|
||||||
:editing-alias="editingAlias"
|
|
||||||
:global-models="availableModels"
|
|
||||||
:fixed-provider="fixedProviderOption"
|
|
||||||
:show-provider-select="true"
|
|
||||||
@update:open="handleDialogVisibility"
|
|
||||||
@submit="handleAliasSubmit"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, computed, onMounted } from 'vue'
|
|
||||||
import { ArrowLeftRight, Plus, Edit, Trash2, Power } from 'lucide-vue-next'
|
|
||||||
import Card from '@/components/ui/card.vue'
|
|
||||||
import Badge from '@/components/ui/badge.vue'
|
|
||||||
import Button from '@/components/ui/button.vue'
|
|
||||||
import AliasDialog from '@/features/models/components/AliasDialog.vue'
|
|
||||||
import { useToast } from '@/composables/useToast'
|
|
||||||
import {
|
|
||||||
getAliases,
|
|
||||||
createAlias,
|
|
||||||
updateAlias,
|
|
||||||
deleteAlias,
|
|
||||||
type ModelAlias,
|
|
||||||
type CreateModelAliasRequest,
|
|
||||||
type UpdateModelAliasRequest,
|
|
||||||
} from '@/api/endpoints/aliases'
|
|
||||||
import { listGlobalModels, type GlobalModelResponse } from '@/api/global-models'
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
|
||||||
provider: any
|
|
||||||
hideAddButton?: boolean
|
|
||||||
}>(), {
|
|
||||||
hideAddButton: false
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
refresh: []
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const { success, error: showError } = useToast()
|
|
||||||
|
|
||||||
// 状态
|
|
||||||
const loading = ref(false)
|
|
||||||
const submitting = ref(false)
|
|
||||||
const togglingId = ref<string | null>(null)
|
|
||||||
const mappings = ref<ModelAlias[]>([])
|
|
||||||
const availableModels = ref<GlobalModelResponse[]>([])
|
|
||||||
const dialogOpen = ref(false)
|
|
||||||
const editingAlias = ref<ModelAlias | null>(null)
|
|
||||||
|
|
||||||
// 固定的 Provider 选项(传递给 AliasDialog)
|
|
||||||
const fixedProviderOption = computed(() => ({
|
|
||||||
id: props.provider.id,
|
|
||||||
name: props.provider.name,
|
|
||||||
display_name: props.provider.display_name
|
|
||||||
}))
|
|
||||||
|
|
||||||
// 加载映射 (实际返回的是该 Provider 的别名列表)
|
|
||||||
async function loadMappings() {
|
|
||||||
try {
|
|
||||||
loading.value = true
|
|
||||||
mappings.value = await getAliases({ provider_id: props.provider.id })
|
|
||||||
} catch (err: any) {
|
|
||||||
showError(err.response?.data?.detail || '加载失败', '错误')
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载可用的 GlobalModel 列表
|
|
||||||
async function loadAvailableModels() {
|
|
||||||
try {
|
|
||||||
const response = await listGlobalModels({ limit: 1000, is_active: true })
|
|
||||||
availableModels.value = response.models || []
|
|
||||||
} catch (err: any) {
|
|
||||||
showError(err.response?.data?.detail || '加载模型列表失败', '错误')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 打开创建对话框
|
|
||||||
function openCreateDialog() {
|
|
||||||
editingAlias.value = null
|
|
||||||
dialogOpen.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 打开编辑对话框
|
|
||||||
function openEditDialog(alias: ModelAlias) {
|
|
||||||
editingAlias.value = alias
|
|
||||||
dialogOpen.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理对话框可见性变化
|
|
||||||
function handleDialogVisibility(value: boolean) {
|
|
||||||
dialogOpen.value = value
|
|
||||||
if (!value) {
|
|
||||||
editingAlias.value = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理别名提交(来自 AliasDialog 组件)
|
|
||||||
async function handleAliasSubmit(data: CreateModelAliasRequest | UpdateModelAliasRequest, isEdit: boolean) {
|
|
||||||
submitting.value = true
|
|
||||||
try {
|
|
||||||
if (isEdit && editingAlias.value) {
|
|
||||||
// 更新
|
|
||||||
await updateAlias(editingAlias.value.id, data as UpdateModelAliasRequest)
|
|
||||||
success(data.mapping_type === 'mapping' ? '映射已更新' : '别名已更新')
|
|
||||||
} else {
|
|
||||||
// 创建 - 确保 provider_id 设置为当前 Provider
|
|
||||||
const createData = data as CreateModelAliasRequest
|
|
||||||
createData.provider_id = props.provider.id
|
|
||||||
await createAlias(createData)
|
|
||||||
success(data.mapping_type === 'mapping' ? '映射已创建' : '别名已创建')
|
|
||||||
}
|
|
||||||
dialogOpen.value = false
|
|
||||||
editingAlias.value = null
|
|
||||||
await loadMappings()
|
|
||||||
emit('refresh')
|
|
||||||
} catch (err: any) {
|
|
||||||
const detail = err.response?.data?.detail || err.message
|
|
||||||
let errorMessage = detail
|
|
||||||
if (detail === '映射已存在') {
|
|
||||||
errorMessage = '该名称已存在,请使用其他名称'
|
|
||||||
}
|
|
||||||
showError(errorMessage, isEdit ? '更新失败' : '创建失败')
|
|
||||||
} finally {
|
|
||||||
submitting.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 切换启用状态
|
|
||||||
async function toggleActive(alias: ModelAlias) {
|
|
||||||
if (togglingId.value) return
|
|
||||||
|
|
||||||
togglingId.value = alias.id
|
|
||||||
try {
|
|
||||||
const newStatus = !alias.is_active
|
|
||||||
await updateAlias(alias.id, { is_active: newStatus })
|
|
||||||
alias.is_active = newStatus
|
|
||||||
} catch (err: any) {
|
|
||||||
showError(err.response?.data?.detail || '操作失败', '错误')
|
|
||||||
} finally {
|
|
||||||
togglingId.value = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 确认删除
|
|
||||||
async function confirmDelete(alias: ModelAlias) {
|
|
||||||
const typeName = alias.mapping_type === 'mapping' ? '映射' : '别名'
|
|
||||||
if (!confirm(`确定要删除${typeName} "${alias.alias}" 吗?`)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await deleteAlias(alias.id)
|
|
||||||
success(`${typeName}已删除`)
|
|
||||||
await loadMappings()
|
|
||||||
emit('refresh')
|
|
||||||
} catch (err: any) {
|
|
||||||
showError(err.response?.data?.detail || err.message, '删除失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
loadMappings()
|
|
||||||
loadAvailableModels()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@@ -165,6 +165,15 @@
|
|||||||
>
|
>
|
||||||
<Edit class="w-3.5 h-3.5" />
|
<Edit class="w-3.5 h-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
class="h-8 w-8"
|
||||||
|
title="管理别名"
|
||||||
|
@click="openAliasDialog(model)"
|
||||||
|
>
|
||||||
|
<Tag class="w-3.5 h-3.5" />
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
@@ -209,7 +218,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { Box, Edit, Trash2, Layers, Eye, Wrench, Zap, Brain, Power, Copy, Image } from 'lucide-vue-next'
|
import { Box, Edit, Trash2, Layers, Eye, Wrench, Zap, Brain, Power, Copy, Image, Tag } from 'lucide-vue-next'
|
||||||
import Card from '@/components/ui/card.vue'
|
import Card from '@/components/ui/card.vue'
|
||||||
import Button from '@/components/ui/button.vue'
|
import Button from '@/components/ui/button.vue'
|
||||||
import { useToast } from '@/composables/useToast'
|
import { useToast } from '@/composables/useToast'
|
||||||
@@ -224,6 +233,7 @@ const emit = defineEmits<{
|
|||||||
'editModel': [model: Model]
|
'editModel': [model: Model]
|
||||||
'deleteModel': [model: Model]
|
'deleteModel': [model: Model]
|
||||||
'batchAssign': []
|
'batchAssign': []
|
||||||
|
'manageAlias': [model: Model]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const { error: showError, success: showSuccess } = useToast()
|
const { error: showError, success: showSuccess } = useToast()
|
||||||
@@ -363,6 +373,11 @@ function openBatchAssignDialog() {
|
|||||||
emit('batchAssign')
|
emit('batchAssign')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 打开别名管理对话框
|
||||||
|
function openAliasDialog(model: Model) {
|
||||||
|
emit('manageAlias', model)
|
||||||
|
}
|
||||||
|
|
||||||
// 切换模型启用状态
|
// 切换模型启用状态
|
||||||
async function toggleModelActive(model: Model) {
|
async function toggleModelActive(model: Model) {
|
||||||
if (togglingModelId.value) return
|
if (togglingModelId.value) return
|
||||||
|
|||||||
@@ -313,7 +313,6 @@ import {
|
|||||||
Gauge,
|
Gauge,
|
||||||
Layers,
|
Layers,
|
||||||
FolderTree,
|
FolderTree,
|
||||||
Tag,
|
|
||||||
Box,
|
Box,
|
||||||
LogOut,
|
LogOut,
|
||||||
SunMoon,
|
SunMoon,
|
||||||
@@ -411,7 +410,6 @@ const navigation = computed(() => {
|
|||||||
{ name: '用户管理', href: '/admin/users', icon: Users },
|
{ name: '用户管理', href: '/admin/users', icon: Users },
|
||||||
{ name: '提供商', href: '/admin/providers', icon: FolderTree },
|
{ name: '提供商', href: '/admin/providers', icon: FolderTree },
|
||||||
{ name: '模型管理', href: '/admin/models', icon: Layers },
|
{ name: '模型管理', href: '/admin/models', icon: Layers },
|
||||||
{ name: '别名映射', href: '/admin/aliases', icon: Tag },
|
|
||||||
{ name: '独立密钥', href: '/admin/keys', icon: Key },
|
{ name: '独立密钥', href: '/admin/keys', icon: Key },
|
||||||
{ name: '使用记录', href: '/admin/usage', icon: BarChart3 },
|
{ name: '使用记录', href: '/admin/usage', icon: BarChart3 },
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -91,11 +91,6 @@ const routes: RouteRecordRaw[] = [
|
|||||||
name: 'ModelManagement',
|
name: 'ModelManagement',
|
||||||
component: () => importWithRetry(() => import('@/views/admin/ModelManagement.vue'))
|
component: () => importWithRetry(() => import('@/views/admin/ModelManagement.vue'))
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: 'aliases',
|
|
||||||
name: 'AliasManagement',
|
|
||||||
component: () => importWithRetry(() => import('@/views/admin/AliasManagement.vue'))
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: 'health-monitor',
|
path: 'health-monitor',
|
||||||
name: 'HealthMonitor',
|
name: 'HealthMonitor',
|
||||||
|
|||||||
@@ -425,25 +425,12 @@
|
|||||||
@success="handleModelFormSuccess"
|
@success="handleModelFormSuccess"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 创建/编辑别名/映射对话框 -->
|
|
||||||
<AliasDialog
|
|
||||||
:open="createAliasDialogOpen"
|
|
||||||
:editing-alias="editingAlias"
|
|
||||||
:global-models="globalModels"
|
|
||||||
:providers="providers"
|
|
||||||
:fixed-target-model="isTargetModelFixed ? selectedModel : null"
|
|
||||||
@update:open="handleAliasDialogUpdate"
|
|
||||||
@submit="handleAliasSubmit"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 模型详情抽屉 -->
|
<!-- 模型详情抽屉 -->
|
||||||
<ModelDetailDrawer
|
<ModelDetailDrawer
|
||||||
:model="selectedModel"
|
:model="selectedModel"
|
||||||
:open="!!selectedModel"
|
:open="!!selectedModel"
|
||||||
:providers="selectedModelProviders"
|
:providers="selectedModelProviders"
|
||||||
:aliases="selectedModelAliases"
|
|
||||||
:loading-providers="loadingModelProviders"
|
:loading-providers="loadingModelProviders"
|
||||||
:loading-aliases="loadingModelAliases"
|
|
||||||
:has-blocking-dialog-open="hasBlockingDialogOpen"
|
:has-blocking-dialog-open="hasBlockingDialogOpen"
|
||||||
:capabilities="capabilities"
|
:capabilities="capabilities"
|
||||||
@update:open="handleDrawerOpenChange"
|
@update:open="handleDrawerOpenChange"
|
||||||
@@ -454,11 +441,6 @@
|
|||||||
@delete-provider="confirmDeleteProviderImplementation"
|
@delete-provider="confirmDeleteProviderImplementation"
|
||||||
@toggle-provider-status="toggleProviderStatus"
|
@toggle-provider-status="toggleProviderStatus"
|
||||||
@refresh-providers="refreshSelectedModelProviders"
|
@refresh-providers="refreshSelectedModelProviders"
|
||||||
@add-alias="openAddAliasDialog"
|
|
||||||
@edit-alias="openEditAliasDialog"
|
|
||||||
@toggle-alias-status="toggleAliasStatusFromDrawer"
|
|
||||||
@delete-alias="confirmDeleteAliasFromDrawer"
|
|
||||||
@refresh-aliases="refreshSelectedModelAliases"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 批量添加关联提供商对话框 -->
|
<!-- 批量添加关联提供商对话框 -->
|
||||||
@@ -736,9 +718,7 @@ import {
|
|||||||
} from 'lucide-vue-next'
|
} from 'lucide-vue-next'
|
||||||
import ModelDetailDrawer from '@/features/models/components/ModelDetailDrawer.vue'
|
import ModelDetailDrawer from '@/features/models/components/ModelDetailDrawer.vue'
|
||||||
import GlobalModelFormDialog from '@/features/models/components/GlobalModelFormDialog.vue'
|
import GlobalModelFormDialog from '@/features/models/components/GlobalModelFormDialog.vue'
|
||||||
import AliasDialog from '@/features/models/components/AliasDialog.vue'
|
|
||||||
import ProviderModelFormDialog from '@/features/providers/components/ProviderModelFormDialog.vue'
|
import ProviderModelFormDialog from '@/features/providers/components/ProviderModelFormDialog.vue'
|
||||||
import type { CreateModelAliasRequest, UpdateModelAliasRequest } from '@/api/endpoints/aliases'
|
|
||||||
import type { Model } from '@/api/endpoints'
|
import type { Model } from '@/api/endpoints'
|
||||||
import { useToast } from '@/composables/useToast'
|
import { useToast } from '@/composables/useToast'
|
||||||
import { useConfirm } from '@/composables/useConfirm'
|
import { useConfirm } from '@/composables/useConfirm'
|
||||||
@@ -768,13 +748,6 @@ import {
|
|||||||
type GlobalModelResponse,
|
type GlobalModelResponse,
|
||||||
} from '@/api/global-models'
|
} from '@/api/global-models'
|
||||||
import { log } from '@/utils/logger'
|
import { log } from '@/utils/logger'
|
||||||
import {
|
|
||||||
getAliases,
|
|
||||||
createAlias,
|
|
||||||
updateAlias,
|
|
||||||
deleteAlias,
|
|
||||||
type ModelAlias,
|
|
||||||
} from '@/api/endpoints/aliases'
|
|
||||||
import { getProvidersSummary } from '@/api/endpoints/providers'
|
import { getProvidersSummary } from '@/api/endpoints/providers'
|
||||||
import { getAllCapabilities, type CapabilityDefinition } from '@/api/endpoints'
|
import { getAllCapabilities, type CapabilityDefinition } from '@/api/endpoints'
|
||||||
|
|
||||||
@@ -788,13 +761,9 @@ const searchQuery = ref('')
|
|||||||
const selectedModel = ref<GlobalModelResponse | null>(null)
|
const selectedModel = ref<GlobalModelResponse | null>(null)
|
||||||
const createModelDialogOpen = ref(false)
|
const createModelDialogOpen = ref(false)
|
||||||
const editingModel = ref<GlobalModelResponse | null>(null)
|
const editingModel = ref<GlobalModelResponse | null>(null)
|
||||||
const createAliasDialogOpen = ref(false)
|
|
||||||
const editingAliasId = ref<string | null>(null)
|
|
||||||
const isTargetModelFixed = ref(false) // 目标模型是否固定(从模型详情抽屉打开时为 true)
|
|
||||||
|
|
||||||
// 数据
|
// 数据
|
||||||
const globalModels = ref<GlobalModelResponse[]>([])
|
const globalModels = ref<GlobalModelResponse[]>([])
|
||||||
const allAliases = ref<ModelAlias[]>([])
|
|
||||||
const providers = ref<any[]>([])
|
const providers = ref<any[]>([])
|
||||||
const capabilities = ref<CapabilityDefinition[]>([])
|
const capabilities = ref<CapabilityDefinition[]>([])
|
||||||
|
|
||||||
@@ -804,9 +773,7 @@ const catalogPageSize = ref(20)
|
|||||||
|
|
||||||
// 选中模型的详细数据
|
// 选中模型的详细数据
|
||||||
const selectedModelProviders = ref<any[]>([])
|
const selectedModelProviders = ref<any[]>([])
|
||||||
const selectedModelAliases = ref<ModelAlias[]>([])
|
|
||||||
const loadingModelProviders = ref(false)
|
const loadingModelProviders = ref(false)
|
||||||
const loadingModelAliases = ref(false)
|
|
||||||
|
|
||||||
// 批量添加关联提供商
|
// 批量添加关联提供商
|
||||||
const batchAddProvidersDialogOpen = ref(false)
|
const batchAddProvidersDialogOpen = ref(false)
|
||||||
@@ -876,19 +843,10 @@ function hasTieredPricing(model: GlobalModelResponse): boolean {
|
|||||||
// 检测是否有对话框打开(防止误关闭抽屉)
|
// 检测是否有对话框打开(防止误关闭抽屉)
|
||||||
const hasBlockingDialogOpen = computed(() =>
|
const hasBlockingDialogOpen = computed(() =>
|
||||||
createModelDialogOpen.value ||
|
createModelDialogOpen.value ||
|
||||||
createAliasDialogOpen.value ||
|
|
||||||
batchAddProvidersDialogOpen.value ||
|
batchAddProvidersDialogOpen.value ||
|
||||||
editProviderDialogOpen.value
|
editProviderDialogOpen.value
|
||||||
)
|
)
|
||||||
|
|
||||||
// 编辑中的别名对象(用于传递给 AliasDialog)
|
|
||||||
const editingAlias = computed(() => {
|
|
||||||
if (!editingAliasId.value) return null
|
|
||||||
return allAliases.value.find(a => a.id === editingAliasId.value) ||
|
|
||||||
selectedModelAliases.value.find(a => a.id === editingAliasId.value) ||
|
|
||||||
null
|
|
||||||
})
|
|
||||||
|
|
||||||
// 能力筛选
|
// 能力筛选
|
||||||
const capabilityFilters = ref({
|
const capabilityFilters = ref({
|
||||||
streaming: false,
|
streaming: false,
|
||||||
@@ -1131,11 +1089,8 @@ async function selectModel(model: GlobalModelResponse) {
|
|||||||
selectedModel.value = model
|
selectedModel.value = model
|
||||||
detailTab.value = 'basic'
|
detailTab.value = 'basic'
|
||||||
|
|
||||||
// 加载该模型的关联提供商和别名
|
// 加载该模型的关联提供商
|
||||||
await Promise.all([
|
await loadModelProviders(model.id)
|
||||||
loadModelProviders(model.id),
|
|
||||||
loadModelAliases(model.id)
|
|
||||||
])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载指定模型的关联提供商
|
// 加载指定模型的关联提供商
|
||||||
@@ -1187,27 +1142,6 @@ async function loadModelProviders(_globalModelId: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载指定模型的别名
|
|
||||||
async function loadModelAliases(globalModelId: string) {
|
|
||||||
loadingModelAliases.value = true
|
|
||||||
try {
|
|
||||||
const aliases = await getAliases({ limit: 1000 })
|
|
||||||
selectedModelAliases.value = aliases.filter(a => a.global_model_id === globalModelId)
|
|
||||||
} catch (err: any) {
|
|
||||||
log.error('加载别名失败:', err)
|
|
||||||
selectedModelAliases.value = []
|
|
||||||
} finally {
|
|
||||||
loadingModelAliases.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 刷新当前选中模型的别名
|
|
||||||
async function refreshSelectedModelAliases() {
|
|
||||||
if (selectedModel.value) {
|
|
||||||
await loadModelAliases(selectedModel.value.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 刷新当前选中模型的关联提供商
|
// 刷新当前选中模型的关联提供商
|
||||||
async function refreshSelectedModelProviders() {
|
async function refreshSelectedModelProviders() {
|
||||||
if (selectedModel.value) {
|
if (selectedModel.value) {
|
||||||
@@ -1329,14 +1263,6 @@ async function confirmDeleteProviderImplementation(provider: any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 打开添加别名对话框(从模型详情抽屉)
|
|
||||||
function openAddAliasDialog() {
|
|
||||||
if (!selectedModel.value) return
|
|
||||||
editingAliasId.value = null
|
|
||||||
isTargetModelFixed.value = true // 目标模型固定为当前选中模型
|
|
||||||
createAliasDialogOpen.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function openCreateModelDialog() {
|
function openCreateModelDialog() {
|
||||||
editingModel.value = null
|
editingModel.value = null
|
||||||
createModelDialogOpen.value = true
|
createModelDialogOpen.value = true
|
||||||
@@ -1391,106 +1317,6 @@ async function toggleModelStatus(model: GlobalModelResponse) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadAliases() {
|
|
||||||
try {
|
|
||||||
allAliases.value = await getAliases({ limit: 1000 })
|
|
||||||
} catch (err: any) {
|
|
||||||
showError(err.response?.data?.detail || err.message, '加载别名失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function openEditAliasDialog(alias: ModelAlias) {
|
|
||||||
editingAliasId.value = alias.id
|
|
||||||
isTargetModelFixed.value = false
|
|
||||||
createAliasDialogOpen.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理别名对话框关闭事件
|
|
||||||
function handleAliasDialogUpdate(value: boolean) {
|
|
||||||
createAliasDialogOpen.value = value
|
|
||||||
if (!value) {
|
|
||||||
editingAliasId.value = null
|
|
||||||
isTargetModelFixed.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理别名提交(来自 AliasDialog 组件)
|
|
||||||
async function handleAliasSubmit(data: CreateModelAliasRequest | UpdateModelAliasRequest, isEdit: boolean) {
|
|
||||||
submitting.value = true
|
|
||||||
try {
|
|
||||||
if (isEdit && editingAliasId.value) {
|
|
||||||
// 更新
|
|
||||||
await updateAlias(editingAliasId.value, data as UpdateModelAliasRequest)
|
|
||||||
success(data.mapping_type === 'mapping' ? '映射已更新' : '别名已更新')
|
|
||||||
} else {
|
|
||||||
// 创建
|
|
||||||
await createAlias(data as CreateModelAliasRequest)
|
|
||||||
success(data.mapping_type === 'mapping' ? '映射已创建' : '别名已创建')
|
|
||||||
}
|
|
||||||
createAliasDialogOpen.value = false
|
|
||||||
editingAliasId.value = null
|
|
||||||
isTargetModelFixed.value = false
|
|
||||||
|
|
||||||
// 刷新数据
|
|
||||||
await loadAliases()
|
|
||||||
if (selectedModel.value) {
|
|
||||||
await loadModelAliases(selectedModel.value.id)
|
|
||||||
}
|
|
||||||
// 刷新外层模型列表以更新 alias_count
|
|
||||||
await loadGlobalModels()
|
|
||||||
} catch (err: any) {
|
|
||||||
const detail = err.response?.data?.detail || err.message
|
|
||||||
// 优化错误提示文案
|
|
||||||
let errorMessage = detail
|
|
||||||
if (detail === '映射已存在') {
|
|
||||||
errorMessage = '目标作用域已存在同名别名,请先删除冲突的映射或选择其他作用域'
|
|
||||||
}
|
|
||||||
showError(errorMessage, isEdit ? '更新失败' : '创建失败')
|
|
||||||
} finally {
|
|
||||||
submitting.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function confirmDeleteAlias(alias: ModelAlias) {
|
|
||||||
const confirmed = await confirmDanger(
|
|
||||||
`确定要删除别名 "${alias.alias}" 吗?`,
|
|
||||||
'删除别名'
|
|
||||||
)
|
|
||||||
if (!confirmed) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
await deleteAlias(alias.id)
|
|
||||||
success('别名已删除')
|
|
||||||
await loadAliases()
|
|
||||||
// 刷新外层模型列表以更新 alias_count
|
|
||||||
await loadGlobalModels()
|
|
||||||
} catch (err: any) {
|
|
||||||
showError(err.response?.data?.detail || err.message, '删除失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function toggleAliasStatus(alias: ModelAlias) {
|
|
||||||
try {
|
|
||||||
await updateAlias(alias.id, { is_active: !alias.is_active })
|
|
||||||
alias.is_active = !alias.is_active
|
|
||||||
success(alias.is_active ? '别名已启用' : '别名已停用')
|
|
||||||
} catch (err: any) {
|
|
||||||
showError(err.response?.data?.detail || err.message, '操作失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从抽屉中切换别名状态
|
|
||||||
async function toggleAliasStatusFromDrawer(alias: ModelAlias) {
|
|
||||||
await toggleAliasStatus(alias)
|
|
||||||
await refreshSelectedModelAliases()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从抽屉中删除别名
|
|
||||||
async function confirmDeleteAliasFromDrawer(alias: ModelAlias) {
|
|
||||||
await confirmDeleteAlias(alias)
|
|
||||||
await refreshSelectedModelAliases()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function refreshData() {
|
async function refreshData() {
|
||||||
await loadGlobalModels()
|
await loadGlobalModels()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user