跳到正文

现有业务说明文档

本文档列出 src/business/ 下当前实现并注册到 business-registry.ts 的所有 业务,包含:业务名(用于数据库路由)、目的、关键参数、流程要点。

业务一览:

业务名用途关键能力
default-call-business默认循环识别 + 回显hear 循环、关键字挂机
ai-voice-platform-demo-businessAI 语音平台 + MicroSIP 视频演示流程callHear 持续 ASR、模拟 LLM、演示日志
ollama-chat-business真实大模型语音对话(接本地 Ollama LLM)callHear 持续 ASR + Ollama /api/chat 流式 + TTS 播报
kb-audio-chat-business知识库录音对话(客户话术→知识库→录音播放)+ 实时盯屏/人工介入callHear 持续 ASR + 知识库 HTTP 查询 + 录音 playback + 逐轮进度上报 + 会话注册表介入
callout-platform-demo-business外呼平台能力演示外呼变量读取、录音、ASR、结果回写
callout-flow-business通用外呼流程解释器:按 callout-server 流程脚本逐节点执行流程图 JSON + NLU decision/extract + transfer 转人工 + 结果回写
prompt-then-recognize-echo-business先播报后识别再回显,一次性顺序 speak/hear,不带 barge-in
verbose-prompt-then-recognize-business与上面相同,但回调全开,用于排查SpeakHandlers / HearHandlers 全集
call-hear-business整通持续 ASR,按 final 文本驱动callHear + 串行 actionQueue
dtmf-business语音 + DTMF 并发,先到先抢占onDtmf + cancelHear
carrier-simulator-business模拟三大运营商早媒体(空号/关机/无法接通/无人接听/忙/停机)pre_answer + TTS 循环播报、按通道变量选样本
carrier-ringback-detection-demo-business回铃音检测演示:选类型 → 外呼模拟器 → 早媒体 ASR 检测originateWithRingbackDetection + DTMF/语音菜单
conference-host-business会议主持人:邀请 + 启动会议 ASR + 广播originate + conference.*
conference-guest-business会议访客:拿到 conferenceId 后入会conference.join
conference-control-demo-business会议全量会控演示:DTMF 菜单邀请/锁会/录音/静音/禁听/踢出/公告/状态/结束conference.* 全集 + onDtmf
agent-conference-guest-business坐席入会(AI 转人工的坐席侧)conference.join + 通道变量角色/flags
browser-softphone-assist-business浏览器软电话辅助坐席:坐席网页呼入建会并外呼客户SIP Header 会话恢复 + 会议转写推流 + 前端命令循环
call-transfer-demo-business转人工(直接 bridge)bridgeToNumber
originate-and-bridge-demo-business转人工(先外呼 B-leg 再桥接)originateAndBridge
parked-dial-demo-business帮拨号:park → 确认 → 桥接originateParkedCall + bridgeCall
verification-code-business验证码识别:持续 ASR + 自动按键应答 + 结果上报callHear + postHangupDrainMs 尾部补收 + 自动 DTMF + HTTP 上报

default-call-business

业务名default-call-business

目的:调试 / 演示用的默认业务。摘机后循环 hear,识别成功就回显,识别 失败就静默重听,听到 “挂机” 二字则播放结束语后挂机。

关键常量(位于业务文件顶部):

  • TTS_TEXT = "欢迎测试语音合成, 你好世界" — 首轮提示音
  • TTS_REPEAT_PREFIX = "你刚刚说了," — 回显前缀

流程

  1. answer + sleep settleDelayMs
  2. startRecording(默认目录与文件名)
  3. 循环:
    • hear({ prompt, interruptible: true, bargeInTrigger: "vad", timeoutMs: 10000 })
    • 识别成功且文本 == "挂机" → 播 "好的,再见" → hangup → 跳出循环
    • 识别成功其他文本 → 用 回显前缀 + 文本 作为下一轮提示音继续
    • 超时 / 失败 → 仅落日志、继续监听
  4. 通话由对端挂机或 socket 关闭终止循环

适合场景:开发联调、回归冒烟、TTS / ASR 流程演示。


ai-voice-platform-demo-business

业务名ai-voice-platform-demo-business

目的:专门用于“AI 语音平台 + MicroSIP”项目介绍视频录制。呼入后接通, 启动整通持续 ASR,播放欢迎语;用户说出“帮我查一下,系统里今天有哪些未处理的 告警订单?”后,业务模拟 LLM 思考和流式文本输出,再播报固定的告警订单查询结果。

关键常量

  • OPENING_PROMPT — 视频脚本中的 AI 首次欢迎语
  • DEMO_ANSWER — 告警订单查询固定答复
  • LLM_STREAM_CHUNKS — 右侧控制台展示用的模拟大模型流式文本片段
  • HANGUP_KEYWORDS — 命中“挂机 / 挂断 / 再见 / 拜拜”后结束通话

