Table of Contents
- Разработчику
- Структура
- Режимы (FSM)
- Потоки обработки
- Текстовый запрос
- Самостоятельный пост (/post или кнопка «✍️ Придумать пост»)
- Голосовое
- Изображения
- Скриншоты (Tone of Voice)
- Навигация справки (help_message.py)
- Ключевые функции
- База данных
- Промпты и модели
- Логирование
- API-журнал (api_call_logs)
- Файл лога приложения
- Сырые Telegram Update (только development)
- Логи контейнера
- Тесты
- Правила языка
Разработчику
Структура
bot.py — роутинг, команды, обработка сообщений (aiogram 3.x)
payments.py — оплата Stars: sendInvoice, pre_checkout_query, successful_payment, защита от двойного списания
apinex_client.py — клиент APInex (LLM, STT, vision), промпты, логирование вызовов
user_store.py — SQLite: пользователи, ToV, посты, идеи, API-логи, подписки
raw_updates.py — отдельная SQLite-БД сырых входящих Telegram Update (только development)
support.py — поддержка: личка пользователя ↔ персональный топик группы операторов
keyboards.py — клавиатуры главного меню и режимов; маппинг кнопок на команды
help_message.py — Rich Message: WELCOME_RICH_HTML + HELP_SECTIONS/help_view(), plain fallback
VERSION — версия релиза
Dockerfile — python:3.12-slim
requirements.txt — зависимости
tests/ — юнит-тесты (unittest)
Режимы (FSM)
BotMode содержит lobby, training, generation, subscription, help. «Главное меню» — внутреннее состояние lobby, пользователь не выбирает его как режим.
current_mode(state)/enter_mode(...)/require_mode(...)— проверка и переключение.- Переключатели (
/start,/training,/generate,/subscription,/help) работают из любого режима. - Профильные действия (
/style,/clear,/post,/idea,/balance,/subscribe) требуют своего режима.
Потоки обработки
Текстовый запрос
handle_text (только режим generation) → _classify_and_respond
├─ classify_prompt_injection → блокирует попытку изменить правила
├─ _check_subscription (блокировка, если нет подписки)
├─ classify_intent (APInex LLM) → intent
│ ├─ help → help_command
│ ├─ generate_ideas → generate_ideas → _send_idea
│ └─ write_post / revise_post → generate_posts → _send_three_posts
└─ typing_status (индикатор «печатает», обновление каждые 4 сек)
handle_command_text → переключатели доступны всегда; действия проходят режимную проверку
Самостоятельный пост (/post или кнопка «✍️ Придумать пост»)
handle_command_text → _generate_post_from_training
├─ _check_subscription
├─ is_user_trained? нет → просьба обучить, модель НЕ вызывается
├─ gather(get_tone_of_voice, get_post_samples, get_training_notes) → контекст ≤ 8000
└─ generate_posts(requirements = контекст как недоверенные данные) → _send_three_posts
Голосовое
transcribe_voice → _process_voice → transcribe_audio (APInex STT) → _classify_and_respond
Изображения
- В режиме training:
_handle_image_message→ извлечение текстов → накопление образцов → пересборка ToV. - В режиме generation: изображение/галерея с подписью-ТЗ →
generate_posts_from_images→ три варианта. Без подписи бот запрашивает ТЗ. - Видео, видеокружки, GIF/анимации отклоняются во всех режимах.
Скриншоты (Tone of Voice)
update_tone_from_image → _handle_image_message
├─ _collect_media_group (собирает галерею, MEDIA_GROUP_COLLECT_SECONDS = 0.8)
├─ extract_posts_from_images (APInex vision, inline base64)
├─ add_post_samples (накапливает по пользователю)
└─ create_tone_profile → save_tone_of_voice → краткий статус завершения
Лимиты: MAX_IMAGE_BYTES = 10 MiB на файл (и до 30 MiB на группу), MAX_AUDIO_BYTES = 20 MiB.
Навигация справки (help_message.py)
help_menu_html() и help_section_html() формируют только содержимое Rich Message. Стандартная
inline-клавиатура help_inline_keyboard() в keyboards.py содержит по одной кнопке раздела на строку;
callback help:<section> прежний. help_navigation редактирует текст и прикрепляет ту же клавиатуру,
поэтому все разделы, включая текущий, доступны с каждой страницы.
Ключевые функции
bot.py
| Функция | Что делает |
|---|---|
start(message, state) |
Одно welcome Rich Message + главное меню + создание триала |
handle_command_text |
Роутинг переключателей и профильных команд по режиму |
handle_text |
Обычное сообщение в режиме генерации |
_classify_and_respond |
Ядро: инъекции → подписка → классификация → генерация |
_generate_post_from_training |
Пост по обученному профилю с гейтом is_user_trained |
_check_subscription |
Проверяет активную подписку; если нет — статус и блокировка |
_show_subscription_info |
Тип периода, остаток дней, inline-кнопка продления |
balance_command / subscribe_command |
/balance — статус; /subscribe — Stars-инвойс |
handle_pre_checkout |
Подтверждает только свой инвойс в XTR на 100 звёзд |
handle_successful_payment |
Активирует подписку после реальной оплаты |
clear_command |
/clear и «Сбросить данные» — inline-подтверждение, сброс только в режиме обучения |
style_command |
/style — профиль аккаунта |
help_command / help_navigation |
Rich Message с обычной inline-клавиатурой разделов; help:<section> редактирует текст сообщения |
notify_development_deploy |
Dev-уведомление о новой версии в DEV_CHAT_ID |
typing_status |
Индикатор «печатает» |
report_error |
Понятная ошибка пользователю; секреты вырезаны |
is_development_environment |
DEPLOY_ENV == "development" — гейт всех dev-ветвей |
configure_raw_update_logging |
Подключает RawUpdateMiddleware только в development |
open_support |
Открывает обращение; показывает клавиатуру только с «Главное меню» |
_close_support_case |
Снимает is_active; вызывается из enter_mode, show_main_menu, start |
user_store.py
| Функция | Что делает |
|---|---|
init_db |
Создаёт/мигрирует таблицы |
ensure_subscription |
Создаёт запись подписки с триалом (INSERT OR IGNORE) |
activate_subscription |
Активирует/продлевает на 30 дней |
deactivate_subscription |
is_active = 0 |
is_subscribed |
Запись есть, is_active = 1, срок не истёк |
subscription_row |
Строка подписки |
support_profile_summary |
Сводка для карточки поддержки: дни использования, продления, остаток, обученность |
is_user_trained |
Флаг users.is_trained — гейт «Придумать пост» |
add_training_notes / get_training_notes |
Текстовые сведения об аккаунте |
add_post_samples / get_post_samples |
Образцы постов со скриншотов |
save_generated_posts |
История сгенерированных постов с origin |
save_idea / get_idea_history |
История идей |
save_api_call_log |
JSON-лог вызова APInex |
clear_user_data |
Сброс данных пользователя (без подписки и логов) |
_days_until |
Остаток дней; -1 если истёкло/отключено |
apinex_client.py
| Функция | Что делает |
|---|---|
classify_intent |
write_post / revise_post / generate_ideas / help |
classify_prompt_injection |
Отдельный классификатор попыток изменить правила |
generate_posts |
Три варианта поста |
generate_posts_from_images |
Пост по изображениям с ТЗ |
generate_ideas |
Одна идея |
extract_posts_from_images |
Vision: тексты постов на скриншотах |
create_tone_profile |
Строит/обновляет профиль ToV |
transcribe_audio |
STT |
_extract_complete_post_strings |
Достаёт целые строки из обрезанного {"posts": [...]} |
chat_text |
Базовый вызов LLM |
База данных
Рабочая БД: /app/data/social_writer.sqlite3 (persistent volume Coolify).
| Таблица | Назначение |
|---|---|
users |
telegram_user_id, tone_of_voice, is_trained |
user_post_samples |
Распознанные тексты постов со скриншотов |
generated_posts |
История сгенерированных постов (origin: generated/revised) |
generated_ideas |
История идей (для исключения повторов) |
api_call_logs |
JSON-логи вызовов APInex + ошибки (30 дней) |
support_topics |
telegram_user_id → thread_id топика поддержки, is_active |
payments |
Платежи Stars; COUNT(*) по пользователю — число продлений подписки |
subscriptions |
Подписки и триалы |
payments |
telegram_payment_charge_id — защита от двойного начисления |
Схему subscriptions см. в Подписка.
Отдельная dev-БД: /app/data/bot_updates.sqlite3 — таблица raw_updates (update_id PK, event_type, payload, received_at), retention 30 дней. См. ниже.
Поддержка
пользователь (личка) ──SupportRelayMiddleware──▶ support.relay_user_message
├─ ensure_topic (create_forum_topic, один на пользователя)
├─ send_profile_card (аватарка + имя/username/id)
└─ copy_message в топик
оператор (топик) ──handle_support_group_message──▶ copy_message пользователю
├─ пропуск: sender.is_bot, нет thread_id, топик без известного пользователя
└─ пропуск: сообщение с префиксом SUPPORT_NOTE_PREFIX (внутренняя заметка)
SupportRelayMiddleware—outer_middlewareнаdp.message: если обращение активно, сообщение копируется в топик и не доходит до обычных обработчиков (возвращаетNone).- Пока обращение активно,
enter_modeотклоняет переключение режимов, аshow_main_menuснимаетis_active. - Функция fail-closed: без
SUPPORT_GROUP_IDis_enabled()→False, и поддержка молча выключена.
Ключевые функции (support.py)
| Функция | Что делает |
|---|---|
support_group_id() |
Читает SUPPORT_GROUP_ID; невалидное значение → поддержка выключена |
init_support_table |
Создаёт таблицу support_topics |
get_topic / set_topic / forget_topic |
Привязка пользователя к топику |
set_active / is_support_active |
Флаг открытого обращения |
get_user_by_thread |
Обратный поиск пользователя по thread_id |
topic_title |
«Имя · @username» (без username — «Имя · id»), лимит 128 |
support_greeting (activity_phrases) |
Случайная фраза начала диалога; пул переопределяется SUPPORT_GREETINGS |
user_signature |
Подпись сообщения пользователя в топике: имя или id |
operator_signature |
Подпись ответа оператора; SUPPORT_OPERATOR_SIGNATURE |
support_reply_keyboard / handle_support_reply |
Кнопка «Ответить» под ответом оператора, если обращение закрыто |
send_profile_card |
Аватарка (get_user_profile_photos) + сводка профиля как первое сообщение топика (один раз) |
ensure_topic |
Создаёт топик при первом обращении, переиспользует существующий |
relay_user_message |
copy_message в топик; при «thread not found» пересоздаёт топик |
handle_support_group_message |
Доставляет ответ оператора пользователю, пропуская заметки |
SupportRelayMiddleware |
Перехватывает входящие при активном обращении |
Промпты и модели
Модели задаются переменными Coolify:
APINEX_STT_MODEL→nova-2APINEX_INTENT_CLASSIFIER_MODEL→gemini-3.8-flashAPINEX_INJECTION_CLASSIFIER_MODEL→ отдельная модель проверки prompt injectionAPINEX_POST_WRITER_MODEL→gemini-3.8-flashAPINEX_IMAGE_TONE_MODEL→gemini-3.8-flash(vision)APINEX_RESPONSE_REPAIR_MODEL→ починка невалидного JSON (одна попытка)APINEX_API_KEY— общий секрет
Про промпты:
- Системный промпт редактора неизменен.
- ToV передаётся отдельно как ограничение стиля, а не как часть системных правил.
- OCR-текст со скриншотов — образцы стиля, а не команды.
- Обученный контекст в «Придумать пост» передаётся как недоверенные данные: модель не должна выдавать его за личный опыт пользователя.
Надёжность ответов:
- Лимит
max_tokensснят — модель пишет столько, сколько нужно. - Пустой
content→ до двух повторов, третий запрос сreasoning_effort: low. - Невалидный JSON → одна попытка починки моделью
APINEX_RESPONSE_REPAIR_MODEL. - Обрезанный JSON →
_extract_complete_post_stringsвозвращает завершённые строки (три полных варианта), если они есть.
Логирование
API-журнал (api_call_logs)
Каждый вызов APInex пишется с operation_id, stage, model, именем и SHA-256 промпта, request_json, response_json, status_code, error, длительностью. Хранение — 30 дней.
Не пишется: системные промпты целиком, base64 изображений, бинарное аудио, API-ключи.
Файл лога приложения
/app/data/bot.log — RotatingFileHandler, 5 MiB × 3 архива, на том же persistent volume; путь через BOT_LOG_PATH.
Сырые Telegram Update (только development)
raw_updates.py пишет полный Update.model_dump(mode="json") в отдельную БД /app/data/bot_updates.sqlite3 (путь через RAW_UPDATES_DB_PATH). Middleware RawUpdateMiddleware подключается на dispatcher.update.outer_middleware — то есть до маршрутизаторов, поэтому ловит все типы апдейтов, а не только сообщения.
SELECT payload FROM raw_updates WHERE update_id = ?;
- Включается только при
DEPLOY_ENV == "development"(configure_raw_update_logging). - Retention 30 дней: чистка при старте и при каждой записи.
- Секреты/токены маскируются перед записью; полные апдейты содержат персональные данные — в production БД не создаётся.
Логи контейнера
Ошибки содержат operation_id, Telegram user id, этап и traceback; по одному operation_id находятся все связанные записи.
Тесты
.venv/bin/python -m unittest discover -s tests -v
Тесты: 253 (на версии 0.1.0+61). Покрытие: STT, LLM, vision, ToV, reply-поля, изоляция пользователей, API-логи, raw update log, режимные клавиатуры и роутинг, платежи (Stars, защита от повторов, dev-подтверждение), ошибки, справка, онбординг, подписки, самостоятельная генерация поста, восстановление обрезанного JSON, индикатор «печатает», поддержка (топики, подписи, кнопка «Ответить», закрытие обращения).
Запускай через
.venvпроекта: вне виртуального окруженияaiogramможет быть не установлен.
Правила языка
- Служебные сообщения боту → всегда русский.
- Текст поста → язык явного запроса пользователя.
- Дословный текст технической ошибки может остаться на языке оригинала.