From aad8594b069bed896d767b09f1356d7d96afa414 Mon Sep 17 00:00:00 2001 From: youpaishidifu Date: Wed, 2 Sep 2026 16:51:10 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=97=A7=E6=B6=88=E6=81=AF?= =?UTF-8?q?=E6=97=B6=E9=97=B4=E6=88=B3=E6=A3=80=E6=9F=A5=E6=BC=8F=E6=B4=9E?= =?UTF-8?q?=20+=20mark=5Fmessage=5Freplied=20AttributeError?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 修复 mark_message_replied AttributeError: - 在 MessageMonitor 类中添加委托方法,转发到 MessageHistory.mark_message_replied() - 解决 gui.py 调用 self.monitor.mark_message_replied() 时找不到方法的问题 2. 陈旧时间戳强制丢弃 (Task #1684): - 将 freshness_score < 0 分支从 score += freshness_score(扣10分)改为 score = -100.0(强制丢弃) - 根因:短消息(+5)+疑问词(+10)可得15分,仅扣10分仍为5分>0,导致数小时前的旧消息通过阈值 - 例:10:55发送的'什么是推广渠道违规?'在16:24被OCR读取,freshness_score=-10,基础分15-10=5>0 → 误回复 - 修复:与 -999.0 统一,所有超过1小时的旧消息均强制丢弃,绝不触发回复 3. 保留时间戳信息: - 将 expanded 列表从存储字符串改为存储 (text, timestamp) 元组 - 保留 OCR 阶段的时间戳信息,供后续两遍扫描使用 - 优化时间戳继承逻辑,优先使用继承的时间戳计算新鲜度 --- monitor.py | 154 +++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 108 insertions(+), 46 deletions(-) diff --git a/monitor.py b/monitor.py index 7812ccc..dde9375 100644 --- a/monitor.py +++ b/monitor.py @@ -1166,6 +1166,11 @@ class MessageMonitor: except Exception as e: logger.error(f"[msg-history] 加载历史回复失败: {e}") + def mark_message_replied(self, platform: str, content: str): + """标记消息已回复 — 委托给 MessageHistory.mark_message_replied""" + if self._msg_history: + self._msg_history.mark_message_replied(platform, content) + def set_poll_interval(self, seconds: float): self._poll_interval = max(0.5, seconds) @@ -4154,7 +4159,7 @@ class MessageMonitor: logger.info(f"[ocr] [{engine_label}/后处理] line[{i}]: {rl[:120]!r}") # —— 时间戳分割(跨行继承) —— - expanded = [] + expanded = [] # 存储 (text, timestamp) 元组,保留时间戳信息 _inherited_ts = None # 跨行继承:上一行的时间戳,供当前行无时间戳时使用 self._inherited_ts_from_past = False # 跨行继承:上一行时间戳是否来自过去日期前缀 # ⭐ QQ系统消息模式:跳过这些消息的时间戳继承,防止旧时间戳覆盖真实用户消息 @@ -4283,14 +4288,14 @@ class MessageMonitor: logger.info(f"[ocr] stage[{i}] [filter4/回复残片] DROP 合并消息中的残片: {_mm[:60]!r}") else: logger.info(f"[ocr] stage[{i}] [filter/放行] 合并消息片段通过: {_mm[:60]!r}") - expanded.append(_mm) + expanded.append((_mm, last_ts)) # 保留时间戳信息 elif not msg: logger.debug(f"[ocr] stage[{i}] 剥离后为空,丢弃防空消息: {part[:40]!r}") elif self._is_reply_fragment(msg): logger.info(f"[ocr] stage[{i}] [filter4/回复残片] DROP 回复残片跳过: {msg[:60]!r}") else: logger.info(f"[ocr] stage[{i}] [filter/放行] 通过所有过滤器,添加到expanded: {msg[:60]!r}") - expanded.append(msg) + expanded.append((msg, last_ts)) # 保留时间戳信息 buf = '' last_ts = part _inherited_past = False # 新时间戳重置过去日期标记 @@ -4380,7 +4385,7 @@ class MessageMonitor: logger.info(f"[ocr] stage[{i}] [filter/放行] 合并尾部消息片段通过: {_mm[:60]!r}") split_msgs = self._split_merged_ocr_text(_mm) for sm in split_msgs: - expanded.append(sm) + expanded.append((sm, last_ts)) # 保留时间戳信息 elif not msg: logger.debug(f"[ocr] stage[{i}] 尾部剥离后为空,丢弃防空消息: {part[:40]!r}") elif self._is_reply_fragment(msg): @@ -4392,7 +4397,7 @@ class MessageMonitor: # 这种合并文本。尝试在中文句尾标点处拆分为多条。 split_msgs = self._split_merged_ocr_text(msg) for sm in split_msgs: - expanded.append(sm) + expanded.append((sm, last_ts)) # 保留时间戳信息 # ⭐ QQ系统消息不更新继承时间戳,防止旧时间戳污染后续真实消息 if not _is_qq_system_msg: # 更新跨行继承时间戳 @@ -4411,9 +4416,15 @@ class MessageMonitor: return [] ocr_lines = expanded texts = [] - for line in ocr_lines: - if not line: + for item in ocr_lines: + if not item: continue + # 解包元组 (text, timestamp) + if isinstance(item, tuple) and len(item) == 2: + line, inherited_ts = item + else: + line = item + inherited_ts = None if line in ("查看更多", "收起", "选择会话开始聊天"): continue if re.match(r'^[✿♡♥❤★☆✦✧❀❁❂❃❄❅❆❇❈❉❊❋➠➡➢➣➤.·•●○◦◎◉◍◐◑◒◓◔◕◖◗◢◣◤◥☺☻☼♠♣♦♤♧♡♢♔♕♖♗♘♙🕐-🕧\s]{1,3}$', line): @@ -5068,10 +5079,10 @@ class MessageMonitor: logger.debug(f"[{platform}] OCR 过滤: 病句(问句+乱码拼接) line={line!r}") continue - texts.append(line) + texts.append((line, inherited_ts)) # 保留时间戳信息 # ⭐ 2026-08-29 修复:在单条消息通过所有过滤器后入库,替代 _ocr_text_has_fresh_timestamp 中整屏入库 if self._msg_history: - self._msg_history.record_user_message(platform, line) + self._msg_history.record_user_message(platform, line) # 只记录文本,不记录时间戳 logger.debug(f"[{platform}] OCR -> {len(ocr_lines)} raw, {len(texts)} filtered, " f"last=\"{texts[-1][:50] if texts else ''}\"") @@ -6357,15 +6368,27 @@ class MessageMonitor: result = [] SIM_THRESHOLD = 0.75 # Jaccard相似度阈值:>=0.75视为语义重复(提高防误杀) - for text in texts: + for item in texts: + # 解包元组 (text, timestamp) + if isinstance(item, tuple) and len(item) == 2: + text, inherited_ts = item + else: + text = item + inherited_ts = None + is_dup = False # ⭐ 问句豁免:含问号/疑问词的消息不视为语义重复(用户可能重复提问) # 例:"登录插件在哪儿下载?" vs baseline轮次的同问句 → 应放行而非静默丢弃 if re.search(r'[??]', text) or any(qw in text for qw in ('吗', '呢', '多少', '怎么', '如何', '哪', '什么')): - result.append(text) + result.append(item) # 保留原始元组 self._semantic_fingerprint_cache.append((text, now)) continue - for kept_text in result: + for kept_item in result: + # 解包保留项的元组 + if isinstance(kept_item, tuple) and len(kept_item) == 2: + kept_text, _ = kept_item + else: + kept_text = kept_item sim = self._semantic_similarity(text, kept_text) if sim >= SIM_THRESHOLD: logger.debug( @@ -6375,7 +6398,7 @@ class MessageMonitor: is_dup = True break if not is_dup: - result.append(text) + result.append(item) # 保留原始元组 self._semantic_fingerprint_cache.append((text, now)) return result @@ -9964,9 +9987,10 @@ class MessageMonitor: # 有结果了,继续处理 logger.debug(f"[{platform}] 获取到后台OCR结果: {len(texts)} 条消息") # ⭐ Task #1680: 调试日志 - 查看texts内容 - for i, t in enumerate(texts): - if t.strip(): - _t = t.strip()[:60] + for i, item in enumerate(texts): + _t_text = item[0] if isinstance(item, tuple) else item + if _t_text.strip(): + _t = _t_text.strip()[:60] logger.debug(f"[{platform}] texts[{i}]: {_t!r}") except Exception as e: @@ -10032,10 +10056,10 @@ class MessageMonitor: stale = 0 # 4a. 全文本签名快速跳过 — OCR 输出与上轮完全相同 → 无变化,跳过 - sig = "||".join([t.strip() for t in texts if t.strip()]) + sig = "||".join([t[0].strip() if isinstance(t, tuple) else t.strip() for t in texts if (t[0].strip() if isinstance(t, tuple) else t.strip())]) if sig == self._last_sig.get(platform, ""): # 检查是否有未回复的消息(防止未回复消息被永久跳过) - unreplied_texts = [t for t in texts if t.strip() and t.strip() not in self._replied_texts] + unreplied_texts = [t for t in texts if (t[0].strip() if isinstance(t, tuple) else t.strip()) and (t[0].strip() if isinstance(t, tuple) else t.strip()) not in self._replied_texts] if not unreplied_texts: # 所有消息均已回复,正常跳过 stale = self._stale_count.get(platform, 0) + 1 @@ -10069,40 +10093,74 @@ class MessageMonitor: # 原因:QQ时间戳出现在消息之间(如 msg1, ts1, msg2, ts2, ...) # 第一遍时msg1没有前驱时间戳可继承,需在第二遍用后继时间戳补救 _ts_positions = [] # [(index, freshness_score)] 时间戳在texts中的位置和分数 - for idx, t in enumerate(texts): + for idx, item in enumerate(texts): + # 解包元组 (text, timestamp) + if isinstance(item, tuple) and len(item) == 2: + t, inherited_ts = item + else: + t = item + inherited_ts = None + ts = t.strip() if not ts: continue - # 计算时间戳新鲜度分数 - freshness_score = self._timestamp_freshness_score(ts) + + # 优先使用继承的时间戳分数,如果没有则计算 + if inherited_ts: + # 使用继承的时间戳计算新鲜度分数 + freshness_score = self._timestamp_freshness_score(inherited_ts) + logger.debug(f"[{platform}] freshness_scores构建(继承): {ts[:20]!r} <- {inherited_ts!r} -> {freshness_score:.1f}") + else: + # 计算时间戳新鲜度分数 + freshness_score = self._timestamp_freshness_score(ts) + if len(ts) < 30: + logger.debug(f"[{platform}] freshness_scores构建(自身): {ts!r} -> {freshness_score:.1f}") + freshness_scores[ts] = freshness_score if freshness_score != -999.0: _ts_positions.append((idx, freshness_score)) - # ⭐ Task #1680: 调试日志 - if len(ts) < 30: - logger.debug(f"[{platform}] freshness_scores构建: {ts!r} -> {freshness_score:.1f}") # 第二遍:为无时间戳的消息分配最近时间戳的新鲜度 - for idx, t in enumerate(texts): + for idx, item in enumerate(texts): + # 解包元组 (text, timestamp) + if isinstance(item, tuple) and len(item) == 2: + t, inherited_ts = item + else: + t = item + inherited_ts = None + ts = t.strip() if not ts: continue - if freshness_scores.get(ts) != -999.0: - continue # 已有合法时间戳,跳过 - # 搜索最近的时间戳(前后双向) - best_freshness = None - best_dist = float('inf') - for ts_idx, ts_fresh in _ts_positions: - dist = abs(idx - ts_idx) - if dist < best_dist: - best_dist = dist - best_freshness = ts_fresh - if best_freshness is not None: - logger.debug(f"[{platform}] 无时间戳消息继承最近时间戳新鲜度 " - f"(pos {idx} <- ts pos {idx-best_dist if best_dist <= idx else idx+best_dist}): " - f"{best_freshness:.1f}: {ts[:20]!r}") - freshness_scores[ts] = best_freshness - # else: 整批无时间戳,保持-999.0 + + # 仅当消息尚无合法时间戳时,尝试继承;否则跳过继承步骤 + if freshness_scores.get(ts) == -999.0: + # 如果有继承的时间戳,使用它 + if inherited_ts: + inherited_score = self._timestamp_freshness_score(inherited_ts) + if inherited_score != -999.0: + logger.debug(f"[{platform}] 无时间戳消息继承时间戳新鲜度 " + f"(pos {idx} <- inherited ts {inherited_ts!r}): " + f"{inherited_score:.1f}: {ts[:20]!r}") + freshness_scores[ts] = inherited_score + + # 若继承后仍无合法时间戳,搜索最近的时间戳(前后双向) + if freshness_scores.get(ts) == -999.0: + best_freshness = None + best_dist = float('inf') + for ts_idx, ts_fresh in _ts_positions: + dist = abs(idx - ts_idx) + if dist < best_dist: + best_dist = dist + best_freshness = ts_fresh + if best_freshness is not None: + logger.debug(f"[{platform}] 无时间戳消息继承最近时间戳新鲜度 " + f"(pos {idx} <- ts pos {idx-best_dist if best_dist <= idx else idx+best_dist}): " + f"{best_freshness:.1f}: {ts[:20]!r}") + freshness_scores[ts] = best_freshness + # else: 整批无时间戳,保持-999.0 + + # === 所有消息都必须经过过滤器(不再跳过) === # === URL-only 兜底:无中文且含 URL → 碎片 === if not re.search(r'[\u4e00-\u9fff]', ts) and re.search(r'https?\S', ts, re.IGNORECASE): logger.debug(f"[{platform}] Poll过滤: URL-only碎片 ts={ts!r}") @@ -10801,14 +10859,18 @@ class MessageMonitor: elif freshness_score == -999.0: # ⭐ Task #1661+#1665: 无有效时间戳(整批OCR丢失时间戳) # 完全无法判断消息年龄,直接强制丢弃 - # 注意:短消息(+5)+疑问词(+10)可得15分,仅扣10分仍为5分>0 - # 会导致无时间戳的旧消息被误处理,所以改为强制设为极低分 + # ⭐ 与 freshness_score < 0 统一:所有无法确认新鲜度的消息均强制丢弃 score = -100.0 logger.info(f"[{platform}] 无有效时间戳(OCR丢失?)强制丢弃: {item[:50]}") elif freshness_score < 0: - # 负分(超过1小时):扣10分 - score += freshness_score # 加上负分 = 扣分 - logger.info(f"[{platform}] 陈旧时间戳扣分{freshness_score:.0f}: {item[:50]}") + # ⭐ Task #1684: 负分(超过1小时)→ 强制丢弃,不再仅扣10分 + # 根因:短消息(+5)+疑问词(+10)可得15分,仅扣10分仍为5分>0, + # 导致数小时前的旧消息通过阈值被错误处理。 + # 例:10:55发送的"什么是推广渠道违规?"在16:24被OCR读取, + # freshness_score=-10,基础分15-10=5>0 → 误回复。 + # 修复:与-999.0一样强制丢弃,确保超过1小时的旧消息绝不触发回复。 + score = -100.0 + logger.info(f"[{platform}] 陈旧时间戳(>{abs(freshness_score)/10:.0f}h)强制丢弃: {item[:50]}") else: # 0-4分(10分钟-1小时):查DB决定是否加分 if self._msg_history and self._msg_history.has_ocr_message_record(platform, cleaned_item):