窗口查找改纯PID+OCR垃圾过滤+md短标题修复
This commit is contained in:
+83
-101
@@ -34,6 +34,7 @@ import win32process
|
||||
import re
|
||||
import ctypes
|
||||
import difflib
|
||||
import psutil
|
||||
import os
|
||||
from datetime import datetime
|
||||
import signal
|
||||
@@ -206,100 +207,62 @@ class MessageMonitor:
|
||||
except Exception:
|
||||
pass
|
||||
elif platform == "qq":
|
||||
# 1. Win32 FindWindow 直搜 TXGuiFoundation(旧版 QQ 主窗口)
|
||||
# ⚠️ 必须检查 IsWindowVisible:QQ NT 在托盘时有一个不可见的 TXGuiFoundation
|
||||
# 托盘辅助窗口(空标题, IsWindowVisible=False),FindWindow 可能返回这个。
|
||||
# 不可见的窗口被缓存后,_poll 里 IsWindowVisible 检查会跳过所有后续 OCR。
|
||||
# 只按进程名找窗口,不管标题是什么。
|
||||
# TIM/QQ 标题是动态的(跟随会话显示),任何标题都合法。
|
||||
for proc_name in ["TIM.exe", "QQ.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:
|
||||
variant = "tim" if "TIM" in proc_name.upper() else "qq_nt"
|
||||
self._app_variant[platform] = variant
|
||||
self._cached_hwnd[platform] = top
|
||||
self._cached_pid[platform] = pid
|
||||
title = win32gui.GetWindowText(top)
|
||||
logger.info(f"[{platform}] 窗口已定位: {proc_name} pid={pid} title={title!r} top={top}")
|
||||
return top
|
||||
except Exception:
|
||||
continue
|
||||
# 兜底:UIA 搜索 TXGuiFoundation 类名窗口(进程已启动但窗口尚未就绪时)
|
||||
try:
|
||||
hwnd = win32gui.FindWindow("TXGuiFoundation", None)
|
||||
if hwnd:
|
||||
top = win32gui.GetAncestor(hwnd, win32con.GA_ROOT)
|
||||
if win32gui.IsWindow(top) and win32gui.IsWindowVisible(top):
|
||||
rect = win32gui.GetWindowRect(top)
|
||||
if (rect[2] - rect[0]) > 200 and (rect[3] - rect[1]) > 100:
|
||||
title = win32gui.GetWindowText(top)
|
||||
self._app_variant[platform] = "tim" if "TIM" in title.upper() else "qq_nt"
|
||||
self._cached_hwnd[platform] = top
|
||||
self._cached_pid[platform] = win32process.GetWindowThreadProcessId(top)[1]
|
||||
logger.info(f"[{platform}] 窗口已定位(Win32 TXGuiFoundation): {title} top={top}")
|
||||
return top
|
||||
root = auto.GetRootControl()
|
||||
for child in root.GetChildren():
|
||||
try:
|
||||
if child.ClassName == "TXGuiFoundation":
|
||||
rect = child.BoundingRectangle
|
||||
if rect.width() > 200 and rect.height() > 100:
|
||||
h = child.NativeWindowHandle
|
||||
top = win32gui.GetAncestor(h, win32con.GA_ROOT)
|
||||
pid = win32process.GetWindowThreadProcessId(top)[1]
|
||||
self._app_variant[platform] = "qq_nt"
|
||||
self._cached_hwnd[platform] = top
|
||||
self._cached_pid[platform] = pid
|
||||
logger.info(f"[{platform}] 窗口已定位(UIA TXGuiFoundation): top={top}")
|
||||
return top
|
||||
except Exception:
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
# 2. Win32 EnumWindows 兜底 — 类名+标题搜索
|
||||
# QQ NT (Electron) 类名 Chrome_WidgetWin_1, 传统 QQ 类名 TXGuiFoundation
|
||||
qq_titles = ("QQ", "TIM")
|
||||
def _enum_callback(hwnd, results):
|
||||
try:
|
||||
cls = win32gui.GetClassName(hwnd)
|
||||
title = win32gui.GetWindowText(hwnd)
|
||||
title_upper = title.upper()
|
||||
# 类名匹配(传统 QQ / TIM)
|
||||
if cls == "TXGuiFoundation":
|
||||
results.append(hwnd)
|
||||
return True
|
||||
# QQ NT Electron: 类名含 Chrome,标题含 QQ/TIM
|
||||
if "Chrome" in cls and ("QQ" in title_upper or "TIM" in title_upper):
|
||||
results.append(hwnd)
|
||||
return True
|
||||
# 标题完全匹配 QQ / TIM
|
||||
if title_upper in qq_titles:
|
||||
results.append(hwnd)
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
return True
|
||||
try:
|
||||
found = []
|
||||
win32gui.EnumWindows(_enum_callback, found)
|
||||
logger.debug(f"[{platform}] EnumWindows 候选: {len(found)} windows: "
|
||||
f"{[(win32gui.GetWindowText(h)[:30], hex(h)) for h in found]}")
|
||||
for hwnd in found:
|
||||
top = win32gui.GetAncestor(hwnd, win32con.GA_ROOT)
|
||||
if not (win32gui.IsWindow(top) and win32gui.IsWindowVisible(top)):
|
||||
cls = win32gui.GetClassName(top)
|
||||
title = win32gui.GetWindowText(top)
|
||||
logger.debug(f"[{platform}] Enum 候选跳过(不可见): cls={cls} title={title[:30]} vis={win32gui.IsWindowVisible(top)}")
|
||||
continue
|
||||
rect = win32gui.GetWindowRect(top)
|
||||
if not ((rect[2] - rect[0]) > 200 and (rect[3] - rect[1]) > 100):
|
||||
cls = win32gui.GetClassName(top)
|
||||
title = win32gui.GetWindowText(top)
|
||||
logger.debug(f"[{platform}] Enum 候选跳过(尺寸): cls={cls} title={title[:30]} rect={rect}")
|
||||
continue
|
||||
cls = win32gui.GetClassName(top)
|
||||
title = win32gui.GetWindowText(top)
|
||||
title_upper = title.upper().strip()
|
||||
# _enum_callback 只加了 QQ/TIM 相关窗口,所以 found 里都是有效候选
|
||||
# 优先选标题含 QQ/TIM 的(传统模式),其次任何 TXGuiFoundation 或 Chrome 都接受
|
||||
if "TIM" in title_upper:
|
||||
self._app_variant[platform] = "tim"
|
||||
else:
|
||||
self._app_variant[platform] = "qq_nt"
|
||||
self._cached_hwnd[platform] = top
|
||||
self._cached_pid[platform] = win32process.GetWindowThreadProcessId(top)[1]
|
||||
if "Chrome" in cls:
|
||||
logger.info(f"[{platform}] 窗口已定位(Electron Chrome): {title} top={top}")
|
||||
else:
|
||||
logger.info(f"[{platform}] 窗口已定位(Win32 Enum): {title} top={top}")
|
||||
return top
|
||||
except Exception:
|
||||
pass
|
||||
# 3. UIA 按窗口名搜索(兜底,可能慢但不挂死)
|
||||
for name in ["QQ", "TIM"]:
|
||||
try:
|
||||
w = auto.WindowControl(Name=name)
|
||||
if w.Exists(maxSearchSeconds=0.3):
|
||||
rect = w.BoundingRectangle
|
||||
if rect.width() > 200 and rect.height() > 100:
|
||||
h = w.NativeWindowHandle
|
||||
top = win32gui.GetAncestor(h, win32con.GA_ROOT)
|
||||
self._cached_hwnd[platform] = top
|
||||
self._cached_pid[platform] = win32process.GetWindowThreadProcessId(top)[1]
|
||||
self._app_variant[platform] = "qq_nt" if name == "QQ" else "tim"
|
||||
logger.info(f"[{platform}] 窗口已定位(UIA 名称): {name} top={top}")
|
||||
return top
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
def _ensure_hwnd_with_timeout(self, platform: str, timeout: float = 12.0) -> Optional[int]:
|
||||
@@ -844,9 +807,15 @@ class MessageMonitor:
|
||||
except Exception as e:
|
||||
logger.debug(f"[ocr] EasyOCR 识别失败: {e}")
|
||||
|
||||
if win_text and easy_text:
|
||||
logger.debug(f"[ocr] Windows OCR: {win_text[:100]!r}...")
|
||||
logger.debug(f"[ocr] EasyOCR: {easy_text[:100]!r}...")
|
||||
# 引擎原始输出逐行标记日志(用于长期对比两引擎能力)
|
||||
if win_text:
|
||||
for i, l in enumerate(win_text.split('\n')):
|
||||
if l.strip():
|
||||
logger.info(f"[ocr] [windows/原始] line[{i}]: {l.strip()[:120]!r}")
|
||||
if easy_text:
|
||||
for i, l in enumerate(easy_text.split('\n')):
|
||||
if l.strip():
|
||||
logger.info(f"[ocr] [easyocr/原始] line[{i}]: {l.strip()[:120]!r}")
|
||||
|
||||
raw_text, chosen_engine = self._pick_ocr_text(win_text, easy_text, im)
|
||||
if not raw_text:
|
||||
@@ -942,9 +911,15 @@ class MessageMonitor:
|
||||
except Exception as e:
|
||||
logger.debug(f"[wechat] EasyOCR 识别失败: {e}")
|
||||
|
||||
if win_text and easy_text:
|
||||
logger.debug(f"[wechat] Windows OCR: {win_text[:100]!r}...")
|
||||
logger.debug(f"[wechat] EasyOCR: {easy_text[:100]!r}...")
|
||||
# 引擎原始输出逐行标记日志(用于长期对比两引擎能力)
|
||||
if win_text:
|
||||
for i, l in enumerate(win_text.split('\n')):
|
||||
if l.strip():
|
||||
logger.info(f"[wechat] [windows/原始] line[{i}]: {l.strip()[:120]!r}")
|
||||
if easy_text:
|
||||
for i, l in enumerate(easy_text.split('\n')):
|
||||
if l.strip():
|
||||
logger.info(f"[wechat] [easyocr/原始] line[{i}]: {l.strip()[:120]!r}")
|
||||
|
||||
raw_text, chosen_engine = self._pick_ocr_text(win_text, easy_text, im)
|
||||
if not raw_text:
|
||||
@@ -981,7 +956,7 @@ class MessageMonitor:
|
||||
ocr_lines.append(l)
|
||||
engine_label = self._last_chosen_engine.get(platform, "")
|
||||
for i, rl in enumerate(ocr_lines):
|
||||
logger.info(f"[ocr] raw[{i}][{engine_label}] raw_line<{rl!r}>")
|
||||
logger.info(f"[ocr] [{engine_label}/后处理] line[{i}]: {rl[:120]!r}")
|
||||
|
||||
# —— 时间戳分割 ——
|
||||
expanded = []
|
||||
@@ -1039,6 +1014,8 @@ class MessageMonitor:
|
||||
continue
|
||||
line = re.sub(r'^0+', '', line)
|
||||
line = re.sub(r'[勖彐"\'\s\u2000-\u206f]+$', '', line)
|
||||
# 末尾裸字母:OCR checkbox [V] 碎成 "? O"、" O"、" V"
|
||||
line = re.sub(r'\s+[OV]\s*$', '', line)
|
||||
if not line:
|
||||
continue
|
||||
if re.match(r'^问题\d+\s*[::]', line):
|
||||
@@ -1059,6 +1036,9 @@ class MessageMonitor:
|
||||
continue
|
||||
if re.match(r'^[一-鿿]{1}$', line):
|
||||
continue
|
||||
# OCR 碎片:≤8 字符且无中文 → 时间戳/checkbox 残片
|
||||
if len(line) <= 8 and not re.search(r'[\u4e00-\u9fff]', line):
|
||||
continue
|
||||
if re.match(r'^\d{1,2}[::\s]\d{1,2}', line):
|
||||
continue
|
||||
if re.match(r'^\d{4}[/-]\d{1,2}', line):
|
||||
@@ -1412,11 +1392,13 @@ class MessageMonitor:
|
||||
Bot 回复用 md 格式(# 标题、[链接](url)、列表等),用户日常对话不用。
|
||||
借此区分 bot 旧回复和历史堆积 vs 用户新消息。
|
||||
"""
|
||||
if not text or len(text) < 10:
|
||||
if not text:
|
||||
return False
|
||||
# 行首是 # 标题(# ## ### #### 等)
|
||||
if re.match(r'^#{1,6}\s', text):
|
||||
# # 行首标题:短文本也检测(OCR 把 ##协议修 切成 5 字符片段)
|
||||
if re.match(r'^#{1,6}(?:\s|\[|[\u4e00-\u9fff])', text):
|
||||
return True
|
||||
if len(text) < 10:
|
||||
return False
|
||||
# ##直接跟汉字(OCR 丢失 ##后空格时常见:##文档导航)
|
||||
if re.match(r'^#{1,6}[\u4e00-\u9fff]', text):
|
||||
return True
|
||||
|
||||
Reference in New Issue
Block a user