0

ch-17

约 15000 字DraftOpenClaw Book

第 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) —— 判断目标是否已经是平台 ID
  • resolveTarget(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 —— 执行函数,接收 paramscontext

⚠️ 踩坑记录

问题:工具注册了但 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 做的事很简单:

  1. 读取 openclaw.plugin.json 确认是有效插件
  2. 复制到 ~/.openclaw/plugins/<id>/
  3. 记录到插件注册表
  4. 下次 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 会用它做两件事:

  1. 配置验证 —— 用户在 openclaw.json 中填写配置时,错误的类型、缺失的必填字段会在启动时报错
  2. 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,包含 idlabelauthcatalog

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 还支持从三个外部生态系统安装内容包:CodexClaudeCursor。这些被称为 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 的 agentshooks.json 自动化、lspServersoutputStyles
  • 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.jsonsettings.json

Claude 特有行为:

  • commands/ 被当作 Skill 内容处理
  • settings.json 被导入到嵌入式 Pi 设置中(Shell 覆盖键会被清理)
  • .mcp.json 中支持的 stdio 工具会暴露给嵌入式 Pi
  • hooks/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 会先检查是否为原生插件:

  1. openclaw.plugin.json 或有效的 package.json(含 openclaw.extensions)→ 原生插件
  2. 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 发布你的插件

如果你开发了一个有用的插件,可以提交到社区列表:

发布步骤:

  1. 发布到 ClawHub 或 npm —— 用户需要能通过 openclaw plugins install 安装
  2. 源码放 GitHub —— 公开仓库,有安装文档和 Issue 跟踪
  3. 提交 PR —— 在官方文档的社区插件页面添加你的插件信息

质量门槛:

要求 原因
发布在 ClawHub 或 npm 用户需要 openclaw plugins install 可用
公开 GitHub 仓库 源码审查、Issue 跟踪、透明度
安装和使用文档 用户需要知道怎么配置
活跃维护 近期有更新或 Issue 响应及时

低质量封装、所有权不明确、无人维护的包会被拒绝。

17.10.5 从旧版 SDK 迁移

如果你的插件还在使用旧版的 openclaw/plugin-sdk/compatopenclaw/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()

旧版兼容层会在下一个大版本中移除。所有核心插件已经迁移完毕。

迁移步骤:

  1. 搜索废弃导入
  2. 替换为精确的子路径导入
  3. extension-api 的直接调用替换为 api.runtime 注入
  4. 构建和测试
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 从"个人工具"变成"商业产品"。