OpenCode 版 SuperPowers 插件通过两个关键钩子实现集成:config 钩子在运行时将技能目录动态注入到 OpenCode 的技能搜索路径中,让原生的 skill 工具自动发现所有技能;experimental.chat.messages.transform 钩子则在每个会话的首条用户消息前注入包含 using-superpowers 技能和工具映射的引导内容,确保 AI 代理从一开始就遵循 SuperPowers 工作流。两者配合,实现了无需手动修改配置文件的原生集成。

OpenCode 版 SuperPowers 插件源码解析:config hook 与消息注入如何工作

SuperPowers 为 OpenCode 提供的适配是一个设计精巧的原生插件,而非简单的文件复制。其核心功能集中在 .opencode/plugins/superpowers.js 这个文件中,通过 OpenCode 的插件系统,在不侵入用户 opencode.json 配置文件的前提下,自动完成技能注册和关键上下文的注入。理解这两个核心机制,是明白 SuperPowers 如何与 OpenCode 实现无缝集成的关键。

插件入口与路径解析

插件通过一个异步函数 SuperpowersPlugin 导出。启动时,它会通过以下方式确定几个关键路径:

const __dirname = path.dirname(fileURLToPath(import.meta.url));
// ...
const superpowersSkillsDir = path.resolve(__dirname, '../../skills');

__dirname 指向插件文件所在的目录(即 .opencode/plugins/),由此向上两级目录定位到仓库根目录下的 skills 文件夹。这种相对路径计算方式保证了插件无论被安装到哪个具体位置,都能准确找到自带的技能库。同时,它使用 normalizePath 函数处理了 ~OPENCODE_CONFIG_DIR 环境变量的情况,增强了跨环境的健壮性。

核心机制一:config hook 自动注册技能目录

第一个核心机制是 config 钩子。它的作用是静默地将 SuperPowers 技能目录添加到 OpenCode 的技能搜索路径中,从而替代了旧版需要手动创建符号链接或修改 opencode.json 配置文件的操作。

钩子函数的代码如下:

config: async (config) => {
  config.skills = config.skills || {};
  config.skills.paths = config.skills.paths || [];
  if (!config.skills.paths.includes(superpowersSkillsDir)) {
    config.skills.paths.push(superpowersSkillsDir);
  }
},

这段代码的执行逻辑是:

  1. 防御性初始化:确保 config.skillsconfig.skills.paths 对象/数组存在。
  2. 去重添加:检查 superpowersSkillsDir 是否已在路径列表中,如果不存在则添加。
  3. 修改缓存单例:源码注释指出,Config.get() 返回的是一个缓存的单例对象。因此,对传入 config 对象的修改会立即生效,并在 OpenCode 后续的技能惰性发现过程中被使用。

这意味着,只要插件被加载,OpenCode 的 skill 工具就能自动扫描并列出 skills/ 目录下所有包含 SKILL.md 文件的技能(如 brainstormingusing-superpowers)。开发者无需进行任何额外配置。

核心机制二:experimental.chat.messages.transform 注入引导上下文

第二个核心机制是 experimental.chat.messages.transform 钩子。它的作用是在每个新会话开始时,将关键的工作流引导指令注入到 AI 代理的上下文中,并确保代理遵循 using-superpowers 技能所设定的规范。

注入的时机非常关键:它发生在第一条用户消息生成之后、发送给模型之前。钩子函数会处理会话的消息列表(output.messages),并主要依赖一个模块级的缓存函数 getBootstrapContent() 来构建注入内容。

注入内容的构建与缓存

getBootstrapContent() 负责构建注入文本,它包含两部分:清理后的 using-superpowers 技能正文,以及一段为 OpenCode 环境定制的工具映射说明。

构建步骤:

  1. 读取与清理:读取 using-superpowers 技能的 SKILL.md 文件,并通过 extractAndStripFrontmatter 函数去除 YAML frontmatter,只保留 Markdown 正文。
  2. 生成工具映射:生成一段专属于 OpenCode 环境的工具映射说明,指导代理如何将 Claude Code 中的工具名(如 TodoWriteTask)映射到 OpenCode 的原生工具(如 todowrite@mention 子代理系统)。
  3. 组合并缓存:将清理后的技能正文和工具映射组合成一段包裹在 <EXTREMELY_IMPORTANT> 标签内的文本,并存储在模块级的 _bootstrapCache 变量中。

