大地图跟随机器人 + 可调图幅 - 爬取与显示区域改为以机器人当前位置为中心(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,全部通过。
144 行
5.9 KiB
JavaScript
144 行
5.9 KiB
JavaScript
'use strict';
|
|
|
|
const test = require('node:test');
|
|
const assert = require('node:assert');
|
|
|
|
const { ScheduledMessenger, normalizeTasks, clampInterval, MIN_INTERVAL_SECONDS, MAX_TASKS } = require('../src/bot/scheduledMessenger');
|
|
|
|
/** 造一个可控的假 BotManager */
|
|
function fakeManager(options) {
|
|
const opts = options || {};
|
|
const sent = [];
|
|
return {
|
|
connection: { username: 'BotA' },
|
|
canSendChat: () => opts.canSend !== false,
|
|
sendChat: (content) => {
|
|
sent.push(content);
|
|
return true;
|
|
},
|
|
sent
|
|
};
|
|
}
|
|
|
|
test('normalizeTasks 丢弃空内容、补齐默认值、按 / 前缀判定类型', () => {
|
|
const tasks = normalizeTasks([
|
|
{ id: 'a', content: '/u restore confirm', intervalSeconds: 300, label: '恢复民心' },
|
|
{ content: '大家好', intervalSeconds: 60 },
|
|
{ content: ' ' },
|
|
null,
|
|
{ intervalSeconds: 10 }
|
|
]);
|
|
assert.strictEqual(tasks.length, 2);
|
|
assert.strictEqual(tasks[0].kind, 'command', '以 / 开头应识别为指令');
|
|
assert.strictEqual(tasks[0].enabled, true, '默认启用');
|
|
assert.strictEqual(tasks[1].kind, 'chat');
|
|
assert.ok(tasks[1].id, '缺 id 时应自动生成');
|
|
});
|
|
|
|
test('normalizeTasks 钳制间隔到合理范围并限制条数', () => {
|
|
assert.strictEqual(clampInterval(1), MIN_INTERVAL_SECONDS, '间隔不能小于下限,否则等同刷屏');
|
|
assert.strictEqual(clampInterval(999999), 86400);
|
|
assert.strictEqual(clampInterval('abc'), 300, '非法值回退到 5 分钟');
|
|
const many = [];
|
|
for (let i = 0; i < MAX_TASKS + 10; i++) many.push({ content: 'm' + i, intervalSeconds: 60 });
|
|
assert.strictEqual(normalizeTasks(many).length, MAX_TASKS);
|
|
});
|
|
|
|
test('id 重复时自动改名,保证每个任务独立计时', () => {
|
|
const tasks = normalizeTasks([
|
|
{ id: 'x', content: 'a', intervalSeconds: 60 },
|
|
{ id: 'x', content: 'b', intervalSeconds: 60 }
|
|
]);
|
|
assert.strictEqual(tasks.length, 2);
|
|
assert.notStrictEqual(tasks[0].id, tasks[1].id);
|
|
});
|
|
|
|
test('到点才发送,且按间隔重复', () => {
|
|
const manager = fakeManager();
|
|
const messenger = new ScheduledMessenger(manager, null);
|
|
const t0 = 1_000_000;
|
|
messenger.setTasks([{ id: 'restore', content: '/u restore confirm', intervalSeconds: 300 }], t0);
|
|
|
|
assert.strictEqual(messenger.tick(t0), 0, '刚设置完不应立即发送');
|
|
assert.strictEqual(messenger.tick(t0 + 299_000), 0, '未到间隔不发');
|
|
assert.strictEqual(messenger.tick(t0 + 300_000), 1, '到点应发送');
|
|
assert.deepStrictEqual(manager.sent, ['/u restore confirm']);
|
|
assert.strictEqual(messenger.tick(t0 + 599_000), 0);
|
|
assert.strictEqual(messenger.tick(t0 + 600_000), 1, '第二个周期应再发一次');
|
|
assert.strictEqual(manager.sent.length, 2);
|
|
});
|
|
|
|
test('未连接 / 未通过登录验证时跳过本次并顺延,不补发', () => {
|
|
const manager = fakeManager({ canSend: false });
|
|
const messenger = new ScheduledMessenger(manager, null);
|
|
const t0 = 1_000_000;
|
|
messenger.setTasks([{ id: 'a', content: 'hi', intervalSeconds: 60 }], t0);
|
|
assert.strictEqual(messenger.tick(t0 + 60_000), 0, '未就绪时不发送');
|
|
assert.deepStrictEqual(manager.sent, []);
|
|
// 顺延一个周期:即使中间恢复也不会把漏掉的补发出来
|
|
manager.canSendChat = () => true;
|
|
assert.strictEqual(messenger.tick(t0 + 61_000), 0, '不能补发上一周期的内容');
|
|
assert.strictEqual(messenger.tick(t0 + 120_000), 1);
|
|
assert.deepStrictEqual(manager.sent, ['hi']);
|
|
});
|
|
|
|
test('禁用的任务不发送', () => {
|
|
const manager = fakeManager();
|
|
const messenger = new ScheduledMessenger(manager, null);
|
|
messenger.setTasks([{ id: 'a', content: 'hi', intervalSeconds: 60, enabled: false }], 1_000_000);
|
|
assert.strictEqual(messenger.tick(1_000_000 + 120_000), 0);
|
|
assert.deepStrictEqual(manager.sent, []);
|
|
});
|
|
|
|
test('多个任务各自独立计时', () => {
|
|
const manager = fakeManager();
|
|
const messenger = new ScheduledMessenger(manager, null);
|
|
const t0 = 1_000_000;
|
|
messenger.setTasks([
|
|
{ id: 'fast', content: 'A', intervalSeconds: 30 },
|
|
{ id: 'slow', content: 'B', intervalSeconds: 300 }
|
|
], t0);
|
|
assert.strictEqual(messenger.tick(t0 + 30_000), 1);
|
|
assert.strictEqual(messenger.tick(t0 + 60_000), 1);
|
|
assert.strictEqual(messenger.tick(t0 + 300_000), 2, '两个任务在同一时刻都到点');
|
|
assert.deepStrictEqual(manager.sent, ['A', 'A', 'A', 'B']);
|
|
});
|
|
|
|
test('snapshot 暴露剩余秒数供控制台展示', () => {
|
|
const manager = fakeManager();
|
|
const messenger = new ScheduledMessenger(manager, null);
|
|
messenger.setTasks([{ id: 'a', label: '恢复', content: '/u restore confirm', intervalSeconds: 300 }], Date.now());
|
|
const snap = messenger.snapshot();
|
|
assert.strictEqual(snap.length, 1);
|
|
assert.strictEqual(snap[0].label, '恢复');
|
|
assert.strictEqual(snap[0].kind, 'command');
|
|
assert.ok(snap[0].nextDueAt > 0);
|
|
assert.ok(snap[0].nextInSeconds <= 300 && snap[0].nextInSeconds >= 0);
|
|
});
|
|
|
|
test('setTasks 整体替换并重置计时', () => {
|
|
const manager = fakeManager();
|
|
const messenger = new ScheduledMessenger(manager, null);
|
|
const t0 = 1_000_000;
|
|
messenger.setTasks([{ id: 'a', content: 'A', intervalSeconds: 60 }], t0);
|
|
messenger.tick(t0 + 60_000);
|
|
assert.strictEqual(manager.sent.length, 1);
|
|
|
|
messenger.setTasks([{ id: 'b', content: 'B', intervalSeconds: 60 }], t0 + 60_000);
|
|
assert.strictEqual(messenger.snapshot().length, 1);
|
|
assert.strictEqual(messenger.tick(t0 + 61_000), 0, '替换后重新计时,不应立刻发送');
|
|
assert.strictEqual(messenger.tick(t0 + 200_000), 1);
|
|
assert.deepStrictEqual(manager.sent, ['A', 'B']);
|
|
});
|
|
|
|
test('start / stop 控制心跳定时器', () => {
|
|
const messenger = new ScheduledMessenger(fakeManager(), null);
|
|
messenger.start();
|
|
assert.ok(messenger._timer, 'start 后应有定时器');
|
|
const timer = messenger._timer;
|
|
messenger.start();
|
|
assert.strictEqual(messenger._timer, timer, '重复 start 不应叠加定时器');
|
|
messenger.stop();
|
|
assert.strictEqual(messenger._timer, null);
|
|
});
|