12 KiB
文件上传功能
概述
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。整批文件先写入暂存目录并全部验证,任一文件失败时不会留下半批结果。
响应:
{
"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
返回网关当前生效的分类限制、总量限制、大文件策略和策略版本。
响应:
{
"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
响应:
{
"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}
响应:
{
"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 在每次请求时会自动收到已上传文件的列表,格式如下:
<uploaded_files>
{经过 JSON 转义和字符预算裁剪的文件元数据、统计与采样摘要}
</uploaded_files>
当前轮文件可附带短摘要;历史轮次只注入元数据;自动生成的 Markdown 侧车不会作为第二个独立附件重复注入。整个 <uploaded_files> 块受数据库策略中的常规/硬字符预算约束。
使用上传的文件
Agent 在沙箱中运行,使用虚拟路径访问文件。Agent 可以直接使用 read_file 工具读取上传的文件:
# 读取原始 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 测试
# 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 测试
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 字符,图片还会校验像素数并生成受限尺寸派生图
技术实现
组件
-
Upload Router (
app/gateway/routers/uploads.py)- 处理文件上传、列表、删除请求
- 使用 markitdown 转换文档
-
Uploads Middleware (
packages/harness/deerflow/agents/middlewares/uploads_middleware.py)- 在每次 Agent 请求前注入文件列表
- 自动生成格式化的文件列表消息
-
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 提取。
离线部署包可在不连接数据库的情况下运行依赖自检:
./OFFLINE_INSTALL.sh --office-check
故障排查
文件上传失败
- 检查文件大小是否超过限制
- 检查 Gateway API 是否正常运行
- 检查磁盘空间是否充足
- 查看 Gateway 日志:
make gateway
文档转换失败
- 检查 markitdown 是否正确安装:
uv run python -c "import markitdown" - 查看日志中的具体错误信息
- 某些损坏或加密的文档可能无法转换,但原文件仍会保存
Agent 看不到上传的文件
- 确认 UploadsMiddleware 已在 agent.py 中注册
- 检查 thread_id 是否正确
- 确认文件确实已上传到
backend/.deer-flow/threads/{thread_id}/user-data/uploads/ - 非本地沙箱场景下,确认上传接口没有报错(需要成功完成 sandbox 同步)
开发建议
前端集成
// 上传文件示例
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();
}
扩展功能建议
- 批量删除:支持一次删除多个文件
- 文件搜索:支持按文件名或类型搜索
- 版本控制:保留文件的多个版本
- 图片 OCR:对上传的图片进行 OCR 识别