乐于分享
好东西不私藏

OpenClaw权威指南 第 14 篇|插件与扩展系统:Extension SDK 的架构设计

OpenClaw权威指南 第 14 篇|插件与扩展系统:Extension SDK 的架构设计

核心与边缘的分离

OpenClaw 主仓库的 src/ 目录是核心——它包含 Gateway、Session、Agent 运行时、内存系统这些跑起来就必须有的东西。但一个可持续的开源项目不能把所有东西都塞进核心。Matrix 频道集成、LanceDB 内存后端、微软 Teams 适配器、OpenTelemetry 链路追踪——这些东西有人需要,但不是所有人都需要,而且维护它们需要各自领域的专业知识。

OpenClaw 的插件系统解决的就是这个”核心要精简,边缘要可扩展”的工程矛盾。

它的设计原则是一句话:发现和验证不需要执行代码,执行才需要。插件的元数据、配置 Schema、UI 标签、依赖声明,全部来自 manifest 文件,在运行时模块被加载之前就可以被读取、校验、展示。只有确认插件被启用且配置合法,才会执行实际的 TypeScript 代码。这个分离让 OpenClaw 能在不加载任何插件代码的情况下,就告诉你”这个插件缺少哪个配置项”、”那个插件与当前 OpenClaw 版本不兼容”。


四类插件来源

Gateway 启动时,src/plugins/discovery.ts 里的 discoverOpenClawPlugins() 函数扫描四个位置,收集所有候选插件。

Bundled 插件是随 OpenClaw npm 包一同分发的内置插件,放在 extensions/ 目录里,每次安装就已经在本地了。这类插件包括各核心平台的通道实现(Discord、Telegram、WhatsApp 的高级功能扩展)、browser 插件(浏览器工具的实际执行后端)、voice-call 插件(Twilio 语音通话集成)、以及 Provider 插件(Anthropic、OpenAI、Google、OpenRouter 等模型 Provider 的认证流程)。

全局插件安装在 ~/.openclaw/extensions/ 目录里,通过 openclaw plugins install 命令安装,对当前用户的所有 Agent 可见。这是第三方社区插件的标准安装位置。

工作区插件在当前 workspace 根目录下的 .openclaw/extensions/ 里,只对这个特定工作区的 Agent 可见。适合为特定项目安装项目级的专属插件,不影响其他工作区。

额外路径通过 plugins.load.paths 配置数组挂载,允许指向任意目录,主要用于本地开发调试——用 openclaw plugins install -l ./my-plugin 以软链接方式挂载本地插件目录,修改代码后重启 Gateway 即可测试,不需要发布 npm 包。

这四个来源的插件 ID 如果冲突,高优先级的覆盖低优先级:工作区 > 全局 > 额外路径 > bundled。一个工作区本地插件可以同名覆盖全局安装的插件,这让在不修改原始包的情况下做局部 patch 成为可能。


发现流程:从 manifest 到候选列表

发现流程读取的第一个文件不是 TypeScript,而是 openclaw.plugin.json——插件的静态 manifest 文件:

{  "id": "nextcloud-talk",  "name": "Nextcloud Talk",  "description": "Self-hosted chat via Nextcloud Talk webhook bots.",  "version": "0.1.0",  "kind": "channel",  "channels": ["nextcloud-talk"],  "configSchema": {    "type": "object",    "additionalProperties": false,    "properties": {      "serverUrl": { "type": "string" },      "botToken":  { "type": "string" }    },    "required": ["serverUrl", "botToken"]  },  "uiHints": {    "serverUrl": { "label": "Nextcloud Server URL", "placeholder": "https://cloud.example.com" },    "botToken":  { "label": "Bot Token", "sensitive": true }  }}

configSchema 是 JSON Schema,用于在不执行任何插件代码的情况下就对用户的配置做类型验证和 UI 渲染。Web UI 的配置表单、openclaw config validate 命令、Zod schema 的动态合并,全部基于这个 schema 工作。uiHints 为每个字段提供人类可读的标签和占位符,sensitive: true 让 Web UI 对该字段做密码框处理。

