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.
Files changed (65) hide show
  1. mobile_mcp/__init__.py +34 -0
  2. mobile_mcp/config.py +142 -0
  3. mobile_mcp/core/basic_tools_lite.py +3266 -0
  4. {core → mobile_mcp/core}/device_manager.py +2 -2
  5. mobile_mcp/core/dynamic_config.py +272 -0
  6. mobile_mcp/core/ios_client_wda.py +569 -0
  7. mobile_mcp/core/ios_device_manager_wda.py +306 -0
  8. {core → mobile_mcp/core}/mobile_client.py +279 -39
  9. mobile_mcp/core/template_matcher.py +429 -0
  10. mobile_mcp/core/templates/close_buttons/auto_x_0112_151217.png +0 -0
  11. mobile_mcp/core/templates/close_buttons/auto_x_0112_152037.png +0 -0
  12. mobile_mcp/core/templates/close_buttons/auto_x_0112_152840.png +0 -0
  13. mobile_mcp/core/templates/close_buttons/auto_x_0112_153256.png +0 -0
  14. mobile_mcp/core/templates/close_buttons/auto_x_0112_154847.png +0 -0
  15. mobile_mcp/core/templates/close_buttons/gray_x_stock_ad.png +0 -0
  16. {core → mobile_mcp/core}/utils/smart_wait.py +3 -3
  17. mobile_mcp/mcp_tools/__init__.py +10 -0
  18. mobile_mcp/mcp_tools/mcp_server.py +1071 -0
  19. mobile_mcp_ai-2.5.8.dist-info/METADATA +469 -0
  20. mobile_mcp_ai-2.5.8.dist-info/RECORD +32 -0
  21. mobile_mcp_ai-2.5.8.dist-info/entry_points.txt +2 -0
  22. mobile_mcp_ai-2.5.8.dist-info/licenses/LICENSE +201 -0
  23. mobile_mcp_ai-2.5.8.dist-info/top_level.txt +1 -0
  24. core/ai/__init__.py +0 -11
  25. core/ai/ai_analyzer.py +0 -197
  26. core/ai/ai_config.py +0 -116
  27. core/ai/ai_platform_adapter.py +0 -399
  28. core/ai/smart_test_executor.py +0 -520
  29. core/ai/test_generator.py +0 -365
  30. core/ai/test_generator_from_history.py +0 -391
  31. core/ai/test_generator_standalone.py +0 -293
  32. core/assertion/__init__.py +0 -9
  33. core/assertion/smart_assertion.py +0 -341
  34. core/basic_tools.py +0 -377
  35. core/h5/__init__.py +0 -10
  36. core/h5/h5_handler.py +0 -548
  37. core/ios_client.py +0 -219
  38. core/ios_device_manager.py +0 -252
  39. core/locator/__init__.py +0 -10
  40. core/locator/cursor_ai_auto_analyzer.py +0 -119
  41. core/locator/cursor_vision_helper.py +0 -414
  42. core/locator/mobile_smart_locator.py +0 -1640
  43. core/locator/position_analyzer.py +0 -813
  44. core/locator/script_updater.py +0 -157
  45. core/nl_test_runner.py +0 -585
  46. core/smart_app_launcher.py +0 -334
  47. core/smart_tools.py +0 -311
  48. mcp/__init__.py +0 -8
  49. mcp/mcp_server.py +0 -1919
  50. mcp/mcp_server_simple.py +0 -476
  51. mobile_mcp_ai-2.1.2.dist-info/METADATA +0 -567
  52. mobile_mcp_ai-2.1.2.dist-info/RECORD +0 -45
  53. mobile_mcp_ai-2.1.2.dist-info/entry_points.txt +0 -2
  54. mobile_mcp_ai-2.1.2.dist-info/top_level.txt +0 -4
  55. vision/__init__.py +0 -10
  56. vision/vision_locator.py +0 -404
  57. {core → mobile_mcp/core}/__init__.py +0 -0
  58. {core → mobile_mcp/core}/utils/__init__.py +0 -0
  59. {core → mobile_mcp/core}/utils/logger.py +0 -0
  60. {core → mobile_mcp/core}/utils/operation_history_manager.py +0 -0
  61. {utils → mobile_mcp/utils}/__init__.py +0 -0
  62. {utils → mobile_mcp/utils}/logger.py +0 -0
  63. {utils → mobile_mcp/utils}/xml_formatter.py +0 -0
  64. {utils → mobile_mcp/utils}/xml_parser.py +0 -0
  65. {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
- 移动端客户端 - 类似Web端的MCPClient
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
- self.u2 = self.device_manager.connect(device_id)
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
- self.smart_wait = SmartWait(self)
60
+ if not lazy_connect:
61
+ self.smart_wait = SmartWait(self)
62
+ else:
63
+ self.smart_wait = None
56
64
  elif platform == "ios":
57
- from .ios_device_manager import IOSDeviceManager
58
- self.device_manager = IOSDeviceManager()
59
- self.driver = self.device_manager.connect(device_id)
60
- self.u2 = None # Android使用
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
- # 🎯 定位失败,自动使用Cursor AI视觉识别(截图分析)
359
- print(f" ⚠️ 元素'{ref}'未找到,自动使用Cursor AI视觉识别(截图分析)...", file=sys.stderr)
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}(已等待3秒,并尝试Cursor视觉识别,可能元素不存在)")
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包名(如 "com.example.app"
742
- wait_time: 等待App启动的时间(秒)- 仅在smart_wait=False时使用
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
- # 优化:默认5秒,最多不超过8
754
- smart_wait_time = min(max(5, wait_time), 8)
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
- 包名或None
923
+ 包名/Bundle ID或None
825
924
  """
826
925
  try:
827
- info = self.u2.app_current()
828
- return info.get('package')
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 = 2.0, change_threshold: float = 0.05) -> bool:
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: 变化阈值(百分比,默认5%)
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(0.3)
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