修复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")
|
||||
|
||||
+16
-6
@@ -2834,6 +2834,7 @@ class MessageMonitor:
|
||||
# —— 时间戳分割(跨行继承) ——
|
||||
expanded = []
|
||||
_inherited_ts = None # 跨行继承:上一行的时间戳,供当前行无时间戳时使用
|
||||
self._inherited_ts_from_past = False # 跨行继承:上一行时间戳是否来自过去日期前缀
|
||||
for i, rl in enumerate(ocr_lines):
|
||||
before = rl
|
||||
rl = self._normalize_ocr_text(rl)
|
||||
@@ -2843,6 +2844,8 @@ class MessageMonitor:
|
||||
logger.debug(f"[ocr] stage[{i}] clean: len={len(rl)} -> {rl[:120]!r}")
|
||||
rl = re.sub(r'\s+', ' ', rl)
|
||||
# 移除中文日期前缀(昨天、前天、星期X等),只保留数字时间
|
||||
# ⭐ 检测是否含有"昨天"等过去日期前缀:若有,后续时间戳年龄检查跳过未来时间拦截
|
||||
_had_past_date_prefix = bool(re.search(r'(昨天|前天|大前天|\d+天前)', rl))
|
||||
rl = re.sub(r'(昨天|前天|大前天|星期[一二三四五六日天]|\d+天前|今天|明天)\s*(?=\d{1,2}[::.;]\d{1,2})', '', rl)
|
||||
# 标准化 EasyOCR 时间戳:16.47 → 16:47, 15;39 → 15:39, 16:03.41 → 16:03:41
|
||||
rl = re.sub(r'(\d{1,2})[.;](\d{2})(?:[.;](\d{2}))?', r'\1:\2:\3', rl)
|
||||
@@ -2853,6 +2856,8 @@ class MessageMonitor:
|
||||
f"{[p[:60] for p in parts if p.strip()]}")
|
||||
buf = ''
|
||||
last_ts = _inherited_ts # 继承前一行的时间戳
|
||||
# ⭐ 继承的过去日期标记:若前一阶段有"昨天"等过去前缀,继承的时间戳也跳过未来检查
|
||||
_inherited_past = _had_past_date_prefix or getattr(self, '_inherited_ts_from_past', False)
|
||||
for part in parts:
|
||||
if re.match(r'^\d{1,2}:\d{1,2}', part):
|
||||
if buf.strip():
|
||||
@@ -2862,18 +2867,18 @@ class MessageMonitor:
|
||||
# ⚠ 不再整段丢弃:可能是被裁切/漏识别时间戳的新消息,
|
||||
# 交由下游 seen/_replied_texts 去重(基线轮会吸收历史)。
|
||||
logger.debug(f"[ocr] stage[{i}] 无前置时间戳归属,交由下游去重: {buf.strip()[:60]!r}")
|
||||
elif self._is_timestamp_old(last_ts, 300):
|
||||
elif self._is_timestamp_old(last_ts, 300, skip_future_check=_inherited_past):
|
||||
# ⚠ 修复漏消息:buf 无独立时间戳(时间戳已被 re.split 分离到 part),
|
||||
# 此处 last_ts 是"继承"的旧时间戳,无法据此判断 buf 年龄。
|
||||
# QQ/微信最新消息常无独立时间戳,仅因继承时间戳较老就丢弃会漏消息。
|
||||
# 不再按年龄丢弃,交由下游 seen 去重;仅当继承时间戳极老(>2h)且
|
||||
# 内容无新鲜特征时视为"翻看远古历史"丢弃,避免翻看旧记录被误回复。
|
||||
if self._is_timestamp_old(last_ts, 7200) and not _fresh:
|
||||
if self._is_timestamp_old(last_ts, 7200, skip_future_check=_inherited_past) and not _fresh:
|
||||
# ⭐ 提问豁免:消息本身是明确提问(以?/?结尾、含中文、非md/噪声),
|
||||
# 即使继承的时间戳极老(来自截图内其他内容如历史/文档时间戳),
|
||||
# 也应放行交由下游去重,避免把用户刚发的提问误判为远古历史永久丢弃。
|
||||
# ⚠ 限制:继承时间戳>24h的老消息不再豁免(截图内远古历史不应触发回复)
|
||||
if self._is_question_message(buf.strip()) and not self._is_timestamp_old(last_ts, 86400):
|
||||
if self._is_question_message(buf.strip()) and not self._is_timestamp_old(last_ts, 86400, skip_future_check=_inherited_past):
|
||||
logger.debug(f"[ocr] stage[{i}] 继承时间戳{last_ts}极老但消息为明确提问,放行交由下游去重: {buf.strip()[:60]!r}")
|
||||
else:
|
||||
logger.debug(f"[ocr] stage[{i}] 继承时间戳{last_ts}极老(>2h)且非新鲜内容,视为远古历史跳过: {buf.strip()[:60]!r}")
|
||||
@@ -2897,6 +2902,7 @@ class MessageMonitor:
|
||||
expanded.append(msg)
|
||||
buf = ''
|
||||
last_ts = part
|
||||
_inherited_past = False # 新时间戳重置过去日期标记
|
||||
else:
|
||||
buf += part
|
||||
if buf.strip():
|
||||
@@ -2910,11 +2916,11 @@ class MessageMonitor:
|
||||
logger.debug(f"[ocr] stage[{i}] 尾部无时间戳归属,交由下游去重: {buf.strip()[:60]!r}")
|
||||
else:
|
||||
_fresh_tail = self._looks_fresh_content(buf.strip())
|
||||
if last_ts and self._is_timestamp_old(last_ts, 300):
|
||||
if self._is_timestamp_old(last_ts, 7200) and not _fresh_tail:
|
||||
if last_ts and self._is_timestamp_old(last_ts, 300, skip_future_check=_inherited_past):
|
||||
if self._is_timestamp_old(last_ts, 7200, skip_future_check=_inherited_past) and not _fresh_tail:
|
||||
# ⭐ 提问豁免(同中段逻辑):明确提问即使是尾部、继承极老时间戳也放行
|
||||
# ⚠ 限制:继承时间戳>24h的老消息不再豁免
|
||||
if self._is_question_message(buf.strip()) and not self._is_timestamp_old(last_ts, 86400):
|
||||
if self._is_question_message(buf.strip()) and not self._is_timestamp_old(last_ts, 86400, skip_future_check=_inherited_past):
|
||||
logger.debug(f"[ocr] stage[{i}] 尾部时间戳{last_ts}极老但消息为明确提问,放行交由下游去重: {buf.strip()[:60]!r}")
|
||||
else:
|
||||
logger.debug(f"[ocr] stage[{i}] 尾部时间戳{last_ts}极老(>2h)且非新鲜内容,视为远古历史跳过尾部: {buf.strip()[:60]!r}")
|
||||
@@ -2944,6 +2950,10 @@ class MessageMonitor:
|
||||
# 更新跨行继承时间戳
|
||||
if last_ts:
|
||||
_inherited_ts = last_ts
|
||||
# ⭐ 若本阶段有过去日期前缀,继承的时间戳也标记为来自过去日期
|
||||
self._inherited_ts_from_past = _had_past_date_prefix
|
||||
else:
|
||||
self._inherited_ts_from_past = False
|
||||
|
||||
# —— 逐行过滤 ——
|
||||
ocr_lines = expanded
|
||||
|
||||
+5
-1
@@ -215,7 +215,11 @@
|
||||
| 齒 | - | 齿字旁 | 龄→齒令 | 龄 |
|
||||
| 龍 | - | 龙字旁 | 聋→龍耳 | 聋 |
|
||||
| 龜 | - | 龟 | 亀→龜 | 亀 |
|
||||
| 丿 | - | 撇 | 儿→丿 | 儿
|
||||
| 丿 | U+4E3F | 撇 | 儿→丿 | 儿 |
|
||||
| 丨 | U+4E28 | 竖 | 中→丨 | 中 |
|
||||
| 虱 | U+8671 | 虱 | 虱→搁 | 虱 |
|
||||
| 凵 | U+51F5 | 凵 | 凵→儿| 凵
|
||||
| 乍 | U+4E4D | 作 | 乍→作 | 乍
|
||||
|
||||
## 使用说明
|
||||
|
||||
|
||||
Reference in New Issue
Block a user