线上实测确认手动执行器(1.7.0 备选方案)不能解决「走不动」:前 20 秒走 13 格后 即卡在「执行中断(stuck)→重规划→缩短段长→跳过目标→重新规划同一目标」的死循环。 只靠 packetStats.outboundTypes 的计数看不出「发出去的内容对不对」,所以补一个 能录原始出站序列、并能在「能走」与「走不动」两种状态下 diff 的诊断能力。 新增 src/core/packetCapture.js: - 只在录制期间挂钩 client.write,其余时间零开销(该钩子每个出站包都要过一遍); - 录到的内容做降深 + 截断的 JSON 化处理,向量对象只留坐标 (move_minecart 这类包能带上万个浮点数,不处理会把缓冲区撑爆); - 只放内存、有上限(默认 4000 包),不写盘。 接口(控制台): - POST /api/bots/:id/capture/start(可带 seconds 到点自动停止) - POST /api/bots/:id/capture/stop - GET /api/bots/:id/capture(支持 ?types= 与 ?limit=) 顺带修掉两个先前就存在的问题: - scripts/check-protocol.js 其实一直跑不通:minecraft-data 的 types.packet[1][1].type[1].fields[包名] 给的是类型名字符串, 要再去 types[名字] 取 ['container', [字段...]] 才能展开,原实现直接 .map() 必然抛错; 另有读 node_modules 少一层 '..' 的路径问题。 - 两条租约用例(expansionController / targetLease)的时序余量只有 15ms, 全量并行跑测试时偶发失败;改为 TTL 400ms 配 280ms 睡眠,各留 120ms 余量, 断言内容不变。 测试:436 → 451 个用例,连续 16 轮全量运行全绿。
18 KiB
TODO:留给后续开发者的待办与已知问题
本文档记录尚未解决的大工程、已经排查过的线索、以及推荐的验证方法。 小事已在代码里做完并提交,这里只写需要集中攻关的部分。
最后更新:2026-09-29
0. 先读这里:项目现状与部署
| 项 | 值 |
|---|---|
| 技术栈 | Node.js ≥ 22 + mineflayer 4.39.0 + mineflayer-pathfinder 2.4.5 |
| 测试 | npm test(436 个用例全绿) |
| 语法与前端 id 自检 | npm run check |
| 本地启动 | npm start(控制台 http://127.0.0.1:3080/) |
| 服务器 | root@103.236.99.177:33851,部署目录 /opt/autostore-bot |
| 控制台 | http://103.236.99.177:78/ ,密码在服务器 .env 的 WEB_PASSWORD |
| 进程管理 | pm2 应用 autostore-bot(端口 78);旧版 Fabric 机器人为 minecraft-bot(端口 91,已停) |
| 私有 Node | /opt/autostore-bot/runtime/current/bin/node(v24.16.0) |
旧版 Fabric 模组源码(重要参考,实机可用的实现):
C:\Users\luoyi\AppData\Local\Temp\autostore-deploy\oldbot.js(1356 行)。
它在这台服务器上是真的能走路、能寻路、能扩地的,所以遇到 mineflayer 行为诡异时,
第一件事应该是拿它的实现逐项对照,而不是自己猜。
⚠️ 上面的临时目录会被系统清理。若已丢失,它只作为历史参考 —— 不影响当前项目运行,但排查移动问题时很有价值,建议接手后先备份一份到仓库外。
📌 本轮(1.7.0)已把本文档里的待办全部落地为代码 —— 详见 CHANGELOG 的 [1.7.0]。 下面每节都标注了「已实现 / 待实测」,未实测的部分需要部署到服务器后按验证方法确认。
0.1 🚀 部署方法(完整步骤)
方式一:一键部署(推荐)
# 推荐:把 SSH_HOST / SSH_PORT / SSH_USER / SSH_PASS 写进 .env(已被 .gitignore 忽略),
# 然后直接跑 —— 命令行传密码会留在 shell 历史与进程列表里
npm run deploy
# 也可以临时用环境变量覆盖(.env 里的值仍作为兜底)
# Windows PowerShell
$env:SSH_HOST='103.236.99.177'; $env:SSH_PORT='33851'
$env:SSH_USER='root'; $env:SSH_PASS='<服务器密码>'
npm run deploy
# Linux / macOS
SSH_HOST=103.236.99.177 SSH_PORT=33851 SSH_USER=root SSH_PASS='<密码>' npm run deploy
scripts/deploy.js 会依次做:打包 → 上传 → 远程执行 update.sh →
轮询确认控制台返回 200。任一步失败都会明确报错。
可选环境变量:DEPLOY_APP(默认 /opt/autostore-bot)、DEPLOY_PORT(默认 78)。
SSH_* 与 DEPLOY_* 只在本机生效,服务器上的 .env 不需要填。
方式二:手动部署
# 1. 打包(务必排除 data / .env / node_modules,否则会覆盖服务器上的真实数据)
tar -czf update.tar.gz \
--exclude=node_modules --exclude=.git --exclude=data --exclude='*.log' --exclude=.env \
-C . .
# 2. 上传
scp -P 33851 update.tar.gz root@103.236.99.177:/opt/autostore-bot/update.tar.gz
# 3. 远程应用(update.sh 会保留 .env / data / node_modules / runtime)
ssh -p 33851 root@103.236.99.177 'bash /opt/autostore-bot/update.sh'
辅助脚本
| 脚本 | 用途 |
|---|---|
scripts/deploy.js |
一键部署(推荐) |
scripts/ssh-helper.js |
单条远程命令 / 上传:node scripts/ssh-helper.js exec "<命令>"、... put <本地> <远程> |
Windows 自带 OpenSSH 但没有 sshpass / plink,无法非交互传密码,
所以这两个脚本用 ssh2 直接实现协议层(ssh2 是 devDependencies,
未安装时脚本会提示 npm install --save-dev ssh2 而不是裸崩)。
凭据一律走环境变量,脚本里不写死任何密码。
update.sh 做了什么
/opt/autostore-bot/update.sh 的行为:
- 解压
update.tar.gz到/opt/autostore-bot(保留.env、data/、node_modules/、runtime/) - 用私有 Node 重新安装/校验依赖
pm2 restart autostore-bot --update-env- 打印进程状态与端口监听情况
常用运维命令
# 进程状态
pm2 status
pm2 list
# 重启 / 停止
pm2 restart autostore-bot --update-env
pm2 stop autostore-bot
# 日志(实时)
tail -f /opt/autostore-bot/data/pm2-out.log # 运行日志
tail -f /opt/autostore-bot/data/pm2-error.log # 告警与错误
pm2 logs autostore-bot
# 端口
ss -lntp | grep ':78 '
# 回滚:把上一份备份目录改回来(update.sh 会先备份旧版本目录)
ls -1d /opt/autostore-bot/backup-* 2>/dev/null | tail -3
服务器上的关键文件
| 路径 | 说明 |
|---|---|
/opt/autostore-bot/.env |
真实凭据(MC_USERNAME、MC_PASSWORD、WEB_PASSWORD、SIMP_API_KEY、SIMP_API_V2_KEY、可选 NOTIFY_WEBHOOK_URL),权限 0600,绝不进仓库 |
/opt/autostore-bot/data/autostore.json |
运行时配置(控制台保存的),会被 update.sh 保留 |
/opt/autostore-bot/data/bots.json |
机器人非敏感设置 |
/opt/autostore-bot/data/bots.credentials.json |
界面添加的账号密码,权限 0600 |
/opt/autostore-bot/data/chat/<玩家名>.jsonl |
公屏完整日志 |
/opt/autostore-bot/data/expansion-log.jsonl |
开拓日志 |
/opt/autostore-bot/data/mapcache/ |
大地图瓦片缓存 |
/opt/autostore-bot/ecosystem.config.js |
pm2 配置 |
/opt/autostore-bot/runtime/current |
私有 Node(服务器自带 Node 太旧时用) |
新增环境变量时
- 在
.env里加真实值(只在服务器上加,不要提交); - 在
.env.example里加占位符(如<your-xxx-key>),绝不含真实密钥; - 确认
.gitignore已包含.env(本项目已含,并有!.env.example例外)。
1. 🟡 「机器人走不动」:本轮已实现两套修正 + 备选执行器,待线上实测确认
现象回顾
- 手动按键移动完全正常:通过控制台「移动测试」按 forward 2 秒,实测位移 4~8 格,位置保持。
- 寻路器驱动的移动被回弹:一旦
bot.pathfinder.setGoal()开始执行, 服务端以 5~9 次/秒 的频率强制同步位置(forcedMove),机器人几乎原地不动。 - 偶尔能挪动一点,但速度只有 0.3 格/秒(正常步行 4.3 格/秒)。
已确认正常的部分(不要再怀疑这些)
| 项 | 状态 | 证据 |
|---|---|---|
player_input 补包 |
✅ 每 physicsTick 都发(~20/s),bitflags 正确 | packetStats.playerInput 持续增长 |
tick_end 补包 |
✅ 每帧发送 | grim.protocolPackets.tickEndSent |
onGround 标志(出站包) |
✅ 已按世界数据复核 | packetStats.onGroundFixed |
| 世界数据可读性 | ✅ world.getBlock 正常,四向可见 115~125 格 |
pathingDiag.visibleDistance |
| 寻路器本身 | ✅ 在开阔地能找到路径 | 实测「已探索 3633 节点 → success,代价 113.8」 |
| 服务端回弹是否持续 | ❌ 只在机器人尝试移动时出现 | 12 秒采样:空闲 +0,寻路中 +105 |
| 关闭疾跑 | 试过,无改善 | allowSprint=false 后仍是 4.8 次/秒回弹 |
关闭合规层(grim.enabled=false) |
试过,无改善 | A/B 实测 6.0 vs 5.5 次/秒,位移都是 0 |
| 世界数据是否错误 | ❌ 不是原因 | 实测脚下 stone,blockBelow 正确 |
🎯 关键线索(未变)
用 /api/status 的 moveDiag 采集到的实测数据:
- 寻路时
forward/sprint都按下、isMoving=true,但水平速度恒为 0; velocity.y = -0.078恰是自由落体第一 tick 的重力值 —— 说明速度每 tick 被清零;bot.entity.onGround被服务端位置包反复置为 false(mineflayer 的physics.js在处理position包时无条件置 false),物理引擎因此一直走「空中」分支。
✅ 本轮已实现的代码(1.7.0)
| 手段 | 开关 | 位置 |
|---|---|---|
| 方案 A:物理引擎 onGround 对齐 | grim.syncGroundToPhysics(默认开) |
botManager._installGroundStateFix |
| 方案 B:例行同步不采纳 onGround=false | grim.trustSetbackOnGround + grim.setbackSyncEpsilon(默认 0.5 格) |
同上(prependListener 抢在 mineflayer 之前快照) |
| 出站包类型分布(假设 4 的对照依据) | 常开 | packetStats.outboundTypes |
| 备选执行器:只借 A* 算路径、自己按键执行 | pathing.executor: 'manual' |
src/bot/manualMover.js |
请按下面的顺序实测:
- 部署后先在控制台确认「Grim 合规」卡片里的
地面修正一行出现非零计数 (物理对齐 N · 保住 M · 最近同步差 X 格); - 用「移动测试」确认手动移动仍然正常;
- 开始扩地,观察
moveDiag.speed是否 > 0.1、moveDiag.onGround是否稳定为 true、 「被服务器传送另有」的计数是否下降、以及是否真的能走完一段路; - 若仍有回弹 → 在配置「寻路 → 寻路执行器」切到 手动执行(备选方案), 再观察一次(它只借用 A* 规划,执行层完全是我们自己的按键时序);
- 若两者都不行 → 回到下面「未完成的排查方向」,用
packetStats.outboundTypes+ 旧模组抓包做逐项 diff(假设 5)。
未完成的排查方向(按优先级)
假设 3:跳跃 / 跑酷的物理预测不匹配
Grim 的 Prediction 检查对跳跃最严格(起跳时机、跳跃高度、疾跑跳加成)。
mineflayer 的 physics.js 在这些细节上与原版有已知偏差。
验证方法:把路径限制成纯平地,禁用一切跳跃:
{"allowSprint": false, "pathing": {"allowParkour": false, "allow1by1towers": false, "maxDropDown": 1}}
若回弹消失 → 需要在 GrimCompat 里拦下 jump 控制状态,改用更保守的移动策略。
假设 5:直接抓包对照(最彻底)
用 scripts/check-protocol.js 的思路扩展一个抓包脚本,
分别在「旧模组移动」和「mineflayer 寻路移动」时记录 5 秒的完整出站包序列,diff 出差异。
packetStats.outboundTypes 已经给出包类型分布作为起点。
提示:
scripts/check-protocol.js里读取node_modules的路径用了path.join(__dirname, 'node_modules', ...),在scripts/下是拿不到的 —— 顺手修一下(应为path.join(__dirname, '..', 'node_modules', ...))。
2. ✅ 原点自救(已实现判定与统计,实测成功率待观察)
服务器把玩家丢到世界原点附近有两种完全不同的原因,以前在日志里长得一样:
| 原因 | 判断依据 | 处理方式 | 现状 |
|---|---|---|---|
| 死亡后被卡住 | 服务器没有要求验证码 | 左右 / 前后移动即可触发服务器重新传送 | ✅ 两轮自救 + 成败判定 |
| 被切到登录服 | 服务器要求输入验证码 | 必须重新输入验证码,只能人工完成 | ✅ 已识别并提示,支持 Webhook 提醒 |
⚠️ 不要用「脚下是不是基岩」来区分这两种情况 —— 实测两种情况下脚下都是基岩。 唯一可靠的信号是服务器有没有发「请直接输入验证码进行验证」这类提示。
本轮改进:分两轮(左右 → 前后 + 跳),每轮结束后等 800ms 让传送落地再判定「位置是否恢复」;
统计 attempts / successes / lastResult(/api/status 与控制台「连接」卡片可见);
两轮都失败时升级为明确提示,并支持 Webhook 提醒。
待观察:线上实际成功率(看 originRescue.successes / attempts),
若长期为 0,说明该服务器对移动输入不再响应,应把重心放在验证码流程上。
3. ✅ 自动回城(已加「原地是否更差」评估与前后坐标)
回城是破坏性操作:它把机器人拉回安全点,会清掉玩家在游戏客户端里手动摆好的位置。
现状(1.7.0 之后):
returnOnTimeout默认关闭;即便打开,returnGraceSeconds(默认 300 秒)内不执行;- 回城前先评估:
pathing.diagnostics().neighborCount为 0(确实无路可走)才回城, 否则记为「原地还能走」并跳过目标 —— 回城只会白白丢掉位置; - 回城后会记录前后坐标与直线距离,日志写「回城结果:机器人从 (A) 挪到了 (B)(直线 X 格)」,
控制台「任务」卡片显示
最近回城;位置几乎没变时明确提示回城没有帮助。
4. ✅ 其它已知问题(本轮都已落地)
4.1 大地图 ✅
- 层级 1~3 现在有后台预热:机器人移动超过
bigMap.prewarmMinMoveBlocks(默认 512 格) 或距上次超过prewarmIntervalMs(默认 10 分钟)时,用一半并发把周边瓦片补下来; - 领土多边形按视野裁剪绘制,全世界视角不再全量绘制。
4.2 控制台 ✅
- 日志改为按帧批量写入(一次布局、一次滚动),并用
content-visibility: auto让浏览器跳过视口外行的渲染(等价于浏览器自带的虚拟列表,不改变内容与滚动位置); - 配置面板新增搜索框:按名称 / 字段名 / 说明过滤,隐藏不删除(保存时仍能读到隐藏字段的值)。
4.3 多机器人 ✅
- 新增目标区块租约表(
src/core/targetLease.js):谁先抢到谁去,其余机器人跳过; - 持有者每个 tick 续租;停止任务 / 完成任务时归还;掉线 / 卡死后租约到期自动失效;
- 候选区块全被其他机器人预定时,等 15 秒再规划(而不是提前判「没有可用区块」)。
- 待观察:多机器人同时扩地的实际撞车率(日志里出现「已被其他机器人预定,跳过」的次数)。
4.4 定时消息 ✅
- 支持 cron 表达式(5 段,本地时间,含步长 / 区间 / 列表 / 星期范围); 非法表达式不会被静默吞掉,列表里会显示原因并按间隔发送;
- 支持发送结果确认:填「期望回复」(正则)后,命中回复记「已确认」,
超时记「未确认」并写警告 —— 不再只靠人翻日志判断
/u restore confirm是否生效。
4.5 登录验证 ✅
- 验证码仍只能人工输入(服务器设计如此),但等待验证码与原点自救失败时会通过 Webhook 通知(默认按飞书 / Lark 自定义机器人的文本格式)提醒,避免错过 120 秒时限;
- 配置位置:控制台「服务器 → 通知提醒(Webhook)」,或环境变量
NOTIFY_WEBHOOK_URL。
5. 常用排查命令
# 机器人完整状态(含寻路诊断 / 地面修正计数 / 自查统计)
curl -s -H "X-Auth-Token: $PW" http://103.236.99.177:78/api/status | jq '.data.bots[0]'
# 寻路诊断:邻居数、世界可读性、四向可见距离、当前执行器
curl -s -H "X-Auth-Token: $PW" http://103.236.99.177:78/api/status | jq '.data.bots[0].pathingDiag'
# 地面状态修正与出站包类型分布
curl -s -H "X-Auth-Token: $PW" http://103.236.99.177:78/api/status | jq '.data.bots[0].packetStats'
# 原点自救成功率 / 最近回城
curl -s -H "X-Auth-Token: $PW" http://103.236.99.177:78/api/status | jq '.data.bots[0].originRescue'
curl -s -H "X-Auth-Token: $PW" http://103.236.99.177:78/api/status | jq '.data.bots[0].task.lastReturn'
# 实时日志
tail -f /opt/autostore-bot/data/pm2-out.log
tail -f /opt/autostore-bot/data/pm2-error.log
# 公屏完整日志(含服务器消息,JSONL)
tail -f /opt/autostore-bot/data/chat/<玩家名>.jsonl
# 寻路状态(判断「探索了多少节点」是定位卡死的关键指标)
grep '寻路状态' /opt/autostore-bot/data/pm2-out.log | tail -20
# 被服务器回弹的频率(>100/30秒 说明移动被反作弊拒绝)
grep '被服务器传送另有' /opt/autostore-bot/data/pm2-error.log | tail -5
# 多机器人协调:谁在占哪块(日志)
grep '已被其他机器人预定' /opt/autostore-bot/data/pm2-out.log | tail -10
# 抓出站包(诊断「走不动」的主力工具,见问题 1 · 假设 5):
# 先在「能走」时抓 5 秒,再在「走不动」时抓 5 秒,然后逐包 diff
BOT=<机器人id>
curl -s -X POST -H "X-Auth-Token: $PW" -H 'Content-Type: application/json' \
-d '{"seconds":5}' http://103.236.99.177:78/api/bots/$BOT/capture/start
# 取包类型分布(先看「发了哪几种、各多少次」)
curl -s -H "X-Auth-Token: $PW" http://103.236.99.177:78/api/bots/$BOT/capture | jq '.data.byType'
# 只取移动相关的原始包内容(看 onGround / hasHorizontalCollision / player_input 位标志 / teleportId)
curl -s -H "X-Auth-Token: $PW" \
'http://103.236.99.177:78/api/bots/$BOT/capture?types=position_look,look,position,flying,player_input,teleport_confirm&limit=60' \
| jq '.data.packets'
# 手动停止(给 seconds 时可省略)
curl -s -X POST -H "X-Auth-Token: $PW" http://103.236.99.177:78/api/bots/$BOT/capture/stop
# 协议字段定义自检(核对 mineflayer 写出的字段名与 1.21.4 是否一致)
node scripts/check-protocol.js
判断「机器人是不是卡在登录服」
curl -s -H "X-Auth-Token: $PW" http://103.236.99.177:78/api/status \
| jq '.data.bots[0] | {position, chunk, needsVerification}'
needsVerification == true→ 服务器正在等待验证码,控制台会显示醒目提示(这是可靠信号)- 位置靠近原点且
needsVerification == false→ 多半是死亡后卡住,机器人会自动自救(并记录成败) - ⚠️
world.blockBelow == "bedrock"不能用来判断 —— 实测两种情况脚下都是基岩
6. 代码约定(改动前请遵守)
- 提交信息用约定式提交(
feat:/fix:/docs:/refactor:/test:/chore:),主题行说清做了什么。 - 所有密钥只走
.env(已在.gitignore里),代码里绝不硬编码; 新增配置项要同时更新.env.example的占位符。 - 注释与文档用中文,解释「为什么」而不是「做了什么」。
- 每个
fix都应补一个能复现原问题的测试;本项目测试在test/,用node:test。 - 改前端记得跑
npm run check,它会校验app.js引用的元素 id 在index.html里都存在。 - ⚠️ 写块注释时注意:中文解释里不要出现
*/这两个字符(例如 cron 步长写法), 它会提前结束注释并导致语法错误(本项目已踩过一次)。