Superpowers 源码解读(六):辅助工具与脚本的精妙设计

0 阅读7分钟

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 };
}
操作码定义
操作码常量用途
0x01TEXT文本消息
0x08CLOSE关闭连接
0x09PING心跳检测
0x0APONG心跳响应

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
调用者时机
brainstormingPhase 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 编码平台。