baiduwangpan-cli 1.0.2 → 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 集成示例
@@ -229,18 +254,25 @@ bdp groups bdp gshares <gid>
229
254
  │ │
230
255
  ├── /mbox/group/list ├── /mbox/group/listshare
231
256
  │ → 获取全部群组 (gid) │ → 获取分享库 (msg_id + from_uk + fs_id)
232
- │ │
257
+ │ │ └ has_more=1 时按 last_msg_time 游标翻页
233
258
  └───────────────────────────────────┘
234
259
  ↓
235
- bdp gls <gid> <fs_id>
236
- │
237
- ├── /mbox/msg/shareinfo
238
- │ → 浏览文件/子目录 (分页)
239
- │ → 递归遍历目录树
240
- ↓
241
- 文件列表 (名称/大小/类型/fs_id)
260
+ bdp gls <gid> <fs_id> bdp gsearch <gid> "关键词"
261
+ │ │
262
+ ├── /mbox/msg/shareinfo ├── 全量遍历: shareinfo 自动翻页 (每页上限 100)
263
+ │ → 浏览文件/子目录 │ → 目录级并发扫描 + 磁盘缓存
264
+ ↓ ↓
265
+ 文件列表 (名称/大小/类型/fs_id) 匹配结果
242
266
  ```
243
267
 
268
+ **逆向实测结论(2026-08)**:
269
+
270
+ - 群聊文件**没有服务端搜索接口**(`listshare`/`shareinfo` 的 `key`/`keyword` 参数被忽略,`/api/search` 不支持群维度)——网页端搜索同样是客户端遍历,`gsearch` 走的是同一条路。
271
+ - `shareinfo` 每页**硬上限 100 条**(`num>100` 不返回更多),`page` 翻页可用 → `gsearch`/`gtree` 自动全页遍历,不再漏数据。
272
+ - 高频调用会触发限流,表现为 `errno=-3`、**self-echo**(`errno=0` 但返回目录自身)、或空列表;工具自动退避重试、连续限流停止扫描,重跑命令即续扫(`--max-requests` 可控制单命令请求预算,默认 400)。
273
+ - 群文件路径可能含**非法百分号编码**(裸 `%`),直接 `decodeURIComponent` 会抛 `URIError` 报废整个目录 → 已改为安全解码。
274
+ - `gsearch`/`gtree`/`gshares` 默认启用**磁盘会话缓存**(目录 30min / 分享 5min,`~/.bdp/cache/`,`--no-cache` 关闭、`bdp cache clear` 清理)——重复搜索秒回,深扫续扫收敛。
275
+
244
276
  ## 📁 项目结构
245
277
 
246
278
  ```
@@ -314,6 +346,7 @@ skills/baiduwangpan/
314
346
  - **下载限速**:非 SVIP 用户下载受百度限速策略限制
315
347
  - **cat 限制**:为防止误读超大文件,`cat` 默认限制 1MB,可通过 `BDP_MAX_CAT_BYTES` 环境变量调整
316
348
  - **群聊文件**:群聊 API 为逆向获取,可能随百度服务端变更而失效
349
+ - **1.1.0 升级**:若引擎丢失(postinstall 网络失败),CLI 会自动发现 `BaiduPCS-Go-v*` 版本子目录中的二进制;config 里失效的绝对 `pcsPath` 也会自动回退到自动发现,无需手改配置
317
350
 
318
351
  ## 🤝 致谢
