fix: 跨轮语义去重+checkbox清理

Task #1548: 跨轮语义去重增强
- 三层策略: Jaccard≥0.75 → 核心词重叠≥0.80 → 子集检测≥0.90
- 新增 _extract_content_keywords 提取CJK bigram关键词
- 修复登录插件去哪儿下载等变体查询未被拦截问题

Task #1549: Markdown小尾巴清理
- _clean_markdown 新增 checkbox 标记移除
- 支持 [√] [✗] [X] [x] [V] [v] [ ] [⚠] [!] 格式
- 修复 bot回复末尾残留 [√] GitHub(稳定版) 问题
This commit is contained in:
youpaishidifu
2026-08-17 21:23:31 +08:00
parent ace66863f1
commit 4005369492
2 changed files with 65 additions and 2 deletions
+6
View File
@@ -622,6 +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)
# 移除加粗语法:**text** → text
text = re.sub(r'\*\*(.+?)\*\*', r'\1', text)
+59 -2
View File
@@ -5359,8 +5359,9 @@ class MessageMonitor:
几分钟后又发送相似查询(如"@服务器最近有哪些公告信息?"),
因OCR变体或细微措辞差异,被当作新查询再次处理。
方案:检查当前查询与最近5分钟内用户查询的 Jaccard 相似度,
若 >= 阈值(0.3)则视为语义重复,阻止回复。
方案:双层匹配——
1. Jaccard相似度(CJK bigram) >= 0.75 → 语义重复
2. 核心关键词完全一致 → 语义重复(兜底:Jaccard对"服务器"等修饰词敏感)
返回:True = 语义重复,应跳过;False = 新查询,可继续处理。
"""
@@ -5374,9 +5375,13 @@ class MessageMonitor:
if now - ts <= self._SEMANTIC_USER_DUP_TTL
]
# ⭐ 提取当前查询的核心关键词(去停用词后)
current_keywords = self._extract_content_keywords(text)
# 与最近查询比较
SIM_THRESHOLD = 0.75 # Jaccard相似度阈值:>=0.75视为语义重复(提高防误杀)
for recent_text, recent_ts in self._recent_user_queries:
# 层1: Jaccard相似度
sim = self._semantic_similarity(text, recent_text)
if sim >= SIM_THRESHOLD:
logger.debug(
@@ -5385,8 +5390,60 @@ class MessageMonitor:
)
return True
# 层2: 核心关键词高重叠(兜底:Jaccard对"服务器"等修饰词敏感时)
# 例: "咱服务器那个登录插件去哪儿下载" vs "咱那个登录插件去哪儿下载"
# Jaccard=55%,但核心词{登录,插件,下载}高度重叠 → 视为重复
# 使用Szymkiewicz-Simpson系数: |A∩B| / min(|A|,|B|)
recent_keywords = self._extract_content_keywords(recent_text)
if current_keywords and recent_keywords and len(current_keywords) >= 2 and len(recent_keywords) >= 2:
overlap = len(current_keywords & recent_keywords)
min_size = min(len(current_keywords), len(recent_keywords))
overlap_ratio = overlap / min_size if min_size > 0 else 0
if overlap_ratio >= 0.80: # 80%重叠视为语义重复
logger.debug(
f"[{platform}] 用户查询语义去重: 核心词重叠({overlap_ratio:.0%}) "
f"'{text[:40]!r}' ~ '{recent_text[:40]!r}'"
)
return True
# 层3: 核心词子集检测(兜底:q2 vs q4 大小写差异时 overlap 偏低)
# 例: "登录插件儿下载" vs "登录插件下载"("儿"是OCR/口语变体)
# 检查过滤后文本是否为对方子串,或核心词bigram是否为对方子集
if current_keywords and recent_keywords and len(current_keywords) >= 3 and len(recent_keywords) >= 3:
# 核心词子集:小集合的bigram是否大部分在大集合中
smaller, larger = (current_keywords, recent_keywords) if len(current_keywords) <= len(recent_keywords) else (recent_keywords, current_keywords)
subset_ratio = len(smaller & larger) / len(smaller) if smaller else 0
if subset_ratio >= 0.90: # 90%子集视为语义重复
logger.debug(
f"[{platform}] 用户查询语义去重: 核心词子集({subset_ratio:.0%}) "
f"'{text[:40]!r}' ~ '{recent_text[:40]!r}'"
)
return True
return False
def _extract_content_keywords(self, text: str) -> frozenset:
"""提取文本的核心内容关键词(CJK bigram,去停用词)。
用于跨轮去重的关键词匹配兜底:当Jaccard相似度因修饰词差异偏低时,
核心关键词一致仍可判定为重复。
例: "咱服务器那个登录插件去哪儿下载啊" → frozenset{"登录", "插件", "下载", ...}
"""
if not text:
return frozenset()
# 提取CJK字符
cjk = ''.join(c for c in text if '\u4e00' <= c <= '\u9fff')
# 去除标点
while cjk and cjk[-1] in self._SEMANTIC_TRIM_PUNCT:
cjk = cjk[:-1]
# 去除停用词单字
filtered = ''.join(c for c in cjk if c not in self._SEMANTIC_STOPWORDS)
if len(filtered) < 2:
return frozenset()
# 提取bigram作为关键词
return frozenset(filtered[i:i+2] for i in range(len(filtered) - 1))
def _add_to_recent_user_queries(self, text: str):
"""将当前用户查询添加到最近查询列表,用于跨轮语义去重。"""
if text and text.strip():