# 文件上传功能 ## 概述 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 识别