聊天和指令本来就是同一个东西:以 / 开头按游戏指令发送,否则按公屏消息发送。 服务端 /api/bots/:id/chat 一直就是这么分流的,前端多一个模式下拉框纯属多余。 - 删掉「公屏聊天 / 游戏指令」下拉框与重复的「快捷公屏消息」输入框 - 只保留:一个输入框 + 快捷发送按钮 + 移动测试按钮 - 接口返回新增 kind 字段(chat / command)说明实际走的是哪条路 - Toast 区分「消息已发送」与「指令已发送:/xxx」 - 快捷发送补上 /u restore confirm(配合定时消息常用) - 空内容提示从「消息为空」改为「内容为空」(不再只针对聊天) 测试 325 -> 326,全部通过。
600 行
21 KiB
JavaScript
600 行
21 KiB
JavaScript
'use strict';
|
||
|
||
const { EventEmitter } = require('events');
|
||
|
||
const { BotManager } = require('./botManager');
|
||
const { ScheduledMessenger } = require('./scheduledMessenger');
|
||
const { ExpansionController } = require('../controller/expansionController');
|
||
const { normalizeConfig } = require('../config/expansionConfig');
|
||
const { sanitizeId } = require('../config/botsConfig');
|
||
|
||
/**
|
||
* 机器人池:同时挂多个玩家(机器人)。
|
||
*
|
||
* 每个机器人由一对 `BotManager + ExpansionController` 组成,各自独立连接、独立登录、
|
||
* 独立跑扩地状态机;但**共享**同一份领土数据服务(markers.json 是全局的,
|
||
* 没必要每个机器人各拉一遍)与同一份运行日志。
|
||
*
|
||
* 账号来源有两个,同名时以 `.env` 为准(环境变量优先级更高,避免「改了 .env 不生效」):
|
||
* 1. `.env` 的 `MC_BOTS` / `MC_USERNAME`(`source: 'env'`);
|
||
* 2. 控制台「添加玩家」写入的 `data/bots.credentials.json`(`source: 'store'`)。
|
||
*
|
||
* 每个机器人可以有自己的一套扩地配置覆盖(存在 `data/bots.json`),
|
||
* 未覆盖的字段回退到全局配置。
|
||
*/
|
||
class BotPool extends EventEmitter {
|
||
/**
|
||
* @param {object} deps
|
||
* @param {Array<object>} deps.connections 连接参数列表(来自 .env,见 botsConfig)
|
||
* @param {object} deps.config 全局配置
|
||
* @param {import('../util/logger').Logger} [deps.logger]
|
||
* @param {import('../core/territoryDataService').TerritoryDataService} deps.dataService
|
||
* @param {import('../core/expansionLog').ExpansionLog} [deps.expansionLog]
|
||
* @param {import('../core/chatLog').ChatLog} [deps.chatLog]
|
||
* @param {{load: Function, save: Function}} [deps.botStore] 机器人设置持久化
|
||
* @param {{load: Function, save: Function}} [deps.credentialStore] 控制台新增账号的凭据持久化
|
||
*/
|
||
constructor(deps) {
|
||
super();
|
||
this.config = deps.config;
|
||
this.logger = deps.logger || null;
|
||
this.dataService = deps.dataService;
|
||
this.expansionLog = deps.expansionLog || null;
|
||
this.chatLog = deps.chatLog || null;
|
||
this.botStore = deps.botStore || null;
|
||
this.credentialStore = deps.credentialStore || null;
|
||
|
||
/** @type {Map<string, object>} 机器人 id → 运行时条目 */
|
||
this.entries = new Map();
|
||
/** 已执行过「启动后自动开始扩地」的机器人(避免重连后把手动停止的任务又拉起来) */
|
||
this._autoStarted = new Set();
|
||
/** 机器人设置(非敏感部分) */
|
||
this.settings = this.botStore ? this.botStore.load().bots : {};
|
||
/** 控制台新增账号的凭据(含密码,落盘权限 0600) */
|
||
this.credentials = this.credentialStore ? this.credentialStore.load().bots : {};
|
||
/** 启动时间戳 */
|
||
this.startedAt = 0;
|
||
|
||
for (const connection of this._mergeConnections(deps.connections || [])) {
|
||
this._addBot(connection);
|
||
}
|
||
this.defaultId = this.entries.size > 0 ? this.entries.keys().next().value : '';
|
||
}
|
||
|
||
/**
|
||
* 合并两个账号来源:`.env` 优先,控制台新增的账号补齐。
|
||
* @param {Array<object>} envConnections
|
||
* @returns {Array<object>}
|
||
*/
|
||
_mergeConnections(envConnections) {
|
||
const merged = [];
|
||
const seen = new Set();
|
||
for (const connection of envConnections) {
|
||
const id = connection.id || sanitizeId(connection.username);
|
||
if (!id || seen.has(id)) continue;
|
||
seen.add(id);
|
||
merged.push(Object.assign({}, connection, { id, source: 'env' }));
|
||
}
|
||
for (const [id, credential] of Object.entries(this.credentials)) {
|
||
if (seen.has(id) || !credential || !credential.username) continue;
|
||
seen.add(id);
|
||
merged.push(Object.assign({
|
||
auth: 'offline',
|
||
host: process.env.MC_HOST || 'bgjq.simpfun.cn',
|
||
port: Number.parseInt(process.env.MC_PORT || '25565', 10),
|
||
version: process.env.MC_VERSION || '1.21.4'
|
||
}, credential, { id, source: 'store' }));
|
||
}
|
||
return merged;
|
||
}
|
||
|
||
// ==================== 组装 ====================
|
||
|
||
/**
|
||
* 创建一个机器人条目:管理器 + 控制器,并接好事件。
|
||
* @param {object} connection
|
||
* @returns {object|null} 已存在同名机器人时返回 null
|
||
*/
|
||
_addBot(connection) {
|
||
const id = connection.id || sanitizeId(connection.username);
|
||
if (!id || this.entries.has(id)) return null;
|
||
const setting = this.settings[id] || {};
|
||
const manager = new BotManager(
|
||
Object.assign({}, connection, { label: setting.label || connection.label }),
|
||
this.getConfigFor(id),
|
||
this.logger,
|
||
{ chatLog: this.chatLog }
|
||
);
|
||
const controller = new ExpansionController({
|
||
botManager: manager,
|
||
dataService: this.dataService,
|
||
logger: this.logger,
|
||
botId: id,
|
||
botName: connection.username,
|
||
expansionLog: this.expansionLog
|
||
});
|
||
// 寻路事件转发给控制器(与 Baritone 的 PathEvent 语义一致)
|
||
manager.on('modulesReady', () => {
|
||
if (manager.pathing) {
|
||
manager.pathing.onPathEvent = (event, detail) => controller.onPathEvent(event, detail);
|
||
}
|
||
// 进入世界并装配完成后按设置决定是否自动开始扩地。
|
||
// 只在本次进程内首次进入世界时触发:断线重连后不自动拉起,
|
||
// 否则用户在控制台手动停止的任务会被重连事件重新启动。
|
||
if (this._settingOf(id).autoStart && !this._autoStarted.has(id)) {
|
||
this._autoStarted.add(id);
|
||
this.startTask(id).catch((err) => {
|
||
if (this.logger) this.logger.log(`[${connection.username}] 自动开始扩地失败:${err.message}`, 'error');
|
||
});
|
||
}
|
||
});
|
||
manager.on('spawn', () => this.emit('botSpawn', id));
|
||
manager.on('end', () => this.emit('botEnd', id));
|
||
|
||
// 定时消息器:按固定间隔重复发送同一条内容(如每 300 秒 /u restore confirm)
|
||
const messenger = new ScheduledMessenger(manager, this.logger);
|
||
messenger.setTasks(setting.schedules || []);
|
||
|
||
this.entries.set(id, {
|
||
id,
|
||
connection,
|
||
manager,
|
||
controller,
|
||
messenger,
|
||
/** 账号来源:env(.env 配置)/ store(控制台新增) */
|
||
source: connection.source || 'env',
|
||
/** 该机器人实际生效的配置(全局 + 覆盖) */
|
||
config: manager.config
|
||
});
|
||
return this.entries.get(id);
|
||
}
|
||
|
||
/**
|
||
* 控制台新增一个玩家(机器人):落盘凭据 → 建立条目 → 立即连接。
|
||
*
|
||
* 密码只写进 `data/bots.credentials.json`(权限 0600,`data/` 已被 .gitignore 忽略),
|
||
* 不会进入代码仓库,也不会出现在任何接口响应里。
|
||
*
|
||
* @param {{username: string, password?: string, label?: string, auth?: string, host?: string, port?: number, version?: string}} input
|
||
* @param {{connect?: boolean}} [options] connect=false 时只登记不连接(测试或先配好再手动连接)
|
||
* @returns {object} 新建的机器人条目
|
||
*/
|
||
addBot(input, options) {
|
||
const opts = options || {};
|
||
const username = String((input && input.username) || '').trim();
|
||
if (!username) throw badRequest('缺少用户名');
|
||
if (!/^[A-Za-z0-9_]{1,16}$/.test(username)) {
|
||
throw badRequest('用户名只能是 1-16 位字母、数字或下划线(Minecraft 账号名规则)');
|
||
}
|
||
const id = sanitizeId(username);
|
||
if (this.entries.has(id)) throw badRequest('该玩家已在列表中:' + username);
|
||
if (!this.credentialStore) throw badRequest('当前部署未启用凭据存储,无法在控制台新增玩家');
|
||
|
||
const connection = {
|
||
id,
|
||
username,
|
||
password: input.password === undefined || input.password === null ? '' : String(input.password),
|
||
label: input.label ? String(input.label) : username,
|
||
auth: String(input.auth || process.env.MC_AUTH || 'offline'),
|
||
host: String(input.host || process.env.MC_HOST || 'bgjq.simpfun.cn'),
|
||
port: Number.isFinite(Number(input.port))
|
||
? Number(input.port)
|
||
: Number.parseInt(process.env.MC_PORT || '25565', 10),
|
||
version: String(input.version || process.env.MC_VERSION || '1.21.4'),
|
||
source: 'store'
|
||
};
|
||
this.credentials[id] = {
|
||
username: connection.username,
|
||
password: connection.password,
|
||
auth: connection.auth,
|
||
host: connection.host,
|
||
port: connection.port,
|
||
version: connection.version
|
||
};
|
||
this.credentialStore.save({ bots: this.credentials });
|
||
|
||
const entry = this._addBot(connection);
|
||
if (!this.defaultId) this.defaultId = id;
|
||
this._log(`已添加玩家 ${username}(密码保存在服务器本地凭据文件,权限 0600)`);
|
||
if (opts.connect !== false && this._settingOf(id).enabled) {
|
||
entry.messenger.start();
|
||
entry.manager.connect();
|
||
}
|
||
return entry;
|
||
}
|
||
|
||
/**
|
||
* 移除一个机器人。
|
||
*
|
||
* 只允许移除控制台新增的账号;`.env` 里配置的账号必须改 `.env`,
|
||
* 否则重启后它又会回来,容易让人误以为「删不掉」。
|
||
*
|
||
* @param {string} id
|
||
* @returns {object} 被移除的条目
|
||
*/
|
||
removeBot(id) {
|
||
const entry = this.require(id);
|
||
if (entry.source === 'env') {
|
||
throw badRequest(`${entry.connection.username} 是在 .env 的 MC_BOTS 里配置的,`
|
||
+ '请修改 .env 后重启;控制台只能移除在这里添加的玩家');
|
||
}
|
||
entry.messenger.stop();
|
||
entry.manager.disconnect();
|
||
this.entries.delete(id);
|
||
delete this.settings[id];
|
||
this._persistSettings();
|
||
if (this.credentials[id]) {
|
||
delete this.credentials[id];
|
||
if (this.credentialStore) this.credentialStore.save({ bots: this.credentials });
|
||
}
|
||
if (this.defaultId === id) {
|
||
this.defaultId = this.entries.size > 0 ? this.entries.keys().next().value : '';
|
||
}
|
||
this._log(`已移除玩家 ${entry.connection.username}`);
|
||
return entry;
|
||
}
|
||
|
||
// ==================== 生命周期 ====================
|
||
|
||
/** 连接所有已启用的机器人 */
|
||
start() {
|
||
this.startedAt = Date.now();
|
||
for (const entry of this.entries.values()) {
|
||
if (!this._settingOf(entry.id).enabled) {
|
||
this._log(`${entry.connection.username} 已在控制台禁用,跳过连接`);
|
||
continue;
|
||
}
|
||
entry.messenger.start();
|
||
entry.manager.connect();
|
||
}
|
||
}
|
||
|
||
/** 断开所有机器人 */
|
||
stop() {
|
||
for (const entry of this.entries.values()) {
|
||
try {
|
||
entry.controller.stop();
|
||
} catch {
|
||
// 忽略
|
||
}
|
||
entry.messenger.stop();
|
||
entry.manager.disconnect();
|
||
}
|
||
}
|
||
|
||
/** 驱动所有机器人的扩地状态机(由外部按 tick 调用) */
|
||
tick() {
|
||
for (const entry of this.entries.values()) {
|
||
try {
|
||
entry.controller.tick();
|
||
} catch (err) {
|
||
if (this.logger) {
|
||
this.logger.log(`[${entry.connection.username}] tick 异常:` + (err && err.stack ? err.stack : err), 'error');
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// ==================== 访问 ====================
|
||
|
||
/** @returns {Array<string>} 全部机器人 id */
|
||
ids() {
|
||
return Array.from(this.entries.keys());
|
||
}
|
||
|
||
/** @returns {number} 机器人数量 */
|
||
get size() {
|
||
return this.entries.size;
|
||
}
|
||
|
||
/**
|
||
* @param {string} [id] 机器人 id;缺省取第一个
|
||
* @returns {object|null} 运行时条目
|
||
*/
|
||
get(id) {
|
||
if (!id) return this.entries.get(this.defaultId) || null;
|
||
return this.entries.get(id) || null;
|
||
}
|
||
|
||
/**
|
||
* 取机器人条目,找不到时抛错(供 REST 接口返回 404 用)。
|
||
* @param {string} id
|
||
* @returns {object}
|
||
*/
|
||
require(id) {
|
||
const entry = this.get(id);
|
||
if (!entry) {
|
||
const err = new Error('机器人不存在:' + id);
|
||
err.statusCode = 404;
|
||
throw err;
|
||
}
|
||
return entry;
|
||
}
|
||
|
||
// ==================== 配置 ====================
|
||
|
||
/** @returns {object} 机器人的非敏感设置(启用 / 备注 / 自动开始 / 定时任务 / 配置覆盖) */
|
||
_settingOf(id) {
|
||
const raw = this.settings[id] || {};
|
||
return {
|
||
enabled: raw.enabled !== false,
|
||
autoStart: raw.autoStart === true,
|
||
label: raw.label || '',
|
||
schedules: Array.isArray(raw.schedules) ? raw.schedules : [],
|
||
config: raw.config && typeof raw.config === 'object' ? raw.config : {}
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 计算某机器人实际生效的配置:全局配置 + 该机器人的覆盖。
|
||
* @param {string} id
|
||
* @returns {object}
|
||
*/
|
||
getConfigFor(id) {
|
||
const setting = this._settingOf(id);
|
||
return normalizeConfig(setting.config, this.config);
|
||
}
|
||
|
||
/**
|
||
* 更新全局配置并下发给所有机器人(各自再叠加自己的覆盖)。
|
||
* @param {object} config 新的全局配置
|
||
*/
|
||
applyGlobalConfig(config) {
|
||
this.config = config;
|
||
for (const entry of this.entries.values()) {
|
||
entry.config = this.getConfigFor(entry.id);
|
||
entry.manager.applyConfig(entry.config);
|
||
entry.controller.config = entry.config;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 保存某机器人的设置(启用 / 备注 / 自动开始 / 配置覆盖)并立即生效。
|
||
*
|
||
* @param {string} id
|
||
* @param {object} patch 部分设置;`config` 字段为配置覆盖(整体替换)
|
||
* @returns {object} 生效后的设置
|
||
*/
|
||
setBotSetting(id, patch) {
|
||
const entry = this.require(id);
|
||
const current = this.settings[id] || {};
|
||
const next = Object.assign({}, current, patch || {});
|
||
if (patch && patch.config !== undefined) {
|
||
// 配置覆盖只接受已知字段,避免脏数据污染运行状态
|
||
next.config = normalizeConfig(patch.config, this.config);
|
||
}
|
||
this.settings[id] = next;
|
||
this._persistSettings();
|
||
|
||
entry.config = this.getConfigFor(id);
|
||
entry.manager.applyConfig(entry.config);
|
||
entry.controller.config = entry.config;
|
||
// 定时任务列表整体替换:把归一化结果写回设置,
|
||
// 保证「接口返回值 / 落盘内容 / 运行时状态」三者一致
|
||
if (patch && patch.schedules !== undefined) {
|
||
const normalized = entry.messenger.setTasks(this._settingOf(id).schedules);
|
||
this.settings[id] = Object.assign({}, this.settings[id], { schedules: normalized });
|
||
this._persistSettings();
|
||
}
|
||
return this._settingOf(id);
|
||
}
|
||
|
||
/** @returns {object} 机器人的设置快照(供控制台表单回填) */
|
||
settingSnapshot(id) {
|
||
const setting = this._settingOf(id);
|
||
return {
|
||
id,
|
||
label: setting.label,
|
||
enabled: setting.enabled,
|
||
autoStart: setting.autoStart,
|
||
hasOverrides: Object.keys(setting.config).length > 0,
|
||
config: this.getConfigFor(id),
|
||
schedules: this.entry(id) ? this.entry(id).messenger.snapshot() : []
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 取运行时条目(不存在时返回 null,不抛异常)。
|
||
* @param {string} id
|
||
* @returns {object|null}
|
||
*/
|
||
entry(id) {
|
||
return this.entries.get(id) || null;
|
||
}
|
||
|
||
_persistSettings() {
|
||
if (!this.botStore) return;
|
||
this.botStore.save({ bots: this.settings });
|
||
}
|
||
|
||
// ==================== 任务与控制 ====================
|
||
|
||
/**
|
||
* 启动某机器人的扩地任务。
|
||
* @param {string} id
|
||
* @param {object} [overrideConfig] 本次任务的配置覆盖(来自控制台表单)
|
||
* @returns {Promise<{ok: boolean, message: string, entry: object}>}
|
||
*/
|
||
async startTask(id, overrideConfig) {
|
||
const entry = this.require(id);
|
||
const config = overrideConfig
|
||
? normalizeConfig(overrideConfig, this.getConfigFor(id))
|
||
: this.getConfigFor(id);
|
||
if (overrideConfig) {
|
||
// 控制台点「开始扩地」时顺带保存该机器人的配置
|
||
this.settings[id] = Object.assign({}, this.settings[id] || {}, { config });
|
||
this._persistSettings();
|
||
entry.config = config;
|
||
entry.manager.applyConfig(config);
|
||
}
|
||
entry.controller.config = config;
|
||
const ok = await entry.controller.start(config);
|
||
return { ok, message: ok ? '任务已启动' : entry.controller.status, entry };
|
||
}
|
||
|
||
/** 停止某机器人的扩地任务 */
|
||
stopTask(id) {
|
||
const entry = this.require(id);
|
||
entry.controller.stop();
|
||
return entry;
|
||
}
|
||
|
||
/** 重连某机器人 */
|
||
reconnect(id) {
|
||
const entry = this.require(id);
|
||
entry.manager.disconnect();
|
||
setTimeout(() => {
|
||
// 主动断开后 BotManager 不会自动重连,这里显式重建连接
|
||
entry.manager.connect();
|
||
}, 500).unref?.();
|
||
return entry;
|
||
}
|
||
|
||
/**
|
||
* 向某机器人发送游戏指令。
|
||
*
|
||
* 登录门禁由 BotManager 判定:验证类指令(login / register / auth 等)即使在
|
||
* 未通过验证时也放行,否则会出现「要先登录才能发登录指令」的死锁。
|
||
*
|
||
* @param {string} id
|
||
* @param {string} command 不带斜杠的指令
|
||
* @returns {{ok: boolean, message: string}}
|
||
*/
|
||
sendCommand(id, command) {
|
||
const entry = this.require(id);
|
||
const text = String(command || '').replace(/^\//, '').trim();
|
||
if (!text) return { ok: false, message: '指令为空' };
|
||
if (!entry.manager.canSendCommand(text)) {
|
||
return { ok: false, message: describeSendFailure(entry.manager, text) };
|
||
}
|
||
const ok = entry.manager.sendCommand(text);
|
||
return { ok, message: ok ? '已发送:' + command : '发送失败' };
|
||
}
|
||
|
||
/**
|
||
* 向某机器人发送公屏聊天消息。
|
||
* @param {string} id
|
||
* @param {string} message 聊天正文(以 `/` 开头时按指令发送)
|
||
* @returns {{ok: boolean, message: string}}
|
||
*/
|
||
/**
|
||
* 向某机器人发送内容:聊天与指令是同一个入口,**以 `/` 开头按指令发送**,否则按公屏消息发送。
|
||
*
|
||
* @param {string} id
|
||
* @param {string} message 内容(可以带或不带前导 `/`)
|
||
* @returns {{ok: boolean, kind: string, message: string}}
|
||
*/
|
||
sendChat(id, message) {
|
||
const entry = this.require(id);
|
||
const text = String(message || '').trim();
|
||
if (!text) return { ok: false, kind: 'chat', message: '内容为空' };
|
||
const isCommand = text.startsWith('/');
|
||
const kind = isCommand ? 'command' : 'chat';
|
||
if (!entry.manager.canSendChat()) {
|
||
if (entry.manager.state !== 'online' || !entry.manager.bot) {
|
||
return { ok: false, kind, message: `${entry.connection.username} 当前未连接(状态:${entry.manager.state}),未发送` };
|
||
}
|
||
return { ok: false, kind, message: `${entry.connection.username} 未完成登录验证,已拒绝发送(服务器要求验证时会自动放行,用于发送验证码)` };
|
||
}
|
||
const ok = entry.manager.sendChat(text);
|
||
return {
|
||
ok,
|
||
kind,
|
||
message: ok ? (isCommand ? '指令已发送:' + text : '消息已发送') : '发送失败'
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 手动移动测试:按住方向键若干秒,返回实际位移。
|
||
* @param {string} id
|
||
* @param {string} direction
|
||
* @param {number} [seconds]
|
||
* @returns {Promise<{ok: boolean, message: string, from: object|null, to: object|null}>}
|
||
*/
|
||
walkTest(id, direction, seconds) {
|
||
const entry = this.require(id);
|
||
return entry.manager.walkTest(direction, seconds);
|
||
}
|
||
|
||
// ==================== 状态快照 ====================
|
||
|
||
/**
|
||
* @returns {Array<object>} 所有机器人的状态快照(含连接、任务与位置)
|
||
*/
|
||
snapshot() {
|
||
const bots = [];
|
||
for (const entry of this.entries.values()) {
|
||
const setting = this._settingOf(entry.id);
|
||
bots.push(Object.assign({}, entry.manager.snapshot(), {
|
||
enabled: setting.enabled,
|
||
autoStart: setting.autoStart,
|
||
source: entry.source,
|
||
task: entry.controller.snapshot(),
|
||
schedules: entry.messenger.snapshot()
|
||
}));
|
||
}
|
||
return bots;
|
||
}
|
||
|
||
/**
|
||
* @returns {Array<object>} 所有机器人的位置(大地图打点用)
|
||
*/
|
||
positions() {
|
||
const result = [];
|
||
for (const entry of this.entries.values()) {
|
||
const snap = entry.manager.snapshot();
|
||
result.push({
|
||
id: entry.id,
|
||
label: entry.manager.label,
|
||
username: entry.connection.username,
|
||
state: snap.state,
|
||
loggedIn: snap.loggedIn,
|
||
position: snap.position,
|
||
chunk: snap.chunk,
|
||
stateName: entry.controller.stateName,
|
||
taskState: entry.controller.state,
|
||
completedClaims: entry.controller.completedClaims
|
||
});
|
||
}
|
||
return result;
|
||
}
|
||
|
||
_log(message, level) {
|
||
if (this.logger) this.logger.log(message, level);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 构造一个 400 错误(由 Web 层转成对应 HTTP 状态码)。
|
||
* @param {string} message
|
||
* @returns {Error}
|
||
*/
|
||
function badRequest(message) {
|
||
const err = new Error(message);
|
||
err.statusCode = 400;
|
||
return err;
|
||
}
|
||
|
||
/**
|
||
* 生成「为什么这条指令发不出去」的可操作说明。
|
||
*
|
||
* 区分「没连上」与「连上了但没过登录验证」——两者的处理方式完全不同,
|
||
* 笼统报一句「发送失败」会让用户无从下手。
|
||
*
|
||
* @param {import('./botManager').BotManager} manager
|
||
* @param {string} command 不带斜杠的指令
|
||
* @returns {string}
|
||
*/
|
||
function describeSendFailure(manager, command) { const name = manager.connection.username;
|
||
if (manager.state !== 'online' || !manager.bot) {
|
||
return `${name} 当前未连接(状态:${manager.state}),指令未发送;请先重连或等待自动重连`;
|
||
}
|
||
if (manager.loginFailed) {
|
||
return `${name} 登录被服务器拒绝,请检查 .env 中的账号密码`;
|
||
}
|
||
return `${name} 未完成登录验证,已拒绝发送 /${command};`
|
||
+ '若是验证类指令(login / register / auth),可在配置的「允许免登录发送的指令前缀」中放行';
|
||
}
|
||
|
||
module.exports = { BotPool, describeSendFailure, badRequest };
|