asterisk-book/glava-12.md

123 KiB
Raw Permalink Blame History

Глава 12. Очереди автоматического распределения вызовов

Англичанин, даже если он один, формирует упорядоченную очередь из одного человека.

-- Джордж Майкс

Автоматическое распределение вызовов (ACD), или организация очереди вызовов позволяет УАТС ставить в очередь входящие вызовы от нескольких пользователей. Оно объединяет несколько вызовов в шаблон удержания, присваивает каждому вызову рейтинг и определяет порядок, в котором этот вызов должен быть доставлен доступному оператору (как правило, в порядке очереди). Когда агент становится доступным, вызывающий абонент с самым высоким рейтингом в очереди доставляется этому агенту, а все остальные повышаются в рейтинге.

Если вы когда-либо звонили в организацию и слышали, что «все наши операторы заняты», вы испытали ACD. Преимущество ACD для вызывающих абонентов в том, что им не нужно продолжать набирать номер в попытке связаться с кем-то, а для организаций преимущества заключаются в том, что они могут лучше обслуживать своих клиентов и решать проблемы, когда звонящих больше, чем агентов.1

Существует два типа колл-центров: входящие и исходящие. ACD относится к технологии, которая обрабатывает входящие вызовы, тогда как термин Dialer (или Predictive Dialer) относится к технологии, которая обрабатывает центры обработки исходящих вызовов. В этой книге мы прежде всего сосредоточимся на входящих звонках.

Мы все были разочарованы плохо спроектированными и управляемыми очередями: длительное удержание, радио вместо мелодии, ошеломляющее время ожидания и бессмысленные сообщения, которые каждые 20 секунд сообщают вам, насколько важен ваш звонок, несмотря на то, что вы ждали 30 минут и прослушали это сообщение так много раз, что можете процитировать его по памяти. С точки зрения обслуживания клиентов - проектирование очереди может быть одним из наиболее важных аспектов вашей телефонной системы. Как и в случае с автосекретарем - прежде всего следует помнить, что ваши абоненты не заинтересованы в том, чтобы ожидать в очереди. Они позвонили потому что хотят поговорить с Вами. Все ваши проектные решения должны помнить об этом важном факте: люди хотят общаться с другими людьми, а не с Вашей телефонной системой.2

Цель этой главы - научить вас создавать и проектировать очереди, доставляющие абонентов по назначению максимально быстро и безболезненно.

В этой главе мы можем переключаться между использованием терминов участники очереди и агенты. Так как мы не собираемся тратить много времени на модуль Asterisk с именем chan_agent (используя AgentLogin()), нам нужно прояснить, что в этой книге, когда мы используем термин agent - имеется в виду конечный пользователь - человек, а не канальная технология в Asterisk с именем chan_agent. Читайте дальше, и это обретёт больше смысла.

Создание простой очереди ACD

Для начала мы собираемся создать простую очередь ACD. Она будет принимать звонящих и пытаться доставить их участнику очереди.

В Asterisk термин участник относится к каналу (обычно одноранговому узлу SIP), назначенному очереди, который можно набрать, например, SIP/0000FFFF0001. Агент технически относится к каналу агента, также используемому для набора конечных точек. К сожалению, канал агента является устаревшей технологией в Asterisk, так как он ограничен в гибкости и может вызвать непредвиденные проблемы, которые трудно диагностировать и разрешать. Мы не будем охватывать использование chan_agent, поэтому имейте в виду, что мы будем использовать термин member(участник) для обозначения телефонного устройства и agent (агент) для обозначения лица, обрабатывающего вызов. Поскольку один из них, как правило, не эффективен без другого, любой термин может относиться к обоим.

Мы создадим очередь(и) в файле queues.conf и добавим в нее участников через консоль Asterisk. В разделе “Участники Очереди” мы рассмотрим, как создать диалплан, позволяющий нам динамически добавлять и удалять участников очереди (а также приостанавливать и возобновлять их).

Первым шагом является создание пустого файла agents.conf в вашем каталоге конфигурации /etc/asterisk. Мы не будем использовать или редактировать этот файл, но модуль app_queue ожидает его нахождения и не будет загружаться, если файл не существует:

$ cd /etc/asterisk
$ sudo -u asterisk touch agents.conf

Поскольку мы еще не сделали этого - мы также собираемся настроить базовую музыку для режима ожидания (MOH), используя файл примера:

$ sudo cp ~/src/asterisk-16.*/configs/samples/musiconhold.conf.sample /etc/asterisk/musiconhold.conf

$ sudo chown asterisk:asterisk /etc/asterisk/musiconhold.conf

Затем вам нужно создать файл queues.conf, но мы не будем его редактировать, потому что мы будем создавать наши очереди в базе данных (файл просто должен быть там):

$ sudo touch -u asterisk queues.conf

Далее мы создадим несколько очередей в нашей базе данных:

MySQL>; INSERT INTO `asterisk`.`queues`

(name,strategy,joinempty,leavewhenempty,ringinuse,autofill,musiconhold,
monitor_format,monitor_type)

VALUES
'sales','rrmemory','unavailable,invalid,unknown','unavailable,invalid,unknown','no','yes',
'default','wav','MixMonitor'),
('support','rrmemory','unavailable,invalid,unknown','unavailable,invalid,unknown','no',
'yes','default','wav','MixMonitor') ;

Это даст нам две очереди с названиями sales и support. Вы можете называть их как угодно, но мы будем использовать эти имена позже в этой книге, поэтому - если вы используете имена очередей, отличные от этих - запомните или запишите ваши названия для дальнейшего использования.

Мы также определили параметры очередей, перечисленные в Таблице 12-1.

Таблица 12-1. Примерные параметры очереди

Параметр Назначение
strategy=rrmemory Использование стратегии кругового перебора с памятью
joinempty=unavailable,invalid,unknown Не присоединяться к очереди, когда нет доступных участников
leavewhenempty=unavailable,invalid,unknown Покинуть очередь когда нет доступных участников
ringinuse=no Не звонить участникам, когда они уже используются (предотвращает многократные звонки участникам)
autofill=yes Распределить всех ожидающих абонентов среди доступных участников
musiconhold=default Воспроизведение музыки из класса [default] (см. musiconhold.conf)

strategy, которую мы будем использовать - это rrmemory, что означает круговой перебор с памятью. Стратегия rrmemory работает путем чередования агентов в очереди в последовательном порядке, отслеживая, какой агент получил последний вызов и предоставляя следующий вызов следующему агенту. Когда он попадает к последнему агенту - очередь возвращается к началу (при входе агентов они добавляются в конец списка).

Несколько примечаний по стратегиям

ringall

Звонит всем доступным участникам (по умолчанию). Эта стратегия распределения на самом деле не считается ACD. В традиционных терминах телефонии это называется "групповой вызов" (ring group).

leastrecent

Каждый следующий звонок будет получать участник, который в последний раз положил трубку раньше всех остальных. В очереди, где есть много вызовов примерно одинаковой продолжительности, это справедливо. Но это не будет справеливым, если агент был на вызове в течение часа а все его коллеги получили последний звонок 30 минут назад,потому что агент, который закончил последним свой 60-минутный вызов получит следующий звонок.

fewestcalls

Вызывается первый свободный участник, который обработал наименьшее количество вызовов из данной очереди. Это может быть несправедливо, если звонки не всегда имеют одинаковую продолжительность. Агент мог обрабатывать три звонка по 15 минут каждый, а его коллега имел четыре 5-секундных звонка; агент, который обработал три звонка, получит следующий звонок.

random

Звонит случайный интерфейс. Это на самом деле может быть хорошо и в конечном итоге будет очень справедливым с точки зрения равномерного распределения вызовов между агентами.

rrmemory

Обзванивает участников по кругу, запоминается последний участник, ответивший на вызов. Это также может быть справедливым, но не так как random.

linear

Звонит участникам в указанном порядке, всегда начиная с начала списка. Это работает, если у вас есть команда, в которой есть некоторые агенты, которые должны обрабатывать большинство вызовов, и другие агенты, которые должны получать вызовы, только если основные агенты заняты.

wrandom

Звонит случайному участнику, но использует пенальти penalty (ударение на первую букву "е") участников в качестве веса weight. Стоит рассмотреть в очередях с большой нагрузкой cреди агентов.

Мы установили joinempty на no, так как ставить абонентов в очередь, где нет доступных агентов чтобы принимать их звонки, это плохо.

Вы можете установить это значение в yes для удобства тестирования, но мы не рекомендуем запускать его в производство, если вы не используете очередь для какой-либо функции, которая не предназначена для передачи абонентов вашим агентам. Никто не хочет ждать в очереди, которая никуда не ведет.

Опция leavewhenempty используется для управления тем, должны ли абоненты выпадать из приложения Queue() и продолжать работу в диалплане, если ни один из участников не может принимать их вызовы. Мы установили это значение на yes, потому что обычно мы не хотим чтобы абоненты ждали в очереди без зарегистрированных агентов.

С точки зрения бизнеса - вы должны сказать своим агентам, чтобы они завершили все звонки в очереди, прежде чем выходить из системы в течение дня. Если вы обнаружите, что в конце дня в очереди много вызовов - возможно, вы решите продлить чью-то смену, чтобы обслужить их. В противном случае, они просто добавят вам стресса, когда перезвонят на следующий день в худшем настроении.

Вы можете использовать GotoIfTime() ближе к концу дня, чтобы перенаправить абонентов на голосовую почту или другое подходящее место в вашем диалплане, пока ваши агенты завершают все оставшиеся вызовы в очереди.

Мы выставим ringinuse на no, что говорит Asterisk не звонить участникам, когда их устройства уже используются. Целью установки ringinuse в no является предотвращение многократных вызовов одного и того же участника из одной или нескольких очередей.

Следует отметить, что упомянутые joinempty и leftwhenempty ищут либо участников, не вошедших в очередь, либо недоступных. Агенты в состоянии Ringing или InUse не считаются недоступными и поэтому не будут блокировать абонентов от присоединения к очереди и заставлять их отключаться при joinempty=no и/или leftwhenempty=yes.

Опция autofill указывает очереди немедленно распределять всех ожидающих абонентов между всеми доступными участниками. Предыдущие версии Asterisk распределяли только одного абонента за один раз - это означало, что в то время как Asterisk подавал сигнал агенту, все остальные вызовы удерживались (даже если другие агенты были доступны) до тех пор, пока первый абонент в очереди не был подключен к агенту (что, очевидно, приводило к узким местам в тех версиях Asterisk, где использовались занятые очереди). Если у вас нет особой потребности в обратной совместимости, этот параметр всегда должен быть установлен в yes.

Убедитесь, что ваш файл /etc/asterisk/extconfig содержит следующие строки:

queues => odbc,asterisk,queues
queue_members => odbc,asterisk,queue_members

Сохраните и перезагрузите конфигурацию очереди из интерфейса командной строки Asterisk CLI:

*CLI> queues reload

Убедитесь, что ваши очереди были загружены в память (не забудьте убедиться, что файл agents.conf существует:

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

Выходные данные queue show предоставляют различную информацию, в том числе детали, подробно описанные в Таблице 12-2.

Таблица 12-2. Описание вывода queue show

Поле Описание
W: Вес очереди
C: Количество вызовов в очереди
A: Количество звонков, на которые ответил участник
SL: Уровень обслуживания

Теперь, когда вы создали очереди, вам нужно настроить диалплан так, чтобы звонки могли попадать в очередь.

Добавьте следующую логику диалплана в файл extensions.conf (где-нибудь в контексте [sets]):

exten => 610,1,Noop()
 same => n,Progress()
 same => n,Queue(sales)
 same => n,Hangup()

exten => 611,1,Noop()
 same => n,Progress()
 same => n,Queue(support)
 same => n,Hangup()

Сохраните изменения в extensions.conf и перезагрузите диалплан с помощью команды CLI dialplan reload.

Если вы наберете добавочный номер 610 или 611, то получите следующий вывод:

== Setting global variable 'SIPDOMAIN' to '172.29.1.178'
-- Executing [610@sets:1] NoOp("PJSIP/SOFTPHONE_A-00000004", "") in new stack
-- Executing [610@sets:2] Progress("PJSIP/SOFTPHONE_A-00000004", "") in new stack
-- Executing [610@sets:3] Queue("PJSIP/SOFTPHONE_A-00000004", "test") in new stack
   > 0x7facc801ed60 -- Strict RTP learning after remote set to: 172.29.1.166:4022
-- Started music on hold, class 'testmoh', on channel 'PJSIP/SOFTPHONE_A-00000004'
   > 0x7facc801ed60 -- Strict RTP switching to RTP target 172.29.1.166:4022 as source
   > 0x7facc801ed60 -- Strict RTP learning complete - Locking on 172.29.1.166:4022
-- Stopped music on hold on PJSIP/SOFTPHONE_A-00000004
== Spawn extension (sets, 610, 3) exited non-zero on 'PJSIP/SOFTPHONE_A-00000004'

Обратите внимание, что в этот момент вы не присоединитесь к очереди, потому что в очереди нет агентов для ответа на вызов. У нас настроены joinempty=no и leftwhenempty=yes - поэтому вызывающие не будут помещаться в очередь. (Это была бы хорошая возможность поэкспериментировать с опциями joinempty и leftwhenempty в queues.conf, чтобы лучше понять их влияние на очереди).

В следующем разделе мы покажем, как добавлять участников в очередь (а также другие взаимодействия участников с очередью, такие как пауза/отмена паузы).

Участники очереди

Очереди не очень полезны, если кто-то не отвечает на входящие вызовы, поэтому нам нужен метод, позволяющий агентам входить в очереди для ответа на вызовы. Существуют различные способы решения этой задачи, поэтому мы покажем вам, как добавлять участников в очередь как вручную (как администратору через CLI или жестко прописывая в таблице queue_members), так и динамически (в качестве агента через расширение, определенное в диалплане). Мы начнем с метода Asterisk CLI, который позволяет легко добавлять участников в очередь для тестирования с минимальными изменениями диалплана. Далее мы покажем, как вы можете определить участников в таблице queue_members. Наконец, мы покажем вам, как добавить логику диалплана, позволяющую агентам входить в очереди и выходить из них, а также приостанавливать и возобновлять себя в очередях, в которые они вошли (это, вероятно, лучший метод для продакшена).

Управление участниками очереди через CLI

Мы можем добавить участников очереди в любую доступную очередь через команду Asterisk CLI queue add. Формат команды добавления очереди queue add (все в одной строке):

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

channel - это канал, который мы хотим добавить в очередь, например SIP/0000FFFF0003, а имя queue будет что-то вроде support или sales - любое имя очереди, которое существует в /etc/asterisk/queues.conf. Пока мы будем игнорировать вариант c penalty, но обсудим его в разделе «Расширенные очереди» (penalty используется для контроля ранга участника в очереди, что может быть важно для операторов, которые вошли в несколько очередей или имеют разные навыки). Мы можем определить membername чтобы предоставить подробные сведения для механизма регистрации очередей.

Опция state_interface информирует очередь о состоянии устройства, отслеживаемое для этого агента. Детали работы с состояниями устройств обсуждаются в Главе 13. Сходите и проработайте эту главу, а затем вернитесь сюда и продолжайте. Не волнуйтесь - мы подождем.

Теперь, когда вы добавили callcounter=yes в sip.conf (мы будем использовать SIP-каналы во всех остальных наших примерах), давайте посмотрим, как добавлять участников в наши очереди из Asterisk CLI.

Добавление участника очереди в очередь support можно выполнить с помощью команды queue add member:

*CLI> queue add member PJSIP/SOFTPHONE_B to support

Added interface 'PJSIP/SOFTPHON_B' 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%, SL2:0.0% within 0s
    Members:
        PJSIP/SOFTPHONE_B (ringinuse disabled) (dynamic)(Not in use) has taken no calls yet
    No Callers

Чтобы удалить участника очереди, вы должны использовать команду queue remove member:

*CLI> queue remove member PJSIP/SOFTPHONE_B from support

Removed interface PJSIP/SOFTPHONE_B from queue 'support'

Конечно же вы можете снова использовать команду queue show, чтобы убедиться, что ваш участник был удален из очереди:

*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%, SL2:0.0% within 0s
   Members:
      PJSIP/SOFTPHONE_B (ringinuse disabled) (dynamic) (Not in use) has taken no calls yet
   No Callers

Мы также можем приостанавливать и возобновлять участников в очереди из консоли Asterisk, используя команды queue pause member и queue unpause member. Они используют формат, аналогичный предыдущим командам, которые мы использовали:

*CLI> queue pause member PJSIP/SOFTPHONE_B queue support reason Callbacks

paused interface 'PJSIP/SOFTPHONE_B' in queue 'support' for reason 'Callbacks'

*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

*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%, SL2:0.0% within 0s
   Members:
      PJSIP/SOFTPHONE_B (ringinuse disabled) (dynamic) (paused:Callbacks) (Not in use)
has taken no calls yet
   No Callers

Добавляя причину (reason) приостановки работы участника очереди, например время обеда (lunchtime), вы гарантируете, что ваши журналы очереди будут содержать дополнительную информацию, которая может оказаться полезной. Вот как можно возобновить работу участника:

*CLI> queue unpause member PJSIP/SOFTPHONE_B queue support reason FinishedCallBacks

unpaused interface 'PJSIP/SOFTPHONE_B' in queue 'support' for reason 'FinishedCallbacks'

В производственной среде CLI (интерфейс командной строки) не является лучшим способом управления состоянием агентов в очереди. Вместо этого существуют приложения диалплана, которые позволяют агентам информировать очередь об их доступности.

Определение участников очереди в таблице queue_members

Если вы определите участника очереди в таблице базы данных asterisk.queue_members, то он всегда будет зарегистрирован в очереди. Это как правило не очень хорошо, если ваши участники люди, так как люди, как правило, встают и передвигаются.

В каждом определении очереди вы просто определяете участников следующим образом:

MySQL> insert into `asterisk`.`queue_members`
(queue_name,interface,penalty)

VALUES
'hotline','PJSIP/SOME_NON_HUMAN','0');

В типичной очереди (в которой есть группа людей, отвечающих на вызовы), вы обнаружите, что определение участников в таблицу queue_members может навредить. Агенты должны иметь возможность входить и выходить из системы (а не автоматически регистрироваться всякий раз, когда очередь перезагружается). Мы не рекомендуем определять участников в таблице queue_members, если только нет других целей (таких как банк устройств, отвечающих на вызовы, где вы хотите использовать очередь для балансировки нагрузки вызовов в пул устройств или групповой вызов, где все телефоны звонят одновременно, независимо от того, сидит ли кто-нибудь рядом с телефоном).

Управление участниками очереди с помощью логики диалплана

В колл-центре, в котором работают живые агенты, чаще всего агенты сами входят в систему и выходят из нее в начале и в конце своей смены (или когда они идут на обед, в ванную или иным образом недоступны для очереди).

Для этого мы будем использовать следующие приложения диалплана:

  • AddQueueMember()
  • RemoveQueueMember()

При входе в очередь может случиться так, что агенту необходимо перевести себя в состояние, когда он временно недоступен для приема вызовов. Следующие приложения позволят сделать это:

  • PauseQueueMember()
  • UnpauseQueueMember()

Приложения Add/Remove используются для входа и выхода из системы, а Pause/Unpause используются для коротких периодов отсутствия агента. Разница лишь в том, что Pause и Unpause устанавливают элемент как недоступный/доступный (unavailable/available), фактически не удаляя их из очереди. Это бывает полезно для отчетности (если участник приостановлен - администратор очереди может видеть, что он вошел в очередь, но просто недоступен для приема вызовов в данный момент). Если вы не уверены, какой из них использовать, мы рекомендуем агентам использовать Add/Remove, когда они физически не находятся у своего телефона и Pause/Unpause, когда они находятся на своем рабочем месте, но временно недоступны.

Если есть сомнения - будет лучше чтобы ваши агенты выходили из системы.

Использование Пауза и Снять с паузы

В некоторых средах Pause и Unpause используются для всех действий в течение дня, которые делают агента недоступным (например, во время обеденного перерыва и при выполнении работы не связанной с очередью). Однако в большинстве call-центров - если агент не находится рядом с телефоном и не готов принять вызов в данный момент - он вообще не должен входить в систему, даже если будет отсутствовать на рабочем месте в течение нескольких минут. (например, для перерыва в ванной).

Некоторым руководителям нравится использовать настройки Pause/Unpause как своего рода часы для отслеживания когда их сотрудники приходят на работу и уходят в конце дня, а также сколько времени они проводят за своими столами и на перерывах. Это может быть неразумной практикой, так как цель этих приложений - информировать очередь о доступности агента, а отслеживание активности - вторичная функция.

Здесь важно отметить, что параметр joinempty в таблице asterisk.queues был рассмотрен ранее. Если агент приостановлен - он все еще находится в очереди. Предположим, что рабочая смена подходит к концу, а один агент несколько часов назад поставил себя на паузу для работы над проектом. Все остальные агенты вышли из системы и ушли домой. Поступает вызов. Очередь заметит, что агент вошел в очередь, и, следовательно, поставит вызов в очередь, несмотря на то, что в действительности в данное время в этой очереди нет людей, способных ответить на вызов. Этот абонент может в конечном итоге задержаться в очереди без персонала на неопределенный срок.

Короче говоря, агенты, которые не сидят за столами и не планируют принимать звонки в течение следующих нескольких минут, должны выйти из системы. Pause/Unpause следует использовать только для кратковременных моментов недоступности (если это вообще возможно). Если вы хотите использовать свою телефонную систему для учета рабочего времени - есть много отличных способов сделать это с помощью Asterisk, но приложения queue member - это не тот способ, который мы посоветуем.

Давайте создадим простую логику диалплана, которая позволит нашим агентам указывать свою доступность для очереди. Мы собираемся использовать функцию диалплана CUT(), чтобы извлечь имя нашего канала из вызова в систему, чтобы очередь знала какой канал входит в очередь.

Мы создали этот диалплан, чтобы показать простой процесс входа и выхода из очереди, а также изменения приостановленного статуса участника в очереди. Мы делаем это только для одной очереди, которую ранее определили в файле queues.conf. Переменные состояния канала, установленные приложениями AddQueueMember(), RemoveQueueMember(), PauseQeueMember() и UnpauseQeueMember() могут использоваться для воспроизведения Playback() объявлений участникам очереди после выполнения ими определенных функций для сообщения им, успешно ли они совершили вход/выход или паузу/возобновление:

exten => *731,1,Page(${PAGELIST},i,120)
exten => *732,1,Verbose(2,Logging In Queue Member)
   same => n,Set(MemberChannel=${CHANNEL(channeltype)}/${CHANNEL(endpoint)})
   same => n,AddQueueMember(support,${MemberChannel})
   same => n,Verbose(1,${AQMSTATUS}) ; ADDED, MEMBERALREADY, NOSUCHQUEUE
   same => n,Playback(agent-loginok)
   same => n,Hangup()

exten => *733,1,Verbose(2,Logging Out Queue Member)
   same => n,Set(MemberChannel=${CHANNEL(channeltype)}/${CHANNEL(endpoint)})
   same => n,RemoveQueueMember(support,${MemberChannel})
   same => n,Verbose(1,${RQMSTATUS}) ; REMOVED, NOTINQUEUE, NOSUCHQUEUE
   same => n,Playback(agent-loggedoff)
   same => n,Hangup()

exten => *734,1,Verbose(2,Pause Queue Member)
   same => n,Set(MemberChannel=${CHANNEL(channeltype)}/${CHANNEL(endpoint)})
   same => n,PauseQueueMember(support,${MemberChannel})
   same => n,Verbose(1,${PQMSTATUS}) ; PAUSED, NOTFOUND
   same => n,Playback(dictate/paused)
   same => n,Hangup()

exten => *735,1,Verbose(2,Unpause Queue Member)
   same => n,Set(MemberChannel=${CHANNEL(channeltype)}/${CHANNEL(endpoint)})
   same => n,UnpauseQueueMember(support,${MemberChannel})
   same => n,Verbose(1,${UPQMSTATUS}) ; UNPAUSED, NOTFOUND
   same => n,Playback(agent-loginok)
   same => n,Hangup()

exten => *98,1,NoOp(Access voicemail retrieval.)

Автоматический вход и выход из нескольких очередей

Довольно часто агент является участником более чем одной очереди. Вместо того, чтобы иметь отдельное расширение для входа в каждую очередь (или требовать от агентов информацию о том, в какие очереди они хотят войти), этот код использует базу данных Asterisk (astdb) для хранения информации об участии в очереди для каждого агента, а затем циклически проходит через каждую очередь, в которую входят агенты, поочередно регистрируя их в каждой очереди.

Для того, чтобы этот код работал, необходимо добавить запись, аналогичную следующей, в AstDB через CLI Asterisk. Например, будет сохранён элемент SOFTPHONE_A как находящийся в очередях support и sales3

CLI> database put queue_agent SOFTPHONE_A/available_queues support^sales

Вам нужно будет сделать это один раз для каждого агента, независимо от того, в скольких очередях они являются участниками.

Если Вы запросите базу данных Asterisk, то должны получить результат, подобный следующему:

pbx*CLI> database show queue_agent
/queue_agent/SOFTPHONE_A/available_queues : support^sales

Следующий код диалплана является примером того, как разрешить автоматическое добавление этого участника в очереди support и sales. Мы определили подпрограмму, которая используется для настройки трех канальных переменных (Member Channel, Member Chan Type,AvailableQueues). Эти переменные канала затем используют расширения на вход (*736), выход (*737), паузу (*738) и возобновление (*739). Каждое из расширений использует подпрограмму subSetupAvailableQueues чтобы установить эти переменные канала и убедиться, что AstDB содержит список одной или нескольких очередей для устройства, с которого вызывается участник очереди.

В конце вашего файла extensions.conf, куда вы поместили свои подпрограммы, добавьте следующее:

[subSetupAvailableQueues]
; Эта функция используется для различных процедур входа/выхода/паузы/возобновления
; в нашем примере вход в несколько очередей.
;
exten => start,1,Verbose(2,Checking for available queues)
; Получаем имя текущих каналов пира
same => n,Set(MemberChannel=${CHANNEL(endpoint)})
; Получаем тип технологии текущих каналов
same => n,Set(MemberChanType=${CHANNEL(channeltype)})
; Полуаем список доступных очередей для этого агента
same => n,Set(AvailableQueues=${DB(queue_agent/${MemberChannel}/available_queues)})
; если этому агенту не назначены очереди, мы будем обрабатывать их
; в расширении no_queues_available
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})
; воспроизведение сообщения о том, что канал еще не назначен
same => n,Playback(silence/1&channel&not-yet-assigned)
same => n,Hangup()

Далее, в контекст [sets] добавьте следующее:

