23 KiB
系统事件流全景图
最后更新: 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 通信机制
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 时序图
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 结构(成功路径)
{
"ok": true,
"agent_ready": true,
"submit_ok": true,
"round": 1,
"message": "信息完整,已自动提交到财务系统"
}
三、场景二:用户提交材料 → 需补充 → 用户上传文件
3.1 时序图
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 结构(需补充)
{
"ok": true,
"agent_ready": false,
"agent_state": "awaiting_supplement",
"round": 1,
"waiting_for_supplement": true
}
四、场景三:用户提交材料 → 需补充 → 用户通过对话提供信息
4.1 时序图
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 时序图
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 事件结构
{
"type": "agent_error",
"message": "LLM 提取失败: ..."
}
6.3 agent_max_rounds 事件结构
{
"type": "agent_max_rounds",
"message": "已达到最大轮次 (5),请检查信息或强制提交"
}
七、状态机完整图
7.1 后端 AgentState 状态机
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 状态机
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 事件
排查步骤:
- 检查
agent_events.log是否存在、是否有内容 - 检查 SSE 连接是否建立成功(浏览器 Network 面板)
- 确认
sse_handler.install_log_collector()是否被调用 - 确认
remove_log_collector()是否过早调用
9.2 提交流程中断
症状: 流程在某个中间状态卡住,没有 done 事件
排查步骤:
- 检查
result.json是否被写入 - 检查后台线程是否异常退出(查看
session.log) - 确认
finally块中的result.json写入逻辑是否执行 - 检查是否触发了 600 秒超时
9.3 状态不一致
症状: 前端 processState 和后端 AgentState 不匹配
排查步骤:
- 对比
agent_events.log中的状态变更序列 - 检查前端是否正确处理了
done事件 - 确认 SSE 连接是否在适当时机关闭和重建
- 检查
App.agentEventSource引用是否正确清理
9.4 补充流程不触发
症状: 用户上传补充文件或输入文字后,没有重新分析
排查步骤:
- 确认
processState是否为awaiting_supplement - 检查补充 API 是否返回
{status: "started"} - 检查新 SSE 连接是否成功建立
- 确认
add_supplement()或process_user_text_supplement()是否被调用
9.5 补充材料后前端无任何消息(result.json 残留问题)
症状: 第二轮及之后的补充材料提交后,前端完全没有任何消息显示,状态栏不更新,聊天区无新增消息。后台日志显示处理正常完成。
根因: _run_agent_task 在每轮启动时未清理上一轮的 result.json。SSE 端点轮询时立即检测到旧的 result.json,直接发射 done 事件并关闭连接,前端断开后无法接收新任务的消息。
排查步骤:
- 检查 session 目录中
result.json的修改时间 — 如果早于当前轮次开始时间,说明是残留文件 - 检查浏览器 Network 面板中 SSE 连接 — 是否在建立后立即收到
done事件 - 确认
_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 单轮处理的完整文件生命周期
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)时,必须检查以下事项:
- 写入方:哪个模块负责写入?写入时机是什么?
- 读取方:SSE 端点是否需要轮询?前端是否需要处理?
- 清理时机:是否需要在
_run_agent_task中清理?如果不需要,为什么? - 原子性:写入是否需要
.tmp+replace模式? - 轮询偏移:SSE 端点是否需要跟踪该文件的读取偏移?
- 更新本文档:在 1.3 操作信号点清单中新增一行,在 11.2 文件状态表中新增一列
- 更新
_run_agent_task:如果需要清理,在清理循环中添加文件名 - 更新前端:在
sse.js或agent.js中添加对应的事件处理器