在前面几篇文章中,我们陆续介绍了 OpenCode 的规则配置、Skills 技能系统、自定义工具和 MCP 服务器。这些都是扩展 OpenCode 行为的方式。但如果你想以一种更底层、更灵活的方式介入 OpenCode 的运行流程——比如在每次工具执行前后加入自定义逻辑、监听会话状态、注入环境变量,甚至在 AI 对话被压缩时插入自己的上下文——那么插件系统就是你需要的武器。
OpenCode 的插件系统基于 JavaScript/TypeScript,允许开发者通过事件钩子介入 OpenCode 的各个生命周期节点。本文将从零开始,带你掌握 OpenCode 插件的使用和开发。
在动手之前,先理清插件和其他扩展方式的区别:
| 扩展方式 | 适用场景 | 运行时机 |
|---------|---------|---------|
| AGENTS.md 规则 | 定义 AI 的行为准则和编码规范 | 每次对话注入系统提示 |
| Skills 技能 | 封装可复用的工作流模板 | 用户主动调用 |
| Custom Tools | 添加新的工具供 AI 调用 | AI 决定调用时 |
| MCP 服务器 | 集成外部服务的标准化工具协议 | 通过工具协议被调用 |
| Plugins 插件 | 介入 OpenCode 内部事件流 | 事件触发时自动执行 |
插件最大的特点是被动响应——你不需要主动调用,它会在特定事件发生时自动执行。这使得插件非常适合做自动化、监控、安全防护等场景。
插件有两种加载方式:本地文件和 npm 包。
将 JavaScript 或 TypeScript 文件放入插件目录即可自动加载:
.opencode/plugins/ — 项目级别~/.config/opencode/plugins/ — 全局级别OpenCode 启动时会自动扫描并加载这些目录下的所有文件。
在 opencode.json 中指定 npm 包名:
{
"$schema": "https://opencode.ai/config.json",
"plugin": [
"opencode-wakatime",
"@my-org/custom-plugin"
]
}
npm 插件会在启动时使用 Bun 自动安装,依赖缓存在 ~/.cache/opencode/node_modules/。
插件的加载遵循以下优先级:
全局配置中的 npm 插件(~/.config/opencode/opencode.json)
项目配置中的 npm 插件(opencode.json)
全局插件目录(~/.config/opencode/plugins/)
项目插件目录(.opencode/plugins/)
所有插件的钩子函数会按序执行。同名的 npm 包不会重复加载,但本地插件和 npm 插件即使名称相似也会分别加载。
一个插件是一个导出异步函数的模块,函数接收上下文对象,返回钩子对象:
// .opencode/plugins/my-plugin.js
export const MyPlugin = async ({ project, client, $, directory, worktree }) => {
console.log("插件已初始化!")
return {
// 钩子实现放在这里
}
}
上下文参数说明:
project:当前项目信息directory:当前工作目录worktree:Git 工作树路径client:OpenCode SDK 客户端,可用于日志、与 AI 交互$:Bun 的 Shell API,可直接执行系统命令如果你使用 TypeScript,可以从 @opencode-ai/plugin 导入类型定义:
// .opencode/plugins/my-plugin.ts
import type { Plugin } from "@opencode-ai/plugin"
export const MyPlugin: Plugin = async (ctx) => {
return {
// 类型安全的钩子实现
}
}
本地插件如需使用外部 npm 包,在 .opencode/ 目录下创建 package.json:
{
"dependencies": {
"shescape": "^2.1.0"
}
}
OpenCode 启动时会自动执行 bun install,之后插件就可以正常 import 这些依赖了:
import { escape } from "shescape"
export const SafeShellPlugin: Plugin = async (ctx) => {
return {
"tool.execute.before": async (input, output) => {
if (input.tool === "bash") {
output.args.command = escape(output.args.command)
}
},
}
}
这个例子展示了插件的典型用法:在 Bash 命令执行前,用 shescape 对命令进行安全转义。
OpenCode 插件提供了丰富的事件钩子,覆盖了从会话生命周期到工具执行的方方面面。
最常用的钩子,让你在工具执行前后介入:
tool.execute.before — 工具执行前触发,可修改参数或阻止执行tool.execute.after — 工具执行后触发,可处理执行结果export const ToolMonitorPlugin = async () => {
return {
"tool.execute.before": async (input, output) => {
console.log(`即将执行工具: ${input.tool}`)
console.log(`参数:`, output.args)
},
"tool.execute.after": async (input) => {
console.log(`工具 ${input.tool} 执行完成`)
},
}
}
跟踪整个对话会话的状态变化:
session.created — 创建新会话session.idle — 会话进入空闲状态(AI 完成响应)session.compacted — 对话被压缩(上下文过长时)session.error — 会话出错session.deleted — 会话被删除session.diff — 会话产生代码变更session.status — 会话状态变更session.updated — 会话数据更新export const SessionTracker = async () => {
return {
event: async ({ event }) => {
switch (event.type) {
case "session.created":
console.log("新会话已创建")
break
case "session.idle":
console.log("AI 响应完成,等待新指令")
break
case "session.error":
console.error("会话出错:", event.payload)
break
}
},
}
}
监听对话消息的变化:
message.updated — 消息更新message.removed — 消息被移除message.part.updated — 消息部分更新message.part.removed — 消息部分移除file.edited — 文件被编辑file.watcher.updated — 文件监视器检测到变更shell.env — 环境变量注入点,可向所有 shell 执行注入自定义环境变量export const InjectEnvPlugin = async () => {
return {
"shell.env": async (input, output) => {
output.env.MY_API_KEY = "my-secret-key"
output.env.PROJECT_ROOT = input.cwd
},
}
}
这个钩子非常实用——你可以将敏感的 API Key 通过插件注入,而不是硬编码在配置文件中。
permission.asked — 弹出权限询问时触发permission.replied — 用户响应权限询问后触发tui.prompt.append — 向输入框追加内容tui.command.execute — 执行 TUI 命令tui.toast.show — 显示提示消息lsp.client.diagnostics — 收到 LSP 诊断信息lsp.updated — LSP 状态更新command.executed — 斜杠命令被执行todo.updated — 任务列表更新installation.updated — 安装状态更新server.connected — 连接到服务器当 AI 完成响应时发送系统通知,让你不用一直盯着终端:
// .opencode/plugins/notification.js
export const NotificationPlugin = async ({ $ }) => {
return {
event: async ({ event }) => {
if (event.type === "session.idle") {
// macOS
await $`osascript -e 'display notification "OpenCode 响应完成" with title "OpenCode"'`
// Linux (需要 notify-send)
// await $`notify-send "OpenCode" "响应完成"`
// Windows
// await $`powershell -Command "New-BurntToastNotification -Text 'OpenCode 响应完成'"`
}
},
}
}
防止 AI 意外读取 .env 等敏感文件:
// .opencode/plugins/env-protection.js
export const EnvProtection = async () => {
return {
"tool.execute.before": async (input, output) => {
if (input.tool === "read" && output.args.filePath.includes(".env")) {
throw new Error("禁止读取 .env 文件")
}
if (input.tool === "read" && output.args.filePath.includes(".secret")) {
throw new Error("禁止读取密钥文件")
}
},
}
}
抛出的错误会被 OpenCode 捕获,AI 会收到一个工具执行失败的反馈,而不是文件内容。
通过插件注册全新的工具供 AI 调用:
// .opencode/plugins/custom-tools.ts
import { type Plugin, tool } from "@opencode-ai/plugin"
export const CustomToolsPlugin: Plugin = async (ctx) => {
return {
tool: {
get_weather: tool({
description: "获取指定城市的天气信息",
args: {
city: tool.schema.string(),
},
async execute(args, context) {
const { city } = args
// 这里可以调用天气 API
return `${city}今天晴朗,气温 25°C,适宜编码。`
},
}),
count_lines: tool({
description: "统计项目的代码行数",
args: {
ext: tool.schema.string().optional(),
},
async execute(args, context) {
const { $ } = context
const ext = args.ext || "ts"
const result = await $`find . -name "*.${ext}" -exec cat {} + | wc -l`.text()
return `项目中共有 ${result.trim()} 行 .${ext} 代码`
},
}),
},
}
}
使用 tool() 辅助函数创建工具,参数通过 Zod schema 定义。注册后,AI 可以像调用内置工具一样调用这些自定义工具。
使用 OpenCode SDK 进行结构化日志记录:
// .opencode/plugins/logger.ts
import type { Plugin } from "@opencode-ai/plugin"
export const LoggerPlugin: Plugin = async ({ client }) => {
let sessionStart = Date.now()
return {
event: async ({ event }) => {
if (event.type === "session.idle") {
const duration = Date.now() - sessionStart
await client.app.log({
body: {
service: "session-logger",
level: "info",
message: "会话完成",
extra: {
duration_ms: duration,
duration_readable: `${(duration / 1000).toFixed(1)}s`,
},
},
})
sessionStart = Date.now()
}
},
}
}
日志支持 debug、info、warn、error 四个级别。
当对话上下文过长触发压缩时,注入自定义上下文保持状态不丢失:
// .opencode/plugins/compaction.ts
import type { Plugin } from "@opencode-ai/plugin"
export const CompactionPlugin: Plugin = async (ctx) => {
return {
"experimental.session.compacting": async (input, output) => {
output.context.push(`## 任务状态总结
当前任务:${ctx.project?.name || "未命名"}
- 正在处理的文件列表
- 已做出的重要决策
- 需要继续的下一步工作
请在压缩后的上下文中保留这些关键信息。`)
},
}
}
通过 experimental.session.compacting 钩子,你可以在压缩发生时注入额外的上下文,确保关键信息不会因为压缩而丢失。你甚至可以完全替换压缩提示词:
output.prompt = `你正在为一个多 Agent 协作会话生成续接提示。 请总结: 1. 当前任务及进度 2. 正在修改的文件 3. 任何阻塞或依赖 4. 后续步骤 格式化为结构化提示。`
插件中的错误不要静默吞掉。在关键钩子中使用 try/catch 并记录日志:
"tool.execute.before": async (input, output) => {
try {
// 你的逻辑
} catch (err) {
console.error("插件处理失败:", err)
// 不要重新抛出,避免中断 OpenCode 运行
}
}
钩子函数会在高频事件中执行,要避免重量级操作。特别是 tool.execute.before 和 tool.execute.after,每次工具调用都会触发。如果需要做耗时操作,考虑异步执行并不阻塞主流程。
为导出的函数使用有意义的名称。这不仅方便调试,多人协作时也能避免命名冲突。
即使你习惯于写 JavaScript,也建议用 TypeScript 编写插件。类型定义能帮你避免很多运行时错误,且 OpenCode 原生支持 .ts 文件。
OpenCode 的插件系统为开发者提供了一种强大而灵活的扩展方式。通过事件驱动架构,你可以:
结合之前介绍的 Skills、规则配置和自定义命令,你现在已经掌握了 OpenCode 完整的扩展体系。合理组合这些能力,你可以将 OpenCode 打造成一个高度定制化、完全适配你工作流的 AI 编程助手。
下一步,不妨从一个小功能开始——比如做一个文件变更通知插件,或者为你的团队做一个统一的敏感文件保护插件——然后发布到 npm 上,分享给 OpenCode 社区。