架构总览与请求生命周期

专栏:new-api 源码拆解 · 第 2 / 12 篇
new-api架构gin

:::info 学习目标 完成本篇后你能够:画出 new-api 的七层架构图;完整追踪一个中继请求经过的全部中间件与模块;说清模块之间的协作接口(上下文键、服务调用、共享表)。 前置:了解基本的 HTTP 服务概念。预计时长:60 分钟。 :::

:::note 本章术语速查(新手建议先读)

  • 网关(Gateway):站在你和几十个 AI 服务商之间的”中间商”——你只对它说话,它负责转发、记账、换协议。
  • gin:Go 语言最流行的 Web 框架,处理 HTTP 请求。
  • 中间件(Middleware):请求进入业务代码前依次经过的”检查站”(鉴权、限流、选路都在这里)。
  • gorm:Go 语言最流行的 ORM——用 Go 对象操作数据库表,不用手写 SQL。
  • go
    :Go 的编译特性,把前端文件直接”打包进”二进制。
  • SSE(Server-Sent Events):服务器向浏览器持续推送消息的技术,AI 回复的”打字机效果”靠它。 :::

new-api 是一个 Go 单体应用:gin 做 HTTP、gorm 做 ORM、React 前端经 go:embed 内嵌进同一个二进制。全仓库约 930 个 Go 文件,按目录划分成职责清晰的模块——读它的关键是先抓住「一次请求的流动路径」,再往路径两侧挂模块

七层架构

图表(newapi-architecture-lifecycle.md)

启动流程:一张依赖关系图

main.go 的启动序列告诉你所有模块的初始化顺序与依赖:

  1. InitResources():加载 .env → 初始化日志 → 倍率设置 → HTTP 客户端/token 编码器 → model.InitDB() → casbin 授权初始化 → model.InitOptionMap() → Redis → i18n → 自定义 OAuth provider;
  2. 若开启内存缓存:model.InitChannelCache() + 后台 SyncChannelCache(第 10 篇);
  3. 启动后台协程:SyncOptions(配置热更新)、SyncTaskPlugins(任务插件)、authz.StartPolicySyncUpdateQuotaData(看板落库)、订阅额度重置、系统任务 runner(DB 租约去重的定时任务);
  4. HTTP 服务:gin.New() + 全局中间件 + router.SetRouter(...),退出时优雅关机(SSE 最长等 120 秒)并 SaveQuotaDataCache() 落库看板数据。
// main.go:236-262(节选)
server := gin.New()
server.Use(gin.CustomRecovery(func(c *gin.Context, err any) { ... }))
// This will cause SSE not to work!!!
//server.Use(gzip.Gzip(gzip.DefaultCompression))
server.Use(middleware.RequestId())
server.Use(middleware.Version())
server.Use(middleware.I18n())
middleware.SetUpLogger(server)
router.SetRouter(server, router.WebAssets{...})

那行注释是全仓库最实用的教训之一:全局 gzip 会破坏 SSE 流式——对网关来说流式是命根子。

两类流量,两套路由

router/main.goSetRouter 依次挂 API、Dashboard、Relay、TaskPluginProtocol、Video、Task、Plugin、Web 八组路由。关键是控制台流量与模型流量泾渭分明

控制台流量中继流量
前缀/api/v1/v1beta/mj/pg
凭据session JWT / access tokensk-xxx 令牌
鉴权中间件UserAuth / AdminAuth / RootAuthTokenAuth
限流GlobalAPIRateLimit / CriticalRateLimitModelRequestRateLimit(按模型/用户)
选路无(业务路由固定)Distribute(渠道选择)
// router/relay-router.go:82-105(节选)
relayV1Router := router.Group("/v1")
relayV1Router.Use(middleware.RouteTag("relay"))
relayV1Router.Use(middleware.SystemPerformanceCheck())  // 过载保护
relayV1Router.Use(middleware.TokenAuth())               // sk-xxx 鉴权
relayV1Router.Use(middleware.ModelRequestRateLimit())   // 按模型限流
httpRouter.POST("/chat/completions", func(c *gin.Context) {
    controller.Relay(c, types.RelayFormatOpenAI)
})
httpRouter.POST("/messages", func(c *gin.Context) {
    controller.Relay(c, types.RelayFormatClaude)
})

未匹配的路由回落到内嵌的 React SPA——单二进制就是完整产品。

中间件链全景

中继请求按顺序穿过(这是全仓库的「主干道」):

全局:Recovery → RequestId → Version → I18n → Logger
relay 组:CORS → DecompressRequest → BodyStorageCleanup → Stats
        → SystemPerformanceCheck(过载→503)
        → TokenAuth(sk-xxx 鉴权 + 分组判定)
        → ModelRequestRateLimit(按模型限流)
        → Distribute(解析 model → 选渠道 → 写上下文)
handler:controller.Relay(c, relayFormat)

API 侧另有 TurnstileCheck(人机验证)、SessionCookieOriginGuard(防 CSRF)、secure_verification(敏感操作二次验证)等。

一次请求的完整生命周期

把中间件链展开成时序图(这是全仓库的主干道,后续每一篇都在放大图中的某一段):

图表(newapi-architecture-lifecycle.md)

模块之间靠什么协作

这是理解代码库的钥匙。模块几乎不互相 import 对方的内部状态,协作靠三种机制:

① gin 上下文键(context keys)——同一请求内的数据总线。 前面的中间件把计算结果写进请求上下文,后面的模块按常量键读取。例如 TokenAuth 写入 ContextKeyUsingGroup(生效分组)、token_idtoken_quotaDistribute 写入 channel_id/type/key/setting/param_override/model_mappingContextKeyRequestStartTimecontroller.Relay 与计费服务全部从这里取。新增一个中间件字段,下游自动可用——这就是模块解耦的方式。

② 服务层调用——跨模块的业务接口。 控制器不直接写库做复杂逻辑,而是调 service:service.PreConsumeBilling(计费)、service.CacheGetRandomSatisfiedChannel(选路)、service.ConvertRequest(协议转换)、service.GetUserUsableGroups(分组)。

③ 共享表与缓存——跨请求的状态。 Option 表(动态配置,SyncOptions 轮询热更新)、Ability 表(group×model×channel 的路由索引)、logs 表(消费/错误日志)、Redis hash(token/user 缓存)。

图表(newapi-architecture-lifecycle.md)

常见踩坑

  1. 在全局中间件挂 gzip——SSE 全挂(源码注释原话警告);
  2. 把控制台 token(PAT)当 sk-xxx 用——两套凭据体系(第 3 篇)互不相通;
  3. 忽略 SystemPerformanceCheck——过载保护返回 503 时,问题可能不在你的代码而在系统负载;
  4. 新增中间件后顺序放错——Distribute 必须在 TokenAuth 之后(要用它写入的分组与令牌信息)。

随堂练习(带验收标准)

  1. clone 仓库后 docker compose up -d 起一个实例,初始化后创建 token 并用 curl 调 /v1/models。验收:返回模型列表 JSON;
  2. middleware/distributor.go 里列出它写入上下文的全部 key,与 controller/relay.go 读取的 key 做配对。验收:配对完整,无”写了没人读/读了没人写”的孤儿键;
  3. 追踪一次请求的日志输出,把每行日志对应到本篇时序图的某一步。

← 返回文章列表