; Вход в несколько очередей через систему AstDB
exten => *736,1,Verbose(2,Logging into multiple queues per the database values)
; получить доступные очереди для этого канала
same => n,GoSub(subSetupAvailableQueues,start,1())
same => n,Set(QueueCounter=1)   ; установка переменной счетчика
; используя CUT() получить первую очередь из списка, возвращенную из AstDB.
; Обратите внимание, что мы использовали '^' в качестве разделителя.
same => n,Set(WorkingQueue=${CUT(AvailableQueues,^,${QueueCounter})})
; Пока переменная канала WorkingQueue содержит значение, циклично выполняем
same => n,While($[${EXISTS(${WorkingQueue})}])
; AddQueueMember(queuename[,interface[,penalty[,options[,membername
;  [,stateinterface]]]]])
; Добавить канал в очередь, настроить интерфейс для вызова
;  и интерфейс для мониторинга состояния устройства
; *** Это все должно быть в одной строке
same => n,AddQueueMember(
    ${WorkingQueue},${MemberChanType}/${MemberChannel},,,${MemberChanType}/${MemberChannel})
    same => n,Set(QueueCounter=$[${QueueCounter} + 1])    ; увеличивает наш счетчик
; получить следующую доступную очередь; если она равна нулю - завершить цикл
    same => n,Set(WorkingQueue=${CUT(AvailableQueues,^,${QueueCounter})})
    same => n,EndWhile()
; пусть агент знает, что он вошёл в систему
   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()

; Используется для регистрации агентов во всех настроенных очередях в базе данных AstDB
exten => *737,1,Verbose(2,Logging out of multiple queues)
; Поскольку мы повторно использовали некоторый код, то поместили дубликат кода в подпрограмму
   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}/${MemberChannel})
   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()

; Используется для приостановки агентов во всех доступных очередях
exten => *738,1,Verbose(2,Pausing member in all queues)
   same => n,GoSub(subSetupAvailableQueues,start,1())
   ; если мы не определяем очередь, то участник приостанавливается во всех очередях
   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(dictate/paused)
   same => n,Hangup()

; Используется для отмены паузы агентов во всех доступных очередях
exten => *739,1,Verbose(2,UnPausing member in all queues)
   same => n,GoSub(subSetupAvailableQueues,start,1())
 ; если мы не определяем очередь, то элемент не будет снят с паузы во всех очередях
   same => n,UnPauseQueueMember(,${MemberChanType}/${MemberChannel})
   same => n,GotoIf($[${UPQMSTATUS} = UNPAUSED]?agent_unpaused,1:agent_not_found,1)

exten => agent_unpaused,1,Verbose(2,Agent paused successfully)
   same => n,Playback(silence/1&available)

; Используется как для приостановки, так и для продолжения функциональности диалплана
exten => agent_not_found,1,Verbose(2,Agent was not found)
   same => n,Playback(silence/1&cannot-complete-as-dialed)

Вы можете дополнительно усовершенствовать эти процедуры входа и выхода, чтобы учесть, что переменные канала AQMSTATUS и RQMSTATUS устанавливаются каждый раз при использовании AddQueueMember() и RemoveQueueMember(). Например, можно установить флаг, позволяющий участнику очереди знать, что он не был добавлен в очередь или даже добавить записи или использовать системы преобразования текста в речь для воспроизведения конкретной очереди, которая создает проблему. Или, если вы отслеживаете очереди через AMI, то можете получить всплывающее окно экрана, или использовать JabberSend() для отправки участнику очереди мгновенного сообщения, или...(Разве Asterisk это не весело?).

Расширенные очереди

В этом разделе мы рассмотрим некоторые более тонкие элементы управления очередью, такие как параметры управления объявлениями и когда абоненты (callers) должны быть помещены в очередь (или удалены из нее). Мы также рассмотрим пенальти (ударение на первую букву "е") и приоритеты, исследуя возможности контроля агентов в нашей очереди, отдавая предпочтение пулу агентов, а затем увеличивая этот пул динамически на основе времени ожидания в очереди. Наконец, мы рассмотрим использование локальных каналов в качестве участников очереди, что даст нам возможность выполнять трюки диалплана до подключения абонента к агенту.

Очередь с приоритетом (Queue Weighting)

Иногда вам нужно добавить людей в очередь с более высоким приоритетом, чем у других абонентов. Возможно, абонент уже провел некоторое время в очереди, и агент принял некоторую информацию, но понял, что абонент должен быть переведен в другую очередь. В этом случае, чтобы свести к минимуму общее время ожидания абонента, возможно, было бы желательно перенести вызов в приоритетную очередь, которая имеет более высокий вес (weight) (и, следовательно, более высокое предпочтение), где ему быстрее ответят.

Установка более высокого приоритета для очереди выполняется с помощью параметра weight. Если у вас есть две очереди с разными весом (например, support и support-priority), агентам, назначенным в обе очереди, будут переданы вызовы из очереди с более высоким приоритетом, а не вызовы из очереди с более низким приоритетом. Эти агенты не будут принимать никаких вызовов из очереди с более низким приоритетом, пока очередь с более высоким приоритетом не будет очищена. (Обычно есть некоторые агенты, которые назначаются только в очередь с более низким приоритетом, чтобы гарантировать своевременную обработку этих вызовов.) Например, если мы поместим участника очереди Джеймса Шоу в обе очереди support и support-priority, абоненты в очереди support-priority будут иметь предпочтительное положение вместе с Джеймсом, по сравнению с абонентами в очереди support.

Давайте посмотрим, как бы мы реализовали это. Во-первых, нам нужно создать новую очередь, аналогичную очереди support, за исключением опции weight.

MySQL> INSERT INTO `asterisk`.`queues`
(name,strategy,joinempty,leavewhenempty,ringinuse,autofill,musiconhold,monitor_format,
monitor_type,weight)

VALUES
('support-priority','rrmemory','unavailable,invalid,unknown','unavailable,invalid,unknown',
'no','yes','default','wav','MixMonitor','10');

С нашей новой настроенной очередью мы можем теперь создать два расширения для трансфера абонентов. Это можно сделать везде, где вы обычно размещаете логику диалплана для выполнения трансферов. Мы будем использовать контекст LocalSets, который ранее включили в качестве начального контекста для наших устройств:

exten => 611,1,Noop()
   same => n,Progress()
   same => n,Queue(support)
   same => n,Hangup()
exten => 612,1,Noop()
   same => n,Progress()
   same => n,Queue(support-priority)
   same => n,Hangup()
exten => *724,1,Noop(Page)

Осталось убедиться, что все ваши участники очереди помещены в обе очереди.

Приоритет участника очереди

Внутри очереди мы можем применить пенальти к участникам, чтобы уменьшить их предпочтение быть вызванными, когда есть люди, ожидающие в определенной очереди. Например, мы можем применять пенальти, когда хотим чтобы они были участниками очереди, но принимали вызовы только тогда, когда очередь заполнится до тех пор, когда все наши предпочтительные агенты будут недоступны. Выставляя величину пенальти для каждого участника очереди4 - мы можем контролировать предпочтения, куда приходят звонки, но при этом гарантировать что другие участники очереди будут доступны для ответа абонентов, если предпочтительный участник недоступен.

Пенальти также могут быть определены с помощью AddQueueMember(). Мы изменим наш вход в несколько очередей, чтобы обеспечить необходимые пенальти.

Во-первых, давайте обновим нашу AstDB, чтобы включить пенальти для участника:

*CLI> database put queue_agent SOFTPHONE_A/penalty 0^2

*CLI> database show queue agent

/queue_agent/SOFTPHONE_A/available_queues : support^sales
/queue_agent/SOFTPHONE_A/penalty : 0^2

Далее, несколько изменений в нашем диалплане. Подпрограмме нужна новая строка (некоторый код был удален для краткости, заменен на ; ...):

 
[subSetupAvailableQueues]
; ...
; Получить список очередей, доступных для этого агента
   same => n,Set(AvailableQueues=${DB(queue_agent/${MemberChannel}/available_queues)})
   same => n,Set(MemberPenalties=${DB(queue_agent/${MemberChannel}/penalty)})
; если нет назначенных очередей ...
 

Контекст [sets] также требует нескольких новых строк (некоторый код был удален для краткости, заменен на ; ...). Вставляйте/изменяйте только код, выделенный жирным шрифтом.

 
exten => \*736,1,Verbose(2,Logging into multiple queues per the database values)
; ...
   same => n,Set(WorkingQueue=${CUT(AvailableQueues,^,${QueueCounter})})
   same => n,Set(WorkingPenalty=${CUT(MemberPenalties,^,${QueueCounter})})
; While the WorkingQueue ...
; ...
     same => n,Set(WorkingQueue=${CUT(AvailableQueues,^,${QueueCounter})})
     same => n,Set(WorkingPenalty=${CUT(MemberPenalties,^,${QueueCounter})})
     same => n,EndWhile()
; ...
 

Эти примеры, вероятно, не подходят для продакшена (мы бы использовали специально построенные таблицы MySQL для такого рода вещей, а не AstDB), но это дает вам представление о том, как диалплан может быть использован для применения динамической логики к более сложным сценариям конфигурации.

Динамическое изменение пенальти (queuerules)

