Superpowers 源码解读(六):辅助工具与脚本的精妙设计
深入解析 Visual Companion WebSocket 服务器、Git Worktrees 集成以及 Graphviz 渲染工具的实现细节
前言
在前五阶段的学习中,我们已经掌握了 Superpowers 的核心架构、技能系统设计、工作流技能、子代理系统和测试系统。第六阶段将目光投向「辅助工具与脚本」——这些看似不起眼,却蕴含精妙设计的小工具。
本阶段揭示一个重要理念:好的工具设计能让复杂系统变得简单可靠。
📁 本文学习目录
skills/brainstorming/scripts/ # Visual Companion 工具
├── server.cjs # WebSocket 服务器
├── start-server.sh # 启动脚本
├── stop-server.sh # 停止脚本
├── frame-template.html # 页面框架模板
└── helper.js # 客户端辅助脚本
skills/using-git-worktrees/ # Git Worktrees 集成
└── SKILL.md
skills/writing-skills/
└── render-graphs.js # Graphviz 渲染工具
一、Visual Companion:零依赖 WebSocket 服务器
1.1 什么是 Visual Companion?
在 brainstorming(头脑风暴)技能中,Agent 需要与用户进行视觉交互——展示 UI 原型图、流程图,让用户点击选择偏好。Visual Companion 是一个实时通信工具:
- Agent 写 HTML 文件 → 浏览器自动刷新显示
- 用户在浏览器点击 → Agent 实时收到选择结果
核心是一个 零依赖的 WebSocket 服务器,只用 Node.js 内置模块实现完整的 RFC 6455 协议。
1.2 WebSocket 协议实现(RFC 6455)
握手阶段
客户端发起 WebSocket 连接时,发送 HTTP Upgrade 请求,包含 Sec-WebSocket-Key 头。服务端计算 Sec-WebSocket-Accept 响应:
const WS_MAGIC = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
function computeAcceptKey(clientKey) {
return crypto
.createHash('sha1')
.update(clientKey + WS_MAGIC)
.digest('base64');
}
这个魔法字符串是 RFC 6455 规定的固定值,用于防止跨协议攻击。
帧编码(服务端 → 客户端)
function encodeFrame(opcode, payload) {
const fin = 0x80; // 最终帧标志
const len = payload.length;
let header;
if (len < 126) {
// 短格式:2 字节头部
header = Buffer.alloc(2);
header[0] = fin | opcode;
header[1] = len;
} else if (len < 65536) {
// 中等格式:4 字节头部,16 位扩展长度
header = Buffer.alloc(4);
header[0] = fin | opcode;
header[1] = 126;
header.writeUInt16BE(len, 2);
} else {
// 长格式:10 字节头部,64 位扩展长度
header = Buffer.alloc(10);
header[0] = fin | opcode;
header[1] = 127;
header.writeBigUInt64BE(BigInt(len), 2);
}
return Buffer.concat([header, payload]);
}
帧解码(客户端 → 服务端)
客户端发送的帧必须进行掩码处理(防止缓存投毒攻击):
function decodeFrame(buffer) {
const opcode = buffer[0] & 0x0F;
const masked = (buffer[1] & 0x80) !== 0;
let payloadLen = buffer[1] & 0x7F;
if (!masked) throw new Error('Client frames must be masked');
// 用 XOR 解码数据
const mask = buffer.slice(maskOffset, dataOffset);
const data = Buffer.alloc(payloadLen);
for (let i = 0; i < payloadLen; i++) {
data[i] = buffer[dataOffset + i] ^ mask[i % 4];
}
return { opcode, payload: data, bytesConsumed: totalLen };
}
操作码定义
| 操作码 | 常量 | 用途 |
|---|---|---|
0x01 | TEXT | 文本消息 |
0x08 | CLOSE | 关闭连接 |
0x09 | PING | 心跳检测 |
0x0A | PONG | 心跳响应 |
1.3 服务器架构
flowchart TB
subgraph HTTP层["HTTP Server"]
A[GET / → 返回最新 HTML]
B[GET /files/* → 静态文件服务]
C[Upgrade → WebSocket 握手]
end
subgraph 监听层["fs.watch()"]
D[监听 .html 文件变化]
E[防抖处理 100ms]
F[广播 reload 消息]
end
subgraph 通信层["WebSocket Pool"]
G[管理客户端连接]
H[接收用户事件]
I[写入 .events 文件]
end
subgraph 生命周期["Lifecycle Manager"]
J[30 分钟无活动退出]
K[所有者进程检测]
L[优雅关闭处理]
end
A --> D
B --> D
C --> G
D --> E --> F --> G
G --> H --> I
J --> L
K --> L
1.4 消息协议
服务端 → 客户端
| 消息类型 | 用途 |
|---|---|
{ "type": "reload" } | 通知浏览器刷新页面 |
{ "type": "screen-added" } | 新屏幕文件创建(日志用) |
{ "type": "screen-updated" } | 屏幕文件更新(日志用) |
客户端 → 服务端
{ "choice": "option-a", "question": "选择配色方案" }
用户的选择会被追加到 .events 文件供 Agent 读取:
function handleMessage(text) {
const event = JSON.parse(text);
touchActivity(); // 更新活动时间
console.log(JSON.stringify({ source: 'user-event', ...event }));
if (event.choice) {
const eventsFile = path.join(SCREEN_DIR, '.events');
fs.appendFileSync(eventsFile, JSON.stringify(event) + '\n');
}
}
1.5 生命周期管理
自动退出机制
const IDLE_TIMEOUT_MS = 30 * 60 * 1000; // 30 分钟无活动
function ownerAlive() {
if (!OWNER_PID) return true;
try {
process.kill(OWNER_PID, 0); // 检查进程是否存活
return true;
} catch (e) {
return false;
}
}
// 每 60 秒检查一次
setInterval(() => {
if (!ownerAlive())
shutdown('owner process exited');
else if (Date.now() - lastActivity > IDLE_TIMEOUT_MS)
shutdown('idle timeout');
}, 60 * 1000);
这种设计确保:
- 所有者进程死亡 → 服务器自动退出(不会变成孤儿)
- 30 分钟无活动 → 服务器自动退出(释放资源)
进程关系
flowchart TD
A[Agent Harness 进程] --> B[start-server.sh 脚本]
B --> C[Node.js server.cjs 进程]
C -->|跟踪祖父 PID| A
style A fill:#e1f5fe
style C fill:#fff3e0
为什么是祖父进程?因为 $PPID 是执行脚本的 shell,脚本结束后就消失了。真正的所有者是 Harness。
二、启动与停止脚本:跨平台兼容的艺术
2.1 启动脚本设计
启动脚本 start-server.sh 需要处理多种复杂场景:后台运行、前台运行、跨平台兼容、进程存活验证等。
命令行参数
./start-server.sh \
--project-dir <path> # 持久化目录(而非 /tmp)
--host <bind-host> # 绑定地址(默认 127.0.0.1)
--url-host <host> # URL 显示主机名
--foreground # 前台运行
--background # 强制后台运行
典型使用场景:
| 场景 | 参数选择 |
|---|---|
| 本地开发(默认) | 无参数,后台运行 |
| Codex CI 环境 | 自动前台(会被检测) |
| Windows/MSYS2 | 自动前台(后台不稳定) |
| 远程服务器 | --host 0.0.0.0 |
2.2 环境自适应
不同环境对后台进程的处理方式不同,脚本需要自动适应:
# Codex CI 环境会杀死后台进程
if [[ -n "${CODEX_CI:-}" && "$FOREGROUND" != "true" && "$FORCE_BACKGROUND" != "true" ]]; then
FOREGROUND="true"
fi
# Windows/MSYS2 的后台进程会被回收
case "${OSTYPE:-}" in
msys*|cygwin*|mingw*) FOREGROUND="true" ;;
esac
if [[ -n "${MSYSTEM:-}" ]]; then
FOREGROUND="true"
fi
这种「渐进式降级」策略确保在各种环境中都能正常工作。
2.3 会话目录设计
每次启动都会创建独立的会话目录,避免冲突:
SESSION_ID="$$-$(date +%s)"
if [[ -n "$PROJECT_DIR" ]]; then
# 持久化目录
SCREEN_DIR="${PROJECT_DIR}/.superpowers/brainstorm/${SESSION_ID}"
else
# 临时目录
SCREEN_DIR="/tmp/brainstorm-${SESSION_ID}"
fi
目录结构:
/tmp/brainstorm-12345-1711440000/
├── .server-info # 服务器信息 JSON
├── .server.pid # 进程 ID
├── .server.log # 日志输出
├── .events # 用户事件记录
├── screen-1.html # 屏幕文件(Agent 写入)
└── screen-2.html
2.4 启动流程详解
mermaid
flowchart TD
A[解析命令行参数] --> B[生成唯一会话 ID]
B --> C[创建会话目录]
C --> D[杀死旧服务器]
D --> E{检测环境}
E -->|Windows/Codex CI| F[前台模式]
E -->|其他| G[后台模式]
F --> H[直接运行 node server.cjs]
G --> I[nohup + disown 后台运行]
H --> J[等待 server-started 消息]
I --> J
J --> K{5 秒内收到消息?}
K -->|否| L[报错退出]
K -->|是| M[验证进程存活]
M --> N{存活?}
N -->|否| O[报错:进程被杀死]
N -->|是| P[输出连接信息]
2.5 启动验证机制
脚本不会简单地启动进程就退出,而是等待服务器真正就绪:
# 等待 server-started 消息(最多 5 秒)
for i in {1..50}; do
if grep -q "server-started" "$LOG_FILE" 2>/dev/null; then
# 额外验证:进程存活检查(防止被进程收割器杀死)
alive="true"
for _ in {1..20}; do
if ! kill -0 "$SERVER_PID" 2>/dev/null; then
alive="false"
break
fi
sleep 0.1
done
if [[ "$alive" != "true" ]]; then
echo '{"error": "Server started but was killed. Retry with --foreground"}'
exit 1
fi
grep "server-started" "$LOG_FILE" | head -1
exit 0
fi
sleep 0.1
done
这种双重验证(消息 + 存活)确保返回的连接信息是真正可用的。
2.6 停止脚本设计
停止脚本 stop-server.sh 实现了优雅关闭流程:
# 1. 尝试优雅关闭(SIGTERM)
kill "$pid" 2>/dev/null || true
# 2. 等待最多 2 秒
for i in {1..20}; do
if ! kill -0 "$pid" 2>/dev/null; then
break
fi
sleep 0.1
done
# 3. 如果仍存活,强制终止(SIGKILL)
if kill -0 "$pid" 2>/dev/null; then
kill -9 "$pid" 2>/dev/null || true
sleep 0.1
fi
# 4. 清理文件
rm -f "$PID_FILE" "${SCREEN_DIR}/.server.log"
# 5. 只删除临时目录,保留持久化目录
if [[ "$SCREEN_DIR" == /tmp/* ]]; then
rm -rf "$SCREEN_DIR"
fi
关键设计决策:
| 目录类型 | 停止后处理 |
|---|---|
/tmp/brainstorm-* | 完全删除 |
.superpowers/brainstorm/ | 保留(供后续查看 mockup) |
这体现了「用户数据优先」的设计理念。
三、Git Worktrees:安全的并行开发隔离
3.1 为什么需要 Worktrees?
在 Superpowers 的工作流中,经常需要同时处理多个任务:
flowchart LR
A[主分支: 运行测试] --> B[功能分支 A: 开发新功能]
A --> C[功能分支 B: 修复 bug]
style A fill:#e8f5e9
style B fill:#fff3e0
style C fill:#e3f2fd
传统的 git checkout 会切换整个工作区,但 git worktree 可以让多个分支同时存在:
# 创建独立工作区
git worktree add .worktrees/feature-auth -b feature/auth
# 结果
project/
├── (主工作区,在 main 分支)
└── .worktrees/
└── feature-auth/ (独立工作区,在 feature/auth 分支)
3.2 目录选择优先级
Superpowers 设计了清晰的优先级规则:
flowchart TD
A[检查现有目录] --> B{.worktrees/ 存在?}
B -->|是| C[使用 .worktrees/]
B -->|否| D{worktrees/ 存在?}
D -->|是| E[使用 worktrees/]
D -->|否| F[检查 CLAUDE.md 配置]
F --> G{有配置?}
G -->|是| H[使用配置路径]
G -->|否| I[询问用户]
实现代码:
# 优先级 1:检查现有目录
ls -d .worktrees 2>/dev/null # 首选(隐藏目录)
ls -d worktrees 2>/dev/null # 备选
# 优先级 2:检查配置文件
grep -i "worktree.*director" CLAUDE.md 2>/dev/null
# 优先级 3:询问用户
echo "No worktree directory found. Where should I create worktrees?"
echo "1. .worktrees/ (project-local, hidden)"
echo "2. ~/.config/superpowers/worktrees/<project-name>/ (global)"
3.3 安全验证:.gitignore 检查
这是最关键的安全措施——必须确保 worktree 目录被忽略:
# 检查目录是否被忽略
git check-ignore -q .worktrees 2>/dev/null || git check-ignore -q worktrees 2>/dev/null
如果未被忽略:
# 按照规则"立即修复损坏的东西"
echo ".worktrees/" >> .gitignore
git add .gitignore
git commit -m "Add .worktrees to gitignore"
# 然后继续创建 worktree
为什么这很重要?
如果 .worktrees/ 未被忽略,会发生:
git status显示 worktree 内的所有文件- 可能意外提交 worktree 内容
- 污染仓库,造成团队协作混乱
3.4 完整创建流程
flowchart TD
A[检测项目名] --> B[检查目录是否被忽略]
B --> C{已忽略?}
C -->|否| D[添加到 .gitignore 并提交]
C -->|是| E[创建 worktree]
D --> E
E --> F[进入工作区]
F --> G[自动检测项目类型]
G --> H[运行依赖安装]
H --> I[运行基线测试]
I --> J{测试通过?}
J -->|是| K[报告就绪]
J -->|否| L[报告失败,询问用户]
自动项目检测:
# Node.js
if [ -f package.json ]; then npm install; fi
# Rust
if [ -f Cargo.toml ]; then cargo build; fi
# Python
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
if [ -f pyproject.toml ]; then poetry install; fi
# Go
if [ -f go.mod ]; then go mod download; fi
3.5 与其他技能的集成
Git Worktrees 是「基础设施」技能,被多个核心技能调用:
flowchart LR
A[brainstorming] -->|设计确认后| B[using-git-worktrees]
B --> C[subagent-driven-development]
C --> D[finishing-a-development-branch]
D -->|清理| B
| 调用者 | 时机 |
|---|---|
| brainstorming | Phase 4:设计批准后,实施前 |
| subagent-driven-development | 执行任务前 |
| executing-plans | 执行计划前 |
四、Graphviz 渲染工具:从代码到可视化
4.1 为什么需要渲染工具?
Superpowers 的技能文档中大量使用 Graphviz DOT 语言描述流程图:
digraph tdd_flow {
rankdir=TB;
start [shape=circle];
start -> "Write Test";
"Write Test" -> "Run Test" -> "Failing?" -> "Write Code";
}
但这些 DOT 代码块在 Markdown 中只是文本,用户无法直观看到流程图。render-graphs.js 工具解决了这个问题。
4.2 工具实现原理
提取 DOT 代码块
function extractDotBlocks(markdown) {
const blocks = [];
const regex = /```dot\n([\s\S]*?)```/g;
let match;
while ((match = regex.exec(markdown)) !== null) {
const content = match[1].trim();
// 提取 digraph 名称
const nameMatch = content.match(/digraph\s+(\w+)/);
const name = nameMatch ? nameMatch[1] : `graph_${blocks.length + 1}`;
blocks.push({ name, content });
}
return blocks;
}
调用系统 Graphviz
function renderToSvg(dotContent) {
try {
return execSync('dot -Tsvg', {
input: dotContent,
encoding: 'utf-8',
maxBuffer: 10 * 1024 * 1024 // 10MB 缓冲区
});
} catch (err) {
console.error('Error running dot:', err.message);
return null;
}
}
4.3 合并模式
一个技能可能有多个流程图,--combine 参数可以将它们合并为一个 SVG:
function combineGraphs(blocks, skillName) {
const bodies = blocks.map((block, i) => {
const body = extractGraphBody(block.content);
// 包装为 cluster 子图
return ` subgraph cluster_${i} {
label="${block.name}";
${body.split('\n').map(line => ' ' + line).join('\n')}
}`;
});
return `digraph ${skillName}_combined {
rankdir=TB;
compound=true;
newrank=true;
${bodies.join('\n\n')}
}`;
}
4.4 使用方式
# 单独渲染每个流程图
./render-graphs.js ../subagent-driven-development
# 输出
# Found 3 diagram(s) in subagent-driven-development/SKILL.md
# Rendered: main_flow.svg
# Rendered: review_cycle.svg
# Rendered: error_handling.svg
# 合并渲染
./render-graphs.js ../subagent-driven-development --combine
# 输出到 diagrams/ 目录
五、设计洞察与最佳实践
5.1 零依赖哲学
Superpowers 的 WebSocket 服务器完全使用 Node.js 内置模块实现:
const crypto = require('crypto'); // SHA1 哈希
const http = require('http'); // HTTP 服务器
const fs = require('fs'); // 文件系统
const path = require('path'); // 路径处理
为什么不用 ws、socket.io 等成熟库?
| 原因 | 说明 |
|---|---|
| 减少依赖 | 用户不需要安装额外 npm 包 |
| 完全控制 | 可以精确处理边缘情况 |
| 学习价值 | 理解协议本质 |
| 安全性 | 减少供应链攻击面 |
5.2 跨平台兼容策略
flowchart TD
A[检测运行环境] --> B{Codex CI?}
B -->|是| C[自动前台运行]
B -->|否| D{Windows/MSYS2?}
D -->|是| C
D -->|否| E{后台运行}
E --> F{进程存活?}
F -->|是| G[正常工作]
F -->|否| H[提示用户使用 --foreground]
style C fill:#fff3e0
style G fill:#e8f5e9
style H fill:#ffebee
| 平台 | 问题 | 解决方案 |
|---|---|---|
| macOS/Linux | 默认行为 | 后台运行(nohup + disown) |
| Windows/MSYS2 | 后台进程会被回收 | 自动前台运行 |
| Codex CI | 会杀死后台进程 | 检测环境变量,自动前台 |
| 远程服务器 | 需要外部访问 | --host 0.0.0.0 --url-host <公网IP> |
渐进式降级原则:
最优方案(后台运行)→ 不支持 → 次优方案(前台运行)→ 不支持 → 报错提示
5.3 安全优先设计
Git Worktrees 技能展示了一个重要原则:安全验证不可跳过
flowchart TD
A[准备创建 worktree] --> B[检查 .gitignore]
B --> C{目录已被忽略?}
C -->|是| D[创建 worktree]
C -->|否| E[强制添加到 .gitignore]
E --> F[提交更改]
F --> D
style C fill:#fff3e0
style E fill:#ffebee
style D fill:#e8f5e9
这种设计防止了:
- 意外提交 worktree 内容
- 污染仓库
- 团队协作混乱
5.4 自动化优先
所有脚本都尽可能减少用户手动操作:
# 自动检测项目类型并安装依赖
[ -f package.json ] && npm install
[ -f Cargo.toml ] && cargo build
[ -f go.mod ] && go mod download
# 自动运行基线测试
npm test || cargo test || pytest || go test ./...
设计理念:让工具自己「弄清楚」该做什么,而不是让用户告诉它。
5.5 生命周期管理
| 机制 | 目的 | 实现 |
|---|---|---|
| 30 分钟无活动退出 | 防止孤儿进程占用资源 | setInterval 检查 lastActivity |
| 所有者进程检测 | Agent 退出时服务器也退出 | process.kill(OWNER_PID, 0) |
| 优雅关闭 + 强制终止 | 确保服务器能停止 | SIGTERM → 等待 → SIGKILL |
| 持久化目录保留 | 用户数据不丢失 | 只删除 /tmp/* 目录 |
六、总结
第六阶段的学习揭示了 Superpowers 项目中「小工具,大智慧」的设计哲学。
核心收获
| 工具 | 关键设计点 |
|---|---|
| WebSocket 服务器 | 零依赖实现完整 RFC 6455,生命周期自动管理 |
| 启动/停止脚本 | 跨平台自适应,双重启动验证,优雅关闭流程 |
| Git Worktrees | 目录选择优先级,强制 .gitignore 检查,自动项目检测 |
| Graphviz 渲染 | 从 Markdown 提取 DOT,单独/合并渲染 |
设计原则总结
| 原则 | 实践 |
|---|---|
| 零依赖 | 减少安装负担,提高可靠性 |
| 跨平台兼容 | 渐进式降级,自动适应环境 |
| 安全优先 | 强制验证,防止用户犯错 |
| 自动化 | 工具自己「弄清楚」,减少用户负担 |
| 生命周期管理 | 防止孤儿进程,资源自动清理 |
参考资料
本文是 Superpowers 源码学习系列第六阶段笔记。下一阶段将深入跨平台支持,了解 superpowers 如何适配 Codex、OpenCode、Gemini 等多个 AI 编码平台。