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 +167 -15
- package/lib/browser-login.js +9 -3
- package/lib/group.js +343 -54
- package/package.json +2 -2
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
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
|
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
|
-
|
|
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();
|
package/lib/browser-login.js
CHANGED
|
@@ -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
|
-
|
|
237
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
72
|
-
`${http.PAN_BASE}/mbox/msg/shareinfo?type=2&from_uk=${
|
|
73
|
-
{
|
|
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
|
-
|
|
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
|
|
95
|
-
const results = [];
|
|
295
|
+
const gidS = toStr(gid);
|
|
96
296
|
const kw = keyword.toLowerCase();
|
|
97
297
|
|
|
98
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
}
|
|
386
|
+
}
|
|
117
387
|
}
|
|
118
388
|
}
|
|
119
389
|
|
|
120
|
-
|
|
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.
|
|
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": {
|