diff --git a/main.py b/main.py index 81081be..bd42f58 100644 --- a/main.py +++ b/main.py @@ -16,6 +16,8 @@ from mcp import ClientSession, StdioServerParameters, types # --- Keyboard Imports --- import threading import time +# Import MessageDeduplication from ui_interaction +from ui_interaction import MessageDeduplication try: import keyboard # Needs pip install keyboard except ImportError: @@ -483,9 +485,12 @@ async def run_main_with_exit_stack(): # 5. Start UI Monitoring in a separate thread print("\n--- Starting UI monitoring thread ---") - # Use the new monitoring loop function, passing both queues + # 5c. Create MessageDeduplication instance + deduplicator = MessageDeduplication(expiry_seconds=3600) # Default 1 hour + + # Use the new monitoring loop function, passing both queues and the deduplicator monitor_task = loop.create_task( - asyncio.to_thread(ui_interaction.run_ui_monitoring_loop, trigger_queue, command_queue), # Pass command_queue + asyncio.to_thread(ui_interaction.run_ui_monitoring_loop, trigger_queue, command_queue, deduplicator), # Pass command_queue and deduplicator name="ui_monitor" ) ui_monitor_task = monitor_task # Store task reference for shutdown @@ -493,6 +498,25 @@ async def run_main_with_exit_stack(): # 5b. Game Window Monitoring is now handled by Setup.py + # 5d. Start Periodic Cleanup Timer for Deduplicator + def periodic_cleanup(): + if not shutdown_requested: # Only run if not shutting down + print("Main Thread: Running periodic deduplicator cleanup...") + deduplicator.purge_expired() + # Reschedule the timer + cleanup_timer = threading.Timer(600, periodic_cleanup) # 10 minutes + cleanup_timer.daemon = True + cleanup_timer.start() + else: + print("Main Thread: Shutdown requested, not rescheduling deduplicator cleanup.") + + print("\n--- Starting periodic deduplicator cleanup timer (10 min interval) ---") + initial_cleanup_timer = threading.Timer(600, periodic_cleanup) + initial_cleanup_timer.daemon = True + initial_cleanup_timer.start() + # Note: This timer will run in a separate thread. + # Ensure it's handled correctly on shutdown if it holds resources. + # Since it's a daemon thread and reschedules itself, it should exit when the main program exits. # 6. Start the main processing loop (non-blocking check on queue) print("\n--- Wolfhart chatbot has started (waiting for triggers) ---") diff --git a/templates/chat_option.png b/templates/chat_option.png new file mode 100644 index 0000000..943862a Binary files /dev/null and b/templates/chat_option.png differ diff --git a/templates/update_confirm.png b/templates/update_confirm.png new file mode 100644 index 0000000..95cd87b Binary files /dev/null and b/templates/update_confirm.png differ diff --git a/ui_interaction.py b/ui_interaction.py index 1b47c2f..d575cfd 100644 --- a/ui_interaction.py +++ b/ui_interaction.py @@ -18,6 +18,64 @@ import queue from typing import List, Tuple, Optional, Dict, Any import threading # Import threading for Lock if needed, or just use a simple flag import math # Added for distance calculation in dual method +import time # Ensure time is imported for MessageDeduplication + +class MessageDeduplication: + def __init__(self, expiry_seconds=3600): # 1 hour expiry time + self.processed_messages = {} # {composite_key: timestamp} + self.expiry_seconds = expiry_seconds + + def create_key(self, sender, content): + """Create a standardized composite key.""" + # Thoroughly standardize text - remove all whitespace and punctuation, lowercase + clean_content = ''.join(c.lower() for c in content if c.isalnum()) + clean_sender = ''.join(c.lower() for c in sender if c.isalnum()) + + # Truncate content to first 100 chars to prevent overly long keys + if len(clean_content) > 100: + clean_content = clean_content[:100] + + return f"{clean_sender}:{clean_content}" + + def is_duplicate(self, sender, content): + """Check if the message is a duplicate within the expiry period.""" + if not sender or not content: + return False # Missing necessary info, treat as new message + + key = self.create_key(sender, content) + current_time = time.time() + + # Check if duplicate and not expired + if key in self.processed_messages: + last_time = self.processed_messages[key] + if current_time - last_time < self.expiry_seconds: + print(f"Deduplicator: Detected duplicate message: {sender} - {content[:20]}...") + return True + + # Update processing time + self.processed_messages[key] = current_time + return False + + def purge_expired(self): + """Remove expired message records.""" + current_time = time.time() + expired_keys = [k for k, t in self.processed_messages.items() + if current_time - t >= self.expiry_seconds] + + for key in expired_keys: + del self.processed_messages[key] + + if expired_keys: # Log only if something was purged + print(f"Deduplicator: Purged {len(expired_keys)} expired message records.") + return len(expired_keys) + + def clear_all(self): + """Clear all recorded messages (for F7/F8 functionality).""" + count = len(self.processed_messages) + self.processed_messages.clear() + if count > 0: # Log only if something was cleared + print(f"Deduplicator: Cleared all {count} message records.") + return count # --- Global Pause Flag --- # Using a simple mutable object (list) for thread-safe-like access without explicit lock @@ -142,6 +200,9 @@ PROFILE_OPTION_IMG = os.path.join(TEMPLATE_DIR, "profile_option.png") COPY_NAME_BUTTON_IMG = os.path.join(TEMPLATE_DIR, "copy_name_button.png") SEND_BUTTON_IMG = os.path.join(TEMPLATE_DIR, "send_button.png") CHAT_INPUT_IMG = os.path.join(TEMPLATE_DIR, "chat_input.png") +# 新增的模板路徑 +CHAT_OPTION_IMG = os.path.join(TEMPLATE_DIR, "chat_option.png") +UPDATE_CONFIRM_IMG = os.path.join(TEMPLATE_DIR, "update_confirm.png") # State Detection PROFILE_NAME_PAGE_IMG = os.path.join(TEMPLATE_DIR, "Profile_Name_page.png") PROFILE_PAGE_IMG = os.path.join(TEMPLATE_DIR, "Profile_page.png") @@ -1629,7 +1690,7 @@ def perform_state_cleanup(detector: DetectionModule, interactor: InteractionModu # --- UI Monitoring Loop Function (To be run in a separate thread) --- -def run_ui_monitoring_loop(trigger_queue: queue.Queue, command_queue: queue.Queue): +def run_ui_monitoring_loop(trigger_queue: queue.Queue, command_queue: queue.Queue, deduplicator: 'MessageDeduplication'): """ Continuously monitors the UI, detects triggers, performs interactions, puts trigger data into trigger_queue, and processes commands from command_queue. @@ -1667,7 +1728,9 @@ def run_ui_monitoring_loop(trigger_queue: queue.Queue, command_queue: queue.Queu 'page_sec': PAGE_SEC_IMG, 'page_str': PAGE_STR_IMG, 'dismiss_button': DISMISS_BUTTON_IMG, 'confirm_button': CONFIRM_BUTTON_IMG, 'close_button': CLOSE_BUTTON_IMG, 'back_arrow': BACK_ARROW_IMG, - 'reply_button': REPLY_BUTTON_IMG + 'reply_button': REPLY_BUTTON_IMG, + # 添加新模板 + 'chat_option': CHAT_OPTION_IMG, 'update_confirm': UPDATE_CONFIRM_IMG, } legacy_templates = { # Deprecated Keywords (for legacy method fallback) @@ -1773,13 +1836,15 @@ def run_ui_monitoring_loop(trigger_queue: queue.Queue, command_queue: queue.Queu elif action == 'clear_history': # Added for F7 print("UI Thread: Processing clear_history command.") recent_texts.clear() - print("UI Thread: recent_texts cleared.") + deduplicator.clear_all() # Simultaneously clear deduplication records + print("UI Thread: recent_texts and deduplicator records cleared.") elif action == 'reset_state': # Added for F8 resume print("UI Thread: Processing reset_state command.") recent_texts.clear() last_processed_bubble_info = None - print("UI Thread: recent_texts cleared and last_processed_bubble_info reset.") + deduplicator.clear_all() # Simultaneously clear deduplication records + print("UI Thread: recent_texts, last_processed_bubble_info, and deduplicator records reset.") else: print(f"UI Thread: Received unknown command: {action}") @@ -1804,6 +1869,19 @@ def run_ui_monitoring_loop(trigger_queue: queue.Queue, command_queue: queue.Queu # --- If not paused, proceed with UI Monitoring --- # print("[DEBUG] UI Loop: Monitoring is active. Proceeding...") # DEBUG REMOVED + # --- 添加檢查 chat_option 狀態 --- + try: + chat_option_locs = detector._find_template('chat_option', confidence=0.8) + if chat_option_locs: + print("UI Thread: Detected chat_option overlay. Pressing ESC to dismiss...") + interactor.press_key('esc') + time.sleep(0.2) # 給一點時間讓界面響應 + print("UI Thread: Pressed ESC to dismiss chat_option. Continuing...") + continue # 重新開始循環以確保界面已清除 + except Exception as chat_opt_err: + print(f"UI Thread: Error checking for chat_option: {chat_opt_err}") + # 繼續執行,不要中斷主流程 + # --- Check for Main Screen Navigation --- # print("[DEBUG] UI Loop: Checking for main screen navigation...") # DEBUG REMOVED try: @@ -1842,8 +1920,19 @@ def run_ui_monitoring_loop(trigger_queue: queue.Queue, command_queue: queue.Queu # Use a slightly lower confidence maybe, or state_confidence chat_room_locs = detector._find_template('chat_room', confidence=detector.state_confidence) if not chat_room_locs: - print("UI Thread: Not in chat room state before bubble detection. Attempting cleanup...") - # Call the existing cleanup function to try and return + print("UI Thread: Not in chat room state before bubble detection. Checking for update confirm...") + + # 檢查是否存在更新確認按鈕 + update_confirm_locs = detector._find_template('update_confirm', confidence=0.8) + if update_confirm_locs: + print("UI Thread: Detected update_confirm button. Clicking to proceed...") + interactor.click_at(update_confirm_locs[0][0], update_confirm_locs[0][1]) + time.sleep(0.5) # 給更新過程一些時間 + print("UI Thread: Clicked update_confirm button. Continuing...") + continue # 重新開始循環以重新檢查狀態 + + # 沒有找到更新確認按鈕,繼續原有的清理邏輯 + print("UI Thread: No update_confirm button found. Attempting cleanup...") perform_state_cleanup(detector, interactor) # Regardless of cleanup success, restart the loop to re-evaluate state from the top print("UI Thread: Continuing loop after attempting chat room cleanup.") @@ -2010,16 +2099,6 @@ def run_ui_monitoring_loop(trigger_queue: queue.Queue, command_queue: queue.Queu perform_state_cleanup(detector, interactor) # Attempt cleanup continue # Skip to next bubble - # Check recent text history - # print("[DEBUG] UI Loop: Checking recent text history...") # DEBUG REMOVED - if bubble_text in recent_texts: - print(f"Content '{bubble_text[:30]}...' in recent history, skipping this bubble.") - continue # Skip to next bubble - - print(">>> New trigger event <<<") - # Add to recent texts *before* potentially long interaction - recent_texts.append(bubble_text) - # 5. Interact: Get Sender Name (uses re-location internally via retrieve_sender_name_interaction) # print("[DEBUG] UI Loop: Retrieving sender name...") # DEBUG REMOVED sender_name = None @@ -2097,6 +2176,32 @@ def run_ui_monitoring_loop(trigger_queue: queue.Queue, command_queue: queue.Queu print("Error: Could not get sender name for this bubble, skipping.") continue # Skip to next bubble + # --- Deduplication Check --- + # This is the new central point for deduplication and recent_texts logic + if sender_name and bubble_text: # Ensure both are valid before deduplication + if deduplicator.is_duplicate(sender_name, bubble_text): + print(f"UI Thread: Skipping duplicate message via Deduplicator: {sender_name} - {bubble_text[:30]}...") + # Cleanup UI state as interaction might have occurred during sender_name retrieval + perform_state_cleanup(detector, interactor) + continue # Skip this bubble + + # If not a duplicate by deduplicator, then check recent_texts (original safeguard) + if bubble_text in recent_texts: + print(f"UI Thread: Content '{bubble_text[:30]}...' in recent_texts history, skipping.") + perform_state_cleanup(detector, interactor) # Cleanup as we are skipping + continue + + # If not a duplicate by any means, add to recent_texts and proceed + print(">>> New trigger event (passed deduplication) <<<") + recent_texts.append(bubble_text) + else: + # This case implies sender_name or bubble_text was None/empty, + # which should have been caught by earlier checks. + # If somehow reached, log and skip. + print(f"Warning: sender_name ('{sender_name}') or bubble_text ('{bubble_text[:30]}...') is invalid before deduplication check. Skipping.") + perform_state_cleanup(detector, interactor) + continue + # --- Attempt to activate reply context --- # print("[DEBUG] UI Loop: Attempting to activate reply context...") # DEBUG REMOVED reply_context_activated = False