fix: 打通登录门禁死锁,失败信息区分未连接/登录被拒/未通过验证
继续修复部署时暴露的门禁设计问题: - 登录门禁死锁:/login、/register、简幻通的 /auth 本身就是登录流程的一部分, 原先门禁把它们一并拦下,形成「要先登录才能发登录指令」,账号永远卡在未验证状态。 新增 server.preLoginCommands 白名单(默认 login,l,register,reg,auth), 这些前缀在未验证时也放行,其余指令仍要求已完成验证。 - 离线时指令被静默丢弃却回报「已发送」:bot.chat() 在未连接时无处可发。 canSendCommand() 现在要求已连接并进入世界,失败信息区分 「未连接」「登录被服务器拒绝」「未通过登录验证」三种情况并给出对应建议。 - 前端配置表单支持 list 类型字段(逗号分隔的字符串数组)。 测试从 252 增至 263,全部通过。
这个提交包含在:
+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']);
|
||||
});
|
||||
|
||||
+47
-1
@@ -146,8 +146,24 @@ test('positions 输出各机器人坐标供大地图打点', () => {
|
||||
assert.strictEqual(positions[0].stateName, '');
|
||||
});
|
||||
|
||||
test('未完成登录验证时拒绝发送指令与聊天', () => {
|
||||
test('未连接时拒绝发送指令与聊天,并说明是「未连接」', () => {
|
||||
const { pool } = setup();
|
||||
const command = pool.sendCommand('BotA', 'u claimmode');
|
||||
assert.strictEqual(command.ok, false);
|
||||
assert.ok(command.message.includes('未连接'), '离线时应明确说明未连接,而不是含糊的失败');
|
||||
|
||||
const chat = pool.sendChat('BotA', '大家好');
|
||||
assert.strictEqual(chat.ok, false);
|
||||
assert.ok(chat.message.includes('未连接'));
|
||||
});
|
||||
|
||||
test('已连接但未完成登录验证时拒绝发送指令与聊天', () => {
|
||||
const { pool } = setup();
|
||||
const entry = pool.get('BotA');
|
||||
entry.manager.bot = { chat: () => {} };
|
||||
entry.manager.state = 'online';
|
||||
entry.manager.loggedIn = false;
|
||||
|
||||
const command = pool.sendCommand('BotA', 'u claimmode');
|
||||
assert.strictEqual(command.ok, false);
|
||||
assert.ok(command.message.includes('未完成登录验证'));
|
||||
@@ -157,6 +173,36 @@ test('未完成登录验证时拒绝发送指令与聊天', () => {
|
||||
assert.ok(chat.message.includes('未完成登录验证'));
|
||||
});
|
||||
|
||||
test('未登录时验证类指令仍可下发(否则无法完成登录)', () => {
|
||||
const { pool } = setup();
|
||||
const entry = pool.get('BotA');
|
||||
const sent = [];
|
||||
entry.manager.bot = { chat: (text) => sent.push(text) };
|
||||
entry.manager.state = 'online';
|
||||
entry.manager.loggedIn = false;
|
||||
|
||||
const result = pool.sendCommand('BotA', '/auth 12345 6789');
|
||||
assert.strictEqual(result.ok, true);
|
||||
// 指令有节流,这里直接驱动写入验证内容
|
||||
entry.manager._commandQueue.length = 0;
|
||||
entry.manager._writeCommand('auth 12345 6789');
|
||||
assert.deepStrictEqual(sent, ['/auth 12345 6789']);
|
||||
});
|
||||
|
||||
test('空指令被拒绝', () => {
|
||||
const { pool } = setup();
|
||||
const result = pool.sendCommand('BotA', ' ');
|
||||
assert.strictEqual(result.ok, false);
|
||||
assert.ok(result.message.includes('指令为空'));
|
||||
});
|
||||
|
||||
test('空聊天消息被拒绝', () => {
|
||||
const { pool } = setup();
|
||||
const result = pool.sendChat('BotA', ' ');
|
||||
assert.strictEqual(result.ok, false);
|
||||
assert.ok(result.message.includes('消息为空'));
|
||||
});
|
||||
|
||||
test('完成登录验证后允许发送指令与聊天', () => {
|
||||
const { pool } = setup();
|
||||
const entry = pool.get('BotA');
|
||||
|
||||
+50
-1
@@ -172,7 +172,7 @@ test('HTTP:机器人未就绪时启动任务返回 ok=false 并给出原因',
|
||||
});
|
||||
|
||||
test('HTTP:聊天接口在未完成登录验证时返回 409,缺参数返回 400', async () => {
|
||||
const { webConsole } = setup();
|
||||
const { webConsole, pool } = setup();
|
||||
await webConsole.start();
|
||||
const base = webConsole.baseUrl();
|
||||
try {
|
||||
@@ -183,6 +183,21 @@ test('HTTP:聊天接口在未完成登录验证时返回 409,缺参数返回
|
||||
});
|
||||
assert.strictEqual(missing.status, 400);
|
||||
|
||||
// 未连接:提示应指向「没连上」而不是含糊的失败
|
||||
const offline = await fetch(base + '/api/bots/BotA/chat', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ message: '大家好' })
|
||||
});
|
||||
assert.strictEqual(offline.status, 409);
|
||||
const offlineBody = await offline.json();
|
||||
assert.ok(offlineBody.message.includes('未连接'), '离线时应说明未连接');
|
||||
|
||||
// 已连接但未通过登录验证:提示应指向登录门禁
|
||||
const manager = pool.get('BotA').manager;
|
||||
manager.state = 'online';
|
||||
manager.bot = { chat: () => {} };
|
||||
manager.loggedIn = false;
|
||||
const denied = await fetch(base + '/api/bots/BotA/chat', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -196,6 +211,40 @@ test('HTTP:聊天接口在未完成登录验证时返回 409,缺参数返回
|
||||
}
|
||||
});
|
||||
|
||||
test('HTTP:指令接口区分「未连接」与「未通过登录验证」,并放行验证类指令', async () => {
|
||||
const { webConsole, pool } = setup();
|
||||
await webConsole.start();
|
||||
const base = webConsole.baseUrl();
|
||||
const post = (command) => fetch(base + '/api/bots/BotA/command', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ command })
|
||||
});
|
||||
try {
|
||||
const offline = await post('u claimmode');
|
||||
assert.strictEqual(offline.status, 409);
|
||||
assert.ok((await offline.json()).message.includes('未连接'));
|
||||
|
||||
const manager = pool.get('BotA').manager;
|
||||
const sent = [];
|
||||
manager.bot = { chat: (text) => sent.push(text) };
|
||||
manager.state = 'online';
|
||||
manager.loggedIn = false;
|
||||
|
||||
// 任务类指令仍被拦下
|
||||
const blocked = await post('u claimmode');
|
||||
assert.strictEqual(blocked.status, 409);
|
||||
assert.ok((await blocked.json()).message.includes('未完成登录验证'));
|
||||
|
||||
// 验证类指令放行(否则无法完成登录,形成死锁)
|
||||
const allowed = await post('/auth 12345 6789');
|
||||
assert.strictEqual(allowed.status, 200);
|
||||
assert.strictEqual((await allowed.json()).ok, true);
|
||||
} finally {
|
||||
await webConsole.stop();
|
||||
}
|
||||
});
|
||||
|
||||
test('HTTP:公屏日志可按机器人查询与导出', async () => {
|
||||
const { webConsole, chatLog } = setup();
|
||||
chatLog.append({ botId: 'BotA', bot: 'BotA', message: '你好', kind: ChatKind.CHAT, username: 'Steve' });
|
||||
|
||||
在新工单中引用
屏蔽一个用户