baiduwangpan-cli 1.0.2 → 1.1.0
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 +15 -8
- package/bin/bdp.js +68 -9
- package/lib/browser-login.js +5 -10
- package/lib/group.js +320 -60
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -229,18 +229,25 @@ bdp groups bdp gshares <gid>
|
|
|
229
229
|
│ │
|
|
230
230
|
├── /mbox/group/list ├── /mbox/group/listshare
|
|
231
231
|
│ → 获取全部群组 (gid) │ → 获取分享库 (msg_id + from_uk + fs_id)
|
|
232
|
-
│ │
|
|
232
|
+
│ │ └ has_more=1 时按 last_msg_time 游标翻页
|
|
233
233
|
└───────────────────────────────────┘
|
|
234
234
|
↓
|
|
235
|
-
bdp gls <gid> <fs_id>
|
|
236
|
-
│
|
|
237
|
-
├── /mbox/msg/shareinfo
|
|
238
|
-
│ → 浏览文件/子目录
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
文件列表 (名称/大小/类型/fs_id)
|
|
235
|
+
bdp gls <gid> <fs_id> bdp gsearch <gid> "关键词"
|
|
236
|
+
│ │
|
|
237
|
+
├── /mbox/msg/shareinfo ├── 全量遍历: shareinfo 自动翻页 (每页上限 100)
|
|
238
|
+
│ → 浏览文件/子目录 │ → 目录级并发扫描 + 磁盘缓存
|
|
239
|
+
↓ ↓
|
|
240
|
+
文件列表 (名称/大小/类型/fs_id) 匹配结果
|
|
242
241
|
```
|
|
243
242
|
|
|
243
|
+
**逆向实测结论(2026-08)**:
|
|
244
|
+
|
|
245
|
+
- 群聊文件**没有服务端搜索接口**(`listshare`/`shareinfo` 的 `key`/`keyword` 参数被忽略,`/api/search` 不支持群维度)——网页端搜索同样是客户端遍历,`gsearch` 走的是同一条路。
|
|
246
|
+
- `shareinfo` 每页**硬上限 100 条**(`num>100` 不返回更多),`page` 翻页可用 → `gsearch`/`gtree` 自动全页遍历,不再漏数据。
|
|
247
|
+
- 高频调用会触发限流,表现为 `errno=-3`、**self-echo**(`errno=0` 但返回目录自身)、或空列表;工具自动退避重试、连续限流停止扫描,重跑命令即续扫。
|
|
248
|
+
- 群文件路径可能含**非法百分号编码**(裸 `%`),直接 `decodeURIComponent` 会抛 `URIError` 报废整个目录 → 已改为安全解码。
|
|
249
|
+
- `gsearch`/`gtree`/`gshares` 默认启用**磁盘会话缓存**(目录 30min / 分享 5min,`~/.bdp/cache/`,`--no-cache` 关闭、`bdp cache clear` 清理)——重复搜索秒回,深扫续扫收敛。
|
|
250
|
+
|
|
244
251
|
## 📁 项目结构
|
|
245
252
|
|
|
246
253
|
```
|
package/bin/bdp.js
CHANGED
|
@@ -112,6 +112,12 @@ function parseArgs(argv) {
|
|
|
112
112
|
opts.flags.depth = parseInt(argv[++i], 10) || 2;
|
|
113
113
|
} else if (arg === "--max-nodes") {
|
|
114
114
|
opts.flags.maxNodes = parseInt(argv[++i], 10) || 2000;
|
|
115
|
+
} else if (arg === "--max-pages") {
|
|
116
|
+
opts.flags.maxPages = parseInt(argv[++i], 10) || 50;
|
|
117
|
+
} else if (arg === "--max-requests") {
|
|
118
|
+
opts.flags.maxRequests = parseInt(argv[++i], 10) || 400;
|
|
119
|
+
} else if (arg === "--no-cache") {
|
|
120
|
+
opts.flags.noCache = true;
|
|
115
121
|
} else if (arg === "--from-uk") {
|
|
116
122
|
opts.flags.fromUk = argv[++i];
|
|
117
123
|
} else if (arg === "--msg-id") {
|
|
@@ -379,7 +385,7 @@ cmds.groups = async (args, json) => {
|
|
|
379
385
|
cmds.gshares = async (args, json) => {
|
|
380
386
|
const gid = args._[0];
|
|
381
387
|
if (!gid) { console.error("Usage: bdp gshares <gid>"); process.exit(1); }
|
|
382
|
-
const shares = await group.listShares(gid);
|
|
388
|
+
const shares = await group.listShares(gid, { cache: args.flags.noCache !== true });
|
|
383
389
|
output(shares, json, (list) => {
|
|
384
390
|
console.log(`${list.length} shares in group ${gid}:\n`);
|
|
385
391
|
list.forEach((s, i) => {
|
|
@@ -397,6 +403,7 @@ cmds.gls = async (args, json) => {
|
|
|
397
403
|
const opts = {
|
|
398
404
|
page: args.flags.page || 1,
|
|
399
405
|
pageSize: args.flags.pageSize || 50,
|
|
406
|
+
cache: args.flags.noCache !== true,
|
|
400
407
|
};
|
|
401
408
|
if (args.flags.fromUk) opts.fromUk = args.flags.fromUk;
|
|
402
409
|
if (args.flags.msgId) opts.msgId = args.flags.msgId;
|
|
@@ -410,6 +417,10 @@ cmds.gls = async (args, json) => {
|
|
|
410
417
|
return;
|
|
411
418
|
}
|
|
412
419
|
|
|
420
|
+
if (result.autoResolved) {
|
|
421
|
+
console.error(`[auto] 传入的 fromUk/msgId 与群 ${gid} 不匹配(API errno=2131),已自动改用 gshares 解析的参数 (fromUk=${result.fromUk}, msgId=${result.msgId})`);
|
|
422
|
+
}
|
|
423
|
+
|
|
413
424
|
if (result.fallback) {
|
|
414
425
|
verboseLog(args, `fallback: ${result.fallback.reason} -> ${result.fallback.resolvedFsId} (${result.fallback.level})`);
|
|
415
426
|
}
|
|
@@ -436,8 +447,11 @@ cmds.gtree = async (args, json) => {
|
|
|
436
447
|
depth: args.flags.depth || 2,
|
|
437
448
|
concurrency: args.flags.concurrency || 4,
|
|
438
449
|
maxNodes: args.flags.maxNodes || 2000,
|
|
450
|
+
maxPages: args.flags.maxPages || 50,
|
|
451
|
+
maxRequests: args.flags.maxRequests || 400,
|
|
452
|
+
cache: args.flags.noCache !== true,
|
|
439
453
|
};
|
|
440
|
-
verboseLog(args, `gtree gid=${gid} depth=${opts.depth} concurrency=${opts.concurrency} maxNodes=${opts.maxNodes}`);
|
|
454
|
+
verboseLog(args, `gtree gid=${gid} depth=${opts.depth} concurrency=${opts.concurrency} maxNodes=${opts.maxNodes} maxPages=${opts.maxPages} maxRequests=${opts.maxRequests} cache=${opts.cache}`);
|
|
441
455
|
const result = await group.treeFiles(gid, opts);
|
|
442
456
|
|
|
443
457
|
if (args.flags.jsonFile || json) {
|
|
@@ -463,13 +477,22 @@ cmds.gsearch = async (args, json) => {
|
|
|
463
477
|
unique: args.flags.unique !== false,
|
|
464
478
|
all: args.flags.all === true,
|
|
465
479
|
depth: args.flags.depth || 1,
|
|
480
|
+
maxPages: args.flags.maxPages || 50,
|
|
481
|
+
maxRequests: args.flags.maxRequests || 400,
|
|
482
|
+
cache: args.flags.noCache !== true,
|
|
466
483
|
};
|
|
467
484
|
|
|
468
|
-
verboseLog(args, `gsearch gid=${gid} keyword="${keyword}" page=${opts.page} limit=${opts.limit} concurrency=${opts.concurrency} unique=${opts.unique} all=${opts.all}`);
|
|
485
|
+
verboseLog(args, `gsearch gid=${gid} keyword="${keyword}" page=${opts.page} limit=${opts.limit} concurrency=${opts.concurrency} unique=${opts.unique} all=${opts.all} depth=${opts.depth} maxPages=${opts.maxPages} maxRequests=${opts.maxRequests} cache=${opts.cache}`);
|
|
469
486
|
const result = await group.searchFiles(gid, keyword, opts);
|
|
470
487
|
|
|
471
488
|
if (result.partial) {
|
|
472
489
|
verboseLog(args, `partial: ${result.failedShares}/${result.totalShares} shares failed`);
|
|
490
|
+
if (result.failedDirs && result.failedDirs.length > 0) {
|
|
491
|
+
verboseLog(args, `failed dirs: ${result.failedDirs.map((f) => `${f.name || f.fsId}(${f.errno})`).slice(0, 10).join(", ")}`);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
if (result.throttled) {
|
|
495
|
+
verboseLog(args, `throttled: API 限流中断,重跑命令可续扫(磁盘缓存只补缺失目录)`);
|
|
473
496
|
}
|
|
474
497
|
|
|
475
498
|
if (args.flags.legacyJson) {
|
|
@@ -485,19 +508,50 @@ cmds.gsearch = async (args, json) => {
|
|
|
485
508
|
const dir = r.isDir ? "[DIR] " : " ";
|
|
486
509
|
console.log(` ${dir}${r.path || r.name} ${formatSize(r.size)} fs_id:${r.fsId}`);
|
|
487
510
|
});
|
|
488
|
-
if (result.
|
|
511
|
+
if (result.throttled) {
|
|
512
|
+
console.error(`⚠️ API 限流中断(已扫描 ${result.scannedShares}/${result.totalShares} 分享,失败 ${result.failedShares})。重跑命令可续扫(磁盘缓存加速)`);
|
|
513
|
+
} else if (result.partial) {
|
|
489
514
|
console.error(`[verbose] ${result.failedShares} shares failed (partial result)`);
|
|
490
515
|
}
|
|
491
516
|
}
|
|
492
517
|
};
|
|
493
518
|
|
|
519
|
+
cmds.cache = async (args, json) => {
|
|
520
|
+
const action = args._[0] || "info";
|
|
521
|
+
const { clearGroupCache } = group;
|
|
522
|
+
if (action === "clear") {
|
|
523
|
+
clearGroupCache();
|
|
524
|
+
output({ cleared: true }, json, () => console.log("✅ 群聊会话缓存已清除"));
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
// info: 显示缓存目录位置
|
|
528
|
+
const path = require("path");
|
|
529
|
+
const os = require("os");
|
|
530
|
+
const dir = process.env.BDP_CACHE_DIR || path.join(os.homedir(), ".bdp", "cache");
|
|
531
|
+
output({ cacheDir: dir, ttl: { dir: "30min", shares: "5min" } }, json, () => {
|
|
532
|
+
console.log(`Cache dir: ${dir}`);
|
|
533
|
+
console.log(`Dir TTL: 30 min (gsearch/gtree 目录列表)`);
|
|
534
|
+
console.log(`Shares TTL: 5 min (gshares 分享列表)`);
|
|
535
|
+
console.log(`Clear with: bdp cache clear`);
|
|
536
|
+
});
|
|
537
|
+
};
|
|
538
|
+
|
|
494
539
|
cmds.error = async (args, json) => {
|
|
495
540
|
const code = args._[0] || "-3";
|
|
496
541
|
const ERRORS = {
|
|
497
|
-
"-3": "群分享 API 拒绝请求。已观察到大目录、错误的 msgId/fromUk
|
|
498
|
-
"处理:减小 --page-size(自动重试 50→20→10
|
|
542
|
+
"-3": "群分享 API 拒绝请求。已观察到大目录、错误的 msgId/fromUk、过期分享、请求过于频繁(限流)都可能触发。\n" +
|
|
543
|
+
"处理:减小 --page-size(自动重试 50→20→10)或 --concurrency;\n" +
|
|
544
|
+
" 限流表现为 errno=0 但只返回目录自身(self-echo),工具会自动退避重试(1s/2s);\n" +
|
|
545
|
+
" 大目录已支持自动全页遍历(每页上限 100,--max-pages 控制页数);\n" +
|
|
499
546
|
" 若回退到父目录(fallback level=parent),Agent 应从父目录换路径继续遍历。",
|
|
500
547
|
"-6": "接口鉴权失败。通常为 access_token/cookie 失效,请重新执行 bdp login。",
|
|
548
|
+
"2131": "群分享 msg_id 不属于该群(shareinfo 以 msg_id 为查找键,from_uk 不参与校验)。\n" +
|
|
549
|
+
"常见原因:gid 与 --from-uk/--msg-id/--parent-fs-id 来自不同群/分享库(参数错配)。\n" +
|
|
550
|
+
"处理:用 bdp gshares <gid> 查看本群分享(或 bdp gsearch <gid> <关键词>),\n" +
|
|
551
|
+
" 取其中一行的 fsId/fromUk/msgId 重试,保证 gid/fsId/fromUk/msgId 同源;\n" +
|
|
552
|
+
" fsId 为子目录时保持其父分享行的 fromUk/msgId 不变;\n" +
|
|
553
|
+
" 或去掉 --from-uk/--msg-id 让 CLI 自动解析(仅顶层分享)。\n" +
|
|
554
|
+
" 工具已支持:显式参数错配时自动按 fsId 纠正(结果含 autoResolved:true)。",
|
|
501
555
|
};
|
|
502
556
|
const msg = ERRORS[code] || `未知错误码 ${code},请提交 Issue: https://github.com/NkAntony777/baiduwangpan-cli/issues`;
|
|
503
557
|
if (args.flags.jsonFile || json) {
|
|
@@ -534,8 +588,10 @@ GROUP CHAT OPERATIONS (群聊)
|
|
|
534
588
|
[--depth N] [--concurrency N] [--max-nodes N]
|
|
535
589
|
gls <gid> <fs_id> Browse files in a share
|
|
536
590
|
[--page N] [--page-size N] [--from-uk X] [--msg-id Y] [--parent-fs-id Z]
|
|
537
|
-
gsearch <gid> <keyword> Search group file names
|
|
538
|
-
[--page N] [--limit N] [--depth N] [--concurrency N]
|
|
591
|
+
gsearch <gid> <keyword> Search group file names (全量遍历目录, 缓存命中秒回)
|
|
592
|
+
[--page N] [--limit N] [--depth N] [--concurrency N]
|
|
593
|
+
[--max-pages N] [--max-requests N] [--no-unique] [--all] [--no-cache]
|
|
594
|
+
cache [clear] Show cache info or clear session cache
|
|
539
595
|
error <code> Explain an error code (e.g. -3)
|
|
540
596
|
|
|
541
597
|
CONFIGURATION
|
|
@@ -559,7 +615,10 @@ OPTIONS
|
|
|
559
615
|
--limit <N> Page size for gsearch (default 50)
|
|
560
616
|
--depth <N> Recursive search/tree depth (gsearch default 1, gtree default 2)
|
|
561
617
|
--max-nodes <N> Tree node cap (gtree default 2000)
|
|
618
|
+
--max-pages <N> Max pages per directory scan (gsearch/gtree default 50, 100 items/page)
|
|
619
|
+
--max-requests <N> Request budget per command (gsearch/gtree default 400, prevents throttle)
|
|
562
620
|
--concurrency <N> Parallel share scans for gsearch/gtree (default 4)
|
|
621
|
+
--no-cache Disable in-process session cache (gsearch/gtree/gshares/gls)
|
|
563
622
|
--no-unique Keep duplicates from different msgId (gsearch)
|
|
564
623
|
--all Fetch all results, no paging (gsearch, not Agent default)
|
|
565
624
|
|
|
@@ -594,7 +653,7 @@ async function main() {
|
|
|
594
653
|
await handler(args, json);
|
|
595
654
|
} catch (e) {
|
|
596
655
|
if (json) {
|
|
597
|
-
console.log(JSON.stringify({ error: e.message }));
|
|
656
|
+
console.log(JSON.stringify({ error: e.message, ...(e.errno !== undefined ? { errno: e.errno } : {}) }));
|
|
598
657
|
} else {
|
|
599
658
|
console.error(`[ERROR] ${e.message}`);
|
|
600
659
|
}
|
package/lib/browser-login.js
CHANGED
|
@@ -281,15 +281,10 @@ async function getBrowserSession(settings = {}) {
|
|
|
281
281
|
|
|
282
282
|
async function browserJson(url, options = {}, settings = {}) {
|
|
283
283
|
const session = await getBrowserSession(settings);
|
|
284
|
-
|
|
285
|
-
try {
|
|
286
|
-
response = await evaluateFetch(session.cdp, url, options);
|
|
287
|
-
} catch (error) {
|
|
288
|
-
session.cdp.close();
|
|
289
|
-
activeSession = null;
|
|
290
|
-
throw error;
|
|
291
|
-
}
|
|
284
|
+
const response = await evaluateFetch(session.cdp, url, options);
|
|
292
285
|
|
|
286
|
+
// 注意:fetch 错误(网络/API 断连)不关闭共享会话——并发批处理中一个请求失败会
|
|
287
|
+
// 连带杀死同批其他在途请求(ws 关闭)。会话死活由下次调用前的 ping 自动检测重连。
|
|
293
288
|
try {
|
|
294
289
|
return JSON.parse(response.text.replace(/^\s+/, ""));
|
|
295
290
|
} catch {
|
|
@@ -384,8 +379,8 @@ async function browserLogin({
|
|
|
384
379
|
|
|
385
380
|
function scheduleDisconnect() {
|
|
386
381
|
if (disconnectTimer) clearTimeout(disconnectTimer);
|
|
387
|
-
// 空闲
|
|
388
|
-
disconnectTimer = setTimeout(disconnectBrowserSession,
|
|
382
|
+
// 空闲 90 秒后断开浏览器会话(并发批处理扫描中请求间隔可能较长,短超时会中途断连)
|
|
383
|
+
disconnectTimer = setTimeout(disconnectBrowserSession, 90000);
|
|
389
384
|
disconnectTimer.unref();
|
|
390
385
|
}
|
|
391
386
|
|
package/lib/group.js
CHANGED
|
@@ -1,5 +1,61 @@
|
|
|
1
1
|
const http = require("./http");
|
|
2
2
|
const { getBdstoken } = http;
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const os = require("os");
|
|
6
|
+
const crypto = require("crypto");
|
|
7
|
+
|
|
8
|
+
// ── 常量(逆向实测结论)────────────────────────────────
|
|
9
|
+
const MAX_PAGE_SIZE = 100; // shareinfo 服务端每页硬上限(num>100 不返回更多)
|
|
10
|
+
const MAX_LIST_SHARE_PAGES = 10; // listshare 分页上限(10 页 × 50 条消息)
|
|
11
|
+
const DIR_CACHE_TTL = 1800000; // 目录列表缓存 30 分钟(群文件树很少变动)
|
|
12
|
+
const EMPTY_DIR_CACHE_TTL = 60000; // 空目录缓存 1 分钟(限流时 API 可能返回空列表,短 TTL 防缓存投毒)
|
|
13
|
+
const SHARE_CACHE_TTL = 300000; // 群分享列表缓存 5 分钟(新分享消息较频繁)
|
|
14
|
+
const DEFAULT_MAX_REQUESTS = 400; // 单条命令 shareinfo 请求预算(防深扫失控触发限流)
|
|
15
|
+
|
|
16
|
+
// ── 会话级缓存(内存 L1 + 磁盘 L2,跨命令复用)──────────
|
|
17
|
+
const cacheStore = new Map();
|
|
18
|
+
|
|
19
|
+
function cacheDir() {
|
|
20
|
+
return process.env.BDP_CACHE_DIR || path.join(os.homedir(), ".bdp", "cache");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function cachePath(key) {
|
|
24
|
+
return path.join(cacheDir(), crypto.createHash("sha1").update(key).digest("hex") + ".json");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function cacheGet(key) {
|
|
28
|
+
const entry = cacheStore.get(key); // L1
|
|
29
|
+
if (entry) {
|
|
30
|
+
if (Date.now() - entry.at <= entry.ttl) return entry.value;
|
|
31
|
+
cacheStore.delete(key);
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
// L2: 磁盘缓存(跨 CLI 进程复用,避免重复扫描触发限流)
|
|
35
|
+
const disk = JSON.parse(fs.readFileSync(cachePath(key), "utf-8"));
|
|
36
|
+
if (Date.now() - disk.at <= disk.ttl) {
|
|
37
|
+
cacheStore.set(key, { at: disk.at, ttl: disk.ttl, value: disk.value });
|
|
38
|
+
return disk.value;
|
|
39
|
+
}
|
|
40
|
+
fs.unlinkSync(cachePath(key));
|
|
41
|
+
} catch {}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function cacheSet(key, value, ttl) {
|
|
46
|
+
cacheStore.set(key, { at: Date.now(), ttl, value });
|
|
47
|
+
try {
|
|
48
|
+
fs.mkdirSync(cacheDir(), { recursive: true });
|
|
49
|
+
fs.writeFileSync(cachePath(key), JSON.stringify({ at: Date.now(), ttl, value }), "utf-8");
|
|
50
|
+
} catch {}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function clearGroupCache() {
|
|
54
|
+
cacheStore.clear();
|
|
55
|
+
try {
|
|
56
|
+
fs.rmSync(cacheDir(), { recursive: true, force: true });
|
|
57
|
+
} catch {}
|
|
58
|
+
}
|
|
3
59
|
|
|
4
60
|
// ── 工具 ──────────────────────────────────────────────
|
|
5
61
|
|
|
@@ -8,6 +64,19 @@ function toStr(v) {
|
|
|
8
64
|
return String(v);
|
|
9
65
|
}
|
|
10
66
|
|
|
67
|
+
// 安全解码:部分群文件路径含非法百分号编码(裸 % 等),decodeURIComponent 会抛 URIError
|
|
68
|
+
// 导致整个目录扫描失败(实测 30/349 目录因此报废)。失败时回退原始字符串。
|
|
69
|
+
function safeDecode(s) {
|
|
70
|
+
if (!s) return "";
|
|
71
|
+
try {
|
|
72
|
+
return decodeURIComponent(s);
|
|
73
|
+
} catch {
|
|
74
|
+
return s;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
79
|
+
|
|
11
80
|
class ShareApiError extends Error {
|
|
12
81
|
constructor(errno, message) {
|
|
13
82
|
super(message);
|
|
@@ -16,6 +85,24 @@ class ShareApiError extends Error {
|
|
|
16
85
|
}
|
|
17
86
|
}
|
|
18
87
|
|
|
88
|
+
// errno=2131 的详细说明(bdp error 2131 与 gls 报错共用)
|
|
89
|
+
function describeErrno2131(gidS, msgIdS) {
|
|
90
|
+
return (
|
|
91
|
+
`shareinfo rejected: msg_id ${msgIdS} 不属于群 ${gidS} 的任何分享 (errno=2131)。` +
|
|
92
|
+
`群分享参数必须同源:gid 与 --from-uk/--msg-id/--parent-fs-id 应来自同一次 gshares/gsearch 结果。` +
|
|
93
|
+
`处理:运行 bdp gshares ${gidS} 查看本群分享(或 bdp gsearch ${gidS} <关键词>),` +
|
|
94
|
+
`取其中一行的 fsId/fromUk/msgId 重试;fsId 为子目录时保持同一行的 fromUk/msgId。`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 限流退化检测:shareinfo 被限流时 errno=0 但返回"目录自身"而非子项。
|
|
99
|
+
// 目录不可能包含自身,故返回列表里出现请求 fs_id 即可判定为退化响应。
|
|
100
|
+
function isSelfEcho(data, fsIdS) {
|
|
101
|
+
const records = data.records;
|
|
102
|
+
if (!Array.isArray(records) || records.length === 0) return false;
|
|
103
|
+
return records.some((r) => toStr(r.fs_id) === toStr(fsIdS));
|
|
104
|
+
}
|
|
105
|
+
|
|
19
106
|
// ── 群组列表 ──────────────────────────────────────────
|
|
20
107
|
|
|
21
108
|
async function listGroups() {
|
|
@@ -40,35 +127,81 @@ async function listGroups() {
|
|
|
40
127
|
|
|
41
128
|
// ── 群内分享库 ────────────────────────────────────────
|
|
42
129
|
|
|
43
|
-
async function listShares(gid) {
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
);
|
|
130
|
+
async function listShares(gid, options = {}) {
|
|
131
|
+
const gidS = toStr(gid);
|
|
132
|
+
const cache = options.cache === true;
|
|
133
|
+
const ttlMs = options.ttlMs || SHARE_CACHE_TTL;
|
|
134
|
+
const key = `shares:${gidS}`;
|
|
49
135
|
|
|
50
|
-
if (
|
|
136
|
+
if (cache) {
|
|
137
|
+
const hit = cacheGet(key);
|
|
138
|
+
if (hit) return hit;
|
|
139
|
+
}
|
|
51
140
|
|
|
141
|
+
const bdstoken = await getBdstoken();
|
|
52
142
|
const shares = [];
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
143
|
+
const seenMsg = new Set();
|
|
144
|
+
let cursor = "";
|
|
145
|
+
let stop = false;
|
|
146
|
+
|
|
147
|
+
// has_more=1 时用 last_msg_time 游标翻页(desc 序);无新消息/无游标则停止
|
|
148
|
+
for (let page = 0; page < MAX_LIST_SHARE_PAGES && !stop; page++) {
|
|
149
|
+
const url =
|
|
150
|
+
`${http.PAN_BASE}/mbox/group/listshare?clienttype=0&app_id=${http.APP_ID}&web=1&type=2&gid=${gidS}` +
|
|
151
|
+
`&limit=50&desc=1${cursor ? `&last_msg_time=${cursor}` : ""}&bdstoken=${bdstoken}`;
|
|
152
|
+
const data = await http.webJson(url, { method: "POST", body: "" });
|
|
153
|
+
|
|
154
|
+
if (data.errno !== 0) throw new Error(`API error: errno=${data.errno}`);
|
|
155
|
+
|
|
156
|
+
let added = 0;
|
|
157
|
+
for (const msg of data.records?.msg_list || []) {
|
|
158
|
+
const mid = toStr(msg.msg_id);
|
|
159
|
+
if (seenMsg.has(mid)) continue; // 防重复页
|
|
160
|
+
seenMsg.add(mid);
|
|
161
|
+
added++;
|
|
162
|
+
for (const f of msg.file_list || []) {
|
|
163
|
+
shares.push({
|
|
164
|
+
msgId: mid,
|
|
165
|
+
fromUk: toStr(msg.uk),
|
|
166
|
+
fromUser: msg.uname,
|
|
167
|
+
name: safeDecode(f.path || f.server_filename || ""),
|
|
168
|
+
fsId: toStr(f.fs_id),
|
|
169
|
+
isDir: f.isdir === "1",
|
|
170
|
+
size: parseInt(f.size || "0", 10),
|
|
171
|
+
});
|
|
172
|
+
}
|
|
64
173
|
}
|
|
174
|
+
|
|
175
|
+
if (data.has_more !== 1 || added === 0) stop = true;
|
|
176
|
+
else cursor = toStr(data.last_msg_time || "");
|
|
177
|
+
if (!cursor) stop = true;
|
|
65
178
|
}
|
|
179
|
+
|
|
180
|
+
if (cache) cacheSet(key, shares, ttlMs);
|
|
66
181
|
return shares;
|
|
67
182
|
}
|
|
68
183
|
|
|
69
184
|
// ── 底层 shareinfo 请求 ───────────────────────────────
|
|
70
185
|
|
|
71
|
-
|
|
186
|
+
function mapShareInfoFile(f, parentFsId, fromUkS, msgIdS) {
|
|
187
|
+
return {
|
|
188
|
+
name: f.server_filename || "",
|
|
189
|
+
path: safeDecode(f.path || ""),
|
|
190
|
+
isDir: f.isdir === 1 || f.isdir === "1",
|
|
191
|
+
size: parseInt(f.size || "0", 10),
|
|
192
|
+
fsId: toStr(f.fs_id),
|
|
193
|
+
md5: f.md5 || "",
|
|
194
|
+
category: parseInt(f.category || "0", 10),
|
|
195
|
+
ctime: toStr(f.server_ctime || ""),
|
|
196
|
+
mtime: toStr(f.server_mtime || ""),
|
|
197
|
+
parentFsId,
|
|
198
|
+
fromUk: fromUkS,
|
|
199
|
+
msgId: msgIdS,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// 单页 shareinfo;errno!=0 抛 ShareApiError;限流退化(self-echo)标记 throttled
|
|
204
|
+
async function shareInfoRaw(gidS, fsIdS, fromUkS, msgIdS, page, pageSize) {
|
|
72
205
|
const bdstoken = await getBdstoken();
|
|
73
206
|
const url =
|
|
74
207
|
`${http.PAN_BASE}/mbox/msg/shareinfo?type=2&from_uk=${fromUkS}&msg_id=${msgIdS}` +
|
|
@@ -80,25 +213,90 @@ async function shareInfoRequest(gidS, fsIdS, fromUkS, msgIdS, page, pageSize) {
|
|
|
80
213
|
if (data.errno !== 0) {
|
|
81
214
|
throw new ShareApiError(data.errno, `API error: errno=${data.errno}`);
|
|
82
215
|
}
|
|
216
|
+
if (isSelfEcho(data, fsIdS)) {
|
|
217
|
+
const e = new ShareApiError(-3, `shareinfo throttled (self-echo) for fs_id ${fsIdS}`);
|
|
218
|
+
e.throttled = true;
|
|
219
|
+
throw e;
|
|
220
|
+
}
|
|
221
|
+
return data;
|
|
222
|
+
}
|
|
83
223
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
224
|
+
// 限流/瞬时错误退避重试:2s → 4s → 2 次后放弃。
|
|
225
|
+
// - 网络/连接错误(非 ShareApiError):本地瞬时故障,重试(实测并发扫描中 API 会随机断连)
|
|
226
|
+
// - self-echo 限流:重试,但受 throttleState 上限约束(consecutive≥3 停止扫描 / total≥5 不再重试)
|
|
227
|
+
// - 硬 errno(-3/2131 等):不重试(listFiles 有 page-size 降级链处理 -3)
|
|
228
|
+
// budget 为整条命令的请求预算,耗尽后拒绝新请求(防深扫失控)。
|
|
229
|
+
async function shareInfoRawWithRetry(gidS, fsIdS, fromUkS, msgIdS, page, pageSize, budget, throttleState) {
|
|
230
|
+
if (budget) {
|
|
231
|
+
if (budget.remaining <= 0) throw new ShareApiError(-3, "shareinfo request budget exhausted");
|
|
232
|
+
budget.remaining--;
|
|
233
|
+
}
|
|
234
|
+
let attempts = 0;
|
|
235
|
+
for (;;) {
|
|
236
|
+
try {
|
|
237
|
+
const data = await shareInfoRaw(gidS, fsIdS, fromUkS, msgIdS, page, pageSize);
|
|
238
|
+
if (throttleState) throttleState.consecutive = 0;
|
|
239
|
+
return data;
|
|
240
|
+
} catch (e) {
|
|
241
|
+
const isThrottled = e instanceof ShareApiError && e.throttled;
|
|
242
|
+
const isNetwork = !(e instanceof ShareApiError);
|
|
243
|
+
if (!isThrottled && !isNetwork) throw e;
|
|
244
|
+
if (isThrottled && throttleState) {
|
|
245
|
+
throttleState.consecutive = (throttleState.consecutive || 0) + 1;
|
|
246
|
+
throttleState.total = (throttleState.total || 0) + 1;
|
|
247
|
+
}
|
|
248
|
+
const maxRetries = isThrottled && throttleState && (throttleState.consecutive >= 3 || throttleState.total >= 5) ? 0 : 2;
|
|
249
|
+
if (attempts >= maxRetries) throw e;
|
|
250
|
+
attempts++;
|
|
251
|
+
await delay(2000 * attempts);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
98
255
|
|
|
256
|
+
// 单页 shareinfo(gls 用),带限流退避
|
|
257
|
+
async function shareInfoRequest(gidS, fsIdS, fromUkS, msgIdS, page, pageSize) {
|
|
258
|
+
const data = await shareInfoRawWithRetry(gidS, fsIdS, fromUkS, msgIdS, page, pageSize);
|
|
259
|
+
const files = (Array.isArray(data.records) ? data.records : []).map((f) =>
|
|
260
|
+
mapShareInfoFile(f, fsIdS, fromUkS, msgIdS)
|
|
261
|
+
);
|
|
99
262
|
return { files, hasMore: data.has_more === 1, page, pageSize };
|
|
100
263
|
}
|
|
101
264
|
|
|
265
|
+
// 全量拉取目录内容(gsearch/gtree 用):自动翻页 + 去重 + 缓存。
|
|
266
|
+
// 逆向实测:shareinfo 每页硬上限 100,has_more=1 时 page=N 可继续翻页;
|
|
267
|
+
// 旧实现只取 page=1,超过 100 子项的目录会漏数据。
|
|
268
|
+
async function fetchShareDir({ gidS, fsIdS, fromUkS, msgIdS, maxPages = 50, cache = false, ttlMs = DIR_CACHE_TTL, budget, throttleState }) {
|
|
269
|
+
const key = `dir:${gidS}:${fsIdS}:${fromUkS}:${msgIdS}`;
|
|
270
|
+
if (cache) {
|
|
271
|
+
const hit = cacheGet(key);
|
|
272
|
+
if (hit) return { ...hit, cached: true };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const files = [];
|
|
276
|
+
const seenFs = new Set();
|
|
277
|
+
let hasMore = false;
|
|
278
|
+
let pages = 0;
|
|
279
|
+
|
|
280
|
+
for (let page = 1; page <= maxPages; page++) {
|
|
281
|
+
const data = await shareInfoRawWithRetry(gidS, fsIdS, fromUkS, msgIdS, page, MAX_PAGE_SIZE, budget, throttleState);
|
|
282
|
+
const records = Array.isArray(data.records) ? data.records : [];
|
|
283
|
+
for (const f of records) {
|
|
284
|
+
const fsId = toStr(f.fs_id);
|
|
285
|
+
if (seenFs.has(fsId)) continue;
|
|
286
|
+
seenFs.add(fsId);
|
|
287
|
+
files.push(mapShareInfoFile(f, fsIdS, fromUkS, msgIdS));
|
|
288
|
+
}
|
|
289
|
+
hasMore = data.has_more === 1;
|
|
290
|
+
pages = page;
|
|
291
|
+
if (!hasMore || records.length === 0) break;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const result = { files, hasMore, pages, truncated: pages >= maxPages };
|
|
295
|
+
// 空目录用短 TTL 缓存:限流时 API 可能返回空列表(缓存投毒会导致静默丢数据)
|
|
296
|
+
if (cache) cacheSet(key, result, files.length === 0 ? EMPTY_DIR_CACHE_TTL : ttlMs);
|
|
297
|
+
return { ...result, cached: false };
|
|
298
|
+
}
|
|
299
|
+
|
|
102
300
|
// ── 列出分享库内容 (gls) ──────────────────────────────
|
|
103
301
|
|
|
104
302
|
async function listFiles(gid, fsId, options = {}) {
|
|
@@ -107,14 +305,15 @@ async function listFiles(gid, fsId, options = {}) {
|
|
|
107
305
|
const parentFsId = toStr(options.parentFsId || "");
|
|
108
306
|
const page = options.page || 1;
|
|
109
307
|
let pageSize = options.pageSize || 50;
|
|
110
|
-
pageSize = Math.min(Math.max(1, pageSize),
|
|
308
|
+
pageSize = Math.min(Math.max(1, pageSize), MAX_PAGE_SIZE);
|
|
309
|
+
const cache = options.cache === true;
|
|
111
310
|
|
|
112
311
|
// 来源解析:显式传入优先;否则仅在顶层分享中精确匹配(禁止"用第一个分享"兜底)
|
|
113
312
|
let fromUkS = toStr(options.fromUk || "");
|
|
114
313
|
let msgIdS = toStr(options.msgId || "");
|
|
115
314
|
|
|
116
315
|
if (!fromUkS || !msgIdS) {
|
|
117
|
-
const shares = await listShares(gidS);
|
|
316
|
+
const shares = await listShares(gidS, { cache });
|
|
118
317
|
const match = shares.find((s) => s.fsId === fsIdS);
|
|
119
318
|
if (!match) {
|
|
120
319
|
throw new Error(
|
|
@@ -147,6 +346,28 @@ async function listFiles(gid, fsId, options = {}) {
|
|
|
147
346
|
lastErrno = -3;
|
|
148
347
|
continue; // 尝试更小的 page-size
|
|
149
348
|
}
|
|
349
|
+
if (e instanceof ShareApiError && e.errno === 2131) {
|
|
350
|
+
// msg_id 不属于该群(常见:gid 与参数来自不同群/分享库)。
|
|
351
|
+
// 尝试按 fsId 从本群 gshares 自动解析纠正一次;失败则给出明确指引。
|
|
352
|
+
try {
|
|
353
|
+
const shares = await listShares(gidS, { cache });
|
|
354
|
+
const match = shares.find((s) => s.fsId === fsIdS);
|
|
355
|
+
if (match) {
|
|
356
|
+
const result = await shareInfoRequest(gidS, fsIdS, match.fromUk, match.msgId, page, size);
|
|
357
|
+
return {
|
|
358
|
+
files: result.files,
|
|
359
|
+
hasMore: result.hasMore,
|
|
360
|
+
page,
|
|
361
|
+
pageSize: size,
|
|
362
|
+
fromUk: match.fromUk,
|
|
363
|
+
msgId: match.msgId,
|
|
364
|
+
fallback: null,
|
|
365
|
+
autoResolved: true,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
} catch {}
|
|
369
|
+
throw new ShareApiError(2131, describeErrno2131(gidS, msgIdS));
|
|
370
|
+
}
|
|
150
371
|
throw e;
|
|
151
372
|
}
|
|
152
373
|
}
|
|
@@ -181,7 +402,7 @@ async function listFiles(gid, fsId, options = {}) {
|
|
|
181
402
|
|
|
182
403
|
// 目标本身是顶层分享 → 退回 gshares 结果
|
|
183
404
|
try {
|
|
184
|
-
const shares = await listShares(gidS);
|
|
405
|
+
const shares = await listShares(gidS, { cache });
|
|
185
406
|
const match = shares.find((s) => s.fsId === fsIdS);
|
|
186
407
|
if (match) {
|
|
187
408
|
return {
|
|
@@ -217,8 +438,8 @@ async function listFiles(gid, fsId, options = {}) {
|
|
|
217
438
|
throw new ShareApiError(
|
|
218
439
|
-3,
|
|
219
440
|
`shareinfo rejected for fsId ${fsIdS} (errno=-3). ` +
|
|
220
|
-
`群分享 API 拒绝请求:已观察到大目录、错误的 msgId/fromUk
|
|
221
|
-
`请减小 --page-size
|
|
441
|
+
`群分享 API 拒绝请求:已观察到大目录、错误的 msgId/fromUk、过期分享、请求过于频繁都可能触发。` +
|
|
442
|
+
`请减小 --page-size,稍后重试,或携带 gsearch 返回的 fromUk/msgId/parentFsId 信息重试。`
|
|
222
443
|
);
|
|
223
444
|
}
|
|
224
445
|
|
|
@@ -229,8 +450,12 @@ async function treeFiles(gid, options = {}) {
|
|
|
229
450
|
const maxDepth = Math.min(Math.max(1, options.depth || 2), 5);
|
|
230
451
|
const concurrency = Math.min(Math.max(1, options.concurrency || 4), 8);
|
|
231
452
|
const maxNodes = options.maxNodes || 2000;
|
|
453
|
+
const maxPages = Math.min(Math.max(1, options.maxPages || 50), 200);
|
|
454
|
+
const cache = options.cache === true;
|
|
455
|
+
const budget = { remaining: Math.min(Math.max(1, options.maxRequests || DEFAULT_MAX_REQUESTS), 2000) };
|
|
456
|
+
const throttleState = { consecutive: 0 };
|
|
232
457
|
|
|
233
|
-
const shares = await listShares(gidS);
|
|
458
|
+
const shares = await listShares(gidS, { cache });
|
|
234
459
|
const tree = [];
|
|
235
460
|
const failed = [];
|
|
236
461
|
|
|
@@ -249,21 +474,12 @@ async function treeFiles(gid, options = {}) {
|
|
|
249
474
|
if (!node.isDir || node.size > 0) return null; // 非目录不进队列
|
|
250
475
|
scanned++;
|
|
251
476
|
try {
|
|
252
|
-
const { files } = await
|
|
477
|
+
const { files } = await fetchShareDir({
|
|
478
|
+
gidS, fsIdS: node.fsId, fromUkS: share.fromUk, msgIdS: share.msgId,
|
|
479
|
+
maxPages, cache, budget, throttleState,
|
|
480
|
+
});
|
|
253
481
|
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
|
-
},
|
|
482
|
+
node: { ...f, fromUser: share.fromUser },
|
|
267
483
|
depth: depth + 1,
|
|
268
484
|
share,
|
|
269
485
|
}));
|
|
@@ -284,9 +500,14 @@ async function treeFiles(gid, options = {}) {
|
|
|
284
500
|
if (tree.length >= maxNodes) break;
|
|
285
501
|
}
|
|
286
502
|
if (tree.length >= maxNodes) break;
|
|
503
|
+
|
|
504
|
+
// 检测到限流后自动放慢下一层
|
|
505
|
+
if (throttleState.consecutive > 0) await delay(300);
|
|
506
|
+
// 连续限流 ≥3 次:停止扫描,结果可续扫(磁盘缓存加速重跑)
|
|
507
|
+
if (throttleState.consecutive >= 3) break;
|
|
287
508
|
}
|
|
288
509
|
|
|
289
|
-
return { tree, failed, scanned, maxDepth, truncated: tree.length >= maxNodes };
|
|
510
|
+
return { tree, failed, scanned, maxDepth, truncated: tree.length >= maxNodes, throttled: throttleState.consecutive >= 3 };
|
|
290
511
|
}
|
|
291
512
|
|
|
292
513
|
// ── 群文件搜索 (gsearch) ──────────────────────────────
|
|
@@ -301,16 +522,24 @@ async function searchFiles(gid, keyword, options = {}) {
|
|
|
301
522
|
const unique = options.unique !== false;
|
|
302
523
|
const fetchAll = options.all === true;
|
|
303
524
|
const depth = Math.min(Math.max(1, options.depth || 1), 5); // 默认 1 层(兼容旧行为)
|
|
525
|
+
const maxPages = Math.min(Math.max(1, options.maxPages || 50), 200); // 单目录最多拉取页数(100/页)
|
|
526
|
+
const cache = options.cache === true;
|
|
527
|
+
const maxRequests = Math.min(Math.max(1, options.maxRequests || DEFAULT_MAX_REQUESTS), 2000);
|
|
528
|
+
const budget = { remaining: maxRequests };
|
|
529
|
+
const throttleState = { consecutive: 0 };
|
|
304
530
|
const startIdx = (page - 1) * limit;
|
|
305
531
|
const stopAt = fetchAll ? Infinity : startIdx + limit + 1; // +1 判断 hasMore
|
|
306
532
|
|
|
307
|
-
const shares = await listShares(gidS);
|
|
533
|
+
const shares = await listShares(gidS, { cache });
|
|
308
534
|
const totalShares = shares.length;
|
|
309
535
|
|
|
310
536
|
const seen = new Set();
|
|
311
537
|
const results = [];
|
|
538
|
+
const failedDirs = [];
|
|
312
539
|
let scannedShares = 0;
|
|
313
540
|
let failedShares = 0;
|
|
541
|
+
let throttledShares = 0;
|
|
542
|
+
let cachedDirs = 0;
|
|
314
543
|
let finished = false;
|
|
315
544
|
|
|
316
545
|
const pushIfMatch = (item) => {
|
|
@@ -346,7 +575,7 @@ async function searchFiles(gid, keyword, options = {}) {
|
|
|
346
575
|
}
|
|
347
576
|
}
|
|
348
577
|
|
|
349
|
-
// 阶段 2:BFS
|
|
578
|
+
// 阶段 2:BFS 递归扫描目录(每层并发,默认 depth=1 保持旧行为)
|
|
350
579
|
const dirShares = shares.filter((s) => s.isDir);
|
|
351
580
|
let queue = dirShares.map((s) => ({ fsId: s.fsId, fromUk: s.fromUk, msgId: s.msgId, depth: 1 }));
|
|
352
581
|
|
|
@@ -359,13 +588,25 @@ async function searchFiles(gid, keyword, options = {}) {
|
|
|
359
588
|
const batchResults = await Promise.all(
|
|
360
589
|
batch.map(async (share) => {
|
|
361
590
|
scannedShares++;
|
|
591
|
+
if (budget.remaining <= 0) {
|
|
592
|
+
// 请求预算耗尽:剩余目录快速失败,避免失控触发限流
|
|
593
|
+
failedShares++;
|
|
594
|
+
return { matches: [], subDirs: [] };
|
|
595
|
+
}
|
|
362
596
|
try {
|
|
363
|
-
const { files } = await
|
|
597
|
+
const { files, cached } = await fetchShareDir({
|
|
598
|
+
gidS, fsIdS: share.fsId, fromUkS: share.fromUk, msgIdS: share.msgId,
|
|
599
|
+
maxPages, cache, budget, throttleState,
|
|
600
|
+
});
|
|
601
|
+
if (cached) cachedDirs++;
|
|
364
602
|
const matches = files.filter((f) => f.name.toLowerCase().includes(kw));
|
|
365
603
|
const subDirs = files.filter((f) => f.isDir);
|
|
366
604
|
return { matches, subDirs };
|
|
367
|
-
} catch {
|
|
605
|
+
} catch (e) {
|
|
368
606
|
failedShares++;
|
|
607
|
+
const errno = e instanceof ShareApiError ? e.errno : "?";
|
|
608
|
+
if (e instanceof ShareApiError && e.throttled) throttledShares++;
|
|
609
|
+
failedDirs.push({ fsId: share.fsId, name: share.name || "", errno, message: e.message });
|
|
369
610
|
return { matches: [], subDirs: [] };
|
|
370
611
|
}
|
|
371
612
|
})
|
|
@@ -380,10 +621,20 @@ async function searchFiles(gid, keyword, options = {}) {
|
|
|
380
621
|
// 收集下一层待扫描目录
|
|
381
622
|
if (level < depth && !finished) {
|
|
382
623
|
for (const d of subDirs) {
|
|
383
|
-
queue.push({ fsId: d.fsId, fromUk: d.fromUk, msgId: d.msgId, depth: level + 1 });
|
|
624
|
+
queue.push({ fsId: d.fsId, name: d.name, fromUk: d.fromUk, msgId: d.msgId, depth: level + 1 });
|
|
384
625
|
}
|
|
385
626
|
}
|
|
386
627
|
}
|
|
628
|
+
|
|
629
|
+
// 检测到限流后自动放慢(批间降速),缓解服务端压力
|
|
630
|
+
if (throttleState.consecutive > 0) await delay(300);
|
|
631
|
+
// 连续限流 ≥3 次:服务端已饱和,停止扫描快速失败。
|
|
632
|
+
// 已成功扫描的目录已写入磁盘缓存,重跑命令只补扫缺失部分(续扫收敛)。
|
|
633
|
+
if (throttleState.consecutive >= 3) {
|
|
634
|
+
if (throttledShares < 3) throttledShares = 3;
|
|
635
|
+
finished = true;
|
|
636
|
+
break;
|
|
637
|
+
}
|
|
387
638
|
}
|
|
388
639
|
}
|
|
389
640
|
|
|
@@ -402,11 +653,20 @@ async function searchFiles(gid, keyword, options = {}) {
|
|
|
402
653
|
unique,
|
|
403
654
|
complete,
|
|
404
655
|
partial: failedShares > 0,
|
|
656
|
+
throttled: throttleState.consecutive >= 3,
|
|
657
|
+
failedDirs: failedShares > 0 ? failedDirs.slice(0, 50) : [],
|
|
405
658
|
scannedShares,
|
|
406
659
|
totalShares,
|
|
407
660
|
failedShares,
|
|
661
|
+
throttledShares,
|
|
662
|
+
cachedDirs,
|
|
663
|
+
budgetUsed: maxRequests - budget.remaining,
|
|
408
664
|
depth,
|
|
665
|
+
maxPages,
|
|
409
666
|
};
|
|
410
667
|
}
|
|
411
668
|
|
|
412
|
-
module.exports = {
|
|
669
|
+
module.exports = {
|
|
670
|
+
listGroups, listShares, listFiles, treeFiles, searchFiles,
|
|
671
|
+
ShareApiError, describeErrno2131, clearGroupCache,
|
|
672
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "baiduwangpan-cli",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "百度网盘 CLI — 全盘读写 · 免下载读取文件内容 · 群聊文件浏览 (Agent 友好)",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node bin/bdp.js",
|
|
11
|
-
"test": "node --test",
|
|
11
|
+
"test": "node --test test/*.test.js",
|
|
12
12
|
"postinstall": "node scripts/postinstall.js"
|
|
13
13
|
},
|
|
14
14
|
"engines": {
|