Используя таблицу asterisk.queuerules можно определить правила, изменяющие значения переменных канала QUEUE_MIN_PENALTY и QUEUE_MAX_PENALTY. Переменные канала QUEUE_MIN_PENALTY и QUEUE_MAX_PENALTY используются для управления тем, какие участники очереди предпочтительнее для обслуживания абонентов. Допустим, у нас есть очередь с названием support и имеется пять участников очереди с различными пенальти в диапазоне от 1 до 5. Если до того, как абонент войдет в очередь, для переменной канала QUEUE_MIN_PENALTY задано значение 2, а для QUEUE_MAX_PENALTY - 4, то для ответа на этот вызов будут считаться доступными только участники очереди, пенальти которых находятся в диапазоне от 2 до 4.:

same => n,Set(QUEUE_MIN_PENALTY=2) ; установить минимальный пенальти участника
same => n,Set(QUEUE_MAX_PENALTY=4) ; установить максимальное пенальти участника
same => n,Queue(support) ; вход в очередь с минимальными и максимальными пенальти
                         ; для участников, которые будут использоваться

Более того, во время пребывания абонента в очереди мы можем динамически изменять значения QUEUE_MIN_PENALTY и QUEUE_MAX_PENALTY для этого абонента. Это позволяет использовать либо больше, либо другой набор участников очереди, в зависимости от того, как долго вызывающий абонент ожидает в очереди. Например, в предыдущем примере мы могли бы изменить минимальное значение пенальти на 1, а максимальное - на 5, если абонент находится более 60 секунд в очереди.

Файл примера ~/src/asterisk-15.*/configs/samples/queuerules.conf.sample содержит отличную справку о том, как работают правила очереди.

Правила определяются с использованием таблицы asterisk.queuerules. Несколько правил могут быть созданы для того, чтобы облегчить различные изменения пенальти на протяжении всего вызова. Давайте посмотрим, как мы можем определить правило.:

MySQL> insert into `asterisk`.`queue_rules`
(rule_name,time,min_penalty,max_penalty)

VALUES
('more_members',60,5,1);

Новые правила будут касаться только новых абонентов, входящих в очередь, а не существующих абонентов, которые уже находятся в ней.

Мы назвали правило more_members и определили следующие значения:

60 Количество секунд ожидания перед изменением значений пенальти.
5 Новое QUEUE_MAX_PENALTY.
1 Новое QUEUE_MIN_PENALTY.

Теперь мы можем указать нашим очередям использовать его.

MySQL> update `asterisk`.`queues`

set defaultrule='more_members' where `name` in ('sales','support')

Файл queuerules.conf.sample показывает, что эти правила достаточно гибкие. Если вы хотите детально контролировать приоритеты вызовов - вам может потребоваться дополнительная лабораторная работа.

Управление объявлениями

Asterisk имеет возможность проигрывать несколько объявлений абонентам, ожидающим в очереди. Например, вы можете объявить позицию вызывающего абонента в очереди, объявить среднее время ожидания или периодически благодарить вызывающих абонентов за ожидание (или все, что скажут ваши аудиофайлы). Важно тщательно настроить значения, контролирующие когда эти объявления воспроизводятся для абонентов, потому что объявление их позиции, благодарность за ожидание и информирование о среднем времени ожидания слишком часто будет раздражать их, что не является нашей целью.

Воспроизведение объявлений между музыкальными файлами на удержании

Вместо того, чтобы разбираться со сложностями объявлений для каждой из ваших очередей - вы можете альтернативно (или совместно) использовать функциональность объявлений, определенную в musiconhold.conf. Перед воспроизведением файла музыки на удержании - будет воспроизведен файл объявления, а затем воспроизведен снова между аудиофайлами. Допустим, у вас есть 5-минутный цикл аудио, но вы хотите воспроизводить сообщение “Спасибо за ожидание” каждые 30 секунд. Вы можете разбить аудиофайл на 30-секундные сегменты, задать их имена, начиная с 00-, 01-, 02- и так далее (чтобы они воспроизводились по порядку), а затем определить объявления в musiconhold.conf чтобы выглядело примерно так:

[moh_jazz_queue]
mode=files
sort=alpha
announcement=queue-thankyou
directory=moh_jazz_queue

В таблице очередей есть несколько параметров, которые можно использовать для точной настройки того, какие и когда объявления воспроизводятся для ваших абонентов. Полный список опций очереди доступен в разделе ~/src/asterisk-16.*/configs/samples/queues.conf.sample. Таблица 12-3 рассматривает несколько наиболее полезных из них.

Таблица 12-3. Параметры, связанные с контролем времени запроса в очереди

Параметр Доступные значения Описание
announce-frequency Значение в секундах Определяет, как часто мы должны объявлять позицию вызывающего абонента и/или предполагаемое время удержания в очереди. Установите это значение на ноль, чтобы отключить.
min-announce-frequency Значение в секундах Указывает минимальное количество времени, которое должно пройти, прежде чем мы снова объявим позицию абонента в очереди. Используется когда позиция абонента может часто меняться для предотвращения прослушивания нескольких объявлений за короткий промежуток времени.
periodic-announce-frequency Значение в секундах Указывает, как часто делать периодические объявления абоненту.
random-periodic-announce yes, no Если установлено значение yes - будут воспроизводиться определенные периодические объявления в случайном порядке. Смотрите periodic-announce.
relative-periodic-announce yes, no Если установлено значение yes - для periodic-announce-frequency таймер запустится когда будет достигнут конец воспроизводимого файла, а не с самого начала. По умолчанию no.
announce-holdtime yes, no, once Определяет, следует ли воспроизводить расчетное время ожидания вместе с периодическими объявлениями. Может быть установлено значение yes, no или только один раз - once.
announce-position yes, no, limit, more Определяет, следует ли объявлять позицию вызывающего абонента в очереди. Если установлено значение no - позиция никогда не будет объявлена. Если установлено значение yes - позиция абонента всегда будет объявлена. При значении limit - абонент услышит свою позицию в очереди только в том случае, если она находится в пределе, определенном параметром announce-position-limit. Если задано значение more - вызывающий услышит свою позицию только в том случае, если она выходит за пределы числа, определенного параметром announce-position-limit.
announce-position-limit Число от нуля и больше Используется, если вы определили объявленную позицию как limit или more.
announce-round-seconds Значение в секундах Если это значение отлично от нуля, число секунд также объявляется и округляется до определенного значения.

Таблица 12-4 определяет файлы, которые будут использоваться при воспроизведении объявлений вызывающему абоненту.

Таблица 12-4. Параметры управления воспроизведением подсказок в очереди

Параметр Доступные значения Описание
musicclass Класс музыки определенный в musiconhold.conf Устанавливает класс музыки, который будет использоваться определенной очередью. Вы также можете переопределить это значение с помощью канальной переменной CHANNEL(musicclass).
queue-thankyou Имя файла для воспроизведения Если не определено - воспроизводится значение по умолчанию («Благодарим за терпение»). Если установлено пустое значение - подсказка не будет воспроизводиться вообще.
queue-youarenext Имя файла для воспроизведения Если не определено - воспроизводится значение по умолчанию («Ваш звонок является первым в очереди и будет отвечен первым доступным оператором»). Если установлено пустое значение - подсказка не будет воспроизводиться вообще.
queue-thereare Имя файла для воспроизведения Если не определено - воспроизводится значение по умолчанию («Ваша позиция в очереди»). Если установлено пустое значение - подсказка не будет воспроизводиться вообще.
queue-callswaiting Имя файла для воспроизведения Если не определено - воспроизводится значение по умолчанию («Ожидайте, пожалуйста, ответа оператора»). Если установлено пустое значение - подсказка не будет воспроизводиться вообще.
queue-holdtime Имя файла для воспроизведения Если не определено - воспроизводится значение по умолчанию («Прогнозируемое время ожидания составляет»). Если установлено пустое значение - подсказка не будет воспроизводиться вообще.
queue-minutes Имя файла для воспроизведения Если не определено - воспроизводится значение по умолчанию («минут»). Если установлено пустое значение - подсказка не будет воспроизводиться вообще.
queue-seconds Имя файла для воспроизведения Если не определено - воспроизводится значение по умолчанию («секунд»). Если установлено пустое значение - подсказка не будет воспроизводиться вообще.
queue-reporthold Имя файла для воспроизведения Если не определено - воспроизводится значение по умолчанию («Время ожидания»). Если установлено пустое значение - подсказка не будет воспроизводиться вообще.
periodic-announce Набор периодических объявлений для воспроизведения, разделенных запятыми Подсказки воспроизводятся в том порядке, в котором они определены. По умолчанию используется параметр queue-periodic-announce ("Все наши операторы заняты, пожалуйста, оставайтесь на линии и Ваш звонок будет обслужен первым доступным оператором").

Существует масса возможностей для гибкости при проектировании взаимодействия абонента во время ожидания, но, пожалуйста, не забывайте, что ваши абоненты никогда не будут счастливы ожидая в очереди. Кроме того, если вы нашли какую-то более-менее приличную музыку для MOH и ваши абоненты наслаждаются ею - прерывание воспроизведения еще одним сообщением несёт риск по-настоящему вскипятить их кровь. Когда абоненту наконец ответит оператор - он получит удар гнева, даже если это на самом деле ваша вина5.

