import { appConfig } from '@/config'; import { HttpImClient } from './httpImClient'; const HEARTBEAT_INTERVAL_MS = 25000; const RECONNECT_MIN_MS = 1000; const RECONNECT_MAX_MS = 15000; function buildWsUrl() { if (appConfig.wsUrl) { return appConfig.wsUrl; } const protocol = appConfig.useWss ? 'wss' : 'ws'; const host = appConfig.imServerHost || window.location.hostname; const port = appConfig.imRoutePort ? `:${appConfig.imRoutePort}` : ''; const path = appConfig.wsPath?.startsWith('/') ? appConfig.wsPath : `/${appConfig.wsPath || 'ws'}`; return `${protocol}://${host}${port}${path}`; } function normalizeConversationId(payload) { const target = payload.target || payload.to || payload.receiver || payload.conversationTarget || payload.from || payload.sender; return payload.conversationId || (target ? `single-${target}` : ''); } function normalizeIncomingMessage(payload, currentUserId) { const item = payload.message || payload; const from = item.from || item.sender || item.fromUser || ''; const target = item.target || item.to || item.receiver || ''; const direction = from && String(from) === String(currentUserId) ? 'out' : 'in'; const conversationId = item.conversationId || normalizeConversationId({ ...item, target: direction === 'out' ? target : from }); if (!conversationId) { return null; } const status = item.status || item.deliveryStatus || 'sent'; return { id: item.id || item.messageId || item.messageUid || `${Date.now()}${Math.random().toString(16).slice(2)}`, messageUid: item.messageUid || item.uid || item.messageId || '', conversationId, from, target, direction, type: item.type || 'text', text: item.text || item.content || item.searchableContent || '', status, deliveryStatus: item.deliveryStatus || (direction === 'out' ? status : ''), timestamp: Number(item.timestamp || item.serverTimestamp || Date.now()), displayTimestamp: Number(item.displayTimestamp || item.timestamp || item.serverTimestamp || Date.now()), raw: item }; } export class LongConnectionImClient extends HttpImClient { constructor() { super(); this.socket = null; this.wsUrl = buildWsUrl(); this.heartbeatTimer = null; this.reconnectTimer = null; this.reconnectAttempts = 0; this.manualClose = false; } getSdkVersion() { return 'JC-LC/0.1'; } shouldUseMessagePolling() { return !this.connected; } async ensureSdk() { if (typeof WebSocket === 'undefined') { throw new Error('当前浏览器不支持 WebSocket 长连接'); } return true; } connect(userId, token) { this.currentUser = { userId, token }; this.manualClose = false; this.openSocket(); } disconnect() { this.manualClose = true; this.clearReconnectTimer(); this.clearHeartbeatTimer(); if (this.socket) { this.socket.close(); this.socket = null; } super.disconnect(); } openSocket() { if (this.socket?.readyState === WebSocket.OPEN) { this.sendAuthFrame(); return; } if (this.socket?.readyState === WebSocket.CONNECTING) { return; } this.emit('connectionStatusChanged', { connected: false, statusText: '长连接连接中' }); try { this.socket = new WebSocket(this.wsUrl); this.socket.binaryType = 'arraybuffer'; } catch (error) { this.emit('connectionStatusChanged', { connected: false, statusText: '长连接创建失败,使用HTTP补偿' }); this.scheduleReconnect(); return; } this.socket.onopen = () => { this.connected = true; this.reconnectAttempts = 0; this.emit('connectionStatusChanged', { connected: true, statusText: '长连接已连接' }); this.sendAuthFrame(); this.startHeartbeat(); this.refreshConversationList(); }; this.socket.onmessage = event => { this.handleFrame(event.data); }; this.socket.onerror = () => { this.emit('connectionStatusChanged', { connected: false, statusText: '长连接异常,使用HTTP补偿' }); }; this.socket.onclose = () => { this.connected = false; this.clearHeartbeatTimer(); this.socket = null; if (this.manualClose) { return; } this.emit('connectionStatusChanged', { connected: false, statusText: '长连接重连中' }); this.scheduleReconnect(); }; } sendAuthFrame() { this.sendFrame({ type: 'auth', userId: this.currentUser?.userId || '', token: this.currentUser?.token || '', clientId: this.getClientId(), platform: appConfig.platform, timestamp: Date.now() }); } sendFrame(frame) { if (this.socket?.readyState !== WebSocket.OPEN) { return false; } this.socket.send(JSON.stringify(frame)); return true; } handleFrame(rawData) { if (rawData instanceof ArrayBuffer) { this.emit('rawFrameReceived', rawData); return; } let payload; try { payload = typeof rawData === 'string' ? JSON.parse(rawData) : rawData; } catch (error) { return; } const type = payload.type || payload.event || payload.cmd || ''; if (type === 'pong' || type === 'auth_ok' || type === 'connected') { return; } if (type === 'contactsChanged') { this.emit('contactsChanged', payload.contacts || []); return; } if (type === 'conversationsChanged') { this.emit('conversationsChanged', payload.conversations || []); return; } if (type === 'messages' && Array.isArray(payload.messages)) { payload.messages .map(message => normalizeIncomingMessage(message, this.currentUser?.userId)) .filter(Boolean) .forEach(message => this.emit('messageReceived', message)); return; } const message = normalizeIncomingMessage(payload, this.currentUser?.userId); if (message) { this.emit(message.direction === 'out' ? 'messageStatusChanged' : 'messageReceived', message); } } startHeartbeat() { this.clearHeartbeatTimer(); this.heartbeatTimer = window.setInterval(() => { this.sendFrame({ type: 'ping', timestamp: Date.now() }); }, HEARTBEAT_INTERVAL_MS); } clearHeartbeatTimer() { if (this.heartbeatTimer) { window.clearInterval(this.heartbeatTimer); this.heartbeatTimer = null; } } scheduleReconnect() { this.clearReconnectTimer(); const delay = Math.min(RECONNECT_MAX_MS, RECONNECT_MIN_MS * 2 ** this.reconnectAttempts); this.reconnectAttempts += 1; this.reconnectTimer = window.setTimeout(() => { this.openSocket(); }, delay); } clearReconnectTimer() { if (this.reconnectTimer) { window.clearTimeout(this.reconnectTimer); this.reconnectTimer = null; } } }