koishi-plugin-kkk 3.3.2 → 3.3.3

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/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # 更新日志
2
2
 
3
+ ## 3.3.3
4
+
5
+ ### 同一条卡片消息不再解析两遍
6
+
7
+ - **卡片消息被重复投递时只处理第一遍**:QQ 会把同一次发送投递多遍(客户端重发、指令按钮 = 消息 + 交互事件),实测同一条 B站卡片会在几秒内进来两次 —— 于是 OCR + 搜索各跑两遍、面板也可能发两条,看起来就是「解析完了又解析一遍」。现在卡片这条链路在 OCR **之前**就去重(按「平台 + 频道 + 用户 + 卡片正文」),并且**先把图片链接的签名参数去掉再比**:同一条卡片重投时链接会重新签名,直接拿原文比会当成两条不同的消息。
8
+ - 顺带加了排查用的 debug 日志:收到卡片时会打 `收到卡片消息(messageId=…,平台=…)`,同一条卡片的两次投递 messageId 是否相同,一眼就能看出是平台重复投递还是别的插件又发了一遍。
9
+
3
10
  ## 3.3.2
4
11
 
5
12
  > 修 3.3.1 的一个**致命笔误**:播放器模块里一行 import 多写了一层 `..`,导致插件在**加载时**就失败(`Cannot find module '../../compat/runtime'`),整个插件都起不来。构建链路只做转译不解析路径,所以当时没拦住 —— 现在补了一个「每个编译产物都 require 得动」的自测(scripts/smoke-require.cjs)。
package/lib/index.js CHANGED
@@ -706,6 +706,33 @@ function registerCommands(ctx, logger, autoParse) {
706
706
  logger.debug('卡片来源不是 B站(%s),跳过卡片解析', cardPlatform || '未知');
707
707
  return next();
708
708
  }
709
+ /**
710
+ * **同一条卡片消息被投递多遍时,只处理第一遍**。
711
+ *
712
+ * 线上实测:同一条 B站卡片会在几秒内进来两次(平台重投 / 另一个中间件再发一遍),
713
+ * 于是 OCR + 搜索各跑两遍、面板也可能发两条 —— 用户看到的就是「解析完了又解析一遍」。
714
+ *
715
+ * 去重键里的卡片正文要**先去掉 URL 的签名参数**:同一条卡片重投时图片链接会被重新签名,
716
+ * 直接拿原文哈希会认为是两条不同的消息(踩过这个坑,第二次照样 OCR)。
717
+ */
718
+ const cardSignature = raw
719
+ .replace(/https?:\/\/[^\s"'<>]+/g, (url) => url.split("?")[0])
720
+ .replace(/\s+/g, " ")
721
+ .trim()
722
+ .slice(0, 2000);
723
+ /**
724
+ * 打一条「收到卡片」的日志(debug 级,排查重复投递用)。
725
+ *
726
+ * 同一条卡片连着来两次、而 **messageId 也相同** → 这条事件被重复投递/重复处理;
727
+ * messageId 不同 → 平台那边确实又发了一条。排查「到底谁重复了」就看它。
728
+ */
729
+ logger.debug('收到卡片消息(messageId=%s,平台=%s)', String(session.messageId ?? '无'), String(session.platform ?? '未知'));
730
+ const { acquireParseLock } = await Promise.resolve().then(() => __importStar(require("./karin/module/utils/ParseLock")));
731
+ const cardKey = ["card", String(session.platform ?? ""), String(session.channelId ?? ""), String(session.userId ?? ""), cardSignature].join(":");
732
+ if (!acquireParseLock(cardKey)) {
733
+ logger.debug("短时间内重复的同一条卡片消息,已忽略(不再重复 OCR/搜索): %s", cardKey.slice(0, 120));
734
+ return;
735
+ }
709
736
  const send = async (content) => {
710
737
  try {
711
738
  await session.send(content);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-kkk",
3
- "version": "3.3.2",
3
+ "version": "3.3.3",
4
4
  "description": "抖音 / B站 / 快手 / 小红书 视频解析与动态推送,带 QQ 交互面板与弹幕烧录(karin-plugin-kkk 的 Koishi 移植版)",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
package/src/index.ts CHANGED
@@ -708,6 +708,35 @@ function registerCommands (
708
708
  logger.debug('卡片来源不是 B站(%s),跳过卡片解析', cardPlatform || '未知')
709
709
  return next()
710
710
  }
711
+ /**
712
+ * **同一条卡片消息被投递多遍时,只处理第一遍**。
713
+ *
714
+ * 线上实测:同一条 B站卡片会在几秒内进来两次(平台重投 / 另一个中间件再发一遍),
715
+ * 于是 OCR + 搜索各跑两遍、面板也可能发两条 —— 用户看到的就是「解析完了又解析一遍」。
716
+ *
717
+ * 去重键里的卡片正文要**先去掉 URL 的签名参数**:同一条卡片重投时图片链接会被重新签名,
718
+ * 直接拿原文哈希会认为是两条不同的消息(踩过这个坑,第二次照样 OCR)。
719
+ */
720
+ const cardSignature = raw
721
+ .replace(/https?:\/\/[^\s"'<>]+/g, (url: string) => url.split("?")[0])
722
+ .replace(/\s+/g, " ")
723
+ .trim()
724
+ .slice(0, 2000)
725
+ /**
726
+ * 打一条「收到卡片」的日志(debug 级,排查重复投递用)。
727
+ *
728
+ * 同一条卡片连着来两次、而 **messageId 也相同** → 这条事件被重复投递/重复处理;
729
+ * messageId 不同 → 平台那边确实又发了一条。排查「到底谁重复了」就看它。
730
+ */
731
+ logger.debug('收到卡片消息(messageId=%s,平台=%s)',
732
+ String((session as any).messageId ?? '无'),
733
+ String((session as any).platform ?? '未知'))
734
+ const { acquireParseLock } = await import("./karin/module/utils/ParseLock")
735
+ const cardKey = ["card", String((session as any).platform ?? ""), String((session as any).channelId ?? ""), String((session as any).userId ?? ""), cardSignature].join(":")
736
+ if (!acquireParseLock(cardKey)) {
737
+ logger.debug("短时间内重复的同一条卡片消息,已忽略(不再重复 OCR/搜索): %s", cardKey.slice(0, 120))
738
+ return
739
+ }
711
740
  const send = async (content: any) => {
712
741
  try {
713
742
  await (session as any).send(content)