mobile-mcp-ai 2.3.3__py3-none-any.whl → 2.3.5__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 +153 -117
- mobile_mcp/core/ios_client_wda.py +1 -0
- mobile_mcp/core/mobile_client.py +22 -27
- mobile_mcp/mcp_tools/mcp_server.py +37 -42
- {mobile_mcp_ai-2.3.3.dist-info → mobile_mcp_ai-2.3.5.dist-info}/METADATA +1 -1
- {mobile_mcp_ai-2.3.3.dist-info → mobile_mcp_ai-2.3.5.dist-info}/RECORD +10 -10
- {mobile_mcp_ai-2.3.3.dist-info → mobile_mcp_ai-2.3.5.dist-info}/WHEEL +0 -0
- {mobile_mcp_ai-2.3.3.dist-info → mobile_mcp_ai-2.3.5.dist-info}/entry_points.txt +0 -0
- {mobile_mcp_ai-2.3.3.dist-info → mobile_mcp_ai-2.3.5.dist-info}/licenses/LICENSE +0 -0
- {mobile_mcp_ai-2.3.3.dist-info → mobile_mcp_ai-2.3.5.dist-info}/top_level.txt +0 -0
|
@@ -56,8 +56,9 @@ class BasicMobileToolsLite:
|
|
|
56
56
|
# ==================== 截图 ====================
|
|
57
57
|
|
|
58
58
|
def take_screenshot(self, description: str = "", compress: bool = True,
|
|
59
|
-
max_width: int = 720, quality: int = 75
|
|
60
|
-
|
|
59
|
+
max_width: int = 720, quality: int = 75,
|
|
60
|
+
crop_x: int = 0, crop_y: int = 0, crop_size: int = 0) -> Dict:
|
|
61
|
+
"""截图(支持压缩和局部裁剪)
|
|
61
62
|
|
|
62
63
|
压缩原理:
|
|
63
64
|
1. 先截取原始 PNG 图片
|
|
@@ -65,11 +66,20 @@ class BasicMobileToolsLite:
|
|
|
65
66
|
3. 转换为 JPEG 格式 + 降低质量(如 100% → 75%)
|
|
66
67
|
4. 最终文件从 2MB 压缩到约 80KB(节省 96%)
|
|
67
68
|
|
|
69
|
+
局部裁剪(用于精确识别小元素):
|
|
70
|
+
- 第一次全屏截图,AI 返回大概坐标
|
|
71
|
+
- 第二次传入 crop_x, crop_y, crop_size 截取局部区域
|
|
72
|
+
- 局部区域不压缩,保持清晰度,AI 可精确识别
|
|
73
|
+
- 返回 crop_offset_x/y 用于坐标换算
|
|
74
|
+
|
|
68
75
|
Args:
|
|
69
76
|
description: 截图描述(可选)
|
|
70
77
|
compress: 是否压缩(默认 True,推荐开启省 token)
|
|
71
78
|
max_width: 压缩后最大宽度(默认 720,对 AI 识别足够)
|
|
72
79
|
quality: JPEG 质量 1-100(默认 75,肉眼几乎看不出区别)
|
|
80
|
+
crop_x: 裁剪中心点 X 坐标(屏幕坐标,0 表示不裁剪)
|
|
81
|
+
crop_y: 裁剪中心点 Y 坐标(屏幕坐标,0 表示不裁剪)
|
|
82
|
+
crop_size: 裁剪区域大小(默认 0 不裁剪,推荐 200-400)
|
|
73
83
|
|
|
74
84
|
压缩效果示例:
|
|
75
85
|
原图 PNG: 2048KB
|
|
@@ -104,29 +114,87 @@ class BasicMobileToolsLite:
|
|
|
104
114
|
|
|
105
115
|
original_size = temp_path.stat().st_size
|
|
106
116
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
117
|
+
# 第2步:打开图片
|
|
118
|
+
img = Image.open(temp_path)
|
|
119
|
+
|
|
120
|
+
# 第2.5步:局部裁剪(如果指定了裁剪参数)
|
|
121
|
+
crop_offset_x, crop_offset_y = 0, 0
|
|
122
|
+
is_cropped = False
|
|
123
|
+
|
|
124
|
+
if crop_x > 0 and crop_y > 0 and crop_size > 0:
|
|
125
|
+
# 计算裁剪区域(以 crop_x, crop_y 为中心)
|
|
126
|
+
half_size = crop_size // 2
|
|
127
|
+
left = max(0, crop_x - half_size)
|
|
128
|
+
top = max(0, crop_y - half_size)
|
|
129
|
+
right = min(img.width, crop_x + half_size)
|
|
130
|
+
bottom = min(img.height, crop_y + half_size)
|
|
131
|
+
|
|
132
|
+
# 记录偏移量(用于坐标换算)
|
|
133
|
+
crop_offset_x = left
|
|
134
|
+
crop_offset_y = top
|
|
135
|
+
|
|
136
|
+
# 裁剪
|
|
137
|
+
img = img.crop((left, top, right, bottom))
|
|
138
|
+
is_cropped = True
|
|
139
|
+
|
|
140
|
+
# ========== 情况1:局部裁剪截图(不压缩,保持清晰度)==========
|
|
141
|
+
if is_cropped:
|
|
142
|
+
# 生成文件名
|
|
143
|
+
if description:
|
|
144
|
+
safe_desc = re.sub(r'[^\w\s-]', '', description).strip().replace(' ', '_')
|
|
145
|
+
filename = f"screenshot_{platform}_crop_{safe_desc}_{timestamp}.png"
|
|
146
|
+
else:
|
|
147
|
+
filename = f"screenshot_{platform}_crop_{timestamp}.png"
|
|
110
148
|
|
|
149
|
+
final_path = self.screenshot_dir / filename
|
|
150
|
+
|
|
151
|
+
# 保存为 PNG(保持清晰度)
|
|
152
|
+
img.save(str(final_path), "PNG")
|
|
153
|
+
|
|
154
|
+
# 删除临时文件
|
|
155
|
+
temp_path.unlink()
|
|
156
|
+
|
|
157
|
+
cropped_size = final_path.stat().st_size
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
"success": True,
|
|
161
|
+
"screenshot_path": str(final_path),
|
|
162
|
+
"screen_width": screen_width,
|
|
163
|
+
"screen_height": screen_height,
|
|
164
|
+
"image_width": img.width,
|
|
165
|
+
"image_height": img.height,
|
|
166
|
+
"crop_offset_x": crop_offset_x,
|
|
167
|
+
"crop_offset_y": crop_offset_y,
|
|
168
|
+
"file_size": f"{cropped_size/1024:.1f}KB",
|
|
169
|
+
"message": f"🔍 局部截图已保存: {final_path}\n"
|
|
170
|
+
f"📐 裁剪区域: ({crop_offset_x}, {crop_offset_y}) 起,{img.width}x{img.height} 像素\n"
|
|
171
|
+
f"📦 文件大小: {cropped_size/1024:.0f}KB\n"
|
|
172
|
+
f"🎯 【坐标换算】AI 返回坐标 (x, y) 后:\n"
|
|
173
|
+
f" 实际屏幕坐标 = ({crop_offset_x} + x, {crop_offset_y} + y)\n"
|
|
174
|
+
f" 或直接调用 mobile_click_at_coords(x, y, crop_offset_x={crop_offset_x}, crop_offset_y={crop_offset_y})"
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
# ========== 情况2:全屏压缩截图 ==========
|
|
178
|
+
elif compress:
|
|
111
179
|
# 第3步:缩小尺寸(保持宽高比)
|
|
180
|
+
image_width, image_height = img.width, img.height
|
|
181
|
+
|
|
112
182
|
if img.width > max_width:
|
|
113
183
|
ratio = max_width / img.width
|
|
114
184
|
new_w = max_width
|
|
115
185
|
new_h = int(img.height * ratio)
|
|
116
186
|
# 兼容不同版本的 Pillow
|
|
117
187
|
try:
|
|
118
|
-
# Pillow 10.0.0+
|
|
119
188
|
resample = Image.Resampling.LANCZOS
|
|
120
189
|
except AttributeError:
|
|
121
190
|
try:
|
|
122
|
-
# Pillow 9.x
|
|
123
191
|
resample = Image.LANCZOS
|
|
124
192
|
except AttributeError:
|
|
125
|
-
# Pillow 旧版本
|
|
126
193
|
resample = Image.ANTIALIAS
|
|
127
194
|
img = img.resize((new_w, new_h), resample)
|
|
195
|
+
image_width, image_height = new_w, new_h
|
|
128
196
|
|
|
129
|
-
#
|
|
197
|
+
# 生成文件名(JPEG 格式)
|
|
130
198
|
if description:
|
|
131
199
|
safe_desc = re.sub(r'[^\w\s-]', '', description).strip().replace(' ', '_')
|
|
132
200
|
filename = f"screenshot_{platform}_{safe_desc}_{timestamp}.jpg"
|
|
@@ -135,10 +203,8 @@ class BasicMobileToolsLite:
|
|
|
135
203
|
|
|
136
204
|
final_path = self.screenshot_dir / filename
|
|
137
205
|
|
|
138
|
-
#
|
|
139
|
-
# 先转换为 RGB 模式,处理可能的 RGBA 或 P 模式
|
|
206
|
+
# 保存为 JPEG(处理透明通道)
|
|
140
207
|
if img.mode in ('RGBA', 'LA', 'P'):
|
|
141
|
-
# 创建白色背景
|
|
142
208
|
background = Image.new('RGB', img.size, (255, 255, 255))
|
|
143
209
|
if img.mode == 'P':
|
|
144
210
|
img = img.convert('RGBA')
|
|
@@ -148,8 +214,6 @@ class BasicMobileToolsLite:
|
|
|
148
214
|
img = img.convert("RGB")
|
|
149
215
|
|
|
150
216
|
img.save(str(final_path), "JPEG", quality=quality)
|
|
151
|
-
|
|
152
|
-
# 第6步:删除临时 PNG
|
|
153
217
|
temp_path.unlink()
|
|
154
218
|
|
|
155
219
|
compressed_size = final_path.stat().st_size
|
|
@@ -160,17 +224,22 @@ class BasicMobileToolsLite:
|
|
|
160
224
|
"screenshot_path": str(final_path),
|
|
161
225
|
"screen_width": screen_width,
|
|
162
226
|
"screen_height": screen_height,
|
|
227
|
+
"image_width": image_width,
|
|
228
|
+
"image_height": image_height,
|
|
163
229
|
"original_size": f"{original_size/1024:.1f}KB",
|
|
164
230
|
"compressed_size": f"{compressed_size/1024:.1f}KB",
|
|
165
231
|
"saved_percent": f"{saved_percent:.0f}%",
|
|
166
232
|
"message": f"📸 截图已保存: {final_path}\n"
|
|
167
233
|
f"📐 屏幕尺寸: {screen_width}x{screen_height}\n"
|
|
234
|
+
f"🖼️ 图片尺寸: {image_width}x{image_height}(AI 分析用)\n"
|
|
168
235
|
f"📦 已压缩: {original_size/1024:.0f}KB → {compressed_size/1024:.0f}KB (省 {saved_percent:.0f}%)\n"
|
|
169
|
-
f"
|
|
170
|
-
f"
|
|
236
|
+
f"⚠️ 【重要】AI 返回的坐标需要转换!\n"
|
|
237
|
+
f" 请使用 mobile_click_at_coords 并传入 image_width={image_width}, image_height={image_height}\n"
|
|
238
|
+
f" 工具会自动将图片坐标转换为屏幕坐标"
|
|
171
239
|
}
|
|
240
|
+
|
|
241
|
+
# ========== 情况3:全屏不压缩截图 ==========
|
|
172
242
|
else:
|
|
173
|
-
# 不压缩,直接重命名临时文件
|
|
174
243
|
if description:
|
|
175
244
|
safe_desc = re.sub(r'[^\w\s-]', '', description).strip().replace(' ', '_')
|
|
176
245
|
filename = f"screenshot_{platform}_{safe_desc}_{timestamp}.png"
|
|
@@ -185,6 +254,8 @@ class BasicMobileToolsLite:
|
|
|
185
254
|
"screenshot_path": str(final_path),
|
|
186
255
|
"screen_width": screen_width,
|
|
187
256
|
"screen_height": screen_height,
|
|
257
|
+
"image_width": screen_width,
|
|
258
|
+
"image_height": screen_height,
|
|
188
259
|
"file_size": f"{original_size/1024:.1f}KB",
|
|
189
260
|
"message": f"📸 截图已保存: {final_path}\n"
|
|
190
261
|
f"📐 屏幕尺寸: {screen_width}x{screen_height}\n"
|
|
@@ -226,11 +297,14 @@ class BasicMobileToolsLite:
|
|
|
226
297
|
width = info.get('displayWidth', 0)
|
|
227
298
|
height = info.get('displayHeight', 0)
|
|
228
299
|
|
|
300
|
+
# 不压缩时,图片尺寸 = 屏幕尺寸
|
|
229
301
|
return {
|
|
230
302
|
"success": True,
|
|
231
303
|
"screenshot_path": str(screenshot_path),
|
|
232
304
|
"screen_width": width,
|
|
233
305
|
"screen_height": height,
|
|
306
|
+
"image_width": width,
|
|
307
|
+
"image_height": height,
|
|
234
308
|
"message": f"📸 截图已保存: {screenshot_path}\n"
|
|
235
309
|
f"📐 屏幕尺寸: {width}x{height}\n"
|
|
236
310
|
f"⚠️ 未压缩(PIL 未安装),建议安装: pip install Pillow"
|
|
@@ -266,25 +340,63 @@ class BasicMobileToolsLite:
|
|
|
266
340
|
|
|
267
341
|
# ==================== 点击操作 ====================
|
|
268
342
|
|
|
269
|
-
def click_at_coords(self, x: int, y: int
|
|
270
|
-
|
|
343
|
+
def click_at_coords(self, x: int, y: int, image_width: int = 0, image_height: int = 0,
|
|
344
|
+
crop_offset_x: int = 0, crop_offset_y: int = 0) -> Dict:
|
|
345
|
+
"""点击坐标(核心功能,支持自动坐标转换)
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
x: X 坐标(来自截图分析或屏幕坐标)
|
|
349
|
+
y: Y 坐标(来自截图分析或屏幕坐标)
|
|
350
|
+
image_width: 截图的宽度(可选,传入后自动转换坐标)
|
|
351
|
+
image_height: 截图的高度(可选,传入后自动转换坐标)
|
|
352
|
+
crop_offset_x: 局部截图的 X 偏移量(可选,局部截图时传入)
|
|
353
|
+
crop_offset_y: 局部截图的 Y 偏移量(可选,局部截图时传入)
|
|
354
|
+
|
|
355
|
+
坐标转换说明:
|
|
356
|
+
1. 全屏压缩截图:传入 image_width/image_height,自动按比例转换
|
|
357
|
+
2. 局部裁剪截图:传入 crop_offset_x/crop_offset_y,自动加上偏移量
|
|
358
|
+
"""
|
|
271
359
|
try:
|
|
272
|
-
#
|
|
360
|
+
# 获取屏幕尺寸
|
|
273
361
|
screen_width, screen_height = 0, 0
|
|
274
362
|
if self._is_ios():
|
|
275
363
|
ios_client = self._get_ios_client()
|
|
276
364
|
if ios_client and hasattr(ios_client, 'wda'):
|
|
277
|
-
ios_client.wda.click(x, y)
|
|
278
365
|
size = ios_client.wda.window_size()
|
|
279
366
|
screen_width, screen_height = size[0], size[1]
|
|
280
367
|
else:
|
|
281
368
|
return {"success": False, "message": "❌ iOS 客户端未初始化"}
|
|
282
369
|
else:
|
|
283
|
-
self.client.u2.click(x, y)
|
|
284
370
|
info = self.client.u2.info
|
|
285
371
|
screen_width = info.get('displayWidth', 0)
|
|
286
372
|
screen_height = info.get('displayHeight', 0)
|
|
287
373
|
|
|
374
|
+
# 🎯 坐标转换
|
|
375
|
+
original_x, original_y = x, y
|
|
376
|
+
converted = False
|
|
377
|
+
conversion_type = ""
|
|
378
|
+
|
|
379
|
+
# 情况1:局部裁剪截图 - 加上偏移量
|
|
380
|
+
if crop_offset_x > 0 or crop_offset_y > 0:
|
|
381
|
+
x = x + crop_offset_x
|
|
382
|
+
y = y + crop_offset_y
|
|
383
|
+
converted = True
|
|
384
|
+
conversion_type = "crop_offset"
|
|
385
|
+
# 情况2:全屏压缩截图 - 按比例转换
|
|
386
|
+
elif image_width > 0 and image_height > 0 and screen_width > 0 and screen_height > 0:
|
|
387
|
+
if image_width != screen_width or image_height != screen_height:
|
|
388
|
+
x = int(x * screen_width / image_width)
|
|
389
|
+
y = int(y * screen_height / image_height)
|
|
390
|
+
converted = True
|
|
391
|
+
conversion_type = "scale"
|
|
392
|
+
|
|
393
|
+
# 执行点击
|
|
394
|
+
if self._is_ios():
|
|
395
|
+
ios_client = self._get_ios_client()
|
|
396
|
+
ios_client.wda.click(x, y)
|
|
397
|
+
else:
|
|
398
|
+
self.client.u2.click(x, y)
|
|
399
|
+
|
|
288
400
|
time.sleep(0.3)
|
|
289
401
|
|
|
290
402
|
# 计算百分比坐标(用于跨设备兼容)
|
|
@@ -303,10 +415,25 @@ class BasicMobileToolsLite:
|
|
|
303
415
|
ref=f"coords_{x}_{y}"
|
|
304
416
|
)
|
|
305
417
|
|
|
306
|
-
|
|
307
|
-
"
|
|
308
|
-
|
|
309
|
-
|
|
418
|
+
if converted:
|
|
419
|
+
if conversion_type == "crop_offset":
|
|
420
|
+
return {
|
|
421
|
+
"success": True,
|
|
422
|
+
"message": f"✅ 点击成功: ({x}, {y})\n"
|
|
423
|
+
f" 🔍 局部截图坐标转换: ({original_x},{original_y}) + 偏移({crop_offset_x},{crop_offset_y}) → ({x},{y})"
|
|
424
|
+
}
|
|
425
|
+
else:
|
|
426
|
+
return {
|
|
427
|
+
"success": True,
|
|
428
|
+
"message": f"✅ 点击成功: ({x}, {y})\n"
|
|
429
|
+
f" 📐 坐标已转换: ({original_x},{original_y}) → ({x},{y})\n"
|
|
430
|
+
f" 🖼️ 图片尺寸: {image_width}x{image_height} → 屏幕: {screen_width}x{screen_height}"
|
|
431
|
+
}
|
|
432
|
+
else:
|
|
433
|
+
return {
|
|
434
|
+
"success": True,
|
|
435
|
+
"message": f"✅ 点击成功: ({x}, {y}) [相对位置: {x_percent}%, {y_percent}%]"
|
|
436
|
+
}
|
|
310
437
|
except Exception as e:
|
|
311
438
|
return {"success": False, "message": f"❌ 点击失败: {e}"}
|
|
312
439
|
|
|
@@ -813,97 +940,6 @@ class BasicMobileToolsLite:
|
|
|
813
940
|
except Exception as e:
|
|
814
941
|
return {"success": False, "message": f"❌ 断言失败: {e}"}
|
|
815
942
|
|
|
816
|
-
def close_ad(self, keywords: Optional[List[str]] = None, max_attempts: int = 3) -> Dict:
|
|
817
|
-
"""关闭广告弹窗
|
|
818
|
-
|
|
819
|
-
自动检测并点击广告关闭按钮,支持多种关闭方式:
|
|
820
|
-
1. 文本匹配:关闭、跳过、Skip、Close 等
|
|
821
|
-
2. 特殊符号:×、X、✕ 等
|
|
822
|
-
3. content-desc 匹配
|
|
823
|
-
|
|
824
|
-
Args:
|
|
825
|
-
keywords: 自定义关键词列表,默认使用内置关键词
|
|
826
|
-
max_attempts: 最大尝试次数,默认3次(处理多层弹窗)
|
|
827
|
-
|
|
828
|
-
Returns:
|
|
829
|
-
关闭结果,包含关闭的广告数量
|
|
830
|
-
"""
|
|
831
|
-
# 默认关键词(按优先级排序)
|
|
832
|
-
default_keywords = [
|
|
833
|
-
'关闭', '跳过', 'Skip', 'Close', 'close',
|
|
834
|
-
'×', 'X', '✕', '╳',
|
|
835
|
-
'我知道了', '稍后再说', '不再提示', '取消',
|
|
836
|
-
'知道了', '好的', '确定',
|
|
837
|
-
'Later', 'No thanks', 'Not now', 'Dismiss'
|
|
838
|
-
]
|
|
839
|
-
|
|
840
|
-
search_keywords = keywords if keywords else default_keywords
|
|
841
|
-
closed_count = 0
|
|
842
|
-
closed_items = []
|
|
843
|
-
|
|
844
|
-
try:
|
|
845
|
-
for attempt in range(max_attempts):
|
|
846
|
-
found_in_this_round = False
|
|
847
|
-
|
|
848
|
-
for keyword in search_keywords:
|
|
849
|
-
try:
|
|
850
|
-
if self._is_ios():
|
|
851
|
-
ios_client = self._get_ios_client()
|
|
852
|
-
if ios_client and hasattr(ios_client, 'wda'):
|
|
853
|
-
# iOS: 尝试 name 和 label
|
|
854
|
-
elem = ios_client.wda(name=keyword)
|
|
855
|
-
if not elem.exists:
|
|
856
|
-
elem = ios_client.wda(label=keyword)
|
|
857
|
-
if not elem.exists:
|
|
858
|
-
elem = ios_client.wda(nameContains=keyword)
|
|
859
|
-
|
|
860
|
-
if elem.exists:
|
|
861
|
-
elem.click()
|
|
862
|
-
time.sleep(0.5)
|
|
863
|
-
closed_count += 1
|
|
864
|
-
closed_items.append(keyword)
|
|
865
|
-
found_in_this_round = True
|
|
866
|
-
break
|
|
867
|
-
else:
|
|
868
|
-
# Android: 尝试 text 和 content-desc
|
|
869
|
-
elem = self.client.u2(text=keyword)
|
|
870
|
-
if not elem.exists(timeout=0.2):
|
|
871
|
-
elem = self.client.u2(textContains=keyword)
|
|
872
|
-
if not elem.exists(timeout=0.2):
|
|
873
|
-
elem = self.client.u2(description=keyword)
|
|
874
|
-
if not elem.exists(timeout=0.2):
|
|
875
|
-
elem = self.client.u2(descriptionContains=keyword)
|
|
876
|
-
|
|
877
|
-
if elem.exists(timeout=0.2):
|
|
878
|
-
elem.click()
|
|
879
|
-
time.sleep(0.5)
|
|
880
|
-
closed_count += 1
|
|
881
|
-
closed_items.append(keyword)
|
|
882
|
-
found_in_this_round = True
|
|
883
|
-
break
|
|
884
|
-
except Exception:
|
|
885
|
-
continue
|
|
886
|
-
|
|
887
|
-
if not found_in_this_round:
|
|
888
|
-
# 这一轮没找到广告,退出
|
|
889
|
-
break
|
|
890
|
-
|
|
891
|
-
if closed_count > 0:
|
|
892
|
-
return {
|
|
893
|
-
"success": True,
|
|
894
|
-
"closed_count": closed_count,
|
|
895
|
-
"closed_items": closed_items,
|
|
896
|
-
"message": f"✅ 已关闭 {closed_count} 个广告弹窗: {', '.join(closed_items)}"
|
|
897
|
-
}
|
|
898
|
-
else:
|
|
899
|
-
return {
|
|
900
|
-
"success": True,
|
|
901
|
-
"closed_count": 0,
|
|
902
|
-
"message": "✅ 未发现广告弹窗(或已全部关闭)"
|
|
903
|
-
}
|
|
904
|
-
except Exception as e:
|
|
905
|
-
return {"success": False, "message": f"❌ 关闭广告失败: {e}"}
|
|
906
|
-
|
|
907
943
|
# ==================== 脚本生成 ====================
|
|
908
944
|
|
|
909
945
|
def get_operation_history(self, limit: Optional[int] = None) -> Dict:
|
mobile_mcp/core/mobile_client.py
CHANGED
|
@@ -825,34 +825,29 @@ class MobileClient:
|
|
|
825
825
|
return {"success": False, "reason": str(e)}
|
|
826
826
|
|
|
827
827
|
# Android平台
|
|
828
|
-
# 🎯
|
|
828
|
+
# 🎯 优先使用智能启动(推荐)
|
|
829
829
|
if smart_wait:
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
return result
|
|
852
|
-
except ImportError:
|
|
853
|
-
# SmartAppLauncher 模块不存在,使用传统方式
|
|
854
|
-
print(f" 💡 智能启动模块未安装,使用传统启动方式", file=sys.stderr)
|
|
855
|
-
# 继续执行下面的传统方式
|
|
830
|
+
from .smart_app_launcher import SmartAppLauncher
|
|
831
|
+
launcher = SmartAppLauncher(self)
|
|
832
|
+
# 优化:快速模式,最多3秒
|
|
833
|
+
smart_wait_time = min(wait_time, 3)
|
|
834
|
+
|
|
835
|
+
# 🎯 从环境变量读取是否自动关闭广告(默认True)
|
|
836
|
+
import os
|
|
837
|
+
auto_close_ads = os.environ.get('AUTO_CLOSE_ADS', 'true').lower() in ['true', '1', 'yes']
|
|
838
|
+
|
|
839
|
+
result = await launcher.launch_with_smart_wait(
|
|
840
|
+
package_name,
|
|
841
|
+
max_wait=smart_wait_time,
|
|
842
|
+
auto_close_ads=auto_close_ads
|
|
843
|
+
)
|
|
844
|
+
|
|
845
|
+
# 打印截图路径(供Cursor AI查看验证)
|
|
846
|
+
if result.get('screenshot_path'):
|
|
847
|
+
print(f"\n📸 启动截图已保存: {result['screenshot_path']}", file=sys.stderr)
|
|
848
|
+
print(f"💡 提示: 请查看截图确认App是否已正确进入主页", file=sys.stderr)
|
|
849
|
+
|
|
850
|
+
return result
|
|
856
851
|
|
|
857
852
|
# 传统方式(快速启动,不等待加载)
|
|
858
853
|
print(f" 📱 启动App: {package_name}", file=sys.stderr)
|
|
@@ -156,16 +156,26 @@ class MobileMCPServer:
|
|
|
156
156
|
# ==================== 截图(视觉兜底)====================
|
|
157
157
|
tools.append(Tool(
|
|
158
158
|
name="mobile_take_screenshot",
|
|
159
|
-
description="📸
|
|
159
|
+
description="📸 截图(支持全屏和局部裁剪)\n\n"
|
|
160
160
|
"🎯 使用场景:\n"
|
|
161
161
|
"- 游戏(Unity/Cocos)无法获取元素时\n"
|
|
162
162
|
"- mobile_list_elements 返回空时\n"
|
|
163
163
|
"- 需要确认页面状态时\n\n"
|
|
164
|
-
"
|
|
164
|
+
"🔍 【局部裁剪】精确识别小元素(如广告关闭按钮):\n"
|
|
165
|
+
" 1. 先全屏截图,AI 返回大概坐标 (600, 200)\n"
|
|
166
|
+
" 2. 再调用 crop_x=600, crop_y=200, crop_size=200 截取局部\n"
|
|
167
|
+
" 3. 局部图不压缩,AI 可精确识别\n"
|
|
168
|
+
" 4. 点击时传入 crop_offset_x/y 自动换算坐标\n\n"
|
|
169
|
+
"⚠️ 【重要】截图会被压缩!\n"
|
|
170
|
+
" - 全屏截图:点击时传 image_width/image_height 转换坐标\n"
|
|
171
|
+
" - 局部截图:点击时传 crop_offset_x/crop_offset_y 转换坐标",
|
|
165
172
|
inputSchema={
|
|
166
173
|
"type": "object",
|
|
167
174
|
"properties": {
|
|
168
|
-
"description": {"type": "string", "description": "截图描述(可选)"}
|
|
175
|
+
"description": {"type": "string", "description": "截图描述(可选)"},
|
|
176
|
+
"crop_x": {"type": "integer", "description": "局部裁剪中心 X 坐标(屏幕坐标,0 表示不裁剪)"},
|
|
177
|
+
"crop_y": {"type": "integer", "description": "局部裁剪中心 Y 坐标(屏幕坐标,0 表示不裁剪)"},
|
|
178
|
+
"crop_size": {"type": "integer", "description": "裁剪区域大小(推荐 200-400,0 表示不裁剪)"}
|
|
169
179
|
},
|
|
170
180
|
"required": []
|
|
171
181
|
}
|
|
@@ -215,13 +225,19 @@ class MobileMCPServer:
|
|
|
215
225
|
"- 游戏(Unity/Cocos)无法获取元素\n"
|
|
216
226
|
"- mobile_list_elements 返回空\n"
|
|
217
227
|
"- 元素没有 id 和 text\n\n"
|
|
218
|
-
"
|
|
219
|
-
"
|
|
228
|
+
"⚠️ 【坐标转换】两种场景:\n"
|
|
229
|
+
" 1. 全屏压缩截图:传入 image_width + image_height → 自动按比例转换\n"
|
|
230
|
+
" 2. 局部裁剪截图:传入 crop_offset_x + crop_offset_y → 自动加偏移\n\n"
|
|
231
|
+
"✅ 自动记录百分比坐标,生成脚本时转换为跨分辨率兼容的百分比定位",
|
|
220
232
|
inputSchema={
|
|
221
233
|
"type": "object",
|
|
222
234
|
"properties": {
|
|
223
|
-
"x": {"type": "number", "description": "X
|
|
224
|
-
"y": {"type": "number", "description": "Y
|
|
235
|
+
"x": {"type": "number", "description": "X 坐标(像素,来自截图分析或屏幕坐标)"},
|
|
236
|
+
"y": {"type": "number", "description": "Y 坐标(像素,来自截图分析或屏幕坐标)"},
|
|
237
|
+
"image_width": {"type": "number", "description": "全屏截图宽度(压缩截图时传入)"},
|
|
238
|
+
"image_height": {"type": "number", "description": "全屏截图高度(压缩截图时传入)"},
|
|
239
|
+
"crop_offset_x": {"type": "number", "description": "局部截图 X 偏移(裁剪截图时传入)"},
|
|
240
|
+
"crop_offset_y": {"type": "number", "description": "局部截图 Y 偏移(裁剪截图时传入)"}
|
|
225
241
|
},
|
|
226
242
|
"required": ["x", "y"]
|
|
227
243
|
}
|
|
@@ -379,32 +395,6 @@ class MobileMCPServer:
|
|
|
379
395
|
}
|
|
380
396
|
))
|
|
381
397
|
|
|
382
|
-
tools.append(Tool(
|
|
383
|
-
name="mobile_close_ad",
|
|
384
|
-
description="📢 关闭广告弹窗(自动检测并点击关闭按钮)\n\n"
|
|
385
|
-
"🎯 自动检测以下关闭方式:\n"
|
|
386
|
-
"- 文本:关闭、跳过、Skip、Close、我知道了、稍后再说\n"
|
|
387
|
-
"- 符号:×、X、✕ 等\n"
|
|
388
|
-
"- 无障碍描述(content-desc)\n\n"
|
|
389
|
-
"💡 支持多层弹窗,最多尝试3次\n"
|
|
390
|
-
"✅ 比视觉识别更准确,推荐使用!",
|
|
391
|
-
inputSchema={
|
|
392
|
-
"type": "object",
|
|
393
|
-
"properties": {
|
|
394
|
-
"keywords": {
|
|
395
|
-
"type": "array",
|
|
396
|
-
"items": {"type": "string"},
|
|
397
|
-
"description": "自定义关键词列表(可选,默认使用内置关键词)"
|
|
398
|
-
},
|
|
399
|
-
"max_attempts": {
|
|
400
|
-
"type": "number",
|
|
401
|
-
"description": "最大尝试次数(可选,默认3次,用于处理多层弹窗)"
|
|
402
|
-
}
|
|
403
|
-
},
|
|
404
|
-
"required": []
|
|
405
|
-
}
|
|
406
|
-
))
|
|
407
|
-
|
|
408
398
|
# ==================== pytest 脚本生成 ====================
|
|
409
399
|
tools.append(Tool(
|
|
410
400
|
name="mobile_get_operation_history",
|
|
@@ -463,7 +453,12 @@ class MobileMCPServer:
|
|
|
463
453
|
try:
|
|
464
454
|
# 截图
|
|
465
455
|
if name == "mobile_take_screenshot":
|
|
466
|
-
result = self.tools.take_screenshot(
|
|
456
|
+
result = self.tools.take_screenshot(
|
|
457
|
+
description=arguments.get("description", ""),
|
|
458
|
+
crop_x=arguments.get("crop_x", 0),
|
|
459
|
+
crop_y=arguments.get("crop_y", 0),
|
|
460
|
+
crop_size=arguments.get("crop_size", 0)
|
|
461
|
+
)
|
|
467
462
|
return [TextContent(type="text", text=self.format_response(result))]
|
|
468
463
|
|
|
469
464
|
elif name == "mobile_get_screen_size":
|
|
@@ -472,7 +467,14 @@ class MobileMCPServer:
|
|
|
472
467
|
|
|
473
468
|
# 点击
|
|
474
469
|
elif name == "mobile_click_at_coords":
|
|
475
|
-
result = self.tools.click_at_coords(
|
|
470
|
+
result = self.tools.click_at_coords(
|
|
471
|
+
arguments["x"],
|
|
472
|
+
arguments["y"],
|
|
473
|
+
arguments.get("image_width", 0),
|
|
474
|
+
arguments.get("image_height", 0),
|
|
475
|
+
arguments.get("crop_offset_x", 0),
|
|
476
|
+
arguments.get("crop_offset_y", 0)
|
|
477
|
+
)
|
|
476
478
|
return [TextContent(type="text", text=self.format_response(result))]
|
|
477
479
|
|
|
478
480
|
elif name == "mobile_click_by_text":
|
|
@@ -540,13 +542,6 @@ class MobileMCPServer:
|
|
|
540
542
|
result = self.tools.assert_text(arguments["text"])
|
|
541
543
|
return [TextContent(type="text", text=self.format_response(result))]
|
|
542
544
|
|
|
543
|
-
elif name == "mobile_close_ad":
|
|
544
|
-
result = self.tools.close_ad(
|
|
545
|
-
arguments.get("keywords"),
|
|
546
|
-
arguments.get("max_attempts", 3)
|
|
547
|
-
)
|
|
548
|
-
return [TextContent(type="text", text=self.format_response(result))]
|
|
549
|
-
|
|
550
545
|
# 脚本生成
|
|
551
546
|
elif name == "mobile_get_operation_history":
|
|
552
547
|
result = self.tools.get_operation_history(arguments.get("limit"))
|
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
mobile_mcp/__init__.py,sha256=sQJZTL_sxQFzmcS7jOtS2AHCfUySz40vhX96N6u1qy4,816
|
|
2
2
|
mobile_mcp/config.py,sha256=yaFLAV4bc2wX0GQPtZDo7OYF9E88tXV-av41fQsJwK4,4480
|
|
3
3
|
mobile_mcp/core/__init__.py,sha256=ndMy-cLAIsQDG5op7gM_AIplycqZSZPWEkec1pEhvEY,170
|
|
4
|
-
mobile_mcp/core/basic_tools_lite.py,sha256=
|
|
4
|
+
mobile_mcp/core/basic_tools_lite.py,sha256=kXcA7GAcfoArBawQ_hIxr8A2Dug6xFJqAqdFuevelqo,54481
|
|
5
5
|
mobile_mcp/core/device_manager.py,sha256=PX3-B5bJFnKNt6C8fT7FSY8JwD-ngZ3toF88bcOV9qA,8766
|
|
6
6
|
mobile_mcp/core/dynamic_config.py,sha256=Ja1n1pfb0HspGByqk2_A472mYVniKmGtNEWyjUjmgK8,9811
|
|
7
|
-
mobile_mcp/core/ios_client_wda.py,sha256=
|
|
7
|
+
mobile_mcp/core/ios_client_wda.py,sha256=1ZL68LhBxfWK9KBWJOngmzpdLrjMjvZwxY5pewFwfgY,18753
|
|
8
8
|
mobile_mcp/core/ios_device_manager_wda.py,sha256=A44glqI-24un7qST-E3w6BQD8mV92YVUbxy4rLlTScY,11264
|
|
9
|
-
mobile_mcp/core/mobile_client.py,sha256=
|
|
9
|
+
mobile_mcp/core/mobile_client.py,sha256=bno3HvU-QSAC3G4TnoFngTxqXeu-ZP5rGlEWdWh8jOo,62570
|
|
10
10
|
mobile_mcp/core/utils/__init__.py,sha256=RhMMsPszmEn8Q8GoNufypVSHJxyM9lio9U6jjpnuoPI,378
|
|
11
11
|
mobile_mcp/core/utils/logger.py,sha256=XXQAHUwT1jc70pq_tYFmL6f_nKrFlYm3hcgl-5RYRg0,3402
|
|
12
12
|
mobile_mcp/core/utils/operation_history_manager.py,sha256=gi8S8HJAMqvkUrY7_-kVbko3Xt7c4GAUziEujRd-N-Y,4792
|
|
13
13
|
mobile_mcp/core/utils/smart_wait.py,sha256=PvKXImfN9Irru3bQJUjf4FLGn8LjY2VLzUNEl-i7xLE,8601
|
|
14
14
|
mobile_mcp/mcp_tools/__init__.py,sha256=xkro8Rwqv_55YlVyhh-3DgRFSsLE3h1r31VIb3bpM6E,143
|
|
15
|
-
mobile_mcp/mcp_tools/mcp_server.py,sha256=
|
|
15
|
+
mobile_mcp/mcp_tools/mcp_server.py,sha256=labrvzCDrpzJOrPQjSaroCzRFfDapDceL8hKwqGvykk,26577
|
|
16
16
|
mobile_mcp/utils/__init__.py,sha256=8EH0i7UGtx1y_j_GEgdN-cZdWn2sRtZSEOLlNF9HRnY,158
|
|
17
17
|
mobile_mcp/utils/logger.py,sha256=Sqq2Nr0Y4p03erqcrbYKVPCGiFaNGHMcE_JwCkeOfU4,3626
|
|
18
18
|
mobile_mcp/utils/xml_formatter.py,sha256=uwTRb3vLbqhT8O-udzWT7s7LsV-DyDUz2DkofD3hXOE,4556
|
|
19
19
|
mobile_mcp/utils/xml_parser.py,sha256=QhL8CWbdmNDzmBLjtx6mEnjHgMFZzJeHpCL15qfXSpI,3926
|
|
20
|
-
mobile_mcp_ai-2.3.
|
|
21
|
-
mobile_mcp_ai-2.3.
|
|
22
|
-
mobile_mcp_ai-2.3.
|
|
23
|
-
mobile_mcp_ai-2.3.
|
|
24
|
-
mobile_mcp_ai-2.3.
|
|
25
|
-
mobile_mcp_ai-2.3.
|
|
20
|
+
mobile_mcp_ai-2.3.5.dist-info/licenses/LICENSE,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
|
|
21
|
+
mobile_mcp_ai-2.3.5.dist-info/METADATA,sha256=j0g3Xx9xilNforvrujiiI5xMrY9hRZeZRj-KqKhInkQ,9705
|
|
22
|
+
mobile_mcp_ai-2.3.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
23
|
+
mobile_mcp_ai-2.3.5.dist-info/entry_points.txt,sha256=KB_FglozgPHBprSM1vFbIzGyheFuHFmGanscRdMJ_8A,68
|
|
24
|
+
mobile_mcp_ai-2.3.5.dist-info/top_level.txt,sha256=lLm6YpbTv855Lbh8BIA0rPxhybIrvYUzMEk9OErHT94,11
|
|
25
|
+
mobile_mcp_ai-2.3.5.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|