乐于分享
好东西不私藏

OpenCode 插件生态开放:V2 Plugin API 发布了

OpenCode 插件生态开放:V2 Plugin API 发布了

OpenCode 的插件系统从诞生起就一直有个尴尬的问题:能用,但不好用。

不是说功能不够——事件钩子、自定义工具、环境变量注入都有。但写过插件的人都知道,V1 的 API 设计有几个让人难受的地方:

  • 所有钩子都是扁平的,命名靠约定(tool.execute.before),没有命名空间隔离
  • 异步处理全靠手动 Promise,没有统一的生命周期管理
  • 插件卸载时的清理工作无处安放,只能靠开发者自己想办法

v1.17.10(2026 年 6 月 24 日)发布的 V2 Plugin API,一次性解决了上面三个痛点。更关键的是,这次改动不仅是为了"修 API",而是为了撑起一个更大的插件生态——让社区开发者不再觉得写 OpenCode 插件是一件拧巴的事。

V1 的痛点:能用,但难受

先回顾一下 V1 的插件长什么样:

exportconst MyPlugin = async ({ project, client, $, directory }) => {
return {
"tool.execute.before"async (input, output) => {
if (input.tool === "read" && output.args.filePath.includes(".env")) {
thrownewError("不要读取 .env 文件")
      }
    },
"session.created"async (input, output) => {
console.log("新会话创建了")
    },
  }
}

看起来还行?那是你没写过复杂的插件。几个现实问题:

钩子命名靠字符串约定。tool.execute.before 只是一个字符串 key。你写对了就能用,写错了(比如 tool.exec.before)就静默失败,没有类型提示,没有编译检查。直到运行时你才发现钩子没生效。

所有事件混在一起。 一个插件同时监听文件事件、会话事件、工具事件,所有的 handler 混在一个对象里。超过 10 个钩子的插件,可读性迅速下降。

没有类型安全。 你把 input 和 output 的类型搞混了、参数名打错了,不会有人告诉你。全靠你自己记文档。

插件卸载不干净。 如果插件创建了文件监听器、启动了定时器、建立了 WebSocket 连接——这些资源在插件卸载时该谁清理?V1 没有提供任何机制。开发者要么忘记清理导致内存泄漏,要么自己写一些不稳定的清理代码。

这些小问题单个看都不致命,但叠加在一起,劝退了不少潜在的插件开发者。而插件生态,恰恰是一个开源项目从"好用"走向"繁荣"的关键一步。

V2 的三个核心变化

在聊具体 API 之前,先搞清楚一件事:V2 并不是取代 V1 的全新系统,而是在 V1 基础上加了三个重要的抽象层。 V1 的所有插件照常运行,V2 只是在运行时新增了新的注册方式。同一个插件甚至可以混合两种风格。

V2 Plugin API 引入了三个关键设计:Effect 和 Promise 两种模式命名空间钩子dispose 生命周期钩子

1. Effect 和 Promise:两种模式,对应不同场景

V2 引入了两个核心抽象:

  • Effect:有副作用的操作,比如"发送通知"、"写入文件"、"调用外部 API"。插件可以产生 Effect,OpenCode 运行时负责执行
  • Promise:对未来事件的承诺,比如"等这个文件改完了通知我"。插件可以返回 Promise,告诉运行时"我关心这件事"

这其实是把事件驱动编程中两个最基础的原语——"产生事件"和"订阅事件"——显式地抽象出来了。

用 Effect 模式写一个日志插件:

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

exportconst LogPlugin: Plugin = async () => {
return {
"session.created"(input) => {
return effect("log", {
        level: "info",
        message: `会话 ${input.session.id} 已创建`,
      })
    },
"session.status"(input) => {
return effect("log", {
        level: "debug",
        message: `会话状态变更: ${input.status}`,
      })
    },
  }
}

Effect 和 普通 return 的关键区别在于:Effect 是可组合、可追踪的。运行时可以审计所有 Effect,可以在开发模式下显示"这个插件产生了什么 Effect",也可以在插件崩溃时自动重试失败的 Effect。

