feat: 大地图跟随机器人并支持可调图幅,新增定时消息,控制台界面重做并适配移动端
大地图跟随机器人 + 可调图幅 - 爬取与显示区域改为以机器人当前位置为中心(bigMap.followBot 默认开启), 不再傻傻地爬世界原点周边 - 界面新增「图幅」下拉(±512/1024/2048/4096/8192/全世界)与「爬取周边」按钮, 只爬当前图幅内的瓦片:层级 0 每张覆盖 4096 格,±4096 只需 9 张,全世界要 100 张 - POST /api/bigmap/crawl 支持 centerX/centerZ/rangeBlocks;GET /api/bigmap 支持 rangeBlocks/bot 查询参数;启动预爬取会先等机器人进入世界再以其位置为中心 领土地图以机器人所在邦国为中心 - 之前初始视图用的是控制器的 playerChunk,而任务启动前它是 (0,0), 导致地图永远停在世界原点附近。现在按「机器人所在区块 → 包含它的邦国 → 该邦国王城/外接矩形」定位 定时重复发送消息(src/bot/scheduledMessenger.js) - 每个机器人可配多条定时任务,按固定间隔重复发送同一内容,如每 300 秒 /u restore confirm - 以 / 开头按游戏指令发送,否则按公屏消息发送 - 只在已连接且通过登录验证时发送;未就绪的周期顺延且不补发,避免重连后一次性连发 - 与指令节流队列共用,间隔下限 5 秒、单机器人最多 20 条 - 控制台新增「定时消息」卡片(新增/删除/启停 + 剩余秒数倒计时) 控制台界面重做 + 移动端适配 - 重新设计的深色主题:统一圆角、阴影、焦点态与悬停态,卡片式分层 - 响应式三档:≥1200px 三栏;≤1200px 两栏;≤820px 单栏 + 底部导航 - 移动端:触控目标 ≥36px、地图/日志高度按视口自适应、避让底部安全区、 横屏压矮地图、尊重「减少动效」偏好、Toast 全宽 - 「添加玩家」折叠进 details;顶栏窄屏自动换行且按钮等分 修复 - POST /api/bigmap/crawl 响应把中心与图幅字段传错(实际爬取正确,返回值显示成原点) - 定时任务保存后接口返回原始列表而非归一化结果,前端拿不到剩余秒数 测试 308 -> 325,全部通过。
这个提交包含在:
+35
-4
@@ -3,6 +3,7 @@
|
||||
const { EventEmitter } = require('events');
|
||||
|
||||
const { BotManager } = require('./botManager');
|
||||
const { ScheduledMessenger } = require('./scheduledMessenger');
|
||||
const { ExpansionController } = require('../controller/expansionController');
|
||||
const { normalizeConfig } = require('../config/expansionConfig');
|
||||
const { sanitizeId } = require('../config/botsConfig');
|
||||
@@ -130,11 +131,16 @@ class BotPool extends EventEmitter {
|
||||
manager.on('spawn', () => this.emit('botSpawn', id));
|
||||
manager.on('end', () => this.emit('botEnd', id));
|
||||
|
||||
// 定时消息器:按固定间隔重复发送同一条内容(如每 300 秒 /u restore confirm)
|
||||
const messenger = new ScheduledMessenger(manager, this.logger);
|
||||
messenger.setTasks(setting.schedules || []);
|
||||
|
||||
this.entries.set(id, {
|
||||
id,
|
||||
connection,
|
||||
manager,
|
||||
controller,
|
||||
messenger,
|
||||
/** 账号来源:env(.env 配置)/ store(控制台新增) */
|
||||
source: connection.source || 'env',
|
||||
/** 该机器人实际生效的配置(全局 + 覆盖) */
|
||||
@@ -190,7 +196,10 @@ class BotPool extends EventEmitter {
|
||||
const entry = this._addBot(connection);
|
||||
if (!this.defaultId) this.defaultId = id;
|
||||
this._log(`已添加玩家 ${username}(密码保存在服务器本地凭据文件,权限 0600)`);
|
||||
if (opts.connect !== false && this._settingOf(id).enabled) entry.manager.connect();
|
||||
if (opts.connect !== false && this._settingOf(id).enabled) {
|
||||
entry.messenger.start();
|
||||
entry.manager.connect();
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
@@ -209,6 +218,7 @@ class BotPool extends EventEmitter {
|
||||
throw badRequest(`${entry.connection.username} 是在 .env 的 MC_BOTS 里配置的,`
|
||||
+ '请修改 .env 后重启;控制台只能移除在这里添加的玩家');
|
||||
}
|
||||
entry.messenger.stop();
|
||||
entry.manager.disconnect();
|
||||
this.entries.delete(id);
|
||||
delete this.settings[id];
|
||||
@@ -234,6 +244,7 @@ class BotPool extends EventEmitter {
|
||||
this._log(`${entry.connection.username} 已在控制台禁用,跳过连接`);
|
||||
continue;
|
||||
}
|
||||
entry.messenger.start();
|
||||
entry.manager.connect();
|
||||
}
|
||||
}
|
||||
@@ -246,6 +257,7 @@ class BotPool extends EventEmitter {
|
||||
} catch {
|
||||
// 忽略
|
||||
}
|
||||
entry.messenger.stop();
|
||||
entry.manager.disconnect();
|
||||
}
|
||||
}
|
||||
@@ -301,13 +313,14 @@ class BotPool extends EventEmitter {
|
||||
|
||||
// ==================== 配置 ====================
|
||||
|
||||
/** @returns {object} 机器人的非敏感设置(启用 / 备注 / 自动开始 / 配置覆盖) */
|
||||
/** @returns {object} 机器人的非敏感设置(启用 / 备注 / 自动开始 / 定时任务 / 配置覆盖) */
|
||||
_settingOf(id) {
|
||||
const raw = this.settings[id] || {};
|
||||
return {
|
||||
enabled: raw.enabled !== false,
|
||||
autoStart: raw.autoStart === true,
|
||||
label: raw.label || '',
|
||||
schedules: Array.isArray(raw.schedules) ? raw.schedules : [],
|
||||
config: raw.config && typeof raw.config === 'object' ? raw.config : {}
|
||||
};
|
||||
}
|
||||
@@ -356,6 +369,13 @@ class BotPool extends EventEmitter {
|
||||
entry.config = this.getConfigFor(id);
|
||||
entry.manager.applyConfig(entry.config);
|
||||
entry.controller.config = entry.config;
|
||||
// 定时任务列表整体替换:把归一化结果写回设置,
|
||||
// 保证「接口返回值 / 落盘内容 / 运行时状态」三者一致
|
||||
if (patch && patch.schedules !== undefined) {
|
||||
const normalized = entry.messenger.setTasks(this._settingOf(id).schedules);
|
||||
this.settings[id] = Object.assign({}, this.settings[id], { schedules: normalized });
|
||||
this._persistSettings();
|
||||
}
|
||||
return this._settingOf(id);
|
||||
}
|
||||
|
||||
@@ -368,10 +388,20 @@ class BotPool extends EventEmitter {
|
||||
enabled: setting.enabled,
|
||||
autoStart: setting.autoStart,
|
||||
hasOverrides: Object.keys(setting.config).length > 0,
|
||||
config: this.getConfigFor(id)
|
||||
config: this.getConfigFor(id),
|
||||
schedules: this.entry(id) ? this.entry(id).messenger.snapshot() : []
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 取运行时条目(不存在时返回 null,不抛异常)。
|
||||
* @param {string} id
|
||||
* @returns {object|null}
|
||||
*/
|
||||
entry(id) {
|
||||
return this.entries.get(id) || null;
|
||||
}
|
||||
|
||||
_persistSettings() {
|
||||
if (!this.botStore) return;
|
||||
this.botStore.save({ bots: this.settings });
|
||||
@@ -486,7 +516,8 @@ class BotPool extends EventEmitter {
|
||||
enabled: setting.enabled,
|
||||
autoStart: setting.autoStart,
|
||||
source: entry.source,
|
||||
task: entry.controller.snapshot()
|
||||
task: entry.controller.snapshot(),
|
||||
schedules: entry.messenger.snapshot()
|
||||
}));
|
||||
}
|
||||
return bots;
|
||||
|
||||
在新工单中引用
屏蔽一个用户