Asterisk权威指南/第十三章 自动呼叫分配(ACD)队列

维基教科书,自由的教学读本

自动呼叫分配(ACD-Automatic Call Distribution),或者称为呼叫排队(call queuing),为PBX提供了为一群用户的来电排队处理的能力:它将多个来电呼叫转接到呼叫保留状态,并为每个呼叫分配一个排名,这个排名用于决定来电被分配给可用坐席的顺序(典型的,采用先进先出)。当某个坐席变为可用时,队列中排在最前面的呼叫会被转给这个坐席处理,并且其它呼叫顺次向前移动一位。

如果你曾经给某些组织打电话时听到过“所有坐席忙,”这样的信息,就意味着你已经有了ACD的使用经验。对拨打电话的人来说,ACD的优点在于他们不必反复拨打以尝试接通;而对于使用了ACD的组织来说,他们将能为客户提供更好的服务,并且可以临时处理一下同时来电数量多于坐席数的情况。

目前有两种呼叫中心:呼入型和呼出型。ACD相关的技术用于处理呼入型呼叫中心,而预拨号器(Predictive Dialer)相关的技术用于处理呼出型呼叫中心。在本书中,我们主要集中讨论呼入型呼叫中心。

我们都有过由设计和管理拙劣的队列带来的糟糕体验:忍受着难听的Hold Music,令人厌恶的等待时间,每20秒重复一遍的毫无意义的信息告诉你“你的来电时如何重要”,尽管你已经等待了30分钟并且重复听这个信息以至于能够背下来了。从客户服务的观点看,队列的设计可能是你电话系统中最重要的一个方面。与使用自动话务员一样,首先要牢牢记住的是,来电者对停留在队列里毫无兴趣。他们打来电话,是因为他们希望和你对话。你所有的设计决定必须以这样一个重要事实为中心:人们是希望与其他人对话,而不是和你的电话系统对话。 本章的目标是教给你如何创建和设计一个队列,从而可以将呼叫者尽量快速而不费力的转接给合适的目标。

在本章中,我们将交替使用术语queue members和agents。除非我们讨论的agents是通过chan_agent(使用AgentLogin())登录的,我们都是在讨论通过AddQueueMember()或命令行(我们将在本章讨论这些命令)增加的queue members。你只需要知道,虽然在Asterisk中agent和queuemember是有区别的,但是我们将简单的使用术语agent来描述Queue()调用的终端(endpoint)。

创建简单的队列[编辑]

作为开始,我们首先创建一个简单的ACD队列。它将接收呼叫者,并将他们分配到几个队列中。

在Asterisk中,术语member指队列中可以被拨叫的一个终端,例如SIP/0000FFFF0001。术语agent技术上是指用于拨叫终端的Agent channel。不幸地是,Agent channel是在Asterisk中废弃了的技术,因为它的灵活性非常有限,而且容易产生一些意想不到的错误,这些错误非常难于诊断和解决。我们不讨论使用chan_agent的情况,所以需要了解的是,我们将使用术语member指电话机设备,而使用术语agent指使用电话机的人。由于这两者单独出现并无意义,所以member或agent任何一个术语都可以用来表示电话机和使用电话的人两者。

我们将在queues.conf文件中创建队列,并且通过Asterisk控制台手工增加队列成员。在本章13.2队列的坐席成员一节,我们将看到如何创建一个dialplan来动态添加和删除队列成员(以及暂停和恢复)。

第一步是在/etc/asterisk配置目录中创建一个空的agents.conf文件。我们不会使用或编辑此文件,但是app_queue模块期望找到它,如果不存在,则不会加载:

首先的步骤是在/etc/asterisk配置目录中创建你的queues.conf文件:

$ cd /etc/asterisk

$ touch agents.conf

然后在其中填入下述配置,这将创建两个命名为[sales]和[support]的队列。你可以将这两个队列命名为任何你希望的名字,但在本书中我们将使用这两个名字。所以,如果你使用了其它的名字,在后续阅读本书时请记得这一点:

接下来你需要创建queue.conf文件,在其中配置定义好的实际队列:

$ touch queues.conf

然后在其中填入下述配置,这将创建两个命名为[sales]和[support]的队列。你可以将这两个队列命名为任何你希望的名字,但在本书中我们将使用这两个名字。所以,如果你使用了其它的名字,在后续阅读本书时请记得这一点:

[general]

autofill=yes

; distribute all waiting callers to available members

shared_lastcall=yes

; respect the wrapup time for members logged into more

; than one queue

[StandardQueue](!)

; template to provide common features

musicclass=default ; play [default] music

strategy=rrmemory ; use the Round Robin Memory strategy

joinempty=no

; do not join the queue when no members available

leavewhenempty=yes

; leave the queue when no members available

ringinuse=no

; don't ring members when already InUse (prevents

; multiple calls to an agent)

[sales](StandardQueue)

; create the sales queue using the parameters in the

; StandardQueue template

[support](StandardQueue)

; create the support queue using the parameters in the

; StandardQueue template

[general]部分定义了默认行为和全局选项。我们在[general]部分仅指定了两个选项,这是因为在这个地方内建的默认值已经可以很好的满足我们的需要。

第一个选项是autofill,它告诉队列将所有等待的呼叫立即分配给所有可用的坐席。在早期的版本中,Asterisk每次只转接一个呼叫,这就意味着当Asterisk向一个坐席转接时,所有其他的呼叫都会进入呼叫保持状态(即使有可用坐席的情况)直到前一个呼叫转接成功(很明显,早期版本的Asterisk在大型、繁忙的队列应用中会导致瓶颈)。除非你有特别的向后兼容的需求,这个选项应当永远被设为yes。

在queues.conf中[general]部分的第二个选项是shared_lastcall。当我们使能shared_lastcall时,对于登录到多个队列的坐席,对于任何一个刚结束的电话,在所有队列中都会开始计算“结束时间”(wrapup time),以避免某个队列将呼叫转接给刚刚结束了另一个队列转接的呼叫,还处于“结束时间”的坐席。如果这个选项设置为no,则“结束时间”的计算仅仅对本队列有效,这将导致一个刚刚接听了support队列的电话尚处于“结束时间”的坐席,仍然会收到sales队列转接的呼叫。这个选项应该一直被设置为yes(默认值)。

下一部分,[StandardQueue]是我们打算应用到sales和support队列的模板(我们通过(!)声明其为模板)。和我们在musiconhold.conf文件中的配置一样,我们定义musicclass为default呼叫保持音乐。我们将strategy配置为rrmemory,代表循环(Round-Robin)存储。rrmemory策略的工作原理是将队列中按顺序排列的坐席循环起来,它跟踪接听了上一个呼叫的坐席,然后将下一个呼叫转接给下一个坐席。当达到最后一个坐席时,它返回队列顶部的坐席(当坐席登录时,他们被加入到队列的尾部)。我们设置joinenpty为no,是因为将呼叫放入一个没有坐席资源的队列是一种坏的做法。

出于测试目的,你可以将这个值设置为yes,但我们不推荐你在正式产品中这么做,除非你使用队列的目的不是转接来电给坐席。没有人愿意等待在一条无人处理的线路上

leavewhenempty选项用于控制当没有可用的坐席能够处理呼叫时,呼叫是否应该离开Queue()应用程序继续执行dialplan。我们将这个选项配置为yes是因为等待在一条无人处理的线路上是毫无意义的。

从商业的观点来看,你应该告诉你的坐席人员在当天登出系统前要处理完队列中所有的呼叫。如果你发现当一天快要结束时有太多的呼叫在队列中,你可能希望考虑延长某些坐席人员的工作时间以处理他们。否则,当他们第二天带着情绪重新打来时,只会给你带来更大的压力。

另一个选择是使用GotoIfTime()在临近下班时间时将呼叫转接到语音信箱,或者dialplan中的其它合适地点。

最后,我们将ringinuse设置为no,它告诉Asterisk不要振铃已经处于振铃状态的坐席。将ringinuse设置为no的目的是避免多个呼叫被转接到来自一个或多个队列的同一个坐席。

