- add scripts/annotate_roster_invoices.py: match passenger names to invoice PDFs, write status + relative encoded hyperlinks into roster xlsx - add scripts/add_invoice_passenger_name.py, scripts/calibrate.py - add .opencode/skills/invoice-wrapup skill - improve invoice_tool.py / web_ui.py / vision_fallback.py automation - add device/screen/windows adaptation docs - ignore out/ (delivered invoice PDFs contain sensitive data)
252 lines
12 KiB
Markdown
252 lines
12 KiB
Markdown
# 屏幕适配 · 识图点与坐标依赖清单
|
||
|
||
> 目的:梳理 H5 开票流程中所有"关键识图点"及其输出,标明哪些依赖具体屏幕分辨率,为适配其他手机做准备。
|
||
>
|
||
**当前手机分辨率:1260 x 2800**(PNG 头实测,`_screenshot_size` 读取)
|
||
|
||
## 一、识图点全链路(按处理顺序)
|
||
|
||
每个乘客的完整处理流程经过以下识图点。标注 **[屏幕依赖]** 的点换手机必须改。
|
||
|
||
### 阶段 0 · 进入列表页(仅 precheck 不在列表时触发)
|
||
|
||
| # | 识图点 | 输入 | 输出给后续环节 | 屏幕依赖 |
|
||
|---|---|---|---|---|
|
||
| 0a | `_vision_find_button("扫码开票")` | 截屏 | 像素坐标 (x,y) or None → tap 进扫码 | 否(0-1000 归一化) |
|
||
| 0b | `_vision_find_button("订单")` | 截屏 | 像素坐标 → tap | 否 |
|
||
| 0c | `_vision_find_button("电子发票")` | 截屏 | 像素坐标 → tap | 否 |
|
||
| 0d | `_vision_find_button("扫码开票")` | 截屏 | 像素坐标 → tap | 否 |
|
||
| 0e | XML 找"相册"按钮 | uiautomator dump | bounds → click | 否(系统返回) |
|
||
| 0f | `_vision_find_qr_thumbnail()` | 相册截屏 | QR 缩略图坐标 → tap 选图 | 否(0-1000 归一化) |
|
||
|
||
### 阶段 1 · 列表页识行(每轮截屏)
|
||
|
||
| # | 识图点 | 输入 | 输出给后续环节 | 屏幕依赖 |
|
||
|---|---|---|---|---|
|
||
| 1 | `vision_list_rows()` | 截屏 | `rows[{masked_name, id_first, id_last, button_bbox:[x1,y1,x2,y2], tap_point:[x,y]}]`, `scroll_position`, `page_ok` → 后续用 tap_point 点击行 | 否(0-1000 归一化后 denormalize) |
|
||
|
||
**防熄屏 tap [屏幕依赖]**:每轮开头 `tap "630" "1400"`(`invoice_tool.py:2013`)— 硬编码像素,换屏幕要改。
|
||
|
||
### 阶段 2 · 点击乘客行 → 子流程
|
||
|
||
用阶段 1 的 `tap_point` tap,进入核验页。
|
||
|
||
### 阶段 3 · 等待页面状态 `wait_for_page_transition`
|
||
|
||
| # | 识图点 | 输入 | 输出给后续环节 | 屏幕依赖 |
|
||
|---|---|---|---|---|
|
||
| 3a | `xml_page_kind(nodes)` | uiautomator dump | page kind: `verify`/`invoice_info`/`invoice_confirm`/`no_ticket`/`success`/`other` → 状态机分发 | 否 |
|
||
| 3b | `vision_detect_page()`(仅 `screenshot_wait=True`) | 截屏 | `{page, summary}` → 补充页面判断 | 否 |
|
||
|
||
### 阶段 4 · 核验页 - 输入身份证后 8 位
|
||
|
||
| # | 识图点 | 输入 | 输出给后续环节 | 屏幕依赖 |
|
||
|---|---|---|---|---|
|
||
| 4 | XML 找 EditText 输入框 | uiautomator dump | bounds → click 输入框 → 输入 id8 | 否 |
|
||
|
||
### 阶段 5 · 点核验按钮
|
||
|
||
| # | 识图点 | 输入 | 输出给后续环节 | 屏幕依赖 |
|
||
|---|---|---|---|---|
|
||
| 5a | XML 找"核验"按钮 | uiautomator dump | bounds → click | 否 |
|
||
| 5b | `_vision_find_button("核验")`(XML 找不到时 fallback) | 截屏 | 像素坐标 → tap | 否 |
|
||
|
||
### 阶段 6 · 等待 after_verify(network_sensitive,2s + 12s)
|
||
|
||
输出 page kind。超时返回 `other` → 进阶段 7。
|
||
|
||
### 阶段 7 · [after_verify 超时 other] 发票管理页纠偏
|
||
|
||
| # | 识图点 | 输入 | 输出给后续环节 | 屏幕依赖 |
|
||
|---|---|---|---|---|
|
||
| 7a | XML 找"查看"按钮 | uiautomator dump | 无(找不到) | 否 |
|
||
| 7b | `_vision_find_button("查看")` | 截屏 | found:false → 触发探测 | 否 |
|
||
| 7c | `vision_detect_page()` | 截屏 | `v_page=invoice_info`, `summary` → 判定为误判 | 否 |
|
||
| 7d | `_vision_find_button("开具")` | 截屏 | 像素坐标 (1071,1047) → tap 开具 | 否 |
|
||
|
||
### 阶段 8 · 发票信息页 - 点提交
|
||
|
||
| # | 识图点 | 输入 | 输出给后续环节 | 屏幕依赖 |
|
||
|---|---|---|---|---|
|
||
| 8a | XML 找"提交"按钮 | uiautomator dump | bounds `[31,2418][1228,2576]` → click | 否 |
|
||
| 8b | `_vision_find_button("提交")`(fallback) | 截屏 | 像素坐标 → tap | 否 |
|
||
|
||
### 阶段 9 · 等待 after_submit_invoice_info → page=invoice_confirm
|
||
|
||
### 阶段 10 · 发票确认页 - 点确认
|
||
|
||
| # | 识图点 | 输入 | 输出给后续环节 | 屏幕依赖 |
|
||
|---|---|---|---|---|
|
||
| 10a | XML 找"确认"按钮 | uiautomator dump | 无(H5 页面常无 text) | 否 |
|
||
| 10b | `_vision_find_button("确认")` | 截屏 | 像素坐标 (850,2153) → tap | 否 |
|
||
|
||
### 阶段 11 · 等待 after_invoice_confirm(network_sensitive)→ 经过 loading → page=success
|
||
|
||
### 阶段 12 · 成功页 - 下载发票
|
||
|
||
| # | 识图点 | 输入 | 输出给后续环节 | 屏幕依赖 |
|
||
|---|---|---|---|---|
|
||
| 12a | XML 找"发票预览/下载" | uiautomator dump | bounds `[31,1942][1228,2096]` → click | 否 |
|
||
| 12b | XML 找"下载" | uiautomator dump | bounds `[52,1372][1207,1526]` → click → `download_pulled` | 否 |
|
||
|
||
### 阶段 13 · 成功页 - 找继续开票按钮回列表
|
||
|
||
| # | 识图点 | 输入 | 输出给后续环节 | 屏幕依赖 |
|
||
|---|---|---|---|---|
|
||
| 13a | XML 找"继续开票" | uiautomator dump | 无 | 否 |
|
||
| 13b | `_vision_find_button("继续开票")` | 截屏 | found:false → sub_step_error | 否 |
|
||
| 13c | 兜底:XML 找"确定" | uiautomator dump | bounds `[192,1591][1067,1724]` → click | 否 |
|
||
| 13d | 兜底:XML 找"继续开票" | uiautomator dump | bounds `[28,2128][1232,2289]` → click | 否 |
|
||
|
||
### 阶段 14 · 回列表
|
||
|
||
`keyevent 4` (back) × N -> 列表页。back 后 H5 页 scroll 重置到顶部,每轮重新截屏。
|
||
|
||
---
|
||
|
||
## 二、滑动设计详解([屏幕依赖] · 换屏必改)
|
||
|
||
> 滑动是**最大的屏幕依赖点**:起止 y 坐标 + 幅度 + duration 都绑定 1260×2800,换屏不改会导致翻页异常(跳过行 / 重复行 / 超出屏幕)。
|
||
|
||
### 2.1 滑动参数全量(H5 主线 `android-h5-page-run`)
|
||
|
||
| 参数 | 当前默认值 | 占屏比例 (2800 高) | 用途 | 代码位置 |
|
||
|---|---|---|---|---|
|
||
| `--h5-scroll-x` | `630` | 50%(中线) | 滑动 x 坐标(起止相同,垂直滑动) | `:3182` |
|
||
| `--h5-scroll-start-y` | `2400` | **85.7%**(屏幕下方) | 向下滚动时的起点 / 回顶时的终点 | `:3183` |
|
||
| `--h5-scroll-end-y` | `600` | **21.4%**(屏幕上方) | 向下滚动时的终点 / 回顶时的起点 | `:3184` |
|
||
| `--h5-scroll-duration-ms` | `800` | - | 单次滑动耗时(ms),越长越慢越可控 | `:3185` |
|
||
| `--h5-scroll-after` | `2.0` | - | 滑动后等待页面稳定(秒) | `:3186` |
|
||
| `--h5-scroll-top-burst` | `5` | - | 回顶时先连续下拉次数(不等识图) | `:3179` |
|
||
| `--h5-scroll-top-max` | `12` | - | 回顶时带签名确认的最大下拉次数 | `:3180` |
|
||
|
||
### 2.2 滑动方向与幅度
|
||
|
||
**向下滚动**(`_h5_scroll_down`,`:2306`)——看更多下方内容:
|
||
```
|
||
swipe (x, start_y=2400) -> (x, end_y=600) # 手指上滑
|
||
幅度 = 2400 - 600 = 1800px = 屏高的 64.3%
|
||
```
|
||
|
||
**回顶下拉**(`h5_scroll_to_top`,`:2233`)——回到顶部:
|
||
```
|
||
swipe (x, end_y=600) -> (x, start_y=2400) # 手指下滑
|
||
幅度 = 同 1800px(方向相反)
|
||
先连续 5 次(burst),再带 vision_list_rows 签名确认到顶
|
||
```
|
||
|
||
### 2.3 幅度为什么是屏幕相关的
|
||
|
||
| 屏幕 | 屏高 | 幅度 1800px 占比 | 问题 |
|
||
|---|---|---|---|
|
||
| 当前 1260×2800 | 2800 | 64.3% | 正常 |
|
||
| 小屏 1080×2400 | 2400 | 75.0% | **滑太多**,可能跳过行(start_y=2400 已贴底) |
|
||
| 大屏 1440×3200 | 3200 | 56.3% | **滑太少**,翻页慢,同屏签名重复判定到底 |
|
||
| 超大 1080×3120 | 3120 | 57.7% | 同上 |
|
||
|
||
**根因**:`start_y` / `end_y` 是绝对像素,不随屏高缩放。换屏后:
|
||
- `start_y=2400` 在 2400 高的屏幕上 = 最底部,swipe 起点贴边可能无效
|
||
- 幅度 1800px 在小屏占比过大 -> 一次滑过太多行 -> 跳过乘客
|
||
- 幅度 1800px 在大屏占比过小 -> 每次只滑一点 -> 效率低 + 签名去重误判
|
||
|
||
### 2.4 duration 也受影响
|
||
|
||
`duration_ms=800` 对 1800px 幅度 = 2.25 px/ms。换屏后若幅度变大(比如 2400px),同样 800ms 会更快 -> Android 可能触发 fling(惯性滚动),不可控。应保持**速度恒定**:`duration = 幅度 / 目标速度`。
|
||
|
||
### 2.5 原生列表滑动(`android-vision-page-run`,非主线但保留)
|
||
|
||
| 参数 | 当前默认 | 占屏比例 | 代码位置 |
|
||
|---|---|---|---|
|
||
| `--swipe-x` | `630` | 50% | `:3146` |
|
||
| `--swipe-start-y` | `2300` | 82.1% | `:3147` |
|
||
| `--swipe-end-y` | `200` | 7.1% | `:3148` |
|
||
| `--swipe-duration-ms` | `700` | - | `:3149` |
|
||
| `--after-swipe` | `1.5` | - | `:3150` |
|
||
|
||
幅度 = 2300-200 = 2100px = 75%,比 H5 更大。同样的问题。
|
||
|
||
---
|
||
|
||
## 三、[屏幕依赖] 硬编码坐标清单(换手机必须改)
|
||
|
||
> 这些是**像素字面量**,绑定 1260×2800 分辨率。换屏幕不改就会 tap/swipe 到错误位置。
|
||
|
||
| 位置 | 当前值 | 用途 | 代码位置 |
|
||
|---|---|---|---|
|
||
| 防熄屏 tap | `(630, 1400)` | 每轮开头轻点唤醒屏幕 | `invoice_tool.py:2013` |
|
||
| 截图尺寸 fallback | `1260 × 2800` | PNG 头读取失败时的兜底尺寸(极少触发) | `invoice_tool.py:1280` |
|
||
| H5 滑动 x / start_y / end_y | `630 / 2400 / 600` | H5 列表页滑动(见第二节详解) | `--h5-scroll-*` 默认 `:3182-3184` |
|
||
| 原生滑动 x / start_y / end_y | `630 / 2300 / 200` | 原生列表页滑动 | `--swipe-*` 默认 `:3146-3148` |
|
||
|
||
**滑动幅度是最大风险点**:换屏幕后 start_y 可能超出屏高,或幅度比例不对导致跳行/重复/翻页异常。
|
||
|
||
---
|
||
|
||
## 四、[屏幕无关] 自动适配的识图点
|
||
|
||
以下点使用 **0-1000 归一化坐标系**,Qwen 返回归一化坐标后,用**实际截图尺寸** denormalize 到像素,自动适配不同分辨率:
|
||
|
||
- `_vision_find_button(text)` → 截图宽高 × (Qwen 返回的 0-1000 值 / 1000)
|
||
- `vision_list_rows()` → 同上,button_bbox 和 tap_point 都已 denormalize
|
||
- `_vision_find_qr_thumbnail()` → 同上
|
||
- XML bounds → 来自 `uiautomator dump`,系统返回的本身就是当前屏幕正确坐标
|
||
- `vision_detect_page()` → 只返回页面类型,无坐标
|
||
|
||
**结论**:视觉识图点本身是屏幕无关的。问题集中在**硬编码像素坐标**(防熄屏 tap + 滑动参数)。
|
||
|
||
---
|
||
|
||
## 五、适配方案建议
|
||
|
||
### 5.1 启动时读屏幕分辨率(一次性)
|
||
|
||
```python
|
||
# adb shell wm size -> "Physical size: 1260x2800"
|
||
SCREEN_W, SCREEN_H = read_screen_size() # 缓存,全流程复用
|
||
```
|
||
|
||
### 5.2 防熄屏 tap 改为比例
|
||
|
||
`(630, 1400)` -> `(SCREEN_W // 2, SCREEN_H // 2)`
|
||
|
||
### 5.3 滑动坐标 + 幅度改为比例(核心)
|
||
|
||
当前值反推比例(以 2800 高为基准):
|
||
|
||
| 参数 | 当前像素 | 比例 | 动态计算 |
|
||
|---|---|---|---|
|
||
| `scroll_x` | 630 | W×50% | `SCREEN_W // 2` |
|
||
| `start_y` | 2400 | H×85.7% | `int(SCREEN_H * 0.857)` |
|
||
| `end_y` | 600 | H×21.4% | `int(SCREEN_H * 0.214)` |
|
||
| **幅度** | 1800 | H×64.3% | `start_y - end_y`(自动随比例缩放) |
|
||
|
||
换屏后幅度自动适配:
|
||
- 2400 高 -> 幅度 = 2400×(0.857-0.214) = 1543px(仍是 64.3%,行为一致)
|
||
- 3200 高 -> 幅度 = 3200×0.643 = 2058px(同比例)
|
||
|
||
### 5.4 duration 按幅度等比缩放(保持滑动速度恒定)
|
||
|
||
当前:幅度 1800px / duration 800ms = **2.25 px/ms**。
|
||
|
||
换屏后若仍用 800ms 但幅度变了,速度会变 -> 可能触发 fling。应保持速度恒定:
|
||
|
||
```python
|
||
SWIPE_SPEED = 2.25 # px/ms, 当前调好的值
|
||
duration_ms = max(300, int(幅度 / SWIPE_SPEED)) # 下限 300ms 防过快
|
||
```
|
||
|
||
- 2400 高 -> 幅度 1543 -> duration = 686ms
|
||
- 3200 高 -> 幅度 2058 -> duration = 915ms
|
||
|
||
### 5.5 截图尺寸 fallback 改用动态值
|
||
|
||
`return 1260, 2800` -> `return SCREEN_W, SCREEN_H`(启动时读的值)
|
||
|
||
### 5.6 保留 argparse override
|
||
|
||
默认值改用动态计算(比例 × 屏幕尺寸),但 `--h5-scroll-*` / `--swipe-*` 参数仍保留,特殊机型可手动覆盖动态值。
|
||
|
||
### 5.7 原生列表滑动同理
|
||
|
||
`--swipe-*` 参数(start_y=2300→82.1%, end_y=200→7.1%)也按同样方式改比例 + duration 缩放。
|