文件
BotStore/test/botsConfig.test.js
T
lyizai 39624da2d5 feat: 支持多机器人挂机、开拓日志、公屏完整日志与大地图
多机器人(src/bot/botPool.js、src/config/botsConfig.js)
- .env 用 MC_BOTS 一次配置多个账号(JSON 数组或 用户名:密码 逗号分隔),未配置时回退单机器人
- 每个机器人独立连接 / 登录 / 扩地状态机,共享一份领土数据服务与运行日志
- 密码只从 .env 读取;data/bots.json 只存启用状态、备注名、自动开始与各自的配置覆盖
- 每个机器人可覆盖全局配置(策略 / 目标邦国 / 目标数量等),未覆盖字段回退全局默认

开拓日志(src/core/expansionLog.js)
- 逐条记录开拓玩家、邦国、区块、方块坐标、时间与判定来源,追加写入 data/expansion-log.jsonl
- 支持按机器人 / 邦国 / 时间筛选、统计、导出带 BOM 的 CSV,超过 16MB 自动轮转

游戏公屏完整日志与聊天框(src/core/chatLog.js)
- 每个机器人的全部公屏 / 系统 / 动作栏消息按机器人分文件留档,自身发出的聊天与指令同样留档
- 控制台新增公屏聊天页签与消息发送框

登录验证门禁
- 游戏侧:未完成登录验证时拒绝发送指令 / 聊天,isReady() 也不放行扩地任务;
  新增 server.loginConfirmTimeoutMs 兜底,避免服务器不回提示导致永远卡在未登录
- 控制台侧:新增 WEB_PASSWORD 与 /api/login、/api/session,页面登录框验证通过才允许操作

大地图(src/core/bigMapService.js)
- 爬取服务器地图站点(squaremap)的瓦片图片与领土标记,一张图上看全部机器人坐标
- 坐标系对源站实测标定并用 27 个王城交叉验证;瓦片磁盘缓存,层级 0 启动预爬取、更高层级按需穿透
- 领土按 markers.json 原始多边形绘制(预编译 Path2D,even-odd 保留孔洞),叠加王城与机器人标签

配套改造
- TerritoryModel 新增 flyingChunksOf / ownChunksOf,邦国归属改为按名称显式传入,
  不再依赖全局 selfStateName(多机器人会互相覆盖)
- Web 接口以 /api/bots/:id/... 为主,原单机器人接口保留为默认机器人的等价别名
- 新增 scripts/check-web-ids.js 校验前端元素引用一致性

测试从 154 增至 242,全部通过。
2026-09-27 21:22:57 +08:00

78 行
3.0 KiB
JavaScript

'use strict';
const test = require('node:test');
const assert = require('node:assert');
const { parseBotsFromEnv, sanitizeId, normalizePort } = require('../src/config/botsConfig');
test('MC_BOTS JSON 数组写法解析多个机器人', () => {
const bots = parseBotsFromEnv({
MC_HOST: 'mc.example.com',
MC_PORT: '25566',
MC_VERSION: '1.21.1',
MC_AUTH: 'offline',
MC_BOTS: JSON.stringify([
{ username: 'BotA', password: 'p1' },
{ username: 'BotB', password: 'p2', label: '二号机' }
])
});
assert.strictEqual(bots.length, 2);
assert.deepStrictEqual(bots.map((b) => b.username), ['BotA', 'BotB']);
assert.strictEqual(bots[0].host, 'mc.example.com');
assert.strictEqual(bots[0].port, 25566);
assert.strictEqual(bots[0].version, '1.21.1');
assert.strictEqual(bots[1].label, '二号机');
assert.strictEqual(bots[1].password, 'p2');
});
test('MC_BOTS 逗号分隔写法解析用户名与密码', () => {
const bots = parseBotsFromEnv({ MC_BOTS: 'BotA:p1, BotB:p2' });
assert.deepStrictEqual(bots.map((b) => [b.username, b.password]), [['BotA', 'p1'], ['BotB', 'p2']]);
});
test('MC_BOTS 中没有冒号的项视为「只有用户名」', () => {
const bots = parseBotsFromEnv({ MC_BOTS: 'BotA' });
assert.strictEqual(bots.length, 1);
assert.strictEqual(bots[0].password, '');
});
test('未配置 MC_BOTS 时回退到 MC_USERNAME / MC_PASSWORD', () => {
const bots = parseBotsFromEnv({ MC_USERNAME: 'SoloBot', MC_PASSWORD: 'pw' });
assert.strictEqual(bots.length, 1);
assert.strictEqual(bots[0].username, 'SoloBot');
assert.strictEqual(bots[0].password, 'pw');
});
test('MC_BOTS 解析失败(非法 JSON)时回退到单机器人兜底', () => {
const bots = parseBotsFromEnv({ MC_BOTS: '[{bad json', MC_USERNAME: 'Fallback', MC_PASSWORD: 'p' });
assert.strictEqual(bots.length, 1);
assert.strictEqual(bots[0].username, 'Fallback');
});
test('重名机器人被去重,空用户名被丢弃', () => {
const bots = parseBotsFromEnv({ MC_BOTS: 'BotA:p1,BotA:p2,, ,BotB:p3' });
assert.deepStrictEqual(bots.map((b) => b.username), ['BotA', 'BotB']);
assert.strictEqual(bots[0].password, 'p1', '应保留第一个同名条目');
});
test('id 由用户名安全化生成,可用于 URL 与文件名', () => {
const bots = parseBotsFromEnv({ MC_BOTS: 'a/b:c:p1' });
assert.strictEqual(bots.length, 1);
assert.strictEqual(bots[0].username, 'a/b');
assert.strictEqual(bots[0].id, 'a_b');
assert.strictEqual(sanitizeId('正常名字_123'), '正常名字_123');
});
test('端口非法时回退到 25565', () => {
assert.strictEqual(normalizePort('abc'), 25565);
assert.strictEqual(normalizePort('0'), 25565);
assert.strictEqual(normalizePort('70000'), 25565);
assert.strictEqual(normalizePort('25565'), 25565);
const bots = parseBotsFromEnv({ MC_BOTS: 'BotA:p', MC_PORT: 'x' });
assert.strictEqual(bots[0].port, 25565);
});
test('没有任何配置时返回空列表', () => {
assert.deepStrictEqual(parseBotsFromEnv({}), []);
});