问题1: QQ读不出消息 — daemon线程SetForegroundWindow频繁失败
-> _run_loop添加PeekMessage创建Windows消息队列
-> _attach_set_foreground内部异常全部捕获,不抛给调用者
问题2: QQ读旧剪贴板数据 — PostMessage键盘 Electron不响应
-> 移除前台跳过检查(降频已足够,20s一次不频繁)
-> _click_message_area PostMessage->SendMessage(Electron响应同步消息)
-> 移除_force_scroll_bottom(PostMessage对Electron无效)
问题3: WeChat消息错发给QQ — auto-reply daemon线程SetForegroundWindow失败
-> auto_reply.on_kb_match不再直接调用_paste_and_send
-> 改为get_reply_data返回{platform,answer,sender}给主线程
-> gui.py添加_reply_queue + _flush_reply_queue(主线程after轮询)
-> _execute_reply在主线程执行_search_contact和_paste_and_send
(SetForegroundWindow从主线程调用永远可靠)
93 lines
3.1 KiB
Python
93 lines
3.1 KiB
Python
"""自动回复 — 消息匹配知识库后准备答案,由主线程发送"""
|
|
|
|
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:
|
|
"""提取答案 + 防重复检查。"""
|
|
answer = doc["content"].strip()
|
|
if not answer:
|
|
return None
|
|
|
|
# 截取过长内容
|
|
if len(answer) > 500:
|
|
answer = answer[:497] + "..."
|
|
|
|
# 防重复指纹
|
|
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
|