OpenCode 的插件系统:按需定制,自由扩展
用过 OpenCode 的人可能都有这种感觉这个 AI 编程助手已经很能打了但总有一些场景想让它再“聪明”一点点或者跟某些内部工具对接一下。官方显然也想到了这一点所以留了一个相当灵活的插件机制。这篇文章就从第三方的视角把 OpenCode 的插件系统从头到尾捋一遍。插件能干什么简单说插件就是一些可以挂载到 OpenCode 各个生命周期里的代码片段。无论是想在 AI 执行某个工具之前做点手脚还是想监听会话状态的变化或者干脆给 OpenCode 加一个全新的工具都可以通过插件来实现。社区里已经有不少现成的例子也可以自己动手写。加载插件的方式目前有两种途径把插件交给 OpenCode。本地文件在特定目录里放 JavaScript 或 TypeScript 文件就行了。OpenCode 启动时会自动扫描并加载它们项目级插件.opencode/plugins/全局插件~/.config/opencode/plugins/这两个目录下的文件都会被加载不用额外配置。npm 包另一种方式是在配置文件里指明 npm 包的名字。配置文件叫opencode.json放在项目根目录或者全局配置目录里都行。下面是一个例子{$schema:https://opencode.ai/config.json,plugin:[opencode-helicone-session,opencode-wakatime,my-org/custom-plugin]}普通的 npm 包和 scoped 包都支持。官方还维护了一个生态页面可以去看看有哪些可用的插件。插件是怎么安装的npm 插件在启动时会通过 Bun 自动安装。所有包以及它们的依赖都会被缓存到~/.cache/opencode/node_modules/里所以不用担心重复下载。本地插件直接从指定目录加载。如果本地插件需要引用外部的 npm 包就得在配置目录里自己放一个package.json文件后面会讲到依赖管理或者干脆把插件发布到 npm 上再用配置的方式引入。加载顺序插件会从多个来源被加载所有钩子函数按顺序执行。具体顺序是这样的全局配置文件~/.config/opencode/opencode.json项目配置文件opencode.json全局插件目录~/.config/opencode/plugins/项目插件目录.opencode/plugins/这里有个细节如果多个来源里出现了同一个 npm 包名称和版本都相同只会被加载一次。但一个本地插件和一个同名的 npm 插件会分别被加载不会合并。动手写一个插件一个插件其实就是一个 JavaScript 或 TypeScript 模块里面导出一个或多个插件函数。每个函数接收一个上下文对象然后返回一个钩子对象。依赖管理本地插件或者自定义工具如果要使用第三方的 npm 包可以在配置目录里添加一个package.json把需要的依赖写进去。比如.opencode/package.json{dependencies:{shescape:^2.1.0}}OpenCode 启动时会自动运行bun install安装这些依赖。之后在插件里就能正常 import 了.opencode/plugins/my-plugin.tsimport{escape}fromshescapeexportconstMyPluginasync(ctx){return{tool.execute.before:async(input,output){if(input.toolbash){output.args.commandescape(output.args.command)}},}}基本结构一个最简单的插件文件长这样.opencode/plugins/example.jsexportconstMyPluginasync({project,client,$,directory,worktree}){console.log(Plugin initialized!)return{// 这里写具体的钩子实现}}插件函数接收的参数解释一下project当前项目的信息directory当前工作目录worktreegit worktree 的路径clientOpenCode SDK 客户端可以用来和 AI 交互$Bun 的 shell API用于执行命令TypeScript 支持如果更喜欢 TypeScript可以从插件包里导入类型定义importtype{Plugin}fromopencode-ai/pluginexportconstMyPlugin:Pluginasync({project,client,$,directory,worktree}){return{// 类型安全的钩子实现}}有哪些事件可以钩插件可以订阅的事件种类相当多这里列几个主要的命令事件command.executed文件事件file.edited、file.watcher.updated安装事件installation.updatedLSP 事件lsp.client.diagnostics、lsp.updated消息事件message.part.removed/updated、message.removed/updated权限事件permission.asked、permission.replied服务器事件server.connected会话事件session.created、session.compacted、session.deleted、session.diff、session.error、session.idle、session.status、session.updated待办事件todo.updatedShell 事件shell.env工具事件tool.execute.after、tool.execute.beforeTUI 事件tui.prompt.append、tui.command.execute、tui.toast.show几个实用的插件例子光说不练假把式来看看几个真实的插件场景。发送系统通知有时候 AI 任务跑完了人可能正切到别的窗口。这时候发个系统通知就很贴心了。用 macOS 的osascript可以做到.opencode/plugins/notification.jsexportconstNotificationPluginasync({project,client,$,directory,worktree}){return{event:async({event}){if(event.typesession.idle){await$osascript -e display notification Session completed! with title opencode}},}}顺带一提如果用的是 OpenCode 桌面版它本身就能在响应准备好或会话出错时自动发送系统通知算是个偷懒的好办法。保护 .env 文件有些敏感文件不想让 AI 读取比如.env。可以在read工具执行之前拦截下来.opencode/plugins/env-protection.jsexportconstEnvProtectionasync({project,client,$,directory,worktree}){return{tool.execute.before:async(input,output){if(input.toolreadoutput.args.filePath.includes(.env)){thrownewError(Do not read .env files)}},}}注入环境变量有时候需要给所有的 shell 执行包括 AI 工具和用户终端注入一些环境变量.opencode/plugins/inject-env.jsexportconstInjectEnvPluginasync(){return{shell.env:async(input,output){output.env.MY_API_KEYsecretoutput.env.PROJECT_ROOTinput.cwd},}}添加自定义工具这是插件系统最强大的地方之一可以给 OpenCode 增加全新的工具。比如创建一个叫mytool的自定义工具.opencode/plugins/custom-tools.tsimport{typePlugin,tool}fromopencode-ai/pluginexportconstCustomToolsPlugin:Pluginasync(ctx){return{tool:{mytool:tool({description:This is a custom tool,args:{foo:tool.schema.string(),},asyncexecute(args,context){const{directory,worktree}contextreturnHello${args.foo}from${directory}(worktree:${worktree})},}),},}}这里的tool辅助函数会帮忙创建工具定义需要提供三部分description工具的作用描述args用 Zod schema 定义参数execute工具被调用时的执行逻辑自定义工具会和内置工具一起出现在 OpenCode 的工具集里。需要注意如果自定义工具的名字跟某个内置工具重了那自定义的会覆盖内置的。结构化日志调试插件的时候建议用client.app.log()而不是console.log这样可以输出结构化的日志.opencode/plugins/my-plugin.tsexportconstMyPluginasync({client}){awaitclient.app.log({body:{service:my-plugin,level:info,message:Plugin initialized,extra:{foo:bar},},})}日志级别支持debug、info、warn、error。定制会话压缩Compaction行为这是一个实验性的钩子叫作experimental.session.compacting。当会话变长需要压缩时这个钩子会在 LLM 生成继续摘要之前触发。可以用它往压缩 prompt 里注入额外的上下文.opencode/plugins/compaction.tsimporttype{Plugin}fromopencode-ai/pluginexportconstCompactionPlugin:Pluginasync(ctx){return{experimental.session.compacting:async(input,output){output.context.push(## Custom Context Include any state that should persist across compaction: - Current task status - Important decisions made - Files being actively worked on)},}}如果想要完全替换默认的压缩 prompt可以直接设置output.prompt.opencode/plugins/custom-compaction.tsimporttype{Plugin}fromopencode-ai/pluginexportconstCustomCompactionPlugin:Pluginasync(ctx){return{experimental.session.compacting:async(input,output){output.promptYou are generating a continuation prompt for a multi-agent swarm session. Summarize: 1. The current task and its status 2. Which files are being modified and by whom 3. Any blockers or dependencies between agents 4. The next steps to complete the work Format as a structured prompt that a new agent can use to resume work.},}}注意一旦设置了output.prompt它会完全取代默认的压缩提示此时output.context数组会被忽略。总的来说OpenCode 的插件系统覆盖了从工具拦截、环境注入到会话定制等方方面面。无论是想在团队里统一工作流还是单纯想给日常开发加点小便利写个插件都不是什么难事。社区里已经有的那些插件也可以直接拿来用或者参考着改一改。上面提到的这些例子基本上就是最常用的几种模式了。