首页
/ DeerFlow 后端 API 参考指南:LangGraph 兼容协议与 Gateway 管理 API 的完整实战手册

DeerFlow 后端 API 参考指南:LangGraph 兼容协议与 Gateway 管理 API 的完整实战手册

2026-09-06 19:12:57作者:郜逊炳

DeerFlow 后端对外暴露两套彼此咬合、职责互补的 API:面向 Agent 交互的 LangGraph 兼容 API/api/langgraph/*)与面向平台管理能力的 Gateway API/api/*,覆盖模型、MCP、技能、文件上传与工件)。本文以仓库文档 backend/docs/API.md 为主体,结合 backend/app/gateway 源码与 config.example.yaml 配置,系统讲解认证体系、线程/运行生命周期、SSE 流式协议、PAT 个人令牌边界,以及可直接复制运行的 SDK 与 cURL 用法,帮助读者快速接入 DeerFlow 进行二次开发或机器人平台集成。

一、API 总览:两套接口的边界与统一入口

DeerFlow 后端对外提供两类 API:

  1. LangGraph 兼容 API —— 负责 Agent 交互、线程管理与流式输出,统一挂载在 /api/langgraph/* 路径下,遵循 LangGraph SDK 的请求/响应约定;
  2. Gateway API —— 负责模型列表、MCP 配置、技能(skills)、文件上传与 artifacts 工件等平台资源,路径前缀为 /api/*

所有 API 均通过监听 2026 端口 的 Nginx 反向代理进入。在统一 Nginx 部署形态下,Gateway 拥有 /api/langgraph/* 前缀,并在代理层将其翻译为 Gateway 原生的 /api/* 线程/运行/流式路由。这一路径翻译逻辑可在 docker/nginx/nginx.conf 中直接看到:

# Rewrites /api/langgraph/* to /api/* before proxying to Gateway.
location /api/langgraph/ {
    rewrite ^/api/langgraph/(.*) /api/$1 break;
}

也就是说,POST /api/langgraph/runs/stream 经 Nginx 改写后最终落在 Gateway 的原生路径 POST /api/runs/stream 上。

会话的两种启动方式

与 Agent 对话时,客户端可以选择两种路径:

  • 先建线程、再运行POST /api/langgraph/threads 显式创建线程,随后调用线程内的运行接口;
  • 无状态直达:直接调用 POST /api/langgraph/runs/stream。Gateway 会在 config.configurable.thread_id 缺省时自动创建线程,并把 thread_idrun_id 写入响应的 Content-Location 头中返回给客户端。

二、认证体系:从浏览器会话到平台级信任

浏览器会话通过登录时签发的 access_token 会话 Cookie 认证;程序化客户端(CI、脚本、服务端集成)则改用 Personal Access Token(PAT),以 Bearer 凭据随请求发送:

POST /api/threads/search
Authorization: Bearer dfp_...
Content-Type: application/json

{}

PAT 要求部署配置了数据库后端(SQLite/PostgreSQL);在纯内存后端上,Bearer 凭据会被拒绝,PAT 管理路由返回 503

2.1 Personal Access Tokens(PAT)

Base URL:/api/v1/auth

PAT 的管理要求交互式会话(一个 PAT 不能管理其他 PAT,也不能修改密码),以此避免泄露的自动化令牌被用来铸造新的凭据。原始令牌只在创建时返回恰好一次,服务端仅保存其 SHA-256 摘要。

创建令牌POST /api/v1/auth/pats):

{
  "name": "ci-runner",
  "scopes": ["threads:read", "runs:create", "runs:read"],
  "expires_in_days": 90
}
  • scopes —— 路由权限的子集,可选值包括 threads:readthreads:writethreads:deleteruns:createruns:readruns:cancel。PAT 只能收窄其所属用户的权限,绝不能放大;
  • expires_in_days —— 可选(1365);省略表示永不过期。

响应(201):

{
  "id": "0f0c6e6a-...",
  "name": "ci-runner",
  "scopes": ["runs:create", "runs:read", "threads:read"],
  "expires_at": "2026-11-25T10:30:00Z",
  "created_at": "2026-08-27T10:30:00Z",
  "token": "dfp_..."
}

请立即保存 token——之后无法再次获取。

列出令牌GET /api/v1/auth/pats):返回调用方自己的令牌及 last_used_at/revoked_at 审计字段;绝不返回摘要或原始令牌。

吊销令牌DELETE /api/v1/auth/pats/{pat_id}):吊销立即生效。

2.2 PAT 的约束边界

  • 携带 Authorization 头但校验失败的请求会得到硬性 401——永不回退到会话 Cookie
  • 取消(cancel)能力要求 runs:cancel 出现在每一个携带该能力的请求维度上,而不只是专门的取消路由:包括 POST /api/threads/{thread_id}/runs/{run_id}/stream 上的 ?action=interrupt|rollback(无 action 的 join 保持 runs:read),以及创建运行时的 multitask_strategy=interrupt|rollback(默认的 reject 保持 runs:create)。仅 join 某运行流属于纯观测——观察者断开连接从不取消该运行;
  • 路由级默认拒绝(default-deny):PAT 请求仅被放行到 v1 scopes 所管辖的线程/运行生命周期路由上——POST /api/threads(创建)、POST /api/threads/search(列表)、GET/PATCH/DELETE /api/threads/{thread_id}、线程的 goal/state/compact/history/branches 子路由,以及已实现的 /runs 子路由(GET|POST /api/threads/{thread_id}/runs、仅 POST 的 streamwaitregenerate/prepareedit-regenerate/prepare 集合端点、GET /api/threads/{thread_id}/runs/{run_id} 及其 cancel(POST)、join/messages/events/workspace-changes(GET)、GET|POST .../runs/{run_id}/stream),外加 POST /api/runs/stream|waitGET /api/runs/{run_id}/messages|feedback。任何后续新增到 /runs 下的路由在被显式加入策略前一律拒绝。其余所有已认证路由——memory、agents、models、MCP/skills 配置、integrations、channels、uploads——无论 scopes 如何,对 PAT 调用者一律返回 403。单独的作用域强制只能约束声明了权限的路由,因此这份白名单是外层边界;会话 Cookie 调用者不受影响;
  • PAT 凭据永不携带管理员能力,即使所属用户是管理员也是如此。这包括扩展(extension)贡献的管理员路由:扩展 principal 投影会为 PAT 调用者抑制全部管理员信号;
  • 吊销或删除所属用户后,其 PAT 会在下一个请求上失效。

2.3 内部认证(Internal Auth):平台 HTTP 集成

面向服务到服务集成(例如飞书、企业微信机器人后端),配置环境变量即可启用:

export DEER_FLOW_INTERNAL_AUTH_TOKEN="<long-random-secret>"
Header 必填 描述
X-DeerFlow-Internal-Token 必须与 DEER_FLOW_INTERNAL_AUTH_TOKEN 一致;缺失/错误 → 401
X-DeerFlow-Owner-User-Id 按用户隔离时必填 平台用户 id(如 feishu_ou_alicewecom_user_bob);省略 → default

该模式不使用浏览器 Cookie 与 CSRF 令牌,也不向 users 表写入记录,而是从 owner 头直接设置 threads_meta.user_id/runs.user_id。DeerFlow 只校验平台令牌本身,不校验 owner id 是否代表真实终端用户——用户合法性完全由平台负责。相关信任边界、持久化与安全说明详见 AUTH_DESIGN.md — Internal Auth。集成时在标准 Gateway 线程/运行端点(POST /api/threadsPOST /api/threads/{thread_id}/runs/stream 等)的每个请求上都附加上述两个头即可。

2.4 四种身份来源对照

DeerFlow 支持四种 HTTP 身份来源。它们共享同一套线程/运行隔离规则,区别仅在于是否在 users 表中创建记录,以及外部身份如何映射。完整设计见 AUTH_DESIGN.md

模式 入口 users 隔离键
浏览器会话 登录/注册后的 access_token Cookie users.id
OIDC / SSO OAuth 回调 → Cookie users.id(见 SSO.md
IM 渠道绑定 连接码 + channel_connections 绑定到已注册用户 channel_connections.owner_user_id
内部认证 X-DeerFlow-Internal-Token + X-DeerFlow-Owner-User-Id threads_meta.user_id 上的 owner 字符串

IM 渠道绑定内部认证都属于平台信任型集成:DeerFlow 信任渠道/平台完成终端用户认证。IM 绑定在 channel_connections/channel_conversations 中持久化映射并要求存在 DeerFlow users 记录;内部认证则允许平台以部署共享令牌 + 每请求 owner 头直接调用 Gateway API——无需 users 记录,但线程/运行/检查点隔离行为完全一致。

2.5 浏览器会话(默认)与 CSRF

DeerFlow 对所有非公开 HTTP 路由强制认证。公开路由仅限健康检查/文档元数据以及下列公开认证端点:

  • POST /api/v1/auth/initialize —— 当不存在管理员时创建第一个管理员账号;
  • POST /api/v1/auth/login/local —— 以邮箱/密码登录并签发 HttpOnly access_token Cookie;
  • POST /api/v1/auth/register —— 创建普通 user 账号并设置会话 Cookie;
  • POST /api/v1/auth/logout —— 清除会话 Cookie;
  • GET /api/v1/auth/setup-status —— 报告是否仍需要创建首个管理员。

已认证的认证端点:

  • GET /api/v1/auth/me —— 返回当前用户;
  • POST /api/v1/auth/change-password —— 修改密码,setup 期间可顺带修改邮箱,递增 token_version 并重发 Cookie。

受保护的改状态请求还要求携带 CSRF 双提交令牌:把 csrf_token Cookie 的值放入 X-CSRF-Token 头。login/register/initialize/logout 属于引导认证端点,豁免于双提交令牌但仍拒绝恶意浏览器 Origin 头。

用户隔离由认证后的用户上下文强制执行:

  • 线程元数据按 threads_meta.user_id 隔离;搜索/读/写/删除 API 只暴露当前用户的线程;
  • 线程文件位于 {base_dir}/users/{user_id}/threads/{thread_id}/user-data/,在沙箱内以 /mnt/user-data/ 暴露;
  • Memory 与自定义 Agent 存放在 {base_dir}/users/{user_id}/...

需要留意的是:MCP 出站连接仍可为配置的 HTTP/SSE MCP 服务器使用 OAuth——这与 DeerFlow API 自身的认证相互独立。

三、LangGraph 兼容 API(/api/langgraph

Base URL:/api/langgraph。该公开 API 遵循 LangGraph SDK 约定,让 LangGraph SDK 客户端与既有生态工具可以零适配接入 DeerFlow。

3.1 线程(Threads)

创建线程

POST /api/langgraph/threads
Content-Type: application/json
{
  "metadata": {}
}

响应:

{
  "thread_id": "abc123",
  "created_at": "2024-01-15T10:30:00Z",
  "metadata": {}
}

获取线程状态

GET /api/langgraph/threads/{thread_id}/state

响应:

{
  "values": {
    "messages": [...],
    "sandbox": {...},
    "artifacts": [...],
    "thread_data": {...},
    "title": "Conversation Title"
  },
  "next": [],
  "config": {...}
}

3.2 创建运行(Create Run)

携带输入执行 Agent:

POST /api/langgraph/threads/{thread_id}/runs
Content-Type: application/json
{
  "input": {
    "messages": [
      {
        "role": "user",
        "content": "Hello, can you help me?"
      }
    ]
  },
  "config": {
    "recursion_limit": 100,
    "configurable": {
      "model_name": "gpt-4",
      "thinking_enabled": false,
      "is_plan_mode": false
    }
  },
  "stream_mode": ["values", "messages-tuple", "custom"]
}

Stream Mode 兼容性:

  • 支持:valuesmessages-tuplecustomupdatesdebugtaskscheckpoints
  • 不支持的模式(含 messageseventstools)会在创建运行前直接返回 422。DeerFlow 不会用 values 替身静默顶替任何不支持的模式。

Run Option 兼容性:

  • 支持的并发策略:rejectrollbackinterrupt
  • 兼容性默认值:if_not_exists="create",与 DeerFlow 当前行为一致;
  • 工件投递(artifact delivery):当某次运行在 /mnt/user-data/outputs 下创建或修改了普通文件时会被自动强制启用。present_files 必须呈现本次运行产出的至少一个路径(或包含它的目录),且终态 receipt 必须被持久化;只呈现无关文件不满足投递要求。没有变更输出的运行保持普通对话行为。artifact_delivery 不是客户端可设置的 run option;
  • 不支持以下选项,返回 422webhookstream_resumable=trueafter_secondsfeedback_keys、任何非空 on_completion 值(包括 SDK 值 "complete""continue")、if_not_exists="reject"multitask_strategy="enqueue"
  • stream_resumable=false 可接受:它是 LangGraph SDK 的默认值,请求的正是 DeerFlow 已经提供的不可续传流;
  • 未声明的 SDK 选项(包括 checkpoint_duringdurability)同样返回 422,而非被静默丢弃。

当运行期间输出了变更时,run.delivery 事件保留 Slice 1 事实(presentedpathsby_tool),并新增 produced_pathspresented_pathsmatched_paths 与显式裁定:verificationstagepresentedmismatchednot_started)和 satisfied。没有输出变更的运行的 receipt 保持既有形态。

3.3 Recursion Limit:默认值、钳制与天花板

config.recursion_limit 限制单次运行中 LangGraph 可执行的图步数。统一 Gateway 路径在 build_run_config 中(见 backend/app/gateway/services.py)将默认值设为 100——对 plan-mode 或子 Agent 密集的运行而言这是更安全的起点。客户端仍可在请求体中显式设置 recursion_limit;运行深层嵌套子 Agent 图时可适当调大。

从源码可以看出 Gateway 的三层设计(backend/app/gateway/services.py):

  • _DEFAULT_RECURSION_LIMIT = 100_DEFAULT_MAX_RECURSION_LIMIT = 1000 分别是服务端默认值与兜底天花板;
  • _clamp_recursion_limit(value, max_limit):布尔值、非整数、非正整数值一律回退到 100,合法正整数按 max_recursion_limit 封顶;
  • _resolve_max_recursion_limit()AppConfig.max_recursion_limit 读取上限,配置不可加载(如无 config.yaml 的裸单元测试环境)时回退到 1000

因此 Gateway 绝不原样信任客户端提交的 recursion_limit——任意大的数值会让单次运行执行不受限的 LangGraph 超级步(每一步至少一次 LLM 调用),导致失控的 API 成本 / DoS。build_run_config 在把请求配置透传之后、覆写之前进行钳制并记录告警日志(clamped client recursion_limit)。

配置项位于 config.example.yaml

# Recursion Limit — Hard ceiling for a client-supplied run recursion_limit
# A run's recursion_limit caps the number of LangGraph super-steps (each is at
# least one LLM call). The Gateway never trusts a client-supplied value
# verbatim: any value above this ceiling is clamped down to it, preventing
# runaway API cost / DoS. Invalid or non-positive client values fall back to
# the server default of 100. Raise this only if you legitimately run very
# deeply nested subagent graphs.
max_recursion_limit: 1000

调度任务(scheduled tasks)不读取客户端 body:它们使用 config.yaml 中的 scheduler.recursion_limit(默认 1000,与 Web UI 一致),并在派发时先钳制到 max_recursion_limit(见 config.example.yamlscheduler: 段与 services.py_resolve_scheduler_recursion_limit),保证操作者配置值永远不会作为未钳制值进入 build_run_config。任何超限或配置加载失败的路径都会产生操作者可见的告警。

补充说明:这个 100 只约束 lead 图自身的 LangGraph 超级步预算,与子 Agent 深度相互独立——一次 task() 派发把整个子 Agent 运行塞进 lead 工具节点的一个步骤里,子 Agent 自身通过 subagents.max_turns 执行自己的上限,二者不应混淆。

3.4 Configurable 选项与底层配置处理

请求体中的可配置项:

  • model_name(string):覆盖默认模型;
  • thinking_enabled(boolean):为支持的模型启用扩展思考;
  • is_plan_mode(boolean):启用 TodoList 中间件进行任务跟踪。

在 Gateway 侧(backend/app/gateway/services.py),build_run_config 还处理若干隐蔽而重要的细节:

  • context 优先:LangGraph ≥ 0.6.0 引入 context 作为传递线程级数据的首选通道,且拒绝同时携带 configurablecontext 的请求。若调用方已发送 context,Gateway 会尊重它并跳过自己拼装的 configurable dict(同时记录告警);
  • 剥离 __ 前缀键:调用方 context 中所有 __ 开头的键会被剥离——它们是该 Harness 私有的运行上下文通道(技能密钥绑定来源、活动密钥集合、运行日志),调用者不得预置这些键伪造内部状态。合法的调用方键(secretsuser_id、模型覆盖项)从不用 __ 前缀;
  • thread_id 永远来自 URL 路径而非调用方配置,并被镜像进 configurable["thread_id"],因为检查点器无论调用方如何驱动运行,始终以 configurable["thread_id"] 为状态隔离键;
  • 自定义 Agent 名称:当 assistant_id 指向默认 "lead_agent"(常量 _DEFAULT_ASSISTANT_ID)之外的 Agent 时,其值被归一化(小写、_-、正则 [a-z0-9-]+ 校验)后同时注入 configurable["agent_name"]context,使旧式 configurable 读取方与 LangGraph ToolRuntime.context 消费者都能感知到正确 Agent——没有它 Agent 会静默以默认 lead agent 运行。

3.5 运行历史、流式运行与无状态流

获取运行历史

GET /api/langgraph/threads/{thread_id}/runs
{
  "runs": [
    {
      "run_id": "run123",
      "status": "success",
      "created_at": "2024-01-15T10:30:00Z"
    }
  ]
}

线程内流式运行POST /api/langgraph/threads/{thread_id}/runs/stream,请求体与 Create Run 相同,返回 SSE 流。

无状态流式运行(Stateless Stream Run):在不先创建线程的情况下直接开聊。当 config.configurable.thread_id 缺省时 Gateway 自动创建线程,并把两个标识符都写进响应头:

POST /api/langgraph/runs/stream
Content-Type: application/json
Accept: text/event-stream

经 Nginx 后 /api/langgraph/runs/stream 被改写为原生 Gateway 路径 POST /api/runs/stream。请求体同 Create Run;省略 thread_id 即开启新对话,携带 thread_id 则续接已有对话。

响应:SSE 流并附带 Content-Location 头:

Content-Location: /api/threads/{thread_id}/runs/{run_id}

客户端应从该头解析 thread_idrun_id(路径以 /runs/{run_id} 结尾)。持久化 thread_id,并在下一轮通过 config.configurable.thread_id 回传,即可保持对话历史。

续接对话:

{
  "input": {
    "messages": [
      {
        "role": "user",
        "content": "What did I just ask?"
      }
    ]
  },
  "config": {
    "configurable": {
      "thread_id": "abc123",
      "model_name": "gpt-4"
    }
  },
  "stream_mode": ["values", "messages-tuple", "custom"]
}

典型 SSE 输出流帧(values/messages/end):

event: values
data: {"messages": [...], "title": "..."}

event: messages
data: {"content": "Hello! I'd be happy to help.", "role": "assistant"}

event: end
data: {}

四、Gateway API(/api

Base URL:/api。以下路由的实现分散在 backend/app/gateway/routers 目录(models.pymcp.pyskills.pyuploads.pythreads.pyartifacts.py 等)。

4.1 模型(Models)

列出模型

GET /api/models
{
  "models": [
    {
      "name": "gpt-4",
      "display_name": "GPT-4",
      "supports_thinking": false,
      "supports_vision": true
    },
    {
      "name": "claude-3-opus",
      "display_name": "Claude 3 Opus",
      "supports_thinking": false,
      "supports_vision": true
    },
    {
      "name": "deepseek-v3",
      "display_name": "DeepSeek V3",
      "supports_thinking": true,
      "supports_vision": false
    }
  ]
}

获取模型详情

GET /api/models/{model_name}
{
  "name": "gpt-4",
  "display_name": "GPT-4",
  "model": "gpt-4",
  "max_tokens": 4096,
  "supports_thinking": false,
  "supports_vision": true
}

4.2 MCP 配置管理

获取 MCP 配置

GET /api/mcp/config

要求已认证的管理员会话。响应中敏感的 env/header/OAuth 密钥值会被掩码;密钥容器之外的环境变量占位符以原始形态返回,确保编辑过程不会暴露或持久化其展开值。非法的操作者自定义 JSON/配置形态返回 400,而不会被当作 Gateway 故障上报。

{
  "mcp_servers": {
    "github": {
      "enabled": true,
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-github"],
      "env": {
        "GITHUB_TOKEN": "***"
      },
      "description": "GitHub operations"
    }
  }
}

整体更新 MCP 配置

PUT /api/mcp/config
Content-Type: application/json

要求已认证的管理员会话。API 托管的 stdio MCP 服务器其 command 只允许使用受信任的可执行名(默认:npxuvx)。当部署需要额外受信启动器时,可设置环境变量 DEER_FLOW_MCP_STDIO_COMMAND_ALLOWLIST(逗号分隔列表)。

{
  "mcp_servers": {
    "github": {
      "enabled": true,
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-github"],
      "env": {
        "GITHUB_TOKEN": "$GITHUB_TOKEN"
      },
      "description": "GitHub operations"
    }
  }
}

响应为完整掩码后的 MCP 配置(与 GET 一致)。注意此处 env 中的 $GITHUB_TOKEN:所有目标变更在写入前,Gateway 都会在副本中解析环境变量,并校验运行时将要加载的同一份展开文档,同时把原始的未展开占位符持久化下去。

更新单个 MCP 服务器状态

PATCH /api/mcp/config
Content-Type: application/json

启用/禁用单个已配置的 MCP 服务器而不替换整个 extensions 配置,要求已认证的管理员会话。启用 stdio 服务器会校验其 command(与完整 PUT 使用同一份白名单);禁用则不需要命令在白名单内,其他服务器上的非法命令也不会阻塞本次更新。该端点保留密钥、环境变量占位符、skills、自定义服务器字段及其他顶层 extensions 配置。SSE/HTTP 目标可任选 DeerFlow 的 type 字段或 MCP 规范的 transport 字段。

{
  "server_name": "semantic-scholar",
  "enabled": false
}

响应为完整掩码后的 MCP 配置。未知 server_name 返回 404;尝试启用命令不在白名单的 stdio 服务器返回 400

新增 MCP 服务器

POST /api/mcp/config/servers
Content-Type: application/json

一次新增一个或多个服务器而不替换既有条目。Gateway 会在共享配置锁下重读文件,因此并发的兄弟变更得以保留。重名返回 409。请求体使用与完整 PUT 相同的 mcp_servers 映射。

替换单个 MCP 服务器

PUT /api/mcp/config/server
Content-Type: application/json

完整替换一个既有服务器并保留兄弟条目。省略的普通字段会被删除或重置;显式 *** 占位符会恢复对应的已存密钥。被禁用的 stdio 替换可以保留一个白名单外的语法合法命令以便离线编辑;命令形态与代码注入类环境变量检查在保存时仍会执行,而白名单与可执行参数策略在服务器被启用时才执行。

{
  "server_name": "github",
  "server": {
    "enabled": true,
    "command": "npx",
    "args": ["-y", "@modelcontextprotocol/server-github"],
    "env": {"GITHUB_TOKEN": "***"}
  }
}

删除单个 MCP 服务器

DELETE /api/mcp/config/servers/{server_name}

删除单个服务器而不替换兄弟条目。服务器名作为路径参数、DELETE 请求无 body。放入 URL 前请对名字做百分号编码;路径转换器也会让遗留的空名与含斜杠名保持可寻址。

所有目标变更端点都返回完整掩码后的 MCP 配置。写前校验流程在《整体更新》一节已述:解析环境变量到副本 → 校验运行时加载的同款展开文档 → 持久化原始占位符。

重置 MCP 工具缓存

POST /api/mcp/cache/reset

要求已认证的管理员会话。进程范围内清空缓存的 MCP 工具与持久化 MCP 会话——会影响当前 Gateway 进程中的全部线程与用户。下一次 Agent 运行或工具查找时,会从配置的 MCP 服务器重新加载工具。

{
  "success": true,
  "message": "MCP tools cache reset. Tools will reload on next use."
}

4.3 技能(Skills)

列出技能

GET /api/skills
{
  "skills": [
    {
      "name": "pdf-processing",
      "display_name": "PDF Processing",
      "description": "Handle PDF documents efficiently",
      "enabled": true,
      "license": "MIT",
      "path": "public/pdf-processing"
    },
    {
      "name": "frontend-design",
      "display_name": "Frontend Design",
      "description": "Design and build frontend interfaces",
      "enabled": false,
      "license": "MIT",
      "path": "public/frontend-design"
    }
  ]
}

获取技能详情

GET /api/skills/{skill_name}
{
  "name": "pdf-processing",
  "display_name": "PDF Processing",
  "description": "Handle PDF documents efficiently",
  "enabled": true,
  "license": "MIT",
  "path": "public/pdf-processing",
  "allowed_tools": ["read_file", "write_file", "bash"],
  "content": "# PDF Processing\n\nInstructions for the agent..."
}

启用 / 禁用技能

POST /api/skills/{skill_name}/enable
POST /api/skills/{skill_name}/disable
{
  "success": true,
  "message": "Skill 'pdf-processing' enabled"
}

安装技能:从 .skill 文件安装。

POST /api/skills/install
Content-Type: multipart/form-data

请求体字段:file —— 待安装的 .skill 文件。

{
  "success": true,
  "message": "Skill 'my-skill' installed successfully",
  "skill": {
    "name": "my-skill",
    "display_name": "My Skill",
    "path": "custom/my-skill"
  }
}

重载技能

POST /api/skills/reload

使当前 Gateway 进程中每个用户的技能提示缓存失效。后续运行会重新扫描配置的 public、custom 与 legacy 技能目录;已经开始运行的运行保留其既有技能快照。请求无 body,要求已认证的管理员。Cookie 认证请求需要把 CSRF Cookie 值放入匹配头:

curl -X POST http://localhost:2026/api/skills/reload \
  -b cookies.txt \
  -H "X-CSRF-Token: <csrf_token-cookie-value>"
{
  "success": true,
  "scope": "process",
  "message": "Skill caches invalidated; subsequent runs in this Gateway process will rescan the latest skills."
}

需要注意 success 只代表缓存失效动作本身成功,不表示磁盘上每个文件都合法:畸形技能沿用既有解析器行为(跳过并记日志)。未认证调用者返回 401,非管理员返回 403;失效机制自身失败或进程本地后台扫描未在缓存刷新超时内完成时,返回通用 500。加载器级故障(如挂载根不可用)不会发布空目录:最后一次成功加载的进程缓存仍可用;超时的扫描会由后台 daemon worker 继续执行,完成后仍可填充进程缓存。

reload 的作用域刻意设计为进程本地。每个 Uvicorn worker 或 Kubernetes Pod 都必须被直接调用;经负载均衡 Service 的重复请求不保证触达每个实例。外部 MinIO/NFS/CSI 写入绕过了 install/edit API 所用的校验、SkillScan 与历史记录,因此挂载目录必须仅对可信操作者可写。

4.4 文件上传

上传文件(支持多文件):

POST /api/threads/{thread_id}/uploads
Content-Type: multipart/form-data

请求体字段:files —— 一个或多个待上传文件。

{
  "success": true,
  "files": [
    {
      "filename": "document.pdf",
      "size": 1234567,
      "path": ".deer-flow/threads/abc123/user-data/uploads/document.pdf",
      "virtual_path": "/mnt/user-data/uploads/document.pdf",
      "artifact_url": "/api/threads/abc123/artifacts/mnt/user-data/uploads/document.pdf",
      "markdown_file": "document.md",
      "markdown_path": ".deer-flow/threads/abc123/user-data/uploads/document.md",
      "markdown_virtual_path": "/mnt/user-data/uploads/document.md",
      "markdown_artifact_url": "/api/threads/abc123/artifacts/mnt/user-data/uploads/document.md"
    }
  ],
  "message": "Successfully uploaded 1 file(s)"
}

支持的文档格式(自动转换为 Markdown):

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

列出已上传文件

GET /api/threads/{thread_id}/uploads/list
{
  "files": [
    {
      "filename": "document.pdf",
      "size": 1234567,
      "path": ".deer-flow/threads/abc123/user-data/uploads/document.pdf",
      "virtual_path": "/mnt/user-data/uploads/document.pdf",
      "artifact_url": "/api/threads/abc123/artifacts/mnt/user-data/uploads/document.pdf",
      "extension": ".pdf",
      "modified": 1705997600.0
    }
  ],
  "count": 1
}

删除文件

DELETE /api/threads/{thread_id}/uploads/{filename}
{
  "success": true,
  "message": "Deleted document.pdf"
}

4.5 线程清理(Thread Cleanup)

在 LangGraph 线程本身被删除后,移除 DeerFlow 托管的本地线程文件(.deer-flow/threads/{thread_id}):

DELETE /api/threads/{thread_id}
{
  "success": true,
  "message": "Deleted local thread data for abc123"
}

错误行为:

  • 非法线程 id 返回 422
  • 500 返回通用 {"detail": "Failed to delete local thread data."},完整异常细节保留在服务端日志中。

4.6 工件(Artifacts)

获取工件

GET /api/threads/{thread_id}/artifacts/{path}

路径示例:

  • /api/threads/abc123/artifacts/mnt/user-data/outputs/result.txt
  • /api/threads/abc123/artifacts/mnt/user-data/uploads/document.pdf

查询参数:

  • download(boolean):为 true 时强制下载,附带 Content-Disposition 头。

响应:带恰当 Content-Type 的文件内容。

五、错误响应格式与 HTTP 状态码

所有 API 都以统一格式返回错误:

{
  "detail": "Error message describing what went wrong"
}

HTTP 状态码:

  • 400 —— Bad Request:输入非法;
  • 404 —— Not Found:资源不存在;
  • 422 —— Validation Error:请求校验失败;
  • 500 —— Internal Server Error:服务端错误。

六、流式支持:SSE 协议与重放语义

Gateway 的 LangGraph 兼容 API 使用 Server-Sent Events(SSE) 流式推送运行事件。

线程内流式(线程必须存在):

POST /api/langgraph/threads/{thread_id}/runs/stream
Accept: text/event-stream

无状态流式(无需预建线程,Gateway 自动创建):

POST /api/langgraph/runs/stream
Accept: text/event-stream

两个端点都返回 Content-Location: /api/threads/{thread_id}/runs/{run_id}。DeerFlow Web UI 与 LangGraph SDK 客户端都依赖该头在新建对话的首条消息上发现被分配的 thread_idrun_id

SSE 重放保留与缺口(gap)

客户端可以用 Last-Event-ID 重连某运行流。重放历史受 stream_bridge.queue_maxsize(默认 256)限制;Redis 后端还受滚动 stream_ttl_seconds 约束。游标若仍在保留窗口内,会在该事件后无额外控制帧地续传。

当语法合法的游标早于已保留的水印时,服务端会在任何保留数据之前发送恰好一个 gap 事件,然后关闭该订阅且不发送 end 事件

event: gap
data: {"code":"stream_replay_gap","run_id":"run-123","requested_event_id":"1718000000000-1","earliest_available_event_id":"1718000000100-42","latest_available_event_id":"1718000000200-84","recovery":"reload_durable_state"}

该帧刻意不带 SSE id: 字段。earliest_available_event_idlatest_available_event_id 均为 string | null(缓冲区无任何保留事件时为 null)。消费者必须重载持久化的线程状态与运行事件/消息,随后可从 latest_available_event_id 重连以跟随更新的在线事件;或当缓冲区为空(latest_available_event_idnull)时不带游标重新加入。gap 不会取消正在运行的 run。同样信号也适用于下述场景:无游标订阅者已建立空流等待,但首次 Redis 唤醒在投递前落后——此时 requested_event_idnull。畸形游标的处理与“合法游标但被逐出”不同,且是后端相关的。

七、限流:默认关闭,生产在 Nginx 配置

默认不实现任何限流。生产部署请在 Nginx 中配置:

limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;

location /api/ {
    limit_req zone=api burst=20 nodelay;
    proxy_pass http://backend;
}

八、SDK 用法与命令行示例

8.1 Python(LangGraph SDK)

from langgraph_sdk import get_client

client = get_client(url="http://localhost:2026/api/langgraph")
run_meta: dict[str, str] = {}


def on_run_created(meta) -> None:
    # langgraph-sdk 0.3.x parses Content-Location only when this callback is set.
    if meta.thread_id:
        run_meta["thread_id"] = meta.thread_id
    run_meta["run_id"] = meta.run_id


# Option A: stateless stream — no thread pre-creation
# Gateway auto-creates a thread and returns thread_id/run_id in Content-Location.
async for event in client.runs.stream(
    None,
    "lead_agent",
    input={"messages": [{"role": "user", "content": "Hello"}]},
    config={"configurable": {"model_name": "gpt-4"}},
    stream_mode=["values", "messages-tuple", "custom"],
    on_run_created=on_run_created,
):
    print(event)

thread_id = run_meta["thread_id"]  # persist before the next turn

# Option A (continued): same thread on the next turn
async for event in client.runs.stream(
    None,
    "lead_agent",
    input={"messages": [{"role": "user", "content": "What did I just ask?"}]},
    config={"configurable": {"thread_id": thread_id, "model_name": "gpt-4"}},
    stream_mode=["values", "messages-tuple", "custom"],
    on_run_created=on_run_created,
):
    print(event)

# Option B: thread-scoped stream — create thread first, then stream
thread = await client.threads.create()
async for event in client.runs.stream(
    thread["thread_id"],
    "lead_agent",
    input={"messages": [{"role": "user", "content": "Hello"}]},
    config={"configurable": {"model_name": "gpt-4"}},
    stream_mode=["values", "messages-tuple", "custom"],
    on_run_created=on_run_created,
):
    print(event)

要点:langgraph-sdk 0.3.x 只有在设置 on_run_created 回调时才会解析 Content-Location 头,因此无状态流场景务必注册该回调,并在下一轮把解析出的 thread_id 通过 config.configurable.thread_id 回传。

8.2 JavaScript / TypeScript(fetch)

// Using fetch for Gateway API
const response = await fetch('/api/models');
const data = await response.json();
console.log(data.models);

function parseRunLocation(contentLocation: string | null) {
  if (!contentLocation) return null;
  const match = /\/threads\/([^/]+)\/runs\/([^/]+)/.exec(contentLocation);
  if (!match) return null;
  return { threadId: match[1], runId: match[2] };
}

// Option A: stateless stream — no thread pre-creation
let threadId: string | undefined;
const firstResponse = await fetch("/api/langgraph/runs/stream", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Accept: "text/event-stream",
  },
  body: JSON.stringify({
    input: { messages: [{ role: "user", content: "Hello" }] },
    stream_mode: ["values", "messages-tuple", "custom"],
  }),
});

const created = parseRunLocation(firstResponse.headers.get("Content-Location"));
threadId = created?.threadId;
console.log("thread_id:", created?.threadId, "run_id:", created?.runId);

// Option B: continue the same thread on the next turn
const followUpResponse = await fetch("/api/langgraph/runs/stream", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Accept: "text/event-stream",
  },
  body: JSON.stringify({
    input: { messages: [{ role: "user", content: "What did I just ask?" }] },
    config: { configurable: { thread_id: threadId } },
    stream_mode: ["values", "messages-tuple", "custom"],
  }),
});

// Option C: thread-scoped stream when you already have a thread_id
const streamResponse = await fetch(`/api/langgraph/threads/${threadId}/runs/stream`, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Accept: "text/event-stream",
  },
  body: JSON.stringify({
    input: { messages: [{ role: "user", content: "Hello" }] },
    stream_mode: ["values", "messages-tuple", "custom"],
  }),
});

const reader = streamResponse.body?.getReader();
// Decode and parse SSE frames from reader in your client code.

8.3 cURL 示例

# List models
curl http://localhost:2026/api/models

# Get MCP config
curl http://localhost:2026/api/mcp/config

# Upload file
curl -X POST http://localhost:2026/api/threads/abc123/uploads \
  -F "files=@document.pdf"

# Enable skill
curl -X POST http://localhost:2026/api/skills/pdf-processing/enable

# Stateless stream — no thread pre-creation
curl -s -D - -N -X POST http://localhost:2026/api/langgraph/runs/stream \
  -H "Content-Type: application/json" \
  -H "Accept: text/event-stream" \
  -d '{
    "input": {"messages": [{"role": "user", "content": "Hello"}]},
    "config": {
      "recursion_limit": 100,
      "configurable": {"model_name": "gpt-4"}
    },
    "stream_mode": ["values", "messages-tuple", "custom"]
  }'
# Read Content-Location: /api/threads/{thread_id}/runs/{run_id} from the headers.

# Continue the same thread on the next turn
curl -s -N -X POST http://localhost:2026/api/langgraph/runs/stream \
  -H "Content-Type: application/json" \
  -H "Accept: text/event-stream" \
  -d '{
    "input": {"messages": [{"role": "user", "content": "What did I just ask?"}]},
    "config": {
      "configurable": {"thread_id": "abc123", "model_name": "gpt-4"}
    },
    "stream_mode": ["values", "messages-tuple", "custom"]
  }'

# Thread-scoped flow — create thread first, then stream
curl -X POST http://localhost:2026/api/langgraph/threads \
  -H "Content-Type: application/json" \
  -d '{}'

curl -X POST http://localhost:2026/api/langgraph/threads/abc123/runs/stream \
  -H "Content-Type: application/json" \
  -H "Accept: text/event-stream" \
  -d '{
    "input": {"messages": [{"role": "user", "content": "Hello"}]},
    "config": {
      "recursion_limit": 100,
      "configurable": {"model_name": "gpt-4"}
    },
    "stream_mode": ["values", "messages-tuple", "custom"]
  }'

九、实践要点小结

  1. 统一走 2026 端口:无论 LangGraph 兼容 API 还是 Gateway 管理 API,客户端入口始终是 http://localhost:2026/api/...,Nginx 负责 /api/langgraph/*/api/* 的路径改写;
  2. 无状态开局、线程续聊:新对话直接 POST /api/langgraph/runs/stream,从 Content-Location 解析 thread_id/run_id 并持久化,下一轮通过 config.configurable.thread_id 回传即可维持上下文;Web UI 与 LangGraph SDK 客户端都依赖这一机制;
  3. PAT 只用于线程/运行生命周期/api/v1/auth/pats 创建令牌需交互式会话、原始令牌仅返回一次;其余管理路由对 PAT 一律 403,取消运行等能力还需要对应维度上的 runs:cancel scope;
  4. 平台集成优先 Internal Auth:服务到服务的机器人后端使用 DEER_FLOW_INTERNAL_AUTH_TOKEN 环境变量 + 两个专用请求头,不落 users 表但隔离语义一致;
  5. recursion_limit 受服务端钳制:客户端显式值会被钳制到 config.yamlmax_recursion_limit(默认 1000),非法/非正值回退到默认 100;调度任务改用 scheduler.recursion_limit,调参位置在 config.example.yaml
  6. SSE 断线重放:用 Last-Event-ID 续传;游标过期会收到 gap 帧(无 end、不取消运行),应据此重载持久状态后从 latest_available_event_id 重连。

如需阅读更多背景,仓库内还提供 AUTH_DESIGN.mdSSO.md 等设计文档,backend/docs/README.md 列出了完整的文档索引。

登录后查看全文
热门项目推荐
相关项目推荐