Skip to content

Gateway API 与路由体系

本章目标:

  1. 讲清 create_app() 工厂如何装配 FastAPI 应用:中间件栈顺序、CORS 条件挂载、/health 健康检查、GATEWAY_ENABLE_DOCS 文档开关。
  2. 厘清 16 个 router 的完整端点矩阵,以及 LangGraph 兼容路径 /api/langgraph/* 与原生 /api/* 的等价关系(nginx rewrite 重写)。
  3. 说明嵌入式 LangGraph runtime 如何通过 lifespan + langgraph_runtime() 挂载到 app.state,以及 deps.py 的依赖注入与 services.py 的运行生命周期。

TL;DR

Gateway 是一个 FastAPI 单体应用(端口 8001),create_app()app.py:214-381 中装配。它既是 REST API 又内嵌 LangGraph runtime:runs.py/thread_runs.py/threads.py 复用进程内的 StreamBridge + RunManager + run_agent() 提供 LangGraph Platform 兼容协议,其余 router 提供 models/mcp/skills/memory 等管理 API。前端与 IM 通道访问的 /api/langgraph/* 由 nginx 用 rewrite 剥掉 langgraph/ 前缀后转发到同一套 /api/* 路由(nginx.conf:43-45)。中间件链顺序为 AuthMiddleware → CSRFMiddleware → CORSMiddleware(后添加者最先执行),runtime 单例在 lifespan 内通过 langgraph_runtime() 上下文管理器初始化到 app.state

Overview

为什么 Gateway 要把 REST API 和 LangGraph runtime 揉进同一个进程?

DeerFlow 2.0 不再依赖独立的 LangGraph Server 进程,而是把 agent 运行时嵌入 Gateway。lifespan 在启动时调用 langgraph_runtime(app),把 StreamBridgeRunManager、checkpointer、store 等单例挂到 app.state(app.py:177,deps.py:41-97)。runs.pythread_runs.py 的处理函数通过 services.start_run() 直接 asyncio.create_task(run_agent(...)) 在同一事件循环里跑 agent(services.py:332-346)。

这样设计有三个直接收益:

  • 零跨进程开销:agent 执行与 HTTP 处理共用进程,无需 LangGraph Server 单独部署。
  • 协议兼容:format_sse() 输出的 SSE 帧严格对齐 LangGraph Platform 线格式,前端 useStream React hook 与 Python langgraph-sdk 无需改动即可消费(services.py:44-57)。
  • 统一入口:nginx 把前端期望的 /api/langgraph/* 重写成 Gateway 实际暴露的 /api/*,Gateway 内部只维护一套路由(鉴权细节见 ./14-鉴权-CSRF与授权.md)。

Architecture

应用装配集中在 create_app()。中间件用 app.add_middleware()Auth → CSRF → CORS 顺序添加,Starlette 中间件栈遵循"后添加者最先执行"语义,因此实际请求处理顺序为 CORS → CSRF → Auth → 路由(app.py:307-324)。CORS 中间件只有在 GATEWAY_CORS_ORIGINS 解析出非空 allowlist 时才挂载(app.py:316-324)。16 个 router 通过 app.include_router() 依次注册(app.py:326-370),/health 用内联 @app.get 定义(app.py:372-379)。

Source 列表:

Components / Subsystems

app 工厂(create_app)

create_app() 读取 get_gateway_config(),据 enable_docs 决定 docs_url/redoc_url/openapi_url 是否为 None(禁用 OpenAPI 三件套)(app.py:220-250)。FastAPI(...) 构造时声明了 openapi_tags 元数据并绑定 lifespan=lifespan(app.py:225-305)。模块末尾 app = create_app() 供 uvicorn 直接加载(app.py:385)。

lifespan 与嵌入式 runtime 挂载

lifespan@asynccontextmanager(app.py:160-211):先 get_app_config() 加载配置到 app.state.config,再 async with langgraph_runtime(app) 初始化全部 runtime 单例,随后 _ensure_admin_user(app) 处理首启动/孤儿 thread 迁移、start_channel_service() 启动 IM 通道。langgraph_runtime()AsyncExitStack 依次进入 make_stream_bridgeinit_engine_from_configmake_checkpointermake_store,并构造 RunRepository/FeedbackRepository/make_thread_store/make_run_event_store/RunManager,全部写入 app.state(deps.py:55-92)。注意 persistence engine 必须在 checkpointer 之前初始化,以便 postgres 自动建库逻辑先跑(deps.py:62-67)。

deps.py 依赖注入

_require(attr, label) 工厂生成 FastAPI 依赖:从 request.app.state.<attr> 取单例,缺失抛 503(deps.py:105-115)。由此派生出 get_stream_bridge/get_run_manager/get_checkpointer/get_run_event_store/get_feedback_repo/get_run_store(deps.py:118-123)。get_store 例外——允许返回 None(deps.py:126-128)。get_run_context() 把 checkpointer/store/event_store/thread_store/app_config 组装成 RunContext(deps.py:139-152)。本文件还含鉴权 helper(get_local_provider/get_current_user_from_request 等),细节归 ./14-鉴权-CSRF与授权.md

services.py 运行生命周期

services.pythread_runs/runs router 的共享业务层。build_run_config() 把请求的 config/context 归一化成 LangGraph RunnableConfig,处理 LangGraph >= 0.6 的 context 优先策略与自定义 agent 的 agent_name 注入(services.py:172-240)。start_run() 校验 model_name allowlist、调用 run_mgr.create_or_reject()、upsert thread 元数据,再 asyncio.create_task(run_agent(...)) 启动后台 agent 任务(services.py:248-353)。sse_consumer()bridge.subscribe() 消费事件并 format_sse(),在 finally 中按 on_disconnect 语义决定客户端断连时是否取消任务(services.py:356-387)。

LangGraph 兼容 router 组

  • thread_runs.py:挂在 prefix="/api/threads",定义 RunCreateRequest/RunResponse Pydantic 模型,实现 create/stream/wait/list/get/cancel/join/stream(GET|POST)/messages/events/token-usage(thread_runs.py:28-401)。stream_run 在响应头注入 Content-Location 供 SDK 用贪婪正则提取 run id(thread_runs.py:124-149)。
  • runs.py:无状态运行,prefix="/api/runs",复用 thread_runs.RunCreateRequest_resolve_thread_id() 从 body 取 thread_id 否则生成 uuid(runs.py:27-32)。
  • threads.py:thread CRUD + checkpoint state/history。@field_validator 在入站模型上剥离 owner_id/user_id 等服务端保留键,防止伪造属主(threads.py:41-48)。
  • assistants_compat.py:LangGraph Platform assistants API 桩,满足 useStream 初始化的 assistants.search()/assistants.get()(assistants_compat.py:1-20)。

管理 router 组

models.py/mcp.py/skills.py/memory.py/agents.py/suggestions.py 均用 prefix="/api",在装饰器里写完整子路径(如 @router.get("/models"))(models.py:7)。uploads.pyprefix="/api/threads/{thread_id}/uploads"(uploads.py:34);artifacts.pyprefix="/api" + path 通配 {path:path}(artifacts.py:15,artifacts.py:99-104);channels.pyprefix="/api/channels"(channels.py:12);feedback.pyprefix="/api/threads"(feedback.py:19);auth.pyprefix="/api/v1/auth"(auth.py:23)。agents.py 全部端点先 _require_agents_api_enabled() 检查 agents_api.enabled,关闭时返回 403(agents.py:81-87)。

Data Flow

下图追踪一个 POST /api/langgraph/threads/{id}/runs/stream 请求:经 nginx 重写、中间件链、router、再到内嵌 runtime 的 SSE 流。

速查表

路径 = router prefix + 装饰器内子路径,均已回源码核实。@require_permission 表示该端点带授权装饰器(机制见第 14 章)。

Router路径前缀方法完整路径 / 作用Source
models/apiGET/api/models 列出全部模型 + token_usagemodels.py:34-40
models/apiGET/api/models/{model_name} 单模型详情models.py:93-99
mcp/apiGET/api/mcp/config 取 MCP 配置mcp.py:66-72
mcp/apiPUT/api/mcp/config 写 extensions_config.jsonmcp.py:98-104
skills/apiGET/api/skills 列出全部技能skills.py:88-94
skills/apiPOST/api/skills/install 从 .skill 归档安装skills.py:103-109
skills/apiGET/api/skills/custom 列出自定义技能skills.py:128
skills/apiGET/api/skills/custom/{skill_name} 取 SKILL.md 内容skills.py:138
skills/apiPUT/api/skills/custom/{skill_name} 编辑(带安全扫描)skills.py:154
skills/apiDELETE/api/skills/custom/{skill_name} 删除自定义技能skills.py:191
skills/apiGET/api/skills/custom/{skill_name}/history 变更历史skills.py:219
skills/apiPOST/api/skills/custom/{skill_name}/rollback 回滚skills.py:234
skills/apiGET/api/skills/{skill_name} 单技能详情skills.py:281-287
skills/apiPUT/api/skills/{skill_name} 改 enabled 状态skills.py:304-310
memory/apiGET/api/memory 取记忆数据memory.py:110-117
memory/apiPOST/api/memory/reload 强制重载memory.py:155-162
memory/apiDELETE/api/memory 清空全部记忆memory.py:175-182
memory/apiPOST/api/memory/facts 手工新建 factmemory.py:192-199
memory/apiDELETE/api/memory/facts/{fact_id} 删 factmemory.py:216-223
memory/apiPATCH/api/memory/facts/{fact_id} 部分更新 factmemory.py:235-242
memory/apiGET/api/memory/export 导出 JSONmemory.py:262-269
memory/apiPOST/api/memory/import 导入覆盖memory.py:275-282
memory/apiGET/api/memory/config 记忆配置memory.py:292-298
memory/apiGET/api/memory/status 配置+数据memory.py:329-336
uploads/api/threads/{thread_id}/uploadsPOST`` (根) 上传多文件,@require_permissionuploads.py:170-172
uploads/api/threads/{thread_id}/uploadsGET/limits 上传限额uploads.py:296-298
uploads/api/threads/{thread_id}/uploadsGET/list 列出已上传文件uploads.py:307-309
uploads/api/threads/{thread_id}/uploadsDELETE/{filename} 删除文件uploads.py:326-328
artifacts/apiGET/api/threads/{thread_id}/artifacts/{path:path} 取 artifactartifacts.py:99-105
threads/api/threadsDELETE/{thread_id} 删本地 thread 数据+checkpoint+metathreads.py:212-214
threads/api/threadsPOST`` (根) 创建 thread (幂等)threads.py:246-247
threads/api/threadsPOST/search 搜索/列出 threadthreads.py:311-312
threads/api/threadsPATCH/{thread_id} 合并元数据threads.py:348-350
threads/api/threadsGET/{thread_id} thread 信息threads.py:377-379
threads/api/threadsGET/{thread_id}/state 最新状态快照threads.py:435-437
threads/api/threadsPOST/{thread_id}/state 更新状态 (HITL/改标题)threads.py:487-489
threads/api/threadsPOST/{thread_id}/history checkpoint 历史threads.py:577-579
agents/apiGET/api/agents 列出自定义 agentagents.py:106-112
agents/apiGET/api/agents/check 校验/查重名agents.py:129-134
agents/apiGET/api/agents/{name} 单 agent + SOUL.mdagents.py:158-164
agents/apiPOST/api/agents 创建 agent (201)agents.py:191-198
agents/apiPUT/api/agents/{name} 更新 agentagents.py:259-265
agents/apiGET/api/user-profile 读全局 USER.mdagents.py:357-363
agents/apiPUT/api/user-profile 写全局 USER.mdagents.py:382-388
agents/apiDELETE/api/agents/{name} 删 agent (204)agents.py:410-416
suggestions/apiPOST/api/threads/{thread_id}/suggestions 生成追问suggestions.py:98-105
channels/api/channelsGET/ 全部 IM 通道状态channels.py:25-26
channels/api/channelsPOST/{name}/restart 重启指定通道channels.py:37-38
assistants_compat/api/assistantsPOST/search 列出 assistantsassistants_compat.py:88-89
assistants_compat/api/assistantsGET/{assistant_id} 取单 assistantassistants_compat.py:106-107
assistants_compat/api/assistantsGET/{assistant_id}/graph 图结构桩assistants_compat.py:115-116
assistants_compat/api/assistantsGET/{assistant_id}/schemas schema 桩assistants_compat.py:133-134
auth/api/v1/authPOST/login/local 本地登录auth.py:275-276
auth/api/v1/authPOST/register /logout /change-password /initializeauth.py:304-458
auth/api/v1/authGET/me /setup-status /oauth/{provider} /callback/{provider}auth.py:378-493
feedback/api/threadsPUT/{thread_id}/runs/{run_id}/feedback upsertfeedback.py:61-62
feedback/api/threadsDELETE/{thread_id}/runs/{run_id}/feedback 删本人反馈feedback.py:92-93
feedback/api/threadsPOST/{thread_id}/runs/{run_id}/feedback 新建反馈feedback.py:112-113
feedback/api/threadsGET/{thread_id}/runs/{run_id}/feedback 列出反馈feedback.py:145-146
feedback/api/threadsGET/{thread_id}/runs/{run_id}/feedback/stats 聚合统计feedback.py:157-158
feedback/api/threadsDELETE/{thread_id}/runs/{run_id}/feedback/{feedback_id} 删指定feedback.py:169-170
thread_runs/api/threadsPOST/{thread_id}/runs 创建后台 runthread_runs.py:116-118
thread_runs/api/threadsPOST/{thread_id}/runs/stream 创建+SSEthread_runs.py:124-126
thread_runs/api/threadsPOST/{thread_id}/runs/wait 创建+阻塞thread_runs.py:152-154
thread_runs/api/threadsGET/{thread_id}/runs 列出 runsthread_runs.py:178-180
thread_runs/api/threadsGET/{thread_id}/runs/{run_id} run 详情thread_runs.py:188-190
thread_runs/api/threadsPOST/{thread_id}/runs/{run_id}/cancel 取消thread_runs.py:200-202
thread_runs/api/threadsGET/{thread_id}/runs/{run_id}/join 加入 SSEthread_runs.py:238-240
thread_runs/api/threadsGET/POST/{thread_id}/runs/{run_id}/stream 加入流/取消后流thread_runs.py:259-261
thread_runs/api/threadsGET/{thread_id}/messages thread 消息(附反馈)thread_runs.py:307-309
thread_runs/api/threadsGET/{thread_id}/runs/{run_id}/messages 分页消息thread_runs.py:352-354
thread_runs/api/threadsGET/{thread_id}/runs/{run_id}/events 全事件流thread_runs.py:379-381
thread_runs/api/threadsGET/{thread_id}/token-usage token 聚合thread_runs.py:394-396
runs/api/runsPOST/stream 无状态 run + SSEruns.py:35-36
runs/api/runsPOST/wait 无状态 run + 阻塞runs.py:60-61
runs/api/runsGET/{run_id}/messages 按 run_id 分页消息runs.py:105-107
runs/api/runsGET/{run_id}/feedback 按 run_id 列反馈runs.py:137-139
(内联)/GET/health 健康检查app.py:372-379

Configuration

配置项来源默认安全标注Source
GATEWAY_HOST环境变量0.0.0.0绑定所有网卡;生产应由 nginx 前置config.py:22
GATEWAY_PORT环境变量8001config.py:23
GATEWAY_ENABLE_DOCS环境变量true生产建议设为 false 关闭 /docs/redoc/openapi.json 以收敛 API 暴露面config.py:24app.py:221-223
GATEWAY_CORS_ORIGINS环境变量(逗号分隔)空(不挂 CORS)同源(经 nginx :2026)默认无需配;分离源/端口转发浏览器客户端需精确显式列出。* 与无效 origin 被丢弃,CORS 与 CSRF 共用同一来源csrf_middleware.py:96-111app.py:316-324
健康检查内联路由{"status":"healthy","service":"deer-flow-gateway"}AuthMiddleware._PUBLIC_PATH_PREFIXES 白名单内,无需鉴权app.py:372-379auth_middleware.py:25-30
CORS allow_credentials代码常量True配合精确 origin allowlist(非 *)才合法携带凭证app.py:318-324

get_gateway_config() 用模块级 _gateway_config 单例缓存,仅首次读取环境变量(config.py:14-26)。

Common Pitfalls / Tips

  • 同源默认不挂 CORS:经 nginx :2026 进入是同源请求,GATEWAY_CORS_ORIGINS 留空时根本不 add_middleware(CORSMiddleware)(app.py:316-317)。若直连 8001 或前端在不同端口,浏览器会因缺 CORS 头报错——需显式配 origin,且 CSRF 的 is_allowed_auth_origin 也读同一变量,二者必须一致(csrf_middleware.py:153-171)。
  • 中间件添加顺序 ≠ 执行顺序:add_middleware 越晚添加越先执行。代码先 AuthCSRFCORS,实际请求先过 CORS(若挂载)再 CSRFAuth(app.py:307-324)。
  • /api/langgraph/* 不是独立服务:它是 nginx 的 rewrite ^/api/langgraph/(.*) /api/$1 break; 把前缀剥掉后转发到同一 Gateway,Gateway 内部并无 langgraph 前缀路由(nginx.conf:43-45)。直连 8001 调试时应去掉 langgraph/ 段。
  • 依赖缺失返回 503 而非 500:_require()app.state.<x>None 时抛 503;若在 lifespan 完成前打请求会看到 503 而非异常栈(deps.py:105-115)。
  • engine 必须先于 checkpointer:langgraph_runtime() 显式注释要求 init_engine_from_config()make_checkpointer() 之前,以便 postgres 自动建库先执行(deps.py:62-67)。
  • artifacts 强制下载活跃内容:text/html/application/xhtml+xml/image/svg+xml 无论是否带 ?download 都强制为 attachment,避免在应用源执行脚本(XSS 防护)(artifacts.py:17-21artifacts.py:191-194)。
  • agents API 默认可被关闭:agents.py 全部端点先 _require_agents_api_enabled(),agents_api.enabled=false 时返回 403,部署默认不暴露 agent 管理面(agents.py:81-87)。

References

章节关系
./14-鉴权-CSRF与授权.md本章只点出 AuthMiddleware/CSRFMiddleware 在链中位置;鉴权 token/CSRF 双提交/@require_permission 授权细节在第 14 章展开
./15-Runtime运行时与StreamBridge.md本章的 start_run()/run_agent()/StreamBridge 嵌入点,其内部运行机制与 SSE 桥接在第 15 章
./03-系统整体架构.mdGateway 在 nginx/前端/provisioner 全局拓扑中的位置,本章是其 API 层的细化
./26-IM通道系统.mdIM 通道通过 langgraph-sdk 走本章的 /api/langgraph/* 兼容路径;channels router 提供其状态/重启端点

公司内部参考 · 由 claude-wiki-gen 基于源码自动生成的二次分析