koishi-plugin-bilirice 0.0.2 → 0.0.4

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.
Files changed (3) hide show
  1. package/lib/index.d.ts +16 -3
  2. package/lib/index.js +161 -125
  3. package/package.json +1 -1
package/lib/index.d.ts CHANGED
@@ -1,8 +1,20 @@
1
1
  import { Context, Schema } from 'koishi';
2
2
  export declare const name = "bilirice";
3
+ interface BiliriceConfig {
4
+ anchors: [string, string][];
5
+ pollInterval?: number;
6
+ templates?: {
7
+ liveStart?: string;
8
+ liveEnd?: string;
9
+ };
10
+ timeout?: number;
11
+ bilibiliCookie?: string;
12
+ }
3
13
  export declare const Config: Schema<Schemastery.ObjectS<{
4
14
  anchors: Schema<[string?, string?, ...any[]][], [string?, string?, ...any[]][]>;
5
15
  pollInterval: Schema<number, number>;
16
+ timeout: Schema<number, number>;
17
+ bilibiliCookie: Schema<string, string>;
6
18
  templates: Schema<Schemastery.ObjectS<{
7
19
  liveStart: Schema<string, string>;
8
20
  liveEnd: Schema<string, string>;
@@ -10,10 +22,11 @@ export declare const Config: Schema<Schemastery.ObjectS<{
10
22
  liveStart: Schema<string, string>;
11
23
  liveEnd: Schema<string, string>;
12
24
  }>>;
13
- timeout: Schema<number, number>;
14
25
  }>, Schemastery.ObjectT<{
15
26
  anchors: Schema<[string?, string?, ...any[]][], [string?, string?, ...any[]][]>;
16
27
  pollInterval: Schema<number, number>;
28
+ timeout: Schema<number, number>;
29
+ bilibiliCookie: Schema<string, string>;
17
30
  templates: Schema<Schemastery.ObjectS<{
18
31
  liveStart: Schema<string, string>;
19
32
  liveEnd: Schema<string, string>;
@@ -21,6 +34,6 @@ export declare const Config: Schema<Schemastery.ObjectS<{
21
34
  liveStart: Schema<string, string>;
22
35
  liveEnd: Schema<string, string>;
23
36
  }>>;
24
- timeout: Schema<number, number>;
25
37
  }>>;
26
- export declare function apply(ctx: Context, config: any): void;
38
+ export declare function apply(ctx: Context, config: BiliriceConfig): void;
39
+ export {};
package/lib/index.js CHANGED
@@ -37,79 +37,121 @@ __export(src_exports, {
37
37
  module.exports = __toCommonJS(src_exports);
38
38
  var import_koishi = require("koishi");
39
39
  var import_axios = __toESM(require("axios"));
40
+ var import_https = __toESM(require("https"));
40
41
  var name = "bilirice";
41
42
  var Config = import_koishi.Schema.object({
42
43
  anchors: import_koishi.Schema.array(import_koishi.Schema.tuple([
43
- import_koishi.Schema.string().description("主播UID"),
44
- import_koishi.Schema.string().description("通知群号(多个用,分隔,OneBot 格式为 group_xxx)")
45
- ])).required().description("需要监听的主播列表"),
46
- pollInterval: import_koishi.Schema.number().default(10).description("直播间状态轮询间隔(秒,建议≥5"),
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
+ // 控制台显示滑块,更易用
49
+ timeout: import_koishi.Schema.number().default(1e4).description("API请求超时时间(毫秒),建议5000-15000").role("number", { min: 5e3, max: 3e4 }),
50
+ // 核心:Cookie字段适配控制台(密码类型,输入隐藏 + 详细说明)
51
+ bilibiliCookie: import_koishi.Schema.string().description(`【必填】B站登录Cookie(解决-352错误)
52
+ 获取方式:
53
+ 1. 登录B站直播页 https://live.bilibili.com/
54
+ 2. F12 → 网络 → 搜索 getInfoByRoom
55
+ 3. 复制请求头中的「Cookie」完整值
56
+ (包含SESSDATA/bili_jct/DedeUserID等核心字段)`).required().role("password"),
57
+ // 控制台显示为密码输入框,输入内容隐藏
58
+ // 通知模板:控制台支持多行文本编辑
47
59
  templates: import_koishi.Schema.object({
48
- liveStart: import_koishi.Schema.string().default("【{uname} 开播啦】\n标题:{title}\n分区:{area}\n链接:{url}\n开播时间:{startTime}\n封面:{cover}").description("开播通知模板"),
49
- liveEnd: import_koishi.Schema.string().default("【{uname} 下播啦】\n直播时长:{liveTime}\n最高在线:{peakOnline}\n观看人数:{watchCount}\n弹幕数:{dmCount}\n人均弹幕:{avgDmPerUser}\n下播时间:{endTime}").description("下播通知模板")
50
- }).description("自定义通知模板"),
51
- timeout: import_koishi.Schema.number().default(5e3).description("B站API请求超时时间(毫秒)")
60
+ 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"),
61
+ // 控制台显示多行文本框
62
+ 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")
63
+ }).description("自定义通知模板(保留占位符即可自动替换数据)")
52
64
  });
53
65
  function apply(ctx, config) {
54
66
  const finalConfig = {
55
67
  pollInterval: config.pollInterval || 10,
56
- timeout: config.timeout || 5e3,
68
+ timeout: config.timeout || 1e4,
57
69
  templates: {
58
70
  liveStart: config.templates?.liveStart || "【{uname} 开播啦】\n标题:{title}\n分区:{area}\n链接:{url}\n开播时间:{startTime}\n封面:{cover}",
59
71
  liveEnd: config.templates?.liveEnd || "【{uname} 下播啦】\n直播时长:{liveTime}\n最高在线:{peakOnline}\n观看人数:{watchCount}\n弹幕数:{dmCount}\n人均弹幕:{avgDmPerUser}\n下播时间:{endTime}"
60
72
  },
61
- anchors: config.anchors || []
73
+ anchors: config.anchors || [],
74
+ bilibiliCookie: config.bilibiliCookie || ""
62
75
  };
63
76
  const anchorStateCache = /* @__PURE__ */ new Map();
64
77
  const api = import_axios.default.create({
65
78
  baseURL: "https://api.live.bilibili.com",
66
79
  timeout: finalConfig.timeout,
80
+ httpsAgent: new import_https.default.Agent({ rejectUnauthorized: false }),
67
81
  headers: {
68
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36"
69
- }
82
+ "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",
83
+ "Referer": "https://live.bilibili.com/",
84
+ "Origin": "https://live.bilibili.com",
85
+ "Accept": "application/json, text/plain, */*",
86
+ "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8,en-US;q=0.7",
87
+ "Cache-Control": "no-cache",
88
+ "Pragma": "no-cache",
89
+ "Sec-Fetch-Dest": "empty",
90
+ "Sec-Fetch-Mode": "cors",
91
+ "Sec-Fetch-Site": "same-site",
92
+ "Cookie": finalConfig.bilibiliCookie
93
+ // 读取控制台配置的Cookie
94
+ },
95
+ transformResponse: [(data) => data]
70
96
  });
71
- async function getRoomIdByMid(mid) {
72
- try {
73
- const res = await api.get("/room/v1/Room/getRoomInfoOld", { params: { mid } });
74
- if (res.data.code !== 0) throw new Error(`获取直播间ID失败: ${res.data.message}`);
75
- return res.data.data.roomid.toString();
76
- } catch (err) {
77
- ctx.logger.error(`获取roomId失败(mid=${mid}):`, err);
78
- throw err;
97
+ api.interceptors.response.use(
98
+ (response) => {
99
+ try {
100
+ response.data = JSON.parse(response.data);
101
+ return response;
102
+ } catch (err) {
103
+ throw new Error(`API响应解析失败: ${err.message}`);
104
+ }
105
+ },
106
+ (error) => {
107
+ throw new Error(`API请求失败: ${error.message || error.code}`);
79
108
  }
109
+ );
110
+ async function getRoomIdByMid(mid) {
111
+ const res = await api.get("/room/v1/Room/getRoomInfoOld", {
112
+ params: { mid: Number(mid) }
113
+ });
114
+ if (res.data.code !== 0) throw new Error(`获取直播间ID失败: ${res.data.code} ${res.data.message || ""}`);
115
+ return res.data.data.roomid.toString();
80
116
  }
81
117
  __name(getRoomIdByMid, "getRoomIdByMid");
82
118
  async function getRoomBaseInfo(roomId) {
83
- try {
84
- const res = await api.get("/room/v1/Room/get_info", { params: { room_id: roomId } });
85
- if (res.data.code !== 0) throw new Error(`获取直播间信息失败: ${res.data.message}`);
86
- return res.data.data;
87
- } catch (err) {
88
- ctx.logger.error(`获取直播间基础信息失败(roomId=${roomId}):`, err);
89
- throw err;
90
- }
119
+ const res = await api.get("/room/v1/Room/get_info", {
120
+ params: { room_id: Number(roomId) }
121
+ });
122
+ if (res.data.code !== 0) throw new Error(`获取直播间基础信息失败: ${res.data.code} ${res.data.message || ""}`);
123
+ return res.data.data;
91
124
  }
92
125
  __name(getRoomBaseInfo, "getRoomBaseInfo");
93
126
  async function getRoomStat(roomId) {
94
- try {
95
- const res = await api.get("/room/v1/Room/get_stat", { params: { room_id: roomId } });
96
- if (res.data.code !== 0) throw new Error(`获取直播间统计失败: ${res.data.message}`);
97
- return res.data.data;
98
- } catch (err) {
99
- ctx.logger.error(`获取直播间统计失败(roomId=${roomId}):`, err);
100
- throw err;
101
- }
127
+ const res = await api.get("/xlive/web-room/v1/index/getInfoByRoom", {
128
+ params: {
129
+ room_id: Number(roomId),
130
+ platform: "web",
131
+ version: "1.0.0",
132
+ ts: Date.now()
133
+ }
134
+ });
135
+ if (res.data.code !== 0) throw new Error(`获取直播间统计失败: ${res.data.code} ${res.data.message || ""}`);
136
+ const data = res.data.data;
137
+ if (!data || !data.room_info) throw new Error("获取直播间统计失败: 返回数据结构异常");
138
+ return {
139
+ online: data.room_info.online || 0,
140
+ watch_num: data.room_info.watch_num || 0,
141
+ interact_num: data.room_info.interact_num || 0,
142
+ dm_count: data.room_info.dm_count || 0,
143
+ medal_count: data.room_info.medal_count || 0,
144
+ fans_count: data.anchor_info?.base_info?.fans_count || 0,
145
+ live_time: data.room_info.live_time || 0
146
+ };
102
147
  }
103
148
  __name(getRoomStat, "getRoomStat");
104
149
  async function getAnchorInfo(roomId) {
105
- try {
106
- const res = await api.get("/live_user/v1/UserInfo/get_anchor_in_room", { params: { roomid: roomId } });
107
- if (res.data.code !== 0) throw new Error(`获取主播信息失败: ${res.data.message}`);
108
- return res.data.data;
109
- } catch (err) {
110
- ctx.logger.error(`获取主播信息失败(roomId=${roomId}):`, err);
111
- throw err;
112
- }
150
+ const res = await api.get("/live_user/v1/UserInfo/get_anchor_in_room", {
151
+ params: { roomid: Number(roomId) }
152
+ });
153
+ if (res.data.code !== 0) throw new Error(`获取主播信息失败: ${res.data.code} ${res.data.message || ""}`);
154
+ return res.data.data;
113
155
  }
114
156
  __name(getAnchorInfo, "getAnchorInfo");
115
157
  function formatTime(timestamp) {
@@ -132,11 +174,7 @@ function apply(ctx, config) {
132
174
  __name(formatSeconds, "formatSeconds");
133
175
  function calcLiveStat(startTime, endTime, lastStat, currentStat) {
134
176
  let fansChange = 0;
135
- try {
136
- fansChange = currentStat.fans_count - (lastStat?.fans_count || currentStat.fans_count);
137
- } catch (e) {
138
- fansChange = Math.floor(Math.random() * 100);
139
- }
177
+ if (lastStat && currentStat) fansChange = currentStat.fans_count - lastStat.fans_count;
140
178
  return {
141
179
  liveTime: endTime - startTime,
142
180
  peakOnline: currentStat.online,
@@ -144,7 +182,7 @@ function apply(ctx, config) {
144
182
  interactCount: currentStat.interact_num,
145
183
  dmCount: currentStat.dm_count,
146
184
  avgDmPerUser: Number((currentStat.dm_count / Math.max(currentStat.watch_num, 1)).toFixed(2)),
147
- medalCount: currentStat.medal_count || 0,
185
+ medalCount: currentStat.medal_count,
148
186
  fansChange,
149
187
  endTime
150
188
  };
@@ -156,33 +194,23 @@ function apply(ctx, config) {
156
194
  __name(renderTemplate, "renderTemplate");
157
195
  async function sendGroupMessage(groupId, message) {
158
196
  const bot = ctx.bots.values().next().value;
159
- if (!bot) {
160
- ctx.logger.error("未找到可用的Bot实例,无法发送消息");
161
- return;
162
- }
163
- try {
164
- await bot.sendMessage(groupId, message);
165
- } catch (err) {
166
- ctx.logger.error(`发送群消息失败(群号=${groupId}):`, err);
167
- }
197
+ if (!bot) throw new Error("未找到可用的Bot实例");
198
+ await bot.sendMessage(groupId, message);
168
199
  }
169
200
  __name(sendGroupMessage, "sendGroupMessage");
170
201
  async function initAnchorState(mid) {
171
202
  if (anchorStateCache.has(mid)) return;
172
- try {
173
- const roomId = await getRoomIdByMid(mid);
174
- const roomInfo = await getRoomBaseInfo(roomId);
175
- const isOnline = roomInfo.live_status === 1;
176
- anchorStateCache.set(mid, {
177
- mid,
178
- roomId,
179
- isOnline,
180
- liveStartTime: isOnline ? roomInfo.live_time : 0,
181
- lastStat: isOnline ? await getRoomStat(roomId) : null
182
- });
183
- } catch (err) {
184
- ctx.logger.error(`初始化主播状态失败(mid=${mid}):`, err);
185
- }
203
+ const roomId = await getRoomIdByMid(mid);
204
+ const roomInfo = await getRoomBaseInfo(roomId);
205
+ const isOnline = roomInfo.live_status === 1;
206
+ const lastStat = isOnline ? await getRoomStat(roomId) : null;
207
+ anchorStateCache.set(mid, {
208
+ mid,
209
+ roomId,
210
+ isOnline,
211
+ liveStartTime: isOnline ? roomInfo.live_time : 0,
212
+ lastStat
213
+ });
186
214
  }
187
215
  __name(initAnchorState, "initAnchorState");
188
216
  async function checkAnchorState(mid, groupIds) {
@@ -191,75 +219,83 @@ function apply(ctx, config) {
191
219
  await initAnchorState(mid);
192
220
  return;
193
221
  }
194
- try {
195
- const roomInfo = await getRoomBaseInfo(state.roomId);
196
- const currentOnline = roomInfo.live_status === 1;
197
- const anchorInfo = await getAnchorInfo(state.roomId);
222
+ const roomInfo = await getRoomBaseInfo(state.roomId);
223
+ const currentOnline = roomInfo.live_status === 1;
224
+ const anchorInfo = await getAnchorInfo(state.roomId);
225
+ const now = Math.floor(Date.now() / 1e3);
226
+ if (!state.isOnline && currentOnline) {
198
227
  const currentStat = await getRoomStat(state.roomId);
199
- const now = Math.floor(Date.now() / 1e3);
200
- if (!state.isOnline && currentOnline) {
201
- ctx.logger.info(`主播${mid}开播,发送通知`);
202
- state.isOnline = true;
203
- state.liveStartTime = roomInfo.live_time;
204
- state.lastStat = currentStat;
205
- const templateData = {
206
- uname: anchorInfo.info.uname,
207
- title: roomInfo.title,
208
- area: `${roomInfo.parent_area_name} - ${roomInfo.area_name}`,
209
- url: `https://live.bilibili.com/${state.roomId}`,
210
- startTime: formatTime(roomInfo.live_time),
211
- cover: import_koishi.segment.image(roomInfo.user_cover)
212
- };
213
- const message = renderTemplate(finalConfig.templates.liveStart, templateData);
214
- for (const gid of groupIds) await sendGroupMessage(gid, message);
215
- } else if (state.isOnline && !currentOnline && state.liveStartTime > 0) {
216
- ctx.logger.info(`主播${mid}下播,发送通知`);
217
- const liveStat = calcLiveStat(state.liveStartTime, now, state.lastStat, currentStat);
218
- state.isOnline = false;
219
- state.liveStartTime = 0;
220
- const templateData = {
221
- uname: anchorInfo.info.uname,
222
- liveTime: formatSeconds(liveStat.liveTime),
223
- peakOnline: liveStat.peakOnline.toLocaleString(),
224
- watchCount: liveStat.watchCount.toLocaleString(),
225
- interactCount: liveStat.interactCount.toLocaleString(),
226
- dmCount: liveStat.dmCount.toLocaleString(),
227
- avgDmPerUser: liveStat.avgDmPerUser,
228
- medalCount: liveStat.medalCount.toLocaleString(),
229
- fansChange: liveStat.fansChange > 0 ? `+${liveStat.fansChange}` : liveStat.fansChange.toString(),
230
- endTime: formatTime(liveStat.endTime),
231
- url: `https://live.bilibili.com/${state.roomId}`
232
- };
233
- const message = renderTemplate(finalConfig.templates.liveEnd, templateData);
234
- for (const gid of groupIds) await sendGroupMessage(gid, message);
235
- } else if (state.isOnline && currentOnline) {
236
- state.lastStat = currentStat;
237
- }
238
- } catch (err) {
239
- ctx.logger.error(`检查主播状态失败(mid=${mid}):`, err);
228
+ state.isOnline = true;
229
+ state.liveStartTime = roomInfo.live_time || now;
230
+ state.lastStat = currentStat;
231
+ const templateData = {
232
+ uname: anchorInfo.info.uname,
233
+ title: roomInfo.title,
234
+ area: `${roomInfo.parent_area_name || ""} - ${roomInfo.area_name || ""}`,
235
+ url: `https://live.bilibili.com/${state.roomId}`,
236
+ startTime: formatTime(state.liveStartTime),
237
+ cover: import_koishi.segment.image(roomInfo.user_cover || "")
238
+ };
239
+ const message = renderTemplate(finalConfig.templates.liveStart, templateData);
240
+ for (const gid of groupIds) await sendGroupMessage(gid, message);
241
+ ctx.logger.info(`主播${mid}开播,已发送通知到指定群聊`);
242
+ } else if (state.isOnline && !currentOnline && state.liveStartTime > 0) {
243
+ const currentStat = await getRoomStat(state.roomId);
244
+ const liveStat = calcLiveStat(state.liveStartTime, now, state.lastStat, currentStat);
245
+ state.isOnline = false;
246
+ state.liveStartTime = 0;
247
+ const templateData = {
248
+ uname: anchorInfo.info.uname,
249
+ liveTime: formatSeconds(liveStat.liveTime),
250
+ peakOnline: liveStat.peakOnline.toLocaleString(),
251
+ watchCount: liveStat.watchCount.toLocaleString(),
252
+ dmCount: liveStat.dmCount.toLocaleString(),
253
+ avgDmPerUser: liveStat.avgDmPerUser,
254
+ medalCount: liveStat.medalCount.toLocaleString(),
255
+ fansChange: liveStat.fansChange > 0 ? `+${liveStat.fansChange}` : liveStat.fansChange.toString(),
256
+ endTime: formatTime(liveStat.endTime),
257
+ url: `https://live.bilibili.com/${state.roomId}`
258
+ };
259
+ const message = renderTemplate(finalConfig.templates.liveEnd, templateData);
260
+ for (const gid of groupIds) await sendGroupMessage(gid, message);
261
+ ctx.logger.info(`主播${mid}下播,已发送统计通知到指定群聊`);
262
+ } else if (state.isOnline && currentOnline) {
263
+ const currentStat = await getRoomStat(state.roomId);
264
+ state.lastStat = currentStat;
240
265
  }
241
266
  }
242
267
  __name(checkAnchorState, "checkAnchorState");
243
268
  ctx.on("ready", async () => {
244
269
  if (!finalConfig.anchors || finalConfig.anchors.length === 0) {
245
- ctx.logger.warn("bilirice插件未配置任何主播,功能将无法使用!");
270
+ ctx.logger.error("bilirice插件未配置任何主播,无法启动!");
271
+ return;
272
+ }
273
+ if (!finalConfig.bilibiliCookie) {
274
+ ctx.logger.error("bilirice插件未配置B站Cookie,无法解决-352错误,请补充配置!");
246
275
  return;
247
276
  }
248
- for (const [mid, _] of finalConfig.anchors) await initAnchorState(mid);
277
+ for (const [mid, _] of finalConfig.anchors) {
278
+ await initAnchorState(mid).catch((err) => ctx.logger.error(`初始化主播${mid}失败:`, err.message));
279
+ }
249
280
  ctx.setInterval(async () => {
250
281
  for (const [mid, groupStr] of finalConfig.anchors) {
251
282
  const groupIds = groupStr.split(",").filter((g) => g.trim());
252
- await checkAnchorState(mid, groupIds);
283
+ await checkAnchorState(mid, groupIds).catch((err) => ctx.logger.error(`检查主播${mid}失败:`, err.message));
253
284
  }
254
285
  }, finalConfig.pollInterval * 1e3);
255
286
  ctx.logger.info(`bilirice插件已启动,监听主播数:${finalConfig.anchors.length},轮询间隔:${finalConfig.pollInterval}秒`);
256
287
  });
257
288
  ctx.command("bilirice <mid>", "手动检查指定主播的直播间状态").action(async ({ session }, mid) => {
258
289
  if (!mid) return "请输入主播UID";
259
- const groupIds = session.guildId ? [session.guildId] : [];
260
- await initAnchorState(mid);
261
- await checkAnchorState(mid, groupIds);
262
- return `已检查主播${mid}的直播间状态(bilirice插件)`;
290
+ if (!finalConfig.bilibiliCookie) return "插件未配置B站Cookie,无法执行操作!";
291
+ try {
292
+ const groupIds = session.guildId ? [session.guildId] : [];
293
+ await initAnchorState(mid);
294
+ await checkAnchorState(mid, groupIds);
295
+ return `已检查主播${mid}的直播间状态(含完整统计数据)`;
296
+ } catch (err) {
297
+ return `检查失败:${err.message}`;
298
+ }
263
299
  });
264
300
  }
265
301
  __name(apply, "apply");
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-bilirice",
3
3
  "description": "这是一个bilibili直播间事件监控插件",
4
- "version": "0.0.2",
4
+ "version": "0.0.4",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [