业务模型
在 callflow-esl 里,一通通话 = 一段业务函数。业务入口用 defineBusiness 把「稳定业务编码 / 中文描述 / handler」就地共置并具名导出;handler 的签名固定:
1 | import { defineBusiness } from "../types/business.ts"; |
defineBusiness 是恒等函数,只为对象字面量提供精确的上下文类型——handler 的 ctx 会自动推导为 CallContext,无需再手动标注 CallBusiness。业务函数从开始执行到 return 或 throw,对应一通通话的完整生命周期。中途挂机、超时、socket 断开,由 runtime 自动兜底清理(停 audio_fork、停 playback、清 conference registry 等)。
注册一个新业务(三步)
第 1 步:写业务文件
简单业务用单文件 src/business/my-business.ts,入口用 defineBusiness 把编码 / 描述 / handler 就地写在一起并具名导出;复杂业务改用文件夹形态 src/business/my-business/index.ts(可再拆 prompts.ts / 子流程等同级文件,参考 ai-voice-platform-demo/):
1 | import { defineBusiness } from "../types/business.ts"; |
第 2 步:导出
src/business/index.ts:
1 | export { myBusiness } from "./my-business.ts"; |
第 3 步:注册
在 src/runtime/business-registry.ts 的 import 块引入它,并追加到 businesses 数组。registry 不再重复 code / desc,只负责「汇总 + 唯一性校验」——重复 code 会在启动期直接抛错:
1 | import { |
改完跑一次 bun run typecheck 确保类型通过。
配数据库路由
callflow-esl 启动时会把 business-registry.ts 中注册的业务同步到
PostgreSQL。被叫号码路由可手工插入映射:
1 | INSERT INTO callflow.call_business_number_mappings |
重启 callflow-esl,拨 2000 即触发 myBusiness。
外呼则在 POST /outbound-calls 的请求体里传businessCode: "my-business"。
CallContext 用法范式
基础三件套:answer / speak / hangup
1 | await ctx.execute("answer"); |
提示
SessionRunner 不自动 answer。交互业务必须显式 answer,给信令时间稳定再做下一步。否则可能出现”接通瞬间 TTS 已经播完一半”的怪现象。
播报
1 | // TTS |
单轮识别 hear
1 | const result = await ctx.hear({ |
result.status 取值:
"recognized":拿到了文本"timeout":超时无说话"cancelled":被业务主动 cancel"hangup":通话已挂机
持续识别 callHear
整通通话保持 ASR,回调驱动。适合需要 partial 反馈或并发多个交互的场景。
1 | const session = await ctx.callHear( |
外呼
1 | // fire-and-forget |
桥接 / 转接
1 | await ctx.bridgeToNumber("9000"); // 直接 bridge 到号码 |
会议
1 | const conferenceId = "meeting-" + Date.now(); |
DTMF
1 | // 一次性接收一个按键 |
录音
1 | const r = await ctx.startRecording(); |
录音随通道挂机自动结束,当前没有显式 stopRecording。
通道变量
1 | const id = await ctx.getVariable("customer_id"); |
错误与超时约定
| 情况 | runtime 行为 | 业务建议 |
|---|---|---|
| 业务函数 throw | runtime 兜底挂机 + 清理 | try/catch 关键节点,自定义降级 |
ctx.hear 超时 |
返回 { status: "timeout" } |
决定重试 / 转人工 / 挂机 |
| 通话提前挂机 | 后续 ctx 调用拒绝执行 / 抛错 | await ctx.waitForHangup() 主动等 |
| ESL 命令超时 | 抛错 commandTimeoutMs |
业务调短超时或重试 |
| socket-end | runtime 自动清理;业务 promise 拒绝 | finally 清理外部资源 |
异步与状态管理
callflow-esl 的核心 API 都是 Promise,但有些场景是回调驱动(callHear、onDtmf、conference.hear)。处理”回调里产生的副作用要按顺序执行”时,常见模式:
1 | let actionQueue: Promise<void> = Promise.resolve(); |
actionQueue 保证多次 final 之间的副作用按到达顺序串行。
配置访问
业务函数里可读 ctx.config(已加载的 config.json 对象):
1 | await ctx.execute("sleep", String(ctx.config["esl-server"].settleDelayMs)); |
新增业务级常量时,先想清楚:
- 业务私有、不跨业务复用 → 直接写在业务文件顶部
const FOO = ... - 多业务共享、运维可调 → 加到
config.json的合适分组下
开发命令
1 | bun run dev # 监听模式开发 |
提交前至少跑 bun run typecheck。
现有业务样例
仓库已实现 15 个 demo 业务,可直接参考或复制:
| 业务名 | 用途 |
|---|---|
default-call-business |
多轮识别 + 回显(默认回显业务) |
ai-voice-platform-demo-business |
AI 语音平台演示(callHear + 模拟 LLM) |
prompt-then-recognize-echo-business |
一次性识别 + 回显 |
verbose-prompt-then-recognize-business |
多轮带详细日志的识别回显 |
carrier-simulator-business |
运营商早媒体 / 提示音模拟(供代拨号检测调用) |
carrier-ringback-detection-demo-business |
运营商回铃音检测演示 |
conference-host-business |
会议主持人 |
conference-guest-business |
会议嘉宾 |
dtmf-business |
语音与 DTMF 并发输入 |
call-hear-business |
整通持续识别(单呼持续 ASR) |
call-transfer-demo-business |
ASR 确认后 bridge 转人工 |
callout-platform-demo-business |
外呼平台能力演示(配合 callout-server) |
originate-and-bridge-demo-business |
先外呼 B-leg 再桥接的转人工 |
proxy-dial-demo-business |
代拨号(带回铃 ASR 检测) |
parked-dial-demo-business |
停泊外呼(park → 确认 → bridge) |
更多模式与最小代码示例见 典型场景。
还想要但当前没有的能力
| 想做 | 状态 |
|---|---|
stopRecording |
当前录音随通道挂机自动结束,无显式 stop |
| 跨业务的全局 KV | 业务之间靠 channel variable / DB 通信 |
| “纯 AI 主持人”模式(conference 不入会) | ctx.conference.hear 需要绑定在当前通道上 |
| 多语种 ASR | 当前默认中文 int8 模型(可切中英双语预设);可换模型,但接口不变 |
需要时按 callflow-esl 分层架构,在对应 controller 加方法、再在 runtime-context.ts 暴露。