From 73f574add4d8c62c7619acb36aea2dd0d295bdef Mon Sep 17 00:00:00 2001 From: kx_guo Date: Fri, 11 Sep 2026 17:06:31 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=A4=9A=E4=BC=9A=E8=AF=9D?= =?UTF-8?q?=E5=B9=B6=E5=8F=91=E6=B5=81=E5=BC=8F=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/RiskAssistantPage.vue | 11 +- src/stores/conversation.store.ts | 260 +++++++++++++++++++------------ 2 files changed, 164 insertions(+), 107 deletions(-) diff --git a/src/pages/RiskAssistantPage.vue b/src/pages/RiskAssistantPage.vue index 2cc3d90..1aca3a8 100644 --- a/src/pages/RiskAssistantPage.vue +++ b/src/pages/RiskAssistantPage.vue @@ -390,7 +390,7 @@ async function submit( const value = prompt.value.trim(); if (!value || loading.value) return; prompt.value = ""; - await conversationStore.streamSend( + const sentConversation = await conversationStore.streamSend( value, (delta) => { void delta; @@ -398,15 +398,16 @@ async function submit( messageType, ); if ( - conversation.value && - route.query.conversation !== String(conversation.value.id) + sentConversation && + conversation.value?.id === sentConversation.id && + route.query.conversation !== String(sentConversation.id) ) { await router.replace({ name: "assessment", - query: { conversation: String(conversation.value.id) }, + query: { conversation: String(sentConversation.id) }, }); } - await scrollToBottom(); + if (conversation.value?.id === sentConversation?.id) await scrollToBottom(); } async function scrollToBottom(behavior: "smooth" | "auto" = "smooth") { diff --git a/src/stores/conversation.store.ts b/src/stores/conversation.store.ts index 0b6eeda..6ea9b66 100644 --- a/src/stores/conversation.store.ts +++ b/src/stores/conversation.store.ts @@ -1,5 +1,5 @@ import { defineStore } from "pinia"; -import { ref } from "vue"; +import { computed, reactive, ref } from "vue"; import { createConversation, @@ -17,12 +17,19 @@ export const useConversationStore = defineStore("conversation", () => { const items = ref([]); const current = ref(null); const loading = ref(false); - const sending = ref(false); + const plainSending = ref(false); const loadingOlder = ref(false); const error = ref(null); - const generating = ref(false); - let activeController: AbortController | null = null; - let activeUserMessageId: number | null = null; + type StreamState = { + controller: AbortController; + userMessageId: number; + assistantMessageId: number; + serverUserMessageId: number | null; + }; + const streamStates = reactive(new Map()); + const conversationCache = reactive(new Map()); + const sending = computed(() => plainSending.value || Boolean(current.value && streamStates.has(current.value.id))); + const generating = sending; let listRequest: Promise | null = null; let listRequestSearch: string | undefined; let listLoadedAt = 0; @@ -62,7 +69,19 @@ export const useConversationStore = defineStore("conversation", () => { try { const response = await fetchConversation(id); if (!response.data.data) throw new Error("对话响应为空"); - current.value = response.data.data; + const remote = response.data.data; + const cached = conversationCache.get(id); + const stream = streamStates.get(id); + const merged = stream && cached + ? { + ...remote, + messages: mergePendingMessages(remote.messages, cached.messages, stream), + messageCount: Math.max(remote.messageCount, cached.messageCount), + messagesTotal: Math.max(remote.messagesTotal, cached.messagesTotal), + } + : remote; + conversationCache.set(id, merged); + current.value = merged; } catch (reason) { error.value = reason instanceof Error ? reason.message : "对话加载失败"; throw reason; @@ -80,20 +99,24 @@ export const useConversationStore = defineStore("conversation", () => { ) { return false; } - const offset = current.value.messagesOffset + current.value.messages.length; + const conversationId = current.value.id; + const base = conversationCache.get(conversationId) ?? current.value; + const offset = base.messagesOffset + base.messages.length; if (!current.value.hasMoreMessages) return false; loadingOlder.value = true; try { - const response = await fetchConversation(current.value.id, offset); + const response = await fetchConversation(conversationId, offset); if (!response.data.data) throw new Error("历史消息响应为空"); const older = response.data.data; - current.value = { - ...current.value, - messages: [...older.messages, ...current.value.messages], + const updated = { + ...base, + messages: [...older.messages, ...base.messages], messagesOffset: 0, hasMoreMessages: older.hasMoreMessages, messagesTotal: older.messagesTotal, }; + conversationCache.set(conversationId, updated); + if (current.value?.id === conversationId) current.value = updated; return true; } finally { loadingOlder.value = false; @@ -103,6 +126,7 @@ export const useConversationStore = defineStore("conversation", () => { async function create(title?: string) { const response = await createConversation(title); if (!response.data.data) throw new Error("新建对话响应为空"); + conversationCache.set(response.data.data.id, response.data.data); current.value = response.data.data; items.value = [toSummary(response.data.data), ...items.value]; return current.value; @@ -114,29 +138,30 @@ export const useConversationStore = defineStore("conversation", () => { ) { if (!current.value) await create(); if (!current.value) throw new Error("当前对话不存在"); - sending.value = true; + const conversationId = current.value.id; + plainSending.value = true; error.value = null; try { const response = await sendConversationMessage( - current.value.id, + conversationId, content, messageType, ); if (!response.data.data) throw new Error("消息响应为空"); - current.value = response.data.data; + setConversation(response.data.data); const index = items.value.findIndex( - (item) => item.id === current.value?.id, + (item) => item.id === conversationId, ); - const summary = toSummary(current.value); + const summary = toSummary(response.data.data); if (index >= 0) items.value.splice(index, 1, summary); else items.value.unshift(summary); items.value.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)); - return current.value; + return response.data.data; } catch (reason) { error.value = reason instanceof Error ? reason.message : "消息发送失败"; throw reason; } finally { - sending.value = false; + plainSending.value = false; } } @@ -147,13 +172,19 @@ export const useConversationStore = defineStore("conversation", () => { ) { if (!current.value) await create(); if (!current.value) throw new Error("当前对话不存在"); - sending.value = true; - generating.value = true; - activeController = new AbortController(); + const conversationId = current.value.id; + if (streamStates.has(conversationId)) throw new Error("当前会话正在生成,请稍候"); + const state: StreamState = { + controller: new AbortController(), + userMessageId: -Date.now(), + assistantMessageId: -Date.now() - 1, + serverUserMessageId: null, + }; + streamStates.set(conversationId, state); error.value = null; const startedAt = new Date().toISOString(); const userMessage = { - id: -Date.now(), + id: state.userMessageId, role: "user" as const, messageType, content, @@ -162,9 +193,8 @@ export const useConversationStore = defineStore("conversation", () => { semantic: null, createdAt: startedAt, }; - activeUserMessageId = userMessage.id; const assistantMessage = { - id: userMessage.id - 1, + id: state.assistantMessageId, role: "assistant" as const, messageType: "text" as const, content: "", @@ -173,14 +203,15 @@ export const useConversationStore = defineStore("conversation", () => { semantic: null as Record | null, createdAt: startedAt, }; - current.value = { + const optimistic = { ...current.value, messages: [...current.value.messages, userMessage, assistantMessage], messageCount: current.value.messageCount + 2, messagesTotal: current.value.messagesTotal + 2, }; + conversationCache.set(conversationId, optimistic); + current.value = optimistic; try { - const conversationId = current.value.id; await streamConversationMessage( conversationId, content, @@ -191,132 +222,156 @@ export const useConversationStore = defineStore("conversation", () => { typeof event.data === "object" && "messageId" in event.data && typeof event.data.messageId === "number" && - current.value + conversationCache.get(conversationId) ) { const serverMessageId = event.data.messageId; - const messages = current.value.messages.map((message) => - message.id === userMessage.id + state.serverUserMessageId = serverMessageId; + const cached = conversationCache.get(conversationId); + if (!cached) return; + const messages = cached.messages.map((message) => + message.id === state.userMessageId ? { ...message, id: serverMessageId } : message, ); - activeUserMessageId = serverMessageId; - current.value = { ...current.value, messages }; + setConversation({ ...cached, messages }); } if (event.type === "delta" && event.content) onDelta(event.content); - if (event.type === "semantic" && event.data && current.value) { - const messages = [...current.value.messages]; - const last = messages[messages.length - 1]; - if (last?.role === "assistant") { - messages[messages.length - 1] = { - ...last, + if (event.type === "semantic" && event.data) { + const cached = conversationCache.get(conversationId); + if (!cached) return; + const messages = cached.messages.map((message) => + message.id === state.assistantMessageId + ? { + ...message, semantic: event.data as Record, - }; - current.value = { ...current.value, messages }; - } + } + : message, + ); + setConversation({ ...cached, messages }); } - if (event.type === "delta" && event.content && current.value) { - const messages = [...current.value.messages]; - const last = messages[messages.length - 1]; - if (last?.role === "assistant") { - messages[messages.length - 1] = { - ...last, - content: last.content + event.content, - }; - current.value = { ...current.value, messages }; - } + if (event.type === "delta" && event.content) { + const cached = conversationCache.get(conversationId); + if (!cached) return; + const messages = cached.messages.map((message) => + message.id === state.assistantMessageId + ? { ...message, content: message.content + event.content } + : message, + ); + setConversation({ ...cached, messages }); } if (event.type === "error") throw new Error(event.message || "AI 回复失败"); if (event.type === "done" && event.data) - current.value = event.data as Conversation; + setConversation(event.data as Conversation); }, messageType, - activeController.signal, + state.controller.signal, ); + const completed = conversationCache.get(conversationId); if ( - !current.value || - !current.value.messages.some( + !completed || + !completed.messages.some( (message) => message.role === "assistant" && message.content.trim(), ) ) { throw new Error("AI 回复为空"); } - const summary = toSummary(current.value); + const summary = toSummary(completed); const index = items.value.findIndex((item) => item.id === summary.id); if (index >= 0) items.value.splice(index, 1, summary); else items.value.unshift(summary); - return current.value; + return completed; } catch (reason) { - const conversationId = current.value?.id; - if (activeController?.signal.aborted) { - if (conversationId) { - try { - const response = await fetchConversation(conversationId); - if (response.data.data) current.value = response.data.data; - } catch { + if (state.controller.signal.aborted) { + try { + const response = await fetchConversation(conversationId); + if (response.data.data) setConversation(response.data.data); + } catch { // If the request was cancelled before persistence, remove both // temporary messages from the local conversation. - if (current.value) { - const messages = current.value.messages.filter( + const cached = conversationCache.get(conversationId); + if (cached) { + const messages = cached.messages.filter( (message) => message.id !== assistantMessage.id && message.id !== userMessage.id, ); - current.value = { - ...current.value, + setConversation({ + ...cached, messages, - messageCount: Math.max(0, current.value.messageCount - 2), - messagesTotal: Math.max(0, current.value.messagesTotal - 2), - }; + messageCount: Math.max(0, cached.messageCount - 2), + messagesTotal: Math.max(0, cached.messagesTotal - 2), + }); } - } - const summary = current.value ? toSummary(current.value) : null; - if (summary) { - const index = items.value.findIndex( - (item) => item.id === summary.id, - ); - if (index >= 0) items.value.splice(index, 1, summary); - else items.value.unshift(summary); - } } - return current.value; + const completed = conversationCache.get(conversationId); + if (completed) upsertSummary(completed); + return completed ?? null; } - if (current.value) { - const messages = current.value.messages.filter( + const cached = conversationCache.get(conversationId); + if (cached) { + const messages = cached.messages.filter( (message) => message.id !== assistantMessage.id && message.id !== userMessage.id, ); - current.value = { - ...current.value, + setConversation({ + ...cached, messages, - messageCount: Math.max(0, current.value.messageCount - 2), - messagesTotal: Math.max(0, current.value.messagesTotal - 2), - }; + messageCount: Math.max(0, cached.messageCount - 2), + messagesTotal: Math.max(0, cached.messagesTotal - 2), + }); } - if (conversationId) { - try { - const response = await fetchConversation(conversationId); - if (response.data.data) current.value = response.data.data; - } catch { - // Keep the local state when recovery is unavailable. - } + try { + const response = await fetchConversation(conversationId); + if (response.data.data) setConversation(response.data.data); + } catch { + // Keep the cached state when recovery is unavailable. } - error.value = reason instanceof Error ? reason.message : "AI 回复失败"; + if (current.value?.id === conversationId) + error.value = reason instanceof Error ? reason.message : "AI 回复失败"; throw reason; } finally { - sending.value = false; - generating.value = false; - activeController = null; - activeUserMessageId = null; + streamStates.delete(conversationId); } } - function stopGeneration() { - activeController?.abort(); + function stopGeneration(conversationId = current.value?.id) { + if (conversationId) streamStates.get(conversationId)?.controller.abort(); } function isGeneratingMessage(messageId: number) { - return activeUserMessageId === messageId && generating.value; + const state = current.value ? streamStates.get(current.value.id) : undefined; + return Boolean(state && (state.userMessageId === messageId || state.serverUserMessageId === messageId)); + } + + function setConversation(conversation: Conversation) { + conversationCache.set(conversation.id, conversation); + if (current.value?.id === conversation.id) current.value = conversation; + upsertSummary(conversation); + } + + function mergePendingMessages( + remoteMessages: Conversation["messages"], + cachedMessages: Conversation["messages"], + state: StreamState, + ) { + const known = new Set(remoteMessages.map((message) => message.id)); + const pending = cachedMessages.filter( + (message) => + (message.id === state.assistantMessageId || + message.id === state.userMessageId || + message.id === state.serverUserMessageId) && + !known.has(message.id), + ); + return [...remoteMessages, ...pending]; + } + + function upsertSummary(conversation: Conversation) { + const summary = toSummary(conversation); + const index = items.value.findIndex((item) => item.id === summary.id); + if (index >= 0) items.value.splice(index, 1, summary); + else items.value.unshift(summary); + items.value.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)); } async function withdraw(messageId: number) { @@ -330,6 +385,7 @@ export const useConversationStore = defineStore("conversation", () => { ); if (!response.data.data) throw new Error("撤回响应为空"); current.value = response.data.data; + conversationCache.set(current.value.id, current.value); const index = items.value.findIndex( (item) => item.id === current.value?.id, );