Metadata-Version: 2.4
Name: visionauto
Version: 0.1.0
Summary: uiautomator2-based device control with AI vision selectors
Project-URL: Homepage, https://github.com/wwwsctvcom/visionauto
Project-URL: Repository, https://github.com/wwwsctvcom/visionauto
Project-URL: Issues, https://github.com/wwwsctvcom/visionauto/issues
Author: visionauto
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: android,automation,ocr,testing,uiautomator2,vision,vlm
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Testing
Requires-Python: >=3.12
Requires-Dist: airtest>=1.3.0
Requires-Dist: json-repair>=0.5.0
Requires-Dist: numpy>=1.24.0
Requires-Dist: openai>=1.0.0
Requires-Dist: opencv-python>=4.8.0
Requires-Dist: pillow>=10.0.0
Requires-Dist: uiautomator2>=3.0.0
Provides-Extra: dev
Requires-Dist: build; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: twine; extra == 'dev'
Description-Content-Type: text/markdown

# visionauto

uiautomator2 设备控制 + AI 视觉选择器。用 u2 做"手"（截图、点击坐标、手势、弹窗 watcher），用 VLM 做"眼"（定位元素）。用法对齐 u2：`d(text="你好").click()`，但定位走视觉，能识别自绘/Canvas/图标按钮等无障碍树拿不到的控件。

## 安装

```bash
pip install -e ".[dev]"          # dev 含 pytest
adb devices                       # 确认有一台设备在线
```

## 快速开始

```python
import visionauto as va

d = va.connect(api_key="sk-xxx", provider="qwen")   # 或纯走环境变量

d.app_start("com.tencent.mm")
d(text="微信").wait(timeout=15)

d(description="右上角的搜索按钮").click()             # 图标无文字 → 语义定位
d(description="搜索输入框").input("你好")
d(text="发送").click()
```

## 配置

优先级：传参 > 环境变量 > 默认值。所有字段通用，不按 provider 区分。

| 字段 | 环境变量 | 默认 | 说明 |
| --- | --- | --- | --- |
| `provider` | `VISIONAUTO_PROVIDER` | `glm` | `glm` / `qwen` / `openai` |
| `api_key` | `VISIONAUTO_API_KEY` | - | API key |
| `model` | `VISIONAUTO_MODEL` | provider 默认 | 模型名 |
| `base_url` | `VISIONAUTO_BASE_URL` | provider 默认 | OpenAI 兼容端点 |
| `temperature` | `VISIONAUTO_TEMPERATURE` | `0.0` | 不支持的模型自动省略 |
| `opencv_threshold` | `VISIONAUTO_OPENCV_THRESHOLD` | `0.8` | image 兜底置信度 |
| `opencv_method` | `VISIONAUTO_OPENCV_METHOD` | `auto` | `auto/template/multiscale/keypoint` |
| `opencv_rgb` | `VISIONAUTO_OPENCV_RGB` | `True` | OpenCV 兜底 RGB 二次校验 |
| `cache_ttl` | `VISIONAUTO_CACHE_TTL` | `2.0` | 截图复用秒数 |
| `default_timeout` | `VISIONAUTO_DEFAULT_TIMEOUT` | `10.0` | `wait()` 默认超时 |
| `normalize_text` | `VISIONAUTO_NORMALIZE_TEXT` | `True` | 文字匹配前折叠空白 |
| `debug` | `VISIONAUTO_DEBUG` | `False` | 调试追踪开关 |
| `debug_dir` | `VISIONAUTO_DEBUG_DIR` | `out/trace` | 追踪输出目录 |

内置 provider：

| provider | 默认模型 | 默认端点 |
| --- | --- | --- |
| `glm` | `GLM-5V-Turbo` | 智谱 `open.bigmodel.cn` |
| `qwen` | `qwen3.7-max-2026-06-08` | 阿里 DashScope |
| `openai` | （需自备） | OpenAI 兼容任意端点 |

## 选择器

`d(**query)` 返回 `Selector`，懒执行。定位方式由传入的键决定：

| 键 | 定位方式 | 示例 |
| --- | --- | --- |
| `text` / `textContains` / `textStartsWith` / `textMatches` | AI 识别所有可点击控件 + OCR 文字，客户端按模式过滤 | `d(text="设置")`、`d(textMatches=r"设置.*")` |
| `description` | 语义定位：把自然语言描述直接喂 AI 选目标 | `d(description="左上角的红色图标")` |
| `image` | VLM 优先；找不到时 airtest OpenCV（template/multiscale/keypoint）兜底 | `d(image="./btn.png")` |
| `index` | 在匹配结果里取第 n 个（从 0） | `d(text="删除", index=2)` |

