news 2026/9/7 16:33:23

OpenCode V2 Effect 插件 API 详解:hook、transform 与 reload 的完整实践指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
OpenCode V2 Effect 插件 API 详解:hook、transform 与 reload 的完整实践指南

OpenCode V2 Effect 插件 API 详解:hook、transform 与 reload 的完整实践指南

【免费下载链接】opencodeThe open source coding agent.项目地址: https://gitcode.com/GitHub_Trending/openc/opencode

本文基于 OpenCode 仓库中的 V2 Effect 插件 API 文档(packages/plugin/src/v2/effect/README.md)展开,系统讲解如何通过@opencode-ai/plugin/v2/effect中的define定义插件、利用 transform hook 参与 agent/catalog/command 等有状态域的构建、通过 runtime hook 拦截运行时操作,以及使用reload重建域状态。读完后你能够独立编写一个具备 hook 注册、作用域生命周期管理和域重载能力的 V2 插件,并理解其底层的 Scope 语义与重建流程。

一、API 定位:两个进程内能力

Effect 插件 API 为插件提供两个进程内(in-process)能力:

  • hook:在 OpenCode 的扩展点(extension point)上安装行为;
  • reload:针对某个有状态域(stateful domain)重新执行其全部 transform hook。

文档同时明确了一个边界:公共的 server client 将单独暴露,目前有意地不包含在PluginContext。也就是说,V2 Effect 插件当前只处理进程内的扩展点,而不是直接操作远程服务端。

整个 API 的入口与导出定义在 index.ts:

export type { PluginContext } from "./context.js" export { define } from "./plugin.js" export type { Plugin } from "./plugin.js"

即对外只有三个公共符号:definePluginContext类型和Plugin类型。该子路径的导出配置见 package.json:

"./v2/effect": "./src/v2/effect/index.ts"

因此插件的导入方式固定为:

import { define } from "@opencode-ai/plugin/v2/effect"

二、定义一个插件

文档给出的最小插件示例如下:

import { define } from "@opencode-ai/plugin/v2/effect" import { Effect } from "effect" export const Plugin = define({ id: "example", effect: Effect.fn(function* (ctx) { yield* ctx.catalog.transform((catalog) => { catalog.provider.update("example", (provider) => { provider.name = "Example" }) }) }), })

从源码结构看(plugin.ts),define就是一个恒等函数,真正约束插件形状的是Plugin接口:

export interface Plugin<R = Scope.Scope> { readonly id: string readonly effect: (context: PluginContext) => Effect.Effect<void, never, R> }

其中几个要点值得注意:

  • id: string是插件的唯一标识,后续ctx.plugin.remove(id)依赖它来卸载插件;
  • effect接收PluginContext,返回一个 Effect 流程;错误类型固定为never,需求类型默认是Scope.Scope——这直接对应下面第四节的生命周期语义;
  • 插件 setup命令式地注册 hook,不返回任何 hook 对象。文档特别强调了这一点:“Plugin setup registers hooks imperatively. It does not return a hook object.”

插件还提供PluginDomain(plugin.ts),允许运行时动态管理插件集合:

export interface PluginDomain { readonly add: (plugin: Plugin) => Effect.Effect<void> readonly remove: (id: string) => Effect.Effect<void> }

对应ctx.plugin.add(plugin)/ctx.plugin.remove(id)两个操作。

三、PluginContext:插件能拿到什么

PluginContext的完整形状定义在 context.ts:

export interface PluginContext { readonly options: PluginOptions readonly agent: AgentHooks & Reload readonly aisdk: AISDKHooks readonly catalog: CatalogHooks & Reload readonly command: CommandHooks & Reload readonly integration: IntegrationHooks & Reload readonly plugin: PluginDomain readonly reference: ReferenceHooks & Reload readonly skill: SkillHooks & Reload }

按用途可以分成三类:

成员能力说明
options读取配置提供给插件的配置,类型见下文
agent/catalog/command/integration/reference/skilltransform + reload六个有状态域,均支持 transform hook 与reload()
aisdkruntime hook拦截 AI SDK 的sdk/language生成过程
plugin插件域管理add/remove动态装卸插件

其中ctx.options的类型很简单,就是一个只读键值映射(options.ts):

export type PluginOptions = Readonly<Record<string, any>>

也就是说,OpenCode 配置中写在插件名下的 options 会原样透传给effect,由插件自行解释字段含义。

四、注册生命周期:Scope 与 dispose

理解 hook 机制的关键在 registration.ts:

export interface Registration { readonly dispose: Effect.Effect<void> } export interface Reload { readonly reload: () => Effect.Effect<void> } export type Hooks<Spec> = { readonly [Name in keyof Spec]: ( callback: (input: Spec[Name]) => Effect.Effect<void> | void, ) => Effect.Effect<Registration, never, Scope.Scope> }

