mobile-mcp-ai 2.6.0__tar.gz → 2.6.2__tar.gz
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_ai-2.6.0/mobile_mcp_ai.egg-info → mobile_mcp_ai-2.6.2}/PKG-INFO +1 -1
- {mobile_mcp_ai-2.6.0 → mobile_mcp_ai-2.6.2}/core/basic_tools_lite.py +180 -50
- {mobile_mcp_ai-2.6.0 → mobile_mcp_ai-2.6.2}/mcp_tools/mcp_server.py +48 -32
- {mobile_mcp_ai-2.6.0 → mobile_mcp_ai-2.6.2/mobile_mcp_ai.egg-info}/PKG-INFO +1 -1
- {mobile_mcp_ai-2.6.0 → mobile_mcp_ai-2.6.2}/setup.py +1 -1
- {mobile_mcp_ai-2.6.0 → mobile_mcp_ai-2.6.2}/LICENSE +0 -0
- {mobile_mcp_ai-2.6.0 → mobile_mcp_ai-2.6.2}/MANIFEST.in +0 -0
- {mobile_mcp_ai-2.6.0 → mobile_mcp_ai-2.6.2}/README.md +0 -0
- {mobile_mcp_ai-2.6.0 → mobile_mcp_ai-2.6.2}/__init__.py +0 -0
- {mobile_mcp_ai-2.6.0 → mobile_mcp_ai-2.6.2}/config.py +0 -0
- {mobile_mcp_ai-2.6.0 → mobile_mcp_ai-2.6.2}/core/__init__.py +0 -0
- {mobile_mcp_ai-2.6.0 → mobile_mcp_ai-2.6.2}/core/device_manager.py +0 -0
- {mobile_mcp_ai-2.6.0 → mobile_mcp_ai-2.6.2}/core/dynamic_config.py +0 -0
- {mobile_mcp_ai-2.6.0 → mobile_mcp_ai-2.6.2}/core/ios_client_wda.py +0 -0
- {mobile_mcp_ai-2.6.0 → mobile_mcp_ai-2.6.2}/core/ios_device_manager_wda.py +0 -0
- {mobile_mcp_ai-2.6.0 → mobile_mcp_ai-2.6.2}/core/mobile_client.py +0 -0
- {mobile_mcp_ai-2.6.0 → mobile_mcp_ai-2.6.2}/core/template_matcher.py +0 -0
- {mobile_mcp_ai-2.6.0 → mobile_mcp_ai-2.6.2}/core/templates/close_buttons/auto_x_0112_151217.png +0 -0
- {mobile_mcp_ai-2.6.0 → mobile_mcp_ai-2.6.2}/core/templates/close_buttons/auto_x_0112_152037.png +0 -0
- {mobile_mcp_ai-2.6.0 → mobile_mcp_ai-2.6.2}/core/templates/close_buttons/auto_x_0112_152840.png +0 -0
- {mobile_mcp_ai-2.6.0 → mobile_mcp_ai-2.6.2}/core/templates/close_buttons/auto_x_0112_153256.png +0 -0
- {mobile_mcp_ai-2.6.0 → mobile_mcp_ai-2.6.2}/core/templates/close_buttons/auto_x_0112_154847.png +0 -0
- {mobile_mcp_ai-2.6.0 → mobile_mcp_ai-2.6.2}/core/templates/close_buttons/gray_x_stock_ad.png +0 -0
- {mobile_mcp_ai-2.6.0 → mobile_mcp_ai-2.6.2}/core/utils/__init__.py +0 -0
- {mobile_mcp_ai-2.6.0 → mobile_mcp_ai-2.6.2}/core/utils/logger.py +0 -0
- {mobile_mcp_ai-2.6.0 → mobile_mcp_ai-2.6.2}/core/utils/operation_history_manager.py +0 -0
- {mobile_mcp_ai-2.6.0 → mobile_mcp_ai-2.6.2}/core/utils/smart_wait.py +0 -0
- {mobile_mcp_ai-2.6.0 → mobile_mcp_ai-2.6.2}/docs/iOS_SETUP_GUIDE.md +0 -0
- {mobile_mcp_ai-2.6.0 → mobile_mcp_ai-2.6.2}/mcp_tools/__init__.py +0 -0
- {mobile_mcp_ai-2.6.0 → mobile_mcp_ai-2.6.2}/mobile_mcp_ai.egg-info/SOURCES.txt +0 -0
- {mobile_mcp_ai-2.6.0 → mobile_mcp_ai-2.6.2}/mobile_mcp_ai.egg-info/dependency_links.txt +0 -0
- {mobile_mcp_ai-2.6.0 → mobile_mcp_ai-2.6.2}/mobile_mcp_ai.egg-info/entry_points.txt +0 -0
- {mobile_mcp_ai-2.6.0 → mobile_mcp_ai-2.6.2}/mobile_mcp_ai.egg-info/not-zip-safe +0 -0
- {mobile_mcp_ai-2.6.0 → mobile_mcp_ai-2.6.2}/mobile_mcp_ai.egg-info/requires.txt +0 -0
- {mobile_mcp_ai-2.6.0 → mobile_mcp_ai-2.6.2}/mobile_mcp_ai.egg-info/top_level.txt +0 -0
- {mobile_mcp_ai-2.6.0 → mobile_mcp_ai-2.6.2}/requirements.txt +0 -0
- {mobile_mcp_ai-2.6.0 → mobile_mcp_ai-2.6.2}/setup.cfg +0 -0
- {mobile_mcp_ai-2.6.0 → mobile_mcp_ai-2.6.2}/templates/close_buttons/auto_x_0112_151217.png +0 -0
- {mobile_mcp_ai-2.6.0 → mobile_mcp_ai-2.6.2}/templates/close_buttons/auto_x_0112_152037.png +0 -0
- {mobile_mcp_ai-2.6.0 → mobile_mcp_ai-2.6.2}/templates/close_buttons/auto_x_0112_152840.png +0 -0
- {mobile_mcp_ai-2.6.0 → mobile_mcp_ai-2.6.2}/templates/close_buttons/auto_x_0112_153256.png +0 -0
- {mobile_mcp_ai-2.6.0 → mobile_mcp_ai-2.6.2}/templates/close_buttons/auto_x_0112_154847.png +0 -0
- {mobile_mcp_ai-2.6.0 → mobile_mcp_ai-2.6.2}/templates/close_buttons/gray_x_stock_ad.png +0 -0
- {mobile_mcp_ai-2.6.0 → mobile_mcp_ai-2.6.2}/utils/__init__.py +0 -0
- {mobile_mcp_ai-2.6.0 → mobile_mcp_ai-2.6.2}/utils/logger.py +0 -0
- {mobile_mcp_ai-2.6.0 → mobile_mcp_ai-2.6.2}/utils/xml_formatter.py +0 -0
- {mobile_mcp_ai-2.6.0 → mobile_mcp_ai-2.6.2}/utils/xml_parser.py +0 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
1281
|
-
|
|
1325
|
+
is_match = True
|
|
1326
|
+
attr_type = 'text'
|
|
1327
|
+
attr_value = text
|
|
1282
1328
|
# 精确匹配 content-desc
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1329
|
+
elif elem_desc == text:
|
|
1330
|
+
is_match = True
|
|
1331
|
+
attr_type = 'description'
|
|
1332
|
+
attr_value = text
|
|
1286
1333
|
# 模糊匹配 text
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1334
|
+
elif text in elem_text:
|
|
1335
|
+
is_match = True
|
|
1336
|
+
attr_type = 'textContains'
|
|
1337
|
+
attr_value = text
|
|
1290
1338
|
# 模糊匹配 content-desc
|
|
1291
|
-
|
|
1292
|
-
|
|
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:
|
|
@@ -2526,19 +2650,24 @@ class BasicMobileToolsLite:
|
|
|
2526
2650
|
area = width * height
|
|
2527
2651
|
screen_area = screen_width * screen_height
|
|
2528
2652
|
|
|
2529
|
-
#
|
|
2530
|
-
# 1. 面积在屏幕的 10%-
|
|
2531
|
-
# 2.
|
|
2653
|
+
# 弹窗容器特征(更严格,排除主要内容区域):
|
|
2654
|
+
# 1. 面积在屏幕的 10%-70% 之间(排除主要内容区域,通常占80%+)
|
|
2655
|
+
# 2. 宽度和高度都要小于屏幕的95%(不是全屏)
|
|
2532
2656
|
# 3. 是容器类型(Layout/View/Dialog)
|
|
2657
|
+
# 4. 不在屏幕左边缘(排除从x=0开始的主要内容容器)
|
|
2658
|
+
# 5. 高度不能占据屏幕的大部分(排除主要内容区域)
|
|
2533
2659
|
is_container = any(kw in class_name for kw in ['Layout', 'View', 'Dialog', 'Card', 'Container'])
|
|
2534
2660
|
area_ratio = area / screen_area
|
|
2535
|
-
is_not_fullscreen = (width < screen_width * 0.
|
|
2536
|
-
is_reasonable_size = 0.
|
|
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
|
|
2537
2666
|
|
|
2538
2667
|
# 排除状态栏区域(y1 通常很小)
|
|
2539
2668
|
is_below_statusbar = y1 > 50
|
|
2540
2669
|
|
|
2541
|
-
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:
|
|
2542
2671
|
popup_containers.append({
|
|
2543
2672
|
'bounds': (x1, y1, x2, y2),
|
|
2544
2673
|
'bounds_str': bounds_str,
|
|
@@ -2970,8 +3099,8 @@ class BasicMobileToolsLite:
|
|
|
2970
3099
|
f"生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
|
|
2971
3100
|
"",
|
|
2972
3101
|
"定位策略(按优先级):",
|
|
2973
|
-
"1.
|
|
2974
|
-
"2.
|
|
3102
|
+
"1. 文本定位 - 最稳定,跨设备兼容",
|
|
3103
|
+
"2. ID 定位 - 稳定,跨设备兼容",
|
|
2975
3104
|
"3. 百分比定位 - 跨分辨率兼容(坐标自动转换)",
|
|
2976
3105
|
f'"""',
|
|
2977
3106
|
"import time",
|
|
@@ -3086,21 +3215,21 @@ class BasicMobileToolsLite:
|
|
|
3086
3215
|
is_coords_ref = ref.startswith('coords_') or ref.startswith('coords:')
|
|
3087
3216
|
is_percent_ref = ref.startswith('percent_')
|
|
3088
3217
|
|
|
3089
|
-
#
|
|
3090
|
-
if ref and
|
|
3091
|
-
# 1️⃣
|
|
3092
|
-
script_lines.append(f" # 步骤{step_num}:
|
|
3093
|
-
script_lines.append(f" safe_click(d, d(resourceId='{ref}'))")
|
|
3094
|
-
elif ref and not is_coords_ref and not is_percent_ref and ':' not in ref:
|
|
3095
|
-
# 2️⃣ 使用文本(稳定)- 排除 "text:xxx" 等带冒号的格式
|
|
3096
|
-
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}' (文本定位,最稳定)")
|
|
3097
3222
|
script_lines.append(f" safe_click(d, d(text='{ref}'))")
|
|
3098
3223
|
elif ref and ':' in ref and not is_coords_ref and not is_percent_ref:
|
|
3099
|
-
#
|
|
3224
|
+
# 1️⃣-b 使用文本(Android 的 text:xxx 或 description:xxx 格式)
|
|
3100
3225
|
# 提取冒号后面的实际文本值
|
|
3101
3226
|
actual_text = ref.split(':', 1)[1] if ':' in ref else ref
|
|
3102
|
-
script_lines.append(f" # 步骤{step_num}: 点击文本 '{actual_text}' (
|
|
3227
|
+
script_lines.append(f" # 步骤{step_num}: 点击文本 '{actual_text}' (文本定位,最稳定)")
|
|
3103
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}'))")
|
|
3104
3233
|
elif has_percent:
|
|
3105
3234
|
# 3️⃣ 使用百分比(跨分辨率兼容)
|
|
3106
3235
|
x_pct = op['x_percent']
|
|
@@ -3166,19 +3295,20 @@ class BasicMobileToolsLite:
|
|
|
3166
3295
|
is_coords_ref = ref.startswith('coords_') or ref.startswith('coords:')
|
|
3167
3296
|
is_percent_ref = ref.startswith('percent_')
|
|
3168
3297
|
|
|
3169
|
-
#
|
|
3170
|
-
if ref and
|
|
3171
|
-
#
|
|
3172
|
-
script_lines.append(f" # 步骤{step_num}:
|
|
3173
|
-
script_lines.append(f" d(resourceId='{ref}').long_click(duration={duration})")
|
|
3174
|
-
elif ref and not is_coords_ref and not is_percent_ref and ':' not in ref:
|
|
3175
|
-
# 使用文本
|
|
3176
|
-
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}' (文本定位,最稳定)")
|
|
3177
3302
|
script_lines.append(f" d(text='{ref}').long_click(duration={duration})")
|
|
3178
3303
|
elif ref and ':' in ref and not is_coords_ref and not is_percent_ref:
|
|
3304
|
+
# 1️⃣-b 使用文本(Android 的 text:xxx 或 description:xxx 格式)
|
|
3179
3305
|
actual_text = ref.split(':', 1)[1] if ':' in ref else ref
|
|
3180
|
-
script_lines.append(f" # 步骤{step_num}: 长按文本 '{actual_text}' (
|
|
3306
|
+
script_lines.append(f" # 步骤{step_num}: 长按文本 '{actual_text}' (文本定位,最稳定)")
|
|
3181
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})")
|
|
3182
3312
|
elif has_percent:
|
|
3183
3313
|
# 使用百分比
|
|
3184
3314
|
x_pct = op['x_percent']
|
|
@@ -206,7 +206,7 @@ class MobileMCPServer:
|
|
|
206
206
|
name="mobile_list_elements",
|
|
207
207
|
description="📋 列出页面所有可交互元素\n\n"
|
|
208
208
|
"⚠️ 【重要】点击元素前必须先调用此工具!\n"
|
|
209
|
-
"如果元素在控件树中存在,使用
|
|
209
|
+
"如果元素在控件树中存在,使用 click_by_text 或 click_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="👆
|
|
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
|
|
332
|
-
"✅
|
|
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
|
-
"⚠️ 【重要】优先使用
|
|
357
|
+
"⚠️ 【重要】优先使用 mobile_click_by_text 或 mobile_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="👆 长按指定坐标(⚠️
|
|
458
|
+
description="👆 长按指定坐标(⚠️ 兜底方案,优先用文本/ID定位!)\n\n"
|
|
451
459
|
"🎯 仅在以下场景使用:\n"
|
|
452
460
|
"- 游戏(Unity/Cocos)无法获取元素\n"
|
|
453
461
|
"- mobile_list_elements 返回空\n"
|
|
@@ -610,22 +618,25 @@ class MobileMCPServer:
|
|
|
610
618
|
name="mobile_find_close_button",
|
|
611
619
|
description="""🔍 智能查找关闭按钮(只找不点,返回位置)
|
|
612
620
|
|
|
613
|
-
|
|
621
|
+
⚡ 【推荐首选】遇到弹窗时优先调用此工具!无需先截图。
|
|
622
|
+
|
|
623
|
+
从元素树中找最可能的关闭按钮,返回坐标和推荐的点击命令。
|
|
614
624
|
|
|
615
625
|
🎯 识别策略(优先级):
|
|
616
|
-
1. 文本匹配:×、X、关闭、取消、跳过
|
|
617
|
-
2.
|
|
618
|
-
3.
|
|
626
|
+
1. 文本匹配:×、X、关闭、取消、跳过 等(得分100)
|
|
627
|
+
2. resource-id 匹配:包含 close/dismiss/skip(得分95)
|
|
628
|
+
3. content-desc 匹配:包含 close/关闭(得分90)
|
|
629
|
+
4. 小尺寸 clickable 元素(右上角优先,得分70+)
|
|
619
630
|
|
|
620
631
|
✅ 返回内容:
|
|
621
632
|
- 坐标 (x, y) 和百分比 (x%, y%)
|
|
622
|
-
-
|
|
623
|
-
-
|
|
633
|
+
- resource-id(如果有)
|
|
634
|
+
- 推荐的点击命令(优先 click_by_text,其次 click_by_id,最后 click_by_percent)
|
|
624
635
|
|
|
625
636
|
💡 使用流程:
|
|
626
|
-
1.
|
|
627
|
-
2.
|
|
628
|
-
3.
|
|
637
|
+
1. 直接调用此工具(无需先截图/列元素)
|
|
638
|
+
2. 根据返回的 click_command 执行点击
|
|
639
|
+
3. 如果返回 success=false,才需要截图分析""",
|
|
629
640
|
inputSchema={"type": "object", "properties": {}, "required": []}
|
|
630
641
|
))
|
|
631
642
|
|
|
@@ -677,7 +688,7 @@ class MobileMCPServer:
|
|
|
677
688
|
name="mobile_clear_operation_history",
|
|
678
689
|
description="🗑️ 清空操作历史记录。\n\n"
|
|
679
690
|
"⚠️ 开始新的测试录制前必须调用!\n"
|
|
680
|
-
"📋 录制流程:清空历史 →
|
|
691
|
+
"📋 录制流程:清空历史 → 执行操作(优先用文本/ID定位)→ 生成脚本",
|
|
681
692
|
inputSchema={"type": "object", "properties": {}, "required": []}
|
|
682
693
|
))
|
|
683
694
|
|
|
@@ -686,15 +697,15 @@ class MobileMCPServer:
|
|
|
686
697
|
description="📝 生成 pytest 测试脚本。基于操作历史自动生成。\n\n"
|
|
687
698
|
"⚠️ 【重要】录制操作时请优先使用稳定定位:\n"
|
|
688
699
|
"1️⃣ 先调用 mobile_list_elements 获取元素列表\n"
|
|
689
|
-
"2️⃣ 优先用
|
|
690
|
-
"3️⃣ 其次用
|
|
700
|
+
"2️⃣ 优先用 mobile_click_by_text(最稳定,跨设备兼容)\n"
|
|
701
|
+
"3️⃣ 其次用 mobile_click_by_id(稳定)\n"
|
|
691
702
|
"4️⃣ 最后才用坐标点击(会自动转百分比,跨分辨率兼容)\n\n"
|
|
692
703
|
"使用流程:\n"
|
|
693
704
|
"1. 清空历史 mobile_clear_operation_history\n"
|
|
694
|
-
"2.
|
|
705
|
+
"2. 执行操作(优先用文本/ID定位)\n"
|
|
695
706
|
"3. 调用此工具生成脚本\n"
|
|
696
707
|
"4. 脚本保存到 tests/ 目录\n\n"
|
|
697
|
-
"💡
|
|
708
|
+
"💡 定位优先级:文本 > ID > 百分比 > 坐标",
|
|
698
709
|
inputSchema={
|
|
699
710
|
"type": "object",
|
|
700
711
|
"properties": {
|
|
@@ -711,24 +722,26 @@ class MobileMCPServer:
|
|
|
711
722
|
name="mobile_close_ad",
|
|
712
723
|
description="""🚫 【推荐】智能关闭广告弹窗
|
|
713
724
|
|
|
714
|
-
|
|
725
|
+
⚡ 直接调用即可,无需先截图!会自动按优先级尝试:
|
|
715
726
|
|
|
716
|
-
1️⃣
|
|
717
|
-
- 自动查找
|
|
727
|
+
1️⃣ **控件树查找**(最可靠,优先)
|
|
728
|
+
- 自动查找 resource-id 包含 close/dismiss
|
|
729
|
+
- 查找文本"关闭"、"跳过"、"×"等
|
|
718
730
|
- 找到直接点击,实时可靠
|
|
719
731
|
|
|
720
732
|
2️⃣ **模板匹配**(次优)
|
|
721
733
|
- 用 OpenCV 匹配已保存的 X 按钮模板
|
|
722
|
-
-
|
|
734
|
+
- 模板越多成功率越高
|
|
723
735
|
|
|
724
736
|
3️⃣ **返回截图供 AI 分析**(兜底)
|
|
725
|
-
-
|
|
737
|
+
- 前两步都失败才截图
|
|
726
738
|
- AI 分析后用 mobile_click_by_percent 点击
|
|
727
|
-
- 点击成功后用 mobile_template_add
|
|
739
|
+
- 点击成功后用 mobile_template_add 添加模板
|
|
728
740
|
|
|
729
|
-
💡
|
|
730
|
-
1. 遇到广告弹窗 →
|
|
741
|
+
💡 正确流程:
|
|
742
|
+
1. 遇到广告弹窗 → 直接调用此工具
|
|
731
743
|
2. 如果成功 → 完成
|
|
744
|
+
3. 只有失败时才需要截图分析
|
|
732
745
|
3. 如果失败 → 看截图找 X → 点击 → 添加模板""",
|
|
733
746
|
inputSchema={
|
|
734
747
|
"type": "object",
|
|
@@ -858,7 +871,10 @@ class MobileMCPServer:
|
|
|
858
871
|
return [TextContent(type="text", text=self.format_response(result))]
|
|
859
872
|
|
|
860
873
|
elif name == "mobile_click_by_text":
|
|
861
|
-
result = self.tools.click_by_text(
|
|
874
|
+
result = self.tools.click_by_text(
|
|
875
|
+
arguments["text"],
|
|
876
|
+
position=arguments.get("position")
|
|
877
|
+
)
|
|
862
878
|
return [TextContent(type="text", text=self.format_response(result))]
|
|
863
879
|
|
|
864
880
|
elif name == "mobile_click_by_id":
|
|
@@ -25,7 +25,7 @@ if requirements_file.exists():
|
|
|
25
25
|
|
|
26
26
|
setup(
|
|
27
27
|
name="mobile-mcp-ai",
|
|
28
|
-
version="2.6.
|
|
28
|
+
version="2.6.2", # find_close_button 增加 resource-id 匹配 + list_elements 文本过滤
|
|
29
29
|
author="douzi",
|
|
30
30
|
author_email="1492994674@qq.com",
|
|
31
31
|
description="移动端自动化 MCP Server - 支持 Android/iOS,AI 功能可选(基础工具不需要 AI)",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mobile_mcp_ai-2.6.0 → mobile_mcp_ai-2.6.2}/core/templates/close_buttons/auto_x_0112_151217.png
RENAMED
|
File without changes
|
{mobile_mcp_ai-2.6.0 → mobile_mcp_ai-2.6.2}/core/templates/close_buttons/auto_x_0112_152037.png
RENAMED
|
File without changes
|
{mobile_mcp_ai-2.6.0 → mobile_mcp_ai-2.6.2}/core/templates/close_buttons/auto_x_0112_152840.png
RENAMED
|
File without changes
|
{mobile_mcp_ai-2.6.0 → mobile_mcp_ai-2.6.2}/core/templates/close_buttons/auto_x_0112_153256.png
RENAMED
|
File without changes
|
{mobile_mcp_ai-2.6.0 → mobile_mcp_ai-2.6.2}/core/templates/close_buttons/auto_x_0112_154847.png
RENAMED
|
File without changes
|
{mobile_mcp_ai-2.6.0 → mobile_mcp_ai-2.6.2}/core/templates/close_buttons/gray_x_stock_ad.png
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|