Files
Auto-Finance/.agents/skills/reimbursement/SKILL.md
T
wandering 787f259e38 fix(upload): 手机页拆成拍照与选文件两个入口,并堵住重复启动导致的孤儿进程
用户实测报回:手机上传只能拍照,选不了相册与已有文件,历史版本可以。根因在 src/upload/page.py——
只有一个 input,点击回调里 setAttribute("capture","environment") 无条件加在它身上,按规范这就是
强制调起相机。它是收件器诞生那次(14b6b4c)带进来的,旧 Web 页不受影响,所以用户的记忆没错。

- 拆两个 input 两个按钮:「拍照上传」带 capture;「从相册或文件选择」不带 capture、multiple、
  accept 含 .pdf。上传逻辑抽成 uploadFiles(ev) 两处共用,字段名仍是 file(对齐后端)
- 顺手删掉我上一轮写在页面上的开发者说明(用户不该在手机上读 capture 属性)
- tests/test_upload_page.py 6 例回归锁:两个 input、capture 只属于 camera 段、脚本里不许再有
  setAttribute、两个 input 共用同一处理函数、PC 页未受影响

更值得记的是第二件事:改完我重启收件器并验真实 HTML,返回的竟还是旧模板。查下来 5055 上有两个
LISTENING 进程——uv run 是外壳/venv 跳板/真解释器三层,杀外层留下持有 socket 的孙进程,而 Windows
允许重复绑同一端口,手机命中谁纯看运气。这就是 AGENTS「多进程共存」那条的现场版,只不过这次是收件器。

- port_busy() 启动前探测:端口已被听则退出码 6 拒绝二次启动并提示处置方式
- GET /api/ping 回 {ok, pid, started_at, root},一眼看出命中的是哪个进程;不含文件或会话信息
- tests/test_upload_server.py 4 例:ping 不泄露、port_busy 对监听与空闲端口的判定、真实渲染的两个入口
- 实测清理:批量停掉全部 upload.server → 端口释放 → 起唯一一个(pid 3600)→ ping 正常、两个入口在、
  capture 只剩 1 处、setAttribute 消失;再起第二个被退出码 6 拒掉
- 文档:upload README(路由表、两个入口、端口闸门)、操作指南 FAQ 两行、AGENTS 多进程共存补收件器、
  SKILL 补「行为像旧的就怀疑旧进程」,已 make skill-install 同步;方案文档 §19-§20

教训:**验证要打到真实服务上,别只跑单测**——单测 6 条全绿时线上还是旧页面。
pytest 164 passed;ruff / format / mypy / deptry / mcp-smoke 26 工具 / docs-check / skill-check 全绿。
--no-verify 提交并手跑等价检查。
2026-10-21 10:43:08 +08:00

8.5 KiB
Raw Blame History

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 子进程不会自己加载新代码)。

流程

  1. mcp__arb__case_create 建会话,记下 case_id,把返回的 upload_url 发给用户——那是这一单专用的 手机直链,点开即是上传页,不用在首页一堆二维码里挑。链接含会话令牌:只给这一次,别在后续 回答里重复它,也别写进任何文件;用户说丢了就用 case_upload_link(case_id) 重取一次
  2. 材料入库:case_add_files(paths=[...])——绝对路径、正斜杠(C:/Users/xx/Downloads/发票1.pdf); 材料可以来自任何目录(下载目录、微信文件目录、当前工程的临时文件夹),工具会拷进会话目录, 原文件不动。手机上传先落 inbox/,再 case_add_files(case_id, source="inbox") 收取。收件器是独立进程 (make upload),没启动时用户那条直链必然打不开;用户说"能打开但行为像旧的",多半是旧进程还在听 upload_base_url(带端口,默认 5055),否则探出来的地址手机连不上
  3. case_render_pages:text_chars > 0 就 case_read_text 读文本层(比读图准且省 token), 否则读它给的 pages/<stem>/p01.png。文本层与读图结论冲突时两条都不许采信(见下面硬纪律)
  4. 逐张出 JSON → case_save_extraction。字段定义调 extraction_spec,判读细则看 references/invoice-fields.md(本技能自带,不依赖仓库)
  5. match_payments:必须逐条处理 review_required(未匹配、发票总额小于支付总额、一对多凑额)。 贪心结果 ok 不等于「已核对」
  6. info_skeleton(info_type) 取骨架 → 填值 → save_info(自动跑规则校验)。拿不准的用 validate_info(draft=...) 不落盘反复试;规则原文 rules_get。差旅与普通可能并存,按 suggested_invoice_type 与明细性质决定,别只有一种 info 就以为报完了
  7. 普通发票:doc_requisition 生成 Excel 申购单(exclude 剔除不参与申购的物品); 纯差旅没有申购明细会返回失败提示,那不是错误
  8. case_status 复查态势(next_action 与 warnings 是最该看的两项);要重跑用 case_reset(scope)
  9. 填报:browser_open(默认有头)→ browser_login → browser_new_form → 按它返回的 steps 顺序逐个 browser_fill(step=...) → browser_page_state 复查 → browser_close(不提交也不存草稿)。 browser_login 返回 need_human 时让用户在已打开的那个窗口里完成扫码/验证码再重调; travel_info 与 normal_info 同时存在时 browser_new_form 必须显式传 kind
  10. 中途断了(进程被杀、浏览器被关)不要重来一遍:browser_open + browser_login 后用 browser_open_form(match=...) 找回原单据续填。先 preview_only=true 核对返回的 bill_no, 确认后去掉该参数再打开;命中多张会直接拒绝。别再 browser_new_form——那会多建一张单, 在财务系统里留下垃圾草稿。match 命中看的是列表页可见文字,长报销说明会被截断, 最稳的是单据编号
  11. 每步都会自己切标签页,所以可以单独重填某一步(同字段是覆盖),不必从 basic 重跑; 续填时前置步骤在财务系统里已完成,工具会给出警告而不是拦死——按页面实际状态判断
  12. 差旅与日常是两个独立列表页,browser_open_form 要在对应 kind 下调用;附件说明没填时 工具自动兜底成「发票:文件名」(财务系统里这栏必填,留空点确定会被静默挡住), 要写清楚就把 attachment_desc 填进 attachments 项

必须遵守

  • 完整性不由你宣布:以 validate_info / save_info 返回的校验报告为准;不完整就向用户要材料, 禁止说「可以提交了」
  • 不得臆造财务数据:金额、日期、发票号、卡号读不清就标出来问用户,不要猜
  • 双通道冲突要复核:PDF 文本层与读图结论不一致时(尤其编号、卡号、金额位数),两条都不许默认 采信,重新放大该区域确认。已知文本层会把「标签」和「值」分段存储,位置不能当对应关系; 康熙部首类字符(⽇ ⻔)NFKC 归一后仍不变形
  • 不回显密码,不把凭据写进任何产物或日志;卡号、身份证号等敏感值打码引用(6282****1682)
  • 截图是核对证据,不是给用户的报告:出问题时拿对应那张说清现象,平时不必逐张呈报
  • required: true 的字段(含值为空串的备注类)必须存在于 JSON 里,这是刻意约束,别改规则文件
  • 工具返回的信封是扁平的:失败时 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 一律以工具返回为准。