修复OCR截图BitBlt卡死DWM→整机冻结2-3分钟

根因: ImageGrab.grab() 底层用 GDI BitBlt 从 DWM 合成表面截屏。
_ensure_visible(SwitchToThisWindow) 触发 DWM 窗口激活合成后,
紧接着 BitBlt 在 DWM 繁忙时阻塞等待→DWM 死锁→桌面冻结 2-3 分钟
→DWM 重启→部分使用 GPU 的软件崩溃

修复(新增 _safe_screenshot):
- daemon 线程内 ImageGrab.grab(), join(timeout=3.0) 超时等待
- 超时后线程 is_alive→返回 None, 跳过本轮(日志: BitBlt 可能卡在 DWM)
- 不会 cancel 线程(无法取消阻塞的 C 调用), 但 daemon 特性确保:
  进程退出时自动终止, 正常运行时 GDI 调用最终完成+线程自动回收
- _ensure_visible 后 sleep 从 0.1s 增加到 0.4s, 给 DWM 完成窗口合成
  降低 BitBlt 在 DWM 繁忙时调用的概率
This commit is contained in:
youpaishidifu
2026-07-29 18:49:34 +08:00
parent ef227eb7b0
commit b1ca2f058c
+33 -4
View File
@@ -1049,6 +1049,29 @@ class MessageMonitor:
)
return (win_text, chosen) if chosen == "windows" else (easy_text, chosen)
@staticmethod
def _safe_screenshot(bbox: tuple, timeout: float = 3.0):
"""带超时的屏幕截图。ImageGrab.grab() 的 BitBlt 可能因 DWM 卡死→整机冻结 2-3min。
用线程+timeout 隔离:BitBlt 卡死只杀线程,不波及 DWM/桌面。
timeout=3.0: 3 秒截不到就跳过本轮,bot 继续下次轮询。
"""
_im = [None]
def _grab():
try:
_im[0] = ImageGrab.grab(bbox=bbox)
except Exception:
pass
import threading
t = threading.Thread(target=_grab, daemon=True)
t.start()
t.join(timeout=timeout)
if t.is_alive():
logger.warning(f"[ocr] 截图超时 ({timeout}s),BitBlt 可能卡在 DWM,跳过本轮")
return None
return _im[0]
def _ocr_dual_engine(self, im_processed: Image.Image, im_original: Image.Image, platform: str) -> tuple:
"""双引擎并发 OCR:Windows OCR + EasyOCR 同时跑,最多等 10 秒。
@@ -1168,8 +1191,9 @@ class MessageMonitor:
win_h = bottom - top
# QQ NT 窗口布局: ~0-5% 标签栏, ~5-30% 联系人/群列表, ~30-55% 会话列表, ~55-100% 消息面板
# 先确保 QQ 可见再截屏,避免 CMD 窗口遮挡→OCR 读到终端文字
# sleep 0.4s 让 DWM 完成窗口激活的合成,防止紧接的 BitBlt 卡死
self._ensure_visible(hwnd)
time.sleep(0.1)
time.sleep(0.4)
variant = self._app_variant.get("qq", "")
# TIM (Electron) 安全:不做 PostMessage 点击 + 不做 keybd_event 全局键
@@ -1212,7 +1236,9 @@ class MessageMonitor:
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))
im = self._safe_screenshot((cap_left, cap_top, cap_right, cap_bottom))
if im is None:
return []
if im.width < 50 or im.height < 50:
return []
@@ -1308,10 +1334,13 @@ class MessageMonitor:
return []
# 先确保微信可见再截屏,避免终端窗口遮挡 → OCR 读到终端文字
# sleep 0.4s 让 DWM 完成窗口激活的合成,防止紧接的 BitBlt 卡死
self._ensure_visible(hwnd)
time.sleep(0.1)
time.sleep(0.4)
im = ImageGrab.grab(bbox=(cap_left, cap_top, cap_right, cap_bottom))
im = self._safe_screenshot((cap_left, cap_top, cap_right, cap_bottom))
if im is None:
return []
if im.width < 50 or im.height < 50:
return []