fix: 剪贴板hash去重 + 双击ESC急停 + 截图日志完善
1. 剪贴板hash去重: _read_via_clipboard_once计算md5(content), 同平台内容未变则跳过,防止QQ读到微信遗留的过时剪贴板数据 答非所问(跨平台stale数据)问题修复 2. 双击ESC急停: _esc_check_loop daemon线程每150ms轮询ESC键, 两次ESC间隔<500ms触发_emergency_stop: 设置_running=False、 杀死EasyOCR子进程、清空发送锁、回调GUI更新状态、 用户可点击开关重新启动。急停后不退出程序。 3. 截图日志完善: - 日志打印完整路径(用户可直接定位) - 统一keep=60(QQ+微信共享清理池,双平台不互删) - 移除微信冗余二次保存(wechat_ocr_*重复save) - 修复silent except→打出save失败日志
This commit is contained in:
+86
-19
@@ -210,6 +210,13 @@ class MessageMonitor:
|
||||
self._easyocr_lock = threading.Lock()
|
||||
# QQ 独立轮询线程(不阻塞微信主循环)
|
||||
self._qq_thread: Optional[threading.Thread] = None
|
||||
# 双击 ESC 急停线程
|
||||
self._emergency_thread: Optional[threading.Thread] = None
|
||||
|
||||
# 剪贴板内容 hash(防止跨平台读到过时数据)
|
||||
self._last_clipboard_hash: dict = {} # platform → str(hash)
|
||||
# 紧急停止回调(通知 GUI 更新状态)
|
||||
self._emergency_stop_callback: Optional[Callable[[], None]] = None
|
||||
|
||||
def set_poll_interval(self, seconds: float):
|
||||
self._poll_interval = max(0.5, seconds)
|
||||
@@ -221,6 +228,9 @@ class MessageMonitor:
|
||||
def set_callback(self, cb: Callable[[str, str], None]):
|
||||
self._on_message = cb
|
||||
|
||||
def set_emergency_callback(self, cb: Callable[[], None]):
|
||||
self._emergency_stop_callback = cb
|
||||
|
||||
def start(self):
|
||||
if self._running:
|
||||
return
|
||||
@@ -231,6 +241,9 @@ class MessageMonitor:
|
||||
# QQ 独立轮询线程,不阻塞微信主循环(WeChat EasyOCR ~15-20s 每轮)
|
||||
self._qq_thread = threading.Thread(target=self._qq_poll_loop, daemon=True, name="monitor-qq")
|
||||
self._qq_thread.start()
|
||||
# 双击 ESC 急停检测线程
|
||||
self._emergency_thread = threading.Thread(target=self._esc_check_loop, daemon=True, name="monitor-esc")
|
||||
self._emergency_thread.start()
|
||||
logger.info("消息监控已启动")
|
||||
|
||||
def stop(self):
|
||||
@@ -241,6 +254,9 @@ class MessageMonitor:
|
||||
if self._qq_thread and self._qq_thread.is_alive():
|
||||
self._qq_thread.join(timeout=3)
|
||||
self._qq_thread = None
|
||||
if self._emergency_thread and self._emergency_thread.is_alive():
|
||||
self._emergency_thread.join(timeout=1)
|
||||
self._emergency_thread = None
|
||||
self._cached_hwnd.clear()
|
||||
self._cached_pid.clear()
|
||||
self._last_sig.clear()
|
||||
@@ -294,6 +310,58 @@ class MessageMonitor:
|
||||
logger.error(f"QQ轮询异常: {e}\n{traceback.format_exc()}")
|
||||
time.sleep(self._poll_interval)
|
||||
|
||||
# ── 双击 ESC 急停 ──────────────────────────────────────
|
||||
|
||||
def _esc_check_loop(self):
|
||||
"""独立 daemon 线程:GetAsyncKeyState 检测双击 ESC → 强制急停。
|
||||
|
||||
用户 bot 卡住或需强制接管时,双击 ESC 即可终止所有操作:
|
||||
- 设置 _running=False 退出所有轮询循环
|
||||
- 杀死 EasyOCR 子进程(GPU 推理可能卡死)
|
||||
- 清空发送锁(解锁被 hang 的发送操作)
|
||||
- 通知 GUI 更新状态按钮
|
||||
"""
|
||||
VK_ESC = 0x1B
|
||||
MAX_INTERVAL = 0.5 # 两次 ESC 间隔 < 500ms 算双击
|
||||
last_press = 0.0
|
||||
while True:
|
||||
try:
|
||||
if ctypes.windll.user32.GetAsyncKeyState(VK_ESC) & 0x8000:
|
||||
now = time.time()
|
||||
if last_press > 0 and (now - last_press) < MAX_INTERVAL:
|
||||
logger.warning("⚠️ 双击ESC急停触发!强制终止所有操作...")
|
||||
self._emergency_stop()
|
||||
return # 线程退出
|
||||
last_press = now
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(0.15)
|
||||
|
||||
def _emergency_stop(self):
|
||||
"""强制停止监控+杀死 EasyOCR+清空发送锁。"""
|
||||
self._running = False
|
||||
|
||||
# 释放发送锁(防止被 hang 的发送操作卡住)
|
||||
self._sending_event.clear()
|
||||
|
||||
# 杀死 EasyOCR 子进程(GPU 推理可能卡死不响应)
|
||||
try:
|
||||
if self._easyocr_proc and self._easyocr_proc.poll() is None:
|
||||
self._easyocr_proc.kill()
|
||||
self._easyocr_proc.wait(timeout=2)
|
||||
logger.warning("[急停] EasyOCR 子进程已杀死")
|
||||
except Exception as e:
|
||||
logger.debug(f"[急停] 杀 EasyOCR 异常: {e}")
|
||||
|
||||
# 通知回调(GUI 更新状态)
|
||||
try:
|
||||
if self._emergency_stop_callback:
|
||||
self._emergency_stop_callback()
|
||||
except Exception as e:
|
||||
logger.debug(f"[急停] 回调异常: {e}")
|
||||
|
||||
logger.warning("⚠️ 急停完成 — 消息监控已停止,点击开关可重新启动")
|
||||
|
||||
# ── 窗口定位 ─────────────────────────────────────────────
|
||||
|
||||
def _ensure_hwnd(self, platform: str) -> Optional[int]:
|
||||
@@ -1003,10 +1071,10 @@ class MessageMonitor:
|
||||
ts = datetime.now().strftime('%Y%m%d_%H%M%S.%f')[:19]
|
||||
_dbg_path = os.path.join(script_dir, f"ocr_debug_qq_{ts}.png")
|
||||
im.save(_dbg_path)
|
||||
logger.debug(f"[qq] 调试截图: {os.path.basename(_dbg_path)}")
|
||||
_clean_old_ocr_debug(script_dir, keep=30)
|
||||
except Exception:
|
||||
pass
|
||||
logger.debug(f"[qq] 调试截图: {_dbg_path}")
|
||||
_clean_old_ocr_debug(script_dir, keep=60)
|
||||
except Exception as e:
|
||||
logger.debug(f"[qq] 调试截图保存失败: {e}")
|
||||
|
||||
# ⭐ 颜色像素检测:如果截图中只有 bot 蓝色气泡,无白色用户消息,跳过
|
||||
if not self._has_user_colors(im, "qq"):
|
||||
@@ -1134,11 +1202,11 @@ class MessageMonitor:
|
||||
ts = datetime.now().strftime('%Y%m%d_%H%M%S.%f')[:19]
|
||||
_dbg_path = os.path.join(script_dir, f"ocr_debug_wx_{ts}.png")
|
||||
im.save(_dbg_path)
|
||||
logger.debug(f"[wechat] 调试截图: {os.path.basename(_dbg_path)}")
|
||||
# 清理 wechat 和 qq 两种前缀的调试图
|
||||
_clean_old_ocr_debug(script_dir, keep=30)
|
||||
except Exception:
|
||||
pass
|
||||
logger.debug(f"[wechat] 调试截图: {_dbg_path}")
|
||||
# 清理 ocr_debug_* 调试图(unified pool)
|
||||
_clean_old_ocr_debug(script_dir, keep=60)
|
||||
except Exception as e:
|
||||
logger.debug(f"[wechat] 调试截图保存失败: {e}")
|
||||
|
||||
# ⭐ 颜色像素检测:如果截图中只有 bot 绿色气泡,无白色用户消息,跳过
|
||||
if not self._has_user_colors(im, "wechat"):
|
||||
@@ -1160,16 +1228,6 @@ class MessageMonitor:
|
||||
self._last_ocr_time["wechat"] = now
|
||||
self._ocr_unchanged.pop("wechat", None)
|
||||
|
||||
# 保存调试截图
|
||||
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"wechat_ocr_{ts}.png")
|
||||
im.save(path)
|
||||
_clean_old_ocr_debug(script_dir, keep=20)
|
||||
except Exception as e:
|
||||
logger.debug(f"[wechat] OCR 保存截屏失败: {e}")
|
||||
|
||||
# 图像预处理(同 QQ)
|
||||
try:
|
||||
im = im.resize((im.width * 2, im.height * 2), Image.LANCZOS)
|
||||
@@ -1710,6 +1768,15 @@ class MessageMonitor:
|
||||
|
||||
logger.debug(f"[{platform}] 剪贴板读取 → {len(raw_lines)} lines")
|
||||
|
||||
# ⭐ 剪贴板 hash:内容没变化 → 跳过(防止跨平台读到过时数据)
|
||||
import hashlib
|
||||
cur_h = hashlib.md5(content.encode('utf-8') if content else b'').hexdigest()
|
||||
last_h = self._last_clipboard_hash.get(platform, '')
|
||||
if cur_h == last_h:
|
||||
logger.debug(f"[{platform}] 剪贴板内容未变,跳过 stale 数据")
|
||||
return []
|
||||
self._last_clipboard_hash[platform] = cur_h
|
||||
|
||||
# ── 过滤:只返回"像用户消息"的行 ──
|
||||
sender_pattern = re.compile(r'^[\u4e00-\u9fff·A-Za-z0-9_\-]{1,10}$')
|
||||
for line in raw_lines:
|
||||
|
||||
Reference in New Issue
Block a user