From 308bae1fa5da001f0383cfff30b742fb20a97b1e Mon Sep 17 00:00:00 2001 From: youpaishidifu Date: Sun, 23 Aug 2026 17:59:59 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8DOCR=E8=87=AA=E8=A8=80?= =?UTF-8?q?=E8=87=AA=E8=AF=AD=E5=BE=AA=E7=8E=AF=20-=20Bot=E5=9B=9E?= =?UTF-8?q?=E5=A4=8D=E5=BD=92=E4=B8=80=E5=8C=96=E5=8E=BB=E9=87=8D=E6=8B=A6?= =?UTF-8?q?=E6=88=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因:OCR读取bot自己回复时,文本格式差异(时间戳前缀、标点全/半角、空格) 导致精确/模糊匹配全部失效,bot反复回复自己的旧回复。 修复(Task #1574): 1. 去重管道最前端新增「Bot自言自语拦截」: - 归一化OCR文本(去时间戳、去标点空格、只保留CJK) - CJK骨架完全相同 → 直接拦截 - 归一化Jaccard相似度>=0.60 → 拦截 2. _last_bot_reply_content检查: - 相似度阈值从0.9降至0.65 - CJK骨架长度要求从6降至4,覆盖短回复场景 --- monitor.py | 51 ++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/monitor.py b/monitor.py index aa59e8b..a1b517d 100644 --- a/monitor.py +++ b/monitor.py @@ -8902,6 +8902,49 @@ class MessageMonitor: if self._fuzzy_duplicate(ts, replied, threshold=0.75): logger.debug(f"[{platform}] 回复模糊跳过: {ts[:50]}") continue + # ⭐ Task #1574: Bot自言自语拦截(归一化比对) + # 根因:OCR读取bot自己的回复时,文本格式与存储的回复有差异 + # (时间戳前缀、标点全/半角、空格数量),导致上述精确/模糊匹配全部失效。 + # 修复:对OCR文本做归一化(去时间戳、去标点、去空格、CJK骨架提取), + # 与最近bot回复做归一化比对。阈值宽松(0.60)确保拦截,同时用CJK骨架 + # 做二重确认防误杀(用户真实新消息的CJK骨架不可能与bot回复完全相同)。 + if len(ts) >= 6 and self._msg_history and recent_replies: + try: + # 归一化函数:去时间戳、去标点空格、去英文 + def _norm_self_check(s): + s = re.sub(r'\d{1,2}:\d{2}(?::\d{2})?', '', s) # 去时间戳 + s = re.sub(r'[^\u4e00-\u9fff]', '', s) # 只保留CJK + return s + ts_norm = _norm_self_check(ts) + if len(ts_norm) >= 4: + # CJK骨架提取:连续中文字符序列 + sk_ts = ''.join(c for c in ts if '\u4e00' <= c <= '\u9fff') + _self_skip = False + # 与最近5分钟的bot回复做归一化比对 + for br in recent_replies: + br_norm = _norm_self_check(br) + if not br_norm or len(br_norm) < 4: + continue + # 策略1:CJK骨架完全相同(OCR把同一段中文读了两遍) + sk_br = ''.join(c for c in br if '\u4e00' <= c <= '\u9fff') + if len(sk_ts) >= 4 and sk_ts == sk_br: + logger.info(f"[{platform}] Bot自言自语拦截(骨架相同): {ts[:50]}") + _self_skip = True + break + # 策略2:归一化后Jaccard相似度>=0.6(吸收OCR标点/空格差异) + set_ts = set(ts_norm) + set_br = set(br_norm) + inter = len(set_ts & set_br) + union = len(set_ts | set_br) + if union > 0 and inter / union >= 0.60: + logger.info(f"[{platform}] Bot自言自语拦截(归一化相似): " + f"jaccard={inter/union:.2f} ts={ts[:40]}") + _self_skip = True + break + if _self_skip: + continue + except Exception: + pass # _replied_texts 子串包含:OCR 提取的 md 表格细胞值(如"依赖插件可选/必选") # 是 bot 回复表的一部分,不是用户提问。长度>5才检查避免误杀短消息。 # OCR 常把 ,转 , 等标点差异,做双向子串检查(原始+归一化标点) @@ -9143,19 +9186,21 @@ class MessageMonitor: # ⭐ 清理OCR噪声前缀(如"必选,")后再搜索知识库 cleaned_item = self._clean_ocr_prefix(item) # ⭐ 检查是否与最近bot回复内容相同(OCR噪声干扰,把bot回复当作用户消息) + # ⭐ Task #1574: 降低阈值 0.9→0.65,骨架长度 6→4,防止自言自语漏过 last_reply = self._last_bot_reply_content.get(platform, "") if last_reply and cleaned_item and len(cleaned_item) >= 4: similarity = SequenceMatcher(None, cleaned_item, last_reply).ratio() - if similarity >= 0.9: + if similarity >= 0.65: logger.info(f"[{platform}] OCR噪声检测: 与最近bot回复相似度{similarity:.2%},跳过: {cleaned_item[:60]}") seen.add(cleaned_item) self._last_new_msg_time[platform] = time.time() continue # 跳过本条消息,处理下一条 # ⭐ 2026-08-12: 中文骨架匹配(吸收 OCR 拉丁混淆 php↔4p) - # 字符级相似度跌破 90% 时(php→4p 整句 ~83%),用中文骨架兜底拦截 + # 字符级相似度跌破 65% 时,用中文骨架兜底拦截 + # ⭐ Task #1574: 放宽骨架长度 6→4,覆盖短回复场景 sk_cur = _cn_skeleton(cleaned_item) sk_rep = _cn_skeleton(last_reply) - if len(sk_cur) >= 6 and sk_rep and sk_cur == sk_rep: + if len(sk_cur) >= 4 and sk_rep and sk_cur == sk_rep: logger.info(f"[{platform}] OCR噪声检测(骨架匹配): 与最近bot回复中文骨架相同,跳过: {cleaned_item[:60]}") seen.add(cleaned_item) self._last_new_msg_time[platform] = time.time()