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_URL、MIDSCENE_MODEL_API_KEY、MIDSCENE_MODEL_NAME 等),由 MidsceneConfig 统一加载与校验。
运行时 Node Bootstrap
早期方案将 Node 二进制与 npm 打入 wheel,但受 PyPI 单文件 100MB 限制及多平台体积影响,改为运行时自举:
pip install midscene-android--- 包内仅含 Python 代码与service.js源码- 首次调用
MidsceneAgent--- 触发node_bootstrap.ensure_node_runtime() - 检查
~/.midscene_android/node_runtime/缓存,缺失则从 nodejs.org 下载 - 执行
npm install @midscene/android到~/.midscene_android/node_service/ - 启动 Node RPC 服务;后续运行复用缓存
版本变更时通过缓存元数据(VERSION_FILE、NPM_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 发布链路。