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