koishi-plugin-bilirice 0.0.4 → 0.0.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.d.ts +3 -0
- package/lib/index.js +89 -16
- package/package.json +1 -1
package/lib/index.d.ts
CHANGED
|
@@ -9,12 +9,14 @@ interface BiliriceConfig {
|
|
|
9
9
|
};
|
|
10
10
|
timeout?: number;
|
|
11
11
|
bilibiliCookie?: string;
|
|
12
|
+
debugMode?: boolean;
|
|
12
13
|
}
|
|
13
14
|
export declare const Config: Schema<Schemastery.ObjectS<{
|
|
14
15
|
anchors: Schema<[string?, string?, ...any[]][], [string?, string?, ...any[]][]>;
|
|
15
16
|
pollInterval: Schema<number, number>;
|
|
16
17
|
timeout: Schema<number, number>;
|
|
17
18
|
bilibiliCookie: Schema<string, string>;
|
|
19
|
+
debugMode: Schema<boolean, boolean>;
|
|
18
20
|
templates: Schema<Schemastery.ObjectS<{
|
|
19
21
|
liveStart: Schema<string, string>;
|
|
20
22
|
liveEnd: Schema<string, string>;
|
|
@@ -27,6 +29,7 @@ export declare const Config: Schema<Schemastery.ObjectS<{
|
|
|
27
29
|
pollInterval: Schema<number, number>;
|
|
28
30
|
timeout: Schema<number, number>;
|
|
29
31
|
bilibiliCookie: Schema<string, string>;
|
|
32
|
+
debugMode: Schema<boolean, boolean>;
|
|
30
33
|
templates: Schema<Schemastery.ObjectS<{
|
|
31
34
|
liveStart: Schema<string, string>;
|
|
32
35
|
liveEnd: Schema<string, string>;
|
package/lib/index.js
CHANGED
|
@@ -45,20 +45,18 @@ var Config = import_koishi.Schema.object({
|
|
|
45
45
|
import_koishi.Schema.string().description("通知群号(OneBot格式加group_前缀,如group_123456789)")
|
|
46
46
|
])).required().description("监听的主播列表 → 点击「添加项」可配置多个主播").role("table"),
|
|
47
47
|
pollInterval: import_koishi.Schema.number().default(10).description("轮询间隔(秒),建议≥5秒(避免触发B站API风控)").role("slider", { min: 5, max: 30, step: 1 }),
|
|
48
|
-
// 控制台显示滑块,更易用
|
|
49
48
|
timeout: import_koishi.Schema.number().default(1e4).description("API请求超时时间(毫秒),建议5000-15000").role("number", { min: 5e3, max: 3e4 }),
|
|
50
|
-
// 核心:Cookie字段适配控制台(密码类型,输入隐藏 + 详细说明)
|
|
51
49
|
bilibiliCookie: import_koishi.Schema.string().description(`【必填】B站登录Cookie(解决-352错误)
|
|
52
50
|
获取方式:
|
|
53
51
|
1. 登录B站直播页 https://live.bilibili.com/
|
|
54
52
|
2. F12 → 网络 → 搜索 getInfoByRoom
|
|
55
53
|
3. 复制请求头中的「Cookie」完整值
|
|
56
54
|
(包含SESSDATA/bili_jct/DedeUserID等核心字段)`).required().role("password"),
|
|
57
|
-
//
|
|
58
|
-
|
|
55
|
+
// 新增:调试模式开关(控制台显示为开关按钮)
|
|
56
|
+
debugMode: import_koishi.Schema.boolean().default(false).description("调试模式:开启后打印API请求/响应、状态变化等详细日志,方便排查问题").role("switch"),
|
|
57
|
+
// 控制台显示为开关,一键开启/关闭
|
|
59
58
|
templates: import_koishi.Schema.object({
|
|
60
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"),
|
|
61
|
-
// 控制台显示多行文本框
|
|
62
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")
|
|
63
61
|
}).description("自定义通知模板(保留占位符即可自动替换数据)")
|
|
64
62
|
});
|
|
@@ -71,9 +69,20 @@ function apply(ctx, config) {
|
|
|
71
69
|
liveEnd: config.templates?.liveEnd || "【{uname} 下播啦】\n直播时长:{liveTime}\n最高在线:{peakOnline}\n观看人数:{watchCount}\n弹幕数:{dmCount}\n人均弹幕:{avgDmPerUser}\n下播时间:{endTime}"
|
|
72
70
|
},
|
|
73
71
|
anchors: config.anchors || [],
|
|
74
|
-
bilibiliCookie: config.bilibiliCookie || ""
|
|
72
|
+
bilibiliCookie: config.bilibiliCookie || "",
|
|
73
|
+
debugMode: config.debugMode || false
|
|
74
|
+
// 读取调试模式配置
|
|
75
75
|
};
|
|
76
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");
|
|
77
86
|
const api = import_axios.default.create({
|
|
78
87
|
baseURL: "https://api.live.bilibili.com",
|
|
79
88
|
timeout: finalConfig.timeout,
|
|
@@ -90,40 +99,64 @@ function apply(ctx, config) {
|
|
|
90
99
|
"Sec-Fetch-Mode": "cors",
|
|
91
100
|
"Sec-Fetch-Site": "same-site",
|
|
92
101
|
"Cookie": finalConfig.bilibiliCookie
|
|
93
|
-
// 读取控制台配置的Cookie
|
|
94
102
|
},
|
|
95
103
|
transformResponse: [(data) => data]
|
|
96
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
|
+
});
|
|
97
117
|
api.interceptors.response.use(
|
|
98
118
|
(response) => {
|
|
99
119
|
try {
|
|
100
120
|
response.data = JSON.parse(response.data);
|
|
121
|
+
debugLog(`API响应:${response.config.url}`, response.data);
|
|
101
122
|
return response;
|
|
102
123
|
} catch (err) {
|
|
124
|
+
debugLog(`API响应解析失败`, err);
|
|
103
125
|
throw new Error(`API响应解析失败: ${err.message}`);
|
|
104
126
|
}
|
|
105
127
|
},
|
|
106
128
|
(error) => {
|
|
129
|
+
debugLog(`API请求失败`, {
|
|
130
|
+
url: error.config?.url,
|
|
131
|
+
code: error.code,
|
|
132
|
+
response: error.response?.data
|
|
133
|
+
});
|
|
107
134
|
throw new Error(`API请求失败: ${error.message || error.code}`);
|
|
108
135
|
}
|
|
109
136
|
);
|
|
110
137
|
async function getRoomIdByMid(mid) {
|
|
138
|
+
debugLog(`开始获取主播${mid}的直播间ID`);
|
|
111
139
|
const res = await api.get("/room/v1/Room/getRoomInfoOld", {
|
|
112
140
|
params: { mid: Number(mid) }
|
|
113
141
|
});
|
|
114
142
|
if (res.data.code !== 0) throw new Error(`获取直播间ID失败: ${res.data.code} ${res.data.message || ""}`);
|
|
115
|
-
|
|
143
|
+
const roomId = res.data.data.roomid.toString();
|
|
144
|
+
debugLog(`主播${mid}的直播间ID:${roomId}`);
|
|
145
|
+
return roomId;
|
|
116
146
|
}
|
|
117
147
|
__name(getRoomIdByMid, "getRoomIdByMid");
|
|
118
148
|
async function getRoomBaseInfo(roomId) {
|
|
149
|
+
debugLog(`开始获取直播间${roomId}基础信息`);
|
|
119
150
|
const res = await api.get("/room/v1/Room/get_info", {
|
|
120
151
|
params: { room_id: Number(roomId) }
|
|
121
152
|
});
|
|
122
153
|
if (res.data.code !== 0) throw new Error(`获取直播间基础信息失败: ${res.data.code} ${res.data.message || ""}`);
|
|
154
|
+
debugLog(`直播间${roomId}基础信息`, res.data.data);
|
|
123
155
|
return res.data.data;
|
|
124
156
|
}
|
|
125
157
|
__name(getRoomBaseInfo, "getRoomBaseInfo");
|
|
126
158
|
async function getRoomStat(roomId) {
|
|
159
|
+
debugLog(`开始获取直播间${roomId}统计数据`);
|
|
127
160
|
const res = await api.get("/xlive/web-room/v1/index/getInfoByRoom", {
|
|
128
161
|
params: {
|
|
129
162
|
room_id: Number(roomId),
|
|
@@ -135,7 +168,7 @@ function apply(ctx, config) {
|
|
|
135
168
|
if (res.data.code !== 0) throw new Error(`获取直播间统计失败: ${res.data.code} ${res.data.message || ""}`);
|
|
136
169
|
const data = res.data.data;
|
|
137
170
|
if (!data || !data.room_info) throw new Error("获取直播间统计失败: 返回数据结构异常");
|
|
138
|
-
|
|
171
|
+
const stat = {
|
|
139
172
|
online: data.room_info.online || 0,
|
|
140
173
|
watch_num: data.room_info.watch_num || 0,
|
|
141
174
|
interact_num: data.room_info.interact_num || 0,
|
|
@@ -144,18 +177,22 @@ function apply(ctx, config) {
|
|
|
144
177
|
fans_count: data.anchor_info?.base_info?.fans_count || 0,
|
|
145
178
|
live_time: data.room_info.live_time || 0
|
|
146
179
|
};
|
|
180
|
+
debugLog(`直播间${roomId}统计数据`, stat);
|
|
181
|
+
return stat;
|
|
147
182
|
}
|
|
148
183
|
__name(getRoomStat, "getRoomStat");
|
|
149
184
|
async function getAnchorInfo(roomId) {
|
|
185
|
+
debugLog(`开始获取直播间${roomId}主播信息`);
|
|
150
186
|
const res = await api.get("/live_user/v1/UserInfo/get_anchor_in_room", {
|
|
151
187
|
params: { roomid: Number(roomId) }
|
|
152
188
|
});
|
|
153
189
|
if (res.data.code !== 0) throw new Error(`获取主播信息失败: ${res.data.code} ${res.data.message || ""}`);
|
|
190
|
+
debugLog(`直播间${roomId}主播信息`, res.data.data);
|
|
154
191
|
return res.data.data;
|
|
155
192
|
}
|
|
156
193
|
__name(getAnchorInfo, "getAnchorInfo");
|
|
157
194
|
function formatTime(timestamp) {
|
|
158
|
-
|
|
195
|
+
const formatted = new Date(timestamp * 1e3).toLocaleString("zh-CN", {
|
|
159
196
|
year: "numeric",
|
|
160
197
|
month: "2-digit",
|
|
161
198
|
day: "2-digit",
|
|
@@ -163,19 +200,24 @@ function apply(ctx, config) {
|
|
|
163
200
|
minute: "2-digit",
|
|
164
201
|
second: "2-digit"
|
|
165
202
|
});
|
|
203
|
+
debugLog(`时间戳${timestamp}格式化结果:${formatted}`);
|
|
204
|
+
return formatted;
|
|
166
205
|
}
|
|
167
206
|
__name(formatTime, "formatTime");
|
|
168
207
|
function formatSeconds(seconds) {
|
|
169
208
|
const h = Math.floor(seconds / 3600);
|
|
170
209
|
const m = Math.floor(seconds % 3600 / 60);
|
|
171
210
|
const s = seconds % 60;
|
|
172
|
-
|
|
211
|
+
const formatted = [h, m, s].map((v) => v.toString().padStart(2, "0")).join(":");
|
|
212
|
+
debugLog(`秒数${seconds}格式化结果:${formatted}`);
|
|
213
|
+
return formatted;
|
|
173
214
|
}
|
|
174
215
|
__name(formatSeconds, "formatSeconds");
|
|
175
216
|
function calcLiveStat(startTime, endTime, lastStat, currentStat) {
|
|
217
|
+
debugLog(`计算下播统计数据`, { startTime, endTime, lastStat, currentStat });
|
|
176
218
|
let fansChange = 0;
|
|
177
219
|
if (lastStat && currentStat) fansChange = currentStat.fans_count - lastStat.fans_count;
|
|
178
|
-
|
|
220
|
+
const stat = {
|
|
179
221
|
liveTime: endTime - startTime,
|
|
180
222
|
peakOnline: currentStat.online,
|
|
181
223
|
watchCount: currentStat.watch_num,
|
|
@@ -186,20 +228,31 @@ function apply(ctx, config) {
|
|
|
186
228
|
fansChange,
|
|
187
229
|
endTime
|
|
188
230
|
};
|
|
231
|
+
debugLog(`下播统计数据计算结果`, stat);
|
|
232
|
+
return stat;
|
|
189
233
|
}
|
|
190
234
|
__name(calcLiveStat, "calcLiveStat");
|
|
191
235
|
function renderTemplate(template, data) {
|
|
192
|
-
|
|
236
|
+
debugLog(`渲染模板`, { template, data });
|
|
237
|
+
const result = template.replace(/\{(\w+)\}/g, (_, key) => data[key] ?? `{${key}}`);
|
|
238
|
+
debugLog(`模板渲染结果`, result);
|
|
239
|
+
return result;
|
|
193
240
|
}
|
|
194
241
|
__name(renderTemplate, "renderTemplate");
|
|
195
242
|
async function sendGroupMessage(groupId, message) {
|
|
243
|
+
debugLog(`准备发送群消息`, { groupId, message });
|
|
196
244
|
const bot = ctx.bots.values().next().value;
|
|
197
245
|
if (!bot) throw new Error("未找到可用的Bot实例");
|
|
198
246
|
await bot.sendMessage(groupId, message);
|
|
247
|
+
debugLog(`群消息发送成功:${groupId}`);
|
|
199
248
|
}
|
|
200
249
|
__name(sendGroupMessage, "sendGroupMessage");
|
|
201
250
|
async function initAnchorState(mid) {
|
|
202
|
-
if (anchorStateCache.has(mid))
|
|
251
|
+
if (anchorStateCache.has(mid)) {
|
|
252
|
+
debugLog(`主播${mid}状态已缓存,跳过初始化`);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
debugLog(`开始初始化主播${mid}状态`);
|
|
203
256
|
const roomId = await getRoomIdByMid(mid);
|
|
204
257
|
const roomInfo = await getRoomBaseInfo(roomId);
|
|
205
258
|
const isOnline = roomInfo.live_status === 1;
|
|
@@ -211,11 +264,14 @@ function apply(ctx, config) {
|
|
|
211
264
|
liveStartTime: isOnline ? roomInfo.live_time : 0,
|
|
212
265
|
lastStat
|
|
213
266
|
});
|
|
267
|
+
debugLog(`主播${mid}状态初始化完成`, anchorStateCache.get(mid));
|
|
214
268
|
}
|
|
215
269
|
__name(initAnchorState, "initAnchorState");
|
|
216
270
|
async function checkAnchorState(mid, groupIds) {
|
|
271
|
+
debugLog(`开始检查主播${mid}状态`, { groupIds });
|
|
217
272
|
const state = anchorStateCache.get(mid);
|
|
218
273
|
if (!state) {
|
|
274
|
+
debugLog(`主播${mid}状态未初始化,先执行初始化`);
|
|
219
275
|
await initAnchorState(mid);
|
|
220
276
|
return;
|
|
221
277
|
}
|
|
@@ -224,6 +280,7 @@ function apply(ctx, config) {
|
|
|
224
280
|
const anchorInfo = await getAnchorInfo(state.roomId);
|
|
225
281
|
const now = Math.floor(Date.now() / 1e3);
|
|
226
282
|
if (!state.isOnline && currentOnline) {
|
|
283
|
+
debugLog(`主播${mid}状态变化:离线 → 在线`);
|
|
227
284
|
const currentStat = await getRoomStat(state.roomId);
|
|
228
285
|
state.isOnline = true;
|
|
229
286
|
state.liveStartTime = roomInfo.live_time || now;
|
|
@@ -240,6 +297,7 @@ function apply(ctx, config) {
|
|
|
240
297
|
for (const gid of groupIds) await sendGroupMessage(gid, message);
|
|
241
298
|
ctx.logger.info(`主播${mid}开播,已发送通知到指定群聊`);
|
|
242
299
|
} else if (state.isOnline && !currentOnline && state.liveStartTime > 0) {
|
|
300
|
+
debugLog(`主播${mid}状态变化:在线 → 离线`);
|
|
243
301
|
const currentStat = await getRoomStat(state.roomId);
|
|
244
302
|
const liveStat = calcLiveStat(state.liveStartTime, now, state.lastStat, currentStat);
|
|
245
303
|
state.isOnline = false;
|
|
@@ -260,12 +318,18 @@ function apply(ctx, config) {
|
|
|
260
318
|
for (const gid of groupIds) await sendGroupMessage(gid, message);
|
|
261
319
|
ctx.logger.info(`主播${mid}下播,已发送统计通知到指定群聊`);
|
|
262
320
|
} else if (state.isOnline && currentOnline) {
|
|
321
|
+
debugLog(`主播${mid}持续开播,更新统计数据`);
|
|
263
322
|
const currentStat = await getRoomStat(state.roomId);
|
|
264
323
|
state.lastStat = currentStat;
|
|
324
|
+
} else {
|
|
325
|
+
debugLog(`主播${mid}状态无变化:离线`);
|
|
265
326
|
}
|
|
266
327
|
}
|
|
267
328
|
__name(checkAnchorState, "checkAnchorState");
|
|
268
329
|
ctx.on("ready", async () => {
|
|
330
|
+
if (finalConfig.debugMode) {
|
|
331
|
+
ctx.logger.info("bilirice插件调试模式已开启,将打印详细日志!");
|
|
332
|
+
}
|
|
269
333
|
if (!finalConfig.anchors || finalConfig.anchors.length === 0) {
|
|
270
334
|
ctx.logger.error("bilirice插件未配置任何主播,无法启动!");
|
|
271
335
|
return;
|
|
@@ -275,12 +339,19 @@ function apply(ctx, config) {
|
|
|
275
339
|
return;
|
|
276
340
|
}
|
|
277
341
|
for (const [mid, _] of finalConfig.anchors) {
|
|
278
|
-
await initAnchorState(mid).catch((err) =>
|
|
342
|
+
await initAnchorState(mid).catch((err) => {
|
|
343
|
+
ctx.logger.error(`初始化主播${mid}失败:`, err.message);
|
|
344
|
+
debugLog(`初始化主播${mid}失败详情`, err);
|
|
345
|
+
});
|
|
279
346
|
}
|
|
280
347
|
ctx.setInterval(async () => {
|
|
348
|
+
debugLog(`开始定时轮询,共${finalConfig.anchors.length}个主播`);
|
|
281
349
|
for (const [mid, groupStr] of finalConfig.anchors) {
|
|
282
350
|
const groupIds = groupStr.split(",").filter((g) => g.trim());
|
|
283
|
-
await checkAnchorState(mid, groupIds).catch((err) =>
|
|
351
|
+
await checkAnchorState(mid, groupIds).catch((err) => {
|
|
352
|
+
ctx.logger.error(`检查主播${mid}失败:`, err.message);
|
|
353
|
+
debugLog(`检查主播${mid}失败详情`, err);
|
|
354
|
+
});
|
|
284
355
|
}
|
|
285
356
|
}, finalConfig.pollInterval * 1e3);
|
|
286
357
|
ctx.logger.info(`bilirice插件已启动,监听主播数:${finalConfig.anchors.length},轮询间隔:${finalConfig.pollInterval}秒`);
|
|
@@ -289,11 +360,13 @@ function apply(ctx, config) {
|
|
|
289
360
|
if (!mid) return "请输入主播UID";
|
|
290
361
|
if (!finalConfig.bilibiliCookie) return "插件未配置B站Cookie,无法执行操作!";
|
|
291
362
|
try {
|
|
363
|
+
debugLog(`手动指令触发:检查主播${mid}状态`, { session });
|
|
292
364
|
const groupIds = session.guildId ? [session.guildId] : [];
|
|
293
365
|
await initAnchorState(mid);
|
|
294
366
|
await checkAnchorState(mid, groupIds);
|
|
295
367
|
return `已检查主播${mid}的直播间状态(含完整统计数据)`;
|
|
296
368
|
} catch (err) {
|
|
369
|
+
debugLog(`手动指令执行失败`, err);
|
|
297
370
|
return `检查失败:${err.message}`;
|
|
298
371
|
}
|
|
299
372
|
});
|