流程

  1. answer + sleep settleDelayMs
  2. startRecording,记录录音文件位置
  3. callHear({ partialResults: true, interruptPlayback: true, bargeInTrigger: "vad" })
  4. 播放欢迎语:“您好!欢迎致电智能语音助手,请问有什么可以帮您?”
  5. ASR partial / final 文本写入 demo_asr_stream_text 日志,供控制台录屏展示
  6. final 文本命中告警 / 订单相关关键词:
    • 写入 demo_llm_thinking
    • 逐段写入 demo_llm_stream_delta
    • 播放固定查询结果
  7. final 文本命中挂机关键词:播放结束语并 hangup("NORMAL_CLEARING")
  8. 其它文本:播报演示提示,引导用户说告警订单查询句式

适合场景:项目介绍视频、MicroSIP 软电话演示、ASR/LLM/TTS/RTP 状态控制台 录屏。该业务不接入真实 LLM 或真实工单系统,查询结果为演示用固定文本。

路由建议:服务启动会把该业务同步到 call_businesses 表,可直接使用 businessCode = ai-voice-platform-demo-business;若要从 MicroSIP 拨固定内线进入, 在 call_business_number_mappings 中把演示内线号码映射到该业务。


ollama-chat-business

业务名ollama-chat-business(文件夹形态 ollama-chat-business/, 入口 index.ts,话术 / 关键词在 prompts.ts,Ollama 客户端在 ollama-chat-client.ts

目的接真实本地大模型的语音对话业务。与 ai-voice-platform-demo-business 的「模拟 LLM 固定答复」不同,本业务把识别到的 final 文本送进 Ollama 的 /api/chat 流式接口,拿到完整回复后再用 TTS 播报,是一个最小但可跑通的 「ASR → 本地 LLM → TTS」闭环。每通电话维护独立的 messages 上下文(含 system prompt),实现多轮记忆。

必填配置(位于 config.json.businessConfig["ollama-chat-business"]):

  • endpoint — 完整的 HTTP(S) Ollama /api/chat 地址
  • model — 模型名称,必须是非空字符串
  • requestTimeoutMs — 单轮正整数超时(毫秒,样例为 60000

缺少配置、URL 非法、URL 不是 /api/chat、模型为空或超时不是正整数时, callflow-esl 会在启动期直接失败,不会回退到 localhost。

本业务也可在 call_businesses.businessConfig 和号码映射的 mappingConfig 中直接 覆盖这三个字段,例如 { "model": "qwen3.5:2b" }。新通话按“文件 < 业务 < 映射” 深合并;显式 business_code 路由跳过映射层。合并结果无效时不会请求 Ollama, 而是记录 business_config_invalid 并播放“系统错误”。

关键常量(位于 prompts.ts):

  • SYSTEM_PROMPT — 约束模型用简洁口语化中文、避免 Markdown / 表格,适合语音播报
  • OPENING_PROMPT / GOODBYE_PROMPT / ERROR_FALLBACK_PROMPT — 欢迎 / 告别 / 兜底语
  • HANGUP_KEYWORDS = ["挂机", "挂断", "再见", "拜拜"] — 命中即结束通话

流程

  1. answer + sleep settleDelayMs + startRecording
  2. 初始化每通电话的 messages(首条为 SYSTEM_PROMPT
  3. callHear({ partialResults: true, interruptPlayback: true, bargeInTrigger: "begin-speaking", noSpeechTimeoutMs: 15000, noTextTimeoutMs: 20000 })
  4. 播放欢迎语后 waitForHangup()
  5. onFinal 经串行 actionQueue 触发 handleFinalTranscript
    • 文本为空 → 跳过;命中挂机关键词 → 播告别语 → hangup("NORMAL_CLEARING")
    • 否则把用户文本追加进 messages,调用 requestOllamaChat(流式,onDelta 逐段落日志),回复追加进 messages 后用 TTS 播报
    • Ollama 调用失败 / 空回复 → 落 ollama_chat_llm_failed 日志(errorKind 区分 connection/http/timeout/response)并播 ERROR_FALLBACK_PROMPT 兜底;新一轮、 挂机或业务关闭导致的主动取消单独记录 ollama_chat_llm_aborted
  6. finally:标记 shuttingDownasrSession.stop("business-complete") → 等 actionQueue 排空

前置条件businessConfig["ollama-chat-business"].endpoint 可从 callflow-esl 主机访问, 且 Ollama 已准备该子项 model 指定的模型;ASR / TTS 服务正常。begin-speaking 打断要求 ASR 推送 speech_started 事件(1.0 已支持)。

适合场景:真实 AI 电话客服 / 语音助手的最小可用样板;想换成 OpenAI 兼容 接口或其它 LLM,只改 ollama-chat-client.tsprompts.ts 即可。


kb-audio-chat-business

业务名kb-audio-chat-business(文件夹形态 kb-audio-chat-business/, 入口 index.ts,知识库客户端与接口契约在 kb-client.ts;盯屏/介入基础设施 复用 shared/live-session.ts

目的知识库对话业务。形态与 ollama-chat-business 相同(持续 ASR + 逐轮问答),但回答来自 llm-chat-server 的独立 conversation strategy, 不经过 Chat 节点或 Ollama;策略返回文字后统一由 callflow-esl 本地 TTS 播放。同时内建 「坐席实时盯屏 + 人工介入」:逐轮对话片段实时 上报 callout-server,坐席在盯屏页可一键介入,业务带着指定坐席走会议转人工。

知识库契约(见 kb-client.ts 顶部注释):接通后先发送 { strategy, action: "start", sessionId },成功响应必须包含 { ok: true, data: { openingPrompt, errorPrompt } };后续问答发送 { strategy, action: "query", sessionId, text },响应为 { ok: true, data: { answerText, hangup } }。初始化失败会以 NORMAL_TEMPORARY_FAILURE 挂机。当前样例使用内置 kb-mock 策略;它固定返回 开场白/失败话术,并确定性回显客户原话。

依赖配置

  • config.businessConfig["kb-audio-chat-business"]endpoint / strategy / token / requestTimeoutMs:conversation strategy 接口、策略 key、 可选内部鉴权 token 和超时;整个子项可缺省,但配置后会在启动期严格校验,开场白与 查询失败话术由策略返回
  • config.callout.calloutServer:callout-server 的 baseUrl / token / requestTimeoutMs; 代码固定调用 /api/call-progress/api/call-results/api/transfers/route/api/transfers/end

流程

  1. answer + settle + 读取 callout 身份变量 + 双声道录音
  2. 登记 liveCallSessionRegistry 会话并上报「通话开始」
  3. 调用 conversation strategy action=start,缓存本通话的开场白与失败话术;成功后才启动 callHear(barge-in)并播放非空开场白,失败则直接挂机并进入清理
  4. 每条 final:写 transcript(同步实时上报)→ 查询策略 → 本地 TTS 播放 answerTexthangup=true 时播完挂机。 每轮返回都经串行门排队按序播放:客户连说多句时各轮话术依次播出(播放中仍 可被 barge-in 打断),不因客户新发言作废后到的应答
  5. 每秒轮询介入信号:命中后等当前播报结束 → agentId 定向转人工 (reserveSpecificAgent)→ 坐席入会成功则 AI 停止应答,客户腿单呼 ASR 切换 为会议级转写(conference.hear),坐席(speaker=agent)与客户的发言持续 写 transcript 实时上报盯屏;失败则停会议转写、重启单呼 ASR 恢复 AI 对话
  6. 转人工路由成功后保存会议 ID;客户在坐席振铃或已入会阶段挂机时,finally 调用 POST /api/transfers/end,令该会议全部坐席/主管 SIP 腿结束,调用失败只记结构化日志
  7. 其余 finally 清理:注销会话 + 上报「通话结束」+ 整段结果回写(result.type = "kb_audio_chat",含 intervention 摘要)+ 挂机

适合场景:需要接入确定性问答策略并复用本地 TTS,批量外呼时 坐席「一人盯多路、有意向即插入」的人机协同外呼。

业务名callout-platform-demo-business

目的:配合 apps/callout-server 的外呼任务演示平台能力。任务接通后进入 本业务,播报外呼平台能力说明,识别客户回应,启动通话录音,并把业务结果回写到 callout 外呼记录,供页面展示摘要、意向和录音回放。

依赖配置

  • config.callout.calloutServer.baseUrl:默认 http://127.0.0.1:9920,结果回写路径 /api/call-results 由代码追加
  • config.callout.calloutServer.token:callout-server 配置 internalApi.callResultToken 后必须同步填写;请求会携带 X-Callout-Internal-Token
  • config.callout.calloutServer.requestTimeoutMs:callout-server 请求独立超时(默认 10000ms)
  • recording.directory:FreeSWITCH 进程可写的录音目录。外呼结果回写只携带 录音短文件名,播放 URL 由 callout-server 的媒体配置统一拼接。

流程

  1. answer + sleep settleDelayMs
  2. 读取 callout_request_idcallout_campaign_idcallout_contact_id
  3. startRecording,录音文件名带外呼 requestId,便于排查
  4. 播放外呼平台能力介绍
  5. hear 识别客户回应
  6. 根据文本归类 interested / not_interested / completed
  7. POST /api/call-results 回写 businessStatusintentLevelintentCategorysummaryrecordingUrl、逐句 transcript 与直接承载结构化演示数据的 businessResult
  8. 播放结束语并 hangup("NORMAL_CLEARING")

适合场景:外呼平台演示、录音质检回放验证、callout 与 callflow 联调冒烟。

路由建议:在 callout-webpage 新建外呼任务时选择 callout-platform-demo-business。该路径依赖 callout-server originate 时注入 的外呼变量,不建议作为普通呼入号码映射业务使用。


callout-flow-business

业务名callout-flow-business(文件夹形态 callout-flow-business/,入口 index.ts,流程图解析在 flow-graph.ts,节点运行辅助在 flow-runtime.ts, callout / NLU HTTP 访问在 http-client.ts

目的通用外呼流程解释器——系统二「流程脚本」的运行时执行端。业务本身不 写死任何话术:接通后按通道变量 script_id 从 callout-server 拉取流程脚本运行时 JSON(config.callout.calloutServer.baseUrl + GET /api/v1/scripts/{id}/runtime), 构建流程图后从 start 节点逐节点执行,支持 start / send / collect / router / chat / end / transfer 七类节点。chat 节点在节点内部执行持续 ASR、Chat SSE、分句 TTS 和语义退出,并在同一通电话内共享受限历史。

预期通道变量(callout-server originate 时经 campaign variables 注入):

  • script_id:要执行的流程脚本 id(必填,缺失直接判失败并回写诊断)
  • callout_attempt_id:外呼尝试记录 id,用于结果回写关联

依赖配置config.callout.*):calloutServer(脚本运行时、transfer 节点预占坐席、 进度和结果回写)、nlucollect.extract_profile 抽取与 router.decision_profile 决策, 走 llm-nlu-server)、chat(Chat 节点 SSE 对话)。每组分别配置 baseUrl / token / requestTimeoutMs

流程要点

  1. script_id / callout_attempt_id,拉取并解析流程脚本运行时 JSON
  2. answer + startRecording,从 start 节点开始沿出边遍历执行:
    • send:渲染模板变量后 TTS 播报
    • collect:播报问题并 hear,把客户回答交给 NLU extract profile 抽取字段
    • router:把上下文交给 NLU decision profile,按返回分支选择出边
    • transfer:调 requestHumanAgentTransfer 预占坐席并把客户腿入会(成功即 结束流程;失败播 unavailable_prompt 走默认出边兜底)
    • end:播报结束语并收尾
  3. 防循环保护:最多执行 MAX_STEPS = 100 个节点,超限视为失败并回写诊断
  4. 结束(含异常)时把逐句 transcript、抽取字段、分支路径与录音短文件名回写 POST /api/call-results

适合场景:所有由 callout-webpage 可视化编排的外呼话术。节点语义与 NLU profile 约定见 apps/callout-server/docs/flow-script-nlu-development.md


prompt-then-recognize-echo-business

业务名prompt-then-recognize-echo-business

目的:最简单的顺序业务:先独立播报一次开场白,等播报完成再开一轮 hear, 识别到文本就回显然后挂机。speakhear 是顺序执行的,不存在提示音 期间的 barge-in。

关键常量

  • TTS_TEXTTTS_REPEAT_PREFIX

流程

  1. answer + sleep settleDelayMs
  2. speak({ kind: "tts", text: TTS_TEXT }) — 独立播报,无 barge-in
  3. hear({}) — 默认参数,等用户开口
  4. 识别成功 → speak(${TTS_REPEAT_PREFIX}${text})hangup("NORMAL_CLEARING")
  5. 识别失败 / 超时 → 落日志后退出,由 SessionRunner 处理后续挂机

适合场景:跑通最小通话链路;新增业务时常作为模板复制。


verbose-prompt-then-recognize-business

业务名verbose-prompt-then-recognize-business

目的:与 prompt-then-recognize-echo-business 的流程一致,但把 SpeakHandlersHearHandlers 的全部回调都接上、并把内部字段全部打到 ctx.log 里。

适合场景

  • 第一次接入时验证回调与字段是否符合预期
  • 排查“为什么提示音没被打断”、“为什么 hear 超时”这类问题时直接复制本业务 并替换业务名即可

call-hear-business

业务名call-hear-business

目的:整通通话的“持续 ASR”模式。一旦进入业务,audio_fork 始终开着, final 文本来一条就触发一轮播报(回显或挂机)。onFinal 通过串行 actionQueue 触发,避免连续 final 引发并发 speak

关键常量

  • TTS_TEXTTTS_REPEAT_PREFIXHANGUP_PROMPT = "好的,再见"

流程

  1. answer + sleep settleDelayMs + startRecording
  2. 启动 ctx.callHear({ partialResults: true, interruptPlayback: true, bargeInTrigger: "vad" })
  3. 播放欢迎语 + waitForHangup()
  4. onFinal 串行触发:
    • 文本为空 → 跳过
    • 文本 == "挂机" → 标记 shuttingDown → 播报结束语 → hangup
    • 其它 → 回显
  5. finallysession.stop("business-complete") + 等 actionQueue 排空

适合场景:实时闲聊 / 持续答疑 / 边听边说的会议笔记类业务。


verification-code-business

业务名verification-code-business(文件夹形态 verification-code-business/, 入口 index.ts,按职责拆为 asr-collector.ts / dtmf-sender.ts / recognition-result.ts / report-client.ts / utils.ts

目的:自动化的「验证码 / 短口令语音识别 + 按键应答 + 结果上报」业务。接通后不 播报提示音,直接启动整通持续 ASR;识别到「请按 X」「press X」这类按键提示时自动回拨 对应 DTMF;挂机后再 drain 一段尾部识别,最终把主被叫与识别结果打印到控制台,并按表单 上报到外部接口。常用于自动接听语音验证码电话、按提示输入并回收验证码内容。

关键常量

  • ASR_POST_HANGUP_DRAIN_MS = 1500index.ts)— 挂机后继续收尾部 ASR 的窗口
  • REPORT_URL = "https://app.yuntl.cc/skkks"report-client.ts)— 结果上报地址, 示例接口,部署时改成自己的回收端点;超时 5s、最多 3 次、失败仅记日志不影响收尾
  • DTMF 发送(dtmf-sender.ts):最多 3 次、重试间隔 [300, 800]ms,用最近 3 段 final 组成短窗口跨片段提取按键,发送成功后对该数字去重

流程

  1. answer + sleep settleDelayMs + startRecording
  2. 启动 ctx.callHear({ partialResults: true, postHangupDrainMs: 1500 })
    • onResult(含 partial)→ 写入 ASR 采集器并落日志
    • onFinal → 交给 DTMF 发送器:命中中文「按 X」/ 英文「press X」提示就自动 sendDtmf(中文数字「四」、英文「four」均可归一为按键)
    • onPostHangupResult → 挂机后尾段继续写入采集器(不再驱动按键)
    • onError / onStop → 写业务日志
  3. waitForHangup() 等对端挂机
  4. finallyasrSession.stop("business-complete") → 等 DTMF 队列排空 → 汇总识别文本 → 控制台打印 → postVerificationCodeReport 上报

采集与去重asr-collector.ts):按 serverSessionId/listenerId + utteranceIndex 归并同一 utterance 的 partial/final,final 优先;缺 utteranceIndex 时用 fallback key, 挂机后 final 会尝试并入重叠的尾段,最终按出现顺序输出去重文本列表。

控制台输出

text
被叫号码:<destinationNumber>
识别结果:<final 文本汇总(空格连接)>
主叫号码:<callerIdNumber>

并输出结构化日志 验证码识别结果,字段含 destinationNumberrecognitionTextcallerIdNumberstatus(无有效 final 文本时为 no-result,否则 recognized)。

上报报文report-client.tsPOST application/x-www-form-urlencoded;charset=UTF-8): from_num=主叫、callee=被叫、content=识别文本、sendTime=YYYY-MM-DD HH:mm:ss

