ceefe3c4eb
联机跑通 open→login→new_form→fill(basic)→page_state→close:财务系统真的落库(单据编号 202610020850,
basic 页点「下一步」即保存)。据此补上续填能力并把测试草稿清理干净(删除前后截图 + 复查 0 命中)。
- 新增第 25 个工具 browser_open_form:列表页 #seacher 定位 → 唯一命中 → 行内单选 → #update 修改,
读回 #EXPENEXPLAIN 证明数据还在;preview_only 只定位不点开并回显 bill_no/state/amount;
命中多张直接拒绝并回 candidates,不替用户猜单据
- 修 4 个真 bug(都是单元测试看不见的):
1) navigate_to_reimburse 单点硬等 5 秒偶发超时 → 全程轮询 + 失败带页面文字与截图;
并查明直达报销 URL 不建立会话(会被踢「长时间未操作,请重新登录!」),门户点击链是必经
2) actor.close_all 用 atexit 注册无效:concurrent.futures.thread 的钩子走 threading._register_atexit,
比 atexit 早跑,executor 已 shutdown → close 提交不进去。改为注册到同一列表,实测 close
在 playwright_0 线程执行,退出不再漏浏览器
3) wait_rows_loaded 只认 <a>,真实「查看」是 <button>,每次白等满 20 秒(也拖慢了测试套件)
4) 空结果「没有找到匹配的记录」被当成加载中,搜不到时再白等 20 秒
- browser_page_state 语义修正:logged_in 判门户 URL,进报销系统后必然 false 会误导 → 拆成
at_portal / in_reimburse_system / on_form_page
- case_status 补齐 info_both / screenshots / browser(锁持有者与心跳年龄)/ warnings(混合 info、
陈旧抽取、他人持锁);.browser.lock 文件名由 cases.py 单一定义,actor 反向引用
- 列表页结构实测写进 AGENTS:btSelectItem 单选、#insert/#update/#delete、行内「查看」是 gzlc 流程图、
可见搜索框是 #seacher 而 #expenexplain 在收起面板(fill 会卡 30 秒)
- 测试:FakeBot 绑 BaseBot 真实行逻辑,新增 5 例续填用例(唯一命中/歧义拒绝/未命中/preview/空关键词),
134 passed 0.36s;文档 docs/MCP.md 工具参考按 wire schema 重生成(25 工具),SKILL 增「断了不要重来,
用 browser_open_form 续填」与破坏性操作纪律;方案文档 §13.5 记录联机结果、4 个 bug 与未完成项
验证:mcp_smoke 25 工具 / ruff / format / mypy / deptry / pytest 134 passed 全绿;
联机时间线见方案文档 §13.5。仍用 --no-verify 提交并手跑等价检查(原因见 AGENTS 提交纪律)。
131 lines
6.0 KiB
Markdown
131 lines
6.0 KiB
Markdown
---
|
||
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** |
|
||
|
||
## 快速开始
|
||
|
||
```bash
|
||
uv sync # 依赖(要求 Python >= 3.12)
|
||
uv run playwright install chromium # 填报要用(P3 工具接入后需要)
|
||
```
|
||
|
||
配好 `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` 会返回带令牌的手机页地址,上传落 `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/MCP.md)。操作步骤(财务口径):[docs/报销操作指南.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,含个人信息)
|
||
```
|
||
|
||
## 开发
|
||
|
||
```bash
|
||
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 + `pywin32` COM;并发驱动 Office 会互抢,
|
||
所以 COM 调用统一排队在单线程 actor(`src/infra/com_thread.py`)
|
||
- 填报会打开 Chromium,自动化期间别手动点
|
||
- `cases/` 与 `scripts/data/` 含身份证号、卡号、凭据,已在 `.gitignore`;发布前请自查历史
|
||
- **提交前务必手动跑门禁**:本仓库 pre-commit 只跑 ruff,但它会 stash 未暂存改动,
|
||
中途中断会把工作区文件回滚成 HEAD(本项目改造过程中真发生过,见方案文档 §11)
|
||
|
||
## 还没做完
|
||
|
||
填报已能分步做(`browser_*`,见 [docs/MCP.md](docs/MCP.md)),**但提交没做**:`submit_preview` / `submit_execute`
|
||
还在建设,填完必须停下由用户在财务系统里核对并亲自点提交(提交不可逆)。
|
||
Word 版易耗品出库单已永久下线(模板自 2026-08-31 起不在仓库,此前该路径一直静默失败)。
|
||
|
||
## 文档索引
|
||
|
||
| 文档 | 内容 |
|
||
|------|------|
|
||
| [docs/MCP.md](docs/MCP.md) | 25 个工具的参数、返回信封、会话目录、注册方法 |
|
||
| [docs/报销操作指南.md](docs/报销操作指南.md) | 财务侧口径与材料准备要求 |
|
||
| [AGENTS.md](AGENTS.md) | 给 Agent 的项目操作指南(开发命令、约束、坑) |
|
||
| [.agents/docs/plans/MCP化改造方案-2026-10-19.md](.agents/docs/plans/MCP化改造方案-2026-10-19.md) | 改造决策、实测数据、踩坑记录 |
|