midscene-android:Python 接入 Android AI 自动化

midscene-android 工程说明

midscene-android 是一个 Python 包,将 Midscene.js 的 Android AI 自动化能力桥接到 Python 测试体系。用户通过 MidsceneAgent 调用能力,无需自行安装 Node.js,也无需维护 UI 选择器。

PyPI:https://pypi.org/project/midscene-android/

下载安装:

bash 复制代码
pip install midscene-android

源码:github.com midscene-python 或者 gitee.com midscene-python


背景与目标

Android UI 自动化通常依赖 resource-id、xpath 等固定选择器,页面变更后脚本维护成本较高。Midscene 提供基于视觉模型和自然语言的自动化能力,但核心实现位于 Node 生态。

本项目的目标:

  • 在 Python / pytest 测试体系中接入 Midscene Android 能力,无需迁移技术栈
  • 提供同步 Python API,贴近测试代码写法
  • 用户无需预装 Node.js;首次运行时自动准备运行时环境
  • 覆盖安装、缓存、异常处理、测试与发布等工程环节

架构

#mermaid-svg-mOndj82kwnZEobo8{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-mOndj82kwnZEobo8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-mOndj82kwnZEobo8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-mOndj82kwnZEobo8 .error-icon{fill:#552222;}#mermaid-svg-mOndj82kwnZEobo8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-mOndj82kwnZEobo8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-mOndj82kwnZEobo8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-mOndj82kwnZEobo8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-mOndj82kwnZEobo8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-mOndj82kwnZEobo8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-mOndj82kwnZEobo8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-mOndj82kwnZEobo8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-mOndj82kwnZEobo8 .marker.cross{stroke:#333333;}#mermaid-svg-mOndj82kwnZEobo8 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-mOndj82kwnZEobo8 p{margin:0;}#mermaid-svg-mOndj82kwnZEobo8 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-mOndj82kwnZEobo8 .cluster-label text{fill:#333;}#mermaid-svg-mOndj82kwnZEobo8 .cluster-label span{color:#333;}#mermaid-svg-mOndj82kwnZEobo8 .cluster-label span p{background-color:transparent;}#mermaid-svg-mOndj82kwnZEobo8 .label text,#mermaid-svg-mOndj82kwnZEobo8 span{fill:#333;color:#333;}#mermaid-svg-mOndj82kwnZEobo8 .node rect,#mermaid-svg-mOndj82kwnZEobo8 .node circle,#mermaid-svg-mOndj82kwnZEobo8 .node ellipse,#mermaid-svg-mOndj82kwnZEobo8 .node polygon,#mermaid-svg-mOndj82kwnZEobo8 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-mOndj82kwnZEobo8 .rough-node .label text,#mermaid-svg-mOndj82kwnZEobo8 .node .label text,#mermaid-svg-mOndj82kwnZEobo8 .image-shape .label,#mermaid-svg-mOndj82kwnZEobo8 .icon-shape .label{text-anchor:middle;}#mermaid-svg-mOndj82kwnZEobo8 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-mOndj82kwnZEobo8 .rough-node .label,#mermaid-svg-mOndj82kwnZEobo8 .node .label,#mermaid-svg-mOndj82kwnZEobo8 .image-shape .label,#mermaid-svg-mOndj82kwnZEobo8 .icon-shape .label{text-align:center;}#mermaid-svg-mOndj82kwnZEobo8 .node.clickable{cursor:pointer;}#mermaid-svg-mOndj82kwnZEobo8 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-mOndj82kwnZEobo8 .arrowheadPath{fill:#333333;}#mermaid-svg-mOndj82kwnZEobo8 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-mOndj82kwnZEobo8 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-mOndj82kwnZEobo8 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-mOndj82kwnZEobo8 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-mOndj82kwnZEobo8 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-mOndj82kwnZEobo8 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-mOndj82kwnZEobo8 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-mOndj82kwnZEobo8 .cluster text{fill:#333;}#mermaid-svg-mOndj82kwnZEobo8 .cluster span{color:#333;}#mermaid-svg-mOndj82kwnZEobo8 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-mOndj82kwnZEobo8 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-mOndj82kwnZEobo8 rect.text{fill:none;stroke-width:0;}#mermaid-svg-mOndj82kwnZEobo8 .icon-shape,#mermaid-svg-mOndj82kwnZEobo8 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-mOndj82kwnZEobo8 .icon-shape p,#mermaid-svg-mOndj82kwnZEobo8 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-mOndj82kwnZEobo8 .icon-shape .label rect,#mermaid-svg-mOndj82kwnZEobo8 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-mOndj82kwnZEobo8 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-mOndj82kwnZEobo8 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-mOndj82kwnZEobo8 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} PyTest Python Script
MidsceneAgent
JsonRpc Local Loopback
NodeServiceManager
service.js
@midscene/android
ADB
AndroidDevice
VisionModelAPI

