baiduwangpan-cli 1.1.3 → 1.1.5

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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  # baiduwangpan-cli
4
4
 
5
- **百度网盘 CLI — 全盘读写 · 免下载读取 · 群聊文件浏览**
5
+ **百度网盘 CLI — 全盘读写 · 免下载读取 · 群聊文件浏览/下载**
6
6
 
7
7
  专为 AI Agent 设计的百度网盘命令行工具
8
8
 
@@ -21,7 +21,7 @@
21
21
 
22
22
  - **🗂️ 全盘访问** — 不受 `/apps/` 目录限制,读写网盘任意路径
23
23
  - **📄 免下载读取** — 通过 HTTP Range 直接读取文件内容到 stdout,无需下载整个文件
24
- - **👥 群聊文件浏览** — 逆向 mbox API,列出群组、浏览群内分享库、搜索群文件
24
+ - **👥 群聊文件浏览 + 下载** — 逆向 mbox API,列出群组、浏览群内分享库、搜索群文件,并支持**直接下载群文件到本地**(免转存)
25
25
  - **🤖 Agent 友好** — 所有命令支持 `--json` 结构化输出,零解析成本
26
26
  - **🔧 双模式** — 既是 CLI 工具,也可作为 Node.js 库 `require()` 使用
27
27
  - **🔐 凭证安全** — 配置存储在 `~/.bdp/config.json`,不写入代码或日志
@@ -43,6 +43,7 @@
43
43
  | **群内分享库** | `bdp gshares <gid>` | 顶层分享目录 |
44
44
  | **浏览群文件** | `bdp gls <gid> <fs_id>` | 分页 + 递归 |
45
45
  | **搜索群文件** | `bdp gsearch <gid> "关键词"` | 按文件名搜索(缓存命中秒回) |
46
+ | **下载群文件** | `bdp gdownload <gid> <fs_id>` | **直接下载到本地,免转存**(逆向 sharedownload API) |
46
47
  | 群目录树 | `bdp gtree <gid>` | BFS 构建目录树 |
47
48
  | 群缓存管理 | `bdp cache [clear]` | 查看 / 清空会话缓存 |
48
49
 
@@ -113,7 +114,8 @@ bdp cat /文档/config.json
113
114
  bdp groups
114
115
  bdp gshares 539478953581833690
115
116
  bdp gls 539478953581833690 742474845517885
116
- bdp gsearch 539478953581833690 "倪海厦"
117
+ # 下载群文件到本地 (免转存)
118
+ bdp gdownload 539478953581833690 954615608563687 -o ./downloads
117
119
  ```
118
120
 
119
121
  ## 📖 完整命令参考
@@ -148,9 +150,18 @@ bdp gsearch <gid> <keyword> 搜索群文件名 (全量遍历目录)
148
150
  [--max-pages N] [--max-requests N] [--no-unique]
149
151
  [--all|--all-results] [--timeout N] [--save-partial]
150
152
  [--any-word] [--exact] [--no-cache]
153
+ bdp gdownload <gid> <fs_id>... 直接下载群文件到本地 (逆向 /api/sharedownload, 免转存)
154
+ 单文件直下;多个 fs_id(同消息)自动 zip 打包下载
155
+ [-o dir] [--filename NAME] [--from-uk X] [--msg-id Y] [--no-cache]
151
156
  bdp cache [clear] 查看 / 清空会话缓存 (~/.bdp/cache/)
152
157
  ```
153
158
 
159
+ > **群文件下载限制说明**(实测,2026-08):
160
+ > - 单文件 ≤ ~20MB:直接下载 ✅
161
+ > - 同消息多文件:zip 打包下载 ✅(总包 ≤ ~100MB)
162
+ > - 大文件(> ~100MB):网页端受百度限制(单文件加密 / zip 报 `31090 package is too large`),需用官方客户端;转存接口已被百度下线(`errno=-10`)
163
+ > - 详见 `docs/group-download-final-verdict.md`
164
+
154
165
  ### 通用选项
155
166
 
