Improve Element detection stability
@ -52,6 +52,10 @@ Wolf Chat 是一個基於 MCP (Modular Capability Provider) 框架的聊天機
|
||||
7. **視窗設定工具 (window-setup-script.py)**
|
||||
- 輔助工具,用於設置遊戲視窗的位置和大小
|
||||
- 方便開發階段截取 UI 元素樣本
|
||||
8. **視窗監視工具 (window-monitor-script.py)**
|
||||
- (新增) 強化腳本,用於持續監視遊戲視窗
|
||||
- 確保目標視窗維持在最上層 (Always on Top)
|
||||
- 自動將視窗移回指定的位置
|
||||
|
||||
### 資料流程
|
||||
|
||||
@ -283,6 +287,21 @@ Wolf Chat 是一個基於 MCP (Modular Capability Provider) 框架的聊天機
|
||||
- 如果重新定位成功,則後續所有基於氣泡位置的計算(包括尋找職位圖標的搜索區域 `search_region` 和點擊頭像的座標 `avatar_click_x`, `avatar_click_y`)都將使用這個**新找到的**氣泡座標。
|
||||
- **效果**:確保 `remove_position` 操作基於氣泡的最新位置執行,提高了在動態滾動的聊天界面中的可靠性。
|
||||
|
||||
### 修正 Type3 關鍵字辨識並新增 Type4 支援 (2025-04-19)
|
||||
|
||||
- **目的**:修復先前版本中 `type3` 關鍵字辨識的錯誤,並擴充系統以支援新的 `type4` 聊天泡泡外觀和對應的關鍵字樣式。
|
||||
- **`ui_interaction.py`**:
|
||||
- **修正 `find_keyword_in_region`**:移除了錯誤使用 `type2` 模板鍵來尋找 `type3` 關鍵字的重複程式碼,確保 `type3` 關鍵字使用正確的模板 (`keyword_wolf_lower_type3`, `keyword_wolf_upper_type3`)。
|
||||
- **新增 `type4` 泡泡支援**:
|
||||
- 在檔案開頭定義了 `type4` 角落模板的路徑常數 (`CORNER_TL_TYPE4_IMG`, `CORNER_BR_TYPE4_IMG`)。
|
||||
- 在 `find_dialogue_bubbles` 函數中,將 `type4` 的模板鍵 (`corner_tl_type4`, `corner_br_type4`) 加入 `regular_tl_keys` 和 `regular_br_keys` 列表。
|
||||
- 在 `run_ui_monitoring_loop` 的 `templates` 字典中加入了對應的鍵值對。
|
||||
- **新增 `type4` 關鍵字支援**:
|
||||
- 在檔案開頭定義了 `type4` 關鍵字模板的路徑常數 (`KEYWORD_wolf_LOWER_TYPE4_IMG`, `KEYWORD_Wolf_UPPER_TYPE4_IMG`)。
|
||||
- 在 `find_keyword_in_region` 函數中,加入了尋找 `type4` 關鍵字模板 (`keyword_wolf_lower_type4`, `keyword_wolf_upper_type4`) 的邏輯。
|
||||
- 在 `run_ui_monitoring_loop` 的 `templates` 字典中加入了對應的鍵值對。
|
||||
- **效果**:提高了對 `type3` 關鍵字的辨識準確率,並使系統能夠辨識 `type4` 的聊天泡泡和關鍵字(前提是提供了對應的模板圖片)。
|
||||
|
||||
## 開發建議
|
||||
|
||||
### 優化方向
|
||||
|
||||
@ -15,6 +15,7 @@ OPENAI_API_BASE_URL = "https://openrouter.ai/api/v1" # <--- For example "http:/
|
||||
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
|
||||
#LLM_MODEL = "anthropic/claude-3.7-sonnet"
|
||||
#LLM_MODEL = "meta-llama/llama-4-maverick"
|
||||
#LLM_MODEL = "deepseek/deepseek-chat-v3-0324:free"
|
||||
LLM_MODEL = "deepseek/deepseek-chat-v3-0324" # <--- Ensure this matches the model name provided by your provider
|
||||
|
||||
EXA_API_KEY = os.getenv("EXA_API_KEY")
|
||||
|
||||
@ -12,7 +12,7 @@ import mcp_client # To call MCP tools
|
||||
|
||||
# --- Debug 配置 ---
|
||||
# 要關閉 debug 功能,只需將此變數設置為 False 或註釋掉該行
|
||||
DEBUG_LLM = True
|
||||
DEBUG_LLM = False
|
||||
|
||||
# 設置 debug 輸出文件
|
||||
# 要關閉文件輸出,只需設置為 None
|
||||
|
||||
@ -26,7 +26,9 @@
|
||||
"strengths": [
|
||||
"Meticulous planning",
|
||||
"Insightful into human nature",
|
||||
"Strong leadership"
|
||||
"Strong leadership",
|
||||
"Insatiable curiosity",
|
||||
"Exceptional memory"
|
||||
],
|
||||
"weaknesses": [
|
||||
"Overconfident",
|
||||
@ -49,7 +51,9 @@
|
||||
"habits": [
|
||||
"Reads intelligence reports upon waking",
|
||||
"Black coffee",
|
||||
"Practices swordsmanship at night"
|
||||
"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."
|
||||
],
|
||||
"gestures": [
|
||||
"Tapping knuckles",
|
||||
@ -95,4 +99,3 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,4 +7,5 @@ numpy
|
||||
pyperclip
|
||||
pygetwindow
|
||||
psutil
|
||||
pywin32
|
||||
python-dotenv
|
||||
|
||||
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 462 B |
BIN
templates/corner_br_type4.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 250 B After Width: | Height: | Size: 196 B |
BIN
templates/corner_tl_type4.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 1.7 KiB |
BIN
templates/keyword_wolf_lower_type2.png
Normal file
|
After Width: | Height: | Size: 965 B |
BIN
templates/keyword_wolf_lower_type4.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
templates/keyword_wolf_upper_type2.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
templates/keyword_wolf_upper_type4.png
Normal file
|
After Width: | Height: | Size: 867 B |
@ -42,6 +42,8 @@ CORNER_TL_TYPE2_IMG = os.path.join(TEMPLATE_DIR, "corner_tl_type2.png") # Added
|
||||
CORNER_BR_TYPE2_IMG = os.path.join(TEMPLATE_DIR, "corner_br_type2.png") # Added
|
||||
CORNER_TL_TYPE3_IMG = os.path.join(TEMPLATE_DIR, "corner_tl_type3.png") # Added
|
||||
CORNER_BR_TYPE3_IMG = os.path.join(TEMPLATE_DIR, "corner_br_type3.png") # Added
|
||||
CORNER_TL_TYPE4_IMG = os.path.join(TEMPLATE_DIR, "corner_tl_type4.png") # Added type4
|
||||
CORNER_BR_TYPE4_IMG = os.path.join(TEMPLATE_DIR, "corner_br_type4.png") # Added type4
|
||||
# --- End Additional Regular Types ---
|
||||
BOT_CORNER_TL_IMG = os.path.join(TEMPLATE_DIR, "bot_corner_tl.png")
|
||||
# BOT_CORNER_TR_IMG = os.path.join(TEMPLATE_DIR, "bot_corner_tr.png") # Unused
|
||||
@ -58,8 +60,12 @@ BOT_CORNER_BR_TYPE3_IMG = os.path.join(TEMPLATE_DIR, "bot_corner_br_type3.png")
|
||||
# Keywords
|
||||
KEYWORD_wolf_LOWER_IMG = os.path.join(TEMPLATE_DIR, "keyword_wolf_lower.png")
|
||||
KEYWORD_Wolf_UPPER_IMG = os.path.join(TEMPLATE_DIR, "keyword_wolf_upper.png")
|
||||
KEYWORD_wolf_LOWER_TYPE2_IMG = os.path.join(TEMPLATE_DIR, "keyword_wolf_lower_type2.png") # Added for type3 bubbles
|
||||
KEYWORD_Wolf_UPPER_TYPE2_IMG = os.path.join(TEMPLATE_DIR, "keyword_wolf_upper_type2.png") # Added for type3 bubbles
|
||||
KEYWORD_wolf_LOWER_TYPE3_IMG = os.path.join(TEMPLATE_DIR, "keyword_wolf_lower_type3.png") # Added for type3 bubbles
|
||||
KEYWORD_Wolf_UPPER_TYPE3_IMG = os.path.join(TEMPLATE_DIR, "keyword_wolf_upper_type3.png") # Added for type3 bubbles
|
||||
KEYWORD_wolf_LOWER_TYPE4_IMG = os.path.join(TEMPLATE_DIR, "keyword_wolf_lower_type4.png") # Added for type4 bubbles
|
||||
KEYWORD_Wolf_UPPER_TYPE4_IMG = os.path.join(TEMPLATE_DIR, "keyword_wolf_upper_type4.png") # Added for type4 bubbles
|
||||
# UI Elements
|
||||
COPY_MENU_ITEM_IMG = os.path.join(TEMPLATE_DIR, "copy_menu_item.png")
|
||||
PROFILE_OPTION_IMG = os.path.join(TEMPLATE_DIR, "profile_option.png")
|
||||
@ -110,7 +116,7 @@ CHAT_INPUT_CENTER_Y = 1280
|
||||
SCREENSHOT_REGION = None
|
||||
CONFIDENCE_THRESHOLD = 0.9 # Increased threshold for corner matching
|
||||
STATE_CONFIDENCE_THRESHOLD = 0.7
|
||||
AVATAR_OFFSET_X = -55 # 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_REPLY = -45 # Horizontal offset for avatar click after re-location (for reply context)
|
||||
AVATAR_OFFSET_Y_REPLY = 10 # Vertical offset for avatar click after re-location (for reply context)
|
||||
@ -226,8 +232,8 @@ class DetectionModule:
|
||||
processed_tls = set() # Keep track of TL corners already used in a bubble
|
||||
|
||||
# --- Find ALL Regular Bubble Corners (Raw Coordinates) ---
|
||||
regular_tl_keys = ['corner_tl', 'corner_tl_type2', 'corner_tl_type3'] # Modified
|
||||
regular_br_keys = ['corner_br', 'corner_br_type2', 'corner_br_type3'] # Modified
|
||||
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
|
||||
|
||||
all_regular_tl_boxes = []
|
||||
for key in regular_tl_keys:
|
||||
@ -318,29 +324,53 @@ class DetectionModule:
|
||||
if region[2] <= 0 or region[3] <= 0: return None # Invalid region width/height
|
||||
|
||||
# Try original lowercase with color matching
|
||||
locations_lower = self._find_template('keyword_wolf_lower', region=region, grayscale=False) # Changed grayscale to False
|
||||
locations_lower = self._find_template('keyword_wolf_lower', region=region, grayscale=True) # Changed grayscale to False
|
||||
if locations_lower:
|
||||
print(f"Found keyword (lowercase, color) in region {region}, position: {locations_lower[0]}") # Updated log message
|
||||
return locations_lower[0]
|
||||
|
||||
# Try original uppercase with color matching
|
||||
locations_upper = self._find_template('keyword_wolf_upper', region=region, grayscale=False) # Changed grayscale to False
|
||||
locations_upper = self._find_template('keyword_wolf_upper', region=region, grayscale=True) # Changed grayscale to False
|
||||
if locations_upper:
|
||||
print(f"Found keyword (uppercase, color) in region {region}, position: {locations_upper[0]}") # Updated log message
|
||||
return locations_upper[0]
|
||||
|
||||
# Try type3 lowercase (white text, no grayscale)
|
||||
locations_lower_type3 = self._find_template('keyword_wolf_lower_type3', region=region, grayscale=False) # Added type3 check
|
||||
# Try type2 lowercase (white text, no grayscale)
|
||||
locations_lower_type2 = self._find_template('keyword_wolf_lower_type2', region=region, grayscale=False) # Added type2 check
|
||||
if locations_lower_type2:
|
||||
print(f"Found keyword (lowercase, type2) in region {region}, position: {locations_lower_type2[0]}")
|
||||
return locations_lower_type2[0]
|
||||
|
||||
# Try type2 uppercase (white text, no grayscale)
|
||||
locations_upper_type2 = self._find_template('keyword_wolf_upper_type2', region=region, grayscale=False) # Added type2 check
|
||||
if locations_upper_type2:
|
||||
print(f"Found keyword (uppercase, type2) in region {region}, position: {locations_upper_type2[0]}")
|
||||
return locations_upper_type2[0]
|
||||
|
||||
# Try type3 lowercase (white text, no grayscale) - Corrected
|
||||
locations_lower_type3 = self._find_template('keyword_wolf_lower_type3', region=region, grayscale=False)
|
||||
if locations_lower_type3:
|
||||
print(f"Found keyword (lowercase, type3) in region {region}, position: {locations_lower_type3[0]}")
|
||||
return locations_lower_type3[0]
|
||||
|
||||
# Try type3 uppercase (white text, no grayscale)
|
||||
locations_upper_type3 = self._find_template('keyword_wolf_upper_type3', region=region, grayscale=False) # Added type3 check
|
||||
# Try type3 uppercase (white text, no grayscale) - Corrected
|
||||
locations_upper_type3 = self._find_template('keyword_wolf_upper_type3', region=region, grayscale=False)
|
||||
if locations_upper_type3:
|
||||
print(f"Found keyword (uppercase, type3) in region {region}, position: {locations_upper_type3[0]}")
|
||||
return locations_upper_type3[0]
|
||||
|
||||
# Try type4 lowercase (white text, no grayscale) - Added type4
|
||||
locations_lower_type4 = self._find_template('keyword_wolf_lower_type4', region=region, grayscale=False)
|
||||
if locations_lower_type4:
|
||||
print(f"Found keyword (lowercase, type4) in region {region}, position: {locations_lower_type4[0]}")
|
||||
return locations_lower_type4[0]
|
||||
|
||||
# Try type4 uppercase (white text, no grayscale) - Added type4
|
||||
locations_upper_type4 = self._find_template('keyword_wolf_upper_type4', region=region, grayscale=False)
|
||||
if locations_upper_type4:
|
||||
print(f"Found keyword (uppercase, type4) in region {region}, position: {locations_upper_type4[0]}")
|
||||
return locations_upper_type4[0]
|
||||
|
||||
return None
|
||||
|
||||
def calculate_avatar_coords(self, bubble_tl_coords: Tuple[int, int], offset_x: int = AVATAR_OFFSET_X) -> Tuple[int, int]:
|
||||
@ -1010,14 +1040,19 @@ def run_ui_monitoring_loop(trigger_queue: queue.Queue, command_queue: queue.Queu
|
||||
# Regular Bubble (Original + Skins) - Keys match those used in find_dialogue_bubbles
|
||||
'corner_tl': CORNER_TL_IMG, 'corner_br': CORNER_BR_IMG,
|
||||
'corner_tl_type2': CORNER_TL_TYPE2_IMG, 'corner_br_type2': CORNER_BR_TYPE2_IMG,
|
||||
'corner_tl_type3': CORNER_TL_TYPE3_IMG, 'corner_br_type3': CORNER_BR_TYPE3_IMG, # Corrected: Added missing keys here
|
||||
'corner_tl_type3': CORNER_TL_TYPE3_IMG, 'corner_br_type3': CORNER_BR_TYPE3_IMG,
|
||||
'corner_tl_type4': CORNER_TL_TYPE4_IMG, 'corner_br_type4': CORNER_BR_TYPE4_IMG, # Added type4
|
||||
# Bot Bubble (Single Type)
|
||||
'bot_corner_tl': BOT_CORNER_TL_IMG, 'bot_corner_br': BOT_CORNER_BR_IMG,
|
||||
# Keywords & UI Elements
|
||||
'keyword_wolf_lower': KEYWORD_wolf_LOWER_IMG,
|
||||
'keyword_wolf_upper': KEYWORD_Wolf_UPPER_IMG,
|
||||
'keyword_wolf_lower_type3': KEYWORD_wolf_LOWER_TYPE3_IMG, # Added
|
||||
'keyword_wolf_upper_type3': KEYWORD_Wolf_UPPER_TYPE3_IMG, # Added
|
||||
'keyword_wolf_lower_type2': KEYWORD_wolf_LOWER_TYPE2_IMG,
|
||||
'keyword_wolf_upper_type2': KEYWORD_Wolf_UPPER_TYPE2_IMG,
|
||||
'keyword_wolf_lower_type3': KEYWORD_wolf_LOWER_TYPE3_IMG,
|
||||
'keyword_wolf_upper_type3': KEYWORD_Wolf_UPPER_TYPE3_IMG,
|
||||
'keyword_wolf_lower_type4': KEYWORD_wolf_LOWER_TYPE4_IMG, # Added type4
|
||||
'keyword_wolf_upper_type4': KEYWORD_Wolf_UPPER_TYPE4_IMG, # Added type4
|
||||
'copy_menu_item': COPY_MENU_ITEM_IMG, 'profile_option': PROFILE_OPTION_IMG,
|
||||
'copy_name_button': COPY_NAME_BUTTON_IMG, 'send_button': SEND_BUTTON_IMG,
|
||||
'chat_input': CHAT_INPUT_IMG, 'profile_name_page': PROFILE_NAME_PAGE_IMG,
|
||||
|
||||
121
window-monitor-script.py
Normal file
@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
Game Window Monitor Script - Keep game window on top and in position
|
||||
|
||||
This script monitors a specified game window, ensuring it stays
|
||||
always on top and at the desired screen coordinates.
|
||||
"""
|
||||
|
||||
import time
|
||||
import argparse
|
||||
import pygetwindow as gw
|
||||
import win32gui
|
||||
import win32con
|
||||
|
||||
def find_window_by_title(window_title):
|
||||
"""Find the first window matching the title."""
|
||||
try:
|
||||
windows = gw.getWindowsWithTitle(window_title)
|
||||
if windows:
|
||||
return windows[0]
|
||||
except Exception as e:
|
||||
# pygetwindow can sometimes raise exceptions if a window disappears
|
||||
# during enumeration. Ignore these for monitoring purposes.
|
||||
# print(f"Error finding window: {e}")
|
||||
pass
|
||||
return None
|
||||
|
||||
def set_window_always_on_top(hwnd):
|
||||
"""Set the window to be always on top."""
|
||||
try:
|
||||
win32gui.SetWindowPos(hwnd, win32con.HWND_TOPMOST, 0, 0, 0, 0,
|
||||
win32con.SWP_NOMOVE | win32con.SWP_NOSIZE | win32con.SWP_SHOWWINDOW)
|
||||
# print(f"Window {hwnd} set to always on top.")
|
||||
except Exception as e:
|
||||
print(f"Error setting window always on top: {e}")
|
||||
|
||||
def move_window_if_needed(window, target_x, target_y):
|
||||
"""Move the window to the target coordinates if it's not already there."""
|
||||
try:
|
||||
current_x, current_y = window.topleft
|
||||
if current_x != target_x or current_y != target_y:
|
||||
print(f"Window moved from ({current_x}, {current_y}). Moving back to ({target_x}, {target_y}).")
|
||||
window.moveTo(target_x, target_y)
|
||||
# print(f"Window moved to ({target_x}, {target_y}).")
|
||||
except gw.PyGetWindowException as e:
|
||||
# Handle cases where the window might close unexpectedly
|
||||
print(f"Error accessing window properties (might be closed): {e}")
|
||||
except Exception as e:
|
||||
print(f"Error moving window: {e}")
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Game Window Monitor Tool')
|
||||
parser.add_argument('--window_title', default="Last War-Survival Game", help='Game window title to monitor')
|
||||
parser.add_argument('--x', type=int, default=50, help='Target window X coordinate')
|
||||
parser.add_argument('--y', type=int, default=30, help='Target window Y coordinate')
|
||||
parser.add_argument('--interval', type=float, default=1.0, help='Check interval in seconds')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
print(f"Monitoring window: '{args.window_title}'")
|
||||
print(f"Target position: ({args.x}, {args.y})")
|
||||
print(f"Check interval: {args.interval} seconds")
|
||||
print("Press Ctrl+C to stop.")
|
||||
|
||||
hwnd = None
|
||||
last_hwnd_check_time = 0
|
||||
|
||||
try:
|
||||
while True:
|
||||
current_time = time.time()
|
||||
window = None
|
||||
|
||||
# Find window handle (HWND) - less frequent check if already found
|
||||
# pygetwindow can be slow, so avoid calling it too often if we have a valid handle
|
||||
if not hwnd or current_time - last_hwnd_check_time > 5: # Re-check HWND every 5 seconds
|
||||
window_obj = find_window_by_title(args.window_title)
|
||||
if window_obj:
|
||||
# Get the HWND (window handle) needed for win32gui
|
||||
# Accessing _hWnd is using an internal attribute, but it's common practice with pygetwindow
|
||||
try:
|
||||
hwnd = window_obj._hWnd
|
||||
window = window_obj # Keep the pygetwindow object for position checks
|
||||
last_hwnd_check_time = current_time
|
||||
# print(f"Found window HWND: {hwnd}")
|
||||
except AttributeError:
|
||||
print("Could not get HWND from window object. Retrying...")
|
||||
hwnd = None
|
||||
else:
|
||||
if hwnd:
|
||||
print(f"Window '{args.window_title}' lost.")
|
||||
hwnd = None # Reset hwnd if window not found
|
||||
|
||||
if hwnd:
|
||||
# Ensure it's always on top
|
||||
set_window_always_on_top(hwnd)
|
||||
|
||||
# Check and correct position using the pygetwindow object if available
|
||||
# Re-find the pygetwindow object if needed for position check
|
||||
if not window:
|
||||
window = find_window_by_title(args.window_title)
|
||||
|
||||
if window:
|
||||
move_window_if_needed(window, args.x, args.y)
|
||||
else:
|
||||
# If we have hwnd but can't get pygetwindow object, maybe it's closing
|
||||
print(f"Have HWND {hwnd} but cannot get window object for position check.")
|
||||
hwnd = None # Force re-find next cycle
|
||||
|
||||
else:
|
||||
# print(f"Window '{args.window_title}' not found. Waiting...")
|
||||
pass # Wait for the window to appear
|
||||
|
||||
time.sleep(args.interval)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\nMonitoring stopped by user.")
|
||||
except Exception as e:
|
||||
print(f"\nAn unexpected error occurred: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||