修复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:
+91
-39
@@ -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__))
|
||||
|
||||
Reference in New Issue
Block a user