修复OCR回复牛头不对马嘴+残留消息+EasyOCR崩溃捕获+360误报

1. knowledge_base.py: 检索排序惩罚标题-only/短正文段,降低heading bonus,奖励正文命中,避免纯标题/免责声明段排第一
2. auto_reply.py: 动作类查询要求正文命中才放行;拒绝纯标题/免责声明答案
3. monitor.py: 调试截图改存%LOCALAPPDATA%/windowmsgbot/ocr_debug避开源码目录(360); 收紧残留消息fresh gate(600s硬门); 启动即崩溃/请求崩溃时读取easyocr_crash.log写入主日志
4. easyocr_worker.py: 加faulthandler+sys.excepthook全局捕获,崩溃traceback落盘%LOCALAPPDATA%/windowmsgbot/easyocr_crash.log
This commit is contained in:
youpaishidifu
2026-08-01 21:21:05 +08:00
parent 9ff019df00
commit 0405e42a90
4 changed files with 225 additions and 46 deletions
+31 -5
View File
@@ -80,8 +80,10 @@ class AutoReply:
"""查询词与匹配文档至少有一个共同2字+词,防偶然匹配。
短查询(≤2个2字+词)直接放行,不做严格检查。
检查顺序:heading → title → content/body。
动作/配置类查询额外要求正文含核心词,不能只命中标题。
高匹配度(≥10.0):如果heading+title+body都无共同词,直接拒绝。
"""
import re
q_tokens = set(t for t in jieba.cut(query) if len(t) >= 2)
if len(q_tokens) <= 2:
return True # 短查询不拦截
@@ -89,20 +91,35 @@ class AutoReply:
# 1. 检查 heading + title
doc_text = doc.get("heading", "") + " " + doc.get("title", "")
d_tokens = set(t for t in jieba.cut(doc_text) if len(t) >= 2)
if q_tokens & d_tokens:
return True
has_heading_hit = bool(q_tokens & d_tokens)
# 2. 检查 content/body(TF-IDF匹配的段落正文)
body = doc.get("content", "") or doc.get("body", "")
b_tokens = set()
has_body_hit = False
if body:
# 截断前500字防超长文档
b_tokens = set(t for t in jieba.cut(body[:500]) if len(t) >= 2)
if q_tokens & b_tokens:
return True
has_body_hit = bool(q_tokens & b_tokens)
# 动作/配置类查询:不能只命中标题,正文必须也含核心词
# 否则"# Sdf1系列登录插件使用说明"会因标题含"登录/插件/使用"被误放
action_keywords = {"怎么", "如何", "使用", "配置", "修改", "设置", "安装", "用法"}
if action_keywords & q_tokens:
core_q_tokens = q_tokens - {"插件", "使用", "怎么", "如何", "吗", "呢", "吧"}
if core_q_tokens and not (core_q_tokens & b_tokens):
logger.debug(
f"动作查询正文相关性不足: query={query[:40]!r} "
f"heading_hit={has_heading_hit} body_hit={has_body_hit}"
)
return False
if has_heading_hit or has_body_hit:
return True
# 3. 高匹配度但无共同词:可能是TF-IDF误匹配,拒绝
# 用户问"PVP击杀播报",KB返回"json格式说明"(匹配度9.53)→ 应拒绝
if score >= 10.0 and not (q_tokens & d_tokens) and not (q_tokens & (b_tokens if body else set())):
if score >= 10.0 and not has_heading_hit and not has_body_hit:
return False
return False
@@ -128,6 +145,15 @@ class AutoReply:
if not answer:
return None
# 拒绝把纯标题/免责声明当作答案发送
import re
if answer.startswith("# ") and len(answer) < 60:
logger.debug(f"[{platform}] 答案为纯标题,跳过: {answer[:60]!r}")
return None
if re.search(r'特别说明|免责声明|本插件.*自行修改|仅供参考|版权所有', answer):
logger.debug(f"[{platform}] 答案为免责声明/通用说明,跳过: {answer[:60]!r}")
return None
# 防重复指纹
fp = hash(f"{platform}:{msg}:{answer[:80]}")
replied_set = self._replied.setdefault(platform, set())