feat: 用 Node.js + mineflayer 重构 AutoStore,UI 改为网页控制台并适配 Grim 反作弊

由 Fabric 客户端模组 AutoStore v1.3.1 重构为独立 Node.js 机器人,无需游戏客户端。

核心实现:
- 领土数据:Dynmap markers.json 拉取解析(奇偶填充识别国中飞地)、SimpCore 开放 API 第四数据源、
  带缓存击穿的时间戳请求与内容未变化跳过解析
- 扩地规划:四种策略(方形外扩 / 区域 S 形扫描 / 玩家周围 / 绕国一圈)逐行对照移植,
  接壤判定只算己方领土,飞地优先填补
- 扩地状态机:规划→寻路→等待开垦→成功/失败→下一目标→完成,含超时重试、
  往边上走两步自救、连续超时回城、耐久保护回城
- 生存辅助:自动进食、自动攻击(豁免中立生物)、自动游泳防溺水、自动用船(默认关闭)
- 寻路:Baritone → mineflayer-pathfinder,高度偏好用包装 Goal 等价移植

Grim 反作弊合规层(新增,核心差异点):
- 旋转不超前:接管 bot.look 按 ≤8.6°/tick 推进朝向,消除 mineflayer
  「本地朝向瞬间跳变、数据包滞后补发」导致的 Prediction 分歧
- 攻击三重校验:朝向已同步 + 距离 ≤3.0 格 + 视线可达 + 攻击冷却 + 非骑乘/进食
- 动作互斥:拦下疾跑+潜行、疾跑+使用物品等原版不可能出现的组合
- 物理参数只读自检 + 寻路约束(禁挖/禁放/禁跑酷/禁搭塔/限下落/绕开水域)

网页控制台(新增):
- 状态面板、Canvas 领土地图(缩放/平移/图层)、分组配置表单、实时日志流
- REST + WebSocket 接口,支持 WEB_TOKEN 令牌鉴权

修复:
- containsFuzzy 只取固定长度前缀的末行编辑距离,导致完全相同的子串也匹配不上
- 占领去重依赖 Set.add 返回值(JS 中恒为真),导致同一区块被重复计数
- 非法枚举值不再钳制到最近边界,改为回退默认值

测试:154 个用例,npm test 全绿;npm run check 语法自检通过。
这个提交包含在:
lyizai
2026-09-27 17:17:41 +08:00
当前提交 a12a24c716
共修改 44 个文件,包含 12225 行新增和 0 行删除
+172
查看文件
@@ -0,0 +1,172 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AutoStore Bot 控制台</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header class="topbar">
<div class="brand">
<span class="logo">AS</span>
<div>
<h1>AutoStore Bot</h1>
<p>Minecraft 自动扩地机器人 · mineflayer · Grim 合规</p>
</div>
</div>
<div class="topbar-right">
<span id="connBadge" class="badge badge-offline">未连接</span>
<span id="taskBadge" class="badge badge-idle">空闲</span>
<button id="btnStart" class="btn btn-primary">开始扩地</button>
<button id="btnStop" class="btn btn-danger">停止</button>
<button id="btnReconnect" class="btn">重连</button>
</div>
</header>
<main class="layout">
<!-- ==================== 左栏:状态面板 ==================== -->
<section class="column col-left">
<div class="card">
<h2>连接</h2>
<dl class="kv">
<dt>服务器</dt><dd id="kServer">-</dd>
<dt>玩家名</dt><dd id="kUsername">-</dd>
<dt>版本</dt><dd id="kVersion">-</dd>
<dt>坐标</dt><dd id="kPos">-</dd>
<dt>所在区块</dt><dd id="kChunk">-</dd>
<dt>生命 / 饥饿</dt><dd id="kHealth">-</dd>
<dt>状态</dt><dd id="kBotState">-</dd>
<dt>重连次数</dt><dd id="kReconnect">0</dd>
</dl>
</div>
<div class="card">
<h2>任务</h2>
<dl class="kv">
<dt>状态机</dt><dd id="kTaskState">IDLE</dd>
<dt>描述</dt><dd id="kTaskStatus">未启动</dd>
<dt>当前目标</dt><dd id="kTarget">-</dd>
<dt>队列</dt><dd id="kQueue">0</dd>
<dt>已完成</dt><dd id="kCompleted">0</dd>
<dt>重试</dt><dd id="kRetry">0</dd>
<dt>开垦进度</dt><dd id="kProgress">0%</dd>
<dt>连续寻路超时</dt><dd id="kTimeout">0s</dd>
<dt>开垦时段</dt><dd id="kClaimTime">-</dd>
</dl>
<div class="progress"><div id="progressBar" class="progress-fill"></div></div>
</div>
<div class="card">
<h2>领土数据源</h2>
<dl class="kv">
<dt>markers.json</dt><dd id="kMarkersUrl" class="mono small">-</dd>
<dt>最近成功</dt><dd id="kLastFetch">-</dd>
<dt>错误</dt><dd id="kDataError">-</dd>
<dt>邦国数</dt><dd id="kStateCount">0</dd>
<dt>已占领区块</dt><dd id="kClaimed">0</dd>
<dt>会话占领</dt><dd id="kSession">0</dd>
<dt>己方飞地</dt><dd id="kFlying">0</dd>
<dt>SimpCore API</dt><dd id="kApi">未启用</dd>
</dl>
<div class="row">
<button id="btnRefreshMap" class="btn btn-sm">刷新地图</button>
<button id="btnFetchAll" class="btn btn-sm">拉取全服领土</button>
</div>
</div>
<div class="card">
<h2>Grim 合规</h2>
<dl class="kv">
<dt>合规层</dt><dd id="kGrimEnabled">-</dd>
<dt>旋转限速</dt><dd id="kGrimRot">-</dd>
<dt>朝向已同步</dt><dd id="kGrimSettled">-</dd>
<dt>攻击距离</dt><dd id="kGrimReach">-</dd>
<dt>攻击冷却</dt><dd id="kGrimCooldown">-</dd>
<dt>已拦下动作</dt><dd id="kGrimBlocked">0</dd>
<dt>物理自检</dt><dd id="kGrimAudit">-</dd>
</dl>
<ul id="grimIssues" class="issues"></ul>
</div>
</section>
<!-- ==================== 中栏:地图 ==================== -->
<section class="column col-center">
<div class="card card-map">
<div class="card-head">
<h2>领土地图</h2>
<div class="row">
<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>
</div>
</div>
<canvas id="mapCanvas"></canvas>
<div class="map-legend">
<span><i class="sw sw-self"></i>己方领土</span>
<span><i class="sw sw-session"></i>本次占领</span>
<span><i class="sw sw-flying"></i>飞地(疏漏)</span>
<span><i class="sw sw-target"></i>当前目标</span>
<span><i class="sw sw-player"></i>机器人</span>
<span><i class="sw sw-capital"></i>王城</span>
<span id="mapInfo" class="mono small"></span>
</div>
</div>
<div class="card card-log">
<div class="card-head">
<h2>运行日志</h2>
<div class="row">
<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>
</div>
</div>
<div id="logView" class="log-view"></div>
</div>
</section>
<!-- ==================== 右栏:配置 ==================== -->
<section class="column col-right">
<div class="card card-config">
<div class="card-head">
<h2>配置</h2>
<div class="row">
<button id="btnSaveConfig" class="btn btn-sm btn-primary">保存配置</button>
<button id="btnReloadConfig" class="btn btn-sm">重新载入</button>
</div>
</div>
<div class="tabs" id="configTabs">
<button class="tab active" data-tab="strategy">策略</button>
<button class="tab" data-tab="timing">超时</button>
<button class="tab" data-tab="pathing">寻路</button>
<button class="tab" data-tab="survival">生存</button>
<button class="tab" data-tab="data">数据</button>
<button class="tab" data-tab="grim">Grim</button>
<button class="tab" data-tab="server">服务器</button>
</div>
<div id="configBody" class="config-body"></div>
</div>
<div class="card">
<h2>手动指令</h2>
<div class="row">
<input id="cmdInput" class="input" type="text" placeholder="u claimmode(不带斜杠)">
<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>
</section>
</main>
<div id="toast" class="toast"></div>
<script src="app.js"></script>
</body>
</html>