Так что не нужно усложнять настройку удержания. Абоненты знают что они ждут, и они не рады этому. Доставьте их оператору как можно быстрее, с минимальным количеством глупостей пока они ожидают в очереди и не поддавайтесь искушению сделать очередь более важной для абонентов, чем она есть на самом деле.

Переполнение

К сожалению, ваша очередь не всегда будет своевременно доставлять ваших абонентов к агенту. Когда различные условия заставляют очередь отклонять входящих абонентов, мы имеем ситуацию переполнения. Переполнение очереди выполняется либо со значением таймаута, либо при отсутствии доступных участников очереди (как определено joinempty или leavewhenempty). В этом разделе мы обсудим, как контролировать возникновение переполнения.

Контроль времени ожидания

Приложение Queue() поддерживает два вида тайм-аута: один определяет максимальный период времени, в течение которого вызывающий абонент находится в очереди, а другой - как долго следует звонить устройству при попытке подключить вызывающего абонента к участнику очереди. Эти парметры не связаны, но могут влиять друг на друга. В этом разделе мы будем говорить о максимальном периоде времени, в течение которого вызывающий абонент остается в приложении Queue() до того, как вызов переполнится, до следующего шага в диалплане, который может быть чем-то вроде VoiceMail() или даже другой очередью. После того, как вызов выпал из очереди - он может отправиться куда угодно, куда обычно может идти вызов, если он контролируется диалпланом.

Тайм-ауты указываются в двух местах. Тайм-аут, указывающий, в течение какого времени звонить участникам очереди - указывается в таблице queues. Абсолютный тайм-аут (время пребывания абонента в очереди) контролируется с помощью приложения Queue(). Чтобы задать максимальное время пребывания абонентов в очереди, просто укажите его после имени очереди в приложении Queue():

; Очередь
exten => 610,1,Noop()
   same => n,Progress()
   same => n,Queue(sales,120)
   same => n,Voicemail(${EXTEN}@queues,u)
   same => n,Hangup()
exten => 611,1,Noop()
   same => n,Progress()
   same => n,Queue(support,120)
   same => n,Voicemail(${EXTEN}@queues,u)
   same => n,Hangup()
exten => 612,1,Noop()
   same => n,Progress()
   same => n,Queue(support-priority,120)
   same => n,Voicemail(${EXTEN}@queues,u)
   same => n,Hangup()

Поскольку мы отправляем звонки на голосовую почту, нам понадобятся почтовые ящики:

MySQL> INSERT INTO `asterisk`.`voicemail`
(context,mailbox,password,fullname,email)

VALUES
('queues','610','192837','Queue sales','name@shifteight.org'),
('queues','611','192837','Queue support','name@shifteight.org'),
('queues','612','192837','Queue support-priority','name@shifteight.org');

Конечно, мы могли бы определить другое назначение, но приложение VoiceMail() является общим местом назначения переполнения для очереди. Очевидно, что отправка звонков на голосовую почту не идеальна (они надеялись поговорить с кем-то вживую), поэтому убедитесь, что кто-то регулярно проверяет эту почту и перезванивает вашим клиентам.

Предположим, мы установили абсолютное время ожидания равным 10 секундам, значение времени ожидания для звонков участникам очереди равным 5 секундам, а значение тайм-аута для повторной попытки - 4 секунды. В этом случае мы будем звонить участнику очереди в течение 5 секунд, а затем ждать 4 секунды, прежде чем звонить другому участнику очереди. Это дает нам до 9 секунд нашего абсолютного тайм-аута в 10 секунд. Получается, мы должны позвонить второму участнику очереди в течение 1 секунды и затем выйти из очереди, или мы должны позвонить этому участнику в течение полных 5 секунд перед выходом?

Мы контролируем, какое значение тайм-аута имеет приоритет с помощью опции timeoutpriority в таблице queues. Доступные значения: app (по умолчанию) и conf. Если мы хотим, чтобы тайм-аут приложения (абсолютный тайм-аут) имел приоритет, что привело бы к тому, что наш абонент был исключен ровно через 10 секунд (даже если он только начинал звонить агенту), мы должны установить значение timeoutpriority в app. Если мы хотим, чтобы таймаут файла конфигурации имел приоритет и закончил звонить участнику очереди, что заставит абонента оставаться в очереди немного дольше - мы должны установить для timeoutpriority значение conf. Значением по умолчанию является app (по умолчанию в предыдущих версиях Asterisk). Вероятно, в большинстве случаев вы захотите использовать conf (особенно если хотите, чтобы опыт работы с абонентами был как можно менее странным).

MySQL> update `asterisk`.`queues` set timeoutpriority='conf'
  where name in ('sales','support','support-priority');

Цель состоит в том, чтобы доставить абонентов к агентам, так?

Управление временем присоединения и выхода из очереди

Asterisk предоставляет две опции, которые контролируют, когда вызывающие абоненты могут присоединиться и вынуждены покинуть очереди, обе на основе статусов участников очереди. Первая опция, joinempty, используется для контроля возможности абонентов изначально входить в очередь. Вторая опция - leftwhenempty, используется для управления событиями, приводящими к тому, что вызывающие абоненты, уже находящиеся в очереди, будут удалены из этой очереди (т.е. если все участники очереди станут недоступными). Оба параметра допускают разделенный запятыми список значений для управления этим поведением, как показано в Таблице 12-5.

Таблица 12-5. Параметры, которые можно установить для joinempty или leftwhenempty

Значение Описание
paused Участники считаются недоступными если они приостановлены.
penalty Участники считаются недоступными если их пенальти меньше, чем QUEUE_MAX_PENALTY.
inuse Участники считаются недоступными если состояние их устройства InUse.
ringing Участники считаются недоступными если состояние их устройства Ringing.
unavailable Применяется главным образом к каналам агента; если агент не вошел в систему, но является участником очереди - канал считается недоступным.
invalid Участники считаются недоступными если их статус устройства является Invalid. Это типичное условие ошибки.
unknown Участники считаются недоступными если состояние устройства unknown.
wrapup Участники считаются недоступными если они в настоящее время находятся в состоянии перерыва после завершения вызова.

Для joinempty, перед помещением вызывающего абонента в очередь, все участники проверяются на доступность, используя факторы, перечисленные в качестве критериев. Если все участники считаются недоступными - вызывающему абоненту не будет разрешено войти в очередь, и выполнение диалплана будет продолжено со следующим приоритетом.6 Для опции leavewhenempty статусы участников периодически проверяются на соответствие перечисленным условиям; если выясняется, что ни один участник не доступен для приема вызовов - абонент удаляется из очереди, а выполнение диалплана продолжается со следующего приоритета.

Примером использования joinempty может быть:

joinempty=unavailable,invalid,unknown

В этой конфигурации до того, как вызывающий абонент войдет в очередь, будут проверены состояния всех участников очереди и вызывающему не будет разрешено войти в очередь, если по крайней мере один участник очереди не будет найден со статусом, который не является unavailable, invalid или unknown.

Примером leavewhenempty может быть что-то вроде:

leavewhenempty=unavailable,invalid,unknown

В этом случае статусы участников очереди будут периодически проверяться, а вызывающие абоненты будут удаляться из очереди, если не будут найдены участники очереди, которые не имеют статуса недоступных, недействительных или неизвестных (navailable,invalid,unknown).

Предыдущие версии Asterisk использовали значения yes,no,strict,loose в качестве доступных значений. Сравнение этих значений показано в Таблице 12-6.

Таблица 12-6. Сравнение между старыми и новыми значениями для контроля присоединения и отключения абонентов от очереди

Значение Сопоставление (joinempty) Сопоставление (leavewhenempty)
yes (пусто) penalty,paused,invalid
no penalty,paused,invalid (пусто)
strict penalty,paused,invalid,unavailable penalty,paused,invalid,unavailable
loose penalty,invalid penalty,invalid

Использование локальных каналов

Использование локальных каналов в качестве участников очереди является мощным инструментом диалплана для вызова устройств агентов. Когда Queue() решает передать вызов агенту - использование локальных каналов позволяет нам определить пользовательские переменные канала, записать в файл журнала, установить некоторое ограничение на длину вызова (например, если это платная услуга), отправлять сообщения всех видов по всем устройствам, выполнять транзакции с базой данных и многие другие действия, которые мы могли бы захотеть сделать именно в этот момент. Обычно мы не контролируем, когда приложение Queue() решило представить вызывающего абонента конкретному участнику, но с локальными каналами мы получаем возможность последнего удара и даже можем вернуть Congestion(), который будет иметь эффект возврата вызывающего абонента в очередь, поскольку очередь не будет считать, что этот вызов был успешно доставлен агенту (это может быть очень удобно, поскольку некоторые внешние условия могут быть оценены до того, как вызов будет просто отправлен на конечную точку).

При использовании локальных каналов для очередей они добавляются так же, как и любые другие каналы, обычно динамически через приложение диалплана AddQueueMember().

