案例:构建 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 客户端断开后,广播失败
诊断流程:
隔离问题
bash# 启动服务端 node server.js # 连接客户端后立即断开 wscat -c ws://localhost:3000 # Ctrl+C 断开 # 观察错误 # Error: Cannot send to closed WebSocket根因分析
- 断开的客户端未从连接列表移除
- 广播时尝试发送到已关闭的连接
修复
javascriptws.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. 系统化调试
- 隔离问题,最小复现
- 根因分析而非表面修复
- 修复后验证边界情况
扩展练习
- 添加认证:为 WebSocket 连接添加 token 验证
- 支持多房间:实现多个独立的 brainstorming 会话
- 持久化历史:将用户选择保存到数据库
相关技能
- brainstorming - 需求探索与设计
- writing-plans - 任务分解
- test-driven-development - TDD 实践
- systematic-debugging - 系统化调试