koishi-plugin-cat-raising 0.1.7 → 0.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.d.ts CHANGED
@@ -1,9 +1,14 @@
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 {
4
9
  targetQQ: string;
5
10
  isGroup: boolean;
6
- monitorGroup: string;
11
+ monitorGroups: string[];
7
12
  historySize: number;
8
13
  }
9
14
  export declare const Config: Schema<Config>;
package/lib/index.js CHANGED
@@ -30,18 +30,68 @@ 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
- monitorGroup: 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
+ function preprocessChineseNumerals(text) {
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
49
+ };
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
+ });
85
+ }
86
+ __name(preprocessChineseNumerals, "preprocessChineseNumerals");
36
87
  function extractAllRoomIds(text) {
37
88
  const patterns = [
38
89
  /(?:播间号|房间号|直播间)[::\s]*(\d{6,15})/g,
39
- /\b(\d{8,15})\b/g
90
+ /\b(\d{6,15})\b/g
40
91
  ];
41
92
  const foundIds = /* @__PURE__ */ new Set();
42
93
  for (const pattern of patterns) {
43
- const matches = text.matchAll(pattern);
44
- for (const match of matches) {
94
+ for (const match of text.matchAll(pattern)) {
45
95
  if (match[1]) foundIds.add(match[1]);
46
96
  }
47
97
  }
@@ -49,35 +99,20 @@ function extractAllRoomIds(text) {
49
99
  }
50
100
  __name(extractAllRoomIds, "extractAllRoomIds");
51
101
  function extractDateTime(line) {
52
- let match = line.match(/(\d{1,2})\s*[月.]\s*(\d{1,2})\s*日?/);
53
- if (match) return `${match[1]}月${match[2]}日`;
54
- match = line.match(/每晚\s*(\d{1,2})\s*点/);
55
- if (match) return `每晚 ${match[1].padStart(2, "0")}:00`;
56
- match = line.match(/(\d{1,2}\s*月\s*(?:上|中|下)旬)/);
57
- if (match) return match[1];
58
- match = line.match(/(\d{1,2})[::.点时]\s*(\d{1,2})/);
59
- if (match && match[2]) {
60
- const hour = match[1].padStart(2, "0");
61
- const minute = match[2].padStart(2, "0");
62
- return `${hour}:${minute}`;
63
- }
64
- match = line.match(/(\d{1,2})\s*点\s*半/);
65
- if (match) return `${match[1].padStart(2, "0")}:30`;
66
- match = line.match(/\b(\d{1,2})\s*[.点时](?!\d)/);
67
- if (match && match[1]) {
68
- const hour = match[1].padStart(2, "0");
69
- return `${hour}:00`;
70
- }
71
- match = line.match(/(\d{1,2})\s*分/);
72
- 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*分/)) {
73
110
  const now = /* @__PURE__ */ new Date();
74
111
  const minuteVal = parseInt(match[1]);
75
112
  let hourVal = now.getMinutes() > minuteVal ? now.getHours() + 1 : now.getHours();
76
- hourVal = hourVal % 24;
77
- return `${hourVal.toString().padStart(2, "0")}:${match[1].padStart(2, "0")}`;
113
+ return `${(hourVal % 24).toString().padStart(2, "0")}:${match[1].padStart(2, "0")}`;
78
114
  }
79
- match = line.match(/.*?(?:生日|周年|新衣|活动).*/);
80
- if (match) return match[0].trim();
115
+ if (match = line.match(/.*?(?:生日|周年|新衣|活动).*/)) return match[0].trim();
81
116
  return null;
82
117
  }
83
118
  __name(extractDateTime, "extractDateTime");
