fix(monitor): 修复消息分割和昵称剥离问题
1. 句尾标点拆分优化(不再因句号/问号强制拆分) - 仅在检测到【新昵称/头衔】出现在标点后时才拆分 - 增加续接代词检查(那/这/我/你/他/最/刚等) - 减少到最多拆1次,防止过度拆分 2. 群聊多消息拆分逻辑增强 - mid-text 昵称分隔符扩展 @ 匹配(支持昵称@昵称格式) - mid-text 剥离后循环剥除残留开头的已知昵称 - 修复 草原探险服务器备用号@游戏专用 格式中第二个昵称残留问题
This commit is contained in:
+68
-14
@@ -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 防御兜底:剥离后为空但输入非空 → 回退原文
|
||||
|
||||
Reference in New Issue
Block a user