Files
Auto-Finance/docs/MCP.md
T
wandering ceefe3c4eb feat(mcp): P3 联机验证通过,新增 browser_open_form 断点续填,修 4 个联机才暴露的真 bug
联机跑通 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 提交纪律)。
2026-10-20 10:44:57 +08:00

389 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
last_reviewed: 2026-10-19
---
# MCP 接口文档
本项目的**唯一业务入口**是 MCP 服务器:`python -m src.mcp.server`(stdio 传输),工具名在宿主里表现为 `mcp__arb__<tool>`。
没有 Web 界面,没有 CLI 产品入口,本项目也不调用任何大模型——**读图与综合判断由宿主 Agent 完成**,
本项目只提供确定性工具(渲染、金额匹配、规则校验、文档生成、浏览器填报)。
## 注册到宿主
DSH(profile patch,`~/.dsh/profiles/<profile>/cordis.patch.yml`):
```yaml
- insert:
- id: mcp-arb
name: '@deepseek-ai/dsh-mcp-client'
config:
serverName: arb
transport: stdio
command: 'D:/Code/AI Agent/自动报销系统/.venv/Scripts/python.exe'
args: ['-m', 'src.mcp.server']
cwd: 'D:/Code/AI Agent/自动报销系统'
```
> 注意:DSH 对 stdio 子进程做了环境变量清洗(抹掉 `*KEY|PASSWORD|SECRET|TOKEN` 与全部 `DSH_*`),
> 所以财务系统凭据只能来自 `scripts/data/config.json`,不能靠环境变量注入。
通用 MCP 客户端等价配置:
```json
{ "mcpServers": { "arb": { "command": "<项目根>/.venv/Scripts/python.exe", "args": ["-m", "src.mcp.server"], "cwd": "<项目根>" } } }
```
改完 Python 代码需要让宿主重连(DSH 会热插拔新工具条目,但**不会**重载已运行子进程里的代码;
杀掉 stdio 子进程即可自动重启并重同步工具)。
## 返回信封
所有工具返回统一信封,宿主判类型看 `ok`:
```json
{ "ok": true, "case_id": "...", "next_action": "..." }
{ "ok": false, "reason": "缺少 payment_records.csv", "next_action": "先 mcp__arb__match_payments 生成汇总" }
```
- **不抛异常穿透协议**:文件缺失、参数不合法、COM 失败都转成 `ok: false` + `reason` + `next_action`。
- `next_action` 是给 Agent 的下一步指令,通常是具体工具名,照做即可。
- 部分客户端会把结果包在 `content[0].text`(JSON 字符串)里,同时提供 `structuredContent`;两者内容一致。
## 会话目录
一个报销任务 = 一个 `cases/<case_id>/` 目录(`case_id` 形如 `20261019-6月毕业采集`)。
**阶段完全由产物推导**,没有状态机文件,可随时断电重来、可人工改文件后继续。
```
cases/<case_id>/
├── case.json 名称、类型、上传令牌(切勿提交)
├── config.json 可选,会话级凭据覆盖(白名单字段)
├── <材料>.pdf|jpg 发票、支付截图、申请单原件
├── inbox/ 手机扫码上传落地点,case_add_files(source="inbox") 收取
├── pages/<stem>/p01.png 渲染页面 + text.txt(PDF 文本层)+ meta.json(dpi/转正角/字符数)
├── .invoice_cache/*.json 单张票据抽取结果(宿主产出,本服务规范化后落盘)
├── match_result.json 匹配结果
├── invoice_groups.json 差旅/普通分组与计数
├── invoice_summary.csv 发票级汇总
├── payment_records.csv 支付记录汇总(申购单取数)
├── travel_applications.json 出差申请单
├── travel_info.json 差旅报销结构化数据
├── normal_info.json 普通报销结构化数据
├── artifacts/ 申购单等文档产物
├── screenshots/ (P3 落地后出现)填报过程截图
└── submit_preview.json (P4 落地后出现)提交前快照与摘要
```
`cases/` 整个目录都在 `.gitignore` 里:发票含身份证号与卡号。
## 标准流程
1. `case_create` → 记下 `case_id` 与返回的 `upload_page`(手机扫码上传页)
2. `case_add_files`(本地路径用**正斜杠**)或 `case_add_files(source="inbox")` 收手机上传
3. `case_render_pages` → `text_chars>0` 先 `case_read_text`,否则读 `pages/**.png`
4. 逐张按 `extraction_spec` 出 JSON → `case_save_extraction`;核对后小改用 `case_patch_extraction`
5. `match_payments` → **逐条处理 `review_required`**(未匹配、发票总额小于支付总额、一对多凑额)
6. `info_skeleton` 取骨架 → 填值 → `save_info`(自动跑校验);`validate_info` 可反复试算不落盘
7. 普通发票再 `doc_requisition` 生成申购单
8. 每步之后 `case_status` 复查;`case_reset(scope)` 清派生产物重跑
## 硬约束
- **不得回显密码**,不得把凭据写进产物或日志
- **不得臆造财务数据**:拿不准的字段标出来问用户,禁止自己宣布"可以提交"
- 产物只写 `cases/<case_id>/`
- 文本层与读图结论冲突时(尤其编号、卡号、金额位数)必须复核,不要默认信任某一路
## 尚未实现(不要调用,也不要在回答里承诺)
浏览器填报(`browser_*`)、提交预览与执行(`submit_*`)。需要这些步骤时告知用户该环节还在建设中,
可临时用旧的 Web 流程(代码在提交 `b2d271c` 里)。
---
# 工具参考
以下内容由运行中的服务器 schema 自动生成,与代码同源(`scripts/mcp_smoke.py` 打印同一份清单)。
## 会话与材料
每个报销任务是一个会话目录 cases/<case_id>/,阶段由产物推导。
### `mcp__arb__case_create`
新建报销会话目录,返回 case_id 与目录路径。
name 是本次报销的人类可读名称(如「6月毕业采集」);
invoice_type 可选 travel|normal,未定时留空,抽取后由发票分类决定。
| 参数 | 类型 | 必填 | 默认 | 说明 |
|------|------|------|------|------|
| `name` | string | 是 | — | — |
| `invoice_type` | null/string | 否 | — | — |
### `mcp__arb__case_list`
列出所有报销会话及其推导阶段、计数与待收文件数。
无参数。
### `mcp__arb__case_status`
单个会话的完整态势:材料数、抽取完成/缺失/陈旧、匹配、信息、产物、收件箱、下一步建议。
| 参数 | 类型 | 必填 | 默认 | 说明 |
|------|------|------|------|------|
| `case_id` | string | 是 | — | — |
### `mcp__arb__case_add_files`
把材料拷入会话目录。
paths 传绝对路径列表(从手机下载目录、微信文件目录等任意位置取文件);
source='inbox' 表示收取手机扫码上传落在 inbox 的文件。
| 参数 | 类型 | 必填 | 默认 | 说明 |
|------|------|------|------|------|
| `case_id` | string | 是 | — | — |
| `paths` | array/null | 否 | — | — |
| `source` | null/string | 否 | — | — |
### `mcp__arb__case_inbox`
列出手机/PC 上传进来但尚未进入流程的文件。
| 参数 | 类型 | 必填 | 默认 | 说明 |
|------|------|------|------|------|
| `case_id` | string | 是 | — | — |
### `mcp__arb__case_reset`
清理派生产物以便重跑。scope=extractions|match|info|pages|artifacts|all。
| 参数 | 类型 | 必填 | 默认 | 说明 |
|------|------|------|------|------|
| `case_id` | string | 是 | — | — |
| `scope` | string | 否 | "extractions" | — |
## 抽取交接
渲染页面与读文本层由宿主 Agent 自己判断,本服务不调用任何模型。
### `mcp__arb__case_render_pages`
为材料准备可读取的页面图片与 PDF 文本层。
返回每项材料的页 PNG 路径、文本层字符数与自动转正角度。
有文本层(text_chars > 0)时应优先读文本层,比读图更准且省 token。
| 参数 | 类型 | 必填 | 默认 | 说明 |
|------|------|------|------|------|
| `case_id` | string | 是 | — | — |
| `files` | array/null | 否 | — | — |
| `dpi` | integer | 否 | 150 | — |
| `force` | boolean | 否 | false | — |
### `mcp__arb__case_read_text`
读取某材料渲染时提取的 PDF 文本层;无文本层时返回空。
| 参数 | 类型 | 必填 | 默认 | 说明 |
|------|------|------|------|------|
| `case_id` | string | 是 | — | — |
| `file` | string | 是 | — | — |
### `mcp__arb__extraction_spec`
返回抽取字段定义。kind 省略给全部文档类型;'info' 给 travel_info/normal_info 骨架。
| 参数 | 类型 | 必填 | 默认 | 说明 |
|------|------|------|------|------|
| `kind` | null/string | 否 | — | — |
### `mcp__arb__case_save_extraction`
把单张票据的抽取结果写入会话缓存(按 <stem><suffix>.json 约定)。
日期会规范成 YYYY-MM-DD、金额规范成两位小数字符串;
返回 warnings 提示必填缺失,但不阻断写入(完整性由 arb_validate_info 判定)。
| 参数 | 类型 | 必填 | 默认 | 说明 |
|------|------|------|------|------|
| `case_id` | string | 是 | — | — |
| `source_file` | string | 是 | — | — |
| `data` | object | 是 | — | — |
### `mcp__arb__case_patch_extraction`
局部修正已落盘的抽取结果(人工核对后改字段用)。
只合并传入的键,返回每个被改字段的 from/to,避免静默覆盖;
未在 schema 中出现的键会告警但仍写入(有些字段是给下游自定义用的)。
| 参数 | 类型 | 必填 | 默认 | 说明 |
|------|------|------|------|------|
| `case_id` | string | 是 | — | — |
| `source_file` | string | 是 | — | — |
| `patch` | object | 是 | — | — |
## 匹配与校验
匹配与校验都是确定性代码;结论以 validate_info 为准,不要自己宣布可提交。
### `mcp__arb__match_payments`
按缓存做发票与支付记录的金额匹配,并写出 CSV 与 invoice_groups.json。
返回 matched / unmatched_invoices / unmatched_payments / review_required。
未匹配与一对多凑额项必须复核,不要盲信贪心结果。
| 参数 | 类型 | 必填 | 默认 | 说明 |
|------|------|------|------|------|
| `case_id` | string | 是 | — | — |
| `tolerance` | null/number | 否 | — | — |
### `mcp__arb__info_skeleton`
按校验规则生成「形状完整、值为空」的可填骨架。
规则里 required 为 true 的字段(含 remark 这类备注)要求键必须存在,
所以别凭记忆拼 JSON:先取骨架再逐个填值。骨架与 validation_rules.json 同源,不会漂移。
| 参数 | 类型 | 必填 | 默认 | 说明 |
|------|------|------|------|------|
| `info_type` | string | 是 | — | — |
### `mcp__arb__save_info`
写 travel_info 或 normal_info 到缓存,并立刻跑一次规则校验。
info_type 取 travel|normal。完整性以返回的 validation 为准,
不要自己在 data 里塞 can_submit。
| 参数 | 类型 | 必填 | 默认 | 说明 |
|------|------|------|------|------|
| `case_id` | string | 是 | — | — |
| `info_type` | string | 是 | — | — |
| `data` | object | 是 | — | — |
### `mcp__arb__validate_info`
校验报销信息完整性。省略 draft 时校验缓存里已存的 info。
也可传 draft 先试校验不落盘,适合改字段时反复迭代。
| 参数 | 类型 | 必填 | 默认 | 说明 |
|------|------|------|------|------|
| `case_id` | string | 是 | — | — |
| `info_type` | null/string | 否 | — | — |
| `draft` | null/object | 否 | — | — |
### `mcp__arb__rules_get`
返回声明式校验规则(config/validation_rules.json),供补字段前对照。
| 参数 | 类型 | 必填 | 默认 | 说明 |
|------|------|------|------|------|
| `info_type` | null/string | 否 | — | — |
## 文档产物
Excel COM,需本机装有 Microsoft Excel;仅普通发票需要。模板顺序:REQUISITION_TEMPLATE → templates/ → 项目根。
### `mcp__arb__doc_requisition`
生成低值易耗品申购单(Excel,仅普通发票)。
exclude 传物品名关键词可剔除不参与申购的发票;project 覆盖「对应实验实训项目」,
默认取 normal_info 的报销事由。需要 Windows + 已安装 Microsoft Excel。
| 参数 | 类型 | 必填 | 默认 | 说明 |
|------|------|------|------|------|
| `case_id` | string | 是 | — | — |
| `exclude` | array/null | 否 | — | — |
| `project` | null/string | 否 | — | — |
## 浏览器分步填报
需先 `uv run playwright install chromium`。默认**有头**;归属由 `cases/<id>/.browser.lock` 记录 (PID + 心跳,180 秒未心跳可被接管)。中途断了不必重来:用 `browser_open_form` 从列表页找回原单据 续填,**不要再 `browser_new_form` 重复建单**。提交不在这里,填完交人工核对。
### `mcp__arb__browser_open`
打开(或复用)该会话的浏览器。
默认有头:提交不可逆,要看得见在点什么。同一会话重复调用直接复用现有浏览器。
被别的进程占用时返回 owned_by_other_process,别绕过去另开一个浏览器。
| 参数 | 类型 | 必填 | 默认 | 说明 |
|------|------|------|------|------|
| `case_id` | string | 是 | — | — |
| `headless` | boolean | 否 | false | — |
### `mcp__arb__browser_login`
登录信息门户。遇到扫码或验证码不要死等:返回 need_human,人工完成后再次调用。
| 参数 | 类型 | 必填 | 默认 | 说明 |
|------|------|------|------|------|
| `case_id` | string | 是 | — | — |
| `wait_seconds` | integer | 否 | 25 | — |
### `mcp__arb__browser_new_form`
进入报销系统并新建单据。kind 省略时按会话里已有的 info 推断。
| 参数 | 类型 | 必填 | 默认 | 说明 |
|------|------|------|------|------|
| `case_id` | string | 是 | — | — |
| `kind` | null/string | 否 | — | — |
### `mcp__arb__browser_open_form`
从列表页找回已存在的单据接着填(中途断了不必重来)。
match 用报销说明里的关键词,必须在当前列表页唯一命中;命中多张直接失败,
让宿主把关键词收紧——财务系统里点错别人的单据是不能接受的后果。preview_only=true
只搜索定位、不点开单据,用来先确认是哪一张。
| 参数 | 类型 | 必填 | 默认 | 说明 |
|------|------|------|------|------|
| `case_id` | string | 是 | — | — |
| `match` | string | 是 | — | — |
| `kind` | null/string | 否 | — | — |
| `preview_only` | boolean | 否 | false | — |
### `mcp__arb__browser_fill`
执行一步填报。step 按 browser_new_form 返回的 steps 顺序,不能跳步。
| 参数 | 类型 | 必填 | 默认 | 说明 |
|------|------|------|------|------|
| `case_id` | string | 是 | — | — |
| `step` | string | 是 | — | — |
### `mcp__arb__browser_page_state`
看当前页面:URL、标题、命中的表单标记、已完成步骤、最近截图。
at_portal 只表示在信息门户;进了报销系统要用 in_reimburse_system 与 on_form_page 判断。
| 参数 | 类型 | 必填 | 默认 | 说明 |
|------|------|------|------|------|
| `case_id` | string | 是 | — | — |
| `screenshot` | boolean | 否 | true | — |
### `mcp__arb__browser_steps`
列出该会话单据类型的步骤顺序与含义(不要求浏览器已打开)。
| 参数 | 类型 | 必填 | 默认 | 说明 |
|------|------|------|------|------|
| `case_id` | string | 是 | — | — |
### `mcp__arb__browser_close`
关闭浏览器并释放锁。不会提交也不会存草稿,填了一半的内容会丢。
| 参数 | 类型 | 必填 | 默认 | 说明 |
|------|------|------|------|------|
| `case_id` | string | 是 | — | — |