From 781a578412c89b9adc86e5d2ff98670914809943 Mon Sep 17 00:00:00 2001 From: youpaishidifu Date: Sat, 29 Aug 2026 21:55:59 +0800 Subject: [PATCH] =?UTF-8?q?fix(msgbot):=20=E4=BF=AE=E5=A4=8DOCR=E7=A9=BA?= =?UTF-8?q?=E5=88=97=E8=A1=A8IndexError=E5=B9=B6=E5=A2=9E=E5=BC=BAstop?= =?UTF-8?q?=E5=86=85=E5=AD=98=E9=87=8A=E6=94=BE=E8=87=B3=E6=9C=80=E4=BD=8E?= =?UTF-8?q?=E5=8D=A0=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task #1622: 行合并循环在 merged_lines 为空时无条件访问 merged_lines[-1] 抛 IndexError,被最外层 except 记为 [qq] OCR失败: list index out of range。 增加 `if merged_lines else False` 守卫,首行非@结尾/不含问号/非bot回复时 不再越界。 Task #1623: 原 _release_memory 仅 SetProcessWorkingSetSize(-1,-1) 不逼出常驻 页,且未释放 winrt Windows OCR 引擎、未清 GPU 缓存,导致 bot 运行后暂停 内存仍高达~600MB。增强为 EmptyWorkingSet 强制归还物理内存 + torch.cuda.empty_cache/synchronize + 释放 self._ocr_engine 引用,与 GUI 侧 _force_system_memory_cleanup 范式一致,使运行后暂停与刚启动待机内存一致。 --- monitor.py | 96 +++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 69 insertions(+), 27 deletions(-) diff --git a/monitor.py b/monitor.py index 5be986b..94b7bb7 100644 --- a/monitor.py +++ b/monitor.py @@ -1020,47 +1020,86 @@ class MessageMonitor: return self._stop_event.wait(timeout=seconds) def _release_memory(self): - """主动释放内存,交还系统重新分配调度。只保留GUI需要的基本内存。""" + """主动释放内存,交还系统重新分配调度。只保留GUI需要的基本内存。 + + ⭐ Task #1623: 增强版——除原有 SetProcessWorkingSetSize 外,新增: + 1. psapi.EmptyWorkingSet 强制把工作集页面归还 OS(SetProcessWorkingSetSize(-1,-1) + 只会回收"已 eligible"的页面,无法把 OCR 引擎/图像缓存等常驻页逼出内存, + 故 600MB 残留)。 + 2. torch.cuda.empty_cache() + synchronize() 归还 GPU 显存。 + 3. 释放主进程内 Windows OCR 引擎 self._ocr_engine(winrt 原生对象,stop 时 + 不释放会长期占用数百 MB)。 + 目标:待机(刚启动 / bot 运行一会后暂停)内存都只保留最低所需。 + """ import gc import ctypes import sys - - # 强制垃圾回收 + + # 1) 先释放 Python 侧大对象引用,再回收,避免 IMAGE/torch tensor 被环引用困住 + # 清空大对象引用 + for attr in ['_last_texts', '_seen_messages', '_replied_texts']: + if hasattr(self, attr): + getattr(self, attr).clear() + if hasattr(self, '_semantic_fingerprint_cache'): + self._semantic_fingerprint_cache.clear() + + # ⭐ Task #1623: 释放 Windows OCR 引擎(winrt 原生对象) + if getattr(self, '_ocr_engine', None) is not None: + try: + del self._ocr_engine + except Exception: + pass + self._ocr_engine = None + logger.debug("[memory] 已释放 Windows OCR 引擎引用") + + # 清空 EasyOCR 相关缓存 + if hasattr(self, '_easyocr_proc') and self._easyocr_proc: + # 子进程已杀,但引用可能残留 + self._easyocr_proc = None + + # 强制垃圾回收(释放上述被清引用占据的对象) gc.collect() - - # Windows 特定:使用 EmptyProcessWorkingSet 释放工作集内存 + + # 2) Windows 特定:把工作集物理内存归还 OS try: - # 获取当前进程句柄 kernel32 = ctypes.windll.kernel32 psapi = ctypes.windll.psapi - - # EmptyProcessWorkingSet 函数:将进程工作集最小化 - # 参数:进程句柄,最小值(-1表示当前进程),最大值(-1表示当前进程) - # 返回:非零表示成功 + h_process = kernel32.GetCurrentProcess() + + # EmptyWorkingSet:强制把进程工作集所有页面换出到页面文件/丢弃, + # 是真正把物理 RAM 交还 OS 的调用(SetProcessWorkingSetSize 仅设上下限)。 + try: + if psapi.EmptyWorkingSet(h_process): + logger.debug("[memory] 已调用 EmptyWorkingSet 归还物理内存") + else: + logger.debug("[memory] EmptyWorkingSet 返回 0(无需归还)") + except Exception as e: + logger.debug(f"[memory] EmptyWorkingSet 失败: {e}") + + # SetProcessWorkingSetSize(-1,-1):允许系统后续按需在最小/最大间调度 kernel32.SetProcessWorkingSetSize( - kernel32.GetCurrentProcess(), + h_process, -1, # 最小工作集:允许系统回收所有可回收页面 -1 # 最大工作集:允许系统回收所有可回收页面 ) logger.debug("[memory] 已调用 SetProcessWorkingSetSize 释放工作集内存") except Exception as e: logger.debug(f"[memory] SetProcessWorkingSetSize 失败: {e}") - - # 清空 EasyOCR 相关缓存 - if hasattr(self, '_easyocr_proc') and self._easyocr_proc: - # 子进程已杀,但引用可能残留 - self._easyocr_proc = None - - # 清空大对象引用 - for attr in ['_last_texts', '_seen_messages', '_replied_texts']: - if hasattr(self, attr): - getattr(self, attr).clear() - if hasattr(self, '_semantic_fingerprint_cache'): - self._semantic_fingerprint_cache.clear() - - # 再次垃圾回收 + + # 3) 归还 GPU 显存(若 torch/CUDA 可用) + try: + import torch + if torch.cuda.is_available(): + torch.cuda.synchronize() + torch.cuda.empty_cache() + logger.debug("[memory] 已调用 torch.cuda.empty_cache 归还显存") + except Exception: + # torch 未安装或当前为 CPU 推理,忽略 + pass + + # 再次垃圾回收,确保上述释放后的垃圾被最终回收 gc.collect() - + logger.info("[memory] 内存释放完成,已交还系统") def _kill_easyocr_proc(self, reason: str = ""): @@ -3619,7 +3658,10 @@ class MessageMonitor: or re.match(r'^[\u4e00-\u9fff]{2,8}[@,,]', _next_line) \ or re.match(r'^[/@((]', _next_line): _next_is_short_or_name = True - _prev_ends_with_comma = merged_lines[-1].rstrip().endswith((',', ',')) + # ⭐ Task #1622: 必须先判断 merged_lines 非空,否则首行不以@结尾/不含问号/ + # 非bot回复时 merged_lines[-1] 抛 IndexError → 被最外层 except 捕获成 + # [qq] OCR失败: list index out of range + _prev_ends_with_comma = merged_lines[-1].rstrip().endswith((',', ',')) if merged_lines else False if merged_lines and not _SENT_END.search(merged_lines[-1]) \ and not _TS_START.match(rl) and not _TS_START.match(merged_lines[-1]) \ and not _is_bot_reply_line and not _is_single_excl and not _prev_is_single_excl \