@mkterswingman/5mghost-wonder 0.0.15 → 0.0.16

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.
@@ -10,6 +10,8 @@ const CDP_CALL_TIMEOUT_MS = 5000;
10
10
  const BROWSER_START_TIMEOUT_MS = 20_000;
11
11
  const DEFAULT_CAPTURE_MS = 8_000;
12
12
  const MAX_NETWORK_CANDIDATES = 40;
13
+ const MAX_IMAGE_DOWNLOADS = 20;
14
+ const MAX_IMAGE_BYTES = 25 * 1024 * 1024;
13
15
  let cdpNextId = 1;
14
16
  export async function runBrowserNoExportProbe(options) {
15
17
  const timeoutMs = options.timeoutMs ?? DEFAULT_CAPTURE_MS;
@@ -157,6 +159,16 @@ export async function runBrowserNoExportRead(options) {
157
159
  evidence.push("opendoc-response");
158
160
  if (extracted?.text)
159
161
  evidence.push("initial-attributed-text");
162
+ const images = options.saveDir && extracted?.imageUrls
163
+ ? await downloadImageResources(extracted.imageUrls, options.saveDir)
164
+ : (extracted?.imageUrls ?? []).map((url) => ({
165
+ url,
166
+ ...parseImageDimensionsFromUrl(url),
167
+ status: "skipped",
168
+ error: "pass --save <dir> to download image resources",
169
+ }));
170
+ if (images.some((image) => image.status === "downloaded"))
171
+ evidence.push("image-resource-download");
160
172
  const result = {
161
173
  mode: "browser-no-export-read",
162
174
  status: extracted?.text ? "partial" : "fail",
@@ -167,6 +179,7 @@ export async function runBrowserNoExportRead(options) {
167
179
  text: extracted?.text,
168
180
  textLength: extracted?.text.length,
169
181
  imageUrls: extracted?.imageUrls,
182
+ images,
170
183
  extraction: extracted?.text ? "opendoc-initial-attributed-text" : "none",
171
184
  evidence,
172
185
  warnings: extracted?.warnings ?? [
@@ -174,7 +187,7 @@ export async function runBrowserNoExportRead(options) {
174
187
  ],
175
188
  missing: [
176
189
  "table merge ranges",
177
- ...(extracted?.imageUrls && extracted.imageUrls.length > 0 ? [] : ["image original resources"]),
190
+ ...(images.some((image) => image.status === "downloaded") ? [] : ["image original resources"]),
178
191
  "image anchors",
179
192
  "floating vs fixed image classification",
180
193
  ],
@@ -426,6 +439,97 @@ function extractImageUrls(text) {
426
439
  .map((url) => url.replace(/[)*,.;:]+$/g, ""))
427
440
  .filter((url) => /qpic\.cn|weixin\.qq\.com|doc\.weixin\.qq\.com/i.test(url))));
428
441
  }
442
+ async function downloadImageResources(imageUrls, saveDir) {
443
+ const imageDir = resolve(saveDir, "images");
444
+ mkdirSync(imageDir, { recursive: true });
445
+ const results = [];
446
+ for (const [index, url] of imageUrls.slice(0, MAX_IMAGE_DOWNLOADS).entries()) {
447
+ const dimensions = parseImageDimensionsFromUrl(url);
448
+ try {
449
+ const res = await fetch(url);
450
+ if (!res.ok) {
451
+ results.push({
452
+ url,
453
+ ...dimensions,
454
+ status: "failed",
455
+ error: `HTTP ${res.status}`,
456
+ });
457
+ continue;
458
+ }
459
+ const contentType = res.headers.get("content-type") ?? undefined;
460
+ const bytes = Buffer.from(await res.arrayBuffer());
461
+ if (bytes.length > MAX_IMAGE_BYTES) {
462
+ results.push({
463
+ url,
464
+ ...dimensions,
465
+ contentType,
466
+ sizeBytes: bytes.length,
467
+ status: "failed",
468
+ error: `image exceeds ${MAX_IMAGE_BYTES} bytes`,
469
+ });
470
+ continue;
471
+ }
472
+ const filePath = resolve(imageDir, `image-${String(index + 1).padStart(3, "0")}${extensionForContentType(contentType)}`);
473
+ writeFileSync(filePath, bytes, { mode: 0o600 });
474
+ results.push({
475
+ url,
476
+ path: filePath,
477
+ contentType,
478
+ sizeBytes: bytes.length,
479
+ ...dimensions,
480
+ status: "downloaded",
481
+ });
482
+ }
483
+ catch (err) {
484
+ results.push({
485
+ url,
486
+ ...dimensions,
487
+ status: "failed",
488
+ error: err instanceof Error ? err.message : String(err),
489
+ });
490
+ }
491
+ }
492
+ if (imageUrls.length > MAX_IMAGE_DOWNLOADS) {
493
+ for (const url of imageUrls.slice(MAX_IMAGE_DOWNLOADS)) {
494
+ results.push({
495
+ url,
496
+ ...parseImageDimensionsFromUrl(url),
497
+ status: "skipped",
498
+ error: `only first ${MAX_IMAGE_DOWNLOADS} images are downloaded`,
499
+ });
500
+ }
501
+ }
502
+ return results;
503
+ }
504
+ export function parseImageDimensionsFromUrl(url) {
505
+ try {
506
+ const parsed = new URL(url);
507
+ const width = Number(parsed.searchParams.get("w"));
508
+ const height = Number(parsed.searchParams.get("h"));
509
+ return {
510
+ width: Number.isFinite(width) && width > 0 ? width : undefined,
511
+ height: Number.isFinite(height) && height > 0 ? height : undefined,
512
+ };
513
+ }
514
+ catch {
515
+ return {};
516
+ }
517
+ }
518
+ function extensionForContentType(contentType) {
519
+ if (!contentType)
520
+ return ".bin";
521
+ if (contentType.includes("png"))
522
+ return ".png";
523
+ if (contentType.includes("jpeg") || contentType.includes("jpg"))
524
+ return ".jpg";
525
+ if (contentType.includes("gif"))
526
+ return ".gif";
527
+ if (contentType.includes("webp"))
528
+ return ".webp";
529
+ if (contentType.includes("svg"))
530
+ return ".svg";
531
+ return ".bin";
532
+ }
429
533
  function normalizeExtractedText(text) {
430
534
  return text
431
535
  .replace(/p\.\d{8,}/g, "")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mkterswingman/5mghost-wonder",
3
- "version": "0.0.15",
3
+ "version": "0.0.16",
4
4
  "description": "企微文档读取 CLI — WeCom document reader",
5
5
  "type": "module",
6
6
  "engines": {
@@ -7,9 +7,9 @@ description: Use this skill when the user wants to install or set up wonder, say
7
7
 
8
8
  ## Skill version
9
9
 
10
- This skill matches **wonder 0.0.15**.
10
+ This skill matches **wonder 0.0.16**.
11
11
 
12
- Once the CLI is installed in Step 1, run `wonder --version`. If the output does not equal `0.0.15`, the CLI on disk has drifted from the skill text loaded in this session. Ask the user to run `/update-5mghost-wonder`, then **start a fresh AI session** (`/exit` and re-enter, or open a new chat) — skill text already loaded into a running session does not refresh after `wonder update`, even though the file on disk has been replaced.
12
+ Once the CLI is installed in Step 1, run `wonder --version`. If the output does not equal `0.0.16`, the CLI on disk has drifted from the skill text loaded in this session. Ask the user to run `/update-5mghost-wonder`, then **start a fresh AI session** (`/exit` and re-enter, or open a new chat) — skill text already loaded into a running session does not refresh after `wonder update`, even though the file on disk has been replaced.
13
13
 
14
14
  After a successful first install, also remind the user to start a fresh AI session before invoking `/use-5mghost-wonder` for the first time. The skill files were just written to disk; the current session never loaded them.
15
15
 
@@ -7,7 +7,7 @@ description: Use this skill when the user wants to update or upgrade wonder, say
7
7
 
8
8
  ## Skill version
9
9
 
10
- This skill matches **wonder 0.0.15**.
10
+ This skill matches **wonder 0.0.16**.
11
11
 
12
12
  ---
13
13
 
@@ -10,10 +10,10 @@ the referenced workflow files needed for the current task.
10
10
 
11
11
  ## Version Gate
12
12
 
13
- This skill matches **wonder 0.0.15**.
13
+ This skill matches **wonder 0.0.16**.
14
14
 
15
15
  On first use in a session, follow `references/session-init.md`. If the installed
16
- CLI version differs from `0.0.15`, stop and ask the user to run
16
+ CLI version differs from `0.0.16`, stop and ask the user to run
17
17
  `/update-5mghost-wonder`, then start a fresh AI session.
18
18
 
19
19
  ## Hard Rules