feat: 大地图跟随机器人并支持可调图幅,新增定时消息,控制台界面重做并适配移动端

大地图跟随机器人 + 可调图幅
- 爬取与显示区域改为以机器人当前位置为中心(bigMap.followBot 默认开启),
  不再傻傻地爬世界原点周边
- 界面新增「图幅」下拉(±512/1024/2048/4096/8192/全世界)与「爬取周边」按钮,
  只爬当前图幅内的瓦片:层级 0 每张覆盖 4096 格,±4096 只需 9 张,全世界要 100 张
- POST /api/bigmap/crawl 支持 centerX/centerZ/rangeBlocks;GET /api/bigmap 支持
  rangeBlocks/bot 查询参数;启动预爬取会先等机器人进入世界再以其位置为中心

领土地图以机器人所在邦国为中心
- 之前初始视图用的是控制器的 playerChunk,而任务启动前它是 (0,0),
  导致地图永远停在世界原点附近。现在按「机器人所在区块 → 包含它的邦国 →
  该邦国王城/外接矩形」定位

定时重复发送消息(src/bot/scheduledMessenger.js)
- 每个机器人可配多条定时任务,按固定间隔重复发送同一内容,如每 300 秒 /u restore confirm
- 以 / 开头按游戏指令发送,否则按公屏消息发送
- 只在已连接且通过登录验证时发送;未就绪的周期顺延且不补发,避免重连后一次性连发
- 与指令节流队列共用,间隔下限 5 秒、单机器人最多 20 条
- 控制台新增「定时消息」卡片(新增/删除/启停 + 剩余秒数倒计时)

控制台界面重做 + 移动端适配
- 重新设计的深色主题:统一圆角、阴影、焦点态与悬停态,卡片式分层
- 响应式三档:≥1200px 三栏;≤1200px 两栏;≤820px 单栏 + 底部导航
- 移动端:触控目标 ≥36px、地图/日志高度按视口自适应、避让底部安全区、
  横屏压矮地图、尊重「减少动效」偏好、Toast 全宽
- 「添加玩家」折叠进 details;顶栏窄屏自动换行且按钮等分

修复
- POST /api/bigmap/crawl 响应把中心与图幅字段传错(实际爬取正确,返回值显示成原点)
- 定时任务保存后接口返回原始列表而非归一化结果,前端拿不到剩余秒数

