Skip to content

第9章 插件与扩展系统

"平台的生命周期取决于一个残酷的比率:用户能扩展的能力 ÷ 用户必须 fork 源码的次数。当这个比率趋近于零,平台就死了。"

本章要点

  • 理解插件系统的三层架构:注册表、加载器、运行时
  • 掌握插件生命周期钩子:从发现、加载到执行的完整链路
  • 深入插件 SDK:工具注册、HTTP 路由、CLI 命令扩展
  • 实战演练:从零开发一个天气预报插件

9.1 引言

前两章,我们深入了通道系统的设计与实现。通道是 OpenClaw 连接外部世界的窗口,但窗户的数量终究有限。如果用户需要一扇你没有预装的窗户呢?

OpenClaw 内建了 Telegram、Discord、WhatsApp 等八大通道,支持 Anthropic、OpenAI、Google 等二十多家模型供应商。但如果你需要接入 LINE?如果你想用一个内部部署的自研模型?如果你需要一个自定义的安全审计钩子?

在没有插件系统的框架中,答案是:fork 源码,改核心代码,然后祈祷下次升级不会冲突。这条路走得通,但走得痛苦。

OpenClaw 的答案干脆利落:写一个插件,五分钟搞定

当你需要的通道不在内置列表里时,你面对的是两条路:fork 源码(成本 O(n) 随升级线性增长),或者写一个插件(成本 O(1),一次投入永久受益)。插件系统的全部价值,就是让第一条路从"不得不走"变成"没人想走"。

🔥 深度洞察:生态系统的进化论

插件系统的成败,决定了一个平台是"产品"还是"生态"——这个区分关乎生死。iPhone 初代没有 App Store,它是一个优秀的产品;有了 App Store,它变成了一个生态系统。产品的价值由创造者决定,生态的价值由参与者共同创造。OpenClaw 的插件系统扮演的正是 App Store 的角色——它把 OpenClaw 从"一个人写的 Agent 运行时"变成"所有人都能扩展的 Agent 平台"。生物学中有个概念叫共生进化(coevolution):花朵进化出蜜腺吸引蜜蜂,蜜蜂进化出长喙采集花蜜,两者在互利中共同变得更好。插件系统就是 OpenClaw 与社区之间的共生接口——核心平台越稳定,插件越多;插件越多,核心平台越有价值。

关键概念:插件系统(Plugin System) 插件系统是 OpenClaw 实现"开闭原则"(对扩展开放,对修改封闭)的核心机制。通过统一的插件接口,开发者可以在不修改核心代码的前提下添加新通道、新工具、新 Provider 甚至新的 CLI 命令。内置功能和第三方扩展使用完全相同的插件接口——没有"一等公民"和"二等公民"之分。

插件系统是 OpenClaw 可扩展性的基石。通过插件,开发者可以添加新通道、新模型供应商、新工具、新钩子、新 HTTP 路由、新 CLI 命令——而不需要修改核心代码的一行一字。本章将深入 src/plugins/src/plugin-sdk/src/extensionAPI.ts 的实现,揭示这套系统如何在开放性与安全性之间走钢丝——既要敞开大门迎接创新,又要守住底线不放风险进门。

图 9-1:插件系统架构

图 9-1:OpenClaw 插件系统架构

9.2 插件类型与定义

9.2.1 OpenClawPluginDefinition

每个插件通过 OpenClawPluginDefinition 定义其身份和功能:

typescript
// src/plugins/types.ts
export type OpenClawPluginDefinition = {
  id: string;
  name: string;
  version?: string;
  description?: string;
  kind?: PluginKind;
  format?: PluginFormat;
  bundleFormat?: PluginBundleFormat;
  bundleCapabilities?: string[];

  configSchema?: OpenClawPluginConfigSchema;

  register?: (api: OpenClawPluginApi) => void;
  registerSetup?: (api: OpenClawPluginApi) => void;
};

kind 字段用于特殊类型的插件:

  • "memory" — 内存存储插件(如 LanceDB)
  • "context-engine" — 上下文引擎插件

9.2.2 插件注册 API

OpenClawPluginApi 是插件与系统交互的核心接口:

