docs: 文档全面对齐 MCP 架构,并脱敏公开操作指南

接口与入门:
- 新增 docs/MCP.md:注册方法(DSH patch + 通用 client)、返回信封、会话目录布局、标准流程、
  硬约束与未实现清单;工具参考段由**运行时 input_schema 自动生成**,与代码同源不会漂移
- 删除 docs/API.md(856 行 Flask 路由/SSE 文档,描述的端点已不存在)
- 重写根 README.md:为什么放弃自带前端、职责分工表、快速开始、注册配置、流程链、结构与风险
- 重写 AGENTS.md:命令表换成 mcp/mcp-smoke/upload;mypy 目标改 src;新增「提交纪律」
  (pre-commit 的 stash 窗口会回滚未暂存改动)、COM 与 Playwright 线程约束、多进程共存提醒

包文档:src/README.md 与各包 README 按现状重写;新增 src/mcp/README.md(写工具的规矩、
宿主侧四个坑)与 src/upload/README.md(路由表与安全边界);tests/scripts/config 三处索引更新。

提示词迁移:src/infra/llm/ 整个删除,三份系统提示词迁到 .agents/skills/reimbursement/references/
(invoice-fields / travel-info-fields / normal-info-fields),删掉 can_submit 与 suggestion
(save_info 也会主动剥离),「严格只输出 JSON」改为「交给对应工具落盘」;新增 SKILL.md 作为
宿主侧操作纪律(已被 DSH 热加载为 reimbursement 技能)。

作废处理:删除 .agents/docs/guides/系统事件流全景图.md(SSE 与 file_events/agent_events 通道已整体
移除,现在没有任何事件流);给项目架构全景图与架构分析-2026-06-15 加「已过时」横幅保留历史;
config.example.json 去 consumable_storage 加 upload_pin;.env.example 去 LLM 段并说明已不再自动加载 .env。

安全:docs/报销操作指南.md 的示例配置里原来写着**真实姓名、16 位公务卡号、内网 LLM 地址与工号**
(自 9fe5d70/2026-05-25 起在版本历史里),本次全部换成占位符。历史清洗与令牌轮换另议。

门禁(改动含 src/infra/llm 删除,故重跑全部门禁):
  ruff check PASS | ruff format --check PASS(42) | mypy src PASS(34) | deptry PASS(36) |
  pytest 107 passed | scripts/mcp_smoke.py PASS(17 工具 / instructions 1449 字节 / case_list ok)
