const WS_APP_URL = 'ws://cloud_test.yuxindazhineng.com' // (工具类)连接、发送、接收、重连... class WebSocketManager { constructor() { this.ws = null this.url = `${WS_APP_URL}/cloud_api/phone/app_ws` this.options = { token: '', conversationId: '', onMessage: null, onError: null, onReconnect: null, onOpen: null, onClose: null } this.reconnectAttempts = 0 this.maxReconnectAttempts = 5 this.reconnectDelay = 3000 this.reconnectTimer = null this.heartbeatTimer = null this.isManualClose = false } /** * 初始化连接 * @param {string} url WebSocket URL * @param {Object} options 配置选项 */ connect(options = {}) { this.options = { ...this.options, ...options } this.isManualClose = false // 关闭现有连接 if (this.ws) { this.close() } try { // #ifdef APP-PLUS || MP-WEIXIN this.ws = uni.connectSocket({ url: this.url, success: () => { console.log('WebSocket 连接请求已发送'); }, fail: (err) => { console.error('WebSocket 连接失败:', err) this.handleError(err) } }) // #endif // #ifdef H5 if (!this.ws) { this.ws = new WebSocket(this.url) } // #endif this.initEventHandlers() } catch (err) { this.handleError(err) } } /** * 初始化事件处理 */ initEventHandlers() { if (!this.ws) return // #ifdef APP-PLUS || MP-WEIXIN // 打开连接 this.ws.onOpen((event) => { console.log('WebSocket 已连接') // 修复:先判断重连状态,再重置计数 if (this.reconnectAttempts > 0) { this.options.onReconnect?.() } this.options.onOpen?.(event) this.reconnectAttempts = 0 // this.startHeartbeat() }) this.ws.onMessage((event) => { try { const data = JSON.parse(event.data) console.log('收到了返回消息:', data); // uni.setStorageSync('currentSessionId', data.task_call_id) this.handleMessage(data) } catch (err) { console.error('消息解析失败:', err) uni.setStorageSync('currentSessionId', event.data.task_call_id) } }) this.ws.onError((err) => { console.error('WebSocket 错误:', err) this.handleError(err) }) this.ws.onClose((event) => { console.log('WebSocket 已关闭:', event.code, event.reason) this.options.onClose?.(event) this.stopHeartbeat() if (!this.isManualClose && this.reconnectAttempts < this.maxReconnectAttempts) { this.reconnect() } }) // #endif // #ifdef H5 // H5 环境使用原生 WebSocket API if (typeof WebSocket !== 'undefined') { this.ws.onopen = (event) => { console.log('WebSocket 已连接 (H5)') if (this.reconnectAttempts > 0) { this.options.onReconnect?.() } this.options.onOpen?.(event) this.reconnectAttempts = 0 } this.ws.onmessage = (event) => { try { const data = JSON.parse(event.data) console.log('收到了返回消息 (H5):', data); this.handleMessage(data) } catch (err) { console.error('消息解析失败 (H5):', err) } } this.ws.onerror = (err) => { console.error('WebSocket 错误 (H5):', err) this.handleError(err) } this.ws.onclose = (event) => { console.log('WebSocket 已关闭 (H5):', event.code, event.reason) this.options.onClose?.(event) this.stopHeartbeat() if (!this.isManualClose && this.reconnectAttempts < this.maxReconnectAttempts) { this.reconnect() } } } // #endif } /** * 处理服务端推送的消息 * @param {Object} msg - 服务端返回的原始消息对象 */ handleMessage(msg) { this.options.onMessage?.(msg) const type = msg.type // const data = msg.data || {} // 连接确认:服务端认证成功响应 if (type === 'auth_ok') { // 发送消息携带的上一次返回的验证id // const taskCallId = data.task_call_id // uni.setStorageSync('taskCallId', taskCallId) console.log('认证成功,socket连接成功'); return } // 断开连接指令:服务端主动要求断开 if (type === 'auth_fail') { if(msg?.reason === "pc offline") console.log("pc端不在线,不可聊天"); // console.log('服务端要求断开连接') this.isManualClose = true this.close() return } } /** * 统一发送消息方法 * 兼容 APP / 微信小程序 环境 * @param {Object} data - 要发送的消息对象 * @returns {Promise} 发送结果 */ send(data) { return new Promise((resolve, reject) => { // 未连接直接拒绝 if (!this.isConnected()) { reject(new Error('WebSocket 未连接')) return } const msg = JSON.stringify(data) console.log("发送的消息是:", msg); // #ifdef APP-PLUS || MP-WEIXIN // APP / 小程序环境:uni-app 封装 API this.ws.send({ data: msg, success: () => resolve(), fail: (err) => { console.error('发送消息失败:', err) reject(err) } }) // #endif // #ifdef H5 // H5 环境:原生 WebSocket API if (this.ws.readyState === WebSocket.OPEN) { this.ws.send(msg) resolve() } else { reject(new Error('WebSocket 未打开')) } // #endif }) } /** * 自动重连机制 * 指数退避重连,避免频繁重连 */ reconnect() { // 已有重连定时器则不重复触发 if (this.reconnectTimer) return this.reconnectAttempts++ console.log(`尝试重连 (${this.reconnectAttempts}/${this.maxReconnectAttempts})...`) // 延迟执行重连,延迟时间随次数递增 this.reconnectTimer = setTimeout(() => { this.reconnectTimer = null // 非手动关闭才重连 if (!this.isManualClose) { this.connect(this.options) } }, this.reconnectDelay * this.reconnectAttempts) } /** * 启动心跳定时器 * 每30秒发送一次 ping 保持连接 */ startHeartbeat() { this.stopHeartbeat() this.heartbeatTimer = setInterval(() => { if (this.isConnected()) { this.send({ ws_event: 'ping' }).catch(() => { console.log('心跳发送失败,准备重连') // 避免在已经重连过程中重复触发 if (!this.reconnectTimer && !this.isManualClose) { this.reconnect() } }) } else if (!this.isManualClose && this.reconnectAttempts < this.maxReconnectAttempts) { // 连接已断开但未触发 close 事件时主动重连 this.reconnect() } }, 30000) } /** * 停止心跳定时器 */ stopHeartbeat() { if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer) this.heartbeatTimer = null } } /** * 统一错误处理 * @param {Error} error - 错误对象 */ handleError(error) { console.error('WebSocket 错误处理:', JSON.stringify(error)) this.options.onError?.(error) } /** * 判断 WebSocket 是否处于已连接状态 * @returns {boolean} */ isConnected() { if (!this.ws) return false // #ifdef APP-PLUS || MP-WEIXIN return this.ws.readyState === 1 // #endif // #ifdef H5 return this.ws.readyState === WebSocket.OPEN // #endif } /** * 方便外部获取连接状态码 * @returns {number} 0:连接中,1:已连接,2:关闭中,3:已关闭 */ getReadyState() { if (!this.ws) return 3 // #ifdef APP-PLUS || MP-WEIXIN return this.ws.readyState // #endif // #ifdef H5 return this.ws.readyState // #endif } /** * 手动关闭 WebSocket 连接 * 清理所有定时器、标记手动关闭、停止重连 */ close() { console.log('手动关闭 WebSocket 连接') this.isManualClose = true this.stopHeartbeat() // 清理重连定时器 if (this.reconnectTimer) { clearTimeout(this.reconnectTimer) this.reconnectTimer = null } if (this.ws) { // 移除所有事件监听,避免内存泄漏 // #ifdef APP-PLUS || MP-WEIXIN if (this.ws.onOpen) this.ws.onOpen(null) if (this.ws.onMessage) this.ws.onMessage(null) if (this.ws.onError) this.ws.onError(null) if (this.ws.onClose) this.ws.onClose(null) // #endif try { // #ifdef APP-PLUS || MP-WEIXIN this.ws.close({ success: () => console.log('连接已关闭'), fail: (err) => console.error('关闭失败:', err) }) // #endif // #ifdef H5 this.ws.close() // #endif } catch (error) { console.error('关闭连接出错:', error) } this.ws = null } // 重置重连计数 this.reconnectAttempts = 0 } /** * 更新配置项 * 合并新配置,不覆盖原有配置 * @param {Object} options - 新配置 */ updateOptions(options) { this.options = { ...this.options, ...options } } } // 单例实例化,全局唯一 export const socketManager = new WebSocketManager() export default socketManager