Files
Auto-Finance/.agents/skills/reimbursement/SKILL.md
T
wandering 5dda126e21 feat(upload): 建会话即给这一单的手机直链,收件器地址改为显式配置
用户选了 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 提交并手跑等价检查。
2026-10-21 10:05:33 +08:00

108 lines
8.5 KiB
Markdown
Raw 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.
---
name: reimbursement
description: >
财务报销自动化(阜阳师范大学):把发票、支付截图、出差申请单变成结构化报销数据,
生成低值易耗品申购单,并在财务系统填报到「待人工提交」。当用户提到报销、发票、差旅费、
公务卡刷卡记录、易耗品申购、财务系统填报时使用。通过 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 服务器:
```json
{ "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`),没启动时用户那条直链必然打不开;多网卡机器还要在服务器侧 `config.json` 配
`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 一律以工具返回为准。