| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449 |
- 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<ChatMessage[]>([])
- const isStreaming = ref(false)
- const currentSessionId = ref('')
- const welcomeMessage = ref('')
- const sessionList = ref<SessionListItem[]>([])
- 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,
- }
- }
|