跳至內容

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在大型、繁忙的佇列應用中會導致瓶頸)。除非你有特別的向下相容; 向后兼容=>zh-mo:向下相容的需求,這個選項應當永遠被設為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是因為等待在一條無人處理的線路上是毫無意義的。

從商業的觀點來看,你應該告訴你的坐席人員在當天退出; 登出=>zh-mo:退出系統前要處理完佇列中所有的呼叫。如果你發現當一天快要結束時有太多的呼叫在佇列中,你可能希望考慮延長某些坐席人員的工作時間以處理他們。否則,當他們第二天帶着情緒重新打來時,只會給你帶來更大的壓力。

另一個選擇是使用GotoIfTime()在臨近下班時間時將呼叫轉接到語音信箱,或者dialplan中的其它合適地點。

最後,我們將ringinuse設定為no,它告訴Asterisk不要振鈴已經處於振鈴狀態的坐席。將ringinuse設定為no的目的是避免多個呼叫被轉接到來自一個或多個佇列的同一個坐席。

你可能注意到joinempty和leavewhenempty都是尋找或者佇列中已經沒有登錄的坐席,或者所有的坐席都失效。處於振鈴(Ringing)或使用中(InUse)狀態的坐席不被認為是失效的,所以當joinempty-no和/或leavewhenempty=yes時不會阻止呼叫加入佇列或導致他們離開佇列

一旦你完成了你的queues.conf檔案的組態,你可以儲存; 保存=>zh-mo:儲存它並透過Asterisk CLI多載app_queue.so模組:

$ asterisk -r

*CLI> module reload app_queue.so

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

然後檢查一下你的佇列是否裝載到記憶體; 内存=>zh-mo:記憶體了:

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章,我們將定義存取這些佇列的選單項。儲存; 保存=>zh-mo:儲存這些變化到你的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邏輯使得坐席人員可以登錄或退出; 登出=>zh-mo:退出佇列,以及在他們登錄的佇列中暫停或恢復他們自己。

透過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檔案中定義佇列成員

[編輯]

用撥號計劃邏輯控制佇列成員

[編輯]

在由坐席人員組成的呼叫中心中,最常見的做法是讓坐席人員自己在上班和下班時登錄及退出; 登出=>zh-mo:退出佇列(或者當他們去吃飯,去洗手間,或因為其它原因不能接聽電話時退出; 登出=>zh-mo:退出佇列)。要做到這一點,我們需要使用下述dialplan應用程式:

  • AddQueueMember()
  • RemoveQueueMember()

當登錄佇列之後,還可能出現坐席人員臨時不能接聽電話的情況。下面的應用程式用於這個目的:

  • PauseQueueMember()
  • UnpauseQueueMember()

也許這樣理解這些應用程式更加容易:加入/離開應用程式用於登錄和退出; 登出=>zh-mo:退出,而暫停/恢復用於坐席短時間不可用。區別是非常簡單的,暫停/恢復是在沒有退出; 登出=>zh-mo:退出佇列的情況下將成員狀態設定為不可用和可用。這對於生成報表十分有用(如果一個成員是暫停的,佇列管理員會看到她已經登錄到佇列,但只是暫時不能接聽電話)。如果你不能確定應該用哪個運管用程式,我們推薦你在任何情況下都使用加入/離開。

使用暫停(Pause)和恢復(Unpause)

使用暫停和恢復大概算一種偏好。在某些環境下,這組選項會被用於在上班時間導致坐席不可用的任何活動(例如午飯時間或進行一些與接聽電話無關的工作)。然而,在大部分呼叫中心,如果一個坐席人員不在她的電話旁準備接聽電話的時候,她根本就不應當登錄系統,即使她只是離開她的座位幾分鐘(例如上洗手間)。

有些主管喜歡利用加入/離開和暫停/恢復組態作為某種打卡鐘,這樣他們可以跟蹤他的職員何時到達開始工作和何時結束工作離開,以及他們在座位上待了多長時間和休息了多長時間。我們並不認為這是一種合理的實踐,因為這些應用程式的目的是通知佇列坐席的可用狀態,而不是跟蹤員工的活動。

在這裏要非常重視的一件事是queues.conf中joinempty的設定,我們之前討論過這個選項。如果坐席是暫停的,她仍舊被認為是登錄到佇列中的。讓我們假設接近下班的時候,某個坐席在幾個小時前將自己設定為暫停狀態。所有其他的坐席都退出; 登出=>zh-mo:退出系統並回家了。這時來了個電話。佇列會注意到仍舊有一個坐席登錄在佇列中,然後就將這個來電加入到佇列,儘管事實上此時已經沒有人在處理這個佇列了。結果是這個來電會被永遠保留在這個無人處理的佇列中。

簡而言之,沒有坐在座位旁邊準備接聽電話的坐席應該退出; 登出=>zh-mo:退出系統。暫停/恢復應該僅僅用於短暫的不可用狀態(如果是真的的話)。如果你希望利用你的電話系統作為打卡鐘,在Asterisk中有很多更好的辦法,但是佇列成員應用程式絕對不是我們推薦的方式。

讓我們建立一些簡單的dialplan邏輯來讓我們的坐席可以指示佇列他們的狀態。我們將使用CUT() dialplan函數從來電中取得channel名,這樣佇列就可以知道哪個channel登錄了佇列。

我們已經建立了一個dialplan來演示登錄及退出; 登出=>zh-mo:退出佇列,以及暫停和恢復佇列成員的簡單過程。我們僅對我們之前在queues.conf檔案中定義的support佇列操作。

AddQueueMember(),RemoveQueueMember(),PauseQueueMember(),以及UnpauseQueueMember()應用程式設定的channel變數將在佇列成員完成操作後透過Playback()播放出來,以通知佇列成員讓他們知道他們是否成功的執行了登錄/退出; 登出=>zh-mo:退出,或者暫停/恢復操作。

[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),退出; 登出=>zh-mo:退出(*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)

你可以進一步改善這些登錄和退出; 登出=>zh-mo:退出常式以實現每次用到AddQueueMember()和RemoveQueueMember()時設定AQMSTATUS和RQMSTATUS channel變數。舉例來說,你可以建立一個標記,透過設定這個標註來使佇列成員了解到他還沒有加入佇列,或者增加錄音或文字; 文本=>zh-mo:文字裝語音系統來播放出現問題的佇列訊息。或者,如果你透過Asterisk Manager Interface來實現監控的話,你可以使用一個彈屏,或者使用JobberSend()透過一個短訊; 短信息=>zh-mo:短訊通知佇列成員。

裝置狀態簡介

[編輯]

在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自動登錄和退出; 登出=>zh-mo:退出多個佇列」一節。

動態調整懲罰值(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

隨着我們的新規則被載入記憶體; 内存=>zh-mo:記憶體,我們可以修改我們的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最佳化; 优化=>zh-mo:最佳化過之後(請參考https://wiki.asterisk.org/wiki/display/AST/Local+Channel+Modifiers获取关于/n修饰符的更多信息,这将导致Local channel不做最佳化; 优化=>zh-mo:最佳化)。佇列將監視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陳述式沒有定義,則這個呼叫將被掛斷。