【免费下载链接】deepseek-harness
DeepSeek Harness: Everything is a Plugin.
本文基于仓库归档架构决策笔记 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 文档。客户端的解码路径是分层的:
- 先校验
ServerRequest信封(envelope); - 再针对该路径校验具体的 frame 联合类型(
MuxFrame/HostFrameunion); - 校验通过后,把窄化后的
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 所有。升级发生之前,必须先通过信任与认证关卡:
- 应用
/api的 Host / Origin 检查; - 再应用与一元 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.describeHTTP 调用成功之后,才发布connected。
验证体系:从契约测试到端到端浏览器回放
决策笔记记录了四层验证手段,且每层在仓库中都有对应物:
| 验证层 | 覆盖内容 | 仓库对应 |
|---|---|---|
| webserver 契约测试 | upgrade-pathname 调度、重复注册拒绝、disposal、teardown | packages/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 fetch | packages/client/connection/src/client/index.ts(createWebConnectionRpc 与 createFixtureConnectionRpc 双实现) |
| 组装式无密钥浏览器回放 | Chromium + 真实 host + HTTP 上行 + 完整 WebSocket 下行链 | 仓库 snapshots 与端到端测试套件 |
备选方案:四个被否决的路径及其理由
决策笔记记录了四个被认真考虑后否决的替代方案,每一条的取舍都值得回味:
- 在一条 WebSocket 内多路复用 mux 与 host:需要引入通道标签、多路复用队列和单连接背压策略,还会改变既有"双流 readiness"语义。而两条 WebSocket 已经能绕开 HTTP/1.1 六连接限制,同时把改动留在物理载体层。
- 把一元调用与
respond也搬进全双工 WebSocket:这会重写超时、取消、HTTP 状态、信任围栏与请求关联(correlation)等一整套行为,但对"下行连接槽位"这个当前问题毫无增益。HTTP 上行是明确保留的边界。 - 保留网络 SSE 回退:双载体会让生产浏览器路径因代理或握手差异而静默分叉,并让连接数限制问题留在受支持分支里。预发布阶段只交付 WebSocket 下行,让既有重连行为与连接状态显式暴露失败。
- 依赖 HTTP/2 提升连接并发:内置开发服务器使用明文 Node HTTP/1.1,且部署前置代理并非产品不变量。物理下行直接使用连接池之外的浏览器原生原语(WebSocket),不把希望寄托在部署拓扑上。
后果与权衡:收益、成本与语义保持
决策笔记的 Consequences 部分做了诚实结算:
收益:每个网页仍维持两条长生命周期下行连接,但不再消耗浏览器 HTTP/1.1 的六连接配额;同源标签页、插件资源与普通 RPC 得以释放连接槽位。
语义保持:运行时仍然消费原始的"两条流",重连、流修复(stream repair)与跨流无序语义全部保留——上层感知不到物理载体的替换。
成本(三处新增维护面):
- webserver 中多出一个升级注册面;
- connection 包的 host 半部分引入 WebSocket 实现依赖;
- 浏览器 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
RemoteStreamreopens its own logical source",且当前架构中不存在单一的HostFrame、events.mux、events.host或通用resync()API。
可见,双独立 WebSocket 解决"六连接配额"的目标达成后,多逻辑流复用又进一步把两条物理连接收敛为一条共享 mux,同时延续了本文所述的全部原则:物理载体与逻辑流解耦、上行保留 HTTP、ready 帧驱动的 generation 语义。两代设计共享的"通道无关协议 + 分层校验 + 显式失败"理念,正是理解 DeepSeek Harness 浏览器通信层的钥匙。
相关源码路径速查
- 归档决策笔记原文:.agents/notes/archived/architecture/2026-08-04-websocket-downlink-carrier.md
- 物理载体与信任策略:packages/client/connection/README.md、packages/client/connection/src/rpc.ts、packages/client/connection/src/client/index.ts
- 升级路由与裸 socket 生命周期:packages/host/webserver/README.md、packages/host/webserver/tests/webserver.spec.ts
- Gateway mux 与逻辑流复用:packages/api/gateway/README.md
- Web Client 当前通信与重连模型:docs/subsystems/web-client.md
【免费下载链接】deepseek-harness
DeepSeek Harness: Everything is a Plugin.
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



