零依赖 Brainstorm Server¶
将 brainstorm companion server 中随仓库提交的 node_modules(express、ws、chokidar —— 共 714 个跟踪文件)替换为一个零依赖的 server.js,仅使用 Node.js 内置模块。
动机¶
将 node_modules 随仓库提交会带来供应链风险:冻结的依赖无法获得安全补丁,714 个第三方代码文件未经审计就被提交,而且对随仓库提交代码的修改看起来与普通提交无异。尽管实际风险较低(仅限 localhost 的开发服务器),消除这一风险也很简单。
架构¶
单个 server.js 文件(约 250-300 行),使用 http、crypto、fs 和 path。该文件承担两个角色:
- 直接运行时(
node server.js):启动 HTTP/WebSocket 服务器 - 被 require 时(
require('./server.js')):导出 WebSocket 协议函数供单元测试使用
WebSocket 协议¶
实现 RFC 6455,仅支持文本帧:
握手: 使用 SHA-1 加上 RFC 6455 的魔数 GUID,从客户端的 Sec-WebSocket-Key 计算出 Sec-WebSocket-Accept。返回 101 Switching Protocols。
帧解码(客户端到服务器): 处理三种带掩码的长度编码: - 小型:payload < 126 字节 - 中型:126-65535 字节(16 位扩展) - 大型:> 65535 字节(64 位扩展)
使用 4 字节掩码密钥对 payload 进行 XOR 去掩码。返回 { opcode, payload, bytesConsumed },对于不完整的缓冲区返回 null。拒绝未加掩码的帧。
帧编码(服务器到客户端): 不带掩码的帧,使用相同的三种长度编码。
处理的操作码: TEXT (0x01)、CLOSE (0x08)、PING (0x09)、PONG (0x0A)。无法识别的操作码会收到状态为 1003(不支持的数据)的关闭帧。
有意跳过的功能: 二进制帧、分片消息、扩展(permessage-deflate)、子协议。对于 localhost 客户端之间的小型 JSON 文本消息,这些都不需要。扩展和子协议是在握手中协商的 —— 由于不对外声明,它们永远不会生效。
缓冲区累积: 每个连接维护一个缓冲区。收到 data 时,追加数据并循环调用 decodeFrame,直到它返回 null 或缓冲区为空。
HTTP 服务器¶
三条路由:
GET /— 按 mtime 从 screen 目录提供最新的.html文件。区分完整文档与片段,将片段包装到 frame 模板中,并注入 helper.js。返回text/html。当不存在任何.html文件时,提供硬编码的等待页面("Waiting for Claude to push a screen...")并注入 helper.js。GET /files/*— 从 screen 目录提供静态文件,通过硬编码的扩展名映射表(html、css、js、png、jpg、gif、svg、json)查找 MIME 类型。未找到时返回 404。- 其他所有请求 — 404。
WebSocket 升级通过 HTTP 服务器上的 'upgrade' 事件处理,与请求处理器分开。
配置¶
环境变量(全部可选):
BRAINSTORM_PORT— 绑定的端口(默认:49152-65535 之间的随机高位端口)BRAINSTORM_HOST— 绑定的接口(默认:127.0.0.1)BRAINSTORM_URL_HOST— 启动 JSON 中 URL 使用的主机名(默认:当 host 为127.0.0.1时为localhost,否则与 host 相同)BRAINSTORM_DIR— screen 目录路径(默认:/tmp/brainstorm)
启动流程¶
- 如果
SCREEN_DIR不存在则创建它(mkdirSync递归) - 从
__dirname加载 frame 模板和 helper.js - 在配置的 host/port 上启动 HTTP 服务器
- 在
SCREEN_DIR上启动fs.watch - 监听成功后,向 stdout 输出
server-startedJSON:{ type, port, host, url_host, url, screen_dir } - 将相同的 JSON 写入
SCREEN_DIR/.server-info,以便 stdout 被隐藏(后台执行)时 agent 也能找到连接信息
应用层 WebSocket 消息¶
当客户端发送 TEXT 帧时:
- 解析为 JSON。如果解析失败,记录到 stderr 并继续。
- 以
{ source: 'user-event', ...event }的形式记录到 stdout。 - 如果事件包含
choice属性,则将 JSON 追加到SCREEN_DIR/.events(每个事件一行)。
文件监视¶
fs.watch(SCREEN_DIR) 取代 chokidar。当 HTML 文件事件发生时:
- 新文件(针对已存在文件的
rename事件):如果存在.events文件则删除(unlinkSync),以 JSON 形式将screen-added记录到 stdout - 文件更改(
change事件):以 JSON 形式将screen-updated记录到 stdout(不要清除.events) - 两种事件都:向所有已连接的 WebSocket 客户端发送
{ type: 'reload' }
按文件名进行约 100ms 的超时去抖,以防止重复事件(这在 macOS 和 Linux 上很常见)。
错误处理¶
- WebSocket 客户端发来的畸形 JSON:记录到 stderr,继续
- 未处理的操作码:以状态 1003 关闭连接
- 客户端断开:从广播集合中移除
fs.watch错误:记录到 stderr,继续- 没有优雅关闭逻辑 —— shell 脚本通过 SIGTERM 处理进程生命周期
变更内容¶
| 之前 | 之后 |
|---|---|
index.js + package.json + package-lock.json + 714 个 node_modules 文件 |
server.js(单个文件) |
| express、ws、chokidar 依赖 | 无 |
| 不提供静态文件服务 | /files/* 从 screen 目录提供静态文件 |
保持不变的内容¶
helper.js— 无更改frame-template.html— 无更改start-server.sh— 一行更新:index.js改为server.jsstop-server.sh— 无更改visual-companion.md— 无更改- 所有现有的服务器行为和外部契约
平台兼容性¶
server.js仅使用跨平台的 Node 内置模块fs.watch在 macOS、Linux 和 Windows 上对单个扁平目录是可靠的- shell 脚本需要 bash(Windows 上为 Git Bash,这是 Claude Code 所必需的)
测试¶
单元测试(ws-protocol.test.js):通过 require server.js 的导出,直接测试 WebSocket 帧编解码、握手计算和协议边界情况。
集成测试(server.test.js):测试完整的服务器行为 —— HTTP 服务、WebSocket 通信、文件监视、brainstorm 工作流。使用 ws npm 包作为仅用于测试的客户端依赖(不随最终用户分发)。
本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 su/superpowers