/** * XYZW WebSocket 客户端 * 基于 readable-xyzw-ws.js 重构,适配本项目架构 */ import { bonProtocol, g_utils } from './bonProtocol.js' /** 生成 [min,max] 的随机整数 */ const randInt = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min /** Promise 版 sleep */ const sleep = (ms) => new Promise((res) => setTimeout(res, ms)) /** * 命令注册器:保存每个 cmd 的默认体,发送时与 params 合并 */ export class CommandRegistry { constructor(encoder, enc) { this.encoder = encoder this.enc = enc this.commands = new Map() } /** 注册命令 */ register(cmd, defaultBody = {}) { this.commands.set(cmd, (ack = 0, seq = 0, params = {}) => ({ cmd, ack, seq, code: 0, rtt: randInt(0, 500), time: Date.now(), body: this.encoder?.bon?.encode ? this.encoder.bon.encode({ ...defaultBody, ...params }) : undefined, c: undefined, hint: undefined, })) return this } /** 特例:系统心跳的 ack 用的是 "_sys/ack" */ registerHeartbeat() { this.commands.set("heart_beat", (ack, seq) => ({ cmd: "_sys/ack", ack, seq, time: Date.now(), body: undefined, c: undefined, hint: undefined, })) return this } /** 生成最终可发送的二进制 */ encodePacket(raw) { if (this.encoder?.encode && this.enc) { // 使用加密编码 return this.encoder.encode(raw, this.enc) } else { // 降级到JSON字符串 return JSON.stringify(raw) } } /** 构造报文 */ build(cmd, ack, seq, params) { const fn = this.commands.get(cmd) if (!fn) throw new Error(`Unknown cmd: ${cmd}`) return fn(ack, seq, params) } } /** 预注册游戏命令 */ export function registerDefaultCommands(reg) { return reg.registerHeartbeat() // 角色/系统 .register("role_getroleinfo", { clientVersion: "1.65.3-wx", inviteUid: 0, platform: "hortor", platformExt: "mix", scene: "", }) .register("system_getdatabundlever", { isAudit: false }) .register("system_buygold", { buyNum: 1 }) .register("system_claimhangupreward") .register("system_signinreward") // 任务相关 .register("task_claimdailypoint", { taskId: 1 }) .register("task_claimdailyreward", { rewardId: 0 }) .register("task_claimweekreward", { rewardId: 0 }) // 好友/招募 .register("friend_batch", { friendId: 0 }) .register("hero_recruit", { byClub: false, recruitNumber: 1, recruitType: 3 }) .register("item_openbox", { itemId: 2001, number: 10 }) // 竞技场 .register("arena_startarea") .register("arena_getareatarget", { refresh: false }) .register("fight_startareaarena", { targetId: 530479307 }) .register("arena_getarearank", { rankType: 0 }) // 商店 .register("store_goodslist", { storeId: 1 }) .register("store_buy", { goodsId: 1 }) .register("store_refresh", { storeId: 1 }) // 军团 .register("legion_getinfo") .register("legion_signin") .register("legion_getwarrank") // 邮件 .register("mail_getlist", { category: [0, 4, 5], lastId: 0, size: 60 }) .register("mail_claimallattachment", { category: 0 }) // 学习问答 .register("study_startgame") .register("study_answer") .register("study_claimreward", { rewardId: 1 }) // 战斗相关 .register("fight_starttower") .register("fight_startboss") .register("fight_startlegionboss") .register("fight_startdungeon") .register("fight_startpvp") // 瓶子机器人 .register("bottlehelper_claim") .register("bottlehelper_start", { bottleType: -1 }) .register("bottlehelper_stop", { bottleType: -1 }) // 军团匹配和签到 .register("legionmatch_rolesignup") .register("legion_signin") // 神器抽奖 .register("artifact_lottery", { lotteryNumber: 1, newFree: true, type: 1 }) // 爬塔相关 .register("tower_getinfo") .register("tower_claimreward") // 队伍相关 .register("presetteam_getteam") .register("presetteam_setteam") .register("presetteam_saveteam") .register("role_gettargetteam") // 排名相关 .register("rank_getroleinfo") // 梦魇相关 .register("nightmare_getroleinfo") } /** * XYZW WebSocket 客户端 */ export class XyzwWebSocketClient { constructor({ url, utils, heartbeatMs = 5000 }) { this.url = url this.utils = utils || g_utils this.enc = this.utils?.getEnc ? this.utils.getEnc("auto") : undefined this.socket = null this.ack = 1 this.seq = 0 this.sendQueue = [] this.sendQueueTimer = null this.heartbeatTimer = null this.heartbeatInterval = heartbeatMs this.dialogStatus = false this.messageListener = null this.showMsg = false this.connected = false this.promises = Object.create(null) this.registry = registerDefaultCommands(new CommandRegistry(this.utils, this.enc)) console.log('🔧 WebSocket客户端初始化:', { url: this.url, hasUtils: !!this.utils, hasEnc: !!this.enc, hasEncoder: !!this.utils?.encode }) // 状态回调 this.onConnect = null this.onDisconnect = null this.onError = null } /** 初始化连接 */ init() { console.log(`🔗 连接 WebSocket: ${this.url}`) this.socket = new WebSocket(this.url) this.socket.onopen = () => { console.log(`✅ WebSocket 连接成功`) this.connected = true console.log(`🔄 启动心跳机制,间隔: ${this.heartbeatInterval}ms`) this._setupHeartbeat() console.log(`🔄 启动消息队列处理`) this._processQueueLoop() if (this.onConnect) this.onConnect() } this.socket.onmessage = (evt) => { try { let packet if (typeof evt.data === "string") { packet = JSON.parse(evt.data) } else if (evt.data instanceof ArrayBuffer) { // 二进制数据需要自动检测并解码 packet = this.utils?.parse ? this.utils.parse(evt.data, "auto") : evt.data } else if (evt.data instanceof Blob) { // 处理Blob数据 console.log('📦 收到Blob数据, 大小:', evt.data.size) evt.data.arrayBuffer().then(buffer => { try { packet = this.utils?.parse ? this.utils.parse(buffer, "auto") : buffer console.log('📦 Blob解析结果:', packet) // 处理消息体解码(ProtoMsg会自动解码) if (packet instanceof Object && packet.rawData !== undefined) { console.log('✅ ProtoMsg消息,使用rawData:', packet.rawData) } else if (packet.body && packet.body instanceof Uint8Array) { try { if (this.utils && this.utils.bon && this.utils.bon.decode) { const decodedBody = this.utils.bon.decode(packet.body) console.log('✅ 手动解码消息体成功:', decodedBody) // 不修改packet.body,而是创建一个新的属性存储解码后的数据 packet.decodedBody = decodedBody } else { console.warn('⚠️ BON解码器不可用:', this.utils) } } catch (error) { console.warn('❌ 消息体解码失败:', error) } } if (this.showMsg) { console.log(`📨 收到消息(Blob解析后):`, packet) } // 回调处理 if (this.messageListener) { this.messageListener(packet) } // Promise 响应处理 this._handlePromiseResponse(packet) } catch (error) { console.error('❌ Blob解析失败:', error) } }) return // 异步处理,直接返回 } else { console.warn('⚠️ 未知数据类型:', typeof evt.data, evt.data) packet = evt.data } if (this.showMsg) { console.log(`📨 收到消息:`, packet) } // 处理消息体解码(ProtoMsg会自动解码) if (packet instanceof Object && packet.rawData !== undefined) { console.log('✅ ProtoMsg消息,使用rawData:', packet.rawData) } else if (packet.body && packet.body instanceof Uint8Array) { try { if (this.utils && this.utils.bon && this.utils.bon.decode) { const decodedBody = this.utils.bon.decode(packet.body) console.log('✅ 手动解码消息体成功:', decodedBody) // 不修改packet.body,而是创建一个新的属性存储解码后的数据 packet.decodedBody = decodedBody } else { console.warn('⚠️ BON解码器不可用:', this.utils) } } catch (error) { console.warn('❌ 消息体解码失败:', error) } } // 回调处理 if (this.messageListener) { this.messageListener(packet) } // Promise 响应处理 this._handlePromiseResponse(packet) } catch (error) { console.error(`❌ 消息处理失败:`, error) } } this.socket.onclose = (evt) => { console.log(`🔌 WebSocket 连接关闭:`, evt.code, evt.reason) console.log(`🔍 关闭详情:`, { code: evt.code, reason: evt.reason || '未提供原因', wasClean: evt.wasClean, timestamp: new Date().toISOString() }) this.connected = false this._clearTimers() if (this.onDisconnect) this.onDisconnect(evt) } this.socket.onerror = (error) => { console.error(`❌ WebSocket 错误:`, error) this.connected = false this._clearTimers() if (this.onError) this.onError(error) } } /** 注册消息回调 */ setMessageListener(fn) { this.messageListener = fn } /** 控制台消息开关 */ setShowMsg(val) { this.showMsg = !!val } /** 重连 */ reconnect() { this.disconnect() setTimeout(() => this.init(), 1000) } /** 断开连接 */ disconnect() { if (this.socket) { this.socket.close() this.socket = null } this.connected = false this._clearTimers() } /** 发送消息 */ send(cmd, params = {}, options = {}) { if (!this.connected) { console.warn(`⚠️ WebSocket 未连接,消息已入队: ${cmd}`) if (!this.dialogStatus) { this.dialogStatus = true this.reconnect() setTimeout(() => { this.dialogStatus = false }, 2000) } } const task = { cmd, params, respKey: options.respKey || cmd, sleep: options.sleep || 0, onSent: options.onSent } this.sendQueue.push(task) return task } /** Promise 版发送 */ sendWithPromise(cmd, params = {}, timeoutMs = 5000) { const respKey = `${cmd}_${this.seq + 1}` return new Promise((resolve, reject) => { if (!this.connected && !this.socket) { return reject(new Error("WebSocket 连接已关闭")) } // 设置 Promise 状态 this.promises[respKey] = { resolve, reject } // 超时处理 const timer = setTimeout(() => { delete this.promises[respKey] reject(new Error(`请求超时: ${cmd} (${timeoutMs}ms)`)) }, timeoutMs) // 发送消息 this.send(cmd, params, { respKey, onSent: () => { clearTimeout(timer) } }) }) } /** 发送心跳 */ sendHeartbeat() { console.log('💓 发送心跳消息') this.send("heart_beat", {}, { respKey: "_sys/ack" }) } /** 获取角色信息 */ getRoleInfo(params = {}) { return this.sendWithPromise("role_getroleinfo", params) } /** 获取数据版本 */ getDataBundleVersion(params = {}) { return this.sendWithPromise("system_getdatabundlever", params) } /** 签到 */ signIn() { return this.sendWithPromise("system_signinreward") } /** 领取日常任务奖励 */ claimDailyReward(rewardId = 0) { return this.sendWithPromise("task_claimdailyreward", { rewardId }) } /** =============== 内部方法 =============== */ /** 设置心跳 */ _setupHeartbeat() { // 延迟3秒后开始发送第一个心跳,避免连接刚建立就发送 setTimeout(() => { if (this.connected && this.socket?.readyState === WebSocket.OPEN) { console.log('💓 开始发送首次心跳') this.sendHeartbeat() } }, 3000) // 设置定期心跳 this.heartbeatTimer = setInterval(() => { if (this.connected && this.socket?.readyState === WebSocket.OPEN) { this.sendHeartbeat() } else { console.log('⚠️ 心跳检查失败: 连接状态异常') } }, this.heartbeatInterval) } /** 队列处理循环 */ _processQueueLoop() { if (this.sendQueueTimer) clearInterval(this.sendQueueTimer) this.sendQueueTimer = setInterval(async () => { if (!this.sendQueue.length) return if (!this.connected || this.socket?.readyState !== WebSocket.OPEN) return const task = this.sendQueue.shift() if (!task) return try { // 构建报文 const raw = this.registry.build(task.cmd, this.ack, this.seq, task.params) if (task.cmd !== "heart_beat") this.seq++ // 编码并发送 const bin = this.registry.encodePacket(raw) this.socket?.send(bin) if (this.showMsg || task.cmd === "heart_beat") { console.log(`📤 发送消息: ${task.cmd}`, task.params) if (this.showMsg) { console.log(`🔐 原始数据:`, raw) console.log(`🚀 编码后数据:`, bin) console.log(`🔧 编码类型:`, typeof bin, bin instanceof Uint8Array ? '✅ Uint8Array (加密)' : '❌ String (明文)') if (bin instanceof Uint8Array && bin.length > 0) { console.log(`🎯 加密验证: 前8字节 [${Array.from(bin.slice(0, 8)).join(', ')}]`) } } } // 触发发送回调 if (task.onSent) { try { task.onSent(task.respKey, task.cmd) } catch (error) { console.warn('发送回调执行失败:', error) } } // 可选延时 if (task.sleep) await sleep(task.sleep) } catch (error) { console.error(`❌ 发送消息失败: ${task.cmd}`, error) } }, 50) } /** 处理 Promise 响应 */ _handlePromiseResponse(packet) { const cmd = packet.cmd if (!cmd) return // 查找对应的 Promise for (const [key, promise] of Object.entries(this.promises)) { if (key.startsWith(cmd) || cmd === key) { delete this.promises[key] if (packet.code === 0 || packet.code === undefined) { promise.resolve(packet.body || packet) } else { promise.reject(new Error(`服务器错误: ${packet.code} - ${packet.hint || '未知错误'}`)) } break } } } /** 清理定时器 */ _clearTimers() { if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer) this.heartbeatTimer = null } if (this.sendQueueTimer) { clearInterval(this.sendQueueTimer) this.sendQueueTimer = null } } } /** 默认导出 */ export default XyzwWebSocketClient