实现Agent对话,合格自动提交,不合格补充材料的能力
This commit is contained in:
+243
-8
@@ -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` 事件成对出现(如果是流式气泡)
|
||||
Reference in New Issue
Block a user