@mkterswingman/5mghost-wonder 0.0.18 → 0.0.20
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/commands/read.js +142 -27
- package/dist/wecom/browser-probe.js +18 -1
- package/dist/wecom/export.js +21 -0
- package/package.json +1 -1
- package/skills/setup-5mghost-wonder/SKILL.md +2 -2
- package/skills/update-5mghost-wonder/SKILL.md +1 -1
- package/skills/use-5mghost-wonder/SKILL.md +2 -2
- package/skills/use-5mghost-wonder/references/read-no-export-browser.md +9 -6
- package/skills/use-5mghost-wonder/references/verification.md +2 -1
package/dist/commands/read.js
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
// Phase 1: URL routing, export, metadata output.
|
|
4
4
|
// Phase 2: --tab cell/merge/image parsing.
|
|
5
5
|
import { parseWecomUrl } from "../wecom/url.js";
|
|
6
|
-
import { exportWecomDoc, ExportError } from "../wecom/export.js";
|
|
7
|
-
import { loadCookies, getCookieStatus } from "../wecom/cookies.js";
|
|
6
|
+
import { exportWecomDoc, ExportError, isExportAuthError } from "../wecom/export.js";
|
|
7
|
+
import { loadCookies, getCookieStatus, saveCookies, validateCookies } from "../wecom/cookies.js";
|
|
8
|
+
import { collectWecomCookiesViaBrowser } from "../wecom/browser.js";
|
|
8
9
|
import { resolveWonderPaths } from "../platform/paths.js";
|
|
9
10
|
import { lookupCachedExport, saveExportToCache } from "../wecom/cache.js";
|
|
10
11
|
import { parseTab } from "../xlsx/parse-tab.js";
|
|
@@ -32,6 +33,7 @@ export async function runReadCommand(args, context) {
|
|
|
32
33
|
writeError({ error: "missing_url", message: "用法:wonder read <url> [--save <dir>]" });
|
|
33
34
|
return { exitCode: 1, telemetry: { outcome: "failure", errorKind: "missing_url" } };
|
|
34
35
|
}
|
|
36
|
+
const sourceUrl = url;
|
|
35
37
|
// --- Parse URL ---
|
|
36
38
|
const parsed = parseWecomUrl(url);
|
|
37
39
|
if (!parsed.ok) {
|
|
@@ -72,14 +74,12 @@ export async function runReadCommand(args, context) {
|
|
|
72
74
|
return { exitCode: 1, telemetry: { outcome: "failure", errorKind: "cookie_invalid" } };
|
|
73
75
|
}
|
|
74
76
|
// Load cookies and convert Record<string,string> → CookieEntry[]
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
? Object.entries(rawCookies).map(([name, value]) => ({ name, value }))
|
|
78
|
-
: [];
|
|
77
|
+
let rawCookies = loadCookies(paths.cookiesPath);
|
|
78
|
+
let cookies = toCookieEntries(rawCookies);
|
|
79
79
|
// --- Resolve save dir ---
|
|
80
80
|
const resolvedSaveDir = saveDir ?? paths.defaultSaveDir;
|
|
81
81
|
// --- Export (with local cache) ---
|
|
82
|
-
|
|
82
|
+
let tokValue = rawCookies?.["TOK"] ?? "";
|
|
83
83
|
const cacheDir = paths.cacheDir;
|
|
84
84
|
const noCache = args.includes("--no-cache");
|
|
85
85
|
let result = null;
|
|
@@ -113,17 +113,7 @@ export async function runReadCommand(args, context) {
|
|
|
113
113
|
}
|
|
114
114
|
if (!result) {
|
|
115
115
|
try {
|
|
116
|
-
const fresh = await
|
|
117
|
-
docId,
|
|
118
|
-
docType,
|
|
119
|
-
sourceUrl: url,
|
|
120
|
-
cookies,
|
|
121
|
-
saveDir: resolvedSaveDir,
|
|
122
|
-
onProgress: (pct) => {
|
|
123
|
-
// Progress → stderr only, so stdout stays valid JSON for pipes.
|
|
124
|
-
context.io.stderr(`Exporting… ${Math.max(1, Math.min(99, pct))}%`);
|
|
125
|
-
},
|
|
126
|
-
});
|
|
116
|
+
const fresh = await exportWithCurrentCookies();
|
|
127
117
|
if (!noCache && tokValue) {
|
|
128
118
|
try {
|
|
129
119
|
saveExportToCache({
|
|
@@ -149,19 +139,57 @@ export async function runReadCommand(args, context) {
|
|
|
149
139
|
}
|
|
150
140
|
catch (err) {
|
|
151
141
|
if (err instanceof ExportError) {
|
|
142
|
+
if (isExportAuthError(err)) {
|
|
143
|
+
const retry = await refreshCookiesAndRetryExport();
|
|
144
|
+
if (retry.ok) {
|
|
145
|
+
const fresh = retry.fresh;
|
|
146
|
+
if (!noCache && tokValue) {
|
|
147
|
+
try {
|
|
148
|
+
saveExportToCache({
|
|
149
|
+
cacheDir,
|
|
150
|
+
docId,
|
|
151
|
+
tokValue,
|
|
152
|
+
sourceFilePath: fresh.filePath,
|
|
153
|
+
fileName: fresh.fileName,
|
|
154
|
+
fileSizeBytes: fresh.fileSizeBytes,
|
|
155
|
+
docType,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
// Cache save failure must not break the command.
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
result = {
|
|
163
|
+
filePath: fresh.filePath,
|
|
164
|
+
fileName: fresh.fileName,
|
|
165
|
+
fileSizeBytes: fresh.fileSizeBytes,
|
|
166
|
+
docType,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
return retry.result;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
writeError({
|
|
175
|
+
error: "export_failed",
|
|
176
|
+
errorKind: err.kind,
|
|
177
|
+
message: err.message,
|
|
178
|
+
});
|
|
179
|
+
return {
|
|
180
|
+
exitCode: 1,
|
|
181
|
+
telemetry: { outcome: "failure", errorKind: err.kind, errorMessage: err.message },
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
152
187
|
writeError({
|
|
153
188
|
error: "export_failed",
|
|
154
|
-
|
|
155
|
-
message: err.message,
|
|
189
|
+
message: msg,
|
|
156
190
|
});
|
|
157
|
-
return {
|
|
158
|
-
exitCode: 1,
|
|
159
|
-
telemetry: { outcome: "failure", errorKind: err.kind, errorMessage: err.message },
|
|
160
|
-
};
|
|
191
|
+
return { exitCode: 1, telemetry: { outcome: "failure", errorKind: "unknown", errorMessage: msg } };
|
|
161
192
|
}
|
|
162
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
163
|
-
writeError({ error: "export_failed", message: msg });
|
|
164
|
-
return { exitCode: 1, telemetry: { outcome: "failure", errorKind: "unknown", errorMessage: msg } };
|
|
165
193
|
}
|
|
166
194
|
}
|
|
167
195
|
// --- Build output ---
|
|
@@ -199,4 +227,91 @@ export async function runReadCommand(args, context) {
|
|
|
199
227
|
const outputType = docType === "doc" ? "doc" : "slide";
|
|
200
228
|
context.io.stdout(JSON.stringify({ type: outputType, path: result.filePath }));
|
|
201
229
|
return { exitCode: 0, telemetry: { outcome: "success" } };
|
|
230
|
+
function exportWithCurrentCookies() {
|
|
231
|
+
return exportWecomDoc({
|
|
232
|
+
docId,
|
|
233
|
+
docType,
|
|
234
|
+
sourceUrl,
|
|
235
|
+
cookies,
|
|
236
|
+
saveDir: resolvedSaveDir,
|
|
237
|
+
onProgress: (pct) => {
|
|
238
|
+
// Progress → stderr only, so stdout stays valid JSON for pipes.
|
|
239
|
+
context.io.stderr(`Exporting… ${Math.max(1, Math.min(99, pct))}%`);
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
async function refreshCookiesAndRetryExport() {
|
|
244
|
+
context.io.stderr("WeCom export returned 401/403. Opening browser to refresh cookies, then retrying once...");
|
|
245
|
+
let refreshed;
|
|
246
|
+
try {
|
|
247
|
+
refreshed = await collectWecomCookiesViaBrowser({
|
|
248
|
+
chromeProfilePath: paths.chromeProfilePath,
|
|
249
|
+
io: context.io,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
catch (refreshErr) {
|
|
253
|
+
const msg = refreshErr instanceof Error ? refreshErr.message : String(refreshErr);
|
|
254
|
+
writeError({ error: "cookie_refresh_failed", message: msg });
|
|
255
|
+
return {
|
|
256
|
+
ok: false,
|
|
257
|
+
result: {
|
|
258
|
+
exitCode: 1,
|
|
259
|
+
telemetry: { outcome: "failure", errorKind: "cookie_refresh_failed", errorMessage: msg },
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
saveCookies(paths.cookiesPath, refreshed);
|
|
264
|
+
const valid = await validateCookies(refreshed);
|
|
265
|
+
if (!valid) {
|
|
266
|
+
const msg = "Cookies were refreshed but WeCom still returned 401/403.";
|
|
267
|
+
writeError({ error: "cookie_refresh_invalid", message: msg });
|
|
268
|
+
return {
|
|
269
|
+
ok: false,
|
|
270
|
+
result: {
|
|
271
|
+
exitCode: 1,
|
|
272
|
+
telemetry: { outcome: "failure", errorKind: "cookie_refresh_invalid", errorMessage: msg },
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
rawCookies = refreshed;
|
|
277
|
+
cookies = toCookieEntries(rawCookies);
|
|
278
|
+
tokValue = rawCookies["TOK"] ?? "";
|
|
279
|
+
try {
|
|
280
|
+
return { ok: true, fresh: await exportWithCurrentCookies() };
|
|
281
|
+
}
|
|
282
|
+
catch (retryErr) {
|
|
283
|
+
if (retryErr instanceof ExportError) {
|
|
284
|
+
writeError({
|
|
285
|
+
error: "export_failed",
|
|
286
|
+
errorKind: retryErr.kind,
|
|
287
|
+
message: retryErr.message,
|
|
288
|
+
});
|
|
289
|
+
return {
|
|
290
|
+
ok: false,
|
|
291
|
+
result: {
|
|
292
|
+
exitCode: 1,
|
|
293
|
+
telemetry: {
|
|
294
|
+
outcome: "failure",
|
|
295
|
+
errorKind: retryErr.kind,
|
|
296
|
+
errorMessage: retryErr.message,
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
const msg = retryErr instanceof Error ? retryErr.message : String(retryErr);
|
|
302
|
+
writeError({ error: "export_failed", message: msg });
|
|
303
|
+
return {
|
|
304
|
+
ok: false,
|
|
305
|
+
result: {
|
|
306
|
+
exitCode: 1,
|
|
307
|
+
telemetry: { outcome: "failure", errorKind: "unknown", errorMessage: msg },
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
function toCookieEntries(rawCookies) {
|
|
314
|
+
return rawCookies
|
|
315
|
+
? Object.entries(rawCookies).map(([name, value]) => ({ name, value }))
|
|
316
|
+
: [];
|
|
202
317
|
}
|
|
@@ -161,6 +161,9 @@ export async function runBrowserNoExportRead(options) {
|
|
|
161
161
|
evidence.push("initial-attributed-text");
|
|
162
162
|
if ((extracted?.tables.length ?? 0) > 0)
|
|
163
163
|
evidence.push("table-control-characters");
|
|
164
|
+
if ((extracted?.tables ?? []).some((table) => table.rows.some((row) => row.some((cell) => (cell.colSpan ?? 1) > 1)))) {
|
|
165
|
+
evidence.push("horizontal-colspan-inference");
|
|
166
|
+
}
|
|
164
167
|
if (extracted?.structureHints)
|
|
165
168
|
evidence.push("structure-hints");
|
|
166
169
|
const images = options.saveDir && extracted?.imageUrls
|
|
@@ -192,7 +195,7 @@ export async function runBrowserNoExportRead(options) {
|
|
|
192
195
|
"No readable opendoc initialAttributedText was captured.",
|
|
193
196
|
],
|
|
194
197
|
missing: [
|
|
195
|
-
"table merge ranges",
|
|
198
|
+
"vertical table merge ranges",
|
|
196
199
|
...(images.some((image) => image.status === "downloaded") ? [] : ["image original resources"]),
|
|
197
200
|
"image anchors",
|
|
198
201
|
"floating vs fixed image classification",
|
|
@@ -333,6 +336,9 @@ export function extractTextFromOpendoc(body) {
|
|
|
333
336
|
structureHints,
|
|
334
337
|
warnings: [
|
|
335
338
|
"No-export text is decoded from opendoc initialAttributedText; rich styles are not reconstructed yet.",
|
|
339
|
+
...(tables.some((table) => table.rows.some((row) => row.some((cell) => (cell.colSpan ?? 1) > 1)))
|
|
340
|
+
? ["Horizontal table colSpan is inferred from row cell counts; vertical rowSpan is not reconstructed yet."]
|
|
341
|
+
: []),
|
|
336
342
|
...(imageUrls.length > 0
|
|
337
343
|
? ["Image resource URLs were detected, but image anchors and fixed/floating placement are not reconstructed yet."]
|
|
338
344
|
: []),
|
|
@@ -352,11 +358,22 @@ function extractTablesFromDecodedText(decodedText) {
|
|
|
352
358
|
.filter((text) => text.length > 0)
|
|
353
359
|
.map((text) => ({ text })))
|
|
354
360
|
.filter((row) => row.length > 0);
|
|
361
|
+
inferHorizontalColSpans(rows);
|
|
355
362
|
if (rows.length > 0)
|
|
356
363
|
tables.push({ rows });
|
|
357
364
|
}
|
|
358
365
|
return tables;
|
|
359
366
|
}
|
|
367
|
+
function inferHorizontalColSpans(rows) {
|
|
368
|
+
const maxColumnCount = Math.max(0, ...rows.map((row) => row.length));
|
|
369
|
+
if (maxColumnCount <= 1)
|
|
370
|
+
return;
|
|
371
|
+
for (const row of rows) {
|
|
372
|
+
if (row.length !== 1)
|
|
373
|
+
continue;
|
|
374
|
+
row[0].colSpan = maxColumnCount;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
360
377
|
function normalizeTableCellText(value) {
|
|
361
378
|
return value
|
|
362
379
|
.replace(/\r/g, "")
|
package/dist/wecom/export.js
CHANGED
|
@@ -20,6 +20,27 @@ export class ExportError extends Error {
|
|
|
20
20
|
this.name = "ExportError";
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
|
+
export function isExportAuthError(err) {
|
|
24
|
+
if (containsAuthFailure(err.detail))
|
|
25
|
+
return true;
|
|
26
|
+
return /(?:\b(?:401|403)\b|用户身份认证失败|not authenticated|unauthorized|forbidden)/i.test(err.message);
|
|
27
|
+
}
|
|
28
|
+
function containsAuthFailure(value) {
|
|
29
|
+
if (value === null || value === undefined)
|
|
30
|
+
return false;
|
|
31
|
+
if (typeof value === "number")
|
|
32
|
+
return value === 401 || value === 403;
|
|
33
|
+
if (typeof value === "string") {
|
|
34
|
+
return /(?:\b(?:401|403)\b|用户身份认证失败|not authenticated|unauthorized|forbidden)/i.test(value);
|
|
35
|
+
}
|
|
36
|
+
if (typeof value !== "object")
|
|
37
|
+
return false;
|
|
38
|
+
for (const entry of Object.values(value)) {
|
|
39
|
+
if (containsAuthFailure(entry))
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
23
44
|
// ---------------------------------------------------------------------------
|
|
24
45
|
// Internal helpers
|
|
25
46
|
// ---------------------------------------------------------------------------
|
package/package.json
CHANGED
|
@@ -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.
|
|
10
|
+
This skill matches **wonder 0.0.20**.
|
|
11
11
|
|
|
12
|
-
Once the CLI is installed in Step 1, run `wonder --version`. If the output does not equal `0.0.
|
|
12
|
+
Once the CLI is installed in Step 1, run `wonder --version`. If the output does not equal `0.0.20`, 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
|
|
|
@@ -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.
|
|
13
|
+
This skill matches **wonder 0.0.20**.
|
|
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.
|
|
16
|
+
CLI version differs from `0.0.20`, stop and ask the user to run
|
|
17
17
|
`/update-5mghost-wonder`, then start a fresh AI session.
|
|
18
18
|
|
|
19
19
|
## Hard Rules
|
|
@@ -38,10 +38,11 @@ wonder browser read <url> --save /tmp/wonder-browser-read
|
|
|
38
38
|
|
|
39
39
|
This currently attempts text extraction from the browser `opendoc`
|
|
40
40
|
`initialAttributedText`, decodes simple table cells from WeCom table control
|
|
41
|
-
characters,
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
and
|
|
41
|
+
characters, infers horizontal `colSpan` when a single-cell row spans the
|
|
42
|
+
table's max row width, downloads detected image resources when `--save` is
|
|
43
|
+
present, and returns safe `structureHints` for `htmlData`,
|
|
44
|
+
`initialAttributedText`, and the full `opendoc` body. Treat it as `partial`
|
|
45
|
+
unless it also proves vertical merge ranges and image anchors.
|
|
45
46
|
|
|
46
47
|
3. If the read is insufficient, run the evidence probe:
|
|
47
48
|
|
|
@@ -63,11 +64,12 @@ Use `--headed` only when login/debug visibility is needed.
|
|
|
63
64
|
- screenshots only as a fallback or visual cross-check
|
|
64
65
|
6. For tables, specifically look for:
|
|
65
66
|
- `tables[].rows[].text` for simple table cell content
|
|
67
|
+
- `tables[].rows[].colSpan` for inferred horizontal merged cells
|
|
66
68
|
- `structureHints.*.hasTableSignals` and `hasMergeSignals`
|
|
67
69
|
- `structureHints.htmlData.hasTableMarkup`
|
|
68
70
|
- cell coordinates
|
|
69
71
|
- displayed text
|
|
70
|
-
- merge ranges
|
|
72
|
+
- vertical merge ranges / `rowSpan`
|
|
71
73
|
- row/column sizes if available
|
|
72
74
|
- fixed cell images
|
|
73
75
|
- floating images with position, size, and anchor
|
|
@@ -91,7 +93,8 @@ Return a concise result:
|
|
|
91
93
|
"tables": [
|
|
92
94
|
{
|
|
93
95
|
"rows": [
|
|
94
|
-
[{ "text": "
|
|
96
|
+
[{ "text": "A1B1", "colSpan": 2 }],
|
|
97
|
+
[{ "text": "A2" }, { "text": "B2" }]
|
|
95
98
|
]
|
|
96
99
|
}
|
|
97
100
|
],
|
|
@@ -41,7 +41,8 @@ The goal is lossless structured data. Verify each field independently:
|
|
|
41
41
|
|
|
42
42
|
- text
|
|
43
43
|
- table cell coordinates
|
|
44
|
-
-
|
|
44
|
+
- horizontal `colSpan`
|
|
45
|
+
- vertical merge ranges / `rowSpan`
|
|
45
46
|
- image original resources or URLs
|
|
46
47
|
- image anchors, positions, and sizes
|
|
47
48
|
- floating vs fixed cell images
|