你可能注意到joinempty和leavewhenempty都是查找或者队列中已经没有登录的坐席,或者所有的坐席都失效。处于振铃(Ringing)或使用中(InUse)状态的坐席不被认为是失效的,所以当joinempty-no和/或leavewhenempty=yes时不会阻止呼叫加入队列或导致他们离开队列

一旦你完成了你的queues.conf文件的配置,你可以保存它并通过Asterisk CLI重载app_queue.so模块:

$ asterisk -r

*CLI> module reload app_queue.so

-- Reloading module 'app_queue.so' (True Call Queueing)

然后检查一下你的队列是否装载到内存了:

localhost*CLI> queue show

support has 0 calls (max unlimited) in 'rrmemory' strategy

(0s holdtime, 0s talktime), W:0, C:0, A:0, SL:0.0% within 0s

   No Members

   No Callers

sales has 0 calls (max unlimited) in 'rrmemory' strategy

(0s holdtime, 0s talktime), W:0, C:0, A:0, SL:0.0% within 0s

   No Members

   No Callers

现在,你已经创建好了队列,下一步你需要配置你的dialplan以允许呼叫进入你的队列。

在extensions.conf文件中增加如下dialplan逻辑:

[Queues]

exten => 7001,1,Verbose(2,${CALLERID(all)} entering the support queue)

same => n,Queue(support)

same => n,Hangup()

exten => 7002,1,Verbose(2,${CALLERID(all)} entering the sales queue)

same => n,Queue(sales)

same => n,Hangup()

[LocalSets]

include => Queues ; allow phones to call queues

我们将Queue context包含在LocalSets context中,所以我们的电话机可以呼叫我们创建的这个队列。在第15章,我们将定义访问这些队列的菜单项。保存这些变化到你的extensions.conf文件,然后通过dialplan reload命令重载dialplan。

如果你此时拨打分机7001和7002,将以得到下面这样的输出而结束:

-- Executing [7001@LocalSets:1] Verbose("SIP/0000FFFF0003-00000001",

"2,"Leif Madsen" <100> entering the support queue") in new stack

== "Leif Madsen" <1--> entering the support queue

-- Executing [7001@LocalSets:2] Queue("SIP/0000FFFF0003-00000001",

"support") in new stack

[2011-02-14 08:59:39] WARNING[13981]: app_queue.c:5738 queue_exec:

Unable to join queue 'support'

-- Executing [7001@LocalSets:3]

Hangup("SIP/0000FFFF0003-00000001", "") in new stack

== Spawn extension (LocalSets, 7001, 3) exited non-zero on

'SIP/0000FFFF0003-00000001'

你现在还不能加入这个队列,因为在这个队列中没有坐席来应答呼叫。因为我们已经在queues.conf中配置joinempty=no和leavewhenempty=yes,所以呼叫不会加入到队列中。(这是一个很好的实验queues.conf中的joinempty和leavewhenempty选项的机会,这将有助于更好的理解这两个选项对队列的影响。)

在下一节中,我们将说明如何在队列中增加坐席成员(以及其它坐席和队列的交互方法,例如暂停/恢复)。

队列的坐席成员[编辑]

如果没有人应答队列中的呼叫,队列是没有什么用处的。所以我们需要一种方法允许坐席人员登录到队列中应答呼叫。有多种方法可以做到这一点,我们将教你如何手动(作为管理员)和自动(作为坐席人员)向队列中增加坐席成员。我们将从Asterisk CLI方法开始,这种方法允许你很容易的以最小的dialplan修改向队列增加坐席成员用于测试。接着我们将展开讨论,教你如何增加dialplan逻辑使得坐席人员可以登入或登出队列,以及在他们登录的队列中暂停或恢复他们自己。

通过CLI控制队列成员[编辑]

我们可以通过Asterisk CLI命令queue add向任何有效的队列增加坐席成员。queue add命令的格式是(注:应该在同一行输入):

*CLI> queue add member <channel> to <queue> [[[penalty <penalty>] as <membername>] state_interface <interface>]

其中<channel>是我们希望加入队列的channel,例如SIP/0000FFFF0003,<queue>是像support或sales这样的队列的名字——任何存在于/etc/asterisk/queues.conf中的队列的名字。我们将暂时忽略<penalty>选项,但是我们会在本章“13.5 高级队列”一节讨论它(penalty用于控制队列中坐席成员的排名,这对于登录到多个队列的坐席非常重要)。我们可以定义<membername>来提供队列登录的细节。state_interface是需要我们仔细研究的一个选项。因为这个选项在Asterisk中对于队列及其成员的所有方面都非常重要,以至于我们要用几个小节来讨论它。所以,你可以继续先阅读本章的“13.2.4 设备状态简介”一节,然后再回到这里继续阅读。

现在,你已经在sip.conf中增加了callcounter=yes(在我们后续的例子中,我们都将使用SIP channels),让我们看看如何通过Asterisk CLI在你的队列中增加成员。(译者注:关于callcounter的说明要参看本章后面的“设备状态简介”一节)。

在support队里中增加队列成员,可以通过queue add member命令实现:

*CLI> queue add member SIP/0000FFFF0001 to support

Added interface 'SIP/0000FFFF0001' to queue 'support'

然后通过查询队列状态来确认新成员已经被增加:

*CLI> queue show support

support has 0 calls (max unlimited) in 'rrmemory' strategy

(0s holdtime, 0s talktime), W:0, C:0, A:0, SL:0.0% within 0s

Members:

   SIP/0000FFFF0001 (dynamic) (Not in use) has taken no calls yet

No Callers

删除队列成员,你可以使用queue remove member命令:

*CLI> queue remove member SIP/0000FFFF0001 from support

Removed interface 'SIP/0000FFFF0001' from queue 'support'

当然,你可以再次使用queue show命令来确认目标成员已经被删除。

我们还可以从Asterisk控制台暂停和恢复队列成员,通过命令queue pause member和queue unpause member命令。它们和我们之前使用的命令格式差不多:

*CLI> queue pause member SIP/0000FFFF0001 queue support reason DoingCallbacks

paused interface 'SIP/0000FFFF0001' in queue 'support' for reason 'DoingCallBacks'

 

*CLI> queue show support

support has 0 calls (max unlimited) in 'rrmemory' strategy

(0s holdtime, 0s talktime), W:0, C:0, A:0, SL:0.0% within 0s

Members:

SIP/0000FFFF0001 (dynamic) (paused) (Not in use) has taken no calls yet

No Callers

通过增加队列成员的暂停原因,例如lunchtime,你可以保证你的队列日志包含一些可能有用的附加信息。下面是如何恢复队列成员:

*CLI> queue unpause member SIP/0000FFFF0001 queue support reason off-break

unpaused interface 'SIP/0000FFFF0001' in queue 'support' for reason 'off-break'

*CLI> queue show support

support has 0 calls (max unlimited) in 'rrmemory' strategy

(0s holdtime,0s talktime), W:0, C:0, A:0, SL:0.0% within 0s

Members:

      SIP/0000FFFF0001 (dynamic) (Not in use) has taken no calls yet

No Callers

在实际应用中,CLI一般不是控制队列中坐席状态的最佳方式。替代地,我们利用dialplan应用程序允许坐席人员通知队列它们的可用状态。

在queues.conf文件中定义队列成员[编辑]

用拨号计划逻辑控制队列成员[编辑]

在由坐席人员组成的呼叫中心中,最常见的做法是让坐席人员自己在上班和下班时登入及登出队列(或者当他们去吃饭,去洗手间,或因为其它原因不能接听电话时登出队列)。要做到这一点,我们需要使用下述dialplan应用程序:

  • AddQueueMember()
  • RemoveQueueMember()

当登入队列之后,还可能出现坐席人员临时不能接听电话的情况。下面的应用程序用于这个目的:

  • PauseQueueMember()
  • UnpauseQueueMember()

