更新
This commit is contained in:
+155
-8
@@ -40,6 +40,10 @@ function isValidMessage(msg) {
|
||||
return hasContent || hasAttachments
|
||||
}
|
||||
|
||||
function hasPendingJobs(list) {
|
||||
return (list || []).some(m => m && m.pending_job)
|
||||
}
|
||||
|
||||
export const useChatStore = defineStore('chat', () => {
|
||||
const conversations = ref([])
|
||||
const currentId = ref(null)
|
||||
@@ -47,7 +51,48 @@ export const useChatStore = defineStore('chat', () => {
|
||||
const loading = ref(false)
|
||||
const sending = ref(false)
|
||||
const streamingContent = ref('')
|
||||
const streamingAttachments = ref([])
|
||||
const sidebarOpen = ref(false)
|
||||
/** 侧边栏当前选中的模型,发送/新建会话都会使用它 */
|
||||
const selectedModelId = ref(null)
|
||||
|
||||
let pendingPollTimer = null
|
||||
let pendingPollInFlight = false
|
||||
|
||||
function stopPendingJobPolling() {
|
||||
if (pendingPollTimer) {
|
||||
clearInterval(pendingPollTimer)
|
||||
pendingPollTimer = null
|
||||
}
|
||||
pendingPollInFlight = false
|
||||
}
|
||||
|
||||
function startPendingJobPolling() {
|
||||
stopPendingJobPolling()
|
||||
if (!currentId.value || !hasPendingJobs(messages.value)) return
|
||||
|
||||
pendingPollTimer = setInterval(async () => {
|
||||
if (!currentId.value || pendingPollInFlight) return
|
||||
if (!hasPendingJobs(messages.value)) {
|
||||
stopPendingJobPolling()
|
||||
return
|
||||
}
|
||||
|
||||
pendingPollInFlight = true
|
||||
try {
|
||||
await loadMessages(currentId.value, { quiet: true })
|
||||
} catch {
|
||||
// 轮询失败时下次再试
|
||||
} finally {
|
||||
pendingPollInFlight = false
|
||||
}
|
||||
|
||||
if (!hasPendingJobs(messages.value)) {
|
||||
stopPendingJobPolling()
|
||||
await fetchConversations().catch(() => {})
|
||||
}
|
||||
}, 4000)
|
||||
}
|
||||
|
||||
async function fetchConversations() {
|
||||
const res = await api.get('/conversations')
|
||||
@@ -55,24 +100,50 @@ export const useChatStore = defineStore('chat', () => {
|
||||
conversations.value = normalizeList(data?.list ?? data)
|
||||
}
|
||||
|
||||
async function loadMessages(id = currentId.value) {
|
||||
async function loadMessages(id = currentId.value, options = {}) {
|
||||
if (!id) return
|
||||
const quiet = !!options.quiet
|
||||
const res = await api.get(`/conversations/${id}/messages`)
|
||||
// 切换会话后丢弃过期响应
|
||||
if (id !== currentId.value) return
|
||||
|
||||
const list = normalizeList(res.data?.data)
|
||||
messages.value = list.filter(isValidMessage)
|
||||
|
||||
if (!quiet) {
|
||||
if (hasPendingJobs(messages.value)) {
|
||||
startPendingJobPolling()
|
||||
} else {
|
||||
stopPendingJobPolling()
|
||||
}
|
||||
} else if (!hasPendingJobs(messages.value)) {
|
||||
stopPendingJobPolling()
|
||||
} else if (!pendingPollTimer) {
|
||||
startPendingJobPolling()
|
||||
}
|
||||
}
|
||||
|
||||
async function createConversation(modelId = null) {
|
||||
const res = await api.post('/conversations', { model_id: modelId })
|
||||
const mid = modelId ?? selectedModelId.value
|
||||
const res = await api.post('/conversations', { model_id: mid })
|
||||
const conv = res.data.data
|
||||
conversations.value.unshift(conv)
|
||||
currentId.value = conv.id
|
||||
if (conv.model_id != null) {
|
||||
selectedModelId.value = Number(conv.model_id)
|
||||
}
|
||||
messages.value = []
|
||||
stopPendingJobPolling()
|
||||
return conv
|
||||
}
|
||||
|
||||
async function selectConversation(id) {
|
||||
currentId.value = id
|
||||
stopPendingJobPolling()
|
||||
const conv = conversations.value.find(c => c.id === id)
|
||||
if (conv?.model_id != null) {
|
||||
selectedModelId.value = Number(conv.model_id)
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
await loadMessages(id)
|
||||
@@ -81,18 +152,46 @@ export const useChatStore = defineStore('chat', () => {
|
||||
}
|
||||
}
|
||||
|
||||
async function setSelectedModel(modelId) {
|
||||
const mid = modelId == null || modelId === '' ? null : Number(modelId)
|
||||
selectedModelId.value = mid
|
||||
|
||||
// 已有会话时立即切换绑定模型,否则下拉只改了 UI、实际仍走旧模型
|
||||
if (!currentId.value || mid == null) return
|
||||
|
||||
const conv = conversations.value.find(c => c.id === currentId.value)
|
||||
if (conv && Number(conv.model_id) === mid) return
|
||||
|
||||
const res = await api.put(`/conversations/${currentId.value}`, { model_id: mid })
|
||||
const updated = res.data?.data
|
||||
if (updated) {
|
||||
conversations.value = conversations.value.map(c =>
|
||||
c.id === currentId.value ? { ...c, ...updated } : c
|
||||
)
|
||||
} else if (conv) {
|
||||
conv.model_id = mid
|
||||
}
|
||||
}
|
||||
|
||||
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 = []
|
||||
stopPendingJobPolling()
|
||||
}
|
||||
}
|
||||
|
||||
async function sendMessage(content, attachments = []) {
|
||||
if (!currentId.value) {
|
||||
await createConversation()
|
||||
await createConversation(selectedModelId.value)
|
||||
} else if (selectedModelId.value != null) {
|
||||
// 发送前确保当前会话已绑定侧边栏所选模型
|
||||
const conv = conversations.value.find(c => c.id === currentId.value)
|
||||
if (conv && Number(conv.model_id) !== Number(selectedModelId.value)) {
|
||||
await setSelectedModel(selectedModelId.value)
|
||||
}
|
||||
}
|
||||
|
||||
const userMsg = {
|
||||
@@ -105,18 +204,24 @@ export const useChatStore = defineStore('chat', () => {
|
||||
messages.value.push(userMsg)
|
||||
sending.value = true
|
||||
streamingContent.value = ''
|
||||
streamingAttachments.value = []
|
||||
|
||||
try {
|
||||
await streamChat(content, attachments)
|
||||
await loadMessages()
|
||||
} catch (e) {
|
||||
// 发送失败时移除本地临时用户消息,随后从服务端重新同步
|
||||
// 连接断开/代理超时:任务可能已落库为 pending,先同步再决定是否抛错
|
||||
messages.value = messages.value.filter(m => m.id !== userMsg.id)
|
||||
await loadMessages().catch(() => {})
|
||||
if (hasPendingJobs(messages.value)) {
|
||||
startPendingJobPolling()
|
||||
return
|
||||
}
|
||||
throw e
|
||||
} finally {
|
||||
sending.value = false
|
||||
streamingContent.value = ''
|
||||
streamingAttachments.value = []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,6 +257,8 @@ export const useChatStore = defineStore('chat', () => {
|
||||
const decoder = new TextDecoder()
|
||||
let buffer = ''
|
||||
let fullContent = ''
|
||||
let gotAttachments = false
|
||||
let pendingDone = false
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
@@ -170,19 +277,55 @@ export const useChatStore = defineStore('chat', () => {
|
||||
throw new Error(data?.message || 'AI 请求失败')
|
||||
}
|
||||
|
||||
if (event === 'progress' && data?.content) {
|
||||
streamingContent.value = data.content
|
||||
}
|
||||
|
||||
if (event === 'image' && Array.isArray(data?.attachments) && data.attachments.length) {
|
||||
streamingAttachments.value = data.attachments
|
||||
gotAttachments = true
|
||||
}
|
||||
|
||||
if (event === 'message' && data?.content) {
|
||||
fullContent += data.content
|
||||
streamingContent.value = fullContent
|
||||
}
|
||||
|
||||
if (event === 'done' && data?.content && !fullContent) {
|
||||
fullContent = data.content
|
||||
streamingContent.value = fullContent
|
||||
if (event === 'done') {
|
||||
if (data?.pending) {
|
||||
pendingDone = true
|
||||
if (data?.content) {
|
||||
streamingContent.value = data.content
|
||||
}
|
||||
}
|
||||
if (data?.content && !fullContent) {
|
||||
fullContent = data.content
|
||||
streamingContent.value = fullContent
|
||||
}
|
||||
if (Array.isArray(data?.attachments) && data.attachments.length) {
|
||||
streamingAttachments.value = data.attachments
|
||||
gotAttachments = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!fullContent.trim()) {
|
||||
if (pendingDone) {
|
||||
await loadMessages().catch(() => {})
|
||||
startPendingJobPolling()
|
||||
await fetchConversations().catch(() => {})
|
||||
return
|
||||
}
|
||||
|
||||
if (!fullContent.trim() && !gotAttachments) {
|
||||
// 生图可能已落库为 pending:刷新后由轮询恢复,不在此当成失败
|
||||
await loadMessages().catch(() => {})
|
||||
if (hasPendingJobs(messages.value) || messages.value.some(m =>
|
||||
Array.isArray(m.attachments) && m.attachments.some(a => a?.type === 'image')
|
||||
)) {
|
||||
startPendingJobPolling()
|
||||
return
|
||||
}
|
||||
throw new Error('AI 未返回内容,请在管理后台测试模型配置是否正确')
|
||||
}
|
||||
|
||||
@@ -204,12 +347,16 @@ export const useChatStore = defineStore('chat', () => {
|
||||
loading,
|
||||
sending,
|
||||
streamingContent,
|
||||
streamingAttachments,
|
||||
sidebarOpen,
|
||||
selectedModelId,
|
||||
fetchConversations,
|
||||
createConversation,
|
||||
selectConversation,
|
||||
setSelectedModel,
|
||||
deleteConversation,
|
||||
sendMessage,
|
||||
loadMessages,
|
||||
toggleSidebar,
|
||||
closeSidebar
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user