缓存设计的核心:源码注释和测试都表明,SKILL.md 文件在一个会话期间不会改变。因此,通过 _bootstrapCache 进行模块级缓存,可以确保每次 transform 钩子被触发(OpenCode 在每个代理步骤都可能重新加载消息数组)时,避免重复的 fs.existsSyncfs.readFileSync 和正则解析工作,显著提升效率。相关的 existsCountreadCount 不增长的行为在测试脚本 test-bootstrap-caching.mjs 中得到了验证。

注入操作的逻辑

const firstUser = output.messages.find(m => m.info.role === 'user');
if (!firstUser || !firstUser.parts.length) return;
// 仅注入一次,防止重复
if (firstUser.parts.some(p => p.type === 'text' && p.text.includes('EXTREMELY_IMPORTANT'))) return;
const ref = firstUser.parts[0];
firstUser.parts.unshift({ ...ref, type: 'text', text: bootstrap });

它找到第一条用户消息,并将引导文本作为新的第一个 text 类型部件插入到消息内容的最前面。同时,通过检查 EXTREMELY_IMPORTANT 标记来避免在同一会话中重复注入。

为什么注入用户消息而非系统消息?

源码注释明确指出了这一设计选择的原因:

  1. 避免 Token 浪费:系统消息可能会在每一轮对话中被重复发送,造成不必要的 token 消耗(对应 issue #750)。
  2. 提高模型兼容性:某些模型(如 Qwen)在处理多个系统消息时可能出现问题(对应 issue #894)。将引导内容作为用户消息的一部分,可以更好地兼容各类模型。

设计演进与测试验证

查看 docs/plans/2025-11-22-opencode-support-design.md 中的早期设计,可以发现初始方案计划使用 session.started 事件来注入上下文。而最终实现选择了 experimental.chat.messages.transform,这表明 OpenCode 的插件系统在实际开发中演化出了更精细的对话消息流控制钩子,允许插件在更晚的“消息生成后”阶段介入,实现更可靠的注入。

插件的功能通过 tests/opencode/ 目录下的测试脚本体系进行了全面验证:

  • test-plugin-loading.sh:验证插件文件是否被正确安装和链接,技能目录是否就位,且引导文本不包含硬编码的错误路径。
  • test-tools.sh:验证通过自然语言指令能否成功调用 OpenCode 原生的 skill 工具,并正确加载个人技能和 SuperPowers 技能(如 brainstorming)。
  • test-priority.sh:这是最精细的测试,它通过创建同名但位于不同目录(项目、个人、SuperPowers)的技能,验证了技能解析的优先级逻辑:项目技能 > 个人技能 > SuperPowers 技能。同时,它也测试了使用 superpowers: 前缀可以强制加载特定来源的技能,绕过默认优先级。
  • test-bootstrap-caching.sh 和 .mjs:专门验证引导内容的缓存行为,包括文件存在时的缓存、文件缺失时的哨兵值缓存,确保 existsCountreadCount 在多次 transform 调用中不增长。

结语

OpenCode 版 SuperPowers 插件通过 configexperimental.chat.messages.transform 这两个精妙的钩子,实现了“零配置”集成。config 钩子负责“基础设施”层面,动态扩展 OpenCode 的技能搜索路径;experimental.chat.messages.transform 钩子负责“运行时”层面,在每次对话开始时注入包含核心工作流和平台适配指令的“启动燃料”。这种设计既保持了配置的简洁性,又确保了 AI 代理从第一次交互开始就能遵循 SuperPowers 的开发方法论,例如在写代码前进行头脑风暴、制定编写计划等。

FAQ

Q: 安装 OpenCode 版 SuperPowers 插件后,如何确认技能已正确加载? A: 重启 OpenCode 后,可以直接对 AI 代理说“Tell me about your superpowers”或“用 skill 工具列出所有技能”。如果代理回应中包含 SuperPowers 的相关介绍或列出 brainstormingusing-superpowers 等技能,即表明插件工作正常。

Q: 当项目、个人目录和 SuperPowers 中存在同名技能时,OpenCode 如何决定使用哪一个? A: OpenCode 遵循明确的优先级顺序:项目目录(.opencode/skills/)中的技能 > 用户个人目录(~/.config/opencode/skills/)中的技能 > SuperPowers 插件自带的技能。此外,在请求技能时使用 superpowers: 前缀(如 superpowers:brainstorming)可以强制加载 SuperPowers 版本,使用 project: 前缀可以强制加载项目版本。这为不同场景下的技能覆盖提供了灵活性。