OpenClaw Plugin SDK 边界治理指南:插件与核心之间的公共契约与演进规范
【免费下载链接】openclawThe AI that really does things. Any OS. Any Platform. The lobster way. 🦞项目地址: https://gitcode.com/GitHub_Trending/cl/openclaw
src/plugin-sdk/是 OpenClaw 中插件与核心(Host)之间的公开契约层,任何改动都会同时影响随包分发的内置插件(bundled plugins)与第三方插件。本文以仓库中的 src/plugin-sdk/AGENTS.md 为骨架,完整梳理 SDK 边界的事实源清单、十五条边界规则、版本化能力约束、验证命令与边界扩充流程,并结合 definePluginEntry 实现、core.ts 渠道入口 等源码逐一印证,帮助你理解"什么能进 SDK、什么不能进、改动了如何验证",无论是维护插件还是向 SDK 提交新能力,都能按规范行事。
为什么需要 SDK Boundary:插件与核心的公共契约
OpenClaw 的插件体系依赖一个简单但严格的信任模型:宿主加载插件,插件通过 SDK 与宿主交互。这个方向的正确性由src/plugin-sdk/这一层专门保证,其职责在 src/plugin-sdk/AGENTS.md 中定义为"plugins and core 之间的公共契约(public contract between plugins and core)"。
契约的敏感性在于它面向两类消费者:
- 内置插件:仓库
extensions/目录下的各渠道与能力插件(如 telegram、slack、discord、openai 等),它们随核心一起发布、一起测试; - 第三方插件:外部开发者通过
openclaw/plugin-sdk/*子路径导入 SDK 编写的插件,核心无法控制它们的发布节奏。
因此对 SDK 的任何修改都是"高风险改动":既不能破坏内置插件的加载拓扑,也不能给第三方插件制造隐性的源兼容性破坏。这也是 docs/plugins/sdk-overview.md 中所有插件 API 均标记为experimental的原因——契约可以在版本间变化,但必须通过文档化迁移路径平滑演进,而不是静默断裂。
事实源(Source of Truth):文档与定义文件的双轨清单
当开发者需要判断"某能力是否属于 SDK、应该从哪里导出"时,src/plugin-sdk/AGENTS.md给出了一份权威清单,分为文档与定义文件两类。这两类文件必须保持同步,任何一方落后都会造成契约漂移。
文档事实源(Docs)
| 文件 | 主题 |
|---|---|
| docs/plugins/sdk-overview.md | 导入映射、注册 API 总览与 SDK 架构,含OpenClawPluginApi对象字段表 |
| docs/plugins/sdk-entrypoints.md | defineToolPlugin、definePluginEntry、defineChannelPluginEntry、defineSetupPluginEntry的类型签名与注册模式 |
| docs/plugins/sdk-runtime.md | api.runtime运行时辅助命名空间参考 |
| docs/plugins/sdk-migration.md | 从废弃表面迁移的指引 |
| docs/plugins/architecture.md | 插件架构与能力模型的深层讲解 |
定义文件事实源(Definition files)
| 文件 | 作用 |
|---|---|
| package.json | 声明exports映射,如"./plugin-sdk/core"指向./dist/plugin-sdk/core.js |
| scripts/lib/plugin-sdk-entrypoints.json | 全部 SDK 子路径入口的权威目录(core、provider-setup、sandbox、routing、runtime等数百个) |
| scripts/lib/plugin-sdk-entries.mts | 从 JSON 派生公开/私有/测试子路径集合,供打包与测试配置复用 |
| src/plugin-sdk/api-baseline.ts | 生成 SDK 公开导出 API 基线,用于契约漂移报告 |
| src/plugin-sdk/plugin-entry.ts | 非渠道插件入口definePluginEntry及全部公开类型 |
| src/plugin-sdk/core.ts | 渠道插件入口defineChannelPluginEntry、defineSetupPluginEntry、createChatChannelPlugin等 |
| src/plugin-sdk/provider-entry.ts | 单供应商插件入口辅助与懒加载目录运行时 |
从源码结构看,这份"双轨清单"本身就是一条强约束:scripts/lib/plugin-sdk-entrypoints.json是子路径的唯一真源,scripts/lib/plugin-sdk-entries.mts 基于它派生出publicPluginSdkEntrypoints、productionPluginSdkEntrypoints等集合,再被 API 基线、打包与边界检查脚本消费——新增子路径若不同步这三处,构建期检查就会失败。
边界规则(Boundary Rules)详解
src/plugin-sdk/AGENTS.md的 Boundary Rules 是整套规范的核心,共十五条,可归纳为五个维度。理解这些规则,就理解了 SDK 的取舍逻辑。
维度一:依赖方向——插件不得反向穿透宿主
Host loads plugins; plugins should not reach through the SDK into arbitrary host internals.
这是最根本的一条:宿主加载插件,插件不得通过 SDK 反向触达宿主任意内部实现。插件只能使用 SDK 暴露的、文档化的契约面,而不是绕过 SDK 直接 import 宿主的src/agents/**、src/channels/**、src/plugins/**内部模块。相应地,规则明确"不要从src/channels/**、src/agents/**、src/plugins/**暴露实现便利(implementation convenience),除非你是有意提升某个受支持的公共契约"。
这一方向性约束在源码中有直接体现:src/plugin-sdk/core.ts 虽然引用了大量宿主内部模块(如../channels/plugins/types.plugin.js、../infra/outbound/deliver.js),但这些引用被封装在 SDK 门面内部,对外只导出窄化的、文档化的类型与函数,插件永远不需要直接接触这些内部路径。
维度二:模块形态——窄子路径优于宽桶形导出
Prefer a small versioned host/kernel seam plus narrow documented SDK entrypoints over broad convenience barrels. Prefer narrow, purpose-built subpaths over broad convenience re-exports.
SDK 的形态哲学是"小而窄":宁可要一个小型、带版本的主机/内核接缝(seam)加上一系列窄化的文档化入口,也不要一个什么都装的便利桶(barrel)。同理,每个子路径应当"一次解决一个能力或运行时需求"(resolve one capability or runtime need at a time),不要新增要求调用者访问宽泛运行时注册表的默认路径。
这一设计在 scripts/lib/plugin-sdk-entrypoints.json 中一目了然:core、runtime、setup、health、routing、model-ref-parse、approval-runtime、outbound-media……每个子路径职责单一,插件作者按需精确导入。
维度三:加载成本——保持入口廉价,异步路径走 runtime 子路径
Keep public SDK entrypoints cheap at module load. If a helper is only needed on async paths such as send, monitor, probe, directory-live, login, or setup, prefer a narrow
*.runtimesubpath over re-exporting it through a broad SDK barrel that hot channel entrypoints import on startup.
渠道入口(hot channel entrypoints)在启动时就会被加载,因此 SDK 公开入口必须在模块加载阶段保持廉价。如果一个辅助函数只在异步路径(发送、监控、探测、目录实时、登录、安装)上需要,就应该放到窄化的*.runtime子路径中,而不是通过宽桶重导出——否则会拖累每个渠道的启动成本。
这与"不要混用静态/动态导入"(Do not mix static and dynamic imports for the same runtime surface)是配套的:如果某个表面必须保持懒加载,就把急切(eager)侧放在轻量契约文件上,把延迟(deferred)侧放在专属 runtime 子路径上。同时"保持 SDK 门面无环"(Keep SDK facades acyclic),禁止添加把轻量契约文件回路由到更重的策略或运行时模块的反向重导出(back-edge re-exports)。
src/plugin-sdk/provider-entry.ts 是这个模式的典范:注册所需的静态元数据直接可用,而实时的模型目录发现能力通过createLazyRuntimeModule(() => import("./provider-catalog-live-runtime.js"))延迟加载,仅在目录钩子真正运行时才拉入(见 provider-entry.ts)。docs/plugins/sdk-entrypoints.md 中的 MCP 子进程运行时同样要求用动态import()在建立连接时才加载mcpStdioRuntime。
维度四:Provider 缝合——家族级接缝与具名辅助函数
For provider work, prefer family-level seams over provider-specific seams.
针对模型供应商(provider)插件的开发,SDK 强调家族级接缝(family-level seams)而非单供应商专属接缝:共享辅助函数应当描述一种可复用的行为——如重放策略(replay policy)、工具 schema 兼容、载荷归一化、流包装组合、传输装饰——而不是只为某个供应商的本地实现包一层壳。除非已经存在第二个消费者,否则不要新增只包裹单一 provider 实现的 SDK 导出。
Prefer named helpers over raw options objects when the options encode a stable contract.
当参数选项编码了一个稳定契约时,优先提供具名辅助函数而非裸的选项对象。AGENTS.md 中给出的例子是"OpenAI 风格 Anthropic 工具载荷兼容":应导出一个专用 helper,而不是让每个插件各自传递同一组模式标志。这既消除了重复,也让契约语义有了名字可查。
Keep transport/runtime policy and plugin-facing helpers aligned.
传输/运行时策略与插件侧辅助函数必须保持一致:如果同一行为既出现在插件注册路径又出现在核心运行时路径,就应暴露一个共享 helper,而不是让两条路径各自实现、逐渐漂移。
维度五:控制面气味——避免"仅为控制面执行运行时"的导出
If a proposed SDK export mainly exists to let setup/config/control-plane code execute plugin runtime, that is usually a boundary smell. Prefer metadata or descriptor-driven control-plane seams first.
若某个候选 SDK 导出存在的唯一理由是让安装/配置/控制面代码去执行插件运行时,这通常是一种边界气味(boundary smell),应优先采用元数据或描述符驱动的控制面接缝,而不是把运行时执行能力直接铺开。
另外两条相关约束:当核心或测试需要内置插件的辅助函数时,优先使用插件包的api.ts或runtime-api.ts加通用 SDK 能力,不要为了"让核心感知某个内置渠道的私有 helper"而新增以 provider 命名的src/plugin-sdk/<id>.ts接缝;解析器/门面加载器(resolver/facade loader)测试是宽源码 API 覆盖的例外——用生成的微型插件夹具(tiny plugin fixtures)验证api.js/runtime-api.js回退行为,不要把这类测试指向真实内置插件的源码 API。
Versioned Required Capabilities:版本化能力约束
当 SDK 契约需要引入新的宿主权威(host authority)时,AGENTS.md 给出了一套"必须/绝不/先问"三档规则,这是 SDK 演进中最关键的安全与兼容约束:
Always(必须)
- 当已发布的 Plugin SDK 参数契约获得必需的宿主权威时,必须引入要求该权威的版本化类型;旧类型在其文档化弃用窗口内保持源兼容,并在同一次变更中迁移所有内置/内部调用者。也就是说,能力收紧与版本化、内部迁移必须原子完成,不允许"先改契约、后改调用者"。
- 保持宿主能力通用且闭包绑定(generic and closure-bound):每一个暴露的工具、准备器(preparer)、回调、审批操作和原生动作表面都必须绑定所有者与能力闭包;保留的副本在所有者或能力关闭后必须失效——包括在等待策略(policy)执行期间发生的关闭。
Never(绝不)
- 绝不把旧版的可选性当作"无需能力"的运行时路径来对待,绝不在插件内部重建宿主权威,也绝不给通用契约添加供应商专属权威。
- 绝不手工编辑生成的 SDK 基线、声明、哈希或预算(baselines, declarations, hashes, budgets),必须通过规范方式重新生成。
Ask first(先问)
- 在以下操作前必须取得 SDK 与安全所有者的认可:缩短兼容窗口、让已发布的类型源不兼容、或扩大某项能力的信任/权威/持久化边界。
这条约束对应的落地工具就是 src/plugin-sdk/api-baseline.ts:它基于 TypeScript 程序捕获每个公开 SDK 模块的导出符号、声明文本与可达声明的闭包哈希(closureHash),形成可对比的 API 基线,任何未同步的导出变更都会在 API diff 检查中被捕获,从而杜绝"手工改基线掩盖破坏"。
验证:改完 SDK 后必须跑什么
AGENTS.md 给出了两条明确的验证门槛:
- 如果改动涉及影响懒加载、热渠道入口或内置插件导入拓扑的 SDK 接缝,运行
pnpm build。构建会同时校验exports映射、类型声明与打包产物的一致性。 - 如果改动可能改变内置渠道的启动成本,还要对受影响的插件运行隔离入口性能剖析器:
OPENCLAW_LOCAL_CHECK=0 node --import tsx scripts/profile-extension-memory.mts --extension <id> --skip-combined --concurrency 1这条命令(对应仓库中的 scripts/profile-extension-memory.mts)以单并发方式单独剖析指定扩展的入口内存占用,用于验证"窄 runtime 子路径 + 懒加载"是否真正控制了启动开销。--extension <id>替换为受影响的插件 id。
扩充边界(Expanding The Boundary)的标准流程
SDK 面已经足够大,因此 AGENTS.md 对"新增能力"的态度是克制的:
- 不要为了便利添加兼容桶、别名或回退导出(compat barrels, aliases, fallback exports),旧入口能替换时就直接替换;公开第三方 API 是唯一的兼容例外——文档化/版本化破坏,先迁移全部内置/内部插件,再激进地弃用未用导出。
- 新增或修改公开子路径时,必须保持五处对齐:
- docs/plugins 下的对应文档;
- scripts/lib/plugin-sdk-entrypoints.json;
- scripts/lib/plugin-sdk-entries.mts;
- package.json 的
exports; - API diff 与导出检查(依赖 src/plugin-sdk/api-baseline.ts)。
- 当内置渠道/辅助函数的跨包需求出现时,先问"这个需求是否真正通用":通用则加一个窄化的通用子路径;不通用则通过插件本地的
api.ts/runtime-api.ts保持插件局部。 - 扩充面向 provider 的接缝时,同步新增或更新锁定契约的窄测试:公开子路径对应 Plugin SDK diff/导出检查,被集中化的行为对应最直接的 provider/插件测试。
- 破坏性移除或重命名是主版本号的工作(major-version work),不是顺手清理(drive-by cleanup)。
这套流程的工程含义是:SDK 的每次扩张都是"文档、目录、导出、基线、测试"五件套的联动变更,任何一件缺失都会在 CI 的边界检查中暴露。
源码级实现证据:入口辅助函数如何落实边界
非渠道插件入口definePluginEntry
src/plugin-sdk/plugin-entry.ts 定义了definePluginEntry的选项与实现。其选项类型DefinePluginEntryOptions是边界规则"窄契约"的缩影:只接收id、name、description、configSchema(默认emptyPluginConfigSchema)、reload、nodeHostCommands、securityAuditCollectors与必填的register。注意kind字段已标记@deprecated——插件的独占类型应在openclaw.plugin.json清单的kind中声明,运行时入口的kind仅作为旧插件的兼容回退。这正是"版本化类型 + 文档化弃用窗口"在入口层面的实践。
configSchema支持传值或惰性工厂,内部用createCachedLazyValueGetter包装成 getter,避免模块加载阶段立即求值——与"保持 SDK 入口廉价"的规则呼应。
渠道插件入口defineChannelPluginEntry
src/plugin-sdk/core.ts 是渠道插件的规范化入口,其register方法根据api.registrationMode分派:
cli-metadata:只执行registerCliMetadata后返回;tool-discovery:执行registerFull+registerCapabilities;- 其他模式先
api.registerChannel({ plugin })注册渠道能力,再按discovery/full模式分别注册 CLI 元数据、完整钩子与能力。
这种按注册模式分层的设计,确保轻量场景(如 CLI 元数据发现)不会加载完整运行时表面。同文件还提供defineSetupPluginEntry(仅导出{ plugin }的极简安装入口)与createChatChannelPlugin(组合安全、配对、线程与外发适配器的聊天渠道工厂),后者将"安全 DM 策略、配对通知、回复模式解析"封装为声明式选项并归一化为渠道适配器,落实了"具名辅助函数优于裸选项对象"的规则。
Provider 入口的懒加载
src/plugin-sdk/provider-entry.ts 为单供应商插件提供 API-key 认证方法与目录构建辅助,并通过createLazyRuntimeModule/createLazyRuntimeMethod将实时目录发现(live provider catalog)延迟到目录钩子运行时才加载。这样"注册需要静态元数据、实时发现按需加载",正是边界规则中"异步路径走 runtime 子路径"的直接实现。
API 基线与目录的联动
src/plugin-sdk/api-baseline.ts 定义了PluginSdkApiExport(含closureHash、declaration、exportName、kind、source)与PluginSdkApiModule(含entrypoint、exports、importSpecifier、source),从 scripts/lib/plugin-sdk-entries.mts 读取公开入口清单后构建整个 API 基线。也就是说,"子路径目录 → 导出声明 → 基线哈希 → 漂移报告"是自动化闭环,任何未经五件套对齐的改动都无法通过检查——这正是"绝不手工编辑生成的基线"的强制执行机制。
对插件作者的实践启示
- 按需导入,不要图省事:从
openclaw/plugin-sdk/*精确导入单个能力子路径,避免宽桶导入拖慢插件加载;异步场景优先*.runtime子路径。 - 锁定并测试宿主版本:所有插件 API 均为 experimental(见 docs/plugins/sdk-overview.md 的 API stability 一节),开发与部署都要固定 OpenClaw 版本,并以实际测试过的宿主版本声明兼容范围。
- 遵守能力闭包:不要在插件内重建宿主权威,不要绕过
api.runtime去触达宿主内部;暴露的每个工具、回调与审批操作都应绑定能力闭包。 - 升级节奏:依赖 SDK 新能力时留意版本化类型的引入与弃用窗口,破坏性变更会随主版本发布,且文档化迁移路径始终存在(docs/plugins/sdk-migration.md)。
小结
src/plugin-sdk/的边界规范回答了一个核心问题:在"宿主加载插件"这一信任模型下,如何让 SDK 保持窄、廉价、无环、可版本化,同时不把宿主内部暴露出去。它通过双轨事实源锁定契约、十五条边界规则约束形态、版本化能力规则约束权威演进、构建与剖析命令验证开销,最终以"文档、目录、导出、基线、测试"五件套对齐的流程保证每次扩张都可控。对插件作者而言,这份规范就是"什么能导入、怎么导入、如何保证兼容"的权威答案。
【免费下载链接】openclawThe AI that really does things. Any OS. Any Platform. The lobster way. 🦞项目地址: https://gitcode.com/GitHub_Trending/cl/openclaw
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考