Day 18 / 共 20 天 · 第 4 周 平台底座

API / REST 层

平台对外的门面。今天读 api/rest_api.py + api/features/v1.py:FastAPI 应用结构、启动钩子、异常映射、主要端点、付费墙、Store 市场。

📍 你在整门课的位置 · 第 4 周 平台化与收官
D17 OAuth D18 REST API D19 经典/Forge D20 收官
💡 今天的类比世界观:REST API 层 = 平台的"前台门面" API 是平台对外的脸面,像餐厅前台:FastAPI app = 前台(接待所有客人请求);lifespan 启动钩子 = 开店前的准备(连好数据库、备好料才开门);统一异常映射 = 把后厨事故翻译成礼貌的说辞(不把吓人的报错栈甩给客人);REST 端点 = 菜单上的每道菜402 付费墙 = 余额不足就礼貌挡在门口Store 市场 = 店里的"外卖商城"。今天都用"前台门面"来想。

👶 小白:后端出错时那一大串吓人的报错堆栈,用户会不会直接看到?

👨‍🏫 老师:不会,也绝不能让他看到(既不友好又泄露内部实现)。平台有统一异常映射这层"翻译官":后厨真出了什么岔子(数据库异常、余额不足……),它统一翻译成干净的 HTTP 状态码 + 一句人话(比如余额不足 → 402 "请充值")。用户只看到礼貌的提示,混乱的内部细节留在日志里给工程师看。

L01

FastAPI app 结构

主 REST app 在 api/rest_api.py:208 创建(app = fastapi.FastAPI(...)),随后挂中间件 SecurityHeadersMiddleware:220)、GZipMiddleware:223),并用大量 include_router(...)(从 :328 起)组装模块化路由。它是整个平台对外的 HTTP 门面——前端和外部集成都通过它操作平台。特点:lifespan 启动钩子、几个中间件(Day 04 讲过 CORS/GZip/安全头)、统一异常映射、大量 include_router(模块化路由)。

一个请求进来,依次穿过:中间件 → 路由 → 业务 → 异常映射 前端/外部 中间件安全头/GZip/CORS 路由 router/api/graphs... 业务逻辑建图/执行/计费 异常映射→ 404/402/500
rest_api.py 就是这条流水线的装配处:FastAPI() 建 app、add_middleware 挂中间件、include_router 挂路由、异常处理器统一把内部异常翻成 HTTP 码。
REST API 是平台的"前台" 前端画布上的每个操作(建 Agent、运行、查余额、逛市场)背后都是一个 REST 请求。REST 层负责:接收请求 → 鉴权 → 调后端逻辑 → 返回结果。它是"用户/前端"和"平台内部"之间的接口层——所有对外能力都在这暴露。用 uvicorn + uvloop + httptools 起服务(高性能 async),显式 ws="none"(WebSocket 交给独立进程,Day 15)。
L02

lifespan 启动钩子

rest_api.py:103 的 lifespan:应用启动时连 DB/Redis、配线程池、注册所有 Block(Day 02 的 load_all_blocks + initialize_blocks)、注册托管凭证 provider、跑数据迁移。

为什么在启动时做这些? 这些是"服务运行的前提"——必须在接第一个请求之前就绪:Block 得先注册好(否则前端问"有哪些块"答不上来)、DB 得连上、迁移得跑完(表结构最新)。lifespan 是 FastAPI 的"启动/关闭钩子"——启动时初始化、关闭时清理。把"一次性准备工作"放这里,保证服务对外可用时一切就绪。和 OpenHands 的 Alembic 启动迁移(Day 10)同一思路。
L03

统一异常映射

🤔 痛点:如果每个端点都自己 try/except 转 HTTP 码会怎样? 平台有几十上百个端点,各处业务都可能抛"余额不足/没权限/找不到"。要是每个端点都手写一遍 try...except...return 404,那是几百份重复代码——而且总有人漏写,导致某个接口把内部异常原样吐给前端(泄漏细节、前端也无法统一处理)。
💡 本质:业务只抛"语义异常",翻译成 HTTP 码的事集中到一处 关注点分离:业务代码只管抛语义清晰的异常(NotFoundUserPaywalledError…),至于"这对应哪个 HTTP 码"由一处统一的异常处理器决定。改一处,全站生效;前端也能靠稳定的 HTTP 码统一反应(收到 402 就弹充值框)。这就是"协议转换"与"业务逻辑"的解耦。

rest_api.py:309 把各种内部异常映射成 HTTP 状态码:

内部异常→ HTTP 码
Prisma 错误500
NotFound404
NotAuthorized403
UserPaywalledError402(付费墙)
PreconditionFailed428
为什么要"统一映射"? 后端各处会抛各种业务异常(余额不足、没权限、找不到)。如果每个端点都自己 try/except 转 HTTP 码,重复且易漏。统一在一处把"业务异常 → HTTP 码"映射好——业务代码只管抛语义化的异常(如 UserPaywalledError),映射层自动转成正确的 HTTP 响应。这样前端能靠 HTTP 码统一处理(收到 402 就弹充值框)。关注点分离:业务抛语义异常、映射层管协议转换。
L04

