mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 01:40:21 +08:00
feat(ui): 增加模型连接性测试功能并优化对话框 ESC 键处理
- 模型测试: 新增测试按钮,支持多 API 格式选择 - ESC 键: 重构为全局栈管理,支持嵌套对话框 - 修复: check_endpoint 使用 FORMAT_ID 替代 name - 优化: Redis 连接池添加健康检查 Co-authored-by: htmambo <htmambo@users.noreply.github.com>
This commit is contained in:
@@ -374,6 +374,7 @@
|
||||
v-if="provider"
|
||||
:key="`models-${provider.id}`"
|
||||
:provider="provider"
|
||||
:endpoints="endpoints"
|
||||
@edit-model="handleEditModel"
|
||||
@delete-model="handleDeleteModel"
|
||||
@batch-assign="handleBatchAssign"
|
||||
|
||||
@@ -109,14 +109,42 @@
|
||||
${{ formatPrice(model.effective_price_per_request ?? model.price_per_request) }}/次
|
||||
</span>
|
||||
</template>
|
||||
<!-- 无计<EFBFBD><EFBFBD>配置 -->
|
||||
<!-- 无计费配置 -->
|
||||
<template v-if="!hasTokenPricing(model) && !hasRequestPricing(model)">
|
||||
<span class="text-muted-foreground">—</span>
|
||||
</template>
|
||||
</div>
|
||||
</td>
|
||||
<td class="align-top px-4 py-3">
|
||||
<div class="flex justify-center gap-1.5">
|
||||
<div class="flex justify-center gap-1">
|
||||
<!-- 测试按钮(支持多格式选择) -->
|
||||
<div class="relative">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
title="测试模型"
|
||||
:disabled="testingModelId === model.id"
|
||||
@click="handleTestClick(model)"
|
||||
>
|
||||
<Loader2 v-if="testingModelId === model.id" class="w-3.5 h-3.5 animate-spin" />
|
||||
<Play v-else class="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
<!-- 格式选择下拉菜单 -->
|
||||
<div
|
||||
v-if="formatMenuModelId === model.id && availableApiFormats.length > 1"
|
||||
class="absolute top-full left-0 mt-1 z-10 bg-popover border rounded-md shadow-md py-1 min-w-[120px]"
|
||||
>
|
||||
<button
|
||||
v-for="fmt in availableApiFormats"
|
||||
:key="fmt"
|
||||
class="w-full px-3 py-1.5 text-left text-sm hover:bg-muted transition-colors"
|
||||
@click="testModelConnection(model, fmt)"
|
||||
>
|
||||
{{ fmt }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -169,17 +197,26 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { Box, Edit, Trash2, Layers, Power, Copy } from 'lucide-vue-next'
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { Box, Edit, Trash2, Layers, Power, Copy, Loader2, Play } from 'lucide-vue-next'
|
||||
import Card from '@/components/ui/card.vue'
|
||||
import Button from '@/components/ui/button.vue'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { useClipboard } from '@/composables/useClipboard'
|
||||
import { getProviderModels, type Model } from '@/api/endpoints'
|
||||
import { getProviderModels, type Model, testModel } from '@/api/endpoints'
|
||||
import { updateModel } from '@/api/endpoints/models'
|
||||
import { parseTestModelError } from '@/utils/errorParser'
|
||||
|
||||
interface Endpoint {
|
||||
id: string
|
||||
api_format: string
|
||||
is_active: boolean
|
||||
active_keys?: number
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
provider: any
|
||||
endpoints?: Endpoint[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -195,6 +232,16 @@ const { copyToClipboard } = useClipboard()
|
||||
const loading = ref(false)
|
||||
const models = ref<Model[]>([])
|
||||
const togglingModelId = ref<string | null>(null)
|
||||
const testingModelId = ref<string | null>(null)
|
||||
const formatMenuModelId = ref<string | null>(null)
|
||||
|
||||
// 获取可用的 API 格式(有活跃端点且有活跃 Key)
|
||||
const availableApiFormats = computed(() => {
|
||||
if (!props.endpoints) return []
|
||||
return props.endpoints
|
||||
.filter(ep => ep.is_active && (ep.active_keys ?? 0) > 0)
|
||||
.map(ep => ep.api_format)
|
||||
})
|
||||
|
||||
// 按名称排序的模型列表
|
||||
const sortedModels = computed(() => {
|
||||
@@ -295,7 +342,7 @@ function getStatusTitle(model: Model): string {
|
||||
return '活跃但不可用'
|
||||
}
|
||||
|
||||
// <EFBFBD><EFBFBD>辑模型
|
||||
// 编辑模型
|
||||
function editModel(model: Model) {
|
||||
emit('editModel', model)
|
||||
}
|
||||
@@ -327,7 +374,69 @@ async function toggleModelActive(model: Model) {
|
||||
}
|
||||
}
|
||||
|
||||
// 测试模型连接性
|
||||
async function testModelConnection(model: Model, apiFormat?: string) {
|
||||
if (testingModelId.value) return
|
||||
|
||||
testingModelId.value = model.id
|
||||
formatMenuModelId.value = null
|
||||
try {
|
||||
const result = await testModel({
|
||||
provider_id: props.provider.id,
|
||||
model_name: model.provider_model_name,
|
||||
message: "hello",
|
||||
api_format: apiFormat
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
// 根据响应内容显示不同的成功消息
|
||||
if (result.data?.response?.choices?.[0]?.message?.content) {
|
||||
const content = result.data.response.choices[0].message.content
|
||||
showSuccess(`测试成功,响应: ${content.substring(0, 100)}${content.length > 100 ? '...' : ''}`)
|
||||
} else if (result.data?.content_preview) {
|
||||
showSuccess(`流式测试成功,预览: ${result.data.content_preview}`)
|
||||
} else {
|
||||
showSuccess(`模型 "${model.provider_model_name}" 测试成功`)
|
||||
}
|
||||
} else {
|
||||
showError(`模型测试失败: ${parseTestModelError(result)}`)
|
||||
}
|
||||
} catch (err: any) {
|
||||
const errorMsg = err.response?.data?.detail || err.message || '测试请求失败'
|
||||
showError(`模型测试失败: ${errorMsg}`)
|
||||
} finally {
|
||||
testingModelId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
// 处理测试按钮点击
|
||||
function handleTestClick(model: Model) {
|
||||
const formats = availableApiFormats.value
|
||||
if (formats.length === 0) {
|
||||
// 没有可用格式信息,使用默认行为
|
||||
testModelConnection(model)
|
||||
} else if (formats.length === 1) {
|
||||
// 只有一种格式,直接测试
|
||||
testModelConnection(model, formats[0])
|
||||
} else {
|
||||
// 多种格式,显示选择菜单
|
||||
formatMenuModelId.value = formatMenuModelId.value === model.id ? null : model.id
|
||||
}
|
||||
}
|
||||
|
||||
// 点击外部关闭格式选择菜单
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
if (formatMenuModelId.value && !(event.target as Element).closest('.relative')) {
|
||||
formatMenuModelId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadModels()
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', handleClickOutside)
|
||||
})
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user