refactor: md checkbox 一刀切,不再枚举符号

用户从不手打 markdown,短方括号结构才是稳定特征:
- [√] [x] [ ] [⚠] [✔] [V] 及 OCR 变体 [ √ ]、全角【√】全部覆盖
- 不再枚举具体勾选符号——OCR 偏差永远撞不全符号
- 多字词方括号([钻石] [VIP])绝不命中,保护用户正文
- \s*.?\s* = 空或单字符,md checkbox 的本质特征

改动点:
1. monitor.py _MD_TAIL_PATTERNS: checkbox 模式改一刀切 + 补全角【】
2. monitor.py _is_markdown_document: checkbox 检测同步放宽
3. monitor.py OCR 过滤链: checkbox残片/多checkbox/计数/行首菜单 4处同步
4. auto_reply.py _clean_markdown: checkbox 移除改一刀切,(?!\() 保护链接

策略(用户定):md在头→整段跳过;在中间→看语义(C#负向前瞻已挡);在尾→一刀切
This commit is contained in:
youpaishidifu
2026-08-17 21:55:28 +08:00
parent 4005369492
commit f8366798f6
2 changed files with 34 additions and 22 deletions
+6 -5
View File
@@ -622,11 +622,12 @@ class AutoReply:
# 移除列表语法:- item → item
text = re.sub(r'^[\-\*]\s+', '', text, flags=re.MULTILINE)
# ⭐ Task #1549: 移除 checkbox 标记:[√] GitHub(稳定版) → GitHub(稳定版)
# KB 中的下载选项列表使用 checkbox 格式,如 "- [ √ ] GitHub(稳定版)"
# 清理后应返回 "GitHub(稳定版)" 而非 "[ √ ] GitHub(稳定版)"
# 支持: [√] [✗] [X] [x] [V] [v] [ ] [⚠] [!]
text = re.sub(r'\[\s*[√✗XxVv⚠! ]?\s*\]\s*', '', text)
# ⭐ Task #1549+ 一刀切:md checkbox 内容=单符号或空白([√] [x] [ ] [⚠]),
# 直接剥掉。不再枚举具体符号——OCR 偏差永远撞不全符号,
# 单字符方括号结构才是稳定特征([ √ ] 带空格变体也覆盖)。
# \s*.?\s* = 空 或 单字符;多字词 [钻石] [VIP] 不剥,保护正文内容。
# (?!\() 保护 md 链接 [text](url):keep_links 模式下不误伤链接体。
text = re.sub(r'\[\s*.?\s*\](?!\()\s*', '', text)
# 移除加粗语法:**text** → text
text = re.sub(r'\*\*(.+?)\*\*', r'\1', text)
+28 -17
View File
@@ -3589,12 +3589,13 @@ class MessageMonitor:
if special_chars / len(line) > 0.15 and alpha_count >= 2 and digit_count >= 2:
logger.debug(f"[{platform}] OCR 过滤: 随机字符垃圾 line={line!r}")
continue
# checkbox 残片:`[V]` `[X]` `[√]` 开头,明显不是用户提问
if re.match(r'^\[[VX√✗]\s*\](?:\s*[A-Za-z0-9_\-./]+)?\s*$', line):
# ⭐ 一刀切 checkbox 残片:单字符方括号 = md checkbox([V]/[X]/[√]/[ ]),
# 不是用户提问(用户方括号是多字词 [钻石] [VIP],此处不命中)
if re.match(r'^\[\s*.?\s*\](?!\()(?:\s*[A-Za-z0-9_\-./]+)?\s*$', line):
logger.debug(f"[{platform}] OCR 过滤: checkbox残片 line={line!r}")
continue
# 多 checkbox 碎片:一行含 2+ 个 [V] 标记,md 列表 OCR 残留
if len(re.findall(r'\[\s*[VXvx√✗]\s*\]', line)) >= 2:
# 多 checkbox 碎片:一行含 2+ 个单字符方括号,md 列表 OCR 残留
if len(re.findall(r'\[\s*.?\s*\](?!\()', line)) >= 2:
logger.debug(f"[{platform}] OCR 过滤: 多checkbox碎片 line={line!r}")
continue
# 全角括号 md 链接碎片:`[text](url` 或 `](` — md 链接 OCR 全角退化
@@ -3610,8 +3611,8 @@ class MessageMonitor:
# 提取不重复的中文词(避免"稳定版稳定版稳定版"重复计数)
_content_words = list(set(re.findall(r'[\u4e00-\u9fff]{2,}', line)))
_question_marks = '?' in line or '?' in line
# 统计checkbox模式数量(如[ ]、[√])
_checkbox_count = len(re.findall(r'\[\s*[√✗XvV ]?\s*\]', line))
# 统计checkbox模式数量(单字符方括号,如[ ]、[√],一刀切不枚举符号)
_checkbox_count = len(re.findall(r'\[\s*.?\s*\](?!\()', line))
# 统计平台名出现次数
_platform_count = len(re.findall(r'\b(?:github|gitee|gitcode)\b', line, re.I))
# 判定规则:
@@ -4001,8 +4002,11 @@ class MessageMonitor:
if _BOT_MENU_LABELS.search(line):
logger.debug(f"[{platform}] OCR 过滤: bot回复菜单(版本标签) line={line!r}")
continue
# checkbox 标记:[ √ ] [ ⚠ ] [ ✗ ] [ X ] [ √] 等(OCR可能漏空格/括号)
if re.search(r'[\[【]\s*[√⚠✗×XvV✓✔]\s*[\]】]', line, re.IGNORECASE):
# ⭐ 一刀切 checkbox 标记(行首):md checkbox 在头部 → 整段跳过。
# 单字符方括号(含全角【】变体)= md checkbox,[ √ ] [ ⚠ ] [ ✗ ] [ X ] 等
# [钻石] [VIP] 等多字词绝不命中,保护用户正文;
# 中间位置 → 交给 _MD_TAIL_PATTERNS 切尾巴,不整行丢(用户策略:md在头整段跳,在尾一刀切)
if re.match(r'^[\[【]\s*.?\s*[\]】](?!\()', line, re.IGNORECASE):
logger.debug(f"[{platform}] OCR 过滤: bot回复菜单(checkbox标记) line={line!r}")
continue
# 纯 checkbox 行:OCR 把 "[ √ ]" 读成 "√ " 或 "√"(无括号)
@@ -6275,11 +6279,11 @@ class MessageMonitor:
return '\n'.join(cleaned)
# —— md 尾巴剥离模式 ——
# ⭐ 2026-08-11:QQ 群截图 OCR 会把 bot 的 md 回复残片拼在用户消息尾部,
# 如 "群管理员其实都是傲娇小南...(头衔UI)...# #来测试# # @C418minecraf"。
# 历史做法整条判 md 丢弃 → 用户原始提问陪葬;若放行 → md 尾巴入库,
# 可能撞知识库关键词把 bot 拽去回复残片内容,脱离用户原始提问。
# —— md 尾巴剥离模式(一刀切)——
# ⭐ 2026-08-17 原则:用户聊天从不手打 markdown(- [ ] 列表、# 标题、**加粗**、
# [x](url) 链接、| 表格、` 代码),任何 md 特有结构都可安全一刀切。
# OCR 偏差(如 [ √ ] 带空格、[✔]、【√】、[x、[ ])会让"逐个符号匹配"漏网,
# 所以 checkbox 用"任意短方括号"结构匹配,而非枚举具体勾选符号。
# 规则:md 在末尾 → 切掉小尾巴保留前段;md 大头/混中间 → 整段切掉。
_MD_TAIL_PATTERNS = [
# # 标题(## 文档 / ##直接跟汉字 / # # 分割 / ###1.)
@@ -6290,8 +6294,14 @@ class MessageMonitor:
# [text](url) 及 OCR 退化 [text](、](
re.compile(r'\[.+?\]\([^)]*\)?'),
re.compile(r'\]('),
# OCR 退化 checkbox [V]/[x]/[√]
re.compile(r'\[\s*[VXvx√✗]\s*\]'),
# ⭐ 一刀切:md checkbox 内容=单符号或空白([√] [x] [ ] [⚠]),
# 用户游戏名词方括号是多字词([钻石] [VIP])绝不命中。
# \s*.?\s* = 空 或 单字符(√✗xXVv⚠!✔ 等任意符号,OCR 变体如 [ √ ] 也覆盖)
# 不再枚举具体符号——OCR 偏差永远撞不全符号,单字符结构才是稳定特征。
# 负向前瞻 (?!\() 保护 md 链接 [text](url) 不被算作 checkbox。
re.compile(r'\[\s*.?\s*\](?!\()\s*(?=\(|[\u4e00-\u9fffA-Za-z]|$)'),
# 全角 checkbox:OCR 把 [√] 读成 【√】(内容仍为单符号或空白)
re.compile(r'【\s*.?\s*】\s*(?=\(|[\u4e00-\u9fffA-Za-z]|$)'),
# - 列表 / 编号列表 / > 引用(行内出现,前有空白或行首)
re.compile(r'(?:^|\s)[-*+]\s+(?=\S)'),
re.compile(r'(?:^|\s)\d{1,3}[\.\)、]\s+(?=\S)'),
@@ -6362,8 +6372,9 @@ class MessageMonitor:
if re.search(r'\[.+?\](', text):
logger.debug(f"[ocr] 过滤器3结果: 检测到全角括号退化链接模式2,返回True")
return True
# OCR 退化 md checkbox:短文本也检测([V] / [x])
if re.search(r'\[\s*[VXvx√✗]\s*\]', text):
# OCR 退化 md checkbox:短文本也检测([V] / [x] / [ √ ] / [ ])
# ⭐ 一刀切:单字符方括号即 md checkbox 特征(用户聊天方括号是多字词 [钻石])
if re.search(r'\[\s*.?\s*\](?!\()', text):
logger.debug(f"[ocr] 过滤器3结果: 检测到OCR退化checkbox,返回True")
return True
# OCR 退化 md 链接缺括号:短文本也检测