|
@@ -1,7 +1,16 @@
|
|
|
-import { ref, reactive } from 'vue'
|
|
|
|
|
|
|
+import { ref } from 'vue'
|
|
|
import type { ChatMessage, ToolCallInfo, SessionListItem } from './interface'
|
|
import type { ChatMessage, ToolCallInfo, SessionListItem } from './interface'
|
|
|
import { useUserStore } from '@/stores/user'
|
|
import { useUserStore } from '@/stores/user'
|
|
|
import { chatDB } from './chatDB'
|
|
import { chatDB } from './chatDB'
|
|
|
|
|
+import {
|
|
|
|
|
+ getAiSession,
|
|
|
|
|
+ getAiSessionList,
|
|
|
|
|
+ createAiSession,
|
|
|
|
|
+ loadAiSession,
|
|
|
|
|
+ deleteAiSession,
|
|
|
|
|
+ cancelAiChat,
|
|
|
|
|
+ resolveAiPermission,
|
|
|
|
|
+} from '@/service/aiChat'
|
|
|
|
|
|
|
|
export function useAiChat() {
|
|
export function useAiChat() {
|
|
|
const messages = ref<ChatMessage[]>([])
|
|
const messages = ref<ChatMessage[]>([])
|
|
@@ -23,25 +32,21 @@ export function useAiChat() {
|
|
|
// 获取会话列表
|
|
// 获取会话列表
|
|
|
async function fetchSessionList() {
|
|
async function fetchSessionList() {
|
|
|
try {
|
|
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)
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ const data = await getAiSessionList()
|
|
|
|
|
+ if (data) {
|
|
|
|
|
+ sessionList.value = data
|
|
|
|
|
+
|
|
|
|
|
+ // 清理本地缓存中不存在于后端列表的会话
|
|
|
|
|
+ const serverIds = new Set(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 {
|
|
|
|
|
|
|
+ }
|
|
|
|
|
+ catch {
|
|
|
// 忽略
|
|
// 忽略
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -50,19 +55,14 @@ export function useAiChat() {
|
|
|
async function fetchSession() {
|
|
async function fetchSession() {
|
|
|
isLoadingSession.value = true
|
|
isLoadingSession.value = true
|
|
|
try {
|
|
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)
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ const data = await getAiSession()
|
|
|
|
|
+ if (data?.welcomeMessage) {
|
|
|
|
|
+ welcomeMessage.value = data.welcomeMessage
|
|
|
|
|
+ }
|
|
|
|
|
+ if (data?.sessionId) {
|
|
|
|
|
+ currentSessionId.value = data.sessionId
|
|
|
|
|
+ // 从 IndexedDB 恢复本地消息
|
|
|
|
|
+ await loadLocalMessages(data.sessionId)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
catch {
|
|
catch {
|
|
@@ -79,27 +79,18 @@ export function useAiChat() {
|
|
|
async function createNewSession() {
|
|
async function createNewSession() {
|
|
|
isLoadingSession.value = true
|
|
isLoadingSession.value = true
|
|
|
try {
|
|
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()
|
|
|
|
|
|
|
+ const data = await createAiSession()
|
|
|
|
|
+ if (data?.sessionId) {
|
|
|
|
|
+ // 保存当前会话消息到 IndexedDB
|
|
|
|
|
+ if (currentSessionId.value && messages.value.length > 0) {
|
|
|
|
|
+ await saveLocalMessages()
|
|
|
}
|
|
}
|
|
|
|
|
+ // 切换到新会话
|
|
|
|
|
+ currentSessionId.value = data.sessionId
|
|
|
|
|
+ messages.value = []
|
|
|
|
|
+ welcomeMessage.value = data.welcomeMessage || '欢迎使用AI助手'
|
|
|
|
|
+ // 刷新列表
|
|
|
|
|
+ await fetchSessionList()
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
catch {
|
|
catch {
|
|
@@ -129,50 +120,39 @@ export function useAiChat() {
|
|
|
welcomeMessage.value = ''
|
|
welcomeMessage.value = ''
|
|
|
|
|
|
|
|
// 后台静默调用 session/load 恢复 Kiro 上下文(不等待回放结果)
|
|
// 后台静默调用 session/load 恢复 Kiro 上下文(不等待回放结果)
|
|
|
- fetch(`${getBaseUrl()}/ai/sessions/${sessionId}/load`, {
|
|
|
|
|
- method: 'POST',
|
|
|
|
|
- headers: {
|
|
|
|
|
- 'Content-Type': 'application/json',
|
|
|
|
|
- Authorization: `Bearer ${userStore.token}`,
|
|
|
|
|
- },
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ loadAiSession(sessionId)
|
|
|
await fetchSessionList()
|
|
await fetchSessionList()
|
|
|
- } else {
|
|
|
|
|
|
|
+ }
|
|
|
|
|
+ else {
|
|
|
// 无缓存:调用 session/load 获取回放消息
|
|
// 无缓存:调用 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()
|
|
|
|
|
|
|
+ const data = await loadAiSession(sessionId)
|
|
|
|
|
+ if (data?.sessionId) {
|
|
|
|
|
+ currentSessionId.value = data.sessionId
|
|
|
|
|
+ welcomeMessage.value = ''
|
|
|
|
|
+
|
|
|
|
|
+ // 将回放消息转为 ChatMessage 格式
|
|
|
|
|
+ if ((data as any).replayMessages && (data as any).replayMessages.length > 0) {
|
|
|
|
|
+ messages.value = (data as any).replayMessages.map((m: any, i: number) => ({
|
|
|
|
|
+ id: m.messageId || `replay_${i}`,
|
|
|
|
|
+ role: m.role,
|
|
|
|
|
+ content: m.content,
|
|
|
|
|
+ timestamp: Date.now() - ((data as any).replayMessages.length - i) * 1000,
|
|
|
|
|
+ }))
|
|
|
|
|
+ // 保存到缓存
|
|
|
|
|
+ await saveLocalMessages()
|
|
|
|
|
+ }
|
|
|
|
|
+ else {
|
|
|
|
|
+ messages.value = []
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ await fetchSessionList()
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- } catch {
|
|
|
|
|
|
|
+ }
|
|
|
|
|
+ catch {
|
|
|
// 忽略
|
|
// 忽略
|
|
|
- } finally {
|
|
|
|
|
|
|
+ }
|
|
|
|
|
+ finally {
|
|
|
isLoadingSession.value = false
|
|
isLoadingSession.value = false
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -180,25 +160,20 @@ export function useAiChat() {
|
|
|
// 删除会话
|
|
// 删除会话
|
|
|
async function deleteSession(sessionId: string) {
|
|
async function deleteSession(sessionId: string) {
|
|
|
try {
|
|
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()
|
|
|
|
|
|
|
+ await deleteAiSession(sessionId)
|
|
|
|
|
+ // 清除 IndexedDB 中的消息
|
|
|
|
|
+ await chatDB.deleteSession(sessionId)
|
|
|
|
|
+ // 如果删的是当前会话,清空消息
|
|
|
|
|
+ if (sessionId === currentSessionId.value) {
|
|
|
|
|
+ messages.value = []
|
|
|
|
|
+ currentSessionId.value = ''
|
|
|
|
|
+ }
|
|
|
|
|
+ // 刷新列表
|
|
|
|
|
+ await fetchSessionList()
|
|
|
|
|
|
|
|
- // 如果没有任何会话了,自动创建新会话
|
|
|
|
|
- if (sessionList.value.length === 0) {
|
|
|
|
|
- await createNewSession()
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // 如果没有任何会话了,自动创建新会话
|
|
|
|
|
+ if (sessionList.value.length === 0) {
|
|
|
|
|
+ await createNewSession()
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
catch {
|
|
catch {
|
|
@@ -338,9 +313,24 @@ export function useAiChat() {
|
|
|
const payload = JSON.parse(data)
|
|
const payload = JSON.parse(data)
|
|
|
|
|
|
|
|
switch (eventType || payload.type) {
|
|
switch (eventType || payload.type) {
|
|
|
- case 'text':
|
|
|
|
|
- aiMessage.content += payload.content || ''
|
|
|
|
|
|
|
+ case 'text': {
|
|
|
|
|
+ const text = payload.content || ''
|
|
|
|
|
+ // 实时检测 $$FILE_DOWNLOAD:{...}$$ 标记
|
|
|
|
|
+ const fileRegex = /\$\$FILE_DOWNLOAD:(.*?)\$\$/g
|
|
|
|
|
+ let match
|
|
|
|
|
+ let cleanText = text
|
|
|
|
|
+ while ((match = fileRegex.exec(text)) !== null) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const fileData = JSON.parse(match[1])
|
|
|
|
|
+ if (!aiMessage.files) aiMessage.files = []
|
|
|
|
|
+ aiMessage.files.push({ key: fileData.key, fileName: fileData.fileName, size: fileData.size })
|
|
|
|
|
+ }
|
|
|
|
|
+ catch { /* ignore malformed */ }
|
|
|
|
|
+ cleanText = cleanText.replace(match[0], '')
|
|
|
|
|
+ }
|
|
|
|
|
+ aiMessage.content += cleanText
|
|
|
break
|
|
break
|
|
|
|
|
+ }
|
|
|
case 'tool_call':
|
|
case 'tool_call':
|
|
|
if (!aiMessage.toolCalls) aiMessage.toolCalls = []
|
|
if (!aiMessage.toolCalls) aiMessage.toolCalls = []
|
|
|
aiMessage.toolCalls.push({
|
|
aiMessage.toolCalls.push({
|
|
@@ -364,21 +354,12 @@ export function useAiChat() {
|
|
|
}
|
|
}
|
|
|
break
|
|
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':
|
|
case 'done':
|
|
|
if (payload.sessionId) {
|
|
if (payload.sessionId) {
|
|
|
currentSessionId.value = payload.sessionId
|
|
currentSessionId.value = payload.sessionId
|
|
|
}
|
|
}
|
|
|
|
|
+ // 最终扫描:处理跨 chunk 的标记
|
|
|
|
|
+ extractFileDownloads(aiMessage)
|
|
|
break
|
|
break
|
|
|
case 'welcome':
|
|
case 'welcome':
|
|
|
welcomeMessage.value = payload.content || ''
|
|
welcomeMessage.value = payload.content || ''
|
|
@@ -392,32 +373,48 @@ export function useAiChat() {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // 最终扫描:处理跨 chunk 拆分导致实时匹配遗漏的标记
|
|
|
|
|
+ function extractFileDownloads(aiMessage: ChatMessage) {
|
|
|
|
|
+ const fileRegex = /\$\$FILE_DOWNLOAD:(.*?)\$\$/g
|
|
|
|
|
+ let match
|
|
|
|
|
+ let hasMatch = false
|
|
|
|
|
+ while ((match = fileRegex.exec(aiMessage.content)) !== null) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const fileData = JSON.parse(match[1])
|
|
|
|
|
+ if (!aiMessage.files) aiMessage.files = []
|
|
|
|
|
+ // 避免重复添加(实时匹配可能已添加)
|
|
|
|
|
+ const exists = aiMessage.files.some(f => f.key === fileData.key)
|
|
|
|
|
+ if (!exists) {
|
|
|
|
|
+ aiMessage.files.push({ key: fileData.key, fileName: fileData.fileName, size: fileData.size })
|
|
|
|
|
+ }
|
|
|
|
|
+ hasMatch = true
|
|
|
|
|
+ }
|
|
|
|
|
+ catch { /* ignore */ }
|
|
|
|
|
+ }
|
|
|
|
|
+ // 清除内容中的标记文本
|
|
|
|
|
+ if (hasMatch) {
|
|
|
|
|
+ aiMessage.content = aiMessage.content.replace(/\$\$FILE_DOWNLOAD:.*?\$\$/g, '').trim()
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
async function resolvePermission(optionId: string) {
|
|
async function resolvePermission(optionId: string) {
|
|
|
try {
|
|
try {
|
|
|
- await fetch(`${getBaseUrl()}/ai/permission`, {
|
|
|
|
|
- method: 'POST',
|
|
|
|
|
- headers: {
|
|
|
|
|
- 'Content-Type': 'application/json',
|
|
|
|
|
- Authorization: `Bearer ${userStore.token}`,
|
|
|
|
|
- },
|
|
|
|
|
- body: JSON.stringify({ optionId }),
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ await resolveAiPermission(optionId)
|
|
|
const lastMsg = messages.value[messages.value.length - 1]
|
|
const lastMsg = messages.value[messages.value.length - 1]
|
|
|
if (lastMsg?.permission) {
|
|
if (lastMsg?.permission) {
|
|
|
lastMsg.permission.resolved = true
|
|
lastMsg.permission.resolved = true
|
|
|
}
|
|
}
|
|
|
- } catch {
|
|
|
|
|
|
|
+ }
|
|
|
|
|
+ catch {
|
|
|
// 忽略
|
|
// 忽略
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
async function cancelChat() {
|
|
async function cancelChat() {
|
|
|
try {
|
|
try {
|
|
|
- await fetch(`${getBaseUrl()}/ai/cancel`, {
|
|
|
|
|
- method: 'POST',
|
|
|
|
|
- headers: { Authorization: `Bearer ${userStore.token}` },
|
|
|
|
|
- })
|
|
|
|
|
- } catch {
|
|
|
|
|
|
|
+ await cancelAiChat()
|
|
|
|
|
+ }
|
|
|
|
|
+ catch {
|
|
|
// 忽略
|
|
// 忽略
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|