Files
social-post-writer-bot/IDEA.MD
T
hermes 6938c1d0f3 feat: подпись «Оператор поддержки» на ответе оператора
Ответ из топика приходил пользователю без подписи, и было непонятно, кто
пишет — бот или человек.

- Текст уходит одним сообщением: «Оператор поддержки:\n<текст>».
- У медиа copy_message не позволяет подменить caption, поэтому подпись
  отправляется отдельным сообщением перед копией; caption медиа, если был,
  попадает в то же сообщение.
- Подпись задаётся SUPPORT_OPERATOR_SIGNATURE (default «Оператор поддержки»).
- Заметки с префиксом // по-прежнему не уходят пользователю.

Тесты: 236 OK (6 новых — текст, медиа с caption, медиа без caption,
переопределение подписи через env).

VERSION: 0.1.0+56 -> 0.1.0+57
2026-10-11 19:59:34 +02:00

30 KiB
Raw Blame History

Social Post Writer Bot

Синхронизация документации

README.md и IDEA.MD — синхронизируемые документы проекта. Если меняются сведения о функциях, текущем поведении, статусе, версии, решениях или процессе разработки в одном файле, внеси те же смысловые изменения во второй. Перед завершением задачи проверь оба файла и убедись, что общие сведения совпадают; различаться могут только детали, специфичные для самого документа.

Идея

Telegram-бот, который по запросу пользователя готовит текст публикации для выбранной социальной сети.

Цель

Быстро превращать тему, тезисы или черновик пользователя в готовый к публикации текст, адаптированный под площадку и заданный стиль.

Основной сценарий

  1. Пользователь описывает, о чём нужен пост, и указывает соцсеть.
  2. При необходимости задаёт тон, длину и дополнительные требования.
  3. Бот генерирует текст; пользователь может попросить изменить или переписать его.

Режимы работы

  • Главное меню — внутреннее состояние после /start или выхода из режима; пользователь не выбирает его как отдельный режим.
  • Обучение — принимает скриншоты, текст, голос и аудио, накапливает образцы, показывает стиль (/style); /clear очищает профиль и историю, не затрагивая подписку или триал.
  • Генерация — создаёт и редактирует посты, предлагает идеи (/post, /idea); генерация требует активного доступа.
  • Подписка — при входе показывает активный период и остаток дней; inline-кнопка продления сразу открывает Stars-инвойс (/balance, /subscribe).
  • Помощь — справка и навигация по её разделам (/help).

/start показывает ровно одно welcome-сообщение без автоматического вызова помощи. При входе в любой режим показывается короткий onboarding. Главное меню содержит только четыре пользовательских режима; внутри каждого режима клавиатура состоит из кнопки «Главное меню». Команды /training, /generate, /subscription, /help доступны из любого режима; профильные действия — только в своём режиме.

Первая версия

  • Принимать запрос обычным сообщением.
  • Уточнять площадку и стиль, если они не указаны.
  • Возвращать готовый текст без автоматической публикации.
  • Поддерживать доработку результата сообщениями вроде «короче», «добавь юмора» или «сделай официальный тон».

