这一批是 2026-10-21 正式填报踩出来的两类问题,一起收进来。 **一、报销口径只在一处判定(新增 src/core/expense_kind.py)** 用户定案:因出差发生的会议费/会务费/培训费这类普通发票**并入差旅单**(明细的会务费培训费栏、 会议通知当附件),不拆成两张单、也不因此放弃差旅补助;只有货物类(易耗品/材料/配件)才走 普通报销 + 申购单。以前 case_status、validate_info、browser_new_form 三处都写"混合发票只能走 普通报销",等于把这条合法路径说成错的,Agent 照做就会白拆一张单还漏补助。现在 is_conference_like / goods_invoices 是唯一判定,工具提示与 case_status 的 warnings 都由它驱动。 **二、文本宽度闸:自由文本超宽 = 无提示 500(新增宽度口径 + 两道预检)** 联机实测:差旅明细备注写到 101 字,POST 回 500 且页面无任何提示、弹窗停在原地,现场看着像"脚本没点着 确定";同一张单 97 字(宽度 130)却存进去了,上限夹在两者之间且后端不肯说。于是取汉字/全角记 2、 字母数字记 1 的"宽度",validator.TEXT_WIDTH_LIMIT=100(≈50 汉字,同时满足"100 字符"与"131 字节" 两种可能口径),普通报销说明另用 DESCRIPTION_WIDTH_LIMIT=40。拦截点两处:规则里的 max_len_bytes (validate_extracted_info 的 missing_fields 会写明"过长:宽度 N > 上限 M")与 browser_fill 在 POST 前的 _text_limit_problems(失败返回 too_long 列出超长字段)。 **三、交通工具代码表挪进 core(新增 src/core/vehicles.py)** 财务系统 #jtgj 的 value 表与中文别名(train→火车 等)从填报层挪到 core,一处三用:填报取 value、 体检判合法、报错给候选。老写法 if vehicle in vehicle_map 遇英文枚举是**静默不选**且仍返回成功, 直到提交被必填挡回来——现在认不出的值直接报错。 其余:cases.goods_general_items 给 case_status 补"有 N 张货物类普通发票"的 warning; travel.py 改用 vehicles 并显式报错;config/validation_rules.json 加 max_len_bytes; INSTRUCTIONS/SKILL/判读参考/操作指南同步口径与宽度提示。新增 tests/test_text_limits.py 与 tests/test_vehicles_and_scope.py。 验证:ruff / format / mypy src / deptry / pytest / mcp-smoke / docs-check / skill-check 全绿。 --no-verify 提交并手跑等价检查。
12 KiB
name, description
| name | description |
|---|---|
| reimbursement | 财务报销自动化(阜阳师范大学):把发票、支付截图、出差申请单变成结构化报销数据, 生成低值易耗品申购单,并在财务系统填报到「待人工提交」。当用户提到报销、发票、差旅费、 公务卡刷卡记录、易耗品申购、财务系统填报时使用。通过 mcp__arb__* 工具操作,禁止臆造财务数据。 本技能与当前工程无关:在任意项目目录下都能用,产物在 MCP 服务器自己的会话目录里。 |
财务报销(arb MCP 工具)
本技能不依赖当前工作目录,也不需要你打开报销项目的仓库:所有产物都由 arb MCP 服务器写到
它自己的会话目录 cases/<case_id>/。字段与规则的唯一权威来源是工具本身
(extraction_spec / info_skeleton / rules_get)——别凭记忆造结构,更别去找某个文件里的字段表。
第 0 步:确认工具可用
-
先调
mcp__arb__case_list(无参数)。返回会话列表就说明服务器已接入,直接进流程。 -
工具不存在或报 unknown tool:arb 服务器没注册。这时不要绕道自己写脚本处理发票, 把下面的配置给用户,让他在宿主(如 DSH)的 MCP 配置里加一条 stdio 服务器:
{ "arb": { "command": "uv", "args": ["run", "python", "-m", "src.mcp.server"], "cwd": "D:/Code/AI Agent/自动报销系统" } }前提:那台机器上有该仓库(Windows-only,申购单要 Excel COM),且
uv sync与uv run playwright install chromium装好;手机扫码上传还要单独起uv run python -m src.upload.server。仓库路径以用户实际位置为准,上面的路径只是本机现状。 -
加了配置要重连宿主才生效(stdio 子进程不会自己加载新代码)。
流程
mcp__arb__case_create建会话,记下case_id,把返回的upload_url发给用户——那是这一单专用的 手机直链,点开即是上传页,不用在首页一堆二维码里挑。链接含会话令牌:只给这一次,别在后续 回答里重复它,也别写进任何文件;用户说丢了就用case_upload_link(case_id)重取一次- 材料入库:
case_add_files(paths=[...])——绝对路径、正斜杠(C:/Users/xx/Downloads/发票1.pdf); 材料可以来自任何目录(下载目录、微信文件目录、当前工程的临时文件夹),工具会拷进会话目录, 原文件不动。手机上传先落inbox/,再case_add_files(case_id, source="inbox")收取。收件器是独立进程 (make upload),没启动时用户那条直链必然打不开;用户说"能打开但行为像旧的",多半是旧进程还在听 5055——GET /api/ping回的是 pid 与启动时间,改完src/upload/的代码必须重启那个进程才生效。 重启要杀真正持有 socket 的那个 PID(Get-NetTCPConnection -LocalPort 5055查得到),别只杀uv run外壳:Windows 允许两个进程绑同一端口,两代收件器并存时手机命中谁全凭运气。 多网卡机器还要在scripts/data/config.json配upload_base_url(带端口,默认 5055), 否则探出来的地址手机连不上。发链接之前先自检:端口有没有人听、/api/ping回的是哪个进程, 再用Get-NetIPAddress确认链接里那个 IP 还在本机网卡上——别用ipconfig, 第二个 WLAN 适配器它会漏(2026-10-21 就是这么差点把链接发给错网段的手机) case_render_pages:text_chars > 0就case_read_text读文本层(比读图准且省 token), 否则读它给的pages/<stem>/p01.png。文本层与读图结论冲突时两条都不许采信(见下面硬纪律)- 逐张出 JSON →
case_save_extraction。字段定义调extraction_spec,判读细则看references/invoice-fields.md(本技能自带,不依赖仓库) match_payments:必须逐条处理review_required(未匹配、发票总额小于支付总额、一对多凑额)。 贪心结果ok不等于「已核对」:它只按金额凑数,等额的发票最容易配错。实单翻过一次车—— 同一次出差两张 338.00 的车票被分到两笔支付上,凑出「453.50 → 338.00+117.50=455.50」这种 多两块钱的组合,还把 115.50 那张留成未匹配。复核要看付款时间与行程方向(先付的款配先乘的程), 不是看金额对不对得上;reimbursement_details与payment_methods两边总额相等 也不等于逐笔配对正确。工具没有人工作改组的入口(只有tolerance), 所以把人工归组的结论写进对应 payment 的remark,下游和人看得懂info_skeleton(info_type)取骨架 → 填值 →save_info(自动跑规则校验)。拿不准的用validate_info(draft=...)不落盘反复试;规则原文rules_get。- 报销口径(用户 2026-10-21 定案):因出差发生的会议费/会务费/培训费并入差旅单
(明细「会务费培训费」栏 + 会议通知当附件),不为它另开普通报销单,也不因此放弃补助;
只有货物类(易耗品/材料/配件)才另走
normal_info+ 申购单。suggested_invoice_type看见 general 发票就报normal,这时以口径为准、不以它为准 - 住宿发票一般不印入住/退房日期:按会议报到日至返程日推实际晚数(4 天行程 3 晚),
别照搬参考文档
days=1的保守默认——那会把 333 元/晚报成 1000 元/晚,财务必打回 - 中转到站的多张车票合并成「去程一条、返程一条」,
bill_count记实际张数、金额相加; 明细的日期列由页面按出差起止带出,填报脚本不填它
- 报销口径(用户 2026-10-21 定案):因出差发生的会议费/会务费/培训费并入差旅单
(明细「会务费培训费」栏 + 会议通知当附件),不为它另开普通报销单,也不因此放弃补助;
只有货物类(易耗品/材料/配件)才另走
- 普通发票:
doc_requisition生成 Excel 申购单(exclude剔除不参与申购的物品); 纯差旅没有申购明细会返回失败提示,那不是错误 case_status复查态势(next_action与warnings是最该看的两项);要重跑用case_reset(scope)- 填报:
browser_open(默认有头)→browser_login→browser_new_form→ 按它返回的steps顺序逐个browser_fill(step=...)→browser_page_state复查。browser_login返回need_human时让用户在 已打开的那个窗口里完成扫码/验证码再重调;travel_info 与 normal_info 同时存在时browser_new_form必须显式传kind。browser_close留到用户提交之后再叫: 填完那会儿单据还不存在(没有编号、列表页查不到),窗口一关就是全丢,不是"存了草稿"。 收尾话术固定两句:「还没提交,别关这个窗口」+「提交后把编号发我,我用 browser_open_form 核对」 - 中途断了(进程被杀、浏览器被关)不要重来一遍:
browser_open+browser_login后用browser_open_form(match=...)找回原单据续填。先preview_only=true核对返回的bill_no, 确认后去掉该参数再打开;命中多张会直接拒绝。别再browser_new_form——那会多建一张单, 在财务系统里留下垃圾草稿。match命中看的是列表页可见文字,长报销说明会被截断, 最稳的是单据编号 - 每步都会自己切标签页,所以可以单独重填某一步(同字段是覆盖),不必从 basic 重跑;
续填时前置步骤在财务系统里已完成,工具会给出警告而不是拦死——按页面实际状态判断。
唯独
attachments不能重跑:它逐个弹窗上传、没有去重,重跑一遍附件就翻倍; 要补附件让人在附件清单页点「增加」手动传(会话目录里的原始文件路径给他)。 还有:差旅单几个标签共用一个页面,「基本信息」那步的截图经常仍拍在明细表格上, 别拿它当基本信息的证据;要确认就让用户目视,或看browser_page_state的 markers - 差旅与日常是两个独立列表页,
browser_open_form要在对应kind下调用;附件说明没填时 工具自动兜底成「发票:文件名」(财务系统里这栏必填,留空点确定会被静默挡住), 要写清楚就把attachment_desc填进 attachments 项
必须遵守
- 完整性不由你宣布:以
validate_info/save_info返回的校验报告为准;不完整就向用户要材料, 禁止说「可以提交了」 - 不得臆造财务数据:金额、日期、发票号、卡号读不清就标出来问用户,不要猜
- 口径不自己发明:因出差发生的会议费/会务费/培训费并入差旅单(不为它拆单、不放弃补助),
货物类普通发票才走普通报销 + 申购单;判定表在
src/core/expense_kind.py,工具提示由它驱动,suggested_invoice_type只是参考。住宿实际晚数、中转车票的合并方式同理——按证据推, 拿不准就问用户一句,别照默认值交卷 - 双通道冲突要复核:PDF 文本层与读图结论不一致时(尤其编号、卡号、金额位数),两条都不许默认
采信,重新放大该区域确认。已知文本层会把「标签」和「值」分段存储,位置不能当对应关系;
康熙部首类字符(
⽇⻔)NFKC 归一后仍不变形 - 不回显密码,不把凭据写进任何产物或日志;卡号、身份证号等敏感值打码引用(
6282****1682) - 截图是核对证据,不是给用户的报告:出问题时拿对应那张说清现象,平时不必逐张呈报
required: true的字段(含值为空串的备注类)必须存在于 JSON 里,这是刻意约束,别改规则文件- 备注/说明要写短:上限按「宽度」算——汉字与全角标点记 2、字母数字记 1,
remark/merchant/attachment_desc≤ 100(约 50 个汉字),普通报销说明 ≤ 40。 财务后端对超宽的答复是500 Internal Server Error,页面上一个字提示都没有,弹窗停在原地, 现场看着像「脚本没点着确定」(2026-10-21 实测:97 字/宽度 130 存进去了,101 字/宽度 171 被拒)。save_info体检与browser_fill(返回too_long)两道闸都会拦,但最好一开始就别把整串发票号 塞进备注——发票本身就是附件,备注只写「谁、哪段、几张票」 - 工具返回的信封是扁平的:失败时
reason/next_action在顶层,而额外字段在detail里 (比如报错附带的screenshot路径是result.detail.screenshot) - 财务数据只在会话目录里(已 gitignore)。别把截图或 info 拷进用户当前的工程, 那等于把身份证号提交进仓库
提交、删除、撤回:都由人在财务系统里做
submit_* 整类(代提交、提交前预览)已被用户整体否决,填报完成就是本技能的终点:
把单据编号和会话目录(从 case_status 的 case_dir 读,不要自己拼)告诉用户,
他自己去系统里核对、提交、查状态。删除(#delete)与撤回同理,工具层不给这类动作开口子。
提交错了要写情况说明,所以宁可停在填完,也不要替用户按下任何不可逆的按钮。
浏览器被别的进程占用时返回 owned_by_other_process:让持有者 browser_close,或等心跳超时 180 秒
自动接管,不要绕过去另开一个 Chromium(那样你看到的截图就不是你在点的页面)。
工具与宿主的两条怪脾气
- 调工具时路径一律正斜杠,反斜杠会被宿主的参数绑定层拒收
- 服务器代码改了要重连宿主才生效(常驻 stdio 子进程内存里有旧代码;本机同时存在多代子进程是常态, 所以浏览器/Office 都有磁盘锁,别假设只有一个实例)
参考(随本技能一起安装,不需要仓库)
references/invoice-fields.md— 七类票据的字段判读细则(火车票/支付截图/酒店/普通发票/事前申请单/出入库单/其他附件)references/travel-info-fields.md— 差旅报销数据:交通/住宿/会议费、支付方式、按人按天的补助清单references/normal-info-fields.md— 普通报销数据:明细与支付方式
这三份是判读经验(怎么认票、冲突怎么取、常见坑),不是 schema。schema 一律以工具返回为准。