"use client"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { fetchConversations, searchConversations, } from "../../agent/services/conversationApi"; import type { ConversationListType } from "../../agent/services/conversationApi"; import type { ConversationStatus } from "../../agent/services/conversationApi"; import { ConversationSummary, MessageItem, VisitorStatusUpdatePayload, } from "../../agent/types"; import { useWebSocket } from "./useWebSocket"; import { WSMessage } from "@/lib/websocket"; import { ChatWebSocketPayload } from "../../agent/types"; import { buildMessagePreview } from "@/utils/format"; import { getAgentWSToken } from "@/utils/storage"; const sortByUpdatedAtDesc = (list: ConversationSummary[]) => [...list].sort( (a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime() ); import type { ConversationFilter } from "@/components/dashboard/ConversationHeader"; interface UseConversationsOptions { agentId?: number | null; filter?: ConversationFilter; /** 内部对话(知识库测试)时传 "internal",默认访客对话 "visitor" */ listType?: ConversationListType; /** 会话状态:open(进行中)/ closed(历史) */ status?: ConversationStatus; } export function useConversations(options?: UseConversationsOptions) { const { agentId, filter = "all", listType = "visitor", status = "open" } = options || {}; const [conversations, setConversations] = useState([]); const [filteredConversations, setFilteredConversations] = useState< ConversationSummary[] >([]); const [selectedConversationId, setSelectedConversationId] = useState< number | null >(null); const [searchQuery, setSearchQuery] = useState(""); const [loading, setLoading] = useState(true); const [isInitialLoad, setIsInitialLoad] = useState(true); const searchRef = useRef(""); const refreshTimerRef = useRef | null>(null); const wsToken = getAgentWSToken() ?? undefined; // 根据 filter 过滤会话 const applyFilter = useCallback( (conversations: ConversationSummary[]): ConversationSummary[] => { if (!agentId) { return conversations; } switch (filter) { case "mine": // 只显示当前用户参与过的会话(has_participated === true) // 即当前用户在该会话中发送过消息的会话 return conversations.filter((conv) => conv.has_participated === true); case "others": // 显示除了当前用户参与过的其他人的会话(has_participated !== true) return conversations.filter((conv) => conv.has_participated !== true); case "all": default: return conversations; } }, [agentId, filter] ); const loadConversations = useCallback(async () => { setLoading(true); try { // 内部对话(知识库测试)必须带 user_id,后端否则返回 400;未登录或 agentId 未就绪时不请求 if (listType === "internal" && !agentId) { setConversations([]); setFilteredConversations([]); setSelectedConversationId(null); return; } const data = await fetchConversations( agentId ?? undefined, listType === "internal" ? { type: "internal", status } : { status } ); setConversations(data); const filtered = listType === "internal" ? data : applyFilter(data); if (!searchRef.current.trim()) { setFilteredConversations(filtered); } setSelectedConversationId((prev) => { if (prev) { return prev; } return filtered.length > 0 ? filtered[0].id : null; }); } catch (error) { console.error(error); } finally { setLoading(false); setIsInitialLoad(false); } }, [applyFilter, agentId, filter, listType, status]); useEffect(() => { loadConversations(); }, [loadConversations]); // 兜底定时刷新:防止 WebSocket 漏事件/无会话时无法建立全局 WS 导致列表长期不更新。 useEffect(() => { if (!agentId) { return; } const interval = setInterval(() => { void loadConversations(); }, 15000); return () => clearInterval(interval); }, [agentId, loadConversations]); // 当 filter / listType 改变时,重新应用过滤(不重新加载数据) useEffect(() => { if (isInitialLoad) { return; } const filtered = listType === "internal" ? conversations : applyFilter(conversations); setFilteredConversations(sortByUpdatedAtDesc(filtered)); }, [filter, listType, conversations, isInitialLoad, applyFilter]); useEffect(() => { if (isInitialLoad) { return; } const handler = setTimeout(async () => { const query = searchQuery.trim(); searchRef.current = query; if (!query) { const filtered = listType === "internal" ? conversations : applyFilter(conversations); setFilteredConversations(sortByUpdatedAtDesc(filtered)); return; } if (listType === "internal") { setFilteredConversations(sortByUpdatedAtDesc(conversations.filter((c) => (c.last_message?.content ?? "").toLowerCase().includes(query.toLowerCase())))); setLoading(false); return; } try { setLoading(true); const data = await searchConversations(query, agentId ?? undefined, { status }); const filtered = applyFilter(data); setFilteredConversations(sortByUpdatedAtDesc(filtered)); } catch (error) { console.error(error); setFilteredConversations([]); } finally { setLoading(false); } }, 300); return () => clearTimeout(handler); }, [searchQuery, conversations, isInitialLoad, applyFilter, agentId, listType, status]); const selectConversation = useCallback((conversationId: number | null) => { setSelectedConversationId((prev) => prev === conversationId ? prev : conversationId ); }, []); const updateConversation = useCallback( ( conversationId: number, updater: (conversation: ConversationSummary) => ConversationSummary, options?: { skipResort?: boolean } ) => { const applyUpdate = (list: ConversationSummary[]) => { let changed = false; const next = list.map((conv) => { if (conv.id === conversationId) { changed = true; return updater(conv); } return conv; }); if (!changed) { return list; } if (options?.skipResort) { return next; } return sortByUpdatedAtDesc(next); }; setConversations((prev) => applyUpdate(prev)); setFilteredConversations((prev) => { if (searchRef.current && !prev.some((item) => item.id === conversationId)) { return prev; } return applyUpdate(prev); }); }, [] ); const setAllConversations = useCallback((data: ConversationSummary[]) => { setConversations(data); if (!searchRef.current.trim()) { const filtered = applyFilter(data); setFilteredConversations(filtered); } }, [applyFilter]); const hasConversation = useCallback( (conversationId: number) => { return conversations.some((conv) => conv.id === conversationId); }, [conversations] ); const scheduleRefreshConversations = useCallback(() => { if (refreshTimerRef.current) { clearTimeout(refreshTimerRef.current); } refreshTimerRef.current = setTimeout(() => { void loadConversations(); }, 500); }, [loadConversations]); // 建立全局 WebSocket 连接以接收 visitor_status_update 等全局事件 // 使用第一个对话的 ID(如果存在),否则不建立连接 const globalConversationId = conversations.length > 0 ? conversations[0].id : null; // 处理全局 WebSocket 事件:访客在线状态 + 新消息摘要 const handleGlobalWebSocketMessage = useCallback( (event: WSMessage) => { if (event.type === "visitor_status_update" && event.data) { const payload = event.data as VisitorStatusUpdatePayload; if (payload?.conversation_id) { if (payload.is_online === true) { // 在线:更新为当前时间(实时更新在线状态) updateConversation(payload.conversation_id, (conv) => ({ ...conv, last_seen_at: new Date().toISOString(), })); } // 离线时,last_seen_at 会在后端更新,这里不需要特殊处理 // 因为对话列表会定期刷新,或者通过其他方式更新 } } else if (event.type === "new_message" && event.data) { const message = event.data as MessageItem; if (typeof message?.conversation_id !== "number") { return; } const isConversationExists = hasConversation(message.conversation_id); if (!isConversationExists) { // 新会话(当前列表里还没有)时,延迟刷新把它拉进来 scheduleRefreshConversations(); return; } const isSystemMessage = (message.message_type ?? "user_message") === "system_message"; const isVisitorMessage = !message.sender_is_agent && !isSystemMessage; const preview = buildMessagePreview(message.content ?? ""); updateConversation(message.conversation_id, (conv) => ({ ...conv, updated_at: message.created_at, last_seen_at: isVisitorMessage ? message.created_at : conv.last_seen_at ?? null, unread_count: isVisitorMessage ? message.conversation_id === selectedConversationId ? 0 : (conv.unread_count ?? 0) + 1 : conv.unread_count ?? 0, last_message: { id: message.id, content: preview, sender_is_agent: message.sender_is_agent, message_type: message.message_type ?? "user_message", is_read: Boolean(message.is_read), read_at: message.read_at ?? null, created_at: message.created_at, }, })); } }, [ hasConversation, scheduleRefreshConversations, selectedConversationId, updateConversation, ] ); useEffect(() => { return () => { if (refreshTimerRef.current) { clearTimeout(refreshTimerRef.current); } }; }, []); // 建立全局 WebSocket 连接(用于接收全局事件) useWebSocket({ conversationId: globalConversationId, enabled: Boolean(globalConversationId && agentId), isVisitor: false, agentId: agentId ?? undefined, wsToken, onMessage: handleGlobalWebSocketMessage, onError: (error) => { // 静默处理错误,避免影响用户体验 }, onClose: () => { // 静默处理关闭,避免影响用户体验 }, }); const contextValue = useMemo( () => ({ conversations, filteredConversations, selectedConversationId, searchQuery, loading, isInitialLoad, setSearchQuery, selectConversation, refresh: loadConversations, updateConversation, setAllConversations, hasConversation, }), [ conversations, filteredConversations, selectedConversationId, searchQuery, loading, isInitialLoad, selectConversation, loadConversations, updateConversation, setAllConversations, setSearchQuery, hasConversation, ] ); return contextValue; }