Promise 模式适合"等一件事完成后再继续"的场景:

exportconst GuardPlugin: Plugin = async () => {
return {
"tool.execute.before"async (input) => {
if (input.tool === "bash" && input.args.command.includes("rm -rf")) {
// 返回 Promise,等待用户确认后再继续
returnnewPromise((resolve) => {
          showConfirmDialog("确认要执行 rm -rf 吗?", resolve)
        })
      }
    },
  }
}

Effect 和 Promise 的组合让插件的处理逻辑更清晰:需要产生作用的用 Effect,需要等待的用 Promise。不再是一个 async 函数一把梭。

2. 命名空间钩子:告别字符串约定

V1 的钩子都是扁平的字符串 key。V2 引入了命名空间钩子:

exportconst MyPlugin: Plugin = async (ctx) => {
return {
// 命名空间分组
    session: {
      created: async (input) => {
console.log("会话创建", input.session.id)
      },
      deleted: async (input) => {
console.log("会话删除", input.session.id)
      },
    },
    tool: {
      execute: {
        before: async (input, output) => {
console.log("工具即将执行", input.tool)
        },
        after: async (input, output) => {
console.log("工具执行完成", input.tool)
        },
      },
    },
    file: {
      edited: async (input) => {
console.log("文件被修改", input.file.path)
      },
    },
  }
}

对比 V1 的 "session.created""session.deleted""tool.execute.before""file.edited"——V2 用嵌套对象天然地做了分组。

好处很明显:

  • 自动补全:你打 session.,编辑器自动提示 createddeletedcompacted 等事件
  • 类型检查session.created 的 input 类型和 file.edited 的 input 类型不同,TypeScript 会自动推导
  • 代码组织:事件按领域分组,一个插件可以同时处理几十个事件而不会一团乱麻

类型安全是最大的收益。V1 时代你写错了钩子名,要到运行时才发现。V2 时代编辑器直接告诉你"这行报错,tool.excute 不存在,你是不是想写 execute?"

迁移成本:如果你有现成的 V1 插件,不需要重写。V2 API 向后兼容 V1 的字符串钩子命名方式。你可以在同一个插件里混合使用:

exportconst HybridPlugin: Plugin = async (ctx) => {
return {
// V1 风格(字符串 key)
"session.created"async (input) => { /* ... */ },
// V2 风格(命名空间)
    session: {
      deleted: async (input) => { /* ... */ },
    },
  }
}

当然,建议新插件直接全用 V2 风格。老插件慢慢迁移不着急。

3. dispose 钩子:插件的"临终关怀"

这是 V1 完全没有的一个能力。

插件的生命周期是:加载 → 运行 → 卸载。V1 只解决了"加载和运行",V2 补上了"卸载"。

exportconst WatcherPlugin: Plugin = async (ctx) => {
// 启动文件监听
const watcher = fs.watch("./src"(event, filename) => {
console.log(`文件变更: ${filename}`)
  })

return {
    dispose: async () => {
// 插件卸载时关闭文件监听
      watcher.close()
console.log("文件监听已清理")
    },
    session: {
      created: async (input) => {
console.log("新会话", input.session.id)
      },
    },
  }
}

dispose 在以下情况下被调用:

  • 插件被用户手动禁用或卸载
  • 插件版本更新,旧版本需要被替换
  • OpenCode 关闭或重启
  • 插件发生不可恢复的错误,运行时决定卸载它

常见的 dispose 场景

资源类型
dispose 中该做什么
文件监听器 (fs.watch)
watcher.close()
定时器 (setInterval)
clearInterval(id)
WebSocket 连接
ws.close()
临时文件
fs.unlinkSync(tmpFile)
事件订阅
取消订阅
子进程
proc.kill()

没有 dispose,这些资源就会泄露。一个插件泄露一点,十个插件加起来就是肉眼可见的内存上涨。V2 补上这个缺口,对于构建一个健康的插件生态来说是必要的。

从 V1 迁移到 V2:具体怎么改

