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 语法自检通过。
这个提交包含在:
+120
@@ -0,0 +1,120 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
/** 环形日志缓冲区最大条数(与原 Java 版一致) */
|
||||
const MAX_LOGS = 2000;
|
||||
|
||||
/**
|
||||
* 运行日志:内存环形缓冲 + 控制台输出 + 可导出到文件。
|
||||
*
|
||||
* 日志格式与原 Java 版保持一致(`[HH:mm:ss] 消息`),
|
||||
* 便于对照排查;同时支持订阅,供 Web 控制台实时推送。
|
||||
*/
|
||||
class Logger {
|
||||
/**
|
||||
* @param {{maxLogs?: number, echo?: boolean, onLog?: (entry: {time: string, message: string, level: string}) => void}} [options]
|
||||
*/
|
||||
constructor(options) {
|
||||
const opts = options || {};
|
||||
this.maxLogs = opts.maxLogs || MAX_LOGS;
|
||||
this.echo = opts.echo !== false;
|
||||
this.onLog = opts.onLog || null;
|
||||
/** @type {Array<{time: string, message: string, level: string}>} */
|
||||
this.entries = [];
|
||||
/** @type {((entry: {time: string, message: string, level: string}) => void)|null} */
|
||||
this.onEntry = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 追加一条日志。
|
||||
* @param {string} message 日志正文
|
||||
* @param {'info'|'warn'|'error'|'success'} [level] 级别(仅用于 Web 控制台着色)
|
||||
*/
|
||||
log(message, level) {
|
||||
const entry = { time: formatTime(new Date()), message: String(message), level: level || 'info' };
|
||||
this.entries.push(entry);
|
||||
while (this.entries.length > this.maxLogs) this.entries.shift();
|
||||
if (this.echo) {
|
||||
const prefix = `[${entry.time}]`;
|
||||
if (entry.level === 'error') console.error(prefix, entry.message);
|
||||
else if (entry.level === 'warn') console.warn(prefix, entry.message);
|
||||
else console.log(prefix, entry.message);
|
||||
}
|
||||
if (this.onEntry) {
|
||||
try {
|
||||
this.onEntry(entry);
|
||||
} catch {
|
||||
// 订阅者异常不得影响主流程
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {string} message */
|
||||
info(message) {
|
||||
this.log(message, 'info');
|
||||
}
|
||||
|
||||
/** @param {string} message */
|
||||
warn(message) {
|
||||
this.log(message, 'warn');
|
||||
}
|
||||
|
||||
/** @param {string} message */
|
||||
error(message) {
|
||||
this.log(message, 'error');
|
||||
}
|
||||
|
||||
/** @param {string} message */
|
||||
success(message) {
|
||||
this.log(message, 'success');
|
||||
}
|
||||
|
||||
/** @returns {Array<{time: string, message: string, level: string}>} 日志副本 */
|
||||
list() {
|
||||
return this.entries.slice();
|
||||
}
|
||||
|
||||
/** 清空日志缓冲 */
|
||||
clear() {
|
||||
this.entries.length = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出运行日志到文件,返回文件路径;失败时返回 null。
|
||||
*
|
||||
* @param {string} filePath 目标路径
|
||||
* @param {object} [summary] 状态摘要(键值对,逐行输出)
|
||||
* @returns {string|null} 导出文件路径
|
||||
*/
|
||||
exportTo(filePath, summary) {
|
||||
try {
|
||||
const lines = ['==== AutoStore Bot 调试日志 ===='];
|
||||
if (summary) {
|
||||
for (const [key, value] of Object.entries(summary)) {
|
||||
lines.push(`${key}: ${value}`);
|
||||
}
|
||||
}
|
||||
lines.push('');
|
||||
for (const entry of this.entries) lines.push(`[${entry.time}] ${entry.message}`);
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
fs.writeFileSync(filePath, lines.join('\n'), 'utf8');
|
||||
return filePath;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化为 HH:mm:ss。
|
||||
* @param {Date} date
|
||||
* @returns {string}
|
||||
*/
|
||||
function formatTime(date) {
|
||||
const pad = (n) => String(n).padStart(2, '0');
|
||||
return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
||||
}
|
||||
|
||||
module.exports = { Logger, formatTime, MAX_LOGS };
|
||||
在新工单中引用
屏蔽一个用户