文件
BotStore/src/core/targetLease.js
T
lyizai 51664c55b6 feat: 1.7.0 落地 TODO 全部待办(地面修正 / 手动执行器 / 自救 / 回城 / 租约 / 通知)
按 TODO.md 的优先级把待办全部落地为代码,详见 CHANGELOG 的 [1.7.0]。

- 「走不动」攻关:地面状态修正(syncGroundToPhysics / trustSetbackOnGround)、
  出站包类型分布、备选执行器 manualMover(只借 A* 规划、自己按键执行)
- 原点自救分两轮并统计成败;回城前评估「原地是否更差」并记录前后坐标
- 新增目标区块租约表(targetLease)避免多机器人撞同一块
- 定时消息支持 cron 与「期望回复」确认;新增人工介入 Webhook 通知(notifier)
- 大地图后台预热与视野裁剪;控制台日志按帧批量写入、配置面板支持搜索

顺带修掉两个影响部署的细节:
- 部署脚本(deploy.js / ssh-helper.js)改为也能从 .env 读 SSH 凭据 ——
  在命令行传密码会留在 shell 历史与进程列表里,写进已被忽略的 .env 更安全;
  显式环境变量仍然优先。.env.example 补上占位符并说明这些只在本机用。
- package.json / package-lock.json 版本对齐到 1.7.0(此前停留在 1.1.0,
  与 CHANGELOG 的语义化版本声明脱节)。

测试:368 → 436 个用例,`npm test` 与 `npm run check` 全绿。
2026-09-28 22:46:39 +08:00

160 行
5.1 KiB
JavaScript
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
'use strict';
const chunkPos = require('./chunkPos');
/**
* 目标区块租约表(多机器人协调用)。
*
* 背景(TODO.md 4.3):领土模型是全局共享的,但「谁要去占哪一块」只存在各机器人
* 自己的队列里,彼此看不见 —— 多个机器人同时扩地时很容易撞在同一批候选区块上:
* 两个机器人同时跑到同一区块、只有一个能开垦成功,另一半时间全白花。
*
* 这里给「目标区块」加一张带租约的占用表:
* - **抢占**:`tryAcquire` 是「检查 + 写入」的原子操作,谁先抢到谁去;
* - **租约**:占用有有效期(默认 3 分钟),持有者持续续租;机器人掉线 / 卡死时
* 租约到期自动失效,区块不会被永久锁死;
* - **释放**:开垦成功 / 放弃目标 / 停止任务时主动释放,其他机器人立刻可用。
*
* 同一 Node 进程内管理所有机器人,因此不需要真正的分布式锁,
* 一张内存表就足以覆盖「同进程多机器人」这个实际场景。
*/
/** 默认租约时长(毫秒) */
const DEFAULT_TTL_MS = 180000;
class TargetLeaseTable {
/**
* @param {{ttlMs?: number}} [options]
*/
constructor(options) {
const opts = options || {};
this.ttlMs = Number.isFinite(opts.ttlMs) && opts.ttlMs > 0 ? opts.ttlMs : DEFAULT_TTL_MS;
/** @type {Map<string, {botId: string, expiresAt: number, acquiredAt: number}>} */
this._leases = new Map();
}
/**
* 尝试占用一个区块(同一个机器人可重复占用 / 续租)。
*
* @param {{x: number, z: number}} chunk
* @param {string} botId
* @param {number} [ttlMs] 本次租约时长,缺省取表上的 ttlMs
* @returns {boolean} 是否占用成功(已被其他机器人占用时返回 false)
*/
tryAcquire(chunk, botId, ttlMs) {
if (!chunk) return false;
const key = chunkPos.key(chunk.x, chunk.z);
const now = Date.now();
const existing = this._leases.get(key);
if (existing && existing.expiresAt > now && existing.botId !== botId) return false;
const ttl = Number.isFinite(ttlMs) && ttlMs > 0 ? ttlMs : this.ttlMs;
this._leases.set(key, {
botId,
expiresAt: now + ttl,
acquiredAt: existing && existing.botId === botId ? existing.acquiredAt : now
});
return true;
}
/**
* 续租(只有当前持有者能续)。
* @param {{x: number, z: number}} chunk
* @param {string} botId
* @param {number} [ttlMs]
* @returns {boolean} 是否续租成功
*/
renew(chunk, botId, ttlMs) {
if (!chunk) return false;
const key = chunkPos.key(chunk.x, chunk.z);
const existing = this._leases.get(key);
if (!existing || existing.botId !== botId) return false;
const ttl = Number.isFinite(ttlMs) && ttlMs > 0 ? ttlMs : this.ttlMs;
existing.expiresAt = Date.now() + ttl;
return true;
}
/**
* 释放占用(只有当前持有者能释放;已过期的占用直接清掉)。
* @param {{x: number, z: number}} chunk
* @param {string} botId
* @returns {boolean} 是否释放了一个占用
*/
release(chunk, botId) {
if (!chunk) return false;
const key = chunkPos.key(chunk.x, chunk.z);
const existing = this._leases.get(key);
if (!existing) return false;
if (existing.botId !== botId) return false;
this._leases.delete(key);
return true;
}
/**
* 释放某个机器人的全部占用(停止任务 / 断线 / 退出时调用)。
* @param {string} botId
* @returns {number} 释放的数量
*/
releaseAll(botId) {
let released = 0;
for (const [key, lease] of this._leases) {
if (lease.botId === botId) {
this._leases.delete(key);
released++;
}
}
return released;
}
/**
* 区块是否被**其他**机器人占用(已过期的占用视为空闲,顺手清理)。
* @param {{x: number, z: number}} chunk
* @param {string} botId 自己
* @returns {boolean}
*/
isLeasedByOther(chunk, botId) {
if (!chunk) return false;
const key = chunkPos.key(chunk.x, chunk.z);
const lease = this._leases.get(key);
if (!lease) return false;
if (lease.expiresAt <= Date.now()) {
this._leases.delete(key);
return false;
}
return lease.botId !== botId;
}
/**
* 清理过期租约。
* @returns {number} 清理掉的数量
*/
sweep() {
const now = Date.now();
let removed = 0;
for (const [key, lease] of this._leases) {
if (lease.expiresAt <= now) {
this._leases.delete(key);
removed++;
}
}
return removed;
}
/** @returns {number} 当前有效占用数量 */
get size() {
this.sweep();
return this._leases.size;
}
/** @returns {Array<{chunk: string, botId: string, remainingMs: number}>} 供控制台展示 */
snapshot() {
const now = Date.now();
const rows = [];
for (const [key, lease] of this._leases) {
if (lease.expiresAt <= now) continue;
rows.push({ chunk: key, botId: lease.botId, remainingMs: lease.expiresAt - now });
}
return rows;
}
}
module.exports = { TargetLeaseTable, DEFAULT_TTL_MS };