Обращение открывается кнопкой «🆘 Поддержка» или /support в режиме «Помощь».
Отдельного FSM-режима нет: пока обращение активно, внизу остаётся только
«Главное меню», а переключение режимов отклоняется.
- support.py: личка пользователя ↔ персональный топик группы SUPPORT_GROUP_ID.
Топик создаётся один раз на пользователя и переиспользуется, поэтому
повторные обращения продолжают ту же переписку. Пересылаются все типы
контента через copy_message; ответ оператора из топика уходит в личку;
сообщение с префиксом // остаётся внутренней заметкой. Функция выключена
без SUPPORT_GROUP_ID.
- Карточка профиля уходит первым сообщением топика: аватарка, имя, username,
id и сводка — сколько дней пользуется ботом, сколько продлений подписки,
сколько дней осталось, обучен ли бот (user_store.support_profile_summary).
- Бот не объясняет пользователю, что «передаёт оператору»: в ответ приходит
случайная фраза из пула SUPPORT_GREETINGS (activity_phrases.support_greeting).
Формулировки безличные, без «я/бот/оператор»; пул переопределяется через
SUPPORT_GREETINGS без деплоя.
Проверено: 207 тестов OK (30 новых в tests/test_support.py).
VERSION: 0.1.0+51 -> 0.1.0+52
157 lines
8.5 KiB
Python
157 lines
8.5 KiB
Python
"""Пулы фраз, которыми бот обозначает ход долгой операции.
|
|
|
|
Фразы показываются в живом драфте (``sendMessageDraft``) в приватном чате и
|
|
сменяют друг друга случайным образом через случайные промежутки времени, пока
|
|
операция не завершилась. Ни одна фраза не обещает конкретного результата — это
|
|
только обозначение процесса.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import random
|
|
|
|
GENERATION_PHRASES: tuple[str, ...] = (
|
|
"⏳ Разбираю запрос и подбираю формулировки…",
|
|
"⏳ Собираю фактуру из того, что ты прислал…",
|
|
"⏳ Придумываю три разных захода к теме…",
|
|
"⏳ Проверяю, что не придумываю лишних фактов…",
|
|
"⏳ Подбираю более точное начало поста…",
|
|
"⏳ Слежу за длиной и читаемостью каждого варианта…",
|
|
"⏳ Убираю канцелярит и лишние вводные…",
|
|
"⏳ Собираю три варианта и проверяю орфографию…",
|
|
)
|
|
|
|
TRAINING_PHRASES: tuple[str, ...] = (
|
|
"⏳ Читаю присланные материалы…",
|
|
"⏳ Выделяю особенности твоей подачи…",
|
|
"⏳ Сопоставляю новые примеры с прежним профилем…",
|
|
"⏳ Уточняю портрет аудитории…",
|
|
"⏳ Отмечаю типичные темы и повороты…",
|
|
"⏳ Собираю обновлённый профиль аккаунта…",
|
|
"⏳ Проверяю, что ничего не потерялось…",
|
|
"⏳ Упаковываю вывод в компактный профиль…",
|
|
)
|
|
|
|
IMAGES_PHRASES: tuple[str, ...] = (
|
|
"⏳ Скачиваю изображения…",
|
|
"⏳ Распознаю тексты на скриншотах…",
|
|
"⏳ Отделяю заголовки от основного текста…",
|
|
"⏳ Сверяю извлечённые посты между собой…",
|
|
"⏳ Ищу повторяющиеся приёмы в текстах…",
|
|
"⏳ Проверяю, что тексты прочитаны полностью…",
|
|
"⏳ Готовлю варианты поста по увиденному…",
|
|
)
|
|
|
|
VOICE_PHRASES: tuple[str, ...] = (
|
|
"⏳ Скачиваю аудиосообщение…",
|
|
"⏳ Перевожу речь в текст…",
|
|
"⏳ Расставляю знаки препинания в расшифровке…",
|
|
"⏳ Уточняю границы фраз…",
|
|
"⏳ Проверяю расшифровку на слух…",
|
|
"⏳ Разбираю, что именно ты просишь…",
|
|
)
|
|
|
|
IDEAS_PHRASES: tuple[str, ...] = (
|
|
"⏳ Ищу свежий угол для темы…",
|
|
"⏳ Проверяю, что идея не повторяет прошлые…",
|
|
"⏳ Подбираю формулировку заголовка…",
|
|
"⏳ Оцениваю пользу идеи для твоей аудитории…",
|
|
"⏳ Формулирую, зачем эта тема аккаунту…",
|
|
"⏳ Отбираю самый сильный вариант…",
|
|
)
|
|
|
|
DEFAULT_PHRASES: tuple[str, ...] = (
|
|
"⏳ Работаю над запросом…",
|
|
"⏳ Привожу результат к нужному виду…",
|
|
"⏳ Почти готова, проверяю детали…",
|
|
)
|
|
|
|
# Фразы, с которых живой оператор начинает диалог в поддержке. Бот не должен
|
|
# рассказывать пользователю про «передачу оператору» — он просто здоровается
|
|
# и приглашает описать проблему: для пользователя это начало разговора с человеком.
|
|
# Формулировки безличные или без глаголов в первом лице, чтобы подходить
|
|
# любому оператору и не спорить с голосом бота.
|
|
SUPPORT_GREETINGS: tuple[str, ...] = (
|
|
"Здравствуйте! На связи поддержка. Расскажите, что случилось — разберёмся.",
|
|
"Добрый день! Слушаю вас: опишите проблему, и постараемся помочь.",
|
|
"Приветствую! Чем могу помочь? Опишите ситуацию подробнее.",
|
|
"Здравствуйте! На связи. Что пошло не так — напишите, посмотрим.",
|
|
"Добрый день! На связи поддержка. Опишите, с чем возникла сложность.",
|
|
"Здравствуйте! Слушаю. Расскажите, что случилось, — постараемся решить.",
|
|
"Приветствую! Готовы помочь. Опишите проблему, и разберёмся вместе.",
|
|
"Добрый день! На связи. Напишите, что не работает, — посмотрим.",
|
|
"Здравствуйте! Поддержка на связи. Опишите вопрос — поможем разобраться.",
|
|
"Приветствую! На связи. С чего начнём — опишите, что случилось.",
|
|
)
|
|
|
|
SUPPORT_GREETING_FALLBACK = "Здравствуйте! На связи поддержка. Расскажите, что случилось."
|
|
|
|
ACTIVITY_PHRASES: dict[str, tuple[str, ...]] = {
|
|
"generation": GENERATION_PHRASES,
|
|
"training": TRAINING_PHRASES,
|
|
"images": IMAGES_PHRASES,
|
|
"voice": VOICE_PHRASES,
|
|
"ideas": IDEAS_PHRASES,
|
|
}
|
|
|
|
|
|
def _positive_float(name: str, default: float) -> float:
|
|
raw = os.getenv(name, "").strip()
|
|
if not raw:
|
|
return default
|
|
try:
|
|
value = float(raw)
|
|
except ValueError:
|
|
return default
|
|
return value if value > 0 else default
|
|
|
|
|
|
def _positive_int(name: str, default: int) -> int:
|
|
raw = os.getenv(name, "").strip()
|
|
if not raw:
|
|
return default
|
|
try:
|
|
value = int(raw)
|
|
except ValueError:
|
|
return default
|
|
return value if value > 0 else default
|
|
|
|
|
|
ACTIVITY_MIN_SECONDS = _positive_float("ACTIVITY_MIN_SECONDS", 3.0)
|
|
ACTIVITY_MAX_SECONDS = _positive_float("ACTIVITY_MAX_SECONDS", 8.0)
|
|
if ACTIVITY_MIN_SECONDS > ACTIVITY_MAX_SECONDS:
|
|
ACTIVITY_MIN_SECONDS, ACTIVITY_MAX_SECONDS = ACTIVITY_MAX_SECONDS, ACTIVITY_MIN_SECONDS
|
|
|
|
# Предохранитель: если операция зависла и finish() не вызвался, ротация
|
|
# всё равно прекратится и не будет бесконечно долбить драфт.
|
|
ACTIVITY_MAX_ROTATIONS = _positive_int("ACTIVITY_MAX_ROTATIONS", 12)
|
|
|
|
|
|
def random_interval() -> float:
|
|
"""Случайная пауза между сменами фраз, в секундах."""
|
|
return random.uniform(ACTIVITY_MIN_SECONDS, ACTIVITY_MAX_SECONDS)
|
|
|
|
|
|
def next_phrase(kind: str | None = None, exclude: str | None = None) -> str:
|
|
"""Случайная фраза из пула операции, не повторяющая только что показанную."""
|
|
pool = ACTIVITY_PHRASES.get(kind or "", DEFAULT_PHRASES)
|
|
if exclude is None or len(pool) < 2:
|
|
return random.choice(pool)
|
|
variants = tuple(phrase for phrase in pool if phrase != exclude)
|
|
return random.choice(variants)
|
|
|
|
|
|
def support_greeting() -> str:
|
|
"""Случайная фраза начала диалога поддержки.
|
|
|
|
Пул задаётся переменной окружения SUPPORT_GREETINGS (разделитель — `|`),
|
|
чтобы оператор мог поменять формулировки без деплоя. Пустые строки
|
|
отбрасываются; если пул пуст, возвращается SUPPORT_GREETING_FALLBACK.
|
|
"""
|
|
raw = os.getenv("SUPPORT_GREETINGS", "").strip()
|
|
if raw:
|
|
pool = tuple(item.strip() for item in raw.split("|") if item.strip())
|
|
if pool:
|
|
return random.choice(pool)
|
|
return random.choice(SUPPORT_GREETINGS) if SUPPORT_GREETINGS else SUPPORT_GREETING_FALLBACK
|