Fix/frontend live data #16

Merged
Kronecker merged 4 commits from fix/frontend-live-data into main 2026-09-04 08:34:51 +08:00
3 changed files with 31 additions and 3 deletions
Showing only changes of commit 8480ed7f5e - Show all commits
@@ -231,3 +231,5 @@ Agent 工具列表按 `mcp.<server_id>.<remote_name>` 的远程工具名匹配
### 聊天模型选择审阅修复
返回聊天页时保留仍启用的提供商与手动模型 ID,仅刷新其模型列表;未选择、已删除或已禁用的提供商才回退到默认值。提供商加载失败时保留当前选择并展示错误。新增页面重新挂载与异常分支回归,前端共 89 项测试通过。
补充卸载时序修复:提供商或技能加载期间离开聊天页后,旧页面的初始化回调不再修改聊天选择,迟到错误也不再更新旧页面。两种加载延迟均通过先失败、修复后通过的回归测试,并验证返回页面后的默认模型和发送按钮状态;前端共 91 项测试通过。
@@ -64,3 +64,26 @@ it('preserves the selection when provider discovery fails', async () => {
expect(wrapper.get('.error-banner').text()).toBe('offline')
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()
})
+6 -3
View File
@@ -1,5 +1,5 @@
<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 type { Citation } from '@/contracts'
import { useChatStore } from '@/stores/chat'
@@ -16,13 +16,15 @@ const workspaceStore = useWorkspaceStore()
const editorStore = useEditorStore()
const router = useRouter()
const loadError = ref('')
let disposed = false
onBeforeUnmount(() => { disposed = true })
const availableModels = computed(() => providerStore.modelsByProvider[chatStore.selectedProviderId] ?? [])
onMounted(async () => {
try {
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)
if (!selected) {
chatStore.selectedProviderId = providerStore.defaultProviderId
@@ -30,6 +32,7 @@ onMounted(async () => {
await refreshModels(selected.provider_id)
}
} catch (error) {
if (disposed) return
loadError.value = error instanceof Error ? error.message : '无法加载 AI 配置,请检查后端连接。'
}
})
@@ -38,7 +41,7 @@ async function refreshModels(providerId: string) {
loadError.value = ''
if (!providerId) return
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) => {