baiduwangpan-cli 1.1.0 → 1.1.1

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
@@ -42,7 +42,9 @@
42
42
  | **列出群组** | `bdp groups` | 所有群聊 + gid |
43
43
  | **群内分享库** | `bdp gshares <gid>` | 顶层分享目录 |
44
44
  | **浏览群文件** | `bdp gls <gid> <fs_id>` | 分页 + 递归 |
45
- | **搜索群文件** | `bdp gsearch <gid> "关键词"` | 按文件名搜索 |
45
+ | **搜索群文件** | `bdp gsearch <gid> "关键词"` | 按文件名搜索(缓存命中秒回) |
46
+ | 群目录树 | `bdp gtree <gid>` | BFS 构建目录树 |
47
+ | 群缓存管理 | `bdp cache [clear]` | 查看 / 清空会话缓存 |
46
48
 
47
49
  ## 🚀 快速开始
48
50
 
@@ -52,7 +54,8 @@
52
54
  npm install -g baiduwangpan-cli
53
55
  ```
54
56
 
55
- 安装时会**自动下载** BaiduPCS-Go 引擎(支持 GitHub 镜像加速,国内也能装)。也支持手动克隆:
57
+ 安装时会**自动下载** BaiduPCS-Go 引擎(镜像优先加速,国内也能装;全部下载源失败时打印手动下载地址,不阻断安装)。
58
+ CLI 会自动发现引擎(包根固定路径或 `BaiduPCS-Go-v4.0.1-windows-x64/` 版本子目录),升级不会丢 `pcsPath` 配置。也支持手动克隆:
56
59
 
57
60
  ```bash
58
61
  git clone https://github.com/NkAntony777/baiduwangpan-cli.git
@@ -135,21 +138,43 @@ bdp rm <path> 删除文件/目录
135
138
 
136
139
  ```
137
140
  bdp groups 列出所有群组
138
- bdp gshares <gid> 列出群内分享库
139
- bdp gls <gid> <fs_id> [--page N] 浏览分享库内容
140
- bdp gsearch <gid> <keyword> 搜索群文件名
141
+ bdp gshares <gid> [--no-cache] 列出群内分享库 (自动游标翻页)
142
+ bdp gls <gid> <fs_id> [--page N] [--page-size N] [--from-uk X] [--msg-id Y]
143
+ [--parent-fs-id Z] [--no-cache] 浏览分享库内容
144
+ bdp gtree <gid> [--depth N] [--concurrency N] [--max-nodes N]
145
+ [--max-pages N] [--max-requests N] [--no-cache] 构建群目录树
146
+ bdp gsearch <gid> <keyword> 搜索群文件名 (全量遍历目录)
147
+ [--page N] [--limit N] [--depth N] [--concurrency N]
148
+ [--max-pages N] [--max-requests N] [--no-unique]
149
+ [--all|--all-results] [--timeout N] [--no-cache]
150
+ bdp cache [clear] 查看 / 清空会话缓存 (~/.bdp/cache/)
141
151
  ```
142
152
 
143
153
  ### 通用选项
144
154
 
145
155
  ```
146
156
  --json JSON 结构化输出 (Agent 友好)
157
+ --json-file <path> JSON 写入文件 (UTF-8, 绕开控制台编码)
158
+ --verbose 进度/日志只写 stderr
147
159
  -n <N> 行数 (head/tail, 默认 20)
148
160
  -p <path> 搜索路径 (search, 默认 /)
149
161
  -i 忽略大小写 (grep)
150
162
  -N 显示行号 (cat/grep)
151
163
  -o <dir> 输出目录 (get)
152
- --page <N> 页码 (gls)
164
+ --page <N> 页码 (gls/gsearch, 默认 1)
165
+ --page-size <N> 每页条数 (gls, 默认 50, 最大 100)
166
+ --limit <N> 每页条数 (gsearch, 默认 50)
167
+ --depth <N> 递归深度 (gsearch 默认 1 / gtree 默认 2)
168
+ --concurrency <N> 并发扫描数 (gsearch/gtree, 默认 4, 1-8)
169
+ --max-nodes <N> 树节点上限 (gtree, 默认 2000)
170
+ --max-pages <N> 单目录最大页数 (gsearch/gtree, 默认 50, 100 条/页)
171
+ --max-requests <N> 单命令请求预算 (gsearch/gtree, 默认 400, 防限流)
172
+ --from-uk <X> / --msg-id <Y> 分享来源参数 (gls 子目录)
173
+ --parent-fs-id <Z> 父目录 fsId (gls, errno=-3 时回退用)
174
+ --no-unique 保留不同 msgId 的重复项 (gsearch)
175
+ --all, --all-results 忽略分页获取全部结果 (gsearch; 不限时, 慢但完整)
176
+ --timeout <N> 超时秒数,到点返回已扫到的部分结果 (gsearch; 0=不限时, 默认 0)
177
+ --no-cache 禁用会话缓存 (gsearch/gtree/gshares/gls)
153
178
  ```
154
179
 
