OpenClaw 插件 SDK 边界指南:从契约、入口到演进规范
【免费下载链接】openclawThe AI that really does things. Any OS. Any Platform. The lobster way. 🦞项目地址: https://gitcode.com/GitHub_Trending/cl/openclaw
OpenClaw 的插件 SDK(Plugin SDK)是插件与核心(core)之间唯一的公共契约层,承载着内置插件与第三方插件的全部能力注册与运行时交互。本文基于仓库中 src/plugin-sdk/CLAUDE.md 这一边界契约文档展开,结合 docs/plugins 下的 SDK 文档与 src/plugin-sdk 的源码实现,系统讲解边界规则的由来、版本化能力契约、验证手段以及如何安全地扩展 SDK 表面。读完本文,你将掌握 OpenClaw 插件 SDK 的边界设计原则、入口辅助函数(definePluginEntry/defineChannelPluginEntry/defineSingleProviderPluginEntry)的注册语义,以及为仓库新增公共子路径时必须对齐的文件清单。
一、什么是 Plugin SDK 边界
src/plugin-sdk/目录在仓库中承担一个特殊角色:它是插件与核心之间的公共契约(public contract)。任何对这里的改动都可能同时影响内置插件(bundled plugins)和第三方插件(third-party plugins),因此仓库用一份专门的 CLAUDE.md 规定改动前的边界规则、必需能力、验证步骤与扩展流程。
边界契约的核心立场可以概括为一句话:宿主(host,即 OpenClaw 核心)加载插件,插件不应穿透 SDK 去抓取任意的宿主内部实现。SDK 应当是一层受控的、窄而文档化的接缝,而不是一个把所有内部工具都倒出来的"便利桶"。
二、边界的事实来源(Source Of Truth)
要修改或扩展 SDK,必须清楚哪些文件是权威事实来源。根据 src/plugin-sdk/CLAUDE.md,SDK 边界的权威定义分散在两类文件中:
文档侧(docs):
- docs/plugins/sdk-overview.md —— 导入映射、注册 API 参考与 SDK 架构
- docs/plugins/sdk-entrypoints.md —— 入口辅助函数与注册模式
- docs/plugins/sdk-runtime.md ——
api.runtime运行时助手 - docs/plugins/sdk-migration.md —— 从已废弃表面迁移
- docs/plugins/architecture.md —— 插件内部架构与能力模型
定义文件(definition files):
package.json—— 包导出(exports)映射- scripts/lib/plugin-sdk-entrypoints.json —— 全部 SDK 子路径清单(共 350+ 个入口)
- scripts/lib/plugin-sdk-entries.mts —— 从清单派生出 public / private / production / deprecated 等入口集合
- src/plugin-sdk/api-baseline.ts —— API 基线(baseline)渲染,用于契约漂移报告
- src/plugin-sdk/plugin-entry.ts —— 非渠道插件的规范入口
- src/plugin-sdk/core.ts —— 渠道插件入口与聊天渠道组合器
- src/plugin-sdk/provider-entry.ts —— 单 Provider 插件入口
可以看到,一套 SDK 公共表面同时由文档、入口清单、包导出和基线校验四类文件锁定,任何一边单独改动都会造成漂移。这正是后文"扩展边界"要强调多文件对齐的原因。
三、边界规则详解
src/plugin-sdk/CLAUDE.md 用十余条规则定义了 SDK 表面应有的形状。下面按主题分组解读,并给出源码层面的印证。
3.1 窄入口优先,拒绝便利桶
- 宿主加载插件,插件不得反向抓取宿主内部。SDK 应提供"小的、带版本号的宿主/内核接缝 + 窄而文档化的入口点",而不是宽泛的 barrel 再导出。
- 不要从
src/channels/**、src/agents/**、src/plugins/**或其他内部模块暴露实现便利,除非是有意将其提升为受支持的公共契约。
这条规则在 src/plugin-sdk/plugin-sdk-entries 相关脚本 中有具体落地:入口被分为publicPluginSdkEntrypoints(公开、带类型与文档)与privateLocalOnlyPluginSdkEntrypoints(仅本地使用、不进公共类型面),从机制上防止内部模块被误导出。
3.2 公共入口在模块加载时必须廉价
Keep public SDK entrypoints cheap at module load.
如果某个助手只会在异步路径(如 send、monitor、probe、directory-live、login、setup)上用到,就应该放在窄的*.runtime子路径里,而不是通过一个宽泛的 SDK barrel 再导出——因为热渠道入口(hot channel entrypoints)在启动时会 import 这些 barrel,过重的加载会影响启动成本。
仓库里大量*-runtime.ts文件正是这一原则的产物:例如 src/plugin-sdk/blob-runtime.ts、src/plugin-sdk/fetch-runtime.ts、src/plugin-sdk/reply-runtime.ts 等。它们只承载运行时实现,注册入口保持轻量。在 src/plugin-sdk/provider-entry.ts 中可以看到具体手法——createLazyRuntimeModule/createLazyRuntimeMethod把 live catalog 等重模块延迟到运行时钩子触发时才加载:
// src/plugin-sdk/provider-entry.ts const liveCatalogRuntime = createLazyRuntimeModule( () => import("./provider-catalog-live-runtime.js"), ); const buildOpenAICompatibleProviderCatalog = createLazyRuntimeMethod( liveCatalogRuntime, (runtime) => runtime.buildOpenAICompatibleProviderCatalog, );3.3 保持 SDK 门面无环
- 不要添加把轻量契约文件重新路由回更重的 policy/runtime 模块的反向再导出(back-edge re-exports)。
- 不要在塑造 SDK 接缝时混用同一运行时表面的静态与动态导入:如果某个表面必须保持懒加载,就把急切侧放在轻量契约文件上,把延迟侧放在专门的 runtime 子路径上。
- 当核心或测试需要内置插件助手时,优先使用插件包自身的
api.ts或runtime-api.ts,加上通用的 SDK 能力;不要为了核心感知某个内置渠道的私有助手,而新增一个以 provider 命名的src/plugin-sdk/<id>.ts接缝。
3.4 Provider 工作优先家族级接缝
- 共享助手应该描述可复用行为,例如重放策略(replay policy)、工具 schema 兼容(tool-schema compat)、载荷归一化(payload normalization)、流包装组合(stream-wrapper composition)、传输装饰(transport decoration)。
- 避免新增只包装某一家 provider 本地实现的 SDK 导出,除非已经存在第二个消费者。
- 当 options 编码的是稳定契约时,优先命名助手而非原始 options 对象。文档中的例子是:导出 "OpenAI 风格的 Anthropic 工具载荷兼容" 助手,而不是让每个插件都传同样的 mode 标志。
- 保持传输/运行时策略与插件面向的助手对齐:如果同一行为既出现在插件注册路径又出现在核心运行时路径,就暴露一个共享助手,避免两条路径各自漂移。
- SDK 子路径应帮助调用方一次解决一个能力或运行时需求,不要长出要求宽泛运行时注册表访问的新表面作为默认路径。
- 如果某个提议的 SDK 导出主要是为了让 setup/config/control-plane 代码执行插件运行时,这通常是边界异味(boundary smell)——应优先采用元数据或描述符驱动的 control-plane 接缝。
四、版本化必需能力(Versioned Required Capabilities)
边界文档用"Always / Never / Ask first"三档约束能力契约的演进,这是 SDK 安全性的核心部分:
| 等级 | 规则 | 含义 |
|---|---|---|
| Always | 已发布的 Plugin SDK 参数契约一旦获得必需的宿主权限,就必须引入需要该权限的版本化类型 | 旧类型在文档化的弃用窗口内保持源码兼容,并在同一改动中迁移所有内置/内部调用方 |
| Always | 宿主能力必须保持通用且闭包绑定(closure-bound) | 每个暴露的 tool、preparer、callback、审批操作与 native-action 表面都要绑定;所有权或能力关闭后(包括 await 策略工作期间的关闭),保留的副本必须失效 |
| Never | 不要把旧的可选性当作无能力的运行时路径 | 禁止在插件内部重建宿主权限,也禁止给通用契约附加 provider 专属权限 |
| Never | 不要手工编辑生成的 SDK 基线、声明、哈希或预算 | 必须通过规范流程重新生成 |
| Ask first | 缩短兼容窗口、让已发布类型源码不兼容、或扩大某项能力的信任/权限/持久化边界 | 必须先征得 SDK 与安全负责人的认可 |
最后一条"Ask first"尤其值得注意:能力的信任、权限或持久化边界(trust, authority, persistence boundary)一旦扩大,等于改变了整个插件生态的安全假设,属于需要审批的变更,而不是顺手就能做的清理。
五、验证(Verification)
改动 SDK 后,仓库要求按影响范围执行验证:
- 涉及懒加载、热渠道入口或内置插件导入拓扑的改动,运行
pnpm build; - 可能改变内置渠道启动成本的改动,还要对受影响的插件运行隔离入口点分析器:
OPENCLAW_LOCAL_CHECK=0 node --import tsx scripts/profile-extension-memory.mts --extension <id> --skip-combined --concurrency 1这条命令来自 scripts/profile-extension-memory.mts,用于度量单个扩展入口的内存/加载画像,是"入口必须廉价"这条规则的量化保障。
六、如何扩展边界(Expanding The Boundary)
6.1 不因便利而扩张
- SDK 表面已经太大:不要为了便利添加兼容 barrel、别名或 fallback 导出。旧入口点应当被替换而不是叠加。
- 公共第三方 API 是唯一的兼容例外:文档化/版本化破坏性变更,先迁移全部内置/内部插件,再激进地弃用未使用的导出。
6.2 新增或修改公共子路径时的对齐清单
当添加或修改一个公共子路径时,必须保持以下四处对齐:
- docs/plugins 下的 SDK 文档;
- scripts/lib/plugin-sdk-entrypoints.json;
- scripts/lib/plugin-sdk-entries.mts;
package.json的 exports 字段;- API diff 与导出检查(src/plugin-sdk/api-diff.ts)。
入口清单与导出映射的联动关系可以在 scripts/lib/plugin-sdk-entries.mts 中看到:buildPluginSdkPackageExports()遍历全部入口,公开入口生成./plugin-sdk/<entry>的 types + default 双导出,打包私有运行时入口只生成 default 导出,其余一律不出现在包导出中。
6.3 跨包边界的判断顺序
如果内置渠道/助手的某个需求要跨包边界,先问:这个需求是否真正通用?
- 是 → 添加一个窄的通用子路径;
- 否 → 通过插件本地的
api.ts/runtime-api.ts保持插件本地化。
6.4 扩展 Provider 接缝时的测试锁定
扩展 provider 面向的接缝时,必须新增或更新匹配的窄测试来锁定契约:
- Plugin SDK 的 diff/export 检查(针对公共子路径);
- 针对被集中化的行为,编写最直接的 provider/plugin 测试。
6.5 破坏性变更的版本纪律
破坏性的删除或重命名属于 major 版本工作,而不是顺手清理(drive-by cleanup)。这要求任何移除导出都必须走完整的弃用窗口、内部迁移与版本号提升流程。
七、从源码看边界的落地:三类入口辅助函数
SDK 边界最终通过一组"入口辅助函数"暴露给插件作者。它们把"插件应该怎么声明自己"规范化,避免每个插件各自发明注册形状。
7.1definePluginEntry—— 非渠道插件入口
定义于 src/plugin-sdk/plugin-entry.ts,适用于 provider、tool、command、service、memory、context-engine 插件。其选项结构为:
| 选项 | 类型 | 说明 |
|---|---|---|
id | string | 插件 id |
name | string | 显示名称 |
description | string | 描述 |
kind | OpenClawPluginDefinition["kind"] | 已弃用:专属插件类型请改在openclaw.plugin.json的 manifestkind中声明,运行时kind仅作为旧插件兼容回退 |
configSchema | OpenClawPluginConfigSchema \| (() => ...) | 配置 schema,支持懒工厂;默认emptyPluginConfigSchema |
reload | OpenClawPluginDefinition["reload"] | 重载注册 |
nodeHostCommands | OpenClawPluginDefinition["nodeHostCommands"] | 节点宿主命令 |
securityAuditCollectors | ... | 安全审计收集器 |
register | (api: OpenClawPluginApi) => void | 必填,核心注册回调 |
实现上,configSchema通过createCachedLazyValueGetter懒求值,其余字段只在提供时才透传——这保证了入口对象本身的加载是廉价的。
7.2defineChannelPluginEntry—— 渠道插件入口
定义于 src/plugin-sdk/core.ts,除注册渠道能力外,还按api.registrationMode分派不同的注册行为,这是理解边界的关键。从 core.ts 的实现 可以看到实际分派逻辑:
cli-metadata模式:只调用registerCliMetadata?.(api),用于 CLI 解析期元数据(对应 docs/plugins/architecture.md 中"parse-time metadata 来自registerCli(..., { descriptors }),真实 CLI 模块保持懒加载"的设计);tool-discovery模式:调用registerFull+registerCapabilities;- 其他非 full 模式:先
api.registerChannel({ plugin })与setRuntime,若为discovery模式再补registerCliMetadata+registerCapabilities; full模式:registerCliMetadata+registerFull+registerCapabilities全部执行。
7.3defineSingleProviderPluginEntry—— 单 Provider 插件入口
定义于 src/plugin-sdk/provider-entry.ts,面向运行时恰好导出一个主模型 provider 的插件。它替你规范化了:
- API-key 认证:从
provider.auth或 manifest 的providerAuthChoices派生认证方法(createProviderApiKeyAuthMethod),并生成 setup wizard 元数据; - 模型目录:支持
buildProvider静态目录、liveModelDiscovery(从 OpenAI 兼容的 model-list 端点实时发现)、或完全自定义的run目录实现; - 统一文本目录投影:通过
projectProviderCatalogResultToUnifiedTextRows把目录结果投影为统一模型目录条目,并自动注册api.registerModelCatalogProvider。
在注册时,如果 provider 既没有run目录、也没有buildProvider、manifest 中也没有对应modelCatalog.providers.<id>,会直接抛出Missing modelCatalog.providers.${providerId}错误——入口辅助函数在边界上做静态完整性校验,而不是把错误留到运行时。
7.4 注册模式与插件形态
根据 docs/plugins/sdk-overview.md,api.registrationMode还包含轻量的"setup-runtime"(setup 流程,runtime 可用);而 docs/plugins/architecture.md 强调api.registrationMode === "full"才是插件模块的完整运行时注册路径。
每个已加载插件都会按实际注册行为归类为一种形态,可用openclaw plugins inspect <id>查看:
| 形态 | 说明 | 示例 |
|---|---|---|
plain-capability | 只注册一种能力类型 | provider-only 的arcee、chutes |
hybrid-capability | 注册多种能力类型 | openai同时拥有文本推理、语音、媒体理解、图像生成 |
hook-only | 只注册 hooks,无能力/工具/命令/服务 | 受支持的路径,但尚未迁移到能力注册 |
non-capability | 有工具/命令/服务/路由,但没有能力 | — |
形态分类的意义在于:它是openclaw doctor、openclaw plugins inspect <id>、openclaw status --all、openclaw plugins doctor输出的兼容性信号(config valid/hook-only提示 / 弃用的 memory-embedding API 警告 / 硬错误)的基础。
八、能力模型:插件是所有权边界,能力是核心契约
docs/plugins/architecture.md 给出了边界之上的能力模型,这是理解"该把代码放哪"的思维框架:
- 插件(plugin)= 所有权边界:一个厂商插件通常应拥有该厂商在 OpenClaw 上的全部表面(文本、语音、图像、视频、web 搜索等),而不是一堆互不相关的集成。
- 能力(capability)= 核心契约:多个插件可以实现或消费它。例如 TTS 由
elevenlabs、google、microsoft、openai各自实现合成,voice-call消费共享的运行时助手,而核心只拥有回复期 TTS 策略、回退顺序与渠道投递。
能力注册方法覆盖推理、CLI 后端、嵌入、语音、实时转录、实时语音、媒体理解、字幕源、图像/音乐/视频生成、web fetch、web search、渠道、网关发现、迁移等十余类(完整表格见 docs/plugins/architecture.md 的 "Public capability model" 一节)。新增领域时的正确顺序是:先在核心定义能力契约 → 通过 SDK 类型化暴露 → 接线渠道/特性消费者 → 让厂商插件注册实现。
九、API 稳定性与兼容策略
docs/plugins/sdk-overview.md 明确:所有 OpenClaw 插件 API 均为实验性(experimental),包括每个openclaw/plugin-sdk/*子路径、注册与运行时 API、渠道与 provider 契约、hooks 以及原生 Control UI API。它们可能在版本间变化。
因此插件作者需要:
- 固定开发与部署所用的 OpenClaw 版本,并对声明的每个宿主版本进行测试;
- 从测试过的版本出发设置包兼容范围,不要假设能跑的构建一定兼容未来版本;
- 实验性状态并不免除文档化的迁移路径——docs/plugins/sdk-migration.md 中的迁移路径依然适用。
SDK 表面由 src/plugin-sdk/api-baseline.ts 渲染成 API 基线:它用 TypeScript 编译程序扫描每个公开入口的导出符号、声明文本与闭包哈希,供契约漂移报告使用——这正是"Never: hand-edit generated SDK baselines"的机器侧保障。
十、实践清单:为 OpenClaw 扩展或维护插件 SDK
综合全文,参与 SDK 相关工作时应按以下清单自查:
- 改动是否影响内置或第三方插件?是 → 遵循 src/plugin-sdk/CLAUDE.md 的边界规则。
- 新增导出是否真正通用?否 → 留在插件本地(
api.ts/runtime-api.ts);是 → 添加窄子路径,不做 barrel。 - 模块加载是否廉价?只走异步路径的助手 → 放
*.runtime子路径;重模块 →createLazyRuntimeModule/ 动态import()。 - 门面是否有环?禁止轻量契约文件回路由到重模块的反向再导出。
- 能力是否闭包绑定?每个 tool/preparer/callback/审批/native-action 表面必须随 owner 或能力关闭而失效。
- 需要新权限?发布过的参数契约必须版本化,并在同一改动迁移全部内部调用方。
- 改动了公共子路径?对齐 docs/plugins 文档、scripts/lib/plugin-sdk-entrypoints.json、scripts/lib/plugin-sdk-entries.mts、
package.jsonexports、API diff 检查。 - 运行验证。涉懒加载/热入口 →
pnpm build;涉启动成本 → 运行 scripts/profile-extension-memory.mts 的隔离入口分析器。 - 是破坏性变更?这是 major 版本工作,走弃用窗口 + 内部迁移,而不是顺手删除。
- 生成了基线/声明/哈希?重新生成,绝不手工编辑。
这套规则最终要回答的问题只有一个:哪些表面是插件可以依赖的契约,哪些只是实现细节。边界清晰,核心与插件才能长期并行演进——这正是 OpenClaw 插件体系在拥有 350+ SDK 子路径和数十个内置插件的情况下仍能保持契约一致性的根本原因。
【免费下载链接】openclawThe AI that really does things. Any OS. Any Platform. The lobster way. 🦞项目地址: https://gitcode.com/GitHub_Trending/cl/openclaw
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考