baiduwangpan-cli 1.0.1 → 1.0.2

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/bin/bdp.js CHANGED
@@ -52,12 +52,38 @@ function output(data, jsonFlag, formatter) {
52
52
  }
53
53
  }
54
54
 
55
+ function emitJson(data, args) {
56
+ const json = JSON.stringify(data, null, 2);
57
+ if (args.flags.jsonFile) {
58
+ require("fs").writeFileSync(args.flags.jsonFile, json, "utf-8");
59
+ console.log(args.flags.jsonFile);
60
+ } else {
61
+ console.log(json);
62
+ }
63
+ }
64
+
65
+ function verboseLog(args, msg) {
66
+ if (args.flags.verbose) process.stderr.write(`[verbose] ${msg}\n`);
67
+ }
68
+
55
69
  function parseArgs(argv) {
56
70
  const opts = { _: [], flags: {} };
57
71
  for (let i = 0; i < argv.length; i++) {
58
72
  const arg = argv[i];
59
73
  if (arg === "--json") {
60
74
  opts.flags.json = true;
75
+ } else if (arg === "--legacy-json") {
76
+ opts.flags.legacyJson = true;
77
+ } else if (arg === "--json-file") {
78
+ opts.flags.jsonFile = argv[++i];
79
+ } else if (arg === "--verbose") {
80
+ opts.flags.verbose = true;
81
+ } else if (arg === "--unique") {
82
+ opts.flags.unique = true;
83
+ } else if (arg === "--no-unique") {
84
+ opts.flags.unique = false;
85
+ } else if (arg === "--all") {
86
+ opts.flags.all = true;
61
87
  } else if (arg === "-n" || arg === "--lines") {
62
88
  opts.flags.n = parseInt(argv[++i], 10) || 20;
63
89
  } else if (arg === "-p" || arg === "--path") {
@@ -76,6 +102,22 @@ function parseArgs(argv) {
76
102
  opts.flags.stoken = argv[++i];
77
103
  } else if (arg === "--page") {
78
104
  opts.flags.page = parseInt(argv[++i], 10) || 1;
105
+ } else if (arg === "--page-size") {
106
+ opts.flags.pageSize = parseInt(argv[++i], 10) || 50;
107
+ } else if (arg === "--limit") {
108
+ opts.flags.limit = parseInt(argv[++i], 10) || 50;
109
+ } else if (arg === "--concurrency") {
110
+ opts.flags.concurrency = parseInt(argv[++i], 10) || 4;
111
+ } else if (arg === "--depth") {
112
+ opts.flags.depth = parseInt(argv[++i], 10) || 2;
113
+ } else if (arg === "--max-nodes") {
114
+ opts.flags.maxNodes = parseInt(argv[++i], 10) || 2000;
115
+ } else if (arg === "--from-uk") {
116
+ opts.flags.fromUk = argv[++i];
117
+ } else if (arg === "--msg-id") {
118
+ opts.flags.msgId = argv[++i];
119
+ } else if (arg === "--parent-fs-id") {
120
+ opts.flags.parentFsId = argv[++i];
79
121
  } else if (arg.startsWith("-")) {
80
122
  opts.flags[arg.replace(/^-+/, "")] = true;
81
123
  } else {
@@ -350,29 +392,119 @@ cmds.gshares = async (args, json) => {
350
392
  cmds.gls = async (args, json) => {
351
393
  const gid = args._[0];
352
394
  const fsId = args._[1];
353
- if (!gid || !fsId) { console.error("Usage: bdp gls <gid> <fs_id>"); process.exit(1); }
354
- const result = await group.listFiles(gid, fsId, { page: args.flags.page || 1 });
355
- output(result, json, (d) => {
356
- console.log(`${d.files.length} items${d.hasMore ? " (has more — use --page)" : ""}:\n`);
357
- d.files.forEach((f) => {
395
+ if (!gid || !fsId) { console.error("Usage: bdp gls <gid> <fs_id> [--page N] [--page-size N]"); process.exit(1); }
396
+
397
+ const opts = {
398
+ page: args.flags.page || 1,
399
+ pageSize: args.flags.pageSize || 50,
400
+ };
401
+ if (args.flags.fromUk) opts.fromUk = args.flags.fromUk;
402
+ if (args.flags.msgId) opts.msgId = args.flags.msgId;
403
+ if (args.flags.parentFsId) opts.parentFsId = args.flags.parentFsId;
404
+
405
+ verboseLog(args, `gls gid=${gid} fsId=${fsId} page=${opts.page} pageSize=${opts.pageSize}`);
406
+ const result = await group.listFiles(gid, fsId, opts);
407
+
408
+ if (args.flags.legacyJson) {
409
+ emitJson(result.files, args);
410
+ return;
411
+ }
412
+
413
+ if (result.fallback) {
414
+ verboseLog(args, `fallback: ${result.fallback.reason} -> ${result.fallback.resolvedFsId} (${result.fallback.level})`);
415
+ }
416
+
417
+ if (args.flags.jsonFile || json) {
418
+ emitJson(result, args);
419
+ } else {
420
+ if (result.fallback) {
421
+ console.log(`⚠️ fallback: ${result.fallback.reason} — 已回退到 ${result.fallback.resolvedFsId} (${result.fallback.level})`);
422
+ }
423
+ console.log(`${result.files.length} items${result.hasMore ? " (has more — use --page)" : ""}:\n`);
424
+ result.files.forEach((f) => {
358
425
  const dir = f.isDir ? "[DIR] " : " ";
359
426
  console.log(` ${dir}${f.name} ${formatSize(f.size)} fs_id:${f.fsId}`);
360
427
  });
361
- });
428
+ }
429
+ };
430
+
431
+ cmds.gtree = async (args, json) => {
432
+ const gid = args._[0];
433
+ if (!gid) { console.error("Usage: bdp gtree <gid> [--depth N] [--concurrency N] [--max-nodes N]"); process.exit(1); }
434
+
435
+ const opts = {
436
+ depth: args.flags.depth || 2,
437
+ concurrency: args.flags.concurrency || 4,
438
+ maxNodes: args.flags.maxNodes || 2000,
439
+ };
440
+ verboseLog(args, `gtree gid=${gid} depth=${opts.depth} concurrency=${opts.concurrency} maxNodes=${opts.maxNodes}`);
441
+ const result = await group.treeFiles(gid, opts);
442
+
443
+ if (args.flags.jsonFile || json) {
444
+ emitJson(result, args);
445
+ } else {
446
+ console.log(`${result.tree.length} nodes (depth ${result.depth}, ${result.failed.length} failed${result.truncated ? ", truncated" : ""}):\n`);
447
+ result.tree.forEach((n) => {
448
+ const dir = n.isDir ? "[DIR] " : " ";
449
+ console.log(` ${dir}${n.path || n.name} ${formatSize(n.size)} fs_id:${n.fsId}`);
450
+ });
451
+ }
362
452
  };
363
453
 
364
454
  cmds.gsearch = async (args, json) => {
365
455
  const gid = args._[0];
366
456
  const keyword = args._[1];
367
- if (!gid || !keyword) { console.error("Usage: bdp gsearch <gid> <keyword>"); process.exit(1); }
368
- const results = await group.searchFiles(gid, keyword);
369
- output(results, json, (list) => {
370
- console.log(`${list.length} matches for "${keyword}":\n`);
371
- list.forEach((r) => {
457
+ if (!gid || !keyword) { console.error("Usage: bdp gsearch <gid> <keyword> [--page N] [--limit N]"); process.exit(1); }
458
+
459
+ const opts = {
460
+ page: args.flags.page || 1,
461
+ limit: args.flags.limit || 50,
462
+ concurrency: args.flags.concurrency || 4,
463
+ unique: args.flags.unique !== false,
464
+ all: args.flags.all === true,
465
+ depth: args.flags.depth || 1,
466
+ };
467
+
468
+ verboseLog(args, `gsearch gid=${gid} keyword="${keyword}" page=${opts.page} limit=${opts.limit} concurrency=${opts.concurrency} unique=${opts.unique} all=${opts.all}`);
469
+ const result = await group.searchFiles(gid, keyword, opts);
470
+
471
+ if (result.partial) {
472
+ verboseLog(args, `partial: ${result.failedShares}/${result.totalShares} shares failed`);
473
+ }
474
+
475
+ if (args.flags.legacyJson) {
476
+ emitJson(result.results, args);
477
+ return;
478
+ }
479
+
480
+ if (args.flags.jsonFile || json) {
481
+ emitJson(result, args);
482
+ } else {
483
+ console.log(`${result.returned} matches for "${keyword}"${result.hasMore ? " (has more — use --page " + (result.nextPage || "?") + ")" : ""}:\n`);
484
+ result.results.forEach((r) => {
372
485
  const dir = r.isDir ? "[DIR] " : " ";
373
486
  console.log(` ${dir}${r.path || r.name} ${formatSize(r.size)} fs_id:${r.fsId}`);
374
487
  });
375
- });
488
+ if (result.partial) {
489
+ console.error(`[verbose] ${result.failedShares} shares failed (partial result)`);
490
+ }
491
+ }
492
+ };
493
+
494
+ cmds.error = async (args, json) => {
495
+ const code = args._[0] || "-3";
496
+ const ERRORS = {
497
+ "-3": "群分享 API 拒绝请求。已观察到大目录、错误的 msgId/fromUk、过期分享都可能触发。\n" +
498
+ "处理:减小 --page-size(自动重试 50→20→10),或携带 gsearch 返回的 fromUk/msgId/parentFsId 信息重试;\n" +
499
+ " 若回退到父目录(fallback level=parent),Agent 应从父目录换路径继续遍历。",
500
+ "-6": "接口鉴权失败。通常为 access_token/cookie 失效,请重新执行 bdp login。",
501
+ };
502
+ const msg = ERRORS[code] || `未知错误码 ${code},请提交 Issue: https://github.com/NkAntony777/baiduwangpan-cli/issues`;
503
+ if (args.flags.jsonFile || json) {
504
+ emitJson({ code, description: msg }, args);
505
+ } else {
506
+ console.log(msg);
507
+ }
376
508
  };
377
509
 
378
510
  // ── Main ───────────────────────────────────────────────
@@ -398,8 +530,13 @@ PAN FILE OPERATIONS (全盘)
398
530
  GROUP CHAT OPERATIONS (群聊)
399
531
  groups List all groups
400
532
  gshares <gid> List share libraries in group
533
+ gtree <gid> Build directory tree (BFS)
534
+ [--depth N] [--concurrency N] [--max-nodes N]
401
535
  gls <gid> <fs_id> Browse files in a share
536
+ [--page N] [--page-size N] [--from-uk X] [--msg-id Y] [--parent-fs-id Z]
402
537
  gsearch <gid> <keyword> Search group file names
538
+ [--page N] [--limit N] [--depth N] [--concurrency N] [--no-unique] [--all]
539
+ error <code> Explain an error code (e.g. -3)
403
540
 
404
541
  CONFIGURATION
405
542
  login Browser QR login (auto-detect cookies)
@@ -409,12 +546,22 @@ CONFIGURATION
409
546
 
410
547
  OPTIONS
411
548
  --json Output JSON (for Agent consumption)
549
+ --json-file <path> Write JSON to file (UTF-8, bypasses console encoding)
550
+ --legacy-json Output bare array (gls/gsearch legacy format)
551
+ --verbose Write progress/log to stderr only
412
552
  -n <N> Number of lines (head/tail)
413
553
  -p <path> Search path (search)
414
554
  -i Ignore case (grep)
415
555
  -N Show line numbers (cat/grep)
416
556
  -o <dir> Output directory (get)
417
- --page <N> Page number (gls)
557
+ --page <N> Page number (gls/gsearch)
558
+ --page-size <N> Page size for gls (default 50, max 100)
559
+ --limit <N> Page size for gsearch (default 50)
560
+ --depth <N> Recursive search/tree depth (gsearch default 1, gtree default 2)
561
+ --max-nodes <N> Tree node cap (gtree default 2000)
562
+ --concurrency <N> Parallel share scans for gsearch/gtree (default 4)
563
+ --no-unique Keep duplicates from different msgId (gsearch)
564
+ --all Fetch all results, no paging (gsearch, not Agent default)
418
565
 
419
566
  EXAMPLES
420
567
  bdp login --bduss abc123 --stoken def456
@@ -424,7 +571,10 @@ EXAMPLES
424
571
  bdp groups --json
425
572
  bdp gshares 539478953581833690
426
573
  bdp gls 539478953581833690 742474845517885 --json
427
- bdp gsearch 539478953581833690 "倪海厦"`;
574
+ bdp gls 539478953581833690 292608024165826 --from-uk 2642611875 --msg-id 5069931974377329661 --json
575
+ bdp gsearch 539478953581833690 "倪海厦" --limit 20 --json
576
+ bdp gsearch 539478953581833690 "古籍" --page 2 --limit 20 --json-file result.json
577
+ bdp error -3`;
428
578
 
429
579
  async function main() {
430
580
  const argv = process.argv.slice(2);
@@ -439,6 +589,7 @@ async function main() {
439
589
  process.exit(0);
440
590
  }
441
591
 
592
+ let exitCode = 0;
442
593
  try {
443
594
  await handler(args, json);
444
595
  } catch (e) {
@@ -447,12 +598,13 @@ async function main() {
447
598
  } else {
448
599
  console.error(`[ERROR] ${e.message}`);
449
600
  }
450
- process.exitCode = 1;
601
+ exitCode = 1;
451
602
  } finally {
452
603
  try {
453
604
  require("../lib/browser-login").disconnectBrowserSession();
454
605
  } catch {}
455
606
  }
607
+ process.exit(exitCode);
456
608
  }
457
609
 
458
610
  main();
@@ -233,8 +233,13 @@ async function openPersistentSession({ browser, profileDir, port, minimized }) {
233
233
  proc = launchBrowser(browser, profileDir, port, { minimized });
234
234
  await waitForDevTools(port, 15000);
235
235
  }
236
- const { cdp, tab } = await connectPanTab(port);
237
- return { browser, profileDir, port, proc, cdp, tab };
236
+ try {
237
+ const { cdp, tab } = await connectPanTab(port);
238
+ return { browser, profileDir, port, proc, cdp, tab };
239
+ } catch (e) {
240
+ if (proc) killBrowser(proc);
241
+ throw e;
242
+ }
238
243
  }
239
244
 
240
245
  async function getBrowserSession(settings = {}) {
@@ -379,7 +384,8 @@ async function browserLogin({
379
384
 
380
385
  function scheduleDisconnect() {
381
386
  if (disconnectTimer) clearTimeout(disconnectTimer);
382
- disconnectTimer = setTimeout(disconnectBrowserSession, 500);
387
+ // 空闲 30 秒后断开浏览器会话(批处理请求间隔通常 <30s)
388
+ disconnectTimer = setTimeout(disconnectBrowserSession, 30000);
383
389
  disconnectTimer.unref();
384
390
  }
385
391
 
package/lib/group.js CHANGED
@@ -1,6 +1,23 @@
1
1
  const http = require("./http");
2
2
  const { getBdstoken } = http;
3
3
 
4
+ // ── 工具 ──────────────────────────────────────────────
5
+
6
+ function toStr(v) {
7
+ if (v === null || v === undefined) return "";
8
+ return String(v);
9
+ }
10
+
11
+ class ShareApiError extends Error {
12
+ constructor(errno, message) {
13
+ super(message);
14
+ this.errno = errno;
15
+ this.name = "ShareApiError";
16
+ }
17
+ }
18
+
19
+ // ── 群组列表 ──────────────────────────────────────────
20
+
4
21
  async function listGroups() {
5
22
  const bdstoken = await getBdstoken();
6
23
  if (!bdstoken) throw new Error("Cannot get bdstoken — check BDUSS/STOKEN config");
@@ -12,19 +29,21 @@ async function listGroups() {
12
29
  if (data.errno !== 0) throw new Error(`API error: errno=${data.errno}`);
13
30
 
14
31
  return (data.records || []).map((g) => ({
15
- gid: g.gid,
32
+ gid: toStr(g.gid),
16
33
  name: g.name,
17
- gnum: g.gnum,
34
+ gnum: toStr(g.gnum),
18
35
  description: g.gdesc || "",
19
- type: g.type,
36
+ type: toStr(g.type),
20
37
  created: g.ctime,
21
38
  }));
22
39
  }
23
40
 
41
+ // ── 群内分享库 ────────────────────────────────────────
42
+
24
43
  async function listShares(gid) {
25
44
  const bdstoken = await getBdstoken();
26
45
  const data = await http.webJson(
27
- `${http.PAN_BASE}/mbox/group/listshare?clienttype=0&app_id=${http.APP_ID}&web=1&type=2&gid=${gid}&limit=50&desc=1&bdstoken=${bdstoken}`,
46
+ `${http.PAN_BASE}/mbox/group/listshare?clienttype=0&app_id=${http.APP_ID}&web=1&type=2&gid=${toStr(gid)}&limit=50&desc=1&bdstoken=${bdstoken}`,
28
47
  { method: "POST", body: "" }
29
48
  );
30
49
 
@@ -34,90 +53,360 @@ async function listShares(gid) {
34
53
  for (const msg of data.records?.msg_list || []) {
35
54
  for (const f of msg.file_list || []) {
36
55
  shares.push({
37
- msgId: msg.msg_id,
38
- fromUk: msg.uk,
56
+ msgId: toStr(msg.msg_id),
57
+ fromUk: toStr(msg.uk),
39
58
  fromUser: msg.uname,
40
59
  name: decodeURIComponent(f.path || f.server_filename || ""),
41
- fsId: f.fs_id,
60
+ fsId: toStr(f.fs_id),
42
61
  isDir: f.isdir === "1",
43
- size: parseInt(f.size || "0"),
62
+ size: parseInt(f.size || "0", 10),
44
63
  });
45
64
  }
46
65
  }
47
66
  return shares;
48
67
  }
49
68
 
50
- async function listFiles(gid, fsId, options = {}) {
51
- const page = options.page || 1;
52
- const num = options.num || 100;
53
-
54
- // First get the fromUk and msgId for this fsId
55
- let { fromUk, msgId } = options;
56
- if (!fromUk || !msgId) {
57
- const shares = await listShares(gid);
58
- const match = shares.find((s) => s.fsId === fsId);
59
- if (match) {
60
- fromUk = match.fromUk;
61
- msgId = match.msgId;
62
- } else if (shares.length > 0) {
63
- fromUk = shares[0].fromUk;
64
- msgId = shares[0].msgId;
65
- } else {
66
- throw new Error("Cannot find share info for this fsId");
67
- }
68
- }
69
+ // ── 底层 shareinfo 请求 ───────────────────────────────
69
70
 
71
+ async function shareInfoRequest(gidS, fsIdS, fromUkS, msgIdS, page, pageSize) {
70
72
  const bdstoken = await getBdstoken();
71
- const data = await http.webJson(
72
- `${http.PAN_BASE}/mbox/msg/shareinfo?type=2&from_uk=${fromUk}&msg_id=${msgId}&to_uk=0&num=${num}&page=${page}&fs_id=${fsId}&gid=${gid}&clienttype=0&app_id=${http.APP_ID}&web=1&bdstoken=${bdstoken}`,
73
- { method: "POST", body: "" }
74
- );
73
+ const url =
74
+ `${http.PAN_BASE}/mbox/msg/shareinfo?type=2&from_uk=${fromUkS}&msg_id=${msgIdS}` +
75
+ `&to_uk=0&num=${pageSize}&page=${page}&fs_id=${fsIdS}&gid=${gidS}` +
76
+ `&clienttype=0&app_id=${http.APP_ID}&web=1&bdstoken=${bdstoken}`;
75
77
 
76
- if (data.errno !== 0) throw new Error(`API error: errno=${data.errno}`);
78
+ const data = await http.webJson(url, { method: "POST", body: "" });
79
+
80
+ if (data.errno !== 0) {
81
+ throw new ShareApiError(data.errno, `API error: errno=${data.errno}`);
82
+ }
77
83
 
78
84
  const files = (Array.isArray(data.records) ? data.records : []).map((f) => ({
79
85
  name: f.server_filename || "",
80
86
  path: decodeURIComponent(f.path || ""),
81
87
  isDir: f.isdir === 1 || f.isdir === "1",
82
- size: parseInt(f.size || "0"),
83
- fsId: f.fs_id,
88
+ size: parseInt(f.size || "0", 10),
89
+ fsId: toStr(f.fs_id),
84
90
  md5: f.md5 || "",
85
- category: parseInt(f.category || "0"),
86
- ctime: f.server_ctime || "",
87
- mtime: f.server_mtime || "",
91
+ category: parseInt(f.category || "0", 10),
92
+ ctime: toStr(f.server_ctime || ""),
93
+ mtime: toStr(f.server_mtime || ""),
94
+ parentFsId: fsIdS,
95
+ fromUk: fromUkS,
96
+ msgId: msgIdS,
88
97
  }));
89
98
 
90
- return { files, hasMore: data.has_more === 1, page };
99
+ return { files, hasMore: data.has_more === 1, page, pageSize };
100
+ }
101
+
102
+ // ── 列出分享库内容 (gls) ──────────────────────────────
103
+
104
+ async function listFiles(gid, fsId, options = {}) {
105
+ const gidS = toStr(gid);
106
+ const fsIdS = toStr(fsId);
107
+ const parentFsId = toStr(options.parentFsId || "");
108
+ const page = options.page || 1;
109
+ let pageSize = options.pageSize || 50;
110
+ pageSize = Math.min(Math.max(1, pageSize), 100);
111
+
112
+ // 来源解析:显式传入优先;否则仅在顶层分享中精确匹配(禁止"用第一个分享"兜底)
113
+ let fromUkS = toStr(options.fromUk || "");
114
+ let msgIdS = toStr(options.msgId || "");
115
+
116
+ if (!fromUkS || !msgIdS) {
117
+ const shares = await listShares(gidS);
118
+ const match = shares.find((s) => s.fsId === fsIdS);
119
+ if (!match) {
120
+ throw new Error(
121
+ `Cannot determine fromUk/msgId for fsId ${fsIdS} (not a top-level share). ` +
122
+ `Pass --from-uk and --msg-id explicitly.`
123
+ );
124
+ }
125
+ fromUkS = match.fromUk;
126
+ msgIdS = match.msgId;
127
+ }
128
+
129
+ // errno=-3 时依次降 page-size: 50 → 20 → 10
130
+ const attempts = [...new Set([pageSize, 50, 20, 10].filter((n) => n <= pageSize))];
131
+ let lastErrno = null;
132
+
133
+ for (const size of attempts) {
134
+ try {
135
+ const result = await shareInfoRequest(gidS, fsIdS, fromUkS, msgIdS, page, size);
136
+ return {
137
+ files: result.files,
138
+ hasMore: result.hasMore,
139
+ page,
140
+ pageSize: size,
141
+ fromUk: fromUkS,
142
+ msgId: msgIdS,
143
+ fallback: null,
144
+ };
145
+ } catch (e) {
146
+ if (e instanceof ShareApiError && e.errno === -3) {
147
+ lastErrno = -3;
148
+ continue; // 尝试更小的 page-size
149
+ }
150
+ throw e;
151
+ }
152
+ }
153
+
154
+ // 全部 page-size 都失败(errno=-3)→ 回退
155
+ if (parentFsId) {
156
+ // 回退到父目录,展示兄弟内容帮助 Agent 换路径
157
+ try {
158
+ const parent = await shareInfoRequest(gidS, toStr(parentFsId), fromUkS, msgIdS, 1, 50);
159
+ return {
160
+ files: parent.files,
161
+ hasMore: parent.hasMore,
162
+ page: 1,
163
+ pageSize: 50,
164
+ fromUk: fromUkS,
165
+ msgId: msgIdS,
166
+ fallback: {
167
+ reason: "errno=-3",
168
+ requestedFsId: fsIdS,
169
+ resolvedFsId: toStr(parentFsId),
170
+ level: "parent",
171
+ },
172
+ };
173
+ } catch (e) {
174
+ throw new ShareApiError(
175
+ -3,
176
+ `shareinfo rejected for fsId ${fsIdS} (errno=-3), and parent fallback also failed. ` +
177
+ `The share may be expired or the directory may be too large for the API.`
178
+ );
179
+ }
180
+ }
181
+
182
+ // 目标本身是顶层分享 → 退回 gshares 结果
183
+ try {
184
+ const shares = await listShares(gidS);
185
+ const match = shares.find((s) => s.fsId === fsIdS);
186
+ if (match) {
187
+ return {
188
+ files: [{
189
+ name: match.name,
190
+ path: match.name,
191
+ isDir: match.isDir,
192
+ size: match.size,
193
+ fsId: match.fsId,
194
+ md5: "",
195
+ category: 0,
196
+ ctime: "",
197
+ mtime: "",
198
+ parentFsId: "",
199
+ fromUk: match.fromUk,
200
+ msgId: match.msgId,
201
+ }],
202
+ hasMore: false,
203
+ page: 1,
204
+ pageSize: 50,
205
+ fromUk: fromUkS,
206
+ msgId: msgIdS,
207
+ fallback: {
208
+ reason: "errno=-3",
209
+ requestedFsId: fsIdS,
210
+ resolvedFsId: fsIdS,
211
+ level: "group-shares",
212
+ },
213
+ };
214
+ }
215
+ } catch {}
216
+
217
+ throw new ShareApiError(
218
+ -3,
219
+ `shareinfo rejected for fsId ${fsIdS} (errno=-3). ` +
220
+ `群分享 API 拒绝请求:已观察到大目录、错误的 msgId/fromUk、过期分享都可能触发。` +
221
+ `请减小 --page-size,或携带 gsearch 返回的 fromUk/msgId/parentFsId 信息重试。`
222
+ );
223
+ }
224
+
225
+ // ── 目录树 (gtree) ────────────────────────────────────
226
+
227
+ async function treeFiles(gid, options = {}) {
228
+ const gidS = toStr(gid);
229
+ const maxDepth = Math.min(Math.max(1, options.depth || 2), 5);
230
+ const concurrency = Math.min(Math.max(1, options.concurrency || 4), 8);
231
+ const maxNodes = options.maxNodes || 2000;
232
+
233
+ const shares = await listShares(gidS);
234
+ const tree = [];
235
+ const failed = [];
236
+
237
+ // BFS: queue 元素 { node, depth, share }
238
+ const queue = shares.map((s) => ({ node: { ...s, parentFsId: "" }, depth: 1, share: s }));
239
+ let scanned = 0;
240
+
241
+ // 逐层 BFS,层内并发
242
+ for (let depth = 1; depth <= maxDepth; depth++) {
243
+ const level = queue.filter((q) => q.depth === depth);
244
+ if (level.length === 0) break;
245
+
246
+ const results = await Promise.all(
247
+ level.map(async (entry) => {
248
+ const { node, share } = entry;
249
+ if (!node.isDir || node.size > 0) return null; // 非目录不进队列
250
+ scanned++;
251
+ try {
252
+ const { files } = await shareInfoRequest(gidS, node.fsId, share.fromUk, share.msgId, 1, 100);
253
+ return files.map((f) => ({
254
+ node: {
255
+ name: f.name,
256
+ path: f.path,
257
+ isDir: f.isDir,
258
+ size: f.size,
259
+ fsId: f.fsId,
260
+ md5: f.md5,
261
+ category: f.category,
262
+ parentFsId: f.parentFsId,
263
+ fromUk: f.fromUk,
264
+ msgId: f.msgId,
265
+ fromUser: share.fromUser,
266
+ },
267
+ depth: depth + 1,
268
+ share,
269
+ }));
270
+ } catch {
271
+ failed.push({ fsId: node.fsId, name: node.name });
272
+ return null;
273
+ }
274
+ })
275
+ );
276
+
277
+ for (const list of results) {
278
+ if (!list) continue;
279
+ for (const item of list) {
280
+ tree.push(item.node);
281
+ if (tree.length >= maxNodes) break;
282
+ if (item.depth <= maxDepth && item.node.isDir) queue.push({ node: item.node, depth: item.depth, share: item.share });
283
+ }
284
+ if (tree.length >= maxNodes) break;
285
+ }
286
+ if (tree.length >= maxNodes) break;
287
+ }
288
+
289
+ return { tree, failed, scanned, maxDepth, truncated: tree.length >= maxNodes };
91
290
  }
92
291
 
292
+ // ── 群文件搜索 (gsearch) ──────────────────────────────
293
+
93
294
  async function searchFiles(gid, keyword, options = {}) {
94
- const maxDepth = options.maxDepth || 1;
95
- const results = [];
295
+ const gidS = toStr(gid);
96
296
  const kw = keyword.toLowerCase();
97
297
 
98
- const shares = await listShares(gid);
298
+ const limit = options.limit || 50;
299
+ const page = options.page || 1;
300
+ const concurrency = Math.min(Math.max(1, options.concurrency || 4), 8);
301
+ const unique = options.unique !== false;
302
+ const fetchAll = options.all === true;
303
+ const depth = Math.min(Math.max(1, options.depth || 1), 5); // 默认 1 层(兼容旧行为)
304
+ const startIdx = (page - 1) * limit;
305
+ const stopAt = fetchAll ? Infinity : startIdx + limit + 1; // +1 判断 hasMore
306
+
307
+ const shares = await listShares(gidS);
308
+ const totalShares = shares.length;
309
+
310
+ const seen = new Set();
311
+ const results = [];
312
+ let scannedShares = 0;
313
+ let failedShares = 0;
314
+ let finished = false;
315
+
316
+ const pushIfMatch = (item) => {
317
+ if (finished) return;
318
+ const key = unique ? toStr(item.fsId) : `${toStr(item.fsId)}|${toStr(item.msgId)}`;
319
+ if (unique && seen.has(key)) return;
320
+ seen.add(key);
321
+ results.push(item);
322
+ if (results.length >= stopAt) finished = true;
323
+ };
99
324
 
325
+ // 阶段 1:顶层分享名称匹配(非目录分享在此完成扫描)
100
326
  for (const share of shares) {
327
+ if (finished) break;
328
+ if (!share.isDir) scannedShares++;
101
329
  if (share.name.toLowerCase().includes(kw)) {
102
- results.push({ ...share, group: gid });
330
+ pushIfMatch({
331
+ name: share.name,
332
+ path: share.name,
333
+ isDir: share.isDir,
334
+ size: share.size,
335
+ fsId: share.fsId,
336
+ md5: "",
337
+ category: 0,
338
+ ctime: "",
339
+ mtime: "",
340
+ parentFsId: "",
341
+ fromUk: share.fromUk,
342
+ msgId: share.msgId,
343
+ fromUser: share.fromUser,
344
+ group: gidS,
345
+ });
103
346
  }
347
+ }
348
+
349
+ // 阶段 2:BFS 递归扫描目录(每层 4 个一批并发,默认 depth=1 保持旧行为)
350
+ const dirShares = shares.filter((s) => s.isDir);
351
+ let queue = dirShares.map((s) => ({ fsId: s.fsId, fromUk: s.fromUk, msgId: s.msgId, depth: 1 }));
352
+
353
+ for (let level = 1; level <= depth && queue.length > 0 && !finished; level++) {
354
+ const current = queue;
355
+ queue = [];
104
356
 
105
- if (share.isDir && maxDepth > 0) {
106
- try {
107
- const { files } = await listFiles(gid, share.fsId, {
108
- fromUk: share.fromUk,
109
- msgId: share.msgId,
110
- });
111
- for (const f of files) {
112
- if (f.name.toLowerCase().includes(kw)) {
113
- results.push({ ...f, group: gid });
357
+ for (let i = 0; i < current.length && !finished; i += concurrency) {
358
+ const batch = current.slice(i, i + concurrency);
359
+ const batchResults = await Promise.all(
360
+ batch.map(async (share) => {
361
+ scannedShares++;
362
+ try {
363
+ const { files } = await shareInfoRequest(gidS, share.fsId, share.fromUk, share.msgId, 1, 100);
364
+ const matches = files.filter((f) => f.name.toLowerCase().includes(kw));
365
+ const subDirs = files.filter((f) => f.isDir);
366
+ return { matches, subDirs };
367
+ } catch {
368
+ failedShares++;
369
+ return { matches: [], subDirs: [] };
370
+ }
371
+ })
372
+ );
373
+
374
+ for (const { matches, subDirs } of batchResults) {
375
+ if (finished) break;
376
+ for (const f of matches) {
377
+ pushIfMatch({ ...f, group: gidS });
378
+ if (finished) break;
379
+ }
380
+ // 收集下一层待扫描目录
381
+ if (level < depth && !finished) {
382
+ for (const d of subDirs) {
383
+ queue.push({ fsId: d.fsId, fromUk: d.fromUk, msgId: d.msgId, depth: level + 1 });
114
384
  }
115
385
  }
116
- } catch {}
386
+ }
117
387
  }
118
388
  }
119
389
 
120
- return results;
390
+ const complete = scannedShares >= totalShares;
391
+ const hasMore = !fetchAll && results.length > startIdx + limit;
392
+ const pageSlice = fetchAll ? results : results.slice(startIdx, startIdx + limit);
393
+
394
+ return {
395
+ results: pageSlice,
396
+ page,
397
+ pageSize: limit,
398
+ returned: pageSlice.length,
399
+ total: null,
400
+ hasMore,
401
+ nextPage: hasMore ? page + 1 : null,
402
+ unique,
403
+ complete,
404
+ partial: failedShares > 0,
405
+ scannedShares,
406
+ totalShares,
407
+ failedShares,
408
+ depth,
409
+ };
121
410
  }
122
411
 
123
- module.exports = { listGroups, listShares, listFiles, searchFiles };
412
+ module.exports = { listGroups, listShares, listFiles, treeFiles, searchFiles, ShareApiError };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "baiduwangpan-cli",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "百度网盘 CLI — 全盘读写 · 免下载读取文件内容 · 群聊文件浏览 (Agent 友好)",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -38,7 +38,7 @@
38
38
  ],
39
39
  "repository": {
40
40
  "type": "git",
41
- "url": "https://github.com/NkAntony777/baiduwangpan-cli.git"
41
+ "url": "git+https://github.com/NkAntony777/baiduwangpan-cli.git"
42
42
  },
43
43
  "homepage": "https://github.com/NkAntony777/baiduwangpan-cli",
44
44
  "bugs": {