156
167
  ```
@@ -224,12 +235,13 @@ const results = await group.searchFiles('539478953581833690', '倪海厦');
224
235
  │ (统一命令解析 + 路由) │
225
236
  ├──────────────────┬──────────────────────────────────┤
226
237
  │ lib/pan.js │ lib/group.js │
227
- │ 全盘文件操作 │ 群聊文件浏览 │
238
+ │ 全盘文件操作 │ 群聊文件浏览 + 下载 │
228
239
  ├──────────────────┼──────────────────────────────────┤
229
240
  │ BaiduPCS-Go │ lib/http.js (curl / browser) │
230
241
  │ (BDUSS 认证) │ mbox API (逆向) │
231
242
  │ locate → dlink │ /mbox/group/listshare │
232
243
  │ upload/download │ /mbox/msg/shareinfo │
244
+ │ │ /api/sharedownload → dlink │
233
245
  ├──────────────────┴──────────────────────────────────┤
234
246
  │ 百度网盘 CDN / API │
235
247
  └─────────────────────────────────────────────────────┘
package/bin/bdp.js CHANGED
@@ -21,6 +21,7 @@
21
21
  * bdp gshares <gid> 列出群内分享库
22
22
  * bdp gls <gid> <fs_id> 浏览分享库内容
23
23
  * bdp gsearch <gid> <keyword> 搜索群文件
24
+ * bdp gdownload <gid> <fs_id> 直接下载群文件到本地(免转存)
24
25
  *
25
26
  * 配置:
26
27
  * bdp login --bduss X --stoken Y 设置认证
@@ -590,6 +591,33 @@ cmds.gsearch = async (args, json) => {
590
591
  }
591
592
  };
592
593
 
594
+ cmds.gdownload = async (args, json) => {
595
+ const gid = args._[0];
596
+ const fsIds = args._.slice(1);
597
+ if (!gid || fsIds.length === 0) { console.error("Usage: bdp gdownload <gid> <fs_id> [fs_id2 ...] [-o dir] [--from-uk X] [--msg-id Y] [--filename NAME]"); process.exit(1); }
598
+
599
+ const opts = {
600
+ cache: args.flags.noCache !== true,
601
+ };
602
+ if (args.flags.fromUk) opts.fromUk = args.flags.fromUk;
603
+ if (args.flags.msgId) opts.msgId = args.flags.msgId;
604
+ if (args.flags.o) opts.outDir = args.flags.o;
605
+ if (args.flags.filename) opts.filename = args.flags.filename;
606
+
607
+ verboseLog(args, `gdownload gid=${gid} fsIds=[${fsIds.join(",")}] outDir=${opts.outDir || process.cwd()} filename=${opts.filename || "auto"} fromUk=${opts.fromUk || "auto"} msgId=${opts.msgId || "auto"}`);
608
+
609
+ // 单文件 → 直接下载;多 fs_id(同一分享消息)→ zip 打包下载(逆向 type=batch)
610
+ const result = fsIds.length === 1
611
+ ? await group.downloadFile(gid, fsIds[0], opts)
612
+ : await group.downloadFiles(gid, fsIds, opts);
613
+
614
+ output(
615
+ { path: result.path, name: result.name, size: result.size, fsId: fsIds.length === 1 ? fsIds[0] : undefined, count: result.count, fromUk: result.fromUk, msgId: result.msgId },
616
+ json,
617
+ () => console.log(`✅ 已下载 ${result.size} 字节 → ${result.path}${result.count ? `(${result.count} 个文件 zip 打包)` : ""}`)
618
+ );
619
+ };
620
+
593
621
  cmds.cache = async (args, json) => {
594
622
  const action = args._[0] || "info";
595
623
  const { clearGroupCache } = group;
@@ -667,6 +695,9 @@ GROUP CHAT OPERATIONS (群聊)
667
695
  [--max-pages N] [--max-requests N] [--no-unique]
668
696
  [--all|--all-results] [--timeout N] [--save-partial]
669
697
  [--any-word] [--exact] [--no-cache]
698
+ gdownload <gid> <fs_id>... Direct download group files (免转存, 逆向 sharedownload API)
699
+ single fs_id → direct file; multiple fs_ids → zip 打包 (type=batch)
700
+ [--from-uk X] [--msg-id Y] [-o dir] [--filename NAME]
670
701
  cache [clear] Show cache info or clear session cache
671
702
  error <code> Explain an error code (e.g. -3)
672
703
 
@@ -713,6 +744,8 @@ EXAMPLES
713
744
  bdp gls 539478953581833690 292608024165826 --from-uk 2642611875 --msg-id 5069931974377329661 --json
714
745
  bdp gsearch 539478953581833690 "倪海厦" --limit 20 --json
715
746
  bdp gsearch 539478953581833690 "古籍" --page 2 --limit 20 --json-file result.json
747
+ bdp gdownload 539478953581833690 954615608563687 -o ./downloads --from-uk 1101635869133 --msg-id 713945176566573051
748
+ bdp gdownload 539478953581833690 527948256537856 986718405708023 -o ./zips # 多文件 zip 打包下载
716
749
  bdp error -3`;
