FreeSWITCH非权威指南 —— Mod_FIFO

mod_fifo

About

mod_fifo 是一个呼叫中心APP,通过分配优先级可以自定义呼叫队列。

FIFO 的意思是『先入先出』。当有呼叫进入队列时,它们按序排列,以便于队列中最长等待时间的呼叫将被第一个得到应答。通常,FIFO呼叫队列被用于『先到先得』的呼叫场景,例如客户服务呼叫中心。

一种替代玩法是mod_callcenter,它更像是传统的ACD应用,由FreeSWITCH™社区的成员提供。

Usage

1
<fifo name>[!<importance_number>] [in [<announce file>|undef] [<music file>|undef] | out [wait|nowait] [<announce file>|undef] [<music file>|undef]]

Definitions

记住如下概念将帮助你玩转mod_fifo

  • Consumer - 摘机坐席。 off-hook agent
    一种注册到FIFO队列并等待『消耗』来自队列的传入呼叫的坐席。摘机坐席的电话在他/她登录到队列后就一直在使用。

HBSpy: 坐席服务完客户之后并不挂机,而是在听MOH,当有新客户呼入时直接接听,省去坐席振铃时间

  • Member - 挂机坐席 on-hook agent
    一种注册到FIFO队列后电话处于空闲状态直到有队列中有传入呼入到来的坐席。给坐席振铃后,他/她才会从队列中拿出一个呼叫来接听。一个呼叫也有可能振多个坐席的铃。

HBSpy: 队列中有呼入后,给注册到该队列的坐席们振铃。实践中的是会依次振铃,间隔会受agent的timeout, lag等参数的影响。

  • Caller - 来电者
    已经进入队列的人,来电者要么在『等待』要么在跟坐席通话。

Configuration

mod_fifo的全局配置参数在以下文件中

1
$CONFDIR/autoload_configs/fifo.conf.xml

Agent Callback

通过以下方式注册坐席。类似Asterisk队列成员

1
2
3
4
5
6
7
<configuration name="fifo.conf" description="FIFO Configuration">
   <fifos>
      <fifo name="fifo1@domain.name.com" importance="0">
         <member timeout="60" simo="1" lag="20">{fifo_member_wait=nowait}user/1005@$${domain}</member>
      </fifo>
   </fifos>
</configuration>
  • timout: 在切换到下一个坐席前的振铃超时(秒)

  • simo: 同时振铃数,至少为1

    • 如果你给多部话机注册了同一个分机号,这里控制有多少话机同时振铃
    • 举例:一个分机号有10部话机,设置simo="10"来同时给这10部话机振铃

HBSpy: 《权威》里把simo定义为『最大能服务的呼叫的数量』,好像跟官方说法有些出入?

  • lag: 在开始接收下一个呼叫前的等待时间(秒),类似Asterisk里的wrapup-time
  • fifo_member_wait: 取值为waitnowait

HBSpy: wait与nowait的区别《权威》里解释的也比较清楚,不清楚的也可以联想一下前面的摘机坐席和挂机坐席

提示:如果你想在fifo呼叫坐席时,给来话一个指定的ID,可以设置origination_caller_id_name和origination_caller_id_num变量。可以通过{}在呼叫字符串上设置,或者通过set app在把来话放入fifo队列的dialplan中设置(要在’fifo in’执行之前)。

HBSpy: 这里的要在’fifo in’执行之前要特别注意,fifo相关的许多变量似乎都是这样的。从fifo执行的debug日志来看,在fifo in之后就会立即从通道变量的值中设置fifo相关的参数值,之后再怎么在执行前修改,比如在坐席接听之前修改fifo_announce,也不会生效。具体是否如此,要等分析完源码之后才能给出定论。所以你想用fifo_announce播放『工号XXX为您服务』似乎是很难的,《权威》中也并没有举这样的例子。

Dialplan Example

把来话压入FIFO队列

1
<action application="fifo" data="myqueue in /tmp/exit-message.wav /tmp/music-on-hold.wav"/>

这会使来话压入一个叫"myqueue"的队列,并且一直播放"/tmp/music-on-hold.wav"的等待音。在来话被出队时,播放"/tmp/exit-message.wav"

HBSpy: 于是这里的exit-message.wav就可以是通常的『为保证服务质量,您的通话将会被录音』,当然你也可以通过设置fifo_announce来实现,本质上是一样的。

把来话压出FIFO队列

1
<action application="fifo" data="myqueue out nowait /tmp/caller-found.wav /tmp/agent-music-on-hold.wav"/>

这会把"myqueue"队列中等待时间最久的来话取出,与坐席在当前通道连接,并给坐席播放"/tmp/caller-found.wav"

如果当前FIFO队列中没有来话,并且你设置的是"wait","/tmp/agent-music-on-hold.wav"就会一直给坐席播放直到有新呼叫到达。并且在通话结束时,坐席不必挂机就会回到FIFO队列

If you have the “nowait” parameter set and there are no calls in the FIFO queue, processing immediately continues past the FIFO queue. If there are one or more calls in the queue, only one call is retrieved and processing continues past the FIFO queue after that call ends.

指定来话出队

如果你想指定一个来话出队,可以设置fifo_bridge_uuid为队列中来话的UDID

设置MOH和放送音

这里有一些方式可以用来给’fifo in’和’fifo out’设置MOH和放送音

1
2
3
4
5
<action application="set" data="fifo_music=<sound path>"/> 
<action application="set" data="fifo_announce=<sound path>" />
<action application="set" data="fifo_orbit_announce=<sound path>" />
<action application="set" data="fifo_orbit_exten=<exten>:[timeout]" />
<action application="set" data="fifo_override_announce=<sound path>" />

HBSpy: 我们说一些在我使用的版本上的实践结果以供参考:

  • fifo_music: 这个是来话在队列中等待时听的音乐,也是摘机坐席在等来话时听的音乐。

  • fifo_announce: 这个是当有坐席摘机时,给来话播放的,此时坐席是听不到的。fifo_music放完之前该来话也没被认为是被服务,也就是说如果在fifo_announce放完之前坐席就把电话挂了,该来话仍在队列中。反应到FIFO-Action上,应该是在consumer_start之后,caller_pop之前(待验证)。然后就是之前说的,目前尚未想到一种好方法,通过此处设置实现播送坐席的工号等信息。

  • fifo_orbit_announce: 这个是来话在队列中等待超过一定时间后的放音。fifo_orbit_*这一系列都是用来控制等待超时的,fifo_orbit_exten中可以定义超时后的跳转exten和超时时长,所以fifo_orbit_announce在没有设置fifo_orbit_exten的超时时长时是不生效的。《权威》中的例子将此设置为『为保证服务质量,您的通话将会被录音』似乎是不合适的。另外还有个设置是fifo_chime_list, fifo_chime_freq在超时放音上也能实现类似的功能,但它其实更看重循环而非超时,并且不能进行exten跳转。

  • fifo_orbit_exten: 如上,超时后的跳转exten和超时时长

  • fifo_override_announce: 这里是给坐席放音的,一般可以用来给坐席提示来电号码。实践中在fifo_announce后播送,并不同时,感觉有些浪费时间,annouce和override_annouce取最长时间之后再双方进行通话不是挺好,不知道是freeSWITCH的BUG还是Feature。另外需注意,由于此设置最终影响的是坐席leg,故你在来话dialplan中设置的tts_engine和tts_voice需要在此通道重新设置。在《权威》中也提到了这个问题,但他的设置方式在我的实践中并没有生效。其它设置方式还可以是坐席的呼叫字符串,全局默认值等。PS: 参考了源码之后发现,缺乏tts_engine和tts_voice变量时,log中的提示也为Invaild Args,没有跟其它情况下的Invaild Args区分,对排错十分不友好。

实现FIFO槽位(直译)

每一个FIFO队列可以有10种优先级槽位(默认优先级是5)。优先级1要高于优先级10。你可以通过如下设置把一个来话放入这10种优先级槽位中

1
<action application="set" data="fifo_priority=1" />

消费者(坐席)可以通过设置fifo_pop_order来影响选择来话服务的优先级

1
<action application="set" data="fifo_pop_order=1,2" />

可以通过逗号分隔符同时设置多个优先级顺序

坐席/来话举例

这个场景有两个号码:7010是坐席用,会一直听MOH直到有来话;7011是来话用,会一直听MOH直到有空闲坐席

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<extension name="Agent_Wait">
  <condition field="destination_number" expression="^7010$">
    <action application="set" data="fifo_music=$${hold_music}"/>
    <action application="answer"/>
    <action application="fifo" data="myq out wait"/>
  </condition>
</extension>
<extension name="Queue_Call_In">
  <condition field="destination_number" expression="^7011$">
    <action application="set" data="fifo_music=$${hold_music}"/>
    <action application="answer"/>
    <action application="fifo" data="myq in"/>
  </condition>
</extension>

坐席通过拨打7010后等待。来话通过被路由或转移到此号码上时就会在队列中等待,直到有空闲坐席为之服务

停泊超时举例

这个例子中,如果来话停泊时间超过了20秒就会被转移到1004这个号码上

1
2
3
4
5
6
7
extension name="park" continue="true">
<condition field="destination_number" expression="^5900$">
<action application="set" data="fifo_music=$${hold_music}"/>
<action application="set" data="fifo_orbit_exten=1004:20"/>
<action application="fifo" data="5900@$${domain} in"/>
</condition>
</extension>

API Commands

fifo

  • list|list_verbose
1
fifo list|list_verbose [ fifo_name ]

'fifo list’和’fifo list_verbose’都能给出许多关于当前定义过的队列的信息,包括正在队列中等待或是被服务的客户(来话),坐席等。如果提供了队列名,就只会列出指定名称队列的信息

HBSpy: list_verbose比list给出的信息多的多,包含了许多非fifo自身的信息,大多是通道变量

  • count
1
fifo count [ fifo_name ]

示例输出

1
2
fifo_1:0:0:1:0:0
fifo_2:0:0:0:0:0

结果是返回每一个队列的一些统计值,用冒号分隔