适合场景:自动接听并回收语音验证码 / 短口令、按提示自动按键、把识别结果回传到外部 系统。业务自身不主动挂机、不回写本平台数据库(结果只走控制台 + 外部上报接口)。


dtmf-business

业务名dtmf-business

目的:演示语音 + DTMF 并发抢占。每轮 hear 期间允许 barge-in;任意时刻 按键会立即 cancelHear,播报“您刚刚按了 X 键”,然后进入下一轮 hear。

关键常量

  • OPENING_PROMPTHANGUP_PROMPTDTMF_PROMPT_PREFIX
  • BARGE_IN_ENABLED = trueBARGE_IN_TRIGGER = "vad"
  • HANGUP_KEYWORDS = ["挂断"]

流程

  1. answer + sleep settleDelayMs
  2. 注册 DTMF 监听:抢占当前轮次、播报数字播报
  3. 循环:
    • 启动 hear({ prompt, interruptible: true, bargeInTrigger: "vad", timeoutMs: 10000 })
    • 若被 DTMF 抢占 → 等 DTMF 动作完成后进入下一轮
    • 识别成功且命中关键词 “挂断” → 播报 “即将挂断电话” → 挂机
    • 其它识别结果 → 落日志、丢弃文本后下一轮
  4. finally 取消 DTMF 订阅

