修复[知识库搜索]:排除链接文本干扰,防止php.md误匹配sdf1插件内容

- 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()检查
This commit is contained in:
youpaishidifu
2026-09-15 21:45:18 +08:00
parent 83bfbff40a
commit 81839bbe91
2 changed files with 51 additions and 13 deletions
+16 -5
View File
@@ -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:
# ⭐ 修复:子串回退必须同时满足两个条件:
+35 -8
View File
@@ -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)