10 KiB
调试跟踪 · 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)
- 新版"开始运行"流程:回顶 → 建队列 → page-run --auto-next
- 翻页是否会在真到底时停在
stop_reason=bottom_reached,不再无限滑 - 决定
scripts/vision_fallback.py接入到哪些叉路:- (a)
page_bottom_reached之前先视觉确认 - (b) 未知页面状态异常时视觉判断
- (c) 关键按钮 adb dump 找不到时视觉给坐标
- (a)
时间线
[14:33] 现场盘点
- Web UI 健康,HTTP 200。
work/web_runner.log只有 prelude 两段:android-page-top rc=0 / at_top iterations=2android-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 行重叠,视觉识别新页 → 去掉重叠 → 下一批
- 一直滑到底
锁定决策(三问三答):
- 子流程(核验/提交/邮件)→ 保留 adb XML 驱动(视觉只用列表页 + 翻页)
- 翻页去重 → anchor 驱动(记上批最后一行 masked_name,新截图视觉找位置,以下为新行)
- 前置自检 → 做(调一次 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→ 新 keyvision.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)。
代码改动:
vision_fallback.py加denormalize_bboxes():任一坐标 > 1000 视为已是绝对像素,否则按 0-1000 反推。同时在每行注入tap_point=[cx,cy]。- 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] 三处修复
- 核验正则:
[一-鿿]{0,3}兼容 澳* / 吴* / 周* 之类 2 字名 safe_back_until_list改:max_back 4→2,每次后 sleep 1→1.5s,加is_on_12306_home()检测,退过头立刻 stop- 外层 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] 标题追加。每条只写发生了什么 / 看到了什么 / 接下来做什么。