Skip to content

案例:构建 WebSocket CLI 服务端

项目背景

本案例展示如何使用 Superpowers 构建一个支持 WebSocket 的 CLI 服务端。该服务端用于支持 brainstorming 技能的可视化界面,提供:

  • HTTP 服务:提供可视化界面
  • WebSocket 通信:实时双向通信
  • 文件监控:自动重载界面
  • 事件记录:持久化用户选择

工作流程演示

阶段一:头脑风暴(brainstorming)

触发场景: 需要构建一个支持实时可视化的 brainstorming 工具

AI 行为:

  • 提问理解需求:服务端需要支持哪些功能?
  • 探索技术方案:WebSocket vs HTTP 长轮询?
  • 分段展示设计:协议设计 → 文件结构 → 接口定义

产出: docs/design.md 设计文档

阶段二:编写计划(writing-plans)

AI 行为:

  • 将设计分解为 2-5 分钟的小任务
  • 每个任务包含:文件路径、具体代码、验证步骤

示例任务分解:

markdown
## Task 1: 创建基础 HTTP 服务端
- 文件:`server.js`
- 步骤:
  1. 引入 http 模块
  2. 创建服务器监听端口
  3. 提供 HTML 页面
- 验证:curl localhost:3000 返回 HTML

## Task 2: 添加 WebSocket 支持
- 文件:`server.js`
- 步骤:
  1. 引入 ws 模块
  2. 处理 WebSocket 升级请求
  3. 实现消息广播
- 验证:使用 wscat 连接并发送消息

阶段三:TDD 实现(test-driven-development)

RED:编写失败测试

javascript
// server.test.js
await test('serves waiting page when no screens exist', async () => {
  const res = await fetch(`http://localhost:${TEST_PORT}/`);
  assert.strictEqual(res.status, 200);
  assert(res.body.includes('Waiting for Claude'));
});

GREEN:最小实现

javascript
// server.js
const http = require('http');
const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/html' });
  res.end('<html><body>Waiting for Claude</body></html>');
});
server.listen(PORT);

REFACTOR:优化代码

添加模板系统、WebSocket 支持、文件监控等。

阶段四:调试(systematic-debugging)

问题: WebSocket 客户端断开后,广播失败

诊断流程:

  1. 隔离问题

    bash
    # 启动服务端
    node server.js
    
    # 连接客户端后立即断开
    wscat -c ws://localhost:3000
    # Ctrl+C 断开
    
    # 观察错误
    # Error: Cannot send to closed WebSocket
  2. 根因分析

    • 断开的客户端未从连接列表移除
    • 广播时尝试发送到已关闭的连接
  3. 修复

    javascript
    ws.on('close', () => {
      const index = clients.indexOf(ws);
      if (index > -1) clients.splice(index, 1);
    });

阶段五:完成(finishing-a-development-branch)

验证清单:

  • [ ] 所有测试通过
  • [ ] 无 console 错误
  • [ ] 文档已更新
  • [ ] 代码已审查

产出: 合并到主分支或创建 PR

关键代码

HTTP 服务

javascript
const http = require('http');
const fs = require('fs');
const path = require('path');

function serveHtml(dir, port) {
  const server = http.createServer((req, res) => {
    if (req.url !== '/') {
      res.writeHead(404);
      return res.end('Not found');
    }

    // 查找最新的 HTML 文件
    const files = fs.readdirSync(dir)
      .filter(f => f.endsWith('.html'))
      .map(f => ({
        name: f,
        mtime: fs.statSync(path.join(dir, f)).mtime
      }))
      .sort((a, b) => b.mtime - a.mtime);

    if (files.length === 0) {
      res.writeHead(200, { 'Content-Type': 'text/html' });
      return res.end('<h1>Waiting for Claude...</h1>');
    }

    const content = fs.readFileSync(path.join(dir, files[0].name), 'utf-8');
    res.writeHead(200, { 'Content-Type': 'text/html' });
    res.end(content);
  });

  server.listen(port);
  console.log(JSON.stringify({ type: 'server-started', port }));
}

WebSocket 通信

javascript
const WebSocket = require('ws');

function setupWebSocket(server, dir) {
  const wss = new WebSocket.Server({ server });
  const clients = new Set();

  wss.on('connection', (ws) => {
    clients.add(ws);

    ws.on('message', (data) => {
      const event = JSON.parse(data.toString());

      // 记录用户选择到文件
      if (event.choice) {
        fs.appendFileSync(
          path.join(dir, '.events'),
          JSON.stringify(event) + '\n'
        );
      }

      // 输出到 stdout 供 Claude 读取
      console.log(JSON.stringify({ ...event, source: 'user-event' }));
    });

    ws.on('close', () => clients.delete(ws));
  });

  return clients;
}

文件监控

javascript
const fs = require('fs');

function watchFiles(dir, clients) {
  fs.watch(dir, (eventType, filename) => {
    if (!filename?.endsWith('.html')) return;

    // 清除之前的事件记录
    const eventsFile = path.join(dir, '.events');
    if (fs.existsSync(eventsFile)) {
      fs.unlinkSync(eventsFile);
    }

    // 通知所有客户端重载
    const message = JSON.stringify({ type: 'reload' });
    clients.forEach(client => {
      if (client.readyState === WebSocket.OPEN) {
        client.send(message);
      }
    });

    console.log(JSON.stringify({ type: 'screen-updated', file: filename }));
  });
}

关键学习点

1. brainstorming 技能使用

  • AI 会主动提问而非直接写代码
  • 设计文档分段展示,便于理解
  • 技术方案会进行对比分析

2. 任务拆分技巧

  • 每个任务 2-5 分钟
  • 明确的验证标准
  • 任务间依赖关系清晰

3. TDD 实践

  • 先写测试,再写代码
  • RED-GREEN-REFACTOR 循环
  • 测试即文档

4. 系统化调试

  • 隔离问题,最小复现
  • 根因分析而非表面修复
  • 修复后验证边界情况

扩展练习

  1. 添加认证:为 WebSocket 连接添加 token 验证
  2. 支持多房间:实现多个独立的 brainstorming 会话
  3. 持久化历史:将用户选择保存到数据库

相关技能