builderbot-code-skill
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseBuilderBot Code Skill
BuilderBot 编码规范
Operate
操作指南
- Identify the stack — confirm provider package, database adapter, and flow entrypoints before writing code.
- Type the callback signature correctly:
- →
ctx: BotContext{ body, from, name, host, ...platform fields } - →
methods: BotMethods{ flowDynamic, gotoFlow, endFlow, fallBack, state, globalState, blacklist, provider, database, extensions }
- Enforce flow-control semantics — these three MUST be ed:
return- ·
return gotoFlow(flow)·return endFlow('msg')return fallBack('msg') - These MUST be ed:
await·await flowDynamic(...)·await state.update(...)await globalState.update(...)
- Prefer small, composable flows — one responsibility per flow file.
- Keep secrets in env vars — never hardcode tokens, keys, or credentials.
- After writing any flow, run through the UX Review Checklist below — simulate the conversation as the end-user and verify every item before considering the flow done.
- Always validate lint before finishing — run and fix every error before delivering code.
eslint . --no-ignore
- 确认技术栈 — 编写代码前先确认provider包、数据库适配器和流程入口。
- 正确编写回调函数签名:
- →
ctx: BotContext{ body, from, name, host, ...平台专属字段 } - →
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(...)
- 优先使用小型可组合流程 — 每个流程文件只承担单一职责。
- 密钥存放在环境变量中 — 永远不要硬编码token、密钥或凭证。
- 写完任何流程后,都要对照下方的UX检查清单走查 — 以终端用户视角模拟对话,确认所有项都满足后才算流程开发完成。
- 完成前务必检查lint规则 — 提交代码前执行并修复所有错误。
eslint . --no-ignore
Critical Rules (common bugs)
关键规则(常见Bug)
| Bug | Wrong | Right |
|---|---|---|
Missing | | |
Missing | | |
| | |
| | Split into two chained |
| Unnecessary dynamic import | | Top-level |
| Circular ESM import | Top-level | Dynamic |
| | |
Using | | |
| Using buttons | | Text-based numbered menu + |
Checking | | Never use |
| 问题 | 错误写法 | 正确写法 |
|---|---|---|
流程控制语句缺少 | | |
状态/动态消息操作缺少 | | |
给 | | |
同一个回调中同时使用 | | 拆分为两个链式 |
| 不必要的动态导入 | 无循环依赖时使用 | 顶层导入 |
| ESM循环导入 | 互相引用的流程之间使用顶层 | 在回调内部使用动态导入 |
| | |
ESM项目中使用 | | |
| 使用按钮组件 | | 基于文本的编号菜单 + |
检查 | | 永远不要使用 |
Flow Chain
流程链
typescript
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) => voidtypescript
addKeyword(keywords, options?) // ActionPropertiesKeyword: capture, idle, media, delay, regex, sensitive ← 永远不要使用`buttons`
.addAnswer(message, options?, cb?, childFlows?)
.addAction(cb)
// cb: async (ctx: BotContext, methods: BotMethods) => voidState Quick Reference
状态快速参考
typescript
// 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()typescript
// 单用户状态
await state.update({ key: value })
state.get('key') // 支持点语法: 'user.profile.name'
state.getMyState()
state.clear()
// 全局状态(所有用户共享)
await globalState.update({ key: value })
globalState.get('key')
globalState.getAllState()UX Review Checklist
UX 检查清单
After building any flow, mentally walk through the conversation as the end-user and verify every point below. Fix anything that fails before delivering the code.
| # | Question | What to check / fix |
|---|---|---|
| 1 | Does every prompt tell the user exactly what to type? | Each |
| 2 | Are all invalid inputs handled? | Every captured step must have a |
| 3 | Is there an idle timeout on long captures? | Add |
| 4 | Can the user always exit? | Provide a cancel keyword (e.g. "cancel", "salir") or honour it inside captures and call |
| 5 | Are messages short, mobile-friendly, and max 3-4 bubbles? | No wall-of-text AND no bubble spam. Group lines with |
| 6 | Is the user's name used where natural? | Greetings and confirmations should reference |
| 7 | Are menus numbered text lists (never | Use |
| 8 | Does each multi-step flow confirm before committing? | Before irreversible actions (order, payment, delete) show a summary and ask "confirm? yes / no". |
| 9 | Does the flow end with a clear closing message? | The final step must tell the user what happened and what to do next (or say goodbye). |
| 10 | Are error messages actionable? | Never say just "error". Say what went wrong and how to fix it: |
完成任意流程开发后,以终端用户身份在脑海中走一遍完整对话,验证下方所有项,提交代码前修复所有不满足的问题。
| 序号 | 检查项 | 校验/修复规则 |
|---|---|---|
| 1 | 每个提示是否都明确告知用户需要输入的内容? | 所有配置了 |
| 2 | 所有非法输入是否都有处理逻辑? | 每个捕获输入的步骤都必须配置 |
| 3 | 长等待的捕获步骤是否配置了 idle 超时? | 添加 |
| 4 | 用户是否随时可以退出流程? | 提供取消关键词(例如:"cancel"、"salir"),在捕获步骤中识别该关键词后调用 |
| 5 | 消息是否简短、适配移动端,且最多3-4个消息气泡? | 不要发送大段文本,也不要一次性发送过多气泡。用 |
| 6 | 合适的场景下是否使用了用户姓名? | 问候和确认类消息有 |
| 7 | 菜单是否是编号文本列表(永远不要用 | 使用 |
| 8 | 多步流程在执行不可逆操作前是否有确认步骤? | 执行不可逆操作(下单、支付、删除)前要展示摘要,询问用户"确认?yes / no"。 |
| 9 | 流程结束时是否有清晰的收尾消息? | 最后一步要告知用户操作结果,以及后续需要做什么(或者道别)。 |
| 10 | 错误提示是否可行动? | 不要只说"出错了",要说明问题原因和修复方式: |
WhatsApp Text Formatting
WhatsApp 文本格式
WhatsApp uses its own markdown — always apply it in message strings.
| Style | Syntax | Example |
|---|---|---|
| Bold | | |
| Italic | | |
| Strikethrough | | |
| Monospace | | |
| Bullet list | | |
WhatsApp有专属的markdown语法,所有消息字符串都要遵循该规范。
| 样式 | 语法 | 示例 |
|---|---|---|
| 粗体 | | |
| 斜体 | | |
| 删除线 | | |
| 等宽字体 | | |
| 无序列表 | | |
Rules
格式规则
- Use for product names, totals, section headers, and actions the user must take.
*bold* - Use for descriptions, hints, and secondary info.
_italic_ - Use (not
•) for list items — renders cleaner on mobile.- - Use (or
━━━━━━━) as a visual divider between sections inside one bubble.--- - Never use HTML tags (,
<b>, etc.) — WhatsApp ignores them.<br> - Never use standard markdown (,
**bold**) — not supported.## heading
- 产品名、总价、章节标题、用户需要执行的操作使用。
*粗体* - 描述、提示、次要信息使用。
_斜体_ - 列表项使用(不要用
•)— 移动端显示效果更好。- - 同一个气泡内的不同章节用(或
━━━━━━━)作为视觉分隔线。--- - 永远不要使用HTML标签(、
<b>等)— WhatsApp会忽略这类标签。<br> - 永远不要使用标准markdown(、
**粗体**)— 不支持。## 标题
Example — well-formatted bubble
示例 — 格式规范的气泡内容
typescript
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')typescript
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 Messaging Norms
WhatsApp 消息发送规范
These rules apply to every flow. Violating them makes the bot feel like spam.
Critical distinction — vs with arrays:
flowDynamicaddAnswer| Call | Array behavior |
|---|---|
| ⚠️ Each string = separate WhatsApp message |
| ✅ Joined into one message with line breaks |
Always usewithFlowDynamicMessage[]in.join('\n')when callingbody. Never pass raw string arrays.flowDynamic
| Rule | Wrong | Right |
|---|---|---|
| Max 3-4 bubbles per turn | | |
| Always use random delay | No delay between bubbles | |
| Never spread item lists | | |
所有流程都要遵守以下规则,违反会让机器人看起来像垃圾消息。
关键区别 — 数组参数在和中的表现:
flowDynamicaddAnswer| 调用方式 | 数组行为 |
|---|---|
| ⚠️ 每个字符串对应一条独立的WhatsApp消息 |
| ✅ 所有字符串用换行拼接成一条消息 |
调用时永远使用flowDynamic格式,把内容用FlowDynamicMessage[]合并到.join('\n')中,不要直接传原始字符串数组。body
| 规则 | 错误写法 | 正确写法 |
|---|---|---|
| 每个轮次最多3-4个气泡 | | |
| 所有气泡都要加随机延迟 | 气泡之间没有延迟 | 每个气泡配置 |
| 不要拆分列表内容 | | |
Random delay helper (define once per file)
随机延迟工具函数(每个文件定义一次即可)
typescript
const rnd = () => Math.floor(Math.random() * 800) + 500typescript
const rnd = () => Math.floor(Math.random() * 800) + 500Correct pattern — 3 bubbles max
正确示例 — 最多3个气泡
typescript
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 3typescript
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或addAction = 第3个气泡Presence Update (Baileys / Sherpa only)
在线状态更新(仅支持Baileys / Sherpa)
Simulates "typing..." or "recording..." before sending a message. Makes the bot feel human. Only available withandBaileysProvider.SherpaProvider
typescript
type WAPresence = 'unavailable' | 'available' | 'composing' | 'recording' | 'paused'
// composing = typing bubble recording = audio bubble发送消息前模拟「输入中...」或「录制中...」状态,让机器人更拟人。仅和BaileysProvider支持该功能。SherpaProvider
typescript
type WAPresence = 'unavailable' | 'available' | 'composing' | 'recording' | 'paused'
// composing = 输入中提示 recording = 录制音频提示Pattern — typing indicator before each message
示例 — 每条消息前显示输入中提示
typescript
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)
})typescript
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)
})Rules
状态更新规则
- Always call after sending — clears the indicator.
sendPresenceUpdate('paused', ...) - Combine with delays: presence update → wait →
rnd()→ paused.flowDynamic - Use for text replies,
composingfor audio context.recording - Never use on non-Baileys/Sherpa providers — will throw at runtime.
- 发送消息后必须调用— 清除状态提示。
sendPresenceUpdate('paused', ...) - 配合延迟使用:更新状态 → 等待 → 发送
rnd()→ 清除状态。flowDynamic - 文本回复使用,音频场景使用
composing。recording - 非Baileys/Sherpa服务商永远不要使用该功能 — 运行时会抛出错误。
Debug Checklist
调试检查清单
- Flow not switching →
return gotoFlow(...) - Session not ending →
return endFlow(...) - Fallback not repeating →
return fallBack(...) - State not saved →
await state.update(...) - Idle not firing → add alongside
capture: true(never checkidle)idleFallBack - EVENTS flow not triggering → verify provider maps the event payload to
EVENTS.* - Circular import crash → use dynamic inside the callback (never
await import())require() - Language server shows "Cannot find module" on dynamic imports → check if a circular dep actually exists; if not, replace with static top-level
import - ESLint errors after changes → run and fix before finishing
eslint . --no-ignore - error →
builderbot/func-prefix-endflow-flowdynamicandflowDynamicare in the same callback; split into two chainedendFlow:addActiontypescript.addAction(async (_, { flowDynamic }) => { await flowDynamic(lines) }) .addAction(async (_, { endFlow }) => { return endFlow('bye') })
- 流程不跳转 → 检查是否加了
return gotoFlow(...) - 会话不结束 → 检查是否加了
return endFlow(...) - Fallback不重复触发 → 检查是否加了
return fallBack(...) - 状态不保存 → 检查是否加了
await state.update(...) - Idle不触发 → 检查是否搭配了
idle(永远不要检查capture: true)idleFallBack - EVENTS流程不触发 → 检查provider是否把事件 payload 映射到了
EVENTS.* - 循环导入崩溃 → 在回调内部使用动态(永远不要用
await import())require() - 动态导入时语言服务提示「找不到模块」→ 检查是否真的存在循环依赖,如果没有,换成顶层静态
import - 修改后出现ESLint错误 → 执行修复后再提交
eslint . --no-ignore - 出现错误 →
builderbot/func-prefix-endflow-flowdynamic和flowDynamic放在了同一个回调中,拆分为两个链式endFlow:addActiontypescript.addAction(async (_, { flowDynamic }) => { await flowDynamic(lines) }) .addAction(async (_, { endFlow }) => { return endFlow('bye') })
Module System
模块系统
This project uses ESM ( in package.json, in tsconfig).
"type": "module""module": "ES2022"- NEVER use — it is not available in ESM.
require() - Default: use static top-level imports. Dynamic imports cause TypeScript language server errors when no circular dependency exists.
- Before using , draw the dependency graph. Dynamic import is only justified when flow A calls flow B and flow B calls flow A (a real cycle).
await import()
✅ 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 callbackWhen flows reference each other (circular), use dynamic inside the callback:
import()typescript
.addAction(async (ctx, { gotoFlow }) => {
const { targetFlow } = await import('./target.flow')
return gotoFlow(targetFlow)
})本项目使用ESM(package.json中配置,tsconfig中配置)。
"type": "module""module": "ES2022"- 永远不要使用— ESM不支持该语法。
require() - 默认使用顶层静态导入:没有循环依赖时使用动态导入会导致TypeScript语言服务报错。
- 使用前先梳理依赖关系,仅当流程A调用流程B且流程B调用流程A(真实循环依赖)时才需要使用动态导入。
await import()
✅ 静态导入(无循环): 欢迎页 → 菜单 → 下单 → 支付
import { orderFlow } from './order.flow' ← 写在文件顶部
✅ 动态导入(真实循环): 欢迎页 ↔ 下单页
const { orderFlow } = await import('./order.flow') ← 写在回调内部当流程之间互相引用(循环依赖)时,在回调内部使用动态:
import()typescript
.addAction(async (ctx, { gotoFlow }) => {
const { targetFlow } = await import('./target.flow')
return gotoFlow(targetFlow)
})Modular Structure (recommended)
推荐模块化结构
Organize flows in a directory with a barrel that exports the assembled flow:
src/flows/index.tssrc/
├── app.ts
├── flows/
│ ├── index.ts # createFlow([...all flows])
│ ├── welcome.flow.ts
│ └── order.flow.ts
└── services/typescript
// 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])typescript
// 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()把流程放在目录下,通过桶文件导出组装好的总流程:
src/flows/index.tssrc/
├── app.ts
├── flows/
│ ├── index.ts # createFlow([...所有子流程])
│ ├── welcome.flow.ts
│ └── order.flow.ts
└── services/typescript
// 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])typescript
// 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()References
参考资料
- Code patterns (scaffold, capture, gotoFlow, media, idle, flowDynamic, fallBack, REST, EVENTS, UX patterns): patterns.md
- Provider configs, database configs, TypeScript types: providers.md
- 代码模式(脚手架、输入捕获、gotoFlow、媒体资源、idle、flowDynamic、fallBack、REST、EVENTS、UX模式):patterns.md
- Provider配置、数据库配置、TypeScript类型定义:providers.md