mobile-mcp-ai 2.6.1__py3-none-any.whl → 2.6.2__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.
@@ -794,11 +794,19 @@ class BasicMobileToolsLite:
794
794
 
795
795
  is_container = any(kw in class_name for kw in ['Layout', 'View', 'Dialog', 'Card', 'Frame'])
796
796
  area_ratio = p_area / screen_area if screen_area > 0 else 0
797
- is_not_fullscreen = (p_width < screen_width * 0.99 or p_height < screen_height * 0.95)
798
- # 放宽面积范围:5% - 95%
799
- is_reasonable_size = 0.05 < area_ratio < 0.95
800
797
 
801
- if is_container and is_not_fullscreen and is_reasonable_size and py1 > 30:
798
+ # 弹窗特征判断(更严格,排除主要内容区域):
799
+ # 1. 不是全屏(宽度和高度都要小于屏幕的95%)
800
+ is_not_fullscreen = (p_width < screen_width * 0.95 and p_height < screen_height * 0.95)
801
+ # 2. 面积范围:10% - 70%(排除主要内容区域,通常占80%+)
802
+ is_reasonable_size = 0.10 < area_ratio < 0.70
803
+ # 3. 不在屏幕左边缘(排除从x=0开始的主要内容容器)
804
+ is_not_at_left_edge = px1 > screen_width * 0.05
805
+ # 4. 高度不能占据屏幕的大部分(排除主要内容区域)
806
+ height_ratio = p_height / screen_height if screen_height > 0 else 0
807
+ is_not_main_content = height_ratio < 0.85
808
+
809
+ if is_container and is_not_fullscreen and is_reasonable_size and is_not_at_left_edge and is_not_main_content and py1 > 30:
802
810
  if popup_bounds is None or p_area > (popup_bounds[2] - popup_bounds[0]) * (popup_bounds[3] - popup_bounds[1]):
803
811
  popup_bounds = (px1, py1, px2, py2)
804
812
 
@@ -1200,8 +1208,16 @@ class BasicMobileToolsLite:
1200
1208
  except Exception as e:
1201
1209
  return {"success": False, "message": f"❌ 百分比点击失败: {e}"}
1202
1210
 
1203
- def click_by_text(self, text: str, timeout: float = 3.0) -> Dict:
1204
- """通过文本点击 - 先查 XML 树,再精准匹配"""
1211
+ def click_by_text(self, text: str, timeout: float = 3.0, position: Optional[str] = None) -> Dict:
1212
+ """通过文本点击 - 先查 XML 树,再精准匹配
1213
+
1214
+ Args:
1215
+ text: 元素的文本内容
1216
+ timeout: 超时时间
1217
+ position: 位置信息,当有多个相同文案时使用。支持:
1218
+ - 垂直方向: "top"/"upper"/"上", "bottom"/"lower"/"下", "middle"/"center"/"中"
1219
+ - 水平方向: "left"/"左", "right"/"右", "center"/"中"
1220
+ """
1205
1221
  try:
1206
1222
  if self._is_ios():
1207
1223
  ios_client = self._get_ios_client()
@@ -1217,14 +1233,24 @@ class BasicMobileToolsLite:
1217
1233
  return {"success": False, "message": f"❌ 文本不存在: {text}"}
1218
1234
  else:
1219
1235
  # 🔍 先查 XML 树,找到元素及其属性
1220
- found_elem = self._find_element_in_tree(text)
1236
+ found_elem = self._find_element_in_tree(text, position=position)
1221
1237
 
1222
1238
  if found_elem:
1223
1239
  attr_type = found_elem['attr_type']
1224
1240
  attr_value = found_elem['attr_value']
1225
1241
  bounds = found_elem.get('bounds')
1226
1242
 
1227
- # 根据找到的属性类型,使用对应的选择器
1243
+ # 如果有位置参数,直接使用坐标点击(避免 u2 选择器匹配到错误的元素)
1244
+ if position and bounds:
1245
+ x = (bounds[0] + bounds[2]) // 2
1246
+ y = (bounds[1] + bounds[3]) // 2
1247
+ self.client.u2.click(x, y)
1248
+ time.sleep(0.3)
1249
+ position_info = f" ({position})" if position else ""
1250
+ self._record_operation('click', element=text, x=x, y=y, ref=f"coords:{x},{y}")
1251
+ return {"success": True, "message": f"✅ 点击成功(坐标定位): '{text}'{position_info} @ ({x},{y})"}
1252
+
1253
+ # 没有位置参数时,使用选择器定位
1228
1254
  if attr_type == 'text':
1229
1255
  elem = self.client.u2(text=attr_value)
1230
1256
  elif attr_type == 'textContains':
@@ -1239,8 +1265,9 @@ class BasicMobileToolsLite:
1239
1265
  if elem and elem.exists(timeout=1):
1240
1266
  elem.click()
