mobile-mcp-ai 2.3.4__py3-none-any.whl → 2.3.6__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.
@@ -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) -> Dict:
60
- """截图(支持压缩,省 token)
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,12 +114,74 @@ class BasicMobileToolsLite:
104
114
 
105
115
  original_size = temp_path.stat().st_size
106
116
 
107
- if compress:
108
- # 第2步:打开图片
109
- img = Image.open(temp_path)
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"
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:
179
+ # 🔴 关键:记录原始图片尺寸(用于坐标转换)
180
+ # 注意:截图尺寸可能和 u2.info 的 displayWidth 不一致!
181
+ original_img_width = img.width
182
+ original_img_height = img.height
110
183
 
111
184
  # 第3步:缩小尺寸(保持宽高比)
112
- # 记录压缩后的图片尺寸(用于坐标转换)
113
185
  image_width, image_height = img.width, img.height
114
186
 
115
187
  if img.width > max_width:
@@ -118,20 +190,16 @@ class BasicMobileToolsLite:
118
190
  new_h = int(img.height * ratio)
119
191
  # 兼容不同版本的 Pillow
120
192
  try:
121
- # Pillow 10.0.0+
122
193
  resample = Image.Resampling.LANCZOS
123
194
  except AttributeError:
124
195
  try:
125
- # Pillow 9.x
126
196
  resample = Image.LANCZOS
127
197
  except AttributeError:
128
- # Pillow 旧版本
129
198
  resample = Image.ANTIALIAS
130
199
  img = img.resize((new_w, new_h), resample)
131
- # 更新为压缩后的尺寸
132
200
  image_width, image_height = new_w, new_h
133
201
 
134
- # 第4步:生成最终文件名(JPEG 格式)
202
+ # 生成文件名(JPEG 格式)
135
203
  if description:
136
204
  safe_desc = re.sub(r'[^\w\s-]', '', description).strip().replace(' ', '_')
137
205
  filename = f"screenshot_{platform}_{safe_desc}_{timestamp}.jpg"
@@ -140,10 +208,8 @@ class BasicMobileToolsLite:
140
208
 
141
209
  final_path = self.screenshot_dir / filename
142
210
 
143
- # 第5步:保存为 JPEG(PNG 可能有透明通道,需转 RGB)
144
- # 先转换为 RGB 模式,处理可能的 RGBA 或 P 模式
211
+ # 保存为 JPEG(处理透明通道)
145
212
  if img.mode in ('RGBA', 'LA', 'P'):
146
- # 创建白色背景
147
213
  background = Image.new('RGB', img.size, (255, 255, 255))
148
214
  if img.mode == 'P':
149
215
  img = img.convert('RGBA')
@@ -153,8 +219,6 @@ class BasicMobileToolsLite:
153
219
  img = img.convert("RGB")
154
220
 
155
221
  img.save(str(final_path), "JPEG", quality=quality)
156
-
157
- # 第6步:删除临时 PNG
158
222
  temp_path.unlink()
159
223
 
160
224
  compressed_size = final_path.stat().st_size
@@ -165,21 +229,23 @@ class BasicMobileToolsLite:
165
229
  "screenshot_path": str(final_path),
166
230
  "screen_width": screen_width,
167
231
  "screen_height": screen_height,
168
- "image_width": image_width,
169
- "image_height": image_height,
232
+ "original_img_width": original_img_width, # 截图原始宽度
233
+ "original_img_height": original_img_height, # 截图原始高度
234
+ "image_width": image_width, # 压缩后宽度(AI 看到的)
235
+ "image_height": image_height, # 压缩后高度(AI 看到的)
170
236
  "original_size": f"{original_size/1024:.1f}KB",
171
237
  "compressed_size": f"{compressed_size/1024:.1f}KB",
172
238
  "saved_percent": f"{saved_percent:.0f}%",
173
239
  "message": f"📸 截图已保存: {final_path}\n"
174
- f"📐 屏幕尺寸: {screen_width}x{screen_height}\n"
175
- f"🖼️ 图片尺寸: {image_width}x{image_height}(AI 分析用)\n"
240
+ f"📐 原始尺寸: {original_img_width}x{original_img_height} → 压缩后: {image_width}x{image_height}\n"
176
241
  f"📦 已压缩: {original_size/1024:.0f}KB → {compressed_size/1024:.0f}KB (省 {saved_percent:.0f}%)\n"
177
- f"⚠️ 【重要】AI 返回的坐标需要转换!\n"
178
- f" 请使用 mobile_click_at_coords 并传入 image_width={image_width}, image_height={image_height}\n"
179
- f" 工具会自动将图片坐标转换为屏幕坐标"
242
+ f"⚠️ 【坐标转换】AI 返回坐标后,请传入:\n"
243
+ f" image_width={image_width}, image_height={image_height},\n"
244
+ f" original_img_width={original_img_width}, original_img_height={original_img_height}"
180
245
  }
246
+
247
+ # ========== 情况3:全屏不压缩截图 ==========
181
248
  else:
182
- # 不压缩,直接重命名临时文件
183
249
  if description:
184
250
  safe_desc = re.sub(r'[^\w\s-]', '', description).strip().replace(' ', '_')
185
251
  filename = f"screenshot_{platform}_{safe_desc}_{timestamp}.png"
@@ -189,19 +255,21 @@ class BasicMobileToolsLite:
189
255
  final_path = self.screenshot_dir / filename
190
256
  temp_path.rename(final_path)
191
257
 
192
- # 不压缩时,图片尺寸 = 屏幕尺寸
258
+ # 不压缩时,用截图实际尺寸(可能和 screen_width 不同)
193
259
  return {
194
260
  "success": True,
195
261
  "screenshot_path": str(final_path),
196
262
  "screen_width": screen_width,
197
263
  "screen_height": screen_height,
198
- "image_width": screen_width,
199
- "image_height": screen_height,
264
+ "original_img_width": img.width, # 截图实际尺寸
265
+ "original_img_height": img.height,
266
+ "image_width": img.width, # 未压缩,和原图一样
267
+ "image_height": img.height,
200
268
  "file_size": f"{original_size/1024:.1f}KB",
201
269
  "message": f"📸 截图已保存: {final_path}\n"
202
- f"📐 屏幕尺寸: {screen_width}x{screen_height}\n"
270
+ f"📐 截图尺寸: {img.width}x{img.height}\n"
203
271
  f"📦 文件大小: {original_size/1024:.0f}KB(未压缩)\n"
204
- f"💡 Cursor 分析图片后,返回的坐标可直接用于 mobile_click_at_coords"
272
+ f"💡 未压缩,坐标可直接使用"
205
273
  }
206
274
  except ImportError:
207
275
  # 如果没有 PIL,回退到原始方式(不压缩)
@@ -281,18 +349,24 @@ class BasicMobileToolsLite:
281
349
 
282
350
  # ==================== 点击操作 ====================
283
351
 
284
- def click_at_coords(self, x: int, y: int, image_width: int = 0, image_height: int = 0) -> Dict:
352
+ def click_at_coords(self, x: int, y: int, image_width: int = 0, image_height: int = 0,
353
+ crop_offset_x: int = 0, crop_offset_y: int = 0,
354
+ original_img_width: int = 0, original_img_height: int = 0) -> Dict:
285
355
  """点击坐标(核心功能,支持自动坐标转换)
