mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
fix: 修复模型测试时正则映射支持及缓存装饰器实例方法兼容
- ModelsTab 加载时并行获取模型映射预览数据 - 测试模型时自动识别正则映射并使用映射名称和指定 key - 密钥变更后并行刷新模型列表和模型映射 - 修复 cache_decorator 对实例方法的 context 参数解析
This commit is contained in:
@@ -383,6 +383,7 @@
|
|||||||
<!-- 模型查看 -->
|
<!-- 模型查看 -->
|
||||||
<ModelsTab
|
<ModelsTab
|
||||||
v-if="provider"
|
v-if="provider"
|
||||||
|
ref="modelsTabRef"
|
||||||
:key="`models-${provider.id}`"
|
:key="`models-${provider.id}`"
|
||||||
:provider="provider"
|
:provider="provider"
|
||||||
:endpoints="endpoints"
|
:endpoints="endpoints"
|
||||||
@@ -585,6 +586,7 @@ const editingModel = ref<Model | null>(null)
|
|||||||
const deleteModelConfirmOpen = ref(false)
|
const deleteModelConfirmOpen = ref(false)
|
||||||
const modelToDelete = ref<Model | null>(null)
|
const modelToDelete = ref<Model | null>(null)
|
||||||
const batchAssignDialogOpen = ref(false)
|
const batchAssignDialogOpen = ref(false)
|
||||||
|
const modelsTabRef = ref<InstanceType<typeof ModelsTab> | null>(null)
|
||||||
const modelMappingTabRef = ref<InstanceType<typeof ModelMappingTab> | null>(null)
|
const modelMappingTabRef = ref<InstanceType<typeof ModelMappingTab> | null>(null)
|
||||||
|
|
||||||
// 密钥列表拖拽排序状态
|
// 密钥列表拖拽排序状态
|
||||||
@@ -797,6 +799,11 @@ async function handleRecoverKey(key: EndpointAPIKey) {
|
|||||||
|
|
||||||
async function handleKeyChanged() {
|
async function handleKeyChanged() {
|
||||||
await loadEndpoints()
|
await loadEndpoints()
|
||||||
|
// 并行刷新模型列表和模型映射(因为模型权限会影响正则映射预览)
|
||||||
|
await Promise.all([
|
||||||
|
modelsTabRef.value?.reload(),
|
||||||
|
modelMappingTabRef.value?.reload()
|
||||||
|
])
|
||||||
emit('refresh')
|
emit('refresh')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -211,7 +211,13 @@ import Card from '@/components/ui/card.vue'
|
|||||||
import Button from '@/components/ui/button.vue'
|
import Button from '@/components/ui/button.vue'
|
||||||
import { useToast } from '@/composables/useToast'
|
import { useToast } from '@/composables/useToast'
|
||||||
import { useClipboard } from '@/composables/useClipboard'
|
import { useClipboard } from '@/composables/useClipboard'
|
||||||
import { getProviderModels, type Model, testModel } from '@/api/endpoints'
|
import {
|
||||||
|
getProviderModels,
|
||||||
|
getProviderMappingPreview,
|
||||||
|
testModel,
|
||||||
|
type Model,
|
||||||
|
type ProviderMappingPreviewResponse
|
||||||
|
} from '@/api/endpoints'
|
||||||
import { updateModel } from '@/api/endpoints/models'
|
import { updateModel } from '@/api/endpoints/models'
|
||||||
import { parseTestModelError } from '@/utils/errorParser'
|
import { parseTestModelError } from '@/utils/errorParser'
|
||||||
|
|
||||||
@@ -239,6 +245,7 @@ const { copyToClipboard } = useClipboard()
|
|||||||
// 状态
|
// 状态
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const models = ref<Model[]>([])
|
const models = ref<Model[]>([])
|
||||||
|
const mappingPreview = ref<ProviderMappingPreviewResponse | null>(null)
|
||||||
const togglingModelId = ref<string | null>(null)
|
const togglingModelId = ref<string | null>(null)
|
||||||
const testingModelId = ref<string | null>(null)
|
const testingModelId = ref<string | null>(null)
|
||||||
const formatMenuModelId = ref<string | null>(null)
|
const formatMenuModelId = ref<string | null>(null)
|
||||||
@@ -265,11 +272,19 @@ async function copyModelId(modelId: string) {
|
|||||||
await copyToClipboard(modelId)
|
await copyToClipboard(modelId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载模型
|
// 加载模型和映射预览
|
||||||
async function loadModels() {
|
async function loadModels() {
|
||||||
try {
|
try {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
models.value = await getProviderModels(props.provider.id)
|
const [modelsData, previewData] = await Promise.all([
|
||||||
|
getProviderModels(props.provider.id),
|
||||||
|
getProviderMappingPreview(props.provider.id).catch((err) => {
|
||||||
|
console.warn('Failed to load mapping preview:', err)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
])
|
||||||
|
models.value = modelsData
|
||||||
|
mappingPreview.value = previewData
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
showError(err.response?.data?.detail || '加载失败', '错误')
|
showError(err.response?.data?.detail || '加载失败', '错误')
|
||||||
} finally {
|
} finally {
|
||||||
@@ -382,6 +397,31 @@ async function toggleModelActive(model: Model) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 查找模型的正则映射信息(返回第一个匹配的活跃 key 和映射名称)
|
||||||
|
function findRegexMapping(model: Model): { keyId: string; mappedName: string } | null {
|
||||||
|
if (!mappingPreview.value) return null
|
||||||
|
|
||||||
|
// 在映射预览中查找该模型的全局模型 ID
|
||||||
|
const globalModelId = model.global_model_id
|
||||||
|
if (!globalModelId) return null
|
||||||
|
|
||||||
|
for (const keyInfo of mappingPreview.value.keys) {
|
||||||
|
// 跳过未激活的 key
|
||||||
|
if (!keyInfo.is_active) continue
|
||||||
|
|
||||||
|
for (const gm of keyInfo.matching_global_models) {
|
||||||
|
if (gm.global_model_id === globalModelId && gm.matched_models.length > 0) {
|
||||||
|
// 返回第一个匹配的映射名称
|
||||||
|
return {
|
||||||
|
keyId: keyInfo.key_id,
|
||||||
|
mappedName: gm.matched_models[0].allowed_model
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
// 测试模型连接性
|
// 测试模型连接性
|
||||||
async function testModelConnection(model: Model, apiFormat?: string) {
|
async function testModelConnection(model: Model, apiFormat?: string) {
|
||||||
if (testingModelId.value) return
|
if (testingModelId.value) return
|
||||||
@@ -389,11 +429,17 @@ async function testModelConnection(model: Model, apiFormat?: string) {
|
|||||||
testingModelId.value = model.id
|
testingModelId.value = model.id
|
||||||
formatMenuModelId.value = null
|
formatMenuModelId.value = null
|
||||||
try {
|
try {
|
||||||
|
// 检查是否有正则映射,如果有则使用映射名称和指定 key
|
||||||
|
const regexMapping = findRegexMapping(model)
|
||||||
|
const modelName = regexMapping?.mappedName || model.provider_model_name
|
||||||
|
const apiKeyId = regexMapping?.keyId
|
||||||
|
|
||||||
const result = await testModel({
|
const result = await testModel({
|
||||||
provider_id: props.provider.id,
|
provider_id: props.provider.id,
|
||||||
model_name: model.provider_model_name,
|
model_name: modelName,
|
||||||
message: "hello",
|
message: "hello",
|
||||||
api_format: apiFormat
|
api_format: apiFormat,
|
||||||
|
api_key_id: apiKeyId
|
||||||
})
|
})
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
@@ -404,7 +450,7 @@ async function testModelConnection(model: Model, apiFormat?: string) {
|
|||||||
} else if (result.data?.content_preview) {
|
} else if (result.data?.content_preview) {
|
||||||
showSuccess(`流式测试成功,预览: ${result.data.content_preview}`)
|
showSuccess(`流式测试成功,预览: ${result.data.content_preview}`)
|
||||||
} else {
|
} else {
|
||||||
showSuccess(`模型 "${model.provider_model_name}" 测试成功`)
|
showSuccess(`模型 "${modelName}" 测试成功`)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
showError(`模型测试失败: ${parseTestModelError(result)}`)
|
showError(`模型测试失败: ${parseTestModelError(result)}`)
|
||||||
@@ -447,4 +493,9 @@ onMounted(() => {
|
|||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
document.removeEventListener('click', handleClickOutside)
|
document.removeEventListener('click', handleClickOutside)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 暴露给父组件
|
||||||
|
defineExpose({
|
||||||
|
reload: loadModels
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -2,13 +2,28 @@
|
|||||||
|
|
||||||
import functools
|
import functools
|
||||||
import json
|
import json
|
||||||
from typing import Any, Callable, Optional
|
from typing import Any, Callable
|
||||||
|
|
||||||
from src.core.logger import logger
|
from src.core.logger import logger
|
||||||
|
|
||||||
from src.clients.redis_client import get_redis_client_sync
|
from src.clients.redis_client import get_redis_client_sync
|
||||||
|
|
||||||
|
|
||||||
|
def _is_adapter_instance(obj: Any) -> bool:
|
||||||
|
"""检查对象是否是 ApiAdapter 的实例(延迟导入避免循环依赖)"""
|
||||||
|
try:
|
||||||
|
from src.api.base.adapter import ApiAdapter
|
||||||
|
|
||||||
|
return isinstance(obj, ApiAdapter)
|
||||||
|
except ImportError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _is_api_context(obj: Any) -> bool:
|
||||||
|
"""检查对象是否是 ApiRequestContext(通过 duck typing)"""
|
||||||
|
return hasattr(obj, "user") and hasattr(obj, "db")
|
||||||
|
|
||||||
|
|
||||||
def cache_result(key_prefix: str, ttl: int = 60, user_specific: bool = True) -> Callable:
|
def cache_result(key_prefix: str, ttl: int = 60, user_specific: bool = True) -> Callable:
|
||||||
"""
|
"""
|
||||||
缓存函数结果的装饰器
|
缓存函数结果的装饰器
|
||||||
@@ -30,8 +45,23 @@ def cache_result(key_prefix: str, ttl: int = 60, user_specific: bool = True) ->
|
|||||||
|
|
||||||
# 构建缓存键
|
# 构建缓存键
|
||||||
try:
|
try:
|
||||||
# 从 args 中获取 context(通常是第一个参数)
|
# 从 args 中获取 context
|
||||||
context = args[0] if args else None
|
# 对于实例方法,args[0] 是 self,args[1] 才是 context
|
||||||
|
# 对于普通函数,args[0] 是 context
|
||||||
|
context = None
|
||||||
|
adapter_self = None
|
||||||
|
|
||||||
|
if len(args) >= 2 and _is_adapter_instance(args[0]) and _is_api_context(args[1]):
|
||||||
|
# 实例方法: handle(self, context)
|
||||||
|
adapter_self = args[0]
|
||||||
|
context = args[1]
|
||||||
|
elif len(args) >= 1 and _is_api_context(args[0]):
|
||||||
|
# 普通函数或 context 在第一个位置
|
||||||
|
context = args[0]
|
||||||
|
elif len(args) >= 1 and _is_adapter_instance(args[0]):
|
||||||
|
# 实例方法但 context 可能在 kwargs 中
|
||||||
|
adapter_self = args[0]
|
||||||
|
context = kwargs.get("context")
|
||||||
|
|
||||||
if user_specific and context and hasattr(context, "user") and context.user:
|
if user_specific and context and hasattr(context, "user") and context.user:
|
||||||
cache_key = f"{key_prefix}:user:{context.user.id}"
|
cache_key = f"{key_prefix}:user:{context.user.id}"
|
||||||
@@ -39,11 +69,11 @@ def cache_result(key_prefix: str, ttl: int = 60, user_specific: bool = True) ->
|
|||||||
cache_key = f"{key_prefix}:global"
|
cache_key = f"{key_prefix}:global"
|
||||||
|
|
||||||
# 如果有额外的参数(如 days),添加到键中
|
# 如果有额外的参数(如 days),添加到键中
|
||||||
if hasattr(args[0], "__dict__"):
|
# 从 adapter_self 获取(dataclass 属性)
|
||||||
# 如果是 dataclass 或对象,获取其属性
|
if adapter_self and hasattr(adapter_self, "__dict__"):
|
||||||
for attr_name in ["days", "limit"]:
|
for attr_name in ["days", "limit"]:
|
||||||
if hasattr(args[0], attr_name):
|
if hasattr(adapter_self, attr_name):
|
||||||
attr_value = getattr(args[0], attr_name)
|
attr_value = getattr(adapter_self, attr_name)
|
||||||
cache_key += f":{attr_name}:{attr_value}"
|
cache_key += f":{attr_name}:{attr_value}"
|
||||||
|
|
||||||
# 尝试从缓存获取
|
# 尝试从缓存获取
|
||||||
|
|||||||
Reference in New Issue
Block a user