mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-08 10:27:46 +08:00
feat: scope model mappings by endpoint
This commit is contained in:
@@ -33,19 +33,19 @@
|
||||
>
|
||||
<div
|
||||
v-for="group in aliasGroups"
|
||||
:key="`${group.model.id}-${group.apiFormatsKey}`"
|
||||
:key="getAliasGroupKey(group)"
|
||||
class="transition-colors"
|
||||
>
|
||||
<!-- 分组头部(可点击展开) -->
|
||||
<div
|
||||
class="flex items-center justify-between px-4 py-3 hover:bg-muted/20 cursor-pointer"
|
||||
@click="toggleAliasGroupExpand(`${group.model.id}-${group.apiFormatsKey}`)"
|
||||
@click="toggleAliasGroupExpand(getAliasGroupKey(group))"
|
||||
>
|
||||
<div class="flex items-center gap-2 flex-1 min-w-0">
|
||||
<!-- 展开/收起图标 -->
|
||||
<ChevronRight
|
||||
class="w-4 h-4 text-muted-foreground shrink-0 transition-transform"
|
||||
:class="{ 'rotate-90': expandedAliasGroups.has(`${group.model.id}-${group.apiFormatsKey}`) }"
|
||||
:class="{ 'rotate-90': expandedAliasGroups.has(getAliasGroupKey(group)) }"
|
||||
/>
|
||||
<!-- 模型名称 -->
|
||||
<span class="font-semibold text-sm truncate">
|
||||
@@ -69,6 +69,12 @@
|
||||
>
|
||||
{{ formatApiFormat(format) }}
|
||||
</Badge>
|
||||
<Badge
|
||||
variant="outline"
|
||||
class="text-xs"
|
||||
>
|
||||
{{ getEndpointScopeLabel(group) }}
|
||||
</Badge>
|
||||
</div>
|
||||
<!-- 映射数量 -->
|
||||
<span class="text-xs text-muted-foreground shrink-0">
|
||||
@@ -103,7 +109,7 @@
|
||||
|
||||
<!-- 展开的映射列表 -->
|
||||
<div
|
||||
v-show="expandedAliasGroups.has(`${group.model.id}-${group.apiFormatsKey}`)"
|
||||
v-show="expandedAliasGroups.has(getAliasGroupKey(group))"
|
||||
class="bg-muted/30 border-t border-border/30"
|
||||
>
|
||||
<div class="px-4 py-2 space-y-1">
|
||||
@@ -128,11 +134,11 @@
|
||||
size="icon"
|
||||
class="h-7 w-7 shrink-0"
|
||||
title="测试映射"
|
||||
:disabled="testingMapping === `${group.model.id}-${group.apiFormatsKey}-${mapping.name}`"
|
||||
:disabled="testingMapping === `${getAliasGroupKey(group)}-${mapping.name}`"
|
||||
@click="testMapping(group, mapping)"
|
||||
>
|
||||
<Loader2
|
||||
v-if="testingMapping === `${group.model.id}-${group.apiFormatsKey}-${mapping.name}`"
|
||||
v-if="testingMapping === `${getAliasGroupKey(group)}-${mapping.name}`"
|
||||
class="w-3 h-3 animate-spin"
|
||||
/>
|
||||
<Play
|
||||
@@ -239,8 +245,36 @@ const providerApiFormats = computed(() => {
|
||||
|
||||
// 生成作用域唯一键
|
||||
function getApiFormatsKey(formats: string[] | undefined): string {
|
||||
if (!formats || formats.length === 0) return ''
|
||||
return [...formats].sort().join(',')
|
||||
return getScopeKey(formats)
|
||||
}
|
||||
|
||||
function normalizeStringList(values: string[] | undefined): string[] {
|
||||
const seen = new Set<string>()
|
||||
const result: string[] = []
|
||||
for (const value of values ?? []) {
|
||||
const normalized = value.trim()
|
||||
if (!normalized || seen.has(normalized)) continue
|
||||
seen.add(normalized)
|
||||
result.push(normalized)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function getScopeKey(values: string[] | undefined): string {
|
||||
return normalizeStringList(values).sort().join(',')
|
||||
}
|
||||
|
||||
function getEndpointIdsKey(endpointIds: string[] | undefined): string {
|
||||
return getScopeKey(endpointIds)
|
||||
}
|
||||
|
||||
function getAliasGroupKey(group: AliasGroup): string {
|
||||
return `${group.model.id}-${group.apiFormatsKey}-${group.endpointIdsKey}`
|
||||
}
|
||||
|
||||
function getEndpointScopeLabel(group: AliasGroup): string {
|
||||
if (!group.endpointIds || group.endpointIds.length === 0) return '全部端点'
|
||||
return `${group.endpointIds.length} 端点`
|
||||
}
|
||||
|
||||
// 按"模型+作用域"分组的映射列表
|
||||
@@ -253,13 +287,16 @@ const aliasGroups = computed<AliasGroup[]>(() => {
|
||||
|
||||
for (const alias of model.provider_model_mappings) {
|
||||
const apiFormatsKey = getApiFormatsKey(alias.api_formats)
|
||||
const groupKey = `${model.id}|${apiFormatsKey}`
|
||||
const endpointIdsKey = getEndpointIdsKey(alias.endpoint_ids)
|
||||
const groupKey = `${model.id}|${apiFormatsKey}|${endpointIdsKey}`
|
||||
|
||||
if (!groupMap.has(groupKey)) {
|
||||
const group: AliasGroup = {
|
||||
model,
|
||||
apiFormatsKey,
|
||||
apiFormats: alias.api_formats || [],
|
||||
endpointIdsKey,
|
||||
endpointIds: normalizeStringList(alias.endpoint_ids),
|
||||
aliases: []
|
||||
}
|
||||
groupMap.set(groupKey, group)
|
||||
@@ -278,6 +315,7 @@ const aliasGroups = computed<AliasGroup[]>(() => {
|
||||
const nameB = (b.model.global_model_display_name || b.model.provider_model_name || '').toLowerCase()
|
||||
if (nameA !== nameB) return nameA.localeCompare(nameB)
|
||||
return a.apiFormatsKey.localeCompare(b.apiFormatsKey)
|
||||
|| a.endpointIdsKey.localeCompare(b.endpointIdsKey)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -299,8 +337,9 @@ const deleteConfirmDescription = computed(() => {
|
||||
const { model, aliases, apiFormats } = deletingGroup.value
|
||||
const modelName = model.global_model_display_name || model.provider_model_name
|
||||
const scopeText = apiFormats.length === 0 ? '全部' : apiFormats.map(f => formatApiFormat(f)).join(', ')
|
||||
const endpointScope = getEndpointScopeLabel(deletingGroup.value)
|
||||
const aliasNames = aliases.map(a => a.name).join(', ')
|
||||
return `确定要删除模型「${modelName}」在作用域「${scopeText}」下的 ${aliases.length} 个映射吗?\n\n映射名称:${aliasNames}`
|
||||
return `确定要删除模型「${modelName}」在作用域「${scopeText} / ${endpointScope}」下的 ${aliases.length} 个映射吗?\n\n映射名称:${aliasNames}`
|
||||
})
|
||||
|
||||
// 切换映射组展开状态
|
||||
@@ -343,14 +382,15 @@ function deleteGroup(group: AliasGroup) {
|
||||
async function confirmDelete() {
|
||||
if (!deletingGroup.value) return
|
||||
|
||||
const { model, aliases, apiFormatsKey } = deletingGroup.value
|
||||
const { model, aliases, apiFormatsKey, endpointIdsKey } = deletingGroup.value
|
||||
|
||||
try {
|
||||
const currentAliases = model.provider_model_mappings || []
|
||||
const aliasNamesToRemove = new Set(aliases.map(a => a.name))
|
||||
const newAliases = currentAliases.filter((a: ProviderModelAlias) => {
|
||||
const currentKey = getApiFormatsKey(a.api_formats)
|
||||
return !(currentKey === apiFormatsKey && aliasNamesToRemove.has(a.name))
|
||||
const currentEndpointIdsKey = getEndpointIdsKey(a.endpoint_ids)
|
||||
return !(currentKey === apiFormatsKey && currentEndpointIdsKey === endpointIdsKey && aliasNamesToRemove.has(a.name))
|
||||
})
|
||||
|
||||
await updateModel(props.provider.id, model.id, {
|
||||
@@ -375,7 +415,7 @@ async function onDialogSaved() {
|
||||
|
||||
// 测试模型映射
|
||||
async function testMapping(group: AliasGroup, mapping: ProviderModelAlias) {
|
||||
const testingKey = `${group.model.id}-${group.apiFormatsKey}-${mapping.name}`
|
||||
const testingKey = `${getAliasGroupKey(group)}-${mapping.name}`
|
||||
testingMapping.value = testingKey
|
||||
|
||||
try {
|
||||
|
||||
@@ -72,6 +72,13 @@
|
||||
<span class="text-xs text-muted-foreground shrink-0">
|
||||
| {{ item.mappings.length }} 个映射
|
||||
</span>
|
||||
<Badge
|
||||
v-if="item.group"
|
||||
variant="outline"
|
||||
class="text-xs shrink-0"
|
||||
>
|
||||
{{ getGroupEndpointScopeLabel(item.group) }}
|
||||
</Badge>
|
||||
</template>
|
||||
<!-- 正则映射 -->
|
||||
<template v-else>
|
||||
@@ -290,6 +297,7 @@
|
||||
v-model:open="dialogOpen"
|
||||
:provider-id="provider.id"
|
||||
:models="models"
|
||||
:endpoints="endpoints"
|
||||
:editing-group="editingGroup"
|
||||
:preselected-model-id="preselectedModelId"
|
||||
:has-auto-fetch-key="hasAutoFetchKey"
|
||||
@@ -446,8 +454,32 @@ const expandedItems = ref<Set<string>>(new Set())
|
||||
|
||||
// 生成作用域唯一键
|
||||
function getApiFormatsKey(formats: string[] | undefined): string {
|
||||
if (!formats || formats.length === 0) return ''
|
||||
return [...formats].sort().join(',')
|
||||
return getScopeKey(formats)
|
||||
}
|
||||
|
||||
function normalizeStringList(values: string[] | undefined): string[] {
|
||||
const seen = new Set<string>()
|
||||
const result: string[] = []
|
||||
for (const value of values ?? []) {
|
||||
const normalized = value.trim()
|
||||
if (!normalized || seen.has(normalized)) continue
|
||||
seen.add(normalized)
|
||||
result.push(normalized)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function getScopeKey(values: string[] | undefined): string {
|
||||
return normalizeStringList(values).sort().join(',')
|
||||
}
|
||||
|
||||
function getEndpointIdsKey(endpointIds: string[] | undefined): string {
|
||||
return getScopeKey(endpointIds)
|
||||
}
|
||||
|
||||
function getGroupEndpointScopeLabel(group: AliasGroup): string {
|
||||
if (!group.endpointIds || group.endpointIds.length === 0) return '全部端点'
|
||||
return `${group.endpointIds.length} 端点`
|
||||
}
|
||||
|
||||
// 精确映射分组(来自 provider_model_mappings)
|
||||
@@ -460,13 +492,16 @@ const exactMappingGroups = computed<AliasGroup[]>(() => {
|
||||
|
||||
for (const alias of model.provider_model_mappings) {
|
||||
const apiFormatsKey = getApiFormatsKey(alias.api_formats)
|
||||
const groupKey = `${model.id}|${apiFormatsKey}`
|
||||
const endpointIdsKey = getEndpointIdsKey(alias.endpoint_ids)
|
||||
const groupKey = `${model.id}|${apiFormatsKey}|${endpointIdsKey}`
|
||||
|
||||
if (!groupMap.has(groupKey)) {
|
||||
const group: AliasGroup = {
|
||||
model,
|
||||
apiFormatsKey,
|
||||
apiFormats: alias.api_formats || [],
|
||||
endpointIdsKey,
|
||||
endpointIds: normalizeStringList(alias.endpoint_ids),
|
||||
aliases: []
|
||||
}
|
||||
groupMap.set(groupKey, group)
|
||||
@@ -587,7 +622,8 @@ const deleteConfirmDescription = computed(() => {
|
||||
const { model, aliases } = deletingGroup.value
|
||||
const modelName = model.global_model_display_name || model.provider_model_name
|
||||
const aliasNames = aliases.map(a => a.name).join(', ')
|
||||
return `确定要删除模型「${modelName}」的 ${aliases.length} 个映射吗?\n\n映射名称:${aliasNames}`
|
||||
const endpointScope = getGroupEndpointScopeLabel(deletingGroup.value)
|
||||
return `确定要删除模型「${modelName}」在「${endpointScope}」下的 ${aliases.length} 个映射吗?\n\n映射名称:${aliasNames}`
|
||||
})
|
||||
|
||||
// 切换展开状态
|
||||
@@ -634,14 +670,15 @@ function deleteGroup(group: AliasGroup) {
|
||||
async function confirmDelete() {
|
||||
if (!deletingGroup.value) return
|
||||
|
||||
const { model, aliases, apiFormatsKey } = deletingGroup.value
|
||||
const { model, aliases, apiFormatsKey, endpointIdsKey } = deletingGroup.value
|
||||
|
||||
try {
|
||||
const currentAliases = model.provider_model_mappings || []
|
||||
const aliasNamesToRemove = new Set(aliases.map(a => a.name))
|
||||
const newAliases = currentAliases.filter((a: ProviderModelAlias) => {
|
||||
const currentKey = getApiFormatsKey(a.api_formats)
|
||||
return !(currentKey === apiFormatsKey && aliasNamesToRemove.has(a.name))
|
||||
const currentEndpointIdsKey = getEndpointIdsKey(a.endpoint_ids)
|
||||
return !(currentKey === apiFormatsKey && currentEndpointIdsKey === endpointIdsKey && aliasNamesToRemove.has(a.name))
|
||||
})
|
||||
|
||||
await updateModel(props.provider.id, model.id, {
|
||||
|
||||
Reference in New Issue
Block a user