#!/bin/bash # AGI-сценарий автодозвона SuperSam. # Аргументы: $1 = сценарий (A|B|C), $2 = тип (dlv|pkp) # A выбор дня + времени (звоним в день готовности) # B день фиксирован (завтра), только время # C слот уже выбран — подтверждение (молчание = подтвердил) # После подтверждения выбора предлагается изменить его (кнопка 2). # Результат: /tmp/ss-result-.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" DIGIT=$(echo "$RESP" | sed -n 's/.*result=\([0-9]*\).*/\1/p') [ -n "$DIGIT" ] && ANY_DIGIT=1 } wait_digit () { # $1=таймаут мс -> DIGIT (после EXEC Playback; 0 у Asterisk = тишина) say "WAIT FOR DIGIT ${1:-9000}" DIGIT=$(echo "$RESP" | sed -n 's/.*result=\([0-9]*\).*/\1/p') if [ "$DIGIT" = "0" ] || [ -z "$DIGIT" ]; then DIGIT="" else 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 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 ;; 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 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 # ─── выбор дня и времени, с возможностью переделать ────────────────── 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