import { useState, useEffect, useRef, useCallback } from 'react' import { createWebSocketClient, ReadyState, type Result as WebSocketResult } from '@/app/api/websocket' // import { createRequestClient, type RequestClient } from '@/api/request' import { request } from '@/http/request' import type { ApiEvent } from '../types' export type { ApiEvent } export interface PlatformConfig { wssUrl: string apiBaseUrl: string agentId: string agentName: string } interface UseNovaEventsOptions { mode: 'chat' | 'share' conversationId?: string platformConfig: PlatformConfig /** WebSocket 重连次数限制,默认 3 */ reconnectLimit?: number /** WebSocket 重连间隔(毫秒),默认 3000 */ reconnectInterval?: number /** 获取认证 Token */ getToken?: () => string | undefined /** 获取租户 ID */ getTenantId?: () => string | undefined /** 新事件回调 */ onEvent?: (event: ApiEvent) => void /** 连接状态变化回调 */ onConnectionChange?: (connected: boolean) => void /** 错误回调 */ onError?: (error: Error) => void } interface UseNovaEventsResult { /** 原始事件列表 */ rawEvents: ApiEvent[] /** 是否正在加载历史记录 */ loading: boolean /** WebSocket 是否已连接 */ connected: boolean /** WebSocket 连接状态 */ readyState: ReadyState /** 错误信息 */ error: Error | null /** 手动刷新历史记录 */ refresh: () => Promise /** 手动重连 WebSocket */ reconnect: () => void /** 清空事件列表 */ clear: () => void /** 切换会话 */ switchConversation: (conversationId: string) => void /** 发送消息 */ sendMessage: WebSocket['send'] } function isApiEventLike(value: unknown): value is ApiEvent { return !!value && typeof value === 'object' && 'event_id' in value } function normalizeIncomingEvent(payload: unknown): ApiEvent | null { if (isApiEventLike(payload)) { return payload } if ( payload && typeof payload === 'object' && 'data' in payload && isApiEventLike((payload as { data?: unknown }).data) ) { return (payload as { data: ApiEvent }).data } return null } /** * Nova 事件管理 Hook * * 负责: * 1. 建立 WebSocket 连接接收实时事件 * 2. 请求 event_list 接口获取历史记录 * 3. 合并和管理事件列表 */ export function useNovaEvents({ mode, conversationId, platformConfig, reconnectLimit = 3, reconnectInterval = 3000, getToken, getTenantId, onEvent, onConnectionChange, onError, }: UseNovaEventsOptions): UseNovaEventsResult { // 状态 const [rawEvents, setRawEvents] = useState([]) const [loading, setLoading] = useState(false) const [connected, setConnected] = useState(false) const [readyState, setReadyState] = useState(ReadyState.Closed) const [error, setError] = useState(null) // Refs const wsClientRef = useRef(null) const isUnmountedRef = useRef(false) const conversationIdRef = useRef(conversationId) const mountCountRef = useRef(0) // 保持 conversationId ref 同步 conversationIdRef.current = conversationId // 回调 refs(避免重新创建 WebSocket) const onEventRef = useRef(onEvent) const onConnectionChangeRef = useRef(onConnectionChange) const onErrorRef = useRef(onError) onEventRef.current = onEvent onConnectionChangeRef.current = onConnectionChange onErrorRef.current = onError // 更新连接状态 const updateConnected = useCallback((value: boolean) => { setConnected(value) onConnectionChangeRef.current?.(value) }, []) // 更新错误状态 const updateError = useCallback((err: Error | null) => { setError(err) if (err) { onErrorRef.current?.(err) } }, []) // 合并事件到列表(按 event_id 去重,新事件替换旧事件) const mergeEvent = useCallback((newEvent: ApiEvent) => { setRawEvents(prev => { const index = prev.findIndex(e => e.event_id === newEvent.event_id) if (index === -1) { // 新事件,追加到末尾 return [...prev, newEvent] } // 已存在,替换 const updated = [...prev] updated[index] = newEvent return updated }) }, []) // 批量合并事件(用于历史记录) const mergeEvents = useCallback((newEvents: ApiEvent[]) => { setRawEvents((prev) => { const eventsMap = new Map() // 先放入现有事件 for (const event of prev) { eventsMap.set(event.event_id, event) } // 合并新事件(会覆盖同 id 的旧事件) for (const event of newEvents) { eventsMap.set(event.event_id, event) } return Array.from(eventsMap.values()) }) }, []) // 获取历史记录 const fetchEventList = useCallback(async () => { if (!conversationIdRef.current) return setLoading(true) updateError(null) try { const payload = { conversation_id: conversationIdRef.current, page_no: 1, page_size: 3000, } const response = mode === 'share' ? await request.post<{ data: { chat_event_list: ApiEvent[] } }>('/v1/super_agent/chat/event_list_share', payload) : await request.get<{ data: { chat_event_list: ApiEvent[] } }>('/chat/event', payload) // 提取 data.data.chat_event_list 字段 const events = response?.data?.chat_event_list || [] if (!isUnmountedRef.current) { // 使用 mergeEvents 而不是直接 setRawEvents,以保留实时推送的新事件 mergeEvents(events) } } catch (err) { if (!isUnmountedRef.current) { const error = err instanceof Error ? err : new Error('Failed to fetch event list') updateError(error) } } finally { if (!isUnmountedRef.current) { setLoading(false) } } }, [mergeEvents, updateError, mode]) // 创建 WebSocket 连接 const createWebSocket = useCallback(() => { if (!conversationIdRef.current) return // 已有连接,不重复创建 if (wsClientRef.current) return // 构建 WebSocket URL const wsUrl = new URL(platformConfig.wssUrl) wsUrl.searchParams.set('conversation_id', conversationIdRef.current) const wsClient = createWebSocketClient(wsUrl.toString(), { reconnectLimit, reconnectInterval, manual: true, // 手动连接,避免 React Strict Mode 下的问题 getToken, getTenantId, onOpen: () => { if (isUnmountedRef.current) return setReadyState(ReadyState.Open) updateConnected(true) updateError(null) // 连接成功后切换到当前会话 if (conversationIdRef.current && wsClientRef.current) { wsClientRef.current.switchConversation(conversationIdRef.current) } }, onClose: () => { if (isUnmountedRef.current) return setReadyState(ReadyState.Closed) updateConnected(false) }, onError: () => { if (isUnmountedRef.current) return updateError(new Error('WebSocket connection error')) }, onMessage: (message: ApiEvent) => { if (isUnmountedRef.current) return const normalizedEvent = normalizeIncomingEvent(message) if (!normalizedEvent) return mergeEvent(normalizedEvent) onEventRef.current?.(normalizedEvent) }, }) wsClientRef.current = wsClient // 手动触发连接 wsClient.connect() }, [ platformConfig.wssUrl, reconnectLimit, reconnectInterval, getToken, getTenantId, mergeEvent, updateConnected, updateError, ]) // 手动重连 const reconnect = useCallback(() => { if (wsClientRef.current) { wsClientRef.current.connect() } else { createWebSocket() } }, [createWebSocket]) // 清空事件列表 const clear = useCallback(() => { setRawEvents([]) }, []) // 切换会话 const switchConversation = useCallback((newConversationId: string) => { if (wsClientRef.current) { wsClientRef.current.switchConversation(newConversationId) } }, []) // 发送消息 const sendMessage = useCallback((data) => { if (wsClientRef.current) { wsClientRef.current.sendMessage(data) } else { throw new Error('WebSocket not connected') } }, []) // 主 effect:当 conversationId 或 platformConfig 变化时处理 useEffect(() => { isUnmountedRef.current = false console.log('conversationId', conversationId) // 无会话 ID 时清空 if (!conversationId) { setRawEvents([]) setConnected(false) setReadyState(ReadyState.Closed) return } // conversationId 存在,获取历史记录 fetchEventList() // 如果已有连接且已打开,直接切换会话;否则创建新连接 if (wsClientRef.current && wsClientRef.current.readyState === ReadyState.Open) { wsClientRef.current.switchConversation(conversationId) } else { createWebSocket() } // 清理函数 return () => { isUnmountedRef.current = true } // eslint-disable-next-line react-hooks/exhaustive-deps }, [conversationId, platformConfig.wssUrl, platformConfig.apiBaseUrl]) // 组件卸载时清理 useEffect(() => { mountCountRef.current += 1 const currentMount = mountCountRef.current return () => { // 延迟执行,如果 mountCount 变了说明重新 mount 了(Strict Mode),不清理 setTimeout(() => { if (currentMount === mountCountRef.current && wsClientRef.current) { wsClientRef.current.cleanup() wsClientRef.current = null } }, 0) } }, []) return { rawEvents, loading, connected, readyState, error, refresh: fetchEventList, reconnect, clear, switchConversation, sendMessage, } } export { ReadyState } export default useNovaEvents