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
+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` 是刻意约束,不要为了让宿主省事而改掉。