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);
}
},
这段代码的执行逻辑是:
- 防御性初始化:确保
config.skills和config.skills.paths对象/数组存在。 - 去重添加:检查
superpowersSkillsDir是否已在路径列表中,如果不存在则添加。 - 修改缓存单例:源码注释指出,
Config.get()返回的是一个缓存的单例对象。因此,对传入config对象的修改会立即生效,并在 OpenCode 后续的技能惰性发现过程中被使用。
这意味着,只要插件被加载,OpenCode 的 skill 工具就能自动扫描并列出 skills/ 目录下所有包含 SKILL.md 文件的技能(如 brainstorming、using-superpowers)。开发者无需进行任何额外配置。
核心机制二:experimental.chat.messages.transform 注入引导上下文
第二个核心机制是 experimental.chat.messages.transform 钩子。它的作用是在每个新会话开始时,将关键的工作流引导指令注入到 AI 代理的上下文中,并确保代理遵循 using-superpowers 技能所设定的规范。
注入的时机非常关键:它发生在第一条用户消息生成之后、发送给模型之前。钩子函数会处理会话的消息列表(output.messages),并主要依赖一个模块级的缓存函数 getBootstrapContent() 来构建注入内容。
注入内容的构建与缓存
getBootstrapContent() 负责构建注入文本,它包含两部分:清理后的 using-superpowers 技能正文,以及一段为 OpenCode 环境定制的工具映射说明。
构建步骤:
- 读取与清理:读取
using-superpowers技能的SKILL.md文件,并通过extractAndStripFrontmatter函数去除 YAML frontmatter,只保留 Markdown 正文。 - 生成工具映射:生成一段专属于 OpenCode 环境的工具映射说明,指导代理如何将 Claude Code 中的工具名(如
TodoWrite、Task)映射到 OpenCode 的原生工具(如todowrite、@mention子代理系统)。 - 组合并缓存:将清理后的技能正文和工具映射组合成一段包裹在
<EXTREMELY_IMPORTANT>标签内的文本,并存储在模块级的_bootstrapCache变量中。
缓存设计的核心:源码注释和测试都表明,SKILL.md 文件在一个会话期间不会改变。因此,通过 _bootstrapCache 进行模块级缓存,可以确保每次 transform 钩子被触发(OpenCode 在每个代理步骤都可能重新加载消息数组)时,避免重复的 fs.existsSync、fs.readFileSync 和正则解析工作,显著提升效率。相关的 existsCount 和 readCount 不增长的行为在测试脚本 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 标记来避免在同一会话中重复注入。
为什么注入用户消息而非系统消息?
源码注释明确指出了这一设计选择的原因:
- 避免 Token 浪费:系统消息可能会在每一轮对话中被重复发送,造成不必要的 token 消耗(对应 issue #750)。
- 提高模型兼容性:某些模型(如 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:专门验证引导内容的缓存行为,包括文件存在时的缓存、文件缺失时的哨兵值缓存,确保
existsCount和readCount在多次 transform 调用中不增长。
结语
OpenCode 版 SuperPowers 插件通过 config 和 experimental.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 的相关介绍或列出 brainstorming、using-superpowers 等技能,即表明插件工作正常。
Q: 当项目、个人目录和 SuperPowers 中存在同名技能时,OpenCode 如何决定使用哪一个?
A: OpenCode 遵循明确的优先级顺序:项目目录(.opencode/skills/)中的技能 > 用户个人目录(~/.config/opencode/skills/)中的技能 > SuperPowers 插件自带的技能。此外,在请求技能时使用 superpowers: 前缀(如 superpowers:brainstorming)可以强制加载 SuperPowers 版本,使用 project: 前缀可以强制加载项目版本。这为不同场景下的技能覆盖提供了灵活性。