typescript
// src/plugins/types.ts — 插件与系统交互的核心 API(13 种注册方法)
export type OpenClawPluginApi = {
  config: OpenClawConfig;           // 当前配置快照
  runtime: PluginRuntime;           // 受控的系统访问入口
  registrationMode: PluginRegistrationMode;  // "setup" | "full"

  // === 能力注册(按领域分组)===
  registerChannel: (params) => void;         // 通道(Telegram/Discord/...)
  registerProvider: (params) => void;        // LLM 提供商
  registerSpeechProvider: (params) => void;  // 语音合成/识别
  registerTool: (tool, opts?) => void;       // 工具
  registerHook: (events, handler) => void;   // 生命周期钩子
  registerGatewayMethod: (method, handler) => void;  // WebSocket 方法
  registerHttpRoute: (params) => void;       // HTTP 路由
  registerService: (service) => void;        // 后台服务
  registerCommand: (command) => void;        // CLI 命令
  // ... 更多:registerImageGeneration, registerWebSearch, registerCli
};

registrationMode 有两种值:

  • "setup" — 仅加载配置 schema,用于启动前设置
  • "full" — 完整注册所有功能

9.3 插件注册表(Plugin Registry)

9.3.1 注册表结构

PluginRegistry 存储所有已注册的插件组件:

typescript
// src/plugins/registry.ts — 注册表存储所有插件组件
export type PluginRegistry = {
  plugins: PluginRecord[];              // 已注册的插件列表
  tools: PluginToolRegistration[];      // 工具
  hooks: PluginHookRegistration[];      // 生命周期钩子
  channels: PluginChannelRegistration[];  // 通道
  providers: PluginProviderRegistration[];  // LLM 提供商
  gatewayHandlers: GatewayRequestHandlers;  // WebSocket 方法
  httpRoutes: PluginHttpRouteRegistration[];  // HTTP 路由
  services: PluginServiceRegistration[];  // 后台服务
  diagnostics: PluginDiagnostic[];      // 冲突/错误诊断
  // ... 更多:speechProviders, commands, cliRegistrars 等
};

每个注册项都包含 pluginIdpluginNamesource(源文件路径)和 rootDir,用于追溯插件来源。

9.3.2 注册函数实现

createPluginRegistry 创建一个注册表实例,提供各种 register* 方法:

typescript
// src/plugins/registry.ts — 注册表工厂(展示 registerTool 核心逻辑)
export function createPluginRegistry(registryParams: PluginRegistryParams) {
  const registry = createEmptyPluginRegistry();

  const registerTool = (record: PluginRecord, tool: AnyAgentTool | OpenClawPluginToolFactory) => {
    // 统一为工厂函数:静态工具对象 → 包装为 () => tool
    const factory = typeof tool === "function" ? tool : () => tool;
    const names = typeof tool !== "function" ? [tool.name] : [];
    registry.tools.push({ pluginId: record.id, factory, names, source: record.source });
  };

  // 类似的 registerHook, registerChannel, registerProvider, registerHttpRoute...
  // 每个都做同样的事:验证 → 冲突检测 → 推入 registry 对应数组
  return { registry, registerTool, registerHook, registerChannel, /* ... */ };
}

9.3.3 冲突检测

注册表会检测并记录冲突:

typescript
// src/plugins/registry.ts — 冲突检测示例(Gateway 方法注册)
const registerGatewayMethod = (record: PluginRecord, method: string, handler) => {
  const trimmed = method.trim();
  // 检测冲突:核心方法不可覆盖,已注册方法不可重复
  if (coreGatewayMethods.has(trimmed) || registry.gatewayHandlers[trimmed]) {
    pushDiagnostic({ level: "error", pluginId: record.id,
      message: `gateway method already registered: ${trimmed}` });
    return;  // 记录诊断但不崩溃——一个插件的错误不影响其他插件
  }
  registry.gatewayHandlers[trimmed] = handler;
};

冲突会被记录为 PluginDiagnostic,而不是抛出异常,确保一个插件的错误不会影响其他插件。

9.4 插件加载器(Plugin Loader)

9.4.1 加载流程

loadPlugins 是插件加载的入口函数:

