import { defineStore } from "pinia"; import { ref } from "vue"; import { createConversation, fetchConversation, fetchConversations, withdrawConversationMessage, streamConversationMessage, sendConversationMessage, } from "@/api/conversations"; import type { Conversation, ConversationSummary } from "@/types/conversation"; type ConversationMessageType = "text" | "image" | "file" | "audio"; export const useConversationStore = defineStore("conversation", () => { const items = ref([]); const current = ref(null); const loading = ref(false); const sending = ref(false); const loadingOlder = ref(false); const error = ref(null); const generating = ref(false); let activeController: AbortController | null = null; let activeUserMessageId: number | null = null; let listRequest: Promise | null = null; let listRequestSearch: string | undefined; let listLoadedAt = 0; async function loadList(search?: string) { const normalizedSearch = search?.trim() || undefined; if (listRequest && listRequestSearch === normalizedSearch) return listRequest; if ( !normalizedSearch && items.value.length && Date.now() - listLoadedAt < 30_000 ) return; loading.value = true; error.value = null; listRequestSearch = normalizedSearch; listRequest = (async () => { try { const response = await fetchConversations(50, normalizedSearch); items.value = response.data.data ?? []; if (!normalizedSearch) listLoadedAt = Date.now(); } catch { error.value = "历史对话加载失败"; } finally { loading.value = false; listRequest = null; listRequestSearch = undefined; } })(); return listRequest; } async function open(id: number) { loading.value = true; error.value = null; try { const response = await fetchConversation(id); if (!response.data.data) throw new Error("对话响应为空"); current.value = response.data.data; } catch (reason) { error.value = reason instanceof Error ? reason.message : "对话加载失败"; throw reason; } finally { loading.value = false; } } async function loadOlder() { if ( loadingOlder.value || !current.value || !current.value.messages.length || !current.value.messagesTotal ) { return false; } const offset = current.value.messagesOffset + current.value.messages.length; if (!current.value.hasMoreMessages) return false; loadingOlder.value = true; try { const response = await fetchConversation(current.value.id, offset); if (!response.data.data) throw new Error("历史消息响应为空"); const older = response.data.data; current.value = { ...current.value, messages: [...older.messages, ...current.value.messages], messagesOffset: 0, hasMoreMessages: older.hasMoreMessages, messagesTotal: older.messagesTotal, }; return true; } finally { loadingOlder.value = false; } } async function create(title?: string) { const response = await createConversation(title); if (!response.data.data) throw new Error("新建对话响应为空"); current.value = response.data.data; items.value = [toSummary(response.data.data), ...items.value]; return current.value; } async function send( content: string, messageType: ConversationMessageType = "text", ) { if (!current.value) await create(); if (!current.value) throw new Error("当前对话不存在"); sending.value = true; error.value = null; try { const response = await sendConversationMessage( current.value.id, content, messageType, ); if (!response.data.data) throw new Error("消息响应为空"); current.value = response.data.data; const index = items.value.findIndex( (item) => item.id === current.value?.id, ); const summary = toSummary(current.value); 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; } catch (reason) { error.value = reason instanceof Error ? reason.message : "消息发送失败"; throw reason; } finally { sending.value = false; } } async function streamSend( content: string, onDelta: (delta: string) => void, messageType: ConversationMessageType = "text", ) { if (!current.value) await create(); if (!current.value) throw new Error("当前对话不存在"); sending.value = true; generating.value = true; activeController = new AbortController(); error.value = null; const startedAt = new Date().toISOString(); const userMessage = { id: -Date.now(), role: "user" as const, messageType, content, riskResult: null, responseCard: null, semantic: null, createdAt: startedAt, }; activeUserMessageId = userMessage.id; const assistantMessage = { id: userMessage.id - 1, role: "assistant" as const, messageType: "text" as const, content: "", riskResult: null, responseCard: null, semantic: null as Record | null, createdAt: startedAt, }; current.value = { ...current.value, messages: [...current.value.messages, userMessage, assistantMessage], messageCount: current.value.messageCount + 2, messagesTotal: current.value.messagesTotal + 2, }; try { const conversationId = current.value.id; await streamConversationMessage( conversationId, content, (event) => { if ( event.type === "start" && event.data && typeof event.data === "object" && "messageId" in event.data && typeof event.data.messageId === "number" && current.value ) { const serverMessageId = event.data.messageId; const messages = current.value.messages.map((message) => message.id === userMessage.id ? { ...message, id: serverMessageId } : message, ); activeUserMessageId = serverMessageId; current.value = { ...current.value, 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, semantic: event.data as Record, }; current.value = { ...current.value, 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 === "error") throw new Error(event.message || "AI 回复失败"); if (event.type === "done" && event.data) current.value = event.data as Conversation; }, messageType, activeController.signal, ); if ( !current.value || !current.value.messages.some( (message) => message.role === "assistant" && message.content.trim(), ) ) { throw new Error("AI 回复为空"); } const summary = toSummary(current.value); 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; } 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 the request was cancelled before persistence, remove both // temporary messages from the local conversation. if (current.value) { const messages = current.value.messages.filter( (message) => message.id !== assistantMessage.id && message.id !== userMessage.id, ); current.value = { ...current.value, messages, messageCount: Math.max(0, current.value.messageCount - 2), messagesTotal: Math.max(0, current.value.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; } if (current.value) { const messages = current.value.messages.filter( (message) => message.id !== assistantMessage.id && message.id !== userMessage.id, ); current.value = { ...current.value, messages, messageCount: Math.max(0, current.value.messageCount - 2), messagesTotal: Math.max(0, current.value.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. } } error.value = reason instanceof Error ? reason.message : "AI 回复失败"; throw reason; } finally { sending.value = false; generating.value = false; activeController = null; activeUserMessageId = null; } } function stopGeneration() { activeController?.abort(); } function isGeneratingMessage(messageId: number) { return activeUserMessageId === messageId && generating.value; } async function withdraw(messageId: number) { if (!current.value) return; loading.value = true; error.value = null; try { const response = await withdrawConversationMessage( current.value.id, messageId, ); if (!response.data.data) throw new Error("撤回响应为空"); current.value = response.data.data; const index = items.value.findIndex( (item) => item.id === current.value?.id, ); const summary = toSummary(current.value); if (index >= 0) items.value.splice(index, 1, summary); else items.value.unshift(summary); return current.value; } catch (reason) { error.value = reason instanceof Error ? reason.message : "撤回失败"; throw reason; } finally { loading.value = false; } } function reset() { current.value = null; error.value = null; } function toSummary(conversation: Conversation): ConversationSummary { return { id: conversation.id, title: conversation.title, messageCount: conversation.messageCount, lastRiskLevel: conversation.lastRiskLevel, updatedAt: conversation.updatedAt, }; } return { items, current, loading, sending, generating, loadingOlder, error, loadList, open, loadOlder, create, send, streamSend, stopGeneration, isGeneratingMessage, withdraw, reset, }; });