宿主的 /api 表面曾经是一个包:packages/host/apiproxy(ctx.apiProxy)——带 SSE 服务端请求通道与 /api/respond 应答路由的四象限 RPC 链路。该包已被删除(commit 4f00a8b82a);共享路由如今由 packages/api/* 家族接管。本页梳理替代方案:今天谁拥有 /api 路由、一元 RPC 与实时流如何传输、浏览器又如何对它鉴权。
| 包 | 角色 |
|---|---|
@deepseek-ai/dsh-api-gateway | 双面 Typert RPC 端点:宿主 ctx.typertGateway、客户端 ctx.remote;拥有 remote-stream WebSocket 多路复用 |
@deepseek-ai/dsh-api-remotes | 应用级 BFF:本客户端组装要挂载哪些宿主 Remote 贡献 |
@deepseek-ai/dsh-api-session-controller | Session 命令、历史流与实时控制状态 |
@deepseek-ai/dsh-api-settings-controller | 配置读写(settings + credentials 命名空间) |
@deepseek-ai/dsh-api-workspace-controller | Workspace 变更与可重连的状态馈送 |
谁拥有 /api 路由
物理路由仍然是注册在 API_PATH = '/api'(packages/client/connection/src/api-path.ts)下的一个前缀。它的处理器是 Connection Node 面的 HTTP 桥(packages/client/connection/src/http-bridge.ts),把请求分派进一个共享 fetch 处理器:
- 先过浏览器信任围栏 + 浏览器会话鉴权——每个请求在分派前都要通过 Host/Origin 信任检查(
isTrustedApiRequest,DNS 重绑定防御)和持久鉴权;被拒请求直接得到401/403,不触碰任何控制器(见下文浏览器会话鉴权)。 - 精确 fetch 路由优先。 处理器按 pathname 选择一个已注册的 fetch 路由;没有精确路由的就回答 404(
new Response('not found', { status: 404 }))——「未认领即落入 API proxy」的旧行为已不存在。注册的是生成的 Remote 命名空间(来自 Typert codec)与少数显式路由(Remote 事件流、index 鉴权)。 - 请求体上限为 300 MiB。
http-bridge.ts里DEFAULT_MAX_REQUEST_BODY_BYTES = 300 * 1024 * 1024——按默认聚合图片上限经 base64 膨胀再加信封余量定尺。超长请求在缓冲完成前以413拒绝。 - 强制媒体类型。 一元 RPC 载荷的
content-type不是application/json时以415拒绝——因此跨站「简单」请求(浏览器不发起 CORS 预检的那种)永远无法盲执行带副作用的 method。
传输:一元 RPC + 一条流式 WebSocket
旧载体有按流分开的 WebSocket(/api/events.mux、/api/events.host),外加 SSE 服务端请求通道与 /api/respond POST。这些全部删除。如今恰好只有两条物理通道:
- 一元 RPC:
fetchPOST 到/api/<endpoint>,携带类型化信封ClientRequest({ type: 'client-request', rpcId, method, payload }),由ServerResponse({ type: 'server-response', rpcId, result })应答——schema 对见前端:客户端运行时与链路。 - 实时 Remote 流:一条共享多路复用 WebSocket,路径
/api/remote.mux(REMOTE_STREAM_MUX_PATH = '/api/remote.mux',packages/api/gateway/src/stream-protocol.ts)。网关拥有 upgrade 路由;一个共享 socket 在彼此独立可取消的 Remote 流间多路复用,服务端定期发送 WebSocket Ping 心跳帧(DEFAULT_WEBSOCKET_HEARTBEAT_INTERVAL_MS = 2_000)并用 pong 跟踪检测死连接。浏览器侧:RemoteStreamMuxClient(packages/api/gateway/src/client/stream-client.ts),打开ws(s)://<origin>/api/remote.mux并把帧扇出给各流的读者。
流式端点用 @Remote({ mode: 'stream' }) 声明(Typert 协议;其他 options 形态都会被拒):生成的客户端面给每个流一个流载体对象——RemoteStream<Item>(普通增量流)、RemoteSnapshotStream<Snapshot, Delta>(完整基线后跟替换帧)、RemoteJournalStream(分页/条目/游标的日志读取)——全部共用同一条物理 socket。
浏览器 宿主
├─ fetch POST /api/session.prompt ──────► /api 前缀路由(信任 + 鉴权,300 MiB 上限)
├─ fetch POST /api/settings.mutate ─────► 精确 fetch 路由 → Typert 网关分派
└─ WS /api/remote.mux ◄────────────────── 网关流多路复用(Ping 保活)
├─ session/control (基线 + 替换帧)
├─ session/follow (历史日志)
└─ workspace/follow (基线 + 增量)session-controller:Sessions
packages/api/session-controller(命名空间 ctx.remote.session)拥有 Session 面向的业务 API:
- 命令(
src/commands.ts):create(幂等收养)、rename、fork、prompt(恢复后接收一条 prompt)、attachment(读取日志可达的图片)、updateQueue、cancel、selectModel,外加openWorkspacePath/canOpenWorkspacePath。 - 历史与列表:
list/search(冷读取、不恢复),以及两条流——page(冷安全、按消息对齐的分页)与follow(@Remote({ mode: 'stream' }):完整开局快照后跟无缝事件帧)。 - 实时控制状态:
control(@Remote({ mode: 'stream' }))先流完整 live-control 基线再流替换帧——composer、队列与回合指示器渲染的正是它。 - 同处还挂载:
modelCatalog(按 provider 分组的可路由模型 + 部署默认)、技能目录(skill-catalog.ts)、文件引用解析(file-references.ts)与 Agent 检查(agent.ts)。
settings-controller:配置 + 凭据
packages/api/settings-controller 把配置平面做成两个命名空间:
settings(src/index.ts):describe()(每个命名空间都在redactSecrets: true下脱敏——返回{ writable, hasDocument, namespaces })、update(ns, patch, expectedRevision)、replace(ns, section, expectedRevision)、mutate(ns, pathOps, expectedRevision),外加openSettingsDocument/openAgentPresetDirectory/canOpenAgentPresetDirectory。写拒绝归类为settings/conflict(陈旧 revision)或settings/rejected。credentials(src/credentials.ts):describe(refs)(批量,≤ 64)、set(ref, value)、unset(ref)——秘密值只单向过线,任何读路径都回显不出它。
这是设置模型层背后的 Remote 传输——缝的浏览器侧见设置 Schema 与表单。
workspace-controller:Workspaces
packages/api/workspace-controller(命名空间 ctx.remote.workspace)拥有 Workspace 变更:create、rename、delete、insertBefore、insertSessionBefore、archiveSession,以及 follow 流(@Remote({ mode: 'stream' }):完整基线后跟有序增量)。它还挂载目录选择命名空间(directory-picker.ts)——把 browse/native 选择器缝暴露到 Remote 上。
gateway + remotes:分派与组装
两个 packages/api 邻居补全了这一层:
dsh-api-gateway是 Typert 分派引擎:宿主ctx.typertGateway.invoke()每次调用都解析描述符与 Cordis 服务、校验精确命名参数、解析 Agent/Session 查找、注入取消、并校验结果;客户端面ClientRemote(ctx.remote)经$mount()挂载贡献、经$on()订阅转发事件。流式端点由网关的RemoteStreamMuxServer在 mux WebSocket 上伺服。详见 API 网关。dsh-api-remotes是应用级 BFF 组装:宿主入口拥有 Agent/Session 身份策略(createApiRemoteAgentResolver);客户端入口选择并挂载生成的 Remote 贡献。在本版本,客户端组装挂载 12 个命名空间——agentPresets、commands、settingsController、goals、llm、dynamic、pluginInventory、messageFeedback、sessionReferences、subagents、session、workspace——在一个ctx.remote.$mount()循环里完成(packages/api/remotes/src/client/index.ts:146-150)。
浏览器会话鉴权
HEAD 在 /api 平面加入了完整的浏览器会话鉴权(packages/client/connection/src/browser-auth.ts),取代旧的「无凭据」立场,改为签名令牌交换:
- 进程启动时宿主铸造一个按进程的 HMAC 启动令牌(32 随机字节),并打印已鉴权 URL——
authenticatedUrl(baseUrl)在根路径上追加?token=<launchToken>。监督器与dsh web就绪行使用的都是这个 URL。 - 打开该 URL 的浏览器一次性交换令牌:
GET /携带恰好一个有效?token=时铸造dsh-auth-<authority>cookie(HMAC-SHA256 签名载荷{ version, authority, issuedAt, expiresAt },HttpOnly; SameSite=Strict),并303跳到干净的/。Cookie 绑定权威,cookieMaxAgeDays(默认 30)后过期。 - 其他没有有效 cookie 的请求一律得到相同的最小 401 正文(「请重新打开 dsh web 打印的 URL」)。持有效 cookie 的请求通过
isAuthenticated()后进入/api前缀。 - 信任围栏依旧叠加其上:
isTrustedApiRequest/assertTrustedAuthority拒绝任何既非环回、又不在trustedHosts配置权威列表中的Host——旧的PRIVILEGED_METHODS环回钉死清单已删除(见前端:客户端运行时与链路)。
模型流量从哪里进入
模型流量不再经过「API proxy 中继」。客户端经 llm Remote(llm.discoverModels、llm.providers)获取模型供选择器使用;真正的 prompt 经 session.prompt 进入 agent——session-controller 的 prompt 路径恢复 Session 并接收 prompt,此后 agent 循环在宿主侧调用 ctx.llm.stream。/api 上不存在客户端到 provider 的请求中继。
关键源文件
| 仓库相对路径 | 提供什么 |
|---|---|
packages/api/gateway/src/stream-protocol.ts | REMOTE_STREAM_MUX_PATH、线上帧、心跳契约 |
packages/api/gateway/src/stream-server.ts | RemoteStreamMuxServer、Ping 保活 |
packages/api/gateway/src/client/ | ClientRemote、RemoteStream/RemoteSnapshotStream/RemoteJournalStream |
packages/api/remotes/src/client/index.ts | 12 命名空间 Remote 组装(apply、$mount 循环) |
packages/api/session-controller/src/index.ts | Session 命令、follow、control |
packages/api/settings-controller/src/index.ts + src/credentials.ts | settings/credentials 命名空间 |
packages/api/workspace-controller/src/index.ts | Workspace 变更 + follow |
packages/client/connection/src/http-bridge.ts | DEFAULT_MAX_REQUEST_BODY_BYTES、/api 桥 |
packages/client/connection/src/browser-auth.ts | authenticatedUrl、cookie 铸造、authorizeIndex、401 |
延伸阅读
- API 网关 — Typert 网关与
ClientRemote的深入剖析 - 宿主平台 — 挂载
/api与 mux 路由的 webserver - 前端:客户端运行时与链路 — 浏览器信封、信任围栏与生命周期
- 前端:设置 Schema 与表单 — settings-controller 的消费方
- 仓库:
packages/api/*/README.md、packages/client/connection/src/browser-auth.ts