SuperPowers 的 brainstorming 伴侣服务器为实现零依赖、降低供应链风险,从使用 Express、ws 和 chokidar 的复杂方案,重构为仅依赖 http、crypto、fs、path 等 Node.js 内置模块的单一文件 (server.cjs) 实现。本文将深入解读其源码,揭示如何用不到 300 行代码,手动实现符合 RFC 6455 标准的 WebSocket 帧处理、HTTP 路由、文件系统监听及浏览器客户端交互的完整工作流。
SuperPowers Brainstorm Server 源码解读:零依赖 HTTP 与 WebSocket 实现
SuperPowers 的工作流强调在编写代码前进行头脑风暴和设计澄清。为了在浏览器中直观地展示方案选项并收集用户反馈,项目实现了一个轻量级的“可视化头脑风暴伴侣”服务器。本文将聚焦于该服务器的核心源码,特别是它如何摒弃了 Express、ws、chokidar 等第三方依赖,转而使用纯粹的 Node.js 内置模块来实现所有功能。
从 714 个文件到 1 个文件:零依赖的动机
最初,服务器依赖 express、ws 和 chokidar 这些库,其 node_modules 目录包含了 714 个被版本控制的文件。这种“vendored dependency”模式带来了明显的弊端:被冻结的依赖无法接收安全补丁,大量未经审计的第三方代码被纳入仓库,且对这些代码的修改看起来像普通的代码提交。
虽然该服务器仅运行在本地主机上,实际安全风险有限,但消除这些依赖在技术上是直接的,并且能带来更清洁的仓库和构建流程。基于这一动机,服务器被重写为一个单一的 server.cjs 文件,其设计规范和实现计划在零依赖设计文档中详细阐述。
核心模块解析
1. WebSocket 协议层:纯手工实现 RFC 6455
服务器最重要的部分之一是它完全自行实现了 WebSocket 协议。这是为了通信浏览器客户端和 Node.js 进程。关键函数均从 server.cjs 导出,以便进行单元测试。
握手计算 (computeAcceptKey):遵循 RFC 6455 规范,将客户端提供的 Sec-WebSocket-Key 与固定的魔术字符串 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 拼接后,进行 SHA-1 哈希并 Base64 编码,生成 Sec-WebSocket-Accept 头的值。测试文件 ws-protocol.test.js 中包含了 RFC 文档提供的示例用例,确保了实现的正确性。
帧编码 (encodeFrame):服务器向客户端发送数据时使用此函数。它构建未掩码(unmasked)的帧,并根据负载长度(payload length)采用三种不同的编码方式:小于 126 字节时使用 2 字节头;126 到 65535 字节时使用 4 字节头(前两字节后跟 16 位长度);大于 65535 字节时使用 10 字节头(前两字节后跟 64 位长度)。这确保了能处理各种大小的消息。
帧解码 (decodeFrame):处理来自客户端的掩码(masked)帧。解码逻辑包括:验证客户端帧必须带有掩码位;处理三种不同的长度编码;使用 4 字节掩码键对负载进行 XOR 反掩码。函数返回一个包含 opcode、payload 和 bytesConsumed 的对象,或者当缓冲区数据不完整时返回 null,这使得它可以安全地用于循环处理网络数据流。
协议定义了 TEXT、CLOSE、PING、PONG 四种操作码。对于未识别的操作码,服务器会回复一个状态码为 1003(Unsupported Data)的 CLOSE 帧。
2. HTTP 服务器与请求处理
服务器使用 Node.js 内置的 http 模块创建。handleRequest 函数是核心,它处理三种路由:
GET /:主页。服务器会查找CONTENT_DIR下最新的.html文件(通过mtime排序)。如果文件是完整的 HTML 文档(以<!doctype或<html开头),则直接使用;否则,将其作为片段包裹在frame-template.html模板中。无论哪种情况,都会在</body>标签前注入helper.js脚本。如果没有找到任何屏幕文件,则返回一个硬编码的等待页面。GET /files/*:静态文件服务。从CONTENT_DIR中提供文件,使用内置的MIME_TYPES映射表返回正确的Content-Type。- 其他所有路径:返回 404。
3. 文件系统监听与客户端广播
替代 chokidar 的是 Node.js 原生的 fs.watch。服务器监听 CONTENT_DIR 目录的变化。为了处理 fs.watch 可能产生的重复事件(尤其在 macOS 和 Linux 上),实现了一个基于文件名的防抖机制(debounce,超时 100ms)。
当监听到一个 .html 文件的变更时:
- 如果是新文件(之前未知的文件名),服务器会清除
state/events文件(记录用户选择的文件),并向标准输出记录screen-added事件。 - 如果是现有文件更新,则记录
screen-updated事件。 - 无论哪种情况,都会通过已建立的 WebSocket 连接向所有客户端广播
{ type: 'reload' }消息。
broadcast 函数遍历所有活跃的客户端 socket 集合(clients),将 JSON 消息编码为 WebSocket TEXT 帧并发送。这确保了浏览器页面能够实时响应服务器端的屏幕内容更新。
4. 状态管理与生命周期
服务器的状态通过文件系统进行外部管理,这有利于外部脚本(如启动和停止脚本)或 AI 代理读取状态:
state/server-info:在服务器成功启动后写入,包含端口、主机、URL 等连接信息。这使得brainstorming技能可以自动发现并连接到服务器。state/events:当浏览器客户端发送包含choice属性的事件时,该事件会被逐行追加到此文件。AI 代理在头脑风暴过程中可以通过读取此文件来获取用户的选择。state/server-stopped:在服务器关闭时写入,记录关闭原因和时间戳。
服务器实现了简单的活动跟踪和生命周期管理。通过 touchActivity 函数记录最后活动时间。一个周期性的检查(每 60 秒)会判断:如果指定了 owner PID 且该进程已退出,则服务器关闭;如果无活动时间超过 30 分钟,也会关闭。这防止了孤儿进程的长期存在。
5. 浏览器客户端交互 (helper.js 与模板)
frame-template.html 提供了一个具有一致 UI 框架(包含页头、主内容区和指示条)的 HTML 模板,支持深色/浅色主题。其中 <!-- CONTENT --> 占位符会被服务器动态替换为实际的头脑风暴内容。
helper.js 脚本被注入到页面中,负责:
- 建立 WebSocket 连接,并在断开后自动重连(间隔 1 秒)。
- 捕获用户点击事件:监听带有
data-choice属性的元素,将点击事件(包含选项文本和值)发送到服务器。 - 管理选择状态:提供
toggleSelect函数用于高亮选中的选项,并更新底部的指示条文本,向用户反馈当前选择。 - 暴露 API:通过
window.brainstorm对象向页面内的其他脚本提供send和choice方法,方便更复杂的交互。
测试验证
项目的可靠性通过两层测试保障:
ws-protocol.test.js:纯单元测试,直接requireserver.cjs导出的协议函数,覆盖了握手计算、各种长度帧的编解码、边界条件(如 125、126、65535、65536 字节)、拒绝未掩码帧等场景。server.test.js:集成测试,启动真实的服务器进程,验证 HTTP 响应内容、WebSocket 通信流程、文件监听触发、以及state/events文件的读写等完整业务逻辑。
总结
SuperPowers Brainstorm Server 的零依赖实现是一个优秀的工程实践案例。它展示了如何利用 Node.js 强大的内置模块,构建一个功能完备、轻量且无供应链风险的本地服务。通过手动实现 WebSocket 协议,开发者获得了对通信层的完全控制权;而基于文件的状态管理,巧妙地解耦了服务器与 AI 代理之间的交互,使整个头脑风暴流程既直观又可靠。这种设计哲学与 SuperPowers 强调的清晰、可控的编写计划和系统化调试理念一脉相承。
FAQ
Q: 为什么选择手动实现 WebSocket 协议而不是使用轻量的第三方库? A: 核心目标是实现“零依赖”。手动实现只针对需要的功能(文本帧、关闭、 ping/pong),避免了引入任何第三方代码,彻底消除了供应链风险,并且协议相关的代码可以导出并独立进行严格的单元测试。
Q: 服务器是如何处理浏览器与 Node.js 进程之间通信的可靠性的?
A: 浏览器端的 helper.js 包含了自动重连逻辑,在连接断开后每秒尝试重新连接。服务器端的 fs.watch 配有防抖处理,避免了文件系统事件的重复触发。状态通过文件系统持久化,即使服务器重启或连接暂时中断,也不会丢失用户的选择信息。