组件职责:

组件 职责
MidsceneAgent Python 侧入口,提供 AI 操作与设备控制 API
NodeServiceManager Node 子进程单例管理,JSON-RPC 通信
service.js Node 微服务,代理 Midscene Android 与 ADB
node_bootstrap 首次运行时下载 Node/npm 并写入本地缓存

设计选择:

  • Python 不重写 Midscene 能力,通过本地 JSON-RPC 桥接 Node 服务
  • 多个 MidsceneAgent 共享同一 Node 进程,通过 sessionId 隔离
  • Python 进程退出时,Node 子进程由 atexit 自动清理

API 设计

API 按测试场景分层,而非直接暴露底层 RPC 方法:

类型 方法 适用场景
复合操作 ai_action() 描述多步业务目标
单步操作 ai_tap()ai_input()ai_scroll() 关键路径的稳定步骤
断言与查询 ai_assert()ai_query()ai_boolean() 基于 UI 语义的验证
设备控制 launch_app()get_screenshot()run_adb_shell() 应用管理与调试

基本用法:

python 复制代码
from midscene_android import MidsceneAgent

with MidsceneAgent("emulator-5556") as agent:
    agent.ai_tap("登录按钮")
    agent.ai_input("用户名输入框", "testuser")
    agent.ai_assert("当前页面是用户首页")

pytest 集成:

python 复制代码
import pytest
from midscene_android import MidsceneAgent


@pytest.fixture
def agent():
    ag = MidsceneAgent("emulator-5556")
    yield ag
    ag.destroy()


def test_login(agent: MidsceneAgent):
    agent.ai_input("用户名输入框", "testuser")
    agent.ai_tap("登录按钮")
    agent.ai_assert("登录成功,显示用户首页")

配置通过环境变量或 .env 提供(MIDSCENE_MODEL_BASE_URLMIDSCENE_MODEL_API_KEYMIDSCENE_MODEL_NAME 等),由 MidsceneConfig 统一加载与校验。


运行时 Node Bootstrap

早期方案将 Node 二进制与 npm 打入 wheel,但受 PyPI 单文件 100MB 限制及多平台体积影响,改为运行时自举:

  1. pip install midscene-android --- 包内仅含 Python 代码与 service.js 源码
  2. 首次调用 MidsceneAgent --- 触发 node_bootstrap.ensure_node_runtime()
  3. 检查 ~/.midscene_android/node_runtime/ 缓存,缺失则从 nodejs.org 下载
  4. 执行 npm install @midscene/android~/.midscene_android/node_service/
  5. 启动 Node RPC 服务;后续运行复用缓存

版本变更时通过缓存元数据(VERSION_FILENPM_DONE_FLAG)触发依赖重装。


可靠性

问题 处理方式
配置缺失 MidsceneConfigError,明确提示缺少的环境变量
RPC 传输错误 统一包装为 MidsceneNodeServiceError,不裸抛 requests 异常
Node 进程崩溃 自动重启一次并重试
stdout 管道阻塞 独立线程持续消费 Node 子进程 stdout,避免长会话卡死
日志文件膨胀 Node 服务日志 5MB 上限并自动轮转

测试分层

测试按依赖程度分为三层,避免全部用例依赖真实设备:

层级 内容 是否需要设备
Level 1 缓存、版本管理、bootstrap 逻辑
Level 2 Node 服务启动、Agent 初始化、RPC 链路
Level 3 完整 AI 操作 是(pytest.mark.device

CI 默认执行 -m "not device",仅跑 Level 1 和 Level 2。


项目结构(核心模块)

复制代码
src/midscene_android/
├── midscene_agent.py    # Python 侧 API 入口
├── node_service.py      # Node 子进程管理与 JSON-RPC
├── node_bootstrap.py    # 运行时 Node/npm 下载与缓存
├── runtime.py           # 缓存路径与版本管理
├── config.py            # 配置加载与校验
└── _node_driver/service/
    ├── service.js       # Node 微服务
    └── package.json

总结

midscene-android 是一个 Python/Node 混合桥接工程:Python 侧提供测试友好的同步 API,Node 侧复用 Midscene.js 能力,运行时 bootstrap 解决 Node 环境分发问题。主要工程工作集中在 RPC 通信、缓存管理、异常语义、测试分层与 PyPI 发布链路。