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 行删除
+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' });