"use client" import { useEffect, useRef } from "react" import { toast } from "sonner" import { createAdminWebSocketUrl } from "@/lib/api/admin" import { type AgentMessage } from "@/lib/api/agent" import { shouldReloadConversationListForRealtimePatch } from "@/lib/agent-conversation-realtime" import { readSession } from "@/lib/auth" import { normalizeRealtimeMessage, type RealtimeConversationPatch, type RealtimeMessageCreatedPayload, } from "@/lib/im-realtime-state" import { createRealtimeConnectionManager } from "@/lib/realtime-connection" import { getNotificationBody, showNotification } from "@/lib/services/notification" import { useAgentConversationsStore } from "@/lib/stores/agent-conversations" type AgentRealtimeConnection = ReturnType type AgentRealtimeEnvelope = { eventId?: string type?: string data?: RealtimeMessageCreatedPayload & RealtimeConversationPatch & { messageId?: number recalledAt?: string sendStatus?: number } } export function useAgentConversationRealtime() { const selectedConversationId = useAgentConversationsStore( (state) => state.selectedConversationId ) const setRealtimeStatus = useAgentConversationsStore( (state) => state.setRealtimeStatus ) const realtimeRef = useRef(null) const subscribedConversationIdRef = useRef(null) const selectedConversationIdRef = useRef(selectedConversationId) const currentUserIdRef = useRef(readSession()?.user.id ?? 0) useEffect(() => { selectedConversationIdRef.current = selectedConversationId }, [selectedConversationId]) useEffect(() => { const realtime = createRealtimeConnectionManager({ createSocket: () => new WebSocket(createAdminWebSocketUrl()), onStatusChange: setRealtimeStatus, onOpen: (socket) => { console.info("[agent-realtime] websocket connected", { url: socket.url, }) const conversationId = selectedConversationIdRef.current if (conversationId) { socket.send( JSON.stringify({ type: "subscribe", topics: [`conversation:${conversationId}`], }) ) subscribedConversationIdRef.current = conversationId } else { subscribedConversationIdRef.current = null } }, onMessage: (event, socket) => { try { const envelope = JSON.parse(event.data) as AgentRealtimeEnvelope const eventType = envelope.type ?? "" const payload = envelope.data const conversationId = payload?.conversationId ?? 0 const eventId = envelope.eventId?.trim() ?? "" if ( eventType === "" || eventType === "connected" || eventType === "pong" || eventType === "subscribed" || eventType === "unsubscribed" ) { return } if (eventId && socket.readyState === WebSocket.OPEN) { socket.send(JSON.stringify({ type: "ack", eventId })) } const store = useAgentConversationsStore.getState() if (eventType === "resyncRequired") { void store.resyncRealtimeData(conversationId).catch((error) => { toast.error(error instanceof Error ? error.message : "同步会话数据失败") }) return } if (eventType === "message.created") { const message = normalizeRealtimeMessage(payload) if (!message) { void store.resyncRealtimeData(conversationId).catch((error) => { toast.error(error instanceof Error ? error.message : "同步消息失败") }) return } store.applyRealtimeMessageCreated(message) const shouldNotify = message.senderType === "customer" && payload?.status === 2 && (payload.currentAssigneeId ?? 0) > 0 && payload.currentAssigneeId === currentUserIdRef.current && typeof document !== "undefined" && document.visibilityState !== "visible" if (shouldNotify) { showNotification("新消息", getNotificationBody(message), () => { void store.selectConversation(message.conversationId) }) } return } if (eventType === "message.recalled" && payload?.messageId) { store.applyRealtimeMessageRecalled(payload.messageId, { sendStatus: payload.sendStatus, recalledAt: payload.recalledAt, }) return } if (eventType.startsWith("conversation.") && payload) { store.applyRealtimeConversationChanged(payload) if (shouldReloadConversationListForRealtimePatch(payload)) { void store.resyncRealtimeData(conversationId).catch((error) => { toast.error(error instanceof Error ? error.message : "同步会话列表失败") }) } } } catch { // ignore invalid ws payload } }, onClose: (event, socket) => { console.log("[agent-realtime] websocket closed", { url: socket.url, readyState: socket.readyState, code: event.code, reason: event.reason, wasClean: event.wasClean, }) subscribedConversationIdRef.current = null }, onError: (_event, socket) => { console.log("[agent-realtime] websocket error", { url: socket.url, readyState: socket.readyState, }) }, onConnectError: (error) => { toast.error(error instanceof Error ? error.message : "连接实时服务失败") }, }) realtimeRef.current = realtime realtime.connect() return () => { realtimeRef.current = null realtime.disconnect() subscribedConversationIdRef.current = null } }, [setRealtimeStatus]) useEffect(() => { const socket = realtimeRef.current?.getSocket() if (!socket || socket.readyState !== WebSocket.OPEN) { return } const previousConversationId = subscribedConversationIdRef.current const nextConversationId = selectedConversationId ?? null if (previousConversationId && previousConversationId !== nextConversationId) { socket.send( JSON.stringify({ type: "unsubscribe", topics: [`conversation:${previousConversationId}`], }) ) } if (nextConversationId && nextConversationId !== previousConversationId) { socket.send( JSON.stringify({ type: "subscribe", topics: [`conversation:${nextConversationId}`], }) ) subscribedConversationIdRef.current = nextConversationId return } if (!nextConversationId) { subscribedConversationIdRef.current = null } }, [selectedConversationId]) }