DeerFlow 文件上传全链路解析:API 端点、沙箱同步与 Agent 上下文注入机制
2026/9/6 17:48:52 网站建设 项目流程

DeerFlow 文件上传全链路解析:API 端点、沙箱同步与 Agent 上下文注入机制

【免费下载链接】deer-flowAn open-source long-horizon SuperAgent harness that researches, codes, and creates. With the help of sandboxes, memories, tools, skill, subagents and message gateway, it handles different levels of tasks that could take minutes to hours.项目地址: https://gitcode.com/GitHub_Trending/de/deer-flow

DeerFlow 后端为每个会话线程(thread)提供了线程隔离的文件上传能力:支持多文件上传、可选的 Office/PDF 转 Markdown、以及让 Agent 在沙箱内通过统一虚拟路径感知并读取附件。读完本文,你可以完整掌握上传 API 的请求/响应契约、config.yamluploads配置段的全部参数、文件在三套路径体系(宿主路径 / 沙箱虚拟路径 / HTTP artifact URL)间的映射关系,以及UploadsMiddleware将当前消息附件注入 Agent 上下文的底层实现。

功能特性总览

DeerFlow 后端提供的文件上传功能具备以下能力:

  • 支持多文件同时上传(multipart/form-data,字段名固定为files
  • 可选地将文档转换为 Markdown(PDF、PPT、Excel、Word)
  • 文件存储在线程隔离的目录中,线程之间互不可见
  • Agent 自动感知当前消息中附带的文件(历史附件按需通过list_uploaded_files工具查询)
  • 支持文件列表查询和删除

网关在应用层限制上传规模,默认最多10 个文件、单文件 50 MiB、单次请求总计 100 MiB;超过限制时后端返回413 Payload Too Large。这三个默认值硬编码在 uploads.py 中:

DEFAULT_MAX_FILES = 10 DEFAULT_MAX_FILE_SIZE = 50 * 1024 * 1024 DEFAULT_MAX_TOTAL_SIZE = 100 * 1024 * 1024

可通过config.yamluploads.max_filesuploads.max_file_sizeuploads.max_total_size调整(样例见 config.example.yaml),前端会读取同一组限制并在选择文件时提示。

API 端点

四个端点全部挂载在 uploads.py 定义的路由前缀下:

router = APIRouter(prefix="/api/threads/{thread_id}/uploads", tags=["uploads"])

每个端点还通过@require_permission装饰器做了权限收敛:上传要求threads:writeowner_check=True,列表/限制查询要求threads:read,删除要求threads:delete(见 uploads.py)。

1. 上传文件

POST /api/threads/{thread_id}/uploads

请求体:multipart/form-data

  • files: 一个或多个文件

响应(UploadResponse):

{ "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.md", "markdown_path": ".deer-flow/threads/{thread_id}/user-data/uploads/document.md", "markdown_virtual_path": "/mnt/user-data/uploads/document.md", "markdown_artifact_url": "/api/threads/{thread_id}/artifacts/mnt/user-data/uploads/document.md" } ], "message": "Successfully uploaded 1 file(s)" }

响应模型UploadedFileInfo(uploads.py)还包含两个文档未强调的字段:original_filename(当文件名被去重重命名为name_1.ext时,保留原始文件名)和skipped_files(因不安全文件名被跳过的文件列表,同时会使successfalse)。

三套路径说明:

字段含义使用者
path实际文件系统路径(相对于backend/目录)运维/排障
virtual_pathAgent 在沙箱中使用的虚拟路径Agent 工具调用
artifact_url前端通过 HTTP 访问文件的 URL浏览器前端

其中virtual_pathartifact_url由 manager.py 统一生成,文件名会做 percent-encoding,保证空格、#?等特殊字符安全:

def upload_artifact_url(thread_id: str, filename: str) -> str: return f"/api/threads/{thread_id}/artifacts{VIRTUAL_PATH_PREFIX}/uploads/{quote(filename, safe='')}" def upload_virtual_path(filename: str) -> str: return f"{VIRTUAL_PATH_PREFIX}/uploads/{filename}"

2. 查询上传限制

GET /api/threads/{thread_id}/uploads/limits

返回网关当前生效的上传限制,供前端在用户选择文件前提示和拦截。响应(UploadLimits):

{ "max_files": 10, "max_file_size": 52428800, "max_total_size": 104857600 }

