koishi-plugin-bilirice 0.0.3 → 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 +19 -3
- package/lib/index.js +237 -153
- package/package.json +1 -1
package/lib/index.d.ts
CHANGED
|
@@ -1,8 +1,22 @@
|
|
|
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
|
+
debugMode?: boolean;
|
|
13
|
+
}
|
|
3
14
|
export declare const Config: Schema<Schemastery.ObjectS<{
|
|
4
15
|
anchors: Schema<[string?, string?, ...any[]][], [string?, string?, ...any[]][]>;
|
|
5
16
|
pollInterval: Schema<number, number>;
|
|
17
|
+
timeout: Schema<number, number>;
|
|
18
|
+
bilibiliCookie: Schema<string, string>;
|
|
19
|
+
debugMode: Schema<boolean, boolean>;
|
|
6
20
|
templates: Schema<Schemastery.ObjectS<{
|
|
7
21
|
liveStart: Schema<string, string>;
|
|
8
22
|
liveEnd: Schema<string, string>;
|
|
@@ -10,10 +24,12 @@ export declare const Config: Schema<Schemastery.ObjectS<{
|
|
|
10
24
|
liveStart: Schema<string, string>;
|
|
11
25
|
liveEnd: Schema<string, string>;
|
|
12
26
|
}>>;
|
|
13
|
-
timeout: Schema<number, number>;
|
|
14
27
|
}>, Schemastery.ObjectT<{
|
|
15
28
|
anchors: Schema<[string?, string?, ...any[]][], [string?, string?, ...any[]][]>;
|
|
16
29
|
pollInterval: Schema<number, number>;
|
|
30
|
+
timeout: Schema<number, number>;
|
|
31
|
+
bilibiliCookie: Schema<string, string>;
|
|
32
|
+
debugMode: Schema<boolean, boolean>;
|
|
17
33
|
templates: Schema<Schemastery.ObjectS<{
|
|
18
34
|
liveStart: Schema<string, string>;
|
|
19
35
|
liveEnd: Schema<string, string>;
|
|
@@ -21,6 +37,6 @@ export declare const Config: Schema<Schemastery.ObjectS<{
|
|
|
21
37
|
liveStart: Schema<string, string>;
|
|
22
38
|
liveEnd: Schema<string, string>;
|
|
23
39
|
}>>;
|
|
24
|
-
timeout: Schema<number, number>;
|
|
25
40
|
}>>;
|
|
26
|
-
export declare function apply(ctx: Context, config:
|
|
41
|
+
export declare function apply(ctx: Context, config: BiliriceConfig): void;
|
|
42
|
+
export {};
|
package/lib/index.js
CHANGED
|
@@ -37,108 +37,162 @@ __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("
|
|
44
|
-
import_koishi.Schema.string().description("
|
|
45
|
-
])).required().description("
|
|
46
|
-
pollInterval: import_koishi.Schema.number().default(10).description("
|
|
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
|
+
// 控制台显示为开关,一键开启/关闭
|
|
47
58
|
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请求超时时间(毫秒)")
|
|
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("自定义通知模板(保留占位符即可自动替换数据)")
|
|
52
62
|
});
|
|
53
63
|
function apply(ctx, config) {
|
|
54
64
|
const finalConfig = {
|
|
55
65
|
pollInterval: config.pollInterval || 10,
|
|
56
|
-
timeout: config.timeout ||
|
|
66
|
+
timeout: config.timeout || 1e4,
|
|
57
67
|
templates: {
|
|
58
68
|
liveStart: config.templates?.liveStart || "【{uname} 开播啦】\n标题:{title}\n分区:{area}\n链接:{url}\n开播时间:{startTime}\n封面:{cover}",
|
|
59
69
|
liveEnd: config.templates?.liveEnd || "【{uname} 下播啦】\n直播时长:{liveTime}\n最高在线:{peakOnline}\n观看人数:{watchCount}\n弹幕数:{dmCount}\n人均弹幕:{avgDmPerUser}\n下播时间:{endTime}"
|
|
60
70
|
},
|
|
61
|
-
anchors: config.anchors || []
|
|
71
|
+
anchors: config.anchors || [],
|
|
72
|
+
bilibiliCookie: config.bilibiliCookie || "",
|
|
73
|
+
debugMode: config.debugMode || false
|
|
74
|
+
// 读取调试模式配置
|
|
62
75
|
};
|
|
63
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");
|
|
64
86
|
const api = import_axios.default.create({
|
|
65
87
|
baseURL: "https://api.live.bilibili.com",
|
|
66
88
|
timeout: finalConfig.timeout,
|
|
89
|
+
httpsAgent: new import_https.default.Agent({ rejectUnauthorized: false }),
|
|
67
90
|
headers: {
|
|
68
|
-
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/
|
|
69
|
-
"Referer": "https://live.bilibili.com/"
|
|
70
|
-
|
|
71
|
-
|
|
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]
|
|
72
104
|
});
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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}`);
|
|
81
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;
|
|
82
146
|
}
|
|
83
147
|
__name(getRoomIdByMid, "getRoomIdByMid");
|
|
84
148
|
async function getRoomBaseInfo(roomId) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
149
|
+
debugLog(`开始获取直播间${roomId}基础信息`);
|
|
150
|
+
const res = await api.get("/room/v1/Room/get_info", {
|
|
151
|
+
params: { room_id: Number(roomId) }
|
|
152
|
+
});
|
|
153
|
+
if (res.data.code !== 0) throw new Error(`获取直播间基础信息失败: ${res.data.code} ${res.data.message || ""}`);
|
|
154
|
+
debugLog(`直播间${roomId}基础信息`, res.data.data);
|
|
155
|
+
return res.data.data;
|
|
93
156
|
}
|
|
94
157
|
__name(getRoomBaseInfo, "getRoomBaseInfo");
|
|
95
158
|
async function getRoomStat(roomId) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
live_time: data.room_info.live_time || 0
|
|
120
|
-
// 开播时间戳
|
|
121
|
-
};
|
|
122
|
-
return stat;
|
|
123
|
-
} catch (err) {
|
|
124
|
-
ctx.logger.error(`获取直播间统计失败(roomId=${roomId}):`, err);
|
|
125
|
-
throw err;
|
|
126
|
-
}
|
|
159
|
+
debugLog(`开始获取直播间${roomId}统计数据`);
|
|
160
|
+
const res = await api.get("/xlive/web-room/v1/index/getInfoByRoom", {
|
|
161
|
+
params: {
|
|
162
|
+
room_id: Number(roomId),
|
|
163
|
+
platform: "web",
|
|
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
|
|
179
|
+
};
|
|
180
|
+
debugLog(`直播间${roomId}统计数据`, stat);
|
|
181
|
+
return stat;
|
|
127
182
|
}
|
|
128
183
|
__name(getRoomStat, "getRoomStat");
|
|
129
184
|
async function getAnchorInfo(roomId) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
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;
|
|
138
192
|
}
|
|
139
193
|
__name(getAnchorInfo, "getAnchorInfo");
|
|
140
194
|
function formatTime(timestamp) {
|
|
141
|
-
|
|
195
|
+
const formatted = new Date(timestamp * 1e3).toLocaleString("zh-CN", {
|
|
142
196
|
year: "numeric",
|
|
143
197
|
month: "2-digit",
|
|
144
198
|
day: "2-digit",
|
|
@@ -146,23 +200,24 @@ function apply(ctx, config) {
|
|
|
146
200
|
minute: "2-digit",
|
|
147
201
|
second: "2-digit"
|
|
148
202
|
});
|
|
203
|
+
debugLog(`时间戳${timestamp}格式化结果:${formatted}`);
|
|
204
|
+
return formatted;
|
|
149
205
|
}
|
|
150
206
|
__name(formatTime, "formatTime");
|
|
151
207
|
function formatSeconds(seconds) {
|
|
152
208
|
const h = Math.floor(seconds / 3600);
|
|
153
209
|
const m = Math.floor(seconds % 3600 / 60);
|
|
154
210
|
const s = seconds % 60;
|
|
155
|
-
|
|
211
|
+
const formatted = [h, m, s].map((v) => v.toString().padStart(2, "0")).join(":");
|
|
212
|
+
debugLog(`秒数${seconds}格式化结果:${formatted}`);
|
|
213
|
+
return formatted;
|
|
156
214
|
}
|
|
157
215
|
__name(formatSeconds, "formatSeconds");
|
|
158
216
|
function calcLiveStat(startTime, endTime, lastStat, currentStat) {
|
|
217
|
+
debugLog(`计算下播统计数据`, { startTime, endTime, lastStat, currentStat });
|
|
159
218
|
let fansChange = 0;
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
} catch (e) {
|
|
163
|
-
fansChange = Math.floor(Math.random() * 100);
|
|
164
|
-
}
|
|
165
|
-
return {
|
|
219
|
+
if (lastStat && currentStat) fansChange = currentStat.fans_count - lastStat.fans_count;
|
|
220
|
+
const stat = {
|
|
166
221
|
liveTime: endTime - startTime,
|
|
167
222
|
peakOnline: currentStat.online,
|
|
168
223
|
watchCount: currentStat.watch_num,
|
|
@@ -173,118 +228,147 @@ function apply(ctx, config) {
|
|
|
173
228
|
fansChange,
|
|
174
229
|
endTime
|
|
175
230
|
};
|
|
231
|
+
debugLog(`下播统计数据计算结果`, stat);
|
|
232
|
+
return stat;
|
|
176
233
|
}
|
|
177
234
|
__name(calcLiveStat, "calcLiveStat");
|
|
178
235
|
function renderTemplate(template, data) {
|
|
179
|
-
|
|
236
|
+
debugLog(`渲染模板`, { template, data });
|
|
237
|
+
const result = template.replace(/\{(\w+)\}/g, (_, key) => data[key] ?? `{${key}}`);
|
|
238
|
+
debugLog(`模板渲染结果`, result);
|
|
239
|
+
return result;
|
|
180
240
|
}
|
|
181
241
|
__name(renderTemplate, "renderTemplate");
|
|
182
242
|
async function sendGroupMessage(groupId, message) {
|
|
243
|
+
debugLog(`准备发送群消息`, { groupId, message });
|
|
183
244
|
const bot = ctx.bots.values().next().value;
|
|
184
|
-
if (!bot)
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}
|
|
188
|
-
try {
|
|
189
|
-
await bot.sendMessage(groupId, message);
|
|
190
|
-
} catch (err) {
|
|
191
|
-
ctx.logger.error(`发送群消息失败(群号=${groupId}):`, err);
|
|
192
|
-
}
|
|
245
|
+
if (!bot) throw new Error("未找到可用的Bot实例");
|
|
246
|
+
await bot.sendMessage(groupId, message);
|
|
247
|
+
debugLog(`群消息发送成功:${groupId}`);
|
|
193
248
|
}
|
|
194
249
|
__name(sendGroupMessage, "sendGroupMessage");
|
|
195
250
|
async function initAnchorState(mid) {
|
|
196
|
-
if (anchorStateCache.has(mid))
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const roomInfo = await getRoomBaseInfo(roomId);
|
|
200
|
-
const isOnline = roomInfo.live_status === 1;
|
|
201
|
-
anchorStateCache.set(mid, {
|
|
202
|
-
mid,
|
|
203
|
-
roomId,
|
|
204
|
-
isOnline,
|
|
205
|
-
liveStartTime: isOnline ? roomInfo.live_time : 0,
|
|
206
|
-
lastStat: isOnline ? await getRoomStat(roomId) : null
|
|
207
|
-
});
|
|
208
|
-
} catch (err) {
|
|
209
|
-
ctx.logger.error(`初始化主播状态失败(mid=${mid}):`, err);
|
|
251
|
+
if (anchorStateCache.has(mid)) {
|
|
252
|
+
debugLog(`主播${mid}状态已缓存,跳过初始化`);
|
|
253
|
+
return;
|
|
210
254
|
}
|
|
255
|
+
debugLog(`开始初始化主播${mid}状态`);
|
|
256
|
+
const roomId = await getRoomIdByMid(mid);
|
|
257
|
+
const roomInfo = await getRoomBaseInfo(roomId);
|
|
258
|
+
const isOnline = roomInfo.live_status === 1;
|
|
259
|
+
const lastStat = isOnline ? await getRoomStat(roomId) : null;
|
|
260
|
+
anchorStateCache.set(mid, {
|
|
261
|
+
mid,
|
|
262
|
+
roomId,
|
|
263
|
+
isOnline,
|
|
264
|
+
liveStartTime: isOnline ? roomInfo.live_time : 0,
|
|
265
|
+
lastStat
|
|
266
|
+
});
|
|
267
|
+
debugLog(`主播${mid}状态初始化完成`, anchorStateCache.get(mid));
|
|
211
268
|
}
|
|
212
269
|
__name(initAnchorState, "initAnchorState");
|
|
213
270
|
async function checkAnchorState(mid, groupIds) {
|
|
271
|
+
debugLog(`开始检查主播${mid}状态`, { groupIds });
|
|
214
272
|
const state = anchorStateCache.get(mid);
|
|
215
273
|
if (!state) {
|
|
274
|
+
debugLog(`主播${mid}状态未初始化,先执行初始化`);
|
|
216
275
|
await initAnchorState(mid);
|
|
217
276
|
return;
|
|
218
277
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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}状态变化:离线 → 在线`);
|
|
223
284
|
const currentStat = await getRoomStat(state.roomId);
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
}
|
|
264
|
-
|
|
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}状态无变化:离线`);
|
|
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
|
-
ctx.logger.
|
|
334
|
+
ctx.logger.error("bilirice插件未配置任何主播,无法启动!");
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
if (!finalConfig.bilibiliCookie) {
|
|
338
|
+
ctx.logger.error("bilirice插件未配置B站Cookie,无法解决-352错误,请补充配置!");
|
|
271
339
|
return;
|
|
272
340
|
}
|
|
273
|
-
for (const [mid, _] of finalConfig.anchors)
|
|
341
|
+
for (const [mid, _] of finalConfig.anchors) {
|
|
342
|
+
await initAnchorState(mid).catch((err) => {
|
|
343
|
+
ctx.logger.error(`初始化主播${mid}失败:`, err.message);
|
|
344
|
+
debugLog(`初始化主播${mid}失败详情`, err);
|
|
345
|
+
});
|
|
346
|
+
}
|
|
274
347
|
ctx.setInterval(async () => {
|
|
348
|
+
debugLog(`开始定时轮询,共${finalConfig.anchors.length}个主播`);
|
|
275
349
|
for (const [mid, groupStr] of finalConfig.anchors) {
|
|
276
350
|
const groupIds = groupStr.split(",").filter((g) => g.trim());
|
|
277
|
-
await checkAnchorState(mid, groupIds)
|
|
351
|
+
await checkAnchorState(mid, groupIds).catch((err) => {
|
|
352
|
+
ctx.logger.error(`检查主播${mid}失败:`, err.message);
|
|
353
|
+
debugLog(`检查主播${mid}失败详情`, err);
|
|
354
|
+
});
|
|
278
355
|
}
|
|
279
356
|
}, finalConfig.pollInterval * 1e3);
|
|
280
357
|
ctx.logger.info(`bilirice插件已启动,监听主播数:${finalConfig.anchors.length},轮询间隔:${finalConfig.pollInterval}秒`);
|
|
281
358
|
});
|
|
282
359
|
ctx.command("bilirice <mid>", "手动检查指定主播的直播间状态").action(async ({ session }, mid) => {
|
|
283
360
|
if (!mid) return "请输入主播UID";
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
361
|
+
if (!finalConfig.bilibiliCookie) return "插件未配置B站Cookie,无法执行操作!";
|
|
362
|
+
try {
|
|
363
|
+
debugLog(`手动指令触发:检查主播${mid}状态`, { session });
|
|
364
|
+
const groupIds = session.guildId ? [session.guildId] : [];
|
|
365
|
+
await initAnchorState(mid);
|
|
366
|
+
await checkAnchorState(mid, groupIds);
|
|
367
|
+
return `已检查主播${mid}的直播间状态(含完整统计数据)`;
|
|
368
|
+
} catch (err) {
|
|
369
|
+
debugLog(`手动指令执行失败`, err);
|
|
370
|
+
return `检查失败:${err.message}`;
|
|
371
|
+
}
|
|
288
372
|
});
|
|
289
373
|
}
|
|
290
374
|
__name(apply, "apply");
|