baiduwangpan-cli 1.1.2 → 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 +20 -5
- package/bin/bdp.js +44 -2
- package/lib/group.js +202 -3
- package/package.json +1 -1
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
|
-
- **👥
|
|
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
|
-
|
|
117
|
+
# 下载群文件到本地 (免转存)
|
|
118
|
+
bdp gdownload 539478953581833690 954615608563687 -o ./downloads
|
|
117
119
|
```
|
|
118
120
|
|
|
119
121
|
## 📖 完整命令参考
|
|
@@ -146,10 +148,20 @@ bdp gtree <gid> [--depth N] [--concurrency N] [--max-nodes N]
|
|
|
146
148
|
bdp gsearch <gid> <keyword> 搜索群文件名 (全量遍历目录)
|
|
147
149
|
[--page N] [--limit N] [--depth N] [--concurrency N]
|
|
148
150
|
[--max-pages N] [--max-requests N] [--no-unique]
|
|
149
|
-
[--all|--all-results] [--timeout N] [--save-partial]
|
|
151
|
+
[--all|--all-results] [--timeout N] [--save-partial]
|
|
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]
|
|
150
156
|
bdp cache [clear] 查看 / 清空会话缓存 (~/.bdp/cache/)
|
|
151
157
|
```
|
|
152
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
|
+
|
|
153
165
|
### 通用选项
|
|
154
166
|
|
|
155
167
|
```
|
|
@@ -175,6 +187,8 @@ bdp cache [clear] 查看 / 清空会话缓存 (~/.bdp/cache/)
|
|
|
175
187
|
--all, --all-results 忽略分页获取全部结果 (gsearch; 不限时, 慢但完整)
|
|
176
188
|
--timeout <N> 超时秒数,到点返回已扫到的部分结果 (gsearch; 0=不限时, 默认 0)
|
|
177
189
|
--save-partial 未完整时自动把已搜到的结果存为 JSON 并打印路径 (gsearch; 配 --json-file 时写该文件)
|
|
190
|
+
--any-word 空格分隔关键词命中任意一个即可 (gsearch; 默认=全部命中 AND)
|
|
191
|
+
--exact 按文件名精确匹配,忽略路径前缀 (gsearch; 大小写不敏感)
|
|
178
192
|
--no-cache 禁用会话缓存 (gsearch/gtree/gshares/gls)
|
|
179
193
|
```
|
|
180
194
|
|
|
@@ -221,12 +235,13 @@ const results = await group.searchFiles('539478953581833690', '倪海厦');
|
|
|
221
235
|
│ (统一命令解析 + 路由) │
|
|
222
236
|
├──────────────────┬──────────────────────────────────┤
|
|
223
237
|
│ lib/pan.js │ lib/group.js │
|
|
224
|
-
│ 全盘文件操作 │
|
|
238
|
+
│ 全盘文件操作 │ 群聊文件浏览 + 下载 │
|
|
225
239
|
├──────────────────┼──────────────────────────────────┤
|
|
226
240
|
│ BaiduPCS-Go │ lib/http.js (curl / browser) │
|
|
227
241
|
│ (BDUSS 认证) │ mbox API (逆向) │
|
|
228
242
|
│ locate → dlink │ /mbox/group/listshare │
|
|
229
243
|
│ upload/download │ /mbox/msg/shareinfo │
|
|
244
|
+
│ │ /api/sharedownload → dlink │
|
|
230
245
|
├──────────────────┴──────────────────────────────────┤
|
|
231
246
|
│ 百度网盘 CDN / API │
|
|
232
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 设置认证
|
|
@@ -91,6 +92,10 @@ function parseArgs(argv) {
|
|
|
91
92
|
opts.flags.timeoutMs = parseInt(argv[++i], 10) || 0;
|
|
92
93
|
} else if (arg === "--save-partial") {
|
|
93
94
|
opts.flags.savePartial = true;
|
|
95
|
+
} else if (arg === "--any-word") {
|
|
96
|
+
opts.flags.anyWord = true;
|
|
97
|
+
} else if (arg === "--exact") {
|
|
98
|
+
opts.flags.exact = true;
|
|
94
99
|
} else if (arg === "-n" || arg === "--lines") {
|
|
95
100
|
opts.flags.n = parseInt(argv[++i], 10) || 20;
|
|
96
101
|
} else if (arg === "-p" || arg === "--path") {
|
|
@@ -483,6 +488,8 @@ cmds.gsearch = async (args, json) => {
|
|
|
483
488
|
concurrency: args.flags.concurrency || 4,
|
|
484
489
|
unique: args.flags.unique !== false,
|
|
485
490
|
all: args.flags.all === true,
|
|
491
|
+
exact: args.flags.exact === true,
|
|
492
|
+
anyWord: args.flags.anyWord === true,
|
|
486
493
|
timeoutMs: args.flags.timeoutMs || (args.flags.timeout ? args.flags.timeout * 1000 : 0),
|
|
487
494
|
depth: args.flags.depth || 1,
|
|
488
495
|
maxPages: args.flags.maxPages || 50,
|
|
@@ -536,7 +543,7 @@ cmds.gsearch = async (args, json) => {
|
|
|
536
543
|
);
|
|
537
544
|
};
|
|
538
545
|
|
|
539
|
-
verboseLog(args, `gsearch gid=${gid} keyword="${keyword}" page=${opts.page} limit=${opts.limit} concurrency=${opts.concurrency} unique=${opts.unique} all=${opts.all} timeoutMs=${opts.timeoutMs} savePartial=${savePartial} depth=${opts.depth} maxPages=${opts.maxPages} maxRequests=${opts.maxRequests} cache=${opts.cache}`);
|
|
546
|
+
verboseLog(args, `gsearch gid=${gid} keyword="${keyword}" page=${opts.page} limit=${opts.limit} concurrency=${opts.concurrency} unique=${opts.unique} all=${opts.all} exact=${opts.exact} anyWord=${opts.anyWord} timeoutMs=${opts.timeoutMs} savePartial=${savePartial} depth=${opts.depth} maxPages=${opts.maxPages} maxRequests=${opts.maxRequests} cache=${opts.cache}`);
|
|
540
547
|
const result = await group.searchFiles(gid, keyword, opts);
|
|
541
548
|
|
|
542
549
|
if (result.partial) {
|
|
@@ -584,6 +591,33 @@ cmds.gsearch = async (args, json) => {
|
|
|
584
591
|
}
|
|
585
592
|
};
|
|
586
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
|
+
|
|
587
621
|
cmds.cache = async (args, json) => {
|
|
588
622
|
const action = args._[0] || "info";
|
|
589
623
|
const { clearGroupCache } = group;
|
|
@@ -659,7 +693,11 @@ GROUP CHAT OPERATIONS (群聊)
|
|
|
659
693
|
gsearch <gid> <keyword> Search group file names (全量遍历目录, 缓存命中秒回)
|
|
660
694
|
[--page N] [--limit N] [--depth N] [--concurrency N]
|
|
661
695
|
[--max-pages N] [--max-requests N] [--no-unique]
|
|
662
|
-
[--all|--all-results] [--timeout N] [--save-partial]
|
|
696
|
+
[--all|--all-results] [--timeout N] [--save-partial]
|
|
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]
|
|
663
701
|
cache [clear] Show cache info or clear session cache
|
|
664
702
|
error <code> Explain an error code (e.g. -3)
|
|
665
703
|
|
|
@@ -692,6 +730,8 @@ OPTIONS
|
|
|
692
730
|
--all, --all-results Fetch all results, no paging (gsearch; slow but never truncates)
|
|
693
731
|
--timeout <N> Abort after N seconds and return partial results (gsearch; 0=unlimited)
|
|
694
732
|
--save-partial Auto-save partial results to JSON when scan is incomplete (gsearch)
|
|
733
|
+
--any-word Match ANY of the space-separated keywords (OR) (gsearch; default=ALL)
|
|
734
|
+
--exact Match exact file name (case-insensitive) instead of substring (gsearch)
|
|
695
735
|
|
|
696
736
|
EXAMPLES
|
|
697
737
|
bdp login --bduss abc123 --stoken def456
|
|
@@ -704,6 +744,8 @@ EXAMPLES
|
|
|
704
744
|
bdp gls 539478953581833690 292608024165826 --from-uk 2642611875 --msg-id 5069931974377329661 --json
|
|
705
745
|
bdp gsearch 539478953581833690 "倪海厦" --limit 20 --json
|
|
706
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 打包下载
|
|
707
749
|
bdp error -3`;
|
|
708
750
|
|
|
709
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=×tamp=
|
|
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=×tamp=&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=×tamp=&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 = {}) {
|
|
@@ -514,7 +694,25 @@ async function treeFiles(gid, options = {}) {
|
|
|
514
694
|
|
|
515
695
|
async function searchFiles(gid, keyword, options = {}) {
|
|
516
696
|
const gidS = toStr(gid);
|
|
517
|
-
|
|
697
|
+
|
|
698
|
+
// 关键词解析:空格分隔多关键词。
|
|
699
|
+
// - 默认(all):每个词都要出现(AND,可换序)→ "玄空 飞星" 只命中同时含两者的名字(精确收窄)
|
|
700
|
+
// - --any-word:任一词出现即可(OR,更宽)→ "玄空 飞星" 命中含"玄空"或"飞星"的名字
|
|
701
|
+
// - --exact:整名精确相等(最严格);单关键词默认仍是子串包含(兼容旧行为)
|
|
702
|
+
const rawKw = String(keyword).toLowerCase();
|
|
703
|
+
const words = rawKw.split(/\s+/).filter(Boolean);
|
|
704
|
+
const matchMode = options.exact ? "exact" : (options.anyWord ? "any" : "all");
|
|
705
|
+
const matchesName = (name) => {
|
|
706
|
+
const n = String(name || "").toLowerCase();
|
|
707
|
+
if (matchMode === "exact") {
|
|
708
|
+
// 精确匹配按文件名(去掉路径前缀),顶层分享 name 形如 "/xxx",子目录文件 name 无前缀
|
|
709
|
+
const base = n.includes("/") ? n.slice(n.lastIndexOf("/") + 1) : n;
|
|
710
|
+
return base === rawKw;
|
|
711
|
+
}
|
|
712
|
+
if (words.length <= 1) return n.includes(rawKw);
|
|
713
|
+
if (matchMode === "any") return words.some((w) => n.includes(w));
|
|
714
|
+
return words.every((w) => n.includes(w));
|
|
715
|
+
};
|
|
518
716
|
|
|
519
717
|
const limit = options.limit || 50;
|
|
520
718
|
const page = options.page || 1;
|
|
@@ -570,7 +768,7 @@ async function searchFiles(gid, keyword, options = {}) {
|
|
|
570
768
|
for (const share of shares) {
|
|
571
769
|
if (finished) break;
|
|
572
770
|
if (!share.isDir) scannedShares++;
|
|
573
|
-
if (share.name
|
|
771
|
+
if (matchesName(share.name)) {
|
|
574
772
|
pushIfMatch({
|
|
575
773
|
name: share.name,
|
|
576
774
|
path: share.name,
|
|
@@ -617,7 +815,7 @@ async function searchFiles(gid, keyword, options = {}) {
|
|
|
617
815
|
maxPages, cache, budget, throttleState,
|
|
618
816
|
});
|
|
619
817
|
if (cached) cachedDirs++;
|
|
620
|
-
const matches = files.filter((f) => f.name
|
|
818
|
+
const matches = files.filter((f) => matchesName(f.name));
|
|
621
819
|
const subDirs = files.filter((f) => f.isDir);
|
|
622
820
|
return { matches, subDirs };
|
|
623
821
|
} catch (e) {
|
|
@@ -733,5 +931,6 @@ async function searchFiles(gid, keyword, options = {}) {
|
|
|
733
931
|
|
|
734
932
|
module.exports = {
|
|
735
933
|
listGroups, listShares, listFiles, treeFiles, searchFiles,
|
|
934
|
+
getShareDlink, downloadFile, getShareZipDlink, downloadFiles,
|
|
736
935
|
ShareApiError, describeErrno2131, clearGroupCache,
|
|
737
936
|
};
|