1241
1267
  time.sleep(0.3)
1268
+ position_info = f" ({position})" if position else ""
1242
1269
  self._record_operation('click', element=text, ref=f"{attr_type}:{attr_value}")
1243
- return {"success": True, "message": f"✅ 点击成功({attr_type}): '{text}'"}
1270
+ return {"success": True, "message": f"✅ 点击成功({attr_type}): '{text}'{position_info}"}
1244
1271
 
1245
1272
  # 如果选择器失败,用坐标兜底
1246
1273
  if bounds:
@@ -1248,24 +1275,37 @@ class BasicMobileToolsLite:
1248
1275
  y = (bounds[1] + bounds[3]) // 2
1249
1276
  self.client.u2.click(x, y)
1250
1277
  time.sleep(0.3)
1278
+ position_info = f" ({position})" if position else ""
1251
1279
  self._record_operation('click', element=text, x=x, y=y, ref=f"coords:{x},{y}")
1252
- return {"success": True, "message": f"✅ 点击成功(坐标兜底): '{text}' @ ({x},{y})"}
1280
+ return {"success": True, "message": f"✅ 点击成功(坐标兜底): '{text}'{position_info} @ ({x},{y})"}
1253
1281
 
1254
1282
  return {"success": False, "message": f"❌ 文本不存在: {text}"}
1255
1283
  except Exception as e:
1256
1284
  return {"success": False, "message": f"❌ 点击失败: {e}"}
1257
1285
 
1258
- def _find_element_in_tree(self, text: str) -> Optional[Dict]:
1259
- """在 XML 树中查找包含指定文本的元素"""
1286
+ def _find_element_in_tree(self, text: str, position: Optional[str] = None) -> Optional[Dict]:
1287
+ """在 XML 树中查找包含指定文本的元素,优先返回可点击的元素
1288
+
1289
+ Args:
1290
+ text: 要查找的文本
1291
+ position: 位置信息,用于在有多个相同文案时筛选
1292
+ """
1260
1293
  try:
1261
1294
  xml = self.client.u2.dump_hierarchy(compressed=False)
1262
1295
  import xml.etree.ElementTree as ET
1263
1296
  root = ET.fromstring(xml)
1264
1297
 
1298
+ # 获取屏幕尺寸
1299
+ screen_width, screen_height = self.client.u2.window_size()
1300
+
1301
+ # 存储所有匹配的元素(包括不可点击的)
1302
+ matched_elements = []
1303
+
1265
1304
  for elem in root.iter():
1266
1305
  elem_text = elem.attrib.get('text', '')
1267
1306
  elem_desc = elem.attrib.get('content-desc', '')
1268
1307
  bounds_str = elem.attrib.get('bounds', '')
1308
+ clickable = elem.attrib.get('clickable', 'false').lower() == 'true'
1269
1309
 
1270
1310
  # 解析 bounds
1271
1311
  bounds = None
@@ -1275,24 +1315,108 @@ class BasicMobileToolsLite:
1275
1315
  if len(match) == 4:
1276
1316
  bounds = [int(x) for x in match]
1277
1317
 
1318
+ # 判断是否匹配
1319
+ is_match = False
1320
+ attr_type = None
1321
+ attr_value = None
1322
+
1278
1323
  # 精确匹配 text
1279
1324
  if elem_text == text:
1280
- return {'attr_type': 'text', 'attr_value': text, 'bounds': bounds}
1281
-
1325
+ is_match = True
1326
+ attr_type = 'text'
1327
+ attr_value = text
1282
1328
  # 精确匹配 content-desc
1283
- if elem_desc == text:
1284
- return {'attr_type': 'description', 'attr_value': text, 'bounds': bounds}
1285
-
1329
+ elif elem_desc == text:
1330
+ is_match = True
1331
+ attr_type = 'description'
1332
+ attr_value = text
1286
1333
  # 模糊匹配 text
1287
- if text in elem_text:
1288
- return {'attr_type': 'textContains', 'attr_value': text, 'bounds': bounds}
1289
-
1334
+ elif text in elem_text:
1335
+ is_match = True
1336
+ attr_type = 'textContains'
1337
+ attr_value = text
1290
1338
  # 模糊匹配 content-desc
