mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-10 13:10:21 +08:00
fix(provider): move key model auto-match into dialog
This commit is contained in:
@@ -19,25 +19,49 @@
|
||||
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>
|
||||
<DropdownMenu :modal="false">
|
||||
<DropdownMenuTrigger as-child>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-9 w-9 shrink-0"
|
||||
:disabled="loadingGlobalModels || loadingProviderKeys || fetchingAutoMatchedModels || providerKeys.length === 0"
|
||||
:title="autoMatchButtonTitle"
|
||||
aria-label="按密钥匹配"
|
||||
>
|
||||
<Loader2
|
||||
v-if="loadingProviderKeys || fetchingAutoMatchedModels"
|
||||
class="w-4 h-4 animate-spin"
|
||||
/>
|
||||
<ListChecks
|
||||
v-else
|
||||
class="w-4 h-4"
|
||||
/>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
class="w-72 max-h-80 overflow-y-auto"
|
||||
>
|
||||
<DropdownMenuItem
|
||||
v-for="key in providerKeys"
|
||||
:key="key.id"
|
||||
class="flex-col items-start gap-0.5"
|
||||
:disabled="fetchingAutoMatchedModels"
|
||||
@select="applyAutoMatchFromKey(key)"
|
||||
>
|
||||
<span class="w-full truncate font-medium">
|
||||
{{ getAutoMatchKeyLabel(key) }}
|
||||
</span>
|
||||
<span
|
||||
v-if="getAutoMatchKeyDetail(key)"
|
||||
class="w-full truncate text-xs text-muted-foreground"
|
||||
>
|
||||
{{ getAutoMatchKeyDetail(key) }}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
<!-- 模型列表 -->
|
||||
@@ -149,6 +173,12 @@ 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 {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
} from '@/components/ui'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { useConfirm } from '@/composables/useConfirm'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
@@ -159,42 +189,48 @@ import {
|
||||
} from '@/api/endpoints/global-models'
|
||||
import {
|
||||
getProviderModels,
|
||||
getProviderKeys,
|
||||
batchAssignModelsToProvider,
|
||||
deleteModel,
|
||||
type Model
|
||||
type Model,
|
||||
type EndpointAPIKey
|
||||
} from '@/api/endpoints'
|
||||
|
||||
interface AutoMatchKey {
|
||||
id: string
|
||||
name?: string | null
|
||||
api_key_masked?: string | null
|
||||
}
|
||||
type AutoMatchKey = Pick<EndpointAPIKey, 'id' | 'name' | 'api_key_masked'>
|
||||
|
||||
const props = defineProps<{
|
||||
interface Props {
|
||||
open: boolean
|
||||
providerId: string
|
||||
providerName?: string
|
||||
autoMatchKey?: AutoMatchKey | null
|
||||
}>()
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:open': [value: boolean]
|
||||
'changed': []
|
||||
}>()
|
||||
|
||||
interface AutoMatchKeyLike {
|
||||
id: string
|
||||
name?: string | null
|
||||
api_key_masked?: string | null
|
||||
}
|
||||
|
||||
const { error: showError, success, warning: showWarning } = useToast()
|
||||
const { confirmWarning } = useConfirm()
|
||||
const { fetchModels: fetchCachedModels } = useUpstreamModelsCache()
|
||||
|
||||
// 状态
|
||||
const loadingGlobalModels = ref(false)
|
||||
const loadingProviderKeys = ref(false)
|
||||
const saving = ref(false)
|
||||
const fetchingAutoMatchedModels = ref(false)
|
||||
const autoMatchedKeyId = ref<string | null>(null)
|
||||
|
||||
// 数据
|
||||
const allGlobalModels = ref<GlobalModelResponse[]>([])
|
||||
const existingModels = ref<Model[]>([])
|
||||
const providerKeys = ref<AutoMatchKey[]>([])
|
||||
|
||||
// 选择状态(本地状态,保存时才提交)
|
||||
const selectedGlobalModelIds = ref<Set<string>>(new Set())
|
||||
@@ -205,11 +241,10 @@ 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)
|
||||
const autoMatchButtonTitle = computed(() => {
|
||||
if (loadingProviderKeys.value) return '正在加载密钥'
|
||||
if (providerKeys.value.length === 0) return '暂无可用于匹配的密钥'
|
||||
return '选择密钥,并按该密钥的上游模型自动勾选同名模型'
|
||||
})
|
||||
|
||||
// 已关联的全局模型 ID 集合(从已有数据计算)
|
||||
@@ -305,15 +340,22 @@ function normalizeModelName(name: string | null | undefined): string {
|
||||
return (name || '').trim()
|
||||
}
|
||||
|
||||
async function applyAutoMatchFromKey(forceRefresh = false) {
|
||||
const key = autoMatchKey.value
|
||||
function getAutoMatchKeyLabel(key: AutoMatchKeyLike): string {
|
||||
return key.name || key.api_key_masked || key.id.slice(0, 8)
|
||||
}
|
||||
|
||||
function getAutoMatchKeyDetail(key: AutoMatchKeyLike): string {
|
||||
if (key.name && key.api_key_masked) return key.api_key_masked
|
||||
return key.name ? key.id.slice(0, 8) : ''
|
||||
}
|
||||
|
||||
async function applyAutoMatchFromKey(key: AutoMatchKey) {
|
||||
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
|
||||
const result = await fetchCachedModels(props.providerId, key.id, true)
|
||||
if (!props.open) return
|
||||
|
||||
if (result.error && result.models.length > 0) {
|
||||
showWarning(`部分格式获取失败: ${result.error}`)
|
||||
@@ -325,7 +367,6 @@ async function applyAutoMatchFromKey(forceRefresh = false) {
|
||||
} else {
|
||||
showWarning('此 Key 未返回可用模型')
|
||||
}
|
||||
autoMatchedKeyId.value = key.id
|
||||
return
|
||||
}
|
||||
|
||||
@@ -338,8 +379,6 @@ async function applyAutoMatchFromKey(forceRefresh = false) {
|
||||
.filter(model => upstreamModelIds.has(normalizeModelName(model.name)))
|
||||
.map(model => model.id)
|
||||
|
||||
autoMatchedKeyId.value = key.id
|
||||
|
||||
if (matchedGlobalModelIds.length === 0) {
|
||||
showWarning('未找到与此 Key 上游模型 ID 同名的全局模型')
|
||||
return
|
||||
@@ -357,7 +396,7 @@ async function applyAutoMatchFromKey(forceRefresh = false) {
|
||||
searchQuery.value = ''
|
||||
|
||||
if (newlySelectedCount > 0) {
|
||||
success(`已按 ${autoMatchKeyLabel.value} 勾选 ${matchedGlobalModelIds.length} 个同名模型`)
|
||||
success(`已按 ${getAutoMatchKeyLabel(key)} 勾选 ${matchedGlobalModelIds.length} 个同名模型`)
|
||||
} else {
|
||||
success(`${matchedGlobalModelIds.length} 个同名模型已在选中列表中`)
|
||||
}
|
||||
@@ -454,23 +493,19 @@ 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()
|
||||
providerKeys.value = []
|
||||
fetchingAutoMatchedModels.value = false
|
||||
autoMatchedKeyId.value = null
|
||||
}
|
||||
})
|
||||
|
||||
// 加载数据
|
||||
async function loadData() {
|
||||
await Promise.all([loadGlobalModels(), loadExistingModels()])
|
||||
await Promise.all([loadGlobalModels(), loadExistingModels(), loadProviderKeys()])
|
||||
syncGlobalModelSelection()
|
||||
}
|
||||
|
||||
@@ -495,4 +530,17 @@ async function loadExistingModels() {
|
||||
showError(parseApiError(err, '加载已关联模型失败'), '错误')
|
||||
}
|
||||
}
|
||||
|
||||
// 加载密钥列表
|
||||
async function loadProviderKeys() {
|
||||
try {
|
||||
loadingProviderKeys.value = true
|
||||
providerKeys.value = await getProviderKeys(props.providerId)
|
||||
} catch (err: unknown) {
|
||||
providerKeys.value = []
|
||||
showError(parseApiError(err, '加载密钥失败'), '错误')
|
||||
} finally {
|
||||
loadingProviderKeys.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -467,15 +467,6 @@
|
||||
>
|
||||
<Shield class="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
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"
|
||||
@@ -1332,7 +1323,6 @@
|
||||
:open="batchAssignDialogOpen"
|
||||
:provider-id="provider.id"
|
||||
:provider-name="provider.name"
|
||||
:auto-match-key="batchAssignAutoMatchKey"
|
||||
@update:open="handleBatchAssignDialogOpenUpdate"
|
||||
@changed="handleBatchAssignChanged"
|
||||
/>
|
||||
@@ -1378,7 +1368,6 @@ import {
|
||||
ShieldX,
|
||||
Globe,
|
||||
GitBranch,
|
||||
ListChecks,
|
||||
} from 'lucide-vue-next'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import { useEscapeKey } from '@/composables/useEscapeKey'
|
||||
@@ -1475,12 +1464,6 @@ 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
|
||||
@@ -1547,7 +1530,6 @@ 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)
|
||||
|
||||
// 密钥列表拖拽排序状态
|
||||
@@ -1756,7 +1738,6 @@ watch(
|
||||
oauthKeyEditDialogOpen.value = false
|
||||
deleteKeyConfirmOpen.value = false
|
||||
batchAssignDialogOpen.value = false
|
||||
batchAssignAutoMatchKey.value = null
|
||||
antigravityQuotaDialogOpen.value = false
|
||||
antigravityQuotaDialogKey.value = null
|
||||
|
||||
@@ -3107,24 +3088,11 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
// 处理批量关联完成
|
||||
|
||||
Reference in New Issue
Block a user