首次真正连上线上服务器(Debian 12 + pm2 + 端口 78)后暴露的问题,单元测试全绿但机器人干不了活: 致命 - 寻路插件从未加载:项目从没调用 bot.loadPlugin(pathfinder),bot.pathfinder 恒为 undefined、isReady() 恒为 false,扩地任务连启动都启动不了,界面只显示含糊的 「机器人尚未就绪」。现在 createBot 后立刻加载,失败时明确报错。 登录门禁相关(三处死锁) - preLoginCommands 白名单:/login、/register、简幻通的 /auth 本身就是登录流程的一部分, 原先被门禁一并拦下,形成「要先登录才能发登录指令」 - 验证码常被要求直接当聊天内容发出(不是指令),原先被 sendChat 的门禁挡住; 现在服务器明确要求验证时自动放行,另有 server.allowChatBeforeLogin 兜底 - 配置了登录指令却没有密码时 loggedIn 永久为 false,控制台连手动指令都发不出; 现在走与其他「服务器不回提示」一致的超时兜底并给出说明 可用性 - 离线时指令被静默丢弃却回报「已发送」;现在要求已连接并进入世界,失败信息区分 「未连接」「登录被拒」「未通过验证」 - 「被服务器传送」日志洪水:实测 3 分钟 748 行,会冲光日志缓冲并打满 WebSocket; 现在按 30 秒窗口聚合,断开时刷出未汇总计数 - 识别外站账号绑定流程并给出可操作告警,新增 server.authCommand 模板 - 重连指数退避(server.reconnectMaxDelayMs) - 控制台瓦片 URL 与后端路由不一致 新增 - 控制台添加 / 移除玩家:不必改 .env 重启。密码只写入 data/bots.credentials.json (强制 0600),不进仓库、不出现在任何响应里;.env 与凭据文件同名时以 .env 为准, .env 里的账号不允许从控制台移除 - 手动移动测试接口与按钮:绕开寻路器直接按方向键,返回真实位移, 用于区分「算不出路径」与「根本走不动」 - 位置同步诊断:位置包收发计数、onGround、游戏模式、维度、脚下方块与六向实心 - grim.reportOnGroundFromWorld:按世界数据修正出站 onGround(默认开启) - scripts/check-protocol.js:核对包名与字段名同目标版本协议是否一致 文档 - README 新增「部署到服务器」与「机器人走不动的排查方法」两章 已知限制:实测本服上 mineflayer 客户端无法移动(按方向键 2 秒位移 0.00~0.02 格, 服务端每 tick 回弹一次),已排除区块数据、onGround、Grim 层、游戏模式、物理引擎、 寻路插件、teleport_confirm 协议字段等因素,彻底定位需与原生客户端逐字节对比移动包。 连接 / 登录 / 指令 / 聊天 / 公屏日志 / 大地图均正常。 测试从 263 增至 290,全部通过。
72 行
2.8 KiB
JavaScript
72 行
2.8 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* 协议自检:核对 mineflayer 写出的 `teleport_confirm` 包在 1.21.4 上是否存在、
|
|
* 字段名是否匹配。
|
|
*
|
|
* 背景:部分服务端在收到 `player_position` 后会每 tick 重发位置包,
|
|
* 直到客户端回一个 teleport_confirm 确认。若 mineflayer 因为版本字段名不一致
|
|
* 而写不出这个包,就会形成「服务端一直传送、客户端一直走不动」的死循环。
|
|
*/
|
|
|
|
const mcData = require('minecraft-data')('1.21.4');
|
|
|
|
const toServer = mcData.protocol.play.toServer;
|
|
const mappings = toServer.types.packet[1][0].type[1].mappings;
|
|
|
|
console.log('=== 1.21.4 客户端 → 服务端 中与传送/位置相关的包名 ===');
|
|
for (const [id, name] of Object.entries(mappings)) {
|
|
if (/teleport|position|confirm/i.test(name)) {
|
|
console.log(` ${id.padStart(3)} ${name}`);
|
|
}
|
|
}
|
|
|
|
console.log('\n=== 客户端 ← 服务端 中与传送/位置相关的包名 ===');
|
|
const toClientMappings = mcData.protocol.play.toClient.types.packet[1][0].type[1].mappings;
|
|
for (const [id, name] of Object.entries(toClientMappings)) {
|
|
if (/teleport|position/i.test(name)) {
|
|
console.log(` ${id.padStart(3)} ${name}`);
|
|
}
|
|
}
|
|
|
|
console.log('\n=== teleport_confirm / position_look 的字段定义(客户端 → 服务端)===');
|
|
function dumpPacketFields(packetName) {
|
|
// protocol 结构:types.packet = ['container', [nameField, paramsField]]
|
|
// paramsField.type = ['switch', { compareTo: 'name', fields: { 包名: [字段...] } }]
|
|
const paramsField = toServer.types.packet[1][1];
|
|
const fields = paramsField.type[1].fields;
|
|
const def = fields[packetName];
|
|
if (!def) {
|
|
console.log(` (没有名为 ${packetName} 的包)`);
|
|
return;
|
|
}
|
|
console.log(` ${packetName}: ` + def.map((f) => `${f.name}:${JSON.stringify(f.type)}`).join(', '));
|
|
}
|
|
|
|
dumpPacketFields('teleport_confirm');
|
|
dumpPacketFields('position');
|
|
dumpPacketFields('position_look');
|
|
|
|
console.log('\n=== 客户端 ← 服务端 position 包的字段定义 ===');
|
|
function dumpToClientFields(packetName) {
|
|
const paramsField = mcData.protocol.play.toClient.types.packet[1][1];
|
|
const def = paramsField.type[1].fields[packetName];
|
|
if (!def) {
|
|
console.log(` (没有名为 ${packetName} 的包)`);
|
|
return;
|
|
}
|
|
console.log(` ${packetName}: ` + def.map((f) => `${f.name}:${JSON.stringify(f.type)}`).join(', '));
|
|
}
|
|
dumpToClientFields('position');
|
|
|
|
console.log('\n=== mineflayer 实际使用的包名 / 字段名 ===');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const physics = fs.readFileSync(path.join(__dirname, 'node_modules', 'mineflayer', 'lib', 'plugins', 'physics.js'), 'utf8');
|
|
const lines = physics.split('\n');
|
|
lines.forEach((line, index) => {
|
|
if (/teleport_confirm|teleportId|teleport_id/.test(line)) {
|
|
console.log(` physics.js:${index + 1} ${line.trim()}`);
|
|
}
|
|
});
|