Каждая попытка и результат видны в карточке заказа; ручной звонок идёт клиенту и показывает своё состояние
Что осталось решить
Праздники и точный график повторов — по вашим правилам
Момент включения боевого обзвона клиентов и по самовывозу
+
+
diff --git a/docs/autocall/deck_content.py b/docs/autocall/deck_content.py
new file mode 100644
index 0000000..50f9896
--- /dev/null
+++ b/docs/autocall/deck_content.py
@@ -0,0 +1,349 @@
+"""Содержание презентации «Автодозвон: как это работает».
+
+Один источник правды: и PDF (how-it-works.py), и HTML (render-html.py) собираются
+отсюда, поэтому версии не разъезжаются при правках.
+
+Факты сверены с кодом: scripts/autocall_scheduler.py, docs/ats/ss-dialog.agi,
+supabase/functions/autocall-result/index.ts и настройками боевой системы.
+"""
+
+ACCENT, ACCENT2, WARN, DANGER = "accent", "accent2", "warn", "danger"
+
+TITLE_SLIDE = {
+ "kind": "title",
+ "kicker": "СУПЕР САМ · ДОСТАВКА И САМОВЫВОЗ",
+ "title": "Автодозвон: как это работает",
+ "sub": "Робот звонит клиенту, согласует день и половину дня — без участия оператора",
+ "lines": [
+ "Что описано: правила звонка, паузы, поведение в нештатных случаях (не дозвонились,",
+ "сбросили трубку, не нажали цифру), где что настраивается и что видит менеджер.",
+ "",
+ "Основано на текущем коде и настройках боевой системы, без обещаний и продажи.",
+ ],
+}
+
+SLIDES = [
+ {
+ "kind": "cards2full",
+ "title": "Что делает робот",
+ "sub": "Коротко: он не продаёт и не консультирует — только согласует день и половину дня",
+ "cards": [
+ {"title": "Три задачи", "accent": ACCENT, "items": [
+ "Дозвониться, когда заказ готов и клиент не подтвердил день по ссылке из SMS",
+ "Предложить конкретные дни голосом и получить выбор цифрой",
+ "Записать выбор в заказ — дату, половину дня и признак «согласовано роботом»",
+ ]},
+ {"title": "Чего он не делает", "accent": WARN, "items": [
+ "Не соединяет с живым менеджером — такой цифры в диалоге нет",
+ "Не спорит и не уговаривает: нет ответа — повторит по расписанию",
+ "Не предлагает нерабочие дни и не звонит вне рабочего времени",
+ ]},
+ ],
+ "full": {"title": "Как это выглядит для клиента", "accent": ACCENT2, "items": [
+ "Звонок с одного номера, голос один и тот же: «Ваш заказ готов. Выберите день и время»",
+ "Клиент нажимает цифру дня и цифру половины дня — робот подтверждает выбор вслух",
+ "После подтверждения даёт возможность изменить выбор (нажатием цифры 2)",
+ ]},
+ },
+ {
+ "kind": "steps",
+ "title": "Путь заказа до звонка",
+ "sub": "Когда именно звучит звонок",
+ "steps": [
+ ("1. Заказ готов", "менеджер отмечает\nготовность в системе"),
+ ("2. SMS со ссылкой", "клиент может выбрать\nдень сам, без звонка"),
+ ("3. Пауза 90 минут", "настройка админки:\nстолько ждём ответа"),
+ ("4. Звонок робота", "если клиент не\nподтвердил по ссылке"),
+ ("5. Результат в заказ", "дата и половина дня\nв самом заказе"),
+ ],
+ "full": {"title": "Важно", "accent": WARN, "items": [
+ "Если клиент подтвердил день по ссылке сам — звонка не будет: заказ уже согласован",
+ "Звонок идёт только в рабочее время и только в рабочие дни (настройки кампании)",
+ ]},
+ },
+ {
+ "kind": "cards2full",
+ "title": "Откуда берутся дни и время",
+ "sub": "Админка → Расписание → Рабочие дни: один источник правды",
+ "cards": [
+ {"title": "Что настраивается", "accent": ACCENT, "items": [
+ "Рабочие дни склада (самовывоз) и рабочие дни доставки",
+ "Часы: открытие и закрытие склада, часы доставки",
+ "«Первая половина — до» — граница между половинами дня",
+ "«Сегодня предлагаем до» — до какого времени предлагать сегодняшний день",
+ ]},
+ {"title": "Как это работает в коде", "accent": ACCENT2, "items": [
+ "Окно звонка считается рабочими днями: выходные не сокращают окно, а пропускаются",
+ "Робот называет реальные дни голосом: «сегодня», «в понедельник», «во вторник»",
+ "Клиентская ссылка использует те же дни — расхождений между звонком и ссылкой нет",
+ ]},
+ ],
+ "full": {"title": "Текущие значения в системе", "accent": WARN, "items": [
+ "Склад: пн–пт, 09:00–17:30, половина дня до 13:00, «сегодня» предлагаем до 15:30",
+ "Доставка: пн–пт (суббота — выходной) 09:00–21:00, половина дня до 15:00",
+ "Утро и вечер: до 13:00 робот спросит, какая половина дня удобнее; после 13:00 — сразу зафиксирует вторую",
+ ]},
+ },
+ {
+ "kind": "tablecard",
+ "title": "Самовывоз: какие дни предлагает",
+ "sub": "Окно — «день готовности + два рабочих дня», выходные пропускаются",
+ "table": {
+ "rows": [
+ ["Заказ готов", "Что предложит робот", "Почему так"],
+ ["в понедельник", "пн, вт, ср", "три рабочих дня подряд"],
+ ["в пятницу", "пт, пн, вт", "суббота и воскресенье — выходные склада"],
+ ["в субботу", "пн, вт, ср", "сам выходной не предлагаем"],
+ ["в пятницу после 15:30", "пн, вт", "«сегодня» больше не предлагаем (настройка)"],
+ ["в понедельник после 13:00", "пн, вт, ср", "половину дня не спрашиваем — сразу вторая"],
+ ["неделю назад", "звонка нет", "окно прошло — звонить не о чем"],
+ ],
+ "widths": [200, 240, 428],
+ },
+ "full": {"title": "Две настройки, которые это двигают", "accent": ACCENT, "items": [
+ "«Сегодня предлагаем до» (сейчас 15:30) — позже этого времени сегодняшний день исчезает из звонка и из ссылки",
+ "«Первая половина — до» (сейчас 13:00) — после неё робот не спрашивает половину дня, сразу пишет вторую",
+ ]},
+ },
+ {
+ "kind": "tablecard",
+ "title": "Доставка: не в день готовности",
+ "sub": "Нужен запас времени, чтобы собрать маршрут — поэтому начинаем со следующего дня",
+ "table": {
+ "rows": [
+ ["Заказ готов", "Что предложит робот"],
+ ["в понедельник", "вторник и среда"],
+ ["в пятницу", "понедельник и вторник (в субботу не возим)"],
+ ["в субботу", "понедельник и вторник"],
+ ["в четверг, звонок в пятницу", "пятница и понедельник"],
+ ["в пятницу, звонок в понедельник", "понедельник и вторник"],
+ ],
+ "widths": [300, 568],
+ },
+ "full": {"title": "Ключевое отличие от самовывоза", "accent": ACCENT2, "items": [
+ "В день готовности доставку не предлагаем никогда — только следующие рабочие дни",
+ "Суббота и воскресенье в графике доставки — выходные, их робот не назовёт",
+ "Время доставки — половина дня: до 15:00 или после (границу задаёт админка)",
+ ]},
+ },
+ {
+ "kind": "tablecard",
+ "title": "Как проходит разговор",
+ "sub": "Робот задаёт вопрос и ждёт цифру; молчание не считается согласием",
+ "table": {
+ "rows": [
+ ["Шаг", "Что говорит робот", "Что делает клиент", "Сколько ждёт"],
+ ["Приветствие", "«Ваш заказ готов. Выберите день и время доставки»", "слушает", "—"],
+ ["День", "«Нажмите один, чтобы доставить. В понедельник. Нажмите два…»", "нажимает 1 или 2", "9 секунд"],
+ ["Половина дня", "«Первая половина дня или вторая?»", "нажимает 1 или 2", "9 секунд"],
+ ["Подтверждение", "«Первая половина дня» — озвучивает выбор", "слушает", "—"],
+ ["Изменить?", "подсказка про кнопку 2 — один раз", "нажимает 2, чтобы переделать", "6 секунд"],
+ ],
+ "widths": [130, 360, 200, 178],
+ },
+ "full": {"title": "Что делает при ошибке ввода", "accent": WARN, "items": [
+ "Не понял цифру — повторяет тот же вопрос один раз",
+ "Снова не понял или молчит — завершает: «Вы не выбрали время. С вами свяжется менеджер»",
+ "Клиент нажимает 2 — диалог начинается заново («день и время»), прежний слот при этом не теряется",
+ ]},
+ },
+ {
+ "kind": "tablecard",
+ "title": "Что можно нажать",
+ "sub": "Всего несколько цифр — и одна из них «больше не звоните»",
+ "table": {
+ "rows": [
+ ["Цифра", "Значение", "Что происходит"],
+ ["1", "первый предложенный день", "робот переходит к выбору половины дня"],
+ ["2", "второй предложенный день", "робот переходит к выбору половины дня"],
+ ["3", "третий день (только самовывоз)", "робот переходит к выбору половины дня"],
+ ["1 / 2", "половина дня", "первая половина / вторая половина"],
+ ["9", "отказ от звонков", "робот больше не позвонит; в карточке — «отказ от звонков»"],
+ ["0 и прочие", "не принимаются", "вопрос повторяется; соединения с оператором нет"],
+ ],
+ "widths": [110, 240, 518],
+ },
+ "full": {"title": "Отдельно про «0»", "accent": DANGER, "items": [
+ "Раньше в диалоге была попытка «позвать менеджера», но она никогда не срабатывала (нажатие не отличалось от тишины)",
+ "Эту ветку убрали: живого соединения с оператором у робота нет, менеджер подключается только задачей из системы",
+ ]},
+ },
+ {
+ "kind": "cards2full",
+ "title": "Если не дозвонились",
+ "sub": "Робот различает «не взял трубку» и «вместо человека ответил автоответчик/оператор»",
+ "cards": [
+ {"title": "Как понимает, что трубку не взяли", "accent": WARN, "items": [
+ "Никто не ответил за 45 секунд — попытка считается неудачной",
+ "Трубку подняли, но гудки шли больше 20 секунд и ни одной цифры не нажали — это объявление оператора, а не человек: пишем «не взял трубку»",
+ "Живой человек почти всегда берёт трубку раньше, поэтому правило надёжно отделяет автоответчик",
+ ]},
+ {"title": "Что происходит дальше", "accent": ACCENT, "items": [
+ "Повторный звонок — не раньше чем через 120 минут (настройка)",
+ "Всего не больше 5 попыток на заказ (настройка)",
+ "Звонит только в рабочие часы кампании: 09:00–20:00, дни — по настройке",
+ "Исчерпали попытки — заказ уходит менеджеру, автозвонок по нему останавливается",
+ ]},
+ ],
+ "full": {"title": "Для менеджера это видно в карточке", "accent": ACCENT2, "items": [
+ "Каждая попытка — отдельная строка истории: время, результат, длительность разговора",
+ "Статус заказа меняется на «нужен менеджер» — с комментарием «не дозвонились / не нажал»",
+ "Отдельно красным предупреждаем, если заявка «Позвонить сейчас» висит больше 15 минут",
+ ]},
+ },
+ {
+ "kind": "tablecard",
+ "title": "Если сбросили трубку в середине разговора",
+ "sub": "Робот запоминает то, что успел услышать, и не портит уже согласованный слот",
+ "table": {
+ "rows": [
+ ["Когда положили трубку", "Что запишет робот", "Что увидит менеджер"],
+ ["до выбора дня", "ответил, ничего не нажал", "«взял, не нажал (день)» — попытка зачтена"],
+ ["день выбрал, время не успел", "день выбран, время нет", "«взял, не нажал (время)»"],
+ ["выбор подтвердил и сбросил", "слот согласован", "дата и половина дня в заказе, статус «согласовано»"],
+ ["начал менять выбор и сбросил", "остаётся прежний слот", "«правка не доведена» — прежнее согласование в силе"],
+ ],
+ "widths": [250, 250, 368],
+ },
+ "full": {"title": "Почему так", "accent": ACCENT2, "items": [
+ "Слот становится согласованным только после двух ответов: день и половина дня — это защита от «услышал половину фразы»",
+ "Обрыв не считается согласием: если клиент не подтвердил, робот перезвонит по расписанию или отдаст заказ менеджеру",
+ "Если клиент уже согласовал слот, а потом начал менять и бросил трубку — прежний выбор сохраняется, ничего не теряется",
+ ]},
+ },
+ {
+ "kind": "cards3full",
+ "title": "Когда подключается менеджер",
+ "sub": "Робот доводит дело до конца сам, но в трёх случаях передаёт человеку",
+ "cards": [
+ {"title": "Не нажал цифру 2 раза", "accent": WARN, "items": [
+ "Робот писал «взял, но не нажал» дважды",
+ "Заказ помечается «нужен менеджер», комментарий «не дозвонились / не нажал»",
+ "Порог «не нажал × N» настраивается в админке автодозвона",
+ ]},
+ {"title": "Исчерпаны 5 попыток", "accent": WARN, "items": [
+ "Каждый звонок — отдельная попытка",
+ "Когда попытки закончились, автозвонок прекращается",
+ "Заказ остаётся в списке у менеджера",
+ ]},
+ {"title": "Отказ или неверный номер", "accent": DANGER, "items": [
+ "Цифра 9 — клиент просит больше не звонить: ставим запрет автоматически",
+ "Неверный номер — задача менеджеру",
+ "Отказ клиента действует и на другие заказы: он в списке «не звонить»",
+ ]},
+ ],
+ "full": {"title": "Что видит менеджер в карточке заказа", "accent": ACCENT, "items": [
+ "Единый статус автодозвона и история всех попыток с результатами и слотами",
+ "Кнопка «Позвонить сейчас» — ручной звонок клиенту (не тестовый номер) и живое состояние: «в очереди», «звоним», «звонок сделан» с результатом",
+ "Кнопка «Не звонить» — если робот не должен беспокоить этого клиента",
+ ]},
+ },
+ {
+ "kind": "cards2full",
+ "title": "Когда робот не звонит вообще",
+ "sub": "Список условий, при которых звонка не будет — проверяются до набора номера",
+ "cards": [
+ {"title": "По клиенту и заказу", "accent": ACCENT, "items": [
+ "Клиент сам подтвердил день по ссылке — звонок не нужен",
+ "Клиент просил больше не звонить (нажал 9) или менеджер поставил «Не звонить»",
+ "Номер в списке исключений — например, на МТС-диапазон звонки не идут",
+ "Самовывоз: автообзвон по самовывозу включается отдельной галочкой (сейчас выключен)",
+ ]},
+ {"title": "По времени и географии", "accent": WARN, "items": [
+ "Вне рабочих часов кампании (09:00–20:00) и вне рабочих дней",
+ "Город исключён целиком или для города выключен канал",
+ "В админке автодозвона выключены звонки по городу — например, Ялта: SMS идут, звонков нет",
+ "Меньше 90 минут прошло после SMS со ссылкой",
+ ]},
+ ],
+ "full": {"title": "Админка автодозвона: где это переключается", "accent": ACCENT2, "items": [
+ "Панель «Автодозвон»: тестовый режим, часы и дни звонков, паузы, лимит попыток, порог «не нажал × N»",
+ "Панель «Локации»: для каждого города — звонки включены или выключены (SMS не затрагивается)",
+ "Панель исключений: номера и префиксы, на которые не звоним, с причиной",
+ ]},
+ },
+ {
+ "kind": "tablecard",
+ "title": "Что записывается в заказ",
+ "sub": "Результат звонка виден там же, где всё остальное",
+ "table": {
+ "rows": [
+ ["Поле", "Значение"],
+ ["Дата", "конкретная дата, которую назвал робот (например, понедельник 21.09)"],
+ ["Время", "«первая половина дня» или «вторая половина дня»"],
+ ["Источник", "«согласовано роботом» — видно, что выбор сделал клиент в звонке"],
+ ["Статус", "«согласовано» / «нужен менеджер» / «отказ от звонков»"],
+ ["Комментарий", "«Согласовано роботом: завтра, первая половина дня»"],
+ ],
+ "widths": [200, 668],
+ },
+ "full": {"title": "Зачем это нужно", "accent": ACCENT, "items": [
+ "Менеджер видит причину и источник решения, а не просто «дата стоит»",
+ "История звонков хранится отдельно: видно, сколько было попыток и чем они закончились",
+ "Дата, названная роботом, используется дальше в работе склада и доставки",
+ ]},
+ },
+ {
+ "kind": "tablecard",
+ "title": "Тестовый режим и текущее состояние",
+ "sub": "Сейчас обзвон клиентов выключен — система в тестовом режиме",
+ "table": {
+ "rows": [
+ ["Параметр", "Значение сейчас", "Что будет при запуске"],
+ ["Обзвон клиентов", "выключен", "включается отдельной командой"],
+ ["Тестовый номер", "звонит только на него", "массовый обзвон идёт клиентам"],
+ ["Кнопка «Позвонить сейчас»", "звонит клиенту сразу", "так же — звонок клиенту"],
+ ["Звонки по самовывозу", "выключены галочкой", "включаются отдельно от доставки"],
+ ],
+ "widths": [230, 280, 358],
+ },
+ "full": {"title": "Как проверяется перед запуском", "accent": ACCENT2, "items": [
+ "Тестовый звонок на свой номер: диалог, дни, половина дня, запись результата",
+ "Прогон диалога без звонка: проверка всех веток (согласие, обрыв, молчание, отказ)",
+ "Проверка окон: робот предлагает именно те дни, которые заданы в админке",
+ ]},
+ },
+ {
+ "kind": "cards2full",
+ "title": "Что пока не учитывается",
+ "sub": "Честный список: это важно знать до запуска",
+ "cards": [
+ {"title": "График", "accent": WARN, "items": [
+ "Праздники: календаря нерабочих дат нет — если праздник попадёт в окно, робот его предложит",
+ "Часы доставки используются частично: границу половины дня робот проговаривает словами, без конкретного времени",
+ ]},
+ {"title": "Настройки, которые пока не влияют", "accent": DANGER, "items": [
+ "«Попыток в день» — поле есть, но расписание повторов задаётся интервалом между попытками",
+ "«Попытки следующего дня (10:00, 14:00)» — пока не используются",
+ "«Таймаут звонка, 30 секунд» — звонок ждёт ответа 45 секунд (значение в самом звонке)",
+ ]},
+ ],
+ "full": {"title": "Что предлагаем уточнить до боевого запуска", "accent": ACCENT2, "items": [
+ "Список праздников (или решение не учитывать их)",
+ "Нужен ли жёсткий график повторов по времени, кроме интервала 120 минут",
+ "Оставить ли «0» без реакции — сейчас это просто неверная цифра",
+ ]},
+ },
+ {
+ "kind": "cards2full",
+ "title": "Коротко",
+ "sub": "Одна страница вместо всей презентации",
+ "cards": [
+ {"title": "Что делает система", "accent": ACCENT, "full": True, "dense": True, "items": [
+ "Ждёт 90 минут после SMS и звонит, если клиент не подтвердил день сам",
+ "Предлагает только рабочие дни по графику: самовывоз — включая сегодня, доставка — со следующего дня",
+ "Спрашивает день и половину дня, подтверждает вслух, даёт один раз переделать выбор",
+ "Повторяет попытки не чаще чем раз в 2 часа, максимум 5 раз, только 09:00–20:00 в рабочие дни",
+ "После двух «взял, но не нажал» или исчерпания попыток передаёт заказ менеджеру",
+ "Отказ (цифра 9) останавливает обзвон клиента навсегда",
+ "Каждая попытка и результат видны в карточке заказа; ручной звонок идёт клиенту и показывает своё состояние",
+ ]},
+ ],
+ "full": {"title": "Что осталось решить", "accent": WARN, "items": [
+ "Праздники и точный график повторов — по вашим правилам",
+ "Момент включения боевого обзвона клиентов и по самовывозу",
+ ]},
+ },
+]
+
+ALL_SLIDES = [TITLE_SLIDE] + SLIDES
diff --git a/docs/autocall/render-html.py b/docs/autocall/render-html.py
new file mode 100644
index 0000000..0bb494c
--- /dev/null
+++ b/docs/autocall/render-html.py
@@ -0,0 +1,188 @@
+#!/usr/bin/env python3
+"""HTML-версия презентации «Автодозвон: как это работает» (тот же текст, что в PDF).
+
+Содержание берётся из deck_content.py, поэтому PDF и HTML не разъезжаются.
+Файл самодостаточный: без внешних шрифтов, картинок и скриптов — открывается
+двойным щелчком и с телефона, и печатается в PDF по одному слайду на страницу.
+
+Запуск: python3 docs/autocall/render-html.py
+"""
+from html import escape
+from pathlib import Path
+
+import deck_content as content
+
+OUT = Path(__file__).with_name("autocall-how-it-works.html")
+
+TONE = {
+ "accent": "#4ADE80",
+ "accent2": "#60A5FA",
+ "warn": "#FBBF24",
+ "danger": "#F87171",
+}
+
+
+def esc(text):
+ return escape(str(text))
+
+
+def card_html(card, extra_class=""):
+ tone = TONE.get(card.get("accent", "accent"), TONE["accent"])
+ items = "".join(f"
{esc(i)}
" for i in card["items"])
+ classes = " ".join(f"card {extra_class} {'dense' if card.get('dense') else ''}".split())
+ return (
+ f''
+ f'
' for w in widths) if widths else ""
+ head_html = "".join(f"
{esc(c)}
" for c in head)
+ body_html = "".join(
+ "
" + "".join(f"
{esc(c)}
" for c in row) + "
" for row in body
+ )
+ return (
+ f'
{"
" + colgroup + "
" if colgroup else ""}'
+ f"
{head_html}
{body_html}
"
+ )
+
+
+def steps_html(steps):
+ out = []
+ for i, (title, desc) in enumerate(steps):
+ lines = "".join(f"{esc(line)}" for line in desc.split("\n"))
+ arrow = '
'
+ body = head
+ if kind == "steps":
+ body += steps_html(slide["steps"])
+ elif kind in ("cards2full", "cards3full"):
+ cards = "".join(card_html(c) for c in slide["cards"])
+ body += f'
{cards}
'
+ elif kind == "tablecard":
+ body += table_html(slide["table"])
+ full = slide.get("full")
+ if full:
+ body += card_html(full, extra_class="full")
+
+ return f'{body}{parts[0]}'
+
+
+def build():
+ slides = "".join(slide_html(s, i + 1) for i, s in enumerate(content.ALL_SLIDES))
+ html = f"""
+
+
+
+
+Автодозвон SuperSam — как это работает
+
+
+
+{slides}
+
+
+"""
+ OUT.write_text(html, encoding="utf-8")
+ print(f"готово: {OUT} — слайдов {len(content.ALL_SLIDES)}")
+
+
+if __name__ == "__main__":
+ build()