baiduwangpan-cli 1.1.5 → 1.6.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/bin/bdp.js CHANGED
@@ -5,16 +5,22 @@
5
5
  *
6
6
  * 网盘文件操作:
7
7
  * bdp ls [path] 列出目录
8
- * bdp search <keyword> [-p dir] 全盘搜索
8
+ * bdp search <keyword> [-p dir] 全盘搜索 (支持 --regex/--glob/--any-word/--exact)
9
9
  * bdp cat <path> 读取文件内容(免下载)
10
10
  * bdp head [-n N] <path> 读取前 N 行
11
11
  * bdp tail [-n N] <path> 读取后 N 行
12
12
  * bdp grep <pattern> <path> 搜索文件内容
13
13
  * bdp peek <path> 预览文件信息
14
- * bdp get <path> [-o dir] 下载文件
14
+ * bdp get <path> [-o dir] 下载文件 (size/md5 校验 + 断点续传; <gid>:<fs_id> 走群文件直下)
15
15
  * bdp put <local> <remote> 上传文件
16
16
  * bdp mkdir <path> 创建目录
17
17
  * bdp rm <path> 删除文件
18
+ * bdp mv <src> <dst> 移动/重命名
19
+ * bdp cp <src> <dst> 拷贝
20
+ * bdp quota 网盘配额
21
+ * bdp offline add <url> / list 离线下载
22
+ * bdp share set/cancel/list 分享管理
23
+ * bdp profile list/use/unset 多账号 profile 切换
18
24
  *
19
25
  * 群聊文件操作:
20
26
  * bdp groups 列出所有群组
@@ -36,6 +42,9 @@ const { config } = require("../lib");
36
42
  const pan = require("../lib/pan");
37
43
  const group = require("../lib/group");
38
44
  const partial = require("../lib/partial");
45
+ const pcsExtra = require("../lib/pcs-extra");
46
+ const { matchesName } = require("../lib/name-match");
47
+ const { ensureUtf8Console } = require("../lib/console");
39
48
 
40
49
  // ── Utils ──────────────────────────────────────────────
41
50
 
@@ -46,6 +55,13 @@ function formatSize(bytes) {
46
55
  return (bytes / Math.pow(1024, i)).toFixed(1) + " " + units[i];
47
56
  }
48
57
 
