topfans/frontend/utils/socket/SocketManager.js
Lenticular Studio Agent af7908e72e feat: 接入微达API中转站,重构镭射卡生图流程
- 替换中转站从 xbcl.link 到 weda.cc
- prompt 模板改为镭射卡全图生成(去掉 6 层合成/抠图依赖)
- 4 路并发调用 + 原图展示 = 5 张 variant
- 前端提示词中译英支持
- 全局 Vue errorHandler
- WebSocket 鉴权失败跳登录
- 删除已弃用的 laserCompositor 微服务

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-23 22:43:49 +08:00

358 lines
12 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.

import { getWebSocketBaseUrl } from '../api'
/**
* WebSocket 管理器
* 支持多个 WebSocket 连接,自动管理鉴权、心跳、重连
*/
class SocketManager {
constructor(options = {}) {
this.serviceName = options.serviceName || 'unknown'
this.baseUrl = 'ws://127.0.0.1:8080' // 占位符,等 connect() 时再获取真实地址
this.token = null
this.socket = null
this.heartbeatTimer = null
this.reconnectTimer = null
this.reconnectInterval = options.reconnectInterval || 3000
this.heartbeatInterval = options.heartbeatInterval || 30000
this.maxReconnectAttempts = options.maxReconnectAttempts || 5
this.reconnectAttempts = 0
// 状态
this.isConnected = false
this.isAuthed = false
this.isClosing = false // 标记是否主动关闭
// 事件处理器
this.eventHandlers = {
'connect': [],
'disconnect': [],
'auth_success': [],
'auth_fail': [],
'error': [],
'message': [] // 通用消息处理
}
// 子类可覆盖的消息类型处理
this.messageHandlers = {}
}
/**
* 连接到 WebSocket 服务器
*/
async connect(token, path) {
this.token = token
this.path = path
this.reconnectAttempts = 0
this.isClosing = false // 重置关闭状态,允许重连
// 异步获取真实 WebSocket 地址(等待环境检测完成)
this.baseUrl = await getWebSocketBaseUrl()
console.log(`[${this.serviceName}] WebSocket base URL: ${this.baseUrl}`)
this._doConnect()
}
_doConnect() {
// 如果已有连接且已连接,不重复连接
if (this.socket && this.isConnected) {
console.log(`[${this.serviceName}] Already connected (${this.isConnected}), skip reconnect`)
return
}
console.log(`[${this.serviceName}] _doConnect called, clearing old socket`)
// 清理旧连接
if (this.socket) {
this.socket = null
}
this.isConnected = false
const url = `${this.baseUrl}${this.path}?token=Bearer_${this.token}`
console.log(`[${this.serviceName}] Connecting to ${url}`)
// UniApp: connectSocket 是异步的,需要处理错误
try {
this.socket = uni.connectSocket({
url,
fail: (err) => {
console.error(`[${this.serviceName}] connectSocket fail:`, err)
var errMsg = (err && (err.errMsg || '')).toLowerCase()
if (errMsg.indexOf('auth') !== -1 || errMsg.indexOf('reject') !== -1 || errMsg.indexOf('401') !== -1) {
console.warn(`[${this.serviceName}] Connection rejected (auth failure)`)
this._emit('auth_fail', { code: 'CONNECT_FAILED', message: err.errMsg || '连接失败' })
this.close()
return
}
this._emit('error', { code: 'CONNECT_FAILED', message: err.errMsg || '连接失败' })
}
})
if (!this.socket) {
console.error(`[${this.serviceName}] socket is null`)
return
}
console.log(`[${this.serviceName}] SocketTask created, checking methods:`, Object.keys(this.socket))
this._setupListeners()
} catch (err) {
console.error(`[${this.serviceName}] Exception during connect:`, err)
}
}
_setupListeners() {
// 检查 socket 是否有效
if (!this.socket) {
console.error(`[${this.serviceName}] socket is null in _setupListeners`)
return
}
// UniApp SocketTask API: onOpen/onClose/onError/onMessage 是设置回调的方法
// 也可能是事件名是 'open', 'close', 'error', 'message'
const socket = this.socket
const self = this
// 连接打开
if (typeof socket.onOpen === 'function') {
socket.onOpen(function() {
console.log(`[${self.serviceName}] WebSocket connected`)
self.isConnected = true
// 清除重连计时器
if (self.reconnectTimer) {
clearTimeout(self.reconnectTimer)
self.reconnectTimer = null
}
self.reconnectAttempts = 0
self._emit('connect')
})
} else if (typeof socket.onopen === 'function') {
// 标准 WebSocket 风格
socket.onopen(function() {
console.log(`[${self.serviceName}] WebSocket connected`)
self.isConnected = true
// 清除重连计时器
if (self.reconnectTimer) {
clearTimeout(self.reconnectTimer)
self.reconnectTimer = null
}
self.reconnectAttempts = 0
self._emit('connect')
})
} else {
console.warn(`[${self.serviceName}] Unknown socket API, socket keys:`, Object.keys(socket))
}
// 接收消息
if (typeof socket.onMessage === 'function') {
socket.onMessage(function(event) {
const data = JSON.parse(event.data)
self._handleMessage(data)
})
} else if (typeof socket.onmessage === 'function') {
socket.onmessage(function(event) {
const data = JSON.parse(event.data)
self._handleMessage(data)
})
}
// 连接关闭
if (typeof socket.onClose === 'function') {
socket.onClose(function() {
console.log(`[${self.serviceName}] WebSocket closed`)
self._cleanup()
self._emit('disconnect')
self._tryReconnect()
})
} else if (typeof socket.onclose === 'function') {
socket.onclose(function() {
console.log(`[${self.serviceName}] WebSocket closed`)
self._cleanup()
self._emit('disconnect')
self._tryReconnect()
})
}
// 连接错误
var handleSocketError = function(err) {
console.error(`[${self.serviceName}] WebSocket error:`, err)
// 检查是否是鉴权相关的错误401/403
var errMsg = (err && (err.errMsg || err.message || '')).toLowerCase()
if (errMsg.indexOf('auth') !== -1 || errMsg.indexOf('reject') !== -1 || errMsg.indexOf('401') !== -1) {
console.warn(`[${self.serviceName}] Connection rejected (auth failure), clearing token`)
self._emit('auth_fail', err)
self.close()
return
}
self._emit('error', err)
}
if (typeof socket.onError === 'function') {
socket.onError(handleSocketError)
} else if (typeof socket.onerror === 'function') {
socket.onerror(handleSocketError)
}
}
_handleMessage(data) {
// 触发通用消息事件
this._emit('message', data)
// 根据消息类型处理
const { type, action } = data
// 1. 鉴权响应(通用)
if (type === 'auth_response') {
if (data.success) {
this.isAuthed = true
this._emit('auth_success', data)
this._startHeartbeat()
} else {
this.isAuthed = false
this._emit('auth_fail', data)
this.close()
}
return
}
// 2. 心跳响应(通用)
if (type === 'pong') {
console.log(`[${this.serviceName}] Heartbeat received`)
return
}
// 3. 错误响应(通用)
if (type === 'error') {
this._emit('error', data)
return
}
// 4. 服务特定消息类型处理
const handler = this.messageHandlers[type] || this.messageHandlers[action]
if (handler) {
handler(data)
}
}
/**
* 发送消息
*/
send(data) {
if (!this.socket || !this.isConnected) {
console.warn(`[${this.serviceName}] Socket not connected`)
return false
}
// UniApp: socket.send({ data, success, fail })
if (typeof this.socket.send === 'function') {
this.socket.send({
data: JSON.stringify(data),
fail: (err) => {
console.error(`[${this.serviceName}] send fail:`, err)
}
})
} else {
console.warn(`[${this.serviceName}] socket.send is not a function`)
}
return true
}
/**
* 发送心跳
*/
_startHeartbeat() {
this._stopHeartbeat()
this.heartbeatTimer = setInterval(() => {
if (this.isConnected) {
this.send({ action: 'ping' })
}
}, this.heartbeatInterval)
}
_stopHeartbeat() {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer)
this.heartbeatTimer = null
}
}
/**
* 重连机制
*/
_tryReconnect() {
if (this.isClosing) {
console.log(`[${this.serviceName}] Closing intentionally, skip reconnect`)
return
}
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.warn(`[${this.serviceName}] Max reconnect attempts reached`)
this._emit('error', { code: 'RECONNECT_FAILED', message: '重连次数已达上限' })
return
}
this.reconnectAttempts++
console.log(`[${this.serviceName}] Auto reconnecting... (${this.reconnectAttempts}/${this.maxReconnectAttempts})`)
this.reconnectTimer = setTimeout(() => {
this._doConnect()
}, this.reconnectInterval)
}
_cleanup() {
this._stopHeartbeat()
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer)
this.reconnectTimer = null
}
this.isConnected = false
this.isAuthed = false
}
/**
* 关闭连接
*/
close() {
this.isClosing = true
this._cleanup()
if (this.socket) {
// UniApp: socket.close() 可能不存在,用 complete 代替
if (typeof this.socket.close === 'function') {
this.socket.close()
} else if (typeof this.socket.complete === 'function') {
this.socket.complete()
}
this.socket = null
}
}
// ===== 事件系统 =====
on(event, handler) {
if (this.eventHandlers[event]) {
this.eventHandlers[event].push(handler)
}
return () => this.off(event, handler) // 返回取消订阅函数
}
off(event, handler) {
if (this.eventHandlers[event]) {
this.eventHandlers[event] = this.eventHandlers[event].filter(h => h !== handler)
}
}
_emit(event, data) {
if (this.eventHandlers[event]) {
this.eventHandlers[event].forEach(handler => handler(data))
}
}
// ===== 订阅特定消息类型 =====
/**
* 注册特定消息类型的处理器
* @param {string} type 消息类型或 action
* @param {function} handler 处理函数
*/
registerHandler(type, handler) {
this.messageHandlers[type] = handler
}
}
/** 导出 */
export default SocketManager