文字匹配默认折叠空白（`normalize_text=True`），"设置 中心" 仍能匹配 "设置中心"，规避 AI 拆字。

## Selector 方法

查询类：

| 方法 | 说明 |
| --- | --- |
| `exists()` | 是否存在 |
| `wait(timeout=None, interval=0.5)` | 轮询等待出现，返回是否出现 |
| `wait_gone(timeout=None)` | 轮询等待消失 |
| `count()` | 匹配数量 |
| `all()` | 返回全部匹配 `Located` |
| `get_text()` | 元素文字（text 必有；description/image 命中时 AI 会 OCR 回填） |
| `center()` | 中心点 `(x, y)`（绝对像素） |
| `bounds()` | `(x1, y1, x2, y2)` 绝对像素 |

动作类（任意 locator 通用，找不到抛 `ElementNotFound`）：

| 方法 | 说明 |
| --- | --- |
| `click()` | 点击中心 |
| `long_click(duration=None)` | 长按 |
| `click_exists(timeout=None)` | 出现就点，返回是否点了 |
| `input(text, clear=False)` | 点击聚焦后输入文字 |
| `drag_to(**query, duration=0.5)` | 拖到另一个元素（同张截图取两点坐标） |
| `swipe(direction, scale=0.9, duration=0.5)` | 从该元素出发按方向滑，u2 `swipe_ext` 风格 |

```python
d(text="A").drag_to(text="B")              # 注意：目标是 kwargs，不是 d(...)
d(text="搜索框").input("visionauto", clear=True)
d(text="列表项").swipe("left", scale=0.5)   # left/right/up/down
```

## 弹窗处理

直接用 uiautomator2 的 `watcher`（基于无障碍树，即时且不耗 AI）。`d.watcher` 已透传到 u2：

```python
for txt in ["允许", "知道了", "暂不", "关闭"]:
    d.watcher.when(txt).click()   # 出现即点
d.watcher.start(2.0)              # 后台每 2s 检查一次
try:
    d.app_start("com.tencent.mm")  # 主流程；弹窗被自动处理
finally:
    d.watcher.stop()
    d.watcher.reset()
```

标准系统弹窗（有 accessibility text）用 watcher 最稳；自绘/无障碍缺失的弹窗再用 `d(description="...").click()` 视觉处理。完整示例见 `examples/popup_watcher.py`。

## 直接使用 uiautomator2 API

`d` 透传底层 u2 设备的全部 API，无需 `.u2` 即可调用：

```python
d.app_start("com.android.settings")
d.press("back")                   # home/back/enter/volume_up...
d.swipe(100, 800, 100, 200)
d.window_size()
d.screen_off()
d.app_stop("com.tencent.mm")
```

视觉选择器走 `d(text=...)`（`__call__`），u2 API 走属性访问（`__getattr__` 委托），两者互不冲突。需要显式拿到原始 u2 设备时用 `d.u2`。

## 调试可视化

开启 debug 后**每次 AI 识别自动落盘**，跑完看 `out/trace/` 回放全流程，定位是哪一步识别错了——无需手动调任何 API：

```python
d.start_debug("out/trace")        # 或 config(debug=True)，或 VISIONAUTO_DEBUG=1
d(text="设置").click()
d(description="返回按钮").click()
d.stop_debug()
```

`out/trace/` 产物：

- `trace.log` —— 完整时间线：每步的 query / 定位类型 / 命中节点数 / 是否缓存命中 / 执行的动作。
- `NNNN_<kind>.png` —— 每次识别的标注截图：**被点击的框用绿色粗框 + 写你的指令**（`description='...'`/`text='...'`），其余匹配框灰色标序号；不写 OCR 文字，方便核对"prompt 是否点对了"。

临时看整屏识别结果（视觉版 `dump_hierarchy`）：

```python
nodes = d.dump("out/dump.png")    # 返回所有 clickable 节点并保存标注图（带 OCR 文字）
```

## 坐标约定

VLM 返回 `[x1,y1,x2,y2]`，GLM-V / Qwen-VL 通常归一化到 `0-999`。`coords.py` 自适应量纲并换算到设备像素：

- `max ≤ 1.0` → 视为 `[0,1]`
- `max ≤ norm_scale`（默认 1000，provider 可在 `COORD_NORM_SCALE` 声明）→ 按 `norm_scale` 归一
- `max > norm_scale` → 视为绝对像素，按截图实际尺寸还原

内部统一转成 `[0,1]` 规范坐标，再 `device_x = vx * window_width`。返回的 JSON 先经 `json-repair` 修复（容忍尾逗号、单引号、代码围栏、夹带说明文字）再解析。

## Provider 与扩展模型

