主题
OpenClaw 源码解读(六)插件系统
本文基于 OpenClaw 2026.3.2 源码,深入解读插件系统的架构设计与实现细节。
一、模块概览
插件系统是 OpenClaw 的可扩展性核心,使得第三方开发者可以在不修改主仓库的情况下为系统添加新的消息渠道(如 MS Teams、Matrix、Zalo)、AI Provider(如自定义模型网关)、Agent 工具、钩子处理器、CLI 命令、HTTP 路由、后台服务等。
1.1 核心源码分布
| 目录/文件 | 行数(约) | 职责 |
|---|---|---|
src/plugins/types.ts | ~400+ | 完整类型定义(API、Hook、Tool、Command 等) |
src/plugins/registry.ts | ~500+ | 注册表工厂(12 种注册能力) |
src/plugins/loader.ts | ~370+ | 插件加载器(发现→校验→加载→注册) |
src/plugins/discovery.ts | ~350+ | 插件发现(文件系统扫描 + 安全校验) |
src/plugins/manifest.ts | ~200 | 清单解析(openclaw.plugin.json + package.json) |
src/plugins/install.ts | ~120+ | 插件安装(npm 包 + 本地路径) |
src/plugins/http-path.ts | ~30 | HTTP 路由路径标准化 |
src/plugins/http-registry.ts | ~30 | HTTP 路由注册辅助 |
src/plugins/config-schema.ts | ~20 | 配置 Schema 辅助 |
src/plugin-sdk/index.ts | ~350+ | Plugin SDK(面向开发者的公共 API 导出) |
src/plugin-sdk/file-lock.ts | ~100+ | 文件锁工具 |
src/plugin-sdk/webhook-path.ts | ~50 | Webhook 路径解析 |
src/plugin-sdk/webhook-targets.ts | ~200+ | Webhook 目标注册与匹配 |
src/plugin-sdk/webhook-request-guards.ts | ~200+ | Webhook 请求安全守卫 |
src/plugin-sdk/channel-lifecycle.ts | ~50 | 渠道生命周期工具 |
src/plugin-sdk/status-helpers.ts | ~150 | 渠道状态构建辅助 |
extensions/*/ | — | 扩展渠道实现(MS Teams、Matrix、Zalo 等) |
插件系统/01-infographic-plugin-system-overview-1775150715818.png)
1.2 系统全景
┌───────────────────────────────────────────────────────────────────────┐
│ 插件系统全景 │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 插件来源(四种优先级) │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ config │ │workspace │ │ global │ │ bundled │ │ │
│ │ │ 配置指定 │ │ 工作区 │ │ 全局安装 │ │ 内置插件 │ │ │
│ │ │ (优先级0) │ │ (优先级1) │ │ (优先级2) │ │ (优先级3) │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │
│ └──────────────────────────┬──────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Discovery(插件发现 + 安全校验) │ │
│ │ ┌──────────────────────────────────────────────────────────┐ │ │
│ │ │ 文件系统扫描 → 路径逃逸检查 → 权限检查 → 所有权检查 │ │ │
│ │ └──────────────────────────────────────────────────────────┘ │ │
│ └──────────────────────────┬──────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Loader(插件加载器) │ │
│ │ ┌──────────────────────────────────────────────────────────┐ │ │
│ │ │ Manifest 解析 → Config Schema 验证 → Module 导入(jiti) │ │ │
│ │ │ → register(api) 调用 → Plugin Record 构建 │ │ │
│ │ └──────────────────────────────────────────────────────────┘ │ │
│ └──────────────────────────┬──────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Registry(注册表) │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ Tools │ │ Channels │ │Providers │ │ Hooks │ │ │
│ │ │ 工具 │ │ 渠道 │ │ 模型提供者│ │ 钩子 │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ Gateway │ │ HTTP │ │ CLI │ │Services │ │ │
│ │ │ Methods │ │ Routes │ │ Commands │ │ 后台服务 │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │
│ │ ┌──────────┐ ┌──────────┐ │ │
│ │ │ Commands │ │Diagnostics│ │ │
│ │ │ 自定义命令│ │ 诊断信息 │ │ │
│ │ └──────────┘ └──────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Plugin SDK(面向开发者) │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ Webhook │ │ File │ │ Status │ │ Lifecycle│ │ │
│ │ │ Helpers │ │ Lock │ │ Builders │ │ Helpers │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────────────┘二、Plugin API——开发者的入口
2.1 OpenClawPluginApi
这是插件开发者与 OpenClaw 交互的唯一接口:
typescript
// src/plugins/types.ts
type OpenClawPluginApi = {
// ── 元信息 ──
id: string; // 插件 ID
name: string; // 显示名称
version?: string; // 版本号
description?: string; // 描述
source: string; // 源文件路径
config: OpenClawConfig; // 当前配置
pluginConfig?: Record<string, unknown>; // 插件私有配置
runtime: PluginRuntime; // 运行时环境
logger: PluginLogger; // 日志记录器
// ── 12 种注册能力 ──
registerTool(tool, opts?): void; // ① 注册 Agent 工具
registerHook(events, handler, opts?): void; // ② 注册事件钩子
registerHttpRoute(params): void; // ③ 注册 HTTP 路由
registerChannel(registration): void; // ④ 注册消息渠道
registerGatewayMethod(method, handler): void; // ⑤ 注册 Gateway RPC 方法
registerCli(registrar, opts?): void; // ⑥ 注册 CLI 命令
registerService(service): void; // ⑦ 注册后台服务
registerProvider(provider): void; // ⑧ 注册 AI 模型 Provider
registerCommand(command): void; // ⑨ 注册自定义命令(绕过 Agent)
// ── 工具方法 ──
resolvePath(input: string): string; // 路径解析
on<K>(hookName, handler, opts?): void; // 生命周期钩子注册
};2.2 插件定义(两种形式)
typescript
// 形式 1: 对象定义
export default {
id: "my-plugin",
name: "My Plugin",
version: "1.0.0",
register(api: OpenClawPluginApi) {
api.registerTool(myTool);
api.registerChannel(myChannel);
}
};
// 形式 2: 函数定义(简写)
export default function(api: OpenClawPluginApi) {
api.registerTool(myTool);
api.registerChannel(myChannel);
}加载器自动识别两种形式:
typescript
function resolvePluginModuleExport(moduleExport: unknown) {
const resolved = moduleExport?.default ?? moduleExport;
if (typeof resolved === "function") {
return { register: resolved }; // 形式 2
}
if (typeof resolved === "object") {
return {
definition: resolved,
register: resolved.register ?? resolved.activate, // 形式 1(兼容 activate)
};
}
return {};
}2.3 十二种注册能力
| # | 注册方法 | 注册内容 | 典型场景 |
|---|---|---|---|
| ① | registerTool | Agent 工具 | 自定义搜索、代码执行等 |
| ② | registerHook | 事件钩子 | 消息预处理/后处理 |
| ③ | registerHttpRoute | HTTP 路由 | Webhook 接收、OAuth 回调 |
| ④ | registerChannel | 消息渠道 | MS Teams、Matrix、Zalo |
| ⑤ | registerGatewayMethod | Gateway RPC | 自定义网关命令 |
| ⑥ | registerCli | CLI 子命令 | openclaw my-plugin ... |
| ⑦ | registerService | 后台服务 | 消息队列消费者、心跳 |
| ⑧ | registerProvider | AI Provider | 自定义模型网关、认证流 |
| ⑨ | registerCommand | 自定义命令 | /tts 等绕过 Agent 的命令 |
| ⑩ | on("before_prompt_build") | 提示词钩子 | 注入上下文到系统提示词 |
| ⑪ | on("before_agent_start") | Agent 启动钩子 | 修改消息列表 |
| ⑫ | on("config_reload") | 配置热重载钩子 | 响应配置变更 |
插件系统/02-infographic-twelve-register-abilities-1775150716545.png)
三、插件发现——安全第一
3.1 四层来源优先级
config (优先级 0) → 配置文件中指定的插件路径
↓
workspace (优先级 1) → 工作区 .openclaw/plugins/ 目录
↓
global (优先级 2) → 全局 ~/.openclaw/plugins/ 目录
↓
bundled (优先级 3) → 内置插件(随安装包分发)3.2 发现流程
typescript
// src/plugins/discovery.ts
function discoverInDirectory(params: {
dir: string;
origin: PluginOrigin; // "bundled" | "global" | "workspace" | "config"
ownershipUid?: number | null;
workspaceDir?: string;
candidates: PluginCandidate[];
diagnostics: PluginDiagnostic[];
seen: Set<string>;
})扫描目录时的安全检查:
扫描插件目录
│
├── 读取 package.json
│ ├── 有 openclaw.extensions 字段 → 按声明的入口点加载
│ └── 无 → 查找默认入口(index.ts/index.js/index.mjs/index.cjs)
│
├── 安全校验(四道检查)
│ ├── ① 路径逃逸检查:源文件 realpath 是否在插件根目录内
│ ├── ② 权限检查:路径是否 world-writable(0o002 位)
│ ├── ③ 所有权检查:文件 UID 是否与当前用户一致(非 bundled)
│ └── ④ 硬链接检查:openBoundaryFileSync 检测符号链接/硬链接逃逸
│
├── 忽略规则
│ ├── .bak 后缀目录
│ ├── .backup-* 目录
│ ├── .disabled 目录
│ └── .d.ts 类型文件
│
└── 构建 PluginCandidate
├── idHint(从包名推导)
├── source(绝对路径)
├── rootDir(插件根目录 realpath)
└── origin(来源类型)3.3 路径逃逸防护
typescript
function checkSourceEscapesRoot(params: { source: string; rootDir: string }) {
const sourceReal = safeRealpathSync(params.source);
const rootReal = safeRealpathSync(params.rootDir);
if (!sourceReal || !rootReal) return null;
if (!sourceReal.startsWith(rootReal + path.sep) && sourceReal !== rootReal) {
return {
reason: "source_escapes_root",
sourcePath: params.source,
sourceRealPath: sourceReal,
rootRealPath: rootReal,
};
}
return null;
}防护场景:攻击者在插件目录中放一个符号链接指向 /etc/passwd → realpath 逃逸检测阻断。
插件系统/03-infographic-discovery-security-1775150717318.png)
3.4 所有权检查
typescript
if (
params.origin !== "bundled" && // 内置插件跳过
params.uid !== null &&
typeof stat.uid === "number" &&
stat.uid !== params.uid && // 文件不属于当前用户
stat.uid !== 0 // 也不属于 root
) {
return { reason: "path_suspicious_ownership", ... };
}3.5 Provenance 追踪
加载完插件后,系统检查每个已加载的插件是否有安装来源记录:
typescript
function warnAboutUntrackedLoadedPlugins(params) {
for (const plugin of params.registry.plugins) {
if (plugin.status !== "loaded" || plugin.origin === "bundled") continue;
if (isTrackedByProvenance({ pluginId: plugin.id, source: plugin.source, index: params.provenance })) {
continue;
}
// 无安装记录 → 发出警告
params.registry.diagnostics.push({
level: "warn",
pluginId: plugin.id,
message: "loaded without install/load-path provenance; treat as untracked local code",
});
}
}四、清单文件——openclaw.plugin.json
4.1 清单结构
json
{
"id": "voice-call",
"kind": "channel",
"name": "Voice Call",
"description": "Telephony voice call integration",
"version": "1.0.0",
"channels": ["voice-call"],
"providers": [],
"skills": [],
"configSchema": {
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": { "type": "boolean" },
"provider": { "type": "string", "enum": ["telnyx", "twilio", "plivo", "mock"] },
...
}
},
"uiHints": {
"provider": { "label": "Provider", "help": "Use twilio, telnyx, or mock." },
"twilio.authToken": { "label": "Twilio Auth Token", "sensitive": true },
"tunnel.ngrokAuthToken": { "label": "ngrok Auth Token", "sensitive": true, "advanced": true }
}
}4.2 关键字段
| 字段 | 必须 | 说明 |
|---|---|---|
id | ✅ | 插件唯一标识(也是配置键名) |
configSchema | ✅ | JSON Schema 格式的配置定义 |
kind | ❌ | 插件类型(channel / provider / tool 等) |
channels | ❌ | 该插件注册的渠道 ID 列表 |
providers | ❌ | 该插件注册的 Provider ID 列表 |
skills | ❌ | 该插件提供的技能 ID 列表 |
uiHints | ❌ | UI 展示提示(标签、敏感字段、高级选项) |
4.3 package.json 中的 openclaw 字段
json
{
"name": "@openclaw/msteams",
"version": "2026.3.2",
"openclaw": {
"extensions": ["./dist/index.js"],
"channel": {
"id": "msteams",
"label": "Microsoft Teams",
"docsPath": "/channels/msteams",
"blurb": "Connect to Microsoft Teams."
},
"install": {
"npmSpec": "@openclaw/msteams",
"localPath": "../extensions/msteams"
}
}
}openclaw.extensions 声明入口点文件列表。一个包可以包含多个扩展。
插件系统/04-infographic-manifest-structure-1775150718198.png)
五、注册表——12 种能力的管理中心
5.1 注册表结构
typescript
// src/plugins/registry.ts
type PluginRegistry = {
plugins: PluginRecord[]; // 所有插件的记录
tools: PluginToolRegistration[]; // Agent 工具
hooks: PluginHookRegistration[]; // 事件钩子
typedHooks: TypedPluginHookRegistration[]; // 类型化钩子
channels: PluginChannelRegistration[]; // 消息渠道
providers: PluginProviderRegistration[]; // AI 模型 Provider
gatewayHandlers: GatewayRequestHandlers; // Gateway RPC 方法
httpRoutes: PluginHttpRouteRegistration[]; // HTTP 路由
cliRegistrars: PluginCliRegistration[]; // CLI 命令注册器
services: PluginServiceRegistration[]; // 后台服务
commands: PluginCommandRegistration[]; // 自定义命令
diagnostics: PluginDiagnostic[]; // 诊断信息
};5.2 插件记录
每个已加载的插件有一份 PluginRecord,记录其所有注册信息:
typescript
type PluginRecord = {
id: string;
name: string;
version?: string;
source: string; // 源文件绝对路径
origin: PluginOrigin; // 来源:bundled/global/workspace/config
enabled: boolean;
status: "loaded" | "disabled" | "error";
error?: string; // 错误信息(如果 status === "error")
toolNames: string[]; // 注册的工具名列表
hookNames: string[]; // 注册的钩子名列表
channelIds: string[]; // 注册的渠道 ID 列表
providerIds: string[]; // 注册的 Provider ID 列表
gatewayMethods: string[]; // 注册的 Gateway 方法列表
cliCommands: string[]; // 注册的 CLI 命令列表
services: string[]; // 注册的服务 ID 列表
commands: string[]; // 注册的自定义命令列表
httpRoutes: number; // 注册的 HTTP 路由数量
hookCount: number; // 注册的钩子数量
configSchema: boolean; // 是否有配置 Schema
configUiHints?: Record<string, PluginConfigUiHint>;
configJsonSchema?: Record<string, unknown>;
};5.3 注册冲突检测
typescript
// Gateway 方法冲突检测
const registerGatewayMethod = (record, method, 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;
record.gatewayMethods.push(trimmed);
};HTTP 路由有两种冲突策略:
- 默认:拒绝注册并发出诊断警告
replaceExisting: true:替换已有路由(热重载场景)
插件系统/05-infographic-registry-structure-1775150718981.png)
六、加载器——从文件到运行时
6.1 完整加载流程
插件候选列表(来自 Discovery)
│
▼
遍历每个候选
│
├── 构建 PluginRecord(初始状态)
│
├── 检查 Allowlist
│ ├── plugins.allow 非空 → 只加载列表中的插件
│ └── plugins.allow 为空 → 加载所有(并发出安全警告)
│
├── 查找并解析 openclaw.plugin.json
│ ├── 有清单 → 提取 configSchema + uiHints
│ └── 无清单 → 可以继续(Schema 可选)
│
├── 验证 Config Schema(如果有)
│ └── validatePluginConfigAgainstSchema(schema, pluginConfig)
│
├── 动态导入模块(jiti)
│ └── import(source) → 获取 module export
│
├── 解析导出
│ ├── resolvePluginModuleExport()
│ └── 提取 register/activate 函数
│
├── 构建 Plugin API 对象
│ └── 绑定 12 种注册方法到 record
│
├── 调用 register(api) / activate(api)
│ └── 插件在此注册工具/渠道/钩子/...
│
├── 更新 PluginRecord 状态
│ └── status: "loaded"
│
└── 记录诊断信息
└── 成功/失败/警告6.2 Config Schema 验证
插件系统/06-infographic-loader-pipeline-1775150719966.png)
在调用 register() 之前,加载器先验证插件私有配置是否符合其声明的 Schema:
typescript
function validatePluginConfigAgainstSchema(params: {
schema: Record<string, unknown>;
value: Record<string, unknown> | undefined;
}): { ok: true; value: ... } | { ok: false; errors: string[] } {
const validator = compilePluginSchemaValidator(params.schema);
const result = validator(params.value);
if (result.ok) return { ok: true, value: params.value };
return { ok: false, errors: result.errors.map(e => e.text) };
}6.3 错误隔离
每个插件的加载失败不会影响其他插件:
typescript
function recordPluginError(params) {
params.logger.error(`${params.logPrefix}${errorText}`);
params.record.status = "error";
params.record.error = errorText;
params.registry.plugins.push(params.record); // 仍然记录到注册表
params.registry.diagnostics.push({
level: "error",
pluginId: params.record.id,
message: `${params.diagnosticMessagePrefix}${errorText}`,
});
}6.4 ID 去重与优先级
同一个 pluginId 只保留第一个加载成功的实例。由于候选列表按来源优先级排序(config → workspace → global → bundled),高优先级来源的插件自动覆盖低优先级的。
typescript
const seenIds = new Map<string, PluginRecord["origin"]>();
for (const candidate of candidates) {
const pluginId = resolvePluginId(candidate);
if (seenIds.has(pluginId)) {
// 已有更高优先级的同名插件 → 跳过
continue;
}
// ... 加载逻辑
seenIds.set(pluginId, candidate.origin);
}七、插件安装
7.1 安装入口
typescript
// src/plugins/install.ts
type InstallPluginResult =
| { ok: true; pluginId: string; targetDir: string; extensions: string[]; ... }
| { ok: false; error: string; code?: PluginInstallErrorCode };7.2 安装验证
安装前的五项验证:
typescript
const PLUGIN_INSTALL_ERROR_CODE = {
INVALID_NPM_SPEC: "invalid_npm_spec",
MISSING_OPENCLAW_EXTENSIONS: "missing_openclaw_extensions",
EMPTY_OPENCLAW_EXTENSIONS: "empty_openclaw_extensions",
NPM_PACKAGE_NOT_FOUND: "npm_package_not_found",
PLUGIN_ID_MISMATCH: "plugin_id_mismatch",
};- NPM Spec 验证:包名/URL 是否有效
- Extensions 声明:package.json 中是否有
openclaw.extensions - Extensions 非空:声明了但列表不能为空
- 包存在性:npm registry 中是否存在
- ID 匹配:清单中的
id是否与预期一致
7.3 安装流程
npm spec 解析
│
▼
下载 npm 包(npm pack 模式)
│
▼
解压到临时目录
│
▼
读取 package.json → 检查 openclaw.extensions
│
▼
运行 npm install --omit=dev(安装运行时依赖)
│
▼
复制到目标目录 ~/.openclaw/plugins/<pluginId>/
│
▼
安全扫描(skill-scanner)
│
▼
记录安装信息到 config(plugins.installs)插件系统/07-infographic-install-pipeline-1775150720832.png)
八、Plugin SDK——开发者工具箱
Plugin SDK 是一个精心策划的 re-export 集合,将核心模块中对插件开发者有用的类型和工具函数重新导出为稳定的公共 API。
8.1 SDK 结构
typescript
// src/plugin-sdk/index.ts
// ── 核心类型 ──
export type { OpenClawPluginApi, OpenClawPluginDefinition, ... } from "../plugins/types.js";
export type { ChannelPlugin, ChannelDock, ... } from "../channels/plugins/types.js";
// ── Webhook 基础设施 ──
export { registerWebhookTarget, resolveWebhookTargetWithAuthOrReject } from "./webhook-targets.js";
export { applyBasicWebhookRequestGuards, readJsonWebhookBodyOrReject } from "./webhook-request-guards.js";
// ── 渠道开发辅助 ──
export { buildBaseAccountStatusSnapshot, buildProbeChannelStatusSummary } from "./status-helpers.js";
export { keepHttpServerTaskAlive, waitUntilAbort } from "./channel-lifecycle.js";
export { createInboundEnvelopeBuilder } from "./inbound-envelope.js";
// ── 安全与认证 ──
export { resolveDirectDmAuthorizationOutcome } from "./command-auth.js";
export { evaluateSenderGroupAccess } from "./group-access.js";
export { isNormalizedSenderAllowed } from "./allow-from.js";
// ── 配置辅助 ──
export { DmConfigSchema, GroupPolicySchema, SecretInputSchema } from "../config/zod-schema.core.js";
export { assertSecretInputResolved, isSecretRef } from "../config/types.secrets.js";
// ── 消息处理 ──
export { chunkTextForOutbound } from "./text-chunking.js";
export { buildAgentMediaPayload } from "./agent-media-payload.js";
export { createNormalizedOutboundDeliverer } from "./reply-payload.js";
// ── 基础设施 ──
export { acquireFileLock, withFileLock } from "./file-lock.js";
export { KeyedAsyncQueue, enqueueKeyedTask } from "./keyed-async-queue.js";
export { createDedupeCache } from "../infra/dedupe.js";
export { createPersistentDedupe } from "./persistent-dedupe.js";8.2 核心工具分类
| 分类 | 工具 | 用途 |
|---|---|---|
| Webhook | registerWebhookTarget() | 注册 Webhook 接收端点 |
applyBasicWebhookRequestGuards() | 请求验证(Content-Type、大小、并发限制) | |
readJsonWebhookBodyOrReject() | 安全读取 JSON 请求体 | |
| 生命周期 | keepHttpServerTaskAlive() | HTTP 服务器保活(优雅关闭) |
waitUntilAbort() | 等待 AbortSignal | |
| 安全 | evaluateSenderGroupAccess() | 群组访问策略评估 |
resolveDirectDmAuthorizationOutcome() | DM 授权判定 | |
isNormalizedSenderAllowed() | AllowFrom 检查 | |
| 状态 | buildBaseAccountStatusSnapshot() | 构建账户状态快照 |
buildProbeChannelStatusSummary() | 构建探测状态摘要 | |
| 文件 | acquireFileLock() | 获取文件锁 |
withFileLock() | RAII 文件锁 | |
| 队列 | KeyedAsyncQueue | 按键异步任务队列 |
createPersistentDedupe() | 持久化去重缓存 |
插件系统/08-infographic-plugin-sdk-toolbox-1775150721573.png)
九、工具注册——两种注册方式
9.1 静态工具
typescript
api.registerTool({
name: "my_tool",
description: "A custom tool",
parameters: { type: "object", properties: { ... } },
execute: async (params) => { return "result"; },
});9.2 工厂工具
typescript
api.registerTool((ctx: OpenClawPluginToolContext) => {
// ctx 包含当前请求的上下文:config、agentId、sessionKey 等
if (!ctx.config?.myPlugin?.enabled) return null; // 条件禁用
return {
name: "my_contextual_tool",
description: "...",
parameters: { ... },
execute: async (params) => { ... },
};
}, { name: "my_contextual_tool" });工厂模式允许工具根据运行时上下文(Agent 配置、会话信息、发送者身份)动态决定是否注册。
插件系统/09-infographic-tool-registration-modes-1775150722450.png)
9.3 工具上下文
typescript
type OpenClawPluginToolContext = {
config: OpenClawConfig;
workspaceDir?: string;
agentDir?: string;
agentId?: string;
sessionKey?: string;
sessionId?: string;
messageChannel?: string;
agentAccountId?: string;
requesterSenderId?: string; // 可信发送者 ID
senderIsOwner?: boolean; // 发送者是否为 owner
sandboxed?: boolean; // 是否在沙盒中
};十、渠道注册——以 MS Teams 为例
10.1 扩展入口
typescript
// extensions/msteams/src/index.ts
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
export default function(api: OpenClawPluginApi) {
const channelPlugin: ChannelPlugin = {
id: "msteams",
meta: {
label: "Microsoft Teams",
docsPath: "/channels/msteams",
// ...
},
capabilities: {
chatTypes: ["direct", "group"],
reactions: true,
edit: true,
reply: true,
media: true,
threads: true,
// ...
},
config: {
// 账户配置读取
listAccounts: (cfg) => { ... },
resolveAccount: (cfg, accountId) => { ... },
resolveAllowFrom: (cfg, accountId) => { ... },
},
gateway: {
// 启停账户
startAccount: async (params) => { ... },
stopAccount: async (params) => { ... },
},
outbound: {
// 出站消息发送
deliveryMode: "direct",
sendText: async (ctx) => { ... },
sendMedia: async (ctx) => { ... },
},
// ... 其余适配器
};
api.registerChannel({ plugin: channelPlugin });
}10.2 Voice Call 扩展——复杂插件示例
Voice Call 是最复杂的扩展插件之一,注册了多种能力:
voice-call 插件
│
├── registerChannel(voiceCallPlugin) → 消息渠道
├── registerHttpRoute(webhookHandler) → Webhook 路由
├── registerService(callManager) → 后台服务(通话管理)
├── registerGatewayMethod("voice-call.*") → Gateway RPC
├── registerCli(voiceCallCli) → CLI 命令
└── registerCommand(ttsCommand) → /tts 自定义命令其配置 Schema 非常丰富(559 行 JSON),包含:
- 电话提供商选择(Twilio/Telnyx/Plivo/Mock)
- TTS 引擎配置(OpenAI/ElevenLabs/Edge)
- Webhook 服务器配置
- 隧道配置(ngrok/Tailscale)
- 流式 STT 配置(OpenAI Realtime)
- 安全选项
十一、Provider 注册——自定义 AI 模型网关
typescript
type ProviderPlugin = {
id: string; // Provider ID(如 "custom-llm")
label: string; // 显示名称
docsPath?: string; // 文档路径
aliases?: string[]; // 别名
envVars?: string[]; // 环境变量(如 CUSTOM_LLM_API_KEY)
models?: ModelProviderConfig; // 模型列表配置
auth: ProviderAuthMethod[]; // 认证方法列表
// 可选
formatApiKey?: (cred) => string; // API Key 格式化
refreshOAuth?: (cred) => Promise<OAuthCred>; // OAuth 刷新
};认证方法支持多种模式:
typescript
type ProviderAuthMethod = {
id: string; // "api_key" | "oauth" | "device_code" | ...
label: string; // "API Key" | "OAuth Login"
hint?: string; // 提示文本
kind: ProviderAuthKind; // "oauth" | "api_key" | "token" | "device_code" | "custom"
run: (ctx: ProviderAuthContext) => Promise<ProviderAuthResult>;
};十二、自定义命令——绕过 Agent
插件可以注册直接命令,不经过 AI Agent 处理:
typescript
type OpenClawPluginCommandDefinition = {
name: string; // 命令名(无前缀斜杠),如 "tts"
description: string; // 描述(显示在 /help 和命令菜单中)
acceptsArgs?: boolean; // 是否接受参数
requireAuth?: boolean; // 是否需要授权(默认 true)
handler: PluginCommandHandler;
};
type PluginCommandHandler = (ctx: PluginCommandContext) => PluginCommandResult | Promise<PluginCommandResult>;处理优先级:插件命令 > 内置命令 > Agent 调用
十三、后台服务
typescript
type OpenClawPluginService = {
id: string;
start: (ctx: OpenClawPluginServiceContext) => void | Promise<void>;
stop?: (ctx: OpenClawPluginServiceContext) => void | Promise<void>;
};
type OpenClawPluginServiceContext = {
config: OpenClawConfig;
workspaceDir?: string;
stateDir: string; // 持久化目录
logger: PluginLogger;
};Gateway 启动时调用 service.start(),关闭时调用 service.stop()。典型用例:
- Voice Call 通话管理器(监听 Webhook、管理通话状态)
- 消息队列消费者
- 定期同步任务
十四、完整数据流
14.1 插件加载流
插件系统/10-infographic-complete-data-flow-1775150723308.png)
Gateway 启动
│
▼
读取配置
├── plugins.enabled → 是否启用插件系统
├── plugins.allow → 允许加载的插件 ID 列表
└── plugins.loadPaths → 额外的加载路径
│
▼
Discovery 阶段
├── 扫描 config 指定路径(优先级 0)
├── 扫描 workspace 插件目录(优先级 1)
├── 扫描 global 插件目录(优先级 2)
└── 收集 bundled 插件(优先级 3)
│
▼
安全校验
├── 路径逃逸检查(realpath)
├── 权限检查(world-writable)
├── 所有权检查(UID 匹配)
└── 硬链接检查(boundary file)
│
▼
Loader 阶段
├── 解析 openclaw.plugin.json → configSchema
├── 验证插件配置(pluginConfig vs configSchema)
├── 动态导入模块(jiti)
├── 解析导出(register/activate/function)
├── 构建 Plugin API
└── 调用 register(api) → 12 种注册能力
│
▼
Registry 构建完成
├── tools: [PluginToolRegistration, ...]
├── channels: [PluginChannelRegistration, ...]
├── providers: [PluginProviderRegistration, ...]
├── hooks: [PluginHookRegistration, ...]
├── gatewayHandlers: { method: handler, ... }
├── httpRoutes: [PluginHttpRouteRegistration, ...]
├── services: [PluginServiceRegistration, ...]
├── commands: [PluginCommandRegistration, ...]
└── diagnostics: [PluginDiagnostic, ...]
│
▼
激活注册表
└── setActivePluginRegistry(registry, cacheKey)
│
▼
Provenance 追踪警告
└── 对未追踪的非 bundled 插件发出安全警告14.2 插件运行时交互流
用户发送消息
│
├── 插件命令检查(PluginCommandRegistration)
│ ├── 匹配 → 直接执行 handler → 返回结果
│ └── 不匹配 → 继续
│
├── 内置命令检查
│ ├── 匹配 → 执行
│ └── 不匹配 → 继续
│
▼
Agent 处理
│
├── before_prompt_build 钩子
│ └── 插件修改系统提示词
│
├── before_agent_start 钩子
│ └── 插件修改消息列表
│
├── 工具调用
│ ├── 核心工具(web_search 等)
│ └── 插件工具(PluginToolRegistration → factory(ctx) → tool)
│
├── 流式输出 → 渠道发送
│ ├── 内置渠道
│ └── 插件渠道(PluginChannelRegistration → outbound.sendText)
│
└── 完成十五、设计模式总结
15.1 插件模式(Plugin Pattern)
核心设计。通过统一的 register(api) 接口,插件可以扩展系统的任意方面。
15.2 注册表模式(Registry Pattern)
PluginRegistry 统一管理所有插件注册的能力,支持按类型查询、版本化缓存。
15.3 工厂模式(Factory Pattern)
OpenClawPluginToolFactory→ 工具按运行时上下文动态生成createPluginRegistry()→ 注册表工厂
15.4 依赖注入(Dependency Injection)
OpenClawPluginApi 注入 config、logger、runtime 等依赖,插件无需自行查找。
15.5 Facade 模式
Plugin SDK 是一个 Facade,将分散在 20+ 个模块中的工具函数统一导出为一个干净的公共接口。
15.6 安全设计模式(Defense in Depth)
四层安全检查(逃逸/权限/所有权/硬链接)+ Allowlist + Provenance 追踪 = 纵深防御。
插件系统/11-infographic-design-patterns-1775150724245.png)
十六、调试建议
16.1 关键断点位置
| 文件 | 位置 | 断点目的 |
|---|---|---|
src/plugins/loader.ts | register(api) 调用处 | 观察插件注册过程 |
src/plugins/loader.ts | recordPluginError() | 插件加载失败原因 |
src/plugins/registry.ts | registerTool() | 工具注册 |
src/plugins/registry.ts | registerChannel() | 渠道注册 |
src/plugins/registry.ts | registerGatewayMethod() | Gateway 方法冲突 |
src/plugins/discovery.ts | isUnsafePluginCandidate() | 安全检查阻断 |
src/plugins/discovery.ts | addCandidate() | 候选添加 |
src/plugins/manifest.ts | loadPluginManifest() | 清单解析 |
src/plugins/install.ts | ensureOpenClawExtensions() | 安装验证 |
16.2 测试命令
bash
# 插件系统核心测试
pnpm test -- --reporter verbose src/plugins/
# 插件 SDK 测试
pnpm test -- --reporter verbose src/plugin-sdk/
# 特定扩展测试
pnpm test -- --reporter verbose extensions/voice-call/
pnpm test -- --reporter verbose extensions/msteams/
pnpm test -- --reporter verbose extensions/matrix/
# 清单解析测试
pnpm test -- --reporter verbose src/plugins/manifest.test.ts
# 发现测试
pnpm test -- --reporter verbose src/plugins/discovery.test.ts
# 安装测试
pnpm test -- --reporter verbose src/plugins/install.test.ts16.3 调试插件加载
bash
# 启用详细插件日志
OPENCLAW_LOG_VERBOSE=1 pnpm openclaw gateway run
# 查看已加载插件列表
pnpm openclaw plugins list
# 查看插件诊断信息
pnpm openclaw doctor十七、文件依赖图
src/plugins/types.ts ← 核心类型(零外部依赖)
↑
src/plugins/registry.ts ← 注册表工厂(依赖 types)
↑
src/plugins/manifest.ts ← 清单解析(零外部依赖)
↑
src/plugins/discovery.ts ← 文件系统发现 + 安全校验
↑
src/plugins/loader.ts ← 加载器(依赖 registry + manifest + discovery)
↑
src/plugins/install.ts ← 安装流程(依赖 manifest + infra)
src/plugin-sdk/index.ts ← SDK Facade(re-export 20+ 模块)
├── src/plugin-sdk/webhook-targets.ts
├── src/plugin-sdk/webhook-request-guards.ts
├── src/plugin-sdk/file-lock.ts
├── src/plugin-sdk/channel-lifecycle.ts
├── src/plugin-sdk/status-helpers.ts
├── src/plugin-sdk/inbound-envelope.ts
├── src/plugin-sdk/command-auth.ts
├── src/plugin-sdk/group-access.ts
└── src/plugin-sdk/allow-from.ts
extensions/msteams/src/index.ts ← 扩展实现(依赖 plugin-sdk)
extensions/voice-call/index.ts ← 扩展实现(依赖 plugin-sdk)
extensions/matrix/src/index.ts ← 扩展实现(依赖 plugin-sdk)插件系统/12-infographic-module-deps-insights-1775150725220.png)
十八、设计洞察
18.1 为什么是 12 种注册能力
这 12 种能力覆盖了 OpenClaw 的所有扩展点:
入站路径:Channel → Agent → Tool
出站路径:Agent → Channel
管理路径:CLI → Config → Provider
基础设施:HTTP Route → Gateway Method → Service → Hook每个扩展点对应一个 register* 方法。没有"万能注册"——这是刻意的设计约束,确保插件的能力边界清晰可审计。
18.2 安全与便利的平衡
发现阶段的四层安全检查看似严苛,但有合理的层次:
- bundled 插件:跳过所有权检查(信任安装包)
- config 指定的插件:仍检查路径逃逸和权限(配置可能被篡改)
- workspace/global 插件:全部检查(任何人可以放文件)
plugins.allow 白名单是最后一道防线。当白名单为空时,系统会发出警告而非静默加载所有发现的插件。
18.3 Plugin SDK 的"稳定表面积"设计
Plugin SDK 只 re-export 被认为是稳定公共 API 的部分。内部模块可以自由重构,只要 SDK 的导出签名不变,所有扩展插件都不受影响。
这也是为什么 openclaw/plugin-sdk 在 peerDependencies 中声明而非 dependencies——运行时通过 jiti 别名解析,开发时通过 TypeScript 路径映射提供类型。
18.4 工厂工具 vs 静态工具
静态工具在插件加载时一次性注册,适合无条件可用的工具。
工厂工具在每次 Agent 运行时动态生成,可以:
- 根据 Agent 配置决定是否启用
- 根据发送者身份决定参数范围
- 根据沙盒状态限制能力
这是一种"延迟绑定"设计,让工具的可见性成为运行时决策而非加载时决策。
18.5 为什么清单文件是 JSON 而非 TypeScript
openclaw.plugin.json 使用纯 JSON 而非 TypeScript/JavaScript,因为:
- 无需执行:清单读取不应触发任何代码执行
- 可静态分析:npm 安装前就能检查配置 Schema
- 安全:避免清单文件中的恶意代码
- 工具友好:IDE、CI、Web UI 都能直接解析