修复两个问题:
Task #1647: 陈旧消息过滤策略重构 - 超过1小时的消息返回-10.0分(扣10分) - 消息分数≤0时丢弃该消息 - 验证并提交 Task #1648: 异步非阻塞OCR处理 - 添加后台OCR状态变量和线程管理 - 实现_start_ocr_bg方法启动后台OCR - 实现_check_ocr_bg_result非阻塞检查结果 - 修改_poll方法使用异步OCR避免GUI阻塞 - 添加超时处理和急停清理 - 所有测试通过
This commit is contained in:
+167
-26
@@ -902,6 +902,16 @@ class MessageMonitor:
|
||||
self._stop_event = threading.Event()
|
||||
# ⭐ EasyOCR 预热线程代际计数器:防止死循环中旧预热线程在新 start() 后醒来错误初始化 EasyOCR
|
||||
self._easyocr_warmup_generation = 0
|
||||
|
||||
# ⭐ Task #1648: 异步OCR状态变量
|
||||
# 后台OCR线程:platform → Thread(用于非阻塞OCR)
|
||||
self._ocr_bg_thread: dict = {}
|
||||
# 后台OCR结果:platform → list|None(None表示OCR正在进行,list表示结果已就绪)
|
||||
self._ocr_bg_results: dict = {}
|
||||
# 后台OCR请求时间:platform → float(用于超时检测)
|
||||
self._ocr_bg_start_time: dict = {}
|
||||
# 后台OCR超时时间(秒)
|
||||
self._ocr_bg_timeout: float = 60.0
|
||||
|
||||
def _load_replied_texts_from_db(self):
|
||||
"""从消息历史数据库加载最近的bot回复到 _replied_texts,
|
||||
@@ -7132,10 +7142,11 @@ class MessageMonitor:
|
||||
- 10分钟内(600秒):+4分
|
||||
- 30分钟内(1800秒):+3分
|
||||
- 1小时内(3600秒):+2分
|
||||
- 2小时内(7200秒):+1分
|
||||
- 超过2小时或无时间戳:+0分
|
||||
- 超过1小时:-10分(扣分,陈旧消息惩罚)
|
||||
|
||||
所有有时间戳的消息都至少+1分(2小时内),确保时间戳新鲜度作为加分项。
|
||||
⭐ Task #1647: 陈旧消息过滤策略重构
|
||||
- 用户要求:超过1小时的消息每条扣10分
|
||||
- 选择消息时分数必须>0才能处理
|
||||
"""
|
||||
# 从文本中提取时间戳
|
||||
timestamps = re.findall(r'\d{1,2}:\d{1,2}(?::\d{1,2})?', text)
|
||||
@@ -7158,10 +7169,9 @@ class MessageMonitor:
|
||||
return 3.0
|
||||
elif age_seconds <= 3600: # 1小时内
|
||||
return 2.0
|
||||
elif age_seconds <= 7200: # 2小时内
|
||||
return 1.0
|
||||
else:
|
||||
return 0.0
|
||||
# 超过1小时:扣10分
|
||||
return -10.0
|
||||
|
||||
@staticmethod
|
||||
def _is_system_message(text: str) -> bool:
|
||||
@@ -9325,11 +9335,117 @@ class MessageMonitor:
|
||||
|
||||
return [t for t, _, _, _ in items]
|
||||
|
||||
# ── 异步OCR后台任务 ────────────────────────────────────────
|
||||
|
||||
def _start_ocr_bg(self, platform: str):
|
||||
"""启动后台OCR任务,避免阻塞GUI。
|
||||
|
||||
任务在守护线程中运行,结果存储在 _ocr_bg_results 中。
|
||||
_poll 方法会检查结果是否就绪,非阻塞获取。
|
||||
"""
|
||||
# 检查是否已有后台OCR任务在运行
|
||||
if self._ocr_bg_thread.get(platform) and self._ocr_bg_thread[platform].is_alive():
|
||||
logger.debug(f"[{platform}] 后台OCR任务已在运行,跳过启动")
|
||||
return
|
||||
|
||||
# 检查是否已有结果待处理
|
||||
if self._ocr_bg_results.get(platform) is not None:
|
||||
logger.debug(f"[{platform}] 后台OCR结果待处理,跳过启动")
|
||||
return
|
||||
|
||||
def _bg_ocr_worker():
|
||||
"""后台OCR工作线程"""
|
||||
try:
|
||||
logger.debug(f"[{platform}] 后台OCR任务开始")
|
||||
self._ocr_bg_start_time[platform] = time.time()
|
||||
self._ocr_bg_results[platform] = None # 标记OCR正在进行
|
||||
|
||||
# 执行OCR读取
|
||||
if platform == "wechat":
|
||||
texts = self._ocr_read_wechat()
|
||||
else: # qq
|
||||
texts = self._ocr_read_qq()
|
||||
|
||||
# 存储结果(空列表也是有效结果)
|
||||
self._ocr_bg_results[platform] = texts if texts else []
|
||||
logger.debug(f"[{platform}] 后台OCR任务完成,获取到 {len(texts)} 条消息")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[{platform}] 后台OCR任务异常: {e}")
|
||||
self._ocr_bg_results[platform] = [] # 异常时返回空列表
|
||||
finally:
|
||||
# 清理线程引用
|
||||
if platform in self._ocr_bg_thread:
|
||||
del self._ocr_bg_thread[platform]
|
||||
|
||||
# 启动守护线程
|
||||
thread = threading.Thread(
|
||||
target=_bg_ocr_worker,
|
||||
daemon=True,
|
||||
name=f"ocr-bg-{platform}"
|
||||
)
|
||||
self._ocr_bg_thread[platform] = thread
|
||||
thread.start()
|
||||
logger.debug(f"[{platform}] 后台OCR任务已启动")
|
||||
|
||||
def _check_ocr_bg_result(self, platform: str) -> list:
|
||||
"""非阻塞检查后台OCR结果。
|
||||
|
||||
返回:
|
||||
- 如果结果就绪:返回结果列表
|
||||
- 如果OCR仍在进行:返回 None
|
||||
- 如果超时:返回空列表并清理
|
||||
"""
|
||||
# 检查是否有结果
|
||||
result = self._ocr_bg_results.get(platform)
|
||||
if result is not None:
|
||||
# 结果已就绪,清理并返回
|
||||
del self._ocr_bg_results[platform]
|
||||
if platform in self._ocr_bg_start_time:
|
||||
del self._ocr_bg_start_time[platform]
|
||||
return result
|
||||
|
||||
# 检查是否超时
|
||||
start_time = self._ocr_bg_start_time.get(platform, 0)
|
||||
if start_time and (time.time() - start_time) > self._ocr_bg_timeout:
|
||||
logger.warning(f"[{platform}] 后台OCR任务超时({self._ocr_bg_timeout}s)")
|
||||
# 清理超时任务
|
||||
if platform in self._ocr_bg_results:
|
||||
del self._ocr_bg_results[platform]
|
||||
if platform in self._ocr_bg_start_time:
|
||||
del self._ocr_bg_start_time[platform]
|
||||
# 尝试终止线程(如果可能)
|
||||
thread = self._ocr_bg_thread.get(platform)
|
||||
if thread and thread.is_alive():
|
||||
logger.warning(f"[{platform}] 尝试终止超时OCR线程")
|
||||
# 无法强制终止线程,但可以设置停止标志
|
||||
# 线程会在下次检查 _running 时退出
|
||||
return [] # 超时返回空列表
|
||||
|
||||
# OCR仍在进行
|
||||
return None
|
||||
|
||||
def _stop_ocr_bg(self, platform: str):
|
||||
"""停止后台OCR任务(急停时调用)"""
|
||||
# 清理状态
|
||||
if platform in self._ocr_bg_results:
|
||||
del self._ocr_bg_results[platform]
|
||||
if platform in self._ocr_bg_start_time:
|
||||
del self._ocr_bg_start_time[platform]
|
||||
thread = self._ocr_bg_thread.get(platform)
|
||||
if thread and thread.is_alive():
|
||||
logger.debug(f"[{platform}] 停止后台OCR任务")
|
||||
# 线程是守护线程,会随进程退出自动回收
|
||||
if platform in self._ocr_bg_thread:
|
||||
del self._ocr_bg_thread[platform]
|
||||
|
||||
# ── 单次轮询 ─────────────────────────────────────────────
|
||||
|
||||
def _poll(self, platform: str):
|
||||
# ⚠️ 急停检查:OCR 期间用户可能已按 ESC 急停,必须立即折返
|
||||
if self._stop_event.is_set() or not self._running:
|
||||
# ⭐ Task #1648: 急停时清理后台OCR任务
|
||||
self._stop_ocr_bg(platform)
|
||||
return
|
||||
# 自动回复发送中跳过轮询(防止窗口焦点被抢)
|
||||
if self._sending_event.is_set():
|
||||
@@ -9377,9 +9493,31 @@ class MessageMonitor:
|
||||
else:
|
||||
win = None # 惰性创建:OCR 失败后需要 fallback 时再建
|
||||
|
||||
# 3. 读消息(Windows OCR 同步阻塞,可能耗时 2-5s)
|
||||
texts = self._read_messages(win, platform)
|
||||
|
||||
# 3. 读消息(异步非阻塞OCR)
|
||||
# ⭐ Task #1648: 使用后台OCR避免GUI阻塞
|
||||
texts = None
|
||||
try:
|
||||
# 首先检查是否有后台OCR结果
|
||||
texts = self._check_ocr_bg_result(platform)
|
||||
|
||||
if texts is None:
|
||||
# OCR仍在进行,检查是否需要启动新的OCR
|
||||
# 如果没有后台任务在运行,启动新的
|
||||
if not self._ocr_bg_thread.get(platform) or not self._ocr_bg_thread[platform].is_alive():
|
||||
# 启动后台OCR
|
||||
self._start_ocr_bg(platform)
|
||||
logger.debug(f"[{platform}] 启动后台OCR任务")
|
||||
|
||||
# 非阻塞返回,等待下次轮询获取结果
|
||||
return
|
||||
else:
|
||||
# 有结果了,继续处理
|
||||
logger.debug(f"[{platform}] 获取到后台OCR结果: {len(texts)} 条消息")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[{platform}] 异步OCR处理异常: {e}")
|
||||
texts = []
|
||||
|
||||
# ⚠️ 急停检查:OCR阻塞可能持续数秒,返回后立即检查,丢弃半成品
|
||||
if self._stop_event.is_set() or not self._running:
|
||||
return
|
||||
@@ -10132,34 +10270,37 @@ class MessageMonitor:
|
||||
continue
|
||||
# ⭐ 添加到候选列表(含质量分)
|
||||
score = self._score_message_quality(cleaned_item)
|
||||
# ⭐ 时间戳新鲜度奖励:使用预先计算的freshness_score
|
||||
# ⭐ 时间戳新鲜度评分:使用预先计算的freshness_score
|
||||
# 从freshness_scores字典中获取当前消息的时间戳新鲜度分数
|
||||
freshness_score = freshness_scores.get(item, 0.0)
|
||||
# 修订逻辑:≥5分直接放行,<5分查DB决定是否补分到5
|
||||
|
||||
# ⭐ Task #1647: 陈旧消息过滤策略重构
|
||||
# - 超过1小时的消息:freshness_score == -10.0(扣10分)
|
||||
# - 选择消息时分数必须>0才能处理
|
||||
# - DB有记录的消息:不加分(避免重复处理)
|
||||
if freshness_score >= 5:
|
||||
# ≥5分:直接放行,加上5分
|
||||
# ≥5分(5分钟内):直接放行,加上5分
|
||||
score += 5.0
|
||||
if freshness_score > 0:
|
||||
logger.info(f"[{platform}] 时间戳新鲜度≥5直接放行(+5): {item[:50]}")
|
||||
logger.info(f"[{platform}] 时间戳新鲜度≥5直接放行(+5): {item[:50]}")
|
||||
elif freshness_score < 0:
|
||||
# 负分(超过1小时):扣10分
|
||||
score += freshness_score # 加上负分 = 扣分
|
||||
logger.info(f"[{platform}] 陈旧时间戳扣分{freshness_score:.0f}: {item[:50]}")
|
||||
else:
|
||||
# <5分:查DB
|
||||
# 0-4分(10分钟-1小时):查DB决定是否加分
|
||||
if self._msg_history and self._msg_history.has_ocr_message_record(platform, cleaned_item):
|
||||
# DB有记录:不加分
|
||||
logger.info(f"[{platform}] 时间戳新鲜度<5且DB有记录,不加分: {item[:50]}")
|
||||
else:
|
||||
# ⭐ Task #1646: 陈旧时间戳硬性拒绝
|
||||
# freshness_score==1.0 表示文本含时间戳且距现在 >3600秒(超1小时),
|
||||
# 视为OCR从旧消息继承来的陈旧时间戳,不应当作新消息处理
|
||||
# (用户报告: 22:32 错误放行了 21:29 的 1h3m 旧时间戳)。
|
||||
# 仅对"有时间戳且过旧(freshness==1.0)"拒绝;无时间戳(freshness==0.0)
|
||||
# 无法判断年龄,沿用旧逻辑补分,避免误杀正常消息。
|
||||
if freshness_score == 1.0:
|
||||
logger.info(f"[{platform}] 陈旧时间戳(>1h)拒绝处理: {item[:50]}")
|
||||
self._last_new_msg_time[platform] = time.time()
|
||||
continue
|
||||
# DB无记录且时间戳不过旧:补分到5
|
||||
# DB无记录:补分到5
|
||||
score += 5.0
|
||||
logger.info(f"[{platform}] 时间戳新鲜度<5且DB无记录,补分到5: {item[:50]}")
|
||||
|
||||
# ⭐ 检查分数是否>0:分数<=0的消息不应被处理
|
||||
if score <= 0:
|
||||
logger.info(f"[{platform}] 消息分数{score:.1f}≤0,丢弃: {item[:50]}")
|
||||
self._last_new_msg_time[platform] = time.time()
|
||||
continue
|
||||
# 保留原有时间戳差值计算(用于排序)
|
||||
_ts_match = re.search(r'(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?', item)
|
||||
if _ts_match:
|
||||
|
||||
Reference in New Issue
Block a user