ByteNoteByteNote
DeepSeek Harness API 网关架构详解
字

字节笔记本

2026年10月6日 · 约 21 分钟读完

DeepSeek Harness API 网关架构详解

API中转
¥120

DeepSeek Harness(简称 dsh)是 DeepSeek 在 GitHub 上开源的 agent 框架,采用 MIT 协议,目前处于开发者预览阶段。它的核心理念是"一切皆插件",整个运行时由 Cordis 依赖注入容器驱动。当客户端需要调用 Host 进程里的能力时,框架没有选择裸 HTTP 接口,而是设计了一套名为 Typert 的 API Gateway:构建期从 TypeScript 源码生成严格的调用约定,运行期复用同一条 Connection 完成 RPC 与 /api 路由的分发。本文基于项目官方文档,梳理这套网关的编程模型、组件职责、生成流水线与运行时调用链。

Typert API Gateway 架构总览:构建期生成、Client 装配、Connection 分发与 Host 调用链

编程模型:用装饰器声明开放面

业务服务通过 @Remote 或 @RemoteScope 两个装饰器选择对 Client 开放的方法。未标记的方法不会进入生成的 Client 类型或运行时贡献,也不能通过 ctx.remote 调用。

@Remote 表示调用根 Host Context 中注册的 Cordis 服务。复杂的 Host 对象不能直接跨网络传输,业务包必须通过 TypertLookupMap 声明它与 wire identity 的关联,并在运行时向 ctx.typert.lookups 注册默认解析提供方。以 Agent 参数为例:Host 签名中参数名为 agent,生成的 wire 字段是 agentId,Gateway 在调用业务方法前把 id 解析回 Host 对象。Host 组合还可以用 ctx.typert.lookups.configure() 覆盖某个 lookup key 的解析策略,而不改变业务包拥有的参数名、wire 字段或规范类型 symbol。

@RemoteScope(key) 则是先通过 ctx.typert.contexts 把 identity 解析为一个作用域 Context,再从该 Context 取出服务并调用方法。它适合方法本身依赖作用域组合、而不需要显式接收 Agent 等对象的情形。

服务通常继承 TypertRemoteService,让 Cordis 服务 key 与默认 Remote namespace 在构造器中显式绑定;已有其他基类的服务可以改为声明 readonly typertRemote = bindTypertRemote(this, serviceKey)。两种方式都会留下可检查的公开 binding,不依赖编译器向构造函数注入 symbol。

ts
import type { Agent } from '@deepseek-ai/dsh-agent'
import { TypertRemoteService, Remote, RemoteScope } from '@deepseek-ai/dsh-typert-protocol'
import type { Context } from '@deepseek-ai/cordis'

export class GoalService extends TypertRemoteService {
  constructor(ctx: Context) {
    super(ctx, 'goals')
  }

  @Remote('create')
  createForClient(agent: Agent, request: CreateGoalRequest, signal: AbortSignal): CreateGoalResult {
    signal.throwIfAborted()
    return this.create(agent, request)
  }

  @RemoteScope('agent', 'current')
  currentForClient(): CreateGoalResult {
    return { accepted: true }
  }
}

Remote 方法可以同步返回,也可以返回 Promise。需要协作式取消时,Host 签名的最后一个参数必须是全局类型的 signal: AbortSignal:它记录在描述符中而不进入 args,Client 生成的方法则接受最后一个可选的 AbortSignal。

Client 侧:具体函数而非 Proxy

Client 使用普通对象上的具体函数,不使用 JavaScript Proxy。直接调用与作用域调用分别出现在 ctx.remote.<namespace> 和 agentCtx.remote.<namespace>,每个 namespace 都是注册为 remote.<namespace> 的可追踪 Cordis 子服务;Client assembly 通过 ctx.remote.$mount() 挂载贡献,最后一个方法撤回后该 namespace 随即卸载。

依赖声明归实际调用方所有:只有读取 ctx.remote.<namespace> 或 agentCtx.remote.<namespace> 的业务包,才在自己的 inject 中同时声明 remote 与 remote.<namespace>;只负责挂载 contribution 的 assembly,以及不调用该 namespace 的上层运行时,都不代业务包声明 namespace 依赖。当一个 @Remote 方法恰好有一个 lookup 参数、且同名 TypertContextMap 使用相同 wire identity 时,生成的作用域签名会省略该 identity 参数;@RemoteScope 只生成作用域调用接口。

ts
export const inject = ['remote', 'remote.goals']

await ctx.remote.goals.create(agentId, { objective: 'ship it' })
await agentCtx.remote.goals.create({ objective: 'ship it' })

