koishi-plugin-cat-raising 0.1.8 → 0.2.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 CHANGED
@@ -1,3 +1,8 @@
1
+ /**
2
+ * @name cat-raising
3
+ * @description 一个用于监控QQ群内B站直播间奖励信息,并自动转发到指定目标的Koishi插件。
4
+ * 它能智能解析非结构化的文本,提取关键信息(直播间号、时间、奖励),并进行去重和信息补全。
5
+ */
1
6
  import { Context, Schema } from 'koishi';
2
7
  export declare const name = "cat-raising";
3
8
  export interface Config {
package/lib/index.js CHANGED
@@ -30,56 +30,58 @@ var name = "cat-raising";
30
30
  var Config = import_koishi.Schema.object({
31
31
  targetQQ: import_koishi.Schema.string().description("目标QQ号或QQ群号").required(),
32
32
  isGroup: import_koishi.Schema.boolean().description("是否为QQ群").default(false),
33
- monitorGroups: import_koishi.Schema.array(import_koishi.Schema.string()).description("监听的群号列表 (可以添加多个群,插件只会处理这些群里的消息)").required(),
34
- historySize: import_koishi.Schema.number().description("防复读历史记录大小 (记录最近N条转发信息,防止短期内对同一直播间的同一活动重复转发)").default(30).min(5).max(100)
33
+ monitorGroups: import_koishi.Schema.array(import_koishi.Schema.string()).description("监听的群号列表 (插件只会处理这些群里的消息)").required(),
34
+ historySize: import_koishi.Schema.number().description("用于防复读的历史记录大小,防止短期内对同一活动重复转发").default(30).min(5).max(100)
35
35
  });
36
36
  function preprocessChineseNumerals(text) {
37
- const replacements = {
38
- "三十六": "36",
39
- "三十五": "35",
40
- "三十四": "34",
41
- "三十三": "33",
42
- "三十二": "32",
43
- "三十一": "31",
44
- "三十": "30",
45
- "二十九": "29",
46
- "二十八": "28",
47
- "二十七": "27",
48
- "二十六": "26",
49
- "二十五": "25",
50
- "二十四": "24",
51
- "二十三": "23",
52
- "二十二": "22",
53
- "二十一": "21",
54
- "二十": "20",
55
- "十九": "19",
56
- "十八": "18",
57
- "十七": "17",
58
- "十六": "16",
59
- "十五": "15",
60
- "十四": "14",
61
- "十三": "13",
62
- "十二": "12",
63
- "十一": "11",
64
- "十": "10",
65
- "一千": "1000",
66
- "一百": "100",
67
- "九": "9",
68
- "八": "8",
69
- "七": "7",
70
- "六": "6",
71
- "五": "5",
72
- "四": "4",
73
- "三": "3",
74
- "两": "2",
75
- "二": "2",
76
- "一": "1"
37
+ const numMap = {
38
+ "": 0,
39
+ "": 1,
40
+ "": 2,
41
+ "": 2,
42
+ "": 3,
43
+ "": 4,
44
+ "": 5,
45
+ "": 6,
46
+ "": 7,
47
+ "": 8,
48
+ "": 9
77
49
  };
78
- let processedText = text;
79
- for (const [cn, ar] of Object.entries(replacements)) {
80
- processedText = processedText.replace(new RegExp(cn, "g"), ar);
81
- }
82
- return processedText;
50
+ const unitMap = {
51
+ "十": { value: 10, isSection: false },
52
+ "百": { value: 100, isSection: false },
53
+ "千": { value: 1e3, isSection: false },
54
+ "万": { value: 1e4, isSection: true },
55
+ "亿": { value: 1e8, isSection: true }
56
+ };
57
+ const chineseNumRegex = /([一二三四五六七八九十百千万亿两零]+)/g;
58
+ return text.replace(chineseNumRegex, (match) => {
59
+ if (match.length === 1 && numMap[match] === void 0 && unitMap[match] === void 0) {
60
+ return match;
61
+ }
62
+ let total = 0;
63
+ let sectionTotal = 0;
64
+ let currentNum = 0;
65
+ for (let i = 0; i < match.length; i++) {
66
+ const char = match[i];
67
+ if (numMap[char] !== void 0) {
68
+ currentNum = numMap[char];
69
+ } else if (unitMap[char]) {
70
+ const { value, isSection } = unitMap[char];
71
+ if (value === 10 && currentNum === 0) {
72
+ currentNum = 1;
73
+ }
74
+ sectionTotal += currentNum * value;
75
+ currentNum = 0;
76
+ if (isSection) {
77
+ total += sectionTotal;
78
+ sectionTotal = 0;
79
+ }
80
+ }
81
+ }
82
+ total += sectionTotal + currentNum;
83
+ return String(total);
84
+ });
83
85
  }
84
86
  __name(preprocessChineseNumerals, "preprocessChineseNumerals");
85
87
  function extractAllRoomIds(text) {
@@ -89,8 +91,7 @@ function extractAllRoomIds(text) {
89
91
  ];
90
92
  const foundIds = /* @__PURE__ */ new Set();
91
93
  for (const pattern of patterns) {
92
- const matches = text.matchAll(pattern);
93
- for (const match of matches) {
94
+ for (const match of text.matchAll(pattern)) {
94
95
  if (match[1]) foundIds.add(match[1]);
95
96
  }
96
97
  }
@@ -98,35 +99,20 @@ function extractAllRoomIds(text) {
98
99
  }
99
100
  __name(extractAllRoomIds, "extractAllRoomIds");
100
101
  function extractDateTime(line) {
101
- let match = line.match(/(\d{1,2})\s*[月.]\s*(\d{1,2})\s*日?/);
102
- if (match) return `${match[1]}月${match[2]}日`;
103
- match = line.match(/每晚\s*(\d{1,2})\s*点/);
104
- if (match) return `每晚 ${match[1].padStart(2, "0")}:00`;
105
- match = line.match(/(\d{1,2}\s*月\s*(?:上|中|下)旬)/);
106
- if (match) return match[1];
107
- match = line.match(/(\d{1,2})[::.点时]\s*(\d{1,2})/);
108
- if (match && match[2]) {
109
- const hour = match[1].padStart(2, "0");
110
- const minute = match[2].padStart(2, "0");
111
- return `${hour}:${minute}`;
112
- }
113
- match = line.match(/(\d{1,2})\s*点\s*半/);
114
- if (match) return `${match[1].padStart(2, "0")}:30`;
115
- match = line.match(/\b(\d{1,2})\s*[.点时](?!\d)/);
116
- if (match && match[1]) {
117
- const hour = match[1].padStart(2, "0");
118
- return `${hour}:00`;
119
- }
120
- match = line.match(/(\d{1,2})\s*分/);
121
- if (match) {
102
+ let match;
103
+ if (match = line.match(/(\d{1,2})\s*[月.]\s*(\d{1,2})\s*日?/)) return `${match[1]}月${match[2]}日`;
104
+ if (match = line.match(/每晚\s*(\d{1,2})\s*点/)) return `每晚 ${match[1].padStart(2, "0")}:00`;
105
+ if (match = line.match(/(\d{1,2}\s*月\s*(?:上|中|下)旬)/)) return match[1];
106
+ if (match = line.match(/(\d{1,2})[::.点时]\s*(\d{1,2})/)) return `${match[1].padStart(2, "0")}:${match[2].padStart(2, "0")}`;
107
+ if (match = line.match(/(\d{1,2})\s*点\s*半/)) return `${match[1].padStart(2, "0")}:30`;
108
+ if (match = line.match(/\b(\d{1,2})\s*[.点时](?!\d)/)) return `${match[1].padStart(2, "0")}:00`;
109
+ if (match = line.match(/(\d{1,2})\s*分/)) {
122
110
  const now = /* @__PURE__ */ new Date();
123
111
  const minuteVal = parseInt(match[1]);
124
112
  let hourVal = now.getMinutes() > minuteVal ? now.getHours() + 1 : now.getHours();
125
- hourVal = hourVal % 24;
126
- return `${hourVal.toString().padStart(2, "0")}:${match[1].padStart(2, "0")}`;
113
+ return `${(hourVal % 24).toString().padStart(2, "0")}:${match[1].padStart(2, "0")}`;
127
114
  }
128
- match = line.match(/.*?(?:生日|周年|新衣|活动).*/);
129
- if (match) return match[0].trim();
115
+ if (match = line.match(/.*?(?:生日|周年|新衣|活动).*/)) return match[0].trim();
130
116
  return null;
131
117
  }
132
118
  __name(extractDateTime, "extractDateTime");
@@ -137,12 +123,7 @@ function extractRewards(line) {
137
123
  while ((match = regex.exec(line)) !== null) {
138
124
  const condition = match[1] ? `${match[1]}级灯牌` : "无限制";
139
125
  let amountStr = (match[2] || "").toLowerCase();
140
- let amount = 0;
141
- if (amountStr.includes("w")) {
142
- amount = parseFloat(amountStr.replace("w", "")) * 1e4;
143
- } else {
144
- amount = parseFloat(amountStr);
145
- }
126
+ let amount = amountStr.includes("w") ? parseFloat(amountStr.replace("w", "")) * 1e4 : parseFloat(amountStr);
146
127
  if (!isNaN(amount) && amount > 0) {
147
128
  rewards.push({ amount, condition });
148
129
  }
@@ -151,8 +132,7 @@ function extractRewards(line) {
151
132
  }
152
133
  __name(extractRewards, "extractRewards");
153
134
  function parseEvents(text) {
154
- const lines = text.split("\n").filter((line) => line.trim() !== "");
155
- const events = [];
135
+ const lines = text.split("\n").filter((line) => line.trim());
156
136
  let globalDateTime = null;
157
137
  for (const line of lines) {
158
138
  const timeInLine = extractDateTime(line);
@@ -161,126 +141,84 @@ function parseEvents(text) {
161
141
  break;
162
142
  }
163
143
  }
164
- const allRewards = [];
165
- for (const line of lines) {
166
- const rewardsInLine = extractRewards(line);
167
- allRewards.push(...rewardsInLine);
168
- }
169
- if (allRewards.length > 0) {
170
- events.push({
171
- dateTime: globalDateTime || "时间未知",
172
- rewards: allRewards
173
- });
174
- }
175
- return events.length > 0 ? events : null;
144
+ const allRewards = lines.flatMap((line) => extractRewards(line));
145
+ return allRewards.length > 0 ? [{ dateTime: globalDateTime || "时间未知", rewards: allRewards }] : null;
176
146
  }
177
147
  __name(parseEvents, "parseEvents");
178
148
  function apply(ctx, config) {
179
149
  const forwardedHistory = [];
180
150
  const warningMessageMap = /* @__PURE__ */ new Map();
151
+ const HARD_REJECTION_KEYWORDS = ["发言榜单"];
181
152
  const REJECTION_KEYWORDS = ["签到", "打卡"];
182
153
  const OVERRIDE_KEYWORDS = ["神金", "发"];
183
154
  ctx.on("message", async (session) => {
184
- if (!config.monitorGroups.includes(session.channelId)) return;
185
- const originalMessageContent = session.content;
186
155
  const messageForChecks = session.stripped.content;
187
- const messageId = session.messageId;
188
- const triggerRegex = /神金|发|掉落|猫猫钻|w|\b\d{3,5}\b|一千|一百|十|九|八|七|六|五|四|三|两|二|一/i;
189
- if (!triggerRegex.test(messageForChecks)) {
156
+ const isPureText = session.elements.every((element) => element.type === "text");
157
+ if (!config.monitorGroups.includes(session.channelId)) return;
158
+ if (!isPureText || !messageForChecks.trim()) return;
159
+ if (HARD_REJECTION_KEYWORDS.some((keyword) => messageForChecks.includes(keyword))) {
160
+ ctx.logger.info(`消息包含硬性拒绝关键词,已忽略: ${messageForChecks.substring(0, 30)}...`);
190
161
  return;
191
162
  }
163
+ const triggerRegex = /神金|发|掉落|猫猫钻|w|\b\d{3,5}\b|一千|一百|十|九|八|七|六|五|四|三|两|二|一/i;
164
+ if (!triggerRegex.test(messageForChecks)) return;
192
165
  const roomIds = extractAllRoomIds(messageForChecks);
193
- if (roomIds.length !== 1) {
194
- return;
195
- }
166
+ if (roomIds.length !== 1) return;
196
167
  const roomId = roomIds[0];
197
168
  const preprocessedMessage = preprocessChineseNumerals(messageForChecks);
198
169
  const hasRejectionKeyword = REJECTION_KEYWORDS.some((keyword) => preprocessedMessage.includes(keyword));
199
- if (hasRejectionKeyword) {
200
- const hasOverrideKeyword = OVERRIDE_KEYWORDS.some((keyword) => preprocessedMessage.includes(keyword));
201
- if (!hasOverrideKeyword) {
202
- ctx.logger.info(`消息包含拒绝关键词且无覆盖词,已忽略: ${messageForChecks.substring(0, 50)}...`);
203
- return;
204
- }
205
- }
206
- const parsedEvents = parseEvents(preprocessedMessage);
207
- if (!parsedEvents) {
170
+ if (hasRejectionKeyword && !OVERRIDE_KEYWORDS.some((keyword) => preprocessedMessage.includes(keyword))) {
171
+ ctx.logger.info(`消息包含软性拒绝关键词且无覆盖词,已忽略: ${messageForChecks.substring(0, 30)}...`);
208
172
  return;
209
173
  }
210
- const strongContextRegex = /神金|发|掉落|猫猫钻|w/i;
211
- const hasStrongContext = strongContextRegex.test(preprocessedMessage);
174
+ const parsedEvents = parseEvents(preprocessedMessage);
175
+ if (!parsedEvents) return;
176
+ const hasStrongContext = /神金|发|w/i.test(preprocessedMessage);
212
177
  const hasTime = parsedEvents.some((event) => event.dateTime !== "时间未知");
213
178
  if (!hasStrongContext && !hasTime) {
214
- ctx.logger.info(`纯数字信息缺少时间,已忽略: ${messageForChecks.replace(/\s+/g, " ").substring(0, 50)}...`);
179
+ ctx.logger.info(`纯数字信息缺少时间或强上下文,已忽略: ${messageForChecks.substring(0, 30)}...`);
215
180
  return;
216
181
  }
217
182
  const currentDateTime = parsedEvents[0].dateTime;
218
183
  if (forwardedHistory.some((entry) => entry.roomId === roomId && entry.dateTime === currentDateTime)) {
219
184
  try {
220
- const sentMessageIds = await session.send(`看到啦看到啦,不要发那么多次嘛~`);
221
- if (sentMessageIds && sentMessageIds.length > 0) {
222
- const warningMessageId = sentMessageIds[0];
223
- warningMessageMap.set(messageId, warningMessageId);
224
- if (warningMessageMap.size > config.historySize) {
225
- const oldestKey = warningMessageMap.keys().next().value;
226
- warningMessageMap.delete(oldestKey);
227
- }
228
- }
185
+ const [warningId] = await session.send(`看到啦看到啦,不要发那么多次嘛~`);
186
+ if (warningId) warningMessageMap.set(session.messageId, warningId);
229
187
  } catch (e) {
230
- ctx.logger.warn("发送重复警告消息时失败:", e);
188
+ ctx.logger.warn("发送重复警告消息失败:", e);
231
189
  }
232
190
  return;
233
191
  }
234
192
  let biliInfo = "";
235
193
  let helperMessageId = void 0;
236
194
  try {
237
- const roomInfoUrl = `https://api.live.bilibili.com/room/v1/Room/get_info?room_id=${roomId}`;
238
- const roomInfo = await ctx.http.get(roomInfoUrl);
239
- if (roomInfo.code !== 0 || !roomInfo.data?.uid) throw new Error("无法通过直播间号获取UID");
240
- const uid = roomInfo.data.uid;
241
- const statsUrl = `https://api.bilibili.com/x/space/navnum?mid=${uid}`;
242
- const statsInfo = await ctx.http.get(statsUrl);
243
- if (statsInfo.code !== 0 || statsInfo.data?.video === void 0) throw new Error("无法获取用户投稿数");
195
+ const roomInfo = await ctx.http.get(`https://api.live.bilibili.com/room/v1/Room/get_info?room_id=${roomId}`);
196
+ if (roomInfo?.data?.uid === void 0) throw new Error("无法获取UID");
197
+ const statsInfo = await ctx.http.get(`https://api.bilibili.com/x/space/navnum?mid=${roomInfo.data.uid}`);
198
+ if (statsInfo?.data?.video === void 0) throw new Error("无法获取投稿数");
244
199
  const videoCount = statsInfo.data.video;
245
200
  biliInfo = `
246
201
 
247
202
  ---
248
203
  用户投稿数: ${videoCount}`;
249
- try {
250
- const sentMessageIds = await session.send(`直播间: ${roomId}
204
+ const [sentId] = await session.send(`直播间: ${roomId}
251
205
  用户投稿数: ${videoCount}`);
252
- if (sentMessageIds && sentMessageIds.length > 0) {
253
- helperMessageId = sentMessageIds[0];
254
- }
255
- } catch (e) {
256
- ctx.logger.warn(`向监听群 ${session.channelId} 发送B站信息时失败:`, e);
257
- }
206
+ helperMessageId = sentId;
258
207
  } catch (error) {
259
208
  ctx.logger.warn(`获取直播间 ${roomId} 的B站信息失败: ${error.message}`);
260
209
  return;
261
210
  }
262
- const forwardMessage = originalMessageContent + biliInfo;
263
211
  try {
264
- let forwardedMessageId;
265
- if (config.isGroup) {
266
- const result = await session.bot.sendMessage(config.targetQQ, forwardMessage);
267
- forwardedMessageId = result[0];
268
- } else {
269
- const result = await session.bot.sendPrivateMessage(config.targetQQ, forwardMessage);
270
- forwardedMessageId = result[0];
271
- }
272
- const newEntry = {
273
- originalMessageId: messageId,
212
+ const forwardMessage = session.content + biliInfo;
213
+ const [forwardedMessageId] = config.isGroup ? await session.bot.sendMessage(config.targetQQ, forwardMessage) : await session.bot.sendPrivateMessage(config.targetQQ, forwardMessage);
214
+ forwardedHistory.push({
215
+ originalMessageId: session.messageId,
274
216
  forwardedMessageId,
275
217
  helperMessageId,
276
- originalContent: originalMessageContent,
277
218
  roomId,
278
219
  dateTime: currentDateTime
279
- };
280
- forwardedHistory.push(newEntry);
281
- if (forwardedHistory.length > config.historySize) {
282
- forwardedHistory.shift();
283
- }
220
+ });
221
+ if (forwardedHistory.length > config.historySize) forwardedHistory.shift();
284
222
  } catch (error) {
285
223
  session.send("🐱 - 转发失败,请检查配置");
286
224
  ctx.logger.error("转发失败:", error);
@@ -295,26 +233,24 @@ function apply(ctx, config) {
295
233
  if (entry.helperMessageId) {
296
234
  try {
297
235
  await session.bot.deleteMessage(session.channelId, entry.helperMessageId);
298
- ctx.logger.info(`成功撤回监听群内的助手消息: ${entry.helperMessageId}`);
299
- } catch (error) {
300
- ctx.logger.error(`撤回助手消息 (ID: ${entry.helperMessageId}) 失败:`, error);
236
+ } catch (e) {
237
+ ctx.logger.error(`撤回助手消息 (ID: ${entry.helperMessageId}) 失败:`, e);
301
238
  }
302
239
  }
303
240
  try {
304
241
  await session.bot.deleteMessage(config.targetQQ, entry.forwardedMessageId);
305
- ctx.logger.info(`成功撤回转发的消息: ${entry.forwardedMessageId}`);
306
- } catch (error) {
307
- ctx.logger.error(`撤回转发消息 (ID: ${entry.forwardedMessageId}) 失败:`, error);
242
+ } catch (e) {
243
+ ctx.logger.error(`撤回转发消息 (ID: ${entry.forwardedMessageId}) 失败:`, e);
308
244
  } finally {
309
245
  forwardedHistory.splice(entryIndex, 1);
310
246
  }
311
- } else if (warningMessageMap.has(originalMessageId)) {
247
+ }
248
+ if (warningMessageMap.has(originalMessageId)) {
312
249
  const warningMessageId = warningMessageMap.get(originalMessageId);
313
250
  try {
314
251
  await session.bot.deleteMessage(session.channelId, warningMessageId);
315
- ctx.logger.info(`成功撤回重复提示消息: ${warningMessageId}`);
316
- } catch (error) {
317
- ctx.logger.error(`撤回重复提示消息 (ID: ${warningMessageId}) 失败:`, error);
252
+ } catch (e) {
253
+ ctx.logger.error(`撤回警告消息 (ID: ${warningMessageId}) 失败:`, e);
318
254
  } finally {
319
255
  warningMessageMap.delete(originalMessageId);
320
256
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-cat-raising",
3
3
  "description": "",
4
- "version": "0.1.8",
4
+ "version": "0.2.1",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [