Files
Auto-Finance/docs/报销操作指南.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

316 lines
15 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.
---
last_reviewed: 2026-10-19
---
# 阜阳师范大学财务报销系统 - 自动化操作指南
> 适用场景:日常报销录入。入口是 MCP 服务器,编排由宿主 Agent(如 DSH)完成;工具参数表见 [MCP.md](./MCP.md)。
> 本文讲财务侧口径、页面要素与材料要求。文中账号、卡号、姓名一律是占位示例——**真实值不要写进仓库任何位置**。
---
## 一、系统概览
```
信息门户(SSO登录) → 财务系统入口 → 单点登录页 → 网络报销 → 日常报销录入
(tyrz.fynu.edu.cn) (点击「财务系统」) (新标签页) (a:has(img)) (/expen/common/common)
```
| 系统 | 地址 | 说明 |
|------|------|------|
| SSO 登录 | `https://tyrz.fynu.edu.cn/sso/login` | 统一认证入口 |
| 信息门户 | `https://tyrz.fynu.edu.cn/oshall` | 登录后跳转目标 |
| 报销系统 | `http://<财务系统内网地址>:8081` | 网络报销主系统 |
| 日常报销录入 | `/expen/common/common?v=4.0` | 普通报销目标页 |
| 差旅报销 | `/expen/travel/travel?v=4.0` | 差旅报销目标页 |
取值优先级:**环境变量 > `scripts/data/config.json` > `src/config.py` 内置默认**。
环境变量名是 `SSO_LOGIN_URL` / `PORTAL_URL` / `REIMBURSE_URL` / `REIMBURSE_PAGE` / `TRAVEL_PAGE`
(见 `.env.example`;项目**不会**自动读 `.env` 文件,要自己 export 或写进宿主的 MCP env)。
配置文件里这几项**可以不写**;写了占位假值(含 `your-`)会被当成没配并打 WARNING,不会拿去登录。
---
## 二、使用方式
没有 Web 界面,也没有 CLI 产品入口。准备三步,之后全部在 Agent 对话里完成:
1. **注册 MCP 服务器**到宿主:`python -m src.mcp.server`(stdio),DSH 的 patch 写法见 `docs/MCP.md`
2. **配好** `scripts/data/config.json`(参考根目录 `config.example.json`)
3. **要手机拍照上传时**启动收件器:`make upload`(默认 `0.0.0.0:5055`)
自检:`make mcp-smoke` 会真起 stdio 子进程握手并列出 25 个工具。
---
## 三、材料准备
| 材料 | 必需 | 说明 |
|------|------|------|
| 发票 PDF / 图片 | 是 | 电子发票尽量给 PDF:有文本层,准确率明显高于读图 |
| 支付截图 | 是 | 公务卡刷卡记录;要能看清**商户**、日期、金额、卡号后四位 |
| 出差事前申请单 | 差旅必需 | 缺它财务不认,Agent 会主动索要 |
| 其他附件 | 视情况 | 会议通知、合同等 |
### 发票类型分流
| 类型 | 判断依据 | 产物 | 报销流程 |
|------|----------|------|----------|
| 差旅发票 | 高铁票、机票、酒店住宿等 | 不生成申购单 | 差旅报销(含按人按天补助) |
| 普通发票 | 其他(办公用品、耗材、元件等) | 低值易耗品**申购单** | 普通报销(一条总明细 + 逐笔支付) |
> Word 版「易耗品出库单」已下线:模板自 2026-08-31 起不在仓库,那条路径此前一直静默失败。
---
## 四、Agent 操作流程
| 步 | 做什么 | 用什么 |
|----|--------|--------|
| 1 | 建会话,拿到手机上传页地址 | `case_create` |
| 2 | 材料入库(本地路径用正斜杠;手机上传后用 `source="inbox"` 收取) | `case_add_files` |
| 3 | 渲染页面与文本层 | `case_render_pages` → `case_read_text` 或直接读 PNG |
| 4 | 逐张抽取落盘;核对后局部修正 | `case_save_extraction` / `case_patch_extraction` |
| 5 | 金额匹配,**逐条处理 `review_required`** | `match_payments` |
| 6 | 取骨架 → 填值 → 落盘校验(试算用 `validate_info` 不落盘) | `info_skeleton` → `save_info` |
| 7 | 普通发票生成申购单(`exclude` 剔除不参与申购的物品) | `doc_requisition` |
| 8 | 随时复查态势;要重跑就清派生产物 | `case_status` / `case_reset` |
| 9 | 填报(可选):开浏览器 → 登录 → 建单 → 按步填 → 复查 → 关 | `browser_open` → `browser_login` → `browser_new_form` → `browser_fill` → `browser_page_state` → `browser_close` |
### 填报这段要注意
- 浏览器默认**有头**:提交不可逆,得看得见在点什么;期间别手动点页面
- `browser_login` 返回 `need_human` 时,人在已经打开的那个窗口里扫码或输验证码,然后**重调 `browser_login`**
——不要另开浏览器。`cases/<id>/.browser.lock` 记着谁拥有它,返回 `owned_by_other_process` 就等对方释放
- `travel_info` 与 `normal_info` 同时存在时,`browser_new_form` 必须显式传 `kind`(混合发票只能走普通报销)
- 步骤不能跳:travel 是 basic/details/payment/subsidy/attachments,normal 没有 subsidy
- 每步截图按 `001-步骤名.png` 落在 `cases/<id>/screenshots/`;失败时截图路径在返回的 `detail.screenshot`
- **`browser_close` 不提交也不存草稿**,填了一半会丢;**提交永远由人做**——本项目不提供点提交的工具,
- 中途断了不必重来:重新开浏览器登录后用 `browser_open_form`(先 `preview_only=true` 核对**单据编号**,
命中多张会直接拒绝)从列表页找回原单续填;**别再 `browser_new_form`**,那会多建一张单
- 列表页那个「查看」按钮是看审批流程图,不是打开单据;要接着填是选中行后点「修改」
**`ok: true` 的含义有限**:`match_payments` 成功只代表贪心跑完。一对多凑额、发票总额小于支付总额、
未匹配项都会进 `review_required`,必须逐条确认——等额发票尤其容易配错。
**什么时候算完成**:`validate_info` 报告 `valid: true` 且无业务约束违反。缺料就向用户要材料;
Agent 不许自己宣布「可以提交了」。
### 产物位置
全部在 `cases/<case_id>/`(整个目录已 gitignore,含身份证号与卡号):
| 产物 | 说明 |
|------|------|
| `pages/<票据>/p01.png` | 渲染页面(已按文本方向自动转正) |
| `.invoice_cache/*.json` | 单张票据结构化数据 |
| `match_result.json` / `invoice_groups.json` | 匹配与分组结果 |
| `invoice_summary.csv` / `payment_records.csv` | 发票级与支付级汇总 |
| `travel_info.json` / `normal_info.json` | 填报所需的完整结构化数据 |
| `artifacts/低值易耗品申购_<case>.xls` | 申购单,拿去资产/财务口径核对 |
---
## 五、页面元素速查
### 基本信息页
| 字段 | 选择器 | 操作 |
|------|--------|------|
| 报销说明 | `#EXPENEXPLAIN` | fill |
| 项目代码 | `#PROJECTCODE` | click → 弹窗选择 |
| 项目弹窗 | `#promodal .fixed-table-body tbody tr` | 点击第一行 |
| 下一步按钮 | `#saveAndNext` | click |
### 报销明细页
| 字段 | 选择器 | 操作 |
|------|--------|------|
| 增加按钮 | `#insertDetail` | click |
| 经济事项代码 | `#economicscode2` | click → 弹窗选择 |
| 经济科目弹窗 | `#econmodal .fixed-table-body tbody tr` | 点击第 3 行 |
| 单据数 | `input[name="expenPwCommondetail.HOWBILLS"]` | fill |
| 报销总金额 | `#je_zwzcdz` | fill |
| 确定按钮 | `#detailAdd` | click |
### 支付方式页
| 字段 | 选择器 | 操作 |
|------|--------|------|
| 增加按钮 | `#insertPay` | click |
| 人员编号 | `#personid2` | fill |
| 人员姓名 | `#accountname2` | fill |
| 刷卡日期 | `#receiptdate2` | fill |
| 公务卡号 | `#localaccount2` | fill |
| 刷卡金额 | `#receiptmoney2` | fill |
| 实报金额 | `#money2` | fill |
| 商户 | `#merchant2` | fill |
| 备注 | `#smark2` | fill |
| 确定按钮 | `#payAdd` | click |
### 附件清单页
| 字段 | 选择器 | 操作 |
|------|--------|------|
| 增加按钮 | `#insertAcc` | click |
| 附件类型 | `#fjlx` | select_option → 1(发票) |
| 附件说明 | `#fpsmxx` | fill |
| 文件上传 | `#file` | set_input_files |
| 确定按钮 | `#cjtj` | click |
### 提交(只作页面知识保留,工具不提供这个动作)
下表是财务系统里提交相关控件的位置,写在这里是为了排查时看得懂页面;
**本项目的工具不会点它们**,提交一律由人来做。
| 操作 | 选择器 |
|------|--------|
| 提交按钮 | `#submit` |
| 提交按钮(备用) | `#submit2` |
---
## 六、数据流向
```
材料(PDF / 图片)
│ case_render_pages:页面 PNG + 文本层 + 自动转正
▼
宿主 Agent 读图与判断 ← 本项目不调用任何模型
│ case_save_extraction(schemas.py 规范化日期与金额)
▼
.invoice_cache/*.json
│ match_payments(分组 + 一对一/一对多贪心 + 业务约束)
▼
match_result.json / invoice_groups.json / 两份 CSV
│ info_skeleton → 填值 → save_info(声明式规则校验)
▼
travel_info.json 或 normal_info.json
│ doc_requisition(Excel COM,仅普通发票)
▼
artifacts/ 申购单
│ browser_open → browser_login → browser_new_form / browser_open_form → browser_fill
▼
财务系统报销单
```
---
## 七、关键设计说明
### 7.1 浏览器启动
Playwright `sync_playwright()` 启动 Chromium,**默认有头**(`headless=False`)——提交不可逆,得看清楚在点什么。
SSO 需要扫码或验证码时人工完成即可,流程会继续。每次运行都要重新登录。
### 7.2 明细录入策略
普通报销用「一条总明细」:所有发票合并成一条,报销总金额 = 各笔支付之和,单据数 = 发票张数;
支付方式逐笔录入,一笔一条。差旅按人去程/返程分条录交通费,补助按人按天算。
### 7.3 经济科目与项目(已知限制,待业务口径)
经济科目弹窗当前固定选第 3 行(`rows[2]`),项目弹窗固定选第 1 行,补助人员取查询结果第一行;
位置在 `src/infra/browser/travel.py` 与 `normal.py`。
**这是已知限制而不是正确策略**:科目选错财务会退单,而代码不知道该按什么规则选。
等给出业务口径(按发票品名?按项目性质?按经费用途?)后改成可配置加唯一匹配。
在此之前,填报完成务必人工核对科目与项目。
### 7.4 网络报销链接动态获取
单点登录页的「网络报销」链接参数每次不同,因此用 `a:has(img[src*="wlbx"])` 定位并读 `href`,不硬编码 URL。
### 7.5 手机上传
收件器是**独立进程**。`case_create` 直接返回这一单的**手机直链** `upload_url`(含令牌),点开即是上传页;
链接丢了用 `case_upload_link` 重取。PC 首页是备用入口(列出全部会话的二维码,容易分不清扫哪个)。
上传落 `cases/<id>/inbox/`,
再由 Agent 调 `case_add_files(source="inbox")` 收取。它只写不读(没有列表也没有下载路由)、令牌强制、
扩展名白名单、25MB/文件、200 文件/会话。旧 Web 的「PC 端轮询同步」已删除。
---
## 八、配置说明
`scripts/data/config.json`(已在 `.gitignore`,**永远不要提交**):
```json
{
"username": "<工号>",
"password": "<密码>",
"default_name": "<报销人姓名>",
"default_card_no": "<公务卡号>",
"default_person_id": "<人员编号>",
"upload_pin": "<手机上传访问码,可留空>"
}
```
| 字段 | 用途 |
|------|------|
| `username` / `password` | 信息门户登录 |
| `default_name` | 支付方式的「人员姓名」 |
| `default_person_id` | 支付方式的「人员编号」 |
| `default_card_no` | 支付方式的「公务卡号」 |
| `upload_pin` | 上传收件器访问码(等价 `--pin` 或环境变量 `UPLOAD_PIN`) |
URL 类字段(`sso_login_url`、`portal_url`、`reimburse_url`、`reimburse_page`、`travel_page`)有内置默认值,
需要才配;配了就生效(早先版本这里写了也不读,已修)。想临时改地址用环境变量更方便,不用动文件。
凭据不放环境变量:DSH 会清洗 stdio 子进程里含 KEY/PASSWORD/SECRET/TOKEN 的变量。
校验规则在 `config/validation_rules.json`;改规则要同步跑测试(骨架与规则有一致性用例)。
---
## 九、日志与排查
- 控制台 + `logs/<日期>.log`(UTF-8)。**没有 SSE 或任何长连接事件流**,进度靠 `case_status` 查
- 浏览器调试截图在 `images/`(已 gitignore);P3 之后过程截图进 `cases/<id>/screenshots/NNN-<step>.png`
- 超时:页面默认 30 秒、门户登录 30 秒、单点登录页 15 秒
- 排查顺序:`case_status` → `match_result.json` → 某张 `.invoice_cache/*.json` → `logs/<日期>.log`
---
## 十、常见问题
| 问题 | 原因 | 解决 |
|------|------|------|
| 登录超时 | SSO 要扫码或验证码 | 有头模式下人工完成,流程会继续 |
| 未找到财务系统入口 | 门户页面结构变了 | 看 `images/debug_*` 截图定位选择器 |
| 经济科目选择失败 | 弹窗加载延迟 | 增大等待,或调整点第几行的索引 |
| 金额不匹配 | 明细合计 ≠ 支付合计 | 复核 `match_result.json` 与 `review_required` |
| 附件上传失败 | PDF 路径不对 | 材料要在 `cases/<id>/` 下,路径用正斜杠 |
| 宿主看不到新增工具 | 旧 stdio 子进程还在跑旧代码 | 杀掉子进程,DSH 会自动重启并重同步工具 |
| 报「binding arguments must be lossless JSON」 | Windows 路径用了反斜杠 | 改成 `D:/Code/...` |
| 手机上传 403 | URL 缺令牌或令牌不对 | 用 `case_create` 返回的完整地址 |
| 抽取字段读错(编号/卡号/金额位数) | 文本层与读图本来就会冲突 | 放大该区域重看,两路都不许默认采信 |
| 申购单生成失败 | 未装 Excel 或 pywin32;或有弹窗未关 | 需要 Windows + Excel,人工关掉残留 Excel 窗口再试 |
| 浏览器起不来 | 没装 Chromium | `uv run playwright install chromium` |
| 返回 `owned_by_other_process` | 上一代 MCP 子进程还占着浏览器 | 让持有者 `browser_close`;心跳超 180 秒自动接管 |
| 登录一直不跳转 | SSO 要扫码或有验证码 | 看返回的 `need_human` 与截图,人工完成后重调 `browser_login` |
| 某一步 fill 失败 | 选择器没命中或页面慢 | 看返回 `detail.screenshot` 的截图,人工看一眼再重填该步 |
---
## 十一、相关命令
```bash
make check # ruff + format + mypy src + deptry
make test # pytest + 覆盖率
make mcp-smoke # MCP stdio 自检:真起子进程握手并列工具
make mcp-docs # 由线上 schema 重新生成 docs/MCP.md 的工具参考
make skill-install # 把报销技能装到用户级目录(跨项目可用)
make upload # 启动扫码上传收件器
# 不经 MCP 单独重生成申购单(调试用)
uv run python -m src.infra.documents.requisition --session cases/<case_id> --exclude 千斤顶
```
---
## 相关文档
- [docs/MCP.md](./MCP.md) — 25 个工具的参数与返回约定
- [.agents/skills/reimbursement/SKILL.md](../.agents/skills/reimbursement/SKILL.md) — 宿主 Agent 的操作纪律
- [AGENTS.md](../AGENTS.md) — 开发约束与踩坑记录