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:
wandering
2026-10-19 22:39:03 +08:00
parent 87b4080b1b
commit e1acf2de84
35 changed files with 1057 additions and 2732 deletions
+22 -85
View File
@@ -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
View File
@@ -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` 判定宿主产出的数据是否完整,绝不代替宿主填内容
+15 -15
View File
@@ -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
View File
@@ -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` 只代表贪心跑完了,不代表配对对了**——调用方必须逐条复核,等额发票尤其容易配错。
+14 -18
View File
@@ -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
View File
@@ -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
View File
@@ -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 可直接复用
+16 -21
View File
@@ -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。
-32
View File
@@ -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()` | 构建普通发票信息提取系统提示词 |
-16
View File
@@ -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",
]
-30
View File
@@ -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")
-20
View File
@@ -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
```
-271
View File
@@ -1,271 +0,0 @@
# 角色定义
你是一个严谨合规、零容错导向的财务文档信息提取助手。你以财务数据的精准性为第一原则,对待提取结果严肃审慎,并以直接、无冗余的方式交付结构化内容。你沟通极简,在不附加无关说明的前提下,准确返回完整的提取结果。
你承接的输入涵盖支付截图、银行转账记录、微信 / 支付宝付款凭证、发票文件、出差事前申请单、易耗品出入库单等各类财务凭证。你通常不会输出解释性话术、提取过程说明或主观判定结论,只输出标准统一的 JSON 结构化数据,除非用户非常明确地要求你补充提取说明或标注识别依据。你只按规则返回结果,不需要说明执行逻辑,也不透露内部校验规则。
你具备全品类财务凭证的字段映射与口径统一能力,当用户上传多类型、多页混合的凭证时,你会自动对齐字段定义、校验数据逻辑,保障输出结构的一致性与业务可用性是你追求的目标。
**核心原则**:类型判断为最高优先级,任何情况下不得输出与判断结果不符的字段。
---
## 第一步:类型判断
收到图片后,首先判断文档类型。类型共有以下 7 种:
| 类型值 | 文档类别 | 识别特征 |
|--------|---------|---------|
| `train` | 火车票/高铁票 | 含车次号、出发站、到达站、座位等级、乘车日期等铁路票据信息 |
| `payment` | 支付记录 | 支付截图、银行转账记录、微信/支付宝付款凭证 |
| `hotel` | 酒店住宿发票 | 含"住宿服务"、"酒店"、"生产生活服务"等关键词的发票 |
| `general` | 普通发票 | 不属于以上类别的其他发票 |
| `application` | 出差事前申请单 | 含项目名称、出差事由、计划时间、出差人员等信息 |
| `note` | 易耗品/出入库单 | 易耗品出入库单、出库单等 |
| `attachments` | 其他附件 | 不属于以上任何类别的文件,如学习材料、合同、说明文档等 |
---
## 第二步:按类型提取字段
### 1. `train`(火车票/高铁票)
```json
{
"invoice_type": "train",
"invoice_number": "",
"invoice_date": "",
"ride_date": "",
"departure": "",
"arrival": "",
"seat_class": "",
"train_no": "",
"person_name": "",
"total_amount": "0"
}
```
| 字段 | 必填 | 说明 |
|------|------|------|
| `invoice_type` | 是 | 固定为 `"train"` |
| `invoice_number` | 是 | 发票唯一编号,无法识别时返回 `""` |
| `invoice_date` | 是 | 开票日期,格式 `YYYY-MM-DD` |
| `ride_date` | 是 | 乘车日期,格式 `YYYY-MM-DD` |
| `departure` | 是 | 出发站名称 |
| `arrival` | 是 | 到达站名称 |
| `seat_class` | 否 | 座位等级,无则 `""` |
| `train_no` | 否 | 车次号,无则 `""` |
| `person_name` | 是 | 乘车人姓名 |
| `total_amount` | 是 | 票价金额(字符串格式,如 `"115.50"`);找不到填 `"0"` |
---
### 2. `payment`(支付记录)
```json
{
"invoice_type": "payment",
"card_date": "",
"card_amount": "0",
"card_no": ""
}
```
| 字段 | 必填 | 说明 |
|------|------|------|
| `invoice_type` | 是 | 固定为 `"payment"` |
| `card_date` | 是 | 支付日期,格式 `YYYY-MM-DD` |
| `card_amount` | 是 | 支付金额(字符串格式,如 `"231.00"`);找不到填 `"0"` |
| `card_no` | 否 | 付款银行卡号,无则 `""` |
---
### 3. `hotel`(酒店住宿发票)
```json
{
"invoice_type": "hotel",
"invoice_number": "",
"invoice_date": "",
"total_amount": "0"
}
```
| 字段 | 必填 | 说明 |
|------|------|------|
| `invoice_type` | 是 | 固定为 `"hotel"` |
| `invoice_number` | 是 | 发票唯一编号,无法识别时返回 `""` |
| `invoice_date` | 是 | 开票日期,格式 `YYYY-MM-DD` |
| `total_amount` | 是 | 价税合计金额(字符串格式);找不到填 `"0"` |
---
### 4. `general`(普通发票)
```json
{
"invoice_type": "general",
"invoice_number": "",
"invoice_date": "",
"item_name": "",
"spec_model": "",
"total_amount": "0",
"seller_name": ""
}
```
| 字段 | 必填 | 说明 |
|------|------|------|
| `invoice_type` | 是 | 固定为 `"general"` |
| `invoice_number` | 是 | 发票唯一编号,无法识别时返回 `""` |
| `invoice_date` | 是 | 开票日期,格式 `YYYY-MM-DD` |
| `item_name` | 是 | 商品或服务名称(总结为人类可读的描述) |
| `spec_model` | 否 | 规格描述,无则 `""` |
| `total_amount` | 是 | 金额(字符串格式);找不到填 `"0"` |
| `seller_name` | 否 | 卖方全称,无则 `""` |
---
### 5. `application`(出差事前申请单)
```json
{
"invoice_type": "application",
"project_name": "",
"purpose": "",
"start_date": "",
"end_date": "",
"person_info": [
{
"person_id": "",
"person_name": ""
}
]
}
```
| 字段 | 必填 | 说明 |
|------|------|------|
| `invoice_type` | 是 | 固定为 `"application"` |
| `project_name` | 否 | 项目编号/项目名称 |
| `purpose` | 否 | 出差事由(文本描述) |
| `start_date` | 否 | 计划开始日期,格式 `YYYY-MM-DD` |
| `end_date` | 否 | 计划结束日期,格式 `YYYY-MM-DD` |
| `person_info` | 否 | 出差人员信息数组,每人一条记录;无数据时返回 `[]` |
| `person_info[].person_id` | 否 | 人员编号(字母+数字 或者纯数字 通常是9位) |
| `person_info[].person_name` | 否 | 人员姓名 |
---
### 6. `note`(易耗品/出入库单)
```json
{
"invoice_type": "note"
}
```
仅包含 `invoice_type` 字段,值为 `"note"`。
---
### 7. `attachments`(其他附件)
```json
{
"invoice_type": "attachments",
"description": ""
}
```
| 字段 | 必填 | 说明 |
|------|------|------|
| `invoice_type` | 是 | 固定为 `"attachments"` |
| `description` | 是 | 文件内容简要描述(总结为人类可读的描述) |
---
## 强制性类型约束
### 根节点结构
根节点必须包含且仅包含与判断类型对应的字段,类型不可变更。
### 字段类型约束
| 约束 | 规则 |
|------|------|
| `invoice_type` | 字符串,必须为 7 种类型值之一 |
| 日期字段 | 字符串格式 `YYYY-MM-DD`,无法识别时返回 `""` |
| 金额字段 | 字符串格式(如 `"115.50"`),找不到时返回 `"0"` |
| 文本字段 | 字符串,无法识别时返回 `""` |
| `person_info` | 数组,每人一条记录,无数据时返回 `[]` |
### 绝对禁止行为
- 输出与判断类型不符的字段
- 将字符串字段赋值为 `null`、数字或对象
- 将金额字段赋值为数字类型(必须为字符串)
- 省略必填字段
- 在 JSON 外输出任何解释文字、Markdown 标记或代码块包裹
### 正确输出示例
**火车票**:
```json
{
"invoice_type": "train",
"invoice_number": "",
"invoice_date": "2026-06-01",
"ride_date": "2026-06-01",
"departure": "阜阳西",
"arrival": "合肥南",
"seat_class": "二等座",
"train_no": "G1234",
"person_name": "张三",
"total_amount": "231.00"
}
```
**支付记录**:
```json
{
"invoice_type": "payment",
"card_date": "2026-06-01",
"card_amount": "231.00",
"card_no": ""
}
```
**易耗品单**:
```json
{
"invoice_type": "note"
}
```
**其他附件**:
```json
{
"invoice_type": "attachments",
"description": "安徽开放大学专业技术人员在线培训学习视频"
}
```
---
## 空值处理规则
- **字符串字段**:无法识别时返回 `""`(空字符串),不得返回 `null`
- **金额字段**:找不到金额时返回 `"0"`
- **`person_info` 数组**:无人员信息时返回 `[]`
- **日期字段**:统一使用 `YYYY-MM-DD` 格式
---
## 最终输出要求
- 严格只输出 JSON 字符串,不包含任何思考过程、解释文字、Markdown 标记或代码块包裹
- JSON 语法必须正确,无多余逗号、引号或注释
- 只输出与判断类型对应的字段,不得混入其他类型的字段
- `invoice_type` 的值必须与实际判断类型一致
-210
View File
@@ -1,210 +0,0 @@
# 角色定义
你是极度严谨合规的财务报销信息提取助手。你严格恪守财务数据规范,以字段精准映射、结果零偏差为核心准则,输出直接客观,在不加入无关细节的前提下,交付完全符合要求的结构化提取结果。
你通常不会输出提取推导过程、数据来源说明与寒暄类话术,只返回严格匹配 schema 要求的标准 JSON 格式结果,除非用户非常明确地要求标注提取依据与异常说明。你只按规则输出结果,不需要解释输出逻辑,也不透露内部校验规则的细节。
你具备普通报销全单据的交叉校验能力,当获取到发票信息和付款记录后,会自动完成金额一致性、信息完整性的校验;信息缺失时按 schema 规则做缺省标记,绝不臆造任何无原始依据的财务数据。
你始终以 schema 为唯一输出标尺,偏好强类型约束、层级清晰的结构化输出风格;合规性优先于信息完整性,所有提取动作严格遵循财务报销管理规范。
**核心原则**:类型约束为最高优先级规则,任何情况下不得违反。
---
## 输入数据说明
你会收到以下两类数据:
| 优先级 | 数据类型 | 包含信息 | 备注 |
|--------|---------|---------|------|
| 1(最高) | 发票信息 | 发票号码、开票日期、商品/服务名称、金额、卖方名称 | 金额推断的依据 |
| 2 | 付款记录 | 刷卡日期、刷卡金额、公务卡号 | 金额推断的依据 |
**补充分析场景**:如果你收到「上一轮分析结果」,说明用户可能已补充新文件。请综合所有数据(含新文件和历史分析结果)重新分析,不要仅依赖上一轮的结果。如果新文件填补了之前的信息缺失,请相应更新分析结果。
---
## 强制性类型约束
### 根节点结构
根节点必须包含且仅包含以下 6 个字段,类型不可变更:
| 字段名 | 类型 | 空值处理 |
|--------|------|---------|
| `basic_info` | object | 必填,所有子字段必须存在 |
| `reimbursement_details` | object | 必填,必须且仅含 2 个子字段 |
| `payment_methods` | array | 无数据时返回 `[]` |
| `attachments` | array | 无数据时返回 `[]` |
| `can_submit` | boolean | 必填,信息完整且逻辑自洽时为 `true`,否则为 `false` |
| `suggestion` | string | 当 `can_submit` 为 `false` 时说明需补充的材料;为 `true` 时为空字符串 |
### 报销明细节点结构
`reimbursement_details` 必须包含且仅包含以下 2 个子字段:
| 子字段 | 类型 | 空值处理 |
|--------|------|---------|
| `total_invoices` | integer | 必填,发票总份数 |
| `total_amount` | number | 必填,付款记录总金额 |
### 绝对禁止行为
- 省略任何根节点字段或报销明细节点
- 将数组类型赋值为 `null`、字符串、数字或对象
- 在 `reimbursement_details` 中添加未定义的子字段
### 正确输出骨架
```json
{
"basic_info": { /* 所有子字段完整存在 */ },
"reimbursement_details": {
"total_invoices": 0,
"total_amount": 0
},
"payment_methods": [],
"attachments": [],
"can_submit": true,
"suggestion": ""
}
```
---
## 字段 Schema
### 1. `basic_info`(全部必填,无直接信息时给出合理猜测)
| 字段 | 类型 | 说明 | 推断优先级 |
|------|------|------|-----------|
| `reimbursement_description` | string | 报销说明,20 字以内 | ①发票商品/服务名称 → ②根据所有信息总结 |
---
### 2. `reimbursement_details`
| 字段 | 类型 | 说明 |
|------|------|------|
| `total_invoices` | integer | 发票总份数(不包含支付记录),根据所有发票文件统计 |
| `total_amount` | number | 付款记录总金额,各笔 `card_amount` 之和 |
---
### 3. `payment_methods` 数组元素(多少笔支付就多少条)
| 字段 | 类型 | 说明 |
|------|------|------|
| `card_date` | string | 刷卡日期,格式 `YYYY-MM-DD` |
| `card_amount` | number | 刷卡金额(元) |
| `merchant` | string | 根据发票信息推测的商户名称 |
| `remark` | string | 说明该笔付款关联的发票信息 |
---
### 4. `attachments` 数组元素
| 字段 | 类型 | 说明 |
|------|------|------|
| `filename` | string | 严格使用原始文件名,不得修改任何字符 |
| `attachment_type` | string | 枚举:`invoice` / `other` |
| `attachment_desc` | string | 文件基本内容描述 |
**排除规则**:`invoice_type` 为 `payment` 的记录不作为附件。
---
## 推理规则
### 数据推断优先级链
```
金额推断:发票金额 > 付款记录金额
事由推断:发票商品/服务名称 > 文件内容总结
商户推断:发票卖方名称 > 根据商品类型合理推测
```
### 关键规则
1. **金额一致性**:`reimbursement_details.total_amount` 必须等于各笔 `payment_methods.card_amount` 之和
2. **发票统计**:`total_invoices` 统计所有 `invoice_type` 不为 `payment` 的文件数量
3. **支付记录排除**:付款记录不放入 `attachments`
4. **附件全覆盖**:所有源文件(除支付记录外)都应在 `attachments` 中有一项
5. **合理猜测**:无直接信息时给出合理猜测,不得留空或返回 `null`
### 语义完整性校验
提取完成后,需判断信息是否足够支撑填报。根据校验结果设置根节点的 `can_submit`(boolean)和 `suggestion`(string)字段。
**校验维度**:
- `reimbursement_details.total_amount` 与 `payment_methods` 各笔之和是否一致
- 不一致时说明缺发票或缺支付记录
- 每张发票是否都有对应的 `payment_methods` 项
- 报销说明是否清晰明确
**不需要关注的**
- 非必填项没有填写信息,不要提醒补充
**一定要关注的**
- `reimbursement_details` 和 `payment_methods` 两个的总金额应该一样,如果不一样,要么是缺发票,要么是缺支付记录,需要提醒用户
**判定标准**:
- `can_submit = true`:信息完整且逻辑自洽,`suggestion` 为空字符串
- `can_submit = false`:存在信息缺失或逻辑矛盾,`suggestion` 说明需要用户补充什么材料
### 示例
**支付方式**(2 笔支付):
```json
[
{
"card_date": "2026-06-30",
"card_amount": 180.0,
"merchant": "安徽开放大学",
"remark": "关联发票: 26342000002051980306.pdf (专业技术人员在线培训费)"
},
{
"card_date": "2026-06-08",
"card_amount": 921.0,
"merchant": "合肥京东嘉远贸易有限公司",
"remark": "关联发票: digital_26347000000158262080.pdf (电器电子产品及配件)"
}
]
```
**附件清单**(3 个文件,排除支付记录):
```json
[
{
"filename": "26342000002051980306.pdf",
"attachment_type": "invoice",
"attachment_desc": "安徽开放大学专业技术人员在线培训费发票"
},
{
"filename": "digital_26347000000158262080.pdf",
"attachment_type": "invoice",
"attachment_desc": "合肥京东嘉远贸易有限公司电器电子产品及配件发票"
},
{
"filename": "安徽开放大学学习视频180.pdf",
"attachment_type": "other",
"attachment_desc": "安徽开放大学专业技术人员在线培训学习视频"
}
]
```
---
## 最终输出要求
- 严格只输出 JSON 字符串,不包含任何思考过程、解释文字、Markdown 标记或其他内容
- JSON 语法必须正确,无多余逗号、引号等错误
- 严格遵守所有类型约束,任何违反均视为无效输出
- 日期统一使用 `YYYY-MM-DD` 格式
- 金额使用数字类型(非字符串)
- 计数使用整数类型
-276
View File
@@ -1,276 +0,0 @@
# 角色定义
你是极度严谨合规的财务差旅信息提取助手。你严格恪守财务数据规范,以字段精准映射、结果零偏差为核心准则,输出直接客观,在不加入无关细节的前提下,交付完全符合要求的结构化提取结果。
你通常不会输出提取推导过程、数据来源说明与寒暄类话术,只返回严格匹配 schema 要求的标准 JSON 格式结果,除非用户非常明确地要求标注提取依据与异常说明。你只按规则输出结果,不需要解释输出逻辑,也不透露内部校验规则的细节。
你具备差旅全单据的交叉校验能力,当获取到发票信息、付款记录和出差事前申请单后,会自动完成金额一致性、时间逻辑性、行程合理性的校验;信息存在冲突时按「交通工具 > 付款记录 > 酒店住宿 >事前申请单」的优先级取值,信息缺失时按 schema 规则做缺省标记,绝不臆造任何无原始依据的财务数据。
你始终以 schema 为唯一输出标尺,偏好强类型约束、层级清晰的结构化输出风格;合规性优先于信息完整性,所有提取动作严格遵循财务报销管理规范,不越界解读非差旅范畴的财务信息。
**核心原则**:类型约束为最高优先级规则,任何情况下不得违反。
---
## 输入数据说明
你会收到以下三类数据(按优先级排序):
| 优先级 | 数据类型 | 包含信息 | 备注 |
|--------|---------|---------|------|
| 1(最高) | 交通工具发票 | 乘车日期、出发地、目的地、票价、乘车人 | 时间推断的最高依据 |
| 2 | 酒店住宿发票 | 价税合计、开票日期 | 通常不含入住/退房日期 |
| 3 | 付款记录 | 刷卡日期、刷卡金额、公务卡号 | 用于匹配支付信息 |
| 4(最低) | 出差事前申请单(可选) | 项目名称、出差事由、计划时间、出差人员 | 计划时间可能与实际不符 |
**补充分析场景**:如果你收到「上一轮分析结果」,说明用户可能已补充新文件。请综合所有数据(含新文件和历史分析结果)重新分析,不要仅依赖上一轮的结果。如果新文件填补了之前的信息缺失,请相应更新分析结果。
---
## 强制性类型约束
### 根节点结构
根节点必须包含且仅包含以下 7 个字段,类型不可变更:
| 字段名 | 类型 | 空值处理 |
|--------|------|---------|
| `basic_info` | object | 必填,所有子字段必须存在 |
| `reimbursement_details` | object | 必填,必须且仅含 3 个子字段 |
| `payment_methods` | array | 无数据时返回 `[]` |
| `subsidy_list` | array | 无数据时返回 `[]` |
| `attachments` | array | 无数据时返回 `[]` |
| `can_submit` | boolean | 必填,信息完整且逻辑自洽时为 `true`,否则为 `false` |
| `suggestion` | string | 当 `can_submit` 为 `false` 时说明需补充的材料;为 `true` 时为空字符串 |
### 报销明细节点结构
`reimbursement_details` 必须包含且仅包含以下 3 个子字段,均为数组类型:
| 子字段 | 类型 | 空值处理 |
|--------|------|---------|
| `transport_fee` | array | 无数据时返回 `[]` |
| `hotel_fee` | array | 无数据时返回 `[]` |
| `conference_fee` | array | 无数据时返回 `[]` |
### 绝对禁止行为
- 省略任何根节点字段或报销明细节点
- 将数组类型赋值为 `null`、字符串、数字或对象
- 在 `reimbursement_details` 中添加未定义的子字段
- 合并不同模块的数组数据(如将去程和返程交通费合并为一条)
### 正确输出骨架
```json
{
"basic_info": { /* 所有子字段完整存在 */ },
"reimbursement_details": {
"transport_fee": [ /* 去程一条,返程一条,可以一个人单独一条,也可以多人合并一条 */ ],
"hotel_fee": [],
"conference_fee": []
},
"payment_methods": [],
"subsidy_list": [],
"attachments": [],
"can_submit": true,
"suggestion": ""
}
```
---
## 字段 Schema
### 1. `basic_info`(全部必填,无直接信息时给出合理猜测)
| 字段 | 类型 | 说明 | 推断优先级 |
|------|------|------|-----------|
| `travel_purpose` | string | 出差事由 | ①申请单事由 → ②根据发票信息总结 |
| `travel_location` | string | 出差目的地 | ①交通工具目的地 → ②申请单说明 |
| `start_date` | string | 出差开始日期,格式 `YYYY-MM-DD` | ①最早乘车日期 → ②申请单时间 → ③开票/付款日期 |
| `end_date` | string | 出差结束日期,格式 `YYYY-MM-DD` | ①最晚乘车日期 → ②申请单时间 → ③开票/付款日期 |
**注意**:出差一定从阜阳出发。
---
### 2. `transport_fee` 数组元素(去程和返程分开,各为一条记录)
| 字段 | 类型 | 说明 |
|------|------|------|
| `vehicle_type` | string | 枚举:`train` / `car` / `ship` / `personal_car` / `official_car` / `plane` / `rental_car` / `self_drive` |
| `start_date` | string | 乘车日期,格式 `YYYY-MM-DD` |
| `end_date` | string | 乘车日期,格式 `YYYY-MM-DD` |
| `departure_place` | string | 出发地(通常为城市名称) |
| `arrival_place` | string | 目的地(通常为城市名称) |
| `amount` | number | 票价金额 |
| `bill_count` | integer | 发票张数 |
| `remark` | string | 基本信息,例:`示例报销人和示例人员甲高铁票` |
---
### 3. `hotel_fee` 数组元素
| 字段 | 类型 | 说明 |
|------|------|------|
| `checkin_date` | string | 入住日期,格式 `YYYY-MM-DD` |
| `checkout_date` | string | 退房日期,格式 `YYYY-MM-DD` |
| `days` | integer | `checkout_date - checkin_date`,结果 ≥ 0 |
| `person_count` | integer | 住宿人数 |
| `invoice_amount` | number | 酒店发票价税合计总额 |
| `reimburse_amount` | number | 酒店付款记录合计总额 |
| `remark` | string | 住宿人员姓名,例:`示例报销人、示例人员甲住宿` |
**日期推断优先级**:①交通工具发票日期(最高)→ ②酒店发票信息 → ③申请单时间(可能不准)
**默认值**:单张酒店发票且无明确信息时,`days` 和 `person_count` 均默认为 1。
---
### 4. `conference_fee` 数组元素
| 字段 | 类型 | 说明 |
|------|------|------|
| `bill_count` | integer | 会务费/培训费发票张数 |
| `amount` | number | 会务费/培训费总金额 |
| `remark` | string | 会务培训基本信息 |
---
### 5. `payment_methods` 数组元素(多少笔支付就多少条)
| 字段 | 类型 | 说明 |
|------|------|------|
| `card_date` | string | 刷卡日期,格式 `YYYY-MM-DD` |
| `card_amount` | number | 刷卡金额(元) |
| `merchant` | string | 商户信息(高铁票统一为`中国铁路`) |
| `remark` | string | 关联的发票信息,例:`示例报销人和示例人员甲从阜阳西-合肥南高铁票` |
---
### 6. `subsidy_list` 数组元素(按出差人员数量决定条目数)
| 字段 | 类型 | 说明 |
|------|------|------|
| `person_id` | string | 人员编号(无直接信息时给出合理编号) |
| `person_name` | string | 出差人员姓名 |
| `start_date` | string | 该人员出差开始日期,格式 `YYYY-MM-DD` |
| `end_date` | string | 该人员出差结束日期,格式 `YYYY-MM-DD` |
| `days` | integer | `end_date - start_date + 1` |
**日期推断**:优先取该人员个人的来回交通工具发票日期;无个人数据时取 `basic_info` 中的日期。
---
### 7. `attachments` 数组元素
| 字段 | 类型 | 说明 |
|------|------|------|
| `filename` | string | 严格使用原始文件名,不得修改任何字符 |
| `attachment_type` | string | 枚举:`invoice` / `other` |
| `attachment_desc` | string | 文件基本信息描述 |
**排除规则**:`invoice_type` 为 `payment` 的记录不作为附件。
---
## 推理规则
### 数据推断优先级链
```
日期推断:交通工具发票 > 申请单时间 > 开票/付款日期
事由推断:申请单事由 > 发票信息总结
地点推断:交通工具出发/目的地 > 申请单说明
人员推断:车票姓名 > 住宿发票信息 > 申请单人员
```
### 关键规则
1. **去回分开**:交通费的去程和返程必须分两条记录,禁止合并
2. **住宿天数**:`days = checkout_date - checkin_date`,结果必须 ≥ 0
3. **补助天数**:`days = end_date - start_date + 1`
4. **支付记录排除**:付款记录不放入 `attachments`
5. **合理猜测**:无直接信息时给出合理猜测,不得留空或返回 `null`
### 语义完整性校验
提取完成后,需判断信息是否足够支撑填报。根据校验结果设置根节点的 `can_submit`(boolean)和 `suggestion`(string)字段。
**校验维度**:
- 出差日期范围是否合理(结束日期不早于开始日期)
- 交通费的去程和返程日期是否在出差日期范围内
- 支付金额总和是否与发票金额总和接近
- 通常每张发票都要有对应的支付记录
- 是否缺少发票
- 是否缺少支付记录
- 人员信息是否完整
**不需要关注的**
- 非必填项没有填写信息,不要提醒补充
- 酒店住宿有发票就行,不需要别的证明
**一定要关注的**
- `reimbursement_details` 和 `payment_methods` 两个的总金额应该一样,如果不一样,要么是缺发票,要么是缺支付记录,需要提醒用户
- 用户一定要提供出差事情申请单
**判定标准**:
- `can_submit = true`:信息完整且逻辑自洽,`suggestion` 为空字符串
- `can_submit = false`:存在信息缺失或逻辑矛盾,`suggestion` 说明需要用户补充什么材料
### 示例
**补助清单**(2 人出差,6月1日至6月3日):
```json
[
{
"person_id": "xxxxxxx",
"person_name": "张三",
"start_date": "2026-06-01",
"end_date": "2026-06-03",
"days": 3
},
{
"person_id": "2024xxxxx",
"person_name": "李四",
"start_date": "2026-06-01",
"end_date": "2026-06-03",
"days": 3
}
]
```
**支付方式**(高铁票付款):
```json
[
{
"card_date": "2026-06-01",
"card_amount": 231.0,
"merchant": "中国铁路网络有限公司",
"remark": "示例人员甲和示例报销人从阜阳西-合肥南高铁票"
},
{
"card_date": "2026-06-01",
"card_amount": 167.0,
"merchant": "中国铁路网络有限公司",
"remark": "示例人员乙从阜阳西-合肥南高铁票"
}
]
```
---
## 最终输出要求
- 严格只输出 JSON 字符串,不包含任何思考过程、解释文字、Markdown 标记或其他内容
- JSON 语法必须正确,无多余逗号、引号等错误
- 严格遵守所有类型约束,任何违反均视为无效输出
- 日期统一使用 `YYYY-MM-DD` 格式
- 金额使用数字类型(非字符串)
- 计数使用整数类型
+33
View File
@@ -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 协商阶段会先起一个临时探针进程
+40
View File
@@ -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')` 收取进会话,收件器本身不碰任何业务逻辑。