ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

OpenClaw 深度解析(四):插件 SDK 与扩展开发机制实战拆解

OpenClaw 深度解析(四):插件 SDK 与扩展开发机制实战拆解 1. 为什么第三方平台接入总卡在“改核心”这一步如果你正在给 OpenClaw 写一个自定义 ChannelPlugin大概率遇到过这个场景想把某个区域性的即时通讯平台接进来核心仓库里已经有 Telegram、Discord、Slack 的实现但你要接的那个平台没有。摆在面前的路只有两条一条是向主仓库提 PR等审核、等合并之后平台 API 一变动你还得跟着 OpenClaw 的发版节奏走另一条是写一个独立扩展包本地加载或者发布到 npm让任何人都能按需安装。第二条路听起来更自由但它对核心提出了一个硬要求核心必须提供一套稳定的扩展契约。不管贡献者用 TypeScript 还是 JavaScript不管发布的是 .ts 源码还是编译后的 .js核心都要能正确加载、隔离运行而且扩展崩溃时核心不能跟着挂掉。这就是 Plugin SDK 存在的意义也是这篇 OpenClaw 插件 SDK 与扩展开发机制实战拆解要讲清楚的东西。我试过把一个内部消息平台接进 OpenClaw最开始图省事直接改核心源码结果一次内部重构把所有自定义逻辑全打挂了。后来改用扩展包的方式才真正体会到 SDK 契约的价值。下面我会从工程目录、SDK 初始化、ChannelPlugin 注册、本地加载验证到常见报错一步步拆给你看你可以直接照着搭一个能跑起来的最小插件。这篇适合三类人需要为 OpenClaw 编写自定义 ChannelPlugin 的开发者、想给 OpenClaw 加工具或后台服务的扩展作者、以及正在评估要不要把内部平台接进来的技术负责人。核心检索词就是 OpenClaw 插件 SDK、扩展开发、ChannelPlugin全文围绕可跟做的步骤展开。2. TaoToken 前置准备给插件接一个稳定的模型后端在动手写插件之前先把模型后端这件事定下来。插件本身负责的是消息通道、工具注册、生命周期钩子但只要你写的扩展涉及 Agent 调用、工具里要请求 LLM就需要一个稳定的 API 入口。我这边统一用 TaoToken 作为模型接入层原因是它的 Base URL 和 Key 管理比较清晰插件里配置一次就能复用。你需要先拿到两样东西API Key 和 Base URL。API Key 在控制台的 API Keys 页面创建Base URL 固定为https://taotoken.net/api。注意这个地址不带任何查询参数插件配置里直接写这个就行。创建 Key 的入口在这里https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite如果你还没注册官网入口是https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content拿到 Key 之后建议先在模型对话页面验证一下这个 Key 能不能正常调用避免后面插件报错时你分不清是 Key 的问题还是插件的问题https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite为什么插件开发要先做这一步因为很多 ChannelPlugin 在收到消息后会把内容交给 Agent 处理Agent 再去调 LLM。如果你的插件里硬编码了某个模型提供商的地址一旦这个地址变动或者限流整个通道就废了。把模型后端抽出来插件只依赖一个 Base URL 加 Key迁移成本最低。这里要提醒一句TaoToken 是模型接入层不是让你绕过任何合规要求。你在插件里配置的 Key 只用于调用模型接口不要把它写进会提交到公共仓库的代码里。推荐用环境变量注入下面配置章节会给具体写法。对于长期做编码和 Agent 扩展的开发者如果调用量比较大可以看一下 Coding Plan它在持续编码场景下的额度更划算https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite前置准备做完接下来进入插件工程本身。3. 可复制配置插件工程目录与 SDK 初始化先给一个最小可运行的插件工程目录结构。这个结构我实测下来能同时兼容 .ts 源码加载和编译后 .js 加载核心靠的是openclaw/plugin-sdk这个固定 import 路径。extensions/zalo-channel/ ├── openclaw.plugin.json # 插件清单声明 id/kind/configSchema ├── package.json # 包元数据声明入口 ├── tsconfig.json # TS 编译配置 ├── src/ │ ├── index.ts # 插件入口导出 register │ ├── channel.ts # ChannelPlugin 与 ChannelDock 定义 │ └── config.ts # 配置解析辅助 └── dist/ # 编译产物发布 .js 时用openclaw.plugin.json是必填的即使插件没有任何配置项也要给一个空 schema。这是刻意的设计强迫作者明确声明“我不需要配置”而不是让核心去猜{ id: zalo-channel, kind: channel, configSchema: { type: object, additionalProperties: false, properties: { allowFrom: { type: array, items: { type: string } }, botToken: { type: string } } } }package.json里要声明入口同时把openclaw/plugin-sdk作为 peer 依赖不要打包进产物{ name: openclaw-zalo-channel, version: 0.1.0, type: module, main: dist/index.js, types: dist/index.d.ts, peerDependencies: { openclaw: * }, scripts: { build: tsc -p tsconfig.json } }tsconfig.json关键是把openclaw/plugin-sdk的路径映射交给运行时处理编译时只做类型检查{ compilerOptions: { target: ES2022, module: ESNext, moduleResolution: Bundler, strict: true, declaration: true, outDir: dist, rootDir: src, skipLibCheck: true }, include: [src] }然后是插件入口src/index.ts。这里用OpenClawPluginDefinition的register字段核心会注入一个OpenClawPluginApi对象你只能通过它告诉核心你想注册什么不能直接操作核心内部状态import type { OpenClawPluginApi } from openclaw/plugin-sdk; import { emptyPluginConfigSchema } from openclaw/plugin-sdk; import { zaloChannel } from ./channel.js; export default { id: zalo-channel, name: Zalo Channel, description: 把 Zalo 接入 OpenClaw 的通道扩展, version: 0.1.0, kind: channel, configSchema: emptyPluginConfigSchema(), register(api: OpenClawPluginApi) { api.registerChannel(zaloChannel); }, };注意register必须是同步的。加载器调用它之后如果返回值是 Promise会记录一条警告并忽略异步结果。原因是 Gateway 启动时需要确切知道哪些工具、通道、钩子已经就绪异步注册会引入不确定的就绪窗口。有持久化需求的初始化逻辑应该放进registerService它有明确的start()回调。模型后端的配置建议通过环境变量注入在插件里读取const TAOTOKEN_BASE_URL process.env.TAOTOKEN_BASE_URL ?? https://taotoken.net/api; const TAOTOKEN_API_KEY process.env.TAOTOKEN_API_KEY ?? ;这样你的插件代码可以安全提交Key 留在本地环境里。4. ChannelPlugin 注册与本地加载验证ChannelPlugin 的建模分两个对象ChannelDock负责能力声明ChannelPlugin负责生命周期。为什么要分开因为 Dock 里的东西是路由层在消息到达时需要的判断必须快、纯、无副作用而 Plugin 里的逻辑涉及网络连接、有状态的 Adapter只在 Gateway 启动或关闭时才需要。两者生命周期不同自然分离。先写src/channel.tsimport type { ChannelPlugin, ChannelDock } from openclaw/plugin-sdk; export const zaloDock: ChannelDock { id: zalo, capabilities: { chatTypes: [direct, group], media: true, blockStreaming: true, }, outbound: { textChunkLimit: 2000, }, config: { resolveAllowFrom: (config) config.allowFrom ?? [], formatAllowFrom: (list) list.join(, ), }, groups: { resolveRequireMention: () true, }, threading: { resolveReplyToMode: () off, }, }; export const zaloChannel: ChannelPlugin { dock: zaloDock, outbound: { async sendText(ctx, text) { const res await fetch(https://openapi.zalo.me/v3.0/oa/message, { method: POST, headers: { Content-Type: application/json, access_token: ctx.config.botToken, }, body: JSON.stringify({ recipient: { user_id: ctx.target }, message: { text }, }), }); if (!res.ok) { throw new Error(zalo send failed: ${res.status}); } return { ok: true }; }, }, };这里outbound.sendText就是出站消息的实际实现。入站消息通常由 Gateway 的 webhook 接收后交给路由层路由层再根据 Dock 的resolveAllowFrom判断是否放行。接下来是本地加载。OpenClaw 的插件发现分四级来源优先级从高到低是 config、workspace、global、bundled。本地开发时最方便的是放到 workspace 的.openclaw/extensions/目录下mkdir -p .openclaw/extensions ln -s /path/to/extensions/zalo-channel .openclaw/extensions/zalo-channel或者直接在openclaw.yml里显式指定路径这是最高优先级plugins: allow: - zalo-channel paths: - /path/to/extensions/zalo-channel加载器在 require 插件入口之前会做安全检查包括 symlink 是否逃出插件根目录、目录是否有 world-writable 权限位、文件 uid 是否可疑。如果你用软链接确保目标在插件根目录内否则会被source_escapes_root拦下。验证插件是否加载成功用openclaw plugins status正常输出会列出已加载的插件 id、kind、来源层级。如果加载失败这里会显示 diagnostics 里的具体原因。再验证通道是否生效可以发一条测试消息openclaw channel send --channel zalo --to user_id --text hello from plugin如果模型后端也配好了可以走一次完整的 Agent 调用确认插件注册的工具或通道能被 Agent 使用。模型对话的验证入口在这里https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite实测下来最容易出问题的不是插件代码本身而是加载路径和权限。下面把常见报错集中列一下。5. 本篇常见错排查401、local proxy failed 与 OAuth插件开发过程中遇到的报错一半来自模型后端配置一半来自加载器。逐个对照。401 Unauthorized。这个最常见通常是TAOTOKEN_API_KEY没注入或者写错了。检查你的环境变量是否在启动 OpenClaw 的同一个 shell 里 export 了。如果你在插件里硬编码了 Key确认没有多余空格。还有一种情况是 Key 被禁用或额度耗尽去控制台确认一下 Key 状态https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewritelocal proxy failed。这个报错一般出现在插件里请求模型接口时网络层没走通。先确认TAOTOKEN_BASE_URL是https://taotoken.net/api不要多加路径后缀。如果你在插件里用了自定义的 fetch 封装检查有没有把 base URL 拼错。另外确认运行环境能正常访问外网插件进程和主进程的网络策略要一致。reading choices of undefined。这是典型的响应结构解析错误。模型接口返回的 JSON 里没有choices字段通常是因为请求体格式不对或者返回的其实是错误对象。在插件里解析响应前先判断状态码和字段是否存在const data await res.json(); if (!res.ok || !data.choices) { throw new Error(unexpected response: ${JSON.stringify(data)}); } const content data.choices[0]?.message?.content ?? ;OAuth 相关报错。如果你接的平台用 OAuth 授权插件里需要处理 token 刷新。常见错误是 refresh token 过期后没有重新走授权流程导致后续请求全部 401。建议在registerService里起一个后台服务定时刷新 token而不是在每次请求时临时刷新避免并发刷新导致 token 互相覆盖。插件加载失败但没报错。检查openclaw.plugin.json的id是否和openclaw.yml里plugins.allow写的一致。id 不匹配时插件会被静默跳过。另外确认register是同步函数返回 Promise 会被警告并忽略。ChannelPlugin 注册了但收不到消息。检查 Dock 里的resolveAllowFrom是否返回了空数组空数组意味着白名单为空所有消息都会被拒绝。调试时可以先临时返回[*]确认链路通不通再收紧白名单。如果你在排查过程中需要确认模型接口本身是否正常直接用模型对话页面发一条消息最快https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite接入相关的完整文档在这里配置项和字段说明都在里面https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite6. 从最小插件到可发布扩展把上面几步串起来你已经有了一个能本地加载、能注册通道、能发消息的最小插件。接下来要做的是把它变成一个别人也能用的扩展包。第一件事是把模型后端配置从代码里彻底抽离。插件只读环境变量不写死任何 Key。发布到 npm 时在 README 里说明需要设置TAOTOKEN_BASE_URL和TAOTOKEN_API_KEY并给出 API Keys 的获取入口https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite第二件事是补全configSchema。上面示例里用了emptyPluginConfigSchema()实际发布时应该把allowFrom、botToken这些配置项写进 JSON Schema让加载器在register之前用 AJV 校验用户配置。配置不合法就直接拒绝加载绝不让错误数据流入扩展。第三件事是处理插件崩溃隔离。加载器对每个插件的register调用都做了 try/catch一个插件崩了不影响其他插件。但你自己在 Adapter 里写的异步逻辑要自己兜底比如sendText里的网络请求要有超时和重试不要让一个未捕获的 Promise rejection 把整个通道拖死。第四件事是考虑发布形态。你可以只发 .ts 源码让 jiti 在运行时转译也可以发编译后的 .js走正常 require。两种方式核心都支持区别在于使用者的环境。如果目标用户大多用 TypeScript 开发环境发源码更透明如果希望开箱即用发编译产物更稳。最后如果你打算长期维护这个扩展并且它会频繁调用模型建议把 Coding Plan 纳入考虑持续编码场景下的额度管理会省心很多https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite插件 SDK 的整个设计围绕一个核心问题展开如何让第三方代码安全地扩展 OpenClaw而不破坏核心的稳定性和安全边界。固定 import 路径让核心重构不破坏扩展Dock 与 Plugin 分离让热路径和生命周期解耦API 注入让扩展无法直接操作核心内部状态四级来源优先级让项目级和用户级插件能覆盖内置行为三项安全检查加边界文件验证防止恶意插件被加载。你按这篇的目录结构和配置走一遍就能得到一个可加载、可验证、可发布的 ChannelPlugin。
返回列表