这解释了文档中的三句描述:

  1. 注册是带作用域的:每个 hook 注册调用返回Effect<Registration, never, Scope.Scope>,即注册结果绑定到一个 EffectScope。这就是为什么 README 说“Registrations are owned by the plugin scope”——插件effect流程所在的作用域持有所有注册项,作用域关闭时它们被自动撤销;
  2. 可以提前撤销Registration自带dispose: Effect<void>,因此一个注册可以在作用域关闭前手动移除(“a registration may also be removed early throughdispose”);
  3. callback 可以同步也可以 Effect 化(input) => Effect<void> | void两种签名都被接受,简单场景直接写同步回调即可。

Hooks<Spec>是一个映射类型:Spec里列出的每个键都变成一个 hook 注册函数。以 agent 域为例(agent.ts):

export type AgentHooks = Hooks<{ transform: AgentDraft }>

所以ctx.agent.transform(callback)就是注册一个接收AgentDraft的转换回调。

五、Transform Hooks:参与有状态域的构建

Transform hook 用于向有状态域贡献状态。文档示例:

yield * ctx.agent.transform((agent) => { agent.update("reviewer", (item) => { item.description = "Reviews code for regressions" item.mode = "subagent" }) })

其重建语义由文档明确给出:OpenCode 在任一 transform 被注册或撤销时重建该域;重建从全新的域状态出发,按注册顺序依次执行所有活跃的 transform。这是一个“全量重建”(rebuild-from-scratch)模型,而不是增量修补——每个 transform 都应当假设自己面对的是初始状态。

可用的 transform hook 按域命名空间组织,共六个:

ctx.agent.transform ctx.catalog.transform ctx.command.transform ctx.integration.transform ctx.reference.transform ctx.skill.transform

从源码结构看,每个域对应一个源文件(agent.ts、catalog.ts、command.ts、integration.ts、reference.ts、skill.ts),hook 的 draft 参数形状定义在其中。

agent 域:AgentDraft

AgentDraft(agent.ts)暴露五个方法,操作对象是 SDK 类型AgentV2Info

export interface AgentDraft { list(): readonly AgentV2Info[] get(id: string): AgentV2Info | undefined default(id: string | undefined): void update(id: string, update: (agent: AgentV2Info) => void): void remove(id: string): void }
  • list()/get(id):枚举与查询当前域中的 agent;
  • default(id?):设置(或清空)默认 agent;
  • update(id, fn):就地修改某个 agent,如示例中的item.descriptionitem.mode = "subagent"
  • remove(id):从域中删除。

catalog 域:CatalogDraft

catalog 域结构最丰富,分为 provider 与 model 两个子面(catalog.ts):

export interface CatalogDraft { readonly provider: { list(): readonly CatalogProviderRecord[] get(providerID: string): CatalogProviderRecord | undefined update(providerID: string, update: (provider: ProviderV2Info) => void): void remove(providerID: string): void } readonly model: { get(providerID: string, modelID: string): ModelV2Info | undefined update(providerID: string, modelID: string, update: (model: ModelV2Info) => void): void remove(providerID: string, modelID: string): void readonly default: { get(): { providerID: string; modelID: string } | undefined set(providerID: string, modelID: string): void } } }

注意provider.list()返回的CatalogProviderRecord同时携带provider: ProviderV2Infomodels: ReadonlyMap<string, ModelV2Info>,方便 transform 一次性查看某 provider 下全部模型。model.default是一个特殊的子对象,支持get()/set()读写默认 provider/model 组合——这是 README 开头示例中catalog.provider.update("example", ...)之外另一个常见的定制点。

六、Runtime Hooks:拦截实时操作

与 transform 不同,runtime hook 不重建域状态,而是拦截“活着的”操作。文档给出的示例是替换 AI SDK 的 provider 实例并接管 language model 生成:

yield * ctx.aisdk.sdk( Effect.fn(function* (event) { if (event.package !== "@ai-sdk/xai") return const mod = yield* Effect.promise(() => import("@ai-sdk/xai")) event.sdk = mod.createXai(event.options) }), ) yield * ctx.aisdk.language((event) => { if (event.model.providerID !== "xai") return event.language = event.sdk.responses(event.model.api.id) })

两个 hook 的 event 形状在 aisdk.ts 中有精确定义:

export type AISDKHooks = Hooks<{ sdk: { readonly model: ModelV2Info readonly package: string readonly options: Record<string, any> sdk?: any } language: { readonly model: ModelV2Info readonly sdk: any readonly options: Record<string, any> language?: LanguageModelV3 } }>

从字段设计可以看出流水线关系:

  • sdkhook 的 event 中sdk字段是可选输出sdk?: any),plugin 按event.package判断是否要接管,然后动态import对应 AI SDK 包并赋给event.sdk
  • languagehook 的 event 里sdk变为必填输入sdk: any),说明它一定运行在sdk解析之后;language是可选输出,类型为@ai-sdk/providerLanguageModelV3
  • 若插件不设置language,系统走默认解析路径;设置后则以插件结果为准。

文档还规定了执行顺序:hooks 按注册顺序串行执行,后面的 hook 能观察到前面 hook 所做的变更(“Hooks run sequentially in registration order. Later hooks observe mutations made by earlier hooks.”)。示例中先注册sdk、再注册language正是利用了这一点——第二个 hook 才能拿到第一个 hook 写入的event.sdk

