修复bot自言自语大小写不敏感问题 + 退出时后台内存回收不阻塞

- monitor.py: 去重管道所有比较改为大小写不敏感(recent_replies时间窗口、_replied_texts、_fuzzy_duplicate、子串包含、长公共子串、_last_bot_reply_content)
- main.py: CTRL+C信号处理将内存回收放到后台线程,不阻塞退出
- gui.py: 无实质修改(仅格式)
This commit is contained in:
youpaishidifu
2026-08-23 21:30:05 +08:00
parent 25b844098f
commit 2a3ecdba14
3 changed files with 142 additions and 13 deletions
+103 -2
View File
@@ -9,6 +9,8 @@ import threading
import os
import re
import time
import gc
import ctypes
import customtkinter as ctk
@@ -20,6 +22,83 @@ from auto_reply import AutoReply
logger = logging.getLogger("msgbot.gui")
def _force_system_memory_cleanup():
"""强制回收Python进程占用的内存归还系统调度。
Windows API:
- EmptyWorkingSet: 清空工作集,强制归还物理内存
- SetProcessWorkingSetSize: 设置工作集大小为最小值,触发系统回收
"""
try:
# 获取当前进程句柄
kernel32 = ctypes.windll.kernel32
psapi = ctypes.windll.psapi
# 获取当前进程句柄
h_process = kernel32.GetCurrentProcess()
# 方法1: EmptyWorkingSet - 清空工作集
try:
psapi.EmptyWorkingSet(h_process)
logger.debug("系统内存清理: EmptyWorkingSet 完成")
except Exception as e:
logger.debug(f"EmptyWorkingSet 失败: {e}")
# 方法2: SetProcessWorkingSetSize - 设置最小工作集
try:
# dwMinimumWorkingSetSize=1, dwMaximumWorkingSetSize=-1 (无限)
# 设置最小值为1字节,最大值为无限,让系统自由回收
kernel32.SetProcessWorkingSetSize(h_process, 1, -1)
logger.debug("系统内存清理: SetProcessWorkingSetSize 完成")
except Exception as e:
logger.debug(f"SetProcessWorkingSetSize 失败: {e}")
# Python GC + torch CUDA 清理
try:
import torch
if torch.cuda.is_available():
torch.cuda.empty_cache()
torch.cuda.synchronize()
logger.debug("系统内存清理: torch.cuda.empty_cache 完成")
except ImportError:
pass # torch 未安装
except Exception as e:
logger.debug(f"torch.cuda.empty_cache 失败: {e}")
# Python GC 强制回收
collected = gc.collect()
logger.debug(f"系统内存清理: gc.collect 回收 {collected} 个对象")
except Exception as e:
logger.debug(f"系统内存清理异常: {e}")
def _kill_easyocr_subprocesses():
"""杀死所有EasyOCR子进程,彻底释放GPU/CPU内存。"""
try:
import psutil
current_pid = os.getpid()
killed = 0
for proc in psutil.process_iter(['pid', 'name', 'cmdline']):
try:
# EasyOCR子进程通常以python启动,cmdline包含easyocr
if proc.pid == current_pid:
continue
cmdline = ' '.join(proc.cmdline() or [])
if 'easyocr' in cmdline.lower() or 'ocr_worker' in cmdline.lower():
proc.kill()
killed += 1
logger.debug(f"杀死EasyOCR子进程: PID={proc.pid}")
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
if killed:
logger.debug(f"共杀死 {killed} 个EasyOCR子进程")
except ImportError:
pass
except Exception as e:
logger.debug(f"杀死EasyOCR子进程异常: {e}")
class OCREngineManager:
"""双引擎OCR管理器:EasyOCR + Windows OCR,得分最高者胜出。
@@ -191,18 +270,29 @@ class OCREngineManager:
self._timer_thread.start()
def _release(self):
"""释放OCR引擎资源"""
"""释放OCR引擎资源 + 杀EasyOCR子进程 + 系统内存回收"""
with self._lock:
self._easyocr_reader = None
self._winocr_engine = None
self._initialized = False
self._log("🗑 OCR引擎已释放")
# 杀EasyOCR子进程(彻底释放GPU内存)
_kill_easyocr_subprocesses()
# 系统级内存回收
_force_system_memory_cleanup()
self._log("🧹 系统内存已回收")
def set_monitor_active(self, active: bool):
"""设置消息监控激活状态"""
self._monitor_active = active
if active and self._initialized:
self._last_use_time = time.time() # 激活时重置计时
elif not active and self._initialized:
# 监控停止 → 立即释放OCR(杀子进程+系统内存回收)
self._log("⏸ 监控停止,立即释放OCR内存...")
self._release()
def recognize(self, image) -> list:
"""双引擎并发识别,返回得分最高的结果
@@ -463,6 +553,10 @@ class MsgBotApp(ctk.CTk):
# 自动回复执行队列(转发到主线程,避免 daemon 线程 SetForegroundWindow 失败)
self._reply_queue: queue.Queue[dict] = queue.Queue()
# ⭐ 启动时系统内存清理(强制回收Python进程内存)
self._log("🧹 启动内存清理...")
threading.Thread(target=_force_system_memory_cleanup, daemon=True).start()
# ⭐ OCR引擎管理器(双引擎:EasyOCR + Windows OCR)
self._ocr_manager = OCREngineManager(log_callback=self._log)
# 预初始化OCR引擎(后台线程,不阻塞UI)
@@ -1325,11 +1419,18 @@ class MsgBotApp(ctk.CTk):
# ── 窗口关闭 ──
def _on_close(self):
# ⭐ 关闭窗口同样把 stop 放到后台线程,完成后再关闭,避免主线程冻结。
# ⭐ 关闭窗口:释放OCR + 系统内存清理 → stop → destroy
def _stop_then_close():
try:
# 释放OCR引擎(杀子进程+系统内存回收)
self._ocr_manager._release()
except Exception:
pass
try:
self.monitor.stop()
except Exception:
pass
# 最终系统内存清理
_force_system_memory_cleanup()
self.after(0, self.destroy)
threading.Thread(target=_stop_then_close, daemon=True, name="gui-stop-close").start()