feat(M2): 语义层工具 de_ui_tree/de_tap_element——UI 树元素定位点击(text/id/desc,多命中 index),无需坐标更可靠;坐标 tap 保留兜底(WebView/画布元素)

This commit is contained in:
2026-09-04 12:52:55 +08:00
parent 7017ff3315
commit 02776b12b8
2 changed files with 74 additions and 0 deletions
+61
View File
@@ -167,6 +167,67 @@ _KEYS = ("back", "home", "recent", "menu", "power", "volume_up",
"volume_down", "enter", "delete", "search", "camera") "volume_down", "enter", "delete", "search", "camera")
@mcp.tool()
def de_ui_tree(serial: str) -> dict:
"""获取当前界面元素树(文本 JSON):每元素含 text/resource_id/description/class/bounds。
优先用它定位元素(元素驱动操作),比纯坐标点击更可靠。
"""
try:
serial = _check_serial(serial)
els = platform().ui_elements(serial)
except PlatformError as e:
return _err(e)
# 精简输出:去掉 suggested/深度噪音,保留可定位属性
slim = []
for e in els:
slim.append({
"text": e.get("text", "")[:50],
"id": e.get("resource_id", "")[:80],
"desc": e.get("description", "")[:50],
"class": e.get("class", "").split(".")[-1],
"bounds": e.get("bounds", ""),
})
audit.audit("de_ui_tree", serial, "", f"{len(slim)} 元素")
return _ok({"count": len(slim), "elements": slim[:300]})
@mcp.tool()
def de_tap_element(serial: str, by: str, value: str, index: int = 1) -> dict:
"""按元素点击(不需要坐标):by=text|id|desc,value 为匹配文本/资源 id/描述。
元素驱动操作比坐标可靠(界面变化自适应);元素不存在时返回错误,
可改用 de_ui_tree 查元素或 de_tap 坐标兜底。index 用于多命中取第几个(默认 1)。
"""
try:
_check_write()
serial = _check_serial(serial)
if by not in ("text", "id", "desc"):
raise PlatformError("invalid_param", "by 可选 text/id/desc")
if not value or index < 1:
raise PlatformError("invalid_param", "value 不能为空且 index>=1")
import uiautomator2 as u2
d = u2.connect(serial)
kw = {"text": value} if by == "text" else (
{"resourceId": value} if by == "id" else {"description": value})
if index > 1:
kw["instance"] = index - 1
el = d(**kw)
if not el.exists:
raise PlatformError("device_offline",
f"未找到元素({by}={value},index={index})——"
f"建议 de_ui_tree 查看实际元素或 de_tap 用坐标")
el.click()
except PlatformError as e:
return _err(e)
except Exception as e:
return _err(PlatformError("platform_unavailable",
f"元素点击失败: {type(e).__name__}: {str(e)[:100]}"))
audit.audit("de_tap_element", serial, f"{by}={value}#{index}", "ok")
return _ok({"action": "tap_element", "serial": serial,
"by": by, "value": value, "index": index})
@mcp.tool() @mcp.tool()
def de_wake(serial: str) -> dict: def de_wake(serial: str) -> dict:
"""点亮设备屏幕并解锁(熄屏时先调用它再截图)。""" """点亮设备屏幕并解锁(熄屏时先调用它再截图)。"""
+13
View File
@@ -107,6 +107,19 @@ class PlatformClient:
"duration": float(duration)}) "duration": float(duration)})
return self._check_op(r, "swipe") return self._check_op(r, "swipe")
def ui_elements(self, serial):
"""UI 元素树(GET /api/uiauto/elements,uiautodev 服务)。"""
r = self._get("/api/uiauto/elements", serial=serial)
if r.status_code == 503:
raise PlatformError("device_offline", r.text[:120])
if r.status_code != 200:
raise PlatformError("platform_unavailable",
f"元素树 HTTP {r.status_code}")
j = r.json() or {}
if not j.get("ok"):
raise PlatformError("device_offline", str(j.get("error", "取元素失败"))[:120])
return j.get("elements", [])
def wake(self, serial): def wake(self, serial):
"""亮屏并解锁(POST /api/device/screen_all mode=on)。""" """亮屏并解锁(POST /api/device/screen_all mode=on)。"""
r = self._post("/api/device/screen_all", r = self._post("/api/device/screen_all",