# 系统事件流全景图 > 最后更新: 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` 中添加对应的事件处理器