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 语法自检通过。
这个提交包含在:
@@ -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; }
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户