mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-10 05:00:19 +08:00
feat(provider): auto-match batch assign models from key
This commit is contained in:
@@ -19,6 +19,25 @@
|
||||
class="pl-8 h-9"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
v-if="autoMatchKey"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="h-9 shrink-0"
|
||||
:disabled="loadingGlobalModels || fetchingAutoMatchedModels"
|
||||
:title="`按 ${autoMatchKeyLabel} 的上游模型自动勾选同名模型`"
|
||||
@click="applyAutoMatchFromKey(true)"
|
||||
>
|
||||
<Loader2
|
||||
v-if="fetchingAutoMatchedModels"
|
||||
class="w-3.5 h-3.5 mr-1.5 animate-spin"
|
||||
/>
|
||||
<ListChecks
|
||||
v-else
|
||||
class="w-3.5 h-3.5 mr-1.5"
|
||||
/>
|
||||
匹配勾选
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- 模型列表 -->
|
||||
@@ -126,13 +145,14 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { Layers, Loader2, Search, Check } from 'lucide-vue-next'
|
||||
import { Layers, Loader2, Search, Check, ListChecks } from 'lucide-vue-next'
|
||||
import Dialog from '@/components/ui/dialog/Dialog.vue'
|
||||
import Button from '@/components/ui/button.vue'
|
||||
import Input from '@/components/ui/input.vue'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { useConfirm } from '@/composables/useConfirm'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import { useUpstreamModelsCache } from '../composables/useUpstreamModelsCache'
|
||||
import {
|
||||
getGlobalModels,
|
||||
type GlobalModelResponse
|
||||
@@ -144,10 +164,17 @@ import {
|
||||
type Model
|
||||
} from '@/api/endpoints'
|
||||
|
||||
interface AutoMatchKey {
|
||||
id: string
|
||||
name?: string | null
|
||||
api_key_masked?: string | null
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean
|
||||
providerId: string
|
||||
providerName?: string
|
||||
autoMatchKey?: AutoMatchKey | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -155,12 +182,15 @@ const emit = defineEmits<{
|
||||
'changed': []
|
||||
}>()
|
||||
|
||||
const { error: showError, success } = useToast()
|
||||
const { error: showError, success, warning: showWarning } = useToast()
|
||||
const { confirmWarning } = useConfirm()
|
||||
const { fetchModels: fetchCachedModels } = useUpstreamModelsCache()
|
||||
|
||||
// 状态
|
||||
const loadingGlobalModels = ref(false)
|
||||
const saving = ref(false)
|
||||
const fetchingAutoMatchedModels = ref(false)
|
||||
const autoMatchedKeyId = ref<string | null>(null)
|
||||
|
||||
// 数据
|
||||
const allGlobalModels = ref<GlobalModelResponse[]>([])
|
||||
@@ -175,6 +205,13 @@ const initialGlobalModelIds = ref<Set<string>>(new Set())
|
||||
// 搜索状态
|
||||
const searchQuery = ref('')
|
||||
|
||||
const autoMatchKey = computed(() => props.autoMatchKey ?? null)
|
||||
const autoMatchKeyLabel = computed(() => {
|
||||
const key = autoMatchKey.value
|
||||
if (!key) return ''
|
||||
return key.name || key.api_key_masked || key.id.slice(0, 8)
|
||||
})
|
||||
|
||||
// 已关联的全局模型 ID 集合(从已有数据计算)
|
||||
const existingGlobalModelIds = computed(() => {
|
||||
return new Set(
|
||||
@@ -264,6 +301,73 @@ function toggleAllGlobalModels() {
|
||||
selectedGlobalModelIds.value = new Set(selectedGlobalModelIds.value)
|
||||
}
|
||||
|
||||
function normalizeModelName(name: string | null | undefined): string {
|
||||
return (name || '').trim()
|
||||
}
|
||||
|
||||
async function applyAutoMatchFromKey(forceRefresh = false) {
|
||||
const key = autoMatchKey.value
|
||||
if (!props.providerId || !key || fetchingAutoMatchedModels.value) return
|
||||
if (!forceRefresh && autoMatchedKeyId.value === key.id) return
|
||||
|
||||
fetchingAutoMatchedModels.value = true
|
||||
try {
|
||||
const result = await fetchCachedModels(props.providerId, key.id, forceRefresh)
|
||||
if (!props.open || autoMatchKey.value?.id !== key.id) return
|
||||
|
||||
if (result.error && result.models.length > 0) {
|
||||
showWarning(`部分格式获取失败: ${result.error}`)
|
||||
}
|
||||
|
||||
if (result.models.length === 0) {
|
||||
if (result.error) {
|
||||
showError(result.error, '获取上游模型失败')
|
||||
} else {
|
||||
showWarning('此 Key 未返回可用模型')
|
||||
}
|
||||
autoMatchedKeyId.value = key.id
|
||||
return
|
||||
}
|
||||
|
||||
const upstreamModelIds = new Set(
|
||||
result.models
|
||||
.map(model => normalizeModelName(model.id))
|
||||
.filter(Boolean)
|
||||
)
|
||||
const matchedGlobalModelIds = allGlobalModels.value
|
||||
.filter(model => upstreamModelIds.has(normalizeModelName(model.name)))
|
||||
.map(model => model.id)
|
||||
|
||||
autoMatchedKeyId.value = key.id
|
||||
|
||||
if (matchedGlobalModelIds.length === 0) {
|
||||
showWarning('未找到与此 Key 上游模型 ID 同名的全局模型')
|
||||
return
|
||||
}
|
||||
|
||||
const nextSelected = new Set(selectedGlobalModelIds.value)
|
||||
let newlySelectedCount = 0
|
||||
for (const id of matchedGlobalModelIds) {
|
||||
if (!nextSelected.has(id)) {
|
||||
newlySelectedCount++
|
||||
}
|
||||
nextSelected.add(id)
|
||||
}
|
||||
selectedGlobalModelIds.value = nextSelected
|
||||
searchQuery.value = ''
|
||||
|
||||
if (newlySelectedCount > 0) {
|
||||
success(`已按 ${autoMatchKeyLabel.value} 勾选 ${matchedGlobalModelIds.length} 个同名模型`)
|
||||
} else {
|
||||
success(`${matchedGlobalModelIds.length} 个同名模型已在选中列表中`)
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '自动匹配模型失败'), '错误')
|
||||
} finally {
|
||||
fetchingAutoMatchedModels.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 处理关闭
|
||||
async function handleClose() {
|
||||
if (hasChanges.value) {
|
||||
@@ -350,11 +454,17 @@ function syncGlobalModelSelection() {
|
||||
// 监听打开状态
|
||||
watch(() => props.open, async (isOpen) => {
|
||||
if (isOpen && props.providerId) {
|
||||
autoMatchedKeyId.value = null
|
||||
await loadData()
|
||||
if (autoMatchKey.value) {
|
||||
await applyAutoMatchFromKey(false)
|
||||
}
|
||||
} else {
|
||||
searchQuery.value = ''
|
||||
selectedGlobalModelIds.value = new Set()
|
||||
initialGlobalModelIds.value = new Set()
|
||||
fetchingAutoMatchedModels.value = false
|
||||
autoMatchedKeyId.value = null
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -467,6 +467,16 @@
|
||||
>
|
||||
<Shield class="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
v-if="key.auto_fetch_models"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7"
|
||||
title="按此 Key 自动勾选同名模型"
|
||||
@click="handleAutoMatchKeyModels(key)"
|
||||
>
|
||||
<ListChecks class="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
<!-- 代理节点配置 -->
|
||||
<Popover
|
||||
:open="proxyPopoverOpenKeyId === key.id"
|
||||
@@ -1323,7 +1333,8 @@
|
||||
:open="batchAssignDialogOpen"
|
||||
:provider-id="provider.id"
|
||||
:provider-name="provider.name"
|
||||
@update:open="batchAssignDialogOpen = $event"
|
||||
:auto-match-key="batchAssignAutoMatchKey"
|
||||
@update:open="handleBatchAssignDialogOpenUpdate"
|
||||
@changed="handleBatchAssignChanged"
|
||||
/>
|
||||
|
||||
@@ -1368,6 +1379,7 @@ import {
|
||||
ShieldX,
|
||||
Globe,
|
||||
GitBranch,
|
||||
ListChecks,
|
||||
} from 'lucide-vue-next'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import { useEscapeKey } from '@/composables/useEscapeKey'
|
||||
@@ -1464,6 +1476,12 @@ interface ProviderEndpointWithKeys extends ProviderEndpoint {
|
||||
rpm_limit?: number
|
||||
}
|
||||
|
||||
interface BatchAssignAutoMatchKey {
|
||||
id: string
|
||||
name?: string | null
|
||||
api_key_masked?: string | null
|
||||
}
|
||||
|
||||
interface Props {
|
||||
providerId: string | null
|
||||
open: boolean
|
||||
@@ -1530,6 +1548,7 @@ const revealedKeys = ref<Map<string, string>>(new Map())
|
||||
const modelFormDialogOpen = ref(false)
|
||||
const editingModel = ref<Model | null>(null)
|
||||
const batchAssignDialogOpen = ref(false)
|
||||
const batchAssignAutoMatchKey = ref<BatchAssignAutoMatchKey | null>(null)
|
||||
const modelMappingTabRef = ref<InstanceType<typeof ModelMappingTab> | null>(null)
|
||||
|
||||
// 密钥列表拖拽排序状态
|
||||
@@ -1738,6 +1757,7 @@ watch(
|
||||
oauthKeyEditDialogOpen.value = false
|
||||
deleteKeyConfirmOpen.value = false
|
||||
batchAssignDialogOpen.value = false
|
||||
batchAssignAutoMatchKey.value = null
|
||||
antigravityQuotaDialogOpen.value = false
|
||||
antigravityQuotaDialogKey.value = null
|
||||
|
||||
@@ -3088,9 +3108,26 @@ function handleEditModel(model: Model) {
|
||||
|
||||
// 处理打开批量关联对话框
|
||||
function handleBatchAssign() {
|
||||
batchAssignAutoMatchKey.value = null
|
||||
batchAssignDialogOpen.value = true
|
||||
}
|
||||
|
||||
function handleAutoMatchKeyModels(key: EndpointAPIKey) {
|
||||
batchAssignAutoMatchKey.value = {
|
||||
id: key.id,
|
||||
name: key.name,
|
||||
api_key_masked: key.api_key_masked,
|
||||
}
|
||||
batchAssignDialogOpen.value = true
|
||||
}
|
||||
|
||||
function handleBatchAssignDialogOpenUpdate(value: boolean) {
|
||||
batchAssignDialogOpen.value = value
|
||||
if (!value) {
|
||||
batchAssignAutoMatchKey.value = null
|
||||
}
|
||||
}
|
||||
|
||||
// 处理批量关联完成
|
||||
async function handleBatchAssignChanged() {
|
||||
await Promise.all([loadEndpoints(), loadMappingPreview()])
|
||||
|
||||
Reference in New Issue
Block a user