koishi-plugin-bilirice 0.0.5 → 0.0.7
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.d.ts +8 -40
- package/lib/index.js +159 -315
- package/lib/session.d.ts +20 -0
- package/package.json +6 -2
package/lib/index.d.ts
CHANGED
|
@@ -1,42 +1,10 @@
|
|
|
1
1
|
import { Context, Schema } from 'koishi';
|
|
2
|
-
export declare const name = "bilirice";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
liveEnd?: string;
|
|
9
|
-
};
|
|
10
|
-
timeout?: number;
|
|
11
|
-
bilibiliCookie?: string;
|
|
12
|
-
debugMode?: boolean;
|
|
2
|
+
export declare const name = "koishi-plugin-bilirice";
|
|
3
|
+
export declare const inject: string[];
|
|
4
|
+
export interface Config {
|
|
5
|
+
roomId: number;
|
|
6
|
+
channelId: string;
|
|
7
|
+
pollInterval: number;
|
|
13
8
|
}
|
|
14
|
-
export declare const Config: Schema<
|
|
15
|
-
|
|
16
|
-
pollInterval: Schema<number, number>;
|
|
17
|
-
timeout: Schema<number, number>;
|
|
18
|
-
bilibiliCookie: Schema<string, string>;
|
|
19
|
-
debugMode: Schema<boolean, boolean>;
|
|
20
|
-
templates: Schema<Schemastery.ObjectS<{
|
|
21
|
-
liveStart: Schema<string, string>;
|
|
22
|
-
liveEnd: Schema<string, string>;
|
|
23
|
-
}>, Schemastery.ObjectT<{
|
|
24
|
-
liveStart: Schema<string, string>;
|
|
25
|
-
liveEnd: Schema<string, string>;
|
|
26
|
-
}>>;
|
|
27
|
-
}>, Schemastery.ObjectT<{
|
|
28
|
-
anchors: Schema<[string?, string?, ...any[]][], [string?, string?, ...any[]][]>;
|
|
29
|
-
pollInterval: Schema<number, number>;
|
|
30
|
-
timeout: Schema<number, number>;
|
|
31
|
-
bilibiliCookie: Schema<string, string>;
|
|
32
|
-
debugMode: Schema<boolean, boolean>;
|
|
33
|
-
templates: Schema<Schemastery.ObjectS<{
|
|
34
|
-
liveStart: Schema<string, string>;
|
|
35
|
-
liveEnd: Schema<string, string>;
|
|
36
|
-
}>, Schemastery.ObjectT<{
|
|
37
|
-
liveStart: Schema<string, string>;
|
|
38
|
-
liveEnd: Schema<string, string>;
|
|
39
|
-
}>>;
|
|
40
|
-
}>>;
|
|
41
|
-
export declare function apply(ctx: Context, config: BiliriceConfig): void;
|
|
42
|
-
export {};
|
|
9
|
+
export declare const Config: Schema<Config>;
|
|
10
|
+
export declare function apply(ctx: Context, config: Config): void;
|
package/lib/index.js
CHANGED
|
@@ -32,349 +32,193 @@ var src_exports = {};
|
|
|
32
32
|
__export(src_exports, {
|
|
33
33
|
Config: () => Config,
|
|
34
34
|
apply: () => apply,
|
|
35
|
+
inject: () => inject,
|
|
35
36
|
name: () => name
|
|
36
37
|
});
|
|
37
38
|
module.exports = __toCommonJS(src_exports);
|
|
38
39
|
var import_koishi = require("koishi");
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
var
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
import_koishi.Schema.string().description("B站主播UID(数字,如老番茄14663353)"),
|
|
45
|
-
import_koishi.Schema.string().description("通知群号(OneBot格式加group_前缀,如group_123456789)")
|
|
46
|
-
])).required().description("监听的主播列表 → 点击「添加项」可配置多个主播").role("table"),
|
|
47
|
-
pollInterval: import_koishi.Schema.number().default(10).description("轮询间隔(秒),建议≥5秒(避免触发B站API风控)").role("slider", { min: 5, max: 30, step: 1 }),
|
|
48
|
-
timeout: import_koishi.Schema.number().default(1e4).description("API请求超时时间(毫秒),建议5000-15000").role("number", { min: 5e3, max: 3e4 }),
|
|
49
|
-
bilibiliCookie: import_koishi.Schema.string().description(`【必填】B站登录Cookie(解决-352错误)
|
|
50
|
-
获取方式:
|
|
51
|
-
1. 登录B站直播页 https://live.bilibili.com/
|
|
52
|
-
2. F12 → 网络 → 搜索 getInfoByRoom
|
|
53
|
-
3. 复制请求头中的「Cookie」完整值
|
|
54
|
-
(包含SESSDATA/bili_jct/DedeUserID等核心字段)`).required().role("password"),
|
|
55
|
-
// 新增:调试模式开关(控制台显示为开关按钮)
|
|
56
|
-
debugMode: import_koishi.Schema.boolean().default(false).description("调试模式:开启后打印API请求/响应、状态变化等详细日志,方便排查问题").role("switch"),
|
|
57
|
-
// 控制台显示为开关,一键开启/关闭
|
|
58
|
-
templates: import_koishi.Schema.object({
|
|
59
|
-
liveStart: import_koishi.Schema.string().default("【{uname} 开播啦】\n标题:{title}\n分区:{area}\n链接:{url}\n开播时间:{startTime}\n封面:{cover}").description("开播通知模板,支持占位符:{uname}/{title}/{area}/{url}/{startTime}/{cover}").role("textarea"),
|
|
60
|
-
liveEnd: import_koishi.Schema.string().default("【{uname} 下播啦】\n直播时长:{liveTime}\n最高在线:{peakOnline}\n观看人数:{watchCount}\n弹幕数:{dmCount}\n人均弹幕:{avgDmPerUser}\n下播时间:{endTime}").description("下播通知模板,额外支持:{liveTime}/{peakOnline}/{watchCount}/{dmCount}/{avgDmPerUser}/{endTime}").role("textarea")
|
|
61
|
-
}).description("自定义通知模板(保留占位符即可自动替换数据)")
|
|
62
|
-
});
|
|
63
|
-
function apply(ctx, config) {
|
|
64
|
-
const finalConfig = {
|
|
65
|
-
pollInterval: config.pollInterval || 10,
|
|
66
|
-
timeout: config.timeout || 1e4,
|
|
67
|
-
templates: {
|
|
68
|
-
liveStart: config.templates?.liveStart || "【{uname} 开播啦】\n标题:{title}\n分区:{area}\n链接:{url}\n开播时间:{startTime}\n封面:{cover}",
|
|
69
|
-
liveEnd: config.templates?.liveEnd || "【{uname} 下播啦】\n直播时长:{liveTime}\n最高在线:{peakOnline}\n观看人数:{watchCount}\n弹幕数:{dmCount}\n人均弹幕:{avgDmPerUser}\n下播时间:{endTime}"
|
|
70
|
-
},
|
|
71
|
-
anchors: config.anchors || [],
|
|
72
|
-
bilibiliCookie: config.bilibiliCookie || "",
|
|
73
|
-
debugMode: config.debugMode || false
|
|
74
|
-
// 读取调试模式配置
|
|
75
|
-
};
|
|
76
|
-
const anchorStateCache = /* @__PURE__ */ new Map();
|
|
77
|
-
const debugLog = /* @__PURE__ */ __name((message, data) => {
|
|
78
|
-
if (finalConfig.debugMode) {
|
|
79
|
-
if (data) {
|
|
80
|
-
ctx.logger.debug(`[bilirice调试] ${message}:`, data);
|
|
81
|
-
} else {
|
|
82
|
-
ctx.logger.debug(`[bilirice调试] ${message}`);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}, "debugLog");
|
|
86
|
-
const api = import_axios.default.create({
|
|
87
|
-
baseURL: "https://api.live.bilibili.com",
|
|
88
|
-
timeout: finalConfig.timeout,
|
|
89
|
-
httpsAgent: new import_https.default.Agent({ rejectUnauthorized: false }),
|
|
90
|
-
headers: {
|
|
91
|
-
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0",
|
|
92
|
-
"Referer": "https://live.bilibili.com/",
|
|
93
|
-
"Origin": "https://live.bilibili.com",
|
|
94
|
-
"Accept": "application/json, text/plain, */*",
|
|
95
|
-
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8,en-US;q=0.7",
|
|
96
|
-
"Cache-Control": "no-cache",
|
|
97
|
-
"Pragma": "no-cache",
|
|
98
|
-
"Sec-Fetch-Dest": "empty",
|
|
99
|
-
"Sec-Fetch-Mode": "cors",
|
|
100
|
-
"Sec-Fetch-Site": "same-site",
|
|
101
|
-
"Cookie": finalConfig.bilibiliCookie
|
|
102
|
-
},
|
|
103
|
-
transformResponse: [(data) => data]
|
|
104
|
-
});
|
|
105
|
-
api.interceptors.request.use((config2) => {
|
|
106
|
-
debugLog(`API请求:${config2.method?.toUpperCase()} ${config2.url}`, {
|
|
107
|
-
params: config2.params,
|
|
108
|
-
headers: {
|
|
109
|
-
// 隐藏Cookie敏感信息,只打印关键头
|
|
110
|
-
"User-Agent": config2.headers["User-Agent"],
|
|
111
|
-
"Referer": config2.headers["Referer"],
|
|
112
|
-
"Cookie": "*** 已隐藏(调试模式可手动查看) ***"
|
|
113
|
-
}
|
|
114
|
-
});
|
|
115
|
-
return config2;
|
|
116
|
-
});
|
|
117
|
-
api.interceptors.response.use(
|
|
118
|
-
(response) => {
|
|
119
|
-
try {
|
|
120
|
-
response.data = JSON.parse(response.data);
|
|
121
|
-
debugLog(`API响应:${response.config.url}`, response.data);
|
|
122
|
-
return response;
|
|
123
|
-
} catch (err) {
|
|
124
|
-
debugLog(`API响应解析失败`, err);
|
|
125
|
-
throw new Error(`API响应解析失败: ${err.message}`);
|
|
126
|
-
}
|
|
127
|
-
},
|
|
128
|
-
(error) => {
|
|
129
|
-
debugLog(`API请求失败`, {
|
|
130
|
-
url: error.config?.url,
|
|
131
|
-
code: error.code,
|
|
132
|
-
response: error.response?.data
|
|
133
|
-
});
|
|
134
|
-
throw new Error(`API请求失败: ${error.message || error.code}`);
|
|
135
|
-
}
|
|
136
|
-
);
|
|
137
|
-
async function getRoomIdByMid(mid) {
|
|
138
|
-
debugLog(`开始获取主播${mid}的直播间ID`);
|
|
139
|
-
const res = await api.get("/room/v1/Room/getRoomInfoOld", {
|
|
140
|
-
params: { mid: Number(mid) }
|
|
141
|
-
});
|
|
142
|
-
if (res.data.code !== 0) throw new Error(`获取直播间ID失败: ${res.data.code} ${res.data.message || ""}`);
|
|
143
|
-
const roomId = res.data.data.roomid.toString();
|
|
144
|
-
debugLog(`主播${mid}的直播间ID:${roomId}`);
|
|
145
|
-
return roomId;
|
|
40
|
+
|
|
41
|
+
// src/session.ts
|
|
42
|
+
var LiveSession = class {
|
|
43
|
+
static {
|
|
44
|
+
__name(this, "LiveSession");
|
|
146
45
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
debugLog(`直播间${roomId}基础信息`, res.data.data);
|
|
155
|
-
return res.data.data;
|
|
46
|
+
startTime = 0;
|
|
47
|
+
danmuCount = 0;
|
|
48
|
+
maxOnline = 0;
|
|
49
|
+
guardNew = 0;
|
|
50
|
+
guardRenew = 0;
|
|
51
|
+
start() {
|
|
52
|
+
this.startTime = Date.now();
|
|
156
53
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
version: "1.0.0",
|
|
165
|
-
ts: Date.now()
|
|
166
|
-
}
|
|
167
|
-
});
|
|
168
|
-
if (res.data.code !== 0) throw new Error(`获取直播间统计失败: ${res.data.code} ${res.data.message || ""}`);
|
|
169
|
-
const data = res.data.data;
|
|
170
|
-
if (!data || !data.room_info) throw new Error("获取直播间统计失败: 返回数据结构异常");
|
|
171
|
-
const stat = {
|
|
172
|
-
online: data.room_info.online || 0,
|
|
173
|
-
watch_num: data.room_info.watch_num || 0,
|
|
174
|
-
interact_num: data.room_info.interact_num || 0,
|
|
175
|
-
dm_count: data.room_info.dm_count || 0,
|
|
176
|
-
medal_count: data.room_info.medal_count || 0,
|
|
177
|
-
fans_count: data.anchor_info?.base_info?.fans_count || 0,
|
|
178
|
-
live_time: data.room_info.live_time || 0
|
|
54
|
+
end() {
|
|
55
|
+
return {
|
|
56
|
+
durationMs: Date.now() - this.startTime,
|
|
57
|
+
danmuCount: this.danmuCount,
|
|
58
|
+
maxOnline: this.maxOnline,
|
|
59
|
+
guardNew: this.guardNew,
|
|
60
|
+
guardRenew: this.guardRenew
|
|
179
61
|
};
|
|
180
|
-
debugLog(`直播间${roomId}统计数据`, stat);
|
|
181
|
-
return stat;
|
|
182
62
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
debugLog(`开始获取直播间${roomId}主播信息`);
|
|
186
|
-
const res = await api.get("/live_user/v1/UserInfo/get_anchor_in_room", {
|
|
187
|
-
params: { roomid: Number(roomId) }
|
|
188
|
-
});
|
|
189
|
-
if (res.data.code !== 0) throw new Error(`获取主播信息失败: ${res.data.code} ${res.data.message || ""}`);
|
|
190
|
-
debugLog(`直播间${roomId}主播信息`, res.data.data);
|
|
191
|
-
return res.data.data;
|
|
63
|
+
onDanmu() {
|
|
64
|
+
this.danmuCount++;
|
|
192
65
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
const formatted = new Date(timestamp * 1e3).toLocaleString("zh-CN", {
|
|
196
|
-
year: "numeric",
|
|
197
|
-
month: "2-digit",
|
|
198
|
-
day: "2-digit",
|
|
199
|
-
hour: "2-digit",
|
|
200
|
-
minute: "2-digit",
|
|
201
|
-
second: "2-digit"
|
|
202
|
-
});
|
|
203
|
-
debugLog(`时间戳${timestamp}格式化结果:${formatted}`);
|
|
204
|
-
return formatted;
|
|
66
|
+
onOnline(count) {
|
|
67
|
+
this.maxOnline = Math.max(this.maxOnline, count);
|
|
205
68
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
const h = Math.floor(seconds / 3600);
|
|
209
|
-
const m = Math.floor(seconds % 3600 / 60);
|
|
210
|
-
const s = seconds % 60;
|
|
211
|
-
const formatted = [h, m, s].map((v) => v.toString().padStart(2, "0")).join(":");
|
|
212
|
-
debugLog(`秒数${seconds}格式化结果:${formatted}`);
|
|
213
|
-
return formatted;
|
|
69
|
+
onGuard(num) {
|
|
70
|
+
num > 1 ? this.guardRenew++ : this.guardNew++;
|
|
214
71
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
debugLog(`计算下播统计数据`, { startTime, endTime, lastStat, currentStat });
|
|
218
|
-
let fansChange = 0;
|
|
219
|
-
if (lastStat && currentStat) fansChange = currentStat.fans_count - lastStat.fans_count;
|
|
220
|
-
const stat = {
|
|
221
|
-
liveTime: endTime - startTime,
|
|
222
|
-
peakOnline: currentStat.online,
|
|
223
|
-
watchCount: currentStat.watch_num,
|
|
224
|
-
interactCount: currentStat.interact_num,
|
|
225
|
-
dmCount: currentStat.dm_count,
|
|
226
|
-
avgDmPerUser: Number((currentStat.dm_count / Math.max(currentStat.watch_num, 1)).toFixed(2)),
|
|
227
|
-
medalCount: currentStat.medal_count,
|
|
228
|
-
fansChange,
|
|
229
|
-
endTime
|
|
230
|
-
};
|
|
231
|
-
debugLog(`下播统计数据计算结果`, stat);
|
|
232
|
-
return stat;
|
|
72
|
+
get avgDanmu() {
|
|
73
|
+
return this.maxOnline ? (this.danmuCount / this.maxOnline).toFixed(2) : "0";
|
|
233
74
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
debugLog(`模板渲染结果`, result);
|
|
239
|
-
return result;
|
|
75
|
+
formatDuration(ms) {
|
|
76
|
+
const h2 = Math.floor(ms / 36e5);
|
|
77
|
+
const m = Math.floor(ms % 36e5 / 6e4);
|
|
78
|
+
return `${h2}小时${m}分钟`;
|
|
240
79
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// src/index.ts
|
|
83
|
+
var import_ws = __toESM(require("ws"));
|
|
84
|
+
var name = "koishi-plugin-bilirice";
|
|
85
|
+
var inject = ["database"];
|
|
86
|
+
var logger = new import_koishi.Logger("bilirice");
|
|
87
|
+
var Config = import_koishi.Schema.object({
|
|
88
|
+
roomId: import_koishi.Schema.number().description("Bilibili 直播间 room_id").required(),
|
|
89
|
+
channelId: import_koishi.Schema.string().description("推送到的群 / 频道 ID").required(),
|
|
90
|
+
pollInterval: import_koishi.Schema.number().default(30).description("轮询间隔(秒)")
|
|
91
|
+
});
|
|
92
|
+
function apply(ctx, config) {
|
|
93
|
+
const session = new LiveSession();
|
|
94
|
+
let isLiving = false;
|
|
95
|
+
let ws = null;
|
|
96
|
+
let liveStartTime = 0;
|
|
97
|
+
async function fetchJSON(url) {
|
|
98
|
+
const res = await ctx.http.get(url);
|
|
99
|
+
if (res?.code !== 0) throw new Error(res?.message || "API Error");
|
|
100
|
+
return res.data;
|
|
248
101
|
}
|
|
249
|
-
__name(
|
|
250
|
-
async function
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
102
|
+
__name(fetchJSON, "fetchJSON");
|
|
103
|
+
async function send(text, image) {
|
|
104
|
+
const content = image ? import_koishi.h.image(image) + "\n" + text : text;
|
|
105
|
+
await ctx.broadcast([config.channelId], content);
|
|
106
|
+
}
|
|
107
|
+
__name(send, "send");
|
|
108
|
+
function formatTime(ts = Date.now()) {
|
|
109
|
+
return new Date(ts).toLocaleString();
|
|
110
|
+
}
|
|
111
|
+
__name(formatTime, "formatTime");
|
|
112
|
+
const getRoomInfo = /* @__PURE__ */ __name(() => fetchJSON(`https://api.live.bilibili.com/room/v1/Room/get_info?room_id=${config.roomId}`), "getRoomInfo");
|
|
113
|
+
const getAnchorInfo = /* @__PURE__ */ __name(() => fetchJSON(`https://api.live.bilibili.com/live_user/v1/UserInfo/get_anchor_in_room?roomid=${config.roomId}`), "getAnchorInfo");
|
|
114
|
+
async function onLiveStart(room, anchor) {
|
|
115
|
+
session.start();
|
|
116
|
+
liveStartTime = Date.now();
|
|
117
|
+
const text = `【开播提醒】${anchor.info.uname} 开始直播啦!
|
|
118
|
+
|
|
119
|
+
📝 直播间标题:${room.title}
|
|
120
|
+
📂 直播分区:${room.parent_area_name} / ${room.area_name}
|
|
121
|
+
🏷️ 标签:${room.tags || "无"}
|
|
122
|
+
⏰ 开播时间:${formatTime(room.live_time * 1e3)}
|
|
123
|
+
|
|
124
|
+
快来直播间一起玩吧~
|
|
125
|
+
👉 https://live.bilibili.com/${config.roomId}`;
|
|
126
|
+
await send(text, room.user_cover);
|
|
127
|
+
connectWS();
|
|
128
|
+
}
|
|
129
|
+
__name(onLiveStart, "onLiveStart");
|
|
130
|
+
async function onLiveEnd(anchor) {
|
|
131
|
+
disconnectWS();
|
|
132
|
+
const result = session.end();
|
|
133
|
+
const text = `【下播提醒】${anchor.info.uname} 结束直播啦!
|
|
134
|
+
|
|
135
|
+
📊 直播时长:${session.formatDuration(Date.now() - liveStartTime)}
|
|
136
|
+
👥 最高在线:${result.maxOnline}
|
|
137
|
+
💬 弹幕统计:${result.danmuCount}
|
|
138
|
+
📈 人均弹幕:${session.avgDanmu}
|
|
139
|
+
⚓ 舰队统计:新增 ${result.guardNew} 人,续费 ${result.guardRenew} 人
|
|
140
|
+
⏰ 下播时间:${formatTime()}
|
|
141
|
+
|
|
142
|
+
感谢大家的陪伴 💖`;
|
|
143
|
+
await send(text);
|
|
144
|
+
}
|
|
145
|
+
__name(onLiveEnd, "onLiveEnd");
|
|
146
|
+
function connectWS() {
|
|
147
|
+
if (ws) return;
|
|
148
|
+
ws = new import_ws.default("wss://broadcastlv.chat.bilibili.com/sub");
|
|
149
|
+
ws.on("open", () => {
|
|
150
|
+
logger.info("WebSocket connected");
|
|
151
|
+
});
|
|
152
|
+
ws.on("message", (data) => {
|
|
153
|
+
try {
|
|
154
|
+
const text = data.toString();
|
|
155
|
+
if (!text.includes("{")) return;
|
|
156
|
+
const msg = JSON.parse(text);
|
|
157
|
+
handleWSMessage(msg);
|
|
158
|
+
} catch {
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
ws.on("close", () => {
|
|
162
|
+
ws = null;
|
|
163
|
+
logger.warn("WebSocket closed");
|
|
266
164
|
});
|
|
267
|
-
debugLog(`主播${mid}状态初始化完成`, anchorStateCache.get(mid));
|
|
268
165
|
}
|
|
269
|
-
__name(
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
if (!state) {
|
|
274
|
-
debugLog(`主播${mid}状态未初始化,先执行初始化`);
|
|
275
|
-
await initAnchorState(mid);
|
|
276
|
-
return;
|
|
277
|
-
}
|
|
278
|
-
const roomInfo = await getRoomBaseInfo(state.roomId);
|
|
279
|
-
const currentOnline = roomInfo.live_status === 1;
|
|
280
|
-
const anchorInfo = await getAnchorInfo(state.roomId);
|
|
281
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
282
|
-
if (!state.isOnline && currentOnline) {
|
|
283
|
-
debugLog(`主播${mid}状态变化:离线 → 在线`);
|
|
284
|
-
const currentStat = await getRoomStat(state.roomId);
|
|
285
|
-
state.isOnline = true;
|
|
286
|
-
state.liveStartTime = roomInfo.live_time || now;
|
|
287
|
-
state.lastStat = currentStat;
|
|
288
|
-
const templateData = {
|
|
289
|
-
uname: anchorInfo.info.uname,
|
|
290
|
-
title: roomInfo.title,
|
|
291
|
-
area: `${roomInfo.parent_area_name || ""} - ${roomInfo.area_name || ""}`,
|
|
292
|
-
url: `https://live.bilibili.com/${state.roomId}`,
|
|
293
|
-
startTime: formatTime(state.liveStartTime),
|
|
294
|
-
cover: import_koishi.segment.image(roomInfo.user_cover || "")
|
|
295
|
-
};
|
|
296
|
-
const message = renderTemplate(finalConfig.templates.liveStart, templateData);
|
|
297
|
-
for (const gid of groupIds) await sendGroupMessage(gid, message);
|
|
298
|
-
ctx.logger.info(`主播${mid}开播,已发送通知到指定群聊`);
|
|
299
|
-
} else if (state.isOnline && !currentOnline && state.liveStartTime > 0) {
|
|
300
|
-
debugLog(`主播${mid}状态变化:在线 → 离线`);
|
|
301
|
-
const currentStat = await getRoomStat(state.roomId);
|
|
302
|
-
const liveStat = calcLiveStat(state.liveStartTime, now, state.lastStat, currentStat);
|
|
303
|
-
state.isOnline = false;
|
|
304
|
-
state.liveStartTime = 0;
|
|
305
|
-
const templateData = {
|
|
306
|
-
uname: anchorInfo.info.uname,
|
|
307
|
-
liveTime: formatSeconds(liveStat.liveTime),
|
|
308
|
-
peakOnline: liveStat.peakOnline.toLocaleString(),
|
|
309
|
-
watchCount: liveStat.watchCount.toLocaleString(),
|
|
310
|
-
dmCount: liveStat.dmCount.toLocaleString(),
|
|
311
|
-
avgDmPerUser: liveStat.avgDmPerUser,
|
|
312
|
-
medalCount: liveStat.medalCount.toLocaleString(),
|
|
313
|
-
fansChange: liveStat.fansChange > 0 ? `+${liveStat.fansChange}` : liveStat.fansChange.toString(),
|
|
314
|
-
endTime: formatTime(liveStat.endTime),
|
|
315
|
-
url: `https://live.bilibili.com/${state.roomId}`
|
|
316
|
-
};
|
|
317
|
-
const message = renderTemplate(finalConfig.templates.liveEnd, templateData);
|
|
318
|
-
for (const gid of groupIds) await sendGroupMessage(gid, message);
|
|
319
|
-
ctx.logger.info(`主播${mid}下播,已发送统计通知到指定群聊`);
|
|
320
|
-
} else if (state.isOnline && currentOnline) {
|
|
321
|
-
debugLog(`主播${mid}持续开播,更新统计数据`);
|
|
322
|
-
const currentStat = await getRoomStat(state.roomId);
|
|
323
|
-
state.lastStat = currentStat;
|
|
324
|
-
} else {
|
|
325
|
-
debugLog(`主播${mid}状态无变化:离线`);
|
|
326
|
-
}
|
|
166
|
+
__name(connectWS, "connectWS");
|
|
167
|
+
function disconnectWS() {
|
|
168
|
+
ws?.close();
|
|
169
|
+
ws = null;
|
|
327
170
|
}
|
|
328
|
-
__name(
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
171
|
+
__name(disconnectWS, "disconnectWS");
|
|
172
|
+
function handleWSMessage(msg) {
|
|
173
|
+
const cmd = msg?.cmd;
|
|
174
|
+
if (cmd === "DANMU_MSG") {
|
|
175
|
+
session.onDanmu();
|
|
332
176
|
}
|
|
333
|
-
if (
|
|
334
|
-
|
|
335
|
-
|
|
177
|
+
if (cmd === "SUPER_CHAT_MESSAGE") {
|
|
178
|
+
const sc = msg.data;
|
|
179
|
+
const price = sc.price * 10;
|
|
180
|
+
const text = `【醒目留言 ${price}电池】
|
|
181
|
+
${sc.user_info.uname}(${sc.uid}):
|
|
182
|
+
${sc.message}`;
|
|
183
|
+
send(text);
|
|
336
184
|
}
|
|
337
|
-
if (
|
|
338
|
-
|
|
339
|
-
|
|
185
|
+
if (cmd === "GUARD_BUY") {
|
|
186
|
+
const g = msg.data;
|
|
187
|
+
session.onGuard(g.num);
|
|
188
|
+
const price = g.price * g.num * 10;
|
|
189
|
+
const text = `【舰队事件】
|
|
190
|
+
${g.username}(${g.uid})
|
|
191
|
+
开通 ${g.gift_name}(${price}电池)`;
|
|
192
|
+
send(text);
|
|
340
193
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
ctx.logger.error(`初始化主播${mid}失败:`, err.message);
|
|
344
|
-
debugLog(`初始化主播${mid}失败详情`, err);
|
|
345
|
-
});
|
|
194
|
+
if (cmd === "ROOM_REAL_TIME_MESSAGE_UPDATE") {
|
|
195
|
+
session.onOnline(msg.data.online);
|
|
346
196
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
const groupIds = groupStr.split(",").filter((g) => g.trim());
|
|
351
|
-
await checkAnchorState(mid, groupIds).catch((err) => {
|
|
352
|
-
ctx.logger.error(`检查主播${mid}失败:`, err.message);
|
|
353
|
-
debugLog(`检查主播${mid}失败详情`, err);
|
|
354
|
-
});
|
|
355
|
-
}
|
|
356
|
-
}, finalConfig.pollInterval * 1e3);
|
|
357
|
-
ctx.logger.info(`bilirice插件已启动,监听主播数:${finalConfig.anchors.length},轮询间隔:${finalConfig.pollInterval}秒`);
|
|
358
|
-
});
|
|
359
|
-
ctx.command("bilirice <mid>", "手动检查指定主播的直播间状态").action(async ({ session }, mid) => {
|
|
360
|
-
if (!mid) return "请输入主播UID";
|
|
361
|
-
if (!finalConfig.bilibiliCookie) return "插件未配置B站Cookie,无法执行操作!";
|
|
197
|
+
}
|
|
198
|
+
__name(handleWSMessage, "handleWSMessage");
|
|
199
|
+
ctx.setInterval(async () => {
|
|
362
200
|
try {
|
|
363
|
-
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
201
|
+
const room = await getRoomInfo();
|
|
202
|
+
const anchor = await getAnchorInfo();
|
|
203
|
+
const living = room.live_status === 1;
|
|
204
|
+
if (living && !isLiving) {
|
|
205
|
+
isLiving = true;
|
|
206
|
+
await onLiveStart(room, anchor);
|
|
207
|
+
}
|
|
208
|
+
if (!living && isLiving) {
|
|
209
|
+
isLiving = false;
|
|
210
|
+
await onLiveEnd(anchor);
|
|
211
|
+
}
|
|
212
|
+
} catch (e) {
|
|
213
|
+
logger.warn(String(e));
|
|
371
214
|
}
|
|
372
|
-
});
|
|
215
|
+
}, config.pollInterval * 1e3);
|
|
373
216
|
}
|
|
374
217
|
__name(apply, "apply");
|
|
375
218
|
// Annotate the CommonJS export names for ESM import in node:
|
|
376
219
|
0 && (module.exports = {
|
|
377
220
|
Config,
|
|
378
221
|
apply,
|
|
222
|
+
inject,
|
|
379
223
|
name
|
|
380
224
|
});
|
package/lib/session.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare class LiveSession {
|
|
2
|
+
private startTime;
|
|
3
|
+
danmuCount: number;
|
|
4
|
+
maxOnline: number;
|
|
5
|
+
guardNew: number;
|
|
6
|
+
guardRenew: number;
|
|
7
|
+
start(): void;
|
|
8
|
+
end(): {
|
|
9
|
+
durationMs: number;
|
|
10
|
+
danmuCount: number;
|
|
11
|
+
maxOnline: number;
|
|
12
|
+
guardNew: number;
|
|
13
|
+
guardRenew: number;
|
|
14
|
+
};
|
|
15
|
+
onDanmu(): void;
|
|
16
|
+
onOnline(count: number): void;
|
|
17
|
+
onGuard(num: number): void;
|
|
18
|
+
get avgDanmu(): string;
|
|
19
|
+
formatDuration(ms: number): string;
|
|
20
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koishi-plugin-bilirice",
|
|
3
3
|
"description": "这是一个bilibili直播间事件监控插件",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.7",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"typings": "lib/index.d.ts",
|
|
7
7
|
"files": [
|
|
@@ -19,6 +19,10 @@
|
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"axios": "^1.13.4",
|
|
22
|
-
"koishi-plugin-adapter-onebot": "^6.8.0"
|
|
22
|
+
"koishi-plugin-adapter-onebot": "^6.8.0",
|
|
23
|
+
"ws": "^8.19.0"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/ws": "^8"
|
|
23
27
|
}
|
|
24
28
|
}
|