dsh-shlr-bilitoolkit 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +122 -3
- package/cordis.patch.yml +13 -0
- package/dist/apis/article.d.ts +12 -0
- package/dist/apis/article.js +49 -0
- package/dist/apis/auth.d.ts +20 -0
- package/dist/apis/auth.js +83 -0
- package/dist/apis/comment.d.ts +37 -0
- package/dist/apis/comment.js +164 -0
- package/dist/apis/download.d.ts +8 -0
- package/dist/apis/download.js +60 -0
- package/dist/apis/dynamic.d.ts +23 -0
- package/dist/apis/dynamic.js +154 -0
- package/dist/apis/interact.d.ts +29 -0
- package/dist/apis/interact.js +224 -0
- package/dist/apis/message.d.ts +18 -0
- package/dist/apis/message.js +109 -0
- package/dist/apis/notify.d.ts +17 -0
- package/dist/apis/notify.js +54 -0
- package/dist/apis/profile.d.ts +15 -0
- package/dist/apis/profile.js +59 -0
- package/dist/apis/relation.d.ts +13 -0
- package/dist/apis/relation.js +47 -0
- package/dist/apis/search.d.ts +17 -0
- package/dist/apis/search.js +81 -0
- package/dist/apis/space.d.ts +17 -0
- package/dist/apis/space.js +89 -0
- package/dist/apis/video.d.ts +18 -0
- package/dist/apis/video.js +153 -0
- package/dist/cache.d.ts +18 -0
- package/dist/cache.js +75 -0
- package/dist/client.d.ts +85 -0
- package/dist/client.js +325 -0
- package/dist/dsh.d.ts +25 -0
- package/dist/dsh.js +484 -0
- package/dist/endpoints.d.ts +55 -0
- package/dist/endpoints.js +69 -0
- package/dist/errors.d.ts +18 -0
- package/dist/errors.js +50 -0
- package/dist/formatting.d.ts +7 -0
- package/dist/formatting.js +69 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +21 -0
- package/dist/models.d.ts +215 -0
- package/dist/models.js +876 -0
- package/dist/pagination.d.ts +7 -0
- package/dist/pagination.js +20 -0
- package/dist/util.d.ts +2 -0
- package/dist/util.js +4 -0
- package/dist/wbi.d.ts +21 -0
- package/dist/wbi.js +105 -0
- package/package.json +52 -6
package/dist/client.js
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
/** BiliClient:统一维护 Session、Cookie、请求细节和账号状态。
|
|
2
|
+
|
|
3
|
+
内置基础节流与退避,目标是让请求节奏接近正常浏览,降低触发 B 站风控的概率:
|
|
4
|
+
|
|
5
|
+
- 请求之间保持最小间隔(默认 0.6 秒,带随机抖动)
|
|
6
|
+
- 写操作之间额外保持间隔(默认 1 秒)
|
|
7
|
+
- GET 遇到网络错误或 -799「请求过于频繁」时指数退避重试
|
|
8
|
+
- 写操作绝不自动重试,避免重复评论 / 重复私信
|
|
9
|
+
- HTTP 412 与业务码 -412 视为风控信号,立即停止,不做重试
|
|
10
|
+
*/
|
|
11
|
+
import axios from "axios";
|
|
12
|
+
import { existsSync } from "node:fs";
|
|
13
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
14
|
+
import * as path from "node:path";
|
|
15
|
+
import * as endpoints from "./endpoints";
|
|
16
|
+
import { BiliApiError, CaptchaRequired, CookieError } from "./errors";
|
|
17
|
+
import { VideoAPI } from "./apis/video";
|
|
18
|
+
import { SearchAPI } from "./apis/search";
|
|
19
|
+
import { SpaceAPI } from "./apis/space";
|
|
20
|
+
import { CommentAPI } from "./apis/comment";
|
|
21
|
+
import { MessageAPI } from "./apis/message";
|
|
22
|
+
import { NotifyAPI } from "./apis/notify";
|
|
23
|
+
import { InteractAPI } from "./apis/interact";
|
|
24
|
+
import { RelationAPI } from "./apis/relation";
|
|
25
|
+
import { DynamicAPI } from "./apis/dynamic";
|
|
26
|
+
import { ArticleAPI } from "./apis/article";
|
|
27
|
+
import { ProfileAPI } from "./apis/profile";
|
|
28
|
+
import { QrCodeLogin } from "./apis/auth";
|
|
29
|
+
import { WbiSigner } from "./wbi";
|
|
30
|
+
import { JsonCache } from "./cache";
|
|
31
|
+
import { sleep } from "./util";
|
|
32
|
+
export const USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " +
|
|
33
|
+
"(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
|
|
34
|
+
export const DEFAULT_HEADERS = {
|
|
35
|
+
"User-Agent": USER_AGENT,
|
|
36
|
+
Referer: "https://www.bilibili.com/",
|
|
37
|
+
};
|
|
38
|
+
const RETRYABLE_CODES = new Set([-799]);
|
|
39
|
+
const RISK_CONTROL_CODES = new Set([-412]);
|
|
40
|
+
const RISK_CONTROL_HTTP = new Set([412]);
|
|
41
|
+
export class BiliClient {
|
|
42
|
+
http;
|
|
43
|
+
timeout; // 毫秒
|
|
44
|
+
cookieFile;
|
|
45
|
+
minInterval; // 毫秒
|
|
46
|
+
jitter; // 毫秒
|
|
47
|
+
maxRetries;
|
|
48
|
+
writeInterval; // 毫秒
|
|
49
|
+
video;
|
|
50
|
+
search;
|
|
51
|
+
space;
|
|
52
|
+
comment;
|
|
53
|
+
message;
|
|
54
|
+
notify;
|
|
55
|
+
interact;
|
|
56
|
+
relation;
|
|
57
|
+
dynamic;
|
|
58
|
+
article;
|
|
59
|
+
profile;
|
|
60
|
+
auth;
|
|
61
|
+
wbi;
|
|
62
|
+
cache;
|
|
63
|
+
lastRequestAt = 0;
|
|
64
|
+
lastWriteAt = 0;
|
|
65
|
+
cookies = {};
|
|
66
|
+
_account = undefined;
|
|
67
|
+
constructor(opts = {}) {
|
|
68
|
+
this.timeout = (opts.timeout ?? 10) * 1000;
|
|
69
|
+
this.cookieFile = opts.cookieFile ?? "bili_cookies.json";
|
|
70
|
+
this.minInterval = (opts.minInterval ?? 0.6) * 1000;
|
|
71
|
+
this.jitter = (opts.jitter ?? 0.4) * 1000;
|
|
72
|
+
this.maxRetries = Math.max(0, opts.maxRetries ?? 2);
|
|
73
|
+
this.writeInterval = (opts.writeInterval ?? 1.0) * 1000;
|
|
74
|
+
this.http = axios.create({ timeout: this.timeout, headers: { ...DEFAULT_HEADERS } });
|
|
75
|
+
this.wbi = new WbiSigner(this);
|
|
76
|
+
this.video = new VideoAPI(this);
|
|
77
|
+
this.search = new SearchAPI(this);
|
|
78
|
+
this.space = new SpaceAPI(this);
|
|
79
|
+
this.comment = new CommentAPI(this);
|
|
80
|
+
this.message = new MessageAPI(this);
|
|
81
|
+
this.notify = new NotifyAPI(this);
|
|
82
|
+
this.interact = new InteractAPI(this);
|
|
83
|
+
this.relation = new RelationAPI(this);
|
|
84
|
+
this.dynamic = new DynamicAPI(this);
|
|
85
|
+
this.article = new ArticleAPI(this);
|
|
86
|
+
this.profile = new ProfileAPI(this);
|
|
87
|
+
this.auth = new QrCodeLogin(this);
|
|
88
|
+
this.cache = new JsonCache(opts.cacheFile ?? process.env.BILI_CACHE_FILE ?? "bili_cache.json");
|
|
89
|
+
}
|
|
90
|
+
// ------------------------------------------------------------------ 请求
|
|
91
|
+
async get(url, params, headers, wbi = false) {
|
|
92
|
+
if (wbi)
|
|
93
|
+
params = await this.wbi.sign(params);
|
|
94
|
+
return this.request("GET", url, { params, headers });
|
|
95
|
+
}
|
|
96
|
+
async post(url, data, headers, wbi = false, params, jsonBody = false) {
|
|
97
|
+
if (wbi && data && typeof data === "object") {
|
|
98
|
+
data = await this.wbi.sign(data);
|
|
99
|
+
}
|
|
100
|
+
return this.request("POST", url, { data, params, headers, jsonBody });
|
|
101
|
+
}
|
|
102
|
+
async getSigned(url, params, headers) {
|
|
103
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
104
|
+
try {
|
|
105
|
+
return await this.get(url, await this.wbi.sign(params), headers);
|
|
106
|
+
}
|
|
107
|
+
catch (exc) {
|
|
108
|
+
if (attempt === 0 &&
|
|
109
|
+
exc instanceof BiliApiError &&
|
|
110
|
+
(exc.code === -403 || exc.code === -352)) {
|
|
111
|
+
await this.wbi.refresh();
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
throw exc;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
throw new BiliApiError("WBI 签名请求失败");
|
|
118
|
+
}
|
|
119
|
+
async request(method, url, extra) {
|
|
120
|
+
// B 站写接口要求表单编码;jsonBody=true 时才按 JSON 发送(如动态点赞)
|
|
121
|
+
const { jsonBody, data: rawData, headers: rawHeaders, ...rest } = extra;
|
|
122
|
+
let data = rawData;
|
|
123
|
+
let headers = rawHeaders;
|
|
124
|
+
if (!jsonBody &&
|
|
125
|
+
data != null &&
|
|
126
|
+
typeof data === "object" &&
|
|
127
|
+
!(data instanceof URLSearchParams)) {
|
|
128
|
+
const form = new URLSearchParams();
|
|
129
|
+
for (const [k, v] of Object.entries(data)) {
|
|
130
|
+
form.append(k, v == null ? "" : String(v));
|
|
131
|
+
}
|
|
132
|
+
data = form;
|
|
133
|
+
headers = {
|
|
134
|
+
...(headers ?? {}),
|
|
135
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
const config = {
|
|
139
|
+
method: method,
|
|
140
|
+
url,
|
|
141
|
+
...rest,
|
|
142
|
+
data,
|
|
143
|
+
headers,
|
|
144
|
+
};
|
|
145
|
+
const write = method.toUpperCase() !== "GET";
|
|
146
|
+
const attempts = write ? 0 : this.maxRetries;
|
|
147
|
+
for (let attempt = 0; attempt <= attempts; attempt++) {
|
|
148
|
+
await this.throttle(write);
|
|
149
|
+
try {
|
|
150
|
+
const response = await this.http.request(config);
|
|
151
|
+
// 像 requests 的 cookie jar 一样,自动吸收响应里的 Set-Cookie
|
|
152
|
+
// (登录接口正是通过 Set-Cookie 下发 SESSDATA / bili_jct)。
|
|
153
|
+
const setCookie = response.headers["set-cookie"];
|
|
154
|
+
if (setCookie) {
|
|
155
|
+
this.updateCookiesFromHeader(Array.isArray(setCookie) ? setCookie : [String(setCookie)]);
|
|
156
|
+
}
|
|
157
|
+
this.rememberRequest(write);
|
|
158
|
+
let payload = response.data;
|
|
159
|
+
if (typeof payload === "string") {
|
|
160
|
+
try {
|
|
161
|
+
payload = JSON.parse(payload);
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
throw new BiliApiError("接口返回的不是合法 JSON");
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (!payload || typeof payload !== "object") {
|
|
168
|
+
throw new BiliApiError("接口返回格式异常");
|
|
169
|
+
}
|
|
170
|
+
const code = payload.code;
|
|
171
|
+
if (code === 0)
|
|
172
|
+
return payload;
|
|
173
|
+
if (RISK_CONTROL_CODES.has(code)) {
|
|
174
|
+
throw new BiliApiError(`触发 B 站风控(${code}),已停止请求,请过段时间再试`, code, payload.data);
|
|
175
|
+
}
|
|
176
|
+
if (RETRYABLE_CODES.has(code) && attempt < attempts) {
|
|
177
|
+
await this.backoff(attempt);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const data = payload.data;
|
|
181
|
+
const gaData = (data ?? {}).ga_data ?? {};
|
|
182
|
+
const decisions = gaData.decisions ?? [];
|
|
183
|
+
if (decisions.length) {
|
|
184
|
+
throw new CaptchaRequired(`B 站要求人机验证(${decisions.join(", ")}),该操作需要先在网页端完成验证`, code, data, decisions);
|
|
185
|
+
}
|
|
186
|
+
throw new BiliApiError(payload.message ?? "接口返回错误", code, data);
|
|
187
|
+
}
|
|
188
|
+
catch (exc) {
|
|
189
|
+
if (exc instanceof BiliApiError)
|
|
190
|
+
throw exc;
|
|
191
|
+
if (axios.isAxiosError(exc)) {
|
|
192
|
+
const status = exc.response?.status;
|
|
193
|
+
if (status && RISK_CONTROL_HTTP.has(status)) {
|
|
194
|
+
throw new BiliApiError(`触发 B 站风控(HTTP ${status}),已停止请求`, undefined, undefined);
|
|
195
|
+
}
|
|
196
|
+
if (attempt < attempts) {
|
|
197
|
+
this.rememberRequest(write);
|
|
198
|
+
await this.backoff(attempt);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
throw new BiliApiError(`请求失败: ${exc.message}`);
|
|
202
|
+
}
|
|
203
|
+
throw exc;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
throw new BiliApiError("请求重试次数已用完");
|
|
207
|
+
}
|
|
208
|
+
// ------------------------------------------------------------------ 节流
|
|
209
|
+
async throttle(write) {
|
|
210
|
+
const now = Date.now();
|
|
211
|
+
let wait = 0;
|
|
212
|
+
if (this.minInterval > 0) {
|
|
213
|
+
const gap = this.minInterval + Math.random() * Math.max(0, this.jitter);
|
|
214
|
+
wait = Math.max(wait, gap - (now - this.lastRequestAt));
|
|
215
|
+
}
|
|
216
|
+
if (write && this.writeInterval > 0) {
|
|
217
|
+
wait = Math.max(wait, this.writeInterval - (now - this.lastWriteAt));
|
|
218
|
+
}
|
|
219
|
+
if (wait > 0)
|
|
220
|
+
await sleep(wait);
|
|
221
|
+
}
|
|
222
|
+
rememberRequest(write) {
|
|
223
|
+
this.lastRequestAt = Date.now();
|
|
224
|
+
if (write)
|
|
225
|
+
this.lastWriteAt = this.lastRequestAt;
|
|
226
|
+
}
|
|
227
|
+
async backoff(attempt) {
|
|
228
|
+
await sleep(Math.min(8000, 2 ** attempt + Math.random() * 500));
|
|
229
|
+
}
|
|
230
|
+
// ------------------------------------------------------------------ Cookie
|
|
231
|
+
applyCookieHeader() {
|
|
232
|
+
const cookie = Object.entries(this.cookies)
|
|
233
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
234
|
+
.join("; ");
|
|
235
|
+
this.http.defaults.headers.common["Cookie"] = cookie;
|
|
236
|
+
}
|
|
237
|
+
async loadCookies() {
|
|
238
|
+
if (!existsSync(this.cookieFile)) {
|
|
239
|
+
throw new CookieError(`未找到 Cookie 文件 [${this.cookieFile}],请先完成登录`);
|
|
240
|
+
}
|
|
241
|
+
let text;
|
|
242
|
+
try {
|
|
243
|
+
text = await readFile(this.cookieFile, "utf-8");
|
|
244
|
+
}
|
|
245
|
+
catch (exc) {
|
|
246
|
+
throw new CookieError(`Cookie 文件读取失败: ${exc.message}`);
|
|
247
|
+
}
|
|
248
|
+
let dict;
|
|
249
|
+
try {
|
|
250
|
+
dict = JSON.parse(text);
|
|
251
|
+
}
|
|
252
|
+
catch (exc) {
|
|
253
|
+
throw new CookieError(`Cookie 文件格式错误: ${exc.message}`);
|
|
254
|
+
}
|
|
255
|
+
if (!dict || typeof dict !== "object" || Object.keys(dict).length === 0) {
|
|
256
|
+
throw new CookieError(`Cookie 文件 [${this.cookieFile}] 内容为空`);
|
|
257
|
+
}
|
|
258
|
+
this.cookies = dict;
|
|
259
|
+
this.applyCookieHeader();
|
|
260
|
+
this._account = undefined;
|
|
261
|
+
return this.cookieFile;
|
|
262
|
+
}
|
|
263
|
+
async saveCookies() {
|
|
264
|
+
await mkdir(path.dirname(this.cookieFile), { recursive: true });
|
|
265
|
+
await writeFile(this.cookieFile, JSON.stringify(this.cookies, null, 4), "utf-8");
|
|
266
|
+
return this.cookieFile;
|
|
267
|
+
}
|
|
268
|
+
/** 清空本地缓存(WBI 密钥、bvid→aid 等),下次请求会自动重新拉取。 */
|
|
269
|
+
clearCache() {
|
|
270
|
+
this.cache.clear();
|
|
271
|
+
}
|
|
272
|
+
/** 供登录流程(未来实现)把 Set-Cookie 写入本地状态。 */
|
|
273
|
+
updateCookiesFromHeader(setCookie) {
|
|
274
|
+
for (const sc of setCookie) {
|
|
275
|
+
const [pair] = sc.split(";");
|
|
276
|
+
const idx = pair.indexOf("=");
|
|
277
|
+
if (idx > 0)
|
|
278
|
+
this.cookies[pair.slice(0, idx).trim()] = pair.slice(idx + 1).trim();
|
|
279
|
+
}
|
|
280
|
+
this.applyCookieHeader();
|
|
281
|
+
}
|
|
282
|
+
get isLoggedIn() {
|
|
283
|
+
return !!this.cookies["SESSDATA"];
|
|
284
|
+
}
|
|
285
|
+
get csrf() {
|
|
286
|
+
const token = this.cookies["bili_jct"];
|
|
287
|
+
if (!token)
|
|
288
|
+
throw new CookieError("缺少 bili_jct,无法执行写操作,请重新登录");
|
|
289
|
+
return token;
|
|
290
|
+
}
|
|
291
|
+
// ------------------------------------------------------------------ 账号
|
|
292
|
+
async fetchAccount() {
|
|
293
|
+
try {
|
|
294
|
+
const payload = await this.get(endpoints.NAV);
|
|
295
|
+
const data = payload?.data ?? {};
|
|
296
|
+
return data.isLogin ? data : null;
|
|
297
|
+
}
|
|
298
|
+
catch (exc) {
|
|
299
|
+
if (exc instanceof BiliApiError && exc.code === -101)
|
|
300
|
+
return null;
|
|
301
|
+
throw exc;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
async getAccount() {
|
|
305
|
+
if (this._account === undefined)
|
|
306
|
+
this._account = await this.fetchAccount();
|
|
307
|
+
return this._account;
|
|
308
|
+
}
|
|
309
|
+
get username() {
|
|
310
|
+
if (this._account && typeof this._account === "object" && this._account.uname) {
|
|
311
|
+
return this._account.uname;
|
|
312
|
+
}
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
async checkLogin() {
|
|
316
|
+
return (await this.fetchAccount()) !== null;
|
|
317
|
+
}
|
|
318
|
+
async requireLogin() {
|
|
319
|
+
const account = await this.fetchAccount();
|
|
320
|
+
if (!account)
|
|
321
|
+
throw new CookieError("未登录或 Cookie 已过期,请重新登录");
|
|
322
|
+
this._account = account;
|
|
323
|
+
return account;
|
|
324
|
+
}
|
|
325
|
+
}
|
package/dist/dsh.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** dsh 插件入口(对齐官方 DeepSeek Harness / cordis 插件规范)。
|
|
2
|
+
|
|
3
|
+
本文件即组合包的插件入口(`package.json` 的 main 指向它)。需要导出:
|
|
4
|
+
- `name` 插件名(Cordis 内部标识)
|
|
5
|
+
- `inject` 声明依赖的服务,这里注入 `tools`
|
|
6
|
+
- `apply(ctx)` 入口,由 Cordis 调用;内部用 `ctx.tools.register(defineTool(...))`
|
|
7
|
+
注册每个工具。
|
|
8
|
+
|
|
9
|
+
工具定义仍以 JSON-Schema 形式写在 `TOOLS` 里(便于阅读),`apply` 会转换成
|
|
10
|
+
官方的 `defineTool({ name, description, parameters, output, execute })` 形式。
|
|
11
|
+
*/
|
|
12
|
+
import { BiliClient } from "./client";
|
|
13
|
+
import { Collection, Page, PlayInfo, Subtitle, User, Video } from "./models";
|
|
14
|
+
export declare const name = "bili-toolkit";
|
|
15
|
+
export declare const inject: string[];
|
|
16
|
+
export interface DshTool {
|
|
17
|
+
id: string;
|
|
18
|
+
description: string;
|
|
19
|
+
parameters: Record<string, unknown>;
|
|
20
|
+
handler: (args: any) => Promise<unknown>;
|
|
21
|
+
}
|
|
22
|
+
export declare const TOOLS: DshTool[];
|
|
23
|
+
export declare function apply(ctx: any): void;
|
|
24
|
+
export * from "./index";
|
|
25
|
+
export { BiliClient, Video, User, Page, PlayInfo, Subtitle, Collection };
|