实现Agent对话,合格自动提交,不合格补充材料的能力

This commit is contained in:
wandering
2026-06-14 12:56:33 +08:00
parent ef9e1d771b
commit 01d4686ceb
68 changed files with 8914 additions and 1908 deletions
+243 -8
View File
@@ -1,5 +1,5 @@
---
last_reviewed: 2026-06-11
last_reviewed: 2026-06-13
---
# src/web 模块设计说明
@@ -11,20 +11,47 @@ last_reviewed: 2026-06-11
- **会话隔离**:每次上传生成独立 `session_id`,文件、日志、配置、结果各自隔离在 `uploads/<session_id>/` 目录下,避免并发冲突。
- **异步处理**:耗时的 PDF 提取、LLM 调用在后台线程执行,前端通过 SSE 实时查看日志流,不阻塞 HTTP 连接。
- **前后端分离最小化**:前端使用原生 JS + Bootstrap 5,不引入构建工具,保持单页应用轻量可维护。
- **统一文件上传**:2026-06-12 改造,将 PDF 和图片上传入口合并为单一上传区,用户通过一个入口上传所有文件类型。
## 变更历史
| 日期 | 变更 |
|------|------|
| 2026-06-13 | LLM 流式思考过程展示:后端 SSE 推送 `llm_stream` 事件(start/chunk/end/error),前端聊天气泡实时展示 AI 思考过程 |
| 2026-06-12 | 文件进度实时反馈:后端 SSE 推送 `file_progress` 事件(processing/done/cached/error),前端文件消息实时更新状态 + 展示提取摘要 |
| 2026-06-12 | 配置交互改为逐项引导:config.json 缺失字段时 AI 逐个提示用户通过聊天输入,全部完成后自动开始处理 |
| 2026-06-12 | 聊天窗口精简:移除 SSE 日志流显示,仅保留关键状态消息;config.json 配置不完整时 AI 主动提示缺失字段 |
| 2026-06-12 | 移除开始处理按钮,改为自动触发:config.json 解析完成且配置完整(username/password)且有发票文件时自动开始处理 |
| 2026-06-12 | 文件上传通知改为逐条消息:每个文件单独一条聊天气泡,上传框 flex 居中、固定高度 |
| 2026-06-12 | 文件上传反馈移至聊天窗口:上传/拖拽/同步文件后以聊天气泡通知,上传框固定高度不再显示文件标签 |
| 2026-06-12 | 修复聊天窗口消息覆盖问题:将消息区域与输入区域分离,消息区独立滚动,新增用户文本输入功能 |
| 2026-06-12 | 合并 PDF/图片上传入口,`/api/files` 返回统一文件列表,前端使用 `allFiles` 单一数组管理 |
| 2026-06-12 | 移除配置表单,config.json 通过统一上传入口自动解析,配置存入 `sessionConfig` 对象,前端不再展示配置输入框 |
| 2026-06-12 | 暗色日志窗口替换为 AI 聊天风格窗口,SSE 日志以聊天气泡形式展示,支持打字指示器动画 |
## 文件结构
```
src/web/
├── app.py # Flask 应用入口,路由、管道编排、日志收集
├── sse_handler.py # SSE 日志收集器、日志转义工具
├── routes.py # 路由定义、SSE 端点
├── templates/
│ ├── index.html # PC 端主界面(上传、配置、处理、编辑、提交)
│ ├── index.html # PC 端主界面(上传、处理、编辑、提交)
│ └── mobile_upload.html # 移动端上传页面(拍照/相册选择)
└── static/
├── css/
│ └── index.css # 全局样式(上传区、日志面板、可编辑表格)
└── js/
└── index.js # 前端逻辑(上传、SSE 日志、表格编辑、二维码同步)
├── index.js # 入口:初始化 App 全局状态、绑定事件
├── state.js # 全局状态管理(App 对象)
├── chat.js # 聊天气泡渲染、文件消息、LLM 流式气泡
├── agent.js # Agent 事件处理、用户输入管理
├── process.js # SSE 连接、事件路由、管道启动
├── config.js # 配置解析、逐项引导
├── upload.js # 文件上传、拖拽处理
├── sync.js # 移动端同步
└── utils.js # HTML 转义等工具函数
```
## 数据流
@@ -64,8 +91,8 @@ graph TD
| GET | `/` | 主界面 |
| POST | `/api/session` | 创建会话,返回 session_id |
| POST | `/api/upload/<sid>` | 上传 PDF/图片 |
| GET | `/api/files/<sid>` | 列出会话文件 |
| POST | `/api/process/<sid>` | 启动管道(后台线程) |
| GET | `/api/files/<sid>` | 列出会话文件(返回统一 `files` 列表,含 `name`、`type`、`size` 字段;旧字段 `pdfs`/`images` 保留向后兼容) |
| POST | `/api/agent/process/<sid>` | 启动 Agent 管道(后台线程) |
| GET | `/api/logs/<sid>` | SSE 日志流 |
| GET | `/api/data/<sid>` | 获取发票数据 JSON |
| POST | `/api/save/<sid>` | 保存前端编辑的发票数据 |
@@ -77,7 +104,7 @@ graph TD
### 日志收集
`_SSELogHandler` 将管道日志写入 `session.log`,SSE 端点通过文件偏移量增量读取,实现前端实时日志展示。日志收集器在管道启动时安装,完成后移除,确保线程安全。
`SSELogHandler` 将管道日志写入 `session.log`,SSE 端点通过文件偏移量增量读取,实现前端实时日志展示。日志收集器在管道启动时安装,完成后移除,确保线程安全。
### 发票类型分流
@@ -97,8 +124,216 @@ Bot 填报时优先使用 LLM 提取的信息(`travel_info`/`normal_info`)
### 移动端同步
PC 端生成二维码指向 `/mobile/<sid>`,手机端上传的图片通过 `syncFiles()` 轮询同步到 PC 端内存中的 `imgFiles` 列表,实现跨设备协作。文件来源标记(`__source`)区分本地选择和服务器同步,避免重复。
PC 端生成二维码指向 `/mobile/<sid>`,手机端上传的文件通过 `syncFiles()` 轮询同步到 PC 端内存中的 `allFiles` 列表,实现跨设备协作。文件来源标记(`__source`)区分本地选择和服务器同步,避免重复。
### 配置管理
配置分两层:项目级 `config.json` 提供默认值,会话级 `uploads/<sid>/config.json` 存储当次会话覆盖值。前端支持通过上传 `config.json` 快速填充配置表单。
配置分两层:项目级 `config.json` 提供默认值,会话级 `uploads/<sid>/config.json` 存储当次会话覆盖值。前端通过统一上传入口接收 `config.json`,自动解析到 `sessionConfig` 对象,不再展示配置输入表单。
### 聊天气泡消息系统
聊天窗口的所有消息通过 **事件文件 + SSE 轮询** 机制传输。后端不直接推送消息,而是将事件追加到 session 目录下的日志文件,SSE 端点以 0.5 秒间隔轮询文件增量,再通过 EventSource 推送到前端。
#### 事件文件总览
session 目录下有四个事件文件:
| 文件 | 用途 | 写入方 | 读取方 |
|------|------|--------|--------|
| `llm_stream.log` | LLM 流式输出(思考过程 + 正式回答) | `llm_extractor` 模块 | SSE 端点 |
| `file_events.log` | 文件处理进度 | `extractor` 模块 | SSE 端点 |
| `agent_events.log` | Agent 状态变更、请求补充等 | `orchestrator` 模块 | SSE 端点 |
| `session.log` | 普通 INFO 日志 | `SSELogHandler` | SSE 端点(当前仅保留,前端已不做处理) |
#### 后端发送事件
所有事件文件遵循相同的写入协议:每行一个 JSON 对象,写入后 flush。
**`llm_stream.log`** — 由 `_emit_llm_stream(source_dir, phase, ...)` 写入:
```python
# 开始 LLM 调用(必需)
_emit_llm_stream(source_dir, "start", label="正在分析文件...")
# 流式文本片段(可选,有内容时发)
_emit_llm_stream(source_dir, "chunk", text="让我来分析...")
# 思考过程片段(可选,模型支持时发)
_emit_llm_stream(source_dir, "reasoning", text="根据发票信息...")
# 调用完成(必需)
_emit_llm_stream(source_dir, "end", label="分析完成")
# 调用失败(异常时发)
_emit_llm_stream(source_dir, "error", error="连接超时")
```
**`agent_events.log`** — 由 `_emit_agent_event(session_dir, event_type, ...)` 写入:
```python
# 状态变更
_emit_agent_event(session_dir, "agent_state_change", state="extracting", message="正在分析文件...")
# 请求补充材料
_emit_agent_event(session_dir, "agent_request_supplement", message="请上传返程车票...")
# 信息完整,可以提交
_emit_agent_event(session_dir, "agent_ready")
# 错误
_emit_agent_event(session_dir, "agent_error", message="LLM 提取失败")
```
**`file_events.log`** — 由 `_emit_file_event(source_dir, ...)` 写入:
```python
# 文件开始处理
_emit_file_event(source_dir, filename, "processing")
# 文件处理完成(带摘要)
_emit_file_event(source_dir, filename, "done", summary={"invoice_number": "...", ...})
# 使用缓存
_emit_file_event(source_dir, filename, "cached")
# 处理失败
_emit_file_event(source_dir, filename, "error", error="PDF 解析失败")
```
**重要约束:`start` 和 `end` 事件不可省略。** 前端 `llmStreamState` 状态机依赖 `start` 创建气泡 DOM,没有 `start` 时后续的 `chunk` 和 `reasoning` 会因守卫条件直接返回。详见 `.agents/docs/error-experience/2026-06-13-llm_query_text缺少start-end事件导致前端不显示.md`。
#### SSE 传输层
`/api/logs/<sid>` 端点(`routes.py`)的轮询逻辑:
```
每 0.5 秒:
1. 读取 session.log 增量 → yield "data: <日志行>"
2. 读取 file_events.log 增量 → 逐行 yield "data: <JSON>"
3. 读取 llm_stream.log 增量 → 逐行 yield "data: <JSON>"
4. 读取 agent_events.log 增量 → 逐行 yield "data: <JSON>"
5. 检查 result.json 是否存在 → yield "data: {type: 'done'}" 后退出
```
#### 前端事件路由
`process.js` 的 `EventSource` 监听器按 `msg.type` 分发:
```
msg.type === 'file_progress' → setFileProcessing / setFileDone / setFileCached / setFileError
msg.type === 'llm_stream' → handleLLMStream()
msg.type 以 'agent_' 开头 → handleAgentEvent()
msg.type === 'done' → 关闭 EventSource,展示结果
```
#### 前端聊天气泡渲染
**LLM 流式气泡**(`chat.js`):
```
start → _createLLMStreamBubble()
├─ 创建 <div class="chat-bubble processing llm-stream-bubble">
├─ 创建 label 元素(显示 label 文本)
├─ 创建 <details> 可折叠区域(思考过程)
└─ 创建 textContent 元素(正式回答)
└─ 注册到 llmStreamState
reasoning → _appendLLMStreamReasoning()
└─ 追加到 reasoningContent.textContent
chunk → _appendLLMStreamChunk()
└─ 追加到 textContent.textContent
end → _closeLLMStreamBubble()
├─ 气泡 class 从 processing 变为 done
└─ 清空 llmStreamState
error → _errorLLMStreamBubble()
├─ 气泡 class 变为 error
└─ 清空 llmStreamState
```
**Agent 状态气泡**(`agent.js`):
```
agent_state_change → _handleAgentStateChange()
└─ 更新最后一条状态消息(不追加新气泡)
agent_request_supplement → _handleAgentRequestSupplement()
└─ 追加请求补充的气泡
agent_ready → _handleAgentReady()
└─ 追加完成状态气泡
agent_error → _handleAgentError()
└─ 追加错误气泡
```
**文件进度气泡**(`chat.js`):
```
file_progress (processing) → setFileProcessing() → 三点动画
file_progress (done) → setFileDone() → 提取摘要
file_progress (cached) → setFileCached() → 缓存标识
file_progress (error) → setFileError() → 错误信息
```
#### 完整数据流
```mermaid
sequenceDiagram
participant Pipe as 后台线程<br/>(管道)
participant Files as 事件文件<br/>(session 目录)
participant SSE as Flask SSE<br/>(routes.py)
participant ES as EventSource<br/>(process.js)
participant Chat as chat.js
participant Agent as agent.js
Note over Pipe: 启动管道
Pipe->>Files: 追加 llm_stream start
Pipe->>Files: 追加 agent_events state_change
Note over SSE: 0.5s 轮询
SSE->>Files: seek(last_size) + read()
SSE->>ES: yield "data: start"
SSE->>ES: yield "data: state_change"
ES->>Chat: handleLLMStream({phase:"start"})
Chat->>Chat: 创建流式气泡
ES->>Agent: handleAgentEvent({type:"agent_state_change"})
Agent->>Agent: 显示状态消息
Note over Pipe: LLM 流式输出
Pipe->>Files: 追加 llm_stream reasoning
Pipe->>Files: 追加 llm_stream chunk
SSE->>Files: seek(last_size) + read()
SSE->>ES: yield "data: reasoning"
SSE->>ES: yield "data: chunk"
ES->>Chat: handleLLMStream({phase:"reasoning"})
Chat->>Chat: 追加思考内容
ES->>Chat: handleLLMStream({phase:"chunk"})
Chat->>Chat: 追加正式回答
Note over Pipe: 完成
Pipe->>Files: 追加 llm_stream end
Pipe->>Files: 写入 result.json
SSE->>Files: seek(last_size) + read()
SSE->>ES: yield "data: end"
ES->>Chat: handleLLMStream({phase:"end"})
Chat->>Chat: 气泡变完成状态
SSE->>Files: 检测 result.json
SSE->>ES: yield "data: {type:'done'}"
ES->>ES: 关闭连接
```
#### 添加新消息类型的步骤
1. 在对应模块定义 `_emit_xxx()` 函数,写入 session 目录的 JSON 文件
2. 在 `routes.py` 的 `stream_logs()` 轮询循环中新增对该文件的轮询
3. 在 `process.js` 的 EventSource 监听器中按 `msg.type` 路由到新处理器
4. 在 `chat.js` 或 `agent.js` 中实现渲染逻辑
5. 确保 `start` 和 `end` 事件成对出现(如果是流式气泡)