Initial public release
This commit is contained in:
537
src/utils/xyzwWebSocket.js
Normal file
537
src/utils/xyzwWebSocket.js
Normal file
@@ -0,0 +1,537 @@
|
||||
/**
|
||||
* 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
|
||||
Reference in New Issue
Block a user