Files
phone-app------test1-/utils/socket.js
2026-06-02 10:42:33 +08:00

366 lines
8.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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