Files
Auto-Finance/AGENTS.md
T
wandering 821822a408 docs: 全面体检并更新文档,工具参考改为从运行时 schema 生成
先体检再动手:44 个 Markdown,相对链接 0 处失效,逐份分辨「现状文档」与「历史记录」。
历史记录(error-experience/*、架构分析、已过时的全景图)保留原貌只加指路,不重写。

- 新增 scripts/gen_mcp_docs.py:起 stdio 子进程 list_tools 后整段重生成 docs/MCP.md 的「工具参考」
  (291 行 / 25 个工具,占这份文档四分之三,手工维护必漂)。--check 漂移退出码 1,--diff 打印差异;
  GROUPS 是刻意闸门——加了工具没登记就报错,防止「有工具没文档」。接进 make mcp-docs / docs-check
  与 tasks.py 同名任务
- 生成时抓到两处真实漂移:invoice_type 线上是 string/null 且默认 null,文档手写成 null/string 无默认;
  case_list 无参数在旧文档里写成一句散文。现已与线上一字不差
- 过期说法清理:src/README 的「browser_* 与 submit_* 尚未实现」(browser 早已联机验通、submit 定案不做);
  tests/README 的「当前 107 例」(现 148)与缺 test_mcp_browser.py 一行、「Playwright 填报不进单测」
  (现在选择器分支/状态判定/错误路径有单测,只有真实页面需要联机);.agents/README 的「Cursor Agent」;
  plans/README 里还写着「提交加确认 token」;scripts/data/README 表格有一行跑到正文后面
- 工程实践指南里把已删除的 CLI 入口当例子:mypy src/main.py、make run、python src/main.py 全部改为现状
  (mypy src / 唯一入口 python -m src.mcp.server),并补上「任务必须透传退出码」这条本轮踩到的坑
- README:「还没做完」重写为「边界与未完事项」——把刻意不做(提交、submit_* 整类、删除、撤回)与
  真的未完(科目/项目/人员固定取第 N 行,待业务口径)分开写;开发命令补 mcp-docs / docs-check / skill-install
- 操作指南:7.3 经济科目与项目从「要改就改索引」改成「已知限制,等口径,填报后务必人工核对」;
  相关命令补齐新目标;MCP.md 标准流程补上填报与「到此为止,提交由人」两步,并改掉与新纪律矛盾的
  「把截图报给用户」表述
- AGENTS:命令表加两行;测试段给出改工具后的固定顺序(mcp-smoke → mcp-docs → 重连宿主);提交纪律
  补「make check 之外还有 docs-check / skill-check 两道专项门禁」;截图产物改标注为排查证据
- install_skill.py:拿不到主目录时退出码改 4,别和「漂移」的 1 混成一类
- 已核对:submit_execute / src/main.py 在文档里的残留都是禁止性表述或「已删除」清单项,不是现状描述

验证:ruff / format --check / mypy src / deptry / pytest 148 passed / mcp-smoke 25 工具 /
docs-check 与 skill-check 均退出码 0。--no-verify 提交并手跑等价检查。
2026-10-20 23:12:46 +08:00

12 KiB
Raw Blame History

description, alwaysApply
description alwaysApply
true

last_reviewed: 2026-10-20

AGENTS — 项目操作指南

本文件为 Agent 提供高信号量的项目操作知识,避免重复探索。

文档边界

  • 禁止使用表情文字输出任何内容。
  • docs/ 目录存放面向开源用户、外部贡献者的公开文档。
  • .agents/ 目录存放维护规范、实施方案、经验总结等内部资料。
  • 报销业务的操作纪律在 .agents/skills/reimbursement/SKILL.md;工具契约在 docs/MCP.md。

开发命令(必须使用 uv)

项目用 uv 管理依赖,版本锁在 uv.lock。

操作 Makefile (跨平台) tasks.py (Windows)
安装依赖 + pre-commit make install python tasks.py install
代码检查(lint+format+typecheck+deptry) make check python tasks.py check
运行测试(含覆盖率报告) make test python tasks.py test
MCP stdio 自检(起子进程握手列工具) make mcp-smoke python tasks.py mcp-smoke
由线上 schema 重新生成 docs/MCP.md 工具参考 make mcp-docs python tasks.py mcp-docs
检查工具文档是否与代码同源(漂移退出码 1) make docs-check python tasks.py docs-check
启动上传收件器 make upload python tasks.py upload
把报销技能装到用户级目录(脱离本仓库也能用) make skill-install python tasks.py skill-install
检查安装副本与仓库是否漂移 make skill-check python tasks.py skill-check
打印 MCP 注册参数 make mcp python tasks.py mcp
清理缓存和虚拟环境 make clean python tasks.py clean

没有 CLI 产品入口,也没有 Web 界面:业务入口只有 python -m src.mcp.server(stdio), 工具在宿主里表现为 mcp__arb__<tool>。

注意: tasks.py 的 check 用 && 连接(PowerShell 不支持 &&,但 tasks.py 内部是单行字符串,没问题)。

代码质量工具链(执行顺序)

  1. Ruff lint — uv run ruff check . (select: E, F, W, I, N, UP, B; ignore: E501)
  2. Ruff format — uv run ruff format --check . (line-length: 120)
  3. MyPy strict — uv run python -m mypy src(整个 src 都在 strict 下,不只是某个入口文件)
  4. deptry — uv run deptry .(检测未声明、未使用、过时依赖)

uv run mypy 在本机偶尔报 "uv trampoline failed to canonicalize script path",用 uv run python -m mypy 代替。

提交纪律(血的教训)

  • 门禁退出码要单独确认再提交,别把"跑检查"和"提交"写在同一条命令链里——检查失败也能走完流程。
  • 本仓库 pre-commit 只挂 ruff,但它会 stash 未暂存改动再跑,跑完才还原。 若这个过程被中断(超时/杀进程),未暂存的文件会被回滚成 HEAD 版本且 git fsck 捞不回来。 本项目改造过程中真发生过一次,四个文件的改动靠锚点重放才恢复。
  • 因此:提交前先把工作区改动 git add 干净,或用 --no-verify 提交并手动跑完等价检查(在提交说明里写明)。
  • 改过 MCP 工具或技能,make check 之外还差两道专项门禁:make docs-check(工具文档同源)与 make skill-check(技能安装副本一致)——它们都靠退出码报漂移,别只看有没有打印错误

项目架构(MCP 服务器模式)

唯一业务入口:python -m src.mcp.server(stdio)。编排(读图判断、缺料追问、是否提交)由宿主 Agent 完成, 本项目不调用任何大模型,只提供确定性工具与内核。

  • src/mcp/ — 工具与协议层:server.py 注册与启动,tools/ 按 materials / extraction / analysis / documents / browser 分模块, schemas.py 抽取字段定义(单一真源),envelope.py 统一 {ok, ...} / {ok:false, reason, next_action} 信封
  • src/cases.py — 会话目录 cases/<case_id>/ 的唯一实现:材料、收件箱、产物与阶段推导(阶段由产物推导,不存在状态机文件)
  • src/core/ — 确定性内核:extraction/(缓存读写、PDF 渲染与文本层、自动转正)、matching/(金额匹配 + runner.py 从缓存匹配)、validation/(声明式规则校验)
  • src/infra/ — 基础设施:browser/(Playwright 填报 + actor.py 单线程持有者与磁盘锁)、documents/(CSV、申购单 Excel COM)、com_thread.py(COM 单线程串行 actor)
  • src/upload/ — 上传收件器(独立进程 python -m src.upload.server):手机扫码/PC 上传落 cases/<id>/inbox/,不含业务编排,绝不由 MCP 子进程拉起

已删除:src/agent/(手写状态机与 SSE)、src/web/(Flask 界面)、src/main.py+pipeline*.py(CLI/双管道)、 core/extraction/{extractor,llm_extractor}.py(内置 LLM 与事件日志)、infra/llm/(提示词迁到 .agents/skills/reimbursement/references/)、 infra/documents/consumable.py(Word 出库单下线)、依赖 llama-index* 与 python-dotenv。

宿主侧两条硬约定

  • 调 MCP 工具路径一律正斜杠(D:/Code/...),反斜杠会被 DSH 的参数绑定层拒收。
  • required: true 的字段(含值为空串的备注类)必须存在于 info JSON 中——这是刻意约束,别改规则文件; 用 mcp__arb__info_skeleton 取骨架再填值,别凭记忆造结构。

数据流关键产物

文件 生成者 作用
pages/<stem>/p01.png + text.txt + meta.json case_render_pages 给宿主读的页面与 PDF 文本层(含转正角度)
.invoice_cache/<stem><ext>.json 宿主抽取 → case_save_extraction 规范化落盘 单张票据结构化数据(小改用 case_patch_extraction)
match_result.json / invoice_groups.json match_payments 支付与发票的关联、差旅/普通分组
invoice_summary.csv / payment_records.csv match_payments 发票级与支付级汇总(申购单取数)
travel_info.json / normal_info.json 宿主综合 → save_info 差旅/普通报销的全部结构化数据
artifacts/*.xls doc_requisition 低值易耗品申购单
screenshots/NNN-<步骤>.png browser_* 分步填报 排查证据(Agent 自己回看用,不必逐张给用户)
.browser.lock browser_open 谁拥有该会话的浏览器(PID + 心跳,180 秒可接管)

缓存机制

会话缓存在 cases/<case_id>/.invoice_cache/,文件名与源文件同名(发票1.pdf → .invoice_cache/发票1.pdf.json)。 缓存 JSON 的 _source_file + 内容指纹决定"是否陈旧",case_status 会报 stale 项;删缓存即重抽。

重要约束

  • Windows-only:申购单依赖 Microsoft Excel + COM(pywin32)。所有 Office COM 调用必须走 src/infra/com_thread.py 的单线程 actor——并发驱动 Word/Excel 会互抢 ActiveDocument 与模态框; 申购单空白模板在 templates/,可用环境变量 REQUISITION_TEMPLATE 指向别处,产物只写会话目录 artifacts/
  • 浏览器自动化:Playwright,会打开 Chromium(默认有头),期间不要手动干扰。Playwright sync API 有 greenlet 线程亲和,所有调用必须走 src/infra/browser/actor.py 的单线程 actor(与 COM actor 分开,套间要求不同); 浏览器归属用 cases/<id>/.browser.lock(PID+心跳)声明。返回 owned_by_other_process 时不要另开窗口, 那会让 Agent 看到的截图和实际操作的页面不是同一个
  • 财务系统列表页结构(联机实测,别再猜):行选择是 input[name=btSelectItem] 单选框,工具栏 #insert 新增 / #update 修改 / #delete 删除都作用于选中行;行内那个「查看」按钮是 gzlc() 工作流程图,不是打开单据。关键字搜索用右上角可见的 #seacher;#expenexplain 那套高级查询 在收起的面板里(元素存在但不可见,fill 会卡 30 秒)。表格是 AJAX 后加载的,用 wait_rows_loaded() 等,别拍脑袋 wait_for_timeout;空状态行(没有找到匹配的记录)不算命中,也别当成还在加载 直达报销 URL 不建立会话,会被踢回「长时间未操作,请重新登录!」
  • 单据页是多标签共用一页:基本信息 / 报销明细 / 支付方式 /(补助清单)/ 附件清单。不在当前标签上的 控件 query_selector 拿得到但不可见,点它必超时——所以每一步都显式 open_tab(...),不要依赖 「上一步把页面留在了哪儿」(旧代码靠 #next3/#next4 串下来,续填时一律失灵)。basic 的候选标记 要给两个:日常单是 #EXPENEXPLAIN,差旅单是 #CAUSE
  • 日常报销与差旅是两个独立列表页(reimburse_page / travel_page),navigate_to_reimburse(page_key) 的快速通道必须比对目标路径,只判断「在不在报销系统」会让人停错页还以为进去了。判断在不在报销系统 用 urlsplit().netloc 精确比主机,别用子串(子串会把 http://elsewhere 判成主机 x)
  • 附件两处硬约束:「附件说明」是 required,留空点确定只会被 HTML5 校验挡住(不报错、不发请求、 弹窗不关),所以缺说明要兜底成「类型:文件名」并在提交后确认弹窗已关;附件清单那张表不是 #table, 按表头里的「附件文件名」找
  • 多进程共存:宿主会保留旧代 stdio 子进程,subagent/teammate 也会各起一条 transport—— observed 4 个 PID 并存。 因此任何"独占资源"(浏览器、Office)都要有磁盘锁,别假设只有一个服务器实例
  • 配置优先级:环境变量 > scripts/data/config.json > src/config.py 内置默认;含 your- 的占位值 按未配置处理并告警。改完配置要重连常驻 MCP 子进程才生效(进程内有缓存)。safe_config() 才是 可以进日志/回显的那份,load_config() 里有明文密码
  • 敏感信息:scripts/data/config.json 含登录凭据、cases/ 含身份证号与卡号,两者都已在 .gitignore, 绝不要提交,也不要回显密码
  • 发票类型区分:差旅发票(高铁票/酒店住宿)走差旅报销流程,不生成申购单;普通发票生成低值易耗品申购单

技能(skill)分发

真源是 .agents/skills/reimbursement/(随仓库版本管理);scripts/install_skill.py 单向同步到 用户级目录,让技能在任意项目里可用(技能内容不引用仓库文件:字段权威来自 extraction_spec / info_skeleton / rules_get 三个工具)。改完 SKILL 记得 make skill-install,别反向编辑安装副本。

  • 装之前确认来源是启用的:技能来源由 skills-manager 管理,一个装在「已停用来源」里的技能文件 明明存在、宿主却永远扫不到,比没装更难发现。脚本会读 ~/.dsh/dsh-skills-manager.log 里该目录 最后一条 source-enable/-disable 记录并在停用时拒绝安装(--force 可强行装上)。 本机现状:~/.agents/skills 启用(默认目标),~/.dsh/skills 已停用
  • 同名技能项目级优先:skill 工具返回的 resourceBase.path 能看出实际加载的是哪一份

上传收件器

uv run python -m src.upload.server            # 0.0.0.0:5055,手机扫码可用
uv run python -m src.upload.server --local    # 只监听 127.0.0.1
uv run python -m src.upload.server --pin 1234 # 额外访问码

页面地址由 case_create 返回(含每会话令牌)。安全边界:令牌强制(无令牌 403)、只写不读(无列表无下载)、 扩展名白名单、25MB/文件、200 文件/会话、PIN 可选。二维码由服务端用 segno 生成,零外部 CDN。

测试

make test   # pytest + coverage report (term-missing)

测试目录 tests/,配置在 pyproject.toml(testpaths、pythonpath=["."])。 COM 相关路径(申购单)不进单测——需要本机装有 Excel,改动后用真实生成验证(约 2-5 秒)。 改完 MCP 工具的顺序:make mcp-smoke(验 wire 上的工具清单,进程内 import 成功不代表宿主能看到)→ make mcp-docs(由线上 schema 重出 docs/MCP.md 的工具参考)→ 重连宿主(杀掉 stdio 子进程会自动重启并重同步工具)。 make docs-check 能在 CI/提交前发现文档与代码漂移(退出码 1)。