首页
/ OpenClaw 扩展边界规范:内置插件如何通过 Plugin SDK 与清单契约与核心解耦

OpenClaw 扩展边界规范:内置插件如何通过 Plugin SDK 与清单契约与核心解耦

2026-09-04 21:43:50作者:魏献源Searcher

本文基于 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")。

这意味着:

  • 内置插件不是核心代码的"特权内圈",它不能随意 import src/** 里的实现细节;
  • 所有内置插件对外暴露的 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 定义文件

边界文档同时列出了承载契约的代码与配置定义文件:

从源码结构看,这些定义文件与文档描述一一对应。例如 plugin-entry.ts 中的 definePluginEntry 是"非频道插件的规范入口助手"(源码注释原文:"Canonical entry helper for non-channel plugins"),其 JSDoc 明确提示:provider、tool、command、service、memory 与 context-engine 插件应使用它,而频道插件应改用 openclaw/plugin-sdk/coredefineChannelPluginEntry 以继承频道能力接线。plugin-entry.ts 还集中再导出 OpenClawPluginApiProviderPluginPluginHookSkillChangedEvent 等全部 manifest 面向的钩子类型——这正是"入口即契约"的实现方式:第三方插件通过 openclaw/plugin-sdk/plugin-entry 拿到类型与工厂函数,而不会(也不应)触达 src/plugins/** 内部实现。

scripts/lib/plugin-sdk-entrypoints.json 则支撑了边界规则中"导出子路径列表"的自动化核对:仓库中存在 check-plugin-sdk-subpath-exportscheck-plugin-sdk-wildcard-reexports 等检查脚本(见 scripts/ 目录),在 pnpm check 中强制校验 SDK 子路径与文档、package.json 导出的一致性。

三、边界规则详解(Boundary Rules)

边界文档的核心是 15 条规则。按主题分组后,可归纳为五组约束。

3.1 导入边界:只许从 SDK 子路径与本地桶文件导入

规则原文要点:

  1. 生产代码只能从 openclaw/plugin-sdk/* 及其自身的本地桶文件(./api.ts./runtime-api.ts)导入
  2. 禁止从 src/**src/channels/**src/plugin-sdk-internal/** 导入核心内部实现
  3. 禁止导入其他扩展的 src/**(即扩展之间不能互相"内部直连");
  4. 禁止使用逃逸出当前扩展包根的相对导入

这条规则在 building-plugins.md 的"Import conventions"一节有配套的正面示例:

// 从聚焦的 SDK 子路径导入
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";

以及反向约束:"在插件包内部,使用 api.tsruntime-api.ts 这类本地桶文件做内部导入;不要通过 SDK 路径导入你自己的插件"。

其工程收益是双向的:

  • 对外:核心可以删除或重构 src/** 内部实现而不影响插件面(插件只依赖 SDK 子路径);
  • 对内:扩展之间通过桶文件而非 src 交叉引用,保持了每个插件包的可独立演进性与可删除性。

仓库中 scripts/check-no-extension-src-imports.tscheck-extension-wildcard-reexports.ts 等边界检查脚本(见 scripts/ 目录)表明这些规则并非仅靠自觉,而是被 CI 强制执行的。

3.2 元数据边界:清单与包元数据必须"免执行"可用

规则原文:

Keep plugin metadata accurate in openclaw.plugin.json and the package openclaw block so discovery and setup work without executing plugin code.

结合 manifest.md 的说明可以理解为:OpenClaw 的发现(discovery)、配置校验(config validation)、setup/onboarding 等控制面流程,全部建立在"先读元数据、后加载运行时"的顺序之上。清单字段如 contracts.toolsactivationproviderschannelscliCommands 等,让核心在不 import 插件运行时的前提下就能回答"谁拥有这个工具/渠道/命令""该不该在启动时加载"这类问题。

building-plugins.md 的快速上手示例给出了最小形态——package.jsonopenclaw 块 + 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.

含义拆解:

  1. 插件依赖(含 runtime peer)声明在该插件自己的 package.json 中;除非根包/包的 dist 自身拥有该 import,否则不得上移到根依赖;
  2. 运行路径永不执行安装。插件缺失依赖时,修复点是 installupdatedoctor 命令——即安装/更新/体检是唯一的依赖修复入口;
  3. 依赖归属类断言应放在通用契约测试中(文档点名了 package-manifest.contract.test.tsextension-runtime-dependencies.contract.test.ts 这两个契约测试文件,它们表达的是"包所有权"),而不是塞进某个插件的 e2e 测试。

building-plugins.mdnpm-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 through api.ts and, if needed, a matching src/plugin-sdk/<id>.ts facade. If core or core tests need a bundled plugin helper, export it from api.ts first instead of letting them deep-import extension internals.

这定义了插件内部代码的"晋升通道":

  • 插件内部的 src/**onboard.ts 等一律视为私有
  • 若核心或核心测试确实需要用到某个内置插件的助手函数,正确做法是先从该插件的 api.ts 导出,而不是让核心深层导入扩展内部实现;
  • 当助手需要成为 SDK 级公共面时,再配套一个 src/plugin-sdk/<id>.ts 门面(facade)。

extensions/ollama 等真实插件同时拥有 api.tsruntime-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 间被反复复制的实现:wrapStreamFnbuildReplayPolicynormalizeToolSchemasinspectToolSchemas 以及兼容补丁(compat patch)助手:

  1. 新增此类 provider 本地助手之前,先检查 openclaw/plugin-sdk/* 是否已提供同样行为——"先复用共享 family helper";
  2. 如果两个内置 provider 共享同一种 replay policy 形状、tool-schema 兼容重写、payload 补丁或 stream-wrapper 链,停止复制:在同一次变更中抽取一个共享助手,并把两个调用点一起迁移;
  3. 优先使用命名的 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 控制面元数据与运行时逻辑分离

最后两条规则进一步收紧了"元数据优先"原则:

  1. 控制面元数据必须与运行时逻辑分离:发现、配置校验、setup 提示、onboarding 提示、激活规划,凡能表达的就应表达在 manifest/descriptor 中;
  2. 若 setup 确实需要运行时执行,就在插件声明的 setup/runtime 面中显式声明(对应 manifest.mdsetup.requiresRuntime 字段:显式 false 表示纯描述符契约,OpenClaw 不会执行 setup-api;省略则保留旧的回退行为),而不是让元数据流程意外 import 到运行时代码;
  3. 核心在热路径上需要插件自有的静态数据时,应暴露轻量级顶层产物,如 gateway-auth-api.tsmessage-tool-api.ts 或同类的窄 *-api.ts 文件,并且该产物与完整插件复用同一本地助手——这样快速路径不会与运行时行为漂移。

这一组规则与 manifest.md 中大量"cheap metadata"字段的语义完全一致:providerAuthChoicestoolMetadataactivation 等均被设计为"在 provider/插件运行时加载前读取的廉价事实"。*-api.ts 顶层产物则是其代码侧对偶——窄入口、无副作用、与全量插件共享实现。

四、扩展边界(Expanding The Boundary)

当现有 SDK 子路径无法满足插件需求时,文档给出三步硬性流程:

  1. 加/替换一个类型化的 Plugin SDK 子路径,而不是伸手进核心("If an extension needs a new seam, add or replace a typed Plugin SDK subpath instead of reaching into core");
  2. 所有内置插件必须在同一次变更中迁移到现代 SDK 接缝——不允许为内部调用者保留扩展本地的兼容路径("Do not keep extension-local compat paths for internal callers");
  3. 有意扩展契约时,必须同一次变更更新四处:文档、导出子路径列表、包 exports、以及 API/契约检查。

第 3 点解释了为什么仓库中同时存在 docs/plugins/sdk-overview.mdscripts/lib/plugin-sdk-entrypoints.json、根 package.jsonexportsscripts/check-plugin-sdk-*.mts 系列检查脚本:四者被当作同一契约的四个投影,任何一处漂移都会被 CI 拦截。对维护者的实际含义是:给 SDK 加一个新子路径不是一个文件的事,而是一份"文档 + 清单 + exports + 检查"的原子提交。

五、对照实操:验证一条内置插件是否合规

结合本文覆盖的规则,检查 extensions/ 下一个内置插件时可依次执行以下可验证步骤:

  1. 导入面检查:插件生产代码的 import 应全部落在 openclaw/plugin-sdk/* 与包内 ./api.ts./runtime-api.ts./src/** 之内;对 src/(仓库根)或其他扩展 src/ 的引用应为零。可先人工核查,再运行仓库的边界检查脚本(pnpm check,入口见 scripts/check.mts);
  2. 元数据检查openclaw.plugin.json 存在且合法;contracts.tools 与运行时 api.registerTool(...) 注册的名称一致;activation.onStartup 为显式布尔值;
  3. 依赖归属检查:运行时 import 的第三方依赖在该插件 package.jsondependencies/optionalDependencies 中,而非 devDependencies
  4. 私有面检查:核心对插件的引用只经过该插件 api.ts(或对应的 SDK 门面),没有深层 import 插件 src/**
  5. provider 重复检查:对 wrapStreamFnbuildReplayPolicynormalizeToolSchemasinspectToolSchemas 做全扩展搜索,确认同类逻辑没有以本地助手形式散落在多个 provider 中。

对应的文档侧参考资料:入口与导入规范见 docs/plugins/building-plugins.md,清单字段全量参考见 docs/plugins/manifest.md,SDK 总览见 docs/plugins/sdk-overview.md;代码侧对照 src/plugin-sdk/plugin-entry.tssrc/plugin-sdk/core.tssrc/plugin-sdk/provider-entry.tssrc/plugin-sdk/channel-contract.ts

六、小结

OpenClaw 的扩展边界规范本质上是一套"内置插件无特权"的契约体系:

  • 导入契约:生产代码只依赖 openclaw/plugin-sdk/* 子路径与包内桶文件;
  • 元数据契约openclaw.plugin.jsonpackage.jsonopenclaw 块承担免执行发现、校验与激活规划;
  • 依赖契约:运行时依赖归插件包所有,运行时永不安装依赖,修复点收敛到 install/update/doctor;
  • 行为契约:厂商行为本地化于 provider 插件,跨插件重复逻辑必须收敛为命名共享助手;
  • 扩展契约:新增接缝必须落成类型化 SDK 子路径,且文档、子路径清单、包 exports、契约检查在同一次变更中同步更新。

这套规则的落点都可在仓库中直接验证:SDK 入口实现位于 src/plugin-sdk/,子路径入口清单位于 scripts/lib/plugin-sdk-entrypoints.json,导出面位于根 package.json,规范文档位于 docs/plugins/,真实插件包结构可参照 extensions/ollama。对 Agent 与 LLM 而言,这份边界文档本身即是高价值的检索锚点:它把"什么能导入、什么属于谁、什么必须先声明"变成了可静态检查、可自动强制执行的工程约束。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
980
502
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384