From 81839bbe912830ab37ce104b257157ffab9cc60a Mon Sep 17 00:00:00 2001 From: youpaishidifu Date: Tue, 15 Sep 2026 21:45:18 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D[=E7=9F=A5=E8=AF=86=E5=BA=93?= =?UTF-8?q?=E6=90=9C=E7=B4=A2]=EF=BC=9A=E6=8E=92=E9=99=A4=E9=93=BE?= =?UTF-8?q?=E6=8E=A5=E6=96=87=E6=9C=AC=E5=B9=B2=E6=89=B0=EF=BC=8C=E9=98=B2?= =?UTF-8?q?=E6=AD=A2php.md=E8=AF=AF=E5=8C=B9=E9=85=8Dsdf1=E6=8F=92?= =?UTF-8?q?=E4=BB=B6=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Task #1720: 修复兑换码插件怎么用查询返回php.md而非sdf1.md的问题 - 根因:php.md的链接文本[sdf1插件](sdf1.md)被误判为内容关键词 - 修复:auto_reply.py中排除链接文本进行核心词匹配 - 修复:使用body_no_links替代body_clean进行关键词匹配 - 修复:动作类查询要求标题和正文都包含核心词 - Task #1719: 修复旧消息被环形缓冲区淘汰后重新回复 - 修复:record_user_message()增加跨缓冲区查重 - 修复:第二关增加is_user_message_replied()检查 --- auto_reply.py | 21 ++++++++++++++++----- monitor.py | 43 +++++++++++++++++++++++++++++++++++-------- 2 files changed, 51 insertions(+), 13 deletions(-) diff --git a/auto_reply.py b/auto_reply.py index f2d1cf6..24d2b40 100644 --- a/auto_reply.py +++ b/auto_reply.py @@ -196,9 +196,15 @@ class AutoReply: # ⭐ 移除代码块内容:避免配置示例/问答数据等代码块污染相关性检查 # 修复"用户组是怎么配置的"被sdf1_game"题目配置格式"(body含代码块)误匹配的问题 body_clean = re.sub(r'```.*?```', '', body_clean, flags=re.DOTALL) + # ⭐ 移除整个链接(包括链接文本),用于核心词匹配 + # 防止php.md的"[sdf1插件](sdf1.md)"链接文本被误判为内容关键词 + # 链接文本是引用,不是真正的内容,不应参与核心词匹配 + body_no_links = _link.sub('', body).lower() + body_no_links = re.sub(r'```.*?```', '', body_no_links, flags=re.DOTALL) ht_tokens = AutoReply._meaningful_tokens(heading_clean + " " + title) - body_tokens = AutoReply._meaningful_tokens(body_clean[:800]) + # ⭐ 使用body_no_links计算body_tokens,排除链接文本干扰 + body_tokens = AutoReply._meaningful_tokens(body_no_links[:800]) has_ht = bool(q_mean & ht_tokens) has_body = bool(q_mean & body_tokens) @@ -240,7 +246,8 @@ class AutoReply: # 规则:heading主体命中(has_ht) + body是表格(|分隔) + 查询动作词在表格中出现 → 放行 _is_table_body = ('|' in (body or '')) or ('-|-' in (body or '')) # 注意:不能用 set(body_clean)——字符集不包含多字符词,须用子串判断 - _action_hits_body = any(kw in body_clean[:800] for kw in (q_mean & action_kw)) + # ⭐ 使用body_no_links进行动作词匹配,排除链接文本干扰 + _action_hits_body = any(kw in body_no_links[:800] for kw in (q_mean & action_kw)) if has_ht and _is_table_body and _action_hits_body: logger.debug( f"动作查询表格兜底放行: query={query[:40]!r} " @@ -267,7 +274,8 @@ class AutoReply: has_body = has_body_nonaction # ⭐ 后台/前台关键词区分:查询含"后台"但文档不含"后台"→ 不相关 - full = heading_clean + " " + title.lower() + " " + body_clean[:800] + # ⭐ 使用body_no_links进行关键词匹配,排除链接文本干扰 + full = heading_clean + " " + title.lower() + " " + body_no_links[:800] # 注意:"管理后台"是整体概念,拆分后"后台"可能不在heading中,但"管理员"+"后端"已表达管理意图 # 所以当heading含"管理员"或"后端"时,不应因缺"后台"而拒绝 if "后台" in query and "后台" not in full: @@ -367,7 +375,9 @@ class AutoReply: # ⭐ OCR碎片过滤:纯ASCII且≤2字符的词(如"丿IJ"切成'ij')是OCR噪声 # 不参与核心词覆盖判定——否则垃圾词抬高分母,真核心词反而达不到阈值 core_kw = {kw for kw in core_kw if not (kw.isascii() and len(kw) <= 2)} - doc_text = heading_clean + " " + title.lower() + " " + body_clean[:800] + # ⭐ 使用body_no_links进行核心词匹配,排除链接文本干扰 + # 防止php.md的"[sdf1插件](sdf1.md)"链接文本被误判为内容关键词 + doc_text = heading_clean + " " + title.lower() + " " + body_no_links[:800] core_hits = sum(1 for kw in core_kw if kw in doc_text) # ⭐ 下载意图检查提前到核心词检查之前 @@ -523,7 +533,8 @@ class AutoReply: return True # 子串回退:jieba 可能产生碎片 token,导致交集为空但文档确实含查询短语 - full = heading_clean + " " + title.lower() + " " + body_clean[:800] + # ⭐ 使用body_no_links进行子串回退检查,排除链接文本干扰 + full = heading_clean + " " + title.lower() + " " + body_no_links[:800] stripped = re.sub(r'[??!!。,,\s]', '', query).lower() if len(stripped) >= 4: # ⭐ 修复:子串回退必须同时满足两个条件: diff --git a/monitor.py b/monitor.py index 5b4fc17..c1dae30 100644 --- a/monitor.py +++ b/monitor.py @@ -728,6 +728,14 @@ class MessageHistory: logger.info(f"[{platform}] 消息去重: 与历史消息相似,跳过入库: {content[:60]!r}") return # 重复消息,不入库 + # ⭐ Task #1719: 检查bot_messages表,防止环形缓冲区淘汰后重新入库 + # 根因:user_messages环形缓冲区仅保留10条/平台,旧消息被淘汰后 + # _is_similar_to_history() 找不到历史记录,导致已回复消息重新入库。 + # 修复:通过bot_messages表(无环形缓冲区限制)检查该消息是否曾被回复过。 + if self.is_user_message_replied(platform, content): + logger.info(f"[{platform}] 消息去重: bot已回复过(跨缓冲区),跳过入库: {content[:60]!r}") + return # 已回复消息,不入库 + content_hash = hashlib.md5(content.strip().encode('utf-8')).hexdigest() # ⭐ 防止重复入库:检查是否已存在完全相同的消息 @@ -10881,15 +10889,19 @@ class MessageMonitor: logger.debug(f"[{platform}] OCR过滤: bot回复碎片(lm={lm},ratio={lm/len(t_norm):.0%}) ts={ts!r}") is_fragment = True break - # ⭐ 策略2:字符级重叠率≥60%且至少有3个中文字符重叠(调整阈值) + # ⭐ 策略2:字符级重叠率检测(调整阈值,防短消息误判) # 计算t_norm中有多少字符出现在r_norm中 r_char_set = set(r_norm) overlap = sum(1 for c in t_norm if c in r_char_set) overlap_ratio = overlap / len(t_norm) if t_norm else 0 - # 如果重叠率≥60%且至少有3个中文字符重叠 → 可能是碎片 cjk_overlap = sum(1 for c in t_norm if '\u4e00' <= c <= '\u9fff' and c in r_char_set) - if overlap_ratio >= 0.6 and cjk_overlap >= 3: - logger.debug(f"[{platform}] OCR过滤: bot回复碎片(char_overlap={overlap}/{len(t_norm)},cjk={cjk_overlap}) ts={ts!r}") + # ⭐ Task #1718:短消息(<10字符)提高阈值,防止域相关短查询被误判 + # 例:"云背包的可用参数"(8字)与bot回复共享"云背包/参数/可用"等分散字符→62.5%命中 + # 短消息要求≥75%重叠+≥4中文字符;长消息保持≥60%+≥3 + _min_ratio = 0.75 if len(t_norm) < 10 else 0.6 + _min_cjk = 4 if len(t_norm) < 10 else 3 + if overlap_ratio >= _min_ratio and cjk_overlap >= _min_cjk: + logger.debug(f"[{platform}] OCR过滤: bot回复碎片(char_overlap={overlap}/{len(t_norm)},cjk={cjk_overlap},ratio={overlap_ratio:.0%},threshold={_min_ratio:.0%}) ts={ts!r}") is_fragment = True break # ⭐ 策略3:完全子串匹配(文本完全包含在已回复文本中) @@ -11137,10 +11149,25 @@ class MessageMonitor: score = -100.0 logger.info(f"[{platform}] 第二关拦截:DB记录时间差{time_diff_seconds:.0f}秒>300秒,丢弃: {item[:50]}") else: - # ocr_staging表有记录但user_messages表无记录:放行(首次抓取) - old_score = score - score = max(score + 5.0, 5.0) - logger.info(f"[{platform}] 第二关通过:ocr_staging有记录但user_messages无记录,放行: {old_score:.1f}→{score:.1f}: {item[:50]}") + # ⭐ Task #1719: ocr_staging有记录但user_messages无记录 + # 根因:Task #1697 将 record_ocr_message() 移到评分管道之前, + # 导致旧消息(已被环形缓冲区淘汰)再次OCR时,ocr_staging 总有刚创建的记录, + # 但 user_messages 中已无该消息。 + # 修复:通过 bot_messages 表检查该消息是否曾被回复过(不受环形缓冲区限制)。 + _already_replied = False + if self._msg_history: + try: + _already_replied = self._msg_history.is_user_message_replied(platform, cleaned_item) + except Exception: + pass + if _already_replied: + score = -100.0 + logger.info(f"[{platform}] 第二关拦截:ocr_staging有记录但user_messages无记录,且bot已回复过,丢弃: {item[:50]}") + else: + # 首次抓取:ocr_staging有记录但user_messages无记录,且bot未回复过 → 放行 + old_score = score + score = max(score + 5.0, 5.0) + logger.info(f"[{platform}] 第二关通过:ocr_staging有记录但user_messages无记录(首次抓取),放行: {old_score:.1f}→{score:.1f}: {item[:50]}") else: # ocr_staging表无记录:检查user_messages表 my_record_ts = self._msg_history.get_message_record_timestamp(platform, cleaned_item)