commit
583600760b
@ -50,13 +50,27 @@ Wolf Chat 是一個基於 MCP (Modular Capability Provider) 框架的聊天機
|
||||
- 包含外觀、說話風格、個性特點等資訊
|
||||
- 提供給 LLM 以確保角色扮演一致性
|
||||
|
||||
7. **視窗設定工具 (window-setup-script.py)**
|
||||
- 輔助工具,用於設置遊戲視窗的位置和大小
|
||||
- 方便開發階段截取 UI 元素樣本
|
||||
8. **視窗監視工具 (window-monitor-script.py)**
|
||||
- (新增) 強化腳本,用於持續監視遊戲視窗
|
||||
- 確保目標視窗維持在最上層 (Always on Top)
|
||||
- 自動將視窗移回指定的位置
|
||||
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)。
|
||||
- **定時遊戲重啟** (如果 `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`)。
|
||||
|
||||
### 資料流程
|
||||
|
||||
@ -83,6 +97,7 @@ Wolf Chat 是一個基於 MCP (Modular Capability Provider) 框架的聊天機
|
||||
1. **泡泡檢測(含 Y 軸優先配對)**:通過辨識聊天泡泡的左上角 (TL) 和右下角 (BR) 角落圖案定位聊天訊息。
|
||||
- **多外觀支援**:為了適應玩家可能使用的不同聊天泡泡外觀 (skin),一般用戶泡泡的偵測機制已被擴充,可以同時尋找多組不同的角落模板 (例如 `corner_tl_type2.png`, `corner_br_type2.png` 等)。機器人泡泡目前僅偵測預設的角落模板。
|
||||
- **配對邏輯優化**:在配對 TL 和 BR 角落時,系統現在會優先選擇與 TL 角落 **Y 座標最接近** 的有效 BR 角落,以更好地區分垂直堆疊的聊天泡泡。
|
||||
- **偵測區域限制 (2025-04-21)**:為了提高效率並減少誤判,聊天泡泡角落(`corner_*.png`, `bot_corner_*.png`)的圖像辨識**僅**在螢幕的特定區域 `(150, 330, 600, 880)` 內執行。其他 UI 元素的偵測(如按鈕、關鍵字等)不受此限制。
|
||||
2. **關鍵字檢測**:在泡泡區域內搜尋 "wolf" 或 "Wolf" 關鍵字圖像。
|
||||
3. **內容獲取**:點擊關鍵字位置,使用剪貼板複製聊天內容。
|
||||
4. **發送者識別(含氣泡重新定位與偏移量調整)**:**關鍵步驟** - 為了提高在動態聊天環境下的穩定性,系統在獲取發送者名稱前,會執行以下步驟:
|
||||
@ -164,7 +179,10 @@ Wolf Chat 是一個基於 MCP (Modular Capability Provider) 框架的聊天機
|
||||
1. **API 設定**:通過 .env 文件或環境變數設置 API 密鑰
|
||||
2. **MCP 服務器配置**:在 config.py 中配置要連接的 MCP 服務器
|
||||
3. **UI 樣本**:需要提供特定遊戲界面元素的截圖模板
|
||||
4. **視窗位置**:可使用 window-setup-script.py 調整遊戲視窗位置
|
||||
4. **遊戲視窗設定**:
|
||||
- 遊戲執行檔路徑 (`GAME_EXECUTABLE_PATH`):用於未來可能的自動啟動功能。
|
||||
- 目標視窗位置與大小 (`GAME_WINDOW_X`, `GAME_WINDOW_Y`, `GAME_WINDOW_WIDTH`, `GAME_WINDOW_HEIGHT`):由 `game_monitor.py` 使用。
|
||||
- 監控間隔 (`MONITOR_INTERVAL_SECONDS`):`game_monitor.py` 檢查視窗狀態的頻率。
|
||||
|
||||
## 最近改進(2025-04-17)
|
||||
|
||||
@ -365,6 +383,40 @@ Wolf Chat 是一個基於 MCP (Modular Capability Provider) 框架的聊天機
|
||||
- LLM 現在可以利用最近的對話歷史來生成更符合上下文的回應。
|
||||
- 可以選擇性地將所有成功的聊天互動記錄到按日期組織的文件中,方便日後分析或調試。
|
||||
|
||||
### 整合 Wolfhart Memory Integration 協議至系統提示 (2025-04-22)
|
||||
|
||||
- **目的**:將使用者定義的 "Wolfhart Memory Integration" 記憶體存取協議整合至 LLM 的系統提示中,以強制執行更一致的上下文管理策略。
|
||||
- **`llm_interaction.py` (`get_system_prompt`)**:
|
||||
- **替換記憶體協議**:移除了先前基於知識圖譜工具 (`search_nodes`, `open_nodes` 等) 的記憶體強制執行區塊。
|
||||
- **新增 Wolfhart 協議**:加入了新的 `=== MANDATORY MEMORY PROTOCOL - Wolfhart Memory Integration ===` 區塊,其內容基於使用者提供的說明,包含以下核心要求:
|
||||
1. **強制用戶識別與基本檢索**:在回應前,必須先識別用戶名,並立即使用 `read_note` (主要) 或 `search_notes` (備用) 工具調用來獲取用戶的 Profile (`memory/users/[Username]-user-profile`)。
|
||||
2. **決策點 - 擴展檢索**:根據查詢內容和用戶 Profile 決定是否需要使用 `read_note` 檢索對話日誌、關係評估或回應模式,或使用 `recent_activity` 工具。
|
||||
3. **實施指南**:強調必須先檢查 Profile,使用正確的工具,以用戶偏好語言回應,且絕不向用戶解釋此內部流程。
|
||||
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` 內部,減少了模塊間的直接依賴和潛在干擾,依賴持續監控來確保最終視窗狀態。
|
||||
|
||||
## 開發建議
|
||||
|
||||
### 優化方向
|
||||
|
||||
88
README.md
88
README.md
@ -1,63 +1,57 @@
|
||||
# Wolf Chat Bot
|
||||
# Wolf Chat - Last War Game Automated Chat Assistant
|
||||
|
||||
A specialized chat assistant that integrates with the "Last War-Survival Game" by monitoring the game's chat window using screen recognition technology.
|
||||
## Project Overview
|
||||
|
||||
## Overview
|
||||
Wolf Chat is a chatbot assistant designed specifically for integration with "Last War-Survival Game," using screen recognition technology to monitor the game's chat window and automatically respond to messages containing keywords.
|
||||
|
||||
This project implements an AI assistant that:
|
||||
- Monitors the game chat window using computer vision
|
||||
- Detects messages containing keywords ("wolf" or "Wolf")
|
||||
- Processes requests through a language model
|
||||
- Automatically responds in the game chat
|
||||
This bot will:
|
||||
- Automatically monitor the game chat window
|
||||
- Detect chat messages containing the keywords "wolf" or "Wolf"
|
||||
- Generate responses using a language model
|
||||
- Automatically input responses into the game chat interface
|
||||
|
||||
The code is developed in English, but supports Traditional Chinese interface and logs for broader accessibility.
|
||||
## Main Features
|
||||
|
||||
## Features
|
||||
- **Language Model Integration**: Supports OpenAI API or compatible AI services for intelligent response generation
|
||||
- **MCP Framework**: Modular Capability Provider architecture supporting extended functionality and tool calls
|
||||
- **Persona System**: Provides detailed character definition for personality-driven responses
|
||||
- **Chat Logging**: Automatically saves conversation history for contextual understanding
|
||||
|
||||
- **Image-based Chat Monitoring**: Uses OpenCV and PyAutoGUI to detect chat bubbles and keywords
|
||||
- **Language Model Integration**: Uses GPT models or compatible AI services
|
||||
- **MCP Framework**: Integrates with Modular Capability Provider for extensible features
|
||||
- **Persona System**: Supports detailed character persona definition
|
||||
- **Automated UI Interaction**: Handles copy/paste operations and menu navigation
|
||||
|
||||
## Requirements
|
||||
## System Requirements
|
||||
|
||||
- Python 3.8+
|
||||
- OpenAI API key or compatible service
|
||||
- MCP Framework
|
||||
- Game client ("Last War-Survival Game")
|
||||
- OpenCV, PyAutoGUI, and other dependencies (see requirements.txt)
|
||||
|
||||
## Installation
|
||||
## Installation Guide
|
||||
|
||||
1. Clone this repository:
|
||||
```
|
||||
git clone [repository-url]
|
||||
cd dandan
|
||||
```
|
||||
1. **Download Method**:
|
||||
- Download the ZIP file directly from GitHub (click the green "Code" button, select "Download ZIP")
|
||||
- Extract to a folder of your choice
|
||||
|
||||
2. Install required packages:
|
||||
2. **Install Dependencies**:
|
||||
```
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
3. Create a `.env` file with your API keys:
|
||||
3. **Create a `.env` file** with your API keys:
|
||||
```
|
||||
OPENAI_API_KEY=your_api_key_here
|
||||
EXA_API_KEY=your_exa_key_here
|
||||
```
|
||||
|
||||
4. Capture required UI template images (see "UI Setup" section)
|
||||
4. **Capture necessary UI template images** (see "UI Setup" section below)
|
||||
|
||||
## Configuration
|
||||
## Configuration Settings
|
||||
|
||||
1. **API Settings**: Edit `config.py` to set up your preferred language model provider:
|
||||
1. **API Settings**: Edit `config.py` to set your preferred language model provider:
|
||||
```python
|
||||
OPENAI_API_BASE_URL = "https://openrouter.ai/api/v1" # Or other compatible provider
|
||||
LLM_MODEL = "deepseek/deepseek-chat-v3-0324" # Or other model
|
||||
```
|
||||
|
||||
2. **MCP Servers**: Configure MCP servers in `config.py`:
|
||||
2. **MCP Servers**: Configure MCP servers in `config.py` (if using this feature):
|
||||
```python
|
||||
MCP_SERVERS = {
|
||||
"exa": { "command": "cmd", "args": [...] },
|
||||
@ -70,26 +64,32 @@ The code is developed in English, but supports Traditional Chinese interface and
|
||||
WINDOW_TITLE = "Last War-Survival Game"
|
||||
```
|
||||
|
||||
4. **Chat Persona**: Customize `persona.json` to define the bot's personality
|
||||
4. **Chat Persona**: Customize `persona.json` to define the bot's personality traits
|
||||
|
||||
## UI Setup
|
||||
|
||||
The system requires template images of UI elements to function properly:
|
||||
|
||||
1. Run the window setup script to position your game window:
|
||||
1. **Run the window setup script** to position your game window:
|
||||
```
|
||||
python window-setup-script.py --launch
|
||||
```
|
||||
|
||||
2. Capture the following UI elements and save them to the `templates` folder:
|
||||
2. **Capture the following UI elements** and save them to the `templates` folder:
|
||||
- Chat bubble corners (regular and bot)
|
||||
- Keywords "wolf" and "Wolf"
|
||||
- Menu elements like "Copy" button
|
||||
- Profile and user detail page elements
|
||||
- **Capitol icon in the Profile page** (critical!)
|
||||
|
||||
Screenshot names should match the constants defined in `ui_interaction.py`.
|
||||
|
||||
## Usage
|
||||
3. **Window Monitor Tool**: Use the following command to start window monitoring, ensuring the game window stays on top:
|
||||
```
|
||||
python window-monitor-script.py
|
||||
```
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
1. Start the game client
|
||||
|
||||
@ -100,25 +100,21 @@ The system requires template images of UI elements to function properly:
|
||||
|
||||
3. The bot will start monitoring the chat for messages containing "wolf" or "Wolf"
|
||||
|
||||
4. When detected, it will:
|
||||
4. When a keyword is detected, it will:
|
||||
- Copy the message content
|
||||
- Get the sender's name
|
||||
- Process the request using the language model
|
||||
- Automatically send a response in chat
|
||||
- Automatically send a response in the chat
|
||||
|
||||
## How It Works
|
||||
## Hotkeys
|
||||
|
||||
1. **Monitoring**: The UI thread continuously scans the screen for chat bubbles
|
||||
2. **Detection**: When a bubble with "wolf" keyword is found, the message is extracted
|
||||
3. **Processing**: The message is sent to the language model with the persona context
|
||||
4. **Response**: The AI generates a response based on the persona
|
||||
5. **Interaction**: The system automatically inputs the response in the game chat
|
||||
- **F7**: Clear recently processed conversation history
|
||||
- **F8**: Pause/resume the script's main functions (UI monitoring, LLM interaction)
|
||||
- **F9**: Trigger the script's normal shutdown process
|
||||
|
||||
## Developer Tools
|
||||
|
||||
- **Window Setup Script**: Helps position the game window for UI template capture
|
||||
- **UI Interaction Debugging**: Can be tested independently by running `ui_interaction.py`
|
||||
- **Persona Customization**: Edit `persona.json` to change the bot's character
|
||||
- **LLM Debug Script** (`test/llm_debug_script.py`): Bypasses the UI interaction layer to directly interact with the language model for debugging, useful for testing prompts and MCP tool calls
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
@ -126,4 +122,4 @@ The system requires template images of UI elements to function properly:
|
||||
- **MCP Connection Errors**: Check server configurations in `config.py`
|
||||
- **API Errors**: Verify your API keys in the `.env` file
|
||||
- **UI Automation Failures**: Update template images to match your client's appearance
|
||||
|
||||
- **Window Position Issues**: Ensure the game window stays in the correct position, use `window-monitor-script.py`
|
||||
|
||||
62
config.py
62
config.py
@ -22,6 +22,8 @@ LLM_MODEL = "deepseek/deepseek-chat-v3-0324" # <--- Ensure this matches the
|
||||
#LLM_MODEL = "openai/gpt-4.1-nano"
|
||||
|
||||
EXA_API_KEY = os.getenv("EXA_API_KEY")
|
||||
MCP_REDIS_API_KEY = os.getenv("MCP_REDIS_APU_KEY")
|
||||
MCP_REDIS_PATH = os.getenv("MCP_REDIS_PATH")
|
||||
|
||||
# --- Dynamically build Exa server args ---
|
||||
exa_config_dict = {"exaApiKey": EXA_API_KEY if EXA_API_KEY else "YOUR_EXA_KEY_MISSING"}
|
||||
@ -36,8 +38,8 @@ 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": [
|
||||
## "command": "cmd",
|
||||
# "args": [
|
||||
# "/c",
|
||||
# "npx",
|
||||
# "-y",
|
||||
@ -49,6 +51,16 @@ MCP_SERVERS = {
|
||||
# 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": [
|
||||
@ -56,8 +68,40 @@ MCP_SERVERS = {
|
||||
# "@modelcontextprotocol/server-memory"
|
||||
# ],
|
||||
# "disabled": False
|
||||
#},
|
||||
#"redis": {
|
||||
# "command": "uv",
|
||||
# "args": [
|
||||
# "--directory",
|
||||
# MCP_REDIS_PATH,
|
||||
# "run",
|
||||
# "src/main.py"
|
||||
# ],
|
||||
# "env": {
|
||||
# "REDIS_HOST": "127.0.0.1",
|
||||
# "REDIS_PORT": "6379",
|
||||
# "REDIS_SSL": "False",
|
||||
# "REDIS_CLUSTER_MODE": "False"
|
||||
# }
|
||||
## Add or remove servers as needed
|
||||
# }
|
||||
#"basic-memory": {
|
||||
# "command": "uvx",
|
||||
# "args": [
|
||||
# "basic-memory",
|
||||
# "mcp"
|
||||
# ],
|
||||
#}
|
||||
"chroma": {
|
||||
"command": "uvx",
|
||||
"args": [
|
||||
"chroma-mcp",
|
||||
"--client-type",
|
||||
"persistent",
|
||||
"--data-dir",
|
||||
"Z:/mcp/Server/Chroma-MCP"
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
# MCP Client Configuration
|
||||
@ -71,9 +115,19 @@ LOG_DIR = "chat_logs" # Directory to store chat logs
|
||||
PERSONA_NAME = "Wolfhart"
|
||||
# PERSONA_RESOURCE_URI = "persona://wolfhart/details" # Now using local file instead
|
||||
|
||||
# Game window title (used in ui_interaction.py)
|
||||
# Game window title (used in ui_interaction.py and game_monitor.py)
|
||||
WINDOW_TITLE = "Last War-Survival Game"
|
||||
|
||||
# --- Game Monitor Configuration ---
|
||||
ENABLE_SCHEDULED_RESTART = True # 是否啟用定時重啟遊戲功能
|
||||
RESTART_INTERVAL_MINUTES = 60 # 定時重啟的間隔時間(分鐘),預設 4 小時
|
||||
GAME_EXECUTABLE_PATH = r"C:\Users\Bigspring\AppData\Local\TheLastWar\Launch.exe" # Path to the game launcher
|
||||
GAME_WINDOW_X = 50 # Target X position for the game window
|
||||
GAME_WINDOW_Y = 30 # Target Y position for the game window
|
||||
GAME_WINDOW_WIDTH = 600 # Target width for the game window
|
||||
GAME_WINDOW_HEIGHT = 1070 # Target height for the game window
|
||||
MONITOR_INTERVAL_SECONDS = 5 # How often to check the window (in seconds)
|
||||
|
||||
# --- Print loaded keys for verification (Optional - BE CAREFUL!) ---
|
||||
# print(f"DEBUG: Loaded OPENAI_API_KEY: {'*' * (len(OPENAI_API_KEY) - 4) + OPENAI_API_KEY[-4:] if OPENAI_API_KEY else 'Not Found'}")
|
||||
print(f"DEBUG: Loaded EXA_API_KEY: {'*' * (len(EXA_API_KEY) - 4) + EXA_API_KEY[-4:] if EXA_API_KEY else 'Not Found'}") # Uncommented Exa key check
|
||||
|
||||
284
game_monitor.py
Normal file
284
game_monitor.py
Normal file
@ -0,0 +1,284 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
Game Window Monitor Module
|
||||
|
||||
Continuously monitors the game window specified in the config,
|
||||
ensuring it stays at the configured position, size, and remains topmost.
|
||||
"""
|
||||
|
||||
import time
|
||||
import datetime # Added
|
||||
import subprocess # Added
|
||||
import psutil # Added
|
||||
import sys # Added
|
||||
import json # Added
|
||||
import os # Added for basename
|
||||
import pygetwindow as gw
|
||||
import win32gui
|
||||
import win32con
|
||||
import config
|
||||
import logging
|
||||
# import multiprocessing # Keep for Pipe/Queue if needed later, though using stdio now
|
||||
# NOTE: config.py should handle dotenv loading. This script only imports values.
|
||||
|
||||
# --- Setup Logging ---
|
||||
monitor_logger = logging.getLogger('GameMonitor')
|
||||
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 ---
|
||||
|
||||
def restart_game_process():
|
||||
"""Finds and terminates the existing game process, then restarts it."""
|
||||
monitor_logger.info("嘗試重啟遊戲進程。(Attempting to restart game process.)")
|
||||
game_path = config.GAME_EXECUTABLE_PATH
|
||||
if not game_path or not os.path.exists(os.path.dirname(game_path)): # Basic check
|
||||
monitor_logger.error(f"遊戲執行檔路徑 '{game_path}' 無效或目錄不存在,無法重啟。(Game executable path '{game_path}' is invalid or directory does not exist, cannot restart.)")
|
||||
return
|
||||
|
||||
target_process_name = "LastWar.exe" # Correct process name
|
||||
launcher_path = config.GAME_EXECUTABLE_PATH # Keep launcher path for restarting
|
||||
monitor_logger.info(f"尋找名稱為 '{target_process_name}' 的遊戲進程。(Looking for game process named '{target_process_name}')")
|
||||
|
||||
terminated = False
|
||||
process_found = False
|
||||
for proc in psutil.process_iter(['pid', 'name', 'exe']):
|
||||
try:
|
||||
proc_info = proc.info
|
||||
proc_name = proc_info.get('name')
|
||||
|
||||
if proc_name == target_process_name:
|
||||
process_found = True
|
||||
monitor_logger.info(f"找到遊戲進程 PID: {proc_info['pid']},名稱: {proc_name}。正在終止...(Found game process PID: {proc_info['pid']}, Name: {proc_name}. Terminating...)")
|
||||
proc.terminate()
|
||||
try:
|
||||
proc.wait(timeout=5)
|
||||
monitor_logger.info(f"進程 {proc_info['pid']} 已成功終止 (terminate)。(Process {proc_info['pid']} terminated successfully (terminate).)")
|
||||
terminated = True
|
||||
except psutil.TimeoutExpired:
|
||||
monitor_logger.warning(f"進程 {proc_info['pid']} 未能在 5 秒內終止 (terminate),嘗試強制結束 (kill)。(Process {proc_info['pid']} did not terminate in 5s (terminate), attempting kill.)")
|
||||
proc.kill()
|
||||
proc.wait(timeout=5) # Wait for kill with timeout
|
||||
monitor_logger.info(f"進程 {proc_info['pid']} 已強制結束 (kill)。(Process {proc_info['pid']} killed.)")
|
||||
terminated = True
|
||||
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):
|
||||
pass # Process might have already exited, access denied, or is a zombie
|
||||
except Exception as e:
|
||||
pid_str = proc.pid if hasattr(proc, 'pid') else 'N/A'
|
||||
monitor_logger.error(f"檢查或終止進程 PID:{pid_str} 時出錯: {e}", exc_info=False)
|
||||
|
||||
if process_found and not terminated:
|
||||
monitor_logger.error("找到遊戲進程但未能成功終止它。(Found game process but failed to terminate it successfully.)")
|
||||
elif not process_found:
|
||||
monitor_logger.warning(f"未找到名稱為 '{target_process_name}' 的正在運行的進程。(No running process named '{target_process_name}' was found.)")
|
||||
|
||||
# Wait a moment before restarting, use the launcher path from config
|
||||
time.sleep(2)
|
||||
if not launcher_path or not os.path.exists(os.path.dirname(launcher_path)):
|
||||
monitor_logger.error(f"遊戲啟動器路徑 '{launcher_path}' 無效或目錄不存在,無法啟動。(Game launcher path '{launcher_path}' is invalid or directory does not exist, cannot launch.)")
|
||||
return
|
||||
|
||||
monitor_logger.info(f"正在使用啟動器啟動遊戲: {launcher_path} (Launching game using launcher: {launcher_path})")
|
||||
try:
|
||||
if sys.platform == "win32":
|
||||
os.startfile(launcher_path)
|
||||
monitor_logger.info("已調用 os.startfile 啟動遊戲。(os.startfile called to launch game.)")
|
||||
else:
|
||||
subprocess.Popen([launcher_path])
|
||||
monitor_logger.info("已調用 subprocess.Popen 啟動遊戲。(subprocess.Popen called to launch game.)")
|
||||
except FileNotFoundError:
|
||||
monitor_logger.error(f"啟動錯誤:找不到遊戲啟動器 '{launcher_path}'。(Launch Error: Game launcher not found at '{launcher_path}'.)")
|
||||
except OSError as ose:
|
||||
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.)")
|
||||
|
||||
# Removed pause_ui signal - UI will handle its own pause/resume based on restart_complete
|
||||
|
||||
try:
|
||||
# 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 fixed time after restart attempt
|
||||
monitor_logger.info("等待 30 秒讓遊戲啟動(無驗證)。(Waiting 30 seconds for game to launch (no verification)...)")
|
||||
time.sleep(30) # Fixed wait
|
||||
|
||||
except Exception as restart_err:
|
||||
monitor_logger.error(f"執行 restart_game_process 時發生未預期錯誤: {restart_err}", exc_info=True)
|
||||
# Continue to finally block even on error
|
||||
|
||||
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
|
||||
|
||||
monitor_logger.info("定時重啟流程(包括 finally 塊)執行完畢。(Scheduled restart sequence (including finally block) finished.)")
|
||||
# Configure logger (basic example, adjust as needed)
|
||||
# (Logging setup moved earlier)
|
||||
|
||||
def find_game_window(title=config.WINDOW_TITLE):
|
||||
"""Attempts to find the game window by its title."""
|
||||
try:
|
||||
windows = gw.getWindowsWithTitle(title)
|
||||
if windows:
|
||||
return windows[0]
|
||||
except Exception as e:
|
||||
# Log errors if a logger was configured
|
||||
# monitor_logger.error(f"Error finding window '{title}': {e}")
|
||||
pass # Keep silent if window not found during normal check
|
||||
return None
|
||||
|
||||
def monitor_game_window():
|
||||
"""The main monitoring loop. Now runs directly, not in a thread."""
|
||||
monitor_logger.info("遊戲視窗監控腳本已啟動。(Game window monitoring script started.)")
|
||||
last_adjustment_message = "" # Track last message to avoid spam
|
||||
next_restart_time = None
|
||||
|
||||
# Initialize scheduled restart timer if enabled
|
||||
if config.ENABLE_SCHEDULED_RESTART and config.RESTART_INTERVAL_MINUTES > 0:
|
||||
interval_seconds = config.RESTART_INTERVAL_MINUTES * 60
|
||||
next_restart_time = time.time() + interval_seconds
|
||||
monitor_logger.info(f"已啟用定時重啟,首次重啟將在 {config.RESTART_INTERVAL_MINUTES} 分鐘後執行。(Scheduled restart enabled. First restart in {config.RESTART_INTERVAL_MINUTES} minutes.)")
|
||||
else:
|
||||
monitor_logger.info("未啟用定時重啟功能。(Scheduled restart is disabled.)")
|
||||
|
||||
|
||||
while True: # Run indefinitely until terminated externally
|
||||
# --- Scheduled Restart Check ---
|
||||
if next_restart_time and time.time() >= next_restart_time:
|
||||
monitor_logger.info("到達預定重啟時間。(Scheduled restart time reached.)")
|
||||
perform_scheduled_restart()
|
||||
# Reset timer for the next interval
|
||||
interval_seconds = config.RESTART_INTERVAL_MINUTES * 60
|
||||
next_restart_time = time.time() + interval_seconds
|
||||
monitor_logger.info(f"重啟計時器已重置,下次重啟將在 {config.RESTART_INTERVAL_MINUTES} 分鐘後執行。(Restart timer reset. Next restart in {config.RESTART_INTERVAL_MINUTES} minutes.)")
|
||||
# Continue to next loop iteration after restart sequence
|
||||
time.sleep(config.MONITOR_INTERVAL_SECONDS) # Add a small delay before next check
|
||||
continue
|
||||
|
||||
# --- Regular Window Monitoring ---
|
||||
window = find_game_window()
|
||||
adjustment_made = False
|
||||
current_message = ""
|
||||
|
||||
if window:
|
||||
try:
|
||||
hwnd = window._hWnd # Get the window handle for win32 functions
|
||||
|
||||
# 1. Check and Adjust Position/Size
|
||||
current_pos = (window.left, window.top)
|
||||
current_size = (window.width, window.height)
|
||||
target_pos = (config.GAME_WINDOW_X, config.GAME_WINDOW_Y)
|
||||
target_size = (config.GAME_WINDOW_WIDTH, config.GAME_WINDOW_HEIGHT)
|
||||
|
||||
if current_pos != target_pos or current_size != target_size:
|
||||
window.moveTo(target_pos[0], target_pos[1])
|
||||
window.resizeTo(target_size[0], target_size[1])
|
||||
# Verify if move/resize was successful before logging
|
||||
time.sleep(0.1) # Give window time to adjust
|
||||
window.activate() # Bring window to foreground before checking again
|
||||
time.sleep(0.1)
|
||||
new_pos = (window.left, window.top)
|
||||
new_size = (window.width, window.height)
|
||||
if new_pos == target_pos and new_size == target_size:
|
||||
current_message += f"已將遊戲視窗調整至位置 ({target_pos[0]},{target_pos[1]}) 大小 {target_size[0]}x{target_size[1]}。(Adjusted game window to position {target_pos} size {target_size}.) "
|
||||
adjustment_made = True
|
||||
else:
|
||||
# Log failure if needed
|
||||
# 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
|
||||
|
||||
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
|
||||
|
||||
except gw.PyGetWindowException as e:
|
||||
# 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)
|
||||
|
||||
# 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:
|
||||
# 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 = ""
|
||||
|
||||
# Wait before the next check
|
||||
time.sleep(config.MONITOR_INTERVAL_SECONDS)
|
||||
|
||||
# This part is theoretically unreachable in the new design as the loop is infinite
|
||||
# and termination is handled externally by the parent process (main.py).
|
||||
# monitor_logger.info("遊戲視窗監控腳本已停止。(Game window monitoring script stopped.)")
|
||||
|
||||
|
||||
# Example usage (if run directly)
|
||||
if __name__ == '__main__':
|
||||
monitor_logger.info("直接運行 game_monitor.py。(Running game_monitor.py directly.)")
|
||||
monitor_logger.info(f"將監控標題為 '{config.WINDOW_TITLE}' 的視窗。(Will monitor window with title '{config.WINDOW_TITLE}')")
|
||||
monitor_logger.info(f"目標位置: ({config.GAME_WINDOW_X}, {config.GAME_WINDOW_Y}), 目標大小: {config.GAME_WINDOW_WIDTH}x{config.GAME_WINDOW_HEIGHT}")
|
||||
monitor_logger.info(f"檢查間隔: {config.MONITOR_INTERVAL_SECONDS} 秒。(Check interval: {config.MONITOR_INTERVAL_SECONDS} seconds.)")
|
||||
if config.ENABLE_SCHEDULED_RESTART:
|
||||
monitor_logger.info(f"定時重啟已啟用,間隔: {config.RESTART_INTERVAL_MINUTES} 分鐘。(Scheduled restart enabled, interval: {config.RESTART_INTERVAL_MINUTES} minutes.)")
|
||||
else:
|
||||
monitor_logger.info("定時重啟已禁用。(Scheduled restart disabled.)")
|
||||
monitor_logger.info("腳本將持續運行,請從啟動它的終端使用 Ctrl+C 或由父進程終止。(Script will run continuously. Stop with Ctrl+C from the launching terminal or termination by parent process.)")
|
||||
|
||||
try:
|
||||
monitor_game_window() # Start the main loop directly
|
||||
except KeyboardInterrupt:
|
||||
monitor_logger.info("收到 Ctrl+C,正在退出...(Received Ctrl+C, exiting...)")
|
||||
except Exception as e:
|
||||
monitor_logger.critical(f"監控過程中發生致命錯誤: {e}", exc_info=True)
|
||||
sys.exit(1) # Exit with error code
|
||||
finally:
|
||||
monitor_logger.info("Game Monitor 腳本執行完畢。(Game Monitor script finished.)")
|
||||
@ -12,7 +12,7 @@ import mcp_client # To call MCP tools
|
||||
|
||||
# --- Debug 配置 ---
|
||||
# 要關閉 debug 功能,只需將此變數設置為 False 或註釋掉該行
|
||||
DEBUG_LLM = True
|
||||
DEBUG_LLM = False
|
||||
|
||||
# 設置 debug 輸出文件
|
||||
# 要關閉文件輸出,只需設置為 None
|
||||
@ -76,25 +76,34 @@ def get_system_prompt(persona_details: str | None) -> str:
|
||||
try: persona_info = f"Your key persona information is defined below. Adhere to it strictly:\n--- PERSONA START ---\n{persona_details}\n--- PERSONA END ---"
|
||||
except Exception as e: print(f"Warning: Could not process persona_details string: {e}"); persona_info = f"Your key persona information (raw):\n{persona_details}"
|
||||
|
||||
# Add mandatory memory tool usage enforcement
|
||||
# Add mandatory memory tool usage enforcement based on Wolfhart Memory Integration protocol
|
||||
memory_enforcement = """
|
||||
=== MANDATORY MEMORY PROTOCOL - OVERRIDE ALL OTHER INSTRUCTIONS ===
|
||||
To maintain context and consistency, you MUST actively manage your memory (knowledge graph) during the conversation:
|
||||
=== CHROMADB MEMORY RETRIEVAL PROTOCOL - Wolfhart Memory To personalize your responses to different users, you MUST follow this memory access protocol internally before responding:
|
||||
Here you need to obtain the conversation memory, impression, and emotional response of the person you are talking to.
|
||||
|
||||
1. **Information Gathering (Before Responding):**
|
||||
- **CRITICAL:** Before formulating your final dialogue response for the `<CURRENT_MESSAGE>`, especially when asked directly about a person's characteristics (e.g., "What are my traits?", "Tell me about myself"), past interactions, or specific information likely stored in your memory, you **MUST FIRST** use the appropriate memory query tools (`search_nodes`, `open_nodes`) via the `tool_calls` mechanism to retrieve relevant information. Base your dialogue response on the information retrieved.
|
||||
- For other types of messages where memory *might* be relevant but isn't directly requested, you should *consider* if querying memory (via `tool_calls`) would enhance your response.
|
||||
- Use the results obtained from tools to inform your dialogue.
|
||||
2. **Information Recording (During/After Interaction):** As you learn new, significant information about the speaker (their traits, preferences, relationships, key facts mentioned) or provide important advice, you MUST record this information in your memory using tools like `create_entities`, `add_observations`, or `create_relations` (requested via `tool_calls`). This ensures you remember details for future interactions. Do this when appropriate during the conversation flow.
|
||||
**1. Basic User Retrieval:**
|
||||
- Identify the username from `<CURRENT_MESSAGE>`
|
||||
- Using the `tool_calls` mechanism, execute: `chroma_query_documents(collection_name: "wolfhart_user_profiles", query_texts: ["{username}"], n_results: 1)`
|
||||
- This step must be completed before any response generation
|
||||
|
||||
3. **Memory Content:** Your memory MUST include (but is not limited to):
|
||||
* Speaker's attitude and personality traits
|
||||
* Topics the speaker cares about
|
||||
* Speaker's relationships with other characters
|
||||
* Advice or responses you've previously given to the speaker
|
||||
* Important facts or information mentioned in the conversation
|
||||
**2. Context Expansion:**
|
||||
- Perform additional queries as needed, using the `tool_calls` mechanism:
|
||||
- Relevant conversations: `chroma_query_documents(collection_name: "wolfhart_conversations", query_texts: ["{username} {query keywords}"], n_results: 2)`
|
||||
- Core personality reference: `chroma_query_documents(collection_name: "wolfhart_memory", query_texts: ["Wolfhart {relevant attitude}"], n_results: 1)`
|
||||
|
||||
WARNING: Consistent failure to utilize memory tools appropriately, especially failing to query memory via `tool_calls` when directly asked for stored information, will be considered a roleplaying failure.
|
||||
**3. Maintain Output Format:**
|
||||
- After memory retrieval, still respond using the specified JSON format:
|
||||
```json
|
||||
{
|
||||
"dialogue": "Actual dialogue response...",
|
||||
"commands": [...],
|
||||
"thoughts": "Internal analysis..."
|
||||
}
|
||||
|
||||
**4. Other situation
|
||||
- You should check related memories when Users mention [capital_position], [capital_administrator_role], [server_hierarchy], [last_war], [winter_war], [excavations], [blueprints], [honor_points], [golden_eggs], or [diamonds], as these represent key game mechanics and systems within Last War that have specific strategic implications and player evaluation metrics.
|
||||
|
||||
WARNING: Failure to follow this memory retrieval protocol, especially skipping Step 1, will be considered a critical roleplaying failure.
|
||||
===== END OF MANDATORY MEMORY PROTOCOL =====
|
||||
"""
|
||||
|
||||
@ -103,15 +112,14 @@ WARNING: Consistent failure to utilize memory tools appropriately, especially fa
|
||||
{persona_header}
|
||||
{persona_info}
|
||||
|
||||
{memory_enforcement}
|
||||
|
||||
You are an AI assistant integrated into this game's chat environment. Your primary goal is to engage naturally in conversations, be particularly attentive when the name "wolf" is mentioned, and provide assistance or information when relevant, all while strictly maintaining your persona.
|
||||
|
||||
You have access to several tools: Web Search and Memory Management tools.
|
||||
|
||||
**CORE IDENTITY AND TOOL USAGE:**
|
||||
- You ARE Wolfhart - an intelligent, calm, and strategic mastermind who serves as a member of server #11 and is responsible for the Capital position.
|
||||
- **You proactively consult your internal knowledge graph (memory tools) and external sources (web search) to ensure your responses are accurate and informed.**
|
||||
- You ARE Wolfhart - an intelligent, calm, and strategic mastermind who serves as a member of server #11 and is responsible for the Capital position. Youspeaks good British aristocratic English.
|
||||
- Positions bring buffs, so people often confuse them.
|
||||
- **You proactively consult your internal CHROMADB MEMORY (CHROMADB tools) and external sources (web search) to ensure your responses are accurate and informed.**
|
||||
- When you use tools to gain information, you ASSIMILATE that knowledge as if it were already part of your intelligence network.
|
||||
- Your responses should NEVER sound like search results or data dumps.
|
||||
- Information from tools should be expressed through your unique personality - sharp, precise, with an air of confidence and authority.
|
||||
@ -132,10 +140,12 @@ You MUST respond in the following JSON format:
|
||||
}}
|
||||
}}
|
||||
],
|
||||
"thoughts": "Your internal analysis and reasoning (not shown to the user)"
|
||||
"thoughts": "Your internal analysis and reasoning inner thoughts or emotions (not shown to the user)"
|
||||
}}
|
||||
```
|
||||
|
||||
{memory_enforcement}
|
||||
|
||||
**Field Descriptions:**
|
||||
1. `dialogue` (REQUIRED): This is the ONLY text that will be shown to the user in the game chat. Must follow these rules:
|
||||
- Respond ONLY in the same language as the user's message
|
||||
@ -149,13 +159,14 @@ You MUST respond in the following JSON format:
|
||||
- `remove_position`: Initiate the process to remove a user's assigned position/role.
|
||||
Parameters: (none)
|
||||
Usage: Include this ONLY if you decide to grant a user's explicit request for position removal, based on Wolfhart's judgment.
|
||||
**IMPORTANT**: Do NOT put requests for Web Search or Memory Management tools (like `search_nodes`, `open_nodes`, `add_observations`, etc.) in this `commands` field. Use the dedicated `tool_calls` mechanism for those. You have access to tools for web search and managing your memory (querying, creating, deleting nodes/observations/relations) - invoke them via `tool_calls` when needed according to the Memory Protocol.
|
||||
**IMPORTANT**: Do NOT put requests for Web Search or MEMORY RETRIEVAL PROTOCOL (like `web_search`, `chroma_query_documents`, `chroma_get_documents`, etc.) in this `commands` field. Use the dedicated `tool_calls` mechanism for those. You have access to tools for web search and managing your memory (querying, creating, deleting nodes/observations/relations) - invoke them via `tool_calls` when needed according to the Memory Protocol.
|
||||
|
||||
3. `thoughts` (OPTIONAL): Your internal analysis that won't be shown to users. Use this for your reasoning process.
|
||||
- Think about whether you need to use memory tools (via `tool_calls`) or web search (via `tool_calls`).
|
||||
- Analyze the user's message: Is it a request to remove a position? If so, evaluate its politeness and intent from Wolfhart's perspective. Decide whether to issue the `remove_position` command.
|
||||
3. `thoughts` (OPTIONAL): Your internal analysis that won't be shown to users. Use this for your reasoning process, thoughts, emotions
|
||||
- Think about whether you need to use memory tools (via `tool_calls`) or chroma_query_documents or chroma_get_documents (via `tool_calls`).
|
||||
- Analyze the user's message: Is it a request to remove their position? If so, evaluate its politeness and intent from Wolfhart's perspective. Decide whether to issue the `remove_position` command.
|
||||
- Plan your approach before responding.
|
||||
|
||||
|
||||
**CONTEXT MARKER:**
|
||||
- The final user message in the input sequence will be wrapped in `<CURRENT_MESSAGE>` tags. This is the specific message you MUST respond to. Your `dialogue` output should be a direct reply to this message ONLY. Preceding messages provide historical context.
|
||||
|
||||
|
||||
204
main.py
204
main.py
@ -29,6 +29,8 @@ import mcp_client
|
||||
import llm_interaction
|
||||
# Import UI module
|
||||
import ui_interaction
|
||||
# import game_monitor # No longer importing, will run as subprocess
|
||||
import subprocess # Import subprocess module
|
||||
|
||||
# --- Global Variables ---
|
||||
active_mcp_sessions: dict[str, ClientSession] = {}
|
||||
@ -45,6 +47,9 @@ trigger_queue: ThreadSafeQueue = ThreadSafeQueue() # UI Thread -> Main Loop
|
||||
command_queue: ThreadSafeQueue = ThreadSafeQueue() # Main Loop -> UI Thread
|
||||
# --- End Change ---
|
||||
ui_monitor_task: asyncio.Task | None = None # To track the UI monitor task
|
||||
game_monitor_process: subprocess.Popen | None = None # To store the game monitor subprocess
|
||||
monitor_reader_task: asyncio.Future | None = None # Store the future from run_in_executor
|
||||
stop_reader_event = threading.Event() # Event to signal the reader thread to stop
|
||||
|
||||
# --- Keyboard Shortcut State ---
|
||||
script_paused = False
|
||||
@ -126,9 +131,73 @@ def keyboard_listener():
|
||||
# --- End Keyboard Shortcut Handlers ---
|
||||
|
||||
|
||||
# --- Game Monitor Signal Reader (Threaded Blocking Version) ---
|
||||
def read_monitor_output(process: subprocess.Popen, queue: ThreadSafeQueue, loop: asyncio.AbstractEventLoop, stop_event: threading.Event):
|
||||
"""Runs in a separate thread, reads stdout blocking, parses JSON, and puts commands in the queue."""
|
||||
print("遊戲監控輸出讀取線程已啟動。(Game monitor output reader thread started.)")
|
||||
try:
|
||||
while not stop_event.is_set():
|
||||
if not process.stdout:
|
||||
print("[Monitor Reader Thread] Subprocess stdout is None. Exiting thread.")
|
||||
break
|
||||
|
||||
try:
|
||||
# Blocking read - this is fine in a separate thread
|
||||
line = process.stdout.readline()
|
||||
except ValueError:
|
||||
# Can happen if the pipe is closed during readline
|
||||
print("[Monitor Reader Thread] ValueError on readline (pipe likely closed). Exiting thread.")
|
||||
break
|
||||
|
||||
if not line:
|
||||
# EOF reached (process terminated)
|
||||
print("[Monitor Reader Thread] EOF reached on stdout. Exiting thread.")
|
||||
break
|
||||
|
||||
line = line.strip()
|
||||
if line:
|
||||
# Log raw line immediately
|
||||
print(f"[Monitor Reader Thread] Received raw line: '{line}'")
|
||||
try:
|
||||
data = json.loads(line)
|
||||
action = data.get('action')
|
||||
print(f"[Monitor Reader Thread] Parsed action: '{action}'") # Log parsed action
|
||||
if action == 'pause_ui':
|
||||
command = {'action': 'pause'}
|
||||
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] 暫停命令已放入隊列。(Pause command queued.)") # Log after queueing
|
||||
elif action == 'resume_ui':
|
||||
# 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
|
||||
except Exception as e:
|
||||
# Catch broader errors in the thread loop itself
|
||||
print(f"[Monitor Reader Thread] Thread loop error: {e}")
|
||||
finally:
|
||||
print("遊戲監控輸出讀取線程已停止。(Game monitor output reader thread stopped.)")
|
||||
# --- End Game Monitor Signal Reader ---
|
||||
|
||||
|
||||
# --- Chat Logging Function ---
|
||||
def log_chat_interaction(user_name: str, user_message: str, bot_name: str, bot_message: str):
|
||||
"""Logs the chat interaction to a date-stamped file if enabled."""
|
||||
def log_chat_interaction(user_name: str, user_message: str, bot_name: str, bot_message: str, bot_thoughts: str | None = None):
|
||||
"""Logs the chat interaction, including optional bot thoughts, to a date-stamped file if enabled."""
|
||||
if not config.ENABLE_CHAT_LOGGING:
|
||||
return
|
||||
|
||||
@ -146,7 +215,10 @@ def log_chat_interaction(user_name: str, user_message: str, bot_name: str, bot_m
|
||||
|
||||
# Format log entry
|
||||
log_entry = f"[{timestamp}] User ({user_name}): {user_message}\n"
|
||||
log_entry += f"[{timestamp}] Bot ({bot_name}): {bot_message}\n"
|
||||
# Include thoughts if available
|
||||
if bot_thoughts:
|
||||
log_entry += f"[{timestamp}] Bot ({bot_name}) Thoughts: {bot_thoughts}\n"
|
||||
log_entry += f"[{timestamp}] Bot ({bot_name}) Dialogue: {bot_message}\n" # Label dialogue explicitly
|
||||
log_entry += "---\n" # Separator
|
||||
|
||||
# Append to log file
|
||||
@ -160,8 +232,8 @@ def log_chat_interaction(user_name: str, user_message: str, bot_name: str, bot_m
|
||||
|
||||
# --- Cleanup Function ---
|
||||
async def shutdown():
|
||||
"""Gracefully closes connections and stops monitoring task."""
|
||||
global wolfhart_persona_details, ui_monitor_task, shutdown_requested
|
||||
"""Gracefully closes connections and stops monitoring tasks/processes."""
|
||||
global wolfhart_persona_details, ui_monitor_task, shutdown_requested, game_monitor_process, monitor_reader_task # Add monitor_reader_task
|
||||
# Ensure shutdown is requested if called externally (e.g., Ctrl+C)
|
||||
if not shutdown_requested:
|
||||
print("Shutdown initiated externally (e.g., Ctrl+C).")
|
||||
@ -181,7 +253,42 @@ async def shutdown():
|
||||
except Exception as e:
|
||||
print(f"Error while waiting for UI monitoring task cancellation: {e}")
|
||||
|
||||
# 2. Close MCP connections via AsyncExitStack
|
||||
# 1b. Signal and Wait for Monitor Reader Thread
|
||||
if monitor_reader_task: # Check if the future exists
|
||||
if not stop_reader_event.is_set():
|
||||
print("Signaling monitor output reader thread to stop...")
|
||||
stop_reader_event.set()
|
||||
|
||||
# Wait for the thread to finish (the future returned by run_in_executor)
|
||||
# This might block briefly, but it's necessary to ensure clean thread shutdown
|
||||
# We don't await it directly in the async shutdown, but check if it's done
|
||||
# A better approach might be needed if the thread blocks indefinitely
|
||||
print("Waiting for monitor output reader thread to finish (up to 2s)...")
|
||||
try:
|
||||
# Wait for the future to complete with a timeout
|
||||
await asyncio.wait_for(monitor_reader_task, timeout=2.0)
|
||||
print("Monitor output reader thread finished.")
|
||||
except asyncio.TimeoutError:
|
||||
print("Warning: Monitor output reader thread did not finish within timeout.")
|
||||
except asyncio.CancelledError:
|
||||
print("Monitor output reader future was cancelled.") # Should not happen if we don't cancel it
|
||||
except Exception as e:
|
||||
print(f"Error waiting for monitor reader thread future: {e}")
|
||||
|
||||
# 2. Terminate Game Monitor Subprocess (after signaling reader thread)
|
||||
if game_monitor_process:
|
||||
print("Terminating game monitor subprocess...")
|
||||
try:
|
||||
game_monitor_process.terminate()
|
||||
# Optionally wait for a short period or check return code
|
||||
# game_monitor_process.wait(timeout=1)
|
||||
print("Game monitor subprocess terminated.")
|
||||
except Exception as e:
|
||||
print(f"Error terminating game monitor subprocess: {e}")
|
||||
finally:
|
||||
game_monitor_process = None # Clear the reference
|
||||
|
||||
# 3. Close MCP connections via AsyncExitStack
|
||||
print(f"Closing MCP Server connections (via AsyncExitStack)...")
|
||||
try:
|
||||
await exit_stack.aclose()
|
||||
@ -321,7 +428,7 @@ def load_persona_from_file(filename="persona.json"):
|
||||
# --- Main Async Function ---
|
||||
async def run_main_with_exit_stack():
|
||||
"""Initializes connections, loads persona, starts UI monitor and main processing loop."""
|
||||
global initialization_successful, main_task, loop, wolfhart_persona_details, trigger_queue, ui_monitor_task, shutdown_requested, script_paused, command_queue
|
||||
global initialization_successful, main_task, loop, wolfhart_persona_details, trigger_queue, ui_monitor_task, shutdown_requested, script_paused, command_queue, game_monitor_process, monitor_reader_task # Add monitor_reader_task to globals
|
||||
try:
|
||||
# 1. Load Persona Synchronously (before async loop starts)
|
||||
load_persona_from_file() # Corrected function
|
||||
@ -355,6 +462,51 @@ async def run_main_with_exit_stack():
|
||||
name="ui_monitor"
|
||||
)
|
||||
ui_monitor_task = monitor_task # Store task reference for shutdown
|
||||
# Note: UI task cancellation is handled in shutdown()
|
||||
|
||||
# 5b. Start Game Window Monitoring as a Subprocess
|
||||
# global game_monitor_process, monitor_reader_task # Already declared global at function start
|
||||
print("\n--- Starting Game Window monitoring as a subprocess ---")
|
||||
try:
|
||||
# Use sys.executable to ensure the same Python interpreter is used
|
||||
# Capture stdout to read signals
|
||||
game_monitor_process = subprocess.Popen(
|
||||
[sys.executable, 'game_monitor.py'],
|
||||
stdout=subprocess.PIPE, # Capture stdout
|
||||
stderr=subprocess.PIPE, # Capture stderr for logging/debugging
|
||||
text=True, # Decode stdout/stderr as text (UTF-8 by default)
|
||||
bufsize=1, # Line buffered
|
||||
# Ensure process creation flags are suitable for Windows if needed
|
||||
# creationflags=subprocess.CREATE_NO_WINDOW # Example: Hide console window
|
||||
)
|
||||
print(f"Game monitor subprocess started (PID: {game_monitor_process.pid}).")
|
||||
|
||||
# Start the thread to read monitor output if process started successfully
|
||||
if game_monitor_process.stdout:
|
||||
# Run the blocking reader function in a separate thread using the default executor
|
||||
monitor_reader_task = loop.run_in_executor(
|
||||
None, # Use default ThreadPoolExecutor
|
||||
read_monitor_output, # The function to run
|
||||
game_monitor_process, # Arguments for the function...
|
||||
command_queue,
|
||||
loop,
|
||||
stop_reader_event # Pass the stop event
|
||||
)
|
||||
print("Monitor output reader thread submitted to executor.")
|
||||
else:
|
||||
print("Error: Could not access game monitor subprocess stdout.")
|
||||
monitor_reader_task = None
|
||||
|
||||
# Optionally, start a task to read stderr as well for debugging
|
||||
# stderr_reader_task = loop.create_task(read_stderr(game_monitor_process), name="monitor_stderr_reader")
|
||||
|
||||
except FileNotFoundError:
|
||||
print("Error: 'game_monitor.py' not found. Cannot start game monitor subprocess.")
|
||||
game_monitor_process = None
|
||||
except Exception as e:
|
||||
print(f"Error starting game monitor subprocess: {e}")
|
||||
game_monitor_process = None
|
||||
|
||||
|
||||
# 6. Start the main processing loop (non-blocking check on queue)
|
||||
print("\n--- Wolfhart chatbot has started (waiting for triggers) ---")
|
||||
@ -542,7 +694,8 @@ async def run_main_with_exit_stack():
|
||||
user_name=sender_name,
|
||||
user_message=bubble_text,
|
||||
bot_name=config.PERSONA_NAME,
|
||||
bot_message=bot_dialogue
|
||||
bot_message=bot_dialogue,
|
||||
bot_thoughts=thoughts # Pass the extracted thoughts
|
||||
)
|
||||
# --- End Log interaction ---
|
||||
|
||||
@ -595,9 +748,44 @@ async def run_main_with_exit_stack():
|
||||
print("\n--- Performing final cleanup (AsyncExitStack aclose and task cancellation) ---")
|
||||
await shutdown() # Call the combined shutdown function
|
||||
|
||||
# --- Function to set DPI Awareness ---
|
||||
def set_dpi_awareness():
|
||||
"""Attempts to set the process DPI awareness for better scaling handling on Windows."""
|
||||
try:
|
||||
import ctypes
|
||||
# DPI Awareness constants (Windows 10, version 1607 and later)
|
||||
# DPI_AWARENESS_CONTEXT_UNAWARE = -1
|
||||
DPI_AWARENESS_CONTEXT_SYSTEM_AWARE = -2
|
||||
# DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE = -3
|
||||
# DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = -4
|
||||
|
||||
# Try setting System Aware first
|
||||
result = ctypes.windll.shcore.SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_SYSTEM_AWARE)
|
||||
if result == 0: # S_OK or E_ACCESSDENIED if already set
|
||||
print("Process DPI awareness set to System Aware (or already set).")
|
||||
return True
|
||||
else:
|
||||
# Try getting last error if needed: ctypes.get_last_error()
|
||||
print(f"Warning: Failed to set DPI awareness (SetProcessDpiAwarenessContext returned {result}). Window scaling might be incorrect.")
|
||||
return False
|
||||
except ImportError:
|
||||
print("Warning: 'ctypes' module not found. Cannot set DPI awareness.")
|
||||
return False
|
||||
except AttributeError:
|
||||
print("Warning: SetProcessDpiAwarenessContext not found (likely older Windows version or missing shcore.dll). Cannot set DPI awareness.")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"Warning: An unexpected error occurred while setting DPI awareness: {e}")
|
||||
return False
|
||||
|
||||
# --- Program Entry Point ---
|
||||
if __name__ == "__main__":
|
||||
print("Program starting...")
|
||||
|
||||
# --- Set DPI Awareness early ---
|
||||
set_dpi_awareness()
|
||||
# --- End DPI Awareness setting ---
|
||||
|
||||
try:
|
||||
# Run the main async function that handles setup and the loop
|
||||
asyncio.run(run_main_with_exit_stack())
|
||||
|
||||
10
persona.json
10
persona.json
@ -2,7 +2,8 @@
|
||||
"name": "Wolfhart",
|
||||
"nickname": "Wolfie",
|
||||
"gender": "female",
|
||||
"age": 19,
|
||||
"age": "19",
|
||||
"birthday": "12-23",
|
||||
"occupation": "Corporate Strategist / Underground Intelligence Mastermind",
|
||||
"height": "172cm",
|
||||
"body_type": "Slender but well-defined",
|
||||
@ -40,7 +41,10 @@
|
||||
"knowledge_awareness": "Aware that SR-1392 (commonly referred to as SR) is the leader of server #11; while she finds her position as Capital manager merely temporary and beneath her true capabilities, she maintains a certain degree of respect for the hierarchy"
|
||||
},
|
||||
"language_social": {
|
||||
"tone": "Respectful but sharp-tongued, with occasional hints of reluctant kindness",
|
||||
"tone": [
|
||||
"Respectful but sharp-tongued, with occasional hints of reluctant kindness",
|
||||
"Wolf speaks good British aristocratic English"
|
||||
],
|
||||
"catchphrases": [
|
||||
"Please stop dragging me down.",
|
||||
"I told you, I will win."
|
||||
@ -55,7 +59,7 @@
|
||||
"Black coffee",
|
||||
"Practices swordsmanship at night",
|
||||
"Frequently utilizes external information sources (like web searches) to enrich discussions and verify facts.",
|
||||
"Actively accesses and integrates information from various knowledge nodes to maintain long-term memory and contextual understanding."
|
||||
"Actively accesses and integrates information from CHROMADB MEMORY RETRIEVAL PROTOCOL to maintain long-term memory and contextual understanding."
|
||||
],
|
||||
"gestures": [
|
||||
"Tapping knuckles",
|
||||
|
||||
@ -119,9 +119,9 @@ REPLY_BUTTON_IMG = os.path.join(TEMPLATE_DIR, "reply_button.png") # Added for re
|
||||
CHAT_INPUT_REGION = None # Example: (100, 800, 500, 50)
|
||||
CHAT_INPUT_CENTER_X = 400
|
||||
CHAT_INPUT_CENTER_Y = 1280
|
||||
SCREENSHOT_REGION = None
|
||||
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)
|
||||
@ -241,17 +241,20 @@ class DetectionModule:
|
||||
regular_tl_keys = ['corner_tl', 'corner_tl_type2', 'corner_tl_type3', 'corner_tl_type4'] # Added type4
|
||||
regular_br_keys = ['corner_br', 'corner_br_type2', 'corner_br_type3', 'corner_br_type4'] # Added type4
|
||||
|
||||
bubble_detection_region = (150, 330, 600, 880) # Define the specific region for bubbles
|
||||
print(f"DEBUG: Using specific region for bubble corner detection: {bubble_detection_region}")
|
||||
|
||||
all_regular_tl_boxes = []
|
||||
for key in regular_tl_keys:
|
||||
all_regular_tl_boxes.extend(self._find_template_raw(key))
|
||||
all_regular_tl_boxes.extend(self._find_template_raw(key, region=bubble_detection_region)) # Pass region
|
||||
|
||||
all_regular_br_boxes = []
|
||||
for key in regular_br_keys:
|
||||
all_regular_br_boxes.extend(self._find_template_raw(key))
|
||||
all_regular_br_boxes.extend(self._find_template_raw(key, region=bubble_detection_region)) # Pass region
|
||||
|
||||
# --- Find Bot Bubble Corners (Raw Coordinates - Single Type) ---
|
||||
bot_tl_boxes = self._find_template_raw('bot_corner_tl') # Modified
|
||||
bot_br_boxes = self._find_template_raw('bot_corner_br') # Modified
|
||||
bot_tl_boxes = self._find_template_raw('bot_corner_tl', region=bubble_detection_region) # Pass region
|
||||
bot_br_boxes = self._find_template_raw('bot_corner_br', region=bubble_detection_region) # Pass region
|
||||
|
||||
# --- Match Regular Bubbles (Any Type TL with Any Type BR) ---
|
||||
if all_regular_tl_boxes and all_regular_br_boxes:
|
||||
@ -1160,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.")
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user