refactor: implement centralized realtime connection manager and enhance message handling logic

This commit is contained in:
mlogclub
2026-04-25 16:27:58 +08:00
parent e109d19ea9
commit 0b22c003b0
5 changed files with 358 additions and 341 deletions
+66 -130
View File
@@ -5,23 +5,26 @@ import { toast } from "sonner"
import { createAdminWebSocketUrl } from "@/lib/api/admin"
import { readSession } from "@/lib/auth"
import { useAgentConversationsStore } from "@/lib/stores/agent-conversations"
import { createRealtimeConnectionManager } from "@/lib/realtime-connection"
import { getNotificationBody, showNotification } from "@/lib/services/notification"
import { useAgentConversationsStore } from "@/lib/stores/agent-conversations"
const RECONNECT_BASE_DELAY = 2000
const RECONNECT_MAX_DELAY = 30000
type AgentRealtimeConnection = ReturnType<typeof createRealtimeConnectionManager>
export function useAgentConversationRealtime() {
const selectedConversationId = useAgentConversationsStore((state) => state.selectedConversationId)
const loadConversations = useAgentConversationsStore((state) => state.loadConversations)
const syncLatestMessages = useAgentConversationsStore(
(state) => state.syncLatestMessages,
const selectedConversationId = useAgentConversationsStore(
(state) => state.selectedConversationId
)
const setRealtimeStatus = useAgentConversationsStore((state) => state.setRealtimeStatus)
const websocketRef = useRef<WebSocket | null>(null)
const reconnectTimerRef = useRef<number | null>(null)
const pingTimerRef = useRef<number | null>(null)
const reconnectAttemptRef = useRef(0)
const loadConversations = useAgentConversationsStore(
(state) => state.loadConversations
)
const syncLatestMessages = useAgentConversationsStore(
(state) => state.syncLatestMessages
)
const setRealtimeStatus = useAgentConversationsStore(
(state) => state.setRealtimeStatus
)
const realtimeRef = useRef<AgentRealtimeConnection | null>(null)
const subscribedConversationIdRef = useRef<number | null>(null)
const selectedConversationIdRef = useRef<number | null>(selectedConversationId)
const currentUserIdRef = useRef<number>(readSession()?.user.id ?? 0)
@@ -31,68 +34,13 @@ export function useAgentConversationRealtime() {
}, [selectedConversationId])
useEffect(() => {
let cancelled = false
const clearTimers = () => {
if (reconnectTimerRef.current) {
window.clearTimeout(reconnectTimerRef.current)
reconnectTimerRef.current = null
}
if (pingTimerRef.current) {
window.clearInterval(pingTimerRef.current)
pingTimerRef.current = null
}
}
const scheduleReconnect = () => {
if (cancelled || reconnectTimerRef.current) {
return
}
const delay = Math.min(
RECONNECT_BASE_DELAY * 2 ** reconnectAttemptRef.current,
RECONNECT_MAX_DELAY
)
reconnectTimerRef.current = window.setTimeout(() => {
reconnectTimerRef.current = null
reconnectAttemptRef.current += 1
if (!cancelled) {
connect()
}
}, delay)
}
const connect = () => {
if (cancelled) {
return
}
let socket: WebSocket
try {
setRealtimeStatus("connecting")
socket = new WebSocket(createAdminWebSocketUrl())
} catch (error) {
setRealtimeStatus("disconnected")
toast.error(error instanceof Error ? error.message : "连接实时服务失败")
scheduleReconnect()
return
}
websocketRef.current = socket
socket.onopen = () => {
const realtime = createRealtimeConnectionManager({
createSocket: () => new WebSocket(createAdminWebSocketUrl()),
onStatusChange: setRealtimeStatus,
onOpen: (socket) => {
console.info("[agent-realtime] websocket connected", {
url: socket.url,
})
setRealtimeStatus("connected")
reconnectAttemptRef.current = 0
if (pingTimerRef.current) {
window.clearInterval(pingTimerRef.current)
}
pingTimerRef.current = window.setInterval(() => {
if (socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify({ type: "ping" }))
}
}, 20000)
const conversationId = selectedConversationIdRef.current
if (conversationId) {
@@ -106,9 +54,8 @@ export function useAgentConversationRealtime() {
} else {
subscribedConversationIdRef.current = null
}
}
socket.onmessage = (event) => {
},
onMessage: (event, socket) => {
try {
const payload = JSON.parse(event.data) as {
eventId?: string
@@ -146,48 +93,52 @@ export function useAgentConversationRealtime() {
const status = payload.data?.status ?? 0
const currentAssigneeId = payload.data?.currentAssigneeId ?? 0
void loadConversations().then(() => {
const store = useAgentConversationsStore.getState()
const shouldNotify =
senderType === "customer" &&
status === 2 &&
currentAssigneeId > 0 &&
currentAssigneeId === currentUserIdRef.current &&
typeof document !== "undefined" &&
document.visibilityState !== "visible"
void loadConversations()
.then(() => {
const store = useAgentConversationsStore.getState()
const shouldNotify =
senderType === "customer" &&
status === 2 &&
currentAssigneeId > 0 &&
currentAssigneeId === currentUserIdRef.current &&
typeof document !== "undefined" &&
document.visibilityState !== "visible"
if (!shouldNotify) {
return
}
showNotification(
"新消息",
getNotificationBody({
messageType: payload.data?.messageType ?? "",
content: payload.data?.content ?? "",
}),
() => {
void store.selectConversation(conversationId)
if (!shouldNotify) {
return
}
)
}).catch((error) => {
toast.error(error instanceof Error ? error.message : "加载消息失败")
})
showNotification(
"新消息",
getNotificationBody({
messageType: payload.data?.messageType ?? "",
content: payload.data?.content ?? "",
}),
() => {
void store.selectConversation(conversationId)
}
)
})
.catch((error) => {
toast.error(error instanceof Error ? error.message : "加载消息失败")
})
} else {
void loadConversations().catch((error) => {
toast.error(error instanceof Error ? error.message : "加载会话列表失败")
})
}
if (conversationId > 0 && selectedConversationIdRef.current === conversationId) {
if (
conversationId > 0 &&
selectedConversationIdRef.current === conversationId
) {
void syncLatestMessages(conversationId)
}
} catch {
// ignore invalid ws payload
}
}
socket.onclose = (event) => {
},
onClose: (event, socket) => {
console.log("[agent-realtime] websocket closed", {
url: socket.url,
readyState: socket.readyState,
@@ -195,46 +146,31 @@ export function useAgentConversationRealtime() {
reason: event.reason,
wasClean: event.wasClean,
})
setRealtimeStatus("disconnected")
if (pingTimerRef.current) {
window.clearInterval(pingTimerRef.current)
pingTimerRef.current = null
}
if (websocketRef.current === socket) {
websocketRef.current = null
}
subscribedConversationIdRef.current = null
scheduleReconnect()
}
socket.onerror = () => {
},
onError: (_event, socket) => {
console.log("[agent-realtime] websocket error", {
url: socket.url,
readyState: socket.readyState,
})
setRealtimeStatus("disconnected")
scheduleReconnect()
}
}
},
onConnectError: (error) => {
toast.error(error instanceof Error ? error.message : "连接实时服务失败")
},
})
connect()
realtimeRef.current = realtime
realtime.connect()
return () => {
cancelled = true
clearTimers()
reconnectAttemptRef.current = 0
const socket = websocketRef.current
websocketRef.current = null
if (socket) {
socket.close()
}
setRealtimeStatus("disconnected")
realtimeRef.current = null
realtime.disconnect()
subscribedConversationIdRef.current = null
}
}, [loadConversations, setRealtimeStatus, syncLatestMessages])
useEffect(() => {
const socket = websocketRef.current
const socket = realtimeRef.current?.getSocket()
if (!socket || socket.readyState !== WebSocket.OPEN) {
return
}