286
356
 
287
357
  Args:
288
358
  x: X 坐标(来自截图分析或屏幕坐标)
289
359
  y: Y 坐标(来自截图分析或屏幕坐标)
290
- image_width: 截图的宽度(可选,传入后自动转换坐标)
291
- image_height: 截图的高度(可选,传入后自动转换坐标)
360
+ image_width: 压缩后图片宽度(AI 看到的图片尺寸)
361
+ image_height: 压缩后图片高度(AI 看到的图片尺寸)
362
+ crop_offset_x: 局部截图的 X 偏移量(局部截图时传入)
363
+ crop_offset_y: 局部截图的 Y 偏移量(局部截图时传入)
364
+ original_img_width: 截图原始宽度(压缩前的尺寸,用于精确转换)
365
+ original_img_height: 截图原始高度(压缩前的尺寸,用于精确转换)
292
366
 
293
367
  坐标转换说明:
294
- 如果截图被压缩过(如 1080→720),AI 返回的坐标是基于压缩图的。
295
- 传入 image_width/image_height 后,工具会自动将坐标转换为屏幕坐标。
368
+ 1. 全屏压缩截图:AI 坐标 → 原图坐标(基于 image/original_img 比例)
369
+ 2. 局部裁剪截图:AI 坐标 + 偏移量 = 屏幕坐标
296
370
  """
297
371
  try:
298
372
  # 获取屏幕尺寸
@@ -309,15 +383,30 @@ class BasicMobileToolsLite:
309
383
  screen_width = info.get('displayWidth', 0)
310
384
  screen_height = info.get('displayHeight', 0)
311
385
 
312
- # 🎯 坐标转换:如果传入了图片尺寸,将图片坐标转换为屏幕坐标
386
+ # 🎯 坐标转换
313
387
  original_x, original_y = x, y
314
388
  converted = False
315
- if image_width > 0 and image_height > 0 and screen_width > 0 and screen_height > 0:
316
- if image_width != screen_width or image_height != screen_height:
317
- # 按比例转换坐标
318
- x = int(x * screen_width / image_width)
319
- y = int(y * screen_height / image_height)
320
- converted = True
389
+ conversion_type = ""
390
+
391
+ # 情况1:局部裁剪截图 - 加上偏移量
392
+ if crop_offset_x > 0 or crop_offset_y > 0:
393
+ x = x + crop_offset_x
394
+ y = y + crop_offset_y
395
+ converted = True
396
+ conversion_type = "crop_offset"
397
+ # 情况2:全屏压缩截图 - 按比例转换到原图尺寸
398
+ elif image_width > 0 and image_height > 0:
399
+ # 优先使用 original_img_width/height(更精确)
400
+ # 如果没传,则用 screen_width/height(兼容旧版本)
401
+ target_width = original_img_width if original_img_width > 0 else screen_width
402
+ target_height = original_img_height if original_img_height > 0 else screen_height
403
+
404
+ if target_width > 0 and target_height > 0:
405
+ if image_width != target_width or image_height != target_height:
406
+ x = int(x * target_width / image_width)
407
+ y = int(y * target_height / image_height)
408
+ converted = True
409
+ conversion_type = "scale"
321
410
 
322
411
  # 执行点击
323
412
  if self._is_ios():
@@ -345,12 +434,19 @@ class BasicMobileToolsLite:
345
434
  )
346
435
 
347
436
  if converted:
348
- return {
349
- "success": True,
350
- "message": f"✅ 点击成功: ({x}, {y})\n"
351
- f" 📐 坐标已转换: ({original_x},{original_y}) → ({x},{y})\n"
352
- f" 🖼️ 图片尺寸: {image_width}x{image_height} → 屏幕: {screen_width}x{screen_height}"
353
- }
437
+ if conversion_type == "crop_offset":
438
+ return {
439
+ "success": True,
440
+ "message": f" 点击成功: ({x}, {y})\n"
441
+ f" 🔍 局部截图坐标转换: ({original_x},{original_y}) + 偏移({crop_offset_x},{crop_offset_y}) ({x},{y})"
442
+ }
443
+ else:
444
+ return {
445
+ "success": True,
446
+ "message": f"✅ 点击成功: ({x}, {y})\n"
447
+ f" 📐 坐标已转换: ({original_x},{original_y}) → ({x},{y})\n"
448
+ f" 🖼️ 图片尺寸: {image_width}x{image_height} → 屏幕: {screen_width}x{screen_height}"
449
+ }
354
450
  else:
355
451
  return {
356
452
  "success": True,
@@ -841,6 +937,99 @@ class BasicMobileToolsLite:
841
937
  except Exception as e:
842
938
  return [{"error": f"获取元素失败: {e}"}]
843
939
 
940
+ def close_popup(self) -> Dict:
941
+ """智能关闭弹窗
942
+
943
+ 策略:
944
+ 1. 从控件树找可能的关闭按钮(clickable=true,尺寸小,位置靠右上角)
945
+ 2. 如果找到,计算中心点并点击
946
+ 3. 如果没找到,返回需要视觉识别的提示
947
+ """
948
+ try:
949
+ # 获取屏幕尺寸
950
+ if self._is_ios():
951
+ return {"success": False, "message": "iOS 暂不支持,请使用截图+坐标点击"}
952
+
953
+ screen_width = self.client.u2.info.get('displayWidth', 720)
954
+ screen_height = self.client.u2.info.get('displayHeight', 1280)
955
+
956
+ # 获取控件树
957
+ xml_string = self.client.u2.dump_hierarchy()
958
+ elements = self.client.xml_parser.parse(xml_string)
959
+
960
+ # 找可能的关闭按钮
961
+ close_candidates = []
962
+ for elem in elements:
963
+ if not elem.get('clickable'):
964
+ continue
965
+
966
+ bounds = elem.get('bounds', '')
967
+ if not bounds:
968
+ continue
969
+
970
+ # 解析 bounds "[x1,y1][x2,y2]"
971
+ import re
972
+ match = re.match(r'\[(\d+),(\d+)\]\[(\d+),(\d+)\]', bounds)
973
+ if not match:
974
+ continue
975
+
976
+ x1, y1, x2, y2 = map(int, match.groups())
977
+ width = x2 - x1
978
+ height = y2 - y1
979
+ center_x = (x1 + x2) // 2
980
+ center_y = (y1 + y2) // 2
981
+
982
+ # 关闭按钮特征:尺寸小(30-100px),位置偏右上
983
+ if 30 <= width <= 100 and 30 <= height <= 100:
984
+ # 计算"右上角"得分(越靠右上越高)
985
+ right_score = center_x / screen_width # 0-1,越大越靠右
986
+ top_score = 1 - (center_y / screen_height) # 0-1,越大越靠上
987
+ # 只考虑屏幕上半部分、右半部分的按钮
988
+ if center_y < screen_height * 0.6 and center_x > screen_width * 0.5:
989
+ score = right_score * 0.5 + top_score * 0.5
990
+ close_candidates.append({
991
+ 'bounds': bounds,
992
+ 'center_x': center_x,
993
+ 'center_y': center_y,
994
+ 'width': width,
995
+ 'height': height,
996
+ 'score': score,
997
+ 'resource_id': elem.get('resource_id', ''),
998
+ 'text': elem.get('text', '')
999
+ })
1000
+
1001
+ if not close_candidates:
1002
+ return {
1003
+ "success": False,
1004
+ "message": "❌ 控件树未找到关闭按钮,请使用截图+视觉识别",
1005
+ "suggestion": "尝试局部截图右上角区域,用 crop_x, crop_y, crop_size 参数"
1006
+ }
1007
+
1008
+ # 按得分排序,取最可能的
1009
+ close_candidates.sort(key=lambda x: x['score'], reverse=True)
1010
+ best = close_candidates[0]
1011
+
1012
+ # 点击
1013
+ self.client.u2.click(best['center_x'], best['center_y'])
1014
+
1015
+ # 记录操作
1016
+ self._record_operation(
1017
+ 'close_popup',
1018
+ x=best['center_x'],
1019
+ y=best['center_y'],
1020
+ bounds=best['bounds']
1021
+ )
1022
+
1023
+ return {
1024
+ "success": True,
1025
+ "message": f"✅ 点击关闭按钮: ({best['center_x']}, {best['center_y']})",
1026
+ "bounds": best['bounds'],
1027
+ "candidates_count": len(close_candidates)
1028
+ }
1029
+
1030
+ except Exception as e:
1031
+ return {"success": False, "message": f"❌ 关闭弹窗失败: {e}"}
1032
+
844
1033
  def assert_text(self, text: str) -> Dict:
845
1034
  """检查页面是否包含文本"""
846
1035
  try:
@@ -549,3 +549,6 @@ class IOSClientWDA:
549
549
 
550
550
 
551
551
 
552
+
553
+
554
+
@@ -99,7 +99,8 @@ class MobileMCPServer:
99
99
 
100
100
  async def initialize(self):
101
101
  """延迟初始化设备连接"""
102
- if self._initialized:
102
+ # 如果已成功初始化,直接返回
103
+ if self._initialized and self.tools is not None:
103
104
  return
104
105
 
105
106
  platform = self._detect_platform()
@@ -110,13 +111,13 @@ class MobileMCPServer:
110
111
 
111
112
  self.client = MobileClient(platform=platform)
112
113
  self.tools = BasicMobileToolsLite(self.client)
114
+ self._initialized = True # 只在成功时标记
113
115
  print(f"📱 已连接到 {platform.upper()} 设备", file=sys.stderr)
114
116
  except Exception as e:
115
- print(f"⚠️ 设备连接失败: {e}", file=sys.stderr)
116
- self.client = type('MockClient', (), {'platform': platform})()
117
+ print(f"⚠️ 设备连接失败: {e},下次调用时将重试", file=sys.stderr)
118
+ self.client = None
117
119
  self.tools = None
118
-
119
- self._initialized = True
120
+ # 不设置 _initialized = True,下次调用会重试
120
121
 
121
122
  def _detect_platform(self) -> str:
122
123
  """自动检测设备平台"""
@@ -156,19 +157,26 @@ class MobileMCPServer:
156
157
  # ==================== 截图(视觉兜底)====================
157
158
  tools.append(Tool(
158
159
  name="mobile_take_screenshot",
159
- description="📸 截图(视觉定位用)。返回截图路径、屏幕尺寸和图片尺寸。\n\n"
160
+ description="📸 截图(支持全屏和局部裁剪)\n\n"
160
161
  "🎯 使用场景:\n"
161
162
  "- 游戏(Unity/Cocos)无法获取元素时\n"
162
163
  "- mobile_list_elements 返回空时\n"
163
164
  "- 需要确认页面状态时\n\n"
165
+ "🔍 【局部裁剪】精确识别小元素(如广告关闭按钮):\n"
166
+ " 1. 先全屏截图,AI 返回大概坐标 (600, 200)\n"
167
+ " 2. 再调用 crop_x=600, crop_y=200, crop_size=200 截取局部\n"
168
+ " 3. 局部图不压缩,AI 可精确识别\n"
169
+ " 4. 点击时传入 crop_offset_x/y 自动换算坐标\n\n"
164
170
  "⚠️ 【重要】截图会被压缩!\n"
165
- " - screen_width/screen_height = 原始屏幕尺寸\n"
166
- " - image_width/image_height = 压缩后图片尺寸(AI 看到的)\n"
167
- " - 点击时必须传入 image_width/image_height 让工具自动转换坐标!",
171
+ " - 全屏截图:点击时传 image_width/image_height 转换坐标\n"
172
+ " - 局部截图:点击时传 crop_offset_x/crop_offset_y 转换坐标",
168
173
  inputSchema={
169
174
  "type": "object",
170
175
  "properties": {
171
- "description": {"type": "string", "description": "截图描述(可选)"}
176
+ "description": {"type": "string", "description": "截图描述(可选)"},
177
+ "crop_x": {"type": "integer", "description": "局部裁剪中心 X 坐标(屏幕坐标,0 表示不裁剪)"},
178
+ "crop_y": {"type": "integer", "description": "局部裁剪中心 Y 坐标(屏幕坐标,0 表示不裁剪)"},
179
+ "crop_size": {"type": "integer", "description": "裁剪区域大小(推荐 200-400,0 表示不裁剪)"}
172
180
  },
173
181
  "required": []
174
182
  }
@@ -218,17 +226,22 @@ class MobileMCPServer:
218
226
  "- 游戏(Unity/Cocos)无法获取元素\n"
219
227
  "- mobile_list_elements 返回空\n"
220
228
  "- 元素没有 id 和 text\n\n"
221
- "⚠️ 【重要】如果坐标来自压缩截图,必须传入 image_width 和 image_height!\n"
222
- " 截图返回的 image_width/image_height 字段就是需要传入的值。\n"
223
- " 工具会自动将图片坐标转换为屏幕坐标。\n\n"
224
- " 自动记录百分比坐标,生成脚本时会转换为跨分辨率兼容的百分比定位",
229
+ "⚠️ 【坐标转换】截图返回的参数直接传入:\n"
230
+ " - image_width/image_height: 压缩后尺寸(AI 看到的)\n"
231
+ " - original_img_width/original_img_height: 原图尺寸(用于转换)\n"
232
+ " - crop_offset_x/crop_offset_y: 局部截图偏移\n\n"
233
+ "✅ 自动记录百分比坐标,生成脚本时转换为跨分辨率兼容的百分比定位",
225
234
  inputSchema={
226
235
  "type": "object",
227
236
  "properties": {
228
- "x": {"type": "number", "description": "X 坐标(像素,来自截图分析或屏幕坐标)"},
229
- "y": {"type": "number", "description": "Y 坐标(像素,来自截图分析或屏幕坐标)"},
230
- "image_width": {"type": "number", "description": "截图的宽度(可选,传入后自动转换坐标)"},
231
- "image_height": {"type": "number", "description": "截图的高度(可选,传入后自动转换坐标)"}
237
+ "x": {"type": "number", "description": "X 坐标(来自 AI 分析截图)"},
238
+ "y": {"type": "number", "description": "Y 坐标(来自 AI 分析截图)"},
239
+ "image_width": {"type": "number", "description": "压缩后图片宽度(截图返回的 image_width)"},
240
+ "image_height": {"type": "number", "description": "压缩后图片高度(截图返回的 image_height)"},
241
+ "original_img_width": {"type": "number", "description": "原图宽度(截图返回的 original_img_width)"},
242
+ "original_img_height": {"type": "number", "description": "原图高度(截图返回的 original_img_height)"},
243
+ "crop_offset_x": {"type": "number", "description": "局部截图 X 偏移(裁剪截图时传入)"},
244
+ "crop_offset_y": {"type": "number", "description": "局部截图 Y 偏移(裁剪截图时传入)"}
232
245
  },
233
246
  "required": ["x", "y"]
234
247
  }
@@ -374,6 +387,22 @@ class MobileMCPServer:
374
387
  ))
375
388
 
376
389
  # ==================== 辅助工具 ====================
390
+ tools.append(Tool(
391
+ name="mobile_close_popup",
392
+ description="""🚫 智能关闭弹窗(推荐!)
393
+
394
+ 自动从控件树识别关闭按钮并点击。
395
+
396
+ 🎯 识别策略:
397
+ 1. 找 clickable=true 且尺寸小(30-100px)的元素
398
+ 2. 位置在屏幕右上角区域
399
+ 3. 计算 bounds 中心点一次点击
400
+
401
+ ✅ 优势:比视觉识别更精准,一次成功率高
402
+ ❌ 限制:如果关闭按钮是图片的一部分(无独立控件),需要用截图+坐标点击""",
403
+ inputSchema={"type": "object", "properties": {}, "required": []}
404
+ ))
405
+
377
406
  tools.append(Tool(
378
407
  name="mobile_assert_text",
379
408
  description="✅ 检查页面是否包含指定文本。用于验证操作结果。",
@@ -444,7 +473,12 @@ class MobileMCPServer:
444
473
  try:
445
474
  # 截图
446
475
  if name == "mobile_take_screenshot":
447
- result = self.tools.take_screenshot(arguments.get("description", ""))
476
+ result = self.tools.take_screenshot(
477
+ description=arguments.get("description", ""),
478
+ crop_x=arguments.get("crop_x", 0),
479
+ crop_y=arguments.get("crop_y", 0),
480
+ crop_size=arguments.get("crop_size", 0)
481
+ )
448
482
  return [TextContent(type="text", text=self.format_response(result))]
449
483
 
450
484
  elif name == "mobile_get_screen_size":
@@ -457,7 +491,11 @@ class MobileMCPServer:
457
491
  arguments["x"],
458
492
  arguments["y"],
459
493
  arguments.get("image_width", 0),
460
- arguments.get("image_height", 0)
494
+ arguments.get("image_height", 0),
495
+ arguments.get("crop_offset_x", 0),
496
+ arguments.get("crop_offset_y", 0),
497
+ arguments.get("original_img_width", 0),
498
+ arguments.get("original_img_height", 0)
461
499
  )
462
500
  return [TextContent(type="text", text=self.format_response(result))]
463
501
 
@@ -522,6 +560,10 @@ class MobileMCPServer:
522
560
  result = self.tools.list_elements()
523
561
  return [TextContent(type="text", text=self.format_response(result))]
524
562
 
563
+ elif name == "mobile_close_popup":
564
+ result = self.tools.close_popup()
565
+ return [TextContent(type="text", text=self.format_response(result))]
566
+
525
567
  elif name == "mobile_assert_text":
526
568
  result = self.tools.assert_text(arguments["text"])
527
569
  return [TextContent(type="text", text=self.format_response(result))]
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.1
2
2
  Name: mobile-mcp-ai
3
- Version: 2.3.4
3
+ Version: 2.3.6
4
4
  Summary: 移动端自动化 MCP Server - 支持 Android/iOS,AI 功能可选(基础工具不需要 AI)
5
5
  Home-page: https://github.com/test111ddff-hash/mobile-mcp-ai
6
6
  Author: douzi
@@ -31,20 +31,6 @@ Provides-Extra: ai
31
31
  Requires-Dist: dashscope>=1.10.0; extra == "ai"
32
32
  Requires-Dist: openai>=1.0.0; extra == "ai"
33
33
  Requires-Dist: anthropic>=0.3.0; extra == "ai"
34
- Provides-Extra: test
35
- Requires-Dist: pytest>=8.0.0; extra == "test"
36
- Requires-Dist: pytest-asyncio>=0.21.0; extra == "test"
37
- Requires-Dist: allure-pytest>=2.13.0; extra == "test"
38
- Provides-Extra: dev
39
- Requires-Dist: pytest>=8.0.0; extra == "dev"
40
- Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
41
- Requires-Dist: twine>=4.0.0; extra == "dev"
42
- Requires-Dist: build>=0.10.0; extra == "dev"
43
- Provides-Extra: ios
44
- Requires-Dist: Appium-Python-Client>=3.0.0; extra == "ios"
45
- Provides-Extra: h5
46
- Requires-Dist: Appium-Python-Client>=3.0.0; extra == "h5"
47
- Requires-Dist: selenium>=4.0.0; extra == "h5"
48
34
  Provides-Extra: all
49
35
  Requires-Dist: dashscope>=1.10.0; extra == "all"
50
36
  Requires-Dist: openai>=1.0.0; extra == "all"
@@ -54,19 +40,20 @@ Requires-Dist: selenium>=4.0.0; extra == "all"
54
40
  Requires-Dist: pytest>=8.0.0; extra == "all"
55
41
  Requires-Dist: pytest-asyncio>=0.21.0; extra == "all"
56
42
  Requires-Dist: allure-pytest>=2.13.0; extra == "all"
57
- Dynamic: author
58
- Dynamic: author-email
59
- Dynamic: classifier
60
- Dynamic: description
61
- Dynamic: description-content-type
62
- Dynamic: home-page
63
- Dynamic: keywords
64
- Dynamic: license-file
65
- Dynamic: project-url
66
- Dynamic: provides-extra
67
- Dynamic: requires-dist
68
- Dynamic: requires-python
69
- Dynamic: summary
43
+ Provides-Extra: dev
44
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
45
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
46
+ Requires-Dist: twine>=4.0.0; extra == "dev"
47
+ Requires-Dist: build>=0.10.0; extra == "dev"
48
+ Provides-Extra: h5
49
+ Requires-Dist: Appium-Python-Client>=3.0.0; extra == "h5"
50
+ Requires-Dist: selenium>=4.0.0; extra == "h5"
51
+ Provides-Extra: ios
52
+ Requires-Dist: Appium-Python-Client>=3.0.0; extra == "ios"
53
+ Provides-Extra: test
54
+ Requires-Dist: pytest>=8.0.0; extra == "test"
55
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == "test"
56
+ Requires-Dist: allure-pytest>=2.13.0; extra == "test"
70
57
 
71
58
  # 📱 Mobile MCP AI
72
59
 
@@ -1,10 +1,10 @@
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=uq2af3L2ru5ZcO_zKuG2yy3iXKvThkd0HWh-qEsnkkI,50496
4
+ mobile_mcp/core/basic_tools_lite.py,sha256=uqd-YG9vTxQmJEVmio_z_yj8LOeZnhvRP7YxvSueuZs,59803
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=St6nOeXW0wiolLm6iQ2Etuwr1cvzwnlnYwGUxA3-JD4,18752
7
+ mobile_mcp/core/ios_client_wda.py,sha256=KudSbWTy-0l8OMQjXpsDYAiL59w7HVrw-i7ApfExJLA,18755
8
8
  mobile_mcp/core/ios_device_manager_wda.py,sha256=A44glqI-24un7qST-E3w6BQD8mV92YVUbxy4rLlTScY,11264
9
9
  mobile_mcp/core/mobile_client.py,sha256=bno3HvU-QSAC3G4TnoFngTxqXeu-ZP5rGlEWdWh8jOo,62570
10
10
  mobile_mcp/core/utils/__init__.py,sha256=RhMMsPszmEn8Q8GoNufypVSHJxyM9lio9U6jjpnuoPI,378
@@ -12,14 +12,14 @@ mobile_mcp/core/utils/logger.py,sha256=XXQAHUwT1jc70pq_tYFmL6f_nKrFlYm3hcgl-5RYR
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=Gh5LF1PbgVjw6-DmrPdk17MAsd7q2H6kWgOQ7RPQNew,25338
15
+ mobile_mcp/mcp_tools/mcp_server.py,sha256=OUpQ53A22pOhbeHRuT6oibEQ_Ycb4PzsIz7u5Ynk7wA,27960
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.4.dist-info/licenses/LICENSE,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
21
- mobile_mcp_ai-2.3.4.dist-info/METADATA,sha256=O5_qo6AvaIWGQzWYDtYF-_OVeImU_FkJDMzXSihxupY,9705
22
- mobile_mcp_ai-2.3.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
23
- mobile_mcp_ai-2.3.4.dist-info/entry_points.txt,sha256=KB_FglozgPHBprSM1vFbIzGyheFuHFmGanscRdMJ_8A,68
24
- mobile_mcp_ai-2.3.4.dist-info/top_level.txt,sha256=lLm6YpbTv855Lbh8BIA0rPxhybIrvYUzMEk9OErHT94,11
25
- mobile_mcp_ai-2.3.4.dist-info/RECORD,,
20
+ mobile_mcp_ai-2.3.6.dist-info/LICENSE,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
21
+ mobile_mcp_ai-2.3.6.dist-info/METADATA,sha256=sJOSvqLIeuVVfxEE0mR_AyuT37uzBnnCHwM1EV3k_1g,9423
22
+ mobile_mcp_ai-2.3.6.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
23
+ mobile_mcp_ai-2.3.6.dist-info/entry_points.txt,sha256=KB_FglozgPHBprSM1vFbIzGyheFuHFmGanscRdMJ_8A,68
24
+ mobile_mcp_ai-2.3.6.dist-info/top_level.txt,sha256=lLm6YpbTv855Lbh8BIA0rPxhybIrvYUzMEk9OErHT94,11
25
+ mobile_mcp_ai-2.3.6.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: bdist_wheel (0.45.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5