fix(chat): 保留页面切换后的提供商与模型选择

This commit is contained in:
2026-09-04 08:24:49 +08:00
parent 9f621371b8
commit 150cf0d994
3 changed files with 83 additions and 3 deletions
@@ -227,3 +227,7 @@ Agent 工具列表按 `mcp.<server_id>.<remote_name>` 的远程工具名匹配
卡片默认显示三行摘要,完整服务原文可展开查看,展开操作不会改变工具选择。服务已提供中文说明时优先保留;未收录的 MCP 工具明确提示暂无中文说明,不将本地摘要当作服务协议或自动翻译结果。原始说明及其中的参数规则完整保留。
验证:前端 84 项测试、类型检查与生产构建通过。新增回归覆盖不同服务器命名空间、未知工具、服务中文说明、原文完整性,以及选择工具时保留原始 ID。
### 聊天模型选择审阅修复
返回聊天页时保留仍启用的提供商与手动模型 ID,仅刷新其模型列表;未选择、已删除或已禁用的提供商才回退到默认值。提供商加载失败时保留当前选择并展示错误。新增页面重新挂载与异常分支回归,前端共 89 项测试通过。
@@ -0,0 +1,66 @@
// @vitest-environment happy-dom
import { beforeEach, expect, it, vi } from 'vitest'
import { flushPromises, mount } from '@vue/test-utils'
import { createPinia, setActivePinia } from 'pinia'
import { useChatStore } from '@/stores/chat'
import { useProviderStore } from '@/stores/provider'
import { useSkillStore } from '@/stores/skill'
import ChatView from './ChatView.vue'
vi.mock('vue-router', () => ({ useRouter: () => ({ push: vi.fn() }) }))
vi.mock('@/stores/editor', () => ({ useEditorStore: () => ({}) }))
vi.mock('@/stores/workspace', () => ({ useWorkspaceStore: () => ({}) }))
vi.mock('@/components/common/MarkdownContent.vue', () => ({ default: { template: '<div />' } }))
beforeEach(() => {
setActivePinia(createPinia())
const providers = useProviderStore()
providers.providers = ['a', 'b'].map(id => ({
provider_id: id, provider_type: 'openai_compatible', name: id,
default_model: `${id}-default`, enabled: true, capabilities: { chat: true }, has_credential: false,
}))
providers.defaultProviderId = 'a'
vi.spyOn(providers, 'loadProviders').mockResolvedValue(undefined)
vi.spyOn(providers, 'loadModels').mockResolvedValue([])
vi.spyOn(useSkillStore(), 'loadSkills').mockResolvedValue(undefined)
})
it('preserves the selected provider and manual model after leaving and returning to chat', async () => {
const chat = useChatStore()
const first = mount(ChatView)
await flushPromises()
await first.get('select').setValue('b')
await first.get('input[list="chat-models"]').setValue('b-manual')
first.unmount()
const returned = mount(ChatView)
await flushPromises()
expect(chat.selectedProviderId).toBe('b')
expect(chat.selectedModel).toBe('b-manual')
expect(useProviderStore().loadModels).toHaveBeenLastCalledWith('b')
returned.unmount()
})
it.each(['missing', 'disabled', 'unselected'])('uses the default when the selected provider is %s', async state => {
const chat = useChatStore()
chat.selectedProviderId = state === 'unselected' ? '' : state === 'missing' ? 'deleted' : 'b'
chat.selectedModel = 'old-model'
if (state === 'disabled') useProviderStore().providers[1]!.enabled = false
const wrapper = mount(ChatView)
await flushPromises()
expect(chat.selectedProviderId).toBe('a')
expect(chat.selectedModel).toBe('a-default')
wrapper.unmount()
})
it('preserves the selection when provider discovery fails', async () => {
const chat = useChatStore()
chat.selectedProviderId = 'b'
chat.selectedModel = 'b-manual'
useProviderStore().error = 'offline'
const wrapper = mount(ChatView)
await flushPromises()
expect(chat.selectedProviderId).toBe('b')
expect(chat.selectedModel).toBe('b-manual')
expect(wrapper.get('.error-banner').text()).toBe('offline')
wrapper.unmount()
})
+13 -3
View File
@@ -22,18 +22,28 @@ const availableModels = computed(() => providerStore.modelsByProvider[chatStore.
onMounted(async () => {
try {
await Promise.all([providerStore.loadProviders(), skillStore.loadSkills()])
chatStore.selectedProviderId = providerStore.defaultProviderId
if (providerStore.error) return
const selected = providerStore.enabledProviders.find(p => p.provider_id === chatStore.selectedProviderId)
if (!selected) {
chatStore.selectedProviderId = providerStore.defaultProviderId
} else {
await refreshModels(selected.provider_id)
}
} catch (error) {
loadError.value = error instanceof Error ? error.message : '无法加载 AI 配置,请检查后端连接。'
}
})
watch(() => chatStore.selectedProviderId, async (providerId) => {
chatStore.selectedModel = providerStore.providers.find(p => p.provider_id === providerId)?.default_model ?? ''
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。' }
}
watch(() => chatStore.selectedProviderId, async (providerId) => {
chatStore.selectedModel = providerStore.providers.find(p => p.provider_id === providerId)?.default_model ?? ''
await refreshModels(providerId)
})
function send() { void chatStore.sendMessage(chatStore.inputText) }