Client 应用只需要装配 @deepseek-ai/dsh-api-remotes 这一个包。它以运行时值导入被选业务包的 /remote 子路径,通过 ctx.remote.$mount() 挂载贡献,同时重新导出相同文件中的声明合并。增加一个 Host Remote 包是 Client 组合所有者的显式选择,业务组件不需要分别加载 Gateway 或业务包的 Remote JS。这套装配与 ctx.remote 约定不依赖 React,Client 能看到的 Host 方法也只限于生成时选中的那些 Remote 方法。

组件职责:一张表看懂八个角色

位置包或入口职责
共享@deepseek-ai/dsh-typert-protocol声明 decorator、Gateway binding、可合并协议映射、调用描述符及提供方类型;不启动 TypeScript 分析,也不注册 Cordis 服务
构建@deepseek-ai/dsh-typert-generator从 Host ts.Program 严格分析 Remote 签名、类型图、lookup、Context 与源码位置,生成 Host 和 Host-for-Client 产物
Host@deepseek-ai/dsh-typert-registry 与 Loader把生成的 Host 描述符、schema 及业务包注册项放入 ctx.typert,并持有 lookup 与 Context 提供方
Host@deepseek-ai/dsh-api-remotes负责应用的 Agent/Session 身份策略,并配置对应的 Typert lookup
Host@deepseek-ai/dsh-api-gateway提供 ctx.typertGateway,认领 Remote endpoint,解析对象或 Context,调用实时 Cordis 服务,并校验请求值和返回值
Client@deepseek-ai/dsh-api-gateway/client提供 ctx.remote 与 remote.<namespace> 子服务,把生成的描述符挂成具体方法,并通过 Connection 发起、校验和取消调用
Client@deepseek-ai/dsh-api-remotes/client显式选择并挂载本应用允许使用的 /remote 贡献,向业务代码带入对应的声明合并
双侧@deepseek-ai/dsh-client-connection提供 RPC carrier、请求关联、信任边界、取消、响应 envelope 与 /api HTTP bridge

API Gateway 包同时拥有 Host dispatcher 与 Client Remote endpoint 两个对等入口,但两侧构建不会进入同一个 ts.Program:Host 入口不导入 Client 的 Cordis Context 合并,Client 入口也不导入 Host Gateway 服务。

构建期:严格生成流水线

根构建依次执行 build:lib:host、build:lib:client 与 build:web。Host lib 阶段先运行 tsc -b tsconfig.host.json,再运行 tsdown 打包;Typert 生成器由正常的 Host Project Reference 图编译,并以 Host aggregate 为唯一的 ts.Program 种子运行,从 Host 源码严格分析 Remote 签名、类型图、lookup、Context 与源码位置。Client lib 阶段随后编译打包,使用刚生成的 Remote Client 声明和运行时贡献,但不会再次启动 Typert。两次 tsdown 都接收完整 workspace,且只打包对应 tsc 阶段发射的 JavaScript;各包的本地配置根据 DSH_BUILD_FACE 环境变量返回当前阶段的入口。

每个贡献业务包把生成文件写进自己的 lib/ 目录而不是源码目录,共五类:typert.host.js(Host 运行时反射、严格调用描述符与 schema)、typert.host.d.ts、typert.remote-client.js(可挂载的 TypertRemoteContribution,含严格描述符与运行时 codec)、typert.remote-client.d.ts(声明合并与 Client-safe 类型)以及 typert.remote-client.d.ts.map。最后这份声明 map 把 ctx.remote.goals.create 最终解析到的生成属性,映射回带 @Remote 的 Host 源方法,支持 declaration map 的编辑器可以从 Client 调用直接跳到真实实现,而不是停在生成的 .d.ts 上。

严格分析要求 Remote 是公开、非静态、有具体实现的实例方法,且不能是泛型;参数必须是具名且必填的简单标识符,不允许解构、默认值、rest 或可选参数。可 JSON 表示的普通类型由 Typert 生成严格 schema;工作区 class 等复杂对象必须具有唯一的 TypertLookupMap 声明。lookup 与 Context 包要同时完成静态声明合并和运行时提供方注册,缺少任何一侧都会导致构建失败,或者首次调用需要该提供方时失败。

运行时:一条 Connection,两层校验

一次 Remote 调用的运行时路径:信任检查、分发、认领、校验、解析与返回

Remote 与既有 API Proxy 共用 Connection 的 /api 路由。Client Remote 调用 connection.rpc.call('/api', '<namespace>/<method>', { args }, signal),HTTP 侧对应 POST /api/<namespace>/<method>,payload 只包含一个具名 args 对象。

Connection 在 HTTP bridge 之前执行 /api 的统一信任检查,再在共享 FetchHandler 内按 interceptor 顺序分发。Typert Gateway 只认领存在严格描述符或活跃 SRC marker 的两段式 endpoint,未认领的请求回退到既有 API Proxy。传输、RPC id、响应 envelope 和请求取消归 Connection 所有,Gateway 只拥有 Remote 数据协议和业务分发,因此未来替换传输层不需要改变 Remote 描述符或 Client 编程接口。

