修复4个问题:KB未回复诊断日志、微信唤醒bug、exe打包、截屏分目录

1. KB搜索命中但未回复:增加诊断日志记录每个检查步骤的失败原因
2. 微信窗口唤醒bug:增加Win32 API进程枚举兜底+可见窗口直接截图(不要求置前)
3. PyInstaller打包修复:自定义hook-torch.py避免isolated子进程access violation,
   改用onedir模式+CUDA_VISIBLE_DEVICES=""环境变量
4. OCR截屏分平台存放:img/raw/qq/ img/raw/wechat/ img/processed/qq/ img/processed/wechat/
This commit is contained in:
youpaishidifu
2026-08-02 23:44:43 +08:00
parent c853b3e504
commit 30106571d7
2 changed files with 107 additions and 39 deletions
+91 -39
View File
@@ -553,7 +553,13 @@ class MessageMonitor:
# 下一次清理时间:随机 30~60 秒后
self._next_ocr_clean_at = now + random.uniform(30, 60)
cutoff = now - self._ocr_clean_ttl
for d in (_get_ocr_raw_dir(), _get_ocr_processed_dir()):
# 清理平台子目录(当前存放位置)+ 根目录(历史遗留文件)
dirs_to_clean = [
_get_ocr_raw_dir("qq"), _get_ocr_raw_dir("wechat"),
_get_ocr_processed_dir("qq"), _get_ocr_processed_dir("wechat"),
_get_ocr_raw_dir(), _get_ocr_processed_dir(),
]
for d in dirs_to_clean:
try:
for f in os.listdir(d):
p = os.path.join(d, f)
@@ -685,7 +691,9 @@ class MessageMonitor:
h = w.NativeWindowHandle
# 确保存顶层窗口句柄(与 GetForegroundWindow 返回值一致)
top = win32gui.GetAncestor(h, win32con.GA_ROOT)
# 窗口不可见(最小化/完全遮挡)→ 截图会取到覆盖窗口内容
# 窗口不可见(最小化/隐藏)→ 跳过
# 注意:IsWindowVisible 检查 WS_VISIBLE 标志位,只要窗口在 Z 顺序中
# 未被标记为隐藏即返回 True(即使被其他窗口覆盖),符合需求。
if not win32gui.IsWindowVisible(top):
continue
self._cached_hwnd[platform] = top
@@ -695,6 +703,42 @@ class MessageMonitor:
return top
except Exception:
pass
# 兜底:通过 Win32 API 按进程名查找微信窗口(UIAutomation 在窗口被覆盖时可能找不到)
# 只要窗口可见(未被最小化/隐藏)就返回,与 QQ 逻辑一致
for proc_name in ["WeChat.exe", "微信"]:
try:
for proc in psutil.process_iter(['name', 'pid']):
if proc.info['name'] and proc.info['name'].lower() == proc_name.lower():
pid = proc.info['pid']
pid_windows = []
def _pid_enum_cb(hwnd, results, _pid=pid):
try:
_, found_pid = win32process.GetWindowThreadProcessId(hwnd)
if found_pid == _pid:
top = win32gui.GetAncestor(hwnd, win32con.GA_ROOT)
if top not in results:
results.append(top)
except Exception:
pass
return True
win32gui.EnumWindows(_pid_enum_cb, pid_windows)
for top in pid_windows:
if not (win32gui.IsWindow(top) and win32gui.IsWindowVisible(top)):
continue
rect = win32gui.GetWindowRect(top)
if (rect[2] - rect[0]) > 200 and (rect[3] - rect[1]) > 100:
self._cached_hwnd[platform] = top
self._cached_pid[platform] = pid
self._window_not_found_until.pop(platform, None)
title = win32gui.GetWindowText(top)
logger.info(f"[{platform}] 窗口已定位(Win32 进程枚举): {proc_name} pid={pid} title={title!r} top={top}")
return top
except Exception:
continue
elif platform == "qq":
# 只按进程名找窗口,不管标题是什么。
# TIM/QQ 标题是动态的(跟随会话显示),任何标题都合法。
@@ -1781,9 +1825,7 @@ class MessageMonitor:
# 保存调试截图(在颜色/哈希检查之前,即使跳过也有图可查)
_dbg_path = ""
try:
debug_dir = _get_ocr_raw_dir()
ts = datetime.now().strftime('%Y%m%d_%H%M%S.%f')[:19]
_dbg_path = os.path.join(debug_dir, f"ocr_debug_qq_{ts}.png")
debug_dir = _get_ocr_raw_dir("qq")
im.save(_dbg_path, format="PNG")
# 删除放缓:不在这里立即清理,改由 _maybe_clean_ocr_dir 每 30~60s 随机定时清理
if os.path.exists(_dbg_path):
@@ -1824,9 +1866,7 @@ class MessageMonitor:
logger.debug(f"[ocr] 预处理: resize 2x + sharpen(1.2) + contrast(1.2)")
# 保存OCR处理后的图片(JPG格式)用于肉眼对比
try:
debug_dir = _get_ocr_processed_dir()
ts = datetime.now().strftime('%Y%m%d_%H%M%S.%f')[:19]
_processed_path = os.path.join(debug_dir, f"ocr_processed_qq_{ts}.jpg")
debug_dir = _get_ocr_processed_dir("qq")
# 转换为RGB模式保存JPG(不支持RGBA)
if im.mode == 'RGBA':
im_rgb = im.convert('RGB')
@@ -1878,31 +1918,37 @@ class MessageMonitor:
return []
# ⭐ 先唤醒再截屏:ImageGrab.grab 截整屏坐标,若微信被遮挡(用户停在终端)
# 截到的就是遮挡物。唤醒后才取窗口 rect(最小化恢复后位置会变化)。
# 用 _ensure_foreground(AttachThreadInput 强置前):微信原生窗口安全,
# 且必须真正前台才截图——仅"可见但被其他窗口压着"时置前失败会截到遮挡物。
if not self._ensure_foreground(hwnd):
logger.debug("[wechat] 窗口未前置,退避5秒(用户可能在接管)")
# ⭐ 保存"窗口不可见"现场截图:即使整轮跳过也有图可查
# (此前微信 hwnd 有效但窗口被遮挡/最小化时直接 return,无任何留图)
try:
_r = win32gui.GetWindowRect(hwnd)
if _r and (_r[2] - _r[0]) > 200 and (_r[3] - _r[1]) > 100:
_im = self._safe_screenshot((_r[0], _r[1], _r[2], _r[3]))
if _im is not None and _im.width > 50 and _im.height > 50:
debug_dir = _get_ocr_raw_dir()
_ts = datetime.now().strftime('%Y%m%d_%H%M%S.%f')[:19]
_p = os.path.join(debug_dir, f"ocr_debug_wx_{_ts}.png")
_im.save(_p, format="PNG")
# 删除放缓:不在这里立即清理,改由 _maybe_clean_ocr_dir 每 30~60s 随机定时清理
logger.info(f"[wechat] 窗口不可见现场截图已保存: {_p}")
except Exception as _e:
logger.debug(f"[wechat] 现场截图保存失败: {_e}")
# 退避期间也要检查急停,避免阻塞5秒
for _ in range(50):
if not self._running:
return []
time.sleep(0.1)
return []
# 用 _ensure_foreground(AttachThreadInput 强置前):微信原生窗口安全。
# 若置前失败但窗口仍然可见(未被最小化/隐藏),仍应尝试截图,
# 与QQ逻辑一致:只要窗口在屏幕上就截,避免漏掉消息。
fg_ok = self._ensure_foreground(hwnd)
if not fg_ok:
# 检查窗口是否可见(未被最小化到托盘/隐藏)
root_hwnd = win32gui.GetAncestor(hwnd, win32con.GA_ROOT)
is_visible = root_hwnd and win32gui.IsWindowVisible(root_hwnd) and not win32gui.IsIconic(root_hwnd)
if is_visible:
logger.debug("[wechat] 窗口未获得前台但仍然可见,继续截图")
else:
logger.debug("[wechat] 窗口未前置且不可见,退避5秒(用户可能在接管)")
# ⭐ 保存"窗口不可见"现场截图:即使整轮跳过也有图可查
try:
_r = win32gui.GetWindowRect(hwnd)
if _r and (_r[2] - _r[0]) > 200 and (_r[3] - _r[1]) > 100:
_im = self._safe_screenshot((_r[0], _r[1], _r[2], _r[3]))
if _im is not None and _im.width > 50 and _im.height > 50:
debug_dir = _get_ocr_raw_dir("wechat")
_ts = datetime.now().strftime('%Y%m%d_%H%M%S.%f')[:19]
_p = os.path.join(debug_dir, f"ocr_debug_wx_{_ts}.png")
_im.save(_p, format="PNG")
logger.info(f"[wechat] 窗口不可见现场截图已保存: {_p}")
except Exception as _e:
logger.debug(f"[wechat] 现场截图保存失败: {_e}")
# 退避期间也要检查急停,避免阻塞5秒
for _ in range(50):
if not self._running:
return []
time.sleep(0.1)
return []
time.sleep(0.4) # 让 DWM 完成窗口激活的合成,防紧接 BitBlt 卡死
rect = win32gui.GetWindowRect(hwnd)
@@ -1931,7 +1977,7 @@ class MessageMonitor:
# 保存调试截图(在颜色/哈希检查之前,即使跳过也有图可查)
_dbg_path = ""
try:
debug_dir = _get_ocr_raw_dir()
debug_dir = _get_ocr_raw_dir("wechat")
ts = datetime.now().strftime('%Y%m%d_%H%M%S.%f')[:19]
_dbg_path = os.path.join(debug_dir, f"ocr_debug_wx_{ts}.png")
im.save(_dbg_path, format="PNG")
@@ -1972,7 +2018,7 @@ class MessageMonitor:
im = ImageEnhance.Contrast(im).enhance(1.2) # 降低对比度,保持笔画连贯
# 保存OCR处理后的图片(JPG格式)用于肉眼对比
try:
debug_dir = _get_ocr_processed_dir()
debug_dir = _get_ocr_processed_dir("wechat")
ts = datetime.now().strftime('%Y%m%d_%H%M%S.%f')[:19]
_processed_path = os.path.join(debug_dir, f"ocr_processed_wx_{ts}.jpg")
# 转换为RGB模式保存JPG(不支持RGBA)
@@ -4686,24 +4732,30 @@ class MessageMonitor:
self._cached_hwnd.pop(platform, None)
self._cached_pid.pop(platform, None)
def _get_ocr_raw_dir() -> str:
"""OCR 原始截图(PNG)保存目录:源码目录下的 img/raw。
def _get_ocr_raw_dir(platform: str = "") -> str:
"""OCR 原始截图(PNG)保存目录:源码目录下的 img/raw[/platform]。
统一存源码目录(历史版本即如此,从未被 360 误报),并按 raw / processed
分目录存放,原图与处理后图片互不混杂,便于排查。
platform 参数非空时返回对应平台子目录(qq / wechat),便于分类存放、
解决定时器轮询清理时图片堆积难以寻找的问题。
"""
try:
base = os.path.join(os.path.dirname(os.path.abspath(__file__)), "img", "raw")
if platform:
base = os.path.join(base, platform)
os.makedirs(base, exist_ok=True)
except Exception:
base = os.path.dirname(os.path.abspath(__file__))
return base
def _get_ocr_processed_dir() -> str:
"""OCR 处理后图片(JPG)保存目录:源码目录下的 img/processed。"""
def _get_ocr_processed_dir(platform: str = "") -> str:
"""OCR 处理后图片(JPG)保存目录:源码目录下的 img/processed[/platform]。"""
try:
base = os.path.join(os.path.dirname(os.path.abspath(__file__)), "img", "processed")
if platform:
base = os.path.join(base, platform)
os.makedirs(base, exist_ok=True)
except Exception:
base = os.path.dirname(os.path.abspath(__file__))