wechat_msg_clicker/wechat_clicker/state_machine.py
2026-04-22 19:28:54 +08:00

202 lines
6.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""UI 状态机
基于微信窗口数量和名称判断当前 UI 状态,提供状态恢复能力。
微信 v4.1.9 窗口模型:
- 1 个窗口("微信"):主聊天列表
- 2 个窗口("微信" + 聊天名):会话已打开
- 3+ 个窗口:可能有图片预览等额外窗口
"""
import logging
import time
from enum import Enum
from .ax_bridge import AXBridge
from .wechat_ui import WeChatUI
logger = logging.getLogger("wechat_clicker.state_machine")
class UIState(Enum):
UNKNOWN = "unknown"
MAIN_CHAT_LIST = "main_chat_list"
CONVERSATION_OPEN = "conversation_open"
MEDIA_PREVIEW = "media_preview"
WECHAT_NOT_RUNNING = "wechat_not_running"
class StateMachine:
"""微信 UI 状态机"""
def __init__(self, ax: AXBridge, wechat_ui: WeChatUI):
self.ax = ax
self.ui = wechat_ui
self._current_state = UIState.UNKNOWN
self._conversation_name = None # 当前打开的会话名称
@property
def current_state(self) -> UIState:
return self._current_state
@property
def conversation_name(self) -> str:
return self._conversation_name or ""
# ----------------------------------------------------------------
# 状态检测
# ----------------------------------------------------------------
def detect_state(self) -> UIState:
"""检测当前微信 UI 状态。"""
windows = self.ui.get_all_windows()
if not windows:
self._current_state = UIState.WECHAT_NOT_RUNNING
self._conversation_name = None
logger.debug("未检测到微信窗口")
return self._current_state
# 分析窗口
main_window = None
other_windows = []
for win in windows:
title = self.ax.get_title(win)
if title == "微信":
main_window = win
elif title:
other_windows.append((win, title))
if main_window is None:
# 微信可能最小化了,或者窗口结构变化
self._current_state = UIState.UNKNOWN
logger.debug("未找到微信主窗口")
return self._current_state
window_count = len(windows)
if window_count == 1:
# 只有主窗口 = 聊天列表
self._current_state = UIState.MAIN_CHAT_LIST
self._conversation_name = None
elif window_count == 2 and len(other_windows) == 1:
# 主窗口 + 一个会话窗口
self._current_state = UIState.CONVERSATION_OPEN
self._conversation_name = other_windows[0][1]
elif window_count >= 3:
# 可能有预览窗口
self._current_state = UIState.MEDIA_PREVIEW
# 第一个非主窗口通常是会话
if other_windows:
self._conversation_name = other_windows[0][1]
else:
self._current_state = UIState.UNKNOWN
logger.debug(
f"状态检测: {self._current_state.value}, "
f"窗口数={window_count}, 会话={self._conversation_name}"
)
return self._current_state
# ----------------------------------------------------------------
# 状态恢复
# ----------------------------------------------------------------
def recover_to_chat_list(self, max_retries: int = 3) -> bool:
"""恢复到主聊天列表状态。关闭所有会话和预览窗口。
Returns:
True 如果成功恢复到聊天列表。
"""
for attempt in range(max_retries):
state = self.detect_state()
if state == UIState.MAIN_CHAT_LIST:
return True
if state == UIState.WECHAT_NOT_RUNNING:
logger.error("微信未运行,无法恢复")
return False
if state == UIState.MEDIA_PREVIEW:
# 先关闭预览Escape
logger.info("关闭媒体预览...")
self.ax.send_escape_key()
time.sleep(0.5)
if state in (UIState.CONVERSATION_OPEN, UIState.MEDIA_PREVIEW):
# 关闭会话窗口
self._close_all_conversations()
time.sleep(0.5)
if state == UIState.UNKNOWN:
# 尝试激活微信并点击侧边栏聊天按钮
self.ui.ensure_wechat_frontmost()
time.sleep(0.5)
self.ui.click_sidebar_chat_tab()
time.sleep(0.5)
logger.debug(f"恢复尝试 {attempt + 1}/{max_retries}")
# 最终检查
final_state = self.detect_state()
if final_state == UIState.MAIN_CHAT_LIST:
return True
logger.error(f"恢复失败,当前状态: {final_state.value}")
return False
def _close_all_conversations(self):
"""关闭所有会话窗口。"""
conv_windows = self.ui.get_conversation_windows()
for win in conv_windows:
title = self.ax.get_title(win)
close_btn = self.ui.get_close_button(win)
if close_btn:
logger.debug(f"关闭会话窗口: {title}")
self.ax.press(close_btn)
time.sleep(0.3)
else:
# 后备方案Cmd+W
logger.debug(f"使用 Cmd+W 关闭窗口: {title}")
self.ax.send_cmd_w()
time.sleep(0.3)
def close_current_conversation(self) -> bool:
"""关闭当前打开的会话窗口。"""
conv_window = self.ui.get_conversation_window()
if conv_window is None:
return True # 没有会话窗口,视为成功
close_btn = self.ui.get_close_button(conv_window)
if close_btn:
self.ax.press(close_btn)
else:
self.ax.send_cmd_w()
time.sleep(0.5)
# 验证
state = self.detect_state()
return state == UIState.MAIN_CHAT_LIST
def dismiss_preview(self) -> bool:
"""关闭媒体预览。"""
state = self.detect_state()
if state != UIState.MEDIA_PREVIEW:
return True # 没有预览打开
# 发送 Escape 关闭预览
self.ax.send_escape_key()
time.sleep(0.5)
# 验证
state = self.detect_state()
if state == UIState.MEDIA_PREVIEW:
# 再试一次
self.ax.send_escape_key()
time.sleep(0.5)
state = self.detect_state()
return state != UIState.MEDIA_PREVIEW