Files
Auto-Finance/README.md
wandering 1b35f07fd7 refactor: 架构重组 — doc/bot → core/infra,新增 Agent 调度模块
- src/doc/ 拆分为 src/core/extraction/, matching/, validation/(核心业务逻辑)
- src/bot/ 重命名为 src/infra/browser/(浏览器自动化基础设施)
- fill_consumable_doc.py → src/infra/documents/consumable.py
- 新增 Agent 调度模块:coordinator.py, events.py, session.py,重构 orchestrator.py
- 更新 AGENTS.md、README.md 及所有子目录 README
2026-07-02 18:36:19 +08:00

439 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 财务报销自动化
自动从 PDF 发票或图片中提取信息,生成发票汇总表与易耗品出库单,并可选在财务系统中自动填报报销单。
**支持发票类型区分**:系统自动识别高铁票、酒店住宿等差旅发票与普通发票。差旅发票不生成易耗品出库单,走差旅报销流程;普通发票生成出库单,走普通报销流程。
## 项目结构
```
├── 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
│ │ └── 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/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
```
项目使用 `uv` 管理依赖,所有包版本锁定在 `uv.lock` 中,确保可复现。
| 依赖 | 用途 |
|------|------|
| 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`);购货人/领用人签字、备注保持空白
## 执行步骤说明
| 步骤 | 命令 | 说明 |
|------|------|------|
| 发票提取 | `--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` 含敏感信息,请勿提交到公开仓库
- **发票类型区分**:差旅发票(高铁票/酒店住宿)不会生成易耗品出库单,差旅报销填报流程已完整实现(含差旅信息提取、明细录入、支付方式、补助清单、附件上传)