feat: 1.7.0 落地 TODO 全部待办(地面修正 / 手动执行器 / 自救 / 回城 / 租约 / 通知)
按 TODO.md 的优先级把待办全部落地为代码,详见 CHANGELOG 的 [1.7.0]。 - 「走不动」攻关:地面状态修正(syncGroundToPhysics / trustSetbackOnGround)、 出站包类型分布、备选执行器 manualMover(只借 A* 规划、自己按键执行) - 原点自救分两轮并统计成败;回城前评估「原地是否更差」并记录前后坐标 - 新增目标区块租约表(targetLease)避免多机器人撞同一块 - 定时消息支持 cron 与「期望回复」确认;新增人工介入 Webhook 通知(notifier) - 大地图后台预热与视野裁剪;控制台日志按帧批量写入、配置面板支持搜索 顺带修掉两个影响部署的细节: - 部署脚本(deploy.js / ssh-helper.js)改为也能从 .env 读 SSH 凭据 —— 在命令行传密码会留在 shell 历史与进程列表里,写进已被忽略的 .env 更安全; 显式环境变量仍然优先。.env.example 补上占位符并说明这些只在本机用。 - package.json / package-lock.json 版本对齐到 1.7.0(此前停留在 1.1.0, 与 CHANGELOG 的语义化版本声明脱节)。 测试:368 → 436 个用例,`npm test` 与 `npm run check` 全绿。
这个提交包含在:
@@ -174,7 +174,17 @@ function defaultConfig() {
|
||||
* A* 的代价搜索半径:节点代价超过「起点到目标的启发值 + 该值」就被剪枝。
|
||||
* 负数表示不限制(复杂地形里最稳,但搜索更慢)。
|
||||
*/
|
||||
searchRadius: 256
|
||||
searchRadius: 256,
|
||||
/**
|
||||
* 寻路执行器:
|
||||
* - `pathfinder`(默认):由 mineflayer-pathfinder 自己执行;
|
||||
* - `manual`:**备选方案**(TODO 问题 1)。只用 A* 算路径,执行改为自己按键
|
||||
* (朝路径点转 → 按住前进 → 遇坎跳),控制键时序与手动移动测试完全一致。
|
||||
* 仅在实测确认 pathfinder 的执行层被服务端回弹时才需要切换。
|
||||
*/
|
||||
executor: 'pathfinder',
|
||||
/** 手动执行器单段执行的超时(毫秒),防止卡死时无限按键 */
|
||||
manualTimeoutMs: 180000
|
||||
},
|
||||
|
||||
// ==================== 战斗辅助(新增) ====================
|
||||
@@ -226,6 +236,30 @@ function defaultConfig() {
|
||||
* 开启后发包前会用脚下方块复核,只有确实不一致时才改写。
|
||||
*/
|
||||
reportOnGroundFromWorld: true,
|
||||
/**
|
||||
* 【方案 A】是否把物理引擎的 onGround 与本地世界数据对齐(默认开启)。
|
||||
*
|
||||
* 服务端频繁下发 `position` 包时,mineflayer 会把 `entity.onGround` 置为 false,
|
||||
* 物理引擎因此一直走「空中」分支(水平加速只有地面的 1/5 且每 tick 被清零),
|
||||
* 表现就是「按着前进键、水平速度恒为 0」。开启后每个物理 tick 都会在
|
||||
* 「物理引擎算完、位置包发出之前」用脚下方块复核,必要时强制同步 onGround,
|
||||
* 让移动回到地面分支。与 `grim.enabled` 无关(实测关闭合规层后回弹依旧)。
|
||||
*/
|
||||
syncGroundToPhysics: true,
|
||||
/**
|
||||
* 【方案 B】是否忽略「例行同步」位置包带来的 onGround=false(默认开启)。
|
||||
*
|
||||
* 服务端下发的目标位置与本地位置几乎一致时,说明它只是例行同步坐标,
|
||||
* 并不代表我们真的在空中;此时只采纳坐标、保留我们自己的 onGround,
|
||||
* 避免物理引擎的地面状态被服务端反复打断。
|
||||
* 位置差超过 `setbackSyncEpsilon` 时按真正的传送处理,不做任何干预。
|
||||
*/
|
||||
trustSetbackOnGround: true,
|
||||
/**
|
||||
* 方案 B 的判定阈值(格):服务端目标位置与本地位置之差不超过它时视为「例行同步」。
|
||||
* 实测可在控制台观察 `packetStats.lastSetbackDelta.distance` 后再调整。
|
||||
*/
|
||||
setbackSyncEpsilon: 0.5,
|
||||
/**
|
||||
* 是否每 tick 补发 `player_input` 包(默认开启,**线上服务器必需**)。
|
||||
*
|
||||
@@ -271,7 +305,51 @@ function defaultConfig() {
|
||||
/** 启动时是否自动预爬取 */
|
||||
crawlOnStart: true,
|
||||
/** 瓦片下载并发数(对源站友好,默认 4) */
|
||||
concurrency: 4
|
||||
concurrency: 4,
|
||||
/**
|
||||
* 是否后台预热高层级瓦片(默认开启)。
|
||||
*
|
||||
* 只有「启动预爬取层级」(默认 0)会批量爬,层级 1~3 原本要靠用户缩放时按需拉取,
|
||||
* 首次浏览要等好几秒。开启后机器人移动到新区域时会顺带把周边高层级瓦片拉下来。
|
||||
*/
|
||||
prewarm: true,
|
||||
/** 预热覆盖半径(方块),以机器人为中心 */
|
||||
prewarmRangeBlocks: 1024,
|
||||
/** 从「启动预爬取层级」往上预热几层(受「最大可浏览层级」限制) */
|
||||
prewarmLevels: 3,
|
||||
/** 机器人移动超过该距离(方块)才重新预热,避免频繁请求源站 */
|
||||
prewarmMinMoveBlocks: 512,
|
||||
/** 距上次预热超过该时间(毫秒)也重新预热一次(默认 10 分钟) */
|
||||
prewarmIntervalMs: 600000
|
||||
},
|
||||
|
||||
// ==================== 多机器人协调(新增) ====================
|
||||
coordination: {
|
||||
/**
|
||||
* 目标区块租约时长(秒)。
|
||||
*
|
||||
* 多个机器人同时扩地时,都用同一份领土模型挑候选区块,很容易撞在同一块上。
|
||||
* 租约表让「谁要去占哪一块」互相可见:抢到的机器人独占该区块,其余机器人跳过;
|
||||
* 持有者持续续租,机器人掉线 / 卡死后租约到期自动释放,不会永久锁死区块。
|
||||
*/
|
||||
targetLeaseTtlSeconds: 180
|
||||
},
|
||||
|
||||
// ==================== 通知提醒(新增) ====================
|
||||
notify: {
|
||||
/**
|
||||
* 是否启用 Webhook 通知(默认关闭)。
|
||||
*
|
||||
* 需要人工介入时(等待登录验证码、原点自救失败)把消息推到飞书 / Lark 自定义机器人,
|
||||
* 不用一直盯着控制台 —— 验证码只有 120 秒时限,错过就得等下次重登。
|
||||
*/
|
||||
enabled: false,
|
||||
/** Webhook 地址(留空时回退环境变量 NOTIFY_WEBHOOK_URL) */
|
||||
webhookUrl: '',
|
||||
/** 服务器要求输入验证码时提醒 */
|
||||
onVerification: true,
|
||||
/** 原点自救失败时提醒(说明可能其实是登录服,需要人工发验证码) */
|
||||
onOriginRescueFailed: true
|
||||
},
|
||||
|
||||
// ==================== 服务器交互(新增) ====================
|
||||
@@ -420,11 +498,15 @@ function validateConfig(config) {
|
||||
config.pathing.tickTimeoutMs = clampInt(config.pathing.tickTimeoutMs, 5, 5000, 40);
|
||||
// searchRadius 允许为负(不限制),因此单独钳制下限
|
||||
config.pathing.searchRadius = clampInt(config.pathing.searchRadius, -1, 100000, 256);
|
||||
if (config.pathing.executor !== 'manual') config.pathing.executor = 'pathfinder';
|
||||
config.pathing.manualTimeoutMs = clampInt(config.pathing.manualTimeoutMs, 10000, 3600000, 180000);
|
||||
|
||||
config.combat.scanRange = clampNumber(config.combat.scanRange, 1, 32, 4.5);
|
||||
config.coordination.targetLeaseTtlSeconds = clampInt(config.coordination.targetLeaseTtlSeconds, 30, 3600, 180);
|
||||
|
||||
config.grim.maxRotationPerTickDeg = clampNumber(config.grim.maxRotationPerTickDeg, 1, 180, 8.6);
|
||||
config.grim.attackReach = clampNumber(config.grim.attackReach, 1, 6, 3.0);
|
||||
config.grim.setbackSyncEpsilon = clampNumber(config.grim.setbackSyncEpsilon, 0, 5, 0.5);
|
||||
|
||||
config.bigMap.zoom = clampInt(config.bigMap.zoom, 0, 8, 0);
|
||||
config.bigMap.maxZoom = clampInt(config.bigMap.maxZoom, 0, 8, 3);
|
||||
@@ -432,6 +514,10 @@ function validateConfig(config) {
|
||||
if (config.bigMap.zoom > config.bigMap.maxZoom) config.bigMap.zoom = config.bigMap.maxZoom;
|
||||
config.bigMap.rangeBlocks = clampInt(config.bigMap.rangeBlocks, 256, 200000, 4096);
|
||||
config.bigMap.concurrency = clampInt(config.bigMap.concurrency, 1, 16, 4);
|
||||
config.bigMap.prewarmRangeBlocks = clampInt(config.bigMap.prewarmRangeBlocks, 256, 200000, 1024);
|
||||
config.bigMap.prewarmLevels = clampInt(config.bigMap.prewarmLevels, 0, 8, 3);
|
||||
config.bigMap.prewarmMinMoveBlocks = clampInt(config.bigMap.prewarmMinMoveBlocks, 0, 100000, 512);
|
||||
config.bigMap.prewarmIntervalMs = clampInt(config.bigMap.prewarmIntervalMs, 60000, 86400000, 600000);
|
||||
if (typeof config.bigMap.world !== 'string') config.bigMap.world = '';
|
||||
if (typeof config.bigMap.followBot !== 'boolean') config.bigMap.followBot = true;
|
||||
|
||||
@@ -454,10 +540,11 @@ function validateConfig(config) {
|
||||
}
|
||||
if (typeof config.stateName !== 'string') config.stateName = '';
|
||||
if (typeof config.apiKey !== 'string') config.apiKey = '';
|
||||
if (typeof config.apiV2Key !== 'string') config.apiV2Key = '';
|
||||
if (typeof config.server.claimCommand !== 'string') config.server.claimCommand = '';
|
||||
if (typeof config.server.loginCommand !== 'string') config.server.loginCommand = '';
|
||||
if (typeof config.server.registerCommand !== 'string') config.server.registerCommand = '';
|
||||
if (typeof config.server.authCommand !== 'string') config.server.authCommand = '';
|
||||
if (typeof config.notify.webhookUrl !== 'string') config.notify.webhookUrl = '';
|
||||
return config;
|
||||
}
|
||||
|
||||
|
||||
在新工单中引用
屏蔽一个用户