feat(messages): 接入用户聊天 SSE 实时刷新
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
|
||||
import { useSessionStore } from '@/stores/session'
|
||||
|
||||
interface ChatEvent {
|
||||
type: string
|
||||
conversation_id: number
|
||||
}
|
||||
|
||||
function getSiteCode() {
|
||||
try {
|
||||
return sessionStorage.getItem('jieyou_site_code') || ''
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
function parseEventBlock(block: string): ChatEvent | null {
|
||||
let data = ''
|
||||
for (const rawLine of block.split('\n')) {
|
||||
const line = rawLine.replace(/\r$/, '')
|
||||
if (!line.startsWith('data:')) continue
|
||||
data += line.slice(5).trimStart() + '\n'
|
||||
}
|
||||
const payload = data.trim()
|
||||
if (!payload) return null
|
||||
try {
|
||||
const parsed = JSON.parse(payload) as Partial<ChatEvent>
|
||||
if (!parsed.type || !parsed.conversation_id) return null
|
||||
return { type: parsed.type, conversation_id: Number(parsed.conversation_id) }
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户端聊天 SSE 桥接:EventSource 无法附加 Authorization,这里用 fetch 流读取。
|
||||
* SSE 只作为实时加速,页面仍保留低频轮询兜底。
|
||||
*/
|
||||
export function ChatEventsBridge() {
|
||||
const queryClient = useQueryClient()
|
||||
const token = useSessionStore(state => state.token)
|
||||
|
||||
useEffect(() => {
|
||||
if (!token) return
|
||||
|
||||
const controller = new AbortController()
|
||||
let stopped = false
|
||||
let retryTimer: number | undefined
|
||||
|
||||
const invalidate = (conversationID: number) => {
|
||||
void queryClient.invalidateQueries({ queryKey: ['chat-conversations'] })
|
||||
void queryClient.invalidateQueries({ queryKey: ['chat-unread-count'] })
|
||||
void queryClient.invalidateQueries({ queryKey: ['notifications'] })
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: ['chat-messages', String(conversationID)],
|
||||
})
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: ['chat-conversation', String(conversationID)],
|
||||
})
|
||||
}
|
||||
|
||||
const connect = async () => {
|
||||
try {
|
||||
const headers: Record<string, string> = {
|
||||
Accept: 'text/event-stream',
|
||||
Authorization: `Bearer ${token}`,
|
||||
}
|
||||
const siteCode = getSiteCode()
|
||||
if (siteCode) headers['X-Distribution-Site-Code'] = siteCode
|
||||
|
||||
const response = await fetch('/api/chats/events', {
|
||||
headers,
|
||||
credentials: 'include',
|
||||
signal: controller.signal,
|
||||
})
|
||||
if (!response.ok || !response.body) {
|
||||
throw new Error(`chat events unavailable: ${response.status}`)
|
||||
}
|
||||
|
||||
const reader = response.body.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
let buffer = ''
|
||||
|
||||
while (!stopped) {
|
||||
const { value, done } = await reader.read()
|
||||
if (done) break
|
||||
buffer += decoder.decode(value, { stream: true }).replace(/\r\n/g, '\n')
|
||||
let boundary = buffer.indexOf('\n\n')
|
||||
while (boundary >= 0) {
|
||||
const block = buffer.slice(0, boundary)
|
||||
buffer = buffer.slice(boundary + 2)
|
||||
const event = parseEventBlock(block)
|
||||
if (event) invalidate(event.conversation_id)
|
||||
boundary = buffer.indexOf('\n\n')
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// 断线后延迟重连;页面轮询仍可兜底。
|
||||
}
|
||||
|
||||
if (!stopped) {
|
||||
retryTimer = window.setTimeout(() => void connect(), 3000)
|
||||
}
|
||||
}
|
||||
|
||||
void connect()
|
||||
|
||||
return () => {
|
||||
stopped = true
|
||||
controller.abort()
|
||||
if (retryTimer) window.clearTimeout(retryTimer)
|
||||
}
|
||||
}, [queryClient, token])
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -84,7 +84,7 @@ export function useChatMessages(id?: string, enabled = true) {
|
||||
getNextPageParam: last =>
|
||||
last.page * last.page_size < last.total ? last.page + 1 : undefined,
|
||||
enabled: Boolean(id) && enabled,
|
||||
refetchInterval: 6000,
|
||||
refetchInterval: 30_000,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user