Files
msgbot/auto_reply.py
T
youpaishidifu 8894124b0a fix(kb): section级搜索+意图排序+分词级匹配修复短查询与跨section排序
1. knowledge_base.py:
   - 文档按 ## 标题切分为独立 section 后索引,每个 section 独立 TF-IDF
   - 分词级匹配(避免"定义"子串匹配"自定义"的假阳性)
   - 文档级优先排序:先按文档聚合,取最高 section 分决定最佳文档
   - 定义类查询(版号/版本号/定义):自动降权无定义类 section 的文档
   - 动作类查询(怎么/如何/用法):按优先级找"参数/指令/使用"→"方法"→"安装" heading
   - 预计算 heading/body 分词集合,避免每次搜索重复 jieba
   - 修复 SYNONYMS 重复 key

2. auto_reply.py:
   - 精简答案提取:section 级搜索后 doc['content'] 即最相关片段
   - 移除冗余的 _extract_relevant_snippet(重复 jieba 分词+section 切分)

3. monitor.py:
   - 增加微信/QQ UI 噪声过滤(通讯录、发现、朋友圈等)
2026-07-27 14:17:51 +08:00

99 lines
3.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""自动回复 — 消息匹配知识库后准备答案,由主线程发送"""
import logging
import threading
logger = logging.getLogger("msgbot.autoreply")
class AutoReply:
"""自动回复控制器"""
def __init__(self, monitor):
self.monitor = monitor
self._enabled = False
self._min_score = 0.30
# 已回复的消息指纹(防重复)
self._replied: dict = {}
self._lock = threading.Lock()
def set_enabled(self, enabled: bool):
self._enabled = enabled
logger.info(f"自动回复 {'已开启' if enabled else '已关闭'}")
def set_min_score(self, score: float):
self._min_score = max(0.0, min(1.0, score))
@property
def enabled(self) -> bool:
return self._enabled
@property
def min_score(self) -> float:
return self._min_score
def on_kb_match(self, platform: str, msg: str, top_doc: dict, score: float) -> str or None:
"""知识库匹配命中时调用。线程安全。
返回要发送的回答文本,返回 None 表示不发送。
"""
if not self._enabled:
return None
if score < self._min_score:
logger.debug(
f"[{platform}] 匹配度 {score:.2f} < 阈值 {self._min_score},跳过回复"
)
return None
with self._lock:
return self._get_answer(platform, msg, top_doc, score)
def get_reply_data(self, platform: str, msg: str, top_doc: dict, score: float) -> dict or None:
"""返回 {platform, answer, sender} 供主线程发送。"""
answer = self.on_kb_match(platform, msg, top_doc, score)
if not answer:
return None
# 获取发送者(在同一线程提取,不涉及窗口操作)
raw = self.monitor.get_raw_clipboard(platform)
sender = self.monitor._parse_last_sender(raw)
# 只有发送者是有效姓名且不在消息内容中时才切会话
if sender and sender not in msg and score >= self._min_score + 0.05:
pass
else:
sender = None
return {"platform": platform, "answer": answer, "sender": sender}
def _get_answer(self, platform: str, msg: str, doc: dict, score: float) -> str or None:
"""提取答案 + 防重复检查。
优先返回 doc['answer'](Q&A 段),兜底使用 doc['content']。
KB 搜索已按 section 切分,doc['content'] 即最相关的片段。
只需截断到 300 字符避免消息过长。
"""
answer = doc.get("answer")
if answer:
answer = answer.strip()
else:
content = doc.get("content", "").strip()
answer = content[:297] + "..." if len(content) > 300 else content
if not answer:
return None
# 防重复指纹
fp = hash(f"{platform}:{msg}:{answer[:80]}")
replied_set = self._replied.setdefault(platform, set())
if fp in replied_set:
logger.debug(f"[{platform}] 重复命中,跳过: {msg[:40]}")
return None
replied_set.add(fp)
# 控制缓存上限
if len(replied_set) > 200:
self._replied[platform] = set(list(replied_set)[-100:])
logger.info(
f"[{platform}] 🎯 自动回复触发 | 匹配度 {score:.2f} | "
f"消息: {msg[:40]}"
)
return answer