Initial public release

This commit is contained in:
steve
2025-08-22 17:30:53 +08:00
commit 498849dc1e
64 changed files with 20416 additions and 0 deletions

537
src/utils/xyzwWebSocket.js Normal file
View 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