@bilibili-notify/live 0.0.1-alpha.3 → 0.1.0-alpha.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.cjs +170 -64
- package/lib/index.d.cts +34 -9
- package/lib/index.d.mts +34 -9
- package/lib/index.mjs +170 -64
- package/package.json +4 -4
package/lib/index.cjs
CHANGED
|
@@ -185,6 +185,8 @@ var RoomContextBase = class {
|
|
|
185
185
|
listenerRecord = {};
|
|
186
186
|
livePushTimerManager = /* @__PURE__ */ new Map();
|
|
187
187
|
disposed = false;
|
|
188
|
+
/** stopMonitoring 主动关闭 listener 时置位;RoomSession.onClose 消费后不做自愈重连。 */
|
|
189
|
+
intentionalCloseRooms = /* @__PURE__ */ new Set();
|
|
188
190
|
/** Cached protobuf type for INTERACT_WORD_V2 decoding (lazy-loaded). */
|
|
189
191
|
interactWord;
|
|
190
192
|
/**
|
|
@@ -222,6 +224,11 @@ var RoomContextBase = class {
|
|
|
222
224
|
emitViewers(uid, viewers) {
|
|
223
225
|
this._emitViewers?.(uid, viewers);
|
|
224
226
|
}
|
|
227
|
+
consumeIntentionalClose(roomId) {
|
|
228
|
+
const hit = this.intentionalCloseRooms.has(roomId);
|
|
229
|
+
this.intentionalCloseRooms.delete(roomId);
|
|
230
|
+
return hit;
|
|
231
|
+
}
|
|
225
232
|
/** 受 `config.imageEnabled` 门控的渲染器视图;关闭时返回 null。 */
|
|
226
233
|
get imageRenderer() {
|
|
227
234
|
return this.config.imageEnabled === false ? null : this._getImageRenderer();
|
|
@@ -253,10 +260,12 @@ var RoomContextBase = class {
|
|
|
253
260
|
closeListener(roomId) {
|
|
254
261
|
const listener = this.listenerRecord[roomId];
|
|
255
262
|
if (!listener) {
|
|
263
|
+
this.intentionalCloseRooms.delete(roomId);
|
|
256
264
|
this.logger.debug(`[conn] 直播间 [${roomId}] 连接不存在,跳过关闭`);
|
|
257
265
|
return;
|
|
258
266
|
}
|
|
259
267
|
if (listener.closed) {
|
|
268
|
+
this.intentionalCloseRooms.delete(roomId);
|
|
260
269
|
this.logger.debug(`[conn] 直播间 [${roomId}] 连接已被远端断开`);
|
|
261
270
|
delete this.listenerRecord[roomId];
|
|
262
271
|
return;
|
|
@@ -280,6 +289,7 @@ var RoomContextBase = class {
|
|
|
280
289
|
stopMonitoring(reason, roomId) {
|
|
281
290
|
if (roomId) {
|
|
282
291
|
this.logger.error(`[conn] [${roomId}] ${reason},已停止该房间的监测`);
|
|
292
|
+
this.intentionalCloseRooms.add(roomId);
|
|
283
293
|
this.closeListener(roomId);
|
|
284
294
|
const timer = this.livePushTimerManager.get(roomId);
|
|
285
295
|
if (timer) {
|
|
@@ -337,6 +347,7 @@ var RoomContext = class extends RoomContextBase {
|
|
|
337
347
|
this.logger.warn(`[conn] 直播间 [${roomId}] 连接已存在,跳过创建`);
|
|
338
348
|
return true;
|
|
339
349
|
}
|
|
350
|
+
this.consumeIntentionalClose(roomId);
|
|
340
351
|
const cookiesStr = this.api.getCookiesHeader();
|
|
341
352
|
let mySelfInfo;
|
|
342
353
|
try {
|
|
@@ -499,37 +510,41 @@ var RoomContext = class extends RoomContextBase {
|
|
|
499
510
|
* - `customSpecialDanmakuUsers.msgTemplate`
|
|
500
511
|
* - `customSpecialUsersEnterTheRoom.msgTemplate`
|
|
501
512
|
*
|
|
502
|
-
*
|
|
503
|
-
*
|
|
504
|
-
*
|
|
505
|
-
*
|
|
513
|
+
* 占位符统一 `{name}` / `{time}` / `{watched}` 语法(与 `@bilibili-notify/internal`
|
|
514
|
+
* 的 `interpolate` 同源)。`applyTemplate` 同时接受 koishi 旧存档里的 legacy
|
|
515
|
+
* `-name` / `-time` 写法 —— 老用户已保存的 `-key` 模板继续生效,新默认与文档
|
|
516
|
+
* 一律走 `{key}`,二者不冲突(单遍正则,longest-first)。
|
|
506
517
|
*/
|
|
507
518
|
/** Defaults applied when neither sub-level nor global config provides a template. */
|
|
508
519
|
const DEFAULT_LIVE_TEMPLATES = {
|
|
509
|
-
liveStart: "
|
|
510
|
-
liveOngoing: "
|
|
511
|
-
liveEnd: "
|
|
520
|
+
liveStart: "{name} 开播啦,当前粉丝数:{follower}\n{link}",
|
|
521
|
+
liveOngoing: "{name} 正在直播,已播 {time},累计观看:{watched}\n{link}",
|
|
522
|
+
liveEnd: "{name} 下播啦,本次直播了 {time},粉丝变化 {follower_change}",
|
|
512
523
|
liveSummaryFallback: "弹幕总结"
|
|
513
524
|
};
|
|
514
525
|
function escapeRegExp(s) {
|
|
515
526
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
516
527
|
}
|
|
517
528
|
/**
|
|
518
|
-
* 单遍替换所有变量 token,再把 `\n`
|
|
529
|
+
* 单遍替换所有变量 token,再把 `\n` 转义展开为真换行。`vars` 以**裸键**(`name`/
|
|
530
|
+
* `follower_change`)给出,每个键同时匹配 `{name}`(主)与 legacy `-name`(兼容)。
|
|
519
531
|
*
|
|
520
532
|
* P2:此前 `for…replaceAll` 顺序替换有两个缺陷 ——
|
|
521
|
-
* 1. **token 注入**:用户可控值(uname / 弹幕内容)含
|
|
533
|
+
* 1. **token 注入**:用户可控值(uname / 弹幕内容)含 `{link}`/`-link` 时
|
|
522
534
|
* 会被后续轮次再次替换;
|
|
523
|
-
* 2.
|
|
535
|
+
* 2. **前缀吞噬**:legacy `-follower` 先于 `-follower_change` 替换,把后者的
|
|
524
536
|
* `-follower` 段吃掉只剩 `_change`。
|
|
525
|
-
*
|
|
526
|
-
*
|
|
537
|
+
* 改为基于原始模板的**单遍正则**:键按长度降序进 alternation(最长优先匹配),
|
|
538
|
+
* 每个 token 恰好替换一次且替换值不再被回扫 → 杜绝注入与吞噬。
|
|
527
539
|
*/
|
|
528
540
|
function applyTemplate(template, vars) {
|
|
529
541
|
const keys = Object.keys(vars).sort((a, b) => b.length - a.length);
|
|
530
542
|
if (keys.length === 0) return template.replaceAll("\\n", "\n");
|
|
531
|
-
const
|
|
532
|
-
|
|
543
|
+
const alts = keys.flatMap((k) => [`\\{${escapeRegExp(k)}\\}`, `-${escapeRegExp(k)}`]);
|
|
544
|
+
const re = new RegExp(alts.join("|"), "g");
|
|
545
|
+
return template.replace(re, (m) => {
|
|
546
|
+
return vars[m.charCodeAt(0) === 123 ? m.slice(1, -1) : m.slice(1)] ?? m;
|
|
547
|
+
}).replaceAll("\\n", "\n");
|
|
533
548
|
}
|
|
534
549
|
/**
|
|
535
550
|
* Format follower-change as a signed magnitude string with a 1万 (10K) cutoff,
|
|
@@ -559,27 +574,27 @@ var LiveTemplateRenderer = class {
|
|
|
559
574
|
/** Compose the "开播" notification text for a sub. */
|
|
560
575
|
renderLiveStart(params) {
|
|
561
576
|
return applyTemplate(resolveCustomLive(params.sub.customLiveMsg, params.globalCustom, "customLiveStart", DEFAULT_LIVE_TEMPLATES.liveStart), {
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
577
|
+
name: params.master.username,
|
|
578
|
+
time: params.diffTime,
|
|
579
|
+
follower: params.followerNum,
|
|
580
|
+
link: params.roomLink
|
|
566
581
|
});
|
|
567
582
|
}
|
|
568
583
|
/** Compose the periodic "正在直播" notification text. */
|
|
569
584
|
renderLiveOngoing(params) {
|
|
570
585
|
return applyTemplate(resolveCustomLive(params.sub.customLiveMsg, params.globalCustom, "customLive", DEFAULT_LIVE_TEMPLATES.liveOngoing), {
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
586
|
+
name: params.master.username,
|
|
587
|
+
time: params.diffTime,
|
|
588
|
+
watched: params.watched,
|
|
589
|
+
link: params.roomLink
|
|
575
590
|
});
|
|
576
591
|
}
|
|
577
592
|
/** Compose the "下播" notification text. */
|
|
578
593
|
renderLiveEnd(params) {
|
|
579
594
|
return applyTemplate(resolveCustomLive(params.sub.customLiveMsg, params.globalCustom, "customLiveEnd", DEFAULT_LIVE_TEMPLATES.liveEnd), {
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
595
|
+
name: params.master.username,
|
|
596
|
+
time: params.diffTime,
|
|
597
|
+
follower_change: formatFollowerChange(params.followerChange)
|
|
583
598
|
});
|
|
584
599
|
}
|
|
585
600
|
/**
|
|
@@ -588,24 +603,24 @@ var LiveTemplateRenderer = class {
|
|
|
588
603
|
*/
|
|
589
604
|
renderGuardBuy(params) {
|
|
590
605
|
return applyTemplate(params.guardBuyConfig.guardBuyMsg ?? "", {
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
606
|
+
uname: params.uname,
|
|
607
|
+
mname: params.master?.username ?? "",
|
|
608
|
+
guard: params.giftName
|
|
594
609
|
});
|
|
595
610
|
}
|
|
596
611
|
/** Compose the "特别关注弹幕" notification text. */
|
|
597
612
|
renderSpecialDanmaku(params) {
|
|
598
613
|
return applyTemplate(params.template, {
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
614
|
+
mastername: params.master?.username ?? "",
|
|
615
|
+
uname: params.uname,
|
|
616
|
+
msg: params.content
|
|
602
617
|
});
|
|
603
618
|
}
|
|
604
619
|
/** Compose the "特别关注进入直播间" notification text. */
|
|
605
620
|
renderSpecialUserEnter(params) {
|
|
606
621
|
return applyTemplate(params.template, {
|
|
607
|
-
|
|
608
|
-
|
|
622
|
+
mastername: params.master?.username ?? "",
|
|
623
|
+
uname: params.uname
|
|
609
624
|
});
|
|
610
625
|
}
|
|
611
626
|
/**
|
|
@@ -618,19 +633,19 @@ var LiveTemplateRenderer = class {
|
|
|
618
633
|
const top = params.topSenders;
|
|
619
634
|
const at = (i) => top[i] ?? ["", 0];
|
|
620
635
|
return applyTemplate(params.template, {
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
636
|
+
dmc: `${params.senderCount}`,
|
|
637
|
+
mdn: params.master?.medalName ?? "",
|
|
638
|
+
dca: `${params.danmakuCount}`,
|
|
639
|
+
un1: at(0)[0],
|
|
640
|
+
dc1: `${at(0)[1]}`,
|
|
641
|
+
un2: at(1)[0],
|
|
642
|
+
dc2: `${at(1)[1]}`,
|
|
643
|
+
un3: at(2)[0],
|
|
644
|
+
dc3: `${at(2)[1]}`,
|
|
645
|
+
un4: at(3)[0],
|
|
646
|
+
dc4: `${at(3)[1]}`,
|
|
647
|
+
un5: at(4)[0],
|
|
648
|
+
dc5: `${at(4)[1]}`
|
|
634
649
|
});
|
|
635
650
|
}
|
|
636
651
|
};
|
|
@@ -707,14 +722,17 @@ var RoomSessionBase = class {
|
|
|
707
722
|
async bootstrap() {
|
|
708
723
|
if (!await this.ctx.startLiveRoomListener(this.sub.roomId, this.buildHandler())) {
|
|
709
724
|
await this.ctx.push.sendPrivateMsg(`直播间 [${this.sub.roomId}] 弹幕连接建立失败,已停止该房间监测`);
|
|
725
|
+
this.onMonitoringStopped();
|
|
710
726
|
this.ctx.closeListener(this.sub.roomId);
|
|
711
727
|
return;
|
|
712
728
|
}
|
|
713
729
|
if (!await this.useLiveRoomInfo(4) || !await this.useMasterInfo(4) || !this.liveRoomInfo || !this.masterInfo) {
|
|
714
730
|
await this.ctx.push.sendPrivateMsg("获取直播间信息失败,启动直播间弹幕检测失败");
|
|
731
|
+
this.onMonitoringStopped();
|
|
715
732
|
this.ctx.closeListener(this.sub.roomId);
|
|
716
733
|
return;
|
|
717
734
|
}
|
|
735
|
+
this.onListenerStarted();
|
|
718
736
|
this.ctx.logger.debug(`[stat] 当前粉丝数:${this.masterInfo.liveOpenFollowerNum}`);
|
|
719
737
|
if (this.liveRoomInfo.live_status === 1) {
|
|
720
738
|
this.liveTime = this.liveRoomInfo.live_time;
|
|
@@ -743,6 +761,10 @@ var RoomSessionBase = class {
|
|
|
743
761
|
this.armPeriodicTimer();
|
|
744
762
|
}
|
|
745
763
|
}
|
|
764
|
+
/** Hook for subclass-owned connection-health bookkeeping after listener bootstrap succeeds. */
|
|
765
|
+
onListenerStarted() {}
|
|
766
|
+
/** Hook for subclass-owned cleanup before this session intentionally stops monitoring. */
|
|
767
|
+
onMonitoringStopped() {}
|
|
746
768
|
async useLiveRoomInfo(liveType) {
|
|
747
769
|
const data = await this.ctx.getLiveRoomInfo(this.sub.roomId);
|
|
748
770
|
if (!data?.uid) return false;
|
|
@@ -792,6 +814,7 @@ var RoomSessionBase = class {
|
|
|
792
814
|
/** Periodic "正在直播" tick (callback for `setInterval`). */
|
|
793
815
|
async tickPushAtTime() {
|
|
794
816
|
if (!await this.useLiveRoomInfo(2) || !this.liveRoomInfo) {
|
|
817
|
+
this.onMonitoringStopped();
|
|
795
818
|
this.ctx.stopMonitoring("获取直播间信息失败,推送直播卡片失败", this.sub.roomId);
|
|
796
819
|
return;
|
|
797
820
|
}
|
|
@@ -842,6 +865,7 @@ var RoomSessionBase = class {
|
|
|
842
865
|
this.setLiveStatus(false);
|
|
843
866
|
this.ctx.danmakuCollector.clear(this.sub.roomId);
|
|
844
867
|
if (this.ctx.isDisposed()) return;
|
|
868
|
+
this.onMonitoringStopped();
|
|
845
869
|
this.ctx.stopMonitoring("获取直播间信息失败,推送直播下播卡片失败", this.sub.roomId);
|
|
846
870
|
return;
|
|
847
871
|
}
|
|
@@ -926,6 +950,8 @@ const RECONNECT_BACKOFF_MS = [
|
|
|
926
950
|
8e3,
|
|
927
951
|
16e3
|
|
928
952
|
];
|
|
953
|
+
/** B 站 live WS 静默自愈:每分钟检查一次,3 分钟无 heartbeat/消息即主动重连。 */
|
|
954
|
+
const LIVE_WS_WATCHDOG_INTERVAL_MS = 6e4;
|
|
929
955
|
var RoomSession = class extends RoomSessionBase {
|
|
930
956
|
lastViewersEmitMs = 0;
|
|
931
957
|
/**
|
|
@@ -944,10 +970,15 @@ var RoomSession = class extends RoomSessionBase {
|
|
|
944
970
|
/** L3:退避 sleep 的 Disposable + 唤醒句柄,cancel/teardown 时清掉,不留回调到 expiry。 */
|
|
945
971
|
reconnectTimer;
|
|
946
972
|
reconnectWake;
|
|
947
|
-
|
|
973
|
+
lastLiveWsActivityAt = 0;
|
|
974
|
+
lastLiveWsActivityReason = "connected";
|
|
975
|
+
watchdogTimer;
|
|
976
|
+
watchdogReconnectCount = 0;
|
|
977
|
+
/** 外层主动停止 listener 时调用,阻止 onError/onClose/watchdog 触发重连。 */
|
|
948
978
|
cancel() {
|
|
949
979
|
this.cancelled = true;
|
|
950
980
|
this.reconnecting = false;
|
|
981
|
+
this.stopLiveWsWatchdog();
|
|
951
982
|
this.clearReconnectSleep();
|
|
952
983
|
}
|
|
953
984
|
/** L3:dispose 退避定时器并唤醒重连循环,使其立刻重校 cancelled/disposed 后退出。 */
|
|
@@ -957,12 +988,62 @@ var RoomSession = class extends RoomSessionBase {
|
|
|
957
988
|
this.reconnectWake?.();
|
|
958
989
|
this.reconnectWake = void 0;
|
|
959
990
|
}
|
|
991
|
+
onListenerStarted() {
|
|
992
|
+
this.markLiveWsActivity("connected");
|
|
993
|
+
this.startLiveWsWatchdog();
|
|
994
|
+
}
|
|
995
|
+
onMonitoringStopped() {
|
|
996
|
+
this.cancel();
|
|
997
|
+
}
|
|
998
|
+
getWsHealthSnapshot() {
|
|
999
|
+
return {
|
|
1000
|
+
lastActivityAt: this.lastLiveWsActivityAt,
|
|
1001
|
+
lastActivityReason: this.lastLiveWsActivityReason,
|
|
1002
|
+
watchdogReconnectCount: this.watchdogReconnectCount
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
markLiveWsActivity(reason) {
|
|
1006
|
+
this.lastLiveWsActivityAt = Date.now();
|
|
1007
|
+
this.lastLiveWsActivityReason = reason;
|
|
1008
|
+
}
|
|
1009
|
+
startLiveWsWatchdog() {
|
|
1010
|
+
if (this.watchdogTimer || this.cancelled || this.ctx.isDisposed()) return;
|
|
1011
|
+
this.watchdogTimer = this.ctx.serviceCtx.setInterval(() => this.checkLiveWsWatchdog(), LIVE_WS_WATCHDOG_INTERVAL_MS);
|
|
1012
|
+
}
|
|
1013
|
+
stopLiveWsWatchdog() {
|
|
1014
|
+
this.watchdogTimer?.dispose();
|
|
1015
|
+
this.watchdogTimer = void 0;
|
|
1016
|
+
}
|
|
1017
|
+
checkLiveWsWatchdog() {
|
|
1018
|
+
if (this.cancelled || this.ctx.isDisposed() || this.reconnecting) return;
|
|
1019
|
+
if (this.lastLiveWsActivityAt <= 0) return;
|
|
1020
|
+
const staleMs = Date.now() - this.lastLiveWsActivityAt;
|
|
1021
|
+
if (staleMs < 18e4) return;
|
|
1022
|
+
this.watchdogReconnectCount++;
|
|
1023
|
+
this.reconnect("watchdog", `${Math.floor(staleMs / 1e3)}s 无 heartbeat/消息(last=${this.lastLiveWsActivityReason},watchdog=${this.watchdogReconnectCount})`);
|
|
1024
|
+
}
|
|
960
1025
|
buildHandler() {
|
|
961
1026
|
const base = {
|
|
1027
|
+
onOpen: () => this.markLiveWsActivity("open"),
|
|
1028
|
+
onStartListen: () => this.markLiveWsActivity("start-listen"),
|
|
1029
|
+
onClose: () => {
|
|
1030
|
+
if (this.cancelled || this.ctx.isDisposed()) return;
|
|
1031
|
+
if (this.ctx.consumeIntentionalClose(this.sub.roomId)) return;
|
|
1032
|
+
this.markLiveWsActivity("close");
|
|
1033
|
+
this.reconnect("close");
|
|
1034
|
+
},
|
|
962
1035
|
onError: () => this.onError(),
|
|
963
|
-
|
|
964
|
-
|
|
1036
|
+
onAttentionChange: () => this.markLiveWsActivity("heartbeat"),
|
|
1037
|
+
onIncomeDanmu: ({ body }) => {
|
|
1038
|
+
this.markLiveWsActivity("danmu");
|
|
1039
|
+
this.onIncomeDanmu(body);
|
|
1040
|
+
},
|
|
1041
|
+
onIncomeSuperChat: ({ body }) => {
|
|
1042
|
+
this.markLiveWsActivity("superchat");
|
|
1043
|
+
return this.onIncomeSuperChat(body);
|
|
1044
|
+
},
|
|
965
1045
|
onWatchedChange: ({ body }) => {
|
|
1046
|
+
this.markLiveWsActivity("watched");
|
|
966
1047
|
this.liveData.watchedNum = body.text_small;
|
|
967
1048
|
const now = Date.now();
|
|
968
1049
|
if (now - this.lastViewersEmitMs >= VIEWERS_EMIT_THROTTLE_MS) {
|
|
@@ -971,42 +1052,61 @@ var RoomSession = class extends RoomSessionBase {
|
|
|
971
1052
|
}
|
|
972
1053
|
},
|
|
973
1054
|
onLikedChange: ({ body }) => {
|
|
1055
|
+
this.markLiveWsActivity("liked");
|
|
974
1056
|
this.liveData.likedNum = body.count;
|
|
975
1057
|
},
|
|
976
|
-
onGuardBuy: ({ body }) =>
|
|
977
|
-
|
|
978
|
-
|
|
1058
|
+
onGuardBuy: ({ body }) => {
|
|
1059
|
+
this.markLiveWsActivity("guard");
|
|
1060
|
+
return this.onGuardBuy(body);
|
|
1061
|
+
},
|
|
1062
|
+
onLiveStart: () => {
|
|
1063
|
+
this.markLiveWsActivity("live-start");
|
|
1064
|
+
return this.onLiveStart();
|
|
1065
|
+
},
|
|
1066
|
+
onLiveEnd: () => {
|
|
1067
|
+
this.markLiveWsActivity("live-end");
|
|
1068
|
+
return this.onLiveEnd();
|
|
1069
|
+
}
|
|
979
1070
|
};
|
|
980
1071
|
if (!this.sub.customSpecialUsersEnterTheRoom.enable) return base;
|
|
981
1072
|
return {
|
|
982
1073
|
...base,
|
|
983
|
-
raw: { INTERACT_WORD_V2: (msg) =>
|
|
1074
|
+
raw: { INTERACT_WORD_V2: (msg) => {
|
|
1075
|
+
this.markLiveWsActivity("interact");
|
|
1076
|
+
return this.onInteractWordV2(msg);
|
|
1077
|
+
} }
|
|
984
1078
|
};
|
|
985
1079
|
}
|
|
986
|
-
|
|
1080
|
+
onError() {
|
|
1081
|
+
return this.reconnect("error");
|
|
1082
|
+
}
|
|
1083
|
+
async reconnect(reason, detail) {
|
|
987
1084
|
if (this.cancelled || this.ctx.isDisposed()) return;
|
|
988
1085
|
if (this.reconnecting) return;
|
|
989
1086
|
this.reconnecting = true;
|
|
990
1087
|
try {
|
|
991
|
-
await this.reconnectLoop();
|
|
1088
|
+
await this.reconnectLoop(reason, detail);
|
|
992
1089
|
} finally {
|
|
993
1090
|
this.reconnecting = false;
|
|
994
1091
|
}
|
|
995
1092
|
}
|
|
996
1093
|
/**
|
|
997
|
-
* 退避重连循环(单飞,由
|
|
1094
|
+
* 退避重连循环(单飞,由 reconnect 持有)。`while` 取代旧的 `setTimeout(0)`
|
|
998
1095
|
* 递归续链 —— 杜绝深栈递归 + 每步都丢弃的定时器 Disposable;每次 sleep 后
|
|
999
1096
|
* 重校 cancelled/disposed,sleep 自身可被 cancel/teardown dispose。
|
|
1000
1097
|
*/
|
|
1001
|
-
async reconnectLoop() {
|
|
1098
|
+
async reconnectLoop(reason, detail) {
|
|
1002
1099
|
while (this.reconnectAttempts < RECONNECT_BACKOFF_MS.length) {
|
|
1003
1100
|
if (this.cancelled || this.ctx.isDisposed()) return;
|
|
1004
|
-
|
|
1005
|
-
|
|
1101
|
+
if (reason === "error") {
|
|
1102
|
+
this.setLiveStatus(false);
|
|
1103
|
+
this.cancelPeriodicTimer();
|
|
1104
|
+
}
|
|
1006
1105
|
this.ctx.closeListener(this.sub.roomId);
|
|
1007
1106
|
const delay = RECONNECT_BACKOFF_MS[this.reconnectAttempts];
|
|
1008
1107
|
this.reconnectAttempts++;
|
|
1009
|
-
|
|
1108
|
+
const reasonText = this.describeReconnectReason(reason, detail);
|
|
1109
|
+
this.ctx.logger.warn(`[conn] 直播间 [${this.sub.roomId}] ${reasonText},${delay / 1e3}s 后重连(第 ${this.reconnectAttempts}/${RECONNECT_BACKOFF_MS.length} 次)`);
|
|
1010
1110
|
await this.sleepReconnect(delay);
|
|
1011
1111
|
if (this.cancelled || this.ctx.isDisposed()) return;
|
|
1012
1112
|
let ok = false;
|
|
@@ -1020,6 +1120,7 @@ var RoomSession = class extends RoomSessionBase {
|
|
|
1020
1120
|
return;
|
|
1021
1121
|
}
|
|
1022
1122
|
if (ok) {
|
|
1123
|
+
this.onListenerStarted();
|
|
1023
1124
|
this.ctx.logger.info(`[conn] 直播间 [${this.sub.roomId}] 重连成功`);
|
|
1024
1125
|
this.reconnectAttempts = 0;
|
|
1025
1126
|
return;
|
|
@@ -1027,9 +1128,15 @@ var RoomSession = class extends RoomSessionBase {
|
|
|
1027
1128
|
this.ctx.logger.warn(`[conn] 直播间 [${this.sub.roomId}] 重连未成功,继续退避`);
|
|
1028
1129
|
}
|
|
1029
1130
|
this.reconnectAttempts = 0;
|
|
1030
|
-
const msg = `直播间 [${this.sub.roomId}]
|
|
1131
|
+
const msg = `直播间 [${this.sub.roomId}] ${this.describeReconnectReason(reason, detail)}后连接持续失败,重试 ${RECONNECT_BACKOFF_MS.length} 次后放弃监听`;
|
|
1031
1132
|
this.ctx.logger.error(`[conn] ${msg}`);
|
|
1032
1133
|
this.ctx.emitEngineError(msg);
|
|
1134
|
+
this.cancel();
|
|
1135
|
+
}
|
|
1136
|
+
describeReconnectReason(reason, detail) {
|
|
1137
|
+
if (reason === "error") return "连接错误";
|
|
1138
|
+
if (reason === "close") return "连接关闭";
|
|
1139
|
+
return detail ? `连接静默(${detail})` : "连接静默";
|
|
1033
1140
|
}
|
|
1034
1141
|
/**
|
|
1035
1142
|
* L3:可被 {@link clearReconnectSleep} 取消的退避 sleep。dispose 时立即
|
|
@@ -1152,6 +1259,7 @@ var RoomSession = class extends RoomSessionBase {
|
|
|
1152
1259
|
if (!await this.useLiveRoomInfo(1) || !await this.useMasterInfo(1) || !this.liveRoomInfo || !this.masterInfo) {
|
|
1153
1260
|
this.setLiveStatus(false);
|
|
1154
1261
|
if (this.ctx.isDisposed()) return;
|
|
1262
|
+
this.onMonitoringStopped();
|
|
1155
1263
|
this.ctx.stopMonitoring("获取直播间信息失败,推送直播开播卡片失败", this.sub.roomId);
|
|
1156
1264
|
return;
|
|
1157
1265
|
}
|
|
@@ -1693,9 +1801,7 @@ var LiveEngine = class {
|
|
|
1693
1801
|
/** Tear down all listeners + per-room state, leaving the engine instance reusable. */
|
|
1694
1802
|
teardown() {
|
|
1695
1803
|
this.logger.info("[live] 关闭所有直播间监听");
|
|
1696
|
-
this.listener.
|
|
1697
|
-
this.listener.clearListeners();
|
|
1698
|
-
this.danmakuCollector.clearAll();
|
|
1804
|
+
this.listener.disposeAll();
|
|
1699
1805
|
}
|
|
1700
1806
|
/** Full rebootstrap. Used after auth-restored. */
|
|
1701
1807
|
rebuildFromSubs(subs) {
|
package/lib/index.d.cts
CHANGED
|
@@ -274,16 +274,16 @@ type LivePushTimerManager = Map<string, () => void>;
|
|
|
274
274
|
* - `customSpecialDanmakuUsers.msgTemplate`
|
|
275
275
|
* - `customSpecialUsersEnterTheRoom.msgTemplate`
|
|
276
276
|
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
277
|
+
* 占位符统一 `{name}` / `{time}` / `{watched}` 语法(与 `@bilibili-notify/internal`
|
|
278
|
+
* 的 `interpolate` 同源)。`applyTemplate` 同时接受 koishi 旧存档里的 legacy
|
|
279
|
+
* `-name` / `-time` 写法 —— 老用户已保存的 `-key` 模板继续生效,新默认与文档
|
|
280
|
+
* 一律走 `{key}`,二者不冲突(单遍正则,longest-first)。
|
|
281
281
|
*/
|
|
282
282
|
/** Defaults applied when neither sub-level nor global config provides a template. */
|
|
283
283
|
declare const DEFAULT_LIVE_TEMPLATES: {
|
|
284
|
-
readonly liveStart: "
|
|
285
|
-
readonly liveOngoing: "
|
|
286
|
-
readonly liveEnd: "
|
|
284
|
+
readonly liveStart: "{name} 开播啦,当前粉丝数:{follower}\n{link}";
|
|
285
|
+
readonly liveOngoing: "{name} 正在直播,已播 {time},累计观看:{watched}\n{link}";
|
|
286
|
+
readonly liveEnd: "{name} 下播啦,本次直播了 {time},粉丝变化 {follower_change}";
|
|
287
287
|
readonly liveSummaryFallback: "弹幕总结";
|
|
288
288
|
};
|
|
289
289
|
/**
|
|
@@ -572,6 +572,8 @@ declare class RoomContextBase {
|
|
|
572
572
|
readonly listenerRecord: Record<string, MessageListener>;
|
|
573
573
|
readonly livePushTimerManager: Map<string, () => void>;
|
|
574
574
|
private disposed;
|
|
575
|
+
/** stopMonitoring 主动关闭 listener 时置位;RoomSession.onClose 消费后不做自愈重连。 */
|
|
576
|
+
private readonly intentionalCloseRooms;
|
|
575
577
|
/** Cached protobuf type for INTERACT_WORD_V2 decoding (lazy-loaded). */
|
|
576
578
|
protected interactWord?: protobuf.Type;
|
|
577
579
|
/**
|
|
@@ -590,6 +592,7 @@ declare class RoomContextBase {
|
|
|
590
592
|
* 同型 no-op 安全调用方。room-session 已做 per-UID 节流,这里只是分发。
|
|
591
593
|
*/
|
|
592
594
|
emitViewers(uid: string, viewers: string): void;
|
|
595
|
+
consumeIntentionalClose(roomId: string): boolean;
|
|
593
596
|
/** 受 `config.imageEnabled` 门控的渲染器视图;关闭时返回 null。 */
|
|
594
597
|
get imageRenderer(): ImageRenderer | null;
|
|
595
598
|
updateConfig(config: ListenerManagerConfig): void;
|
|
@@ -737,6 +740,10 @@ declare abstract class RoomSessionBase {
|
|
|
737
740
|
bootstrap(): Promise<void>;
|
|
738
741
|
/** Build the platform-specific {@link MsgHandler}; provided by the subclass. */
|
|
739
742
|
protected abstract buildHandler(): MsgHandler;
|
|
743
|
+
/** Hook for subclass-owned connection-health bookkeeping after listener bootstrap succeeds. */
|
|
744
|
+
protected onListenerStarted(): void;
|
|
745
|
+
/** Hook for subclass-owned cleanup before this session intentionally stops monitoring. */
|
|
746
|
+
protected onMonitoringStopped(): void;
|
|
740
747
|
protected useLiveRoomInfo(liveType: LiveType): Promise<boolean>;
|
|
741
748
|
protected useMasterInfo(liveType: LiveType): Promise<boolean>;
|
|
742
749
|
/**
|
|
@@ -766,6 +773,7 @@ declare abstract class RoomSessionBase {
|
|
|
766
773
|
}
|
|
767
774
|
//#endregion
|
|
768
775
|
//#region src/room-session.d.ts
|
|
776
|
+
type LiveWsActivityReason = "connected" | "open" | "start-listen" | "heartbeat" | "danmu" | "superchat" | "watched" | "liked" | "guard" | "live-start" | "live-end" | "interact" | "close";
|
|
769
777
|
declare class RoomSession extends RoomSessionBase {
|
|
770
778
|
private lastViewersEmitMs;
|
|
771
779
|
/**
|
|
@@ -784,18 +792,35 @@ declare class RoomSession extends RoomSessionBase {
|
|
|
784
792
|
/** L3:退避 sleep 的 Disposable + 唤醒句柄,cancel/teardown 时清掉,不留回调到 expiry。 */
|
|
785
793
|
private reconnectTimer?;
|
|
786
794
|
private reconnectWake?;
|
|
787
|
-
|
|
795
|
+
private lastLiveWsActivityAt;
|
|
796
|
+
private lastLiveWsActivityReason;
|
|
797
|
+
private watchdogTimer?;
|
|
798
|
+
private watchdogReconnectCount;
|
|
799
|
+
/** 外层主动停止 listener 时调用,阻止 onError/onClose/watchdog 触发重连。 */
|
|
788
800
|
cancel(): void;
|
|
789
801
|
/** L3:dispose 退避定时器并唤醒重连循环,使其立刻重校 cancelled/disposed 后退出。 */
|
|
790
802
|
private clearReconnectSleep;
|
|
803
|
+
protected onListenerStarted(): void;
|
|
804
|
+
protected onMonitoringStopped(): void;
|
|
805
|
+
getWsHealthSnapshot(): {
|
|
806
|
+
lastActivityAt: number;
|
|
807
|
+
lastActivityReason: LiveWsActivityReason;
|
|
808
|
+
watchdogReconnectCount: number;
|
|
809
|
+
};
|
|
810
|
+
private markLiveWsActivity;
|
|
811
|
+
private startLiveWsWatchdog;
|
|
812
|
+
private stopLiveWsWatchdog;
|
|
813
|
+
private checkLiveWsWatchdog;
|
|
791
814
|
protected buildHandler(): MsgHandler;
|
|
792
815
|
private onError;
|
|
816
|
+
private reconnect;
|
|
793
817
|
/**
|
|
794
|
-
* 退避重连循环(单飞,由
|
|
818
|
+
* 退避重连循环(单飞,由 reconnect 持有)。`while` 取代旧的 `setTimeout(0)`
|
|
795
819
|
* 递归续链 —— 杜绝深栈递归 + 每步都丢弃的定时器 Disposable;每次 sleep 后
|
|
796
820
|
* 重校 cancelled/disposed,sleep 自身可被 cancel/teardown dispose。
|
|
797
821
|
*/
|
|
798
822
|
private reconnectLoop;
|
|
823
|
+
private describeReconnectReason;
|
|
799
824
|
/**
|
|
800
825
|
* L3:可被 {@link clearReconnectSleep} 取消的退避 sleep。dispose 时立即
|
|
801
826
|
* resolve,让 reconnectLoop 醒来重校 cancelled/disposed 后退出 —— 不再留
|
package/lib/index.d.mts
CHANGED
|
@@ -274,16 +274,16 @@ type LivePushTimerManager = Map<string, () => void>;
|
|
|
274
274
|
* - `customSpecialDanmakuUsers.msgTemplate`
|
|
275
275
|
* - `customSpecialUsersEnterTheRoom.msgTemplate`
|
|
276
276
|
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
277
|
+
* 占位符统一 `{name}` / `{time}` / `{watched}` 语法(与 `@bilibili-notify/internal`
|
|
278
|
+
* 的 `interpolate` 同源)。`applyTemplate` 同时接受 koishi 旧存档里的 legacy
|
|
279
|
+
* `-name` / `-time` 写法 —— 老用户已保存的 `-key` 模板继续生效,新默认与文档
|
|
280
|
+
* 一律走 `{key}`,二者不冲突(单遍正则,longest-first)。
|
|
281
281
|
*/
|
|
282
282
|
/** Defaults applied when neither sub-level nor global config provides a template. */
|
|
283
283
|
declare const DEFAULT_LIVE_TEMPLATES: {
|
|
284
|
-
readonly liveStart: "
|
|
285
|
-
readonly liveOngoing: "
|
|
286
|
-
readonly liveEnd: "
|
|
284
|
+
readonly liveStart: "{name} 开播啦,当前粉丝数:{follower}\n{link}";
|
|
285
|
+
readonly liveOngoing: "{name} 正在直播,已播 {time},累计观看:{watched}\n{link}";
|
|
286
|
+
readonly liveEnd: "{name} 下播啦,本次直播了 {time},粉丝变化 {follower_change}";
|
|
287
287
|
readonly liveSummaryFallback: "弹幕总结";
|
|
288
288
|
};
|
|
289
289
|
/**
|
|
@@ -572,6 +572,8 @@ declare class RoomContextBase {
|
|
|
572
572
|
readonly listenerRecord: Record<string, MessageListener>;
|
|
573
573
|
readonly livePushTimerManager: Map<string, () => void>;
|
|
574
574
|
private disposed;
|
|
575
|
+
/** stopMonitoring 主动关闭 listener 时置位;RoomSession.onClose 消费后不做自愈重连。 */
|
|
576
|
+
private readonly intentionalCloseRooms;
|
|
575
577
|
/** Cached protobuf type for INTERACT_WORD_V2 decoding (lazy-loaded). */
|
|
576
578
|
protected interactWord?: protobuf.Type;
|
|
577
579
|
/**
|
|
@@ -590,6 +592,7 @@ declare class RoomContextBase {
|
|
|
590
592
|
* 同型 no-op 安全调用方。room-session 已做 per-UID 节流,这里只是分发。
|
|
591
593
|
*/
|
|
592
594
|
emitViewers(uid: string, viewers: string): void;
|
|
595
|
+
consumeIntentionalClose(roomId: string): boolean;
|
|
593
596
|
/** 受 `config.imageEnabled` 门控的渲染器视图;关闭时返回 null。 */
|
|
594
597
|
get imageRenderer(): ImageRenderer | null;
|
|
595
598
|
updateConfig(config: ListenerManagerConfig): void;
|
|
@@ -737,6 +740,10 @@ declare abstract class RoomSessionBase {
|
|
|
737
740
|
bootstrap(): Promise<void>;
|
|
738
741
|
/** Build the platform-specific {@link MsgHandler}; provided by the subclass. */
|
|
739
742
|
protected abstract buildHandler(): MsgHandler;
|
|
743
|
+
/** Hook for subclass-owned connection-health bookkeeping after listener bootstrap succeeds. */
|
|
744
|
+
protected onListenerStarted(): void;
|
|
745
|
+
/** Hook for subclass-owned cleanup before this session intentionally stops monitoring. */
|
|
746
|
+
protected onMonitoringStopped(): void;
|
|
740
747
|
protected useLiveRoomInfo(liveType: LiveType): Promise<boolean>;
|
|
741
748
|
protected useMasterInfo(liveType: LiveType): Promise<boolean>;
|
|
742
749
|
/**
|
|
@@ -766,6 +773,7 @@ declare abstract class RoomSessionBase {
|
|
|
766
773
|
}
|
|
767
774
|
//#endregion
|
|
768
775
|
//#region src/room-session.d.ts
|
|
776
|
+
type LiveWsActivityReason = "connected" | "open" | "start-listen" | "heartbeat" | "danmu" | "superchat" | "watched" | "liked" | "guard" | "live-start" | "live-end" | "interact" | "close";
|
|
769
777
|
declare class RoomSession extends RoomSessionBase {
|
|
770
778
|
private lastViewersEmitMs;
|
|
771
779
|
/**
|
|
@@ -784,18 +792,35 @@ declare class RoomSession extends RoomSessionBase {
|
|
|
784
792
|
/** L3:退避 sleep 的 Disposable + 唤醒句柄,cancel/teardown 时清掉,不留回调到 expiry。 */
|
|
785
793
|
private reconnectTimer?;
|
|
786
794
|
private reconnectWake?;
|
|
787
|
-
|
|
795
|
+
private lastLiveWsActivityAt;
|
|
796
|
+
private lastLiveWsActivityReason;
|
|
797
|
+
private watchdogTimer?;
|
|
798
|
+
private watchdogReconnectCount;
|
|
799
|
+
/** 外层主动停止 listener 时调用,阻止 onError/onClose/watchdog 触发重连。 */
|
|
788
800
|
cancel(): void;
|
|
789
801
|
/** L3:dispose 退避定时器并唤醒重连循环,使其立刻重校 cancelled/disposed 后退出。 */
|
|
790
802
|
private clearReconnectSleep;
|
|
803
|
+
protected onListenerStarted(): void;
|
|
804
|
+
protected onMonitoringStopped(): void;
|
|
805
|
+
getWsHealthSnapshot(): {
|
|
806
|
+
lastActivityAt: number;
|
|
807
|
+
lastActivityReason: LiveWsActivityReason;
|
|
808
|
+
watchdogReconnectCount: number;
|
|
809
|
+
};
|
|
810
|
+
private markLiveWsActivity;
|
|
811
|
+
private startLiveWsWatchdog;
|
|
812
|
+
private stopLiveWsWatchdog;
|
|
813
|
+
private checkLiveWsWatchdog;
|
|
791
814
|
protected buildHandler(): MsgHandler;
|
|
792
815
|
private onError;
|
|
816
|
+
private reconnect;
|
|
793
817
|
/**
|
|
794
|
-
* 退避重连循环(单飞,由
|
|
818
|
+
* 退避重连循环(单飞,由 reconnect 持有)。`while` 取代旧的 `setTimeout(0)`
|
|
795
819
|
* 递归续链 —— 杜绝深栈递归 + 每步都丢弃的定时器 Disposable;每次 sleep 后
|
|
796
820
|
* 重校 cancelled/disposed,sleep 自身可被 cancel/teardown dispose。
|
|
797
821
|
*/
|
|
798
822
|
private reconnectLoop;
|
|
823
|
+
private describeReconnectReason;
|
|
799
824
|
/**
|
|
800
825
|
* L3:可被 {@link clearReconnectSleep} 取消的退避 sleep。dispose 时立即
|
|
801
826
|
* resolve,让 reconnectLoop 醒来重校 cancelled/disposed 后退出 —— 不再留
|
package/lib/index.mjs
CHANGED
|
@@ -161,6 +161,8 @@ var RoomContextBase = class {
|
|
|
161
161
|
listenerRecord = {};
|
|
162
162
|
livePushTimerManager = /* @__PURE__ */ new Map();
|
|
163
163
|
disposed = false;
|
|
164
|
+
/** stopMonitoring 主动关闭 listener 时置位;RoomSession.onClose 消费后不做自愈重连。 */
|
|
165
|
+
intentionalCloseRooms = /* @__PURE__ */ new Set();
|
|
164
166
|
/** Cached protobuf type for INTERACT_WORD_V2 decoding (lazy-loaded). */
|
|
165
167
|
interactWord;
|
|
166
168
|
/**
|
|
@@ -198,6 +200,11 @@ var RoomContextBase = class {
|
|
|
198
200
|
emitViewers(uid, viewers) {
|
|
199
201
|
this._emitViewers?.(uid, viewers);
|
|
200
202
|
}
|
|
203
|
+
consumeIntentionalClose(roomId) {
|
|
204
|
+
const hit = this.intentionalCloseRooms.has(roomId);
|
|
205
|
+
this.intentionalCloseRooms.delete(roomId);
|
|
206
|
+
return hit;
|
|
207
|
+
}
|
|
201
208
|
/** 受 `config.imageEnabled` 门控的渲染器视图;关闭时返回 null。 */
|
|
202
209
|
get imageRenderer() {
|
|
203
210
|
return this.config.imageEnabled === false ? null : this._getImageRenderer();
|
|
@@ -229,10 +236,12 @@ var RoomContextBase = class {
|
|
|
229
236
|
closeListener(roomId) {
|
|
230
237
|
const listener = this.listenerRecord[roomId];
|
|
231
238
|
if (!listener) {
|
|
239
|
+
this.intentionalCloseRooms.delete(roomId);
|
|
232
240
|
this.logger.debug(`[conn] 直播间 [${roomId}] 连接不存在,跳过关闭`);
|
|
233
241
|
return;
|
|
234
242
|
}
|
|
235
243
|
if (listener.closed) {
|
|
244
|
+
this.intentionalCloseRooms.delete(roomId);
|
|
236
245
|
this.logger.debug(`[conn] 直播间 [${roomId}] 连接已被远端断开`);
|
|
237
246
|
delete this.listenerRecord[roomId];
|
|
238
247
|
return;
|
|
@@ -256,6 +265,7 @@ var RoomContextBase = class {
|
|
|
256
265
|
stopMonitoring(reason, roomId) {
|
|
257
266
|
if (roomId) {
|
|
258
267
|
this.logger.error(`[conn] [${roomId}] ${reason},已停止该房间的监测`);
|
|
268
|
+
this.intentionalCloseRooms.add(roomId);
|
|
259
269
|
this.closeListener(roomId);
|
|
260
270
|
const timer = this.livePushTimerManager.get(roomId);
|
|
261
271
|
if (timer) {
|
|
@@ -313,6 +323,7 @@ var RoomContext = class extends RoomContextBase {
|
|
|
313
323
|
this.logger.warn(`[conn] 直播间 [${roomId}] 连接已存在,跳过创建`);
|
|
314
324
|
return true;
|
|
315
325
|
}
|
|
326
|
+
this.consumeIntentionalClose(roomId);
|
|
316
327
|
const cookiesStr = this.api.getCookiesHeader();
|
|
317
328
|
let mySelfInfo;
|
|
318
329
|
try {
|
|
@@ -475,37 +486,41 @@ var RoomContext = class extends RoomContextBase {
|
|
|
475
486
|
* - `customSpecialDanmakuUsers.msgTemplate`
|
|
476
487
|
* - `customSpecialUsersEnterTheRoom.msgTemplate`
|
|
477
488
|
*
|
|
478
|
-
*
|
|
479
|
-
*
|
|
480
|
-
*
|
|
481
|
-
*
|
|
489
|
+
* 占位符统一 `{name}` / `{time}` / `{watched}` 语法(与 `@bilibili-notify/internal`
|
|
490
|
+
* 的 `interpolate` 同源)。`applyTemplate` 同时接受 koishi 旧存档里的 legacy
|
|
491
|
+
* `-name` / `-time` 写法 —— 老用户已保存的 `-key` 模板继续生效,新默认与文档
|
|
492
|
+
* 一律走 `{key}`,二者不冲突(单遍正则,longest-first)。
|
|
482
493
|
*/
|
|
483
494
|
/** Defaults applied when neither sub-level nor global config provides a template. */
|
|
484
495
|
const DEFAULT_LIVE_TEMPLATES = {
|
|
485
|
-
liveStart: "
|
|
486
|
-
liveOngoing: "
|
|
487
|
-
liveEnd: "
|
|
496
|
+
liveStart: "{name} 开播啦,当前粉丝数:{follower}\n{link}",
|
|
497
|
+
liveOngoing: "{name} 正在直播,已播 {time},累计观看:{watched}\n{link}",
|
|
498
|
+
liveEnd: "{name} 下播啦,本次直播了 {time},粉丝变化 {follower_change}",
|
|
488
499
|
liveSummaryFallback: "弹幕总结"
|
|
489
500
|
};
|
|
490
501
|
function escapeRegExp(s) {
|
|
491
502
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
492
503
|
}
|
|
493
504
|
/**
|
|
494
|
-
* 单遍替换所有变量 token,再把 `\n`
|
|
505
|
+
* 单遍替换所有变量 token,再把 `\n` 转义展开为真换行。`vars` 以**裸键**(`name`/
|
|
506
|
+
* `follower_change`)给出,每个键同时匹配 `{name}`(主)与 legacy `-name`(兼容)。
|
|
495
507
|
*
|
|
496
508
|
* P2:此前 `for…replaceAll` 顺序替换有两个缺陷 ——
|
|
497
|
-
* 1. **token 注入**:用户可控值(uname / 弹幕内容)含
|
|
509
|
+
* 1. **token 注入**:用户可控值(uname / 弹幕内容)含 `{link}`/`-link` 时
|
|
498
510
|
* 会被后续轮次再次替换;
|
|
499
|
-
* 2.
|
|
511
|
+
* 2. **前缀吞噬**:legacy `-follower` 先于 `-follower_change` 替换,把后者的
|
|
500
512
|
* `-follower` 段吃掉只剩 `_change`。
|
|
501
|
-
*
|
|
502
|
-
*
|
|
513
|
+
* 改为基于原始模板的**单遍正则**:键按长度降序进 alternation(最长优先匹配),
|
|
514
|
+
* 每个 token 恰好替换一次且替换值不再被回扫 → 杜绝注入与吞噬。
|
|
503
515
|
*/
|
|
504
516
|
function applyTemplate(template, vars) {
|
|
505
517
|
const keys = Object.keys(vars).sort((a, b) => b.length - a.length);
|
|
506
518
|
if (keys.length === 0) return template.replaceAll("\\n", "\n");
|
|
507
|
-
const
|
|
508
|
-
|
|
519
|
+
const alts = keys.flatMap((k) => [`\\{${escapeRegExp(k)}\\}`, `-${escapeRegExp(k)}`]);
|
|
520
|
+
const re = new RegExp(alts.join("|"), "g");
|
|
521
|
+
return template.replace(re, (m) => {
|
|
522
|
+
return vars[m.charCodeAt(0) === 123 ? m.slice(1, -1) : m.slice(1)] ?? m;
|
|
523
|
+
}).replaceAll("\\n", "\n");
|
|
509
524
|
}
|
|
510
525
|
/**
|
|
511
526
|
* Format follower-change as a signed magnitude string with a 1万 (10K) cutoff,
|
|
@@ -535,27 +550,27 @@ var LiveTemplateRenderer = class {
|
|
|
535
550
|
/** Compose the "开播" notification text for a sub. */
|
|
536
551
|
renderLiveStart(params) {
|
|
537
552
|
return applyTemplate(resolveCustomLive(params.sub.customLiveMsg, params.globalCustom, "customLiveStart", DEFAULT_LIVE_TEMPLATES.liveStart), {
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
553
|
+
name: params.master.username,
|
|
554
|
+
time: params.diffTime,
|
|
555
|
+
follower: params.followerNum,
|
|
556
|
+
link: params.roomLink
|
|
542
557
|
});
|
|
543
558
|
}
|
|
544
559
|
/** Compose the periodic "正在直播" notification text. */
|
|
545
560
|
renderLiveOngoing(params) {
|
|
546
561
|
return applyTemplate(resolveCustomLive(params.sub.customLiveMsg, params.globalCustom, "customLive", DEFAULT_LIVE_TEMPLATES.liveOngoing), {
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
562
|
+
name: params.master.username,
|
|
563
|
+
time: params.diffTime,
|
|
564
|
+
watched: params.watched,
|
|
565
|
+
link: params.roomLink
|
|
551
566
|
});
|
|
552
567
|
}
|
|
553
568
|
/** Compose the "下播" notification text. */
|
|
554
569
|
renderLiveEnd(params) {
|
|
555
570
|
return applyTemplate(resolveCustomLive(params.sub.customLiveMsg, params.globalCustom, "customLiveEnd", DEFAULT_LIVE_TEMPLATES.liveEnd), {
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
571
|
+
name: params.master.username,
|
|
572
|
+
time: params.diffTime,
|
|
573
|
+
follower_change: formatFollowerChange(params.followerChange)
|
|
559
574
|
});
|
|
560
575
|
}
|
|
561
576
|
/**
|
|
@@ -564,24 +579,24 @@ var LiveTemplateRenderer = class {
|
|
|
564
579
|
*/
|
|
565
580
|
renderGuardBuy(params) {
|
|
566
581
|
return applyTemplate(params.guardBuyConfig.guardBuyMsg ?? "", {
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
582
|
+
uname: params.uname,
|
|
583
|
+
mname: params.master?.username ?? "",
|
|
584
|
+
guard: params.giftName
|
|
570
585
|
});
|
|
571
586
|
}
|
|
572
587
|
/** Compose the "特别关注弹幕" notification text. */
|
|
573
588
|
renderSpecialDanmaku(params) {
|
|
574
589
|
return applyTemplate(params.template, {
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
590
|
+
mastername: params.master?.username ?? "",
|
|
591
|
+
uname: params.uname,
|
|
592
|
+
msg: params.content
|
|
578
593
|
});
|
|
579
594
|
}
|
|
580
595
|
/** Compose the "特别关注进入直播间" notification text. */
|
|
581
596
|
renderSpecialUserEnter(params) {
|
|
582
597
|
return applyTemplate(params.template, {
|
|
583
|
-
|
|
584
|
-
|
|
598
|
+
mastername: params.master?.username ?? "",
|
|
599
|
+
uname: params.uname
|
|
585
600
|
});
|
|
586
601
|
}
|
|
587
602
|
/**
|
|
@@ -594,19 +609,19 @@ var LiveTemplateRenderer = class {
|
|
|
594
609
|
const top = params.topSenders;
|
|
595
610
|
const at = (i) => top[i] ?? ["", 0];
|
|
596
611
|
return applyTemplate(params.template, {
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
612
|
+
dmc: `${params.senderCount}`,
|
|
613
|
+
mdn: params.master?.medalName ?? "",
|
|
614
|
+
dca: `${params.danmakuCount}`,
|
|
615
|
+
un1: at(0)[0],
|
|
616
|
+
dc1: `${at(0)[1]}`,
|
|
617
|
+
un2: at(1)[0],
|
|
618
|
+
dc2: `${at(1)[1]}`,
|
|
619
|
+
un3: at(2)[0],
|
|
620
|
+
dc3: `${at(2)[1]}`,
|
|
621
|
+
un4: at(3)[0],
|
|
622
|
+
dc4: `${at(3)[1]}`,
|
|
623
|
+
un5: at(4)[0],
|
|
624
|
+
dc5: `${at(4)[1]}`
|
|
610
625
|
});
|
|
611
626
|
}
|
|
612
627
|
};
|
|
@@ -683,14 +698,17 @@ var RoomSessionBase = class {
|
|
|
683
698
|
async bootstrap() {
|
|
684
699
|
if (!await this.ctx.startLiveRoomListener(this.sub.roomId, this.buildHandler())) {
|
|
685
700
|
await this.ctx.push.sendPrivateMsg(`直播间 [${this.sub.roomId}] 弹幕连接建立失败,已停止该房间监测`);
|
|
701
|
+
this.onMonitoringStopped();
|
|
686
702
|
this.ctx.closeListener(this.sub.roomId);
|
|
687
703
|
return;
|
|
688
704
|
}
|
|
689
705
|
if (!await this.useLiveRoomInfo(4) || !await this.useMasterInfo(4) || !this.liveRoomInfo || !this.masterInfo) {
|
|
690
706
|
await this.ctx.push.sendPrivateMsg("获取直播间信息失败,启动直播间弹幕检测失败");
|
|
707
|
+
this.onMonitoringStopped();
|
|
691
708
|
this.ctx.closeListener(this.sub.roomId);
|
|
692
709
|
return;
|
|
693
710
|
}
|
|
711
|
+
this.onListenerStarted();
|
|
694
712
|
this.ctx.logger.debug(`[stat] 当前粉丝数:${this.masterInfo.liveOpenFollowerNum}`);
|
|
695
713
|
if (this.liveRoomInfo.live_status === 1) {
|
|
696
714
|
this.liveTime = this.liveRoomInfo.live_time;
|
|
@@ -719,6 +737,10 @@ var RoomSessionBase = class {
|
|
|
719
737
|
this.armPeriodicTimer();
|
|
720
738
|
}
|
|
721
739
|
}
|
|
740
|
+
/** Hook for subclass-owned connection-health bookkeeping after listener bootstrap succeeds. */
|
|
741
|
+
onListenerStarted() {}
|
|
742
|
+
/** Hook for subclass-owned cleanup before this session intentionally stops monitoring. */
|
|
743
|
+
onMonitoringStopped() {}
|
|
722
744
|
async useLiveRoomInfo(liveType) {
|
|
723
745
|
const data = await this.ctx.getLiveRoomInfo(this.sub.roomId);
|
|
724
746
|
if (!data?.uid) return false;
|
|
@@ -768,6 +790,7 @@ var RoomSessionBase = class {
|
|
|
768
790
|
/** Periodic "正在直播" tick (callback for `setInterval`). */
|
|
769
791
|
async tickPushAtTime() {
|
|
770
792
|
if (!await this.useLiveRoomInfo(2) || !this.liveRoomInfo) {
|
|
793
|
+
this.onMonitoringStopped();
|
|
771
794
|
this.ctx.stopMonitoring("获取直播间信息失败,推送直播卡片失败", this.sub.roomId);
|
|
772
795
|
return;
|
|
773
796
|
}
|
|
@@ -818,6 +841,7 @@ var RoomSessionBase = class {
|
|
|
818
841
|
this.setLiveStatus(false);
|
|
819
842
|
this.ctx.danmakuCollector.clear(this.sub.roomId);
|
|
820
843
|
if (this.ctx.isDisposed()) return;
|
|
844
|
+
this.onMonitoringStopped();
|
|
821
845
|
this.ctx.stopMonitoring("获取直播间信息失败,推送直播下播卡片失败", this.sub.roomId);
|
|
822
846
|
return;
|
|
823
847
|
}
|
|
@@ -902,6 +926,8 @@ const RECONNECT_BACKOFF_MS = [
|
|
|
902
926
|
8e3,
|
|
903
927
|
16e3
|
|
904
928
|
];
|
|
929
|
+
/** B 站 live WS 静默自愈:每分钟检查一次,3 分钟无 heartbeat/消息即主动重连。 */
|
|
930
|
+
const LIVE_WS_WATCHDOG_INTERVAL_MS = 6e4;
|
|
905
931
|
var RoomSession = class extends RoomSessionBase {
|
|
906
932
|
lastViewersEmitMs = 0;
|
|
907
933
|
/**
|
|
@@ -920,10 +946,15 @@ var RoomSession = class extends RoomSessionBase {
|
|
|
920
946
|
/** L3:退避 sleep 的 Disposable + 唤醒句柄,cancel/teardown 时清掉,不留回调到 expiry。 */
|
|
921
947
|
reconnectTimer;
|
|
922
948
|
reconnectWake;
|
|
923
|
-
|
|
949
|
+
lastLiveWsActivityAt = 0;
|
|
950
|
+
lastLiveWsActivityReason = "connected";
|
|
951
|
+
watchdogTimer;
|
|
952
|
+
watchdogReconnectCount = 0;
|
|
953
|
+
/** 外层主动停止 listener 时调用,阻止 onError/onClose/watchdog 触发重连。 */
|
|
924
954
|
cancel() {
|
|
925
955
|
this.cancelled = true;
|
|
926
956
|
this.reconnecting = false;
|
|
957
|
+
this.stopLiveWsWatchdog();
|
|
927
958
|
this.clearReconnectSleep();
|
|
928
959
|
}
|
|
929
960
|
/** L3:dispose 退避定时器并唤醒重连循环,使其立刻重校 cancelled/disposed 后退出。 */
|
|
@@ -933,12 +964,62 @@ var RoomSession = class extends RoomSessionBase {
|
|
|
933
964
|
this.reconnectWake?.();
|
|
934
965
|
this.reconnectWake = void 0;
|
|
935
966
|
}
|
|
967
|
+
onListenerStarted() {
|
|
968
|
+
this.markLiveWsActivity("connected");
|
|
969
|
+
this.startLiveWsWatchdog();
|
|
970
|
+
}
|
|
971
|
+
onMonitoringStopped() {
|
|
972
|
+
this.cancel();
|
|
973
|
+
}
|
|
974
|
+
getWsHealthSnapshot() {
|
|
975
|
+
return {
|
|
976
|
+
lastActivityAt: this.lastLiveWsActivityAt,
|
|
977
|
+
lastActivityReason: this.lastLiveWsActivityReason,
|
|
978
|
+
watchdogReconnectCount: this.watchdogReconnectCount
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
markLiveWsActivity(reason) {
|
|
982
|
+
this.lastLiveWsActivityAt = Date.now();
|
|
983
|
+
this.lastLiveWsActivityReason = reason;
|
|
984
|
+
}
|
|
985
|
+
startLiveWsWatchdog() {
|
|
986
|
+
if (this.watchdogTimer || this.cancelled || this.ctx.isDisposed()) return;
|
|
987
|
+
this.watchdogTimer = this.ctx.serviceCtx.setInterval(() => this.checkLiveWsWatchdog(), LIVE_WS_WATCHDOG_INTERVAL_MS);
|
|
988
|
+
}
|
|
989
|
+
stopLiveWsWatchdog() {
|
|
990
|
+
this.watchdogTimer?.dispose();
|
|
991
|
+
this.watchdogTimer = void 0;
|
|
992
|
+
}
|
|
993
|
+
checkLiveWsWatchdog() {
|
|
994
|
+
if (this.cancelled || this.ctx.isDisposed() || this.reconnecting) return;
|
|
995
|
+
if (this.lastLiveWsActivityAt <= 0) return;
|
|
996
|
+
const staleMs = Date.now() - this.lastLiveWsActivityAt;
|
|
997
|
+
if (staleMs < 18e4) return;
|
|
998
|
+
this.watchdogReconnectCount++;
|
|
999
|
+
this.reconnect("watchdog", `${Math.floor(staleMs / 1e3)}s 无 heartbeat/消息(last=${this.lastLiveWsActivityReason},watchdog=${this.watchdogReconnectCount})`);
|
|
1000
|
+
}
|
|
936
1001
|
buildHandler() {
|
|
937
1002
|
const base = {
|
|
1003
|
+
onOpen: () => this.markLiveWsActivity("open"),
|
|
1004
|
+
onStartListen: () => this.markLiveWsActivity("start-listen"),
|
|
1005
|
+
onClose: () => {
|
|
1006
|
+
if (this.cancelled || this.ctx.isDisposed()) return;
|
|
1007
|
+
if (this.ctx.consumeIntentionalClose(this.sub.roomId)) return;
|
|
1008
|
+
this.markLiveWsActivity("close");
|
|
1009
|
+
this.reconnect("close");
|
|
1010
|
+
},
|
|
938
1011
|
onError: () => this.onError(),
|
|
939
|
-
|
|
940
|
-
|
|
1012
|
+
onAttentionChange: () => this.markLiveWsActivity("heartbeat"),
|
|
1013
|
+
onIncomeDanmu: ({ body }) => {
|
|
1014
|
+
this.markLiveWsActivity("danmu");
|
|
1015
|
+
this.onIncomeDanmu(body);
|
|
1016
|
+
},
|
|
1017
|
+
onIncomeSuperChat: ({ body }) => {
|
|
1018
|
+
this.markLiveWsActivity("superchat");
|
|
1019
|
+
return this.onIncomeSuperChat(body);
|
|
1020
|
+
},
|
|
941
1021
|
onWatchedChange: ({ body }) => {
|
|
1022
|
+
this.markLiveWsActivity("watched");
|
|
942
1023
|
this.liveData.watchedNum = body.text_small;
|
|
943
1024
|
const now = Date.now();
|
|
944
1025
|
if (now - this.lastViewersEmitMs >= VIEWERS_EMIT_THROTTLE_MS) {
|
|
@@ -947,42 +1028,61 @@ var RoomSession = class extends RoomSessionBase {
|
|
|
947
1028
|
}
|
|
948
1029
|
},
|
|
949
1030
|
onLikedChange: ({ body }) => {
|
|
1031
|
+
this.markLiveWsActivity("liked");
|
|
950
1032
|
this.liveData.likedNum = body.count;
|
|
951
1033
|
},
|
|
952
|
-
onGuardBuy: ({ body }) =>
|
|
953
|
-
|
|
954
|
-
|
|
1034
|
+
onGuardBuy: ({ body }) => {
|
|
1035
|
+
this.markLiveWsActivity("guard");
|
|
1036
|
+
return this.onGuardBuy(body);
|
|
1037
|
+
},
|
|
1038
|
+
onLiveStart: () => {
|
|
1039
|
+
this.markLiveWsActivity("live-start");
|
|
1040
|
+
return this.onLiveStart();
|
|
1041
|
+
},
|
|
1042
|
+
onLiveEnd: () => {
|
|
1043
|
+
this.markLiveWsActivity("live-end");
|
|
1044
|
+
return this.onLiveEnd();
|
|
1045
|
+
}
|
|
955
1046
|
};
|
|
956
1047
|
if (!this.sub.customSpecialUsersEnterTheRoom.enable) return base;
|
|
957
1048
|
return {
|
|
958
1049
|
...base,
|
|
959
|
-
raw: { INTERACT_WORD_V2: (msg) =>
|
|
1050
|
+
raw: { INTERACT_WORD_V2: (msg) => {
|
|
1051
|
+
this.markLiveWsActivity("interact");
|
|
1052
|
+
return this.onInteractWordV2(msg);
|
|
1053
|
+
} }
|
|
960
1054
|
};
|
|
961
1055
|
}
|
|
962
|
-
|
|
1056
|
+
onError() {
|
|
1057
|
+
return this.reconnect("error");
|
|
1058
|
+
}
|
|
1059
|
+
async reconnect(reason, detail) {
|
|
963
1060
|
if (this.cancelled || this.ctx.isDisposed()) return;
|
|
964
1061
|
if (this.reconnecting) return;
|
|
965
1062
|
this.reconnecting = true;
|
|
966
1063
|
try {
|
|
967
|
-
await this.reconnectLoop();
|
|
1064
|
+
await this.reconnectLoop(reason, detail);
|
|
968
1065
|
} finally {
|
|
969
1066
|
this.reconnecting = false;
|
|
970
1067
|
}
|
|
971
1068
|
}
|
|
972
1069
|
/**
|
|
973
|
-
* 退避重连循环(单飞,由
|
|
1070
|
+
* 退避重连循环(单飞,由 reconnect 持有)。`while` 取代旧的 `setTimeout(0)`
|
|
974
1071
|
* 递归续链 —— 杜绝深栈递归 + 每步都丢弃的定时器 Disposable;每次 sleep 后
|
|
975
1072
|
* 重校 cancelled/disposed,sleep 自身可被 cancel/teardown dispose。
|
|
976
1073
|
*/
|
|
977
|
-
async reconnectLoop() {
|
|
1074
|
+
async reconnectLoop(reason, detail) {
|
|
978
1075
|
while (this.reconnectAttempts < RECONNECT_BACKOFF_MS.length) {
|
|
979
1076
|
if (this.cancelled || this.ctx.isDisposed()) return;
|
|
980
|
-
|
|
981
|
-
|
|
1077
|
+
if (reason === "error") {
|
|
1078
|
+
this.setLiveStatus(false);
|
|
1079
|
+
this.cancelPeriodicTimer();
|
|
1080
|
+
}
|
|
982
1081
|
this.ctx.closeListener(this.sub.roomId);
|
|
983
1082
|
const delay = RECONNECT_BACKOFF_MS[this.reconnectAttempts];
|
|
984
1083
|
this.reconnectAttempts++;
|
|
985
|
-
|
|
1084
|
+
const reasonText = this.describeReconnectReason(reason, detail);
|
|
1085
|
+
this.ctx.logger.warn(`[conn] 直播间 [${this.sub.roomId}] ${reasonText},${delay / 1e3}s 后重连(第 ${this.reconnectAttempts}/${RECONNECT_BACKOFF_MS.length} 次)`);
|
|
986
1086
|
await this.sleepReconnect(delay);
|
|
987
1087
|
if (this.cancelled || this.ctx.isDisposed()) return;
|
|
988
1088
|
let ok = false;
|
|
@@ -996,6 +1096,7 @@ var RoomSession = class extends RoomSessionBase {
|
|
|
996
1096
|
return;
|
|
997
1097
|
}
|
|
998
1098
|
if (ok) {
|
|
1099
|
+
this.onListenerStarted();
|
|
999
1100
|
this.ctx.logger.info(`[conn] 直播间 [${this.sub.roomId}] 重连成功`);
|
|
1000
1101
|
this.reconnectAttempts = 0;
|
|
1001
1102
|
return;
|
|
@@ -1003,9 +1104,15 @@ var RoomSession = class extends RoomSessionBase {
|
|
|
1003
1104
|
this.ctx.logger.warn(`[conn] 直播间 [${this.sub.roomId}] 重连未成功,继续退避`);
|
|
1004
1105
|
}
|
|
1005
1106
|
this.reconnectAttempts = 0;
|
|
1006
|
-
const msg = `直播间 [${this.sub.roomId}]
|
|
1107
|
+
const msg = `直播间 [${this.sub.roomId}] ${this.describeReconnectReason(reason, detail)}后连接持续失败,重试 ${RECONNECT_BACKOFF_MS.length} 次后放弃监听`;
|
|
1007
1108
|
this.ctx.logger.error(`[conn] ${msg}`);
|
|
1008
1109
|
this.ctx.emitEngineError(msg);
|
|
1110
|
+
this.cancel();
|
|
1111
|
+
}
|
|
1112
|
+
describeReconnectReason(reason, detail) {
|
|
1113
|
+
if (reason === "error") return "连接错误";
|
|
1114
|
+
if (reason === "close") return "连接关闭";
|
|
1115
|
+
return detail ? `连接静默(${detail})` : "连接静默";
|
|
1009
1116
|
}
|
|
1010
1117
|
/**
|
|
1011
1118
|
* L3:可被 {@link clearReconnectSleep} 取消的退避 sleep。dispose 时立即
|
|
@@ -1128,6 +1235,7 @@ var RoomSession = class extends RoomSessionBase {
|
|
|
1128
1235
|
if (!await this.useLiveRoomInfo(1) || !await this.useMasterInfo(1) || !this.liveRoomInfo || !this.masterInfo) {
|
|
1129
1236
|
this.setLiveStatus(false);
|
|
1130
1237
|
if (this.ctx.isDisposed()) return;
|
|
1238
|
+
this.onMonitoringStopped();
|
|
1131
1239
|
this.ctx.stopMonitoring("获取直播间信息失败,推送直播开播卡片失败", this.sub.roomId);
|
|
1132
1240
|
return;
|
|
1133
1241
|
}
|
|
@@ -1669,9 +1777,7 @@ var LiveEngine = class {
|
|
|
1669
1777
|
/** Tear down all listeners + per-room state, leaving the engine instance reusable. */
|
|
1670
1778
|
teardown() {
|
|
1671
1779
|
this.logger.info("[live] 关闭所有直播间监听");
|
|
1672
|
-
this.listener.
|
|
1673
|
-
this.listener.clearListeners();
|
|
1674
|
-
this.danmakuCollector.clearAll();
|
|
1780
|
+
this.listener.disposeAll();
|
|
1675
1781
|
}
|
|
1676
1782
|
/** Full rebootstrap. Used after auth-restored. */
|
|
1677
1783
|
rebuildFromSubs(subs) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bilibili-notify/live",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0-alpha.5",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/Akokk0/bilibili-notify"
|
|
@@ -30,10 +30,10 @@
|
|
|
30
30
|
"jieba-wasm": "^2.4.0",
|
|
31
31
|
"luxon": "^3.5.0",
|
|
32
32
|
"protobufjs": "^7.4.0",
|
|
33
|
-
"@bilibili-notify/api": "^0.2.0-alpha.2",
|
|
34
33
|
"@bilibili-notify/ai": "^0.0.1-alpha.1",
|
|
35
|
-
"@bilibili-notify/
|
|
36
|
-
"@bilibili-notify/
|
|
34
|
+
"@bilibili-notify/image": "^0.0.1-alpha.2",
|
|
35
|
+
"@bilibili-notify/internal": "^0.1.0-alpha.4",
|
|
36
|
+
"@bilibili-notify/api": "^0.2.0-alpha.2"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@types/luxon": "^3.4.2",
|