mobile-mcp-ai 2.6.6__tar.gz → 2.6.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.6.6/mobile_mcp_ai.egg-info → mobile_mcp_ai-2.6.8}/PKG-INFO +1 -1
- {mobile_mcp_ai-2.6.6 → mobile_mcp_ai-2.6.8}/core/basic_tools_lite.py +294 -6
- {mobile_mcp_ai-2.6.6 → mobile_mcp_ai-2.6.8}/mcp_tools/mcp_server.py +57 -3
- {mobile_mcp_ai-2.6.6 → mobile_mcp_ai-2.6.8/mobile_mcp_ai.egg-info}/PKG-INFO +1 -1
- {mobile_mcp_ai-2.6.6 → mobile_mcp_ai-2.6.8}/mobile_mcp_ai.egg-info/SOURCES.txt +21 -1
- {mobile_mcp_ai-2.6.6 → mobile_mcp_ai-2.6.8}/setup.py +1 -1
- {mobile_mcp_ai-2.6.6 → mobile_mcp_ai-2.6.8}/LICENSE +0 -0
- {mobile_mcp_ai-2.6.6 → mobile_mcp_ai-2.6.8}/MANIFEST.in +0 -0
- {mobile_mcp_ai-2.6.6 → mobile_mcp_ai-2.6.8}/README.md +0 -0
- {mobile_mcp_ai-2.6.6 → mobile_mcp_ai-2.6.8}/__init__.py +0 -0
- {mobile_mcp_ai-2.6.6 → mobile_mcp_ai-2.6.8}/config.py +0 -0
- {mobile_mcp_ai-2.6.6 → mobile_mcp_ai-2.6.8}/core/__init__.py +0 -0
- {mobile_mcp_ai-2.6.6 → mobile_mcp_ai-2.6.8}/core/device_manager.py +0 -0
- {mobile_mcp_ai-2.6.6 → mobile_mcp_ai-2.6.8}/core/dynamic_config.py +0 -0
- {mobile_mcp_ai-2.6.6 → mobile_mcp_ai-2.6.8}/core/ios_client_wda.py +0 -0
- {mobile_mcp_ai-2.6.6 → mobile_mcp_ai-2.6.8}/core/ios_device_manager_wda.py +0 -0
- {mobile_mcp_ai-2.6.6 → mobile_mcp_ai-2.6.8}/core/mobile_client.py +0 -0
- {mobile_mcp_ai-2.6.6 → mobile_mcp_ai-2.6.8}/core/template_matcher.py +0 -0
- {mobile_mcp_ai-2.6.6 → mobile_mcp_ai-2.6.8}/core/templates/close_buttons/auto_x_0112_151217.png +0 -0
- {mobile_mcp_ai-2.6.6 → mobile_mcp_ai-2.6.8}/core/templates/close_buttons/auto_x_0112_152037.png +0 -0
- {mobile_mcp_ai-2.6.6 → mobile_mcp_ai-2.6.8}/core/templates/close_buttons/auto_x_0112_152840.png +0 -0
- {mobile_mcp_ai-2.6.6 → mobile_mcp_ai-2.6.8}/core/templates/close_buttons/auto_x_0112_153256.png +0 -0
- {mobile_mcp_ai-2.6.6 → mobile_mcp_ai-2.6.8}/core/templates/close_buttons/auto_x_0112_154847.png +0 -0
- {mobile_mcp_ai-2.6.6 → mobile_mcp_ai-2.6.8}/core/templates/close_buttons/gray_x_stock_ad.png +0 -0
- {mobile_mcp_ai-2.6.6 → mobile_mcp_ai-2.6.8}/core/utils/__init__.py +0 -0
- {mobile_mcp_ai-2.6.6 → mobile_mcp_ai-2.6.8}/core/utils/logger.py +0 -0
- {mobile_mcp_ai-2.6.6 → mobile_mcp_ai-2.6.8}/core/utils/operation_history_manager.py +0 -0
- {mobile_mcp_ai-2.6.6 → mobile_mcp_ai-2.6.8}/core/utils/smart_wait.py +0 -0
- {mobile_mcp_ai-2.6.6 → mobile_mcp_ai-2.6.8}/docs/iOS_SETUP_GUIDE.md +0 -0
- {mobile_mcp_ai-2.6.6 → mobile_mcp_ai-2.6.8}/mcp_tools/__init__.py +0 -0
- {mobile_mcp_ai-2.6.6 → mobile_mcp_ai-2.6.8}/mobile_mcp_ai.egg-info/dependency_links.txt +0 -0
- {mobile_mcp_ai-2.6.6 → mobile_mcp_ai-2.6.8}/mobile_mcp_ai.egg-info/entry_points.txt +0 -0
- {mobile_mcp_ai-2.6.6 → mobile_mcp_ai-2.6.8}/mobile_mcp_ai.egg-info/not-zip-safe +0 -0
- {mobile_mcp_ai-2.6.6 → mobile_mcp_ai-2.6.8}/mobile_mcp_ai.egg-info/requires.txt +0 -0
- {mobile_mcp_ai-2.6.6 → mobile_mcp_ai-2.6.8}/mobile_mcp_ai.egg-info/top_level.txt +0 -0
- {mobile_mcp_ai-2.6.6 → mobile_mcp_ai-2.6.8}/requirements.txt +0 -0
- {mobile_mcp_ai-2.6.6 → mobile_mcp_ai-2.6.8}/setup.cfg +0 -0
- {mobile_mcp_ai-2.6.6 → mobile_mcp_ai-2.6.8}/templates/close_buttons/auto_x_0112_151217.png +0 -0
- {mobile_mcp_ai-2.6.6 → mobile_mcp_ai-2.6.8}/templates/close_buttons/auto_x_0112_152037.png +0 -0
- {mobile_mcp_ai-2.6.6 → mobile_mcp_ai-2.6.8}/templates/close_buttons/auto_x_0112_152840.png +0 -0
- {mobile_mcp_ai-2.6.6 → mobile_mcp_ai-2.6.8}/templates/close_buttons/auto_x_0112_153256.png +0 -0
- {mobile_mcp_ai-2.6.6 → mobile_mcp_ai-2.6.8}/templates/close_buttons/auto_x_0112_154847.png +0 -0
- {mobile_mcp_ai-2.6.6 → mobile_mcp_ai-2.6.8}/templates/close_buttons/gray_x_stock_ad.png +0 -0
- {mobile_mcp_ai-2.6.6 → mobile_mcp_ai-2.6.8}/utils/__init__.py +0 -0
- {mobile_mcp_ai-2.6.6 → mobile_mcp_ai-2.6.8}/utils/logger.py +0 -0
- {mobile_mcp_ai-2.6.6 → mobile_mcp_ai-2.6.8}/utils/xml_formatter.py +0 -0
- {mobile_mcp_ai-2.6.6 → mobile_mcp_ai-2.6.8}/utils/xml_parser.py +0 -0
|
@@ -2271,6 +2271,170 @@ class BasicMobileToolsLite:
|
|
|
2271
2271
|
time.sleep(seconds)
|
|
2272
2272
|
return {"success": True, "message": f"✅ 已等待 {seconds} 秒"}
|
|
2273
2273
|
|
|
2274
|
+
async def drag_progress_bar(self, direction: str = "right", distance_percent: float = 30.0,
|
|
2275
|
+
y_percent: Optional[float] = None, y: Optional[int] = None) -> Dict:
|
|
2276
|
+
"""智能拖动进度条
|
|
2277
|
+
|
|
2278
|
+
自动检测进度条是否可见:
|
|
2279
|
+
- 如果进度条已显示,直接拖动(无需先点击播放区域)
|
|
2280
|
+
- 如果进度条未显示,先点击播放区域显示控制栏,再拖动
|
|
2281
|
+
|
|
2282
|
+
Args:
|
|
2283
|
+
direction: 拖动方向,'left'(倒退)或 'right'(前进),默认 'right'
|
|
2284
|
+
distance_percent: 拖动距离百分比 (0-100),默认 30%
|
|
2285
|
+
y_percent: 进度条的垂直位置百分比 (0-100),如果未指定则自动检测
|
|
2286
|
+
y: 进度条的垂直位置坐标(像素),如果未指定则自动检测
|
|
2287
|
+
"""
|
|
2288
|
+
try:
|
|
2289
|
+
import xml.etree.ElementTree as ET
|
|
2290
|
+
import re
|
|
2291
|
+
|
|
2292
|
+
if self._is_ios():
|
|
2293
|
+
return {"success": False, "message": "❌ iOS 暂不支持,请使用 mobile_swipe"}
|
|
2294
|
+
|
|
2295
|
+
if direction not in ['left', 'right']:
|
|
2296
|
+
return {"success": False, "message": f"❌ 拖动方向必须是 'left' 或 'right': {direction}"}
|
|
2297
|
+
|
|
2298
|
+
screen_width, screen_height = self.client.u2.window_size()
|
|
2299
|
+
|
|
2300
|
+
# 获取 XML 查找进度条
|
|
2301
|
+
xml_string = self.client.u2.dump_hierarchy(compressed=False)
|
|
2302
|
+
root = ET.fromstring(xml_string)
|
|
2303
|
+
|
|
2304
|
+
progress_bar_found = False
|
|
2305
|
+
progress_bar_y = None
|
|
2306
|
+
progress_bar_y_percent = None
|
|
2307
|
+
|
|
2308
|
+
# 查找进度条元素(SeekBar、ProgressBar)
|
|
2309
|
+
for elem in root.iter():
|
|
2310
|
+
class_name = elem.attrib.get('class', '')
|
|
2311
|
+
resource_id = elem.attrib.get('resource-id', '')
|
|
2312
|
+
bounds_str = elem.attrib.get('bounds', '')
|
|
2313
|
+
|
|
2314
|
+
# 检查是否是进度条
|
|
2315
|
+
is_progress_bar = (
|
|
2316
|
+
'SeekBar' in class_name or
|
|
2317
|
+
'ProgressBar' in class_name or
|
|
2318
|
+
'progress' in resource_id.lower() or
|
|
2319
|
+
'seek' in resource_id.lower()
|
|
2320
|
+
)
|
|
2321
|
+
|
|
2322
|
+
if is_progress_bar and bounds_str:
|
|
2323
|
+
# 解析 bounds 获取进度条位置
|
|
2324
|
+
match = re.match(r'\[(\d+),(\d+)\]\[(\d+),(\d+)\]', bounds_str)
|
|
2325
|
+
if match:
|
|
2326
|
+
x1, y1, x2, y2 = map(int, match.groups())
|
|
2327
|
+
center_y = (y1 + y2) // 2
|
|
2328
|
+
progress_bar_y = center_y
|
|
2329
|
+
progress_bar_y_percent = round(center_y / screen_height * 100, 1)
|
|
2330
|
+
progress_bar_found = True
|
|
2331
|
+
break
|
|
2332
|
+
|
|
2333
|
+
# 如果未找到进度条,尝试点击播放区域显示控制栏
|
|
2334
|
+
if not progress_bar_found:
|
|
2335
|
+
# 点击屏幕中心显示控制栏
|
|
2336
|
+
center_x, center_y = screen_width // 2, screen_height // 2
|
|
2337
|
+
self.client.u2.click(center_x, center_y)
|
|
2338
|
+
time.sleep(0.5)
|
|
2339
|
+
|
|
2340
|
+
# 再次查找进度条
|
|
2341
|
+
xml_string = self.client.u2.dump_hierarchy(compressed=False)
|
|
2342
|
+
root = ET.fromstring(xml_string)
|
|
2343
|
+
|
|
2344
|
+
for elem in root.iter():
|
|
2345
|
+
class_name = elem.attrib.get('class', '')
|
|
2346
|
+
resource_id = elem.attrib.get('resource-id', '')
|
|
2347
|
+
bounds_str = elem.attrib.get('bounds', '')
|
|
2348
|
+
|
|
2349
|
+
is_progress_bar = (
|
|
2350
|
+
'SeekBar' in class_name or
|
|
2351
|
+
'ProgressBar' in class_name or
|
|
2352
|
+
'progress' in resource_id.lower() or
|
|
2353
|
+
'seek' in resource_id.lower()
|
|
2354
|
+
)
|
|
2355
|
+
|
|
2356
|
+
if is_progress_bar and bounds_str:
|
|
2357
|
+
match = re.match(r'\[(\d+),(\d+)\]\[(\d+),(\d+)\]', bounds_str)
|
|
2358
|
+
if match:
|
|
2359
|
+
x1, y1, x2, y2 = map(int, match.groups())
|
|
2360
|
+
center_y = (y1 + y2) // 2
|
|
2361
|
+
progress_bar_y = center_y
|
|
2362
|
+
progress_bar_y_percent = round(center_y / screen_height * 100, 1)
|
|
2363
|
+
progress_bar_found = True
|
|
2364
|
+
break
|
|
2365
|
+
|
|
2366
|
+
# 确定使用的高度位置
|
|
2367
|
+
if y_percent is not None:
|
|
2368
|
+
swipe_y = int(screen_height * y_percent / 100)
|
|
2369
|
+
used_y_percent = y_percent
|
|
2370
|
+
elif y is not None:
|
|
2371
|
+
swipe_y = y
|
|
2372
|
+
used_y_percent = round(y / screen_height * 100, 1)
|
|
2373
|
+
elif progress_bar_found:
|
|
2374
|
+
swipe_y = progress_bar_y
|
|
2375
|
+
used_y_percent = progress_bar_y_percent
|
|
2376
|
+
else:
|
|
2377
|
+
# 默认使用屏幕底部附近(进度条常见位置)
|
|
2378
|
+
swipe_y = int(screen_height * 0.91)
|
|
2379
|
+
used_y_percent = 91.0
|
|
2380
|
+
|
|
2381
|
+
# 计算滑动距离
|
|
2382
|
+
swipe_distance = int(screen_width * distance_percent / 100)
|
|
2383
|
+
|
|
2384
|
+
# 计算起始和结束位置
|
|
2385
|
+
center_x = screen_width // 2
|
|
2386
|
+
if direction == 'left':
|
|
2387
|
+
start_x = min(center_x + swipe_distance // 2, screen_width - 10)
|
|
2388
|
+
end_x = start_x - swipe_distance
|
|
2389
|
+
if end_x < 10:
|
|
2390
|
+
end_x = 10
|
|
2391
|
+
start_x = min(end_x + swipe_distance, screen_width - 10)
|
|
2392
|
+
else: # right
|
|
2393
|
+
start_x = max(center_x - swipe_distance // 2, 10)
|
|
2394
|
+
end_x = start_x + swipe_distance
|
|
2395
|
+
if end_x > screen_width - 10:
|
|
2396
|
+
end_x = screen_width - 10
|
|
2397
|
+
start_x = max(end_x - swipe_distance, 10)
|
|
2398
|
+
|
|
2399
|
+
# 执行拖动
|
|
2400
|
+
self.client.u2.swipe(start_x, swipe_y, end_x, swipe_y, duration=0.5)
|
|
2401
|
+
time.sleep(0.3)
|
|
2402
|
+
|
|
2403
|
+
# 记录操作
|
|
2404
|
+
self._record_swipe(direction)
|
|
2405
|
+
|
|
2406
|
+
# 检查应用是否跳转
|
|
2407
|
+
app_check = self._check_app_switched()
|
|
2408
|
+
return_result = None
|
|
2409
|
+
if app_check['switched']:
|
|
2410
|
+
return_result = self._return_to_target_app()
|
|
2411
|
+
|
|
2412
|
+
# 构建返回消息
|
|
2413
|
+
msg = f"✅ 进度条拖动成功: {direction} (高度: {used_y_percent}%, 距离: {distance_percent}%)"
|
|
2414
|
+
if not progress_bar_found:
|
|
2415
|
+
msg += "\n💡 已自动点击播放区域显示控制栏"
|
|
2416
|
+
else:
|
|
2417
|
+
msg += "\n💡 进度条已显示,直接拖动"
|
|
2418
|
+
|
|
2419
|
+
if app_check['switched']:
|
|
2420
|
+
msg += f"\n{app_check['message']}"
|
|
2421
|
+
if return_result and return_result.get('success'):
|
|
2422
|
+
msg += f"\n{return_result['message']}"
|
|
2423
|
+
|
|
2424
|
+
return {
|
|
2425
|
+
"success": True,
|
|
2426
|
+
"message": msg,
|
|
2427
|
+
"progress_bar_found": progress_bar_found,
|
|
2428
|
+
"y_percent": used_y_percent,
|
|
2429
|
+
"distance_percent": distance_percent,
|
|
2430
|
+
"direction": direction,
|
|
2431
|
+
"app_check": app_check,
|
|
2432
|
+
"return_to_app": return_result
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
except Exception as e:
|
|
2436
|
+
return {"success": False, "message": f"❌ 拖动进度条失败: {e}"}
|
|
2437
|
+
|
|
2274
2438
|
# ==================== 应用管理 ====================
|
|
2275
2439
|
|
|
2276
2440
|
async def launch_app(self, package_name: str) -> Dict:
|
|
@@ -2731,6 +2895,9 @@ class BasicMobileToolsLite:
|
|
|
2731
2895
|
# 如果置信度不够高,记录但继续尝试查找关闭按钮
|
|
2732
2896
|
popup_detected = popup_bounds is not None and popup_confidence >= 0.6
|
|
2733
2897
|
|
|
2898
|
+
# 【重要修复】如果没有检测到弹窗区域,只搜索有明确关闭特征的元素(文本、resource-id等)
|
|
2899
|
+
# 避免误点击普通页面的右上角图标
|
|
2900
|
+
|
|
2734
2901
|
# ===== 第二步:在弹窗范围内查找关闭按钮 =====
|
|
2735
2902
|
for idx, elem in enumerate(all_elements):
|
|
2736
2903
|
text = elem.attrib.get('text', '')
|
|
@@ -2738,6 +2905,7 @@ class BasicMobileToolsLite:
|
|
|
2738
2905
|
bounds_str = elem.attrib.get('bounds', '')
|
|
2739
2906
|
class_name = elem.attrib.get('class', '')
|
|
2740
2907
|
clickable = elem.attrib.get('clickable', 'false') == 'true'
|
|
2908
|
+
resource_id = elem.attrib.get('resource-id', '')
|
|
2741
2909
|
|
|
2742
2910
|
if not bounds_str:
|
|
2743
2911
|
continue
|
|
@@ -2754,7 +2922,7 @@ class BasicMobileToolsLite:
|
|
|
2754
2922
|
center_y = (y1 + y2) // 2
|
|
2755
2923
|
|
|
2756
2924
|
# 如果检测到弹窗区域,检查元素是否在弹窗范围内或附近
|
|
2757
|
-
in_popup =
|
|
2925
|
+
in_popup = False
|
|
2758
2926
|
popup_edge_bonus = 0
|
|
2759
2927
|
is_floating_close = False # 是否是浮动关闭按钮(在弹窗外部上方)
|
|
2760
2928
|
if popup_bounds:
|
|
@@ -2795,6 +2963,20 @@ class BasicMobileToolsLite:
|
|
|
2795
2963
|
# 浮动关闭按钮(在弹窗上方外侧)给予高额加分
|
|
2796
2964
|
if is_floating_close:
|
|
2797
2965
|
popup_edge_bonus += 5.0 # 大幅加分
|
|
2966
|
+
elif not popup_detected:
|
|
2967
|
+
# 没有检测到弹窗时,只处理有明确关闭特征的元素
|
|
2968
|
+
# 检查是否有明确的关闭特征(文本、resource-id、content-desc)
|
|
2969
|
+
has_explicit_close_feature = (
|
|
2970
|
+
text in close_texts or
|
|
2971
|
+
any(kw in content_desc.lower() for kw in close_desc_keywords) or
|
|
2972
|
+
'close' in resource_id.lower() or
|
|
2973
|
+
'dismiss' in resource_id.lower() or
|
|
2974
|
+
'cancel' in resource_id.lower()
|
|
2975
|
+
)
|
|
2976
|
+
if not has_explicit_close_feature:
|
|
2977
|
+
continue # 没有明确关闭特征,跳过
|
|
2978
|
+
# 有明确关闭特征时,允许处理
|
|
2979
|
+
in_popup = True
|
|
2798
2980
|
|
|
2799
2981
|
if not in_popup:
|
|
2800
2982
|
continue
|
|
@@ -3102,6 +3284,15 @@ class BasicMobileToolsLite:
|
|
|
3102
3284
|
resource_id = elem.attrib.get('resource-id', '')
|
|
3103
3285
|
clickable = elem.attrib.get('clickable', 'false') == 'true'
|
|
3104
3286
|
|
|
3287
|
+
# 检查是否是关闭按钮
|
|
3288
|
+
is_close_button = (
|
|
3289
|
+
'close' in resource_id.lower() or
|
|
3290
|
+
'dismiss' in resource_id.lower() or
|
|
3291
|
+
'cancel' in resource_id.lower() or
|
|
3292
|
+
'×' in elem.attrib.get('text', '') or
|
|
3293
|
+
'X' in elem.attrib.get('text', '')
|
|
3294
|
+
)
|
|
3295
|
+
|
|
3105
3296
|
all_elements.append({
|
|
3106
3297
|
'idx': idx,
|
|
3107
3298
|
'bounds': (x1, y1, x2, y2),
|
|
@@ -3114,6 +3305,7 @@ class BasicMobileToolsLite:
|
|
|
3114
3305
|
'clickable': clickable,
|
|
3115
3306
|
'center_x': (x1 + x2) // 2,
|
|
3116
3307
|
'center_y': (y1 + y2) // 2,
|
|
3308
|
+
'is_close_button': is_close_button,
|
|
3117
3309
|
})
|
|
3118
3310
|
|
|
3119
3311
|
if not all_elements:
|
|
@@ -3122,6 +3314,8 @@ class BasicMobileToolsLite:
|
|
|
3122
3314
|
# 弹窗检测关键词
|
|
3123
3315
|
dialog_class_keywords = ['Dialog', 'Popup', 'Alert', 'Modal', 'BottomSheet', 'PopupWindow']
|
|
3124
3316
|
dialog_id_keywords = ['dialog', 'popup', 'alert', 'modal', 'bottom_sheet', 'overlay', 'mask']
|
|
3317
|
+
# 广告弹窗关键词(全屏广告、激励视频等)
|
|
3318
|
+
ad_popup_keywords = ['ad_close', 'ad_button', 'full_screen', 'interstitial', 'reward', 'close_icon', 'close_btn']
|
|
3125
3319
|
|
|
3126
3320
|
popup_candidates = []
|
|
3127
3321
|
has_mask_layer = False
|
|
@@ -3152,6 +3346,59 @@ class BasicMobileToolsLite:
|
|
|
3152
3346
|
if y1 < 50:
|
|
3153
3347
|
continue
|
|
3154
3348
|
|
|
3349
|
+
# 【非弹窗特征】如果元素包含底部导航栏(底部tab),则不是弹窗
|
|
3350
|
+
# 底部导航栏通常在屏幕底部,高度约100-200像素
|
|
3351
|
+
if y2 > screen_height * 0.85:
|
|
3352
|
+
# 检查是否包含tab相关的resource-id或class
|
|
3353
|
+
if 'tab' in resource_id.lower() or 'Tab' in class_name or 'navigation' in resource_id.lower():
|
|
3354
|
+
continue # 跳过底部导航栏
|
|
3355
|
+
|
|
3356
|
+
# 【非弹窗特征】如果元素包含顶部搜索栏,则不是弹窗
|
|
3357
|
+
if y1 < screen_height * 0.15:
|
|
3358
|
+
if 'search' in resource_id.lower() or 'Search' in class_name:
|
|
3359
|
+
continue # 跳过顶部搜索栏
|
|
3360
|
+
|
|
3361
|
+
# 先检查是否有强弹窗特征(用于后续判断)
|
|
3362
|
+
has_strong_popup_feature = (
|
|
3363
|
+
any(kw in class_name for kw in dialog_class_keywords) or
|
|
3364
|
+
any(kw in resource_id.lower() for kw in dialog_id_keywords) or
|
|
3365
|
+
any(kw in resource_id.lower() for kw in ad_popup_keywords) # 广告弹窗关键词
|
|
3366
|
+
)
|
|
3367
|
+
|
|
3368
|
+
# 检查是否有子元素是关闭按钮(作为弹窗特征)
|
|
3369
|
+
has_close_button_child = False
|
|
3370
|
+
elem_bounds = elem['bounds']
|
|
3371
|
+
for other_elem in all_elements:
|
|
3372
|
+
if other_elem['idx'] == elem['idx']:
|
|
3373
|
+
continue
|
|
3374
|
+
if other_elem['is_close_button']:
|
|
3375
|
+
# 检查关闭按钮是否在这个元素范围内
|
|
3376
|
+
ox1, oy1, ox2, oy2 = other_elem['bounds']
|
|
3377
|
+
ex1, ey1, ex2, ey2 = elem_bounds
|
|
3378
|
+
if ex1 <= ox1 and ey1 <= oy1 and ex2 >= ox2 and ey2 >= oy2:
|
|
3379
|
+
has_close_button_child = True
|
|
3380
|
+
break
|
|
3381
|
+
|
|
3382
|
+
# 【非弹窗特征】如果元素包含明显的页面内容特征,则不是弹窗
|
|
3383
|
+
# 检查是否包含视频播放器、内容列表等页面元素
|
|
3384
|
+
page_content_keywords = ['video', 'player', 'recycler', 'list', 'scroll', 'viewpager', 'fragment']
|
|
3385
|
+
if any(kw in resource_id.lower() or kw in class_name.lower() for kw in page_content_keywords):
|
|
3386
|
+
# 如果面积很大且没有强弹窗特征,则不是弹窗
|
|
3387
|
+
if area_ratio > 0.6 and not has_strong_popup_feature:
|
|
3388
|
+
continue
|
|
3389
|
+
|
|
3390
|
+
# 【非弹窗特征】如果元素面积过大(接近全屏),即使居中也不应该是弹窗
|
|
3391
|
+
# 真正的弹窗通常不会超过屏幕的60%
|
|
3392
|
+
# 对于面积 > 0.6 的元素,如果没有强特征,直接跳过(避免误判首页内容区域)
|
|
3393
|
+
if area_ratio > 0.6 and not has_strong_popup_feature:
|
|
3394
|
+
continue # 跳过大面积非弹窗元素(接近全屏的内容区域,如首页视频播放区域)
|
|
3395
|
+
|
|
3396
|
+
# 对于面积 > 0.7 的元素,即使有强特征也要更严格
|
|
3397
|
+
if area_ratio > 0.7:
|
|
3398
|
+
# 需要非常强的特征才认为是弹窗
|
|
3399
|
+
if not has_strong_popup_feature:
|
|
3400
|
+
continue
|
|
3401
|
+
|
|
3155
3402
|
confidence = 0.0
|
|
3156
3403
|
|
|
3157
3404
|
# 【强特征】class 名称包含弹窗关键词 (+0.5)
|
|
@@ -3162,19 +3409,46 @@ class BasicMobileToolsLite:
|
|
|
3162
3409
|
if any(kw in resource_id.lower() for kw in dialog_id_keywords):
|
|
3163
3410
|
confidence += 0.4
|
|
3164
3411
|
|
|
3412
|
+
# 【强特征】resource-id 包含广告弹窗关键词 (+0.4)
|
|
3413
|
+
if any(kw in resource_id.lower() for kw in ad_popup_keywords):
|
|
3414
|
+
confidence += 0.4
|
|
3415
|
+
|
|
3416
|
+
# 【强特征】包含关闭按钮作为子元素 (+0.3)
|
|
3417
|
+
if has_close_button_child:
|
|
3418
|
+
confidence += 0.3
|
|
3419
|
+
|
|
3165
3420
|
# 【中等特征】居中显示 (+0.2)
|
|
3421
|
+
# 但如果没有强特征,降低权重
|
|
3166
3422
|
center_x = elem['center_x']
|
|
3167
3423
|
center_y = elem['center_y']
|
|
3168
3424
|
is_centered_x = abs(center_x - screen_width / 2) < screen_width * 0.15
|
|
3169
3425
|
is_centered_y = abs(center_y - screen_height / 2) < screen_height * 0.25
|
|
3426
|
+
|
|
3427
|
+
has_strong_feature = (
|
|
3428
|
+
any(kw in class_name for kw in dialog_class_keywords) or
|
|
3429
|
+
any(kw in resource_id.lower() for kw in dialog_id_keywords) or
|
|
3430
|
+
any(kw in resource_id.lower() for kw in ad_popup_keywords) or
|
|
3431
|
+
has_close_button_child
|
|
3432
|
+
)
|
|
3433
|
+
|
|
3170
3434
|
if is_centered_x and is_centered_y:
|
|
3171
|
-
|
|
3435
|
+
if has_strong_feature:
|
|
3436
|
+
confidence += 0.2
|
|
3437
|
+
else:
|
|
3438
|
+
confidence += 0.1 # 没有强特征时降低权重
|
|
3172
3439
|
elif is_centered_x:
|
|
3173
|
-
|
|
3440
|
+
if has_strong_feature:
|
|
3441
|
+
confidence += 0.1
|
|
3442
|
+
else:
|
|
3443
|
+
confidence += 0.05 # 没有强特征时降低权重
|
|
3174
3444
|
|
|
3175
3445
|
# 【中等特征】非全屏但有一定大小 (+0.15)
|
|
3446
|
+
# 但如果没有强特征,降低权重
|
|
3176
3447
|
if 0.15 < area_ratio < 0.75:
|
|
3177
|
-
|
|
3448
|
+
if has_strong_feature:
|
|
3449
|
+
confidence += 0.15
|
|
3450
|
+
else:
|
|
3451
|
+
confidence += 0.08 # 没有强特征时降低权重
|
|
3178
3452
|
|
|
3179
3453
|
# 【弱特征】XML 顺序靠后(在视图层级上层)(+0.1)
|
|
3180
3454
|
if elem['idx'] > len(all_elements) * 0.5:
|
|
@@ -3201,8 +3475,22 @@ class BasicMobileToolsLite:
|
|
|
3201
3475
|
popup_candidates.sort(key=lambda x: (x['confidence'], x['idx']), reverse=True)
|
|
3202
3476
|
best = popup_candidates[0]
|
|
3203
3477
|
|
|
3204
|
-
#
|
|
3205
|
-
|
|
3478
|
+
# 更严格的阈值:只有置信度 >= 0.7 才返回弹窗
|
|
3479
|
+
# 如果没有强特征(class或resource-id包含弹窗关键词),需要更高的置信度
|
|
3480
|
+
has_strong_feature = (
|
|
3481
|
+
any(kw in best['class'] for kw in dialog_class_keywords) or
|
|
3482
|
+
any(kw in best['resource_id'].lower() for kw in dialog_id_keywords) or
|
|
3483
|
+
any(kw in best['resource_id'].lower() for kw in ad_popup_keywords)
|
|
3484
|
+
)
|
|
3485
|
+
|
|
3486
|
+
if has_strong_feature:
|
|
3487
|
+
# 有强特征时,阈值0.7
|
|
3488
|
+
threshold = 0.7
|
|
3489
|
+
else:
|
|
3490
|
+
# 没有强特征时,阈值0.85(更严格)
|
|
3491
|
+
threshold = 0.85
|
|
3492
|
+
|
|
3493
|
+
if best['confidence'] >= threshold:
|
|
3206
3494
|
return best['bounds'], best['confidence']
|
|
3207
3495
|
|
|
3208
3496
|
return None, best['confidence']
|
|
@@ -255,7 +255,7 @@ class MobileMCPServer:
|
|
|
255
255
|
"- 自动检测弹窗,标注可能的关闭按钮位置\n"
|
|
256
256
|
"- 适用于所有页面和所有操作\n\n"
|
|
257
257
|
"⚡ 推荐流程:\n"
|
|
258
|
-
"1.
|
|
258
|
+
"1. 找不到目标元素时,优先调用此工具\n"
|
|
259
259
|
"2. 看标注图,找到目标元素编号\n"
|
|
260
260
|
"3. 调用 mobile_click_by_som(编号) 精准点击\n"
|
|
261
261
|
"4. 🔴【必须】点击后再次截图确认操作是否成功!",
|
|
@@ -514,14 +514,24 @@ class MobileMCPServer:
|
|
|
514
514
|
tools.append(Tool(
|
|
515
515
|
name="mobile_swipe",
|
|
516
516
|
description="👆 滑动屏幕。方向:up/down/left/right\n\n"
|
|
517
|
+
"🎯 适用场景:\n"
|
|
518
|
+
"- 滑动页面(列表、页面切换)\n"
|
|
519
|
+
"- 拖动进度条/滑块(SeekBar、ProgressBar)\n"
|
|
520
|
+
"- 滑动选择器(Picker、Slider)\n\n"
|
|
517
521
|
"💡 左右滑动时,可指定高度坐标或百分比:\n"
|
|
518
522
|
"- y: 指定高度坐标(像素)\n"
|
|
519
523
|
"- y_percent: 指定高度百分比 (0-100)\n"
|
|
520
|
-
"- 两者都未指定时,使用屏幕中心高度\n
|
|
524
|
+
"- 两者都未指定时,使用屏幕中心高度\n"
|
|
525
|
+
"- 📌 拖动进度条时,使用进度条的 Y 位置(百分比或像素)\n\n"
|
|
521
526
|
"💡 横向滑动(left/right)时,可指定滑动距离:\n"
|
|
522
527
|
"- distance: 滑动距离(像素)\n"
|
|
523
528
|
"- distance_percent: 滑动距离百分比 (0-100)\n"
|
|
524
|
-
"- 两者都未指定时,使用默认距离(屏幕宽度的 60
|
|
529
|
+
"- 两者都未指定时,使用默认距离(屏幕宽度的 60%)\n"
|
|
530
|
+
"- 📌 拖动进度条时,distance_percent 控制拖动幅度\n\n"
|
|
531
|
+
"💡 拖动进度条示例:\n"
|
|
532
|
+
"- 倒退:direction='left', y_percent=91(进度条位置), distance_percent=30\n"
|
|
533
|
+
"- 前进:direction='right', y_percent=91, distance_percent=30\n\n"
|
|
534
|
+
"⚠️ **推荐使用 mobile_drag_progress_bar 拖动进度条**(自动检测进度条位置,无需手动指定)",
|
|
525
535
|
inputSchema={
|
|
526
536
|
"type": "object",
|
|
527
537
|
"properties": {
|
|
@@ -551,6 +561,50 @@ class MobileMCPServer:
|
|
|
551
561
|
}
|
|
552
562
|
))
|
|
553
563
|
|
|
564
|
+
tools.append(Tool(
|
|
565
|
+
name="mobile_drag_progress_bar",
|
|
566
|
+
description="🎯 智能拖动进度条(⭐⭐ 推荐用于拖动视频/音频进度条)\n\n"
|
|
567
|
+
"✅ **自动检测进度条是否可见**:\n"
|
|
568
|
+
"- 如果进度条已显示,直接拖动(无需先点击播放区域)\n"
|
|
569
|
+
"- 如果进度条未显示,自动点击播放区域显示控制栏,再拖动\n\n"
|
|
570
|
+
"🎯 优势:\n"
|
|
571
|
+
"- 自动检测进度条位置,无需手动指定 y_percent\n"
|
|
572
|
+
"- 智能判断是否需要显示控制栏\n"
|
|
573
|
+
"- 使用 swipe 拖动,更稳定可靠\n\n"
|
|
574
|
+
"💡 参数说明:\n"
|
|
575
|
+
"- direction: 'left'(倒退)或 'right'(前进),默认 'right'\n"
|
|
576
|
+
"- distance_percent: 拖动距离百分比 (0-100),默认 30%\n"
|
|
577
|
+
"- y_percent: 进度条位置(可选,未指定则自动检测)\n"
|
|
578
|
+
"- y: 进度条位置坐标(可选,未指定则自动检测)\n\n"
|
|
579
|
+
"📋 使用示例:\n"
|
|
580
|
+
"- 前进30%:mobile_drag_progress_bar(direction='right', distance_percent=30)\n"
|
|
581
|
+
"- 倒退30%:mobile_drag_progress_bar(direction='left', distance_percent=30)\n"
|
|
582
|
+
"- 前进到指定位置:先点击进度条位置,或使用 mobile_swipe",
|
|
583
|
+
inputSchema={
|
|
584
|
+
"type": "object",
|
|
585
|
+
"properties": {
|
|
586
|
+
"direction": {
|
|
587
|
+
"type": "string",
|
|
588
|
+
"enum": ["left", "right"],
|
|
589
|
+
"description": "拖动方向:'left'(倒退)或 'right'(前进),默认 'right'"
|
|
590
|
+
},
|
|
591
|
+
"distance_percent": {
|
|
592
|
+
"type": "number",
|
|
593
|
+
"description": "拖动距离百分比 (0-100),默认 30%"
|
|
594
|
+
},
|
|
595
|
+
"y_percent": {
|
|
596
|
+
"type": "number",
|
|
597
|
+
"description": "进度条的垂直位置百分比 (0-100),可选,未指定则自动检测"
|
|
598
|
+
},
|
|
599
|
+
"y": {
|
|
600
|
+
"type": "integer",
|
|
601
|
+
"description": "进度条的垂直位置坐标(像素),可选,未指定则自动检测"
|
|
602
|
+
}
|
|
603
|
+
},
|
|
604
|
+
"required": []
|
|
605
|
+
}
|
|
606
|
+
))
|
|
607
|
+
|
|
554
608
|
tools.append(Tool(
|
|
555
609
|
name="mobile_press_key",
|
|
556
610
|
description="⌨️ 按键操作。支持:home, back, enter, search",
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
LICENSE
|
|
2
2
|
MANIFEST.in
|
|
3
3
|
README.md
|
|
4
|
+
__init__.py
|
|
5
|
+
config.py
|
|
4
6
|
requirements.txt
|
|
5
7
|
setup.py
|
|
6
8
|
./__init__.py
|
|
@@ -29,13 +31,27 @@ setup.py
|
|
|
29
31
|
./utils/logger.py
|
|
30
32
|
./utils/xml_formatter.py
|
|
31
33
|
./utils/xml_parser.py
|
|
34
|
+
core/__init__.py
|
|
35
|
+
core/basic_tools_lite.py
|
|
36
|
+
core/device_manager.py
|
|
37
|
+
core/dynamic_config.py
|
|
38
|
+
core/ios_client_wda.py
|
|
39
|
+
core/ios_device_manager_wda.py
|
|
40
|
+
core/mobile_client.py
|
|
41
|
+
core/template_matcher.py
|
|
32
42
|
core/templates/close_buttons/auto_x_0112_151217.png
|
|
33
43
|
core/templates/close_buttons/auto_x_0112_152037.png
|
|
34
44
|
core/templates/close_buttons/auto_x_0112_152840.png
|
|
35
45
|
core/templates/close_buttons/auto_x_0112_153256.png
|
|
36
46
|
core/templates/close_buttons/auto_x_0112_154847.png
|
|
37
47
|
core/templates/close_buttons/gray_x_stock_ad.png
|
|
48
|
+
core/utils/__init__.py
|
|
49
|
+
core/utils/logger.py
|
|
50
|
+
core/utils/operation_history_manager.py
|
|
51
|
+
core/utils/smart_wait.py
|
|
38
52
|
docs/iOS_SETUP_GUIDE.md
|
|
53
|
+
mcp_tools/__init__.py
|
|
54
|
+
mcp_tools/mcp_server.py
|
|
39
55
|
mobile_mcp_ai.egg-info/PKG-INFO
|
|
40
56
|
mobile_mcp_ai.egg-info/SOURCES.txt
|
|
41
57
|
mobile_mcp_ai.egg-info/dependency_links.txt
|
|
@@ -48,4 +64,8 @@ templates/close_buttons/auto_x_0112_152037.png
|
|
|
48
64
|
templates/close_buttons/auto_x_0112_152840.png
|
|
49
65
|
templates/close_buttons/auto_x_0112_153256.png
|
|
50
66
|
templates/close_buttons/auto_x_0112_154847.png
|
|
51
|
-
templates/close_buttons/gray_x_stock_ad.png
|
|
67
|
+
templates/close_buttons/gray_x_stock_ad.png
|
|
68
|
+
utils/__init__.py
|
|
69
|
+
utils/logger.py
|
|
70
|
+
utils/xml_formatter.py
|
|
71
|
+
utils/xml_parser.py
|
|
@@ -25,7 +25,7 @@ if requirements_file.exists():
|
|
|
25
25
|
|
|
26
26
|
setup(
|
|
27
27
|
name="mobile-mcp-ai",
|
|
28
|
-
version="2.6.
|
|
28
|
+
version="2.6.8", # 增强弹窗检测 - 添加浮动关闭按钮检测
|
|
29
29
|
author="douzi",
|
|
30
30
|
author_email="1492994674@qq.com",
|
|
31
31
|
description="移动端自动化 MCP Server - 支持 Android/iOS,AI 功能可选(基础工具不需要 AI)",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mobile_mcp_ai-2.6.6 → mobile_mcp_ai-2.6.8}/core/templates/close_buttons/auto_x_0112_151217.png
RENAMED
|
File without changes
|
{mobile_mcp_ai-2.6.6 → mobile_mcp_ai-2.6.8}/core/templates/close_buttons/auto_x_0112_152037.png
RENAMED
|
File without changes
|
{mobile_mcp_ai-2.6.6 → mobile_mcp_ai-2.6.8}/core/templates/close_buttons/auto_x_0112_152840.png
RENAMED
|
File without changes
|
{mobile_mcp_ai-2.6.6 → mobile_mcp_ai-2.6.8}/core/templates/close_buttons/auto_x_0112_153256.png
RENAMED
|
File without changes
|
{mobile_mcp_ai-2.6.6 → mobile_mcp_ai-2.6.8}/core/templates/close_buttons/auto_x_0112_154847.png
RENAMED
|
File without changes
|
{mobile_mcp_ai-2.6.6 → mobile_mcp_ai-2.6.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
|