新增编程格式块剥离函数,头部整段跳过,尾部切掉(含兼容格式匹配)

- 新增 _strip_programming_format_blocks 函数,检测消息头部/尾部的JSON、YAML、Markdown表格等编程格式块
- 头部检测:JSON对象、Markdown表格、YAML块 → 整段丢弃
- 尾部检测:JSON对象、Markdown表格、YAML块 → 切掉尾部保留前段
- 兼容格式匹配:检测不完整JSON片段(中英文双引号包裹键值对,以逗号/括号结尾),即使OCR识别不完整也能拦截
- 在过滤管道中位于 _strip_json_yaml_blocks 之后调用
- 修复Markdown表格和JSON配置通过过滤器触发自动回复的问题
This commit is contained in:
youpaishidifu
2026-08-21 12:34:45 +08:00
parent f337ad1df5
commit dac243dcc0
+173
View File
@@ -6501,6 +6501,179 @@ class MessageMonitor:
return '\n'.join(cleaned)
@staticmethod
def _strip_programming_format_blocks(raw_text: str, platform: str = "") -> str:
"""剥离消息头部/尾部的编程格式块(JSON、YAML、Markdown表格等)。
用户要求:所有JSON、YAML、Markdown等编程语言格式,如果在消息头部,则整段跳过不处理;
如果在尾部,则一刀切掉尾部。
策略:
1. 检测消息头部(去除空行后)是否为JSON对象、YAML块、Markdown表格。
如果是,整段丢弃(返回空字符串)。
2. 如果不是头部,则检测消息尾部是否包含这些格式。
如果是,切掉尾部(从格式开始到末尾),返回前段。
3.否则原样返回。
安全边界:
- 用户聊天不会以 { 开头(JSON),也不会以 | 表格行开头(Markdown表格)。
- YAML 块以 key: value 开头,用户聊天不会这样开头。
- 因此头部检测误杀风险低。
"""
if not raw_text or not raw_text.strip():
return raw_text
lines = raw_text.split('\n')
# 去除空行,得到非空行列表
non_empty_lines = [line.strip() for line in lines if line.strip()]
if not non_empty_lines:
return raw_text
# —— 兼容格式:检测不完整JSON片段(中英文双引号包裹键值对,以逗号/括号结尾)——
def _is_json_fragment(line: str) -> bool:
"""检测单行是否为JSON片段(键值对模式),即使不完整。"""
# 匹配 "key": "value", 或 "key": value} 或 "key": "value"]
# 支持中英文双引号,键和值都可能被引号包裹,也可能没有
pattern = r'["\u201c\u201d].*?["\u201c\u201d]\s*:\s*["\u201c\u201d].*?["\u201c\u201d]\s*[,}\])]'
if re.search(pattern, line):
return True
# 也匹配不完整情况: "key": "value" 后缺少闭合引号,但有逗号/括号
# 例如:"text":"test", 或 "text":"test}
pattern2 = r'["\u201c\u201d].*?["\u201c\u201d]\s*:\s*["\u201c\u201d].*?[,}\])]'
if re.search(pattern2, line):
return True
# 匹配键没有引号的情况: key: "value", 或 key: value}
pattern3 = r'^[a-zA-Z_][\w]*\s*:\s*["\u201c\u201d].*?["\u201c\u201d]\s*[,}\])]'
if re.search(pattern3, line):
return True
return False
# —— 头部检测 ——
# 如果非空行首行以 { 开头,可能为JSON对象
first_line = non_empty_lines[0]
if first_line.startswith('{'):
# 尝试检测完整的JSON对象(大括号深度归零)
depth = 0
json_end_index = -1
for idx, line in enumerate(non_empty_lines):
depth += line.count('{') - line.count('}')
if depth <= 0:
json_end_index = idx
break
if json_end_index >= 0:
# 找到完整JSON对象,整段丢弃
logger.debug(f"[{platform}] 编程格式剥离: 头部JSON对象,整段丢弃: {first_line[:60]!r}")
return ''
# 兼容格式:如果首行是JSON片段(即使不完整),整段丢弃
if _is_json_fragment(first_line):
logger.debug(f"[{platform}] 编程格式剥离: 头部JSON片段(兼容),整段丢弃: {first_line[:60]!r}")
return ''
# 如果非空行首行以 | 开头,可能为Markdown表格
if first_line.startswith('|'):
# 检测至少两行以 | 结尾(表格特征)
table_lines = 0
for line in non_empty_lines[:5]: # 只检查前5行
if line.endswith('|'):
table_lines += 1
if table_lines >= 2:
logger.debug(f"[{platform}] 编程格式剥离: 头部Markdown表格,整段丢弃: {first_line[:60]!r}")
return ''
# 如果非空行首行匹配YAML键值对模式(key: value)
if re.match(r'^[a-zA-Z_][\w]*\s*:\s*', first_line) and \
not re.match(r'^(https?|ftp)://', first_line) and \
not re.match(r'^[a-zA-Z_][\w]*\s*:\s*/', first_line):
# 检测后续行是否有缩进或 - 列表项(YAML块特征)
yaml_block_lines = 1
for line in non_empty_lines[1:10]: # 检查后续9行
if re.match(r'^\s+', line) or re.match(r'^-\s+\S', line):
yaml_block_lines += 1
else:
break
if yaml_block_lines >= 2: # 至少2行符合YAML特征
logger.debug(f"[{platform}] 编程格式剥离: 头部YAML块,整段丢弃: {first_line[:60]!r}")
return ''
# —— 尾部检测 ——
# 从尾部向前扫描,找到编程格式开始的位置
last_line = non_empty_lines[-1]
# 尾部JSON对象:以 } 结尾,且前面有对应的 {
if last_line.endswith('}'):
#向前找到匹配的 {
depth = 0
json_start_index = -1
for idx in range(len(non_empty_lines)-1, -1, -1):
line = non_empty_lines[idx]
depth += line.count('}') - line.count('{')
if depth <= 0:
json_start_index = idx
break
if json_start_index >= 0 and json_start_index < len(non_empty_lines)-1:
# 切掉从json_start_index开始到末尾的所有行
cut_lines = lines[:json_start_index]
result = '\n'.join(cut_lines).strip()
if result:
logger.debug(f"[{platform}] 编程格式剥离: 尾部JSON对象,切掉尾部: {last_line[:60]!r}")
return result
else:
# 切完为空,说明整段都是JSON,丢弃
return ''
# 兼容格式:如果尾行是JSON片段(即使不完整),切掉尾部
if _is_json_fragment(last_line):
# 找到JSON片段开始的行(从尾部向上找第一个不是JSON片段的行)
json_start = len(non_empty_lines) - 1
while json_start > 0 and _is_json_fragment(non_empty_lines[json_start-1]):
json_start -= 1
# 切掉从json_start开始到末尾的所有行
cut_lines = lines[:json_start]
result = '\n'.join(cut_lines).strip()
if result:
logger.debug(f"[{platform}] 编程格式剥离: 尾部JSON片段(兼容),切掉尾部: {last_line[:60]!r}")
return result
else:
return ''
# 尾部Markdown表格:以 | 结尾,且前一行也以 | 结尾
if last_line.endswith('|') and len(non_empty_lines) >= 2 and non_empty_lines[-2].endswith('|'):
# 找到表格开始的行(从尾部向上找第一个不以 | 结尾的行)
table_start = len(non_empty_lines) - 1
while table_start > 0 and non_empty_lines[table_start-1].endswith('|'):
table_start -= 1
# 切掉从table_start开始到末尾的所有行
cut_lines = lines[:table_start]
result = '\n'.join(cut_lines).strip()
if result:
logger.debug(f"[{platform}] 编程格式剥离: 尾部Markdown表格,切掉尾部: {last_line[:60]!r}")
return result
else:
return ''
# 尾部YAML块:以 key: value 或 - item 结尾,且前面有缩进
if (re.match(r'^[a-zA-Z_][\w]*\s*:\s*', last_line) or re.match(r'^-\s+\S', last_line)) and \
len(non_empty_lines) >= 2:
# 向前找到YAML块开始(第一个不缩进且不匹配YAML特征的行)
yaml_start = len(non_empty_lines) - 1
while yaml_start > 0:
prev_line = non_empty_lines[yaml_start-1]
if re.match(r'^\s+', prev_line) or re.match(r'^[a-zA-Z_][\w]*\s*:\s*', prev_line) or re.match(r'^-\s+\S', prev_line):
yaml_start -= 1
else:
break
if yaml_start < len(non_empty_lines)-1:
cut_lines = lines[:yaml_start]
result = '\n'.join(cut_lines).strip()
if result:
logger.debug(f"[{platform}] 编程格式剥离: 尾部YAML块,切掉尾部: {last_line[:60]!r}")
return result
else:
return ''
# 无编程格式,原样返回
return raw_text
# —— md 尾巴剥离模式(一刀切)——
# ⭐ 2026-08-17 原则:用户聊天从不手打 markdown(- [ ] 列表、# 标题、**加粗**、
# [x](url) 链接、| 表格、` 代码),任何 md 特有结构都可安全一刀切。