适合场景:IVR 菜单中需要同时接受语音和数字键的场景。


carrier-simulator-business

业务名carrier-simulator-business(文件夹形态 carrier-simulator-business/, 旧名 carrier-empty-number-simulator-business 已废弃,导出符号仍兼容别名 carrierEmptyNumberSimulatorBusiness

目的:模拟三大运营商(移动 / 联通 / 电信)常见的早媒体提示音,作为 carrier-ringback-detection-demo-businessoriginateWithRingbackDetection 链路的被叫陪练。 不发 200 OK,只 pre_answer 发 183 early media,再用项目统一 TTS 链路 循环播报提示音文本;业务自身不主动挂机,由主叫取消或上游检测器清理 B-leg。

样本库samples.ts,共 18 条):覆盖 empty_number(空号)、powered_off (关机)、unreachable(无法接通)、no_answer(无人接听)、busy(忙)、 out_of_service(停机)六类场景,外加一条电信助理样本 telecom_assistant。 样本文本刻意保留 ringback-detector 依赖的关键词(“空号”“已关机”“暂时无法接通” “正在通话中”等),即便 TTS 音色有差异,ASR 文本仍能被关键字分类命中。

通道变量(由 originate 拨号串注入,constants.ts):

  • carrier_simulator_carriercmcc / unicom / telecom,可大小写
  • carrier_simulator_prompt(或 SIP 头 sip_h_X-Carrier-Simulator-Prompt):上述六类 + telecom_assistant
  • carrier_simulator_repeat_gap_ms:提示音重复间隔,默认 1000ms
  • carrier_simulator_no_answer_ring_ms:无人接听场景先放回铃的时长,默认 30000ms
  • carrier_simulator_ringback_tone:回铃 tone_stream,默认 tone_stream://%(1000,4000,450);loops=1

