diff --git a/docs/ats/ss-dialog.agi b/docs/ats/ss-dialog.agi index eb98b6a..4fca788 100644 --- a/docs/ats/ss-dialog.agi +++ b/docs/ats/ss-dialog.agi @@ -28,6 +28,7 @@ say () { echo "$1"; read -r resp; echo "$(date +%T) $1 -> $resp" >> "$LOG"; 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 @@ -104,6 +105,19 @@ 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}" @@ -142,6 +156,9 @@ while [ -z "$OUTCOME" ]; do 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 @@ -162,6 +179,9 @@ while [ -z "$OUTCOME" ]; do 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 diff --git a/scripts/autocall_scheduler.py b/scripts/autocall_scheduler.py index e3bbe19..3398339 100644 --- a/scripts/autocall_scheduler.py +++ b/scripts/autocall_scheduler.py @@ -315,11 +315,98 @@ def has_pending_items(conn): return cur.fetchone() is not None +def backfill_no_answer(conn, settings): + """Дописывает no_answer звонкам, которые не получили ответа сети. + + Call-файл с истёкшим WaitTime не запускает AGI — в autocall_log ничего + не пишется, и «не дозвонились» не появляется никогда. Ищем обработанные + заявки старше 10 минут без строки в логе и дописываем no_answer. + Самозакрывающаяся: вставленная строка сама удовлетворяет проверке + существования, повторов не будет. + """ + with conn.cursor(cursor_factory=RealDictCursor) as cur: + cur.execute(""" + SELECT q.id, q.order_group_id, q.phone, q.is_test_call, + q.requested_at, q.processed_at + FROM autocall_queue q + WHERE q.processed_at IS NOT NULL + AND q.processed_at < NOW() - make_interval(mins => 10) + ORDER BY q.processed_at ASC + LIMIT 20 + """) + rows = [dict(r) for r in cur.fetchall()] + backfilled = 0 + for q in rows: + gid = q["order_group_id"] + with conn.cursor(cursor_factory=RealDictCursor) as cur: + if gid: + cur.execute(""" + SELECT 1 FROM autocall_log l + WHERE l.order_group_id = %s AND l.created_at >= %s LIMIT 1 + """, (gid, q["requested_at"])) + else: + cur.execute(""" + SELECT 1 FROM autocall_log l + WHERE l.order_group_id IS NULL + AND l.customer_phone = %s + AND l.created_at >= %s LIMIT 1 + """, (normalize_phone(q["phone"] or ""), q["requested_at"])) + if cur.fetchone(): + continue # строка есть (AGI отработал) — дописывать нечего + # Телефон из заказа (или нормализованный из заявки для pure-теста) + phone = normalize_phone(q["phone"] or "") + group_created = None + if gid: + with conn.cursor(cursor_factory=RealDictCursor) as cur: + cur.execute(""" + SELECT customer_phone_normalized, customer_phone, created_at + FROM order_groups WHERE id = %s + """, (gid,)) + grow = cur.fetchone() or {} + phone = (grow.get("customer_phone_normalized") + or normalize_phone(grow.get("customer_phone") or "") + or phone) + group_created = grow.get("created_at") + # По исключённым номерам звонка не было — строку не пишем + # (статус «исключение» и так показывается из настроек). + if get_exclusion_match(conn, phone): + continue + scenario = resolve_scenario(group_created) if gid else "A" + with conn.cursor() as cur: + if gid: + cur.execute("SELECT count(*) FROM autocall_log WHERE order_group_id = %s", (gid,)) + attempt_no = (cur.fetchone()[0] or 0) + 1 + else: + attempt_no = 1 + test_mode = bool(q["is_test_call"] or settings.get("test_mode", True)) + with conn.cursor() as cur: + cur.execute(""" + INSERT INTO autocall_log + (order_group_id, customer_phone, attempt_no, scenario, + call_started_at, outcome, was_test_mode, channel_state) + VALUES (%s, %s, %s, %s, %s, 'no_answer', %s, 'NoAnswer') + """, (gid, phone, attempt_no, scenario, + q["processed_at"] or q["requested_at"], test_mode)) + conn.commit() + backfilled += 1 + log.info(f"Backfilled no_answer for queue item {q['id']} (phone={phone})") + return backfilled + + def main(): conn = get_db_conn() try: settings = load_settings(conn) + # Добор «не взял трубку»: заявки, по которым сеть так и не ответила + # (истёкший call-файл, AGI не запускался). Только работа с БД. + try: + nb = backfill_no_answer(conn, settings) + if nb: + log.info(f"Backfilled {nb} no_answer rows") + except Exception as e: + log.warning(f"backfill_no_answer failed: {e}") + # Fast-path: если кампания выключена и очередь пуста — выходим молча. # Таймер стучит каждую минуту; при 1440 тиках/сутки не хотим шум в логе. if not settings.get("enabled", False) and not has_pending_items(conn):