OpenClaw Plugins 开发:Extensions、RPC 注册与自定义工具
当标准工具集无法满足业务需求时,插件(Plugin)机制是扩展AI Agent能力的最后防线。OpenClaw采用进程内微内核架构,通过RPC方法注册与标准化命名规范,实现了"零重启热插拔"的扩展能力。本文深度拆解其运行时模型、命名契约与分发机制,并以Voice Call插件为例展示完整开发闭环。
一、Plugin架构:进程内微内核设计
OpenClaw的插件系统采用进程内(In-Process)微内核架构,与外部进程(Out-of-Process)方案相比,在延迟与复杂度之间取得了精妙平衡。
1.1 架构模式对比
| 维度 | OpenClaw(进程内) | 传统Agent(外部进程) |
|---|---|---|
| 通信机制 | 函数调用(纳秒级) | IPC/HTTP(毫秒级) |
| 故障隔离 | 共享进程空间(需沙箱) | 进程级隔离 |
| 资源开销 | 低(共享Event Loop) | 高(独立运行时) |
| 开发复杂度 | 中(需遵循命名规范) | 高(需定义协议) |
| 热更新 | 支持(模块重载) | 需重启 |
| 适用场景 | 高频工具、实时交互 | 重型计算、危险操作 |
1.2 运行时架构图
md
+---------------------+ +---------------------+ +---------------------+
| Gateway Core | | Plugin Manager | | Plugin Instances |
| (Node.js Runtime) |<------>| (Registry & Loader)|<------>| (In-Process) |
+---------------------+ +---------------------+ +---------------------+
| | |
v v v
+---------+----------+ +---------+---------+ +---------+---------+
| 消息路由层 | | 插件生命周期管理 | | Voice Call Plugin |
| (Agent Loop) | | - 加载/卸载 | | - twilio_call |
| | | - 依赖注入 | | - twilio_hangup |
| | | - 版本兼容 | | - twilio_status |
+---------------------+ +-------------------+ +-------------------+
|
v
+------+------+
| Skill Pack |
| (内嵌技能) |
+-------------+
核心特征:
- 单Runtime:所有插件与Gateway共享同一个Node.js事件循环
- 沙箱隔离:通过VM2或Node.js的vm模块限制插件访问系统资源
- 热重载:支持openclaw plugins reload无重启更新(开发模式)
1.3 RPC方法注册机制
插件通过RPC(Remote Procedure Call)风格的方法注册暴露能力,采用pluginId.action的命名规范:
js
// 插件入口:src/index.ts
import { OpenClawPlugin, ToolContext } from '@openclaw/plugin-sdk';
export default class VoiceCallPlugin implements OpenClawPlugin {
pluginId = 'voice-call'; // 全局唯一标识
async register(gateway: GatewayAPI): Promise<void> {
// 注册工具方法(Tools)
gateway.registerTool('voice-call.twilio_dial', this.dial.bind(this));
gateway.registerTool('voice-call.twilio_hangup', this.hangup.bind(this));
// 注册网关方法(Gateway Methods)
gateway.registerMethod('voiceCall.getActiveCalls', this.getActiveCalls.bind(this));
// 注册CLI命令
gateway.registerCommand('voice-call:list', this.listCalls.bind(this));
}
async dial(args: { to: string; message?: string }, ctx: ToolContext): Promise<string> {
// 实现逻辑
}
}
命名空间隔离:
- Tools:pluginId.tool_name(snake_case),供Agent调用
- Gateway Methods:pluginId.methodName(camelCase),供外部系统调用
- CLI:plugin-id:command(kebab-case),供管理员使用
二、命名规范体系:三层契约设计
OpenClaw强制要求三层命名规范,分别对应不同的调用上下文与受众。
2.1 命名规范对照表
| 层级 | 受众 | 规范 | 示例 | 说明 |
|---|---|---|---|---|
| Gateway方法 | 外部系统/TypeScript代码 | camelCase | voiceCall.initiateCall |
类方法风格 |
| Tools | AI Agent/LLM | snake_case | voice_call.twilio_dial |
Unix命令风格 |
| CLI | 系统管理员/Shell | kebab-case | voice-call:make-call |
命令行友好 |
2.2 命名空间与防冲突
插件ID全局唯一性:
md
命名空间规则:
- 官方插件:@openclaw/*
例:@openclaw/voice-call → 工具名:voice_call.*
- 社区插件:@scope/*
例:@acme/scheduler → 工具名:acme_scheduler.*
- 本地插件:无scope
例:custom-logger → 工具名:custom_logger.*
冲突解决策略:
- 完全限定名:pluginId.tool_name确保全局唯一
- 版本锁定:openclaw.plugin.json中声明peerDependencies防止API不兼容
- 沙箱隔离:即使ID冲突,运行时通过命名空间隔离(但强烈建议避免)
2.3 命名规范实战
以Voice Call插件为例展示三层命名:
js
// 1. Gateway Method(camelCase)- 供外部HTTP/WebSocket调用
class VoiceCallPlugin {
// 获取活跃通话列表(内部API)
async getActiveCalls(filter?: { status: 'ringing' | 'connected' }): Promise<Call[]> {
return this.calls.filter(c => !filter || c.status === filter.status);
}
// 初始化通话(初始化配置)
async configureTwilio(credentials: TwilioCredentials): Promise<void> {
this.twilioClient = new Twilio(credentials.sid, credentials.token);
}
}
// 2. Tool(snake_case)- 供Agent通过LLM调用
const tools = {
// 拨打电话(Tool暴露给Agent)
'voice_call.twilio_dial': async (args: { to: string; body?: string }) => {
return plugin.dial(args.to, args.body);
},
// 挂断电话
'voice_call.twilio_hangup': async (args: { callSid: string }) => {
return plugin.hangup(args.callSid);
},
// 获取通话状态
'voice_call.get_status': async (args: { callSid: string }) => {
return plugin.getCallStatus(args.callSid);
}
};
// 3. CLI(kebab-case)- 供管理员命令行操作
const commands = {
// 查看通话列表
'voice-call:list': {
description: 'List all active voice calls',
args: [{ name: 'status', optional: true }],
handler: async (args) => {
const calls = await plugin.getActiveCalls(args.status && { status: args.status });
console.table(calls);
}
},
// 发起测试通话
'voice-call:test-dial': {
description: 'Make a test call to specified number',
args: [{ name: 'to', required: true }, { name: 'message', optional: true }],
handler: async (args) => {
const result = await tools['voice_call.twilio_dial']({ to: args.to, body: args.message });
console.log('Call initiated:', result.sid);
}
}
};
三、技能打包:插件内嵌Skill机制
OpenClaw插件不仅是代码包,更是技能的载体。通过内嵌Skill,插件可以扩展Agent的认知能力。
3.1 Skill打包结构
插件目录结构遵循约定优于配置(CoC)原则:
md
voice-call-plugin/
├── src/
│ ├── index.ts # 插件入口(RPC注册)
│ ├── twilio-client.ts # 业务逻辑实现
│ └── tools/
│ ├── dial.ts
│ ├── hangup.ts
│ └── status.ts
├── skills/ # 内嵌Skill(核心!)
│ ├── voice-communication.md # 语音通信最佳实践
│ ├── twilio-error-handling.md # 错误处理指南
│ └── call-etiquette.md # 通话礼仪规范
├── openclaw.plugin.json # 插件声明文件(必需)
├── package.json
└── tsconfig.json
3.2 openclaw.plugin.json声明文件
json
{
"id": "voice-call",
"version": "1.2.0",
"name": "Voice Call Integration",
"description": "Twilio-powered voice calling capabilities",
"author": "OpenClaw Team",
"license": "MIT",
"entry": "./dist/index.js",
"types": "./dist/index.d.ts",
"runtime": {
"node": ">=18.0.0",
"sandbox": {
"enabled": true,
"permissions": ["network", "filesystem-read"]
}
},
"rpc": {
"methods": [
{ "name": "getActiveCalls", "access": "gateway", "params": ["filter"] },
{ "name": "configureTwilio", "access": "admin" }
],
"tools": [
{ "name": "twilio_dial", "dangerous": false, "confirm": true },
{ "name": "twilio_hangup", "dangerous": true, "confirm": true },
{ "name": "get_status", "dangerous": false }
],
"commands": [
{ "name": "list", "description": "List active calls" },
{ "name": "test-dial", "description": "Make test call" }
]
},
"skills": {
"packs": [
{ "path": "./skills/voice-communication.md", "autoLoad": true },
{ "path": "./skills/twilio-error-handling.md", "autoLoad": true }
],
"variables": {
"TWILIO_PHONE_NUMBER": { "required": true, "description": "Outgoing caller ID" }
}
},
"peerDependencies": {
"@openclaw/plugin-sdk": "^0.4.0",
"openclaw": "^0.4.0"
}
}
关键字段解析:
- runtime.sandbox:声明所需权限,Gateway据此决定是否允许加载(权限不足时拒绝)
- rpc.tools[*].dangerous:标记危险操作(如挂断电话),触发二次确认
- rpc.tools[*].confirm:要求用户/Agent显式确认后执行
- skills.packs:自动注入到Agent的System Prompt中
3.3 Skill注入机制
当插件加载时,Skill自动合并到Agent的认知层:
md
+-------------------------------------------------------------+
| Agent System Prompt |
+-------------------------------------------------------------+
| 基础身份(SOUL.md/AGENTS.md) |
| ... |
| |
| +---------------------+ +---------------------+ |
| | voice-communication | | twilio-error- | |
| | .md | | handling.md | <-- 插件Skill |
| | - 语音通话最佳实践 | | - 错误码映射 | |
| | - 号码格式化规则 | | - 重试策略 | |
| | - 通话状态机说明 | | - 降级处理 | |
| +---------------------+ +---------------------+ |
| |
| 用户当前对话上下文 |
+-------------------------------------------------------------+
运行时Skill访问:
js
// 在Tool实现中访问Skill内容
async twilioDial(args, context) {
// context.skills 包含所有已加载的Skill文本
const bestPractice = context.skills['voice-communication'];
// 根据Skill指导进行号码格式化
const formattedNumber = this.formatNumber(args.to, bestPractice.regionRules);
return this.client.calls.create({
to: formattedNumber,
from: context.config.TWILIO_PHONE_NUMBER
});
}
四、分发与安装:npm生态与本地开发
OpenClaw插件支持两种分发模式:npm生态分发(生产)与本地路径加载(开发)。
4.1 npm包分发(官方推荐)
命名空间规范:
官方插件:@openclaw/*(需官方签名)
社区插件:@scope/openclaw-plugin-或openclaw-plugin-
发布流程:
bash
# 1. 构建与测试
npm run build
npm test
# 2. 版本管理
npm version minor # 1.1.0 -> 1.2.0
# 3. 发布(官方插件需2FA)
npm publish --access public
# 4. 元数据更新(可选)
openclaw plugins register @openclaw/voice-call --category "communication"
安装与激活:
bash
# 从npm安装
openclaw plugins install @openclaw/voice-call
# 安装特定版本
openclaw plugins install @openclaw/voice-call@1.2.0
# 查看已安装
openclaw plugins list
# Plugin ID | Version | Status | Author
# -------------+---------+----------+-------------
# voice-call | 1.2.0 | active | OpenClaw Team
# scheduler | 0.5.1 | inactive | Community
# 激活/停用
openclaw plugins enable voice-call
openclaw plugins disable voice-call
# 卸载
openclaw plugins uninstall voice-call
4.2 本地路径加载(开发模式)
开发阶段使用本地路径快速迭代:
bash
# 本地路径安装(软链接模式)
openclaw plugins install ./voice-call-plugin --link
# 开发模式热重载(监听文件变更)
openclaw plugins install ./voice-call-plugin --link --watch
# 查看日志
openclaw logs --follow --filter "plugin:voice-call"
开发配置(~/.openclaw/config.json):
json
{
plugins: {
development: {
hotReload: true, // 保存即重载
sourceMap: true, // 启用TS调试
verboseLogging: true // 详细RPC日志
},
registry: {
npm: "https://registry.npmjs.org",
allowUnofficial: true, // 允许非官方插件(企业内网)
trustOnFirstUse: false // 首次使用需显式信任
}
}
}
安全警告:生产环境禁止--link模式,防止路径遍历攻击。
五、实战案例:Voice Call插件(Twilio集成)
以完整的Voice Call插件为例,展示从配置到工具暴露的全流程。
5.1 场景与需求
业务场景:客服Agent需要主动外呼用户,并获取通话结果。
功能需求:
- 拨打电话(TTS播报预设消息)
- 实时获取通话状态(ringing/completed/failed)
- 支持通话录音与转写
- 异常处理(无人接听、线路忙)
5.2 配置结构设计
js
// src/config.ts
export interface VoiceCallConfig {
// Twilio凭证(从环境变量或加密存储读取)
twilio: {
accountSid: string; // ACxxxx...
authToken: string; // 加密存储
fromNumber: string; // +1xxxx...
};
// 通话行为配置
behavior: {
maxDuration: number; // 最大通话时长(秒)
recordCalls: boolean; // 是否录音
retryAttempts: number; // 失败重试次数
retryDelay: number; // 重试间隔(秒)
};
// TTS配置
tts: {
voice: 'man' | 'woman' | 'alice';
language: string; // en-US, zh-CN
speed: number; // 0.5 - 2.0
};
}
// 配置验证
export const validateConfig = (config: unknown): VoiceCallConfig => {
const schema = z.object({
twilio: z.object({
accountSid: z.string().startsWith('AC'),
authToken: z.string().min(32),
fromNumber: z.string().regex(/^\+[1-9]\d{1,14}$/)
}),
behavior: z.object({
maxDuration: z.number().max(3600).default(300),
recordCalls: z.boolean().default(true)
}).default({}),
tts: z.object({
voice: z.enum(['man', 'woman', 'alice']).default('alice'),
language: z.string().default('zh-CN')
}).default({})
});
return schema.parse(config);
};
5.3 工具暴露与实现
js
// src/tools/dial.ts
import { Twilio } from 'twilio';
import { ToolContext, ToolResult } from '@openclaw/plugin-sdk';
export interface DialArgs {
to: string; // E.164格式号码
message: string; // TTS播报内容
context?: Record<string, any>; // 透传上下文
}
export async function twilioDial(
args: DialArgs,
ctx: ToolContext
): Promise<ToolResult> {
const config = ctx.config as VoiceCallConfig;
const client = new Twilio(config.twilio.accountSid, config.twilio.authToken);
// 号码标准化(根据内嵌Skill规则)
const normalizedTo = normalizePhoneNumber(args.to, ctx.skills['voice-communication']);
try {
const call = await client.calls.create({
to: normalizedTo,
from: config.twilio.fromNumber,
twiml: `<Response><Say voice="${config.tts.voice}" language="${config.tts.language}">${escapeXml(args.message)}</Say></Response>`,
record: config.behavior.recordCalls,
timeLimit: config.behavior.maxDuration,
statusCallback: `${ctx.gateway.webhookUrl}/voice-call/status`,
statusCallbackEvent: ['initiated', 'ringing', 'answered', 'completed']
});
// 注册到活跃通话管理器
ctx.pluginState.activeCalls.set(call.sid, {
to: normalizedTo,
message: args.message,
startedAt: new Date(),
status: 'initiated'
});
return {
success: true,
data: {
callSid: call.sid,
status: call.status,
uri: call.uri
},
message: `Call initiated to ${normalizedTo}, SID: ${call.sid}`
};
} catch (error) {
// 根据Skill指导进行错误分类
const errorType = classifyError(error, ctx.skills['twilio-error-handling']);
return {
success: false,
error: {
code: errorType.code,
message: error.message,
retryable: errorType.retryable
},
message: `Failed to initiate call: ${errorType.userMessage}`
};
}
}
// 工具元数据(用于LLM理解)
export const dialMetadata = {
name: 'voice_call.twilio_dial',
description: 'Initiate an outbound voice call to a phone number using Twilio. The call will play a TTS message.',
parameters: {
type: 'object',
properties: {
to: {
type: 'string',
description: 'Phone number in E.164 format (e.g., +86138xxxxxxxx)',
pattern: '^\\+[1-9]\\d{1,14}$'
},
message: {
type: 'string',
description: 'The message to be spoken via TTS. Should be concise and professional.',
maxLength: 500
}
},
required: ['to', 'message']
},
returns: {
type: 'object',
properties: {
callSid: { type: 'string', description: 'Unique call identifier' },
status: { type: 'string', enum: ['queued', 'ringing', 'in-progress', 'completed', 'failed'] }
}
},
dangerous: false,
confirm: true, // 要求用户确认
examples: [
{ to: '+86138xxxxxxxx', message: '您好,这是OpenClaw智能助手,您的验证码是123456。' }
]
};
5.4 事件处理与状态同步
js
// src/webhook-handler.ts
export async function handleStatusCallback(
body: TwilioStatusCallback,
ctx: ToolContext
): Promise<void> {
const { CallSid, CallStatus, RecordingUrl, CallDuration } = body;
// 更新内部状态
const call = ctx.pluginState.activeCalls.get(CallSid);
if (!call) return;
call.status = CallStatus;
call.duration = parseInt(CallDuration) || 0;
// 通话完成处理
if (CallStatus === 'completed') {
if (RecordingUrl) {
call.recordingUrl = RecordingUrl;
// 触发转写(异步)
ctx.queueTask('transcribe', { recordingUrl: RecordingUrl, callSid: CallSid });
}
// 通知Agent(通过Memory注入)
await ctx.memory.add({
type: 'call_completed',
content: `通话已完成。号码:${call.to},时长:${call.duration}秒,录音:${RecordingUrl || '无'}`,
metadata: { callSid: CallSid, status: CallStatus }
});
// 清理活跃列表
ctx.pluginState.activeCalls.delete(CallSid);
}
// 失败处理(触发重试逻辑)
if (['failed', 'busy', 'no-answer'].includes(CallStatus)) {
await handleFailedCall(CallSid, CallStatus, ctx);
}
}
5.5 安装与配置
用户安装后的配置流程:
bash
# 1. 安装插件
openclaw plugins install @openclaw/voice-call
# 2. 配置凭证(加密存储)
openclaw plugins configure voice-call
# 交互式输入:
# - Twilio Account SID: ACxxxx...
# - Twilio Auth Token: [hidden]
# - 主叫号码: +8610xxxxxxxx
# 3. 验证配置
openclaw plugins test voice-call --tool twilio_dial --args '{"to":"+86138xxxxxxxx","message":"测试通话"}'
# 4. 授权Agent使用
openclaw agents allow-tools main --tools 'voice_call.*'
Agent使用示例:
md
用户:请致电客户138xxxx8888,告知订单已发货。
Agent思考:
1. 需要调用voice_call.twilio_dial
2. 参数:to="+86138xxxx8888", message="您好,您的订单已发货,预计3天后到达。"
3. 该工具标记为confirm=true,需等待用户确认
Agent回复:
即将致电客户138xxxx8888播报发货通知。确认执行?[确认/取消]
(用户确认后)
Agent调用工具 → 返回Call SID → 告知用户已拨号并播报内容
六、生产环境部署建议
6.1 插件安全审查清单
bash
# 安装前审查
openclaw plugins audit @openclaw/voice-call
# 检查项:
# ✓ 代码签名验证
# ✓ 依赖漏洞扫描(npm audit)
# ✓ 权限声明审查(sandbox.permissions)
# ✓ 网络访问范围(egress rules)
# 运行时监控
openclaw plugins monitor voice-call --metrics
# 监控项:
# - RPC调用频率
# - 错误率
# - 内存占用
# - 网络流量
6.2 版本管理与回滚
bash
# 蓝绿部署(同时加载新旧版本)
openclaw plugins install @openclaw/voice-call@2.0.0 --alias voice-call-v2
openclaw plugins enable voice-call-v2 --gradual 10% # 10%流量灰度
# 观察监控,无异常后全量切换
openclaw plugins disable voice-call
openclaw plugins enable voice-call-v2 --gradual 100%
# 紧急回滚
openclaw plugins rollback voice-call # 回退到上一版本
6.3 性能优化
冷启动优化:
- 使用esbuild预编译,减少 require 时间
- 延迟加载重型依赖(如Twilio SDK仅在调用时加载)
- 启用v8-compile-cache缓存字节码
运行时优化:
js
// 使用连接池(Twilio客户端复用)
export class VoiceCallPlugin {
private twilioClient: Twilio | null = null;
async getClient(): Promise<Twilio> {
if (!this.twilioClient) {
this.twilioClient = new Twilio(this.config.accountSid, this.config.authToken);
}
return this.twilioClient;
}
// 定期清理空闲连接
@Cron('0 */5 * * * *') // 每5分钟
async healthCheck(): Promise<void> {
// 检查活跃通话,清理僵尸连接
}
}
七、总结
OpenClaw的插件架构通过进程内微内核设计,在保持低延迟的同时实现了安全的扩展能力。其三层命名规范(camelCase/snake_case/kebab-case)确保了代码、Agent与管理员三类受众的清晰边界,而Skill内嵌机制则让插件不仅是工具集,更是领域知识的载体。
架构设计要点:
- 进程内高效性:纳秒级函数调用延迟,适合高频实时工具
- 命名空间隔离:pluginId.action确保全局唯一,防止冲突
- Skill即代码:Markdown格式的Skill内嵌,实现领域知识版本化管理
- 分层权限:Gateway/Tool/CLI三层访问控制,最小权限原则
- 生态兼容:npm分发与本地开发双轨支持,降低参与门槛
对于企业级开发,建议优先选择@openclaw/*官方命名空间,实施严格的代码签名与沙箱权限审查,并建立插件版本的灰度发布机制。
本文章基于OpenClaw官方文档学习撰写。仅供学习参考,请勿用于商业用途。