baiduwangpan-cli 1.0.0 → 1.0.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 +66 -3
- package/bin/bdp.js +81 -10
- package/lib/browser-login.js +426 -0
- package/lib/config.js +18 -2
- package/lib/group.js +3 -3
- package/lib/http.js +38 -6
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -65,13 +65,25 @@ npm link # 全局注册 bdp 命令
|
|
|
65
65
|
|
|
66
66
|
### 2. 配置认证
|
|
67
67
|
|
|
68
|
+
**方式一:浏览器自动登录(推荐)**
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
bdp login
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
自动启动 Chrome/Edge → 打开百度网盘登录页 → **手机扫码** → 在页面会话内验证登录 → 自动同步引擎。无需 F12。
|
|
75
|
+
|
|
76
|
+
扫码产生的新会话可能被百度限制为只能在原浏览器上下文中使用。因此 `bdp` 会保留专用 profile,并在登录后将浏览器最小化;PCS API 继续使用 BDUSS/STOKEN,网页和群聊 API 通过该浏览器的 CDP 会话调用。浏览器被关闭后,下次网页 API 调用会使用同一 profile 自动重启。
|
|
77
|
+
|
|
78
|
+
**方式二:手动凭证**
|
|
79
|
+
|
|
68
80
|
```bash
|
|
69
81
|
bdp login --bduss <你的BDUSS> --stoken <你的STOKEN>
|
|
70
82
|
bdp whoami # 验证
|
|
71
83
|
```
|
|
72
84
|
|
|
73
85
|
<details>
|
|
74
|
-
<summary>📋 如何获取 BDUSS 和 STOKEN
|
|
86
|
+
<summary>📋 如何获取 BDUSS 和 STOKEN(手动方式)</summary>
|
|
75
87
|
|
|
76
88
|
1. 浏览器打开 [pan.baidu.com](https://pan.baidu.com) 并登录
|
|
77
89
|
2. 按 `F12` → **Application** → **Cookies** → `https://pan.baidu.com`
|
|
@@ -79,6 +91,8 @@ bdp whoami # 验证
|
|
|
79
91
|
|
|
80
92
|
> ⚠️ STOKEN 必须在百度网盘页面获取(不是百度首页),值中应包含大写字母。
|
|
81
93
|
|
|
94
|
+
> 💡 推荐直接运行 `bdp login`,全程自动化,无需手动操作。
|
|
95
|
+
|
|
82
96
|
</details>
|
|
83
97
|
|
|
84
98
|
### 3. 开始使用
|
|
@@ -183,7 +197,7 @@ const results = await group.searchFiles('539478953581833690', '倪海厦');
|
|
|
183
197
|
│ lib/pan.js │ lib/group.js │
|
|
184
198
|
│ 全盘文件操作 │ 群聊文件浏览 │
|
|
185
199
|
├──────────────────┼──────────────────────────────────┤
|
|
186
|
-
│ BaiduPCS-Go │
|
|
200
|
+
│ BaiduPCS-Go │ lib/http.js (curl / browser) │
|
|
187
201
|
│ (BDUSS 认证) │ mbox API (逆向) │
|
|
188
202
|
│ locate → dlink │ /mbox/group/listshare │
|
|
189
203
|
│ upload/download │ /mbox/msg/shareinfo │
|
|
@@ -237,14 +251,63 @@ baiduwangpan-cli/
|
|
|
237
251
|
├── lib/
|
|
238
252
|
│ ├── index.js # 库导出
|
|
239
253
|
│ ├── config.js # 配置管理 (~/.bdp/config.json)
|
|
240
|
-
│ ├── http.js #
|
|
254
|
+
│ ├── http.js # 网页 API 双传输 (curl / browser)
|
|
255
|
+
│ ├── browser-login.js # 浏览器登录、持久 profile 与 CDP 请求
|
|
241
256
|
│ ├── pan.js # 网盘操作 (BaiduPCS-Go 桥接)
|
|
242
257
|
│ └── group.js # 群聊操作 (逆向 mbox API)
|
|
258
|
+
├── test/ # Node.js 回归测试
|
|
259
|
+
├── skills/
|
|
260
|
+
│ └── baiduwangpan/ # Agent Skill 源文件 (SKILL.md + reference + scripts)
|
|
261
|
+
├── baiduwangpan-skill.zip # Agent Skill 打包 (Releases 附件)
|
|
243
262
|
├── bdp.js # 早期独立版 (保留参考)
|
|
244
263
|
├── bdp-group.js # 早期独立版 (保留参考)
|
|
245
264
|
└── bdp.py # Python 版 (备用)
|
|
246
265
|
```
|
|
247
266
|
|
|
267
|
+
## 🤖 Agent Skill 使用
|
|
268
|
+
|
|
269
|
+
本仓库附带一个标准格式的 Agent Skill,让 Claude Code / Codex / OpenClaw / Cursor 等 Agent 通过自然语言操作百度网盘。
|
|
270
|
+
|
|
271
|
+
### 安装 Skill
|
|
272
|
+
|
|
273
|
+
从 [Releases](https://github.com/NkAntony777/baiduwangpan-cli/releases) 下载 `baiduwangpan-skill.zip`,或直接复制仓库 `skills/baiduwangpan/` 文件夹:
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
# 各平台 skills 目录
|
|
277
|
+
~/.claude/skills/ # Claude Code
|
|
278
|
+
~/.codex/skills/ # Codex CLI
|
|
279
|
+
~/.config/opencode/skills/ # OpenCode
|
|
280
|
+
# 或对应 Agent 的 skills 目录
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Skill 使用
|
|
284
|
+
|
|
285
|
+
Agent 加载 skill 后,自然语言即可驱动(skill 会自动触发):
|
|
286
|
+
|
|
287
|
+
```
|
|
288
|
+
用户: 看看我网盘里有什么
|
|
289
|
+
用户: 在群里找倪海厦的资料
|
|
290
|
+
用户: 读一下 /文档/data.csv 前几行
|
|
291
|
+
用户: 搜索网盘里所有报告文件
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
skill 会自动执行 `bdp` 命令并返回结构化结果(含 `--json` 输出)。
|
|
295
|
+
|
|
296
|
+
### Skill 内容
|
|
297
|
+
|
|
298
|
+
```
|
|
299
|
+
skills/baiduwangpan/
|
|
300
|
+
├── SKILL.md # 技能定义(触发规则 + 命令规范 + 安全边界)
|
|
301
|
+
├── reference/
|
|
302
|
+
│ ├── commands.md # 完整命令参考
|
|
303
|
+
│ ├── authentication.md # 凭证获取与配置
|
|
304
|
+
│ ├── examples.md # Agent 使用示例 + JSON 输出格式
|
|
305
|
+
│ └── troubleshooting.md # 故障排查
|
|
306
|
+
└── scripts/
|
|
307
|
+
├── setup.sh # macOS/Linux 自动安装
|
|
308
|
+
└── setup.ps1 # Windows 自动安装
|
|
309
|
+
```
|
|
310
|
+
|
|
248
311
|
## ⚠️ 注意事项
|
|
249
312
|
|
|
250
313
|
- **BDUSS 有效期**:Cookie 会过期,失效后重新执行 `bdp login`
|
package/bin/bdp.js
CHANGED
|
@@ -90,14 +90,77 @@ function parseArgs(argv) {
|
|
|
90
90
|
const cmds = {};
|
|
91
91
|
|
|
92
92
|
cmds.login = async (args, json) => {
|
|
93
|
-
|
|
94
|
-
if (args.flags.
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
93
|
+
// 方式 1: 手动传入凭证
|
|
94
|
+
if (args.flags.bduss || args.flags.stoken) {
|
|
95
|
+
if (!args.flags.bduss || !args.flags.stoken) {
|
|
96
|
+
throw new Error("Manual login requires both --bduss and --stoken");
|
|
97
|
+
}
|
|
98
|
+
config.setAuth({
|
|
99
|
+
bduss: args.flags.bduss,
|
|
100
|
+
stoken: args.flags.stoken,
|
|
101
|
+
webTransport: "curl",
|
|
102
|
+
});
|
|
103
|
+
const cfg = config.get();
|
|
104
|
+
output(
|
|
105
|
+
{ success: true, bduss: cfg.bduss ? "***set***" : "missing", stoken: cfg.stoken ? "***set***" : "missing" },
|
|
106
|
+
json,
|
|
107
|
+
() => console.log("Credentials saved to " + config.CONFIG_FILE)
|
|
108
|
+
);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 方式 2: 浏览器自动登录(扫码,Agent 代劳)
|
|
113
|
+
const { browserLogin } = require("../lib/browser-login");
|
|
114
|
+
const status = (msg) => {
|
|
115
|
+
if (!json) console.log(msg);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
status("🔐 bdp login: 未提供 --bduss/--stoken,启动浏览器扫码登录...");
|
|
119
|
+
try {
|
|
120
|
+
const previous = config.get();
|
|
121
|
+
const creds = await browserLogin({
|
|
122
|
+
onStatus: status,
|
|
123
|
+
profileDir: previous.browserProfile || undefined,
|
|
124
|
+
port: previous.webTransport === "browser" ? previous.browserPort : undefined,
|
|
125
|
+
});
|
|
126
|
+
config.setAuth({
|
|
127
|
+
bduss: creds.bduss,
|
|
128
|
+
stoken: creds.stoken,
|
|
129
|
+
webTransport: "browser",
|
|
130
|
+
browserProfile: creds.browserProfile,
|
|
131
|
+
browserPort: creds.browserPort,
|
|
132
|
+
});
|
|
133
|
+
status("✅ 登录成功,凭证已保存到 " + config.CONFIG_FILE);
|
|
134
|
+
|
|
135
|
+
// 同步登录 BaiduPCS-Go 引擎(全盘操作依赖)
|
|
136
|
+
try {
|
|
137
|
+
const { spawnSync } = require("child_process");
|
|
138
|
+
const cfg = config.get();
|
|
139
|
+
const loginResult = spawnSync(cfg.pcsPath, ["login", `-bduss=${creds.bduss}`, `-stoken=${creds.stoken}`], {
|
|
140
|
+
encoding: "utf-8",
|
|
141
|
+
timeout: 30000,
|
|
142
|
+
});
|
|
143
|
+
if (loginResult.stdout && loginResult.stdout.includes("登录成功")) {
|
|
144
|
+
status("✅ BaiduPCS-Go 引擎已同步登录");
|
|
145
|
+
} else {
|
|
146
|
+
status("⚠️ BaiduPCS-Go 引擎登录失败: " + (loginResult.stdout || loginResult.stderr || "未知错误").trim().substring(0, 100));
|
|
147
|
+
}
|
|
148
|
+
} catch (e) {
|
|
149
|
+
status("⚠️ BaiduPCS-Go 引擎登录异常: " + e.message);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (json) {
|
|
153
|
+
console.log(JSON.stringify({ success: true, method: "browser-qr", bduss: "***set***", stoken: "***set***" }));
|
|
154
|
+
}
|
|
155
|
+
} catch (e) {
|
|
156
|
+
if (json) {
|
|
157
|
+
console.log(JSON.stringify({ error: e.message }));
|
|
158
|
+
} else {
|
|
159
|
+
console.error(`[ERROR] ${e.message}`);
|
|
160
|
+
console.error(" 提示: 也可手动方式 bdp login --bduss <值> --stoken <值>");
|
|
161
|
+
}
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
101
164
|
};
|
|
102
165
|
|
|
103
166
|
cmds.whoami = async (args, json) => {
|
|
@@ -107,6 +170,7 @@ cmds.whoami = async (args, json) => {
|
|
|
107
170
|
loggedIn: !!(cfg.bduss && cfg.stoken),
|
|
108
171
|
bduss: cfg.bduss ? "***set***" : "missing",
|
|
109
172
|
stoken: cfg.stoken ? "***set***" : "missing",
|
|
173
|
+
webTransport: cfg.webTransport,
|
|
110
174
|
pcsPath: cfg.pcsPath,
|
|
111
175
|
configFile: config.CONFIG_FILE,
|
|
112
176
|
},
|
|
@@ -115,6 +179,7 @@ cmds.whoami = async (args, json) => {
|
|
|
115
179
|
console.log(`Status: ${d.loggedIn ? "✅ Logged in" : "❌ Not logged in"}`);
|
|
116
180
|
console.log(`BDUSS: ${d.bduss}`);
|
|
117
181
|
console.log(`STOKEN: ${d.stoken}`);
|
|
182
|
+
console.log(`Web API: ${d.webTransport}`);
|
|
118
183
|
console.log(`PCS Path: ${d.pcsPath}`);
|
|
119
184
|
console.log(`Config: ${d.configFile}`);
|
|
120
185
|
}
|
|
@@ -127,6 +192,7 @@ cmds.config = async (args, json) => {
|
|
|
127
192
|
console.log("Configuration:");
|
|
128
193
|
console.log(` bduss: ${d.bduss ? d.bduss.substring(0, 10) + "..." : "(not set)"}`);
|
|
129
194
|
console.log(` stoken: ${d.stoken ? d.stoken.substring(0, 10) + "..." : "(not set)"}`);
|
|
195
|
+
console.log(` web API: ${d.webTransport}`);
|
|
130
196
|
console.log(` pcsPath: ${d.pcsPath}`);
|
|
131
197
|
console.log(` ua: ${d.ua.substring(0, 40)}...`);
|
|
132
198
|
console.log(` maxBytes: ${d.maxBytes}`);
|
|
@@ -336,7 +402,8 @@ GROUP CHAT OPERATIONS (群聊)
|
|
|
336
402
|
gsearch <gid> <keyword> Search group file names
|
|
337
403
|
|
|
338
404
|
CONFIGURATION
|
|
339
|
-
login
|
|
405
|
+
login Browser QR login (auto-detect cookies)
|
|
406
|
+
login --bduss X --stoken Y Save credentials manually
|
|
340
407
|
whoami Check login status
|
|
341
408
|
config Show configuration
|
|
342
409
|
|
|
@@ -380,7 +447,11 @@ async function main() {
|
|
|
380
447
|
} else {
|
|
381
448
|
console.error(`[ERROR] ${e.message}`);
|
|
382
449
|
}
|
|
383
|
-
process.
|
|
450
|
+
process.exitCode = 1;
|
|
451
|
+
} finally {
|
|
452
|
+
try {
|
|
453
|
+
require("../lib/browser-login").disconnectBrowserSession();
|
|
454
|
+
} catch {}
|
|
384
455
|
}
|
|
385
456
|
}
|
|
386
457
|
|
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
const { spawn, execSync } = require("child_process");
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const os = require("os");
|
|
5
|
+
const http = require("http");
|
|
6
|
+
|
|
7
|
+
const PAN_HOME = "https://pan.baidu.com/disk/main";
|
|
8
|
+
const DEFAULT_PROFILE_DIR = path.join(os.homedir(), ".bdp", "browser-profile");
|
|
9
|
+
const APP_ID = "250528";
|
|
10
|
+
|
|
11
|
+
let activeSession = null;
|
|
12
|
+
let disconnectTimer = null;
|
|
13
|
+
|
|
14
|
+
function findBrowser() {
|
|
15
|
+
const candidates = [];
|
|
16
|
+
|
|
17
|
+
if (os.platform() === "win32") {
|
|
18
|
+
const programFiles = [
|
|
19
|
+
process.env.PROGRAMFILES || "C:\\Program Files",
|
|
20
|
+
process.env["PROGRAMFILES(X86)"] || "C:\\Program Files (x86)",
|
|
21
|
+
process.env.LOCALAPPDATA || "",
|
|
22
|
+
];
|
|
23
|
+
for (const pf of programFiles) {
|
|
24
|
+
candidates.push(path.join(pf, "Google\\Chrome\\Application\\chrome.exe"));
|
|
25
|
+
candidates.push(path.join(pf, "Microsoft\\Edge\\Application\\msedge.exe"));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (os.platform() === "darwin") {
|
|
30
|
+
candidates.push("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome");
|
|
31
|
+
candidates.push("/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
for (const name of ["google-chrome", "chromium", "chromium-browser", "microsoft-edge", "chrome"]) {
|
|
35
|
+
try {
|
|
36
|
+
const out = execSync(`command -v ${name}`, { stdio: "pipe" }).toString().trim();
|
|
37
|
+
if (out) candidates.push(out);
|
|
38
|
+
} catch {}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
for (const candidate of candidates) {
|
|
42
|
+
if (candidate && fs.existsSync(candidate)) return candidate;
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function httpRequest(url, method = "GET") {
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
const req = http.request(url, { method }, (res) => {
|
|
50
|
+
let data = "";
|
|
51
|
+
res.on("data", (chunk) => (data += chunk));
|
|
52
|
+
res.on("end", () => resolve(data));
|
|
53
|
+
});
|
|
54
|
+
req.setTimeout(2000, () => req.destroy(new Error("DevTools request timed out")));
|
|
55
|
+
req.on("error", reject);
|
|
56
|
+
req.end();
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function getDevToolsVersion(port) {
|
|
61
|
+
const raw = await httpRequest(`http://127.0.0.1:${port}/json/version`);
|
|
62
|
+
const version = JSON.parse(raw);
|
|
63
|
+
if (!version.webSocketDebuggerUrl || !/Chrome|Chromium|Edg/i.test(version.Browser || "")) {
|
|
64
|
+
throw new Error("The configured DevTools port is not a Chromium browser");
|
|
65
|
+
}
|
|
66
|
+
return version;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function waitForDevTools(port, timeoutMs) {
|
|
70
|
+
const deadline = Date.now() + timeoutMs;
|
|
71
|
+
while (Date.now() < deadline) {
|
|
72
|
+
try {
|
|
73
|
+
return await getDevToolsVersion(port);
|
|
74
|
+
} catch {}
|
|
75
|
+
await delay(300);
|
|
76
|
+
}
|
|
77
|
+
throw new Error("DevTools 启动超时");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function listTabs(port) {
|
|
81
|
+
return JSON.parse(await httpRequest(`http://127.0.0.1:${port}/json/list`));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function openTab(port, url) {
|
|
85
|
+
const encoded = encodeURIComponent(url);
|
|
86
|
+
return JSON.parse(await httpRequest(`http://127.0.0.1:${port}/json/new?${encoded}`, "PUT"));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function getWebSocketImpl() {
|
|
90
|
+
if (typeof WebSocket !== "undefined") return WebSocket;
|
|
91
|
+
try {
|
|
92
|
+
return require("ws");
|
|
93
|
+
} catch {
|
|
94
|
+
throw new Error("Browser login requires Node.js 22+ or the optional 'ws' package");
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
class CDPClient {
|
|
99
|
+
constructor(wsUrl) {
|
|
100
|
+
const WebSocketImpl = getWebSocketImpl();
|
|
101
|
+
this.ws = new WebSocketImpl(wsUrl);
|
|
102
|
+
this.id = 0;
|
|
103
|
+
this.pending = new Map();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
connect() {
|
|
107
|
+
return new Promise((resolve, reject) => {
|
|
108
|
+
this.ws.onopen = resolve;
|
|
109
|
+
this.ws.onerror = () => reject(new Error("WebSocket 连接失败"));
|
|
110
|
+
this.ws.onclose = () => {
|
|
111
|
+
for (const pending of this.pending.values()) {
|
|
112
|
+
pending.reject(new Error("DevTools connection closed"));
|
|
113
|
+
}
|
|
114
|
+
this.pending.clear();
|
|
115
|
+
};
|
|
116
|
+
this.ws.onmessage = (event) => {
|
|
117
|
+
const raw = typeof event.data === "string" ? event.data : event.data.toString();
|
|
118
|
+
const msg = JSON.parse(raw);
|
|
119
|
+
if (msg.id && this.pending.has(msg.id)) {
|
|
120
|
+
const pending = this.pending.get(msg.id);
|
|
121
|
+
this.pending.delete(msg.id);
|
|
122
|
+
if (msg.error) pending.reject(new Error(msg.error.message));
|
|
123
|
+
else pending.resolve(msg.result);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
send(method, params = {}) {
|
|
130
|
+
return new Promise((resolve, reject) => {
|
|
131
|
+
const id = ++this.id;
|
|
132
|
+
this.pending.set(id, { resolve, reject });
|
|
133
|
+
this.ws.send(JSON.stringify({ id, method, params }));
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
close() {
|
|
138
|
+
try { this.ws.close(); } catch {}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function randomPort() {
|
|
143
|
+
return 9000 + Math.floor(Math.random() * 2000);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function launchBrowser(browser, profileDir, port, { minimized = false } = {}) {
|
|
147
|
+
fs.mkdirSync(profileDir, { recursive: true });
|
|
148
|
+
const args = [
|
|
149
|
+
`--remote-debugging-port=${port}`,
|
|
150
|
+
`--user-data-dir=${profileDir}`,
|
|
151
|
+
"--no-first-run",
|
|
152
|
+
"--no-default-browser-check",
|
|
153
|
+
"--disable-extensions",
|
|
154
|
+
"--disable-popup-blocking",
|
|
155
|
+
];
|
|
156
|
+
if (minimized) args.push("--start-minimized");
|
|
157
|
+
args.push("about:blank");
|
|
158
|
+
|
|
159
|
+
const proc = spawn(browser, args, {
|
|
160
|
+
stdio: "ignore",
|
|
161
|
+
detached: true,
|
|
162
|
+
windowsHide: minimized,
|
|
163
|
+
});
|
|
164
|
+
proc.unref();
|
|
165
|
+
return proc;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function connectPanTab(port) {
|
|
169
|
+
const tabs = await listTabs(port);
|
|
170
|
+
let tab = tabs.find((item) => item.type === "page" && /^https:\/\/pan\.baidu\.com\//.test(item.url));
|
|
171
|
+
if (!tab) tab = await openTab(port, PAN_HOME);
|
|
172
|
+
|
|
173
|
+
const cdp = new CDPClient(tab.webSocketDebuggerUrl);
|
|
174
|
+
await cdp.connect();
|
|
175
|
+
await cdp.send("Network.enable");
|
|
176
|
+
await cdp.send("Runtime.enable");
|
|
177
|
+
return { cdp, tab };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function waitForPanPage(cdp, timeoutMs = 20000) {
|
|
181
|
+
const deadline = Date.now() + timeoutMs;
|
|
182
|
+
while (Date.now() < deadline) {
|
|
183
|
+
try {
|
|
184
|
+
const result = await cdp.send("Runtime.evaluate", {
|
|
185
|
+
expression: "location.hostname === 'pan.baidu.com' && document.readyState !== 'loading'",
|
|
186
|
+
returnByValue: true,
|
|
187
|
+
});
|
|
188
|
+
if (result.result?.value === true) return;
|
|
189
|
+
} catch {}
|
|
190
|
+
await delay(300);
|
|
191
|
+
}
|
|
192
|
+
throw new Error("百度网盘页面加载超时");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function evaluateFetch(cdp, url, options = {}) {
|
|
196
|
+
const request = {
|
|
197
|
+
method: options.method || "GET",
|
|
198
|
+
credentials: "include",
|
|
199
|
+
headers: options.headers || {},
|
|
200
|
+
};
|
|
201
|
+
if (Object.prototype.hasOwnProperty.call(options, "body")) request.body = options.body;
|
|
202
|
+
|
|
203
|
+
const expression = `fetch(${JSON.stringify(url)}, ${JSON.stringify(request)})
|
|
204
|
+
.then(async response => ({
|
|
205
|
+
ok: response.ok,
|
|
206
|
+
status: response.status,
|
|
207
|
+
url: response.url,
|
|
208
|
+
text: await response.text()
|
|
209
|
+
}))`;
|
|
210
|
+
const evaluated = await cdp.send("Runtime.evaluate", {
|
|
211
|
+
expression,
|
|
212
|
+
awaitPromise: true,
|
|
213
|
+
returnByValue: true,
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
if (evaluated.exceptionDetails) {
|
|
217
|
+
const message = evaluated.exceptionDetails.exception?.description ||
|
|
218
|
+
evaluated.exceptionDetails.text || "Browser fetch failed";
|
|
219
|
+
throw new Error(message);
|
|
220
|
+
}
|
|
221
|
+
const response = evaluated.result?.value;
|
|
222
|
+
if (!response || typeof response.text !== "string") {
|
|
223
|
+
throw new Error("Browser fetch returned no response");
|
|
224
|
+
}
|
|
225
|
+
return response;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function openPersistentSession({ browser, profileDir, port, minimized }) {
|
|
229
|
+
let proc = null;
|
|
230
|
+
try {
|
|
231
|
+
await getDevToolsVersion(port);
|
|
232
|
+
} catch {
|
|
233
|
+
proc = launchBrowser(browser, profileDir, port, { minimized });
|
|
234
|
+
await waitForDevTools(port, 15000);
|
|
235
|
+
}
|
|
236
|
+
const { cdp, tab } = await connectPanTab(port);
|
|
237
|
+
return { browser, profileDir, port, proc, cdp, tab };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function getBrowserSession(settings = {}) {
|
|
241
|
+
if (activeSession) {
|
|
242
|
+
try {
|
|
243
|
+
await activeSession.cdp.send("Runtime.evaluate", { expression: "1" });
|
|
244
|
+
return activeSession;
|
|
245
|
+
} catch {
|
|
246
|
+
activeSession.cdp.close();
|
|
247
|
+
activeSession = null;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const browser = settings.browser || findBrowser();
|
|
252
|
+
if (!browser) throw new Error("未找到 Chrome/Edge,无法使用扫码登录会话调用网页 API");
|
|
253
|
+
const profileDir = settings.profileDir || DEFAULT_PROFILE_DIR;
|
|
254
|
+
const configuredPort = Number(settings.port) || randomPort();
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
activeSession = await openPersistentSession({
|
|
258
|
+
browser,
|
|
259
|
+
profileDir,
|
|
260
|
+
port: configuredPort,
|
|
261
|
+
minimized: true,
|
|
262
|
+
});
|
|
263
|
+
} catch (error) {
|
|
264
|
+
if (!settings.port) throw error;
|
|
265
|
+
activeSession = await openPersistentSession({
|
|
266
|
+
browser,
|
|
267
|
+
profileDir,
|
|
268
|
+
port: randomPort(),
|
|
269
|
+
minimized: true,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
await waitForPanPage(activeSession.cdp);
|
|
273
|
+
if (settings.onPort) settings.onPort(activeSession.port);
|
|
274
|
+
return activeSession;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function browserJson(url, options = {}, settings = {}) {
|
|
278
|
+
const session = await getBrowserSession(settings);
|
|
279
|
+
let response;
|
|
280
|
+
try {
|
|
281
|
+
response = await evaluateFetch(session.cdp, url, options);
|
|
282
|
+
} catch (error) {
|
|
283
|
+
session.cdp.close();
|
|
284
|
+
activeSession = null;
|
|
285
|
+
throw error;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
try {
|
|
289
|
+
return JSON.parse(response.text.replace(/^\s+/, ""));
|
|
290
|
+
} catch {
|
|
291
|
+
return { _raw: response.text, _status: response.status, _url: response.url };
|
|
292
|
+
} finally {
|
|
293
|
+
scheduleDisconnect();
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function getTemplateVariableUrl() {
|
|
298
|
+
const fields = encodeURIComponent('["bdstoken","token","uk","isdocuser","servertime"]');
|
|
299
|
+
return `https://pan.baidu.com/api/gettemplatevariable?clienttype=0&app_id=${APP_ID}&web=1&fields=${fields}`;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async function browserLogin({
|
|
303
|
+
timeoutMs = 180000,
|
|
304
|
+
onStatus,
|
|
305
|
+
profileDir = DEFAULT_PROFILE_DIR,
|
|
306
|
+
port = randomPort(),
|
|
307
|
+
} = {}) {
|
|
308
|
+
const browser = findBrowser();
|
|
309
|
+
if (!browser) {
|
|
310
|
+
throw new Error("未找到 Chrome/Edge。请手动获取 BDUSS/STOKEN 后使用 bdp login --bduss --stoken");
|
|
311
|
+
}
|
|
312
|
+
port = Number(port) || randomPort();
|
|
313
|
+
|
|
314
|
+
if (onStatus) onStatus("启动浏览器...");
|
|
315
|
+
let proc = null;
|
|
316
|
+
|
|
317
|
+
let cdp;
|
|
318
|
+
try {
|
|
319
|
+
try {
|
|
320
|
+
await getDevToolsVersion(port);
|
|
321
|
+
} catch {
|
|
322
|
+
proc = launchBrowser(browser, profileDir, port);
|
|
323
|
+
await waitForDevTools(port, 15000);
|
|
324
|
+
}
|
|
325
|
+
const tab = await openTab(port, PAN_HOME);
|
|
326
|
+
if (onStatus) onStatus("浏览器已打开,请登录百度网盘(扫码或账号密码均可)...");
|
|
327
|
+
|
|
328
|
+
cdp = new CDPClient(tab.webSocketDebuggerUrl);
|
|
329
|
+
await cdp.connect();
|
|
330
|
+
await cdp.send("Network.enable");
|
|
331
|
+
await cdp.send("Runtime.enable");
|
|
332
|
+
|
|
333
|
+
const deadline = Date.now() + timeoutMs;
|
|
334
|
+
let lastStatus = "";
|
|
335
|
+
let authRetries = 0;
|
|
336
|
+
|
|
337
|
+
while (Date.now() < deadline) {
|
|
338
|
+
try {
|
|
339
|
+
const { cookies } = await cdp.send("Network.getCookies", {
|
|
340
|
+
urls: ["https://pan.baidu.com/", "https://passport.baidu.com/"],
|
|
341
|
+
});
|
|
342
|
+
const bduss = cookies.find((cookie) => cookie.name === "BDUSS")?.value || "";
|
|
343
|
+
const stoken = cookies.find((cookie) => cookie.name === "STOKEN")?.value || "";
|
|
344
|
+
const status = bduss ? (stoken ? "both" : "bduss-only") : "none";
|
|
345
|
+
|
|
346
|
+
if (status !== lastStatus) {
|
|
347
|
+
lastStatus = status;
|
|
348
|
+
if (status === "both" && onStatus) onStatus("检测到登录凭证,正在浏览器会话内验证...");
|
|
349
|
+
if (status === "bduss-only" && onStatus) onStatus("已检测到 BDUSS,等待 STOKEN...");
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (bduss && stoken) {
|
|
353
|
+
await waitForPanPage(cdp, 5000);
|
|
354
|
+
const response = await evaluateFetch(cdp, getTemplateVariableUrl());
|
|
355
|
+
const data = JSON.parse(response.text.replace(/^\s+/, ""));
|
|
356
|
+
const bdstoken = data?.result?.bdstoken;
|
|
357
|
+
if (data?.errno === 0 && bdstoken) {
|
|
358
|
+
await minimizeWindow(cdp, tab.id);
|
|
359
|
+
cdp.close();
|
|
360
|
+
return { bduss, stoken, bdstoken, browserProfile: profileDir, browserPort: port };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
authRetries++;
|
|
364
|
+
if (onStatus && authRetries % 3 === 0) {
|
|
365
|
+
onStatus("登录凭证尚未通过网页 API 验证,请等待页面完成跳转...");
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
} catch {}
|
|
369
|
+
await delay(1500);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
throw new Error(`等待登录超时(${Math.round(timeoutMs / 1000)}秒)`);
|
|
373
|
+
} catch (error) {
|
|
374
|
+
if (cdp) cdp.close();
|
|
375
|
+
killBrowser(proc);
|
|
376
|
+
throw error;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function scheduleDisconnect() {
|
|
381
|
+
if (disconnectTimer) clearTimeout(disconnectTimer);
|
|
382
|
+
disconnectTimer = setTimeout(disconnectBrowserSession, 500);
|
|
383
|
+
disconnectTimer.unref();
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function disconnectBrowserSession() {
|
|
387
|
+
if (disconnectTimer) {
|
|
388
|
+
clearTimeout(disconnectTimer);
|
|
389
|
+
disconnectTimer = null;
|
|
390
|
+
}
|
|
391
|
+
if (activeSession) {
|
|
392
|
+
activeSession.cdp.close();
|
|
393
|
+
activeSession = null;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
async function minimizeWindow(cdp, targetId) {
|
|
398
|
+
try {
|
|
399
|
+
const { windowId } = await cdp.send("Browser.getWindowForTarget", { targetId });
|
|
400
|
+
await cdp.send("Browser.setWindowBounds", { windowId, bounds: { windowState: "minimized" } });
|
|
401
|
+
} catch {}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function killBrowser(proc) {
|
|
405
|
+
if (!proc || proc.killed) return;
|
|
406
|
+
try {
|
|
407
|
+
if (os.platform() === "win32") {
|
|
408
|
+
execSync(`taskkill /pid ${proc.pid} /T /F`, { stdio: "ignore" });
|
|
409
|
+
} else {
|
|
410
|
+
try { process.kill(-proc.pid, "SIGKILL"); } catch { proc.kill("SIGKILL"); }
|
|
411
|
+
}
|
|
412
|
+
} catch {}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function delay(ms) {
|
|
416
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
module.exports = {
|
|
420
|
+
browserLogin,
|
|
421
|
+
browserJson,
|
|
422
|
+
findBrowser,
|
|
423
|
+
DEFAULT_PROFILE_DIR,
|
|
424
|
+
disconnectBrowserSession,
|
|
425
|
+
_internals: { CDPClient, evaluateFetch, getTemplateVariableUrl },
|
|
426
|
+
};
|
package/lib/config.js
CHANGED
|
@@ -2,7 +2,7 @@ const fs = require("fs");
|
|
|
2
2
|
const path = require("path");
|
|
3
3
|
const os = require("os");
|
|
4
4
|
|
|
5
|
-
const CONFIG_DIR = path.join(os.homedir(), ".bdp");
|
|
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
8
|
function load() {
|
|
@@ -24,6 +24,10 @@ function get() {
|
|
|
24
24
|
return {
|
|
25
25
|
bduss: fileCfg.bduss || process.env.BDP_BDUSS || "",
|
|
26
26
|
stoken: fileCfg.stoken || process.env.BDP_STOKEN || "",
|
|
27
|
+
cookie: fileCfg.cookie || process.env.BDP_COOKIE || "",
|
|
28
|
+
webTransport: fileCfg.webTransport || "curl",
|
|
29
|
+
browserProfile: fileCfg.browserProfile || "",
|
|
30
|
+
browserPort: parseInt(fileCfg.browserPort || "0", 10) || 0,
|
|
27
31
|
pcsPath: fileCfg.pcsPath || process.env.BAIDUPCS_CMD || findPCS(),
|
|
28
32
|
ua: fileCfg.ua || process.env.NETDISK_UA ||
|
|
29
33
|
"netdisk;P2SP;3.0.0.8;netdisk;11.12.3;ANG-AN00;android-android;10.0;JSbridge4.4.0;jointBridge;1.1.0;",
|
|
@@ -37,6 +41,18 @@ function set(key, value) {
|
|
|
37
41
|
save(cfg);
|
|
38
42
|
}
|
|
39
43
|
|
|
44
|
+
function update(values) {
|
|
45
|
+
save({ ...load(), ...values });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function setAuth(values) {
|
|
49
|
+
const cfg = load();
|
|
50
|
+
for (const key of ["bduss", "stoken", "cookie", "webTransport", "browserProfile", "browserPort"]) {
|
|
51
|
+
delete cfg[key];
|
|
52
|
+
}
|
|
53
|
+
save({ ...cfg, ...values });
|
|
54
|
+
}
|
|
55
|
+
|
|
40
56
|
function findPCS() {
|
|
41
57
|
// 1. Look in package root (postinstall target)
|
|
42
58
|
const pkgRoot = path.join(__dirname, "..");
|
|
@@ -56,4 +72,4 @@ function isLoggedIn() {
|
|
|
56
72
|
return !!(cfg.bduss && cfg.stoken);
|
|
57
73
|
}
|
|
58
74
|
|
|
59
|
-
module.exports = { get, set, load, save, isLoggedIn, CONFIG_DIR, CONFIG_FILE };
|
|
75
|
+
module.exports = { get, set, update, setAuth, load, save, isLoggedIn, CONFIG_DIR, CONFIG_FILE };
|
package/lib/group.js
CHANGED
|
@@ -5,7 +5,7 @@ async function listGroups() {
|
|
|
5
5
|
const bdstoken = await getBdstoken();
|
|
6
6
|
if (!bdstoken) throw new Error("Cannot get bdstoken — check BDUSS/STOKEN config");
|
|
7
7
|
|
|
8
|
-
const data = http.
|
|
8
|
+
const data = await http.webJson(
|
|
9
9
|
`${http.PAN_BASE}/mbox/group/list?clienttype=0&app_id=${http.APP_ID}&web=1&bdstoken=${bdstoken}`
|
|
10
10
|
);
|
|
11
11
|
|
|
@@ -23,7 +23,7 @@ async function listGroups() {
|
|
|
23
23
|
|
|
24
24
|
async function listShares(gid) {
|
|
25
25
|
const bdstoken = await getBdstoken();
|
|
26
|
-
const data = http.
|
|
26
|
+
const data = await http.webJson(
|
|
27
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}`,
|
|
28
28
|
{ method: "POST", body: "" }
|
|
29
29
|
);
|
|
@@ -68,7 +68,7 @@ async function listFiles(gid, fsId, options = {}) {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
const bdstoken = await getBdstoken();
|
|
71
|
-
const data = http.
|
|
71
|
+
const data = await http.webJson(
|
|
72
72
|
`${http.PAN_BASE}/mbox/msg/shareinfo?type=2&from_uk=${fromUk}&msg_id=${msgId}&to_uk=0&num=${num}&page=${page}&fs_id=${fsId}&gid=${gid}&clienttype=0&app_id=${http.APP_ID}&web=1&bdstoken=${bdstoken}`,
|
|
73
73
|
{ method: "POST", body: "" }
|
|
74
74
|
);
|
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.1",
|
|
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",
|