1
队列名:消费者数:来话数:成员数:正在振铃的坐席数:空闲坐席数
  • importance
1
fifo importance <fifo_name>

输出指定队列的重要权值

  • reparse [del_all]
1
fifo reparse [del_all]

重新载入mod_fifo的配置;如果你使用的是mod_xml_curl来生成的配置,那么这个操作也会重新请求这个xml

del_all参数会引发内存清理(当队列为空时,并不会自动被删除)。内存在大多数情况下并不是什么问题,每个fifo队列的开销非常少,但在多租户的场景下,可能会是个事。

fifo_member

  • add
1
add <fifo_name> <originate_string> [<simo_count>] [<timeout>] [<lag>] [expires] [taking-calls]

给指定队列添加成员,使用的参数跟在Agent Callback章节里的一致。如果指定名称的FIFO队列不存在,就会自动创建一个。

  • del
1
del <fifo_name> <originate_string>

从指定队列中删除成员。当最后一个成员从队列中移除时,队列并不会被自动删除。

Additional variables

  • fifo_destroy_after_use: 当一个FIFO队列第一次使用时,FreeSWITCH会自动创建之,然后存在内存中。这个变量告诉FreeSWITCH销毁此队列,为了节省内存。用于构建一次性队列。

mod_fifo使用或设置的Caller Leg侧通道变量

  • fifo_chime_list: 当客户在队列中等待人员应答时广播的文件列表
  • fifo_chime_freq: 指定fifo_chime_list播放的频率,单位秒
  • fifo_orbit_exten: (exten:timeout), 在超时后将来话转移到指定exten
  • fifo_orbit_dialplan: 超时转移时指定的dialplan,没指定即为当前dialplan
  • fifo_orbit_context: 超时转移时指定的context,没指定即为当前context
  • fifo_orbit_announce: 超时转移前给来话播放的信息
  • fifo_caller_exit_key: 来话退出键,退出后默认挂断
  • fifo_caller_exit_to_orbit: 和fifo_caller_exit_key配合使用,退出后转移来话到指定exten,而不是挂断
  • fifo_serviced_uuid: ?(这个我也很迷)
  • fifo_status: WAITING, TIMEOUT, ABORTED, DONE
  • fifo_timestamp: 记录caller被bridage到consumer的时间戳
  • fifo_serviced_by: 接听电话的Consumer Leg侧的UUID
  • fifo_serviced_uuid: 接听电话的Consumer Leg侧的UUID,如果没设置fifo_consumer_id,内容和fifo_serviced_by一样

mod_fifo设置的Consumer Leg侧通道变量

这些通道变量是mod_fifo根据该consumer的相关配置设置的。

如果该consumer是静态配置成member的,这些变量的值可以通过在consumer的呼叫字符串前用{}设置。使用{}设置多个变量时,以逗号分隔的 变量=值 形式。

如果consumer是通过执行一个dialplan来从fifo中接听电话的,这些变量也可以在consumer所执行的dialplan中用set应用设置。

HBSpy: 总结一下就是提醒你,Caller Leg和Consumer Leg不是一个Leg,一些在Caller Leg中设置的变量并不一定会自动的设置在Consumer Leg上,想想之前的tts_engine和tts_voice

  • fifo_strategy: 来话出队的策略,“more_ppl"或"waiting_longer”[默认]
  • fifo_consumer_id: 设置了之后,给队列指定consumer uuid,当consumer有不同的uuid时使用
  • fifo_record_template: 设置了之后,这里就是会话录音的文件
  • fifo_status: WAITING or TALKING
  • fifo_target: 这个consumer正在服务的来话uuid
  • fifo_override_announce: 给坐席播放的提示音
  • fifo_consumer_wrapup_sound: 服务结束后给来话播放的内容
  • fifo_consumer_wrapup_key: 触发按键
  • fifo_pop_order: 符号分隔的该consumer接听的优先级
  • fifo_member_wait: 可以设置为’wait’或’nowait’
    • 如果设置为’wait’,则当来话挂断后,Consumer Leg并不挂断[默认]
    • 如果设置为’nowait’,则当来话挂断后,Consumer Leg也挂断

总结

  1. mod_fifo在实现了基本的先入先出队列的基础上,又适当的加入了一些呼叫业务相关的功能,fifo_announce等,再加上FIFO队列创建的高度自由性,于是乎成为了一个相对于mod_callcenter而言更好的轮子。通过与ESL相配合,可以开发出功能丰富的呼叫中心业务,同时又不必过于关心通话层面的复杂逻辑处理。(在FreeSWITCH的文档中提到过,想通过自己的脚本处理大多数通话过程,是自寻死路)[1]
  2. FIFO队列的创建非常自由,fifo_member add时会自动创建;fifo in时也会自动创建
  3. 一些不常用的参数的功能目前有待源码验证,官方文档不全,也说得不清不楚

  1. It is generally best to do most telephony processing in the dialplan, not in your script. The FS team has put a great deal of effort into making sofia handle many edge cases of SIP processing, so trying to do all that in your script would prove hopeless. ↩︎