refactor: 提取正则工具模块并清理废弃代码

前端:
- 新增 model-mapping-regex.ts 工具模块,统一正则验证和 LRU 缓存
- ModelMappingsTab/RoutingTab 重构为使用 computed 缓存匹配结果
- 移除多个组件中未使用的变量和函数
- 添加 HTMLImageElement/HTMLIFrameElement 到 ESLint 全局类型
- 修复 vitest 需要 --experimental-require-module 的问题

后端:
- 移除废弃的异步数据库支持 (get_async_db, AsyncSession)
- 将 async_utils 从 database/ 迁移到 utils/
- 改进 database.py 类型标注
- 修复 email 模块的 aiosmtplib 可选导入类型问题
This commit is contained in:
fawney19
2026-01-15 17:03:19 +08:00
parent bc16c0eec8
commit a223819dd7
25 changed files with 526 additions and 688 deletions

View File

@@ -116,8 +116,14 @@
:title="isLocked(model) ? '已锁定 - 点击解锁' : '点击锁定(刷新时不会被删除)'"
@click="toggleLock(model, $event)"
>
<Lock v-if="isLocked(model)" class="w-3.5 h-3.5" />
<LockOpen v-else class="w-3.5 h-3.5" />
<Lock
v-if="isLocked(model)"
class="w-3.5 h-3.5"
/>
<LockOpen
v-else
class="w-3.5 h-3.5"
/>
</button>
</div>
</div>
@@ -181,8 +187,14 @@
:title="isLocked(model.name) ? '已锁定 - 点击解锁' : '点击锁定(刷新时不会被删除)'"
@click="toggleLock(model.name, $event)"
>
<Lock v-if="isLocked(model.name)" class="w-3.5 h-3.5" />
<LockOpen v-else class="w-3.5 h-3.5" />
<Lock
v-if="isLocked(model.name)"
class="w-3.5 h-3.5"
/>
<LockOpen
v-else
class="w-3.5 h-3.5"
/>
</button>
</div>
</div>
@@ -243,8 +255,14 @@
:title="isLocked(model) ? '已锁定 - 点击解锁' : '点击锁定(刷新时不会被删除)'"
@click="toggleLock(model, $event)"
>
<Lock v-if="isLocked(model)" class="w-3.5 h-3.5" />
<LockOpen v-else class="w-3.5 h-3.5" />
<Lock
v-if="isLocked(model)"
class="w-3.5 h-3.5"
/>
<LockOpen
v-else
class="w-3.5 h-3.5"
/>
</button>
</div>
</div>
@@ -600,7 +618,7 @@ async function loadGlobalModels() {
name: m.name,
display_name: m.display_name
}))
} catch (err) {
} catch {
if (loadingCancelled) return
showError('加载全局模型失败', '错误')
} finally {

View File

@@ -256,7 +256,7 @@
</template>
<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue'
import { ref, computed, onMounted } from 'vue'
import { Dialog, Button, Input, Label, Switch } from '@/components/ui'
import { Key, SquarePen } from 'lucide-vue-next'
import { useToast } from '@/composables/useToast'

View File

@@ -259,10 +259,7 @@ function handleDrop(targetIndex: number) {
const groupNewPriority = new Map<number, number>() // 原优先级 -> 新优先级
let currentPriority = 1
// 找到被拖动项在原数组中的索引对应的原始优先级
const draggedOriginalPriority = originalPriorityMap.get(dragIndex)!
items.forEach((alias, newIdx) => {
items.forEach(alias => {
// 找到这个映射在原数组中的索引
const originalIdx = aliases.value.findIndex(a => a === alias)
const originalPriority = originalIdx >= 0 ? originalPriorityMap.get(originalIdx)! : alias.priority

View File

@@ -416,20 +416,6 @@
@endpoint-updated="handleEndpointChanged"
/>
<!-- 删除端点确认对话框 -->
<AlertDialog
v-if="open"
:model-value="deleteEndpointConfirmOpen"
title="删除端点"
:description="`确定要删除端点 ${endpointToDelete?.api_format} 吗?这将同时删除其所有密钥。`"
confirm-text="删除"
cancel-text="取消"
type="danger"
@update:model-value="deleteEndpointConfirmOpen = $event"
@confirm="confirmDeleteEndpoint"
@cancel="deleteEndpointConfirmOpen = false"
/>
<!-- 密钥编辑对话框 -->
<KeyFormDialog
v-if="open"
@@ -535,11 +521,9 @@ import EndpointFormDialog from '@/features/providers/components/EndpointFormDial
import ProviderModelFormDialog from '@/features/providers/components/ProviderModelFormDialog.vue'
import AlertDialog from '@/components/common/AlertDialog.vue'
import {
deleteEndpoint as deleteEndpointAPI,
deleteEndpointKey,
recoverKeyHealth,
getProviderKeys,
updateEndpoint,
updateProviderKey,
revealEndpointKey,
type ProviderEndpoint,
@@ -579,12 +563,9 @@ const loading = ref(false)
const provider = ref<any>(null)
const endpoints = ref<ProviderEndpointWithKeys[]>([])
const providerKeys = ref<EndpointAPIKey[]>([]) // Provider 级别的 keys
const expandedEndpoints = ref<Set<string>>(new Set())
// 端点相关状态
const endpointDialogOpen = ref(false)
const deleteEndpointConfirmOpen = ref(false)
const endpointToDelete = ref<ProviderEndpoint | null>(null)
// 密钥相关状态
const keyFormDialogOpen = ref(false)
@@ -593,13 +574,10 @@ const currentEndpoint = ref<ProviderEndpoint | null>(null)
const editingKey = ref<EndpointAPIKey | null>(null)
const deleteKeyConfirmOpen = ref(false)
const keyToDelete = ref<EndpointAPIKey | null>(null)
const recoveringEndpointId = ref<string | null>(null)
const togglingEndpointId = ref<string | null>(null)
const togglingKeyId = ref<string | null>(null)
// 密钥显示状态key_id -> 完整密钥
const revealedKeys = ref<Map<string, string>>(new Map())
const revealingKeyId = ref<string | null>(null)
// 模型相关状态
const modelFormDialogOpen = ref(false)
@@ -609,14 +587,6 @@ const modelToDelete = ref<Model | null>(null)
const batchAssignDialogOpen = ref(false)
const modelMappingTabRef = ref<InstanceType<typeof ModelMappingTab> | null>(null)
// 拖动排序相关状态(旧的端点级别拖拽,保留以兼容)
const dragState = ref({
isDragging: false,
draggedKeyId: null as string | null,
targetKeyId: null as string | null,
dragEndpointId: null as string | null
})
// 密钥列表拖拽排序状态
const keyDragState = ref({
isDragging: false,
@@ -640,7 +610,6 @@ const multiplierSaving = ref(false)
// 任意模态窗口打开时,阻止抽屉被误关闭
const hasBlockingDialogOpen = computed(() =>
endpointDialogOpen.value ||
deleteEndpointConfirmOpen.value ||
keyFormDialogOpen.value ||
keyPermissionsDialogOpen.value ||
deleteKeyConfirmOpen.value ||
@@ -702,18 +671,15 @@ watch(() => props.open, (newOpen) => {
provider.value = null
endpoints.value = []
providerKeys.value = [] // 清空 Provider 级别的 keys
expandedEndpoints.value.clear()
// 重置所有对话框状态
endpointDialogOpen.value = false
deleteEndpointConfirmOpen.value = false
keyFormDialogOpen.value = false
keyPermissionsDialogOpen.value = false
deleteKeyConfirmOpen.value = false
batchAssignDialogOpen.value = false
// 重置临时数据
endpointToDelete.value = null
currentEndpoint.value = null
editingKey.value = null
keyToDelete.value = null
@@ -737,15 +703,6 @@ function handleClose() {
}
}
// 切换端点展开/收起
function toggleEndpoint(endpointId: string) {
if (expandedEndpoints.value.has(endpointId)) {
expandedEndpoints.value.delete(endpointId)
} else {
expandedEndpoints.value.add(endpointId)
}
}
// 显示端点管理对话框
function showAddEndpointDialog() {
endpointDialogOpen.value = true
@@ -757,27 +714,6 @@ function handleEditEndpoint(_endpoint: ProviderEndpoint) {
endpointDialogOpen.value = true
}
function handleDeleteEndpoint(endpoint: ProviderEndpoint) {
endpointToDelete.value = endpoint
deleteEndpointConfirmOpen.value = true
}
async function confirmDeleteEndpoint() {
if (!endpointToDelete.value) return
try {
await deleteEndpointAPI(endpointToDelete.value.id)
showSuccess('端点已删除')
await loadEndpoints()
emit('refresh')
} catch (err: any) {
showError(err.response?.data?.detail || '删除失败', '错误')
} finally {
deleteEndpointConfirmOpen.value = false
endpointToDelete.value = null
}
}
async function handleEndpointChanged() {
await Promise.all([loadProvider(), loadEndpoints()])
emit('refresh')
@@ -808,37 +744,18 @@ function handleKeyPermissions(key: EndpointAPIKey) {
keyPermissionsDialogOpen.value = true
}
// 切换密钥显示/隐藏
async function toggleKeyReveal(key: EndpointAPIKey) {
if (revealedKeys.value.has(key.id)) {
// 已显示,隐藏它
revealedKeys.value.delete(key.id)
return
}
// 未显示,调用 API 获取完整密钥
revealingKeyId.value = key.id
try {
const result = await revealEndpointKey(key.id)
revealedKeys.value.set(key.id, result.api_key)
} catch (err: any) {
showError(err.response?.data?.detail || '获取密钥失败', '错误')
} finally {
revealingKeyId.value = null
}
}
// 复制完整密钥
async function copyFullKey(key: EndpointAPIKey) {
// 如果已经显示了,直接复制
if (revealedKeys.value.has(key.id)) {
copyToClipboard(revealedKeys.value.get(key.id)!)
const cached = revealedKeys.value.get(key.id)
if (cached) {
copyToClipboard(cached)
return
}
// 否则先获取再复制
try {
const result = await revealEndpointKey(key.id)
revealedKeys.value.set(key.id, result.api_key)
copyToClipboard(result.api_key)
} catch (err: any) {
showError(err.response?.data?.detail || '获取密钥失败', '错误')
@@ -878,79 +795,11 @@ async function handleRecoverKey(key: EndpointAPIKey) {
}
}
// 检查端点是否有不健康的密钥
function hasUnhealthyKeys(endpoint: ProviderEndpointWithKeys): boolean {
if (!endpoint.keys || endpoint.keys.length === 0) return false
return endpoint.keys.some(key =>
key.circuit_breaker_open ||
(key.health_score !== undefined && key.health_score < 1)
)
}
// 批量恢复端点下所有密钥的健康状态
async function handleRecoverAllKeys(endpoint: ProviderEndpointWithKeys) {
if (!endpoint.keys || endpoint.keys.length === 0) return
const keysToRecover = endpoint.keys.filter(key =>
key.circuit_breaker_open ||
(key.health_score !== undefined && key.health_score < 1)
)
if (keysToRecover.length === 0) {
showSuccess('所有密钥已处于健康状态')
return
}
recoveringEndpointId.value = endpoint.id
let successCount = 0
let failCount = 0
try {
for (const key of keysToRecover) {
try {
await recoverKeyHealth(key.id)
successCount++
} catch {
failCount++
}
}
if (failCount === 0) {
showSuccess(`已恢复 ${successCount} 个密钥的健康状态`)
} else {
showSuccess(`恢复完成: ${successCount} 成功, ${failCount} 失败`)
}
await loadEndpoints()
emit('refresh')
} finally {
recoveringEndpointId.value = null
}
}
async function handleKeyChanged() {
await loadEndpoints()
emit('refresh')
}
// 切换端点启用状态
async function toggleEndpointActive(endpoint: ProviderEndpointWithKeys) {
if (togglingEndpointId.value) return
togglingEndpointId.value = endpoint.id
try {
const newStatus = !endpoint.is_active
await updateEndpoint(endpoint.id, { is_active: newStatus })
endpoint.is_active = newStatus
showSuccess(newStatus ? '端点已启用' : '端点已停用')
emit('refresh')
} catch (err: any) {
showError(err.response?.data?.detail || '操作失败', '错误')
} finally {
togglingEndpointId.value = null
}
}
// 切换密钥启用状态
async function toggleKeyActive(key: EndpointAPIKey) {
if (togglingKeyId.value) return
@@ -1022,123 +871,6 @@ async function confirmDeleteModel() {
}
}
// ===== 拖动排序处理 =====
function handleDragStart(event: DragEvent, key: EndpointAPIKey, endpoint: ProviderEndpointWithKeys) {
dragState.value.isDragging = true
dragState.value.draggedKeyId = key.id
dragState.value.dragEndpointId = endpoint.id
if (event.dataTransfer) {
event.dataTransfer.effectAllowed = 'move'
}
}
function handleDragEnd() {
dragState.value.isDragging = false
dragState.value.draggedKeyId = null
dragState.value.targetKeyId = null
dragState.value.dragEndpointId = null
}
function handleDragOver(event: DragEvent, targetKey: EndpointAPIKey) {
event.preventDefault()
if (event.dataTransfer) {
event.dataTransfer.dropEffect = 'move'
}
if (dragState.value.draggedKeyId !== targetKey.id) {
dragState.value.targetKeyId = targetKey.id
}
}
function handleDragLeave() {
dragState.value.targetKeyId = null
}
async function handleDrop(event: DragEvent, targetKey: EndpointAPIKey, endpoint: ProviderEndpointWithKeys) {
event.preventDefault()
const draggedKeyId = dragState.value.draggedKeyId
if (!draggedKeyId || !endpoint.keys || draggedKeyId === targetKey.id) {
handleDragEnd()
return
}
// 只允许在同一端点内拖动
if (dragState.value.dragEndpointId !== endpoint.id) {
showError('不能跨端点拖动密钥')
handleDragEnd()
return
}
const keys = [...endpoint.keys]
const draggedIndex = keys.findIndex(k => k.id === draggedKeyId)
const targetIndex = keys.findIndex(k => k.id === targetKey.id)
if (draggedIndex === -1 || targetIndex === -1) {
handleDragEnd()
return
}
// 记录原始优先级分组(排除被拖动的密钥)
// key: 原始优先级值, value: 密钥ID数组
const originalGroups = new Map<number, string[]>()
for (const key of keys) {
if (key.id === draggedKeyId) continue // 被拖动的密钥离开原组
const priority = key.internal_priority ?? 0
if (!originalGroups.has(priority)) {
originalGroups.set(priority, [])
}
originalGroups.get(priority)!.push(key.id)
}
// 重排数组
const [removed] = keys.splice(draggedIndex, 1)
keys.splice(targetIndex, 0, removed)
endpoint.keys = keys
// 按新顺序为每个组分配新的优先级
// 同组的密钥保持相同的优先级
const priorities: { key_id: string; internal_priority: number }[] = []
const groupNewPriority = new Map<number, number>() // 原优先级 -> 新优先级
let currentPriority = 0
for (const key of keys) {
if (key.id === draggedKeyId) {
// 被拖动的密钥是独立的新组,获得当前优先级
priorities.push({ key_id: key.id, internal_priority: currentPriority })
currentPriority++
} else {
const originalPriority = key.internal_priority ?? 0
if (groupNewPriority.has(originalPriority)) {
// 这个组已经分配过优先级,使用相同的值
priorities.push({ key_id: key.id, internal_priority: groupNewPriority.get(originalPriority)! })
} else {
// 这个组第一次出现,分配新优先级
groupNewPriority.set(originalPriority, currentPriority)
priorities.push({ key_id: key.id, internal_priority: currentPriority })
currentPriority++
}
}
}
handleDragEnd()
// 调用 API 批量更新(使用循环调用 updateProviderKey 替代已废弃的 batchUpdateKeyPriority
try {
await Promise.all(
priorities.map(p => updateProviderKey(p.key_id, { internal_priority: p.internal_priority }))
)
showSuccess('优先级已更新')
// 重新加载以获取更新后的数据
await loadEndpoints()
emit('refresh')
} catch (err: any) {
showError(err.response?.data?.detail || '更新优先级失败', '错误')
// 回滚 - 重新加载
await loadEndpoints()
}
}
// ===== 点击编辑优先级 =====
function startEditPriority(key: EndpointAPIKey) {
editingPriorityKey.value = key.id
@@ -1386,25 +1118,6 @@ async function handleKeyDrop(event: DragEvent, targetIndex: number) {
}
}
// 格式化探测时间
function formatProbeTime(probeTime: string): string {
if (!probeTime) return '-'
const now = new Date()
const probe = new Date(probeTime)
const diffMs = probe.getTime() - now.getTime()
if (diffMs < 0) return '待探测'
const diffMinutes = Math.floor(diffMs / 60000)
const diffHours = Math.floor(diffMinutes / 60)
const diffDays = Math.floor(diffHours / 24)
if (diffDays > 0) return `${diffDays}天后`
if (diffHours > 0) return `${diffHours}小时后`
if (diffMinutes > 0) return `${diffMinutes}分钟后`
return '即将探测'
}
// 获取密钥的 API 格式列表(按指定顺序排序)
function getKeyApiFormats(key: EndpointAPIKey, endpoint?: ProviderEndpointWithKeys): string[] {
let formats: string[] = []
@@ -1491,7 +1204,7 @@ function getFormatProbeCountdown(key: EndpointAPIKey, format: string): string {
const now = new Date()
const diffMs = nextProbe.getTime() - now.getTime()
if (diffMs > 0) {
return ' ' + formatCountdown(diffMs)
return ` ${formatCountdown(diffMs)}`
} else {
return ' 探测中'
}

View File

@@ -129,8 +129,14 @@
: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" />
<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