- 替换中转站从 xbcl.link 到 weda.cc - prompt 模板改为镭射卡全图生成(去掉 6 层合成/抠图依赖) - 4 路并发调用 + 原图展示 = 5 张 variant - 前端提示词中译英支持 - 全局 Vue errorHandler - WebSocket 鉴权失败跳登录 - 删除已弃用的 laserCompositor 微服务 Co-Authored-By: Claude <noreply@anthropic.com>
358 lines
12 KiB
JavaScript
358 lines
12 KiB
JavaScript
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 |