Нам нужно будет определить локальный канал, где происходит все волшебство и, поскольку локальные каналы обычно используются аналогично подпрограммам, нам нравится называть и находить их в диалплане с подпрограммами, с контекстным именем, начинающимся с "local" (сродни тому, как подпрограммы начинаются с sub). Если вы создавали свой диалплан вместе с книгой, то заметите, что у вас уже есть локальный канал [localDialDelay]. Добавьте этот код где-нибудь в этой части диалплана.

[localMemberConnector]
exten => _[A-Za-z0-9].,1,Verbose(2,Connect ${CALLERID(all)} to Agent at ${EXTEN})
 ; отфильтровать все плохие символы, разрешить буквенно-цифровые символы и дефис
 same => n,Set(QueueMember=${FILTER(A-Za-z0-9-,${EXTEN})})
 ; назначить первое поле QueueMember технологии; дефис в качестве разделителя
 same => n,Set(Technology=${CUT(QueueMember,-,1)})
 ; назначить второе поле QueueMember устройству, дефис в качестве разделителя
 same => n,Set(Device=${CUT(QueueMember,-,2)})
 ; вызов агента
 same => n,Dial(${Technology}/${Device})
 same => n,Hangup()

Этот код, возможно, еще не имеет полного смысла, но то, что он делает - это берет ${EXTEN} (который на данный момент является сложной буквенно-цифровой строкой) и нарезает его для извлечения фактически вызываемого канала (т.е. мы передаем как часть локального канала всю информацию, необходимую для набора фактического канала).

Давайте посмотрим на код AddQueueMember и посмотрим, сможем ли мы придать ему больше смысла:

exten => *740,1,Noop(Logging in device ${CHANNEL(endpoint)} into the support queue)
 same => n,Set(MemberTech=${CHANNEL(channeltype)})
 same => n,Set(MemberIdent=${CHANNEL(endpoint)})
 same => n,Set(Interface=${MemberTech}/${MemberIdent})
 ;;; СЛЕДУЮЩЕЕ ДОЛЖНО БЫТЬ ВСЕ В ОДНУ СТРОКУ
 same => n,AddQueueMember(support,Local/${MemberTech}-${MemberIdent}@localMemberConnector
,,,${IF($[${MemberTech} = PJSIP]?${Interface})})
 same => n,Playback(silence/1)
 same => n,Playback(${IF($[${AQMSTATUS} = ADDED]?agent-loginok:agent-incorrect)})
 same => n,Hangup()

Как только вы все это введете и перезагрузите свой диалплан, войдите в очередь, набрав *740, и давайте посмотрим, что у нас есть.

*CLI> queue show support

support has 0 calls (max unlimited) in 'rrmemory' strategy (1s holdtime, 0s talktime),
W:0, C:1, A:1, SL:0.0%, SL2:0.0% within 0s
    Members:
       PJSIP/SOFTPHONE_A (Local/PJSIP-SOFTPHONE_A@localMemberConnector)
(ringinuse disabled) (dynamic) (Not in use)
    No Callers

Теперь участник очереди идентифицируется как локальный канал с именем PJSIP-SOFTPHONE_A в контексте [localMemberConnector]. (Канал PJSIP/SOFTPHONE_A будет отслеживаться на предмет фактического состояния конечной точки.) Когда Queue() решает послать вызов участнику - вызов будет в конечном итоге в контексте [localMemberConnector], где EXTEN (PJSIP-SOFTPHONE_A) будет порезан вдоль и поперёк, чтобы получить наш тип канала и конечную точку7, которая фактически будет вызвана.

На данном этапе цель всей этой дополнительной сложности ясна не сразу. Пока что мы не получаем ничего полезного от всего этого дополнительного кода.

Итак, теперь, когда мы можем добавлять устройства в очередь, используя локальные каналы, давайте посмотрим, как это может быть полезно.

Допустим, у нас есть клиент, который просто не выносит нашего лучшего агента. Он хороший клиент, поэтому мы не хотим его потерять, но это наш лучший агент и мы не собираемся его увольнять.

Чтобы настроить это, мы собираемся назначить идентификатор вызывающего абонента SOFTPHONE_B, так что у нас есть что предложить.

MySQL> UPDATE `asterisk`.`ps_endpoints` SET callerid='SOFTPHONE_B <103>'
WHERE id='SOFTPHONE_B';

Мы собираемся встроить небольшую хитрость в наш диалплан, которая будет отклонять вызов агенту, если идентификатор вызывающего абонента соответствует нашему чувствительному клиенту.

[localMemberConnector]
exten => _[A-Za-z0-9].,1,Verbose(2,Connect ${CALLERID(all)} to Agent at ${EXTEN})
 same => n,Wait(0.1) ; Prevent loop from completely hogging CPU
 same => n,Set(QueueMember=${FILTER(A-Za-z0-9-_,${EXTEN})}) ; allow alphanum, - , _
 same => n,Set(Technology=${CUT(QueueMember,-,1)}) ; first field, hyphen is separator
 same => n,Set(Device=${CUT(QueueMember,-,2)}) ; second field, hypen separator
 ; is this our mismatched pair?
 same => n,DumpChan()
 same => n,Noop(${CALLERID(all)} : ${Device})
 same=>n,GotoIf($["${CALLERID(num)}"="103"&"${Device}"="SOFTPHONE_A"]?rejectcall:ringagent)
 ; dial the agent
 same => n(ringagent),Dial(${Technology}/${Device})
 same => n,Hangup()
 ; send it back!
 same => n(rejectcall),Congestion()
 same => n,Hangup()

Передача Congestion() приведет к тому, что вызывающий будет возвращен в очередь (пока это происходит, вызывающий не получает никаких признаков того, что что-то не так, и продолжает слушать музыку пока на его вызов не ответит какой-то канал)8. В идеале, ваша очередь запрограммирована на попытку вызова другого агента; однако, вы должны иметь в виду, что если app_queue определяет, что этот участник все еще является его приоритетным выбором при вызове - вызов будет просто повторно подключен к тому же агенту (и снова получит перегрузку, и таким образом потенциально создаст логический цикл захвата процессора). Чтобы избежать этого - вам нужно будет убедиться, что очередь использует стратегию распределения, такую как round_robin, random или любую другую, которая гарантирует, что один и тот же участник не будет вызван снова и снова. Именно поэтому мы добавляем крошечную небольшую задержку в наш контекст [LocalMemberConnector] так, что если цикл, подобный этому, действительно произойдет - в нем есть по крайней мере небольшой регулятор.

Давайте просто рассмотрим наш код. Установите для CallerID значение, отличное от 103 и вызов должен пройти.

MySQL> UPDATE `asterisk`.`ps_endpoints` SET callerid='SOFTPHONE_B <123>'
WHERE id='SOFTPHONE_B';

Использование локальных каналов для ваших каналов участников не облегчит проектирование и отладку очереди, но даст вам гораздо больше власти над вашими очередями, чем простое использование app_queue, поэтому, если у вас сложное требование очереди - использование локальных каналов даст вам уровень контроля, который вы не имели бы в противном случае.

Статистика очереди: файл queue_log

Файл queue_log (обычно расположенный в /var/log/asterisk) содержит совокупную информацию о событиях для очередей, определенных в вашей системе (например, когда очередь перезагружается, когда участники очереди добавляются или удаляются, события паузы/возобновления и т.д.), а также некоторые сведения о вызовах (например, их статус и каналы, к которым были подключены абоненты). Журнал очередей включен по умолчанию, но им можно управлять с помощью файла /etc/asterisk/logger.conf. Есть три параметра, связанных с файлом queue_log, в частности:

queue_log

Определяет, включен ли журнал очередей или нет. Допустимые значения yes или no (по умолчанию - yes).

queue_log_to_file

Определяет, следует ли записывать журнал очереди в файл, даже если имеется серверная часть в real-time. Допустимые значения - yes или no (по умолчанию - no).

queue_log_name

Управляет именем журнала очереди. По умолчанию это queue_log.

Журнал очереди представляет собой разделенный на каналы список событий. Поля в файле queue_log:

  • Метка времени UNIX Epoch
  • Уникальный идентификатор звонка
  • Имя очереди
  • Имя соединительного канала (bridged chanel)
  • Тип события
  • Пусто или дополнительные параметры события

Информация, содержащаяся в параметрах события, зависит от типа события. Типичный файл queue_log будет выглядеть примерно так:

