fix: 修正原点(00)两种原因的判断依据 —— 两种情况下脚下都是基岩

用户指出:死亡被卡到 00 时脚下**也是基岩**,所以原来用
lockBelow === 'bedrock' 区分「登录服」和「死亡卡住」是错的,
会把死亡卡住误判成登录服,于是不做自救、只让用户去输验证码。

改成以「服务器有没有要求输入验证码」为唯一可靠信号:
- 要求验证码 -> 判定为登录服,提示人工发验证码(不做无意义的左右移动)
- 没要求验证码 -> 判定为死亡卡住,自动左右移动触发服务器重新传送

自救本身在两种情况下都无害(在登录服里按左右键不会有后果),
所以即使判断错了也不会把事情弄得更糟。

另外:验证码提示往往比 spawn 晚到,所以 _handleAuthPrompt 里也补了一次判断 ——
若提示到达时机器人正好在原点附近,会明确点出「被反复传送刷屏」正是登录服造成的,
把这个因果关系讲清楚,避免用户只看到传送刷屏而不知原因。

同步修正 TODO.md 与 CHANGELOG 里那条错误结论,并标注「不要用基岩做判断依据」。

测试 363 -> 368,全部通过。
这个提交包含在:
lyizai
2026-09-28 20:32:49 +08:00
父节点 9ca80d2f6e
当前提交 0cd65cf9be
共修改 4 个文件,包含 129 行新增和 21 行删除
+5 -2
查看文件
@@ -18,8 +18,11 @@
- 每次回城都明确记日志,说明「会丢失当前位置与玩家手动摆好的位置」
- 因未启用而跳过时也会记一条说明,避免用户以为「怎么不回城了」
- **区分「被传送到原点」的两种原因**(以前在日志里长得一样,常被误判成机器人坏了):
- 脚下是 **bedrock** → 在登录服,必须人工输入验证码,给出明确提示
- 脚下不是基岩 → 死亡后被卡住,**自动左右移动触发服务器重新传送**(`_escapeFromOrigin()`)
- **服务器要求输入验证码** → 被切到登录服,必须人工发验证码,给出明确提示
- **服务器没要求验证码** → 死亡后被卡住,**自动左右移动触发服务器重新传送**(`_escapeFromOrigin()`)
- ⚠️ 这两种情况**脚下都是基岩**,所以不能用「脚下是不是基岩」区分;
唯一可靠信号是服务器有没有发验证码提示。自救在两种情况下都无害,
因此即使判断错了也不会让情况变糟
- **移动执行诊断 `moveDiag`**:暴露当前按下的控制键、速度、寻路器内部状态
(`isMoving` / `isMining` / `goal` / 剩余路径点数)。这是排查「走不动」的关键手段 ——
能一刀切开「寻路器没执行」与「执行了但服务端不认」。
+12 -6
查看文件
@@ -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"` **不能**用来判断 —— 实测死亡卡住和登录服两种情况脚下都是基岩
---
+24 -13
查看文件
@@ -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 });
+88
查看文件
@@ -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);