mobile-mcp-ai 2.1.2__py3-none-any.whl → 2.5.8__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.
- mobile_mcp/__init__.py +34 -0
- mobile_mcp/config.py +142 -0
- mobile_mcp/core/basic_tools_lite.py +3266 -0
- {core → mobile_mcp/core}/device_manager.py +2 -2
- mobile_mcp/core/dynamic_config.py +272 -0
- mobile_mcp/core/ios_client_wda.py +569 -0
- mobile_mcp/core/ios_device_manager_wda.py +306 -0
- {core → mobile_mcp/core}/mobile_client.py +279 -39
- mobile_mcp/core/template_matcher.py +429 -0
- mobile_mcp/core/templates/close_buttons/auto_x_0112_151217.png +0 -0
- mobile_mcp/core/templates/close_buttons/auto_x_0112_152037.png +0 -0
- mobile_mcp/core/templates/close_buttons/auto_x_0112_152840.png +0 -0
- mobile_mcp/core/templates/close_buttons/auto_x_0112_153256.png +0 -0
- mobile_mcp/core/templates/close_buttons/auto_x_0112_154847.png +0 -0
- mobile_mcp/core/templates/close_buttons/gray_x_stock_ad.png +0 -0
- {core → mobile_mcp/core}/utils/smart_wait.py +3 -3
- mobile_mcp/mcp_tools/__init__.py +10 -0
- mobile_mcp/mcp_tools/mcp_server.py +1071 -0
- mobile_mcp_ai-2.5.8.dist-info/METADATA +469 -0
- mobile_mcp_ai-2.5.8.dist-info/RECORD +32 -0
- mobile_mcp_ai-2.5.8.dist-info/entry_points.txt +2 -0
- mobile_mcp_ai-2.5.8.dist-info/licenses/LICENSE +201 -0
- mobile_mcp_ai-2.5.8.dist-info/top_level.txt +1 -0
- core/ai/__init__.py +0 -11
- core/ai/ai_analyzer.py +0 -197
- core/ai/ai_config.py +0 -116
- core/ai/ai_platform_adapter.py +0 -399
- core/ai/smart_test_executor.py +0 -520
- core/ai/test_generator.py +0 -365
- core/ai/test_generator_from_history.py +0 -391
- core/ai/test_generator_standalone.py +0 -293
- core/assertion/__init__.py +0 -9
- core/assertion/smart_assertion.py +0 -341
- core/basic_tools.py +0 -377
- core/h5/__init__.py +0 -10
- core/h5/h5_handler.py +0 -548
- core/ios_client.py +0 -219
- core/ios_device_manager.py +0 -252
- core/locator/__init__.py +0 -10
- core/locator/cursor_ai_auto_analyzer.py +0 -119
- core/locator/cursor_vision_helper.py +0 -414
- core/locator/mobile_smart_locator.py +0 -1640
- core/locator/position_analyzer.py +0 -813
- core/locator/script_updater.py +0 -157
- core/nl_test_runner.py +0 -585
- core/smart_app_launcher.py +0 -334
- core/smart_tools.py +0 -311
- mcp/__init__.py +0 -8
- mcp/mcp_server.py +0 -1919
- mcp/mcp_server_simple.py +0 -476
- mobile_mcp_ai-2.1.2.dist-info/METADATA +0 -567
- mobile_mcp_ai-2.1.2.dist-info/RECORD +0 -45
- mobile_mcp_ai-2.1.2.dist-info/entry_points.txt +0 -2
- mobile_mcp_ai-2.1.2.dist-info/top_level.txt +0 -4
- vision/__init__.py +0 -10
- vision/vision_locator.py +0 -404
- {core → mobile_mcp/core}/__init__.py +0 -0
- {core → mobile_mcp/core}/utils/__init__.py +0 -0
- {core → mobile_mcp/core}/utils/logger.py +0 -0
- {core → mobile_mcp/core}/utils/operation_history_manager.py +0 -0
- {utils → mobile_mcp/utils}/__init__.py +0 -0
- {utils → mobile_mcp/utils}/logger.py +0 -0
- {utils → mobile_mcp/utils}/xml_formatter.py +0 -0
- {utils → mobile_mcp/utils}/xml_parser.py +0 -0
- {mobile_mcp_ai-2.1.2.dist-info → mobile_mcp_ai-2.5.8.dist-info}/WHEEL +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
# -*- coding: utf-8 -*-
|
|
3
3
|
"""
|
|
4
|
-
|
|
4
|
+
|
|
5
5
|
|
|
6
6
|
功能:
|
|
7
7
|
1. 设备连接管理
|
|
@@ -23,19 +23,18 @@ from .device_manager import DeviceManager
|
|
|
23
23
|
from ..utils.xml_parser import XMLParser
|
|
24
24
|
from ..utils.xml_formatter import XMLFormatter
|
|
25
25
|
from .utils.smart_wait import SmartWait
|
|
26
|
+
from .dynamic_config import DynamicConfig
|
|
26
27
|
|
|
27
28
|
|
|
28
29
|
class MobileClient:
|
|
29
30
|
"""
|
|
30
|
-
移动端客户端 - 类似Web端的MCPClient
|
|
31
|
-
|
|
32
31
|
用法:
|
|
33
32
|
client = MobileClient(device_id=None, platform="android")
|
|
34
33
|
await client.launch_app("com.example.app")
|
|
35
34
|
await client.click("登录按钮")
|
|
36
35
|
"""
|
|
37
36
|
|
|
38
|
-
def __init__(self, device_id: Optional[str] = None, platform: str = "android", lock_orientation: bool = True):
|
|
37
|
+
def __init__(self, device_id: Optional[str] = None, platform: str = "android", lock_orientation: bool = True, lazy_connect: bool = False):
|
|
39
38
|
"""
|
|
40
39
|
初始化移动端客户端
|
|
41
40
|
|
|
@@ -43,21 +42,33 @@ class MobileClient:
|
|
|
43
42
|
device_id: 设备ID,None则自动选择第一个设备
|
|
44
43
|
platform: 平台类型 ("android" 或 "ios")
|
|
45
44
|
lock_orientation: 是否锁定屏幕方向为竖屏(默认True,仅Android有效)
|
|
45
|
+
lazy_connect: 是否延迟连接(默认False)。如果为True,则不立即连接设备
|
|
46
46
|
"""
|
|
47
47
|
self.platform = platform
|
|
48
|
+
self._device_id = device_id
|
|
49
|
+
self._lazy_connect = lazy_connect
|
|
48
50
|
|
|
49
51
|
if platform == "android":
|
|
50
52
|
self.device_manager = DeviceManager(platform="android")
|
|
51
|
-
|
|
53
|
+
if not lazy_connect:
|
|
54
|
+
self.u2 = self.device_manager.connect(device_id)
|
|
55
|
+
else:
|
|
56
|
+
self.u2 = None
|
|
52
57
|
self.driver = None # iOS使用
|
|
53
58
|
|
|
54
59
|
# 初始化智能等待工具
|
|
55
|
-
|
|
60
|
+
if not lazy_connect:
|
|
61
|
+
self.smart_wait = SmartWait(self)
|
|
62
|
+
else:
|
|
63
|
+
self.smart_wait = None
|
|
56
64
|
elif platform == "ios":
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
self.
|
|
60
|
-
self.
|
|
65
|
+
# 🍎 iOS 支持:使用 tidevice + facebook-wda
|
|
66
|
+
from .ios_client_wda import IOSClientWDA
|
|
67
|
+
self._ios_client = IOSClientWDA(device_id=device_id, lazy_connect=lazy_connect)
|
|
68
|
+
self.device_manager = self._ios_client.device_manager
|
|
69
|
+
self.wda = self._ios_client.wda if not lazy_connect else None
|
|
70
|
+
self.driver = None
|
|
71
|
+
self.u2 = None
|
|
61
72
|
else:
|
|
62
73
|
raise ValueError(f"不支持的平台: {platform}")
|
|
63
74
|
|
|
@@ -164,8 +175,22 @@ class MobileClient:
|
|
|
164
175
|
if current_time - self._cache_timestamp < self._cache_ttl:
|
|
165
176
|
return self._snapshot_cache
|
|
166
177
|
|
|
178
|
+
# iOS平台使用不同的实现
|
|
179
|
+
if self.platform == "ios":
|
|
180
|
+
if not self.driver:
|
|
181
|
+
raise RuntimeError("iOS设备未连接")
|
|
182
|
+
# 获取iOS页面源码
|
|
183
|
+
xml_string = self.driver.page_source
|
|
184
|
+
if not isinstance(xml_string, str):
|
|
185
|
+
xml_string = str(xml_string)
|
|
186
|
+
# iOS的XML格式可能不同,直接返回或简单格式化
|
|
187
|
+
self._snapshot_cache = xml_string
|
|
188
|
+
self._cache_timestamp = time.time()
|
|
189
|
+
return xml_string
|
|
190
|
+
|
|
191
|
+
# Android平台
|
|
167
192
|
# 获取XML
|
|
168
|
-
xml_string = self.u2.dump_hierarchy()
|
|
193
|
+
xml_string = self.u2.dump_hierarchy(compressed=False)
|
|
169
194
|
|
|
170
195
|
# 确保xml_string是字符串类型
|
|
171
196
|
if not isinstance(xml_string, str):
|
|
@@ -296,7 +321,7 @@ class MobileClient:
|
|
|
296
321
|
# 🎯 改进:尝试模糊匹配(忽略空格、括号)
|
|
297
322
|
ref_normalized = ref.replace(' ', '').replace('(', '').replace(')', '').replace('(', '').replace(')', '')
|
|
298
323
|
# 获取所有元素,手动匹配
|
|
299
|
-
xml_string = self.u2.dump_hierarchy()
|
|
324
|
+
xml_string = self.u2.dump_hierarchy(compressed=False)
|
|
300
325
|
elements = self.xml_parser.parse(xml_string)
|
|
301
326
|
for elem in elements:
|
|
302
327
|
elem_desc = elem.get('content_desc', '')
|
|
@@ -355,10 +380,12 @@ class MobileClient:
|
|
|
355
380
|
break
|
|
356
381
|
|
|
357
382
|
if not found:
|
|
358
|
-
# 🎯
|
|
359
|
-
|
|
383
|
+
# 🎯 定位失败,提示用户
|
|
384
|
+
# 注意:CursorVisionHelper 是实验性功能,当前版本建议使用 MCP 方式
|
|
385
|
+
print(f" ⚠️ 元素'{ref}'未找到", file=sys.stderr)
|
|
360
386
|
try:
|
|
361
387
|
from .locator.cursor_vision_helper import CursorVisionHelper
|
|
388
|
+
print(f" 🔍 尝试使用Cursor AI视觉识别...", file=sys.stderr)
|
|
362
389
|
cursor_helper = CursorVisionHelper(self)
|
|
363
390
|
# 🎯 传递 auto_analyze=True,自动创建请求文件并等待结果
|
|
364
391
|
cursor_result = await cursor_helper.analyze_with_cursor(element, auto_analyze=True)
|
|
@@ -390,19 +417,24 @@ class MobileClient:
|
|
|
390
417
|
# 其他情况,抛出异常
|
|
391
418
|
screenshot_path = cursor_result.get('screenshot_path', 'unknown') if cursor_result else 'unknown'
|
|
392
419
|
raise ValueError(f"Cursor AI分析失败: {screenshot_path}")
|
|
420
|
+
except ImportError:
|
|
421
|
+
# CursorVisionHelper 模块不存在,跳过视觉识别
|
|
422
|
+
print(f" 💡 提示:建议使用 MCP 方式调用,Cursor AI 会自动进行视觉识别", file=sys.stderr)
|
|
393
423
|
except ValueError as ve:
|
|
394
424
|
if "Cursor AI" in str(ve):
|
|
395
425
|
raise ve
|
|
396
426
|
print(f" ⚠️ Cursor视觉识别失败: {ve}", file=sys.stderr)
|
|
427
|
+
except Exception as e:
|
|
428
|
+
print(f" ⚠️ 视觉识别异常: {e}", file=sys.stderr)
|
|
397
429
|
|
|
398
|
-
raise ValueError(f"无法找到元素: {ref}
|
|
430
|
+
raise ValueError(f"无法找到元素: {ref}(建议使用 MCP 方式,Cursor AI 会自动进行视觉识别)")
|
|
399
431
|
|
|
400
432
|
# 验证点击(可选)
|
|
401
433
|
page_changed = False
|
|
402
434
|
if verify:
|
|
403
435
|
# 获取点击前页面状态
|
|
404
436
|
try:
|
|
405
|
-
initial_xml = self.u2.dump_hierarchy()
|
|
437
|
+
initial_xml = self.u2.dump_hierarchy(compressed=False)
|
|
406
438
|
initial_length = len(initial_xml)
|
|
407
439
|
|
|
408
440
|
# 等待页面变化
|
|
@@ -675,6 +707,31 @@ class MobileClient:
|
|
|
675
707
|
- verified: 是否经过验证
|
|
676
708
|
- page_changed: 页面是否变化(仅 verify=True)
|
|
677
709
|
"""
|
|
710
|
+
# iOS平台使用不同的实现
|
|
711
|
+
if self.platform == "ios":
|
|
712
|
+
if not self.driver:
|
|
713
|
+
return {"success": False, "reason": "iOS设备未连接"}
|
|
714
|
+
try:
|
|
715
|
+
size = self.driver.get_window_size()
|
|
716
|
+
width = size['width']
|
|
717
|
+
height = size['height']
|
|
718
|
+
|
|
719
|
+
if direction == 'up':
|
|
720
|
+
self.driver.swipe(width // 2, int(height * 0.8), width // 2, int(height * 0.2))
|
|
721
|
+
elif direction == 'down':
|
|
722
|
+
self.driver.swipe(width // 2, int(height * 0.2), width // 2, int(height * 0.8))
|
|
723
|
+
elif direction == 'left':
|
|
724
|
+
self.driver.swipe(int(width * 0.8), height // 2, int(width * 0.2), height // 2)
|
|
725
|
+
elif direction == 'right':
|
|
726
|
+
self.driver.swipe(int(width * 0.2), height // 2, int(width * 0.8), height // 2)
|
|
727
|
+
else:
|
|
728
|
+
return {"success": False, "reason": f"不支持的滑动方向: {direction}"}
|
|
729
|
+
|
|
730
|
+
return {"success": True, "direction": direction}
|
|
731
|
+
except Exception as e:
|
|
732
|
+
return {"success": False, "reason": str(e)}
|
|
733
|
+
|
|
734
|
+
# Android平台
|
|
678
735
|
# 获取屏幕尺寸
|
|
679
736
|
width, height = self.u2.window_size()
|
|
680
737
|
|
|
@@ -700,7 +757,7 @@ class MobileClient:
|
|
|
700
757
|
initial_length = 0
|
|
701
758
|
if verify:
|
|
702
759
|
try:
|
|
703
|
-
initial_xml = self.u2.dump_hierarchy()
|
|
760
|
+
initial_xml = self.u2.dump_hierarchy(compressed=False)
|
|
704
761
|
initial_length = len(initial_xml)
|
|
705
762
|
except Exception as e:
|
|
706
763
|
print(f" ⚠️ 获取初始页面状态失败: {e}", file=sys.stderr)
|
|
@@ -735,23 +792,45 @@ class MobileClient:
|
|
|
735
792
|
|
|
736
793
|
async def launch_app(self, package_name: str, wait_time: int = 3, smart_wait: bool = True):
|
|
737
794
|
"""
|
|
738
|
-
启动App
|
|
795
|
+
启动App(快速模式:最多等待3秒+截图验证)
|
|
739
796
|
|
|
740
797
|
Args:
|
|
741
|
-
package_name: App
|
|
742
|
-
wait_time: 等待App启动的时间(秒)-
|
|
743
|
-
smart_wait:
|
|
798
|
+
package_name: App包名(Android)或Bundle ID(iOS),如 "com.example.app"
|
|
799
|
+
wait_time: 等待App启动的时间(秒)- 默认3秒
|
|
800
|
+
smart_wait: 是否启用智能等待(自动关闭广告、截图验证)- 仅Android
|
|
744
801
|
|
|
745
802
|
Returns:
|
|
746
|
-
|
|
803
|
+
操作结果(包含screenshot_path字段供AI验证)
|
|
747
804
|
"""
|
|
748
805
|
try:
|
|
806
|
+
# iOS平台使用不同的实现
|
|
807
|
+
if self.platform == "ios":
|
|
808
|
+
if not self.driver:
|
|
809
|
+
return {"success": False, "reason": "iOS设备未连接"}
|
|
810
|
+
try:
|
|
811
|
+
print(f" 📱 启动iOS App: {package_name}", file=sys.stderr)
|
|
812
|
+
self.driver.activate_app(package_name)
|
|
813
|
+
await asyncio.sleep(wait_time)
|
|
814
|
+
|
|
815
|
+
# 验证是否启动成功
|
|
816
|
+
current = await self.get_current_package()
|
|
817
|
+
if current == package_name:
|
|
818
|
+
print(f" ✅ iOS App启动成功: {package_name}", file=sys.stderr)
|
|
819
|
+
return {"success": True, "package": package_name}
|
|
820
|
+
else:
|
|
821
|
+
print(f" ⚠️ iOS App可能未启动成功,当前App: {current},期望: {package_name}", file=sys.stderr)
|
|
822
|
+
return {"success": True, "package": package_name, "warning": f"当前App: {current}"}
|
|
823
|
+
except Exception as e:
|
|
824
|
+
print(f" ❌ iOS App启动异常: {e}", file=sys.stderr)
|
|
825
|
+
return {"success": False, "reason": str(e)}
|
|
826
|
+
|
|
827
|
+
# Android平台
|
|
749
828
|
# 🎯 优先使用智能启动(推荐)
|
|
750
829
|
if smart_wait:
|
|
751
830
|
from .smart_app_launcher import SmartAppLauncher
|
|
752
831
|
launcher = SmartAppLauncher(self)
|
|
753
|
-
#
|
|
754
|
-
smart_wait_time = min(
|
|
832
|
+
# 优化:快速模式,最多3秒
|
|
833
|
+
smart_wait_time = min(wait_time, 3)
|
|
755
834
|
|
|
756
835
|
# 🎯 从环境变量读取是否自动关闭广告(默认True)
|
|
757
836
|
import os
|
|
@@ -762,6 +841,12 @@ class MobileClient:
|
|
|
762
841
|
max_wait=smart_wait_time,
|
|
763
842
|
auto_close_ads=auto_close_ads
|
|
764
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
|
+
|
|
765
850
|
return result
|
|
766
851
|
|
|
767
852
|
# 传统方式(快速启动,不等待加载)
|
|
@@ -802,13 +887,27 @@ class MobileClient:
|
|
|
802
887
|
停止App
|
|
803
888
|
|
|
804
889
|
Args:
|
|
805
|
-
package_name: App
|
|
890
|
+
package_name: App包名(Android)或Bundle ID(iOS)
|
|
806
891
|
|
|
807
892
|
Returns:
|
|
808
893
|
操作结果
|
|
809
894
|
"""
|
|
810
895
|
try:
|
|
811
896
|
print(f" 📱 停止App: {package_name}", file=sys.stderr)
|
|
897
|
+
|
|
898
|
+
# iOS平台使用不同的实现
|
|
899
|
+
if self.platform == "ios":
|
|
900
|
+
if not self.driver:
|
|
901
|
+
return {"success": False, "reason": "iOS设备未连接"}
|
|
902
|
+
try:
|
|
903
|
+
self.driver.terminate_app(package_name)
|
|
904
|
+
print(f" ✅ iOS App已停止: {package_name}", file=sys.stderr)
|
|
905
|
+
return {"success": True}
|
|
906
|
+
except Exception as e:
|
|
907
|
+
print(f" ❌ iOS App停止失败: {e}", file=sys.stderr)
|
|
908
|
+
return {"success": False, "reason": str(e)}
|
|
909
|
+
|
|
910
|
+
# Android平台
|
|
812
911
|
self.u2.app_stop(package_name)
|
|
813
912
|
print(f" ✅ App已停止: {package_name}", file=sys.stderr)
|
|
814
913
|
return {"success": True}
|
|
@@ -818,14 +917,19 @@ class MobileClient:
|
|
|
818
917
|
|
|
819
918
|
async def get_current_package(self) -> Optional[str]:
|
|
820
919
|
"""
|
|
821
|
-
获取当前App
|
|
920
|
+
获取当前App包名(Android)或Bundle ID(iOS)
|
|
822
921
|
|
|
823
922
|
Returns:
|
|
824
|
-
|
|
923
|
+
包名/Bundle ID或None
|
|
825
924
|
"""
|
|
826
925
|
try:
|
|
827
|
-
|
|
828
|
-
|
|
926
|
+
if self.platform == "ios":
|
|
927
|
+
if not self.driver:
|
|
928
|
+
return None
|
|
929
|
+
return self.driver.current_package
|
|
930
|
+
else:
|
|
931
|
+
info = self.u2.app_current()
|
|
932
|
+
return info.get('package')
|
|
829
933
|
except:
|
|
830
934
|
return None
|
|
831
935
|
|
|
@@ -853,6 +957,34 @@ class MobileClient:
|
|
|
853
957
|
- page_changed: 页面是否变化(仅 verify=True 时)
|
|
854
958
|
- fallback_used: 是否使用了备选方案(仅搜索键)
|
|
855
959
|
"""
|
|
960
|
+
# iOS平台使用不同的实现
|
|
961
|
+
if self.platform == "ios":
|
|
962
|
+
if not self.driver:
|
|
963
|
+
return {"success": False, "reason": "iOS设备未连接"}
|
|
964
|
+
try:
|
|
965
|
+
# iOS按键映射(使用XCUITest的按键)
|
|
966
|
+
ios_key_map = {
|
|
967
|
+
'enter': 'return',
|
|
968
|
+
'回车': 'return',
|
|
969
|
+
'back': 'back',
|
|
970
|
+
'返回': 'back',
|
|
971
|
+
'home': 'home',
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
key_lower = key.lower()
|
|
975
|
+
if key_lower in ios_key_map:
|
|
976
|
+
ios_key = ios_key_map[key_lower]
|
|
977
|
+
# iOS使用execute_script发送按键
|
|
978
|
+
self.driver.execute_script("mobile: pressButton", {"name": ios_key})
|
|
979
|
+
print(f" ✅ iOS按键成功: {key} ({ios_key})", file=sys.stderr)
|
|
980
|
+
return {"success": True, "key": key, "verified": False}
|
|
981
|
+
else:
|
|
982
|
+
return {"success": False, "reason": f"iOS不支持的按键: {key}"}
|
|
983
|
+
except Exception as e:
|
|
984
|
+
print(f" ❌ iOS按键失败: {e}", file=sys.stderr)
|
|
985
|
+
return {"success": False, "reason": str(e)}
|
|
986
|
+
|
|
987
|
+
# Android平台
|
|
856
988
|
key_map = {
|
|
857
989
|
'enter': 66, # KEYCODE_ENTER
|
|
858
990
|
'回车': 66,
|
|
@@ -876,7 +1008,7 @@ class MobileClient:
|
|
|
876
1008
|
try:
|
|
877
1009
|
if verify:
|
|
878
1010
|
# 获取操作前页面状态
|
|
879
|
-
initial_xml = self.u2.dump_hierarchy()
|
|
1011
|
+
initial_xml = self.u2.dump_hierarchy(compressed=False)
|
|
880
1012
|
initial_length = len(initial_xml)
|
|
881
1013
|
|
|
882
1014
|
self.u2.press(key.lower())
|
|
@@ -905,7 +1037,7 @@ class MobileClient:
|
|
|
905
1037
|
# 标准按键处理
|
|
906
1038
|
if verify:
|
|
907
1039
|
# 获取操作前页面状态
|
|
908
|
-
initial_xml = self.u2.dump_hierarchy()
|
|
1040
|
+
initial_xml = self.u2.dump_hierarchy(compressed=False)
|
|
909
1041
|
initial_length = len(initial_xml)
|
|
910
1042
|
|
|
911
1043
|
# 使用keycode按键 - uiautomator2使用shell命令
|
|
@@ -967,7 +1099,7 @@ class MobileClient:
|
|
|
967
1099
|
print(f" 🔍 智能搜索键:先尝试SEARCH键...", file=sys.stderr)
|
|
968
1100
|
|
|
969
1101
|
# 获取初始页面状态
|
|
970
|
-
initial_xml = self.u2.dump_hierarchy()
|
|
1102
|
+
initial_xml = self.u2.dump_hierarchy(compressed=False)
|
|
971
1103
|
initial_length = len(initial_xml)
|
|
972
1104
|
|
|
973
1105
|
# 方案1: 尝试 SEARCH 键 (keycode=84)
|
|
@@ -994,7 +1126,7 @@ class MobileClient:
|
|
|
994
1126
|
|
|
995
1127
|
# 方案2: 尝试 ENTER 键 (keycode=66)
|
|
996
1128
|
# 重新获取当前页面状态(因为可能有轻微变化)
|
|
997
|
-
current_xml = self.u2.dump_hierarchy()
|
|
1129
|
+
current_xml = self.u2.dump_hierarchy(compressed=False)
|
|
998
1130
|
current_length = len(current_xml)
|
|
999
1131
|
|
|
1000
1132
|
self.u2.shell('input keyevent 66')
|
|
@@ -1028,25 +1160,31 @@ class MobileClient:
|
|
|
1028
1160
|
print(f" ❌ 搜索键执行失败: {e}", file=sys.stderr)
|
|
1029
1161
|
return {"success": False, "reason": str(e)}
|
|
1030
1162
|
|
|
1031
|
-
async def _verify_page_change(self, initial_length: int, timeout: float =
|
|
1163
|
+
async def _verify_page_change(self, initial_length: int, timeout: float = None, change_threshold: float = None) -> bool:
|
|
1032
1164
|
"""
|
|
1033
1165
|
验证页面是否发生变化
|
|
1034
1166
|
|
|
1035
1167
|
Args:
|
|
1036
1168
|
initial_length: 初始页面XML长度
|
|
1037
|
-
timeout:
|
|
1038
|
-
change_threshold:
|
|
1169
|
+
timeout: 最大等待时间(秒),None则使用动态配置
|
|
1170
|
+
change_threshold: 变化阈值(百分比),None则使用动态配置
|
|
1039
1171
|
|
|
1040
1172
|
Returns:
|
|
1041
1173
|
页面是否发生了明显变化
|
|
1042
1174
|
"""
|
|
1175
|
+
# 使用动态配置(支持AI调整)
|
|
1176
|
+
if timeout is None:
|
|
1177
|
+
timeout = DynamicConfig.page_change_timeout
|
|
1178
|
+
if change_threshold is None:
|
|
1179
|
+
change_threshold = DynamicConfig.page_change_threshold
|
|
1180
|
+
|
|
1043
1181
|
start_time = time.time()
|
|
1044
1182
|
|
|
1045
1183
|
while time.time() - start_time < timeout:
|
|
1046
1184
|
await asyncio.sleep(0.1) # 每100ms检查一次
|
|
1047
1185
|
|
|
1048
1186
|
try:
|
|
1049
|
-
current_xml = self.u2.dump_hierarchy()
|
|
1187
|
+
current_xml = self.u2.dump_hierarchy(compressed=False)
|
|
1050
1188
|
current_length = len(current_xml)
|
|
1051
1189
|
|
|
1052
1190
|
# 计算变化百分比
|
|
@@ -1054,8 +1192,9 @@ class MobileClient:
|
|
|
1054
1192
|
|
|
1055
1193
|
if change_percent > change_threshold:
|
|
1056
1194
|
print(f" 📊 页面变化检测: {change_percent*100:.1f}% (阈值: {change_threshold*100}%)", file=sys.stderr)
|
|
1057
|
-
#
|
|
1058
|
-
await asyncio.sleep(
|
|
1195
|
+
# 等待页面稳定(使用动态配置)
|
|
1196
|
+
await asyncio.sleep(DynamicConfig.wait_page_stable)
|
|
1197
|
+
print(f" ⏳ 已等待页面稳定 {DynamicConfig.wait_page_stable}秒", file=sys.stderr)
|
|
1059
1198
|
return True
|
|
1060
1199
|
except Exception as e:
|
|
1061
1200
|
print(f" ⚠️ 页面变化检测异常: {e}", file=sys.stderr)
|
|
@@ -1080,4 +1219,105 @@ class MobileClient:
|
|
|
1080
1219
|
x1, y1, x2, y2 = map(int, match.groups())
|
|
1081
1220
|
return ((x1 + x2) // 2, (y1 + y2) // 2)
|
|
1082
1221
|
return (0, 0)
|
|
1222
|
+
|
|
1223
|
+
async def _ios_click(self, element: str, ref: Optional[str] = None):
|
|
1224
|
+
"""
|
|
1225
|
+
iOS平台的点击实现
|
|
1226
|
+
|
|
1227
|
+
Args:
|
|
1228
|
+
element: 元素描述
|
|
1229
|
+
ref: 元素定位器
|
|
1230
|
+
|
|
1231
|
+
Returns:
|
|
1232
|
+
操作结果
|
|
1233
|
+
"""
|
|
1234
|
+
try:
|
|
1235
|
+
from selenium.webdriver.common.by import By
|
|
1236
|
+
|
|
1237
|
+
# 如果提供了ref,直接使用
|
|
1238
|
+
if ref:
|
|
1239
|
+
if ref.startswith('//') or ref.startswith('/'):
|
|
1240
|
+
# XPath
|
|
1241
|
+
elem = self.driver.find_element(By.XPATH, ref)
|
|
1242
|
+
elif ref.startswith('id='):
|
|
1243
|
+
# accessibility_id
|
|
1244
|
+
elem = self.driver.find_element(By.ID, ref.replace('id=', ''))
|
|
1245
|
+
else:
|
|
1246
|
+
# 默认作为accessibility_id
|
|
1247
|
+
elem = self.driver.find_element(By.ID, ref)
|
|
1248
|
+
else:
|
|
1249
|
+
# 尝试多种定位方式
|
|
1250
|
+
selectors = [
|
|
1251
|
+
(By.XPATH, f"//*[@name='{element}']"),
|
|
1252
|
+
(By.XPATH, f"//*[@label='{element}']"),
|
|
1253
|
+
(By.XPATH, f"//*[contains(@name, '{element}')]"),
|
|
1254
|
+
]
|
|
1255
|
+
|
|
1256
|
+
elem = None
|
|
1257
|
+
for by, selector in selectors:
|
|
1258
|
+
try:
|
|
1259
|
+
elem = self.driver.find_element(by, selector)
|
|
1260
|
+
break
|
|
1261
|
+
except:
|
|
1262
|
+
continue
|
|
1263
|
+
|
|
1264
|
+
if not elem:
|
|
1265
|
+
raise ValueError(f"未找到元素: {element}")
|
|
1266
|
+
|
|
1267
|
+
elem.click()
|
|
1268
|
+
|
|
1269
|
+
# 记录操作
|
|
1270
|
+
self.operation_history.append({
|
|
1271
|
+
'action': 'click',
|
|
1272
|
+
'element': element,
|
|
1273
|
+
'ref': ref or 'auto',
|
|
1274
|
+
'success': True
|
|
1275
|
+
})
|
|
1276
|
+
|
|
1277
|
+
return {"success": True, "ref": ref or element}
|
|
1278
|
+
|
|
1279
|
+
except Exception as e:
|
|
1280
|
+
return {"success": False, "reason": str(e)}
|
|
1281
|
+
|
|
1282
|
+
async def _ios_type_text(self, element: str, text: str, ref: Optional[str] = None):
|
|
1283
|
+
"""
|
|
1284
|
+
iOS平台的输入文本实现
|
|
1285
|
+
|
|
1286
|
+
Args:
|
|
1287
|
+
element: 元素描述
|
|
1288
|
+
text: 要输入的文本
|
|
1289
|
+
ref: 元素定位器
|
|
1290
|
+
|
|
1291
|
+
Returns:
|
|
1292
|
+
操作结果
|
|
1293
|
+
"""
|
|
1294
|
+
try:
|
|
1295
|
+
from selenium.webdriver.common.by import By
|
|
1296
|
+
|
|
1297
|
+
# 定位输入框
|
|
1298
|
+
if ref:
|
|
1299
|
+
if ref.startswith('//'):
|
|
1300
|
+
elem = self.driver.find_element(By.XPATH, ref)
|
|
1301
|
+
else:
|
|
1302
|
+
elem = self.driver.find_element(By.ID, ref)
|
|
1303
|
+
else:
|
|
1304
|
+
# 查找第一个输入框
|
|
1305
|
+
elem = self.driver.find_element(By.XPATH, "//XCUIElementTypeTextField | //XCUIElementTypeSecureTextField")
|
|
1306
|
+
|
|
1307
|
+
elem.clear()
|
|
1308
|
+
elem.send_keys(text)
|
|
1309
|
+
|
|
1310
|
+
# 记录操作
|
|
1311
|
+
self.operation_history.append({
|
|
1312
|
+
'action': 'type',
|
|
1313
|
+
'element': element,
|
|
1314
|
+
'text': text,
|
|
1315
|
+
'ref': ref or 'auto',
|
|
1316
|
+
'success': True
|
|
1317
|
+
})
|
|
1318
|
+
|
|
1319
|
+
return {"success": True, "ref": ref or element}
|
|
1320
|
+
|
|
1321
|
+
except Exception as e:
|
|
1322
|
+
return {"success": False, "reason": str(e)}
|
|
1083
1323
|
|