Task #1670: 改进预分割逻辑,避免因局部噪声特征导致整个消息被丢弃
1. 改进_split_and_denoise_segments()方法: - 按时间戳作为消息边界分割 - 按系统消息作为分割点 - 避免因局部噪声特征导致整个消息被丢弃 2. 改进_is_system_message()方法: - 先剥离时间戳,再检查系统消息特征 - 避免时间戳被误判为系统消息 3. 改进噪声检测逻辑: - 先剥离时间戳 - 再检查系统消息 - 最后检查噪声 解决用户报告的问题: OCR把消息拼接成一个整体,只要其中一个命中丢弃规则就会跳过导致消息丢失。
This commit is contained in:
+75
-31
@@ -7445,18 +7445,27 @@ class MessageMonitor:
|
||||
|
||||
@staticmethod
|
||||
def _is_system_message(text: str) -> bool:
|
||||
"""过滤机器人/系统消息,不作为用户消息处理。"""
|
||||
"""过滤机器人/系统消息,不作为用户消息处理。
|
||||
|
||||
⭐ Task #1670: 改进系统消息检测,避免因局部噪声特征导致整个消息被丢弃。
|
||||
策略:先剥离时间戳,再检查系统消息特征。
|
||||
"""
|
||||
if not text:
|
||||
return True
|
||||
# NFKC 标准化:去除 UIA/OCR 夹杂的零宽字符、不可见 Unicode
|
||||
text = unicodedata.normalize('NFKC', text)
|
||||
|
||||
# ⭐ Task #1670: 先剥离时间戳,再检查系统消息
|
||||
# 这样可以避免时间戳被误判为系统消息
|
||||
cleaned_text = re.sub(r'(?:\d{4}[/-]\d{1,2}[/-]\d{1,2})?\s*\d{1,2}[;:.]\d{2}[;:.]?\d{0,2}', '', text).strip()
|
||||
|
||||
# 系统前缀/标记
|
||||
system_markers = [
|
||||
"✅", "❌", "📝", "🔍", "📄", "➡", "♡", "✿",
|
||||
"[WECHAT]", "[QQ]", "[wechat]", "[qq]",
|
||||
]
|
||||
for m in system_markers:
|
||||
if text.startswith(m):
|
||||
if cleaned_text.startswith(m):
|
||||
return True
|
||||
# ⭐ QQ/微信 界面噪声文字(按钮/提示/碎片/导航,非用户消息)
|
||||
qq_ui_noise = [
|
||||
@@ -7475,7 +7484,7 @@ class MessageMonitor:
|
||||
"发送", "接收", "取消", "确认", "确定", "关闭",
|
||||
]
|
||||
for n in qq_ui_noise:
|
||||
if text == n:
|
||||
if cleaned_text == n:
|
||||
return True
|
||||
# 系统事件模板 — 注意:用 startswith 而非 in,避免混合文本(OCR 多行合一)误杀
|
||||
system_patterns = [
|
||||
@@ -7490,15 +7499,15 @@ class MessageMonitor:
|
||||
"PS ", "❯ ", "$ ",
|
||||
]
|
||||
for p in system_patterns:
|
||||
if text.startswith(p):
|
||||
if cleaned_text.startswith(p):
|
||||
return True
|
||||
# QQ系统消息残片:撤回了一条消息、你撤回了, 条消息等
|
||||
if re.match(r'^(?:你|对方|他|她|管理员)\s*撤回了\s*,?\s*\d*\s*条?\s*消息', text):
|
||||
if re.match(r'^(?:你|对方|他|她|管理员)\s*撤回了\s*,?\s*\d*\s*条?\s*消息', cleaned_text):
|
||||
return True
|
||||
if re.match(r'^撤回了一条消息', text):
|
||||
if re.match(r'^撤回了一条消息', cleaned_text):
|
||||
return True
|
||||
# 纯时间格式(不含消息内容)
|
||||
if re.match(r'^\d{1,2}:\d{2}', text):
|
||||
if re.match(r'^\d{1,2}:\d{2}', cleaned_text):
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -8255,6 +8264,11 @@ class MessageMonitor:
|
||||
- 时间戳 HH:MM / HH:MM:SS:消息间天然边界
|
||||
- 图片 URL (https://img.ypshidifu.cn/...):bot 回复中的图片引用
|
||||
- 长空白/间隔:消息间的自然分隔
|
||||
|
||||
⭐ Task #1670: 改进分割逻辑,支持更细粒度的分割
|
||||
- 按时间戳作为消息边界分割
|
||||
- 按系统消息作为分割点
|
||||
- 避免因局部噪声特征导致整个消息被丢弃
|
||||
"""
|
||||
if not raw_text:
|
||||
return ""
|
||||
@@ -8263,14 +8277,20 @@ class MessageMonitor:
|
||||
if not self._is_noise_text(raw_text) and '<br' not in raw_text and 'https://img' not in raw_text:
|
||||
return raw_text
|
||||
|
||||
# ---- 分割成候选段 ----
|
||||
import copy
|
||||
# ---- ⭐ Task #1670: 改进分割逻辑 ----
|
||||
# 首先按时间戳和系统消息作为分割点,进行细粒度分割
|
||||
# 这样可以避免一个段包含多种不同性质的内容
|
||||
|
||||
# 分割策略:
|
||||
# 1. 按时间戳分割(支持变体分隔符)
|
||||
# 2. 按系统消息分割
|
||||
# 3. 按换行符分割
|
||||
segments = []
|
||||
|
||||
# 按多级分隔符分割
|
||||
# 1) <br> 或 <br/> 是 bot 回复中最强的噪声标记,优先在此处切分
|
||||
parts = re.split(r'(<\s*br\s*/?\s*>|\s*<br>\s*)', raw_text)
|
||||
# 步骤1: 按时间戳分割(支持多种格式)
|
||||
# 匹配: 2026/9/121:46;36, 2026/9/122:43;23, 9;51.24, 16:28:40 等
|
||||
timestamp_pattern = r'(?<!\d)(?:\d{4}[/-]\d{1,2}[/-]\d{1,2})?\s*\d{1,2}[;:.]\d{2}[;:.]?\d{0,2}(?!\d)'
|
||||
parts = re.split(f'({timestamp_pattern})', raw_text)
|
||||
|
||||
for part in parts:
|
||||
if not part or not part.strip():
|
||||
@@ -8278,14 +8298,10 @@ class MessageMonitor:
|
||||
part = part.strip()
|
||||
|
||||
# 如果段落仍然很长且包含多种内容,尝试进一步切分
|
||||
if len(part) > 100 and ('https://' in part or re.search(r'\d{1,2}:\d{1,2}', part)):
|
||||
# 按 URL 前后的问号句或时间戳进一步切分
|
||||
sub_parts = re.split(
|
||||
r'(?<=。|?|!|\.)\s+(?=[^<])'
|
||||
r'|(?<=[^:])\s+(?=\d{1,2}:\d{1,2})'
|
||||
r'|(?<=\S)\s{2,}(?=\S)',
|
||||
part
|
||||
)
|
||||
if len(part) > 100:
|
||||
# 按系统消息进一步切分
|
||||
system_msg_pattern = r'(?:你|对方|他|她|管理员)\s*撤回了|撤回了一条消息|你撤回了'
|
||||
sub_parts = re.split(f'({system_msg_pattern})', part)
|
||||
for sp in sub_parts:
|
||||
sp = sp.strip()
|
||||
if sp:
|
||||
@@ -8293,6 +8309,15 @@ class MessageMonitor:
|
||||
else:
|
||||
segments.append(part)
|
||||
|
||||
# 如果上述分割没有产生多个段,尝试按换行符分割
|
||||
if len(segments) <= 1:
|
||||
segments = []
|
||||
lines = raw_text.split('\n')
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if line:
|
||||
segments.append(line)
|
||||
|
||||
if not segments:
|
||||
return raw_text # 分割失败,回退到原始文本
|
||||
|
||||
@@ -8308,23 +8333,42 @@ class MessageMonitor:
|
||||
stripped_seg = re.sub(r'([\u4e00-\u9fff])\*(?!\*)', r'\1', stripped_seg)
|
||||
if stripped_seg != stripped_seg_before_star:
|
||||
logger.debug(f"[{platform}] 预分割: 清理markdown加粗残留*: {stripped_seg_before_star[:60]!r} -> {stripped_seg[:60]!r}")
|
||||
is_noise = self._is_noise_text(stripped_seg) if stripped_seg else True
|
||||
|
||||
# ⭐ Task #1670: 改进噪声检测逻辑
|
||||
# 1. 先剥离时间戳
|
||||
# 2. 再检测系统消息
|
||||
# 3. 最后检测噪声
|
||||
is_noise = False
|
||||
_is_cfg_fragment = False
|
||||
if not is_noise and stripped_seg:
|
||||
# ⭐ Task #1596: 二次过滤——bot 配置面板碎片(kv<3 漏网)也要丢弃,
|
||||
# 否则合并回整段后总 kv 数 ≥ 阈值触发整体丢弃,连带牺牲用户消息。
|
||||
_is_cfg_fragment = self._is_config_fragment(stripped_seg)
|
||||
if _is_cfg_fragment:
|
||||
|
||||
if stripped_seg:
|
||||
# 剥离时间戳后的内容
|
||||
cleaned_seg = re.sub(r'(?:\d{4}[/-]\d{1,2}[/-]\d{1,2})?\s*\d{1,2}[;:.]\d{2}[;:.]?\d{0,2}', '', stripped_seg).strip()
|
||||
|
||||
# 检查是否为系统消息(只检查开头)
|
||||
if cleaned_seg and re.match(r'^(?:你|对方|他|她|管理员)\s*撤回了|撤回了一条消息|你撤回了', cleaned_seg):
|
||||
is_noise = True
|
||||
if is_noise:
|
||||
noise_count += 1
|
||||
if _is_cfg_fragment:
|
||||
logger.debug(f"[{platform}] 预分割: 段[{i}] 检测到系统消息,丢弃: {stripped_seg[:60]!r}")
|
||||
# 检查是否为配置碎片
|
||||
elif self._is_config_fragment(stripped_seg):
|
||||
is_noise = True
|
||||
_is_cfg_fragment = True
|
||||
logger.debug(f"[{platform}] 预分割: 段[{i}] 含bot配置碎片,丢弃: {stripped_seg[:60]!r}")
|
||||
# 检查是否为噪声
|
||||
elif self._is_noise_text(stripped_seg):
|
||||
is_noise = True
|
||||
logger.debug(f"[{platform}] 预分割: 段[{i}] 含噪声特征,丢弃: {stripped_seg[:60]!r}")
|
||||
else:
|
||||
logger.debug(f"[{platform}] 预分割: 段[{i}] 含噪声特征,丢弃: {stripped_seg[:60]!r}" if stripped_seg else f"[{platform}] 预分割: 段[{i}] 剥离bot回复后为空,丢弃: {seg[:60]!r}")
|
||||
else:
|
||||
# 保留干净的段
|
||||
clean_segments.append(stripped_seg)
|
||||
logger.debug(f"[{platform}] 预分割: 段[{i}] 干净,保留: {stripped_seg[:60]!r}")
|
||||
else:
|
||||
# 空段,丢弃
|
||||
is_noise = True
|
||||
logger.debug(f"[{platform}] 预分割: 段[{i}] 剥离bot回复后为空,丢弃: {seg[:60]!r}")
|
||||
|
||||
if is_noise:
|
||||
noise_count += 1
|
||||
|
||||
# ---- 结果汇总 ----
|
||||
if not clean_segments:
|
||||
|
||||
Reference in New Issue
Block a user