初始化模版工程

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

570
app/api/websocket/index.ts Normal file
View File

@@ -0,0 +1,570 @@
/**
* WebSocket 客户端封装
*
* 提供自动重连、心跳检测、网络状态监听等功能
*/
function latest<T>(value: T) {
const ref = { current: value }
return ref
}
export const ReadyState = {
Connecting: 0,
Open: 1,
Closing: 2,
Closed: 3,
} as const
export type ReadyState = (typeof ReadyState)[keyof typeof ReadyState]
export interface Result {
sendMessage: WebSocket['send']
disconnect: () => void
connect: () => void
readyState: ReadyState
webSocketIns?: WebSocket
clearHeartbeat: () => void
switchConversation: (conversationId: string) => void
cleanup: () => void
}
export interface HeartbeatOptions {
/** 心跳间隔,默认 20000ms */
heartbeatInterval?: number
/** 心跳超时,默认 22000ms */
heartbeatTimeout?: number
/** 心跳消息,默认 { message_type: 'ping' } */
heartbeatMessage?: string | object | (() => string | object)
/** 心跳响应类型,默认 'pong' */
heartbeatResponseType?: string
}
export interface ApiEvent {
event_id: string
event_type?: string
role?: 'user' | 'assistant' | 'system'
content?: {
text?: string
content?: string
[key: string]: unknown
}
created_at?: string
task_id?: string
is_display?: boolean
metadata?: Record<string, unknown>
stream?: boolean
event_status?: string
[key: string]: unknown
}
// WebSocket 事件类型定义
type WSOpenEvent = Event
interface WSCloseEvent {
code?: number
reason?: string
wasClean?: boolean
}
interface WSMessageEvent {
data: string | ArrayBuffer | Blob
}
type WSErrorEvent = Event
export interface Options {
/** 重连次数限制,默认 3 */
reconnectLimit?: number
/** 重连间隔,默认 3000ms */
reconnectInterval?: number
/** 是否手动连接,默认 false */
manual?: boolean
/** 连接成功回调 */
onOpen?: (event: WSOpenEvent, instance: WebSocket) => void
/** 连接关闭回调 */
onClose?: (event: WSCloseEvent, instance: WebSocket) => void
/** 收到消息回调 */
onMessage?: (message: ApiEvent, instance: WebSocket) => void
/** 连接错误回调 */
onError?: (event: WSErrorEvent, instance: WebSocket) => void
/** WebSocket 协议 */
protocols?: string | string[]
/** 心跳配置,传 false 禁用心跳 */
heartbeat?: HeartbeatOptions | boolean
/** 是否监听网络状态变化,默认 true */
enableNetworkListener?: boolean
/** 是否监听文档可见性变化,默认 true */
enableVisibilityListener?: boolean
/** 重连延迟,默认 300ms */
reconnectDelay?: number
/** 重连防抖时间,默认 1000ms */
reconnectDebounce?: number
/** 获取认证 Token */
getToken?: () => string | undefined
/** 获取租户 ID */
getTenantId?: () => string | undefined
}
/**
* 创建 WebSocket 客户端
*/
export function createWebSocketClient(
socketUrl: string,
options: Options = {},
): Result {
const {
reconnectLimit = 3,
reconnectInterval = 3 * 1000,
manual = false,
onOpen,
onClose,
onMessage,
onError,
protocols,
heartbeat: enableHeartbeat = true,
enableNetworkListener = true,
enableVisibilityListener = true,
reconnectDelay = 300,
reconnectDebounce = 1000,
getToken,
getTenantId,
} = options
const heartbeatOptions: HeartbeatOptions =
typeof enableHeartbeat === 'object' ? enableHeartbeat : {}
const {
heartbeatInterval = 20000,
heartbeatTimeout = 22000,
heartbeatMessage = { message_type: 'ping' },
heartbeatResponseType = 'pong',
} = heartbeatOptions
// 提前声明函数,避免使用前未定义
let disconnectFn: () => void = () => {
throw new Error('disconnectFn not initialized')
}
let connectWsFn: () => void = () => {
throw new Error('connectWsFn not initialized')
}
const onOpenRef = latest(onOpen)
const onCloseRef = latest(onClose)
const onMessageRef = latest(onMessage)
const onErrorRef = latest(onError)
// 确保 ref 始终指向最新的回调
if (onMessage) {
onMessageRef.current = onMessage
}
const reconnectTimesRef = latest(0)
const reconnectTimerRef = latest<ReturnType<typeof setTimeout> | undefined>(undefined)
const websocketRef = latest<WebSocket | undefined>(undefined)
const readyStateRef = latest<ReadyState>(ReadyState.Closed)
// 心跳相关
const heartbeatTimerRef = latest<ReturnType<typeof setTimeout> | undefined>(undefined)
const heartbeatTimeoutTimerRef = latest<ReturnType<typeof setTimeout> | undefined>(undefined)
const waitingForPongRef = latest(false)
// 网络和可见性状态
const isOnlineRef = latest(typeof navigator !== 'undefined' ? navigator.onLine : true)
const isVisibleRef = latest(typeof document !== 'undefined' ? !document.hidden : true)
// 重连防抖定时器
const reconnectDebounceTimerRef = latest<ReturnType<typeof setTimeout> | undefined>(undefined)
// 更新 readyState 的辅助函数
const setReadyState = (state: ReadyState) => {
readyStateRef.current = state
}
// 获取当前 readyState
const getReadyState = (): ReadyState => {
if (websocketRef.current) {
const wsState = websocketRef.current.readyState
if (wsState === WebSocket.CONNECTING) return ReadyState.Connecting
if (wsState === WebSocket.OPEN) return ReadyState.Open
if (wsState === WebSocket.CLOSING) return ReadyState.Closing
if (wsState === WebSocket.CLOSED) return ReadyState.Closed
}
return readyStateRef.current
}
// 清除心跳相关定时器
const clearHeartbeat = () => {
if (heartbeatTimerRef.current) {
clearTimeout(heartbeatTimerRef.current)
heartbeatTimerRef.current = undefined
}
if (heartbeatTimeoutTimerRef.current) {
clearTimeout(heartbeatTimeoutTimerRef.current)
heartbeatTimeoutTimerRef.current = undefined
}
waitingForPongRef.current = false
}
// 处理心跳超时
const handlePingTimeout = () => {
if (!isOnlineRef.current) {
waitingForPongRef.current = false
clearHeartbeat()
return
}
waitingForPongRef.current = false
clearHeartbeat()
disconnectFn()
}
// 发送心跳
const sendHeartbeat = () => {
if (!isOnlineRef.current) {
return
}
if (waitingForPongRef.current) {
return
}
if (websocketRef.current && getReadyState() === ReadyState.Open) {
try {
const message =
typeof heartbeatMessage === 'function'
? heartbeatMessage()
: heartbeatMessage
websocketRef.current.send(
typeof message === 'string' ? message : JSON.stringify(message),
)
waitingForPongRef.current = true
heartbeatTimeoutTimerRef.current = setTimeout(
handlePingTimeout,
heartbeatTimeout,
)
} catch {
clearHeartbeat()
}
} else {
clearHeartbeat()
}
}
// 处理心跳响应
const handlePongReceived = () => {
if (!waitingForPongRef.current) {
return
}
waitingForPongRef.current = false
if (heartbeatTimeoutTimerRef.current) {
clearTimeout(heartbeatTimeoutTimerRef.current)
heartbeatTimeoutTimerRef.current = undefined
}
heartbeatTimerRef.current = setTimeout(sendHeartbeat, heartbeatInterval)
}
// 启动心跳
const startHeartbeat = () => {
if (!enableHeartbeat) return
clearHeartbeat()
heartbeatTimerRef.current = setTimeout(sendHeartbeat, 1000)
}
// 处理原始消息,检查是否是心跳响应
const handleRawMessage = (messageData: string): boolean => {
try {
const rawMessage = JSON.parse(messageData)
if (
rawMessage.data?.message_type === heartbeatResponseType ||
rawMessage.message_type === heartbeatResponseType
) {
handlePongReceived()
return true
}
return false
} catch {
return false
}
}
const reconnect = () => {
if (
reconnectTimesRef.current < reconnectLimit &&
getReadyState() !== ReadyState.Open
) {
if (reconnectTimerRef.current) {
clearTimeout(reconnectTimerRef.current)
}
reconnectTimerRef.current = setTimeout(() => {
connectWsFn()
reconnectTimesRef.current++
}, reconnectInterval)
}
}
connectWsFn = () => {
if (reconnectTimerRef.current) {
clearTimeout(reconnectTimerRef.current)
}
if (websocketRef.current) {
websocketRef.current.close()
}
// 构建 WebSocket URL
const url = new URL(socketUrl)
// 添加认证信息
const token = getToken?.()
const tenantId = getTenantId?.()
if (token) {
url.searchParams.set('Authorization', token)
}
if (tenantId) {
url.searchParams.set('Tenant-Id', tenantId)
}
const ws = new WebSocket(url.toString(), protocols)
setReadyState(ReadyState.Connecting)
ws.onerror = event => {
if (websocketRef.current !== ws) {
return
}
reconnect()
onErrorRef.current?.(event, ws)
setReadyState(ReadyState.Closed)
}
ws.onopen = event => {
if (websocketRef.current !== ws) {
return
}
onOpenRef.current?.(event, ws)
reconnectTimesRef.current = 0
setReadyState(ReadyState.Open)
startHeartbeat()
}
ws.onmessage = (message: WSMessageEvent) => {
if (websocketRef.current !== ws) {
return
}
const messageData =
typeof message.data === 'string' ? message.data : String(message.data)
// 先检查是否是心跳响应
if (enableHeartbeat && handleRawMessage(messageData)) {
return
}
// 解析消息并触发回调
try {
const parsedMessage: ApiEvent = JSON.parse(messageData)
onMessageRef.current?.(parsedMessage, ws)
} catch {
// 如果解析失败,尝试作为原始数据传递
console.warn('[WebSocket] Failed to parse message:', messageData)
}
}
ws.onclose = event => {
onCloseRef.current?.(event, ws)
clearHeartbeat()
// closed by server
if (websocketRef.current === ws) {
reconnect()
}
// closed by disconnect or closed by server
if (!websocketRef.current || websocketRef.current === ws) {
setReadyState(ReadyState.Closed)
}
}
websocketRef.current = ws
}
const sendMessage: WebSocket['send'] = message => {
const currentState = getReadyState()
if (currentState === ReadyState.Open) {
websocketRef.current?.send(message)
} else {
throw new Error('WebSocket disconnected')
}
}
// 切换会话
const switchConversation = (conversationId: string) => {
// 检查网络状态
if (!isOnlineRef.current) {
throw new Error('网络连接异常,无法切换会话')
}
// 检查 WebSocket 连接状态
const currentState = getReadyState()
if (!websocketRef.current || currentState !== ReadyState.Open) {
throw new Error('WebSocket 未连接,无法切换会话')
}
try {
const message = JSON.stringify({
message_type: 'switch_conversation',
conversation_id: conversationId,
})
websocketRef.current.send(message)
} catch (error) {
throw new Error(`切换会话失败: ${error}`)
}
}
const connect = () => {
reconnectTimesRef.current = 0
connectWsFn()
}
disconnectFn = () => {
if (reconnectTimerRef.current) {
clearTimeout(reconnectTimerRef.current)
}
if (reconnectDebounceTimerRef.current) {
clearTimeout(reconnectDebounceTimerRef.current)
}
reconnectTimesRef.current = reconnectLimit
clearHeartbeat()
websocketRef.current?.close(1000, '手动断开')
websocketRef.current = undefined
setReadyState(ReadyState.Closed)
}
// 处理网络断开
const handleNetworkOffline = () => {
clearHeartbeat()
const currentState = getReadyState()
if (
currentState === ReadyState.Open ||
currentState === ReadyState.Connecting
) {
disconnectFn()
}
}
// 重连函数 - 统一处理重连逻辑
const attemptReconnect = () => {
// 清除之前的防抖定时器
if (reconnectDebounceTimerRef.current) {
clearTimeout(reconnectDebounceTimerRef.current)
}
reconnectDebounceTimerRef.current = setTimeout(() => {
const currentState = getReadyState()
// 已连接或正在连接时跳过
if (
currentState === ReadyState.Open ||
currentState === ReadyState.Connecting
) {
return
}
clearHeartbeat()
const isClosed =
currentState === ReadyState.Closed ||
currentState === ReadyState.Closing
if (isClosed) {
connect()
} else {
disconnectFn()
setTimeout(() => {
if (
isOnlineRef.current &&
getReadyState() !== ReadyState.Open &&
getReadyState() !== ReadyState.Connecting
) {
connect()
}
}, reconnectDelay)
}
}, reconnectDebounce)
}
// 网络状态监听
let cleanupNetworkListener: (() => void) | undefined
if (enableNetworkListener && typeof window !== 'undefined') {
const handleOnline = () => {
isOnlineRef.current = true
attemptReconnect()
}
const handleOffline = () => {
isOnlineRef.current = false
handleNetworkOffline()
}
window.addEventListener('online', handleOnline)
window.addEventListener('offline', handleOffline)
cleanupNetworkListener = () => {
window.removeEventListener('online', handleOnline)
window.removeEventListener('offline', handleOffline)
}
}
// 文档可见性监听
let cleanupVisibilityListener: (() => void) | undefined
if (enableVisibilityListener && typeof document !== 'undefined') {
const handleVisibilityChange = () => {
const isVisible = !document.hidden
isVisibleRef.current = isVisible
if (isVisible && isOnlineRef.current) {
const currentState = getReadyState()
if (
currentState === ReadyState.Closed ||
currentState === ReadyState.Closing
) {
attemptReconnect()
}
}
}
document.addEventListener('visibilitychange', handleVisibilityChange)
cleanupVisibilityListener = () => {
document.removeEventListener('visibilitychange', handleVisibilityChange)
}
}
// 自动连接
if (!manual && socketUrl) {
connect()
}
// 清理函数
const cleanup = () => {
disconnectFn()
cleanupNetworkListener?.()
cleanupVisibilityListener?.()
}
const result: Result = {
sendMessage,
connect,
disconnect: disconnectFn,
get readyState() {
return getReadyState()
},
get webSocketIns() {
return websocketRef.current
},
clearHeartbeat,
switchConversation,
cleanup,
}
return result
}
export default createWebSocketClient