主题
Gateway API 与路由体系
本章目标:
- 讲清
create_app()工厂如何装配 FastAPI 应用:中间件栈顺序、CORS 条件挂载、/health健康检查、GATEWAY_ENABLE_DOCS文档开关。- 厘清 16 个 router 的完整端点矩阵,以及 LangGraph 兼容路径
/api/langgraph/*与原生/api/*的等价关系(nginxrewrite重写)。- 说明嵌入式 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),把 StreamBridge、RunManager、checkpointer、store 等单例挂到 app.state(app.py:177,deps.py:41-97)。runs.py 与 thread_runs.py 的处理函数通过 services.start_run() 直接 asyncio.create_task(run_agent(...)) 在同一事件循环里跑 agent(services.py:332-346)。
这样设计有三个直接收益:
- 零跨进程开销:agent 执行与 HTTP 处理共用进程,无需 LangGraph Server 单独部署。
- 协议兼容:
format_sse()输出的 SSE 帧严格对齐 LangGraph Platform 线格式,前端useStreamReact hook 与 Pythonlanggraph-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 列表:
- 应用工厂:app.py:214-385
- 中间件挂载:app.py:307-324
- router import 与 include:routers/init.py:1-3、app.py:13-29
- runtime 单例:deps.py:41-97
- nginx 重写:nginx.conf:41-45
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_bridge → init_engine_from_config → make_checkpointer → make_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.py 是 thread_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/RunResponsePydantic 模型,实现 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.py 用 prefix="/api/threads/{thread_id}/uploads"(uploads.py:34);artifacts.py 用 prefix="/api" + path 通配 {path:path}(artifacts.py:15,artifacts.py:99-104);channels.py 用 prefix="/api/channels"(channels.py:12);feedback.py 用 prefix="/api/threads"(feedback.py:19);auth.py 用 prefix="/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 | /api | GET | /api/models 列出全部模型 + token_usage | models.py:34-40 |
| models | /api | GET | /api/models/{model_name} 单模型详情 | models.py:93-99 |
| mcp | /api | GET | /api/mcp/config 取 MCP 配置 | mcp.py:66-72 |
| mcp | /api | PUT | /api/mcp/config 写 extensions_config.json | mcp.py:98-104 |
| skills | /api | GET | /api/skills 列出全部技能 | skills.py:88-94 |
| skills | /api | POST | /api/skills/install 从 .skill 归档安装 | skills.py:103-109 |
| skills | /api | GET | /api/skills/custom 列出自定义技能 | skills.py:128 |
| skills | /api | GET | /api/skills/custom/{skill_name} 取 SKILL.md 内容 | skills.py:138 |
| skills | /api | PUT | /api/skills/custom/{skill_name} 编辑(带安全扫描) | skills.py:154 |
| skills | /api | DELETE | /api/skills/custom/{skill_name} 删除自定义技能 | skills.py:191 |
| skills | /api | GET | /api/skills/custom/{skill_name}/history 变更历史 | skills.py:219 |
| skills | /api | POST | /api/skills/custom/{skill_name}/rollback 回滚 | skills.py:234 |
| skills | /api | GET | /api/skills/{skill_name} 单技能详情 | skills.py:281-287 |
| skills | /api | PUT | /api/skills/{skill_name} 改 enabled 状态 | skills.py:304-310 |
| memory | /api | GET | /api/memory 取记忆数据 | memory.py:110-117 |
| memory | /api | POST | /api/memory/reload 强制重载 | memory.py:155-162 |
| memory | /api | DELETE | /api/memory 清空全部记忆 | memory.py:175-182 |
| memory | /api | POST | /api/memory/facts 手工新建 fact | memory.py:192-199 |
| memory | /api | DELETE | /api/memory/facts/{fact_id} 删 fact | memory.py:216-223 |
| memory | /api | PATCH | /api/memory/facts/{fact_id} 部分更新 fact | memory.py:235-242 |
| memory | /api | GET | /api/memory/export 导出 JSON | memory.py:262-269 |
| memory | /api | POST | /api/memory/import 导入覆盖 | memory.py:275-282 |
| memory | /api | GET | /api/memory/config 记忆配置 | memory.py:292-298 |
| memory | /api | GET | /api/memory/status 配置+数据 | memory.py:329-336 |
| uploads | /api/threads/{thread_id}/uploads | POST | `` (根) 上传多文件,@require_permission | uploads.py:170-172 |
| uploads | /api/threads/{thread_id}/uploads | GET | /limits 上传限额 | uploads.py:296-298 |
| uploads | /api/threads/{thread_id}/uploads | GET | /list 列出已上传文件 | uploads.py:307-309 |
| uploads | /api/threads/{thread_id}/uploads | DELETE | /{filename} 删除文件 | uploads.py:326-328 |
| artifacts | /api | GET | /api/threads/{thread_id}/artifacts/{path:path} 取 artifact | artifacts.py:99-105 |
| threads | /api/threads | DELETE | /{thread_id} 删本地 thread 数据+checkpoint+meta | threads.py:212-214 |
| threads | /api/threads | POST | `` (根) 创建 thread (幂等) | threads.py:246-247 |
| threads | /api/threads | POST | /search 搜索/列出 thread | threads.py:311-312 |
| threads | /api/threads | PATCH | /{thread_id} 合并元数据 | threads.py:348-350 |
| threads | /api/threads | GET | /{thread_id} thread 信息 | threads.py:377-379 |
| threads | /api/threads | GET | /{thread_id}/state 最新状态快照 | threads.py:435-437 |
| threads | /api/threads | POST | /{thread_id}/state 更新状态 (HITL/改标题) | threads.py:487-489 |
| threads | /api/threads | POST | /{thread_id}/history checkpoint 历史 | threads.py:577-579 |
| agents | /api | GET | /api/agents 列出自定义 agent | agents.py:106-112 |
| agents | /api | GET | /api/agents/check 校验/查重名 | agents.py:129-134 |
| agents | /api | GET | /api/agents/{name} 单 agent + SOUL.md | agents.py:158-164 |
| agents | /api | POST | /api/agents 创建 agent (201) | agents.py:191-198 |
| agents | /api | PUT | /api/agents/{name} 更新 agent | agents.py:259-265 |
| agents | /api | GET | /api/user-profile 读全局 USER.md | agents.py:357-363 |
| agents | /api | PUT | /api/user-profile 写全局 USER.md | agents.py:382-388 |
| agents | /api | DELETE | /api/agents/{name} 删 agent (204) | agents.py:410-416 |
| suggestions | /api | POST | /api/threads/{thread_id}/suggestions 生成追问 | suggestions.py:98-105 |
| channels | /api/channels | GET | / 全部 IM 通道状态 | channels.py:25-26 |
| channels | /api/channels | POST | /{name}/restart 重启指定通道 | channels.py:37-38 |
| assistants_compat | /api/assistants | POST | /search 列出 assistants | assistants_compat.py:88-89 |
| assistants_compat | /api/assistants | GET | /{assistant_id} 取单 assistant | assistants_compat.py:106-107 |
| assistants_compat | /api/assistants | GET | /{assistant_id}/graph 图结构桩 | assistants_compat.py:115-116 |
| assistants_compat | /api/assistants | GET | /{assistant_id}/schemas schema 桩 | assistants_compat.py:133-134 |
| auth | /api/v1/auth | POST | /login/local 本地登录 | auth.py:275-276 |
| auth | /api/v1/auth | POST | /register /logout /change-password /initialize | auth.py:304-458 |
| auth | /api/v1/auth | GET | /me /setup-status /oauth/{provider} /callback/{provider} | auth.py:378-493 |
| feedback | /api/threads | PUT | /{thread_id}/runs/{run_id}/feedback upsert | feedback.py:61-62 |
| feedback | /api/threads | DELETE | /{thread_id}/runs/{run_id}/feedback 删本人反馈 | feedback.py:92-93 |
| feedback | /api/threads | POST | /{thread_id}/runs/{run_id}/feedback 新建反馈 | feedback.py:112-113 |
| feedback | /api/threads | GET | /{thread_id}/runs/{run_id}/feedback 列出反馈 | feedback.py:145-146 |
| feedback | /api/threads | GET | /{thread_id}/runs/{run_id}/feedback/stats 聚合统计 | feedback.py:157-158 |
| feedback | /api/threads | DELETE | /{thread_id}/runs/{run_id}/feedback/{feedback_id} 删指定 | feedback.py:169-170 |
| thread_runs | /api/threads | POST | /{thread_id}/runs 创建后台 run | thread_runs.py:116-118 |
| thread_runs | /api/threads | POST | /{thread_id}/runs/stream 创建+SSE | thread_runs.py:124-126 |
| thread_runs | /api/threads | POST | /{thread_id}/runs/wait 创建+阻塞 | thread_runs.py:152-154 |
| thread_runs | /api/threads | GET | /{thread_id}/runs 列出 runs | thread_runs.py:178-180 |
| thread_runs | /api/threads | GET | /{thread_id}/runs/{run_id} run 详情 | thread_runs.py:188-190 |
| thread_runs | /api/threads | POST | /{thread_id}/runs/{run_id}/cancel 取消 | thread_runs.py:200-202 |
| thread_runs | /api/threads | GET | /{thread_id}/runs/{run_id}/join 加入 SSE | thread_runs.py:238-240 |
| thread_runs | /api/threads | GET/POST | /{thread_id}/runs/{run_id}/stream 加入流/取消后流 | thread_runs.py:259-261 |
| thread_runs | /api/threads | GET | /{thread_id}/messages thread 消息(附反馈) | thread_runs.py:307-309 |
| thread_runs | /api/threads | GET | /{thread_id}/runs/{run_id}/messages 分页消息 | thread_runs.py:352-354 |
| thread_runs | /api/threads | GET | /{thread_id}/runs/{run_id}/events 全事件流 | thread_runs.py:379-381 |
| thread_runs | /api/threads | GET | /{thread_id}/token-usage token 聚合 | thread_runs.py:394-396 |
| runs | /api/runs | POST | /stream 无状态 run + SSE | runs.py:35-36 |
| runs | /api/runs | POST | /wait 无状态 run + 阻塞 | runs.py:60-61 |
| runs | /api/runs | GET | /{run_id}/messages 按 run_id 分页消息 | runs.py:105-107 |
| runs | /api/runs | GET | /{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 | 环境变量 | 8001 | — | config.py:23 |
GATEWAY_ENABLE_DOCS | 环境变量 | true | 生产建议设为 false 关闭 /docs、/redoc、/openapi.json 以收敛 API 暴露面 | config.py:24、app.py:221-223 |
GATEWAY_CORS_ORIGINS | 环境变量(逗号分隔) | 空(不挂 CORS) | 同源(经 nginx :2026)默认无需配;仅分离源/端口转发浏览器客户端需精确显式列出。* 与无效 origin 被丢弃,CORS 与 CSRF 共用同一来源 | csrf_middleware.py:96-111、app.py:316-324 |
| 健康检查 | 内联路由 | {"status":"healthy","service":"deer-flow-gateway"} | 在 AuthMiddleware._PUBLIC_PATH_PREFIXES 白名单内,无需鉴权 | app.py:372-379、auth_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越晚添加越先执行。代码先Auth后CSRF再CORS,实际请求先过CORS(若挂载)再CSRF再Auth(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-21、artifacts.py:191-194)。 - agents API 默认可被关闭:
agents.py全部端点先_require_agents_api_enabled(),agents_api.enabled=false时返回 403,部署默认不暴露 agent 管理面(agents.py:81-87)。
References
- backend/app/gateway/app.py —
create_app()工厂、lifespan、中间件挂载、router include、/health - backend/app/gateway/deps.py —
langgraph_runtime()上下文管理器、_require()依赖工厂、get_run_context() - backend/app/gateway/services.py —
format_sse()、build_run_config()、start_run()、sse_consumer() - backend/app/gateway/config.py —
GatewayConfig、get_gateway_config() - backend/app/gateway/routers/thread_runs.py — LangGraph 兼容 runs 生命周期与
RunCreateRequest - backend/app/gateway/routers/threads.py — thread CRUD/state/history
- backend/app/gateway/csrf_middleware.py —
get_configured_cors_origins()、auth origin 校验 - docker/nginx/nginx.conf —
/api/langgraph/*rewrite 与各/api/*location
Related Pages
| 章节 | 关系 |
|---|---|
| ./14-鉴权-CSRF与授权.md | 本章只点出 AuthMiddleware/CSRFMiddleware 在链中位置;鉴权 token/CSRF 双提交/@require_permission 授权细节在第 14 章展开 |
| ./15-Runtime运行时与StreamBridge.md | 本章的 start_run()/run_agent()/StreamBridge 嵌入点,其内部运行机制与 SSE 桥接在第 15 章 |
| ./03-系统整体架构.md | Gateway 在 nginx/前端/provisioner 全局拓扑中的位置,本章是其 API 层的细化 |
| ./26-IM通道系统.md | IM 通道通过 langgraph-sdk 走本章的 /api/langgraph/* 兼容路径;channels router 提供其状态/重启端点 |