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 行删除
+10 -2
查看文件
@@ -271,6 +271,7 @@ const CONFIG_SCHEMA = {
{ path: 'server.loginCommand', label: '登录指令模板', type: 'text', hint: '用 {password} 占位,密码取自 .env 的 MC_BOTS / MC_PASSWORD' },
{ path: 'server.registerCommand', label: '注册指令模板', type: 'text', hint: '留空则不自动注册' },
{ path: 'server.authCommand', label: '账号绑定 / 二次验证模板', type: 'text', hint: '如简幻通的 auth {password};扫码类验证无法自动完成,留空即可(日志会提示人工验证一次)' },
{ path: 'server.preLoginCommands', label: '允许免登录发送的指令前缀', type: 'list', hint: '逗号分隔,默认 login,l,register,reg,auth;这些指令在登录验证前也放行,避免死锁' },
{ path: 'server.loginConfirmTimeoutMs', label: '登录结果确认超时(毫秒)', type: 'number', min: 0, hint: '超时未收到登录提示则按已登录放行;0 表示禁用兜底' },
{ path: 'server.commandCooldownMs', label: '指令最小间隔(毫秒)', type: 'number', min: 0 },
{ path: 'server.autoReconnect', label: '断线自动重连', type: 'checkbox' },
@@ -438,7 +439,12 @@ function buildField(field) {
if (field.min !== undefined) input.min = field.min;
if (field.max !== undefined) input.max = field.max;
if (field.step !== undefined) input.step = field.step;
input.value = value === undefined || value === null ? '' : String(value);
if (field.type === 'list') {
// 字符串数组用逗号分隔编辑,避免为几个短列表单独做一套增删 UI
input.value = Array.isArray(value) ? value.join(', ') : '';
} else {
input.value = value === undefined || value === null ? '' : String(value);
}
input.dataset.path = field.path;
input.dataset.type = field.type;
wrap.appendChild(input);
@@ -464,7 +470,9 @@ function collectConfig() {
const raw = input.value;
value = /^-?\d+$/.test(raw) ? Number(raw) : raw;
} else if (type === 'number') value = input.value === '' ? 0 : Number(input.value);
else value = input.value;
else if (type === 'list') {
value = input.value.split(',').map((item) => item.trim()).filter(Boolean);
} else value = input.value;
setByPath(result, path, value);
}
return result;