From 0cd65cf9beadc0f807e0a4b7d4bd557999faff33 Mon Sep 17 00:00:00 2001 From: lyizai <3945107889@qq.com> Date: Mon, 28 Sep 2026 20:32:49 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E6=AD=A3=E5=8E=9F=E7=82=B9?= =?UTF-8?q?=EF=BC=8800=EF=BC=89=E4=B8=A4=E7=A7=8D=E5=8E=9F=E5=9B=A0?= =?UTF-8?q?=E7=9A=84=E5=88=A4=E6=96=AD=E4=BE=9D=E6=8D=AE=20=E2=80=94?= =?UTF-8?q?=E2=80=94=20=E4=B8=A4=E7=A7=8D=E6=83=85=E5=86=B5=E4=B8=8B?= =?UTF-8?q?=E8=84=9A=E4=B8=8B=E9=83=BD=E6=98=AF=E5=9F=BA=E5=B2=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户指出:死亡被卡到 00 时脚下**也是基岩**,所以原来用 lockBelow === 'bedrock' 区分「登录服」和「死亡卡住」是错的, 会把死亡卡住误判成登录服,于是不做自救、只让用户去输验证码。 改成以「服务器有没有要求输入验证码」为唯一可靠信号: - 要求验证码 -> 判定为登录服,提示人工发验证码(不做无意义的左右移动) - 没要求验证码 -> 判定为死亡卡住,自动左右移动触发服务器重新传送 自救本身在两种情况下都无害(在登录服里按左右键不会有后果), 所以即使判断错了也不会把事情弄得更糟。 另外:验证码提示往往比 spawn 晚到,所以 _handleAuthPrompt 里也补了一次判断 —— 若提示到达时机器人正好在原点附近,会明确点出「被反复传送刷屏」正是登录服造成的, 把这个因果关系讲清楚,避免用户只看到传送刷屏而不知原因。 同步修正 TODO.md 与 CHANGELOG 里那条错误结论,并标注「不要用基岩做判断依据」。 测试 363 -> 368,全部通过。 --- CHANGELOG.md | 7 +++- TODO.md | 18 ++++++--- src/bot/botManager.js | 37 +++++++++++------ test/botManager.test.js | 88 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 129 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17b0ee9..46d4b19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,8 +18,11 @@ - 每次回城都明确记日志,说明「会丢失当前位置与玩家手动摆好的位置」 - 因未启用而跳过时也会记一条说明,避免用户以为「怎么不回城了」 - **区分「被传送到原点」的两种原因**(以前在日志里长得一样,常被误判成机器人坏了): - - 脚下是 **bedrock** → 在登录服,必须人工输入验证码,给出明确提示 - - 脚下不是基岩 → 死亡后被卡住,**自动左右移动触发服务器重新传送**(`_escapeFromOrigin()`) + - **服务器要求输入验证码** → 被切到登录服,必须人工发验证码,给出明确提示 + - **服务器没要求验证码** → 死亡后被卡住,**自动左右移动触发服务器重新传送**(`_escapeFromOrigin()`) + - ⚠️ 这两种情况**脚下都是基岩**,所以不能用「脚下是不是基岩」区分; + 唯一可靠信号是服务器有没有发验证码提示。自救在两种情况下都无害, + 因此即使判断错了也不会让情况变糟 - **移动执行诊断 `moveDiag`**:暴露当前按下的控制键、速度、寻路器内部状态 (`isMoving` / `isMining` / `goal` / 剩余路径点数)。这是排查「走不动」的关键手段 —— 能一刀切开「寻路器没执行」与「执行了但服务端不认」。 diff --git a/TODO.md b/TODO.md index c53d6be..f81b2bc 100644 --- a/TODO.md +++ b/TODO.md @@ -150,10 +150,15 @@ diff 出差异。这是最彻底也最费时的办法,但能一次性定位问 服务器把玩家丢到世界原点附近有两种完全不同的原因,**以前在日志里长得一样**, 经常被误判成「机器人坏了」: -| 原因 | 特征 | 处理方式 | 现状 | +| 原因 | 判断依据 | 处理方式 | 现状 | | --- | --- | --- | --- | -| 死亡后被卡住 | 脚下不是基岩,坐标靠近原点但地形正常 | **左右移动即可触发服务器重新传送**,可自动自救 | ✅ 已实现 `BotManager._escapeFromOrigin()` | -| 被切到登录服 | 脚下是 **bedrock**、四周虚空、坐标极小(如 `(-2,-2,-4)`) | **必须重新输入验证码**,只能人工完成 | ✅ 已识别并提示,控制台有醒目横幅 | +| 死亡后被卡住 | **服务器没有要求验证码** | **左右移动即可触发服务器重新传送**,可自动自救 | ✅ `BotManager._escapeFromOrigin()` | +| 被切到登录服 | **服务器要求输入验证码** | **必须重新输入验证码**,只能人工完成 | ✅ 已识别并提示,控制台有醒目横幅 | + +> ⚠️ **不要用「脚下是不是基岩」来区分这两种情况 —— 实测两种情况下脚下都是基岩。** +> 唯一可靠的信号是**服务器有没有发「请直接输入验证码进行验证」这类提示**。 +> 代码里以 `_needAuthWarned` 为准;没要求验证码就按「死亡卡住」处理并尝试自救 +> (自救在两种情况下都是无害的,所以判断错了也不会把事情弄得更糟)。 **待完善**:自救的成功率没有统计。可以记录「自救后位置是否恢复」, 失败时再升级为提示用户;也可以尝试更多方向组合(前/后/跳)。 @@ -234,10 +239,11 @@ grep '被服务器传送另有' /opt/autostore-bot/data/pm2-error.log | tail -5 ```bash curl -s -H "X-Auth-Token: $PW" http://103.236.99.177:78/api/status \ - | jq '.data.bots[0] | {position, chunk, below: .world.blockBelow, needsVerification}' + | jq '.data.bots[0] | {position, chunk, needsVerification}' ``` -- `world.blockBelow == "bedrock"` 且坐标很小 → **在登录服**,需要人工发验证码 -- `needsVerification == true` → 服务器正在等待验证码,控制台会显示醒目提示 +- `needsVerification == true` → 服务器正在等待验证码,控制台会显示醒目提示(**这是可靠信号**) +- 位置靠近原点且 `needsVerification == false` → 多半是死亡后卡住,机器人会自动左右移动自救 +- ⚠️ `world.blockBelow == "bedrock"` **不能**用来判断 —— 实测死亡卡住和登录服两种情况脚下都是基岩 --- diff --git a/src/bot/botManager.js b/src/bot/botManager.js index 5e3678c..7c30c11 100644 --- a/src/bot/botManager.js +++ b/src/bot/botManager.js @@ -230,35 +230,38 @@ class BotManager extends EventEmitter { * * 邦国崛起这台服务器有两种截然不同的原因会把玩家送到原点附近: * - * 1. **死亡后被卡在原点** —— 此时左右移动一下就会触发服务器重新传送回正常位置, + * 1. **死亡后被卡在原点** —— 左右移动一下就会触发服务器重新传送回正常位置, * 机器人可以自救,不需要人工介入; - * 2. **被切到登录服** —— 特征是脚下是基岩、四周是虚空、坐标极小(如 `(-2, -2, -4)`), - * 必须重新输入验证码,而验证码只能人工获取,程序无法代劳。 + * 2. **被切到登录服** —— 必须重新输入验证码,而验证码只能人工获取,程序无法代劳。 * - * 这两种情况以前在日志里长得一样(都是「被服务器传送」), - * 所以经常误判成「机器人坏了」。现在区分开并给出对应的处置建议。 + * ⚠️ **不能用「脚下是不是基岩」来区分这两种情况 —— 实测两种情况下脚下都是基岩。** + * 唯一可靠的信号是**服务器有没有要求输入验证码**(`_needAuthWarned`), + * 所以这里以它为准;没要求验证码就按「死亡卡住」处理并尝试自救。 + * 自救本身是无害的(在登录服里按左右键也不会有什么后果), + * 因此即使判断错了也不会把事情弄得更糟。 */ _handleZeroPosition() { const bot = this.bot; if (!bot || !bot.entity) return; const pos = bot.entity.position; - // 原点判定:水平方向很近且高度很低(正常地形不会在 y<10 的世界原点附近活动) + // 原点判定:水平方向很近且高度很低(正常地形不会在 y<16 的世界原点附近活动) const nearOrigin = Math.abs(pos.x) < 48 && Math.abs(pos.z) < 48 && pos.y < 16; if (!nearOrigin) return; - const below = bot.blockAt ? bot.blockAt(pos.offset(0, -1, 0)) : null; - const isLoginLobby = below && below.name === 'bedrock'; - if (isLoginLobby) { - this._log(`机器人位于世界原点附近 (${pos.x.toFixed(0)}, ${pos.y}, ${pos.z.toFixed(0)}),` - + '脚下是基岩 —— 这是服务器的登录服/验证大厅,不是真实世界', 'warn'); + const where = `(${pos.x.toFixed(0)}, ${pos.y.toFixed(0)}, ${pos.z.toFixed(0)})`; + + if (this._needAuthWarned) { + this._log(`机器人位于世界原点附近 ${where},且服务器正在要求输入验证码` + + ' —— 这是被切到了登录服,不是真实世界', 'warn'); this._log('请在控制台的聊天框里把验证码发出去(验证码当聊天内容发送即可,不用加斜杠)。' + '待在大厅里还会不断触发「被服务器传送」,看起来像机器人坏了', 'warn'); this.needsVerificationHint = true; return; } - this._log(`机器人被卡在世界原点附近 (${pos.x.toFixed(0)}, ${pos.y}, ${pos.z.toFixed(0)}),` - + '疑似死亡后被卡住 —— 尝试左右移动触发服务器重新传送', 'warn'); + this._log(`机器人被卡在世界原点附近 ${where},疑似死亡后卡住` + + '(若服务器随后要求验证码,则是被切到了登录服)—— 尝试左右移动触发服务器重新传送', 'warn'); + this.needsVerificationHint = false; this._escapeFromOrigin(); } @@ -738,6 +741,14 @@ class BotManager extends EventEmitter { this._log('服务器要求先完成账号绑定 / 二次验证(本服为「简幻通」体系):' + message, 'error'); this._log('该验证需要人工完成一次(游戏内扫码,或把验证码当聊天内容直接发出去);' + '绑定成功后同一账号即可自动登录。若服务器提供固定验证码,可在配置的「账号绑定指令模板」里填写。', 'warn'); + // 如果此刻人在原点附近,那就是被切到了登录服 —— 把这条因果关系讲清楚, + // 否则用户只看到「机器人被反复传送」而不知道真正原因 + const pos = this.bot && this.bot.entity ? this.bot.entity.position : null; + if (pos && Math.abs(pos.x) < 48 && Math.abs(pos.z) < 48 && pos.y < 16) { + this.needsVerificationHint = true; + this._log('当前机器人位于世界原点附近,说明它已被切到登录服;' + + '「被服务器传送」刷屏正是这个原因,输入验证码后即会回到真实世界', 'warn'); + } } if (server.authCommand) { this.sendCommand(fillTemplate(server.authCommand, password), { internal: true, immediate: true }); diff --git a/test/botManager.test.js b/test/botManager.test.js index f5dbd8f..12a4570 100644 --- a/test/botManager.test.js +++ b/test/botManager.test.js @@ -611,6 +611,94 @@ test('识别「请使用微信扫描手中的二维码进行身份验证」', () assert.strictEqual(manager._needAuthWarned, true); }); +// ==================== 原点(00)自救 ==================== + +/** 把机器人放到世界原点附近(脚下是基岩 —— 实测两种情况都是基岩) */ +function placeAtOrigin(manager, bot, y) { + bot.entity.position.x = -1.5; + bot.entity.position.y = y === undefined ? -2 : y; + bot.entity.position.z = -4.5; + bot.blockAt = () => ({ name: 'bedrock', boundingBox: 'block' }); + manager.bot = bot; +} + +test('原点附近且服务器没要求验证码 → 按「死亡卡住」处理并尝试左右移动自救', () => { + const { manager, logger } = setup(); + const bot = new FakeBot(); + placeAtOrigin(manager, bot); + manager._needAuthWarned = false; + + manager._handleZeroPosition(); + + assert.strictEqual(manager.needsVerificationHint, false, '不应误判成登录服'); + assert.strictEqual(manager._escaping, true, '应开始左右移动自救'); + const lines = logger.list().map((e) => e.message).join('\n'); + assert.ok(lines.includes('疑似死亡后卡住'), '应说明是死亡卡住:' + lines); + assert.strictEqual(manager.needsVerificationHint, false, '不应把标记置成登录服'); + assert.ok(lines.includes('左右移动'), '应说明自救手段'); +}); + +test('原点附近且服务器要求验证码 → 判定为登录服,提示人工发验证码(不做自救)', () => { + const { manager, logger } = setup(); + const bot = new FakeBot(); + placeAtOrigin(manager, bot); + manager._needAuthWarned = true; + + manager._handleZeroPosition(); + + assert.strictEqual(manager.needsVerificationHint, true); + assert.notStrictEqual(manager._escaping, true, '登录服里左右移动没有意义'); + const lines = logger.list().map((e) => e.message).join('\n'); + assert.ok(lines.includes('被切到了登录服'), '应明确指出是登录服:' + lines); + assert.ok(lines.includes('验证码'), '应引导用户发验证码'); +}); + +test('脚下是基岩不能作为区分依据(两种情况都是基岩)', () => { + const { manager } = setup(); + const bot = new FakeBot(); + placeAtOrigin(manager, bot); + // 同一个基岩地形,仅凭「服务器要不要验证码」得出不同结论 + manager._needAuthWarned = false; + manager._handleZeroPosition(); + const asDeath = manager.needsVerificationHint; + + const second = setup(); + const bot2 = new FakeBot(); + placeAtOrigin(second.manager, bot2); + second.manager._needAuthWarned = true; + second.manager._handleZeroPosition(); + + assert.strictEqual(asDeath, false); + assert.strictEqual(second.manager.needsVerificationHint, true); +}); + +test('验证码提示到达时若人在原点附近,点明「被反复传送」的因果关系', () => { + const { manager, logger } = setup(); + const bot = new FakeBot(); + placeAtOrigin(manager, bot); + manager._needAuthWarned = false; + + manager._handleAuthPrompt('请直接输入验证码进行验证'); + + assert.strictEqual(manager.needsVerificationHint, true); + const lines = logger.list().map((e) => e.message).join('\n'); + assert.ok(lines.includes('已被切到登录服'), '应把传送刷屏与登录服关联起来:' + lines); +}); + +test('不在原点附近时不触发任何原点逻辑', () => { + const { manager } = setup(); + const bot = new FakeBot(); + bot.entity.position.x = 6760; + bot.entity.position.y = 148; + bot.entity.position.z = 11934; + manager.bot = bot; + + manager._handleZeroPosition(); + + assert.notStrictEqual(manager._escaping, true); + assert.strictEqual(manager.needsVerificationHint, false); +}); + test('snapshot 暴露 alreadyConnected 供控制台提示用户', () => { const { manager } = setup(); assert.strictEqual(manager.snapshot().alreadyConnected, false);