样本选择优先级(selection.ts):carrier+prompt 精确命中 → 仅 carrier 时该运营商内随机 → 仅 prompt 时跨运营商随机 → 都缺失/非法则全量随机。

流程

  1. 读初始通道变量,selectCarrierPrompt 选样本、resolveSimulatorSettings 解析参数
  2. pre_answer 发 183 early media(注意不是 answer)+ sleep settleDelayMs
  3. 按样本 timeline 分支:
    • ring_then_announcement_loop(仅无人接听场景):先循环 playback 回铃音 noAnswerRingMs 毫秒,再进入提示音播报循环
    • announcement_loop(其余场景):直接进入提示音播报循环
  4. 提示音循环无本地退出条件:speak 文本 + sleep repeatGapMs,直到主叫取消 / 检测器清理 B-leg 导致 socket 关闭,由 SessionRunner 走正常结束路径清理

适合场景:测试回铃 / 早媒体识别链路,做闭环验证。配合 carrier-ringback-detection-demo-business(交互式选类型)联调最方便。


carrier-ringback-detection-demo-business

业务名carrier-ringback-detection-demo-business

目的:回铃音检测能力的交互式自助演示。呼入后让用户从 A-leg 选一种运营商 提示音类型,业务据此外呼到本地 carrier-simulator-business,由 originateWithRingbackDetection 在 B-leg 早媒体阶段执行 ASR 检测,最后把检测 状态、归类和原始识别文本一并播报给用户。是 carrier-simulator-business + originateWithRingbackDetection 的端到端联调闭环。

