koishi-plugin-cat-raising 0.2.1 → 0.3.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
@@ -2,13 +2,19 @@
2
2
  * @name cat-raising
3
3
  * @description 一个用于监控QQ群内B站直播间奖励信息,并自动转发到指定目标的Koishi插件。
4
4
  * 它能智能解析非结构化的文本,提取关键信息(直播间号、时间、奖励),并进行去重和信息补全。
5
+ * @version 2.1.0
6
+ * @author YourName
5
7
  */
6
8
  import { Context, Schema } from 'koishi';
7
9
  export declare const name = "cat-raising";
10
+ export interface MonitorGroupConfig {
11
+ groupId: string;
12
+ sendHelperMessages: boolean;
13
+ }
8
14
  export interface Config {
9
15
  targetQQ: string;
10
16
  isGroup: boolean;
11
- monitorGroups: string[];
17
+ monitorGroups: MonitorGroupConfig[];
12
18
  historySize: number;
13
19
  }
14
20
  export declare const Config: Schema<Config>;
package/lib/index.js CHANGED
@@ -30,23 +30,15 @@ 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(),
33
+ // 【改动 3】修改 Schema 定义,使其成为一个对象数组
34
+ monitorGroups: import_koishi.Schema.array(import_koishi.Schema.object({
35
+ groupId: import_koishi.Schema.string().description("要监听的 QQ 群号").required(),
36
+ sendHelperMessages: import_koishi.Schema.boolean().description("是否在此群内发送“看到啦”之类的辅助/警告消息").default(true)
37
+ })).description("监听的群组列表及其配置").required(),
34
38
  historySize: import_koishi.Schema.number().description("用于防复读的历史记录大小,防止短期内对同一活动重复转发").default(30).min(5).max(100)
35
39
  });
36
40
  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
