用户选了 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 提交并手跑等价检查。
15 KiB
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 对话里完成:
- 注册 MCP 服务器到宿主:
python -m src.mcp.server(stdio),DSH 的 patch 写法见docs/MCP.md - 配好
scripts/data/config.json(参考根目录config.example.json) - 要手机拍照上传时启动收件器:
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,永远不要提交):
{
"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 的截图,人工看一眼再重填该步 |
十一、相关命令
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 — 25 个工具的参数与返回约定
- .agents/skills/reimbursement/SKILL.md — 宿主 Agent 的操作纪律
- AGENTS.md — 开发约束与踩坑记录