autotrain/docs/debug-trace-2026-06-09.md
2026-06-11 12:18:53 +08:00

209 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 调试跟踪 · 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] 标题` 追加。每条只写**发生了什么 / 看到了什么 / 接下来做什么**。