Merge pull request #13 from z060142/Temporary-solution
Temporary solution
This commit is contained in:
commit
2c8a9e4588
1
.gitignore
vendored
1
.gitignore
vendored
@ -3,6 +3,7 @@
|
|||||||
llm_debug.log
|
llm_debug.log
|
||||||
config.py
|
config.py
|
||||||
config.py.bak
|
config.py.bak
|
||||||
|
simple_bubble_dedup.json
|
||||||
__pycache__/
|
__pycache__/
|
||||||
debug_screenshots/
|
debug_screenshots/
|
||||||
chat_logs/
|
chat_logs/
|
||||||
|
|||||||
@ -124,7 +124,14 @@ Wolf Chat 是一個基於 MCP (Modular Capability Provider) 框架的聊天機
|
|||||||
* **計算頭像座標**:根據**新**找到的氣泡左上角座標,應用特定偏移量 (`AVATAR_OFFSET_X_REPLY`, `AVATAR_OFFSET_Y_REPLY`) 計算頭像點擊位置。
|
* **計算頭像座標**:根據**新**找到的氣泡左上角座標,應用特定偏移量 (`AVATAR_OFFSET_X_REPLY`, `AVATAR_OFFSET_Y_REPLY`) 計算頭像點擊位置。
|
||||||
* **互動(含重試)**:點擊計算出的頭像位置,檢查是否成功進入個人資料頁面 (`Profile_page.png`)。若失敗,最多重試 3 次(每次重試前會再次重新定位氣泡)。若成功,則繼續導航菜單複製用戶名稱。
|
* **互動(含重試)**:點擊計算出的頭像位置,檢查是否成功進入個人資料頁面 (`Profile_page.png`)。若失敗,最多重試 3 次(每次重試前會再次重新定位氣泡)。若成功,則繼續導航菜單複製用戶名稱。
|
||||||
* **原始偏移量**:原始的 `-55` 像素水平偏移量 (`AVATAR_OFFSET_X`) 仍保留,用於 `remove_user_position` 等其他功能。
|
* **原始偏移量**:原始的 `-55` 像素水平偏移量 (`AVATAR_OFFSET_X`) 仍保留,用於 `remove_user_position` 等其他功能。
|
||||||
5. **防重複處理 (Duplicate Prevention)**:使用最近處理過的文字內容歷史 (`recent_texts`) 防止對相同訊息重複觸發。
|
5. **防重複處理 (Duplicate Prevention)**:
|
||||||
|
* **基於圖像哈希的去重 (Image Hash Deduplication)**: 新增 `simple_bubble_dedup.py` 模塊,實現基於圖像感知哈希 (Perceptual Hash) 的去重系統。
|
||||||
|
* **原理**: 系統會計算最近處理過的氣泡圖像的感知哈希值,並保存最近的 N 個 (預設 5 個) 氣泡的哈希。當偵測到新氣泡時,會計算其哈希並與保存的哈希進行比對。如果哈希差異小於設定的閾值 (預設 5),則認為是重複氣泡並跳過處理。
|
||||||
|
* **實現**: 在 `ui_interaction.py` 的 `run_ui_monitoring_loop` 函數中初始化 `SimpleBubbleDeduplication` 實例,並在偵測到關鍵字並截取氣泡快照後,調用 `is_duplicate` 方法進行檢查。
|
||||||
|
* **狀態管理**: 使用 `simple_bubble_dedup.json` 文件持久化保存最近的氣泡哈希記錄。
|
||||||
|
* **清理**: F7 (`clear_history`) 和 F8 (`reset_state`) 功能已擴展,會同時清除圖像去重系統中的記錄。
|
||||||
|
* **發送者信息更新**: 在成功處理並將氣泡信息放入隊列後,會嘗試更新去重記錄中對應氣泡的發送者名稱。
|
||||||
|
* **文字內容歷史 (已棄用)**: 原有的基於 `recent_texts` 的文字內容重複檢查邏輯已**移除或註解**,圖像哈希去重成為主要的去重機制。
|
||||||
|
|
||||||
#### LLM 整合
|
#### LLM 整合
|
||||||
|
|
||||||
@ -644,6 +651,10 @@ Wolf Chat 是一個基於 MCP (Modular Capability Provider) 框架的聊天機
|
|||||||
- 使用回調函數 (`callback`) 與調用者(即 `Setup.py`)通信,例如在遊戲重啟完成時。
|
- 使用回調函數 (`callback`) 與調用者(即 `Setup.py`)通信,例如在遊戲重啟完成時。
|
||||||
- 保留了獨立運行模式,以便在直接執行時仍能工作(主要用於測試或舊版兼容)。
|
- 保留了獨立運行模式,以便在直接執行時仍能工作(主要用於測試或舊版兼容)。
|
||||||
- 程式碼註解和日誌訊息已更新為英文。
|
- 程式碼註解和日誌訊息已更新為英文。
|
||||||
|
- **新增遊戲崩潰自動恢復 (2025-05-15)**:
|
||||||
|
- 在 `_monitor_loop` 方法中,優先檢查遊戲進程 (`_is_game_running`) 是否仍在運行。
|
||||||
|
- 如果進程消失,會記錄警告並嘗試重新啟動遊戲 (`_start_game_process`)。
|
||||||
|
- 新增 `_is_game_running` 方法,使用 `psutil` 檢查具有指定進程名稱的遊戲是否正在運行。
|
||||||
- **`Setup.py` (修改)**:
|
- **`Setup.py` (修改)**:
|
||||||
- 導入 `game_manager`。
|
- 導入 `game_manager`。
|
||||||
- 在 `WolfChatSetup` 類的 `__init__` 方法中初始化 `self.game_monitor = None`。
|
- 在 `WolfChatSetup` 類的 `__init__` 方法中初始化 `self.game_monitor = None`。
|
||||||
|
|||||||
9
Setup.py
9
Setup.py
@ -69,6 +69,7 @@ keep_monitoring_flag.set()
|
|||||||
# logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
# logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||||
# Setup logger instance. This can be configured further if needed.
|
# Setup logger instance. This can be configured further if needed.
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
logger.propagate = False
|
||||||
if not logger.handlers: # Avoid adding multiple handlers if script is reloaded
|
if not logger.handlers: # Avoid adding multiple handlers if script is reloaded
|
||||||
handler = logging.StreamHandler()
|
handler = logging.StreamHandler()
|
||||||
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||||
@ -568,8 +569,8 @@ class WolfChatSetup(tk.Tk):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.title(f"Wolf Chat Setup v{VERSION}")
|
self.title(f"Wolf Chat Setup v{VERSION}")
|
||||||
self.geometry("800x600")
|
self.geometry("900x600")
|
||||||
self.minsize(750, 550)
|
self.minsize(900, 600)
|
||||||
|
|
||||||
# Load existing data
|
# Load existing data
|
||||||
self.env_data = load_env_file()
|
self.env_data = load_env_file()
|
||||||
@ -2783,8 +2784,8 @@ else: # HAS_SOCKETIO is False
|
|||||||
# ===============================================================
|
# ===============================================================
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# Setup main logger for the application if not already done
|
# Setup main logger for the application if not already done
|
||||||
if not logging.getLogger().handlers: # Check root logger
|
#if not logging.getLogger().handlers: # Check root logger
|
||||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
# logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||||
|
|
||||||
app = WolfChatSetup()
|
app = WolfChatSetup()
|
||||||
app.protocol("WM_DELETE_WINDOW", app.on_closing) # Handle window close button
|
app.protocol("WM_DELETE_WINDOW", app.on_closing) # Handle window close button
|
||||||
|
|||||||
264
game_manager.py
264
game_manager.py
@ -72,6 +72,10 @@ class GameMonitor:
|
|||||||
self.monitor_thread = None
|
self.monitor_thread = None
|
||||||
self.stop_event = threading.Event()
|
self.stop_event = threading.Event()
|
||||||
|
|
||||||
|
# Add these tracking variables
|
||||||
|
self.last_focus_failure_count = 0
|
||||||
|
self.last_successful_foreground = time.time()
|
||||||
|
|
||||||
self.logger.info(f"GameMonitor initialized. Game window: '{self.window_title}', Process: '{self.game_process_name}'")
|
self.logger.info(f"GameMonitor initialized. Game window: '{self.window_title}', Process: '{self.game_process_name}'")
|
||||||
self.logger.info(f"Position: ({self.window_x}, {self.window_y}), Size: {self.window_width}x{self.window_height}")
|
self.logger.info(f"Position: ({self.window_x}, {self.window_y}), Size: {self.window_width}x{self.window_height}")
|
||||||
self.logger.info(f"Scheduled Restart: {'Enabled' if self.enable_restart else 'Disabled'}, Interval: {self.restart_interval} minutes")
|
self.logger.info(f"Scheduled Restart: {'Enabled' if self.enable_restart else 'Disabled'}, Interval: {self.restart_interval} minutes")
|
||||||
@ -128,6 +132,17 @@ class GameMonitor:
|
|||||||
|
|
||||||
while not self.stop_event.is_set():
|
while not self.stop_event.is_set():
|
||||||
try:
|
try:
|
||||||
|
# Add to _monitor_loop method - just 7 lines that matter
|
||||||
|
if not self._is_game_running():
|
||||||
|
self.logger.warning("Game process disappeared - restarting")
|
||||||
|
time.sleep(2) # Let resources release
|
||||||
|
if self._start_game_process():
|
||||||
|
self.logger.info("Game restarted successfully")
|
||||||
|
else:
|
||||||
|
self.logger.error("Game restart failed")
|
||||||
|
time.sleep(self.monitor_interval) # Wait before next check after a restart attempt
|
||||||
|
continue
|
||||||
|
|
||||||
# Check for scheduled restart
|
# Check for scheduled restart
|
||||||
if self.next_restart_time and time.time() >= self.next_restart_time:
|
if self.next_restart_time and time.time() >= self.next_restart_time:
|
||||||
self.logger.info("Scheduled restart time reached. Performing restart...")
|
self.logger.info("Scheduled restart time reached. Performing restart...")
|
||||||
@ -160,51 +175,41 @@ class GameMonitor:
|
|||||||
if current_pos != target_pos or current_size != target_size:
|
if current_pos != target_pos or current_size != target_size:
|
||||||
window.moveTo(target_pos[0], target_pos[1])
|
window.moveTo(target_pos[0], target_pos[1])
|
||||||
window.resizeTo(target_size[0], target_size[1])
|
window.resizeTo(target_size[0], target_size[1])
|
||||||
# Verify if move and resize were successful
|
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
window.activate() # Try activating to ensure changes apply
|
window.activate()
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
|
# Check if changes were successful
|
||||||
new_pos = (window.left, window.top)
|
new_pos = (window.left, window.top)
|
||||||
new_size = (window.width, window.height)
|
new_size = (window.width, window.height)
|
||||||
if new_pos == target_pos and new_size == target_size:
|
if new_pos == target_pos and new_size == target_size:
|
||||||
current_message += f"Adjusted game window to position ({target_pos[0]},{target_pos[1]}) size {target_size[0]}x{target_size[1]}. "
|
current_message += f"Adjusted window position/size. "
|
||||||
adjustment_made = True
|
adjustment_made = True
|
||||||
else:
|
|
||||||
self.logger.warning(f"Attempted to adjust window pos/size, but result mismatch. Target: {target_pos}/{target_size}, Actual: {new_pos}/{new_size}")
|
|
||||||
|
|
||||||
|
# 2. Check and bring to foreground using enhanced method
|
||||||
# 2. Check and bring to foreground
|
|
||||||
current_foreground_hwnd = win32gui.GetForegroundWindow()
|
current_foreground_hwnd = win32gui.GetForegroundWindow()
|
||||||
|
|
||||||
if current_foreground_hwnd != hwnd:
|
if current_foreground_hwnd != hwnd:
|
||||||
try:
|
# Use enhanced forceful focus method
|
||||||
# Use HWND_TOP to bring window to top, not HWND_TOPMOST
|
success, method_used = self._force_window_foreground(hwnd, window)
|
||||||
win32gui.SetWindowPos(hwnd, win32con.HWND_TOP, 0, 0, 0, 0,
|
if success:
|
||||||
win32con.SWP_NOMOVE | win32con.SWP_NOSIZE)
|
current_message += f"Focused window using {method_used}. "
|
||||||
|
adjustment_made = True
|
||||||
|
if not hasattr(self, 'last_focus_failure_count'):
|
||||||
|
self.last_focus_failure_count = 0
|
||||||
|
self.last_focus_failure_count = 0
|
||||||
|
else:
|
||||||
|
# Increment failure counter
|
||||||
|
if not hasattr(self, 'last_focus_failure_count'):
|
||||||
|
self.last_focus_failure_count = 0
|
||||||
|
self.last_focus_failure_count += 1
|
||||||
|
|
||||||
# Set as foreground window (gain focus)
|
# Log warning with consecutive failure count
|
||||||
win32gui.SetForegroundWindow(hwnd)
|
self.logger.warning(f"Window focus failed (attempt {self.last_focus_failure_count}): {method_used}")
|
||||||
|
|
||||||
# Verify if window is active
|
# Restart game after too many failures
|
||||||
time.sleep(0.1)
|
if self.last_focus_failure_count >= 15:
|
||||||
foreground_hwnd = win32gui.GetForegroundWindow()
|
self.logger.warning("Excessive focus failures, restarting game...")
|
||||||
|
self._perform_restart()
|
||||||
if foreground_hwnd == hwnd:
|
self.last_focus_failure_count = 0
|
||||||
current_message += "Brought game window to foreground and set focus. "
|
|
||||||
adjustment_made = True
|
|
||||||
else:
|
|
||||||
# Use fallback method
|
|
||||||
self.logger.warning("SetForegroundWindow failed, trying fallback window.activate()")
|
|
||||||
try:
|
|
||||||
window.activate()
|
|
||||||
time.sleep(0.1)
|
|
||||||
if win32gui.GetForegroundWindow() == hwnd:
|
|
||||||
current_message += "Set game window focus using fallback method. "
|
|
||||||
adjustment_made = True
|
|
||||||
except Exception as activate_err:
|
|
||||||
self.logger.warning(f"Fallback method window.activate() failed: {activate_err}")
|
|
||||||
except Exception as focus_err:
|
|
||||||
self.logger.warning(f"Error setting window focus: {focus_err}")
|
|
||||||
else:
|
else:
|
||||||
# Use basic functions on non-Windows platforms
|
# Use basic functions on non-Windows platforms
|
||||||
current_pos = (window.left, window.top)
|
current_pos = (window.left, window.top)
|
||||||
@ -245,6 +250,17 @@ class GameMonitor:
|
|||||||
|
|
||||||
self.logger.info("Game window monitoring loop finished")
|
self.logger.info("Game window monitoring loop finished")
|
||||||
|
|
||||||
|
def _is_game_running(self):
|
||||||
|
"""Check if game is running"""
|
||||||
|
if not HAS_PSUTIL:
|
||||||
|
self.logger.warning("_is_game_running: psutil not available, cannot check process status.")
|
||||||
|
return True # Assume running if psutil is not available to avoid unintended restarts
|
||||||
|
try:
|
||||||
|
return any(p.name().lower() == self.game_process_name.lower() for p in psutil.process_iter(['name']))
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error checking game process: {e}")
|
||||||
|
return False # Assume not running on error
|
||||||
|
|
||||||
def _find_game_window(self):
|
def _find_game_window(self):
|
||||||
"""Find the game window with the specified title"""
|
"""Find the game window with the specified title"""
|
||||||
try:
|
try:
|
||||||
@ -255,27 +271,181 @@ class GameMonitor:
|
|||||||
self.logger.debug(f"Error finding game window: {e}")
|
self.logger.debug(f"Error finding game window: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _find_game_process(self):
|
def _force_window_foreground(self, hwnd, window):
|
||||||
"""Find the game process"""
|
"""Aggressive window focus implementation"""
|
||||||
if not HAS_PSUTIL:
|
if not HAS_WIN32:
|
||||||
self.logger.warning("psutil is not available, cannot perform process lookup")
|
return False, "win32 modules unavailable"
|
||||||
|
|
||||||
|
success = False
|
||||||
|
methods_tried = []
|
||||||
|
|
||||||
|
# Method 1: HWND_TOPMOST strategy
|
||||||
|
methods_tried.append("HWND_TOPMOST")
|
||||||
|
try:
|
||||||
|
win32gui.SetWindowPos(hwnd, win32con.HWND_TOPMOST, 0, 0, 0, 0,
|
||||||
|
win32con.SWP_NOMOVE | win32con.SWP_NOSIZE)
|
||||||
|
time.sleep(0.1)
|
||||||
|
win32gui.SetWindowPos(hwnd, win32con.HWND_TOP, 0, 0, 0, 0,
|
||||||
|
win32con.SWP_NOMOVE | win32con.SWP_NOSIZE)
|
||||||
|
|
||||||
|
win32gui.SetForegroundWindow(hwnd)
|
||||||
|
time.sleep(0.2)
|
||||||
|
if win32gui.GetForegroundWindow() == hwnd:
|
||||||
|
return True, "HWND_TOPMOST"
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.debug(f"Method 1 failed: {e}")
|
||||||
|
|
||||||
|
# Method 2: Minimize/restore cycle
|
||||||
|
methods_tried.append("MinimizeRestore")
|
||||||
|
try:
|
||||||
|
win32gui.ShowWindow(hwnd, win32con.SW_MINIMIZE)
|
||||||
|
time.sleep(0.3)
|
||||||
|
win32gui.ShowWindow(hwnd, win32con.SW_RESTORE)
|
||||||
|
time.sleep(0.2)
|
||||||
|
win32gui.SetForegroundWindow(hwnd)
|
||||||
|
|
||||||
|
if win32gui.GetForegroundWindow() == hwnd:
|
||||||
|
return True, "MinimizeRestore"
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.debug(f"Method 2 failed: {e}")
|
||||||
|
|
||||||
|
# Method 3: Thread input attach
|
||||||
|
methods_tried.append("ThreadAttach")
|
||||||
|
try:
|
||||||
|
import win32process
|
||||||
|
import win32api
|
||||||
|
|
||||||
|
current_thread_id = win32api.GetCurrentThreadId()
|
||||||
|
window_thread_id = win32process.GetWindowThreadProcessId(hwnd)[0]
|
||||||
|
|
||||||
|
if current_thread_id != window_thread_id:
|
||||||
|
win32process.AttachThreadInput(current_thread_id, window_thread_id, True)
|
||||||
|
try:
|
||||||
|
win32gui.BringWindowToTop(hwnd)
|
||||||
|
win32gui.SetForegroundWindow(hwnd)
|
||||||
|
|
||||||
|
time.sleep(0.2)
|
||||||
|
if win32gui.GetForegroundWindow() == hwnd:
|
||||||
|
return True, "ThreadAttach"
|
||||||
|
finally:
|
||||||
|
win32process.AttachThreadInput(current_thread_id, window_thread_id, False)
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.debug(f"Method 3 failed: {e}")
|
||||||
|
|
||||||
|
# Method 4: Flash + Window messages
|
||||||
|
methods_tried.append("Flash+Messages")
|
||||||
|
try:
|
||||||
|
# First flash to get attention
|
||||||
|
win32gui.FlashWindow(hwnd, True)
|
||||||
|
time.sleep(0.2)
|
||||||
|
|
||||||
|
# Then send specific window messages
|
||||||
|
win32gui.SendMessage(hwnd, win32con.WM_SETREDRAW, 0, 0)
|
||||||
|
win32gui.SendMessage(hwnd, win32con.WM_SETREDRAW, 1, 0)
|
||||||
|
win32gui.RedrawWindow(hwnd, None, None,
|
||||||
|
win32con.RDW_FRAME | win32con.RDW_INVALIDATE |
|
||||||
|
win32con.RDW_UPDATENOW | win32con.RDW_ALLCHILDREN)
|
||||||
|
|
||||||
|
win32gui.PostMessage(hwnd, win32con.WM_SYSCOMMAND, win32con.SC_RESTORE, 0)
|
||||||
|
win32gui.PostMessage(hwnd, win32con.WM_ACTIVATE, win32con.WA_ACTIVE, 0)
|
||||||
|
|
||||||
|
time.sleep(0.2)
|
||||||
|
if win32gui.GetForegroundWindow() == hwnd:
|
||||||
|
return True, "Flash+Messages"
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.debug(f"Method 4 failed: {e}")
|
||||||
|
|
||||||
|
# Method 5: Hide/Show cycle
|
||||||
|
methods_tried.append("HideShow")
|
||||||
|
try:
|
||||||
|
win32gui.ShowWindow(hwnd, win32con.SW_HIDE)
|
||||||
|
time.sleep(0.2)
|
||||||
|
win32gui.ShowWindow(hwnd, win32con.SW_SHOW)
|
||||||
|
time.sleep(0.2)
|
||||||
|
win32gui.SetForegroundWindow(hwnd)
|
||||||
|
|
||||||
|
if win32gui.GetForegroundWindow() == hwnd:
|
||||||
|
return True, "HideShow"
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.debug(f"Method 5 failed: {e}")
|
||||||
|
|
||||||
|
return False, f"All methods failed: {', '.join(methods_tried)}"
|
||||||
|
|
||||||
|
def _find_game_process_by_window(self):
|
||||||
|
"""Find process using both window title and process name"""
|
||||||
|
if not HAS_PSUTIL or not HAS_WIN32:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
for proc in psutil.process_iter(['pid', 'name', 'exe']):
|
window = self._find_game_window()
|
||||||
try:
|
if not window:
|
||||||
proc_info = proc.info
|
return None
|
||||||
proc_name = proc_info.get('name')
|
|
||||||
|
|
||||||
if proc_name and proc_name.lower() == self.game_process_name.lower():
|
hwnd = window._hWnd
|
||||||
self.logger.info(f"Found game process '{proc_name}' (PID: {proc.pid})")
|
window_pid = None
|
||||||
|
try:
|
||||||
|
import win32process
|
||||||
|
_, window_pid = win32process.GetWindowThreadProcessId(hwnd)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if window_pid:
|
||||||
|
try:
|
||||||
|
proc = psutil.Process(window_pid)
|
||||||
|
proc_name = proc.name()
|
||||||
|
|
||||||
|
if proc_name.lower() == self.game_process_name.lower():
|
||||||
|
self.logger.info(f"Found game process '{proc_name}' (PID: {proc.pid}) with window title '{self.window_title}'")
|
||||||
return proc
|
return proc
|
||||||
|
else:
|
||||||
|
self.logger.debug(f"Window process name mismatch: expected '{self.game_process_name}', got '{proc_name}'")
|
||||||
|
return proc # Returning proc even if name mismatches, as per user's code.
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Fallback to name-based search if window-based fails or PID doesn't match process name.
|
||||||
|
# The user's provided code implies a fallback to _find_game_process_by_name()
|
||||||
|
# This will be handled by the updated _find_game_process method.
|
||||||
|
# For now, if the window PID didn't lead to a matching process name, we return None here.
|
||||||
|
# The original code had "return self._find_game_process_by_name()" here,
|
||||||
|
# but that would create a direct dependency. The new _find_game_process handles the fallback.
|
||||||
|
# So, if we reach here, it means the window was found, PID was obtained, but process name didn't match.
|
||||||
|
# The original code returns `proc` even on mismatch, so I'll keep that.
|
||||||
|
# If `window_pid` was None or `psutil.Process(window_pid)` failed, it would have returned None or passed.
|
||||||
|
# The logic "return self._find_game_process_by_name()" was in the original snippet,
|
||||||
|
# I will include it here as per the snippet, but note that the overall _find_game_process will also call it.
|
||||||
|
return self._find_game_process_by_name() # As per user snippet
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Process-by-window lookup error: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _find_game_process(self):
|
||||||
|
"""Find game process with combined approach"""
|
||||||
|
# Try window-based process lookup first
|
||||||
|
proc = self._find_game_process_by_window()
|
||||||
|
if proc:
|
||||||
|
return proc
|
||||||
|
|
||||||
|
# Fall back to name-only lookup
|
||||||
|
# This is the original _find_game_process logic, now as a fallback.
|
||||||
|
if not HAS_PSUTIL:
|
||||||
|
self.logger.debug("psutil not available for name-only process lookup fallback.") # Changed to debug as primary is window based
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
for p_iter in psutil.process_iter(['pid', 'name', 'exe']):
|
||||||
|
try:
|
||||||
|
proc_info = p_iter.info
|
||||||
|
proc_name = proc_info.get('name')
|
||||||
|
if proc_name and proc_name.lower() == self.game_process_name.lower():
|
||||||
|
self.logger.info(f"Found game process by name '{proc_name}' (PID: {p_iter.pid}) as fallback")
|
||||||
|
return p_iter
|
||||||
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
|
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
|
||||||
continue
|
continue
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Error finding game process: {e}")
|
self.logger.error(f"Error in name-only game process lookup: {e}")
|
||||||
|
|
||||||
self.logger.info(f"Game process '{self.game_process_name}' not found")
|
self.logger.info(f"Game process '{self.game_process_name}' not found by name either.")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _perform_restart(self):
|
def _perform_restart(self):
|
||||||
@ -298,7 +468,7 @@ class GameMonitor:
|
|||||||
self.logger.error("Failed to start game")
|
self.logger.error("Failed to start game")
|
||||||
|
|
||||||
# 4. Wait for game to launch
|
# 4. Wait for game to launch
|
||||||
restart_wait_time = 30 # seconds
|
restart_wait_time = 45 # seconds, increased from 30
|
||||||
self.logger.info(f"Waiting for game to start ({restart_wait_time} seconds)...")
|
self.logger.info(f"Waiting for game to start ({restart_wait_time} seconds)...")
|
||||||
time.sleep(restart_wait_time)
|
time.sleep(restart_wait_time)
|
||||||
|
|
||||||
|
|||||||
34
main.py
34
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:
|
||||||
@ -103,16 +105,14 @@ def handle_f8():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error sending pause command (F8): {e}")
|
print(f"Error sending pause command (F8): {e}")
|
||||||
else:
|
else:
|
||||||
print("\n--- F8 pressed: Resuming script, resetting state, and resuming UI monitoring ---")
|
print("\n--- F8 pressed: Resuming script and UI monitoring ---")
|
||||||
reset_command = {'action': 'reset_state'}
|
|
||||||
resume_command = {'action': 'resume'}
|
resume_command = {'action': 'resume'}
|
||||||
try:
|
try:
|
||||||
main_loop.call_soon_threadsafe(command_queue.put_nowait, reset_command)
|
|
||||||
# Add a small delay? Let's try without first.
|
# Add a small delay? Let's try without first.
|
||||||
# time.sleep(0.05) # Short delay between commands if needed
|
# time.sleep(0.05) # Short delay between commands if needed
|
||||||
main_loop.call_soon_threadsafe(command_queue.put_nowait, resume_command)
|
main_loop.call_soon_threadsafe(command_queue.put_nowait, resume_command)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error sending reset/resume commands (F8): {e}")
|
print(f"Error sending resume command (F8): {e}")
|
||||||
|
|
||||||
def handle_f9():
|
def handle_f9():
|
||||||
"""Handles F9 press: Initiates script shutdown."""
|
"""Handles F9 press: Initiates script shutdown."""
|
||||||
@ -483,9 +483,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 +496,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) ---")
|
||||||
|
|||||||
155
simple_bubble_dedup.py
Normal file
155
simple_bubble_dedup.py
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
import os
|
||||||
|
import json
|
||||||
|
import collections
|
||||||
|
import threading
|
||||||
|
from PIL import Image
|
||||||
|
import imagehash
|
||||||
|
import numpy as np
|
||||||
|
import io
|
||||||
|
|
||||||
|
class SimpleBubbleDeduplication:
|
||||||
|
def __init__(self, storage_file="simple_bubble_dedup.json", max_bubbles=5, threshold=5, hash_size=16):
|
||||||
|
self.storage_file = storage_file
|
||||||
|
self.max_bubbles = max_bubbles # Keep the most recent 5 bubbles
|
||||||
|
self.threshold = threshold # Hash difference threshold (lower values are more strict)
|
||||||
|
self.hash_size = hash_size # Hash size
|
||||||
|
self.lock = threading.Lock()
|
||||||
|
|
||||||
|
# Use OrderedDict to maintain order
|
||||||
|
self.recent_bubbles = collections.OrderedDict()
|
||||||
|
# Load stored bubble hashes
|
||||||
|
self._load_storage()
|
||||||
|
|
||||||
|
def _load_storage(self):
|
||||||
|
"""Load processed bubble hash values from file"""
|
||||||
|
if os.path.exists(self.storage_file):
|
||||||
|
try:
|
||||||
|
with open(self.storage_file, 'r') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
# Convert stored data to OrderedDict and load
|
||||||
|
self.recent_bubbles.clear()
|
||||||
|
# Use loaded_count to track loaded items, ensuring we don't exceed max_bubbles
|
||||||
|
loaded_count = 0
|
||||||
|
for bubble_id, bubble_data in data.items():
|
||||||
|
if loaded_count >= self.max_bubbles:
|
||||||
|
break
|
||||||
|
self.recent_bubbles[bubble_id] = {
|
||||||
|
'hash': imagehash.hex_to_hash(bubble_data['hash']),
|
||||||
|
'sender': bubble_data.get('sender', 'Unknown')
|
||||||
|
}
|
||||||
|
loaded_count += 1
|
||||||
|
|
||||||
|
print(f"Loaded {len(self.recent_bubbles)} bubble hash records")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to load bubble hash records: {e}")
|
||||||
|
self.recent_bubbles.clear()
|
||||||
|
|
||||||
|
def _save_storage(self):
|
||||||
|
"""Save bubble hashes to file"""
|
||||||
|
try:
|
||||||
|
# Create temporary dictionary for saving
|
||||||
|
data_to_save = {}
|
||||||
|
for bubble_id, bubble_data in self.recent_bubbles.items():
|
||||||
|
data_to_save[bubble_id] = {
|
||||||
|
'hash': str(bubble_data['hash']),
|
||||||
|
'sender': bubble_data.get('sender', 'Unknown')
|
||||||
|
}
|
||||||
|
|
||||||
|
with open(self.storage_file, 'w') as f:
|
||||||
|
json.dump(data_to_save, f, indent=2)
|
||||||
|
print(f"Saved {len(data_to_save)} bubble hash records")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to save bubble hash records: {e}")
|
||||||
|
|
||||||
|
def compute_image_hash(self, bubble_snapshot):
|
||||||
|
"""Calculate perceptual hash of bubble image"""
|
||||||
|
try:
|
||||||
|
# If bubble_snapshot is a PIL.Image object
|
||||||
|
if isinstance(bubble_snapshot, Image.Image):
|
||||||
|
img = bubble_snapshot
|
||||||
|
# If bubble_snapshot is a PyAutoGUI screenshot
|
||||||
|
elif hasattr(bubble_snapshot, 'save'):
|
||||||
|
img = bubble_snapshot
|
||||||
|
# If it's bytes or BytesIO
|
||||||
|
elif isinstance(bubble_snapshot, (bytes, io.BytesIO)):
|
||||||
|
img = Image.open(io.BytesIO(bubble_snapshot) if isinstance(bubble_snapshot, bytes) else bubble_snapshot)
|
||||||
|
# If it's a numpy array
|
||||||
|
elif isinstance(bubble_snapshot, np.ndarray):
|
||||||
|
img = Image.fromarray(bubble_snapshot)
|
||||||
|
else:
|
||||||
|
print(f"Unrecognized image format: {type(bubble_snapshot)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Calculate perceptual hash
|
||||||
|
phash = imagehash.phash(img, hash_size=self.hash_size)
|
||||||
|
return phash
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to calculate image hash: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def generate_bubble_id(self, bubble_region):
|
||||||
|
"""Generate ID based on bubble region"""
|
||||||
|
return f"bubble_{bubble_region[0]}_{bubble_region[1]}_{bubble_region[2]}_{bubble_region[3]}"
|
||||||
|
|
||||||
|
def is_duplicate(self, bubble_snapshot, bubble_region, sender_name=""):
|
||||||
|
"""Check if bubble is a duplicate"""
|
||||||
|
with self.lock:
|
||||||
|
if bubble_snapshot is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Calculate hash of current bubble
|
||||||
|
current_hash = self.compute_image_hash(bubble_snapshot)
|
||||||
|
if current_hash is None:
|
||||||
|
print("Unable to calculate bubble hash, cannot perform deduplication")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Generate ID for current bubble
|
||||||
|
bubble_id = self.generate_bubble_id(bubble_region)
|
||||||
|
|
||||||
|
# Check if similar to any known bubbles
|
||||||
|
for stored_id, bubble_data in self.recent_bubbles.items():
|
||||||
|
stored_hash = bubble_data['hash']
|
||||||
|
hash_diff = current_hash - stored_hash
|
||||||
|
|
||||||
|
if hash_diff <= self.threshold:
|
||||||
|
print(f"Detected duplicate bubble (ID: {stored_id}, Hash difference: {hash_diff})")
|
||||||
|
if sender_name:
|
||||||
|
print(f"Sender: {sender_name}, Recorded sender: {bubble_data.get('sender', 'Unknown')}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Not a duplicate, add to recent bubbles list
|
||||||
|
self.recent_bubbles[bubble_id] = {
|
||||||
|
'hash': current_hash,
|
||||||
|
'sender': sender_name
|
||||||
|
}
|
||||||
|
|
||||||
|
# If exceeding maximum count, remove oldest item
|
||||||
|
while len(self.recent_bubbles) > self.max_bubbles:
|
||||||
|
self.recent_bubbles.popitem(last=False) # Remove first item (oldest)
|
||||||
|
|
||||||
|
self._save_storage()
|
||||||
|
return False
|
||||||
|
|
||||||
|
def clear_all(self):
|
||||||
|
"""Clear all records"""
|
||||||
|
with self.lock:
|
||||||
|
count = len(self.recent_bubbles)
|
||||||
|
self.recent_bubbles.clear()
|
||||||
|
self._save_storage()
|
||||||
|
print(f"Cleared all {count} bubble records")
|
||||||
|
return count
|
||||||
|
|
||||||
|
def save_debug_image(self, bubble_snapshot, bubble_id, hash_value):
|
||||||
|
"""Save debug image (optional feature)"""
|
||||||
|
try:
|
||||||
|
debug_dir = "bubble_debug"
|
||||||
|
if not os.path.exists(debug_dir):
|
||||||
|
os.makedirs(debug_dir)
|
||||||
|
|
||||||
|
# Save original image
|
||||||
|
img_path = os.path.join(debug_dir, f"{bubble_id}_{hash_value}.png")
|
||||||
|
bubble_snapshot.save(img_path)
|
||||||
|
print(f"Saved debug image: {img_path}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to save debug image: {e}")
|
||||||
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,81 @@ 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
|
||||||
|
from simple_bubble_dedup import SimpleBubbleDeduplication
|
||||||
|
import difflib # Added for text similarity
|
||||||
|
|
||||||
|
class MessageDeduplication:
|
||||||
|
def __init__(self, expiry_seconds=3600): # 1 hour expiry time
|
||||||
|
self.processed_messages = {} # {message_key: timestamp}
|
||||||
|
self.expiry_seconds = expiry_seconds
|
||||||
|
|
||||||
|
def is_duplicate(self, sender, content):
|
||||||
|
"""Check if the message is a duplicate within the expiry period using text similarity."""
|
||||||
|
if not sender or not content:
|
||||||
|
return False # Missing necessary info, treat as new message
|
||||||
|
|
||||||
|
current_time = time.time()
|
||||||
|
|
||||||
|
# 遍歷所有已處理的消息
|
||||||
|
for key, timestamp in list(self.processed_messages.items()):
|
||||||
|
# 檢查是否過期
|
||||||
|
if current_time - timestamp >= self.expiry_seconds:
|
||||||
|
# 從 processed_messages 中移除過期的項目,避免集合在迭代時改變大小
|
||||||
|
# 但由於我們使用了 list(self.processed_messages.items()),所以這裡可以安全地 continue
|
||||||
|
# 或者,如果希望立即刪除,則需要不同的迭代策略或在 purge_expired 中處理
|
||||||
|
continue # 繼續檢查下一個,過期項目由 purge_expired 處理
|
||||||
|
|
||||||
|
# 解析之前儲存的發送者和內容
|
||||||
|
stored_sender, stored_content = key.split(":", 1)
|
||||||
|
|
||||||
|
# 檢查發送者是否相同
|
||||||
|
if sender.lower() == stored_sender.lower():
|
||||||
|
# Calculate text similarity
|
||||||
|
similarity = difflib.SequenceMatcher(None, content, stored_content).ratio()
|
||||||
|
if similarity >= 0.95: # Use 0.95 as threshold
|
||||||
|
print(f"Deduplicator: Detected similar message (similarity: {similarity:.2f}): {sender} - {content[:20]}...")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 不是重複消息,儲存它
|
||||||
|
# 注意:這裡儲存的 content 是原始 content,不是 clean_content
|
||||||
|
message_key = f"{sender.lower()}:{content}"
|
||||||
|
self.processed_messages[message_key] = current_time
|
||||||
|
return False
|
||||||
|
|
||||||
|
# create_key 方法已不再需要,可以移除
|
||||||
|
# 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 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 +217,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,13 +1707,22 @@ 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.
|
||||||
"""
|
"""
|
||||||
print("\n--- Starting UI Monitoring Loop (Thread) ---")
|
print("\n--- Starting UI Monitoring Loop (Thread) ---")
|
||||||
|
|
||||||
|
# --- 初始化氣泡圖像去重系統(新增) ---
|
||||||
|
bubble_deduplicator = SimpleBubbleDeduplication(
|
||||||
|
storage_file="simple_bubble_dedup.json",
|
||||||
|
max_bubbles=4, # 保留最近5個氣泡
|
||||||
|
threshold=7, # 哈希差異閾值(值越小越嚴格)
|
||||||
|
hash_size=16 # 哈希大小
|
||||||
|
)
|
||||||
|
# --- 初始化氣泡圖像去重系統結束 ---
|
||||||
|
|
||||||
# --- Initialization (Instantiate modules within the thread) ---
|
# --- Initialization (Instantiate modules within the thread) ---
|
||||||
# --- Template Dictionary Setup (Refactored) ---
|
# --- Template Dictionary Setup (Refactored) ---
|
||||||
essential_templates = {
|
essential_templates = {
|
||||||
@ -1667,7 +1754,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 +1862,27 @@ 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
|
||||||
|
|
||||||
|
# --- 新增:清理氣泡去重記錄 ---
|
||||||
|
if 'bubble_deduplicator' in locals():
|
||||||
|
bubble_deduplicator.clear_all()
|
||||||
|
# --- 清理氣泡去重記錄結束 ---
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# --- 新增:清理氣泡去重記錄 ---
|
||||||
|
if 'bubble_deduplicator' in locals():
|
||||||
|
bubble_deduplicator.clear_all()
|
||||||
|
# --- 清理氣泡去重記錄結束 ---
|
||||||
|
|
||||||
|
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 +1907,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 +1958,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.")
|
||||||
@ -1944,6 +2071,13 @@ def run_ui_monitoring_loop(trigger_queue: queue.Queue, command_queue: queue.Queu
|
|||||||
print("Warning: Failed to capture bubble snapshot. Skipping this bubble.")
|
print("Warning: Failed to capture bubble snapshot. Skipping this bubble.")
|
||||||
continue # Skip to next bubble
|
continue # Skip to next bubble
|
||||||
|
|
||||||
|
# --- New: Image deduplication check ---
|
||||||
|
if bubble_deduplicator.is_duplicate(bubble_snapshot, bubble_region_tuple):
|
||||||
|
print("Detected duplicate bubble, skipping processing")
|
||||||
|
perform_state_cleanup(detector, interactor)
|
||||||
|
continue # Skip processing this bubble
|
||||||
|
# --- End of image deduplication check ---
|
||||||
|
|
||||||
# --- Save Snapshot for Debugging ---
|
# --- Save Snapshot for Debugging ---
|
||||||
try:
|
try:
|
||||||
screenshot_index = (screenshot_counter % MAX_DEBUG_SCREENSHOTS) + 1
|
screenshot_index = (screenshot_counter % MAX_DEBUG_SCREENSHOTS) + 1
|
||||||
@ -2010,16 +2144,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 +2221,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) # No longer needed with image deduplication
|
||||||
|
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
|
||||||
@ -2173,6 +2323,16 @@ def run_ui_monitoring_loop(trigger_queue: queue.Queue, command_queue: queue.Queu
|
|||||||
trigger_queue.put(data_to_send)
|
trigger_queue.put(data_to_send)
|
||||||
print("Trigger info (with region, reply flag, snapshot, search_area) placed in Queue.")
|
print("Trigger info (with region, reply flag, snapshot, search_area) placed in Queue.")
|
||||||
|
|
||||||
|
# --- 新增:更新氣泡去重記錄中的發送者信息 ---
|
||||||
|
# 注意:我們在前面已經添加了氣泡到去重系統,但當時還沒獲取發送者名稱
|
||||||
|
# 這裡我們嘗試再次更新發送者信息(如果實現允許的話)
|
||||||
|
if 'bubble_deduplicator' in locals() and bubble_snapshot and sender_name:
|
||||||
|
bubble_id = bubble_deduplicator.generate_bubble_id(bubble_region_tuple)
|
||||||
|
if bubble_id in bubble_deduplicator.recent_bubbles:
|
||||||
|
bubble_deduplicator.recent_bubbles[bubble_id]['sender'] = sender_name
|
||||||
|
bubble_deduplicator._save_storage()
|
||||||
|
# --- 更新發送者信息結束 ---
|
||||||
|
|
||||||
# --- CRITICAL: Break loop after successfully processing one trigger ---
|
# --- CRITICAL: Break loop after successfully processing one trigger ---
|
||||||
print("--- Single bubble processing complete. Breaking scan cycle. ---")
|
print("--- Single bubble processing complete. Breaking scan cycle. ---")
|
||||||
break # Exit the 'for target_bubble_info in sorted_bubbles' loop
|
break # Exit the 'for target_bubble_info in sorted_bubbles' loop
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user