从源码看,限制解析函数_get_upload_limit兼容历史配置键(max_file_countmax_single_file_size),且对<= 0或非法值自动回退到默认值并打警告日志(uploads.py),因此写错配置不会导致上传功能不可用,只会退回默认限制。

3. 列出已上传文件

GET /api/threads/{thread_id}/uploads/list

响应(UploadListResponse):

{ "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 }

列表由 manager.py 的list_files_in_dir生成:按文件名排序、只收常规文件(follow_symlinks=False)、并自动过滤网关上传暂存文件(.upload-*.part前缀),因此前端不会看到上传中断留下的临时文件。

4. 删除文件

DELETE /api/threads/{thread_id}/uploads/{filename}

响应:

{ "success": true, "message": "Deleted document.pdf" }

删除逻辑在 manager.py 的delete_file_safe中:先做路径遍历校验,若文件扩展名属于可转换集合,会连带删除转换生成的同名.md伴生文件missing_ok=True)。文件不存在返回404,检测到路径遍历返回400

配置说明:uploads 配置段

config.example.yaml 中给出的完整配置段:

uploads: # Application-level upload limits enforced by the gateway and exposed to the # frontend before file selection. max_files: 10 max_file_size: 52428800 # 50 MiB max_total_size: 104857600 # 100 MiB # Automatic Office/PDF conversion runs on the backend host before sandbox # isolation applies. Keep this disabled unless uploads come from a fully # trusted source and you intentionally accept host-side parser risk. auto_convert_documents: false # Controls which PDF-to-Markdown converter is used whenever PDF conversion # runs. Automatic upload conversion is gated separately by # auto_convert_documents. pdf_converter: auto

各参数说明:

参数默认值作用
max_files10单次请求最多文件数
max_file_size52428800 (50 MiB)单文件上限(字节)
max_total_size104857600 (100 MiB)单次请求总大小上限(字节)
auto_convert_documentsfalse是否自动将 Office/PDF 转换为 Markdown
pdf_converterautoPDF 转换策略:auto/pymupdf4llm/markitdown

auto_convert_documents的解析实现(uploads.py)同时接受布尔值和字符串"1"/"true"/"yes"/"on"(大小写不敏感),任何解析异常都回退为False——即安全默认是关闭。文档中的安全说明值得强调:自动转换默认关闭,是为了避免在网关主机上对不受信任的 Office/PDF 上传执行解析(解析发生在沙箱隔离生效之前)。只有在受信任部署中明确接受此风险时,才应设为true

pdf_converter的三种取值在 file_conversion.py 中校验,非法值会警告并回退autoauto模式的策略是:优先使用pymupdf4llm(需另行安装),若输出"稀疏度"异常(少于 50 字符/页,判定为图片型或加密 PDF)则回退到 MarkItDown;未安装pymupdf4llm时直接用 MarkItDown。

支持的文档格式与转换策略

显式启用uploads.auto_convert_documents: true时,以下格式会自动转换为 Markdown:

  • PDF (.pdf)
  • PowerPoint (.ppt,.pptx)
  • Excel (.xls,.xlsx)
  • Word (.doc,.docx)

这组扩展名由 file_conversion.py 的CONVERTIBLE_EXTENSIONS常量定义。转换后的 Markdown 文件保存在同一目录下,文件名为原文件名 +.md扩展名;若转换失败,原文件仍然保留,仅日志记录错误(转换函数返回None时路由不写入markdown_*字段)。

从源码看还有两个工程细节:

  1. 大文件异步转换:超过 1 MB 的文件通过asyncio.to_thread放到线程池转换,避免阻塞事件循环(file_conversion.py)。
  2. 伴生.md的命名去重:上传路由在写.md之前先通过claim_unique_filename预留名称,防止转换输出静默覆盖同请求内的其他文件;转换失败时再释放该名称。

Agent 集成:当前消息的文件上下文

发送消息时,前端会把该消息附带的上传文件元数据放入HumanMessage.additional_kwargs.files(每个条目含filenamesizepathstatus)。UploadsMiddleware(uploads_middleware.py)只把当前消息中的文件注入 Agent 上下文:

<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>

