koishi-plugin-cat-raising 0.2.1 → 0.3.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 +7 -1
- package/lib/index.js +84 -86
- package/package.json +1 -1
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:
|
|
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
|
-
|
|
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]
|
|
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 (
|
|
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
77
|
/(?:播间号|房间号|直播间)[::\s]*(\d{6,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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
126
|
-
|
|
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
|
|
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 ?
|
|
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(
|
|
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
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
if (!
|
|
159
|
-
if (HARD_REJECTION_KEYWORDS.some((keyword) =>
|
|
160
|
-
|
|
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(
|
|
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
|
-
|
|
172
|
-
|
|
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 =
|
|
178
|
-
if (!hasStrongContext && !hasTime)
|
|
179
|
-
|
|
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
|
|
183
|
-
if (
|
|
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
|
|
193
|
-
|
|
197
|
+
let helperMessageId;
|
|
198
|
+
if (groupConfig.sendHelperMessages) {
|
|
199
|
+
[helperMessageId] = await session.send(`直播间: ${roomId}
|
|
200
|
+
投稿数: ${biliInfo.videoCount}`);
|
|
201
|
+
}
|
|
194
202
|
try {
|
|
195
|
-
const
|
|
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
|
-
|
|
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
|
|
214
|
+
dateTime
|
|
220
215
|
});
|
|
221
216
|
if (forwardedHistory.length > config.historySize) forwardedHistory.shift();
|
|
222
217
|
} catch (error) {
|
|
223
|
-
session.send("🐱 -
|
|
224
|
-
ctx.logger.error("
|
|
218
|
+
session.send("🐱 - 转发失败,请检查目标QQ/群号配置是否正确");
|
|
219
|
+
ctx.logger.error("[转发] 失败:", error);
|
|
225
220
|
}
|
|
226
221
|
});
|
|
227
222
|
ctx.on("message-deleted", async (session) => {
|
|
228
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
});
|