更新
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import api from '@/api'
|
||||
|
||||
function parseSseBlock(block) {
|
||||
const lines = block.split('\n')
|
||||
let event = 'message'
|
||||
let data = null
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('event:')) {
|
||||
event = line.slice(6).trim()
|
||||
} else if (line.startsWith('data:')) {
|
||||
const raw = line.slice(5).trim()
|
||||
try {
|
||||
data = JSON.parse(raw)
|
||||
} catch {
|
||||
data = { content: raw }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { event, data }
|
||||
}
|
||||
|
||||
function normalizeList(data) {
|
||||
if (Array.isArray(data)) return data
|
||||
if (data && typeof data === 'object') {
|
||||
if (Array.isArray(data.list)) return data.list
|
||||
return Object.values(data)
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
function isValidMessage(msg) {
|
||||
if (!msg || typeof msg !== 'object') return false
|
||||
const hasContent = !!(msg.content && String(msg.content).trim())
|
||||
const attachments = msg.attachments || []
|
||||
const hasAttachments = Array.isArray(attachments) ? attachments.length > 0 : false
|
||||
return hasContent || hasAttachments
|
||||
}
|
||||
|
||||
export const useChatStore = defineStore('chat', () => {
|
||||
const conversations = ref([])
|
||||
const currentId = ref(null)
|
||||
const messages = ref([])
|
||||
const loading = ref(false)
|
||||
const sending = ref(false)
|
||||
const streamingContent = ref('')
|
||||
const sidebarOpen = ref(false)
|
||||
|
||||
async function fetchConversations() {
|
||||
const res = await api.get('/conversations')
|
||||
const data = res.data?.data
|
||||
conversations.value = normalizeList(data?.list ?? data)
|
||||
}
|
||||
|
||||
async function loadMessages(id = currentId.value) {
|
||||
if (!id) return
|
||||
const res = await api.get(`/conversations/${id}/messages`)
|
||||
const list = normalizeList(res.data?.data)
|
||||
messages.value = list.filter(isValidMessage)
|
||||
}
|
||||
|
||||
async function createConversation(modelId = null) {
|
||||
const res = await api.post('/conversations', { model_id: modelId })
|
||||
const conv = res.data.data
|
||||
conversations.value.unshift(conv)
|
||||
currentId.value = conv.id
|
||||
messages.value = []
|
||||
return conv
|
||||
}
|
||||
|
||||
async function selectConversation(id) {
|
||||
currentId.value = id
|
||||
loading.value = true
|
||||
try {
|
||||
await loadMessages(id)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteConversation(id) {
|
||||
await api.delete(`/conversations/${id}`)
|
||||
conversations.value = conversations.value.filter(c => c.id !== id)
|
||||
if (currentId.value === id) {
|
||||
currentId.value = null
|
||||
messages.value = []
|
||||
}
|
||||
}
|
||||
|
||||
async function sendMessage(content, attachments = []) {
|
||||
if (!currentId.value) {
|
||||
await createConversation()
|
||||
}
|
||||
|
||||
const userMsg = {
|
||||
id: Date.now(),
|
||||
role: 'user',
|
||||
content,
|
||||
attachments,
|
||||
created_at: new Date().toISOString()
|
||||
}
|
||||
messages.value.push(userMsg)
|
||||
sending.value = true
|
||||
streamingContent.value = ''
|
||||
|
||||
try {
|
||||
await streamChat(content, attachments)
|
||||
await loadMessages()
|
||||
} catch (e) {
|
||||
// 发送失败时移除本地临时用户消息,随后从服务端重新同步
|
||||
messages.value = messages.value.filter(m => m.id !== userMsg.id)
|
||||
await loadMessages().catch(() => {})
|
||||
throw e
|
||||
} finally {
|
||||
sending.value = false
|
||||
streamingContent.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function streamChat(content, attachments) {
|
||||
const token = localStorage.getItem('token')
|
||||
const response = await fetch('/api/chat/completions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
conversation_id: currentId.value,
|
||||
content,
|
||||
attachments,
|
||||
stream: true
|
||||
})
|
||||
})
|
||||
|
||||
const contentType = response.headers.get('content-type') || ''
|
||||
|
||||
if (!contentType.includes('text/event-stream')) {
|
||||
const json = await response.json().catch(() => null)
|
||||
const msg = json?.message || `请求失败 (${response.status})`
|
||||
throw new Error(msg)
|
||||
}
|
||||
|
||||
if (!response.body) {
|
||||
throw new Error('浏览器不支持流式响应')
|
||||
}
|
||||
|
||||
const reader = response.body.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
let buffer = ''
|
||||
let fullContent = ''
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
|
||||
buffer += decoder.decode(value, { stream: true })
|
||||
const blocks = buffer.split('\n\n')
|
||||
buffer = blocks.pop() || ''
|
||||
|
||||
for (const block of blocks) {
|
||||
if (!block.trim()) continue
|
||||
|
||||
const { event, data } = parseSseBlock(block)
|
||||
|
||||
if (event === 'error') {
|
||||
throw new Error(data?.message || 'AI 请求失败')
|
||||
}
|
||||
|
||||
if (event === 'message' && data?.content) {
|
||||
fullContent += data.content
|
||||
streamingContent.value = fullContent
|
||||
}
|
||||
|
||||
if (event === 'done' && data?.content && !fullContent) {
|
||||
fullContent = data.content
|
||||
streamingContent.value = fullContent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!fullContent.trim()) {
|
||||
throw new Error('AI 未返回内容,请在管理后台测试模型配置是否正确')
|
||||
}
|
||||
|
||||
await fetchConversations()
|
||||
}
|
||||
|
||||
function toggleSidebar() {
|
||||
sidebarOpen.value = !sidebarOpen.value
|
||||
}
|
||||
|
||||
function closeSidebar() {
|
||||
sidebarOpen.value = false
|
||||
}
|
||||
|
||||
return {
|
||||
conversations,
|
||||
currentId,
|
||||
messages,
|
||||
loading,
|
||||
sending,
|
||||
streamingContent,
|
||||
sidebarOpen,
|
||||
fetchConversations,
|
||||
createConversation,
|
||||
selectConversation,
|
||||
deleteConversation,
|
||||
sendMessage,
|
||||
toggleSidebar,
|
||||
closeSidebar
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user