中间件的实际注入比文档示例更丰富(before_agent钩子,uploads_middleware.py):

  • 每个文件条目除名称、大小、路径外,还会附带文档大纲(通过extract_outline_for_file提取的标题及行号),提示 Agent 用read_file按行号定位章节;无结构标题的文档则给出开头预览文本,并建议用grep(pattern='keyword', path='/mnt/user-data/uploads/')搜索;
  • 每条上下文最多列出 10 个文件(_MAX_FILES_PER_CONTEXT_SECTION),超出部分只汇总类型统计,并提示用glob列出全部;
  • 文件名、路径、大纲标题等用户可控内容都经过neutralize_untrusted_tags净化,防止恶意构造的文件名在可信的<current_uploads>包裹内注入指令标签。

以前轮次上传的文件不会在每次请求中重复注入(这是控制 token 消耗的关键设计,见 uploads_middleware.py 的模块注释)。Agent 可按需调用list_uploaded_files工具(实现位于 list_uploaded_files_tool.py)查询历史上传;如果已知文件名,也可直接用read_filegrep访问/mnt/user-data/uploads/下的文件。

中间件还提供了abefore_agent异步钩子,把目录枚举、stat、大纲读取等阻塞式文件 IO 通过run_in_executor派发到工作线程,避免拖慢事件循环。

使用上传的文件:路径映射与存储结构

Agent 在沙箱中运行,使用虚拟路径访问文件,可直接用read_file工具读取:

# 读取原始 PDF(如果支持) read_file(path="/mnt/user-data/uploads/document.pdf") # 读取转换后的 Markdown(推荐) read_file(path="/mnt/user-data/uploads/document.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)

文件存储结构:

backend/.deer-flow/threads/ └── {thread_id}/ └── user-data/ └── uploads/ ├── document.pdf # 原始文件 ├── document.md # 转换后的 Markdown ├── presentation.pptx ├── presentation.md └── ...

