diff --git a/ClaudeCode.md b/ClaudeCode.md index 327e565..b7964f0 100644 --- a/ClaudeCode.md +++ b/ClaudeCode.md @@ -54,9 +54,23 @@ Wolf Chat 是一個基於 MCP (Modular Capability Provider) 框架的聊天機 - 持續監控遊戲視窗 (`config.WINDOW_TITLE`)。 - 確保視窗維持在設定檔 (`config.py`) 中指定的位置 (`GAME_WINDOW_X`, `GAME_WINDOW_Y`) 和大小 (`GAME_WINDOW_WIDTH`, `GAME_WINDOW_HEIGHT`)。 - 確保視窗維持在最上層 (Always on Top)。 - - **作為獨立進程運行**:由 `main.py` 使用 `subprocess.Popen` 啟動,以隔離執行環境,確保縮放行為一致。 + - **定時遊戲重啟** (如果 `config.ENABLE_SCHEDULED_RESTART` 為 True): + - 根據 `config.RESTART_INTERVAL_MINUTES` 設定的間隔執行。 + - **簡化流程 (2025-04-25)**: + 1. 通過 `stdout` 向 `main.py` 發送 JSON 訊號 (`{'action': 'pause_ui'}`),請求暫停 UI 監控。 + 2. 等待固定時間(30 秒)。 + 3. 調用 `restart_game_process` 函數,**嘗試**終止 (`terminate`/`kill`) `LastWar.exe` 進程(**無驗證**)。 + 4. 等待固定時間(2 秒)。 + 5. **嘗試**使用 `os.startfile` 啟動 `config.GAME_EXECUTABLE_PATH`(**無驗證**)。 + 6. 等待固定時間(30 秒)。 + 7. 使用 `try...finally` 結構確保**總是**執行下一步。 + 8. 通過 `stdout` 向 `main.py` 發送 JSON 訊號 (`{'action': 'resume_ui'}`),請求恢復 UI 監控。 + - **視窗調整**:遊戲視窗的位置/大小/置頂狀態的調整完全由 `monitor_game_window` 的主循環持續負責,重啟流程不再進行立即調整。 + - **作為獨立進程運行**:由 `main.py` 使用 `subprocess.Popen` 啟動,捕獲其 `stdout` (用於 JSON 訊號) 和 `stderr` (用於日誌)。 + - **進程間通信**: + - `game_monitor.py` -> `main.py`:通過 `stdout` 發送 JSON 格式的 `pause_ui` 和 `resume_ui` 訊號。 + - **日誌處理**:`game_monitor.py` 的日誌被配置為輸出到 `stderr`,以保持 `stdout` 清潔,確保訊號傳遞可靠性。`main.py` 會讀取 `stderr` 並可能顯示這些日誌。 - **生命週期管理**:由 `main.py` 在啟動時創建,並在 `shutdown` 過程中嘗試終止 (`terminate`)。 - - 僅在進行調整時打印訊息。 ### 資料流程 @@ -381,6 +395,28 @@ Wolf Chat 是一個基於 MCP (Modular Capability Provider) 框架的聊天機 4. **工具優先級**:明確定義了內部工具使用的優先順序:`read_note` > `search_notes` > `recent_activity`。 - **效果**:預期 LLM 在回應前會更穩定地執行記憶體檢索步驟,特別是強制性的用戶 Profile 檢查,從而提高回應的上下文一致性和角色扮演的準確性。 +### 遊戲監控與定時重啟穩定性改進 (2025-04-25) + +- **目的**:解決 `game_monitor.py` 在執行定時重啟時,可能出現遊戲未成功關閉/重啟,且 UI 監控未恢復的問題。 +- **`game_monitor.py` (第一階段修改)**: + - **日誌重定向**:將所有 `logging` 輸出重定向到 `stderr`,確保 `stdout` 只用於傳輸 JSON 訊號 (`pause_ui`, `resume_ui`) 給 `main.py`,避免訊號被日誌干擾。 + - **終止驗證**:在 `restart_game_process` 中,嘗試終止遊戲進程後,加入循環檢查(最多 10 秒),使用 `psutil.pid_exists` 確認進程確實已結束。 + - **啟動驗證**:在 `restart_game_process` 中,嘗試啟動遊戲後,使用循環檢查(最多 90 秒),調用 `find_game_window` 確認遊戲視窗已出現,取代固定的等待時間。 + - **立即調整嘗試**:在 `perform_scheduled_restart` 中,於成功驗證遊戲啟動後,立即嘗試調整一次視窗位置/大小/置頂。 + - **保證恢復訊號**:在 `perform_scheduled_restart` 中,使用 `try...finally` 結構包裹遊戲重啟邏輯,確保無論重啟成功與否,都會嘗試通過 `stdout` 發送 `resume_ui` 訊號給 `main.py`。 +- **`game_monitor.py` (第二階段修改 - 簡化)**: + - **移除驗證與立即調整**:根據使用者回饋,移除了終止驗證、啟動驗證以及立即調整視窗的邏輯。 + - **恢復固定等待**:重啟流程恢復使用固定的 `time.sleep()` 等待時間。 + - **發送重啟完成訊號**:在重啟流程結束後,發送 `{'action': 'restart_complete'}` JSON 訊號給 `main.py`。 +- **`main.py`**: + - **轉發重啟完成訊號**:`read_monitor_output` 線程接收到 `game_monitor.py` 的 `{'action': 'restart_complete'}` 訊號後,將 `{'action': 'handle_restart_complete'}` 命令放入 `command_queue`。 +- **`ui_interaction.py`**: + - **內部處理重啟完成**:`run_ui_monitoring_loop` 接收到 `{'action': 'handle_restart_complete'}` 命令後,在 UI 線程內部執行: + 1. 暫停 UI 監控。 + 2. 等待固定時間(30 秒),讓遊戲啟動並穩定。 + 3. 恢復 UI 監控並重置狀態(清除 `recent_texts` 和 `last_processed_bubble_info`)。 +- **效果**:將暫停/恢復 UI 監控的時序控制權移至 `ui_interaction.py` 內部,減少了模塊間的直接依賴和潛在干擾,依賴持續監控來確保最終視窗狀態。 + ## 開發建議 ### 優化方向 diff --git a/config.py b/config.py index a53b71d..56a063c 100644 --- a/config.py +++ b/config.py @@ -37,30 +37,30 @@ exa_config_arg_string_single_dump = json.dumps(exa_config_dict) # Use this one # --- MCP Server Configuration --- MCP_SERVERS = { - "exa": { # Temporarily commented out to prevent blocking startup - "command": "cmd", - "args": [ - "/c", - "npx", - "-y", - "@smithery/cli@latest", - "run", - "exa", - "--config", - # Pass the dynamically created config string with the environment variable key - exa_config_arg_string_single_dump # Use the single dump variable - ], - }, - #"exa": { - # "command": "npx", - # "args": [ - # "Z:/mcp/Server/exa-mcp-server/build/index.js", - # "--tools=web_search,research_paper_search,twitter_search,company_research,crawling,competitor_finder" - # ], - # "env": { - # "EXA_API_KEY": EXA_API_KEY - # } + #"exa": { # Temporarily commented out to prevent blocking startup + ## "command": "cmd", + # "args": [ + # "/c", + # "npx", + # "-y", + # "@smithery/cli@latest", + # "run", + # "exa", + # "--config", + # # Pass the dynamically created config string with the environment variable key + # exa_config_arg_string_single_dump # Use the single dump variable + # ], #}, + "exa": { + "command": "npx", + "args": [ + "C:/Users/Bigspring/AppData/Roaming/npm/exa-mcp-server", + "--tools=web_search,research_paper_search,twitter_search,company_research,crawling,competitor_finder" + ], + "env": { + "EXA_API_KEY": EXA_API_KEY + } + }, #"github.com/modelcontextprotocol/servers/tree/main/src/memory": { # "command": "npx", # "args": [ diff --git a/game_monitor.py b/game_monitor.py index a2153c4..4363769 100644 --- a/game_monitor.py +++ b/game_monitor.py @@ -23,8 +23,15 @@ import logging # --- Setup Logging --- monitor_logger = logging.getLogger('GameMonitor') -# Basic config for direct run, main.py might configure differently -logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') +monitor_logger.setLevel(logging.INFO) # Set level for the logger +log_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +# Create handler for stderr +stderr_handler = logging.StreamHandler(sys.stderr) # Explicitly use stderr +stderr_handler.setFormatter(log_formatter) +# Add handler to the logger +if not monitor_logger.hasHandlers(): # Avoid adding multiple handlers if run multiple times + monitor_logger.addHandler(stderr_handler) +monitor_logger.propagate = False # Prevent propagation to root logger if basicConfig was called elsewhere # --- Helper Functions --- @@ -64,6 +71,7 @@ def restart_game_process(): except Exception as wait_kill_err: monitor_logger.error(f"等待進程 {proc_info['pid']} 強制結束時出錯: {wait_kill_err}", exc_info=False) + # Removed Termination Verification - Rely on main loop for eventual state correction monitor_logger.info(f"已處理匹配的進程 PID: {proc_info['pid']},停止搜索。(Processed matching process PID: {proc_info['pid']}, stopping search.)") break # Exit the loop once a process is handled except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): @@ -97,43 +105,43 @@ def restart_game_process(): monitor_logger.error(f"啟動錯誤 (OSError): {ose} - 檢查路徑和權限。(Launch Error (OSError): {ose} - Check path and permissions.)", exc_info=True) except Exception as e: monitor_logger.error(f"啟動遊戲時發生未預期錯誤: {e}", exc_info=True) + # Don't return False here, let the process continue to send resume signal + # Removed Startup Verification - Rely on main loop for eventual state correction + # Always return True (or nothing) to indicate the attempt was made + return # Or return True, doesn't matter much now def perform_scheduled_restart(): """Handles the sequence of pausing UI, restarting game, resuming UI.""" monitor_logger.info("開始執行定時重啟流程。(Starting scheduled restart sequence.)") - # 1. Signal main process to pause UI monitoring via stdout - pause_signal_data = {'action': 'pause_ui'} + # Removed pause_ui signal - UI will handle its own pause/resume based on restart_complete + try: - json_signal = json.dumps(pause_signal_data) - monitor_logger.info(f"準備發送暫停訊號: {json_signal} (Preparing to send pause signal)") # Log before print - print(json_signal, flush=True) - monitor_logger.info("已發送暫停 UI 監控訊號。(Sent pause UI monitoring signal.)") - except Exception as e: - monitor_logger.error(f"發送暫停訊號 '{json_signal}' 失敗: {e}", exc_info=True) # Log signal data on error + # 1. Attempt to restart the game (no verification) + monitor_logger.info("嘗試執行遊戲重啟。(Attempting game restart process.)") + restart_game_process() # Fire and forget restart attempt + monitor_logger.info("遊戲重啟嘗試已執行。(Game restart attempt executed.)") - # 2. Wait 1 minute - monitor_logger.info("等待 60 秒以暫停 UI。(Waiting 60 seconds for UI pause...)") - time.sleep(30) + # 2. Wait fixed time after restart attempt + monitor_logger.info("等待 30 秒讓遊戲啟動(無驗證)。(Waiting 30 seconds for game to launch (no verification)...)") + time.sleep(30) # Fixed wait - # 3. Restart the game - restart_game_process() + except Exception as restart_err: + monitor_logger.error(f"執行 restart_game_process 時發生未預期錯誤: {restart_err}", exc_info=True) + # Continue to finally block even on error - # 4. Wait 1 minute - monitor_logger.info("等待 60 秒讓遊戲啟動。(Waiting 60 seconds for game to launch...)") - time.sleep(30) + finally: + # 3. Signal main process that restart attempt is complete via stdout + monitor_logger.info("發送重啟完成訊號。(Sending restart complete signal.)") + restart_complete_signal_data = {'action': 'restart_complete'} + try: + json_signal = json.dumps(restart_complete_signal_data) + print(json_signal, flush=True) + monitor_logger.info("已發送重啟完成訊號。(Sent restart complete signal.)") + except Exception as e: + monitor_logger.error(f"發送重啟完成訊號 '{json_signal}' 失敗: {e}", exc_info=True) # Log signal data on error - # 5. Signal main process to resume UI monitoring via stdout - resume_signal_data = {'action': 'resume_ui'} - try: - json_signal = json.dumps(resume_signal_data) - monitor_logger.info(f"準備發送恢復訊號: {json_signal} (Preparing to send resume signal)") # Log before print - print(json_signal, flush=True) - monitor_logger.info("已發送恢復 UI 監控訊號。(Sent resume UI monitoring signal.)") - except Exception as e: - monitor_logger.error(f"發送恢復訊號 '{json_signal}' 失敗: {e}", exc_info=True) # Log signal data on error - - monitor_logger.info("定時重啟流程完成。(Scheduled restart sequence complete.)") + monitor_logger.info("定時重啟流程(包括 finally 塊)執行完畢。(Scheduled restart sequence (including finally block) finished.)") # Configure logger (basic example, adjust as needed) # (Logging setup moved earlier) @@ -229,19 +237,18 @@ def monitor_game_window(): pass # Keep silent except gw.PyGetWindowException as e: - # monitor_logger.warning(f"無法訪問視窗屬性 (可能已關閉): {e} (Could not access window properties (may be closed): {e})") - pass # Window might have closed between find and access + # Log PyGetWindowException specifically, might indicate window closed during check + monitor_logger.warning(f"監控循環中無法訪問視窗屬性 (可能已關閉): {e} (Could not access window properties in monitor loop (may be closed): {e})") except Exception as e: + # Log other exceptions during monitoring monitor_logger.error(f"監控遊戲視窗時發生未預期錯誤: {e} (Unexpected error during game window monitoring: {e})", exc_info=True) - # Print adjustment message only if an adjustment was made and it's different from the last one + # Log adjustment message only if an adjustment was made and it's different from the last one # This should NOT print JSON signals if adjustment_made and current_message and current_message != last_adjustment_message: - # monitor_logger.info(f"遊戲視窗狀態已調整: {current_message.strip()}") # Log instead of print - # Keep print for now for visibility of adjustments, but ensure ONLY JSON goes for signals - # print(f"[GameMonitor] {current_message.strip()}") # REMOVED to prevent non-JSON output - monitor_logger.info(f"[GameMonitor] {current_message.strip()}") # Log instead - last_adjustment_message = current_message + # Log the adjustment message instead of printing to stdout + monitor_logger.info(f"[GameMonitor] {current_message.strip()}") + last_adjustment_message = current_message elif not window: # Reset last message if window disappears last_adjustment_message = "" diff --git a/main.py b/main.py index 9b5095d..0c4d930 100644 --- a/main.py +++ b/main.py @@ -168,14 +168,22 @@ def read_monitor_output(process: subprocess.Popen, queue: ThreadSafeQueue, loop: loop.call_soon_threadsafe(queue.put_nowait, command) print("[Monitor Reader Thread] 暫停命令已放入隊列。(Pause command queued.)") # Log after queueing elif action == 'resume_ui': - command = {'action': 'resume'} - print(f"[Monitor Reader Thread] 準備將命令放入隊列: {command} (Preparing to queue command)") # Log before queueing - loop.call_soon_threadsafe(queue.put_nowait, command) - print("[Monitor Reader Thread] 恢復命令已放入隊列。(Resume command queued.)") # Log after queueing + # Removed direct resume_ui handling - ui_interaction will handle pause/resume based on restart_complete + print("[Monitor Reader Thread] 收到舊的 'resume_ui' 訊號,忽略。(Received old 'resume_ui' signal, ignoring.)") + elif action == 'restart_complete': + command = {'action': 'handle_restart_complete'} + print(f"[Monitor Reader Thread] 收到 'restart_complete' 訊號,準備將命令放入隊列: {command} (Received 'restart_complete' signal, preparing to queue command)") + try: + loop.call_soon_threadsafe(queue.put_nowait, command) + print("[Monitor Reader Thread] 'handle_restart_complete' 命令已放入隊列。(handle_restart_complete command queued.)") + except Exception as q_err: + print(f"[Monitor Reader Thread] 將 'handle_restart_complete' 命令放入隊列時出錯: {q_err} (Error putting 'handle_restart_complete' command in queue: {q_err})") else: print(f"[Monitor Reader Thread] 從監控器收到未知動作: {action} (Received unknown action from monitor: {action})") except json.JSONDecodeError: print(f"[Monitor Reader Thread] ERROR: 無法解析來自監控器的 JSON: '{line}' (Could not decode JSON from monitor: '{line}')") + # Log the raw line that failed to parse + # print(f"[Monitor Reader Thread] Raw line that failed JSON decode: '{line}'") # Already logged raw line earlier except Exception as e: print(f"[Monitor Reader Thread] 處理監控器輸出時出錯: {e} (Error processing monitor output: {e})") # No sleep needed here as readline() is blocking diff --git a/ui_interaction.py b/ui_interaction.py index 22583e7..eac3e12 100644 --- a/ui_interaction.py +++ b/ui_interaction.py @@ -121,7 +121,7 @@ CHAT_INPUT_CENTER_X = 400 CHAT_INPUT_CENTER_Y = 1280 SCREENSHOT_REGION = (70, 50, 800, 1365) # Updated region CONFIDENCE_THRESHOLD = 0.9 # Increased threshold for corner matching -STATE_CONFIDENCE_THRESHOLD = 0.7 +STATE_CONFIDENCE_THRESHOLD = 0.9 AVATAR_OFFSET_X = -45 # Original offset, used for non-reply interactions like position removal # AVATAR_OFFSET_X_RELOCATED = -50 # Replaced by specific reply offsets AVATAR_OFFSET_X_REPLY = -45 # Horizontal offset for avatar click after re-location (for reply context) @@ -1163,7 +1163,26 @@ def run_ui_monitoring_loop(trigger_queue: queue.Queue, command_queue: queue.Queu if monitoring_paused_flag[0]: # Avoid redundant prints if already running print("UI Thread: Processing resume command. Resuming monitoring.") monitoring_paused_flag[0] = False - # No continue needed here + # No state reset here, reset_state command handles that + + elif action == 'handle_restart_complete': # Added for game monitor restart signal + print("UI Thread: Received 'handle_restart_complete' command. Initiating internal pause/wait/resume sequence.") + # --- Internal Pause/Wait/Resume Sequence --- + if not monitoring_paused_flag[0]: # Only pause if not already paused + print("UI Thread: Pausing monitoring internally for restart.") + monitoring_paused_flag[0] = True + # No need to send command back to main loop, just update flag + + print("UI Thread: Waiting 30 seconds for game to stabilize after restart.") + time.sleep(30) # Wait for game to launch and stabilize + + print("UI Thread: Resuming monitoring internally after restart wait.") + monitoring_paused_flag[0] = False + # Clear state to ensure fresh detection after restart + recent_texts.clear() + last_processed_bubble_info = None + print("UI Thread: Monitoring resumed and state reset after restart.") + # --- End Internal Sequence --- elif action == 'clear_history': # Added for F7 print("UI Thread: Processing clear_history command.")