opencode 插件系统

5 阅读6分钟

opencode 插件系统

什么是插件

插件是 JavaScript/TypeScript 模块,通过钩子(Hook)  订阅 opencode 内部事件(Event) ,从而扩展或修改 opencode 的默认行为。常见用途包括:

  • 监听会话状态发送通知
  • 拦截工具执行(如阻止读取 .env 文件)
  • 注入环境变量
  • 添加自定义工具供 LLM 调用
  • 自定义上下文压缩策略

插件 & 钩子 & 事件的关系

Event(事件)
  └─ opencode 内部触发(如 session.idle、tool.execute.before)

Hook(钩子)
  └─ 插件注册的事件监听函数
  └─ 可读取/修改事件的 inputoutput

Plugin(插件)
  └─ 导出一个或多个函数
  └─ 每个函数返回一个 hooks 对象(key 为事件名,value 为处理函数)

简单来说:  插件通过钩子响应事件。事件由 opencode 触发,钩子拦截事件并执行自定义逻辑。


加载插件

方式一:本地文件

将 .js 或 .ts 文件放入以下目录,启动时自动加载:

  • 项目级:.opencode/plugins/
  • 全局:~/.config/opencode/plugins/

方式二:npm 包

在 opencode.json 中配置:

{
  "$schema": "https://opencode.ai/config.json",
  "plugin": ["opencode-helicone-session", "opencode-wakatime", "@my-org/custom-plugin"]
}

npm 插件在启动时由 Bun 自动安装,缓存在 ~/.cache/opencode/node_modules/

