Add message deduplication system and UI fallback handling for updated game states

- Implemented `MessageDeduplication` class to suppress duplicate bot replies:
  - Normalizes sender and message content for reliable comparison.
  - Tracks processed messages with timestamp-based expiry (default 1 hour).
  - Integrated into `run_ui_monitoring_loop()` with support for F7/F8-based history resets.
  - Periodic cleanup thread purges expired entries every 10 minutes.

- Added new UI fallback handling logic to address post-update game state changes:
  - Detects `chat_option.png` overlay before bubble detection and presses ESC to dismiss.
  - Detects `update_confirm.png` when chat room state is unavailable and clicks it to proceed.
  - Both behaviors improve UI stability following game version changes.

- Updated `essential_templates` dictionary and constants with the two new template paths:
  - `chat_option.png`
  - `update_confirm.png`

These improvements reduce redundant bot responses and enhance UI resilience against inconsistent or obstructed states in the latest game versions.
This commit is contained in:
z060142 2025-05-15 02:16:24 +08:00
parent 51a99ee5ad
commit 890772f70e
4 changed files with 147 additions and 18 deletions

28
main.py
View File

@ -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) ---")

BIN
templates/chat_option.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -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