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 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</summary>
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 │ lib/http.js (curl) │
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 # HTTP 客户端 (curl 封装)
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
- if (args.flags.bduss) config.set("bduss", args.flags.bduss);
94
- if (args.flags.stoken) config.set("stoken", args.flags.stoken);
95
- const cfg = config.get();
96
- output(
97
- { success: true, bduss: cfg.bduss ? "***set***" : "missing", stoken: cfg.stoken ? "***set***" : "missing" },
98
- json,
99
- () => console.log("Credentials saved to " + config.CONFIG_FILE)
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 --bduss X --stoken Y Save credentials
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.exit(1);
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.curlJson(
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.curlJson(
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.curlJson(
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: `BDUSS=${cfg.bduss}; STOKEN=${cfg.stoken}`,
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: `BDUSS=${cfg.bduss}; STOKEN=${cfg.stoken}`,
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 = curlJson(
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. Check BDUSS/STOKEN.\n` +
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
- ` Run: bdp login --bduss <value> --stoken <value>`
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.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",