"""自动回复 — 消息匹配知识库后准备答案,由主线程发送""" 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.35 # 已回复的消息指纹(防重复) 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 段),兜底用整段 content。 非 Q&A 文档限制返回内容首 200 字符(之前 500 仍过多), 避免子串匹配触发后把一整个文件塞回去。 """ answer = doc.get("answer") if answer: answer = answer.strip() else: # 非 Q&A 文档 → 只取前 200 字符 content = doc.get("content", "").strip() answer = content[:197] + "..." if len(content) > 200 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