mobile-mcp-ai 2.2.6__py3-none-any.whl → 2.5.3__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/config.py +3 -2
- mobile_mcp/core/basic_tools_lite.py +3193 -0
- mobile_mcp/core/ios_client_wda.py +569 -0
- mobile_mcp/core/ios_device_manager_wda.py +306 -0
- mobile_mcp/core/mobile_client.py +246 -20
- 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
- mobile_mcp/mcp_tools/__init__.py +10 -0
- mobile_mcp/mcp_tools/mcp_server.py +992 -0
- mobile_mcp_ai-2.5.3.dist-info/METADATA +456 -0
- mobile_mcp_ai-2.5.3.dist-info/RECORD +32 -0
- mobile_mcp_ai-2.5.3.dist-info/entry_points.txt +2 -0
- mobile_mcp/core/ai/__init__.py +0 -11
- mobile_mcp/core/ai/ai_analyzer.py +0 -197
- mobile_mcp/core/ai/ai_config.py +0 -116
- mobile_mcp/core/ai/ai_platform_adapter.py +0 -399
- mobile_mcp/core/ai/smart_test_executor.py +0 -520
- mobile_mcp/core/ai/test_generator.py +0 -365
- mobile_mcp/core/ai/test_generator_from_history.py +0 -391
- mobile_mcp/core/ai/test_generator_standalone.py +0 -293
- mobile_mcp/core/assertion/__init__.py +0 -9
- mobile_mcp/core/assertion/smart_assertion.py +0 -341
- mobile_mcp/core/basic_tools.py +0 -945
- mobile_mcp/core/h5/__init__.py +0 -10
- mobile_mcp/core/h5/h5_handler.py +0 -548
- mobile_mcp/core/ios_client.py +0 -219
- mobile_mcp/core/ios_device_manager.py +0 -252
- mobile_mcp/core/locator/__init__.py +0 -10
- mobile_mcp/core/locator/cursor_ai_auto_analyzer.py +0 -119
- mobile_mcp/core/locator/cursor_vision_helper.py +0 -414
- mobile_mcp/core/locator/mobile_smart_locator.py +0 -1747
- mobile_mcp/core/locator/position_analyzer.py +0 -813
- mobile_mcp/core/locator/script_updater.py +0 -157
- mobile_mcp/core/nl_test_runner.py +0 -585
- mobile_mcp/core/smart_app_launcher.py +0 -421
- mobile_mcp/core/smart_tools.py +0 -311
- mobile_mcp/mcp/__init__.py +0 -13
- mobile_mcp/mcp/mcp_server.py +0 -1126
- mobile_mcp/mcp/mcp_server_simple.py +0 -23
- mobile_mcp/vision/__init__.py +0 -10
- mobile_mcp/vision/vision_locator.py +0 -405
- mobile_mcp_ai-2.2.6.dist-info/METADATA +0 -503
- mobile_mcp_ai-2.2.6.dist-info/RECORD +0 -49
- mobile_mcp_ai-2.2.6.dist-info/entry_points.txt +0 -2
- {mobile_mcp_ai-2.2.6.dist-info → mobile_mcp_ai-2.5.3.dist-info}/WHEEL +0 -0
- {mobile_mcp_ai-2.2.6.dist-info → mobile_mcp_ai-2.5.3.dist-info}/licenses/LICENSE +0 -0
- {mobile_mcp_ai-2.2.6.dist-info → mobile_mcp_ai-2.5.3.dist-info}/top_level.txt +0 -0
mobile_mcp/core/h5/__init__.py
DELETED
mobile_mcp/core/h5/h5_handler.py
DELETED
|
@@ -1,548 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
# -*- coding: utf-8 -*-
|
|
3
|
-
"""
|
|
4
|
-
H5处理器 - 智能处理H5/WebView内容
|
|
5
|
-
|
|
6
|
-
策略:
|
|
7
|
-
1. 自动检测页面是否包含WebView
|
|
8
|
-
2. 优先使用UIAutomator2的text定位(适用80%场景)
|
|
9
|
-
3. 失败时自动切换到Appium context(需要安装Appium)
|
|
10
|
-
|
|
11
|
-
用法:
|
|
12
|
-
handler = H5Handler(mobile_client)
|
|
13
|
-
result = await handler.smart_click("提交按钮")
|
|
14
|
-
"""
|
|
15
|
-
import asyncio
|
|
16
|
-
from typing import Dict, Optional
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class H5Handler:
|
|
20
|
-
"""
|
|
21
|
-
H5智能处理器
|
|
22
|
-
|
|
23
|
-
自动检测并处理H5/WebView内容
|
|
24
|
-
"""
|
|
25
|
-
|
|
26
|
-
def __init__(self, mobile_client):
|
|
27
|
-
"""
|
|
28
|
-
初始化H5处理器
|
|
29
|
-
|
|
30
|
-
Args:
|
|
31
|
-
mobile_client: MobileClient实例
|
|
32
|
-
"""
|
|
33
|
-
self.mobile_client = mobile_client
|
|
34
|
-
self.u2 = mobile_client.u2
|
|
35
|
-
|
|
36
|
-
# 缓存
|
|
37
|
-
self._last_check_time = 0
|
|
38
|
-
self._has_webview_cache = None
|
|
39
|
-
self._cache_ttl = 2 # 缓存2秒
|
|
40
|
-
|
|
41
|
-
# Appium支持(延迟加载)
|
|
42
|
-
self._appium_driver = None
|
|
43
|
-
self._appium_available = None
|
|
44
|
-
|
|
45
|
-
async def is_h5_page(self, use_cache: bool = True) -> bool:
|
|
46
|
-
"""
|
|
47
|
-
检测当前页面是否包含H5/WebView
|
|
48
|
-
|
|
49
|
-
Args:
|
|
50
|
-
use_cache: 是否使用缓存
|
|
51
|
-
|
|
52
|
-
Returns:
|
|
53
|
-
True表示有H5内容
|
|
54
|
-
"""
|
|
55
|
-
import time
|
|
56
|
-
|
|
57
|
-
# 检查缓存
|
|
58
|
-
if use_cache and self._has_webview_cache is not None:
|
|
59
|
-
current_time = time.time()
|
|
60
|
-
if current_time - self._last_check_time < self._cache_ttl:
|
|
61
|
-
return self._has_webview_cache
|
|
62
|
-
|
|
63
|
-
# 获取页面XML
|
|
64
|
-
xml = self.u2.dump_hierarchy()
|
|
65
|
-
|
|
66
|
-
# 检测WebView
|
|
67
|
-
has_webview = any([
|
|
68
|
-
'android.webkit.WebView' in xml,
|
|
69
|
-
'com.tencent.smtt.webkit.WebView' in xml, # X5内核
|
|
70
|
-
'com.uc.webview' in xml, # UC浏览器内核
|
|
71
|
-
'org.xwalk.core.XWalkView' in xml, # CrossWalk
|
|
72
|
-
])
|
|
73
|
-
|
|
74
|
-
# 更新缓存
|
|
75
|
-
self._has_webview_cache = has_webview
|
|
76
|
-
self._last_check_time = time.time()
|
|
77
|
-
|
|
78
|
-
return has_webview
|
|
79
|
-
|
|
80
|
-
async def get_webview_info(self) -> Optional[Dict]:
|
|
81
|
-
"""
|
|
82
|
-
获取WebView详细信息
|
|
83
|
-
|
|
84
|
-
Returns:
|
|
85
|
-
WebView信息字典,无WebView则返回None
|
|
86
|
-
"""
|
|
87
|
-
xml = self.u2.dump_hierarchy()
|
|
88
|
-
|
|
89
|
-
if 'android.webkit.WebView' not in xml:
|
|
90
|
-
return None
|
|
91
|
-
|
|
92
|
-
# 解析WebView信息
|
|
93
|
-
from xml.etree import ElementTree as ET
|
|
94
|
-
root = ET.fromstring(xml)
|
|
95
|
-
|
|
96
|
-
webviews = root.findall(".//node[@class='android.webkit.WebView']")
|
|
97
|
-
if not webviews:
|
|
98
|
-
return None
|
|
99
|
-
|
|
100
|
-
# 返回第一个WebView的信息
|
|
101
|
-
wv = webviews[0]
|
|
102
|
-
|
|
103
|
-
info = {
|
|
104
|
-
'count': len(webviews),
|
|
105
|
-
'resource_id': wv.get('resource-id', ''),
|
|
106
|
-
'bounds': wv.get('bounds', ''),
|
|
107
|
-
'text': wv.get('text', ''),
|
|
108
|
-
'content_desc': wv.get('content-desc', ''),
|
|
109
|
-
'has_children': len(list(wv)) > 0,
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
return info
|
|
113
|
-
|
|
114
|
-
async def smart_click(self, element_desc: str, locator=None) -> Dict:
|
|
115
|
-
"""
|
|
116
|
-
智能点击(自动处理H5和原生)
|
|
117
|
-
|
|
118
|
-
策略:
|
|
119
|
-
1. 先检测是否在WebView中
|
|
120
|
-
2. 优先使用UIAutomator2的text定位(简单快速)
|
|
121
|
-
3. 失败时尝试Appium context切换(复杂H5)
|
|
122
|
-
|
|
123
|
-
Args:
|
|
124
|
-
element_desc: 元素描述
|
|
125
|
-
locator: SmartLocator实例(可选)
|
|
126
|
-
|
|
127
|
-
Returns:
|
|
128
|
-
操作结果
|
|
129
|
-
"""
|
|
130
|
-
print(f"🎯 H5智能点击: {element_desc}")
|
|
131
|
-
|
|
132
|
-
# 检测是否在H5页面
|
|
133
|
-
is_h5 = await self.is_h5_page()
|
|
134
|
-
|
|
135
|
-
if is_h5:
|
|
136
|
-
print(f" ✅ 检测到H5内容")
|
|
137
|
-
|
|
138
|
-
# 方案1: UIAutomator2 text定位(适用80%场景)
|
|
139
|
-
print(f" 📱 尝试UIAutomator2定位...")
|
|
140
|
-
|
|
141
|
-
# 如果提供了locator,使用智能定位
|
|
142
|
-
if locator:
|
|
143
|
-
result = await locator.locate(element_desc)
|
|
144
|
-
if result:
|
|
145
|
-
click_result = await self.mobile_client.click(
|
|
146
|
-
element_desc,
|
|
147
|
-
ref=result['ref'],
|
|
148
|
-
verify=False
|
|
149
|
-
)
|
|
150
|
-
if click_result.get('success'):
|
|
151
|
-
print(f" ✅ UIAutomator2定位成功")
|
|
152
|
-
return click_result
|
|
153
|
-
|
|
154
|
-
# 直接text定位
|
|
155
|
-
try:
|
|
156
|
-
if self.u2(text=element_desc).exists(timeout=1):
|
|
157
|
-
self.u2(text=element_desc).click()
|
|
158
|
-
print(f" ✅ UIAutomator2 text定位成功")
|
|
159
|
-
return {"success": True, "method": "uiautomator2_text"}
|
|
160
|
-
|
|
161
|
-
# 尝试description定位
|
|
162
|
-
if self.u2(description=element_desc).exists(timeout=1):
|
|
163
|
-
self.u2(description=element_desc).click()
|
|
164
|
-
print(f" ✅ UIAutomator2 description定位成功")
|
|
165
|
-
return {"success": True, "method": "uiautomator2_desc"}
|
|
166
|
-
|
|
167
|
-
# 尝试包含匹配
|
|
168
|
-
if self.u2(textContains=element_desc).exists(timeout=1):
|
|
169
|
-
self.u2(textContains=element_desc).click()
|
|
170
|
-
print(f" ✅ UIAutomator2 textContains定位成功")
|
|
171
|
-
return {"success": True, "method": "uiautomator2_contains"}
|
|
172
|
-
|
|
173
|
-
except Exception as e:
|
|
174
|
-
print(f" ⚠️ UIAutomator2定位失败: {e}")
|
|
175
|
-
|
|
176
|
-
# 方案2: Appium context切换(复杂H5)
|
|
177
|
-
print(f" 🔄 尝试Appium context切换...")
|
|
178
|
-
appium_result = await self._try_appium_click(element_desc)
|
|
179
|
-
if appium_result.get('success'):
|
|
180
|
-
return appium_result
|
|
181
|
-
|
|
182
|
-
# 方案3: 坐标点击(最后手段)
|
|
183
|
-
print(f" 📍 尝试坐标点击...")
|
|
184
|
-
return await self._try_coordinate_click(element_desc)
|
|
185
|
-
|
|
186
|
-
else:
|
|
187
|
-
# 原生页面,直接使用普通定位
|
|
188
|
-
print(f" 📱 原生页面,使用普通定位")
|
|
189
|
-
if locator:
|
|
190
|
-
result = await locator.locate(element_desc)
|
|
191
|
-
if result:
|
|
192
|
-
return await self.mobile_client.click(
|
|
193
|
-
element_desc,
|
|
194
|
-
ref=result['ref'],
|
|
195
|
-
verify=False
|
|
196
|
-
)
|
|
197
|
-
|
|
198
|
-
return {"success": False, "reason": "需要提供locator"}
|
|
199
|
-
|
|
200
|
-
async def smart_input(self, element_desc: str, text: str, locator=None) -> Dict:
|
|
201
|
-
"""
|
|
202
|
-
智能输入(自动处理H5和原生)
|
|
203
|
-
|
|
204
|
-
Args:
|
|
205
|
-
element_desc: 元素描述
|
|
206
|
-
text: 要输入的文本
|
|
207
|
-
locator: SmartLocator实例(可选)
|
|
208
|
-
|
|
209
|
-
Returns:
|
|
210
|
-
操作结果
|
|
211
|
-
"""
|
|
212
|
-
print(f"⌨️ H5智能输入: {element_desc} = {text}")
|
|
213
|
-
|
|
214
|
-
# 检测是否在H5页面
|
|
215
|
-
is_h5 = await self.is_h5_page()
|
|
216
|
-
|
|
217
|
-
if is_h5:
|
|
218
|
-
print(f" ✅ 检测到H5内容")
|
|
219
|
-
|
|
220
|
-
# 方案1: UIAutomator2定位
|
|
221
|
-
print(f" 📱 尝试UIAutomator2定位...")
|
|
222
|
-
|
|
223
|
-
if locator:
|
|
224
|
-
result = await locator.locate(element_desc)
|
|
225
|
-
if result:
|
|
226
|
-
# 🔥 不使用client.type_text,直接操作EditText以支持clear_text
|
|
227
|
-
ref = result['ref']
|
|
228
|
-
try:
|
|
229
|
-
if ref.startswith('com.') or ':' in ref:
|
|
230
|
-
# resource-id定位
|
|
231
|
-
edittext = self.u2(resourceId=ref)
|
|
232
|
-
elif ref.startswith('[') and '][' in ref:
|
|
233
|
-
# bounds定位 - 不需要处理,直接查找EditText
|
|
234
|
-
edittext = self.u2(className='android.widget.EditText')
|
|
235
|
-
else:
|
|
236
|
-
# text定位 - 🔥 关键:不要用text定位,直接查找EditText
|
|
237
|
-
# 因为ref可能是TextView的text,而不是EditText的text
|
|
238
|
-
edittext = self.u2(className='android.widget.EditText')
|
|
239
|
-
|
|
240
|
-
if edittext.exists(timeout=1):
|
|
241
|
-
edittext.click()
|
|
242
|
-
await asyncio.sleep(0.5)
|
|
243
|
-
edittext.clear_text() # 🔥 关键:先清空
|
|
244
|
-
await asyncio.sleep(0.3)
|
|
245
|
-
edittext.set_text(text)
|
|
246
|
-
await asyncio.sleep(0.5)
|
|
247
|
-
self.u2.press("back") # 关闭键盘
|
|
248
|
-
await asyncio.sleep(0.5)
|
|
249
|
-
|
|
250
|
-
print(f" ✅ UIAutomator2定位成功")
|
|
251
|
-
return {"success": True, "method": "uiautomator2_locator"}
|
|
252
|
-
except Exception as e:
|
|
253
|
-
print(f" ⚠️ UIAutomator2定位输入失败: {e}")
|
|
254
|
-
|
|
255
|
-
# 直接定位EditText
|
|
256
|
-
try:
|
|
257
|
-
# 查找所有EditText
|
|
258
|
-
edittexts = self.u2(className='android.widget.EditText')
|
|
259
|
-
if edittexts.exists(timeout=1):
|
|
260
|
-
# 找到第一个EditText并输入
|
|
261
|
-
edittext = self.u2(className='android.widget.EditText')
|
|
262
|
-
|
|
263
|
-
# 🎯 关键:先点击聚焦,清空,再输入
|
|
264
|
-
edittext.click()
|
|
265
|
-
await asyncio.sleep(0.5)
|
|
266
|
-
|
|
267
|
-
# 🔥 关键:先清空现有内容
|
|
268
|
-
edittext.clear_text()
|
|
269
|
-
await asyncio.sleep(0.3)
|
|
270
|
-
|
|
271
|
-
# 使用set_text输入(支持中文)
|
|
272
|
-
edittext.set_text(text)
|
|
273
|
-
await asyncio.sleep(0.5)
|
|
274
|
-
|
|
275
|
-
# 🔥 关键:输入后按back键关闭键盘(不是enter)
|
|
276
|
-
try:
|
|
277
|
-
self.u2.press("back") # 按返回键关闭键盘
|
|
278
|
-
await asyncio.sleep(0.5)
|
|
279
|
-
except:
|
|
280
|
-
pass
|
|
281
|
-
|
|
282
|
-
print(f" ✅ UIAutomator2输入成功")
|
|
283
|
-
return {"success": True, "method": "uiautomator2_edittext"}
|
|
284
|
-
except Exception as e:
|
|
285
|
-
print(f" ⚠️ UIAutomator2输入失败: {e}")
|
|
286
|
-
|
|
287
|
-
# 方案2: Appium context切换
|
|
288
|
-
print(f" 🔄 尝试Appium context切换...")
|
|
289
|
-
appium_result = await self._try_appium_input(element_desc, text)
|
|
290
|
-
if appium_result.get('success'):
|
|
291
|
-
return appium_result
|
|
292
|
-
|
|
293
|
-
return {"success": False, "reason": "所有H5输入方法都失败"}
|
|
294
|
-
|
|
295
|
-
else:
|
|
296
|
-
# 原生页面
|
|
297
|
-
print(f" 📱 原生页面,使用普通输入")
|
|
298
|
-
if locator:
|
|
299
|
-
result = await locator.locate(element_desc)
|
|
300
|
-
if result:
|
|
301
|
-
return await self.mobile_client.type_text(
|
|
302
|
-
element_desc,
|
|
303
|
-
text,
|
|
304
|
-
ref=result['ref']
|
|
305
|
-
)
|
|
306
|
-
|
|
307
|
-
return {"success": False, "reason": "需要提供locator"}
|
|
308
|
-
|
|
309
|
-
async def _try_appium_click(self, element_desc: str) -> Dict:
|
|
310
|
-
"""
|
|
311
|
-
尝试使用Appium context切换点击H5元素
|
|
312
|
-
|
|
313
|
-
Args:
|
|
314
|
-
element_desc: 元素描述
|
|
315
|
-
|
|
316
|
-
Returns:
|
|
317
|
-
操作结果
|
|
318
|
-
"""
|
|
319
|
-
# 检查Appium是否可用
|
|
320
|
-
if not await self._check_appium():
|
|
321
|
-
return {
|
|
322
|
-
"success": False,
|
|
323
|
-
"reason": "Appium未安装或未启动",
|
|
324
|
-
"suggestion": "pip install Appium-Python-Client"
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
try:
|
|
328
|
-
# 切换到WebView context
|
|
329
|
-
contexts = self._appium_driver.contexts
|
|
330
|
-
print(f" 📋 可用contexts: {contexts}")
|
|
331
|
-
|
|
332
|
-
webview_context = None
|
|
333
|
-
for ctx in contexts:
|
|
334
|
-
if 'WEBVIEW' in ctx:
|
|
335
|
-
webview_context = ctx
|
|
336
|
-
break
|
|
337
|
-
|
|
338
|
-
if not webview_context:
|
|
339
|
-
return {"success": False, "reason": "未找到WEBVIEW context"}
|
|
340
|
-
|
|
341
|
-
# 切换context
|
|
342
|
-
self._appium_driver.switch_to.context(webview_context)
|
|
343
|
-
print(f" ✅ 已切换到: {webview_context}")
|
|
344
|
-
|
|
345
|
-
# 使用Selenium定位(H5元素)
|
|
346
|
-
from selenium.webdriver.common.by import By
|
|
347
|
-
|
|
348
|
-
# 尝试多种定位方式
|
|
349
|
-
selectors = [
|
|
350
|
-
(By.XPATH, f"//*[text()='{element_desc}']"),
|
|
351
|
-
(By.XPATH, f"//button[contains(text(), '{element_desc}')]"),
|
|
352
|
-
(By.XPATH, f"//a[contains(text(), '{element_desc}')]"),
|
|
353
|
-
(By.CSS_SELECTOR, f"button:contains('{element_desc}')"),
|
|
354
|
-
]
|
|
355
|
-
|
|
356
|
-
for by, selector in selectors:
|
|
357
|
-
try:
|
|
358
|
-
element = self._appium_driver.find_element(by, selector)
|
|
359
|
-
element.click()
|
|
360
|
-
print(f" ✅ Appium点击成功: {selector}")
|
|
361
|
-
|
|
362
|
-
# 切回原生context
|
|
363
|
-
self._appium_driver.switch_to.context('NATIVE_APP')
|
|
364
|
-
|
|
365
|
-
return {"success": True, "method": "appium_webview"}
|
|
366
|
-
except:
|
|
367
|
-
continue
|
|
368
|
-
|
|
369
|
-
# 切回原生context
|
|
370
|
-
self._appium_driver.switch_to.context('NATIVE_APP')
|
|
371
|
-
|
|
372
|
-
return {"success": False, "reason": "Appium未找到元素"}
|
|
373
|
-
|
|
374
|
-
except Exception as e:
|
|
375
|
-
print(f" ⚠️ Appium操作失败: {e}")
|
|
376
|
-
return {"success": False, "reason": str(e)}
|
|
377
|
-
|
|
378
|
-
async def _try_appium_input(self, element_desc: str, text: str) -> Dict:
|
|
379
|
-
"""
|
|
380
|
-
尝试使用Appium context切换输入H5元素
|
|
381
|
-
|
|
382
|
-
Args:
|
|
383
|
-
element_desc: 元素描述
|
|
384
|
-
text: 要输入的文本
|
|
385
|
-
|
|
386
|
-
Returns:
|
|
387
|
-
操作结果
|
|
388
|
-
"""
|
|
389
|
-
if not await self._check_appium():
|
|
390
|
-
return {"success": False, "reason": "Appium未安装"}
|
|
391
|
-
|
|
392
|
-
try:
|
|
393
|
-
# 切换到WebView context
|
|
394
|
-
contexts = self._appium_driver.contexts
|
|
395
|
-
webview_context = next((c for c in contexts if 'WEBVIEW' in c), None)
|
|
396
|
-
|
|
397
|
-
if not webview_context:
|
|
398
|
-
return {"success": False, "reason": "未找到WEBVIEW context"}
|
|
399
|
-
|
|
400
|
-
self._appium_driver.switch_to.context(webview_context)
|
|
401
|
-
|
|
402
|
-
# 使用Selenium定位输入框
|
|
403
|
-
from selenium.webdriver.common.by import By
|
|
404
|
-
|
|
405
|
-
selectors = [
|
|
406
|
-
(By.NAME, element_desc),
|
|
407
|
-
(By.ID, element_desc),
|
|
408
|
-
(By.CSS_SELECTOR, f"input[placeholder*='{element_desc}']"),
|
|
409
|
-
(By.XPATH, f"//input[contains(@placeholder, '{element_desc}')]"),
|
|
410
|
-
]
|
|
411
|
-
|
|
412
|
-
for by, selector in selectors:
|
|
413
|
-
try:
|
|
414
|
-
element = self._appium_driver.find_element(by, selector)
|
|
415
|
-
element.clear()
|
|
416
|
-
element.send_keys(text)
|
|
417
|
-
print(f" ✅ Appium输入成功")
|
|
418
|
-
|
|
419
|
-
# 切回原生context
|
|
420
|
-
self._appium_driver.switch_to.context('NATIVE_APP')
|
|
421
|
-
|
|
422
|
-
return {"success": True, "method": "appium_webview"}
|
|
423
|
-
except:
|
|
424
|
-
continue
|
|
425
|
-
|
|
426
|
-
# 切回原生context
|
|
427
|
-
self._appium_driver.switch_to.context('NATIVE_APP')
|
|
428
|
-
|
|
429
|
-
return {"success": False, "reason": "Appium未找到输入框"}
|
|
430
|
-
|
|
431
|
-
except Exception as e:
|
|
432
|
-
return {"success": False, "reason": str(e)}
|
|
433
|
-
|
|
434
|
-
async def _try_coordinate_click(self, element_desc: str) -> Dict:
|
|
435
|
-
"""
|
|
436
|
-
尝试坐标点击(最后手段)
|
|
437
|
-
|
|
438
|
-
Args:
|
|
439
|
-
element_desc: 元素描述
|
|
440
|
-
|
|
441
|
-
Returns:
|
|
442
|
-
操作结果
|
|
443
|
-
"""
|
|
444
|
-
# 获取WebView信息
|
|
445
|
-
webview_info = await self.get_webview_info()
|
|
446
|
-
|
|
447
|
-
if not webview_info or not webview_info.get('bounds'):
|
|
448
|
-
return {"success": False, "reason": "无法获取WebView坐标"}
|
|
449
|
-
|
|
450
|
-
# 解析bounds
|
|
451
|
-
import re
|
|
452
|
-
bounds = webview_info['bounds']
|
|
453
|
-
match = re.search(r'\[(\d+),(\d+)\]\[(\d+),(\d+)\]', bounds)
|
|
454
|
-
|
|
455
|
-
if not match:
|
|
456
|
-
return {"success": False, "reason": "无法解析bounds"}
|
|
457
|
-
|
|
458
|
-
x1, y1, x2, y2 = map(int, match.groups())
|
|
459
|
-
|
|
460
|
-
# 如果元素描述包含"提交"、"确认"等,点击底部中心
|
|
461
|
-
bottom_keywords = ['提交', '确认', '确定', '保存', '发送', '登录', '注册']
|
|
462
|
-
if any(kw in element_desc for kw in bottom_keywords):
|
|
463
|
-
# 点击WebView底部中心(95%位置)
|
|
464
|
-
center_x = (x1 + x2) // 2
|
|
465
|
-
bottom_y = int(y1 + (y2 - y1) * 0.95)
|
|
466
|
-
|
|
467
|
-
self.u2.click(center_x, bottom_y)
|
|
468
|
-
print(f" ✅ 坐标点击成功: ({center_x}, {bottom_y})")
|
|
469
|
-
|
|
470
|
-
return {"success": True, "method": "coordinate_bottom"}
|
|
471
|
-
|
|
472
|
-
# 默认点击中心
|
|
473
|
-
center_x = (x1 + x2) // 2
|
|
474
|
-
center_y = (y1 + y2) // 2
|
|
475
|
-
|
|
476
|
-
self.u2.click(center_x, center_y)
|
|
477
|
-
print(f" ✅ 坐标点击成功: ({center_x}, {center_y})")
|
|
478
|
-
|
|
479
|
-
return {"success": True, "method": "coordinate_center"}
|
|
480
|
-
|
|
481
|
-
async def _check_appium(self) -> bool:
|
|
482
|
-
"""
|
|
483
|
-
检查Appium是否可用
|
|
484
|
-
|
|
485
|
-
Returns:
|
|
486
|
-
True表示可用
|
|
487
|
-
"""
|
|
488
|
-
# 使用缓存
|
|
489
|
-
if self._appium_available is not None:
|
|
490
|
-
return self._appium_available
|
|
491
|
-
|
|
492
|
-
try:
|
|
493
|
-
from appium import webdriver
|
|
494
|
-
from appium.options.android import UiAutomator2Options
|
|
495
|
-
|
|
496
|
-
# 如果还没有driver,尝试创建
|
|
497
|
-
if not self._appium_driver:
|
|
498
|
-
# 获取当前设备信息
|
|
499
|
-
device_info = self.mobile_client.device_manager.check_device_status()
|
|
500
|
-
|
|
501
|
-
if not device_info.get('connected'):
|
|
502
|
-
self._appium_available = False
|
|
503
|
-
return False
|
|
504
|
-
|
|
505
|
-
# 配置Appium
|
|
506
|
-
options = UiAutomator2Options()
|
|
507
|
-
options.platform_name = 'Android'
|
|
508
|
-
options.device_name = device_info.get('device_id', 'Android')
|
|
509
|
-
options.automation_name = 'UiAutomator2'
|
|
510
|
-
|
|
511
|
-
# 获取当前App包名
|
|
512
|
-
current_package = await self.mobile_client.get_current_package()
|
|
513
|
-
if current_package:
|
|
514
|
-
options.app_package = current_package
|
|
515
|
-
options.app_activity = '.' # 使用当前Activity
|
|
516
|
-
|
|
517
|
-
# 连接Appium Server
|
|
518
|
-
self._appium_driver = webdriver.Remote(
|
|
519
|
-
'http://localhost:4723',
|
|
520
|
-
options=options
|
|
521
|
-
)
|
|
522
|
-
|
|
523
|
-
print(f" ✅ Appium连接成功")
|
|
524
|
-
|
|
525
|
-
self._appium_available = True
|
|
526
|
-
return True
|
|
527
|
-
|
|
528
|
-
except ImportError:
|
|
529
|
-
print(f" ⚠️ Appium未安装: pip install Appium-Python-Client")
|
|
530
|
-
self._appium_available = False
|
|
531
|
-
return False
|
|
532
|
-
except Exception as e:
|
|
533
|
-
print(f" ⚠️ Appium连接失败: {e}")
|
|
534
|
-
print(f" 💡 提示: 请确保Appium Server已启动(appium)")
|
|
535
|
-
self._appium_available = False
|
|
536
|
-
return False
|
|
537
|
-
|
|
538
|
-
def close_appium(self):
|
|
539
|
-
"""关闭Appium连接"""
|
|
540
|
-
if self._appium_driver:
|
|
541
|
-
try:
|
|
542
|
-
self._appium_driver.quit()
|
|
543
|
-
print(f"✅ Appium连接已关闭")
|
|
544
|
-
except:
|
|
545
|
-
pass
|
|
546
|
-
self._appium_driver = None
|
|
547
|
-
self._appium_available = None
|
|
548
|
-
|