API gateway 是 Host 与浏览器 Client 之间类型化的远程调用桥梁。它构建于 Typert 类型生成系统之上:业务包用装饰器声明一元 RPC 方法,构建生成匹配的 Host 与 Client 契约,每一次调用都经共享的 Connection RPC /api 路由传输。权威设计文档是 docs/api-gateway.md;实现在 packages/api/gateway(gateway)与 packages/api/remotes(应用级 BFF 门面)中。
| 包 | 角色 |
|---|---|
@deepseek-ai/dsh-api-gateway | 双端 Typert RPC 端点:Host ctx.typertGateway、Client ctx.remote |
@deepseek-ai/dsh-api-remotes | 应用级 BFF:Agent/Session 身份策略、转发事件白名单、Client 挂载门面 |
@deepseek-ai/dsh-typert-protocol | @Remote / @RemoteScope 装饰器、TypertRemoteService、调用描述符 |
为什么需要 gateway
web client 运行在浏览器中;agent 循环、sandbox 与存储运行在 Node host 中。与其为每个功能手写一套 REST surface,dsh 选择生成一套:
- 业务服务用
@Remote('name')(根上下文服务)或@RemoteScope('name')(per-agent 作用域上下文服务)标记它们对外暴露的方法。未标记的方法永远到不了 client——无论生成类型还是运行时都是如此。 - Typert 生成(见 SDK:Typert)读取这些声明,发射
InvocationDescriptor以及类型化的 client stub。 - Connection 提供物理通道:同一套被其他一切使用的 HTTP + SSE 桥,带一个共享的
/apiFetchHandler。 - gateway 刻意设计为双端:Host 入口注册
TypertGatewayService(ctx.typertGateway),Client 入口提供ClientRemote(ctx.remote),两者消费同一套生成的描述符契约。
Host 侧:TypertGatewayService
ctx.typertGateway.invoke() 是唯一入口:它为每次调用解析当前的描述符与 Cordis 服务,校验精确的具名参数,解析注册的对象或 Context 身份,调用公开业务方法,并校验结果。
关键机制,来自 packages/api/gateway/src 与 docs/api-gateway.md:
- Strict 与 SRC 模式。 Strict 模式从
ctx.typert.local读取生成的调用描述符。SRC 模式是对从未有过 strict 定义的端点的开发期兜底:它解析简单参数名,并且只接受非查找参数的 JSON 安全值。撤销一条已被观察到的 strict 定义会失败,而不是弱化校验。 - 查找(Lookups)。 复杂的 Host 对象无法跨线传输。业务包通过
TypertLookupMap以及经由ctx.typert.lookups的默认解析 provider 注册其身份映射;一个名为agent的Agent参数会变成一个agentId线字段,在调用前解析回一个存活的 Host 对象。Host 装配可以通过 effect 作用域的ctx.typert.lookups.configure()覆盖策略。 @RemoteScope通过ctx.typert.contexts把一个身份解析到作用域 Context,然后从那个 Context 获取服务——用于方法依赖 per-agent 装配时。- 取消(Cancellation)。 一个 Remote 方法把
signal: AbortSignal声明为其最后的 Host 参数。它是描述符元数据,而非线参数:Connection 把 signal 交给 gateway,由它在解码完业务参数后注入。 - 错误(Errors)。 直接的
invoke()调用保留业务错误;TypertGatewayError区分由派发、绑定、provider、查找、Context、参数与编解码器所拥有的失败。resolve 器可以在TypertLookupFailure中携带一个已有的 RPC 错误以保留其原始错误码(用于如冷恢复失败之类的策略拒绝)。
Client 侧:ClientRemote
ctx.remote.$mount() 校验并注册一次生成的 Host-for-Client contribution,随后为调用方的 Cordis fiber 安装具体的 direct 与 scoped 方法。每个 namespace 是一个带追踪的 remote.<namespace> 子服务,在其最后一个方法被撤销后卸载。重复的端点、namespace 冲突以及缺少严格生成编解码器的描述符都会在方法可调用之前失败。
每次调用都校验位置参数,构造描述符精确的具名 args,并通过 ctx.connection.rpc.call('/api', endpoint, ...) 发送。生成的取消感知方法接受一个可选的最后 AbortSignal 参数,与其 contribution 挂载生命周期结合。
ctx.remote.$on() 订阅一个转发的 Host 事件;它的合法键恰是 Host 装配的转发选择(见 packages/api/remotes/src/remote-events.ts 中的 API_REMOTE_FORWARDED_EVENTS),且监听器类型是所属包自己的 Cordis Events 声明——因此任何第二个签名都不可能与之产生漂移。订阅属于调用它的 fiber,并随之消失。
dsh-api-remotes:应用 BFF
packages/api/remotes 是由本应用选定的双端门面:
- Host 入口拥有 Agent/Session 身份策略。
createApiRemoteAgentResolver()复用存活的 agent、恢复普通的冷会话、对并发的恢复去重、保留 subagent 所有权护栏,并为 Typert 的agent与session查找配置同一个 resolve-器——因此已迁移与未迁移的方法共享同一套策略实现。 - Client 入口把生成的
/remote工件作为运行时值导入,通过ctx.remote.$mount()挂载每个 contribution,并仅以类型方式重新导出声明合并。在本修订中,Client 装配挂载了 Goal Remote contribution 与只读的 plugin 清单(pluginInventory/list)。 - 这个包是本仓库唯一刻意做成"分裂面"(split-face)的包:其 Host 入口必须参与 Host Typert 图,而其 Client 入口在 Host tsdown 生成各业务包的
/remote声明之前无法编译(见packages/api/remotes/README.md)。
一个完整示例
来自 docs/api-gateway.md —— 一个暴露 goal 创建的业务服务:
import type { Agent } from '@deepseek-ai/dsh-agent'
import { TypertRemoteService, Remote, RemoteScope } from '@deepseek-ai/dsh-typert-protocol'
export interface CreateGoalRequest { objective: string }
export interface CreateGoalResult { accepted: boolean }
export class GoalService extends TypertRemoteService {
constructor(ctx: Context) { super(ctx, 'goals') }
@Remote('create')
createForClient(agent: Agent, request: CreateGoalRequest, signal: AbortSignal): CreateGoalResult {
// agent is resolved from agentId by the gateway before this runs
}
@RemoteScope('agent', 'current')
currentForClient(): CreateGoalResult {
// 当前 agent 变体;完整示例还展示了一个两者共享的私有 create() 辅助方法
}
}线路上永远不会出现 Agent;它看到的是 agentId,并由 gateway 在由 api-remotes 配置的身份策略下,将其解析为 Host 存活的 agent 注册表中的一个条目。
与 API 代理的关系
gateway 与代理(见 API 代理(apiproxy))共享 /api 路由:Connection 把复合 handler 传给它的 HTTP 桥,handler 将已被认领的端点派发给 Gateway,把未被认领的端点派发给 API 代理。因此模型的聊天流量走代理路径,而类型化的业务 RPC 走 gateway 路径。
关键源文件
| 仓库相对路径 | 提供什么 |
|---|---|
packages/api/gateway/src/index.ts | Host TypertGatewayService、ctx.typertGateway |
packages/api/gateway/src/client/ | Client 面:ClientRemote、ctx.remote |
packages/api/gateway/src/types.ts | 线类型、TypertGatewayError |
packages/api/remotes/src/remote-events.ts | 转发事件白名单 |
packages/api/remotes/src/index.ts | createApiRemoteAgentResolver、身份策略 |
docs/api-gateway.md | 设计参考(双语) |
延伸阅读
- API 代理(apiproxy) ——
/api路由的另一半 - Typert:类型生成器 —— 描述符与 client stub 如何生成
- Host 平台 —— 挂载这些服务的 webserver
- LLM 层 —— 模型流量与 provider
- 仓库文件:
docs/api-gateway.md、packages/api/gateway/README.md、packages/api/remotes/README.md