加载顺序

  1. 全局配置(~/.config/opencode/opencode.json
  2. 项目配置(opencode.json
  3. 全局插件目录(~/.config/opencode/plugins/
  4. 项目插件目录(.opencode/plugins/

相同名称和版本的 npm 包只加载一次;本地插件与同名 npm 插件各自独立加载。


创建插件

基本结构

export const MyPlugin = async ({ project, client, $, directory, worktree }) => {
  console.log("Plugin initialized!")

  return {
    // 钩子:key 为事件名,value 为处理函数
    "session.idle": async (event) => {
      console.log("Session is idle")
    },
  }
}

插件函数接收的上下文参数:

参数说明
project当前项目信息
directory当前工作目录
worktreeGit worktree 路径
clientopencode SDK 客户端,可与 AI 交互
$Bun Shell API,用于执行 shell 命令

TypeScript 类型支持

import type { Plugin } from "@opencode-ai/plugin"

export const MyPlugin: Plugin = async ({ project, client, $, directory, worktree }) => {
  return {
    // 类型安全的钩子实现
  }
}

使用外部依赖

在配置目录下创建 package.json,opencode 启动时自动运行 bun install

{
  "dependencies": {
    "shescape": "^2.1.0"
  }
}

然后在插件中导入:

import { escape } from "shescape"

export const MyPlugin = async (ctx) => {
  return {
    "tool.execute.before": async (input, output) => {
      if (input.tool === "bash") {
        output.args.command = escape(output.args.command)
      }
    },
  }
}

可用钩子与事件

钩子分两类:

  • 直接钩子:在 Hooks 对象中以具名 key 注册,opencode 在对应时机直接调用,可读写 input/output
  • 系统事件(通过 event 钩子):opencode 广播的 SSE 事件,只读,通过 event.type 区分

直接钩子(Hooks)

Chat 钩子
钩子inputoutput说明
chat.message{ sessionID, agent?, model?, messageID?, variant? }{ message, parts }新消息收到时,可修改消息内容
chat.params{ sessionID, agent, model, provider, message }{ temperature, topP, topK, maxOutputTokens, options }修改发送给 LLM 的参数
chat.headers{ sessionID, agent, model, provider, message }{ headers }修改发送给 LLM 的 HTTP 请求头
Tool 钩子
钩子inputoutput说明
tool.execute.before{ tool, sessionID, callID }{ args }工具执行前,可修改参数或抛出错误阻止执行
tool.execute.after{ tool, sessionID, callID, args }{ title, output, metadata }工具执行后,可修改返回结果
tool.definition{ toolID }{ description, parameters }修改发送给 LLM 的工具定义(描述和参数 schema)
Permission & Command 钩子
钩子inputoutput说明
permission.askPermission`{ status: "ask" \"allow" \
command.execute.before{ command, sessionID, arguments }{ parts }slash 命令执行前,可修改或注入内容
Shell 钩子
钩子inputoutput说明
shell.env{ cwd, sessionID?, callID? }{ env }每次 shell 执行前,注入环境变量
特殊钩子
钩子说明
event订阅系统 SSE 事件(见下方系统事件列表)
config修改全局配置,返回合并后的 Config
tool注册自定义工具供 LLM 调用
auth注册自定义提供商认证方式(支持 OAuth / API Key)
provider注册自定义提供商及其模型列表
实验性钩子
钩子inputoutput说明
experimental.chat.system.transform{ sessionID?, model }{ system: string[] }追加或替换系统提示词
experimental.chat.messages.transform{}{ messages: [{info, parts}] }转换发送给 LLM 的完整消息历史
experimental.session.compacting{ sessionID }{ context: string[], prompt? }会话压缩前,追加上下文或替换压缩提示词
experimental.compaction.autocontinue{ sessionID, agent, model, provider, message, overflow }{ enabled: boolean }压缩完成后控制是否自动续写(默认 true)
experimental.text.complete{ sessionID, messageID, partID }{ text }文本补全时触发

系统事件(通过 event 钩子订阅)

通过 event 钩子监听,用 event.type 区分:

export const MyPlugin = async () => ({
  event: async ({ event }) => {
    switch (event.type) {
      case "session.idle": /* ... */ break
      case "file.edited":  /* ... */ break
    }
  },
})
Command 事件
事件说明
command.executedslash 命令执行完成后
File 事件
事件说明
file.edited文件被编辑
file.watcher.updated文件监听器检测到变化
Installation 事件
事件说明
installation.updatedopencode 安装/更新
LSP 事件
事件说明
lsp.client.diagnosticsLSP 诊断信息(如代码错误/警告)
lsp.updatedLSP 服务器状态更新
Message 事件
事件说明
message.part.removed消息片段被移除
message.part.updated消息片段更新(流式输出每个 chunk)
message.removed消息被删除
message.updated消息整体更新
Permission 事件
事件说明
permission.asked权限请求弹出给用户
permission.replied用户回应了权限请求
Server 事件
事件说明
server.connected服务器连接建立
Session 事件
事件说明
session.created会话创建
session.compacted会话上下文压缩完成
session.deleted会话删除
session.diff会话产生文件变更
session.error会话出错
session.idle会话空闲(LLM 响应完成)
session.status会话状态变化(running/idle 等)
session.updated会话属性更新
Todo 事件
事件说明
todo.updatedTodo 列表更新
TUI 事件
事件说明
tui.prompt.appendTUI 提示词被追加文本
tui.command.executeTUI 执行命令
tui.toast.showTUI 显示 toast 通知

插件示例

1. 发送系统通知(macOS)

监听 session.idle 事件(LLM 响应完成),用 AppleScript 发送系统通知:

export const NotificationPlugin = async ({ $ }) => {
  return {
    event: async ({ event }) => {
      if (event.type === "session.idle") {
        await $`osascript -e 'display notification "Session completed!" with title "opencode"'`
      }
      // 也可以监听会话出错
      if (event.type === "session.error") {
        await $`osascript -e 'display notification "Session error!" with title "opencode"'`
      }
    },
  }
}

2. 阻止读取 .env 文件

拦截 tool.execute.before 钩子,在工具执行前检查参数并抛出错误阻止执行:

export const EnvProtection = async () => {
  return {
    // input: { tool, sessionID, callID }
    // output: { args }  — 可修改传给工具的参数
    "tool.execute.before": async (input, output) => {
      if (input.tool === "read" && output.args.filePath.includes(".env")) {
        throw new Error("Do not read .env files")
      }
    },
  }
}

同样可以拦截 bash 工具,对命令进行安全转义:

import { escape } from "shescape"

export const BashEscapePlugin = async () => {
  return {
    "tool.execute.before": async (input, output) => {
      if (input.tool === "bash") {
        output.args.command = escape(output.args.command)
      }
    },
  }
}

3. 工具执行后处理结果

使用 tool.execute.after 对工具输出进行后处理:

export const ToolLoggerPlugin = async ({ client }) => {
  return {
    // input: { tool, sessionID, callID, args }
    // output: { title, output, metadata }
    "tool.execute.after": async (input, output) => {
      await client.app.log({
        body: {
          service: "tool-logger",
          level: "info",
          message: `Tool executed: ${input.tool}`,
          extra: { args: input.args, result: output.output?.slice(0, 100) },
        },
      })
    },
  }
}

4. 注入环境变量

shell.env 钩子在每次 shell 执行(AI 工具调用和用户终端)前触发,可注入任意环境变量:

export const InjectEnvPlugin = async () => {
  return {
    // input: { cwd, sessionID?, callID? }
    // output: { env: Record<string, string> }
    "shell.env": async (input, output) => {
      output.env.MY_API_KEY = "secret"
      output.env.PROJECT_ROOT = input.cwd
      output.env.NODE_ENV = "development"
    },
  }
}

5. 修改 LLM 请求参数

使用 chat.params 钩子动态调整 temperature、maxOutputTokens 等:

import type { Plugin } from "@opencode-ai/plugin"

export const ChatParamsPlugin: Plugin = async () => {
  return {
    // input: { sessionID, agent, model, provider, message }
    // output: { temperature, topP, topK, maxOutputTokens, options }
    "chat.params": async (input, output) => {
      // 对特定模型禁用 maxOutputTokens(如 OpenAI reasoning 模型)
      if (input.model.providerID === "openai" && input.model.capabilities?.reasoning) {
        output.maxOutputTokens = undefined
      }
      // 为代码任务降低随机性
      if (input.agent === "build") {
        output.temperature = 0.2
      }
    },
  }
}

6. 修改 HTTP 请求头

使用 chat.headers 钩子向 LLM 请求添加自定义 Header:

import type { Plugin } from "@opencode-ai/plugin"

export const ChatHeadersPlugin: Plugin = async () => {
  return {
    // input: { sessionID, agent, model, provider, message }
    // output: { headers: Record<string, string> }
    "chat.headers": async (input, output) => {
      // 为 Anthropic 模型启用 interleaved thinking
      if (input.model.api?.npm === "@ai-sdk/anthropic") {
        output.headers["anthropic-beta"] = "interleaved-thinking-2025-05-14"
      }
      // 注入会话追踪 ID
      output.headers["x-session-id"] = input.sessionID
    },
  }
}

7. 修改系统提示词

使用实验性钩子 experimental.chat.system.transform 追加或替换系统提示词:

import type { Plugin } from "@opencode-ai/plugin"

export const SystemPromptPlugin: Plugin = async () => {
  return {
    // input: { sessionID?, model }
    // output: { system: string[] }  — 数组,每项追加到系统提示
    "experimental.chat.system.transform": async (input, output) => {
      output.system.push(`
## 项目规范
- 所有代码必须有单元测试
- 提交前运行 npm run lint
- 使用 TypeScript,禁用 any 类型
      `)
    },
  }
}

8. 自定义权限控制

使用 permission.ask 钩子在用户看到权限弹窗前自动处理:

import type { Plugin } from "@opencode-ai/plugin"

export const AutoPermissionPlugin: Plugin = async () => {
  return {
    // input: Permission 对象
    // output: { status: "ask" | "allow" | "deny" }
    "permission.ask": async (input, output) => {
      // 自动允许读取操作
      if (input.name === "read") {
        output.status = "allow"
      }
      // 自动拒绝删除操作
      if (input.name === "bash" && input.pattern?.includes("rm -rf")) {
        output.status = "deny"
      }
    },
  }
}

9. 自定义上下文压缩

使用 experimental.session.compacting 在会话压缩前注入额外上下文,或完全替换压缩提示词:

import type { Plugin } from "@opencode-ai/plugin"

export const CompactionPlugin: Plugin = async (ctx) => {
  return {
    // input: { sessionID }
    // output: { context: string[], prompt?: string }
    "experimental.session.compacting": async (input, output) => {
      // 方式一:追加额外上下文(常用)
      output.context.push(`
## 需要保留的上下文
- 当前正在实现的功能:用户认证模块
- 关键决策:使用 JWT,过期时间 7 天
- 待完成:编写单元测试
      `)

      // 方式二:完全替换压缩提示词(设置后 output.context 被忽略)
      // output.prompt = `
      // 你正在为多 Agent 协作会话生成续写提示词。
      // 请总结:当前任务状态、修改的文件、下一步计划。
      // `
    },
  }
}

10. 自定义工具

import { type Plugin, tool } from "@opencode-ai/plugin"

export const CustomToolsPlugin: Plugin = async (ctx) => {
  return {
    tool: {
      // 工具名为 "search_docs"
      search_docs: tool({
        description: "搜索项目文档",
        args: {
          query: tool.schema.string().describe("搜索关键词"),
          limit: tool.schema.number().optional().describe("返回条数,默认 5"),
        },
        async execute(args, context) {
          const { directory } = context
          // 实现搜索逻辑...
          return `在 ${directory} 中搜索 "${args.query}" 的结果`
        },
      }),
    },
  }
}

注意:  自定义工具与内置工具同名时,自定义工具优先。


11. 结构化日志

使用 client.app.log() 替代 console.log,日志会集成到 opencode 日志系统:

export const MyPlugin = async ({ client }) => {
  await client.app.log({
    body: {
      service: "my-plugin",
      level: "info",  // debug | info | warn | error
      message: "Plugin initialized",
      extra: { version: "1.0.0" },
    },
  })

  return {
    "session.created": async ({ event }) => {
      await client.app.log({
        body: {
          service: "my-plugin",
          level: "debug",
          message: "New session created",
          extra: { sessionID: event.properties?.id },
        },
      })
    },
  }
}