mobile-mcp-ai 2.5.4__tar.gz → 2.5.8__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.5.4/mobile_mcp_ai.egg-info → mobile_mcp_ai-2.5.8}/PKG-INFO +14 -1
- {mobile_mcp_ai-2.5.4 → mobile_mcp_ai-2.5.8}/README.md +13 -0
- {mobile_mcp_ai-2.5.4 → mobile_mcp_ai-2.5.8}/core/basic_tools_lite.py +103 -30
- {mobile_mcp_ai-2.5.4 → mobile_mcp_ai-2.5.8}/core/device_manager.py +2 -2
- {mobile_mcp_ai-2.5.4 → mobile_mcp_ai-2.5.8}/core/mobile_client.py +9 -9
- {mobile_mcp_ai-2.5.4 → mobile_mcp_ai-2.5.8}/core/utils/smart_wait.py +3 -3
- {mobile_mcp_ai-2.5.4 → mobile_mcp_ai-2.5.8}/mcp_tools/mcp_server.py +54 -10
- {mobile_mcp_ai-2.5.4 → mobile_mcp_ai-2.5.8/mobile_mcp_ai.egg-info}/PKG-INFO +14 -1
- {mobile_mcp_ai-2.5.4 → mobile_mcp_ai-2.5.8}/setup.py +1 -1
- {mobile_mcp_ai-2.5.4 → mobile_mcp_ai-2.5.8}/LICENSE +0 -0
- {mobile_mcp_ai-2.5.4 → mobile_mcp_ai-2.5.8}/MANIFEST.in +0 -0
- {mobile_mcp_ai-2.5.4 → mobile_mcp_ai-2.5.8}/__init__.py +0 -0
- {mobile_mcp_ai-2.5.4 → mobile_mcp_ai-2.5.8}/config.py +0 -0
- {mobile_mcp_ai-2.5.4 → mobile_mcp_ai-2.5.8}/core/__init__.py +0 -0
- {mobile_mcp_ai-2.5.4 → mobile_mcp_ai-2.5.8}/core/dynamic_config.py +0 -0
- {mobile_mcp_ai-2.5.4 → mobile_mcp_ai-2.5.8}/core/ios_client_wda.py +0 -0
- {mobile_mcp_ai-2.5.4 → mobile_mcp_ai-2.5.8}/core/ios_device_manager_wda.py +0 -0
- {mobile_mcp_ai-2.5.4 → mobile_mcp_ai-2.5.8}/core/template_matcher.py +0 -0
- {mobile_mcp_ai-2.5.4 → mobile_mcp_ai-2.5.8}/core/templates/close_buttons/auto_x_0112_151217.png +0 -0
- {mobile_mcp_ai-2.5.4 → mobile_mcp_ai-2.5.8}/core/templates/close_buttons/auto_x_0112_152037.png +0 -0
- {mobile_mcp_ai-2.5.4 → mobile_mcp_ai-2.5.8}/core/templates/close_buttons/auto_x_0112_152840.png +0 -0
- {mobile_mcp_ai-2.5.4 → mobile_mcp_ai-2.5.8}/core/templates/close_buttons/auto_x_0112_153256.png +0 -0
- {mobile_mcp_ai-2.5.4 → mobile_mcp_ai-2.5.8}/core/templates/close_buttons/auto_x_0112_154847.png +0 -0
- {mobile_mcp_ai-2.5.4 → mobile_mcp_ai-2.5.8}/core/templates/close_buttons/gray_x_stock_ad.png +0 -0
- {mobile_mcp_ai-2.5.4 → mobile_mcp_ai-2.5.8}/core/utils/__init__.py +0 -0
- {mobile_mcp_ai-2.5.4 → mobile_mcp_ai-2.5.8}/core/utils/logger.py +0 -0
- {mobile_mcp_ai-2.5.4 → mobile_mcp_ai-2.5.8}/core/utils/operation_history_manager.py +0 -0
- {mobile_mcp_ai-2.5.4 → mobile_mcp_ai-2.5.8}/docs/iOS_SETUP_GUIDE.md +0 -0
- {mobile_mcp_ai-2.5.4 → mobile_mcp_ai-2.5.8}/mcp_tools/__init__.py +0 -0
- {mobile_mcp_ai-2.5.4 → mobile_mcp_ai-2.5.8}/mobile_mcp_ai.egg-info/SOURCES.txt +0 -0
- {mobile_mcp_ai-2.5.4 → mobile_mcp_ai-2.5.8}/mobile_mcp_ai.egg-info/dependency_links.txt +0 -0
- {mobile_mcp_ai-2.5.4 → mobile_mcp_ai-2.5.8}/mobile_mcp_ai.egg-info/entry_points.txt +0 -0
- {mobile_mcp_ai-2.5.4 → mobile_mcp_ai-2.5.8}/mobile_mcp_ai.egg-info/not-zip-safe +0 -0
- {mobile_mcp_ai-2.5.4 → mobile_mcp_ai-2.5.8}/mobile_mcp_ai.egg-info/requires.txt +0 -0
- {mobile_mcp_ai-2.5.4 → mobile_mcp_ai-2.5.8}/mobile_mcp_ai.egg-info/top_level.txt +0 -0
- {mobile_mcp_ai-2.5.4 → mobile_mcp_ai-2.5.8}/requirements.txt +0 -0
- {mobile_mcp_ai-2.5.4 → mobile_mcp_ai-2.5.8}/setup.cfg +0 -0
- {mobile_mcp_ai-2.5.4 → mobile_mcp_ai-2.5.8}/templates/close_buttons/auto_x_0112_151217.png +0 -0
- {mobile_mcp_ai-2.5.4 → mobile_mcp_ai-2.5.8}/templates/close_buttons/auto_x_0112_152037.png +0 -0
- {mobile_mcp_ai-2.5.4 → mobile_mcp_ai-2.5.8}/templates/close_buttons/auto_x_0112_152840.png +0 -0
- {mobile_mcp_ai-2.5.4 → mobile_mcp_ai-2.5.8}/templates/close_buttons/auto_x_0112_153256.png +0 -0
- {mobile_mcp_ai-2.5.4 → mobile_mcp_ai-2.5.8}/templates/close_buttons/auto_x_0112_154847.png +0 -0
- {mobile_mcp_ai-2.5.4 → mobile_mcp_ai-2.5.8}/templates/close_buttons/gray_x_stock_ad.png +0 -0
- {mobile_mcp_ai-2.5.4 → mobile_mcp_ai-2.5.8}/tests/test_mind_cloud_my_space.py +0 -0
- {mobile_mcp_ai-2.5.4 → mobile_mcp_ai-2.5.8}/tests/test_mind_correct.py +0 -0
- {mobile_mcp_ai-2.5.4 → mobile_mcp_ai-2.5.8}/tests/test_mind_improved.py +0 -0
- {mobile_mcp_ai-2.5.4 → mobile_mcp_ai-2.5.8}/tests/test_mind_optimized.py +0 -0
- {mobile_mcp_ai-2.5.4 → mobile_mcp_ai-2.5.8}/tests/test_open_mind.py +0 -0
- {mobile_mcp_ai-2.5.4 → mobile_mcp_ai-2.5.8}/tests/test_priority_demo.py +0 -0
- {mobile_mcp_ai-2.5.4 → mobile_mcp_ai-2.5.8}/tests/test_simple.py +0 -0
- {mobile_mcp_ai-2.5.4 → mobile_mcp_ai-2.5.8}/tests/test_/344/270/276/346/212/245.py" +0 -0
- {mobile_mcp_ai-2.5.4 → mobile_mcp_ai-2.5.8}/tests/test_/345/210/207/346/215/242/350/257/255/350/250/200/345/210/260English.py" +0 -0
- {mobile_mcp_ai-2.5.4 → mobile_mcp_ai-2.5.8}/tests/test_/346/265/213/350/257/225.py" +0 -0
- {mobile_mcp_ai-2.5.4 → mobile_mcp_ai-2.5.8}/utils/__init__.py +0 -0
- {mobile_mcp_ai-2.5.4 → mobile_mcp_ai-2.5.8}/utils/logger.py +0 -0
- {mobile_mcp_ai-2.5.4 → mobile_mcp_ai-2.5.8}/utils/xml_formatter.py +0 -0
- {mobile_mcp_ai-2.5.4 → mobile_mcp_ai-2.5.8}/utils/xml_parser.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mobile-mcp-ai
|
|
3
|
-
Version: 2.5.
|
|
3
|
+
Version: 2.5.8
|
|
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
|
|
@@ -408,10 +408,18 @@ tidevice list
|
|
|
408
408
|
|:---:|------|------|
|
|
409
409
|
| 📋 | `mobile_list_elements` | 列出页面元素 |
|
|
410
410
|
| 📸 | `mobile_take_screenshot` | 截图 |
|
|
411
|
+
| 📸 | `mobile_screenshot_with_som` | Set-of-Mark 截图(智能标注) |
|
|
412
|
+
| 📸 | `mobile_screenshot_with_grid` | 带网格坐标的截图 |
|
|
411
413
|
| 📐 | `mobile_get_screen_size` | 屏幕尺寸 |
|
|
412
414
|
| 👆 | `mobile_click_by_text` | 文本点击 |
|
|
413
415
|
| 👆 | `mobile_click_by_id` | ID 点击 |
|
|
414
416
|
| 👆 | `mobile_click_at_coords` | 坐标点击 |
|
|
417
|
+
| 👆 | `mobile_click_by_percent` | 百分比点击 |
|
|
418
|
+
| 👆 | `mobile_click_by_som` | SoM 编号点击 |
|
|
419
|
+
| 👆 | `mobile_long_press_by_id` | ID 长按 |
|
|
420
|
+
| 👆 | `mobile_long_press_by_text` | 文本长按 |
|
|
421
|
+
| 👆 | `mobile_long_press_by_percent` | 百分比长按 |
|
|
422
|
+
| 👆 | `mobile_long_press_at_coords` | 坐标长按 |
|
|
415
423
|
| ⌨️ | `mobile_input_text_by_id` | ID 输入 |
|
|
416
424
|
| ⌨️ | `mobile_input_at_coords` | 坐标输入 |
|
|
417
425
|
| 👆 | `mobile_swipe` | 滑动 |
|
|
@@ -422,6 +430,11 @@ tidevice list
|
|
|
422
430
|
| 📦 | `mobile_list_apps` | 列出应用 |
|
|
423
431
|
| 📱 | `mobile_list_devices` | 列出设备 |
|
|
424
432
|
| 🔌 | `mobile_check_connection` | 检查连接 |
|
|
433
|
+
| 🔍 | `mobile_find_close_button` | 查找关闭按钮 |
|
|
434
|
+
| 🚫 | `mobile_close_popup` | 关闭弹窗 |
|
|
435
|
+
| 🚫 | `mobile_close_ad` | 智能关闭广告弹窗 |
|
|
436
|
+
| 🎯 | `mobile_template_close` | 模板匹配关闭弹窗 |
|
|
437
|
+
| ➕ | `mobile_template_add` | 添加 X 号模板 |
|
|
425
438
|
| ✅ | `mobile_assert_text` | 断言文本 |
|
|
426
439
|
| 📜 | `mobile_get_operation_history` | 操作历史 |
|
|
427
440
|
| 🗑️ | `mobile_clear_operation_history` | 清空历史 |
|
|
@@ -337,10 +337,18 @@ tidevice list
|
|
|
337
337
|
|:---:|------|------|
|
|
338
338
|
| 📋 | `mobile_list_elements` | 列出页面元素 |
|
|
339
339
|
| 📸 | `mobile_take_screenshot` | 截图 |
|
|
340
|
+
| 📸 | `mobile_screenshot_with_som` | Set-of-Mark 截图(智能标注) |
|
|
341
|
+
| 📸 | `mobile_screenshot_with_grid` | 带网格坐标的截图 |
|
|
340
342
|
| 📐 | `mobile_get_screen_size` | 屏幕尺寸 |
|
|
341
343
|
| 👆 | `mobile_click_by_text` | 文本点击 |
|
|
342
344
|
| 👆 | `mobile_click_by_id` | ID 点击 |
|
|
343
345
|
| 👆 | `mobile_click_at_coords` | 坐标点击 |
|
|
346
|
+
| 👆 | `mobile_click_by_percent` | 百分比点击 |
|
|
347
|
+
| 👆 | `mobile_click_by_som` | SoM 编号点击 |
|
|
348
|
+
| 👆 | `mobile_long_press_by_id` | ID 长按 |
|
|
349
|
+
| 👆 | `mobile_long_press_by_text` | 文本长按 |
|
|
350
|
+
| 👆 | `mobile_long_press_by_percent` | 百分比长按 |
|
|
351
|
+
| 👆 | `mobile_long_press_at_coords` | 坐标长按 |
|
|
344
352
|
| ⌨️ | `mobile_input_text_by_id` | ID 输入 |
|
|
345
353
|
| ⌨️ | `mobile_input_at_coords` | 坐标输入 |
|
|
346
354
|
| 👆 | `mobile_swipe` | 滑动 |
|
|
@@ -351,6 +359,11 @@ tidevice list
|
|
|
351
359
|
| 📦 | `mobile_list_apps` | 列出应用 |
|
|
352
360
|
| 📱 | `mobile_list_devices` | 列出设备 |
|
|
353
361
|
| 🔌 | `mobile_check_connection` | 检查连接 |
|
|
362
|
+
| 🔍 | `mobile_find_close_button` | 查找关闭按钮 |
|
|
363
|
+
| 🚫 | `mobile_close_popup` | 关闭弹窗 |
|
|
364
|
+
| 🚫 | `mobile_close_ad` | 智能关闭广告弹窗 |
|
|
365
|
+
| 🎯 | `mobile_template_close` | 模板匹配关闭弹窗 |
|
|
366
|
+
| ➕ | `mobile_template_add` | 添加 X 号模板 |
|
|
354
367
|
| ✅ | `mobile_assert_text` | 断言文本 |
|
|
355
368
|
| 📜 | `mobile_get_operation_history` | 操作历史 |
|
|
356
369
|
| 🗑️ | `mobile_clear_operation_history` | 清空历史 |
|
|
@@ -354,7 +354,7 @@ class BasicMobileToolsLite:
|
|
|
354
354
|
if show_popup_hints and not self._is_ios():
|
|
355
355
|
try:
|
|
356
356
|
import xml.etree.ElementTree as ET
|
|
357
|
-
xml_string = self.client.u2.dump_hierarchy()
|
|
357
|
+
xml_string = self.client.u2.dump_hierarchy(compressed=False)
|
|
358
358
|
root = ET.fromstring(xml_string)
|
|
359
359
|
|
|
360
360
|
# 检测弹窗区域
|
|
@@ -531,7 +531,7 @@ class BasicMobileToolsLite:
|
|
|
531
531
|
else:
|
|
532
532
|
try:
|
|
533
533
|
import xml.etree.ElementTree as ET
|
|
534
|
-
xml_string = self.client.u2.dump_hierarchy()
|
|
534
|
+
xml_string = self.client.u2.dump_hierarchy(compressed=False)
|
|
535
535
|
root = ET.fromstring(xml_string)
|
|
536
536
|
|
|
537
537
|
for elem in root.iter():
|
|
@@ -1089,7 +1089,7 @@ class BasicMobileToolsLite:
|
|
|
1089
1089
|
def _find_element_in_tree(self, text: str) -> Optional[Dict]:
|
|
1090
1090
|
"""在 XML 树中查找包含指定文本的元素"""
|
|
1091
1091
|
try:
|
|
1092
|
-
xml = self.client.u2.dump_hierarchy()
|
|
1092
|
+
xml = self.client.u2.dump_hierarchy(compressed=False)
|
|
1093
1093
|
import xml.etree.ElementTree as ET
|
|
1094
1094
|
root = ET.fromstring(xml)
|
|
1095
1095
|
|
|
@@ -1126,9 +1126,16 @@ class BasicMobileToolsLite:
|
|
|
1126
1126
|
except Exception:
|
|
1127
1127
|
return None
|
|
1128
1128
|
|
|
1129
|
-
def click_by_id(self, resource_id: str) -> Dict:
|
|
1130
|
-
"""通过 resource-id
|
|
1129
|
+
def click_by_id(self, resource_id: str, index: int = 0) -> Dict:
|
|
1130
|
+
"""通过 resource-id 点击(支持点击第 N 个元素)
|
|
1131
|
+
|
|
1132
|
+
Args:
|
|
1133
|
+
resource_id: 元素的 resource-id
|
|
1134
|
+
index: 第几个元素(从 0 开始),默认 0 表示第一个
|
|
1135
|
+
"""
|
|
1131
1136
|
try:
|
|
1137
|
+
index_desc = f"[{index}]" if index > 0 else ""
|
|
1138
|
+
|
|
1132
1139
|
if self._is_ios():
|
|
1133
1140
|
ios_client = self._get_ios_client()
|
|
1134
1141
|
if ios_client and hasattr(ios_client, 'wda'):
|
|
@@ -1136,18 +1143,28 @@ class BasicMobileToolsLite:
|
|
|
1136
1143
|
if not elem.exists:
|
|
1137
1144
|
elem = ios_client.wda(name=resource_id)
|
|
1138
1145
|
if elem.exists:
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1146
|
+
# 获取所有匹配的元素
|
|
1147
|
+
elements = elem.find_elements()
|
|
1148
|
+
if index < len(elements):
|
|
1149
|
+
elements[index].click()
|
|
1150
|
+
time.sleep(0.3)
|
|
1151
|
+
self._record_operation('click', element=f"{resource_id}{index_desc}", ref=resource_id, index=index)
|
|
1152
|
+
return {"success": True, "message": f"✅ 点击成功: {resource_id}{index_desc}"}
|
|
1153
|
+
else:
|
|
1154
|
+
return {"success": False, "message": f"❌ 索引超出范围: 找到 {len(elements)} 个元素,但请求索引 {index}"}
|
|
1143
1155
|
return {"success": False, "message": f"❌ 元素不存在: {resource_id}"}
|
|
1144
1156
|
else:
|
|
1145
1157
|
elem = self.client.u2(resourceId=resource_id)
|
|
1146
1158
|
if elem.exists(timeout=0.5):
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1159
|
+
# 获取匹配元素数量
|
|
1160
|
+
count = elem.count
|
|
1161
|
+
if index < count:
|
|
1162
|
+
elem[index].click()
|
|
1163
|
+
time.sleep(0.3)
|
|
1164
|
+
self._record_operation('click', element=f"{resource_id}{index_desc}", ref=resource_id, index=index)
|
|
1165
|
+
return {"success": True, "message": f"✅ 点击成功: {resource_id}{index_desc}" + (f" (共 {count} 个)" if count > 1 else "")}
|
|
1166
|
+
else:
|
|
1167
|
+
return {"success": False, "message": f"❌ 索引超出范围: 找到 {count} 个元素,但请求索引 {index}"}
|
|
1151
1168
|
return {"success": False, "message": f"❌ 元素不存在: {resource_id}"}
|
|
1152
1169
|
except Exception as e:
|
|
1153
1170
|
return {"success": False, "message": f"❌ 点击失败: {e}"}
|
|
@@ -1587,8 +1604,14 @@ class BasicMobileToolsLite:
|
|
|
1587
1604
|
|
|
1588
1605
|
# ==================== 导航操作 ====================
|
|
1589
1606
|
|
|
1590
|
-
async def swipe(self, direction: str) -> Dict:
|
|
1591
|
-
"""滑动屏幕
|
|
1607
|
+
async def swipe(self, direction: str, y: Optional[int] = None, y_percent: Optional[float] = None) -> Dict:
|
|
1608
|
+
"""滑动屏幕
|
|
1609
|
+
|
|
1610
|
+
Args:
|
|
1611
|
+
direction: 滑动方向 (up/down/left/right)
|
|
1612
|
+
y: 左右滑动时指定的高度坐标(像素)
|
|
1613
|
+
y_percent: 左右滑动时指定的高度百分比 (0-100)
|
|
1614
|
+
"""
|
|
1592
1615
|
try:
|
|
1593
1616
|
if self._is_ios():
|
|
1594
1617
|
ios_client = self._get_ios_client()
|
|
@@ -1602,11 +1625,26 @@ class BasicMobileToolsLite:
|
|
|
1602
1625
|
|
|
1603
1626
|
center_x, center_y = width // 2, height // 2
|
|
1604
1627
|
|
|
1628
|
+
# 对于左右滑动,如果指定了 y 或 y_percent,使用指定的高度
|
|
1629
|
+
if direction in ['left', 'right']:
|
|
1630
|
+
if y_percent is not None:
|
|
1631
|
+
if not (0 <= y_percent <= 100):
|
|
1632
|
+
return {"success": False, "message": f"❌ y_percent 必须在 0-100 之间: {y_percent}"}
|
|
1633
|
+
swipe_y = int(height * y_percent / 100)
|
|
1634
|
+
elif y is not None:
|
|
1635
|
+
if not (0 <= y <= height):
|
|
1636
|
+
return {"success": False, "message": f"❌ y 坐标超出屏幕范围 (0-{height}): {y}"}
|
|
1637
|
+
swipe_y = y
|
|
1638
|
+
else:
|
|
1639
|
+
swipe_y = center_y
|
|
1640
|
+
else:
|
|
1641
|
+
swipe_y = center_y
|
|
1642
|
+
|
|
1605
1643
|
swipe_map = {
|
|
1606
1644
|
'up': (center_x, int(height * 0.8), center_x, int(height * 0.2)),
|
|
1607
1645
|
'down': (center_x, int(height * 0.2), center_x, int(height * 0.8)),
|
|
1608
|
-
'left': (int(width * 0.8),
|
|
1609
|
-
'right': (int(width * 0.2),
|
|
1646
|
+
'left': (int(width * 0.8), swipe_y, int(width * 0.2), swipe_y),
|
|
1647
|
+
'right': (int(width * 0.2), swipe_y, int(width * 0.8), swipe_y),
|
|
1610
1648
|
}
|
|
1611
1649
|
|
|
1612
1650
|
if direction not in swipe_map:
|
|
@@ -1619,9 +1657,23 @@ class BasicMobileToolsLite:
|
|
|
1619
1657
|
else:
|
|
1620
1658
|
self.client.u2.swipe(x1, y1, x2, y2, duration=0.5)
|
|
1621
1659
|
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1660
|
+
# 记录操作信息
|
|
1661
|
+
record_info = {'direction': direction}
|
|
1662
|
+
if y is not None:
|
|
1663
|
+
record_info['y'] = y
|
|
1664
|
+
if y_percent is not None:
|
|
1665
|
+
record_info['y_percent'] = y_percent
|
|
1666
|
+
self._record_operation('swipe', **record_info)
|
|
1667
|
+
|
|
1668
|
+
# 构建返回消息
|
|
1669
|
+
msg = f"✅ 滑动成功: {direction}"
|
|
1670
|
+
if direction in ['left', 'right']:
|
|
1671
|
+
if y_percent is not None:
|
|
1672
|
+
msg += f" (高度: {y_percent}% = {swipe_y}px)"
|
|
1673
|
+
elif y is not None:
|
|
1674
|
+
msg += f" (高度: {y}px)"
|
|
1675
|
+
|
|
1676
|
+
return {"success": True, "message": msg}
|
|
1625
1677
|
except Exception as e:
|
|
1626
1678
|
return {"success": False, "message": f"❌ 滑动失败: {e}"}
|
|
1627
1679
|
|
|
@@ -1779,7 +1831,7 @@ class BasicMobileToolsLite:
|
|
|
1779
1831
|
return ios_client.list_elements()
|
|
1780
1832
|
return [{"error": "iOS 暂不支持元素列表,建议使用截图"}]
|
|
1781
1833
|
else:
|
|
1782
|
-
xml_string = self.client.u2.dump_hierarchy()
|
|
1834
|
+
xml_string = self.client.u2.dump_hierarchy(compressed=False)
|
|
1783
1835
|
elements = self.client.xml_parser.parse(xml_string)
|
|
1784
1836
|
|
|
1785
1837
|
result = []
|
|
@@ -1816,7 +1868,7 @@ class BasicMobileToolsLite:
|
|
|
1816
1868
|
screen_height = self.client.u2.info.get('displayHeight', 1280)
|
|
1817
1869
|
|
|
1818
1870
|
# 获取元素列表
|
|
1819
|
-
xml_string = self.client.u2.dump_hierarchy()
|
|
1871
|
+
xml_string = self.client.u2.dump_hierarchy(compressed=False)
|
|
1820
1872
|
import xml.etree.ElementTree as ET
|
|
1821
1873
|
root = ET.fromstring(xml_string)
|
|
1822
1874
|
|
|
@@ -1970,7 +2022,7 @@ class BasicMobileToolsLite:
|
|
|
1970
2022
|
screen_height = self.client.u2.info.get('displayHeight', 1280)
|
|
1971
2023
|
|
|
1972
2024
|
# 获取原始 XML
|
|
1973
|
-
xml_string = self.client.u2.dump_hierarchy()
|
|
2025
|
+
xml_string = self.client.u2.dump_hierarchy(compressed=False)
|
|
1974
2026
|
|
|
1975
2027
|
# 关闭按钮的文本特征
|
|
1976
2028
|
close_texts = ['×', 'X', 'x', '关闭', '取消', 'close', 'Close', 'CLOSE', '跳过', '知道了']
|
|
@@ -2325,22 +2377,43 @@ class BasicMobileToolsLite:
|
|
|
2325
2377
|
return 0.5
|
|
2326
2378
|
|
|
2327
2379
|
def assert_text(self, text: str) -> Dict:
|
|
2328
|
-
"""
|
|
2380
|
+
"""检查页面是否包含文本(支持精确匹配和包含匹配)"""
|
|
2329
2381
|
try:
|
|
2382
|
+
exists = False
|
|
2383
|
+
match_type = ""
|
|
2384
|
+
|
|
2330
2385
|
if self._is_ios():
|
|
2331
2386
|
ios_client = self._get_ios_client()
|
|
2332
2387
|
if ios_client and hasattr(ios_client, 'wda'):
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2388
|
+
# 先尝试精确匹配
|
|
2389
|
+
if ios_client.wda(name=text).exists or ios_client.wda(label=text).exists:
|
|
2390
|
+
exists = True
|
|
2391
|
+
match_type = "精确匹配"
|
|
2392
|
+
# 再尝试包含匹配
|
|
2393
|
+
elif ios_client.wda(nameContains=text).exists or ios_client.wda(labelContains=text).exists:
|
|
2394
|
+
exists = True
|
|
2395
|
+
match_type = "包含匹配"
|
|
2396
|
+
else:
|
|
2397
|
+
# Android: 先尝试精确匹配
|
|
2398
|
+
if self.client.u2(text=text).exists():
|
|
2399
|
+
exists = True
|
|
2400
|
+
match_type = "精确匹配"
|
|
2401
|
+
# 再尝试包含匹配
|
|
2402
|
+
elif self.client.u2(textContains=text).exists():
|
|
2403
|
+
exists = True
|
|
2404
|
+
match_type = "包含匹配"
|
|
2405
|
+
|
|
2406
|
+
if exists:
|
|
2407
|
+
message = f"✅ 文本'{text}' 存在({match_type})"
|
|
2336
2408
|
else:
|
|
2337
|
-
|
|
2409
|
+
message = f"❌ 文本'{text}' 不存在"
|
|
2338
2410
|
|
|
2339
2411
|
return {
|
|
2340
2412
|
"success": True,
|
|
2341
2413
|
"found": exists,
|
|
2342
2414
|
"text": text,
|
|
2343
|
-
"
|
|
2415
|
+
"match_type": match_type if exists else None,
|
|
2416
|
+
"message": message
|
|
2344
2417
|
}
|
|
2345
2418
|
except Exception as e:
|
|
2346
2419
|
return {"success": False, "message": f"❌ 断言失败: {e}"}
|
|
@@ -2821,7 +2894,7 @@ class BasicMobileToolsLite:
|
|
|
2821
2894
|
import xml.etree.ElementTree as ET
|
|
2822
2895
|
|
|
2823
2896
|
# ========== 第1步:控件树查找关闭按钮 ==========
|
|
2824
|
-
xml_string = self.client.u2.dump_hierarchy()
|
|
2897
|
+
xml_string = self.client.u2.dump_hierarchy(compressed=False)
|
|
2825
2898
|
root = ET.fromstring(xml_string)
|
|
2826
2899
|
|
|
2827
2900
|
# 关闭按钮的常见特征
|
|
@@ -208,7 +208,7 @@ class DeviceManager:
|
|
|
208
208
|
|
|
209
209
|
try:
|
|
210
210
|
# 尝试获取页面结构,如果失败可能是无障碍服务未启用
|
|
211
|
-
xml = self.u2.dump_hierarchy()
|
|
211
|
+
xml = self.u2.dump_hierarchy(compressed=False)
|
|
212
212
|
if xml and len(xml) > 100: # 有内容说明无障碍服务正常
|
|
213
213
|
print(f" ✅ 无障碍服务: 已启用", file=sys.stderr)
|
|
214
214
|
return
|
|
@@ -235,7 +235,7 @@ class DeviceManager:
|
|
|
235
235
|
|
|
236
236
|
try:
|
|
237
237
|
# 尝试获取页面结构
|
|
238
|
-
xml = self.u2.dump_hierarchy()
|
|
238
|
+
xml = self.u2.dump_hierarchy(compressed=False)
|
|
239
239
|
if xml and len(xml) > 100:
|
|
240
240
|
return {
|
|
241
241
|
'enabled': True,
|
|
@@ -190,7 +190,7 @@ class MobileClient:
|
|
|
190
190
|
|
|
191
191
|
# Android平台
|
|
192
192
|
# 获取XML
|
|
193
|
-
xml_string = self.u2.dump_hierarchy()
|
|
193
|
+
xml_string = self.u2.dump_hierarchy(compressed=False)
|
|
194
194
|
|
|
195
195
|
# 确保xml_string是字符串类型
|
|
196
196
|
if not isinstance(xml_string, str):
|
|
@@ -321,7 +321,7 @@ class MobileClient:
|
|
|
321
321
|
# 🎯 改进:尝试模糊匹配(忽略空格、括号)
|
|
322
322
|
ref_normalized = ref.replace(' ', '').replace('(', '').replace(')', '').replace('(', '').replace(')', '')
|
|
323
323
|
# 获取所有元素,手动匹配
|
|
324
|
-
xml_string = self.u2.dump_hierarchy()
|
|
324
|
+
xml_string = self.u2.dump_hierarchy(compressed=False)
|
|
325
325
|
elements = self.xml_parser.parse(xml_string)
|
|
326
326
|
for elem in elements:
|
|
327
327
|
elem_desc = elem.get('content_desc', '')
|
|
@@ -434,7 +434,7 @@ class MobileClient:
|
|
|
434
434
|
if verify:
|
|
435
435
|
# 获取点击前页面状态
|
|
436
436
|
try:
|
|
437
|
-
initial_xml = self.u2.dump_hierarchy()
|
|
437
|
+
initial_xml = self.u2.dump_hierarchy(compressed=False)
|
|
438
438
|
initial_length = len(initial_xml)
|
|
439
439
|
|
|
440
440
|
# 等待页面变化
|
|
@@ -757,7 +757,7 @@ class MobileClient:
|
|
|
757
757
|
initial_length = 0
|
|
758
758
|
if verify:
|
|
759
759
|
try:
|
|
760
|
-
initial_xml = self.u2.dump_hierarchy()
|
|
760
|
+
initial_xml = self.u2.dump_hierarchy(compressed=False)
|
|
761
761
|
initial_length = len(initial_xml)
|
|
762
762
|
except Exception as e:
|
|
763
763
|
print(f" ⚠️ 获取初始页面状态失败: {e}", file=sys.stderr)
|
|
@@ -1008,7 +1008,7 @@ class MobileClient:
|
|
|
1008
1008
|
try:
|
|
1009
1009
|
if verify:
|
|
1010
1010
|
# 获取操作前页面状态
|
|
1011
|
-
initial_xml = self.u2.dump_hierarchy()
|
|
1011
|
+
initial_xml = self.u2.dump_hierarchy(compressed=False)
|
|
1012
1012
|
initial_length = len(initial_xml)
|
|
1013
1013
|
|
|
1014
1014
|
self.u2.press(key.lower())
|
|
@@ -1037,7 +1037,7 @@ class MobileClient:
|
|
|
1037
1037
|
# 标准按键处理
|
|
1038
1038
|
if verify:
|
|
1039
1039
|
# 获取操作前页面状态
|
|
1040
|
-
initial_xml = self.u2.dump_hierarchy()
|
|
1040
|
+
initial_xml = self.u2.dump_hierarchy(compressed=False)
|
|
1041
1041
|
initial_length = len(initial_xml)
|
|
1042
1042
|
|
|
1043
1043
|
# 使用keycode按键 - uiautomator2使用shell命令
|
|
@@ -1099,7 +1099,7 @@ class MobileClient:
|
|
|
1099
1099
|
print(f" 🔍 智能搜索键:先尝试SEARCH键...", file=sys.stderr)
|
|
1100
1100
|
|
|
1101
1101
|
# 获取初始页面状态
|
|
1102
|
-
initial_xml = self.u2.dump_hierarchy()
|
|
1102
|
+
initial_xml = self.u2.dump_hierarchy(compressed=False)
|
|
1103
1103
|
initial_length = len(initial_xml)
|
|
1104
1104
|
|
|
1105
1105
|
# 方案1: 尝试 SEARCH 键 (keycode=84)
|
|
@@ -1126,7 +1126,7 @@ class MobileClient:
|
|
|
1126
1126
|
|
|
1127
1127
|
# 方案2: 尝试 ENTER 键 (keycode=66)
|
|
1128
1128
|
# 重新获取当前页面状态(因为可能有轻微变化)
|
|
1129
|
-
current_xml = self.u2.dump_hierarchy()
|
|
1129
|
+
current_xml = self.u2.dump_hierarchy(compressed=False)
|
|
1130
1130
|
current_length = len(current_xml)
|
|
1131
1131
|
|
|
1132
1132
|
self.u2.shell('input keyevent 66')
|
|
@@ -1184,7 +1184,7 @@ class MobileClient:
|
|
|
1184
1184
|
await asyncio.sleep(0.1) # 每100ms检查一次
|
|
1185
1185
|
|
|
1186
1186
|
try:
|
|
1187
|
-
current_xml = self.u2.dump_hierarchy()
|
|
1187
|
+
current_xml = self.u2.dump_hierarchy(compressed=False)
|
|
1188
1188
|
current_length = len(current_xml)
|
|
1189
1189
|
|
|
1190
1190
|
# 计算变化百分比
|
|
@@ -61,7 +61,7 @@ class SmartWait:
|
|
|
61
61
|
while time.time() - start_time < timeout:
|
|
62
62
|
try:
|
|
63
63
|
# 获取当前页面快照(只获取元素数量,不解析详细内容)
|
|
64
|
-
xml = self.client.u2.dump_hierarchy()
|
|
64
|
+
xml = self.client.u2.dump_hierarchy(compressed=False)
|
|
65
65
|
current_snapshot = len(xml) # 使用XML长度作为简单的页面状态标识
|
|
66
66
|
|
|
67
67
|
if last_snapshot is not None:
|
|
@@ -137,14 +137,14 @@ class SmartWait:
|
|
|
137
137
|
|
|
138
138
|
try:
|
|
139
139
|
# 获取初始页面状态
|
|
140
|
-
initial_xml = self.client.u2.dump_hierarchy()
|
|
140
|
+
initial_xml = self.client.u2.dump_hierarchy(compressed=False)
|
|
141
141
|
initial_length = len(initial_xml)
|
|
142
142
|
|
|
143
143
|
while time.time() - start_time < timeout:
|
|
144
144
|
await asyncio.sleep(self.poll_interval)
|
|
145
145
|
|
|
146
146
|
try:
|
|
147
|
-
current_xml = self.client.u2.dump_hierarchy()
|
|
147
|
+
current_xml = self.client.u2.dump_hierarchy(compressed=False)
|
|
148
148
|
current_length = len(current_xml)
|
|
149
149
|
|
|
150
150
|
# 页面变化超过5%认为有变化
|
|
@@ -34,9 +34,21 @@ from pathlib import Path
|
|
|
34
34
|
from typing import Optional
|
|
35
35
|
|
|
36
36
|
# 添加项目根目录到 Python 路径
|
|
37
|
-
#
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
# 支持两种运行方式:
|
|
38
|
+
# 1. 从源码运行:__file__ 在 mcp_tools/ 目录下,往上两级到项目根目录
|
|
39
|
+
# 2. 从已安装包运行:包已安装时,mobile_mcp 应该可以直接导入
|
|
40
|
+
# 先尝试从已安装的包导入,如果失败则从源码路径导入
|
|
41
|
+
try:
|
|
42
|
+
# 尝试导入已安装的包
|
|
43
|
+
import mobile_mcp.core.mobile_client
|
|
44
|
+
import mobile_mcp.core.basic_tools_lite
|
|
45
|
+
# 如果成功,说明包已安装,不需要添加路径
|
|
46
|
+
except ImportError:
|
|
47
|
+
# 包未安装或导入失败,从源码运行
|
|
48
|
+
# __file__ 在 mcp_tools/ 目录下,往上两级到项目根目录
|
|
49
|
+
project_root = Path(__file__).parent.parent
|
|
50
|
+
if str(project_root) not in sys.path:
|
|
51
|
+
sys.path.insert(0, str(project_root))
|
|
40
52
|
|
|
41
53
|
# 尝试导入 MCP,处理可能的路径冲突
|
|
42
54
|
try:
|
|
@@ -115,8 +127,19 @@ class MobileMCPServer:
|
|
|
115
127
|
platform = self._detect_platform()
|
|
116
128
|
|
|
117
129
|
try:
|
|
118
|
-
|
|
119
|
-
|
|
130
|
+
# 尝试导入,如果失败会抛出 ImportError
|
|
131
|
+
try:
|
|
132
|
+
from mobile_mcp.core.mobile_client import MobileClient
|
|
133
|
+
from mobile_mcp.core.basic_tools_lite import BasicMobileToolsLite
|
|
134
|
+
except ImportError as import_err:
|
|
135
|
+
# 如果导入失败,尝试从源码路径导入
|
|
136
|
+
# 这通常发生在开发模式下,包未安装时
|
|
137
|
+
project_root = Path(__file__).parent.parent
|
|
138
|
+
if str(project_root) not in sys.path:
|
|
139
|
+
sys.path.insert(0, str(project_root))
|
|
140
|
+
# 再次尝试导入
|
|
141
|
+
from mobile_mcp.core.mobile_client import MobileClient
|
|
142
|
+
from mobile_mcp.core.basic_tools_lite import BasicMobileToolsLite
|
|
120
143
|
|
|
121
144
|
self.client = MobileClient(platform=platform)
|
|
122
145
|
self.tools = BasicMobileToolsLite(self.client)
|
|
@@ -308,11 +331,13 @@ class MobileMCPServer:
|
|
|
308
331
|
description="👆 通过 resource-id 点击元素(最推荐)\n\n"
|
|
309
332
|
"✅ 最稳定的定位方式\n"
|
|
310
333
|
"✅ 实时检测元素是否存在,元素不存在会报错\n"
|
|
311
|
-
"📋 使用前先调用 mobile_list_elements 获取元素 ID"
|
|
334
|
+
"📋 使用前先调用 mobile_list_elements 获取元素 ID\n"
|
|
335
|
+
"💡 当有多个相同 ID 的元素时,用 index 指定第几个(从 0 开始)",
|
|
312
336
|
inputSchema={
|
|
313
337
|
"type": "object",
|
|
314
338
|
"properties": {
|
|
315
|
-
"resource_id": {"type": "string", "description": "元素的 resource-id"}
|
|
339
|
+
"resource_id": {"type": "string", "description": "元素的 resource-id"},
|
|
340
|
+
"index": {"type": "integer", "description": "第几个元素(从 0 开始),默认 0 表示第一个", "default": 0}
|
|
316
341
|
},
|
|
317
342
|
"required": ["resource_id"]
|
|
318
343
|
}
|
|
@@ -480,7 +505,11 @@ class MobileMCPServer:
|
|
|
480
505
|
# ==================== 导航操作 ====================
|
|
481
506
|
tools.append(Tool(
|
|
482
507
|
name="mobile_swipe",
|
|
483
|
-
description="👆 滑动屏幕。方向:up/down/left/right"
|
|
508
|
+
description="👆 滑动屏幕。方向:up/down/left/right\n\n"
|
|
509
|
+
"💡 左右滑动时,可指定高度坐标或百分比:\n"
|
|
510
|
+
"- y: 指定高度坐标(像素)\n"
|
|
511
|
+
"- y_percent: 指定高度百分比 (0-100)\n"
|
|
512
|
+
"- 两者都未指定时,使用屏幕中心高度",
|
|
484
513
|
inputSchema={
|
|
485
514
|
"type": "object",
|
|
486
515
|
"properties": {
|
|
@@ -488,6 +517,14 @@ class MobileMCPServer:
|
|
|
488
517
|
"type": "string",
|
|
489
518
|
"enum": ["up", "down", "left", "right"],
|
|
490
519
|
"description": "滑动方向"
|
|
520
|
+
},
|
|
521
|
+
"y": {
|
|
522
|
+
"type": "integer",
|
|
523
|
+
"description": "左右滑动时指定的高度坐标(像素,0-屏幕高度)"
|
|
524
|
+
},
|
|
525
|
+
"y_percent": {
|
|
526
|
+
"type": "number",
|
|
527
|
+
"description": "左右滑动时指定的高度百分比 (0-100)"
|
|
491
528
|
}
|
|
492
529
|
},
|
|
493
530
|
"required": ["direction"]
|
|
@@ -825,7 +862,10 @@ class MobileMCPServer:
|
|
|
825
862
|
return [TextContent(type="text", text=self.format_response(result))]
|
|
826
863
|
|
|
827
864
|
elif name == "mobile_click_by_id":
|
|
828
|
-
result = self.tools.click_by_id(
|
|
865
|
+
result = self.tools.click_by_id(
|
|
866
|
+
arguments["resource_id"],
|
|
867
|
+
arguments.get("index", 0)
|
|
868
|
+
)
|
|
829
869
|
return [TextContent(type="text", text=self.format_response(result))]
|
|
830
870
|
|
|
831
871
|
elif name == "mobile_click_by_percent":
|
|
@@ -880,7 +920,11 @@ class MobileMCPServer:
|
|
|
880
920
|
|
|
881
921
|
# 导航
|
|
882
922
|
elif name == "mobile_swipe":
|
|
883
|
-
result = await self.tools.swipe(
|
|
923
|
+
result = await self.tools.swipe(
|
|
924
|
+
arguments["direction"],
|
|
925
|
+
y=arguments.get("y"),
|
|
926
|
+
y_percent=arguments.get("y_percent")
|
|
927
|
+
)
|
|
884
928
|
return [TextContent(type="text", text=self.format_response(result))]
|
|
885
929
|
|
|
886
930
|
elif name == "mobile_press_key":
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mobile-mcp-ai
|
|
3
|
-
Version: 2.5.
|
|
3
|
+
Version: 2.5.8
|
|
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
|
|
@@ -408,10 +408,18 @@ tidevice list
|
|
|
408
408
|
|:---:|------|------|
|
|
409
409
|
| 📋 | `mobile_list_elements` | 列出页面元素 |
|
|
410
410
|
| 📸 | `mobile_take_screenshot` | 截图 |
|
|
411
|
+
| 📸 | `mobile_screenshot_with_som` | Set-of-Mark 截图(智能标注) |
|
|
412
|
+
| 📸 | `mobile_screenshot_with_grid` | 带网格坐标的截图 |
|
|
411
413
|
| 📐 | `mobile_get_screen_size` | 屏幕尺寸 |
|
|
412
414
|
| 👆 | `mobile_click_by_text` | 文本点击 |
|
|
413
415
|
| 👆 | `mobile_click_by_id` | ID 点击 |
|
|
414
416
|
| 👆 | `mobile_click_at_coords` | 坐标点击 |
|
|
417
|
+
| 👆 | `mobile_click_by_percent` | 百分比点击 |
|
|
418
|
+
| 👆 | `mobile_click_by_som` | SoM 编号点击 |
|
|
419
|
+
| 👆 | `mobile_long_press_by_id` | ID 长按 |
|
|
420
|
+
| 👆 | `mobile_long_press_by_text` | 文本长按 |
|
|
421
|
+
| 👆 | `mobile_long_press_by_percent` | 百分比长按 |
|
|
422
|
+
| 👆 | `mobile_long_press_at_coords` | 坐标长按 |
|
|
415
423
|
| ⌨️ | `mobile_input_text_by_id` | ID 输入 |
|
|
416
424
|
| ⌨️ | `mobile_input_at_coords` | 坐标输入 |
|
|
417
425
|
| 👆 | `mobile_swipe` | 滑动 |
|
|
@@ -422,6 +430,11 @@ tidevice list
|
|
|
422
430
|
| 📦 | `mobile_list_apps` | 列出应用 |
|
|
423
431
|
| 📱 | `mobile_list_devices` | 列出设备 |
|
|
424
432
|
| 🔌 | `mobile_check_connection` | 检查连接 |
|
|
433
|
+
| 🔍 | `mobile_find_close_button` | 查找关闭按钮 |
|
|
434
|
+
| 🚫 | `mobile_close_popup` | 关闭弹窗 |
|
|
435
|
+
| 🚫 | `mobile_close_ad` | 智能关闭广告弹窗 |
|
|
436
|
+
| 🎯 | `mobile_template_close` | 模板匹配关闭弹窗 |
|
|
437
|
+
| ➕ | `mobile_template_add` | 添加 X 号模板 |
|
|
425
438
|
| ✅ | `mobile_assert_text` | 断言文本 |
|
|
426
439
|
| 📜 | `mobile_get_operation_history` | 操作历史 |
|
|
427
440
|
| 🗑️ | `mobile_clear_operation_history` | 清空历史 |
|
|
@@ -25,7 +25,7 @@ if requirements_file.exists():
|
|
|
25
25
|
|
|
26
26
|
setup(
|
|
27
27
|
name="mobile-mcp-ai",
|
|
28
|
-
version="2.5.
|
|
28
|
+
version="2.5.8", # dump_hierarchy 使用 compressed=False 获取完整 XML 树
|
|
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
|
{mobile_mcp_ai-2.5.4 → mobile_mcp_ai-2.5.8}/core/templates/close_buttons/auto_x_0112_151217.png
RENAMED
|
File without changes
|
{mobile_mcp_ai-2.5.4 → mobile_mcp_ai-2.5.8}/core/templates/close_buttons/auto_x_0112_152037.png
RENAMED
|
File without changes
|
{mobile_mcp_ai-2.5.4 → mobile_mcp_ai-2.5.8}/core/templates/close_buttons/auto_x_0112_152840.png
RENAMED
|
File without changes
|
{mobile_mcp_ai-2.5.4 → mobile_mcp_ai-2.5.8}/core/templates/close_buttons/auto_x_0112_153256.png
RENAMED
|
File without changes
|
{mobile_mcp_ai-2.5.4 → mobile_mcp_ai-2.5.8}/core/templates/close_buttons/auto_x_0112_154847.png
RENAMED
|
File without changes
|
{mobile_mcp_ai-2.5.4 → mobile_mcp_ai-2.5.8}/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
|
|
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
|