如果你有现成的 V1 插件,迁移到 V2 其实不复杂。基本步骤就三步。

第一步:改导入路径。

// V1
// 没有专门的类型导入
exportconst MyPlugin = async (ctx) => { ... }

// V2
importtype { Plugin } from"@opencode-ai/plugin"
exportconst MyPlugin: Plugin = async (ctx) => { ... }

@opencode-ai/plugin 包是 V2 新增的官方类型包。装不装都可以——不装也能用——但装了能获得完整的 TypeScript 类型提示和编译检查。建议安装。

第二步:把字符串钩子改为命名空间钩子。

// V1
return {
"session.created": handler1,
"session.deleted": handler2,
"tool.execute.before": handler3,
"file.edited": handler4,
}

// V2
return {
  session: {
    created: handler1,
    deleted: handler2,
  },
  tool: {
    execute: {
      before: handler3,
    },
  },
  file: {
    edited: handler4,
  },
}

这是纯机械的转换。把带点的字符串拆成嵌套对象就行。不需要改 handler 内部的逻辑。

第三步:加上 dispose。

如果你插件里创建了需要清理的资源(定时器、文件监听、网络连接),加一个 dispose 钩子。如果没有,这一步可以跳过,不需要硬加。

迁移完后跑一遍,确认所有事件监听都正常工作。V2 向后兼容保证了这个过程可以分批进行——你不必一次改完所有插件,改一个验证一个。

和 SDK 的关系:V2 Plugin API + V2 SDK

v1.17.10 一起发布的还有一批 SDK 的新能力。它们和 V2 Plugin API 是配合的关系:

  • 实时事件订阅流:SDK 新增了 subscribe API,可以在插件外部实时监听 OpenCode 的事件。这让你可以写独立的监控应用——比如一个 Grafana 面板显示 OpenCode 的使用量
  • SDK 访问活跃会话:插件可以通过 client 对象获取当前活跃的会话列表、查看会话状态、中断运行中的会话
  • 分页持久化会话历史:SDK 现在可以获取完整的会话历史,支持分页,不再仅限于当前会话的上下文窗口
  • 会话权限请求端点:可以通过 SDK 创建和查询会话权限请求

这些 SDK 能力的升级让插件开发者可以做更多事情。举个例子:一个"代码审查统计"插件可以这样工作:

exportconst ReviewStatsPlugin: Plugin = async ({ client }) => {
return {
    session: {
      diff: async (input) => {
// 每次用户审查 diff 时,记录审查的文件数和修改行数
await client.app.log({
          body: {
            service: "review-stats",
            level: "info",
            message: "Diff reviewed",
            extra: {
              files: input.files.length,
              additions: input.stats.additions,
              deletions: input.stats.deletions,
            },
          },
        })
      },
      compacted: async () => {
// 会话压缩时,获取完整历史并分析统计数据
const history = await client.sessions.list({ limit: 100 })
        analyzeUsagePatterns(history)
      },
    },
  }
}

对比 V1,V2 的 SDK 提供了会话管理和事件订阅的能力,插件不再局限于"监听钩子 + 做点小事",而是可以主动查询和操作 OpenCode 的运行状态。这为开发类似"会话历史分析"、"代码修改统计"、"团队使用报告"等复杂插件打开了空间。

V2 插件的完整示例

来看一个实际点的例子:一个在 AI 写代码时自动备份被修改文件的插件。

import { type Plugin, effect } from"@opencode-ai/plugin"
import { copyFile, mkdir } from"fs/promises"
import { join } from"path"

exportconst AutoBackupPlugin: Plugin = async ({ directory }) => {
const backupDir = join(directory, ".opencode""backups")

return {
// 插件初始化时创建备份目录
    init: async () => {
await mkdir(backupDir, { recursive: true })
    },
// 文件被编辑前自动备份
    tool: {
      execute: {
        before: async (input) => {
if (input.tool === "write") {
const filePath = input.args.filePath
const timestamp = Date.now()
await copyFile(filePath, join(backupDir, `${timestamp}-${filePath.replace(/\//g"_")}`))
          }
        },
      },
    },
// 清理旧备份(保留最近 50 个)
    file: {
      edited: async () => {
const files = await readdir(backupDir)
if (files.length > 50) {
const sorted = files.sort()
const toDelete = sorted.slice(0, files.length - 50)
for (const f of toDelete) {
await unlink(join(backupDir, f))
          }
        }
      },
    },
// 插件卸载时清理
    dispose: async () => {
console.log("备份插件已卸载")
    },
  }
}

