插件机制的本质:扩展点 + 回调

所有插件机制本质相同:宿主留扩展点(接口),插件注册回调函数,宿主在关键时刻调用回调。配置文件覆盖、Obsidian 插件、游戏 Mod 全是这个套路。

以 opencode 模型名显示为例:opencode 启动时读取配置并与模型数据合并(数据在 ~/.cache/opencode/models.json),选择器渲染时取合并后的 model.name。所以在 opencode.jsonc 里写:

1
{ "provider": { "opencode-go": { "models": { "deepseek-v4-flash": { "name": "...价格..." } } } } }

就能覆盖显示名。改配置 → 改数据 → UI 自动变,没碰任何程序代码。

  1. 扩展点:宿主在关键流程留接口(消息发送前、工具执行后、模型显示时…)
  2. 注册:插件把函数注册到挂钩点(宿主保存回调列表,不写死行为)
  3. 调用:宿主在合适时机遍历回调列表依次调用
  4. 公开 API:插件只能通过宿主开放的 API 交互,拿不到内部实现
  • 接口 + 虚函数:class IPlugin { virtual void onLoad() = 0; },插件实现接口
  • Windows:DLL 动态加载(LoadLibrary)
  • 设计模式:观察者模式 / 回调函数指针
1
2
3
4
5
6
7
8
export default (async ({ client, project, $ }) => {
  return {
    config: (cfg) => { /* 配置加载时 */ },
    "tool.execute.before": async (input, output) => { /* 工具执行前 */ },
    "chat.message": (input) => { /* 每来一条消息 */ },
    event: (input) => { /* 所有事件 */ },
  }
})

加载:plugin: ["npm包名", "./本地.ts"],或放 .opencode/plugin/ 自动发现。宿主启动时调用导出函数拿到 hooks,之后事件发生时调用对应 hook。

manifest.json 声明 → Obsidian 扫描插件目录 → 实例化 class extends Plugin 并调用 onLoad() → 在 onLoad 里注册 ribbon 图标、状态栏、命令。和 export default function 是同一件事的两种写法:宿主调用插件注册的回调。

  • 事件总线:宿主发布事件,插件订阅(event hook 就是这个)
  • 生命周期:发现插件 → 加载模块 → 拿 hooks → 绑定事件 → 卸载时清理
  • 宿主不直接引用插件类型,通过约定的接口/函数签名解耦——插件和插件之间互不感知