初始化模版工程

This commit is contained in:
Cloud Bot
2026-03-20 07:33:46 +00:00
commit 23717e0ecd
386 changed files with 51675 additions and 0 deletions

View File

@@ -0,0 +1,370 @@
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<void>
/** 手动重连 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<ApiEvent[]>([])
const [loading, setLoading] = useState(false)
const [connected, setConnected] = useState(false)
const [readyState, setReadyState] = useState<ReadyState>(ReadyState.Closed)
const [error, setError] = useState<Error | null>(null)
// Refs
const wsClientRef = useRef<WebSocketResult | null>(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<string, ApiEvent>()
// 先放入现有事件
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<WebSocket['send']>((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