上传流程采用"线程目录优先"策略(对应 uploads.py 的upload_files实现):

  • 先写入backend/.deer-flow/threads/{thread_id}/user-data/uploads/作为权威存储;
  • 本地沙箱(sandbox_id=local)直接使用线程目录内容;
  • 默认情况下,非本地沙箱通过acquire_async(路由内为try_acquire_sandbox_for_request)获取沙箱后,再额外把文件update_file同步到/mnt/user-data/uploads/*,确保运行时可见。同步前会通过_make_file_sandbox_writable放宽文件权限位——Docker 沙箱中网关以 root 写文件(0o600),容器内非 root 进程需要 group/other 读写位;
  • 如果 Gateway 与远端沙箱保证挂载同一份线程 user-data(例如正确对齐的共享 PVC、NFS 或 hostPath),可设置sandbox.thread_data_mounts: true;上传路由检测到沙箱提供者的uses_thread_data_mounts为真时会跳过 sandbox acquire 和逐文件同步;
  • 不确定挂载关系时应省略该配置并保留自动检测。错误地设为true会导致文件只存在于 Gateway 存储、沙箱内不可见。

源码中还有一个权限细节值得注意:当调用方被授权体系拒绝sandbox:execute时,上传本身仍会成功(文件留在线程 uploads 目录),只是跳过沙箱同步——因为被拒绝沙箱执行的 Agent 本就无法消费这些文件。

安全设计:文件名校验与写入防护

文档提到"系统会自动验证文件路径,防止目录遍历攻击",manager.py 给出了完整的实现证据链:

  • normalize_filename只保留 basename,拒绝空名、./..、含反斜杠的名称,并限制文件名为 255 字节(UTF-8);
  • validate_upload_destination确认目标是 uploads 目录内的常规文件且无多硬链接,再做resolve().relative_to(base)的遍历校验;
  • 写入路径使用open_upload_file_no_symlink:POSIX 下以O_NOFOLLOW打开,防止上传目录被沙箱进程预埋符号链接后、网关以自身权限覆盖目录外文件;
  • 上传采用暂存文件模式(.upload-*.part),先写临时文件再os.replace原子落位,中途失败自动回滚删除已写文件;网关硬崩溃遗留的暂存文件由cleanup_stale_upload_staging_files在启动时清理。

测试与验证

使用 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())

仓库内有对应的自动化测试:test_uploads_manager.py 覆盖存储管理器逻辑(文件名归一化、去重、路径遍历、安全删除),test_uploads_router.py 覆盖 HTTP 端点的限制与错误码。

限制与 Nginx 配置

  • 最大文件大小:100MB(可在 nginx.conf 中配置client_max_body_size
  • 文件名安全性:系统会自动验证文件路径,防止目录遍历攻击
  • 线程隔离:每个线程的上传文件相互隔离,无法跨线程访问
  • 自动文档转换默认关闭;如需启用,需在config.yaml中显式设置uploads.auto_convert_documents: true

Nginx 侧的实际配置在 nginx.conf:

# Custom API: Uploads endpoint location ~ ^/api/threads/[^/]+/uploads { proxy_pass http://$gateway_upstream; proxy_http_version 1.1; ... # Large file upload support client_max_body_size 100M; proxy_request_buffering off; # Disable response buffering to avoid permission errors }

要点是client_max_body_size 100Mproxy_request_buffering off(关闭请求缓冲,大文件边收边转发、不落 nginx 临时盘)。注意 nginx 层的 100M 略大于应用层max_total_size100 MiB,实际生效的瓶颈以应用层限制为准。

技术实现组件

  1. Upload Router(uploads.py):处理文件上传、列表、删除、限制查询请求;使用 markitdown(可选 pymupdf4llm)转换文档;
  2. Uploads Middleware(uploads_middleware.py):读取当前消息的additional_kwargs.files,在 Agent 请求前生成并注入<current_uploads>文件上下文;历史上传由list_uploaded_files按需查询,不会每轮自动注入;
  3. 共享管理器(manager.py):纯业务逻辑,无 FastAPI 依赖,Gateway 和 Client 两侧共用——文件名安全、暂存/原子落盘、列举、安全删除与路径 URL 生成都在此;
  4. Nginx 配置(nginx.conf):正则路由上传请求到 Gateway API,配置大文件上传支持。

依赖:

  • markitdown>=0.0.1a2— 文档转换
  • python-multipart>=0.0.20— 文件上传处理

故障排查

文件上传失败

  1. 检查文件大小是否超过限制(对比GET /uploads/limits返回值)
  2. 检查 Gateway API 是否正常运行
  3. 检查磁盘空间是否充足
  4. 查看 Gateway 日志:make gateway
  5. 若走 Nginx 反代,注意413 Request Entity Too Large可能来自 nginx 层(client_max_body_size),而非应用层

文档转换失败

  1. 检查 markitdown 是否正确安装:uv run python -c "import markitdown"
  2. 查看日志中的具体错误信息
  3. 某些损坏或加密的文档可能无法转换,但原文件仍会保存(响应中不出现markdown_*字段)

Agent 看不到上传的文件

  1. 确认UploadsMiddleware已在 agent 组装流程中注册
  2. 检查thread_id是否正确
  3. 确认文件确实已上传到backend/.deer-flow/threads/{thread_id}/user-data/uploads/(中间件会校验文件在磁盘上真实存在,缺失条目会被静默跳过并打 info 日志)
  4. 非本地沙箱场景下,确认上传接口没有报错(需要成功完成 sandbox 同步);若误设了sandbox.thread_data_mounts: true而挂载实际未对齐,也会出现"网关有文件、沙箱看不到"的现象

前端集成

// 上传文件示例 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(); }

生产集成时建议先调用GET /uploads/limits获取当前网关限制,在文件选择阶段就拦截超限输入,避免上传到一半收到413

小结

DeerFlow 的文件上传子系统是一条"HTTP 网关 → 线程隔离存储 → 沙箱可见 → Agent 上下文"的完整链路:应用层限制在 uploads.py 中逐字节校验并返回413;存储落位由 manager.py 的安全写入原语保证;沙箱可见性依赖"线程目录优先 + 按需同步/挂载"策略,可用sandbox.thread_data_mounts显式声明共享挂载;Agent 侧则由 uploads_middleware.py 将仅当前消息的附件(含文档大纲)注入<current_uploads>上下文,历史附件交给list_uploaded_files工具按需发现。理解这套三路径映射(宿主路径 / 虚拟路径 / artifact URL)与"当前轮注入、历史按需查"的上下文策略,是排查上传类问题的核心抓手。

【免费下载链接】deer-flowAn open-source long-horizon SuperAgent harness that researches, codes, and creates. With the help of sandboxes, memories, tools, skill, subagents and message gateway, it handles different levels of tasks that could take minutes to hours.项目地址: https://gitcode.com/GitHub_Trending/de/deer-flow

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询