@zhin.js/plugin-bilibili 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/api.js ADDED
@@ -0,0 +1,298 @@
1
+ /**
2
+ * Minimal Bilibili web API client.
3
+ *
4
+ * Only public, key-less endpoints are used. Every response is validated
5
+ * defensively: when Bilibili changes a payload we raise a `BiliApiError` with
6
+ * kind `shape` instead of crashing the poller, and risk-control responses
7
+ * (-352 / -412 / -799 / HTTP 412) are reported as kind `risk` so callers can
8
+ * back off.
9
+ */
10
+ import { mixinKey, signWbi, wbiKeyFromUrl } from './wbi.js';
11
+ export const DEFAULT_USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36';
12
+ export class BiliApiError extends Error {
13
+ kind;
14
+ code;
15
+ constructor(kind, message, code) {
16
+ super(message);
17
+ this.name = 'BiliApiError';
18
+ this.kind = kind;
19
+ this.code = code;
20
+ }
21
+ }
22
+ const RISK_CODES = new Set([-352, -412, -509, -799, -401]);
23
+ const NOT_FOUND_CODES = new Set([-404, -626, 4100000, 60004, 1]);
24
+ const WBI_TTL_MS = 6 * 60 * 60 * 1000;
25
+ /**
26
+ * Browser fingerprint hints the web client sends with the space feed. Without
27
+ * them anonymous requests are answered with -352 far more often.
28
+ */
29
+ const DM_PARAMS = Object.freeze({
30
+ dm_img_list: '[]',
31
+ dm_img_str: Buffer.from('WebGL 1.0 (OpenGL ES 2.0 Chromium)').toString('base64').replace(/=+$/u, ''),
32
+ dm_cover_img_str: Buffer.from('ANGLE (Apple, ANGLE Metal Renderer: Apple M1, Unspecified Version)Google Inc. (Apple)')
33
+ .toString('base64')
34
+ .replace(/=+$/u, ''),
35
+ });
36
+ function isRecord(value) {
37
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
38
+ }
39
+ function str(value) {
40
+ return typeof value === 'string' ? value : typeof value === 'number' && Number.isFinite(value) ? String(value) : '';
41
+ }
42
+ function int(value) {
43
+ const parsed = typeof value === 'number' ? value : typeof value === 'string' ? Number(value) : Number.NaN;
44
+ return Number.isFinite(parsed) ? Math.trunc(parsed) : 0;
45
+ }
46
+ function clip(text, max = 120) {
47
+ const flat = text.replace(/\s+/gu, ' ').trim();
48
+ return flat.length > max ? `${flat.slice(0, max)}…` : flat;
49
+ }
50
+ function absoluteUrl(url) {
51
+ if (url.startsWith('//'))
52
+ return `https:${url}`;
53
+ return url;
54
+ }
55
+ export class BiliClient {
56
+ #timeout;
57
+ #userCookie;
58
+ #fetch;
59
+ #now;
60
+ #buvid;
61
+ #wbi;
62
+ constructor(options = {}) {
63
+ this.#timeout = options.timeout ?? 10_000;
64
+ this.#userCookie = options.cookie?.trim() ?? '';
65
+ this.#fetch = options.fetch ?? ((input, init) => fetch(input, init));
66
+ this.#now = options.now ?? Date.now;
67
+ }
68
+ /** Forget cached device id and WBI key, e.g. after a risk-control response. */
69
+ resetSession() {
70
+ this.#buvid = undefined;
71
+ this.#wbi = undefined;
72
+ }
73
+ async #request(url, init = {}) {
74
+ const headers = {
75
+ 'user-agent': DEFAULT_USER_AGENT,
76
+ accept: 'application/json, text/plain, */*',
77
+ referer: init.referer ?? 'https://www.bilibili.com/',
78
+ ...init.headers,
79
+ };
80
+ if (init.cookie !== false) {
81
+ const cookie = await this.#cookie();
82
+ if (cookie)
83
+ headers.cookie = cookie;
84
+ }
85
+ let response;
86
+ try {
87
+ response = await this.#fetch(url, {
88
+ method: init.method ?? 'GET',
89
+ headers,
90
+ ...(init.body === undefined ? {} : { body: init.body }),
91
+ signal: AbortSignal.timeout(this.#timeout),
92
+ });
93
+ }
94
+ catch (error) {
95
+ const cause = error;
96
+ throw new BiliApiError('network', cause.name === 'TimeoutError' ? `请求超时(${this.#timeout}ms)` : `请求失败: ${cause.message}`);
97
+ }
98
+ if (response.status === 412)
99
+ throw new BiliApiError('risk', 'B 站风控拦截(HTTP 412)', 412);
100
+ if (!response.ok)
101
+ throw new BiliApiError('http', `HTTP ${response.status}`, response.status);
102
+ let body;
103
+ try {
104
+ body = await response.json();
105
+ }
106
+ catch {
107
+ throw new BiliApiError('shape', '接口返回的不是 JSON');
108
+ }
109
+ if (!isRecord(body))
110
+ throw new BiliApiError('shape', '接口返回格式异常');
111
+ const code = int(body.code);
112
+ if (code !== 0 && !init.acceptCodes?.includes(code)) {
113
+ const message = str(body.message) || str(body.msg) || `错误码 ${code}`;
114
+ if (RISK_CODES.has(code)) {
115
+ this.resetSession();
116
+ throw new BiliApiError('risk', `B 站风控拦截(${code}: ${message})`, code);
117
+ }
118
+ if (NOT_FOUND_CODES.has(code))
119
+ throw new BiliApiError('not_found', message, code);
120
+ throw new BiliApiError('http', message, code);
121
+ }
122
+ return body.data;
123
+ }
124
+ async #cookie() {
125
+ if (this.#userCookie)
126
+ return this.#userCookie;
127
+ if (this.#buvid !== undefined)
128
+ return this.#buvid;
129
+ try {
130
+ const data = await this.#request('https://api.bilibili.com/x/frontend/finger/spi', { cookie: false });
131
+ const b3 = isRecord(data) ? str(data.b_3) : '';
132
+ const b4 = isRecord(data) ? str(data.b_4) : '';
133
+ this.#buvid = b3 ? `buvid3=${b3}${b4 ? `; buvid4=${encodeURIComponent(b4)}` : ''}` : '';
134
+ }
135
+ catch {
136
+ // Device id is best effort; most endpoints still answer without it.
137
+ this.#buvid = '';
138
+ }
139
+ return this.#buvid;
140
+ }
141
+ async #wbiKey() {
142
+ if (this.#wbi && this.#now() - this.#wbi.at < WBI_TTL_MS)
143
+ return this.#wbi.key;
144
+ // `nav` answers -101 (not logged in) for guests but still carries wbi_img.
145
+ const data = await this.#request('https://api.bilibili.com/x/web-interface/nav', { acceptCodes: [-101] });
146
+ const img = isRecord(data) && isRecord(data.wbi_img) ? data.wbi_img : undefined;
147
+ const imgUrl = img ? str(img.img_url) : '';
148
+ const subUrl = img ? str(img.sub_url) : '';
149
+ if (!imgUrl || !subUrl)
150
+ throw new BiliApiError('shape', '无法获取 WBI 密钥');
151
+ const key = mixinKey(wbiKeyFromUrl(imgUrl), wbiKeyFromUrl(subUrl));
152
+ this.#wbi = { key, at: this.#now() };
153
+ return key;
154
+ }
155
+ /** Batch live status by UP uid (no auth). */
156
+ async getLiveStatus(uids) {
157
+ const result = new Map();
158
+ for (let index = 0; index < uids.length; index += 50) {
159
+ const chunk = uids.slice(index, index + 50).map(Number);
160
+ const data = await this.#request('https://api.live.bilibili.com/room/v1/Room/get_status_info_by_uids', {
161
+ method: 'POST',
162
+ headers: { 'content-type': 'application/json' },
163
+ body: JSON.stringify({ uids: chunk }),
164
+ referer: 'https://live.bilibili.com/',
165
+ });
166
+ // An empty result is serialised as [] instead of {}.
167
+ if (Array.isArray(data))
168
+ continue;
169
+ if (!isRecord(data))
170
+ throw new BiliApiError('shape', '开播状态接口格式异常');
171
+ for (const [key, value] of Object.entries(data)) {
172
+ if (!isRecord(value))
173
+ continue;
174
+ const uid = str(value.uid) || key;
175
+ const roomId = str(value.room_id);
176
+ result.set(uid, {
177
+ uid,
178
+ uname: str(value.uname),
179
+ roomId,
180
+ title: str(value.title),
181
+ liveStatus: int(value.live_status),
182
+ liveTime: int(value.live_time),
183
+ cover: str(value.cover_from_user) || str(value.keyframe),
184
+ area: str(value.area_v2_name) || str(value.area_name),
185
+ url: roomId ? `https://live.bilibili.com/${roomId}` : '',
186
+ });
187
+ }
188
+ }
189
+ return result;
190
+ }
191
+ async getRoomInfo(roomId) {
192
+ const data = await this.#request(`https://api.live.bilibili.com/room/v1/Room/get_info?room_id=${encodeURIComponent(roomId)}`, { referer: 'https://live.bilibili.com/' });
193
+ if (!isRecord(data) || !str(data.room_id))
194
+ throw new BiliApiError('shape', '直播间接口格式异常');
195
+ const realId = str(data.room_id);
196
+ return {
197
+ uid: str(data.uid),
198
+ roomId: realId,
199
+ title: str(data.title),
200
+ liveStatus: int(data.live_status),
201
+ area: str(data.area_name),
202
+ online: int(data.online),
203
+ url: `https://live.bilibili.com/${realId}`,
204
+ };
205
+ }
206
+ async getUserCard(uid) {
207
+ const data = await this.#request(`https://api.bilibili.com/x/web-interface/card?mid=${encodeURIComponent(uid)}`);
208
+ const card = isRecord(data) && isRecord(data.card) ? data.card : undefined;
209
+ if (!card)
210
+ throw new BiliApiError('shape', '用户信息接口格式异常');
211
+ return { uid: str(card.mid) || uid, name: str(card.name) };
212
+ }
213
+ /** Latest space dynamics of one UP (WBI signed). Newest first, as returned. */
214
+ async getDynamics(uid) {
215
+ const key = await this.#wbiKey();
216
+ const query = signWbi({ host_mid: uid, features: 'itemOpusStyle', ...DM_PARAMS }, key, Math.floor(this.#now() / 1000));
217
+ const data = await this.#request(`https://api.bilibili.com/x/polymer/web-dynamic/v1/feed/space?${query}`, {
218
+ referer: `https://space.bilibili.com/${uid}/dynamic`,
219
+ headers: { origin: 'https://space.bilibili.com' },
220
+ });
221
+ if (!isRecord(data) || !Array.isArray(data.items))
222
+ throw new BiliApiError('shape', '动态接口格式异常');
223
+ const items = [];
224
+ for (const raw of data.items) {
225
+ const item = parseDynamic(raw);
226
+ if (item)
227
+ items.push(item);
228
+ }
229
+ return items;
230
+ }
231
+ }
232
+ /** Turn one raw feed item into a compact, push-ready record (undefined = skip). */
233
+ export function parseDynamic(raw) {
234
+ if (!isRecord(raw))
235
+ return undefined;
236
+ const id = str(raw.id_str);
237
+ if (!id)
238
+ return undefined;
239
+ const type = str(raw.type);
240
+ // Live rooms are pushed by the live watcher, not as dynamics.
241
+ if (type === 'DYNAMIC_TYPE_LIVE_RCMD' || type === 'DYNAMIC_TYPE_LIVE')
242
+ return undefined;
243
+ const modules = isRecord(raw.modules) ? raw.modules : {};
244
+ const author = isRecord(modules.module_author) ? modules.module_author : {};
245
+ const dynamic = isRecord(modules.module_dynamic) ? modules.module_dynamic : {};
246
+ const tag = isRecord(modules.module_tag) ? modules.module_tag : {};
247
+ const major = isRecord(dynamic.major) ? dynamic.major : {};
248
+ const desc = isRecord(dynamic.desc) ? str(dynamic.desc.text) : '';
249
+ const archive = isRecord(major.archive) ? major.archive : undefined;
250
+ const opus = isRecord(major.opus) ? major.opus : undefined;
251
+ const article = isRecord(major.article) ? major.article : undefined;
252
+ const opusText = opus && isRecord(opus.summary) ? str(opus.summary.text) : '';
253
+ const opusTitle = opus ? str(opus.title) : '';
254
+ let summary;
255
+ let url = `https://t.bilibili.com/${id}`;
256
+ switch (type) {
257
+ case 'DYNAMIC_TYPE_AV':
258
+ summary = `投稿了视频:${archive ? str(archive.title) : desc}`;
259
+ if (archive && str(archive.jump_url))
260
+ url = absoluteUrl(str(archive.jump_url));
261
+ break;
262
+ case 'DYNAMIC_TYPE_ARTICLE':
263
+ summary = `发布了专栏:${opusTitle || (article ? str(article.title) : '') || clip(opusText)}`;
264
+ break;
265
+ case 'DYNAMIC_TYPE_FORWARD':
266
+ summary = `转发了动态:${clip(desc) || '(无附言)'}`;
267
+ break;
268
+ default:
269
+ summary = clip(opusText || desc || opusTitle) || '发布了新动态';
270
+ }
271
+ return {
272
+ id,
273
+ author: str(author.name),
274
+ pubTs: int(author.pub_ts),
275
+ type,
276
+ pinned: str(tag.text) === '置顶',
277
+ summary,
278
+ url,
279
+ };
280
+ }
281
+ /** Accepts `123`, `UID:123`, `https://space.bilibili.com/123/...`. */
282
+ export function parseUid(input) {
283
+ const text = input.trim();
284
+ const fromUrl = /space\.bilibili\.com\/(\d{1,20})/u.exec(text);
285
+ if (fromUrl)
286
+ return fromUrl[1];
287
+ const plain = /^(?:uid[::]?)?(\d{1,20})$/iu.exec(text);
288
+ return plain?.[1];
289
+ }
290
+ /** Accepts `21452505` or `https://live.bilibili.com/21452505`. */
291
+ export function parseRoomId(input) {
292
+ const text = input.trim();
293
+ const fromUrl = /live\.bilibili\.com\/(?:h5\/)?(\d{1,20})/u.exec(text);
294
+ if (fromUrl)
295
+ return fromUrl[1];
296
+ return /^\d{1,20}$/u.test(text) ? text : undefined;
297
+ }
298
+ //# sourceMappingURL=api.js.map
package/lib/api.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Minimal Bilibili web API client.\n *\n * Only public, key-less endpoints are used. Every response is validated\n * defensively: when Bilibili changes a payload we raise a `BiliApiError` with\n * kind `shape` instead of crashing the poller, and risk-control responses\n * (-352 / -412 / -799 / HTTP 412) are reported as kind `risk` so callers can\n * back off.\n */\nimport { mixinKey, signWbi, wbiKeyFromUrl } from './wbi.js';\n\nexport const DEFAULT_USER_AGENT =\n 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36';\n\nexport type BiliErrorKind = 'risk' | 'not_found' | 'http' | 'shape' | 'network';\n\nexport class BiliApiError extends Error {\n readonly kind: BiliErrorKind;\n readonly code: number | undefined;\n constructor(kind: BiliErrorKind, message: string, code?: number) {\n super(message);\n this.name = 'BiliApiError';\n this.kind = kind;\n this.code = code;\n }\n}\n\nconst RISK_CODES = new Set([-352, -412, -509, -799, -401]);\nconst NOT_FOUND_CODES = new Set([-404, -626, 4100000, 60004, 1]);\nconst WBI_TTL_MS = 6 * 60 * 60 * 1000;\n\n/**\n * Browser fingerprint hints the web client sends with the space feed. Without\n * them anonymous requests are answered with -352 far more often.\n */\nconst DM_PARAMS: Readonly<Record<string, string>> = Object.freeze({\n dm_img_list: '[]',\n dm_img_str: Buffer.from('WebGL 1.0 (OpenGL ES 2.0 Chromium)').toString('base64').replace(/=+$/u, ''),\n dm_cover_img_str: Buffer.from('ANGLE (Apple, ANGLE Metal Renderer: Apple M1, Unspecified Version)Google Inc. (Apple)')\n .toString('base64')\n .replace(/=+$/u, ''),\n});\n\nexport interface LiveInfo {\n readonly uid: string;\n readonly uname: string;\n readonly roomId: string;\n readonly title: string;\n /** 0 = offline, 1 = live, 2 = replay carousel. */\n readonly liveStatus: number;\n /** Unix seconds of the current live start, 0 when unknown. */\n readonly liveTime: number;\n readonly cover: string;\n readonly area: string;\n readonly url: string;\n}\n\nexport interface RoomInfo {\n readonly uid: string;\n readonly roomId: string;\n readonly title: string;\n readonly liveStatus: number;\n readonly area: string;\n readonly online: number;\n readonly url: string;\n}\n\nexport interface DynamicItem {\n readonly id: string;\n readonly author: string;\n /** Unix seconds. */\n readonly pubTs: number;\n readonly type: string;\n readonly pinned: boolean;\n readonly summary: string;\n readonly url: string;\n}\n\nexport interface UserCard {\n readonly uid: string;\n readonly name: string;\n}\n\nexport type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;\n\nexport interface BiliClientOptions {\n readonly timeout?: number;\n readonly cookie?: string;\n readonly fetch?: FetchLike;\n readonly now?: () => number;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value);\n}\n\nfunction str(value: unknown): string {\n return typeof value === 'string' ? value : typeof value === 'number' && Number.isFinite(value) ? String(value) : '';\n}\n\nfunction int(value: unknown): number {\n const parsed = typeof value === 'number' ? value : typeof value === 'string' ? Number(value) : Number.NaN;\n return Number.isFinite(parsed) ? Math.trunc(parsed) : 0;\n}\n\nfunction clip(text: string, max = 120): string {\n const flat = text.replace(/\\s+/gu, ' ').trim();\n return flat.length > max ? `${flat.slice(0, max)}…` : flat;\n}\n\nfunction absoluteUrl(url: string): string {\n if (url.startsWith('//')) return `https:${url}`;\n return url;\n}\n\nexport class BiliClient {\n readonly #timeout: number;\n readonly #userCookie: string;\n readonly #fetch: FetchLike;\n readonly #now: () => number;\n #buvid: string | undefined;\n #wbi: { key: string; at: number } | undefined;\n\n constructor(options: BiliClientOptions = {}) {\n this.#timeout = options.timeout ?? 10_000;\n this.#userCookie = options.cookie?.trim() ?? '';\n this.#fetch = options.fetch ?? ((input, init) => fetch(input, init));\n this.#now = options.now ?? Date.now;\n }\n\n /** Forget cached device id and WBI key, e.g. after a risk-control response. */\n resetSession(): void {\n this.#buvid = undefined;\n this.#wbi = undefined;\n }\n\n async #request(url: string, init: RequestInit & { cookie?: boolean; referer?: string; acceptCodes?: readonly number[] } = {}): Promise<unknown> {\n const headers: Record<string, string> = {\n 'user-agent': DEFAULT_USER_AGENT,\n accept: 'application/json, text/plain, */*',\n referer: init.referer ?? 'https://www.bilibili.com/',\n ...(init.headers as Record<string, string> | undefined),\n };\n if (init.cookie !== false) {\n const cookie = await this.#cookie();\n if (cookie) headers.cookie = cookie;\n }\n let response: Response;\n try {\n response = await this.#fetch(url, {\n method: init.method ?? 'GET',\n headers,\n ...(init.body === undefined ? {} : { body: init.body }),\n signal: AbortSignal.timeout(this.#timeout),\n });\n } catch (error) {\n const cause = error as Error;\n throw new BiliApiError('network', cause.name === 'TimeoutError' ? `请求超时(${this.#timeout}ms)` : `请求失败: ${cause.message}`);\n }\n if (response.status === 412) throw new BiliApiError('risk', 'B 站风控拦截(HTTP 412)', 412);\n if (!response.ok) throw new BiliApiError('http', `HTTP ${response.status}`, response.status);\n let body: unknown;\n try {\n body = await response.json();\n } catch {\n throw new BiliApiError('shape', '接口返回的不是 JSON');\n }\n if (!isRecord(body)) throw new BiliApiError('shape', '接口返回格式异常');\n const code = int(body.code);\n if (code !== 0 && !init.acceptCodes?.includes(code)) {\n const message = str(body.message) || str(body.msg) || `错误码 ${code}`;\n if (RISK_CODES.has(code)) {\n this.resetSession();\n throw new BiliApiError('risk', `B 站风控拦截(${code}: ${message})`, code);\n }\n if (NOT_FOUND_CODES.has(code)) throw new BiliApiError('not_found', message, code);\n throw new BiliApiError('http', message, code);\n }\n return body.data;\n }\n\n async #cookie(): Promise<string> {\n if (this.#userCookie) return this.#userCookie;\n if (this.#buvid !== undefined) return this.#buvid;\n try {\n const data = await this.#request('https://api.bilibili.com/x/frontend/finger/spi', { cookie: false });\n const b3 = isRecord(data) ? str(data.b_3) : '';\n const b4 = isRecord(data) ? str(data.b_4) : '';\n this.#buvid = b3 ? `buvid3=${b3}${b4 ? `; buvid4=${encodeURIComponent(b4)}` : ''}` : '';\n } catch {\n // Device id is best effort; most endpoints still answer without it.\n this.#buvid = '';\n }\n return this.#buvid;\n }\n\n async #wbiKey(): Promise<string> {\n if (this.#wbi && this.#now() - this.#wbi.at < WBI_TTL_MS) return this.#wbi.key;\n // `nav` answers -101 (not logged in) for guests but still carries wbi_img.\n const data = await this.#request('https://api.bilibili.com/x/web-interface/nav', { acceptCodes: [-101] });\n const img = isRecord(data) && isRecord(data.wbi_img) ? data.wbi_img : undefined;\n const imgUrl = img ? str(img.img_url) : '';\n const subUrl = img ? str(img.sub_url) : '';\n if (!imgUrl || !subUrl) throw new BiliApiError('shape', '无法获取 WBI 密钥');\n const key = mixinKey(wbiKeyFromUrl(imgUrl), wbiKeyFromUrl(subUrl));\n this.#wbi = { key, at: this.#now() };\n return key;\n }\n\n /** Batch live status by UP uid (no auth). */\n async getLiveStatus(uids: readonly string[]): Promise<Map<string, LiveInfo>> {\n const result = new Map<string, LiveInfo>();\n for (let index = 0; index < uids.length; index += 50) {\n const chunk = uids.slice(index, index + 50).map(Number);\n const data = await this.#request('https://api.live.bilibili.com/room/v1/Room/get_status_info_by_uids', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ uids: chunk }),\n referer: 'https://live.bilibili.com/',\n });\n // An empty result is serialised as [] instead of {}.\n if (Array.isArray(data)) continue;\n if (!isRecord(data)) throw new BiliApiError('shape', '开播状态接口格式异常');\n for (const [key, value] of Object.entries(data)) {\n if (!isRecord(value)) continue;\n const uid = str(value.uid) || key;\n const roomId = str(value.room_id);\n result.set(uid, {\n uid,\n uname: str(value.uname),\n roomId,\n title: str(value.title),\n liveStatus: int(value.live_status),\n liveTime: int(value.live_time),\n cover: str(value.cover_from_user) || str(value.keyframe),\n area: str(value.area_v2_name) || str(value.area_name),\n url: roomId ? `https://live.bilibili.com/${roomId}` : '',\n });\n }\n }\n return result;\n }\n\n async getRoomInfo(roomId: string): Promise<RoomInfo> {\n const data = await this.#request(\n `https://api.live.bilibili.com/room/v1/Room/get_info?room_id=${encodeURIComponent(roomId)}`,\n { referer: 'https://live.bilibili.com/' },\n );\n if (!isRecord(data) || !str(data.room_id)) throw new BiliApiError('shape', '直播间接口格式异常');\n const realId = str(data.room_id);\n return {\n uid: str(data.uid),\n roomId: realId,\n title: str(data.title),\n liveStatus: int(data.live_status),\n area: str(data.area_name),\n online: int(data.online),\n url: `https://live.bilibili.com/${realId}`,\n };\n }\n\n async getUserCard(uid: string): Promise<UserCard> {\n const data = await this.#request(`https://api.bilibili.com/x/web-interface/card?mid=${encodeURIComponent(uid)}`);\n const card = isRecord(data) && isRecord(data.card) ? data.card : undefined;\n if (!card) throw new BiliApiError('shape', '用户信息接口格式异常');\n return { uid: str(card.mid) || uid, name: str(card.name) };\n }\n\n /** Latest space dynamics of one UP (WBI signed). Newest first, as returned. */\n async getDynamics(uid: string): Promise<DynamicItem[]> {\n const key = await this.#wbiKey();\n const query = signWbi({ host_mid: uid, features: 'itemOpusStyle', ...DM_PARAMS }, key, Math.floor(this.#now() / 1000));\n const data = await this.#request(`https://api.bilibili.com/x/polymer/web-dynamic/v1/feed/space?${query}`, {\n referer: `https://space.bilibili.com/${uid}/dynamic`,\n headers: { origin: 'https://space.bilibili.com' },\n });\n if (!isRecord(data) || !Array.isArray(data.items)) throw new BiliApiError('shape', '动态接口格式异常');\n const items: DynamicItem[] = [];\n for (const raw of data.items) {\n const item = parseDynamic(raw);\n if (item) items.push(item);\n }\n return items;\n }\n}\n\n/** Turn one raw feed item into a compact, push-ready record (undefined = skip). */\nexport function parseDynamic(raw: unknown): DynamicItem | undefined {\n if (!isRecord(raw)) return undefined;\n const id = str(raw.id_str);\n if (!id) return undefined;\n const type = str(raw.type);\n // Live rooms are pushed by the live watcher, not as dynamics.\n if (type === 'DYNAMIC_TYPE_LIVE_RCMD' || type === 'DYNAMIC_TYPE_LIVE') return undefined;\n const modules = isRecord(raw.modules) ? raw.modules : {};\n const author = isRecord(modules.module_author) ? modules.module_author : {};\n const dynamic = isRecord(modules.module_dynamic) ? modules.module_dynamic : {};\n const tag = isRecord(modules.module_tag) ? modules.module_tag : {};\n const major = isRecord(dynamic.major) ? dynamic.major : {};\n const desc = isRecord(dynamic.desc) ? str(dynamic.desc.text) : '';\n const archive = isRecord(major.archive) ? major.archive : undefined;\n const opus = isRecord(major.opus) ? major.opus : undefined;\n const article = isRecord(major.article) ? major.article : undefined;\n const opusText = opus && isRecord(opus.summary) ? str(opus.summary.text) : '';\n const opusTitle = opus ? str(opus.title) : '';\n\n let summary: string;\n let url = `https://t.bilibili.com/${id}`;\n switch (type) {\n case 'DYNAMIC_TYPE_AV':\n summary = `投稿了视频:${archive ? str(archive.title) : desc}`;\n if (archive && str(archive.jump_url)) url = absoluteUrl(str(archive.jump_url));\n break;\n case 'DYNAMIC_TYPE_ARTICLE':\n summary = `发布了专栏:${opusTitle || (article ? str(article.title) : '') || clip(opusText)}`;\n break;\n case 'DYNAMIC_TYPE_FORWARD':\n summary = `转发了动态:${clip(desc) || '(无附言)'}`;\n break;\n default:\n summary = clip(opusText || desc || opusTitle) || '发布了新动态';\n }\n return {\n id,\n author: str(author.name),\n pubTs: int(author.pub_ts),\n type,\n pinned: str(tag.text) === '置顶',\n summary,\n url,\n };\n}\n\n/** Accepts `123`, `UID:123`, `https://space.bilibili.com/123/...`. */\nexport function parseUid(input: string): string | undefined {\n const text = input.trim();\n const fromUrl = /space\\.bilibili\\.com\\/(\\d{1,20})/u.exec(text);\n if (fromUrl) return fromUrl[1];\n const plain = /^(?:uid[::]?)?(\\d{1,20})$/iu.exec(text);\n return plain?.[1];\n}\n\n/** Accepts `21452505` or `https://live.bilibili.com/21452505`. */\nexport function parseRoomId(input: string): string | undefined {\n const text = input.trim();\n const fromUrl = /live\\.bilibili\\.com\\/(?:h5\\/)?(\\d{1,20})/u.exec(text);\n if (fromUrl) return fromUrl[1];\n return /^\\d{1,20}$/u.test(text) ? text : undefined;\n}\n"]}
@@ -0,0 +1,15 @@
1
+ /** A subscription destination uses public Message fields, never CapabilityId internals. */
2
+ export interface ChannelInfo {
3
+ adapterName: string;
4
+ endpointKey: string;
5
+ channelType: 'private' | 'group' | 'channel';
6
+ channelId: string;
7
+ senderId: string;
8
+ }
9
+ export declare const CHANNEL_REQUIRED_MESSAGE = "\u8BF7\u5728\u673A\u5668\u4EBA\u79C1\u804A\u3001\u7FA4\u804A\u6216\u9891\u9053\u5185\u6267\u884C\u6B64\u547D\u4EE4\uFF1B\u5F53\u524D\u8C03\u7528\u6CA1\u6709\u6709\u6548\u7684\u8BA2\u9605\u4F1A\u8BDD";
10
+ export declare const UNSUPPORTED_ADDRESS_MESSAGE = "\u6682\u4E0D\u652F\u6301\u7EBF\u7A0B\uFF0F\u7236\u9891\u9053\u5BFB\u5740\u7684\u8BA2\u9605\uFF0C\u8BF7\u5728\u65E0\u9700\u7EBF\u7A0B\u6216\u7236\u9891\u9053\u4FE1\u606F\u7684\u4F1A\u8BDD\u5185\u6267\u884C";
11
+ export declare function channelSourceError(input: unknown): string;
12
+ /** No IM source means no destination; callers must not invent a global subscription. */
13
+ export declare function extractChannelInfo(input: unknown): ChannelInfo | null;
14
+ /** Full subscription scope, including the bot endpoint for multi-account adapters. */
15
+ export declare function subscriptionScope(channel: ChannelInfo): Record<string, string>;
package/lib/channel.js ADDED
@@ -0,0 +1,47 @@
1
+ /** A subscription destination uses public Message fields, never CapabilityId internals. */
2
+ export const CHANNEL_REQUIRED_MESSAGE = '请在机器人私聊、群聊或频道内执行此命令;当前调用没有有效的订阅会话';
3
+ export const UNSUPPORTED_ADDRESS_MESSAGE = '暂不支持线程/父频道寻址的订阅,请在无需线程或父频道信息的会话内执行';
4
+ function isRecord(value) {
5
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
6
+ }
7
+ function isIdentifier(value) {
8
+ return typeof value === 'string' && value.trim().length > 0;
9
+ }
10
+ function hasUnsupportedAddress(input) {
11
+ return isRecord(input) && isRecord(input.conversation)
12
+ && (input.conversation.parent !== undefined || input.conversation.threadId !== undefined);
13
+ }
14
+ export function channelSourceError(input) {
15
+ return hasUnsupportedAddress(input) ? UNSUPPORTED_ADDRESS_MESSAGE : CHANNEL_REQUIRED_MESSAGE;
16
+ }
17
+ /** No IM source means no destination; callers must not invent a global subscription. */
18
+ export function extractChannelInfo(input) {
19
+ if (!isRecord(input) || !isRecord(input.conversation))
20
+ return null;
21
+ if (hasUnsupportedAddress(input))
22
+ return null;
23
+ const conversation = input.conversation;
24
+ const kind = conversation.kind;
25
+ if (kind !== 'private' && kind !== 'group' && kind !== 'channel')
26
+ return null;
27
+ if (!isIdentifier(input.clientAdapter) || !isIdentifier(input.endpointId) || !isIdentifier(conversation.id))
28
+ return null;
29
+ const sender = input.sender;
30
+ return {
31
+ adapterName: input.clientAdapter,
32
+ endpointKey: input.endpointId,
33
+ channelType: kind,
34
+ channelId: conversation.id,
35
+ senderId: isRecord(sender) && isIdentifier(sender.id) ? sender.id : '',
36
+ };
37
+ }
38
+ /** Full subscription scope, including the bot endpoint for multi-account adapters. */
39
+ export function subscriptionScope(channel) {
40
+ return {
41
+ adapter_name: channel.adapterName,
42
+ endpoint_id: channel.endpointKey,
43
+ channel_type: channel.channelType,
44
+ channel_id: channel.channelId,
45
+ };
46
+ }
47
+ //# sourceMappingURL=channel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"channel.js","sourceRoot":"","sources":["../src/channel.ts"],"names":[],"mappings":"AAAA,2FAA2F;AAU3F,MAAM,CAAC,MAAM,wBAAwB,GAAG,mCAAmC,CAAC;AAC5E,MAAM,CAAC,MAAM,2BAA2B,GAAG,oCAAoC,CAAC;AAEhF,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC;AAED,SAAS,YAAY,CAAC,KAAc;IAClC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC;AAC9D,CAAC;AAED,SAAS,qBAAqB,CAAC,KAAc;IAC3C,OAAO,QAAQ,CAAC,KAAK,CAAC,IAAI,QAAQ,CAAC,KAAK,CAAC,YAAY,CAAC;WACjD,CAAC,KAAK,CAAC,YAAY,CAAC,MAAM,KAAK,SAAS,IAAI,KAAK,CAAC,YAAY,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC;AAC9F,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,KAAc;IAC/C,OAAO,qBAAqB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,2BAA2B,CAAC,CAAC,CAAC,wBAAwB,CAAC;AAC/F,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,kBAAkB,CAAC,KAAc;IAC/C,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,YAAY,CAAC;QAAE,OAAO,IAAI,CAAC;IACnE,IAAI,qBAAqB,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAC9C,MAAM,YAAY,GAAG,KAAK,CAAC,YAAY,CAAC;IACxC,MAAM,IAAI,GAAG,YAAY,CAAC,IAAI,CAAC;IAC/B,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,OAAO,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IAC9E,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,aAAa,CAAC,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,YAAY,CAAC,YAAY,CAAC,EAAE,CAAC;QAAE,OAAO,IAAI,CAAC;IACzH,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC;IAC5B,OAAO;QACL,WAAW,EAAE,KAAK,CAAC,aAAa;QAChC,WAAW,EAAE,KAAK,CAAC,UAAU;QAC7B,WAAW,EAAE,IAAI;QACjB,SAAS,EAAE,YAAY,CAAC,EAAE;QAC1B,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,IAAI,YAAY,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE;KACvE,CAAC;AACJ,CAAC;AAED,sFAAsF;AACtF,MAAM,UAAU,iBAAiB,CAAC,OAAoB;IACpD,OAAO;QACL,YAAY,EAAE,OAAO,CAAC,WAAW;QACjC,WAAW,EAAE,OAAO,CAAC,WAAW;QAChC,YAAY,EAAE,OAAO,CAAC,WAAW;QACjC,UAAU,EAAE,OAAO,CAAC,SAAS;KAC9B,CAAC;AACJ,CAAC","sourcesContent":["/** A subscription destination uses public Message fields, never CapabilityId internals. */\n\nexport interface ChannelInfo {\n adapterName: string;\n endpointKey: string;\n channelType: 'private' | 'group' | 'channel';\n channelId: string;\n senderId: string;\n}\n\nexport const CHANNEL_REQUIRED_MESSAGE = '请在机器人私聊、群聊或频道内执行此命令;当前调用没有有效的订阅会话';\nexport const UNSUPPORTED_ADDRESS_MESSAGE = '暂不支持线程/父频道寻址的订阅,请在无需线程或父频道信息的会话内执行';\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value);\n}\n\nfunction isIdentifier(value: unknown): value is string {\n return typeof value === 'string' && value.trim().length > 0;\n}\n\nfunction hasUnsupportedAddress(input: unknown): boolean {\n return isRecord(input) && isRecord(input.conversation)\n && (input.conversation.parent !== undefined || input.conversation.threadId !== undefined);\n}\n\nexport function channelSourceError(input: unknown): string {\n return hasUnsupportedAddress(input) ? UNSUPPORTED_ADDRESS_MESSAGE : CHANNEL_REQUIRED_MESSAGE;\n}\n\n/** No IM source means no destination; callers must not invent a global subscription. */\nexport function extractChannelInfo(input: unknown): ChannelInfo | null {\n if (!isRecord(input) || !isRecord(input.conversation)) return null;\n if (hasUnsupportedAddress(input)) return null;\n const conversation = input.conversation;\n const kind = conversation.kind;\n if (kind !== 'private' && kind !== 'group' && kind !== 'channel') return null;\n if (!isIdentifier(input.clientAdapter) || !isIdentifier(input.endpointId) || !isIdentifier(conversation.id)) return null;\n const sender = input.sender;\n return {\n adapterName: input.clientAdapter,\n endpointKey: input.endpointId,\n channelType: kind,\n channelId: conversation.id,\n senderId: isRecord(sender) && isIdentifier(sender.id) ? sender.id : '',\n };\n}\n\n/** Full subscription scope, including the bot endpoint for multi-account adapters. */\nexport function subscriptionScope(channel: ChannelInfo): Record<string, string> {\n return {\n adapter_name: channel.adapterName,\n endpoint_id: channel.endpointKey,\n channel_type: channel.channelType,\n channel_id: channel.channelId,\n };\n}\n"]}
@@ -0,0 +1,15 @@
1
+ /** Plugin configuration; defaults mirror schema.json. */
2
+ export interface BilibiliConfig {
3
+ readonly pollCron?: string;
4
+ readonly jitterMs?: number;
5
+ readonly requestGapMs?: number;
6
+ readonly maxBackoffMinutes?: number;
7
+ readonly maxPerChannel?: number;
8
+ readonly timeout?: number;
9
+ readonly live?: boolean;
10
+ readonly dynamic?: boolean;
11
+ readonly cookie?: string;
12
+ }
13
+ export type ResolvedBilibiliConfig = Required<BilibiliConfig>;
14
+ export declare const DEFAULT_BILIBILI_CONFIG: ResolvedBilibiliConfig;
15
+ export declare function resolveBilibiliConfig(raw: unknown): ResolvedBilibiliConfig;
package/lib/config.js ADDED
@@ -0,0 +1,35 @@
1
+ export const DEFAULT_BILIBILI_CONFIG = Object.freeze({
2
+ pollCron: '0 * * * * *',
3
+ jitterMs: 15_000,
4
+ requestGapMs: 1_500,
5
+ maxBackoffMinutes: 30,
6
+ maxPerChannel: 20,
7
+ timeout: 10_000,
8
+ live: true,
9
+ dynamic: true,
10
+ cookie: '',
11
+ });
12
+ function isRecord(value) {
13
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
14
+ }
15
+ function num(value, fallback, min, max) {
16
+ if (typeof value !== 'number' || !Number.isFinite(value))
17
+ return fallback;
18
+ return Math.min(max, Math.max(min, value));
19
+ }
20
+ export function resolveBilibiliConfig(raw) {
21
+ const input = isRecord(raw) ? raw : {};
22
+ const d = DEFAULT_BILIBILI_CONFIG;
23
+ return {
24
+ pollCron: typeof input.pollCron === 'string' && input.pollCron.trim() ? input.pollCron.trim() : d.pollCron,
25
+ jitterMs: num(input.jitterMs, d.jitterMs, 0, 120_000),
26
+ requestGapMs: num(input.requestGapMs, d.requestGapMs, 0, 30_000),
27
+ maxBackoffMinutes: num(input.maxBackoffMinutes, d.maxBackoffMinutes, 1, 720),
28
+ maxPerChannel: Math.floor(num(input.maxPerChannel, d.maxPerChannel, 1, 200)),
29
+ timeout: num(input.timeout, d.timeout, 1_000, 60_000),
30
+ live: typeof input.live === 'boolean' ? input.live : d.live,
31
+ dynamic: typeof input.dynamic === 'boolean' ? input.dynamic : d.dynamic,
32
+ cookie: typeof input.cookie === 'string' ? input.cookie.trim() : d.cookie,
33
+ };
34
+ }
35
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAeA,MAAM,CAAC,MAAM,uBAAuB,GAA2B,MAAM,CAAC,MAAM,CAAC;IAC3E,QAAQ,EAAE,aAAa;IACvB,QAAQ,EAAE,MAAM;IAChB,YAAY,EAAE,KAAK;IACnB,iBAAiB,EAAE,EAAE;IACrB,aAAa,EAAE,EAAE;IACjB,OAAO,EAAE,MAAM;IACf,IAAI,EAAE,IAAI;IACV,OAAO,EAAE,IAAI;IACb,MAAM,EAAE,EAAE;CACX,CAAC,CAAC;AAEH,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC;AAED,SAAS,GAAG,CAAC,KAAc,EAAE,QAAgB,EAAE,GAAW,EAAE,GAAW;IACrE,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,QAAQ,CAAC;IAC1E,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC;AAC7C,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,GAAY;IAChD,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;IACvC,MAAM,CAAC,GAAG,uBAAuB,CAAC;IAClC,OAAO;QACL,QAAQ,EAAE,OAAO,KAAK,CAAC,QAAQ,KAAK,QAAQ,IAAI,KAAK,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ;QAC1G,QAAQ,EAAE,GAAG,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,EAAE,OAAO,CAAC;QACrD,YAAY,EAAE,GAAG,CAAC,KAAK,CAAC,YAAY,EAAE,CAAC,CAAC,YAAY,EAAE,CAAC,EAAE,MAAM,CAAC;QAChE,iBAAiB,EAAE,GAAG,CAAC,KAAK,CAAC,iBAAiB,EAAE,CAAC,CAAC,iBAAiB,EAAE,CAAC,EAAE,GAAG,CAAC;QAC5E,aAAa,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,aAAa,EAAE,CAAC,CAAC,aAAa,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC;QAC5E,OAAO,EAAE,GAAG,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC;QACrD,IAAI,EAAE,OAAO,KAAK,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI;QAC3D,OAAO,EAAE,OAAO,KAAK,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO;QACvE,MAAM,EAAE,OAAO,KAAK,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM;KAC1E,CAAC;AACJ,CAAC","sourcesContent":["/** Plugin configuration; defaults mirror schema.json. */\nexport interface BilibiliConfig {\n readonly pollCron?: string;\n readonly jitterMs?: number;\n readonly requestGapMs?: number;\n readonly maxBackoffMinutes?: number;\n readonly maxPerChannel?: number;\n readonly timeout?: number;\n readonly live?: boolean;\n readonly dynamic?: boolean;\n readonly cookie?: string;\n}\n\nexport type ResolvedBilibiliConfig = Required<BilibiliConfig>;\n\nexport const DEFAULT_BILIBILI_CONFIG: ResolvedBilibiliConfig = Object.freeze({\n pollCron: '0 * * * * *',\n jitterMs: 15_000,\n requestGapMs: 1_500,\n maxBackoffMinutes: 30,\n maxPerChannel: 20,\n timeout: 10_000,\n live: true,\n dynamic: true,\n cookie: '',\n});\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value);\n}\n\nfunction num(value: unknown, fallback: number, min: number, max: number): number {\n if (typeof value !== 'number' || !Number.isFinite(value)) return fallback;\n return Math.min(max, Math.max(min, value));\n}\n\nexport function resolveBilibiliConfig(raw: unknown): ResolvedBilibiliConfig {\n const input = isRecord(raw) ? raw : {};\n const d = DEFAULT_BILIBILI_CONFIG;\n return {\n pollCron: typeof input.pollCron === 'string' && input.pollCron.trim() ? input.pollCron.trim() : d.pollCron,\n jitterMs: num(input.jitterMs, d.jitterMs, 0, 120_000),\n requestGapMs: num(input.requestGapMs, d.requestGapMs, 0, 30_000),\n maxBackoffMinutes: num(input.maxBackoffMinutes, d.maxBackoffMinutes, 1, 720),\n maxPerChannel: Math.floor(num(input.maxPerChannel, d.maxPerChannel, 1, 200)),\n timeout: num(input.timeout, d.timeout, 1_000, 60_000),\n live: typeof input.live === 'boolean' ? input.live : d.live,\n dynamic: typeof input.dynamic === 'boolean' ? input.dynamic : d.dynamic,\n cookie: typeof input.cookie === 'string' ? input.cookie.trim() : d.cookie,\n };\n}\n"]}
@@ -0,0 +1,5 @@
1
+ import type { DynamicItem, LiveInfo, RoomInfo } from './api.js';
2
+ export declare function liveStatusText(status: number): string;
3
+ export declare function formatLiveStart(info: LiveInfo): string;
4
+ export declare function formatDynamic(uname: string, item: DynamicItem): string;
5
+ export declare function formatRoom(room: RoomInfo, uname: string, liveTime: number, nowMs?: number): string;
package/lib/format.js ADDED
@@ -0,0 +1,42 @@
1
+ const LIVE_STATUS_TEXT = { 0: '未开播', 1: '直播中', 2: '轮播中' };
2
+ export function liveStatusText(status) {
3
+ return LIVE_STATUS_TEXT[status] ?? `未知状态 (${status})`;
4
+ }
5
+ function since(liveTime, nowMs) {
6
+ if (liveTime <= 0)
7
+ return '';
8
+ const minutes = Math.max(0, Math.floor((nowMs / 1000 - liveTime) / 60));
9
+ if (minutes < 60)
10
+ return `已开播 ${minutes} 分钟`;
11
+ return `已开播 ${Math.floor(minutes / 60)} 小时 ${minutes % 60} 分钟`;
12
+ }
13
+ export function formatLiveStart(info) {
14
+ const lines = [`【开播】${info.uname || info.uid} 开播啦!`];
15
+ if (info.title)
16
+ lines.push(`标题:${info.title}`);
17
+ if (info.area)
18
+ lines.push(`分区:${info.area}`);
19
+ if (info.url)
20
+ lines.push(info.url);
21
+ return lines.join('\n');
22
+ }
23
+ export function formatDynamic(uname, item) {
24
+ return `【动态】${item.author || uname} ${item.summary}\n${item.url}`;
25
+ }
26
+ export function formatRoom(room, uname, liveTime, nowMs = Date.now()) {
27
+ const lines = [`${uname ? `${uname} 的直播间` : '直播间'} ${room.roomId}:${liveStatusText(room.liveStatus)}`];
28
+ if (room.title)
29
+ lines.push(`标题:${room.title}`);
30
+ if (room.area)
31
+ lines.push(`分区:${room.area}`);
32
+ if (room.liveStatus === 1) {
33
+ const elapsed = since(liveTime, nowMs);
34
+ if (elapsed)
35
+ lines.push(elapsed);
36
+ if (room.online > 0)
37
+ lines.push(`人气:${room.online}`);
38
+ }
39
+ lines.push(room.url);
40
+ return lines.join('\n');
41
+ }
42
+ //# sourceMappingURL=format.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format.js","sourceRoot":"","sources":["../src/format.ts"],"names":[],"mappings":"AAEA,MAAM,gBAAgB,GAA2B,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC;AAElF,MAAM,UAAU,cAAc,CAAC,MAAc;IAC3C,OAAO,gBAAgB,CAAC,MAAM,CAAC,IAAI,SAAS,MAAM,GAAG,CAAC;AACxD,CAAC;AAED,SAAS,KAAK,CAAC,QAAgB,EAAE,KAAa;IAC5C,IAAI,QAAQ,IAAI,CAAC;QAAE,OAAO,EAAE,CAAC;IAC7B,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,GAAG,IAAI,GAAG,QAAQ,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;IACxE,IAAI,OAAO,GAAG,EAAE;QAAE,OAAO,OAAO,OAAO,KAAK,CAAC;IAC7C,OAAO,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,EAAE,CAAC,OAAO,OAAO,GAAG,EAAE,KAAK,CAAC;AACjE,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,IAAc;IAC5C,MAAM,KAAK,GAAG,CAAC,OAAO,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,GAAG,OAAO,CAAC,CAAC;IACrD,IAAI,IAAI,CAAC,KAAK;QAAE,KAAK,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;IAC/C,IAAI,IAAI,CAAC,IAAI;QAAE,KAAK,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;IAC7C,IAAI,IAAI,CAAC,GAAG;QAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACnC,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,KAAa,EAAE,IAAiB;IAC5D,OAAO,OAAO,IAAI,CAAC,MAAM,IAAI,KAAK,IAAI,IAAI,CAAC,OAAO,KAAK,IAAI,CAAC,GAAG,EAAE,CAAC;AACpE,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,IAAc,EAAE,KAAa,EAAE,QAAgB,EAAE,QAAgB,IAAI,CAAC,GAAG,EAAE;IACpG,MAAM,KAAK,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,GAAG,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,MAAM,IAAI,cAAc,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC;IACvG,IAAI,IAAI,CAAC,KAAK;QAAE,KAAK,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;IAC/C,IAAI,IAAI,CAAC,IAAI;QAAE,KAAK,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;IAC7C,IAAI,IAAI,CAAC,UAAU,KAAK,CAAC,EAAE,CAAC;QAC1B,MAAM,OAAO,GAAG,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;QACvC,IAAI,OAAO;YAAE,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACjC,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC;YAAE,KAAK,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;IACvD,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACrB,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC","sourcesContent":["import type { DynamicItem, LiveInfo, RoomInfo } from './api.js';\n\nconst LIVE_STATUS_TEXT: Record<number, string> = { 0: '未开播', 1: '直播中', 2: '轮播中' };\n\nexport function liveStatusText(status: number): string {\n return LIVE_STATUS_TEXT[status] ?? `未知状态 (${status})`;\n}\n\nfunction since(liveTime: number, nowMs: number): string {\n if (liveTime <= 0) return '';\n const minutes = Math.max(0, Math.floor((nowMs / 1000 - liveTime) / 60));\n if (minutes < 60) return `已开播 ${minutes} 分钟`;\n return `已开播 ${Math.floor(minutes / 60)} 小时 ${minutes % 60} 分钟`;\n}\n\nexport function formatLiveStart(info: LiveInfo): string {\n const lines = [`【开播】${info.uname || info.uid} 开播啦!`];\n if (info.title) lines.push(`标题:${info.title}`);\n if (info.area) lines.push(`分区:${info.area}`);\n if (info.url) lines.push(info.url);\n return lines.join('\\n');\n}\n\nexport function formatDynamic(uname: string, item: DynamicItem): string {\n return `【动态】${item.author || uname} ${item.summary}\\n${item.url}`;\n}\n\nexport function formatRoom(room: RoomInfo, uname: string, liveTime: number, nowMs: number = Date.now()): string {\n const lines = [`${uname ? `${uname} 的直播间` : '直播间'} ${room.roomId}:${liveStatusText(room.liveStatus)}`];\n if (room.title) lines.push(`标题:${room.title}`);\n if (room.area) lines.push(`分区:${room.area}`);\n if (room.liveStatus === 1) {\n const elapsed = since(liveTime, nowMs);\n if (elapsed) lines.push(elapsed);\n if (room.online > 0) lines.push(`人气:${room.online}`);\n }\n lines.push(room.url);\n return lines.join('\\n');\n}\n"]}
package/lib/index.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ export { BiliApiError, BiliClient, parseDynamic, parseRoomId, parseUid } from './api.js';
2
+ export type { BiliClientOptions, DynamicItem, FetchLike, LiveInfo, RoomInfo, UserCard } from './api.js';
3
+ export { DEFAULT_BILIBILI_CONFIG, resolveBilibiliConfig } from './config.js';
4
+ export type { BilibiliConfig, ResolvedBilibiliConfig } from './config.js';
5
+ export { mixinKey, signWbi, wbiKeyFromUrl } from './wbi.js';
6
+ export { BILI_STATE_TABLE, BILI_SUBS_TABLE, createInMemoryBiliDb, defineBiliTables, getBiliState, getBiliSubs, } from './store.js';
7
+ export { BilibiliWatcher, selectFreshDynamics, shouldPushLive } from './poll.js';
8
+ export type { TickResult } from './poll.js';
9
+ export { bilibiliRuntimeToken } from './runtime.js';
10
+ export type { BilibiliRuntime, BiliOutboundPush } from './runtime.js';
package/lib/index.js ADDED
@@ -0,0 +1,7 @@
1
+ export { BiliApiError, BiliClient, parseDynamic, parseRoomId, parseUid } from './api.js';
2
+ export { DEFAULT_BILIBILI_CONFIG, resolveBilibiliConfig } from './config.js';
3
+ export { mixinKey, signWbi, wbiKeyFromUrl } from './wbi.js';
4
+ export { BILI_STATE_TABLE, BILI_SUBS_TABLE, createInMemoryBiliDb, defineBiliTables, getBiliState, getBiliSubs, } from './store.js';
5
+ export { BilibiliWatcher, selectFreshDynamics, shouldPushLive } from './poll.js';
6
+ export { bilibiliRuntimeToken } from './runtime.js';
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAEzF,OAAO,EAAE,uBAAuB,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAE7E,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAC5D,OAAO,EACL,gBAAgB,EAChB,eAAe,EACf,oBAAoB,EACpB,gBAAgB,EAChB,YAAY,EACZ,WAAW,GACZ,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,eAAe,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEjF,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC","sourcesContent":["export { BiliApiError, BiliClient, parseDynamic, parseRoomId, parseUid } from './api.js';\nexport type { BiliClientOptions, DynamicItem, FetchLike, LiveInfo, RoomInfo, UserCard } from './api.js';\nexport { DEFAULT_BILIBILI_CONFIG, resolveBilibiliConfig } from './config.js';\nexport type { BilibiliConfig, ResolvedBilibiliConfig } from './config.js';\nexport { mixinKey, signWbi, wbiKeyFromUrl } from './wbi.js';\nexport {\n BILI_STATE_TABLE,\n BILI_SUBS_TABLE,\n createInMemoryBiliDb,\n defineBiliTables,\n getBiliState,\n getBiliSubs,\n} from './store.js';\nexport { BilibiliWatcher, selectFreshDynamics, shouldPushLive } from './poll.js';\nexport type { TickResult } from './poll.js';\nexport { bilibiliRuntimeToken } from './runtime.js';\nexport type { BilibiliRuntime, BiliOutboundPush } from './runtime.js';\n"]}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Who may change a conversation's Bilibili subscriptions.
3
+ *
4
+ * - Private chat: the user manages their own conversation, no restriction.
5
+ * - Group or channel: only the group owner, group admins or the bot master.
6
+ *
7
+ * The rule itself lives in `canManageConversation` (`zhin.js/command`); adapters project
8
+ * the canonical `owner` / `admin` role names that it checks.
9
+ */
10
+ export declare const SUBSCRIPTION_PERMISSION_DENIED_MESSAGE = "\u53EA\u6709\u7FA4\u4E3B\u3001\u7FA4\u7BA1\u7406\u5458\u6216\u673A\u5668\u4EBA\u4E3B\u4EBA\u53EF\u4EE5\u5728\u7FA4\u804A\u4E2D\u4FEE\u6539 B \u7AD9\u8BA2\u9605\uFF1B\u79C1\u804A\u4E2D\u53EF\u4EE5\u76F4\u63A5\u8BA2\u9605\u672C\u4F1A\u8BDD\u3002";
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Who may change a conversation's Bilibili subscriptions.
3
+ *
4
+ * - Private chat: the user manages their own conversation, no restriction.
5
+ * - Group or channel: only the group owner, group admins or the bot master.
6
+ *
7
+ * The rule itself lives in `canManageConversation` (`zhin.js/command`); adapters project
8
+ * the canonical `owner` / `admin` role names that it checks.
9
+ */
10
+ export const SUBSCRIPTION_PERMISSION_DENIED_MESSAGE = '只有群主、群管理员或机器人主人可以在群聊中修改 B 站订阅;私聊中可以直接订阅本会话。';
11
+ //# sourceMappingURL=permission.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permission.js","sourceRoot":"","sources":["../src/permission.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,sCAAsC,GACjD,6CAA6C,CAAC","sourcesContent":["/**\n * Who may change a conversation's Bilibili subscriptions.\n *\n * - Private chat: the user manages their own conversation, no restriction.\n * - Group or channel: only the group owner, group admins or the bot master.\n *\n * The rule itself lives in `canManageConversation` (`zhin.js/command`); adapters project\n * the canonical `owner` / `admin` role names that it checks.\n */\nexport const SUBSCRIPTION_PERMISSION_DENIED_MESSAGE =\n '只有群主、群管理员或机器人主人可以在群聊中修改 B 站订阅;私聊中可以直接订阅本会话。';\n"]}