@@ -88,12 +123,7 @@ function extractRewards(line) {
88
123
  while ((match = regex.exec(line)) !== null) {
89
124
  const condition = match[1] ? `${match[1]}级灯牌` : "无限制";
90
125
  let amountStr = (match[2] || "").toLowerCase();
91
- let amount = 0;
92
- if (amountStr.includes("w")) {
93
- amount = parseFloat(amountStr.replace("w", "")) * 1e4;
94
- } else {
95
- amount = parseFloat(amountStr);
96
- }
126
+ let amount = amountStr.includes("w") ? parseFloat(amountStr.replace("w", "")) * 1e4 : parseFloat(amountStr);
97
127
  if (!isNaN(amount) && amount > 0) {
98
128
  rewards.push({ amount, condition });
99
129
  }
@@ -102,8 +132,7 @@ function extractRewards(line) {
102
132
  }
103
133
  __name(extractRewards, "extractRewards");
104
134
  function parseEvents(text) {
105
- const lines = text.split("\n").filter((line) => line.trim() !== "");
106
- const events = [];
135
+ const lines = text.split("\n").filter((line) => line.trim());
107
136
  let globalDateTime = null;
108
137
  for (const line of lines) {
109
138
  const timeInLine = extractDateTime(line);
@@ -112,119 +141,91 @@ function parseEvents(text) {
112
141
  break;
113
142
  }
114
143
  }
115
- const allRewards = [];
116
- for (const line of lines) {
117
- const rewardsInLine = extractRewards(line);
118
- allRewards.push(...rewardsInLine);
119
- }
120
- if (allRewards.length > 0) {
121
- events.push({
122
- dateTime: globalDateTime || "时间未知",
123
- rewards: allRewards
124
- });
125
- }
126
- return events.length > 0 ? events : null;
144
+ const allRewards = lines.flatMap((line) => extractRewards(line));
145
+ return allRewards.length > 0 ? [{ dateTime: globalDateTime || "时间未知", rewards: allRewards }] : null;
127
146
  }
128
147
  __name(parseEvents, "parseEvents");
129
148
  function apply(ctx, config) {
130
149
  const forwardedHistory = [];
150
+ const warningMessageMap = /* @__PURE__ */ new Map();
151
+ const HARD_REJECTION_KEYWORDS = ["发言榜单"];
131
152
  const REJECTION_KEYWORDS = ["签到", "打卡"];
132
153
  const OVERRIDE_KEYWORDS = ["神金", "发"];
133
154
  ctx.on("message", async (session) => {
134
- if (session.channelId !== config.monitorGroup) return;
135
- const originalMessageContent = session.content;
136
155
  const messageForChecks = session.stripped.content;
137
- const messageId = session.messageId;
138
- const triggerRegex = /神金|发|掉落|猫猫钻|w|\b\d{3,5}\b/i;
139
- 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)}...`);
140
161
  return;
141
162
  }
142
- const hasRejectionKeyword = REJECTION_KEYWORDS.some((keyword) => messageForChecks.includes(keyword));
143
- if (hasRejectionKeyword) {
144
- const hasOverrideKeyword = OVERRIDE_KEYWORDS.some((keyword) => messageForChecks.includes(keyword));
145
- if (!hasOverrideKeyword) {
146
- ctx.logger.info(`消息包含拒绝关键词且无覆盖词,已忽略: ${messageForChecks.substring(0, 50)}...`);
147
- return;
148
- }
149
- }
163
+ const triggerRegex = /神金|发|掉落|猫猫钻|w|\b\d{3,5}\b|一千|一百|十|九|八|七|六|五|四|三|两|二|一/i;
164
+ if (!triggerRegex.test(messageForChecks)) return;
150
165
  const roomIds = extractAllRoomIds(messageForChecks);
151
- if (roomIds.length > 1) {
166
+ if (roomIds.length !== 1) return;
167
+ const roomId = roomIds[0];
168
+ const preprocessedMessage = preprocessChineseNumerals(messageForChecks);
169
+ const hasRejectionKeyword = REJECTION_KEYWORDS.some((keyword) => preprocessedMessage.includes(keyword));
170
+ if (hasRejectionKeyword && !OVERRIDE_KEYWORDS.some((keyword) => preprocessedMessage.includes(keyword))) {
171
+ ctx.logger.info(`消息包含软性拒绝关键词且无覆盖词,已忽略: ${messageForChecks.substring(0, 30)}...`);
152
172
  return;
153
173
  }
154
- const roomId = roomIds.length === 1 ? roomIds[0] : null;
155
- const parsedEvents = parseEvents(messageForChecks);
156
- if (!parsedEvents || !roomId) {
157
- return;
158
- }
159
- const strongContextRegex = /神金|发|掉落|猫猫钻|w/i;
160
- const hasStrongContext = strongContextRegex.test(messageForChecks);
174
+ const parsedEvents = parseEvents(preprocessedMessage);
175
+ if (!parsedEvents) return;
176
+ const hasStrongContext = /神金|发|w/i.test(preprocessedMessage);
161
177
  const hasTime = parsedEvents.some((event) => event.dateTime !== "时间未知");
162
178
  if (!hasStrongContext && !hasTime) {
163
- ctx.logger.info(`纯数字信息缺少时间,已忽略: ${messageForChecks.replace(/\s+/g, " ").substring(0, 50)}...`);
179
+ ctx.logger.info(`纯数字信息缺少时间或强上下文,已忽略: ${messageForChecks.substring(0, 30)}...`);
164
180
  return;
165
181
  }
166
182
  const currentDateTime = parsedEvents[0].dateTime;
167
183
  if (forwardedHistory.some((entry) => entry.roomId === roomId && entry.dateTime === currentDateTime)) {
168
- session.send(`看到啦看到啦,不要发那么多次嘛~`);
184
+ try {
185
+ const [warningId] = await session.send(`看到啦看到啦,不要发那么多次嘛~`);
186
+ if (warningId) warningMessageMap.set(session.messageId, warningId);
187
+ } catch (e) {
188
+ ctx.logger.warn("发送重复警告消息失败:", e);
189
+ }
169
190
  return;
170
191
  }
171
192
  let biliInfo = "";
172
193
  let helperMessageId = void 0;
173
194
  try {
174
- const roomInfoUrl = `https://api.live.bilibili.com/room/v1/Room/get_info?room_id=${roomId}`;
175
- const roomInfo = await ctx.http.get(roomInfoUrl);
176
- if (roomInfo.code !== 0 || !roomInfo.data?.uid) throw new Error("无法通过直播间号获取UID");
177
- const uid = roomInfo.data.uid;
178
- const statsUrl = `https://api.bilibili.com/x/space/navnum?mid=${uid}`;
179
- const statsInfo = await ctx.http.get(statsUrl);
180
- 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("无法获取投稿数");
181
199
  const videoCount = statsInfo.data.video;
182
200
  biliInfo = `
183
201
 
184
202
  ---
185
203
  用户投稿数: ${videoCount}`;
186
- try {
187
- const sentMessageIds = await session.send(`直播间: ${roomId}
204
+ const [sentId] = await session.send(`直播间: ${roomId}
188
205
  用户投稿数: ${videoCount}`);
189
- if (sentMessageIds && sentMessageIds.length > 0) {
190
- helperMessageId = sentMessageIds[0];
191
- }
192
- } catch (e) {
193
- ctx.logger.warn(`向监听群 ${config.monitorGroup} 发送B站信息时失败:`, e);
194
- }
206
+ helperMessageId = sentId;
195
207
  } catch (error) {
196
208
  ctx.logger.warn(`获取直播间 ${roomId} 的B站信息失败: ${error.message}`);
197
209
  return;
198
210
  }
199
- const forwardMessage = originalMessageContent + biliInfo;
200
211
  try {
201
- let forwardedMessageId;
202
- if (config.isGroup) {
203
- const result = await session.bot.sendMessage(config.targetQQ, forwardMessage);
204
- forwardedMessageId = result[0];
205
- } else {
206
- const result = await session.bot.sendPrivateMessage(config.targetQQ, forwardMessage);
207
- forwardedMessageId = result[0];
208
- }
209
- const newEntry = {
210
- 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,
211
216
  forwardedMessageId,
212
217
  helperMessageId,
213
- // [核心改动 4] 存入历史记录
214
- originalContent: originalMessageContent,
215
218
  roomId,
216
219
  dateTime: currentDateTime
217
- };
218
- forwardedHistory.push(newEntry);
219
- if (forwardedHistory.length > config.historySize) {
220
- forwardedHistory.shift();
221
- }
220
+ });
221
+ if (forwardedHistory.length > config.historySize) forwardedHistory.shift();
222
222
  } catch (error) {
223
223
  session.send("🐱 - 转发失败,请检查配置");
224
224
  ctx.logger.error("转发失败:", error);
225
225
  }
226
226
  });
227
227
  ctx.on("message-deleted", async (session) => {
228
+ if (!config.monitorGroups.includes(session.channelId)) return;
228
229
  const originalMessageId = session.messageId;
229
230
  const entryIndex = forwardedHistory.findIndex((entry) => entry.originalMessageId === originalMessageId);
230
231
  if (entryIndex !== -1) {
@@ -232,20 +233,28 @@ function apply(ctx, config) {
232
233
  if (entry.helperMessageId) {
233
234
  try {
234
235
  await session.bot.deleteMessage(session.channelId, entry.helperMessageId);
235
- ctx.logger.info(`成功撤回监听群内的助手消息: ${entry.helperMessageId}`);
236
- } catch (error) {
237
- ctx.logger.error(`撤回助手消息 (ID: ${entry.helperMessageId}) 失败:`, error);
236
+ } catch (e) {
237
+ ctx.logger.error(`撤回助手消息 (ID: ${entry.helperMessageId}) 失败:`, e);
238
238
  }
239
239
  }
240
240
  try {
241
241
  await session.bot.deleteMessage(config.targetQQ, entry.forwardedMessageId);
242
- ctx.logger.info(`成功撤回转发的消息: ${entry.forwardedMessageId}`);
243
- } catch (error) {
244
- ctx.logger.error(`撤回转发消息 (ID: ${entry.forwardedMessageId}) 失败:`, error);
242
+ } catch (e) {
243
+ ctx.logger.error(`撤回转发消息 (ID: ${entry.forwardedMessageId}) 失败:`, e);
245
244
  } finally {
246
245
  forwardedHistory.splice(entryIndex, 1);
247
246
  }
248
247
  }
248
+ if (warningMessageMap.has(originalMessageId)) {
249
+ const warningMessageId = warningMessageMap.get(originalMessageId);
250
+ try {
251
+ await session.bot.deleteMessage(session.channelId, warningMessageId);
252
+ } catch (e) {
253
+ ctx.logger.error(`撤回警告消息 (ID: ${warningMessageId}) 失败:`, e);
254
+ } finally {
255
+ warningMessageMap.delete(originalMessageId);
256
+ }
257
+ }
249
258
  });
250
259
  }
251
260
  __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": "0.1.7",
4
+ "version": "0.2.0",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [