@mkterswingman/5mghost-wonder 0.0.13 → 0.0.15

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,677 @@
1
+ // src/wecom/browser-probe.ts
2
+ // Experimental browser runtime probe for no-export reads. This intentionally
3
+ // records evidence summaries, not raw network bodies or cookies.
4
+ import { spawn } from "node:child_process";
5
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
6
+ import { createServer } from "node:net";
7
+ import { join, resolve } from "node:path";
8
+ import WebSocket from "ws";
9
+ const CDP_CALL_TIMEOUT_MS = 5000;
10
+ const BROWSER_START_TIMEOUT_MS = 20_000;
11
+ const DEFAULT_CAPTURE_MS = 8_000;
12
+ const MAX_NETWORK_CANDIDATES = 40;
13
+ let cdpNextId = 1;
14
+ export async function runBrowserNoExportProbe(options) {
15
+ const timeoutMs = options.timeoutMs ?? DEFAULT_CAPTURE_MS;
16
+ const browser = findInstalledBrowser(options.executablePath);
17
+ const port = await getFreePort();
18
+ const endpoint = `http://127.0.0.1:${port}`;
19
+ const child = spawnBrowser({
20
+ browser,
21
+ port,
22
+ profilePath: options.chromeProfilePath,
23
+ url: options.url,
24
+ headed: options.headed ?? false,
25
+ });
26
+ try {
27
+ await waitForDebugger(endpoint, BROWSER_START_TIMEOUT_MS);
28
+ const pageWsUrl = await waitForPageWebSocket(endpoint, options.url, BROWSER_START_TIMEOUT_MS);
29
+ const socket = await openDevToolsSocket(pageWsUrl);
30
+ const responses = [];
31
+ try {
32
+ socket.addEventListener("message", (event) => {
33
+ const payload = safeJsonParse(String(event.data));
34
+ if (payload?.method !== "Network.responseReceived")
35
+ return;
36
+ const response = payload.params?.response;
37
+ const requestId = payload.params?.requestId;
38
+ if (!response?.url || !requestId)
39
+ return;
40
+ if (!isInterestingNetworkUrl(response.url, response.mimeType ?? ""))
41
+ return;
42
+ responses.push({
43
+ requestId,
44
+ url: response.url,
45
+ status: response.status ?? 0,
46
+ mimeType: response.mimeType ?? "",
47
+ });
48
+ });
49
+ await sendCdpCommand(socket, "Network.enable");
50
+ await sendCdpCommand(socket, "Page.enable");
51
+ await sendCdpCommand(socket, "Runtime.enable");
52
+ await sendCdpCommand(socket, "Page.navigate", { url: options.url });
53
+ await delay(timeoutMs);
54
+ const pageInfo = await evaluatePageInfo(socket);
55
+ const networkArtifacts = await collectNetworkArtifacts(socket, responses);
56
+ const networkCandidates = summarizeNetworkArtifacts(networkArtifacts);
57
+ const evidence = ["browser-page"];
58
+ if (pageInfo.visibleTextSample)
59
+ evidence.push("visible-text");
60
+ if (pageInfo.runtimeGlobals.length > 0)
61
+ evidence.push("runtime-globals");
62
+ if (networkCandidates.length > 0)
63
+ evidence.push("network-response-summary");
64
+ const missing = [
65
+ "raw structured document model",
66
+ "merge ranges",
67
+ "image original resources",
68
+ "image anchors",
69
+ ];
70
+ const result = {
71
+ mode: "browser-no-export-probe",
72
+ status: "partial",
73
+ url: redactUrl(options.url),
74
+ finalUrl: pageInfo.finalUrl ? redactUrl(pageInfo.finalUrl) : undefined,
75
+ title: pageInfo.title,
76
+ loginLikely: pageInfo.loginLikely,
77
+ visibleTextSample: pageInfo.visibleTextSample,
78
+ runtimeGlobals: pageInfo.runtimeGlobals,
79
+ networkCandidates,
80
+ evidence,
81
+ missing,
82
+ };
83
+ if (options.saveDir) {
84
+ mkdirSync(options.saveDir, { recursive: true });
85
+ const savedPath = resolve(options.saveDir, `wonder-browser-probe-${Date.now()}.json`);
86
+ writeFileSync(savedPath, JSON.stringify(result, null, 2), { mode: 0o600 });
87
+ result.savedPath = savedPath;
88
+ }
89
+ return result;
90
+ }
91
+ finally {
92
+ socket.close();
93
+ }
94
+ }
95
+ catch (err) {
96
+ return {
97
+ mode: "browser-no-export-probe",
98
+ status: "fail",
99
+ url: redactUrl(options.url),
100
+ loginLikely: false,
101
+ runtimeGlobals: [],
102
+ networkCandidates: [],
103
+ evidence: [],
104
+ missing: [err instanceof Error ? err.message : String(err)],
105
+ };
106
+ }
107
+ finally {
108
+ await terminateBrowserProcess(child);
109
+ }
110
+ }
111
+ export async function runBrowserNoExportRead(options) {
112
+ const timeoutMs = options.timeoutMs ?? DEFAULT_CAPTURE_MS;
113
+ const browser = findInstalledBrowser(options.executablePath);
114
+ const port = await getFreePort();
115
+ const endpoint = `http://127.0.0.1:${port}`;
116
+ const child = spawnBrowser({
117
+ browser,
118
+ port,
119
+ profilePath: options.chromeProfilePath,
120
+ url: options.url,
121
+ headed: options.headed ?? false,
122
+ });
123
+ try {
124
+ await waitForDebugger(endpoint, BROWSER_START_TIMEOUT_MS);
125
+ const pageWsUrl = await waitForPageWebSocket(endpoint, options.url, BROWSER_START_TIMEOUT_MS);
126
+ const socket = await openDevToolsSocket(pageWsUrl);
127
+ const responses = [];
128
+ try {
129
+ socket.addEventListener("message", (event) => {
130
+ const payload = safeJsonParse(String(event.data));
131
+ if (payload?.method !== "Network.responseReceived")
132
+ return;
133
+ const response = payload.params?.response;
134
+ const requestId = payload.params?.requestId;
135
+ if (!response?.url || !requestId)
136
+ return;
137
+ if (!isInterestingNetworkUrl(response.url, response.mimeType ?? ""))
138
+ return;
139
+ responses.push({
140
+ requestId,
141
+ url: response.url,
142
+ status: response.status ?? 0,
143
+ mimeType: response.mimeType ?? "",
144
+ });
145
+ });
146
+ await sendCdpCommand(socket, "Network.enable");
147
+ await sendCdpCommand(socket, "Page.enable");
148
+ await sendCdpCommand(socket, "Runtime.enable");
149
+ await sendCdpCommand(socket, "Page.navigate", { url: options.url });
150
+ await delay(timeoutMs);
151
+ const pageInfo = await evaluatePageInfo(socket);
152
+ const networkArtifacts = await collectNetworkArtifacts(socket, responses);
153
+ const opendoc = networkArtifacts.find((artifact) => artifact.rawUrl.includes("/dop-api/opendoc") && artifact.body);
154
+ const extracted = opendoc?.body ? extractTextFromOpendoc(opendoc.body) : null;
155
+ const evidence = ["browser-page", "network-response-summary"];
156
+ if (opendoc)
157
+ evidence.push("opendoc-response");
158
+ if (extracted?.text)
159
+ evidence.push("initial-attributed-text");
160
+ const result = {
161
+ mode: "browser-no-export-read",
162
+ status: extracted?.text ? "partial" : "fail",
163
+ url: redactUrl(options.url),
164
+ finalUrl: pageInfo.finalUrl ? redactUrl(pageInfo.finalUrl) : undefined,
165
+ title: extracted?.title ?? pageInfo.title,
166
+ padType: extracted?.padType,
167
+ text: extracted?.text,
168
+ textLength: extracted?.text.length,
169
+ imageUrls: extracted?.imageUrls,
170
+ extraction: extracted?.text ? "opendoc-initial-attributed-text" : "none",
171
+ evidence,
172
+ warnings: extracted?.warnings ?? [
173
+ "No readable opendoc initialAttributedText was captured.",
174
+ ],
175
+ missing: [
176
+ "table merge ranges",
177
+ ...(extracted?.imageUrls && extracted.imageUrls.length > 0 ? [] : ["image original resources"]),
178
+ "image anchors",
179
+ "floating vs fixed image classification",
180
+ ],
181
+ };
182
+ if (options.saveDir) {
183
+ mkdirSync(options.saveDir, { recursive: true });
184
+ const savedPath = resolve(options.saveDir, `wonder-browser-read-${Date.now()}.json`);
185
+ writeFileSync(savedPath, JSON.stringify(result, null, 2), { mode: 0o600 });
186
+ result.savedPath = savedPath;
187
+ }
188
+ return result;
189
+ }
190
+ finally {
191
+ socket.close();
192
+ }
193
+ }
194
+ catch (err) {
195
+ return {
196
+ mode: "browser-no-export-read",
197
+ status: "fail",
198
+ url: redactUrl(options.url),
199
+ extraction: "none",
200
+ evidence: [],
201
+ warnings: [],
202
+ missing: [err instanceof Error ? err.message : String(err)],
203
+ };
204
+ }
205
+ finally {
206
+ await terminateBrowserProcess(child);
207
+ }
208
+ }
209
+ async function evaluatePageInfo(socket) {
210
+ const expression = `(() => {
211
+ const text = (document.body && document.body.innerText || "").replace(/\\s+/g, " ").trim();
212
+ const globals = Object.keys(window)
213
+ .filter((key) => /doc|sheet|wecom|weixin|store|redux|editor|canvas/i.test(key))
214
+ .slice(0, 80);
215
+ const loginLikely = /登录|扫码|二维码|login|sign in/i.test(text);
216
+ return {
217
+ finalUrl: location.href,
218
+ title: document.title,
219
+ loginLikely,
220
+ visibleTextSample: text.slice(0, 2000),
221
+ runtimeGlobals: globals
222
+ };
223
+ })()`;
224
+ const payload = await sendCdpCommand(socket, "Runtime.evaluate", {
225
+ expression,
226
+ returnByValue: true,
227
+ awaitPromise: false,
228
+ });
229
+ const value = payload.result?.value;
230
+ return {
231
+ finalUrl: value?.finalUrl,
232
+ title: value?.title,
233
+ loginLikely: value?.loginLikely ?? false,
234
+ visibleTextSample: value?.visibleTextSample,
235
+ runtimeGlobals: value?.runtimeGlobals ?? [],
236
+ };
237
+ }
238
+ async function collectNetworkArtifacts(socket, responses) {
239
+ const unique = new Map();
240
+ for (const response of responses) {
241
+ if (unique.size >= MAX_NETWORK_CANDIDATES)
242
+ break;
243
+ unique.set(response.requestId, response);
244
+ }
245
+ const artifacts = [];
246
+ for (const response of unique.values()) {
247
+ let body = "";
248
+ let base64Encoded = false;
249
+ try {
250
+ const payload = await sendCdpCommand(socket, "Network.getResponseBody", { requestId: response.requestId });
251
+ body = payload.body ?? "";
252
+ base64Encoded = payload.base64Encoded ?? false;
253
+ }
254
+ catch {
255
+ // Some responses are streamed, cached, too large, or not retained.
256
+ }
257
+ const signals = detectBodySignals(body, response.mimeType);
258
+ artifacts.push({
259
+ requestId: response.requestId,
260
+ rawUrl: response.url,
261
+ url: redactUrl(response.url),
262
+ status: response.status,
263
+ mimeType: response.mimeType,
264
+ bodyLength: body ? body.length : undefined,
265
+ base64Encoded: body ? base64Encoded : undefined,
266
+ signals,
267
+ body,
268
+ });
269
+ }
270
+ return artifacts;
271
+ }
272
+ function summarizeNetworkArtifacts(artifacts) {
273
+ return artifacts.map((artifact) => ({
274
+ url: artifact.url,
275
+ status: artifact.status,
276
+ mimeType: artifact.mimeType,
277
+ bodyLength: artifact.bodyLength,
278
+ base64Encoded: artifact.base64Encoded,
279
+ signals: artifact.signals,
280
+ }));
281
+ }
282
+ export function extractTextFromOpendoc(body) {
283
+ let payload;
284
+ try {
285
+ payload = JSON.parse(body);
286
+ }
287
+ catch {
288
+ return null;
289
+ }
290
+ const initialText = payload.clientVars?.collab_client_vars?.initialAttributedText?.text;
291
+ const chunks = Array.isArray(initialText) ? initialText : [];
292
+ const decodedChunks = chunks
293
+ .filter((chunk) => typeof chunk === "string")
294
+ .flatMap((chunk) => extractInitialAttributedTextCandidates(chunk));
295
+ const textCandidate = chooseTextCandidate(decodedChunks);
296
+ const text = normalizeExtractedText(textCandidate ?? decodedChunks.join("\n"));
297
+ if (!text)
298
+ return null;
299
+ const imageUrls = extractImageUrls(decodedChunks.join("\n"));
300
+ return {
301
+ title: payload.clientVars?.title ?? payload.clientVars?.initialTitle,
302
+ padType: payload.clientVars?.padType ?? payload.padType,
303
+ text,
304
+ imageUrls,
305
+ warnings: [
306
+ "No-export text is decoded from opendoc initialAttributedText; rich styles are not reconstructed yet.",
307
+ ...(imageUrls.length > 0
308
+ ? ["Image resource URLs were detected, but image anchors and fixed/floating placement are not reconstructed yet."]
309
+ : []),
310
+ ],
311
+ };
312
+ }
313
+ function extractInitialAttributedTextCandidates(chunk) {
314
+ let decoded;
315
+ try {
316
+ decoded = Buffer.from(chunk, "base64");
317
+ }
318
+ catch {
319
+ decoded = Buffer.from(chunk, "utf8");
320
+ }
321
+ const candidates = collectProtobufStringCandidates(decoded);
322
+ if (candidates.length > 0)
323
+ return candidates.map((candidate) => candidate.text);
324
+ return [decoded.toString("utf8")];
325
+ }
326
+ function chooseTextCandidate(candidates) {
327
+ const scored = candidates
328
+ .map((text) => ({
329
+ text,
330
+ score: text.length +
331
+ (/\[[^\]]+\]/.test(text) ? 200 : 0) +
332
+ (/[\u4e00-\u9fff]/.test(text) ? 200 : 0) -
333
+ ((text.match(/https?:\/\//g)?.length ?? 0) * 100),
334
+ }))
335
+ .filter((candidate) => candidate.text.length > 0)
336
+ .sort((a, b) => b.score - a.score);
337
+ return scored[0]?.text ?? null;
338
+ }
339
+ function collectProtobufStringCandidates(buffer) {
340
+ const candidates = [];
341
+ walkProtobuf(buffer, 0, buffer.length, 0, candidates);
342
+ return candidates
343
+ .filter((candidate) => candidate.byteLength >= 2 &&
344
+ candidate.printableRatio >= 0.8 &&
345
+ /[\p{L}\p{N}\[]/u.test(candidate.text))
346
+ .sort((a, b) => b.byteLength - a.byteLength);
347
+ }
348
+ function walkProtobuf(buffer, start, end, depth, candidates) {
349
+ if (depth > 6)
350
+ return;
351
+ let offset = start;
352
+ let fieldsSeen = 0;
353
+ while (offset < end && fieldsSeen < 10_000) {
354
+ fieldsSeen++;
355
+ const key = readVarint(buffer, offset, end);
356
+ if (!key)
357
+ return;
358
+ offset = key.nextOffset;
359
+ const wireType = key.value & 7;
360
+ if (wireType === 0) {
361
+ const value = readVarint(buffer, offset, end);
362
+ if (!value)
363
+ return;
364
+ offset = value.nextOffset;
365
+ }
366
+ else if (wireType === 1) {
367
+ offset += 8;
368
+ }
369
+ else if (wireType === 5) {
370
+ offset += 4;
371
+ }
372
+ else if (wireType === 2) {
373
+ const length = readVarint(buffer, offset, end);
374
+ if (!length)
375
+ return;
376
+ offset = length.nextOffset;
377
+ const fieldEnd = offset + length.value;
378
+ if (fieldEnd > end)
379
+ return;
380
+ const raw = buffer.subarray(offset, fieldEnd);
381
+ const text = raw.toString("utf8");
382
+ candidates.push({
383
+ text,
384
+ byteLength: length.value,
385
+ printableRatio: printableRatio(text),
386
+ });
387
+ if (length.value > 2) {
388
+ walkProtobuf(buffer, offset, fieldEnd, depth + 1, candidates);
389
+ }
390
+ offset = fieldEnd;
391
+ }
392
+ else {
393
+ return;
394
+ }
395
+ }
396
+ }
397
+ function readVarint(buffer, offset, end) {
398
+ let value = 0n;
399
+ let shift = 0n;
400
+ let current = offset;
401
+ while (current < end) {
402
+ const byte = buffer[current++];
403
+ value |= BigInt(byte & 0x7f) << shift;
404
+ if ((byte & 0x80) === 0) {
405
+ return { value: Number(value), nextOffset: current };
406
+ }
407
+ shift += 7n;
408
+ if (shift > 63n)
409
+ return null;
410
+ }
411
+ return null;
412
+ }
413
+ function printableRatio(text) {
414
+ if (!text)
415
+ return 0;
416
+ let printable = 0;
417
+ for (const char of text) {
418
+ if (/[\p{L}\p{N}\p{P}\p{Zs}\r\n\t]/u.test(char))
419
+ printable++;
420
+ }
421
+ return printable / text.length;
422
+ }
423
+ function extractImageUrls(text) {
424
+ const urls = text.match(/https?:\/\/[^\s"'<>\\\u0000-\u001f]+/g) ?? [];
425
+ return Array.from(new Set(urls
426
+ .map((url) => url.replace(/[)*,.;:]+$/g, ""))
427
+ .filter((url) => /qpic\.cn|weixin\.qq\.com|doc\.weixin\.qq\.com/i.test(url))));
428
+ }
429
+ function normalizeExtractedText(text) {
430
+ return text
431
+ .replace(/p\.\d{8,}/g, "")
432
+ .replace(/\b\d{12,}@eJ\b/g, "")
433
+ .replace(/[^\S\r\n]+/g, " ")
434
+ .replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]+/g, "\n")
435
+ .split(/\r?\n/)
436
+ .map((line) => line.trim())
437
+ .filter((line) => line.length > 0)
438
+ .filter((line) => line.length > 1 || /[\u4e00-\u9fff]/.test(line))
439
+ .filter((line) => !line.includes("\uFFFD"))
440
+ .filter((line) => /[\p{L}\p{N}\]\)]/u.test(line))
441
+ .join("\n")
442
+ .replace(/\n{3,}/g, "\n\n")
443
+ .trim();
444
+ }
445
+ function detectBodySignals(body, mimeType) {
446
+ const haystack = `${mimeType}\n${body.slice(0, 200_000)}`;
447
+ const signals = [];
448
+ if (/merge|merged|rowspan|colspan|mergeCell|mergeCells/i.test(haystack))
449
+ signals.push("merge-like");
450
+ if (/image|img|pic|picture|media|download_url|url/i.test(haystack))
451
+ signals.push("image-like");
452
+ if (/cell|row|column|sheet|workbook|table/i.test(haystack))
453
+ signals.push("table-like");
454
+ if (/text|paragraph|content|doc|delta|operation|op/i.test(haystack))
455
+ signals.push("document-like");
456
+ if (/websocket|realtime|sync/i.test(haystack))
457
+ signals.push("realtime-like");
458
+ return signals;
459
+ }
460
+ function spawnBrowser(args) {
461
+ mkdirSync(args.profilePath, { recursive: true });
462
+ const browserArgs = [
463
+ `--remote-debugging-port=${args.port}`,
464
+ `--user-data-dir=${args.profilePath}`,
465
+ "--no-first-run",
466
+ "--no-default-browser-check",
467
+ "--disable-features=DialMediaRouteProvider",
468
+ ];
469
+ if (!args.headed) {
470
+ browserArgs.push("--headless=new");
471
+ browserArgs.push("--disable-gpu");
472
+ }
473
+ browserArgs.push(args.url);
474
+ return spawn(args.browser.executablePath, browserArgs, {
475
+ stdio: "ignore",
476
+ detached: process.platform !== "win32",
477
+ });
478
+ }
479
+ function getKnownBrowserPaths() {
480
+ if (process.platform === "darwin") {
481
+ return [
482
+ {
483
+ label: "Google Chrome",
484
+ executablePath: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
485
+ },
486
+ {
487
+ label: "Microsoft Edge",
488
+ executablePath: "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
489
+ },
490
+ ];
491
+ }
492
+ if (process.platform === "win32") {
493
+ const lad = process.env["LOCALAPPDATA"] ?? "";
494
+ const pf = process.env["PROGRAMFILES"] ?? "C:\\Program Files";
495
+ const pf86 = process.env["PROGRAMFILES(X86)"] ?? "C:\\Program Files (x86)";
496
+ return [
497
+ { label: "Google Chrome", executablePath: join(lad, "Google", "Chrome", "Application", "chrome.exe") },
498
+ { label: "Google Chrome", executablePath: join(pf, "Google", "Chrome", "Application", "chrome.exe") },
499
+ { label: "Google Chrome", executablePath: join(pf86, "Google", "Chrome", "Application", "chrome.exe") },
500
+ { label: "Microsoft Edge", executablePath: join(pf, "Microsoft", "Edge", "Application", "msedge.exe") },
501
+ { label: "Microsoft Edge", executablePath: join(pf86, "Microsoft", "Edge", "Application", "msedge.exe") },
502
+ { label: "Microsoft Edge", executablePath: join(lad, "Microsoft", "Edge", "Application", "msedge.exe") },
503
+ ];
504
+ }
505
+ return [
506
+ { label: "Google Chrome", executablePath: "/usr/bin/google-chrome-stable" },
507
+ { label: "Google Chrome", executablePath: "/usr/bin/google-chrome" },
508
+ { label: "Chromium", executablePath: "/usr/bin/chromium" },
509
+ { label: "Chromium", executablePath: "/usr/bin/chromium-browser" },
510
+ { label: "Microsoft Edge", executablePath: "/usr/bin/microsoft-edge" },
511
+ ];
512
+ }
513
+ function findInstalledBrowser(executablePath) {
514
+ if (executablePath) {
515
+ if (!existsSync(executablePath)) {
516
+ throw new Error(`Configured browser not found: ${executablePath}`);
517
+ }
518
+ return { label: "Configured browser", executablePath };
519
+ }
520
+ for (const candidate of getKnownBrowserPaths()) {
521
+ if (existsSync(candidate.executablePath))
522
+ return candidate;
523
+ }
524
+ throw new Error("Could not find a supported Chrome/Edge installation");
525
+ }
526
+ async function getFreePort() {
527
+ return new Promise((resolve, reject) => {
528
+ const server = createServer();
529
+ server.listen(0, "127.0.0.1", () => {
530
+ const address = server.address();
531
+ if (!address || typeof address === "string") {
532
+ server.close();
533
+ reject(new Error("Failed to allocate a browser debugging port"));
534
+ return;
535
+ }
536
+ const port = address.port;
537
+ server.close((err) => {
538
+ if (err)
539
+ reject(err);
540
+ else
541
+ resolve(port);
542
+ });
543
+ });
544
+ server.on("error", reject);
545
+ });
546
+ }
547
+ async function waitForDebugger(endpoint, timeoutMs) {
548
+ const startedAt = Date.now();
549
+ while (Date.now() - startedAt < timeoutMs) {
550
+ try {
551
+ const res = await fetch(`${endpoint}/json/version`);
552
+ if (res.ok)
553
+ return;
554
+ }
555
+ catch {
556
+ // Startup race.
557
+ }
558
+ await delay(500);
559
+ }
560
+ throw new Error("Browser did not expose the remote debugging endpoint in time");
561
+ }
562
+ async function waitForPageWebSocket(endpoint, expectedUrl, timeoutMs) {
563
+ const startedAt = Date.now();
564
+ while (Date.now() - startedAt < timeoutMs) {
565
+ try {
566
+ const res = await fetch(`${endpoint}/json/list`);
567
+ if (res.ok) {
568
+ const targets = (await res.json());
569
+ const expectedHost = new URL(expectedUrl).host;
570
+ const target = targets.find((t) => t.type === "page" &&
571
+ t.webSocketDebuggerUrl &&
572
+ (t.url?.includes(expectedHost) || targets.length === 1));
573
+ if (target?.webSocketDebuggerUrl)
574
+ return target.webSocketDebuggerUrl;
575
+ }
576
+ }
577
+ catch {
578
+ // Startup race.
579
+ }
580
+ await delay(500);
581
+ }
582
+ throw new Error("Browser did not expose a page DevTools websocket in time");
583
+ }
584
+ async function openDevToolsSocket(url) {
585
+ return new Promise((resolve, reject) => {
586
+ const socket = new WebSocket(url);
587
+ const timer = setTimeout(() => {
588
+ socket.close();
589
+ reject(new Error("Timed out connecting to browser DevTools websocket"));
590
+ }, CDP_CALL_TIMEOUT_MS);
591
+ socket.once("open", () => {
592
+ clearTimeout(timer);
593
+ resolve(socket);
594
+ });
595
+ socket.once("error", () => {
596
+ clearTimeout(timer);
597
+ reject(new Error("Failed to connect to browser DevTools websocket"));
598
+ });
599
+ });
600
+ }
601
+ async function sendCdpCommand(socket, method, params = {}) {
602
+ const id = cdpNextId++;
603
+ return new Promise((resolve, reject) => {
604
+ const timer = setTimeout(() => {
605
+ socket.removeEventListener("message", handleMessage);
606
+ reject(new Error(`CDP timeout: ${method}`));
607
+ }, CDP_CALL_TIMEOUT_MS);
608
+ const handleMessage = (event) => {
609
+ const payload = safeJsonParse(String(event.data));
610
+ if (payload?.id !== id)
611
+ return;
612
+ clearTimeout(timer);
613
+ socket.removeEventListener("message", handleMessage);
614
+ if (payload.error)
615
+ reject(new Error(payload.error.message ?? `CDP error: ${method}`));
616
+ else
617
+ resolve((payload.result ?? {}));
618
+ };
619
+ socket.addEventListener("message", handleMessage);
620
+ socket.send(JSON.stringify({ id, method, params }));
621
+ });
622
+ }
623
+ async function terminateBrowserProcess(child) {
624
+ if (child.exitCode !== null)
625
+ return;
626
+ if (process.platform !== "win32" && child.pid) {
627
+ try {
628
+ process.kill(-child.pid, "SIGTERM");
629
+ }
630
+ catch {
631
+ child.kill();
632
+ }
633
+ }
634
+ else {
635
+ child.kill();
636
+ }
637
+ await delay(500);
638
+ if (child.exitCode === null) {
639
+ try {
640
+ if (process.platform !== "win32" && child.pid)
641
+ process.kill(-child.pid, "SIGKILL");
642
+ else
643
+ child.kill("SIGKILL");
644
+ }
645
+ catch {
646
+ // Already exited.
647
+ }
648
+ }
649
+ }
650
+ function isInterestingNetworkUrl(url, mimeType) {
651
+ if (!/doc\.weixin\.qq\.com|weixin\.qq\.com|wecom/i.test(url))
652
+ return false;
653
+ return /json|text|javascript|octet-stream|protobuf|grpc|xml|html/i.test(mimeType) ||
654
+ /cgi|api|doc|sheet|media|image|file|sync|ws/i.test(url);
655
+ }
656
+ function redactUrl(rawUrl) {
657
+ try {
658
+ const u = new URL(rawUrl);
659
+ u.search = "";
660
+ u.hash = "";
661
+ return u.toString();
662
+ }
663
+ catch {
664
+ return rawUrl.split("?")[0] ?? rawUrl;
665
+ }
666
+ }
667
+ function safeJsonParse(value) {
668
+ try {
669
+ return JSON.parse(value);
670
+ }
671
+ catch {
672
+ return null;
673
+ }
674
+ }
675
+ function delay(ms) {
676
+ return new Promise((resolve) => setTimeout(resolve, ms));
677
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mkterswingman/5mghost-wonder",
3
- "version": "0.0.13",
3
+ "version": "0.0.15",
4
4
  "description": "企微文档读取 CLI — WeCom document reader",
5
5
  "type": "module",
6
6
  "engines": {
@@ -25,7 +25,8 @@
25
25
  "scripts": {
26
26
  "build": "rm -rf dist && tsc && chmod +x dist/cli.js",
27
27
  "typecheck": "tsc --noEmit",
28
- "test": "node dist/wecom/url.test.js && node --test tests/sheet-parity.test.mjs && node --test tests/export-sanitize.test.mjs && node --test tests/format.test.mjs && node --test tests/cookies-validation.test.mjs",
28
+ "check:skills": "node scripts/check-skills.mjs",
29
+ "test": "npm run check:skills && node dist/wecom/url.test.js && node --test tests/sheet-parity.test.mjs && node --test tests/export-sanitize.test.mjs && node --test tests/format.test.mjs && node --test tests/cookies-validation.test.mjs && node --test tests/browser-read.test.mjs",
29
30
  "smoke": "npm run build && node dist/cli.js help > /dev/null",
30
31
  "postinstall": "node scripts/postinstall.mjs"
31
32
  },