koishi-plugin-cat-raising 1.1.0 → 1.2.0
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.js +75 -110
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -46,60 +46,62 @@ var Config = import_koishi.Schema.object({
|
|
|
46
46
|
groupId: import_koishi.Schema.string().description("要监听的 QQ 群号").required(),
|
|
47
47
|
sendHelperMessages: import_koishi.Schema.boolean().description("是否在此群内发送“看到啦”之类的辅助/警告消息").default(true)
|
|
48
48
|
})).description("监听的群组列表及其配置").required(),
|
|
49
|
-
historySize: import_koishi.Schema.number().description("
|
|
49
|
+
historySize: import_koishi.Schema.number().description("用于防复读的历史记录大小").default(30).min(5).max(100),
|
|
50
50
|
biliAccessKeys: import_koishi.Schema.array(import_koishi.Schema.object({
|
|
51
51
|
key: import_koishi.Schema.string().description("Bilibili access_key").required(),
|
|
52
|
-
remark: import_koishi.Schema.string().description("对此 access_key
|
|
53
|
-
})).description("用于发送B站弹幕的 access_key
|
|
52
|
+
remark: import_koishi.Schema.string().description("对此 access_key 的备注")
|
|
53
|
+
})).description("用于发送B站弹幕的 access_key 列表").default([])
|
|
54
54
|
});
|
|
55
55
|
function preprocessChineseNumerals(text) {
|
|
56
56
|
const numMap = { "零": 0, "一": 1, "二": 2, "两": 2, "三": 3, "四": 4, "五": 5, "六": 6, "七": 7, "八": 8, "九": 9 };
|
|
57
|
-
const unitMap = {
|
|
58
|
-
"十": { value: 10, isSection: false },
|
|
59
|
-
"百": { value: 100, isSection: false },
|
|
60
|
-
"千": { value: 1e3, isSection: false },
|
|
61
|
-
"万": { value: 1e4, isSection: true },
|
|
62
|
-
"亿": { value: 1e8, isSection: true }
|
|
63
|
-
};
|
|
57
|
+
const unitMap = { "十": 10, "百": 100, "千": 1e3, "万": 1e4, "亿": 1e8 };
|
|
64
58
|
const chineseNumRegex = /([一二三四五六七八九十百千万亿两零]+)/g;
|
|
65
59
|
return text.replace(chineseNumRegex, (match) => {
|
|
66
|
-
|
|
67
|
-
let
|
|
68
|
-
|
|
69
|
-
let currentNum = 0;
|
|
70
|
-
for (const char of match) {
|
|
60
|
+
let total = 0, tempVal = 0, sectionVal = 0;
|
|
61
|
+
for (let i = 0; i < match.length; i++) {
|
|
62
|
+
const char = match[i];
|
|
71
63
|
if (numMap[char] !== void 0) {
|
|
72
|
-
|
|
64
|
+
tempVal = numMap[char];
|
|
73
65
|
} else if (unitMap[char]) {
|
|
74
|
-
const
|
|
75
|
-
if (
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
66
|
+
const unit = unitMap[char];
|
|
67
|
+
if (unit >= 1e4) {
|
|
68
|
+
sectionVal += tempVal;
|
|
69
|
+
total += sectionVal * unit;
|
|
70
|
+
sectionVal = 0;
|
|
71
|
+
} else {
|
|
72
|
+
sectionVal += (tempVal || 1) * unit;
|
|
81
73
|
}
|
|
74
|
+
tempVal = 0;
|
|
82
75
|
}
|
|
83
76
|
}
|
|
84
|
-
total +=
|
|
77
|
+
total += sectionVal + tempVal;
|
|
85
78
|
return String(total);
|
|
86
79
|
});
|
|
87
80
|
}
|
|
88
81
|
__name(preprocessChineseNumerals, "preprocessChineseNumerals");
|
|
89
82
|
function extractAllRoomIds(text) {
|
|
90
83
|
const sanitizedText = text.replace(/<[^>]+>/g, "");
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
84
|
+
const explicitPattern = /(?:播间号|房间号|直播间)[::\s]*(\d{3,15})/gi;
|
|
85
|
+
const explicitIds = /* @__PURE__ */ new Set();
|
|
86
|
+
for (const match of sanitizedText.matchAll(explicitPattern)) {
|
|
87
|
+
if (match[1]) explicitIds.add(match[1]);
|
|
88
|
+
}
|
|
89
|
+
if (explicitIds.size > 0) return Array.from(explicitIds);
|
|
90
|
+
const genericPattern = /\b(\d{3,15})\b/g;
|
|
91
|
+
const allNumericCandidates = /* @__PURE__ */ new Set();
|
|
92
|
+
for (const match of sanitizedText.matchAll(genericPattern)) {
|
|
93
|
+
if (match[1]) allNumericCandidates.add(match[1]);
|
|
101
94
|
}
|
|
102
|
-
return Array.from(
|
|
95
|
+
if (allNumericCandidates.size <= 1) return Array.from(allNumericCandidates);
|
|
96
|
+
const preprocessedText = preprocessChineseNumerals(sanitizedText);
|
|
97
|
+
const rewards = extractRewards(preprocessedText);
|
|
98
|
+
const rewardAmounts = new Set(rewards.map((r) => String(r.amount)));
|
|
99
|
+
let filteredIds = Array.from(allNumericCandidates).filter((id) => !rewardAmounts.has(id));
|
|
100
|
+
if (filteredIds.length > 1) {
|
|
101
|
+
filteredIds.sort((a, b) => b.length - a.length);
|
|
102
|
+
return [filteredIds[0]];
|
|
103
|
+
}
|
|
104
|
+
return filteredIds;
|
|
103
105
|
}
|
|
104
106
|
__name(extractAllRoomIds, "extractAllRoomIds");
|
|
105
107
|
function extractDateTime(line) {
|
|
@@ -122,14 +124,31 @@ function extractDateTime(line) {
|
|
|
122
124
|
__name(extractDateTime, "extractDateTime");
|
|
123
125
|
function extractRewards(line) {
|
|
124
126
|
const rewards = [];
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
127
|
+
const foundAmounts = /* @__PURE__ */ new Set();
|
|
128
|
+
const addReward = /* @__PURE__ */ __name((amount, condition) => {
|
|
129
|
+
if (!isNaN(amount) && amount > 0 && !foundAmounts.has(amount)) {
|
|
130
|
+
rewards.push({ amount, condition });
|
|
131
|
+
foundAmounts.add(amount);
|
|
132
|
+
}
|
|
133
|
+
}, "addReward");
|
|
134
|
+
const strongRegex = /(?:(\d{1,2})\s*级(?:灯牌)?\s*)?(?:(发|掉落)\s*)?(?:(神金|钻石|猫猫钻)\s*(\d+\.?\d*w?|\b\d{3,5}\b)|(\d+\.?\d*w|\b\d{3,5}\b)\s*(?:神金|钻石|猫猫钻|w))/gi;
|
|
135
|
+
for (const match of line.matchAll(strongRegex)) {
|
|
128
136
|
const condition = match[1] ? `${match[1]}级灯牌` : "无限制";
|
|
129
|
-
const amountStr = (match[
|
|
137
|
+
const amountStr = (match[4] || match[5] || "").toLowerCase();
|
|
130
138
|
const amount = amountStr.includes("w") ? parseFloat(amountStr.replace("w", "")) * 1e4 : parseFloat(amountStr);
|
|
131
|
-
|
|
132
|
-
|
|
139
|
+
addReward(amount, condition);
|
|
140
|
+
}
|
|
141
|
+
if (rewards.length === 0) {
|
|
142
|
+
const conditionMatch = line.match(/(\d{1,2})\s*级(?:灯牌)?/);
|
|
143
|
+
if (conditionMatch) {
|
|
144
|
+
const condition = conditionMatch[0];
|
|
145
|
+
const conditionLevel = conditionMatch[1];
|
|
146
|
+
for (const numMatch of line.matchAll(/\b(\d{3,5})\b/g)) {
|
|
147
|
+
if (numMatch[1] !== conditionLevel) {
|
|
148
|
+
const amount = parseFloat(numMatch[1]);
|
|
149
|
+
addReward(amount, condition);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
133
152
|
}
|
|
134
153
|
}
|
|
135
154
|
return rewards;
|
|
@@ -159,51 +178,25 @@ var BILI_APPSECRET = "59b43e04ad6965f34319062b478f83dd";
|
|
|
159
178
|
function signBilibiliParams(params, appSecret) {
|
|
160
179
|
const sortedKeys = Object.keys(params).sort();
|
|
161
180
|
const queryString = sortedKeys.map((key) => `${key}=${params[key]}`).join("&");
|
|
162
|
-
|
|
163
|
-
return sign;
|
|
181
|
+
return crypto.createHash("md5").update(queryString + appSecret).digest("hex");
|
|
164
182
|
}
|
|
165
183
|
__name(signBilibiliParams, "signBilibiliParams");
|
|
166
184
|
async function sendBilibiliDanmaku(ctx, keyConfig, roomId, message) {
|
|
167
|
-
const MAX_RETRIES = 4;
|
|
168
|
-
const RETRY_DELAY_MS = 3e3;
|
|
169
|
-
const FREQUENCY_LIMIT_KEYWORD = "频率过快";
|
|
185
|
+
const MAX_RETRIES = 4, RETRY_DELAY_MS = 3e3, FREQUENCY_LIMIT_KEYWORD = "频率过快";
|
|
170
186
|
const url = "https://api.live.bilibili.com/xlive/app-room/v1/dM/sendmsg";
|
|
171
187
|
const logIdentifier = keyConfig.remark || keyConfig.key.slice(0, 8);
|
|
172
188
|
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
173
|
-
if (attempt > 0)
|
|
174
|
-
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
|
|
175
|
-
}
|
|
189
|
+
if (attempt > 0) await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
|
|
176
190
|
const ts = Math.floor(Date.now() / 1e3);
|
|
177
|
-
const baseParams = {
|
|
178
|
-
access_key: keyConfig.key,
|
|
179
|
-
actionKey: "appkey",
|
|
180
|
-
appkey: BILI_APPKEY,
|
|
181
|
-
cid: roomId,
|
|
182
|
-
msg: message,
|
|
183
|
-
rnd: ts,
|
|
184
|
-
color: "16777215",
|
|
185
|
-
// 白色
|
|
186
|
-
fontsize: "25",
|
|
187
|
-
mode: "1",
|
|
188
|
-
// 滚动弹幕
|
|
189
|
-
ts
|
|
190
|
-
};
|
|
191
|
+
const baseParams = { access_key: keyConfig.key, actionKey: "appkey", appkey: BILI_APPKEY, cid: roomId, msg: message, rnd: ts, color: "16777215", fontsize: "25", mode: "1", ts };
|
|
191
192
|
const sign = signBilibiliParams(baseParams, BILI_APPSECRET);
|
|
192
193
|
const params = { ...baseParams, sign };
|
|
193
194
|
const formData = new import_url.URLSearchParams();
|
|
194
|
-
for (const key in params)
|
|
195
|
-
formData.append(key, params[key]);
|
|
196
|
-
}
|
|
195
|
+
for (const key in params) formData.append(key, params[key]);
|
|
197
196
|
try {
|
|
198
|
-
const response = await ctx.http.post(url, formData, {
|
|
199
|
-
headers: {
|
|
200
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
201
|
-
"User-Agent": "Mozilla/5.0 BiliDroid/6.73.1 (bbcallen@gmail.com) os/android model/Mi 10 Pro mobi_app/android build/6731100 channel/xiaomi innerVer/6731110 osVer/12 network/2"
|
|
202
|
-
}
|
|
203
|
-
});
|
|
197
|
+
const response = await ctx.http.post(url, formData, { headers: { "Content-Type": "application/x-www-form-urlencoded", "User-Agent": "Mozilla/5.0 BiliDroid/6.73.1" } });
|
|
204
198
|
if (response.code === 0) {
|
|
205
|
-
|
|
206
|
-
ctx.logger.info(successMessage);
|
|
199
|
+
ctx.logger.info(`[弹幕] [${logIdentifier}] 成功向直播间 ${roomId} 发送弹幕${attempt > 0 ? ` (重试 ${attempt} 次后)` : `: "${message}"`}`);
|
|
207
200
|
return;
|
|
208
201
|
}
|
|
209
202
|
if (response.message?.includes(FREQUENCY_LIMIT_KEYWORD)) {
|
|
@@ -241,32 +234,25 @@ async function fetchBilibiliInfo(ctx, roomId) {
|
|
|
241
234
|
__name(fetchBilibiliInfo, "fetchBilibiliInfo");
|
|
242
235
|
function apply(ctx, config) {
|
|
243
236
|
const forwardedHistory = [];
|
|
244
|
-
const warningMessageMap = /* @__PURE__ */ new Map();
|
|
245
237
|
ctx.on("message", async (session) => {
|
|
246
238
|
const groupConfig = config.monitorGroups.find((g) => g.groupId === session.channelId);
|
|
247
239
|
if (!groupConfig) return;
|
|
248
240
|
const strippedContent = session.stripped.content;
|
|
249
241
|
if (!strippedContent.trim()) return;
|
|
250
242
|
if (HARD_REJECTION_KEYWORDS.some((keyword) => strippedContent.includes(keyword))) return;
|
|
251
|
-
if (CHECK_IN_REJECTION_REGEX.test(strippedContent))
|
|
252
|
-
ctx.logger.info(`[忽略] 消息包含签到模式 (如 110+),判定为非奖励信息。内容: "${strippedContent.replace(/\n/g, " ")}"`);
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
243
|
+
if (CHECK_IN_REJECTION_REGEX.test(strippedContent)) return;
|
|
255
244
|
if (!TRIGGER_REGEX.test(strippedContent)) return;
|
|
256
245
|
const roomIds = extractAllRoomIds(session.content);
|
|
257
246
|
if (roomIds.length !== 1) {
|
|
258
|
-
if (roomIds.length > 1) ctx.logger.info(`[忽略]
|
|
247
|
+
if (roomIds.length > 1) ctx.logger.info(`[忽略] 消息包含多个可能的房间号: ${roomIds.join(", ")}`);
|
|
259
248
|
return;
|
|
260
249
|
}
|
|
261
250
|
const roomId = roomIds[0];
|
|
262
251
|
const preprocessedMessage = preprocessChineseNumerals(strippedContent);
|
|
263
|
-
|
|
264
|
-
if (hasRejectionKeyword && !OVERRIDE_KEYWORDS.some((keyword) => preprocessedMessage.includes(keyword))) return;
|
|
252
|
+
if (REJECTION_KEYWORDS.some((k) => preprocessedMessage.includes(k)) && !OVERRIDE_KEYWORDS.some((k) => preprocessedMessage.includes(k))) return;
|
|
265
253
|
const parsedEvent = parseEventFromText(preprocessedMessage);
|
|
266
254
|
if (!parsedEvent) return;
|
|
267
|
-
|
|
268
|
-
const hasTime = parsedEvent.dateTime !== "时间未知";
|
|
269
|
-
if (!hasStrongContext && !hasTime) return;
|
|
255
|
+
if (!/神金|发|w/i.test(preprocessedMessage) && parsedEvent.dateTime === "时间未知") return;
|
|
270
256
|
const biliInfo = await fetchBilibiliInfo(ctx, roomId);
|
|
271
257
|
if (!biliInfo) return;
|
|
272
258
|
let helperMessageId;
|
|
@@ -289,20 +275,11 @@ function apply(ctx, config) {
|
|
|
289
275
|
---
|
|
290
276
|
投稿数: ${biliInfo.videoCount}`;
|
|
291
277
|
const [forwardedMessageId] = config.isGroup ? await session.bot.sendMessage(config.targetQQ, forwardMessage) : await session.bot.sendPrivateMessage(config.targetQQ, forwardMessage);
|
|
292
|
-
forwardedHistory.push({
|
|
293
|
-
originalMessageId: session.messageId,
|
|
294
|
-
forwardedMessageId,
|
|
295
|
-
helperMessageId,
|
|
296
|
-
// 存储辅助消息ID用于撤回联动
|
|
297
|
-
roomId,
|
|
298
|
-
dateTime
|
|
299
|
-
});
|
|
278
|
+
forwardedHistory.push({ originalMessageId: session.messageId, forwardedMessageId, helperMessageId, roomId, dateTime });
|
|
300
279
|
if (forwardedHistory.length > config.historySize) forwardedHistory.shift();
|
|
301
|
-
if (config.biliAccessKeys
|
|
280
|
+
if (config.biliAccessKeys?.length > 0) {
|
|
302
281
|
ctx.logger.info(`[弹幕] 准备为 ${config.biliAccessKeys.length} 个账号发送弹幕到直播间 ${roomId}...`);
|
|
303
|
-
const danmakuPromises = config.biliAccessKeys.map(
|
|
304
|
-
(keyConfig) => sendBilibiliDanmaku(ctx, keyConfig, roomId, "喵喵喵")
|
|
305
|
-
);
|
|
282
|
+
const danmakuPromises = config.biliAccessKeys.map((keyConfig) => sendBilibiliDanmaku(ctx, keyConfig, roomId, "喵喵喵"));
|
|
306
283
|
Promise.allSettled(danmakuPromises);
|
|
307
284
|
}
|
|
308
285
|
} catch (error) {
|
|
@@ -311,8 +288,7 @@ function apply(ctx, config) {
|
|
|
311
288
|
}
|
|
312
289
|
});
|
|
313
290
|
ctx.on("message-deleted", async (session) => {
|
|
314
|
-
|
|
315
|
-
if (!isMonitored) return;
|
|
291
|
+
if (!config.monitorGroups.some((g) => g.groupId === session.channelId)) return;
|
|
316
292
|
const originalMessageId = session.messageId;
|
|
317
293
|
const entryIndex = forwardedHistory.findIndex((entry) => entry.originalMessageId === originalMessageId);
|
|
318
294
|
if (entryIndex !== -1) {
|
|
@@ -334,17 +310,6 @@ function apply(ctx, config) {
|
|
|
334
310
|
ctx.logger.info(`[撤回] 已联动撤回与源消息 ${originalMessageId} 相关的转发。`);
|
|
335
311
|
}
|
|
336
312
|
}
|
|
337
|
-
if (warningMessageMap.has(originalMessageId)) {
|
|
338
|
-
const warningMessageId = warningMessageMap.get(originalMessageId);
|
|
339
|
-
try {
|
|
340
|
-
await session.bot.deleteMessage(session.channelId, warningMessageId);
|
|
341
|
-
} catch (e) {
|
|
342
|
-
ctx.logger.warn(`[撤回] 警告消息 (ID: ${warningMessageId}) 失败:`, e);
|
|
343
|
-
} finally {
|
|
344
|
-
warningMessageMap.delete(originalMessageId);
|
|
345
|
-
ctx.logger.info(`[撤回] 已联动撤回与源消息 ${originalMessageId} 相关的警告。`);
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
313
|
});
|
|
349
314
|
}
|
|
350
315
|
__name(apply, "apply");
|