如果插件目录里没有 openclaw.plugin.json,发现器会退而求其次读 package.json 里的 openclaw 字段——这是兼容老格式的回退路径,新插件应该优先提供独立的 manifest 文件。

候选插件收集完毕后,每个候选经过三项安全前检:路径解析——用 realpath() 解析候选入口文件路径,确认解析后的路径仍然在插件根目录之内(防止符号链接逃逸);权限检查——非 bundled 插件的目录不能是 world-writable(其他用户可写),否则存在恶意覆盖风险;所有权验证——检查目录的实际所有者,和当前进程的有效 UID 是否一致。任何一项检查不通过,这个候选直接被丢弃,不进入后续的加载流程,openclaw plugins doctor 会报告被丢弃的原因。


启用状态解析

通过安全前检的候选插件,进入启用状态解析,由 resolveEnableState() 函数决定这个插件最终是 enableddisabledblocked 还是 slot-selected

plugins.allow 和 plugins.deny 提供白名单和黑名单控制。deny 优先于 allow——出现在 deny 里的插件永远不加载,不管 allow 里有没有它。如果配置了 allow 白名单但不是空数组,则只有白名单里的插件才被允许加载,其余全部 blocked。这个机制适合生产部署时锁定插件集合,防止意外安装新插件改变系统行为。

plugins.entries.<id>.enabled 控制单个插件的启用开关,false 时插件被 disabled,不执行任何代码,但 manifest 仍然被读取,配置 Schema 仍然合并进来(让你在重新启用时不需要重新填写配置)。

Slots 是一个特殊的独占机制,用于”只能有一个实现”的扩展点。plugins.slots.memory 定义内存后端的独占 slot,默认使用 builtin 后端,设为 "memory-lancedb" 就切换到 LanceDB 后端。同一个 slot 只能有一个插件被激活,后来者自动进入 slot-not-selected 状态,不报错,只是不执行。


运行时加载:jiti 让 TypeScript 直接跑

通过启用状态解析后,插件才进入运行时加载阶段。这里用的是 jiti——一个能直接加载和执行 TypeScript 文件的运行时模块加载器,不需要预先编译:

const jiti createJiti(import.meta.url, {  interopDefaulttrue,  extensions: [".ts"".tsx"".mts"".cts"".js"".mjs"".cjs"".json"],  alias: {    "openclaw/plugin-sdk": pluginSdkAlias,  },});const mod jiti(candidate.source) as OpenClawPluginModule;

alias 配置把所有 openclaw/plugin-sdk 的导入重定向到 pluginSdkAlias——一个指向主包 dist 目录里 Plugin SDK 子路径导出的绝对路径。这是 Plugin SDK 的设计约束:插件只能通过 openclaw/plugin-sdk 及其子路径(openclaw/plugin-sdk/coreopenclaw/plugin-sdk/telegram 等)导入主包的内容,不能用相对路径绕过这个边界直接访问 src/ 目录的内部实现。

这个边界有双重意义:对插件开发者来说,Plugin SDK 是稳定的公开 API,破坏性变更会走版本号管理;对 OpenClaw 核心来说,内部实现可以自由重构,只要 Plugin SDK 的接口不变,所有插件就不会受影响。

bundled 插件在开发时使用 openclaw/plugin-sdk 自引用路径,package.json 里有 "openclaw/plugin-sdk": "." 的 self-import 声明。外部插件使用同样的路径,jiti 的 alias 把它指向安装好的主包的 dist 目录。两者在写法上完全一致,只是解析目标不同——这让 bundled 插件的开发体验和外部插件完全相同,共用同一套开发规范。


register(api):插件能做的六件事

加载完成的模块必须导出一个 register 函数(或导出一个带 register 方法的对象)。这个函数接收 OpenClawPluginApi 对象作为唯一参数,通过这个 API 向系统注册所有能力:

