Fix Game monitor not restart game issue

This commit is contained in:
z060142 2025-04-25 23:50:32 +08:00
parent 94e3b55136
commit 96f53ecdfc
5 changed files with 138 additions and 68 deletions

View File

@ -54,9 +54,23 @@ Wolf Chat 是一個基於 MCP (Modular Capability Provider) 框架的聊天機
- 持續監控遊戲視窗 (`config.WINDOW_TITLE`)。 - 持續監控遊戲視窗 (`config.WINDOW_TITLE`)。
- 確保視窗維持在設定檔 (`config.py`) 中指定的位置 (`GAME_WINDOW_X`, `GAME_WINDOW_Y`) 和大小 (`GAME_WINDOW_WIDTH`, `GAME_WINDOW_HEIGHT`)。 - 確保視窗維持在設定檔 (`config.py`) 中指定的位置 (`GAME_WINDOW_X`, `GAME_WINDOW_Y`) 和大小 (`GAME_WINDOW_WIDTH`, `GAME_WINDOW_HEIGHT`)。
- 確保視窗維持在最上層 (Always on Top)。 - 確保視窗維持在最上層 (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`)。 - **生命週期管理**:由 `main.py` 在啟動時創建,並在 `shutdown` 過程中嘗試終止 (`terminate`)。
- 僅在進行調整時打印訊息。
### 資料流程 ### 資料流程
@ -381,6 +395,28 @@ Wolf Chat 是一個基於 MCP (Modular Capability Provider) 框架的聊天機
4. **工具優先級**:明確定義了內部工具使用的優先順序:`read_note` > `search_notes` > `recent_activity` 4. **工具優先級**:明確定義了內部工具使用的優先順序:`read_note` > `search_notes` > `recent_activity`
- **效果**:預期 LLM 在回應前會更穩定地執行記憶體檢索步驟,特別是強制性的用戶 Profile 檢查,從而提高回應的上下文一致性和角色扮演的準確性。 - **效果**:預期 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` 內部,減少了模塊間的直接依賴和潛在干擾,依賴持續監控來確保最終視窗狀態。
## 開發建議 ## 開發建議
### 優化方向 ### 優化方向

View File

@ -37,30 +37,30 @@ exa_config_arg_string_single_dump = json.dumps(exa_config_dict) # Use this one
# --- MCP Server Configuration --- # --- MCP Server Configuration ---
MCP_SERVERS = { MCP_SERVERS = {
"exa": { # Temporarily commented out to prevent blocking startup #"exa": { # Temporarily commented out to prevent blocking startup
"command": "cmd", ## "command": "cmd",
"args": [ # "args": [
"/c", # "/c",
"npx", # "npx",
"-y", # "-y",
"@smithery/cli@latest", # "@smithery/cli@latest",
"run", # "run",
"exa", # "exa",
"--config", # "--config",
# Pass the dynamically created config string with the environment variable key # # Pass the dynamically created config string with the environment variable key
exa_config_arg_string_single_dump # Use the single dump variable # 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": {
"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": { #"github.com/modelcontextprotocol/servers/tree/main/src/memory": {
# "command": "npx", # "command": "npx",
# "args": [ # "args": [

View File

@ -23,8 +23,15 @@ import logging
# --- Setup Logging --- # --- Setup Logging ---
monitor_logger = logging.getLogger('GameMonitor') monitor_logger = logging.getLogger('GameMonitor')
# Basic config for direct run, main.py might configure differently monitor_logger.setLevel(logging.INFO) # Set level for the logger
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') 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 --- # --- Helper Functions ---
@ -64,6 +71,7 @@ def restart_game_process():
except Exception as wait_kill_err: except Exception as wait_kill_err:
monitor_logger.error(f"等待進程 {proc_info['pid']} 強制結束時出錯: {wait_kill_err}", exc_info=False) 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.)") 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 break # Exit the loop once a process is handled
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): 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) monitor_logger.error(f"啟動錯誤 (OSError): {ose} - 檢查路徑和權限。(Launch Error (OSError): {ose} - Check path and permissions.)", exc_info=True)
except Exception as e: except Exception as e:
monitor_logger.error(f"啟動遊戲時發生未預期錯誤: {e}", exc_info=True) 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(): def perform_scheduled_restart():
"""Handles the sequence of pausing UI, restarting game, resuming UI.""" """Handles the sequence of pausing UI, restarting game, resuming UI."""
monitor_logger.info("開始執行定時重啟流程。(Starting scheduled restart sequence.)") monitor_logger.info("開始執行定時重啟流程。(Starting scheduled restart sequence.)")
# 1. Signal main process to pause UI monitoring via stdout # Removed pause_ui signal - UI will handle its own pause/resume based on restart_complete
pause_signal_data = {'action': 'pause_ui'}
try: try:
json_signal = json.dumps(pause_signal_data) # 1. Attempt to restart the game (no verification)
monitor_logger.info(f"準備發送暫停訊號: {json_signal} (Preparing to send pause signal)") # Log before print monitor_logger.info("嘗試執行遊戲重啟。(Attempting game restart process.)")
print(json_signal, flush=True) restart_game_process() # Fire and forget restart attempt
monitor_logger.info("已發送暫停 UI 監控訊號。(Sent pause UI monitoring signal.)") monitor_logger.info("遊戲重啟嘗試已執行。(Game restart attempt executed.)")
except Exception as e:
monitor_logger.error(f"發送暫停訊號 '{json_signal}' 失敗: {e}", exc_info=True) # Log signal data on error
# 2. Wait 1 minute # 2. Wait fixed time after restart attempt
monitor_logger.info("等待 60 秒以暫停 UI。(Waiting 60 seconds for UI pause...)") monitor_logger.info("等待 30 秒讓遊戲啟動(無驗證)。(Waiting 30 seconds for game to launch (no verification)...)")
time.sleep(30) time.sleep(30) # Fixed wait
# 3. Restart the game except Exception as restart_err:
restart_game_process() monitor_logger.error(f"執行 restart_game_process 時發生未預期錯誤: {restart_err}", exc_info=True)
# Continue to finally block even on error
# 4. Wait 1 minute finally:
monitor_logger.info("等待 60 秒讓遊戲啟動。(Waiting 60 seconds for game to launch...)") # 3. Signal main process that restart attempt is complete via stdout
time.sleep(30) 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 monitor_logger.info("定時重啟流程(包括 finally 塊)執行完畢。(Scheduled restart sequence (including finally block) finished.)")
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.)")
# Configure logger (basic example, adjust as needed) # Configure logger (basic example, adjust as needed)
# (Logging setup moved earlier) # (Logging setup moved earlier)
@ -229,19 +237,18 @@ def monitor_game_window():
pass # Keep silent pass # Keep silent
except gw.PyGetWindowException as e: except gw.PyGetWindowException as e:
# monitor_logger.warning(f"無法訪問視窗屬性 (可能已關閉): {e} (Could not access window properties (may be closed): {e})") # Log PyGetWindowException specifically, might indicate window closed during check
pass # Window might have closed between find and access monitor_logger.warning(f"監控循環中無法訪問視窗屬性 (可能已關閉): {e} (Could not access window properties in monitor loop (may be closed): {e})")
except Exception as 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) 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 # This should NOT print JSON signals
if adjustment_made and current_message and current_message != last_adjustment_message: if adjustment_made and current_message and current_message != last_adjustment_message:
# monitor_logger.info(f"遊戲視窗狀態已調整: {current_message.strip()}") # Log instead of print # Log the adjustment message instead of printing to stdout
# Keep print for now for visibility of adjustments, but ensure ONLY JSON goes for signals monitor_logger.info(f"[GameMonitor] {current_message.strip()}")
# print(f"[GameMonitor] {current_message.strip()}") # REMOVED to prevent non-JSON output last_adjustment_message = current_message
monitor_logger.info(f"[GameMonitor] {current_message.strip()}") # Log instead
last_adjustment_message = current_message
elif not window: elif not window:
# Reset last message if window disappears # Reset last message if window disappears
last_adjustment_message = "" last_adjustment_message = ""

16
main.py
View File

@ -168,14 +168,22 @@ def read_monitor_output(process: subprocess.Popen, queue: ThreadSafeQueue, loop:
loop.call_soon_threadsafe(queue.put_nowait, command) loop.call_soon_threadsafe(queue.put_nowait, command)
print("[Monitor Reader Thread] 暫停命令已放入隊列。(Pause command queued.)") # Log after queueing print("[Monitor Reader Thread] 暫停命令已放入隊列。(Pause command queued.)") # Log after queueing
elif action == 'resume_ui': elif action == 'resume_ui':
command = {'action': 'resume'} # Removed direct resume_ui handling - ui_interaction will handle pause/resume based on restart_complete
print(f"[Monitor Reader Thread] 準備將命令放入隊列: {command} (Preparing to queue command)") # Log before queueing print("[Monitor Reader Thread] 收到舊的 'resume_ui' 訊號,忽略。(Received old 'resume_ui' signal, ignoring.)")
loop.call_soon_threadsafe(queue.put_nowait, command) elif action == 'restart_complete':
print("[Monitor Reader Thread] 恢復命令已放入隊列。(Resume command queued.)") # Log after queueing 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: else:
print(f"[Monitor Reader Thread] 從監控器收到未知動作: {action} (Received unknown action from monitor: {action})") print(f"[Monitor Reader Thread] 從監控器收到未知動作: {action} (Received unknown action from monitor: {action})")
except json.JSONDecodeError: except json.JSONDecodeError:
print(f"[Monitor Reader Thread] ERROR: 無法解析來自監控器的 JSON: '{line}' (Could not decode JSON from monitor: '{line}')") 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: except Exception as e:
print(f"[Monitor Reader Thread] 處理監控器輸出時出錯: {e} (Error processing monitor output: {e})") print(f"[Monitor Reader Thread] 處理監控器輸出時出錯: {e} (Error processing monitor output: {e})")
# No sleep needed here as readline() is blocking # No sleep needed here as readline() is blocking

View File

@ -121,7 +121,7 @@ CHAT_INPUT_CENTER_X = 400
CHAT_INPUT_CENTER_Y = 1280 CHAT_INPUT_CENTER_Y = 1280
SCREENSHOT_REGION = (70, 50, 800, 1365) # Updated region SCREENSHOT_REGION = (70, 50, 800, 1365) # Updated region
CONFIDENCE_THRESHOLD = 0.9 # Increased threshold for corner matching 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 = -45 # Original offset, used for non-reply interactions like position removal
# AVATAR_OFFSET_X_RELOCATED = -50 # Replaced by specific reply offsets # 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) 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 if monitoring_paused_flag[0]: # Avoid redundant prints if already running
print("UI Thread: Processing resume command. Resuming monitoring.") print("UI Thread: Processing resume command. Resuming monitoring.")
monitoring_paused_flag[0] = False 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 elif action == 'clear_history': # Added for F7
print("UI Thread: Processing clear_history command.") print("UI Thread: Processing clear_history command.")