DeepSeek Harness WebSocket 下行承载设计:用双独立 WebSocket 解除浏览器六连接瓶颈

  • 人工智能
  • AI Agent
  • Agent 框架
  • DeepSeek

【免费下载链接】deepseek-harness

DeepSeek Harness: Everything is a Plugin.

项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness
点击查看 免费下载

本文基于仓库归档架构决策笔记 2026-08-04-websocket-downlink-carrier.md 展开。DeepSeek Harness 的浏览器 Web GUI 长期依赖两条 SSE 响应流(events.mux 与 events.host)承载 Host → 浏览器的下行事件。在 HTTP/1.1 下,每条页面连接都会永久占用两个稀缺的连接槽位,导致同源标签页、插件资源与普通 RPC 互相争抢。本文完整梳理了用"每条下行流类别一条独立 WebSocket"替代双 SSE 的决策背景、协议边界、升级与生命周期管理、验证体系、备选方案与代价,并结合仓库中 dsh-host-webserver、dsh-client-connection、dsh-api-gateway 的源码与测试做纵深印证。

背景问题:两条 SSE 把页面钉死在 HTTP/1.1 的连接配额上

HTTP/1.1 协议规范建议浏览器对同一 origin 最多保持六个并发连接(主流浏览器均采用约 6 条的上限)。DeepSeek Harness 的浏览器 Web GUI 长期为 events.mux 和 events.host 各维持一条 SSE 响应,一个页面就"永久"占用两条连接。由此带来的连锁效应是:

  • 同一 origin 下打开多个标签页时,每个页面各占两条,连接配额迅速耗尽;
  • 插件资源(bundle、静态资源)、以及普通的一元 RPC 请求会与这些长连接争抢剩余槽位;
  • 一旦达到连接上限,新的请求不是变慢,而是排队等待,表现为整页卡顿而非单纯的延迟上升。

决策笔记同时点明了重构的底层约束:RPC 协议本身与通道无关(channel-independent)。会话/运行时对象层不应感知到浏览器物理载体的限制——"一个浏览器物理载体的约束,不应泄漏进 session/runtime 对象层"。这正是把连接槽位问题限制在"物理载体层"解决的根本原因,也是后续一切设计的出发点。

核心决策:每条下行流类别一条独立 WebSocket

决策笔记记录的实施结果是:真正的浏览器物理载体为每个下行流类别各打开一条独立 WebSocket:

  • /api/events.mux —— 只发送 MuxFrame;
  • /api/events.host —— 只发送 HostFrame。

两条流拥有独立的生命周期,不提供任何跨流顺序保证;其中任何一条结束,都会使整个连接代(connection generation)失败,并在既有退避策略下整体重建。

消息格式:一消息一 JSON 文档,信封先行

每条 WebSocket 文本消息就是一整个完整的 ServerRequest JSON 文档。客户端的解码路径是分层的:

  1. 先校验 ServerRequest 信封(envelope);
  2. 再针对该路径校验具体的 frame 联合类型(MuxFrame / HostFrame union);
  3. 校验通过后,把窄化后的 RpcRequest<Frame> 形式交给既有的 ConnectionController 处理。

这种"先信封、后帧联合"的两段式校验,让物理载体只关心"传输什么帧",而把业务语义完全交给上层。当前仓库 packages/client/connection/src/client/index.ts 仍然 re-export 着 RpcRequest、RpcResponse、RpcResult、RpcError、ClientRequest、ServerResponse、RpcMessage 等线协议类型,印证了这一协议族在 connection 包中的核心地位;fixture.client.spec.ts 中也可以看到 RpcRequest<FixtureSessionRequests[K]> 作为统一请求载体被用于测试装配。

方向性边界:WebSocket 只下行

