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.
@@ -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,29 +114,87 @@ 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"
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
- # 第4步:生成最终文件名(JPEG 格式)
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
- # 第5步:保存为 JPEG(PNG 可能有透明通道,需转 RGB)
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"💡 Cursor 分析图片后,返回的坐标可直接用于 mobile_click_at_coords\n"
170
- f"💡 或使用 mobile_click_by_percent 进行跨设备兼容点击"
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) -> Dict:
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
- return {
307
- "success": True,
308
- "message": f"✅ 点击成功: ({x}, {y}) [相对位置: {x_percent}%, {y_percent}%]"
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:
@@ -549,3 +549,4 @@ class IOSClientWDA:
549
549
 
550
550
 
551
551
 
552
+
@@ -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
- try:
831
- from .smart_app_launcher import SmartAppLauncher
832
- launcher = SmartAppLauncher(self)
833
- # 优化:快速模式,最多3
834
- smart_wait_time = min(wait_time, 3)
835
-
836
- # 🎯 从环境变量读取是否自动关闭广告(默认True)
837
- import os
838
- auto_close_ads = os.environ.get('AUTO_CLOSE_ADS', 'true').lower() in ['true', '1', 'yes']
839
-
840
- result = await launcher.launch_with_smart_wait(
841
- package_name,
842
- max_wait=smart_wait_time,
843
- auto_close_ads=auto_close_ads
844
- )
845
-
846
- # 打印截图路径(供Cursor AI查看验证)
847
- if result.get('screenshot_path'):
848
- print(f"\n📸 启动截图已保存: {result['screenshot_path']}", file=sys.stderr)
849
- print(f"💡 提示: 请查看截图确认App是否已正确进入主页", file=sys.stderr)
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="📸 截图(视觉定位用)。返回截图路径和屏幕尺寸。\n\n"
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
- " 自动记录百分比坐标,生成脚本时会转换为跨分辨率兼容的百分比定位\n"
219
- "💡 录制测试脚本时,请先调用 mobile_list_elements 尝试获取元素!",
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(arguments.get("description", ""))
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(arguments["x"], arguments["y"])
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mobile-mcp-ai
3
- Version: 2.3.3
3
+ Version: 2.3.5
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
@@ -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=7C2Z87vG69fqmR0FiOC3qxshdo9Sjl5jhADiIFTC8uk,52023
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=St6nOeXW0wiolLm6iQ2Etuwr1cvzwnlnYwGUxA3-JD4,18752
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=lmscB8-osGh_ngVG9XdNGe6fNJLsAwNI5qUSobE-Ln8,62964
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=pAph7z6ERezoK3aAVqc8OkZ7sPllQhPPsIW1HlDC12g,25892
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.3.dist-info/licenses/LICENSE,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
21
- mobile_mcp_ai-2.3.3.dist-info/METADATA,sha256=Ur70uKGsYUu_Ibg6jZ9XLpdx3bod8Ir_pLXSoGe6KD4,9705
22
- mobile_mcp_ai-2.3.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
23
- mobile_mcp_ai-2.3.3.dist-info/entry_points.txt,sha256=KB_FglozgPHBprSM1vFbIzGyheFuHFmGanscRdMJ_8A,68
24
- mobile_mcp_ai-2.3.3.dist-info/top_level.txt,sha256=lLm6YpbTv855Lbh8BIA0rPxhybIrvYUzMEk9OErHT94,11
25
- mobile_mcp_ai-2.3.3.dist-info/RECORD,,
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,,