也许这样理解这些应用程序更加容易:加入/离开应用程序用于登入和登出,而暂停/恢复用于坐席短时间不可用。区别是非常简单的,暂停/恢复是在没有登出队列的情况下将成员状态设置为不可用和可用。这对于生成报表十分有用(如果一个成员是暂停的,队列管理员会看到她已经登录到队列,但只是暂时不能接听电话)。如果你不能确定应该用哪个运管用程序,我们推荐你在任何情况下都使用加入/离开。

使用暂停(Pause)和恢复(Unpause)

使用暂停和恢复大概算一种偏好。在某些环境下,这组选项会被用于在上班时间导致坐席不可用的任何活动(例如午饭时间或进行一些与接听电话无关的工作)。然而,在大部分呼叫中心,如果一个坐席人员不在她的电话旁准备接听电话的时候,她根本就不应当登入系统,即使她只是离开她的座位几分钟(例如上洗手间)。

有些主管喜欢利用加入/离开和暂停/恢复配置作为某种打卡钟,这样他们可以跟踪他的职员何时到达开始工作和何时结束工作离开,以及他们在座位上待了多长时间和休息了多长时间。我们并不认为这是一种合理的实践,因为这些应用程序的目的是通知队列坐席的可用状态,而不是跟踪员工的活动。

在这里要非常重视的一件事是queues.conf中joinempty的设置,我们之前讨论过这个选项。如果坐席是暂停的,她仍旧被认为是登录到队列中的。让我们假设接近下班的时候,某个坐席在几个小时前将自己设置为暂停状态。所有其他的坐席都登出系统并回家了。这时来了个电话。队列会注意到仍旧有一个坐席登录在队列中,然后就将这个来电加入到队列,尽管事实上此时已经没有人在处理这个队列了。结果是这个来电会被永远保留在这个无人处理的队列中。

简而言之,没有坐在座位旁边准备接听电话的坐席应该登出系统。暂停/恢复应该仅仅用于短暂的不可用状态(如果是真的的话)。如果你希望利用你的电话系统作为打卡钟,在Asterisk中有很多更好的办法,但是队列成员应用程序绝对不是我们推荐的方式。

让我们创建一些简单的dialplan逻辑来让我们的坐席可以指示队列他们的状态。我们将使用CUT() dialplan函数从来电中获取channel名,这样队列就可以知道哪个channel登入了队列。

我们已经建立了一个dialplan来演示登入及登出队列,以及暂停和恢复队列成员的的简单过程。我们仅对我们之前在queues.conf文件中定义的support队列操作。

AddQueueMember(),RemoveQueueMember(),PauseQueueMember(),以及UnpauseQueueMember()应用程序设置的channel变量将在队列成员完成操作后通过Playback()播放出来,以通知队列成员让他们知道他们是否成功的执行了登入/登出,或者暂停/恢复操作。

[QueueMemberFunctions]

exten => *54,1,Verbose(2,Logging In Queue Member)

same => n,Set(MemberChannel=${CHANNEL(channeltype)}/${CHANNEL(peername)})

same => n,AddQueueMember(support,${MemberChannel})

; ${AQMSTATUS}

; ADDED

; MEMBERALREADY

; NOSUCHQUEUE

exten => *56,1,Verbose(2,Logging Out Queue Member)

same => n,Set(MemberChannel=${CHANNEL(channeltype)}/${CHANNEL(peername)})

same => n,RemoveQueueMember(support,${MemberChannel})

; ${RQMSTATUS}:

; REMOVED

; NOTINQUEUE

; NOSUCHQUEUE

exten => *72,1,Verbose(2,Pause Queue Member)

same => n,Set(MemberChannel=${CHANNEL(channeltype)}/${CHANNEL(peername)})

same => n,PauseQueueMember(support,${MemberChannel})

; ${PQMSTATUS}:

; PAUSED

; NOTFOUND

exten => *87,1,Verbose(2,Unpause Queue Member)

same => n,Set(MemberChannel=${CHANNEL(channeltype)}/${CHANNEL(peername)})

same => n,UnpauseQueueMember(support,${MemberChannel})

; ${UPQMSTATUS}:

; UNPAUSED

; NOTFOUND

自动登录和退出多个队列[编辑]

一个坐席人员属于多个队列的情况是非常常见的。与其为登录不同队列配置多个独立的extension(或者询问坐席人员他们希望登录哪个队列),不如利用Asterisk数据库(astdb)来为每个坐席存储他所属于的队列信息,然后由Asterisk依次将坐席自动登录到他们所属的队列。

为了让这段代码能工作,需要通过Asterisk CLI对AstDB进行类似下面例子中的操作。例如,下面的例子将存储成员0000FFFF0001属于support和sales两个队列的信息:

*CLI> database put queue_agent 0000FFFF0001/available_queues support^sales

下面的dialplan代码用于示例如何将队列成员自动加入到support和sales队列中。我们已经定义了一个例程,该例程用于建立了三个channel变量(MemberChannel,MemberChanType,AvailableQueues)。然后这些channel变量被用于登入(*54),登出(*56),暂停(*72),和恢复(*87)。这些extensions总的每个都使用subSetupAvailableQueies例程来设置这些channel变量和验证AstDB中包含的队列列表:

[subSetupAvailableQueues]

;

; This subroutine is used by the various login/logout/pausing/unpausing routines

; in the [ACD] context. The purpose of the subroutine is centralize the retrieval

; of information easier.

;

exten => start,1,Verbose(2,Checking for available queues)

; Get the current channel's peer name (0000FFFF0001)

same => n,Set(MemberChannel=${CHANNEL(peername)})

; Get the current channel's technology type (SIP, IAX, etc)

same => n,Set(MemberChanType=${CHANNEL(channeltype)})

; Get the list of queues available for this agent

same => n,Set(AvailableQueues=${DB(queue_agent/${MemberChannel}/available_queues)})

; if there are no queues assigned to this agent we'll handle it in the

; no_queues_available extension

same => n,GotoIf($[${ISNULL(${AvailableQueues})}]?no_queues_available,1)

same => n,Return()

exten => no_queues_available,1,Verbose(2,No queues available for agent${MemberChannel})

; playback a message stating the channel has not yet been assigned

same => n,Playback(silence/1&channel&not-yet-assigned)

same => n,Hangup()

[ACD]

;

; Used for logging agents into all configured queues per the AstDB

; ;

; Logging into multiple queues via the AstDB system

exten => *54,1,Verbose(2,Logging into multiple queues per the database values)

; get the available queues for this channel

same => n,GoSub(subSetupAvailableQueues,start,1())

same => n,Set(QueueCounter=1) ; setup a counter variable

; using CUT(), get the first listed queue returned from the AstDB

same => n,Set(WorkingQueue=${CUT(AvailableQueues,^,${QueueCounter})})

; While the WorkingQueue channel variable contains a value, loop

same => n,While($[${EXISTS(${WorkingQueue})}])

; AddQueueMember(queuename[,interface[,penalty[,options[,membername[,stateinterface]]]]])

; Add the channel to a queue, setting the interface for calling

; and the interface for monitoring of device state

;

; *** This should all be on a single line

same => n,AddQueueMember(${WorkingQueue},${MemberChanType}/

${MemberChannel},,,${MemberChanType}/${MemberChannel})

same => n,Set(QueueCounter=$[${QueueCounter} + 1]) ; increase our counter

; get the next available queue; if it is null our loop will end

same => n,Set(WorkingQueue=${CUT(AvailableQueues,^,${QueueCounter})})

same => n,EndWhile()

; let the agent know they were logged in okay

same => n,Playback(silence/1&agent-loginok)

same => n,Hangup()

exten => no_queues_available,1,Verbose(2,No queues available for ${MemberChannel})

same => n,Playback(silence/1&channel&not-yet-assigned)

same => n,Hangup()

; -------------------------

; Used for logging agents out of all configured queues per the AstDB

exten => *56,1,Verbose(2,Logging out of multiple queues)

; Because we reused some code, we've placed the duplicate code into a subroutine