这个插件演示了 V2 的几个核心特性:init 钩子做初始化、嵌套命名空间组织事件、dispose 做清理。对比 V1,你不会需要在 "tool.execute.before" 和 "file.edited" 前面加一堆前缀来区分逻辑了,代码结构自然反应了你的意图。

更复杂的插件模式:几个常见场景

掌握了基础语法后,来看看一些常见的插件场景在 V2 下的写法。

自定义工具 + 权限控制

下面的插件注册了一个自定义工具 search-code,并限制只能在特定目录下使用:

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

exportconst SearchPlugin: Plugin = async (ctx) => {
return {
    tool: {
      search: tool({
        description: "在项目中搜索代码",
        args: {
          query: tool.schema.string().describe("搜索关键词"),
          path: tool.schema.string().optional().describe("限定搜索目录"),
        },
async execute(args) {
const { query, path } = args
const searchDir = path || "."
return`在 ${searchDir} 中搜索 "${query}"...`
        },
      }),
    },
// 限制自定义工具的调用范围
"tool.execute.before"async (input, output) => {
if (input.tool === "search" && output.args.path?.includes("..")) {
thrownewError("不允许越过项目根目录搜索")
      }
    },
  }
}

这里混合了 V2(tool.search)和 V1("tool.execute.before")的风格。V2 目前对自定义工具的注册和限制没有统一的命名空间位置,所以限制逻辑暂时还是用 V1 的字符串钩子。这种混合使用是被支持的。

跨插件通信

V2 虽然没有提供内置的插件间消息通道,但你可以通过文件系统或者环境变量来实现简单的通信:

// 插件 A:监控文件修改频率
exportconst FileMonitorPlugin: Plugin = async ({ directory }) => {
return {
    file: {
      edited: async (input) => {
// 把修改记录写入共享文件
await appendFile(
          join(directory, ".opencode"".file-changes.log"),
`${Date.now()},${input.file.path}\n`
        )
      },
    },
  }
}

// 插件 B:基于文件修改频率决定是否需要清理
exportconst FileCleanupPlugin: Plugin = async ({ directory }) => {
return {
    session: {
      compacted: async () => {
const logPath = join(directory, ".opencode"".file-changes.log")
const entries = (await readFile(logPath, "utf-8")).trim().split("\n")
// 如果最近 10 分钟内改了超过 50 个文件,提示用户考虑压缩
if (entries.length > 50) {
console.log("检测到高频文件修改,建议执行 /compact 节省上下文")
        }
      },
    },
  }
}

跨插件通信是一个高阶需求,V2 目前没有专门的支持。上述的文件共享方式适用于简单场景。如果社区真的有强需求,未来版本可能会加入插件间消息总线。

中间件模式

V2 的 tool.execute.before 和 tool.execute.after 本质上是 AOP(面向切面编程)的 before/after 拦截器。你可以利用它实现类似"中间件链"的效果:

exportconst MiddlewarePlugin: Plugin = async () => {
return {
    tool: {
      execute: {
// before:对所有工具调用计时
        before: async (input) => {
          input._startTime = Date.now()
        },
// after:输出每个工具的耗时
        after: async (input, output) => {
const duration = Date.now() - (input._startTime || Date.now())
console.log(`工具 ${input.tool} 耗时 ${duration}ms`)
// 如果工具超时(超过 30s),发出警告
if (duration > 30000) {
console.warn(`警告:${input.tool} 执行超过 30 秒`)
          }
        },
      },
    },
  }
}

这种模式对于做性能监控、使用量统计、审计日志非常有用。

实战:从零写一个发布到 npm 的插件