319
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") {
@@ -112,6 +116,12 @@ function parseArgs(argv) {
112
116
  opts.flags.depth = parseInt(argv[++i], 10) || 2;
113
117
  } else if (arg === "--max-nodes") {
114
118
  opts.flags.maxNodes = parseInt(argv[++i], 10) || 2000;
119
+ } else if (arg === "--max-pages") {
120
+ opts.flags.maxPages = parseInt(argv[++i], 10) || 50;
121
+ } else if (arg === "--max-requests") {
122
+ opts.flags.maxRequests = parseInt(argv[++i], 10) || 400;
123
+ } else if (arg === "--no-cache") {
124
+ opts.flags.noCache = true;
115
125
  } else if (arg === "--from-uk") {
116
126
  opts.flags.fromUk = argv[++i];
117
127
  } else if (arg === "--msg-id") {
@@ -379,7 +389,7 @@ cmds.groups = async (args, json) => {
379
389
  cmds.gshares = async (args, json) => {
380
390
  const gid = args._[0];
381
391
  if (!gid) { console.error("Usage: bdp gshares <gid>"); process.exit(1); }
382
- const shares = await group.listShares(gid);
392
+ const shares = await group.listShares(gid, { cache: args.flags.noCache !== true });
383
393
  output(shares, json, (list) => {
384
394
  console.log(`${list.length} shares in group ${gid}:\n`);
385
395
  list.forEach((s, i) => {
@@ -397,6 +407,7 @@ cmds.gls = async (args, json) => {
397
407
  const opts = {
398
408
  page: args.flags.page || 1,
399
409
  pageSize: args.flags.pageSize || 50,
410
+ cache: args.flags.noCache !== true,
400
411
  };
401
412
  if (args.flags.fromUk) opts.fromUk = args.flags.fromUk;
402
413
  if (args.flags.msgId) opts.msgId = args.flags.msgId;
@@ -410,6 +421,10 @@ cmds.gls = async (args, json) => {
410
421
  return;
411
422
  }
412
423
 
424
+ if (result.autoResolved) {
425
+ console.error(`[auto] 传入的 fromUk/msgId 与群 ${gid} 不匹配(API errno=2131),已自动改用 gshares 解析的参数 (fromUk=${result.fromUk}, msgId=${result.msgId})`);
426
+ }
427
+
413
428
  if (result.fallback) {
414
429
  verboseLog(args, `fallback: ${result.fallback.reason} -> ${result.fallback.resolvedFsId} (${result.fallback.level})`);
415
430
  }
@@ -436,8 +451,11 @@ cmds.gtree = async (args, json) => {
436
451
  depth: args.flags.depth || 2,
437
452
  concurrency: args.flags.concurrency || 4,
438
453
  maxNodes: args.flags.maxNodes || 2000,
454
+ maxPages: args.flags.maxPages || 50,
455
+ maxRequests: args.flags.maxRequests || 400,
456
+ cache: args.flags.noCache !== true,
439
457
  };
440
- verboseLog(args, `gtree gid=${gid} depth=${opts.depth} concurrency=${opts.concurrency} maxNodes=${opts.maxNodes}`);
458
+ verboseLog(args, `gtree gid=${gid} depth=${opts.depth} concurrency=${opts.concurrency} maxNodes=${opts.maxNodes} maxPages=${opts.maxPages} maxRequests=${opts.maxRequests} cache=${opts.cache}`);
441
459
  const result = await group.treeFiles(gid, opts);
442
460
 
443
461
  if (args.flags.jsonFile || json) {
@@ -462,14 +480,64 @@ cmds.gsearch = async (args, json) => {
462
480
  concurrency: args.flags.concurrency || 4,
463
481
  unique: args.flags.unique !== false,
464
482
  all: args.flags.all === true,
483
+ timeoutMs: args.flags.timeoutMs || (args.flags.timeout ? args.flags.timeout * 1000 : 0),
465
484
  depth: args.flags.depth || 1,
485
+ maxPages: args.flags.maxPages || 50,
486
+ maxRequests: args.flags.maxRequests || 400,
487
+ cache: args.flags.noCache !== true,
466
488
  };
467
489
 
468
- verboseLog(args, `gsearch gid=${gid} keyword="${keyword}" page=${opts.page} limit=${opts.limit} concurrency=${opts.concurrency} unique=${opts.unique} all=${opts.all}`);
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}`);
469
531
  const result = await group.searchFiles(gid, keyword, opts);
470
532
 
471
533
  if (result.partial) {
472
534
  verboseLog(args, `partial: ${result.failedShares}/${result.totalShares} shares failed`);
535
+ if (result.failedDirs && result.failedDirs.length > 0) {
536
+ verboseLog(args, `failed dirs: ${result.failedDirs.map((f) => `${f.name || f.fsId}(${f.errno})`).slice(0, 10).join(", ")}`);
537
+ }
538
+ }
539
+ if (result.throttled) {
540
+ verboseLog(args, `throttled: API 限流中断,重跑命令可续扫(磁盘缓存只补缺失目录)`);
473
541
  }
474
542
 
475
543
  if (args.flags.legacyJson) {
@@ -485,19 +553,52 @@ cmds.gsearch = async (args, json) => {
485
553
  const dir = r.isDir ? "[DIR] " : " ";
486
554
  console.log(` ${dir}${r.path || r.name} ${formatSize(r.size)} fs_id:${r.fsId}`);
487
555
  });
488
- if (result.partial) {
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) {
559
+ console.error(`⚠️ API 限流中断(已扫描 ${result.scannedShares}/${result.totalShares} 分享,失败 ${result.failedShares})。重跑命令可续扫(磁盘缓存加速)`);
560
+ } else if (result.partial) {
489
561
  console.error(`[verbose] ${result.failedShares} shares failed (partial result)`);
490
562
  }
491
563
  }
492
564
  };
493
565
 
566
+ cmds.cache = async (args, json) => {
567
+ const action = args._[0] || "info";
568
+ const { clearGroupCache } = group;
569
+ if (action === "clear") {
570
+ clearGroupCache();
571
+ output({ cleared: true }, json, () => console.log("✅ 群聊会话缓存已清除"));
572
+ return;
573
+ }
574
+ // info: 显示缓存目录位置
575
+ const path = require("path");
576
+ const os = require("os");
577
+ const dir = process.env.BDP_CACHE_DIR || path.join(os.homedir(), ".bdp", "cache");
578
+ output({ cacheDir: dir, ttl: { dir: "30min", shares: "5min" } }, json, () => {
579
+ console.log(`Cache dir: ${dir}`);
580
+ console.log(`Dir TTL: 30 min (gsearch/gtree 目录列表)`);
581
+ console.log(`Shares TTL: 5 min (gshares 分享列表)`);
582
+ console.log(`Clear with: bdp cache clear`);
583
+ });
584
+ };
585
+
494
586
  cmds.error = async (args, json) => {
495
587
  const code = args._[0] || "-3";
496
588
  const ERRORS = {
497
- "-3": "群分享 API 拒绝请求。已观察到大目录、错误的 msgId/fromUk、过期分享都可能触发。\n" +
498
- "处理:减小 --page-size(自动重试 50→20→10),或携带 gsearch 返回的 fromUk/msgId/parentFsId 信息重试;\n" +
589
+ "-3": "群分享 API 拒绝请求。已观察到大目录、错误的 msgId/fromUk、过期分享、请求过于频繁(限流)都可能触发。\n" +
590
+ "处理:减小 --page-size(自动重试 50→20→10)或 --concurrency;\n" +
591
+ " 限流表现为 errno=0 但只返回目录自身(self-echo),工具会自动退避重试(1s/2s);\n" +
592
+ " 大目录已支持自动全页遍历(每页上限 100,--max-pages 控制页数);\n" +
499
593
  " 若回退到父目录(fallback level=parent),Agent 应从父目录换路径继续遍历。",
500
594
  "-6": "接口鉴权失败。通常为 access_token/cookie 失效,请重新执行 bdp login。",
595
+ "2131": "群分享 msg_id 不属于该群(shareinfo 以 msg_id 为查找键,from_uk 不参与校验)。\n" +
596
+ "常见原因:gid 与 --from-uk/--msg-id/--parent-fs-id 来自不同群/分享库(参数错配)。\n" +
597
+ "处理:用 bdp gshares <gid> 查看本群分享(或 bdp gsearch <gid> <关键词>),\n" +
598
+ " 取其中一行的 fsId/fromUk/msgId 重试,保证 gid/fsId/fromUk/msgId 同源;\n" +
599
+ " fsId 为子目录时保持其父分享行的 fromUk/msgId 不变;\n" +
600
+ " 或去掉 --from-uk/--msg-id 让 CLI 自动解析(仅顶层分享)。\n" +
601
+ " 工具已支持:显式参数错配时自动按 fsId 纠正(结果含 autoResolved:true)。",
501
602
  };
502
603
  const msg = ERRORS[code] || `未知错误码 ${code},请提交 Issue: https://github.com/NkAntony777/baiduwangpan-cli/issues`;
503
604
  if (args.flags.jsonFile || json) {
@@ -534,8 +635,11 @@ GROUP CHAT OPERATIONS (群聊)
534
635
  [--depth N] [--concurrency N] [--max-nodes N]
535
636
  gls <gid> <fs_id> Browse files in a share
536
637
  [--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] [--no-unique] [--all]
638
+ gsearch <gid> <keyword> Search group file names (全量遍历目录, 缓存命中秒回)
639
+ [--page N] [--limit N] [--depth N] [--concurrency N]
640
+ [--max-pages N] [--max-requests N] [--no-unique]
641
+ [--all|--all-results] [--timeout N] [--no-cache]
642
+ cache [clear] Show cache info or clear session cache
539
643
  error <code> Explain an error code (e.g. -3)
540
644
 
541
645
  CONFIGURATION
@@ -559,9 +663,13 @@ OPTIONS
559
663
  --limit <N> Page size for gsearch (default 50)
560
664
  --depth <N> Recursive search/tree depth (gsearch default 1, gtree default 2)
561
665
  --max-nodes <N> Tree node cap (gtree default 2000)
666
+ --max-pages <N> Max pages per directory scan (gsearch/gtree default 50, 100 items/page)
667
+ --max-requests <N> Request budget per command (gsearch/gtree default 400, prevents throttle)
562
668
  --concurrency <N> Parallel share scans for gsearch/gtree (default 4)
669
+ --no-cache Disable in-process session cache (gsearch/gtree/gshares/gls)
563
670
  --no-unique Keep duplicates from different msgId (gsearch)
564
- --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)
565
673
 
566
674
  EXAMPLES
567
675
  bdp login --bduss abc123 --stoken def456
@@ -594,7 +702,7 @@ async function main() {
594
702
  await handler(args, json);
595
703
  } catch (e) {
596
704
  if (json) {
597
- console.log(JSON.stringify({ error: e.message }));
705
+ console.log(JSON.stringify({ error: e.message, ...(e.errno !== undefined ? { errno: e.errno } : {}) }));
598
706
  } else {
599
707
  console.error(`[ERROR] ${e.message}`);
600
708
  }
@@ -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
- let response;
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
- // 空闲 30 秒后断开浏览器会话(批处理请求间隔通常 <30s)
388
- disconnectTimer = setTimeout(disconnectBrowserSession, 30000);
382
+ // 空闲 90 秒后断开浏览器会话(并发批处理扫描中请求间隔可能较长,短超时会中途断连)
383
+ disconnectTimer = setTimeout(disconnectBrowserSession, 90000);
389
384
  disconnectTimer.unref();
390
385
  }
391
386
 
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 };