typescript
// src/plugins/loader.ts — 插件加载五阶段流程
export async function loadPlugins(options: PluginLoadOptions): Promise<PluginLoadResult> {
  // 1. 解析 → 2. 发现(内置 + 配置 + node_modules + 本地)
  const discovered = await discoverOpenClawPlugins({ config, workspaceDir, env });
  // 3. 并行加载(错误不阻断其他插件)
  const loaded = await Promise.all(discovered.map(async (src) => {
    try { return await loadPluginModule(src); }
    catch (error) { return { error, source: src }; }
  }));
  // 4. 注册到 Registry → 5. 激活插件服务
  const registry = createPluginRegistry({ /* ... */ });
  for (const p of loaded) {
    if ("error" in p) { registry.diagnostics.push(/* ... */); continue; }
    registerPlugin(p, registry);  // 触发 plugin.register(api)
  }
  if (options.activate !== false) await activatePlugins(registry);
  return registry;
}

⚠️ 注意:插件加载采用"容错并行"策略——单个插件加载失败不会阻止其他插件启动。但失败的插件会在 registry.diagnostics 中记录诊断信息。部署后务必检查 openclaw doctor 的输出,确认所有预期的插件都已成功加载。

💡 最佳实践:开发插件时,将重量级初始化(如建立网络连接、加载大型资源)放在 activate 阶段而非 register 阶段。register 阶段应该尽可能轻量——只做能力声明和回调注册。这样即使激活失败,其他插件的注册也不会受影响。



![图 9-2:插件加载与注册流程](../images/mermaid/fig-09-m2.png)

