填报从「一把梭」变成分步:Agent 可以开浏览器、登录、建单、按步填、看页面、关,每步有截图, 中途失败不必从头重来。提交不在本步——提交不可逆,留给 P4 的 submit_preview/submit_execute。 - src/infra/browser/actor.py(新):Playwright 单线程 actor(sync API 的 greenlet 线程亲和要求所有 调用回到创建线程)+ 会话注册表 + cases/<id>/.browser.lock(PID/started_at/heartbeat/url, 心跳 180 秒过期才允许接管;锁文件损坏按无锁处理;atexit 尽力关浏览器)。与 COM actor 分开。 - base.py:login_portal 拆成 submit_login + wait_for_portal(返回布尔不抛异常),新增 portal_reached/ human_required;_screenshot 支持 screenshot_dir 并按 001-步骤名.png 编号、返回路径、失败只告警 - src/mcp/tools/browser.py(新):browser_open/login/new_form/fill/steps/page_state/close,工具 17 → 24 · need_human 用 ok:true + logged_in:false 返回(扫码不是失败,避免宿主当错误重试或另开窗口) · 步骤顺序硬约束,跳步拒绝并用 next_action 指回该补的一步;步骤报错回传 detail.screenshot · browser_close 明确不提交不存草稿;travel 五步、normal 四步(无 subsidy) - tests/test_mcp_browser.py(新,17 例,FakeBot/FakePage 不起浏览器):锁四种情形、need_human 分支与 截图落点、重复登录幂等、未打开时各工具拒绝、类型推断与两 info 并存时拒猜、未登录拒建单、跳步拦截、 步骤推进与 remaining、未知步骤、报错截图、close 释放会话与锁、open 复用、steps 两种流程 - 文档:docs/MCP.md 工具段按 wire schema 重生成(24 工具);AGENTS/SKILL/操作指南/README/浏览器 README 同步分步流程、锁与心跳、有头默认、提交必须交人工;方案文档 §13 记录实现、取舍与未完成项 验证: - 联机跑通 open(3.1s 起有头 Chromium)→ 锁写入(pid/heartbeat)→ page_state(about:blank, 001-state.png 4721 字节)→ 重复 open 复用(already_open=true,不多开窗)→ close(锁释放、注册表清空) - 登录与填报的联机验证需要用户在场(SSO 可能扫码),本轮未做 - 门禁:ruff / format --check / mypy src / deptry / pytest 129 passed / mcp_smoke 24 工具 instructions 2109 字节。仍用 --no-verify 提交并手跑等价检查(原因见 AGENTS 提交纪律)。
8.4 KiB
description, alwaysApply
| description | alwaysApply |
|---|---|
| true |
last_reviewed: 2026-10-19
AGENTS — 项目操作指南
本文件为 Agent 提供高信号量的项目操作知识,避免重复探索。
文档边界
- 禁止使用表情文字输出任何内容。
docs/目录存放面向开源用户、外部贡献者的公开文档。.agents/目录存放维护规范、实施方案、经验总结等内部资料。- 报销业务的操作纪律在
.agents/skills/reimbursement/SKILL.md;工具契约在docs/MCP.md。
开发命令(必须使用 uv)
项目用 uv 管理依赖,版本锁在 uv.lock。
| 操作 | Makefile (跨平台) | tasks.py (Windows) |
|---|---|---|
| 安装依赖 + pre-commit | make install |
python tasks.py install |
| 代码检查(lint+format+typecheck+deptry) | make check |
python tasks.py check |
| 运行测试(含覆盖率报告) | make test |
python tasks.py test |
| MCP stdio 自检(起子进程握手列工具) | make mcp-smoke |
python tasks.py mcp-smoke |
| 启动上传收件器 | make upload |
python tasks.py upload |
| 打印 MCP 注册参数 | make mcp |
python tasks.py mcp |
| 清理缓存和虚拟环境 | make clean |
python tasks.py clean |
没有 CLI 产品入口,也没有 Web 界面:业务入口只有 python -m src.mcp.server(stdio),
工具在宿主里表现为 mcp__arb__<tool>。
注意: tasks.py 的 check 用 && 连接(PowerShell 不支持 &&,但 tasks.py 内部是单行字符串,没问题)。
代码质量工具链(执行顺序)
- Ruff lint —
uv run ruff check .(select: E, F, W, I, N, UP, B; ignore: E501) - Ruff format —
uv run ruff format --check .(line-length: 120) - MyPy strict —
uv run python -m mypy src(整个 src 都在 strict 下,不只是某个入口文件) - deptry —
uv run deptry .(检测未声明、未使用、过时依赖)
uv run mypy 在本机偶尔报 "uv trampoline failed to canonicalize script path",用 uv run python -m mypy 代替。
提交纪律(血的教训)
- 门禁退出码要单独确认再提交,别把"跑检查"和"提交"写在同一条命令链里——检查失败也能走完流程。
- 本仓库
pre-commit只挂 ruff,但它会 stash 未暂存改动再跑,跑完才还原。 若这个过程被中断(超时/杀进程),未暂存的文件会被回滚成 HEAD 版本且git fsck捞不回来。 本项目改造过程中真发生过一次,四个文件的改动靠锚点重放才恢复。 - 因此:提交前先把工作区改动
git add干净,或用--no-verify提交并手动跑完等价检查(在提交说明里写明)。
项目架构(MCP 服务器模式)
唯一业务入口:python -m src.mcp.server(stdio)。编排(读图判断、缺料追问、是否提交)由宿主 Agent 完成,
本项目不调用任何大模型,只提供确定性工具与内核。
src/mcp/— 工具与协议层:server.py注册与启动,tools/按 materials / extraction / analysis / documents 分模块,schemas.py抽取字段定义(单一真源),envelope.py统一{ok, ...}/{ok:false, reason, next_action}信封src/cases.py— 会话目录cases/<case_id>/的唯一实现:材料、收件箱、产物与阶段推导(阶段由产物推导,不存在状态机文件)src/core/— 确定性内核:extraction/(缓存读写、PDF 渲染与文本层、自动转正)、matching/(金额匹配 +runner.py从缓存匹配)、validation/(声明式规则校验)src/infra/— 基础设施:browser/(Playwright 填报 +actor.py单线程持有者与磁盘锁)、documents/(CSV、申购单 Excel COM)、com_thread.py(COM 单线程串行 actor)src/upload/— 上传收件器(独立进程python -m src.upload.server):手机扫码/PC 上传落cases/<id>/inbox/,不含业务编排,绝不由 MCP 子进程拉起
已删除:src/agent/(手写状态机与 SSE)、src/web/(Flask 界面)、src/main.py+pipeline*.py(CLI/双管道)、
core/extraction/{extractor,llm_extractor}.py(内置 LLM 与事件日志)、infra/llm/(提示词迁到 .agents/skills/reimbursement/references/)、
infra/documents/consumable.py(Word 出库单下线)、依赖 llama-index* 与 python-dotenv。
宿主侧两条硬约定
- 调 MCP 工具路径一律正斜杠(
D:/Code/...),反斜杠会被 DSH 的参数绑定层拒收。 required: true的字段(含值为空串的备注类)必须存在于 info JSON 中——这是刻意约束,别改规则文件; 用mcp__arb__info_skeleton取骨架再填值,别凭记忆造结构。
数据流关键产物
| 文件 | 生成者 | 作用 |
|---|---|---|
pages/<stem>/p01.png + text.txt + meta.json |
case_render_pages |
给宿主读的页面与 PDF 文本层(含转正角度) |
.invoice_cache/<stem><ext>.json |
宿主抽取 → case_save_extraction 规范化落盘 |
单张票据结构化数据(小改用 case_patch_extraction) |
match_result.json / invoice_groups.json |
match_payments |
支付与发票的关联、差旅/普通分组 |
invoice_summary.csv / payment_records.csv |
match_payments |
发票级与支付级汇总(申购单取数) |
travel_info.json / normal_info.json |
宿主综合 → save_info |
差旅/普通报销的全部结构化数据 |
artifacts/*.xls |
doc_requisition |
低值易耗品申购单 |
screenshots/NNN-<步骤>.png |
browser_* 分步填报 |
过程截图,出问题时给用户看 |
.browser.lock |
browser_open |
谁拥有该会话的浏览器(PID + 心跳,180 秒可接管) |
缓存机制
会话缓存在 cases/<case_id>/.invoice_cache/,文件名与源文件同名(发票1.pdf → .invoice_cache/发票1.pdf.json)。
缓存 JSON 的 _source_file + 内容指纹决定"是否陈旧",case_status 会报 stale 项;删缓存即重抽。
重要约束
- Windows-only:申购单依赖 Microsoft Excel + COM(
pywin32)。所有 Office COM 调用必须走src/infra/com_thread.py的单线程 actor——并发驱动 Word/Excel 会互抢 ActiveDocument 与模态框; 申购单空白模板在templates/,可用环境变量REQUISITION_TEMPLATE指向别处,产物只写会话目录artifacts/ - 浏览器自动化:Playwright,会打开 Chromium(默认有头),期间不要手动干扰。Playwright sync API 有 greenlet
线程亲和,所有调用必须走
src/infra/browser/actor.py的单线程 actor(与 COM actor 分开,套间要求不同); 浏览器归属用cases/<id>/.browser.lock(PID+心跳)声明。返回owned_by_other_process时不要另开窗口, 那会让 Agent 看到的截图和实际操作的页面不是同一个 - 多进程共存:宿主会保留旧代 stdio 子进程,subagent/teammate 也会各起一条 transport—— observed 4 个 PID 并存。 因此任何"独占资源"(浏览器、Office)都要有磁盘锁,别假设只有一个服务器实例
- 敏感信息:
scripts/data/config.json含登录凭据、cases/含身份证号与卡号,两者都已在.gitignore, 绝不要提交,也不要回显密码 - 发票类型区分:差旅发票(高铁票/酒店住宿)走差旅报销流程,不生成申购单;普通发票生成低值易耗品申购单
上传收件器
uv run python -m src.upload.server # 0.0.0.0:5055,手机扫码可用
uv run python -m src.upload.server --local # 只监听 127.0.0.1
uv run python -m src.upload.server --pin 1234 # 额外访问码
页面地址由 case_create 返回(含每会话令牌)。安全边界:令牌强制(无令牌 403)、只写不读(无列表无下载)、
扩展名白名单、25MB/文件、200 文件/会话、PIN 可选。二维码由服务端用 segno 生成,零外部 CDN。
测试
make test # pytest + coverage report (term-missing)
测试目录 tests/,配置在 pyproject.toml(testpaths、pythonpath=["."])。
COM 相关路径(申购单)不进单测——需要本机装有 Excel,改动后用真实生成验证(约 2-5 秒)。
改完 MCP 工具记得 make mcp-smoke(它验的是 wire 上的工具清单,不是进程内对象)。