This commit is contained in:
Your Name
2026-07-14 10:00:34 +08:00
parent 01152efce6
commit 99a6ca0a77
59 changed files with 10014 additions and 0 deletions
+216
View File
@@ -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
}
})