# 文件上传功能
## 概述
DeerFlow 后端提供了完整的文件上传功能,支持多文件上传,并可选地将 Office 文档和 PDF 转换为 Markdown 格式。
## 功能特性
- ✅ 支持多文件同时上传,默认单条消息最多 20 个
- ✅ 上传策略持久化到数据库,管理员可在「设置 → 上传设置」实时调整
- ✅ 按文件类别设置格式与单文件上限,并校验扩展名和文件真实签名
- ✅ 可选地转换文档为 Markdown(PDF、PPT、Excel、Word)
- ✅ 文件存储在线程隔离的目录中
- ✅ Agent 自动感知已上传的文件
- ✅ 大文件采用流式统计和头部/中段/尾部采样,不把全文直接塞入模型上下文
- ✅ 支持文件列表查询和删除
## API 端点
### 1. 上传文件
```
POST /api/threads/{thread_id}/uploads
```
**请求体:** `multipart/form-data`
- `files`: 一个或多个文件
网关每次上传都会读取数据库中的全局上传策略;前端也读取同一策略并在选择文件时预校验。内置默认值如下:
| 类别 | 默认格式 | 默认单文件上限 |
|---|---|---:|
| 文档/文本 | `.txt`、`.md`、`.doc`、`.docx`、`.ppt`、`.pptx`、`.pdf` | 10 MiB |
| 图片 | `.jpg`、`.jpeg`、`.png` | 10 MiB |
| 表格 | `.xls`、`.xlsx`、`.csv` | 5 MiB |
单条消息默认最多 20 个文件、附件合计默认不超过 100 MiB。管理员可在「设置 → 上传设置」调整数量、总量、各类别格式和大小,以及大文件采样阈值与输出字符预算。服务端始终执行最终校验;超过限制返回 `413 Payload Too Large`,不支持或内容签名不匹配返回 `415 Unsupported Media Type`。整批文件先写入暂存目录并全部验证,任一文件失败时不会留下半批结果。
**响应:**
```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/uploads/document.pdf.md",
"markdown_virtual_path": "/mnt/user-data/uploads/document.pdf.md",
"markdown_artifact_url": "/api/threads/{thread_id}/artifacts/mnt/user-data/uploads/document.pdf.md"
}
],
"message": "Successfully uploaded 1 file(s)"
}
```
**路径说明:**
- `path`: 实际文件系统路径(相对于 `backend/` 目录)
- `virtual_path`: Agent 在沙箱中使用的虚拟路径
- `artifact_url`: 前端通过 HTTP 访问文件的 URL
### 2. 查询和管理上传策略
所有已登录用户都可读取当前策略:
```
GET /api/upload-policy
```
管理员原子替换或恢复默认策略:
```
PUT /api/upload-policy
POST /api/upload-policy/reset
```
策略首次启动时会将内置默认值写入数据库表 `upload_policy`,后续修改带递增 `revision` 和更新人/更新时间审计信息。各 Gateway worker 在读取或修改策略时同步刷新 harness 运行时副本。
兼容旧前端的线程级限制查询仍保留:
```
GET /api/threads/{thread_id}/uploads/limits
```
返回网关当前生效的分类限制、总量限制、大文件策略和策略版本。
**响应:**
```json
{
"max_files": 20,
"max_file_size": 10485760,
"max_total_size": 104857600,
"max_files_per_message": 20,
"max_total_bytes": 104857600,
"groups": {
"document": {"max_bytes": 10485760, "extensions": [".txt", ".md", ".doc", ".docx", ".ppt", ".pptx", ".pdf"]},
"image": {"max_bytes": 10485760, "extensions": [".jpg", ".jpeg", ".png"]},
"spreadsheet": {"max_bytes": 5242880, "extensions": [".xls", ".xlsx", ".csv"]}
},
"large_file": {
"threshold_bytes": 1048576,
"max_result_chars": 12000,
"hard_max_result_chars": 32000
},
"revision": 1
}
```
### 3. 有界文本预览
```
GET /api/threads/{thread_id}/artifact-preview?path=/mnt/user-data/uploads/example.txt&max_chars=12000
```
小文件返回有界完整文本;大文件返回流式统计、开头/中段/结尾的确定性分块采样、截断标记和警告。浏览器预览不会调用无界的 `response.text()`。
### 4. 列出已上传文件
```
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
}
```
### 5. 删除文件
```
DELETE /api/threads/{thread_id}/uploads/{filename}
```
**响应:**
```json
{
"success": true,
"message": "Deleted document.pdf"
}
```
## 支持的文档格式
以下格式在显式启用 `uploads.auto_convert_documents: true` 时会自动转换为 Markdown:
- PDF (`.pdf`)
- PowerPoint (`.ppt`, `.pptx`)
- Excel (`.xls`, `.xlsx`)
- Word (`.doc`, `.docx`)
- OpenDocument (`.odt`, `.ods`, `.odp`)
转换后的 Markdown 文件会保存在同一目录下,文件名为完整原文件名再追加 `.md`,例如 `report.docx.md`。这样 `report.docx` 和 `report.pdf` 不会同时覆盖同一个 `report.md`。前端预览仍兼容升级前的旧侧车名称。
通用部署可以关闭自动转换,以避免在网关主机上对不受信任的 Office/PDF 上传执行解析。本仓库当前 `config.yaml` 和纯 DeerFlow 离线部署包默认设置为 `uploads.auto_convert_documents: true`,用于受信任的内网部署。
## Agent 集成
### 自动文件列举
Agent 在每次请求时会自动收到已上传文件的列表,格式如下:
```text
{经过 JSON 转义和字符预算裁剪的文件元数据、统计与采样摘要}
```
当前轮文件可附带短摘要;历史轮次只注入元数据;自动生成的 Markdown 侧车不会作为第二个独立附件重复注入。整个 `` 块受数据库策略中的常规/硬字符预算约束。
### 使用上传的文件
Agent 在沙箱中运行,使用虚拟路径访问文件。Agent 可以直接使用 `read_file` 工具读取上传的文件:
```python
# 读取原始 PDF(如果支持)
read_file(path="/mnt/user-data/uploads/document.pdf")
# 读取转换后的 Markdown(推荐)
read_file(path="/mnt/user-data/uploads/document.pdf.md")
```
当文本达到大文件阈值时,`read_file` 不会一次性返回全文,而会逐块读取并产生:总字节数/行数、空行和乱码统计、最长行、内容哈希,以及开头/中段/结尾样本。需要进一步核实时应使用明确的行区间或有界 `grep`。超长单行也按固定大小片段处理,内存占用不会随单行长度无限增长。
**路径映射关系:**
- 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)
上传流程采用“线程目录优先”策略:
- 先写入 `backend/.deer-flow/threads/{thread_id}/user-data/uploads/` 作为权威存储
- 本地沙箱(`sandbox_id=local`)直接使用线程目录内容
- 非本地沙箱会额外同步到 `/mnt/user-data/uploads/*`,确保运行时可见
## 测试示例
### 使用 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.pdf.md # 转换后的 Markdown
├── presentation.pptx
├── presentation.pptx.md
└── ...
```
## 限制
- 数据库策略可配置范围带服务端硬上限:文件数最多 100、单文件最多 100 MiB、单次总量最多 200 MiB;反向代理的 `client_max_body_size` 还必须不低于所需请求大小
- 文件名安全性:系统会自动验证文件路径,防止目录遍历攻击
- 文件真实性:PDF、JPG、PNG、OLE 与 OOXML 会校验签名/容器结构,OOXML 同时限制条目数、解压总量和压缩比
- 线程隔离:每个线程的上传文件相互隔离,无法跨线程访问
- 自动文档转换由 `uploads.auto_convert_documents` 控制;转换在事件循环外执行、限制并发,提取的 Markdown 还有固定字符上限
- 模型安全硬边界:单次文件摘要/读取结果最多 32,000 字符,图片还会校验像素数并生成受限尺寸派生图
## 技术实现
### 组件
1. **Upload Router** (`app/gateway/routers/uploads.py`)
- 处理文件上传、列表、删除请求
- 使用 markitdown 转换文档
2. **Uploads Middleware** (`packages/harness/deerflow/agents/middlewares/uploads_middleware.py`)
- 在每次 Agent 请求前注入文件列表
- 自动生成格式化的文件列表消息
3. **Nginx 配置** (`nginx.conf`)
- 路由上传请求到 Gateway API
- 配置大文件上传支持
### 依赖
- `markitdown>=0.0.1a2` - 文档转换
- `python-multipart>=0.0.20` - 文件上传处理
- 纯 DeerFlow 离线镜像还内置 `python-docx`、`openpyxl`、`python-pptx`、LibreOffice Headless、Pandoc、Poppler 和中文字体。旧版 DOC/XLS/PPT 及 ODT/ODS/ODP 会先经 LibreOffice 规范化,再由 MarkItDown 提取。
离线部署包可在不连接数据库的情况下运行依赖自检:
```bash
./OFFLINE_INSTALL.sh --office-check
```
## 故障排查
### 文件上传失败
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. **图片 OCR**:对上传的图片进行 OCR 识别