feat: 掉线重连后自动恢复扩地任务(autoResumeAfterReconnect)

本服会周期性把机器人踢下线(实测约 1~6 分钟一次,踢出原因是
"An internal error occurred in your connection"),每次都会走到「世界已退出,任务停止」。
原来的行为是就此停住 —— 机器人虽然能被原点自救送回真实世界(实测 4/4 成功),
却再也不会继续扩地,于是永远停在原地。

现在世界退出时把本次任务的配置记下来(resumeConfig),
重新进入世界后自动 start() 接上;状态文案改为「世界已退出,等待重连后自动恢复」,
并打印「已重新进入世界,自动恢复扩地任务」。

只有因世界退出而停的那次会登记:手动停止 / 到开垦时段外 / 耐久保护停机
都会在 stop() 里清空 resumeConfig,不会被重连重新拉起。
开关 autoResumeAfterReconnect 默认开启,可关。

测试 470 → 473。
这个提交包含在:
lyizai
2026-10-01 16:34:56 +08:00
父节点 5513ef9f5f
当前提交 0bed12e189
共修改 6 个文件,包含 106 行新增和 5 行删除
+21
查看文件
@@ -2,6 +2,27 @@
本项目遵循 [约定式提交](https://www.conventionalcommits.org/zh-hans/) 与 [语义化版本](https://semver.org/lang/zh-CN/)。
## [1.9.3] - 2026-10-01
**掉线重连后自动恢复扩地任务** —— 这是让扩地能真正连续跑起来的最后一环。
### 新增
- **`autoResumeAfterReconnect`(默认开启)**:本服会周期性把机器人踢下线
(实测约 1~6 分钟一次,踢出原因是 `An internal error occurred in your connection`),
每次都会走到「世界已退出,任务停止」。原来的行为是就此停住 ——
机器人虽然能被**原点自救**送回真实世界(实测 4/4 成功),却再也不会继续扩地,
于是永远停在原地。
现在世界退出时会把本次任务的配置记下来,重新进入世界后自动 `start()` 接上。
手动停止 / 到开垦时段外 / 耐久保护停机**不会**被自动恢复(只有因世界退出而停的那次会登记)。
- 状态文案改为「世界已退出,等待重连后自动恢复」,日志会打出
「已重新进入世界,自动恢复扩地任务」。
### 测试
新增 3 个用例(470 → 473):世界退出后暂停并在重连时恢复、开关可关闭、
手动停止不会被重连重新拉起。
## [1.9.2] - 2026-10-01
修正物理录制器里一个会误导排查的分类错误。
自动生成
+2 -2
查看文件
@@ -1,12 +1,12 @@
{
"name": "autostore-bot",
"version": "1.9.2",
"version": "1.9.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "autostore-bot",
"version": "1.9.2",
"version": "1.9.3",
"license": "MIT",
"dependencies": {
"chalk": "^4.1.2",
+1 -1
查看文件
@@ -1,6 +1,6 @@
{
"name": "autostore-bot",
"version": "1.9.2",
"version": "1.9.3",
"description": "AutoStore Bot —— Minecraft 自动扩地机器人(mineflayer 重写版,多机器人挂机,Web 控制台,开拓日志,大地图,Grim 反作弊适配)",
"main": "src/index.js",
"scripts": {
+10
查看文件
@@ -122,6 +122,15 @@ function defaultConfig() {
durabilityGuard: true,
/** 耐久保护阈值:剩余耐久百分比 ≤ 该值时回城(1-99) */
durabilityThreshold: 15,
/**
* 掉线重连后是否自动恢复被中断的扩地任务。
*
* 本服会周期性把机器人踢下线(实测约 1~6 分钟一次),每次都会触发
* 「世界已退出,任务停止」。若不自动恢复,任务就永远停在那里 ——
* 机器人虽然能被原点自救送回真实世界,却再也不会继续扩地。
* 手动停止 / 到时段外 / 耐久保护停机都不会被自动恢复。
*/
autoResumeAfterReconnect: true,
/** 是否启用自动用船过水 */
autoBoat: false,
/** 触发自动用船的水面最小宽度(格) */
@@ -495,6 +504,7 @@ function validateConfig(config) {
config.claimEndHour = clampInt(config.claimEndHour, 1, 24, 22);
config.eatThreshold = clampInt(config.eatThreshold, 0, 20, 14);
config.durabilityThreshold = clampInt(config.durabilityThreshold, 1, 99, 15);
config.autoResumeAfterReconnect = config.autoResumeAfterReconnect !== false;
config.autoBoatMinWidth = clampInt(config.autoBoatMinWidth, 1, 64, 4);
config.autoBoatMinLength = clampInt(config.autoBoatMinLength, 1, 128, 6);
config.refreshIntervalSeconds = clampInt(config.refreshIntervalSeconds, 10, 3600, 30);
+29 -1
查看文件
@@ -128,6 +128,15 @@ class ExpansionController {
/** 等待开拓时段自动开始:不在时段时点击开始会暂存配置(null 表示无等待任务) */
this.pendingStart = null;
/**
* 被「世界退出」打断的任务配置:重连进入世界后用它自动恢复。
*
* 本服会周期性把机器人踢下线(实测约 1~6 分钟一次),而每次掉线都会走到
* 「世界已退出,任务停止」。没有这个字段的话,机器人虽然能被原点自救送回
* 真实世界,任务却再也不会继续 —— 扩地永远跑不长。
* 手动停止 / 到时段外 / 耐久保护停机都不会写入它。
*/
this.resumeConfig = null;
/** 连续寻路超时累计时长(毫秒) */
this.consecutiveTimeoutMillis = 0;
/** 发送回城指令的时间戳 */
@@ -228,6 +237,8 @@ class ExpansionController {
return false;
}
this.config = requestedConfig;
// 显式启动的任务优先于「掉线待恢复」的那一次
this.resumeConfig = null;
// 现实时间限制:不在允许时段时登记「等待自动开始」,进入时段后由 tick() 自动启动
if (!this.isWithinClaimTime()) {
this.pendingStart = requestedConfig;
@@ -308,6 +319,8 @@ class ExpansionController {
/** 手动停止任务 */
stop() {
const bm = this.botManager;
// 任何显式停止都作废「掉线待恢复」——只有 tick() 里因世界退出而停的那次会重新登记
this.resumeConfig = null;
if (this.pendingStart) {
this.pendingStart = null;
this.setState(State.IDLE, '已取消自动开始');
@@ -335,11 +348,26 @@ class ExpansionController {
if (!this.config) return; // 尚未启动过任务,没有可驱动的状态
if (!bm.isSpawned()) {
if (this.isRunning()) {
// 先记下这次任务的配置:本服会周期性把机器人踢下线,重连回来时若没人管,
// 任务就永远停在这里。stop() 会清空 resumeConfig,所以要在它之后重新登记。
const interrupted = this.config;
this.stop();
this.status = '世界已退出,任务停止';
this.status = '世界已退出,等待重连后自动恢复';
if (interrupted && interrupted.autoResumeAfterReconnect !== false) {
this.resumeConfig = interrupted;
this._log('世界已退出,任务已暂停;重新进入世界后会自动恢复');
}
}
return;
}
// 重新进入世界:把被掉线打断的任务接上
if (this.resumeConfig && !this.isRunning()) {
const resume = this.resumeConfig;
this.resumeConfig = null;
this._log('已重新进入世界,自动恢复扩地任务');
this.start(resume).catch((err) => this._log('自动恢复任务失败:' + err.message, 'error'));
return;
}
this.playerChunk = this._playerChunk();
// 续租当前目标:长距离寻路可能要走几分钟,不能让租约中途过期被别人抢走
if (this.isRunning()) this._renewTargetLease();
+43 -1
查看文件
@@ -87,6 +87,8 @@ class FakeBotManager {
this.connection = { username: 'TestBot' };
this.commands = [];
this._model = model;
/** 设为 false 模拟「世界已退出」(掉线 / 被踢 / 被切到登录服) */
this.spawned = true;
}
isReady() {
@@ -94,7 +96,7 @@ class FakeBotManager {
}
isSpawned() {
return true;
return this.spawned;
}
sendCommand(command) {
@@ -147,6 +149,46 @@ function setup(options) {
return { controller, botManager, dataService, model, bot, logger, config };
}
test('世界退出后任务暂停,重连进入世界时自动恢复', async () => {
// 本服会周期性把机器人踢下线(实测约 1~6 分钟一次)。没有自动恢复的话,
// 机器人虽然能被原点自救送回真实世界,任务却再也不会继续 —— 扩地永远跑不长。
const { controller, botManager, config } = setup();
await controller.start(config);
assert.strictEqual(controller.isRunning(), true);
botManager.spawned = false;
controller.tick();
assert.strictEqual(controller.isRunning(), false, '世界退出应停止任务');
assert.match(controller.status, /等待重连后自动恢复/);
botManager.spawned = true;
controller.tick();
assert.strictEqual(controller.isRunning(), true, '重连后应自动恢复任务');
assert.ok(controller.target, '恢复后应重新选出目标区块');
assert.strictEqual(botManager.pathing.pathCalls, 2, '恢复后应重新下发寻路');
});
test('自动恢复可以关闭', async () => {
const { controller, botManager, config } = setup({ config: { autoResumeAfterReconnect: false } });
await controller.start(config);
botManager.spawned = false;
controller.tick();
botManager.spawned = true;
controller.tick();
assert.strictEqual(controller.isRunning(), false, '关掉开关后不应自动恢复');
});
test('手动停止的任务不会被自动恢复', async () => {
const { controller, botManager, config } = setup();
await controller.start(config);
controller.stop();
botManager.spawned = false;
controller.tick();
botManager.spawned = true;
controller.tick();
assert.strictEqual(controller.isRunning(), false, '手动停止不应被重连重新拉起');
});
test('start:自动识别所在邦国并下发开垦模式指令', async () => {
const { controller, botManager, config, model } = setup({ selfName: '' });
const ok = await controller.start(config);