Skip to content

插件进阶 ​

💡 一句话总结:掌握所有钩子类型,实现高级插件功能。

📝 课程笔记 ​

本课核心知识点整理:

5.12b 插件进阶学霸笔记


学完你能做什么 ​

  • 理解事件钩子与功能钩子的区别
  • 使用所有可用的钩子类型
  • 创建自定义工具
  • 实现认证插件

钩子分类 ​

OpenCode 插件有两类钩子:

类型特点用途
事件钩子被动监听,不修改数据日志、通知、统计
功能钩子主动拦截,可修改数据权限控制、参数修改、数据转换

事件钩子 ​

使用 event 统一订阅所有事件:

ts
export const MyPlugin: Plugin = async () => {
  return {
    event: async ({ event }) => {
      console.log(`Event: ${event.type}`, event.properties);
    },
  };
};

功能钩子 ​

使用具体钩子名拦截特定操作:

ts
export const MyPlugin: Plugin = async () => {
  return {
    "tool.execute.before": async (input, output) => {
      // 可以修改 output 影响后续执行
      console.log(`Tool: ${input.tool}`);
    },
  };
};

事件类型 ​

所有事件通过 event 钩子订阅,按 event.type 区分:

命令事件 ​

事件触发时机
command.executed斜杠命令执行后

文件事件 ​

事件触发时机
file.edited文件被编辑后
file.watcher.updated文件监视器检测到变化

安装事件 ​

事件触发时机
installation.updatedOpenCode 安装/更新后

LSP 事件 ​

事件触发时机
lsp.client.diagnosticsLSP 诊断信息更新
lsp.updatedLSP 服务状态变化

消息事件 ​

事件触发时机
message.part.removed消息片段被删除
message.part.updated消息片段被更新
message.removed消息被删除
message.updated消息被更新

权限事件 ​

事件触发时机
permission.replied用户响应权限请求
permission.updated权限状态变化

服务器事件 ​

事件触发时机
server.connected服务器连接成功

会话事件 ​

事件触发时机
session.created新会话创建
session.compacted会话压缩完成
session.deleted会话被删除
session.diff会话差异生成
session.error会话发生错误
session.idle会话进入空闲状态(AI 响应完成)
session.status会话状态变化
session.updated会话信息更新

待办事件 ​

事件触发时机
todo.updated待办列表更新

TUI 事件 ​

事件触发时机
tui.prompt.append提示框追加内容
tui.command.executeTUI 命令执行
tui.toast.show显示提示通知

功能钩子详解 ​

config ​

配置加载后触发,可修改配置:

ts
export const MyPlugin: Plugin = async () => {
  return {
    config: async config => {
      // config: Config 对象(完整类型定义见 config.ts)
      // 可直接修改属性,如:
      config.model = "anthropic/claude-opus-4-5-thinking";
    },
  };
};

参数类型:config: Config(可读写)

chat.message ​

新消息接收时触发,可修改消息内容:

ts
export const MyPlugin: Plugin = async () => {
  return {
    "chat.message": async (input, output) => {
      // input: { sessionID, agent, model, messageID, variant }
      // output: { message, parts }
      console.log(`New message in session: ${input.sessionID}`);
    },
  };
};

input 类型:

字段类型说明
sessionIDstring会话 ID
agentstringAgent 名称
model{ providerID, modelID }模型信息
messageIDstring消息 ID
variantstring消息变体

output 类型:

字段类型说明
messageMessage消息对象(可修改)
partsPart[]消息内容部分(可修改)

chat.params ​

LLM 调用前触发,可修改模型参数:

ts
export const MyPlugin: Plugin = async () => {
  return {
    "chat.params": async (input, output) => {
      // input: { sessionID, agent, model, provider, message }
      // output: { temperature, topP, topK, options }

      // 强制使用低温度
      output.temperature = 0.3;

      // 添加自定义选项(会作为 HTTP 头传递)
      output.options.customHeader = "my-value";
    },
  };
};

input 类型:

字段类型说明
sessionIDstring会话 ID
agentstringAgent 名称
model{ providerID, modelID }模型信息
providerProvider提供商对象
messageMessage当前消息

output 类型(可修改):

字段类型说明
temperaturenumber?温度参数
topPnumber?Top-P 参数
topKnumber?Top-K 参数
optionsRecord<string, unknown>自定义选项(作为 HTTP 头传递)

permission.ask ​

权限请求时触发,可修改权限决策:

