修TIM剪贴板死循环:_replied_texts永久集+UIA优先+剪贴板行预过滤
This commit is contained in:
+67
-17
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user