Loading...
Loading...
Compare original and translation side by side
ctx: BotContext{ body, from, name, host, ...platform fields }methods: BotMethods{ flowDynamic, gotoFlow, endFlow, fallBack, state, globalState, blacklist, provider, database, extensions }returnreturn gotoFlow(flow)return endFlow('msg')return fallBack('msg')awaitawait flowDynamic(...)await state.update(...)await globalState.update(...)eslint . --no-ignorectx: BotContext{ body, from, name, host, ...平台专属字段 }methods: BotMethods{ flowDynamic, gotoFlow, endFlow, fallBack, state, globalState, blacklist, provider, database, extensions }returnreturn gotoFlow(flow)return endFlow('msg')return fallBack('msg')awaitawait flowDynamic(...)await state.update(...)await globalState.update(...)eslint . --no-ignore| 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项目中使用 | | |
| 使用按钮组件 | | 基于文本的编号菜单 + |
检查 | | 永远不要使用 |
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) => voidaddKeyword(keywords, options?) // ActionPropertiesKeyword: capture, idle, media, delay, regex, sensitive ← 永远不要使用`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()// 单用户状态
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()| # | 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 | 错误提示是否可行动? | 不要只说"出错了",要说明问题原因和修复方式: |
| Style | Syntax | Example |
|---|---|---|
| Bold | | |
| Italic | | |
| Strikethrough | | |
| Monospace | | |
| Bullet list | | |
| 样式 | 语法 | 示例 |
|---|---|---|
| 粗体 | | |
| 斜体 | | |
| 删除线 | | |
| 等宽字体 | | |
| 无序列表 | | |
*bold*_italic_•-━━━━━━━---<b><br>**bold**## heading*粗体*_斜体_•-━━━━━━━---<b><br>**粗体**## 标题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')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')These rules apply to every flow. Violating them makes the bot feel like spam.
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个气泡 | | |
| 所有气泡都要加随机延迟 | 气泡之间没有延迟 | 每个气泡配置 |
| 不要拆分列表内容 | | |
const rnd = () => Math.floor(Math.random() * 800) + 500const rnd = () => Math.floor(Math.random() * 800) + 500await 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 3await 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个气泡Simulates "typing..." or "recording..." before sending a message. Makes the bot feel human. Only available withandBaileysProvider.SherpaProvider
type WAPresence = 'unavailable' | 'available' | 'composing' | 'recording' | 'paused'
// composing = typing bubble recording = audio bubble发送消息前模拟「输入中...」或「录制中...」状态,让机器人更拟人。仅和BaileysProvider支持该功能。SherpaProvider
type WAPresence = 'unavailable' | 'available' | 'composing' | 'recording' | 'paused'
// composing = 输入中提示 recording = 录制音频提示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)
})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()flowDynamiccomposingrecordingsendPresenceUpdate('paused', ...)rnd()flowDynamiccomposingrecordingreturn gotoFlow(...)return endFlow(...)return fallBack(...)await state.update(...)capture: trueidleidleFallBackEVENTS.*await import()require()importeslint . --no-ignorebuilderbot/func-prefix-endflow-flowdynamicflowDynamicendFlowaddAction.addAction(async (_, { flowDynamic }) => { await flowDynamic(lines) })
.addAction(async (_, { endFlow }) => { return endFlow('bye') })return gotoFlow(...)return endFlow(...)return fallBack(...)await state.update(...)idlecapture: trueidleFallBackEVENTS.*await import()require()importeslint . --no-ignorebuilderbot/func-prefix-endflow-flowdynamicflowDynamicendFlowaddAction.addAction(async (_, { flowDynamic }) => { await flowDynamic(lines) })
.addAction(async (_, { endFlow }) => { return endFlow('bye') })"type": "module""module": "ES2022"require()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 callbackimport().addAction(async (ctx, { gotoFlow }) => {
const { targetFlow } = await import('./target.flow')
return gotoFlow(targetFlow)
})"type": "module""module": "ES2022"require()await import()✅ 静态导入(无循环): 欢迎页 → 菜单 → 下单 → 支付
import { orderFlow } from './order.flow' ← 写在文件顶部
✅ 动态导入(真实循环): 欢迎页 ↔ 下单页
const { orderFlow } = await import('./order.flow') ← 写在回调内部import().addAction(async (ctx, { gotoFlow }) => {
const { targetFlow } = await import('./target.flow')
return gotoFlow(targetFlow)
})src/flows/index.tssrc/
├── 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()src/flows/index.tssrc/
├── app.ts
├── flows/
│ ├── index.ts # createFlow([...所有子流程])
│ ├── 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()