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:
parent
51a99ee5ad
commit
890772f70e
28
main.py
28
main.py
@ -16,6 +16,8 @@ from mcp import ClientSession, StdioServerParameters, types
|
|||||||
# --- Keyboard Imports ---
|
# --- Keyboard Imports ---
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
# Import MessageDeduplication from ui_interaction
|
||||||
|
from ui_interaction import MessageDeduplication
|
||||||
try:
|
try:
|
||||||
import keyboard # Needs pip install keyboard
|
import keyboard # Needs pip install keyboard
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@ -483,9 +485,12 @@ async def run_main_with_exit_stack():
|
|||||||
|
|
||||||
# 5. Start UI Monitoring in a separate thread
|
# 5. Start UI Monitoring in a separate thread
|
||||||
print("\n--- Starting UI monitoring 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(
|
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"
|
name="ui_monitor"
|
||||||
)
|
)
|
||||||
ui_monitor_task = monitor_task # Store task reference for shutdown
|
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
|
# 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)
|
# 6. Start the main processing loop (non-blocking check on queue)
|
||||||
print("\n--- Wolfhart chatbot has started (waiting for triggers) ---")
|
print("\n--- Wolfhart chatbot has started (waiting for triggers) ---")
|
||||||
|
|||||||
BIN
templates/chat_option.png
Normal file
BIN
templates/chat_option.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.3 KiB |
BIN
templates/update_confirm.png
Normal file
BIN
templates/update_confirm.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.0 KiB |
@ -18,6 +18,64 @@ import queue
|
|||||||
from typing import List, Tuple, Optional, Dict, Any
|
from typing import List, Tuple, Optional, Dict, Any
|
||||||
import threading # Import threading for Lock if needed, or just use a simple flag
|
import threading # Import threading for Lock if needed, or just use a simple flag
|
||||||
import math # Added for distance calculation in dual method
|
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 ---
|
# --- Global Pause Flag ---
|
||||||
# Using a simple mutable object (list) for thread-safe-like access without explicit lock
|
# 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")
|
COPY_NAME_BUTTON_IMG = os.path.join(TEMPLATE_DIR, "copy_name_button.png")
|
||||||
SEND_BUTTON_IMG = os.path.join(TEMPLATE_DIR, "send_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_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
|
# State Detection
|
||||||
PROFILE_NAME_PAGE_IMG = os.path.join(TEMPLATE_DIR, "Profile_Name_page.png")
|
PROFILE_NAME_PAGE_IMG = os.path.join(TEMPLATE_DIR, "Profile_Name_page.png")
|
||||||
PROFILE_PAGE_IMG = os.path.join(TEMPLATE_DIR, "Profile_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) ---
|
# --- 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,
|
Continuously monitors the UI, detects triggers, performs interactions,
|
||||||
puts trigger data into trigger_queue, and processes commands from command_queue.
|
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,
|
'page_sec': PAGE_SEC_IMG, 'page_str': PAGE_STR_IMG,
|
||||||
'dismiss_button': DISMISS_BUTTON_IMG, 'confirm_button': CONFIRM_BUTTON_IMG,
|
'dismiss_button': DISMISS_BUTTON_IMG, 'confirm_button': CONFIRM_BUTTON_IMG,
|
||||||
'close_button': CLOSE_BUTTON_IMG, 'back_arrow': BACK_ARROW_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 = {
|
legacy_templates = {
|
||||||
# Deprecated Keywords (for legacy method fallback)
|
# 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
|
elif action == 'clear_history': # Added for F7
|
||||||
print("UI Thread: Processing clear_history command.")
|
print("UI Thread: Processing clear_history command.")
|
||||||
recent_texts.clear()
|
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
|
elif action == 'reset_state': # Added for F8 resume
|
||||||
print("UI Thread: Processing reset_state command.")
|
print("UI Thread: Processing reset_state command.")
|
||||||
recent_texts.clear()
|
recent_texts.clear()
|
||||||
last_processed_bubble_info = None
|
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:
|
else:
|
||||||
print(f"UI Thread: Received unknown command: {action}")
|
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 ---
|
# --- If not paused, proceed with UI Monitoring ---
|
||||||
# print("[DEBUG] UI Loop: Monitoring is active. Proceeding...") # DEBUG REMOVED
|
# 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 ---
|
# --- Check for Main Screen Navigation ---
|
||||||
# print("[DEBUG] UI Loop: Checking for main screen navigation...") # DEBUG REMOVED
|
# print("[DEBUG] UI Loop: Checking for main screen navigation...") # DEBUG REMOVED
|
||||||
try:
|
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
|
# Use a slightly lower confidence maybe, or state_confidence
|
||||||
chat_room_locs = detector._find_template('chat_room', confidence=detector.state_confidence)
|
chat_room_locs = detector._find_template('chat_room', confidence=detector.state_confidence)
|
||||||
if not chat_room_locs:
|
if not chat_room_locs:
|
||||||
print("UI Thread: Not in chat room state before bubble detection. Attempting cleanup...")
|
print("UI Thread: Not in chat room state before bubble detection. Checking for update confirm...")
|
||||||
# Call the existing cleanup function to try and return
|
|
||||||
|
# 檢查是否存在更新確認按鈕
|
||||||
|
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)
|
perform_state_cleanup(detector, interactor)
|
||||||
# Regardless of cleanup success, restart the loop to re-evaluate state from the top
|
# Regardless of cleanup success, restart the loop to re-evaluate state from the top
|
||||||
print("UI Thread: Continuing loop after attempting chat room cleanup.")
|
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
|
perform_state_cleanup(detector, interactor) # Attempt cleanup
|
||||||
continue # Skip to next bubble
|
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)
|
# 5. Interact: Get Sender Name (uses re-location internally via retrieve_sender_name_interaction)
|
||||||
# print("[DEBUG] UI Loop: Retrieving sender name...") # DEBUG REMOVED
|
# print("[DEBUG] UI Loop: Retrieving sender name...") # DEBUG REMOVED
|
||||||
sender_name = None
|
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.")
|
print("Error: Could not get sender name for this bubble, skipping.")
|
||||||
continue # Skip to next bubble
|
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 ---
|
# --- Attempt to activate reply context ---
|
||||||
# print("[DEBUG] UI Loop: Attempting to activate reply context...") # DEBUG REMOVED
|
# print("[DEBUG] UI Loop: Attempting to activate reply context...") # DEBUG REMOVED
|
||||||
reply_context_activated = False
|
reply_context_activated = False
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user