每个 provider 各自实现 `supports_temperature()`，对 thinking/reasoning 类模型（qwq、`*-thinking`、GLM-Z1、o1/o3 等）自动不传 `temperature`，普通模型传 `temperature=0`。

加一个新模型 provider：

```python
from visionauto.providers.base import OpenAICompatibleProvider
from visionauto.providers import register_provider

class MyProvider(OpenAICompatibleProvider):
    DEFAULT_BASE_URL = "https://api.example.com/v1/"
    DEFAULT_MODEL = "my-vl-model"
    def supports_temperature(self) -> bool:
        return "thinking" not in (self._model or "").lower()

register_provider("my", MyProvider)
# 然后 VISIONAUTO_PROVIDER=my
```

## 架构

```text
visionauto/
├── config.py            通用配置（api_key/model/base_url/阈值/超时/debug）
├── device.py            VisionDevice: 包 u2.Device + d(...) 工厂 + 截图缓存 + dump
├── selector.py          Selector: 懒执行链式 API（exists/click/wait/drag_to/swipe...）
├── located.py           Located: bbox + text
├── coords.py            [0,1] 规范坐标 → 设备像素，自适应 0-999/[0,1]/绝对像素
├── cache.py             截图+解析短 TTL 缓存
├── prompts.py           三套 prompt（统一 schema：clickable 节点 + bbox + OCR text）
├── utils.py             json-repair 解析
├── viz.py               标注绘图（trace 指令标签 / dump OCR 标签）
├── debug.py             DebugRecorder: 自动记录每次 AI 识别 + 动作时间线
├── exceptions.py        ElementNotFound 等
├── providers/           传输+鉴权层（OpenAI 兼容）
│   ├── base.py          OpenAICompatibleProvider + supports_temperature
│   ├── glm.py           GLM（智谱）
│   ├── qwen.py          Qwen（阿里 DashScope）
│   └── openai.py        通用 OpenAI 兼容
├── matching/            image 兜底：airtest OpenCV（template/multiscale/keypoint）
│   └── opencv.py
└── locator/             策略层
    ├── text.py          一次 VLM 取全部可点击控件+OCR 文字，客户端按模式过滤
    ├── description.py   语义定位：描述直接喂 AI
    └── image.py         VLM 优先，airtest OpenCV 兜底
```

## 示例

`examples/` 下：

- `wechat_search.py` —— 打开微信 → 搜索 → 进聊天 → 发消息（完整视觉流程）
- `search_download.py` —— 主页搜索"微信" → 判断第一个结果右侧"下载/打开"按钮 → 点击（含条件分支）
- `popup_watcher.py` —— u2 watcher 自动处理权限/升级弹窗
- `_config.py` —— 示例统一的 provider/model/api_key 配置（默认 qwen，env 可覆盖）

运行：

```bash
python examples/wechat_search.py
python examples/search_download.py
python examples/popup_watcher.py
```

## 测试

```bash
# 用 adb 真机截图跑三个核心 prompt，识别结果画框存到 out/
VISIONAUTO_API_KEY=... VISIONAUTO_PROVIDER=glm pytest -v -s tests/test_core_prompts.py

# 测试多个模型的图像问答能力
VISIONAUTO_PROVIDER=qwen VISIONAUTO_API_KEY=sk-... \
VISIONAUTO_TEST_IMAGE=./x.png \
VISIONAUTO_TEST_MODELS=qwen3.7-max-2026-06-08,qwen3.7-plus \
pytest -v -s tests/test_provider_vision.py
```

## 发布到 PyPI

发布前先补全 `pyproject.toml` 里的 `authors` 和 `project.urls`（标了 TODO），并确认 `visionauto` 名称在 PyPI 未被占用。

```bash
pip install -e ".[dev]"          # 含 build / twine
python -m build                  # 生成 dist/*.whl 和 dist/*.tar.gz
twine check dist/*               # 校验元数据
twine upload --repository testpypi dist/*   # 先传 TestPyPI 验证
pip install -i https://test.pypi.org/simple/ visionauto   # 试装

# 正式发布
twine upload dist/*
# 之后任何人： pip install visionauto
```

要点：

- 包代码在 `visionauto/`，`tests/`、`examples/`、`out/` 不会被打进 wheel（hatchling 只打包 `packages = ["visionauto"]`）。
- 每次发布记得递增 `pyproject.toml` 的 `version`，PyPI 不允许覆盖已上传的版本。
- 重依赖（`airtest`/`opencv-python`/`uiautomator2`）体积较大；若想瘦身可把 image 兜底依赖挪到 `[project.optional-dependencies]` 的 extra（如 `pip install visionauto[image]`），需同步改 `matching/opencv.py` 的 import 容错（已有 `try/except ImportError`）。
