Fix/frontend live data #16
@@ -231,3 +231,5 @@ Agent 工具列表按 `mcp.<server_id>.<remote_name>` 的远程工具名匹配
|
|||||||
### 聊天模型选择审阅修复
|
### 聊天模型选择审阅修复
|
||||||
|
|
||||||
返回聊天页时保留仍启用的提供商与手动模型 ID,仅刷新其模型列表;未选择、已删除或已禁用的提供商才回退到默认值。提供商加载失败时保留当前选择并展示错误。新增页面重新挂载与异常分支回归,前端共 89 项测试通过。
|
返回聊天页时保留仍启用的提供商与手动模型 ID,仅刷新其模型列表;未选择、已删除或已禁用的提供商才回退到默认值。提供商加载失败时保留当前选择并展示错误。新增页面重新挂载与异常分支回归,前端共 89 项测试通过。
|
||||||
|
|
||||||
|
补充卸载时序修复:提供商或技能加载期间离开聊天页后,旧页面的初始化回调不再修改聊天选择,迟到错误也不再更新旧页面。两种加载延迟均通过先失败、修复后通过的回归测试,并验证返回页面后的默认模型和发送按钮状态;前端共 91 项测试通过。
|
||||||
|
|||||||
@@ -64,3 +64,26 @@ it('preserves the selection when provider discovery fails', async () => {
|
|||||||
expect(wrapper.get('.error-banner').text()).toBe('offline')
|
expect(wrapper.get('.error-banner').text()).toBe('offline')
|
||||||
wrapper.unmount()
|
wrapper.unmount()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it.each(['providers', 'skills'])('ignores initialization after unmount while %s are loading', async source => {
|
||||||
|
const chat = useChatStore()
|
||||||
|
let finish!: () => void
|
||||||
|
const pending = new Promise<void>(resolve => { finish = resolve })
|
||||||
|
if (source === 'providers') vi.mocked(useProviderStore().loadProviders).mockReturnValueOnce(pending)
|
||||||
|
else vi.mocked(useSkillStore().loadSkills).mockReturnValueOnce(pending)
|
||||||
|
const first = mount(ChatView)
|
||||||
|
first.unmount()
|
||||||
|
finish()
|
||||||
|
await flushPromises()
|
||||||
|
expect(chat.selectedProviderId).toBe('')
|
||||||
|
expect(chat.selectedModel).toBe('')
|
||||||
|
expect(useProviderStore().loadModels).not.toHaveBeenCalled()
|
||||||
|
|
||||||
|
const returned = mount(ChatView)
|
||||||
|
await flushPromises()
|
||||||
|
expect(chat.selectedProviderId).toBe('a')
|
||||||
|
expect(chat.selectedModel).toBe('a-default')
|
||||||
|
await returned.get('textarea').setValue('hello')
|
||||||
|
expect(returned.get('button.button-primary').attributes('disabled')).toBeUndefined()
|
||||||
|
returned.unmount()
|
||||||
|
})
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref, watch } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import type { Citation } from '@/contracts'
|
import type { Citation } from '@/contracts'
|
||||||
import { useChatStore } from '@/stores/chat'
|
import { useChatStore } from '@/stores/chat'
|
||||||
@@ -16,13 +16,15 @@ const workspaceStore = useWorkspaceStore()
|
|||||||
const editorStore = useEditorStore()
|
const editorStore = useEditorStore()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const loadError = ref('')
|
const loadError = ref('')
|
||||||
|
let disposed = false
|
||||||
|
onBeforeUnmount(() => { disposed = true })
|
||||||
|
|
||||||
const availableModels = computed(() => providerStore.modelsByProvider[chatStore.selectedProviderId] ?? [])
|
const availableModels = computed(() => providerStore.modelsByProvider[chatStore.selectedProviderId] ?? [])
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
await Promise.all([providerStore.loadProviders(), skillStore.loadSkills()])
|
await Promise.all([providerStore.loadProviders(), skillStore.loadSkills()])
|
||||||
if (providerStore.error) return
|
if (disposed || providerStore.error) return
|
||||||
const selected = providerStore.enabledProviders.find(p => p.provider_id === chatStore.selectedProviderId)
|
const selected = providerStore.enabledProviders.find(p => p.provider_id === chatStore.selectedProviderId)
|
||||||
if (!selected) {
|
if (!selected) {
|
||||||
chatStore.selectedProviderId = providerStore.defaultProviderId
|
chatStore.selectedProviderId = providerStore.defaultProviderId
|
||||||
@@ -30,6 +32,7 @@ onMounted(async () => {
|
|||||||
await refreshModels(selected.provider_id)
|
await refreshModels(selected.provider_id)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (disposed) return
|
||||||
loadError.value = error instanceof Error ? error.message : '无法加载 AI 配置,请检查后端连接。'
|
loadError.value = error instanceof Error ? error.message : '无法加载 AI 配置,请检查后端连接。'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -38,7 +41,7 @@ async function refreshModels(providerId: string) {
|
|||||||
loadError.value = ''
|
loadError.value = ''
|
||||||
if (!providerId) return
|
if (!providerId) return
|
||||||
try { await providerStore.loadModels(providerId) }
|
try { await providerStore.loadModels(providerId) }
|
||||||
catch (error) { if (chatStore.selectedProviderId === providerId) loadError.value = error instanceof Error ? error.message : '模型列表加载失败,请手动填写模型 ID。' }
|
catch (error) { if (!disposed && chatStore.selectedProviderId === providerId) loadError.value = error instanceof Error ? error.message : '模型列表加载失败,请手动填写模型 ID。' }
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(() => chatStore.selectedProviderId, async (providerId) => {
|
watch(() => chatStore.selectedProviderId, async (providerId) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user