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:
+54
-23
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user