任务#1441: 修复bot自言自语和双重回复问题
**问题1: 自言自语** - OCR把bot自己的回复再读一遍,当成新消息触发二次回复 - 修复: 查库对比bot历史回复,重合率≥50%(高分消息)或≥70%(普通消息)则丢弃 **问题2: 双重回复** - 用户操作电脑打断bot回复发送时,_last_hash_replied标志被重置,下次轮询重复触发 - 修复: 新增_reply_in_progress标志,回复进行中时跳过所有轮询处理 **修改文件:** - monitor.py: 添加_reply_in_progress状态追踪+动态阈值bot消息过滤 - gui.py: 在回复执行前后设置/清除_reply_in_progress标志 - _keyword_scorer.py: 新增get_last_score()方法用于查询得分而不记录
This commit is contained in:
@@ -187,6 +187,23 @@ class KeywordScorer:
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def get_last_score(self, platform: str, text: str) -> int:
|
||||
"""获取指定文本最近一次的关键词得分(不记录新行)"""
|
||||
keywords = self.extract_keywords(text)
|
||||
if not keywords:
|
||||
return 0
|
||||
conn = sqlite3.connect(self.db_path, check_same_thread=False)
|
||||
try:
|
||||
cursor = conn.execute("""
|
||||
SELECT score FROM keyword_scores
|
||||
WHERE platform = ? AND message_text = ?
|
||||
ORDER BY id DESC LIMIT 1
|
||||
""", (platform, text.strip()))
|
||||
row = cursor.fetchone()
|
||||
return row[0] if row else 0
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def should_skip_message(self, text: str, platform: str,
|
||||
ocr_time: Optional[str] = None,
|
||||
threshold: int = 2) -> Tuple[bool, int, List[str]]:
|
||||
|
||||
@@ -1212,6 +1212,8 @@ class MsgBotApp(ctk.CTk):
|
||||
|
||||
# 设发送锁,阻止监控轮询切走焦点
|
||||
self.monitor._sending_event.set()
|
||||
# ⭐ 设置回复进行中标志:防止打断后下次轮询重复触发
|
||||
self.monitor._reply_in_progress[platform] = True
|
||||
try:
|
||||
if sender:
|
||||
# 搜索联系人切换会话
|
||||
@@ -1239,6 +1241,8 @@ class MsgBotApp(ctk.CTk):
|
||||
self._log(f"[{platform}] ⚠ 自动回复发送失败(连续{max_retries}次),用户可能在接管,本轮跳过")
|
||||
finally:
|
||||
self.monitor._sending_event.clear()
|
||||
# ⭐ 回复完成或失败后清除进行中标志
|
||||
self.monitor._reply_in_progress.pop(platform, None)
|
||||
|
||||
def _check_message_freshness(self, msg: str) -> float or None:
|
||||
"""检查消息时间戳是否新鲜(≤300秒)。返回消息年龄(秒),无法解析返回None。"""
|
||||
|
||||
+30
-7
@@ -540,6 +540,8 @@ class MessageMonitor:
|
||||
self._foreground_cooldown_until: dict = {} # platform → float(time.time)
|
||||
# 同平台回复冷却:回复后此秒数内同平台跳过 OCR(防读到自身回复回声)
|
||||
self._self_reply_cooldown_until: dict = {} # platform → float(time.time)
|
||||
# ⭐ 回复进行中标志:防止bot回复发送被打断时,下次轮询重复触发
|
||||
self._reply_in_progress: dict = {} # platform → bool
|
||||
|
||||
# 本轮选中的 OCR 引擎(用于日志标记)
|
||||
self._last_chosen_engine: dict = {} # platform → "windows"|"easyocr"
|
||||
@@ -694,6 +696,7 @@ class MessageMonitor:
|
||||
self._failed_consecutive_count.clear()
|
||||
self._foreground_cooldown_until.clear()
|
||||
self._self_reply_cooldown_until.clear()
|
||||
self._reply_in_progress.clear() # ⭐ 清理回复进行中标志
|
||||
# OCR/cache 状态
|
||||
self._last_texts.clear()
|
||||
self._raw_clipboard.clear()
|
||||
@@ -758,6 +761,7 @@ class MessageMonitor:
|
||||
self._ocr_last_text.clear()
|
||||
self._last_chosen_engine.clear()
|
||||
self._self_reply_cooldown_until.clear()
|
||||
self._reply_in_progress.clear() # ⭐ 清理回复进行中标志
|
||||
self._ocr_engine_wins = {"windows": 0, "easyocr": 0}
|
||||
# OCR 截屏定时清理:启动后 30~60 秒随机触发第一次
|
||||
self._next_ocr_clean_at = time.time() + random.uniform(30, 60)
|
||||
@@ -1191,6 +1195,7 @@ class MessageMonitor:
|
||||
self._ocr_last_text.clear()
|
||||
self._last_chosen_engine.clear()
|
||||
self._self_reply_cooldown_until.clear()
|
||||
self._reply_in_progress.clear() # ⭐ 清理回复进行中标志
|
||||
self._ocr_engine_wins = {"windows": 0, "easyocr": 0}
|
||||
|
||||
# 通知回调(GUI 更新状态)
|
||||
@@ -4128,8 +4133,9 @@ class MessageMonitor:
|
||||
continue
|
||||
|
||||
# ⭐ Task #1441: bot自身消息查库过滤
|
||||
# 检查OCR读到的内容是否与数据库中bot回复的消息重合率≥90%
|
||||
# 如果是,说明这是bot自己回复的消息,不是用户消息,应丢弃
|
||||
# 检查OCR读到的内容是否与数据库中bot回复的消息重合率
|
||||
# - 得分≥2时:重合率≥50%丢弃(防bot自言自语)
|
||||
# - 得分<2时:重合率≥70%丢弃(放宽阈值)
|
||||
if len(line) > 10 and self._msg_history:
|
||||
try:
|
||||
import difflib
|
||||
@@ -4141,9 +4147,18 @@ class MessageMonitor:
|
||||
# 计算重合率
|
||||
sm = difflib.SequenceMatcher(None, line, bot_reply)
|
||||
ratio = sm.ratio()
|
||||
if ratio >= 0.7:
|
||||
logger.debug(f"[{platform}] OCR 过滤: bot自身消息(查库重合率{ratio:.2f}) line={line[:60]!r}")
|
||||
continue
|
||||
# 获取当前消息的关键词得分
|
||||
threshold = 0.7 # 默认阈值
|
||||
if self._keyword_scorer:
|
||||
try:
|
||||
score = self._keyword_scorer.get_last_score(platform, line)
|
||||
if score >= 2:
|
||||
threshold = 0.5 # 高分消息降低阈值
|
||||
except Exception:
|
||||
pass
|
||||
if ratio >= threshold:
|
||||
logger.debug(f"[{platform}] OCR 过滤: bot自身消息(查库重合率{ratio:.2f}≥{threshold:.0%}) line={line[:60]!r}")
|
||||
break # 匹配到任一bot回复即丢弃
|
||||
except Exception as e:
|
||||
logger.debug(f"[{platform}] OCR 查库过滤异常: {e}")
|
||||
|
||||
@@ -8445,11 +8460,14 @@ class MessageMonitor:
|
||||
self._last_hash_queued[platform] = True
|
||||
# ⭐ OCR返回文本但未被回复,重置hash回复标志(等待消息被回复)
|
||||
# 注意:_last_hash_replied 在消息被回复时才设置为True
|
||||
if not self._last_hash_replied.get(platform, False):
|
||||
# ⭐ 修复:如果正在回复中,不重置标志(防止打断后重复触发)
|
||||
if not self._last_hash_replied.get(platform, False) and not self._reply_in_progress.get(platform, False):
|
||||
self._last_hash_replied[platform] = False
|
||||
else:
|
||||
self._last_hash_queued[platform] = False
|
||||
self._last_hash_replied[platform] = False
|
||||
# ⭐ 修复:如果正在回复中,不重置标志(防止打断后重复触发)
|
||||
if not self._reply_in_progress.get(platform, False):
|
||||
self._last_hash_replied[platform] = False
|
||||
|
||||
# ⚠️ OCR 返回后立即检查急停标志:急停可能在 OCR 阻塞期间触发,
|
||||
# Windows OCR 无法被中断,但后续文本处理可以立即跳过。
|
||||
@@ -8477,6 +8495,11 @@ class MessageMonitor:
|
||||
if not self._running:
|
||||
return
|
||||
|
||||
# ⭐ 修复:如果正在回复中,跳过本轮处理(防止打断后重复触发)
|
||||
if self._reply_in_progress.get(platform, False):
|
||||
logger.debug(f"[{platform}] Poll: 正在回复中,跳过本轮")
|
||||
return
|
||||
|
||||
# 冻结计数器(不在 early-return 里定义,保证所有路径都能访问)
|
||||
stale = 0
|
||||
|
||||
|
||||
Reference in New Issue
Block a user