diff --git a/mcp_server/mcp_server.py b/mcp_server/mcp_server.py index a783e59..630fd24 100644 --- a/mcp_server/mcp_server.py +++ b/mcp_server/mcp_server.py @@ -167,6 +167,67 @@ _KEYS = ("back", "home", "recent", "menu", "power", "volume_up", "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() def de_wake(serial: str) -> dict: """点亮设备屏幕并解锁(熄屏时先调用它再截图)。""" diff --git a/mcp_server/platform_client.py b/mcp_server/platform_client.py index cb8cadc..761b457 100644 --- a/mcp_server/platform_client.py +++ b/mcp_server/platform_client.py @@ -107,6 +107,19 @@ class PlatformClient: "duration": float(duration)}) 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): """亮屏并解锁(POST /api/device/screen_all mode=on)。""" r = self._post("/api/device/screen_all",