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語句沒有定義,則這個呼叫將被掛斷。