第 17 章 · 插件生态与 SDK 开发
OpenClaw 的插件系统是它的"基因编辑工具"——不改源码就能给 Agent 增加新的通道、模型、工具,甚至新的能力维度。
本章你将学到:
- 理解插件架构的六大能力模型和四层加载管线
- 从零开发一个工具插件、通道插件和模型提供商插件
- 掌握 SDK 的运行时 API、测试模式和发布流程
17.1 插件架构总览
在动手写代码之前,你需要先理解 OpenClaw 插件系统的设计哲学。它不是"什么都能装的万能插件"——而是一个有明确边界、有所有权规则、有能力层级的结构化扩展体系。
17.1.1 六大能力维度
OpenClaw 把插件能提供的功能划分为六个维度:
┌─────────────────────────────────────────────────────────┐
│ OpenClaw 能力维度 │
├─────────────────────────────────────────────────────────┤
│ │
│ 1. 文本推理(Text Inference) │
│ └─ 模型提供商:Anthropic, OpenAI, Ollama, ... │
│ │
│ 2. 语音能力(Speech) │
│ └─ TTS + STT:ElevenLabs, OpenAI, Azure, ... │
│ │
│ 3. 媒体理解(Media Understanding) │
│ └─ 图片描述 + 音频转写 │
│ │
│ 4. 图片生成(Image Generation) │
│ └─ DALL-E, Stable Diffusion, ... │
│ │
│ 5. 网页搜索(Web Search) │
│ └─ Brave, Gemini, Grok, ... │
│ │
│ 6. 通道(Channels) │
│ └─ WhatsApp, Telegram, Discord, Slack, ... │
│ │
└─────────────────────────────────────────────────────────┘
每个能力维度都有独立的注册 API,插件可以注册其中一个或多个。一个典型的"公司级插件"会同时注册文本推理、语音和媒体理解——比如 Anthropic 的官方插件就同时提供模型和语音能力。
17.1.2 能力所有权模型
能力所有权是 OpenClaw 插件系统的核心约束:同一时刻,同一个能力维度只能由一个插件"拥有"(own)。
这意味着什么?假设你同时安装了两个都提供 TTS 的插件,OpenClaw 不会把它们合并——它只会使用配置中指定的那个。这个设计避免了"两个 TTS 插件打架"的问题。
能力所有权规则:
✓ 同一能力 → 只有一个拥有者(通过配置选择)
✓ 不同能力 → 可以由同一个插件提供(推荐的公司级插件模式)
✓ 工具注册 → 没有所有权限制,多个插件可以注册同名工具
✓ Hook 注册 → 没有"拥有"概念,所有匹配的 Hook 都会执行
实际影响:当你开发插件时,如果注册的是"能力提供者"(Provider),就要意识到可能有其他插件也注册了同类能力。你的插件应该做好"未被选中"的准备——不崩溃、不报错,只是安静地等待被激活。
17.1.3 四层加载管线
OpenClaw 加载插件分四个阶段,每个阶段有明确的职责:
阶段 1:发现(Discovery)
└─ 扫描插件目录,读取 openclaw.plugin.json
└─ 检测 Bundle 格式(Codex/Claude/Cursor)
阶段 2:清单解析(Manifest Parsing)
└─ 解析 configSchema、providerAuthEnvVars
└─ 构建能力注册表(哪些插件提供哪些能力)
阶段 3:运行时加载(Runtime Loading)
└─ jiti 动态 import 插件入口文件
└─ 调用 register() 注册工具、Provider、通道等
阶段 4:就绪(Ready)
└─ 插件完全加载,工具可用
└─ registerFull() 注册 HTTP 路由、CLI 命令等
关键区别:阶段 1-2 是轻量级的,只读 JSON 不加载代码。这意味着 OpenClaw 可以在不实际加载插件的情况下,知道它能提供什么能力——用于配置验证和 onboarding 向导。
17.1.4 通道插件与共享消息工具
通道插件有一个独特的设计约束:通道插件不需要、也不应该注册自己的 send/edit/react 工具。
OpenClaw 核心维护一个共享的 message 工具,所有通道都通过它发送消息。通道插件的职责是:
通道插件负责:
├── 配置(Config)── 账户解析和设置向导
├── 安全(Security)── DM 策略和 Allowlist
├── 配对(Pairing)── DM 审批流程
├── 出站(Outbound)── 发送文本、媒体、投票
└── 线程(Threading)── 回复如何关联到原始消息
核心负责:
├── 共享消息工具(message tool)
├── 提示词装配
├── 会话簿记
└── 消息分发
这种分离意味着通道插件只需要关心"如何和平台 API 通信",不需要操心"Agent 如何决定发消息"。核心已经帮你把消息决策做好了。
17.1.5 通道目标解析
当 Agent 决定发送消息时,核心需要把一个"目标描述"(比如 @alice 或 +1234567890)解析为平台特定的收件人。这个解析过程由通道插件提供:
核心调用流程:
1. Agent 返回 {"to": "@alice", "text": "你好"}
2. 核心查找当前通道的 resolveTarget 函数
3. 通道插件把 "@alice" 解析为平台用户 ID
4. 核心通过通道插件的 sendText 发送消息
通道插件需要实现三个函数来支持目标解析:
inferTargetChatType(target)—— 推断目标是 DM 还是群聊looksLikeId(target)—— 判断目标是否已经是平台 IDresolveTarget(target, mode)—— 把目标解析为平台收件人
17.2 动手开发第一个工具插件
理论够了。现在从零开始写一个工具插件。
17.2.1 插件长什么样
一个最小的工具插件只需要三个文件:
my-plugin/
├── package.json # 包元数据 + OpenClaw 声明
├── openclaw.plugin.json # 插件清单
├── index.ts # 插件入口
└── src/
├── weather.ts # 天气查询逻辑
└── weather.test.ts # 测试
没有 node_modules,没有 tsconfig.json,没有构建脚本——OpenClaw 用 jiti 在运行时直接加载 TypeScript。
17.2.2 package.json
{
"name": "@myorg/openclaw-weather",
"version": "1.0.0",
"type": "module",
"openclaw": {
"extensions": ["./index.ts"]
}
}
openclaw.extensions 告诉 OpenClaw:"加载这个插件时,请 import 这个文件"。type: "module" 是必须的——OpenClaw 的插件系统基于 ESM。
17.2.3 openclaw.plugin.json
{
"id": "weather",
"name": "Weather Plugin",
"description": "查询全球天气信息",
"configSchema": {
"type": "object",
"additionalProperties": false,
"properties": {
"weather": {
"type": "object",
"properties": {
"apiKey": {
"type": "string",
"description": "OpenWeatherMap API Key"
},
"defaultCity": {
"type": "string",
"description": "默认城市"
}
}
}
}
}
}
清单文件做两件事:声明插件身份,声明配置结构。configSchema 会被 OpenClaw 的配置验证器使用——用户填错配置时,他们会在启动时就得到错误提示,而不是运行时才发现。
17.2.4 index.ts —— 插件入口
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
export default definePluginEntry({
id: "weather",
name: "Weather Plugin",
description: "查询全球天气信息",
register(api) {
// 注册工具:查询天气
api.registerTool({
name: "weather_query",
description: "查询指定城市的当前天气",
required: ["city"],
parameters: {
type: "object",
properties: {
city: {
type: "string",
description: "城市名称,如 '北京'、'Tokyo'"
},
units: {
type: "string",
enum: ["metric", "imperial"],
description: "温度单位,默认 metric(摄氏度)"
}
}
},
// 工具的执行函数
async execute(params, context) {
const apiKey = context.config.weather?.apiKey;
if (!apiKey) {
return { error: "Weather API key not configured" };
}
const city = params.city;
const units = params.units || "metric";
// 调用天气 API
const response = await fetch(
`https://api.openweathermap.org/data/2.5/weather?q=${encodeURIComponent(city)}&appid=${apiKey}&units=${units}`
);
const data = await response.json();
if (data.cod !== 200) {
return { error: `City not found: ${city}` };
}
return {
city: data.name,
temperature: data.main.temp,
feelsLike: data.main.feels_like,
humidity: data.main.humidity,
description: data.weather[0].description,
};
}
});
// 注册可选的 Hook:每次对话前检查是否需要天气提醒
api.registerHook({
event: "before:model-call",
description: "在模型调用前注入天气上下文",
async handler(context) {
// 只在有特定关键词时激活
if (context.message.includes("天气")) {
const defaultCity = context.config.weather?.defaultCity;
if (defaultCity) {
// 返回 null 表示不修改(Hook 可以不返回任何内容)
return null;
}
}
}
});
}
});
definePluginEntry 是所有工具插件的入口函数。它接收一个对象,包含:
id—— 插件唯一标识name/description—— 人类可读信息register(api)—— 注册函数,api对象提供了所有注册方法
registerTool 的关键参数:
name—— 工具名(Agent 在提示词中看到的名字)description—— 工具描述(Agent 据此判断何时使用这个工具)required—— 必填参数列表parameters—— JSON Schema 格式的参数定义execute—— 执行函数,接收params和context
⚠️ 踩坑记录
问题:工具注册了但 Agent 从不调用它。
原因:
description写得太模糊。Agent 依赖 description 来判断工具用途,"查询天气"不如"查询指定城市的当前天气、温度、湿度和天气描述"。解决:把 description 写成"人在什么时候会用这个工具"的句式,而不是"这个工具是什么"。
17.2.5 安装和运行
# 安装插件
openclaw plugins install ./my-plugin
# 验证安装
openclaw plugins list
# 输出应显示:weather | Weather Plugin | Format: native
# 重启 Gateway 加载插件
openclaw gateway restart
# 验证工具可用
/status
# 在工具列表中应看到 weather_query
安装后,插件会被复制到 OpenClaw 的插件目录。openclaw.plugins install 做的事很简单:
- 读取
openclaw.plugin.json确认是有效插件 - 复制到
~/.openclaw/plugins/<id>/ - 记录到插件注册表
- 下次 Gateway 启动时自动加载
17.2.6 从 npm 安装
如果你的插件发布到了 npm,安装更简单:
openclaw plugins install @myorg/openclaw-weather
OpenClaw 会先检查 ClawHub(官方插件仓库),找不到再回退到 npm。对于用户来说,安装体验是一样的。
17.3 SDK 入口与注册 API
上一节用 definePluginEntry 写了一个最简单的工具插件。但 OpenClaw 的插件系统远不止于此——它有三种入口函数、十几种注册方法,以及完整的 Hook 机制。
17.3.1 三种入口函数
根据插件类型选择不同的入口函数:
// 1. 通用工具插件
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
export default definePluginEntry({
id: "my-tool",
register(api) { /* 注册工具、Provider 等 */ }
});
// 2. 通道插件
import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core";
export default defineChannelPluginEntry({
id: "my-channel",
plugin: myChannelPlugin,
registerFull(api) { /* 注册 HTTP 路由、CLI 命令 */ }
});
// 3. Setup 入口(轻量加载)
import { defineSetupPluginEntry } from "openclaw/plugin-sdk/core";
export default defineSetupPluginEntry(myChannelPlugin);
为什么通道插件需要两个入口?
通道插件有一个 setup-entry.ts 文件,它在插件被禁用或未配置时也会被加载。它的作用是让 OpenClaw 的 onboarding 向导能够:
- 检查通道是否已配置(
inspectAccount) - 引导用户完成配置(
resolveAccount) - 显示在可用通道列表中
而不需要加载完整的通道运行时代码。
加载流程:
┌─────────────────────────────────────────┐
│ Gateway 启动 │
│ ├─ 扫描所有插件 │
│ ├─ 加载 setup-entry.ts(轻量) │
│ │ └─ 提供 inspectAccount/resolveAccount│
│ ├─ 检查插件是否已配置 │
│ │ ├─ 已配置 → 加载 index.ts(完整) │
│ │ └─ 未配置 → 跳过,等用户配置 │
│ └─ 调用 register() / registerFull() │
└─────────────────────────────────────────┘
17.3.2 注册 API 全景
api 对象是插件与 OpenClaw 交互的唯一通道。不同入口函数提供不同范围的 API:
// definePluginEntry 提供的 api 对象
api.registerTool(tooltip) // 注册工具
api.registerHook(hook) // 注册 Hook
api.registerProvider(provider) // 注册模型提供商
api.registerSpeechProvider(...) // 注册 TTS 提供商
api.registerMediaUnderstandingProvider(...) // 注册媒体理解
api.registerImageGenerationProvider(...) // 注册图片生成
// defineChannelPluginEntry 额外提供
api.registerFull(api) {
api.registerCli(...) // 注册 CLI 命令
api.registerHttpRoute(...) // 注册 HTTP 路由
api.registerChannel(...) // 注册通道
}
Provider 注册方法对照表:
| 注册方法 | 能力维度 | 典型用途 |
|---|---|---|
registerProvider |
文本推理 | 添加新的 LLM 提供商 |
registerSpeechProvider |
语音 | TTS + STT |
registerMediaUnderstandingProvider |
媒体理解 | 图片描述、音频转写 |
registerImageGenerationProvider |
图片生成 | DALL-E、SD 等 |
17.3.3 Hook 机制
Hook 是插件系统的"事件监听器"。OpenClaw 在 Agent 运行的各个阶段发出事件,插件可以注册 Hook 来响应这些事件。
api.registerHook({
event: "before:model-call", // 事件名称
description: "在模型调用前注入额外上下文",
async handler(context) {
// 可以修改上下文
// 返回修改后的值,或返回 null 表示不修改
return null;
}
});
Hook 的决策语义:所有匹配的 Hook 都会被调用,但只有第一个非 null 的返回值会被采纳。这叫做"竞态决策"——多个 Hook 竞争同一个决策权,先返回结果的获胜。
Hook 执行流程:
before:model-call 事件触发
├─ Hook A: handler() → null(不修改)
├─ Hook B: handler() → { extraContext: "..." }(修改)
└─ Hook C: 不再调用(B 已经返回了非 null 结果)
最终使用 Hook B 的返回值
17.3.4 导入路径规范
OpenClaw 的 SDK 使用精细导入路径——每个导入路径是一个独立的、自包含的模块。不要从根路径导入:
// ❌ 禁止:根路径 barrel 导入
import { something } from "openclaw/plugin-sdk";
// ✓ 正确:使用精确的子路径
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { createChatChannelPlugin } from "openclaw/plugin-sdk/core";
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
为什么这么设计?
旧版的 openclaw/plugin-sdk/compat 是一个"大杂烩"导入——导出几十个辅助函数。问题是:
- 导入一个函数会加载几十个无关模块 → 启动变慢
- 宽泛的导出容易制造循环依赖
- 无法区分哪些导出是稳定 API、哪些是内部实现
新的精细路径解决了这些问题:每个子路径只导出自己需要的几个函数,启动时只加载必要的代码。
常用导入路径速查:
| 导入路径 | 用途 | 主要导出 |
|---|---|---|
plugin-sdk/plugin-entry |
通用插件入口 | definePluginEntry |
plugin-sdk/core |
通道定义、通道构建器 | defineChannelPluginEntry, createChatChannelPlugin |
plugin-sdk/runtime-store |
持久化插件存储 | createPluginRuntimeStore |
plugin-sdk/provider-auth |
Provider 认证 | createProviderApiKeyAuthMethod |
plugin-sdk/channel-reply-pipeline |
回复前缀 + 输入中提示线 | createChannelReplyPipeline |
plugin-sdk/command-auth |
命令门控 | resolveControlCommandGate |
plugin-sdk/testing |
测试工具 | installCommonResolveTargetErrorCases |
plugin-sdk/keyed-async-queue |
有序异步队列 | KeyedAsyncQueue |
17.4 运行时 API
注册工具和 Hook 只是插件的一部分。很多时候,插件需要访问 OpenClaw 的运行时能力——比如读取 Agent 配置、启动子 Agent、调用 TTS、发起网页搜索。这些通过 api.runtime 对象提供。
17.4.1 api.runtime 全景
// 在 registerFull 中访问运行时
registerFull(api) {
const rt = api.runtime;
// Agent 相关
rt.agent.resolveAgentDir() // Agent 目录路径
rt.agent.resolveAgentWorkspaceDir() // Workspace 目录路径
rt.agent.resolveAgentIdentity() // Agent 身份信息
rt.agent.resolveThinkingDefault() // 默认思考级别
rt.agent.ensureAgentWorkspace() // 确保 Workspace 存在
rt.agent.runEmbeddedPiAgent({...}) // 运行嵌入式 Agent
// 子 Agent
rt.subagent.spawn({...}) // 派生子 Agent
rt.subagent.run({...}) // 运行子任务
// TTS(文本转语音)
rt.tts.synthesize({...}) // 合成语音
// 媒体理解
rt.mediaUnderstanding.describeImage({...}) // 图片描述
rt.mediaUnderstanding.transcribeAudio({...}) // 音频转写
// 图片生成
rt.imageGeneration.generate({...}) // 生成图片
// 网页搜索
rt.webSearch.search({...}) // 搜索网页
// 媒体处理
rt.media.download(url) // 下载媒体文件
// 配置
rt.config.loadConfig() // 加载配置
rt.config.writeConfigFile(...) // 写入配置
// 系统
rt.system.getPlatform() // 获取平台信息
rt.system.getNodeInfo() // 获取节点信息
// 事件
rt.events.on("event-name", handler) // 监听事件
// 日志
rt.logging.info("message") // 记录日志
rt.logging.error("message") // 记录错误
// 模型认证
rt.modelAuth.resolveApiKey(providerId) // 解析 API Key
// 状态
rt.state.get(key) // 读取状态
rt.state.set(key, value) // 写入状态
// 工具
rt.tools.invoke(name, params) // 调用其他工具
}
17.4.2 插件运行时存储
插件通常需要在运行时存储一些持久化数据(比如缓存的 token、用户偏好等)。OpenClaw 提供了 createPluginRuntimeStore:
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
// 创建存储
const store = createPluginRuntimeStore<MyData>("initial state");
// 在 registerFull 中设置运行时
registerFull(api) {
store.setRuntime(api.runtime);
}
// 在工具执行中读取
async execute(params, context) {
const rt = store.getRuntime();
const agentDir = rt.agent.resolveAgentDir();
// ...
}
为什么不用全局变量? 因为插件的运行时是延迟注入的——在 register() 阶段,运行时还没准备好。createPluginRuntimeStore 提供了一个"先占位、后填充"的模式,确保你在需要的时候一定能拿到运行时。
17.4.3 在工具中调用运行时
工具的 execute 函数通过 context 参数间接访问运行时:
api.registerTool({
name: "my_tool",
async execute(params, context) {
// context.config —— 当前 Agent 的完整配置
const apiKey = context.config.myPlugin?.apiKey;
// context.agentId —— 当前 Agent 的 ID
const agentId = context.agentId;
// context.sessionId —— 当前会话 ID
const sessionId = context.sessionId;
// 如果需要完整的运行时能力,通过 store 获取
const rt = store.getRuntime();
const workspaceDir = rt.agent.resolveAgentWorkspaceDir();
}
});
17.5 Manifest 系统
openclaw.plugin.json 是插件的"身份证"。它不仅告诉 OpenClaw "我是谁",还声明了配置结构、认证方式、UI 提示等元信息。
17.5.1 完整 Schema 参考
{
"id": "my-plugin", // 唯一标识(必填)
"name": "My Plugin", // 显示名称(必填)
"description": "插件描述", // 简短描述(必填)
"kind": "tool", // 插件类型:tool | channel | provider
"channels": ["my-channel"], // 声明提供的通道 ID(通道插件用)
"providers": ["my-provider"], // 声明提供的 Provider ID(Provider 插件用)
"configSchema": { // 配置的 JSON Schema
"type": "object",
"properties": { /* ... */ }
},
"providerAuthEnvVars": { // Provider 认证环境变量
"my-provider": ["MY_PROVIDER_API_KEY"]
},
"providerAuthChoices": [ // Provider 认证选项(UI 用)
{
"provider": "my-provider",
"method": "api-key",
"choiceId": "my-provider-api-key",
"choiceLabel": "My Provider API Key",
"groupId": "my-provider",
"groupLabel": "My Provider",
"cliFlag": "--my-provider-api-key",
"cliOption": "--my-provider-api-key <key>",
"cliDescription": "My Provider API Key"
}
],
"uiHints": { // UI 提示
"icon": "cloud-sun",
"category": "tools"
},
"contracts": { // 能力合约声明
"tools": ["my_tool"],
"providers": ["my-provider"]
}
}
17.5.2 providerAuthEnvVars 的作用
这个字段让 OpenClaw 在不加载插件代码的情况下,就能检测认证凭据是否可用:
启动流程:
1. 扫描所有插件的 providerAuthEnvVars
2. 检查环境变量中是否有对应的值
3. 如果有 → 标记该 Provider 为"已认证"
4. 在 onboarding 向导中显示为可用选项
全部不需要 import 插件的任何代码
这对启动速度至关重要——如果你的系统装了 20 个插件,但没有加载任何一个插件的运行时代码,启动会快得多。
17.5.3 configSchema 的设计原则
configSchema 使用标准 JSON Schema 格式。OpenClaw 会用它做两件事:
- 配置验证 —— 用户在
openclaw.json中填写配置时,错误的类型、缺失的必填字段会在启动时报错 - UI 生成 —— Web Dashboard 可以根据 Schema 自动生成配置表单
{
"configSchema": {
"type": "object",
"additionalProperties": false,
"properties": {
"myPlugin": {
"type": "object",
"properties": {
"apiKey": {
"type": "string",
"description": "API 密钥"
},
"maxRetries": {
"type": "integer",
"description": "最大重试次数",
"default": 3,
"minimum": 0,
"maximum": 10
},
"mode": {
"type": "string",
"enum": ["fast", "thorough"],
"description": "运行模式",
"default": "fast"
}
},
"required": ["apiKey"]
}
}
}
}
💡 提示:
additionalProperties: false是推荐的做法。它防止用户在配置中写错字段名(比如api_key而不是apiKey)时默默忽略,而是直接报错提示。
17.6 通道插件开发实战
工具插件给 Agent 增加"能力",通道插件给 Agent 增加"通道"——让它能在新的平台上收发消息。开发一个通道插件比工具插件复杂得多,因为你需要处理平台的 API 对接、消息格式转换、安全策略等。
17.6.1 通道插件的最小结构
extensions/acme-chat/
├── package.json # openclaw.channel 元数据
├── openclaw.plugin.json # 清单
├── index.ts # defineChannelPluginEntry
├── setup-entry.ts # defineSetupPluginEntry
├── api.ts # 公共导出(可选)
├── runtime-api.ts # 内部运行时导出(可选)
└── src/
├── channel.ts # ChannelPlugin 定义
├── channel.test.ts # 测试
├── client.ts # 平台 API 客户端
└── runtime.ts # 运行时存储(如需要)
17.6.2 package.json
{
"name": "@myorg/openclaw-acme-chat",
"version": "1.0.0",
"type": "module",
"openclaw": {
"extensions": ["./index.ts"],
"setupEntry": "./setup-entry.ts",
"channel": {
"id": "acme-chat",
"label": "Acme Chat",
"blurb": "Connect OpenClaw to Acme Chat."
}
}
}
openclaw.channel 是通道插件独有的元数据——它告诉 OpenClaw:"这是一个通道插件,提供 acme-chat 通道"。
17.6.3 构建 ChannelPlugin 对象
通道插件的核心是 createChatChannelPlugin。它接收一个声明式配置对象,帮你组合出完整的通道适配器:
import {
createChatChannelPlugin,
createChannelPluginBase,
} from "openclaw/plugin-sdk/core";
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
import { acmeChatApi } from "./client.js";
// 定义账户解析结果类型
type ResolvedAccount = {
accountId: string | null;
token: string;
allowFrom: string[];
dmPolicy: string | undefined;
};
// 从配置中解析账户信息
function resolveAccount(
cfg: OpenClawConfig,
accountId?: string | null,
): ResolvedAccount {
const section = (cfg.channels as Record<string, any>)?.["acme-chat"];
const token = section?.token;
if (!token) throw new Error("acme-chat: token is required");
return {
accountId: accountId ?? null,
token,
allowFrom: section?.allowFrom ?? [],
dmPolicy: section?.dmSecurity,
};
}
// 构建通道插件
export const acmeChatPlugin = createChatChannelPlugin<ResolvedAccount>({
base: createChannelPluginBase({
id: "acme-chat",
setup: {
resolveAccount,
// 检查账户状态(不暴露密钥)
inspectAccount(cfg, accountId) {
const section =
(cfg.channels as Record<string, any>)?.["acme-chat"];
return {
enabled: Boolean(section?.token),
configured: Boolean(section?.token),
tokenStatus: section?.token ? "available" : "missing",
};
},
},
}),
// DM 安全:谁可以给 Bot 发消息
security: {
dm: {
channelKey: "acme-chat",
resolvePolicy: (account) => account.dmPolicy,
resolveAllowFrom: (account) => account.allowFrom,
defaultPolicy: "allowlist",
},
},
// 配对:新 DM 联系人的审批流程
pairing: {
text: {
idLabel: "Acme Chat username",
message: "Send this code to verify your identity:",
notify: async ({ target, code }) => {
await acmeChatApi.sendDm(target, `Pairing code: ${code}`);
},
},
},
// 线程:回复如何关联到原始消息
threading: { topLevelReplyToMode: "reply" },
// 出站:发送消息到平台
outbound: {
attachedResults: {
sendText: async (params) => {
const result = await acmeChatApi.sendMessage(
params.to,
params.text,
);
return { messageId: result.id };
},
},
base: {
sendMedia: async (params) => {
await acmeChatApi.sendFile(params.to, params.filePath);
},
},
},
});
createChatChannelPlugin 帮你把多个适配器接口组合成一个完整的通道插件。你可以选择性地提供:
| 选项 | 作用 |
|---|---|
security.dm |
DM 安全策略解析 |
pairing.text |
基于文本的 DM 配对流程 |
threading |
回复模式(固定、按账户、自定义) |
outbound.attachedResults |
发送函数,返回消息 ID |
outbound.base |
基础发送(文本、媒体) |
17.6.4 入口文件和 Setup 入口
// index.ts —— 完整入口
import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core";
import { acmeChatPlugin } from "./src/channel.js";
export default defineChannelPluginEntry({
id: "acme-chat",
name: "Acme Chat",
description: "Acme Chat channel plugin",
plugin: acmeChatPlugin,
registerFull(api) {
// 注册 CLI 命令
api.registerCli(
({ program }) => {
program
.command("acme-chat")
.description("Acme Chat management");
},
{ commands: ["acme-chat"] },
);
// 注册 HTTP 路由(用于接收 Webhook)
api.registerHttpRoute({
path: "/acme-chat/webhook",
auth: "plugin",
handler: async (req, res) => {
const event = parseWebhookPayload(req);
await handleAcmeChatInbound(api, event);
res.statusCode = 200;
res.end("ok");
return true;
},
});
},
});
// setup-entry.ts —— 轻量入口
import { defineSetupPluginEntry } from "openclaw/plugin-sdk/core";
import { acmeChatPlugin } from "./src/channel.js";
export default defineSetupPluginEntry(acmeChatPlugin);
17.6.5 处理入站消息
通道插件需要接收平台发来的消息并转发给 OpenClaw。典型模式是通过 Webhook:
// 入站消息处理
async function handleAcmeChatInbound(api, event) {
// 1. 解析消息内容
const text = event.message.text;
const senderId = event.sender.id;
const chatId = event.chat.id;
// 2. 通过通道的 inbound handler 转发给 OpenClaw
// 具体的接线方式取决于平台 SDK
// 参考内置插件:extensions/msteams、extensions/googlechat
}
入站消息处理是通道特有的——每个平台的 API 不同、消息格式不同、鉴权方式不同。所以 OpenClaw 不提供统一的入站框架,而是让每个通道插件自己处理。你可以参考内置的 MS Teams 和 Google Chat 插件来了解具体的接线模式。
17.6.6 线程模式
threading 选项控制回复如何关联到原始消息:
| 模式 | 行为 | 适用场景 |
|---|---|---|
"reply" |
始终回复到原始消息 | Discord、Slack 等支持线程的平台 |
"none" |
不关联,作为独立消息发送 | SMS、邮件等不支持线程的平台 |
| 自定义函数 | 根据条件决定 | 混合场景 |
17.7 模型提供商插件开发实战
工具插件给 Agent 增加"手脚",通道插件给它增加"嘴巴和耳朵",模型提供商插件则给它换一个"大脑"——接入新的 LLM。
17.7.1 最小 Provider 插件
一个 Provider 插件只需要注册一个 Provider,包含 id、label、auth 和 catalog:
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth";
export default definePluginEntry({
id: "acme-ai",
name: "Acme AI",
description: "Acme AI model provider",
register(api) {
api.registerProvider({
id: "acme-ai",
label: "Acme AI",
docsPath: "/providers/acme-ai",
envVars: ["ACME_AI_API_KEY"],
// 认证方式
auth: [
createProviderApiKeyAuthMethod({
providerId: "acme-ai",
methodId: "api-key",
label: "Acme AI API key",
hint: "API key from your Acme AI dashboard",
optionKey: "acmeAiApiKey",
flagName: "--acme-ai-api-key",
envVar: "ACME_AI_API_KEY",
promptMessage: "Enter your Acme AI API key",
defaultModel: "acme-ai/acme-large",
}),
],
// 模型目录
catalog: {
order: "simple",
run: async (ctx) => {
const apiKey =
ctx.resolveProviderApiKey("acme-ai").apiKey;
if (!apiKey) return null;
return {
provider: {
baseUrl: "https://api.acme-ai.com/v1",
apiKey,
api: "openai-completions",
models: [
{
id: "acme-large",
name: "Acme Large",
reasoning: true,
input: ["text", "image"],
cost: {
input: 3, output: 15,
cacheRead: 0.3, cacheWrite: 3.75
},
contextWindow: 200000,
maxTokens: 32768,
},
{
id: "acme-small",
name: "Acme Small",
reasoning: false,
input: ["text"],
cost: {
input: 1, output: 5,
cacheRead: 0.1, cacheWrite: 1.25
},
contextWindow: 128000,
maxTokens: 8192,
},
],
},
};
},
},
});
},
});
这就是一个可工作的 Provider 插件。用户可以:
# Onboarding 时配置
openclaw onboard --acme-ai-api-key <key>
# 选择模型
/model acme-ai/acme-large
17.7.2 catalog.order 的含义
catalog.order 控制你的模型目录相对于内置 Provider 的合并顺序:
| 值 | 合并时机 | 适用场景 |
|---|---|---|
"simple" |
第一轮 | 普通 API Key 认证的 Provider |
"profile" |
simple 之后 | 需要认证 Profile 的 Provider |
"paired" |
profile 之后 | 需要合成多个相关条目的 Provider |
"late" |
最后一轮 | 覆盖已有 Provider(冲突时后者获胜) |
大多数第三方 Provider 用 "simple" 就够了。如果你做的是一个 OpenAI 兼容的代理/路由器,可能需要 "late" 来覆盖内置的 OpenAI 条目。
17.7.3 动态模型解析
如果你的 Provider 接受任意模型 ID(比如一个 API 代理或路由器),添加 resolveDynamicModel:
api.registerProvider({
// ... 其他字段
resolveDynamicModel: (ctx) => ({
id: ctx.modelId,
name: ctx.modelId,
provider: "acme-ai",
api: "openai-completions",
baseUrl: "https://api.acme-ai.com/v1",
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 128000,
maxTokens: 8192,
}),
});
这样即使用户输入了一个不在 catalog 中的模型 ID(比如 acme-ai/gpt-4-turbo-preview),Provider 也能正确处理。
如果解析模型需要网络请求(比如查询远端 API 获取模型信息),用 prepareDynamicModel 做异步预热——resolveDynamicModel 会在它完成后再次运行。
17.7.4 运行时钩子
大多数 Provider 只需要 catalog + resolveDynamicModel。但有些 Provider 需要更精细的控制。OpenClaw 提供了 22 个运行时钩子,按执行顺序排列:
| # | 钩子 | 用途 |
|---|---|---|
| 1 | catalog |
模型目录或 Base URL 默认值 |
| 2 | resolveDynamicModel |
接受任意上游模型 ID |
| 3 | prepareDynamicModel |
异步元数据获取 |
| 4 | normalizeResolvedModel |
传输层重写 |
| 5 | capabilities |
转录/工具元数据 |
| 6 | prepareExtraParams |
默认请求参数 |
| 7 | wrapStreamFn |
自定义请求头/Body |
| 8 | formatApiKey |
自定义 Token 格式 |
| 9 | refreshOAuth |
自定义 OAuth 刷新 |
| 10 | buildAuthDoctorHint |
认证修复提示 |
| 11 | isCacheTtlEligible |
Prompt Cache TTL 门控 |
| 12 | buildMissingAuthMessage |
缺失认证提示 |
| 13 | suppressBuiltInModel |
隐藏过时的上游条目 |
| 14 | augmentModelCatalog |
合成向前兼容条目 |
| 15 | isBinaryThinking |
二值思考开关 |
| 16 | supportsXHighThinking |
xhigh 推理支持 |
| 17 | resolveDefaultThinkingLevel |
默认 /think 策略 |
| 18 | isModernModelRef |
实时/冒烟模型匹配 |
| 19 | prepareRuntimeAuth |
推理前 Token 交换 |
| 20 | resolveUsageAuth |
自定义用量凭据 |
| 21 | fetchUsageSnapshot |
自定义用量端点 |
| 22 | onModelSelected |
选择后回调(遥测等) |
常用钩子示例:
Token 交换(每次推理前换取新 Token):
prepareRuntimeAuth: async (ctx) => {
const exchanged = await exchangeToken(ctx.apiKey);
return {
apiKey: exchanged.token,
baseUrl: exchanged.baseUrl,
expiresAt: exchanged.expiresAt,
};
},
自定义请求头(给每次推理请求加自定义 Header):
wrapStreamFn: (ctx) => {
if (!ctx.streamFn) return undefined;
const inner = ctx.streamFn;
return async (params) => {
params.headers = {
...params.headers,
"X-Acme-Version": "2",
};
return inner(params);
};
},
17.7.5 混合能力插件
一个插件可以同时注册多种能力。这是推荐的公司级插件模式——一个插件对应一个供应商,包含它所有的能力:
register(api) {
// 文本推理
api.registerProvider({ id: "acme-ai", /* ... */ });
// 语音合成
api.registerSpeechProvider({
id: "acme-ai",
label: "Acme Speech",
isConfigured: ({ config }) => Boolean(config.messages?.tts),
synthesize: async (req) => ({
audioBuffer: Buffer.from(/* PCM 数据 */),
outputFormat: "mp3",
fileExtension: ".mp3",
voiceCompatible: false,
}),
});
// 媒体理解
api.registerMediaUnderstandingProvider({
id: "acme-ai",
capabilities: ["image", "audio"],
describeImage: async (req) => ({ text: "A photo of..." }),
transcribeAudio: async (req) => ({ text: "Transcript..." }),
});
// 图片生成
api.registerImageGenerationProvider({
id: "acme-ai",
label: "Acme Images",
generate: async (req) => ({ /* 图片结果 */ }),
});
}
这种模式的好处是:用户只需要安装一个包,就能获得某个供应商的全部能力。配置也集中在一个地方。
17.7.6 简化入口:defineSingleProviderPluginEntry
如果你的 Provider 插件只注册一个文本 Provider,用 API Key 认证,加上一个 catalog 支持的运行时,可以用更简洁的入口:
import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry";
export default defineSingleProviderPluginEntry({
id: "acme-ai",
name: "Acme AI",
description: "Acme AI model provider",
provider: {
label: "Acme AI",
docsPath: "/providers/acme-ai",
auth: [
{
methodId: "api-key",
label: "Acme AI API key",
hint: "API key from your Acme AI dashboard",
optionKey: "acmeAiApiKey",
flagName: "--acme-ai-api-key",
envVar: "ACME_AI_API_KEY",
promptMessage: "Enter your Acme AI API key",
defaultModel: "acme-ai/acme-large",
},
],
catalog: {
buildProvider: () => ({
api: "openai-completions",
baseUrl: "https://api.acme-ai.com/v1",
models: [
{ id: "acme-large", name: "Acme Large" },
{ id: "acme-small", name: "Acme Small" },
],
}),
},
},
});
17.8 插件测试
不写测试的插件,上线就是赌博。
17.8.1 测试工具导入
import {
installCommonResolveTargetErrorCases,
shouldAckReaction,
removeAckReactionAfterReply,
} from "openclaw/plugin-sdk/testing";
// 测试中常用的类型
import type {
ChannelAccountSnapshot,
ChannelGatewayContext,
OpenClawConfig,
PluginRuntime,
RuntimeEnv,
MockFn,
} from "openclaw/plugin-sdk/testing";
17.8.2 通道插件单元测试
import { describe, it, expect } from "vitest";
import { acmeChatPlugin } from "./channel.js";
describe("acme-chat plugin", () => {
it("从配置中解析账户", () => {
const cfg = {
channels: {
"acme-chat": { token: "test-token", allowFrom: ["user1"] },
},
} as any;
const account = acmeChatPlugin.setup!.resolveAccount(cfg, undefined);
expect(account.token).toBe("test-token");
expect(account.allowFrom).toEqual(["user1"]);
});
it("检查账户状态时不泄露密钥", () => {
const cfg = {
channels: { "acme-chat": { token: "test-token" } },
} as any;
const result = acmeChatPlugin.setup!.inspectAccount!(cfg, undefined);
expect(result.configured).toBe(true);
expect(result.tokenStatus).toBe("available");
// 关键:不暴露 token 值
expect(result).not.toHaveProperty("token");
});
it("报告缺失配置", () => {
const cfg = { channels: {} } as any;
const result = acmeChatPlugin.setup!.inspectAccount!(cfg, undefined);
expect(result.configured).toBe(false);
});
});
测试原则:inspectAccount 是面向 UI/CLI 的,它只返回"是否已配置"的状态信息,绝对不能返回密钥值本身。这是安全测试的关键断言。
17.8.3 Provider 插件单元测试
import { describe, it, expect } from "vitest";
import { acmeProvider } from "./provider.js";
describe("acme-ai provider", () => {
it("解析动态模型", () => {
const model = acmeProvider.resolveDynamicModel!({
modelId: "acme-beta-v3",
} as any);
expect(model.id).toBe("acme-beta-v3");
expect(model.provider).toBe("acme-ai");
expect(model.api).toBe("openai-completions");
});
it("有 API Key 时返回目录", async () => {
const result = await acmeProvider.catalog!.run({
resolveProviderApiKey: () => ({ apiKey: "test-key" }),
} as any);
expect(result?.provider?.models).toHaveLength(2);
});
it("无 API Key 时返回 null", async () => {
const result = await acmeProvider.catalog!.run({
resolveProviderApiKey: () => ({ apiKey: undefined }),
} as any);
expect(result).toBeNull();
});
});
17.8.4 Mock 运行时
当插件代码需要访问运行时(比如读取配置、获取 Agent 目录),在测试中需要 Mock:
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
import type { PluginRuntime } from "openclaw/plugin-sdk/runtime-store";
import { vi } from "vitest";
const store = createPluginRuntimeStore<PluginRuntime>("test runtime not set");
// 测试前设置 Mock
const mockRuntime = {
agent: {
resolveAgentDir: vi.fn().mockReturnValue("/tmp/agent"),
resolveAgentWorkspaceDir: vi.fn().mockReturnValue("/tmp/agent/workspace"),
},
config: {
loadConfig: vi.fn(),
writeConfigFile: vi.fn(),
},
} as unknown as PluginRuntime;
store.setRuntime(mockRuntime);
// 测试后清理
store.clearRuntime();
Mock 的最佳实践:优先使用"每个实例的 Stub"而不是"原型链修改":
// ✓ 推荐:per-instance stub
const client = new MyChannelClient();
client.sendMessage = vi.fn().mockResolvedValue({ id: "msg-1" });
// ✗ 避免:prototype mutation
// MyChannelClient.prototype.sendMessage = vi.fn();
17.8.5 通道目标解析的共享测试
installCommonResolveTargetErrorCases 提供了一套标准的错误场景测试——所有通道插件都应该通过的"基础安全测试":
import { describe } from "vitest";
import { installCommonResolveTargetErrorCases } from "openclaw/plugin-sdk/testing";
describe("my-channel target resolution", () => {
installCommonResolveTargetErrorCases({
resolveTarget: ({ to, mode, allowFrom }) => {
return myChannelResolveTarget({ to, mode, allowFrom });
},
implicitAllowFrom: ["user1", "user2"],
});
// 加上通道特有的测试用例
it("should resolve @username targets", () => {
// ...
});
});
这个共享测试会自动验证:空目标、非法目标、未授权目标等常见错误场景。
17.8.6 运行测试
# 运行所有测试
pnpm test
# 运行特定插件的测试
pnpm test -- extensions/my-channel/
# 按测试名称过滤
pnpm test -- extensions/my-channel/ -t "resolves account"
# 带覆盖率
pnpm test:coverage
# 内存不足时的低配模式
OPENCLAW_TEST_PROFILE=low OPENCLAW_TEST_SERIAL_GATEWAY=1 pnpm test
17.8.7 合约测试
内置插件有"合约测试"——验证注册归属和形状正确性:
pnpm test -- src/plugins/contracts/
这些测试断言:
- 哪个插件注册了哪个 Provider
- 哪个插件注册了哪个 Speech Provider
- 注册形状是否正确
- 运行时合约是否合规
外部插件不受这些 Lint 规则约束,但遵循同样的模式是推荐做法。
17.9 Bundle 生态
不是所有的"插件"都是原生插件。OpenClaw 还支持从三个外部生态系统安装内容包:Codex、Claude 和 Cursor。这些被称为 Bundle。
17.9.1 Bundle vs 原生插件
原生插件(Native Plugin):
├─ 在进程内运行
├─ 可以注册任何能力
├─ 可以注册工具、Hook、通道、Provider
└─ 完全信任,完全控制
Bundle(内容包):
├─ 不加载运行时代码(安全边界更窄)
├─ 只映射部分功能到 OpenClaw 原生特性
├─ 支持:Skill 内容、命令、Hook Packs、MCP 工具、Settings
└─ 检测到但不执行的功能更多
17.9.2 支持的功能映射
| 功能 | 映射方式 | 适用格式 |
|---|---|---|
| Skill 内容 | 作为 OpenClaw Skill 加载 | 全部 |
| 命令 | commands/ 作为 Skill 根目录 |
Claude, Cursor |
| Hook Packs | OpenClaw 风格的 HOOK.md + handler.ts |
Codex |
| MCP 工具 | MCP 配置合并到嵌入式 Pi 设置中 | 全部 |
| Settings | Claude settings.json 导入为 Pi 默认值 |
Claude |
检测到但不执行的功能(会在诊断中显示,但不会运行):
- Claude 的
agents、hooks.json自动化、lspServers、outputStyles - Cursor 的
.cursor/agents、.cursor/hooks.json、.cursor/rules - Codex 的内联/应用元数据
17.9.3 三种 Bundle 格式
Codex Bundle
标记文件:.codex-plugin/plugin.json
可选内容:skills/、hooks/、.mcp.json、.app.json
Claude Bundle
两种检测模式:
- 基于 Manifest:
.claude-plugin/plugin.json - 无 Manifest:默认 Claude 布局(
skills/、commands/、agents/、hooks/、.mcp.json、settings.json)
Claude 特有行为:
commands/被当作 Skill 内容处理settings.json被导入到嵌入式 Pi 设置中(Shell 覆盖键会被清理).mcp.json中支持的 stdio 工具会暴露给嵌入式 Pihooks/hooks.json只检测不执行
Cursor Bundle
标记文件:.cursor-plugin/plugin.json
可选内容:skills/、.cursor/commands/、.cursor/agents/、.cursor/rules/、.cursor/hooks.json、.mcp.json
17.9.4 安装 Bundle
# 从本地目录安装
openclaw plugins install ./my-bundle
# 从压缩包安装
openclaw plugins install ./my-bundle.tgz
# 从 Claude Marketplace 安装
openclaw plugins marketplace list <marketplace-name>
openclaw plugins install <plugin-name>@<marketplace-name>
# 验证安装
openclaw plugins list
openclaw plugins inspect <id>
# 重启生效
openclaw gateway restart
17.9.5 检测优先级
OpenClaw 会先检查是否为原生插件:
openclaw.plugin.json或有效的package.json(含openclaw.extensions)→ 原生插件- Bundle 标记文件(
.codex-plugin/、.claude-plugin/或默认布局)→ Bundle
如果一个目录同时包含两种格式,OpenClaw 优先使用原生插件路径。这防止了双格式包被部分安装为 Bundle。
⚠️ 踩坑记录
问题:安装了一个 Claude 命令包,但命令没有出现在 Skill 列表中。
原因:Bundle 的
commands/目录中的 Markdown 文件不在检测的根目录内。解决:确保命令文件位于
commands/或skills/根目录下,而不是嵌套的子目录中。
17.10 社区生态与发布
OpenClaw 的插件生态正在快速增长。了解如何发布和发现插件,是成为 OpenClaw 高级用户的关键一步。
17.10.1 安装社区插件
# 安装(先查 ClawHub,再查 npm)
openclaw plugins install <package-name>
# 查看 ClawHub 上的可用插件
openclaw plugins marketplace list <marketplace-name>
17.10.2 热门社区插件
| 插件 | 用途 | 安装命令 |
|---|---|---|
| DingTalk | 钉钉企业机器人(Stream 模式) | openclaw plugins install @largezhou/ddingtalk |
| QQbot | QQ 机器人(支持私聊、群聊、富媒体) | openclaw plugins install @sliverp/qqbot |
| WeCom | 企业微信(WebSocket 持久连接) | openclaw plugins install @wecom/wecom-openclaw-plugin |
| Lossless Claw | DAG 上下文压缩(无损压缩 Token) | openclaw plugins install @martian-engineering/lossless-claw |
| Opik | Agent 追踪导出到 Opik(监控成本/Token/错误) | openclaw plugins install @opik/opik-openclaw |
| Codex App Server Bridge | Codex App Server 对话桥接 | openclaw plugins install openclaw-codex-app-server |
这些社区插件覆盖了中国用户最需要的场景——钉钉、QQ、企业微信的接入。
17.10.3 Voice Call 插件:语音通话的完整示例
Voice Call 是 OpenClaw 官方提供的一个通道插件,展示了如何构建一个复杂的、有安全要求的通道。它支持通过 Twilio、Telnyx、Plivo 拨打和接听电话。
# 安装
openclaw plugins install @openclaw/voice-call
# 重启
openclaw gateway restart
配置示例:
{
plugins: {
entries: {
"voice-call": {
enabled: true,
config: {
provider: "twilio", // twilio | telnyx | plivo | mock
fromNumber: "+15550001234",
toNumber: "+15550005678",
twilio: {
accountSid: "ACxxxxxxxx",
authToken: "...",
},
// Webhook 服务器
serve: {
port: 3334,
path: "/voice/webhook",
},
// 出站默认模式
outbound: {
defaultMode: "notify", // notify | conversation
},
// 流式语音
streaming: {
enabled: true,
streamPath: "/voice/stream",
},
},
},
},
},
}
Voice Call 插件展示了通道插件的几个高级特性:
- Webhook 安全:签名验证、防重放、代理信任
- 双向通话:出站通知 + 入站接听(allowlist 策略)
- TTS 集成:使用核心
messages.tts配置,支持插件级覆盖 - 流式媒体:WebSocket 媒体流 + 连接管理
- 过期清理:
staleCallReaperSeconds自动清理僵尸通话
Agent 通过 voice_call 工具控制通话:
Actions:
initiate_call(message, to?, mode?) // 发起通话
continue_call(callId, message) // 继续对话
speak_to_user(callId, message) // 朗读文本
end_call(callId) // 挂断
get_status(callId) // 查询状态
17.10.4 发布你的插件
如果你开发了一个有用的插件,可以提交到社区列表:
发布步骤:
- 发布到 ClawHub 或 npm —— 用户需要能通过
openclaw plugins install安装 - 源码放 GitHub —— 公开仓库,有安装文档和 Issue 跟踪
- 提交 PR —— 在官方文档的社区插件页面添加你的插件信息
质量门槛:
| 要求 | 原因 |
|---|---|
| 发布在 ClawHub 或 npm | 用户需要 openclaw plugins install 可用 |
| 公开 GitHub 仓库 | 源码审查、Issue 跟踪、透明度 |
| 安装和使用文档 | 用户需要知道怎么配置 |
| 活跃维护 | 近期有更新或 Issue 响应及时 |
低质量封装、所有权不明确、无人维护的包会被拒绝。
17.10.5 从旧版 SDK 迁移
如果你的插件还在使用旧版的 openclaw/plugin-sdk/compat 或 openclaw/extension-api,需要尽快迁移:
# 检查是否有废弃导入
grep -r "plugin-sdk/compat" my-plugin/
grep -r "openclaw/extension-api" my-plugin/
迁移对照表:
| 旧导入 | 新导入 |
|---|---|
import { X } from "openclaw/plugin-sdk/compat" |
import { X } from "openclaw/plugin-sdk/<specific-subpath>" |
import { runEmbeddedPiAgent } from "openclaw/extension-api" |
api.runtime.agent.runEmbeddedPiAgent(...) |
import { resolveAgentDir } from "openclaw/extension-api" |
api.runtime.agent.resolveAgentDir() |
import { resolveAgentIdentity } from "openclaw/extension-api" |
api.runtime.agent.resolveAgentIdentity() |
旧版兼容层会在下一个大版本中移除。所有核心插件已经迁移完毕。
迁移步骤:
- 搜索废弃导入
- 替换为精确的子路径导入
- 把
extension-api的直接调用替换为api.runtime注入 - 构建和测试
pnpm build
pnpm test -- my-plugin/
本章小结
- OpenClaw 插件系统围绕六大能力维度(文本推理、语音、媒体理解、图片生成、网页搜索、通道)构建
- 能力所有权模型保证同一维度只有一个活跃提供者,避免冲突
- 四层加载管线(发现→清单解析→运行时加载→就绪)确保启动效率
definePluginEntry是工具插件的入口,defineChannelPluginEntry是通道插件的入口- SDK 使用精细导入路径(
openclaw/plugin-sdk/<subpath>),避免根路径 barrel 导入 api.runtime提供运行时能力:Agent、子 Agent、TTS、媒体理解、网页搜索等- Manifest(
openclaw.plugin.json)是插件的身份证,声明配置、认证方式和 UI 提示 - 通道插件通过
createChatChannelPlugin声明式构建,不需要注册自己的消息工具 - Provider 插件通过
registerProvider注册,支持 22 个运行时钩子做精细控制 - Bundle 生态让 Codex/Claude/Cursor 的内容包可以直接在 OpenClaw 中使用
- 测试是插件质量的底线——单元测试、Mock 运行时、合约测试三件套
下一步
插件系统让 OpenClaw 成为一个可以无限扩展的平台。但"能用"和"能卖"之间还有一段距离——下一章讨论如何基于 OpenClaw 构建商业产品:API 暴露、白标定制、多租户 SaaS 架构,以及如何让你的 Agent 从"个人工具"变成"商业产品"。