import type { OpenClawPluginApi } from "openclaw/plugin-sdk";export function register(api: OpenClawPluginApi) {  // 注册通道  api.registerChannel({ ... });  // 注册工具  api.registerTool({ ... });  // 注册模型 Provider  api.registerProvider({ ... });  // 注册 Hook  api.registerHook({ ... });  // 注册 HTTP 路由  api.registerHttpRoute({ ... });  // 注册 CLI 命令  api.registerCommand({ ... });}

registerChannel 是最复杂的注册类型,需要提供四个 Adapter 实现:ChannelConfigAdapter(解析和校验通道配置)、ChannelOutboundAdapter(发送消息)、ChannelSecurityAdapterdmPolicy 访问控制)、ChannelStatusAdapter(健康检查和状态报告)。这四个 Adapter 覆盖了第六篇讲过的通道生命周期的全部职责,让外部通道插件和内置通道在行为上完全对等。

registerProvider 让插件向 OpenClaw 的认证系统注入新的模型 Provider,包括 OAuth 流程、API Key 验证、Device Code 认证三种方式。注册后,用户可以通过 openclaw models auth login --provider <id> 在终端里完成认证,不需要任何外部脚本。这是 Provider 插件(OpenAI、Google、MiniMax 等)能做到”一行命令完成 OAuth 授权”的原因。

registerHook 让插件向工作流的关键节点注入代码,在消息到达前后、Agent 执行前后、通道连接时、Gateway 启动关闭时触发。Hook 目录结构遵循 HOOK.md(给 Agent 看的钩子说明)+ handler.ts(实际执行代码)的格式,这让 Hook 也可以有 Skills 那样的自然语言描述,Agent 可以理解这个钩子在做什么。

