Ответ из топика приходил пользователю без подписи, и было непонятно, кто пишет — бот или человек. - Текст уходит одним сообщением: «Оператор поддержки:\n<текст>». - У медиа copy_message не позволяет подменить caption, поэтому подпись отправляется отдельным сообщением перед копией; caption медиа, если был, попадает в то же сообщение. - Подпись задаётся SUPPORT_OPERATOR_SIGNATURE (default «Оператор поддержки»). - Заметки с префиксом // по-прежнему не уходят пользователю. Тесты: 236 OK (6 новых — текст, медиа с caption, медиа без caption, переопределение подписи через env). VERSION: 0.1.0+56 -> 0.1.0+57
30 KiB
Social Post Writer Bot
Синхронизация документации
README.md и IDEA.MD — синхронизируемые документы проекта. Если меняются сведения о функциях,
текущем поведении, статусе, версии, решениях или процессе разработки в одном файле, внеси те же
смысловые изменения во второй. Перед завершением задачи проверь оба файла и убедись, что общие
сведения совпадают; различаться могут только детали, специфичные для самого документа.
Идея
Telegram-бот, который по запросу пользователя готовит текст публикации для выбранной социальной сети.
Цель
Быстро превращать тему, тезисы или черновик пользователя в готовый к публикации текст, адаптированный под площадку и заданный стиль.
Основной сценарий
- Пользователь описывает, о чём нужен пост, и указывает соцсеть.
- При необходимости задаёт тон, длину и дополнительные требования.
- Бот генерирует текст; пользователь может попросить изменить или переписать его.
Режимы работы
- Главное меню — внутреннее состояние после
/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 volumed32u5zcq2g4odmdpq9hw91yn-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; поиск по Telegramupdate_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):
- Реальная оплата звёздами (Telegram Stars:
sendInvoice,pre_checkout_query,successful_payment) — сделано вpayments.py; осталось проверить возврат (refundStarPayment) и автопродление. - Автопродление подписки и уведомления об окончании.
- Админ-панель: статистика пользователей, подписок, расходов APInex.
- Лимиты на генерации (защита расходов на API).
- Обновление Gitea 1.26.2 → 28.x (пропущены security-фиксы 1.27.0).
- Расширение тестов: e2e-сценарии, мок платежей.
- CI: автозапуск тестов и линтера на push в
dev.
Как ведём проект
- Одна задача = одна атомарная доработка (issue).
- Порядок работы по любой правке бота (обязательная последовательность):
- Правки в коде — код + тесты, прогнать
python -m unittest discover -s tests(всё должно быть OK). - Поднять
VERSION— следующий билд (0.1.0+N), плюс запись в README/IDEA.MD о новой версии. - Коммит — в
dev, одним атомарным коммитом с описанием, что изменилось и что проверено. - Пуш в
dev—git push origin dev. - Деплой —
bash /root/.hermes/scripts/gitea_deploy.sh dev. - Вебхук — дождаться уведомления от Coolify на
POST https://hrms217.dynv6.net/webhooks/coolify. Это и есть сигнал, что деплой доехал. Статус в Coolify API не поллим — у приложения нет healthcheck,running:unknownничего не доказывает. - Отчёт — коротко: что сделано, что проверено, что осталось непроверенным.
- Правки в коде — код + тесты, прогнать
- Готово → задача в колонку «Готово» и закрыта (closed).
- Документация (README / IDEA.MD / вики) обновляется вместе с кодом.