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 +33 -7
- package/bin/bdp.js +54 -5
- package/lib/config.js +64 -16
- package/lib/group.js +69 -4
- package/package.json +1 -1
- package/scripts/postinstall.js +110 -65
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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]
|
|
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
|
|
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
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
package/scripts/postinstall.js
CHANGED
|
@@ -4,8 +4,15 @@
|
|
|
4
4
|
* postinstall.js — 自动下载 BaiduPCS-Go 二进制
|
|
5
5
|
*
|
|
6
6
|
* 跨平台支持: Windows / macOS / Linux (x64, arm64, 386)
|
|
7
|
-
* 下载加速:
|
|
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
|
-
|
|
116
|
+
fail(new Error("too many redirects"));
|
|
96
117
|
return;
|
|
97
118
|
}
|
|
98
119
|
|
|
99
120
|
const client = url.startsWith("https") ? https : http;
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
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 ===
|
|
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
|
-
|
|
167
|
-
|
|
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
|
-
|
|
222
|
-
|
|
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
|
|
234
|
-
if (!ok) {
|
|
235
|
-
console.warn(" bdp: ⚠️
|
|
236
|
-
console.warn(` bdp:
|
|
237
|
-
console.warn(` bdp:
|
|
238
|
-
console.warn(` bdp:
|
|
239
|
-
console.warn(" bdp:
|
|
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: ⚠️
|
|
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: ⚠️
|
|
277
|
-
console.warn(` bdp:
|
|
321
|
+
console.warn(" bdp: ⚠️ 解压后未找到二进制。");
|
|
322
|
+
console.warn(` bdp: 请手动解压并把 ${binName} 放到: ${PKG_ROOT}`);
|
|
278
323
|
}
|
|
279
324
|
|
|
280
325
|
// 清理 zip 和解压目录
|