registerHttpRoute 把自定义 HTTP 路由挂载到 Gateway 的 HTTP 服务器上,比如 Webhook 类通道(Slack、Google Chat)需要注册一个 POST 路由来接收平台推送的消息。这个路由和 Gateway 内置的 /v1/* 路由共享同一个 HTTP 服务器实例,但路径前缀隔离。


Plugin SDK 的三个模块

Plugin SDK 发布在 openclaw 主包的子路径导出下,分三个模块组织。

openclaw/plugin-sdk(核心模块)导出所有类型定义、definePluginEntry 工具函数(包装 register 函数,提供运行时安全检查)、以及 registerPluginHooksFromDir(批量注册一个目录下的所有 Hook,是最常用的 Hook 注册方式)。

openclaw/plugin-sdk/core 导出 Gateway 内部的核心工具函数,包括 Session 工具、消息规范化、配置工具等。这些是内部 API,破坏性变更概率更高,建议只在必要时使用。

openclaw/plugin-sdk/<channel> 系列(telegramdiscordslack 等)导出各通道适配器的基类和工具函数。如果你在写一个基于现有通道做增强的插件,可以直接复用这些基类,不需要从零实现 Adapter 接口。

有一条需要牢记的边界规则:插件内部不能使用 openclaw/plugin-sdk/<extension> 格式的路径自引用(即在插件代码里引用另一个 bundled 扩展的子路径),因为这些路径只在主包内部有解析规则,在插件的 jiti 运行时环境里不存在对应的解析。scripts/check-extension-plugin-sdk-boundary.mjs 脚本在 CI 流水线里静态检测这类违规导入。


内置插件一览

extensions/ 目录里的 bundled 插件值得单独罗列,它们代表了 OpenClaw 对”哪些能力值得内置”的判断:

browser 是浏览器工具的执行后端,默认启用,如果你要替换浏览器实现,需要先禁用它。voice-call 提供 Twilio 和 log 两种后端的语音通话能力,默认禁用。copilot-proxy 是 VS Code Copilot Proxy 桥接,把 GitHub Copilot 协议流量转发给 OpenClaw Agent,默认禁用。memory-lancedb 是基于 LanceDB 的高性能向量内存后端,作为 memory slot 的可选替代实现。diagnostics-otel 提供 OpenTelemetry 链路追踪和指标导出,适合需要监控 Agent 执行性能的场景。nostr 是 Nostr 去中心化协议的通道适配器。zai 是 z.ai 模型服务的 Provider 插件。

所有 Provider 插件(Anthropic、OpenAI、Google、OpenRouter 等 28 个)也是 bundled 插件,但它们以轻量 manifest 形式存在——只提供认证流程和模型 Provider 注册,不包含大量业务逻辑,保持了体积精简。


供应链安全:三道防线

插件系统和 Skills 系统面临同样的供应链安全挑战,但因为插件是真正的 TypeScript 代码而不是 Markdown 说明书,风险等级更高。OpenClaw 为插件设置了三道防线。

第一道:安装时禁用 lifecycle scripts。openclaw plugins install 内部调用 npm install --ignore-scripts,npm 的 preinstallpostinstallprepare 钩子全部不执行。这消除了”安装即 RCE(远程代码执行)”这类最常见的 npm 供应链攻击路径。

第二道:pnpm.onlyBuiltDependencies 白名单。monorepo 根目录的 pnpm-workspace.yaml 维护了一个允许执行构建脚本的依赖白名单,只有真正需要编译原生模块的包(better-sqlite3sharpcanvas 等)才在名单里,其余所有包的构建脚本都被 pnpm 忽略。这防止了依赖链深处的恶意包通过构建脚本悄悄执行代码。

第三道:路径安全检查。发现阶段的 realpath() + 所有权验证,防止符号链接劫持和其他用户的目录污染。

三道防线之后,官方文档仍然明确写着:插件在进程内与 Gateway 共享运行,把它当成受信任的代码对待,只安装你信任的插件。安全机制降低了攻击面,但不能代替信任判断。


开发一个最小插件

理解了整个体系,写一个最小可用插件的步骤非常清晰。

创建目录结构:

my-plugin/├── openclaw.plugin.json   ← manifest├── package.json           ← 声明 openclaw.extensions└── index.ts               ← 入口,导出 register 函数

package.json

{  "name": "my-plugin",  "openclaw": {    "extensions": ["./index.ts"]  }}

openclaw.plugin.json

{  "id": "my-plugin",  "name": "My Plugin",  "description": "做某件事的插件",  "version": "1.0.0"}

index.ts

import type { OpenClawPluginApi } from "openclaw/plugin-sdk";export functionregister(api: OpenClawPluginApi{  api.registerTool({    name"my_tool",    description"这个工具做某件事",    inputSchema: {      type"object",      properties: {        input: { type"string", description"输入" }      },      required: ["input"]    },    executeasync ({ input }) => {      return { result: `处理了: ${input}` };    }  });}

本地安装测试:

openclaw plugins install -l ./my-pluginopenclaw gateway restartopenclaw plugins list  # 确认插件出现

整个过程不需要发布 npm,不需要构建步骤,jiti 在运行时直接加载 TypeScript 源文件。


小结

OpenClaw 的插件系统是一个控制平面(manifest + 配置 Schema)与数据平面(register 函数 + 运行时行为)严格分离的架构。控制平面不执行代码,保证了配置验证、UI 渲染、依赖检查的安全性;数据平面通过 jiti 在运行时动态加载,保证了开发体验的零构建开销。Plugin SDK 的子路径导出边界让核心可以自由重构,插件开发者只需要依赖稳定的公开 API。

这套设计让一个 Matrix 通道插件的作者,能用和 OpenClaw 核心团队完全一致的开发体验构建他的扩展,不需要 fork,不需要修改主仓库,不需要等待合并请求。

下一篇,我们进入多 Agent 系统——当一个任务复杂到单个 Agent 处理不了,父 Agent 是如何生成子 Agent、编排工作流、以及整个系统是如何维持确定性的。


源码参考:src/plugins/discovery.ts · src/plugins/loader.ts · src/plugins/registry.ts · src/plugins/manifest-registry.ts · src/plugin-sdk/ · extensions/voice-call/ · docs/tools/plugin.md基于 commit bf6ec64f 版本