修复时间戳继承导致回复旧消息的问题 (Task #1710)
QQ客户端的时间戳渲染在消息下方(作为分隔符),但代码假设时间戳在消息上方,导致新消息时间戳被错误地'继承'给旧消息,机器人回复旧消息而非新消息。 三处关键修复: 1. stage splitting阶段:使用当前时间戳(part)处理文本,而非last_ts 2. 添加后处理步骤:重新分配时间戳,使每条消息使用其下方的时间戳 3. freshness_scores计算:优先使用前方(下一个)时间戳,后方时间戳惩罚2倍
This commit is contained in:
+99
-65
@@ -4275,6 +4275,10 @@ class MessageMonitor:
|
|||||||
last_ts = None if _is_qq_system_msg else _inherited_ts
|
last_ts = None if _is_qq_system_msg else _inherited_ts
|
||||||
# ⭐ 继承的过去日期标记:若前一阶段有"昨天"等过去前缀,继承的时间戳也跳过未来检查
|
# ⭐ 继承的过去日期标记:若前一阶段有"昨天"等过去前缀,继承的时间戳也跳过未来检查
|
||||||
_inherited_past = _had_past_date_prefix or getattr(self, '_inherited_ts_from_past', False)
|
_inherited_past = _had_past_date_prefix or getattr(self, '_inherited_ts_from_past', False)
|
||||||
|
# ⭐ QQ时间戳在消息下方:待处理消息缓冲区
|
||||||
|
# 消息应该关联到其下方的时间戳,而不是上方的时间戳
|
||||||
|
# 当遇到时间戳时,处理之前的待处理消息
|
||||||
|
_pending_msgs = [] # [(msg_text, ...), ...] 待处理的消息列表
|
||||||
# ⭐ Task #1558: 完全用关键词打分控制消息留舍,OCR时间戳仅作为参考
|
# ⭐ Task #1558: 完全用关键词打分控制消息留舍,OCR时间戳仅作为参考
|
||||||
# - 不依赖时间戳丢弃消息(时间戳可能被OCR继承错误)
|
# - 不依赖时间戳丢弃消息(时间戳可能被OCR继承错误)
|
||||||
# - 消息抓来后直接打分:历史消息≥2分则丢弃,新消息≤1分继续处理
|
# - 消息抓来后直接打分:历史消息≥2分则丢弃,新消息≤1分继续处理
|
||||||
@@ -4283,15 +4287,16 @@ class MessageMonitor:
|
|||||||
for part in parts:
|
for part in parts:
|
||||||
if re.match(r'^\d{1,2}:\d{1,2}', part):
|
if re.match(r'^\d{1,2}:\d{1,2}', part):
|
||||||
if buf.strip():
|
if buf.strip():
|
||||||
if not last_ts:
|
# ⭐ QQ时间戳在消息下方:时间戳属于其上方的消息
|
||||||
# ⭐ 无前置时间戳:位于首个时间戳之前的消息。
|
# 使用当前时间戳(part)处理之前的buf,而不是last_ts
|
||||||
# ⚠ 不再整段丢弃:可能是被裁切/漏识别时间戳的新消息,
|
_effective_ts = part
|
||||||
# 交由下游DB查重(避免连续聊天时时间戳被覆盖误杀用户消息)。
|
if not last_ts and not _inherited_ts:
|
||||||
logger.info(f"[ocr] stage[{i}] [filter/放行] 无前置时间戳,交由DB查重: {buf.strip()[:60]!r}")
|
# ⭐ 无前置时间戳也无继承时间戳:交由DB查重
|
||||||
|
logger.info(f"[ocr] stage[{i}] [filter/放行] 无时间戳,交由DB查重: {buf.strip()[:60]!r}")
|
||||||
else:
|
else:
|
||||||
# ⭐ Task #1558: 不再因时间戳陈旧而丢弃,统一交由过滤器层+关键词打分
|
# ⭐ Task #1558: 不再因时间戳陈旧而丢弃,统一交由过滤器层+关键词打分
|
||||||
# 时间戳仅记录,不参与丢弃判断
|
# 使用当前时间戳(part)处理之前的buf
|
||||||
logger.debug(f"[ocr] stage[{i}] 时间戳{last_ts}更新,消息交由过滤器层: {buf.strip()[:60]!r}")
|
logger.debug(f"[ocr] stage[{i}] 时间戳{_effective_ts}更新,消息交由过滤器层: {buf.strip()[:60]!r}")
|
||||||
# —— 过滤器层(始终运行,绝不静默吞噬;每层放行/打回都记录)——
|
# —— 过滤器层(始终运行,绝不静默吞噬;每层放行/打回都记录)——
|
||||||
# ⭐ 2026-08-12 调整顺序:必须先剥离 bot 旧回复片段,再剥 md 尾巴。
|
# ⭐ 2026-08-12 调整顺序:必须先剥离 bot 旧回复片段,再剥 md 尾巴。
|
||||||
# 原因:OCR 常把「用户新消息 + bot 旧回复」拼成一行,bot 回复若含
|
# 原因:OCR 常把「用户新消息 + bot 旧回复」拼成一行,bot 回复若含
|
||||||
@@ -4352,25 +4357,28 @@ class MessageMonitor:
|
|||||||
logger.info(f"[ocr] stage[{i}] [filter4/回复残片] DROP 合并消息中的残片: {_mm[:60]!r}")
|
logger.info(f"[ocr] stage[{i}] [filter4/回复残片] DROP 合并消息中的残片: {_mm[:60]!r}")
|
||||||
else:
|
else:
|
||||||
logger.info(f"[ocr] stage[{i}] [filter/放行] 合并消息片段通过: {_mm[:60]!r}")
|
logger.info(f"[ocr] stage[{i}] [filter/放行] 合并消息片段通过: {_mm[:60]!r}")
|
||||||
expanded.append((_mm, last_ts)) # 保留时间戳信息
|
expanded.append((_mm, _effective_ts)) # 保留时间戳信息
|
||||||
elif not msg:
|
elif not msg:
|
||||||
logger.debug(f"[ocr] stage[{i}] 剥离后为空,丢弃防空消息: {part[:40]!r}")
|
logger.debug(f"[ocr] stage[{i}] 剥离后为空,丢弃防空消息: {part[:40]!r}")
|
||||||
elif self._is_reply_fragment(msg):
|
elif self._is_reply_fragment(msg):
|
||||||
logger.info(f"[ocr] stage[{i}] [filter4/回复残片] DROP 回复残片跳过: {msg[:60]!r}")
|
logger.info(f"[ocr] stage[{i}] [filter4/回复残片] DROP 回复残片跳过: {msg[:60]!r}")
|
||||||
else:
|
else:
|
||||||
logger.info(f"[ocr] stage[{i}] [filter/放行] 通过所有过滤器,添加到expanded: {msg[:60]!r}")
|
logger.info(f"[ocr] stage[{i}] [filter/放行] 通过所有过滤器,添加到expanded: {msg[:60]!r}")
|
||||||
expanded.append((msg, last_ts)) # 保留时间戳信息
|
expanded.append((msg, _effective_ts)) # 保留时间戳信息
|
||||||
buf = ''
|
buf = ''
|
||||||
last_ts = part
|
last_ts = part
|
||||||
_inherited_past = False # 新时间戳重置过去日期标记
|
_inherited_past = False # 新时间戳重置过去日期标记
|
||||||
|
# ⭐ QQ时间戳在消息下方:时间戳属于其上方的消息
|
||||||
|
# 当前时间戳已经用于处理之前的buf,不再设置_inherited_ts供后续消息使用
|
||||||
|
# 后续消息需要等待下一个时间戳
|
||||||
|
# 注意:_inherited_ts 保持不变(可能是 None 或之前的值),让后续消息等待下一个时间戳
|
||||||
else:
|
else:
|
||||||
buf += part
|
buf += part
|
||||||
if buf.strip():
|
if buf.strip():
|
||||||
# 尾部检查:尾部消息位于最后一个可见时间戳之后,无独立时间戳。
|
# 尾部检查:尾部消息位于最后一个可见时间戳之后,无独立时间戳。
|
||||||
# 关键修复:过滤器层(md/噪声/回复残片)必须【始终运行】,无论继承时间戳新旧,
|
# ⭐ QQ时间戳在消息下方:尾部消息没有自己的时间戳,使用继承的时间戳
|
||||||
# 之前错误地嵌在"时间戳较老"分支内,导致新鲜尾部消息(如用户刚发的提问)
|
# _inherited_ts 可能是 None(如果本阶段没有遇到时间戳)或之前的值
|
||||||
# 被静默吞噬、永不进入 expanded。
|
# 交由下游DB查重+关键词打分
|
||||||
# ⭐ Task #1558: 不再因时间戳陈旧而丢弃,统一交由过滤器层+关键词打分
|
|
||||||
logger.info(f"[ocr] stage[{i}] [filter/放行] 尾部消息交由DB查重+关键词打分: {buf.strip()[:60]!r}")
|
logger.info(f"[ocr] stage[{i}] [filter/放行] 尾部消息交由DB查重+关键词打分: {buf.strip()[:60]!r}")
|
||||||
# —— 过滤器层(始终运行,绝不静默吞噬;每层放行/打回都记录)——
|
# —— 过滤器层(始终运行,绝不静默吞噬;每层放行/打回都记录)——
|
||||||
if buf.strip():
|
if buf.strip():
|
||||||
@@ -4449,7 +4457,7 @@ class MessageMonitor:
|
|||||||
logger.info(f"[ocr] stage[{i}] [filter/放行] 合并尾部消息片段通过: {_mm[:60]!r}")
|
logger.info(f"[ocr] stage[{i}] [filter/放行] 合并尾部消息片段通过: {_mm[:60]!r}")
|
||||||
split_msgs = self._split_merged_ocr_text(_mm)
|
split_msgs = self._split_merged_ocr_text(_mm)
|
||||||
for sm in split_msgs:
|
for sm in split_msgs:
|
||||||
expanded.append((sm, last_ts)) # 保留时间戳信息
|
expanded.append((sm, _inherited_ts)) # 保留时间戳信息
|
||||||
elif not msg:
|
elif not msg:
|
||||||
logger.debug(f"[ocr] stage[{i}] 尾部剥离后为空,丢弃防空消息: {part[:40]!r}")
|
logger.debug(f"[ocr] stage[{i}] 尾部剥离后为空,丢弃防空消息: {part[:40]!r}")
|
||||||
elif self._is_reply_fragment(msg):
|
elif self._is_reply_fragment(msg):
|
||||||
@@ -4461,7 +4469,7 @@ class MessageMonitor:
|
|||||||
# 这种合并文本。尝试在中文句尾标点处拆分为多条。
|
# 这种合并文本。尝试在中文句尾标点处拆分为多条。
|
||||||
split_msgs = self._split_merged_ocr_text(msg)
|
split_msgs = self._split_merged_ocr_text(msg)
|
||||||
for sm in split_msgs:
|
for sm in split_msgs:
|
||||||
expanded.append((sm, last_ts)) # 保留时间戳信息
|
expanded.append((sm, _inherited_ts)) # 保留时间戳信息
|
||||||
# ⭐ QQ系统消息不更新继承时间戳,防止旧时间戳污染后续真实消息
|
# ⭐ QQ系统消息不更新继承时间戳,防止旧时间戳污染后续真实消息
|
||||||
if not _is_qq_system_msg:
|
if not _is_qq_system_msg:
|
||||||
# 更新跨行继承时间戳
|
# 更新跨行继承时间戳
|
||||||
@@ -4474,6 +4482,46 @@ class MessageMonitor:
|
|||||||
else:
|
else:
|
||||||
logger.debug(f"[ocr] stage[{i}] QQ系统消息不更新继承时间戳: {rl[:60]!r}")
|
logger.debug(f"[ocr] stage[{i}] QQ系统消息不更新继承时间戳: {rl[:60]!r}")
|
||||||
|
|
||||||
|
# ⭐ QQ时间戳在消息下方:重新分配时间戳
|
||||||
|
# 每条消息应关联到其下方的时间戳(即下一条消息的继承时间戳)
|
||||||
|
# 例如:消息A → 17:25 → 消息B → 17:54 → 消息C
|
||||||
|
# 消息A应使用17:25,消息B应使用17:54,消息C使用当前时间
|
||||||
|
if expanded:
|
||||||
|
# 构建时间戳位置映射:记录每个时间戳在expanded中的位置
|
||||||
|
_ts_in_expanded = {} # {ts_value: [position_indices]}
|
||||||
|
for _eidx, _eitem in enumerate(expanded):
|
||||||
|
if isinstance(_eitem, tuple) and len(_eitem) == 2:
|
||||||
|
_etxt, _ets = _eitem
|
||||||
|
if _ets:
|
||||||
|
if _ets not in _ts_in_expanded:
|
||||||
|
_ts_in_expanded[_ets] = []
|
||||||
|
_ts_in_expanded[_ets].append(_eidx)
|
||||||
|
|
||||||
|
# 为每条消息找到下一个时间戳
|
||||||
|
for _eidx in range(len(expanded)):
|
||||||
|
_eitem = expanded[_eidx]
|
||||||
|
if not isinstance(_eitem, tuple) or len(_eitem) != 2:
|
||||||
|
continue
|
||||||
|
_etxt, _ets = _eitem
|
||||||
|
if not _etxt:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 搜索下一个时间戳(在expanded中位置在当前消息之后的)
|
||||||
|
_next_ts = None
|
||||||
|
for _nidx in range(_eidx + 1, len(expanded)):
|
||||||
|
_nitem = expanded[_nidx]
|
||||||
|
if isinstance(_nitem, tuple) and len(_nitem) == 2:
|
||||||
|
_ntxt, _nts = _nitem
|
||||||
|
if _nts and _nts != _ets: # 找到不同的时间戳
|
||||||
|
_next_ts = _nts
|
||||||
|
break
|
||||||
|
|
||||||
|
if _next_ts:
|
||||||
|
# ⭐ 找到下一个时间戳,使用它(QQ时间戳在消息下方)
|
||||||
|
expanded[_eidx] = (_etxt, _next_ts)
|
||||||
|
logger.debug(f"[ocr] QQ时间戳重分配: pos {_eidx} <- 下一个时间戳 {_next_ts!r}: {_etxt[:30]!r}")
|
||||||
|
# else: 没有下一个时间戳,保持原样(使用继承的时间戳或None)
|
||||||
|
|
||||||
# —— 逐行过滤 ——
|
# —— 逐行过滤 ——
|
||||||
# ⚠️ 急停检查:stage splitting 完成后,逐行过滤前再查一次
|
# ⚠️ 急停检查:stage splitting 完成后,逐行过滤前再查一次
|
||||||
if not self._running:
|
if not self._running:
|
||||||
@@ -8834,30 +8882,6 @@ class MessageMonitor:
|
|||||||
logger.debug(f"[ocr] 过滤器2结果: 检测到闲聊消息(无具体主题),返回True: {text!r}")
|
logger.debug(f"[ocr] 过滤器2结果: 检测到闲聊消息(无具体主题),返回True: {text!r}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# ⭐ 闲聊消息检测(2026-08-22 新增)
|
|
||||||
# 特征:短文本 + 疑问词 + 无具体查询主题
|
|
||||||
# 例:"有时间吗"、"在不在"、"好的"、"嗯嗯" → 缺乏可查库的具体主题
|
|
||||||
# 检测逻辑:
|
|
||||||
# 1. 长度 ≤ 8 字符(太短无法构成完整查询)
|
|
||||||
# 2. 包含疑问词 "吗" 或 "呢"(疑问句式)
|
|
||||||
# 3. 无核心关键词(无插件名、功能词、名词等)
|
|
||||||
_chat_patterns = [
|
|
||||||
r'^有[\s\S]*?吗$', # "有时间吗"、"有东西吗"
|
|
||||||
r'^在不在$', # "在不在"
|
|
||||||
r'^你好吗?$', # "你好"、"你好吗"
|
|
||||||
r'^好的?$', # "好"、"好的"
|
|
||||||
r'^嗯(?:嗯)?$', # "嗯"、"嗯嗯"
|
|
||||||
r'^哦?$', # "哦"
|
|
||||||
r'^嗯哼$', # "嗯哼"
|
|
||||||
r'^收到$', # "收到"
|
|
||||||
r'^明白$', # "明白"
|
|
||||||
r'^知道了$', # "知道了"
|
|
||||||
]
|
|
||||||
if len(text) <= 8 and ('吗' in text or '呢' in text):
|
|
||||||
for pat in _chat_patterns:
|
|
||||||
if re.match(pat, text.strip()):
|
|
||||||
logger.debug(f"[ocr] 过滤器2结果: 检测到闲聊消息(无具体主题),返回True: {text!r}")
|
|
||||||
return True
|
|
||||||
# HTML 标签:<br>、<br/>、<p>、</p>、<div>等
|
# HTML 标签:<br>、<br/>、<p>、</p>、<div>等
|
||||||
if re.search(r'<\s*(?:br|p|div|span|a|img)\s*/?\s*>', text, re.IGNORECASE):
|
if re.search(r'<\s*(?:br|p|div|span|a|img)\s*/?\s*>', text, re.IGNORECASE):
|
||||||
logger.debug(f"[ocr] 过滤器2结果: 检测到HTML标签,返回True")
|
logger.debug(f"[ocr] 过滤器2结果: 检测到HTML标签,返回True")
|
||||||
@@ -10291,32 +10315,42 @@ class MessageMonitor:
|
|||||||
if not ts:
|
if not ts:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 仅当消息尚无合法时间戳时,尝试继承;否则跳过继承步骤
|
# ⭐ QQ时间戳在消息下方:所有非自身时间戳的消息都需要重新分配
|
||||||
if freshness_scores.get(ts) == -999.0:
|
# 包括:无时间戳(-999.0)和有继承时间戳的消息
|
||||||
# 如果有继承的时间戳,使用它
|
# 因为继承的时间戳是"上方"的时间戳,QQ中应使用"下方"的时间戳
|
||||||
if inherited_ts:
|
_needs_reassign = (freshness_scores.get(ts) == -999.0) or inherited_ts
|
||||||
inherited_score = self._timestamp_freshness_score(inherited_ts)
|
if _needs_reassign:
|
||||||
if inherited_score != -999.0:
|
# ⭐ QQ时间戳在消息下方:搜索最近的时间戳,优先使用前方(下一个)时间戳
|
||||||
logger.debug(f"[{platform}] 无时间戳消息继承时间戳新鲜度 "
|
# QQ客户端的时间戳显示在消息下方,消息应关联到其下方的时间戳
|
||||||
f"(pos {idx} <- inherited ts {inherited_ts!r}): "
|
# 即:消息位于时间戳T1和T2之间时,应使用T2(更晚的时间戳)
|
||||||
f"{inherited_score:.1f}: {ts[:20]!r}")
|
best_freshness = None
|
||||||
freshness_scores[ts] = inherited_score
|
best_dist = float('inf')
|
||||||
|
best_is_forward = False
|
||||||
# 若继承后仍无合法时间戳,搜索最近的时间戳(前后双向)
|
for ts_idx, ts_fresh in _ts_positions:
|
||||||
if freshness_scores.get(ts) == -999.0:
|
# ⭐ 优先使用前方(下一个)时间戳
|
||||||
best_freshness = None
|
# forward_dist > 0 表示时间戳在消息之后(QQ正确语义)
|
||||||
best_dist = float('inf')
|
# forward_dist <= 0 表示时间戳在消息之前
|
||||||
for ts_idx, ts_fresh in _ts_positions:
|
forward_dist = ts_idx - idx
|
||||||
dist = abs(idx - ts_idx)
|
if forward_dist > 0:
|
||||||
if dist < best_dist:
|
# 前方时间戳:直接使用距离(不惩罚)
|
||||||
best_dist = dist
|
dist = forward_dist
|
||||||
best_freshness = ts_fresh
|
is_forward = True
|
||||||
if best_freshness is not None:
|
else:
|
||||||
logger.debug(f"[{platform}] 无时间戳消息继承最近时间戳新鲜度 "
|
# 后方时间戳:惩罚2倍(QQ中后方时间戳语义不正确)
|
||||||
f"(pos {idx} <- ts pos {idx-best_dist if best_dist <= idx else idx+best_dist}): "
|
dist = (-forward_dist) * 2
|
||||||
f"{best_freshness:.1f}: {ts[:20]!r}")
|
is_forward = False
|
||||||
freshness_scores[ts] = best_freshness
|
if dist < best_dist or (dist == best_dist and is_forward and not best_is_forward):
|
||||||
# else: 整批无时间戳,保持-999.0
|
best_dist = dist
|
||||||
|
best_freshness = ts_fresh
|
||||||
|
best_is_forward = is_forward
|
||||||
|
if best_freshness is not None:
|
||||||
|
_fwd_hint = "前方" if best_is_forward else "后方"
|
||||||
|
_actual_pos = idx + best_dist // (1 if best_is_forward else 2)
|
||||||
|
logger.debug(f"[{platform}] 无时间戳消息继承{_fwd_hint}时间戳新鲜度 "
|
||||||
|
f"(pos {idx} <- ts pos {_actual_pos}): "
|
||||||
|
f"{best_freshness:.1f}: {ts[:20]!r}")
|
||||||
|
freshness_scores[ts] = best_freshness
|
||||||
|
# else: 整批无时间戳,保持-999.0
|
||||||
|
|
||||||
# === 所有消息都必须经过过滤器(不再跳过) ===
|
# === 所有消息都必须经过过滤器(不再跳过) ===
|
||||||
# === URL-only 兜底:无中文且含 URL → 碎片 ===
|
# === URL-only 兜底:无中文且含 URL → 碎片 ===
|
||||||
|
|||||||
Reference in New Issue
Block a user