mobile-mcp-ai 2.3.4__py3-none-any.whl → 2.5.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mobile_mcp/core/basic_tools_lite.py +2319 -143
- mobile_mcp/core/device_manager.py +2 -2
- mobile_mcp/core/ios_client_wda.py +18 -0
- mobile_mcp/core/mobile_client.py +31 -10
- mobile_mcp/core/template_matcher.py +429 -0
- mobile_mcp/core/templates/close_buttons/auto_x_0112_151217.png +0 -0
- mobile_mcp/core/templates/close_buttons/auto_x_0112_152037.png +0 -0
- mobile_mcp/core/templates/close_buttons/auto_x_0112_152840.png +0 -0
- mobile_mcp/core/templates/close_buttons/auto_x_0112_153256.png +0 -0
- mobile_mcp/core/templates/close_buttons/auto_x_0112_154847.png +0 -0
- mobile_mcp/core/templates/close_buttons/gray_x_stock_ad.png +0 -0
- mobile_mcp/core/utils/smart_wait.py +3 -3
- mobile_mcp/mcp_tools/mcp_server.py +548 -60
- {mobile_mcp_ai-2.3.4.dist-info → mobile_mcp_ai-2.5.9.dist-info}/METADATA +16 -2
- mobile_mcp_ai-2.5.9.dist-info/RECORD +32 -0
- mobile_mcp_ai-2.3.4.dist-info/RECORD +0 -25
- {mobile_mcp_ai-2.3.4.dist-info → mobile_mcp_ai-2.5.9.dist-info}/WHEEL +0 -0
- {mobile_mcp_ai-2.3.4.dist-info → mobile_mcp_ai-2.5.9.dist-info}/entry_points.txt +0 -0
- {mobile_mcp_ai-2.3.4.dist-info → mobile_mcp_ai-2.5.9.dist-info}/licenses/LICENSE +0 -0
- {mobile_mcp_ai-2.3.4.dist-info → mobile_mcp_ai-2.5.9.dist-info}/top_level.txt +0 -0
|
@@ -208,7 +208,7 @@ class DeviceManager:
|
|
|
208
208
|
|
|
209
209
|
try:
|
|
210
210
|
# 尝试获取页面结构,如果失败可能是无障碍服务未启用
|
|
211
|
-
xml = self.u2.dump_hierarchy()
|
|
211
|
+
xml = self.u2.dump_hierarchy(compressed=False)
|
|
212
212
|
if xml and len(xml) > 100: # 有内容说明无障碍服务正常
|
|
213
213
|
print(f" ✅ 无障碍服务: 已启用", file=sys.stderr)
|
|
214
214
|
return
|
|
@@ -235,7 +235,7 @@ class DeviceManager:
|
|
|
235
235
|
|
|
236
236
|
try:
|
|
237
237
|
# 尝试获取页面结构
|
|
238
|
-
xml = self.u2.dump_hierarchy()
|
|
238
|
+
xml = self.u2.dump_hierarchy(compressed=False)
|
|
239
239
|
if xml and len(xml) > 100:
|
|
240
240
|
return {
|
|
241
241
|
'enabled': True,
|
mobile_mcp/core/mobile_client.py
CHANGED
|
@@ -189,8 +189,29 @@ class MobileClient:
|
|
|
189
189
|
return xml_string
|
|
190
190
|
|
|
191
191
|
# Android平台
|
|
192
|
-
# 获取XML
|
|
193
|
-
xml_string =
|
|
192
|
+
# 获取XML - 优先使用 ADB 直接 dump(更完整,包含 NAF 元素)
|
|
193
|
+
xml_string = None
|
|
194
|
+
try:
|
|
195
|
+
# 方法1: 使用 ADB 直接 dump(获取最完整的 UI 树,包括 NAF 元素)
|
|
196
|
+
import subprocess
|
|
197
|
+
import tempfile
|
|
198
|
+
import os
|
|
199
|
+
|
|
200
|
+
# 在设备上执行 dump
|
|
201
|
+
self.u2.shell('uiautomator dump /sdcard/ui_dump.xml')
|
|
202
|
+
|
|
203
|
+
# 读取文件内容
|
|
204
|
+
result = self.u2.shell('cat /sdcard/ui_dump.xml')
|
|
205
|
+
if result and isinstance(result, str) and result.strip().startswith('<?xml'):
|
|
206
|
+
xml_string = result.strip()
|
|
207
|
+
# 清理临时文件
|
|
208
|
+
self.u2.shell('rm /sdcard/ui_dump.xml')
|
|
209
|
+
except Exception as e:
|
|
210
|
+
print(f" ⚠️ ADB dump 失败,使用 uiautomator2: {e}", file=sys.stderr)
|
|
211
|
+
|
|
212
|
+
# 方法2: 回退到 uiautomator2 的 dump_hierarchy
|
|
213
|
+
if not xml_string:
|
|
214
|
+
xml_string = self.u2.dump_hierarchy(compressed=False)
|
|
194
215
|
|
|
195
216
|
# 确保xml_string是字符串类型
|
|
196
217
|
if not isinstance(xml_string, str):
|
|
@@ -321,7 +342,7 @@ class MobileClient:
|
|
|
321
342
|
# 🎯 改进:尝试模糊匹配(忽略空格、括号)
|
|
322
343
|
ref_normalized = ref.replace(' ', '').replace('(', '').replace(')', '').replace('(', '').replace(')', '')
|
|
323
344
|
# 获取所有元素,手动匹配
|
|
324
|
-
xml_string = self.u2.dump_hierarchy()
|
|
345
|
+
xml_string = self.u2.dump_hierarchy(compressed=False)
|
|
325
346
|
elements = self.xml_parser.parse(xml_string)
|
|
326
347
|
for elem in elements:
|
|
327
348
|
elem_desc = elem.get('content_desc', '')
|
|
@@ -434,7 +455,7 @@ class MobileClient:
|
|
|
434
455
|
if verify:
|
|
435
456
|
# 获取点击前页面状态
|
|
436
457
|
try:
|
|
437
|
-
initial_xml = self.u2.dump_hierarchy()
|
|
458
|
+
initial_xml = self.u2.dump_hierarchy(compressed=False)
|
|
438
459
|
initial_length = len(initial_xml)
|
|
439
460
|
|
|
440
461
|
# 等待页面变化
|
|
@@ -757,7 +778,7 @@ class MobileClient:
|
|
|
757
778
|
initial_length = 0
|
|
758
779
|
if verify:
|
|
759
780
|
try:
|
|
760
|
-
initial_xml = self.u2.dump_hierarchy()
|
|
781
|
+
initial_xml = self.u2.dump_hierarchy(compressed=False)
|
|
761
782
|
initial_length = len(initial_xml)
|
|
762
783
|
except Exception as e:
|
|
763
784
|
print(f" ⚠️ 获取初始页面状态失败: {e}", file=sys.stderr)
|
|
@@ -1008,7 +1029,7 @@ class MobileClient:
|
|
|
1008
1029
|
try:
|
|
1009
1030
|
if verify:
|
|
1010
1031
|
# 获取操作前页面状态
|
|
1011
|
-
initial_xml = self.u2.dump_hierarchy()
|
|
1032
|
+
initial_xml = self.u2.dump_hierarchy(compressed=False)
|
|
1012
1033
|
initial_length = len(initial_xml)
|
|
1013
1034
|
|
|
1014
1035
|
self.u2.press(key.lower())
|
|
@@ -1037,7 +1058,7 @@ class MobileClient:
|
|
|
1037
1058
|
# 标准按键处理
|
|
1038
1059
|
if verify:
|
|
1039
1060
|
# 获取操作前页面状态
|
|
1040
|
-
initial_xml = self.u2.dump_hierarchy()
|
|
1061
|
+
initial_xml = self.u2.dump_hierarchy(compressed=False)
|
|
1041
1062
|
initial_length = len(initial_xml)
|
|
1042
1063
|
|
|
1043
1064
|
# 使用keycode按键 - uiautomator2使用shell命令
|
|
@@ -1099,7 +1120,7 @@ class MobileClient:
|
|
|
1099
1120
|
print(f" 🔍 智能搜索键:先尝试SEARCH键...", file=sys.stderr)
|
|
1100
1121
|
|
|
1101
1122
|
# 获取初始页面状态
|
|
1102
|
-
initial_xml = self.u2.dump_hierarchy()
|
|
1123
|
+
initial_xml = self.u2.dump_hierarchy(compressed=False)
|
|
1103
1124
|
initial_length = len(initial_xml)
|
|
1104
1125
|
|
|
1105
1126
|
# 方案1: 尝试 SEARCH 键 (keycode=84)
|
|
@@ -1126,7 +1147,7 @@ class MobileClient:
|
|
|
1126
1147
|
|
|
1127
1148
|
# 方案2: 尝试 ENTER 键 (keycode=66)
|
|
1128
1149
|
# 重新获取当前页面状态(因为可能有轻微变化)
|
|
1129
|
-
current_xml = self.u2.dump_hierarchy()
|
|
1150
|
+
current_xml = self.u2.dump_hierarchy(compressed=False)
|
|
1130
1151
|
current_length = len(current_xml)
|
|
1131
1152
|
|
|
1132
1153
|
self.u2.shell('input keyevent 66')
|
|
@@ -1184,7 +1205,7 @@ class MobileClient:
|
|
|
1184
1205
|
await asyncio.sleep(0.1) # 每100ms检查一次
|
|
1185
1206
|
|
|
1186
1207
|
try:
|
|
1187
|
-
current_xml = self.u2.dump_hierarchy()
|
|
1208
|
+
current_xml = self.u2.dump_hierarchy(compressed=False)
|
|
1188
1209
|
current_length = len(current_xml)
|
|
1189
1210
|
|
|
1190
1211
|
# 计算变化百分比
|
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OpenCV 模板匹配器 - 用于精确识别广告弹窗X号
|
|
3
|
+
核心优势:
|
|
4
|
+
1. 收集常见X号样式建立模板库
|
|
5
|
+
2. 多尺度匹配解决分辨率差异
|
|
6
|
+
3. 返回精确坐标,点击准确率高
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import cv2
|
|
11
|
+
import numpy as np
|
|
12
|
+
from typing import Dict, List, Tuple, Optional
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TemplateMatcher:
|
|
17
|
+
"""OpenCV 模板匹配器"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, template_dir: Optional[str] = None):
|
|
20
|
+
"""
|
|
21
|
+
初始化模板匹配器
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
template_dir: 模板目录路径,默认为 templates/close_buttons/
|
|
25
|
+
"""
|
|
26
|
+
if template_dir is None:
|
|
27
|
+
# 默认模板目录:优先使用包内目录,其次使用项目根目录
|
|
28
|
+
core_dir = Path(__file__).parent
|
|
29
|
+
# 1. 包内目录 (pip 安装后)
|
|
30
|
+
pkg_template_dir = core_dir / "templates" / "close_buttons"
|
|
31
|
+
# 2. 项目根目录 (开发时)
|
|
32
|
+
root_template_dir = core_dir.parent / "templates" / "close_buttons"
|
|
33
|
+
|
|
34
|
+
if pkg_template_dir.exists():
|
|
35
|
+
self.template_dir = pkg_template_dir
|
|
36
|
+
elif root_template_dir.exists():
|
|
37
|
+
self.template_dir = root_template_dir
|
|
38
|
+
else:
|
|
39
|
+
# 默认创建包内目录
|
|
40
|
+
self.template_dir = pkg_template_dir
|
|
41
|
+
else:
|
|
42
|
+
self.template_dir = Path(template_dir)
|
|
43
|
+
|
|
44
|
+
# 确保目录存在
|
|
45
|
+
self.template_dir.mkdir(parents=True, exist_ok=True)
|
|
46
|
+
|
|
47
|
+
# 多尺度匹配的缩放范围
|
|
48
|
+
self.scales = [0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.1, 1.2, 1.3, 1.5, 1.8, 2.0]
|
|
49
|
+
|
|
50
|
+
# 匹配阈值(越高越严格)
|
|
51
|
+
self.match_threshold = 0.75
|
|
52
|
+
|
|
53
|
+
# 缓存加载的模板
|
|
54
|
+
self._template_cache: Dict[str, np.ndarray] = {}
|
|
55
|
+
|
|
56
|
+
def load_templates(self) -> List[Tuple[str, np.ndarray]]:
|
|
57
|
+
"""
|
|
58
|
+
加载所有模板图片
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
List of (template_name, template_image) tuples
|
|
62
|
+
"""
|
|
63
|
+
templates = []
|
|
64
|
+
|
|
65
|
+
if not self.template_dir.exists():
|
|
66
|
+
return templates
|
|
67
|
+
|
|
68
|
+
# 支持的图片格式
|
|
69
|
+
extensions = ['.png', '.jpg', '.jpeg', '.bmp']
|
|
70
|
+
|
|
71
|
+
for file in self.template_dir.iterdir():
|
|
72
|
+
if file.suffix.lower() in extensions:
|
|
73
|
+
template_name = file.stem
|
|
74
|
+
|
|
75
|
+
# 使用缓存
|
|
76
|
+
if template_name in self._template_cache:
|
|
77
|
+
templates.append((template_name, self._template_cache[template_name]))
|
|
78
|
+
continue
|
|
79
|
+
|
|
80
|
+
# 读取模板(支持透明通道)
|
|
81
|
+
template = cv2.imread(str(file), cv2.IMREAD_UNCHANGED)
|
|
82
|
+
if template is not None:
|
|
83
|
+
self._template_cache[template_name] = template
|
|
84
|
+
templates.append((template_name, template))
|
|
85
|
+
|
|
86
|
+
return templates
|
|
87
|
+
|
|
88
|
+
def match_single_template(
|
|
89
|
+
self,
|
|
90
|
+
screenshot: np.ndarray,
|
|
91
|
+
template: np.ndarray,
|
|
92
|
+
threshold: Optional[float] = None
|
|
93
|
+
) -> List[Dict]:
|
|
94
|
+
"""
|
|
95
|
+
单模板多尺度匹配
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
screenshot: 截图 (BGR格式)
|
|
99
|
+
template: 模板图片
|
|
100
|
+
threshold: 匹配阈值
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
匹配结果列表
|
|
104
|
+
"""
|
|
105
|
+
if threshold is None:
|
|
106
|
+
threshold = self.match_threshold
|
|
107
|
+
|
|
108
|
+
results = []
|
|
109
|
+
|
|
110
|
+
# 转灰度图
|
|
111
|
+
if len(screenshot.shape) == 3:
|
|
112
|
+
gray_screen = cv2.cvtColor(screenshot, cv2.COLOR_BGR2GRAY)
|
|
113
|
+
else:
|
|
114
|
+
gray_screen = screenshot
|
|
115
|
+
|
|
116
|
+
# 处理模板(可能有透明通道)
|
|
117
|
+
# 注意:不使用 mask,因为 TM_CCOEFF_NORMED + mask 可能返回 INF
|
|
118
|
+
if len(template.shape) == 3:
|
|
119
|
+
if template.shape[2] == 4: # BGRA
|
|
120
|
+
template_gray = cv2.cvtColor(template[:, :, :3], cv2.COLOR_BGR2GRAY)
|
|
121
|
+
else: # BGR
|
|
122
|
+
template_gray = cv2.cvtColor(template, cv2.COLOR_BGR2GRAY)
|
|
123
|
+
else:
|
|
124
|
+
template_gray = template
|
|
125
|
+
|
|
126
|
+
template_h, template_w = template_gray.shape[:2]
|
|
127
|
+
|
|
128
|
+
# 多尺度匹配
|
|
129
|
+
for scale in self.scales:
|
|
130
|
+
# 缩放模板
|
|
131
|
+
new_w = int(template_w * scale)
|
|
132
|
+
new_h = int(template_h * scale)
|
|
133
|
+
|
|
134
|
+
# 跳过太小或太大的模板
|
|
135
|
+
if new_w < 10 or new_h < 10:
|
|
136
|
+
continue
|
|
137
|
+
if new_w > gray_screen.shape[1] or new_h > gray_screen.shape[0]:
|
|
138
|
+
continue
|
|
139
|
+
|
|
140
|
+
resized_template = cv2.resize(template_gray, (new_w, new_h))
|
|
141
|
+
|
|
142
|
+
# 模板匹配
|
|
143
|
+
try:
|
|
144
|
+
result = cv2.matchTemplate(
|
|
145
|
+
gray_screen, resized_template,
|
|
146
|
+
cv2.TM_CCOEFF_NORMED
|
|
147
|
+
)
|
|
148
|
+
except cv2.error:
|
|
149
|
+
continue
|
|
150
|
+
|
|
151
|
+
# 跳过包含 INF/NAN 的结果
|
|
152
|
+
if np.isinf(result).any() or np.isnan(result).any():
|
|
153
|
+
continue
|
|
154
|
+
|
|
155
|
+
# 找所有超过阈值的匹配点
|
|
156
|
+
locations = np.where(result >= threshold)
|
|
157
|
+
|
|
158
|
+
for pt in zip(*locations[::-1]): # (x, y)
|
|
159
|
+
confidence = float(result[pt[1], pt[0]])
|
|
160
|
+
center_x = int(pt[0] + new_w // 2)
|
|
161
|
+
center_y = int(pt[1] + new_h // 2)
|
|
162
|
+
|
|
163
|
+
results.append({
|
|
164
|
+
'x': center_x,
|
|
165
|
+
'y': center_y,
|
|
166
|
+
'width': int(new_w),
|
|
167
|
+
'height': int(new_h),
|
|
168
|
+
'scale': float(scale),
|
|
169
|
+
'confidence': confidence,
|
|
170
|
+
'top_left': (int(pt[0]), int(pt[1])),
|
|
171
|
+
'bottom_right': (int(pt[0] + new_w), int(pt[1] + new_h))
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
# 非极大值抑制(去除重叠的检测框)
|
|
175
|
+
results = self._non_max_suppression(results)
|
|
176
|
+
|
|
177
|
+
return results
|
|
178
|
+
|
|
179
|
+
def _non_max_suppression(self, results: List[Dict], overlap_thresh: float = 0.3) -> List[Dict]:
|
|
180
|
+
"""
|
|
181
|
+
非极大值抑制,去除重叠的检测框
|
|
182
|
+
"""
|
|
183
|
+
if len(results) == 0:
|
|
184
|
+
return []
|
|
185
|
+
|
|
186
|
+
# 按置信度排序
|
|
187
|
+
results = sorted(results, key=lambda x: x['confidence'], reverse=True)
|
|
188
|
+
|
|
189
|
+
kept = []
|
|
190
|
+
for result in results:
|
|
191
|
+
is_duplicate = False
|
|
192
|
+
for kept_result in kept:
|
|
193
|
+
# 计算中心点距离
|
|
194
|
+
dx = abs(result['x'] - kept_result['x'])
|
|
195
|
+
dy = abs(result['y'] - kept_result['y'])
|
|
196
|
+
|
|
197
|
+
# 如果中心点距离小于框的一半大小,认为是重复
|
|
198
|
+
avg_size = (result['width'] + result['height'] +
|
|
199
|
+
kept_result['width'] + kept_result['height']) / 4
|
|
200
|
+
|
|
201
|
+
if dx < avg_size * overlap_thresh and dy < avg_size * overlap_thresh:
|
|
202
|
+
is_duplicate = True
|
|
203
|
+
break
|
|
204
|
+
|
|
205
|
+
if not is_duplicate:
|
|
206
|
+
kept.append(result)
|
|
207
|
+
|
|
208
|
+
return kept
|
|
209
|
+
|
|
210
|
+
def find_close_buttons(
|
|
211
|
+
self,
|
|
212
|
+
screenshot_path: str,
|
|
213
|
+
threshold: Optional[float] = None
|
|
214
|
+
) -> Dict:
|
|
215
|
+
"""
|
|
216
|
+
在截图中查找所有关闭按钮
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
screenshot_path: 截图路径
|
|
220
|
+
threshold: 匹配阈值 (0-1)
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
匹配结果
|
|
224
|
+
"""
|
|
225
|
+
# 读取截图
|
|
226
|
+
screenshot = cv2.imread(screenshot_path)
|
|
227
|
+
if screenshot is None:
|
|
228
|
+
return {
|
|
229
|
+
"success": False,
|
|
230
|
+
"error": f"无法读取截图: {screenshot_path}"
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
img_height, img_width = screenshot.shape[:2]
|
|
234
|
+
|
|
235
|
+
# 加载模板
|
|
236
|
+
templates = self.load_templates()
|
|
237
|
+
if not templates:
|
|
238
|
+
return {
|
|
239
|
+
"success": False,
|
|
240
|
+
"error": "没有找到模板图片,请在 templates/close_buttons/ 目录添加X号模板",
|
|
241
|
+
"template_dir": str(self.template_dir),
|
|
242
|
+
"tip": "添加常见X号截图到模板目录,命名如 x_circle.png, x_white.png 等"
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
all_matches = []
|
|
246
|
+
|
|
247
|
+
for template_name, template in templates:
|
|
248
|
+
matches = self.match_single_template(screenshot, template, threshold)
|
|
249
|
+
for match in matches:
|
|
250
|
+
match['template'] = template_name
|
|
251
|
+
all_matches.append(match)
|
|
252
|
+
|
|
253
|
+
# 按置信度排序
|
|
254
|
+
all_matches = sorted(all_matches, key=lambda x: x['confidence'], reverse=True)
|
|
255
|
+
|
|
256
|
+
# 再次 NMS 去除不同模板的重复检测
|
|
257
|
+
all_matches = self._non_max_suppression(all_matches)
|
|
258
|
+
|
|
259
|
+
if not all_matches:
|
|
260
|
+
return {
|
|
261
|
+
"success": False,
|
|
262
|
+
"message": "未找到匹配的关闭按钮",
|
|
263
|
+
"templates_used": [t[0] for t in templates],
|
|
264
|
+
"threshold": threshold or self.match_threshold,
|
|
265
|
+
"tip": "可能需要添加新的X号模板,或降低匹配阈值"
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
# 计算百分比坐标
|
|
269
|
+
for match in all_matches:
|
|
270
|
+
match['x_percent'] = round(match['x'] / img_width * 100, 1)
|
|
271
|
+
match['y_percent'] = round(match['y'] / img_height * 100, 1)
|
|
272
|
+
|
|
273
|
+
best = all_matches[0]
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
"success": True,
|
|
277
|
+
"message": f"✅ 找到 {len(all_matches)} 个关闭按钮",
|
|
278
|
+
"best_match": {
|
|
279
|
+
"template": best['template'],
|
|
280
|
+
"center": {"x": int(best['x']), "y": int(best['y'])},
|
|
281
|
+
"percent": {"x": float(best['x_percent']), "y": float(best['y_percent'])},
|
|
282
|
+
"size": f"{best['width']}x{best['height']}",
|
|
283
|
+
"confidence": float(round(best['confidence'] * 100, 1))
|
|
284
|
+
},
|
|
285
|
+
"click_command": f"mobile_click_by_percent({best['x_percent']}, {best['y_percent']})",
|
|
286
|
+
"all_matches": [
|
|
287
|
+
{
|
|
288
|
+
"template": m['template'],
|
|
289
|
+
"percent": f"({m['x_percent']}%, {m['y_percent']}%)",
|
|
290
|
+
"confidence": f"{m['confidence']*100:.1f}%"
|
|
291
|
+
}
|
|
292
|
+
for m in all_matches[:5] # 最多返回5个
|
|
293
|
+
],
|
|
294
|
+
"image_size": {"width": img_width, "height": img_height}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
def add_template(self, image_path: str, template_name: str) -> Dict:
|
|
298
|
+
"""
|
|
299
|
+
添加新模板到模板库
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
image_path: 图片路径(可以是截图的一部分)
|
|
303
|
+
template_name: 模板名称
|
|
304
|
+
|
|
305
|
+
Returns:
|
|
306
|
+
结果
|
|
307
|
+
"""
|
|
308
|
+
# 读取图片
|
|
309
|
+
img = cv2.imread(image_path)
|
|
310
|
+
if img is None:
|
|
311
|
+
return {"success": False, "error": f"无法读取图片: {image_path}"}
|
|
312
|
+
|
|
313
|
+
# 保存到模板目录
|
|
314
|
+
output_path = self.template_dir / f"{template_name}.png"
|
|
315
|
+
cv2.imwrite(str(output_path), img)
|
|
316
|
+
|
|
317
|
+
# 清除缓存
|
|
318
|
+
self._template_cache.clear()
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
"success": True,
|
|
322
|
+
"message": f"✅ 模板已保存: {output_path}",
|
|
323
|
+
"template_name": template_name
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
def crop_and_add_template(
|
|
327
|
+
self,
|
|
328
|
+
screenshot_path: str,
|
|
329
|
+
x: int, y: int,
|
|
330
|
+
width: int, height: int,
|
|
331
|
+
template_name: str
|
|
332
|
+
) -> Dict:
|
|
333
|
+
"""
|
|
334
|
+
从截图中裁剪区域并添加为模板
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
screenshot_path: 截图路径
|
|
338
|
+
x, y: 左上角坐标
|
|
339
|
+
width, height: 裁剪尺寸
|
|
340
|
+
template_name: 模板名称
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
结果
|
|
344
|
+
"""
|
|
345
|
+
img = cv2.imread(screenshot_path)
|
|
346
|
+
if img is None:
|
|
347
|
+
return {"success": False, "error": f"无法读取截图: {screenshot_path}"}
|
|
348
|
+
|
|
349
|
+
# 裁剪
|
|
350
|
+
cropped = img[y:y+height, x:x+width]
|
|
351
|
+
|
|
352
|
+
if cropped.size == 0:
|
|
353
|
+
return {"success": False, "error": "裁剪区域无效"}
|
|
354
|
+
|
|
355
|
+
# 保存
|
|
356
|
+
output_path = self.template_dir / f"{template_name}.png"
|
|
357
|
+
cv2.imwrite(str(output_path), cropped)
|
|
358
|
+
|
|
359
|
+
# 清除缓存
|
|
360
|
+
self._template_cache.clear()
|
|
361
|
+
|
|
362
|
+
return {
|
|
363
|
+
"success": True,
|
|
364
|
+
"message": f"✅ 模板已保存: {output_path}",
|
|
365
|
+
"template_name": template_name,
|
|
366
|
+
"size": f"{width}x{height}"
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
def list_templates(self) -> Dict:
|
|
370
|
+
"""列出所有模板"""
|
|
371
|
+
templates = self.load_templates()
|
|
372
|
+
|
|
373
|
+
if not templates:
|
|
374
|
+
return {
|
|
375
|
+
"success": True,
|
|
376
|
+
"templates": [],
|
|
377
|
+
"message": "模板库为空",
|
|
378
|
+
"template_dir": str(self.template_dir)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
template_info = []
|
|
382
|
+
for name, img in templates:
|
|
383
|
+
h, w = img.shape[:2]
|
|
384
|
+
template_info.append({
|
|
385
|
+
"name": name,
|
|
386
|
+
"size": f"{w}x{h}",
|
|
387
|
+
"path": str(self.template_dir / f"{name}.png")
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
"success": True,
|
|
392
|
+
"templates": template_info,
|
|
393
|
+
"count": len(template_info),
|
|
394
|
+
"template_dir": str(self.template_dir)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
def delete_template(self, template_name: str) -> Dict:
|
|
398
|
+
"""删除模板"""
|
|
399
|
+
# 查找模板文件
|
|
400
|
+
for ext in ['.png', '.jpg', '.jpeg', '.bmp']:
|
|
401
|
+
path = self.template_dir / f"{template_name}{ext}"
|
|
402
|
+
if path.exists():
|
|
403
|
+
path.unlink()
|
|
404
|
+
self._template_cache.pop(template_name, None)
|
|
405
|
+
return {
|
|
406
|
+
"success": True,
|
|
407
|
+
"message": f"✅ 已删除模板: {template_name}"
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return {
|
|
411
|
+
"success": False,
|
|
412
|
+
"error": f"模板不存在: {template_name}"
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
# 便捷函数
|
|
417
|
+
def match_close_button(screenshot_path: str, threshold: float = 0.75) -> Dict:
|
|
418
|
+
"""
|
|
419
|
+
快速匹配关闭按钮
|
|
420
|
+
|
|
421
|
+
用法:
|
|
422
|
+
from core.template_matcher import match_close_button
|
|
423
|
+
result = match_close_button("screenshot.png")
|
|
424
|
+
if result["success"]:
|
|
425
|
+
print(result["click_command"])
|
|
426
|
+
"""
|
|
427
|
+
matcher = TemplateMatcher()
|
|
428
|
+
return matcher.find_close_buttons(screenshot_path, threshold)
|
|
429
|
+
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -61,7 +61,7 @@ class SmartWait:
|
|
|
61
61
|
while time.time() - start_time < timeout:
|
|
62
62
|
try:
|
|
63
63
|
# 获取当前页面快照(只获取元素数量,不解析详细内容)
|
|
64
|
-
xml = self.client.u2.dump_hierarchy()
|
|
64
|
+
xml = self.client.u2.dump_hierarchy(compressed=False)
|
|
65
65
|
current_snapshot = len(xml) # 使用XML长度作为简单的页面状态标识
|
|
66
66
|
|
|
67
67
|
if last_snapshot is not None:
|
|
@@ -137,14 +137,14 @@ class SmartWait:
|
|
|
137
137
|
|
|
138
138
|
try:
|
|
139
139
|
# 获取初始页面状态
|
|
140
|
-
initial_xml = self.client.u2.dump_hierarchy()
|
|
140
|
+
initial_xml = self.client.u2.dump_hierarchy(compressed=False)
|
|
141
141
|
initial_length = len(initial_xml)
|
|
142
142
|
|
|
143
143
|
while time.time() - start_time < timeout:
|
|
144
144
|
await asyncio.sleep(self.poll_interval)
|
|
145
145
|
|
|
146
146
|
try:
|
|
147
|
-
current_xml = self.client.u2.dump_hierarchy()
|
|
147
|
+
current_xml = self.client.u2.dump_hierarchy(compressed=False)
|
|
148
148
|
current_length = len(current_xml)
|
|
149
149
|
|
|
150
150
|
# 页面变化超过5%认为有变化
|