DeepSeek Harness 源码解析 · 第 34 讲
DeepSeek Harness 源码解析系列 第 34 讲:API 网关与 Remote 服务(api-remotes BFF 装配) 基于 DeepSeek Harness 源码 · 2026-09-17 💡 本讲一句话: 第 33 讲讲的是"网关怎么把一次 RPC 调通",这一讲讲的是"调用之前,一个 sessionId 字符串怎么安全地换回一个活 Agent"——api-remotes 包用三条策略守住这条边界:live 复用、冷恢复去重 + preset 从日志重建、subagent 会话围栏;外加一份 Host/消费端共用的事件转发白名单和一个客户端装配门面。读完你能看懂远端 API 的身份解析为什么不会把 subagent 会话抢走、也不会让重启后的会话跑在错误的工具集上。 一、这一讲解决什么问题 第 33 讲把 Typert RPC 网关的 wire 契约(RemoteResult / codec)、声明侧 @Remote、endpoint 认领和 invoke() 主流程走了一遍。但有一个问题它刻意没展开: 远端方法几乎都以 sessionId 开头,而 sessionId 只是一串字符串 ——prompt、goals/edit、models/list 全都先要回答"这个 id 现在对应哪个 Agent?"。 答案在 @deepseek-ai/dsh-api-remotes (packages/api/remotes),包描述写得很直白:"Remote BFF assembly and Host Agent/Session lookup policy"。它拥有三样东西: 🔹 身份解析策略 (agent-lookup.ts):live Agent 直接复用;冷会话按 id 去重地恢复一次;subagent 拥有的会话一律拒绝——三种失败映射成三个稳定的 wire 错误码。 🔹 事件转发白名单 (remote-events.ts):Host 端"哪些 cordis 事件可以原样推给远端消费方"的唯一事实源,同时是消费端 ctx.remote.$on 的合法键集合。 🔹 客户端装配门面 (client/index.ts):业务包在这里挂载自己选定的 Remote 命名空间,不用直接 import 任何 Host 包。 真正的接线在 packages/host/apiproxy/src/api-proxy.ts——Host BFF 把上面三样拼进自己的 RPC 服务。下面按"身份策略 → 事件白名单 → 客户端装配 → Host 接线"的顺序读源码。 二、错误分类学:三种失败,三个稳定 wire 码 身份解析的失败不是"抛个 Error 算了"——远端调用方(浏览器客户端、SDK)需要稳定的错误形状来做 UI 分支。api-remotes 把 caller-facing 失败收敛成一个判别联合: 📄 packages/api/remotes/src/agent-lookup.ts (第 10-53 行) /** Caller-facing failures preserved by the Gateway's RPC adapter. */ // 注释点明:这些失败形状会被网关的 RPC 适配器原样保留给调用方 export type ApiRemoteLookupError = // 判别联合:三种 caller-facing 失败,code 字段是判别键 | { readonly code: 'agent-busy'; readonly message: string; readonly details: { readonly reason: string } } // subagent 围栏复用 legacy agent-busy 形状——details.reason 告诉调用方"该走 subagent delivery" | { readonly code: 'session-not-found'; readonly message: string; readonly details: { readonly sessionId: SessionId } } // 冷身份在持久化存储里不存在;details 回带 se…