1291
- if text in elem_desc:
1292
- return {'attr_type': 'descriptionContains', 'attr_value': text, 'bounds': bounds}
1339
+ elif text in elem_desc:
1340
+ is_match = True
1341
+ attr_type = 'descriptionContains'
1342
+ attr_value = text
1343
+
1344
+ if is_match and bounds:
1345
+ # 计算元素的中心点坐标
1346
+ center_x = (bounds[0] + bounds[2]) / 2
1347
+ center_y = (bounds[1] + bounds[3]) / 2
1348
+
1349
+ matched_elements.append({
1350
+ 'attr_type': attr_type,
1351
+ 'attr_value': attr_value,
1352
+ 'bounds': bounds,
1353
+ 'clickable': clickable,
1354
+ 'center_x': center_x,
1355
+ 'center_y': center_y
1356
+ })
1357
+
1358
+ if not matched_elements:
1359
+ return None
1360
+
1361
+ # 如果有位置信息,根据位置筛选
1362
+ if position and len(matched_elements) > 1:
1363
+ position_lower = position.lower()
1364
+
1365
+ # 根据位置信息排序
1366
+ if position_lower in ['top', 'upper', '上', '上方']:
1367
+ # 选择 y 坐标最小的(最上面的)
1368
+ matched_elements = sorted(matched_elements, key=lambda x: x['center_y'])
1369
+ elif position_lower in ['bottom', 'lower', '下', '下方', '底部']:
1370
+ # 选择 y 坐标最大的(最下面的)
1371
+ matched_elements = sorted(matched_elements, key=lambda x: x['center_y'], reverse=True)
1372
+ elif position_lower in ['left', '左', '左侧']:
1373
+ # 选择 x 坐标最小的(最左边的)
1374
+ matched_elements = sorted(matched_elements, key=lambda x: x['center_x'])
1375
+ elif position_lower in ['right', '右', '右侧']:
1376
+ # 选择 x 坐标最大的(最右边的)
1377
+ matched_elements = sorted(matched_elements, key=lambda x: x['center_x'], reverse=True)
1378
+ elif position_lower in ['middle', 'center', '中', '中间']:
1379
+ # 选择最接近屏幕中心的
1380
+ screen_mid_x = screen_width / 2
1381
+ screen_mid_y = screen_height / 2
1382
+ matched_elements = sorted(
1383
+ matched_elements,
1384
+ key=lambda x: abs(x['center_x'] - screen_mid_x) + abs(x['center_y'] - screen_mid_y)
1385
+ )
1386
+
1387
+ # 如果有位置信息,优先返回排序后的第一个元素(最符合位置要求的)
1388
+ # 如果没有位置信息,优先返回可点击的元素
1389
+ if position and matched_elements:
1390
+ # 有位置信息时,直接返回排序后的第一个(最符合位置要求的)
1391
+ first_match = matched_elements[0]
1392
+ return {
1393
+ 'attr_type': first_match['attr_type'],
1394
+ 'attr_value': first_match['attr_value'],
1395
+ 'bounds': first_match['bounds']
1396
+ }
1397
+
1398
+ # 没有位置信息时,优先返回可点击的元素
1399
+ for match in matched_elements:
1400
+ if match['clickable']:
1401
+ return {
1402
+ 'attr_type': match['attr_type'],
1403
+ 'attr_value': match['attr_value'],
1404
+ 'bounds': match['bounds']
1405
+ }
1406
+
1407
+ # 如果没有可点击的元素,直接返回第一个匹配元素的 bounds(使用坐标点击)
1408
+ if matched_elements:
1409
+ first_match = matched_elements[0]
1410
+ return {
1411
+ 'attr_type': first_match['attr_type'],
1412
+ 'attr_value': first_match['attr_value'],
1413
+ 'bounds': first_match['bounds']
1414
+ }
1293
1415
 
1294
1416
  return None
1295
- except Exception:
1417
+ except Exception as e:
1418
+ import traceback
1419
+ traceback.print_exc()
1296
1420
  return None
1297
1421
 
1298
1422
  def click_by_id(self, resource_id: str, index: int = 0) -> Dict:
@@ -2349,7 +2473,6 @@ class BasicMobileToolsLite:
2349
2473
  for elem in root.iter():
2350
2474
  text = elem.attrib.get('text', '')
2351
2475
  content_desc = elem.attrib.get('content-desc', '')
2352
- resource_id = elem.attrib.get('resource-id', '')
2353
2476
  bounds_str = elem.attrib.get('bounds', '')
2354
2477
  class_name = elem.attrib.get('class', '')
2355
2478
  clickable = elem.attrib.get('clickable', 'false') == 'true'
@@ -2384,13 +2507,6 @@ class BasicMobileToolsLite:
2384
2507
  score = 90
2385
2508
  reason = f"描述='{content_desc}'"
2386
2509
 
2387
- # 策略2.5:resource-id 包含关闭关键词(如 close_icon, ad_close 等)
2388
- elif resource_id and any(kw in resource_id.lower() for kw in ['close', 'dismiss', 'skip', 'cancel']):
2389
- score = 95
2390
- # 提取简短的 id 名
2391
- short_id = resource_id.split('/')[-1] if '/' in resource_id else resource_id
2392
- reason = f"resource-id='{short_id}'"
2393
-
2394
2510
  # 策略3:小尺寸的 clickable 元素(可能是 X 图标)
2395
2511
  elif clickable:
2396
2512
  min_size = max(20, int(screen_width * 0.03))
