fix(autocall): исход «не взял трубку» — добор истёкших заявок + детект объявления оператора в AGI
This commit is contained in:
parent
73b9512f15
commit
46a3609233
|
|
@ -28,6 +28,7 @@ say () { echo "$1"; read -r resp; echo "$(date +%T) $1 -> $resp" >> "$LOG"; RESP
|
||||||
digit () { # $1=озвучка, $2=таймаут мс -> DIGIT
|
digit () { # $1=озвучка, $2=таймаут мс -> DIGIT
|
||||||
say "GET DATA $1 ${2:-9000} 1"
|
say "GET DATA $1 ${2:-9000} 1"
|
||||||
DIGIT=$(echo "$RESP" | sed -n 's/.*result=\([0-9]*\).*/\1/p')
|
DIGIT=$(echo "$RESP" | sed -n 's/.*result=\([0-9]*\).*/\1/p')
|
||||||
|
[ -n "$DIGIT" ] && ANY_DIGIT=1
|
||||||
}
|
}
|
||||||
# Чтение channel-переменной из call-файла (Setvar) через AGI.
|
# Чтение channel-переменной из call-файла (Setvar) через AGI.
|
||||||
getvar () { # $1=имя -> VAR
|
getvar () { # $1=имя -> VAR
|
||||||
|
|
@ -104,6 +105,19 @@ save
|
||||||
say "ANSWER"
|
say "ANSWER"
|
||||||
say "EXEC Wait 1"
|
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: подтверждение уже выбранного слота ───────────────────────────
|
# ─── C: подтверждение уже выбранного слота ───────────────────────────
|
||||||
if [ "$SCENARIO" = "C" ]; then
|
if [ "$SCENARIO" = "C" ]; then
|
||||||
play "ru/ss_c_intro_${TYPE}"
|
play "ru/ss_c_intro_${TYPE}"
|
||||||
|
|
@ -142,6 +156,9 @@ while [ -z "$OUTCOME" ]; do
|
||||||
elif [ "$DAY" = "0" ]; then
|
elif [ "$DAY" = "0" ]; then
|
||||||
play "ru/ss_operator"; OUTCOME=operator; break
|
play "ru/ss_operator"; OUTCOME=operator; break
|
||||||
elif [ "$DAY" != "1" ] && [ "$DAY" != "2" ]; then
|
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
|
play "ru/ss_no_input"; OUTCOME=no_input_day; break
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -162,6 +179,9 @@ while [ -z "$OUTCOME" ]; do
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$HALF" != "1" ] && [ "$HALF" != "2" ]; then
|
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
|
play "ru/ss_no_input"; OUTCOME=no_input_half; break
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -315,11 +315,98 @@ def has_pending_items(conn):
|
||||||
return cur.fetchone() is not None
|
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():
|
def main():
|
||||||
conn = get_db_conn()
|
conn = get_db_conn()
|
||||||
try:
|
try:
|
||||||
settings = load_settings(conn)
|
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: если кампания выключена и очередь пуста — выходим молча.
|
# Fast-path: если кампания выключена и очередь пуста — выходим молча.
|
||||||
# Таймер стучит каждую минуту; при 1440 тиках/сутки не хотим шум в логе.
|
# Таймер стучит каждую минуту; при 1440 тиках/сутки не хотим шум в логе.
|
||||||
if not settings.get("enabled", False) and not has_pending_items(conn):
|
if not settings.get("enabled", False) and not has_pending_items(conn):
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue