supersam/docs/ats/ss-dialog.agi

467 lines
19 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/bin/bash
# AGI-сценарий автодозвона SuperSam.
# Аргументы: $1 = сценарий (A|B|C), $2 = тип (dlv|pkp)
# A выбор дня + времени (звоним в день готовности)
# B день фиксирован (завтра), только время
# C слот уже выбран — подтверждение (молчание = подтвердил)
# После подтверждения выбора предлагается изменить его (кнопка 2).
# Результат: /tmp/ss-result-<uniqueid>.txt. Конфиги Asterisk не меняет.
RES=/tmp/ss-result
LOG=/tmp/ss-dialog.log
UNIQ=""
while read -r line; do
line=${line%$'\r'}
[ -z "$line" ] && break
case "$line" in
agi_uniqueid:*) UNIQ=$(echo "$line" | cut -d' ' -f2) ;;
agi_arg_1:*) SCENARIO=$(echo "$line" | cut -d' ' -f2) ;;
agi_arg_2:*) TYPE=$(echo "$line" | cut -d' ' -f2) ;;
esac
done
SCENARIO=${SCENARIO:-A}
TYPE=${TYPE:-dlv}
OUT="${RES}-${UNIQ}.txt"
say () { echo "$1"; read -r resp; echo "$(date +%T) $1 -> $resp" >> "$LOG"; RESP="$resp"; }
digit () { # $1=озвучка, $2=таймаут мс -> DIGIT
say "GET DATA $1 ${2:-9000} 1"
# GET DATA отдаёт саму цифру (200 result=1). Тишина приходит как «(timeout)»,
# а нажатый «0» — как result=0: раньше их путали и «0» (позвать менеджера)
# был неотличим от молчания.
case "$RESP" in
*"(timeout)"*|*"result=-1"*) DIGIT="" ;;
*) DIGIT=$(echo "$RESP" | sed -n 's/^200 result=\([0-9]*\)$/\1/p') ;;
esac
[ -n "$DIGIT" ] && ANY_DIGIT=1
}
wait_digit () { # $1=таймаут мс -> DIGIT (после EXEC Playback; 0 у Asterisk = тишина)
say "WAIT FOR DIGIT ${1:-9000}"
# WAIT FOR DIGIT возвращает ASCII-код нажатой цифры (49 = «1»), 0 = тишина,
# -1 = обрыв. Раньше код сравнивал ASCII с «1»/«2», поэтому меню из фрагментов
# не принимало ни одного нажатия — превращаем код в символ.
RAW=$(echo "$RESP" | sed -n 's/^200 result=\(-\{0,1\}[0-9]*\)$/\1/p')
if [ -z "$RAW" ] || [ "$RAW" -le 0 ]; then
DIGIT=""
else
DIGIT=$(printf "\\$(printf '%03o' "$RAW")")
ANY_DIGIT=1
fi
}
# Чтение channel-переменной из call-файла (Setvar) через AGI.
getvar () { # $1=имя -> VAR
say "GET VARIABLE $1"
VAR=$(echo "$RESP" | sed -n 's/^200 result=1 (\(.*\))$/\1/p')
}
play () { say "STREAM FILE $1 \"\""; }
# Обрыв: клиент положил трубку посередине диалога. Asterisk шлёт SIGHUP и убивает
# скрипт — без этого обработчика результат не уходил в БД, и звонок задним числом
# записывался как «не взял трубку», хотя человек ответил. Сохраняем то, что успели.
on_interrupt () {
if [ -z "$OUTCOME" ]; then
if [ -n "$HALF" ]; then
OUTCOME=no_input_half # день выбрал, время не назвал
elif [ -n "$DAY" ] && [ "$DAY" != "0" ] && [ "$DAY" != "9" ]; then
OUTCOME=no_input_half # день выбран, до времени не дошло
elif [ -z "$ANY_DIGIT" ] && [ "$RING_SEC" -ge 20 ]; then
OUTCOME=no_answer # объявление оператора: человека не было
else
OUTCOME=no_input_day # ответил, но ничего не нажал
fi
save
fi
exit 0
}
trap on_interrupt HUP TERM INT
# Channel-переменные из call-файла (Setvar). Читаем ДО диалога, т.к. AGI
# не видит их как bash-переменные — только через GET VARIABLE.
getvar API_URL; API_URL=$VAR
getvar API_KEY; API_KEY=$VAR
getvar GROUP_ID; GROUP_ID=$VAR
getvar ATTEMPT; ATTEMPT=$VAR
getvar TEST_MODE; TEST_MODE=$VAR
getvar CUSTOMER_PHONE; CUSTOMER_PHONE=$VAR
# Даты самовывоза (заполняет планировщик по окну «готовность + 0..2 дня»)
getvar PICKUP_DAY1; PICKUP_DAY1=$VAR
getvar PICKUP_DAY2; PICKUP_DAY2=$VAR
getvar PICKUP_DAY3; PICKUP_DAY3=$VAR
getvar PICKUP_DAY1_FIRST; PICKUP_DAY1_FIRST=$VAR
# Даты доставки (планировщик берёт рабочие дни графика доставки из админки:
# не в день готовности и без выходных — готов в пятницу → понедельник и вторник)
getvar DELIVERY_DAY1; DELIVERY_DAY1=$VAR
getvar DELIVERY_DAY2; DELIVERY_DAY2=$VAR
CHOSEN_DATE=""
# Пишем состояние ПОСЛЕ КАЖДОГО шага: если клиент положит трубку,
# выбор всё равно сохранён (AGI умирает молча, финальной записи не будет).
save () {
{
echo "uniqueid=$UNIQ"
echo "scenario=$SCENARIO"
echo "outcome=${OUTCOME:-in_progress}"
echo "dtmf_day=$CONF_DAY"
echo "dtmf_half=$CONF_HALF"
echo "in_progress_day=$DAY"
echo "in_progress_half=$HALF"
echo "changed=$CHANGED"
echo "edits=$EDITS"
echo "updated_at=$(date '+%F %T')"
} > "$OUT"
# Отправляем в БД сразу при финальном исходе (клиент может бросить трубку
# в любой момент — результат должен успеть уйти). Идемпотентность по uniqueid.
case "$OUTCOME" in
confirmed|confirmed_kept_previous|confirmed_silent|no_answer|no_input_day|no_input_half|operator|opt_out|invalid_number|error)
send_result ;;
esac
}
# Отправка результата в БД (edge function autocall-result).
# Переменные приходят из call-файла (Setvar) и прочитаны через getvar выше.
# При потере сети звонок не валится: curl с таймаутом, результат в $OUT.
send_result () {
[ -n "$API_URL" ] && [ -n "$API_KEY" ] || return 0
curl -s --max-time 5 -X POST "$API_URL" \
-H "Content-Type: application/json" \
-H "x-webhook-secret: $API_KEY" \
-d "{
\"asterisk_uniqueid\":\"$UNIQ\",
\"order_group_id\":\"${GROUP_ID:-}\",
\"campaign_type\":\"autocall_slot\",
\"customer_phone\":\"${CUSTOMER_PHONE:-}\",
\"attempt_no\":${ATTEMPT:-1},
\"scenario\":\"$SCENARIO\",
\"call_type\":\"${TYPE:-dlv}\",
\"chosen_date\":\"${CHOSEN_DATE:-}\",
\"call_started_at\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",
\"answered_at\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",
\"duration_sec\":0,
\"sip_code\":200,
\"channel_state\":\"Up\",
\"dtmf_day\":${CONF_DAY:-null},
\"dtmf_half\":${CONF_HALF:-null},
\"changed\":\"$CHANGED\",
\"outcome\":\"$OUTCOME\",
\"was_test_mode\":${TEST_MODE:-0}
}" >/dev/null 2>&1 || echo "curl result failed (network?)" >> "$LOG"
}
echo "=== $(date) uniq=$UNIQ scenario=$SCENARIO type=$TYPE ===" >> "$LOG"
DAY=""; HALF=""; OUTCOME=""; CHANGED=no; EDITS=0
CONF_DAY=""; CONF_HALF="" # согласованный слот: только он идёт в БД
save
say "ANSWER"
say "EXEC Wait 1"
# Сколько гудело до «ответа»: старт AGI минус время создания канала.
# Если «ответ» пришёл после долгого гудка, а цифр за весь диалог не было —
# это объявление оператора (сброс/недоступен), а не человек. Такие звонки
# пишем как no_answer, а не «не нажал». Порог 20 сек: живые обычно берут
# раньше; твой сброшенный звонок гудел ~25 сек до объявления.
if [ -n "$UNIQ" ]; then
RING_SEC=$(($(date +%s) - ${UNIQ%%.*}))
[ "$RING_SEC" -lt 0 ] && RING_SEC=0
else
RING_SEC=0
fi
ANY_DIGIT=""
# ─── C: подтверждение уже выбранного слота ───────────────────────────
if [ "$SCENARIO" = "C" ]; then
play "ru/ss_c_intro_${TYPE}"
play "ru/ss_c_day_tomorrow"
play "ru/ss_ok_first"
digit "ru/ss_c_ask" 8000
if [ "$DIGIT" = "1" ]; then
play "ru/ss_c_change"
CHANGED=yes
SCENARIO=A
else
play "ru/ss_c_confirmed"
OUTCOME=confirmed_silent
DAY=1; HALF=1
fi
fi
# ─── Самовывоз: даты приходят из call-файла (PICKUP_DAY1..3) ─────────
# Робот называет конкретные дни, окно считает планировщик:
# готов сегодня → сегодня/завтра/послезавтра, вчера → сегодня/завтра, позавчера → только сегодня.
# Отвечаем выбранной датой (chosen_date), а не абстрактным «день 1/2».
if [ "$TYPE" = "pkp" ] && [ -n "$PICKUP_DAY1" ]; then
play "ru/ss_pkp_intro"
DAYS=1
[ -n "$PICKUP_DAY2" ] && DAYS=2
[ -n "$PICKUP_DAY3" ] && DAYS=3
# Дни идут подряд от сегодня → играем готовые подсказки (живая фраза целиком).
# Если внутри окна выходной (например, пятница → понедельник), подсказка
# «послезавтра» соврала бы — собираем меню из фрагментов и называем реальные дни.
PICK_TODAY=$(date +%F)
PICK_TOMORROW=$(date -d "+1 day" +%F)
PICK_DAYAFTER=$(date -d "+2 days" +%F)
CONTIG=1
# Первый вариант — всегда «сегодня»; если окно начинается не с сегодня
# (звонок попал на выходной), подсказка «сегодня» соврала бы.
if [ "$PICKUP_DAY1" != "$PICK_TODAY" ]; then CONTIG=0; fi
if [ "$DAYS" -ge 2 ] && [ "$PICKUP_DAY2" != "$PICK_TOMORROW" ]; then CONTIG=0; fi
if [ "$DAYS" -ge 3 ] && [ "$PICKUP_DAY3" != "$PICK_DAYAFTER" ]; then CONTIG=0; fi
if [ "$CONTIG" != "1" ]; then
day_frag () {
case "$1" in
"$PICK_TODAY") echo "ru/ss_pkp_d_today" ;;
"$PICK_TOMORROW") echo "ru/ss_pkp_d_tomorrow" ;;
*) echo "ru/ss_pkp_d_$(date -d "$1" +%u)" ;;
esac
}
if [ "$DAYS" = "1" ]; then
PICK_MENU="ru/ss_pkp_only&$(day_frag "$PICKUP_DAY1")"
else
PICK_MENU="ru/ss_pkp_menu1&$(day_frag "$PICKUP_DAY1")"
[ -n "$PICKUP_DAY2" ] && PICK_MENU="${PICK_MENU}&ru/ss_pkp_menu2&$(day_frag "$PICKUP_DAY2")"
[ -n "$PICKUP_DAY3" ] && PICK_MENU="${PICK_MENU}&ru/ss_pkp_menu3&$(day_frag "$PICKUP_DAY3")"
fi
fi
while [ -z "$OUTCOME" ]; do
if [ "$CONTIG" = "1" ]; then
digit "ru/ss_pkp_day${DAYS}" 9000
else
say "EXEC Playback \"$PICK_MENU\""
wait_digit 9000
fi
CHOSEN_DAY=$DIGIT
case "$CHOSEN_DAY" in
9) play "ru/ss_optout"; OUTCOME=opt_out; break ;;
0) play "ru/ss_operator"; OUTCOME=operator; break ;;
1|2|3) [ "$CHOSEN_DAY" -le "$DAYS" ] || CHOSEN_DAY="" ;;
*) CHOSEN_DAY="" ;;
esac
if [ -z "$CHOSEN_DAY" ]; then
if [ "$CONTIG" = "1" ]; then
digit "ru/ss_pkp_day${DAYS}" 9000
else
say "EXEC Playback \"$PICK_MENU\""
wait_digit 9000
fi
CHOSEN_DAY=$DIGIT
case "$CHOSEN_DAY" in
0) play "ru/ss_operator"; OUTCOME=operator; break ;;
1|2|3) [ "$CHOSEN_DAY" -le "$DAYS" ] || CHOSEN_DAY="" ;;
*) CHOSEN_DAY="" ;;
esac
fi
if [ -z "$CHOSEN_DAY" ]; then
if [ -z "$ANY_DIGIT" ] && [ "$RING_SEC" -ge 20 ]; then
OUTCOME=no_answer; break
fi
play "ru/ss_no_input"; OUTCOME=no_input_day; break
fi
case "$CHOSEN_DAY" in
1) CHOSEN_DATE="$PICKUP_DAY1" ;;
2) CHOSEN_DATE="$PICKUP_DAY2" ;;
3) CHOSEN_DATE="$PICKUP_DAY3" ;;
esac
DAY=$CHOSEN_DAY
save
# Половина дня: если осталось только «сегодня» и первая половина уже прошла — не спрашиваем.
if [ "$CHOSEN_DAY" = "1" ] && [ "$PICKUP_DAY1_FIRST" != "1" ]; then
HALF=2
play "ru/ss_ok_second"
else
digit "ru/ss_half_pkp" 9000
HALF=$DIGIT
if [ "$HALF" != "1" ] && [ "$HALF" != "2" ]; then
digit "ru/ss_half_pkp" 9000
HALF=$DIGIT
fi
if [ "$HALF" != "1" ] && [ "$HALF" != "2" ]; then
if [ -z "$ANY_DIGIT" ] && [ "$RING_SEC" -ge 20 ]; then
OUTCOME=no_answer; break
fi
play "ru/ss_no_input"; OUTCOME=no_input_half; break
fi
[ "$HALF" = "1" ] && play "ru/ss_ok_first" || play "ru/ss_ok_second"
fi
CONF_DAY=$CHOSEN_DAY; CONF_HALF=$HALF
OUTCOME=confirmed
play "ru/ss_tail_pkp"
done
fi
# ─── Доставка: даты приходят из call-файла (DELIVERY_DAY1..2) ────────
# Доставляем только рабочими днями из «Графика работы» (админка) и не в день
# готовности: готов в пятницу → предлагаем понедельник и вторник, а не субботу
# с воскресеньем. Робот называет реальные дни, окно считает планировщик.
if [ "$TYPE" = "dlv" ] && [ -n "$DELIVERY_DAY1" ]; then
play "ru/ss_dlv_intro"
DLV_DAYS=1
[ -n "$DELIVERY_DAY2" ] && DLV_DAYS=2
DLV_TODAY=$(date +%F)
DLV_TOMORROW=$(date -d "+1 day" +%F)
dlv_day_frag () {
case "$1" in
"$DLV_TODAY") echo "ru/ss_pkp_d_today" ;;
"$DLV_TOMORROW") echo "ru/ss_pkp_d_tomorrow" ;;
*) echo "ru/ss_pkp_d_$(date -d "$1" +%u)" ;;
esac
}
if [ "$DLV_DAYS" = "1" ]; then
DLV_MENU="ru/ss_dlv_only&$(dlv_day_frag "$DELIVERY_DAY1")"
else
DLV_MENU="ru/ss_dlv_menu1&$(dlv_day_frag "$DELIVERY_DAY1")&ru/ss_dlv_menu2&$(dlv_day_frag "$DELIVERY_DAY2")"
fi
while [ -z "$OUTCOME" ]; do
say "EXEC Playback \"$DLV_MENU\""
wait_digit 9000
CHOSEN_DAY=$DIGIT
case "$CHOSEN_DAY" in
9) play "ru/ss_optout"; OUTCOME=opt_out; break ;;
0) play "ru/ss_operator"; OUTCOME=operator; break ;;
1|2) [ "$CHOSEN_DAY" -le "$DLV_DAYS" ] || CHOSEN_DAY="" ;;
*) CHOSEN_DAY="" ;;
esac
if [ -z "$CHOSEN_DAY" ]; then
say "EXEC Playback \"$DLV_MENU\""
wait_digit 9000
CHOSEN_DAY=$DIGIT
case "$CHOSEN_DAY" in
0) play "ru/ss_operator"; OUTCOME=operator; break ;;
1|2) [ "$CHOSEN_DAY" -le "$DLV_DAYS" ] || CHOSEN_DAY="" ;;
*) CHOSEN_DAY="" ;;
esac
fi
if [ -z "$CHOSEN_DAY" ]; then
if [ -z "$ANY_DIGIT" ] && [ "$RING_SEC" -ge 20 ]; then
OUTCOME=no_answer; break
fi
play "ru/ss_no_input"; OUTCOME=no_input_day; break
fi
case "$CHOSEN_DAY" in
1) CHOSEN_DATE="$DELIVERY_DAY1" ;;
2) CHOSEN_DATE="$DELIVERY_DAY2" ;;
esac
DAY=$CHOSEN_DAY
save
# Время доставки: до или после середины дня (границу задаёт админка,
# робот говорит просто «первая/вторая половина дня»).
digit "ru/ss_half_dlv" 9000
HALF=$DIGIT
if [ "$HALF" != "1" ] && [ "$HALF" != "2" ]; then
digit "ru/ss_half_dlv" 9000
HALF=$DIGIT
fi
if [ "$HALF" != "1" ] && [ "$HALF" != "2" ]; then
if [ -z "$ANY_DIGIT" ] && [ "$RING_SEC" -ge 20 ]; then
OUTCOME=no_answer; break
fi
play "ru/ss_no_input"; OUTCOME=no_input_half; break
fi
[ "$HALF" = "1" ] && play "ru/ss_ok_first" || play "ru/ss_ok_second"
CONF_DAY=$CHOSEN_DAY; CONF_HALF=$HALF
OUTCOME=confirmed
play "ru/ss_tail_dlv"
done
fi
# ─── выбор дня и времени, с возможностью переделать ──────────────────
while [ -z "$OUTCOME" ]; do
# шаг 1: день
if [ "$SCENARIO" = "A" ]; then
digit "ru/ss_a_day_${TYPE}"
DAY=$DIGIT
if [ "$DAY" != "1" ] && [ "$DAY" != "2" ] && [ "$DAY" != "0" ] && [ "$DAY" != "9" ]; then
digit "ru/ss_a_day_${TYPE}"
DAY=$DIGIT
fi
else
DAY=1 # сценарий B: день фиксирован = завтра
fi
save
if [ "$DAY" = "9" ]; then
play "ru/ss_optout"; OUTCOME=opt_out; break
elif [ "$DAY" = "0" ]; then
play "ru/ss_operator"; OUTCOME=operator; break
elif [ "$DAY" != "1" ] && [ "$DAY" != "2" ]; then
if [ -z "$ANY_DIGIT" ] && [ "$RING_SEC" -ge 20 ]; then
OUTCOME=no_answer; break # объявление оператора, человека не было
fi
play "ru/ss_no_input"; OUTCOME=no_input_day; break
fi
# шаг 2: время
if [ "$SCENARIO" = "B" ]; then
digit "ru/ss_b_half_${TYPE}"
else
digit "ru/ss_half_${TYPE}"
fi
HALF=$DIGIT
if [ "$HALF" != "1" ] && [ "$HALF" != "2" ]; then
if [ "$SCENARIO" = "B" ]; then
digit "ru/ss_b_half_${TYPE}"
else
digit "ru/ss_half_${TYPE}"
fi
HALF=$DIGIT
fi
if [ "$HALF" != "1" ] && [ "$HALF" != "2" ]; then
if [ -z "$ANY_DIGIT" ] && [ "$RING_SEC" -ge 20 ]; then
OUTCOME=no_answer; break # объявление оператора, человека не было
fi
play "ru/ss_no_input"; OUTCOME=no_input_half; break
fi
# ЕДИНАЯ ТОЧКА ФИКСАЦИИ: оба шага пройдены — только теперь слот становится
# согласованным. Обрыв на любом шаге раньше не портит уже согласованный слот.
CONF_DAY=$DAY; CONF_HALF=$HALF
OUTCOME=confirmed
save
# шаг 3: озвучиваем выбор
[ "$DAY" = "1" ] && play "ru/ss_ok_tomorrow" || play "ru/ss_ok_dayafter"
[ "$HALF" = "1" ] && play "ru/ss_ok_first" || play "ru/ss_ok_second"
# шаг 4: предложить изменить (один раз)
if [ "$EDITS" -lt 1 ]; then
digit "ru/ss_change_hint" 6000
if [ "$DIGIT" = "2" ]; then
# CONF_* НЕ трогаем: пока новый выбор не сделан целиком, в силе прежний.
# Рабочие DAY/HALF обнуляем, чтобы не склеить новый день со старым временем.
# ВАЖНО: OUTCOME к этому моменту = confirmed (выставлен на шаге фиксации).
# Сбрасываем его в пусто, иначе цикл while [ -z OUTCOME ] завершится и день
# не переспросит. Контракт: пустой OUTCOME = диалог ещё идёт.
DAY=""; HALF=""; OUTCOME=""
EDITS=$((EDITS+1)); CHANGED=yes; SCENARIO=A
save
continue # заново день и время
fi
fi
# подтверждено
if [ "$EDITS" -gt 0 ]; then
play "ru/ss_tail_final_${TYPE}"
else
play "ru/ss_tail_${TYPE}"
fi
OUTCOME=confirmed
done
# Правка начата, но не доведена (тишина/сброс): согласованный слот остаётся в силе
if [ -n "$CONF_DAY" ] && [ "$OUTCOME" != "confirmed" ]; then
OUTCOME=confirmed_kept_previous
fi
save
echo "finished_at=$(date '+%F %T')" >> "$OUT"
say "HANGUP"
exit 0