关键常量

  • SIMULATOR_NUMBER = "5002"DIAL_STRING_TEMPLATE = "sofia/gateway/local-freeswitch-internal/"
  • DETECTION_TIMEOUT_MS = 20000;无人接听场景放宽到 NO_ANSWER_DETECTION_TIMEOUT_MS = 45000
  • 六类可测类型:按 1~6(空号/关机/无法接通/无人接听/忙/停机),按 7 或说“挂断/挂机”退出
  • MAX_SELECTION_ATTEMPTS = 2

流程

  1. answer + sleep settleDelayMs + 播放开场白
  2. askForSelection:每轮 hear(带提示音 + barge-in)与 onDtmf 并发, DTMF 与语音先到先抢占;语音按关键字匹配类型,DTMF 按 1~6/7 匹配
  3. 选中类型后 runDetectionRound:用 buildSimulatorVariables 注入 business_code / carrier_simulator_prompt 等变量,调用 originateWithRingbackDetection(SIMULATOR_NUMBER, { dialStringTemplate, variables, ... })
  4. status === "answered"(极少数),hangupCall 掉 B-leg
  5. 播报演示结果:选择类型、外呼状态、检测归类(carrierPromptKind)、识别文本、失败原因
  6. 回到菜单继续,直到用户挂机或两次未识别到有效选择

适合场景:演示 / 验收回铃音检测;无需真实外线,单机即可跑通 A-leg 菜单 → B-leg 模拟器 → 早媒体 ASR 的完整链路。


conference-host-business

业务名conference-host-business

目的:会议主持人。验证主叫号是否有主持权限 → 生成 conferenceId → 邀请访客 → 启动会议级 ASR → 主持人入会。

关键常量

  • HOST_NUMBER = "1000"GUEST_NUMBER = "1001"
  • GUEST_BUSINESS_CODE = "conference-guest-business"
  • OPENING_PROMPT / JOIN_ANNOUNCEMENT / INVALID_HOST_PROMPT / INVITE_FAILED_PROMPT

流程

  1. answer + sleep settleDelayMs + startRecording
  2. 校验主叫号 == HOST_NUMBER,否则播放拒绝语 + 挂机
  3. setVariables({ conference_id, conference_role: "host", conference_host_number })
  4. 播开场白
  5. ctx.originate({ destinationNumber: GUEST_NUMBER, businessCode: GUEST_BUSINESS_CODE, variables: { conference_id, conference_role: "guest" } })
  6. 启动会议级 ASR ctx.conference.hear
    • onResult(partial):落日志
    • onFinal:构造“{ 中文化主叫号 } 说:{ 文本 }” 的播报,再 ctx.conference.play 广播给整个会议
  7. ctx.conference.play({ delaySeconds: 2 }) 预排一段“即将开始”的会议播报
  8. ctx.conference.join({ flags: ["moderator"] })

适合场景:多方语音会议、AI 会议秘书。number2ChinaNum 把号码转成 “一零零一” 便于 TTS 播报。


conference-guest-business

业务名conference-guest-business

目的:会议访客。读 conference_id 后入会。

流程

  1. answer + sleep settleDelayMs
  2. getVariable("conference_id");若空 → 播放“会议参数缺失” → 挂机
  3. setVariable("conference_role", "guest")
  4. 播放 “正在加入会议”
  5. ctx.conference.join({ conferenceId })