ts
export const MyPlugin: Plugin = async () => {
  return {
    "permission.ask": async (input, output) => {
      // input: Permission 对象
      // output: { status: "ask" | "deny" | "allow" }

      // 自动允许特定工具
      if (input.tool === "read" && input.path?.startsWith("/safe/")) {
        output.status = "allow";
      }
    },
  };
};

tool.execute.before ​

工具执行前触发,可修改参数或抛出错误阻止执行:

ts
export const MyPlugin: Plugin = async () => {
  return {
    "tool.execute.before": async (input, output) => {
      // input: { tool, sessionID, callID }
      // output: { args }

      if (input.tool === "bash" && output.args.command.includes("rm -rf")) {
        throw new Error("Dangerous command blocked!");
      }
    },
  };
};

input 类型:

字段类型说明
toolstring工具名称(如 read、bash、write)
sessionIDstring会话 ID
callIDstring工具调用 ID

output 类型(可修改):

字段类型说明
argsRecord<string, unknown>工具参数(可修改或拦截)

抛出错误:抛出 Error 会阻止工具执行,错误信息返回给 LLM。

tool.execute.after ​

工具执行后触发,可修改输出:

ts
export const MyPlugin: Plugin = async () => {
  return {
    "tool.execute.after": async (input, output) => {
      // input: { tool, sessionID, callID }
      // output: { title, output, metadata }

      // 添加执行时间戳
      output.metadata.executedAt = new Date().toISOString();
    },
  };
};

input 类型:

字段类型说明
toolstring工具名称
sessionIDstring会话 ID
callIDstring工具调用 ID

output 类型(可修改):

字段类型说明
titlestring工具执行标题(显示在 UI 中)
outputstring工具输出内容(返回给 LLM)
metadataRecord<string, unknown>元数据(可自由添加)

实验性钩子 ​

⚠️ 警告:以下钩子以 experimental. 开头,API 可能在未来版本变化。

experimental.session.compacting ​

会话压缩前触发,可自定义压缩上下文:

ts
export const CompactionPlugin: Plugin = async () => {
  return {
    "experimental.session.compacting": async (input, output) => {
      // input: { sessionID }
      // output: { context: string[], prompt?: string }

      // 方式1:追加额外上下文
      output.context.push(`
## 自定义上下文

保留以下状态:
- 当前任务状态
- 重要决策
- 正在处理的文件
`);
    },
  };
};

完全替换压缩提示:

ts
export const CustomCompactionPlugin: Plugin = async () => {
  return {
    "experimental.session.compacting": async (input, output) => {
      // 设置 prompt 会完全替换默认压缩提示
      // 此时 output.context 数组将被忽略
      output.prompt = `
你正在为多代理会话生成续写提示。

请总结:
1. 当前任务及其状态
2. 正在修改的文件及负责人
3. 代理之间的依赖关系
4. 完成工作的下一步

格式化为新代理可以用来恢复工作的结构化提示。
`;
    },
  };
};

experimental.chat.messages.transform ​

消息发送给 LLM 前触发,可转换消息列表:

ts
export const MyPlugin: Plugin = async () => {
  return {
    "experimental.chat.messages.transform": async (input, output) => {
      // output.messages: Array<{ info: Message, parts: Part[] }>

      // 过滤某些消息
      output.messages = output.messages.filter(m => !m.parts.some(p => p.type === "text" && p.text.includes("SECRET")));
    },
  };
};

experimental.chat.system.transform ​

系统提示发送给 LLM 前触发:

ts
export const MyPlugin: Plugin = async () => {
  return {
    "experimental.chat.system.transform": async (input, output) => {
      // output.system: string[]

      // 追加自定义系统指令
      output.system.push("Always respond in formal English.");
    },
  };
};

experimental.text.complete ​

文本补全后触发:

ts
export const MyPlugin: Plugin = async () => {
  return {
    "experimental.text.complete": async (input, output) => {
      // input: { sessionID, messageID, partID }
      // output: { text }

      // 可以修改最终输出的文本
      output.text = output.text.replace(/\bAI\b/g, "Assistant");
    },
  };
};

自定义工具 ​

插件可以添加自定义工具供 AI 调用:

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

export const CustomToolsPlugin: Plugin = async () => {
  return {
    tool: {
      mytool: tool({
        description: "这是一个自定义工具",
        args: {
          foo: tool.schema.string().describe("输入参数"),
          count: tool.schema.number().optional().describe("可选的数字参数"),
        },
        async execute(args, ctx) {
          // args: { foo: string, count?: number }
          // ctx: ToolContext
          return `Hello ${args.foo}!`;
        },
      }),
    },
  };
};

