修复两个问题:

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:
youpaishidifu
2026-08-31 00:10:24 +08:00
parent 99abaa92bb
commit 63f8c749c1
+167 -26
View File
@@ -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: