修复3个OCR过滤问题:
1. 时间戳过滤误判:昨天前缀剥离后裸时间戳被当未来时间拦截,添加skip_future_check传递 2. 纯时间戳触发查库:_on_new_message入口检查中文字符,无中文跳过查库 3. 偏旁库补全:添加U+4E28(丨),启用U+4E3F(丿)过滤
This commit is contained in:
@@ -7,6 +7,7 @@ import logging
|
||||
import queue
|
||||
import threading
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
|
||||
import customtkinter as ctk
|
||||
@@ -48,6 +49,10 @@ class MsgBotApp(ctk.CTk):
|
||||
|
||||
# 自动回复
|
||||
self.ar = AutoReply(self.monitor)
|
||||
|
||||
# ⭐ 跨平台消息处理锁:防止同一消息被QQ和微信同时处理
|
||||
self._processing_lock = threading.Lock()
|
||||
self._processing_messages = set() # 正在处理的消息hash集合
|
||||
self.ar.set_enabled(self.cfg.get("auto_reply_enabled", False))
|
||||
self.ar.set_min_score(self.cfg.get("auto_reply_min_score", 0.35))
|
||||
|
||||
@@ -301,60 +306,80 @@ class MsgBotApp(ctk.CTk):
|
||||
|
||||
def _on_new_message(self, platform: str, msg: str):
|
||||
"""收到新消息 -> 搜索知识库 -> 日志输出"""
|
||||
# ⭐ 纯时间戳/纯日期过滤:不含中文字符的消息不送查库
|
||||
# OCR 常把日期"2026/8 / 6"作为独立 part 传入,匹配度虚高但答非所问
|
||||
if not re.search(r'[\u4e00-\u9fff]', msg):
|
||||
self._log_queue.put(f"[{platform.upper()}] ⚠ 消息无中文字符,跳过查库: {msg[:40]}")
|
||||
return
|
||||
|
||||
if not self.kb.is_ready:
|
||||
self._log_queue.put(f"[{platform.upper()}] {msg}")
|
||||
self._log_queue.put(" ⚠ 知识库未加载,无法搜索答案")
|
||||
return
|
||||
|
||||
# ⭐ 跨平台互斥:同一消息只允许一个平台处理
|
||||
import hashlib
|
||||
msg_hash = hashlib.md5(msg.strip().encode('utf-8')).hexdigest()
|
||||
with self._processing_lock:
|
||||
if msg_hash in self._processing_messages:
|
||||
self._log_queue.put(f"[{platform.upper()}] ⚠ 消息已被其他平台处理,跳过: {msg[:40]}")
|
||||
return
|
||||
self._processing_messages.add(msg_hash)
|
||||
|
||||
# 在后台线程搜索
|
||||
threading.Thread(target=self._search_and_log,
|
||||
args=(platform, msg), daemon=True).start()
|
||||
args=(platform, msg, msg_hash), daemon=True).start()
|
||||
|
||||
def _search_and_log(self, platform: str, msg: str):
|
||||
def _search_and_log(self, platform: str, msg: str, msg_hash: str):
|
||||
"""搜索知识库并输出结果,匹配成功时队列自动回复到主线程"""
|
||||
results = self.kb.search(msg, top_k=3)
|
||||
lines = [f"[{platform.upper()}] {msg}"]
|
||||
if results:
|
||||
answer_title = results[0][0]["title"]
|
||||
answer_score = results[0][1]
|
||||
lines.append(f" 🔍 找到 {len(results)} 条相关结果:")
|
||||
for i, (doc, score) in enumerate(results):
|
||||
title = doc["title"]
|
||||
heading = doc.get("heading", "")
|
||||
body = doc.get("body", "")
|
||||
content = doc.get("content", "")
|
||||
# 显示更详细的信息
|
||||
lines.append(f" 📄 [{title}] (匹配度 {score:.2f})")
|
||||
lines.append(f" 标题: {heading[:60]}")
|
||||
lines.append(f" 正文片段: {body[:100].replace(chr(10), ' ')}...")
|
||||
if i == 0:
|
||||
lines.append(f" 完整内容片段: {content[:200].replace(chr(10), ' ')}...")
|
||||
try:
|
||||
results = self.kb.search(msg, top_k=3)
|
||||
lines = [f"[{platform.upper()}] {msg}"]
|
||||
if results:
|
||||
answer_title = results[0][0]["title"]
|
||||
answer_score = results[0][1]
|
||||
lines.append(f" 🔍 找到 {len(results)} 条相关结果:")
|
||||
for i, (doc, score) in enumerate(results):
|
||||
title = doc["title"]
|
||||
heading = doc.get("heading", "")
|
||||
body = doc.get("body", "")
|
||||
content = doc.get("content", "")
|
||||
# 显示更详细的信息
|
||||
lines.append(f" 📄 [{title}] (匹配度 {score:.2f})")
|
||||
lines.append(f" 标题: {heading[:60]}")
|
||||
lines.append(f" 正文片段: {body[:100].replace(chr(10), ' ')}...")
|
||||
if i == 0:
|
||||
lines.append(f" 完整内容片段: {content[:200].replace(chr(10), ' ')}...")
|
||||
|
||||
# 诊断日志:记录自动回复触发条件
|
||||
if not self.ar.enabled:
|
||||
lines.append(" ⚠ 自动回复已关闭,不触发回复")
|
||||
elif answer_score < self.ar.min_score:
|
||||
lines.append(f" ⚠ 匹配度 {answer_score:.2f} < 阈值 {self.ar.min_score},跳过")
|
||||
else:
|
||||
# 检查内容相关性
|
||||
has_relevance = self.ar._has_content_relevance(msg, results[0][0], answer_score)
|
||||
if not has_relevance:
|
||||
heading = results[0][0].get("heading", "")[:40]
|
||||
lines.append(f" ⚠ 内容相关性检查失败 (heading={heading!r}),跳过")
|
||||
# 诊断日志:记录自动回复触发条件
|
||||
if not self.ar.enabled:
|
||||
lines.append(" ⚠ 自动回复已关闭,不触发回复")
|
||||
elif answer_score < self.ar.min_score:
|
||||
lines.append(f" ⚠ 匹配度 {answer_score:.2f} < 阈值 {self.ar.min_score},跳过")
|
||||
else:
|
||||
lines.append(" ✅ 匹配通过,准备发送自动回复")
|
||||
# 检查内容相关性
|
||||
has_relevance = self.ar._has_content_relevance(msg, results[0][0], answer_score)
|
||||
if not has_relevance:
|
||||
heading = results[0][0].get("heading", "")[:40]
|
||||
lines.append(f" ⚠ 内容相关性检查失败 (heading={heading!r}),跳过")
|
||||
else:
|
||||
lines.append(" ✅ 匹配通过,准备发送自动回复")
|
||||
|
||||
# 自动回复:队列到主线程执行(daemon 线程不能可靠 SetForegroundWindow)
|
||||
reply_data = self.ar.get_reply_data(platform, msg, results[0][0], results[0][1])
|
||||
if reply_data:
|
||||
self._reply_queue.put(reply_data)
|
||||
# 自动回复:队列到主线程执行(daemon 线程不能可靠 SetForegroundWindow)
|
||||
reply_data = self.ar.get_reply_data(platform, msg, results[0][0], results[0][1])
|
||||
if reply_data:
|
||||
self._reply_queue.put(reply_data)
|
||||
else:
|
||||
lines.append(" ⚠ get_reply_data 返回 None(重复命中或内部检查失败)")
|
||||
else:
|
||||
lines.append(" ⚠ get_reply_data 返回 None(重复命中或内部检查失败)")
|
||||
else:
|
||||
lines.append(" ❌ 知识库中未找到相关答案")
|
||||
lines.append(" ❌ 知识库中未找到相关答案")
|
||||
|
||||
for line in lines:
|
||||
self._log_queue.put(line)
|
||||
for line in lines:
|
||||
self._log_queue.put(line)
|
||||
finally:
|
||||
# ⭐ 处理完成后从集合中移除消息hash
|
||||
with self._processing_lock:
|
||||
self._processing_messages.discard(msg_hash)
|
||||
|
||||
def _log(self, text: str):
|
||||
"""直接写日志(主线程)"""
|
||||
@@ -394,9 +419,16 @@ class MsgBotApp(ctk.CTk):
|
||||
platform = data["platform"]
|
||||
answer = data["answer"]
|
||||
sender = data.get("sender")
|
||||
msg = data.get("msg", "")
|
||||
max_retries = 3
|
||||
retry_delay = 5 # 秒
|
||||
|
||||
# ⭐ 发送前强制检查时间戳:≤300秒则发送,≥300秒则丢弃
|
||||
msg_age = self._check_message_freshness(msg)
|
||||
if msg_age is not None and msg_age > 300:
|
||||
self._log(f"[{platform}] ⚠ 消息时间戳过期({msg_age:.0f}秒>300秒),丢弃: {msg[:40]}")
|
||||
return
|
||||
|
||||
# 设发送锁,阻止监控轮询切走焦点
|
||||
self.monitor._sending_event.set()
|
||||
try:
|
||||
@@ -422,6 +454,52 @@ class MsgBotApp(ctk.CTk):
|
||||
finally:
|
||||
self.monitor._sending_event.clear()
|
||||
|
||||
def _check_message_freshness(self, msg: str) -> float or None:
|
||||
"""检查消息时间戳是否新鲜(≤300秒)。返回消息年龄(秒),无法解析返回None。"""
|
||||
import re
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# 尝试从消息中提取时间戳
|
||||
# 常见格式:HH:MM, YYYY-MM-DD HH:MM, MM-DD HH:MM
|
||||
patterns = [
|
||||
(r'(\d{1,2}):(\d{2})', 'time_only'), # HH:MM
|
||||
(r'(\d{4})-(\d{1,2})-(\d{1,2})\s+(\d{1,2}):(\d{2})', 'full'), # YYYY-MM-DD HH:MM
|
||||
(r'(\d{1,2})-(\d{1,2})\s+(\d{1,2}):(\d{2})', 'date_time'), # MM-DD HH:MM
|
||||
]
|
||||
|
||||
for pattern, fmt in patterns:
|
||||
match = re.search(pattern, msg)
|
||||
if match:
|
||||
now = datetime.now()
|
||||
try:
|
||||
if fmt == 'time_only':
|
||||
# 只有时间,假设是今天
|
||||
hour, minute = int(match.group(1)), int(match.group(2))
|
||||
msg_time = now.replace(hour=hour, minute=minute, second=0, microsecond=0)
|
||||
# 如果时间在未来,可能是昨天的消息
|
||||
if msg_time > now:
|
||||
msg_time -= timedelta(days=1)
|
||||
elif fmt == 'full':
|
||||
year, month, day = int(match.group(1)), int(match.group(2)), int(match.group(3))
|
||||
hour, minute = int(match.group(4)), int(match.group(5))
|
||||
msg_time = datetime(year, month, day, hour, minute)
|
||||
elif fmt == 'date_time':
|
||||
month, day = int(match.group(1)), int(match.group(2))
|
||||
hour, minute = int(match.group(3)), int(match.group(4))
|
||||
msg_time = now.replace(month=month, day=day, hour=hour, minute=minute, second=0, microsecond=0)
|
||||
# 如果时间在未来,可能是去年的消息
|
||||
if msg_time > now:
|
||||
msg_time = msg_time.replace(year=now.year - 1)
|
||||
|
||||
age = (now - msg_time).total_seconds()
|
||||
if age >= 0:
|
||||
return age
|
||||
except (ValueError, OverflowError):
|
||||
continue
|
||||
|
||||
# 无法解析时间戳,返回None(允许发送)
|
||||
return None
|
||||
|
||||
def _clear_log(self):
|
||||
self._log_text.configure(state="normal")
|
||||
self._log_text.delete("1.0", "end")
|
||||
|
||||
Reference in New Issue
Block a user