申购单在依赖清理后另已实机复测:2.4s 生成,产物单元格读回正确。
This commit is contained in:
wandering
2026-10-19 22:39:03 +08:00
parent 87b4080b1b
commit e1acf2de84
35 changed files with 1057 additions and 2732 deletions
+7 -6
View File
@@ -2,11 +2,12 @@
本目录存放维护人员与自动化代理相关资料,主要用于项目运维,不属于对外公开的用户文档。
- standards/:存放维护人员需遵守的规范制度与校验规则
- plans/:存放实施方案与工作交接说明
- error-experience/、good-experience/:存放内部经验总结文档
- guides/:面向维护人员的工作流程及集成实操手册
- architecture/manifest.yaml:记录可读性检查所覆盖的文件路径
- `docs/plans/`:实施方案与阶段记录(当前主线是 `MCP化改造方案-2026-10-19.md`,含实测数据与踩坑)
- `docs/guides/`:内部实操手册(`项目架构全景图.md` 已标注改造前的结构,以方案文档为准)
- `docs/standards/`:规范制度;`docs/error-experience/`、`docs/good-experience/`:经验总结(按日期命名,保留历史原貌)
- `skills/`:Agent 技能定义(见 `skills/README.md`)
对外用户文档及说明文件请统一放置在 docs/ 目录下。
对外用户文档统一放 `docs/`(工具契约是 `docs/MCP.md`)。
> 已作废并删除:`docs/guides/系统事件流全景图.md`(SSE 与 file_events/agent_events 通道已随 Web/Agent 一起移除,
> 现在没有任何长连接事件流;需要看历史内容用 git 提交 `0c54dc8^`)。
@@ -1,657 +0,0 @@
# 系统事件流全景图
> 最后更新: 2026-06-15
> 用途: 排查 SSE 事件问题、提交流程中断、状态不一致等 Bug
---
## 一、核心概念
### 1.1 前后端状态映射
| 前端 `App.processState` | 后端 `AgentState` | 含义 |
|---|---|---|
| `idle` | `IDLE` | 初始状态,等待用户操作 |
| `processing` | `EXTRACTING` | LLM 正在分析文件 |
| `awaiting_supplement` | `AWAITING_SUPPLEMENT` | 信息不完整,等待用户补充 |
| `submitting` | `SUBMITTING` | 正在提交到财务系统 |
| `ready` | `READY` | 信息完整,可以提交 |
| `submitting` | `SUBMITTING` | 正在提交到财务系统 |
| `done` | `DONE` / `ERROR` | 流程结束(成功或失败) |
### 1.2 通信机制
```mermaid
sequenceDiagram
participant F as 前端
participant S as SSE连接
participant B as 后端线程
F->>B: POST /api/agent/process/:sid
B-->>F: {status: "started"}
F->>S: GET /api/logs/:sid (SSE长连接)
S-->>F: message: file_progress (轮询 file_events.log)
S-->>F: message: llm_stream (轮询 llm_stream.log)
S-->>F: message: agent_* (轮询 agent_events.log)
S-->>F: message: done (检测到 result.json)
S->>S: 连接关闭
```
**关键约束**:
- 后端所有处理接口均返回 `{status: "started"}`,实际工作在 daemon 线程中执行
- SSE 通过每 0.5 秒轮询 4 个日志文件实现(非原生 SSE,是长轮询模拟)
- `result.json` 的原子写入:先写 `.tmp`,再 `replace()` 重命名
- SSE 超时:600 秒后自动断开
### 1.3 操作信号点清单
每个 API 操作涉及的信号文件生命周期如下。**新增或修改信号文件时必须同步更新此清单**。
| 序号 | 操作 | API 端点 | 线程启动时清理 | 一次写入且不被清理 | 轮次结束时写入 |
|---|---|---|---|---|---|
| 1 | 初始处理 | `POST /api/agent/process/:sid` | `llm_stream.log`, `agent_events.log`, `result.json` | `file_events.log`, `session.log` | `result.json` |
| 2 | 补充文件 | `POST /api/agent/supplement/:sid` | `llm_stream.log`, `agent_events.log`, `result.json` | `file_events.log`, `session.log` | `result.json` |
| 3 | 文字补充 | `POST /api/agent/user-supplement/:sid` | `llm_stream.log`, `agent_events.log`, `result.json` | `file_events.log`, `session.log` | `result.json` |
| 4 | 强制提交 | `POST /api/agent/force-submit/:sid` | `llm_stream.log`, `agent_events.log`, `result.json` | `file_events.log`, `session.log` | `result.json` |
| 5 | 手动财务提交 | `POST /api/submit-financial/:sid` | `llm_stream.log`, `agent_events.log`, `result.json` | `file_events.log`, `session.log` | `result.json` |
**信号点说明**:
| 信号文件 | 读/写方 | 生命周期 | 作用 |
|---|---|---|---|
| `result.json` | 后端线程写入,SSE 端点读取 | 每轮开始时删除,`finally` 块中原子写入 | SSE 检测到该文件即发射 `done` 事件并断开连接 |
| `agent_events.log` | Agent 调度器追加写入,SSE 端点读取 | 每轮开始时删除,Agent 运行时持续追加 | 传递 agent 状态变化事件给前端 |
| `llm_stream.log` | LLM 回调追加写入,SSE 端点读取 | 每轮开始时删除,LLM 运行时持续追加 | 传递 LLM 流式输出给前端 |
| `file_events.log` | `pipeline_web` 追加写入,SSE 端点读取 | 会话内持续追加,不删除 | 传递文件处理进度给前端 |
| `session.log` | `sse_handler` 追加写入,SSE 端点读取 | 会话内持续追加,不删除 | 传递普通日志行给前端 |
### 1.4 关键约束(修改代码前必读)
**约束 1:`result.json` 必须在每轮线程启动时删除**
SSE 端点通过检测 `result.json` 是否存在来判断任务是否完成。如果上一轮的 `result.json` 残留,SSE 会立即读到旧数据并发射 `done` 事件,导致前端断开连接,新任务的消息无法送达。
- 实现位置:`_run_agent_task()` 的 `try` 块开头
- 删除时机:在 `install_log_collector()` 之后、`task_fn()` 执行之前
- 写入位置:`finally` 块中统一写入(唯一写入点)
- 写入规则:`finally` 始终执行原子写入,不再有条件判断
- `_emit_ready_and_submit` 只返回 result 字典,不写入文件
**约束 2:`result.json` 的写入必须使用 `finally` 块**
无论任务成功或失败,SSE 端点都需要 `result.json` 来发送 `done` 事件。如果仅在成功路径写入,异常时 SSE 会一直轮询直到 600 秒超时,前端无反馈。
**约束 3:SSE 新建连接时,当前文件偏移必须从 0 开始**
`_run_agent_task` 在启动时删除 `llm_stream.log` 和 `agent_events.log`,确保 SSE 重新建立连接后从 0 偏移开始读取。如果文件不被删除,旧的事件会被重复发送给前端。
**约束 4:前端 SSE 连接的生命周期**
- 前端在每次 POST 请求返回 `{status: "started"}` 后立即创建新的 SSE 连接
- 收到 `done` 事件后关闭连接
- 旧的连接引用必须清理(`agent.js` 中的 `agentEventSource`)
- 如果前端在 POST 之前就创建了 SSE 连接,会读到旧数据
---
## 二、场景一:用户提交材料 → LLM 分析完整 → 直接提交
### 2.1 时序图
```mermaid
sequenceDiagram
participant F as 前端
participant S as SSE连接
participant B as 后端线程
participant A as Agent调度器
F->>F: startProcess()
F->>B: POST /api/agent/process/:sid
B-->>F: {status: "started"}
F->>S: GET /api/logs/:sid
Note over B,A: 后台线程启动
B->>A: extract_invoices()
S-->>F: file_progress (processing/done)
Note over S: 轮询 file_events.log
S-->>F: llm_stream (start/chunk/end)
Note over S: 轮询 llm_stream.log
S-->>F: agent_state_change (state=extracting)
Note over S: 轮询 agent_events.log
Note over A: _do_extraction_with_validation()<br/>LLM提取 → validator校验<br/>最多3次重试
S-->>F: agent_state_change (校验通过/未通过)
Note over A: can_submit == true
A->>A: state → READY
A->>A: _emit_agent_event (agent_ready)
A->>A: _emit_ready_and_submit()
A->>A: run_financial_submit()
S-->>F: agent_ready
Note over B: 写入 result.json
S-->>F: done (携带 result)
Note over S: 检测到 result.json
F->>F: es.close()
F->>F: App.processState = 'done'
F->>F: addChatMessage(成功)
Note over B: remove_log_collector
```
### 2.2 事件流清单
| 序号 | 事件类型 | 来源文件 | 触发时机 | 前端处理 |
|---|---|---|---|---|
| 1 | `file_progress` | `file_events.log` | 每个文件处理开始/完成 | 更新文件状态 UI |
| 2 | `llm_stream` | `llm_stream.log` | LLM 流式输出 | 显示聊天气泡 |
| 3 | `agent_state_change` | `agent_events.log` | 状态变为 `extracting` | 显示瞬态状态提示 |
| 4 | `agent_state_change` | `agent_events.log` | 校验通过/未通过 | 更新瞬态状态 |
| 5 | `agent_ready` | `agent_events.log` | 双重校验通过 | 由 `done` 事件统一处理 |
| 6 | `done` | SSE 检测到 `result.json` | 流程结束 | 根据 `result` 判断终态 |
### 2.3 result.json 结构(成功路径)
```json
{
"ok": true,
"agent_ready": true,
"submit_ok": true,
"round": 1,
"message": "信息完整,已自动提交到财务系统"
}
```
---
## 三、场景二:用户提交材料 → 需补充 → 用户上传文件
### 3.1 时序图
```mermaid
sequenceDiagram
participant F as 前端
participant S as SSE连接
participant B as 后端线程
participant A as Agent调度器
Note over F,A: 阶段1: 初次分析
F->>B: POST /api/agent/process/:sid
B-->>F: {status: "started"}
F->>S: GET /api/logs/:sid
S-->>F: agent_state_change (state=extracting)
Note over A: can_submit == false
A->>A: state → AWAITING_SUPPLEMENT
S-->>F: agent_request_supplement
Note over B: 写入 result.json<br/>(waiting_for_supplement=true)
S-->>F: done
F->>F: es.close()
F->>F: App.processState = 'awaiting_supplement'
F->>F: showStatus('请补充')
F->>F: showAgentRequest()
Note over B: remove_log_collector
Note over F,A: 阶段2: 用户上传补充文件
F->>F: 用户点击"上传补充材料"
F->>F: 文件上传完成
F->>F: handleSupplementUpload(newFilenames)
F->>B: POST /api/agent/supplement/:sid<br/>{files: [...]}
B-->>F: {status: "started"}
F->>S: GET /api/logs/:sid
Note over B: 新后台线程启动
A->>A: add_supplement()<br/>(记录文件名, 发射收到事件)
S-->>F: agent_supplement_received
A->>A: extract_invoices()<br/>(重新提取所有文件)
A->>A: run_agent_round(new_files=[...])
Note over A: 加载上一轮结果作为<br/>previous_analysis
S-->>F: agent_state_change (state=extracting)
Note over A: LLM提取 → 校验循环
alt 分支A: 补充后仍不完整
S-->>F: agent_request_supplement
S-->>F: done (waiting=true)
F->>F: es.close()
F->>F: App.processState = 'awaiting_supplement'
else 分支B: 补充后完整
Note over A: can_submit == true
A->>A: state → READY
A->>A: _emit_ready_and_submit()
S-->>F: agent_ready
S-->>F: done (submit_ok=true)
F->>F: es.close()
F->>F: App.processState = 'done'
F->>F: addChatMessage(成功)
end
```
### 3.2 事件流清单(补充文件路径)
| 序号 | 事件类型 | 来源文件 | 触发时机 | 前端处理 |
|---|---|---|---|---|
| 1 | `agent_supplement_received` | `agent_events.log` | 收到补充文件列表 | 显示"已收到补充文件" |
| 2 | `agent_state_change` | `agent_events.log` | 开始重新分析 | 显示瞬态状态 |
| 3 | `agent_request_supplement` | `agent_events.log` | 仍不完整 | 更新补充请求面板 |
| 4 | `agent_ready` | `agent_events.log` | 校验通过 | 由 `done` 统一处理 |
| 5 | `done` | SSE 检测到 `result.json` | 流程结束 | 判断终态 |
### 3.3 result.json 结构(需补充)
```json
{
"ok": true,
"agent_ready": false,
"agent_state": "awaiting_supplement",
"round": 1,
"waiting_for_supplement": true
}
```
---
## 四、场景三:用户提交材料 → 需补充 → 用户通过对话提供信息
### 4.1 时序图
```mermaid
sequenceDiagram
participant F as 前端
participant S as SSE连接
participant B as 后端线程
participant A as Agent调度器
Note over F,A: 阶段1: 初次分析
F->>B: POST /api/agent/process/:sid
B-->>F: {status: "started"}
F->>S: GET /api/logs/:sid
S-->>F: agent_request_supplement
S-->>F: done (waiting=true)
F->>F: es.close()
F->>F: App.processState = 'awaiting_supplement'
Note over B: remove_log_collector
Note over F,A: 阶段2: 用户输入文字
F->>F: 用户在输入框输入文字
F->>F: handleUserSupplement()
F->>B: POST /api/agent/user-supplement/:sid<br/>{text: "..."}
B-->>F: {status: "started"}
F->>S: GET /api/logs/:sid
Note over B: 新后台线程启动
S-->>F: agent_supplement_received
A->>A: process_user_text_supplement()
A->>A: process_user_supplement()<br/>(LLM解析用户文字)
S-->>F: llm_stream (解析过程)
A->>A: merge_supplement_into_info()<br/>(合并到 extracted_info)
Note over A: 保存到缓存文件
A->>A: run_agent_round()<br/>(重新校验)
S-->>F: agent_state_change<br/>(state=extracting, 正在重新校验)
alt 分支A: 补充后仍不完整
S-->>F: agent_request_supplement
S-->>F: done (waiting=true)
F->>F: es.close()
F->>F: App.processState = 'awaiting_supplement'
else 分支B: 补充后完整
Note over A: can_submit == true
A->>A: state → READY
A->>A: _emit_ready_and_submit()
S-->>F: agent_ready
S-->>F: done (submit_ok=true)
F->>F: es.close()
F->>F: App.processState = 'done'
F->>F: addChatMessage(成功)
end
```
### 4.2 事件流清单(文字补充路径)
| 序号 | 事件类型 | 来源文件 | 触发时机 | 前端处理 |
|---|---|---|---|---|
| 1 | `agent_supplement_received` | `agent_events.log` | 收到用户文字 | 显示"已收到补充" |
| 2 | `llm_stream` | `llm_stream.log` | LLM 解析用户文字 | 显示解析过程 |
| 3 | `agent_state_change` | `agent_events.log` | 开始重新校验 | 显示"正在重新校验" |
| 4 | `agent_request_supplement` | `agent_events.log` | 仍不完整 | 更新补充请求 |
| 5 | `agent_ready` | `agent_events.log` | 校验通过 | 由 `done` 统一处理 |
| 6 | `done` | SSE 检测到 `result.json` | 流程结束 | 判断终态 |
---
## 五、强制提交流程
### 5.1 时序图
```mermaid
sequenceDiagram
participant F as 前端
participant S as SSE连接
participant B as 后端线程
F->>F: handleForceSubmit()
F->>F: App.forceSubmitting = true
F->>B: POST /api/agent/force-submit/:sid
B-->>F: {status: "started"}
F->>S: GET /api/logs/:sid
Note over B: 后台线程启动
B->>B: force_submit()<br/>(state → READY)
S-->>F: agent_force_submit
B->>B: run_financial_submit()
Note over B: 写入 result.json
S-->>F: done
F->>F: es.close()
F->>F: App.forceSubmitting = false
F->>F: App.processState = 'done'
```
---
## 六、错误处理路径
### 6.1 错误场景和事件
| 错误场景 | 后端行为 | 发射事件 | 前端表现 |
|---|---|---|---|
| LLM 提取异常 | `state → ERROR` | `agent_error` | 聊天显示错误,`processState → 'done'` |
| 规则校验 3 次失败 | 返回最后一次结果,继续语义判断 | `agent_state_change` | 依赖 `can_submit` 字段决定 |
| 轮次超限 (5 轮) | `state → ERROR` | `agent_max_rounds` | 聊天显示错误,可强制提交 |
| 财务提交失败 | `result.submit_ok = false` | 无独立事件 | `done` 事件携带错误信息 |
| SSE 连接中断 | 无 | `es.onerror` 触发 | 显示"连接中断" |
| 超时 (600s) | SSE 轮询循环退出 | 连接自然断开 | 连接断开 |
### 6.2 agent_error 事件结构
```json
{
"type": "agent_error",
"message": "LLM 提取失败: ..."
}
```
### 6.3 agent_max_rounds 事件结构
```json
{
"type": "agent_max_rounds",
"message": "已达到最大轮次 (5),请检查信息或强制提交"
}
```
---
## 七、状态机完整图
### 7.1 后端 AgentState 状态机
```mermaid
stateDiagram-v2
[*] --> IDLE
IDLE --> EXTRACTING: POST /api/agent/process\nPOST /api/agent/supplement\nPOST /api/agent/user-supplement
EXTRACTING --> READY: can_submit == true
EXTRACTING --> AWAITING_SUPPLEMENT: can_submit == false
EXTRACTING --> ERROR: 异常 / 轮次超限
READY --> SUBMITTING: _emit_ready_and_submit()
SUBMITTING --> DONE: 财务提交完成
AWAITING_SUPPLEMENT --> EXTRACTING: 用户补充文件/文字
READY: 准备提交\n(终态保护)
SUBMITTING: 财务提交中\n(终态保护)
DONE: 终态\n(终态保护)
ERROR: 错误状态\n(可强制提交)
note right of EXTRACTING
LLM 提取 + validator 校验\n最多 3 次重试
end note
```
### 7.2 前端 processState 状态机
```mermaid
stateDiagram-v2
[*] --> idle
idle --> processing: startProcess()
processing --> awaiting_supplement: done事件\nresult.waiting_for_supplement
processing --> done: done事件\nresult.ok
awaiting_supplement --> processing: 补充文件或文字
awaiting_supplement --> submitting: 强制提交
submitting --> done: done事件
done: 流程结束
idle: 初始状态
processing: 处理中
awaiting_supplement: 等待补充
submitting: 提交中
```
---
## 八、SSE 事件类型完整参考
### 8.1 Agent 事件 (agent_events.log)
| 事件类型 | 数据结构 | 触发条件 |
|---|---|---|
| `agent_state_change` | `{type, state, round, attempt, message}` | 状态切换 |
| `agent_ready` | `{type, round, message}` | 双重校验通过 |
| `agent_request_supplement` | `{type, round, missing_fields, missing_materials, semantic_issues, suggestion}` | 校验未通过 |
| `agent_supplement_received` | `{type, files}` | 收到用户补充 |
| `agent_force_submit` | `{type, message}` | 用户强制提交 |
| `agent_error` | `{type, message}` | 提取失败 |
| `agent_max_rounds` | `{type, message}` | 达到最大轮次 |
### 8.2 文件进度事件 (file_events.log)
| 事件类型 | 数据结构 | 触发条件 |
|---|---|---|
| `file_progress` | `{type, file, status, summary?, error?}` | 文件处理状态变更 |
`status` 取值: `processing` / `done` / `cached` / `error`
### 8.3 LLM 流式事件 (llm_stream.log)
| 事件类型 | 数据结构 | 触发条件 |
|---|---|---|
| `llm_stream` | `{type, phase, text?}` | LLM 输出流 |
`phase` 取值: `start` / `reasoning` / `chunk` / `end` / `error`
### 8.4 完成事件 (SSE 直接发送)
| 事件类型 | 数据结构 | 触发条件 |
|---|---|---|
| `done` | `{type, result: {...}}` | `result.json` 出现 |
---
## 九、常见问题排查清单
### 9.1 SSE 事件丢失
**症状**: 前端没有收到预期的 agent 事件
**排查步骤**:
1. 检查 `agent_events.log` 是否存在、是否有内容
2. 检查 SSE 连接是否建立成功(浏览器 Network 面板)
3. 确认 `sse_handler.install_log_collector()` 是否被调用
4. 确认 `remove_log_collector()` 是否过早调用
### 9.2 提交流程中断
**症状**: 流程在某个中间状态卡住,没有 `done` 事件
**排查步骤**:
1. 检查 `result.json` 是否被写入
2. 检查后台线程是否异常退出(查看 `session.log`)
3. 确认 `finally` 块中的 `result.json` 写入逻辑是否执行
4. 检查是否触发了 600 秒超时
### 9.3 状态不一致
**症状**: 前端 `processState` 和后端 `AgentState` 不匹配
**排查步骤**:
1. 对比 `agent_events.log` 中的状态变更序列
2. 检查前端是否正确处理了 `done` 事件
3. 确认 SSE 连接是否在适当时机关闭和重建
4. 检查 `App.agentEventSource` 引用是否正确清理
### 9.4 补充流程不触发
**症状**: 用户上传补充文件或输入文字后,没有重新分析
**排查步骤**:
1. 确认 `processState` 是否为 `awaiting_supplement`
2. 检查补充 API 是否返回 `{status: "started"}`
3. 检查新 SSE 连接是否成功建立
4. 确认 `add_supplement()` 或 `process_user_text_supplement()` 是否被调用
### 9.5 补充材料后前端无任何消息(`result.json` 残留问题)
**症状**: 第二轮及之后的补充材料提交后,前端完全没有任何消息显示,状态栏不更新,聊天区无新增消息。后台日志显示处理正常完成。
**根因**: `_run_agent_task` 在每轮启动时未清理上一轮的 `result.json`。SSE 端点轮询时立即检测到旧的 `result.json`,直接发射 `done` 事件并关闭连接,前端断开后无法接收新任务的消息。
**排查步骤**:
1. 检查 session 目录中 `result.json` 的修改时间 — 如果早于当前轮次开始时间,说明是残留文件
2. 检查浏览器 Network 面板中 SSE 连接 — 是否在建立后立即收到 `done` 事件
3. 确认 `_run_agent_task` 是否在启动时清理了 `result.json`
**修复**: 在 `_run_agent_task` 的 `try` 块开头同时清理 `llm_stream.log`、`agent_events.log` 和 `result.json` 三个文件。
**详细记录**: 参见 `.agents/docs/error-experience/2026-06-15-补充材料SSE立即读到旧result.json导致前端无消息.md`
---
## 十、关键文件索引
| 文件 | 职责 |
|---|---|
| `src/web/static/js/process.js` | 主提交流程入口,SSE 事件分发 |
| `src/web/static/js/agent.js` | Agent 事件处理,补充/强制提交逻辑 |
| `src/web/static/js/state.js` | 全局状态管理 |
| `src/web/routes.py` | 后端路由,后台线程启动 |
| `src/agent/orchestrator.py` | Agent 调度器,状态机,校验循环 |
| `src/web/sse_handler.py` | SSE 日志收集器 |
| `src/web/pipeline_web.py` | 发票提取管道,财务提交 |
---
## 十一、文件生命周期与操作信号点
### 11.1 单轮处理的完整文件生命周期
```mermaid
sequenceDiagram
participant API as 路由层
participant RT as _run_agent_task
participant TF as task_fn
participant SS as _emit_ready_and_submit
participant SSE as SSE 端点
Note over API: 1. 创建 handler
API->>API: install_log_collector(session_dir)
Note over API: 创建 SSE 日志收集器<br/>随即开始写入 session.log
API->>RT: threading.Thread(target=_run_agent_task)
Note over RT: 2. 清理残留文件
RT->>RT: unlink(llm_stream.log)
RT->>RT: unlink(agent_events.log)
RT->>RT: unlink(result.json)
Note over RT: 3. 执行任务
RT->>TF: task_fn(session_dir, config)
Note over TF: 执行期间各个文件由对应模块写入:
TF-->>TF: file_events.log (pipeline_web)
TF-->>TF: llm_stream.log (LLM 回调)
TF-->>TF: agent_events.log (Agent 调度器)
TF-->>RT: 返回 (agent_session, 占位 result)
alt 成功路径 (READY)
RT->>SS: _emit_ready_and_submit()
Note over SS: 发射 agent_ready 事件<br/>执行财务提交<br/>返回 result 字典
SS-->>RT: result 字典
Note over RT: result = {...}
else 需补充路径 (AWAITING_SUPPLEMENT)
Note over RT: result = {waiting_for_supplement: true}
else 异常路径
Note over RT: result = {ok: false, error: ...}
end
Note over RT: 4. finally 块 — 唯一写入点
RT->>RT: 原子写入 result.json (.tmp → replace)
RT->>RT: remove_log_collector(handler)
Note over SSE: 5. SSE 端点检测
SSE->>SSE: 轮询检测到 result.json
SSE-->>SSE: 发射 done 事件
SSE->>SSE: break 退出轮询
```
### 11.2 各阶段信号文件状态
| 阶段 | `result.json` | `llm_stream.log` | `agent_events.log` | `file_events.log` | `session.log` |
|---|---|---|---|---|---|
| 会话创建 | 不存在 | 不存在 | 不存在 | 不存在 | 不存在 |
| `install_log_collector` 后 | 不存在 | 不存在 | 不存在 | 不存在 | 开始写入 |
| `_run_agent_task` 清理后 | 已删除 | 已删除 | 已删除 | 保持 | 保持 |
| 文件提取中 | 不存在 | 不存在 | 不存在 | 持续追加 | 持续追加 |
| LLM 提取中 | 不存在 | 持续追加 | 持续追加 | 保持 | 持续追加 |
| 校验中 | 不存在 | 保持 | 持续追加 | 保持 | 持续追加 |
| 任务完成 (READY) | 已写入 | 保持 | 保持 | 保持 | 保持 |
| 任务完成 (需补充) | 已写入 | 保持 | 保持 | 保持 | 保持 |
| 任务异常 | 已写入 | 保持 | 保持 | 保持 | 保持 |
| SSE done 事件后 | 保持 | 保持 | 保持 | 保持 | 保持 |
### 11.3 新增信号文件检查清单
当需要在系统中新增一个信号文件(如 `submit_progress.log`)时,必须检查以下事项:
1. **写入方**:哪个模块负责写入?写入时机是什么?
2. **读取方**:SSE 端点是否需要轮询?前端是否需要处理?
3. **清理时机**:是否需要在 `_run_agent_task` 中清理?如果不需要,为什么?
4. **原子性**:写入是否需要 `.tmp` + `replace` 模式?
5. **轮询偏移**:SSE 端点是否需要跟踪该文件的读取偏移?
6. **更新本文档**:在 1.3 操作信号点清单中新增一行,在 11.2 文件状态表中新增一列
7. **更新 `_run_agent_task`**:如果需要清理,在清理循环中添加文件名
8. **更新前端**:在 `sse.js` 或 `agent.js` 中添加对应的事件处理器
+10 -1
View File
@@ -1,3 +1,12 @@
> **已过时(2026-10-19 MCP 化改造)**:本文描述的是改造前的架构,保留作为历史与决策背景。
> 现状请看 `docs/MCP.md`(工具契约)、`AGENTS.md`(项目操作指南)与
> `.agents/docs/plans/MCP化改造方案-2026-10-19.md`(决策与实测记录)。
> 具体差异:`src/agent/`(状态机与 SSE)、`src/web/`(Flask 界面)、CLI 入口、内置 LLM 抽取
> 与 `llama-index` 依赖均已删除;Word 出库单功能下线;提示词迁移为 `.agents/skills/reimbursement/references/`。
---
# 项目架构全景图
> 最后更新: 2026-06-15
@@ -398,4 +407,4 @@ graph TD
| `src/web/sse_handler.py` | SSE 日志收集器 |
| `src/web/static/js/process.js` | 前端主提交流程 |
| `src/web/static/js/agent.js` | 前端 Agent 交互处理 |
| `config.json` | 项目配置 |
| `config.json` | 项目配置 |
@@ -251,6 +251,10 @@ P0 是唯一可能推翻已定决策的阶段,所以它只做验证、不做
验证脚手架留在 `cases/_p0-gate/` 与 `cases/20261019-P0闸门自测/`(不入库),P1 收尾时删除。
### 10.5 日期更正
本文初版误按 `AGENTS.md` 的 `last_reviewed: 2026-07-02` 推断为 2026-07-23 并以此命名,实际系统日期 2026-10-19,文件已改名。
### 10.6 第二批工具(2026-10-19)
新增 `mcp__arb__match_payments` / `save_info` / `validate_info` / `rules_get`(共 14 个工具),并新增 `src/core/matching/runner.py`(从缓存匹配,无 LLM);`match_result.json` 的写入从 extractor 私有函数上移为 `cache.save_match_result`,消除两份实现。`tests/test_mcp_analysis.py` 补 8 个用例(一对多凑额、违反业务约束、can_submit 剥离、draft 不落盘、规则形状、非法 info_type)。
@@ -264,10 +268,6 @@ P0 是唯一可能推翻已定决策的阶段,所以它只做验证、不做
遗留观察(进 P1):`validation_rules.json` 里 `remark` 之类的可选字段写成 `required: true, check_empty: false`,含义变成"键必须存在",模型输出省略空备注时会被判缺失。应把这类字段改 `required: false`,否则每次都要为备注来回补数据。
### 10.5 日期更正
本文初版误按 `AGENTS.md` 的 `last_reviewed: 2026-07-02` 推断为 2026-07-23 并以此命名,实际系统日期 2026-10-19,文件已改名。
### 10.7 重连实验与进程模型(2026-10-19)
按约定做了 kill 验证:`taskkill /PID <旧服务进程> /F` 之后,DSH 的 mcp-client 监督器**自动重新拉起子进程并重同步工具列表**——工具数从 10 变成 15,新增的 `info_skeleton` / `match_payments` / `save_info` / `validate_info` / `rules_get` 当场可调用,且全部通过真实 MCP 线路验证(`info_skeleton` 出的骨架填值后 `save_info` 校验 valid=true、置信度 1.0)。**不需要重启 dsh 就能上新工具**,只要让旧子进程退出。
@@ -279,3 +279,84 @@ P0 是唯一可能推翻已定决策的阶段,所以它只做验证、不做
3. 同理,**任何端口/单例资源都不能靠「进程只有一个」假设**(收件器已是独立进程,不受影响)。
小遗留:`case_status` 在 travel_info 与 normal_info 同时存在时只报 `info: travel_info`。P1 改成显式提示冲突(混合发票只能走普通报销),避免误按纯差旅提交。
## 11. P1 落地记录(2026-10-19 晚)
### 11.1 提交与体量
| 提交 | 内容 |
|------|------|
| `cffa4cc` | MCP 服务器 + 上传收件器 + `cases/` 会话模型 + 内核拆分(+1975/-1272 行的前半) |
| `0c54dc8` | 删除 `src/agent/`(878 行)与 Web 的 5 个 Agent 端点(824→491 行、18→13 端点) |
| `5f57400` | 2a:`com_thread.py` 单线程 actor、`doc_requisition`、`case_patch_extraction`(工具 15→18) |
| `53b1abe` | 2b:删除 `src/web/`、CLI、内置 LLM、出库单功能与 `llama-index*`+`python-dotenv`(工具 18→17) |
改造后:`src/mcp` 808 行 / 9 文件,`src/core` 1481 / 9,`src/infra` 1456 / 11,`src/upload` 343 / 3,`tests` 1333 / 6。
整个 `src` 从约 6900 行降到约 4100 行;依赖 6 个(mcp、segno、flask、playwright、PyMuPDF、pywin32)。
### 11.2 验证
- `scripts/mcp_smoke.py`(新增):真起 stdio 子进程握手 → **17 个工具、instructions 1449 字节、`case_list` 正常**
- 申购单实机:合成 2 张普通发票 + 1 笔 464 元支付 → 匹配(一对多、`review_required` 如期提示)→
`exclude=['万用表']` → 5.0s 生成 19456 字节 .xls;依赖清理后复测 2.4s,产物单元格读回正确
(`xlrd` 随 llama-index 一起消失,但申购单走 Excel COM,不依赖它——这是复测的直接动因)
- `case_patch_extraction` 语义验证:回显 `from`/`to`、未知键告警、与现值相同不写
- 门禁:`ruff check` + `format --check`(44 文件)+ `mypy src`(36 文件)+ `deptry` + `pytest 107 passed`
### 11.3 事故:pre-commit 的 stash 窗口把工作区回滚了
`git commit` 触发 pre-commit,它会 **stash 未暂存改动** 跑完再还原;我的执行程序在 120s 上限被杀,
还原步骤没跑完,`AGENTS.md`、`src/web/routes.py`、`requisition.py`、`tests/test_requisition.py` 四个文件
被回滚成 HEAD 版本。`git fsck --unreachable --dangling` 显示 **0 个游离对象**,说明 pre-commit 没有留下可捞的
stash,只能按锚点重放。重放时我又因「往上找分隔注释」找过头,把 `_validate_session` 与 `_build_web_config`
一起删了——靠**审计删除行清单**(`git diff` 里出现 `-def _validate_session`)才发现,从 HEAD 取回并跑 Web 冒烟确认。
顺带清掉两个我造成的污染:`git add -A --intent-to-add` 误把 `.mnemon/`(DSH 本地记忆库)放进暂存区;
钩子在我给的清空 `HOME` 环境里把缓存写成了仓库内的 `~/` 目录(里面是它 clone 的 ruff-pre-commit 仓库和 venv)。
两者都已处理,`.mnemon/` 进 `.gitignore`。
结论已写进 AGENTS.md「提交纪律」:门禁退出码单独确认;提交前工作区 `git add` 干净,否则用 `--no-verify`
并手动跑等价检查。本次 P1 之后的提交都用了 `--no-verify`,等价的 ruff/format/mypy/deptry/pytest 全部手跑。
另一个自己制造的疏漏:2b 第一次提交时 `mcp_smoke.py` 还带 2 个可自动修复的 lint 错误就进了 commit,
已修复并 `git commit --amend`(现 HEAD `53b1abe`),HEAD 是干净的。
### 11.4 出库单功能下线(用户决定)
做 2a 时 `doc_consumable` 无法实测,追出一个真实缺陷:模板 `易耗品、出库单.doc` 在提交 `63b97c6`
(2026-08-31,同一提交新增了 Excel 申购单)被删除,此后 Word 路径必然 `FileNotFoundError`;
而 Web 的 `_try_fill_consumable_doc` 把异常吞成一条日志——**普通发票提交有 7 周在静默跳过出库单生成**。
差旅发票本来就 skip,所以一直没暴露。
从历史导出模板做了 PII 扫描:内容是空白校用表单(「阜阳师范大学科研类低值易耗品入、出库单」),
无姓名、无卡号、无任何 9 位以上数字串。用户选择直接删除该功能,于是删掉 `consumable.py`、
`doc_consumable` 工具、`consumable_storage` 配置项,文档里的出库单说法全部改成申购单。
教训(适用于本项目所有产物代码):**失败必须冒出来**。返回 `doc_ok=false` 而不报错,等于把 7 周的
产物缺失藏进日志。MCP 工具统一用 `ok:false + reason + next_action` 就是这个原因。
### 11.5 遗留债务(按优先级)
1. **P3 浏览器分步工具**:`BrowserActor` 单线程 + `.browser.lock`(PID+心跳,防多代进程抢)+
`browser_open/login/new_form/fill(step)/page_state/close`,截图进 `cases/<id>/screenshots/`,扫码登录返回 `need_human`
2. **P4 提交护栏**:`submit_preview`(快照+摘要+token)与 `submit_execute`(一次性、摘要变更后失效),默认有头
3. **申购单模板选择**:现在是「项目根目录 `低值易耗品申购*.xls` 里名字最大者」,应该改成固定模板路径
(`templates/` 或配置项),避免将来根目录出现产物就选错
4. **文本层行重排**:用 `get_text("words")` 的坐标把标签和值配对回去,并在与读图冲突时输出 warning,
而不是让宿主自己猜哪一路对(§10.3 的实测证据)
5. `case_delete` / `case_rename`:宿主需要清理 scratch 会话;反斜杠路径建议在服务端归一化而不是只写文档
6. `case_status` 应标出「同时存在 travel_info 与 normal_info」的会话(当前两个都存着不报错)
7. `cases/` 下的 scratch 会话(`_p0-gate`、`20261019-P0闸门自测` 等)是 gitignore 的本地产物,可随手删
### 11.6 文档同步(P1 附带完成)
- 新增 `docs/MCP.md`(工具参考由运行时 schema 生成,与代码同源);删除 `docs/API.md`(Flask/SSE 文档)
- 重写根 `README.md`(为什么改成 MCP、快速开始、注册配置、流程图、结构与风险)与 `AGENTS.md`
(含「提交纪律」与多进程共存、COM/Playwright 线程约束)
- 各包 `README.md` 全部按现状重写;新增 `src/mcp/README.md`、`src/upload/README.md`
- 提示词 `src/infra/llm/prompts/*.md` → `.agents/skills/reimbursement/references/*.md`,
删除其中 `can_submit`/`suggestion`(`save_info` 也会主动剥离),「只输出 JSON」改成「交给对应工具落盘」;
新增 `.agents/skills/reimbursement/SKILL.md`(宿主侧操作纪律,已被 DSH 热加载为 `reimbursement` 技能)
- 删除作废的 `.agents/docs/guides/系统事件流全景图.md`(事件流通道已整体移除),给
`项目架构全景图.md` 与 `架构分析-2026-06-15.md` 加「已过时」横幅保留历史
- `config.example.json` 去掉 `consumable_storage` 加 `upload_pin`;`.env.example` 去掉 LLM 段并说明
**已不再自动加载 .env**(python-dotenv 移除),凭据只走 `scripts/data/config.json`
+10 -1
View File
@@ -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*