修复EasyOCR三个问题:正常调度误判、内存检测误判、消息hash漏消息
This commit is contained in:
+8
-5
@@ -112,7 +112,9 @@ try:
|
||||
_commit_gb = _used_phys_gb + _pagefile_used_gb
|
||||
_commit_total_gb = _total_phys_gb + _vm.get("total_pagefile_gb", 0)
|
||||
_commit_pct = (_commit_gb / max(_commit_total_gb, 1)) * 100
|
||||
if _commit_pct > 90.0:
|
||||
# ⭐ 放宽阈值:提交量>95%才强制CPU,物理内存<2GB才强制CPU
|
||||
# 避免内存充足时误判(用户报告提交量81.8%被误判为不足)
|
||||
if _commit_pct > 95.0:
|
||||
_msg = (
|
||||
f"系统内存压力过高: 物理剩余{_avail_phys_gb:.1f}GB,"
|
||||
f"提交量{_commit_pct:.1f}% ({_commit_gb:.1f}GB/{_commit_total_gb:.1f}GB)"
|
||||
@@ -122,11 +124,11 @@ try:
|
||||
os.environ["CUDA_VISIBLE_DEVICES"] = "" # 禁用CUDA,防止加载CUDA库导致崩溃
|
||||
sys.stderr.write(f"[easyocr_worker] ⚠️ {_msg},降级CPU模式\n")
|
||||
sys.stderr.flush()
|
||||
elif _avail_phys_gb < 3.0:
|
||||
elif _avail_phys_gb < 2.0:
|
||||
_msg = (
|
||||
f"物理内存不足: 剩余{_avail_phys_gb:.1f}GB"
|
||||
f"(总计{_total_phys_gb:.1f}GB)"
|
||||
f",CUDA加载需要≥3GB物理内存"
|
||||
f",CUDA加载需要≥2GB物理内存"
|
||||
)
|
||||
_write_crash_log(f"{datetime.now()} [easyocr_worker] 物理内存预检失败: {_msg}")
|
||||
os.environ["EASYOCR_FORCE_CPU"] = "1"
|
||||
@@ -135,8 +137,9 @@ try:
|
||||
sys.stderr.flush()
|
||||
except Exception:
|
||||
pass # GlobalMemoryStatusEx 失败时不阻断
|
||||
# ⭐ 检查页面文件:百分比>95% 或 剩余<2GB 时退出(防冻结)
|
||||
if _vm and (_vm.get("commit_pct", 0) > 95.0 or _vm.get("avail_pagefile_gb", 99) < 2.0):
|
||||
# ⭐ 放宽退出阈值:百分比>98% 或 剩余<1GB 时退出(防冻结)
|
||||
# 避免内存充足时误退出(用户报告提交量81.8%被误判为不足)
|
||||
if _vm and (_vm.get("commit_pct", 0) > 98.0 or _vm.get("avail_pagefile_gb", 99) < 1.0):
|
||||
_msg = (
|
||||
f"页面文件不足: 已用{_vm.get('commit_pct', '?')}%"
|
||||
f"(剩余{_vm.get('avail_pagefile_gb', '?')}GB/{_vm.get('total_pagefile_gb', '?')}GB)"
|
||||
|
||||
+95
-51
@@ -253,17 +253,44 @@ class MessageHistory:
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def is_user_message_replied(self, platform: str, content: str) -> bool:
|
||||
"""检查用户消息是否已被回复过(数据库中有相同内容的bot回复)"""
|
||||
def is_user_message_replied(self, platform: str, content: str, user_msg_time: float = 0) -> bool:
|
||||
"""检查用户消息是否已被回复过(数据库中有相同内容的bot回复)
|
||||
|
||||
user_msg_time: 用户消息的时间戳(Unix时间),如果晚于最后bot回复时间,则视为新消息不跳过
|
||||
"""
|
||||
content_hash = hashlib.md5(content.strip().encode('utf-8')).hexdigest()
|
||||
conn = self._get_conn()
|
||||
try:
|
||||
# 首先检查是否有相同内容的bot回复
|
||||
cursor = conn.execute(
|
||||
"SELECT COUNT(*) FROM message_history WHERE platform=? AND sender_type='bot' AND content_hash=?",
|
||||
(platform, content_hash)
|
||||
)
|
||||
count = cursor.fetchone()[0]
|
||||
return count > 0
|
||||
if count == 0:
|
||||
return False # 没有相同内容的bot回复,未被回复过
|
||||
|
||||
# 如果有相同内容的bot回复,但用户消息时间戳晚于最后bot回复时间,则视为新消息
|
||||
if user_msg_time > 0:
|
||||
cursor = conn.execute(
|
||||
"SELECT created_at FROM message_history WHERE platform=? AND sender_type='bot' ORDER BY id DESC LIMIT 1",
|
||||
(platform,)
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
if row and row[0]:
|
||||
# 将数据库时间字符串转换为时间戳
|
||||
try:
|
||||
from datetime import datetime
|
||||
bot_time_str = row[0]
|
||||
# 假设格式为 "2026-08-05 15:09:53"
|
||||
bot_time = datetime.strptime(bot_time_str, "%Y-%m-%d %H:%M:%S").timestamp()
|
||||
# 如果用户消息时间戳晚于最后bot回复时间,视为新消息
|
||||
if user_msg_time > bot_time:
|
||||
return False
|
||||
except Exception:
|
||||
pass # 时间解析失败,按原逻辑处理
|
||||
|
||||
return True
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
@@ -368,6 +395,8 @@ class MessageMonitor:
|
||||
self._easyocr_max_startup_failures: int = 5 # 连续5次启动失败后长冷却(内存不足场景)
|
||||
# EasyOCR CUDA 错误计数:≥2 次 OOM/kernel error → 下次启动强制 CPU(防冻结)
|
||||
self._easyocr_cuda_oom_count: int = 0
|
||||
# EasyOCR 正常调度重启标志:正常资源释放后重启子进程时跳过内存检测
|
||||
self._easyocr_normal_restart: bool = False
|
||||
# 引擎选择计数器
|
||||
self._ocr_engine_wins: dict = {"windows": 0, "easyocr": 0}
|
||||
# 上次 OCR 原始文本(用于增量检测)
|
||||
@@ -1488,18 +1517,25 @@ class MessageMonitor:
|
||||
# 直接 GPU 推理 crash(access violation)会杀整个进程 + 冻电脑。
|
||||
# 子进程方案:GPU crash 只杀子进程,主进程降级到 Windows OCR。
|
||||
|
||||
def _ensure_easyocr_subprocess(self) -> bool:
|
||||
def _ensure_easyocr_subprocess(self, is_normal_restart: bool = False) -> bool:
|
||||
"""确保 EasyOCR 子进程在运行。返回 True = 可用。
|
||||
|
||||
启动后等待 worker 的 {"ready": true} 握手(最长 60s 初始化预算),
|
||||
初始化时间不计入请求超时。子进程崩溃/超时 → 杀进程 + 设置冷却,
|
||||
冷却期后自动重试(不再永久 _easyocr_broken)。
|
||||
|
||||
is_normal_restart: 正常调度重启时跳过内存检测,避免正常资源释放后重启被误判为错误
|
||||
"""
|
||||
# ⭐ 检查监控是否仍在运行:stop()/急停后 warmup 线程不应重启子进程
|
||||
if not self._running:
|
||||
return False
|
||||
if self._easyocr_broken:
|
||||
return False
|
||||
# ⭐ 检查是否为正常调度重启:正常资源释放后重启子进程清GPU缓存
|
||||
if hasattr(self, '_easyocr_normal_restart') and self._easyocr_normal_restart:
|
||||
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 # 还在跑
|
||||
@@ -1511,54 +1547,56 @@ class MessageMonitor:
|
||||
return False
|
||||
|
||||
# ⭐ 内存预检:物理内存 + 页面文件(虚拟内存)双重检查
|
||||
try:
|
||||
import ctypes
|
||||
class _MEMORYSTATUSEX(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("dwLength", ctypes.c_ulong), ("dwMemoryLoad", ctypes.c_ulong),
|
||||
("ullTotalPhys", ctypes.c_ulonglong), ("ullAvailPhys", ctypes.c_ulonglong),
|
||||
("ullTotalPageFile", ctypes.c_ulonglong), ("ullAvailPageFile", ctypes.c_ulonglong),
|
||||
("ullTotalVirtual", ctypes.c_ulonglong), ("ullAvailVirtual", ctypes.c_ulonglong),
|
||||
("ullAvailExtendedVirtual", ctypes.c_ulonglong),
|
||||
]
|
||||
_mem = _MEMORYSTATUSEX()
|
||||
_mem.dwLength = ctypes.sizeof(_MEMORYSTATUSEX)
|
||||
ctypes.windll.kernel32.GlobalMemoryStatusEx(ctypes.byref(_mem))
|
||||
|
||||
# 物理内存
|
||||
_phys_total_gb = _mem.ullTotalPhys / (1024**3)
|
||||
_phys_avail_gb = _mem.ullAvailPhys / (1024**3)
|
||||
_phys_used_gb = _phys_total_gb - _phys_avail_gb
|
||||
_phys_used_pct = round(_mem.dwMemoryLoad, 1)
|
||||
|
||||
# 页面文件(虚拟内存)
|
||||
_page_total_gb = _mem.ullTotalPageFile / (1024**3)
|
||||
_page_avail_gb = _mem.ullAvailPageFile / (1024**3)
|
||||
_page_used_gb = _page_total_gb - _page_avail_gb
|
||||
_page_used_pct = round((1 - _mem.ullAvailPageFile / max(_mem.ullTotalPageFile, 1)) * 100, 1)
|
||||
|
||||
# 虚拟内存(进程地址空间)
|
||||
_virt_total_gb = _mem.ullTotalVirtual / (1024**3)
|
||||
_virt_avail_gb = _mem.ullAvailVirtual / (1024**3)
|
||||
|
||||
logger.debug(f"[ocr] 内存状态:物理 {_phys_used_gb:.1f}/{_phys_total_gb:.1f}GB({_phys_used_pct}%),"
|
||||
f"页面文件 {_page_used_gb:.1f}/{_page_total_gb:.1f}GB({_page_used_pct}%),"
|
||||
f"虚拟地址空间 {_virt_avail_gb:.1f}GB可用")
|
||||
|
||||
# ⭐ 页面文件百分比阈值:>85% 已用即视为不足(防页面文件大户系统冻结)
|
||||
# 旧阈值只看绝对值<2GB,35GB总页面文件剩1.9GB看似"有空间"实则94.7%已满
|
||||
if _page_used_pct > 85.0:
|
||||
# 正常调度重启时跳过内存检测,避免正常资源释放后重启被误判为错误
|
||||
if not is_normal_restart:
|
||||
try:
|
||||
import ctypes
|
||||
class _MEMORYSTATUSEX(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("dwLength", ctypes.c_ulong), ("dwMemoryLoad", ctypes.c_ulong),
|
||||
("ullTotalPhys", ctypes.c_ulonglong), ("ullAvailPhys", ctypes.c_ulonglong),
|
||||
("ullTotalPageFile", ctypes.c_ulonglong), ("ullAvailPageFile", ctypes.c_ulonglong),
|
||||
("ullTotalVirtual", ctypes.c_ulonglong), ("ullAvailVirtual", ctypes.c_ulonglong),
|
||||
("ullAvailExtendedVirtual", ctypes.c_ulonglong),
|
||||
]
|
||||
_mem = _MEMORYSTATUSEX()
|
||||
_mem.dwLength = ctypes.sizeof(_MEMORYSTATUSEX)
|
||||
ctypes.windll.kernel32.GlobalMemoryStatusEx(ctypes.byref(_mem))
|
||||
|
||||
# 物理内存
|
||||
_phys_total_gb = _mem.ullTotalPhys / (1024**3)
|
||||
_phys_avail_gb = _mem.ullAvailPhys / (1024**3)
|
||||
_phys_used_gb = _phys_total_gb - _phys_avail_gb
|
||||
_phys_used_pct = round(_mem.dwMemoryLoad, 1)
|
||||
|
||||
# 页面文件(虚拟内存)
|
||||
_page_total_gb = _mem.ullTotalPageFile / (1024**3)
|
||||
_page_avail_gb = _mem.ullAvailPageFile / (1024**3)
|
||||
_page_used_gb = _page_total_gb - _page_avail_gb
|
||||
_page_used_pct = round((1 - _mem.ullAvailPageFile / max(_mem.ullTotalPageFile, 1)) * 100, 1)
|
||||
|
||||
# 虚拟内存(进程地址空间)
|
||||
_virt_total_gb = _mem.ullTotalVirtual / (1024**3)
|
||||
_virt_avail_gb = _mem.ullAvailVirtual / (1024**3)
|
||||
|
||||
logger.debug(f"[ocr] 内存状态:物理 {_phys_used_gb:.1f}/{_phys_total_gb:.1f}GB({_phys_used_pct}%),"
|
||||
f"页面文件 {_page_used_gb:.1f}/{_page_total_gb:.1f}GB({_page_used_pct}%),"
|
||||
f"虚拟地址空间 {_virt_avail_gb:.1f}GB可用")
|
||||
|
||||
# ⭐ 放宽页面文件百分比阈值:>95% 已用即视为不足(避免内存充足时误判)
|
||||
# 用户报告提交量81.8%被误判为不足,提高阈值到95%
|
||||
if _page_used_pct > 95.0:
|
||||
logger.error(f"[ocr] ⚠️ 页面文件不足: 已用{_page_used_pct}%({_page_used_gb:.1f}/{_page_total_gb:.1f}GB),"
|
||||
f"剩余{_page_avail_gb:.1f}GB。EasyOCR 冷却1小时。建议:增大 Windows 页面文件")
|
||||
self._easyocr_retry_until = time.time() + 3600
|
||||
return False
|
||||
|
||||
# ⭐ 系统总提交量检查:提交量 > 90% 总commit容量(物理+页面文件) 时冻结风险极高
|
||||
# torch/CUDA 加载需额外 ~6GB,提交量已超标时再加载必然触发换页风暴
|
||||
# ⭐ 放宽系统总提交量检查:提交量 > 95% 总commit容量(物理+页面文件) 时冻结风险极高
|
||||
# 避免内存充足时误判(用户报告提交量81.8%被误判为不足)
|
||||
_commit_gb = _phys_used_gb + _page_used_gb
|
||||
_commit_total_gb = _phys_total_gb + _page_total_gb
|
||||
_commit_pct = (_commit_gb / _commit_total_gb * 100) if _commit_total_gb > 0 else 0
|
||||
if _commit_pct > 90.0:
|
||||
if _commit_pct > 95.0:
|
||||
logger.error(f"[ocr] ⚠️ 系统提交量过高: {_commit_gb:.1f}GB ({_commit_pct:.0f}%总commit容量{_commit_total_gb:.1f}GB),"
|
||||
f"EasyOCR 冷却1小时。当前物理{_phys_used_pct}%+页面{_page_used_pct}%。"
|
||||
f"建议:关闭占用内存的程序或增大物理内存")
|
||||
@@ -1571,9 +1609,11 @@ class MessageMonitor:
|
||||
f"EasyOCR 冷却30分钟。建议:关闭其他程序释放内存")
|
||||
self._easyocr_retry_until = time.time() + 1800
|
||||
return False
|
||||
|
||||
except Exception:
|
||||
pass # 获取失败则不阻断
|
||||
|
||||
except Exception:
|
||||
pass # 获取失败则不阻断
|
||||
else:
|
||||
logger.debug("[ocr] 正常调度重启:跳过内存检测,直接启动子进程")
|
||||
|
||||
worker_path = os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),
|
||||
@@ -1591,8 +1631,9 @@ class MessageMonitor:
|
||||
# ⭐ EASYOCR_WORKER=1: 让 main.py 检测到 worker 模式,跳过 GUI 初始化
|
||||
_worker_env["EASYOCR_WORKER"] = "1"
|
||||
|
||||
# 内存检测:可用内存 < 3GB 时强制CPU模式,防止CUDA加载导致冻结
|
||||
# 内存检测:可用内存 < 2GB 时强制CPU模式,防止CUDA加载导致冻结
|
||||
# CUDA加载torch需要大量内存,物理内存不足时强制使用页面文件会导致系统冻结
|
||||
# ⭐ 放宽阈值:物理内存<2GB才强制CPU,提交量>95%才强制CPU
|
||||
try:
|
||||
mem = psutil.virtual_memory()
|
||||
available_gb = mem.available / (1024**3)
|
||||
@@ -1616,12 +1657,13 @@ class MessageMonitor:
|
||||
_commit_check_gb = used_gb + swap_used_gb
|
||||
_commit_total_check_gb = total_gb + swap_total_gb
|
||||
_commit_check_pct = (_commit_check_gb / _commit_total_check_gb * 100) if _commit_total_check_gb > 0 else 0
|
||||
if swap_pct > 85.0 or _commit_check_pct > 90.0:
|
||||
# ⭐ 放宽阈值:提交量>95%才强制CPU,避免内存充足时误判
|
||||
if swap_pct > 95.0 or _commit_check_pct > 95.0:
|
||||
_worker_env["EASYOCR_FORCE_CPU"] = "1"
|
||||
logger.warning(f"[ocr] 内存压力高(物理{mem.percent}%+页面{swap_pct}%,"
|
||||
f"总提交{_commit_check_gb:.1f}GB ({_commit_check_pct:.0f}%总commit容量{_commit_total_check_gb:.1f}GB),"
|
||||
f"EasyOCR 子进程强制CPU模式(避免CUDA加载冻结)")
|
||||
elif available_gb < 3.0:
|
||||
elif available_gb < 2.0:
|
||||
_worker_env["EASYOCR_FORCE_CPU"] = "1"
|
||||
logger.warning(f"[ocr] 可用内存不足({available_gb:.1f}GB/总{total_gb:.1f}GB,"
|
||||
f"使用{mem.percent}%),页面文件{swap_avail_gb:.1f}GB可用({swap_pct}%),"
|
||||
@@ -1820,6 +1862,8 @@ class MessageMonitor:
|
||||
logger.debug(f"[ocr] EasyOCR 正常资源释放:{self._easyocr_max_calls}次调用完成,重启子进程清GPU缓存(非错误,不计入失败计数器)")
|
||||
self._kill_easyocr_proc("正常资源释放")
|
||||
self._easyocr_call_count = 0
|
||||
# ⭐ 正常调度重启:标记为正常重启,跳过内存检测直接启动
|
||||
self._easyocr_normal_restart = True
|
||||
return result["text"]
|
||||
else:
|
||||
err = result.get('error', '?')[:100]
|
||||
@@ -3923,7 +3967,7 @@ class MessageMonitor:
|
||||
last_bot_time = self._msg_history.last_bot_reply_time(platform)
|
||||
if last_bot_time > 0 and time.time() - last_bot_time <= 60:
|
||||
# 用户消息挨着bot消息
|
||||
if not self._msg_history.is_user_message_replied(platform, stripped):
|
||||
if not self._msg_history.is_user_message_replied(platform, stripped, time.time()):
|
||||
# 数据库无记录,无条件放行
|
||||
logger.debug(f"[ocr] 过滤器1结果: 无时间戳但用户消息挨着bot消息且无记录,放行: {stripped[:80]!r}")
|
||||
self._msg_history.record_user_message(platform, stripped)
|
||||
|
||||
Reference in New Issue
Block a user