fix(Task #1559): @提及用户消息被UI规则/短句碎片误杀 - 昵称剥离联动豁免

根因:
1. _strip_qq_nickname 全局先行,把')游戏专用关于我们'->')关于我们'、
   '@游戏专用关于我们'->'@关于我们',破坏规则4已知昵称豁免证据(28b3c93),
   规则4/3b把残留当UI元素整行误杀;短句碎片杀'关于我们'残留
2. 残留孤立@字符:'那关于我们@游戏专用'只剥昵称留尾@,污染后续
3. 短@噪声行'有人@拜'并入上行污染用户消息

修复:
1. _strip_qq_nickname 收集提及余量集合(@/)昵称剥离后正文),返回元组
2. 全局/模糊剥昵称时连紧邻提及符(@)(( ))一起剥除
3. per-line 提及衍生消息豁免规则3b/规则4/短句碎片(余量非空=用户提问;
   纯提及空余量仍是UI,规则3b照常拦截)
4. 行合并防护:含@无问号中文<=4字的噪声行不并入上行

验证:3单元测试+1端到端(真实日志输入)全过
- 5条'关于我们'变体全豁免存活,'有人@拜'/bot菜单/时间戳全被正确拦截
This commit is contained in:
youpaishidifu
2026-08-18 19:26:39 +08:00
parent 7587137b43
commit 00335535cd
+73 -14
View File
@@ -3188,7 +3188,7 @@ class MessageMonitor:
# 防止 OCR 原始文本中的昵称污染时间戳检测、行合并等早期步骤。
# 例如:"草原探险服务器备用号,@游戏专用有个问题..." 中的昵称会被提前剥离,
# 避免昵称字符干扰后续时间戳解析(如"备用号"中的数字被误读为时间戳片段)。
raw_text = self._strip_qq_nickname(raw_text)
raw_text, _mention_remainders = self._strip_qq_nickname(raw_text)
# —— 垃圾检测 ——
if self._is_ocr_console_garbage(raw_text):
@@ -3239,6 +3239,8 @@ class MessageMonitor:
ocr_lines = []
# ⭐ 2026-08-11: 被")昵称 "前缀剥离的行 → 短句检查豁免(真实用户消息)
_ui_prefix_stripped = set()
# ⭐ 2026-08-18: @提及剥离后的真实用户消息余量("@游戏专用关于我们"→"关于我们"):
# 这些行是"用户@机器人提问",必须豁免规则3b/规则4/短句碎片,否则用户消息被当UI吞掉。
for l in raw_text.split('\n'):
l = re.sub(r'\s*(?:[A-Za-z0-9]~)+\s*$', '', l).strip()
# ⭐ Task #1476: 行内图片引用清理。
@@ -3247,6 +3249,13 @@ class MessageMonitor:
l = re.sub(r'!0?\s*\(?\s*img/[\w./\\-]+\)?', '', l, flags=re.IGNORECASE).strip()
if not l:
continue
# ⭐ 2026-08-18: 提及衍生豁免判定——行剥掉孤立 @/括号/分隔符后 ∈ 余量集合
# 则它是"用户@机器人"的真实消息(余量 = 剥离昵称后的正文证据)
_norm_mention = l.strip().lstrip('@()()').rstrip('@()(),,。.::;;')
_mention_derived = _norm_mention in _mention_remainders
if _mention_derived:
_ui_prefix_stripped.add(l)
logger.debug(f"[{platform}] OCR: @提及衍生消息(豁免UI/短句): {l[:60]!r}")
# ⭐ Task #1475: 单行 Markdown 跳过。
# 正常用户从不手动打 markdown(# 标题、|表格|、**加粗** 等)。
# 撞上 md 段落标记 → 跳过该行;没有 md 标记 → 正常收集。
@@ -3270,7 +3279,9 @@ class MessageMonitor:
# ⭐ Task #1476: QQ 非消息文本跳过。
# QQ 群聊截图中,OCR 会识别出图片引用、用户头衔/徽章、用户标签等
# 非消息内容,混入消息流后触发残片判定。整行跳过这些 UI 元素。
if self._is_qq_ui_element(l):
# ⭐ 2026-08-18: 提及衍生消息豁免——"@游戏专用关于我们"剥离后是"@关于我们",
# 规则3b会整行当UI杀;它是用户真实消息,跳过UI检查。
if not _mention_derived and self._is_qq_ui_element(l):
logger.debug(f"[{platform}] OCR: 跳过QQ UI元素: {l[:60]!r}")
continue
# ⭐ 规则4放行的 ")昵称 内容" 格式(2026-08-11):_is_qq_ui_element 规则4
@@ -3324,9 +3335,14 @@ class MessageMonitor:
_is_single_excl = len(rl.strip()) == 1 and re.match(r'^[嘿哈哦呃啊哎嗬]$', rl.strip())
_prev_is_single_excl = merged_lines and len(merged_lines[-1].strip()) == 1 \
and re.match(r'^[嘿哈哦呃啊哎嗬]$', merged_lines[-1].strip())
# ⭐ 2026-08-18: 短@噪声行(含@、无问号、中文≤4字,如"有人@拜")不并入上行——
# 它是OCR对@提及的误读残片,会被短句碎片拦截;并入会把上行用户消息污染。
_cjk_in_rl = sum(1 for c in rl if '\u4e00' <= c <= '\u9fff')
_at_noise_line = ('@' in rl) and not re.search(r'[??]', rl) and _cjk_in_rl <= 4
if merged_lines and not _SENT_END.search(merged_lines[-1]) \
and not _TS_START.match(rl) and not _TS_START.match(merged_lines[-1]) \
and not _is_bot_reply_line and not _is_single_excl and not _prev_is_single_excl:
and not _is_bot_reply_line and not _is_single_excl and not _prev_is_single_excl \
and not _at_noise_line:
merged_lines[-1] = merged_lines[-1] + ',' + rl
else:
merged_lines.append(rl)
@@ -3436,7 +3452,7 @@ class MessageMonitor:
msg = re.sub(r'\s+#[\u4e00-\u9fff][^\s#\n]{0,30}$', '', msg).strip()
if msg:
# ⭐ 剥离QQ用户昵称,防止昵称拼凑进消息文本导致查库关键词被压住
msg = self._strip_qq_nickname(msg)
msg, _ = self._strip_qq_nickname(msg)
# ⭐ 2026-08-15 消息全身检查:剥离所有昵称和联系方式,防止误触回复
msg = self._sanitize_message(msg)
# ⭐ 2026-08-15 修复:OCR合并多条消息时,拆分所有消息段
@@ -3521,7 +3537,7 @@ class MessageMonitor:
msg = re.sub(r'\s+#[\u4e00-\u9fff][^\s#\n]{0,30}$', '', msg).strip()
if msg:
# ⭐ 剥离QQ用户昵称,防止昵称拼凑进消息文本导致查库关键词被压住
msg = self._strip_qq_nickname(msg)
msg, _ = self._strip_qq_nickname(msg)
# ⭐ 2026-08-15 消息全身检查:剥离所有昵称和联系方式,防止误触回复
msg = self._sanitize_message(msg)
# ⭐ 2026-08-15 修复:OCR合并多条消息时,拆分所有消息段
@@ -4967,7 +4983,7 @@ class MessageMonitor:
return i
return -1
def _strip_qq_nickname(self, text: str) -> str:
def _strip_qq_nickname(self, text: str):
"""从消息文本中剥离QQ用户昵称,防止昵称拼凑进消息文本导致查库关键词被压住。
QQ群聊截图中,OCR会把用户昵称和消息文本拼在一起,如:
@@ -4978,9 +4994,13 @@ class MessageMonitor:
1. 前缀匹配(优先):处理常见的粘连格式("昵称+消息"无分隔符)
2. 全局匹配(兜底):处理昵称隐藏在文本中间的情况
3. 分隔符清理:移除昵称后残留的标点/空格
返回 (剥离后文本, mention_remainders集合):
mention_remainders = QQ @提及行剥离昵称后的余量(如"@游戏专用关于我们"→"关于我们")。
下游规则3b/规则4/短句碎片用它识别"用户@机器人提问"的真实消息,避免误当UI元素吞掉。
"""
if not text or not text.strip():
return text
return text, set()
ts = text.strip()
logger.debug(f"[qq] _strip_qq_nickname输入: {ts[:80]!r}")
@@ -4997,6 +5017,31 @@ class MessageMonitor:
"游戏专用",
]
# ⭐ 2026-08-18: 先收集@提及行剥离后的余量(真实用户消息证据)
# "那关于我们@游戏专用" → "那关于我们";"@游戏专用关于我们" → "关于我们";
# ")游戏专用关于我们"(OCR把@误读成))→ "关于我们";行首裸昵称"游戏专用关于我们"→"关于我们"
# ⚠ 纯提及无正文("@游戏专用"余量为空)不入集合——那才是真正的UI元素,仍需规则3b拦截
_mention_remainders = set()
for _line in ts.split('\n'):
for _nick in sorted(nicknames, key=len, reverse=True):
_search_from = 0
while True:
_pos = _line.find(_nick, _search_from)
if _pos < 0:
break
_before = _line[_pos - 1] if _pos > 0 else ''
_after = _line[_pos + len(_nick)] if _pos + len(_nick) < len(_line) else ''
# ⚠ 注意:'' in '任意串' 恒为 True!必须用 (_before and _before in ...) 短路
_is_mention = _pos == 0 or (_before and _before in '@))((') \
or (_after and _after in '@))((')
if _is_mention:
_s = _pos - 1 if (_before and _before in '@))((') else _pos
_e = _pos + len(_nick) + 1 if (_after and _after in '@))((') else _pos + len(_nick)
_rem = (_line[:_s] + _line[_e:]).strip(' @))((,,。.;;::')
if len(_rem) >= 2:
_mention_remainders.add(_rem)
_search_from = _pos + len(_nick)
qq_title_pattern = re.compile(r'^[\[【][^】\]]+[】\]]\s*(?:[A-Za-z0-9_\u4e00-\u9fff]{2,30})[,,]?\s*')
_rank_pattern = re.compile(r'^[((][^))]+[))]\s*')
# ⭐ Task #1508: 前导垃圾不再吞噬句末标点 !!?? —— OCR 剥离@昵称后残留的
@@ -5102,14 +5147,23 @@ class MessageMonitor:
# 但有时前缀匹配失败,需要全局扫描
# ⭐ 2026-08-16: 添加OCR容错匹配——当精确匹配失败时,允许1字符差异
# 例:OCR把"草原探险服务器备用号"误识别为"草原深险服务器备用号"
# ⭐ 2026-08-18: 紧邻提及符(@ ) ( ( ))一并剥除——OCR把QQ @提及读成
# "那关于我们@游戏专用" / ")游戏专用关于我们" 等格式,只剥昵称会残留
# 孤立的 @/) 字符,导致下方规则3b/规则4把这些真实用户消息当UI元素误杀。
if not changed: # 前缀匹配无变化时才进行全局匹配
# 按长度降序排序,优先匹配长昵称(避免短前缀误匹配)
for nick in sorted(nicknames, key=len, reverse=True):
# 先尝试精确匹配
start = ts.find(nick)
if start >= 0:
ts = (ts[:start] + ts[start + len(nick):]).lstrip()
logger.debug(f"[qq] 全局匹配剥除昵称 {nick!r} @ [{start}:{start + len(nick)}]")
# 连同紧邻的提及符一起剥(@昵称 / 昵称@ / )昵称 / (昵称)
_e = start + len(nick)
if start > 0 and ts[start - 1] in '@))((':
start -= 1
elif _e < len(ts) and ts[_e] in '@))((':
_e += 1
ts = (ts[:start] + ts[_e:]).lstrip()
logger.debug(f"[qq] 全局匹配剥除昵称 {nick!r} @ [{start}:{_e}]")
changed = True
break
@@ -5120,8 +5174,13 @@ class MessageMonitor:
fuzzy_start = self._fuzzy_find(ts, nick, max_edits=1)
if fuzzy_start >= 0:
fuzzy_match = ts[fuzzy_start:fuzzy_start + len(nick)]
ts = (ts[:fuzzy_start] + ts[fuzzy_start + len(nick):]).lstrip()
logger.debug(f"[qq] 模糊匹配剥除OCR误识别昵称 {nick!r} -> {fuzzy_match!r} @ [{fuzzy_start}:{fuzzy_start + len(nick)}]")
_e = fuzzy_start + len(nick)
if fuzzy_start > 0 and ts[fuzzy_start - 1] in '@))((':
fuzzy_start -= 1
elif _e < len(ts) and ts[_e] in '@))((':
_e += 1
ts = (ts[:fuzzy_start] + ts[_e:]).lstrip()
logger.debug(f"[qq] 模糊匹配剥除OCR误识别昵称 {nick!r} -> {fuzzy_match!r} @ [{fuzzy_start}:{_e}]")
changed = True
break
@@ -5196,10 +5255,10 @@ class MessageMonitor:
cleaned = text.strip()
if len(cleaned) <= 1 or cleaned == '@':
logger.debug(f"[qq] 原文本身就是无意义字符,返回空: {cleaned!r}")
return ''
return '', _mention_remainders
logger.warning(f"[qq] 剥离后为空,回退原文防消息丢失: {cleaned!r}")
return cleaned
return ts.strip()
return cleaned, _mention_remainders
return ts.strip(), _mention_remainders
def _sanitize_message(self, text: str) -> str:
"""消息入队前的全身检查:剥离昵称和联系方式,防止误触回复。