两条真实草稿走完全流程并逐张截图核对:普通报销 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 提交并手跑等价检查。
last_reviewed
| last_reviewed |
|---|
| 2026-10-19 |
财务报销自动化(MCP 服务器)
把发票、支付截图、出差申请单变成财务系统要的结构化数据,顺带生成低值易耗品申购单。
它不是一个应用,而是一个 MCP 服务器:读图、判断、决定下一步交给宿主 Agent(如 DSH), 本项目只做确定性的活——PDF 渲染转正、金额匹配、规则校验、Excel 产物、浏览器填报。
为什么改:原来自带一整套前端 + 内置 LLM + 手写状态机(提取-校验-修正循环、轮次上限、SSE 事件流)。 灵活性太差——想换个模型、加一类票据、改流程都得动代码。2026-10-19 起全部交给宿主 Agent 编排。
它做什么
| 环节 | 谁做 |
|---|---|
| 看发票、判断类型、抽字段 | 宿主 Agent(多模态模型) |
| 页面渲染、PDF 文本层、自动转正 | 本项目(PyMuPDF) |
| 发票与刷卡记录金额匹配 | 本项目(确定性贪心 + 业务约束,输出待复核清单) |
| 完整性校验 | 本项目(config/validation_rules.json 声明式规则) |
| 申购单 Excel、财务系统填报 | 本项目(COM / Playwright) |
| 流程编排、缺料追问、要不要提交 | 宿主 Agent |
快速开始
uv sync # 依赖(要求 Python >= 3.12)
uv run playwright install chromium # 填报要用(P3 工具接入后需要)
配好 scripts/data/config.json(参考 config.example.json:工号、密码、默认报销人、公务卡号、人员编号、上传 PIN)。
该目录整体在 .gitignore 里,别提交。
把服务器注册到宿主(DSH profile patch):
- insert:
- id: mcp-arb
name: '@deepseek-ai/dsh-mcp-client'
config:
serverName: arb
transport: stdio
command: '<项目根>/.venv/Scripts/python.exe'
args: ['-m', 'src.mcp.server']
cwd: '<项目根>'
自检(真的起一个子进程走 stdio 握手):
make mcp-smoke # 或 python tasks.py mcp-smoke
手机扫码上传材料(独立进程,必须单独起):
make upload # 默认 0.0.0.0:5055;--local 只监听 127.0.0.1;--pin 加访问码
case_create 会返回带令牌的手机页地址,上传落 cases/<id>/inbox/,之后由 Agent 收取。
上传服务是只写不读的:没有列表也没有下载接口,扩展名白名单 + 25MB + 每会话 200 个文件封顶。
一次报销长什么样
case_create → case_add_files → case_render_pages → case_read_text / 读 PNG
→ case_save_extraction(逐张)→ match_payments(处理 review_required)
→ info_skeleton → save_info → doc_requisition
→(可选)browser_open → browser_login → browser_new_form → browser_fill × 步骤 → browser_page_state
提交动作不在里面:submit_preview / submit_execute 还没做,填报完成必须交人工核对后自己点提交。
25 个工具的参数表与返回约定:docs/MCP.md。操作步骤(财务口径):docs/报销操作指南.md。
项目结构
src/
├── mcp/ 工具与协议层(server.py 注册;tools/ 分材料·抽取交接·分析·文档)
├── cases.py 会话目录 cases/<id>/ 的唯一实现:阶段由产物推导,无状态机
├── upload/ 扫码上传收件器(独立进程,绝不由 MCP 子进程拉起)
├── core/ 确定性内核:extraction(缓存·渲染·转正)matching validation
├── infra/ browser(Playwright 填报)documents(CSV·申购单 COM)
├── config.py scripts/data/config.json + 环境变量
└── exceptions.py 异常定义(MCP 层转成 ok:false 信封,不抛穿协议)
.agents/skills/reimbursement/ 宿主侧操作手册与字段判读参考(从旧 LLM 提示词迁移)
docs/MCP.md 工具契约
config/validation_rules.json 声明式校验规则
templates/低值易耗品申购.xls 申购单空白模板(可用 REQUISITION_TEMPLATE 覆盖)
cases/ 会话产物(gitignore,含个人信息)
开发
make install # uv sync + pre-commit
make check # ruff check + format --check + mypy src + deptry
make test # pytest + 覆盖率
make upload # 上传收件器
make mcp-smoke # stdio 自检
测试覆盖:配置加载、发票分类与 CSV、金额匹配、会话模型、分析工具、骨架与规则一致性、申购单取值。
平台与风险
- Windows-only:申购单依赖 Microsoft Excel +
pywin32COM;并发驱动 Office 会互抢, 所以 COM 调用统一排队在单线程 actor(src/infra/com_thread.py) - 填报会打开 Chromium,自动化期间别手动点
cases/与scripts/data/含身份证号、卡号、凭据,已在.gitignore;发布前请自查历史- 提交前务必手动跑门禁:本仓库 pre-commit 只跑 ruff,但它会 stash 未暂存改动, 中途中断会把工作区文件回滚成 HEAD(本项目改造过程中真发生过,见方案文档 §11)
还没做完
填报已能分步做(browser_*,见 docs/MCP.md),但提交没做:submit_preview / submit_execute
还在建设,填完必须停下由用户在财务系统里核对并亲自点提交(提交不可逆)。
Word 版易耗品出库单已永久下线(模板自 2026-08-31 起不在仓库,此前该路径一直静默失败)。
文档索引
| 文档 | 内容 |
|---|---|
| docs/MCP.md | 25 个工具的参数、返回信封、会话目录、注册方法 |
| docs/报销操作指南.md | 财务侧口径与材料准备要求 |
| AGENTS.md | 给 Agent 的项目操作指南(开发命令、约束、坑) |
| .agents/docs/plans/MCP化改造方案-2026-10-19.md | 改造决策、实测数据、踩坑记录 |