OpenClaw 采用插件架构,第三方可以注册新渠道、工具、Hook。设计一个插件系统需要考虑哪些关键问题?OpenClaw 的插件 API 长什么样?
参考答案
设计插件系统要解决四个核心问题:
1)注册与发现:插件怎么被框架找到、加载后怎么告诉框架自己能干什么。
就像手机装 App:系统能找到 App、装上它、让它告诉系统自己提供了什么功能。OpenClaw 约定插件放在 extensions/ 目录下,插件只需要导出一个 register(api) 方法,框架启动时自动扫描加载并调用。加载用的是 jiti(运行时直接解析 TypeScript,不需要先编译),插件开发者改完代码直接就能跑。
2)隔离性:插件之间以及插件和核心之间不能互相干扰。
比如一个浏览器扩展出 bug 了不应该影响其他扩展的正常工作。OpenClaw 靠包结构实现隔离:每个插件有自己的 package.json,运行时依赖放 dependencies,openclaw 本身放 peerDependencies,安装时只装运行依赖。
3)API 稳定性:暴露给插件的接口要尽量稳定,核心重构不能动不动就把插件搞崩。
OpenClaw 通过一个统一的 plugin-sdk 入口收敛暴露面,插件只能 import from "openclaw/plugin-sdk",内部实现随便改,只要 SDK 接口不变插件就不受影响。
4)扩展点的粒度设计:插件能做什么、不能做什么,要有清晰的边界。
OpenClaw 的 Plugin API 提供了细粒度的注册方法,每种能力都有独立的注册入口:
| 注册方法 | 能力 | 示例场景 |
|---|---|---|
registerTool |
注册工具 | 让 Agent 能发邮件、查数据库 |
registerChannel |
注册渠道 | 接入 钉钉、飞书等新平台 |
registerHook / on |
注册 Hook | 消息进来前做内容审核 |
registerProvider |
注册 LLM Provider | 接入自部署的模型 |
registerHttpRoute |
注册 HTTP 路由 | 插件自己的 webhook 端点 |
registerService |
注册后台服务 | 长驻的监控服务 |
registerCommand |
注册 CLI 命令 | 插件的管理命令 |
框架能精确知道每个插件提供了什么能力,不像某些系统只给一个大而全的 activate 方法让插件自己去搞。

扩展知识
PluginRegistry:反转控制的核心
PluginRegistry 是一个全局收集器,统一管理所有插件注册的能力(tools、hooks、channels、providers 等)。
核心模块想拿到所有注册的工具列表,只需要问 Registry 要,不用知道这些工具是哪个插件注册的、代码在哪。
这是典型的 IoC(控制反转) 模式:核心不依赖插件,插件反过来向核心注册自己。
新加一个插件不用改核心一行代码,删掉一个插件也不会编译报错。和 Spring 的 Bean 容器、VSCode 的 Extension Registry 是同一个思路。
插件配置的自动化
每个插件可以定义 configSchema(带 safeParse、validate 和 uiHints),同时支持 jsonSchema。OpenClaw 的 UI 可以自动根据 Schema 生成插件配置表单,配置校验也是自动的。
这跟 VSCode 扩展的 package.json contributes 很像,声明式描述配置结构,框架自动渲染 UI 和校验。
和其他插件系统的横向对比
| 对比维度 | OpenClaw | Webpack Tapable | VSCode Extension |
|---|---|---|---|
| 注册粒度 | 每种能力独立注册 | 基于 Hook 订阅 | activate 统一入口 |
| 类型安全 | TypeScript 强类型 Hook | 弱类型 | TypeScript 强类型 |
| 隔离方式 | 包结构隔离 | 无隔离 | Extension Host 进程隔离 |
| 配置声明 | configSchema + uiHints | 无 | package.json contributes |
| 加载方式 | jiti JIT 解析 | require/import | Extension Host 动态加载 |
OpenClaw 的隔离方式比 Webpack 强(有包级隔离),但比 VSCode 轻(不搞进程级隔离),在”够安全”和”不复杂”之间找了个平衡点。
为什么选 jiti 而不是 tsc 编译
tsc 需要先编译成 JavaScript 再执行,第三方插件作者每改一行代码都得跑编译流程。jiti 是运行时直接加载 TypeScript,内部做转译,首次加载有一点开销(毫秒级),后续就缓存了。对插件场景特别合适,因为你不能要求所有第三方开发者都熟悉你的构建工具链。

面试官追问
提问:如果两个插件注册了同名的工具,怎么处理冲突?
回答:一般两种策略:报错拒绝,或者后注册的覆盖先注册的。OpenClaw 的做法是加载顺序决定优先级,后加载的覆盖前面的。如果想严格防冲突,可以给工具名加插件前缀做命名空间隔离,比如 myPlugin.sendEmail。更严格的做法是在 Registry 层面检查到重名直接抛错,强制插件作者解决冲突。
提问:Hook 系统和事件总线有什么区别?
回答:Hook 是同步的拦截点,能修改数据或中断流程。比如 beforeAgentRun Hook 能在 Agent 执行前做校验、改参数,甚至直接拒绝执行,它有”否决权”。事件总线是异步广播,发出去就不管了,订阅者自己处理,发布者拿不到返回值。OpenClaw 的 Hook 同时支持两种模式:registerHook 注册的 handler 可以返回值影响流程(拦截模式),on 注册的更像事件订阅(通知模式)。
提问:插件的 configSchema 升级后配置格式变了,老配置怎么兼容?
回答:得靠插件作者在 safeParse 里做兼容处理,拿到老格式的配置自动迁移到新格式。框架层面提供了 validate 方法,插件升级后如果老配置校验不过会告警但不会直接崩。最佳实践是只做增量变更:新字段给默认值,老字段标记 deprecated 继续兼容一两个版本再移除。这跟数据库 migration 是同一个思路。
提问:插件能不能访问其他插件注册的能力?
回答:可以,但只能通过 Registry 间接访问,不能直接 import 另一个插件的代码。比如插件 A 注册了一个工具,插件 B 可以通过 api.getRegisteredTools() 拿到工具列表,但拿到的是标准化的接口描述,不是 A 的内部实现。这种间接通信保证了插件之间的松耦合,删掉插件 A,插件 B 只是少了一个可用工具,不会编译报错或运行崩溃。