WebSocket 只承载 Host → 浏览器 的单向下行。所有 Client → Host 的一元调用 以及服务端请求的 respond 回写,继续走既有的 POST /api/* HTTP 通道;WebSocket 不接受任何客户端应用消息。因此决策笔记中的 WebApiClient 呈现为"双载具"形态:

  • 上行:持有 HTTP fetch;
  • 下行:持有 WebSocket。

而测试 fixture 与 InProcessApiClient(toFetchHandler(api)) 继续实现同一条双流 IApiClient 抽象。换句话说,上下行通道的物理实现可以替换,但应用层看到的始终是同一套 API 接口。

这一点在 packages/client/connection/src/rpc.ts 的 ClientConnectionRpc 上依然清晰可见:call() 是通用的一元调用入口(channel 为 /api,endpoint 如 goals/create),而 open() 方法注释明确写着"Browser transports omit this method; API Gateway owns their WebSocket mux"——浏览器传输不提供进程内流打开,WebSocket 复用在 API Gateway 一侧。rpc.ts 第 198-207 行的 requestRejection() 则把 Host/Origin 检查与浏览器认证统一暴露给"HTTP 或 upgrade 请求"共用,正是决策笔记所说"升级前应用与一元 HTTP 相同的信任检查"的代码落点。

进程内载体保留 SSE:验证协议的同构性

为了证明"协议与物理通道无关",进程内 fetch 载体保留 SSE 编码与解码,用于验证通道无关协议的同构性(isomorphism):同样的帧序列,无论经过 SSE 还是 WebSocket 物理编解码,应用层拿到的是同一组逻辑流。与此同时,网络层 GET /api/events.* 只返回 Upgrade Required(HTTP 426 语义),不提供浏览器兼容回退——刻意避免出现"两套载体并存导致生产路径分叉"的局面。

升级与生命周期边界:webserver 与 connection 各司其职

决策笔记强调了一个清晰的职责切分:

dsh-host-webserver:只管升级路由与裸 socket

dsh-host-webserver 提供的是一条精确的升级路由注册点(upgrade-route registration):

  • 与普通 HTTP 路由并列注册;
  • 仅按 pathname 精确调度 Node 的 upgrade socket;
  • 吞掉裸 socket 上的错误;
  • 服务器拆除(teardown)时,等待所有仍存活的升级连接关闭后才返回。

它完全不了解 Harness 帧或 WebSocket 消息——升级路由层与协议层被彻底解耦。这一设计在 packages/host/webserver/README.md 中有明确记载:registerUpgrade(route) 为精确 pathname 添加升级路由,升级表内精确匹配、未匹配的连接直接关闭;升级处理函数抛异常或升级 socket 传输错误时记录 warning 并销毁对应 socket,而不会拖垮进程。README 还特别指出,Node 的 closeAllConnections() 并不包含升级后的 socket,因此服务需要显式跟踪每条升级连接以保证拆除完整。

对应的契约测试在 packages/host/webserver/tests/webserver.spec.ts:用原始 TCP socket 发起 Connection: Upgrade / Upgrade: dsh-test 握手,验证升级路由按精确 pathname 匹配、重复注册被拒绝(/duplicate upgrade route/)、dispose 后可以重新注册、以及 teardown 时对升级连接的所有权。

dsh-client-connection:拥有握手、帧输出与流取消

WebSocket 的握手、帧输出和流取消全部归 dsh-client-connection 所有。升级发生之前,必须先通过信任与认证关卡:

  1. 应用 /api 的 Host / Origin 检查;
  2. 再应用与一元 HTTP 相同的签名浏览器 cookie 认证。

失败的分级语义在 packages/client/connection/README.md 中有精确记录,也与决策笔记一致:

  • 不受信任的 authority 或跨源 Origin → 403(例如 DNS rebinding、跨站请求);
  • 受信任但未认证 → 401;
  • 两种情况都不会启动 Remote 流。

换言之,信任检查(身份不明先拒绝)在认证之前执行,且前者"绝不建立身份"。

流的取消与失败传播

  • 浏览器 abort 或 socket 关闭 → 取消对应的 host 流;插件拆除(teardown)也会等待该 source iterator 的清理完成;
  • host 流中途抛出异常 → carrier 先发送一条既有的 stream/error 帧,然后关闭 socket;客户端把该帧视为连接丢失处理,而不是投递给业务 sink;
  • 每条 WebSocket 独立上报 open 状态;
  • 既有的 readiness 握手保持不变:必须等到 mux 与 host 两条 WebSocket 都打开、且 host.describe HTTP 调用成功之后,才发布 connected。

验证体系:从契约测试到端到端浏览器回放

决策笔记记录了四层验证手段,且每层在仓库中都有对应物:

验证层覆盖内容仓库对应
webserver 契约测试upgrade-pathname 调度、重复注册拒绝、disposal、teardownpackages/host/webserver/tests/webserver.spec.ts
connection 真实网络测试每条 WebSocket 的信任检查、open、schema 信封、帧顺序、流错误、关闭取消packages/client/connection/tests/node-half.host.spec.ts(以 fake webserver 捕获 upgrade 路由注册与 pathname)
client 测试下行创建 ws:/wss: URL,而一元调用与 respond 仍走 HTTP fetchpackages/client/connection/src/client/index.ts(createWebConnectionRpc 与 createFixtureConnectionRpc 双实现)
组装式无密钥浏览器回放Chromium + 真实 host + HTTP 上行 + 完整 WebSocket 下行链仓库 snapshots 与端到端测试套件

备选方案:四个被否决的路径及其理由

决策笔记记录了四个被认真考虑后否决的替代方案,每一条的取舍都值得回味:

  1. 在一条 WebSocket 内多路复用 mux 与 host:需要引入通道标签、多路复用队列和单连接背压策略,还会改变既有"双流 readiness"语义。而两条 WebSocket 已经能绕开 HTTP/1.1 六连接限制,同时把改动留在物理载体层。
  2. 把一元调用与 respond 也搬进全双工 WebSocket:这会重写超时、取消、HTTP 状态、信任围栏与请求关联(correlation)等一整套行为,但对"下行连接槽位"这个当前问题毫无增益。HTTP 上行是明确保留的边界。
  3. 保留网络 SSE 回退:双载体会让生产浏览器路径因代理或握手差异而静默分叉,并让连接数限制问题留在受支持分支里。预发布阶段只交付 WebSocket 下行,让既有重连行为与连接状态显式暴露失败。
  4. 依赖 HTTP/2 提升连接并发:内置开发服务器使用明文 Node HTTP/1.1,且部署前置代理并非产品不变量。物理下行直接使用连接池之外的浏览器原生原语(WebSocket),不把希望寄托在部署拓扑上。

后果与权衡:收益、成本与语义保持

决策笔记的 Consequences 部分做了诚实结算:

收益:每个网页仍维持两条长生命周期下行连接,但不再消耗浏览器 HTTP/1.1 的六连接配额;同源标签页、插件资源与普通 RPC 得以释放连接槽位。

语义保持:运行时仍然消费原始的"两条流",重连、流修复(stream repair)与跨流无序语义全部保留——上层感知不到物理载体的替换。

成本(三处新增维护面):

  1. webserver 中多出一个升级注册面;
  2. connection 包的 host 半部分引入 WebSocket 实现依赖;
  3. 浏览器 WebSocket 与进程内 SSE 两套物理编解码器需要分别维护。

但两者共享同一套 ServerRequest/frame schemas 与 IApiClient 语义,因此没有引入第二个应用协议——这是整个设计"通道无关"原则的最终落点。

后续演进:从双 WebSocket 到 Gateway 单 mux

需要说明的是,该决策笔记归档于 2026-08-27(实现于其前),而当前仓库代码显示这一层此后又经历了一次收敛演进。在 packages/api/gateway/README.md 与 docs/subsystems/web-client.md 中,物理下行已统一为 API Gateway 拥有的单一 /api/remote.mux WebSocket:

  • 所有可独立取消的逻辑流(RemoteStream)共享这条物理连接,每个逻辑流各自负责自身的重建与 baseline 语义;
  • Host 按 websocketHeartbeatIntervalMs(默认 30 秒)发送 Ping 控制帧,浏览器在 WebSocket 协议层应答 Pong,让空闲网络中间层持续看到流量而不产生任何业务帧;
  • 内部 $events 逻辑端点保留为唯一的 Connection generation source,其 opening { type: 'ready', clientId, host: { home } } 帧建立 generation 并提供 Host 事实;
  • web-client.md 明确写到:物理恢复与逻辑恢复分离——"Gateway mux restores the physical WebSocket; each RemoteStream reopens its own logical source",且当前架构中不存在单一的 HostFrame、events.mux、events.host 或通用 resync() API。

可见,双独立 WebSocket 解决"六连接配额"的目标达成后,多逻辑流复用又进一步把两条物理连接收敛为一条共享 mux,同时延续了本文所述的全部原则:物理载体与逻辑流解耦、上行保留 HTTP、ready 帧驱动的 generation 语义。两代设计共享的"通道无关协议 + 分层校验 + 显式失败"理念,正是理解 DeepSeek Harness 浏览器通信层的钥匙。

相关源码路径速查

  • 人工智能
  • AI Agent
  • Agent 框架
  • DeepSeek

【免费下载链接】deepseek-harness

DeepSeek Harness: Everything is a Plugin.

项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness
点击查看 免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值