feat: 新增机器人上/下线按钮,识别「账号已在别处登录」与「等待验证码」

下线按钮
- 顶栏新增「下线 / 上线」切换:下线会断开连接、停止自动重连,并停掉定时消息与
  扩地任务;需要在游戏客户端登同一账号时必须先下线,否则会被服务端以
  「您已经连接到此代理服务器了」反复踢掉
- 接口 POST /api/bots/:id/offline 与 /online;上线时会清掉「已在别处登录」标记
  并把重连计数归零,否则重连会一直退避到最大间隔

识别两类此前「看起来像启动不了」的情况
- 「您已经连接到此代理服务器了」:服务端踢人时发的是聊天组件对象,
  原来 JSON.stringify 之后没法看,关键信息被淹没。新增 extractKickReason()
  提取纯文本,命中后给出「请先在客户端退出」的可操作提示,
  并直接把重连退避到最大间隔,不再频繁硬撞服务器
- 「请直接输入验证码进行验证」:账号绑定过之后服务器仍可能每次登录都要求验证码,
  而原来的正则只认「完成验证 / 身份验证」,漏掉了这句话,用户只看到机器人卡在大厅。
  现在补上匹配,并在控制台弹出醒目提示 + 把输入框 placeholder 换成「输入验证码后回车发送」

测试 343 -> 355,全部通过。
这个提交包含在:
lyizai
2026-09-28 14:21:14 +08:00
父节点 71e78f06dc
当前提交 42a9dd6cf0
共修改 9 个文件,包含 390 行新增和 13 行删除
+43
查看文件
@@ -290,6 +290,49 @@ test('tick 驱动全部机器人且单个异常不影响其它', () => {
assert.deepStrictEqual(ticks, ['B']);
});
// ==================== 上线 / 下线 ====================
test('disconnectBot 断线并停止自动重连、定时消息与扩地任务', () => {
const { pool } = setup();
const entry = pool.get('BotA');
let disconnected = false;
let taskStopped = false;
let messengerStopped = false;
entry.manager.disconnect = () => { disconnected = true; };
entry.controller.stop = () => { taskStopped = true; };
entry.messenger.stop = () => { messengerStopped = true; };
pool.disconnectBot('BotA');
assert.strictEqual(disconnected, true, '应断开连接');
assert.strictEqual(taskStopped, true, '应停掉扩地任务');
assert.strictEqual(messengerStopped, true, '应停掉定时消息');
});
test('connectBot 清掉「已在别处登录」标记并恢复连接', () => {
const { pool } = setup();
const entry = pool.get('BotA');
entry.manager.alreadyConnected = true;
entry.manager.reconnectAttempts = 5;
const connected = [];
entry.manager.connect = () => connected.push('BotA');
let messengerStarted = false;
entry.messenger.start = () => { messengerStarted = true; };
pool.connectBot('BotA');
assert.strictEqual(entry.manager.alreadyConnected, false, '不清掉标记会一直退避到最大间隔');
assert.strictEqual(entry.manager.reconnectAttempts, 0);
assert.strictEqual(messengerStarted, true);
assert.deepStrictEqual(connected, ['BotA']);
});
test('上线 / 下线对不存在的机器人抛 404', () => {
const { pool } = setup();
assert.throws(() => pool.disconnectBot('不存在'), (err) => err.statusCode === 404);
assert.throws(() => pool.connectBot('不存在'), (err) => err.statusCode === 404);
});
test('未启用(enabled=false)的机器人不参与连接', () => {
const { pool } = setup({ settings: { BotB: { enabled: false } } });
const connected = [];