@kzheart_/mc-pilot 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import { MctError } from "../util/errors.js";
|
|
5
|
+
import { resolveMctHome } from "../util/paths.js";
|
|
6
|
+
import { wrapCommand } from "../util/command.js";
|
|
7
|
+
function resolveEventsFile(clientName) {
|
|
8
|
+
if (!clientName) {
|
|
9
|
+
throw new MctError({
|
|
10
|
+
code: "INVALID_PARAMS",
|
|
11
|
+
message: "Client name is required. Use --client <name> or set an active profile."
|
|
12
|
+
}, 4);
|
|
13
|
+
}
|
|
14
|
+
return path.join(resolveMctHome(), "logs", clientName, "events.jsonl");
|
|
15
|
+
}
|
|
16
|
+
function parseSince(raw, nowMs) {
|
|
17
|
+
if (!raw)
|
|
18
|
+
return undefined;
|
|
19
|
+
const trimmed = raw.trim();
|
|
20
|
+
// 支持 "30s" / "5m" / "1h" / "200ms" / epoch 毫秒
|
|
21
|
+
const m = /^(\d+(?:\.\d+)?)(ms|s|m|h)?$/.exec(trimmed);
|
|
22
|
+
if (!m) {
|
|
23
|
+
throw new MctError({ code: "INVALID_PARAMS", message: `Invalid --since value: ${raw}` }, 4);
|
|
24
|
+
}
|
|
25
|
+
const value = Number(m[1]);
|
|
26
|
+
const unit = m[2];
|
|
27
|
+
if (!unit) {
|
|
28
|
+
// epoch millis
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
const multipliers = { ms: 1, s: 1000, m: 60_000, h: 3_600_000 };
|
|
32
|
+
return nowMs - value * multipliers[unit];
|
|
33
|
+
}
|
|
34
|
+
function readAllEvents(filePath) {
|
|
35
|
+
if (!existsSync(filePath))
|
|
36
|
+
return [];
|
|
37
|
+
const raw = readFileSync(filePath, "utf8");
|
|
38
|
+
const lines = raw.split("\n").filter((line) => line.length > 0);
|
|
39
|
+
const out = [];
|
|
40
|
+
for (const line of lines) {
|
|
41
|
+
try {
|
|
42
|
+
out.push(JSON.parse(line));
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// 跳过损坏行
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
export function createEventsCommand() {
|
|
51
|
+
const command = new Command("events").description("Inspect the client event log (written by the mod to ~/.mct/logs/<client>/events.jsonl)");
|
|
52
|
+
command
|
|
53
|
+
.command("list")
|
|
54
|
+
.description("Print events. Defaults to the last 20 for the active client.")
|
|
55
|
+
.option("--tail <n>", "Show the last N events (default 20)", (v) => Number(v))
|
|
56
|
+
.option("--since <duration>", "Only show events since duration ago (e.g. 30s, 5m, 1h) or epoch ms")
|
|
57
|
+
.option("--type <types>", "Comma-separated list of event types to include")
|
|
58
|
+
.option("--all", "Show all events (ignore --tail)")
|
|
59
|
+
.option("--file <path>", "Override the log file path")
|
|
60
|
+
.action(wrapCommand(async (context, { options, globalOptions }) => {
|
|
61
|
+
const clientName = globalOptions.client ?? context.activeProfile?.clients[0];
|
|
62
|
+
const filePath = options.file ?? resolveEventsFile(clientName);
|
|
63
|
+
const events = readAllEvents(filePath);
|
|
64
|
+
let filtered = events;
|
|
65
|
+
const sinceMs = parseSince(options.since, Date.now());
|
|
66
|
+
if (sinceMs !== undefined) {
|
|
67
|
+
filtered = filtered.filter((e) => e.t >= sinceMs);
|
|
68
|
+
}
|
|
69
|
+
if (options.type) {
|
|
70
|
+
const wanted = new Set(options.type.split(",").map((s) => s.trim()).filter(Boolean));
|
|
71
|
+
filtered = filtered.filter((e) => wanted.has(e.type));
|
|
72
|
+
}
|
|
73
|
+
if (!options.all) {
|
|
74
|
+
const tail = options.tail ?? 20;
|
|
75
|
+
if (filtered.length > tail) {
|
|
76
|
+
filtered = filtered.slice(filtered.length - tail);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
file: filePath,
|
|
81
|
+
total: events.length,
|
|
82
|
+
returned: filtered.length,
|
|
83
|
+
events: filtered
|
|
84
|
+
};
|
|
85
|
+
}));
|
|
86
|
+
command
|
|
87
|
+
.command("tail")
|
|
88
|
+
.description("Shortcut for `events list --tail N`")
|
|
89
|
+
.argument("[n]", "Number of events (default 20)")
|
|
90
|
+
.option("--type <types>", "Comma-separated event types to include")
|
|
91
|
+
.option("--file <path>", "Override the log file path")
|
|
92
|
+
.action(wrapCommand(async (context, { args, options, globalOptions }) => {
|
|
93
|
+
const tail = args[0] ? Number(args[0]) : 20;
|
|
94
|
+
const clientName = globalOptions.client ?? context.activeProfile?.clients[0];
|
|
95
|
+
const filePath = options.file ?? resolveEventsFile(clientName);
|
|
96
|
+
let events = readAllEvents(filePath);
|
|
97
|
+
if (options.type) {
|
|
98
|
+
const wanted = new Set(options.type.split(",").map((s) => s.trim()).filter(Boolean));
|
|
99
|
+
events = events.filter((e) => wanted.has(e.type));
|
|
100
|
+
}
|
|
101
|
+
if (events.length > tail)
|
|
102
|
+
events = events.slice(events.length - tail);
|
|
103
|
+
return { file: filePath, returned: events.length, events };
|
|
104
|
+
}));
|
|
105
|
+
command
|
|
106
|
+
.command("clear")
|
|
107
|
+
.description("Truncate the event log for the active client")
|
|
108
|
+
.option("--file <path>", "Override the log file path")
|
|
109
|
+
.action(wrapCommand(async (context, { options, globalOptions }) => {
|
|
110
|
+
const { truncateSync, existsSync: exists, mkdirSync } = await import("node:fs");
|
|
111
|
+
const clientName = globalOptions.client ?? context.activeProfile?.clients[0];
|
|
112
|
+
const filePath = options.file ?? resolveEventsFile(clientName);
|
|
113
|
+
if (exists(filePath)) {
|
|
114
|
+
truncateSync(filePath, 0);
|
|
115
|
+
return { cleared: true, file: filePath };
|
|
116
|
+
}
|
|
117
|
+
// ensure directory exists for future writes
|
|
118
|
+
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
119
|
+
return { cleared: false, reason: "file_not_found", file: filePath };
|
|
120
|
+
}));
|
|
121
|
+
command
|
|
122
|
+
.command("path")
|
|
123
|
+
.description("Print the expected path to the event log for the active client")
|
|
124
|
+
.action(wrapCommand(async (context, { globalOptions }) => {
|
|
125
|
+
const clientName = globalOptions.client ?? context.activeProfile?.clients[0];
|
|
126
|
+
return { file: resolveEventsFile(clientName), exists: existsSync(resolveEventsFile(clientName)) };
|
|
127
|
+
}));
|
|
128
|
+
return command;
|
|
129
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import { createChatCommand } from "./commands/chat.js";
|
|
|
9
9
|
import { createCombatCommand } from "./commands/combat.js";
|
|
10
10
|
import { createAnvilCommand, createCraftCommand, createEnchantCommand, createTradeCommand } from "./commands/craft.js";
|
|
11
11
|
import { createEntityCommand } from "./commands/entity.js";
|
|
12
|
+
import { createEventsCommand } from "./commands/events.js";
|
|
12
13
|
import { createGuiCommand } from "./commands/gui.js";
|
|
13
14
|
import { createHudCommand } from "./commands/hud.js";
|
|
14
15
|
import { createInputCommand } from "./commands/input.js";
|
|
@@ -90,6 +91,7 @@ export function buildProgram() {
|
|
|
90
91
|
program.addCommand(createEnchantCommand());
|
|
91
92
|
program.addCommand(createTradeCommand());
|
|
92
93
|
program.addCommand(createWaitCommand());
|
|
94
|
+
program.addCommand(createEventsCommand());
|
|
93
95
|
return program;
|
|
94
96
|
}
|
|
95
97
|
const program = buildProgram();
|
|
@@ -60,6 +60,8 @@ export declare class ClientInstanceManager {
|
|
|
60
60
|
inWorld: boolean;
|
|
61
61
|
position: unknown;
|
|
62
62
|
}>;
|
|
63
|
+
private buildDiagnostics;
|
|
64
|
+
private formatDiagnostics;
|
|
63
65
|
getClient(name?: string): Promise<ClientRuntimeEntry>;
|
|
64
66
|
loadMeta(clientName: string): Promise<ClientInstanceMeta>;
|
|
65
67
|
updateMeta(clientName: string, updates: Partial<ClientInstanceMeta>): Promise<ClientInstanceMeta>;
|
|
@@ -190,6 +190,7 @@ export class ClientInstanceManager {
|
|
|
190
190
|
const requireWorld = options.requireWorld ?? true;
|
|
191
191
|
// 阶段 A:等 WS 连通
|
|
192
192
|
let connected = false;
|
|
193
|
+
let lastConnectError;
|
|
193
194
|
while (Date.now() < deadline) {
|
|
194
195
|
try {
|
|
195
196
|
const ws = new WebSocketClient(wsUrl);
|
|
@@ -197,12 +198,22 @@ export class ClientInstanceManager {
|
|
|
197
198
|
connected = true;
|
|
198
199
|
break;
|
|
199
200
|
}
|
|
200
|
-
catch {
|
|
201
|
+
catch (err) {
|
|
202
|
+
lastConnectError = err instanceof Error ? err.message : String(err);
|
|
201
203
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
202
204
|
}
|
|
203
205
|
}
|
|
204
206
|
if (!connected) {
|
|
205
|
-
|
|
207
|
+
const diag = this.buildDiagnostics(entry.pid, entry.wsPort, {
|
|
208
|
+
wsConnected: false,
|
|
209
|
+
inWorld: false,
|
|
210
|
+
lastError: lastConnectError
|
|
211
|
+
});
|
|
212
|
+
throw new MctError({
|
|
213
|
+
code: "TIMEOUT",
|
|
214
|
+
message: `Timed out after ${timeoutSeconds}s waiting for client ${clientName} (${wsUrl}). ${this.formatDiagnostics(diag)}`,
|
|
215
|
+
details: diag
|
|
216
|
+
}, 2);
|
|
206
217
|
}
|
|
207
218
|
if (!requireWorld) {
|
|
208
219
|
return { connected: true, url: wsUrl };
|
|
@@ -221,16 +232,47 @@ export class ClientInstanceManager {
|
|
|
221
232
|
break;
|
|
222
233
|
}
|
|
223
234
|
}
|
|
224
|
-
catch {
|
|
225
|
-
|
|
235
|
+
catch (err) {
|
|
236
|
+
lastErrorCode = err instanceof Error ? `WS_ERROR(${err.message})` : "WS_ERROR";
|
|
226
237
|
}
|
|
227
238
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
228
239
|
}
|
|
240
|
+
const diag = this.buildDiagnostics(entry.pid, entry.wsPort, {
|
|
241
|
+
wsConnected: true,
|
|
242
|
+
inWorld: false,
|
|
243
|
+
lastError: lastErrorCode
|
|
244
|
+
});
|
|
229
245
|
throw new MctError({
|
|
230
246
|
code: "TIMEOUT",
|
|
231
|
-
message: `
|
|
247
|
+
message: `Timed out after ${timeoutSeconds}s waiting for client ${clientName} to join a world (${wsUrl}). ${this.formatDiagnostics(diag)} Tip: try \`mct client reconnect --address <server>\` or \`mct client launch --force\`.`,
|
|
248
|
+
details: diag
|
|
232
249
|
}, 2);
|
|
233
250
|
}
|
|
251
|
+
buildDiagnostics(pid, wsPort, extras) {
|
|
252
|
+
const processAlive = isProcessRunning(pid);
|
|
253
|
+
const portListening = getListeningPids(wsPort).length > 0;
|
|
254
|
+
return {
|
|
255
|
+
pid,
|
|
256
|
+
processAlive,
|
|
257
|
+
wsPort,
|
|
258
|
+
portListening,
|
|
259
|
+
wsConnected: extras.wsConnected,
|
|
260
|
+
inWorld: extras.inWorld,
|
|
261
|
+
lastError: extras.lastError
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
formatDiagnostics(diag) {
|
|
265
|
+
const parts = [
|
|
266
|
+
`processAlive=${diag.processAlive}`,
|
|
267
|
+
`portListening=${diag.portListening}`,
|
|
268
|
+
`wsConnected=${diag.wsConnected}`,
|
|
269
|
+
`inWorld=${diag.inWorld}`
|
|
270
|
+
];
|
|
271
|
+
if (diag.lastError) {
|
|
272
|
+
parts.push(`lastError=${diag.lastError}`);
|
|
273
|
+
}
|
|
274
|
+
return `Diagnostics: ${parts.join(", ")}.`;
|
|
275
|
+
}
|
|
234
276
|
async getClient(name) {
|
|
235
277
|
const state = await this.globalState.readClientState();
|
|
236
278
|
const resolvedName = name ?? state.defaultClient;
|