掌握了语法后,看看怎么把一个插件发布出去让别人用。

搭建项目

mkdir opencode-my-plugin
cd opencode-my-plugin
npm init -y
npm install @opencode-ai/plugin typescript

配置 package.json

{
"name""opencode-my-plugin",
"version""1.0.0",
"type""module",
"main""dist/index.js",
"types""dist/index.d.ts",
"files": ["dist"],
"scripts": {
"build""tsc"
  }
}

写插件

// src/index.ts
importtype { Plugin } from"@opencode-ai/plugin"

exportconst MyPlugin: Plugin = async (ctx) => {
return {
    session: {
      created: async (input) => {
console.log(`会话 ${input.session.id} 已开始`)
      },
      deleted: async (input) => {
console.log(`会话 ${input.session.id} 已结束`)
      },
    },
  }
}

构建并发布

npm run build
npm publish

用户安装时只需要在 opencode.json 里加上一行:

{
"plugin": ["opencode-my-plugin"]
}

OpenCode 自动通过 Bun 安装并加载。不需要用户手动下载文件、解压、放到某个目录。这也是 V2 想要达到的效果——让插件的安装像 VS Code 一样简单:一行配置,自动安装,即装即用。

如果你不想走 npm 流程,也可以直接把 .ts 文件放到 .opencode/plugins/ 目录下。OpenCode 自动加载该目录下的所有插件文件,支持 TypeScript 源码直接运行。

V2 对生态的意义

说完了技术细节,说说更大的图景。

OpenCode 在 GitHub 上已经有 179K stars,900+ 贡献者,750 万月活开发者。但插件的数量一直不多——截至 v1.17.10,官方生态系统里登记的插件只有几十个。对比 VS Code 数万个扩展的数量级,差距非常大。

根本原因不复杂:写 OpenCode 插件一直不够爽。V1 的 API 设计让开发者感觉是在"凑合着用",而不是"享受写插件的过程"。V2 的目的就是降低这个门槛。

三个关键信号:

npm 包可以一键安装。 指定 "plugin": ["opencode-helicone-session"],OpenCode 自动帮你用 Bun 安装并缓存。和 VS Code 的 ext install 体验类似。V2 没有改变安装方式,但更好的 API 意味着更多人愿意去 npm 上发布插件——开发者先有动力写,才有东西可以装。

现有插件可以直接迁移。 目前官方的生态系统里登记的插件包括:

  • opencode-wakatime:自动记录编码时间到 WakaTime
  • opencode-helicone-session:将 OpenCode 会话数据同步到 Helicone 做分析
  • opencode-diff:增强的 Diff 查看器,支持行级接受/拒绝
  • opencode-diff-viewer:和 VS Code 集成的 Diff 追踪插件

这些都是 V1 时期的插件。V2 发布后,它们都可以逐步迁移以利用命名空间钩子和 dispose。更重要的是,V2 降低了新插件的开发门槛,生态会进入一个加速增长的阶段。

对比 VS Code 的发展历程:2015 年 VS Code 刚发布时只有几十个扩展。直到 2016 年扩展 API 稳定并引入 Yeoman 脚手架后,扩展数量才开始指数级增长。OpenCode 的 V2 Plugin API 很可能就是这个引爆点。

写插件前需要考虑的问题

虽然有 V2 API 的加持,但不是所有功能都适合写成插件。判断一个需求适不适合插件,可以问自己三个问题:

这个功能需要监听事件吗? 如果只是"在 AI 做某件事时顺带做另一件事",那它适合写成插件。如果是一个独立的工具(比如把 Markdown 转成 HTML),应该做成一个独立的 CLI 工具,通过 OpenCode 的自定义工具来调用。

你需要清理资源吗? 如果插件不创建任何长期资源(只是纯函数式的监听和处理),V1 和 V2 对你来说区别不大。如果你要开 WebSocket、监听文件、定时轮询,那 V2 的 dispose 是必要的。