主要 REST 端点

核心 v1 路由在 api/features/v1.py(前缀 /api):

功能端点
列出所有 BlockGET /api/blocks(缓存 + GZip)
直接执行单个 BlockPOST /api/blocks/{id}/execute(先扣费)
创建 Agent(图)POST /api/graphs
执行 AgentPOST /api/graphs/{id}/execute/{version}
停止/查状态/查成本.../stop.../executions.../cost_summary
查余额/充值GET /api/credits.../request_top_up
GET /api/blocks 返回 300+ 块,响应很大 → 用缓存 + GZip 压缩(Day 04)。POST /api/graphs/{id}/execute 就是 Day 05 追踪的执行入口。这张端点表 = 平台对外能力的清单。前端的每个功能都对应这里的某个端点。
L05

付费墙 402

执行/直跑 Block 的路由挂了 Depends(enforce_payment_paywall)v1.py:1848)——余额 ≤0 就返回 402 Payment Required

📝 举个例子:余额为 0 时执行 Agent 会收到什么 请求:POST /api/graphs/abc/execute/3(你的余额 = 0)。
因为该路由挂了 Depends(enforce_payment_paywall),请求还没进业务逻辑就被前置依赖拦下 →
响应:402 Payment Required,body {"detail":"Insufficient balance"}
前端一看到 402,不显示报错,而是弹出"余额不足,去充值"对话框(Day 16 的 Stripe 充值)。充值后重试即可。
402 这个冷门状态码 HTTP 402 "Payment Required" 是个很少用的状态码——大部分网站没有"必须付费才能用"的接口。但 AutoGPT Platform 是计量 SaaS,402 正好表达"你余额不够,请充值"。前端收到 402 → 弹出充值框。Depends(enforce_payment_paywall) 是 FastAPI 的依赖注入——把"检查余额"作为这些端点的前置依赖,统一拦截,不用每个端点手写检查。用依赖注入做横切的"付费墙"检查——优雅。
L06

Store 市场

api/features/store/routes.py 是 Agent 市场:列出商店 Agent(:141)、看单个 Agent、下载、创作者列表、提交上架:424)、上传媒体/生成封面图。审核走 admin 路由。

市场(marketplace)的意义 用户搭好一个好用的 Agent,可以上架到市场分享/出售;别人可以浏览、下载、fork(Day 11)来用或改。这形成生态:创作者贡献 Agent、使用者受益、平台抽成——像 App Store。结合 Day 09 的计费,别人跑你上架的 Agent 消耗 credit,可能给你分成。市场 + 计费 = 把"搭 Agent"变成一门生意——这是平台商业模式的关键一环。回忆 Day 01 的转型:从"炫技的自主智能体"到"可持续的 Agent 经济平台"。
L07

路由模块化

rest_api.py:328 用大量 include_router 按功能拆分:/api/integrations(Day 17)、/api/analytics/api/store/api/builder/api/library/api/chat/api/mcp/api/oauth、一批 /api/admin/*,还 mount 了外部 API /external-api(给 API-key 调用方)。

为什么模块化路由? 平台功能多(几十上百个端点)。按功能把路由拆成独立模块(integrations 一个、store 一个…),主 app 用 include_router 组装——每个模块管自己的端点,清晰、易维护、易协作(不同团队管不同模块)。和 OpenHands 的 v1_router 汇总、CrewAI 的模块化一样。/external-api(给外部程序用 API key 调)体现平台不只服务前端,也能被程序化调用——真正的开放平台。
L08

今日小结 + 动手

🧠 今天你应该能回答

  • REST 层是平台的什么角色?为什么 ws="none"?
  • lifespan 启动钩子做什么?为什么在启动时做?
  • 统一异常映射的好处?402 是什么?
  • 主要端点有哪些?付费墙怎么用依赖注入实现?
  • Store 市场的意义?为什么路由要模块化?

✋ 动手

P=autogpt_platform/backend/backend
grep -n 'lifespan\|include_router\|UserPaywalledError\|402' $P/api/rest_api.py | head
grep -n 'blocks\|graphs.*execute\|enforce_payment_paywall' $P/api/features/v1.py | head
grep -n 'store/agents\|create.*submission' $P/api/features/store/routes.py | head
明天预告 · Day 19经典 AutoGPT 与 Forge(起源故事)——回到 2023 那个引爆热潮的"自主循环智能体",读 classic/ 的 think→plan→act 循环和 Forge 框架。理解 Agent 领域的来路。
← Day 17 集成凭证 Day 19 · 经典起源 →