docs: 文档全面对齐 MCP 架构,并脱敏公开操作指南
接口与入门: - 新增 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 生成,产物单元格读回正确。
This commit is contained in:
@@ -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^`)。
|
||||
|
||||
@@ -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()<br/>LLM提取 → validator校验<br/>最多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<br/>(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<br/>{files: [...]}
|
||||
B-->>F: {status: "started"}
|
||||
F->>S: GET /api/logs/:sid
|
||||
|
||||
Note over B: 新后台线程启动
|
||||
|
||||
A->>A: add_supplement()<br/>(记录文件名, 发射收到事件)
|
||||
|
||||
S-->>F: agent_supplement_received
|
||||
|
||||
A->>A: extract_invoices()<br/>(重新提取所有文件)
|
||||
A->>A: run_agent_round(new_files=[...])
|
||||
Note over A: 加载上一轮结果作为<br/>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<br/>{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()<br/>(LLM解析用户文字)
|
||||
|
||||
S-->>F: llm_stream (解析过程)
|
||||
|
||||
A->>A: merge_supplement_into_info()<br/>(合并到 extracted_info)
|
||||
Note over A: 保存到缓存文件
|
||||
|
||||
A->>A: run_agent_round()<br/>(重新校验)
|
||||
|
||||
S-->>F: agent_state_change<br/>(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()<br/>(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 日志收集器<br/>随即开始写入 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 事件<br/>执行财务提交<br/>返回 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` 中添加对应的事件处理器
|
||||
@@ -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` | 项目配置 |
|
||||
@@ -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/<id>/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`
|
||||
|
||||
@@ -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*
|
||||
@@ -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 在用户请求提交代码或检查代码质量时自动触发对应技能,无需手动调用。
|
||||
命中描述的场景时 Agent 自动加载,无需手动调用。`reimbursement` 的技能正文与 `docs/MCP.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/<stem>/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/<case_id>/`(该目录已 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` — 架构决策与实测记录(含抽取质量闸门数据)
|
||||
+6
-3
@@ -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` 的值必须与实际判断类型一致
|
||||
- `invoice_type` 的值必须与实际判断类型一致
|
||||
+8
-13
@@ -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` 格式
|
||||
- 金额使用数字类型(非字符串)
|
||||
+9
-14
@@ -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` 格式
|
||||
- 金额使用数字类型(非字符串)
|
||||
- 计数使用整数类型
|
||||
- 计数使用整数类型
|
||||
+7
-6
@@ -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
|
||||
# 上传收件器访问码(也可用 --pin 参数或 config.json 的 upload_pin)
|
||||
UPLOAD_PIN=
|
||||
|
||||
@@ -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__<tool>`。
|
||||
|
||||
**注意:** `tasks.py` 的 `check` 用 `&&` 连接(PowerShell 不支持 `&&`,但 tasks.py 内部是单行字符串,没问题)。
|
||||
|
||||
## 代码质量工具链(执行顺序)
|
||||
|
||||
1. **Ruff lint** — `uv run ruff check .` (select: E, F, W, I, N, UP, B; ignore: E501)
|
||||
2. **Ruff format** — `uv run ruff format --check .` (line-length: 120)
|
||||
3. **MyPy strict 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/<case_id>/` 的唯一实现:材料、收件箱、产物与**阶段推导**(阶段由产物推导,不存在状态机文件)
|
||||
- `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/<id>/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/<stem><ext>.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/<stem>/p01.png` + `text.txt` + `meta.json` | `case_render_pages` | 给宿主读的页面与 PDF 文本层(含转正角度) |
|
||||
| `.invoice_cache/<stem><ext>.json` | 宿主抽取 → `case_save_extraction` 规范化落盘 | 单张票据结构化数据(小改用 `case_patch_extraction`) |
|
||||
| `match_result.json` / `invoice_groups.json` | `match_payments` | 支付与发票的关联、差旅/普通分组 |
|
||||
| `invoice_summary.csv` / `payment_records.csv` | `match_payments` | 发票级与支付级汇总(申购单取数) |
|
||||
| `travel_info.json` / `normal_info.json` | 宿主综合 → `save_info` | 差旅/普通报销的全部结构化数据 |
|
||||
| `artifacts/*.xls` | `doc_requisition` | 低值易耗品申购单 |
|
||||
|
||||
### 缓存机制
|
||||
|
||||
MCP 会话:`cases/<case_id>/.invoice_cache/`(**新**)
|
||||
遗留只读:`scripts/data/`(CLI)与 `src/web/uploads/<session_id>/`(Web),两者即将删除
|
||||
|
||||
缓存文件与源文件同名(如 `发票1.pdf` → `.invoice_cache/发票1.json`),后续步骤均从缓存读取。删除缓存后下次处理会重新提取。
|
||||
会话缓存在 `cases/<case_id>/.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/<session_id>/`,每次上传生成独立会话。
|
||||
页面地址由 `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 上的工具清单,不是进程内对象)。
|
||||
|
||||
@@ -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/<id>/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/<id>/ 的唯一实现:阶段由产物推导,无状态机
|
||||
├── 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<br/>差旅填报流程]
|
||||
TravelInfo -->|报销明细| Bot_T
|
||||
TravelInfo -->|支付方式| Bot_T
|
||||
TravelInfo -->|补助清单| Bot_T
|
||||
TravelInfo -->|附件清单| Bot_T
|
||||
Bot_T --> Submit_T[差旅报销提交]
|
||||
|
||||
NormalInfo -->|报销说明| Bot_G[infra/browser/normal.py<br/>普通填报流程]
|
||||
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/<session_id>/.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/<session_id>/`:
|
||||
|
||||
| 产物 | 说明 |
|
||||
|------|------|
|
||||
| `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` 含敏感信息,请勿提交到公开仓库
|
||||
- **发票类型区分**:差旅发票(高铁票/酒店住宿)不会生成易耗品出库单,差旅报销填报流程已完整实现(含差旅信息提取、明细录入、支付方式、补助清单、附件上传)
|
||||
| [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) | 改造决策、实测数据、踩坑记录 |
|
||||
|
||||
+1
-1
@@ -4,5 +4,5 @@
|
||||
"default_name": "默认报销人姓名",
|
||||
"default_card_no": "默认公务卡号",
|
||||
"default_person_id": "默认人员编号",
|
||||
"consumable_storage": "躬行楼 C205"
|
||||
"upload_pin": "手机上传页访问码,可留空"
|
||||
}
|
||||
|
||||
+18
-11
@@ -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`(有骨架与规则一致性的用例),否则骨架与规则会漂移。
|
||||
|
||||
-856
@@ -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/<session_id>` | 移动端上传页面 |
|
||||
| 3 | POST | `/api/session` | 创建会话 |
|
||||
| 4 | POST | `/api/upload/<session_id>` | 上传文件(PDF/图片) |
|
||||
| 5 | GET | `/api/files/<session_id>` | 列出会话目录中的文件 |
|
||||
| 6 | GET | `/api/download/<session_id>/<filename>` | 下载生成的文件 |
|
||||
| 7 | POST | `/api/mobile-upload/<session_id>` | 移动端上传图片 |
|
||||
| 8 | GET | `/api/config/<session_id>` | 获取会话配置 |
|
||||
| 9 | GET | `/api/data/<session_id>` | 获取发票数据(JSON) |
|
||||
| 10 | POST | `/api/save/<session_id>` | 保存编辑后的发票数据 |
|
||||
| 11 | POST | `/api/process/<session_id>` | 启动管道处理(仅发票提取,不自动提交) |
|
||||
| 12 | GET | `/api/logs/<session_id>` | SSE 日志流 |
|
||||
| 13 | POST | `/api/submit-financial/<session_id>` | 提交到财务系统 |
|
||||
| **14** | **GET** | **`/api/agent/state/<session_id>`** | **获取 Agent 会话状态** |
|
||||
| **15** | **POST** | **`/api/agent/process/<session_id>`** | **启动 Agent 多轮处理(主入口)** |
|
||||
| **16** | **POST** | **`/api/agent/supplement/<session_id>`** | **补充文件后重新分析** |
|
||||
| **17** | **POST** | **`/api/agent/user-supplement/<session_id>`** | **通过文字补充信息** |
|
||||
| **18** | **POST** | **`/api/agent/force-submit/<session_id>`** | **强制提交,跳过校验** |
|
||||
|
||||
> 加粗条目为 Agent 多轮校验流程新增接口。
|
||||
|
||||
### 入口选择建议
|
||||
|
||||
- **推荐使用** `/api/agent/process`:完整流程,包含发票提取、LLM 信息校验、自动提交财务系统。
|
||||
- **仅发票提取** `/api/process`:跳过 Agent 校验,只做文档解析和发票分类。适合调试发票提取本身,或仅需导出 CSV 的场景。
|
||||
|
||||
---
|
||||
|
||||
## 会话与目录
|
||||
|
||||
- 调用 `POST /api/session` 获得 `session_id`
|
||||
- 该会话下所有文件存放在 `src/web/uploads/<session_id>/`
|
||||
- 典型产物:`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/<session_id>
|
||||
Content-Type: multipart/form-data
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| file | File | PDF 发票或支付截图 |
|
||||
|
||||
**响应(成功):**
|
||||
|
||||
```json
|
||||
{ "ok": true, "filename": "1. 电容一批.pdf" }
|
||||
```
|
||||
|
||||
**响应(失败):**
|
||||
|
||||
```json
|
||||
{ "error": "未选择文件" }
|
||||
```
|
||||
|
||||
HTTP `400`
|
||||
|
||||
---
|
||||
|
||||
### 3. 列出会话文件
|
||||
|
||||
```
|
||||
GET /api/files/<session_id>
|
||||
```
|
||||
|
||||
**响应:**
|
||||
|
||||
```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/<session_id>/<filename>
|
||||
```
|
||||
|
||||
**响应:** 文件二进制流,带 `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/<session_id>
|
||||
```
|
||||
|
||||
返回移动端 HTML 页面。
|
||||
|
||||
---
|
||||
|
||||
### 6. 移动端上传图片
|
||||
|
||||
```
|
||||
POST /api/mobile-upload/<session_id>
|
||||
Content-Type: multipart/form-data
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| file | File | 图片文件 |
|
||||
|
||||
逻辑与 `POST /api/upload/<session_id>` 相同。
|
||||
|
||||
---
|
||||
|
||||
### 7. 获取会话配置
|
||||
|
||||
```
|
||||
GET /api/config/<session_id>
|
||||
```
|
||||
|
||||
获取当前会话的配置,供前端回填表单。优先读取会话目录下的 `config.json`,未找到则使用项目全局配置。
|
||||
|
||||
**响应:**
|
||||
|
||||
```json
|
||||
{
|
||||
"username": "",
|
||||
"password": "",
|
||||
"default_name": "",
|
||||
"default_card_no": "",
|
||||
"default_person_id": "",
|
||||
"consumable_storage": ""
|
||||
}
|
||||
```
|
||||
|
||||
注意:`password` 字段始终返回空字符串。
|
||||
|
||||
---
|
||||
|
||||
### 8. 获取发票数据
|
||||
|
||||
```
|
||||
GET /api/data/<session_id>
|
||||
```
|
||||
|
||||
**响应:**
|
||||
|
||||
```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/<session_id>
|
||||
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/<session_id>/%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/<session_id>
|
||||
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/<session_id>/config.json`。
|
||||
|
||||
**响应(立即):**
|
||||
|
||||
```json
|
||||
{ "status": "started" }
|
||||
```
|
||||
|
||||
处理在后台线程执行,进度与结果通过 `GET /api/logs/<session_id>`(SSE)获取。
|
||||
|
||||
**SSE 完成时 `result` 示例(成功):**
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"elapsed": "45.2s",
|
||||
"invoice_count": 4,
|
||||
"csv_url": "/api/download/<session_id>/invoice_summary.csv",
|
||||
"travel_count": 2,
|
||||
"general_count": 2,
|
||||
"doc_url": "/api/download/<session_id>/%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/<session_id>
|
||||
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/<session_id>
|
||||
```
|
||||
|
||||
**前置条件:**
|
||||
|
||||
- 会话目录存在 `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/<session_id>
|
||||
```
|
||||
|
||||
**响应:**
|
||||
|
||||
```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/<session_id>
|
||||
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/<session_id>
|
||||
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/<session_id>
|
||||
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/<session_id>
|
||||
```
|
||||
|
||||
当 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 校验<br/>最多 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)。
|
||||
+304
@@ -0,0 +1,304 @@
|
||||
---
|
||||
last_reviewed: 2026-10-19
|
||||
---
|
||||
|
||||
# MCP 接口文档
|
||||
|
||||
本项目的**唯一业务入口**是 MCP 服务器:`python -m src.mcp.server`(stdio 传输),工具名在宿主里表现为 `mcp__arb__<tool>`。
|
||||
没有 Web 界面,没有 CLI 产品入口,本项目也不调用任何大模型——**读图与综合判断由宿主 Agent 完成**,
|
||||
本项目只提供确定性工具(渲染、金额匹配、规则校验、文档生成、浏览器填报)。
|
||||
|
||||
## 注册到宿主
|
||||
|
||||
DSH(profile patch,`~/.dsh/profiles/<profile>/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>/` 目录(`case_id` 形如 `20261019-6月毕业采集`)。
|
||||
**阶段完全由产物推导**,没有状态机文件,可随时断电重来、可人工改文件后继续。
|
||||
|
||||
```
|
||||
cases/<case_id>/
|
||||
├── case.json 名称、类型、上传令牌(切勿提交)
|
||||
├── config.json 可选,会话级凭据覆盖(白名单字段)
|
||||
├── <材料>.pdf|jpg 发票、支付截图、申请单原件
|
||||
├── inbox/ 手机扫码上传落地点,case_add_files(source="inbox") 收取
|
||||
├── pages/<stem>/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/<case_id>/`
|
||||
- 文本层与读图结论冲突时(尤其编号、卡号、金额位数)必须复核,不要默认信任某一路
|
||||
|
||||
## 尚未实现(不要调用,也不要在回答里承诺)
|
||||
|
||||
浏览器填报(`browser_*`)、提交预览与执行(`submit_*`)。需要这些步骤时告知用户该环节还在建设中,
|
||||
可临时用旧的 Web 流程(代码在提交 `5f57400` 里)。
|
||||
|
||||
---
|
||||
|
||||
# 工具参考
|
||||
|
||||
以下内容由运行中的服务器 schema 自动生成,与代码同源(`scripts/mcp_smoke.py` 会打印同一份清单)。
|
||||
|
||||
## 会话与材料
|
||||
|
||||
每个报销任务是一个会话目录 `cases/<case_id>/`,阶段由产物推导。
|
||||
|
||||
### `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`
|
||||
|
||||
把单张票据的抽取结果写入会话缓存(按 <stem><suffix>.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 | 否 | — | — |
|
||||
+8
-5
@@ -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/` 目录下,不混入本目录。
|
||||
维护规范、实施方案、经验总结等内部资料统一放在 `.agents/`;宿主 Agent 的报销操作纪律在
|
||||
`.agents/skills/reimbursement/SKILL.md`。
|
||||
|
||||
+142
-313
@@ -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/<case_id>/`(整个目录已 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/低值易耗品申购_<case>.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/<id>/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/<id>/screenshots/NNN-<step>.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/<id>/` 下,路径用正斜杠 |
|
||||
| 宿主看不到新增工具 | 旧 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/<case_id> --exclude 千斤顶
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 附录:API 文档
|
||||
## 相关文档
|
||||
|
||||
详细的 API 接口文档见 [API.md](./API.md)。
|
||||
- [docs/MCP.md](./MCP.md) — 17 个工具的参数与返回约定
|
||||
- [.agents/skills/reimbursement/SKILL.md](../.agents/skills/reimbursement/SKILL.md) — 宿主 Agent 的操作纪律
|
||||
- [AGENTS.md](../AGENTS.md) — 开发约束与踩坑记录
|
||||
|
||||
+8
-15
@@ -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 成功不代表宿主能看到工具。
|
||||
|
||||
+22
-85
@@ -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/<id>/ 唯一实现:材料·收件箱·产物·阶段推导·上传令牌
|
||||
├── 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) |
|
||||
|
||||
**注意**:差旅发票和普通发票不支持混报,混合时会报错。
|
||||
- **判断在上,事实在下**:读图、综合、决定交给宿主 Agent;本项目只做渲染、匹配、校验、生成
|
||||
- **无状态**:阶段从磁盘产物推导,进程随时可杀可重启
|
||||
- **不承诺未完成的事**:INSTRUCTIONS 只列已注册工具,`browser_*` 与 `submit_*` 尚未实现
|
||||
|
||||
+9
-11
@@ -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` 判定宿主产出的数据是否完整,绝不代替宿主填内容
|
||||
|
||||
@@ -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/` 读写、路径推导(`<stem><ext>.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` | 写盘;支付主键形如 `"<card file> (¥amount)"`,未匹配走 `__unmatched__` |
|
||||
|
||||
## 缓存机制
|
||||
## 已知文本层陷阱(务必复核)
|
||||
|
||||
提取结果缓存在 `.invoice_cache/` 目录中,文件名与源文件同名(`发票1.pdf` → `.invoice_cache/发票1.json`),避免重复调用 LLM。
|
||||
- 电子发票文本层把所有**标签**排完再排所有**值**,不是一行一条,别按行配对
|
||||
- 康熙部首类字符(U+2F47 形似的「日」、门 的部首形式)NFKC 之后仍不变形,字段名看着对其实错
|
||||
- 文本层与读图出现过真实分歧(发票号一位之差、银行账号不同)→ 冲突时两路都不许默认采信,放大该区域重看
|
||||
|
||||
+12
-12
@@ -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` 只代表贪心跑完了,不代表配对对了**——调用方必须逐条复核,等额发票尤其容易配错。
|
||||
|
||||
@@ -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` 是刻意约束,不要为了让宿主省事而改掉。
|
||||
|
||||
+12
-12
@@ -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)需要磁盘锁,别假设只有一个服务器实例
|
||||
|
||||
+15
-32
@@ -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/<id>/screenshots/NNN-<step>.png`,并用 `.browser.lock`(PID + 心跳)防止多代进程
|
||||
抢同一个浏览器;P4 再加提交前预览与一次性 token 护栏(提交不可逆)。
|
||||
|
||||
## 填报流程
|
||||
## 注意
|
||||
|
||||
### 差旅报销(travel)
|
||||
1. 填写基本信息(事由、地点、日期、项目编号)
|
||||
2. 添加差旅明细(交通费用逐条录入)
|
||||
3. 填写支付方式(公务卡刷卡记录)
|
||||
4. 填写补助清单(按天计算交通补助 + 伙食补助)
|
||||
5. 上传附件(发票、申请单等)
|
||||
|
||||
### 普通报销(normal)
|
||||
1. 填写基本信息(报销事由、金额)
|
||||
2. 填写发票明细(总数、总金额)
|
||||
3. 填写支付方式
|
||||
4. 上传附件
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 浏览器填报会启动 Chromium,请勿手动干扰自动化流程
|
||||
- 调试截图保存在 `images/` 目录
|
||||
- Web 模式以无头模式运行
|
||||
- 会打开 Chromium,自动化期间不要手动点页面
|
||||
- 调试截图落在 `images/`(已 gitignore,不进仓库)
|
||||
- 步骤函数最好返回「填了什么 / 什么失败」的结构,P3 可直接复用
|
||||
|
||||
@@ -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/<id>/artifacts/`,不再污染项目根目录(旧实现把带时间戳的产物生成在模板旁边,
|
||||
会让下次模板查找命中上次的产物)
|
||||
- 表头在第 2 行、数据从第 3 行起、末尾追加合计行;列位与行号常量在 `requisition.py` 顶部
|
||||
|
||||
## 易耗品出库单
|
||||
|
||||
- 需要 **Windows + Microsoft Word + pywin32**
|
||||
- 模板文件为项目根目录的 `易耗品、出库单.doc`
|
||||
- 填写规则:日期用当天日期,品名/规格/数量/单价从 CSV 解析,字体统一宋体五号
|
||||
> Word 版易耗品出库单(`consumable.py`)已删除:模板自提交 `63b97c6`(2026-08-31)起不在仓库,
|
||||
> 那条路径此后一直静默失败。历史实现看 git。
|
||||
|
||||
@@ -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()` | 构建普通发票信息提取系统提示词 |
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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")
|
||||
@@ -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
|
||||
```
|
||||
@@ -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 协商阶段会先起一个临时探针进程
|
||||
@@ -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/<case_id>` | 服务端生成的二维码 PNG(segno) |
|
||||
| `GET /u/<case_id>` | 手机上传页(令牌在 URL 里) |
|
||||
| `POST /u/<case_id>` | 接收文件,落 `cases/<id>/inbox/` |
|
||||
| `GET /api/inbox/<case_id>` | 该会话待收数量(只给数量,不给内容) |
|
||||
|
||||
## 安全边界
|
||||
|
||||
- 默认 `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/<id>/inbox/` 里的文件由宿主调 `case_add_files(source='inbox')` 收取进会话,收件器本身不碰任何业务逻辑。
|
||||
+15
-14
@@ -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/
|
||||
```
|
||||
- **Office COM**(申购单):需要本机装有 Excel,改动后用真实生成验证(约 2-5 秒,产物单元格肉眼确认)
|
||||
- **Playwright 填报**:需要真实财务系统账号,手工验证
|
||||
- **MCP wire 层**:用 `make mcp-smoke`(真起 stdio 子进程握手、列工具、调一次 `case_list`)
|
||||
|
||||
测试不碰 `cases/` 真实目录,一律用 `tmp_path` 造会话。
|
||||
|
||||
Reference in New Issue
Block a user