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 行删除
+69
查看文件
@@ -0,0 +1,69 @@
'use strict';
const test = require('node:test');
const assert = require('node:assert');
const chunkPos = require('../src/core/chunkPos');
test('fromBlock 向下取整且兼容负数坐标', () => {
assert.deepStrictEqual(chunkPos.fromBlock(0, 0), { x: 0, z: 0 });
assert.deepStrictEqual(chunkPos.fromBlock(15, 15), { x: 0, z: 0 });
assert.deepStrictEqual(chunkPos.fromBlock(16, 31), { x: 1, z: 1 });
// 负数必须向下取整到更小的区块号,而不是向零取整
assert.deepStrictEqual(chunkPos.fromBlock(-1, -16), { x: -1, z: -1 });
assert.deepStrictEqual(chunkPos.fromBlock(-17, -33), { x: -2, z: -3 });
});
test('key/decode 往返一致(含负数与边界)', () => {
const samples = [
[0, 0], [1, 0], [0, 1], [-1, 0], [0, -1], [-1, -1],
[1279, 1279], [-1280, -1280], [12345, -54321], [-2048, 2048]
];
for (const [x, z] of samples) {
const decoded = chunkPos.decode(chunkPos.key(x, z));
assert.deepStrictEqual(decoded, { x, z }, `坐标 ${x},${z} 编解码不一致`);
}
});
test('key 对不同坐标产生不同键', () => {
const seen = new Set();
for (let x = -50; x <= 50; x++) {
for (let z = -50; z <= 50; z++) {
const k = chunkPos.key(x, z);
assert.ok(!seen.has(k), `键冲突:${x},${z}`);
seen.add(k);
}
}
assert.strictEqual(seen.size, 101 * 101);
});
test('centerBlock 取区块中心(+8)', () => {
assert.strictEqual(chunkPos.centerBlockX(0), 8);
assert.strictEqual(chunkPos.centerBlockX(-1), -8);
assert.strictEqual(chunkPos.centerBlockZ(3), 56);
});
test('曼哈顿与切比雪夫距离', () => {
assert.strictEqual(chunkPos.manhattan({ x: 0, z: 0 }, { x: 3, z: -4 }), 7);
assert.strictEqual(chunkPos.chebyshev({ x: 0, z: 0 }, { x: 3, z: -4 }), 4);
assert.strictEqual(chunkPos.chebyshev({ x: 5, z: 5 }, { x: 5, z: 5 }), 0);
});
test('equals 处理 null 与同值', () => {
assert.strictEqual(chunkPos.equals({ x: 1, z: 2 }, { x: 1, z: 2 }), true);
assert.strictEqual(chunkPos.equals({ x: 1, z: 2 }, { x: 2, z: 1 }), false);
assert.strictEqual(chunkPos.equals(null, null), true);
assert.strictEqual(chunkPos.equals({ x: 1, z: 2 }, null), false);
});
test('cardinalNeighbors 返回四个正方向邻居', () => {
const neighbors = chunkPos.cardinalNeighbors(0, 0).map((c) => `${c.x},${c.z}`);
assert.deepStrictEqual(neighbors.sort(), ['-1,0', '0,-1', '0,1', '1,0']);
});
test('withinWorldBorder 以 ±1280 为界', () => {
assert.strictEqual(chunkPos.withinWorldBorder(0, 0), true);
assert.strictEqual(chunkPos.withinWorldBorder(1279, -1280), true);
assert.strictEqual(chunkPos.withinWorldBorder(1280, 0), false);
assert.strictEqual(chunkPos.withinWorldBorder(0, -1281), false);
});