fix: 打通登录门禁死锁,失败信息区分未连接/登录被拒/未通过验证

继续修复部署时暴露的门禁设计问题:

- 登录门禁死锁:/login、/register、简幻通的 /auth 本身就是登录流程的一部分,
  原先门禁把它们一并拦下,形成「要先登录才能发登录指令」,账号永远卡在未验证状态。
  新增 server.preLoginCommands 白名单(默认 login,l,register,reg,auth),
  这些前缀在未验证时也放行,其余指令仍要求已完成验证。

- 离线时指令被静默丢弃却回报「已发送」:bot.chat() 在未连接时无处可发。
  canSendCommand() 现在要求已连接并进入世界,失败信息区分
  「未连接」「登录被服务器拒绝」「未通过登录验证」三种情况并给出对应建议。

- 前端配置表单支持 list 类型字段(逗号分隔的字符串数组)。

测试从 252 增至 263,全部通过。
这个提交包含在:
lyizai
2026-09-27 22:11:52 +08:00
父节点 d6b1339867
当前提交 2671da95d5
共修改 10 个文件,包含 279 行新增和 18 行删除
+58 -3
查看文件
@@ -17,9 +17,6 @@ function setup(options) {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'autostore-botmgr-'));
const chatLog = new ChatLog({ dir });
const config = normalizeConfig(Object.assign(defaultConfig(), opts.config || {}));
if (opts.config && opts.config.server) {
Object.assign(config.server, opts.config.server);
}
const logger = new Logger({ echo: false });
const manager = new BotManager({
id: 'BotA',
@@ -294,3 +291,61 @@ test('config 校验补全 authCommand 与 reconnectMaxDelayMs', () => {
assert.strictEqual(config.server.authCommand, '');
assert.strictEqual(config.server.reconnectMaxDelayMs, 120000);
});
// ==================== 免登录指令白名单(避免「先登录才能发登录指令」死锁)====================
test('验证类指令在未登录时也放行(login / register / auth)', () => {
const { manager, sent } = setup({ config: { server: { commandCooldownMs: 0 } } });
manager.state = 'online';
manager.loggedIn = false;
assert.strictEqual(manager.canSendCommand('login mypass'), true);
assert.strictEqual(manager.canSendCommand('/auth 12345 6789'), true);
assert.strictEqual(manager.canSendCommand('register mypass'), true);
assert.strictEqual(manager.canSendCommand('l mypass'), true);
assert.strictEqual(manager.sendCommand('auth 12345 6789', { immediate: true }), true);
assert.deepStrictEqual(sent, ['/auth 12345 6789']);
});
test('任务类指令在未登录时仍被拒绝', () => {
const { manager, sent } = setup();
manager.state = 'online';
manager.loggedIn = false;
assert.strictEqual(manager.canSendCommand('u claimmode'), false);
assert.strictEqual(manager.canSendCommand('u return'), false);
assert.strictEqual(manager.canSendCommand('spawn'), false);
assert.strictEqual(manager.sendCommand('u claimmode'), false);
assert.deepStrictEqual(sent, []);
});
test('前缀匹配不会误放行同前缀的其它指令', () => {
const { manager } = setup();
manager.state = 'online';
manager.loggedIn = false;
// auth 放行,但 authx / authenticate 不属于白名单里的精确前缀
assert.strictEqual(manager.canSendCommand('auth 1 2'), true);
assert.strictEqual(manager.canSendCommand('authx 1 2'), false);
assert.strictEqual(manager.canSendCommand('login'), true);
assert.strictEqual(manager.canSendCommand('loginx'), false);
});
test('白名单可通过配置清空(全部指令都要求登录验证)', () => {
const { manager } = setup({ config: { server: { preLoginCommands: [] } } });
manager.state = 'online';
manager.loggedIn = false;
assert.strictEqual(manager.canSendCommand('login mypass'), false);
});
test('登录验证通过后白名单不再是唯一通道', () => {
const { manager } = setup();
manager.state = 'online';
manager.loggedIn = true;
assert.strictEqual(manager.canSendCommand('u claimmode'), true);
});
test('preLoginCommands 配置被归一化(去斜杠、去空项、转小写)', () => {
const { config } = setup({ config: { server: { preLoginCommands: ['/Login', ' AUTH ', '', ' ', 'reg'] } } });
assert.deepStrictEqual(config.server.preLoginCommands, ['login', 'auth', 'reg']);
});