717
750
 
718
751
  async function main() {
package/lib/group.js CHANGED
@@ -443,6 +443,186 @@ async function listFiles(gid, fsId, options = {}) {
443
443
  );
444
444
  }
445
445
 
446
+ // ── 群文件下载 (gdownload) ─────────────────────────────
447
+ //
448
+ // 逆向结论(2026-08-12):
449
+ // - /mbox/msg/transfer 转存已被百度拒绝(非空目录一律 errno=-10,UI 已移除保存按钮)
450
+ // - 新版 IM 前端的下载链路是 POST /api/sharedownload?sign=&timestamp=
451
+ // (sign/timestamp 留空即可),product=mbox 参数族,登录态直接返回可下载的 dlink
452
+ // - dlink 有 8 小时有效期;只支持单文件(目录 fs_id 返回的 dlink 为空)
453
+
454
+ // 解析 fs_id 的来源(fromUk/msgId):显式优先;顶层分享自动解析;否则报错提示
455
+ // 复用逻辑与 listFiles 一致,保证 gdownload 与 gls/gsearch 参数同源
456
+ async function resolveShareSource(gidS, fsIdS, options = {}, { cache }) {
457
+ let fromUkS = toStr(options.fromUk || "");
458
+ let msgIdS = toStr(options.msgId || "");
459
+ if (!fromUkS || !msgIdS) {
460
+ const shares = await listShares(gidS, { cache });
461
+ const match = shares.find((s) => s.fsId === fsIdS);
462
+ if (!match) {
463
+ throw new Error(
464
+ `Cannot determine fromUk/msgId for fsId ${fsIdS} (not a top-level share). ` +
465
+ `Pass --from-uk and --msg-id explicitly (use bdp gsearch/gsearch --json to get them).`
466
+ );
467
+ }
468
+ fromUkS = match.fromUk;
469
+ msgIdS = match.msgId;
470
+ }
471
+ return { fromUk: fromUkS, msgId: msgIdS };
472
+ }
473
+
474
+ // 获取群文件直链。返回 { dlink, name, size, isDir, md5, path, fsId, fromUk, msgId }
475
+ async function getShareDlink(gid, fsId, options = {}) {
476
+ const gidS = toStr(gid);
477
+ const fsIdS = toStr(fsId);
478
+ const cache = options.cache === true;
479
+
480
+ const { fromUk, msgId } = await resolveShareSource(gidS, fsIdS, options, { cache });
481
+ const bdstoken = await getBdstoken();
482
+ const url = `${http.PAN_BASE}/api/sharedownload?sign=&timestamp=&clienttype=0&app_id=${http.APP_ID}&web=1&bdstoken=${bdstoken}`;
483
+ const body = new URLSearchParams({
484
+ uk: fromUk,
485
+ product: "mbox",
486
+ encrypt: "0",
487
+ primaryid: msgId,
488
+ fid_list: JSON.stringify([fsIdS]),
489
+ extra: JSON.stringify({ type: "group", gid: gidS }),
490
+ }).toString();
491
+
492
+ const data = await http.webJson(url, { method: "POST", body });
493
+ if (data.errno !== 0) {
494
+ throw new Error(`API error: errno=${data.errno} (msg_id ${msgId} 不属于群 ${gidS} 或分享已失效)`);
495
+ }
496
+ const item = (data.list || [])[0];
497
+ if (!item) throw new Error(`No file info returned for fsId ${fsIdS}`);
498
+
499
+ const result = {
500
+ dlink: item.dlink || "",
501
+ name: item.server_filename || "",
502
+ size: parseInt(item.size || "0", 10),
503
+ isDir: item.isdir === 1 || item.isdir === "1",
504
+ md5: item.md5 || "",
505
+ path: safeDecode(item.path || ""),
506
+ fsId: toStr(item.fs_id),
507
+ fromUk,
508
+ msgId,
509
+ };
510
+ // 目录的 dlink 为无效串(实测 "&clienttype=0"),先于 dlink 判空检查
511
+ if (result.isDir) {
512
+ throw new Error(
513
+ `fsId ${fsIdS} 是目录(${result.name})。sharedownload 只支持单文件下载。` +
514
+ `请用 bdp gls <gid> <fs_id> 或 bdp gsearch <gid> <关键词> 找到具体文件后下载。`
515
+ );
516
+ }
517
+ if (!result.dlink) {
518
+ throw new Error(`No dlink returned for fsId ${fsIdS}(分享可能已失效或禁止下载)`);
519
+ }
520
+ return result;
521
+ }
522
+
523
+ // 下载群文件到本地。options: { outDir, filename, fromUk, msgId, ua }
524
+ // 返回 { path, name, size, dlink, fromUk, msgId }
525
+ async function downloadFile(gid, fsId, options = {}) {
526
+ const info = await getShareDlink(gid, fsId, options);
527
+ const { spawnSync } = require("child_process");
528
+ const fs = require("fs");
529
+ const path = require("path");
530
+
531
+ const outDir = options.outDir || process.cwd();
532
+ const filename = options.filename || info.name || `fs_${info.fsId}`;
533
+ const outPath = path.join(outDir, filename);
534
+
535
+ fs.mkdirSync(outDir, { recursive: true });
536
+ // dlink 校验 UA:netdisk 客户端 UA 会返回 118 字节错误页,浏览器 UA 正常(实测)
537
+ const args = [
538
+ "-s", "-L",
539
+ "-H", `User-Agent: ${options.ua || "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"}`,
540
+ "-o", outPath,
541
+ "--max-time", String(options.timeoutSec || 3600),
542
+ info.dlink,
543
+ ];
544
+ const result = spawnSync("curl", args, { encoding: "utf-8", maxBuffer: 10 * 1024 * 1024, timeout: (options.timeoutSec || 3600) * 1000 });
545
+ if (result.error) throw new Error(`Download failed: ${result.error.message}`);
546
+
547
+ let size = 0;
548
+ try { size = fs.statSync(outPath).size; } catch {}
549
+ if (info.size && size !== info.size) {
550
+ throw new Error(
551
+ `Download size mismatch: got ${size} bytes, expected ${info.size} bytes. ` +
552
+ `dlink 可能已过期(有效期 8h),请重试。`
553
+ );
554
+ }
555
+ return { path: outPath, name: filename, size, dlink: info.dlink, fromUk: info.fromUk, msgId: info.msgId };
556
+ }
557
+
558
+ // ── 群文件批量打包下载 (gdownload 多 fs_id → zip) ───────
559
+ //
560
+ // 逆向结论(2026-08-12 实测):
561
+ // - sharedownload 单文件 >~20MB 时返回加密 list(无法解密,key 未破解)
562
+ // - type=batch 时返回明文 zip 打包链接(method=batchdownload&zipcontent=...),
563
+ // 小文件打包实测可用(10MB zip 完整下载);超大文件报 error_code=31090 "package is too large"
564
+ // - zip 打包按 msg_id 归组:所有 fs_id 必须属于同一条分享消息
565
+
566
+ // 获取多文件 zip 打包直链。options: { fromUk, msgId, cache }
567
+ async function getShareZipDlink(gid, fsIds, options = {}) {
568
+ const gidS = toStr(gid);
569
+ const fsIdList = fsIds.map(toStr);
570
+ if (fsIdList.length < 2) throw new Error(`getShareZipDlink needs >= 2 fs_ids`);
571
+
572
+ // 来源以第一个 fs_id 为准(其余视为同一分享消息内的文件)
573
+ const { fromUk, msgId } = await resolveShareSource(gidS, fsIdList[0], options, { cache: options.cache === true });
574
+ const bdstoken = await getBdstoken();
575
+ const url = `${http.PAN_BASE}/api/sharedownload?sign=&timestamp=&clienttype=0&app_id=${http.APP_ID}&web=1&bdstoken=${bdstoken}`;
576
+ const body = new URLSearchParams({
577
+ uk: fromUk,
578
+ product: "mbox",
579
+ encrypt: "0",
580
+ type: "batch",
581
+ primaryid: msgId,
582
+ fid_list: JSON.stringify(fsIdList),
583
+ extra: JSON.stringify({ type: "group", gid: gidS }),
584
+ }).toString();
585
+
586
+ const data = await http.webJson(url, { method: "POST", body });
587
+ if (data.errno !== 0) {
588
+ throw new Error(`API error: errno=${data.errno} (zip batch)`);
589
+ }
590
+ if (!data.dlink) {
591
+ throw new Error(`No zip dlink returned for ${fsIdList.length} files`);
592
+ }
593
+ return { dlink: data.dlink, fromUk, msgId, count: fsIdList.length };
594
+ }
595
+
596
+ // 批量打包下载。options: { outDir, filename, fromUk, msgId, ua }
597
+ async function downloadFiles(gid, fsIds, options = {}) {
598
+ const { dlink, fromUk, msgId, count } = await getShareZipDlink(gid, fsIds, options);
599
+ const { spawnSync } = require("child_process");
600
+ const fs = require("fs");
601
+ const path = require("path");
602
+
603
+ const outDir = options.outDir || process.cwd();
604
+ const filename = options.filename || `bdp-group-${count}files.zip`;
605
+ const outPath = path.join(outDir, filename);
606
+
607
+ fs.mkdirSync(outDir, { recursive: true });
608
+ const args = [
609
+ "-s", "-L",
610
+ "-H", `User-Agent: ${options.ua || "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"}`,
611
+ "-o", outPath,
612
+ "--max-time", String(options.timeoutSec || 7200),
613
+ dlink,
614
+ ];
615
+ const result = spawnSync("curl", args, { encoding: "utf-8", maxBuffer: 10 * 1024 * 1024, timeout: (options.timeoutSec || 7200) * 1000 });
616
+ if (result.error) throw new Error(`Download failed: ${result.error.message}`);
617
+
618
+ let size = 0;
619
+ try { size = fs.statSync(outPath).size; } catch {}
620
+ if (size === 0) {
621
+ throw new Error(`Zip download failed (0 bytes). dlink 可能已过期或包太大(error_code 31090)。`);
622
+ }
623
+ return { path: outPath, name: filename, size, dlink, fromUk, msgId, count };
624
+ }
625
+
446
626
  // ── 目录树 (gtree) ────────────────────────────────────
447
627
 
448
628
  async function treeFiles(gid, options = {}) {
@@ -751,5 +931,6 @@ async function searchFiles(gid, keyword, options = {}) {
751
931
 
752
932
  module.exports = {
753
933
  listGroups, listShares, listFiles, treeFiles, searchFiles,
934
+ getShareDlink, downloadFile, getShareZipDlink, downloadFiles,
754
935
  ShareApiError, describeErrno2131, clearGroupCache,
755
936
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "baiduwangpan-cli",
3
- "version": "1.1.3",
3
+ "version": "1.1.5",
4
4
  "description": "百度网盘 CLI — 全盘读写 · 免下载读取文件内容 · 群聊文件浏览 (Agent 友好)",
5
5
  "main": "lib/index.js",
6
6
  "bin": {