Compare commits
No commits in common. "75476c9d70293d9002c02e20ac3617200c7f4166" and "d6c3e02f249218dc2e681dbf4cf6255f35f82a41" have entirely different histories.
75476c9d70
...
d6c3e02f24
|
|
@ -1,211 +0,0 @@
|
||||||
#!/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')
|
|
||||||
}
|
|
||||||
# Чтение 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_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"
|
|
||||||
|
|
||||||
# ─── 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
|
|
||||||
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
|
|
||||||
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
|
|
||||||
|
|
@ -7,7 +7,6 @@ const ALLOWED_ORIGINS = [
|
||||||
'http://localhost:5174',
|
'http://localhost:5174',
|
||||||
'http://localhost:3000',
|
'http://localhost:3000',
|
||||||
'https://supasevdev.mkn8n.ru',
|
'https://supasevdev.mkn8n.ru',
|
||||||
'https://dev.mkn8n.ru',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export function createServiceClient() {
|
export function createServiceClient() {
|
||||||
|
|
|
||||||
|
|
@ -83,23 +83,14 @@ Deno.serve(async (request) => {
|
||||||
const scenario = body.scenario || "A";
|
const scenario = body.scenario || "A";
|
||||||
const wasTest = body.was_test_mode ?? false;
|
const wasTest = body.was_test_mode ?? false;
|
||||||
|
|
||||||
// ── Идемпотентность: повторный curl с тем же uniqueid обновляет строку ──
|
// ── Идемпотентность: повторный curl с тем же uniqueid не плодит строки ──
|
||||||
const { data: existing } = await supabase
|
const { data: existing } = await supabase
|
||||||
.from("autocall_log")
|
.from("autocall_log")
|
||||||
.select("id")
|
.select("id")
|
||||||
.eq("asterisk_uniqueid", body.asterisk_uniqueid)
|
.eq("asterisk_uniqueid", body.asterisk_uniqueid)
|
||||||
.maybeSingle();
|
.maybeSingle();
|
||||||
if (existing) {
|
if (existing) {
|
||||||
// Уже записано (клиент бросил трубку после фиксации, AGI шлёт повторно
|
return jsonResponse({ ok: true, duplicate: true }, 200, cors);
|
||||||
// при следующем save). Обновляем outcome/слот, не плодя строки.
|
|
||||||
await supabase.from("autocall_log").update({
|
|
||||||
outcome: body.outcome,
|
|
||||||
dtmf_day: body.dtmf_day ?? null,
|
|
||||||
dtmf_half: body.dtmf_half ?? null,
|
|
||||||
changed: body.changed ?? false,
|
|
||||||
was_test_mode: body.was_test_mode ?? false,
|
|
||||||
}).eq("id", existing.id);
|
|
||||||
return jsonResponse({ ok: true, updated: true }, 200, cors);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Вставка в лог ──
|
// ── Вставка в лог ──
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue