You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何将Twilio呼入呼出通话转为会议实现语音coaching功能

实现方案总思路

你的判断是对的,基于Twilio会议实现coaching是最优方案,推荐直接把现有呼入、呼出通话的逻辑从「两方直连」改成「默认进入专属会议」,不需要等到需要加coach的时候再转正在进行的通话——后者需要额外调用Twilio REST API触发通话转移,链路更长更容易出问题,默认进会议的方案两方通话体验和之前完全一致,还天然支持后续加人。
如果确实需要按需触发把已经在进行的普通通话转会议,下文也会给出对应实现。


一、默认进会议的改造方案(推荐)

1. 呼出场景改造

前端代码不需要做任何修改,还是沿用原来的device.connect触发呼叫逻辑,仅需改造后端接口:

呼出场景后端改造代码

const response = new VoiceResponse();
// 生成唯一会议ID,可和业务侧的通话记录绑定存储,后续加coach时直接用
const conferenceId = `conf_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
// 先把主叫(前端Twilio Device用户)接入会议
const dial = response.dial({ 
  callerId: fromNumber, 
  record: "record-from-answer", 
  recordingStatusCallback: "/twilio/voice/record" 
});
dial.conference({
  // 关闭会议默认等待音,两方接入后直接通话
  waitUrl: '',
  startConferenceOnEnter: true,
  endConferenceOnExit: true
}, conferenceId);
res.set("Content-Type", "text/xml");
res.send(response.toString());

额外新增两个回调接口,用于主叫接入后自动呼叫被叫进同一个会议:

// 会议状态回调接口,需要在Twilio控制台配置或者创建会议时指定statusCallback
app.post('/twilio/conference/event', async (req, res) => {
  const { ConferenceSid, StatusCallBackEvent, To } = req.body;
  // 主叫成功进入会议后,触发外呼被叫
  if (StatusCallBackEvent === 'participant-join' && req.body.From === fromNumber) {
    const twilioClient = require('twilio')(accountSid, authToken);
    // 外呼被叫
    await twilioClient.calls.create({
      url: `/twilio/conference/join?confId=${conferenceId}`, // 被叫接通后进会议的TwiML接口
      to: To,
      from: fromNumber
    })
  }
  res.sendStatus(200);
})
// 被叫进会议的通用TwiML接口
app.post('/twilio/conference/join', (req, res) => {
  const response = new VoiceResponse();
  const dial = response.dial();
  dial.conference({
    waitUrl: '',
    startConferenceOnEnter: true,
    endConferenceOnExit: true
  }, req.query.confId);
  res.set("Content-Type", "text/xml");
  res.send(response.toString());
})

2. 呼入场景改造

呼入场景后端改造代码

const response = new VoiceResponse();
// 生成唯一会议ID,和当前呼入记录绑定存储
const conferenceId = `conf_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
// 先呼前端坐席用户
const dial = response.dial({
  callerId: req.body.From,
  answerOnBridge: true,
  record: "record-from-answer",
  recordingStatusCallback: "/twilio/voice/record",
});
dial.conference({
  waitUrl: '',
  startConferenceOnEnter: true,
  endConferenceOnExit: true
}, conferenceId);
res.set("Content-Type", "text/xml");
res.send(response.toString());

坐席接入会议后的逻辑和呼出场景完全一致,在/twilio/conference/event回调里判断坐席接入成功后,把呼入用户接入同一个会议即可。

注意:如果需要coach加入后,原有两方挂断会议也不会自动结束,把endConferenceOnExit参数改为false即可。


二、按需转移现有普通通话的实现方案

如果不想修改现有默认通话逻辑,仅在需要加coach的时候才把正在进行的两方通话转会议,按以下步骤操作:

  1. 提前在通话建立时,把两方的CallSid存储到业务数据库和当前通话绑定
  2. 触发转会议时,分别调用Twilio API把两路通话重定向到进会议的TwiML接口即可,转移过程中仅有极短静音,用户感知很弱:
const twilioClient = require('twilio')(accountSid, authToken);
const conferenceId = '生成的会议ID';
// 转移第一路通话
await twilioClient.calls(第一路通话的CallSid).update({
  url: `/twilio/conference/join?confId=${conferenceId}`,
  method: 'POST'
});
// 转移第二路通话
await twilioClient.calls(第二路通话的CallSid).update({
  url: `/twilio/conference/join?confId=${conferenceId}`,
  method: 'POST'
});

三、三种coach模式的实现

Twilio会议的参与者参数天然支持三种模式,呼叫coach进会议时传入对应参数即可,参与者SID可以通过Twilio会议参与者列表接口获取,也可以在每个参会人加入会议时存储到业务侧:

  • 主动参会模式:coach可听可讲,所有参会人都能听到coach的声音,coach也能听到所有人的声音
    await twilioClient.conferences(会议的ConferenceSid).participants.create({
      to: coach的手机号码,
      from: 你的平台外呼号码,
      muted: false,
      coach: null
    })
    
  • 被动旁听模式:coach仅能听到所有参会人声音,不能发言
    await twilioClient.conferences(会议的ConferenceSid).participants.create({
      to: coach的手机号码,
      from: 你的平台外呼号码,
      muted: true,
      coach: null
    })
    
  • 仅对单方耳语模式:coach的发言仅指定的一方能听到,coach可以听到所有参会人的声音
    await twilioClient.conferences(会议的ConferenceSid).participants.create({
      to: coach的手机号码,
      from: 你的平台外呼号码,
      // 填入需要被耳语的参会方的ParticipantSid
      coach: 目标参会方的ParticipantSid
    })
    

内容的提问来源于stack exchange,提问作者Dominik Myszkowski

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.30 09:06:03