Skip to content

宿主的 /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-controllerSession 命令、历史流与实时控制状态
@deepseek-ai/dsh-api-settings-controller配置读写(settings + credentials 命名空间)
@deepseek-ai/dsh-api-workspace-controllerWorkspace 变更与可重连的状态馈送

谁拥有 /api 路由 ​

物理路由仍然是注册在 API_PATH = '/api'(packages/client/connection/src/api-path.ts)下的一个前缀。它的处理器是 Connection Node 面的 HTTP 桥(packages/client/connection/src/http-bridge.ts),把请求分派进一个共享 fetch 处理器:

  1. 先过浏览器信任围栏 + 浏览器会话鉴权——每个请求在分派前都要通过 Host/Origin 信任检查(isTrustedApiRequest,DNS 重绑定防御)和持久鉴权;被拒请求直接得到 401 / 403,不触碰任何控制器(见下文浏览器会话鉴权)。
  2. 精确 fetch 路由优先。 处理器按 pathname 选择一个已注册的 fetch 路由;没有精确路由的就回答 404(new Response('not found', { status: 404 }))——「未认领即落入 API proxy」的旧行为已不存在。注册的是生成的 Remote 命名空间(来自 Typert codec)与少数显式路由(Remote 事件流、index 鉴权)。
  3. 请求体上限为 300 MiB。 http-bridge.ts 里 DEFAULT_MAX_REQUEST_BODY_BYTES = 300 * 1024 * 1024——按默认聚合图片上限经 base64 膨胀再加信封余量定尺。超长请求在缓冲完成前以 413 拒绝。
  4. 强制媒体类型。 一元 RPC 载荷的 content-type 不是 application/json 时以 415 拒绝——因此跨站「简单」请求(浏览器不发起 CORS 预检的那种)永远无法盲执行带副作用的 method。

传输:一元 RPC + 一条流式 WebSocket ​

旧载体有按流分开的 WebSocket(/api/events.mux、/api/events.host),外加 SSE 服务端请求通道与 /api/respond POST。这些全部删除。如今恰好只有两条物理通道:

  • 一元 RPC:fetch POST 到 /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。

txt
浏览器                                  宿主
  ├─ 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.tsREMOTE_STREAM_MUX_PATH、线上帧、心跳契约
packages/api/gateway/src/stream-server.tsRemoteStreamMuxServer、Ping 保活
packages/api/gateway/src/client/ClientRemote、RemoteStream/RemoteSnapshotStream/RemoteJournalStream
packages/api/remotes/src/client/index.ts12 命名空间 Remote 组装(apply、$mount 循环)
packages/api/session-controller/src/index.tsSession 命令、follow、control
packages/api/settings-controller/src/index.ts + src/credentials.tssettings/credentials 命名空间
packages/api/workspace-controller/src/index.tsWorkspace 变更 + follow
packages/client/connection/src/http-bridge.tsDEFAULT_MAX_REQUEST_BODY_BYTES、/api 桥
packages/client/connection/src/browser-auth.tsauthenticatedUrl、cookie 铸造、authorizeIndex、401

延伸阅读 ​