tool 函数参数 ​

参数类型说明
descriptionstring工具功能描述,AI 根据此决定何时使用
argsRecord<string, ZodType>使用 Zod schema 定义参数
execute(args, ctx) => Promise<string>工具执行函数

ToolContext ​

execute 函数的第二个参数提供执行上下文:

属性类型说明
sessionIDstring当前会话 ID
messageIDstring当前消息 ID
agentstring调用工具的 Agent 名称
abortAbortSignal中止信号,用于取消长时间操作

使用 abort 信号 ​

ts
tool({
  description: "长时间运行的任务",
  args: {},
  async execute(args, ctx) {
    for (let i = 0; i < 100; i++) {
      if (ctx.abort.aborted) {
        return "任务被取消";
      }
      await doWork(i);
    }
    return "任务完成";
  },
});

Zod Schema 速查 ​

tool.schema 就是 Zod,常用类型:

ts
tool.schema.string()           // 字符串
tool.schema.number()           // 数字
tool.schema.boolean()          // 布尔
tool.schema.array(...)         // 数组
tool.schema.object({...})      // 对象
tool.schema.enum(["a", "b"])   // 枚举
tool.schema.optional()         // 可选(链式调用)
tool.schema.describe("...")    // 描述(链式调用)

认证钩子 ​

插件可以为提供商实现自定义认证:

ts
export const MyAuthPlugin: Plugin = async () => {
  return {
    auth: {
      provider: "my-provider",

      // 可选:从已有认证加载配置
      loader: async (auth, provider) => {
        const token = await auth();
        return { apiKey: token.key };
      },

      methods: [
        {
          type: "api",
          label: "API Key",
          prompts: [
            {
              type: "text",
              key: "apiKey",
              message: "Enter your API key",
              validate: value => (value.length < 10 ? "Key too short" : undefined),
            },
          ],
          authorize: async inputs => {
            // 验证并返回结果
            return {
              type: "success",
              key: inputs.apiKey,
            };
          },
        },
        {
          type: "oauth",
          label: "OAuth Login",
          authorize: async () => {
            return {
              url: "https://example.com/oauth/authorize",
              instructions: "Complete login in browser",
              method: "auto",
              callback: async () => {
                // 等待 OAuth 回调
                return {
                  type: "success",
                  access: "access_token",
                  refresh: "refresh_token",
                  expires: Date.now() + 3600000,
                };
              },
            };
          },
        },
      ],
    },
  };
};

认证方法类型 ​

类型说明
apiAPI Key 方式,用户直接输入密钥
oauthOAuth 方式,跳转浏览器授权

prompts 配置 ​

类型说明
text文本输入框
select下拉选择框

每个 prompt 可以配置:

  • key:输入值的键名
  • message:提示信息
  • validate:验证函数
  • condition:条件函数,决定是否显示此 prompt

完整示例:时间追踪插件 ​

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

export const TimeTrackingPlugin: Plugin = async ({ client }) => {
  const sessionTimes = new Map<string, number>();

  return {
    event: async ({ event }) => {
      if (event.type === "session.created") {
        sessionTimes.set(event.properties.id, Date.now());
        await client.app.log({
          service: "time-tracking",
          level: "info",
          message: `Session started: ${event.properties.id}`,
        });
      }

      if (event.type === "session.idle") {
        const startTime = sessionTimes.get(event.properties.sessionID);
        if (startTime) {
          const duration = Date.now() - startTime;
          await client.app.log({
            service: "time-tracking",
            level: "info",
            message: `Session duration: ${Math.round(duration / 1000)}s`,
            extra: { sessionID: event.properties.sessionID, duration },
          });
        }
      }
    },

    "chat.params": async (input, output) => {
      // 为所有请求添加追踪头
      output.options["X-Session-ID"] = input.sessionID;
    },
  };
};

踩坑提醒 ​

现象原因解决
钩子不触发函数名拼写错误使用 TypeScript 获得类型检查
output 修改无效返回了新对象而非修改原对象直接修改 output.xxx = ...
实验性钩子失效版本更新后 API 变化查看更新日志,调整代码
认证插件无效provider 名称不匹配确保与配置中的提供商 ID 一致
abort 信号未响应未检查 ctx.abort.aborted在长循环中定期检查

本课小结 ​

你学会了:

  1. 事件钩子与功能钩子的区别
  2. 所有可用的钩子类型及其用途
  3. 创建自定义工具(含 abort 信号处理)
  4. 实现认证插件

相关资源 ​

最近更新