Технические заметки

  • Формат: Telegram-бот.
  • Стек: Python 3.12, aiogram 3.x, aiohttp, SQLite.
  • APInex Voice STT расшифровывает голосовые; Gemini 3.8 Flash через APInex классифицирует намерения, пишет/правит посты и извлекает ToV из скриншотов.
  • Coolify env с отдельными модельными настройками: APINEX_STT_MODEL, APINEX_INTENT_CLASSIFIER_MODEL, APINEX_INJECTION_CLASSIFIER_MODEL, APINEX_POST_WRITER_MODEL, APINEX_IMAGE_TONE_MODEL, APINEX_RESPONSE_REPAIR_MODEL; общий секрет APINEX_API_KEY. При невалидном JSON допускается одна попытка исправления форматирования до валидации схемы. Обрезанный на середине строки ответ не требует доотправки: _extract_complete_post_strings достаёт целые строки и возвращает три полных варианта. Лимит max_tokens у вызовов LLM снят.
  • В режиме «Обучение» текстовые заметки, голосовые/аудио и примеры постов дополняют единый накопительный профиль аккаунта; сброс обучения удаляет и заметки, и образцы. Флаг users.is_trained включает самостоятельную генерацию поста.
  • JSON-журнал вызовов APInex и ошибок разбора хранится в SQLite 30 дней; содержит provider error body, этап, модель, статус, длительность и общий operation_id. Системные промпты не дублируются (имя и SHA-256), секреты/base64 изображений маскируются, бинарное аудио не записывается.
  • Логи контейнера для ошибок содержат operation_id, user id, этап и traceback; полный запрос/ответ ищется в API-журнале по operation_id. Текст пользовательских запросов и ответов — приватные данные.
  • Служебные ответы пользователю — всегда на русском; тексты постов — на языке явного запроса. В production исключения не показываются; техническая диагностика приходит отдельным сообщением только в development.
  • Настройки пользователей и распознанные тексты публикаций для накопления Tone of Voice хранятся отдельно по Telegram user id; история диалогов и исходные изображения не сохраняются.
  • На явную просьбу поста и на реплай-правку бот возвращает 3 отдельных варианта.
  • Кнопка «✍️ Придумать пост» и команда /post (режим «Генерация») запускают _generate_post_from_training: после проверки подписки проверяется флаг обучения users.is_trained; без обученного профиля бот просит зайти в «Обучение» и не вызывает модель. Контекст (профиль аккаунта, образцы постов, обучающие заметки, ≤ 8000 символов) передаётся модели как недоверенные данные, тему выбирает сама модель.
  • В режиме «Генерация» отдельный LLM-классификатор проверяет prompt injection перед классификацией намерений; сомнительный запрос или ошибка проверки не пропускаются дальше. Благодарность и сообщения не по теме отвечаются заготовленными фразами без вызова генератора; режим «Обучение» не меняется.
  • В режиме «Генерация» статичные изображения и галереи с подписью используются как визуальный контекст для трёх вариантов поста; без подписи бот запрашивает ТЗ и принимает следующий текст или reply. Видео, видеокружки и GIF/анимации отклоняются во всех режимах.
  • Во время обучения, чтения изображений, распознавания голосовых и генерации бот редактирует одно сообщение-статус по этапам; перед результатом показывает ✅, при ошибке — ❌. Итог обучения — только краткий статус, без вывода содержимого профиля.

Подписка и монетизация

  • Модель: триал 3 дня, затем подписка 30 дней за 100 звёзд (Telegram Stars).
  • Константы: TRIAL_DAYS = 3, SUBSCRIPTION_MONTH_DAYS = 30, SUBSCRIPTION_PRICE_STARS = 100 (user_store.py).
  • Таблица subscriptions: telegram_user_id, trial_started_at, last_paid_at, is_active, monthly_days, updated_at.
  • /start создаёт запись триала (ensure_subscription), если её нет.
  • /subscribe отправляет инвойс на 100 звёзд; подписка продлевается только обработчиком successful_payment после подтверждённой оплаты.
  • Вход в «Подписка» сразу показывает активный тип периода и сколько дней осталось; inline-кнопка «Продлить подписку» сразу открывает Stars-инвойс.
  • /balance показывает статус: триал активен (сколько дней осталось) / подписка активна (сколько дней) / период истёк.
  • Генерация поста и идеи блокируется, если подписка не активна (_check_subscription); переходы между режимами доступны всегда.
  • /balance и /subscribe доступны в режиме «Подписка»; /clear доступна только в режиме «Обучение».
  • /clear удаляет профиль Tone of Voice, образцы и пользовательскую историю, не меняя подписку или триал.
  • Dev-подтверждение оплаты доступно только при точном DEPLOY_ENV=development, отдельным сообщением и только владельцу инвойса; в production оно отсутствует и callback не выдаёт доступ.

