koishi-plugin-cat-raising 1.0.1 → 1.1.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,5 +1,12 @@
1
1
  import { Context, Schema } from 'koishi';
2
2
  export declare const name = "cat-raising";
3
+ /** B站 access_key 配置项 */
4
+ export interface BiliAccessKeyConfig {
5
+ /** Bilibili access_key */
6
+ key: string;
7
+ /** 对此 access_key 的备注,例如所属账号 */
8
+ remark?: string;
9
+ }
3
10
  /** 监听群组的配置 */
4
11
  export interface MonitorGroupConfig {
5
12
  /** 要监听的 QQ 群号 */
@@ -18,7 +25,7 @@ export interface Config {
18
25
  /** 用于防复读的历史记录大小 */
19
26
  historySize: number;
20
27
  /** 用于发送B站弹幕的 access_key 列表 */
21
- biliAccessKeys: string[];
28
+ biliAccessKeys: BiliAccessKeyConfig[];
22
29
  }
23
30
  export declare const Config: Schema<Config>;
24
31
  /**
package/lib/index.js CHANGED
@@ -47,7 +47,10 @@ var Config = import_koishi.Schema.object({
47
47
  sendHelperMessages: import_koishi.Schema.boolean().description("是否在此群内发送“看到啦”之类的辅助/警告消息").default(true)
48
48
  })).description("监听的群组列表及其配置").required(),
49
49
  historySize: import_koishi.Schema.number().description("用于防复读的历史记录大小,防止短期内对同一活动重复转发").default(30).min(5).max(100),
50
- biliAccessKeys: import_koishi.Schema.array(import_koishi.Schema.string()).description("用于发送B站弹幕的 access_key 列表。插件会随机选择一个使用。如果留空,则不执行发送弹幕功能。").default([])
50
+ biliAccessKeys: import_koishi.Schema.array(import_koishi.Schema.object({
51
+ key: import_koishi.Schema.string().description("Bilibili access_key").required(),
52
+ remark: import_koishi.Schema.string().description("对此 access_key 的备注,例如所属账号")
53
+ })).description("用于发送B站弹幕的 access_key 列表。插件会为列表中的每个 key 发送弹幕。如果留空,则不执行发送弹幕功能。").default([])
51
54
  });
52
55
  function preprocessChineseNumerals(text) {
53
56
  const numMap = { "零": 0, "一": 1, "二": 2, "两": 2, "三": 3, "四": 4, "五": 5, "六": 6, "七": 7, "八": 8, "九": 9 };
@@ -149,6 +152,7 @@ __name(parseEventFromText, "parseEventFromText");
149
152
  var HARD_REJECTION_KEYWORDS = ["发言榜单", "投稿数:"];
150
153
  var REJECTION_KEYWORDS = ["签到", "打卡"];
151
154
  var OVERRIDE_KEYWORDS = ["神金", "发"];
155
+ var CHECK_IN_REJECTION_REGEX = /\b\d{2,3}\s*\+/;
152
156
  var TRIGGER_REGEX = /神金|发|掉落|猫猫钻|w|\b\d{3,5}\b|一千|一百|十|九|八|七|六|五|四|三|两|二|一/i;
153
157
  var BILI_APPKEY = "4409e2ce8ffd12b8";
154
158
  var BILI_APPSECRET = "59b43e04ad6965f34319062b478f83dd";
@@ -159,48 +163,64 @@ function signBilibiliParams(params, appSecret) {
159
163
  return sign;
160
164
  }
161
165
  __name(signBilibiliParams, "signBilibiliParams");
162
- async function sendBilibiliDanmaku(ctx, config, roomId, message) {
163
- if (!config.biliAccessKeys || config.biliAccessKeys.length === 0) {
164
- ctx.logger.info("[弹幕] 未配置 access_key,跳过发送弹幕。");
165
- return;
166
- }
167
- const accessKey = config.biliAccessKeys[Math.floor(Math.random() * config.biliAccessKeys.length)];
166
+ async function sendBilibiliDanmaku(ctx, keyConfig, roomId, message) {
167
+ const MAX_RETRIES = 4;
168
+ const RETRY_DELAY_MS = 3e3;
169
+ const FREQUENCY_LIMIT_KEYWORD = "频率过快";
168
170
  const url = "https://api.live.bilibili.com/xlive/app-room/v1/dM/sendmsg";
169
- const ts = Math.floor(Date.now() / 1e3);
170
- const baseParams = {
171
- access_key: accessKey,
172
- actionKey: "appkey",
173
- appkey: BILI_APPKEY,
174
- cid: roomId,
175
- msg: message,
176
- rnd: ts,
177
- color: "16777215",
178
- // 白色
179
- fontsize: "25",
180
- mode: "1",
181
- // 滚动弹幕
182
- ts
183
- };
184
- const sign = signBilibiliParams(baseParams, BILI_APPSECRET);
185
- const params = { ...baseParams, sign };
186
- const formData = new import_url.URLSearchParams();
187
- for (const key in params) {
188
- formData.append(key, params[key]);
189
- }
190
- try {
191
- const response = await ctx.http.post(url, formData, {
192
- headers: {
193
- "Content-Type": "application/x-www-form-urlencoded",
194
- "User-Agent": "Mozilla/5.0 BiliDroid/6.73.1 (bbcallen@gmail.com) os/android model/Mi 10 Pro mobi_app/android build/6731100 channel/xiaomi innerVer/6731110 osVer/12 network/2"
171
+ const logIdentifier = keyConfig.remark || keyConfig.key.slice(0, 8);
172
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
173
+ if (attempt > 0) {
174
+ await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
175
+ }
176
+ const ts = Math.floor(Date.now() / 1e3);
177
+ const baseParams = {
178
+ access_key: keyConfig.key,
179
+ actionKey: "appkey",
180
+ appkey: BILI_APPKEY,
181
+ cid: roomId,
182
+ msg: message,
183
+ rnd: ts,
184
+ color: "16777215",
185
+ // 白色
186
+ fontsize: "25",
187
+ mode: "1",
188
+ // 滚动弹幕
189
+ ts
190
+ };
191
+ const sign = signBilibiliParams(baseParams, BILI_APPSECRET);
192
+ const params = { ...baseParams, sign };
193
+ const formData = new import_url.URLSearchParams();
194
+ for (const key in params) {
195
+ formData.append(key, params[key]);
196
+ }
197
+ try {
198
+ const response = await ctx.http.post(url, formData, {
199
+ headers: {
200
+ "Content-Type": "application/x-www-form-urlencoded",
201
+ "User-Agent": "Mozilla/5.0 BiliDroid/6.73.1 (bbcallen@gmail.com) os/android model/Mi 10 Pro mobi_app/android build/6731100 channel/xiaomi innerVer/6731110 osVer/12 network/2"
202
+ }
203
+ });
204
+ if (response.code === 0) {
205
+ const successMessage = attempt > 0 ? `[弹幕] [${logIdentifier}] 成功向直播间 ${roomId} 发送弹幕 (重试 ${attempt} 次后)` : `[弹幕] [${logIdentifier}] 成功向直播间 ${roomId} 发送弹幕: "${message}"`;
206
+ ctx.logger.info(successMessage);
207
+ return;
195
208
  }
196
- });
197
- if (response.code === 0) {
198
- ctx.logger.info(`[弹幕] 成功向直播间 ${roomId} 发送弹幕: "${message}"`);
199
- } else {
200
- ctx.logger.warn(`[弹幕] 发送失败,直播间 ${roomId}。原因: ${response.message || "未知错误"}`);
209
+ if (response.message?.includes(FREQUENCY_LIMIT_KEYWORD)) {
210
+ if (attempt < MAX_RETRIES) {
211
+ ctx.logger.warn(`[弹幕] [${logIdentifier}] 发送频率过快 (尝试 ${attempt + 1}/${MAX_RETRIES + 1})。准备重试...`);
212
+ continue;
213
+ } else {
214
+ ctx.logger.warn(`[弹幕] [${logIdentifier}] 发送频率过快,已达最大重试次数 (${MAX_RETRIES}),放弃发送。`);
215
+ return;
216
+ }
217
+ }
218
+ ctx.logger.warn(`[弹幕] [${logIdentifier}] 发送失败,直播间 ${roomId}。原因: ${response.message || "未知错误"}`);
219
+ return;
220
+ } catch (error) {
221
+ ctx.logger.error(`[弹幕] [${logIdentifier}] 发送请求时发生网络错误 (尝试 ${attempt + 1}),直播间 ${roomId}:`, error);
222
+ return;
201
223
  }
202
- } catch (error) {
203
- ctx.logger.error(`[弹幕] 发送请求时发生网络错误,直播间 ${roomId}:`, error);
204
224
  }
205
225
  }
206
226
  __name(sendBilibiliDanmaku, "sendBilibiliDanmaku");
@@ -228,6 +248,10 @@ function apply(ctx, config) {
228
248
  const strippedContent = session.stripped.content;
229
249
  if (!strippedContent.trim()) return;
230
250
  if (HARD_REJECTION_KEYWORDS.some((keyword) => strippedContent.includes(keyword))) return;
251
+ if (CHECK_IN_REJECTION_REGEX.test(strippedContent)) {
252
+ ctx.logger.info(`[忽略] 消息包含签到模式 (如 110+),判定为非奖励信息。内容: "${strippedContent.replace(/\n/g, " ")}"`);
253
+ return;
254
+ }
231
255
  if (!TRIGGER_REGEX.test(strippedContent)) return;
232
256
  const roomIds = extractAllRoomIds(session.content);
233
257
  if (roomIds.length !== 1) {
@@ -243,25 +267,21 @@ function apply(ctx, config) {
243
267
  const hasStrongContext = /神金|发|w/i.test(preprocessedMessage);
244
268
  const hasTime = parsedEvent.dateTime !== "时间未知";
245
269
  if (!hasStrongContext && !hasTime) return;
246
- const { dateTime } = parsedEvent;
247
- if (forwardedHistory.some((entry) => entry.roomId === roomId && entry.dateTime === dateTime)) {
248
- ctx.logger.info(`[防复读] 检测到重复活动: 房间=${roomId}, 时间=${dateTime}`);
249
- if (groupConfig.sendHelperMessages) {
250
- try {
251
- const [warningId] = await session.send(`看到啦看到啦,不要发那么多次嘛~`);
252
- if (warningId) warningMessageMap.set(session.messageId, warningId);
253
- } catch (e) {
254
- ctx.logger.warn("[消息] 发送重复警告失败:", e);
255
- }
256
- }
257
- return;
258
- }
259
270
  const biliInfo = await fetchBilibiliInfo(ctx, roomId);
260
271
  if (!biliInfo) return;
261
272
  let helperMessageId;
262
273
  if (groupConfig.sendHelperMessages) {
263
- [helperMessageId] = await session.send(`直播间: ${roomId}
274
+ try {
275
+ [helperMessageId] = await session.send(`直播间: ${roomId}
264
276
  投稿数: ${biliInfo.videoCount}`);
277
+ } catch (e) {
278
+ ctx.logger.warn("[消息] 发送辅助信息失败:", e);
279
+ }
280
+ }
281
+ const { dateTime } = parsedEvent;
282
+ if (forwardedHistory.some((entry) => entry.roomId === roomId && entry.dateTime === dateTime)) {
283
+ ctx.logger.info(`[防复读] 检测到重复活动,已发送辅助信息,跳过转发: 房间=${roomId}, 时间=${dateTime}`);
284
+ return;
265
285
  }
266
286
  try {
267
287
  const forwardMessage = `${session.content}
@@ -273,12 +293,18 @@ function apply(ctx, config) {
273
293
  originalMessageId: session.messageId,
274
294
  forwardedMessageId,
275
295
  helperMessageId,
276
- // helperMessageId 可能是 undefined
296
+ // 存储辅助消息ID用于撤回联动
277
297
  roomId,
278
298
  dateTime
279
299
  });
280
300
  if (forwardedHistory.length > config.historySize) forwardedHistory.shift();
281
- await sendBilibiliDanmaku(ctx, config, roomId, "喵喵喵");
301
+ if (config.biliAccessKeys && config.biliAccessKeys.length > 0) {
302
+ ctx.logger.info(`[弹幕] 准备为 ${config.biliAccessKeys.length} 个账号发送弹幕到直播间 ${roomId}...`);
303
+ const danmakuPromises = config.biliAccessKeys.map(
304
+ (keyConfig) => sendBilibiliDanmaku(ctx, keyConfig, roomId, "喵喵喵")
305
+ );
306
+ Promise.allSettled(danmakuPromises);
307
+ }
282
308
  } catch (error) {
283
309
  session.send("🐱 - 转发失败,请检查目标QQ/群号配置是否正确");
284
310
  ctx.logger.error("[转发] 失败:", error);
@@ -299,7 +325,8 @@ function apply(ctx, config) {
299
325
  }
300
326
  }
301
327
  try {
302
- await session.bot.deleteMessage(config.targetQQ, entry.forwardedMessageId);
328
+ const targetChannel = config.isGroup ? config.targetQQ : `private:${config.targetQQ}`;
329
+ await session.bot.deleteMessage(targetChannel, entry.forwardedMessageId);
303
330
  } catch (e) {
304
331
  ctx.logger.warn(`[撤回] 转发消息 (ID: ${entry.forwardedMessageId}) 失败:`, e);
305
332
  } finally {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-cat-raising",
3
3
  "description": "",
4
- "version": "1.0.1",
4
+ "version": "1.1.0",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [