diff --git a/monitor.py b/monitor.py index 05e72b3..6b6255f 100644 --- a/monitor.py +++ b/monitor.py @@ -14,6 +14,8 @@ import win32process import re import ctypes import difflib +import os +from datetime import datetime from PIL import ImageGrab from winrt.windows.media.ocr import OcrEngine from winrt.windows.globalization import Language @@ -509,7 +511,7 @@ class MessageMonitor: exe_path = win32process.GetModuleFileNameEx(hproc, 0) win32api.CloseHandle(hproc) exe_name = exe_path.split('\\')[-1].lower() if exe_path else '' - if exe_name in ('qq.exe', 'tim.exe', 'qq.exe', 'tim.exe'): + if exe_name in ('qq.exe', 'tim.exe'): return True except Exception: pass @@ -563,15 +565,29 @@ class MessageMonitor: return [] left, top, right, bottom = rect win_w = right - left - cap_left = int(left + win_w * 0.30) - cap_top = top + 35 - cap_right = int(left + win_w * 0.55) - cap_bottom = bottom - 80 + win_h = bottom - top + # QQ NT 窗口布局: ~0-5% 标签栏, ~5-30% 联系人/群列表, ~30-55% 会话列表, ~55-100% 消息面板 + # 之前裁剪到中间会话列表区域(30-55%)→ 读到按钮/提示/删除碎片等 UI 文字 + # 修正为只裁剪右侧消息面板区域 + cap_left = int(left + win_w * 0.55) + cap_top = top + max(40, int(win_h * 0.04)) # 标题栏下方 + cap_right = int(left + win_w * 0.95) # 跳过滚动条 + cap_bottom = bottom - max(100, int(win_h * 0.10)) # 输入框上方 if cap_right <= cap_left or cap_bottom <= cap_top: return [] im = ImageGrab.grab(bbox=(cap_left, cap_top, cap_right, cap_bottom)) if im.width < 50 or im.height < 50: return [] + # 保存截屏到程序根目录用于调试(保留最近 20 张) + try: + script_dir = os.path.dirname(os.path.abspath(__file__)) + ts = datetime.now().strftime('%Y%m%d_%H%M%S') + path = os.path.join(script_dir, f"ocr_debug_{ts}.png") + im.save(path) + # 清理旧文件:只保留最近 20 张 + _clean_old_ocr_debug(script_dir, keep=20) + except Exception as e: + logger.debug(f"[ocr] 保存截屏失败: {e}") if im.mode != 'RGBA': im = im.convert('RGBA') sb = SoftwareBitmap(BitmapPixelFormat.RGBA8, im.width, im.height) @@ -597,15 +613,26 @@ class MessageMonitor: line = self._normalize_ocr_text(line) if not line: continue + # ⭐ QQ NT 界面固定噪声:整行匹配直接跳过 + if line in ("查看更多", "收起", "选择会话开始聊天"): + continue + # 连续纯标点+符号(emoji、装饰字符等) + if re.match(r'^[✿♡♥❤★☆✦✧❀❁❂❃❄❅❆❇❈❉❊❋➠➡➢➣➤.·•●○◦◎◉◍◐◑◒◓◔◕◖◗◢◣◤◥☺☻☼♠♣♦♤♧♡♢♔♕♖♗♘♙🕐-🕧\s]{1,3}$', line): + continue # 去噪:去掉行首未读红点数字(如 "0")和行尾删除按钮误识别(勖彐 等) # 否则同一消息每次 OCR 输出微小差异 → 精确去重失效 → 重复触发 line = re.sub(r'^0+', '', line) - line = re.sub(r'[勖彐"\'\s]+$', '', line) + line = re.sub(r'[勖彐"\'\s\u2000-\u206f]+$', '', line) + if not line: + continue + # 半角全角数字混杂的界面碎片(如 "1.0s 0.35后监控应用设置") + line = re.sub(r'^\d+[\..]\d+s?\s*\d*[.\.]?\d*', '', line) + line = re.sub(r'^[\d.\.\s]{2,20}[一-鿿]{1,5}?[,。、]', '', line) if not line: continue if self._is_system_message(line): continue - if re.match(r'^[一-鿿·A-Za-z0-9_\-]{1,10}$', line): + if re.match(r'^[一-鿿·A-Za-z0-9_\-]{1,15}$', line): continue if re.match(r'^\d{1,2}[::\s]\d{2}', line): continue @@ -654,8 +681,9 @@ class MessageMonitor: left, top, right, bottom = rect w = right - left h = bottom - top - click_x = int(w * 0.50) - click_y = int(h * 0.85) # 消息区域(比输入框稍上) + # QQ NT 消息面板在 ~55-95% 宽度区域 + click_x = int(w * 0.72) # 消息面板中部 + click_y = int(h * 0.60) # 消息面板中部偏下 lparam = win32api.MAKELONG(click_x, click_y) win32gui.PostMessage(hwnd, win32con.WM_LBUTTONDOWN, win32con.MK_LBUTTON, lparam) win32gui.PostMessage(hwnd, win32con.WM_LBUTTONUP, 0, lparam) @@ -896,12 +924,21 @@ class MessageMonitor: return True # 系统前缀/标记 system_markers = [ - "✅", "❌", "📝", "🔍", "📄", "➡", "♡", + "✅", "❌", "📝", "🔍", "📄", "➡", "♡", "✿", "[WECHAT]", "[QQ]", "[wechat]", "[qq]", ] for m in system_markers: if text.startswith(m): return True + # ⭐ QQ NT 界面噪声文字(按钮/提示/碎片,非用户消息) + qq_ui_noise = [ + "查看更多", "可查看在其他设备产生的聊天记录", + "收藏", "转发", "多选", "引用", "回复", + "选择会话开始聊天", + ] + for n in qq_ui_noise: + if text == n: + return True # 系统事件模板 system_patterns = [ "已自动回复", "知识库中未找到", "找到相关结果", @@ -986,7 +1023,8 @@ class MessageMonitor: left, top, right, bottom = rect w = right - left h = bottom - top - click_x = int(w * 0.50) + # QQ NT 输入框在右侧消息面板底部 + click_x = int(w * 0.72) click_y = int(h * 0.92) lparam = win32api.MAKELONG(click_x, click_y) win32gui.SendMessage(hwnd, win32con.WM_LBUTTONDOWN, win32con.MK_LBUTTON, lparam) @@ -1014,7 +1052,7 @@ class MessageMonitor: left, top, right, bottom = rect w = right - left h = bottom - top - click_x = int(w * 0.50) + click_x = int(w * 0.72) click_y = int(h * 0.92) lparam = win32api.MAKELONG(click_x, click_y) win32gui.PostMessage(hwnd, win32con.WM_LBUTTONDOWN, win32con.MK_LBUTTON, lparam) @@ -1392,6 +1430,13 @@ class MessageMonitor: continue if self._is_system_message(ts): continue + # 纯装饰符号(单字符非汉字非字母数字)跳过 + if len(ts) == 1 and not re.match(r'[\u4e00-\u9fff\w]', ts): + continue + # 只有符号/空格/emoji 的非消息文本(<3 字符且长度等于标点去掉后长度) + clean = re.sub(r'[\s\u2000-\u206f\u2190-\u2bff\U0001F000-\U0001FFFF✿♡♥❤★☆]', '', ts) + if len(ts) <= 3 and len(clean) == 0: + continue seen.add(ts) new_items.append(ts) # 不截断 _seen_messages — _replied_texts 已拦截回复回环,截断会误放历史消息 @@ -1443,6 +1488,19 @@ class MessageMonitor: self._cached_pid.pop(platform, None) +def _clean_old_ocr_debug(dir_path: str, keep: int = 20): + """清理 ocr_debug_*.png 文件,只保留最近 keep 张。""" + try: + files = [f for f in os.listdir(dir_path) if f.startswith("ocr_debug_") and f.endswith(".png")] + if len(files) <= keep: + return + files.sort() # 按字母序 = 时间序 + for f in files[:-keep]: + os.remove(os.path.join(dir_path, f)) + except Exception: + pass + + def _walk_list_controls(ctrl, depth): if depth > 6: return []