diff --git a/monitor.py b/monitor.py index 0b8091d..03885f7 100644 --- a/monitor.py +++ b/monitor.py @@ -4465,24 +4465,48 @@ class MessageMonitor: EasyOCR 常把横向相邻的两条消息(用户问句 + 群昵称/UI文字/其他人消息) 检测为同一行文本,导致"兑换码导入错误时如何处置?游戏专用小筑憩-草一"式合并。 - 启发式: 在[。?!?]后紧跟CJK字符的位置拆分,前提是两侧都有足够中文。 - 不拆分续接句(第二段以"但/而/并/且/或/还/也/又/就/才/再/变"开头)。 + ⭐ 2026-08-13 修复:不再因为句号/问号就强制拆分。 + 语音输入或 casual typing 中,用户可能用"。"、"?"作为语气停顿, + 如"PHP,的那个管理员密码"、"服务器。最近的公告信息?" + 如果机械地在标点后拆分,会把同一条消息拆成多条碎片, + 导致 bot 只回复其中一段,另一段被丢弃。 + + 启发式规则: + 1. 仅在检测到【新昵称/头衔】出现在标点之后时才拆分 + (如"问个问题。草原探险服务器备用号@游戏专用那服务器...") + 2. 如果标点后的文本是续接内容(以"那/这/我/你"等代词开头),不拆分 + 3. 最多拆 1 次(2段),防止过度拆分 """ if len(text) < 10: return [text] _continuations = {'但', '而', '并', '且', '或', '还', '也', '又', '就', '才', '再', '变', '便', '则', '却', '虽', - '因', '为', '与', '和', '同', '跟'} + '因', '为', '与', '和', '同', '跟', + # ⭐ 新增:这些代词开头通常是续接,不应拆分 + '那', '这', '我', '你', '他', '她', '它', + '谁', '什么', '怎么', '如何', '哪', '哪', + # ⭐ 新增:时间/程度副词开头,通常是续接(如"最近"、"刚才") + '最', '刚', '还', '又', '也', '便', '即', '则'} + + # ⭐ 新增:昵称模式,用于判断是否应该拆分 + # 检测[头衔]昵称或@昵称的模式 + _nick_pattern = re.compile( + r'(?:[\[【][^】\]]+[】\]]\s*|[①②③④⑤⑥⑦⑧⑨⑩]?\s*)' + r'([A-Za-z0-9_\u4e00-\u9fff]{2,20})' + r'\s*[@@]?\s*' + ) + result = [] remaining = text - max_splits = 2 # 最多拆 2 次(3 段),防过度拆分 + max_splits = 1 # ⭐ 最多拆 1 次(2段),防止过度拆分 while max_splits > 0: # 找[。?!?]后紧跟CJK的位置 m = re.search(r'([。?!?])([\u4e00-\u9fff])', remaining) if not m: break + split_pos = m.start() + 1 # 标点后 first = remaining[:split_pos] second = remaining[split_pos:] @@ -4493,15 +4517,23 @@ class MessageMonitor: if first_cjk < 3 or second_cjk < 3: break - # 第二段不以续接词开头 + # ⭐ 第二段不以续接词开头 → 不拆 if second[0] in _continuations: break - # 第二段太短(少于 3 CJK)→ 不拆 - first_part = first - second_part = second - result.append(first_part) - remaining = second_part + # ⭐ 检查第二段是否以【新昵称/头衔】开头 + # 如果是,说明是新消息开始,可以拆分 + # 如果不是,说明是续接内容,不拆分 + has_nick = bool(_nick_pattern.match(second.strip())) + if not has_nick: + # 没有检测到新昵称,且不是续接词开头,检查是否是普通对话续接 + # 如果第二段长度<30且包含疑问词,可能是同一条消息的续问,不拆分 + if len(second) < 50 and any(qw in second for qw in ['?', '?', '吗', '呢', '多少', '怎么', '如何']): + break + + # 通过所有检查,执行拆分 + result.append(first) + remaining = second max_splits -= 1 result.append(remaining) @@ -4699,14 +4731,15 @@ class MessageMonitor: # 如 "C4炸弹起来准备测试了,[专业皮肤定制] C4炸弹,[专业皮肤定制] C4炸弹,youpaishidifu关于我们" # 策略:找到所有QQ昵称分隔符位置,取最后一个分隔符之后的文本(最新用户消息) - # 模式1: [头衔]用户名 后跟逗号/冒号(QQ群头衔格式) - _mid_qq_title = re.compile(r'[\[【][^】\]]+[】\]]\s*(?:[A-Za-z0-9_\u4e00-\u9fff]{2,30})\s*[,,::]') - # 模式2: 已知裸昵称 后跟逗号/冒号(无头衔格式) + # 模式1: [头衔]用户名 后跟逗号/冒号/@(QQ群头衔格式) + _mid_qq_title = re.compile(r'[\[【][^】\]]+[】\]]\s*(?:[A-Za-z0-9_\u4e00-\u9fff]{2,30})\s*[,,::@]') + # 模式2: 已知裸昵称 后跟逗号/冒号/@(无头衔格式) + # ⭐ 2026-08-13 扩展:增加@匹配,支持"昵称@其他用户"式拼接 _mid_bare_nick = None if nicknames: # 构造已知昵称的转义正则 _escaped = [re.escape(n) for n in nicknames] - _mid_bare_nick = re.compile(r'(?:' + '|'.join(_escaped) + r')\s*[,,::]') + _mid_bare_nick = re.compile(r'(?:' + '|'.join(_escaped) + r')\s*[,,::@]') # 找到最后一个分隔符位置,截取其后的文本(最新消息) last_sep_end = -1 @@ -4727,6 +4760,27 @@ class MessageMonitor: # 清理残留的前导标点/空格 ts = _leading_junk.sub('', ts) logger.info(f"[qq] 群聊拆分: 取最后分隔符后的文本,长度={len(ts)}") + # ⭐ 2026-08-13 修复:mid-text 剥离后,结果可能以另一个昵称开头 + # 如 "草原探险服务器备用号@游戏专用那我问你个事儿" → 剥离"草原探险服务器备用号@" + # 后得到 "游戏专用那我问你个事儿","游戏专用" 仍是已知昵称,需继续剥离 + while True: + _leading_junk.sub('', ts) + if not ts: + break + # 尝试剥开头裸昵称 + _stripped = False + for nick in nicknames: + if ts.startswith(nick): + ts = ts[len(nick):].lstrip() + _stripped = True + break + # 尝试剥 @昵称 + m_at = _at_user_pattern.match(ts) + if m_at: + ts = ts[m_at.end():].lstrip() + _stripped = True + if not _stripped: + break logger.debug(f"[qq] _strip_qq_nickname输出: {ts.strip()[:80]!r}") # ⭐ Task #1508 防御兜底:剥离后为空但输入非空 → 回退原文