deer-flow/backend/docs/FILE_UPLOAD.md
2026-08-06 10:27:19 +08:00

343 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 文件上传功能
## 概述
DeerFlow 后端提供了完整的文件上传功能,支持多文件上传,并可选地将 Office 文档和 PDF 转换为 Markdown 格式。
## 功能特性
- ✅ 支持多文件同时上传
- ✅ 可选地转换文档为 MarkdownPDF、PPT、Excel、Word
- ✅ 文件存储在线程隔离的目录中
- ✅ 跨请求、跨进程的同名文件不会互相覆盖
- ✅ 生成的 Markdown 与用户上传命名空间隔离
- ✅ Agent 自动感知当前消息中附带的文件
- ✅ 支持文件列表查询和删除
## API 端点
### 1. 上传文件
```
POST /api/threads/{thread_id}/uploads
```
**请求体:** `multipart/form-data`
- `files`: 一个或多个文件
网关会在应用层限制上传规模,默认最多 10 个文件、单文件 50 MiB、单次请求总计 100 MiB。可通过 `config.yaml``uploads.max_files``uploads.max_file_size``uploads.max_total_size` 调整;前端会读取同一组限制并在选择文件时提示,超过限制时后端返回 `413 Payload Too Large`
**响应:**
```json
{
"success": true,
"files": [
{
"filename": "document.pdf",
"size": 1234567,
"path": ".deer-flow/threads/{thread_id}/user-data/uploads/document.pdf",
"virtual_path": "/mnt/user-data/uploads/document.pdf",
"artifact_url": "/api/threads/{thread_id}/artifacts/mnt/user-data/uploads/document.pdf",
"markdown_file": "document.pdf.md",
"markdown_path": ".deer-flow/threads/{thread_id}/user-data/.upload-conversions/document.pdf.md",
"markdown_virtual_path": "/mnt/user-data/.upload-conversions/document.pdf.md",
"markdown_artifact_url": "/api/threads/{thread_id}/artifacts/mnt/user-data/.upload-conversions/document.pdf.md"
}
],
"message": "Successfully uploaded 1 file(s)"
}
```
**路径说明:**
- `path`: 实际文件系统路径(相对于 `backend/` 目录)
- `virtual_path`: Agent 在沙箱中使用的虚拟路径
- `artifact_url`: 前端通过 HTTP 访问文件的 URL
所有上传入口都先完整写入同目录暂存文件,再以“不替换已有条目”的原子操作发布。同名碰撞依次命名为 `document.pdf``document_1.pdf``document_2.pdf`;响应中的 `filename` 和各路径字段始终使用实际发布名。
### 2. 查询上传限制
```
GET /api/threads/{thread_id}/uploads/limits
```
返回网关当前生效的上传限制,供前端在用户选择文件前提示和拦截。
**响应:**
```json
{
"max_files": 10,
"max_file_size": 52428800,
"max_total_size": 104857600
}
```
### 3. 列出已上传文件
```
GET /api/threads/{thread_id}/uploads/list
```
**响应:**
```json
{
"files": [
{
"filename": "document.pdf",
"size": 1234567,
"path": ".deer-flow/threads/{thread_id}/user-data/uploads/document.pdf",
"virtual_path": "/mnt/user-data/uploads/document.pdf",
"artifact_url": "/api/threads/{thread_id}/artifacts/mnt/user-data/uploads/document.pdf",
"extension": ".pdf",
"modified": 1705997600.0
}
],
"count": 1
}
```
列表只包含 `uploads/` 下的用户主文件;系统生成的 `.upload-conversions/` 资产不会出现在该接口中。
### 4. 删除文件
```
DELETE /api/threads/{thread_id}/uploads/{filename}
```
**响应:**
```json
{
"success": true,
"message": "Deleted document.pdf"
}
```
删除 `document.pdf` 时,只会额外删除它精确拥有的 `.upload-conversions/document.pdf.md`。系统不会推断或删除 `uploads/document.md`;该文件可能是用户独立上传的内容。
## 支持的文档格式
以下格式在显式启用 `uploads.auto_convert_documents: true` 时会自动转换为 Markdown
- PDF (`.pdf`)
- PowerPoint (`.ppt`, `.pptx`)
- Excel (`.xls`, `.xlsx`)
- Word (`.doc`, `.docx`)
转换后的 Markdown 文件保存在系统拥有的 `.upload-conversions/` 目录中,文件名包含完整的实际主文件名。例如:
```text
Primary: /mnt/user-data/uploads/report.pdf
Generated: /mnt/user-data/.upload-conversions/report.pdf.md
Collision: report.pdf, report_1.pdf, report_2.pdf
Deletion: 删除 report.pdf 时只删除 .upload-conversions/report.pdf.md
/mnt/user-data/uploads/report.md 永远不会被推断为生成文件或自动删除。
```
默认情况下,自动转换是关闭的,以避免在网关主机上对不受信任的 Office/PDF 上传执行解析。只有在受信任部署中明确接受此风险时,才应将 `uploads.auto_convert_documents` 设置为 `true`
## Agent 集成
### 当前消息中的文件上下文
发送消息时,前端会把该消息附带的上传文件元数据放入
`HumanMessage.additional_kwargs.files``UploadsMiddleware` 只把当前消息中的文件
注入 Agent 上下文,格式如下:
```xml
<current_uploads>
The following files were uploaded in this message:
- document.pdf (1.2 MB)
Path: /mnt/user-data/uploads/document.pdf
To work with these files:
- Read from the file first — use the outline line numbers and `read_file` to locate relevant sections.
- Use `grep` to search for keywords when you are not sure which section to look at.
- Use `glob` to find files by name pattern.
</current_uploads>
```
以前轮次上传的文件不会在每次请求中重复注入。Agent 可按需调用
`list_uploaded_files` 查询历史上传;如果已知文件名,也可直接使用
`read_file``grep` 访问 `/mnt/user-data/uploads/` 下的文件。
### 使用上传的文件
Agent 在沙箱中运行使用虚拟路径访问文件。Agent 可以直接使用 `read_file` 工具读取上传的文件:
```python
# 读取原始 PDF如果支持
read_file(path="/mnt/user-data/uploads/document.pdf")
# 读取转换后的 Markdown推荐
read_file(path="/mnt/user-data/.upload-conversions/document.pdf.md")
```
**路径映射关系:**
- Agent 使用:`/mnt/user-data/uploads/document.pdf`(虚拟路径)
- 实际存储:`backend/.deer-flow/threads/{thread_id}/user-data/uploads/document.pdf`
- 前端访问:`/api/threads/{thread_id}/artifacts/mnt/user-data/uploads/document.pdf`HTTP URL
- 转换结果:`/mnt/user-data/.upload-conversions/document.pdf.md`(以上传响应的 `markdown_virtual_path` 为准,不要自行推导)
上传流程采用“线程目录优先”策略:
- 先写入 `backend/.deer-flow/threads/{thread_id}/user-data/uploads/` 作为权威存储
- 本地沙箱(`sandbox_id=local`)直接使用线程目录内容
- 默认情况下,非本地沙箱通过 `acquire_async` 获取后,再额外同步到 `/mnt/user-data/uploads/*`,确保运行时可见
- 如果 Gateway 与远端沙箱保证挂载同一份线程 user-data例如正确对齐的共享 PVC、NFS 或 hostPath可设置 `sandbox.thread_data_mounts: true`;上传路由会跳过 sandbox acquire 和逐文件同步
- 不确定挂载关系时应省略该配置并保留自动检测。错误地设为 `true` 会导致文件只存在于 Gateway 存储、沙箱内不可见
## 测试示例
### 使用 curl 测试
```bash
# 1. 上传单个文件
curl -X POST http://localhost:2026/api/threads/test-thread/uploads \
-F "files=@/path/to/document.pdf"
# 2. 上传多个文件
curl -X POST http://localhost:2026/api/threads/test-thread/uploads \
-F "files=@/path/to/document.pdf" \
-F "files=@/path/to/presentation.pptx" \
-F "files=@/path/to/spreadsheet.xlsx"
# 3. 列出已上传文件
curl http://localhost:2026/api/threads/test-thread/uploads/list
# 4. 删除文件
curl -X DELETE http://localhost:2026/api/threads/test-thread/uploads/document.pdf
```
### 使用 Python 测试
```python
import requests
thread_id = "test-thread"
base_url = "http://localhost:2026"
# 上传文件
files = [
("files", open("document.pdf", "rb")),
("files", open("presentation.pptx", "rb")),
]
response = requests.post(
f"{base_url}/api/threads/{thread_id}/uploads",
files=files
)
print(response.json())
# 列出文件
response = requests.get(f"{base_url}/api/threads/{thread_id}/uploads/list")
print(response.json())
# 删除文件
response = requests.delete(
f"{base_url}/api/threads/{thread_id}/uploads/document.pdf"
)
print(response.json())
```
## 文件存储结构
```
backend/.deer-flow/threads/
└── {thread_id}/
└── user-data/
├── uploads/
│ ├── document.pdf # 用户主文件
│ ├── document.md # 用户独立上传,绝不按名称推断归属
│ └── presentation.pptx
└── .upload-conversions/
├── document.pdf.md # document.pdf 的生成结果
└── presentation.pptx.md # presentation.pptx 的生成结果
```
## 限制
- 最大文件大小100MB可在 nginx.conf 中配置 `client_max_body_size`
- 文件名安全性:系统会自动验证文件路径,防止目录遍历攻击
- 线程隔离:每个线程的上传文件相互隔离,无法跨线程访问
- 自动文档转换默认关闭;如需启用,需在 `config.yaml` 中显式设置 `uploads.auto_convert_documents: true`
## 技术实现
### 组件
1. **Upload Router** (`app/gateway/routers/uploads.py`)
- 处理文件上传、列表、删除请求
- 流式写入暂存文件,并通过共享上传管理器原子发布
- 使用 markitdown 转换文档;生成文件发布到系统拥有的隔离目录
2. **Uploads Middleware** (`packages/harness/deerflow/agents/middlewares/uploads_middleware.py`)
- 读取当前消息的 `additional_kwargs.files`
- 在 Agent 请求前生成并注入 `<current_uploads>` 文件上下文
- 历史上传由 `list_uploaded_files` 按需查询,不会每轮自动注入
3. **Nginx 配置** (`nginx.conf`)
- 路由上传请求到 Gateway API
- 配置大文件上传支持
### 依赖
- `markitdown>=0.0.1a2` - 文档转换
- `python-multipart>=0.0.20` - 文件上传处理
## 故障排查
### 文件上传失败
1. 检查文件大小是否超过限制
2. 检查 Gateway API 是否正常运行
3. 检查磁盘空间是否充足
4. 查看 Gateway 日志:`make gateway`
### 文档转换失败
1. 检查 markitdown 是否正确安装:`uv run python -c "import markitdown"`
2. 查看日志中的具体错误信息
3. 某些损坏或加密的文档可能无法转换,但原文件仍会保存
### Agent 看不到上传的文件
1. 确认 UploadsMiddleware 已在 agent.py 中注册
2. 检查 thread_id 是否正确
3. 确认文件确实已上传到 `backend/.deer-flow/threads/{thread_id}/user-data/uploads/`
4. 非本地沙箱场景下,确认上传接口没有报错(需要成功完成 sandbox 同步)
## 开发建议
### 前端集成
```typescript
// 上传文件示例
async function uploadFiles(threadId: string, files: File[]) {
const formData = new FormData();
files.forEach(file => {
formData.append('files', file);
});
const response = await fetch(
`/api/threads/${threadId}/uploads`,
{
method: 'POST',
body: formData,
}
);
return response.json();
}
// 列出文件
async function listFiles(threadId: string) {
const response = await fetch(
`/api/threads/${threadId}/uploads/list`
);
return response.json();
}
```
### 扩展功能建议
1. **文件预览**:添加预览端点,支持在浏览器中直接查看文件
2. **批量删除**:支持一次删除多个文件
3. **文件搜索**:支持按文件名或类型搜索
4. **版本控制**:保留文件的多个版本
5. **压缩包支持**:自动解压 zip 文件
6. **图片 OCR**:对上传的图片进行 OCR 识别