你的用户需要复杂配置吗? 如果插件需要用户填写 API Key、选择目录、配置黑白名单,最好设计一个 opencode-my-plugin.json 配置文件放在项目根目录,而不是把配置硬编码在 opencode.json 的 plugin 数组里。

TypeScript 类型全覆盖。 V2 API 从设计时就把类型安全作为一级需求。命名空间钩子、Effect 类型、Promise 类型都有完整的 TypeScript 声明。V1 时代你需要到处 @ts-ignore,V2 时代你写完代码只要不报红,基本没错。

生命周期完整闭环。 从 init 到事件处理到 dispose,插件有了完整的生命周期管理。这对复杂的插件(比如集成外部 API、开 WebSocket、管理子进程)来说是必要条件。只有在生命周期完整的情况下,开发者才敢写"真正有用的复杂插件"。

一些需要注意的地方

V2 API 还不够稳定。 文档上标注的还是实验性的。命名空间钩子的具体结构在后续版本中可能会微调。如果你准备写生产插件,建议锁定 OpenCode 版本,或者关注 changelog 中的 breaking change 通知。

部分 V1 事件还没有 V2 等价物。 比如 experimental.session.compacting(会话压缩钩子)目前还没有对应的命名空间位置。如果你在用这个钩子,保持 V1 风格。等后续版本补齐。

自定义工具 API 保持兼容。tool() 辅助函数在 V2 中没有变化。如果你之前写过自定义工具插件,不需要改动。

插件加载顺序需要注意。 V2 没有改变加载顺序:全局 opencode.json → 项目 opencode.json → 全局插件目录 → 项目插件目录。如果你的插件依赖另一个插件的输出,需要考虑这个顺序。

迁移节奏建议: V1 插件不用急着改。V2 目前是实验性的,等到它正式稳定(预计 v1.18 或 v1.19)再做系统迁移。期间新开发的插件优先使用 V2 风格,积累经验。

展望:V2 之后还会有什么?

从 OpenCode 的版本节奏来看——2026 年 5 月到 7 月就迭代了 13 个小版本——插件系统还会继续演化。几个可能的方向:

  • 插件间通信 API:如果跨插件通信的需求足够强烈,未来可能会加入正式的插件间消息通道
  • UI 扩展能力:目前插件只能通过控制台输出做交互(console.log / toast 通知),无法添加自定义 UI 组件。如果 Desktop 版本普及,UI 扩展插件的需求会更突出
  • 插件市场:在 opencode.ai 上构建一个插件市场,让用户可以直接搜索和安装插件,不需要手动编辑 JSON 配置文件
  • 更细粒度的权限模型:插件可以声明需要的权限(网络访问、文件系统读写、执行命令),用户可以在安装时审查和限制

当然这些只是推测。V2 Plugin API 的发布已经是一个明确的信号:OpenCode 团队意识到插件生态的重要性,并开始认真投入。 这比 API 本身具体长什么样更重要。

总结

V2 Plugin API 不是一次革命性的重写,而是一次有诚意的优化。它没有推翻 V1 的设计(保持了向后兼容),但在开发者体验上有了质的提升:

  • Effect + Promise 模式让插件的意图更清晰:产生作用的写 Effect,等待结果的写 Promise
  • 命名空间钩子结束了字符串命名的黑暗时代,类型安全让写插件变得从容
  • dispose 钩子补上了插件的生命周期缺口,资源泄漏不再是写插件的心理负担

对于插件开发者:如果你一直在观望 OpenCode 的插件系统,现在是个好时机。V2 API 的 TypeScript 体验比 V1 好一个档次。

对于普通用户:短期内你的使用体验不会有明显变化,但如果插件生态因此繁荣起来,长期受益者是你。

参考资料:

  • OpenCode v1.17.10 Release(V2 Plugin API 发布) - https://github.com/anomalyco/opencode/releases/tag/v1.17.10
  • OpenCode 插件开发文档 - https://opencode.ai/docs/plugins/
  • OpenCode 生态系统的社区插件 - https://opencode.ai/docs/ecosystem
  • OpenCode SDK 文档 - https://opencode.ai/docs/sdk/