测试 308 -> 325,全部通过。
这个提交包含在:
lyizai
2026-09-28 00:27:58 +08:00
父节点 fcd5fb2bd9
当前提交 a83dea9677
共修改 14 个文件,包含 1579 行新增和 343 行删除
+137 -79
查看文件
@@ -2,7 +2,8 @@
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<meta name="theme-color" content="#0d1117">
<title>AutoStore Bot 控制台</title>
<link rel="stylesheet" href="style.css">
</head>
@@ -10,46 +11,62 @@
<header class="topbar">
<div class="brand">
<span class="logo">AS</span>
<div>
<div class="brand-text">
<h1>AutoStore Bot</h1>
<p>多机器人自动扩地 · mineflayer · Grim 合规 · 大地图</p>
</div>
</div>
<div class="topbar-right">
<label class="inline-field">
当前机器人
<label class="field-inline">
<span class="field-label">当前机器人</span>
<select id="botSelect" class="input input-sm"></select>
</label>
<span id="connBadge" class="badge badge-offline">未连接</span>
<span id="taskBadge" class="badge badge-idle">空闲</span>
<span id="authBadge" class="badge badge-idle" title="控制台登录状态">未登录</span>
<button id="btnStart" class="btn btn-primary">开始扩地</button>
<button id="btnStop" class="btn btn-danger">停止</button>
<button id="btnReconnect" class="btn">重连</button>
<div class="badges">
<span id="connBadge" class="badge badge-offline">未连接</span>
<span id="taskBadge" class="badge badge-idle">空闲</span>
<span id="authBadge" class="badge badge-idle" title="控制台登录状态">未登录</span>
</div>
<div class="topbar-actions">
<button id="btnStart" class="btn btn-primary">开始扩地</button>
<button id="btnStop" class="btn btn-danger">停止</button>
<button id="btnReconnect" class="btn">重连</button>
</div>
</div>
</header>
<nav class="mobile-nav" id="mobileNav">
<button class="mobile-nav-item active" data-scroll="panelStatus">状态</button>
<button class="mobile-nav-item" data-scroll="panelMap">地图</button>
<button class="mobile-nav-item" data-scroll="panelLogs">日志</button>
<button class="mobile-nav-item" data-scroll="panelConfig">配置</button>
</nav>
<main class="layout">
<!-- ==================== 左栏:状态面板 ==================== -->
<section class="column col-left">
<section class="column col-left" id="panelStatus">
<div class="card">
<div class="card-head">
<h2>机器人(<span id="botCount">0</span>)</h2>
<div class="row">
<div class="row row-compact">
<button id="btnStartAll" class="btn btn-sm">全部开始</button>
<button id="btnStopAll" class="btn btn-sm">全部停止</button>
</div>
</div>
<div id="botList" class="bot-list"></div>
<div class="add-bot">
<div class="row">
<input id="newBotName" class="input input-sm" type="text" placeholder="玩家名" maxlength="16">
<input id="newBotPass" class="input input-sm" type="password" placeholder="服务器密码(可留空)">
<input id="newBotLabel" class="input input-sm" type="text" placeholder="备注(可留空)">
<button id="btnAddBot" class="btn btn-sm btn-primary">添加玩家</button>
<details class="add-bot">
<summary>+ 添加玩家</summary>
<div class="field">
<input id="newBotName" class="input" type="text" placeholder="玩家名(1-16 位字母/数字/下划线)" maxlength="16">
</div>
<div class="field">
<input id="newBotPass" class="input" type="password" placeholder="服务器密码(可留空)" autocomplete="new-password">
</div>
<div class="field">
<input id="newBotLabel" class="input" type="text" placeholder="备注(可留空)">
</div>
<button id="btnAddBot" class="btn btn-sm btn-primary btn-block">添加</button>
<p class="hint">密码只保存在服务器本地凭据文件(权限 0600),不会进仓库、也不会出现在任何接口响应里。.env 里配置的账号需改 .env 才能移除。</p>
</div>
</details>
</div>
<div class="card">
@@ -89,6 +106,35 @@
<div class="progress"><div id="progressBar" class="progress-fill"></div></div>
</div>
<div class="card">
<div class="card-head">
<h2>定时消息</h2>
<button id="btnAddSchedule" class="btn btn-sm">+ 新增</button>
</div>
<div id="scheduleList" class="schedule-list"></div>
<div id="scheduleEditor" class="schedule-editor hidden">
<div class="field">
<label>内容(以 / 开头按游戏指令发送)</label>
<input id="schedContent" class="input" type="text" placeholder="/u restore confirm">
</div>
<div class="field-row">
<div class="field">
<label>间隔(秒)</label>
<input id="schedInterval" class="input" type="number" min="5" value="300">
</div>
<div class="field">
<label>备注</label>
<input id="schedLabel" class="input" type="text" placeholder="恢复民心">
</div>
</div>
<div class="row row-compact">
<button id="btnSaveSchedule" class="btn btn-sm btn-primary">保存</button>
<button id="btnCancelSchedule" class="btn btn-sm">取消</button>
</div>
<p class="hint">只会在机器人已连接且通过登录验证时发送;未就绪的周期会跳过,不会补发。</p>
</div>
</div>
<div class="card">
<h2>领土数据源</h2>
<dl class="kv">
@@ -101,7 +147,7 @@
<dt>己方飞地</dt><dd id="kFlying">0</dd>
<dt>SimpCore API</dt><dd id="kApi">未启用</dd>
</dl>
<div class="row">
<div class="row row-compact">
<button id="btnRefreshMap" class="btn btn-sm">刷新地图</button>
<button id="btnFetchAll" class="btn btn-sm">拉取全服领土</button>
</div>
@@ -115,6 +161,7 @@
<dt>朝向已同步</dt><dd id="kGrimSettled">-</dd>
<dt>攻击距离</dt><dd id="kGrimReach">-</dd>
<dt>攻击冷却</dt><dd id="kGrimCooldown">-</dd>
<dt>协议补包</dt><dd id="kGrimPackets">-</dd>
<dt>已拦下动作</dt><dd id="kGrimBlocked">0</dd>
<dt>物理自检</dt><dd id="kGrimAudit">-</dd>
</dl>
@@ -123,26 +170,36 @@
</section>
<!-- ==================== 中栏:地图与日志 ==================== -->
<section class="column col-center">
<section class="column col-center" id="panelMap">
<div class="card card-map">
<div class="card-head">
<div class="card-head card-head-wrap">
<div class="tabs tabs-inline" id="mapTabs">
<button class="tab active" data-view="big">大地图</button>
<button class="tab" data-view="territory">领土地图</button>
</div>
<div class="row" id="bigMapControls">
<div class="row row-compact" id="bigMapControls">
<label class="field-inline">
<span class="field-label">图幅</span>
<select id="bigMapRange" class="input input-sm">
<option value="512">±512 格</option>
<option value="1024">±1024 格</option>
<option value="2048">±2048 格</option>
<option value="4096" selected>±4096 格</option>
<option value="8192">±8192 格</option>
<option value="20480">全世界</option>
</select>
</label>
<label class="chk"><input type="checkbox" id="chkTerritory" checked> 领土标记</label>
<label class="chk"><input type="checkbox" id="chkBotLabels" checked> 机器人名字</label>
<button id="btnMapFitWorld" class="btn btn-sm">全世界</button>
<label class="chk"><input type="checkbox" id="chkBotLabels" checked> 名字</label>
<button id="btnCrawlAround" class="btn btn-sm btn-primary">爬取周边</button>
<button id="btnMapCenterBots" class="btn btn-sm">回到机器人</button>
<button id="btnCrawlMap" class="btn btn-sm">爬取地图</button>
</div>
<div class="row hidden" id="territoryMapControls">
<label class="chk"><input type="checkbox" id="chkFull" checked> 显示全部邦国</label>
<label class="chk"><input type="checkbox" id="chkFlying" checked> 标出飞地</label>
<div class="row row-compact hidden" id="territoryMapControls">
<label class="chk"><input type="checkbox" id="chkFull" checked> 全部邦国</label>
<label class="chk"><input type="checkbox" id="chkFlying" checked> 飞地</label>
<label class="chk"><input type="checkbox" id="chkGrid" checked> 网格</label>
<button id="btnMapCenter" class="btn btn-sm">回到玩家</button>
<button id="btnMapFit" class="btn btn-sm">全图</button>
<button id="btnMapCenter" class="btn btn-sm">回到机器人</button>
<button id="btnMapFit" class="btn btn-sm">看己方全境</button>
</div>
</div>
<canvas id="mapCanvas"></canvas>
@@ -165,26 +222,27 @@
</div>
</div>
<div class="card card-log">
<div class="card-head">
<div class="card card-log" id="panelLogs">
<div class="card-head card-head-wrap">
<div class="tabs tabs-inline" id="logTabs">
<button class="tab active" data-pane="log">运行日志</button>
<button class="tab" data-pane="chat">公屏聊天</button>
<button class="tab" data-pane="exp">开拓日志</button>
</div>
<div class="row" id="logControls">
<div class="row row-compact" id="logControls">
<label class="chk"><input type="checkbox" id="chkAutoScroll" checked> 自动滚动</label>
<button id="btnClearLog" class="btn btn-sm">清屏</button>
<button id="btnExportLog" class="btn btn-sm">导出日志</button>
<button id="btnExportLog" class="btn btn-sm">导出</button>
</div>
<div class="row hidden" id="chatControls">
<div class="row row-compact hidden" id="chatControls">
<label class="chk"><input type="checkbox" id="chkChatAutoScroll" checked> 自动滚动</label>
<label class="chk"><input type="checkbox" id="chkChatAllBots"> 全部机器人</label>
<button id="btnClearChat" class="btn btn-sm">清屏</button>
<button id="btnExportChat" class="btn btn-sm">导出公屏日志</button>
<button id="btnExportChat" class="btn btn-sm">导出</button>
</div>
<div class="row hidden" id="expControls">
<label class="chk">机器人
<div class="row row-compact hidden" id="expControls">
<label class="field-inline">
<span class="field-label">机器人</span>
<select id="expFilterBot" class="input input-sm"><option value="">全部</option></select>
</label>
<label class="chk"><input type="checkbox" id="chkExpAutoScroll" checked> 自动滚动</label>
@@ -194,7 +252,7 @@
</div>
<div id="logView" class="log-view"></div>
<div id="chatView" class="log-view hidden"></div>
<div id="expView" class="log-view hidden">
<div id="expView" class="log-view log-table hidden">
<table class="exp-table">
<thead>
<tr><th>时间</th><th>机器人</th><th>邦国</th><th>区块</th><th>方块坐标</th><th>来源</th></tr>
@@ -206,12 +264,46 @@
</section>
<!-- ==================== 右栏:配置与控制 ==================== -->
<section class="column col-right">
<section class="column col-right" id="panelConfig">
<div class="card">
<div class="card-head">
<h2>控制台 / 聊天框</h2>
<span class="mono small" id="consoleTarget">-</span>
</div>
<p class="hint" id="consoleHint">登录验证通过后可向该机器人发送指令或公屏消息。</p>
<div class="row row-compact">
<select id="sendMode" class="input input-sm">
<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>
</div>
<div class="row row-compact">
<button class="btn btn-sm" data-cmd="u return">/u return</button>
<button class="btn btn-sm" data-cmd="u claimmode">/u claimmode</button>
<button class="btn btn-sm" data-cmd="spawn">/spawn</button>
<button class="btn btn-sm" data-cmd="list">/list</button>
</div>
<div class="row row-compact">
<input id="quickChatInput" class="input input-grow" type="text" placeholder="快捷公屏消息">
<button id="btnQuickChat" class="btn btn-sm">公屏发送</button>
</div>
<div class="row row-compact">
<span class="field-label">移动测试</span>
<button class="btn btn-sm" data-move="forward">前</button>
<button class="btn btn-sm" data-move="back">后</button>
<button class="btn btn-sm" data-move="left">左</button>
<button class="btn btn-sm" data-move="right">右</button>
<button class="btn btn-sm" data-move="jump">跳</button>
</div>
</div>
<div class="card card-config">
<div class="card-head">
<h2>配置</h2>
<div class="row">
<button id="btnSaveConfig" class="btn btn-sm btn-primary">保存配置</button>
<div class="row row-compact">
<button id="btnSaveConfig" class="btn btn-sm btn-primary">保存</button>
<button id="btnReloadConfig" class="btn btn-sm">重新载入</button>
</div>
</div>
@@ -231,40 +323,6 @@
</div>
<div id="configBody" class="config-body"></div>
</div>
<div class="card">
<div class="card-head">
<h2>控制台 / 聊天框</h2>
<span class="mono small" id="consoleTarget">-</span>
</div>
<p class="hint" id="consoleHint">登录验证通过后可向该机器人发送指令或公屏消息。</p>
<div class="row">
<select id="sendMode" class="input input-sm">
<option value="chat">公屏聊天</option>
<option value="command">游戏指令</option>
</select>
<input id="cmdInput" class="input" type="text" placeholder="输入内容后回车发送">
<button id="btnSendCmd" class="btn btn-sm">发送</button>
</div>
<div class="row">
<button class="btn btn-sm" data-cmd="u return">/u return</button>
<button class="btn btn-sm" data-cmd="u claimmode">/u claimmode</button>
<button class="btn btn-sm" data-cmd="spawn">/spawn</button>
<button class="btn btn-sm" data-cmd="list">/list</button>
</div>
<div class="row">
<input id="quickChatInput" class="input" type="text" placeholder="快捷公屏消息(如:大家好)">
<button id="btnQuickChat" class="btn btn-sm">公屏发送</button>
</div>
<div class="row">
<span class="hint">移动测试:</span>
<button class="btn btn-sm" data-move="forward">前</button>
<button class="btn btn-sm" data-move="back">后</button>
<button class="btn btn-sm" data-move="left">左</button>
<button class="btn btn-sm" data-move="right">右</button>
<button class="btn btn-sm" data-move="jump">跳</button>
</div>
</div>
</section>
</main>
@@ -275,8 +333,8 @@
<p class="hint">验证通过后才允许操作各机器人的控制台、聊天框与扩地任务。</p>
<input id="loginPassword" class="input" type="password" placeholder="控制台密码 / 令牌" autocomplete="current-password">
<div class="row">
<button id="btnLogin" class="btn btn-primary">登录</button>
<button id="btnLogout" class="btn btn-sm">退出登录</button>
<button id="btnLogin" class="btn btn-primary btn-grow">登录</button>
<button id="btnLogout" class="btn">退出登录</button>
</div>
<p id="loginError" class="hint error-text"></p>
</div>