5dda126e21
用户选了 B 方案。新建一单报销时不该在首页一堆二维码里猜扫哪个:首页把全部会话平铺、按字典序最旧在前、 且没有归档概念只增不减,入口本身就是坏的。现在 case_create 直接返回 upload_url。 - src/config.py 新增可选字段 upload_base_url(环境变量 UPLOAD_BASE_URL;留空才退回探测) - src/cases.py:upload_page_url() 优先用配置;新增 CaseStore.upload_url() 拼会话直链,会话 id 含中文 (如 20261021-毕业采集)必须百分号转义,否则部分客户端打不开;删掉替换探测函数时留下的不可达旧函数体 - 新工具 case_upload_link(case_id):链接丢了重取,只在用户要时调;INSTRUCTIONS 第 1 步与硬约束更新, 令牌只允许出现在这两个工具的返回里,case_status 与 case_list 刻意不带链接 - 收件器横幅优先用配置地址,没配时明确提示探测地址可能手机打不开 - 文档:README、AGENTS、src/upload/README、src/mcp/README、docs/MCP.md 标准流程、操作指南 7.5、 scripts/data/README、config.example.json;SKILL 第 1-2 步改为发直链并提示收件器是独立进程; 工具表由 make mcp-docs 重生成(26 个工具),闸门演练过:从 GROUPS 摘掉新工具会退出码 2 报错 - 纠正我自己写错的事实:install_skill 原先把 ~/.agents/skills 写死成本机启用,而来源开关会变 (本机今早 08:11 就翻回 ~/.dsh/skills)。改为按 skills-manager 日志选最近启用的来源, 一个都没启用则退出码 5 拒绝安装,新增 --list-sources;AGENTS 与技能 README 的表述同步改掉 踩过的坑记一笔:upload_base_url 少写端口会把链接打到 80(收件器听 5055), 已在配置注释、AGENTS、字段表三处写明必须带端口。 测试:tests/test_upload_link.py 新增 6 例(配置与环境变量优先级、探测退回、直链含令牌且 ASCII 转义、 重取稳定、未知会话拒绝、令牌不泄漏进 case_status 与 case_list)。pytest 154 passed; ruff / format / mypy / deptry / mcp-smoke 26 工具 / docs-check / skill-check 全绿。 端到端实测:拿生成的直链访问真实运行中的收件器返回 200 且页面是上传页。 仍用 --no-verify 提交并手跑等价检查。
415 lines
17 KiB
Markdown
415 lines
17 KiB
Markdown
---
|
||
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|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。
|
||
未匹配与一对多凑额项必须复核,不要盲信贪心结果。
|
||
|
||
| 参数 | 类型 | 必填 | 默认 | 说明 |
|
||
|------|------|------|------|------|
|
||
| `case_id` | string | 是 | — | — |
|
||
| `tolerance` | number/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。
|
||
|
||
| 参数 | 类型 | 必填 | 默认 | 说明 |
|
||
|------|------|------|------|------|
|
||
| `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。
|
||
|
||
| 参数 | 类型 | 必填 | 默认 | 说明 |
|
||
|------|------|------|------|------|
|
||
| `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 顺序,不能跳步。
|
||
|
||
| 参数 | 类型 | 必填 | 默认 | 说明 |
|
||
|------|------|------|------|------|
|
||
| `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 | 是 | — | — |
|