1530389309|NONE|NONE|NONE|QUEUESTART|
1530409313|CLI|support|PJSIP/SOFTPHONE_B|ADDMEMBER|
1530409467|CLI|support|PJSIP/SOFTPHONE_B|REMOVEMEMBER|
1530409666|NONE|support|PJSIP/SOFTPHONE_B|PAUSE|Callbacks
1530411108|NONE|support|PJSIP/SOFTPHONE_B|UNPAUSE|FinishedCallbacks
1530440239|1530440239.10|support|PJSIP/SOFTPHONE_A|ADDMEMBER|
1530440303|1530440303.16|support|PJSIP/SOFTPHONE_A|REMOVEMEMBER|
1530497165|1530497165.54|support|Local/PJSIP-SOFTPHONE_A@MemberConnector|ADDMEMBER|
1530497388|CLI|support|Local/PJSIP-SOFTPHONE_A@MemberConnector|REMOVEMEMBER|
1530497408|1530497408.60|support|Local/PJSIP-SOFTPHONE_A@localMemberConnector|ADDMEMBER|
1530497506|1530497506.71|support|NONE|ENTERQUEUE||SOFTPHONE_B|1
1530497511|1530497506.71|support|PJSIP/SOFTPHONE_A|CONNECT|5|1530497506.72|4
1530497517|1530497506.71|support|PJSIP/SOFTPHONE_A|COMPLETEAGENT|5|6|1
1530509861|1530509861.134|support|NONE|ENTERQUEUE||SOFTPHONE_B|1
1530509864|1530509861.134|support|PJSIP/SOFTPHONE_A|RINGCANCELED|2224
1530509864|1530509861.134|support|NONE|ABANDON|1|1|3
1530510503|1530510503.156|support|NONE|ENTERQUEUE||103|1
1530510503|1530510503.156|support|PJSIP/SOFTPHONE_A|RINGNOANSWER|0
1530510511|1530510503.156|support|NONE|ABANDON|1|1|8
1530510738|1530510738.163|support|NONE|ENTERQUEUE||123|1
1530510742|1530510738.163|support|PJSIP/SOFTPHONE_A|CONNECT|4|1530510738.164|4
1530510752|1530510738.163|support|PJSIP/SOFTPHONE_A|COMPLETECALLER|4|10|1

Как видно из этого примера - уникальный идентификатор события может существовать не всегда. Внешние службы, такие как Asterisk CLI, могут выполнять действия в очереди, и в этих случаях вы увидите что-то вроде CLI в поле Уникальный идентификатор.

Доступные события и информация, которую они предоставляют, описаны в Таблице 12-7.

Таблица 12-7. События в журнале очереди Asterisk

Событие Предоставленная информация
ABANDON Пишется когда абонент в очереди вешает трубку до того, как на его звонок ответит агент. Для ABANDON предусмотрены три параметра: положение вызывающего абонента при отбое, исходное положение вызывающего абонента при входе в очередь и время ожидания вызывающего абонента до отбоя.
ADDMEMBER Пишется при добавлении участника в очередь. Имя соеденительного канала будет заполнено названием канала, добавленного в очередь.
AGENTDUMP Указывает, что агент повесил трубку на вызывающем абоненте во время воспроизведения объявления очереди, прежде чем они были соединены вместе.
AGENTLOGIN Пишется при входе агента в систему. Поле bridged channel будет содержать что-то вроде Agent/9994, если войти в систему с помощью chan_agent, а поле первого параметра будет содержать вошедший канал (например, SIP/0000FFFF0001).
AGENTLOGOFF Пишется когда агент выходит из системы, вместе с параметром, указывающим, как долго агент входил в систему. Обратите внимание, что поскольку вы часто будете использовать RemoveQueueMember() для выхода из системы - этот параметр может не записываться. Вместо него смотрите событие REMOVEMEMBER.
COMPLETEAGENT Пишется когда вызов соединяется с оператором и агент вешает трубку, наряду с параметрами, указывающими время, в течение которого вызывающий абонент удерживался в очереди, продолжительность вызова с оператором и исходное положение, в котором вызывающий абонент вошел в очередь.
COMPLETECALLER То же, что COMPLETEAGENT, за исключением того, что вызывающий абонент повесил трубку, а не агент.
CONFIGURELOAD Указывает, что конфигурация очереди была перезагружена (например, через module reload app_queue.so).
CONNECT Пишется при соединении абонента и агента. Записываются также три параметра: время ожидания абонента в очереди, уникальный идентификатор канала участника очереди, к которому был подключен абонент, и время, в течение которого телефон участника очереди звонил до получения ответа.
ENTERQUEUE Записывается, при входе абонента в очередь. Также записываются два параметра: URL (если указан) и идентификатор вызывающего абонента.
EXITEMPTY Пишется когда вызывающий объект удаляется из очереди из-за отсутствия агентов, доступных для ответа на вызов (как указано параметром leavewhenempty). Записываются также три параметра: положение вызывающего абонента в очереди, исходное положение, с которым абонент вошел в очередь и время, в течение которого вызывающий абонент находился в очереди.
EXITWITHKEY Пишется когда вызывающий абонент выходит из очереди, нажав одну клавишу DTMF на своем телефоне чтобы выйти из очереди и продолжить в диалплане (как разрешено параметром context в queues.conf). Записываются четыре параметра: ключ, используемый для выхода из очереди, позиция вызывающего абонента в очереди при выходе, исходная позиция, с которой абонент вошел в очередь и количество времени, в течение которого вызывающий абонент ожидал в очереди.
EXITWITHTIMEOUT Пишется когда вызывающий удален из очереди из-за timeout, указаного параметром timeout для Queue(). Также записываются три параметра: положение, в котором находился вызывающий абонент при выходе из очереди, исходное положение вызывающего абонента при входе в очередь и количество времени, в течение которого вызывающий абонент ожидал в очереди.
PAUSE Пишется когда участник очереди приостановлен.
PAUSEALL Пишется когда все участники очереди приостановлены.
UNPAUSE Пишется когда участник очереди возобновлён.
UNPAUSEALL Пишется когда все участники очереди возобновлены.
PENALTY Пишется при изменении пенальти участника. Пенальти может быть изменен несколькими способами, такими как функция QUEUE_MEMBER_PENALTY(), Asterisk Manager Interface или команда Asterisk CLI.
REMOVEMEMBER Пишется когда участник очереди удаляется из очереди. Поле bridge channel будет содержать имя участника, удаленного из очереди.
RINGNOANSWER Пишется когда участник очереди звонит в течение определенного периода времени, и превышается значение времени ожидания для вызова участника очереди. Также будет записан один параметр, указывающий время, в течение которого звонил добавочный номер участника.
TRANSFER Записывается при переходе абонента на другой добавочный номер. Также записываются дополнительные параметры, которые включают расширение и контекст, в который был передан абонент, время удержания абонента в очереди, количество времени, в течение которого вызывающий абонент разговаривал с участником очереди и исходное положение вызывающего абонента, когда он вошел в очередь.a
SYSCOMPAT Записывается если агент пытается ответить на вызов, но не удается установить вызов из-за несовместимости в настройке медиапотока.

a Обратите внимание, что при передаче вызывающего абонента с использованием SIP-трансфера (а не встроенных трансферов, запускаемых DTMF и настраиваемых в features.conf), событие TRANSFER может не записываться.

Вывод

Мы начали эту главу с рассмотрения основных очередей вызовов, обсуждения того, что они из себя представляют, как работают и когда вы, возможно, захотите их использовать. После создания простой очереди мы изучили как управлять участниками очереди с помощью различных средств (включая использование локальных каналов, которые обеспечивают возможность выполнения некоторой логики диалплана непосредственно перед подключением к участнику очереди). Конечно, нам нужна возможность отслеживать что делают наши очереди, поэтому мы быстро просмотрели файл queue_log и различные поля, записанные в результате событий, происходящих в наших очередях.

Благодаря информации, представленной в этой главе, вы обладаете большинством базовых знаний, необходимых для реализации очередей в Asterisk.


  1. Это распространенное заблуждение, что очередь может позволить вам обрабатывать больше вызовов. Это не совсем верно: ваши абоненты все равно захотят поговорить с живым человеком, и они будут готовы ждать настолько долго. Другими словами, если у вас мало сотрудников - ваша очередь может оказаться не более чем препятствием для ваших абонентов. Это то же самое, говорите ли вы по телефону или на кассе Walmart. Никто не любит ждать в очереди. Идеальная очередь невидима для звонящих, так как на их звонки отвечают сразу, без ожидания.
  2. Существует несколько книг, в которых обсуждаются метрики колл-центра и доступные стратегии организации очередей, например, «Руководство по метрикам колл-центра» Джеймса Эббота (Роберт Хьюстон Смит).
  3. Мы собираемся использовать символ ^ в качестве разделителя. Возможно, вы могли бы использовать вместо него другой символ, только кроме такого, который анализатор синтаксиса Asterisk будет рассматривать как обычный разделитель (и, таким образом, будет сбит с толку). Поэтому избегайте запятых, точек с запятой и так далее.
  4. Похоже на добавление балласта к жокею или гоночному автомобилю.
  5. Просто предупреждаю.
  6. Если приоритет n+1 (откуда было вызвано приложение Queue()) не определен, вызов будет прерван. Другими словами, не используйте эту функцию, если ваш диалплан не делает что-то полезное на шаге, следующем сразу за Queue().
  7. Возможно, мы могли бы использовать / вместо - в качестве разделителя, давая нам Local/PJSIP/SOFTPHONE_A@localMemberConnector, но мы чувствовали, что это приведёт к странным синтаксическим ошибкам и неудобным для фильтрации и анализа, поэтому мы пошли с -.
  8. Очевидно, что не стоит использовать какой-либо код диалплана в локальном канале, отвечающий на канал, например, Answer(), Playback() и т.д.

Глава 11. Функции АТС, включая парковку, пейджинг и конференц-связь | Содержание | Глава 13. Состояния устройств