修复颜色检测+自身冷却+QQ裁切+2324异常 4项:QQ cap_left 0.25→0.50(避会话列表) / _has_user_colors改左右半区策略 / 回复后同平台15s冷却 / 2324行try-except兜住SetForegroundWindow崩溃
This commit is contained in:
+73
-54
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user