baiduwangpan-cli 1.0.0 → 1.0.2
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 +66 -3
- package/bin/bdp.js +247 -24
- package/lib/browser-login.js +432 -0
- package/lib/config.js +18 -2
- package/lib/group.js +345 -56
- package/lib/http.js +38 -6
- package/package.json +6 -2
package/lib/group.js
CHANGED
|
@@ -1,30 +1,49 @@
|
|
|
1
1
|
const http = require("./http");
|
|
2
2
|
const { getBdstoken } = http;
|
|
3
3
|
|
|
4
|
+
// ── 工具 ──────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
function toStr(v) {
|
|
7
|
+
if (v === null || v === undefined) return "";
|
|
8
|
+
return String(v);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
class ShareApiError extends Error {
|
|
12
|
+
constructor(errno, message) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.errno = errno;
|
|
15
|
+
this.name = "ShareApiError";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ── 群组列表 ──────────────────────────────────────────
|
|
20
|
+
|
|
4
21
|
async function listGroups() {
|
|
5
22
|
const bdstoken = await getBdstoken();
|
|
6
23
|
if (!bdstoken) throw new Error("Cannot get bdstoken — check BDUSS/STOKEN config");
|
|
7
24
|
|
|
8
|
-
const data = http.
|
|
25
|
+
const data = await http.webJson(
|
|
9
26
|
`${http.PAN_BASE}/mbox/group/list?clienttype=0&app_id=${http.APP_ID}&web=1&bdstoken=${bdstoken}`
|
|
10
27
|
);
|
|
11
28
|
|
|
12
29
|
if (data.errno !== 0) throw new Error(`API error: errno=${data.errno}`);
|
|
13
30
|
|
|
14
31
|
return (data.records || []).map((g) => ({
|
|
15
|
-
gid: g.gid,
|
|
32
|
+
gid: toStr(g.gid),
|
|
16
33
|
name: g.name,
|
|
17
|
-
gnum: g.gnum,
|
|
34
|
+
gnum: toStr(g.gnum),
|
|
18
35
|
description: g.gdesc || "",
|
|
19
|
-
type: g.type,
|
|
36
|
+
type: toStr(g.type),
|
|
20
37
|
created: g.ctime,
|
|
21
38
|
}));
|
|
22
39
|
}
|
|
23
40
|
|
|
41
|
+
// ── 群内分享库 ────────────────────────────────────────
|
|
42
|
+
|
|
24
43
|
async function listShares(gid) {
|
|
25
44
|
const bdstoken = await getBdstoken();
|
|
26
|
-
const data = http.
|
|
27
|
-
`${http.PAN_BASE}/mbox/group/listshare?clienttype=0&app_id=${http.APP_ID}&web=1&type=2&gid=${gid}&limit=50&desc=1&bdstoken=${bdstoken}`,
|
|
45
|
+
const data = await http.webJson(
|
|
46
|
+
`${http.PAN_BASE}/mbox/group/listshare?clienttype=0&app_id=${http.APP_ID}&web=1&type=2&gid=${toStr(gid)}&limit=50&desc=1&bdstoken=${bdstoken}`,
|
|
28
47
|
{ method: "POST", body: "" }
|
|
29
48
|
);
|
|
30
49
|
|
|
@@ -34,90 +53,360 @@ async function listShares(gid) {
|
|
|
34
53
|
for (const msg of data.records?.msg_list || []) {
|
|
35
54
|
for (const f of msg.file_list || []) {
|
|
36
55
|
shares.push({
|
|
37
|
-
msgId: msg.msg_id,
|
|
38
|
-
fromUk: msg.uk,
|
|
56
|
+
msgId: toStr(msg.msg_id),
|
|
57
|
+
fromUk: toStr(msg.uk),
|
|
39
58
|
fromUser: msg.uname,
|
|
40
59
|
name: decodeURIComponent(f.path || f.server_filename || ""),
|
|
41
|
-
fsId: f.fs_id,
|
|
60
|
+
fsId: toStr(f.fs_id),
|
|
42
61
|
isDir: f.isdir === "1",
|
|
43
|
-
size: parseInt(f.size || "0"),
|
|
62
|
+
size: parseInt(f.size || "0", 10),
|
|
44
63
|
});
|
|
45
64
|
}
|
|
46
65
|
}
|
|
47
66
|
return shares;
|
|
48
67
|
}
|
|
49
68
|
|
|
50
|
-
|
|
51
|
-
const page = options.page || 1;
|
|
52
|
-
const num = options.num || 100;
|
|
53
|
-
|
|
54
|
-
// First get the fromUk and msgId for this fsId
|
|
55
|
-
let { fromUk, msgId } = options;
|
|
56
|
-
if (!fromUk || !msgId) {
|
|
57
|
-
const shares = await listShares(gid);
|
|
58
|
-
const match = shares.find((s) => s.fsId === fsId);
|
|
59
|
-
if (match) {
|
|
60
|
-
fromUk = match.fromUk;
|
|
61
|
-
msgId = match.msgId;
|
|
62
|
-
} else if (shares.length > 0) {
|
|
63
|
-
fromUk = shares[0].fromUk;
|
|
64
|
-
msgId = shares[0].msgId;
|
|
65
|
-
} else {
|
|
66
|
-
throw new Error("Cannot find share info for this fsId");
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
+
// ── 底层 shareinfo 请求 ───────────────────────────────
|
|
69
70
|
|
|
71
|
+
async function shareInfoRequest(gidS, fsIdS, fromUkS, msgIdS, page, pageSize) {
|
|
70
72
|
const bdstoken = await getBdstoken();
|
|
71
|
-
const
|
|
72
|
-
`${http.PAN_BASE}/mbox/msg/shareinfo?type=2&from_uk=${
|
|
73
|
-
{
|
|
74
|
-
|
|
73
|
+
const url =
|
|
74
|
+
`${http.PAN_BASE}/mbox/msg/shareinfo?type=2&from_uk=${fromUkS}&msg_id=${msgIdS}` +
|
|
75
|
+
`&to_uk=0&num=${pageSize}&page=${page}&fs_id=${fsIdS}&gid=${gidS}` +
|
|
76
|
+
`&clienttype=0&app_id=${http.APP_ID}&web=1&bdstoken=${bdstoken}`;
|
|
75
77
|
|
|
76
|
-
|
|
78
|
+
const data = await http.webJson(url, { method: "POST", body: "" });
|
|
79
|
+
|
|
80
|
+
if (data.errno !== 0) {
|
|
81
|
+
throw new ShareApiError(data.errno, `API error: errno=${data.errno}`);
|
|
82
|
+
}
|
|
77
83
|
|
|
78
84
|
const files = (Array.isArray(data.records) ? data.records : []).map((f) => ({
|
|
79
85
|
name: f.server_filename || "",
|
|
80
86
|
path: decodeURIComponent(f.path || ""),
|
|
81
87
|
isDir: f.isdir === 1 || f.isdir === "1",
|
|
82
|
-
size: parseInt(f.size || "0"),
|
|
83
|
-
fsId: f.fs_id,
|
|
88
|
+
size: parseInt(f.size || "0", 10),
|
|
89
|
+
fsId: toStr(f.fs_id),
|
|
84
90
|
md5: f.md5 || "",
|
|
85
|
-
category: parseInt(f.category || "0"),
|
|
86
|
-
ctime: f.server_ctime || "",
|
|
87
|
-
mtime: f.server_mtime || "",
|
|
91
|
+
category: parseInt(f.category || "0", 10),
|
|
92
|
+
ctime: toStr(f.server_ctime || ""),
|
|
93
|
+
mtime: toStr(f.server_mtime || ""),
|
|
94
|
+
parentFsId: fsIdS,
|
|
95
|
+
fromUk: fromUkS,
|
|
96
|
+
msgId: msgIdS,
|
|
88
97
|
}));
|
|
89
98
|
|
|
90
|
-
return { files, hasMore: data.has_more === 1, page };
|
|
99
|
+
return { files, hasMore: data.has_more === 1, page, pageSize };
|
|
91
100
|
}
|
|
92
101
|
|
|
102
|
+
// ── 列出分享库内容 (gls) ──────────────────────────────
|
|
103
|
+
|
|
104
|
+
async function listFiles(gid, fsId, options = {}) {
|
|
105
|
+
const gidS = toStr(gid);
|
|
106
|
+
const fsIdS = toStr(fsId);
|
|
107
|
+
const parentFsId = toStr(options.parentFsId || "");
|
|
108
|
+
const page = options.page || 1;
|
|
109
|
+
let pageSize = options.pageSize || 50;
|
|
110
|
+
pageSize = Math.min(Math.max(1, pageSize), 100);
|
|
111
|
+
|
|
112
|
+
// 来源解析:显式传入优先;否则仅在顶层分享中精确匹配(禁止"用第一个分享"兜底)
|
|
113
|
+
let fromUkS = toStr(options.fromUk || "");
|
|
114
|
+
let msgIdS = toStr(options.msgId || "");
|
|
115
|
+
|
|
116
|
+
if (!fromUkS || !msgIdS) {
|
|
117
|
+
const shares = await listShares(gidS);
|
|
118
|
+
const match = shares.find((s) => s.fsId === fsIdS);
|
|
119
|
+
if (!match) {
|
|
120
|
+
throw new Error(
|
|
121
|
+
`Cannot determine fromUk/msgId for fsId ${fsIdS} (not a top-level share). ` +
|
|
122
|
+
`Pass --from-uk and --msg-id explicitly.`
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
fromUkS = match.fromUk;
|
|
126
|
+
msgIdS = match.msgId;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// errno=-3 时依次降 page-size: 50 → 20 → 10
|
|
130
|
+
const attempts = [...new Set([pageSize, 50, 20, 10].filter((n) => n <= pageSize))];
|
|
131
|
+
let lastErrno = null;
|
|
132
|
+
|
|
133
|
+
for (const size of attempts) {
|
|
134
|
+
try {
|
|
135
|
+
const result = await shareInfoRequest(gidS, fsIdS, fromUkS, msgIdS, page, size);
|
|
136
|
+
return {
|
|
137
|
+
files: result.files,
|
|
138
|
+
hasMore: result.hasMore,
|
|
139
|
+
page,
|
|
140
|
+
pageSize: size,
|
|
141
|
+
fromUk: fromUkS,
|
|
142
|
+
msgId: msgIdS,
|
|
143
|
+
fallback: null,
|
|
144
|
+
};
|
|
145
|
+
} catch (e) {
|
|
146
|
+
if (e instanceof ShareApiError && e.errno === -3) {
|
|
147
|
+
lastErrno = -3;
|
|
148
|
+
continue; // 尝试更小的 page-size
|
|
149
|
+
}
|
|
150
|
+
throw e;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 全部 page-size 都失败(errno=-3)→ 回退
|
|
155
|
+
if (parentFsId) {
|
|
156
|
+
// 回退到父目录,展示兄弟内容帮助 Agent 换路径
|
|
157
|
+
try {
|
|
158
|
+
const parent = await shareInfoRequest(gidS, toStr(parentFsId), fromUkS, msgIdS, 1, 50);
|
|
159
|
+
return {
|
|
160
|
+
files: parent.files,
|
|
161
|
+
hasMore: parent.hasMore,
|
|
162
|
+
page: 1,
|
|
163
|
+
pageSize: 50,
|
|
164
|
+
fromUk: fromUkS,
|
|
165
|
+
msgId: msgIdS,
|
|
166
|
+
fallback: {
|
|
167
|
+
reason: "errno=-3",
|
|
168
|
+
requestedFsId: fsIdS,
|
|
169
|
+
resolvedFsId: toStr(parentFsId),
|
|
170
|
+
level: "parent",
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
} catch (e) {
|
|
174
|
+
throw new ShareApiError(
|
|
175
|
+
-3,
|
|
176
|
+
`shareinfo rejected for fsId ${fsIdS} (errno=-3), and parent fallback also failed. ` +
|
|
177
|
+
`The share may be expired or the directory may be too large for the API.`
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// 目标本身是顶层分享 → 退回 gshares 结果
|
|
183
|
+
try {
|
|
184
|
+
const shares = await listShares(gidS);
|
|
185
|
+
const match = shares.find((s) => s.fsId === fsIdS);
|
|
186
|
+
if (match) {
|
|
187
|
+
return {
|
|
188
|
+
files: [{
|
|
189
|
+
name: match.name,
|
|
190
|
+
path: match.name,
|
|
191
|
+
isDir: match.isDir,
|
|
192
|
+
size: match.size,
|
|
193
|
+
fsId: match.fsId,
|
|
194
|
+
md5: "",
|
|
195
|
+
category: 0,
|
|
196
|
+
ctime: "",
|
|
197
|
+
mtime: "",
|
|
198
|
+
parentFsId: "",
|
|
199
|
+
fromUk: match.fromUk,
|
|
200
|
+
msgId: match.msgId,
|
|
201
|
+
}],
|
|
202
|
+
hasMore: false,
|
|
203
|
+
page: 1,
|
|
204
|
+
pageSize: 50,
|
|
205
|
+
fromUk: fromUkS,
|
|
206
|
+
msgId: msgIdS,
|
|
207
|
+
fallback: {
|
|
208
|
+
reason: "errno=-3",
|
|
209
|
+
requestedFsId: fsIdS,
|
|
210
|
+
resolvedFsId: fsIdS,
|
|
211
|
+
level: "group-shares",
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
} catch {}
|
|
216
|
+
|
|
217
|
+
throw new ShareApiError(
|
|
218
|
+
-3,
|
|
219
|
+
`shareinfo rejected for fsId ${fsIdS} (errno=-3). ` +
|
|
220
|
+
`群分享 API 拒绝请求:已观察到大目录、错误的 msgId/fromUk、过期分享都可能触发。` +
|
|
221
|
+
`请减小 --page-size,或携带 gsearch 返回的 fromUk/msgId/parentFsId 信息重试。`
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ── 目录树 (gtree) ────────────────────────────────────
|
|
226
|
+
|
|
227
|
+
async function treeFiles(gid, options = {}) {
|
|
228
|
+
const gidS = toStr(gid);
|
|
229
|
+
const maxDepth = Math.min(Math.max(1, options.depth || 2), 5);
|
|
230
|
+
const concurrency = Math.min(Math.max(1, options.concurrency || 4), 8);
|
|
231
|
+
const maxNodes = options.maxNodes || 2000;
|
|
232
|
+
|
|
233
|
+
const shares = await listShares(gidS);
|
|
234
|
+
const tree = [];
|
|
235
|
+
const failed = [];
|
|
236
|
+
|
|
237
|
+
// BFS: queue 元素 { node, depth, share }
|
|
238
|
+
const queue = shares.map((s) => ({ node: { ...s, parentFsId: "" }, depth: 1, share: s }));
|
|
239
|
+
let scanned = 0;
|
|
240
|
+
|
|
241
|
+
// 逐层 BFS,层内并发
|
|
242
|
+
for (let depth = 1; depth <= maxDepth; depth++) {
|
|
243
|
+
const level = queue.filter((q) => q.depth === depth);
|
|
244
|
+
if (level.length === 0) break;
|
|
245
|
+
|
|
246
|
+
const results = await Promise.all(
|
|
247
|
+
level.map(async (entry) => {
|
|
248
|
+
const { node, share } = entry;
|
|
249
|
+
if (!node.isDir || node.size > 0) return null; // 非目录不进队列
|
|
250
|
+
scanned++;
|
|
251
|
+
try {
|
|
252
|
+
const { files } = await shareInfoRequest(gidS, node.fsId, share.fromUk, share.msgId, 1, 100);
|
|
253
|
+
return files.map((f) => ({
|
|
254
|
+
node: {
|
|
255
|
+
name: f.name,
|
|
256
|
+
path: f.path,
|
|
257
|
+
isDir: f.isDir,
|
|
258
|
+
size: f.size,
|
|
259
|
+
fsId: f.fsId,
|
|
260
|
+
md5: f.md5,
|
|
261
|
+
category: f.category,
|
|
262
|
+
parentFsId: f.parentFsId,
|
|
263
|
+
fromUk: f.fromUk,
|
|
264
|
+
msgId: f.msgId,
|
|
265
|
+
fromUser: share.fromUser,
|
|
266
|
+
},
|
|
267
|
+
depth: depth + 1,
|
|
268
|
+
share,
|
|
269
|
+
}));
|
|
270
|
+
} catch {
|
|
271
|
+
failed.push({ fsId: node.fsId, name: node.name });
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
})
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
for (const list of results) {
|
|
278
|
+
if (!list) continue;
|
|
279
|
+
for (const item of list) {
|
|
280
|
+
tree.push(item.node);
|
|
281
|
+
if (tree.length >= maxNodes) break;
|
|
282
|
+
if (item.depth <= maxDepth && item.node.isDir) queue.push({ node: item.node, depth: item.depth, share: item.share });
|
|
283
|
+
}
|
|
284
|
+
if (tree.length >= maxNodes) break;
|
|
285
|
+
}
|
|
286
|
+
if (tree.length >= maxNodes) break;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return { tree, failed, scanned, maxDepth, truncated: tree.length >= maxNodes };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ── 群文件搜索 (gsearch) ──────────────────────────────
|
|
293
|
+
|
|
93
294
|
async function searchFiles(gid, keyword, options = {}) {
|
|
94
|
-
const
|
|
95
|
-
const results = [];
|
|
295
|
+
const gidS = toStr(gid);
|
|
96
296
|
const kw = keyword.toLowerCase();
|
|
97
297
|
|
|
98
|
-
const
|
|
298
|
+
const limit = options.limit || 50;
|
|
299
|
+
const page = options.page || 1;
|
|
300
|
+
const concurrency = Math.min(Math.max(1, options.concurrency || 4), 8);
|
|
301
|
+
const unique = options.unique !== false;
|
|
302
|
+
const fetchAll = options.all === true;
|
|
303
|
+
const depth = Math.min(Math.max(1, options.depth || 1), 5); // 默认 1 层(兼容旧行为)
|
|
304
|
+
const startIdx = (page - 1) * limit;
|
|
305
|
+
const stopAt = fetchAll ? Infinity : startIdx + limit + 1; // +1 判断 hasMore
|
|
306
|
+
|
|
307
|
+
const shares = await listShares(gidS);
|
|
308
|
+
const totalShares = shares.length;
|
|
309
|
+
|
|
310
|
+
const seen = new Set();
|
|
311
|
+
const results = [];
|
|
312
|
+
let scannedShares = 0;
|
|
313
|
+
let failedShares = 0;
|
|
314
|
+
let finished = false;
|
|
315
|
+
|
|
316
|
+
const pushIfMatch = (item) => {
|
|
317
|
+
if (finished) return;
|
|
318
|
+
const key = unique ? toStr(item.fsId) : `${toStr(item.fsId)}|${toStr(item.msgId)}`;
|
|
319
|
+
if (unique && seen.has(key)) return;
|
|
320
|
+
seen.add(key);
|
|
321
|
+
results.push(item);
|
|
322
|
+
if (results.length >= stopAt) finished = true;
|
|
323
|
+
};
|
|
99
324
|
|
|
325
|
+
// 阶段 1:顶层分享名称匹配(非目录分享在此完成扫描)
|
|
100
326
|
for (const share of shares) {
|
|
327
|
+
if (finished) break;
|
|
328
|
+
if (!share.isDir) scannedShares++;
|
|
101
329
|
if (share.name.toLowerCase().includes(kw)) {
|
|
102
|
-
|
|
330
|
+
pushIfMatch({
|
|
331
|
+
name: share.name,
|
|
332
|
+
path: share.name,
|
|
333
|
+
isDir: share.isDir,
|
|
334
|
+
size: share.size,
|
|
335
|
+
fsId: share.fsId,
|
|
336
|
+
md5: "",
|
|
337
|
+
category: 0,
|
|
338
|
+
ctime: "",
|
|
339
|
+
mtime: "",
|
|
340
|
+
parentFsId: "",
|
|
341
|
+
fromUk: share.fromUk,
|
|
342
|
+
msgId: share.msgId,
|
|
343
|
+
fromUser: share.fromUser,
|
|
344
|
+
group: gidS,
|
|
345
|
+
});
|
|
103
346
|
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// 阶段 2:BFS 递归扫描目录(每层 4 个一批并发,默认 depth=1 保持旧行为)
|
|
350
|
+
const dirShares = shares.filter((s) => s.isDir);
|
|
351
|
+
let queue = dirShares.map((s) => ({ fsId: s.fsId, fromUk: s.fromUk, msgId: s.msgId, depth: 1 }));
|
|
352
|
+
|
|
353
|
+
for (let level = 1; level <= depth && queue.length > 0 && !finished; level++) {
|
|
354
|
+
const current = queue;
|
|
355
|
+
queue = [];
|
|
104
356
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
357
|
+
for (let i = 0; i < current.length && !finished; i += concurrency) {
|
|
358
|
+
const batch = current.slice(i, i + concurrency);
|
|
359
|
+
const batchResults = await Promise.all(
|
|
360
|
+
batch.map(async (share) => {
|
|
361
|
+
scannedShares++;
|
|
362
|
+
try {
|
|
363
|
+
const { files } = await shareInfoRequest(gidS, share.fsId, share.fromUk, share.msgId, 1, 100);
|
|
364
|
+
const matches = files.filter((f) => f.name.toLowerCase().includes(kw));
|
|
365
|
+
const subDirs = files.filter((f) => f.isDir);
|
|
366
|
+
return { matches, subDirs };
|
|
367
|
+
} catch {
|
|
368
|
+
failedShares++;
|
|
369
|
+
return { matches: [], subDirs: [] };
|
|
370
|
+
}
|
|
371
|
+
})
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
for (const { matches, subDirs } of batchResults) {
|
|
375
|
+
if (finished) break;
|
|
376
|
+
for (const f of matches) {
|
|
377
|
+
pushIfMatch({ ...f, group: gidS });
|
|
378
|
+
if (finished) break;
|
|
379
|
+
}
|
|
380
|
+
// 收集下一层待扫描目录
|
|
381
|
+
if (level < depth && !finished) {
|
|
382
|
+
for (const d of subDirs) {
|
|
383
|
+
queue.push({ fsId: d.fsId, fromUk: d.fromUk, msgId: d.msgId, depth: level + 1 });
|
|
114
384
|
}
|
|
115
385
|
}
|
|
116
|
-
}
|
|
386
|
+
}
|
|
117
387
|
}
|
|
118
388
|
}
|
|
119
389
|
|
|
120
|
-
|
|
390
|
+
const complete = scannedShares >= totalShares;
|
|
391
|
+
const hasMore = !fetchAll && results.length > startIdx + limit;
|
|
392
|
+
const pageSlice = fetchAll ? results : results.slice(startIdx, startIdx + limit);
|
|
393
|
+
|
|
394
|
+
return {
|
|
395
|
+
results: pageSlice,
|
|
396
|
+
page,
|
|
397
|
+
pageSize: limit,
|
|
398
|
+
returned: pageSlice.length,
|
|
399
|
+
total: null,
|
|
400
|
+
hasMore,
|
|
401
|
+
nextPage: hasMore ? page + 1 : null,
|
|
402
|
+
unique,
|
|
403
|
+
complete,
|
|
404
|
+
partial: failedShares > 0,
|
|
405
|
+
scannedShares,
|
|
406
|
+
totalShares,
|
|
407
|
+
failedShares,
|
|
408
|
+
depth,
|
|
409
|
+
};
|
|
121
410
|
}
|
|
122
411
|
|
|
123
|
-
module.exports = { listGroups, listShares, listFiles, searchFiles };
|
|
412
|
+
module.exports = { listGroups, listShares, listFiles, treeFiles, searchFiles, ShareApiError };
|
package/lib/http.js
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
const { spawnSync } = require("child_process");
|
|
2
2
|
const config = require("./config");
|
|
3
3
|
|
|
4
|
+
function buildCookieHeader(cfg) {
|
|
5
|
+
if (cfg.cookie) return cfg.cookie;
|
|
6
|
+
return `BDUSS=${cfg.bduss}; STOKEN=${cfg.stoken}`;
|
|
7
|
+
}
|
|
8
|
+
|
|
4
9
|
function curlGet(url, extraHeaders = {}) {
|
|
5
10
|
const cfg = config.get();
|
|
6
11
|
const headers = {
|
|
7
|
-
Cookie:
|
|
12
|
+
Cookie: buildCookieHeader(cfg),
|
|
8
13
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
|
9
14
|
Referer: "https://pan.baidu.com/disk/main",
|
|
10
15
|
...extraHeaders,
|
|
@@ -23,7 +28,7 @@ function curlGet(url, extraHeaders = {}) {
|
|
|
23
28
|
function curlPost(url, body = "", extraHeaders = {}) {
|
|
24
29
|
const cfg = config.get();
|
|
25
30
|
const headers = {
|
|
26
|
-
Cookie:
|
|
31
|
+
Cookie: buildCookieHeader(cfg),
|
|
27
32
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
|
28
33
|
Referer: "https://pan.baidu.com/disk/main",
|
|
29
34
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
@@ -67,18 +72,45 @@ function curlJson(url, options = {}) {
|
|
|
67
72
|
}
|
|
68
73
|
}
|
|
69
74
|
|
|
75
|
+
async function webJson(url, options = {}) {
|
|
76
|
+
const cfg = config.get();
|
|
77
|
+
if (cfg.webTransport !== "browser") return curlJson(url, options);
|
|
78
|
+
|
|
79
|
+
const { browserJson } = require("./browser-login");
|
|
80
|
+
const headers = { ...(options.headers || {}) };
|
|
81
|
+
if (options.method === "POST" && !headers["Content-Type"]) {
|
|
82
|
+
headers["Content-Type"] = "application/x-www-form-urlencoded";
|
|
83
|
+
}
|
|
84
|
+
return browserJson(
|
|
85
|
+
url,
|
|
86
|
+
{
|
|
87
|
+
method: options.method || "GET",
|
|
88
|
+
...(Object.prototype.hasOwnProperty.call(options, "body") ? { body: options.body } : {}),
|
|
89
|
+
headers,
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
profileDir: cfg.browserProfile,
|
|
93
|
+
port: cfg.browserPort,
|
|
94
|
+
onPort: (browserPort) => {
|
|
95
|
+
if (browserPort !== cfg.browserPort) config.update({ browserPort });
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
70
101
|
async function getBdstoken() {
|
|
71
102
|
const fields = encodeURIComponent('["bdstoken","token","uk","isdocuser","servertime"]');
|
|
72
|
-
const data =
|
|
103
|
+
const data = await webJson(
|
|
73
104
|
`https://pan.baidu.com/api/gettemplatevariable?clienttype=0&app_id=250528&web=1&fields=${fields}`
|
|
74
105
|
);
|
|
75
106
|
const token = data?.result?.bdstoken || null;
|
|
76
107
|
if (!token) {
|
|
77
108
|
const cfg = require("./config").get();
|
|
78
109
|
throw new Error(
|
|
79
|
-
`Cannot get bdstoken.
|
|
110
|
+
`Cannot get bdstoken. The web session is not authenticated.\n` +
|
|
80
111
|
` BDUSS: ${cfg.bduss ? "set" : "MISSING"} STOKEN: ${cfg.stoken ? "set" : "MISSING"}\n` +
|
|
81
|
-
`
|
|
112
|
+
` Web API transport: ${cfg.webTransport}\n` +
|
|
113
|
+
` Run: bdp login, or bdp login --bduss <value> --stoken <value>`
|
|
82
114
|
);
|
|
83
115
|
}
|
|
84
116
|
return token;
|
|
@@ -87,4 +119,4 @@ async function getBdstoken() {
|
|
|
87
119
|
const APP_ID = "250528";
|
|
88
120
|
const PAN_BASE = "https://pan.baidu.com";
|
|
89
121
|
|
|
90
|
-
module.exports = { curlGet, curlPost, curlRange, curlJson, getBdstoken, APP_ID, PAN_BASE };
|
|
122
|
+
module.exports = { curlGet, curlPost, curlRange, curlJson, webJson, getBdstoken, APP_ID, PAN_BASE };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "baiduwangpan-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "百度网盘 CLI — 全盘读写 · 免下载读取文件内容 · 群聊文件浏览 (Agent 友好)",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -8,11 +8,15 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node bin/bdp.js",
|
|
11
|
+
"test": "node --test",
|
|
11
12
|
"postinstall": "node scripts/postinstall.js"
|
|
12
13
|
},
|
|
13
14
|
"engines": {
|
|
14
15
|
"node": ">=14"
|
|
15
16
|
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"ws": "^8.18.0"
|
|
19
|
+
},
|
|
16
20
|
"license": "MIT",
|
|
17
21
|
"keywords": [
|
|
18
22
|
"baidu",
|
|
@@ -34,7 +38,7 @@
|
|
|
34
38
|
],
|
|
35
39
|
"repository": {
|
|
36
40
|
"type": "git",
|
|
37
|
-
"url": "https://github.com/NkAntony777/baiduwangpan-cli.git"
|
|
41
|
+
"url": "git+https://github.com/NkAntony777/baiduwangpan-cli.git"
|
|
38
42
|
},
|
|
39
43
|
"homepage": "https://github.com/NkAntony777/baiduwangpan-cli",
|
|
40
44
|
"bugs": {
|