Gateway 每次调用都从当前注册表解析描述符和实时服务,不缓存业务对象。它要求 args 的字段集合与描述符完全一致,先用 codec 校验 wire 值,再通过注册的 lookup 或 Context 提供方解析对象或接收者,最后调用 binding 指向的服务方法并校验返回值。参数缺失或多余、schema 失败、identity 未命中、binding 不一致、方法不存在,都会在进入业务代码之前或离开业务代码之后立刻失败。

lookup 提供方的 register() 同时提供稳定声明和默认 resolver;configure() 提供由 Host 组合拥有、可异步执行且受 effect 生命周期约束的 resolver。配置可以先于提供方挂载:没有提供方时调用以 lookup-unavailable 失败,配置卸载后恢复提供方默认策略。API Remotes 负责 agent 与 session 的标准 agentFor() 语义:复用 live Agent,自动恢复普通冷会话,对并发恢复去重,并拒绝由 subagent routing 拥有的 identity。恢复失败和 ownership fence 通过既有 RPC error 原样返回,不会被折叠成 Gateway 的 internal 错误。

卸载行为同样严格:Client 撤回一个贡献时,会一起移除描述符和具体方法,中止其进行中的调用,并让外部仍持有的陈旧方法句柄拒绝继续调用;Host 上已注册的严格 endpoint 被撤回后也不会降级到 SRC 推断,避免热卸载悄悄降低校验强度。

开发模式与 SRC 回退

Host 通过 node --import tsx/esm 从源码启动时,不会执行 Typert 编译插件。此时标准 decorator 初始化器仍会把方法名和调用模式记录到模块私有 WeakMap,TypertRemoteService 或 bindTypertRemote() 提供的显式服务 binding 也还在,Gateway 因而可以在不启动 ts.Program 的情况下构造一个较弱的临时描述符,这就是 SRC 回退。SRC 从运行中的函数解析简单参数名:参数名与某个已注册 lookup 的 parameter 相同(例如 agent 或 session),就使用其 agentId 或 sessionId wire 字段并在 Host 解析对象;其他参数只检查值是否为无循环、无特殊 prototype 的 JSON-safe 数据。SRC 不读取 TypeScript 类型,不生成 schema,也不支持解构、默认值、rest 或重复参数名。

SRC 只解决 Host 源码进程的分发问题。Client 不会从运行中的 Host 发现 decorator,Client Remote 也拒绝挂载缺少严格 codec 的 SRC 描述符,其类型、codec 和 Remote 注册值始终来自最近一次生成的 lib/typert.remote-client.*。

日常开发的推荐流程是先用 pnpm run build 准备当前 Host、Client 与 Web 产物,然后在两个终端分别运行源码 Host 与 Client 插件 watcher:

sh
pnpm dsh web
pnpm run dev:web

dsh 通过 tsx 启动 Host 源码,所以 Host 可以使用 SRC 回退;dev:web 只监听带 dsh.client 声明的 Client 插件并重写其 lib/client.js,它不会分析 Host decorator,也不会生成 Remote Client DTS。只修改 Remote 方法实现体而不改变约定时,无需重新生成 Typert 文件;一旦新增或删除 decorator、修改导出名、namespace、参数、返回值、lookup、Context 或取消签名,就要重新执行有序 lib 构建,让 Host 先生成严格约定,Client 再编译并打包新的贡献:

sh
pnpm run build:lib

运行中的 Client watcher 会在重新打包时消费这些生成文件。若已单独运行 pnpm run build:lib:host 刷新 Host 约定,也可以再运行 pnpm run build:lib:client 完成 Client 侧,但干净工作树不能跳过 Host 阶段。仅重新编译前端源码不能从 Host decorator 推导新类型;pnpm run typecheck 会先执行 Host lib 阶段再运行 Client tsc,CI 与发布构建也使用同一顺序。

边界:Remote 不做什么

Remote 只处理有单个请求与单个结果的一元方法调用。会话事件流、分页、增量 reduce、projection 和实体子流需要独立的数据协议与注册模型;即使它们复用 Connection,也不应伪装成 Remote 方法或塞进调用描述符。API 各层按 remotes → gateway → connection → webserver 组织,没有 Remote 描述符的 endpoint 由独立的 API Proxy 处理。

lookup 策略按 key 配置,因此所有 agent 或 session 参数共享冷恢复行为。"只接受 live 对象"这类逐参数或逐 endpoint 的策略并不存在,业务方法内部也无法猜测对象是否来自恢复。把约定前置到构建期、把校验前置到业务代码之外,正是这套网关与常见"运行时反射 RPC"最大的区别,也是它敢把客户端调用直接暴露成类型安全方法签名的底气。

相关文章

分享: