import { ref, reactive } from 'vue' import type { ChatMessage, ToolCallInfo, SessionListItem } from './interface' import { useUserStore } from '@/stores/user' import { chatDB } from './chatDB' export function useAiChat() { const messages = ref([]) const isStreaming = ref(false) const currentSessionId = ref('') const welcomeMessage = ref('') const sessionList = ref([]) const isLoadingSession = ref(false) const userStore = useUserStore() function getBaseUrl(): string { const base = import.meta.env.BASE_URL || '/' return `${base}api` } // ============ 会话列表管理 ============ // 获取会话列表 async function fetchSessionList() { try { const resp = await fetch(`${getBaseUrl()}/ai/sessions/list`, { headers: { Authorization: `Bearer ${userStore.token}` }, }) if (resp.ok) { const json = await resp.json() if (json.data) { sessionList.value = json.data // 清理本地缓存中不存在于后端列表的会话 const serverIds = new Set(json.data.map((s: any) => s.sessionId)) const localIds = await chatDB.getAllSessionIds() for (const localId of localIds) { if (!serverIds.has(localId)) { await chatDB.deleteSession(localId) } } } } } catch { // 忽略 } } // 获取当前会话信息 async function fetchSession() { isLoadingSession.value = true try { const resp = await fetch(`${getBaseUrl()}/ai/sessions`, { headers: { Authorization: `Bearer ${userStore.token}` }, }) if (resp.ok) { const json = await resp.json() if (json.data?.welcomeMessage) { welcomeMessage.value = json.data.welcomeMessage } if (json.data?.sessionId) { currentSessionId.value = json.data.sessionId // 从 IndexedDB 恢复本地消息 await loadLocalMessages(json.data.sessionId) } } } catch { // 忽略 } finally { isLoadingSession.value = false } // 同时获取会话列表 await fetchSessionList() } // 创建新会话 async function createNewSession() { isLoadingSession.value = true try { const resp = await fetch(`${getBaseUrl()}/ai/sessions/new`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${userStore.token}`, }, }) if (resp.ok) { const json = await resp.json() if (json.data?.sessionId) { // 保存当前会话消息到 IndexedDB if (currentSessionId.value && messages.value.length > 0) { await saveLocalMessages() } // 切换到新会话 currentSessionId.value = json.data.sessionId messages.value = [] welcomeMessage.value = json.data.welcomeMessage || '欢迎使用AI助手' // 刷新列表 await fetchSessionList() } } } catch { // 忽略 } finally { isLoadingSession.value = false } } // 加载历史会话 async function loadSession(sessionId: string) { if (sessionId === currentSessionId.value) return isLoadingSession.value = true try { // 保存当前会话消息到 IndexedDB if (currentSessionId.value && messages.value.length > 0) { await saveLocalMessages() } // 先尝试从 IndexedDB 缓存加载 const cached = await chatDB.getSession(sessionId) if (cached && cached.messages.length > 0) { currentSessionId.value = sessionId messages.value = cached.messages welcomeMessage.value = '' // 后台静默调用 session/load 恢复 Kiro 上下文(不等待回放结果) fetch(`${getBaseUrl()}/ai/sessions/${sessionId}/load`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${userStore.token}`, }, }) await fetchSessionList() } else { // 无缓存:调用 session/load 获取回放消息 const resp = await fetch(`${getBaseUrl()}/ai/sessions/${sessionId}/load`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${userStore.token}`, }, }) if (resp.ok) { const json = await resp.json() if (json.data?.sessionId) { currentSessionId.value = json.data.sessionId welcomeMessage.value = '' // 将回放消息转为 ChatMessage 格式 if (json.data.replayMessages && json.data.replayMessages.length > 0) { messages.value = json.data.replayMessages.map((m: any, i: number) => ({ id: m.messageId || `replay_${i}`, role: m.role, content: m.content, timestamp: Date.now() - (json.data.replayMessages.length - i) * 1000, })) // 保存到缓存 await saveLocalMessages() } else { messages.value = [] } await fetchSessionList() } } } } catch { // 忽略 } finally { isLoadingSession.value = false } } // 删除会话 async function deleteSession(sessionId: string) { try { const resp = await fetch(`${getBaseUrl()}/ai/sessions/${sessionId}`, { method: 'DELETE', headers: { Authorization: `Bearer ${userStore.token}` }, }) if (resp.ok) { // 清除 IndexedDB 中的消息 await chatDB.deleteSession(sessionId) // 如果删的是当前会话,清空消息 if (sessionId === currentSessionId.value) { messages.value = [] currentSessionId.value = '' } // 刷新列表 await fetchSessionList() // 如果没有任何会话了,自动创建新会话 if (sessionList.value.length === 0) { await createNewSession() } } } catch { // 忽略 } } // ============ IndexedDB 本地消息管理 ============ async function saveLocalMessages() { if (!currentSessionId.value || messages.value.length === 0) return const title = getSessionTitle() await chatDB.saveSession(currentSessionId.value, title, toRaw(messages.value)) } async function loadLocalMessages(sessionId: string) { const session = await chatDB.getSession(sessionId) if (session) { messages.value = session.messages } else { messages.value = [] } } function getSessionTitle(): string { const firstUserMsg = messages.value.find((m) => m.role === 'user') if (firstUserMsg) { const text = firstUserMsg.content return text.length > 30 ? `${text.slice(0, 30)}...` : text } return '新对话' } // ============ 消息发送 ============ function generateId(): string { return `${Date.now()}-${Math.random().toString(36).slice(2)}` } async function sendMessage(content: string) { if (!content.trim() || isStreaming.value) return // 添加用户消息 messages.value.push({ id: generateId(), role: 'user', content: content.trim(), timestamp: Date.now(), }) // 添加 AI 消息占位 messages.value.push({ id: generateId(), role: 'assistant', content: '', toolCalls: [], timestamp: Date.now(), }) const aiMessage: ChatMessage = messages.value.at(-1)! isStreaming.value = true try { const response = await fetch(`${getBaseUrl()}/ai/chat`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${userStore.token}`, }, body: JSON.stringify({ message: content.trim(), sessionId: currentSessionId.value, }), }) if (!response.ok) { aiMessage.content = `错误: ${response.status} ${response.statusText}` isStreaming.value = false return } const reader = response.body?.getReader() if (!reader) { aiMessage.content = '错误: 无法读取响应流' isStreaming.value = false return } const decoder = new TextDecoder() let buffer = '' while (true) { const { done, value } = await reader.read() if (done) break buffer += decoder.decode(value, { stream: true }) // 按 \n\n 分割 SSE 事件 const parts = buffer.split('\n\n') buffer = parts.pop() || '' for (const part of parts) { if (!part.trim()) continue parseSSEEvent(part, aiMessage) } } // 处理剩余 buffer if (buffer.trim()) { parseSSEEvent(buffer, aiMessage) } } catch (err: any) { aiMessage.content += `\n\n[连接错误: ${err.message || '未知错误'}]` } finally { isStreaming.value = false // 每次消息完成后保存到 IndexedDB await saveLocalMessages() } } function parseSSEEvent(raw: string, aiMessage: ChatMessage) { const lines = raw.split('\n') let eventType = '' let data = '' for (const line of lines) { if (line.startsWith('event: ')) { eventType = line.slice(7) } else if (line.startsWith('data: ')) { data = line.slice(6) } } if (!data) return try { const payload = JSON.parse(data) switch (eventType || payload.type) { case 'text': aiMessage.content += payload.content || '' break case 'tool_call': if (!aiMessage.toolCalls) aiMessage.toolCalls = [] aiMessage.toolCalls.push({ name: payload.name || '', status: 'running', }) break case 'tool_update': { const tc = aiMessage.toolCalls?.find((t: ToolCallInfo) => t.name === payload.name) if (tc) tc.status = payload.status || 'completed' break } case 'permission': { const permData = typeof payload.content === 'string' ? JSON.parse(payload.content) : payload.content aiMessage.permission = { requestId: permData.requestId, title: permData.title, options: permData.options, resolved: false, } break } case 'file_created': { const fileData = typeof payload.content === 'string' ? JSON.parse(payload.content) : payload.content if (!aiMessage.files) aiMessage.files = [] aiMessage.files.push({ key: fileData.key, fileName: fileData.fileName, size: fileData.size, }) break } case 'done': if (payload.sessionId) { currentSessionId.value = payload.sessionId } break case 'welcome': welcomeMessage.value = payload.content || '' break case 'error': aiMessage.content += `\n\n[错误: ${payload.content || '未知'}]` break } } catch { // 非 JSON 数据忽略 } } async function resolvePermission(optionId: string) { try { await fetch(`${getBaseUrl()}/ai/permission`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${userStore.token}`, }, body: JSON.stringify({ optionId }), }) const lastMsg = messages.value[messages.value.length - 1] if (lastMsg?.permission) { lastMsg.permission.resolved = true } } catch { // 忽略 } } async function cancelChat() { try { await fetch(`${getBaseUrl()}/ai/cancel`, { method: 'POST', headers: { Authorization: `Bearer ${userStore.token}` }, }) } catch { // 忽略 } } function clearMessages() { messages.value = [] if (currentSessionId.value) { chatDB.deleteSession(currentSessionId.value) } } return { messages, isStreaming, currentSessionId, welcomeMessage, sessionList, isLoadingSession, sendMessage, cancelChat, clearMessages, resolvePermission, fetchSession, fetchSessionList, createNewSession, loadSession, deleteSession, } }