- };
41
+ const numMap = { "零": 0, "一": 1, "二": 2, "两": 2, "三": 3, "四": 4, "五": 5, "六": 6, "七": 7, "八": 8, "九": 9 };
50
42
  const unitMap = {
51
43
  "十": { value: 10, isSection: false },
52
44
  "百": { value: 100, isSection: false },
@@ -56,21 +48,16 @@ function preprocessChineseNumerals(text) {
56
48
  };
57
49
  const chineseNumRegex = /([一二三四五六七八九十百千万亿两零]+)/g;
58
50
  return text.replace(chineseNumRegex, (match) => {
59
- if (match.length === 1 && numMap[match] === void 0 && unitMap[match] === void 0) {
60
- return match;
61
- }
51
+ if (match.length === 1 && !numMap[match] && !unitMap[match]) return match;
62
52
  let total = 0;
63
53
  let sectionTotal = 0;
64
54
  let currentNum = 0;
65
- for (let i = 0; i < match.length; i++) {
66
- const char = match[i];
55
+ for (const char of match) {
67
56
  if (numMap[char] !== void 0) {
68
57
  currentNum = numMap[char];
69
58
  } else if (unitMap[char]) {
70
59
  const { value, isSection } = unitMap[char];
71
- if (value === 10 && currentNum === 0) {
72
- currentNum = 1;
73
- }
60
+ if (value === 10 && currentNum === 0) currentNum = 1;
74
61
  sectionTotal += currentNum * value;
75
62
  currentNum = 0;
76
63
  if (isSection) {
@@ -85,13 +72,15 @@ function preprocessChineseNumerals(text) {
85
72
  }
86
73
  __name(preprocessChineseNumerals, "preprocessChineseNumerals");
87
74
  function extractAllRoomIds(text) {
75
+ const sanitizedText = text.replace(/<[^>]+>/g, "");
88
76
  const patterns = [
89
- /(?:播间号|房间号|直播间)[::\s]*(\d{6,15})/g,
77
+ /(?:播间号|房间号|直播间)[::\s]*(\d{3,15})/g,
90
78
  /\b(\d{6,15})\b/g
79
+ // 使用单词边界确保匹配的是独立数字
91
80
  ];
92
81
  const foundIds = /* @__PURE__ */ new Set();
93
82
  for (const pattern of patterns) {
94
- for (const match of text.matchAll(pattern)) {
83
+ for (const match of sanitizedText.matchAll(pattern)) {
95
84
  if (match[1]) foundIds.add(match[1]);
96
85
  }
97
86
  }
@@ -101,7 +90,7 @@ __name(extractAllRoomIds, "extractAllRoomIds");
101
90
  function extractDateTime(line) {
102
91
  let match;
103
92
  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`;
93
+ if (match = line.match(/每晚\s*(\d{1,2})\s*[点时]/)) return `每晚 ${match[1].padStart(2, "0")}:00`;
105
94
  if (match = line.match(/(\d{1,2}\s*月\s*(?:上|中|下)旬)/)) return match[1];
106
95
  if (match = line.match(/(\d{1,2})[::.点时]\s*(\d{1,2})/)) return `${match[1].padStart(2, "0")}:${match[2].padStart(2, "0")}`;
107
96
  if (match = line.match(/(\d{1,2})\s*点\s*半/)) return `${match[1].padStart(2, "0")}:30`;
@@ -109,7 +98,7 @@ function extractDateTime(line) {
109
98
  if (match = line.match(/(\d{1,2})\s*分/)) {
110
99
  const now = /* @__PURE__ */ new Date();
111
100
  const minuteVal = parseInt(match[1]);
112
- let hourVal = now.getMinutes() > minuteVal ? now.getHours() + 1 : now.getHours();
101
+ const hourVal = now.getMinutes() > minuteVal ? now.getHours() + 1 : now.getHours();
113
102
  return `${(hourVal % 24).toString().padStart(2, "0")}:${match[1].padStart(2, "0")}`;
114
103
  }
115
104
  if (match = line.match(/.*?(?:生日|周年|新衣|活动).*/)) return match[0].trim();
@@ -122,8 +111,8 @@ function extractRewards(line) {
122
111
  let match;
123
112
  while ((match = regex.exec(line)) !== null) {
124
113
  const condition = match[1] ? `${match[1]}级灯牌` : "无限制";
125
- let amountStr = (match[2] || "").toLowerCase();
126
- let amount = amountStr.includes("w") ? parseFloat(amountStr.replace("w", "")) * 1e4 : parseFloat(amountStr);
114
+ const amountStr = (match[2] || "").toLowerCase();
115
+ const amount = amountStr.includes("w") ? parseFloat(amountStr.replace("w", "")) * 1e4 : parseFloat(amountStr);
127
116
  if (!isNaN(amount) && amount > 0) {
128
117
  rewards.push({ amount, condition });
129
118
  }
@@ -131,7 +120,7 @@ function extractRewards(line) {
131
120
  return rewards;
132
121
  }
133
122
  __name(extractRewards, "extractRewards");
134
- function parseEvents(text) {
123
+ function parseEventFromText(text) {
135
124
  const lines = text.split("\n").filter((line) => line.trim());
136
125
  let globalDateTime = null;
137
126
  for (const line of lines) {
@@ -142,90 +131,97 @@ function parseEvents(text) {
142
131
  }
143
132
  }
144
133
  const allRewards = lines.flatMap((line) => extractRewards(line));
145
- return allRewards.length > 0 ? [{ dateTime: globalDateTime || "时间未知", rewards: allRewards }] : null;
134
+ return allRewards.length > 0 ? { dateTime: globalDateTime || "时间未知", rewards: allRewards } : null;
135
+ }
136
+ __name(parseEventFromText, "parseEventFromText");
137
+ var HARD_REJECTION_KEYWORDS = ["发言榜单"];
138
+ var REJECTION_KEYWORDS = ["签到", "打卡"];
139
+ var OVERRIDE_KEYWORDS = ["神金", "发"];
140
+ var TRIGGER_REGEX = /神金|发|掉落|猫猫钻|w|\b\d{3,5}\b|一千|一百|十|九|八|七|六|五|四|三|两|二|一/i;
141
+ async function fetchBilibiliInfo(ctx, roomId) {
142
+ try {
143
+ const roomInfo = await ctx.http.get(`https://api.live.bilibili.com/room/v1/Room/get_info?room_id=${roomId}`);
144
+ const uid = roomInfo?.data?.uid;
145
+ if (!uid) throw new Error("无法从房间信息中获取UID");
146
+ const statsInfo = await ctx.http.get(`https://api.bilibili.com/x/space/navnum?mid=${uid}`);
147
+ const videoCount = statsInfo?.data?.video;
148
+ if (videoCount === void 0) throw new Error("无法从空间信息中获取投稿数");
149
+ return { videoCount };
150
+ } catch (error) {
151
+ ctx.logger.warn(`[API] 获取直播间 ${roomId} 的B站信息失败: ${error.message}`);
152
+ return null;
153
+ }
146
154
  }
147
- __name(parseEvents, "parseEvents");
155
+ __name(fetchBilibiliInfo, "fetchBilibiliInfo");
148
156
  function apply(ctx, config) {
149
157
  const forwardedHistory = [];
150
158
  const warningMessageMap = /* @__PURE__ */ new Map();
151
- const HARD_REJECTION_KEYWORDS = ["发言榜单"];
152
- const REJECTION_KEYWORDS = ["签到", "打卡"];
153
- const OVERRIDE_KEYWORDS = ["神金", "发"];
154
159
  ctx.on("message", async (session) => {
155
- const messageForChecks = session.stripped.content;
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)}...`);
160
+ const groupConfig = config.monitorGroups.find((g) => g.groupId === session.channelId);
161
+ if (!groupConfig) return;
162
+ const strippedContent = session.stripped.content;
163
+ if (!strippedContent.trim()) return;
164
+ if (HARD_REJECTION_KEYWORDS.some((keyword) => strippedContent.includes(keyword))) return;
165
+ if (!TRIGGER_REGEX.test(strippedContent)) return;
166
+ const roomIds = extractAllRoomIds(session.content);
167
+ if (roomIds.length !== 1) {
168
+ if (roomIds.length > 1) ctx.logger.info(`[忽略] 消息包含多个房间号: ${roomIds.join(", ")}`);
161
169
  return;
162
170
  }
163
- const triggerRegex = /神金|发|掉落|猫猫钻|w|\b\d{3,5}\b|一千|一百|十|九|八|七|六|五|四|三|两|二|一/i;
164
- if (!triggerRegex.test(messageForChecks)) return;
165
- const roomIds = extractAllRoomIds(messageForChecks);
166
- if (roomIds.length !== 1) return;
167
171
  const roomId = roomIds[0];
168
- const preprocessedMessage = preprocessChineseNumerals(messageForChecks);
172
+ const preprocessedMessage = preprocessChineseNumerals(strippedContent);
169
173
  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)}...`);
172
- return;
173
- }
174
- const parsedEvents = parseEvents(preprocessedMessage);
175
- if (!parsedEvents) return;
174
+ if (hasRejectionKeyword && !OVERRIDE_KEYWORDS.some((keyword) => preprocessedMessage.includes(keyword))) return;
175
+ const parsedEvent = parseEventFromText(preprocessedMessage);
176
+ if (!parsedEvent) return;
176
177
  const hasStrongContext = /神金|发|w/i.test(preprocessedMessage);
177
- const hasTime = parsedEvents.some((event) => event.dateTime !== "时间未知");
178
- if (!hasStrongContext && !hasTime) {
179
- ctx.logger.info(`纯数字信息缺少时间或强上下文,已忽略: ${messageForChecks.substring(0, 30)}...`);
178
+ const hasTime = parsedEvent.dateTime !== "时间未知";
179
+ if (!hasStrongContext && !hasTime) return;
180
+ const { dateTime } = parsedEvent;
181
+ if (forwardedHistory.some((entry) => entry.roomId === roomId && entry.dateTime === dateTime)) {
182
+ ctx.logger.info(`[防复读] 检测到重复活动: 房间=${roomId}, 时间=${dateTime}`);
183
+ if (groupConfig.sendHelperMessages) {
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
+ }
190
+ }
180
191
  return;
181
192
  }
182
- const currentDateTime = parsedEvents[0].dateTime;
183
- if (forwardedHistory.some((entry) => entry.roomId === roomId && entry.dateTime === currentDateTime)) {
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
- }
193
+ const biliInfo = await fetchBilibiliInfo(ctx, roomId);
194
+ if (!biliInfo) {
190
195
  return;
191
196
  }
192
- let biliInfo = "";
193
- let helperMessageId = void 0;
197
+ let helperMessageId;
198
+ if (groupConfig.sendHelperMessages) {
199
+ [helperMessageId] = await session.send(`直播间: ${roomId}
200
+ 投稿数: ${biliInfo.videoCount}`);
201
+ }
194
202
  try {
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("无法获取投稿数");
199
- const videoCount = statsInfo.data.video;
200
- biliInfo = `
203
+ const forwardMessage = `${session.content}
201
204
 
202
205
  ---
203
- 用户投稿数: ${videoCount}`;
204
- const [sentId] = await session.send(`直播间: ${roomId}
205
- 用户投稿数: ${videoCount}`);
206
- helperMessageId = sentId;
207
- } catch (error) {
208
- ctx.logger.warn(`获取直播间 ${roomId} 的B站信息失败: ${error.message}`);
209
- return;
210
- }
211
- try {
212
- const forwardMessage = session.content + biliInfo;
206
+ 投稿数: ${biliInfo.videoCount}`;
213
207
  const [forwardedMessageId] = config.isGroup ? await session.bot.sendMessage(config.targetQQ, forwardMessage) : await session.bot.sendPrivateMessage(config.targetQQ, forwardMessage);
214
208
  forwardedHistory.push({
215
209
  originalMessageId: session.messageId,
216
210
  forwardedMessageId,
217
211
  helperMessageId,
212
+ // helperMessageId 可能是 undefined,这没有问题
218
213
  roomId,
219
- dateTime: currentDateTime
214
+ dateTime
220
215
  });
221
216
  if (forwardedHistory.length > config.historySize) forwardedHistory.shift();
222
217
  } catch (error) {
223
- session.send("🐱 - 转发失败,请检查配置");
224
- ctx.logger.error("转发失败:", error);
218
+ session.send("🐱 - 转发失败,请检查目标QQ/群号配置是否正确");
219
+ ctx.logger.error("[转发] 失败:", error);
225
220
  }
226
221
  });
227
222
  ctx.on("message-deleted", async (session) => {
228
- if (!config.monitorGroups.includes(session.channelId)) return;
223
+ const isMonitored = config.monitorGroups.some((g) => g.groupId === session.channelId);
224
+ if (!isMonitored) return;
229
225
  const originalMessageId = session.messageId;
230
226
  const entryIndex = forwardedHistory.findIndex((entry) => entry.originalMessageId === originalMessageId);
231
227
  if (entryIndex !== -1) {
@@ -234,15 +230,16 @@ function apply(ctx, config) {
234
230
  try {
235
231
  await session.bot.deleteMessage(session.channelId, entry.helperMessageId);
236
232
  } catch (e) {
237
- ctx.logger.error(`撤回助手消息 (ID: ${entry.helperMessageId}) 失败:`, e);
233
+ ctx.logger.warn(`[撤回] 助手消息 (ID: ${entry.helperMessageId}) 失败:`, e);
238
234
  }
239
235
  }
240
236
  try {
241
237
  await session.bot.deleteMessage(config.targetQQ, entry.forwardedMessageId);
242
238
  } catch (e) {
243
- ctx.logger.error(`撤回转发消息 (ID: ${entry.forwardedMessageId}) 失败:`, e);
239
+ ctx.logger.warn(`[撤回] 转发消息 (ID: ${entry.forwardedMessageId}) 失败:`, e);
244
240
  } finally {
245
241
  forwardedHistory.splice(entryIndex, 1);
242
+ ctx.logger.info(`[撤回] 已联动撤回与源消息 ${originalMessageId} 相关的转发。`);
246
243
  }
247
244
  }
248
245
  if (warningMessageMap.has(originalMessageId)) {
@@ -250,9 +247,10 @@ function apply(ctx, config) {
250
247
  try {
251
248
  await session.bot.deleteMessage(session.channelId, warningMessageId);
252
249
  } catch (e) {
253
- ctx.logger.error(`撤回警告消息 (ID: ${warningMessageId}) 失败:`, e);
250
+ ctx.logger.warn(`[撤回] 警告消息 (ID: ${warningMessageId}) 失败:`, e);
254
251
  } finally {
255
252
  warningMessageMap.delete(originalMessageId);
253
+ ctx.logger.info(`[撤回] 已联动撤回与源消息 ${originalMessageId} 相关的警告。`);
256
254
  }
257
255
  }
258
256
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-cat-raising",
3
3
  "description": "",
4
- "version": "0.2.1",
4
+ "version": "0.3.1",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [