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 行删除
+239
查看文件
@@ -0,0 +1,239 @@
:root {
--bg: #0d1117;
--panel: #161b22;
--panel-2: #1c2330;
--border: #30363d;
--text: #c9d1d9;
--muted: #8b949e;
--accent: #e94560;
--accent-2: #2f81f7;
--green: #3fb950;
--yellow: #d29922;
--red: #f85149;
--purple: #bc8cff;
}
* { box-sizing: border-box; }
html, body {
margin: 0;
padding: 0;
height: 100%;
background: var(--bg);
color: var(--text);
font-family: "Segoe UI", "Microsoft YaHei", system-ui, -apple-system, sans-serif;
font-size: 14px;
}
.mono { font-family: Consolas, "Cascadia Mono", monospace; }
.small { font-size: 12px; }
/* ==================== 顶栏 ==================== */
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 18px;
background: linear-gradient(90deg, #161b22, #1b2230);
border-bottom: 1px solid var(--border);
position: sticky;
top: 0;
z-index: 10;
}
.brand { display: flex; align-items: center; gap: 12px; }
.logo {
width: 38px; height: 38px;
display: grid; place-items: center;
border-radius: 9px;
background: var(--accent);
color: #fff; font-weight: 700; letter-spacing: 1px;
}
.brand h1 { font-size: 16px; margin: 0; }
.brand p { font-size: 12px; margin: 2px 0 0; color: var(--muted); }
.topbar-right { display: flex; align-items: center; gap: 8px; }
.badge {
padding: 4px 10px;
border-radius: 999px;
font-size: 12px;
border: 1px solid var(--border);
background: var(--panel-2);
}
.badge-online { color: var(--green); border-color: #1f6f34; }
.badge-offline { color: var(--muted); }
.badge-running { color: var(--accent-2); border-color: #1f4f8f; }
.badge-idle { color: var(--muted); }
.badge-error { color: var(--red); border-color: #8f2b26; }
.btn {
padding: 6px 12px;
border-radius: 6px;
border: 1px solid var(--border);
background: var(--panel-2);
color: var(--text);
cursor: pointer;
font-size: 13px;
transition: background .15s, border-color .15s;
}
.btn:hover { background: #232b39; border-color: #48505e; }
.btn-primary { background: var(--accent); border-color: var(--accent); color: #fff; }
.btn-primary:hover { background: #d63851; border-color: #d63851; }
.btn-danger { background: #3a1d22; border-color: #7a2b34; color: #ff8b98; }
.btn-danger:hover { background: #4a2329; }
.btn-sm { padding: 4px 9px; font-size: 12px; }
/* ==================== 布局 ==================== */
.layout {
display: grid;
grid-template-columns: 300px minmax(420px, 1fr) 360px;
gap: 12px;
padding: 12px;
align-items: start;
}
.column { display: flex; flex-direction: column; gap: 12px; min-width: 0; }
.card {
background: var(--panel);
border: 1px solid var(--border);
border-radius: 10px;
padding: 12px;
}
.card h2 {
font-size: 13px;
margin: 0 0 8px;
color: var(--muted);
text-transform: uppercase;
letter-spacing: .6px;
}
.card-head {
display: flex; align-items: center; justify-content: space-between;
gap: 8px; margin-bottom: 8px;
}
.card-head h2 { margin: 0; }
.row { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; margin-top: 8px; }
/* ==================== 键值表 ==================== */
.kv {
display: grid;
grid-template-columns: 88px 1fr;
gap: 4px 8px;
margin: 0;
font-size: 13px;
}
.kv dt { color: var(--muted); }
.kv dd { margin: 0; word-break: break-all; }
.progress {
margin-top: 10px; height: 6px; border-radius: 3px;
background: #0b0f14; overflow: hidden;
}
.progress-fill {
height: 100%; width: 0%;
background: linear-gradient(90deg, var(--accent-2), var(--green));
transition: width .25s;
}
.issues { margin: 8px 0 0; padding-left: 18px; color: var(--yellow); font-size: 12px; }
.issues li { margin-bottom: 3px; }
/* ==================== 地图 ==================== */
.card-map { display: flex; flex-direction: column; }
#mapCanvas {
width: 100%;
height: 380px;
background: #05070a;
border: 1px solid var(--border);
border-radius: 8px;
display: block;
cursor: grab;
}
#mapCanvas:active { cursor: grabbing; }
.map-legend {
display: flex; gap: 14px; flex-wrap: wrap;
margin-top: 8px; font-size: 12px; color: var(--muted);
align-items: center;
}
.sw {
display: inline-block; width: 10px; height: 10px;
border-radius: 2px; margin-right: 4px; vertical-align: -1px;
}
.sw-self { background: #2f81f7; }
.sw-session { background: #3fb950; }
.sw-flying { background: #f85149; }
.sw-target { background: #d29922; }
.sw-player { background: #ffffff; }
.sw-capital { background: #bc8cff; }
.chk { display: flex; align-items: center; gap: 4px; font-size: 12px; color: var(--muted); cursor: pointer; }
/* ==================== 日志 ==================== */
.card-log { display: flex; flex-direction: column; }
.log-view {
height: 260px;
overflow-y: auto;
background: #05070a;
border: 1px solid var(--border);
border-radius: 8px;
padding: 8px;
font-family: Consolas, "Cascadia Mono", monospace;
font-size: 12px;
line-height: 1.55;
white-space: pre-wrap;
word-break: break-all;
}
.log-line { display: block; }
.log-time { color: #586069; margin-right: 6px; }
.log-info { color: var(--text); }
.log-warn { color: var(--yellow); }
.log-error { color: var(--red); }
.log-success { color: var(--green); }
/* ==================== 配置 ==================== */
.card-config { display: flex; flex-direction: column; }
.tabs { display: flex; gap: 4px; flex-wrap: wrap; margin-bottom: 8px; }
.tab {
padding: 4px 10px; font-size: 12px; cursor: pointer;
background: var(--panel-2); border: 1px solid var(--border);
border-radius: 6px; color: var(--muted);
}
.tab.active { color: #fff; background: var(--accent-2); border-color: var(--accent-2); }
.config-body { max-height: 520px; overflow-y: auto; }
.field { margin-bottom: 9px; }
.field label { display: block; font-size: 12px; color: var(--muted); margin-bottom: 3px; }
.field .hint { font-size: 11px; color: #5d6672; margin-top: 2px; }
.input, select.input {
width: 100%; padding: 5px 8px;
background: #0b0f14; border: 1px solid var(--border);
border-radius: 6px; color: var(--text); font-size: 13px;
outline: none;
}
.input:focus { border-color: var(--accent-2); }
.field-inline { display: flex; align-items: center; gap: 8px; }
.field-inline input[type=checkbox] { width: 15px; height: 15px; }
.group-title {
font-size: 12px; color: var(--accent-2);
margin: 12px 0 6px; padding-bottom: 4px;
border-bottom: 1px dashed var(--border);
}
.group-title:first-child { margin-top: 0; }
/* ==================== 提示 ==================== */
.toast {
position: fixed; right: 18px; bottom: 18px;
background: var(--panel-2); border: 1px solid var(--border);
border-left: 3px solid var(--accent-2);
padding: 10px 14px; border-radius: 8px;
font-size: 13px; max-width: 380px;
opacity: 0; transform: translateY(8px);
transition: opacity .2s, transform .2s;
pointer-events: none;
z-index: 100;
}
.toast.show { opacity: 1; transform: translateY(0); }
.toast.error { border-left-color: var(--red); }
@media (max-width: 1280px) {
.layout { grid-template-columns: 1fr; }
.config-body { max-height: none; }
}