公开路径表
HybridInference 包含两个常驻的 HTTP 服务:FastAPI 网关(apps/backend)和 Next.js 控制台(apps/frontend)。只有其中一个需要暴露到公网。
公开路径表归控制台管。公网流量就终止在控制台这个进程上。静态的网关转发写在 apps/frontend/next.config.js 的 rewrites() 配置里;目标地址在镜像构建后还需要能改的代理,则写成 App Router 的 route handler。两种机制都没列出的路径,由控制台自己的页面来响应。
所以,暴露控制台的端口,就等于暴露了下表中的每一条网关路径;而公开一条新路径,意味着要改 next.config.js 或新增一个 route handler,而不是去改前面的反向代理或隧道。两边都没有的路径会得到控制台的 HTML 404 页面——在 API 客户端看来,这更像「网关挂了」,而不是「这个路径没有被转发」。
client ──▶ (your edge: CDN / tunnel / reverse proxy)
│
▼
Next.js console ──┬──▶ FastAPI gateway (build-time rewrites)
├──▶ cloud agent (runtime route handler)
├──▶ pgAdmin (runtime route handler, admin-gated)
└──▶ its own pages (everything else)
路径表
下表把静态 rewrite 和文件系统里的 route handler 放在一起列出。rewrite 以 apps/frontend/next.config.js 为准;运行时 handler 以对应的 route.ts 文件为准。
目标地址的解析时机是有意分开的。Next 在构建时解析 rewrites(),并写进 .next/routes-manifest.json;route handler 则在请求到来时读取只在服务端可见的环境变量,所以改这些目标只需重新创建容器,不用重新构建镜像。
变量 |
默认值 |
解析时机 |
指向 |
|---|---|---|---|
|
|
构建时(用于 rewrite 和服务端的后端调用) |
FastAPI 网关 |
|
(未设置) |
运行时 |
独立部署的 cloud agent 网页应用 |
|
(未设置) |
运行时 |
那个 agent 的控制面 API |
|
|
运行时 |
pgAdmin |
BACKEND_INTERNAL_URL 在构建控制台镜像时就固定了,而不是在容器启动时才确定。同一个值会同时编译进 rewrite manifest 和只在服务端可见的 BUILT_BACKEND_INTERNAL_URL(/site-config 和 pgAdmin 的管理员校验用的就是它),所以不会出现运行时覆盖了这个值、结果只有一部分后端请求悄悄发到新地址的情况。发布的镜像用的是 http://backend:8080,因此运行这个镜像的部署必须让后端使用这个网络名。换一个后端地址,就要重新构建控制台。
运行时 route handler——cloud agent 代理
apps/frontend/src/app/agents/[[...path]]/route.ts 每次请求都会重新读取两个 agent 目标。只要有一个变量没设置,/agents 就如实返回 404;目标地址无效或配置的服务连不上时,返回 502。
如果 agent 用自己的域名提供服务,把 AGENT_PUBLIC_URL 设成它的公开 HTTPS URL(不要带凭据)。仪表盘上的 Agents 卡片在运行时使用这个 URL,与代理目标无关;只要有一个代理目标没设置,/agents 依然是关闭的。没有配置公开 URL 时,只有两个代理目标都配置好了,卡片才会链接到 /agents。这张卡片仍然只对 internal 用户显示。和私有的代理目标不同,公开 URL 会出现在浏览器拿到的站点配置里。
源路径 |
目标 |
说明 |
|---|---|---|
|
|
前缀被去掉——控制面在自己的根路径上提供这些路由 |
|
|
前缀被保留——那个应用是以 |
|
|
只有前缀本身 |
这里有三个细节要注意:
/agents/api/:path*会先于/agents/:path*匹配。前者是后者的前缀,顺序反过来的话,每个 API 调用拿到的都会是网页应用的 HTML。这个 handler 会流式转发请求体和响应体,包括 SSE,但不支持 WebSocket 升级。以后如果要加 WebSocket 端点,需要在 Next.js 前面放一个支持协议升级的代理。
逐跳(hop-by-hop)头会去掉,cookie 原样保留;如果重定向指向任一 agent 内部服务,会改写回对应的公开前缀下。
旧版 beforeFiles 兼容
如果在构建镜像时同时提供了两个 agent URL(一些较旧的发行版流水线就是这么做的),next.config.js 仍会把同样的三条规则生成为 beforeFiles rewrite。本仓库的 Compose 不传这两个值,它的镜像用的是上面的运行时 handler。由于 beforeFiles 的优先级高于文件系统路由,用这些值构建出的镜像会保留构建时写死的目标地址,只改容器的环境变量无法让它指向新目标。
afterFiles——网关
下面每一条的目标都是 ${BACKEND_INTERNAL_URL} 加上同样的路径。
源路径 |
提供什么 |
|---|---|
|
OpenAI 兼容的 API 接口 |
|
Anthropic Messages 接口 |
|
认证路由 |
|
用户仪表盘 API |
|
管理员 API |
|
通过 cookie 会话校验管理员身份(pgAdmin handler 会调用) |
|
仅限管理员的模型 playground,由控制台仪表盘调用 |
|
读取模型目录 |
|
读取单个用户的状态 |
|
某个用户能否使用 Cloud Agent,由独立部署的 agent 读取(见后端扩展) |
|
签发一个推理 grant( |
|
单个 grant 的续期 / 用量查询 / 吊销 |
|
健康检查 |
|
公开的站点横幅 |
|
对外公开的部署身份信息,供控制台的 |
关于 /internal:这些条目是有意逐条列出的,而不是用一条 /internal/:path* 整体转发。这个前缀是多方共用的(/internal/verify-admin 就靠 cookie 认证浏览器会话),一条整体规则会把以后新加到 /internal 下的路由直接暴露出去,而没有人专门决定过它该不该能从外部访问。/internal/agent-grants 是唯一放开整个子前缀的例外:那个 router 在 router 层给每条路由都加了 dispatch token 依赖,从结构上就保证了都要鉴权。
为什么 /pgadmin 是 route handler 而不是 rewrite
本节写给需要自己写 route handler 的贡献者;pgAdmin 代理就是现成的示例。
pgAdmin 只能让管理员访问。rewrite 做不了鉴权——它只是一条静态映射,在你的任何代码运行之前就已经求值,既不能向外发请求,也不能检查会话或拒绝请求。所以 /pgadmin 根本不在上面的表里,而是一条应用路由 apps/frontend/src/app/pgadmin/[[...path]]/route.ts。它属于文件系统路由,因此优先于 afterFiles 里的 rewrite。这个 handler 先检查调用方,再自己把请求代理给 pgAdmin。
这个 handler 是个短小的例子,演示了怎样在 Next.js route handler 里实现带鉴权的反向代理,它的每个设计决定都能推广到别处:
失败即拒绝(fail closed)。
verifyAdmin()带着调用方的 cookie、以 5 秒超时调用网关上的GET /internal/verify-admin。只有明确返回200才放行。后端挂了、响应慢,或者返回了意料之外的内容,一律拒绝。它假定自己是数据库控制台前面唯一的一道关,因为它没法判断 pgAdmin 自己有没有登录(这取决于PGADMIN_CONFIG_SERVER_MODE,默认是False)。转发前去掉控制台自己的会话 cookie。pgAdmin 用不到它;把会话凭据转发给被代理的应用,凭据就是这样泄漏的。
丢掉逐跳(hop-by-hop)头(RFC 9110 §7.6.1),外加
host和content-length——这两个头fetch会根据要发出的请求自己算出来。返回时要丢掉content-encoding,因为fetch已经把 body 解压过了。用getSetCookie()重新拆分Set-Cookie——Headers.forEach会把重复的值合并成一个字符串。把上游返回的绝对地址重定向改回纯路径。如果请求缺少某条路由要求的尾斜杠,Werkzeug 会用它看到的
Host拼出一个绝对地址的重定向——在这里就是内部容器名,浏览器根本解析不了。foldUpstreamRedirect()会把这类地址改写成路径;它按 hostname 而不是 origin 匹配,所以不管 URL 里带的是什么端口都能处理。重定向用纯路径,不用绝对 URL。在隧道后面,应用看到的
Host是它自己的绑定地址,于是new URL('/login', request.nextUrl)会变成https://0.0.0.0:3001/login,浏览器根本打不开。NextResponse.redirect()只接受绝对 URL,所以这个 handler 自己手写Location头。
尾斜杠重定向的冲突
next.config.js 里全局设置了 skipTrailingSlashRedirect: true,因为被代理的应用应该自己决定路径的含义。Next 默认会通过重定向去掉尾斜杠,而 pgAdmin(Flask)或 agent 网页应用可能又把它加回来。如果保留 Next 的默认行为,浏览器就可能在这两层之间来回跳转,永远停不下来。被代理的路径必须原封不动地按浏览器请求的样子送到上游。
但如果在所有地方都跳过这个重定向,站点上其他所有 URL 的行为都会变,所以 apps/frontend/src/middleware.ts 为 /pgadmin 和 /agents 前缀以外的所有路径重新实现了这个重定向:用 308 跳到去掉尾斜杠的路径。目标 URL 是用 new URL(request.url) 而不是 nextUrl.clone() 构造的——克隆出来的 NextURL 会记住请求带来的尾斜杠,并在序列化时加回去,结果把请求重定向到它原本所在的地址。
新增一条公开路径
选择路由机制。在
apps/frontend/next.config.js的rewrites()中添加静态规则;如果目标必须在构建后仍可配置,或请求需要应用逻辑,就使用route.tshandler。路径要写具体。优先用
/prefix/thing,而不是/prefix/:path*,除非这个前缀下现有和将来的每条路由都从结构上保证了鉴权。如果这条路径需要一次目标服务自己做不了的检查,那它就该是 route handler,而不是 rewrite。
改了源码或 rewrite 表,就要重新构建控制台。新镜像部署之后,运行时 handler 的目标只需重新创建容器就能修改;静态 rewrite 的目标则仍然要重新构建镜像。
把这条路径加进上面的表格。