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 +6 -1
- package/lib/index.js +121 -112
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
+
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{
|
|
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
|
|
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
|
|
53
|
-
if (match) return `${match[1]}月${match[2]}日`;
|
|
54
|
-
match = line.match(/每晚\s*(\d{1,2})\s*点/)
|
|
55
|
-
if (match) return
|
|
56
|
-
match = line.match(/(\d{1,2}\s
|
|
57
|
-
if (match) return match[1]
|
|
58
|
-
match = line.match(
|
|
59
|
-
if (match
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
138
|
-
|
|
139
|
-
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)}...`);
|
|
140
161
|
return;
|
|
141
162
|
}
|
|
142
|
-
const
|
|
143
|
-
if (
|
|
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
|
|
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
|
|
155
|
-
|
|
156
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
187
|
-
const sentMessageIds = await session.send(`直播间: ${roomId}
|
|
204
|
+
const [sentId] = await session.send(`直播间: ${roomId}
|
|
188
205
|
用户投稿数: ${videoCount}`);
|
|
189
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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.
|
|
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
|
-
|
|
236
|
-
|
|
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
|
-
|
|
243
|
-
|
|
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");
|