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 +5 -0
- package/lib/index.js +102 -166
- package/package.json +1 -1
package/lib/index.d.ts
CHANGED
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("监听的群号列表 (
|
|
34
|
-
historySize: import_koishi.Schema.number().description("
|
|
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
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
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
|
|
102
|
-
if (match) return `${match[1]}月${match[2]}日`;
|
|
103
|
-
match = line.match(/每晚\s*(\d{1,2})\s*点/)
|
|
104
|
-
if (match) return
|
|
105
|
-
match = line.match(/(\d{1,2}\s
|
|
106
|
-
if (match) return match[1]
|
|
107
|
-
match = line.match(
|
|
108
|
-
if (match
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
188
|
-
|
|
189
|
-
if (!
|
|
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
|
-
|
|
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
|
|
211
|
-
|
|
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(
|
|
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
|
|
221
|
-
if (
|
|
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("
|
|
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
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
250
|
-
const sentMessageIds = await session.send(`直播间: ${roomId}
|
|
204
|
+
const [sentId] = await session.send(`直播间: ${roomId}
|
|
251
205
|
用户投稿数: ${videoCount}`);
|
|
252
|
-
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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.
|
|
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
|
-
|
|
299
|
-
|
|
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
|
-
|
|
306
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
316
|
-
|
|
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
|
}
|