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.
Files changed (2) hide show
  1. package/lib/index.js +75 -110
  2. 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("用于防复读的历史记录大小,防止短期内对同一活动重复转发").default(30).min(5).max(100),
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 列表。插件会为列表中的每个 key 发送弹幕。如果留空,则不执行发送弹幕功能。").default([])
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
- if (match.length === 1 && !numMap[match] && !unitMap[match]) return match;
67
- let total = 0;
68
- let sectionTotal = 0;
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
- currentNum = numMap[char];
64
+ tempVal = numMap[char];
73
65
  } else if (unitMap[char]) {
74
- const { value, isSection } = unitMap[char];
75
- if (value === 10 && currentNum === 0) currentNum = 1;
76
- sectionTotal += currentNum * value;
77
- currentNum = 0;
78
- if (isSection) {
79
- total += sectionTotal;
80
- sectionTotal = 0;
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 += sectionTotal + currentNum;
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 patterns = [
92
- /(?:播间号|房间号|直播间)[::\s]*(\d{3,15})/g,
93
- /\b(\d{6,15})\b/g
94
- // 使用单词边界确保匹配的是独立数字
95
- ];
96
- const foundIds = /* @__PURE__ */ new Set();
97
- for (const pattern of patterns) {
98
- for (const match of sanitizedText.matchAll(pattern)) {
99
- if (match[1]) foundIds.add(match[1]);
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(foundIds);
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 regex = /(?:(\d{1,2})\s*级(?:灯牌)?\s*)?(?:发\s*)?(\d+\.?\d*w\+?|\b\d{3,5}\b)(?:神金|钻石|猫猫钻)?/gi;
126
- let match;
127
- while ((match = regex.exec(line)) !== null) {
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[2] || "").toLowerCase();
137
+ const amountStr = (match[4] || match[5] || "").toLowerCase();
130
138
  const amount = amountStr.includes("w") ? parseFloat(amountStr.replace("w", "")) * 1e4 : parseFloat(amountStr);
131
- if (!isNaN(amount) && amount > 0) {
132
- rewards.push({ amount, condition });
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
- const sign = crypto.createHash("md5").update(queryString + appSecret).digest("hex");
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
- const successMessage = attempt > 0 ? `[弹幕] [${logIdentifier}] 成功向直播间 ${roomId} 发送弹幕 (重试 ${attempt} 次后)` : `[弹幕] [${logIdentifier}] 成功向直播间 ${roomId} 发送弹幕: "${message}"`;
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(`[忽略] 消息包含多个房间号: ${roomIds.join(", ")}`);
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
- const hasRejectionKeyword = REJECTION_KEYWORDS.some((keyword) => preprocessedMessage.includes(keyword));
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
- const hasStrongContext = /神金|发|w/i.test(preprocessedMessage);
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 && config.biliAccessKeys.length > 0) {
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
- const isMonitored = config.monitorGroups.some((g) => g.groupId === session.channelId);
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");
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-cat-raising",
3
3
  "description": "",
4
- "version": "1.1.0",
4
+ "version": "1.2.0",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [