koishi-plugin-bilirice 0.0.1

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 ADDED
@@ -0,0 +1,26 @@
1
+ import { Context, Schema } from 'koishi';
2
+ export declare const name = "bilirice";
3
+ export declare const Config: Schema<Schemastery.ObjectS<{
4
+ anchors: Schema<[string?, string?, ...any[]][], [string?, string?, ...any[]][]>;
5
+ pollInterval: Schema<number, number>;
6
+ templates: Schema<Schemastery.ObjectS<{
7
+ liveStart: Schema<string, string>;
8
+ liveEnd: Schema<string, string>;
9
+ }>, Schemastery.ObjectT<{
10
+ liveStart: Schema<string, string>;
11
+ liveEnd: Schema<string, string>;
12
+ }>>;
13
+ timeout: Schema<number, number>;
14
+ }>, Schemastery.ObjectT<{
15
+ anchors: Schema<[string?, string?, ...any[]][], [string?, string?, ...any[]][]>;
16
+ pollInterval: Schema<number, number>;
17
+ templates: Schema<Schemastery.ObjectS<{
18
+ liveStart: Schema<string, string>;
19
+ liveEnd: Schema<string, string>;
20
+ }>, Schemastery.ObjectT<{
21
+ liveStart: Schema<string, string>;
22
+ liveEnd: Schema<string, string>;
23
+ }>>;
24
+ timeout: Schema<number, number>;
25
+ }>>;
26
+ export declare function apply(ctx: Context, config: any): void;
package/lib/index.js ADDED
@@ -0,0 +1,271 @@
1
+ var __create = Object.create;
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getProtoOf = Object.getPrototypeOf;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
8
+ var __export = (target, all) => {
9
+ for (var name2 in all)
10
+ __defProp(target, name2, { get: all[name2], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var src_exports = {};
32
+ __export(src_exports, {
33
+ Config: () => Config,
34
+ apply: () => apply,
35
+ name: () => name
36
+ });
37
+ module.exports = __toCommonJS(src_exports);
38
+ var import_koishi = require("koishi");
39
+ var import_axios = __toESM(require("axios"));
40
+ var name = "bilirice";
41
+ var Config = import_koishi.Schema.object({
42
+ 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)"),
47
+ 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请求超时时间(毫秒)")
52
+ });
53
+ function apply(ctx, config) {
54
+ const finalConfig = {
55
+ pollInterval: config.pollInterval || 10,
56
+ timeout: config.timeout || 5e3,
57
+ templates: {
58
+ liveStart: config.templates?.liveStart || "【{uname} 开播啦】\n标题:{title}\n分区:{area}\n链接:{url}\n开播时间:{startTime}\n封面:{cover}",
59
+ liveEnd: config.templates?.liveEnd || "【{uname} 下播啦】\n直播时长:{liveTime}\n最高在线:{peakOnline}\n观看人数:{watchCount}\n弹幕数:{dmCount}\n人均弹幕:{avgDmPerUser}\n下播时间:{endTime}"
60
+ },
61
+ anchors: config.anchors || []
62
+ };
63
+ const anchorStateCache = /* @__PURE__ */ new Map();
64
+ const api = import_axios.default.create({
65
+ baseURL: "https://api.live.bilibili.com",
66
+ timeout: finalConfig.timeout,
67
+ 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
+ }
70
+ });
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;
79
+ }
80
+ }
81
+ __name(getRoomIdByMid, "getRoomIdByMid");
82
+ 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
+ }
91
+ }
92
+ __name(getRoomBaseInfo, "getRoomBaseInfo");
93
+ 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
+ }
102
+ }
103
+ __name(getRoomStat, "getRoomStat");
104
+ 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
+ }
113
+ }
114
+ __name(getAnchorInfo, "getAnchorInfo");
115
+ function formatTime(timestamp) {
116
+ return new Date(timestamp * 1e3).toLocaleString("zh-CN", {
117
+ year: "numeric",
118
+ month: "2-digit",
119
+ day: "2-digit",
120
+ hour: "2-digit",
121
+ minute: "2-digit",
122
+ second: "2-digit"
123
+ });
124
+ }
125
+ __name(formatTime, "formatTime");
126
+ function formatSeconds(seconds) {
127
+ const h = Math.floor(seconds / 3600);
128
+ const m = Math.floor(seconds % 3600 / 60);
129
+ const s = seconds % 60;
130
+ return [h, m, s].map((v) => v.toString().padStart(2, "0")).join(":");
131
+ }
132
+ __name(formatSeconds, "formatSeconds");
133
+ function calcLiveStat(startTime, endTime, lastStat, currentStat) {
134
+ 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
+ }
140
+ return {
141
+ liveTime: endTime - startTime,
142
+ peakOnline: currentStat.online,
143
+ watchCount: currentStat.watch_num,
144
+ interactCount: currentStat.interact_num,
145
+ dmCount: currentStat.dm_count,
146
+ avgDmPerUser: Number((currentStat.dm_count / Math.max(currentStat.watch_num, 1)).toFixed(2)),
147
+ medalCount: currentStat.medal_count || 0,
148
+ fansChange,
149
+ endTime
150
+ };
151
+ }
152
+ __name(calcLiveStat, "calcLiveStat");
153
+ function renderTemplate(template, data) {
154
+ return template.replace(/\{(\w+)\}/g, (_, key) => data[key] ?? `{${key}}`);
155
+ }
156
+ __name(renderTemplate, "renderTemplate");
157
+ async function sendGroupMessage(groupId, message) {
158
+ 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
+ }
168
+ }
169
+ __name(sendGroupMessage, "sendGroupMessage");
170
+ async function initAnchorState(mid) {
171
+ 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
+ }
186
+ }
187
+ __name(initAnchorState, "initAnchorState");
188
+ async function checkAnchorState(mid, groupIds) {
189
+ const state = anchorStateCache.get(mid);
190
+ if (!state) {
191
+ await initAnchorState(mid);
192
+ return;
193
+ }
194
+ try {
195
+ const roomInfo = await getRoomBaseInfo(state.roomId);
196
+ const currentOnline = roomInfo.live_status === 1;
197
+ const anchorInfo = await getAnchorInfo(state.roomId);
198
+ 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);
240
+ }
241
+ }
242
+ __name(checkAnchorState, "checkAnchorState");
243
+ ctx.on("ready", async () => {
244
+ if (!finalConfig.anchors || finalConfig.anchors.length === 0) {
245
+ ctx.logger.warn("bilirice插件未配置任何主播,功能将无法使用!");
246
+ return;
247
+ }
248
+ for (const [mid, _] of finalConfig.anchors) await initAnchorState(mid);
249
+ ctx.setInterval(async () => {
250
+ for (const [mid, groupStr] of finalConfig.anchors) {
251
+ const groupIds = groupStr.split(",").filter((g) => g.trim());
252
+ await checkAnchorState(mid, groupIds);
253
+ }
254
+ }, finalConfig.pollInterval * 1e3);
255
+ ctx.logger.info(`bilirice插件已启动,监听主播数:${finalConfig.anchors.length},轮询间隔:${finalConfig.pollInterval}秒`);
256
+ });
257
+ ctx.command("bilirice <mid>", "手动检查指定主播的直播间状态").action(async ({ session }, mid) => {
258
+ if (!mid) return "请输入主播UID";
259
+ const groupIds = session.guildId ? [session.guildId] : [];
260
+ await initAnchorState(mid);
261
+ await checkAnchorState(mid, groupIds);
262
+ return `已检查主播${mid}的直播间状态(bilirice插件)`;
263
+ });
264
+ }
265
+ __name(apply, "apply");
266
+ // Annotate the CommonJS export names for ESM import in node:
267
+ 0 && (module.exports = {
268
+ Config,
269
+ apply,
270
+ name
271
+ });
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "koishi-plugin-bilirice",
3
+ "description": "这是一个bilibili直播间事件监控插件",
4
+ "version": "0.0.1",
5
+ "main": "lib/index.js",
6
+ "typings": "lib/index.d.ts",
7
+ "files": [
8
+ "lib",
9
+ "dist"
10
+ ],
11
+ "license": "MIT",
12
+ "keywords": [
13
+ "chatbot",
14
+ "koishi",
15
+ "plugin"
16
+ ],
17
+ "peerDependencies": {
18
+ "koishi": "^4.18.7"
19
+ },
20
+ "dependencies": {
21
+ "axios": "^1.13.4",
22
+ "koishi-plugin-adapter-onebot": "^6.8.0"
23
+ }
24
+ }
package/readme.md ADDED
@@ -0,0 +1,118 @@
1
+ 一款为 Koishi 框架开发的 B 站直播间监控插件,支持实时监听指定 UP 主的开播 / 下播状态,并自动发送通知到指定 QQ 群,同时提供下播数据统计、自定义通知模板等功能。
2
+ 功能特性
3
+ • 🚨 实时监控:定时轮询 B 站直播 API,检测 UP 主开播 / 下播状态
4
+ • 📢 自动通知:开播 / 下播时自动发送消息到指定 QQ 群
5
+ • 📊 数据统计:下播时展示直播时长、最高在线人数、弹幕数等数据
6
+ • ✨ 自定义模板:支持自定义开播 / 下播通知的文案格式
7
+ • ⚙️ 灵活配置:可自定义轮询间隔、API 超时时间等参数
8
+ • 📝 手动指令:支持手动触发检查指定 UP 主的直播间状态
9
+ 安装要求
10
+ • Koishi v4.x+
11
+ • Node.js 16.x+
12
+ • 已安装 OneBot 适配器(如 go-cqhttp)并配置好 QQ 机器人
13
+ 安装步骤
14
+ 1. 安装依赖
15
+ 在 Koishi 项目根目录执行:
16
+ npm install axios
17
+ 2. 部署插件
18
+ 将插件文件(bilirice.ts/bilirice.js)放入 Koishi 项目的 plugins 目录下。
19
+ 3. 启用插件
20
+ 方式 1:配置文件(推荐)
21
+ 编辑 koishi.config.js,添加插件配置:
22
+ module.exports = {
23
+ bots: [{
24
+ type: 'onebot:http',
25
+ endpoint: 'http://127.0.0.1:5700', // go-cqhttp 地址
26
+ selfId: '123456789', // 你的 QQ 机器人账号
27
+ }],
28
+ selfUrl: 'http://localhost:5140', // 解决控制台静态资源警告
29
+ plugins: {
30
+ // 启用 bilirice 插件
31
+ bilirice: {
32
+ // 必填:监听的主播列表
33
+ anchors: [
34
+ ['14663353', 'group_123456789'], // [UP主UID, 通知群号(OneBot格式)]
35
+ // ['20206767', 'group_987654321,group_876543210'], // 多个群号用逗号分隔
36
+ ],
37
+ // 可选配置(以下为默认值)
38
+ pollInterval: 10, // 轮询间隔(秒)
39
+ timeout: 5000, // API 请求超时时间(毫秒)
40
+ // 可选:自定义通知模板
41
+ templates: {
42
+ liveStart: '🎉 {uname} 开播啦!\n标题:{title}\n分区:{area}\n链接:{url}\n{cover}',
43
+ liveEnd: '🛑 {uname} 下播啦!\n直播时长:{liveTime}\n最高在线:{peakOnline}人\n弹幕数:{dmCount}',
44
+ },
45
+ },
46
+ // 可选:启用 Koishi 控制台
47
+ '@koishijs/plugin-console': { port: 5140 },
48
+ },
49
+ };
50
+ 方式 2:控制台可视化配置
51
+ 1. 启动 Koishi 后访问控制台(默认:http://localhost:5140)
52
+ 2. 进入「本地插件」→ 找到 bilirice 插件 → 点击「配置」
53
+ 3. 填写配置项后保存,重启插件即可
54
+ 配置项说明
55
+ 配置项 类型 必填 默认值 说明
56
+ anchors [string, string][] 是 - 监听的主播列表,格式:[UP主UID, 通知群号];多个群号用逗号分隔,OneBot 适配器群号需加 group_ 前缀(如 group_123456789)pollInterval number 否 10 直播间状态轮询间隔(秒),建议≥5 秒,避免频繁请求 API
57
+ timeout number 否 5000 B站API请求超时时间(毫秒)
58
+ templates.liveStart string 否 详见配置示例
59
+ 开播通知模板,支持以下占位符:
60
+ - {uname}:UP 主昵称
61
+ - {title}:直播间标题
62
+ - {area}:直播分区
63
+ - {url}:直播间链接
64
+ - {startTime}:开播时间
65
+ - {cover}:直播间封面图片
66
+ templates.liveEnd string 否 详见配置示例
67
+ 下播通知模板,除开播模板占位符外,还支持:
68
+ - {liveTime}:直播时长(时:分: 秒)
69
+ - {peakOnline}:最高在线人数
70
+ - {watchCount}:观看人数
71
+ - {dmCount}:弹幕数
72
+ - {avgDmPerUser}:人均弹幕数
73
+ - {medalCount}:舰队数
74
+ - {endTime}:下播时间
75
+ 使用方法
76
+ 1. 自动监控
77
+ 插件启动后会自动初始化监听列表中的 UP 主,并按配置的轮询间隔检查状态,开播 / 下播时自动发送通知到指定群聊。
78
+ 2. 手动指令
79
+ 在 QQ 群 / 私聊中发送指令,手动检查指定 UP 主的直播间状态:
80
+ bilirice <UP主UID>
81
+ 示例:
82
+ bilirice 14663353
83
+ 执行后会返回检查结果,并同步发送通知到当前群聊(仅群聊环境有效)。
84
+ 通知示例
85
+ 开播通知
86
+ 🎉 老番茄 开播啦!
87
+ 标题:【直播】玩点新游戏!
88
+ 分区:游戏 - 单机游戏
89
+ 链接:https://live.bilibili.com/123456
90
+ [图片:直播间封面]
91
+ 下播通知
92
+ 🛑 老番茄 下播啦!
93
+ 直播时长:01:20:30
94
+ 最高在线:12345人
95
+ 观看人数:123456
96
+ 弹幕数:65432
97
+ 人均弹幕:0.53
98
+ 注意事项
99
+ 1. 群号格式:使用 OneBot 适配器(如 go-cqhttp)时,群号必须加 group_ 前缀(如 group_123456789);其他适配器直接填写群号即可。
100
+ 2. API 限制:请勿将轮询间隔设置过小(<5 秒),避免触发 B 站 API 频率限制。
101
+ 3. 机器人权限:确保 QQ 机器人已加入目标群聊,且拥有发送消息、发送图片的权限。
102
+ 4. 隐私说明:插件仅用于个人 / 非商业用途,请勿滥用 B 站 API,遵守平台规范。
103
+ 5. 依赖检查:确保已安装 axios 依赖,否则插件会启动失败。
104
+ 常见问题
105
+ Q1: 报错 $.anchors missing required value
106
+ 原因:未配置必填项 anchors。解决:在配置文件中补充 anchors 配置,填写正确的 UP 主 UID 和群号。
107
+ Q2: 提示 未找到可用的Bot实例
108
+ 原因:机器人未登录 / 适配器配置错误。解决:检查 bots 配置中的 selfId、endpoint 是否正确,确保 go-cqhttp 已正常运行并连接。
109
+ Q3: 插件启动但不发送通知
110
+ 解决步骤:
111
+ 1. 检查群号格式是否正确(OneBot 需加 group_ 前缀);
112
+ 2. 确认机器人已加入目标群聊;
113
+ 3. 查看 Koishi 日志,检查是否有 API 请求失败的错误;
114
+ 4. 验证 UP 主 UID 是否正确,且当前是否在开播状态。
115
+ Q4: 控制台显示 assets missing config selfUrl
116
+ 解决:在配置文件中添加 selfUrl: 'http://localhost:5140' 即可消除警告。
117
+ 许可证
118
+ 本插件基于 MIT 许可证开源,仅供学习和非商业使用。使用本插件需遵守 B 站平台规范,请勿用于违法违规场景。