@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.
@@ -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 };