155
180
  ## 🤖 Agent 集成示例
@@ -244,7 +269,7 @@ bdp groups bdp gshares <gid>
244
269
 
245
270
  - 群聊文件**没有服务端搜索接口**(`listshare`/`shareinfo` 的 `key`/`keyword` 参数被忽略,`/api/search` 不支持群维度)——网页端搜索同样是客户端遍历,`gsearch` 走的是同一条路。
246
271
  - `shareinfo` 每页**硬上限 100 条**(`num>100` 不返回更多),`page` 翻页可用 → `gsearch`/`gtree` 自动全页遍历,不再漏数据。
247
- - 高频调用会触发限流,表现为 `errno=-3`、**self-echo**(`errno=0` 但返回目录自身)、或空列表;工具自动退避重试、连续限流停止扫描,重跑命令即续扫。
272
+ - 高频调用会触发限流,表现为 `errno=-3`、**self-echo**(`errno=0` 但返回目录自身)、或空列表;工具自动退避重试、连续限流停止扫描,重跑命令即续扫(`--max-requests` 可控制单命令请求预算,默认 400)。
248
273
  - 群文件路径可能含**非法百分号编码**(裸 `%`),直接 `decodeURIComponent` 会抛 `URIError` 报废整个目录 → 已改为安全解码。
249
274
  - `gsearch`/`gtree`/`gshares` 默认启用**磁盘会话缓存**(目录 30min / 分享 5min,`~/.bdp/cache/`,`--no-cache` 关闭、`bdp cache clear` 清理)——重复搜索秒回,深扫续扫收敛。
250
275
 
@@ -321,6 +346,7 @@ skills/baiduwangpan/
321
346
  - **下载限速**:非 SVIP 用户下载受百度限速策略限制
322
347
  - **cat 限制**:为防止误读超大文件,`cat` 默认限制 1MB,可通过 `BDP_MAX_CAT_BYTES` 环境变量调整
323
348
  - **群聊文件**:群聊 API 为逆向获取,可能随百度服务端变更而失效
349
+ - **1.1.0 升级**:若引擎丢失(postinstall 网络失败),CLI 会自动发现 `BaiduPCS-Go-v*` 版本子目录中的二进制;config 里失效的绝对 `pcsPath` 也会自动回退到自动发现,无需手改配置
324
350
 
325
351
  ## 🤝 致谢
326
352
 
package/bin/bdp.js CHANGED
@@ -82,8 +82,12 @@ function parseArgs(argv) {
82
82
  opts.flags.unique = true;
83
83
  } else if (arg === "--no-unique") {
84
84
  opts.flags.unique = false;
85
- } else if (arg === "--all") {
85
+ } else if (arg === "--all" || arg === "--all-results") {
86
86
  opts.flags.all = true;
87
+ } else if (arg === "--timeout") {
88
+ opts.flags.timeout = parseInt(argv[++i], 10) || 0;
89
+ } else if (arg === "--timeout-ms") {
90
+ opts.flags.timeoutMs = parseInt(argv[++i], 10) || 0;
87
91
  } else if (arg === "-n" || arg === "--lines") {
88
92
  opts.flags.n = parseInt(argv[++i], 10) || 20;
89
93
  } else if (arg === "-p" || arg === "--path") {
@@ -476,13 +480,54 @@ cmds.gsearch = async (args, json) => {
476
480
  concurrency: args.flags.concurrency || 4,
477
481
  unique: args.flags.unique !== false,
478
482
  all: args.flags.all === true,
483
+ timeoutMs: args.flags.timeoutMs || (args.flags.timeout ? args.flags.timeout * 1000 : 0),
479
484
  depth: args.flags.depth || 1,
480
485
  maxPages: args.flags.maxPages || 50,
481
486
  maxRequests: args.flags.maxRequests || 400,
482
487
  cache: args.flags.noCache !== true,
483
488
  };
484
489
 
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}`);
490
+ // --json-file:扫描过程中持续写部分结果,即使被外部超时/杀掉,部分结果也已落盘
491
+ const jsonFile = args.flags.jsonFile;
492
+ const fsMod = require("fs");
493
+ let lastProgressWrite = 0;
494
+ if (jsonFile) {
495
+ fsMod.writeFileSync(jsonFile, JSON.stringify({ running: true, results: [], complete: false, partial: true }, null, 2), "utf-8");
496
+ }
497
+ opts.onProgress = (snap) => {
498
+ if (!jsonFile) return;
499
+ const now = Date.now();
500
+ if (snap.running && now - lastProgressWrite < 400) return; // 节流,避免频繁写盘
501
+ lastProgressWrite = now;
502
+ fsMod.writeFileSync(
503
+ jsonFile,
504
+ JSON.stringify(
505
+ {
506
+ running: snap.running,
507
+ results: snap.results,
508
+ page: snap.page,
509
+ pageSize: snap.pageSize,
510
+ complete: snap.complete,
511
+ partial: snap.partial,
512
+ timedOut: snap.timedOut,
513
+ stoppedReason: snap.stoppedReason,
514
+ scannedShares: snap.scannedShares,
515
+ totalShares: snap.totalShares,
516
+ failedShares: snap.failedShares,
517
+ throttledShares: snap.throttledShares,
518
+ cachedDirs: snap.cachedDirs,
519
+ budgetUsed: snap.budgetUsed,
520
+ depth: snap.depth,
521
+ maxPages: snap.maxPages,
522
+ },
523
+ null,
524
+ 2
525
+ ),
526
+ "utf-8"
527
+ );
528
+ };
529
+
530
+ 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} depth=${opts.depth} maxPages=${opts.maxPages} maxRequests=${opts.maxRequests} cache=${opts.cache}`);
486
531
  const result = await group.searchFiles(gid, keyword, opts);
