模块边界与 SDK 契约 🔴

OpenClaw 最重要的架构守则是:第三方插件只能通过 openclaw/plugin-sdk/* 访问核心功能,绝对不能 import src/**。本章深入解析这个契约的实现原理和工程保障。

本章目标

读完本章你将能够:


一、模块边界是什么,为什么重要

在 OpenClaw 代码库中,"模块边界"(module boundary)指的是哪些代码是对外承诺稳定的 API,哪些是内部实现细节

这对 OpenClaw 格外重要,因为:

  1. 有第三方插件存在:社区开发的渠道插件、Provider 插件依赖 SDK API,破坏 API = 破坏生态
  2. Bundled 插件数量庞大(90+):核心代码的任何改动都可能影响大量插件
  3. 核心需要独立演进:核心团队需要重构内部实现,而不影响插件开发者
graph LR
    EXT["extensions/\n第三方插件"] -->|"只允许通过这里"| SDK["openclaw/plugin-sdk/*\n公开 SDK"]
    SDK -->|"实现细节"| CORE["src/ 内部实现\n私有代码"]
    EXT -. "禁止" .-> CORE

AGENTS.md 中的一句话总结了这个原则:

"This directory is the public contract between plugins and core. Changes here can affect bundled plugins and third-party plugins."


二、Plugin SDK 入口点全景

src/plugin-sdk/entrypoints.ts 中定义了所有公开的 SDK 入口点列表(pluginSdkEntrypoints),package.jsonexports 字段基于这个列表自动生成。

主要入口点及其用途:

入口点import 路径用途
coreopenclaw/plugin-sdk/core最核心的入口:插件定义、工具注册、Hook 注册
plugin-entryopenclaw/plugin-sdk/plugin-entry定义通用插件的基础 API
provider-entryopenclaw/plugin-sdk/provider-entry定义 LLM Provider 插件
channel-contractopenclaw/plugin-sdk/channel-contract渠道插件接口类型
channel-setupopenclaw/plugin-sdk/channel-setup渠道插件的 Setup 入口
runtimeopenclaw/plugin-sdk/runtime运行时 API(获取配置、发消息等)
runtime-envopenclaw/plugin-sdk/runtime-env运行时环境访问
routingopenclaw/plugin-sdk/routing路由相关工具
setupopenclaw/plugin-sdk/setupSetup 流程 API
sandboxopenclaw/plugin-sdk/sandbox工具沙箱(Bash 执行安全)
approval-runtimeopenclaw/plugin-sdk/approval-runtime执行审批运行时
config-runtimeopenclaw/plugin-sdk/config-runtime配置运行时访问
reply-runtimeopenclaw/plugin-sdk/reply-runtime消息回复运行时

core.ts — SDK 最核心的入口

src/plugin-sdk/core.ts(21KB)是最常用的 SDK 入口,它的作用是从内部模块精选并重新导出插件开发者需要的类型和工具:

// core.ts 的作用(简化版)
// 它不包含实现代码,只是一个精心设计的 re-export 文件

// 从 plugin-entry 重新导出(通用插件 API)
  AnyAgentTool,
  OpenClawPluginApi,
  OpenClawPluginCommandDefinition,
  OpenClawPluginDefinition,
  OpenClawPluginService,
  PluginCommandContext,
  ProviderPlugin,
  // ... 80+ 类型
} from './plugin-entry.js';

// 渠道插件专用类型
  ChannelPlugin,
  ChannelMessagingAdapter,
  ChannelOutboundAdapter,
  // ... 30+ 渠道类型
} from './channel-contract.js';

// 常用工具函数
  definePlugin,
  defineChannelPlugin,
  defineProviderPlugin,
} from './define-plugin.js';

三、插件入口点模式

definePlugin() — 通用插件定义

插件的主入口文件通常导出一个使用 definePlugin() 创建的对象:

// extensions/example-channel/src/index.ts

  name: 'example-channel',
  description: 'Example messaging channel',

  setup(api) {
    // api 是 OpenClawPluginApi 类型
    // 通过 api 注册所有能力

    // 注册渠道
    api.channel.register({
      id: 'example',
      // ...
    });

    // 注册工具
    api.tools.register({
      name: 'example_tool',
      description: '...',
      factory: async (ctx) => {
        // 工具实现
      }
    });

    // 注册 Hook
    api.hooks.on('before-agent-reply', async (ctx) => {
      // 前置钩子
    });
  }
});

provider-entry.ts — SingleProviderPlugin 模式

provider-entry.ts 暴露了一个高度封装的模式,让简单的 API Key 认证 Provider 只需几行代码:

// extensions/deepseek/src/index.ts(实际代码)

  id: 'deepseek',
  name: 'DeepSeek',
  description: 'DeepSeek AI models',
  provider: {
    id: 'deepseek',
    label: 'DeepSeek',
    docsPath: '/providers/deepseek',
    auth: [{
      methodId: 'api-key',
      label: 'DeepSeek API Key',
      envVar: 'DEEPSEEK_API_KEY',
      hint: 'Get your key from platform.deepseek.com',
    }],
    catalog: {
      buildProvider: ({ apiKey }) => {
        return new DeepSeekProvider({ apiKey });
      }
    }
  }
});

defineSingleProviderPlugin() 内部自动处理:

这让 Provider 插件的开发者不需要关心 OpenClaw 内部的认证基础设施细节。


四、API Baseline — 契约破坏检测

这是 OpenClaw 最有工程价值的机制之一。

问题

Plugin SDK 有 20+ 个入口点,每个导出数十个类型和函数。如何保证任何人的 PR 不会意外删除或重命名一个已有的公开 API?

解决方案:Snapshot + Diff

src/plugin-sdk/api-baseline.ts 中定义了一套机制:

脚本:scripts/generate-plugin-sdk-api-baseline.ts
        ↓ 执行
docs/.generated/plugin-sdk-api-baseline.json
        ↓ 提交到 Git

这个 JSON 文件包含当前所有 Plugin SDK 入口点的完整 API 快照:

// docs/.generated/plugin-sdk-api-baseline.json(节选)
{
  "generatedBy": "scripts/generate-plugin-sdk-api-baseline.ts",
  "modules": [
    {
      "category": "core",
      "entrypoint": "core",
      "importSpecifier": "openclaw/plugin-sdk/core",
      "exports": [
        {
          "exportName": "OpenClawPluginApi",
          "kind": "type",
          "declaration": "export type OpenClawPluginApi = { ... }"
        },
        {
          "exportName": "definePlugin",
          "kind": "function",
          "declaration": "export function definePlugin(def: OpenClawPluginDefinition): ..."
        }
        // ...
      ]
    }
  ]
}

CI 中会运行验证脚本:如果实际的 SDK 导出与 baseline.json 不一致(有新增或删除/改名),CI 报错,要求开发者显式更新 baseline.json 并说明理由

这不仅防止意外破坏,也让每个 SDK 变更都有清晰的 Git 记录。


五、各层边界规则汇总

根据 CLAUDE.md 的 "Architecture Boundaries" 章节,各层边界规则总结如下:

Plugin 边界

✅ 允许:
  extensions/**  →  openclaw/plugin-sdk/*(任意子路径)
  extensions/**  →  manifest metadata
  extensions/**  →  documented runtime helpers

❌ 禁止:
  extensions/**  →  src/**(任意核心内部模块)
  extensions/A  →   extensions/B/src/**(跨插件私有依赖)

核心 →  extensions/**(私有模块)

Channel 边界

✅ 允许:
  Plugin authors  →  openclaw/plugin-sdk/channel-contract(渠道接口类型)
  Plugin authors  →  openclaw/plugin-sdk/channel-setup(Setup API)

❌ 禁止:
  Plugin authors  →  src/channels/**(渠道内部实现)

Provider 边界

✅ 允许:
  Provider plugins  →  openclaw/plugin-sdk/provider-entry
  Provider plugins  →  openclaw/plugin-sdk/core

❌ 禁止:
  Core  →  plugins.entries.<id>.config(直接读取特定插件配置)
  Core  →  provider-specific internals(Provider 实现细节)

Gateway Protocol 边界

✅ 允许:
  任何客户端  →  src/gateway/protocol/**(协议类型可公开使用)

规则:
  协议变更 = 契约变更 = 需要版本化 + 向后兼容
  不兼容变更 = 递增 PROTOCOL_VERSION

六、sdk-alias 是契约实现的技术基础

模块边界在运行时通过 jiti + sdk-alias 机制强制执行:

flowchart TB
    PLUGIN["插件代码\nimport 'openclaw/plugin-sdk/core'"]
    JITI["jiti loader\n+ aliasMap"]
    CHECK{"resolveOpenClawPackageRoot\n找到 openclaw 根目录"}
    DIST["→ dist/plugin-sdk/core.js\n(打包/安装模式)"]
    SRC["→ src/plugin-sdk/core.ts\n(开发模式)"]
    CORE_INTERNAL["src/agents/\nsrc/channels/\nsrc/plugins/\n等内部模块"]

    PLUGIN -->|"import"| JITI
    JITI --> CHECK
    CHECK -->|"有 dist/"| DIST
    CHECK -->|"无 dist/"| SRC
    DIST --> OK["✅ 允许,解析成功"]
    SRC --> OK
    PLUGIN -. "import 'openclaw/src/agents/...'" .-> CORE_INTERNAL
    CORE_INTERNAL --> BLOCK["❌ 无 alias 映射\n→ 模块找不到 / 报错"]

src/ 下的内部模块不会被添加到 alias 映射表中,因此插件尝试直接 import 'src/**' 时,jiti 找不到对应的别名,会报模块解析错误。这是运行时层面的边界强制。


关键源码索引

文件大小作用
src/plugin-sdk/core.ts21KBSDK 最核心入口,精选导出
src/plugin-sdk/plugin-entry.ts-通用插件 API 类型定义
src/plugin-sdk/provider-entry.ts-Provider 插件 API,defineSingleProviderPlugin()
src/plugin-sdk/channel-contract.ts-渠道插件接口类型
src/plugin-sdk/entrypoints.ts1.4KBSDK 入口点列表(pluginSdkEntrypoints
src/plugin-sdk/api-baseline.ts14KBAPI Baseline 类型和生成逻辑
src/plugin-sdk/AGENTS.md1.9KBSDK 边界规则说明(给 AI 助手看)
scripts/lib/plugin-sdk-entrypoints.json-入口点列表 JSON(机器可读)
docs/.generated/plugin-sdk-api-baseline.json-当前 SDK API 快照(CI 验证基准)
CLAUDE.md(Architecture Boundaries 章节)-官方架构边界规范

小结

  1. 模块边界的核心规则:插件只能通过 openclaw/plugin-sdk/* 访问核心功能,不能直接 import src/**
  2. SDK 入口点是精心策划的:每个 openclaw/plugin-sdk/<name> 都是有意图的"公开门户",内部的 src/plugin-sdk/*.ts 文件数量是入口点数量的 10 倍以上,大部分是不对外的。
  3. API Baseline 防止意外破坏:snapshot 快照 + CI diff 检查,让每个 SDK API 变更都显式可见。
  4. 边界在运行时通过 jiti alias 强制:没有 alias 映射的路径在运行时找不到,形成技术层面的屏障。
  5. defineSingleProviderPlugin() 是高层封装:简单 Provider 只需声明认证方式和 catalog,无需关心底层框架细节。

延伸阅读