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