--- last_reviewed: 2026-10-20 --- # 财务报销自动化(MCP 服务器) 把发票、支付截图、出差申请单变成财务系统要的结构化数据,顺带生成低值易耗品申购单。 **它不是一个应用,而是一个 MCP 服务器**:读图、判断、决定下一步交给宿主 Agent(如 DSH), 本项目只做确定性的活——PDF 渲染转正、金额匹配、规则校验、Excel 产物、浏览器填报。 > 为什么改:原来自带一整套前端 + 内置 LLM + 手写状态机(提取-校验-修正循环、轮次上限、SSE 事件流)。 > 灵活性太差——想换个模型、加一类票据、改流程都得动代码。2026-10-19 起全部交给宿主 Agent 编排。 ## 它做什么 | 环节 | 谁做 | |------|------| | 看发票、判断类型、抽字段 | **宿主 Agent**(多模态模型) | | 页面渲染、PDF 文本层、自动转正 | 本项目(PyMuPDF) | | 发票与刷卡记录金额匹配 | 本项目(确定性贪心 + 业务约束,输出待复核清单) | | 完整性校验 | 本项目(`config/validation_rules.json` 声明式规则) | | 申购单 Excel、财务系统填报 | 本项目(COM / Playwright) | | 流程编排、缺料追问、要不要提交 | **宿主 Agent** | ## 快速开始 ```bash uv sync # 依赖(要求 Python >= 3.12) uv run playwright install chromium # 浏览器填报要用 ``` 配好 `scripts/data/config.json`(参考 `config.example.json`:工号、密码、默认报销人、公务卡号、人员编号、上传 PIN)。 该目录整体在 `.gitignore` 里,别提交。 把服务器注册到宿主(DSH profile patch): ```yaml - 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 握手): ```bash make mcp-smoke # 或 python tasks.py mcp-smoke ``` 手机拍照上传材料(独立进程,必须单独起): ```bash make upload # 默认 0.0.0.0:5055;--local 只监听 127.0.0.1;--pin 加访问码 ``` `case_create` 返回的 **`upload_url` 就是这一单的手机直链**(含会话令牌),点开即是上传页,不用在首页 一堆二维码里挑;链接只在那一次给出,别复制转发。收件器没跑时链接打不开。上传落 `cases//inbox/`, 之后由 Agent 用 `case_add_files(source="inbox")` 收取。 上传服务是只写不读的:没有列表也没有下载接口,扩展名白名单 + 25MB + 每会话 200 个文件封顶。 多网卡机器上要在 `config.json` 写 `upload_base_url`(**必须带端口**,收件器默认听 5055), 否则地址是探测出来的,手机多半打不开。 ## 一次报销长什么样 ``` 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 ``` 提交动作**不在工具能力范围内,也永远不会是**:本项目不提供点提交的工具,填完由本人在财务系统里核对并亲自提交。 26 个工具的参数表与返回约定:[docs/MCP.md](docs/MCP.md)。操作步骤(财务口径):[docs/报销操作指南.md](docs/报销操作指南.md)。 ## 项目结构 ``` src/ ├── mcp/ 工具与协议层(server.py 注册;tools/ 分材料·抽取交接·分析·文档) ├── cases.py 会话目录 cases// 的唯一实现:阶段由产物推导,无状态机 ├── 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,含个人信息) ``` ## 开发 ```bash make install # uv sync + pre-commit make check # ruff check + format --check + mypy src + deptry make test # pytest + 覆盖率(148 例,不需要网络与真实账号) make mcp-smoke # stdio 自检:真起子进程握手、列工具 make mcp-docs # 由线上 schema 重新生成 docs/MCP.md 的工具参考 make docs-check # 工具文档漂移检测(退出码 1 即不一致) make skill-install # 把报销技能装到用户级目录,跨项目可用 make upload # 上传收件器(手机拍照上传,独立进程) ``` 测试覆盖:配置加载、发票分类与 CSV、金额匹配、会话与分析工具、骨架与规则一致性、申购单取值, 以及分步填报的步骤顺序、续填找回、附件提交校验(用假页面跑真实逻辑,不碰 Playwright 与账号)。 ## 平台与风险 - **Windows-only**:申购单依赖 Microsoft Excel + `pywin32` COM;并发驱动 Office 会互抢, 所以 COM 调用统一排队在单线程 actor(`src/infra/com_thread.py`) - 填报会打开 Chromium,自动化期间别手动点 - `cases/` 与 `scripts/data/` 含身份证号、卡号、凭据,已在 `.gitignore`;发布前请自查历史 - **提交前务必手动跑门禁**:本仓库 pre-commit 只跑 ruff,但它会 stash 未暂存改动, 中途中断会把工作区文件回滚成 HEAD(本项目改造过程中真发生过,见方案文档 §11) ## 边界与未完事项 **刻意不做**:提交、提交前预览(`submit_*` 整类)、删除、撤回——都由本人在财务系统里做, 工具层不给这些动作开口子(错一条要写情况说明,不可逆的动作不交给自动化)。填报完成就是终点, 把单据编号与会话目录告诉用户即可。 **已知未完**(截至 2026-10-20): - 经济科目固定取弹窗第 3 行、项目固定取第 1 行、补助人员取查询结果第一行:会静默填出财务上 不正确的数据,等用户给业务口径后改成可配置 + 唯一匹配 - Word 版易耗品出库单永久下线(模板自 2026-08-31 起不在仓库,此前该路径一直静默失败) ## 文档索引 | 文档 | 内容 | |------|------| | [docs/MCP.md](docs/MCP.md) | 26 个工具的参数、返回信封、会话目录、注册方法 | | [docs/报销操作指南.md](docs/报销操作指南.md) | 财务侧口径与材料准备要求 | | [AGENTS.md](AGENTS.md) | 给 Agent 的项目操作指南(开发命令、约束、坑) | | [.agents/skills/reimbursement/SKILL.md](.agents/skills/reimbursement/SKILL.md) | 宿主 Agent 的操作纪律(可装到用户级跨项目用) | | [.agents/docs/plans/MCP化改造方案-2026-10-19.md](.agents/docs/plans/MCP化改造方案-2026-10-19.md) | 改造决策、实测数据、踩坑记录、各项定案 |