```mermaid
flowchart TD
    A["🔄 loadPlugins()"] --> B["📋 解析配置<br/>config.plugins.entries"]

    subgraph Discovery["🔍 四源发现"]
        B --> C1["内置插件<br/>discoverBundledPlugins()"]
        B --> C2["配置声明<br/>discoverConfiguredPlugins()"]
        B --> C3["node_modules<br/>discoverNodeModulesPlugins()"]
        B --> C4["本地开发<br/>discoverLocalPlugins()"]
    end

    C1 & C2 & C3 & C4 --> D["📦 并行加载模块<br/>Promise.all + Jiti 编译"]
    D --> E{加载成功?}
    E -->|"✅ 是"| F["📝 注册到 Registry<br/>plugin.register(api)"]
    E -->|"❌ 否"| G["⚠️ 记录诊断<br/>registry.diagnostics.push()"]
    G --> F

    subgraph Registration["注册能力"]
        F --> R1["registerChannel"]
        F --> R2["registerTool"]
        F --> R3["registerProvider"]
        F --> R4["registerHook"]
        F --> R5["registerHttpRoute"]
        F --> R6["registerService"]
    end

    R1 & R2 & R3 & R4 & R5 & R6 --> H{需要激活?}
    H -->|"是"| I["🚀 activatePlugins()<br/>启动后台服务"]
    H -->|"否"| J["✅ 返回 PluginRegistry"]
    I --> J

    style A fill:#e1f5fe,stroke:#2196f3
    style J fill:#c8e6c9,stroke:#4caf50
    style Discovery fill:#fff3e0,stroke:#ff9800
    style Registration fill:#f3e5f5,stroke:#9c27b0

图 9-2:插件加载与注册流程

9.4.2 模块解析与别名

插件可能通过不同的模块加载器(ESM、CommonJS、jiti)加载。为确保 openclaw/plugin-sdkopenclaw/extension-api 正确解析,加载器使用别名机制:

typescript
// src/plugins/loader.ts
function buildPluginLoaderAliasMap(modulePath: string): Record<string, string> {
  const pluginSdkAlias = resolvePluginSdkAlias({ modulePath });
  const extensionApiAlias = resolveExtensionApiAlias({ modulePath });
  return {
    ...(extensionApiAlias ? { "openclaw/extension-api": extensionApiAlias } : {}),
    ...(pluginSdkAlias ? { "openclaw/plugin-sdk": pluginSdkAlias } : {}),
    ...resolvePluginSdkScopedAliasMap({ modulePath }),
  };
}

这确保无论打包或加载方式如何,插件都能正确引用 OpenClaw SDK。

9.4.3 Jiti 运行时编译

对于 TypeScript 插件,OpenClaw 使用 Jiti 进行运行时编译:

typescript
// src/plugins/loader.ts
const jiti = createJiti(resolvePluginRuntimeModulePath() ?? import.meta.url, {
  esmResolve: true,
  interopDefault: true,
  moduleCache: false,
  alias: buildPluginLoaderAliasMap(modulePath),
  // ...
});

const pluginModule = await jiti.import(pluginPath);

Jiti 允许直接加载 .ts 文件,无需预编译,加速开发迭代。

9.5 插件运行时(Plugin Runtime)

9.5.1 Runtime 结构

PluginRuntime 是插件访问系统功能的入口:

typescript
// src/plugins/runtime/types.ts — 插件运行时(受控的系统访问入口)
export type PluginRuntime = {
  version: string;
  config: { loadConfig(); writeConfigFile(cfg); resolveConfigPath() };   // 配置读写
  agent: { resolveAgentDir(id); resolveAgentWorkspaceDir(id) };         // Agent 目录
  subagent: { spawnSubagent; listSubagents; killSubagent };             // 子 Agent 管控
  channel: {                                                             // 各通道运行时
    telegram: TelegramChannelRuntime;
    discord: DiscordChannelRuntime;
    // ... whatsapp, signal, slack, feishu 等
  };
  logging: { shouldLogVerbose(); createSubsystemLogger(name) };         // 日志
  state: { getStateDir(id); readState<T>(id, key); writeState<T>(id, key, val) };  // 持久状态
  // ... 更多功能模块
};

9.5.2 延迟加载

运行时使用 createLazyRuntimeNamedExport 实现延迟加载,避免循环依赖:

typescript
// src/plugin-sdk/lazy-runtime.ts — 延迟加载避免循环依赖
export function createLazyRuntimeNamedExport<T>(
  loadModule: () => Promise<{ [key: string]: T }>, exportName: string,
): () => T {
  let cached: T | undefined;
  return () => {
    if (!cached) {
      // Proxy 拦截属性访问,首次使用时才触发真正的模块加载
      cached = new Proxy({} as T, {
        get: (_target, prop) => { /* 异步等待模块加载并转发属性 */ },
      }) as T;
    }
    return cached;
  };
}

这允许通道插件在模块加载阶段就调用 getTelegramRuntime() 等,而实际的运行时模块在需要时才加载。

9.6 插件 SDK

9.6.1 SDK 结构

src/plugin-sdk/ 目录提供插件开发所需的所有类型和工具函数:

text
src/plugin-sdk/
├── core.ts              # 核心类型和工具函数
├── channel-actions.ts   # 通道消息操作
├── channel-config-helpers.ts  # 通道配置辅助函数
├── channel-pairing.ts   # 配对适配器
├── channel-send-result.ts  # 发送结果处理
├── allowlist-config-edit.ts  # 白名单配置
├── routing.ts           # 路由工具
├── infra-runtime.ts     # 基础设施运行时
├── lazy-runtime.ts      # 延迟加载工具
├── telegram.ts          # Telegram 专用工具
├── discord.ts           # Discord 专用工具
├── whatsapp.ts          # WhatsApp 专用工具
├── signal.ts            # Signal 专用工具
├── slack.ts             # Slack 专用工具
├── feishu.ts            # Feishu 专用工具
└── ...                  # 更多通道专用工具

9.6.2 defineChannelPluginEntry

defineChannelPluginEntry 是创建通道插件入口的标准模板(通道插件的完整架构设计详见第7章,各通道的具体实现详见第8章):

typescript
// src/plugin-sdk/core.ts — 通道插件入口模板
export function defineChannelPluginEntry<TPlugin extends ChannelPlugin>({
  id, name, plugin, configSchema, setRuntime, registerFull,
}: DefineChannelPluginEntryOptions<TPlugin>) {
  return definePluginEntry({ id, name, configSchema,
    register(api: OpenClawPluginApi) {
      setRuntime?.(api.runtime);         // 设置运行时引用
      api.registerChannel({ plugin });    // 注册通道
      if (api.registrationMode === "full") registerFull?.(api);  // 完整模式额外注册
    },
  });
}

9.6.3 createChannelPluginBase

createChannelPluginBase 创建通道插件的基础对象,减少重复代码:

typescript
// src/plugin-sdk/core.ts
export function createChannelPluginBase<TResolvedAccount>(
  params: CreateChannelPluginBaseOptions<TResolvedAccount>,
): CreatedChannelPluginBase<TResolvedAccount> {
  return {
    id: params.id,
    meta: {
      ...getChatChannelMeta(params.id as Parameters<typeof getChatChannelMeta>[0]),
      ...params.meta,
    },
    ...(params.setupWizard ? { setupWizard: params.setupWizard } : {}),
    ...(params.capabilities ? { capabilities: params.capabilities } : {}),
    ...(params.agentPrompt ? { agentPrompt: params.agentPrompt } : {}),
    ...(params.configSchema ? { configSchema: params.configSchema } : {}),
    ...(params.config ? { config: params.config } : {}),
    ...(params.security ? { security: params.security } : {}),
    ...(params.groups ? { groups: params.groups } : {}),
    setup: params.setup,
  } as CreatedChannelPluginBase<TResolvedAccount>;
}

9.6.4 共享适配器工厂

SDK 提供多种适配器工厂函数,简化常见模式的实现:

createTextPairingAdapter — 文本配对适配器:

typescript
// src/plugin-sdk/channel-pairing.ts
export function createTextPairingAdapter(params: {
  idLabel: string;
  message: string;
  normalizeAllowEntry?: (entry: string) => string;
  notify: (params: { cfg: OpenClawConfig; id: string; message: string }) => Promise<void>;
}): ChannelPairingAdapter {
  return {
    idLabel: params.idLabel,
    message: params.message,
    normalizeAllowEntry: params.normalizeAllowEntry,
    notify: params.notify,
  };
}

createScopedDmSecurityResolver — DM 安全策略解析器:

typescript
// src/plugin-sdk/channel-config-helpers.ts
export function createScopedDmSecurityResolver<TAccount>(params: {
  channelKey: string;
  resolvePolicy: (account: TAccount) => DmPolicy | undefined;
  resolveAllowFrom: (account: TAccount) => AllowFrom | undefined;
  policyPathSuffix?: string;
  normalizeEntry?: (entry: string) => string;
}): (params: { cfg: OpenClawConfig; accountId?: string | null }) => DmSecurityPolicy {
  // ...
}

createAttachedChannelResultAdapter — 出站结果适配器:

typescript
// src/plugin-sdk/channel-send-result.ts
export function createAttachedChannelResultAdapter<TSendResult>(params: {
  channel: string;
  sendText: (params: SendTextParams) => Promise<TSendResult>;
  sendMedia: (params: SendMediaParams) => Promise<TSendResult>;
  sendPoll?: (params: SendPollParams) => Promise<TSendResult>;
}): Partial<ChannelOutboundAdapter> {
  return {
    sendText: async (params) => {
      const result = await params.sendText(params);
      return attachChannelToResult(params.channel, result);
    },
    sendMedia: async (params) => {
      const result = await params.sendMedia(params);
      return attachChannelToResult(params.channel, result);
    },
    // ...
  };
}

9.7 插件钩子系统

9.7.1 钩子类型

OpenClaw 的钩子系统允许插件在系统事件发生时执行自定义逻辑:

typescript
// src/plugins/types.ts
export type PluginHookName =
  | "before_agent_start"
  | "after_agent_start"
  | "before_tool_call"
  | "after_tool_call"
  | "before_model_call"
  | "after_model_call"
  | "before_message_send"
  | "after_message_send"
  | "on_inbound_message"
  | "on_outbound_message"
  | "on_session_created"
  | "on_session_reset"
  | "on_config_reload"
  | "on_compaction"
  | "on_subagent_spawn"
  | "on_subagent_result"
  // ... 更多钩子
  ;

9.7.2 类型化钩子

类型化钩子提供更严格的类型安全和更好的开发体验:

基于 VitePress 构建