feat: 新增出站包抓取,供「走不动」逐包对照(TODO 假设 5)
线上实测确认手动执行器(1.7.0 备选方案)不能解决「走不动」:前 20 秒走 13 格后 即卡在「执行中断(stuck)→重规划→缩短段长→跳过目标→重新规划同一目标」的死循环。 只靠 packetStats.outboundTypes 的计数看不出「发出去的内容对不对」,所以补一个 能录原始出站序列、并能在「能走」与「走不动」两种状态下 diff 的诊断能力。 新增 src/core/packetCapture.js: - 只在录制期间挂钩 client.write,其余时间零开销(该钩子每个出站包都要过一遍); - 录到的内容做降深 + 截断的 JSON 化处理,向量对象只留坐标 (move_minecart 这类包能带上万个浮点数,不处理会把缓冲区撑爆); - 只放内存、有上限(默认 4000 包),不写盘。 接口(控制台): - POST /api/bots/:id/capture/start(可带 seconds 到点自动停止) - POST /api/bots/:id/capture/stop - GET /api/bots/:id/capture(支持 ?types= 与 ?limit=) 顺带修掉两个先前就存在的问题: - scripts/check-protocol.js 其实一直跑不通:minecraft-data 的 types.packet[1][1].type[1].fields[包名] 给的是类型名字符串, 要再去 types[名字] 取 ['container', [字段...]] 才能展开,原实现直接 .map() 必然抛错; 另有读 node_modules 少一层 '..' 的路径问题。 - 两条租约用例(expansionController / targetLease)的时序余量只有 15ms, 全量并行跑测试时偶发失败;改为 TTL 400ms 配 280ms 睡眠,各留 120ms 余量, 断言内容不变。 测试:436 → 451 个用例,连续 16 轮全量运行全绿。
这个提交包含在:
+54
-2
@@ -5,6 +5,7 @@ const assert = require('node:assert');
|
||||
|
||||
const { flattenChunks, boundsOf, timingSafeEqual, WebConsole } = require('../src/web/server');
|
||||
const { TerritoryModel } = require('../src/core/territoryModel');
|
||||
const { PacketCapture } = require('../src/core/packetCapture');
|
||||
const { defaultConfig } = require('../src/config/expansionConfig');
|
||||
const { Logger } = require('../src/util/logger');
|
||||
const chunkPos = require('../src/core/chunkPos');
|
||||
@@ -49,9 +50,21 @@ function setupConsole(options) {
|
||||
model.addSessionClaim({ x: 1, z: 0 });
|
||||
|
||||
const logger = new Logger({ echo: false });
|
||||
// 抓包接口的桩:挂一个真的 PacketCapture + 假 client,接口测到的是真实逻辑
|
||||
const captureClient = {
|
||||
sent: [],
|
||||
write(name, params) {
|
||||
this.sent.push({ name, params });
|
||||
}
|
||||
};
|
||||
const packetCapture = new PacketCapture();
|
||||
packetCapture.setClient(captureClient);
|
||||
const botManager = {
|
||||
snapshot: () => ({ state: 'online', username: 'TestBot' }),
|
||||
sendCommand: () => {}
|
||||
sendCommand: () => {},
|
||||
startPacketCapture: (seconds) => packetCapture.start(seconds),
|
||||
stopPacketCapture: () => packetCapture.stop(),
|
||||
packetCaptureSnapshot: (options) => packetCapture.snapshot(options)
|
||||
};
|
||||
const controller = {
|
||||
target: { x: 2, z: 0 },
|
||||
@@ -79,7 +92,7 @@ function setupConsole(options) {
|
||||
getConfig: () => config,
|
||||
applyConfig: (next) => next
|
||||
}, { host: '127.0.0.1', port: 0 });
|
||||
return { webConsole, model, logger, controller };
|
||||
return { webConsole, model, logger, controller, captureClient, packetCapture };
|
||||
}
|
||||
|
||||
test('mapPayload 返回己方 / 会话 / 飞地 / 目标与各邦国摘要', () => {
|
||||
@@ -221,3 +234,42 @@ test('HTTP 接口:POST /api/command 缺参数时返回 400', async () => {
|
||||
await webConsole.stop();
|
||||
}
|
||||
});
|
||||
|
||||
test('HTTP 接口:抓包可开始、取结果(含按包名过滤)、停止', async () => {
|
||||
const { webConsole, captureClient } = setupConsole();
|
||||
await webConsole.start();
|
||||
const base = webConsole.baseUrl();
|
||||
try {
|
||||
const started = await fetch(base + '/api/bots/bot/capture/start', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ seconds: 5 })
|
||||
}).then((r) => r.json());
|
||||
assert.strictEqual(started.ok, true);
|
||||
assert.match(started.message, /已开始抓包/);
|
||||
|
||||
captureClient.write('position_look', { x: 1, y: 64, z: 2, yaw: 90, onGround: true });
|
||||
captureClient.write('look', { yaw: 91 });
|
||||
|
||||
const snap = await fetch(base + '/api/bots/bot/capture').then((r) => r.json());
|
||||
assert.strictEqual(snap.ok, true);
|
||||
assert.strictEqual(snap.data.running, true);
|
||||
assert.strictEqual(snap.data.count, 2);
|
||||
assert.deepStrictEqual(snap.data.byType, { position_look: 1, look: 1 });
|
||||
assert.strictEqual(snap.data.packets[0].params.onGround, true);
|
||||
|
||||
const filtered = await fetch(base + '/api/bots/bot/capture?types=look').then((r) => r.json());
|
||||
assert.strictEqual(filtered.data.count, 2, 'count 是过滤前的总数');
|
||||
assert.strictEqual(filtered.data.returned, 1);
|
||||
assert.strictEqual(filtered.data.packets[0].name, 'look');
|
||||
|
||||
const stopped = await fetch(base + '/api/bots/bot/capture/stop', { method: 'POST' }).then((r) => r.json());
|
||||
assert.strictEqual(stopped.ok, true);
|
||||
|
||||
const after = await fetch(base + '/api/bots/bot/capture').then((r) => r.json());
|
||||
assert.strictEqual(after.data.running, false);
|
||||
assert.strictEqual(after.data.count, 2, '停止后已录内容仍可读');
|
||||
} finally {
|
||||
await webConsole.stop();
|
||||
}
|
||||
});
|
||||
|
||||
在新工单中引用
屏蔽一个用户