"""主自动化逻辑 编排整个工作流程:扫描未读聊天 → 点击进入 → 点击图片/文件 → 关闭预览 → 关闭会话 → 循环 """ import logging import time from .ax_bridge import AXBridge from .config import Config from .human_like import HumanBehavior from .state_machine import StateMachine, UIState from .wechat_ui import WeChatUI logger = logging.getLogger("wechat_clicker.automator") class WeChatAutomator: """微信消息自动点击器""" def __init__(self, config: Config, dry_run: bool = False): self.config = config self.dry_run = dry_run # 初始化各组件 self.ax = AXBridge() self.ui = WeChatUI(self.ax, config.bundle_id) self.state = StateMachine(self.ax, self.ui) self.human = HumanBehavior(config) # 统计 self._scan_count = 0 self._total_chats_processed = 0 self._total_media_clicked = 0 # ---------------------------------------------------------------- # 启动检查 # ---------------------------------------------------------------- def verify_setup(self) -> bool: """验证环境和权限。""" # 检查辅助功能权限 if not self.ax.check_accessibility(): logger.error("缺少辅助功能权限,无法继续") return False # 检查微信是否运行 app_ref = self.ui.get_app_ref() if app_ref is None: logger.error("微信未运行,请先启动微信桌面端") return False # 检查主窗口 main_win = self.ui.get_main_window() if main_win is None: logger.error("未找到微信主窗口,请确保微信已登录并可见") return False logger.info("环境检查通过") return True # ---------------------------------------------------------------- # 主循环 # ---------------------------------------------------------------- def run(self): """永久运行的主循环。""" logger.info("微信自动点击器启动") if not self.verify_setup(): return while True: try: self._run_one_cycle() except KeyboardInterrupt: logger.info("用户中断,正在退出...") break except Exception as e: logger.error(f"主循环异常: {e}", exc_info=True) # 尝试恢复状态 try: self.state.recover_to_chat_list() except Exception: pass time.sleep(10) self._print_stats() def run_once(self): """运行一次扫描循环。""" logger.info("执行单次扫描") if not self.verify_setup(): return self._run_one_cycle() self._print_stats() def _run_one_cycle(self): """执行一个完整的扫描-处理循环。""" # 检查工作时间 if self.human.is_off_hours(): logger.info("当前为非工作时间,等待中...") time.sleep(300) return # 偶尔触发长休息 if self.human.should_take_break(): self.human.long_break() return # 确保微信在前台 self.ui.ensure_wechat_frontmost() time.sleep(0.5) # 恢复到聊天列表 if not self.state.recover_to_chat_list(): logger.warning("无法恢复到聊天列表,跳过本次循环") time.sleep(10) return self._scan_count += 1 # 检查是否有未读消息 global_unread = self.ui.get_global_unread_count() if global_unread == 0: logger.debug(f"[扫描#{self._scan_count}] 没有未读消息") sleep_time = self.human.scan_interval_with_jitter() time.sleep(sleep_time) return logger.info(f"[扫描#{self._scan_count}] 全局未读: {global_unread}") # 获取未读聊天列表 unread_chats = self.ui.get_unread_chats() if not unread_chats: logger.debug("聊天列表中未发现未读项(可能需要滚动)") sleep_time = self.human.scan_interval_with_jitter() time.sleep(sleep_time) return # 过滤和限制数量 chats_to_process = [] for chat in unread_chats: if self.config.should_process_chat(chat.name): chats_to_process.append(chat) count = self.human.random_subset_count( len(chats_to_process), self.config.max_chats_per_scan ) chats_to_process = chats_to_process[:count] logger.info( f"将处理 {len(chats_to_process)} 个聊天 " f"(共 {len(unread_chats)} 个未读)" ) # 逐个处理 for chat in chats_to_process: self.human.delay("before_click_chat") self._process_chat(chat) self.human.delay("before_close_chat") # 等待下次扫描 sleep_time = self.human.scan_interval_with_jitter() logger.debug(f"等待 {sleep_time:.0f}s 后进行下次扫描") time.sleep(sleep_time) # ---------------------------------------------------------------- # 处理单个聊天 # ---------------------------------------------------------------- def _process_chat(self, chat): """打开一个聊天,处理其中的媒体消息,然后关闭。""" logger.info(f"处理聊天: {chat.name} (未读: {chat.unread_count})") if self.dry_run: logger.info(f" [DRY-RUN] 跳过点击: {chat.name}") return # 点击聊天项打开会话 if not self.ax.press(chat.element): logger.warning(f" 点击聊天项失败: {chat.name}") return self.human.delay("after_open_chat") # 验证会话窗口已打开 state = self.state.detect_state() if state != UIState.CONVERSATION_OPEN: logger.warning( f" 会话窗口未打开 (状态: {state.value}),尝试恢复" ) self.state.recover_to_chat_list() return # 处理会话中的媒体 conv_window = self.ui.get_conversation_window() if conv_window: media_count = self._process_media(conv_window, chat.name) self._total_media_clicked += media_count self._total_chats_processed += 1 # 关闭会话窗口 self.human.delay("before_close_chat") if not self.state.close_current_conversation(): logger.warning(" 关闭会话失败,尝试恢复") self.state.recover_to_chat_list() # ---------------------------------------------------------------- # 处理媒体消息 # ---------------------------------------------------------------- def _process_media(self, conv_window, chat_name: str) -> int: """在打开的会话中查找并点击媒体消息。 Returns: 点击的媒体数量。 """ media_messages = self.ui.get_media_messages(conv_window) if not media_messages: logger.debug(f" {chat_name}: 未发现可见媒体消息") return 0 # 过滤需要点击的类型 targets = [] for msg in media_messages: if msg.msg_type == "image" and self.config.click_images: targets.append(msg) elif msg.msg_type == "file" and self.config.click_files: targets.append(msg) elif msg.msg_type == "video" and self.config.click_videos: targets.append(msg) if not targets: logger.debug(f" {chat_name}: 无需处理的媒体") return 0 # 限制数量,从最新的开始(列表末尾 = 最新) max_count = self.config.max_media_per_chat targets = targets[-max_count:] if len(targets) > max_count else targets # 反转,从最新的开始处理 targets = list(reversed(targets)) logger.info(f" {chat_name}: 发现 {len(targets)} 个媒体消息待处理") clicked = 0 for msg in targets: self.human.delay("before_click_media") short_title = msg.title.replace("\n", " ")[:50] logger.info(f" 点击{msg.msg_type}: {short_title}") # 点击媒体 if not self.ax.press(msg.element): logger.warning(f" 点击失败: {short_title}") continue self.human.delay("after_click_media") # 关闭可能出现的预览 self._dismiss_preview_safe() clicked += 1 self.human.delay("between_messages") # 检查连续错误 if self.ax.should_backoff(): logger.warning("连续错误过多,暂停处理") self.ax.reset_error_count() break return clicked def _dismiss_preview_safe(self): """安全地关闭可能出现的媒体预览。""" # 等一小会儿让预览可能出现 self.human.micro_jitter() state = self.state.detect_state() if state == UIState.MEDIA_PREVIEW: self.human.delay("before_close_preview") self.state.dismiss_preview() else: # 保守策略:即使未检测到预览也发送 Escape # 因为某些预览可能不会创建新窗口 self.ax.send_escape_key() time.sleep(0.3) # ---------------------------------------------------------------- # 调试工具 # ---------------------------------------------------------------- def dump_ui_tree(self): """输出微信 UI 元素树(调试用)。""" if not self.verify_setup(): return self.ui.ensure_wechat_frontmost() time.sleep(0.5) # 输出主窗口 main_win = self.ui.get_main_window() if main_win: print("=== 主窗口 (微信) ===") print(self.ax.dump_element(main_win, max_depth=5)) # 输出会话窗口 conv_windows = self.ui.get_conversation_windows() for win in conv_windows: title = self.ax.get_title(win) print(f"\n=== 会话窗口 ({title}) ===") print(self.ax.dump_element(win, max_depth=5)) # 输出聊天列表解析结果 print("\n=== 聊天列表解析 ===") items = self.ui.get_chat_items() for item in items: status = f"[未读:{item.unread_count}]" if item.unread_count > 0 else "" print(f" {item.name} {status} | {item.preview} | {item.timestamp}") # ---------------------------------------------------------------- # 统计 # ---------------------------------------------------------------- def _print_stats(self): """输出运行统计。""" logger.info( f"运行统计: 扫描={self._scan_count}, " f"处理聊天={self._total_chats_processed}, " f"点击媒体={self._total_media_clicked}" )