fix(autocall): исход «не взял трубку» — добор истёкших заявок + детект объявления оператора в AGI

This commit is contained in:
Hermes 2026-09-10 15:27:59 +02:00
parent 73b9512f15
commit 46a3609233
2 changed files with 107 additions and 0 deletions

View File

@ -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

View File

@ -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):