Files
wandering 5903dae864 feat(match): match_payments 支持 manual_groups 人工改组
用户第 3 条:等额发票被贪心配错时只能靠 remark 记一笔,下游 CSV / invoice_groups.json 仍是错的。

- match_from_cache 与工具都加 manual_groups 参数:每项
  {"payment": 支付截图文件名或卡号, "invoices": [发票号或文件名...], "note": "..."}
- 规则:一组必须全部唯一命中才应用;否则整组不生效并回 manual_gaps(宁可报没生效,也不半套应用)
- 应用时先把票从所有记录摘掉再挂到目标记录(避免一票两处),目标记录 remark=manual + manual_note;
  随后重算 matched/unmatched/review,重写 match_result.json、payment_records.csv、invoice_summary.csv、
  invoice_groups.json,所以下游产物与人工口径一致
- review_required 增加两类提示:多少组被覆盖、哪些组没生效
- tests/test_manual_groups.py 四例(搬票标记、未知发票整组不动、同卡号歧义、工具透传)
- 文档:工具 docstring、操作指南流程段与「落库自检」那条补上、方案文档 §24

边界:manual_groups 只改发票归属,不改金额;金额错要先 case_patch_extraction 修抽取。
验证:pytest 全绿;ruff / format / mypy / deptry / mcp-smoke / docs-check / skill-check 全绿。--no-verify 提交。
2026-10-21 13:54:33 +08:00

