From 32411ce6fe1f557b08653482759f30105664c95e Mon Sep 17 00:00:00 2001 From: KiriAky 107 Date: Sat, 5 Sep 2026 15:12:07 +0800 Subject: [PATCH] =?UTF-8?q?fix(chat):=20=E5=88=A0=E9=99=A4=E4=BC=9A?= =?UTF-8?q?=E8=AF=9D=E6=9C=9F=E9=97=B4=E9=98=BB=E6=AD=A2=E5=8F=91=E9=80=81?= =?UTF-8?q?=E5=92=8C=E9=87=8D=E5=A4=8D=E5=88=A0=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/stores/chat.spec.ts | 61 ++++++++++++++++++++++++++++++-- frontend/src/stores/chat.ts | 8 ++++- 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/frontend/src/stores/chat.spec.ts b/frontend/src/stores/chat.spec.ts index 1b5e30a..7bf4375 100644 --- a/frontend/src/stores/chat.spec.ts +++ b/frontend/src/stores/chat.spec.ts @@ -23,8 +23,9 @@ const page = { total: 0, limit: 100, offset: 0 } function deferred() { let resolve!: (value: T) => void - const promise = new Promise(done => { resolve = done }) - return { promise, resolve } + let reject!: (reason: Error) => void + const promise = new Promise((done, fail) => { resolve = done; reject = fail }) + return { promise, resolve, reject } } beforeEach(() => { @@ -224,3 +225,59 @@ it('ignores old history after switching to a new conversation and sending', asyn expect(store.messages.map(m => m.content)).toEqual(['new question', '']) expect(store.isStreaming).toBe(true) }) + +it.each(['success', 'failure'])('blocks sends and duplicate deletes until deletion ends with %s', async outcome => { + const store = useChatStore() + store.selectedProviderId = 'real' + store.selectedModel = 'model' + await store.createNewConversation() + const id = store.activeConversationId! + const removal = deferred>>() + vi.mocked(removeConversation).mockReturnValueOnce(removal.promise) + const deleting = store.deleteConversation(id) + store.inputText = 'keep this draft' + expect(store.canSend).toBe(false) + await store.sendMessage(store.inputText) + await store.deleteConversation(id) + expect(streamChat).not.toHaveBeenCalled() + expect(removeConversation).toHaveBeenCalledTimes(1) + expect(store.inputText).toBe('keep this draft') + if (outcome === 'success') removal.resolve(undefined) + else removal.reject(new Error('offline')) + await deleting + expect(store.isStreaming).toBe(false) + expect(store.canSend).toBe(true) + expect(store.activeConversationId).toBe(outcome === 'success' ? null : id) + await store.sendMessage(store.inputText) + expect(streamChat).toHaveBeenCalledTimes(1) + const request = vi.mocked(streamChat).mock.calls[0]![0] + if (outcome === 'success') expect(request.conversation_id).not.toBe(id) + else expect(request.conversation_id).toBe(id) +}) + +it('keeps a deleting conversation blocked after reselecting it without blocking other conversations', async () => { + const store = useChatStore() + store.selectedProviderId = 'real' + store.selectedModel = 'model' + await store.createNewConversation() + const a = store.activeConversationId! + await store.createNewConversation() + const b = store.activeConversationId! + const removal = deferred>>() + vi.mocked(removeConversation).mockReturnValueOnce(removal.promise) + const deleting = store.deleteConversation(a) + await store.setActiveConversation(a) + expect(store.canSend).toBe(false) + await store.sendMessage('blocked') + expect(streamChat).not.toHaveBeenCalled() + await store.setActiveConversation(b) + expect(store.canSend).toBe(true) + await store.sendMessage('belongs to b') + const client = vi.mocked(streamChat).mock.results[0]!.value as SseClient + removal.resolve(undefined) + await deleting + expect(store.activeConversationId).toBe(b) + expect(store.messages[0]?.content).toBe('belongs to b') + expect(store.isStreaming).toBe(true) + expect(client.cancel).not.toHaveBeenCalled() +}) diff --git a/frontend/src/stores/chat.ts b/frontend/src/stores/chat.ts index 68ad589..c5af1d7 100644 --- a/frontend/src/stores/chat.ts +++ b/frontend/src/stores/chat.ts @@ -18,7 +18,9 @@ export const useChatStore = defineStore('chat', () => { const isStreaming = ref(false) const isPreparing = ref(false) const messagesReady = ref(true) - const canSend = computed(() => messagesReady.value && !isPreparing.value && !isStreaming.value) + const deletingConversations = reactive(new Set()) + const canSend = computed(() => messagesReady.value && !isPreparing.value && !isStreaming.value + && (!activeConversationId.value || !deletingConversations.has(activeConversationId.value))) const inputText = ref('') const useRag = ref(true) const selectedSkillId = ref(null) @@ -262,6 +264,8 @@ export const useChatStore = defineStore('chat', () => { } async function deleteConversation(id: string) { + if (deletingConversations.has(id)) return + deletingConversations.add(id) if (activeConversationId.value === id) stopGeneration() historyError.value = '' try { @@ -275,6 +279,8 @@ export const useChatStore = defineStore('chat', () => { } } catch (error) { historyError.value = error instanceof Error ? error.message : t('会话删除失败', 'Failed to delete conversation') + } finally { + deletingConversations.delete(id) } }