OpenClaw 扩展边界规范:内置插件如何通过 Plugin SDK 与清单契约与核心解耦
本文基于 OpenClaw 仓库中的扩展边界规范文档 extensions/AGENTS.md 展开,结合 src/plugin-sdk/ 入口契约、package.json 的导出定义与 extensions/ 下真实插件包结构,系统讲解 extensions/ 目录中内置插件(bundled plugins)必须遵守的公共契约、导入边界、依赖归属与元数据规则,帮助你在为 OpenClaw 新增或维护内置插件时,写出与第三方插件同等级别约束的代码,并在需要时正确、安全地扩展 Plugin SDK 边界。
一、extensions/ 的定位:内置插件与第三方插件同界
extensions/ 目录存放的是 OpenClaw 的内置插件(bundled plugins)。边界规范的第一条原则非常明确:
该目录包含内置插件。应将其视为第三方插件所看到的同一个边界("Treat it as the same boundary that third-party plugins see")。
这意味着:
- 内置插件不是核心代码的"特权内圈",它不能随意
importsrc/**里的实现细节; - 所有内置插件对外暴露的 API 面,必须与第三方插件可依赖的面一致——即
openclaw/plugin-sdk/*子路径加上本包内的本地桶文件(barrel); - 任何针对内置插件的"特殊通道",都必须先沉淀为类型化的 Plugin SDK 子路径,让所有插件(包括第三方)都能使用。
这一原则是整个边界文档的基石:OpenClaw 用同一套契约同时约束"自己人"和"外部人",避免内置插件演化成事实上的核心内部代码。
从仓库结构看,extensions/ 下每个插件都是一个独立的 workspace 包。以 extensions/ollama 为例,其目录形态完整体现了边界规范中提到的本地桶文件模式:
| 文件 | 角色 |
|---|---|
| openclaw.plugin.json | 插件清单:身份、能力归属(contracts)、配置 schema 等元数据 |
| package.json | 包元数据,含 openclaw 块(运行时入口、compat 等) |
| api.ts | 包级公共桶文件:对外/对核心暴露的受控出口 |
| runtime-api.ts | 运行时桶文件:运行时相关的受控出口 |
src/ |
私有实现目录,除非经 api.ts 提升,否则视为非公共 |
| tsconfig.json | 包级 TypeScript 配置 |
这正是文档中反复出现的"本地桶文件"(local barrels such as ./api.ts and ./runtime-api.ts)在实际仓库中的落点。
二、公共契约(Public Contracts):文档与定义文件清单
边界文档将"公共契约"分为两类来源:
2.1 契约文档
以下文档构成插件边界的规范性说明,全部位于 docs/plugins/ 目录:
| 文档 | 关注点 |
|---|---|
| building-plugins.md | 插件开发快速上手:包元数据、清单、入口、本地验证 |
| architecture.md | 插件架构与能力模型 |
| sdk-overview.md | Plugin SDK 总览与导入地图 |
| sdk-entrypoints.md | 入口点契约(entrypoints contract) |
| sdk-runtime.md | 运行时助手(api.runtime 等) |
| sdk-channel-plugins.md | 频道(channel)插件开发 |
| sdk-provider-plugins.md | 模型/媒体 provider 插件开发 |
| manifest.md | openclaw.plugin.json 清单全字段参考 |
其中 manifest.md 强调了一条与边界规则直接呼应的设计:OpenClaw 读取清单来校验配置,而无需执行插件代码("without executing plugin code")。清单缺失或非法会直接阻断配置校验,并被视为插件错误。
2.2 定义文件
边界文档同时列出了承载契约的代码与配置定义文件:
- src/plugin-sdk/plugin-entry.ts:非频道插件的规范入口助手
definePluginEntry; - src/plugin-sdk/core.ts:SDK 核心入口,频道插件使用其中的
defineChannelPluginEntry; - src/plugin-sdk/provider-entry.ts:provider 插件入口契约,含 provider 钩子、模型目录与运行时适配;
- src/plugin-sdk/channel-contract.ts:纯类型形式的频道契约(Channel 适配器、消息动作、状态/诊断适配等公开类型);
- scripts/lib/plugin-sdk-entrypoints.json:SDK 子路径入口点清单,用于自动化校验;
- package.json:根包的
openclaw块与exports,声明openclaw/plugin-sdk/*子路径的对外发布面。
从源码结构看,这些定义文件与文档描述一一对应。例如 plugin-entry.ts 中的 definePluginEntry 是"非频道插件的规范入口助手"(源码注释原文:"Canonical entry helper for non-channel plugins"),其 JSDoc 明确提示:provider、tool、command、service、memory 与 context-engine 插件应使用它,而频道插件应改用 openclaw/plugin-sdk/core 的 defineChannelPluginEntry 以继承频道能力接线。plugin-entry.ts 还集中再导出 OpenClawPluginApi、ProviderPlugin、PluginHookSkillChangedEvent 等全部 manifest 面向的钩子类型——这正是"入口即契约"的实现方式:第三方插件通过 openclaw/plugin-sdk/plugin-entry 拿到类型与工厂函数,而不会(也不应)触达 src/plugins/** 内部实现。
scripts/lib/plugin-sdk-entrypoints.json 则支撑了边界规则中"导出子路径列表"的自动化核对:仓库中存在 check-plugin-sdk-subpath-exports、check-plugin-sdk-wildcard-reexports 等检查脚本(见 scripts/ 目录),在 pnpm check 中强制校验 SDK 子路径与文档、package.json 导出的一致性。
三、边界规则详解(Boundary Rules)
边界文档的核心是 15 条规则。按主题分组后,可归纳为五组约束。
3.1 导入边界:只许从 SDK 子路径与本地桶文件导入
规则原文要点:
- 生产代码只能从
openclaw/plugin-sdk/*及其自身的本地桶文件(./api.ts、./runtime-api.ts)导入; - 禁止从
src/**、src/channels/**、src/plugin-sdk-internal/**导入核心内部实现; - 禁止导入其他扩展的
src/**(即扩展之间不能互相"内部直连"); - 禁止使用逃逸出当前扩展包根的相对导入。
这条规则在 building-plugins.md 的"Import conventions"一节有配套的正面示例:
// 从聚焦的 SDK 子路径导入
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
以及反向约束:"在插件包内部,使用 api.ts 与 runtime-api.ts 这类本地桶文件做内部导入;不要通过 SDK 路径导入你自己的插件"。
其工程收益是双向的:
- 对外:核心可以删除或重构
src/**内部实现而不影响插件面(插件只依赖 SDK 子路径); - 对内:扩展之间通过桶文件而非
src交叉引用,保持了每个插件包的可独立演进性与可删除性。
仓库中 scripts/check-no-extension-src-imports.ts、check-extension-wildcard-reexports.ts 等边界检查脚本(见 scripts/ 目录)表明这些规则并非仅靠自觉,而是被 CI 强制执行的。
3.2 元数据边界:清单与包元数据必须"免执行"可用
规则原文:
Keep plugin metadata accurate in
openclaw.plugin.jsonand the packageopenclawblock so discovery and setup work without executing plugin code.
结合 manifest.md 的说明可以理解为:OpenClaw 的发现(discovery)、配置校验(config validation)、setup/onboarding 等控制面流程,全部建立在"先读元数据、后加载运行时"的顺序之上。清单字段如 contracts.tools、activation、providers、channels、cliCommands 等,让核心在不 import 插件运行时的前提下就能回答"谁拥有这个工具/渠道/命令""该不该在启动时加载"这类问题。
building-plugins.md 的快速上手示例给出了最小形态——package.json 的 openclaw 块 + openclaw.plugin.json 清单:
{
"name": "@myorg/openclaw-my-plugin",
"type": "module",
"peerDependencies": { "openclaw": ">=2026.3.24-beta.2" },
"openclaw": {
"extensions": ["./index.ts"],
"compat": {
"pluginApi": ">=2026.3.24-beta.2",
"minGatewayVersion": "2026.3.24-beta.2"
}
}
}
{
"id": "my-plugin",
"name": "My Plugin",
"contracts": { "tools": ["my_tool"] },
"activation": { "onStartup": true },
"configSchema": { "type": "object", "additionalProperties": false }
}
要点:
- 每个插件都需要清单,即使没有任何配置;
- 运行时注册的工具必须出现在
contracts.tools中,这样 OpenClaw 才能"不预加载所有插件运行时"就完成所有权发现; activation.onStartup必须显式设置——manifest.md 明确"省略onStartup不再隐式启动加载插件"。
这与边界文档中"不要依赖急加载的全局注册表播种或 import 时副作用来让插件'可用'"("Do not rely on eager global registry seeding or import-time side effects to make a plugin 'available'。Plugin availability should come from manifest ownership plus targeted activation")互为表里:可用性 = 清单所有权 + 定向激活,而不是 import 副作用。
3.3 依赖归属边界:运行时依赖归插件包所有
规则原文:
Plugin runtime dependencies belong to the owning plugin package. If a plugin dependency has a runtime peer, declare/provide it in that plugin's
package.json; do not move it to root unless root/package dist owns the import. Runtime never installs deps; install/update/doctor are repair points.
含义拆解:
- 插件依赖(含 runtime peer)声明在该插件自己的
package.json中;除非根包/包的 dist 自身拥有该 import,否则不得上移到根依赖; - 运行路径永不执行安装。插件缺失依赖时,修复点是
install、update、doctor命令——即安装/更新/体检是唯一的依赖修复入口; - 依赖归属类断言应放在通用契约测试中(文档点名了
package-manifest.contract.test.ts与extension-runtime-dependencies.contract.test.ts这两个契约测试文件,它们表达的是"包所有权"),而不是塞进某个插件的 e2e 测试。
building-plugins.md 中 npm-pack: 安装验证一节也呼应了这一点:openclaw plugins install npm-pack:/tmp/<plugin-package>.tgz 使用 OpenClaw 托管的每插件 npm 工程,能暴露"运行时 import 只写在 devDependencies 而没进 dependencies/optionalDependencies"这类源码检出方式测不出来的依赖错误。
3.4 私有面与公共面:src/** 默认私有,提升需走桶文件
规则原文:
Treat files like
src/**,onboard.ts, and other local helpers as private unless you intentionally promote them throughapi.tsand, if needed, a matchingsrc/plugin-sdk/<id>.tsfacade. If core or core tests need a bundled plugin helper, export it fromapi.tsfirst instead of letting them deep-import extension internals.
这定义了插件内部代码的"晋升通道":
- 插件内部的
src/**、onboard.ts等一律视为私有; - 若核心或核心测试确实需要用到某个内置插件的助手函数,正确做法是先从该插件的
api.ts导出,而不是让核心深层导入扩展内部实现; - 当助手需要成为 SDK 级公共面时,再配套一个
src/plugin-sdk/<id>.ts门面(facade)。
extensions/ollama 等真实插件同时拥有 api.ts 与 runtime-api.ts 两层桶文件(见 extensions/ollama 目录结构),正是这一"私有实现 → 桶文件 → SDK 门面"晋升路径的实例化:桶文件把"哪些导出允许跨包可见"从约定变成了可静态审查的文件结构。
3.5 Provider 插件的行为边界:厂商行为本地化,重复逻辑必须抽取为共享助手
针对 provider 插件,文档给出了两层约束:
第一层:行为归属本地化
For provider plugins, keep auth, onboarding, catalog selection, and vendor-only product behavior local to the plugin. Do not move those into core just because two providers look similar.
认证(auth)、onboarding、目录选择、以及纯厂商特有的产品行为,必须留在插件内部。不能因为两个 provider 长得像,就把它们的逻辑上提到核心。 这条规则防止核心被厂商细节污染,也保证了新 provider 的加入不牵连核心变更。
第二层:跨插件重复必须收敛为命名助手
文档点名了四类容易在 provider 间被反复复制的实现:wrapStreamFn、buildReplayPolicy、normalizeToolSchemas、inspectToolSchemas 以及兼容补丁(compat patch)助手:
- 新增此类 provider 本地助手之前,先检查
openclaw/plugin-sdk/*是否已提供同样行为——"先复用共享 family helper"; - 如果两个内置 provider 共享同一种 replay policy 形状、tool-schema 兼容重写、payload 补丁或 stream-wrapper 链,停止复制:在同一次变更中抽取一个共享助手,并把两个调用点一起迁移;
- 优先使用命名的 provider-family 助手,而非重复原始 option 包。例如需要"OpenAI 风格的 Anthropic 工具 payload 兼容""Gemini schema 清理"或"XAI 兼容补丁"时,应使用命名共享助手,而不是再次内联策略旋钮。
从 provider-entry.ts 的结构可以印证这一方向:该文件聚合了 provider 目录构建、API key 认证方法(createProviderApiKeyAuthMethod)、清单驱动的认证选择(providerAuthChoices)等通用件,并且对"live 发现"等重逻辑采用 createLazyRuntimeMethod 懒加载("Registration needs static metadata; live discovery loads only when its catalog hook runs")——静态注册保持廉价,重行为延迟到真正需要时加载。这正是"控制面元数据与运行时逻辑分离"在 SDK 入口层的落地。
3.6 控制面元数据与运行时逻辑分离
最后两条规则进一步收紧了"元数据优先"原则:
- 控制面元数据必须与运行时逻辑分离:发现、配置校验、setup 提示、onboarding 提示、激活规划,凡能表达的就应表达在 manifest/descriptor 中;
- 若 setup 确实需要运行时执行,就在插件声明的 setup/runtime 面中显式声明(对应 manifest.md 中
setup.requiresRuntime字段:显式false表示纯描述符契约,OpenClaw 不会执行setup-api;省略则保留旧的回退行为),而不是让元数据流程意外 import 到运行时代码; - 核心在热路径上需要插件自有的静态数据时,应暴露轻量级顶层产物,如
gateway-auth-api.ts、message-tool-api.ts或同类的窄*-api.ts文件,并且该产物与完整插件复用同一本地助手——这样快速路径不会与运行时行为漂移。
这一组规则与 manifest.md 中大量"cheap metadata"字段的语义完全一致:providerAuthChoices、toolMetadata、activation 等均被设计为"在 provider/插件运行时加载前读取的廉价事实"。*-api.ts 顶层产物则是其代码侧对偶——窄入口、无副作用、与全量插件共享实现。
四、扩展边界(Expanding The Boundary)
当现有 SDK 子路径无法满足插件需求时,文档给出三步硬性流程:
- 加/替换一个类型化的 Plugin SDK 子路径,而不是伸手进核心("If an extension needs a new seam, add or replace a typed Plugin SDK subpath instead of reaching into core");
- 所有内置插件必须在同一次变更中迁移到现代 SDK 接缝——不允许为内部调用者保留扩展本地的兼容路径("Do not keep extension-local compat paths for internal callers");
- 有意扩展契约时,必须同一次变更更新四处:文档、导出子路径列表、包 exports、以及 API/契约检查。
第 3 点解释了为什么仓库中同时存在 docs/plugins/sdk-overview.md、scripts/lib/plugin-sdk-entrypoints.json、根 package.json 的 exports 与 scripts/check-plugin-sdk-*.mts 系列检查脚本:四者被当作同一契约的四个投影,任何一处漂移都会被 CI 拦截。对维护者的实际含义是:给 SDK 加一个新子路径不是一个文件的事,而是一份"文档 + 清单 + exports + 检查"的原子提交。
五、对照实操:验证一条内置插件是否合规
结合本文覆盖的规则,检查 extensions/ 下一个内置插件时可依次执行以下可验证步骤:
- 导入面检查:插件生产代码的 import 应全部落在
openclaw/plugin-sdk/*与包内./api.ts、./runtime-api.ts、./src/**之内;对src/(仓库根)或其他扩展src/的引用应为零。可先人工核查,再运行仓库的边界检查脚本(pnpm check,入口见 scripts/check.mts); - 元数据检查:
openclaw.plugin.json存在且合法;contracts.tools与运行时api.registerTool(...)注册的名称一致;activation.onStartup为显式布尔值; - 依赖归属检查:运行时 import 的第三方依赖在该插件
package.json的dependencies/optionalDependencies中,而非devDependencies; - 私有面检查:核心对插件的引用只经过该插件
api.ts(或对应的 SDK 门面),没有深层import插件src/**; - provider 重复检查:对
wrapStreamFn、buildReplayPolicy、normalizeToolSchemas、inspectToolSchemas做全扩展搜索,确认同类逻辑没有以本地助手形式散落在多个 provider 中。
对应的文档侧参考资料:入口与导入规范见 docs/plugins/building-plugins.md,清单字段全量参考见 docs/plugins/manifest.md,SDK 总览见 docs/plugins/sdk-overview.md;代码侧对照 src/plugin-sdk/plugin-entry.ts、src/plugin-sdk/core.ts、src/plugin-sdk/provider-entry.ts、src/plugin-sdk/channel-contract.ts。
六、小结
OpenClaw 的扩展边界规范本质上是一套"内置插件无特权"的契约体系:
- 导入契约:生产代码只依赖
openclaw/plugin-sdk/*子路径与包内桶文件; - 元数据契约:
openclaw.plugin.json与package.json的openclaw块承担免执行发现、校验与激活规划; - 依赖契约:运行时依赖归插件包所有,运行时永不安装依赖,修复点收敛到 install/update/doctor;
- 行为契约:厂商行为本地化于 provider 插件,跨插件重复逻辑必须收敛为命名共享助手;
- 扩展契约:新增接缝必须落成类型化 SDK 子路径,且文档、子路径清单、包 exports、契约检查在同一次变更中同步更新。
这套规则的落点都可在仓库中直接验证:SDK 入口实现位于 src/plugin-sdk/,子路径入口清单位于 scripts/lib/plugin-sdk-entrypoints.json,导出面位于根 package.json,规范文档位于 docs/plugins/,真实插件包结构可参照 extensions/ollama。对 Agent 与 LLM 而言,这份边界文档本身即是高价值的检索锚点:它把"什么能导入、什么属于谁、什么必须先声明"变成了可静态检查、可自动强制执行的工程约束。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00