跳转至

零依赖 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 服务器

三条路由:

  1. GET / — 按 mtime 从 screen 目录提供最新的 .html 文件。区分完整文档与片段,将片段包装到 frame 模板中,并注入 helper.js。返回 text/html。当不存在任何 .html 文件时,提供硬编码的等待页面("Waiting for Claude to push a screen...")并注入 helper.js。
  2. GET /files/* — 从 screen 目录提供静态文件,通过硬编码的扩展名映射表(html、css、js、png、jpg、gif、svg、json)查找 MIME 类型。未找到时返回 404。
  3. 其他所有请求 — 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)

启动流程

  1. 如果 SCREEN_DIR 不存在则创建它(mkdirSync 递归)
  2. 从 __dirname 加载 frame 模板和 helper.js
  3. 在配置的 host/port 上启动 HTTP 服务器
  4. 在 SCREEN_DIR 上启动 fs.watch
  5. 监听成功后,向 stdout 输出 server-started JSON:{ type, port, host, url_host, url, screen_dir }
  6. 将相同的 JSON 写入 SCREEN_DIR/.server-info,以便 stdout 被隐藏(后台执行)时 agent 也能找到连接信息

应用层 WebSocket 消息

当客户端发送 TEXT 帧时:

  1. 解析为 JSON。如果解析失败,记录到 stderr 并继续。
  2. 以 { source: 'user-event', ...event } 的形式记录到 stdout。
  3. 如果事件包含 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.js
  • stop-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