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