Files
Auto-Finance/docs/报销操作指南.md
T
wandering 94c2f4c94e feat(travel): 交通费行按实际乘车日填开始/结束日期(联机实测控件 #ksrq/#jsrq)
用户第 4 条:交通费行的日期由页面按出差起止自动带出,cost=1 分支不填日期,于是「6-02 出发、
6-05 才坐车」这种情况报销日期是错的。要按乘车日必须先知道控件 id,所以做了一次联机探测:
登录 → new_form(travel) → fill(basic) 落草稿 → actor 里打开明细、点 #insertDetail、选 cost=1、
dump 可见 input → 截图;探完按守卫删草稿(唯一命中+未提交+当日制单,bill_no 202610030261,复查 0 命中)。

探测结果:交通费行日期控件 #ksrq(expenPwTraveldetail.STARTDATE)/ #jsrq(OVERDATE),
页面已按出差起止预填(实测 2026-06-02 → 06-04)。

- add_travel_items 的 cost=1 分支用 transport_fee[].start_date/end_date 覆盖 #ksrq/#jsrq
- 缺日期时保留页面带出的值并 log.warning,不静默按出差起止报销
- tests/test_travel_dates.py 两例:有乘车日则覆盖、缺日期不覆盖
- 文档:AGENTS 记下控件名与口径、操作指南 7.2 补一句、方案文档 §25 记探测方法与结论(含可复用的探测套路)

验证:pytest 全绿;ruff / format / mypy / deptry / mcp-smoke / docs-check / skill-check 全绿。
--no-verify 提交并手跑等价检查。
2026-10-21 13:58:27 +08:00

17 KiB
Raw Blame History

last_reviewed
last_reviewed
2026-10-19

阜阳师范大学财务报销系统 - 自动化操作指南

适用场景:日常报销录入。入口是 MCP 服务器,编排由宿主 Agent(如 DSH)完成;工具参数表见 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
5b 等额发票配错时人工改组(整组唯一命中才生效,没生效的原因在 manual_gaps) match_payments(manual_groups=...)
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,两张单分别走差旅/日常列表页
  • 口径:因出差发生的会议费/会务费/培训费并入差旅单(明细「会务费培训费」栏,会议通知当附件), 不为此另开一张普通报销单;只有货物类(易耗品、材料)才走普通报销 + 申购单。判定写在 src/core/expense_kind.py,case_status 与 validate_info 的提示由它驱动
  • 明细页顶部的红字把「公务卡刷卡pos单」也列进了附件清单要求里:支付记录默认不当附件 (它们体现在「支付方式」页),要不要补传由你决定——要补就在附件清单页点「增加」手动传, 别让工具重跑 attachments 那一步(会重复上传已有的附件)
  • 步骤不能跳: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 不许自己宣布「可以提交了」。

  • 每步填报后工具会回读页面做落库后自检:数据行数与表尾合计跟 info 比对,差值写进 warnings, 返回里的 self_check 是原始读数。看到「自检不一致」先看图再决定重填,别当没看见。

产物位置

全部在 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 明细录入策略

普通报销用「一条总明细」:所有发票合并成一条,报销总金额 = 各笔支付之和,单据数 = 发票张数; 支付方式逐笔录入,一笔一条。差旅按人去程/返程分条录交通费,补助按人按天算。

交通费行的开始/结束日期按实际乘车日填(页面默认会按出差起止带出,但那不等于乘车日): 数据来自 transport_fee[].start_date/end_date,控件为 #ksrq/#jsrq(联机实测)。

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,永远不要提交):

{
    "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
手机只能拍照、选不了相册和已有文件 两代原因:capture 挂到了唯一 input;相册入口带 accept 被微信内置浏览器/
部分国产 ROM 直接路由到相机。都已修:相册入口不带 accept,类型交给服务端白名单
改了却像没生效 手机侧重开上传页;PC 侧开 /api/ping 看是不是旧进程在服务,重启收件器加载新代码
「选择已有文件」里没有相册页签 各手机给的文件选择器不同;照片在里面能翻到(多在 图片 / DCIM / Download 目录),或者直接用「拍照上传」
改了收件器页面却"没生效" 多半是旧的孤儿进程还在听 5055。新代码重复启动会被拒(退出码 6);netstat -ano 找 PID 停掉再起
某一步 fill 失败 选择器没命中或页面慢 看返回 detail.screenshot 的截图,人工看一眼再重填该步

十一、相关命令

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 千斤顶

相关文档