fix(ui): 修复模型连接测试的关键问题并增强代码质量

修复问题:
1. 修复 API 参数调用错误 - addModel 方法缺少 key 参数导致验证失败
2. 修复 API Key 脱敏问题 - 测试连接时使用脱敏后的 Key 导致认证失败
3. 修复状态管理时序问题 - 使用 nextTick 替代不可靠的 setTimeout

增强功能:
1. 强化连接测试的前置校验逻辑,增加配置完整性检查
2. 优化并发控制,测试期间自动禁用按钮,防止重复请求
3. 统一守卫逻辑到单一数据源,遵循 DRY 原则
4. 改进 paramOverrides 更新逻辑,支持参数删除操作

技术改进:
- 使用 Vue nextTick API 替代 setTimeout,提升可靠性
- 实现临时模型测试模式,避免污染原始配置
- 增强 canTestFormConnection 校验,包含配置完整性检查
- 正确处理 API Key 脱敏/恢复逻辑

影响范围:
- packages/core/src/services/model/manager.ts
- packages/ui/src/composables/useTextModelManager.ts
- packages/ui/src/components/TextModelEditModal.vue
- packages/ui/src/components/TextModelManager.vue
This commit is contained in:
linshen committed 2025-10-08 02:00:08 +08:00
1 parent 935c6e3c5b
commit 2fb11e13ba
4 files changed
+95 -16

No files matched your search

+5 -5
View File
@@ -332,11 +332,11 @@ export class ModelManager implements IModelManager {
...existingTextModelConfig.connectionConfig,
...(config.connectionConfig || {})
},
// Deep merge paramOverrides
paramOverrides: {
...existingTextModelConfig.paramOverrides,
...(config.paramOverrides || {})
}
// 处理 paramOverrides:如果明确传入了 paramOverrides,则直接替换而不是合并
// 这样可以确保用户删除的参数不会被错误地保留
paramOverrides: config.paramOverrides !== undefined
? config.paramOverrides
: existingTextModelConfig.paramOverrides || {}
};
// 如果更新了关键字段,需要验证配置
@@ -331,7 +331,7 @@
</template>
<script setup lang="ts">
import { computed, inject } from 'vue'
import { computed, inject, nextTick } from 'vue'
import { useI18n } from 'vue-i18n'
import {
NModal,
@@ -403,11 +403,15 @@ const isSaving = manager.isSaving
const isEditing = computed(() => !!manager.editingModelId.value)
const handleUpdateShow = (value: boolean) => {
const handleUpdateShow = async (value: boolean) => {
emit('update:show', value)
// 只有在明确关闭时才重置表单状态
if (!value) {
// 等待父组件处理状态变化后再重置表单
await nextTick()
manager.resetFormState()
}
emit('update:show', value)
}
const handleSubmit = async () => {
@@ -41,6 +41,11 @@ const handleModelUpdated = async (id?: string) => {
if (targetId) {
emit('modelsUpdated', targetId)
}
// 保存成功后关闭模态框并重置表单状态
showEditModal.value = false
editingModelId.value = null
manager.resetFormState()
}
const handleTestConnection = async (id: string) => {
@@ -48,16 +53,27 @@ const handleTestConnection = async (id: string) => {
}
const handleEditModel = async (id: string) => {
await manager.prepareForEdit(id)
// 如果已经在编辑同一个模型且模态框已经打开,直接返回
if (editingModelId.value === id && showEditModal.value === true) {
return
}
// 如果切换到不同的模型,重置表单状态
if (editingModelId.value && editingModelId.value !== id) {
manager.resetFormState()
}
// 准备编辑模式(总是会执行,因为我们需要确保状态正确)
await manager.prepareForEdit(id, true)
editingModelId.value = id
showEditModal.value = true
}
const updateEditModalVisibility = (value: boolean) => {
showEditModal.value = value
// 当模态框关闭时,重置编辑状态但不重置表单数据
if (!value) {
editingModelId.value = null
manager.resetFormState()
}
}
@@ -139,7 +139,18 @@ export function useTextModelManager() {
)
})
const canTestFormConnection = computed(() => !!editingModelId.value && !isTestingFormConnection.value)
const canTestFormConnection = computed(() => {
// 必须在编辑模式下
if (!editingModelId.value) return false
// 测试期间禁用
if (isTestingFormConnection.value) return false
// 必须有必需的连接配置
if (!isConnectionConfigured.value) return false
// 必须有模型名称
if (!form.value.name?.trim()) return false
return true
})
const canRefreshModelOptions = computed(() => {
return selectedProvider.value?.supportsDynamicModels && isConnectionConfigured.value && !isLoadingModelOptions.value
})
@@ -334,7 +345,12 @@ export function useTextModelManager() {
formReady.value = true
}
const prepareForEdit = async (id: string) => {
const prepareForEdit = async (id: string, forceReload = true) => {
// 如果已经在编辑同一个模型且不强制重新加载,则跳过
if (!forceReload && editingModelId.value === id && formReady.value) {
return
}
resetFormState()
editingModelId.value = id
await ensureProvidersLoaded()
@@ -546,13 +562,56 @@ export function useTextModelManager() {
}
const testFormConnection = async () => {
if (!editingModelId.value || isTestingFormConnection.value) return
// 使用 canTestFormConnection 的完整校验逻辑
if (!canTestFormConnection.value) return
isTestingFormConnection.value = true
formConnectionStatus.value = { type: 'info', message: t('modelManager.testing') }
try {
await llmService.testConnection(editingModelId.value)
formConnectionStatus.value = { type: 'success', message: t('modelManager.testSuccess', { provider: form.value.name }) }
toast.success(t('modelManager.testSuccess', { provider: form.value.name }))
// 获取原始配置
const existingConfig = await modelManager.getModel(editingModelId.value)
if (!existingConfig) {
throw new Error('模型配置不存在')
}
// 创建临时测试配置,使用当前表单的参数覆盖
// 注意:如果 API Key 被脱敏显示,需要使用原始 Key
const connectionConfig = {
...form.value.connectionConfig,
apiKey: form.value.displayMaskedKey && form.value.originalApiKey
? form.value.originalApiKey
: form.value.connectionConfig.apiKey
}
const tempConfig: TextModelConfig = {
...existingConfig,
id: `temp-test-${editingModelId.value}`,
name: form.value.name,
enabled: form.value.enabled,
providerMeta: { ...existingConfig.providerMeta },
modelMeta: { ...existingConfig.modelMeta },
connectionConfig,
paramOverrides: { ...(form.value.paramOverrides || {}) }
}
// 创建临时模型用于测试
await modelManager.addModel(tempConfig.id, tempConfig)
try {
// 测试临时模型
await llmService.testConnection(tempConfig.id)
formConnectionStatus.value = { type: 'success', message: t('modelManager.testSuccess', { provider: form.value.name }) }
toast.success(t('modelManager.testSuccess', { provider: form.value.name }))
} finally {
// 清理临时模型
try {
await modelManager.deleteModel(tempConfig.id)
} catch (cleanupError) {
console.warn('清理临时测试模型失败:', cleanupError)
}
}
} catch (error: any) {
console.error('连接测试失败:', error)
formConnectionStatus.value = {