Hi,大家好 👋 今天来聊聊 OpenClaw 插件开发中那些让人头秃的坑 😅
作为过来人,我把踩过的坑整理成这份避坑指南,建议先收藏再看,防止以后用到时找不着 📌
1. outbound.sendText vs deliver 回调:别再傻傻分不清 🔀
😵 问题现象
这两个货太容易搞混了!用 sendText 回复用户消息、用 deliver 推送系统通知......结果消息跑错路,业务逻辑全乱套,调试调到头秃。
🔍 问题根源
它俩是两条完全不同的消息发送通道,井水不犯河水:
| 特性 | deliver 回调 | outbound.sendText 方法 |
|---|---|---|
| 触发时机 | 用户主动发消息 → 我来响应 | 系统主动推消息 → 不用等用户操作 |
| 典型场景 | 回复用户提问、反馈操作结果 | 系统通知、告警提示、定时推送 |
| messageId | 直接用用户原消息 ID,别自己造 | 手动生成,建议 Date.now().toString(36) |
| 能力 | 支持工具调用、流式输出、多轮对话 | 只能发一次性文本,不支持流式 |
✅ 解决方案
一句话记住:用户主动找你的 → deliver;系统自己要推的 → sendText。
typescript
// 🚀 sendText:主动推送(系统通知、告警等)
outbound: {
sendText: async ({ to, text, accountId }) => {
// 手动生成唯一ID
const messageId = Date.now().toString(36);
// 写到文件日志,方便排查
const fs = require('fs');
const logPath = path.join(__dirname, 'outbound.log');
fs.appendFileSync(logPath,
`[${new Date().toLocaleString()}] messageId: ${messageId} | 发给: ${to}\n`);
await wsClient.send({ type: 'push', payload: { text, messageId, to } });
return { channel: 'yeizi', ok: true, messageId };
}
}
typescript
// 🚀 deliver:被动响应(回复用户消息)
if (message.type === 'message' && message.text) {
// 复用用户的ID,别自己造!
const originalMessageId = message.messageId;
await runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
ctx: finalized,
cfg,
dispatcherOptions: {
deliver: async (payload, info) => {
await wsClient.send({
type: 'response',
payload: { content: payload.content, messageId: originalMessageId }
});
}
}
});
}
2. sendText 日志「消失」怎么办 📝
😵 问题现象
在 sendText 里写了 console.log,控制台毛都没有......函数到底有没有跑?参数对不对?完全不知道,排查问题全靠猜。
🔍 问题根源
sendText 是出站子系统异步调用的,跟主进程控制台不连通,console.log 相当于「对牛弹琴」。
✅ 解决方案
放弃 console.log,写文件日志才是正道:
typescript
const fs = require('fs');
const logPath = path.join(__dirname, 'sendText-debug.log');
const log = (msg) => {
fs.appendFileSync(logPath,
`[${new Date().toISOString()}] ${msg}\n`, { encoding: 'utf8' });
};
log(`发送成功 | messageId: ${messageId} | 内容: ${text}`);
想看日志?type sendText-debug.log 或者直接打开文件,一目了然 👀
3. deliver 回调里抓取 AI 干活的全过程 🤖
😵 问题现象
想知道 AI 到底调用了哪个工具、返回了什么结果,可 deliver 回调触发好几次(工具调用、流式文本、最终回复全混在一起),根本分不清谁是谁。
🔍 问题根源
- 没开详细日志,OpenClaw 默认不输出工具调用信息
- deliver 回调返回的消息分三种类型,得靠
info.kind来区分
✅ 解决方案
第一步:开启完整日志(在 openclaw.json 里)
json
{
"agents": {
// 开启后能看到 AI 调工具的全过程
"verboseDefault": "full"
}
}
第二步:靠 info.kind 区分消息类型
| info.kind | 意思是 | 能拿到的信息 |
|---|---|---|
tool |
AI 调用工具了 | payload.content(工具返回结果)或 payload.error(报错信息) |
block |
AI 正在「打字」中 | payload.content(这波输出的文本片段) |
final |
AI 终于说完了 | payload.content(完整回复) |
代码示例:
typescript
deliver: async (payload, info) => {
if (!info) return;
switch (info.kind) {
case 'tool':
console.log('🛠️ 工具调用/返回:', payload.content || payload.error);
break;
case 'block':
console.log('💬 AI 正在打字:', payload.content);
break;
case 'final':
console.log('✅ AI 最终回复:', payload.content);
break;
}
}
💡 小技巧 :日志会同时写入
/var/log/openclaw/runtime.log,两个地方都可以看。
4. 多账户配置:鉴权鉴到怀疑人生怎么破 🔐
😵 问题现象
配置了 N 个账户,结果每个账户都独立鉴权、WebSocket 连接也各自建一个......服务器压力山大,连接还可能不稳定,鉴权失败的错误弹到麻木。
🔍 问题根源
没搞复用!每个账户都「自立门户」,鉴权、连接全重做了一遍。
✅ 解决方案
方案一:单账户走天下(推荐)
如果业务不需要严格区分用户身份,一个账户就能服务所有人,省心又省力。
方案二:共享连接池(多账户必用)
typescript
// 全局就一个 WebSocket 连接,大家共用
let sharedWsConnection = null;
const getSharedConnection = async () => {
if (!sharedWsConnection) {
// 只有第一次需要鉴权创建连接
sharedWsConnection = await createAndAuthenticateWs();
}
return sharedWsConnection;
};
gateway.startAccount = async (accountId) => {
// 复用!不再重复鉴权
const ws = await getSharedConnection();
// 业务逻辑继续...
};
5. 插件依赖:别再重复安装内置包啦 📦
😵 问题现象
插件启动失败、版本冲突、包体积膨胀......一查,哦豁,dependencies 里写了 openclaw 和 typescript,好家伙,这两个 OpenClaw 运行环境里已经有了!
🔍 问题根源
OpenClaw 内置的包(openclaw、typescript 等),别再 npm install 了,否则版本冲突没商量。
✅ 解决方案
❌ 错误示范(别学):
json
{
"dependencies": {
// 千万别装!
"openclaw": ">=2026.3.12",
// 千万别装!
"typescript": "^5.0.0"
}
}
✅ 正确姿势:
json
{
"name": "@myorg/openclaw-yeizi",
"version": "1.0.0",
"type": "module",
"peerDependencies": {
// 声明版本即可,不实际安装
"openclaw": ">=2026.3.13"
},
"dependencies": {
// 只装业务真正需要的
"ws": "^8.16.0"
},
"devDependencies": {
// 开发用类型定义,可选
"@types/ws": "^8.5.10"
}
}
验证方法:
node_modules里没有 openclaw、typescript 文件夹 ✓npm run start能正常启动插件 ✓
🎉 祝大家插件开发顺利,少掉头发,多掉 bug(然后快速修好)💪
如果觉得有帮助,点个赞 👍 支持一下,也欢迎在评论区分享你的踩坑经历!