same => n,GoSub(subSetupAvailableQueues,start,1())

same => n,Set(QueueCounter=1)

same => n,Set(WorkingQueue=${CUT(AvailableQueues,^,${QueueCounter})})

same => n,While($[${EXISTS(${WorkingQueue})}])

same => n,RemoveQueueMember(${WorkingQueue},${MemberChanType}/${MemberCha

same => n,Set(QueueCounter=$[${QueueCounter} + 1])

same => n,Set(WorkingQueue=${CUT(AvailableQueues,^,${QueueCounter})})

same => n,EndWhile()

same => n,Playback(silence/1&agent-loggedoff)

same => n,Hangup()

; -------------------------

; Used for pausing agents in all available queues

exten => *72,1,Verbose(2,Pausing member in all queues)

same => n,GoSub(subSetupAvailableQueues,start,1())

; if we don't define a queue, the member is paused in all queues

same => n,PauseQueueMember(,${MemberChanType}/${MemberChannel})

same => n,GotoIf($[${PQMSTATUS} = PAUSED]?agent_paused,1:agent_not_found,1)

exten => agent_paused,1,Verbose(2,Agent paused successfully)

same => n,Playback(silence/1&unavailable)

same => n,Hangup()

; -------------------------

; Used for unpausing agents in all available queues

exten => *87,1,Verbose(2,UnPausing member in all queues)

same => n,GoSub(subSetupAvailableQueues,start,1())

; if we don't define a queue, then the member is unpaused from all queues

same => n,UnPauseQueueMember(,${MemberChanType}/${MemberChannel})

same => n,GotoIf($[${PQMSTATUS} = PAUSED]?agent_unpaused,1:agent_not_found,1)

exten => agent_unpaused,1,Verbose(2,Agent paused successfully)

same => n,Playback(silence/1&available)

same => n,Hangup()

; -------------------------

;Used by both pausing and unpausing dialplan functionality

exten => agent_not_found,1,Verbose(2,Agent was not found)

same => n,Playback(silence/1&cannot-complete-as-dialed)

你可以进一步改善这些登入和登出例程以实现每次用到AddQueueMember()和RemoveQueueMember()时设置AQMSTATUS和RQMSTATUS channel变量。举例来说,你可以建立一个标记,通过设置这个标注来使队列成员了解到他还没有加入队列,或者增加录音或文本装语音系统来播放出现问题的队列信息。或者,如果你通过Asterisk Manager Interface来实现监控的话,你可以使用一个弹屏,或者使用JobberSend()通过一个短信息通知队列成员。

设备状态简介[编辑]

在Asterisk中的设备状态用于通知多个应用程序关于你的设备当前是否正在使用中。这对于队列来说特别重要,因为我们不会希望将呼叫转接给一个已经在接听电话的坐席。设备状态是由channel模块控制的,而且在Asterisk中只有chan_sip能做出合适的处理。当队列查询设备状态时,它首先询问channel驱动(例如,chan_sip)。如果某个channel不能直接提供设备状态信息(例如chan_iax2),队列查询Asterisk内核来确定设备状态,这通过搜索当前在处理的所有channels来实现。

不幸地是,简单的要求内核去搜索活跃的channels是不精确的,所以对于队列来说,通过chan_sip以外的方法来获得设备状态的方法可靠性要差一些。我们将在本章的“高级队列”一节探索一些在其它类型channels上控制呼叫的方法,但是现在我们将集中讨论SIPchannels,它不需要复杂的设备状态要求。关于设备状态的更进一步信息,请参阅第十四章。

为了在Asterisk中正确的确定设备的状态,我们需要在sip.conf中使能呼叫计数器(call counters)。通过使能呼叫计数器,我们告诉Asterisk去跟踪设备的活跃呼叫,所以这些信息会被报告给channel模块,从而设备状态可以被精确的反映给队列。首先,让我们看一下没有配置callcounter选项时的情况:

*CLI> queue show support

support has 0 calls (max unlimited) in 'rrmemory' strategy

(0s holdtime, 0s talktime), W:0, C:0, A:0, SL:0.0% within 0s

Members:

SIP/0000FFFF0001 (dynamic) (Not in use) has taken no calls yet

No Callers

现在假设我们的dialplan中配置了分机555调用MusicOnHold()。如果我们在没有使能call counters的情况下拨打这个分机,通过Asterisk CLI查询support队列(SIP/0000FFFF0001是一个成员)将看到类似下面这样的信息:

-- Executing [555@LocalSets:1] MusicOnHold("SIP/0000FFFF0001-00000000",

"") in new stack

-- Started music on hold, class 'default', on SIP/0000FFFF0001-00000000

*CLI> queue show support

support has 0 calls (max unlimited) in 'rrmemory' strategy

(0s holdtime, 0s talktime), W:0, C:0, A:0, SL:0.0% within 0s

Members:

SIP/0000FFFF0001 (dynamic) (Not in use) has taken no calls yet

No Callers

请注意尽管分机555因为正在通话中而应该被标记为In Use,但是当我们查询这个队列状态时它并没有这么显示。这很明显是个问题,因为这个队列会以为这个设备是空闲的,尽管它实际上已经在进行一个通话了。

为了改正这个问题,我们需要在sip.conf文件的[general]部分增加callcounter=yes。我们也可以指定这个配置给某个终端(因为它是一个终端级别的配置选项);然而,这实在是一个你应该配置给所有可能加入一个队列的终端的选项,所以通常最佳的做法是将这个选项配置在[general]部分(也可以配置在一个被所有加入队列的终端引用的模板中)。

像下面这样编辑你的sip.conf文件:

[general]

context=unauthenticated ; default context for incoming calls

allowguest=no ; disable unauthenticated calls

srvlookup=yes ; enabled DNS SRV record lookup on outbound calls

udpbindaddr=0.0.0.0 ; listen for UDP request on all interfaces

tcpenable=no ; disable TCP support

callcounter=yes ; enable device states for SIP devices

然后重载chan_sip模块并再次测试:

*CLI> sip reload

Reloading SIP

== Parsing '/etc/asterisk/sip.conf': == Found

现在,当这个设备处于通话中时,它将显示In Use状态:

== Parsing '/etc/asterisk/sip.conf': == Found

== Using SIP RTP CoS mark 5

-- Executing [555@LocalSets:1] MusicOnHold("SIP/0000FFFF0001-00000001",

"") in new stack

-- Started music on hold, class 'default', on SIP/0000FFFF0001-00000001

*CLI> queue show support

support has 0 calls (max unlimited) in 'rrmemory' strategy

(0s holdtime, 0s talktime), W:0, C:0, A:0, SL:0.0% within 0s

Members:

SIP/0000FFFF0001 (dynamic) (In use) has taken no calls yet

No Callers

简单的说,Queue()需要知道设备的状态以恰当的管理呼叫分配。在sip.conf中的callcounter选项是一个正确运行的队列的必要组成部分。

queues.conf 配置文件[编辑]

我们之前已经提到过queues.conf文件,但是这个文件中有太多的选项,我们列表整理如下:

agents.conf配置文件[编辑]

如果你浏览了~/src/asterisk-complete/1.8/configs/目录下的例子,你可能已经注意到agents.conf文件。它看起来似乎是吸引人的,而且它有它的用处,但是总体而言,实现队列的最佳办法是使用sip channels。这有两个原因。第一个原因是SIP channels是唯一能提供真实设备状态信息的channel类型。另一个原因是如果使用agent channel的话agents会一直处于登录状态,因此如果你使用远程agents的话,对带宽的要求可能比你想象的要大。然而,在繁忙的呼叫中心中,强制坐席立即接听电话而不是等待他们按下电话上的应答按钮也许是值得的。

配置文件agents.conf用于定义使用agents channel的坐席成员。这个channel在本质上与Asterisk中的其它channel(local,SIP,IAX2,等)类似,但它更像一个将来电连接到利用其它channel登录到系统中的坐席的伪channel(pseudo-channel)。例如,假设我们使用我们的SIP话机通过AgentLogin() dialplan应用程序登录到Asterisk。一旦登入,这个channel将在整个登录期间保持在线状态,然后呼叫通过agent channel转接给它。

下面让我们来看一下agents.conf中的不同选项,以更好的理解它的作用。Table 13-3展示了agents.conf中[general]部分的唯一一个选项。Table13-4展示了[agents]部分的有效选项。

高级队列[编辑]

在本节我们将讨论一些精细的队列控制,例如控制通知信息播放的选项,以及控制呼应应当何时进入(或离开)队列的选项。我们也将讨论惩罚值和优先级,探索如何控制队列中的坐席以实现让一组坐席优先应答电话以及根据来电在队列中的等待时间动态调整组成员的多少。最后,我们将讨论Local channels作为队列成员的情况,这将是我们可以在将呼叫转接给坐席之前执行dialplan功能。

优先队列(队列权重)[编辑]

有时候你需要将某些呼叫放入比其它呼叫更高优先级的队列中。比如可能这些呼叫已经在这个队列中等待了一段时间,或者某个坐席在接听后意识到需要将呼叫转接到其它队列中。在这种情况下,为了最小化呼叫者的总体等待时间,最好将这个呼叫转接到一个更高优先级权重的队列中去,这样它将更快被接听。

给队列设置更高的优先级通过weight选项来实现。如果有两个不同权重的队列(例如support和support-priority),同时加入两个队列的坐席将更优先从高优先级队列中获取呼叫。这些坐席在处理完高优先级队列中的呼叫前不会从低优先级队列中获取呼叫。(通常地,应该有一些坐席只被分配给低优先级队列,以保证低优先级队列中的呼叫可以被及时处理)。举例来说,如果我们将队列成员James Shaw分配给support和support-priority队列,support-priority队列中的呼叫将比support队列中的呼叫优先得到James的处理。

让我们看一下如何实现这件工作。首先,我们需要创建两个队列,它们除了weight选项之外完全相同。我们使用模板来保证着两个队列保持一致,即便将来需要修改也是这样:

 [support_template](!)

musicclass=default

strategy=rrmemory

joinempty=no

leavewhenempty=yes

ringinuse=no

[support](support_template)

weight=0

[support-priority](support_template)

weight=10

创建了队列之后(以及随后通过Asterisk控制台命令moudule reload app_queue.so进行了重新加载),我们现在可以创建两个extensions来转接呼叫。这段代码可以放在任何一个你需要放置dialplan逻辑实现转接的地方。我们将使用LocalSets,这是我们之前在我们设备中使能的起始context:

[LocalSets]

include => Queue ; allow direct transfer of calls to queues

[Queues]

exten => 7000,1,Verbose(2,Entering the support queue)

same => n,Queue(support) ; standard support queue available

; at extension 7000

same => n,VoiceMail(7000@queues,u) ; if there are no members in the queue,

; we exit and send the caller to voicemail

same => n,Hangup()

exten => 8000,1,Verbose(2,Entering the priority support queue)

same => n,Queue(support-priority) ; priority queue available at

; extension 8000

same => n,VoiceMail(7000@queues,u) ; if there are no members in the queue,

; we exit and send the caller to voicemail

same => n,Hangup()

现在你已经有了两个不同权重的队列。我们将标准队列配置在extension 7000,而高优先级队列配置在8000。我们可以通过简单的在7XXX和8XXX范围对称的配置来镜像多个队列。举例来说,如果我们配置sales队列在7004,则priority-sales队列可以被放在镜像队列8004,它具有更高的权重。

剩下唯一要做的配置是确保你的部分或全部坐席在两个队列中都添加了。如果你在7XXX队列有更多的来电,你可能希望更多的坐席登录到这个队列,同时一定比例的坐席登录到两个队列。如何正确地配置你的队列取决于具体情况。

队列成员优先级[编辑]

在一个队列中,我们可以通过惩罚(penalize)某个成员以降低他接听电话的优先级。举例来说,当我们希望某些坐席是特定队列的成员,但是又只有当这个队列满了而所有高优先级的坐席又都在忙时才被使用,我们就可以通过惩罚(penalize)队列成员来实现。这意味着我们可以有三个队列(例如,support,sales,和billing),每个队列都包含同样的三个坐席:James Shaw,Kay Madsen,和Danielle Roberts。

假设,无论如何,我们希望James Shaw是support队列的优先联系人,Kay Madsen是sales队列的优先联系人,而Danielle Roberts是billing队列的优先联系人。通过在support队列惩罚Kay Madsen和Danielle Roberts,我们可以保证James Shaw优先获得这个队列的呼叫。同样地,我们可以在sales队列惩罚James Shaw和Danielle Roberts来保证Kay Madsen是优先联系人,以及在billing队列惩罚James Shaw和Kay Madsen来保证Danielle Roberts是优先联系人。

惩罚队列成员可以通过编辑queues.conf文件来实现,如果你静态指定队列成员的话;或者通过AddQueueMember() dialplan应用程序来实现。让我们来看一下如何通过queues.conf在队列中设置静态成员。我们将使用之前在本章定义的StandardQueue模板:

 [support](StandardQueue)

member => SIP/0000FFFF0001,0,James Shaw ; preferred

member => SIP/0000FFFF0002,10,Kay Madsen ; second preferred

member => SIP/0000FFFF0003,20,Danielle Roberts ; least preferred

[sales](StandardQueue)

member => SIP/0000FFFF0002,0,Kay Madsen

member => SIP/0000FFFF0003,10,Danielle Robert

member => SIP/0000FFFF0001,20,James Shaw

[billing](StandardQueue)

member => SIP/0000FFFF0003,0,Danielle Roberts

member => SIP/0000FFFF0001,10,James Shaw

member => SIP/0000FFFF0002,20,Kay Madsen

通过给每个队列成员定义不同的惩罚,可以帮助我们控制呼叫优先分配给谁,同时仍然保证当优先做些不可用时其它队列成员也可以接听。惩罚也可以使用AddQueueMember()来定义,如下面的例子所示:

exten => *54,1,Verbose(2,Logging In Queue Member)

same => n,Set(MemberChannel=${CHANNEL(channeltype)}/${CHANNEL(peername)})

; *CLI> database put queue support/0000FFFF0001/penalty 0

same => n,Set(QueuePenalty=${DB(queue/support/${CHANNEL(peername)}/penalty)})

; *CLI> database put queue support/0000FFFF0001/membername "James Shaw"

same => n,Set(MemberName=${DB(queue/support/${CHANNEL(peername)}/membername)})

; AddQueueMember(queuename[,interface[,penalty[,options[,membername

; [,stateinterface]]]]])

same => n,AddQueueMember(support,${MemberChannel},

${QueuePenalty},,${MemberName})

通过AddQueueMember(),我们展示了在一个队列中如何通过成员名获得惩罚值,以及如何在她登入队列的时候指定这个惩罚值。为了让这种办法在多队列的情况工作,还需要讨论额外的概念;进一步信息请参阅“13.2.3自动登入和登出多个队列”一节。

动态调整惩罚值(queuerules.conf)[编辑]

通过使用queuerules.conf文件,可以指定一些规则来改变QUEUE_MIN_PENALTY和QUEUE_MAX_PENALTY的值。QUEUE_MIN_PENALTY和QUEUE_MAX_PENALTY channel变量用于控制队列中的那些成员可以用于接听电话。假设我们有一个队列命名为support,而且我们有5个队列成员,他们的惩罚值为从1到5。如果在呼叫到达队列前,QUEUE_MIN_PENALTY的值设置为2,而QUEUE_MAX_PENALTY的值设置为4,只有惩罚值被设置在2到4之间的队列成员可以应答电话:

[Queues]

exten => 7000,1,Verbose(2,Entering the support queue)

same => n,Set(QUEUE_MIN_PENALTY=2) ; set minimum member penalty

same => n,Set(QUEUE_MAX_PENALTY=4) ; set maximum member penalty

same => n,Queue(support) ; entering the queue with min and max

; member penalties to be used

更进一步,在来电停留在队列中期间,我们可以动态改变QUEUE_MIN_PENALTY和QUEUE_MAX_PENALTY的值。这可以允许更多或不同的队列成员被使用,具体取决于来电在队列中等待了多久。例如,在前面的例子中,我们可以调整最小惩罚值为1同时最大惩罚值5,如果来电在队列中的等待超过60秒的话。

这些规则在queuerules.conf中定义。为了实现通话过程中的多种不同惩罚值变化,可以创建多条规则。让我们看看如何定义上一段描述的变化:

[more_members]

penaltychange => 60,5,1

如果你修改了queuerules.conf文件,并重载了app_queue.so,新的规则将对到达队列的新呼叫生效,不会影响已经存在的呼叫。

我们已经在queuerules.conf中定义的了规则more_members并且将后面的值赋给了penaltychange:60是改变惩罚值前需要等待的秒数,5是新的QUEUE_MAX_PENALTY值,而1是新的QUEUE_MIN_PENALTY值。对于我们新定义的规则,我们必须重新加载app_queue.so使它生效:

*CLI> module reload app_queue.so

-- Reloading module 'app_queue.so' (True Call Queueing)

== Parsing '/etc/asterisk/queuerules.conf': == Found

我们可以在控制台通过queue show rules进行检查:

*CLI> queue show rules

Rule: more_members

After 60 seconds, adjust QUEUE_MAX_PENALTY to 5 and QUEUE_MIN_PENALTY to 1

随着我们的新规则被载入内存,我们可以修改我们的dialplan来使用它。只需要修改Queue()这一行来包含新规则,就像这样:

[Queues]

exten => 7000,1,Verbose(2,Entering the support queue)

same => n,Set(QUEUE_MIN_PENALTY=2) ; set minimum queue member penalty

same => n,Set(QUEUE_MAX_PENALTY=4) ; set maximum queue member penalty

; Queue(queuename[,options[,URL[,announceoverride[,timeout[,AGI[,macro

; [,gosub[,rule[,position]]]]]]]]])

same => n,Queue(support,,,,,,,,more_members) ; enter queue with minimum and

; maximum member penalties

queuerules.conf文件非常灵活。我们可以使用相对值而不是绝对值来定义我们的规则,从而可以定义多条规则:

[more_members]

penaltychange => 30,+1

penaltychange => 45,,-1

penaltychange => 60,+1

penaltychange => 120,+2

这里,我们修改了more_members规则为使用相对值。30秒后,我们将最大惩罚值加1(如果使用我们的dialplan例子,惩罚值将变为5)。45秒后,我们将最小惩罚值减1,等等。我们可以在Asterisk控制台重新加载module reload app_queue.so之后检查我们的新规则变化:

*CLI> queue show rules

Rule: more_members

After 30 seconds, adjust QUEUE_MAX_PENALTY by 1 and QUEUE_MIN_PENALTY by 0

After 45 seconds, adjust QUEUE_MAX_PENALTY by 0 and QUEUE_MIN_PENALTY by -1

After 60 seconds, adjust QUEUE_MAX_PENALTY by 1 and QUEUE_MIN_PENALTY by 0

After 120 seconds, adjust QUEUE_MAX_PENALTY by 2 and QUEUE_MIN_PENALTY by 0

通知控制[编辑]

Asterisk有能力向队列中等待的客户播放几条通知。例如,你可能想通知呼叫者在队列中的位置,平均等待时间,或者周期性的播放感谢客户等待的信息(或者任意你指定的声音文件)。调整控制何时向来电用户播放这些通知的参数十分重要,因为过于频繁的通知他们的位置,感谢他们等待,或者告诉他们等待时间的话可能会惹恼他们,导致他们或者挂断电话,或者向你的坐席人员发火。

在queues.conf文件中有几个选项用于微调什么内容及何时向你的客户播放通知。完整的queue选项我们在本章前几节已经介绍过了,但是我们仍将在这里复习一下相关的选项。

Table13-5列出了用于控制何时向客户播放通知的选项。

Table13-6显示了当向客户播放通知时会用到哪些文件。

如果这些专用于播放通知的选项是其价值的指示,大概我们最感兴趣的就是如何利用它们发挥最大的潜力。在Table13-5中的选项帮助我们定义何时播放通知,而Table13-6中的选项帮助我们控制我们向客户播放什么。有这两个表在手,让我们看一个队列的例子,在这里我们将定义一些值。我们将从我们的基本队列模板开始:

[general]

autofill=yes

; distribute all waiting callers to available members

shared_lastcall=yes

; respect the wrapup time for members logged into more

; than one queue

[StandardQueue](!)

; template to provide common features

musicclass=default ; play [default] music

strategy=rrmemory ; use the Round Robin Memory strategy

joinempty=yes

; do not join the queue when no members available

leavewhenempty=no

; leave the queue when no members available

ringinuse=no

; don't ring members when already InUse (prevents

; multiple calls to an agent)

[sales](StandardQueue) ; create the sales queue using the parameters in the

; StandardQueue template

[support](StandardQueue)

; create the support queue using the parameters in the

; StandardQueue template

现在修改StandardQueue模板来控制我们的通知:

[StandardQueue](!)

; template to provide common features

musicclass=default ; play [default] music

strategy=rrmemory ; use the Round Robin Memory strategy

joinempty=yes

; do not join the queue when no members available

leavewhenempty=no

; leave the queue when no members available

ringinuse=no ; don't ring members when already InUse (prevents multiple calls to an agent)

; -------- Announcement Control --------

announce-frequency=30

; announces caller's hold time and position every 30

; seconds

min-announce-frequency=30

; minimum amount of time that must pass before the

; caller's position is announced

periodic-announce-frequency=45

; defines how often to play a periodic announcement to caller

random-periodic-announce=no

; defines whether to play periodic announcements in

; a random order, or serially

relative-periodic-announce=yes

; defines whether the timer starts at the end of

; file playback (yes) or the beginning (no)

announce-holdtime=once

; defines whether the estimated hold time should be

; played along with the periodic announcement

announce-position=limit

; defines if we should announce the caller's position

; in the queue

announce-position-limit=10

; defines the limit value where we announce the

; caller's position (when announce-position is set to limit or more)

announce-round-seconds=30 ; rounds the hold time announcement to the nearest 30-second value

让我们描述一下刚才我们在StandardQueue模板中设置了什么。

我们将每隔30秒(announce-frequency)通知一次呼叫者等待时间和他在队列中的位置,并且保证我们再次通知前的最短时间是30秒(min-announce-frequency)。我们通过这两个参数来限制我们的通知向客户播放的频率,以免惹人厌烦。周期性地,我们播放一条通知给客户感谢他们的耐心等待并向他们保证客服人员将很快接听电话。(这个通知信息通过periodic-announce选项定义。我们使用默认的通知,但是你也可以使用periodic-announce定义一个你自己的一个或多个通知。)

这个周期性的通知每隔45秒(periodic-announce-frequency)播放一次,按照预先定义的顺序播放(random-period-announce)。为了确定periodic-announce-frequency定时器何时启动,我们使用relative-periodic-announce。这个选项设置为yes,意味着定时器会在通知播放停止后启动,而不是在开始播放时启动。把这个选项设置为no的话你将遇到的问题是,如果你的通知很长(例如30秒),这将导致通知每隔15秒播放一次,而不是你打算的每隔45秒播放一次。

向呼叫者通知几次等待时间通过announce-holdtime来控制,我们的例子配置为once。将这个值配置为yes将每次都通知等待时间,而设置为no将禁止通知等待时间。

如何及何时通知呼叫者的估计剩余等待时间通过announce-position控制,我们的例子中设置为limit。设置announce-position为limit将使我们仅仅当呼叫者的位置在announce-position-limit限制之内时才通知客户。因此,在我们的例子中,仅当客户处于队列前10名的位置时我们才向他播放通知。我们也可以将这个选项配置为yes,这样每次周期性播放时都会通知客户在队列中的位置,配置为no则不通知排队位置信息。或者可以将其配置为more,如果我们希望仅当客户的排队位置大于announce-position-limit时才通知他排队位置信息。

我们要介绍的最后一个选项,announce-round-seconds,其作用是控制当通知客户等待时间时对时间值的四舍五入。在我们的例子中,不会播放“1分钟23秒”,时间值会被舍入到最近似的30秒,结果是通知提示音为“1分钟30秒”。

溢出(Overflow)[编辑]

队列溢出将发生在或者等待超时时,或者当没有有效的队列成员时(当定义了joinempty或leavewhenempty时)。在本节中,我们将讨论如何控制何时发生溢出。

控制超时[编辑]

Queue()应用程序支持两种超时:一种是呼叫者在队列中的最大等待时间,另一种是当Asterisk试图将呼叫者转接给某个坐席时的最大振铃时间。我们将讨论呼叫者在这个呼叫溢出到dialplan的其它位置(例如VoiceMail())之前在队列中的最大等待时间。一旦这个呼叫放弃了这个队列,它可以转接到dialplan中任何呼叫可以转接到的位置。

超时被定义在两个地方。队列成员的最大振铃时间定义在queues.conf文件中。绝对超时时间(呼叫者在队列中的最大停留时间)通过Queue()应用程序控制。要设置呼叫者在队列中的最大停留时间,只要简单的在Queue()应用程序中的queue name参数之后指定就可以:

[Queues]

exten => 7000,1,Verbose(2,Joining the support queue for a maximum of 2 minutes)

same => n,Queue(support,120)

same => n,VoiceMail(support@queues,u)

same => n,Hangup()

当然,我们可以定义不同的溢出转接目的地,但是VoiceMail()应用程序已经足够好了。只是需要注意你应当将呼叫者转接到一个经常有人检查并回拨给客户的语音信箱。

现在假设有这么一个场景:我们设置绝对超时时间为10秒,队列成员的振铃超时时间为5秒,重拨坐席的超时时间为4秒。在这个场景下,我们将振铃队列成员5秒,然后在尝试拨打另一个队列成员前等待4秒。这些将用去9秒,而绝对超时时间是10秒。在这时候,我们应该振铃下一个队列成员1秒然后退出队列,还是应该在退出队列前完整的振铃下一个队列成员5秒?

我们通过queues.conf文件中的timeoutpriority选项控制这种情况。这个选项的有效取值为app和conf。如果我们希望应用程序超时(绝对超时)优先,这将导致呼叫者精确地在10秒后退出队列,我们应该设置timeoutpriority的值为app。如果我们希望配置文件超时优先从而完成振铃一个队列成员——这将导致呼叫者在队列中的停留时间稍长,我们将设置timeoutpriority的值为conf。这个选项的默认值是app(这是旧版本Asterisk的默认行为)。

控制何时加入和离开队列[编辑]

Asterisk提供了两个选项控制呼叫者根据队列成员的不同状态何时可以加入及何时被强制移出队列。第一个选项,joinempty,用于控制是否允许呼叫者加入队列。而leavewhenempty选项用于控制何时将已经在队列中的呼叫者强制删除(例如,当所有的队列成员都无效时)。

这两个选项都有一系列用逗号隔开的取值来控制它们的行为。参见Table13-7。

对于joinempty选项来说,在将呼叫者放入队列前,所有的队列成员都将根据你配置的参数作为标准检查其有效性。如果所有的队列成员都被认为是无效的,呼叫者将不被允许加入队列,dialplan将执行下一条。对于leavewhempty选项来说,将根据你配置的条件周期性的检查队列成员状态;如果判定已经没有有效成员可以接听电话,呼叫者会被移出这个队列, dialplan将继续执行下一条。

举一个使用joinempty的例子:

joinempty=paused,inuse,invalid

在这个配置下,在呼叫者进入队列之前将会检查所有队列成员的状态,并且呼叫者不会被允许加入队列,除非至少有一个成员的状态不是paused,inuse,或invalid。

再举一个leaveehempty的例子:

leavewhenempty=inuse,ringing

在这个例子中,队列成员的状态被周期性的检查,而呼叫者将被移出队列,如果不能找到某个队列成员的状态既不是inuse也不是ringing。

早期的Asterisk版本使用yes,no,strict,以及loose作为这两个选项的有效取值。新旧取值之间的对照关系参见Table13-8。

使用Local Channels[编辑]

使用Local channels作为队列成员是一种在实际拨打坐席之前执行部分dialplan逻辑或执行检查的流行方式。举例来说,它将允许我们实现诸如启动录音,设置channel变量,写入日志文件,设置呼叫时长(比如这是个付费服务),或者做任何我们一旦知道转接目的地之后希望做的事。

当在队列中使用Local channels时,它们可以像任何其它channels一样加入队列。在queues.conf文件中,增加Local channel的做法如下:

; queues.conf

[support](StandardQueue)

member => Local/SIP-0000FFFF0001@MemberConnector

; pass the technology to dial

; over and the device

; identifier, separated by a

; hyphen. We'll break it apart

; inside the MemberConnector

; context.

请注意我们是如何将你要拨打设备的技术类型和设备标示符传递给MemberConnector context的。我们简单的使用连字符(尽管我们可以使用差不多任意字符作为分隔符)作为字段标记。我们将在MemberConnector context中使用CUT()函数并分配第一个字段(SIP)给一个channel变量,分配第二个字段(0000FFFF0001)给另一个channel变量,然后我们将使用这两个channel变量呼叫终端。

传递信息,然后在context中被Local channel“炸开”的做法是一种常用且有用的技术(类似于PHP中的explode()函数)。

当然,我们需要MemberConnector context实际连接呼叫者到坐席:

[MemberConnector]

exten => _[A-Za-z0-9].,1,Verbose(2,Connect ${CALLERID(all)} to Agent at ${EXTEN})

; filter out any bad characters, allow alphanumeric chars and hyphen

same => n,Set(QueueMember=${FILTER(A-Za-z0-9\-,${EXTEN})})

; assign the first field of QueueMember to Technology; hyphen as separator

same => n,Set(Technology=${CUT(QueueMember,-,1)})

; assign the second field of QueueMember to Device using the hyphen separator

same => n,Set(Device=${CUT(QueueMember,-,2)})

; dial the agent

same => n,Dial(${Technology}/${Device})

same => n,Hangup()

这样,你就将队列成员传递给了context,然后我们就可以拨打这个设备。然而,因为我们使用Local channel作为queue member,Queue()不需要知道呼叫的状态,特别是当Localchannel优化过之后(请参考https://wiki.asterisk.org/wiki/display/AST/Local+Channel+Modifiers获取关于/n修饰符的更多信息,这将导致Local channel不做优化)。队列将监视Localchannel的状态,而不是我们实际要监视的设备的状态。

幸运地,我们可以让Queue()监视实际的设备并将其状态与Local channel关联起来,所以Local channel的状态总是反应了我们最终拨打的设备的状态。我们的队列成员将在queues.conf中修改如下:

; queues.conf

[support](StandardQueue)

member => Local/SIP-0000FFFF0001@MemberConnector,,,SIP/0000FFFF0001

只有SIP channels有能力可靠地发回状态信息,所以我们强烈推荐你在使用Local channels作为队列成员时只使用SIP channels。

你也可以使用AddQueueMember()和RemoveQueueMember()应用程序来增加或删除队列成员,就像处理任何其它channel一样。AddQueueMember()也具备设置状态接口(state interface)的能力,就像我们在queues.conf中静态定义的那样。举例说明如下:

 [QueueMemberLogin]

exten => 500,1,Verbose(2,Logging in device ${CHANNEL(peername)}

into the support queue)

; Save the device's technology to the MemberTech channel variable

same => n,Set(MemberTech=${CHANNEL(channeltype)})

; Save the device's identifier to the MemberIdent channel variable

same => n,Set(MemberIdent=${CHANNEL(peername)})

; Build up the interface name from the channel type and peer name

; and assign it to the Interface channel variable

same => n,Set(Interface=${MemberTech}/${MemberIdent})

; Add the member to the support queue using a Local channel.

; We're using the same format as before, separating the

; technology and the device indentifier witha hyphen and

; passing that information to the MemberConnector context.

; We then use the IF() function to determine if the member's

; technology is SIP and, if so, to pass back the contents of

; the Interface channel variable as the value to the state

; interface field of the AddQueueMember() application.

;

; When debugging, you might want to hard code

; an AddQueueMember with a single interface specified, to

; get a feel for the syntax, such as:

; ***This line should not have any line breaks

same => n,AddQueueMember(support,Local/SIP-0000FFFF0001

@MemberConnector,,,SIP/0000FFFF0001)

; your dialplan, however, should be coded to handle all

; channels and members, like this:

; *** This line should not have any line breaks

same => n,AddQueueMember(support,Local/${MemberTech}-${MemberIdent}

@MemberConnector,,,${IF($[${MemberTech} = SIP]?${Interface})})

same => n,Playback(silence/1)

; Play back either the agent-loginok or agent-incorrect file, depending on

; what the AQMSTATUS variable is set to.

same => n,Playback(${IF($[${AQMSTATUS} = ADDED]?agent-loginok:agent-incorrect)})

same => n,Hangup()

现在我们可以使用Local channels向队列中增加设备了,让我们看一下如何控制若干呼叫向多于一线的非SIP channels或设备发起的情况。我们可以用GROUP()和GROUP_COUNT()函数跟踪发向一个终端的呼叫计数。我们可以修改MemberConnector context来实现这一点:

[MemberConnector]

exten => _[A-Za-z0-9].,1,Verbose(2,Connect ${CALLERID(all)} to ${EXTEN})

; filter out any bad characters, allowing alphanumeric and hyphen

same => n,Set(QueueMember=${FILTER(A-Za-z0-9\-,${EXTEN})})

; assign the first field to Technology using hyphen as separator

same => n,Set(Technology=${CUT(QueueMember,-,1)})

; assign the second field of to Device using hyphen as separator

same => n,Set(Device=${CUT(QueueMember,-,2)})

; Increase value of group inside queue_members category by one

same => n,Set(GROUP(queue_members)=${Technology}-${Device})

; Check if the group@category greater than 1, and return Congestion()

; (too many channels)

;

; *** This line should not have any line breaks

same => n,ExecIf($[${GROUP_COUNT(${Technology}-${Device}@queue_members)} > 1]

?Congestion())

; dial the agent

same => n,Dial(${Technology}/${Device})

same => n,Hangup()

执行Congestion()返回会导致呼叫者返回队列(虽然这发生了,但呼叫者不会得到任何错误提示,并继续听到等待音乐直到我们将他实际连接到一个坐席)。虽然这不是理想的情况,因为队列将不断尝试连接(或者至少将其包含到坐席循环中,这取决于你有多少个队列成员和他们的状态),但它比一个坐席同时得到多个呼叫要好。

我们也可以用这种方法来创建一种预定过程。如果你希望直接呼叫某个坐席(例如,呼叫者需要联系特定的坐席),你可以通过使用GROUP()和GROUP_COUNT()函数触发暂停这个坐席直到这个呼叫者被连接的方式来预定这个坐席(译者注:这是指采用上面例子中ExecIf类似的语法来实现)。这对于在连接呼叫者到某个坐席之前需要播放一些通知,而你又不希望这个坐席在通知播放的过程中被连接到其它呼叫者的情况特别有用。

队列统计: queue_log文件[编辑]

在/var/log/asterisk目录下的queue_log文件包含着关于你系统中定义的队列的信息(队列何时加载的,队列成员何时加入和删除的,等等),以及进入这个队列的呼叫的信息(例如,它们的状态和呼叫者连接的channel类型)。队列日志默认是打开的,但是可以通过logger.conf文件来控制。与queue_log文件相关的选项有三个:

queue_log

Controls whether the queue log is enabled or not. Valid values are yes or no (defaults to yes).

queue_log_to_file

Controls whether the queue log should be written to a file even when a real time backend is present. Valid values are yes or no (defaults to no).

queue_log_name

Controls the name of the queue log. The default is queue_log.

队列日志是用竖线符号分隔的一系列事件。queue_log文件的字段如下:

• Epoch timestamp of the event

• Unique ID of the call

• Name of the queue

• Name of bridged channel

• Type of event

• Zero or more event parameters

包含在事件参数中的信息取决于事件类型。举例queue_log文件如下:

1292281046|psy1-1292281041.87|7100|NONE|ENTERQUEUE||4165551212|1

1292281046|psy1-1292281041.87|7100|Local/9996@MemberConnector|RINGNOANSWER|0

1292281048|psy1-1292281041.87|7100|Local/9990@MemberConnector|CONNECT|2

|psy1-1292281046.90|0

1292284121|psy1-1292281041.87|7100|Local/9990@MemberCo|COMPLETECALLER|2|3073|1

1292284222|MANAGER|7100|Local/9990@MemberConnector|REMOVEMEMBER|

1292284222|MANAGER|7200|Local/9990@MemberConnector|REMOVEMEMBER|

1292284491|MANAGER|7100|Local/9990@MemberConnector|ADDMEMBER|

1292284491|MANAGER|7200|Local/9990@MemberConnector|ADDMEMBER|

1292284519|psy1-1292284515.93|7100|NONE|ENTERQUEUE||4165551212|1

1292284519|psy1-1292284515.93|7100|Local/9996@MemberConnector|RINGNOANSWER|0

1292284521|psy1-1292284515.93|7100|Local/9990@MemberConnector|CONNECT|2

|psy1-1292284519.96|0

1292284552|MANAGER|7100|Local/9990@MemberConnector|REMOVEMEMBER|

1292284552|MANAGER|7200|Local/9990@MemberConnector|REMOVEMEMBER|

1292284562|psy1-1292284515.93|7100|Local/9990@MemberCo|COMPLETECALLER|2|41|1

如同我们在这个例子中见到的,对每个事件并不总是有唯一ID。在某些情况下,例如Asterisk Manager Interface(AMI)这样的外部服务程序,会对队列执行操作;在这种情况下,你会在唯一ID字段看到类似MANAGER这样的字符。

Table13-9描述了有效事件及它们提供的信息。

结论[编辑]

本章我们从介绍基本呼叫队列开始,讨论了它们是什么,它们怎么工作,以及何时你会用到它们。在创建了一个简单队列之后,我们探索了如何通过不同的方法控制队列成员(包括使用Local channels,它提供了在转接给队列成员之前执行一些dialplan逻辑的能力)。我们也探索了在queues.conf,agents.conf,和queuerules.conf文件中的所有选项,它们为我们提供了精细控制队列的能力。当然,我们还需要监视我们的队列在做什么,所以最后我们介绍了队列日志,以及当不同情况发生时所记录的各种事件和事件参数。

根据本章提供的知识,你应该能够很好的以你的方式为你们公司实现一组成功的队列。

注释:

注1.一个常见的误解是认为排队机制可以让你处理更多的呼叫。严格的说,这是不对的,你的客户仍然希望和服务人员通话,他们只是愿意多等一会。换句话说,如果你人手短缺,排队机制将没有意义,它会变得更像你为客户制造的障碍。一个理想的排队机制最好是客户根本感觉不到,因为他们总是立即得到应答而不会进入呼叫保留状态。

注2.市面上有几本书是讨论呼叫中心指标和有效的排队策略的,例如James C.Abbott的The Executive Guide to Call Center Metrics ( Robert Houston Smith).

注3.结束时间(Wrapup time)是为了给坐席人员提供一段时间在完成接听后处理做记录或其它相关工作。它提供了几秒的宽限期给坐席人员在接听下一个电话前处理这些任务。

注4.呼叫者的位置和等待时间仅在队列中有多于一人等待时才通知。

注5.如果Queue()的下一条dialplan语句没有定义,则这个呼叫将被挂断。