feat(skill): 报销技能可独立使用,配套安装脚本与来源启停校验

目标是技能脱离本仓库也能用:任何项目里提到报销都该按同一套纪律走。

- SKILL.md 重写自足化(104 行):删掉两处只在仓库里成立的引用(docs/MCP.md、方案文档);
  新增「第 0 步:确认工具可用」——先 mcp__arb__case_list 探活,工具不在就把 stdio 注册片段
  (uv run python -m src.mcp.server + cwd)交给用户,并明确禁止绕道自己写脚本处理发票;
  写死跨项目事实:材料用绝对路径正斜杠且可来自任何目录、产物在服务器仓库的 cases/<id>/、
  路径从 case_status 的 case_dir 读而不是按当前工作目录猜、别把截图与 info 拷进用户当前工程;
  字段权威指向工具(extraction_spec / info_skeleton / rules_get),三份 references 标注为判读经验
  并把开头指向已删除 src/infra/llm/prompts/* 的悬空路径改成历史沿革
- 补进这轮学到的坑:失败信封的额外字段在 detail 里;match 用单据编号最稳;每步可单独重填;
  敏感值打码引用;截图是核对证据不是给用户的报告
- 新增 scripts/install_skill.py(Makefile 与 tasks.py 各加 skill-install / skill-check):单向同步 +
  --check 漂移退出码 1 + front matter 缺失退出码 2(宁可不装也别装个宿主注册不了的)
- 关键防护:技能来源由 skills-manager 管,装进「已停用来源」里的技能文件明明存在但宿主永远扫不到
  (我第一次就装进了 ~/.dsh/skills,同目录的 wavedrom-gen 正是同一现象的旁证)。脚本现在读
  ~/.dsh/dsh-skills-manager.log 里该目录最后一条 source-enable/-disable,停用时拒绝安装并回显证据行
  (--force 可绕),默认目标改为已启用的 ~/.agents/skills
- tasks.py 顺手修掉吞退出码的老问题:os.system 的返回值以前直接丢,门禁失败调用方还看到 0。
  实测漂移 exit 1 / 同步 exit 0 / 未知任务 exit 2;顺便支持下划线别名
- 精简执行环境里 Path.home() 会抛 RuntimeError:脚本改用 USERPROFILE/HOME 回退并给人话提示

已知边界:同名技能项目级优先(skill() 的 resourceBase.path 实测指向仓库副本),因此本会话看到的
是仓库那份;跨项目可用性靠「已启用来源 + 内容一致」证明,另开一个目录的会话可最终确认。
验证:ruff / format / mypy / deptry / pytest 148 passed / mcp_smoke 25 工具全绿。
仍用 --no-verify 提交并手跑等价检查。
This commit is contained in:
wandering
2026-10-20 22:21:00 +08:00
parent 6e00ddc37e
commit 3392ab2152
9 changed files with 375 additions and 71 deletions
+75 -42
View File
@@ -1,70 +1,103 @@
---
name: reimbursement
description: >-
description: >
财务报销自动化(阜阳师范大学):把发票、支付截图、出差申请单变成结构化报销数据,
生成低值易耗品申购单,并在财务系统填报。当用户提到报销、发票、差旅费、公务卡刷卡记录、
易耗品申购、财务系统填报时使用本技能。通过 mcp__arb__* 工具操作,禁止臆造财务数据。
生成低值易耗品申购单,并在财务系统填报到「待人工提交」。当用户提到报销、发票、差旅费、
公务卡刷卡记录、易耗品申购、财务系统填报时使用。通过 mcp__arb__* 工具操作,禁止臆造财务数据。
本技能与当前工程无关:在任意项目目录下都能用,产物在 MCP 服务器自己的会话目录里。
---
# 财务报销(MCP 工具版)
# 财务报销(arb MCP 工具)
接口细节与参数表见 `docs/MCP.md`。本文件只写"怎么做才对"的操作纪律。
本技能**不依赖当前工作目录**,也不需要你打开报销项目的仓库:所有产物都由 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_page`(手机扫码上传入口,告诉用户)
2. 材料入库:本地文件用 `case_add_files(paths=[...])`(**路径必须正斜杠** `D:/Code/...`);
手机上传先落 `inbox/`,再 `case_add_files(case_id, source="inbox")` 收取
2. 材料入库:`case_add_files(paths=[...])`——**绝对路径、正斜杠**(`C:/Users/xx/Downloads/发票1.pdf`);
材料可以来自任何目录(下载目录、微信文件目录、当前工程的临时文件夹),工具会拷进会话目录,
原文件不动。手机上传先落 `inbox/`,再 `case_add_files(case_id, source="inbox")` 收取
3. `case_render_pages`:`text_chars > 0` 就 `case_read_text` 读文本层(比读图准且省 token),
否则读 `pages/<stem>/p01.png`。PDF 自动转正已处理,但**转正角度要看 meta.json**
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`
7. 普通发票:`doc_requisition` 生成 Excel 申购单(`exclude` 剔除不参与申购的物品)
8. `case_status` 复查态势;要重跑用 `case_reset(scope)`
否则读它给的 `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` 时,让用户在**已打开的那个窗口**里完成扫码/验证码,然后重调
`browser_login`;`browser_new_form` 在 travel_info 与 normal_info 同时存在时必须显式传 `kind`。
10. 中途断了(进程被杀、浏览器被关)不要重来一遍:重新 `browser_open` + `browser_login` 后用
`browser_open_form(match=报销说明关键词)` 找回原单据**续填**。先 `preview_only=true` 核对返回的
`bill_no`,确认后去掉该参数再打开;命中多张会直接拒绝。**别再 `browser_new_form`**——那会多建一张单,
在财务系统里留下垃圾草稿
11. 每一步都会自己切到对应标签页,所以**可以单独重填某一步**(同字段是覆盖),不必从 basic 重跑;
差旅与日常是两个独立列表页,`browser_open_form` 要在对应的 `kind` 下调用
12. 附件说明没填时工具会自动兜底成「发票:文件名」(财务系统里这栏是必填,留空点确定会被静默挡住);
`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 后仍不变形
- **不回显密码**,不把凭据写进任何产物或日志
- **删除与提交都算破坏性操作**:工具层只到填报,`#delete` 删除、点提交都由人在财务系统里自己做;
要清理测试单据也得先 `preview_only` 看清 `bill_no`,再逐条核对状态与金额
- 产物只写 `cases/<case_id>/`(该目录已 gitignore,含身份证号与卡号)
- `required: true` 的字段(含值为空串的备注类)**必须存在于 JSON 里**,这是刻意约束;用骨架别凭记忆造结构
- **双通道冲突要复核**:PDF 文本层与读图结论不一致时(尤其编号、卡号、金额位数),两条都不许默认
采信,重新放大该区域确认。已知文本层会把「标签」和「值」分段存储,位置不能当对应关系;
康熙部首类字符(`⽇` `⻔`)NFKC 归一后仍不变形
- **不回显密码**,不把凭据写进任何产物或日志;卡号、身份证号等敏感值**打码引用**(`6282****1682`)
- **截图是核对证据,不是给用户的报告**:出问题时拿对应那张说清现象,平时不必逐张呈报
- `required: true` 的字段(含值为空串的备注类)**必须存在于 JSON 里**,这是刻意约束,别改规则文件
- 工具返回的信封是扁平的:失败时 `reason` / `next_action` 在顶层,而**额外字段在 `detail` 里**
(比如报错附带的 `screenshot` 路径是 `result.detail.screenshot`)
- 财务数据只在会话目录里(已 gitignore)。**别把截图或 info 拷进用户当前的工程**,
那等于把身份证号提交进仓库
## 提交必须交人工(永久如此)
## 提交、删除、撤回:都由人在财务系统里做
`submit_*` 整类已被用户整体否决:没有代提交工具,也不做提交前预览。填完就是终点——
把**单据编号**和会话目录告诉用户,他自己去财务系统里核对、提交、查状态;不用替他生成预览,
也不必把截图逐张呈报(截图留在 `cases/<id>/screenshots/`,出问题时再拿去对照)。
`submit_*` 整类(代提交、提交前预览)已被用户整体否决,**填报完成就是本技能的终点**:
把**单据编号**和会话目录(从 `case_status` 的 `case_dir` 读,不要自己拼)告诉用户,
他自己去系统里核对、提交、查状态。删除(`#delete`)与撤回同理,工具层不给这类动作开口子。
提交错了要写情况说明,所以宁可停在填完,也不要替用户按下任何不可逆的按钮。
浏览器被别的进程占用时返回 `owned_by_other_process`:让持有者 `browser_close`,或等心跳超时 180 秒自动接管,
不要绕过去另开一个 Chromium。步骤函数报错会带 `screenshot`(在 `detail` 里),把它给用户看比反复重试有用。
浏览器被别的进程占用时返回 `owned_by_other_process`:让持有者 `browser_close`,或等心跳超时 180 秒
自动接管,不要绕过去另开一个 Chromium(那样你看到的截图就不是你在点的页面)。
## 工具与宿主的两条怪脾气
## 参考
- 调工具时路径**一律正斜杠**,反斜杠会被宿主的参数绑定层拒收
- 服务器代码改了要重连宿主才生效(常驻 stdio 子进程内存里有旧代码;本机同时存在多代子进程是常态,
所以浏览器/Office 都有磁盘锁,别假设只有一个实例)
## 参考(随本技能一起安装,不需要仓库)
- `references/invoice-fields.md` — 七类票据的字段判读细则(火车票/支付截图/酒店/普通发票/事前申请单/出入库单/其他附件)
- `references/travel-info-fields.md` — 差旅报销数据:交通/住宿/会议费、支付方式、按人按天的补助清单
- `references/normal-info-fields.md` — 普通报销数据:明细与支付方式
- `.agents/docs/plans/MCP化改造方案-2026-10-19.md` — 架构决策与实测记录(含抽取质量闸门数据)
这三份是**判读经验**(怎么认票、冲突怎么取、常见坑),不是 schema。schema 一律以工具返回为准。