@bilibili-notify/live 0.0.1-alpha.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/LICENSE +21 -0
- package/lib/index.cjs +1809 -0
- package/lib/index.d.cts +1002 -0
- package/lib/index.d.mts +1002 -0
- package/lib/index.mjs +1764 -0
- package/package.json +50 -0
package/lib/index.d.cts
ADDED
|
@@ -0,0 +1,1002 @@
|
|
|
1
|
+
import { CommentaryCallOverride, CommentaryGenerator } from "@bilibili-notify/ai";
|
|
2
|
+
import { BilibiliAPI, LiveRoomInfo } from "@bilibili-notify/api";
|
|
3
|
+
import { ImageRenderer } from "@bilibili-notify/image";
|
|
4
|
+
import { Disposable, Logger, ServiceContext } from "@bilibili-notify/internal";
|
|
5
|
+
import { MessageListener, MsgHandler } from "blive-message-listener";
|
|
6
|
+
import protobuf from "protobufjs";
|
|
7
|
+
|
|
8
|
+
//#region src/content-builder.d.ts
|
|
9
|
+
/**
|
|
10
|
+
* Platform-neutral content-builder injection point.
|
|
11
|
+
*
|
|
12
|
+
* The original koishi `BilibiliNotifyLive` constructed messages with
|
|
13
|
+
* `h("message", [...])` / `h.image(buffer, mime)` / `h.text(...)` / `h.image(url)`
|
|
14
|
+
* and `h.at("all")` and shipped them straight to `BilibiliPush.broadcastToTargets`.
|
|
15
|
+
*
|
|
16
|
+
* To stay decoupled from `koishi`'s `h(...)` factory, live-engine builds these
|
|
17
|
+
* fragments through a `LiveContentBuilder` provided by the adapter. The koishi
|
|
18
|
+
* shell wires the builder to the real `h` exports; the standalone runtime maps
|
|
19
|
+
* each call onto its own `NotificationPayload` shape.
|
|
20
|
+
*
|
|
21
|
+
* Each method returns an opaque `unknown` — the engine never inspects the
|
|
22
|
+
* result; it only passes the value back to the adapter via `PushLike`.
|
|
23
|
+
*/
|
|
24
|
+
interface LiveContentBuilder {
|
|
25
|
+
/** Wrap a plain text run. Equivalent to `h.text(text)`. */
|
|
26
|
+
text(text: string): unknown;
|
|
27
|
+
/**
|
|
28
|
+
* Wrap a remote image URL or in-memory buffer.
|
|
29
|
+
* `mime` is provided when `source` is a `Buffer`.
|
|
30
|
+
*/
|
|
31
|
+
image(source: string | Buffer, mime?: string): unknown;
|
|
32
|
+
/** Equivalent to `h.at("all")` (i.e. mention everyone in a group / channel). */
|
|
33
|
+
atAll(): unknown;
|
|
34
|
+
/**
|
|
35
|
+
* Compose a message with an array of segments (text / image / atAll fragments
|
|
36
|
+
* created by this same builder, plus `null` / `undefined` placeholders that
|
|
37
|
+
* are dropped). Equivalent to `h("message", segments)`.
|
|
38
|
+
*/
|
|
39
|
+
message(segments: Array<unknown>): unknown;
|
|
40
|
+
}
|
|
41
|
+
//#endregion
|
|
42
|
+
//#region src/danmaku-collector.d.ts
|
|
43
|
+
/**
|
|
44
|
+
* Per-room danmaku buffer powering the wordcloud + live-summary post-processing.
|
|
45
|
+
*
|
|
46
|
+
* - `recordDanmaku(roomId, content, username)` segments the danmaku via jieba
|
|
47
|
+
* and updates both word-frequency and per-user count maps for that room.
|
|
48
|
+
* - `snapshot(roomId)` returns the sorted word list + raw sender map for
|
|
49
|
+
* passing to {@link WordcloudGenerator} / {@link LiveSummaryRequester}.
|
|
50
|
+
* - `clear(roomId)` is invoked at live-end after the wordcloud + summary have
|
|
51
|
+
* been dispatched (or the start of a new live session for that room).
|
|
52
|
+
*
|
|
53
|
+
* The collector intentionally does NOT decide whether collection is enabled —
|
|
54
|
+
* the listener-manager checks the wordcloud / liveSummary master+target gates
|
|
55
|
+
* before calling `recordDanmaku`. This keeps the collector zero-config.
|
|
56
|
+
*/
|
|
57
|
+
declare class DanmakuCollector {
|
|
58
|
+
/** roomId → { word: count } */
|
|
59
|
+
private readonly weightByRoom;
|
|
60
|
+
/** roomId → { username: count } */
|
|
61
|
+
private readonly senderByRoom;
|
|
62
|
+
private readonly stopwords;
|
|
63
|
+
constructor(stopwords: Iterable<string>);
|
|
64
|
+
/** Replace the active stop-word set (called on config update). */
|
|
65
|
+
setStopwords(stopwords: Iterable<string>): void;
|
|
66
|
+
/** Make sure a room is being tracked (called when listener starts). */
|
|
67
|
+
registerRoom(roomId: string): void;
|
|
68
|
+
/**
|
|
69
|
+
* Tokenise an incoming danmaku and update word-frequency + per-user count.
|
|
70
|
+
* Words shorter than 2 characters or in the stop-word set are dropped.
|
|
71
|
+
*/
|
|
72
|
+
recordDanmaku(roomId: string, content: string, username: string): void;
|
|
73
|
+
/**
|
|
74
|
+
* Read a sorted snapshot of the current buffer for a room.
|
|
75
|
+
*
|
|
76
|
+
* - `sortedWords`: descending by frequency.
|
|
77
|
+
* - `senderRecord`: raw username → count map (consumer decides ordering).
|
|
78
|
+
* - `senderCount`: number of distinct usernames.
|
|
79
|
+
* - `danmakuCount`: total danmaku recorded.
|
|
80
|
+
*/
|
|
81
|
+
snapshot(roomId: string): {
|
|
82
|
+
sortedWords: Array<[string, number]>;
|
|
83
|
+
senderRecord: Record<string, number>;
|
|
84
|
+
senderCount: number;
|
|
85
|
+
danmakuCount: number;
|
|
86
|
+
};
|
|
87
|
+
/** Drop all collected data for a room (called at live-end / room-stop). */
|
|
88
|
+
clear(roomId: string): void;
|
|
89
|
+
/** Drop everything (called on engine stop / auth-lost). */
|
|
90
|
+
clearAll(): void;
|
|
91
|
+
}
|
|
92
|
+
//#endregion
|
|
93
|
+
//#region src/push-like.d.ts
|
|
94
|
+
/** Push category enum — numeric values are the historical bilibili-notify push-type codes. */
|
|
95
|
+
declare enum LivePushType {
|
|
96
|
+
Live = 0,
|
|
97
|
+
StartBroadcasting = 3,
|
|
98
|
+
LiveGuardBuy = 4,
|
|
99
|
+
/** 历史上承载词云+总结合包推送;现在仅用于词云,总结走 {@link LiveSummary}。 */
|
|
100
|
+
WordCloudAndLiveSummary = 5,
|
|
101
|
+
Superchat = 6,
|
|
102
|
+
UserDanmakuMsg = 7,
|
|
103
|
+
UserActions = 8,
|
|
104
|
+
LiveEnd = 9,
|
|
105
|
+
LiveSummary = 10
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Channel-level feature keys (mirror `@bilibili-notify/push`'s `PushFeature`).
|
|
109
|
+
* Each entry on `SubItemView.target` maps to a list of resolved channel
|
|
110
|
+
* identifiers; an empty / missing list means "not subscribed for this feature".
|
|
111
|
+
*/
|
|
112
|
+
type LivePushFeature = "dynamic" | "live" | "liveEnd" | "liveGuardBuy" | "superchat" | "wordcloud" | "liveSummary" | "specialDanmaku" | "specialUserEnterTheRoom";
|
|
113
|
+
/**
|
|
114
|
+
* Master-level feature keys — the boolean toggles set per-UP. Subset of
|
|
115
|
+
* `LivePushFeature` which omits `specialDanmaku` / `specialUserEnterTheRoom`
|
|
116
|
+
* (those are gated by `customSpecial*.enable` instead).
|
|
117
|
+
*/
|
|
118
|
+
type LiveMasterFeature = Exclude<LivePushFeature, "specialDanmaku" | "specialUserEnterTheRoom">;
|
|
119
|
+
/**
|
|
120
|
+
* Subset of `LiveMasterFeature` whose subscription requires an active live-room
|
|
121
|
+
* WebSocket connection. Mirrors `@bilibili-notify/push`'s `LIVE_ROOM_MASTERS`.
|
|
122
|
+
*/
|
|
123
|
+
declare const LIVE_ROOM_MASTER_KEYS: readonly LiveMasterFeature[];
|
|
124
|
+
/** Sub-level customisation blocks copied from `@bilibili-notify/push`. */
|
|
125
|
+
interface CustomCardStyleLike {
|
|
126
|
+
enable: boolean;
|
|
127
|
+
cardColorStart?: string;
|
|
128
|
+
cardColorEnd?: string;
|
|
129
|
+
}
|
|
130
|
+
interface CustomLiveMsgLike {
|
|
131
|
+
enable: boolean;
|
|
132
|
+
customLiveStart?: string;
|
|
133
|
+
customLive?: string;
|
|
134
|
+
customLiveEnd?: string;
|
|
135
|
+
}
|
|
136
|
+
interface CustomGuardBuyLike {
|
|
137
|
+
enable: boolean;
|
|
138
|
+
guardBuyMsg?: string;
|
|
139
|
+
captainImgUrl?: string;
|
|
140
|
+
supervisorImgUrl?: string;
|
|
141
|
+
governorImgUrl?: string;
|
|
142
|
+
}
|
|
143
|
+
interface CustomLiveSummaryLike {
|
|
144
|
+
enable: boolean;
|
|
145
|
+
liveSummary?: string;
|
|
146
|
+
}
|
|
147
|
+
interface CustomSpecialDanmakuUsersLike {
|
|
148
|
+
enable: boolean;
|
|
149
|
+
specialDanmakuUsers?: string[];
|
|
150
|
+
msgTemplate: string;
|
|
151
|
+
}
|
|
152
|
+
interface CustomSpecialUsersEnterTheRoomLike {
|
|
153
|
+
enable: boolean;
|
|
154
|
+
specialUsersEnterTheRoom?: string[];
|
|
155
|
+
msgTemplate: string;
|
|
156
|
+
}
|
|
157
|
+
/** Per-feature target list (already resolved to channel identifiers). */
|
|
158
|
+
type SubItemTargetLike = Partial<Record<LivePushFeature, unknown[]>>;
|
|
159
|
+
/**
|
|
160
|
+
* Platform-neutral view of a single subscription, structurally compatible with
|
|
161
|
+
* `@bilibili-notify/push`'s `SubItem`. The live engine only reads this shape; the
|
|
162
|
+
* adapter is responsible for providing instances (the Koishi shell hands its
|
|
163
|
+
* `SubItem`s through unchanged, since their fields match by name).
|
|
164
|
+
*/
|
|
165
|
+
interface SubItemView {
|
|
166
|
+
uid: string;
|
|
167
|
+
uname: string;
|
|
168
|
+
roomId: string;
|
|
169
|
+
dynamic: boolean;
|
|
170
|
+
live: boolean;
|
|
171
|
+
liveEnd: boolean;
|
|
172
|
+
liveGuardBuy: boolean;
|
|
173
|
+
superchat: boolean;
|
|
174
|
+
wordcloud: boolean;
|
|
175
|
+
liveSummary: boolean;
|
|
176
|
+
target: SubItemTargetLike;
|
|
177
|
+
customCardStyle: CustomCardStyleLike;
|
|
178
|
+
customLiveMsg: CustomLiveMsgLike;
|
|
179
|
+
customGuardBuy: CustomGuardBuyLike;
|
|
180
|
+
customLiveSummary: CustomLiveSummaryLike;
|
|
181
|
+
customSpecialDanmakuUsers: CustomSpecialDanmakuUsersLike;
|
|
182
|
+
customSpecialUsersEnterTheRoom: CustomSpecialUsersEnterTheRoomLike;
|
|
183
|
+
/**
|
|
184
|
+
* Per-UP 覆盖项。adapter 已通过 `resolve(sub, defaults)` 折叠 globals + overrides,
|
|
185
|
+
* 这里只保留实际跟全局可能不同的字段;undefined 表示「按全局走」。room-session /
|
|
186
|
+
* room-session-base / live-summary-requester 在用值前一律 `?? ctx.config.X` 回退。
|
|
187
|
+
*
|
|
188
|
+
* 这些字段也会随 LiveScopedChange 增量推送给 LiveEngine.applyOps,Object.assign
|
|
189
|
+
* 合进活跃 sub 后,SC / 上舰 / restartPush / liveSummary 调用点会读到新值;pushTime
|
|
190
|
+
* 因 setInterval 句柄 ms 不可变,engine 在 update 时检测变化后会单独 rearm。
|
|
191
|
+
*/
|
|
192
|
+
minScPrice?: number;
|
|
193
|
+
minGuardLevel?: 1 | 2 | 3;
|
|
194
|
+
pushTime?: number;
|
|
195
|
+
restartPush?: boolean;
|
|
196
|
+
aiOverride?: CommentaryCallOverride;
|
|
197
|
+
}
|
|
198
|
+
type SubscriptionsView = Record<string, SubItemView>;
|
|
199
|
+
/**
|
|
200
|
+
* Scoped change object — mirrors `koishi-plugin-bilibili-notify`'s
|
|
201
|
+
* `SubChange` so the koishi adapter can forward incremental subscription
|
|
202
|
+
* updates without translation.
|
|
203
|
+
*/
|
|
204
|
+
type LiveScopedChange = {
|
|
205
|
+
scope: "live";
|
|
206
|
+
} & Partial<Pick<SubItemView, "live" | "liveEnd" | "liveGuardBuy" | "superchat" | "wordcloud" | "liveSummary" | "uname" | "roomId" | "customCardStyle" | "customLiveMsg" | "customGuardBuy" | "customLiveSummary" | "customSpecialDanmakuUsers" | "customSpecialUsersEnterTheRoom" | "minScPrice" | "minGuardLevel" | "pushTime" | "restartPush" | "aiOverride">>;
|
|
207
|
+
type DynamicScopedChange = {
|
|
208
|
+
scope: "dynamic";
|
|
209
|
+
} & Partial<Pick<SubItemView, "dynamic">>;
|
|
210
|
+
type TargetScopedChange = {
|
|
211
|
+
scope: "target";
|
|
212
|
+
} & Pick<SubItemView, "target">;
|
|
213
|
+
type LiveSubChange = LiveScopedChange | DynamicScopedChange | TargetScopedChange;
|
|
214
|
+
type LiveSubscriptionOp = {
|
|
215
|
+
type: "add";
|
|
216
|
+
sub: SubItemView;
|
|
217
|
+
} | {
|
|
218
|
+
type: "delete";
|
|
219
|
+
uid: string;
|
|
220
|
+
} | {
|
|
221
|
+
type: "update";
|
|
222
|
+
uid: string;
|
|
223
|
+
changes: LiveSubChange[];
|
|
224
|
+
};
|
|
225
|
+
/**
|
|
226
|
+
* Push-out interface required by live-engine. Mirrors the methods on
|
|
227
|
+
* `@bilibili-notify/push`'s `BilibiliPush` we actually call.
|
|
228
|
+
*
|
|
229
|
+
* `content` is intentionally `unknown` — the koishi adapter passes koishi's
|
|
230
|
+
* `h(...)` element fragments while the standalone adapter will pass its own
|
|
231
|
+
* `NotificationPayload`. The engine only forwards the value through.
|
|
232
|
+
*/
|
|
233
|
+
interface PushLike {
|
|
234
|
+
broadcastToTargets(uid: string, content: unknown, type: LivePushType): Promise<void>;
|
|
235
|
+
sendPrivateMsg(content: string): Promise<void>;
|
|
236
|
+
}
|
|
237
|
+
//#endregion
|
|
238
|
+
//#region src/types.d.ts
|
|
239
|
+
declare enum LiveType {
|
|
240
|
+
NotLiveBroadcast = 0,
|
|
241
|
+
StartBroadcasting = 1,
|
|
242
|
+
LiveBroadcast = 2,
|
|
243
|
+
StopBroadcast = 3,
|
|
244
|
+
FirstLiveBroadcast = 4
|
|
245
|
+
}
|
|
246
|
+
interface MasterInfo {
|
|
247
|
+
username: string;
|
|
248
|
+
userface: string;
|
|
249
|
+
roomId: number;
|
|
250
|
+
liveOpenFollowerNum: number;
|
|
251
|
+
liveEndFollowerNum: number;
|
|
252
|
+
liveFollowerChange: number;
|
|
253
|
+
medalName: string;
|
|
254
|
+
}
|
|
255
|
+
interface LiveData {
|
|
256
|
+
watchedNum?: string | number;
|
|
257
|
+
likedNum?: string | number;
|
|
258
|
+
fansNum?: string | number;
|
|
259
|
+
fansChanged?: string | number;
|
|
260
|
+
}
|
|
261
|
+
interface UserInfoInLiveData {
|
|
262
|
+
uid: number;
|
|
263
|
+
uname: string;
|
|
264
|
+
face: string;
|
|
265
|
+
is_admin: number;
|
|
266
|
+
}
|
|
267
|
+
type LivePushTimerManager = Map<string, () => void>;
|
|
268
|
+
//#endregion
|
|
269
|
+
//#region src/template-renderer.d.ts
|
|
270
|
+
/**
|
|
271
|
+
* Plain string-substitution based template renderer for live-related notification
|
|
272
|
+
* text. Mirrors the per-occurrence templates supported in the koishi schema:
|
|
273
|
+
*
|
|
274
|
+
* - `customLiveStart` / `customLive` / `customLiveEnd`
|
|
275
|
+
* - `customGuardBuy.guardBuyMsg`
|
|
276
|
+
* - `customSpecialDanmakuUsers.msgTemplate`
|
|
277
|
+
* - `customSpecialUsersEnterTheRoom.msgTemplate`
|
|
278
|
+
*
|
|
279
|
+
* The variable syntax follows the existing `-name` / `-time` / `-watched` style
|
|
280
|
+
* (NOT the `{key}` syntax used by `@bilibili-notify/internal`'s `interpolate`),
|
|
281
|
+
* because that's what users have in their existing Koishi configs and we keep
|
|
282
|
+
* 1:1 backward compatibility.
|
|
283
|
+
*/
|
|
284
|
+
/** Defaults applied when neither sub-level nor global config provides a template. */
|
|
285
|
+
declare const DEFAULT_LIVE_TEMPLATES: {
|
|
286
|
+
readonly liveStart: "-name 开播啦,当前粉丝数:-follower\n-link";
|
|
287
|
+
readonly liveOngoing: "-name 正在直播,已播 -time,累计观看:-watched\n-link";
|
|
288
|
+
readonly liveEnd: "-name 下播啦,本次直播了 -time,粉丝变化 -follower_change";
|
|
289
|
+
readonly liveSummaryFallback: "弹幕总结";
|
|
290
|
+
};
|
|
291
|
+
/**
|
|
292
|
+
* Format follower-change as a signed magnitude string with a 1万 (10K) cutoff,
|
|
293
|
+
* mirroring live-service's inline formatting.
|
|
294
|
+
*/
|
|
295
|
+
declare function formatFollowerChange(n: number): string;
|
|
296
|
+
/** Format follower count, abbreviating ≥10K to `X.X万`. */
|
|
297
|
+
declare function formatFollowerCount(n: number): string;
|
|
298
|
+
/** Build the canonical room link from `LiveRoomInfo.data` style fields. */
|
|
299
|
+
declare function buildRoomLink(info: {
|
|
300
|
+
short_id: number;
|
|
301
|
+
room_id: number;
|
|
302
|
+
}): string;
|
|
303
|
+
declare class LiveTemplateRenderer {
|
|
304
|
+
/** Compose the "开播" notification text for a sub. */
|
|
305
|
+
renderLiveStart(params: {
|
|
306
|
+
sub: SubItemView;
|
|
307
|
+
globalCustom?: CustomLiveMsgLike;
|
|
308
|
+
master: MasterInfo;
|
|
309
|
+
diffTime: string;
|
|
310
|
+
followerNum: string;
|
|
311
|
+
roomLink: string;
|
|
312
|
+
}): string;
|
|
313
|
+
/** Compose the periodic "正在直播" notification text. */
|
|
314
|
+
renderLiveOngoing(params: {
|
|
315
|
+
sub: SubItemView;
|
|
316
|
+
globalCustom?: CustomLiveMsgLike;
|
|
317
|
+
master: MasterInfo;
|
|
318
|
+
diffTime: string;
|
|
319
|
+
watched: string;
|
|
320
|
+
roomLink: string;
|
|
321
|
+
}): string;
|
|
322
|
+
/** Compose the "下播" notification text. */
|
|
323
|
+
renderLiveEnd(params: {
|
|
324
|
+
sub: SubItemView;
|
|
325
|
+
globalCustom?: CustomLiveMsgLike;
|
|
326
|
+
master: MasterInfo;
|
|
327
|
+
diffTime: string;
|
|
328
|
+
followerChange: number;
|
|
329
|
+
}): string;
|
|
330
|
+
/**
|
|
331
|
+
* Compose the "上舰" notification text using the effective custom-guard
|
|
332
|
+
* config (resolved by listener-manager).
|
|
333
|
+
*/
|
|
334
|
+
renderGuardBuy(params: {
|
|
335
|
+
guardBuyConfig: CustomGuardBuyLike;
|
|
336
|
+
uname: string;
|
|
337
|
+
master: MasterInfo | undefined;
|
|
338
|
+
giftName: string;
|
|
339
|
+
}): string;
|
|
340
|
+
/** Compose the "特别关注弹幕" notification text. */
|
|
341
|
+
renderSpecialDanmaku(params: {
|
|
342
|
+
template: string;
|
|
343
|
+
uname: string;
|
|
344
|
+
master: MasterInfo | undefined;
|
|
345
|
+
content: string;
|
|
346
|
+
}): string;
|
|
347
|
+
/** Compose the "特别关注进入直播间" notification text. */
|
|
348
|
+
renderSpecialUserEnter(params: {
|
|
349
|
+
template: string;
|
|
350
|
+
uname: string;
|
|
351
|
+
master: MasterInfo | undefined;
|
|
352
|
+
}): string;
|
|
353
|
+
/**
|
|
354
|
+
* Compose the templated "弹幕总结" text used as the fallback when AI
|
|
355
|
+
* summarisation is unavailable. Variables: `-dmc` (sender count), `-mdn`
|
|
356
|
+
* (master medal name), `-dca` (total danmaku), `-un1..5` (top usernames),
|
|
357
|
+
* `-dc1..5` (top counts).
|
|
358
|
+
*/
|
|
359
|
+
renderLiveSummary(params: {
|
|
360
|
+
template: string;
|
|
361
|
+
senderCount: number;
|
|
362
|
+
master: MasterInfo | undefined;
|
|
363
|
+
danmakuCount: number;
|
|
364
|
+
topSenders: Array<[string, number]>;
|
|
365
|
+
}): string;
|
|
366
|
+
}
|
|
367
|
+
//#endregion
|
|
368
|
+
//#region src/live-summary-requester.d.ts
|
|
369
|
+
/**
|
|
370
|
+
* Threshold below which the engine refuses to generate a live summary.
|
|
371
|
+
* Mirrors the original live-service heuristic.
|
|
372
|
+
*/
|
|
373
|
+
declare const LIVE_SUMMARY_MIN_SENDERS = 5;
|
|
374
|
+
/**
|
|
375
|
+
* Builds the AI prompt + dispatches it through {@link CommentaryGenerator},
|
|
376
|
+
* falling back to the template-based summary when AI is unavailable or fails.
|
|
377
|
+
*
|
|
378
|
+
* Two-tier strategy (kept identical to live-service):
|
|
379
|
+
* 1. If `commentary` is non-null, build a prompt summarising sender count,
|
|
380
|
+
* medal name, total danmaku, top-10 words and top-5 senders, then call
|
|
381
|
+
* `commentary.comment(prompt, "liveSummary")`.
|
|
382
|
+
* 2. On failure (AI not configured / API error), fall back to the user-supplied
|
|
383
|
+
* template (`customLiveSummary` per-sub or the global default).
|
|
384
|
+
*
|
|
385
|
+
* Returns `undefined` when sender count is below the threshold (signals the
|
|
386
|
+
* caller to skip the summary push entirely).
|
|
387
|
+
*/
|
|
388
|
+
declare class LiveSummaryRequester {
|
|
389
|
+
private commentary;
|
|
390
|
+
private readonly isAiEnabled;
|
|
391
|
+
private readonly templateRenderer;
|
|
392
|
+
private readonly logger;
|
|
393
|
+
constructor(opts: {
|
|
394
|
+
commentary: CommentaryGenerator | null;
|
|
395
|
+
/**
|
|
396
|
+
* AI 总开关查询。返回 false 时跳过 commentary 调用,直接走模板回退,
|
|
397
|
+
* 与 commentary === null 行为等价。Adapter 用 `() => globals.defaults.ai.enabled` 填充,
|
|
398
|
+
* 缺省 () => true。
|
|
399
|
+
*/
|
|
400
|
+
isAiEnabled?: () => boolean;
|
|
401
|
+
templateRenderer: LiveTemplateRenderer;
|
|
402
|
+
logger: Logger;
|
|
403
|
+
});
|
|
404
|
+
/** 热替换 CommentaryGenerator 实例。null 表示降级到模板回退。 */
|
|
405
|
+
setCommentary(commentary: CommentaryGenerator | null): void;
|
|
406
|
+
generate(params: {
|
|
407
|
+
senderRecord: Record<string, number>;
|
|
408
|
+
sortedWords: Array<[string, number]>;
|
|
409
|
+
master: MasterInfo | undefined;
|
|
410
|
+
customLiveSummary: string;
|
|
411
|
+
/**
|
|
412
|
+
* per-UP AI 覆盖。adapter 在 SubItemView.aiOverride 上注入,room-session 在调
|
|
413
|
+
* `generate()` 时透传过来。CommentaryGenerator 内部 `?? this.config` 兜底,
|
|
414
|
+
* 缺失字段不影响其它字段生效。
|
|
415
|
+
*/
|
|
416
|
+
aiOverride?: CommentaryCallOverride;
|
|
417
|
+
}): Promise<string | undefined>;
|
|
418
|
+
}
|
|
419
|
+
//#endregion
|
|
420
|
+
//#region src/wordcloud-generator.d.ts
|
|
421
|
+
/**
|
|
422
|
+
* Threshold for refusing to render a wordcloud — stays in sync with the
|
|
423
|
+
* original `live-service` heuristic: a board-level wordcloud needs a baseline
|
|
424
|
+
* vocabulary or it looks empty.
|
|
425
|
+
*/
|
|
426
|
+
declare const WORDCLOUD_MIN_WORDS = 50;
|
|
427
|
+
/** Cap on how many top words are passed into the wordcloud renderer. */
|
|
428
|
+
declare const WORDCLOUD_TOP_WORDS = 90;
|
|
429
|
+
/**
|
|
430
|
+
* Wraps {@link ImageRenderer.generateWordCloudImg} with the engine's gating
|
|
431
|
+
* logic (≥50 unique words required) and surfaces logger messages identical to
|
|
432
|
+
* the original live-service.
|
|
433
|
+
*
|
|
434
|
+
* The output is a `Buffer` so the caller decides how to wrap it for the target
|
|
435
|
+
* platform (e.g. via `LiveContentBuilder.image`).
|
|
436
|
+
*/
|
|
437
|
+
declare class WordcloudGenerator {
|
|
438
|
+
private readonly imageRenderer;
|
|
439
|
+
private readonly isImageEnabled;
|
|
440
|
+
private readonly logger;
|
|
441
|
+
constructor(opts: {
|
|
442
|
+
imageRenderer: ImageRenderer | null;
|
|
443
|
+
/**
|
|
444
|
+
* 卡片渲染总开关查询。返回 false 时直接跳过 puppeteer 调用,与缺失 imageRenderer
|
|
445
|
+
* 等价。Adapter 通常用 `() => globals.defaults.cardStyle.enabled` 填充;缺省 () => true。
|
|
446
|
+
*/
|
|
447
|
+
isImageEnabled?: () => boolean;
|
|
448
|
+
logger: Logger;
|
|
449
|
+
});
|
|
450
|
+
/**
|
|
451
|
+
* Render a wordcloud image for `(masterName, masterAvatarUrl)`.
|
|
452
|
+
*
|
|
453
|
+
* Returns `undefined` when:
|
|
454
|
+
* - There are fewer than {@link WORDCLOUD_MIN_WORDS} unique words.
|
|
455
|
+
* - No `ImageRenderer` was injected (image-engine isn't installed).
|
|
456
|
+
* - The renderer threw; the error is logged and swallowed so the rest of
|
|
457
|
+
* the live-end pipeline (summary + downstream push) keeps running.
|
|
458
|
+
*/
|
|
459
|
+
generate(sortedWords: Array<[string, number]>, masterName: string, masterAvatarUrl?: string): Promise<Buffer | undefined>;
|
|
460
|
+
}
|
|
461
|
+
//#endregion
|
|
462
|
+
//#region src/room-context.d.ts
|
|
463
|
+
/**
|
|
464
|
+
* Configuration that listener-manager + room-session both consume.
|
|
465
|
+
* Mirrors the koishi `BilibiliNotifyLiveConfig` minus the `logLevel` /
|
|
466
|
+
* `wordcloudStopWords` fields (those are owned at the LiveEngine level).
|
|
467
|
+
*/
|
|
468
|
+
interface ListenerManagerConfig {
|
|
469
|
+
pushTime: number;
|
|
470
|
+
restartPush: boolean;
|
|
471
|
+
minScPrice: number;
|
|
472
|
+
minGuardLevel: 1 | 2 | 3;
|
|
473
|
+
customGuardBuy: {
|
|
474
|
+
enable: boolean;
|
|
475
|
+
guardBuyMsg?: string;
|
|
476
|
+
captainImgUrl?: string;
|
|
477
|
+
supervisorImgUrl?: string;
|
|
478
|
+
governorImgUrl?: string;
|
|
479
|
+
};
|
|
480
|
+
customLiveMsg: {
|
|
481
|
+
enable: boolean;
|
|
482
|
+
customLiveStart?: string;
|
|
483
|
+
customLive?: string;
|
|
484
|
+
customLiveEnd?: string;
|
|
485
|
+
};
|
|
486
|
+
/** Default global `liveSummary` template (joined with `\n`). */
|
|
487
|
+
liveSummaryDefault: string;
|
|
488
|
+
/**
|
|
489
|
+
* 图片卡片渲染总开关。`false` 时 RoomContext 暴露的 imageRenderer 始终为 null,
|
|
490
|
+
* 直播开播 / SC / 上舰 等路径自然走 `if (renderer?.generateXxx)` 落入文字回退。缺省视为 true。
|
|
491
|
+
*/
|
|
492
|
+
imageEnabled?: boolean;
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Constructor options for {@link RoomContext}. Mirrors the dependency set
|
|
496
|
+
* passed into `LiveEngine`; the engine builds one `RoomContext` and shares it
|
|
497
|
+
* with both {@link import("./listener-manager").ListenerManager} (for lifecycle
|
|
498
|
+
* + connection setup) and {@link import("./room-session").RoomSession} (for
|
|
499
|
+
* per-room dispatcher).
|
|
500
|
+
*/
|
|
501
|
+
interface RoomContextOptions {
|
|
502
|
+
serviceCtx: ServiceContext;
|
|
503
|
+
api: BilibiliAPI;
|
|
504
|
+
push: PushLike;
|
|
505
|
+
contentBuilder: LiveContentBuilder;
|
|
506
|
+
templateRenderer: LiveTemplateRenderer;
|
|
507
|
+
wordcloudGenerator: WordcloudGenerator;
|
|
508
|
+
liveSummaryRequester: LiveSummaryRequester;
|
|
509
|
+
danmakuCollector: DanmakuCollector;
|
|
510
|
+
imageRenderer: ImageRenderer | null;
|
|
511
|
+
config: ListenerManagerConfig;
|
|
512
|
+
emitEngineError: (message: string) => void;
|
|
513
|
+
/**
|
|
514
|
+
* 推送 per-UID 直播状态变化(`onLiveStart` / `onLiveEnd` / `bootstrap 已开播` /
|
|
515
|
+
* `stopMonitoring 时挂掉的活房间`)。Adapter 实现:
|
|
516
|
+
* - standalone: `(uid, status) => bus.emit("live-state-changed", uid, status)`
|
|
517
|
+
* - koishi: `(uid, status) => ctx.emit("bilibili-notify/live-state-changed", uid, status)`
|
|
518
|
+
* 可选;缺省时不推送 —— 仅在 dashboard 走 WS 实时刷新"正在直播"面板时有意义。
|
|
519
|
+
*/
|
|
520
|
+
emitLiveState?: (uid: string, status: "live" | "idle") => void;
|
|
521
|
+
/**
|
|
522
|
+
* 推送 per-UID 累计观看人数变化(B 站 `WATCHED_CHANGE` 帧节流后转发)。Adapter
|
|
523
|
+
* 实现与 emitLiveState 同型:
|
|
524
|
+
* - standalone: `(uid, viewers) => bus.emit("live-viewers-changed", uid, viewers)`
|
|
525
|
+
* - koishi: `(uid, viewers) => ctx.emit("bilibili-notify/live-viewers-changed", uid, viewers)`
|
|
526
|
+
* 可选;缺省时不推送。room-session 在调用前做 per-UID 2s throttle,所以这里收到
|
|
527
|
+
* 的频率已经稀疏(每个直播间最多每 2s 一次)。
|
|
528
|
+
*/
|
|
529
|
+
emitViewers?: (uid: string, viewers: string) => void;
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Shared room-level infrastructure surface. Stores all engine-injected deps,
|
|
533
|
+
* the per-room listener registry, and the periodic-timer registry; offers the
|
|
534
|
+
* lifecycle / predicate / disposal primitives consumed by both
|
|
535
|
+
* {@link import("./listener-manager").ListenerManager} and
|
|
536
|
+
* {@link import("./room-session").RoomSession}.
|
|
537
|
+
*
|
|
538
|
+
* The data-fetch / card-render / time-format helpers live in
|
|
539
|
+
* {@link import("./room-helpers").RoomContextHelpers} (a subclass of this
|
|
540
|
+
* class). The split keeps each file focused: this one handles state + WS
|
|
541
|
+
* lifecycle, the helpers file wraps every external API/IO call.
|
|
542
|
+
*/
|
|
543
|
+
declare class RoomContextBase {
|
|
544
|
+
readonly serviceCtx: ServiceContext;
|
|
545
|
+
readonly logger: Logger;
|
|
546
|
+
readonly api: BilibiliAPI;
|
|
547
|
+
readonly push: PushLike;
|
|
548
|
+
readonly contentBuilder: LiveContentBuilder;
|
|
549
|
+
readonly templateRenderer: LiveTemplateRenderer;
|
|
550
|
+
readonly wordcloudGenerator: WordcloudGenerator;
|
|
551
|
+
readonly liveSummaryRequester: LiveSummaryRequester;
|
|
552
|
+
readonly danmakuCollector: DanmakuCollector;
|
|
553
|
+
/**
|
|
554
|
+
* 真实注入的渲染器引用,private 是因为外部应通过 `imageRenderer` getter 访问 ——
|
|
555
|
+
* 后者会在 `config.imageEnabled === false` 时返回 null,让所有
|
|
556
|
+
* `if (this.imageRenderer?.generateXxx)` 自然落入文字回退分支。
|
|
557
|
+
*/
|
|
558
|
+
private readonly _imageRenderer;
|
|
559
|
+
readonly emitEngineError: (message: string) => void;
|
|
560
|
+
private readonly _emitLiveState;
|
|
561
|
+
private readonly _emitViewers;
|
|
562
|
+
config: ListenerManagerConfig;
|
|
563
|
+
readonly listenerRecord: Record<string, MessageListener>;
|
|
564
|
+
readonly livePushTimerManager: Map<string, () => void>;
|
|
565
|
+
private disposed;
|
|
566
|
+
/** Cached protobuf type for INTERACT_WORD_V2 decoding (lazy-loaded). */
|
|
567
|
+
protected interactWord?: protobuf.Type;
|
|
568
|
+
/**
|
|
569
|
+
* Set once the proto load/lookup has failed (missing/invalid
|
|
570
|
+
* `proto/interact_word.proto`) so we degrade gracefully instead of
|
|
571
|
+
* re-attempting + error-spamming on every INTERACT_WORD_V2 frame.
|
|
572
|
+
*/
|
|
573
|
+
protected interactWordUnavailable: boolean;
|
|
574
|
+
private readonly instanceId;
|
|
575
|
+
constructor(opts: RoomContextOptions);
|
|
576
|
+
/**
|
|
577
|
+
* 安全调用方:adapter 未注入时静默 no-op,业务代码无需在调用点判空。
|
|
578
|
+
*/
|
|
579
|
+
emitLiveState(uid: string, status: "live" | "idle"): void;
|
|
580
|
+
/**
|
|
581
|
+
* 同型 no-op 安全调用方。room-session 已做 per-UID 节流,这里只是分发。
|
|
582
|
+
*/
|
|
583
|
+
emitViewers(uid: string, viewers: string): void;
|
|
584
|
+
/** 受 `config.imageEnabled` 门控的渲染器视图;关闭时返回 null。 */
|
|
585
|
+
get imageRenderer(): ImageRenderer | null;
|
|
586
|
+
updateConfig(config: ListenerManagerConfig): void;
|
|
587
|
+
isDisposed(): boolean;
|
|
588
|
+
setDisposed(value: boolean): void;
|
|
589
|
+
getListenerCount(): number;
|
|
590
|
+
logSideEffectState(stage: string): void;
|
|
591
|
+
hasTargets(sub: SubItemView, ...types: LivePushFeature[]): boolean;
|
|
592
|
+
isSubscribed(sub: SubItemView, type: LiveMasterFeature): boolean;
|
|
593
|
+
needsLiveMonitor(sub: SubItemView): boolean;
|
|
594
|
+
closeListener(roomId: string): void;
|
|
595
|
+
clearListeners(): void;
|
|
596
|
+
clearPushTimers(): void;
|
|
597
|
+
stopMonitoring(reason: string, roomId?: string): void;
|
|
598
|
+
}
|
|
599
|
+
//#endregion
|
|
600
|
+
//#region src/room-helpers.d.ts
|
|
601
|
+
/**
|
|
602
|
+
* Extends {@link RoomContextBase} with the data-fetch / card-render /
|
|
603
|
+
* time-format helpers — every call here either hits the Bilibili HTTP API or
|
|
604
|
+
* the optional `ImageRenderer`. Keeping them on a separate class keeps the
|
|
605
|
+
* base file focused on state / lifecycle while preserving the inheritance
|
|
606
|
+
* chain so {@link RoomSession} sees a single `ctx.foo()` API surface.
|
|
607
|
+
*/
|
|
608
|
+
declare class RoomContext extends RoomContextBase {
|
|
609
|
+
/**
|
|
610
|
+
* Bring up the WebSocket listener for `roomId`.
|
|
611
|
+
*
|
|
612
|
+
* L4: returns `true` iff there is an active listener for the room *after*
|
|
613
|
+
* this call — either freshly created OR already present (the latter lets a
|
|
614
|
+
* reconnect that races with a backoff-window restore treat the room as
|
|
615
|
+
* recovered). Returns `false` on every failure mode so the reconnect caller
|
|
616
|
+
* only resets its backoff on a real success instead of the old
|
|
617
|
+
* void-swallow that recorded "reconnected" with no listener attached.
|
|
618
|
+
*/
|
|
619
|
+
startLiveRoomListener(roomId: string, handler: MsgHandler, shouldAbort?: () => boolean): Promise<boolean>;
|
|
620
|
+
/** Fetch live-room info; on failure, notifies admin + tears down this room. */
|
|
621
|
+
getLiveRoomInfo(roomId: string): Promise<LiveRoomInfo["data"] | undefined>;
|
|
622
|
+
/**
|
|
623
|
+
* Fetch + project a `MasterInfo` snapshot. Carries forward `liveOpenFollowerNum`
|
|
624
|
+
* across mid-session refreshes so that the live-end card reports an accurate
|
|
625
|
+
* follower delta.
|
|
626
|
+
*/
|
|
627
|
+
getMasterInfo(uid: string, previous: MasterInfo | undefined, liveType: LiveType): Promise<MasterInfo>;
|
|
628
|
+
/** Fire-and-forget push wrapper; logs + drops any rejection. */
|
|
629
|
+
safeBroadcast(uid: string, content: unknown, type: LivePushType): void;
|
|
630
|
+
/**
|
|
631
|
+
* Push a "live start / live ongoing / live end" notification card. Generates
|
|
632
|
+
* an image via {@link ImageRenderer.generateLiveCard} when available; falls
|
|
633
|
+
* back to plain text on failure.
|
|
634
|
+
*/
|
|
635
|
+
sendLiveNotifyCard(params: {
|
|
636
|
+
liveType: LiveType;
|
|
637
|
+
liveData: LiveData;
|
|
638
|
+
liveRoomInfo: LiveRoomInfo["data"];
|
|
639
|
+
master: MasterInfo;
|
|
640
|
+
cardStyle: SubItemView["customCardStyle"];
|
|
641
|
+
uid: string;
|
|
642
|
+
notifyMsg: string;
|
|
643
|
+
}): Promise<void>;
|
|
644
|
+
/** Format `dateString` (yyyy-MM-dd HH:mm:ss UTC+8) as elapsed-time text. */
|
|
645
|
+
getTimeDifference(dateString: string): Promise<string>;
|
|
646
|
+
/**
|
|
647
|
+
* Decode a base64-encoded INTERACT_WORD_V2 protobuf payload.
|
|
648
|
+
*
|
|
649
|
+
* P0-4: 此前用 `resolve(__dirname, "./proto/interact_word.proto")` —— (a) 该
|
|
650
|
+
* .proto 文件从未随包提交、tsdown 也不拷进 lib;(b) ESM(.mjs)产物里裸
|
|
651
|
+
* `__dirname` 为 undefined。两者叠加导致每个 INTERACT_WORD_V2 帧必抛,
|
|
652
|
+
* "特别关注用户进房"特性在双端构建里全死。
|
|
653
|
+
*
|
|
654
|
+
* 现:`__dirname` 改 `import.meta.url`(与 routes/health.ts 同款,tsdown
|
|
655
|
+
* cjs/esm 双产物都正确);proto 缺失/损坏时**优雅降级**——只在首次告警一
|
|
656
|
+
* 次并置 `interactWordUnavailable`,后续帧直接返回 `{}`(调用方
|
|
657
|
+
* onInteractWordV2 对空对象天然 no-op:`msgType==="1"` 为 false,零误推),
|
|
658
|
+
* 不再每帧崩/刷屏。
|
|
659
|
+
*
|
|
660
|
+
* 注:让该特性真正可用仍需在 `src/proto/interact_word.proto` 放入**经核实
|
|
661
|
+
* 的**权威 schema(`bilibili.live.xuserreward.v1.InteractWord`)并在打包时
|
|
662
|
+
* 拷进 `lib/proto/`——字段号必须来自可信源,不可臆造,故作为独立后续任务。
|
|
663
|
+
*/
|
|
664
|
+
decodeBase64PB(base64: string): Promise<Record<string, unknown>>;
|
|
665
|
+
}
|
|
666
|
+
//#endregion
|
|
667
|
+
//#region src/room-session-base.d.ts
|
|
668
|
+
/**
|
|
669
|
+
* Cooldown window between accepting `onLiveStart` / `onLiveEnd` events; the
|
|
670
|
+
* Bilibili WS sometimes fires duplicates for the same transition.
|
|
671
|
+
*/
|
|
672
|
+
declare const LIVE_EVENT_COOLDOWN: number;
|
|
673
|
+
/**
|
|
674
|
+
* Base class for {@link import("./room-session").RoomSession}, holding all
|
|
675
|
+
* per-room mutable state and the high-level lifecycle / transition logic
|
|
676
|
+
* (bootstrap, periodic-timer arm/cancel, live-end pipeline). Event handlers
|
|
677
|
+
* (`onLiveStart`, `onIncomeSuperChat`, etc.) live in the subclass.
|
|
678
|
+
*
|
|
679
|
+
* State fields are `protected` so the subclass can read & mutate them
|
|
680
|
+
* directly when handling MsgHandler events.
|
|
681
|
+
*/
|
|
682
|
+
declare abstract class RoomSessionBase {
|
|
683
|
+
protected readonly ctx: RoomContext;
|
|
684
|
+
protected readonly sub: SubItemView;
|
|
685
|
+
protected liveTime: string;
|
|
686
|
+
protected liveStatus: boolean;
|
|
687
|
+
protected liveRoomInfo: LiveRoomInfo["data"] | undefined;
|
|
688
|
+
protected masterInfo: MasterInfo | undefined;
|
|
689
|
+
protected readonly liveData: LiveData;
|
|
690
|
+
protected pushAtTimeTimer: Disposable | null;
|
|
691
|
+
protected lastLiveStart: number;
|
|
692
|
+
protected lastLiveEnd: number;
|
|
693
|
+
constructor(ctx: RoomContext, sub: SubItemView);
|
|
694
|
+
/** Whether the underlying B-station room is currently broadcasting. */
|
|
695
|
+
get isLive(): boolean;
|
|
696
|
+
/**
|
|
697
|
+
* 唯一允许翻转 `liveStatus` 的入口。只在真实 transition 时通过 RoomContext
|
|
698
|
+
* 推送 `live-state-changed` 事件,前端的"正在直播"面板靠它实时收敛。
|
|
699
|
+
* 直接赋值 `this.liveStatus = ...` 会绕过这里,**不要这样做**。
|
|
700
|
+
*/
|
|
701
|
+
protected setLiveStatus(next: boolean): void;
|
|
702
|
+
/**
|
|
703
|
+
* Read-only diagnostic snapshot for routes / dashboards. Includes `uid`,
|
|
704
|
+
* `roomId`, and — when `liveRoomInfo` was successfully fetched — `title`,
|
|
705
|
+
* `cover`, `areaName`, `startedAt`. Returns undefined fields rather than
|
|
706
|
+
* partial data so consumers can render fallbacks deterministically.
|
|
707
|
+
*/
|
|
708
|
+
getLiveSnapshot(): {
|
|
709
|
+
uid: string;
|
|
710
|
+
roomId: string;
|
|
711
|
+
isLive: boolean;
|
|
712
|
+
title?: string;
|
|
713
|
+
cover?: string;
|
|
714
|
+
areaName?: string;
|
|
715
|
+
startedAt?: string;
|
|
716
|
+
/**
|
|
717
|
+
* B 站 WS `WATCHED_CHANGE` 帧给出的"累计观看人数",预格式化字符串(如 "1.2万")。
|
|
718
|
+
* 还没收到该帧时为 undefined,前端显示 "—"。我们不存原始 num,因为 bilibili 自己
|
|
719
|
+
* 给的 text_small 已是用户预期的中文压缩形式。
|
|
720
|
+
*/
|
|
721
|
+
viewers?: string;
|
|
722
|
+
};
|
|
723
|
+
/**
|
|
724
|
+
* Open the WS connection (via `RoomContext.startLiveRoomListener`), pull
|
|
725
|
+
* the initial live-room snapshot, and — if the room is already live —
|
|
726
|
+
* kick off the `restartPush` branch + arm the periodic timer.
|
|
727
|
+
*/
|
|
728
|
+
bootstrap(): Promise<void>;
|
|
729
|
+
/** Build the platform-specific {@link MsgHandler}; provided by the subclass. */
|
|
730
|
+
protected abstract buildHandler(): MsgHandler;
|
|
731
|
+
protected useLiveRoomInfo(liveType: LiveType): Promise<boolean>;
|
|
732
|
+
protected useMasterInfo(liveType: LiveType): Promise<boolean>;
|
|
733
|
+
/**
|
|
734
|
+
* Live 配置 `pushTime` 热更后调用:重新按当前(可能已变更的) `pushTime`
|
|
735
|
+
* arm 定时器。仅对正在直播的房间生效,因为只有 live 状态下才会有 timer。
|
|
736
|
+
*
|
|
737
|
+
* 注意:`setInterval` 句柄的 ms 参数是 immutable,只能 dispose 重建。
|
|
738
|
+
*/
|
|
739
|
+
rearmPeriodicTimer(): void;
|
|
740
|
+
protected armPeriodicTimer(): void;
|
|
741
|
+
protected cancelPeriodicTimer(): void;
|
|
742
|
+
/** Periodic "正在直播" tick (callback for `setInterval`). */
|
|
743
|
+
protected tickPushAtTime(): Promise<void>;
|
|
744
|
+
/**
|
|
745
|
+
* Live-end pipeline (shared by the WS `onLiveEnd` event and the polling
|
|
746
|
+
* fallback in {@link tickPushAtTime}).
|
|
747
|
+
*
|
|
748
|
+
* Order: cancel periodic timer → refresh room/master info → push live-end
|
|
749
|
+
* card → kick off wordcloud + summary → drain danmaku buffer.
|
|
750
|
+
*/
|
|
751
|
+
protected handleLiveEnd(source: "ws" | "polling"): Promise<void>;
|
|
752
|
+
/**
|
|
753
|
+
* Run wordcloud + AI live-summary in parallel and dispatch whichever
|
|
754
|
+
* succeeded. Skipped entirely when neither feature is subscribed.
|
|
755
|
+
*/
|
|
756
|
+
protected dispatchWordCloudAndSummary(customLiveSummary: string): Promise<void>;
|
|
757
|
+
}
|
|
758
|
+
//#endregion
|
|
759
|
+
//#region src/room-session.d.ts
|
|
760
|
+
declare class RoomSession extends RoomSessionBase {
|
|
761
|
+
private lastViewersEmitMs;
|
|
762
|
+
/**
|
|
763
|
+
* 当前 RoomSession 是否已被外层(stopForUid / disposeAll / liveEnd 主动关闭)取消。
|
|
764
|
+
* 一旦设为 true,onError 跳过重连。listener-manager.stopForUid 在 closeListener
|
|
765
|
+
* 之前调用 cancel() 设置。
|
|
766
|
+
*/
|
|
767
|
+
private cancelled;
|
|
768
|
+
private reconnectAttempts;
|
|
769
|
+
/**
|
|
770
|
+
* L1 单飞守卫:并发 onError(WS 错误常突发多帧)若都进入重连路径,会各自
|
|
771
|
+
* closeListener + 退避 + startLiveRoomListener,装回多个 listener。一旦一个
|
|
772
|
+
* onError 拿到重连权,其余直接返回。
|
|
773
|
+
*/
|
|
774
|
+
private reconnecting;
|
|
775
|
+
/** L3:退避 sleep 的 Disposable + 唤醒句柄,cancel/teardown 时清掉,不留回调到 expiry。 */
|
|
776
|
+
private reconnectTimer?;
|
|
777
|
+
private reconnectWake?;
|
|
778
|
+
/** 外层主动停止 listener 时调用,阻止 onError 触发重连。 */
|
|
779
|
+
cancel(): void;
|
|
780
|
+
/** L3:dispose 退避定时器并唤醒重连循环,使其立刻重校 cancelled/disposed 后退出。 */
|
|
781
|
+
private clearReconnectSleep;
|
|
782
|
+
protected buildHandler(): MsgHandler;
|
|
783
|
+
private onError;
|
|
784
|
+
/**
|
|
785
|
+
* 退避重连循环(单飞,由 onError 持有)。`while` 取代旧的 `setTimeout(0)`
|
|
786
|
+
* 递归续链 —— 杜绝深栈递归 + 每步都丢弃的定时器 Disposable;每次 sleep 后
|
|
787
|
+
* 重校 cancelled/disposed,sleep 自身可被 cancel/teardown dispose。
|
|
788
|
+
*/
|
|
789
|
+
private reconnectLoop;
|
|
790
|
+
/**
|
|
791
|
+
* L3:可被 {@link clearReconnectSleep} 取消的退避 sleep。dispose 时立即
|
|
792
|
+
* resolve,让 reconnectLoop 醒来重校 cancelled/disposed 后退出 —— 不再留
|
|
793
|
+
* 一个无法清除的延迟回调到 expiry。
|
|
794
|
+
*/
|
|
795
|
+
private sleepReconnect;
|
|
796
|
+
private onIncomeDanmu;
|
|
797
|
+
private onIncomeSuperChat;
|
|
798
|
+
private onGuardBuy;
|
|
799
|
+
private onLiveStart;
|
|
800
|
+
private onLiveEnd;
|
|
801
|
+
private onInteractWordV2;
|
|
802
|
+
}
|
|
803
|
+
//#endregion
|
|
804
|
+
//#region src/listener-manager.d.ts
|
|
805
|
+
/**
|
|
806
|
+
* Constructor options for {@link ListenerManager}. Same shape as
|
|
807
|
+
* {@link RoomContextOptions}; the manager builds its `RoomContext` internally.
|
|
808
|
+
*/
|
|
809
|
+
type ListenerManagerOptions = RoomContextOptions;
|
|
810
|
+
/**
|
|
811
|
+
* Top-level lifecycle for live-room listeners.
|
|
812
|
+
*
|
|
813
|
+
* Owns:
|
|
814
|
+
* - the per-uid {@link SubItemView} registry (mutable clones we update through
|
|
815
|
+
* `bilibili-notify/subscription-changed` ops).
|
|
816
|
+
* - the underlying {@link RoomContext} (which in turn owns the listener
|
|
817
|
+
* record + periodic-timer record and exposes shared helpers consumed by
|
|
818
|
+
* {@link RoomSession}).
|
|
819
|
+
*
|
|
820
|
+
* Per-room state and the dispatcher closure live in {@link RoomSession}; the
|
|
821
|
+
* manager only orchestrates start / stop / clearAll.
|
|
822
|
+
*/
|
|
823
|
+
declare class ListenerManager {
|
|
824
|
+
private readonly ctx;
|
|
825
|
+
private readonly subRecord;
|
|
826
|
+
private readonly sessionRecord;
|
|
827
|
+
constructor(opts: ListenerManagerOptions);
|
|
828
|
+
/** Replace runtime config (called when the adapter receives a config update). */
|
|
829
|
+
updateConfig(config: ListenerManagerConfig): void;
|
|
830
|
+
isDisposed(): boolean;
|
|
831
|
+
getListenerCount(): number;
|
|
832
|
+
/** Active mutable sub by uid (used by `applyOps` to detect existence). */
|
|
833
|
+
getActiveSub(uid: string): SubItemView | undefined;
|
|
834
|
+
/**
|
|
835
|
+
* Read-only live-state snapshot for every monitored room. Each entry pairs
|
|
836
|
+
* the room's `isLive` boolean (mirrors RoomSession.liveStatus) with the
|
|
837
|
+
* latest fetched `liveRoomInfo` fields. Used by the standalone dashboard's
|
|
838
|
+
* `GET /api/live/listening` route to populate the "正在直播" panel.
|
|
839
|
+
*/
|
|
840
|
+
listLiveSnapshots(): ReturnType<RoomSession["getLiveSnapshot"]>[];
|
|
841
|
+
/**
|
|
842
|
+
* `pushTime` 热更后调用:把所有 active session 的"正在直播"复推 timer 按当前
|
|
843
|
+
* `pushTime` 重新 arm。LiveEngine.updateConfig 在检测到变化时转发。
|
|
844
|
+
*/
|
|
845
|
+
rearmAllPeriodicTimers(): void;
|
|
846
|
+
/**
|
|
847
|
+
* 单个 UID 的 pushTime 热更入口。LiveEngine.applyOps 在 update 分支检测到
|
|
848
|
+
* `LiveScopedChange.pushTime` 变化时调用,仅对该 uid 的活跃 session rearm。
|
|
849
|
+
*/
|
|
850
|
+
rearmPeriodicTimerForUid(uid: string): void;
|
|
851
|
+
/** Whether any feature on this sub requires the live-room WS connection. */
|
|
852
|
+
needsLiveMonitor(sub: SubItemView): boolean;
|
|
853
|
+
/** Start listeners for everything in `subs` that needs one. */
|
|
854
|
+
startAll(subs: Record<string, SubItemView>): void;
|
|
855
|
+
/** Start a single sub's listener via a fresh {@link RoomSession}. */
|
|
856
|
+
startForUid(sub: SubItemView, logPrefix?: string): void;
|
|
857
|
+
private bootstrapForUid;
|
|
858
|
+
private resolveRoomId;
|
|
859
|
+
/** Stop a single sub's listener and drop its bookkeeping. */
|
|
860
|
+
stopForUid(uid: string): void;
|
|
861
|
+
/** Tear down everything. Used by engine `stop()` / `auth-lost`. */
|
|
862
|
+
disposeAll(): void;
|
|
863
|
+
/** Tear down listeners + timers but keep registry / disposed flag intact. */
|
|
864
|
+
clearListeners(): void;
|
|
865
|
+
/** Cancel every periodic "正在直播" timer. */
|
|
866
|
+
clearPushTimers(): void;
|
|
867
|
+
}
|
|
868
|
+
//#endregion
|
|
869
|
+
//#region src/live-engine.d.ts
|
|
870
|
+
/**
|
|
871
|
+
* Top-level platform-neutral configuration for {@link LiveEngine}.
|
|
872
|
+
*
|
|
873
|
+
* Mirrors the runtime-relevant subset of `BilibiliNotifyLiveConfig` (the koishi
|
|
874
|
+
* Schema). Adapters translate their native config into this struct.
|
|
875
|
+
*
|
|
876
|
+
* The engine intentionally drops `logLevel` (the adapter sets it on the
|
|
877
|
+
* provided logger before construction) and folds `liveSummary` (originally a
|
|
878
|
+
* `string[]` joined by `\n`) into a single `liveSummaryDefault` string per the
|
|
879
|
+
* plan's §七 "customLiveSummary string vs string[]" cleanup.
|
|
880
|
+
*/
|
|
881
|
+
interface LiveEngineConfig {
|
|
882
|
+
/**
|
|
883
|
+
* Comma-separated additional stop-words appended to the bundled
|
|
884
|
+
* Chinese-stop-word list before tokenisation.
|
|
885
|
+
*/
|
|
886
|
+
wordcloudStopWords?: string;
|
|
887
|
+
/** Hours between periodic "正在直播" pushes; `0` disables. */
|
|
888
|
+
pushTime: number;
|
|
889
|
+
/** Whether to push a "正在直播" card immediately on engine start when a sub is live. */
|
|
890
|
+
restartPush: boolean;
|
|
891
|
+
/** SC minimum-price gate (yuan); SC under this value is dropped. */
|
|
892
|
+
minScPrice: number;
|
|
893
|
+
/**
|
|
894
|
+
* Lowest allowed guard tier to push (1 = governor, 2 = supervisor,
|
|
895
|
+
* 3 = captain — preserves Bilibili semantics).
|
|
896
|
+
*/
|
|
897
|
+
minGuardLevel: 1 | 2 | 3;
|
|
898
|
+
/** Default global "弹幕总结" template (single string; adapter joins lines if needed). */
|
|
899
|
+
liveSummaryDefault: string;
|
|
900
|
+
customGuardBuy: ListenerManagerConfig["customGuardBuy"];
|
|
901
|
+
customLiveMsg: ListenerManagerConfig["customLiveMsg"];
|
|
902
|
+
/**
|
|
903
|
+
* 是否启用图片卡片渲染。`false` 时直播开播 / SC / 上舰 / 弹幕词云全部走文字回退。
|
|
904
|
+
* 缺省视为 true。Adapter 通常用 `globals.defaults.cardStyle.enabled` 填充。
|
|
905
|
+
*/
|
|
906
|
+
imageEnabled?: boolean;
|
|
907
|
+
/**
|
|
908
|
+
* 是否启用 AI 直播总结。`false` 时跳过 commentary 调用,直接走模板回退。
|
|
909
|
+
* 缺省视为 true。Adapter 通常用 `globals.defaults.ai.enabled` 填充。
|
|
910
|
+
*/
|
|
911
|
+
aiEnabled?: boolean;
|
|
912
|
+
}
|
|
913
|
+
interface LiveEngineOptions {
|
|
914
|
+
serviceCtx: ServiceContext;
|
|
915
|
+
api: BilibiliAPI;
|
|
916
|
+
push: PushLike;
|
|
917
|
+
contentBuilder: LiveContentBuilder;
|
|
918
|
+
/** Optional — if absent, image-based pushes are skipped / fall back to text. */
|
|
919
|
+
imageRenderer?: ImageRenderer | null;
|
|
920
|
+
/** Optional — if absent, live summaries fall back to the configured template. */
|
|
921
|
+
commentary?: CommentaryGenerator | null;
|
|
922
|
+
config: LiveEngineConfig;
|
|
923
|
+
/**
|
|
924
|
+
* Called by the engine to surface an `engine-error` to the host. Adapters
|
|
925
|
+
* forward this to their MessageBus / koishi `ctx.emit('bilibili-notify/engine-error')`.
|
|
926
|
+
*/
|
|
927
|
+
emitEngineError: (message: string) => void;
|
|
928
|
+
/**
|
|
929
|
+
* Optional — adapter pipe for per-UID live-state transitions. Adapter forwards
|
|
930
|
+
* to `bus.emit("live-state-changed", uid, status)`. When absent the engine
|
|
931
|
+
* runs without state broadcasts (koishi shell may opt out if it has nothing
|
|
932
|
+
* subscribing).
|
|
933
|
+
*/
|
|
934
|
+
emitLiveState?: (uid: string, status: "live" | "idle") => void;
|
|
935
|
+
/**
|
|
936
|
+
* Optional — adapter pipe for per-UID watched-count updates. Adapter forwards
|
|
937
|
+
* to `bus.emit("live-viewers-changed", uid, viewers)`. Throttled per-UID to
|
|
938
|
+
* 2s at the room-session boundary.
|
|
939
|
+
*/
|
|
940
|
+
emitViewers?: (uid: string, viewers: string) => void;
|
|
941
|
+
}
|
|
942
|
+
/**
|
|
943
|
+
* Platform-neutral live-monitoring engine. Wires the five helpers
|
|
944
|
+
* (listener-manager / danmaku-collector / wordcloud-generator /
|
|
945
|
+
* template-renderer / live-summary-requester) together and exposes the public
|
|
946
|
+
* surface previously offered by the koishi `BilibiliNotifyLive` service.
|
|
947
|
+
*
|
|
948
|
+
* Lifecycle:
|
|
949
|
+
*
|
|
950
|
+
* - {@link start}: register subscription set, open listeners for those that need them.
|
|
951
|
+
* - {@link applyOps}: incremental subscription delta (add / delete / update);
|
|
952
|
+
* adapter forwards `bilibili-notify/subscription-changed` events here.
|
|
953
|
+
* - {@link rebuildFromSubs}: full rebootstrap (used after `auth-restored`).
|
|
954
|
+
* - {@link teardown}: tear down all listeners + records (used on `auth-lost`).
|
|
955
|
+
* - {@link stop}: dispose; called by the adapter on plugin disposal.
|
|
956
|
+
*/
|
|
957
|
+
declare class LiveEngine {
|
|
958
|
+
private readonly logger;
|
|
959
|
+
private readonly listener;
|
|
960
|
+
private readonly danmakuCollector;
|
|
961
|
+
private readonly liveSummaryRequester;
|
|
962
|
+
private config;
|
|
963
|
+
constructor(opts: LiveEngineOptions);
|
|
964
|
+
/**
|
|
965
|
+
* Bootstrap the engine with the initial subscription set. Idempotent —
|
|
966
|
+
* calling it again replaces the active set (used by `auth-restored`).
|
|
967
|
+
*/
|
|
968
|
+
start(subs: SubscriptionsView): void;
|
|
969
|
+
/** Tear down all listeners + per-room state, leaving the engine instance reusable. */
|
|
970
|
+
teardown(): void;
|
|
971
|
+
/** Full rebootstrap. Used after auth-restored. */
|
|
972
|
+
rebuildFromSubs(subs: SubscriptionsView): void;
|
|
973
|
+
/**
|
|
974
|
+
* Apply incremental subscription ops (the adapter receives these as a
|
|
975
|
+
* `bilibili-notify/subscription-changed` event payload). Handles the same
|
|
976
|
+
* three cases as the original live-service: add / delete / update.
|
|
977
|
+
*/
|
|
978
|
+
applyOps(ops: LiveSubscriptionOp[], lookupFullSub: (uid: string) => SubItemView | undefined): void;
|
|
979
|
+
/** Replace runtime config (called when the adapter receives a config-changed event). */
|
|
980
|
+
updateConfig(config: LiveEngineConfig): void;
|
|
981
|
+
/**
|
|
982
|
+
* 热替换 CommentaryGenerator 实例。adapter 在用户运行时打开 / 关闭 / 更换 AI
|
|
983
|
+
* 配置后调用,引擎随后的直播总结会立即用新实例 (或回退到模板) ,无需重启 server。
|
|
984
|
+
*/
|
|
985
|
+
setCommentary(commentary: CommentaryGenerator | null): void;
|
|
986
|
+
/** Final dispose; the engine instance must not be reused after this. */
|
|
987
|
+
stop(): void;
|
|
988
|
+
/** Diagnostic accessor, used by the koishi shell for `[conn] state` logging. */
|
|
989
|
+
get listenerCount(): number;
|
|
990
|
+
/**
|
|
991
|
+
* Per-room live-state snapshot for every active monitor. Routes / dashboards
|
|
992
|
+
* filter on `isLive` to show "正在直播" panels.
|
|
993
|
+
*/
|
|
994
|
+
listLiveSnapshots(): ReturnType<ListenerManager["listLiveSnapshots"]>;
|
|
995
|
+
/** Read-only view of the engine config (for the koishi shell to pass through). */
|
|
996
|
+
getConfig(): LiveEngineConfig;
|
|
997
|
+
}
|
|
998
|
+
//#endregion
|
|
999
|
+
//#region src/stop-words.d.ts
|
|
1000
|
+
declare const stopwords: Set<string>;
|
|
1001
|
+
//#endregion
|
|
1002
|
+
export { type CustomCardStyleLike, type CustomGuardBuyLike, type CustomLiveMsgLike, type CustomLiveSummaryLike, type CustomSpecialDanmakuUsersLike, type CustomSpecialUsersEnterTheRoomLike, DEFAULT_LIVE_TEMPLATES, DanmakuCollector, type DynamicScopedChange, LIVE_EVENT_COOLDOWN, LIVE_ROOM_MASTER_KEYS, LIVE_SUMMARY_MIN_SENDERS, ListenerManager, type ListenerManagerConfig, type ListenerManagerOptions, type LiveContentBuilder, type LiveData, LiveEngine, type LiveEngineConfig, type LiveEngineOptions, type LiveMasterFeature, type LivePushFeature, type LivePushTimerManager, LivePushType, type LiveScopedChange, type LiveSubChange, type LiveSubscriptionOp, LiveSummaryRequester, LiveTemplateRenderer, LiveType, type MasterInfo, type PushLike, RoomContext, RoomContextBase, type RoomContextOptions, RoomSession, RoomSessionBase, type SubItemTargetLike, type SubItemView, type SubscriptionsView, type TargetScopedChange, type UserInfoInLiveData, WORDCLOUD_MIN_WORDS, WORDCLOUD_TOP_WORDS, WordcloudGenerator, buildRoomLink, stopwords as defaultStopWords, formatFollowerChange, formatFollowerCount };
|