From 94cd3ca448e4c703a718e5f6d627956e32b73d71 Mon Sep 17 00:00:00 2001 From: youpaishidifu Date: Wed, 29 Jul 2026 00:53:36 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=A2=9C=E8=89=B2=E6=A3=80?= =?UTF-8?q?=E6=B5=8B+=E8=87=AA=E8=BA=AB=E5=86=B7=E5=8D=B4+QQ=E8=A3=81?= =?UTF-8?q?=E5=88=87+2324=E5=BC=82=E5=B8=B8=204=E9=A1=B9=EF=BC=9AQQ=20cap?= =?UTF-8?q?=5Fleft=200.25=E2=86=920.50(=E9=81=BF=E4=BC=9A=E8=AF=9D?= =?UTF-8?q?=E5=88=97=E8=A1=A8)=20/=20=5Fhas=5Fuser=5Fcolors=E6=94=B9?= =?UTF-8?q?=E5=B7=A6=E5=8F=B3=E5=8D=8A=E5=8C=BA=E7=AD=96=E7=95=A5=20/=20?= =?UTF-8?q?=E5=9B=9E=E5=A4=8D=E5=90=8E=E5=90=8C=E5=B9=B3=E5=8F=B015s?= =?UTF-8?q?=E5=86=B7=E5=8D=B4=20/=202324=E8=A1=8Ctry-except=E5=85=9C?= =?UTF-8?q?=E4=BD=8FSetForegroundWindow=E5=B4=A9=E6=BA=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- monitor.py | 127 ++++++++++++++++++++++++++++++----------------------- 1 file changed, 73 insertions(+), 54 deletions(-) diff --git a/monitor.py b/monitor.py index 380d195..61702eb 100644 --- a/monitor.py +++ b/monitor.py @@ -180,6 +180,8 @@ class MessageMonitor: # 跨平台前台冷却:某平台刚切过前台(回复后),对端平台等待此时间再截图 self._foreground_cooldown_until: dict = {} # platform → float(time.time) + # 同平台回复冷却:回复后此秒数内同平台跳过 OCR(防读到自身回复回声) + self._self_reply_cooldown_until: dict = {} # platform → float(time.time) # 本轮选中的 OCR 引擎(用于日志标记) self._last_chosen_engine: dict = {} # platform → "windows"|"easyocr" @@ -895,6 +897,9 @@ class MessageMonitor: """OCR 截屏 QQ 消息区域 -> 双引擎(Windows OCR + EasyOCR)提取文本。 包含窗口验证 + 垃圾过滤 + 连续垃圾清缓存。 """ + # 同平台冷却:回复后 15s 内跳过,防读到自身回复回声 + if time.time() < self._self_reply_cooldown_until.get("qq", 0): + return [] hwnd = self._cached_hwnd.get("qq") if not hwnd: return [] @@ -954,9 +959,9 @@ class MessageMonitor: win32gui.PostMessage(hwnd, win32con.WM_KEYUP, win32con.VK_END, 0) time.sleep(0.3) # TIM 窗口布局: ~5-30% 左栏, ~30-50% 会话列表(含文档预览), ~50-100% 消息面板 - # 退回 c9acd7e 裁剪: 25% 覆盖消息面板两侧(对方+自己消息都可见) + # cap_left=50% 从消息面板左边界开始,避开会话列表干扰(文本→颜色检测 false) # 右边界 78% 排除群成员侧栏(~80%+), md 过滤负责拦截 bot 的 md 格式回复 - cap_left = int(left + win_w * 0.25) + cap_left = int(left + win_w * 0.50) cap_top = top + max(80, int(win_h * 0.06)) # 跳过标题栏+群名头栏 cap_right = int(left + win_w * 0.78) # 跳过右栏群成员列表(~80%+) 和滚动条 cap_bottom = bottom - max(100, int(win_h * 0.10)) # 输入框上方 @@ -1053,6 +1058,9 @@ class MessageMonitor: 微信 UIA 树不暴露聊天消息内容(新版微信 UIA 架构变化),必须用 OCR。 微信自绘控件,keybd_event 无法滚动,只截屏当前可见区域。 """ + # 同平台冷却:回复后 15s 内跳过,防读到自身回复回声 + if time.time() < self._self_reply_cooldown_until.get("wechat", 0): + return [] hwnd = self._cached_hwnd.get("wechat") if not hwnd: return [] @@ -1833,69 +1841,75 @@ class MessageMonitor: @staticmethod def _has_user_colors(im, platform: str) -> bool: - """颜色像素检测:判断截图中有没有用户消息(白色底板)。 + """区域颜色检测:左/右半区独立分析,判断截图中有无用户消息。 - QQ: bot 蓝色气泡 #7FBAFF 附近,用户白色气泡 - WeChat: bot 绿色气泡 #95EC69 附近,用户白色气泡 + QQ: bot 蓝色气泡右对齐,用户白色气泡左对齐 + WeChat: bot 绿色气泡右对齐,用户白色气泡左对齐 - 规则: - - 只有 bot 色(无白色)→ 整张截图只有 bot 回复 → 跳过本轮 - - 有白色像素 → 有用户消息 → 继续处理 + 策略: + - 跳过顶部 20%(聊天头部名称/标题干扰) + - 左右半区分别采样(~15×15 网格/半区) + - 右半区有 bot 色 + 左半区几乎无"其它"像素 → 仅 bot 回复 → 跳过 + - 左半区有大量"其它"像素 → 用户内容(头像/文字/时间戳)→ 继续 - 返回 True=有用户消息,False=仅 bot 回复(应跳过) + 返回 True=有用户内容/拿不准,False=仅 bot 回复 """ w, h = im.size if w < 10 or h < 10: - return True # 太小无法判断,保守处理 - - # 网格采样(约 20×20 网格) - step_x = max(2, w // 20) - step_y = max(2, h // 20) - - bot_pixels = 0 - white_pixels = 0 - total = 0 - - pixels = im.load() - for y in range(0, h, step_y): - for x in range(0, w, step_x): - total += 1 - try: - r, g, b = pixels[x, y][:3] - except Exception: - continue - - if platform == "qq": - # QQ 蓝色气泡:偏蓝的亮色 - is_bot = (110 <= r <= 190 and 170 <= g <= 235 and 220 <= b <= 255) - elif platform == "wechat": - # 微信绿色气泡:偏绿的亮色 - is_bot = (135 <= r <= 180 and 205 <= g <= 250 and 75 <= b <= 125) - else: - is_bot = False - - # 白色/接近白色(用户消息底板 + 聊天背景) - is_white = (r > 225 and g > 225 and b > 225) - - if is_bot: - bot_pixels += 1 - elif is_white: - white_pixels += 1 - - if total == 0: return True - # 有白色像素 → 有用户消息 - if white_pixels >= 3: + # 跳过顶部 20%(群名/大标题栏) + check = im.crop((0, int(h * 0.20), w, h)) + cw, ch = check.size + if cw < 20 or ch < 20: return True - # 只有 bot 色,无白色 → 仅 bot 回复 - if bot_pixels >= 3 and white_pixels == 0: - logger = logging.getLogger("msgbot.monitor") - logger.debug(f"[{platform}] 像素检测: 仅bot色({bot_pixels}/{total}), 无用户白色消息, 跳过本轮") + # 左右半区 + left_half = check.crop((0, 0, cw // 2, ch)) + right_half = check.crop((cw // 2, 0, cw, ch)) + + step_x = max(2, cw // 15) + step_y = max(2, ch // 15) + + if platform == "qq": + def is_bot(r, g, b): + return 110 <= r <= 190 and 170 <= g <= 235 and 220 <= b <= 255 + else: + def is_bot(r, g, b): + return 135 <= r <= 180 and 205 <= g <= 250 and 75 <= b <= 125 + + def analyze(img): + p = img.load() + bot = other = white = total = 0 + for y in range(0, img.height, step_y): + for x in range(0, img.width, step_x): + total += 1 + try: + r, g, b = p[x, y][:3] + except: + continue + if r > 225 and g > 225 and b > 225: + white += 1 + elif is_bot(r, g, b): + bot += 1 + else: + other += 1 + return bot, other, white, total + + left_bot, left_other, left_white, left_total = analyze(left_half) + right_bot, right_other, right_white, right_total = analyze(right_half) + + rlogger = logging.getLogger("msgbot.monitor") + rlogger.debug(f"[{platform}] 像素: 左其它={left_other}/{left_total} bot={left_bot} 白={left_white} | " + f"右其它={right_other}/{right_total} bot={right_bot} 白={right_white}") + + # 右半区有 bot 气泡 + 左半区几乎无"其它" → 仅 bot 回复 + # "其它"阈值:QQ 5 / WeChat 5(bot 气泡内白字不会产生其它像素) + if right_bot >= 3 and left_other < 5 and left_bot == 0: + rlogger.debug(f"[{platform}] 像素检测: 仅bot回复(右bot={right_bot}), 跳过") return False - # 拿不准就保守处理 + # 拿不准保守处理 return True def _is_reply_fragment(self, text: str) -> bool: @@ -2321,7 +2335,10 @@ class MessageMonitor: time.sleep(0.3) # 再试一次 SetForegroundWindow(SendMessage 点击后可能已生效) self._attach_set_foreground(root_hwnd) - win32gui.SetForegroundWindow(root_hwnd) + try: + win32gui.SetForegroundWindow(root_hwnd) + except Exception: + pass time.sleep(0.2) # keybd_event 发送(不依赖 PostMessage——Electron 不吃 PostMessage 键盘) @@ -2386,6 +2403,8 @@ class MessageMonitor: self._replied_texts.add(no_hash) self._seen_messages.add(no_hash) self._last_reply_time[platform] = time.time() + # 同平台 15s 冷却:回复回声截到也不触发 + self._self_reply_cooldown_until[platform] = time.time() + 15.0 # 追加前30字符/后30字符变体(OCR 碎片常截取回复的开头或结尾) flat = reply_text.replace('\n', ' ') if len(flat) > 15: