diff --git a/CHANGELOG.md b/CHANGELOG.md index 4918cd8..9e9532f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 修正物理录制器里一个会误导排查的分类错误。 diff --git a/package-lock.json b/package-lock.json index f483be2..7fa7c2f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 942d5ea..fc70ed8 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/config/expansionConfig.js b/src/config/expansionConfig.js index 9b3499b..cbe05de 100644 --- a/src/config/expansionConfig.js +++ b/src/config/expansionConfig.js @@ -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); diff --git a/src/controller/expansionController.js b/src/controller/expansionController.js index 2f71a4e..8af7b0a 100644 --- a/src/controller/expansionController.js +++ b/src/controller/expansionController.js @@ -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(); diff --git a/test/expansionController.test.js b/test/expansionController.test.js index 6a5ef2b..1e23955 100644 --- a/test/expansionController.test.js +++ b/test/expansionController.test.js @@ -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);