487
532
 
488
533
  if (result.partial) {
@@ -508,7 +553,9 @@ cmds.gsearch = async (args, json) => {
508
553
  const dir = r.isDir ? "[DIR] " : " ";
509
554
  console.log(` ${dir}${r.path || r.name} ${formatSize(r.size)} fs_id:${r.fsId}`);
510
555
  });
511
- if (result.throttled) {
556
+ if (result.timedOut) {
557
+ console.error(`⚠️ 搜索超时(${Math.round(opts.timeoutMs / 1000)}s),已返回部分结果(complete:false, partial:true)。重跑或增大 --timeout / 用 --all-results 不限时`);
558
+ } else if (result.throttled) {
512
559
  console.error(`⚠️ API 限流中断(已扫描 ${result.scannedShares}/${result.totalShares} 分享,失败 ${result.failedShares})。重跑命令可续扫(磁盘缓存加速)`);
513
560
  } else if (result.partial) {
514
561
  console.error(`[verbose] ${result.failedShares} shares failed (partial result)`);
@@ -590,7 +637,8 @@ GROUP CHAT OPERATIONS (群聊)
590
637
  [--page N] [--page-size N] [--from-uk X] [--msg-id Y] [--parent-fs-id Z]
591
638
  gsearch <gid> <keyword> Search group file names (全量遍历目录, 缓存命中秒回)
592
639
  [--page N] [--limit N] [--depth N] [--concurrency N]
593
- [--max-pages N] [--max-requests N] [--no-unique] [--all] [--no-cache]
640
+ [--max-pages N] [--max-requests N] [--no-unique]
641
+ [--all|--all-results] [--timeout N] [--no-cache]
594
642
  cache [clear] Show cache info or clear session cache
595
643
  error <code> Explain an error code (e.g. -3)
596
644
 
@@ -620,7 +668,8 @@ OPTIONS
620
668
  --concurrency <N> Parallel share scans for gsearch/gtree (default 4)
621
669
  --no-cache Disable in-process session cache (gsearch/gtree/gshares/gls)
622
670
  --no-unique Keep duplicates from different msgId (gsearch)
623
- --all Fetch all results, no paging (gsearch, not Agent default)
671
+ --all, --all-results Fetch all results, no paging (gsearch; slow but never truncates)
672
+ --timeout <N> Abort after N seconds and return partial results (gsearch; 0=unlimited)
624
673
 
625
674
  EXAMPLES
626
675
  bdp login --bduss abc123 --stoken def456
package/lib/config.js CHANGED
@@ -5,6 +5,8 @@ const os = require("os");
5
5
  const CONFIG_DIR = process.env.BDP_CONFIG_DIR || path.join(os.homedir(), ".bdp");
6
6
  const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
7
7
 
8
+ const PCS_BINARY_NAMES = ["BaiduPCS-Go.exe", "BaiduPCS-Go"];
9
+
8
10
  function load() {
9
11
  try {
10
12
  const raw = fs.readFileSync(CONFIG_FILE, "utf-8");
@@ -19,6 +21,49 @@ function save(cfg) {
19
21
  fs.writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2), "utf-8");
20
22
  }
21
23
 
24
+ // 在目录内查找 BaiduPCS-Go 二进制:
25
+ // 1) 固定路径(postinstall 目标,如 node_modules/baiduwangpan-cli/BaiduPCS-Go.exe)
26
+ // 2) 版本子目录(如 BaiduPCS-Go-v4.0.1-windows-x64/BaiduPCS-Go.exe,
27
+ // postinstall 失败后手动解压的常见位置)
28
+ function findPCSInDir(dir) {
29
+ if (!dir) return null;
30
+ for (const name of PCS_BINARY_NAMES) {
31
+ const p = path.join(dir, name);
32
+ try {
33
+ if (fs.existsSync(p) && fs.statSync(p).isFile()) return p;
34
+ } catch {}
35
+ }
36
+ let entries;
37
+ try {
38
+ entries = fs.readdirSync(dir, { withFileTypes: true });
39
+ } catch {
40
+ return null;
41
+ }
42
+ for (const entry of entries) {
43
+ if (!entry.isDirectory() || !entry.name.includes("BaiduPCS-Go")) continue;
44
+ for (const name of PCS_BINARY_NAMES) {
45
+ const p = path.join(dir, entry.name, name);
46
+ try {
47
+ if (fs.existsSync(p) && fs.statSync(p).isFile()) return p;
48
+ } catch {}
49
+ }
50
+ }
51
+ return null;
52
+ }
53
+
54
+ function findPCS() {
55
+ // 1. 包根目录(postinstall 目标):固定路径 + 版本子目录
56
+ const pkgRoot = path.join(__dirname, "..");
57
+ const found = findPCSInDir(pkgRoot);
58
+ if (found) return found;
59
+ // 2. 全局 node_modules 目录(含版本子目录)
60
+ const nmGlobal = path.join(__dirname, "..", "..");
61
+ const foundGlobal = findPCSInDir(nmGlobal);
62
+ if (foundGlobal) return foundGlobal;
63
+ // 3. Fallback to PATH
64
+ return "BaiduPCS-Go";
65
+ }
66
+
22
67
  function get() {
23
68
  const fileCfg = load();
24
69
  return {
@@ -28,13 +73,29 @@ function get() {
28
73
  webTransport: fileCfg.webTransport || "curl",
29
74
  browserProfile: fileCfg.browserProfile || "",
30
75
  browserPort: parseInt(fileCfg.browserPort || "0", 10) || 0,
31
- pcsPath: fileCfg.pcsPath || process.env.BAIDUPCS_CMD || findPCS(),
76
+ pcsPath: resolvePcsPath(fileCfg),
32
77
  ua: fileCfg.ua || process.env.NETDISK_UA ||
33
78
  "netdisk;P2SP;3.0.0.8;netdisk;11.12.3;ANG-AN00;android-android;10.0;JSbridge4.4.0;jointBridge;1.1.0;",
34
79
  maxBytes: parseInt(fileCfg.maxBytes || process.env.BDP_MAX_CAT_BYTES || "1048576", 10),
35
80
  };
36
81
  }
37
82
 
83
+ // pcsPath 解析顺序:config 文件 → 环境变量 BAIDUPCS_CMD → 自动发现。
84
+ // 兼容 1.1.0 升级丢路径问题:若 config 里存的绝对路径已失效(升级后旧包被替换),
85
+ // 自动回退到自动发现(findPCS 会搜版本子目录),避免 search/cat/get/grep 全部报
86
+ // "BaiduPCS-Go not found at: BaiduPCS-Go"。
87
+ function resolvePcsPath(fileCfg) {
88
+ const stored = fileCfg.pcsPath || process.env.BAIDUPCS_CMD || "";
89
+ const auto = findPCS();
90
+ if (stored && path.isAbsolute(stored)) {
91
+ try {
92
+ if (fs.existsSync(stored)) return stored;
93
+ } catch {}
94
+ return auto; // 绝对路径已失效 → 自动发现
95
+ }
96
+ return stored || auto;
97
+ }
98
+
38
99
  function set(key, value) {
39
100
  const cfg = load();
40
101
  cfg[key] = value;
@@ -50,26 +111,13 @@ function setAuth(values) {
50
111
  for (const key of ["bduss", "stoken", "cookie", "webTransport", "browserProfile", "browserPort"]) {
51
112
  delete cfg[key];
52
113
  }
114
+ // setAuth 只替换认证相关字段,保留 pcsPath/ua/maxBytes 等自定义配置(升级不丢 pcsPath)
53
115
  save({ ...cfg, ...values });
54
116
  }
55
117
 
56
- function findPCS() {
57
- // 1. Look in package root (postinstall target)
58
- const pkgRoot = path.join(__dirname, "..");
59
- for (const name of ["BaiduPCS-Go.exe", "BaiduPCS-Go"]) {
60
- const p = path.join(pkgRoot, name);
61
- if (fs.existsSync(p)) return p;
62
- }
63
- // 2. Look in global node_modules
64
- const nmGlobal = path.join(__dirname, "..", "..", "BaiduPCS-Go.exe");
65
- if (fs.existsSync(nmGlobal)) return nmGlobal;
66
- // 3. Fallback to PATH
67
- return "BaiduPCS-Go";
68
- }
69
-
70
118
  function isLoggedIn() {
71
119
  const cfg = get();
72
120
  return !!(cfg.bduss && cfg.stoken);
73
121
  }
74
122
 
75
- module.exports = { get, set, update, setAuth, load, save, isLoggedIn, CONFIG_DIR, CONFIG_FILE };
123
+ module.exports = { get, set, update, setAuth, load, save, isLoggedIn, findPCS, findPCSInDir, resolvePcsPath, CONFIG_DIR, CONFIG_FILE };
package/lib/group.js CHANGED
@@ -525,6 +525,7 @@ async function searchFiles(gid, keyword, options = {}) {
525
525
  const maxPages = Math.min(Math.max(1, options.maxPages || 50), 200); // 单目录最多拉取页数(100/页)
526
526
  const cache = options.cache === true;
527
527
  const maxRequests = Math.min(Math.max(1, options.maxRequests || DEFAULT_MAX_REQUESTS), 2000);
528
+ const timeoutMs = options.timeoutMs > 0 ? options.timeoutMs : 0; // 0 = 不限时
528
529
  const budget = { remaining: maxRequests };
529
530
  const throttleState = { consecutive: 0 };
530
531
  const startIdx = (page - 1) * limit;
@@ -541,6 +542,17 @@ async function searchFiles(gid, keyword, options = {}) {
541
542
  let throttledShares = 0;
542
543
  let cachedDirs = 0;
543
544
  let finished = false;
545
+ let timedOut = false;
546
+ let stoppedReason = null;
547
+
548
+ // 超时控制:不中断当前批,批结束后停下并返回已拿到的部分结果
549
+ // (complete:false, partial:true)。配合 CLI --json-file 的 onProgress 持续写盘,
550
+ // 即使被外部超时/杀掉,部分结果也已提前落盘。
551
+ let timeoutTimer = null;
552
+ if (timeoutMs > 0) {
553
+ timeoutTimer = setTimeout(() => { timedOut = true; }, timeoutMs);
554
+ if (typeof timeoutTimer.unref === "function") timeoutTimer.unref();
555
+ }
544
556
 
545
557
  const pushIfMatch = (item) => {
546
558
  if (finished) return;
@@ -548,7 +560,10 @@ async function searchFiles(gid, keyword, options = {}) {
548
560
  if (unique && seen.has(key)) return;
549
561
  seen.add(key);
550
562
  results.push(item);
551
- if (results.length >= stopAt) finished = true;
563
+ if (results.length >= stopAt) {
564
+ finished = true;
565
+ if (!stoppedReason) stoppedReason = "page-limit";
566
+ }
552
567
  };
553
568
 
554
569
  // 阶段 1:顶层分享名称匹配(非目录分享在此完成扫描)
@@ -580,10 +595,12 @@ async function searchFiles(gid, keyword, options = {}) {
580
595
  let queue = dirShares.map((s) => ({ fsId: s.fsId, fromUk: s.fromUk, msgId: s.msgId, depth: 1 }));
581
596
 
582
597
  for (let level = 1; level <= depth && queue.length > 0 && !finished; level++) {
598
+ if (timedOut) { finished = true; stoppedReason = stoppedReason || "timeout"; break; }
583
599
  const current = queue;
584
600
  queue = [];
585
601
 
586
602
  for (let i = 0; i < current.length && !finished; i += concurrency) {
603
+ if (timedOut) { finished = true; stoppedReason = stoppedReason || "timeout"; break; }
587
604
  const batch = current.slice(i, i + concurrency);
588
605
  const batchResults = await Promise.all(
589
606
  batch.map(async (share) => {
@@ -591,6 +608,7 @@ async function searchFiles(gid, keyword, options = {}) {
591
608
  if (budget.remaining <= 0) {
592
609
  // 请求预算耗尽:剩余目录快速失败,避免失控触发限流
593
610
  failedShares++;
611
+ if (!stoppedReason) stoppedReason = "budget";
594
612
  return { matches: [], subDirs: [] };
595
613
  }
596
614
  try {
@@ -628,21 +646,59 @@ async function searchFiles(gid, keyword, options = {}) {
628
646
 
629
647
  // 检测到限流后自动放慢(批间降速),缓解服务端压力
630
648
  if (throttleState.consecutive > 0) await delay(300);
649
+ if (timedOut) { finished = true; stoppedReason = stoppedReason || "timeout"; break; }
631
650
  // 连续限流 ≥3 次:服务端已饱和,停止扫描快速失败。
632
651
  // 已成功扫描的目录已写入磁盘缓存,重跑命令只补扫缺失部分(续扫收敛)。
633
652
  if (throttleState.consecutive >= 3) {
634
653
  if (throttledShares < 3) throttledShares = 3;
635
654
  finished = true;
655
+ stoppedReason = stoppedReason || "throttled";
636
656
  break;
637
657
  }
658
+
659
+ // 每批扫描完主动上报部分结果(CLI 用于持续写 --json-file)
660
+ if (typeof options.onProgress === "function") {
661
+ options.onProgress({
662
+ results,
663
+ page,
664
+ pageSize: limit,
665
+ total: null,
666
+ complete: false,
667
+ partial: true,
668
+ running: true,
669
+ timedOut: false,
670
+ stoppedReason,
671
+ scannedShares,
672
+ totalShares,
673
+ failedShares,
674
+ throttledShares,
675
+ cachedDirs,
676
+ budgetUsed: maxRequests - budget.remaining,
677
+ depth,
678
+ maxPages,
679
+ });
680
+ }
638
681
  }
639
682
  }
640
683
 
641
- const complete = scannedShares >= totalShares;
684
+ if (timeoutTimer) clearTimeout(timeoutTimer);
685
+
686
+ if (timedOut) {
687
+ finished = true;
688
+ stoppedReason = stoppedReason || "timeout";
689
+ }
690
+
691
+ // 完整度判定:BFS 自然结束(队列清空 / 达到 depth 上限)且未被分页/限流/超时/预算中断,
692
+ // 且没有任何失败,才算 complete。之前"scannedShares >= totalShares"在 depth>1 或
693
+ // 提前截断时会误报 complete:true。
694
+ const scanDone = !finished && !timedOut;
695
+ const complete = scanDone && failedShares === 0;
642
696
  const hasMore = !fetchAll && results.length > startIdx + limit;
643
697
  const pageSlice = fetchAll ? results : results.slice(startIdx, startIdx + limit);
644
698
 
645
- return {
699
+ if (!stoppedReason) stoppedReason = complete ? "complete" : "stopped";
700
+
701
+ const out = {
646
702
  results: pageSlice,
647
703
  page,
648
704
  pageSize: limit,
@@ -652,7 +708,9 @@ async function searchFiles(gid, keyword, options = {}) {
652
708
  nextPage: hasMore ? page + 1 : null,
653
709
  unique,
654
710
  complete,
655
- partial: failedShares > 0,
711
+ partial: !complete,
712
+ timedOut,
713
+ stoppedReason,
656
714
  throttled: throttleState.consecutive >= 3,
657
715
  failedDirs: failedShares > 0 ? failedDirs.slice(0, 50) : [],
658
716
  scannedShares,
@@ -664,6 +722,13 @@ async function searchFiles(gid, keyword, options = {}) {
664
722
  depth,
665
723
  maxPages,
666
724
  };
725
+
726
+ // 结束态上报(running:false),CLI 据此写最终 JSON
727
+ if (typeof options.onProgress === "function") {
728
+ options.onProgress({ ...out, results, running: false });
729
+ }
730
+
731
+ return out;
667
732
  }
668
733
 
669
734
  module.exports = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "baiduwangpan-cli",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "百度网盘 CLI — 全盘读写 · 免下载读取文件内容 · 群聊文件浏览 (Agent 友好)",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -4,8 +4,15 @@
4
4
  * postinstall.js — 自动下载 BaiduPCS-Go 二进制
5
5
  *
6
6
  * 跨平台支持: Windows / macOS / Linux (x64, arm64, 386)
7
- * 下载加速: GitHub → ghproxy 镜像 → ghfast 镜像 → 降级提示
7
+ * 下载加速: 镜像(ghfast/ghproxy...) 优先 → GitHub 兜底(大陆网络 GitHub 常 DNS 污染)
8
8
  * 解压方式: Windows (PowerShell) / Unix (unzip / python3 / tar)
9
+ *
10
+ * 1.1.0 已知问题修复:
11
+ * - 镜像顺序: 国内网络优先用 ghfast/ghproxy,GitHub 放最后
12
+ * - download() 补 file stream error 处理:第一个镜像部分下载失败时不再直接崩溃,
13
+ * 确保 5 个源按顺序 fallback(此前"只跑 1 行就抛出")
14
+ * - 全部失败时打印每个源的完整下载地址,便于手动下载
15
+ * - 发现版本子目录(BaiduPCS-Go-vX/)里的二进制时提升到固定路径(修复后无需手改 config)
9
16
  */
10
17
 
11
18
  const https = require("https");
@@ -19,13 +26,13 @@ const PKG_ROOT = path.join(__dirname, "..");
19
26
  const REPO = "qjfoidnh/BaiduPCS-Go";
20
27
  const VERSION = "v4.0.1";
21
28
 
22
- // GitHub 下载加速镜像(按优先级排列)
29
+ // GitHub 下载加速镜像(按优先级排列):大陆网络优先镜像,GitHub 放最后兜底
23
30
  const MIRRORS = [
24
- (asset) => `https://github.com/${REPO}/releases/download/${VERSION}/${asset}`,
25
- (asset) => `https://ghproxy.net/https://github.com/${REPO}/releases/download/${VERSION}/${asset}`,
26
31
  (asset) => `https://ghfast.top/https://github.com/${REPO}/releases/download/${VERSION}/${asset}`,
32
+ (asset) => `https://ghproxy.net/https://github.com/${REPO}/releases/download/${VERSION}/${asset}`,
27
33
  (asset) => `https://mirror.ghproxy.com/https://github.com/${REPO}/releases/download/${VERSION}/${asset}`,
28
34
  (asset) => `https://gh-proxy.com/https://github.com/${REPO}/releases/download/${VERSION}/${asset}`,
35
+ (asset) => `https://github.com/${REPO}/releases/download/${VERSION}/${asset}`,
29
36
  ];
30
37
 
31
38
  // ── 平台检测 ────────────────────────────────────────────
@@ -84,67 +91,69 @@ function download(url, dest, timeout = 60000) {
84
91
  const timer = setTimeout(() => {
85
92
  if (!settled) {
86
93
  settled = true;
87
- file.close();
94
+ try { file.close(); } catch {}
88
95
  fs.unlink(dest, () => {});
89
96
  reject(new Error("timeout"));
90
97
  }
91
98
  }, timeout);
92
99
 
100
+ // 统一失败路径:清定时器 + 关流 + 删残留文件。file stream 的 error 也走这里,
101
+ // 防止第一个镜像部分下载失败后 file 抛 EPIPE/ENOSPC 未捕获导致整个 postinstall 崩溃
102
+ // (1.1.0 实测"只跑 1 行就抛出",fallback 没机会执行)。
103
+ const fail = (err) => {
104
+ if (settled) return;
105
+ settled = true;
106
+ clearTimeout(timer);
107
+ try { file.close(); } catch {}
108
+ fs.unlink(dest, () => {});
109
+ reject(err);
110
+ };
111
+
112
+ file.on("error", (e) => fail(e));
113
+
93
114
  const req = (url, redirects = 0) => {
94
115
  if (redirects > 5) {
95
- if (!settled) { settled = true; clearTimeout(timer); reject(new Error("too many redirects")); }
116
+ fail(new Error("too many redirects"));
96
117
  return;
97
118
  }
98
119
 
99
120
  const client = url.startsWith("https") ? https : http;
100
- const r = client.get(url, { timeout: 15000 }, (res) => {
101
- // 处理重定向
102
- if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
103
- res.resume();
104
- req(res.headers.location, redirects + 1);
105
- return;
106
- }
107
- if (res.statusCode !== 200) {
108
- res.resume();
109
- if (!settled) {
110
- settled = true;
111
- clearTimeout(timer);
112
- file.close();
113
- fs.unlink(dest, () => {});
114
- reject(new Error(`HTTP ${res.statusCode}`));
121
+ let r;
122
+ try {
123
+ r = client.get(url, { timeout: 15000 }, (res) => {
124
+ // 处理重定向
125
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
126
+ res.resume();
127
+ req(res.headers.location, redirects + 1);
128
+ return;
115
129
  }
116
- return;
117
- }
118
- res.pipe(file);
119
- file.on("finish", () => {
120
- file.close(() => {
121
- if (!settled) {
122
- settled = true;
123
- clearTimeout(timer);
124
- resolve();
125
- }
130
+ if (res.statusCode !== 200) {
131
+ res.resume();
132
+ fail(new Error(`HTTP ${res.statusCode}`));
133
+ return;
134
+ }
135
+ res.pipe(file);
136
+ file.on("finish", () => {
137
+ file.close(() => {
138
+ if (!settled) {
139
+ settled = true;
140
+ clearTimeout(timer);
141
+ resolve();
142
+ }
143
+ });
126
144
  });
127
145
  });
128
- });
146
+ } catch (e) {
147
+ fail(e);
148
+ return;
149
+ }
129
150
 
130
- r.on("error", (e) => {
131
- if (!settled) {
132
- settled = true;
133
- clearTimeout(timer);
134
- file.close();
135
- fs.unlink(dest, () => {});
136
- reject(e);
137
- }
138
- });
151
+ r.on("error", (e) => fail(e));
139
152
 
140
153
  r.on("timeout", () => {
141
154
  if (!settled) {
142
- settled = true;
143
- clearTimeout(timer);
144
155
  r.destroy();
145
- file.close();
146
- fs.unlink(dest, () => {});
147
- reject(new Error("connection timeout"));
156
+ fail(new Error("connection timeout"));
148
157
  }
149
158
  });
150
159
  };
@@ -154,20 +163,29 @@ function download(url, dest, timeout = 60000) {
154
163
  }
155
164
 
156
165
  async function downloadWithMirrors(assetName, dest) {
166
+ const errors = [];
157
167
  for (let i = 0; i < MIRRORS.length; i++) {
158
168
  const url = MIRRORS[i](assetName);
159
- const label = i === 0 ? "GitHub" : new URL(MIRRORS[i]("x")).hostname;
169
+ const label = i === MIRRORS.length - 1 ? "GitHub" : new URL(MIRRORS[i]("x")).hostname;
160
170
  process.stdout.write(` bdp: trying ${label}...`);
161
171
  try {
162
172
  await download(url, dest, 90000);
163
173
  console.log(" OK");
164
- return true;
174
+ return { ok: true, url, label };
165
175
  } catch (e) {
166
- console.log(` failed (${e.message})`);
167
- if (fs.existsSync(dest)) fs.unlinkSync(dest);
176
+ const msg = e.message || String(e);
177
+ errors.push(`${label}: ${msg}`);
178
+ console.log(` failed (${msg})`);
179
+ if (fs.existsSync(dest)) {
180
+ try { fs.unlinkSync(dest); } catch {}
181
+ }
182
+ // GitHub DNS 污染提示
183
+ if (/ENOTFOUND|EAI_AGAIN|ETIMEDOUT/i.test(msg) && label === "GitHub") {
184
+ console.log(" bdp: (GitHub 解析失败——大陆网络常见 DNS 污染,前面的镜像通常可用)");
185
+ }
168
186
  }
169
187
  }
170
- return false;
188
+ return { ok: false, errors };
171
189
  }
172
190
 
173
191
  // ── 解压 ────────────────────────────────────────────────
@@ -213,13 +231,39 @@ function extractZip(zipPath, destDir) {
213
231
 
214
232
  // ── 主逻辑 ──────────────────────────────────────────────
215
233
 
234
+ function findExistingBinary(binName) {
235
+ const binPath = path.join(PKG_ROOT, binName);
236
+ if (fs.existsSync(binPath)) return { path: binPath, versioned: false };
237
+ // 版本子目录(上次 postinstall 失败后手动解压的常见位置)
238
+ try {
239
+ for (const entry of fs.readdirSync(PKG_ROOT, { withFileTypes: true })) {
240
+ if (entry.isDirectory() && entry.name.includes("BaiduPCS-Go")) {
241
+ const p = path.join(PKG_ROOT, entry.name, binName);
242
+ if (fs.existsSync(p)) return { path: p, versioned: true };
243
+ }
244
+ }
245
+ } catch {}
246
+ return null;
247
+ }
248
+
216
249
  async function main() {
217
250
  const binName = getBinaryName();
218
251
  const binPath = path.join(PKG_ROOT, binName);
219
252
 
220
- // 已存在则跳过
221
- if (fs.existsSync(binPath)) {
222
- console.log(" bdp: BaiduPCS-Go already exists, skipping.");
253
+ // 已存在则跳过;版本子目录里的二进制提升到固定路径(CLI 固定路径优先,且不依赖版本号)
254
+ const existing = findExistingBinary(binName);
255
+ if (existing) {
256
+ if (existing.versioned) {
257
+ try {
258
+ fs.copyFileSync(existing.path, binPath);
259
+ if (os.platform() !== "win32") { try { fs.chmodSync(binPath, 0o755); } catch {} }
260
+ console.log(` bdp: ✅ BaiduPCS-Go promoted from ${path.basename(path.dirname(existing.path))} to fixed path.`);
261
+ } catch (e) {
262
+ console.log(` bdp: ⚠️ 版本子目录已发现但提升失败: ${e.message}`);
263
+ }
264
+ } else {
265
+ console.log(" bdp: BaiduPCS-Go already exists, skipping.");
266
+ }
223
267
  return;
224
268
  }
225
269
 
@@ -229,14 +273,15 @@ async function main() {
229
273
  console.log("");
230
274
  console.log(" bdp: Installing BaiduPCS-Go (百度网盘 CLI 引擎)...");
231
275
 
232
- // 下载
233
- const ok = await downloadWithMirrors(assetName, zipPath);
234
- if (!ok) {
235
- console.warn(" bdp: ⚠️ All download mirrors failed.");
236
- console.warn(` bdp: Please manually download ${assetName}`);
237
- console.warn(` bdp: from https://github.com/${REPO}/releases/tag/${VERSION}`);
238
- console.warn(` bdp: and extract ${binName} to: ${PKG_ROOT}`);
239
- console.warn(" bdp: (the package still works if BaiduPCS-Go is in your PATH)");
276
+ // 下载(镜像优先 → GitHub 兜底)
277
+ const result = await downloadWithMirrors(assetName, zipPath);
278
+ if (!result.ok) {
279
+ console.warn(" bdp: ⚠️ 所有下载源失败(大陆网络常见:GitHub DNS 污染 / 镜像暂时抽风)。");
280
+ console.warn(` bdp: 请手动下载 ${assetName}(任一地址均可):`);
281
+ for (const m of MIRRORS) console.warn(` bdp: ${m(assetName)}`);
282
+ console.warn(` bdp: 解压后把 ${binName} 放到: ${PKG_ROOT}`);
283
+ console.warn(" bdp: (或版本子目录,如 BaiduPCS-Go-v4.0.1-windows-x64/,CLI 会自动发现)");
284
+ console.warn(" bdp: 也可直接确保 BaiduPCS-Go 在 PATH 中(包仍可正常使用)。");
240
285
  console.warn("");
241
286
  process.exit(0); // 不阻断 npm install
242
287
  }
@@ -244,7 +289,7 @@ async function main() {
244
289
  // 解压
245
290
  console.log(" bdp: Extracting...");
246
291
  if (!extractZip(zipPath, PKG_ROOT)) {
247
- console.warn(` bdp: ⚠️ Cannot extract. Please manually unzip: ${zipPath}`);
292
+ console.warn(` bdp: ⚠️ 解压失败。请手动解压: ${zipPath}`);
248
293
  process.exit(0);
249
294
  }
250
295
 
@@ -273,8 +318,8 @@ async function main() {
273
318
  }
274
319
  console.log(` bdp: ✅ BaiduPCS-Go installed.`);
275
320
  } else {
276
- console.warn(" bdp: ⚠️ Binary not found after extraction.");
277
- console.warn(` bdp: Please manually extract and place ${binName} in: ${PKG_ROOT}`);
321
+ console.warn(" bdp: ⚠️ 解压后未找到二进制。");
322
+ console.warn(` bdp: 请手动解压并把 ${binName} 放到: ${PKG_ROOT}`);
278
323
  }
279
324
 
280
325
  // 清理 zip 和解压目录