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

10 KiB
Raw Blame History

调试跟踪 · 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 个状态文件 mvarchive/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.modeldoubao-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.pydenormalize_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] 标题 追加。每条只写发生了什么 / 看到了什么 / 接下来做什么