@@ -2425,9 +2541,7 @@ class BasicMobileToolsLite:
2425
2541
  'center_y': center_y,
2426
2542
  'x_percent': x_percent,
2427
2543
  'y_percent': y_percent,
2428
- 'size': f"{width}x{height}",
2429
- 'resource_id': resource_id,
2430
- 'text': text
2544
+ 'size': f"{width}x{height}"
2431
2545
  })
2432
2546
 
2433
2547
  if not candidates:
@@ -2453,16 +2567,7 @@ class BasicMobileToolsLite:
2453
2567
  candidates.sort(key=lambda x: x['score'], reverse=True)
2454
2568
  best = candidates[0]
2455
2569
 
2456
- # 生成推荐的点击命令(优先使用 resource-id)
2457
- if best.get('resource_id'):
2458
- short_id = best['resource_id'].split('/')[-1] if '/' in best['resource_id'] else best['resource_id']
2459
- click_cmd = f"mobile_click_by_id('{best['resource_id']}')"
2460
- elif best.get('text') and best['text'] in ['×', 'X', 'x', '关闭', '取消', '跳过', '知道了']:
2461
- click_cmd = f"mobile_click_by_text('{best['text']}')"
2462
- else:
2463
- click_cmd = f"mobile_click_by_percent({best['x_percent']}, {best['y_percent']})"
2464
-
2465
- result = {
2570
+ return {
2466
2571
  "success": True,
2467
2572
  "message": f"✅ 找到可能的关闭按钮",
2468
2573
  "best_candidate": {
@@ -2473,7 +2578,7 @@ class BasicMobileToolsLite:
2473
2578
  "size": best['size'],
2474
2579
  "score": best['score']
2475
2580
  },
2476
- "click_command": click_cmd,
2581
+ "click_command": f"mobile_click_by_percent({best['x_percent']}, {best['y_percent']})",
2477
2582
  "other_candidates": [
2478
2583
  {"reason": c['reason'], "percent": f"({c['x_percent']}%, {c['y_percent']}%)", "score": c['score']}
2479
2584
  for c in candidates[1:4]
@@ -2481,14 +2586,6 @@ class BasicMobileToolsLite:
2481
2586
  "screen_size": {"width": screen_width, "height": screen_height}
2482
2587
  }
2483
2588
 
2484
- # 如果有 resource-id,额外提供
2485
- if best.get('resource_id'):
2486
- result["best_candidate"]["resource_id"] = best['resource_id']
2487
- if best.get('text'):
2488
- result["best_candidate"]["text"] = best['text']
2489
-
2490
- return result
2491
-
2492
2589
  except Exception as e:
2493
2590
  return {"success": False, "message": f"❌ 查找关闭按钮失败: {e}"}
2494
2591
 
@@ -2553,19 +2650,24 @@ class BasicMobileToolsLite:
2553
2650
  area = width * height
2554
2651
  screen_area = screen_width * screen_height
2555
2652
 
2556
- # 弹窗容器特征:
2557
- # 1. 面积在屏幕的 10%-90% 之间(非全屏)
2558
- # 2. 宽度或高度不等于屏幕尺寸
2653
+ # 弹窗容器特征(更严格,排除主要内容区域):
2654
+ # 1. 面积在屏幕的 10%-70% 之间(排除主要内容区域,通常占80%+)
2655
+ # 2. 宽度和高度都要小于屏幕的95%(不是全屏)
2559
2656
  # 3. 是容器类型(Layout/View/Dialog)
2657
+ # 4. 不在屏幕左边缘(排除从x=0开始的主要内容容器)
2658
+ # 5. 高度不能占据屏幕的大部分(排除主要内容区域)
2560
2659
  is_container = any(kw in class_name for kw in ['Layout', 'View', 'Dialog', 'Card', 'Container'])
2561
2660
  area_ratio = area / screen_area
2562
- is_not_fullscreen = (width < screen_width * 0.98 or height < screen_height * 0.98)
2563
- is_reasonable_size = 0.08 < area_ratio < 0.9
2661
+ is_not_fullscreen = (width < screen_width * 0.95 and height < screen_height * 0.95)
2662
+ is_reasonable_size = 0.10 < area_ratio < 0.70
2663
+ is_not_at_left_edge = x1 > screen_width * 0.05
2664
+ height_ratio = height / screen_height if screen_height > 0 else 0
2665
+ is_not_main_content = height_ratio < 0.85
2564
2666
 
2565
2667
  # 排除状态栏区域(y1 通常很小)
2566
2668
  is_below_statusbar = y1 > 50
2567
2669
 
2568
- if is_container and is_not_fullscreen and is_reasonable_size and is_below_statusbar:
2670
+ if is_container and is_not_fullscreen and is_reasonable_size and is_not_at_left_edge and is_not_main_content and is_below_statusbar:
2569
2671
  popup_containers.append({
2570
2672
  'bounds': (x1, y1, x2, y2),
2571
2673
  'bounds_str': bounds_str,
@@ -2997,8 +3099,8 @@ class BasicMobileToolsLite:
2997
3099
  f"生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
2998
3100
  "",
2999
3101
  "定位策略(按优先级):",
3000
- "1. ID 定位 - 最稳定,跨设备兼容",
3001
- "2. 文本定位 - 稳定,跨设备兼容",
3102
+ "1. 文本定位 - 最稳定,跨设备兼容",
3103
+ "2. ID 定位 - 稳定,跨设备兼容",
3002
3104
  "3. 百分比定位 - 跨分辨率兼容(坐标自动转换)",
3003
3105
  f'"""',
3004
3106
  "import time",
@@ -3113,21 +3215,21 @@ class BasicMobileToolsLite:
3113
3215
  is_coords_ref = ref.startswith('coords_') or ref.startswith('coords:')
3114
3216
  is_percent_ref = ref.startswith('percent_')
3115
3217
 
3116
- # 优先级:ID > 文本 > 百分比 > 坐标(兜底)
3117
- if ref and (':id/' in ref or ref.startswith('com.')):
3118
- # 1️⃣ 使用 resource-id(最稳定)
3119
- script_lines.append(f" # 步骤{step_num}: 点击元素 (ID定位,最稳定)")
3120
- script_lines.append(f" safe_click(d, d(resourceId='{ref}'))")
3121
- elif ref and not is_coords_ref and not is_percent_ref and ':' not in ref:
3122
- # 2️⃣ 使用文本(稳定)- 排除 "text:xxx" 等带冒号的格式
3123
- script_lines.append(f" # 步骤{step_num}: 点击文本 '{ref}' (文本定位)")
3218
+ # 优先级:文本 > ID > 百分比 > 坐标(兜底)
3219
+ if ref and not is_coords_ref and not is_percent_ref and ':' not in ref:
3220
+ # 1️⃣ 使用文本(最稳定,优先)- 排除 "text:xxx" 等带冒号的格式
3221
+ script_lines.append(f" # 步骤{step_num}: 点击文本 '{ref}' (文本定位,最稳定)")
3124
3222
  script_lines.append(f" safe_click(d, d(text='{ref}'))")
3125
3223
  elif ref and ':' in ref and not is_coords_ref and not is_percent_ref:
3126
- # 2️⃣-b 使用文本(Android 的 text:xxx 或 description:xxx 格式)
3224
+ # 1️⃣-b 使用文本(Android 的 text:xxx 或 description:xxx 格式)
3127
3225
  # 提取冒号后面的实际文本值
3128
3226
  actual_text = ref.split(':', 1)[1] if ':' in ref else ref
3129
- script_lines.append(f" # 步骤{step_num}: 点击文本 '{actual_text}' (文本定位)")
3227
+ script_lines.append(f" # 步骤{step_num}: 点击文本 '{actual_text}' (文本定位,最稳定)")
3130
3228
  script_lines.append(f" safe_click(d, d(text='{actual_text}'))")
3229
+ elif ref and (':id/' in ref or ref.startswith('com.')):
3230
+ # 2️⃣ 使用 resource-id(稳定)
3231
+ script_lines.append(f" # 步骤{step_num}: 点击元素 (ID定位)")
3232
+ script_lines.append(f" safe_click(d, d(resourceId='{ref}'))")
3131
3233
  elif has_percent:
3132
3234
  # 3️⃣ 使用百分比(跨分辨率兼容)
3133
3235
  x_pct = op['x_percent']
@@ -3193,19 +3295,20 @@ class BasicMobileToolsLite:
3193
3295
  is_coords_ref = ref.startswith('coords_') or ref.startswith('coords:')
3194
3296
  is_percent_ref = ref.startswith('percent_')
3195
3297
 
3196
- # 优先级:ID > 文本 > 百分比 > 坐标
3197
- if ref and (':id/' in ref or ref.startswith('com.')):
3198
- # 使用 resource-id
3199
- script_lines.append(f" # 步骤{step_num}: 长按元素 (ID定位,最稳定)")
3200
- script_lines.append(f" d(resourceId='{ref}').long_click(duration={duration})")
3201
- elif ref and not is_coords_ref and not is_percent_ref and ':' not in ref:
3202
- # 使用文本
3203
- script_lines.append(f" # 步骤{step_num}: 长按文本 '{ref}' (文本定位)")
3298
+ # 优先级:文本 > ID > 百分比 > 坐标
3299
+ if ref and not is_coords_ref and not is_percent_ref and ':' not in ref:
3300
+ # 1️⃣ 使用文本(最稳定,优先)
3301
+ script_lines.append(f" # 步骤{step_num}: 长按文本 '{ref}' (文本定位,最稳定)")
3204
3302
  script_lines.append(f" d(text='{ref}').long_click(duration={duration})")
3205
3303
  elif ref and ':' in ref and not is_coords_ref and not is_percent_ref:
3304
+ # 1️⃣-b 使用文本(Android 的 text:xxx 或 description:xxx 格式)
3206
3305
  actual_text = ref.split(':', 1)[1] if ':' in ref else ref
3207
- script_lines.append(f" # 步骤{step_num}: 长按文本 '{actual_text}' (文本定位)")
3306
+ script_lines.append(f" # 步骤{step_num}: 长按文本 '{actual_text}' (文本定位,最稳定)")
3208
3307
  script_lines.append(f" d(text='{actual_text}').long_click(duration={duration})")
3308
+ elif ref and (':id/' in ref or ref.startswith('com.')):
3309
+ # 2️⃣ 使用 resource-id(稳定)
3310
+ script_lines.append(f" # 步骤{step_num}: 长按元素 (ID定位)")
3311
+ script_lines.append(f" d(resourceId='{ref}').long_click(duration={duration})")
3209
3312
  elif has_percent:
3210
3313
  # 使用百分比
3211
3314
  x_pct = op['x_percent']
@@ -3469,16 +3572,6 @@ class BasicMobileToolsLite:
3469
3572
  reason = f"文本含'{kw}'"
3470
3573
  break
3471
3574
 
3472
- # resource-id 匹配(如 close_icon, ad_close 等)
3473
- if resource_id:
3474
- res_id_lower = resource_id.lower()
3475
- for kw in ['close', 'dismiss', 'skip', 'cancel']:
3476
- if kw in res_id_lower:
3477
- score += 9
3478
- short_id = resource_id.split('/')[-1] if '/' in resource_id else resource_id
3479
- reason = f"resource-id='{short_id}'"
3480
- break
3481
-
3482
3575
  # content-desc 匹配
3483
3576
  for kw in close_content_desc:
3484
3577
  if kw.lower() in content_desc.lower():
@@ -206,7 +206,7 @@ class MobileMCPServer:
206
206
  name="mobile_list_elements",
207
207
  description="📋 列出页面所有可交互元素\n\n"
208
208
  "⚠️ 【重要】点击元素前必须先调用此工具!\n"
209
- "如果元素在控件树中存在,使用 click_by_idclick_by_text 定位。\n"
209
+ "如果元素在控件树中存在,使用 click_by_textclick_by_id 定位。\n"
210
210
  "只有当此工具返回空或找不到目标元素时,才使用截图+坐标方式。\n\n"
211
211
  "📌 控件树定位优势:\n"
212
212
  "- 实时检测元素是否存在\n"
@@ -313,14 +313,21 @@ class MobileMCPServer:
313
313
  # ==================== 点击操作 ====================
314
314
  tools.append(Tool(
315
315
  name="mobile_click_by_text",
316
- description="👆 通过文本点击元素(推荐)\n\n"
316
+ description="👆 通过文本点击元素(最推荐)\n\n"
317
+ "✅ 最稳定的定位方式,跨设备兼容\n"
317
318
  "✅ 实时检测元素是否存在,元素不存在会报错\n"
318
319
  "✅ 不会误点击到其他位置\n"
319
- "📋 使用前先调用 mobile_list_elements 确认元素文本",
320
+ "📋 使用前先调用 mobile_list_elements 确认元素文本\n"
321
+ "💡 定位优先级:文本 > ID > 百分比 > 坐标\n\n"
322
+ "📍 当页面有多个相同文案时,可使用 position 参数指定位置:\n"
323
+ " - 垂直方向: \"top\"/\"upper\"/\"上\", \"bottom\"/\"lower\"/\"下\", \"middle\"/\"center\"/\"中\"\n"
324
+ " - 水平方向: \"left\"/\"左\", \"right\"/\"右\", \"center\"/\"中\"\n"
325
+ " 例如:点击\"底部\"的\"微剧\"tab,使用 position=\"bottom\"",
320
326
  inputSchema={
321
327
  "type": "object",
322
328
  "properties": {
323
- "text": {"type": "string", "description": "元素的文本内容(精确匹配)"}
329
+ "text": {"type": "string", "description": "元素的文本内容(精确匹配)"},
330
+ "position": {"type": "string", "description": "位置信息(可选)。当有多个相同文案时使用,支持:top/bottom/left/right/middle 或 上/下/左/右/中"}
324
331
  },
325
332
  "required": ["text"]
326
333
  }
@@ -328,11 +335,12 @@ class MobileMCPServer:
328
335
 
329
336
  tools.append(Tool(
330
337
  name="mobile_click_by_id",
331
- description="👆 通过 resource-id 点击元素(最推荐)\n\n"
332
- "✅ 最稳定的定位方式\n"
338
+ description="👆 通过 resource-id 点击元素(推荐)\n\n"
339
+ "✅ 稳定的定位方式\n"
333
340
  "✅ 实时检测元素是否存在,元素不存在会报错\n"
334
341
  "📋 使用前先调用 mobile_list_elements 获取元素 ID\n"
335
- "💡 当有多个相同 ID 的元素时,用 index 指定第几个(从 0 开始)",
342
+ "💡 当有多个相同 ID 的元素时,用 index 指定第几个(从 0 开始)\n"
343
+ "💡 定位优先级:文本 > ID > 百分比 > 坐标",
336
344
  inputSchema={
337
345
  "type": "object",
338
346
  "properties": {
@@ -346,7 +354,7 @@ class MobileMCPServer:
346
354
  tools.append(Tool(
347
355
  name="mobile_click_at_coords",
348
356
  description="👆 点击指定坐标(兜底方案)\n\n"
349
- "⚠️ 【重要】优先使用 mobile_click_by_idmobile_click_by_text!\n"
357
+ "⚠️ 【重要】优先使用 mobile_click_by_textmobile_click_by_id!\n"
350
358
  "仅在 mobile_list_elements 无法获取元素时使用此工具。\n\n"
351
359
  "⚠️ 【时序限制】截图分析期间页面可能变化:\n"
352
360
  "- 坐标是基于截图时刻的,点击时页面可能已不同\n"
@@ -447,7 +455,7 @@ class MobileMCPServer:
447
455
 
448
456
  tools.append(Tool(
449
457
  name="mobile_long_press_at_coords",
450
- description="👆 长按指定坐标(⚠️ 兜底方案,优先用 ID/文本定位!)\n\n"
458
+ description="👆 长按指定坐标(⚠️ 兜底方案,优先用文本/ID定位!)\n\n"
451
459
  "🎯 仅在以下场景使用:\n"
452
460
  "- 游戏(Unity/Cocos)无法获取元素\n"
453
461
  "- mobile_list_elements 返回空\n"
@@ -623,7 +631,7 @@ class MobileMCPServer:
623
631
  ✅ 返回内容:
624
632
  - 坐标 (x, y) 和百分比 (x%, y%)
625
633
  - resource-id(如果有)
626
- - 推荐的点击命令(优先 click_by_id,其次 click_by_text,最后 click_by_percent)
634
+ - 推荐的点击命令(优先 click_by_text,其次 click_by_id,最后 click_by_percent)
627
635
 
628
636
  💡 使用流程:
629
637
  1. 直接调用此工具(无需先截图/列元素)
@@ -680,7 +688,7 @@ class MobileMCPServer:
680
688
  name="mobile_clear_operation_history",
681
689
  description="🗑️ 清空操作历史记录。\n\n"
682
690
  "⚠️ 开始新的测试录制前必须调用!\n"
683
- "📋 录制流程:清空历史 → 执行操作(优先用ID/文本定位)→ 生成脚本",
691
+ "📋 录制流程:清空历史 → 执行操作(优先用文本/ID定位)→ 生成脚本",
684
692
  inputSchema={"type": "object", "properties": {}, "required": []}
685
693
  ))
686
694
 
@@ -689,15 +697,15 @@ class MobileMCPServer:
689
697
  description="📝 生成 pytest 测试脚本。基于操作历史自动生成。\n\n"
690
698
  "⚠️ 【重要】录制操作时请优先使用稳定定位:\n"
691
699
  "1️⃣ 先调用 mobile_list_elements 获取元素列表\n"
692
- "2️⃣ 优先用 mobile_click_by_id(最稳定,跨设备兼容)\n"
693
- "3️⃣ 其次用 mobile_click_by_text(稳定)\n"
700
+ "2️⃣ 优先用 mobile_click_by_text(最稳定,跨设备兼容)\n"
701
+ "3️⃣ 其次用 mobile_click_by_id(稳定)\n"
694
702
  "4️⃣ 最后才用坐标点击(会自动转百分比,跨分辨率兼容)\n\n"
695
703
  "使用流程:\n"
696
704
  "1. 清空历史 mobile_clear_operation_history\n"
697
- "2. 执行操作(优先用 ID/文本定位)\n"
705
+ "2. 执行操作(优先用文本/ID定位)\n"
698
706
  "3. 调用此工具生成脚本\n"
699
707
  "4. 脚本保存到 tests/ 目录\n\n"
700
- "💡 定位优先级:ID > 文本 > 百分比 > 坐标",
708
+ "💡 定位优先级:文本 > ID > 百分比 > 坐标",
701
709
  inputSchema={
702
710
  "type": "object",
703
711
  "properties": {
@@ -863,7 +871,10 @@ class MobileMCPServer:
863
871
  return [TextContent(type="text", text=self.format_response(result))]
864
872
 
865
873
  elif name == "mobile_click_by_text":
866
- result = self.tools.click_by_text(arguments["text"])
874
+ result = self.tools.click_by_text(
875
+ arguments["text"],
876
+ position=arguments.get("position")
877
+ )
867
878
  return [TextContent(type="text", text=self.format_response(result))]
868
879
 
869
880
  elif name == "mobile_click_by_id":
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mobile-mcp-ai
3
- Version: 2.6.1
3
+ Version: 2.6.2
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,7 +1,7 @@
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=zBKruc_z-iYaa5PysC91AAB0eydt_qDFNTT7o5OuUqU,181240
4
+ mobile_mcp/core/basic_tools_lite.py,sha256=UwsUqM69DzjulaFC7_o2-TlyjvrtyWYXIZb-UWX3hZg,186486
5
5
  mobile_mcp/core/device_manager.py,sha256=xG5DoeNFs45pl-FTEhEWblqVwxtFK-FmVEGlNL6EqRI,8798
6
6
  mobile_mcp/core/dynamic_config.py,sha256=Ja1n1pfb0HspGByqk2_A472mYVniKmGtNEWyjUjmgK8,9811
7
7
  mobile_mcp/core/ios_client_wda.py,sha256=Nq9WxevhTWpVpolM-Ymp-b0nUQV3tXLFszmJHbDC4wA,18770
@@ -19,14 +19,14 @@ mobile_mcp/core/utils/logger.py,sha256=XXQAHUwT1jc70pq_tYFmL6f_nKrFlYm3hcgl-5RYR
19
19
  mobile_mcp/core/utils/operation_history_manager.py,sha256=gi8S8HJAMqvkUrY7_-kVbko3Xt7c4GAUziEujRd-N-Y,4792
20
20
  mobile_mcp/core/utils/smart_wait.py,sha256=N5wKTUYrNWPruBILqrAjpvtso8Z3GRWCfMIR_aZxPLg,8649
21
21
  mobile_mcp/mcp_tools/__init__.py,sha256=xkro8Rwqv_55YlVyhh-3DgRFSsLE3h1r31VIb3bpM6E,143
22
- mobile_mcp/mcp_tools/mcp_server.py,sha256=T6H3jAEfxQzGeQgiTg9ROn2GpgonARrrlFWrzVfxmKU,50135
22
+ mobile_mcp/mcp_tools/mcp_server.py,sha256=maeI0x6MLIk8Aw-xeZP6t8lvAxaU2LsoZjYWrgXPhjM,51106
23
23
  mobile_mcp/utils/__init__.py,sha256=8EH0i7UGtx1y_j_GEgdN-cZdWn2sRtZSEOLlNF9HRnY,158
24
24
  mobile_mcp/utils/logger.py,sha256=Sqq2Nr0Y4p03erqcrbYKVPCGiFaNGHMcE_JwCkeOfU4,3626
25
25
  mobile_mcp/utils/xml_formatter.py,sha256=uwTRb3vLbqhT8O-udzWT7s7LsV-DyDUz2DkofD3hXOE,4556
26
26
  mobile_mcp/utils/xml_parser.py,sha256=QhL8CWbdmNDzmBLjtx6mEnjHgMFZzJeHpCL15qfXSpI,3926
27
- mobile_mcp_ai-2.6.1.dist-info/licenses/LICENSE,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
28
- mobile_mcp_ai-2.6.1.dist-info/METADATA,sha256=oa_vxBNab-rKPX3yXVbetv6Joc-jjdh_XxgYA7vbEfY,10495
29
- mobile_mcp_ai-2.6.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
30
- mobile_mcp_ai-2.6.1.dist-info/entry_points.txt,sha256=KB_FglozgPHBprSM1vFbIzGyheFuHFmGanscRdMJ_8A,68
31
- mobile_mcp_ai-2.6.1.dist-info/top_level.txt,sha256=lLm6YpbTv855Lbh8BIA0rPxhybIrvYUzMEk9OErHT94,11
32
- mobile_mcp_ai-2.6.1.dist-info/RECORD,,
27
+ mobile_mcp_ai-2.6.2.dist-info/licenses/LICENSE,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
28
+ mobile_mcp_ai-2.6.2.dist-info/METADATA,sha256=E364-gQ0cu_Rn_6pWDqwEaK3n4TltzNhZt1mq9nbw7Y,10495
29
+ mobile_mcp_ai-2.6.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
30
+ mobile_mcp_ai-2.6.2.dist-info/entry_points.txt,sha256=KB_FglozgPHBprSM1vFbIzGyheFuHFmGanscRdMJ_8A,68
31
+ mobile_mcp_ai-2.6.2.dist-info/top_level.txt,sha256=lLm6YpbTv855Lbh8BIA0rPxhybIrvYUzMEk9OErHT94,11
32
+ mobile_mcp_ai-2.6.2.dist-info/RECORD,,