填报从「一把梭」变成分步:Agent 可以开浏览器、登录、建单、按步填、看页面、关,每步有截图, 中途失败不必从头重来。提交不在本步——提交不可逆,留给 P4 的 submit_preview/submit_execute。 - src/infra/browser/actor.py(新):Playwright 单线程 actor(sync API 的 greenlet 线程亲和要求所有 调用回到创建线程)+ 会话注册表 + cases/<id>/.browser.lock(PID/started_at/heartbeat/url, 心跳 180 秒过期才允许接管;锁文件损坏按无锁处理;atexit 尽力关浏览器)。与 COM actor 分开。 - base.py:login_portal 拆成 submit_login + wait_for_portal(返回布尔不抛异常),新增 portal_reached/ human_required;_screenshot 支持 screenshot_dir 并按 001-步骤名.png 编号、返回路径、失败只告警 - src/mcp/tools/browser.py(新):browser_open/login/new_form/fill/steps/page_state/close,工具 17 → 24 · need_human 用 ok:true + logged_in:false 返回(扫码不是失败,避免宿主当错误重试或另开窗口) · 步骤顺序硬约束,跳步拒绝并用 next_action 指回该补的一步;步骤报错回传 detail.screenshot · browser_close 明确不提交不存草稿;travel 五步、normal 四步(无 subsidy) - tests/test_mcp_browser.py(新,17 例,FakeBot/FakePage 不起浏览器):锁四种情形、need_human 分支与 截图落点、重复登录幂等、未打开时各工具拒绝、类型推断与两 info 并存时拒猜、未登录拒建单、跳步拦截、 步骤推进与 remaining、未知步骤、报错截图、close 释放会话与锁、open 复用、steps 两种流程 - 文档:docs/MCP.md 工具段按 wire schema 重生成(24 工具);AGENTS/SKILL/操作指南/README/浏览器 README 同步分步流程、锁与心跳、有头默认、提交必须交人工;方案文档 §13 记录实现、取舍与未完成项 验证: - 联机跑通 open(3.1s 起有头 Chromium)→ 锁写入(pid/heartbeat)→ page_state(about:blank, 001-state.png 4721 字节)→ 重复 open 复用(already_open=true,不多开窗)→ close(锁释放、注册表清空) - 登录与填报的联机验证需要用户在场(SSO 可能扫码),本轮未做 - 门禁:ruff / format --check / mypy src / deptry / pytest 129 passed / mcp_smoke 24 工具 instructions 2109 字节。仍用 --no-verify 提交并手跑等价检查(原因见 AGENTS 提交纪律)。
14 KiB
last_reviewed
| 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):
- 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 客户端等价配置:
{ "mcpServers": { "arb": { "command": "<项目根>/.venv/Scripts/python.exe", "args": ["-m", "src.mcp.server"], "cwd": "<项目根>" } } }
改完 Python 代码需要让宿主重连(DSH 会热插拔新工具条目,但不会重载已运行子进程里的代码; 杀掉 stdio 子进程即可自动重启并重同步工具)。
返回信封
所有工具返回统一信封,宿主判类型看 ok:
{ "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 里:发票含身份证号与卡号。
标准流程
case_create→ 记下case_id与返回的upload_page(手机扫码上传页)case_add_files(本地路径用正斜杠)或case_add_files(source="inbox")收手机上传case_render_pages→text_chars>0先case_read_text,否则读pages/**.png- 逐张按
extraction_spec出 JSON →case_save_extraction;核对后小改用case_patch_extraction match_payments→ 逐条处理review_required(未匹配、发票总额小于支付总额、一对多凑额)info_skeleton取骨架 → 填值 →save_info(自动跑校验);validate_info可反复试算不落盘- 普通发票再
doc_requisition生成申购单 - 每步之后
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
把单张票据的抽取结果写入会话缓存(按 .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 秒未心跳可被接管)。提交不在这里:填完交人工核对。
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_fill
执行一步填报。step 按 browser_new_form 返回的 steps 顺序,不能跳步。
| 参数 | 类型 | 必填 | 默认 | 说明 |
|---|---|---|---|---|
case_id |
string | 是 | — | — |
step |
string | 是 | — | — |
mcp__arb__browser_steps
列出该会话单据类型的步骤顺序与含义(不要求浏览器已打开)。
| 参数 | 类型 | 必填 | 默认 | 说明 |
|---|---|---|---|---|
case_id |
string | 是 | — | — |
mcp__arb__browser_page_state
看当前页面:URL、标题、命中的表单标记、已完成步骤、最近截图。
| 参数 | 类型 | 必填 | 默认 | 说明 |
|---|---|---|---|---|
case_id |
string | 是 | — | — |
screenshot |
boolean | 否 | true |
— |
mcp__arb__browser_close
关闭浏览器并释放锁。不会提交也不会存草稿,填了一半的内容会丢。
| 参数 | 类型 | 必填 | 默认 | 说明 |
|---|---|---|---|---|
case_id |
string | 是 | — | — |