supersam/docs/ats/ss-dialog.agi

233 lines
9.2 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"
DIGIT=$(echo "$RESP" | sed -n 's/.*result=\([0-9]*\).*/\1/p')
[ -n "$DIGIT" ] && ANY_DIGIT=1
}
# Чтение 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 \"\""; }
# 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
# Пишем состояние ПОСЛЕ КАЖДОГО шага: если клиент положит трубку,
# выбор всё равно сохранён (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}\",
\"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
# ─── выбор дня и времени, с возможностью переделать ──────────────────
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