#1448 auto_reply: _has_content_relevance() 不再对≤2token短查询无条件放行 - 短查询必须整个query作为子串出现在文档中才放行 - 修复下载→前置依赖误匹配(无任何共同token/token交集) #1449 monitor: 严格300s报废策略 - stage splitting: 继承时间戳>300s一律丢弃,移除新鲜内容/提问豁免 - 区块级: 硬截止从1800s→300s,移除CJK内容/新鲜特征bypass - 修复16:07消息在21:08仍被处理的问题
This commit is contained in:
+12
-2
@@ -85,8 +85,9 @@ class AutoReply:
|
||||
"""
|
||||
import re
|
||||
q_tokens = set(t for t in jieba.cut(query) if len(t) >= 2)
|
||||
if len(q_tokens) <= 2:
|
||||
return True # 短查询不拦截
|
||||
# ⭐ 修复:短查询不再无条件放行,改为必须有子串命中才放行
|
||||
# 原逻辑导致"下载"(1 token)→"前置依赖"误匹配(无任何共同token却放行)
|
||||
_is_short_query = len(q_tokens) <= 2
|
||||
|
||||
# 1. 检查 heading + title(排除链接文本)
|
||||
heading = doc.get("heading", "")
|
||||
@@ -130,9 +131,18 @@ class AutoReply:
|
||||
# 3. 子串回退:jieba分词可能产生"包是""还是"等碎片token,
|
||||
# 导致token交集为空但文档确实包含查询核心词(如"资源包")。
|
||||
# 检查查询中任意2~6字子串是否出现在文档文本中。
|
||||
# ⭐ 短查询(≤2 token):检查整个query是否出现在文档中("下载"必须在文档中出现)
|
||||
# ⭐ 使用清理后的文本(排除链接文本)
|
||||
full_text = doc_text + " " + body_no_link[:500] if body else doc_text
|
||||
stripped_query = re.sub(r'[??!!。,,\s]', '', query)
|
||||
# 短查询特殊处理:整个query必须作为子串出现在文档中
|
||||
if _is_short_query and len(stripped_query) >= 2:
|
||||
if stripped_query in full_text:
|
||||
logger.debug(f"短查询子串命中: query={stripped_query!r} in doc={doc.get('heading','')[:30]!r}")
|
||||
return True
|
||||
else:
|
||||
logger.debug(f"短查询子串未命中: query={stripped_query!r} 不在文档中,拒绝")
|
||||
return False
|
||||
if len(stripped_query) >= 4:
|
||||
for qlen in range(min(len(stripped_query), 6), 1, -1):
|
||||
for start in range(len(stripped_query) - qlen + 1):
|
||||
|
||||
+136
-86
@@ -405,6 +405,11 @@ class MessageMonitor:
|
||||
# EasyOCR 子进程(GPU crash 隔离,崩溃只杀子进程不冻电脑)
|
||||
self._easyocr_proc: Optional['subprocess.Popen'] = None
|
||||
self._easyocr_broken: bool = False
|
||||
# ⭐ Task #1446:worker 是否已完成 ready 握手(就绪感知,避免未就绪即派发请求)
|
||||
self._easyocr_ready: bool = False
|
||||
self._easyocr_ready_event = threading.Event() # 握手完成信号,并发等待线程用
|
||||
self._easyocr_ready_timeout: float = 60.0 # 等待握手的最长预算(=初始化预算,不计入请求超时)
|
||||
self._easyocr_starting: bool = False # 是否有线程正在拉起+Popen 子进程
|
||||
# EasyOCR 子进程崩溃冷却:崩溃后 30s 内不重试(防快速崩溃循环刷屏)
|
||||
self._easyocr_retry_until: float = 0.0
|
||||
# EasyOCR 子进程调用计数:累积 GPU 内存泄漏,每 MAX 次主动重启清缓存
|
||||
@@ -445,6 +450,8 @@ class MessageMonitor:
|
||||
|
||||
# EasyOCR 子进程锁(QQ/微信独立线程共用同一子进程)
|
||||
self._easyocr_lock = threading.Lock()
|
||||
# ⭐ Task #1446:串行化子进程启动/Popen,防并发双 Popen 与「未就绪快路径」竞态
|
||||
self._easyocr_startup_lock = threading.Lock()
|
||||
# QQ 独立轮询线程(不阻塞微信主循环)
|
||||
self._qq_thread: Optional[threading.Thread] = None
|
||||
# 双击 ESC 急停线程
|
||||
@@ -502,6 +509,9 @@ class MessageMonitor:
|
||||
logger.info(f"[ocr] 软重启:保持上次EasyOCR状态(cuda_oom_count={self._easyocr_cuda_oom_count})")
|
||||
|
||||
self._easyocr_proc = None # 确保 clean state
|
||||
self._easyocr_ready = False
|
||||
self._easyocr_ready_event.clear()
|
||||
self._easyocr_starting = False
|
||||
# ⭐ OCR 状态清零:截图hash/垃圾计数/引擎选择/冷却等,确保重启不残留上一轮状态
|
||||
# (上次 CUDA 报错降级 CPU 的计数、超时计数、截图指纹都从头开始)
|
||||
self._last_screenshot_hash.clear()
|
||||
@@ -671,6 +681,10 @@ class MessageMonitor:
|
||||
logger.debug(f"{_tag}: 无可用诊断信息(worker 未留下状态文件/崩溃日志)")
|
||||
|
||||
self._easyocr_proc = None
|
||||
# ⭐ Task #1446:进程已销毁 → 清除就绪标志与握手信号,强制后续调用重新等待/握手
|
||||
self._easyocr_ready = False
|
||||
self._easyocr_ready_event.clear()
|
||||
self._easyocr_starting = False
|
||||
# 3) 主进程 GC:释放 PIL Image / numpy array 等残留引用
|
||||
try:
|
||||
gc.collect()
|
||||
@@ -1669,16 +1683,83 @@ class MessageMonitor:
|
||||
is_normal_restart = True
|
||||
self._easyocr_normal_restart = False # 重置标志
|
||||
logger.debug("[ocr] 检测到正常调度重启标志,跳过内存检测")
|
||||
if self._easyocr_proc is not None:
|
||||
if self._easyocr_proc.poll() is None:
|
||||
return True # 还在跑
|
||||
# ── Task #1446 修复:就绪感知 + 启动串行化 ─────────────────────
|
||||
# 旧快路径只看 poll() is None(进程存活),并发线程在初始化窗口抢到
|
||||
# 「进程已存活但未完成 ready 握手」的快路径 return True,导致在 worker
|
||||
# 未就绪时就派发 OCR 请求、触发 10s 请求超时("刚就绪即超时"竞态)。
|
||||
# 新逻辑:
|
||||
# 1) 快路径要求「进程存活【且】_easyocr_ready(已握手)」;
|
||||
# 2) 进程存活但未就绪 → 说明另一线程正在握手,等待握手事件(≤60s
|
||||
# 初始化预算),绝不提前派发请求;
|
||||
# 3) 需要启动时用 _easyocr_startup_lock 串行化 Popen,仅一个线程真正
|
||||
# 拉起,其余线程等待其握手结果。于是 _call_easyocr_safe 的 10s 请求
|
||||
# 计时只会在 worker 真正就绪后才开始。
|
||||
|
||||
# 快路径(稳态):进程存活且已完成 ready 握手
|
||||
if self._easyocr_proc is not None and self._easyocr_proc.poll() is None and self._easyocr_ready:
|
||||
return True
|
||||
|
||||
# 进程存活但未就绪:另一线程正在握手,等待其完成(≤60s),不重 Popen、不提前派发
|
||||
if self._easyocr_proc is not None and self._easyocr_proc.poll() is None and not self._easyocr_ready:
|
||||
logger.debug("[ocr] EasyOCR 子进程存活但握手未完成,等待就绪(≤60s)...")
|
||||
self._easyocr_ready_event.wait(timeout=self._easyocr_ready_timeout)
|
||||
if self._easyocr_proc is not None and self._easyocr_proc.poll() is None and self._easyocr_ready:
|
||||
return True
|
||||
# 等待超时或进程已死:清理卡住的未就绪子进程,落到重启逻辑
|
||||
if self._easyocr_proc is not None and self._easyocr_proc.poll() is None and not self._easyocr_ready:
|
||||
logger.warning("[ocr] 等待握手超时,杀掉未就绪子进程并重试")
|
||||
self._kill_easyocr_proc("握手等待超时")
|
||||
# 继续往下走重启
|
||||
|
||||
# 进程已退出(poll 非 None)→ 清理后走重启逻辑
|
||||
if self._easyocr_proc is not None and self._easyocr_proc.poll() is not None:
|
||||
logger.warning("[ocr] EasyOCR 子进程已退出,重启...")
|
||||
self._easyocr_proc = None
|
||||
self._easyocr_ready = False
|
||||
self._easyocr_ready_event.clear()
|
||||
|
||||
# 冷却期:子进程刚崩溃,30s 内不立即重试(防快速崩溃循环刷屏)
|
||||
if time.time() < self._easyocr_retry_until:
|
||||
return False
|
||||
|
||||
# 串行化启动:仅一个线程负责 Popen(防「proc 为 None」时并发双 Popen),
|
||||
# 其余线程等待其握手结果。Popen 在锁内完成;握手在锁外执行,避免持锁 60s。
|
||||
_i_am_starter = False
|
||||
with self._easyocr_startup_lock:
|
||||
if self._easyocr_starting or (self._easyocr_proc is not None and self._easyocr_proc.poll() is None):
|
||||
_i_am_starter = False # 另一线程已拉起/正在握手 → 本线程不 Popen
|
||||
else:
|
||||
self._easyocr_starting = True # 持锁标记本线程负责启动
|
||||
_i_am_starter = True
|
||||
_started = self._start_easyocr_proc_locked(is_normal_restart)
|
||||
if not _started:
|
||||
self._easyocr_starting = False
|
||||
# 锁已释放
|
||||
if not _i_am_starter:
|
||||
# 另一线程正在握手(或本线程未启动但进程存在)→ 等待其握手结果
|
||||
if self._easyocr_proc is not None and self._easyocr_proc.poll() is None:
|
||||
self._easyocr_ready_event.wait(timeout=self._easyocr_ready_timeout)
|
||||
if self._easyocr_proc is not None and self._easyocr_proc.poll() is None and self._easyocr_ready:
|
||||
return True
|
||||
return False
|
||||
# 本线程成功拉起进程 → 执行握手(锁外,不持锁 60s)
|
||||
return self._handshake_easyocr_subprocess(is_normal_restart)
|
||||
|
||||
def _start_easyocr_proc_locked(self, is_normal_restart: bool) -> bool:
|
||||
"""(在 _easyocr_startup_lock 内调用)启动 EasyOCR 子进程:worker 路径检查 + 内存预检 + Popen。
|
||||
|
||||
返回 True = 子进程已拉起(self._easyocr_proc 存活);False = 启动失败
|
||||
(内部已记录日志 / 设置 _easyocr_broken / 冷却)。
|
||||
"""
|
||||
worker_path = os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),
|
||||
'easyocr_worker.py'
|
||||
)
|
||||
if not os.path.exists(worker_path):
|
||||
logger.error(f"[ocr] EasyOCR worker 不存在: {worker_path}")
|
||||
self._easyocr_broken = True
|
||||
return False
|
||||
|
||||
# ⭐ 内存预检:物理内存 + 页面文件(虚拟内存)双重检查
|
||||
# 正常调度重启时跳过内存检测,避免正常资源释放后重启被误判为错误
|
||||
if not is_normal_restart:
|
||||
@@ -1748,15 +1829,6 @@ class MessageMonitor:
|
||||
else:
|
||||
logger.debug("[ocr] 正常调度重启:跳过内存检测,直接启动子进程")
|
||||
|
||||
worker_path = os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),
|
||||
'easyocr_worker.py'
|
||||
)
|
||||
if not os.path.exists(worker_path):
|
||||
logger.error(f"[ocr] EasyOCR worker 不存在: {worker_path}")
|
||||
self._easyocr_broken = True
|
||||
return False
|
||||
|
||||
try:
|
||||
_worker_env = dict(os.environ)
|
||||
# ⭐ CUDA_LAUNCH_BLOCKING=1: CUDA 操作同步化,access violation 时只杀子进程不冻整机
|
||||
@@ -1818,13 +1890,19 @@ class MessageMonitor:
|
||||
env=_worker_env,
|
||||
)
|
||||
logger.debug("[ocr] EasyOCR 子进程已启动,等待初始化握手...")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"[ocr] 启动 EasyOCR 子进程失败: {e}")
|
||||
self._easyocr_broken = True
|
||||
self._easyocr_proc = None
|
||||
return False
|
||||
|
||||
# ⭐ 等待 ready 握手(60s 初始化预算,不计入请求超时)
|
||||
def _handshake_easyocr_subprocess(self, is_normal_restart: bool) -> bool:
|
||||
"""(本线程成功拉起进程后调用)等待 worker 的 {"ready": true} 握手(最长 60s 初始化预算)。
|
||||
|
||||
握手成功:设置 _easyocr_ready + 触发 _easyocr_ready_event,返回 True。
|
||||
握手失败/超时/崩溃:杀进程 + 冷却 + 清除就绪信号,返回 False。
|
||||
"""
|
||||
try:
|
||||
from queue import Queue, Empty as _QueueEmpty
|
||||
_q = Queue()
|
||||
@@ -1874,15 +1952,24 @@ class MessageMonitor:
|
||||
else:
|
||||
if not is_normal_restart:
|
||||
self._easyocr_retry_until = time.time() + 30
|
||||
# ⭐ Task #1446:进程已死,清除就绪信号与握手事件
|
||||
self._easyocr_proc = None
|
||||
self._easyocr_ready = False
|
||||
self._easyocr_ready_event.clear()
|
||||
self._easyocr_starting = False
|
||||
return False
|
||||
try:
|
||||
payload = json.loads(line)
|
||||
if payload.get("ready"):
|
||||
logger.debug("[ocr] EasyOCR 子进程就绪")
|
||||
logger.info("[ocr] EasyOCR 子进程就绪")
|
||||
self._easyocr_consecutive_startup_failures = 0 # 成功启动,重置启动失败计数
|
||||
# ⭐ 截断崩溃日志:清除旧条目,防止旧记录混淆用户
|
||||
_truncate_easyocr_crash_log()
|
||||
# ⭐ Task #1446:握手完成 → 标记就绪并广播事件,唤醒等待中的并发线程,
|
||||
# 保证 _call_easyocr_safe 的 10s 请求计时只在 worker 真正就绪后才开始
|
||||
self._easyocr_ready = True
|
||||
self._easyocr_ready_event.set()
|
||||
self._easyocr_starting = False # 启动完成,清除「正在启动」标志
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
@@ -2887,48 +2974,32 @@ class MessageMonitor:
|
||||
for part in parts:
|
||||
if re.match(r'^\d{1,2}:\d{1,2}', part):
|
||||
if buf.strip():
|
||||
_fresh = self._looks_fresh_content(buf.strip())
|
||||
if not last_ts:
|
||||
# ⭐ 无前置时间戳:位于首个时间戳之前的消息。
|
||||
# ⚠ 不再整段丢弃:可能是被裁切/漏识别时间戳的新消息,
|
||||
# 交由下游 seen/_replied_texts 去重(基线轮会吸收历史)。
|
||||
logger.debug(f"[ocr] stage[{i}] 无前置时间戳归属,交由下游去重: {buf.strip()[:60]!r}")
|
||||
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, 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, 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}")
|
||||
# ⭐ 严格300s报废:继承时间戳>5min一律丢弃,无新鲜特征/提问豁免
|
||||
# 原逻辑允许新鲜内容/提问绕过,导致16:07消息在21:08仍被处理
|
||||
logger.info(f"[ocr] stage[{i}] [age] DROP 继承时间戳{last_ts}较老(>300s),严格报废: {buf.strip()[:60]!r}")
|
||||
buf = ''
|
||||
last_ts = part
|
||||
continue
|
||||
# ⭐ Task #1440: 继承时间戳超过5分钟(300s)的消息直接丢弃
|
||||
# 修复:12:09的消息能通过13:49的过滤器(差100分钟)
|
||||
# 根因:无独立时间戳的消息被放行交由下游去重,但下游去重不够严格
|
||||
logger.debug(f"[ocr] stage[{i}] 继承时间戳{last_ts}较老(>5min)且消息无独立时间戳,丢弃: {buf.strip()[:60]!r}")
|
||||
buf = ''
|
||||
# —— 过滤器层(始终运行,绝不静默吞噬;每层放行/打回都记录)——
|
||||
if self._is_markdown_document(buf.strip()):
|
||||
logger.debug(f"[ocr] stage[{i}] md文档直接跳过: {buf.strip()[:80]!r}")
|
||||
logger.info(f"[ocr] stage[{i}] [filter3/md] DROP md文档跳过: {buf.strip()[:80]!r}")
|
||||
elif self._is_noise_text(buf.strip()):
|
||||
logger.debug(f"[ocr] stage[{i}] 噪声文本跳过: {buf.strip()[:80]!r}")
|
||||
logger.info(f"[ocr] stage[{i}] [filter2/噪声] DROP 噪声文本跳过: {buf.strip()[:80]!r}")
|
||||
else:
|
||||
msg = re.sub(r'\s*(?:[A-Za-z0-9]~)+\s*$', '', buf.strip())
|
||||
if msg:
|
||||
msg = re.sub(r'\s+#[\u4e00-\u9fff][^\s#\n]{0,30}$', '', msg).strip()
|
||||
if msg:
|
||||
if self._is_reply_fragment(msg):
|
||||
logger.debug(f"[ocr] stage[{i}] 回复残片跳过: {msg[:60]!r}")
|
||||
logger.info(f"[ocr] stage[{i}] [filter4/回复残片] DROP 回复残片跳过: {msg[:60]!r}")
|
||||
else:
|
||||
logger.debug(f"[ocr] stage[{i}] 消息通过所有过滤器,添加到expanded: {msg[:60]!r}")
|
||||
logger.info(f"[ocr] stage[{i}] [filter/放行] 通过所有过滤器,添加到expanded: {msg[:60]!r}")
|
||||
expanded.append(msg)
|
||||
buf = ''
|
||||
last_ts = part
|
||||
@@ -2937,43 +3008,35 @@ class MessageMonitor:
|
||||
buf += part
|
||||
if buf.strip():
|
||||
# 尾部检查:尾部消息位于最后一个可见时间戳之后,无独立时间戳。
|
||||
# ⚠ 修复漏消息:聊天软件最新消息常无独立时间戳、排在最后时间戳之后,
|
||||
# 仅因继承的最后时间戳较老就丢弃会连坐丢失用户刚发的新消息。
|
||||
# 不再按年龄丢弃,交由下游 seen/_replied_texts 去重(基线轮吸收历史);
|
||||
# 仅当继承时间戳极老(>2h)且内容无新鲜特征时视为远古历史丢弃。
|
||||
# 关键修复:过滤器层(md/噪声/回复残片)必须【始终运行】,无论继承时间戳新旧,
|
||||
# 之前错误地嵌在"时间戳较老"分支内,导致新鲜尾部消息(如用户刚发的提问)
|
||||
# 被静默吞噬、永不进入 expanded。
|
||||
# 仅当继承时间戳较老(>5min)且无新鲜特征且非提问豁免时才丢弃(Task #1440 的5分钟窗口),
|
||||
# 其余一律保留交给过滤器层,绝不静默吞噬。
|
||||
if not last_ts:
|
||||
# 尾部无任何时间戳(整行无时间戳):交给下游去重,不整段丢弃
|
||||
logger.debug(f"[ocr] stage[{i}] 尾部无时间戳归属,交由下游去重: {buf.strip()[:60]!r}")
|
||||
logger.debug(f"[ocr] stage[{i}] [age] PASS 尾部无时间戳归属,交由下游去重: {buf.strip()[:60]!r}")
|
||||
else:
|
||||
_fresh_tail = self._looks_fresh_content(buf.strip())
|
||||
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, 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}")
|
||||
buf = ''
|
||||
else:
|
||||
# ⭐ Task #1440: 尾部继承时间戳超过5分钟(300s)的消息直接丢弃
|
||||
# 修复:与中段逻辑一致,超时一律丢弃
|
||||
logger.debug(f"[ocr] stage[{i}] 尾部时间戳{last_ts}较老(>5min)且消息无独立时间戳,丢弃: {buf.strip()[:60]!r}")
|
||||
# ⭐ 严格300s报废:继承时间戳>5min一律丢弃,无新鲜特征/提问豁免
|
||||
if self._is_timestamp_old(last_ts, 300, skip_future_check=_inherited_past):
|
||||
logger.info(f"[ocr] stage[{i}] [age] DROP 尾部时间戳{last_ts}较老(>300s),严格报废: {buf.strip()[:60]!r}")
|
||||
buf = ''
|
||||
# else: 时间戳新鲜(<5min),直接保留
|
||||
# —— 过滤器层(始终运行,绝不静默吞噬;每层放行/打回都记录)——
|
||||
if buf.strip():
|
||||
if self._is_markdown_document(buf.strip()):
|
||||
logger.debug(f"[ocr] stage[{i}] md文档直接跳过(尾部): {buf.strip()[:80]!r}")
|
||||
logger.info(f"[ocr] stage[{i}] [filter3/md] DROP md文档跳过(尾部): {buf.strip()[:80]!r}")
|
||||
elif self._is_noise_text(buf.strip()):
|
||||
logger.debug(f"[ocr] stage[{i}] 噪声文本跳过(尾部): {buf.strip()[:80]!r}")
|
||||
logger.info(f"[ocr] stage[{i}] [filter2/噪声] DROP 噪声文本跳过(尾部): {buf.strip()[:80]!r}")
|
||||
else:
|
||||
msg = re.sub(r'\s*(?:[A-Za-z0-9]~)+\s*$', '', buf.strip())
|
||||
if msg:
|
||||
msg = re.sub(r'\s+#[\u4e00-\u9fff][^\s#\n]{0,30}$', '', msg).strip()
|
||||
if msg:
|
||||
if self._is_reply_fragment(msg):
|
||||
logger.debug(f"[ocr] stage[{i}] 回复残片跳过: {msg[:60]!r}")
|
||||
logger.info(f"[ocr] stage[{i}] [filter4/回复残片] DROP 回复残片跳过: {msg[:60]!r}")
|
||||
else:
|
||||
logger.debug(f"[ocr] stage[{i}] 尾部消息通过所有过滤器,添加到expanded: {msg[:60]!r}")
|
||||
logger.info(f"[ocr] stage[{i}] [filter/放行] 尾部消息通过所有过滤器,添加到expanded: {msg[:60]!r}")
|
||||
# EasyOCR 常把横向相邻的两条消息(用户消息+群昵称/UI文字)
|
||||
# 检测为同一行,导致"兑换码导入错误时如何处置?游戏专用小筑憩-草一"
|
||||
# 这种合并文本。尝试在中文句尾标点处拆分为多条。
|
||||
@@ -4257,7 +4320,7 @@ class MessageMonitor:
|
||||
pass
|
||||
if not newest_fresh_enough:
|
||||
# ⭐ 硬截止:最新时间戳 >30min → 无论是否含 CJK 均丢弃(同主路径)
|
||||
_hard_cutoff_s = 1800
|
||||
_hard_cutoff_s = 300 # 严格5分钟报废
|
||||
_newest_age_s = None
|
||||
for ts in timestamps:
|
||||
try:
|
||||
@@ -4267,18 +4330,13 @@ class MessageMonitor:
|
||||
except Exception:
|
||||
pass
|
||||
if _newest_age_s is not None and _newest_age_s > _hard_cutoff_s:
|
||||
logger.debug(f"[ocr] 过滤器1结果: 过去日期+最新时间戳已过期>{_hard_cutoff_s}s({_newest_age_s:.0f}s),硬截止丢弃: {text[:80]!r}")
|
||||
logger.debug(f"[ocr] 过滤器1结果: 过去日期+最新时间戳已过期>{_hard_cutoff_s}s({_newest_age_s:.0f}s),严格报废丢弃: {text[:80]!r}")
|
||||
return False
|
||||
if self._block_has_cjk_message(normalized):
|
||||
logger.debug(f"[ocr] 过滤器1结果: 过去日期+时间戳全过期但含中文消息内容(潜在新消息),放行交由下游去重: {text[:80]!r}")
|
||||
return True
|
||||
logger.debug(f"[ocr] 过滤器1结果: 过去日期+最新时间戳已过期≥600s且无中文消息内容,丢弃: {text[:80]!r}")
|
||||
# ⭐ 严格300s:有时间戳且全过期→直接丢弃
|
||||
logger.debug(f"[ocr] 过滤器1结果: 过去日期+时间戳全过期(>300s),丢弃: {text[:80]!r}")
|
||||
return False
|
||||
# 最新时间戳在 300-600s 之间,且文本含明确新鲜特征 → 放行
|
||||
if self._looks_fresh_content(text):
|
||||
logger.debug(f"[ocr] 过滤器1结果: 过去日期+时间戳全过期但最新≤600s且含新鲜特征,放行: {text[:80]!r}")
|
||||
return True
|
||||
logger.debug(f"[ocr] 过滤器1结果: 过去日期+时间戳全过期且无新鲜特征,丢弃: {text[:80]!r}")
|
||||
# ⭐ 严格300s:最新时间戳在300-600s之间也视为过期,直接丢弃
|
||||
logger.debug(f"[ocr] 过滤器1结果: 过去日期+最新时间戳已过期(>300s),丢弃: {text[:80]!r}")
|
||||
return False
|
||||
# 有新鲜时间戳 → 确定有新消息
|
||||
for ts in timestamps:
|
||||
@@ -4305,7 +4363,7 @@ class MessageMonitor:
|
||||
if not newest_fresh_enough:
|
||||
# ⭐ 硬截止:最新时间戳 >30min → 无论是否含 CJK 均丢弃
|
||||
# 修复:15:09 看到 13:34 消息(90min 前)因含 CJK 被放行触发回复
|
||||
_hard_cutoff_s = 1800 # 30 分钟
|
||||
_hard_cutoff_s = 300 # 严格5分钟报废 # 30 分钟
|
||||
_newest_age_s = None
|
||||
for ts in timestamps:
|
||||
try:
|
||||
@@ -4315,21 +4373,13 @@ class MessageMonitor:
|
||||
except Exception:
|
||||
pass
|
||||
if _newest_age_s is not None and _newest_age_s > _hard_cutoff_s:
|
||||
logger.debug(f"[ocr] 过滤器1结果: 最新时间戳已过期>{_hard_cutoff_s}s({_newest_age_s:.0f}s),硬截止丢弃: {text[:80]!r}")
|
||||
logger.debug(f"[ocr] 过滤器1结果: 最新时间戳已过期>{_hard_cutoff_s}s({_newest_age_s:.0f}s),严格报废丢弃: {text[:80]!r}")
|
||||
return False
|
||||
if self._block_has_cjk_message(normalized):
|
||||
logger.debug(f"[ocr] 过滤器1结果: 时间戳全过期但含中文消息内容(潜在新消息),放行交由下游去重: {text[:80]!r}")
|
||||
return True
|
||||
logger.debug(f"[ocr] 过滤器1结果: 最新可见时间戳已过期≥600s且无中文消息内容,丢弃: {text[:80]!r}")
|
||||
# ⭐ 严格300s:有时间戳且全过期→直接丢弃,不再放行CJK内容
|
||||
logger.debug(f"[ocr] 过滤器1结果: 时间戳全过期(>300s)且无新鲜时间戳,丢弃: {text[:80]!r}")
|
||||
return False
|
||||
# 最新时间戳在 300-600s 之间,且文本含明确新鲜特征 → 放行给 stage splitting
|
||||
if self._looks_fresh_content(text):
|
||||
logger.debug(f"[ocr] 过滤器1结果: 时间戳全过期但最新≤600s且含新鲜特征,放行: {text[:80]!r}")
|
||||
return True
|
||||
# ⭐ 修复:时间戳过期(>600s)时不再无条件放行
|
||||
# 原逻辑:只要块内有中文消息内容就放行,导致过期消息(如20:41的消息在20:59时仍被放行)
|
||||
# 新逻辑:时间戳过期超过600s(10分钟)的消息必须丢弃,除非有明确新鲜特征
|
||||
logger.debug(f"[ocr] 过滤器1结果: 时间戳全过期且无新鲜特征,丢弃: {text[:80]!r}")
|
||||
# ⭐ 严格300s:最新时间戳在300-600s之间也视为过期,直接丢弃
|
||||
logger.debug(f"[ocr] 过滤器1结果: 最新时间戳已过期(>300s),丢弃: {text[:80]!r}")
|
||||
return False
|
||||
|
||||
# 未来时间戳容忍偏差(秒):同日内时间戳比系统时间晚超过此值 = 未到来,直接丢弃。
|
||||
|
||||
Reference in New Issue
Block a user