@botcord/daemon 0.2.57 → 0.2.58
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.
- package/dist/diagnostics.d.ts +2 -0
- package/dist/diagnostics.js +24 -0
- package/dist/gateway/channels/wechat.d.ts +2 -0
- package/dist/gateway/channels/wechat.js +4 -1
- package/dist/index.js +2 -0
- package/package.json +1 -1
- package/src/__tests__/diagnostics.test.ts +6 -0
- package/src/__tests__/wechat-channel.test.ts +9 -7
- package/src/diagnostics.ts +33 -0
- package/src/gateway/channels/wechat.ts +7 -1
- package/src/index.ts +2 -0
package/dist/diagnostics.d.ts
CHANGED
package/dist/diagnostics.js
CHANGED
|
@@ -189,6 +189,28 @@ function createZip(entries) {
|
|
|
189
189
|
]);
|
|
190
190
|
return Buffer.concat([...localParts, central, end]);
|
|
191
191
|
}
|
|
192
|
+
function shellQuote(s) {
|
|
193
|
+
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
194
|
+
}
|
|
195
|
+
function diagnosticBundleCommands(filePath) {
|
|
196
|
+
if (process.platform === "darwin") {
|
|
197
|
+
return {
|
|
198
|
+
revealCommand: `open -R ${shellQuote(filePath)}`,
|
|
199
|
+
copyPathCommand: `printf '%s' ${shellQuote(filePath)} | pbcopy`,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
if (process.platform === "win32") {
|
|
203
|
+
const psPath = filePath.replace(/'/g, "''");
|
|
204
|
+
return {
|
|
205
|
+
revealCommand: `explorer.exe /select,"${filePath.replace(/"/g, '""')}"`,
|
|
206
|
+
copyPathCommand: `powershell.exe -NoProfile -Command "Set-Clipboard -Value '${psPath}'"`,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
revealCommand: `xdg-open ${shellQuote(path.dirname(filePath))}`,
|
|
211
|
+
copyPathCommand: `printf '%s' ${shellQuote(filePath)} | xclip -selection clipboard`,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
192
214
|
export async function createDiagnosticBundle(opts = {}) {
|
|
193
215
|
const createdAt = new Date();
|
|
194
216
|
const stamp = createdAt.toISOString().replace(/[:.]/g, "-");
|
|
@@ -238,11 +260,13 @@ export async function createDiagnosticBundle(opts = {}) {
|
|
|
238
260
|
const zip = createZip(entries);
|
|
239
261
|
const out = path.join(diagnosticsDir, filename);
|
|
240
262
|
writeFileSync(out, zip, { mode: 0o600 });
|
|
263
|
+
const commands = diagnosticBundleCommands(out);
|
|
241
264
|
return {
|
|
242
265
|
path: out,
|
|
243
266
|
filename,
|
|
244
267
|
sizeBytes: zip.length,
|
|
245
268
|
createdAt: createdAt.toISOString(),
|
|
269
|
+
...commands,
|
|
246
270
|
};
|
|
247
271
|
}
|
|
248
272
|
export async function uploadDiagnosticBundle(opts) {
|
|
@@ -18,6 +18,8 @@ export interface WechatChannelOptions {
|
|
|
18
18
|
stateDebounceMs?: number;
|
|
19
19
|
/** Test hook: override Date.now() for trace cache TTL assertions. */
|
|
20
20
|
now?: () => number;
|
|
21
|
+
/** Test hook: override trace context cache cap without a 5000-poll test. */
|
|
22
|
+
traceContextMax?: number;
|
|
21
23
|
}
|
|
22
24
|
/**
|
|
23
25
|
* WeChat (iLink Bot API) channel adapter.
|
|
@@ -53,6 +53,9 @@ export function createWechatChannel(opts) {
|
|
|
53
53
|
const allowedSenderIds = new Set((opts.allowedSenderIds ?? []).map((s) => String(s)));
|
|
54
54
|
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
55
55
|
const now = opts.now ?? (() => Date.now());
|
|
56
|
+
const traceContextMax = opts.traceContextMax && opts.traceContextMax > 0
|
|
57
|
+
? opts.traceContextMax
|
|
58
|
+
: TRACE_CONTEXT_MAX;
|
|
56
59
|
let botToken = opts.botToken;
|
|
57
60
|
let stateStore = null;
|
|
58
61
|
let stopCallback = null;
|
|
@@ -103,7 +106,7 @@ export function createWechatChannel(opts) {
|
|
|
103
106
|
}
|
|
104
107
|
function rememberTrace(traceId, ctx) {
|
|
105
108
|
// W1: prune oldest entry by updatedAt when cap is reached.
|
|
106
|
-
if (traceContexts.size >=
|
|
109
|
+
if (traceContexts.size >= traceContextMax) {
|
|
107
110
|
let oldestKey;
|
|
108
111
|
let oldestAt = Infinity;
|
|
109
112
|
for (const [k, v] of traceContexts) {
|
package/dist/index.js
CHANGED
|
@@ -1223,6 +1223,8 @@ async function cmdDoctor(args) {
|
|
|
1223
1223
|
}
|
|
1224
1224
|
console.log(`diagnostic bundle written: ${bundle.path}`);
|
|
1225
1225
|
console.log(`size: ${bundle.sizeBytes} bytes`);
|
|
1226
|
+
console.log(`open in Finder/file manager: ${bundle.revealCommand}`);
|
|
1227
|
+
console.log(`copy path to clipboard: ${bundle.copyPathCommand}`);
|
|
1226
1228
|
console.log("Send this zip file to the BotCord developer/support contact.");
|
|
1227
1229
|
return;
|
|
1228
1230
|
}
|
package/package.json
CHANGED
|
@@ -25,6 +25,12 @@ describe("diagnostics bundle", () => {
|
|
|
25
25
|
});
|
|
26
26
|
expect(bundle.filename).toMatch(/^botcord-daemon-diagnostics-.*\.zip$/);
|
|
27
27
|
expect(bundle.path).toContain(diagnosticsDir);
|
|
28
|
+
if (process.platform === "linux") {
|
|
29
|
+
expect(bundle.revealCommand).toContain(diagnosticsDir);
|
|
30
|
+
} else {
|
|
31
|
+
expect(bundle.revealCommand).toContain(bundle.path);
|
|
32
|
+
}
|
|
33
|
+
expect(bundle.copyPathCommand).toContain(bundle.path);
|
|
28
34
|
expect(existsSync(bundle.path)).toBe(true);
|
|
29
35
|
const bytes = readFileSync(bundle.path);
|
|
30
36
|
expect(bytes.subarray(0, 4).toString("binary")).toBe("PK\u0003\u0004");
|
|
@@ -1080,7 +1080,7 @@ describe("W1: traceContexts hard cap", () => {
|
|
|
1080
1080
|
rmSync(tmpCap, { recursive: true, force: true });
|
|
1081
1081
|
});
|
|
1082
1082
|
|
|
1083
|
-
it("
|
|
1083
|
+
it("keeps trace context map at the configured cap (oldest pruned)", async () => {
|
|
1084
1084
|
// Build an adapter with a fake clock so we can control updatedAt order.
|
|
1085
1085
|
let nowMs = 1_000_000;
|
|
1086
1086
|
const fetchImpl = buildFetchStub(
|
|
@@ -1088,8 +1088,9 @@ describe("W1: traceContexts hard cap", () => {
|
|
|
1088
1088
|
{
|
|
1089
1089
|
match: "getupdates",
|
|
1090
1090
|
respond: (idx) => {
|
|
1091
|
-
if (idx <
|
|
1092
|
-
// Each poll returns one message so we get
|
|
1091
|
+
if (idx < 3) {
|
|
1092
|
+
// Each poll returns one message so we get one more entry than
|
|
1093
|
+
// the test cap without looping 5001 times in CI.
|
|
1093
1094
|
nowMs += 1;
|
|
1094
1095
|
return {
|
|
1095
1096
|
body: {
|
|
@@ -1106,7 +1107,7 @@ describe("W1: traceContexts hard cap", () => {
|
|
|
1106
1107
|
},
|
|
1107
1108
|
};
|
|
1108
1109
|
}
|
|
1109
|
-
return { body: { ret: 0, get_updates_buf: `buf-
|
|
1110
|
+
return { body: { ret: 0, get_updates_buf: `buf-3`, msgs: [] } };
|
|
1110
1111
|
},
|
|
1111
1112
|
},
|
|
1112
1113
|
],
|
|
@@ -1121,11 +1122,12 @@ describe("W1: traceContexts hard cap", () => {
|
|
|
1121
1122
|
stateDebounceMs: 0,
|
|
1122
1123
|
allowedSenderIds: ["alice@im.wechat"],
|
|
1123
1124
|
now: () => nowMs,
|
|
1125
|
+
traceContextMax: 2,
|
|
1124
1126
|
});
|
|
1125
|
-
const h = startAdapter(adapter, { stopAfterEnvelopes:
|
|
1127
|
+
const h = startAdapter(adapter, { stopAfterEnvelopes: 3 });
|
|
1126
1128
|
await h.pollDone;
|
|
1127
|
-
//
|
|
1128
|
-
expect(h.envelopes.length).toBe(
|
|
1129
|
+
// 3 messages were accepted; the cap should have kept the map <= 2.
|
|
1130
|
+
expect(h.envelopes.length).toBe(3);
|
|
1129
1131
|
// We can't read traceContexts directly, but we verify that the send() for
|
|
1130
1132
|
// the very first trace ID now fails (it was evicted as the oldest entry).
|
|
1131
1133
|
const firstTraceId = h.envelopes[0]!.message.trace!.id;
|
package/src/diagnostics.ts
CHANGED
|
@@ -43,6 +43,8 @@ export interface DiagnosticBundleResult {
|
|
|
43
43
|
filename: string;
|
|
44
44
|
sizeBytes: number;
|
|
45
45
|
createdAt: string;
|
|
46
|
+
revealCommand: string;
|
|
47
|
+
copyPathCommand: string;
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
export interface DiagnosticUploadResult {
|
|
@@ -242,6 +244,35 @@ function createZip(entries: Array<{ name: string; data: string | Buffer }>): Buf
|
|
|
242
244
|
return Buffer.concat([...localParts, central, end]);
|
|
243
245
|
}
|
|
244
246
|
|
|
247
|
+
function shellQuote(s: string): string {
|
|
248
|
+
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function diagnosticBundleCommands(filePath: string): {
|
|
252
|
+
revealCommand: string;
|
|
253
|
+
copyPathCommand: string;
|
|
254
|
+
} {
|
|
255
|
+
if (process.platform === "darwin") {
|
|
256
|
+
return {
|
|
257
|
+
revealCommand: `open -R ${shellQuote(filePath)}`,
|
|
258
|
+
copyPathCommand: `printf '%s' ${shellQuote(filePath)} | pbcopy`,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (process.platform === "win32") {
|
|
263
|
+
const psPath = filePath.replace(/'/g, "''");
|
|
264
|
+
return {
|
|
265
|
+
revealCommand: `explorer.exe /select,"${filePath.replace(/"/g, '""')}"`,
|
|
266
|
+
copyPathCommand: `powershell.exe -NoProfile -Command "Set-Clipboard -Value '${psPath}'"`,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
revealCommand: `xdg-open ${shellQuote(path.dirname(filePath))}`,
|
|
272
|
+
copyPathCommand: `printf '%s' ${shellQuote(filePath)} | xclip -selection clipboard`,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
245
276
|
export async function createDiagnosticBundle(
|
|
246
277
|
opts: CreateDiagnosticBundleOptions = {},
|
|
247
278
|
): Promise<DiagnosticBundleResult> {
|
|
@@ -296,11 +327,13 @@ export async function createDiagnosticBundle(
|
|
|
296
327
|
const zip = createZip(entries);
|
|
297
328
|
const out = path.join(diagnosticsDir, filename);
|
|
298
329
|
writeFileSync(out, zip, { mode: 0o600 });
|
|
330
|
+
const commands = diagnosticBundleCommands(out);
|
|
299
331
|
return {
|
|
300
332
|
path: out,
|
|
301
333
|
filename,
|
|
302
334
|
sizeBytes: zip.length,
|
|
303
335
|
createdAt: createdAt.toISOString(),
|
|
336
|
+
...commands,
|
|
304
337
|
};
|
|
305
338
|
}
|
|
306
339
|
|
|
@@ -65,6 +65,8 @@ export interface WechatChannelOptions {
|
|
|
65
65
|
stateDebounceMs?: number;
|
|
66
66
|
/** Test hook: override Date.now() for trace cache TTL assertions. */
|
|
67
67
|
now?: () => number;
|
|
68
|
+
/** Test hook: override trace context cache cap without a 5000-poll test. */
|
|
69
|
+
traceContextMax?: number;
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
interface WechatSecret {
|
|
@@ -138,6 +140,10 @@ export function createWechatChannel(opts: WechatChannelOptions): ChannelAdapter
|
|
|
138
140
|
const fetchImpl: FetchLike =
|
|
139
141
|
opts.fetchImpl ?? ((globalThis.fetch as unknown) as FetchLike);
|
|
140
142
|
const now: () => number = opts.now ?? (() => Date.now());
|
|
143
|
+
const traceContextMax =
|
|
144
|
+
opts.traceContextMax && opts.traceContextMax > 0
|
|
145
|
+
? opts.traceContextMax
|
|
146
|
+
: TRACE_CONTEXT_MAX;
|
|
141
147
|
|
|
142
148
|
let botToken: string | undefined = opts.botToken;
|
|
143
149
|
let stateStore: GatewayStateStore | null = null;
|
|
@@ -195,7 +201,7 @@ export function createWechatChannel(opts: WechatChannelOptions): ChannelAdapter
|
|
|
195
201
|
|
|
196
202
|
function rememberTrace(traceId: string, ctx: TraceContext): void {
|
|
197
203
|
// W1: prune oldest entry by updatedAt when cap is reached.
|
|
198
|
-
if (traceContexts.size >=
|
|
204
|
+
if (traceContexts.size >= traceContextMax) {
|
|
199
205
|
let oldestKey: string | undefined;
|
|
200
206
|
let oldestAt = Infinity;
|
|
201
207
|
for (const [k, v] of traceContexts) {
|
package/src/index.ts
CHANGED
|
@@ -1354,6 +1354,8 @@ async function cmdDoctor(args: ParsedArgs): Promise<void> {
|
|
|
1354
1354
|
}
|
|
1355
1355
|
console.log(`diagnostic bundle written: ${bundle.path}`);
|
|
1356
1356
|
console.log(`size: ${bundle.sizeBytes} bytes`);
|
|
1357
|
+
console.log(`open in Finder/file manager: ${bundle.revealCommand}`);
|
|
1358
|
+
console.log(`copy path to clipboard: ${bundle.copyPathCommand}`);
|
|
1357
1359
|
console.log("Send this zip file to the BotCord developer/support contact.");
|
|
1358
1360
|
return;
|
|
1359
1361
|
}
|