diff --git a/ClaudeCode.md b/ClaudeCode.md index 8207283..c329354 100644 --- a/ClaudeCode.md +++ b/ClaudeCode.md @@ -58,7 +58,7 @@ Wolf Chat 是一個基於 MCP (Modular Capability Provider) 框架的聊天機 7. **遊戲視窗監控模組 (game_monitor.py)** (取代 window-setup-script.py 和舊的 window-monitor-script.py) - 持續監控遊戲視窗 (`config.WINDOW_TITLE`)。 - 確保視窗維持在設定檔 (`config.py`) 中指定的位置 (`GAME_WINDOW_X`, `GAME_WINDOW_Y`) 和大小 (`GAME_WINDOW_WIDTH`, `GAME_WINDOW_HEIGHT`)。 - - 確保視窗維持在最上層 (Always on Top)。 + - **確保視窗保持活躍**:如果遊戲視窗不是目前的前景視窗,則嘗試將其帶到前景並啟用 (Bring to Foreground/Activate),取代了之前的強制置頂 (Always on Top) 邏輯 (修改於 2025-05-12)。 - **定時遊戲重啟** (如果 `config.ENABLE_SCHEDULED_RESTART` 為 True): - 根據 `config.RESTART_INTERVAL_MINUTES` 設定的間隔執行。 - **簡化流程 (2025-04-25)**: @@ -598,6 +598,18 @@ Wolf Chat 是一個基於 MCP (Modular Capability Provider) 框架的聊天機 - **依賴項**:Windows 上的控制台事件處理仍然依賴 `pywin32` 套件。如果未安裝,程式會打印警告,關閉時的可靠性可能略有降低(但 `stdio_client` 的正常清理機制應在多數情況下仍然有效)。 - **效果**:恢復了與 `mcp` 庫的兼容性,同時通過標準的上下文管理和輔助性的 Windows 事件處理,實現了在主程式退出時關閉 MCP 伺服器子進程的目標。 +## 最近改進(2025-05-12) + +### 遊戲視窗置頂邏輯修改 + +- **目的**:將 `game_monitor.py` 中強制遊戲視窗「永遠在最上層」(Always on Top) 的行為,修改為「臨時置頂並獲得焦點」(Bring to Foreground/Activate),以解決原方法僅覆蓋其他視窗的問題。 +- **`game_monitor.py`**: + - 在 `monitor_game_window` 函數的監控循環中,移除了使用 `win32gui.SetWindowPos` 和 `win32con.HWND_TOPMOST` 來檢查和設定 `WS_EX_TOPMOST` 樣式的程式碼。 + - 替換為檢查當前前景視窗 (`win32gui.GetForegroundWindow()`) 是否為目標遊戲視窗 (`hwnd`)。 + - 如果不是,則調用 `win32gui.BringWindowToTop(hwnd)` 和 `win32gui.SetForegroundWindow(hwnd)` 來嘗試將遊戲視窗帶到前景並啟用。 + - 更新了相關的日誌訊息以反映新的行為。 +- **效果**:監控腳本現在會嘗試將失去焦點的遊戲視窗重新激活並帶到前景,而不是強制其覆蓋所有其他視窗。這更符合一般視窗的行為模式。 + ## 開發建議 ### 優化方向 diff --git a/Setup.py b/Setup.py index bd0918e..580d463 100644 --- a/Setup.py +++ b/Setup.py @@ -2443,6 +2443,7 @@ if HAS_SOCKETIO: self.authenticated = False self.should_exit_flag = threading.Event() # Use an event for thread control self.client_thread = None + self.last_successful_connection_time = None # Track last successful connection/auth self.registered_commands = [ "restart bot", "restart game", "restart all", @@ -2473,46 +2474,80 @@ if HAS_SOCKETIO: last_heartbeat = time.time() # For heartbeat retry_delay = 1.0 # Start with 1 second delay for exponential backoff max_delay = 300.0 # Maximum delay of 5 minutes for exponential backoff - + hourly_refresh_interval = 3600 # 1 hour in seconds + while not self.should_exit_flag.is_set(): + current_time = time.time() # Get current time at the start of the loop iteration + if not self.sio.connected: + # Reset connection time tracker when attempting to connect + self.last_successful_connection_time = None try: logger.info(f"ControlClient: Attempting to connect to {self.server_url}...") self.sio.connect(self.server_url) - logger.info("ControlClient: Successfully connected.") - retry_delay = 1.0 # Reset delay on successful connection - last_heartbeat = time.time() # Reset heartbeat timer on new connection + # Connection successful, wait for authentication to set last_successful_connection_time + logger.info("ControlClient: Successfully established socket connection. Waiting for authentication.") + retry_delay = 1.0 # Reset delay on successful connection attempt + # last_heartbeat = time.time() # Reset heartbeat timer only after authentication? Or here? Let's keep it after auth. except socketio.exceptions.ConnectionError as e: logger.error(f"ControlClient: Connection failed: {e}. Retrying in {retry_delay:.2f}s.") self.should_exit_flag.wait(retry_delay) # Implement exponential backoff with jitter retry_delay = min(retry_delay * 2, max_delay) * (0.8 + 0.4 * random.random()) retry_delay = max(1.0, retry_delay) # Ensure it's at least 1s - continue + continue except Exception as e: # Catch other potential errors during connection logger.error(f"ControlClient: Unexpected error during connection attempt: {e}. Retrying in {retry_delay:.2f}s.") self.should_exit_flag.wait(retry_delay) retry_delay = min(retry_delay * 2, max_delay) * (0.8 + 0.4 * random.random()) retry_delay = max(1.0, retry_delay) # Ensure it's at least 1s continue - - # If connected, manage heartbeat and check for exit signal + + # If connected (socket established, maybe not authenticated yet) if self.sio.connected: - current_time = time.time() - if current_time - last_heartbeat > 60: # Send heartbeat every 60 seconds + # Check for hourly refresh ONLY if authenticated and timer is set + if self.authenticated and self.last_successful_connection_time and (current_time - self.last_successful_connection_time > hourly_refresh_interval): + logger.info(f"ControlClient: Hourly session refresh triggered (Connected for > {hourly_refresh_interval}s). Disconnecting for refresh...") + try: + self.sio.disconnect() + # Reset flags immediately after intentional disconnect + self.connected = False + self.authenticated = False + self.last_successful_connection_time = None + logger.info("ControlClient: Disconnected for hourly refresh. Will attempt reconnect in next cycle.") + # Continue to the start of the loop to handle reconnection logic + continue + except Exception as e: + logger.error(f"ControlClient: Error during planned hourly disconnect: {e}") + # Reset flags anyway and let the loop retry + self.connected = False + self.authenticated = False + self.last_successful_connection_time = None + + + # Manage heartbeat if authenticated + if self.authenticated and current_time - last_heartbeat > 60: # Send heartbeat every 60 seconds try: self.sio.emit('heartbeat', {'timestamp': current_time}) last_heartbeat = current_time - logger.debug("ControlClient: Sent heartbeat to keep connection alive.") - except Exception as e: + logger.debug("ControlClient: Sent heartbeat.") + except Exception as e: logger.error(f"ControlClient: Error sending heartbeat: {e}. Connection might be lost.") - + # Consider triggering disconnect/reconnect logic here if heartbeat fails repeatedly + + # Wait before next loop iteration, checking for exit signal self.should_exit_flag.wait(1) # Check for exit signal every second - else: - # Fallback if not connected after attempt block (should be rare with current logic) - logger.debug(f"ControlClient: Not connected (unexpected state in loop), waiting {retry_delay:.2f}s before next cycle.") + + else: # Not connected (e.g., after a disconnect, or failed connection attempt) + # This path is hit after disconnects (intentional or unintentional) + # Reset connection time tracker if not already None + if self.last_successful_connection_time is not None: + logger.debug("ControlClient: Resetting connection timer as client is not connected.") + self.last_successful_connection_time = None + + logger.debug(f"ControlClient: Not connected, waiting {retry_delay:.2f}s before next connection attempt.") self.should_exit_flag.wait(retry_delay) - # Optionally re-calculate retry_delay here if this path is hit, to maintain backoff progression + # Exponential backoff for reconnection attempts retry_delay = min(retry_delay * 2, max_delay) * (0.8 + 0.4 * random.random()) retry_delay = max(1.0, retry_delay) @@ -2522,6 +2557,7 @@ if HAS_SOCKETIO: def _on_connect(self): self.connected = True + # Don't reset timer here, wait for authentication logger.info("ControlClient: Connected to server. Authenticating...") self.sio.emit('authenticate', { 'type': 'client', @@ -2530,26 +2566,30 @@ if HAS_SOCKETIO: }) def _on_disconnect(self): + was_connected = self.connected # Store previous state self.connected = False self.authenticated = False - logger.info("ControlClient: Disconnected from server.") - - # Force reconnection if not intentionally stopping - if not self.should_exit_flag.is_set(): - logger.info("ControlClient: Attempting immediate reconnection from _on_disconnect...") - try: - # This is an immediate attempt; _run_forever handles sustained retries. - if not self.sio.connected: # Check before trying to connect - self.sio.connect(self.server_url) - except Exception as e: - logger.error(f"ControlClient: Immediate reconnection from _on_disconnect failed: {e}") + self.last_successful_connection_time = None # Reset timer on any disconnect + if was_connected: # Only log if it was previously connected + logger.info("ControlClient: Disconnected from server.") + else: + logger.debug("ControlClient: Received disconnect event, but was already marked as disconnected.") + + # Remove the immediate reconnection attempt here, let _run_forever handle it with backoff + # if not self.should_exit_flag.is_set(): + # logger.info("ControlClient: Disconnected. Reconnection will be handled by the main loop.") def _on_authenticated(self, data): if data.get('success'): self.authenticated = True - logger.info("ControlClient: Authentication successful.") + self.last_successful_connection_time = time.time() # Start timer on successful auth + # Reset heartbeat timer upon successful authentication + # Find where last_heartbeat is accessible or make it accessible (e.g., self.last_heartbeat) + # For now, assume last_heartbeat is managed within _run_forever and will naturally reset timing + logger.info("ControlClient: Authentication successful. Hourly refresh timer started.") else: self.authenticated = False + self.last_successful_connection_time = None # Ensure timer is reset if auth fails logger.error(f"ControlClient: Authentication failed: {data.get('error', 'Unknown error')}") self.sio.disconnect() # Disconnect if auth fails diff --git a/game_monitor.py b/game_monitor.py index 4363769..8897517 100644 --- a/game_monitor.py +++ b/game_monitor.py @@ -217,24 +217,28 @@ def monitor_game_window(): # monitor_logger.warning(f"Failed to adjust window. Current: {new_pos} {new_size}, Target: {target_pos} {target_size}") pass # Keep silent on failure for now - # 2. Check and Set Topmost - style = win32gui.GetWindowLong(hwnd, win32con.GWL_EXSTYLE) - is_topmost = style & win32con.WS_EX_TOPMOST + # 2. Check and Bring to Foreground/Activate + current_foreground_hwnd = win32gui.GetForegroundWindow() - if not is_topmost: - # Set topmost, -1 for HWND_TOPMOST, flags = SWP_NOMOVE | SWP_NOSIZE - win32gui.SetWindowPos(hwnd, win32con.HWND_TOPMOST, 0, 0, 0, 0, - win32con.SWP_NOMOVE | win32con.SWP_NOSIZE) - # Verify - time.sleep(0.1) - new_style = win32gui.GetWindowLong(hwnd, win32con.GWL_EXSTYLE) - if new_style & win32con.WS_EX_TOPMOST: - current_message += "已將遊戲視窗設為最上層。(Set game window to topmost.)" - adjustment_made = True - else: - # Log failure if needed - # monitor_logger.warning("Failed to set window to topmost.") - pass # Keep silent + if current_foreground_hwnd != hwnd: + try: + # Attempt to bring to top and set foreground + # Note: SetForegroundWindow might fail if the calling process doesn't have foreground rights + win32gui.BringWindowToTop(hwnd) + win32gui.SetForegroundWindow(hwnd) + # Short delay to allow window manager to process + time.sleep(0.1) + # Verify if it became the foreground window + if win32gui.GetForegroundWindow() == hwnd: + current_message += "已將遊戲視窗帶到前景並啟用。(Brought game window to foreground and activated.) " + adjustment_made = True + else: + # Optional: Log if setting foreground failed, might happen if another app steals focus quickly + # monitor_logger.warning("嘗試將視窗設為前景後,它並未成為前景視窗。(Attempted to set window foreground, but it did not become the foreground window.)") + pass + except Exception as fg_err: + # This can happen if the window handle is invalid or other win32 errors occur + monitor_logger.warning(f"嘗試將視窗設為前景時出錯: {fg_err} (Error trying to set window foreground: {fg_err})") except gw.PyGetWindowException as e: # Log PyGetWindowException specifically, might indicate window closed during check