Files
Auto-Finance/src/infra/browser
wandering 6aec1bc837 fix(browser): 全流程联机验证通过,修掉附件静默假成功等 5 个真问题
两条真实草稿走完全流程并逐张截图核对:普通报销 4 步(明细 3010109/3张/464、支付 231 工行公务卡、
附件入库,单据 202610020884);差旅报销 5 步(明细 交通费/2张/233 火车 阜阳→合肥、补助 3×80+3×100=540、
附件入库,单据 202610030245 总额 773.00)。验完两条都按守卫删除并复查 0 命中。

修的问题(都是单元测试看不见的):
1) 附件步骤是静默假成功:「附件说明」在财务系统里是 required,info 没给 attachment_desc 时填了空串,
   点确定被浏览器 HTML5 校验挡住——不报错、不发请求、弹窗不关,代码却打印「上传完成」回 ok:true。
   新增 base.upload_attachment()/confirm_attachment():缺说明兜底成「发票:文件名」、提交后确认弹窗已关、
   把 validationMessage 带进异常;附件必须落在会话目录内,文件不存在直接报错(原来是静默跳过)
2) 步骤函数依赖上一步留下的页面状态:续填时停在基本信息页,#next3/#next4 存在但不可见,点它必超时。
   所有步骤改成显式 open_tab(...),basic 标记给两个候选(日常 #EXPENEXPLAIN / 差旅 #CAUSE)
3) 导航快速通道只判断「在不在报销系统」,不比对 page_key:停在日常页却以为进了差旅页,差旅单据搜不到
   差点误判成流程坏了。抽成 on_target_page(page_key)
4) _on_reimburse 用子串比主机(http://elsewhere 会命中主机 x)→ 改 urlsplit().netloc 精确比,
   收紧后立刻联机回归,两类页面的判定各自正确
5) 空状态行「没有找到匹配的记录」既被 wait_rows_loaded 当成还在加载(每次搜索白等 20 秒),
   又被 search_forms 算成 1 条命中;两处显式判定,等待超时也从 20 秒收到 10 秒

- browser_fill:续填时前置步骤在财务系统里已完成、内存无记录,降级为 warnings 而不是拒绝(否则合法
  操作被挡),next_action 也不再瞎猜下一步
- browser_open_form 文档与未命中提示:命中判据是列表页可见文字,长说明会被截断,最稳的 match 是单据编号
- tests/test_mcp_browser.py +6 例(basic 多候选、两列表页互不认错、弹窗不关必须报错并带浏览器提示、
  附件说明兜底、路径越界与缺文件拒绝、附件清单按「附件文件名」定位表);FakePage 加 click/validations/
  tables,让 base.py 真实逻辑不依赖 Playwright 也能测。148 passed / 1.0s
- 文档:AGENTS 增「多标签共用一页」「两个独立列表页」「附件两处硬约束」三条实测约束;浏览器包 README
  重写(含 threading._register_atexit 的正确姿势与截图含个人信息的提醒);SKILL 补「每步自洽可单独重填」
  与附件说明兜底;方案文档 §15 记录全流程结果、5 个问题、我自己的两次误判与剩余项

验证:ruff / format / mypy src / deptry / pytest 148 passed / mcp_smoke 25 工具全绿;联机回归 16:14-16:16。
删除与提交类动作仍不进工具层。--no-verify 提交并手跑等价检查。
2026-10-20 16:32:56 +08:00
..

last_reviewed
last_reviewed
2026-10-20

src/infra/browser — 浏览器自动化

Playwright 操作财务报销系统:登录信息门户、进入报销系统、新建或找回单据、分步填报、上传附件。

文件 职责
base.py BaseBot:生命周期、登录、导航、列表页找回单据、标签页切换、附件提交校验、截图
travel.py 差旅填报:基本信息 → 差旅明细 → 支付方式 → 补助清单 → 附件
normal.py 普通填报:基本信息 → 总明细 → 支付方式 → 附件
actor.py 单线程持有者 + cases/<id>/.browser.lock 磁盘锁(PID + 心跳)
__init__.py run_bot() / run_bot_web() 一把梭编排,仅手工回归用

两种用法

  • MCP 分步工具(推荐):browser_open → browser_login → browser_new_form(或 browser_open_form 找回已有单据)→ 逐步 browser_fill(step) → browser_page_state → browser_close; 工具层在 src/mcp/tools/browser.py
  • run_bot() / run_bot_web():一次跑完的编排,只留给手工验证;新代码别用——中途失败只能整体重来

步骤函数的三条铁律

  1. 每步自己切标签页:单据页是「基本信息 / 报销明细 / 支付方式 /(补助清单)/ 附件清单」共用一个页面, 不在当前标签的控件 query_selector 拿得到但不可见。所以每步开头调 bot.open_tab(...), 不要依赖上一步把页面留在了哪儿——旧写法靠 #next3/#next4 串,续填时必卡(联机实测点 #next3 超时)。
  2. 出错必须回抛:log.error 之后要 raise。
  3. 没抛异常不等于做成了:附件那步出现过「无异常、无请求、弹窗不关、清单零行」的假成功—— 提交动作必须回查页面状态。bot.upload_attachment() 会确认弹窗真的关闭,并把浏览器的必填提示 (validationMessage)带进异常。这个项目已经因为静默失败丢过 7 周的出库单产物(方案文档 §11.4)。

线程与进程约束

  • Playwright sync 的 browser/page 只能在创建它的线程里用;MCP 是多线程调用,所以一律经 actor.run_browser() 提交到专属单线程执行(与 COM actor 分开,套间要求不同)
  • 浏览器归属写在 cases/<id>/.browser.lock(PID + 心跳;180 秒未更新可被别的进程接管)。 宿主可能同时跑多代 MCP 子进程(重连窗口、subagent/teammate 各一条 transport), 各开一个 Chromium 会让 Agent 看到的截图不是它正在操作的那个页面
  • 退出钩子必须用 threading._register_atexit(见 actor._register_shutdown_hook): concurrent.futures.thread 的清理走的是这个列表,比 atexit 早跑;注册到 atexit 上就等于 「executor 已被 shutdown,close() 提交不进去」,浏览器关不掉

计划中的 P4

submit_preview(快照 + 摘要 + 一次性 token)与 submit_execute(token 一次性、摘要变更即失效)。 在那之前,填报完成必须停下交人工核对提交。

注意

  • 会打开 Chromium(默认有头),自动化期间不要手动点页面
  • 分步填报的截图写 cases/<id>/screenshots/NNN-<步骤名>.png;只有走 run_bot() 才写 images/debug_*.png(已 gitignore)
  • 截图里有真实姓名、卡号、票据内容:留在会话目录里(已 gitignore),不要贴进对外文档或提交