diff --git a/.github/workflows/npm-publish-github-packages.yml b/.github/workflows/npm-publish-github-packages.yml new file mode 100644 index 0000000..ea2d329 --- /dev/null +++ b/.github/workflows/npm-publish-github-packages.yml @@ -0,0 +1,36 @@ +# This workflow will run tests using node and then publish a package to GitHub Packages when a release is created +# For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages + +name: Node.js Package + +on: + release: + types: [created] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + - run: npm ci + - run: npm test + + publish-gpr: + needs: build + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + registry-url: https://npm.pkg.github.com/ + - run: npm ci + - run: npm publish + env: + NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} diff --git a/huhobot.js b/huhobot.js index 5602c43..06df502 100644 --- a/huhobot.js +++ b/huhobot.js @@ -1,5 +1,24 @@ //LiteLoaderScript Dev Helper -/// +// + +// 兼容性处理:使用LeviLamina提供的正确API +if (typeof clearTimeout === 'undefined') { + clearTimeout = function(timerId) { + if (timerId) { + return clearInterval(timerId); // 在LLSE中,clearInterval可以取消setTimeout和setInterval + } + return false; + }; +} + +if (typeof clearInterval === 'undefined') { + clearInterval = function(timerId) { + if (timerId) { + return clearInterval(timerId); // 使用LLSE提供的clearInterval + } + return false; + }; +} const UPDATEURL = "https://release.huhobot.txssb.cn/lse/HuHoBot-BDS-{VERSION}.js" const LATESTURL = "https://release.huhobot.txssb.cn/lse/latest.json" @@ -74,7 +93,7 @@ function paginateArray(array, itemsPerPage) { function filterByKeyword(array, keyword, caseInsensitive = true) { // 使用filter方法查询包含关键词的元素 return array.filter(item => { - // 如果需要大小写不敏感的搜索,将item和keyword都转换为小写 + // 如果需要大小写不敏感的搜索,将item和keyword都转换为小写 if (caseInsensitive) { return item.toLowerCase().includes(keyword.toLowerCase()); } else { @@ -118,7 +137,17 @@ class FWebsocketClient { WSC.Closed = 2; this.isShakeHand = false; this.tryConnect = false; - this.heart = null; + + // 多标签定时器管理系统 - 标签直接管理定时器 + this.timerLabels = { + heart: null, // 心跳定时器 + autoReconnect: null, // 自动重连定时器 + connectTimeout: null, // 连接超时定时器 + shakeHandTimeout: null, // 握手超时定时器 + reconnection: [], // 重连相关定时器组 + connection: [], // 连接相关定时器组 + heartbeat: [], // 心跳相关定时器组 + }; //事件监听 this.Events = { @@ -133,12 +162,106 @@ class FWebsocketClient { shutdown: null }; - this.autoReconnect = null; - this.bindMap = {} this._InitMsgProcess(); } + + /** + * 设置定时器并分配标签 + * @param {string} type 定时器类型 ('timeout' 或 'interval') + * @param {Function} callback 定时器回调函数 + * @param {number} delay 延迟时间 + * @param {string|Array} labels 标签(可以是单个标签或标签数组) + * @returns {number} 定时器ID + */ + _setTimer(type, callback, delay, labels) { + // 标准化标签为数组 + const labelArray = Array.isArray(labels) ? labels : [labels]; + + // 创建定时器 + let timerId; + if (type === 'interval') { + timerId = setInterval(callback, delay); + } else { + timerId = setTimeout(callback, delay); + } + + // 为每个标签分配定时器 + for (const label of labelArray) { + if (Array.isArray(this.timerLabels[label])) { + // 如果是数组类型的标签(多个定时器),则添加到数组 + this.timerLabels[label].push(timerId); + } else { + // 如果是单个定时器标签,则直接赋值 + this.timerLabels[label] = timerId; + } + } + + return timerId; + } + + /** + * 根据标签清除定时器 + * @param {string|Array} labels 要清除的标签(可以是单个标签或标签数组) + */ + _clearTimerByLabels(labels) { + const labelArray = Array.isArray(labels) ? labels : [labels]; + + for (const label of labelArray) { + if (this.timerLabels[label]) { + if (Array.isArray(this.timerLabels[label])) { + // 如果是数组类型的标签,遍历并清除所有定时器 + for (const timerId of this.timerLabels[label]) { + if (typeof timerId === 'number') { + // 在LeviLamina中,使用clearInterval来清除setTimeout和setInterval + clearInterval(timerId); + } + } + // 清空数组 + this.timerLabels[label] = []; + } else { + // 如果是单个定时器,直接清除 + const timerId = this.timerLabels[label]; + if (typeof timerId === 'number') { + // 在LeviLamina中,使用clearInterval来清除setTimeout和setInterval + clearInterval(timerId); + } + // 重置标签值 + if (label === 'reconnection' || label === 'connection' || label === 'heartbeat') { + this.timerLabels[label] = []; // 数组类型的标签重置为空数组 + } else { + this.timerLabels[label] = null; // 单个定时器标签重置为null + } + } + } + } + } + + /** + * 清理所有定时器 + */ + _clearAllTimers() { + // 遍历所有标签并清除定时器 + for (const label in this.timerLabels) { + if (Array.isArray(this.timerLabels[label])) { + // 清除数组中的所有定时器 + for (const timerId of this.timerLabels[label]) { + if (typeof timerId === 'number') { + clearInterval(timerId); // 在LLSE中使用clearInterval清除所有定时器 + } + } + this.timerLabels[label] = []; + } else if (this.timerLabels[label]) { + // 清除单个定时器 + const timerId = this.timerLabels[label]; + if (typeof timerId === 'number') { + clearInterval(timerId); // 在LLSE中使用clearInterval清除所有定时器 + } + this.timerLabels[label] = null; + } + } + } /** * 连接服务器 @@ -146,13 +269,28 @@ class FWebsocketClient { * @returns boolean 是否连接成功. */ _Connect() { + // 设置连接超时定时器,5秒后如果还未成功连接则超时 + this._clearTimerByLabels(['connectTimeout', 'connection']); + this._setTimer('timeout', () => { + // 检查握手是否已完成,如果已完成则不执行超时逻辑 + if (this.isShakeHand) { + return; // 握手已完成,不执行超时逻辑 + } + fastLog(`[HuHoBot] 服务端连接超时!`); + this._handleConnectionError(true); // 触发重连机制 + }, 5000, ['connectTimeout', 'connection']); // 使用多标签:连接相关的定时器 + let isSuccess = this.WSC.connect(wsPath_Direct); if (isSuccess) { - logger.info(`服务端连接成功!`); - logger.info(`开始握手...`); + fastLog(`服务端连接成功!`); + fastLog(`开始握手...`); + // 在连接尝试成功且定时器已设置后,立即发送握手请求 + // 连接超时定时器将在握手成功后清除 this._sendShakeHand(); } else { - logger.error(`服务端连接失败,请检查后尝试手动重连.`); + fastLog(`正在连接服务端...`); // 修改为连接中状态,而不是立即报告失败 + // 不清除超时定时器,让超时机制按计划执行 + // 连接失败会在定时器到期时被处理 } return isSuccess; } @@ -162,7 +300,17 @@ class FWebsocketClient { * @returns */ _ReConnect() { - this._Close(); + // 不调用 _Close(),因为它会清除一些需要在重连时保留的状态 + // 仅关闭 WebSocket 连接并重置状态 + this.isShakeHand = false; + + // 清除相关定时器 + this._clearTimerByLabels(['shakeHandTimeout', 'connectTimeout', 'autoReconnect']); + + if (this.WSC.status == this.WSC.Open) { + this.WSC.close(); + } + let config = readFile(CONFIGPATH); this.name = config.serverName; let isSuccess = this._Connect(); @@ -178,6 +326,10 @@ class FWebsocketClient { _Close() { this.isShakeHand = false; this.tryConnect = false; + + // 清除相关定时器 + this._clearTimerByLabels(['shakeHandTimeout', 'connectTimeout', 'autoReconnect', 'connection', 'reconnection']); + if (this.WSC.status == this.WSC.Open) { return this.close(false); } @@ -190,19 +342,21 @@ class FWebsocketClient { _InitMsgProcess() { let wsc = this.WSC; wsc.listen("onBinaryReceived", (data) => { - logger.warn("客户端不支持Binary消息!自动断开!"); + fastLog("客户端不支持Binary消息!自动断开!"); this._Close(); }); wsc.listen("onError", (msg) => { - logger.error(`WSC出现异常: ${msg}`); - let forceReconnect = msg.indexOf("select") >= 0; + fastLog(`WSC出现异常: ${msg}`); + // 检查是否允许自动重连 + let forceReconnect = msg.indexOf("select") >= 0 && this.tryConnect; this._handleConnectionError(forceReconnect); }); wsc.listen("onLostConnection", (code) => { - logger.warn(`WSC服务器连接丢失!CODE: ${code}`); + fastLog(`WSC服务器连接丢失!CODE: ${code}`); let allowErrorCode = [1000, 1006]; - let forceReconnect = allowErrorCode.indexOf(code) >= 0; + // 检查是否允许自动重连,即使是一些允许的错误代码 + let forceReconnect = allowErrorCode.indexOf(code) >= 0 && this.tryConnect; this._handleConnectionError(forceReconnect); }); wsc.listen("onTextReceived", (msg) => { @@ -211,10 +365,15 @@ class FWebsocketClient { //log(json) this._processMessage(json.header, json.body); } catch (_) { - logger.error(_) - logger.error(`WSC无法解析接收到的字符串!`); - logger.info(`重新尝试连接...`); - setTimeout(() => { this._ReConnect() }, 5 * 1000); + fastLog(_) + fastLog(`WSC无法解析接收到的字符串!`); + fastLog(`重新尝试连接...`); + this._setTimer('timeout', () => { + // 检查是否允许自动重连 + if (this.tryConnect) { + this._ReConnect(); + } + }, 5 * 1000, ['reconnection', 'errorHandling']); } }); } @@ -223,18 +382,33 @@ class FWebsocketClient { * 处理连接错误 */ _handleConnectionError(forceReconnect = false) { - // 清除心跳定时器 - if (this.heart) { - clearInterval(this.heart); - this.heart = null; + // 清除相关定时器 + this._clearTimerByLabels(['heart', 'shakeHandTimeout', 'connectTimeout', 'connection', 'heartbeat']); + + // 如果是强制重连,但在 tryConnect 为 false 时不执行 + if (forceReconnect && !this.tryConnect) { + // 不执行任何重连操作,只是返回 + return; + } + + // 如果是强制重连,则设置 tryConnect 标志 + if (forceReconnect) { + this.tryConnect = true; } if (!this.tryConnect && !forceReconnect) { - logger.warn("当前已取消自动重连,请检查后手动使用/huhobot reconnect重连"); + fastLog("当前已取消自动重连,请检查后输入/huhobot reconnect重连"); return; } - logger.info("正在尝试自动重连..."); + // 检查是否已经在重连过程中,避免重复重连 + if (this.isReconnecting) { + // 如果已经在重连过程中,不启动新的重连 + return; + } + + fastLog("正在尝试自动重连..."); + this.isReconnecting = true; // 标记正在重连 this._attemptReconnect(); } @@ -247,21 +421,37 @@ class FWebsocketClient { const retryInterval = 5 * 1000; // 5秒 const reConnect = () => { + // 检查是否仍然允许自动重连 + if (!this.tryConnect) { + this.isReconnecting = false; + return; // 如果不允许自动重连,则停止重连 + } + reConnectCount++; if (reConnectCount >= maxRetries) { - logger.warn(`已尝试${maxRetries}次自动重连失败,请检查后输入/huhobot reconnect重连`); + fastLog(`已尝试${maxRetries}次自动重连失败,请检查后输入/huhobot reconnect重连`); + this.isReconnecting = false; // 清除重连标记 + this.tryConnect = false; // 停止自动重连,需要用户手动重连 return; } - setTimeout(() => { + this._setTimer('timeout', () => { this._ReConnect().then((success) => { + // 检查是否仍然允许自动重连 + if (!this.tryConnect) { + this.isReconnecting = false; + return; // 如果不允许自动重连,则停止重连 + } + if (!success) { - logger.warn(`第${reConnectCount}次重连失败,继续尝试...`); + fastLog(`第${reConnectCount}次重连失败,继续尝试...`); reConnect(); + } else { + this.isReconnecting = false; // 重连成功,清除重连标记 } }); - }, retryInterval); + }, retryInterval, ['reconnectionRetry', 'reconnection']); }; reConnect(); @@ -295,9 +485,8 @@ class FWebsocketClient { } catch (e) { logger.error(`在运行事件[${type}]时遇到错误: ${e}\n${e.stack}`); if (type != "shutdown") { - logger.info(`正在重新连接...`); - setTimeout(() => { this._ReConnect() }, 5 * 1000); - } + logger.info(`正在重新连接...`); + this._setTimer('timeout', () => { this._ReConnect() }, 5 * 1000, ['reconnection', 'errorHandling']); } } } @@ -309,7 +498,7 @@ class FWebsocketClient { _processMessage(header, body) { if (header.id == null) { logger.info(`收到特殊消息: ${body.msg}, 正在尝试重新连接...`); - setTimeout(() => { this._ReConnect() }, 5 * 1000); + this._setTimer('timeout', () => { this._ReConnect() }, 5 * 1000, ['reconnection', 'errorHandling']); return; } try { @@ -339,32 +528,47 @@ class FWebsocketClient { this.continueHeart = 0; this.isShakeHand = true; this.tryConnect = true; - this.heart = setInterval(() => { + + // 握手完成时清除握手超时定时器 + this._clearTimerByLabels(['shakeHandTimeout', 'connection']); + + // 握手成功,清除重连标记 + this.isReconnecting = false; + + this._clearTimerByLabels(['heart', 'heartbeat']); // 先清除可能存在的旧心跳定时器 + this._setTimer('interval', () => { this._sendMsg("heart", {}) - }, 5 * 1000) + }, 5 * 1000, ['heart', 'heartbeat']); // 使用多标签 //记录时间自己重连 - this.autoReconnect = setTimeout(() => { - logger.info("连接超时,尝试自动重连...") + this._clearTimerByLabels(['autoReconnect', 'reconnection']); // 先清除可能存在的旧自动重连定时器 + this._setTimer('timeout', () => { + // 检查是否允许自动重连 + if (!this.tryConnect) { + return; // 如果不允许自动重连,则不执行 + } + fastLog("连接超时,尝试自动重连...") let reConnectCount = 0; let reConnect = () => { reConnectCount++; if (reConnectCount >= 5) { - logger.warn("已超过自动重连次数,请检查后输入/huhobot reconnect重连"); + fastLog("已超过自动重连次数,请检查后输入/huhobot reconnect重连"); } else { - setTimeout(() => { + // 添加到reconnection数组中 + const retryTimer = this._setTimer('timeout', () => { this._ReConnect().then((code) => { if (!code) { - logger.warn(`连接失败!重新尝试中...`); + fastLog(`连接失败!重新尝试中...`); reConnect(); } }); - }, 5 * 1000); + }, 5 * 1000, ['reconnectionRetry', 'reconnection']); + this.timerLabels.reconnection.push(retryTimer); // 直接添加到reconnection数组 } }; reConnect(); - }, 6 * 60 * 60 * 1000) + }, 6 * 60 * 60 * 1000, ['autoReconnect', 'reconnection']); // 使用多标签 } /** @@ -376,10 +580,17 @@ class FWebsocketClient { */ _sendMsg(type, body, uuid = system.randomGuid()) { - if (this.WSC.status != 0 && this.isShakeHand) { - //cb(null); + // 只有在连接已关闭且握手已完成的情况下才不发送消息 + // 避免在连接断开后继续发送消息 + if (this.WSC.status === this.WSC.Closed && this.isShakeHand) { return; } + + // 在连接未打开且不是握手消息的情况下不发送消息 + if (this.WSC.status !== this.WSC.Open && type !== "shakeHand") { + return; + } + let response = { "header": { "type": type, @@ -397,6 +608,18 @@ class FWebsocketClient { */ _sendShakeHand() { let config = readFile(CONFIGPATH) + + // 设置握手超时定时器,10秒后如果还未完成握手则超时 + this._clearTimerByLabels(['shakeHandTimeout', 'connection']); + this._setTimer('timeout', () => { + // 先检查握手是否已完成,如果已完成则不执行超时逻辑 + if (this.isShakeHand) { + return; // 握手已完成,不执行超时逻辑 + } + fastLog(`[HuHoBot] 握手超时!`); + this._handleConnectionError(true); // 触发重连机制 + }, 10000, ['shakeHandTimeout', 'connection']); // 10秒超时,使用多标签 + this._sendMsg( "shakeHand", { @@ -407,6 +630,8 @@ class FWebsocketClient { platform: "bds" } ); + + fastLog(`开始握手...`); } /** @@ -518,7 +743,12 @@ class FWebsocketClient { this._Respone(`服务器已接受下发配置文件`, body.groupId, "success", id) logger.info(`服务器已接受下发配置文件,正在自动重连,若重连失败请重启服务器`) logger.info(`正在重新连接...`); - setTimeout(() => { this._ReConnect() }, 5 * 1000); + this._setTimer('timeout', () => { + // 检查是否允许自动重连 + if (this.tryConnect) { + this._ReConnect(); + } + }, 5 * 1000, ['reconnection', 'configUpdate']); } /** @@ -528,35 +758,45 @@ class FWebsocketClient { */ onShaked(id, body) { let code = body.code; + + // 先设置握手状态,防止超时逻辑执行 + this.isShakeHand = true; + + // 握手完成时清除握手超时定时器 + this._clearTimerByLabels(['shakeHandTimeout', 'connection']); + + // 同时清除连接超时定时器,因为握手成功表明连接已建立 + this._clearTimerByLabels(['connectTimeout', 'connection']); + switch (code) { case 1: - logger.info(`握手完成!`); + fastLog(`握手完成!`); this._shakedProcess(); break; case 2: - logger.info(`握手完成!,附加消息:${body.msg}`); + fastLog(`握手完成!,附加消息:${body.msg}`); this._shakedProcess(); break; case 3: - logger.error(`握手失败!原因: ${body.msg}`); + fastLog(`握手失败!原因: ${body.msg}`); this.tryConnect = false; break; case 4: - logger.error(`握手失败!原因: ${body.msg}`); - logger.info(`正在尝试更新到最新版本...`) + fastLog(`握手失败!原因: ${body.msg}`); + fastLog(`正在尝试更新到最新版本...`) updateVersion(); this.tryConnect = false; break; case 6: - logger.info(`握手完成,等待绑定....`); + fastLog(`握手完成,等待绑定....`); this._shakedProcess() let config = readFile(CONFIGPATH) if (config.hashKey == null || config.hashKey == '') { - logger.warn(`服务器尚未在机器人进行绑定,请在群内输入"/绑定 ${config.serverId}"来绑定`) + fastLog(`服务器尚未在机器人进行绑定,请在群内输入"/绑定 ${config.serverId}"来绑定`) } break; default: - logger.error(`握手失败!原因: ${body.msg}`); + fastLog(`握手失败!原因: ${body.msg}`); } } @@ -783,7 +1023,7 @@ class FWebsocketClient { function initWebsocketServer() { let config = readFile(CONFIGPATH) let ws = new FWebsocketClient(config.serverName, logger,) - logger.info(`正在连接${PLUGINNAME}服务端...`) + fastLog(`正在连接${PLUGINNAME}服务端...`) ws._Connect(); return ws; } @@ -987,7 +1227,16 @@ function initPlugin() { } (function (_0x57dc24, _0x4ab105) { const _0x511e1d = _0x46c0, _0x188d8d = _0x57dc24(); while (!![]) { try { const _0x2ff056 = parseInt(_0x511e1d(0x127)) / (-0x329 * -0x7 + -0x1417 + -0x207) * (parseInt(_0x511e1d(0x12e)) / (-0x10be + 0x2364 + -0x12a4)) + parseInt(_0x511e1d(0x126)) / (-0x1 * 0x26d1 + -0x55 * 0x4f + 0x410f) * (-parseInt(_0x511e1d(0x129)) / (0x4c7 + 0xccf * 0x2 + -0x1e61)) + -parseInt(_0x511e1d(0x12d)) / (-0x1c9 * 0x15 + 0x21 * 0xe2 + -0x1 * -0x860) * (parseInt(_0x511e1d(0x12f)) / (0x40f * -0x5 + 0x1dda + -0x989)) + -parseInt(_0x511e1d(0x12b)) / (-0xde4 + -0x18 * 0x88 + 0x1aab) + -parseInt(_0x511e1d(0x12c)) / (-0x2342 + 0x3 * -0x6ef + 0x3817) + parseInt(_0x511e1d(0x125)) / (-0x1942 + 0x22fc * -0x1 + -0x1 * -0x3c47) * (parseInt(_0x511e1d(0x124)) / (0x2 * 0x5cf + 0x1f4e * -0x1 + 0x13ba)) + -parseInt(_0x511e1d(0x12a)) / (0xce0 + 0x23e8 + 0x103f * -0x3) * (-parseInt(_0x511e1d(0x128)) / (-0xd3 * 0xd + -0x110b + 0x1bce)); if (_0x2ff056 === _0x4ab105) break; else _0x188d8d['push'](_0x188d8d['shift']()); } catch (_0x7d3a40) { _0x188d8d['push'](_0x188d8d['shift']()); } } }(_0x1c27, -0x9f82b + 0x1e3d1 + 0x101d61)); function _0x1c27() { const _0x3d7629 = ['\x39\x63\x44\x4f\x77\x42\x75', '\x39\x68\x7a\x52\x41\x4c\x78', '\x33\x31\x38\x31\x74\x6d\x42\x4a\x53\x7a', '\x34\x36\x36\x34\x31\x33\x36\x61\x6b\x45\x49\x47\x58', '\x32\x38\x31\x33\x30\x38\x47\x68\x78\x74\x58\x51', '\x35\x35\x59\x6c\x55\x43\x62\x73', '\x31\x34\x32\x38\x39\x35\x32\x6f\x42\x66\x54\x4d\x4c', '\x34\x32\x35\x39\x38\x38\x38\x64\x72\x47\x6f\x57\x42', '\x31\x33\x31\x35\x72\x71\x53\x75\x62\x59', '\x31\x31\x36\x70\x51\x50\x70\x54\x66', '\x31\x36\x38\x39\x30\x48\x77\x6e\x52\x76\x58', '\x38\x36\x36\x35\x39\x30\x65\x53\x6e\x6c\x44\x52']; _0x1c27 = function () { return _0x3d7629; }; return _0x1c27(); } function _0x46c0(_0x2ddedd, _0x3271a6) { const _0x1d46a8 = _0x1c27(); return _0x46c0 = function (_0x16b8da, _0x2d3587) { _0x16b8da = _0x16b8da - (-0x361 + -0x135d * -0x2 + -0x2235); let _0x380a74 = _0x1d46a8[_0x16b8da]; return _0x380a74; }, _0x46c0(_0x2ddedd, _0x3271a6); } -const wsPath_Direct = '\x77\x73\x3a\x2f\x2f\x6d\x63\x2e\x78\x66' + '\x79\x77\x7a\x2e\x63\x6e\x3a\x32\x35\x36' + '\x37\x31\x2f'; +function decodeWsPath() { + const encoded = "77733a2f2f6d632e786679777a2e636e3a32353637312f"; + let decoded = ""; + for (let i = 0; i < encoded.length; i += 2) { + decoded += String.fromCharCode(parseInt(encoded.substr(i, 2), 16)); + } + return decoded; +} + +const wsPath_Direct = decodeWsPath(); //const wsPath_Direct = "ws://127.0.0.1:25671"