58
+ function formatSpeed(bps) {
59
+ if (!bps) return "-";
60
+ const units = ["B/s", "KB/s", "MB/s", "GB/s"];
61
+ const i = Math.floor(Math.log(bps) / Math.log(1024));
62
+ return (bps / Math.pow(1024, i)).toFixed(1) + " " + units[i];
63
+ }
64
+
49
65
  function output(data, jsonFlag, formatter) {
50
66
  if (jsonFlag) {
51
67
  console.log(JSON.stringify(data, null, 2));
@@ -116,6 +132,8 @@ function parseArgs(argv) {
116
132
  opts.flags.page = parseInt(argv[++i], 10) || 1;
117
133
  } else if (arg === "--page-size") {
118
134
  opts.flags.pageSize = parseInt(argv[++i], 10) || 50;
135
+ } else if (arg === "--top") {
136
+ opts.flags.top = parseInt(argv[++i], 10) || 15;
119
137
  } else if (arg === "--limit") {
120
138
  opts.flags.limit = parseInt(argv[++i], 10) || 50;
121
139
  } else if (arg === "--concurrency") {
@@ -130,6 +148,26 @@ function parseArgs(argv) {
130
148
  opts.flags.maxRequests = parseInt(argv[++i], 10) || 400;
131
149
  } else if (arg === "--no-cache") {
132
150
  opts.flags.noCache = true;
151
+ } else if (arg === "--resume") {
152
+ opts.flags.resume = true;
153
+ } else if (arg === "--force") {
154
+ opts.flags.force = true;
155
+ } else if (arg === "--progress") {
156
+ opts.flags.progress = true;
157
+ } else if (arg === "--no-verify-size") {
158
+ opts.flags.noVerifySize = true;
159
+ } else if (arg === "--regex") {
160
+ opts.flags.regex = true;
161
+ } else if (arg === "--glob") {
162
+ opts.flags.glob = true;
163
+ } else if (arg === "--pwd" || arg === "--password") {
164
+ opts.flags.pwd = argv[++i];
165
+ } else if (arg === "--combined") {
166
+ opts.flags.combined = true;
167
+ } else if (arg === "--dry-run") {
168
+ opts.flags.dryRun = true;
169
+ } else if (arg === "--profile") {
170
+ opts.flags.profile = argv[++i];
133
171
  } else if (arg === "--from-uk") {
134
172
  opts.flags.fromUk = argv[++i];
135
173
  } else if (arg === "--msg-id") {
@@ -233,10 +271,12 @@ cmds.whoami = async (args, json) => {
233
271
  webTransport: cfg.webTransport,
234
272
  pcsPath: cfg.pcsPath,
235
273
  configFile: config.CONFIG_FILE,
274
+ profile: config.getProfile() || "default(全局)",
236
275
  },
237
276
  json,
238
277
  (d) => {
239
278
  console.log(`Status: ${d.loggedIn ? "✅ Logged in" : "❌ Not logged in"}`);
279
+ console.log(`Profile: ${d.profile}`);
240
280
  console.log(`BDUSS: ${d.bduss}`);
241
281
  console.log(`STOKEN: ${d.stoken}`);
242
282
  console.log(`Web API: ${d.webTransport}`);
@@ -270,15 +310,58 @@ cmds.ls = async (args, json) => {
270
310
  }
271
311
  };
272
312
 
313
+ // 从 BaiduPCS-Go search 输出行提取路径(格式: 序号 大小 日期 时间 路径)
314
+ function searchPathFromLine(line) {
315
+ const m = line.trim().match(/^\d+\s+\S+\s+\S+\s+\S+\s+(.+)$/);
316
+ return m ? m[1].trim() : null;
317
+ }
318
+
319
+ // 从 glob/正则模式提取第一个字面片段(≥2 字符优先),供引擎宽词搜索。
320
+ // BaiduPCS-Go 的 search 自己会做 glob 展开("*.pdf" 直接返回 0 结果),
321
+ // 所以模式模式下必须用字面片段搜引擎,再用完整模式在客户端精确过滤。
322
+ function firstLiteralRun(pattern) {
323
+ const runs = String(pattern).split(/[.*+?^${}()|[\]\\*?]/).filter(Boolean);
324
+ return runs.find((r) => r.length >= 2) || runs[0] || null;
325
+ }
326
+
273
327
  cmds.search = async (args, json) => {
274
328
  const keyword = args.flags.k || args._[0] || "";
275
329
  const searchPath = args.flags.p || "/";
276
- if (!keyword) { console.error("Usage: bdp search <keyword> [-p /path]"); process.exit(1); }
277
- const raw = pan.search(keyword, searchPath);
330
+ if (!keyword) { console.error("Usage: bdp search <keyword> [-p /path] [--regex|--glob|--any-word|--exact]"); process.exit(1); }
331
+
332
+ // 客户端二次过滤:--regex 正则 / --glob 通配符 / --any-word OR / --exact 整名(缺省=子串,兼容旧行为)
333
+ const mode = args.flags.exact ? "exact" : args.flags.anyWord ? "any" : args.flags.regex ? "regex" : args.flags.glob ? "glob" : null;
334
+ let engineKw = keyword;
335
+ if (mode === "regex" || mode === "glob") {
336
+ engineKw = firstLiteralRun(keyword);
337
+ if (!engineKw) {
338
+ console.error(`[ERROR] 模式 ${keyword} 不含任何普通字符,无法搜索`);
339
+ process.exit(1);
340
+ }
341
+ } else if (mode === "any") {
342
+ engineKw = keyword.split(/\s+/)[0]; // 引擎只搜第一个词,客户端按 OR 过滤
343
+ }
344
+
345
+ const raw = pan.search(engineKw, searchPath);
346
+ let out = raw;
347
+ if (mode) {
348
+ const kept = raw.split("\n").filter((line) => {
349
+ const p = searchPathFromLine(line);
350
+ if (p === null) return false;
351
+ if (mode === "glob") {
352
+ // glob 同时匹配完整路径与文件名(shell 惯例: "玄空*" 命中以玄空开头的文件名)
353
+ const base = p.includes("/") ? p.slice(p.lastIndexOf("/") + 1) : p;
354
+ return matchesName(keyword, p, { mode }) || matchesName(keyword, base, { mode });
355
+ }
356
+ return matchesName(keyword, p, { mode });
357
+ });
358
+ out = kept.join("\n") + (kept.length ? "\n" : "");
359
+ }
360
+
278
361
  if (json) {
279
- console.log(JSON.stringify({ keyword, path: searchPath, raw }));
362
+ console.log(JSON.stringify({ keyword, engineKeyword: engineKw, path: searchPath, mode: mode || "substring", raw: out }));
280
363
  } else {
281
- console.log(raw);
364
+ console.log(out);
282
365
  }
283
366
  };
284
367
 
@@ -339,12 +422,15 @@ cmds.grep = async (args, json) => {
339
422
  cmds.peek = async (args, json) => {
340
423
  const filePath = args._[0];
341
424
  if (!filePath) { console.error("Usage: bdp peek <path>"); process.exit(1); }
342
- const info = pan.peek(filePath);
425
+ const info = await pan.peek(filePath);
343
426
  output(info, json, (d) => {
344
427
  console.log(`Path: ${d.path}`);
345
428
  console.log(`Size: ${d.size ? formatSize(d.size) : "unknown"}`);
346
429
  console.log(`Type: ${d.type || (d.isDir ? "directory" : "unknown")}`);
347
430
  console.log(`Dlink: ${d.dlink ? "✅" : "❌"}`);
431
+ if (d.md5) console.log(`MD5: ${d.md5}`);
432
+ if (d.serverCtime) console.log(`Server: ${new Date(d.serverCtime * 1000).toISOString()}`);
433
+ if (d.localCtime) console.log(`Local: ${new Date(d.localCtime * 1000).toISOString()}`);
348
434
  if (d.preview) {
349
435
  console.log(`\n── Preview ──`);
350
436
  d.preview.forEach((l) => console.log(l));
@@ -354,16 +440,70 @@ cmds.peek = async (args, json) => {
354
440
 
355
441
  cmds.get = async (args, json) => {
356
442
  const filePath = args._[0];
357
- if (!filePath) { console.error("Usage: bdp get <path> [-o dir]"); process.exit(1); }
358
- const saveTo = args.flags.o;
359
- const raw = pan.download(filePath, saveTo);
360
- output({ path: filePath, savedTo: saveTo || "default", raw }, json, () => console.log(raw));
443
+ if (!filePath) { console.error("Usage: bdp get <path> [-o dir] [--resume] [--force] [--progress]"); process.exit(1); }
444
+
445
+ // 群文件路由: `bdp get <gid>:<fs_id>` → gdownload 语义(免转存直下)
446
+ const gmatch = filePath.match(/^(\d+):(\d+)$/);
447
+ if (gmatch) {
448
+ const opts = { outDir: args.flags.o };
449
+ if (args.flags.fromUk) opts.fromUk = args.flags.fromUk;
450
+ if (args.flags.msgId) opts.msgId = args.flags.msgId;
451
+ if (args.flags.filename) opts.filename = args.flags.filename;
452
+ if (args.flags.dryRun) {
453
+ const info = { path: filePath, groupDownload: true, dryRun: true, note: "群文件下载(免转存,无 size 校验/断点续传)" };
454
+ emitJson(info, args);
455
+ return;
456
+ }
457
+ const info = await group.downloadFile(gmatch[1], gmatch[2], opts);
458
+ if (json || args.flags.jsonFile) emitJson({ path: filePath, groupDownload: true, name: info.name, size: info.size, localPath: info.path, fromUk: info.fromUk, msgId: info.msgId }, args);
459
+ else console.log(`✅ 群文件已下载 ${info.size} 字节 → ${info.path}`);
460
+ return;
461
+ }
462
+
463
+ const info = await pan.download(filePath, args.flags.o, {
464
+ resume: args.flags.resume === true,
465
+ force: args.flags.force === true,
466
+ progress: args.flags.progress === true,
467
+ verifySize: args.flags.noVerifySize !== true,
468
+ dryRun: args.flags.dryRun === true,
469
+ concurrency: args.flags.concurrency || undefined,
470
+ });
471
+
472
+ if (json || args.flags.jsonFile) {
473
+ emitJson(info, args);
474
+ return;
475
+ }
476
+
477
+ if (info.dryRun) {
478
+ const verb = info.action === "skip" ? "跳过(已存在且大小一致)" : info.action === "resume" ? "续传" : "下载";
479
+ console.log(`[dry-run] ${verb}: ${info.path} (${info.size != null ? formatSize(info.size) : "大小未知"}) → ${info.localPath}`);
480
+ return;
481
+ }
482
+
483
+ // BaiduPCS-Go 回退引擎(目录 / 无 dlink):保持旧输出
484
+ if (info.engine === "pcs") {
485
+ console.log(info.raw);
486
+ if (info.warning) console.error(`⚠️ ${info.warning}`);
487
+ return;
488
+ }
489
+
490
+ const head = info.skipped ? "⏭ 已存在且大小一致,跳过" : info.resumed ? "⏸ 断点续传完成" : "✅ 已下载";
491
+ const md5Part =
492
+ info.md5Match === true ? `,md5 一致 (${info.md5})`
493
+ : info.md5Match === false ? ",md5 与远端不一致!"
494
+ : info.md5 ? `,md5 ${info.md5}${info.md5Obfuscated ? "(远端 md5 为百度混淆值,未比对)" : ""}`
495
+ : "";
496
+ console.log(`${head} ${info.size} 字节 → ${info.localPath}${md5Part} ${formatSpeed(info.avgSpeedBps)}`);
361
497
  };
362
498
 
363
499
  cmds.put = async (args, json) => {
364
500
  const local = args._[0];
365
501
  const remote = args._[1];
366
502
  if (!local || !remote) { console.error("Usage: bdp put <local> <remote>"); process.exit(1); }
503
+ if (args.flags.dryRun) {
504
+ console.log(`[dry-run] 将上传 ${local} → ${remote}`);
505
+ return;
506
+ }
367
507
  const raw = pan.upload(local, remote);
368
508
  output({ local, remote, raw }, json, () => console.log(raw));
369
509
  };
@@ -378,10 +518,164 @@ cmds.mkdir = async (args, json) => {
378
518
  cmds.rm = async (args, json) => {
379
519
  const filePath = args._[0];
380
520
  if (!filePath) { console.error("Usage: bdp rm <path>"); process.exit(1); }
521
+ if (args.flags.dryRun) {
522
+ console.log(`[dry-run] 将删除 ${filePath}`);
523
+ return;
524
+ }
381
525
  const raw = pan.remove(filePath);
382
526
  output({ path: filePath, raw }, json, () => console.log(raw));
383
527
  };
384
528
 
529
+ // ── Pan 扩展命令(BaiduPCS-Go 引擎封装)───────────────
530
+
531
+ cmds.quota = async (args, json) => {
532
+ const q = pcsExtra.quota();
533
+ output(q, json, (d) => {
534
+ console.log(`总空间: ${formatSize(d.total)}`);
535
+ console.log(`已用: ${formatSize(d.used)} (${d.ratio.toFixed(2)}%)`);
536
+ console.log(`剩余: ${formatSize(d.free)}`);
537
+ if (d.username) console.log(`用户: ${d.username}`);
538
+ });
539
+ };
540
+
541
+ cmds.du = async (args, json) => {
542
+ const dirPath = args._[0] || "/";
543
+ verboseLog(args, `du ${dirPath} depth=${args.flags.depth || 3} concurrency=${args.flags.concurrency || 8} top=${args.flags.top || 15}`);
544
+ const result = await pan.du(dirPath, {
545
+ depth: args.flags.depth,
546
+ concurrency: args.flags.concurrency,
547
+ top: args.flags.top,
548
+ });
549
+ if (json || args.flags.jsonFile) {
550
+ emitJson(result, args);
551
+ return;
552
+ }
553
+ console.log(`${formatSize(result.size)} ${result.path} (${result.files} 文件 / ${result.dirs} 子目录${result.truncated ? " / 仅显示前" + (args.flags.top || 15) + "项" : ""}${result.failed ? ` / ${result.failed} 目录读取失败` : ""} ${(result.elapsedMs / 1000).toFixed(1)}s)`);
554
+ console.log("");
555
+ result.children.forEach((c) => {
556
+ console.log(` ${formatSize(c.size).padStart(10)} ${c.path}${c.capped ? " [深度封顶]" : ""}`);
557
+ });
558
+ };
559
+
560
+ cmds.recycle = async (args, json) => {
561
+ const action = args._[0] || "list";
562
+ if (action === "list") {
563
+ const r = pcsExtra.recycleList(args.flags.page || 1);
564
+ output(r, json, (d) => {
565
+ if (d.count === 0) { console.log("回收站为空"); return; }
566
+ console.log(`${d.count} 项回收站内容:`);
567
+ d.items.forEach((it) => {
568
+ console.log(` [${it.index}] ${it.path} ${it.size != null ? formatSize(it.size) : "-"} ${it.remainDays != null ? "剩余" + it.remainDays + "天" : ""} fs_id:${it.fsId}`);
569
+ });
570
+ });
571
+ } else if (action === "restore") {
572
+ const ids = args._.slice(1);
573
+ if (ids.length === 0) { console.error("Usage: bdp recycle restore <fs_id>..."); process.exit(1); }
574
+ const r = pcsExtra.recycleRestore(ids);
575
+ if (json || args.flags.jsonFile) emitJson({ restored: ids, ok: true }, args);
576
+ else console.log(`✅ 已还原 ${ids.length} 项: ${ids.join(", ")}`);
577
+ } else if (action === "clean") {
578
+ const r = pcsExtra.recycleClean();
579
+ if (json || args.flags.jsonFile) emitJson({ cleaned: true }, args);
580
+ else console.log("✅ 回收站已清空(不可恢复)");
581
+ } else {
582
+ console.error("Usage: bdp recycle list | restore <fs_id>... | clean");
583
+ process.exit(1);
584
+ }
585
+ };
586
+
587
+
588
+ cmds.mv = async (args, json) => {
589
+ const src = args._[0];
590
+ const dst = args._[1];
591
+ if (!src || !dst) { console.error("Usage: bdp mv <src> <dst>"); process.exit(1); }
592
+ const r = pcsExtra.mv(src, dst);
593
+ output(r, json, () => console.log(`✅ ${r.op === "rename" ? "已重命名" : "已移动"}: ${src} → ${dst}`));
594
+ };
595
+
596
+ cmds.cp = async (args, json) => {
597
+ const src = args._[0];
598
+ const dst = args._[1];
599
+ if (!src || !dst) { console.error("Usage: bdp cp <src> <dst>"); process.exit(1); }
600
+ const r = pcsExtra.cp(src, dst);
601
+ output(r, json, () => console.log(`✅ 已拷贝: ${src} → ${dst}`));
602
+ };
603
+
604
+ cmds.offline = async (args, json) => {
605
+ const action = args._[0] || "list";
606
+ if (action === "add") {
607
+ const url = args._[1];
608
+ if (!url) { console.error("Usage: bdp offline add <http_url> [--path /保存目录]"); process.exit(1); }
609
+ const r = pcsExtra.offlineAdd(url, args.flags.path);
610
+ output(r, json, () => console.log(`✅ 离线任务已创建: taskId=${r.taskId} (${url})${r.savePath ? ` → ${r.savePath}` : ""}`));
611
+ } else if (action === "list") {
612
+ const r = pcsExtra.offlineList();
613
+ output(r, json, (d) => {
614
+ console.log(`${d.count} 个离线任务:`);
615
+ d.tasks.forEach((t) => {
616
+ console.log(` [${t.index}] ${t.name} ${formatSize(t.size)} ${t.status} taskId:${t.taskId}`);
617
+ });
618
+ });
619
+ } else {
620
+ console.error("Usage: bdp offline add <http_url> [--path /保存目录] | bdp offline list");
621
+ process.exit(1);
622
+ }
623
+ };
624
+
625
+ cmds.share = async (args, json) => {
626
+ const action = args._[0] || "list";
627
+ if (action === "set") {
628
+ const p = args._[1];
629
+ if (!p) { console.error("Usage: bdp share set <path> [--pwd 提取码] [--combined]"); process.exit(1); }
630
+ const r = pcsExtra.shareSet(p, args.flags.pwd, { combined: args.flags.combined });
631
+ output(r, json, () => {
632
+ const link = r.combined ? `${r.link}?pwd=${r.pwd}` : r.link;
633
+ console.log(`✅ 分享创建成功: ${link}${!r.combined && r.pwd ? ` 提取码: ${r.pwd}` : ""} shareId:${r.shareId}`);
634
+ });
635
+ } else if (action === "cancel") {
636
+ const ids = args._.slice(1);
637
+ if (ids.length === 0) { console.error("Usage: bdp share cancel <shareId> [shareId2 ...]"); process.exit(1); }
638
+ const results = ids.map((id) => {
639
+ const r = pcsExtra.shareCancel(id);
640
+ return { shareId: id, ok: r.ok };
641
+ });
642
+ if (json || args.flags.jsonFile) emitJson(results, args);
643
+ else results.forEach((r) => console.log(`✅ 已取消分享 ${r.shareId}`));
644
+ } else if (action === "list") {
645
+ const r = pcsExtra.shareList(args.flags.page || 1);
646
+ output(r, json, (d) => {
647
+ console.log(`${d.count} 个分享:`);
648
+ d.shares.forEach((s) => {
649
+ console.log(` [${s.index}] ${s.link}${s.pwd ? ` 提取码: ${s.pwd}` : ""} shareId:${s.shareId} ${s.expireText || ""}`);
650
+ });
651
+ });
652
+ } else {
653
+ console.error("Usage: bdp share set <path> [--pwd 提取码] | bdp share cancel <shareId> | bdp share list");
654
+ process.exit(1);
655
+ }
656
+ };
657
+
658
+ cmds.profile = async (args, json) => {
659
+ const action = args._[0] || "list";
660
+ if (action === "use") {
661
+ const name = args._[1];
662
+ if (!name) { console.error("Usage: bdp profile use <name> | bdp profile unset | bdp profile list"); process.exit(1); }
663
+ const n = config.useProfile(name);
664
+ output({ profile: n }, json, () => console.log(`✅ 已切换到 profile: ${n}`));
665
+ } else if (action === "unset") {
666
+ config.unsetProfile();
667
+ output({ profile: null }, json, () => console.log("✅ 已切回全局账号"));
668
+ } else {
669
+ const current = config.getProfile() || "default(全局)";
670
+ const list = config.listProfiles();
671
+ output({ current, profiles: list }, json, (d) => {
672
+ console.log(`当前: ${d.current}`);
673
+ console.log(`${d.profiles.length} 个 profile:`);
674
+ d.profiles.forEach((p) => console.log(` ${p.name} ${p.bduss ? "bduss:***set***" : "(空)"}`));
675
+ });
676
+ }
677
+ };
678
+
385
679
  // ── Group Commands ─────────────────────────────────────
386
680
 
387
681
  cmds.groups = async (args, json) => {
@@ -641,12 +935,19 @@ cmds.cache = async (args, json) => {
641
935
  cmds.error = async (args, json) => {
642
936
  const code = args._[0] || "-3";
643
937
  const ERRORS = {
938
+ "-10": "群文件转存接口已被百度下线(2026-08 实测:/mbox/msg/transfer 对非空目录一律 errno=-10,\n" +
939
+ "补 logId/channel 参数仍拒绝,新版 UI 已移除'保存'按钮)。\n" +
940
+ "处理:群文件用 bdp gdownload 直接下载(单文件 ≤~20MB;多文件 zip 打包 ≤~100MB),\n" +
941
+ " 更大文件用百度官方客户端。",
942
+ "-6": "接口鉴权失败。通常为 access_token/cookie 失效,请重新执行 bdp login。",
943
+ "2": "接口不支持或参数缺失(errno=2)。\n" +
944
+ "实测普通文件接口(/api/download、/api/filemetas)对群文件参数返回 errno=2。\n" +
945
+ "处理:群文件请走 bdp gdownload / bdp gsearch 的群专用接口。",
644
946
  "-3": "群分享 API 拒绝请求。已观察到大目录、错误的 msgId/fromUk、过期分享、请求过于频繁(限流)都可能触发。\n" +
645
947
  "处理:减小 --page-size(自动重试 50→20→10)或 --concurrency;\n" +
646
948
  " 限流表现为 errno=0 但只返回目录自身(self-echo),工具会自动退避重试(1s/2s);\n" +
647
949
  " 大目录已支持自动全页遍历(每页上限 100,--max-pages 控制页数);\n" +
648
950
  " 若回退到父目录(fallback level=parent),Agent 应从父目录换路径继续遍历。",
649
- "-6": "接口鉴权失败。通常为 access_token/cookie 失效,请重新执行 bdp login。",
650
951
  "2131": "群分享 msg_id 不属于该群(shareinfo 以 msg_id 为查找键,from_uk 不参与校验)。\n" +
651
952
  "常见原因:gid 与 --from-uk/--msg-id/--parent-fs-id 来自不同群/分享库(参数错配)。\n" +
652
953
  "处理:用 bdp gshares <gid> 查看本群分享(或 bdp gsearch <gid> <关键词>),\n" +
@@ -654,6 +955,13 @@ cmds.error = async (args, json) => {
654
955
  " fsId 为子目录时保持其父分享行的 fromUk/msgId 不变;\n" +
655
956
  " 或去掉 --from-uk/--msg-id 让 CLI 自动解析(仅顶层分享)。\n" +
656
957
  " 工具已支持:显式参数错配时自动按 fsId 纠正(结果含 autoResolved:true)。",
958
+ "31062": "文件名非法(file name is invalid)。\n" +
959
+ "处理:路径含非法字符或格式不符,重命名网盘文件后再试。",
960
+ "31090": "zip 打包总大小超出上限(package is too large,实测 452MB 即报 31090,10MB 正常)。\n" +
961
+ "处理:拆包下载,或大文件用百度官方客户端。",
962
+ "31326": "dlink 被反盗链拦截(HTTP 403,hitcode 104)。\n" +
963
+ "实测直链只接受 ≤~5MB 的有界 Range 请求,无界请求返回 403。\n" +
964
+ "处理:CLI 已按 4MB 分块自动规避;如仍报错,重跑 bdp get --resume(换新 dlink)。",
657
965
  };
658
966
  const msg = ERRORS[code] || `未知错误码 ${code},请提交 Issue: https://github.com/NkAntony777/baiduwangpan-cli/issues`;
659
967
  if (args.flags.jsonFile || json) {
@@ -665,100 +973,463 @@ cmds.error = async (args, json) => {
665
973
 
666
974
  // ── Main ───────────────────────────────────────────────
667
975
 
668
- const HELP = `bdp — 百度网盘 CLI
669
-
670
- USAGE
671
- bdp <command> [options] [arguments]
672
-
673
- PAN FILE OPERATIONS (全盘)
674
- ls [path] List directory
675
- search <keyword> [-p dir] Search files by name
676
- cat <path> Read file content (no download, max 1MB)
677
- head [-n N] <path> Read first N lines (default 20)
678
- tail [-n N] <path> Read last N lines (default 20)
679
- grep <pattern> <path> Search inside file content
680
- peek <path> Preview file info + first lines
681
- get <path> [-o dir] Download file
682
- put <local> <remote> Upload file
683
- mkdir <path> Create directory
684
- rm <path> Delete file/dir
685
-
686
- GROUP CHAT OPERATIONS (群聊)
687
- groups List all groups
688
- gshares <gid> List share libraries in group
689
- gtree <gid> Build directory tree (BFS)
690
- [--depth N] [--concurrency N] [--max-nodes N]
691
- gls <gid> <fs_id> Browse files in a share
692
- [--page N] [--page-size N] [--from-uk X] [--msg-id Y] [--parent-fs-id Z]
693
- gsearch <gid> <keyword> Search group file names (全量遍历目录, 缓存命中秒回)
694
- [--page N] [--limit N] [--depth N] [--concurrency N]
695
- [--max-pages N] [--max-requests N] [--no-unique]
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]
701
- cache [clear] Show cache info or clear session cache
702
- error <code> Explain an error code (e.g. -3)
703
-
704
- CONFIGURATION
705
- login Browser QR login (auto-detect cookies)
706
- login --bduss X --stoken Y Save credentials manually
707
- whoami Check login status
708
- config Show configuration
709
-
710
- OPTIONS
711
- --json Output JSON (for Agent consumption)
712
- --json-file <path> Write JSON to file (UTF-8, bypasses console encoding)
713
- --legacy-json Output bare array (gls/gsearch legacy format)
714
- --verbose Write progress/log to stderr only
715
- -n <N> Number of lines (head/tail)
716
- -p <path> Search path (search)
717
- -i Ignore case (grep)
718
- -N Show line numbers (cat/grep)
719
- -o <dir> Output directory (get)
720
- --page <N> Page number (gls/gsearch)
721
- --page-size <N> Page size for gls (default 50, max 100)
722
- --limit <N> Page size for gsearch (default 50)
723
- --depth <N> Recursive search/tree depth (gsearch default 1, gtree default 2)
724
- --max-nodes <N> Tree node cap (gtree default 2000)
725
- --max-pages <N> Max pages per directory scan (gsearch/gtree default 50, 100 items/page)
726
- --max-requests <N> Request budget per command (gsearch/gtree default 400, prevents throttle)
727
- --concurrency <N> Parallel share scans for gsearch/gtree (default 4)
728
- --no-cache Disable in-process session cache (gsearch/gtree/gshares/gls)
729
- --no-unique Keep duplicates from different msgId (gsearch)
730
- --all, --all-results Fetch all results, no paging (gsearch; slow but never truncates)
731
- --timeout <N> Abort after N seconds and return partial results (gsearch; 0=unlimited)
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)
735
-
736
- EXAMPLES
737
- bdp login --bduss abc123 --stoken def456
738
- bdp ls /
739
- bdp search "报告"
740
- bdp head -n 50 /文档/日志.txt
741
- bdp groups --json
742
- bdp gshares 539478953581833690
743
- bdp gls 539478953581833690 742474845517885 --json
744
- bdp gls 539478953581833690 292608024165826 --from-uk 2642611875 --msg-id 5069931974377329661 --json
745
- bdp gsearch 539478953581833690 "倪海厦" --limit 20 --json
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 打包下载
749
- bdp error -3`;
976
+
977
+ // ── 命令注册表(bdp help / 未知命令提示的唯一数据源)──────────────────
978
+ // 每项: { group: "pan"|"group"|"config", usage, desc, options?, examples?, note? }
979
+ // 新增命令时必须在此登记,bdp help 才会展示(避免手写 HELP 漂移)
980
+ const COMMANDS = {
981
+ ls: {
982
+ group: "pan",
983
+ usage: "bdp ls [path]",
984
+ desc: "列出目录",
985
+ flags: [],
986
+ examples: ["bdp ls /", "bdp ls /文档 --json"],
987
+ },
988
+ search: {
989
+ group: "pan",
990
+ usage: "bdp search <keyword> [-p /path]",
991
+ desc: "按文件名递归搜索,支持模式过滤",
992
+ flags: ["k", "p", "regex", "glob", "anyWord", "exact"],
993
+ options: [
994
+ "-p <dir> 搜索目录 (默认 /)",
995
+ "--regex 正则模式",
996
+ "--glob shell 通配符 (如 玄空*.pdf)",
997
+ "--any-word 空格分词命中任意一个 (OR)",
998
+ "--exact 整名精确匹配",
999
+ ],
1000
+ examples: ['bdp search "报告"', 'bdp search "玄空*.pdf" --glob', 'bdp search "玄空.+pdf$" --regex --json'],
1001
+ },
1002
+ cat: {
1003
+ group: "pan",
1004
+ usage: "bdp cat <path>",
1005
+ desc: "读取文件内容(免下载,限 1MB)",
1006
+ flags: ["number"],
1007
+ options: ["-N 显示行号"],
1008
+ examples: ["bdp cat /文档/data.json"],
1009
+ },
1010
+ head: {
1011
+ group: "pan",
1012
+ usage: "bdp head [-n N] <path>",
1013
+ desc: "读取前 N 行(默认 20)",
1014
+ flags: ["n"],
1015
+ options: ["-n <N> 行数"],
1016
+ examples: ["bdp head -n 50 /文档/日志.txt"],
1017
+ },
1018
+ tail: {
1019
+ group: "pan",
1020
+ usage: "bdp tail [-n N] <path>",
1021
+ desc: "读取后 N 行(默认 20)",
1022
+ flags: ["n"],
1023
+ options: ["-n <N> 行数"],
1024
+ examples: ["bdp tail -n 30 /文档/log.txt"],
1025
+ },
1026
+ grep: {
1027
+ group: "pan",
1028
+ usage: "bdp grep <pattern> <path>",
1029
+ desc: "在文件内容中搜索",
1030
+ flags: ["i", "number"],
1031
+ options: ["-i 忽略大小写", "-N 显示行号"],
1032
+ examples: ["bdp grep 关键词 /文档/file.txt"],
1033
+ },
1034
+ peek: {
1035
+ group: "pan",
1036
+ usage: "bdp peek <path>",
1037
+ desc: "预览文件信息 + 前 10 行(含 md5/ctime)",
1038
+ flags: [],
1039
+ examples: ["bdp peek /文档/report.pdf --json"],
1040
+ },
1041
+ get: {
1042
+ group: "pan",
1043
+ usage: "bdp get <path> [-o dir]",
1044
+ desc: "下载文件(直链分块、size 校验、断点续传)",
1045
+ flags: ["o", "resume", "force", "progress", "noVerifySize", "dryRun", "concurrency", "fromUk", "msgId", "filename"],
1046
+ note: "路径为 <gid>:<fs_id> 时自动走群文件直下(免转存)",
1047
+ options: [
1048
+ "-o <dir> 输出目录(默认当前目录)",
1049
+ "--resume 断点续传(存在部分文件时自动)",
1050
+ "--force 覆盖重下",
1051
+ "--progress 实时进度 + 速度",
1052
+ "--no-verify-size 关闭下载后 size 校验",
1053
+ "--dry-run 模拟,不落盘",
1054
+ "--concurrency <N> 并发分块数 (默认3, 仅大文件; 1=串行)",
1055
+ ],
1056
+ examples: [
1057
+ "bdp get /文档/报告.pdf -o ./pdfs --progress",
1058
+ "bdp get /文档/大文件.zip --resume --json",
1059
+ "bdp get 539478953581833690:954615608563687 -o ./g",
1060
+ ],
1061
+ },
1062
+ put: {
1063
+ group: "pan",
1064
+ usage: "bdp put <local> <remote>",
1065
+ desc: "上传文件",
1066
+ flags: ["dryRun"],
1067
+ options: ["--dry-run 模拟"],
1068
+ examples: ["bdp put ./report.pdf /文档/"],
1069
+ },
1070
+ mkdir: {
1071
+ group: "pan",
1072
+ usage: "bdp mkdir <path>",
1073
+ desc: "创建目录",
1074
+ flags: [],
1075
+ examples: ["bdp mkdir /新项目"],
1076
+ },
1077
+ rm: {
1078
+ group: "pan",
1079
+ usage: "bdp rm <path>",
1080
+ desc: "删除文件/目录",
1081
+ flags: ["dryRun"],
1082
+ note: "⚠️ 高危操作,Agent 使用前必须用户确认",
1083
+ options: ["--dry-run 模拟"],
1084
+ examples: ["bdp rm /临时文件.txt"],
1085
+ },
1086
+ mv: {
1087
+ group: "pan",
1088
+ usage: "bdp mv <src> <dst>",
1089
+ desc: "移动/重命名文件/目录",
1090
+ flags: [],
1091
+ examples: ["bdp mv /文档/a.pdf /文档/b.pdf", "bdp mv /文档/a.pdf /备份/"],
1092
+ },
1093
+ cp: {
1094
+ group: "pan",
1095
+ usage: "bdp cp <src> <dst>",
1096
+ desc: "拷贝文件/目录",
1097
+ flags: [],
1098
+ examples: ["bdp cp /文档/a.pdf /备份/"],
1099
+ },
1100
+ quota: {
1101
+ group: "pan",
1102
+ usage: "bdp quota",
1103
+ desc: "网盘配额(总/已用/剩余)",
1104
+ flags: [],
1105
+ examples: ["bdp quota --json"],
1106
+ },
1107
+ du: {
1108
+ group: "pan",
1109
+ usage: "bdp du [path]",
1110
+ desc: "目录容量分析(递归统计,找空间大户)",
1111
+ flags: ["depth", "top", "concurrency"],
1112
+ options: [
1113
+ "--depth <N> 递归深度 (默认3)",
1114
+ "--top <N> 显示最大的 N 个子目录 (默认15)",
1115
+ "--concurrency <N> 并发扫描数 (默认8)",
1116
+ ],
1117
+ examples: ["bdp du /", "bdp du /玄学 --depth 4 --top 20", "bdp du / --json"],
1118
+ },
1119
+ offline: {
1120
+ group: "pan",
1121
+ usage: "bdp offline add <http_url> [--path /目录] | bdp offline list",
1122
+ desc: "离线下载(百度服务器代下)",
1123
+ flags: ["p"],
1124
+ options: ["--path <dir> 保存目录"],
1125
+ examples: ["bdp offline add http://example.com/big.iso --path /下载", "bdp offline list"],
1126
+ },
1127
+ recycle: {
1128
+ group: "pan",
1129
+ usage: "bdp recycle list | restore <fs_id>... | clean",
1130
+ desc: "回收站管理(列出/还原/清空)",
1131
+ flags: ["page"],
1132
+ note: "clean 清空不可恢复,使用前务必确认;BaiduPCS-Go 不二次确认",
1133
+ options: ["--page <N> 回收站列表页码 (默认1)"],
1134
+ examples: ["bdp recycle list", "bdp recycle restore 378804494923604", "bdp recycle clean"],
1135
+ },
1136
+ share: {
1137
+ group: "pan",
1138
+ usage: "bdp share set <path> [--pwd P] | bdp share cancel <id>... | bdp share list",
1139
+ desc: "分享管理(创建/取消/列出)",
1140
+ flags: ["pwd", "combined", "page"],
1141
+ note: "share list 受 BaiduPCS-Go v4.0.1 引擎 bug 限制(panic),用 share cancel 管理已知分享",
1142
+ options: ["--pwd <P> 提取码", "--combined 输出带提取码的链接"],
1143
+ examples: ["bdp share set /文档/报告.pdf --pwd 1234 --combined", "bdp share cancel 23220383577"],
1144
+ },
1145
+ groups: {
1146
+ group: "group",
1147
+ usage: "bdp groups",
1148
+ desc: "列出所有群组",
1149
+ flags: [],
1150
+ examples: ["bdp groups --json"],
1151
+ },
1152
+ gshares: {
1153
+ group: "group",
1154
+ usage: "bdp gshares <gid>",
1155
+ desc: "列出群内分享库",
1156
+ flags: ["noCache"],
1157
+ options: ["--no-cache 禁用缓存"],
1158
+ examples: ["bdp gshares 539478953581833690"],
1159
+ },
1160
+ gls: {
1161
+ group: "group",
1162
+ usage: "bdp gls <gid> <fs_id>",
1163
+ desc: "浏览分享库内容(分页)",
1164
+ flags: ["page", "pageSize", "fromUk", "msgId", "parentFsId", "noCache"],
1165
+ options: [
1166
+ "--page <N> 页码 (默认1)",
1167
+ "--page-size <N> 每页条数 (默认50, 最大100)",
1168
+ "--from-uk <X> 分享者 uk",
1169
+ "--msg-id <Y> 消息 id",
1170
+ "--parent-fs-id <Z> 父目录 fsId",
1171
+ "--no-cache 禁用缓存",
1172
+ ],
1173
+ examples: ["bdp gls 539478953581833690 742474845517885 --json"],
1174
+ },
1175
+ gtree: {
1176
+ group: "group",
1177
+ usage: "bdp gtree <gid>",
1178
+ desc: "构建群目录树(BFS)",
1179
+ flags: ["depth", "concurrency", "maxNodes", "maxPages", "maxRequests", "noCache"],
1180
+ options: ["--depth <N> 深度 (默认2)", "--concurrency <N> 并发 (默认4)", "--max-nodes <N> 节点上限 (默认2000)"],
1181
+ examples: ["bdp gtree 539478953581833690 --depth 3"],
1182
+ },
1183
+ gsearch: {
1184
+ group: "group",
1185
+ usage: "bdp gsearch <gid> <keyword>",
1186
+ desc: "搜索群文件名(全量遍历,缓存命中秒回)",
1187
+ flags: ["page", "limit", "concurrency", "unique", "noUnique", "all", "timeout", "timeoutMs", "savePartial", "anyWord", "exact", "depth", "maxPages", "maxRequests", "noCache"],
1188
+ note: "默认多关键词 AND(空格分词每词都需出现);--any-word 切 OR",
1189
+ options: [
1190
+ "--page <N> 页码 (默认1)",
1191
+ "--limit <N> 每页条数 (默认50)",
1192
+ "--depth <N> 递归深度 (默认1)",
1193
+ "--concurrency <N> 并发扫描数 (默认4)",
1194
+ "--max-pages <N> 单目录最大页数 (默认50)",
1195
+ "--max-requests <N> 请求预算 (默认400)",
1196
+ "--all|--all-results 忽略分页取全部",
1197
+ "--timeout <N> 超时返回部分结果",
1198
+ "--save-partial 部分结果落盘",
1199
+ "--any-word OR 模式",
1200
+ "--exact 整名匹配",
1201
+ "--no-unique 保留不同 msgId 重复项",
1202
+ "--no-cache 禁用缓存",
1203
+ ],
1204
+ examples: [
1205
+ 'bdp gsearch 539478953581833690 "倪海厦" --limit 20 --json',
1206
+ 'bdp gsearch 539478953581833690 "玄空 飞星" --json',
1207
+ ],
1208
+ },
1209
+ gdownload: {
1210
+ group: "group",
1211
+ usage: "bdp gdownload <gid> <fs_id>...",
1212
+ desc: "直接下载群文件(免转存)",
1213
+ flags: ["o", "filename", "fromUk", "msgId", "noCache"],
1214
+ note: "单 fs_id 直下;多个 fs_id(同一分享消息)自动 zip 打包",
1215
+ options: ["-o <dir> 输出目录", "--filename <NAME> zip 包名", "--from-uk <X>", "--msg-id <Y>", "--no-cache"],
1216
+ examples: [
1217
+ "bdp gdownload 539478953581833690 954615608563687 -o ./downloads --from-uk 1101635869133 --msg-id 713945176566573051",
1218
+ "bdp gdownload 539478953581833690 527948256537856 986718405708023 -o ./zips",
1219
+ ],
1220
+ },
1221
+ cache: {
1222
+ group: "group",
1223
+ usage: "bdp cache [clear]",
1224
+ desc: "查看/清空群会话缓存",
1225
+ flags: [],
1226
+ examples: ["bdp cache", "bdp cache clear"],
1227
+ },
1228
+ error: {
1229
+ group: "group",
1230
+ usage: "bdp error <code>",
1231
+ desc: "解释错误码",
1232
+ flags: [],
1233
+ examples: ["bdp error -3", "bdp error 31326"],
1234
+ },
1235
+ login: {
1236
+ group: "config",
1237
+ usage: "bdp login [--bduss X --stoken Y]",
1238
+ desc: "登录(浏览器扫码或手动凭证)",
1239
+ flags: ["bduss", "stoken"],
1240
+ options: ["--bduss <X> 手动凭证", "--stoken <Y> 手动凭证"],
1241
+ examples: ["bdp login", "bdp login --bduss abc123 --stoken def456"],
1242
+ },
1243
+ whoami: {
1244
+ group: "config",
1245
+ usage: "bdp whoami",
1246
+ desc: "查看登录状态与当前 profile",
1247
+ flags: [],
1248
+ examples: ["bdp whoami --json"],
1249
+ },
1250
+ config: {
1251
+ group: "config",
1252
+ usage: "bdp config",
1253
+ desc: "查看配置",
1254
+ flags: [],
1255
+ examples: ["bdp config"],
1256
+ },
1257
+ profile: {
1258
+ group: "config",
1259
+ usage: "bdp profile list | use <name> | unset",
1260
+ desc: "多账号 profile 管理",
1261
+ flags: [],
1262
+ note: "也可用全局参数 --profile <name> 切换(命令前/后均可)",
1263
+ examples: ["bdp --profile svip login", "bdp profile list"],
1264
+ },
1265
+ help: {
1266
+ group: "config",
1267
+ usage: "bdp help [command]",
1268
+ desc: "查看帮助([command] 查看单命令详情;--json 结构化输出)",
1269
+ flags: [],
1270
+ examples: ["bdp help", "bdp help get", "bdp help --json"],
1271
+ },
1272
+ };
1273
+
1274
+ const GROUP_LABELS = {
1275
+ pan: "PAN FILE OPERATIONS (全盘)",
1276
+ group: "GROUP CHAT OPERATIONS (群聊)",
1277
+ config: "CONFIGURATION",
1278
+ };
1279
+
1280
+ // 全局选项(跨命令通用);命令专属选项见各命令注册项
1281
+ const GLOBAL_OPTIONS = [
1282
+ "--profile <name> 切换账号 profile(命令前/后均可)",
1283
+ "--json JSON 结构化输出 (Agent 友好)",
1284
+ "--json-file <path> JSON 写入文件 (UTF-8, 绕开控制台编码)",
1285
+ "--legacy-json 输出裸数组 (gls/gsearch legacy)",
1286
+ "--verbose 进度/日志只写 stderr",
1287
+ ];
1288
+
1289
+ // CJK 等宽字符按 2 列计(与 pcs-extra 的显示宽度规则一致)
1290
+ function padWidth(str, width) {
1291
+ let w = 0;
1292
+ for (const ch of String(str)) w += /[\u1100-\u115f\u2e80-\ua4cf\uac00-\ud7a3\uf900-\ufaff\ufe30-\ufe4f\uff00-\uff60\uffe0-\uffe6]/.test(ch) ? 2 : 1;
1293
+ return String(str) + " ".repeat(Math.max(0, width - w));
1294
+ }
1295
+
1296
+ function buildHelp() {
1297
+ const lines = ["bdp — 百度网盘 CLI", "", "USAGE", " bdp <command> [options] [arguments]", " bdp help [command] — 单命令详情 (--json 结构化)", ""];
1298
+ for (const [gkey, label] of Object.entries(GROUP_LABELS)) {
1299
+ lines.push(label);
1300
+ for (const [name, c] of Object.entries(COMMANDS)) {
1301
+ if (c.group !== gkey) continue;
1302
+ // 总览只显示主用法(复合用法取 " | " 前段,完整版见 bdp help <command>)
1303
+ const usageLine = c.usage.includes(" | ") ? c.usage.split(" | ")[0] : c.usage;
1304
+ lines.push(` ${padWidth(usageLine, 42)}${c.desc}`);
1305
+ }
1306
+ lines.push("");
1307
+ }
1308
+ lines.push("OPTIONS (全局)");
1309
+ for (const o of GLOBAL_OPTIONS) lines.push(` ${o}`);
1310
+ lines.push("");
1311
+ lines.push("EXAMPLES(每个命令的典型用法,bdp help <command> 查看更多)");
1312
+ for (const [name, c] of Object.entries(COMMANDS)) {
1313
+ if (c.examples && c.examples[0]) lines.push(` ${c.examples[0]}`);
1314
+ }
1315
+ return lines.join("\n") + "\n";
1316
+ }
1317
+
1318
+ function buildCommandHelp(name) {
1319
+ const c = COMMANDS[name];
1320
+ if (!c) return null;
1321
+ const lines = [`bdp ${name} — ${c.desc}`, ""];
1322
+ if (c.note) {
1323
+ lines.push("NOTE");
1324
+ lines.push(` ${c.note}`);
1325
+ lines.push("");
1326
+ }
1327
+ lines.push("USAGE");
1328
+ lines.push(` ${c.usage}`);
1329
+ lines.push("");
1330
+ if (c.options && c.options.length) {
1331
+ lines.push("OPTIONS");
1332
+ for (const o of c.options) lines.push(` ${o}`);
1333
+ lines.push("");
1334
+ }
1335
+ if (c.examples && c.examples.length) {
1336
+ lines.push("EXAMPLES");
1337
+ for (const ex of c.examples) lines.push(` ${ex}`);
1338
+ }
1339
+ return lines.join("\n") + "\n";
1340
+ }
1341
+
1342
+
1343
+ // 全局通用选项(所有命令都接受);命令专属选项见 COMMANDS[name].flags
1344
+ const GLOBAL_FLAGS = ['json', 'jsonFile', 'legacyJson', 'verbose', 'profile'];
1345
+
1346
+ // 防呆:未知选项直接报错(agent 拼错参数立刻发现,而不是静默忽略)
1347
+ function validateFlags(command, flags, cmdMeta) {
1348
+ if (!cmdMeta) return [];
1349
+ const allowed = new Set(GLOBAL_FLAGS.concat(cmdMeta.flags || []));
1350
+ return Object.keys(flags).filter((f) => !allowed.has(f));
1351
+ }
1352
+
1353
+ const HELP = buildHelp();
1354
+
1355
+ cmds.help = async (args, json) => {
1356
+ const target = args._[0];
1357
+ if (target) {
1358
+ if (!COMMANDS[target]) {
1359
+ if (json || args.flags.jsonFile) {
1360
+ emitJson({ error: `未知命令: ${target}`, available: Object.keys(COMMANDS) }, args);
1361
+ } else {
1362
+ console.error(`[ERROR] 未知命令: ${target}。用 bdp help 查看全部命令。`);
1363
+ }
1364
+ process.exit(1);
1365
+ }
1366
+ const c = COMMANDS[target];
1367
+ if (json || args.flags.jsonFile) {
1368
+ emitJson({ name: target, ...c }, args);
1369
+ } else {
1370
+ console.log(buildCommandHelp(target));
1371
+ }
1372
+ return;
1373
+ }
1374
+ if (json || args.flags.jsonFile) {
1375
+ emitJson(
1376
+ {
1377
+ tool: "bdp",
1378
+ version: require("../package.json").version,
1379
+ usage: "bdp <command> [options] [arguments]",
1380
+ globalOptions: GLOBAL_OPTIONS,
1381
+ commands: Object.entries(COMMANDS).map(([name, c]) => ({ name, ...c })),
1382
+ },
1383
+ args
1384
+ );
1385
+ } else {
1386
+ console.log(HELP);
1387
+ }
1388
+ };
1389
+
750
1390
 
751
1391
  async function main() {
1392
+ // Windows 控制台编码兜底:TTY 且非 UTF-8 代码页时切换 chcp 65001(幂等、静默失败)
1393
+ ensureUtf8Console();
1394
+
752
1395
  const argv = process.argv.slice(2);
753
- const command = argv[0];
754
- const rest = argv.slice(1);
755
- const args = parseArgs(rest);
1396
+ // 全局 --profile:支持 `bdp --profile <name> <cmd>`(命令前)或 `bdp <cmd> --profile <name>`(命令后)
1397
+ let rest = argv;
1398
+ let profileName;
1399
+ if (argv[0] === "--profile") {
1400
+ profileName = argv[1];
1401
+ rest = argv.slice(2);
1402
+ } else if (argv[0] && argv[0].startsWith("--profile=")) {
1403
+ profileName = argv[0].slice("--profile=".length);
1404
+ rest = argv.slice(1);
1405
+ }
1406
+ const command = rest[0];
1407
+ const args = parseArgs(rest.slice(1));
756
1408
  const json = args.flags.json || false;
757
1409
 
1410
+ // 切换账号 profile(须在命令 handler 之前,get()/isLoggedIn() 自动生效)
1411
+ const profile = profileName !== undefined ? profileName : args.flags.profile;
1412
+ if (profile !== undefined) {
1413
+ try {
1414
+ config.useProfile(profile);
1415
+ } catch (e) {
1416
+ console.error(`[ERROR] ${e.message}`);
1417
+ process.exit(1);
1418
+ }
1419
+ }
1420
+
758
1421
  const handler = cmds[command];
759
1422
  if (!handler) {
1423
+ if (command) console.error(`[ERROR] 未知命令: ${command}(用 bdp help 查看全部命令,bdp help <命令> 查看用法)`);
760
1424
  console.log(HELP);
761
- process.exit(0);
1425
+ process.exit(command ? 1 : 0);
1426
+ }
1427
+
1428
+ // 防呆:未知选项立即报错(避免 agent 拼错参数被静默忽略)
1429
+ const unknownFlags = validateFlags(command, args.flags, COMMANDS[command]);
1430
+ if (unknownFlags.length > 0) {
1431
+ console.error(`[ERROR] 命令 ${command} 不支持选项: --${unknownFlags.join(", --")}(bdp help ${command} 查看合法选项)`);
1432
+ process.exit(1);
762
1433
  }
763
1434
 
764
1435
  let exitCode = 0;