436 lines
18 KiB
Markdown
Raw Permalink 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 落地后出现)填报过程截图
```
`cases/` 整个目录都在 `.gitignore` 里:发票含身份证号与卡号。
## 标准流程
1. `case_create` → 记下 `case_id`,把返回的 **`upload_url` 发给用户**(这一单的手机直链,点开即上传)
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` 复查(`next_action` 与 `warnings` 最该看);`case_reset(scope)` 清派生产物重跑
9. 填报:`browser_open` → `browser_login`(返回 `need_human` 就请用户在**已打开的窗口**里完成再重调)→
`browser_new_form`(断线改用 `browser_open_form` 按**单据编号**找回原单续填,别重开新单)→
按返回的 `steps` 逐个 `browser_fill` → `browser_page_state` 复查 → `browser_close`
10. 到此为止:把单据编号与会话目录告诉用户,**提交由他本人在财务系统里做**
## 硬约束
- **不得回显密码**,不得把凭据写进产物或日志
- **不得臆造财务数据**:拿不准的字段标出来问用户,禁止自己宣布"可以提交"
- 产物只写 `cases/<case_id>/`
- 文本层与读图结论冲突时(尤其编号、卡号、金额位数)必须复核,不要默认信任某一路
## 提交:不做,永久不做
**提交由人类在财务系统里亲自点。** 本项目不提供、也不计划提供 `submit_execute` 这类代提交工具
(用户明确否决)。填报完成就是 Agent 的终点:把**单据编号**和会话目录(`case_status` 的 `case_dir`)告诉用户,
由他本人到财务系统里核对、提交、查状态。删除(`#delete`)、撤回同理,都在页面上手工做。
`submit_*` 整类(代提交、提交前预览 `submit_preview`)也一并不做:用户自己到财务系统里核对、提交、
查状态。Agent 到 `browser_fill` / `browser_page_state` 为止,把单据编号与会话目录告诉用户就够了;
截图留在 `cases/<id>/screenshots/` 供排查,不必逐张呈报。
同样已下线的:Word 版易耗品出库单(模板自 2026-08 起不在仓库,历史上长期静默失败)。
---
# 工具参考
<!-- 本节由 scripts/gen_mcp_docs.py 从运行时 schema 生成,别手改;改完工具跑 make mcp-docs -->
共 26 个工具,在宿主里都带 `mcp__arb__` 前缀。
## 会话与材料
每个报销任务是一个会话目录 cases/<case_id>/,阶段由产物推导。
### `mcp__arb__case_create`
新建报销会话目录,返回 case_id 与目录路径。
name 是本次报销的人类可读名称(如「6月毕业采集」);
invoice_type 可选 travel|normal,未定时留空,抽取后由发票分类决定。
| 参数 | 类型 | 必填 | 默认 | 说明 |
|------|------|------|------|------|
| `name` | string | 是 | — | — |
| `invoice_type` | string/null | 否 | null | — |
### `mcp__arb__case_upload_link`
重新取该会话的手机上传直链(用户把上一条弄丢了,或换了设备时用)。
链接含令牌:只在用户明确要链接时调用,别在常规流程里反复调,也别把令牌抄进别处。
| 参数 | 类型 | 必填 | 默认 | 说明 |
|------|------|------|------|------|
| `case_id` | 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 | 否 | null | — |
| `source` | string/null | 否 | null | — |
### `mcp__arb__case_inbox`
列出手机/PC 上传进来但尚未进入流程的文件。
| 参数 | 类型 | 必填 | 默认 | 说明 |
|------|------|------|------|------|
| `case_id` | string | 是 | — | — |
### `mcp__arb__case_reset`
清理派生产物以便重跑。scope=extractions|match|info|pages|artifacts|screenshots|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 | 否 | 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` | string/null | 否 | null | — |
### `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 | 是 | — | — |
## 匹配与校验
金额匹配是确定性的;完整性的裁判是规则文件,不是宿主。
### `mcp__arb__match_payments`
按缓存做发票与支付记录的金额匹配,并写出 CSV 与 invoice_groups.json。
返回 matched / unmatched_invoices / unmatched_payments / review_required。
未匹配与一对多凑额项必须复核,不要盲信贪心结果。
等额发票被贪心配错时,用 manual_groups 人工改组(会重写 match_result.json / CSV / invoice_groups.json):
每项 {"payment": 支付截图文件名或卡号, "invoices": [发票号或文件名, ...], "note": "为什么这样配"}。
**一组必须全部唯一命中才应用**,否则整组不生效并回在 manual_gaps 里;应用过的在 manual_applied,
对应记录的 remark 会变成 manual。别在半套命中的情况下改账。
| 参数 | 类型 | 必填 | 默认 | 说明 |
|------|------|------|------|------|
| `case_id` | string | 是 | — | — |
| `tolerance` | number/null | 否 | null | — |
| `manual_groups` | array/null | 否 | null | — |
### `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。
规则里带 max_len_bytes 的是**文本宽度**上限(汉字算 2、字母数字算 1):
备注写太长,财务后端不回"太长了"而是一记 500,页面上什么都看不到,
所以在这里就拦住,missing_fields 会写成
"payment_methods[0].remark|备注过长:宽度 171 > 上限 100(约 50 个汉字),请改短"。
| 参数 | 类型 | 必填 | 默认 | 说明 |
|------|------|------|------|------|
| `case_id` | string | 是 | — | — |
| `info_type` | string | 是 | — | — |
| `data` | object | 是 | — | — |
### `mcp__arb__validate_info`
校验报销信息完整性。省略 draft 时校验缓存里已存的 info。
也可传 draft 先试校验不落盘,适合改字段时反复迭代。
| 参数 | 类型 | 必填 | 默认 | 说明 |
|------|------|------|------|------|
| `case_id` | string | 是 | — | — |
| `info_type` | string/null | 否 | null | — |
| `draft` | object/null | 否 | null | — |
### `mcp__arb__rules_get`
返回声明式校验规则(config/validation_rules.json),供补字段前对照。
| 参数 | 类型 | 必填 | 默认 | 说明 |
|------|------|------|------|------|
| `info_type` | string/null | 否 | null | — |
## 文档产物
需要 Windows + Excel(COM)。Word 出库单已下线,只剩申购单。
### `mcp__arb__doc_requisition`
生成低值易耗品申购单(Excel,仅普通发票)。
exclude 传物品名关键词可剔除不参与申购的发票;project 覆盖「对应实验实训项目」,
默认取 normal_info 的报销事由。需要 Windows + 已安装 Microsoft Excel。
会议费/会务费/培训费这类**因出差发生**的普通发票会自动跳过(并入差旅单的会务费培训费栏),
返回里的 skipped_conference 会列出被跳过的条目——别把它们当货物列进申购单。
| 参数 | 类型 | 必填 | 默认 | 说明 |
|------|------|------|------|------|
| `case_id` | string | 是 | — | — |
| `exclude` | array/null | 否 | null | — |
| `project` | string/null | 否 | null | — |
## 浏览器分步填报
常驻会话加分步执行;提交由人在财务系统里做,工具不含提交动作。断线用 browser_open_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` | string/null | 否 | null | — |
### `mcp__arb__browser_open_form`
从列表页找回已存在的单据接着填(中途断了不必重来)。
match 必须在当前列表页唯一命中;命中多张直接失败,让宿主把关键词收紧——财务系统里
点错别人的单据是不能接受的后果。注意命中判据是**列表页可见文字**,长报销说明会被
CSS 截断(`元器件采购(P3 全 ...`),所以最稳的 match 是**单据编号**(不会被截断)。
preview_only=true 只搜索定位、不点开单据,用来先确认是哪一张。
| 参数 | 类型 | 必填 | 默认 | 说明 |
|------|------|------|------|------|
| `case_id` | string | 是 | — | — |
| `match` | string | 是 | — | — |
| `kind` | string/null | 否 | null | — |
| `preview_only` | boolean | 否 | false | — |
### `mcp__arb__browser_fill`
执行一步填报。step 按 browser_new_form 返回的 steps 顺序,不能跳步。
提交前会量这一步要 POST 的自由文本宽度(汉字算 2、字母数字算 1,上限 100,
普通报销说明 40):财务后端对超宽只回 500、页面上任何提示都没有,弹窗停在原地
看着像"按钮没点着",所以宁可在这里拦下来,失败时 too_long 会列出超长字段。
每步 POST 完会做一次**落库后自检**:回读页面上的数据行数与表尾合计,跟 info 的期望值比对,
不一致就放进 warnings(返回里的 self_check 是原始读数);读不到表格时也会明确说「本步没有核对过」。
| 参数 | 类型 | 必填 | 默认 | 说明 |
|------|------|------|------|------|
| `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 | 是 | — | — |