fix: 急停/停止即时生效+基线吞消息永久拦截破除

问题1: 用户消息通过四层过滤器但不回复——根因基线轮吞消息后永久拦截:
- 基线轮 seen.add(item) 将历史消息永久加入 _seen_messages
- _last_sig 无条件更新 → 之后 OCR 每轮读到相同屏幕 sig 相同静默 return
- 双保险永久拦截: 用户重发相同内容永远等不到回复
修复: 基线轮 seen.discard(item) + 清空 _last_sig + 不 seen.add,
防重复改由 sig 静默跳过承担; sig 静默跳过加日志暴露(4131)

问题2: OCR 识别期间急停/手动停止不生效——主线程卡死在等待:
- _ocr_dual_engine join(timeout=10) 卡死
- _call_easyocr_safe queue.get(timeout=9) 卡死(EasyOCR子进程被杀后
  proc.poll()!=None → _read_worker 立即退出 → _q 永空 → 等满超时)
- _safe_screenshot join(timeout=3) 卡死
修复: 三处等待全部改 while self._running 轮询(50ms粒度),
急停瞬间丢弃未出结果的 OCR 折返; _safe_screenshot 去 @staticmethod

验证: py_compile OK; 隔离测试 test_baseline_fix.py 4组断言全过
(基线后 seen 不含 item、sig 清空、重发触发回复、回复后防重复)
This commit is contained in:
youpaishidifu
2026-08-02 16:44:00 +08:00
parent 19c8817893
commit 3853804ca4
+54 -23
View File
@@ -1261,21 +1261,31 @@ class MessageMonitor:
pass
_t = threading.Thread(target=_read_worker, daemon=True)
_t.start()
try:
line = _q.get(timeout=timeout)
except _QueueEmpty:
self._easyocr_consecutive_failures += 1
if self._easyocr_consecutive_failures >= self._easyocr_max_failures:
logger.debug(f"[ocr] EasyOCR 连续{self._easyocr_consecutive_failures}次超时,杀子进程重来")
try:
if self._easyocr_proc and self._easyocr_proc.poll() is None:
self._easyocr_proc.kill()
except Exception:
pass
self._easyocr_proc = None
self._easyocr_consecutive_failures = 0
# ⭐ 急停检查:轮询 _running,急停后立即放弃等待(丢弃未出结果的 OCR)
_deadline = time.time() + timeout
line = None
while self._running and time.time() < _deadline:
try:
line = _q.get(timeout=0.05)
break
except _QueueEmpty:
continue
if line is None:
if not self._running:
logger.debug("[ocr] EasyOCR 等待期间急停,丢弃未出结果的 OCR 请求")
else:
logger.debug(f"[ocr] EasyOCR 第{self._easyocr_consecutive_failures}次超时({timeout}s),保留子进程不杀")
self._easyocr_consecutive_failures += 1
if self._easyocr_consecutive_failures >= self._easyocr_max_failures:
logger.debug(f"[ocr] EasyOCR 连续{self._easyocr_consecutive_failures}次超时,杀子进程重来")
try:
if self._easyocr_proc and self._easyocr_proc.poll() is None:
self._easyocr_proc.kill()
except Exception:
pass
self._easyocr_proc = None
self._easyocr_consecutive_failures = 0
else:
logger.debug(f"[ocr] EasyOCR 第{self._easyocr_consecutive_failures}次超时({timeout}s),保留子进程不杀")
return None
else:
line = self._easyocr_proc.stdout.readline()
@@ -1373,12 +1383,12 @@ class MessageMonitor:
)
return (win_text, chosen) if chosen == "windows" else (easy_text, chosen)
@staticmethod
def _safe_screenshot(bbox: tuple, timeout: float = 3.0):
def _safe_screenshot(self, bbox: tuple, timeout: float = 3.0):
"""带超时的屏幕截图。ImageGrab.grab() 的 BitBlt 可能因 DWM 卡死→整机冻结 2-3min。
用线程+timeout 隔离:BitBlt 卡死只杀线程,不波及 DWM/桌面。
timeout=3.0: 3 秒截不到就跳过本轮,bot 继续下次轮询。
⭐ 等待期间轮询 _running:急停/停止后立即折返,不等截屏结果。
"""
_im = [None]
def _grab():
@@ -1390,9 +1400,15 @@ class MessageMonitor:
import threading
t = threading.Thread(target=_grab, daemon=True)
t.start()
t.join(timeout=timeout)
# ⭐ 急停检查:join 期间轮询 _running,急停/停止后立即折返,不等截屏结果
deadline = time.time() + timeout
while self._running and t.is_alive() and time.time() < deadline:
t.join(0.05)
if t.is_alive():
logger.warning(f"[ocr] 截图超时 ({timeout}s),BitBlt 可能卡在 DWM,跳过本轮")
if not self._running:
logger.debug("[ocr] 截图期间急停,丢弃未完成的截屏")
else:
logger.warning(f"[ocr] 截图超时 ({timeout}s),BitBlt 可能卡在 DWM,跳过本轮")
return None
return _im[0]
@@ -1459,15 +1475,22 @@ class MessageMonitor:
# ── 用 daemon 线程替代 ThreadPoolExecutor ──
# ThreadPoolExecutor.__exit__ 调 shutdown(wait=True) 会永久阻塞主线程。
# daemon 线程:join 超时后直接丢弃,线程随进程退出自动回收。
_OCR_TIMEOUT = 10.0 # 总超时秒数(单次OCR请求)
_t_win = threading.Thread(target=_win_worker, daemon=True, name="ocr-win")
_t_easy = threading.Thread(target=_easy_worker, daemon=True, name="ocr-easy")
_t_win.start()
_t_easy.start()
_t_win.join(timeout=_OCR_TIMEOUT)
_t_easy.join(timeout=_OCR_TIMEOUT)
# ⭐ 急停检查:join 期间轮询 _running,急停/停止后立即折返,不等 OCR 结果
_OCR_TIMEOUT = 10.0 # 总超时秒数(单次OCR请求)
_deadline = time.time() + _OCR_TIMEOUT
while self._running and time.time() < _deadline:
if not _t_win.is_alive() and not _t_easy.is_alive():
break
_t_win.join(0.05)
_t_easy.join(0.05)
if not self._running:
logger.debug(f"[{platform}] 双引擎 OCR 期间急停,丢弃未出结果的 OCR")
# 超时的线程还在跑?记录警告,不阻塞——daemon 线程随进程退出自动回收
if _t_win.is_alive():
@@ -4131,6 +4154,7 @@ class MessageMonitor:
if sig == self._last_sig.get(platform, ""):
stale = self._stale_count.get(platform, 0) + 1
self._stale_count[platform] = stale
logger.debug(f"[{platform}] Poll: 签名相同(#{stale})静默跳过: {sig[:60]!r}")
return
# 4. 用全局 _seen_messages 去重(跨平台、跨轮次防回环)
@@ -4431,9 +4455,16 @@ class MessageMonitor:
# ⭐ 首次成功轮询(上次无消息→本次有消息):拦截历史基线
is_baseline = self._last_new_msg_time.get(platform, 0) == 0
if is_baseline:
# 基线轮:日志标记但不触发回复,添加到 seen 防止下次再来
# 基线轮:日志标记但不触发回复。
# ⚠ 三项关键处置,防止"基线吞消息后永不回复":
# 1. 从 seen 移除 item(4383 循环已 seen.add)——否则用户重发
# 相同内容 → ts in seen 永久拦截
# 2. 清空 _last_sig ——否则下一轮 sig 相同 → 4131 静默跳过,
# 用户刚发的新消息被基线误吞后永远等不到回复
# 3. 不 seen.add ——防重复由 sig 静默跳过承担
logger.info(f"[{platform}] 基线↺(不回复): {item[:60]}")
seen.add(item)
seen.discard(item)
self._last_sig[platform] = ""
self._last_new_msg_time[platform] = time.time()
else:
self._last_new_msg_time[platform] = time.time()