fix: 右侧消息区域 30x 权重 + 连续空读强制重扫,修复缓存错选联系人列表
This commit is contained in:
+64
-55
@@ -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():
|
||||||
|
|||||||
Reference in New Issue
Block a user