插件可以暴露自定义方法和事件,这些方法和事件在服务端运行,并可被其他插件或客户端调用。

定义

使用 Rpc.define 来列出 RPC 的方法、错误和事件。
import { Rpc } from "@opencode/plugin/rpc"

export const Acme = Rpc.define({
  id: "acme",
  methods: {
    search: {
      input: {
        type: "object",
        properties: { query: { type: "string" } },
        required: ["query"],
        additionalProperties: false,
      },
      output: {
        type: "object",
        properties: { text: { type: "string" } },
        required: ["text"],
        additionalProperties: false,
      },
      errors: {
        not_found: {
          type: "object",
          properties: { query: { type: "string" } },
          required: ["query"],
          additionalProperties: false,
        },
      },
    },
  },
  events: {
    updated: {
      schema: {
        type: "object",
        properties: {
          itemID: { type: "string" },
          text: { type: "string" },
        },
        required: ["itemID", "text"],
        additionalProperties: false,
      },
    },
  },
})

校验

RPC 定义描述了方法、事件和错误的输入与输出结构。它支持两种 schema 格式:
  • JSON Schema,简单且无需依赖。
  • 任何符合 Standard Schema 的校验器
    • Zod
    • Valibot
    • ArkType

输入与输出

每个方法都可以为其参数定义 input schema,并为其返回值定义 output schema。如果方法不接受或不返回某个值,可以省略其中之一。
search: {
  input: {
    type: "object",
    properties: { query: { type: "string" } },
    required: ["query"],
  },
  output: {
    type: "object",
    properties: { text: { type: "string" } },
    required: ["text"],
  },
}
JSON Schema 的值在 TypeScript 中是 unknown,因此在使用前需要先做类型收窄。Standard Schema 会推断输入和输出的类型。

错误

在方法上添加 errors 映射来声明预期的失败。每个键会成为错误的 type,其 schema 定义错误的 data。
errors: {
  not_found: {
    type: "object",
    properties: { query: { type: "string" } },
    required: ["query"],
    additionalProperties: false,
  },
}
以 rpc. 开头的错误名称由 OpenCode 保留。

事件

将事件添加到顶层的 events 映射中。每个事件都有一个 schema,用于描述发送给订阅者的数据。
事件数据必须是对象。当没有数据时使用空对象 schema:
events: {
  refreshed: {
    schema: { type: "object", additionalProperties: false },
  },
}
标量、数组、null 和 undefined 不是有效的事件数据。

实现

在 setup 中注册实现:
import { Plugin } from "@opencode/plugin"
import { Acme } from "./rpc.js"

export default Plugin.define({
  id: "acme-plugin",
  async setup(ctx) {
    const registration = await ctx.rpc.register(Acme, {
      search: async (input, context) => {
        const { query } = input as { query: string }
        const text = await findText(query, { signal: context.signal })
        if (!text) return context.error("not_found", "Result not found", { query })
        return { text }
      },
    })

    const acme = ctx.rpc(Acme)
    const result = await acme.search({ query: "hello" })

    await registration.events.emit("updated", { itemID: "item-1", text: "ready" })
  },
})
注册 RPC 之后,同一插件可以通过 ctx.rpc(Acme) 调用它。
第二个参数包含用于取消的 signal,以及用于已声明错误的 context.error(...)。你可以返回或抛出该错误。
一个插件可以注册多个 RPC。销毁(dispose)注册会将其移除。

调用

RPC 注册完成后,可以通过 HTTP 调用,也可以从另一个插件调用。

HTTP

创建一个 OpenCode 客户端,然后使用 client.rpc 为该 RPC 创建子客户端:
import { OpenCode } from "@opencode/client"
import { Acme } from "opencode-acme-plugin/rpc"

const client = OpenCode.make({
  baseUrl: "http://localhost:4096",
})

const acme = client.rpc(Acme)
const result = await acme.search({ query: "hello" })

插件

插件已经拥有一个 OpenCode 客户端。例如,一个 TUI 插件可以从 context.client 创建相同的 RPC 子客户端:
import { Plugin } from "@opencode/plugin/tui"
import { Acme } from "opencode-acme-plugin/rpc"

export default Plugin.define({
  id: "acme-tui",
  async setup(context) {
    const acme = context.client.rpc(Acme)
    const result = await acme.search({ query: "hello" })
    console.log(result)
  },
})

订阅

使用 events.on 注册回调,并在不再需要该监听器时取消订阅:
const unsubscribe = acme.events.on("updated", (event) => {
  console.log(event.type, event.location.directory, event.data.text)
})

unsubscribe()
若要使用异步可迭代对象,请使用 events.subscribe("updated"):
for await (const event of acme.events.subscribe("updated")) {
  console.log(event.data.text)
}
事件订阅仅为实时推送,因此断开连接的订阅者会错过事件。
  • 使用本地名称订阅,例如 updated。
  • 接收到的类型带有前缀,例如 rpc.acme.updated。
  • 每个事件都包含 data 和 location。
  • 插件卸载时会关闭其订阅。
外部客户端会接收来自所有位置的事件,因此在需要时请检查 event.location。服务端插件仍然需要配置并处于运行状态。