fix: 修复OCR自言自语循环 - Bot回复归一化去重拦截
根因: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,覆盖短回复场景
This commit is contained in:
+48
-3
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user