import env from '../config/env.js' const HEARTBEAT_CMD = { cmd: 1 } function safeJsonParse(data) { if (typeof data !== 'string') return data try { return JSON.parse(data) } catch (error) { return data } } function getHostFromBaseUrl(baseUrl) { return String(baseUrl || '') .replace(/^https?:\/\//, '') .replace(/\/+$/, '') } function getShortStack() { try { const stack = new Error().stack || '' return stack.split('\n').slice(2, 8).join('\n') } catch (error) { return '' } } class WebSocketService { constructor() { this.socketTask = null this.isConnected = false this.isManualClose = false this.connectingPromise = null this.connectOptions = null this.reconnectAttempts = 0 this.maxReconnectAttempts = 5 this.reconnectInterval = 3000 this.reconnectTimer = null this.heartbeatInterval = 20000 this.heartbeatTimer = null this.listeners = {} this.useGlobalSocketApi = false this.globalSocketHandlers = null } on(eventName, handler) { if (!eventName || typeof handler !== 'function') return () => {} if (!this.listeners[eventName]) this.listeners[eventName] = new Set() this.listeners[eventName].add(handler) return () => this.off(eventName, handler) } off(eventName, handler) { if (!eventName || !this.listeners[eventName]) return if (!handler) { this.listeners[eventName].clear() return } this.listeners[eventName].delete(handler) } emit(eventName, payload) { const group = this.listeners[eventName] if (!group || !group.size) return group.forEach((fn) => { try { fn(payload) } catch (error) { console.error(`[WebSocket] listener error for ${eventName}:`, error) } }) } buildSocketUrl(options = {}) { const role = options.role || 'parent' // const base = 'wss://yx.newfeifan.cn/btcSkt' // 开发地址(保留注释):const base = 'ws://192.168.220.13:8080/btcSkt' const base = 'wss://m.hfdcschool.com/btcSkt' // 原 env 写法(保留注释):const base = `wss://${getHostFromBaseUrl(env.baseUrl)}/btcSkt` if (role === 'device') { const ssDevId = options.ssDevId || '' if (!ssDevId) throw new Error('缺少 ssDevId') return `${base}?ssDevId=${encodeURIComponent(ssDevId)}` } const ssToken = options.ssToken || '' if (!ssToken) throw new Error('缺少 ssToken') return `${base}?ssToken=${encodeURIComponent(ssToken)}` } async connect(options = {}) { console.log('[WebSocket] connect called', { isConnected: this.isConnected, hasSocketTask: !!this.socketTask, hasConnectingPromise: !!this.connectingPromise, options, stack: getShortStack() }) if (this.isConnected && this.socketTask && !options.forceReconnect) { console.log('[WebSocket] connect skip: already connected') return true } if (this.connectingPromise) { console.log('[WebSocket] connect reuse: existing connectingPromise') return this.connectingPromise } if (options.forceReconnect) { await this.disconnect() } if (this.socketTask && !this.isConnected && !options.forceReconnect) { console.warn('[WebSocket] found stale socketTask while disconnected, cleanup before reconnect') await this.disconnect() } const merged = { role: options.role || this.connectOptions?.role || 'parent', ssToken: options.ssToken || this.connectOptions?.ssToken || '', ssDevId: options.ssDevId || this.connectOptions?.ssDevId || '', heartbeat: options.heartbeat ?? this.connectOptions?.heartbeat ?? false, heartbeatInterval: options.heartbeatInterval || this.connectOptions?.heartbeatInterval || this.heartbeatInterval, autoReconnect: options.autoReconnect ?? this.connectOptions?.autoReconnect ?? true, maxReconnectAttempts: options.maxReconnectAttempts || this.connectOptions?.maxReconnectAttempts || this.maxReconnectAttempts, reconnectInterval: options.reconnectInterval || this.connectOptions?.reconnectInterval || this.reconnectInterval } this.connectOptions = merged this.heartbeatInterval = merged.heartbeatInterval this.maxReconnectAttempts = merged.maxReconnectAttempts this.reconnectInterval = merged.reconnectInterval this.isManualClose = false const wsUrl = this.buildSocketUrl(merged) console.log('[WebSocket] connecting...', { wsUrl, merged }) const socketApi = typeof wx !== 'undefined' ? wx : uni this.connectingPromise = new Promise((resolve, reject) => { let settled = false let openTimeout = null const resolveOnce = (value) => { if (settled) return settled = true resolve(value) } const rejectOnce = (error) => { if (settled) return settled = true reject(error) } const handleOpen = () => { if (openTimeout) clearTimeout(openTimeout) this.isConnected = true this.reconnectAttempts = 0 this.connectingPromise = null if (merged.heartbeat) { this.startHeartbeat() } else { this.stopHeartbeat() } this.emit('open', { url: wsUrl, options: merged }) console.log('[WebSocket] open:', wsUrl) resolveOnce(true) } const handleMessage = (res) => { const data = safeJsonParse(res?.data) this.emit('message', data) if (data && typeof data === 'object' && data.cmd !== undefined) { this.emit(`cmd:${data.cmd}`, data) if (data.cmd === 51) this.emit('refresh', data) } } const handleError = (error) => { console.error('[WebSocket] error:', { wsUrl, error }) this.emit('error', error) if (!this.isConnected && this.connectingPromise) { this.connectingPromise = null rejectOnce(error) } } const handleClose = (res) => { console.log('[WebSocket] close:', { wsUrl, isManualClose: this.isManualClose, reconnectEnabled: !!this.connectOptions?.autoReconnect, reconnectAttempts: this.reconnectAttempts, closeEvent: res }) this.isConnected = false this.stopHeartbeat() this.emit('close', res) if (!this.isManualClose && this.connectOptions?.autoReconnect) { this.tryReconnect() } } let task = null try { task = socketApi.connectSocket({ url: wsUrl }) } catch (error) { console.error('[WebSocket] connectSocket throw:', { wsUrl, error }) this.connectingPromise = null rejectOnce(error) return } if (task && typeof task.onOpen === 'function') { this.useGlobalSocketApi = false this.socketTask = task task.onOpen(handleOpen) task.onMessage(handleMessage) task.onError(handleError) task.onClose(handleClose) console.log('[WebSocket] using task-level socket handlers') return } if ( typeof socketApi.onSocketOpen !== 'function' || typeof socketApi.onSocketMessage !== 'function' || typeof socketApi.onSocketError !== 'function' || typeof socketApi.onSocketClose !== 'function' ) { this.connectingPromise = null rejectOnce(new Error('创建 WebSocket 失败')) return } this.useGlobalSocketApi = true this.socketTask = { __global: true } const openHandler = () => handleOpen() const messageHandler = (res) => handleMessage(res) const errorHandler = (err) => handleError(err) const closeHandler = (res) => handleClose(res) this.globalSocketHandlers = { openHandler, messageHandler, errorHandler, closeHandler } socketApi.onSocketOpen(openHandler) socketApi.onSocketMessage(messageHandler) socketApi.onSocketError(errorHandler) socketApi.onSocketClose(closeHandler) console.log('[WebSocket] using global socket handlers') openTimeout = setTimeout(() => { this.connectingPromise = null rejectOnce(new Error('WebSocket 连接超时(未收到 onSocketOpen)')) }, 15000) }) return this.connectingPromise } tryReconnect() { if (this.reconnectTimer) return if (this.reconnectAttempts >= this.maxReconnectAttempts) return this.reconnectAttempts += 1 console.warn('[WebSocket] schedule reconnect', { reconnectAttempts: this.reconnectAttempts, maxReconnectAttempts: this.maxReconnectAttempts, reconnectInterval: this.reconnectInterval, connectOptions: this.connectOptions }) this.reconnectTimer = setTimeout(async () => { this.reconnectTimer = null try { console.warn('[WebSocket] reconnecting now', { reconnectAttempts: this.reconnectAttempts }) await this.connect(this.connectOptions || {}) } catch (error) { console.error('[WebSocket] reconnect failed', error) this.tryReconnect() } }, this.reconnectInterval) } startHeartbeat() { this.stopHeartbeat() console.log('[WebSocket] heartbeat start', { heartbeatInterval: this.heartbeatInterval }) this.heartbeatTimer = setInterval(() => { if (!this.isConnected) return console.log('[WebSocket] heartbeat send', HEARTBEAT_CMD) this.send(HEARTBEAT_CMD).catch(() => {}) }, this.heartbeatInterval) } stopHeartbeat() { if (!this.heartbeatTimer) return console.log('[WebSocket] heartbeat stop', { isConnected: this.isConnected, stack: getShortStack() }) clearInterval(this.heartbeatTimer) this.heartbeatTimer = null } async disconnect() { console.log('[WebSocket] disconnect called', { hasSocketTask: !!this.socketTask, useGlobalSocketApi: this.useGlobalSocketApi }) this.isManualClose = true this.stopHeartbeat() if (this.reconnectTimer) { clearTimeout(this.reconnectTimer) this.reconnectTimer = null } const socketApi = typeof wx !== 'undefined' ? wx : uni return new Promise((resolve) => { if (!this.socketTask) { this.socketTask = null this.useGlobalSocketApi = false this.isConnected = false this.connectingPromise = null resolve() return } if (this.useGlobalSocketApi || this.socketTask.__global) { if (this.globalSocketHandlers) { const { openHandler, messageHandler, errorHandler, closeHandler } = this.globalSocketHandlers if (typeof socketApi.offSocketOpen === 'function') socketApi.offSocketOpen(openHandler) if (typeof socketApi.offSocketMessage === 'function') socketApi.offSocketMessage(messageHandler) if (typeof socketApi.offSocketError === 'function') socketApi.offSocketError(errorHandler) if (typeof socketApi.offSocketClose === 'function') socketApi.offSocketClose(closeHandler) this.globalSocketHandlers = null } socketApi.closeSocket({ complete: () => { this.socketTask = null this.useGlobalSocketApi = false this.isConnected = false this.connectingPromise = null resolve() } }) return } if (typeof this.socketTask.close !== 'function') { this.socketTask = null this.useGlobalSocketApi = false this.isConnected = false this.connectingPromise = null resolve() return } this.socketTask.close({ complete: () => { this.socketTask = null this.useGlobalSocketApi = false this.isConnected = false this.connectingPromise = null resolve() } }) }) } async send(payload) { if (!this.socketTask || !this.isConnected) { throw new Error('WebSocket 未连接') } const socketApi = typeof wx !== 'undefined' ? wx : uni return new Promise((resolve, reject) => { if (this.useGlobalSocketApi || this.socketTask.__global) { socketApi.sendSocketMessage({ data: JSON.stringify(payload), success: () => resolve(true), fail: (error) => reject(error) }) return } if (typeof this.socketTask.send !== 'function') { reject(new Error('WebSocket 发送不可用')) return } this.socketTask.send({ data: JSON.stringify(payload), success: () => resolve(true), fail: (error) => reject(error) }) }) } async ensureConnected(options = {}) { console.log('[WebSocket] ensureConnected called', { isConnected: this.isConnected, hasSocketTask: !!this.socketTask, options, stack: getShortStack() }) if (this.isConnected && this.socketTask) return true await this.connect(options) return true } } const websocketService = new WebSocketService() export default websocketService