From e1acf2de84ae74f214ef736963584bc181a200fb Mon Sep 17 00:00:00 2001 From: wandering Date: Mon, 19 Oct 2026 22:39:03 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E6=96=87=E6=A1=A3=E5=85=A8=E9=9D=A2?= =?UTF-8?q?=E5=AF=B9=E9=BD=90=20MCP=20=E6=9E=B6=E6=9E=84=EF=BC=8C=E5=B9=B6?= =?UTF-8?q?=E8=84=B1=E6=95=8F=E5=85=AC=E5=BC=80=E6=93=8D=E4=BD=9C=E6=8C=87?= =?UTF-8?q?=E5=8D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 接口与入门: - 新增 docs/MCP.md:注册方法(DSH patch + 通用 client)、返回信封、会话目录布局、标准流程、 硬约束与未实现清单;工具参考段由**运行时 input_schema 自动生成**,与代码同源不会漂移 - 删除 docs/API.md(856 行 Flask 路由/SSE 文档,描述的端点已不存在) - 重写根 README.md:为什么放弃自带前端、职责分工表、快速开始、注册配置、流程链、结构与风险 - 重写 AGENTS.md:命令表换成 mcp/mcp-smoke/upload;mypy 目标改 src;新增「提交纪律」 (pre-commit 的 stash 窗口会回滚未暂存改动)、COM 与 Playwright 线程约束、多进程共存提醒 包文档:src/README.md 与各包 README 按现状重写;新增 src/mcp/README.md(写工具的规矩、 宿主侧四个坑)与 src/upload/README.md(路由表与安全边界);tests/scripts/config 三处索引更新。 提示词迁移:src/infra/llm/ 整个删除,三份系统提示词迁到 .agents/skills/reimbursement/references/ (invoice-fields / travel-info-fields / normal-info-fields),删掉 can_submit 与 suggestion (save_info 也会主动剥离),「严格只输出 JSON」改为「交给对应工具落盘」;新增 SKILL.md 作为 宿主侧操作纪律(已被 DSH 热加载为 reimbursement 技能)。 作废处理:删除 .agents/docs/guides/系统事件流全景图.md(SSE 与 file_events/agent_events 通道已整体 移除,现在没有任何事件流);给项目架构全景图与架构分析-2026-06-15 加「已过时」横幅保留历史; config.example.json 去 consumable_storage 加 upload_pin;.env.example 去 LLM 段并说明已不再自动加载 .env。 安全:docs/报销操作指南.md 的示例配置里原来写着**真实姓名、16 位公务卡号、内网 LLM 地址与工号** (自 9fe5d70/2026-05-25 起在版本历史里),本次全部换成占位符。历史清洗与令牌轮换另议。 门禁(改动含 src/infra/llm 删除,故重跑全部门禁): ruff check PASS | ruff format --check PASS(42) | mypy src PASS(34) | deptry PASS(36) | pytest 107 passed | scripts/mcp_smoke.py PASS(17 工具 / instructions 1449 字节 / case_list ok) 申购单在依赖清理后另已实机复测:2.4s 生成,产物单元格读回正确。 --- .agents/docs/README.md | 13 +- .agents/docs/guides/系统事件流全景图.md | 657 -------------- .agents/docs/guides/项目架构全景图.md | 11 +- .../docs/plans/MCP化改造方案-2026-10-19.md | 89 +- .agents/docs/plans/架构分析-2026-06-15.md | 11 +- .agents/skills/README.md | 16 +- .agents/skills/reimbursement/SKILL.md | 50 + .../references/invoice-fields.md | 9 +- .../references/normal-info-fields.md | 21 +- .../references/travel-info-fields.md | 23 +- .env.example | 13 +- AGENTS.md | 102 ++- README.md | 571 +++--------- config.example.json | 2 +- config/README.md | 29 +- docs/API.md | 856 ------------------ docs/MCP.md | 304 +++++++ docs/README.md | 13 +- docs/报销操作指南.md | 455 +++------- scripts/README.md | 23 +- src/README.md | 107 +-- src/core/README.md | 20 +- src/core/extraction/README.md | 30 +- src/core/matching/README.md | 24 +- src/core/validation/README.md | 32 +- src/infra/README.md | 24 +- src/infra/browser/README.md | 47 +- src/infra/documents/README.md | 37 +- src/infra/llm/README.md | 32 - src/infra/llm/__init__.py | 16 - src/infra/llm/prompt.py | 30 - src/infra/llm/prompts/README.md | 20 - src/mcp/README.md | 33 + src/upload/README.md | 40 + tests/README.md | 29 +- 35 files changed, 1057 insertions(+), 2732 deletions(-) delete mode 100644 .agents/docs/guides/系统事件流全景图.md create mode 100644 .agents/skills/reimbursement/SKILL.md rename src/infra/llm/prompts/invoice_system.md => .agents/skills/reimbursement/references/invoice-fields.md (93%) rename src/infra/llm/prompts/normal_info_system.md => .agents/skills/reimbursement/references/normal-info-fields.md (89%) rename src/infra/llm/prompts/travel_info_system.md => .agents/skills/reimbursement/references/travel-info-fields.md (92%) delete mode 100644 docs/API.md create mode 100644 docs/MCP.md delete mode 100644 src/infra/llm/README.md delete mode 100644 src/infra/llm/__init__.py delete mode 100644 src/infra/llm/prompt.py delete mode 100644 src/infra/llm/prompts/README.md create mode 100644 src/mcp/README.md create mode 100644 src/upload/README.md diff --git a/.agents/docs/README.md b/.agents/docs/README.md index f5edfc6..170b4c6 100644 --- a/.agents/docs/README.md +++ b/.agents/docs/README.md @@ -2,11 +2,12 @@ 本目录存放维护人员与自动化代理相关资料,主要用于项目运维,不属于对外公开的用户文档。 -- standards/:存放维护人员需遵守的规范制度与校验规则 -- plans/:存放实施方案与工作交接说明 -- error-experience/、good-experience/:存放内部经验总结文档 -- guides/:面向维护人员的工作流程及集成实操手册 -- architecture/manifest.yaml:记录可读性检查所覆盖的文件路径 +- `docs/plans/`:实施方案与阶段记录(当前主线是 `MCP化改造方案-2026-10-19.md`,含实测数据与踩坑) +- `docs/guides/`:内部实操手册(`项目架构全景图.md` 已标注改造前的结构,以方案文档为准) +- `docs/standards/`:规范制度;`docs/error-experience/`、`docs/good-experience/`:经验总结(按日期命名,保留历史原貌) +- `skills/`:Agent 技能定义(见 `skills/README.md`) -对外用户文档及说明文件请统一放置在 docs/ 目录下。 +对外用户文档统一放 `docs/`(工具契约是 `docs/MCP.md`)。 +> 已作废并删除:`docs/guides/系统事件流全景图.md`(SSE 与 file_events/agent_events 通道已随 Web/Agent 一起移除, +> 现在没有任何长连接事件流;需要看历史内容用 git 提交 `0c54dc8^`)。 diff --git a/.agents/docs/guides/系统事件流全景图.md b/.agents/docs/guides/系统事件流全景图.md deleted file mode 100644 index b5b13f1..0000000 --- a/.agents/docs/guides/系统事件流全景图.md +++ /dev/null @@ -1,657 +0,0 @@ -# 系统事件流全景图 - -> 最后更新: 2026-06-15 -> 用途: 排查 SSE 事件问题、提交流程中断、状态不一致等 Bug - ---- - -## 一、核心概念 - -### 1.1 前后端状态映射 - -| 前端 `App.processState` | 后端 `AgentState` | 含义 | -|---|---|---| -| `idle` | `IDLE` | 初始状态,等待用户操作 | -| `processing` | `EXTRACTING` | LLM 正在分析文件 | -| `awaiting_supplement` | `AWAITING_SUPPLEMENT` | 信息不完整,等待用户补充 | -| `submitting` | `SUBMITTING` | 正在提交到财务系统 | -| `ready` | `READY` | 信息完整,可以提交 | -| `submitting` | `SUBMITTING` | 正在提交到财务系统 | -| `done` | `DONE` / `ERROR` | 流程结束(成功或失败) | - -### 1.2 通信机制 - -```mermaid -sequenceDiagram - participant F as 前端 - participant S as SSE连接 - participant B as 后端线程 - - F->>B: POST /api/agent/process/:sid - B-->>F: {status: "started"} - F->>S: GET /api/logs/:sid (SSE长连接) - S-->>F: message: file_progress (轮询 file_events.log) - S-->>F: message: llm_stream (轮询 llm_stream.log) - S-->>F: message: agent_* (轮询 agent_events.log) - S-->>F: message: done (检测到 result.json) - S->>S: 连接关闭 -``` - -**关键约束**: -- 后端所有处理接口均返回 `{status: "started"}`,实际工作在 daemon 线程中执行 -- SSE 通过每 0.5 秒轮询 4 个日志文件实现(非原生 SSE,是长轮询模拟) -- `result.json` 的原子写入:先写 `.tmp`,再 `replace()` 重命名 -- SSE 超时:600 秒后自动断开 - -### 1.3 操作信号点清单 - -每个 API 操作涉及的信号文件生命周期如下。**新增或修改信号文件时必须同步更新此清单**。 - -| 序号 | 操作 | API 端点 | 线程启动时清理 | 一次写入且不被清理 | 轮次结束时写入 | -|---|---|---|---|---|---| -| 1 | 初始处理 | `POST /api/agent/process/:sid` | `llm_stream.log`, `agent_events.log`, `result.json` | `file_events.log`, `session.log` | `result.json` | -| 2 | 补充文件 | `POST /api/agent/supplement/:sid` | `llm_stream.log`, `agent_events.log`, `result.json` | `file_events.log`, `session.log` | `result.json` | -| 3 | 文字补充 | `POST /api/agent/user-supplement/:sid` | `llm_stream.log`, `agent_events.log`, `result.json` | `file_events.log`, `session.log` | `result.json` | -| 4 | 强制提交 | `POST /api/agent/force-submit/:sid` | `llm_stream.log`, `agent_events.log`, `result.json` | `file_events.log`, `session.log` | `result.json` | -| 5 | 手动财务提交 | `POST /api/submit-financial/:sid` | `llm_stream.log`, `agent_events.log`, `result.json` | `file_events.log`, `session.log` | `result.json` | - -**信号点说明**: - -| 信号文件 | 读/写方 | 生命周期 | 作用 | -|---|---|---|---| -| `result.json` | 后端线程写入,SSE 端点读取 | 每轮开始时删除,`finally` 块中原子写入 | SSE 检测到该文件即发射 `done` 事件并断开连接 | -| `agent_events.log` | Agent 调度器追加写入,SSE 端点读取 | 每轮开始时删除,Agent 运行时持续追加 | 传递 agent 状态变化事件给前端 | -| `llm_stream.log` | LLM 回调追加写入,SSE 端点读取 | 每轮开始时删除,LLM 运行时持续追加 | 传递 LLM 流式输出给前端 | -| `file_events.log` | `pipeline_web` 追加写入,SSE 端点读取 | 会话内持续追加,不删除 | 传递文件处理进度给前端 | -| `session.log` | `sse_handler` 追加写入,SSE 端点读取 | 会话内持续追加,不删除 | 传递普通日志行给前端 | - -### 1.4 关键约束(修改代码前必读) - -**约束 1:`result.json` 必须在每轮线程启动时删除** - -SSE 端点通过检测 `result.json` 是否存在来判断任务是否完成。如果上一轮的 `result.json` 残留,SSE 会立即读到旧数据并发射 `done` 事件,导致前端断开连接,新任务的消息无法送达。 - -- 实现位置:`_run_agent_task()` 的 `try` 块开头 -- 删除时机:在 `install_log_collector()` 之后、`task_fn()` 执行之前 -- 写入位置:`finally` 块中统一写入(唯一写入点) -- 写入规则:`finally` 始终执行原子写入,不再有条件判断 -- `_emit_ready_and_submit` 只返回 result 字典,不写入文件 - -**约束 2:`result.json` 的写入必须使用 `finally` 块** - -无论任务成功或失败,SSE 端点都需要 `result.json` 来发送 `done` 事件。如果仅在成功路径写入,异常时 SSE 会一直轮询直到 600 秒超时,前端无反馈。 - -**约束 3:SSE 新建连接时,当前文件偏移必须从 0 开始** - -`_run_agent_task` 在启动时删除 `llm_stream.log` 和 `agent_events.log`,确保 SSE 重新建立连接后从 0 偏移开始读取。如果文件不被删除,旧的事件会被重复发送给前端。 - -**约束 4:前端 SSE 连接的生命周期** - -- 前端在每次 POST 请求返回 `{status: "started"}` 后立即创建新的 SSE 连接 -- 收到 `done` 事件后关闭连接 -- 旧的连接引用必须清理(`agent.js` 中的 `agentEventSource`) -- 如果前端在 POST 之前就创建了 SSE 连接,会读到旧数据 - ---- - -## 二、场景一:用户提交材料 → LLM 分析完整 → 直接提交 - -### 2.1 时序图 - -```mermaid -sequenceDiagram - participant F as 前端 - participant S as SSE连接 - participant B as 后端线程 - participant A as Agent调度器 - - F->>F: startProcess() - F->>B: POST /api/agent/process/:sid - B-->>F: {status: "started"} - F->>S: GET /api/logs/:sid - - Note over B,A: 后台线程启动 - B->>A: extract_invoices() - S-->>F: file_progress (processing/done) - Note over S: 轮询 file_events.log - - S-->>F: llm_stream (start/chunk/end) - Note over S: 轮询 llm_stream.log - - S-->>F: agent_state_change (state=extracting) - Note over S: 轮询 agent_events.log - - Note over A: _do_extraction_with_validation()
LLM提取 → validator校验
最多3次重试 - - S-->>F: agent_state_change (校验通过/未通过) - - Note over A: can_submit == true - - A->>A: state → READY - A->>A: _emit_agent_event (agent_ready) - A->>A: _emit_ready_and_submit() - A->>A: run_financial_submit() - - S-->>F: agent_ready - - Note over B: 写入 result.json - - S-->>F: done (携带 result) - Note over S: 检测到 result.json - - F->>F: es.close() - F->>F: App.processState = 'done' - F->>F: addChatMessage(成功) - Note over B: remove_log_collector -``` - -### 2.2 事件流清单 - -| 序号 | 事件类型 | 来源文件 | 触发时机 | 前端处理 | -|---|---|---|---|---| -| 1 | `file_progress` | `file_events.log` | 每个文件处理开始/完成 | 更新文件状态 UI | -| 2 | `llm_stream` | `llm_stream.log` | LLM 流式输出 | 显示聊天气泡 | -| 3 | `agent_state_change` | `agent_events.log` | 状态变为 `extracting` | 显示瞬态状态提示 | -| 4 | `agent_state_change` | `agent_events.log` | 校验通过/未通过 | 更新瞬态状态 | -| 5 | `agent_ready` | `agent_events.log` | 双重校验通过 | 由 `done` 事件统一处理 | -| 6 | `done` | SSE 检测到 `result.json` | 流程结束 | 根据 `result` 判断终态 | - -### 2.3 result.json 结构(成功路径) - -```json -{ - "ok": true, - "agent_ready": true, - "submit_ok": true, - "round": 1, - "message": "信息完整,已自动提交到财务系统" -} -``` - ---- - -## 三、场景二:用户提交材料 → 需补充 → 用户上传文件 - -### 3.1 时序图 - -```mermaid -sequenceDiagram - participant F as 前端 - participant S as SSE连接 - participant B as 后端线程 - participant A as Agent调度器 - - Note over F,A: 阶段1: 初次分析 - F->>B: POST /api/agent/process/:sid - B-->>F: {status: "started"} - F->>S: GET /api/logs/:sid - - S-->>F: agent_state_change (state=extracting) - - Note over A: can_submit == false - - A->>A: state → AWAITING_SUPPLEMENT - - S-->>F: agent_request_supplement - - Note over B: 写入 result.json
(waiting_for_supplement=true) - - S-->>F: done - F->>F: es.close() - F->>F: App.processState = 'awaiting_supplement' - F->>F: showStatus('请补充') - F->>F: showAgentRequest() - Note over B: remove_log_collector - - Note over F,A: 阶段2: 用户上传补充文件 - F->>F: 用户点击"上传补充材料" - F->>F: 文件上传完成 - F->>F: handleSupplementUpload(newFilenames) - F->>B: POST /api/agent/supplement/:sid
{files: [...]} - B-->>F: {status: "started"} - F->>S: GET /api/logs/:sid - - Note over B: 新后台线程启动 - - A->>A: add_supplement()
(记录文件名, 发射收到事件) - - S-->>F: agent_supplement_received - - A->>A: extract_invoices()
(重新提取所有文件) - A->>A: run_agent_round(new_files=[...]) - Note over A: 加载上一轮结果作为
previous_analysis - - S-->>F: agent_state_change (state=extracting) - - Note over A: LLM提取 → 校验循环 - - alt 分支A: 补充后仍不完整 - S-->>F: agent_request_supplement - S-->>F: done (waiting=true) - F->>F: es.close() - F->>F: App.processState = 'awaiting_supplement' - else 分支B: 补充后完整 - Note over A: can_submit == true - A->>A: state → READY - A->>A: _emit_ready_and_submit() - S-->>F: agent_ready - S-->>F: done (submit_ok=true) - F->>F: es.close() - F->>F: App.processState = 'done' - F->>F: addChatMessage(成功) - end -``` - -### 3.2 事件流清单(补充文件路径) - -| 序号 | 事件类型 | 来源文件 | 触发时机 | 前端处理 | -|---|---|---|---|---| -| 1 | `agent_supplement_received` | `agent_events.log` | 收到补充文件列表 | 显示"已收到补充文件" | -| 2 | `agent_state_change` | `agent_events.log` | 开始重新分析 | 显示瞬态状态 | -| 3 | `agent_request_supplement` | `agent_events.log` | 仍不完整 | 更新补充请求面板 | -| 4 | `agent_ready` | `agent_events.log` | 校验通过 | 由 `done` 统一处理 | -| 5 | `done` | SSE 检测到 `result.json` | 流程结束 | 判断终态 | - -### 3.3 result.json 结构(需补充) - -```json -{ - "ok": true, - "agent_ready": false, - "agent_state": "awaiting_supplement", - "round": 1, - "waiting_for_supplement": true -} -``` - ---- - -## 四、场景三:用户提交材料 → 需补充 → 用户通过对话提供信息 - -### 4.1 时序图 - -```mermaid -sequenceDiagram - participant F as 前端 - participant S as SSE连接 - participant B as 后端线程 - participant A as Agent调度器 - - Note over F,A: 阶段1: 初次分析 - F->>B: POST /api/agent/process/:sid - B-->>F: {status: "started"} - F->>S: GET /api/logs/:sid - - S-->>F: agent_request_supplement - S-->>F: done (waiting=true) - F->>F: es.close() - F->>F: App.processState = 'awaiting_supplement' - Note over B: remove_log_collector - - Note over F,A: 阶段2: 用户输入文字 - F->>F: 用户在输入框输入文字 - F->>F: handleUserSupplement() - F->>B: POST /api/agent/user-supplement/:sid
{text: "..."} - B-->>F: {status: "started"} - F->>S: GET /api/logs/:sid - - Note over B: 新后台线程启动 - - S-->>F: agent_supplement_received - - A->>A: process_user_text_supplement() - A->>A: process_user_supplement()
(LLM解析用户文字) - - S-->>F: llm_stream (解析过程) - - A->>A: merge_supplement_into_info()
(合并到 extracted_info) - Note over A: 保存到缓存文件 - - A->>A: run_agent_round()
(重新校验) - - S-->>F: agent_state_change
(state=extracting, 正在重新校验) - - alt 分支A: 补充后仍不完整 - S-->>F: agent_request_supplement - S-->>F: done (waiting=true) - F->>F: es.close() - F->>F: App.processState = 'awaiting_supplement' - else 分支B: 补充后完整 - Note over A: can_submit == true - A->>A: state → READY - A->>A: _emit_ready_and_submit() - S-->>F: agent_ready - S-->>F: done (submit_ok=true) - F->>F: es.close() - F->>F: App.processState = 'done' - F->>F: addChatMessage(成功) - end -``` - -### 4.2 事件流清单(文字补充路径) - -| 序号 | 事件类型 | 来源文件 | 触发时机 | 前端处理 | -|---|---|---|---|---| -| 1 | `agent_supplement_received` | `agent_events.log` | 收到用户文字 | 显示"已收到补充" | -| 2 | `llm_stream` | `llm_stream.log` | LLM 解析用户文字 | 显示解析过程 | -| 3 | `agent_state_change` | `agent_events.log` | 开始重新校验 | 显示"正在重新校验" | -| 4 | `agent_request_supplement` | `agent_events.log` | 仍不完整 | 更新补充请求 | -| 5 | `agent_ready` | `agent_events.log` | 校验通过 | 由 `done` 统一处理 | -| 6 | `done` | SSE 检测到 `result.json` | 流程结束 | 判断终态 | - ---- - -## 五、强制提交流程 - -### 5.1 时序图 - -```mermaid -sequenceDiagram - participant F as 前端 - participant S as SSE连接 - participant B as 后端线程 - - F->>F: handleForceSubmit() - F->>F: App.forceSubmitting = true - F->>B: POST /api/agent/force-submit/:sid - B-->>F: {status: "started"} - F->>S: GET /api/logs/:sid - - Note over B: 后台线程启动 - - B->>B: force_submit()
(state → READY) - - S-->>F: agent_force_submit - - B->>B: run_financial_submit() - - Note over B: 写入 result.json - - S-->>F: done - F->>F: es.close() - F->>F: App.forceSubmitting = false - F->>F: App.processState = 'done' -``` - ---- - -## 六、错误处理路径 - -### 6.1 错误场景和事件 - -| 错误场景 | 后端行为 | 发射事件 | 前端表现 | -|---|---|---|---| -| LLM 提取异常 | `state → ERROR` | `agent_error` | 聊天显示错误,`processState → 'done'` | -| 规则校验 3 次失败 | 返回最后一次结果,继续语义判断 | `agent_state_change` | 依赖 `can_submit` 字段决定 | -| 轮次超限 (5 轮) | `state → ERROR` | `agent_max_rounds` | 聊天显示错误,可强制提交 | -| 财务提交失败 | `result.submit_ok = false` | 无独立事件 | `done` 事件携带错误信息 | -| SSE 连接中断 | 无 | `es.onerror` 触发 | 显示"连接中断" | -| 超时 (600s) | SSE 轮询循环退出 | 连接自然断开 | 连接断开 | - -### 6.2 agent_error 事件结构 - -```json -{ - "type": "agent_error", - "message": "LLM 提取失败: ..." -} -``` - -### 6.3 agent_max_rounds 事件结构 - -```json -{ - "type": "agent_max_rounds", - "message": "已达到最大轮次 (5),请检查信息或强制提交" -} -``` - ---- - -## 七、状态机完整图 - -### 7.1 后端 AgentState 状态机 - -```mermaid -stateDiagram-v2 - [*] --> IDLE - - IDLE --> EXTRACTING: POST /api/agent/process\nPOST /api/agent/supplement\nPOST /api/agent/user-supplement - - EXTRACTING --> READY: can_submit == true - EXTRACTING --> AWAITING_SUPPLEMENT: can_submit == false - EXTRACTING --> ERROR: 异常 / 轮次超限 - - READY --> SUBMITTING: _emit_ready_and_submit() - SUBMITTING --> DONE: 财务提交完成 - - AWAITING_SUPPLEMENT --> EXTRACTING: 用户补充文件/文字 - - READY: 准备提交\n(终态保护) - SUBMITTING: 财务提交中\n(终态保护) - DONE: 终态\n(终态保护) - ERROR: 错误状态\n(可强制提交) - - note right of EXTRACTING - LLM 提取 + validator 校验\n最多 3 次重试 - end note -``` - -### 7.2 前端 processState 状态机 - -```mermaid -stateDiagram-v2 - [*] --> idle - - idle --> processing: startProcess() - - processing --> awaiting_supplement: done事件\nresult.waiting_for_supplement - processing --> done: done事件\nresult.ok - - awaiting_supplement --> processing: 补充文件或文字 - awaiting_supplement --> submitting: 强制提交 - - submitting --> done: done事件 - - done: 流程结束 - idle: 初始状态 - processing: 处理中 - awaiting_supplement: 等待补充 - submitting: 提交中 -``` - ---- - -## 八、SSE 事件类型完整参考 - -### 8.1 Agent 事件 (agent_events.log) - -| 事件类型 | 数据结构 | 触发条件 | -|---|---|---| -| `agent_state_change` | `{type, state, round, attempt, message}` | 状态切换 | -| `agent_ready` | `{type, round, message}` | 双重校验通过 | -| `agent_request_supplement` | `{type, round, missing_fields, missing_materials, semantic_issues, suggestion}` | 校验未通过 | -| `agent_supplement_received` | `{type, files}` | 收到用户补充 | -| `agent_force_submit` | `{type, message}` | 用户强制提交 | -| `agent_error` | `{type, message}` | 提取失败 | -| `agent_max_rounds` | `{type, message}` | 达到最大轮次 | - -### 8.2 文件进度事件 (file_events.log) - -| 事件类型 | 数据结构 | 触发条件 | -|---|---|---| -| `file_progress` | `{type, file, status, summary?, error?}` | 文件处理状态变更 | - -`status` 取值: `processing` / `done` / `cached` / `error` - -### 8.3 LLM 流式事件 (llm_stream.log) - -| 事件类型 | 数据结构 | 触发条件 | -|---|---|---| -| `llm_stream` | `{type, phase, text?}` | LLM 输出流 | - -`phase` 取值: `start` / `reasoning` / `chunk` / `end` / `error` - -### 8.4 完成事件 (SSE 直接发送) - -| 事件类型 | 数据结构 | 触发条件 | -|---|---|---| -| `done` | `{type, result: {...}}` | `result.json` 出现 | - ---- - -## 九、常见问题排查清单 - -### 9.1 SSE 事件丢失 - -**症状**: 前端没有收到预期的 agent 事件 - -**排查步骤**: -1. 检查 `agent_events.log` 是否存在、是否有内容 -2. 检查 SSE 连接是否建立成功(浏览器 Network 面板) -3. 确认 `sse_handler.install_log_collector()` 是否被调用 -4. 确认 `remove_log_collector()` 是否过早调用 - -### 9.2 提交流程中断 - -**症状**: 流程在某个中间状态卡住,没有 `done` 事件 - -**排查步骤**: -1. 检查 `result.json` 是否被写入 -2. 检查后台线程是否异常退出(查看 `session.log`) -3. 确认 `finally` 块中的 `result.json` 写入逻辑是否执行 -4. 检查是否触发了 600 秒超时 - -### 9.3 状态不一致 - -**症状**: 前端 `processState` 和后端 `AgentState` 不匹配 - -**排查步骤**: -1. 对比 `agent_events.log` 中的状态变更序列 -2. 检查前端是否正确处理了 `done` 事件 -3. 确认 SSE 连接是否在适当时机关闭和重建 -4. 检查 `App.agentEventSource` 引用是否正确清理 - -### 9.4 补充流程不触发 - -**症状**: 用户上传补充文件或输入文字后,没有重新分析 - -**排查步骤**: -1. 确认 `processState` 是否为 `awaiting_supplement` -2. 检查补充 API 是否返回 `{status: "started"}` -3. 检查新 SSE 连接是否成功建立 -4. 确认 `add_supplement()` 或 `process_user_text_supplement()` 是否被调用 - -### 9.5 补充材料后前端无任何消息(`result.json` 残留问题) - -**症状**: 第二轮及之后的补充材料提交后,前端完全没有任何消息显示,状态栏不更新,聊天区无新增消息。后台日志显示处理正常完成。 - -**根因**: `_run_agent_task` 在每轮启动时未清理上一轮的 `result.json`。SSE 端点轮询时立即检测到旧的 `result.json`,直接发射 `done` 事件并关闭连接,前端断开后无法接收新任务的消息。 - -**排查步骤**: -1. 检查 session 目录中 `result.json` 的修改时间 — 如果早于当前轮次开始时间,说明是残留文件 -2. 检查浏览器 Network 面板中 SSE 连接 — 是否在建立后立即收到 `done` 事件 -3. 确认 `_run_agent_task` 是否在启动时清理了 `result.json` - -**修复**: 在 `_run_agent_task` 的 `try` 块开头同时清理 `llm_stream.log`、`agent_events.log` 和 `result.json` 三个文件。 - -**详细记录**: 参见 `.agents/docs/error-experience/2026-06-15-补充材料SSE立即读到旧result.json导致前端无消息.md` - ---- - -## 十、关键文件索引 - -| 文件 | 职责 | -|---|---| -| `src/web/static/js/process.js` | 主提交流程入口,SSE 事件分发 | -| `src/web/static/js/agent.js` | Agent 事件处理,补充/强制提交逻辑 | -| `src/web/static/js/state.js` | 全局状态管理 | -| `src/web/routes.py` | 后端路由,后台线程启动 | -| `src/agent/orchestrator.py` | Agent 调度器,状态机,校验循环 | -| `src/web/sse_handler.py` | SSE 日志收集器 | -| `src/web/pipeline_web.py` | 发票提取管道,财务提交 | - ---- - -## 十一、文件生命周期与操作信号点 - -### 11.1 单轮处理的完整文件生命周期 - -```mermaid -sequenceDiagram - participant API as 路由层 - participant RT as _run_agent_task - participant TF as task_fn - participant SS as _emit_ready_and_submit - participant SSE as SSE 端点 - - Note over API: 1. 创建 handler - API->>API: install_log_collector(session_dir) - Note over API: 创建 SSE 日志收集器
随即开始写入 session.log - - API->>RT: threading.Thread(target=_run_agent_task) - - Note over RT: 2. 清理残留文件 - RT->>RT: unlink(llm_stream.log) - RT->>RT: unlink(agent_events.log) - RT->>RT: unlink(result.json) - - Note over RT: 3. 执行任务 - RT->>TF: task_fn(session_dir, config) - - Note over TF: 执行期间各个文件由对应模块写入: - TF-->>TF: file_events.log (pipeline_web) - TF-->>TF: llm_stream.log (LLM 回调) - TF-->>TF: agent_events.log (Agent 调度器) - - TF-->>RT: 返回 (agent_session, 占位 result) - - alt 成功路径 (READY) - RT->>SS: _emit_ready_and_submit() - Note over SS: 发射 agent_ready 事件
执行财务提交
返回 result 字典 - SS-->>RT: result 字典 - Note over RT: result = {...} - else 需补充路径 (AWAITING_SUPPLEMENT) - Note over RT: result = {waiting_for_supplement: true} - else 异常路径 - Note over RT: result = {ok: false, error: ...} - end - - Note over RT: 4. finally 块 — 唯一写入点 - RT->>RT: 原子写入 result.json (.tmp → replace) - - RT->>RT: remove_log_collector(handler) - - Note over SSE: 5. SSE 端点检测 - SSE->>SSE: 轮询检测到 result.json - SSE-->>SSE: 发射 done 事件 - SSE->>SSE: break 退出轮询 -``` - -### 11.2 各阶段信号文件状态 - -| 阶段 | `result.json` | `llm_stream.log` | `agent_events.log` | `file_events.log` | `session.log` | -|---|---|---|---|---|---| -| 会话创建 | 不存在 | 不存在 | 不存在 | 不存在 | 不存在 | -| `install_log_collector` 后 | 不存在 | 不存在 | 不存在 | 不存在 | 开始写入 | -| `_run_agent_task` 清理后 | 已删除 | 已删除 | 已删除 | 保持 | 保持 | -| 文件提取中 | 不存在 | 不存在 | 不存在 | 持续追加 | 持续追加 | -| LLM 提取中 | 不存在 | 持续追加 | 持续追加 | 保持 | 持续追加 | -| 校验中 | 不存在 | 保持 | 持续追加 | 保持 | 持续追加 | -| 任务完成 (READY) | 已写入 | 保持 | 保持 | 保持 | 保持 | -| 任务完成 (需补充) | 已写入 | 保持 | 保持 | 保持 | 保持 | -| 任务异常 | 已写入 | 保持 | 保持 | 保持 | 保持 | -| SSE done 事件后 | 保持 | 保持 | 保持 | 保持 | 保持 | - -### 11.3 新增信号文件检查清单 - -当需要在系统中新增一个信号文件(如 `submit_progress.log`)时,必须检查以下事项: - -1. **写入方**:哪个模块负责写入?写入时机是什么? -2. **读取方**:SSE 端点是否需要轮询?前端是否需要处理? -3. **清理时机**:是否需要在 `_run_agent_task` 中清理?如果不需要,为什么? -4. **原子性**:写入是否需要 `.tmp` + `replace` 模式? -5. **轮询偏移**:SSE 端点是否需要跟踪该文件的读取偏移? -6. **更新本文档**:在 1.3 操作信号点清单中新增一行,在 11.2 文件状态表中新增一列 -7. **更新 `_run_agent_task`**:如果需要清理,在清理循环中添加文件名 -8. **更新前端**:在 `sse.js` 或 `agent.js` 中添加对应的事件处理器 \ No newline at end of file diff --git a/.agents/docs/guides/项目架构全景图.md b/.agents/docs/guides/项目架构全景图.md index 649ca57..e4fc9f3 100644 --- a/.agents/docs/guides/项目架构全景图.md +++ b/.agents/docs/guides/项目架构全景图.md @@ -1,3 +1,12 @@ + +> **已过时(2026-10-19 MCP 化改造)**:本文描述的是改造前的架构,保留作为历史与决策背景。 +> 现状请看 `docs/MCP.md`(工具契约)、`AGENTS.md`(项目操作指南)与 +> `.agents/docs/plans/MCP化改造方案-2026-10-19.md`(决策与实测记录)。 +> 具体差异:`src/agent/`(状态机与 SSE)、`src/web/`(Flask 界面)、CLI 入口、内置 LLM 抽取 +> 与 `llama-index` 依赖均已删除;Word 出库单功能下线;提示词迁移为 `.agents/skills/reimbursement/references/`。 + +--- + # 项目架构全景图 > 最后更新: 2026-06-15 @@ -398,4 +407,4 @@ graph TD | `src/web/sse_handler.py` | SSE 日志收集器 | | `src/web/static/js/process.js` | 前端主提交流程 | | `src/web/static/js/agent.js` | 前端 Agent 交互处理 | -| `config.json` | 项目配置 | +| `config.json` | 项目配置 | \ No newline at end of file diff --git a/.agents/docs/plans/MCP化改造方案-2026-10-19.md b/.agents/docs/plans/MCP化改造方案-2026-10-19.md index 7949cf1..c47771a 100644 --- a/.agents/docs/plans/MCP化改造方案-2026-10-19.md +++ b/.agents/docs/plans/MCP化改造方案-2026-10-19.md @@ -251,6 +251,10 @@ P0 是唯一可能推翻已定决策的阶段,所以它只做验证、不做 验证脚手架留在 `cases/_p0-gate/` 与 `cases/20261019-P0闸门自测/`(不入库),P1 收尾时删除。 +### 10.5 日期更正 + +本文初版误按 `AGENTS.md` 的 `last_reviewed: 2026-07-02` 推断为 2026-07-23 并以此命名,实际系统日期 2026-10-19,文件已改名。 + ### 10.6 第二批工具(2026-10-19) 新增 `mcp__arb__match_payments` / `save_info` / `validate_info` / `rules_get`(共 14 个工具),并新增 `src/core/matching/runner.py`(从缓存匹配,无 LLM);`match_result.json` 的写入从 extractor 私有函数上移为 `cache.save_match_result`,消除两份实现。`tests/test_mcp_analysis.py` 补 8 个用例(一对多凑额、违反业务约束、can_submit 剥离、draft 不落盘、规则形状、非法 info_type)。 @@ -264,10 +268,6 @@ P0 是唯一可能推翻已定决策的阶段,所以它只做验证、不做 遗留观察(进 P1):`validation_rules.json` 里 `remark` 之类的可选字段写成 `required: true, check_empty: false`,含义变成"键必须存在",模型输出省略空备注时会被判缺失。应把这类字段改 `required: false`,否则每次都要为备注来回补数据。 -### 10.5 日期更正 - -本文初版误按 `AGENTS.md` 的 `last_reviewed: 2026-07-02` 推断为 2026-07-23 并以此命名,实际系统日期 2026-10-19,文件已改名。 - ### 10.7 重连实验与进程模型(2026-10-19) 按约定做了 kill 验证:`taskkill /PID <旧服务进程> /F` 之后,DSH 的 mcp-client 监督器**自动重新拉起子进程并重同步工具列表**——工具数从 10 变成 15,新增的 `info_skeleton` / `match_payments` / `save_info` / `validate_info` / `rules_get` 当场可调用,且全部通过真实 MCP 线路验证(`info_skeleton` 出的骨架填值后 `save_info` 校验 valid=true、置信度 1.0)。**不需要重启 dsh 就能上新工具**,只要让旧子进程退出。 @@ -279,3 +279,84 @@ P0 是唯一可能推翻已定决策的阶段,所以它只做验证、不做 3. 同理,**任何端口/单例资源都不能靠「进程只有一个」假设**(收件器已是独立进程,不受影响)。 小遗留:`case_status` 在 travel_info 与 normal_info 同时存在时只报 `info: travel_info`。P1 改成显式提示冲突(混合发票只能走普通报销),避免误按纯差旅提交。 + +## 11. P1 落地记录(2026-10-19 晚) + +### 11.1 提交与体量 + +| 提交 | 内容 | +|------|------| +| `cffa4cc` | MCP 服务器 + 上传收件器 + `cases/` 会话模型 + 内核拆分(+1975/-1272 行的前半) | +| `0c54dc8` | 删除 `src/agent/`(878 行)与 Web 的 5 个 Agent 端点(824→491 行、18→13 端点) | +| `5f57400` | 2a:`com_thread.py` 单线程 actor、`doc_requisition`、`case_patch_extraction`(工具 15→18) | +| `53b1abe` | 2b:删除 `src/web/`、CLI、内置 LLM、出库单功能与 `llama-index*`+`python-dotenv`(工具 18→17) | + +改造后:`src/mcp` 808 行 / 9 文件,`src/core` 1481 / 9,`src/infra` 1456 / 11,`src/upload` 343 / 3,`tests` 1333 / 6。 +整个 `src` 从约 6900 行降到约 4100 行;依赖 6 个(mcp、segno、flask、playwright、PyMuPDF、pywin32)。 + +### 11.2 验证 + +- `scripts/mcp_smoke.py`(新增):真起 stdio 子进程握手 → **17 个工具、instructions 1449 字节、`case_list` 正常** +- 申购单实机:合成 2 张普通发票 + 1 笔 464 元支付 → 匹配(一对多、`review_required` 如期提示)→ + `exclude=['万用表']` → 5.0s 生成 19456 字节 .xls;依赖清理后复测 2.4s,产物单元格读回正确 + (`xlrd` 随 llama-index 一起消失,但申购单走 Excel COM,不依赖它——这是复测的直接动因) +- `case_patch_extraction` 语义验证:回显 `from`/`to`、未知键告警、与现值相同不写 +- 门禁:`ruff check` + `format --check`(44 文件)+ `mypy src`(36 文件)+ `deptry` + `pytest 107 passed` + +### 11.3 事故:pre-commit 的 stash 窗口把工作区回滚了 + +`git commit` 触发 pre-commit,它会 **stash 未暂存改动** 跑完再还原;我的执行程序在 120s 上限被杀, +还原步骤没跑完,`AGENTS.md`、`src/web/routes.py`、`requisition.py`、`tests/test_requisition.py` 四个文件 +被回滚成 HEAD 版本。`git fsck --unreachable --dangling` 显示 **0 个游离对象**,说明 pre-commit 没有留下可捞的 +stash,只能按锚点重放。重放时我又因「往上找分隔注释」找过头,把 `_validate_session` 与 `_build_web_config` +一起删了——靠**审计删除行清单**(`git diff` 里出现 `-def _validate_session`)才发现,从 HEAD 取回并跑 Web 冒烟确认。 + +顺带清掉两个我造成的污染:`git add -A --intent-to-add` 误把 `.mnemon/`(DSH 本地记忆库)放进暂存区; +钩子在我给的清空 `HOME` 环境里把缓存写成了仓库内的 `~/` 目录(里面是它 clone 的 ruff-pre-commit 仓库和 venv)。 +两者都已处理,`.mnemon/` 进 `.gitignore`。 + +结论已写进 AGENTS.md「提交纪律」:门禁退出码单独确认;提交前工作区 `git add` 干净,否则用 `--no-verify` +并手动跑等价检查。本次 P1 之后的提交都用了 `--no-verify`,等价的 ruff/format/mypy/deptry/pytest 全部手跑。 +另一个自己制造的疏漏:2b 第一次提交时 `mcp_smoke.py` 还带 2 个可自动修复的 lint 错误就进了 commit, +已修复并 `git commit --amend`(现 HEAD `53b1abe`),HEAD 是干净的。 + +### 11.4 出库单功能下线(用户决定) + +做 2a 时 `doc_consumable` 无法实测,追出一个真实缺陷:模板 `易耗品、出库单.doc` 在提交 `63b97c6` +(2026-08-31,同一提交新增了 Excel 申购单)被删除,此后 Word 路径必然 `FileNotFoundError`; +而 Web 的 `_try_fill_consumable_doc` 把异常吞成一条日志——**普通发票提交有 7 周在静默跳过出库单生成**。 +差旅发票本来就 skip,所以一直没暴露。 + +从历史导出模板做了 PII 扫描:内容是空白校用表单(「阜阳师范大学科研类低值易耗品入、出库单」), +无姓名、无卡号、无任何 9 位以上数字串。用户选择直接删除该功能,于是删掉 `consumable.py`、 +`doc_consumable` 工具、`consumable_storage` 配置项,文档里的出库单说法全部改成申购单。 + +教训(适用于本项目所有产物代码):**失败必须冒出来**。返回 `doc_ok=false` 而不报错,等于把 7 周的 +产物缺失藏进日志。MCP 工具统一用 `ok:false + reason + next_action` 就是这个原因。 + +### 11.5 遗留债务(按优先级) + +1. **P3 浏览器分步工具**:`BrowserActor` 单线程 + `.browser.lock`(PID+心跳,防多代进程抢)+ + `browser_open/login/new_form/fill(step)/page_state/close`,截图进 `cases//screenshots/`,扫码登录返回 `need_human` +2. **P4 提交护栏**:`submit_preview`(快照+摘要+token)与 `submit_execute`(一次性、摘要变更后失效),默认有头 +3. **申购单模板选择**:现在是「项目根目录 `低值易耗品申购*.xls` 里名字最大者」,应该改成固定模板路径 + (`templates/` 或配置项),避免将来根目录出现产物就选错 +4. **文本层行重排**:用 `get_text("words")` 的坐标把标签和值配对回去,并在与读图冲突时输出 warning, + 而不是让宿主自己猜哪一路对(§10.3 的实测证据) +5. `case_delete` / `case_rename`:宿主需要清理 scratch 会话;反斜杠路径建议在服务端归一化而不是只写文档 +6. `case_status` 应标出「同时存在 travel_info 与 normal_info」的会话(当前两个都存着不报错) +7. `cases/` 下的 scratch 会话(`_p0-gate`、`20261019-P0闸门自测` 等)是 gitignore 的本地产物,可随手删 + +### 11.6 文档同步(P1 附带完成) + +- 新增 `docs/MCP.md`(工具参考由运行时 schema 生成,与代码同源);删除 `docs/API.md`(Flask/SSE 文档) +- 重写根 `README.md`(为什么改成 MCP、快速开始、注册配置、流程图、结构与风险)与 `AGENTS.md` + (含「提交纪律」与多进程共存、COM/Playwright 线程约束) +- 各包 `README.md` 全部按现状重写;新增 `src/mcp/README.md`、`src/upload/README.md` +- 提示词 `src/infra/llm/prompts/*.md` → `.agents/skills/reimbursement/references/*.md`, + 删除其中 `can_submit`/`suggestion`(`save_info` 也会主动剥离),「只输出 JSON」改成「交给对应工具落盘」; + 新增 `.agents/skills/reimbursement/SKILL.md`(宿主侧操作纪律,已被 DSH 热加载为 `reimbursement` 技能) +- 删除作废的 `.agents/docs/guides/系统事件流全景图.md`(事件流通道已整体移除),给 + `项目架构全景图.md` 与 `架构分析-2026-06-15.md` 加「已过时」横幅保留历史 +- `config.example.json` 去掉 `consumable_storage` 加 `upload_pin`;`.env.example` 去掉 LLM 段并说明 + **已不再自动加载 .env**(python-dotenv 移除),凭据只走 `scripts/data/config.json` diff --git a/.agents/docs/plans/架构分析-2026-06-15.md b/.agents/docs/plans/架构分析-2026-06-15.md index 24e87d6..eeaf9c7 100644 --- a/.agents/docs/plans/架构分析-2026-06-15.md +++ b/.agents/docs/plans/架构分析-2026-06-15.md @@ -1,3 +1,12 @@ + +> **已过时(2026-10-19 MCP 化改造)**:本文描述的是改造前的架构,保留作为历史与决策背景。 +> 现状请看 `docs/MCP.md`(工具契约)、`AGENTS.md`(项目操作指南)与 +> `.agents/docs/plans/MCP化改造方案-2026-10-19.md`(决策与实测记录)。 +> 该分析提出的「灵活性差」问题正是 MCP 化改造的动因,方案与落地结果见 +> `MCP化改造方案-2026-10-19.md`(同目录)。 + +--- + # 项目架构分析与重构建议 ## 一、当前架构总览 @@ -222,4 +231,4 @@ agent/ --- -*生成时间: 2026-06-15* +*生成时间: 2026-06-15* \ No newline at end of file diff --git a/.agents/skills/README.md b/.agents/skills/README.md index b00ef2b..c7feac4 100644 --- a/.agents/skills/README.md +++ b/.agents/skills/README.md @@ -1,17 +1,19 @@ --- -last_reviewed: 2026-06-11 +last_reviewed: 2026-10-19 --- -# .agents/skills — Cursor Agent 技能目录 +# .agents/skills — Agent 技能目录 -存放 Cursor Agent 可调用的自动化技能定义。 - -## 技能清单 +本目录由 DSH(及兼容的 Agent)自动发现,每个子目录一个技能,`SKILL.md` 用 YAML front matter 描述触发条件。 | 技能 | 说明 | |------|------| -| `pre-commit-check/` | 提交前代码质量检查:运行 ruff lint/format、mypy 类型检查、deptry 依赖审计,自动修复可修复问题 | +| `reimbursement/` | 报销业务操作纪律:流程顺序、`review_required` 必须逐条复核、双通道冲突处理、禁止宣布可提交; + `references/` 下是七类票据与两种报销数据的字段判读细则(由原内置 LLM 提示词迁移而来) | +| `pre-commit-check/` | 提交前质量门禁:ruff lint/format、mypy、deptry,并自动修复可修项 | +| `clean-git-history/` | 用 git-filter-repo 清理误提交的敏感文件与历史 | ## 使用方式 -Agent 在用户请求提交代码或检查代码质量时自动触发对应技能,无需手动调用。 \ No newline at end of file +命中描述的场景时 Agent 自动加载,无需手动调用。`reimbursement` 的技能正文与 `docs/MCP.md` 有意分工: +前者讲「怎么做才对」,后者讲「接口长什么样」。 diff --git a/.agents/skills/reimbursement/SKILL.md b/.agents/skills/reimbursement/SKILL.md new file mode 100644 index 0000000..276d8eb --- /dev/null +++ b/.agents/skills/reimbursement/SKILL.md @@ -0,0 +1,50 @@ +--- +name: reimbursement +description: >- + 财务报销自动化(阜阳师范大学):把发票、支付截图、出差申请单变成结构化报销数据, + 生成低值易耗品申购单,并在财务系统填报。当用户提到报销、发票、差旅费、公务卡刷卡记录、 + 易耗品申购、财务系统填报时使用本技能。通过 mcp__arb__* 工具操作,禁止臆造财务数据。 +--- + +# 财务报销(MCP 工具版) + +接口细节与参数表见 `docs/MCP.md`。本文件只写"怎么做才对"的操作纪律。 + +## 流程 + +1. `mcp__arb__case_create` 建会话,记下 `case_id` 与 `upload_page`(手机扫码上传入口,告诉用户) +2. 材料入库:本地文件用 `case_add_files(paths=[...])`(**路径必须正斜杠** `D:/Code/...`); + 手机上传先落 `inbox/`,再 `case_add_files(case_id, source="inbox")` 收取 +3. `case_render_pages`:`text_chars > 0` 就 `case_read_text` 读文本层(比读图准且省 token), + 否则读 `pages//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)` + +## 必须遵守 + +- **完整性不由你宣布**:以 `validate_info` / `save_info` 返回的校验报告为准;不完整就向用户要材料, + 禁止说"可以提交了" +- **不得臆造财务数据**:金额、日期、发票号、卡号读不清就标出来问用户,不要猜 +- **双通道冲突要复核**:PDF 文本层与读图结论不一致时(尤其编号、卡号、金额位数),两条都不许默认采信, + 重新放大该区域确认。已知文本层会把"标签"和"值"分段存储,且康熙部首类字符(`⽇` `⻔`)NFKC 后仍不变形 +- **不回显密码**,不把凭据写进任何产物或日志 +- 产物只写 `cases//`(该目录已 gitignore,含身份证号与卡号) +- `required: true` 的字段(含值为空串的备注类)**必须存在于 JSON 里**,这是刻意约束;用骨架别凭记忆造结构 + +## 尚未实现,不要承诺 + +浏览器填报(`browser_*`)、提交预览与执行(`submit_*`)还没做。用户要提交时如实说明该环节在建设中, +可临时用旧 Web 流程(代码在 git 提交 `5f57400` 里)。 + +## 参考 + +- `references/invoice-fields.md` — 七类票据的字段判读细则(火车票/支付截图/酒店/普通发票/事前申请单/出入库单/其他附件) +- `references/travel-info-fields.md` — 差旅报销数据:交通/住宿/会议费、支付方式、按人按天的补助清单 +- `references/normal-info-fields.md` — 普通报销数据:明细与支付方式 +- `.agents/docs/plans/MCP化改造方案-2026-10-19.md` — 架构决策与实测记录(含抽取质量闸门数据) diff --git a/src/infra/llm/prompts/invoice_system.md b/.agents/skills/reimbursement/references/invoice-fields.md similarity index 93% rename from src/infra/llm/prompts/invoice_system.md rename to .agents/skills/reimbursement/references/invoice-fields.md index 0cfe359..bb62b7e 100644 --- a/src/infra/llm/prompts/invoice_system.md +++ b/.agents/skills/reimbursement/references/invoice-fields.md @@ -1,3 +1,6 @@ +> 迁移说明(2026-10-19):原为内置 LLM 的系统提示词 `src/infra/llm/prompts/invoice_system.md`, +> 随 MCP 化改造改为宿主 Agent 的判读参考。字段语义与推理规则仍然有效; +> 其中「只输出 JSON」类措辞按 MCP 流程理解:JSON 交给对应工具落盘,完整性由 `mcp__arb__validate_info` 判定。 # 角色定义 你是一个严谨合规、零容错导向的财务文档信息提取助手。你以财务数据的精准性为第一原则,对待提取结果严肃审慎,并以直接、无冗余的方式交付结构化内容。你沟通极简,在不附加无关说明的前提下,准确返回完整的提取结果。 @@ -263,9 +266,9 @@ --- -## 最终输出要求 +## 输出方式 -- 严格只输出 JSON 字符串,不包含任何思考过程、解释文字、Markdown 标记或代码块包裹 +- 每张票据单独产出一个 JSON,用 `mcp__arb__case_save_extraction` 落盘(不是贴进聊天) - JSON 语法必须正确,无多余逗号、引号或注释 - 只输出与判断类型对应的字段,不得混入其他类型的字段 -- `invoice_type` 的值必须与实际判断类型一致 \ No newline at end of file +- `invoice_type` 的值必须与实际判断类型一致 diff --git a/src/infra/llm/prompts/normal_info_system.md b/.agents/skills/reimbursement/references/normal-info-fields.md similarity index 89% rename from src/infra/llm/prompts/normal_info_system.md rename to .agents/skills/reimbursement/references/normal-info-fields.md index b0494c1..9435097 100644 --- a/src/infra/llm/prompts/normal_info_system.md +++ b/.agents/skills/reimbursement/references/normal-info-fields.md @@ -1,3 +1,6 @@ +> 迁移说明(2026-10-19):原为内置 LLM 的系统提示词 `src/infra/llm/prompts/normal_info_system.md`, +> 随 MCP 化改造改为宿主 Agent 的判读参考。字段语义与推理规则仍然有效; +> 其中「只输出 JSON」类措辞按 MCP 流程理解:JSON 交给对应工具落盘,完整性由 `mcp__arb__validate_info` 判定。 # 角色定义 你是极度严谨合规的财务报销信息提取助手。你严格恪守财务数据规范,以字段精准映射、结果零偏差为核心准则,输出直接客观,在不加入无关细节的前提下,交付完全符合要求的结构化提取结果。 @@ -37,8 +40,6 @@ | `reimbursement_details` | object | 必填,必须且仅含 2 个子字段 | | `payment_methods` | array | 无数据时返回 `[]` | | `attachments` | array | 无数据时返回 `[]` | -| `can_submit` | boolean | 必填,信息完整且逻辑自洽时为 `true`,否则为 `false` | -| `suggestion` | string | 当 `can_submit` 为 `false` 时说明需补充的材料;为 `true` 时为空字符串 | ### 报销明细节点结构 @@ -65,9 +66,7 @@ "total_amount": 0 }, "payment_methods": [], - "attachments": [], - "can_submit": true, - "suggestion": "" + "attachments": [] } ``` @@ -135,7 +134,7 @@ ### 语义完整性校验 -提取完成后,需判断信息是否足够支撑填报。根据校验结果设置根节点的 `can_submit`(boolean)和 `suggestion`(string)字段。 +填完后按下列维度自查,但**结论不由你宣布**:完整性以 `mcp__arb__validate_info` 为准,缺什么直接向用户要材料。 **校验维度**: @@ -150,10 +149,6 @@ **一定要关注的** - `reimbursement_details` 和 `payment_methods` 两个的总金额应该一样,如果不一样,要么是缺发票,要么是缺支付记录,需要提醒用户 -**判定标准**: - -- `can_submit = true`:信息完整且逻辑自洽,`suggestion` 为空字符串 -- `can_submit = false`:存在信息缺失或逻辑矛盾,`suggestion` 说明需要用户补充什么材料 ### 示例 @@ -200,10 +195,10 @@ --- -## 最终输出要求 +## 输出方式 -- 严格只输出 JSON 字符串,不包含任何思考过程、解释文字、Markdown 标记或其他内容 -- JSON 语法必须正确,无多余逗号、引号等错误 +- JSON 通过 `mcp__arb__save_info` 落盘,不要把整坨 JSON 当作聊天内容贴给用户 +- JSON 语法必须正确,无多余逗号、引号或注释 - 严格遵守所有类型约束,任何违反均视为无效输出 - 日期统一使用 `YYYY-MM-DD` 格式 - 金额使用数字类型(非字符串) diff --git a/src/infra/llm/prompts/travel_info_system.md b/.agents/skills/reimbursement/references/travel-info-fields.md similarity index 92% rename from src/infra/llm/prompts/travel_info_system.md rename to .agents/skills/reimbursement/references/travel-info-fields.md index ee7e48b..6c72b77 100644 --- a/src/infra/llm/prompts/travel_info_system.md +++ b/.agents/skills/reimbursement/references/travel-info-fields.md @@ -1,3 +1,6 @@ +> 迁移说明(2026-10-19):原为内置 LLM 的系统提示词 `src/infra/llm/prompts/travel_info_system.md`, +> 随 MCP 化改造改为宿主 Agent 的判读参考。字段语义与推理规则仍然有效; +> 其中「只输出 JSON」类措辞按 MCP 流程理解:JSON 交给对应工具落盘,完整性由 `mcp__arb__validate_info` 判定。 # 角色定义 你是极度严谨合规的财务差旅信息提取助手。你严格恪守财务数据规范,以字段精准映射、结果零偏差为核心准则,输出直接客观,在不加入无关细节的前提下,交付完全符合要求的结构化提取结果。 @@ -40,8 +43,6 @@ | `payment_methods` | array | 无数据时返回 `[]` | | `subsidy_list` | array | 无数据时返回 `[]` | | `attachments` | array | 无数据时返回 `[]` | -| `can_submit` | boolean | 必填,信息完整且逻辑自洽时为 `true`,否则为 `false` | -| `suggestion` | string | 当 `can_submit` 为 `false` 时说明需补充的材料;为 `true` 时为空字符串 | ### 报销明细节点结构 @@ -72,9 +73,7 @@ }, "payment_methods": [], "subsidy_list": [], - "attachments": [], - "can_submit": true, - "suggestion": "" + "attachments": [] } ``` @@ -196,7 +195,7 @@ ### 语义完整性校验 -提取完成后,需判断信息是否足够支撑填报。根据校验结果设置根节点的 `can_submit`(boolean)和 `suggestion`(string)字段。 +填完后按下列维度自查,但**结论不由你宣布**:完整性以 `mcp__arb__validate_info` 为准,缺什么直接向用户要材料。 **校验维度**: @@ -217,10 +216,6 @@ - `reimbursement_details` 和 `payment_methods` 两个的总金额应该一样,如果不一样,要么是缺发票,要么是缺支付记录,需要提醒用户 - 用户一定要提供出差事情申请单 -**判定标准**: - -- `can_submit = true`:信息完整且逻辑自洽,`suggestion` 为空字符串 -- `can_submit = false`:存在信息缺失或逻辑矛盾,`suggestion` 说明需要用户补充什么材料 ### 示例 @@ -266,11 +261,11 @@ --- -## 最终输出要求 +## 输出方式 -- 严格只输出 JSON 字符串,不包含任何思考过程、解释文字、Markdown 标记或其他内容 -- JSON 语法必须正确,无多余逗号、引号等错误 +- JSON 通过 `mcp__arb__save_info` 落盘,不要把整坨 JSON 当作聊天内容贴给用户 +- JSON 语法必须正确,无多余逗号、引号或注释 - 严格遵守所有类型约束,任何违反均视为无效输出 - 日期统一使用 `YYYY-MM-DD` 格式 - 金额使用数字类型(非字符串) -- 计数使用整数类型 \ No newline at end of file +- 计数使用整数类型 diff --git a/.env.example b/.env.example index e95652f..6c4656d 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,8 @@ -# 系统 URL 配置(服务端专用,Web 用户无需配置) -# 复制此文件为 .env 并填入实际值 +# 系统 URL 覆盖(可选)——不设则使用 src/config.py 里的内置默认值 + +注意:项目已移除 python-dotenv,不会再自动读取 .env 文件。 +这些是真实的进程环境变量名:自己 export,或写进宿主的 MCP server env 配置。 +财务账号密码不放这里:DSH 会清洗 stdio 子进程的敏感环境变量,凭据只放 scripts/data/config.json。 SSO_LOGIN_URL=https://your-sso-url/login PORTAL_URL=https://your-portal-url/oshall @@ -7,7 +10,5 @@ REIMBURSE_URL=http://your-reimburse-host:8081 REIMBURSE_PAGE=/expen/common/common?v=4.0 TRAVEL_PAGE=/expen/travel/travel?v=4.0 -# LLM 配置 -LLM_MODEL=your-model-name -LLM_API_BASE=http://your-llm-host:port/v1 -LLM_API_KEY=your-api-key \ No newline at end of file +# 上传收件器访问码(也可用 --pin 参数或 config.json 的 upload_pin) +UPLOAD_PIN= diff --git a/AGENTS.md b/AGENTS.md index 50fb9f8..8492d8a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,86 +16,104 @@ last_reviewed: 2026-10-19 * **禁止使用表情文字**输出任何内容。 * `docs/` 目录存放面向开源用户、外部贡献者的公开文档。 * `.agents/` 目录存放维护规范、实施方案、经验总结等内部资料。 -* 每个文件夹下都有 `README.md` 说明该文件夹的作用和重要信息。 +* 报销业务的操作纪律在 `.agents/skills/reimbursement/SKILL.md`;工具契约在 `docs/MCP.md`。 ## 开发命令(必须使用 uv) -项目使用 `uv` 管理依赖,所有包版本锁定在 `uv.lock` 中。 +项目用 `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` | -| 运行 CLI 全流程(遗留,即将删除) | `make run` | `python tasks.py run` | +| MCP stdio 自检(起子进程握手列工具) | `make mcp-smoke` | `python tasks.py mcp-smoke` | +| 启动上传收件器 | `make upload` | `python tasks.py upload` | +| 打印 MCP 注册参数 | `make mcp` | `python tasks.py mcp` | | 清理缓存和虚拟环境 | `make clean` | `python tasks.py clean` | -**注意:** `tasks.py` 中的 `check` 命令使用 `&&` 连接,Windows PowerShell 不支持 `&&`,但 `tasks.py` 内部已处理为单行字符串。 +没有 CLI 产品入口,也没有 Web 界面:业务入口只有 `python -m src.mcp.server`(stdio), +工具在宿主里表现为 `mcp__arb__`。 + +**注意:** `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 mode** — `uv run mypy src/main.py` (strict=true, warn_return_any, ignore_missing_imports) -4. **deptry** — `uv run deptry .` (检测未声明、未使用、过时依赖) +3. **MyPy strict** — `uv run python -m mypy src`(整个 src 都在 strict 下,不只是某个入口文件) +4. **deptry** — `uv run deptry .`(检测未声明、未使用、过时依赖) -### pre-commit 钩子(仅 Ruff) +`uv run mypy` 在本机偶尔报 "uv trampoline failed to canonicalize script path",用 `uv run python -m mypy` 代替。 -`.pre-commit-config.yaml` 配置了两个 hook: -- `ruff --fix` — lint 并自动修复 -- `ruff-format` — 格式化 +### 提交纪律(血的教训) -**注意:** MyPy 和 deptry **不在** pre-commit 中,需要手动运行 `make check`。 +* **门禁退出码要单独确认再提交**,别把"跑检查"和"提交"写在同一条命令链里——检查失败也能走完流程。 +* 本仓库 `pre-commit` 只挂 ruff,但它会 **stash 未暂存改动**再跑,跑完才还原。 + 若这个过程被中断(超时/杀进程),未暂存的文件会**被回滚成 HEAD 版本**且 `git fsck` 捞不回来。 + 本项目改造过程中真发生过一次,四个文件的改动靠锚点重放才恢复。 +* 因此:提交前先把工作区改动 `git add` 干净,或用 `--no-verify` 提交并手动跑完等价检查(在提交说明里写明)。 ## 项目架构(MCP 服务器模式) -**唯一业务入口是 MCP 服务器**:`python -m src.mcp.server`(stdio),工具名前缀 `mcp__arb__`。 -编排由宿主 Agent(DSH)完成,本项目只提供工具与确定性内核。 +**唯一业务入口**:`python -m src.mcp.server`(stdio)。编排(读图判断、缺料追问、是否提交)由宿主 Agent 完成, +本项目**不调用任何大模型**,只提供确定性工具与内核。 -> 过渡期状态:`src/main.py`(CLI)与 `src/web`(Flask,已移除 Agent 端点,仅剩上传/表格编辑/下载/提交) -> 仍在工作,但**已确定删除**,不要把它们的代码当参考实现,也不要往里加功能。`.invoice_cache` 键名约定两边共用,那部分可以信。 - -- `src/mcp/` — 工具与协议层(`server.py` 注册与启动,`tools/` 按职能分模块,`schemas.py` 抽取字段定义,`envelope.py` 统一返回信封) +- `src/mcp/` — 工具与协议层:`server.py` 注册与启动,`tools/` 按 materials / extraction / analysis / documents 分模块, + `schemas.py` 抽取字段定义(单一真源),`envelope.py` 统一 `{ok, ...}` / `{ok:false, reason, next_action}` 信封 - `src/cases.py` — 会话目录 `cases//` 的唯一实现:材料、收件箱、产物与**阶段推导**(阶段由产物推导,不存在状态机文件) -- `src/core/` — 确定性内核:`extraction/`(缓存读写、PDF 渲染与文本层)、`matching/`(金额匹配 + `runner.py` 从缓存匹配)、`validation/`(声明式规则校验) -- `src/infra/` — 基础设施:`browser/`(Playwright 填报)、`documents/`(CSV/Word/Excel/PDF) +- `src/core/` — 确定性内核:`extraction/`(缓存读写、PDF 渲染与文本层、自动转正)、`matching/`(金额匹配 + `runner.py` 从缓存匹配)、`validation/`(声明式规则校验) +- `src/infra/` — 基础设施:`browser/`(Playwright 填报,工具层待接入)、`documents/`(CSV、申购单 Excel COM)、`com_thread.py`(COM 单线程串行 actor) - `src/upload/` — 上传收件器(**独立进程** `python -m src.upload.server`):手机扫码/PC 上传落 `cases//inbox/`,不含业务编排,绝不由 MCP 子进程拉起 -已删除:`src/agent/`(手写状态机与 SSE 事件)。`required: true` 的字段(含值为空串的备注类字段) -必须存在于 info JSON 中——这是刻意约束;用 `mcp__arb__info_skeleton` 取骨架再填值,别凭记忆造结构。 -宿主侧调 MCP 工具时**路径一律用正斜杠**(`D:/Code/...`),反斜杠会被 DSH 的参数绑定层拒收。 +已删除:`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` 取骨架再填值,别凭记忆造结构。 ### 数据流关键产物 -| 文件 | 生成阶段 | 作用 | -|------|---------|------| -| `.invoice_cache/.json` | 宿主 Agent 抽取后 `mcp__arb__case_save_extraction` 落盘 | 单张发票/支付记录的结构化数据 | -| `match_result.json` | `mcp__arb__match_payments` | 支付截图与发票的关联关系 | -| `travel_info.json` / `normal_info.json` | 宿主 Agent 综合,`mcp__arb__save_info` 落盘 | 差旅/普通报销所需的全部结构化数据 | -| `invoice_summary.csv` / `payment_records.csv` | `mcp__arb__match_payments` | 发票级与支付级汇总(出库单、申购单取数) | +| 文件 | 生成者 | 作用 | +|------|--------|------| +| `pages//p01.png` + `text.txt` + `meta.json` | `case_render_pages` | 给宿主读的页面与 PDF 文本层(含转正角度) | +| `.invoice_cache/.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` | 低值易耗品申购单 | ### 缓存机制 -MCP 会话:`cases//.invoice_cache/`(**新**) -遗留只读:`scripts/data/`(CLI)与 `src/web/uploads//`(Web),两者即将删除 - -缓存文件与源文件同名(如 `发票1.pdf` → `.invoice_cache/发票1.json`),后续步骤均从缓存读取。删除缓存后下次处理会重新提取。 +会话缓存在 `cases//.invoice_cache/`,文件名与源文件同名(`发票1.pdf` → `.invoice_cache/发票1.pdf.json`)。 +缓存 JSON 的 `_source_file` + 内容指纹决定"是否陈旧",`case_status` 会报 stale 项;删缓存即重抽。 ## 重要约束 -* **Windows-only**:易耗品出库单填写依赖 Microsoft Word + COM (`pywin32`),仅 Windows 可用 -* **浏览器自动化**:使用 Playwright,填报时会打开 Chromium,请勿手动干扰 -* **敏感信息**:`scripts/config.json` 含登录凭据,勿提交到公开仓库 -* **发票类型区分**:差旅发票(高铁票/酒店住宿)不生成易耗品出库单,走差旅报销流程;普通发票生成出库单 +* **Windows-only**:申购单依赖 Microsoft Excel + COM(`pywin32`)。所有 Office COM 调用必须走 + `src/infra/com_thread.py` 的单线程 actor——并发驱动 Word/Excel 会互抢 ActiveDocument 与模态框 +* **浏览器自动化**:Playwright,填报时会打开 Chromium,请勿手动干扰;Playwright sync API 有 greenlet 线程亲和, + 接入 MCP 时同样要固定到专属单线程(P3) +* **多进程共存**:宿主会保留旧代 stdio 子进程,subagent/teammate 也会各起一条 transport—— observed 4 个 PID 并存。 + 因此任何"独占资源"(浏览器、Office)都要有磁盘锁,别假设只有一个服务器实例 +* **敏感信息**:`scripts/data/config.json` 含登录凭据、`cases/` 含身份证号与卡号,两者都已在 `.gitignore`, + 绝不要提交,也不要回显密码 +* **发票类型区分**:差旅发票(高铁票/酒店住宿)走差旅报销流程,不生成申购单;普通发票生成低值易耗品申购单 -## Web 服务(遗留,即将删除) +## 上传收件器 ```bash -uv run python src/web/app.py -# 访问 http://localhost:5000 +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 # 额外访问码 ``` -Web 端浏览器填报以无头模式运行。会话产物存放在 `src/web/uploads//`,每次上传生成独立会话。 +页面地址由 `case_create` 返回(含每会话令牌)。安全边界:令牌强制(无令牌 403)、只写不读(无列表无下载)、 +扩展名白名单、25MB/文件、200 文件/会话、PIN 可选。二维码由服务端用 segno 生成,零外部 CDN。 ## 测试 @@ -103,4 +121,6 @@ Web 端浏览器填报以无头模式运行。会话产物存放在 `src/web/upl make test # pytest + coverage report (term-missing) ``` -测试目录:`tests/`,配置在 `pyproject.toml` 中 (`testpaths = ["tests"]`, `pythonpath = ["."]`)。 +测试目录 `tests/`,配置在 `pyproject.toml`(`testpaths`、`pythonpath=["."]`)。 +COM 相关路径(申购单)不进单测——需要本机装有 Excel,改动后用真实生成验证(约 2-5 秒)。 +改完 MCP 工具记得 `make mcp-smoke`(它验的是 wire 上的工具清单,不是进程内对象)。 diff --git a/README.md b/README.md index 5d0fdd4..9d12068 100644 --- a/README.md +++ b/README.md @@ -1,474 +1,125 @@ -# 财务报销自动化 +--- +last_reviewed: 2026-10-19 +--- -自动从 PDF 发票或图片中提取信息,生成发票汇总表与易耗品出库单,并可选在财务系统中自动填报报销单。 +# 财务报销自动化(MCP 服务器) -**支持发票类型区分**:系统自动识别高铁票、酒店住宿等差旅发票与普通发票。差旅发票不生成易耗品出库单,走差旅报销流程;普通发票生成出库单,走普通报销流程。 +把发票、支付截图、出差申请单变成财务系统要的结构化数据,顺带生成低值易耗品申购单。 + +**它不是一个应用,而是一个 MCP 服务器**:读图、判断、决定下一步交给宿主 Agent(如 DSH), +本项目只做确定性的活——PDF 渲染转正、金额匹配、规则校验、Excel 产物、浏览器填报。 + +> 为什么改:原来自带一整套前端 + 内置 LLM + 手写状态机(提取-校验-修正循环、轮次上限、SSE 事件流)。 +> 灵活性太差——想换个模型、加一类票据、改流程都得动代码。2026-10-19 起全部交给宿主 Agent 编排。 + +## 它做什么 + +| 环节 | 谁做 | +|------|------| +| 看发票、判断类型、抽字段 | **宿主 Agent**(多模态模型) | +| 页面渲染、PDF 文本层、自动转正 | 本项目(PyMuPDF) | +| 发票与刷卡记录金额匹配 | 本项目(确定性贪心 + 业务约束,输出待复核清单) | +| 完整性校验 | 本项目(`config/validation_rules.json` 声明式规则) | +| 申购单 Excel、财务系统填报 | 本项目(COM / Playwright) | +| 流程编排、缺料追问、要不要提交 | **宿主 Agent** | + +## 快速开始 + +```bash +uv sync # 依赖(要求 Python >= 3.12) +uv run playwright install chromium # 填报要用(P3 工具接入后需要) +``` + +配好 `scripts/data/config.json`(参考 `config.example.json`:工号、密码、默认报销人、公务卡号、人员编号、上传 PIN)。 +该目录整体在 `.gitignore` 里,别提交。 + +把服务器注册到宿主(DSH profile patch): + +```yaml +- insert: + - id: mcp-arb + name: '@deepseek-ai/dsh-mcp-client' + config: + serverName: arb + transport: stdio + command: '<项目根>/.venv/Scripts/python.exe' + args: ['-m', 'src.mcp.server'] + cwd: '<项目根>' +``` + +自检(真的起一个子进程走 stdio 握手): + +```bash +make mcp-smoke # 或 python tasks.py mcp-smoke +``` + +手机扫码上传材料(独立进程,必须单独起): + +```bash +make upload # 默认 0.0.0.0:5055;--local 只监听 127.0.0.1;--pin 加访问码 +``` + +`case_create` 会返回带令牌的手机页地址,上传落 `cases//inbox/`,之后由 Agent 收取。 +上传服务是只写不读的:没有列表也没有下载接口,扩展名白名单 + 25MB + 每会话 200 个文件封顶。 + +## 一次报销长什么样 + +``` +case_create → case_add_files → case_render_pages → case_read_text / 读 PNG + → case_save_extraction(逐张)→ match_payments(处理 review_required) + → info_skeleton → save_info → doc_requisition +``` + +17 个工具的参数表与返回约定:[docs/MCP.md](docs/MCP.md)。操作步骤(财务口径):[docs/报销操作指南.md](docs/报销操作指南.md)。 ## 项目结构 ``` -├── pyproject.toml # 项目配置(依赖、工具链) -├── uv.lock # 依赖锁定文件 -├── Makefile # 任务脚本(跨平台) -├── tasks.py # 任务脚本(Windows 兼容) -├── .pre-commit-config.yaml # pre-commit 钩子配置 -├── .env.example # 环境变量示例(SSO 地址、LLM 配置等) -├── config.example.json # 用户配置示例 -├── 易耗品、出库单.doc # 易耗品出库单 Word 模板 -├── src/ -│ ├── __init__.py # 包初始化 / 日志器 -│ ├── config.py # 配置加载 -│ ├── exceptions.py # 异常定义 -│ ├── pipeline.py # CLI 流程编排 -│ ├── pipeline_core.py # CLI/Web 公共管道逻辑 -│ ├── main.py # CLI 入口 -│ ├── agent/ # Agent 调度模块 -│ │ ├── orchestrator.py # 总调度入口 -│ │ ├── coordinator.py # 校验-修正循环 -│ │ ├── session.py # 状态机与会话数据 -│ │ └── events.py # SSE 事件发射 -│ ├── core/ # 核心业务逻辑 -│ │ ├── extraction/ # 信息提取 -│ │ │ ├── extractor.py # 编排入口:串联文件扫描 → 提取 → 分类 -│ │ │ └── llm_extractor.py # LLM 多模态信息提取 -│ │ ├── matching/ # 金额匹配 -│ │ │ └── matcher.py # 支付记录与发票关联 -│ │ └── validation/ # 校验模块 -│ │ └── validator.py # 声明式校验器 -│ ├── infra/ # 基础设施层 -│ │ ├── browser/ # 浏览器自动化 -│ │ │ ├── base.py # BaseBot 基类 -│ │ │ ├── travel.py # 差旅报销填报流程 -│ │ │ └── normal.py # 普通报销填报流程 -│ │ ├── documents/ # 文档处理 -│ │ │ ├── invoice.py # 发票数据模型 + CSV 工具 -│ │ │ ├── pdf.py # PDF 图片渲染 -│ │ │ ├── consumable.py # 易耗品出库单填写(Word COM) -│ │ │ └── requisition.py # 低值易耗品申购单填写(Excel COM) -│ │ └── llm/ # LLM 接口 -│ │ ├── prompt.py # 提示词加载 -│ │ └── prompts/ # 提示词模板文件 -│ └── web/ # Web 界面模块 -│ ├── app.py # Flask 应用入口 -│ ├── routes.py # 路由定义 -│ ├── pipeline_web.py # Web 管道逻辑 -│ ├── sse_handler.py # SSE 日志流处理 -│ ├── templates/ -│ │ ├── index.html # PC 端主页 -│ │ └── mobile_upload.html # 移动端扫码上传 -│ ├── static/ -│ │ ├── css/ # 样式文件 -│ │ └── js/ # 前端脚本 -│ └── uploads/ # 按会话隔离的上传与产物目录 -├── scripts/ # CLI 数据目录 -│ ├── data/ # 发票源文件、config.json 与 .invoice_cache 缓存 -│ └── test_*.py # 测试脚本 -├── tests/ # 测试目录 -├── docs/ # 用户文档(API 说明、操作指南等) -├── images/ # 浏览器调试截图 -└── *.pdf / *.jpg / *.png # 发票 PDF 或图片(CLI 模式,放在 scripts/data/) +src/ +├── mcp/ 工具与协议层(server.py 注册;tools/ 分材料·抽取交接·分析·文档) +├── cases.py 会话目录 cases// 的唯一实现:阶段由产物推导,无状态机 +├── upload/ 扫码上传收件器(独立进程,绝不由 MCP 子进程拉起) +├── core/ 确定性内核:extraction(缓存·渲染·转正)matching validation +├── infra/ browser(Playwright 填报)documents(CSV·申购单 COM) +├── config.py scripts/data/config.json + 环境变量 +└── exceptions.py 异常定义(MCP 层转成 ok:false 信封,不抛穿协议) +.agents/skills/reimbursement/ 宿主侧操作手册与字段判读参考(从旧 LLM 提示词迁移) +docs/MCP.md 工具契约 +config/validation_rules.json 声明式校验规则 +cases/ 会话产物(gitignore,含个人信息) ``` -## 声明式校验器 - -`src/core/validation/validator.py` 采用**规则配置与校验引擎分离**的设计模式,支持声明式定义校验规则: - -### 设计特点 - -| 特性 | 说明 | -|------|------| -| **声明式配置** | 校验规则以数据结构形式定义,无需编写代码 | -| **统一路径定位** | 使用 `path` 统一定位字段,如 `["basic_info", "travel_purpose"]` | -| **自定义校验函数** | 支持为字段定义自定义校验逻辑(日期格式、正数检查等) | -| **数组元素校验** | 支持校验数组字段的最小元素数量及每个元素的必填字段 | -| **向后兼容** | 支持简单格式 `["field1", "field2"]` 和详细格式 `{"path": [...], "custom_check": ...}` | - -### 规则配置示例 - -```python -# 差旅报销校验规则 -TRAVEL_VALIDATION_RULES = { - "fields": [ - {"path": ["basic_info", "travel_purpose"], "description": "出差事由"}, - {"path": ["basic_info", "start_date"], "custom_check": _is_valid_date}, - ], - "arrays": [ - { - "path": ["payment_methods"], - "min_items": 1, # 至少1条支付记录 - "element_fields": [ - {"path": ["card_date"], "description": "刷卡日期"}, - {"path": ["card_amount"], "custom_check": _is_positive_number}, - ], - }, - ], -} -``` - -### 校验规则类型 - -| 规则类型 | 用途 | 关键字段 | -|----------|------|----------| -| `fields` | 顶层单值字段校验 | `path`, `custom_check`, `check_empty` | -| `arrays` | 数组字段校验 | `path`, `min_items`, `element_fields` | - -### 内置校验函数 - -- `_is_valid_date(value)` — 检查日期格式是否为 `YYYY-MM-DD` -- `_is_positive_number(value)` — 检查值是否为正数 - -### 扩展自定义校验 - -```python -# 定义自定义校验函数 -def check_vehicle_type(value): - valid_types = ["飞机", "火车", "汽车", "打车"] - return isinstance(value, str) and value.strip() in valid_types - -# 在规则中使用 -{"path": ["vehicle_type"], "custom_check": check_vehicle_type} -``` - -## 数据流 - -```mermaid -flowchart TB - PDF[PDF 发票 / 图片] --> Extract[extractor 多模态提取] - Extract --> Cache[(.invoice_cache/*.json)] - - Cache --> Classify{发票类型分类} - Classify -->|差旅发票| Travel[高铁票 / 酒店住宿] - Classify -->|普通发票| General[普通发票] - Classify -->|支付记录| Payment[支付截图] - Classify -->|申请单| Application[出差事前申请单] - - Travel --> Matcher[matcher 金额匹配] - Payment --> Matcher - Matcher --> MatchResult[(match_result.json)] - - Cache --> TravelLLM[LLM 差旅信息提取] - MatchResult --> TravelLLM - TravelLLM --> TravelInfo[(travel_info.json)] - - Cache --> NormalLLM[LLM 普通发票信息提取] - MatchResult --> NormalLLM - NormalLLM --> NormalInfo[(normal_info.json)] - - TravelInfo -->|差旅基本信息| Bot_T[infra/browser/travel.py
差旅填报流程] - TravelInfo -->|报销明细| Bot_T - TravelInfo -->|支付方式| Bot_T - TravelInfo -->|补助清单| Bot_T - TravelInfo -->|附件清单| Bot_T - Bot_T --> Submit_T[差旅报销提交] - - NormalInfo -->|报销说明| Bot_G[infra/browser/normal.py
普通填报流程] - NormalInfo -->|发票总数/金额| Bot_G - NormalInfo -->|支付方式| Bot_G - NormalInfo -->|附件清单| Bot_G - Bot_G --> Submit_G[普通报销提交] - - General --> CSV[(invoice_summary.csv)] - CSV --> Fill[consumable.py] - Fill --> Doc[易耗品、出库单.doc] -``` - -### 关键中间产物 - -| 文件 | 生成阶段 | 作用 | -|------|---------|------| -| `.invoice_cache/*.json` | extractor 提取 | 单张发票/支付记录/申请单的结构化数据 | -| `match_result.json` | matcher 匹配 | 支付截图与发票的关联关系(按金额匹配) | -| `travel_info.json` | LLM 差旅信息提取 | 综合发票缓存 + 匹配结果,生成差旅报销所需的全部结构化数据 | -| `normal_info.json` | LLM 普通发票信息提取 | 综合普通发票 + 匹配结果,生成普通报销所需的全部结构化数据 | -| `invoice_summary.csv` | extractor 提取 | 普通发票汇总(用于生成易耗品出库单) | - -### bot 模块架构 - -`infra/browser/` 包负责浏览器自动化填报,仅接收已提取的信息并执行填报操作,不承担信息提取职责: - -| 模块 | 职责 | -|------|------| -| `infra/browser/base.py` | `BaseBot` 基类:浏览器生命周期、登录、导航、截图 | -| `infra/browser/travel.py` | 差旅填报流程:基本信息 → 差旅明细 → 支付方式 → 补助清单 → 附件上传 | -| `infra/browser/normal.py` | 普通填报流程:基本信息 → 总明细 → 支付方式 → 附件上传 | -| `infra/browser/__init__.py` | 入口函数:`run_bot()` / `run_bot_web()`,负责类型判断和流程路由 | - -## 环境要求 - -- **Python 3.12+** -- **uv** 包管理器([安装指南](https://docs.astral.sh/uv/getting-started/installation/)) -- Windows(易耗品出库单填写依赖 Microsoft Word + COM,仅 Windows 可用) - -## 快速开始 - -### 1. 安装依赖 +## 开发 ```bash -# 同步所有依赖(运行时 + 开发工具) -make install -# Windows 上等效命令: -python tasks.py install +make install # uv sync + pre-commit +make check # ruff check + format --check + mypy src + deptry +make test # pytest + 覆盖率 +make upload # 上传收件器 +make mcp-smoke # stdio 自检 ``` -项目使用 `uv` 管理依赖,所有包版本锁定在 `uv.lock` 中,确保可复现。 +测试覆盖:配置加载、发票分类与 CSV、金额匹配、会话模型、分析工具、骨架与规则一致性、申购单取值。 -| 依赖 | 用途 | +## 平台与风险 + +- **Windows-only**:申购单依赖 Microsoft Excel + `pywin32` COM;并发驱动 Office 会互抢, + 所以 COM 调用统一排队在单线程 actor(`src/infra/com_thread.py`) +- 填报会打开 Chromium,自动化期间别手动点 +- `cases/` 与 `scripts/data/` 含身份证号、卡号、凭据,已在 `.gitignore`;发布前请自查历史 +- **提交前务必手动跑门禁**:本仓库 pre-commit 只跑 ruff,但它会 stash 未暂存改动, + 中途中断会把工作区文件回滚成 HEAD(本项目改造过程中真发生过,见方案文档 §11) + +## 还没做完 + +向财务系统填报与提交(`browser_*`、`submit_*`)正在建设;在那之前本项目只到"产物齐备 + 校验通过"。 +Word 版易耗品出库单已下线(模板自 2026-08-31 起不在仓库,功能此前一直静默失败)。 + +## 文档索引 + +| 文档 | 内容 | |------|------| -| PyMuPDF | PDF 图片渲染(供多模态 LLM 使用) | -| llama-index | LLM 信息提取(发票识别、差旅信息提取) | -| playwright | 财务系统浏览器自动化 | -| flask | Web 服务 | -| pywin32 | 填写 Word 出库单(`fill_consumable_doc`) | - -### 2. 准备数据(CLI 模式) - -将发票 PDF 或图片(`.jpg`、`.png`、`.webp`、`.bmp`)放在 `scripts/data/` 目录下。 - -### 3. 配置 - -编辑 `scripts/config.json`(参考 `config.example.json`): - -```json -{ - "username": "你的工号", - "password": "你的密码", - "default_name": "默认报销人姓名", - "default_card_no": "默认公务卡号", - "default_person_id": "默认人员编号", - "consumable_storage": "物料存储地" -} -``` - -| 字段 | 说明 | -|------|------| -| `username` / `password` | 信息门户登录凭据 | -| `default_name` | 默认报销人姓名 | -| `default_card_no` | 默认公务卡号 | -| `default_person_id` | 默认人员编号(工号) | -| `consumable_storage` | 出库单「存放地点」列默认值(默认: `躬行楼 C205`) | - -**服务端配置**(SSO 地址、报销系统 URL、LLM 参数)通过环境变量提供,有默认值,一般无需修改: - -| 环境变量 | 默认值 | 说明 | -|----------|--------|------| -| `SSO_LOGIN_URL` | `https://tyrz.fynu.edu.cn/sso/login` | SSO 登录地址 | -| `PORTAL_URL` | `https://tyrz.fynu.edu.cn/oshall` | 统一信息平台地址 | -| `REIMBURSE_URL` | `http://210.45.32.214:8081` | 报销系统地址 | -| `REIMBURSE_PAGE` | `/expen/common/common?v=4.0` | 普通报销页面路径 | -| `TRAVEL_PAGE` | `/expen/travel/travel?v=4.0` | 差旅报销页面路径 | -| `LLM_MODEL` | `qwen-vl-max` | LLM 模型名称 | -| `LLM_API_BASE` | `http://localhost:8080/v1` | LLM API 地址 | -| `LLM_API_KEY` | `lm-studio` | LLM API 密钥 | - -### 4. 运行(CLI) - -```bash -# 全流程(发票提取 → 浏览器填报) -make run -# Windows 等效: -python tasks.py run - -# 仅执行某一步 -uv run python src/main.py --step invoice # 仅发票提取 -uv run python src/main.py --step submit # 仅浏览器填报 - -# 覆盖配置中的登录凭据 -uv run python src/main.py -u 工号 -p 密码 -``` - -### 5. 填写易耗品出库单(CLI) - -需已生成 `invoice_summary.csv`,且本机已安装 **Microsoft Word**: - -```bash -uv run python -m src.infra.documents.consumable -uv run python -m src.infra.documents.consumable --csv invoice_summary.csv --doc "易耗品、出库单.doc" -uv run python -m src.infra.documents.consumable --config scripts/config.json # 指定配置文件 -uv run python -m src.infra.documents.consumable --no-backup # 不生成 .doc.bak 备份 -``` - -填写规则概要: - -- 表头「日期」使用**填写当天**的日期(非发票开票日期) -- 从 `spec_model` 解析品名、规格、单位、数量、单价;`card_amount` 写入金额列 -- 单价/金额保留两位小数;表格内统一为 **宋体五号(10.5 磅)** -- 存放地点取自 `consumable_storage`(默认: `躬行楼 C205`);购货人/领用人签字、备注保持空白 - -### 6. 生成低值易耗品申购单(CLI) - -从 Web 会话目录的发票缓存读取普通发票,自动填入「低值易耗品申购」Excel 模板。 -需本机已安装 **Microsoft Excel**(通过 COM 操作,仅 Windows 可用)。 - -```bash -# 传入 Web 会话目录(含 .invoice_cache/ 与 agent_state.json) -uv run python -m src.infra.documents.requisition --session src/web/uploads/35c63986686b - -# 排除物品名称含指定关键词的发票(逗号分隔多个) -uv run python -m src.infra.documents.requisition --session src/web/uploads/0c9b7e7ef1ac --exclude 千斤顶,台钻 - -# 指定模板 / 输出路径 / 实验实训项目 -uv run python -m src.infra.documents.requisition --session <会话目录> --template 模板.xls --output 输出.xls --project "项目名称" -``` - -参数说明: - -| 参数 | 说明 | -|------|------| -| `--session` | Web 会话目录,脚本自动读取其中的 `.invoice_cache/` 与 `agent_state.json` | -| `--template` | 申购单 Excel 模板路径;默认取项目根目录下现有的 `低值易耗品申购*.xls` | -| `--output` | 输出文件路径;默认同目录生成带时间戳的新文件,不覆盖模板 | -| `--project` | 「对应实验实训项目」列内容;默认读 `agent_state.json` 的 `reimbursement_description` | -| `--exclude` | 排除物品名称含这些关键词的发票,逗号分隔 | - -填写规则概要: - -- 仅收录 `invoice_type=general` 的普通发票(支付截图/备注文件自动排除),按发票号去重 -- 物品名称去除 `*分类*` 前缀(如 `*半导体*半导体分立器件` → `半导体分立器件`) -- 规格:优先用发票提取的型号;无型号或为泛化名称(集成电路/电阻/连接器等)时,从名称提取封装/型号(如 `SOP-8`、`TO-220`、`STM32F103C8T6`),仍无法确定则填 `多品种` -- 数量固定 `1`,单价(含税) = 金额 = 发票价税合计 -- 末尾自动追加「合计」行 - -## 执行步骤说明 - -| 步骤 | 命令 | 说明 | -|------|------|------| -| 发票提取 | `--step invoice` | 扫描 `scripts/data/` 目录的 PDF 和图片,生成 `invoice_summary.csv` | -| 浏览器填报 | `--step submit` | 登录信息门户 → 报销系统 → 自动填单、上传附件 | - -> 全流程执行时数据在内存中流转,CSV 为参考产物。分步执行时,缓存数据会自动成为下一步的输入。 - -### 缓存机制 - -系统使用 JSON 缓存作为数据中转站,串联整个处理流程: - -``` -源文件 (PDF/图片) → LLM 多模态提取 → JSON 缓存 → 匹配/分类/填报 -``` - -| 缓存文件 | 说明 | -|----------|------| -| `<文件名>.json` | 每个源文件的 LLM 提取结果(发票信息、支付记录等) | -| `match_result.json` | 发票与支付记录的匹配结果 | -| `travel_info.json` | 差旅信息(事由、地点、时间等)提取结果 | - -缓存位置:CLI 模式为 `scripts/data/.invoice_cache/`,Web 模式为 `src/web/uploads//.invoice_cache/`。缓存文件与源文件同名(如 `发票1.pdf` 对应 `.invoice_cache/发票1.json`),后续步骤(金额匹配、发票分类、浏览器填报)均从缓存读取结构化数据。删除缓存后下次处理会重新提取。 - -### CSV 字段说明 - -发票级别 CSV(`invoice_summary.csv`)使用英文列名: - -| 列名 | 说明 | -|------|------| -| `index` | 行号 | -| `invoice_type` | 发票类型:`train` / `hotel` / `general` | -| `invoice_number` | 电子发票号码 | -| `invoice_date` | 开票日期 | -| `item_name` | 货物或应税劳务名称 | -| `spec_model` | 规格型号(差旅发票为出发站→到达站) | -| `total_amount` | 发票含税金额 | -| `seller_name` | 销方名称 | -| `departure` / `arrival` | 出发站 / 到达站(高铁票专用) | -| `train_no` / `ride_date` / `seat_class` | 车次 / 乘车日期 / 座位等级(高铁票专用) | -| `person_name` | 人员姓名 | -| `card_date` / `card_no` / `card_amount` | 刷卡日期 / 公务卡号 / 刷卡金额 | -| `remark` | 备注 | -| `person_id` | 工号 | - -## Web 服务 - -提供浏览器界面:上传文件 → 自动处理 → 在线编辑 → 下载产物 → 可选提交财务系统。 - -```bash -uv run python src/web/app.py -``` - -访问 `http://localhost:5000`。 - -### 推荐使用流程 - -1. 上传 PDF 或图片(或上传已有 CSV) -2. 填写配置(账号、密码、姓名、公务卡号、存放地点等),可上传 `config.json` 一键填充 -3. 点击 **开始处理** — 完成发票提取、生成 CSV,系统自动识别发票类型并分类统计 -4. **普通发票**:自动生成 **易耗品、出库单.doc**,可下载 -5. **差旅发票**(高铁票/酒店住宿):跳过出库单生成,直接进入差旅报销流程 -6. 在表格中核对、修改发票数据(提交财务系统前会自动保存) -7. 确认无误后点击 **提交到财务系统** - -### 处理流程 - -上传 PDF 或图片 → LLM 识别文档类型 → 结构化提取 → 分类处理 - -系统通过 LLM 多模态识别自动判断每张文档的类型,无需手动指定: - -| 文档类型 | 处理方式 | 状态 | -|----------|----------|------| -| 发票(高铁票/酒店住宿/普通发票) | 提取发票信息 → 金额匹配 → 分类 | 已实现 | -| 支付记录(刷卡截图) | 提取刷卡信息 → 与发票匹配 | 已实现 | -| 出差事前申请单 | 提取出差事由、地点、时间 | 已实现 | -| 飞机票 | 同高铁票处理流程 | 计划中 | - -### 功能一览 - -| 功能 | 说明 | -|------|------| -| 发票提取 | 上传 PDF 或图片后自动完成 | -| 发票类型自动分类 | 高铁票/酒店住宿/普通发票,自动分流处理 | -| 易耗品出库单 | 仅普通发票自动生成 Word,差旅发票跳过 | -| 表格在线编辑 | 处理完成后可修改 CSV 各字段;保存后重新生成出库单 | -| 财务系统填报 | 单独按钮触发,处理阶段不会自动提交 | -| 实时日志 | SSE 推送处理进度 | -| 配置上传 | 支持上传 `config.json` 填充表单 | -| 手机扫码上传 | 二维码打开移动端页面,拍照上传,PC 端轮询同步 | - -> Web 端浏览器填报以无头模式运行。未上传 PDF 或图片时,填报阶段会跳过附件上传。 -> 出库单生成需要 **Windows + Word + pywin32**;若失败,页面会显示具体原因,CSV 等其它产物仍可正常使用。 - -### 会话产物 - -每次上传生成独立会话,产物存放在 `src/web/uploads//`: - -| 产物 | 说明 | -|------|------| -| `invoice_summary.csv` | 发票汇总数据(含 LLM 识别结果) | -| `payment_records.csv` | 支付记录级别数据(含匹配结果) | -| `travel_applications.json` | 出差事前申请单数据(JSON 格式) | -| `易耗品、出库单.doc` | 自动填写的出库单(仅普通发票) | -| `config.json` | 当次会话配置 | -| `session.log` | 处理日志 | -| `result.json` | 处理结果 | -| `.invoice_cache/` | LLM 提取结果缓存(JSON 格式,避免重复处理) | - -会话缓存机制与 CLI 模式相同,缓存目录中的 JSON 数据是后续匹配、分类和填报的唯一数据来源。 - -接口说明见 [API.md](./API.md)。 - -## 开发任务 - -项目提供统一的任务脚本,支持跨平台使用: - -| 任务 | Makefile | tasks.py | 说明 | -|------|----------|----------|------| -| 安装依赖 | `make install` | `python tasks.py install` | 同步依赖 + 安装 pre-commit | -| 代码检查 | `make check` | `python tasks.py check` | Ruff lint + 格式化 + MyPy 类型检查 + deptry 依赖检查 | -| 运行测试 | `make test` | `python tasks.py test` | pytest + 覆盖率报告 | -| 运行 CLI | `make run` | `python tasks.py run` | 执行全流程 | -| 清理缓存 | `make clean` | `python tasks.py clean` | 删除虚拟环境和缓存 | - -## 代码质量 - -项目配置了完整的代码质量工具链: - -- **Ruff** — 快速 lint 检查和代码格式化(替代 flake8 + isort + black) -- **MyPy** — 严格模式类型检查(`strict = true`) -- **deptry** — 检测未声明、未使用、过时依赖 -- **pre-commit** — 提交前自动运行 Ruff 检查和格式化 - -所有检查通过后方可提交代码。 - -## 注意事项 - -- 浏览器填报时会打开或使用 Chromium,请勿手动干扰自动化流程 -- 调试截图保存在 `images/` 目录 -- 项目根目录需保留 `易耗品、出库单.doc` 模板;Web 每次从模板复制到会话目录再填写,不修改原模板 -- `scripts/config.json` 含敏感信息,请勿提交到公开仓库 -- **发票类型区分**:差旅发票(高铁票/酒店住宿)不会生成易耗品出库单,差旅报销填报流程已完整实现(含差旅信息提取、明细录入、支付方式、补助清单、附件上传) \ No newline at end of file +| [docs/MCP.md](docs/MCP.md) | 17 个工具的参数、返回信封、会话目录、注册方法 | +| [docs/报销操作指南.md](docs/报销操作指南.md) | 财务侧口径与材料准备要求 | +| [AGENTS.md](AGENTS.md) | 给 Agent 的项目操作指南(开发命令、约束、坑) | +| [.agents/docs/plans/MCP化改造方案-2026-10-19.md](.agents/docs/plans/MCP化改造方案-2026-10-19.md) | 改造决策、实测数据、踩坑记录 | diff --git a/config.example.json b/config.example.json index 9309afa..3d6397e 100644 --- a/config.example.json +++ b/config.example.json @@ -4,5 +4,5 @@ "default_name": "默认报销人姓名", "default_card_no": "默认公务卡号", "default_person_id": "默认人员编号", - "consumable_storage": "躬行楼 C205" + "upload_pin": "手机上传页访问码,可留空" } diff --git a/config/README.md b/config/README.md index bd2864a..ec01a4c 100644 --- a/config/README.md +++ b/config/README.md @@ -1,24 +1,31 @@ --- -last_reviewed: 2026-06-15 +last_reviewed: 2026-10-19 --- -# config — 配置文件目录 - -## 文件 +# config — 规则配置目录 | 文件 | 说明 | |------|------| -| `validation_rules.json` | 声明式校验规则配置:定义差旅和普通报销的必填字段、数组元素校验规则和自定义校验函数 | +| `validation_rules.json` | 声明式校验规则:差旅与普通报销的必填字段、数组元素规则、自定义校验函数 | -## validation_rules.json 结构 +## 结构 ```json { - "version": "1.0", - "custom_checks": { ... }, - "travel": { "fields": [...], "arrays": [...] }, - "normal": { "fields": [...], "arrays": [...] } + "version": "1.0", + "custom_checks": { ... }, + "travel": { "fields": [...], "arrays": [...] }, + "normal": { "fields": [...], "arrays": [...] } } ``` -校验引擎 `src/core/validation/validator.py` 在启动时读取此文件,若文件不存在则使用内置默认规则。 +引擎 `src/core/validation/validator.py` 启动时读取;文件缺失时用内置默认规则。 + +## 两条不要动的东西 + +- **`required: true` 是刻意约束**:包括值为空串的备注类字段,键必须存在。宿主动手写 JSON 容易漏, + 所以提供 `mcp__arb__info_skeleton` 从同一份规则生成骨架,而不是把 required 改成 false +- **`can_submit` / `suggestion` 不属于规则**:完整性结论只由校验报告给出, + 历史提示词里的这两个字段已在 MCP 化时删除,`save_info` 也会主动剥离 + +改规则后必须跑 `pytest`(有骨架与规则一致性的用例),否则骨架与规则会漂移。 diff --git a/docs/API.md b/docs/API.md deleted file mode 100644 index 510222e..0000000 --- a/docs/API.md +++ /dev/null @@ -1,856 +0,0 @@ ---- -last_reviewed: 2026-06-13 ---- - -# 财务报销自动化 — API 文档 - -> 基础地址: `http://localhost:5000` -> 启动: `uv run python src/web/app.py` - -## 总览 - -| # | 方法 | 路径 | 说明 | -|---|------|------|------| -| 1 | GET | `/` | PC 端主页 | -| 2 | GET | `/mobile/` | 移动端上传页面 | -| 3 | POST | `/api/session` | 创建会话 | -| 4 | POST | `/api/upload/` | 上传文件(PDF/图片) | -| 5 | GET | `/api/files/` | 列出会话目录中的文件 | -| 6 | GET | `/api/download//` | 下载生成的文件 | -| 7 | POST | `/api/mobile-upload/` | 移动端上传图片 | -| 8 | GET | `/api/config/` | 获取会话配置 | -| 9 | GET | `/api/data/` | 获取发票数据(JSON) | -| 10 | POST | `/api/save/` | 保存编辑后的发票数据 | -| 11 | POST | `/api/process/` | 启动管道处理(仅发票提取,不自动提交) | -| 12 | GET | `/api/logs/` | SSE 日志流 | -| 13 | POST | `/api/submit-financial/` | 提交到财务系统 | -| **14** | **GET** | **`/api/agent/state/`** | **获取 Agent 会话状态** | -| **15** | **POST** | **`/api/agent/process/`** | **启动 Agent 多轮处理(主入口)** | -| **16** | **POST** | **`/api/agent/supplement/`** | **补充文件后重新分析** | -| **17** | **POST** | **`/api/agent/user-supplement/`** | **通过文字补充信息** | -| **18** | **POST** | **`/api/agent/force-submit/`** | **强制提交,跳过校验** | - -> 加粗条目为 Agent 多轮校验流程新增接口。 - -### 入口选择建议 - -- **推荐使用** `/api/agent/process`:完整流程,包含发票提取、LLM 信息校验、自动提交财务系统。 -- **仅发票提取** `/api/process`:跳过 Agent 校验,只做文档解析和发票分类。适合调试发票提取本身,或仅需导出 CSV 的场景。 - ---- - -## 会话与目录 - -- 调用 `POST /api/session` 获得 `session_id` -- 该会话下所有文件存放在 `src/web/uploads//` -- 典型产物:`invoice_summary.csv`、`payment_records.csv`、`易耗品、出库单.doc`、`config.json`、`session.log`、`result.json`、`agent_state.json`、`agent_events.log`、`file_events.log`、`llm_stream.log` - ---- - -## 接口详情 - -### 1. 创建会话 - -``` -POST /api/session -``` - -**响应:** - -```json -{ "session_id": "a1b2c3d4e5f6" } -``` - ---- - -### 2. 上传文件(PDF/图片) - -``` -POST /api/upload/ -Content-Type: multipart/form-data -``` - -| 字段 | 类型 | 说明 | -|------|------|------| -| file | File | PDF 发票或支付截图 | - -**响应(成功):** - -```json -{ "ok": true, "filename": "1. 电容一批.pdf" } -``` - -**响应(失败):** - -```json -{ "error": "未选择文件" } -``` - -HTTP `400` - ---- - -### 3. 列出会话文件 - -``` -GET /api/files/ -``` - -**响应:** - -```json -{ - "files": [ - { "name": "1. 电容一批.pdf", "type": "pdf", "size": 12345 }, - { "name": "payment_01.jpg", "type": "image", "size": 67890 } - ], - "pdfs": ["1. 电容一批.pdf"], - "images": ["payment_01.jpg"] -} -``` - -`images` 包含扩展名:`.png`、`.jpg`、`.jpeg`、`.bmp`、`.webp`。 - ---- - -### 4. 下载文件 - -``` -GET /api/download// -``` - -**响应:** 文件二进制流,带 `Content-Disposition: attachment` 与 UTF-8 文件名。 - -| 扩展名 | Content-Type | -|--------|----------------| -| `.csv` | `text/csv; charset=utf-8` | -| `.doc` | `application/msword` | -| 其它 | `application/octet-stream` | - -**常见文件名:** - -| 文件名 | 说明 | -|--------|------| -| `invoice_summary.csv` | 发票汇总 | -| `payment_records.csv` | 支付记录 | -| `易耗品、出库单.doc` | 自动填写的出库单 | -| `travel_applications.json` | 差旅申请信息 | -| `result.json` | 处理结果 | -| `agent_state.json` | Agent 会话状态 | - -**错误:** - -```json -{ "error": "文件不存在" } -``` - -HTTP `404`。`filename` 仅允许会话目录内的文件名(防止路径穿越)。 - ---- - -### 5. 移动端上传页面 - -``` -GET /mobile/ -``` - -返回移动端 HTML 页面。 - ---- - -### 6. 移动端上传图片 - -``` -POST /api/mobile-upload/ -Content-Type: multipart/form-data -``` - -| 字段 | 类型 | 说明 | -|------|------|------| -| file | File | 图片文件 | - -逻辑与 `POST /api/upload/` 相同。 - ---- - -### 7. 获取会话配置 - -``` -GET /api/config/ -``` - -获取当前会话的配置,供前端回填表单。优先读取会话目录下的 `config.json`,未找到则使用项目全局配置。 - -**响应:** - -```json -{ - "username": "", - "password": "", - "default_name": "", - "default_card_no": "", - "default_person_id": "", - "consumable_storage": "" -} -``` - -注意:`password` 字段始终返回空字符串。 - ---- - -### 8. 获取发票数据 - -``` -GET /api/data/ -``` - -**响应:** - -```json -{ - "csv_filename": "payment_records.csv", - "fields": [ - "序号", "发票号码", "开票日期", "项目名称", "规格型号", - "价税合计", "销售方名称", "人员姓名", "刷卡日期", - "公务卡号", "刷卡金额", "备注", "工号" - ], - "data": [ - { - "__row": 0, - "序号": "1", - "发票号码": "26442000005432755951", - "价税合计": "2900.00" - } - ] -} -``` - -读取优先级:`payment_records.csv` → `invoice_summary.csv` → 任意 `.csv` 文件。 - -- `fields`:列顺序 -- `data[].__row`:内部行索引(保存时不需要提交,服务端按数组顺序写回) - -**错误:** `404` 未找到 CSV;`500` 读取失败。 - ---- - -### 9. 保存编辑后的发票数据 - -``` -POST /api/save/ -Content-Type: application/json -``` - -**请求体:** - -```json -{ - "csv_filename": "invoice_summary.csv", - "data": [ - { - "序号": "1", - "发票号码": "26442000005432755951", - "开票日期": "2026/05/18", - "项目名称": "...", - "规格型号": "...", - "价税合计": "2900.00", - "销售方名称": "...", - "人员姓名": "", - "刷卡日期": "2026/04/28", - "公务卡号": "", - "刷卡金额": "2850.00", - "备注": "", - "工号": "200000001" - } - ] -} -``` - -**响应(成功):** - -```json -{ - "ok": true, - "doc_ok": true, - "doc_url": "/api/download//%E6%98%93%E8%80%97%E5%93%81%E3%80%81%E5%87%BA%E5%BA%93%E5%8D%95.doc" -} -``` - -保存后会根据最新 CSV **重新生成** 出库单 Word(与会话 `config.json` 中的 `consumable_storage` 等配置一致)。 - -**纯差旅发票跳过出库单:** - -```json -{ - "ok": true, - "doc_ok": null, - "doc_skipped": true -} -``` - -**出库单生成失败:** - -```json -{ - "ok": true, - "doc_ok": false, - "doc_error": "出库单模板不存在,请将模板放在项目根目录" -} -``` - ---- - -### 10. 启动管道处理(仅发票提取) - -``` -POST /api/process/ -Content-Type: application/json -``` - -> 此接口仅执行发票提取和 LLM 识别,**不会**触发 Agent 多轮校验,也**不会**自动提交到财务系统。如需完整的 Agent 校验流程,请使用 `/api/agent/process`。 - -**请求体:** - -| 字段 | 类型 | 必填 | 说明 | -|------|------|------|------| -| username | string | 否 | 财务系统工号 | -| password | string | 否 | 登录密码 | -| default_name | string | 否 | 默认报销人姓名 | -| default_card_no | string | 否 | 默认公务卡号 | -| default_person_id | string | 否 | 默认人员编号 | -| consumable_storage | string | 否 | 出库单存放地点 | - -**处理内容:** - -1. 从会话目录 PDF 提取发票信息 → `invoice_summary.csv` -2. 对支付截图多模态 LLM 识别,回填刷卡字段 -3. 根据发票类型自动分类:差旅发票(高铁票/酒店住宿)不生成出库单;普通发票从模板复制并自动填写 - -配置会写入 `src/web/uploads//config.json`。 - -**响应(立即):** - -```json -{ "status": "started" } -``` - -处理在后台线程执行,进度与结果通过 `GET /api/logs/`(SSE)获取。 - -**SSE 完成时 `result` 示例(成功):** - -```json -{ - "ok": true, - "elapsed": "45.2s", - "invoice_count": 4, - "csv_url": "/api/download//invoice_summary.csv", - "travel_count": 2, - "general_count": 2, - "doc_url": "/api/download//%E6%98%93%E8%80%97%E5%93%81%E3%80%81%E5%87%BA%E5%BA%93%E5%8D%95.doc", - "doc_ok": true -} -``` - ---- - -### 11. SSE 日志流 - -``` -GET /api/logs/ -Accept: text/event-stream -``` - -每 0.5 秒轮询 4 个日志文件,通过文件 size 增量检测新内容: - -| 文件 | 内容 | -|------|------| -| `session.log` | 普通日志(extractor、llm_extractor、matcher、pipeline、bot、agent、validator 等模块) | -| `file_events.log` | 文件处理进度事件 | -| `llm_stream.log` | LLM 流式输出 | -| `agent_events.log` | Agent 调度事件 | - -检测到 `result.json` 存在时,读取后发送 `done` 事件并断开连接。 - -**SSE 超时:** 900 秒。 - ---- - -### 12. 提交到财务系统 - -``` -POST /api/submit-financial/ -``` - -**前置条件:** - -- 会话目录存在 `config.json`,否则返回 `400` -- 存在可用的发票 CSV(通常为 `invoice_summary.csv` 或 `payment_records.csv`) - -**说明:** - -- 前端一般在提交前调用 `/api/save` 保存表格修改 -- 根据发票类型选择填报模式:纯差旅发票走差旅报销流程,含普通发票走普通报销流程 - -**响应(立即):** - -```json -{ "status": "started" } -``` - -**SSE 完成示例:** - -```json -{ - "type": "done", - "result": { - "ok": true, - "submit_ok": true - } -} -``` - -失败时 `submit_ok: false`,`submit_error` 为错误描述。 - ---- - -## Agent 多轮校验流程 - -Agent 是系统的调度中枢,负责编排信息提取、规则校验、补充材料请求的完整流程。推荐使用 `/api/agent/process` 作为主入口。 - -### Agent 状态机 - -```mermaid -stateDiagram-v2 - [*] --> IDLE - IDLE --> EXTRACTING: 启动处理 - EXTRACTING --> READY: can_submit == true - EXTRACTING --> AWAITING_SUPPLEMENT: can_submit == false - EXTRACTING --> ERROR: 异常 / 轮次超限 - READY --> SUBMITTING: _emit_ready_and_submit() - SUBMITTING --> DONE: 财务提交完成 - AWAITING_SUPPLEMENT --> EXTRACTING: 用户补充文件/文字 - AWAITING_SUPPLEMENT --> READY: 用户强制提交 - - note right of EXTRACTING - LLM 提取 + validator 校验\n最多 3 次重试 - end note -``` - -### 13. 获取 Agent 会话状态 - -``` -GET /api/agent/state/ -``` - -**响应:** - -```json -{ - "session_id": "a1b2c3d4e5f6", - "state": "extracting", - "rounds": 1, - "max_rounds": 5, - "invoice_type": "travel", - "extracted_info": { ... }, - "validation_reports": [ ... ], - "user_supplements": [ ... ], - "error_message": "" -} -``` - -**状态值:** - -| 状态 | 含义 | -|------|------| -| `idle` | 初始状态 | -| `extracting` | LLM 正在分析文件 | -| `awaiting_supplement` | 信息不完整,等待用户补充 | -| `ready` | 信息完整,可以提交 | -| `submitting` | 正在提交到财务系统 | -| `done` | 流程结束 | -| `error` | 出错 | - ---- - -### 14. 启动 Agent 多轮处理(主入口) - -``` -POST /api/agent/process/ -Content-Type: application/json -``` - -**请求体:** - -| 字段 | 类型 | 必填 | 说明 | -|------|------|------|------| -| username | string | 否 | 财务系统工号 | -| password | string | 否 | 登录密码 | -| default_name | string | 否 | 默认报销人姓名 | -| default_card_no | string | 否 | 默认公务卡号 | -| default_person_id | string | 否 | 默认人员编号 | -| consumable_storage | string | 否 | 出库单存放地点 | - -**处理流程:** - -1. 发票提取(同 `/api/process`) -2. Agent 调度 LLM 分析提取结果 -3. validator 规则校验(最多 3 次校验-修正循环) -4. LLM 语义判断信息完整性(`can_submit` 字段) -5. 校验通过 → 自动提交到财务系统 -6. 校验未通过 → 等待用户补充材料 - -**响应(立即):** - -```json -{ "status": "started" } -``` - -**SSE done 事件 - 信息完整(成功提交):** - -```json -{ - "type": "done", - "result": { - "ok": true, - "agent_ready": true, - "submit_ok": true, - "round": 1, - "message": "信息完整,已自动提交到财务系统" - } -} -``` - -**SSE done 事件 - 信息完整但提交失败:** - -```json -{ - "type": "done", - "result": { - "ok": true, - "agent_ready": true, - "submit_ok": false, - "submit_error": "提交失败原因", - "round": 1, - "message": "校验通过但提交失败" - } -} -``` - -**SSE done 事件 - 需补充材料:** - -```json -{ - "type": "done", - "result": { - "ok": true, - "agent_ready": false, - "agent_state": "awaiting_supplement", - "round": 1, - "waiting_for_supplement": true - } -} -``` - -**SSE done 事件 - 处理失败:** - -```json -{ - "type": "done", - "result": { - "ok": false, - "error": "未提取到任何发票数据" - } -} -``` - ---- - -### 15. 补充文件后重新分析 - -``` -POST /api/agent/supplement/ -Content-Type: application/json -``` - -在 Agent 请求补充材料后,用户上传新文件并调用此接口触发重新分析。 - -**请求体:** - -```json -{ - "files": ["补充材料1.pdf", "补充材料2.jpg"] -} -``` - -**处理流程:** - -1. 记录补充文件 -2. 重新提取所有发票(包含新文件) -3. 加载上一轮分析结果作为历史上下文 -4. 重新执行 Agent 校验 - -**响应(立即):** - -```json -{ "status": "started" } -``` - -**SSE done 事件:** 同上(可能仍需补充或校验通过自动提交)。 - -**错误:** - -```json -{ "error": "未找到 Agent 状态" } -``` - -HTTP `404`(未先调用 `/api/agent/process` 或 Agent 状态已丢失)。 - ---- - -### 16. 通过文字补充信息 - -``` -POST /api/agent/user-supplement/ -Content-Type: application/json -``` - -用户通过对话方式提供补充信息,LLM 解析后更新已提取的信息并重新校验。 - -**请求体:** - -```json -{ - "text": "报销人是张三,公务卡号是 6228480402564890001" -} -``` - -**处理流程:** - -1. LLM 分析用户文字,提取需要更新的字段 -2. 合并到已提取的信息中 -3. 保存到缓存 -4. 重新执行 Agent 校验 - -**响应(立即):** - -```json -{ "status": "started" } -``` - -**SSE done 事件:** 同上(可能仍需补充或校验通过自动提交)。 - -**错误:** - -```json -{ "error": "请输入补充信息" } -``` - -HTTP `400`(文本为空)。 - ---- - -### 17. 强制提交,跳过校验 - -``` -POST /api/agent/force-submit/ -``` - -当 Agent 校验未通过或出错时,用户可选择强制提交,跳过所有校验直接提交到财务系统。 - -**处理流程:** - -1. 将 Agent 状态设为 `READY` -2. 执行财务提交 - -**响应(立即):** - -```json -{ "status": "started" } -``` - -**SSE done 事件:** - -```json -{ - "type": "done", - "result": { - "ok": true, - "submit_ok": true, - "submit_error": null - } -} -``` - ---- - -## SSE 事件类型 - -### Agent 事件 - -通过 `agent_events.log` 轮询推送: - -| 事件类型 | 数据结构 | 触发条件 | -|---|---|---| -| `agent_state_change` | `{type, state, round, attempt, message}` | 状态切换 | -| `agent_ready` | `{type, round, message}` | 双重校验通过 | -| `agent_request_supplement` | `{type, round, missing_fields, missing_materials, semantic_issues, suggestion}` | 校验未通过 | -| `agent_supplement_received` | `{type, files}` | 收到用户补充 | -| `agent_force_submit` | `{type, message}` | 用户强制提交 | -| `agent_extract_status` | `{type, state, round, attempt, message}` | 校验-修正循环中的每次尝试结果 | -| `agent_error` | `{type, message}` | 提取失败 | -| `agent_max_rounds` | `{type, message}` | 达到最大轮次 | - -### 文件进度事件 - -通过 `file_events.log` 轮询推送: - -| 事件类型 | 数据结构 | 触发条件 | -|---|---|---| -| `file_progress` | `{type, file, status, summary?, error?}` | 文件处理状态变更 | - -`status` 取值: `processing` / `done` / `cached` / `error` - -### LLM 流式事件 - -通过 `llm_stream.log` 轮询推送: - -| 事件类型 | 数据结构 | 触发条件 | -|---|---|---| -| `llm_stream` | `{type, phase, text?}` | LLM 输出流 | - -`phase` 取值: `start` / `reasoning` / `chunk` / `end` / `error` - -### 完成事件 - -SSE 检测到 `result.json` 后直接发送: - -```json -{ - "type": "done", - "result": { ... } -} -``` - ---- - -## 错误码汇总 - -| HTTP | 场景 | -|------|------| -| 400 | 参数缺失、未找到配置、文本为空等 | -| 404 | `session_id` 不存在、文件不存在、Agent 状态丢失 | -| 500 | CSV 读取失败、服务未初始化等内部错误 | - -统一错误体: - -```json -{ "error": "错误描述" } -``` - ---- - -## 发票类型分类 - -系统自动将发票分为两类,影响出库单生成和后续报销流程: - -| 类型 | 判断依据 | 出库单 | 报销流程 | -|------|----------|--------|----------| -| 差旅发票 | 高铁票、酒店住宿等 | 不生成 | 差旅报销 | -| 普通发票 | 其他(办公用品、耗材等) | 自动生成 | 普通报销 | - -`/api/process` 和 `/api/save` 的响应中 `travel_count` / `general_count` 即为分类统计。 - ---- - -## 端到端流程 - -```mermaid -sequenceDiagram - participant F as 前端 - participant S as SSE连接 - participant B as 后端线程 - participant A as Agent调度器 - - F->>B: POST /api/session - B-->>F: session_id - - F->>B: POST /api/upload/{sid} (多次) - F->>B: POST /api/agent/process/{sid} - B-->>F: {status: "started"} - - F->>S: GET /api/logs/{sid} - - Note over B,A: 后台线程启动 - B->>A: extract_invoices() - S-->>F: file_progress (processing/done) - S-->>F: llm_stream (start/chunk/end) - S-->>F: agent_state_change (extracting) - - Note over A: LLM 提取 + validator 校验
最多 3 次重试 - - alt 信息完整 - A->>A: state → READY - A->>A: _emit_ready_and_submit() - S-->>F: agent_ready - S-->>F: done (submit_ok=true) - F->>F: es.close() - else 信息不完整 - A->>A: state → AWAITING_SUPPLEMENT - S-->>F: agent_request_supplement - S-->>F: done (waiting_for_supplement=true) - F->>F: es.close() - - F->>B: 上传补充文件或输入文字 - alt 文件补充 - F->>B: POST /api/agent/supplement/{sid} - else 文字补充 - F->>B: POST /api/agent/user-supplement/{sid} - end - B-->>F: {status: "started"} - F->>S: GET /api/logs/{sid} - - Note over A: 重新分析 + 校验 - - alt 仍不完整 - S-->>F: agent_request_supplement - S-->>F: done (waiting=true) - F->>F: es.close() - Note over F: 可继续补充或强制提交 - else 完整 - A->>A: state → READY - A->>A: _emit_ready_and_submit() - S-->>F: agent_ready - S-->>F: done (submit_ok=true) - F->>F: es.close() - end - - alt 强制提交 - F->>B: POST /api/agent/force-submit/{sid} - B-->>F: {status: "started"} - F->>S: GET /api/logs/{sid} - S-->>F: agent_force_submit - S-->>F: done - F->>F: es.close() - end - end - - F->>B: GET /api/data/{sid} - B-->>F: fields + data - F->>B: POST /api/save/{sid} - Note over B: 更新 CSV,重新生成出库单 - B-->>F: doc_url - - F->>B: GET /api/download/{sid}/易耗品、出库单.doc -``` - ---- - -## 相关 CLI - -不经过 Web、在本地直接填写出库单: - -```bash -uv run python -m src.infra.documents.consumable --csv invoice_summary.csv --doc "易耗品、出库单.doc" -``` - -详见 [README.md](./README.md)。 \ No newline at end of file diff --git a/docs/MCP.md b/docs/MCP.md new file mode 100644 index 0000000..a118ff0 --- /dev/null +++ b/docs/MCP.md @@ -0,0 +1,304 @@ +--- +last_reviewed: 2026-10-19 +--- + +# MCP 接口文档 + +本项目的**唯一业务入口**是 MCP 服务器:`python -m src.mcp.server`(stdio 传输),工具名在宿主里表现为 `mcp__arb__`。 +没有 Web 界面,没有 CLI 产品入口,本项目也不调用任何大模型——**读图与综合判断由宿主 Agent 完成**, +本项目只提供确定性工具(渲染、金额匹配、规则校验、文档生成、浏览器填报)。 + +## 注册到宿主 + +DSH(profile patch,`~/.dsh/profiles//cordis.patch.yml`): + +```yaml +- insert: + - id: mcp-arb + name: '@deepseek-ai/dsh-mcp-client' + config: + serverName: arb + transport: stdio + command: 'D:/Code/AI Agent/自动报销系统/.venv/Scripts/python.exe' + args: ['-m', 'src.mcp.server'] + cwd: 'D:/Code/AI Agent/自动报销系统' +``` + +> 注意:DSH 对 stdio 子进程做了环境变量清洗(抹掉 `*KEY|PASSWORD|SECRET|TOKEN` 与全部 `DSH_*`), +> 所以财务系统凭据只能来自 `scripts/data/config.json`,不能靠环境变量注入。 + +通用 MCP 客户端等价配置: + +```json +{ "mcpServers": { "arb": { "command": "<项目根>/.venv/Scripts/python.exe", "args": ["-m", "src.mcp.server"], "cwd": "<项目根>" } } } +``` + +改完 Python 代码需要让宿主重连(DSH 会热插拔新工具条目,但**不会**重载已运行子进程里的代码; +杀掉 stdio 子进程即可自动重启并重同步工具)。 + +## 返回信封 + +所有工具返回统一信封,宿主判类型看 `ok`: + +```json +{ "ok": true, "case_id": "...", "next_action": "..." } +{ "ok": false, "reason": "缺少 payment_records.csv", "next_action": "先 mcp__arb__match_payments 生成汇总" } +``` + +- **不抛异常穿透协议**:文件缺失、参数不合法、COM 失败都转成 `ok: false` + `reason` + `next_action`。 +- `next_action` 是给 Agent 的下一步指令,通常是具体工具名,照做即可。 +- 部分客户端会把结果包在 `content[0].text`(JSON 字符串)里,同时提供 `structuredContent`;两者内容一致。 + +## 会话目录 + +一个报销任务 = 一个 `cases//` 目录(`case_id` 形如 `20261019-6月毕业采集`)。 +**阶段完全由产物推导**,没有状态机文件,可随时断电重来、可人工改文件后继续。 + +``` +cases// +├── case.json 名称、类型、上传令牌(切勿提交) +├── config.json 可选,会话级凭据覆盖(白名单字段) +├── <材料>.pdf|jpg 发票、支付截图、申请单原件 +├── inbox/ 手机扫码上传落地点,case_add_files(source="inbox") 收取 +├── pages//p01.png 渲染页面 + text.txt(PDF 文本层)+ meta.json(dpi/转正角/字符数) +├── .invoice_cache/*.json 单张票据抽取结果(宿主产出,本服务规范化后落盘) +├── match_result.json 匹配结果 +├── invoice_groups.json 差旅/普通分组与计数 +├── invoice_summary.csv 发票级汇总 +├── payment_records.csv 支付记录汇总(申购单取数) +├── travel_applications.json 出差申请单 +├── travel_info.json 差旅报销结构化数据 +├── normal_info.json 普通报销结构化数据 +├── artifacts/ 申购单等文档产物 +├── screenshots/ (P3 落地后出现)填报过程截图 +└── submit_preview.json (P4 落地后出现)提交前快照与摘要 +``` + +`cases/` 整个目录都在 `.gitignore` 里:发票含身份证号与卡号。 + +## 标准流程 + +1. `case_create` → 记下 `case_id` 与返回的 `upload_page`(手机扫码上传页) +2. `case_add_files`(本地路径用**正斜杠**)或 `case_add_files(source="inbox")` 收手机上传 +3. `case_render_pages` → `text_chars>0` 先 `case_read_text`,否则读 `pages/**.png` +4. 逐张按 `extraction_spec` 出 JSON → `case_save_extraction`;核对后小改用 `case_patch_extraction` +5. `match_payments` → **逐条处理 `review_required`**(未匹配、发票总额小于支付总额、一对多凑额) +6. `info_skeleton` 取骨架 → 填值 → `save_info`(自动跑校验);`validate_info` 可反复试算不落盘 +7. 普通发票再 `doc_requisition` 生成申购单 +8. 每步之后 `case_status` 复查;`case_reset(scope)` 清派生产物重跑 + +## 硬约束 + +- **不得回显密码**,不得把凭据写进产物或日志 +- **不得臆造财务数据**:拿不准的字段标出来问用户,禁止自己宣布"可以提交" +- 产物只写 `cases//` +- 文本层与读图结论冲突时(尤其编号、卡号、金额位数)必须复核,不要默认信任某一路 + +## 尚未实现(不要调用,也不要在回答里承诺) + +浏览器填报(`browser_*`)、提交预览与执行(`submit_*`)。需要这些步骤时告知用户该环节还在建设中, +可临时用旧的 Web 流程(代码在提交 `5f57400` 里)。 + +--- + +# 工具参考 + +以下内容由运行中的服务器 schema 自动生成,与代码同源(`scripts/mcp_smoke.py` 会打印同一份清单)。 + +## 会话与材料 + +每个报销任务是一个会话目录 `cases//`,阶段由产物推导。 + +### `mcp__arb__case_create` + +新建报销会话目录,返回 case_id 与目录路径。 + + name 是本次报销的人类可读名称(如「6月毕业采集」); + invoice_type 可选 travel|normal,未定时留空,抽取后由发票分类决定。 + +| 参数 | 类型 | 必填 | 默认 | 说明 | +|------|------|------|------|------| +| `name` | string | 是 | — | — | +| `invoice_type` | null/string | 否 | — | — | + +### `mcp__arb__case_list` + +列出所有报销会话及其推导阶段、计数与待收文件数。 + +无参数。 + +### `mcp__arb__case_status` + +单个会话的完整态势:材料数、抽取完成/缺失/陈旧、匹配、信息、产物、收件箱、下一步建议。 + +| 参数 | 类型 | 必填 | 默认 | 说明 | +|------|------|------|------|------| +| `case_id` | string | 是 | — | — | + +### `mcp__arb__case_add_files` + +把材料拷入会话目录。 + + paths 传绝对路径列表(从手机下载目录、微信文件目录等任意位置取文件); + source='inbox' 表示收取手机扫码上传落在 inbox 的文件。 + +| 参数 | 类型 | 必填 | 默认 | 说明 | +|------|------|------|------|------| +| `case_id` | string | 是 | — | — | +| `paths` | array/null | 否 | — | — | +| `source` | null/string | 否 | — | — | + +### `mcp__arb__case_inbox` + +列出手机/PC 上传进来但尚未进入流程的文件。 + +| 参数 | 类型 | 必填 | 默认 | 说明 | +|------|------|------|------|------| +| `case_id` | string | 是 | — | — | + +### `mcp__arb__case_reset` + +清理派生产物以便重跑。scope=extractions|match|info|pages|artifacts|all。 + +| 参数 | 类型 | 必填 | 默认 | 说明 | +|------|------|------|------|------| +| `case_id` | string | 是 | — | — | +| `scope` | string | 否 | `"extractions"` | — | + +## 抽取交接 + +渲染页面与读文本层由宿主 Agent 自己判断,本服务不调用任何模型。 + +### `mcp__arb__case_render_pages` + +为材料准备可读取的页面图片与 PDF 文本层。 + + 返回每项材料的页 PNG 路径、文本层字符数与自动转正角度。 + 有文本层(text_chars > 0)时应优先读文本层,比读图更准且省 token。 + +| 参数 | 类型 | 必填 | 默认 | 说明 | +|------|------|------|------|------| +| `case_id` | string | 是 | — | — | +| `files` | array/null | 否 | — | — | +| `dpi` | integer | 否 | `150` | — | +| `force` | boolean | 否 | `false` | — | + +### `mcp__arb__case_read_text` + +读取某材料渲染时提取的 PDF 文本层;无文本层时返回空。 + +| 参数 | 类型 | 必填 | 默认 | 说明 | +|------|------|------|------|------| +| `case_id` | string | 是 | — | — | +| `file` | string | 是 | — | — | + +### `mcp__arb__extraction_spec` + +返回抽取字段定义。kind 省略给全部文档类型;'info' 给 travel_info/normal_info 骨架。 + +| 参数 | 类型 | 必填 | 默认 | 说明 | +|------|------|------|------|------| +| `kind` | null/string | 否 | — | — | + +### `mcp__arb__case_save_extraction` + +把单张票据的抽取结果写入会话缓存(按 .json 约定)。 + + 日期会规范成 YYYY-MM-DD、金额规范成两位小数字符串; + 返回 warnings 提示必填缺失,但不阻断写入(完整性由 arb_validate_info 判定)。 + +| 参数 | 类型 | 必填 | 默认 | 说明 | +|------|------|------|------|------| +| `case_id` | string | 是 | — | — | +| `source_file` | string | 是 | — | — | +| `data` | object | 是 | — | — | + +### `mcp__arb__case_patch_extraction` + +局部修正已落盘的抽取结果(人工核对后改字段用)。 + + 只合并传入的键,返回每个被改字段的 from/to,避免静默覆盖; + 未在 schema 中出现的键会告警但仍写入(有些字段是给下游自定义用的)。 + +| 参数 | 类型 | 必填 | 默认 | 说明 | +|------|------|------|------|------| +| `case_id` | string | 是 | — | — | +| `source_file` | string | 是 | — | — | +| `patch` | object | 是 | — | — | + +## 匹配与校验 + +匹配与校验都是确定性代码;结论以 `validate_info` 为准,不要自己宣布可提交。 + +### `mcp__arb__match_payments` + +按缓存做发票与支付记录的金额匹配,并写出 CSV 与 invoice_groups.json。 + + 返回 matched / unmatched_invoices / unmatched_payments / review_required。 + 未匹配与一对多凑额项必须复核,不要盲信贪心结果。 + +| 参数 | 类型 | 必填 | 默认 | 说明 | +|------|------|------|------|------| +| `case_id` | string | 是 | — | — | +| `tolerance` | null/number | 否 | — | — | + +### `mcp__arb__info_skeleton` + +按校验规则生成「形状完整、值为空」的可填骨架。 + + 规则里 required 为 true 的字段(含 remark 这类备注)要求键必须存在, + 所以别凭记忆拼 JSON:先取骨架再逐个填值。骨架与 validation_rules.json 同源,不会漂移。 + +| 参数 | 类型 | 必填 | 默认 | 说明 | +|------|------|------|------|------| +| `info_type` | string | 是 | — | — | + +### `mcp__arb__save_info` + +写 travel_info 或 normal_info 到缓存,并立刻跑一次规则校验。 + + info_type 取 travel|normal。完整性以返回的 validation 为准, + 不要自己在 data 里塞 can_submit。 + +| 参数 | 类型 | 必填 | 默认 | 说明 | +|------|------|------|------|------| +| `case_id` | string | 是 | — | — | +| `info_type` | string | 是 | — | — | +| `data` | object | 是 | — | — | + +### `mcp__arb__validate_info` + +校验报销信息完整性。省略 draft 时校验缓存里已存的 info。 + + 也可传 draft 先试校验不落盘,适合改字段时反复迭代。 + +| 参数 | 类型 | 必填 | 默认 | 说明 | +|------|------|------|------|------| +| `case_id` | string | 是 | — | — | +| `info_type` | null/string | 否 | — | — | +| `draft` | null/object | 否 | — | — | + +### `mcp__arb__rules_get` + +返回声明式校验规则(config/validation_rules.json),供补字段前对照。 + +| 参数 | 类型 | 必填 | 默认 | 说明 | +|------|------|------|------|------| +| `info_type` | null/string | 否 | — | — | + +## 文档产物 + +Excel COM,需本机装有 Microsoft Excel;仅普通发票需要。 + +### `mcp__arb__doc_requisition` + +生成低值易耗品申购单(Excel,仅普通发票)。 + + exclude 传物品名关键词可剔除不参与申购的发票;project 覆盖「对应实验实训项目」, + 默认取 normal_info 的报销事由。需要 Windows + 已安装 Microsoft Excel。 + +| 参数 | 类型 | 必填 | 默认 | 说明 | +|------|------|------|------|------| +| `case_id` | string | 是 | — | — | +| `exclude` | array/null | 否 | — | — | +| `project` | null/string | 否 | — | — | diff --git a/docs/README.md b/docs/README.md index e2fad72..dca1e35 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,18 +1,21 @@ --- -last_reviewed: 2026-06-11 +last_reviewed: 2026-10-19 --- # docs — 公开文档目录 -本目录存放面向项目用户和外部贡献者的公开文档及说明文件。 +本目录存放面向项目用户和外部贡献者的公开文档。 ## 文档清单 | 文件 | 说明 | |------|------| -| `API.md` | Web API 完整接口文档:路由、请求/响应格式、SSE 日志流、错误码、端到端流程 | -| `报销操作指南.md` | 面向最终用户的操作步骤说明 | +| `MCP.md` | MCP 工具完整契约:注册方法、返回信封、会话目录、标准流程、17 个工具的参数表 | +| `报销操作指南.md` | 面向最终用户的报销口径与材料准备说明 | + +> 旧的 `API.md`(Flask 路由/SSE 文档)已随 Web 界面删除;需要历史接口形态时看 git 提交 `5f57400`。 ## 文档边界 -维护规范、实施方案、经验总结等内部资料统一放置在 `.agents/` 目录下,不混入本目录。 \ No newline at end of file +维护规范、实施方案、经验总结等内部资料统一放在 `.agents/`;宿主 Agent 的报销操作纪律在 +`.agents/skills/reimbursement/SKILL.md`。 diff --git a/docs/报销操作指南.md b/docs/报销操作指南.md index 318cdbf..66bd2cb 100644 --- a/docs/报销操作指南.md +++ b/docs/报销操作指南.md @@ -1,211 +1,101 @@ --- -last_reviewed: 2026-06-09 +last_reviewed: 2026-10-19 --- # 阜阳师范大学财务报销系统 - 自动化操作指南 -> 适用场景:日常报销录入(支持 Web 界面和 CLI 两种方式) -> 最后更新:2026-06-09 +> 适用场景:日常报销录入。入口是 MCP 服务器,编排由宿主 Agent(如 DSH)完成;工具参数表见 [MCP.md](./MCP.md)。 +> 本文讲财务侧口径、页面要素与材料要求。文中账号、卡号、姓名一律是占位示例——**真实值不要写进仓库任何位置**。 --- ## 一、系统概览 ``` -整体流程: - 信息门户(SSO登录) → 财务系统入口 → 单点登录页 → 网络报销 → 日常报销录入 -(tyrz.fynu.edu.cn) (点击"财务系统") (新标签页) (a:has(img)) (/expen/common/common) - -目标系统: http://210.45.32.214:8081 -用户: 张三 (工号: xxxxxxx) +(tyrz.fynu.edu.cn) (点击「财务系统」) (新标签页) (a:has(img)) (/expen/common/common) ``` -### 关键 URL - -| 系统 | URL | 说明 | -|------|-----|------| +| 系统 | 地址 | 说明 | +|------|------|------| | SSO 登录 | `https://tyrz.fynu.edu.cn/sso/login` | 统一认证入口 | | 信息门户 | `https://tyrz.fynu.edu.cn/oshall` | 登录后跳转目标 | -| 报销系统 | `http://210.45.32.214:8081` | 网络报销主系统 | -| 日常报销录入 | `/expen/common/common?v=4.0` | 目标录入页面 | +| 报销系统 | `http://<财务系统内网地址>:8081` | 网络报销主系统 | +| 日常报销录入 | `/expen/common/common?v=4.0` | 普通报销目标页 | +| 差旅报销 | `/expen/travel/travel?v=4.0` | 差旅报销目标页 | + +地址来自 `scripts/data/config.json`,也可用环境变量覆盖(`SSO_LOGIN_URL` 等,见 `.env.example`; +项目**不会**自动读取 `.env` 文件,需要自己导出或写进宿主的 env 配置)。 --- -## 二、两种使用方式 +## 二、使用方式 -### 方式一:Web 界面(推荐) +没有 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`) -```bash -uv run python src/web/app.py -# 访问: http://localhost:5000 -``` - -### 方式二:CLI 命令行 - -适合脚本化、批量处理。 - -```bash -# 全流程(发票提取 → LLM 识别 → 浏览器填报) -uv run python src/main.py - -# 分步执行 -uv run python src/main.py --step invoice # 仅发票提取 -uv run python src/main.py --step submit # 仅浏览器填报 - -# 覆盖登录凭据 -uv run python src/main.py -u 200000001 -p "your_password" -``` +自检:`make mcp-smoke` 会真起 stdio 子进程握手并列出 17 个工具。 --- -## 三、数据准备 +## 三、材料准备 -### 3.1 发票数据 CSV +| 材料 | 必需 | 说明 | +|------|------|------| +| 发票 PDF / 图片 | 是 | 电子发票尽量给 PDF:有文本层,准确率明显高于读图 | +| 支付截图 | 是 | 公务卡刷卡记录;要能看清**商户**、日期、金额、卡号后四位 | +| 出差事前申请单 | 差旅必需 | 缺它财务不认,Agent 会主动索要 | +| 其他附件 | 视情况 | 会议通知、合同等 | -上传 PDF 发票文件和支付截图,系统自动完成: -1. 从 PDF 提取发票信息 -2. 多模态 LLM 识别支付截图中的刷卡信息 -3. 生成 `invoice_summary.csv` +### 发票类型分流 -### 3.2 发票类型分类 +| 类型 | 判断依据 | 产物 | 报销流程 | +|------|----------|------|----------| +| 差旅发票 | 高铁票、机票、酒店住宿等 | 不生成申购单 | 差旅报销(含按人按天补助) | +| 普通发票 | 其他(办公用品、耗材、元件等) | 低值易耗品**申购单** | 普通报销(一条总明细 + 逐笔支付) | -系统自动将发票分为两类,影响后续处理流程: - -| 类型 | 判断依据 | 出库单 | 报销流程 | -|------|----------|--------|----------| -| 差旅发票 | 高铁票、酒店住宿等 | 不生成 | 差旅报销 | -| 普通发票 | 其他(办公用品、耗材等) | 自动生成 | 普通报销 | - -### 3.3 附件文件 - -PDF 发票文件需与支付截图配对上传。系统通过金额匹配自动关联发票和支付记录。 +> Word 版「易耗品出库单」已下线:模板自 2026-08-31 起不在仓库,那条路径此前一直静默失败。 --- -## 四、Web 界面操作流程 +## 四、Agent 操作流程 -### 4.1 启动服务 +| 步 | 做什么 | 用什么 | +|----|--------|--------| +| 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` | -```bash -uv run python src/web/app.py -# 访问: http://localhost:5000 -``` +**`ok: true` 的含义有限**:`match_payments` 成功只代表贪心跑完。一对多凑额、发票总额小于支付总额、 +未匹配项都会进 `review_required`,必须逐条确认——等额发票尤其容易配错。 -### 4.2 上传文件 +**什么时候算完成**:`validate_info` 报告 `valid: true` 且无业务约束违反。缺料就向用户要材料; +Agent 不许自己宣布「可以提交了」。 -1. **发票 PDF**:点击或拖拽上传 PDF 文件(支持多选) -2. **支付截图**:点击或拖拽上传图片文件(支持多选) -3. **手机扫码上传**:扫描页面二维码,通过手机拍照上传支付截图 +### 产物位置 -### 4.3 配置信息 +全部在 `cases//`(整个目录已 gitignore,含身份证号与卡号): -填写以下配置项(也可通过上传 `config.json` 快速填充): - -| 字段 | 说明 | +| 产物 | 说明 | |------|------| -| 账号 | 财务系统工号 | -| 密码 | 登录密码 | -| 默认姓名 | 报销人姓名 | -| 公务卡号 | 公务卡卡号 | -| 人员编号 | 人员编号 | -| 存放地点 | 出库单存放地点 | - -### 4.4 开始处理 - -点击"开始处理"按钮,系统自动执行: - -1. **发票提取**:从 PDF 提取发票信息 -2. **LLM 信息提取**:多模态 LLM 识别支付截图中的刷卡信息 -3. **发票分类**:自动区分差旅发票和普通发票 -4. **出库单生成**:普通发票自动生成易耗品出库单(差旅发票跳过) - -处理过程中可在日志面板实时查看进度。 - -### 4.5 编辑发票数据 - -处理完成后,发票数据以可编辑表格形式展示: - -- 直接点击单元格即可编辑 -- 修改后点击"提交到财务系统"时自动保存 -- 保存后会自动重新生成出库单 - -### 4.6 下载文件 - -处理完成后下载: - -| 文件 | 说明 | -|------|------| -| `invoice_summary.csv` | 发票汇总数据 | -| `易耗品、出库单.doc` | 自动填写的出库单(仅普通发票) | - -### 4.7 提交到财务系统 - -确认数据无误后,点击"提交到财务系统"按钮,系统自动: - -1. 登录信息门户 -2. 进入财务系统 -3. 创建报销单 -4. 填写基本信息 -5. 录入报销明细 -6. 录入支付方式 -7. 上传附件 +| `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/低值易耗品申购_.xls` | 申购单,拿去资产/财务口径核对 | --- -## 五、CLI 执行流程 - -### 5.1 全流程执行 - -```bash -uv run python src/main.py -``` - -执行步骤: - -``` -Step 1: 发票提取 - ├── 扫描 PDF 发票文件 - ├── 提取发票信息 - ├── 生成 invoice_summary.csv - └── 自动分类(差旅/普通) - -Step 2: LLM 信息提取 - ├── 扫描支付截图 - ├── 多模态 LLM 识别刷卡信息 - ├── 回填 CSV 刷卡字段 - └── 更新 invoice_summary.csv - -Step 3: 报销提交 - ├── 登录信息门户 - ├── 进入财务系统 - ├── 创建报销单 - ├── 填写基本信息 - ├── 录入报销明细 - ├── 录入支付方式 - ├── 上传附件 - └── 提交(需手动确认) -``` - -### 5.2 分步执行 - -```bash -# 仅发票提取 -uv run python src/main.py --step invoice - -# 仅浏览器填报 -uv run python src/main.py --step submit -``` - -### 5.3 上传目录 - -CLI 模式自动发现 `src/web/uploads/` 下最新的会话文件夹,该文件夹包含上传的 PDF 和图片。 - ---- - -## 六、页面元素速查 +## 五、页面元素速查 ### 基本信息页 @@ -247,7 +137,7 @@ CLI 模式自动发现 `src/web/uploads/` 下最新的会话文件夹,该文 | 字段 | 选择器 | 操作 | |------|--------|------| | 增加按钮 | `#insertAcc` | click | -| 附件类型 | `#fjlx` | select_option → '1'(发票) | +| 附件类型 | `#fjlx` | select_option → 1(发票) | | 附件说明 | `#fpsmxx` | fill | | 文件上传 | `#file` | set_input_files | | 确定按钮 | `#cjtj` | click | @@ -257,198 +147,137 @@ CLI 模式自动发现 `src/web/uploads/` 下最新的会话文件夹,该文 | 操作 | 选择器 | |------|--------| | 提交按钮 | `#submit` | -| 提交按钮(备用) | `#submit2` | +| 提交按钮(备用) | `#submit2` | --- -## 七、数据流向 +## 六、数据流向 ``` -PDF 发票 + 支付截图 - │ - ▼ extract_invoices() - │ - ├── 读取 PDF 发票 - ├── 提取发票信息 - ├── 自动分类(差旅/普通) - └── 输出: invoice_summary.csv - │ - ▼ enrich_with_llm() - ├── 多模态 LLM 识别支付截图 - ├── 回填刷卡字段 - └── 更新 invoice_summary.csv - │ - ▼ fill_consumable_from_template() - ├── 仅普通发票 - ├── 从模板复制出库单 - └── 自动填写出库单 - │ - ▼ run_bot() - ├── 登录信息门户 - ├── 进入财务系统 - ├── 创建报销单 - ├── 填写基本信息 - ├── 录入报销明细 - ├── 录入支付方式 - ├── 上传附件 - └── 提交 +材料(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/ 申购单 + │ 浏览器填报与提交:P3/P4 建设中,当前没有工具 + ▼ +财务系统报销单 ``` --- -## 八、关键设计说明 +## 七、关键设计说明 -### 8.1 浏览器启动 +### 7.1 浏览器启动 -系统使用 Playwright 的 `sync_playwright()` 启动 Chromium 浏览器,支持 `headless` 模式。每次运行均启动新浏览器实例,需重新登录。可通过 `headless` 参数控制是否显示浏览器窗口。 +Playwright `sync_playwright()` 启动 Chromium,**默认有头**(`headless=False`)——提交不可逆,得看清楚在点什么。 +SSO 需要扫码或验证码时人工完成即可,流程会继续。每次运行都要重新登录。 -### 8.2 明细录入策略 +### 7.2 明细录入策略 -系统采用"一条总明细"策略:将所有发票合并为一条报销明细,报销总金额为所有发票刷卡金额之和,单据数为发票总张数。支付方式则逐张发票分别录入,每张发票对应一条支付记录。 +普通报销用「一条总明细」:所有发票合并成一条,报销总金额 = 各笔支付之和,单据数 = 发票张数; +支付方式逐笔录入,一笔一条。差旅按人去程/返程分条录交通费,补助按人按天算。 -### 8.3 经济科目选择 +### 7.3 经济科目与项目 -系统在经济科目弹窗中固定选择第 3 行。如需更改科目,修改 `rows[2]` 的索引即可。 +经济科目弹窗固定选第 3 行(`rows[2]`),项目弹窗固定选第 1 行。要改就改这两个索引, +位置在 `src/infra/browser/travel.py` 与 `normal.py`。 -### 8.4 项目选择 +### 7.4 网络报销链接动态获取 -系统在项目选择弹窗中固定选择第 1 行。如需更改项目,修改 `first_row` 的选择逻辑即可。 +单点登录页的「网络报销」链接参数每次不同,因此用 `a:has(img[src*="wlbx"])` 定位并读 `href`,不硬编码 URL。 -### 8.5 网络报销链接动态获取 +### 7.5 手机上传 -单点登录页的"网络报销"链接参数每次不同,系统通过 `a:has(img[src*="wlbx"])` 精确定位链接,动态提取 `href` 属性后导航,不硬编码 URL。 - -### 8.6 发票类型分流 - -- **差旅发票**(高铁票/酒店住宿):不生成易耗品出库单,走差旅报销流程 -- **普通发票**:生成易耗品出库单,走普通报销流程 - -### 8.7 移动端同步 - -PC 端生成二维码指向移动端上传页面,手机端上传的图片通过轮询同步到 PC 端,实现跨设备协作。 +收件器是**独立进程**:`case_create` 返回带令牌的手机页地址,上传落 `cases//inbox/`, +再由 Agent 调 `case_add_files(source="inbox")` 收取。它只写不读(没有列表也没有下载路由)、令牌强制、 +扩展名白名单、25MB/文件、200 文件/会话。旧 Web 的「PC 端轮询同步」已删除。 --- -## 九、配置说明 +## 八、配置说明 -### config.json - -项目根目录 `config.json` 包含默认配置: +`scripts/data/config.json`(已在 `.gitignore`,**永远不要提交**): ```json { - "username": "200000001", - "password": "your_password", - "sso_login_url": "https://tyrz.fynu.edu.cn/sso/login", - "portal_url": "https://tyrz.fynu.edu.cn/oshall", - "reimburse_url": "http://210.45.32.214:8081", - "reimburse_page": "/expen/common/common?v=4.0", - "default_name": "示例报销人", - "default_card_no": "6282880000000000", - "default_person_id": "200000001", - "consumable_storage": "躬行楼 C205", - "llm": { - "model": "qwen3.5-9b", - "api_base": "http://10.0.0.10:1234/v1", - "api_key": "123456" - } + "username": "<工号>", + "password": "<密码>", + "default_name": "<报销人姓名>", + "default_card_no": "<公务卡号>", + "default_person_id": "<人员编号>", + "upload_pin": "<手机上传访问码,可留空>" } ``` -| 字段 | 说明 | +| 字段 | 用途 | |------|------| -| `username` | 财务系统工号 | -| `password` | 登录密码 | -| `sso_login_url` | SSO 登录地址 | -| `portal_url` | 信息门户地址 | -| `reimburse_url` | 报销系统地址 | -| `reimburse_page` | 报销录入页面路径 | -| `default_name` | 默认报销人姓名 | -| `default_card_no` | 默认公务卡号 | -| `default_person_id` | 默认人员编号 | -| `consumable_storage` | 出库单存放地点 | -| `llm.model` | LLM 模型名称 | -| `llm.api_base` | LLM API 地址 | -| `llm.api_key` | LLM API 密钥 | +| `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`;改规则要同步跑测试(骨架与规则有一致性用例)。 --- -## 十、日志与调试 +## 九、日志与排查 -### 日志输出 - -- 控制台实时输出(INFO 级别) -- 文件日志:`logs/<日期>.log`(UTF-8 编码) -- Web 界面:实时 SSE 日志流 - -### 截图保存 - -每个关键步骤自动截图到 `images/` 目录: - -| 截图文件 | 对应步骤 | -|----------|----------| -| `debug_portal_loaded.png` | 登录成功 | -| `debug_step3_project_modal.png` | 项目弹窗打开 | -| `debug_step3_project_selected.png` | 项目选择完成 | -| `debug_step3_done.png` | 基本信息完成 | -| `debug_after_add_click.png` | 点击新增后 | -| `debug_item_total.png` | 总明细录入完成 | -| `debug_step5_done.png` | 支付方式完成 | -| `debug_step6_done.png` | 附件上传完成 | -| `debug_error.png` | 异常状态 | - -### 超时设置 - -- 页面默认超时:30 秒 -- 登录门户等待:最多 30 秒 -- 单点登录页等待:最多 15 秒 -- SSE 日志流超时:10 分钟(600 秒) +- 控制台 + `logs/<日期>.log`(UTF-8)。**没有 SSE 或任何长连接事件流**,进度靠 `case_status` 查 +- 浏览器调试截图在 `images/`(已 gitignore);P3 之后过程截图进 `cases//screenshots/NNN-.png` +- 超时:页面默认 30 秒、门户登录 30 秒、单点登录页 15 秒 +- 排查顺序:`case_status` → `match_result.json` → 某张 `.invoice_cache/*.json` → `logs/<日期>.log` --- -## 十一、常见问题 +## 十、常见问题 -| 问题 | 原因 | 解决方案 | -|------|------|----------| -| 登录超时 | SSO 需要手动验证码/微信扫码 | 手动完成验证后脚本继续 | -| 未找到财务系统入口 | 门户页面结构变化 | 检查 `images/debug_*` 截图定位 | -| 经济科目选择失败 | 弹窗加载延迟 | 检查超时设置,增加等待时间 | -| 附件上传失败 | PDF 文件不存在或路径错误 | 确认 PDF 在当前工作目录 | -| 金额不匹配 | 明细合计 ≠ 支付合计 | 检查 CSV 数据中刷卡金额 | -| 提交被拦截 | 必填项为空 | 检查 `logs/<日期>.log` 定位失败步骤 | -| LLM 多模态提取失败 | llama-index 版本不兼容 | 确保使用 llama-index-core >= 0.14.x,多模态消息使用 `blocks` 格式 | -| 出库单生成失败 | 缺少 pywin32 或模板文件 | 安装 `pywin32`,确保项目根目录有 `易耗品、出库单.doc` 模板 | -| 差旅发票生成了出库单 | 分类不准确 | 检查发票内容是否包含"高铁票""酒店住宿"等关键词 | +| 问题 | 原因 | 解决 | +|------|------|------| +| 登录超时 | SSO 要扫码或验证码 | 有头模式下人工完成,流程会继续 | +| 未找到财务系统入口 | 门户页面结构变了 | 看 `images/debug_*` 截图定位选择器 | +| 经济科目选择失败 | 弹窗加载延迟 | 增大等待,或调整点第几行的索引 | +| 金额不匹配 | 明细合计 ≠ 支付合计 | 复核 `match_result.json` 与 `review_required` | +| 附件上传失败 | PDF 路径不对 | 材料要在 `cases//` 下,路径用正斜杠 | +| 宿主看不到新增工具 | 旧 stdio 子进程还在跑旧代码 | 杀掉子进程,DSH 会自动重启并重同步工具 | +| 报「binding arguments must be lossless JSON」 | Windows 路径用了反斜杠 | 改成 `D:/Code/...` | +| 手机上传 403 | URL 缺令牌或令牌不对 | 用 `case_create` 返回的完整地址 | +| 抽取字段读错(编号/卡号/金额位数) | 文本层与读图本来就会冲突 | 放大该区域重看,两路都不许默认采信 | +| 申购单生成失败 | 未装 Excel 或 pywin32;或有弹窗未关 | 需要 Windows + Excel,人工关掉残留 Excel 窗口再试 | --- -## 十二、相关 CLI 命令 - -### 单独填写出库单 +## 十一、相关命令 ```bash -uv run python -m src.infra.documents.consumable --csv invoice_summary.csv --doc "易耗品、出库单.doc" -``` +make check # ruff + format + mypy src + deptry +make test # pytest + 覆盖率 +make mcp-smoke # MCP stdio 自检 +make upload # 启动扫码上传收件器 -### 分步执行管道 - -```bash -# 仅发票提取 -uv run python src/main.py --step invoice - -# 仅浏览器填报 -uv run python src/main.py --step submit -``` - -### 启动 Web 服务 - -```bash -uv run python src/web/app.py -# 访问: http://localhost:5000 +# 不经 MCP 单独重生成申购单(调试用) +uv run python -m src.infra.documents.requisition --session cases/ --exclude 千斤顶 ``` --- -## 附录:API 文档 +## 相关文档 -详细的 API 接口文档见 [API.md](./API.md)。 \ No newline at end of file +- [docs/MCP.md](./MCP.md) — 17 个工具的参数与返回约定 +- [.agents/skills/reimbursement/SKILL.md](../.agents/skills/reimbursement/SKILL.md) — 宿主 Agent 的操作纪律 +- [AGENTS.md](../AGENTS.md) — 开发约束与踩坑记录 diff --git a/scripts/README.md b/scripts/README.md index 4a0c8c1..9cf11e4 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -1,20 +1,13 @@ --- -last_reviewed: 2026-06-15 +last_reviewed: 2026-10-19 --- -# scripts — 调试脚本与数据目录 +# scripts — 自检脚本与数据目录 -## 子目录 +| 文件/目录 | 说明 | +|-----------|------| +| `mcp_smoke.py` | MCP stdio 自检:拉起自身、打印工具清单、验证 instructions 已下发、调一次 `case_list`。 + `make mcp-smoke` 或 `python tasks.py mcp-smoke` | +| `data/` | 项目级配置目录(见 `data/README.md`) | -| 目录 | 说明 | -|------|------| -| `data/` | CLI 模式的数据目录:发票源文件、`config.json`、`.invoice_cache` 缓存 | - -## 脚本 - -| 文件 | 说明 | -|------|------| -| `debug_stream_fields.py` | 诊断 stream_chat 返回对象的字段结构 | -| `test_application_extract.py` | 测试出差申请单提取 | -| `test_multimodal.py` | 测试多模态 LLM 识别 | -| `test_travel_info.py` | 测试差旅信息提取 | +改完 MCP 工具一定要跑 `mcp_smoke`:它验的是**线上传输**的契约,进程内 import 成功不代表宿主能看到工具。 diff --git a/src/README.md b/src/README.md index 8167751..c49850b 100644 --- a/src/README.md +++ b/src/README.md @@ -1,97 +1,34 @@ --- -last_reviewed: 2026-06-15 +last_reviewed: 2026-10-19 --- -# src — 主源码目录 +# src — 源码 -包含财务报销自动化系统的全部源码模块。 - -## 架构分层 +MCP 服务器的全部实现:工具层在上,确定性内核在下,infra 是被调用的手。 ``` src/ -├── agent/ Agent 调度层(协调提取-校验-修正循环) -├── core/ 核心业务层(提取、匹配、校验) -├── infra/ 基础设施层(浏览器、文档、LLM 提示词) -├── web/ Web 界面层(Flask + SSE) -├── pipeline.py CLI 流程编排 -├── pipeline_core.py CLI/Web 公共管道逻辑 -├── main.py CLI 入口 -├── config.py 配置加载 -└── exceptions.py 异常定义 +├── mcp/ 工具与协议层(17 个工具,stdio) +│ ├── server.py INSTRUCTIONS + TOOLS 注册 + build_server() +│ ├── tools/ materials 材料 / extraction 抽取交接 / analysis 匹配校验 / documents 产物 +│ ├── schemas.py 抽取字段定义单一真源 + 日期金额规范化 +│ └── envelope.py ok/fail 统一信封(工具不把异常抛穿协议) +├── cases.py 会话目录 cases// 唯一实现:材料·收件箱·产物·阶段推导·上传令牌 +├── upload/ 扫码上传收件器(独立进程,Flask + segno) +├── core/ 确定性内核:extraction matching validation +├── infra/ browser · documents · com_thread +├── config.py scripts/data/config.json + 环境变量 +├── exceptions.py 异常层次 +└── __init__.py get_logger(终端 + logs/<日期>.log) ``` -## 模块清单 +## 依赖方向 -| 文件/目录 | 说明 | -|-----------|------| -| `agent/` | Agent 调度:校验-修正循环、状态机管理、SSE 事件发射 | -| `core/` | 核心业务逻辑:信息提取、金额匹配、信息校验 | -| `infra/` | 基础设施:浏览器自动填报、文档处理、LLM 提示词管理 | -| `web/` | Web 界面:Flask 应用、SSE 日志流、可编辑表格、移动端上传、会话隔离 | -| `pipeline.py` | CLI 流程编排:串联提取 → 类型判断 → 信息提取 → 浏览器填报 | -| `pipeline_core.py` | CLI/Web 公共管道逻辑:发票类型判断、缓存提取 | -| `main.py` | CLI 入口:`--step` 分步执行、`-u/-p` 覆盖凭据 | -| `config.py` | 配置加载:`config.json` + 环境变量 | -| `exceptions.py` | 异常层次定义 | +`mcp` 依赖 `core`/`infra`/`cases`;`core` 不依赖 `infra` 与任何框架;`infra` 不依赖 `mcp`。 +`cases.py` 只依赖标准库,是会话目录布局的唯一真相:想知道产物叫什么,读它,别猜。 -## 数据流 +## 分层原则 -```mermaid -graph TD - A[CLI/Web 入口] --> B["pipeline.py (编排)"] - B --> C["core/extraction/extractor.py (统一提取入口)"] - C --> D["infra/documents/pdf.py (PDF 渲染为图片)"] - C --> E["core/extraction/llm_extractor.py (多模态 LLM 识别)"] - E --> F["发票 invoice_type=train/hotel/general"] - E --> G["支付记录 invoice_type=payment"] - E --> H["出差事前申请单 invoice_type=application"] - C --> I["core/matching/matcher.py (发票与支付记录按金额匹配)"] - I --> J["一对一匹配 发票数 == 刷卡数"] - I --> K["一对多匹配 贪心算法 相对容差 3%"] - C --> L["infra/documents/invoice.py (CSV/JSON 读写)"] - L --> M["payment_records.csv (支付记录级别)"] - L --> N["invoice_summary.csv (发票级别)"] - L --> O["travel_applications.json (出差申请单)"] - B --> R{"判断报销类型"} - R -->|差旅| T["core/extraction/llm_extractor.py (差旅信息提取)"] - R -->|普通| V["core/extraction/llm_extractor.py (普通发票信息提取)"] - T --> W["travel_info.json (差旅信息: 交通/住宿明细、补贴、附件清单)"] - V --> X["normal_info.json (普通发票信息: 报销说明、发票总数、总金额、支付方式、附件清单)"] - W --> P["infra/browser/ (浏览器填报 - 仅接收信息并填报)"] - X --> P - P --> Q["差旅模式: travel_info.json → 填报差旅单 → 上传差旅附件"] - P --> S["普通模式: 基本信息 → 录入明细 → 支付信息 → 上传附件"] -``` - -## 子模块文档 - -| 目录 | 文档 | -|------|------| -| `agent/` | [`agent/README.md`](agent/README.md) | -| `core/` | [`core/README.md`](core/README.md) | -| `infra/` | [`infra/README.md`](infra/README.md) | -| `web/` | [`web/README.md`](web/README.md) | - -## 启动方式 - -```bash -# CLI 模式 -uv run python src/main.py --step all - -# Web 模式 -uv run python src/web/app.py -# 访问: http://localhost:5000 -``` - -## 发票类型与路由 - -系统根据 `invoice_type` 字段自动分流: - -| 发票类型 | 走什么流程 | 是否生成出库单 | -|----------|-----------|--------------| -| `train` / `hotel` | 差旅报销 | 否 | -| `general` | 普通报销 | 是(易耗品出库单) | -| `application` | 出差事前申请单 | 否(单独存储为 JSON) | - -**注意**:差旅发票和普通发票不支持混报,混合时会报错。 \ No newline at end of file +- **判断在上,事实在下**:读图、综合、决定交给宿主 Agent;本项目只做渲染、匹配、校验、生成 +- **无状态**:阶段从磁盘产物推导,进程随时可杀可重启 +- **不承诺未完成的事**:INSTRUCTIONS 只列已注册工具,`browser_*` 与 `submit_*` 尚未实现 diff --git a/src/core/README.md b/src/core/README.md index 38d555d..87a43b1 100644 --- a/src/core/README.md +++ b/src/core/README.md @@ -1,21 +1,19 @@ --- -last_reviewed: 2026-06-15 +last_reviewed: 2026-10-19 --- -# src/core — 核心业务逻辑 +# src/core — 确定性内核 -项目的核心业务层,负责信息提取、金额匹配和信息校验。此层不依赖 Web 框架或浏览器自动化等基础设施。 - -## 子模块 +信息存取、金额匹配、信息校验。不依赖 Flask、Playwright、COM,也不调用任何模型。 | 目录 | 说明 | |------|------| -| `extraction/` | 文档信息提取:PDF/图片 → LLM 多模态识别 → 结构化数据 | -| `matching/` | 发票与支付记录按金额匹配(一对一 / 一对多) | -| `validation/` | 声明式信息完整性校验,规则从 JSON 配置文件加载 | +| `extraction/` | 缓存读写 + PDF 渲染(页面 PNG、文本层、自动转正) | +| `matching/` | 发票与支付记录按金额匹配;`runner.py` 从会话缓存跑全流程并落盘产物 | +| `validation/` | 声明式完整性校验,规则来自 `config/validation_rules.json` | ## 设计原则 -- **零外部依赖**:不依赖 Flask、Playwright 等框架 -- **接口契约**:每个子模块通过 `__init__.py` 导出稳定的对外接口 -- **错误传播**:明确的异常层次,便于上层统一处理 +- **零框架依赖**:可单独跑、单独测 +- **接口契约**:每个子模块用 `__init__.py` 导出稳定接口 +- **裁判不是作者**:`validation` 判定宿主产出的数据是否完整,绝不代替宿主填内容 diff --git a/src/core/extraction/README.md b/src/core/extraction/README.md index 302558d..892ba42 100644 --- a/src/core/extraction/README.md +++ b/src/core/extraction/README.md @@ -1,29 +1,29 @@ --- -last_reviewed: 2026-06-15 +last_reviewed: 2026-10-19 --- -# src/core/extraction — 信息提取 +# src/core/extraction — 存取与渲染 -从 PDF 发票和图片中提取结构化数据,是系统数据流的起点。 - -## 文件 +**没有 LLM**:抽取由宿主 Agent 完成,本模块负责落盘、读回、渲染页面与文本层。 | 文件 | 职责 | |------|------| -| `extractor.py` | 编排入口:扫描目录 → 逐文件提取 → 分类(发票/支付记录/申请单)→ 金额匹配 | -| `llm_extractor.py` | LLM 多模态提取核心:统一文档提取、差旅/普通信息提取、缓存管理、SSE 流式事件 | +| `cache.py` | `.invoice_cache/` 读写、路径推导(`.json`)、匹配结果落盘 | +| `render.py` | PDF → 页面 PNG + 文本层 + `meta.json`;按文本行方向多数派自动转正;图片预处理 | ## 对外接口 | 函数 | 说明 | |------|------| -| `extract_invoices(directory)` | 统一提取入口,返回 `(payment_records, applications, groups)` | -| `extract_document(file_path)` | 从单个图片/PDF 提取信息 | -| `extract_travel_info(source_dir)` | 综合发票和匹配结果提取差旅信息 | -| `extract_normal_info(source_dir)` | 提取普通发票报销信息 | -| `load_cache(source_dir)` | 加载缓存的结构化数据 | -| `llm_query_text(...)` | 纯文本 LLM 查询(供 Agent 调度使用) | +| `render_pdf(pdf, out_dir, dpi, force)` | 逐页出 `p01.png` + `text.txt` + `meta.json`(dpi / 转正角度 / 文本字符数) | +| `detect_content_rotation(page)` | 文本行方向统计,90 度吸附;不明显就不转 | +| `prepare_image(path)` | 图片转正与尺寸准备 | +| `load_cache(case_dir)` / `load_match_result(case_dir)` | 读会话缓存 | +| `extraction_path(case_dir, source_file)` | 抽取结果路径;键名与旧管道一致,历史缓存可直接复用 | +| `save_extraction` / `save_json` / `save_match_result` | 写盘;支付主键形如 `" (¥amount)"`,未匹配走 `__unmatched__` | -## 缓存机制 +## 已知文本层陷阱(务必复核) -提取结果缓存在 `.invoice_cache/` 目录中,文件名与源文件同名(`发票1.pdf` → `.invoice_cache/发票1.json`),避免重复调用 LLM。 +- 电子发票文本层把所有**标签**排完再排所有**值**,不是一行一条,别按行配对 +- 康熙部首类字符(U+2F47 形似的「日」、门 的部首形式)NFKC 之后仍不变形,字段名看着对其实错 +- 文本层与读图出现过真实分歧(发票号一位之差、银行账号不同)→ 冲突时两路都不许默认采信,放大该区域重看 diff --git a/src/core/matching/README.md b/src/core/matching/README.md index ae31fe1..490b30e 100644 --- a/src/core/matching/README.md +++ b/src/core/matching/README.md @@ -1,27 +1,27 @@ --- -last_reviewed: 2026-06-15 +last_reviewed: 2026-10-19 --- # src/core/matching — 金额匹配 -将提取到的发票数据与支付记录(刷卡截图)按金额进行匹配。 - -## 文件 - | 文件 | 职责 | |------|------| -| `matcher.py` | 匹配引擎:一对一匹配、一对多贪心匹配、未匹配发票处理 | +| `matcher.py` | 匹配引擎:一对一、一对多贪心、未匹配处理、CSV 汇总构造 | +| `runner.py` | 从会话缓存跑完整流程:分组 → 匹配 → 写 `match_result.json`、`invoice_groups.json`、两份 CSV、`travel_applications.json` | ## 匹配策略 | 场景 | 策略 | |------|------| -| 发票数 == 支付记录数 | 一对一匹配:按金额降序配对,相对容差内即匹配 | -| 发票数 > 支付记录数 | 一对多匹配:贪心算法凑金额,相对容差 3% | -| 文件名匹配 | 最高优先级:文件名(不含后缀)一致时直接匹配 | -| 未匹配发票 | 单独列为一条支付记录,`remark` 标记为 `"unmatched"` | +| 发票数 == 支付数 | 一对一:按金额降序配对,相对容差内即匹配 | +| 发票数 > 支付数 | 一对多:贪心凑额,默认相对容差 3% | +| 文件名一致 | 最高优先级,直接匹配 | +| 未匹配发票 | 单列一条支付记录,`remark` 标 `unmatched` | ## 业务约束 -- 发票总金额 >= 支付总金额 -- 输出以支付记录为主键的结果列表 +- 发票总额应 >= 支付总额,违反要报出来 +- 输出以支付记录为主键 + +`match_from_cache()` 额外返回 `review_required`:未匹配项、发票总额小于支付总额、一对多凑额。 +**信封 `ok: true` 只代表贪心跑完了,不代表配对对了**——调用方必须逐条复核,等额发票尤其容易配错。 diff --git a/src/core/validation/README.md b/src/core/validation/README.md index 65c5fad..29eea49 100644 --- a/src/core/validation/README.md +++ b/src/core/validation/README.md @@ -1,34 +1,30 @@ --- -last_reviewed: 2026-06-15 +last_reviewed: 2026-10-19 --- # src/core/validation — 信息校验 -对 LLM 提取的报销信息进行声明式规则校验,判断是否满足填报要求。 - -## 文件 +对宿主 Agent 产出的报销信息做声明式规则校验,判断是否满足填报要求。 | 文件 | 职责 | |------|------| -| `validator.py` | 校验引擎:加载 JSON 规则配置 → 遍历字段/数组 → 输出校验报告 | +| `validator.py` | 加载 JSON 规则 → 遍历字段与数组 → 输出校验报告 | ## 设计特点 -- **规则与引擎分离**:校验规则存储在 `config/validation_rules.json`,引擎只负责执行 -- **统一路径定位**:使用 `path` 列表定位嵌套字段,如 `["basic_info", "travel_purpose"]` -- **自定义校验**:支持 `custom_check` 函数(日期格式、正数检查等) -- **数组元素校验**:支持 `min_items` 最小数量 + 每个元素的必填字段 - -## 校验规则类型 - -| 类型 | 用途 | 配置项 | -|------|------|--------| -| `fields` | 顶层单值字段 | `path`, `required`, `custom_check`, `check_empty` | -| `arrays` | 数组字段 | `path`, `min_items`, `element_fields` | +- **规则与引擎分离**:规则在 `config/validation_rules.json`,引擎只执行;`mcp__arb__rules_get` 把原文给宿主 +- **统一路径定位**:`path` 列表定位嵌套字段,如 `basic_info` + `travel_purpose` +- **自定义校验**:`custom_check` 支持日期格式、正数、金额一致等 +- **数组元素校验**:`min_items` + 每个元素的必填字段 +- **同源骨架**:`mcp__arb__info_skeleton` 直接由本规则生成,保证 `required: true` 的键必然存在(含值为空串的备注类字段) ## 对外接口 | 函数 | 说明 | |------|------| -| `validate(info, invoice_type)` | 执行校验,返回 `ValidationReport` | -| `get_missing_fields(report)` | 提取缺失字段列表 | +| `validate(info, invoice_type)` | 返回 `ValidationReport`(confidence 与逐项问题) | +| `get_missing_fields(report)` | 缺失字段列表 | +| `get_validation_rules()` | 规则原文,骨架与 `rules_get` 都取它 | + +改规则必须同步跑 `pytest`(含骨架与规则一致性用例),否则骨架会与规则漂移。 +`required: true` 是刻意约束,不要为了让宿主省事而改掉。 diff --git a/src/infra/README.md b/src/infra/README.md index 0627e75..258a50b 100644 --- a/src/infra/README.md +++ b/src/infra/README.md @@ -1,21 +1,21 @@ --- -last_reviewed: 2026-06-15 +last_reviewed: 2026-10-19 --- # src/infra — 基础设施层 -提供浏览器自动化、文档处理和 LLM 接口等底层能力。此层不包含业务逻辑,只提供工具和平台能力。 +只提供手和眼,不含业务判断。 -## 子模块 - -| 目录 | 说明 | +| 模块 | 说明 | |------|------| -| `browser/` | Playwright 驱动的财务系统自动填报 | -| `documents/` | 发票数据模型、PDF 渲染、Word 出库单填写 | -| `llm/` | LLM 提示词模板加载与管理 | +| `browser/` | Playwright 驱动财务系统:登录、新建单据、差旅与普通填报、附件上传 | +| `documents/` | 发票与支付 CSV、低值易耗品申购单(Excel COM) | +| `com_thread.py` | Office COM 单线程串行 actor:所有 COM 调用排队执行 | -## 设计原则 +## 线程与并发约束 -- **无业务逻辑**:只提供工具能力,不包含业务流程判断 -- **可替换性**:每个子模块通过 `__init__.py` 导出接口,便于替换实现 -- **与 core 层解耦**:infra 不依赖 core,core 可通过接口调用 infra +- **COM**:并发驱动 Word/Excel 会互抢 `ActiveDocument` 与模态框,必须走 `run_com()` +- **Playwright sync**:greenlet 有线程亲和,创建与使用必须在同一固定线程;接 MCP 时单独一个 actor(P3), + 不要和 COM 挤同一线程(套间要求不同) +- **多进程**:宿主会保留旧代 stdio 子进程,subagent/teammate 也各起一条 transport(实测 4 个 PID 并存), + 独占资源(浏览器、Office)需要磁盘锁,别假设只有一个服务器实例 diff --git a/src/infra/browser/README.md b/src/infra/browser/README.md index 1e9e16d..4d35911 100644 --- a/src/infra/browser/README.md +++ b/src/infra/browser/README.md @@ -1,44 +1,27 @@ --- -last_reviewed: 2026-06-15 +last_reviewed: 2026-10-19 --- # src/infra/browser — 浏览器自动化 -使用 Playwright 操作财务报销系统,自动完成登录、填单、上传附件等操作。 - -## 文件 +Playwright 操作财务报销系统:登录信息门户、进入报销系统、新建单据、填单、上传附件。 | 文件 | 职责 | |------|------| -| `base.py` | `BaseBot` 基类:浏览器生命周期、登录信息门户、导航到报销系统、创建新单据、截图 | -| `travel.py` | 差旅报销填报流程:基本信息 → 差旅明细 → 支付方式 → 补助清单 → 附件上传 | -| `normal.py` | 普通报销填报流程:基本信息 → 总明细 → 支付方式 → 附件上传 | -| `__init__.py` | 入口函数:`run_bot()` / `run_bot_web()`,负责类型路由和流程调度 | +| `base.py` | `BaseBot`:浏览器生命周期、登录、导航、新建单据、截图(默认**有头**模式) | +| `travel.py` | 差旅填报:基本信息 → 差旅明细 → 支付方式 → 补助清单 → 附件 | +| `normal.py` | 普通填报:基本信息 → 总明细 → 支付方式 → 附件 | +| `__init__.py` | `run_bot(config, travel_info, normal_info)`;`run_bot_web(config, work_dir)` 从会话缓存加载后执行 | -## 对外接口 +## 现状与计划 -| 函数 | 说明 | -|------|------| -| `run_bot(config, travel_info, normal_info)` | CLI 模式:根据传入信息判断差旅/普通报销 | -| `run_bot_web(config, work_dir)` | Web 模式:从缓存加载信息后执行填报 | +现有入口是**一次性跑完**的编排函数,适合手工验证,不适合 Agent 分步介入:中途出错只能整体重来, +也没法在填到一半时先看一眼。P3 计划拆成 `browser_open / login / new_form / fill(step) / page_state / close` +分步工具,过程截图进 `cases//screenshots/NNN-.png`,并用 `.browser.lock`(PID + 心跳)防止多代进程 +抢同一个浏览器;P4 再加提交前预览与一次性 token 护栏(提交不可逆)。 -## 填报流程 +## 注意 -### 差旅报销(travel) -1. 填写基本信息(事由、地点、日期、项目编号) -2. 添加差旅明细(交通费用逐条录入) -3. 填写支付方式(公务卡刷卡记录) -4. 填写补助清单(按天计算交通补助 + 伙食补助) -5. 上传附件(发票、申请单等) - -### 普通报销(normal) -1. 填写基本信息(报销事由、金额) -2. 填写发票明细(总数、总金额) -3. 填写支付方式 -4. 上传附件 - -## 注意事项 - -- 浏览器填报会启动 Chromium,请勿手动干扰自动化流程 -- 调试截图保存在 `images/` 目录 -- Web 模式以无头模式运行 +- 会打开 Chromium,自动化期间不要手动点页面 +- 调试截图落在 `images/`(已 gitignore,不进仓库) +- 步骤函数最好返回「填了什么 / 什么失败」的结构,P3 可直接复用 diff --git a/src/infra/documents/README.md b/src/infra/documents/README.md index 0bc8e66..af5b5cc 100644 --- a/src/infra/documents/README.md +++ b/src/infra/documents/README.md @@ -1,36 +1,31 @@ --- -last_reviewed: 2026-06-15 +last_reviewed: 2026-10-19 --- # src/infra/documents — 文档处理 -提供发票数据模型、PDF 渲染和 Word 出库单填写功能。 - -## 文件 - | 文件 | 职责 | |------|------| -| `invoice.py` | 发票数据模型:类型常量、CSV 列定义、CSV/JSON 读写工具、发票分类 | -| `pdf.py` | PDF 渲染为图片(PyMuPDF),供多模态 LLM 识别使用 | -| `consumable.py` | 易耗品出库单填写:读取 CSV → 填入 Word 模板(pywin32 COM,仅 Windows) | +| `invoice.py` | 发票类型常量、CSV 列定义、CSV/JSON 读写、批量分类 | +| `requisition.py` | 低值易耗品申购单:从 `.invoice_cache` 取明细 → 清洗与去重 → Excel COM 填模板 | ## 对外接口 | 函数 | 说明 | |------|------| -| `load_csv(path)` | 读取支付记录 CSV | -| `save_csv(payment_records, path)` | 保存支付记录 CSV | -| `save_invoice_csv(payment_records, path)` | 保存发票级别 CSV | -| `classify_invoice_batch(cache_map)` | 按类型批量分类发票 | -| `render_pdf_to_images(pdf_path)` | PDF → 图片列表 | -| `fill_consumable_doc(csv_path, doc_path)` | 将 CSV 数据填入 Word 模板 | +| `load_csv` / `save_csv` / `save_invoice_csv` | 支付记录与发票级 CSV 读写 | +| `classify_invoice_batch(cache_map)` | 按类型分组:travel / general / application / attachments | +| `load_requisition_items(cache_dir, exclude_keywords)` | 申购明细:清洗品名、猜规格、按发票号去重、关键词排除 | +| `load_project_desc(session_dir)` | 「对应实验实训项目」取值顺序:`normal_info` → `travel_info` → 旧 `agent_state.json` → 默认 | +| `generate_requisition(session_dir, template, output, project_desc, exclude)` | 一站式生成;`mcp__arb__doc_requisition` 经 `run_com` 调它 | -## 缓存目录 +## 申购单 -`.invoice_cache/` 是系统级缓存目录名常量,定义在 `invoice.py` 中,被提取和匹配模块统一引用。 +- 需要 **Windows + Microsoft Excel + pywin32** +- 模板取项目根目录 `低值易耗品申购*.xls` 中名字最大者(仓库里保留的就是它) +- 产物写 `cases//artifacts/`,不再污染项目根目录(旧实现把带时间戳的产物生成在模板旁边, + 会让下次模板查找命中上次的产物) +- 表头在第 2 行、数据从第 3 行起、末尾追加合计行;列位与行号常量在 `requisition.py` 顶部 -## 易耗品出库单 - -- 需要 **Windows + Microsoft Word + pywin32** -- 模板文件为项目根目录的 `易耗品、出库单.doc` -- 填写规则:日期用当天日期,品名/规格/数量/单价从 CSV 解析,字体统一宋体五号 +> Word 版易耗品出库单(`consumable.py`)已删除:模板自提交 `63b97c6`(2026-08-31)起不在仓库, +> 那条路径此后一直静默失败。历史实现看 git。 diff --git a/src/infra/llm/README.md b/src/infra/llm/README.md deleted file mode 100644 index 5eb0bfd..0000000 --- a/src/infra/llm/README.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -last_reviewed: 2026-06-15 ---- - -# src/infra/llm — LLM 提示词管理 - -管理 LLM 提示词模板的加载,供 `core/extraction/llm_extractor.py` 调用。 - -## 文件 - -| 文件 | 职责 | -|------|------| -| `prompt.py` | 提示词加载:从 `prompts/` 目录读取 `.md` 模板文件 | -| `prompts/` | 提示词模板目录(Markdown 格式) | - -## 提示词模板 - -| 文件 | 用途 | -|------|------| -| `invoice_system.md` | 发票提取系统提示词 | -| `travel_info_system.md` | 差旅信息提取系统提示词 | -| `normal_info_system.md` | 普通发票信息提取系统提示词 | -| `supplement_system.md` | 用户补充信息后的二次提取提示词 | -| `validation_system.md` | 校验修正提示词 | - -## 对外接口 - -| 函数 | 说明 | -|------|------| -| `build_invoice_system_prompt()` | 构建发票提取系统提示词 | -| `build_travel_info_system_prompt()` | 构建差旅信息提取系统提示词 | -| `build_normal_info_system_prompt()` | 构建普通发票信息提取系统提示词 | diff --git a/src/infra/llm/__init__.py b/src/infra/llm/__init__.py deleted file mode 100644 index 4e0891c..0000000 --- a/src/infra/llm/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -"""LLM 接口模块 - -提供 LLM 提示词模板加载功能。 -""" - -from .prompt import ( - build_invoice_system_prompt, - build_normal_info_system_prompt, - build_travel_info_system_prompt, -) - -__all__ = [ - "build_invoice_system_prompt", - "build_normal_info_system_prompt", - "build_travel_info_system_prompt", -] diff --git a/src/infra/llm/prompt.py b/src/infra/llm/prompt.py deleted file mode 100644 index b98fe3e..0000000 --- a/src/infra/llm/prompt.py +++ /dev/null @@ -1,30 +0,0 @@ -"""LLM 提示词模板 - -从 infra/llm/prompts/ 目录加载 .md 文件作为提示词模板。 -""" - -import os - -_PROMPTS_DIR = os.path.join(os.path.dirname(__file__), "prompts") - - -def _load_prompt(filename: str) -> str: - """从 prompts 目录加载提示词文件内容。""" - path = os.path.join(_PROMPTS_DIR, filename) - with open(path, encoding="utf-8") as f: - return f.read() - - -def build_invoice_system_prompt() -> str: - """构建发票提取系统提示词。""" - return _load_prompt("invoice_system.md") - - -def build_travel_info_system_prompt() -> str: - """构建差旅信息提取系统提示词。""" - return _load_prompt("travel_info_system.md") - - -def build_normal_info_system_prompt() -> str: - """构建普通发票信息提取系统提示词。""" - return _load_prompt("normal_info_system.md") diff --git a/src/infra/llm/prompts/README.md b/src/infra/llm/prompts/README.md deleted file mode 100644 index 68f0a0e..0000000 --- a/src/infra/llm/prompts/README.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -last_reviewed: 2026-06-12 ---- - -# src/infra/llm/prompts — LLM 提示词模板 - -存放 LLM 信息提取使用的系统提示词模板文件,由 `src/infra/llm/prompt.py` 动态加载。 - -## 模板清单 - -| 文件 | 用途 | -|------|------| -| `invoice_system.md` | 发票提取系统提示词:指导 LLM 从发票图片、支付截图、出差申请单等文档中提取结构化信息 | -| `travel_info_system.md` | 差旅信息提取系统提示词:指导 LLM 整合已结构化的发票信息、付款记录和出差申请单,生成差旅报销所需的结构化数据 | - -## 加载方式 - -```python -from src.infra.llm import build_invoice_system_prompt, build_travel_info_system_prompt -``` \ No newline at end of file diff --git a/src/mcp/README.md b/src/mcp/README.md new file mode 100644 index 0000000..e20c77f --- /dev/null +++ b/src/mcp/README.md @@ -0,0 +1,33 @@ +--- +last_reviewed: 2026-10-19 +--- + +# src/mcp — 工具与协议层 + +MCP 服务器的全部对外面。17 个工具的参数表在 `docs/MCP.md`(那份表由运行时 schema 生成,与代码同源)。 + +| 文件 | 职责 | +|------|------| +| `server.py` | `MCPServer(name=auto-reimbursement, instructions=INSTRUCTIONS)`、`TOOLS` 注册表、`build_server()`、`main()` 走 stdio | +| `tools/materials.py` | 会话与材料:create / list / status / add_files / inbox / reset | +| `tools/extraction.py` | 抽取交接:render_pages / read_text / save_extraction / patch_extraction / extraction_spec | +| `tools/analysis.py` | 匹配与校验:match_payments / info_skeleton / save_info / validate_info / rules_get | +| `tools/documents.py` | 产物:doc_requisition(经 `run_com`) | +| `schemas.py` | 七类票据的字段定义、日期与金额规范化;`invoice_type` 非法直接拒收 | +| `envelope.py` | `ok(**data)` / `fail(reason, next_action=..., **detail)` | + +## 写工具的规矩 + +- **返回信封,不抛异常穿透协议**:文件缺失、参数不合法、COM 失败都转成 `ok: false` + `reason` + `next_action` +- `next_action` 写给 Agent 看,尽量是具体工具名,让它照着做 +- 不返回密码、令牌明文等敏感值;上传令牌只从 `case_create` 的页面地址里给一次 +- 未实现的能力**不要出现在 INSTRUCTIONS 里**,也不要在工具描述里暗示存在 +- 新增工具后:`make mcp-smoke` 验 wire 清单;宿主侧要重连(杀掉 stdio 子进程会自动重启并重同步工具) + +## 已知宿主侧坑 + +- DSH 会清洗 stdio 子进程的环境变量(抹掉含 KEY/PASSWORD/SECRET/TOKEN 的以及全部 `DSH_*`), + 所以凭据只能来自 `scripts/data/config.json` +- DSH 的参数绑定要求 JSON 无损:Windows 路径必须正斜杠,反斜杠会被拒 +- 结果可能被包在 `content[0].text`(JSON 字符串)里,同时给 `structuredContent` +- 服务器 import 与构造必须无副作用:stdio 协商阶段会先起一个临时探针进程 diff --git a/src/upload/README.md b/src/upload/README.md new file mode 100644 index 0000000..37c3d23 --- /dev/null +++ b/src/upload/README.md @@ -0,0 +1,40 @@ +--- +last_reviewed: 2026-10-19 +--- + +# src/upload — 扫码上传收件器 + +**独立进程**,不是 MCP 工具,也绝不由 MCP 子进程拉起(生命周期要独立于宿主,重启宿主不能带走上传口)。 + +```bash +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 --port 6000 --pin 1234 +``` + +| 文件 | 职责 | +|------|------| +| `server.py` | Flask 应用与路由、上限与白名单、`lan_ip()`、命令行参数 | +| `page.py` | 页面模板(内联 CSS,零外部 CDN,`__PLACEHOLDER__` 占位替换) | + +## 路由 + +| 路由 | 说明 | +|------|------| +| `GET /` | PC 端首页(可选 PIN 校验) | +| `GET /qr/` | 服务端生成的二维码 PNG(segno) | +| `GET /u/` | 手机上传页(令牌在 URL 里) | +| `POST /u/` | 接收文件,落 `cases//inbox/` | +| `GET /api/inbox/` | 该会话待收数量(只给数量,不给内容) | + +## 安全边界 + +- 默认 `0.0.0.0` 是刻意的(手机要用局域网 IP 访问),补偿控制如下 +- **每会话令牌强制**:`case.json` 里的 `upload_token`,缺失或错误一律 403;工具输出里永不回显令牌明文, + 只在 `case_create` 返回的页面地址中出现一次 +- **只写不读**:没有文件列表、没有下载路由 +- 扩展名白名单、单文件 25MB、每会话 200 个文件封顶 +- 文件名一律 `Path(raw.replace(chr(92), '/')).name` 取基名 + 重名加序号,防目录穿越 +- 可选 `--pin` / `UPLOAD_PIN` / `config.json` 的 `upload_pin` 作为额外访问码 + +`cases//inbox/` 里的文件由宿主调 `case_add_files(source='inbox')` 收取进会话,收件器本身不碰任何业务逻辑。 diff --git a/tests/README.md b/tests/README.md index 98e27a7..59cd15c 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,23 +1,24 @@ --- -last_reviewed: 2026-06-11 +last_reviewed: 2026-10-19 --- -# tests — 单元测试目录 +# tests — 单元测试 -存放项目单元测试,使用 pytest 运行。 - -## 测试清单 +pytest 运行(`make test` 带覆盖率)。当前 107 例。 | 文件 | 覆盖范围 | |------|---------| -| `test_config.py` | 配置加载模块 | -| `test_extractor.py` | 发票提取编排:空目录、提取失败、正常流程、分类结果、申请单分离、支付匹配 | -| `test_invoice.py` | 发票分类、CSV 读写 | -| `test_llm_extractor.py` | LLM 信息提取 | -| `test_matcher.py` | 发票与支付记录匹配 | +| `test_config.py` | 配置加载:默认值、文件覆盖、环境变量优先级 | +| `test_invoice.py` | 发票常量、差旅判定、批量分类 | +| `test_matcher.py` | 金额匹配:容差、一对一、一对多贪心、未匹配、CSV 汇总构造 | +| `test_mcp_analysis.py` | MCP 分析工具:缓存分组、凑额复核、业务约束违反、无发票场景、`save_info` 剥离多余字段、 + 草稿不落盘、`rules_get` 形状、`info_skeleton` 与规则同源、骨架填满即通过 | +| `test_requisition.py` | 申购明细:品名清洗、规格猜测、发票号去重、关键词排除、项目名取值链与回退 | -## 运行方式 +## 不进单测的部分 -```bash -uv run pytest tests/ -``` \ No newline at end of file +- **Office COM**(申购单):需要本机装有 Excel,改动后用真实生成验证(约 2-5 秒,产物单元格肉眼确认) +- **Playwright 填报**:需要真实财务系统账号,手工验证 +- **MCP wire 层**:用 `make mcp-smoke`(真起 stdio 子进程握手、列工具、调一次 `case_list`) + +测试不碰 `cases/` 真实目录,一律用 `tmp_path` 造会话。