前置条件:必须由 conference-host-business 通过 originate 触发, 并在 variables 中带 conference_id


conference-control-demo-business

业务名conference-control-demo-business

目的:会议全量会控演示。主持人拨入后创建独立会议,通过会议内 DTMF 菜单演示邀请成员、锁会、录音、成员静音 / 禁听 / 踢出、公告、状态播报和结束会议 等会控能力。被邀请成员仍复用 conference-guest-business 入会。

菜单(DTMF 1~9):

动作
1邀请嘉宾(originate conference-guest-business 入会)
2锁定 / 解锁会议
3开始 / 停止会议录音
4静音 / 取消静音第一个嘉宾
5禁听 / 恢复第一个嘉宾
6踢出第一个嘉宾
7播放全员会议公告
8播报会议状态(成员数 / 锁定 / 录音)
9结束会议

流程要点

  1. answer + sleep settleDelayMs + startRecording
  2. 生成 conferenceIdsetVariables({ conference_id, conference_role: "host" })
  3. ctx.conference.join({ flags: ["moderator"] })
  4. 进入 DTMF 菜单循环:onDtmf 抢占当前播报,按键分发到对应会控动作
  5. 每个动作调用 ctx.conference.*inviteMembers / lock / unlock / startRecording / stopRecording / muteMember / unmuteMember / deafMember / undeafMember / kickMember / play / getState / end), 并播报执行结果
  6. 按 9 结束会议或主持人挂机退出

