OpenCode 插件系统完全指南:用 JavaScript 扩展你的 AI 编程助手

OpenCode 插件系统完全指南:用 JavaScript 扩展你的 AI 编程助手

引言

在前面几篇文章中,我们陆续介绍了 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 启动时会自动扫描并加载这些目录下的所有文件。

npm 包方式

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 支持

如果你使用 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 事件

  • 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 事件

  • tui.prompt.append — 向输入框追加内容
  • tui.command.execute — 执行 TUI 命令
  • tui.toast.show — 显示提示消息

LSP 事件

  • 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()
      }
    },
  }
}

日志支持 debuginfowarnerror 四个级别。

示例五:对话压缩注入

当对话上下文过长触发压缩时,注入自定义上下文保持状态不丢失:

// .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. 后续步骤

格式化为结构化提示。`

最佳实践

1. 错误处理

插件中的错误不要静默吞掉。在关键钩子中使用 try/catch 并记录日志:

"tool.execute.before": async (input, output) => {
  try {
    // 你的逻辑
  } catch (err) {
    console.error("插件处理失败:", err)
    // 不要重新抛出,避免中断 OpenCode 运行
  }
}

2. 性能意识

钩子函数会在高频事件中执行,要避免重量级操作。特别是 tool.execute.beforetool.execute.after,每次工具调用都会触发。如果需要做耗时操作,考虑异步执行并不阻塞主流程。

3. 插件命名

为导出的函数使用有意义的名称。这不仅方便调试,多人协作时也能避免命名冲突。

4. TypeScript 优先

即使你习惯于写 JavaScript,也建议用 TypeScript 编写插件。类型定义能帮你避免很多运行时错误,且 OpenCode 原生支持 .ts 文件。

总结

OpenCode 的插件系统为开发者提供了一种强大而灵活的扩展方式。通过事件驱动架构,你可以:

  • 监控:跟踪工具调用、会话状态、文件变更
  • 保护:拦截敏感文件读取,注入安全策略
  • 自动化:发送通知、记录日志、注入环境变量
  • 扩展:注册自定义工具,增强 AI 的能力边界

结合之前介绍的 Skills、规则配置和自定义命令,你现在已经掌握了 OpenCode 完整的扩展体系。合理组合这些能力,你可以将 OpenCode 打造成一个高度定制化、完全适配你工作流的 AI 编程助手。

下一步,不妨从一个小功能开始——比如做一个文件变更通知插件,或者为你的团队做一个统一的敏感文件保护插件——然后发布到 npm 上,分享给 OpenCode 社区。