@mkterswingman/5mghost-wonder 0.0.19 → 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.
@@ -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
- const rawCookies = loadCookies(paths.cookiesPath);
76
- const cookies = rawCookies
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
- const tokValue = rawCookies?.["TOK"] ?? "";
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 exportWecomDoc({
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
- errorKind: err.kind,
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
  }
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mkterswingman/5mghost-wonder",
3
- "version": "0.0.19",
3
+ "version": "0.0.20",
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.19**.
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.19`, 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.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
 
@@ -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.19**.
10
+ This skill matches **wonder 0.0.20**.
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.19**.
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.19`, stop and ask the user to run
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