适合场景:会议会控能力的完整演示与回归;可作为「会议主席台」类业务的脚手架。 会控能力的 HTTP 外部控制面见 apps/callflow-esl/src/http/conference-server.ts/conferences/* RESTful API)。


agent-conference-guest-business

业务名agent-conference-guest

目的AI 转人工的坐席侧入会业务。由 callout-server 的 ACD 路由 (POST /api/transfers/route)通过 inbound ESL originate 坐席分机触发,接通后 按 business_code=agent-conference-guest 路由到本业务。坐席与客户进入同一会议室 即完成转人工接入;后续「坐席间流转」(盲转 / 协商转 / 主管监听强插)在同一会议室 上做成员动态进出,本业务无需改动。

预期通道变量(callout-server originate 时注入):

  • conference_id:会议 ID(必填,缺失则播报「转接参数缺失」并挂机)
  • callout_agent_call_record_id:坐席通话记录 ID,供 freeswitch-events 回写接听/挂断状态
  • callout_conference_role:坐席角色(agent / supervisor
  • callout_conference_flags:会议 flags,| 分隔(如主管静默监听 mute

流程

  1. answer + sleep settleDelayMs
  2. getVariable("conference_id");缺失 → 播报兜底语 → hangup("NORMAL_TEMPORARY_FAILURE")
  3. 读取 callout_conference_role / callout_conference_flags(透传角色与会议 flags)
  4. ctx.conference.join({ conferenceId, flags? }),阻塞至坐席退出会议(自然挂机或被踢)

适合场景:AI 外呼通话转人工坐席、坐席间流转、主管监听 / 强插。完整 ACD 链路 (路由、预占、originate、坐席间流转、僵尸会话兜底)见 apps/callout-server/docs/transfer-to-human.md


browser-softphone-assist-business

业务名browser-softphone-assist-business(单文件形态,配合 src/runtime/browser-softphone-session-hub.ts 会话中枢)

目的浏览器软电话的通话侧业务。坐席在网页端(callout-webpage 坐席通话台) 发起外呼时,浏览器 SIP UA 先拨固定业务号呼入本业务;业务从 SIP Header 里的 sessionId 在 browserSoftphoneSessionHub 中找回会话,建立独立会议(坐席 = host)、 originate 客户入会(复用 conference-guest-business),开启会议转写实时推送前端, 并持续消费前端下发的控制命令。

流程要点

  1. 坐席 SIP 呼入 → answer + settle → 读 SIP Header sessionId;缺失或会话失效 → 播错误提示 → 挂机(CALL_REJECTED
  2. 会话有效 → 建会议(role=host)+ 录音 + originate 客户入会(guest);客户呼叫 失败 → 播报失败 + 结束会话 → 挂机
  3. 成功后开启会议转写:onResult 按角色 upsert 消息推送前端(坐席通话台实时字幕)
  4. 并行两条线:conference.join(host,阻塞至坐席离开)与 runCommandLoop 消费 前端命令——agent_text(打字文本 TTS 仅注入客户腿)、hold / resume (deaf / undeaf 坐席腿)、dtmf(发给客户腿)、transfer(v1 仅记录)、 end(结束会议 + 挂机)
  5. finally:停转写 + 结束会话 + 等命令循环结束

前置条件:callout-server 侧软电话会话与 SIP 配置(/api/softphone-sessions/*、 坐席 SIP 账号),见 apps/callout-server/docs/softphone-sip.md

适合场景:坐席在浏览器直接外呼客户并全程辅助(实时字幕、打字转语音、按键、 保持 / 恢复);也是「网页 ↔ 通话业务」双向桥接的参考实现。


call-transfer-demo-business

业务名call-transfer-demo-business

目的:转人工最简实现:问一遍是否转接,确认后直接 bridgeToNumber

关键常量

  • TRANSFER_TARGET_NUMBER = "1001"
  • MAX_CONFIRM_ATTEMPTS = 2

流程

  1. answer + sleep settleDelayMs
  2. 欢迎语
  3. askForConfirmation:最多两次 hear,匹配“确认/否”意图
  4. 确认 → 播 “正在为您转接人工” → ctx.bridgeToNumber(TRANSFER_TARGET_NUMBER)
  5. 桥接成功 → 主动 hangup,让两端通话由 FS 自然延续

适合场景:基础转接场景(A → AI → 人工坐席)。


originate-and-bridge-demo-business

业务名originate-and-bridge-demo-business

目的:与 call-transfer-demo-business 的目标一致(接通后转人工),但走 “先 originate 一路独立 B-leg 再 uuid_bridge” 的链路。相比 bridgeToNumber 的「同步阻塞桥接」,业务侧能拿到独立的 B-leg UUID 做后续控制;常用于需要在桥接前后插入额外编排(埋点、第三方录音、座席策略)的场景。

关键常量

  • TRANSFER_TARGET_NUMBER = "1001"
  • MAX_CONFIRM_ATTEMPTS = 2

流程

  1. answer + sleep settleDelayMs
  2. 欢迎语
  3. askForConfirmation:最多两次 hear,匹配“确认 / 转人工 / 否”意图
  4. 确认 → 播 “正在为您发起后台外呼” → ctx.originateAndBridge(TRANSFER_TARGET_NUMBER)
  5. status === "bridged" → 落日志后主动 ctx.hangup(),让 FreeSWITCH 完成两端通话
  6. status === "not_answered" / 其它失败 → 播报失败原因后挂机

适合场景:需要拿到 B-leg UUID 做精细控制的转接、外呼后桥接、座席分配演示。


parked-dial-demo-business

业务名parked-dial-demo-business

目的:“帮我拨号” 类业务:先把对方拨通停在 park,再问当前用户是否需要 连接;同意则 bridgeCall,并保留当前通道能在被叫挂机后继续控制;不 同意则 hangupCall

关键常量

  • PARKED_DIAL_TARGET_NUMBER = "1001"
  • PARKED_DIAL_TIMEOUT_MS = 20000
  • MAX_CONFIRM_ATTEMPTS = 2

流程要点

  1. 欢迎语 + 询问是否需要拨号
  2. 进入 while (true) 重试循环:
    • 播 “正在为您拨号”
    • originateParkedCall(target, { dialStringTemplate: "user/" })
    • 不是 parked → 询问是否重试
    • parked → 询问是否连接
      • 确认 → bridgeCall(bLegUuid, { preserveCurrentCallAfterBridge: true })
      • bridge 成功 → Promise.race 等待被叫 / 主叫挂机;被叫先挂可再询问重试
      • bridge 失败 → hangupCall(bLegUuid) + 询问重试
      • 不连接 → hangupCall(bLegUuid, "ORIGINATOR_CANCEL") + 询问重试

适合场景:智能助理代用户拨号、电话秘书。


业务路由数据示意

call_businesses 表样例(按 businessCode 命中):

idbusinessCodebusinessNameisEnabled
1default-call-business默认回显业务1
2prompt-then-recognize-echo-business顺序播报识别回显业务1
3conference-host-business会议主持人业务1
4conference-guest-business会议嘉宾业务1
5dtmf-business语音与 DTMF 并发输入演示1
6call-hear-business单呼持续 ASR 演示业务1
7call-transfer-demo-businessASR 确认转人工演示业务1
8originate-and-bridge-demo-business外呼后桥接演示业务1
9parked-dial-demo-business停泊外呼确认桥接演示业务1
10carrier-simulator-business运营商早媒体模拟业务1
11ai-voice-platform-demo-businessAI 语音平台演示业务1
12callout-platform-demo-businessAI 外呼平台产品介绍业务1
13carrier-ringback-detection-demo-business运营商回铃音检测演示业务1
14conference-control-demo-business会议全量会控演示业务1
15agent-conference-guest坐席入会(AI 转人工)业务1
16ollama-chat-business真实大模型对话业务1
17verification-code-business验证码识别打印业务1
18verbose-prompt-then-recognize-business详细日志播报识别回显业务1
19callout-flow-business通用外呼流程解释器1
20browser-softphone-assist-business浏览器软电话辅助坐席业务1

call_business_number_mappings 表样例(按被叫号码命中):

iddestinationNumberbusinessIdisEnabled
1991111
2991221
3100031
4100141
5991951

注意:HTTP 外呼时优先使用 businessCode,回拨进入 ESL 后 business_code 通道变量 非空则命中 call_businesses;否则按被叫号查 call_business_number_mappings inner join。

文档与代码在同一仓库维护,现有 Markdown 是唯一内容源。