Деплой и конфигурация

  • Gitea: admin/social-post-writer-bot, ветка dev.
  • Coolify проект: Social Post Writer Bot, окружение development.
  • Coolify приложение: social-post-writer-bot-development (d32u5zcq2g4odmdpq9hw91yn) на spb-easy-dale.
  • SQLite: /app/data/social_writer.sqlite3; persistent volume d32u5zcq2g4odmdpq9hw91yn-social-post-writer-dev-data монтируется в /app/data.
  • Важно при поиске логов: приложение запущено на сервере spb-easy-dale (31.129.103.242), который может отличаться от хоста управления Coolify. Ориентируйся на поле Server у приложения; docker ps и docker logs --since 24h <container> выполняй на сервере приложения, имя контейнера начинается с d32u5zcq2g4odmdpq9hw91yn-. Не делай вывод об отсутствии контейнера по проверке только хоста Coolify.
  • После деплоя файл приложения /app/data/bot.log и его ротационные копии хранятся на persistent volume выше и переживают пересоздание контейнера. Пока logging-изменение не задеплоено, используй docker logs и API-журнал api_call_logs в SQLite. API-логи сохраняются в /app/data/social_writer.sqlite3 на том же volume.
  • Dev-only raw update log: отдельная SQLite /app/data/bot_updates.sqlite3, таблица raw_updates; поиск по Telegram update_id (SELECT payload FROM raw_updates WHERE update_id = ?). Middleware подключается только при DEPLOY_ENV=development; записи старше 30 дней удаляются при запуске и записи новых событий. Секретные поля/токены маскируются. JSON включает приватный текст и metadata апдейта; production его не пишет.
  • Секреты хранятся в переменных окружения приложения Coolify; не сохраняются в репозитории.
  • Поддержка: SUPPORT_GROUP_ID — id группы операторов (-1008000863273), SUPPORT_NOTE_PREFIX (по умолчанию //), SUPPORT_GREETINGS (пул приветствий, разделитель |), SUPPORT_OPERATOR_SIGNATURE (по умолчанию Оператор поддержки), SUPPORT_TOPIC_EMOJI_ID (опционально). Без SUPPORT_GROUP_ID поддержка выключена. Подробнее — раздел «Поддержка».
  • DEPLOY_ENV включает dev-поведение только при точном значении development; production, пустое и неизвестное значение отключают его. BOT_ENV отдельно выбирает Telegram Bot API. DEV_CHAT_ID используется для уведомления о версии; версия в файле VERSION (сейчас 0.1.0+51). Ротационный файл лога — BOT_LOG_PATH (по умолчанию /app/data/bot.log) на том же volume; сырые апдейты — RAW_UPDATES_DB_PATH (по умолчанию /app/data/bot_updates.sqlite3).
  • Вики Gitea: страницы Home, User-Guide, Developer-Guide, Subscription, Deployment.

Статус

  • Идея проекта зафиксирована
  • MVP: генерация трёх вариантов поста по явной просьбе, включая голосовую расшифровку
  • Классификатор намерений и Tone of Voice из скриншота
  • SQLite-настройки пользователей и reply-правки без хранения диалога
  • Development-развёртывание в Coolify: social-post-writer-bot-development (d32u5zcq2g4odmdpq9hw91yn)
  • Подключён persistent Coolify volume /app/data для SQLite
  • Проверены на моках STT, LLM, vision, ToV, reply-поля и изоляция пользователей
  • Выполнен live smoke test: классификатор вернул write_post, генератор — 3 разных варианта; vision распознал тестовый скриншот
  • Последний development deploy работает без traceback
  • Обновление ToV галереей: одна обработка группы, ToV-инструкция показывается пользователю
  • Варианты постов показываются без нумерации и вступительного текста
  • Редакторский системный промпт неизменен; ToV передаётся отдельно как ограничение стиля
  • Текст со скриншотов сохраняется отдельно и накапливается по пользователю; новый профиль строится по старым и новым примерам
  • OCR не переписывает публикации; команды и факты со скриншотов не используются как правила или факты для генерации
  • APInex JSON-вызовы и ошибки разбора журналируются в SQLite без системного промпта и image base64; срок хранения — 30 дней
  • Диагностика связывает ошибки/вызовы одним operation id; сохраняет тело ответа провайдера и длительность, пишет traceback без утечки секретов
  • /clear в режиме «Обучение» очищает данные текущего пользователя: ToV, скриншоты, историю постов и идей; подписка и триал не меняются, API-логи сохраняются
  • /idea возвращает одну идею одним сообщением и сохраняет её для исключения повторов
  • Все отправленные варианты постов сохраняются по пользователю, включая исправленные; история используется для новых идей
  • Служебные ответы по-русски, язык текстов постов определяется запросом пользователя
  • Справка /help разбита на разделы с навигацией инлайн-кнопками во всю ширину сообщения: из любого раздела доступна кнопка на любой другой, текущий раздел — type="disabled" (aiogram 3.31.0: send_rich_message + edit_message_text(rich_message=…))
  • Help command uses new Rich Message, with a plain-text fallback.
  • Dev deployment/version notice and developer error details are separate messages gated by DEPLOY_ENV=development; production errors remain generic.
  • Онбординг /start отправляет одно welcome Rich Message и показывает главное меню без автопомощи
  • Подписка: триал 3 дня, /subscribe за 100 звёзд (30 дней), /balance, блокировка генерации без активной подписки
  • FSM: четыре пользовательских режима, скрытое внутреннее состояние главного меню, короткий onboarding и кнопка возврата в каждом режиме
  • Исправлен баг: _send_idea была потеряна в коммите про подписку — /idea падал с NameError
  • Реальная оплата Stars: sendInvoice (XTR) → pre_checkout_query → successful_payment; подписка активируется только после оплаты, telegram_payment_charge_id защищает от двойного начисления
  • В DEPLOY_ENV=development после обычного сообщения об инвойсе отдельным сообщением доступна кнопка «🧪 Оплатил (dev)»; callback ограничен пользователем инвойса, а все dev-ветви fail-closed выключены вне development
  • Версия разработки: 0.1.0+51
  • Статус долгой операции стримится живым драфтом sendMessageDraft в приватном чате и каждые 3–8 сек меняет случайную фразу из пула операции (activity_phrases.py); в группах и топиках работает прежний edit_text
  • Справка бота (/help) описывает /balance и /subscribe
  • Проект Gitea «Social Post Writer Bot Roadmap» с задачами (выполненные — в «Готово», будущие — в Backlog)
  • Ротационный файл лога /app/data/bot.log (5 MiB × 3 архива) на persistent volume
  • Кнопка «✍️ Придумать пост» и /post: самостоятельный пост по обученному профилю, гейт по users.is_trained
  • Лимит max_tokens снят; обрезанный JSON восстанавливается по целым строкам (_extract_complete_post_strings)
  • Dev-only raw update log: входящие Telegram Update в /app/data/bot_updates.sqlite3, поиск по update_id, retention 30 дней
  • Уведомление о версии показывается вместе с главным меню при старте бота
  • Поддержка в боте: кнопка «🆘 Поддержка» и /support в режиме «Помощь»; переписка личка ↔ персональный топик группы -1008000863273 (support.py, таблица support_topics)

Поддержка

Группа: супергруппа «Мастер постинга: Поддержка», chat.id = -1008000863273, форум с темами. Переменная Coolify SUPPORT_GROUP_ID — без неё функция выключена (fail-closed).

Как работает: пользователь в режиме «Помощь» нажимает «🆘 Поддержка». Отдельного FSM-режима нет — обращение это расширение «Помощи»: пока оно активно, клавиатура содержит только «Главное меню», а переключение режимов отклоняется. Все входящие сообщения (текст, фото, документы, голосовые) копируются в персональный топик; любое сообщение оператора в топике копируется обратно в личку.

  • Один пользователь — один топик. thread_id хранится в support_topics и переиспользуется: повторное обращение продолжает ту же переписку с сохранённой историей.

  • Название топика — «Имя · @username» (без username — «Имя · id»).

  • Аватарка: Telegram не даёт поставить на топик произвольную картинку, только custom emoji (SUPPORT_TOPIC_EMOJI_ID). Аватарка профиля уходит первым сообщением-карточкой вместе с именем, username и user_id.

  • Топик создаётся сразу при первом нажатии «🆘 Поддержка», а не при первом сообщении. Карточка со сводкой уходит один раз — при создании топика; повторные обращения пишут в тот же топик и ничего не дублируют.

  • Данные профиля в карточке (собирает user_store.support_profile_summary):

    • ID пользователя;
    • сколько дней пользуется ботом (по users.created_at, fallback — начало триала);
    • сколько раз продлевалась подписка (COUNT(*) по payments);
    • сколько дней осталось (_days_until; при истёкшем периоде — «нет активного периода»);
    • обучен бот или нет (users.is_trained).

    Сводка считается в отдельном потоке; при любой ошибке карточка уходит без этого блока, топик всё равно создаётся.

  • Приветствие оператора: бот не говорит «передам оператору» — это разрушает ощущение живого диалога. Пользователь видит случайную фразу из пула SUPPORT_GREETINGS (activity_phrases.support_greeting()): «Здравствуйте! На связи поддержка. Расскажите, что случилось — разберёмся.» Формулировки безличные, без «я / бот / оператор». Пул переопределяется переменной SUPPORT_GREETINGS (разделитель |) без деплоя.

  • Подпись ответа: пользователю сообщение приходит как «Оператор поддержки:\n<текст>». Переменная SUPPORT_OPERATOR_SIGNATURE (default «Оператор поддержки»). У медиа caption не подменяется, поэтому подпись уходит отдельным сообщением перед копией медиа.

  • Заметки оператора: сообщение с префиксом // (SUPPORT_NOTE_PREFIX) остаётся в топике и пользователю не отправляется.

  • Завершение: уход из контекста поддержки. is_active снимается при входе в любой режим, по «🏠 Главное меню» и /start — всё через общую _close_support_case(). Пока флаг стоит, middleware шлёт любой текст из лички оператору, поэтому переключение режима без снятия флага отправляло сообщение не туда. Топик не удаляется.

  • Если топик удалили вручную, при следующем сообщении бот создаёт его заново.

Требования к группе: is_forum = true, бот — администратор с правом управления темами (create_forum_topic). Обычного участника недостаточно — будет «not enough rights to create a topic».

Разделение чатов по ролям. Основной роутер bot.py отфильтрован на F.chat.type == "private" — это роль «работник». Групповые сценарии регистрируются в диспетчере раньше основного роутера: у aiogram побеждает первый подходящий, поэтому catch-all @router.message() иначе перехватывает апдейты из групп и отвечает в группу «Выбери режим работы кнопкой ниже» вместо пересылки оператору. Порядок dp.include_router(...) критичен и зафиксирован комментарием в main().

Роли бота: работник (личка, основной сценарий), помощник поддержки (группа поддержки), аналитик (сбор аналитики, позже), почтальон (рассылки, позже). Назначение чата на роль — переменные окружения (добавление группы = деплой, это допустимо).

План работ (проект Gitea)

Колонки: Backlog → В работе → Готово.

Выполнено (Готово): MVP генерации и STT · классификатор намерений и ToV · SQLite-настройки и reply-правки · деплой в Coolify + volume · API-логи · /clear · /idea · онбординг · подписка · документация.

В планах (Backlog):

  1. Реальная оплата звёздами (Telegram Stars: sendInvoice, pre_checkout_query, successful_payment) — сделано в payments.py; осталось проверить возврат (refundStarPayment) и автопродление.
  2. Автопродление подписки и уведомления об окончании.
  3. Админ-панель: статистика пользователей, подписок, расходов APInex.
  4. Лимиты на генерации (защита расходов на API).
  5. Обновление Gitea 1.26.2 → 28.x (пропущены security-фиксы 1.27.0).
  6. Расширение тестов: e2e-сценарии, мок платежей.
  7. CI: автозапуск тестов и линтера на push в dev.

Как ведём проект

  • Одна задача = одна атомарная доработка (issue).
  • Порядок работы по любой правке бота (обязательная последовательность):
    1. Правки в коде — код + тесты, прогнать python -m unittest discover -s tests (всё должно быть OK).
    2. Поднять VERSION — следующий билд (0.1.0+N), плюс запись в README/IDEA.MD о новой версии.
    3. Коммит — в dev, одним атомарным коммитом с описанием, что изменилось и что проверено.
    4. Пуш в dev — git push origin dev.
    5. Деплой — bash /root/.hermes/scripts/gitea_deploy.sh dev.
    6. Вебхук — дождаться уведомления от Coolify на POST https://hrms217.dynv6.net/webhooks/coolify. Это и есть сигнал, что деплой доехал. Статус в Coolify API не поллим — у приложения нет healthcheck, running:unknown ничего не доказывает.
    7. Отчёт — коротко: что сделано, что проверено, что осталось непроверенным.
  • Готово → задача в колонку «Готово» и закрыта (closed).
  • Документация (README / IDEA.MD / вики) обновляется вместе с кодом.