fix: 右侧消息区域 30x 权重 + 连续空读强制重扫,修复缓存错选联系人列表

This commit is contained in:
youpaishidifu
2026-07-24 20:31:44 +08:00
parent 33cc692a3a
commit 2563077bbc
+64 -55
View File
@@ -11,7 +11,7 @@ logger = logging.getLogger("msgbot.monitor")
class MessageMonitor: class MessageMonitor:
"""多线程消息监控器 — 窗口/消息列表缓存 + 浅层扫描""" """多线程消息监控器 — 右侧消息区加权 + 空读自动重扫"""
def __init__(self, on_message: Optional[Callable[[str, str], None]] = None): def __init__(self, on_message: Optional[Callable[[str, str], None]] = None):
self._running = False self._running = False
@@ -24,10 +24,12 @@ class MessageMonitor:
# 内容指纹 # 内容指纹
self._last_msg = {"wechat": "", "qq": ""} self._last_msg = {"wechat": "", "qq": ""}
# 窗口缓存 {(platform): Control 或 None} # 窗口缓存
self._cached_window: dict = {} self._cached_window: dict = {}
# 消息列表控件缓存 {(platform): Control 或 None} # 消息列表控件缓存
self._cached_list: dict = {} self._cached_list: dict = {}
# 连续空读计数(超阈值则强制重扫)
self._empty_reads: dict = {}
def set_poll_interval(self, seconds: float): def set_poll_interval(self, seconds: float):
self._poll_interval = max(0.5, seconds) self._poll_interval = max(0.5, seconds)
@@ -54,6 +56,7 @@ class MessageMonitor:
self._thread = None self._thread = None
self._cached_window.clear() self._cached_window.clear()
self._cached_list.clear() self._cached_list.clear()
self._empty_reads.clear()
logger.info("消息监控已停止") logger.info("消息监控已停止")
@property @property
@@ -65,11 +68,11 @@ class MessageMonitor:
def _run_loop(self): def _run_loop(self):
while self._running: while self._running:
try: try:
wechat_ok = not self._wechat_enabled or self._ensure_window("wechat") if self._wechat_enabled:
qq_ok = not self._qq_enabled or self._ensure_window("qq") self._ensure_window("wechat")
if wechat_ok:
self._poll("wechat") self._poll("wechat")
if qq_ok: if self._qq_enabled:
self._ensure_window("qq")
self._poll("qq") self._poll("qq")
except Exception as e: except Exception as e:
logger.debug(f"监控轮询异常: {e}") logger.debug(f"监控轮询异常: {e}")
@@ -77,12 +80,12 @@ class MessageMonitor:
# ── 窗口缓存管理 ──────────────────────────────────────── # ── 窗口缓存管理 ────────────────────────────────────────
def _ensure_window(self, platform: str) -> bool: def _ensure_window(self, platform: str):
"""确保窗口已缓存且仍有效,无效则清除缓存返回 False""" """缓存窗口,不缓存消息列表(每次 poll 重找,但直取右半侧)"""
win = self._cached_window.get(platform) win = self._cached_window.get(platform)
if win and win.Exists(maxSearchSeconds=0.1): if win and win.Exists(maxSearchSeconds=0.1):
return True return
# 缓存失效,重新搜索
self._cached_window.pop(platform, None) self._cached_window.pop(platform, None)
self._cached_list.pop(platform, None) self._cached_list.pop(platform, None)
names = {"wechat": ["微信", "WeChat"], "qq": ["QQ", "TIM"]} names = {"wechat": ["微信", "WeChat"], "qq": ["QQ", "TIM"]}
@@ -90,44 +93,37 @@ class MessageMonitor:
w = auto.WindowControl(Name=name) w = auto.WindowControl(Name=name)
if w.Exists(maxSearchSeconds=0.3): if w.Exists(maxSearchSeconds=0.3):
self._cached_window[platform] = w self._cached_window[platform] = w
# 同时找到消息列表控件 return
self._cached_list[platform] = self._find_list_control(w)
return True
return False
# ── 快速定位消息列表(只搜两层,非递归全树) ───────── # ── 定位右侧消息区域(窗口直接子控件,右侧加权 30x) ─
def _find_list_control(self, window): def _find_right_panel(self, window):
"""在窗口的直接子控件中找最可能是消息列表的控件。 """在窗口直接子控件中找最右侧且包含最多文本的控件。"""
策略:取所有第一层子控件 → 收集其直接子文本 Name →
按文本数排序,取文本最多的那个。
完全不递归,UIA 调用仅 2 层深度。
"""
try: try:
win_rect = window.BoundingRectangle
win_cx = win_rect.left + win_rect.width() // 2
best_ctrl = None best_ctrl = None
best_score = 0 best_score = 0
for child in window.GetChildren(): for child in window.GetChildren():
try: try:
# 跳过过小的控件(基本不可能是消息列表)
rect = child.BoundingRectangle rect = child.BoundingRectangle
if rect.width() < 100 or rect.height() < 50:
# 跳过太小的控件
if rect.width() < 80 or rect.height() < 40:
continue continue
# 收集该控件的直接子文本 # 右侧权重:聊天消息区域必定在右半侧
own_texts = 0 on_right = rect.left >= win_cx - 30
grand_texts = 0 right_weight = 30.0 if on_right else 1.0
# 只取一层子 Name,不递归全树
texts = 0
for sub in child.GetChildren(): for sub in child.GetChildren():
if sub.Name and sub.Name.strip(): if sub.Name and sub.Name.strip():
own_texts += 1 texts += 1
# 再下一层——但只取 Name,不递归
for gsub in sub.GetChildren():
if gsub.Name and gsub.Name.strip():
grand_texts += 1
# 评分 = 直接子文本 + 0.5 * 孙子文本(避免噪声) score = texts * right_weight
score = own_texts + grand_texts * 0.5
if score > best_score: if score > best_score:
best_score = score best_score = score
best_ctrl = child best_ctrl = child
@@ -147,35 +143,48 @@ class MessageMonitor:
if not win: if not win:
return return
try: # 重扫次数限制:最多每 10 秒重扫一次,避免空转
# 优先使用缓存的列表控件 now = time.time()
last_scan = getattr(self, f"_last_scan_{platform}", 0)
if now - last_scan < 10:
# 用缓存的老控件,但验证有效性
list_ctrl = self._cached_list.get(platform) list_ctrl = self._cached_list.get(platform)
if list_ctrl and list_ctrl.Exists(maxSearchSeconds=0.1): if list_ctrl and list_ctrl.Exists(maxSearchSeconds=0.1):
texts = self._get_texts_fast(list_ctrl) texts = self._get_texts_fast(list_ctrl)
else: else:
# 缓存失效,重新找 texts = []
list_ctrl = self._find_list_control(win) else:
self._cached_list[platform] = list_ctrl # 可以做一次重扫
texts = self._get_texts_fast(list_ctrl) if list_ctrl else [] list_ctrl = self._find_right_panel(win)
self._cached_list[platform] = list_ctrl
setattr(self, f"_last_scan_{platform}", now)
texts = self._get_texts_fast(list_ctrl) if list_ctrl else []
if not texts: if not texts:
return # 空读积累,连续 3 次空读强制重扫下一次
empty = self._empty_reads.get(platform, 0) + 1
self._empty_reads[platform] = empty
if empty >= 3:
self._cached_list.pop(platform, None)
setattr(self, f"_last_scan_{platform}", 0) # 允许立即重扫
return
latest = texts[-1].strip() # 有文本时重置空读计数器
if not latest: self._empty_reads[platform] = 0
return
# 内容指纹比较 latest = texts[-1].strip()
if latest != self._last_msg.get(platform, ""): if not latest:
self._last_msg[platform] = latest return
logger.info(f"[{platform}] {latest}")
if self._on_message: # 内容指纹比较
self._on_message(platform, latest) if latest != self._last_msg.get(platform, ""):
except Exception as e: self._last_msg[platform] = latest
logger.debug(f"[{platform}] 轮询异常: {e}") logger.info(f"[{platform}] {latest}")
if self._on_message:
self._on_message(platform, latest)
def _get_texts_fast(self, ctrl) -> list: def _get_texts_fast(self, ctrl) -> list:
"""从控件快速提取子文本(只取一层 Name + 常见子文本控件)。""" """从控件快速提取子文本(只取一层 Name)。"""
texts = [] texts = []
try: try:
for child in ctrl.GetChildren(): for child in ctrl.GetChildren():