七、Reload:域重载的正确姿势

当 transform 捕获的外部数据发生变化时,需要重载受影响域。文档示例:

let data = yield * loadCatalog() yield * ctx.catalog.transform((catalog) => { applyCatalog(data, catalog) }) data = yield * loadCatalog() yield * ctx.catalog.reload()

可用的 reload 操作与 transform 一一对应:

ctx.agent.reload() ctx.catalog.reload() ctx.command.reload() ctx.integration.reload() ctx.reference.reload() ctx.skill.reload()

这里有一个容易误解的点,文档专门澄清了:reload 属于域,而不是某一次注册ctx.catalog.reload()会重新执行当前全部活跃的 catalog transform 并发布重建后的 catalog——即使没有任何新注册或 dispose 发生。这为“transform 闭包捕获了可变数据、数据变了但注册没变”的场景提供了手动触发重建的手段。Reload接口的定义也很简单(registration.ts):

export interface Reload { readonly reload: () => Effect.Effect<void> }

每个域类型都以& Reload交叉出这个成员(见 context.ts),所以六个域的transformreload总是成对出现。

八、实践要点小结与源码索引

综合文档与源码,编写一个 V2 Effect 插件时可以遵循以下要点:

  1. define({ id, effect })定义插件,effect内用Effect.fn组织流程,ctx.options读取配置;
  2. 需要修改域内容(agent、catalog、command、integration、reference、skill)时注册transform,并保持“每次重建都从全新状态执行”的幂等假设;
  3. 需要干预 AI SDK 实例或 language model 生成时,使用ctx.aisdk.sdk/ctx.aisdk.language,并注意注册顺序决定数据流;
  4. 注册项生命周期由插件作用域托管,作用域关闭自动清理,也可通过返回的Registration提前dispose
  5. 外部数据变化后,调用所属域的ctx.<domain>.reload()触发全量重建与发布。

相关源码与文档索引:

  • API 文档(本文主体):README.md
  • 插件定义与 PluginDomain:plugin.ts
  • 生命周期与 Hooks 映射类型:registration.ts
  • 上下文结构:context.ts
  • 各域 draft 类型:agent.ts、catalog.ts、aisdk.ts
  • 配置类型:options.ts
  • 导出路径配置:package.json
  • 同目录还有一份实现计划文档 PLAN.md,以及 Promise 风格的姊妹 API(导出子路径./v2/promise,见 packages/plugin/src/v2/promise/README.md),两者可对照阅读。

适用前提说明:以上结论均基于当前仓库中@opencode-ai/pluginv2 子包的实际源码;effect@opencode-ai/sdk均以 workspace 依赖方式引入,effect依赖以 catalog 版本锁定(package.json),具体版本以仓库根目录锁文件为准。

【免费下载链接】opencodeThe open source coding agent.项目地址: https://gitcode.com/GitHub_Trending/openc/opencode

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 16:32:44

基于微信小程序的付费自习室系统设计与实现(毕设源码+文档)

温馨提示&#xff1a;本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;本人主页置顶文章(点我)开头有 CSDN 平台…

作者头像 李华
网站建设 2026/9/7 16:29:02

虚拟电厂多时间尺度调度:模型、代码与滚动优化实践

1. 虚拟电厂多时间尺度调度到底在解决什么问题 先别急着打开Matlab敲代码&#xff0c;这个方向的核心问题得先想明白。虚拟电厂&#xff08;Virtual Power Plant&#xff0c;VPP&#xff09;这个概念在新型电力系统里已经不算新词了&#xff0c;它本质上就是把分散的分布式电源…

作者头像 李华
网站建设 2026/9/7 16:25:08

单片机计算机毕设之基于 STM32 或 51 单片机的定时自动晾衣控制系统设计与实现 基于 STM32 或 51 单片机的环境监测型智能门窗控制器设计(025606)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于嵌入式单片机&#xff0c;Java、小程序技术领域和毕业项目实战 ✌️…

作者头像 李华
网站建设 2026/9/7 16:21:46

播放量高但广告曝光低?一文搞定广告变现数据实时联动与排查

做广告变现的App&#xff0c;早晚会被一句灵魂拷问戳到&#xff1a;播放量明明在涨&#xff0c;为什么广告曝光没跟上&#xff1f;曝光看着不错&#xff0c;后台收益却纹丝不动&#xff1f;前阵子运营同事拿着截图来找我&#xff0c;说后台昨天某条视频播放量80万&#xff0c;广…

作者头像 李华
网站建设 2026/9/7 16:21:20

从静态SOP到3D作业指导:车间数字化落地的完整实践指南

车间里的工艺卡&#xff0c;永远是我做数字化项目时最头疼的东西。二维爆炸图上密密麻麻的编号&#xff0c;旁边夹着一堆文字工序说明&#xff0c;操作工得眯着眼睛对着找半天。去年我们在装配产线试点Bowell Studio做3D作业指导&#xff0c;一开始我只当是把纸质SOP换成三维动…

作者头像 李华