opencode 插件系统
什么是插件
插件是 JavaScript/TypeScript 模块,通过钩子(Hook) 订阅 opencode 内部事件(Event) ,从而扩展或修改 opencode 的默认行为。常见用途包括:
- 监听会话状态发送通知
- 拦截工具执行(如阻止读取
.env文件) - 注入环境变量
- 添加自定义工具供 LLM 调用
- 自定义上下文压缩策略
插件 & 钩子 & 事件的关系
Event(事件)
└─ opencode 内部触发(如 session.idle、tool.execute.before)
Hook(钩子)
└─ 插件注册的事件监听函数
└─ 可读取/修改事件的 input 和 output
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/。
加载顺序
- 全局配置(
~/.config/opencode/opencode.json) - 项目配置(
opencode.json) - 全局插件目录(
~/.config/opencode/plugins/) - 项目插件目录(
.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 | 当前工作目录 |
worktree | Git worktree 路径 |
client | opencode 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 钩子
| 钩子 | input | output | 说明 |
|---|---|---|---|
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 钩子
| 钩子 | input | output | 说明 |
|---|---|---|---|
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 钩子
| 钩子 | input | output | 说明 |
|---|---|---|---|
permission.ask | Permission | `{ status: "ask" \ | "allow" \ |
command.execute.before | { command, sessionID, arguments } | { parts } | slash 命令执行前,可修改或注入内容 |
Shell 钩子
| 钩子 | input | output | 说明 |
|---|---|---|---|
shell.env | { cwd, sessionID?, callID? } | { env } | 每次 shell 执行前,注入环境变量 |
特殊钩子
| 钩子 | 说明 |
|---|---|
event | 订阅系统 SSE 事件(见下方系统事件列表) |
config | 修改全局配置,返回合并后的 Config |
tool | 注册自定义工具供 LLM 调用 |
auth | 注册自定义提供商认证方式(支持 OAuth / API Key) |
provider | 注册自定义提供商及其模型列表 |
实验性钩子
| 钩子 | input | output | 说明 |
|---|---|---|---|
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.executed | slash 命令执行完成后 |
File 事件
| 事件 | 说明 |
|---|---|
file.edited | 文件被编辑 |
file.watcher.updated | 文件监听器检测到变化 |
Installation 事件
| 事件 | 说明 |
|---|---|
installation.updated | opencode 安装/更新 |
LSP 事件
| 事件 | 说明 |
|---|---|
lsp.client.diagnostics | LSP 诊断信息(如代码错误/警告) |
lsp.updated | LSP 服务器状态更新 |
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.updated | Todo 列表更新 |
TUI 事件
| 事件 | 说明 |
|---|---|
tui.prompt.append | TUI 提示词被追加文本 |
tui.command.execute | TUI 执行命令 |
tui.toast.show | TUI 显示 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 },
},
})
},
}
}