一、OpenClaw 为什么需要插件
OpenClaw 是一个 AI Agent 平台,它的核心是对话引擎和工具调度。但一个平台不可能内置所有能力------今天要接飞书,明天要接钉钉;今天要查日历,明天要操作数据库。如果每接一个新平台、新能力都要改 OpenClaw 的核心代码,这个架构很快就会崩掉。
所以 OpenClaw 选择了一条经典的路:「平台做薄,能力外挂。」
核心引擎只负责对话管理、模型调用和工具调度,所有跟外部系统打交道的能力------接入哪个消息平台、能操作什么 API------全部通过**「插件」**来扩展。每装一个插件,OpenClaw 就多一项能力;卸掉插件,能力随之消失,核心不受影响。
本文就来拆解这套插件机制是怎么设计的。我们以官方飞书插件(@larksuite/openclaw-lark)为例,从安装到运行,把整个链路讲透。不贴大段源码,只讲你需要理解的核心逻辑。
二、插件到底是什么
先说它"不是"什么
很多人一听"插件",脑子里会蹦出各种架构:
-
「是不是把插件代码编译进 OpenClaw 主程序?」 不是。
-
「是不是像微服务一样,插件单独起一个进程?」 不是。
-
「是不是通过 HTTP/gRPC 远程调用?」 也不是。
它是什么
「插件就是一个普通的 npm 包,被 OpenClaw 主进程动态 import() 加载,运行在同一个 Node.js 进程里。」
你可以类比这些你熟悉的东西:
| 宿主 | 扩展形式 | 加载方式 |
|---|---|---|
| VS Code | 扩展 (.vsix) | 同进程加载 |
| Chrome | 扩展 (.crx) | 独立沙箱进程 |
| Webpack | Plugin (npm 包) | 同进程加载 |
| 「OpenClaw」 | 「插件 (npm 包)」 | 「同进程加载」 |
OpenClaw 的插件跟 VS Code 扩展最像:装进去、加载到宿主进程、共享运行时上下文、宿主控制生命周期。
插件怎么开发:依赖 Plugin SDK
编写插件需要用到 OpenClaw 提供的 「Plugin SDK」 (openclaw/plugin-sdk)。SDK 定义了插件和平台之间的所有接口------注册频道用什么类型、工具返回什么格式、配置怎么读取。插件在 package.json 里把 openclaw 声明为 peerDependency,开发时依赖它获取类型定义,但不会打包进插件产物里------运行时由 OpenClaw 主程序提供。就像写 React 组件库要依赖 react,但不会把 react 打包进去一样。
插件能做三件事
一个插件可以向 OpenClaw 平台注册三种能力,按"重量"从轻到重:
「1. 注册工具(Tool)------ 最轻量」
给 AI Agent 增加一个可调用的函数。比如飞书插件注册了 feishu_calendar_event 工具,Agent 就能调用它来创建日程。
这就像给 Agent 配了一把螺丝刀------它知道什么时候该用,用的时候调一下插件提供的函数就行。
「2. 注册命令(Command)------ 中等」
给用户提供斜杠命令,比如 /feishu doctor 可以诊断飞书连接状态。这不经过 AI,是用户直接触发的快捷操作。
「3. 注册频道(Channel)------ 最重」
接入一整个消息平台。注册频道意味着插件要负责:消息的收(入站网关)、消息的发(出站适配器)、用户鉴权、消息格式转换......这是全套工程。
飞书插件就是一个注册了完整频道的重量级插件------它不仅注册了 30 多个工具,还接管了飞书消息的整个收发链路。
三、插件的声明与发现:OpenClaw 怎么知道有这个插件
安装
假设你的服务器上已经跑着 OpenClaw,安装飞书插件就一条命令:
go
openclaw install @larksuite/openclaw-lark
这条命令背后,OpenClaw 就是用 npm 把这个包下载到自己的插件目录下(类似 ~/.openclaw/extensions/)。跟你在项目里 npm install 一个依赖没有本质区别。
两个声明文件
安装完成后,OpenClaw 怎么知道这个 npm 包是一个合法的插件、提供了什么能力?靠两个声明文件。
「第一个:package.json 里的 openclaw 字段」
go
{
"name": "@larksuite/openclaw-lark",
"openclaw": {
"extensions": ["./dist/index.mjs"],
"channel": {
"id": "openclaw-lark",
"label": "Feishu",
"aliases": ["lark"]
},
"install": {
"npmSpec": "@larksuite/openclaw-lark"
}
}
}
这个字段告诉平台三件事:
-
「入口文件在哪」 :
./dist/index.mjs------平台要 import 这个文件 -
「我是什么频道」:id 叫 openclaw-lark,显示名叫 Feishu,别名 lark
-
「怎么安装我」 :npm 包名是
@larksuite/openclaw-lark
「第二个:openclaw.plugin.json」
go
{
"id": "openclaw-lark",
"channels": ["feishu"],
"skills": ["./skills"],
"configSchema": { ... },
"channelConfigs": {
"feishu": { "schema": { "type": "object" } }
}
}
这个文件告诉平台:
-
「插件 ID」:唯一标识
-
「提供了哪些频道」:feishu
-
「有哪些 AI 技能」 :指向
./skills目录,里面有 9 个技能定义(多维表格操作指南、日历管理指南等),这些技能文件(SKILL.md)是给 AI 看的,帮助它理解什么时候该用什么工具、参数怎么填 -
「配置 Schema」:告诉平台这个频道需要哪些配置项(appId、appSecret、各种策略等)
加载过程
OpenClaw 启动(或执行热重载)时,按这个顺序完成加载:
go
① 扫描插件目录
└── 找到 @larksuite/openclaw-lark
② 读取 package.json 的 openclaw 字段
└── 拿到入口文件路径:./dist/index.mjs
③ 读取 openclaw.plugin.json
└── 知道它提供 feishu 频道 + 9 个技能
④ 动态 import('./dist/index.mjs')
└── 拿到默认导出的 plugin 对象
⑤ 调用 plugin.register(api)
└── 插件在这个函数里完成所有注册
注意第 ⑤ 步------这是整个插件机制的核心。平台给插件传了一个 api 对象,插件通过这个对象把自己的能力"挂"到平台上。
四、注册机制:插件如何把能力"挂"到平台上
register(api):一切的起点
每个 OpenClaw 插件都必须导出一个对象,其中包含一个 register 方法:
go
const plugin = {
id: 'openclaw-lark',
name: 'Feishu',
register(api) {
// 在这里注册所有能力
}
};
export default plugin;
api 是平台传进来的"注册中心",它提供了这些关键方法:
| 方法 | 作用 |
|---|---|
api.registerChannel() |
注册一个消息频道 |
api.registerTool() |
注册一个 AI 可调用的工具 |
api.registerCommand() |
注册一个斜杠命令 |
api.registerCli() |
注册一个 CLI 命令 |
api.on() |
监听平台事件 |
api.config |
读取当前配置 |
api.runtime |
访问运行时环境 |
飞书插件的 register() 做了这些事情(按顺序):
第一步:注入运行时
go
LarkClient.setRuntime(api.runtime);
把平台的运行时环境存下来,后续发消息、读配置都要用到。
第二步:注册频道
go
api.registerChannel({ plugin: feishuPlugin });
这一行把一个实现了 ChannelPlugin 接口的对象注册进平台。这个接口是平台和频道插件之间的"契约",定义了频道必须提供的所有能力:
-
「入站网关(Gateway)」:怎么连接飞书、怎么收消息
-
-
飞书插件的实现:建立 WebSocket 长连接到飞书服务器
-
平台在启动时调用
gateway.startAccount(),插件建连 -
平台在关闭时调用
gateway.stopAccount(),插件断连
-
-
「出站适配器(Outbound)」:AI 生成了回复,怎么发到飞书
-
-
平台调用
outbound.sendText()→ 插件调飞书 API 发消息 -
平台调用
outbound.sendMedia()→ 插件上传文件再发送
-
-
「能力声明(Capabilities)」:告诉平台这个频道支持什么
gocapabilities: { chatTypes: ['direct', 'group'], // 支持私聊和群聊 media: true, // 支持发送图片/文件 reactions: true, // 支持表情回应 threads: true, // 支持消息线程 blockStreaming: true, // 支持流式卡片 } -
「配置管理」:多账号支持、白名单、权限策略
-
「消息目标解析」 :把
user:ou_xxx或chat:oc_xxx解析成飞书能识别的 ID -
「状态探针」:定期检查连接是否正常
第三步:注册工具
go
registerOapiTools(api); // 日历、任务、多维表格等 30+ 工具
registerFeishuMcpDocTools(api); // 文档读写工具
registerFeishuOAuthTool(api); // OAuth 授权工具
registerAskUserQuestionTool(api);// 交互式提问工具
每个工具注册时需要提供:
-
「名称和描述」:AI 靠这个判断什么时候该调用这个工具
-
「参数 Schema」:工具接受哪些参数(JSON Schema 格式),AI 会据此生成正确的参数
-
「execute 函数」:实际执行逻辑
以日历搜索工具为例,注册时大致是这样的:
go
api.registerTool({
name: 'feishu_search_doc_wiki',
description: '飞书文档与 Wiki 统一搜索工具...',
parameters: { /* JSON Schema:query, filter, page_size 等 */ },
async execute(toolCallId, params) {
// 1. 获取工具客户端(自动处理身份认证)
const client = toolClient();
// 2. 以用户身份调用飞书搜索 API
const res = await client.invoke(
'feishu_search_doc_wiki.search',
(sdk, opts, uat) => sdk.request({ ... }),
{ as: 'user' }
);
// 3. 返回结果给 AI
return { content: [{ type: 'text', text: JSON.stringify(res.data) }] };
}
});
这里有个关键设计:「工具不需要关心身份认证的细节」 。toolClient() 内部会自动判断是用用户身份(UAT)还是应用身份(TAT)调用 API,自动处理 Token 刷新、权限检查。工具代码只需要声明 { as: 'user' } 表示"我要以用户身份执行"就够了。
第四步:注册事件钩子和命令
go
// 监听工具调用,只追踪飞书相关的
api.on('before_tool_call', (event) => {
if (event.toolName.startsWith('feishu_')) {
log.info(`tool call: ${event.toolName}`);
}
});
// 注册聊天命令
api.registerCommand({
name: 'feishu_doctor',
async handler(ctx) {
const markdown = await runFeishuDoctor(ctx.config);
return { text: markdown };
}
});
// 注册 CLI 命令
api.registerCli((ctx) => {
ctx.program.command('feishu-diagnose')
.action(async () => { /* 诊断逻辑 */ });
});
「注册完成后」 ,OpenClaw 平台就"认识"了飞书这个频道------知道怎么连接它、知道 AI 可以用哪些飞书工具、知道用户可以输入什么命令。整个过程,「插件没有修改平台的任何代码,只是通过 api 对象"注册"了自己的能力。」
五、插件机制的设计哲学
看完飞书插件的注册流程,我们可以提炼出 OpenClaw 插件机制背后的几个核心设计思想。
1. 契约式集成
平台和插件之间通过**「接口契约」** 交互。ChannelPlugin 接口定义了频道必须实现的方法,ToolResult 定义了工具必须返回的格式。
这意味着:
-
平台不关心你内部怎么实现,只要你按接口约定交付结果
-
插件不需要了解平台内部实现,只需要调 api 上的方法
-
两边可以独立迭代,只要接口不变
2. 声明式发现
插件通过两个 JSON 文件(package.json 的 openclaw 字段 + openclaw.plugin.json)声明自己的能力。平台扫描这些声明就知道每个插件能干什么,不需要加载代码就能在 UI 上展示可用插件列表。
这跟 Chrome 扩展的 manifest.json、VS Code 扩展的 package.json#contributes 是一个思路------「先声明,再加载。」
3. 同进程、共享上下文
插件跑在 OpenClaw 主进程里,共享 Node.js 的事件循环、内存空间。这带来几个好处:
-
「性能好」:工具调用没有 RPC 开销,就是一次函数调用
-
「上下文共享」:插件可以通过 AsyncLocalStorage 获取当前请求的完整上下文(谁发的消息、在哪个群、哪个账号),不需要额外传参
-
「生命周期统一」:平台启动,插件加载;平台关闭,插件卸载。不存在"插件进程挂了但平台不知道"的问题
当然也有代价:「一个插件崩了可能影响整个进程」。所以插件质量很重要,OpenClaw 在这方面也做了一些防护(比如工具执行有 try-catch 兜底、WebSocket 断连自动重连等)。
4. 能力可组合
一个插件可以按需注册能力:
-
「只注册工具」 :最简单,写一个函数就行。比如做一个"翻译工具"插件,只需要注册一个
translate工具 -
「注册工具 + 命令」:稍微复杂一点。工具给 AI 用,命令给人用
-
「注册完整频道」:最复杂,需要实现消息收发的全套链路。但你获得的是让 AI Agent 接入一整个消息平台的能力
飞书插件是最复杂的那种------「一个插件同时注册了频道 + 30 多个工具 + 多个命令」,覆盖了消息、文档、日历、任务、多维表格等几乎所有飞书开放 API 能力。
5. 与其他平台的对比
| 维度 | OpenClaw 插件 | VS Code 扩展 | ChatGPT Plugin | Grafana 插件 |
|---|---|---|---|---|
| 加载方式 | 同进程 import | 同进程 require | HTTP 远程调用 | 同进程 + iframe |
| 通信方式 | 直接函数调用 | VS Code API | RESTful API | 数据源接口 |
| 声明文件 | package.json + plugin.json | package.json | ai-plugin.json | plugin.json |
| 能力类型 | 频道/工具/命令 | 命令/视图/语言 | API 端点 | 数据源/面板 |
| 沙箱隔离 | 无 | 有(Extension Host) | 天然隔离(HTTP) | 部分(iframe) |
OpenClaw 的方案更像 VS Code 早期版本------「信任插件,给予完全的进程内访问权限,换取最低的调用开销和最大的灵活性」。随着生态成熟,未来可能会引入更多隔离机制。
六、结语
回到开头的问题:一个 AI Agent 平台如何在不改核心代码的前提下,不断扩展能力?
OpenClaw 给出的答案是一套三步走的插件机制:
-
「声明」------用两个 JSON 文件告诉平台"我是谁、我能干什么"
-
「注册」------在 register() 函数里把频道、工具、命令挂到平台上
-
「执行」------平台在需要时调用插件注册的函数,插件完成实际工作
飞书插件是这套机制的一个完整实践------一个 npm 包,注册了完整的消息频道 + 30 多个工具 + 多个诊断命令,覆盖了飞书消息、文档、日历、任务、多维表格等几乎所有开放 API。所有这些能力,都通过一条 openclaw install 命令接入,运行在同一个进程里,不需要额外部署任何服务。
「平台做薄,能力外挂。」 这个设计让 OpenClaw 的能力边界不再受限于核心团队的开发速度,而是取决于整个插件生态的广度。今天是飞书,明天可以是钉钉、企业微信、Notion、Jira------只要有人写一个插件。