关于 builderbot-code-skill
BuilderBot Code Skill 为使用 BuilderBot 编写聊天机器人流程的开发者提供实现规范与检查清单。内容涵盖流程控制、状态管理、模块导入和 WhatsApp 消息体验。
主要功能
- 01规范流程控制的 return 与 await
- 02提供状态和回调类型参考
- 03说明模块导入与循环依赖处理
- 04提供对话 UX 审查清单
- 05涵盖 WhatsApp 格式与消息规范
适用场景
- 01实现包含输入捕获的多步骤流程
- 02排查流程跳转和状态更新问题
- 03组织可组合的流程模块
- 04检查 WhatsApp 消息的可读性
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ leifermendez/ skill-builderbot-code --skill builderbot-code-skill安装后在 Claude Code 中输入/builderbot-code-skill确认已加载。
附带文件patterns.mdproviders.md
详细介绍
BuilderBot Code Skill
操作
- 识别技术栈 — 编写代码前,确认提供方包、数据库适配器和流程入口。
- 正确指定回调签名:
ctx: BotContext→{ body, from, name, host, ...platform fields }methods: BotMethods→{ flowDynamic, gotoFlow, endFlow, fallBack, state, globalState, blacklist, provider, database, extensions }
- 遵循流程控制语义 — 以下三项必须使用
return:return gotoFlow(flow)·return endFlow('msg')·return fallBack('msg')- 以下各项必须使用
await:await flowDynamic(...)·await state.update(...)·await globalState.update(...)
- 优先使用小型、可组合的流程 — 每个流程文件只负责一项职责。
- 将密钥保存在环境变量中 — 切勿将令牌、密钥或凭据硬编码。
- 编写任何流程后,都要完成下方的 UX 审查清单 — 模拟最终用户进行对话,并在认为流程完成前逐项核验。
- 结束前务必验证 lint — 运行
eslint . --no-ignore,并在交付代码前修复所有错误。
关键规则(常见错误)
| 错误 | 错误写法 | 正确写法 |
|---|---|---|
流程控制缺少 return | gotoFlow(flow) | return gotoFlow(flow) |
状态更新或动态消息缺少 await | state.update({...}) | await state.update({...}) |
flowDynamic 使用原始字符串数组 | flowDynamic(['a','b','c']) — 发送 3 条独立消息 | flowDynamic([{ body: ['a','b','c'].join('\n'), delay: rnd() }]) |
在同一回调中使用 flowDynamic 和 endFlow | await flowDynamic(...); return endFlow() | 拆分成两个链式 addAction — 第一个发送 flowDynamic,第二个调用 return endFlow(...) |
| 不必要的动态导入 | 不存在循环依赖时使用 await import('./flow') | 顶层 import { flow } from './flow' — 仅在流程 A → B 且 B → A 时使用动态导入 |
| ESM 循环导入 | 在相互引用的流程之间使用顶层 import | 在回调中使用动态导入 const { xFlow } = await import('./x.flow') |
idle 未搭配 capture | { idle: 5000 } | { capture: true, idle: 5000 } |
在 ESM 中使用 require() | require('./flow') | await import('./flow') — 项目为 "type": "module",无法使用 require |
| 使用按钮 | { buttons: [...] } | 基于文本的编号菜单 + capture: true |
检查 idleFallBack | if (ctx?.idleFallBack) { ... } | 切勿使用 idleFallBack — 只需设置 { capture: true, idle: N },让流程自动过期 |
流程链
addKeyword(keywords, options?) // ActionPropertiesKeyword: capture, idle, media, delay, regex, sensitive ← NEVER use `buttons`
.addAnswer(message, options?, cb?, childFlows?)
.addAction(cb)
// cb: async (ctx: BotContext, methods: BotMethods) => void
状态快速参考
// Per-user
await state.update({ key: value })
state.get('key') // dot notation supported: 'user.profile.name'
state.getMyState()
state.clear()
// Global (shared across all users)
await globalState.update({ key: value })
globalState.get('key')
globalState.getAllState()
UX 审查清单
构建任何流程后,请从最终用户的角度在脑海中走一遍对话,并核对以下每一项。交付代码前,修复所有未通过的项目。
| # | 问题 | 检查/修复内容 |
|---|---|---|
| 1 | 每条提示是否都明确告诉用户要输入什么? | 每个带有 capture: true 的 addAnswer 都必须说明预期输入(例如“回复 1、2 或 3”。)。 |
| 2 | 是否处理了所有无效输入? | 每个捕获输入的步骤都必须有用于处理错误输入的 fallBack('...') 分支。 |
| 3 | 长时间等待输入时是否设置了闲置超时? | 添加 { capture: true, idle: 60000 } — 流程将在超时后自动过期。 |
| 4 | 用户是否始终可以退出? | 提供取消关键词(例如“cancel”、“salir”),或在输入捕获中处理取消,并调用 return endFlow('...')。 |
| 5 | 消息是否简短、适合手机阅读,且最多包含 3-4 个气泡? | 不要写大段文字,也不要刷屏。将多行内容用 \n 合并到一个 FlowDynamicMessage.body 中。flowDynamic([...]) 数组中的每个字符串都会成为一条单独的 WhatsApp 消息 — 切勿将长列表拆开逐条发送。每个气泡都添加随机延迟。 |
| 6 | 是否在合适的地方使用了用户姓名? | 问候和确认消息应在 ctx.name 可用时提及用户姓名。 |
| 7 | 菜单是否使用编号文本列表(而非 buttons)? | 使用 ['Option 1', '1. Foo', '2. Bar'] + capture: true + fallBack。 |
| 8 | 每个多步骤流程是否都在执行操作前进行确认? | 对于不可逆操作(下单、付款、删除),先显示摘要并询问“确认?yes / no”。 |
| 9 | 流程结束时是否有明确的结束消息? | 最后一步必须告知用户发生了什么以及接下来该做什么(或道别)。 |
| 10 | 错误消息是否能指导用户采取行动? | 切勿只说“错误”。应说明出了什么问题以及如何修复:return fallBack('That doesn\'t look like a valid email. Try again:') |
WhatsApp 文本格式
WhatsApp 使用自己的 Markdown — 消息字符串中始终应用该格式。
| 样式 | 语法 | 示例 |
|---|---|---|
| 粗体 | *text* | *Pepperoni* |
| 斜体 | _text_ | _Tomate y mozzarella_ |
| 删除线 | ~text~ | ~$15~ |
| 等宽字体 | ```text``` | ```CODE123``` |
| 项目符号列表 | • 或 - | • Opción 1 |
规则
- 使用
*bold*标记产品名称、总计、章节标题,以及用户必须执行的操作。 - 使用
_italic_标记描述、提示和次要信息。 - 列表项使用
•(而非-)— 在手机上显示更清晰。 - 在同一个气泡的不同章节之间使用
━━━━━━━(或---)作为视觉分隔符。 - 切勿使用 HTML 标签(
<b>、<br>等)— WhatsApp 会忽略它们。 - 切勿使用标准 Markdown(
**bold**、## heading)— 不受支持。
示例 — 格式良好的气泡
const body = [
'*🍕 Tu pedido*',
'━━━━━━━━━━━━━━',
`• Pizza: *Pepperoni*`,
`• Tamaño: *Mediana (30 cm)*`,
`• Cantidad: *2*`,
'',
`💰 Total: *$26 USD*`,
'',
'_Responde *sí* para confirmar o *no* para cancelar._',
].join('\n')
WhatsApp 消息规范
这些规则适用于每个流程。违反规则会让机器人显得像在发送垃圾消息。
关键区别 — flowDynamic 与使用数组的 addAnswer:
| 调用 | 数组行为 |
|---|---|
flowDynamic(['a','b','c']) | ⚠️ 每个字符串都会成为一条单独的 WhatsApp 消息 |
addAnswer(['a','b','c']) | ✅ 合并成一条消息,各行之间用换行符分隔 |
调用
flowDynamic时,始终在body中使用带有.join('\n')的FlowDynamicMessage[]。切勿传入原始字符串数组。
| 规则 | 错误示例 | 正确示例 |
|---|---|---|
| 每轮最多 3-4 个气泡 | flowDynamic(['line1','line2','line3',...]) → 每个字符串都会成为一个气泡 | flowDynamic([{ body: ['line1','line2','line3'].join('\n'), delay: rnd() }]) |
| 始终使用随机延迟 | 气泡之间没有延迟 | 每个气泡都设置 delay: Math.floor(Math.random() * 800) + 500 |
| 切勿拆开发送列表项 | flowDynamic([...items.map(...)]) | flowDynamic([{ body: items.map(...).join('\n'), delay: rnd() }]) |
随机延迟辅助函数(每个文件定义一次)
const rnd = () => Math.floor(Math.random() * 800) + 500
正确模式 — 最多 3 个气泡
await flowDynamic([
{
body: [
'*Título*',
'',
items.map(i => `• ${i.name} — $${i.price}`).join('\n'),
].join('\n'),
delay: rnd(),
},
{
body: '*Sección 2*\nLínea A\nLínea B',
delay: rnd(),
},
])
// addAnswer o addAction siguiente = bubble 3
在线状态更新(仅限 Baileys / Sherpa)
在发送消息前模拟“正在输入……”或“正在录音……”。让机器人表现得更像真人。仅适用于
BaileysProvider和SherpaProvider。
type WAPresence = 'unavailable' | 'available' | 'composing' | 'recording' | 'paused'
// composing = typing bubble recording = audio bubble
模式 — 每条消息前显示输入状态
const waitT = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
.addAction(async (ctx, { provider, flowDynamic }) => {
await provider.vendor.sendPresenceUpdate('composing', ctx.key.remoteJid)
await waitT(1500)
await flowDynamic([{ body: 'Mensaje que parece escrito por humano', delay: rnd() }])
await provider.vendor.sendPresenceUpdate('paused', ctx.key.remoteJid)
})
规则
- 发送后始终调用
sendPresenceUpdate('paused', ...),以清除状态指示。 - 与
rnd()延迟结合使用:更新状态 → 等待 →flowDynamic→ paused。 - 文本回复使用
composing,音频场景使用recording。 - 切勿在非 Baileys/Sherpa 服务提供方上使用——运行时会抛出错误。
调试清单
- 流程未切换 →
return gotoFlow(...) - 会话未结束 →
return endFlow(...) - 回退未重复 →
return fallBack(...) - 状态未保存 →
await state.update(...) - 空闲事件未触发 → 在
idle旁添加capture: true(切勿检查idleFallBack) - EVENTS 流程未触发 → 确认服务提供方是否将事件负载映射到
EVENTS.* - 循环导入导致崩溃 → 在回调中使用动态
await import()(切勿使用require()) - 语言服务器在动态导入时显示“Cannot find module” → 检查是否确实存在循环依赖;如果没有,则替换为顶层静态
import - 修改后出现 ESLint 错误 → 运行
eslint . --no-ignore并在完成前修复错误 - 出现
builderbot/func-prefix-endflow-flowdynamic错误 →flowDynamic和endFlow位于同一个回调中;拆分为两个串联的addAction:.addAction(async (_, { flowDynamic }) => { await flowDynamic(lines) }) .addAction(async (_, { endFlow }) => { return endFlow('bye') })
模块系统
本项目使用 ESM(package.json 中的 "type": "module",tsconfig 中的 "module": "ES2022")。
- 绝不可使用
require()——ESM 中不可用。 - 默认使用顶层静态导入。 不存在循环依赖时,动态导入会导致 TypeScript 语言服务器报错。
- **使用
await import()前,**先绘制依赖关系图。只有当流程 A 调用流程 B,且流程 B 调用流程 A(确实存在循环)时,才有理由使用动态导入。
✅ Static (no cycle): welcome → menu → order → payment
import { orderFlow } from './order.flow' ← top of file
✅ Dynamic (real cycle): welcome ↔ order
const { orderFlow } = await import('./order.flow') ← inside callback
当流程相互引用(形成循环)时,在回调中使用动态 import():
.addAction(async (ctx, { gotoFlow }) => {
const { targetFlow } = await import('./target.flow')
return gotoFlow(targetFlow)
})
模块化结构(推荐)
建议将流程整理到 src/flows/ 目录中,并使用导出已组装流程的 barrel index.ts:
src/
├── app.ts
├── flows/
│ ├── index.ts # createFlow([...all flows])
│ ├── welcome.flow.ts
│ └── order.flow.ts
└── services/
// src/flows/index.ts
import { createFlow } from '@builderbot/bot'
import { welcomeFlow } from './welcome.flow'
import { orderFlow } from './order.flow'
export const flow = createFlow([welcomeFlow, orderFlow])
// src/app.ts
import { createBot, createProvider } from '@builderbot/bot'
import { MemoryDB as Database } from '@builderbot/bot'
import { BaileysProvider as Provider } from '@builderbot/provider-baileys'
import { flow } from './flows'
const main = async () => {
const provider = createProvider(Provider)
const database = new Database()
await createBot({ flow, provider, database })
provider.initHttpServer(+(process.env.PORT ?? 3008))
}
main()
参考资料
- 代码模式(脚手架、capture、gotoFlow、媒体、idle、flowDynamic、fallBack、REST、EVENTS、UX 模式):patterns.md
- 服务提供方配置、数据库配置、TypeScript 类型:providers.md
评论
支持 Markdown