refactor: 控制台输入框不再区分聊天与指令,只留一个入口
聊天和指令本来就是同一个东西:以 / 开头按游戏指令发送,否则按公屏消息发送。 服务端 /api/bots/:id/chat 一直就是这么分流的,前端多一个模式下拉框纯属多余。 - 删掉「公屏聊天 / 游戏指令」下拉框与重复的「快捷公屏消息」输入框 - 只保留:一个输入框 + 快捷发送按钮 + 移动测试按钮 - 接口返回新增 kind 字段(chat / command)说明实际走的是哪条路 - Toast 区分「消息已发送」与「指令已发送:/xxx」 - 快捷发送补上 /u restore confirm(配合定时消息常用) - 空内容提示从「消息为空」改为「内容为空」(不再只针对聊天) 测试 325 -> 326,全部通过。
这个提交包含在:
+10
@@ -2,6 +2,16 @@
|
|||||||
|
|
||||||
本项目遵循 [约定式提交](https://www.conventionalcommits.org/zh-hans/) 与 [语义化版本](https://semver.org/lang/zh-CN/)。
|
本项目遵循 [约定式提交](https://www.conventionalcommits.org/zh-hans/) 与 [语义化版本](https://semver.org/lang/zh-CN/)。
|
||||||
|
|
||||||
|
## [1.4.1] - 2026-09-27
|
||||||
|
|
||||||
|
### 变更
|
||||||
|
|
||||||
|
- **控制台输入框不再区分聊天与指令**:两者本来就是同一个入口 ——
|
||||||
|
以 `/` 开头按游戏指令发送,否则按公屏消息发送。因此删掉了「公屏聊天 / 游戏指令」下拉框
|
||||||
|
和重复的「快捷公屏消息」输入框,只保留**一个输入框 + 快捷发送 + 移动测试**。
|
||||||
|
接口返回新增 `kind` 字段说明实际走的是哪条路,Toast 也会区分「消息已发送 / 指令已发送」
|
||||||
|
- 快捷发送按钮补上 `/u restore confirm`(配合定时消息常用)
|
||||||
|
|
||||||
## [1.4.0] - 2026-09-27
|
## [1.4.0] - 2026-09-27
|
||||||
|
|
||||||
大地图改为跟随机器人、支持可调图幅;新增定时重复发送消息;控制台界面重做并适配移动端。
|
大地图改为跟随机器人、支持可调图幅;新增定时重复发送消息;控制台界面重做并适配移动端。
|
||||||
|
|||||||
@@ -441,7 +441,17 @@ npm start
|
|||||||
- **中栏**:
|
- **中栏**:
|
||||||
- 地图页签 —— **大地图**(瓦片 + 领土多边形 + 全部机器人坐标)与 **领土地图**(区块级)
|
- 地图页签 —— **大地图**(瓦片 + 领土多边形 + 全部机器人坐标)与 **领土地图**(区块级)
|
||||||
- 日志页签 —— **运行日志** / **公屏聊天** / **开拓日志**
|
- 日志页签 —— **运行日志** / **公屏聊天** / **开拓日志**
|
||||||
- **右栏**:控制台 / 聊天框(含移动测试)+ 8 个标签页的配置表单
|
- **右栏**:控制台(统一输入框 + 快捷发送 + 移动测试)+ 8 个标签页的配置表单
|
||||||
|
|
||||||
|
### 控制台输入框:聊天与指令是同一个入口
|
||||||
|
|
||||||
|
不需要切换「聊天 / 指令」模式 —— **以 `/` 开头就按游戏指令发送,否则按公屏消息发送**。
|
||||||
|
服务端 `/api/bots/:id/chat` 就是这么分流的,返回里带 `kind` 字段说明实际走的是哪条路。
|
||||||
|
|
||||||
|
另外提供:
|
||||||
|
|
||||||
|
- **快捷发送**:`/u return`、`/u claimmode`、`/u restore confirm`、`/spawn`、`/list` 一键发出
|
||||||
|
- **移动测试**:前 / 后 / 左 / 右 / 跳,绕开寻路器直接按控制键 2 秒并回报真实位移
|
||||||
|
|
||||||
### 响应式与移动端
|
### 响应式与移动端
|
||||||
|
|
||||||
@@ -510,8 +520,8 @@ npm start
|
|||||||
| POST | `/api/bots/:id/move` | 手动移动测试(`direction` / `seconds`),返回真实位移 |
|
| POST | `/api/bots/:id/move` | 手动移动测试(`direction` / `seconds`),返回真实位移 |
|
||||||
| POST | `/api/bots` | 控制台新增玩家(`username` / `password` / `label`) |
|
| POST | `/api/bots` | 控制台新增玩家(`username` / `password` / `label`) |
|
||||||
| DELETE | `/api/bots/:id` | 移除控制台新增的玩家(`.env` 里的会返回 400) |
|
| DELETE | `/api/bots/:id` | 移除控制台新增的玩家(`.env` 里的会返回 400) |
|
||||||
| POST | `/api/bots/:id/command` | 向该机器人发送游戏指令(要求已登录验证) |
|
| POST | `/api/bots/:id/chat` | 发送内容:**以 `/` 开头按指令发送,否则按公屏消息发送**(要求已登录验证) |
|
||||||
| POST | `/api/bots/:id/chat` | 向该机器人发送公屏消息(要求已登录验证) |
|
| POST | `/api/bots/:id/command` | 同上,但强制按指令发送(等价于给内容加 `/` 前缀) |
|
||||||
| GET | `/api/bots/:id/chat` | 该机器人的公屏日志(`limit` / `since` / `kind` / `keyword`) |
|
| GET | `/api/bots/:id/chat` | 该机器人的公屏日志(`limit` / `since` / `kind` / `keyword`) |
|
||||||
| POST | `/api/bots/:id/chat/export` | 导出该机器人的公屏日志到 `data/` |
|
| POST | `/api/bots/:id/chat/export` | 导出该机器人的公屏日志到 `data/` |
|
||||||
| GET | `/api/expansion-log` | 开拓日志(`limit` / `botId` / `state` / `since`)+ 统计 |
|
| GET | `/api/expansion-log` | 开拓日志(`limit` / `botId` / `state` / `since`)+ 统计 |
|
||||||
|
|||||||
+17
-4
@@ -477,18 +477,31 @@ class BotPool extends EventEmitter {
|
|||||||
* @param {string} message 聊天正文(以 `/` 开头时按指令发送)
|
* @param {string} message 聊天正文(以 `/` 开头时按指令发送)
|
||||||
* @returns {{ok: boolean, message: string}}
|
* @returns {{ok: boolean, message: string}}
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* 向某机器人发送内容:聊天与指令是同一个入口,**以 `/` 开头按指令发送**,否则按公屏消息发送。
|
||||||
|
*
|
||||||
|
* @param {string} id
|
||||||
|
* @param {string} message 内容(可以带或不带前导 `/`)
|
||||||
|
* @returns {{ok: boolean, kind: string, message: string}}
|
||||||
|
*/
|
||||||
sendChat(id, message) {
|
sendChat(id, message) {
|
||||||
const entry = this.require(id);
|
const entry = this.require(id);
|
||||||
const text = String(message || '').trim();
|
const text = String(message || '').trim();
|
||||||
if (!text) return { ok: false, message: '消息为空' };
|
if (!text) return { ok: false, kind: 'chat', message: '内容为空' };
|
||||||
|
const isCommand = text.startsWith('/');
|
||||||
|
const kind = isCommand ? 'command' : 'chat';
|
||||||
if (!entry.manager.canSendChat()) {
|
if (!entry.manager.canSendChat()) {
|
||||||
if (entry.manager.state !== 'online' || !entry.manager.bot) {
|
if (entry.manager.state !== 'online' || !entry.manager.bot) {
|
||||||
return { ok: false, message: `${entry.connection.username} 当前未连接(状态:${entry.manager.state}),消息未发送` };
|
return { ok: false, kind, message: `${entry.connection.username} 当前未连接(状态:${entry.manager.state}),未发送` };
|
||||||
}
|
}
|
||||||
return { ok: false, message: `${entry.connection.username} 未完成登录验证,已拒绝发送公屏消息(服务器要求验证时会自动放行,用于发送验证码)` };
|
return { ok: false, kind, message: `${entry.connection.username} 未完成登录验证,已拒绝发送(服务器要求验证时会自动放行,用于发送验证码)` };
|
||||||
}
|
}
|
||||||
const ok = entry.manager.sendChat(text);
|
const ok = entry.manager.sendChat(text);
|
||||||
return { ok, message: ok ? '已发送' : '发送失败' };
|
return {
|
||||||
|
ok,
|
||||||
|
kind,
|
||||||
|
message: ok ? (isCommand ? '指令已发送:' + text : '消息已发送') : '发送失败'
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+11
-26
@@ -709,8 +709,8 @@ function renderStatus(status) {
|
|||||||
|
|
||||||
const canOperate = bot.canOperate === true;
|
const canOperate = bot.canOperate === true;
|
||||||
$('consoleHint').textContent = canOperate
|
$('consoleHint').textContent = canOperate
|
||||||
? '登录验证通过:可以直接向该机器人发送指令或公屏消息。'
|
? '以 / 开头按游戏指令发送,否则按公屏消息发送。'
|
||||||
: '该机器人尚未通过游戏内登录验证,发送指令 / 消息会被拒绝。';
|
: '该机器人尚未通过游戏内登录验证,发送会被拒绝(服务器要求验证时会自动放行,用于发验证码)。';
|
||||||
$('consoleHint').style.color = canOperate ? 'var(--muted)' : 'var(--yellow)';
|
$('consoleHint').style.color = canOperate ? 'var(--muted)' : 'var(--yellow)';
|
||||||
|
|
||||||
// ===== 任务卡片 =====
|
// ===== 任务卡片 =====
|
||||||
@@ -1990,13 +1990,9 @@ function bindEvents() {
|
|||||||
$('cmdInput').addEventListener('keydown', (e) => {
|
$('cmdInput').addEventListener('keydown', (e) => {
|
||||||
if (e.key === 'Enter') sendConsole();
|
if (e.key === 'Enter') sendConsole();
|
||||||
});
|
});
|
||||||
$('btnQuickChat').addEventListener('click', sendQuickChat);
|
// 快捷发送:把预设内容填进唯一的输入框并立即发送
|
||||||
$('quickChatInput').addEventListener('keydown', (e) => {
|
|
||||||
if (e.key === 'Enter') sendQuickChat();
|
|
||||||
});
|
|
||||||
for (const btn of document.querySelectorAll('[data-cmd]')) {
|
for (const btn of document.querySelectorAll('[data-cmd]')) {
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', () => {
|
||||||
$('sendMode').value = 'command';
|
|
||||||
$('cmdInput').value = btn.dataset.cmd;
|
$('cmdInput').value = btn.dataset.cmd;
|
||||||
sendConsole();
|
sendConsole();
|
||||||
});
|
});
|
||||||
@@ -2028,33 +2024,22 @@ function bindEvents() {
|
|||||||
});
|
});
|
||||||
$('btnLogout').addEventListener('click', doLogout);
|
$('btnLogout').addEventListener('click', doLogout);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送控制台内容。
|
||||||
|
*
|
||||||
|
* 聊天和指令不区分模式:**以 `/` 开头按游戏指令发送,否则按公屏消息发送** ——
|
||||||
|
* 服务端 `/api/bots/:id/chat` 就是这么分流的,前端不需要多一个下拉框。
|
||||||
|
*/
|
||||||
async function sendConsole() {
|
async function sendConsole() {
|
||||||
const text = $('cmdInput').value.trim();
|
const text = $('cmdInput').value.trim();
|
||||||
if (!text || !selectedBotId) return;
|
if (!text || !selectedBotId) return;
|
||||||
const mode = $('sendMode').value;
|
|
||||||
const path = mode === 'command'
|
|
||||||
? `/api/bots/${encodeURIComponent(selectedBotId)}/command`
|
|
||||||
: `/api/bots/${encodeURIComponent(selectedBotId)}/chat`;
|
|
||||||
const body = mode === 'command' ? { command: text } : { message: text };
|
|
||||||
try {
|
|
||||||
await api(path, { method: 'POST', body: JSON.stringify(body) });
|
|
||||||
$('cmdInput').value = '';
|
|
||||||
toast('已发送');
|
|
||||||
} catch (err) {
|
|
||||||
toast('发送失败:' + err.message, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function sendQuickChat() {
|
|
||||||
const text = $('quickChatInput').value.trim();
|
|
||||||
if (!text || !selectedBotId) return;
|
|
||||||
try {
|
try {
|
||||||
await api(`/api/bots/${encodeURIComponent(selectedBotId)}/chat`, {
|
await api(`/api/bots/${encodeURIComponent(selectedBotId)}/chat`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ message: text })
|
body: JSON.stringify({ message: text })
|
||||||
});
|
});
|
||||||
$('quickChatInput').value = '';
|
$('cmdInput').value = '';
|
||||||
toast('公屏消息已发送');
|
toast(text.startsWith('/') ? '指令已发送:' + text : '消息已发送');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast('发送失败:' + err.message, true);
|
toast('发送失败:' + err.message, true);
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-15
@@ -267,27 +267,21 @@
|
|||||||
<section class="column col-right" id="panelConfig">
|
<section class="column col-right" id="panelConfig">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-head">
|
<div class="card-head">
|
||||||
<h2>控制台 / 聊天框</h2>
|
<h2>控制台</h2>
|
||||||
<span class="mono small" id="consoleTarget">-</span>
|
<span class="mono small" id="consoleTarget">-</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="hint" id="consoleHint">登录验证通过后可向该机器人发送指令或公屏消息。</p>
|
<p class="hint" id="consoleHint">以 / 开头按游戏指令发送,否则按公屏消息发送。登录验证通过后才允许发送。</p>
|
||||||
<div class="row row-compact">
|
<div class="row row-compact">
|
||||||
<select id="sendMode" class="input input-sm">
|
<input id="cmdInput" class="input input-grow" type="text" placeholder="输入内容或 /指令,回车发送">
|
||||||
<option value="chat">公屏聊天</option>
|
|
||||||
<option value="command">游戏指令</option>
|
|
||||||
</select>
|
|
||||||
<input id="cmdInput" class="input input-grow" type="text" placeholder="输入内容后回车发送">
|
|
||||||
<button id="btnSendCmd" class="btn btn-sm btn-primary">发送</button>
|
<button id="btnSendCmd" class="btn btn-sm btn-primary">发送</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="row row-compact">
|
<div class="row row-compact">
|
||||||
<button class="btn btn-sm" data-cmd="u return">/u return</button>
|
<span class="field-label">快捷发送</span>
|
||||||
<button class="btn btn-sm" data-cmd="u claimmode">/u claimmode</button>
|
<button class="btn btn-sm" data-cmd="/u return">/u return</button>
|
||||||
<button class="btn btn-sm" data-cmd="spawn">/spawn</button>
|
<button class="btn btn-sm" data-cmd="/u claimmode">/u claimmode</button>
|
||||||
<button class="btn btn-sm" data-cmd="list">/list</button>
|
<button class="btn btn-sm" data-cmd="/u restore confirm">/u restore confirm</button>
|
||||||
</div>
|
<button class="btn btn-sm" data-cmd="/spawn">/spawn</button>
|
||||||
<div class="row row-compact">
|
<button class="btn btn-sm" data-cmd="/list">/list</button>
|
||||||
<input id="quickChatInput" class="input input-grow" type="text" placeholder="快捷公屏消息">
|
|
||||||
<button id="btnQuickChat" class="btn btn-sm">公屏发送</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="row row-compact">
|
<div class="row row-compact">
|
||||||
<span class="field-label">移动测试</span>
|
<span class="field-label">移动测试</span>
|
||||||
|
|||||||
+5
-3
@@ -957,12 +957,14 @@ class SingleBotPool {
|
|||||||
sendChat(id, message) {
|
sendChat(id, message) {
|
||||||
const entry = this.require(id);
|
const entry = this.require(id);
|
||||||
const text = String(message || '').trim();
|
const text = String(message || '').trim();
|
||||||
if (!text) return { ok: false, message: '消息为空' };
|
if (!text) return { ok: false, kind: 'chat', message: '内容为空' };
|
||||||
|
const isCommand = text.startsWith('/');
|
||||||
|
const kind = isCommand ? 'command' : 'chat';
|
||||||
if (!entry.manager.canSendChat()) {
|
if (!entry.manager.canSendChat()) {
|
||||||
return { ok: false, message: '未完成登录验证,已拒绝发送公屏消息' };
|
return { ok: false, kind, message: '未完成登录验证,已拒绝发送' };
|
||||||
}
|
}
|
||||||
const ok = entry.manager.sendChat(text);
|
const ok = entry.manager.sendChat(text);
|
||||||
return { ok, message: ok ? '已发送' : '发送失败' };
|
return { ok, kind, message: ok ? (isCommand ? '指令已发送:' + text : '消息已发送') : '发送失败' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+17
-1
@@ -202,7 +202,23 @@ test('空聊天消息被拒绝', () => {
|
|||||||
const { pool } = setup();
|
const { pool } = setup();
|
||||||
const result = pool.sendChat('BotA', ' ');
|
const result = pool.sendChat('BotA', ' ');
|
||||||
assert.strictEqual(result.ok, false);
|
assert.strictEqual(result.ok, false);
|
||||||
assert.ok(result.message.includes('消息为空'));
|
assert.ok(result.message.includes('内容为空'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('聊天与指令共用同一入口:以 / 开头按指令发送', () => {
|
||||||
|
const { pool } = setup();
|
||||||
|
const entry = pool.get('BotA');
|
||||||
|
entry.manager.bot = { chat: () => {} };
|
||||||
|
entry.manager.state = 'online';
|
||||||
|
entry.manager.loggedIn = true;
|
||||||
|
|
||||||
|
const chat = pool.sendChat('BotA', '大家好');
|
||||||
|
assert.strictEqual(chat.kind, 'chat');
|
||||||
|
assert.ok(chat.message.includes('消息已发送'));
|
||||||
|
|
||||||
|
const command = pool.sendChat('BotA', '/u restore confirm');
|
||||||
|
assert.strictEqual(command.kind, 'command', '以 / 开头应识别为指令');
|
||||||
|
assert.ok(command.message.includes('指令已发送'));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('完成登录验证后允许发送指令与聊天', () => {
|
test('完成登录验证后允许发送指令与聊天', () => {
|
||||||
|
|||||||
在新工单中引用
屏蔽一个用户