209 lines
10 KiB
Markdown
209 lines
10 KiB
Markdown
# 调试跟踪 · 2026-06-09 实测
|
||
|
||
> 真机调试现场记录。按时间顺序追加,**不要回头改老条目**——错就再写一条新的把前条作废。
|
||
> 命名规则:`docs/debug-trace-YYYY-MM-DD.md`,一天一份。
|
||
|
||
## 环境快照
|
||
|
||
| 项 | 值 |
|
||
|---|---|
|
||
| 日期 | 2026-06-09 |
|
||
| Web UI | `http://127.0.0.1:8765` (PID 71108,启动 01:02) |
|
||
| Android 设备 | `10ADCX2GUK002A8` (已 `adb devices` 确认) |
|
||
| 名单总数 | 149 |
|
||
| 当前进度 | 5 完成 / 3 失败 / 141 待办 |
|
||
| 日志路径 | `work/web_runner.log` · `work/web_ui.log` |
|
||
|
||
## 验证目标(来自 Handoff)
|
||
|
||
1. 新版"开始运行"流程:回顶 → 建队列 → page-run --auto-next
|
||
2. 翻页是否会在真到底时停在 `stop_reason=bottom_reached`,不再无限滑
|
||
3. 决定 `scripts/vision_fallback.py` 接入到哪些叉路:
|
||
- (a) `page_bottom_reached` 之前先视觉确认
|
||
- (b) 未知页面状态异常时视觉判断
|
||
- (c) 关键按钮 adb dump 找不到时视觉给坐标
|
||
|
||
## 时间线
|
||
|
||
### [14:33] 现场盘点
|
||
|
||
- Web UI 健康,HTTP 200。`work/web_runner.log` 只有 prelude 两段:
|
||
- `android-page-top rc=0 / at_top iterations=2`
|
||
- `android-build-page-queue rc=0 / rows=7 pending=0`
|
||
- 顶页 7 行全部 skipped:3 个 `skip_progress`(程*桐 / 朱*玉 / 王*平,已开过)+ 4 个 `no_match`(张*丽 / 李*国 / 王*楠 / 陈*胜,名单匹配不上)
|
||
- runner 子进程已不在(`ps -ef | grep invoice_tool` 空)。Web UI 重启后内存里的 `RUNNER` 是 None,前次留下的日志是孤儿。
|
||
- 结论:**需要重新触发一次"开始运行",从顶部跑起,观察后续翻页**。
|
||
|
||
### 待办清单
|
||
|
||
- [ ] 调 `/api/start` 触发完整流程
|
||
- [ ] tail -f `work/web_runner.log` 观察 `page_done` / `page_open` / `page_bottom_reached` 是否按预期出现
|
||
- [ ] 真到底时是否停在 `stop_reason=bottom_reached`
|
||
- [ ] 出现需要 fallback 的卡点时,跑 `python3 scripts/vision_fallback.py detect-page` 抓 JSON
|
||
|
||
### [14:37] 从零开始 · 清空工作态
|
||
|
||
用户决定从零起测,流程**从 UI 上传名单**开始。
|
||
|
||
把 `work/` 下 7 个状态文件 `mv` 到 `archive/reset_20260609_<ts>/`(**遵循"不 rm,只归档"项目规则**):
|
||
|
||
| 文件 | 用途 |
|
||
|---|---|
|
||
| `passenger_roster.csv` | 名单 |
|
||
| `android_invoice_progress.csv` | 进度 |
|
||
| `android_page_queue.csv` | 当前页队列 |
|
||
| `web_runner.log` | runner 日志 |
|
||
| `web_ui.log` | UI 日志 |
|
||
| `invoice_tasks.csv` | 任务清单 |
|
||
| `last_screenshot.png` | 最近截屏 |
|
||
|
||
保留 `work/android_*.txt`(纯调试 dump 快照,不影响行为)。
|
||
|
||
清空后 `/api/status` 响应:
|
||
- `counts: {total:0, completed:0, failed:0, in_progress:0, pending:0}` ✓
|
||
- `roster: []` ✓
|
||
- `runner.status: failed`(残留 RUNNER 指针指向上次的死子进程,无害,下次 start 会覆盖)
|
||
|
||
**下一步**:等用户在浏览器里 `http://127.0.0.1:8765` 点"上传名单"导入 xlsx,再观察 `/api/status` 是否吃进 149 行。
|
||
|
||
### [14:55] 方向大调整 · 改走视觉驱动
|
||
|
||
用户暂停传统路径,提新方案:
|
||
- 手机端**用户手动**扫码进入开票界面(不需要程序回顶)
|
||
- 点"开始"→ **直接截图 → 视觉识别**当前页所有可见乘客行 → 按页为批次开票
|
||
- 一批完后**滑动**,**保证至少 1 行重叠**,视觉识别新页 → 去掉重叠 → 下一批
|
||
- 一直滑到底
|
||
|
||
锁定决策(三问三答):
|
||
1. 子流程(核验/提交/邮件)→ **保留 adb XML 驱动**(视觉只用列表页 + 翻页)
|
||
2. 翻页去重 → **anchor 驱动**(记上批最后一行 masked_name,新截图视觉找位置,以下为新行)
|
||
3. 前置自检 → **做**(调一次 detect-page 要 `page=list`)
|
||
|
||
我自定的小决策:
|
||
- `button_bbox` 取中心 tap
|
||
- 截图原始像素传给豆包,要求按真实坐标返
|
||
- 批次内失败重试 1 次,翻页后不回头
|
||
- 老的 `android-page-run` 路径保留并行
|
||
|
||
### [15:00] 步骤① 视觉 list-rows 接口已加
|
||
|
||
`scripts/vision_fallback.py` 新增 `list-rows` 子命令:
|
||
- 截图 → 读 PNG header 提取真实像素 width/height → 塞给 prompt
|
||
- prompt 要求豆包返回 `{rows:[{masked_name, id_first, id_last, button_bbox:[x1,y1,x2,y2]}], scroll_position, page_ok}`
|
||
- 增加 `parse_json_lenient`,容错 markdown 代码块包裹
|
||
|
||
下一步:跑 `python3 scripts/vision_fallback.py list-rows` 抽样验证 bbox 准确性,再做步骤②。
|
||
|
||
### [15:03] 步骤③ dry-run 失败 · 豆包 key 401
|
||
|
||
`list-rows` 调用返回 401 AuthenticationError(`The API key doesn't exist`)。
|
||
昨晚 handoff 明确说"测完明天记得把它置空或撤销"—— 用户应该已撤销。
|
||
|
||
**阻塞**:需要用户在 `config.local.json` 重填 `vision.api_key` 才能继续视觉路径验证。
|
||
|
||
### [15:18] 模型切换 · Doubao-Seed-2.0-mini
|
||
|
||
用户提供新 key,授权范围 `Doubao-Seed-2.0-mini`。查方舟 `/models`:
|
||
- 实际 model id:`doubao-seed-2-0-mini-260428`(2026-04 版,带 audio 模态最新版)
|
||
- 状态 Active,modalities 含 image
|
||
- 之前认为应该用 `doubao-seed-1-6-vision-250815`,但用户 key 限定 mini → 改用 mini
|
||
|
||
更新 `config.local.json`:
|
||
- `vision.api_key` → 新 key
|
||
- `vision.model` → `doubao-seed-2-0-mini-260428`
|
||
|
||
### [15:21] 通路验证 OK,但手机黑屏
|
||
|
||
`python3 scripts/vision_fallback.py detect-page` 返回 200,2.97s 响应,JSON 合规。
|
||
模型回答:`page=other, summary=当前页面为全黑,无法识别具体内容`。
|
||
说明:**API 通,但截屏拿到的是锁屏黑画面**。需要用户先解锁 + 打开 12306 列表页。
|
||
|
||
成本对账(本次调用):
|
||
- prompt_tokens=1618(图片占大头)
|
||
- completion_tokens=212(含 182 reasoning)
|
||
- 单次成本估算几厘钱
|
||
|
||
**阻塞**:等用户解锁手机 + 切到扫码开票单列表页,然后重跑 `list-rows`。
|
||
|
||
### [15:30] 步骤③ dry-run 结果:豆包 mini 不能给坐标
|
||
|
||
跑 `list-rows`,7 个名字 OCR 完美(全对),但 **bbox 是模型按"7 行均分屏"猜的等差数列**:
|
||
- 模型 reasoning 漏嘴:"按钮的x1是720,y1是290,...大概的坐标"
|
||
- y 值 295/385/475/565/655/745/835 等差 90;真实 y 值 845/1139/1433/1727/2021/2315/2609 等差 294
|
||
- 误差 500-1700 px,完全不可用
|
||
|
||
**结论**:Doubao-Seed-2.0-mini 没有 visual grounding 能力,只能 OCR 不能定位。
|
||
|
||
### [15:36] 换硅基流动 Qwen3-VL-8B,grounding 通过
|
||
|
||
用户给了硅基流动 key + `Qwen/Qwen3-VL-8B-Instruct`。更新 `config.local.json`。
|
||
|
||
重跑 `list-rows`,bbox 精度大幅提升,但发现 Qwen 用 **0-1000 归一化坐标**(Qwen 系标准约定,不论 prompt 怎么写都按这个返)。
|
||
反推到绝对像素:`x = qwen_x * image_width / 1000`,误差稳定在 5-21 px,**全部落在按钮内**(按钮 250×94)。
|
||
|
||
代码改动:
|
||
1. `vision_fallback.py` 加 `denormalize_bboxes()`:任一坐标 > 1000 视为已是绝对像素,否则按 0-1000 反推。同时在每行注入 `tap_point=[cx,cy]`。
|
||
2. prompt 改为明确要求 Qwen 返 0-1000 归一化,免得不同图片混乱。
|
||
|
||
成本量级:Qwen3-VL-8B / 硅基流动,单次 ~4200 token ≈ 几厘钱;全程 21-25 次 ≈ ¥0.05-0.1。可以接受。
|
||
|
||
### [15:42] 进入步骤② · android-vision-page-run
|
||
|
||
设计循环(已对齐):
|
||
- 自检 detect-page == list
|
||
- 视觉 list-rows → anchor 去重 → 匹配 roster → tap pending 行
|
||
- tap 后复用现有 `android_invoice_step` 跑子流程(核验/提交/邮件)直到回列表
|
||
- 本批末行 masked_name 作下批 anchor
|
||
- swipe `y=2300 → y=200`(2800 屏保 1-2 行重叠)
|
||
- 终止:scroll_position=bottom 且去重后空集 / 或 anchor 连续两批没变(安全网)
|
||
|
||
### [15:50] 步骤② 完成 + dry-run 全过
|
||
|
||
- `invoice_tool.py` 新增 `android_vision_page_run` + 4 个 helper(`call_vision_subcommand` / `vision_detect_list_page` / `vision_list_rows` / `android_swipe_up` + `drive_invoice_subflow`),老命令 `android-page-run` 不动并行存在
|
||
- 注册新 subparser `android-vision-page-run` 含 `--dry-run / --max-actions / --max-batches / --no-precheck` 等
|
||
- dry-run 输出:precheck ✓ / 视觉 7 行 ✓ / roster 匹配 3 pending(程*桐/朱*玉/王*平)/ tap 坐标对齐 XML 真值
|
||
|
||
### [15:51] 单人真测 · 程*桐 → no_ticket 分支闭环
|
||
|
||
`--max-actions 1`:tap [1029, 841] → 视觉抓出"扫码开票单 / 您没有可开具电子发票的客票" → 标 `no_ticket` → back 回列表(1 次)→ `sub_back_to_list steps=1` → max_actions 命中即停。
|
||
完整流程 ✓:**视觉抓行 + tap + 子流程分支 + 状态写回 + 自动退出**,全部按预期。
|
||
|
||
### [15:55] 用户改要求 · "不管 roster,识别出来就 tap"
|
||
|
||
用户决定:no_match 名也 tap,12306 返啥就记啥。改 `android_vision_page_run` 移除 `match_count != 1` 跳过,无名单匹配也进 sub-flow(预期:已开过 → no_ticket 干净退;未开 → 核验弹窗 → 报错 → 兜底 back)。
|
||
|
||
### [16:04] 翻车一 · denormalize 阈值 bug
|
||
|
||
`--max-actions 2`,视觉抓到屏幕已被人为滚动到的 9 行新名字。tap 坐标变成 [816, 156] / [816, 261] — y 极小,落在标题栏。
|
||
原因:Qwen 这次最大坐标返回 1013(赵*丹 底部行),`denormalize_bboxes` 的判定 `> 1000 视为绝对像素` 太严,1013 被误判 → bbox 没反归一 → 中心点直接用归一化值当像素 tap。
|
||
修:阈值改成 `max <= 1100 且 image_min_dim > 1100`,允许 Qwen rounding 飘到 1000-1030。
|
||
|
||
### [16:08] 翻车二 · 2 字脱敏名正则失配 + back 退过头
|
||
|
||
修阈值后真 tap 落点:李*霞 ✓ 进 no_ticket、澳* tap ✓ 进核验框,但核验弹窗内"请输入乘车人 澳* 证件号码后8位" 被正则 `[一-鿿]{1,3}` 强制 ≥1 字过滤,**澳* 是 2 字脱敏**,无法解析。
|
||
报错 → 兜底连按 4 次 back → 把列表页一起退出 → 手机停在 12306 首页。
|
||
|
||
### [16:13] 三处修复
|
||
|
||
1. 核验正则:`[一-鿿]{0,3}` 兼容 澳* / 吴* / 周* 之类 2 字名
|
||
2. `safe_back_until_list` 改:max_back 4→2,每次后 sleep 1→1.5s,加 `is_on_12306_home()` 检测,退过头立刻 stop
|
||
3. 外层 abort 闸门:子流程返回非 `back_to_list` 时,如不在列表页就 `return`,不再 tap
|
||
|
||
### [16:14] 用户提出录制示范方案
|
||
|
||
用户决定换打法:**他手动操作一遍 → 我用 `record_step.py` 抓 XML+screenshot → 反推脚本逻辑**。
|
||
新工具 `scripts/record_step.py` 就位(session 自增、`--new` / `--list` / 默认追加)。
|
||
本次 session 未真正录制,留给下次。
|
||
|
||
### [16:15] 收尾 · 用户重开 session
|
||
|
||
写 CHANGELOG 4 条 + INDEX.md Handoff 全状态盘点 + 本日志收口。下次开会的 Claude 看 INDEX 接力点即可上手。
|
||
|
||
---
|
||
|
||
> 本日志到此封顶。后续工作重新写 `docs/debug-trace-YYYY-MM-DD.md`,不在本文件追加。
|
||
|
||
---
|
||
|
||
> 后续条目继续以 `### [HH:MM] 标题` 追加。每条只写**发生了什么 / 看到了什么 / 接下来做什么**。
|