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

基于Node.js+React的Twilio通话应用默认转会议需求及问题咨询

基于Twilio的呼叫保持与呼入转接会议解决方案

针对呼叫保持失效、ENQUEUE指令导致呼叫断开的问题,最可靠的方案是将所有呼入呼叫默认接入Twilio会议,坐席通过Twilio Client加入该会议。Twilio会议原生支持呼叫保持、多参与者管理,完全规避队列方案的兼容性问题。


后端Node.js+Express实现

以下是处理呼入Webhook、生成Client Token的核心代码:

const express = require('express');
const twilio = require('twilio');
const VoiceResponse = twilio.twiml.VoiceResponse;
const app = express();

// 解析Twilio请求体
app.use(express.urlencoded({ extended: true }));

// 处理呼入呼叫的Webhook路由
app.post('/incoming-call', (req, res) => {
  const twiml = new VoiceResponse();
  // 用呼叫SID生成唯一会议ID,避免冲突
  const conferenceId = `call-${req.body.CallSid}`;

  // 配置呼入者进入会议:静音等待、播放等待音乐、坐席进入后启动会议
  const conference = twiml.conference({
    startConferenceOnEnter: false,
    endConferenceOnExit: true,
    waitUrl: 'http://twimlets.com/holdmusic?Bucket=com.twilio.music.ambient',
    muted: true
  });
  conference.say('请稍等,正在为您连接坐席');

  // 可选:发送通知到前端,告知有新呼叫及会议ID(可通过WebSocket/API实现)
  // 示例逻辑(需自行实现前端接收接口)
  // fetch('http://your-frontend-service/new-call', {
  //   method: 'POST',
  //   headers: { 'Content-Type': 'application/json' },
  //   body: JSON.stringify({ conferenceId, caller: req.body.From })
  // });

  res.type('text/xml');
  res.send(twiml.toString());
});

// 生成Twilio Client授权Token(供前端坐席使用)
app.get('/token', (req, res) => {
  const accountSid = 'YOUR_TWILIO_ACCOUNT_SID';
  const authToken = 'YOUR_TWILIO_AUTH_TOKEN';
  const apiKeySid = 'YOUR_TWILIO_API_KEY_SID';
  const apiKeySecret = 'YOUR_TWILIO_API_KEY_SECRET';
  const twimlAppSid = 'YOUR_TWILIO_TWIML_APP_SID';
  const agentIdentity = 'agent-1'; // 自定义坐席标识

  const token = new twilio.jwt.AccessToken(
    accountSid,
    apiKeySid,
    apiKeySecret,
    { identity: agentIdentity }
  );

  const voiceGrant = new twilio.jwt.AccessToken.VoiceGrant({
    outgoingApplicationSid: twimlAppSid,
    incomingAllow: true
  });

  token.addGrant(voiceGrant);
  res.send({ token: token.toJwt() });
});

app.listen(3000, () => console.log('Backend running on port 3000'));

前端React.js实现

以下是坐席端接收呼叫、加入会议、切换呼叫保持的核心代码:

import React, { useState, useEffect } from 'react';
import { Device } from '@twilio/voice-sdk';

const AgentConsole = () => {
  const [twilioDevice, setTwilioDevice] = useState(null);
  const [activeCall, setActiveCall] = useState(null);
  const [pendingConference, setPendingConference] = useState(null);

  // 初始化Twilio Client设备
  useEffect(() => {
    const initDevice = async () => {
      const tokenRes = await fetch('/token');
      const { token } = await tokenRes.json();
      const device = new Device(token);

      device.on('ready', () => console.log('Twilio Client 已就绪'));
      device.on('error', err => console.error('设备错误:', err));

      setTwilioDevice(device);
    };

    initDevice();
  }, []);

  // 监听新呼叫通知(需自行实现后端到前端的通知机制,如WebSocket)
  useEffect(() => {
    // 示例:假设通过WebSocket接收新呼叫
    // const socket = new WebSocket('ws://your-backend-service');
    // socket.onmessage = (event) => {
    //   const { conferenceId, caller } = JSON.parse(event.data);
    //   setPendingConference({ conferenceId, caller });
    // };
  }, []);

  // 加入会议(接听呼叫)
  const answerCall = () => {
    if (!twilioDevice || !pendingConference) return;

    const connection = twilioDevice.connect({
      params: {
        To: `conference:${pendingConference.conferenceId}`,
        StartConferenceOnEnter: true,
        EndConferenceOnExit: true
      }
    });

    connection.on('connect', () => {
      setActiveCall(connection);
      setPendingConference(null);
      console.log('已接入会议');
    });

    connection.on('disconnect', () => {
      setActiveCall(null);
      console.log('已结束通话');
    });
  };

  // 切换呼叫保持/恢复
  const toggleHold = () => {
    if (!activeCall) return;

    const isOnHold = activeCall.isOnHold();
    activeCall.hold(!isOnHold);
    console.log(isOnHold ? '已恢复通话' : '已保持通话');
  };

  return (
    <div className="agent-console">
      <h2>坐席控制台</h2>
      {pendingConference && (
        <div className="pending-call">
          <p>新呼叫来自: {pendingConference.caller}</p>
          <button onClick={answerCall}>接听</button>
        </div>
      )}
      {activeCall && (
        <button onClick={toggleHold}>
          {activeCall.isOnHold() ? '恢复通话' : '保持通话'}
        </button>
      )}
    </div>
  );
};

export default AgentConsole;

替代方案:队列(ENQUEUE)问题排查

如果仍想使用队列方案,需检查以下几点:

  • 确保Twilio队列SID配置正确,且队列在控制台中已创建
  • ENQUEUE的TwiML格式正确:
    <Response>
      <Enqueue waitUrl="http://twimlets.com/holdmusic?Bucket=com.twilio.music.ambient">support-queue</Enqueue>
    </Response>
    
  • 坐席需通过<Dial><Queue>support-queue</Queue></Dial>指令接听队列呼叫,而非直接用Client连接
  • 查看Twilio控制台的错误日志,定位呼叫断开的具体原因(如权限不足、waitUrl不可达)

内容的提问来源于stack exchange,提问作者Debashis Saha

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.18 12:32:06