修复[知识库搜索]:排除链接文本干扰,防止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:
+16
-5
@@ -196,9 +196,15 @@ class AutoReply:
|
|||||||
# ⭐ 移除代码块内容:避免配置示例/问答数据等代码块污染相关性检查
|
# ⭐ 移除代码块内容:避免配置示例/问答数据等代码块污染相关性检查
|
||||||
# 修复"用户组是怎么配置的"被sdf1_game"题目配置格式"(body含代码块)误匹配的问题
|
# 修复"用户组是怎么配置的"被sdf1_game"题目配置格式"(body含代码块)误匹配的问题
|
||||||
body_clean = re.sub(r'```.*?```', '', body_clean, flags=re.DOTALL)
|
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)
|
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_ht = bool(q_mean & ht_tokens)
|
||||||
has_body = bool(q_mean & body_tokens)
|
has_body = bool(q_mean & body_tokens)
|
||||||
|
|
||||||
@@ -240,7 +246,8 @@ class AutoReply:
|
|||||||
# 规则:heading主体命中(has_ht) + body是表格(|分隔) + 查询动作词在表格中出现 → 放行
|
# 规则:heading主体命中(has_ht) + body是表格(|分隔) + 查询动作词在表格中出现 → 放行
|
||||||
_is_table_body = ('|' in (body or '')) or ('-|-' in (body or ''))
|
_is_table_body = ('|' in (body or '')) or ('-|-' in (body or ''))
|
||||||
# 注意:不能用 set(body_clean)——字符集不包含多字符词,须用子串判断
|
# 注意:不能用 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:
|
if has_ht and _is_table_body and _action_hits_body:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"动作查询表格兜底放行: query={query[:40]!r} "
|
f"动作查询表格兜底放行: query={query[:40]!r} "
|
||||||
@@ -267,7 +274,8 @@ class AutoReply:
|
|||||||
has_body = has_body_nonaction
|
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中,但"管理员"+"后端"已表达管理意图
|
||||||
# 所以当heading含"管理员"或"后端"时,不应因缺"后台"而拒绝
|
# 所以当heading含"管理员"或"后端"时,不应因缺"后台"而拒绝
|
||||||
if "后台" in query and "后台" not in full:
|
if "后台" in query and "后台" not in full:
|
||||||
@@ -367,7 +375,9 @@ class AutoReply:
|
|||||||
# ⭐ OCR碎片过滤:纯ASCII且≤2字符的词(如"丿IJ"切成'ij')是OCR噪声
|
# ⭐ OCR碎片过滤:纯ASCII且≤2字符的词(如"丿IJ"切成'ij')是OCR噪声
|
||||||
# 不参与核心词覆盖判定——否则垃圾词抬高分母,真核心词反而达不到阈值
|
# 不参与核心词覆盖判定——否则垃圾词抬高分母,真核心词反而达不到阈值
|
||||||
core_kw = {kw for kw in core_kw if not (kw.isascii() and len(kw) <= 2)}
|
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)
|
core_hits = sum(1 for kw in core_kw if kw in doc_text)
|
||||||
|
|
||||||
# ⭐ 下载意图检查提前到核心词检查之前
|
# ⭐ 下载意图检查提前到核心词检查之前
|
||||||
@@ -523,7 +533,8 @@ class AutoReply:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
# 子串回退:jieba 可能产生碎片 token,导致交集为空但文档确实含查询短语
|
# 子串回退: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()
|
stripped = re.sub(r'[??!!。,,\s]', '', query).lower()
|
||||||
if len(stripped) >= 4:
|
if len(stripped) >= 4:
|
||||||
# ⭐ 修复:子串回退必须同时满足两个条件:
|
# ⭐ 修复:子串回退必须同时满足两个条件:
|
||||||
|
|||||||
+35
-8
@@ -728,6 +728,14 @@ class MessageHistory:
|
|||||||
logger.info(f"[{platform}] 消息去重: 与历史消息相似,跳过入库: {content[:60]!r}")
|
logger.info(f"[{platform}] 消息去重: 与历史消息相似,跳过入库: {content[:60]!r}")
|
||||||
return # 重复消息,不入库
|
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()
|
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}")
|
logger.debug(f"[{platform}] OCR过滤: bot回复碎片(lm={lm},ratio={lm/len(t_norm):.0%}) ts={ts!r}")
|
||||||
is_fragment = True
|
is_fragment = True
|
||||||
break
|
break
|
||||||
# ⭐ 策略2:字符级重叠率≥60%且至少有3个中文字符重叠(调整阈值)
|
# ⭐ 策略2:字符级重叠率检测(调整阈值,防短消息误判)
|
||||||
# 计算t_norm中有多少字符出现在r_norm中
|
# 计算t_norm中有多少字符出现在r_norm中
|
||||||
r_char_set = set(r_norm)
|
r_char_set = set(r_norm)
|
||||||
overlap = sum(1 for c in t_norm if c in r_char_set)
|
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
|
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)
|
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:
|
# ⭐ Task #1718:短消息(<10字符)提高阈值,防止域相关短查询被误判
|
||||||
logger.debug(f"[{platform}] OCR过滤: bot回复碎片(char_overlap={overlap}/{len(t_norm)},cjk={cjk_overlap}) ts={ts!r}")
|
# 例:"云背包的可用参数"(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
|
is_fragment = True
|
||||||
break
|
break
|
||||||
# ⭐ 策略3:完全子串匹配(文本完全包含在已回复文本中)
|
# ⭐ 策略3:完全子串匹配(文本完全包含在已回复文本中)
|
||||||
@@ -11137,10 +11149,25 @@ class MessageMonitor:
|
|||||||
score = -100.0
|
score = -100.0
|
||||||
logger.info(f"[{platform}] 第二关拦截:DB记录时间差{time_diff_seconds:.0f}秒>300秒,丢弃: {item[:50]}")
|
logger.info(f"[{platform}] 第二关拦截:DB记录时间差{time_diff_seconds:.0f}秒>300秒,丢弃: {item[:50]}")
|
||||||
else:
|
else:
|
||||||
# ocr_staging表有记录但user_messages表无记录:放行(首次抓取)
|
# ⭐ Task #1719: ocr_staging有记录但user_messages无记录
|
||||||
old_score = score
|
# 根因:Task #1697 将 record_ocr_message() 移到评分管道之前,
|
||||||
score = max(score + 5.0, 5.0)
|
# 导致旧消息(已被环形缓冲区淘汰)再次OCR时,ocr_staging 总有刚创建的记录,
|
||||||
logger.info(f"[{platform}] 第二关通过:ocr_staging有记录但user_messages无记录,放行: {old_score:.1f}→{score:.1f}: {item[:50]}")
|
# 但 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:
|
else:
|
||||||
# ocr_staging表无记录:检查user_messages表
|
# ocr_staging表无记录:检查user_messages表
|
||||||
my_record_ts = self._msg_history.get_message_record_timestamp(platform, cleaned_item)
|
my_record_ts = self._msg_history.get_message_record_timestamp(platform, cleaned_item)
|
||||||
|
|||||||
Reference in New Issue
Block a user