feat: 新增机器人上/下线按钮,识别「账号已在别处登录」与「等待验证码」
下线按钮 - 顶栏新增「下线 / 上线」切换:下线会断开连接、停止自动重连,并停掉定时消息与 扩地任务;需要在游戏客户端登同一账号时必须先下线,否则会被服务端以 「您已经连接到此代理服务器了」反复踢掉 - 接口 POST /api/bots/:id/offline 与 /online;上线时会清掉「已在别处登录」标记 并把重连计数归零,否则重连会一直退避到最大间隔 识别两类此前「看起来像启动不了」的情况 - 「您已经连接到此代理服务器了」:服务端踢人时发的是聊天组件对象, 原来 JSON.stringify 之后没法看,关键信息被淹没。新增 extractKickReason() 提取纯文本,命中后给出「请先在客户端退出」的可操作提示, 并直接把重连退避到最大间隔,不再频繁硬撞服务器 - 「请直接输入验证码进行验证」:账号绑定过之后服务器仍可能每次登录都要求验证码, 而原来的正则只认「完成验证 / 身份验证」,漏掉了这句话,用户只看到机器人卡在大厅。 现在补上匹配,并在控制台弹出醒目提示 + 把输入框 placeholder 换成「输入验证码后回车发送」 测试 343 -> 355,全部通过。
这个提交包含在:
+58
-6
@@ -703,17 +703,44 @@ function renderStatus(status) {
|
||||
: `${bot.health.toFixed(1)} / ${bot.food === null || bot.food === undefined ? '-' : bot.food}`;
|
||||
$('kBotState').textContent = (bot.riding ? '乘船中 · ' : '') + (bot.inWater ? '水中' : '陆地');
|
||||
const loginEl = $('kLogin');
|
||||
loginEl.textContent = bot.loginFailed ? '登录被拒绝' : (bot.loggedIn ? '已通过' : '未通过');
|
||||
loginEl.style.color = bot.loginFailed ? 'var(--red)' : (bot.loggedIn ? 'var(--green)' : 'var(--yellow)');
|
||||
if (bot.alreadyConnected) {
|
||||
// 同一账号只能有一个连接:玩家在客户端登着没退时,机器人每次连上都会被踢
|
||||
loginEl.textContent = '账号已在别处登录';
|
||||
loginEl.style.color = 'var(--red)';
|
||||
loginEl.title = '该账号正在别处登录(通常是你的游戏客户端没退)。请先在客户端退出,或点「上线」重试。';
|
||||
} else {
|
||||
loginEl.textContent = bot.loginFailed ? '登录被拒绝' : (bot.loggedIn ? '已通过' : '未通过');
|
||||
loginEl.style.color = bot.loginFailed ? 'var(--red)' : (bot.loggedIn ? 'var(--green)' : 'var(--yellow)');
|
||||
loginEl.title = '';
|
||||
}
|
||||
$('kReconnect').textContent = String(bot.reconnectAttempts || 0);
|
||||
|
||||
// 上线 / 下线按钮:在线时是「下线」,离线时是「上线」
|
||||
const offlineBtn = $('btnOffline');
|
||||
const isOnline = bot.state === 'online' || bot.state === 'connecting';
|
||||
offlineBtn.textContent = isOnline ? '下线' : '上线';
|
||||
offlineBtn.className = 'btn' + (isOnline ? '' : ' btn-primary');
|
||||
offlineBtn.title = isOnline
|
||||
? '断开且不再自动重连(需要在游戏客户端登同一账号时用)'
|
||||
: '重新连接并恢复定时消息';
|
||||
if (document.activeElement !== $('chkEnabled')) $('chkEnabled').checked = bot.enabled !== false;
|
||||
if (document.activeElement !== $('chkAutoStart')) $('chkAutoStart').checked = bot.autoStart === true;
|
||||
|
||||
const canOperate = bot.canOperate === true;
|
||||
$('consoleHint').textContent = canOperate
|
||||
? '以 / 开头按游戏指令发送,否则按公屏消息发送。'
|
||||
: '该机器人尚未通过游戏内登录验证,发送会被拒绝(服务器要求验证时会自动放行,用于发验证码)。';
|
||||
$('consoleHint').style.color = canOperate ? 'var(--muted)' : 'var(--yellow)';
|
||||
if (bot.needsVerification) {
|
||||
$('consoleHint').textContent = '服务器正在等待验证码:把验证码填进输入框发送即可。';
|
||||
$('consoleHint').style.color = 'var(--yellow)';
|
||||
$('cmdInput').placeholder = '输入验证码后回车发送';
|
||||
} else if (canOperate) {
|
||||
$('consoleHint').textContent = '以 / 开头按游戏指令发送,否则按公屏消息发送。';
|
||||
$('consoleHint').style.color = 'var(--muted)';
|
||||
$('cmdInput').placeholder = '输入内容或 /指令,回车发送';
|
||||
} else {
|
||||
$('consoleHint').textContent = '该机器人尚未通过游戏内登录验证,发送会被拒绝(服务器要求验证时会自动放行,用于发验证码)。';
|
||||
$('consoleHint').style.color = 'var(--yellow)';
|
||||
$('cmdInput').placeholder = '输入内容或 /指令,回车发送';
|
||||
}
|
||||
$('verifyBanner').classList.toggle('hidden', bot.needsVerification !== true);
|
||||
|
||||
// ===== 任务卡片 =====
|
||||
$('kTaskState').textContent = task.state || 'IDLE';
|
||||
@@ -2367,6 +2394,7 @@ function bindEvents() {
|
||||
$('btnStart').addEventListener('click', () => startTask(selectedBotId));
|
||||
$('btnStop').addEventListener('click', () => stopTask(selectedBotId));
|
||||
$('btnReconnect').addEventListener('click', () => reconnectBot(selectedBotId));
|
||||
$('btnOffline').addEventListener('click', () => toggleBotOnline(selectedBotId));
|
||||
|
||||
$('btnStartAll').addEventListener('click', async () => {
|
||||
const bots = (lastStatus && lastStatus.bots) || [];
|
||||
@@ -2744,6 +2772,30 @@ async function reconnectBot(botId) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 上线 / 下线切换。
|
||||
*
|
||||
* 下线会断开连接并**停止自动重连**、同时停掉定时消息与扩地任务 ——
|
||||
* 需要在游戏客户端登同一个账号时必须先下线,因为同一账号只能有一个连接,
|
||||
* 否则会一直被服务端以「您已经连接到此代理服务器了」踢掉。
|
||||
*/
|
||||
async function toggleBotOnline(botId) {
|
||||
if (!botId) return;
|
||||
const bot = selectedBot(lastStatus || {});
|
||||
const isOnline = bot && (bot.state === 'online' || bot.state === 'connecting');
|
||||
const action = isOnline ? 'offline' : 'online';
|
||||
if (isOnline && !window.confirm('让机器人下线?\n\n会断开连接并停止自动重连,同时停掉定时消息与扩地任务。\n之后点「上线」即可恢复。')) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await api('/api/bots/' + encodeURIComponent(botId) + '/' + action, { method: 'POST' });
|
||||
toast(res.message || '已切换');
|
||||
if (res.data) renderStatus(res.data);
|
||||
} catch (err) {
|
||||
toast('操作失败:' + err.message, true);
|
||||
}
|
||||
}
|
||||
|
||||
/** 控制台新增一个玩家(机器人) */
|
||||
async function addBot() {
|
||||
const username = $('newBotName').value.trim();
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
<button id="btnStart" class="btn btn-primary">开始扩地</button>
|
||||
<button id="btnStop" class="btn btn-danger">停止</button>
|
||||
<button id="btnReconnect" class="btn">重连</button>
|
||||
<button id="btnOffline" class="btn" title="让机器人断开且不再自动重连(需要在游戏客户端登同一账号时用)">下线</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
@@ -272,6 +273,10 @@
|
||||
<span class="mono small" id="consoleTarget">-</span>
|
||||
</div>
|
||||
<p class="hint" id="consoleHint">以 / 开头按游戏指令发送,否则按公屏消息发送。登录验证通过后才允许发送。</p>
|
||||
<div id="verifyBanner" class="verify-banner hidden">
|
||||
<strong>服务器正在等待验证码</strong>
|
||||
<p>把验证码填进下面的输入框直接发送即可(当聊天内容发出去,不用加斜杠)。账号绑定过的服务器可能每次登录都要输一次。</p>
|
||||
</div>
|
||||
<div class="row row-compact">
|
||||
<input id="cmdInput" class="input input-grow" type="text" placeholder="输入内容或 /指令,回车发送">
|
||||
<button id="btnSendCmd" class="btn btn-sm btn-primary">发送</button>
|
||||
|
||||
@@ -338,6 +338,19 @@ html, body {
|
||||
/* ==================== 表单 ==================== */
|
||||
.card-config { display: flex; flex-direction: column; }
|
||||
.config-scope { margin-bottom: 8px; }
|
||||
|
||||
/* 需要验证码时的醒目提示 */
|
||||
.verify-banner {
|
||||
margin: 8px 0;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #7a5a18;
|
||||
border-left: 3px solid var(--yellow);
|
||||
border-radius: var(--radius);
|
||||
background: rgba(214, 160, 31, .12);
|
||||
font-size: 12.5px;
|
||||
}
|
||||
.verify-banner strong { color: var(--yellow); display: block; margin-bottom: 3px; }
|
||||
.verify-banner p { margin: 0; color: var(--text-dim); line-height: 1.5; }
|
||||
.tabs { display: flex; gap: 5px; flex-wrap: wrap; margin-bottom: 9px; }
|
||||
.tabs-inline { margin-bottom: 0; }
|
||||
.tab {
|
||||
|
||||
@@ -368,6 +368,40 @@ class WebConsole {
|
||||
});
|
||||
});
|
||||
|
||||
// 下线:断开且不再自动重连(需要在游戏客户端登同一账号时用)
|
||||
app.post('/api/bots/:id/offline', (req, res) => {
|
||||
if (typeof this.pool.disconnectBot !== 'function') {
|
||||
res.status(400).json({ ok: false, message: '当前运行模式不支持下线操作' });
|
||||
return;
|
||||
}
|
||||
this._withEntry(req, res, (entry) => {
|
||||
this.pool.disconnectBot(entry.id);
|
||||
res.json({
|
||||
ok: true,
|
||||
message: entry.connection.username + ' 已下线(不会自动重连)',
|
||||
data: this.statusPayload()
|
||||
});
|
||||
return null;
|
||||
});
|
||||
});
|
||||
|
||||
// 上线:恢复连接与定时消息
|
||||
app.post('/api/bots/:id/online', (req, res) => {
|
||||
if (typeof this.pool.connectBot !== 'function') {
|
||||
res.status(400).json({ ok: false, message: '当前运行模式不支持上线操作' });
|
||||
return;
|
||||
}
|
||||
this._withEntry(req, res, (entry) => {
|
||||
this.pool.connectBot(entry.id);
|
||||
res.json({
|
||||
ok: true,
|
||||
message: entry.connection.username + ' 正在上线',
|
||||
data: this.statusPayload()
|
||||
});
|
||||
return null;
|
||||
});
|
||||
});
|
||||
|
||||
// 手动移动测试:绕开寻路器直接按方向键,用于判断「算不出路径」还是「根本走不动」
|
||||
app.post('/api/bots/:id/move', async (req, res) => {
|
||||
const body = req.body || {};
|
||||
|
||||
在新工单中引用
屏蔽一个用户