修TIM剪贴板死循环:_replied_texts永久集+UIA优先+剪贴板行预过滤

This commit is contained in:
youpaishidifu
2026-07-25 18:19:16 +08:00
parent a4b6f05a80
commit 0a8c861603
+67 -17
View File
@@ -56,6 +56,8 @@ class MessageMonitor:
# 已处理消息的全局持久化集合(跨平台、跨轮次,防回环)
self._seen_messages: set = set()
# 已回复文本的永久集—永不裁切(剪贴板读到自己回复时必拦)
self._replied_texts: set = set()
# 上次回复时间戳(冷却期内跳过该平台 poll)
self._last_reply_time: dict = {} # platform → timestamp
@@ -378,7 +380,11 @@ class MessageMonitor:
return texts
def _read_via_clipboard_once(self, platform: str) -> list:
"""剪贴板法单次执行(安全键盘导航:Enter + Ctrl+End + Ctrl+A/C,无 Esc)。"""
"""剪贴板法单次执行(安全键盘导航:Enter + Ctrl+End + Ctrl+A/C,无 Esc)。
返回前把所有原始行预塞进 _seen_messages,再过滤只返回"像用户消息"的行。
这样 caller 不会把发送者名/时间/历史消息当新消息处理。
"""
hwnd = self._cached_hwnd.get(platform)
if not hwnd or not win32gui.IsWindow(hwnd):
return []
@@ -460,11 +466,42 @@ class MessageMonitor:
if wc.IsClipboardFormatAvailable(wc.CF_UNICODETEXT):
content = wc.GetClipboardData(wc.CF_UNICODETEXT)
if content:
texts = [l.strip() for l in content.strip().split('\n') if l.strip()]
raw_lines = [l.strip() for l in content.strip().split('\n') if l.strip()]
self._raw_clipboard[platform] = content
wc.CloseClipboard()
logger.debug(f"[{platform}] 剪贴板读取 → {len(texts)} lines")
logger.debug(f"[{platform}] 剪贴板读取 → {len(raw_lines)} lines")
# ── 关键:所有原始行预塞进 _seen_messages,避免 caller 当新消息处理 ──
self._seen_messages.update(raw_lines)
# 回复文本的变体也预塞
for line in raw_lines:
line2 = line.replace('\n', ' ').rstrip('.')
if line2 != line and line2:
self._seen_messages.add(line2)
# ── 过滤:只返回"像用户消息"的行 ──
sender_pattern = re.compile(r'^[\u4e00-\u9fff·A-Za-z0-9_\-]{1,10}$')
for line in raw_lines:
# 跳过:系统消息、纯时间、纯数字、无意义短行
if self._is_system_message(line):
continue
# 跳过:单行发送者名(1-10汉字/字母/数字/下划线)
if sender_pattern.match(line):
continue
# 跳过:纯时间格式(连字符/斜杠分格日期)
if re.match(r'^\d{1,2}[::\s]\d{2}', line):
continue
if re.match(r'^\d{4}[/-]\d{1,2}', line):
continue
# 跳过:纯 emoji / 纯标点(短于 6 个字符且只有符号)
clean = re.sub('[~!@#$%^&*()_+\\-=\\[\\]{};:\\\'",.<>/?·!@#¥%……&*()—+。,、;:""\'\'【】{}「」『』《》【】 \\t\\n\\r\\u2000-\\u206f\\u2190-\\u2bff\\U0001F000-\\U0001FFFF]', '', line)
if len(clean) == 0 and len(line) <= 6:
continue
# 通过全部检查 → 保留
texts.append(line)
logger.debug(f"[{platform}] 剪贴板过滤后 → {len(texts)} 条用户消息")
except Exception as e:
logger.debug(f"[{platform}] 剪贴板失败: {e}")
finally:
@@ -630,6 +667,10 @@ class MessageMonitor:
"监控已启动", "监控已暂停", "设置已保存",
"🤖", "找不到联系人", "当前会话回复",
"自动回复触发", "匹配度",
# KB 文档内容格式(机器人回复内容特征)
"答:", "问题", "回答",
# 纯系统命令反馈
"PS ", "❯ ", "$ ",
]
for p in system_patterns:
if p in text:
@@ -711,16 +752,20 @@ class MessageMonitor:
time.sleep(0.5)
logger.info(f"[{platform}] 已回复: {text[:60]}")
# 将回复文本加入全局 seen 集合,多平台去重
# 将回复文本加入 _replied_texts(永久集,永不裁切,必拦)和 seen
reply_text = text.strip()
self._replied_texts.add(reply_text)
self._seen_messages.add(reply_text)
# 添加换行符归一化变体(跨频道转发时会替换 \n → 空格)
if '\n' in reply_text:
self._replied_texts.add(reply_text.replace('\n', ' '))
self._replied_texts.add(reply_text.replace('\n', ' ').rstrip('.'))
self._seen_messages.add(reply_text.replace('\n', ' '))
self._seen_messages.add(reply_text.replace('\n', ' ').rstrip('.'))
for part in reply_text.split('\n'):
part = part.strip()
if part and len(part) > 3:
self._replied_texts.add(part)
self._seen_messages.add(part)
self._last_reply_time[platform] = time.time()
return True
@@ -743,15 +788,8 @@ class MessageMonitor:
def _read_messages(self, window, platform: str = "") -> list:
"""每轮重新定位 ListControl。"""
if platform == "qq":
# TIM/QQ NT Electron: UIA ListControl 缓存易过期(只返回老旧消息)
# 优先走剪贴板(含安全键盘导航 Enter+Ctrl+End,读最新消息)
texts = self._read_via_clipboard_once(platform)
if texts:
return texts
texts = self._read_via_clipboard(platform)
if texts:
return texts
# UIA 兜底
# TIM/QQ NT: UIA 优先(不切焦点、不全局键盘、读新消息)
# 剪贴板路径会读到全对话文本+发送者名+时间→需要大量后过滤
texts = self._read_message_list(window)
if texts:
return texts
@@ -780,6 +818,13 @@ class MessageMonitor:
return texts
except Exception:
pass
# 剪贴板兜底(含预去重+行过滤,不会回环)
texts = self._read_via_clipboard_once(platform)
if texts:
return texts
texts = self._read_via_clipboard(platform)
if texts:
return texts
# 最终兜底:win32 文本
texts = self._read_win32_texts(platform)
if texts:
@@ -1034,16 +1079,21 @@ class MessageMonitor:
# 4. 用全局 _seen_messages 去重(跨平台、跨轮次防回环)
seen = self._seen_messages
replied = self._replied_texts
new_items = []
for t in texts:
ts = t.strip()
if (ts and ts not in seen
if not ts:
continue
# _replied_texts 优先检查(永不裁切,100% 拦住自己的回复)
if ts in replied:
continue
if (ts not in seen
and not self._is_system_message(ts)):
seen.add(ts)
new_items.append(ts)
# 控制集合大小防内存泄漏
if len(seen) > 500:
self._seen_messages = set(list(seen)[-300:])
# 不再主动截断 _seen_messages—剪贴板全行已预塞,截断会杀掉历史信息引发回环
# 只有极端情况(>50万行)才清,但这个场景几乎不会出现
# 单次 poll 最多处理 3 条新消息,防止系统消息海量涌入触发回环
MAX_NEW_ITEMS = 3