- activate_subscription_days now adds timedelta to the real expires_at (max(now, current_expires)) instead of _days_until() floor value; prevents losing fractional time on each renewal (10d + 1d -> was 10d) - return ceil day count for display consistency - keep _days_until only for is_subscribed gate and support stats
Social Post Writer Bot — development
Telegram-бот для генерации постов для соцсетей. Готовит текст по явной просьбе пользователя, адаптируя под его Tone of Voice, и работает по модели подписки (триал 3 дня, затем 100 звёзд в месяц).
Содержание
- Как работает бот (UX)
- Архитектура кода
- Поддержка
- Подписка и монетизация
- Данные и хранилище
- Команды
- Деплой и конфигурация
- Тесты
Версия разработки: 0.1.0+64.
Синхронизация документации
README.md и IDEA.MD — синхронизируемые документы проекта. Если меняются сведения о функциях,
текущем поведении, статусе, версии, решениях или процессе разработки в одном файле, внеси те же
смысловые изменения во второй. Перед завершением задачи проверь оба файла и убедись, что общие
сведения совпадают; различаться могут только детали, специфичные для самого документа.
Как работает бот (UX)
Dev-подтверждение подписки
Только при точном DEPLOY_ENV=development после обычного сообщения о счёте отдельным сообщением
показывается кнопка «🧪 Оплатил (dev)». Она включает доступ на 30 дней без реального платежа и
доступна только пользователю, которому выставлен счёт. В production, при пустом или неизвестном
значении DEPLOY_ENV dev-кнопка и callback не выдают доступ.
Reply-клавиатура
После /start отображается одно welcome-сообщение с нижним меню; справка автоматически не вызывается.
В главном меню доступны только пользовательские режимы:
🎓 Обучение ✍️ Генерация
💳 Подписка 📖 Помощь
«Главное меню» — внутренняя точка входа после /start и выхода из режима, а не отдельный режим.
Внутри любого режима reply-клавиатура содержит только «🏠 Главное меню». В «Генерации» добавляется
«✍️ Придумать пост», в «Обучении» — «🗑 Сбросить данные», в «Помощи» — «🆘 Поддержка». При входе показывается короткий
onboarding. В «Подписке» сразу отображаются тип активного периода и оставшиеся дни; inline-кнопка
«Продлить подписку» сразу открывает Stars-инвойс на оплату.
Команды перехода: /start, /training, /generate, /subscription, /help; /start возвращает
в главное меню. Команды /style и /clear доступны только в обучении; /post и /idea — в генерации;
/balance и /subscribe — в подписке. Все команды публикуются через set_my_commands при старте.
Онбординг (/start)
/start создаёт запись триала, очищает старое состояние FSM и возвращает пользователя в главное меню:
- Отправляет ровно одно
WELCOME_RICH_HTML: приветствует пользователя, объясняет накопительное обучение профиля аккаунта и подготовку нескольких черновиков поста без автоматической публикации. - Показывает клавиатуру главного меню; справка открывается отдельно кнопкой или командой
/help.
Справка (/help)
Меню и страницы справки — Rich Message с обычной клавиатурой InlineKeyboardMarkup из keyboards.py.
По одной кнопке на строку; все разделы остаются доступны на каждой странице, включая текущую.
Выбор редактирует текст Rich Message без смены callback help:<section>.
Поддержка (support.py)
Отдельного FSM-режима нет: обращение — расширение режима «Помощь». Пользователь нажимает
«🆘 Поддержка» (или /support), и до выхода в главное меню все его сообщения уходят оператору.
Схема:
личка пользователя ──copy_message──▶ топик в SUPPORT_GROUP_ID
▲ │
└────────────copy_message─────────────┘
(любое сообщение оператора)
- Топик создаётся один на пользователя и переиспользуется:
support_topicsхранитtelegram_user_id → thread_idи флаг активности. Название — «Имя · @username» (без username — «Имя · id»). - Telegram не даёт поставить на топик произвольную картинку, только custom emoji
(
SUPPORT_TOPIC_EMOJI_ID). Аватарка профиля уходит первым сообщением-карточкой в топик (get_user_profile_photos→send_photoс подписью «Имя / @username / ID»). - Топик создаётся сразу при первом нажатии «🆘 Поддержка», а не при первом сообщении. Карточка со сводкой профиля уходит один раз — при создании топика; повторные обращения пишут в тот же топик и ничего не дублируют. Если топик удалили вручную, при следующем сообщении он создаётся заново — и сводка уйдёт снова.
- Сводка профиля в карточке (
user_store.support_profile_summary): ID пользователя; сколько дней пользуется ботом; сколько раз продлевалась подписка; сколько дней осталось; обучен бот или нет. - Разделение чатов по ролям. Основной роутер (
bot.py) отфильтрован наF.chat.type == "private"— это роль «работник». Групповые сценарии (роль «помощник поддержки») живут вsupport.pyи регистрируются в диспетчере раньше основного роутера: иначе catch-all@router.message()перехватывает апдейты из групп и отвечает в группу «Выбери режим работы кнопкой ниже» вместо пересылки сообщения оператору. Порядок включения роутеров критичен — у aiogram побеждает первый подходящий. Планируемые роли: работник (личка), помощник поддержки (группа), аналитик (сбор аналитики), почтальон (рассылки). - Сообщение пользователя в топике подписывается: «<Имя>:\n<текст>», а если имени нет —
«:\n<текст>» (
user_signature()). Caption медиа тоже уходит текстом с подписью, само медиа копируется как есть. - Первое сообщение — приветствие оператора. Бот не объясняет, что «передаёт» сообщение:
это разрушает ощущение живого диалога. Пользователь видит случайную фразу из пула
SUPPORT_GREETINGS(activity_phrases.support_greeting()), например «Здравствуйте! На связи поддержка. Расскажите, что случилось — разберёмся.» Формулировки безличные и без «я/бот/оператор», чтобы подходить любому оператору. Пул можно переопределить без деплоя:SUPPORT_GREETINGSв Coolify, разделитель|. - Ответ оператора — любое сообщение в топике от живого участника. Пользователю оно приходит
с подписью: «Оператор поддержки:\n<текст>». Подпись задаётся
SUPPORT_OPERATOR_SIGNATURE(по умолчанию «Оператор поддержки»). У медиаcopy_messageне позволяет подменить caption, поэтому подпись уходит отдельным сообщением перед самим медиа. Если обращение закрыто, к сообщению добавляется инлайн-кнопка «↩️ Ответить»: она снова открывает обращение (is_active = 1) и показывает клавиатуру только с «Главное меню». При активном обращении кнопки нет — пользователь и так пишет оператору. Сообщение с префиксом//(SUPPORT_NOTE_PREFIX) остаётся заметкой внутри топика и пользователю не уходит. - Завершение — уход из контекста поддержки. Флаг
is_activeснимается при входе в любой режим (/training,/generate,/subscription,/help), а также по «🏠 Главное меню» и/start— общая точка_close_support_case(). Пока флаг стоит, middleware отправляет любой текст из лички оператору, поэтому переключение в «Обучение» без снятия флага приводило к тому, что сообщение уезжало не туда. Топик при этом не удаляется: следующее «🆘 Поддержка» продолжает ту же переписку. Повторное «Поддержка» продолжает переписку в том же топике с сохранённой историей. - Пока обращение активно, клавиатура содержит только «Главное меню», а переключение в другие режимы отклоняется — чтобы режимы и генерация не смешивались с обращением.
- Команды и кнопки бота работают всегда, даже при активном обращении:
SupportRelayMiddlewareпропускает их через_is_bot_control()и не отправляет оператору. Иначе «📖 Помощь» и «🎓 Обучение» уезжали оператору как обычный текст, режим не переключался, и бот в личке молчал.
Переменные Coolify: SUPPORT_GROUP_ID (обязательно, без неё функция выключена),
SUPPORT_NOTE_PREFIX (default //), SUPPORT_TOPIC_EMOJI_ID (optional custom emoji id),
SUPPORT_GREETINGS (optional; пул приветствий оператора, разделитель |),
SUPPORT_OPERATOR_SIGNATURE (optional; подпись ответа оператора, default Оператор поддержки).
Группа должна быть супергруппой с включёнными темами (
is_forum), бот — её администратором с правом «Управление темами» (create_forum_topic). Обычного участника недостаточно: Telegram вернёт «not enough rights to create a topic», и топик не создастся. ВDEPLOY_ENV=developmentбот покажет эту причину прямо в сообщении об ошибке, в production — только общую заглушку.
Основные сценарии
| Что хочет пользователь | Что делает | Что видит |
|---|---|---|
| Написать пост | В режиме «Генерация» пишет «напиши пост про запуск продукта» | Индикатор «печатает», затем 3 отдельных сообщения |
| Изменить пост | В режиме «Генерация» отвечает реплаем на вариант бота | Три новых варианта с учётом исходного текста |
| Придумать идею | В режиме «Генерация» пишет «предложи тему» или /idea |
Одна идея одним сообщением |
| Поблагодарить или написать не по теме | В режиме «Генерация» отправляет обычное сообщение | Одна фраза из заготовленного набора; генератор постов/идей не вызывается |
| Пост по изображению | В режиме «Генерация» отправляет изображение/галерею с подписью-ТЗ | Vision изучает изображения и бот возвращает 3 варианта поста |
| Изображение без ТЗ | В режиме «Генерация» отправляет изображение/галерею без подписи | Бот спрашивает, о чём написать, затем принимает обычный текст или reply |
| Настроить стиль | В режиме «Обучение» присылает скриншоты, текст, голос или аудио | Бот накапливает сведения, обновляет профиль аккаунта и показывает только статус завершения |
| Пост по обученным данным | В режиме «Генерация» нажимает «✍️ Придумать пост» или вводит /post |
Бот сам выбирает тему по накопленному профилю и возвращает 3 варианта; без обучения просит сначала обучить |
| Голосом | В режиме «Генерация» отправляет голосовое | STT расшифровывает → обычный текстовый запрос |
| Узнать статус / продлить | Входит в режим «Подписка» и нажимает «Продлить подписку» | Текущий тип периода, остаток дней и Stars-инвойс на 100 звёзд |
| Очистить данные | В режиме «Обучение» нажимает «Сбросить данные» или вводит /clear, затем подтверждает |
Очищает стиль и историю, не меняя подписку или триал |
Индикаторы и обратная связь
- Пока идёт обработка (классификация, расшифровка, чтение скриншота, генерация) — бот показывает «печатает» (
typing_status, обновление каждые 4 секунды). - В приватном чате статус операции стримится живым драфтом (
sendMessageDraft): он не тратит лимит отправки в чат, не оставляет после себя сообщения и анимируется при смене текста. Пока операция длится, драфт каждые 3–8 секунд (случайно) получает случайную фразу из пула, привязанного к типу операции, — видно, что работа не встала. Финальный результат уходит обычным сообщением. - В группах и топиках драфты недоступны: там работает прежний механизм — отдельное сообщение-статус, которое редактируется по этапам; перед результатом оно меняется на ✅, при ошибке — на ❌. Ротация фраз в группах отключена.
- Пулы фраз живут в
activity_phrases.py(generation,training,images,voice,ideas), интервал задаётся переменнымиACTIVITY_MIN_SECONDS/ACTIVITY_MAX_SECONDS. - Драфт исчезает только когда бот отправляет реальное сообщение, поэтому каждый путь завершения операции обязан закрывать статус (
status.finish(...)либоfinish_status(status, ...), который терпитNone). Иначе последняя фраза висит в чате до конца TTL драфта.finish_if_open()— страховка от повторного закрытия. - В режиме «Генерация» запрос сначала проверяет отдельный LLM-классификатор prompt injection, затем классификатор намерений. При обнаружении попытки изменить правила генератор не вызывается; ошибка проверки блокирует дальнейшую обработку. Модель проверки настраивается отдельно через
APINEX_INJECTION_CLASSIFIER_MODEL. - Благодарность и сообщения не по теме получают случайную заготовленную фразу и не запускают генерацию/идеи. Режим «Обучение» этим изменением не затронут.
- В режиме «Генерация» изображения и галереи используются для подготовки поста; подпись считается ТЗ. Без подписи бот сначала запрашивает ТЗ. Видео, видеокружки и GIF/анимации отклоняются во всех режимах.
- При ошибке пользователь получает общий безопасный ответ. Только в development отдельным сообщением дополнительно отправляется диагностика; production не раскрывает текст исключения.
- Служебные тексты — всегда на русском. Язык самого поста определяется языком запроса.
Блокировка по подписке
Если подписка не активна (триал/подписка истекли или is_active = 0), генерация блокируется. Переключатели режимов доступны всегда; для оплаты пользователь переходит в режим «Подписка».
Самостоятельный пост по обученным данным
Кнопка «✍️ Придумать пост» (режим «Генерация») и команда /post идут не через классификатор намерений, а в
_generate_post_from_training: сначала проверяется подписка, затем флаг обучения is_user_trained
(users.is_trained). Если профиль не обучен, бот просит зайти в режим «Обучение» и прислать сведения об
аккаунте или примеры постов, не вызывая модель. Иначе собранный контекст (профиль аккаунта, образцы постов,
обучающие заметки, до 8000 символов) передаётся как недоверенные данные, и модель сама выбирает одну тему.
Обученный флаг выставляется при сохранении Tone of Voice или обучающих заметок и сбрасывается /clear.
- Статистика бота: команда
/statsдоступна в топике «Статистика» группы «Мастер постинга: Админка» (chat_id=-1008001427913,message_thread_id=5). Отчёт отправляется Rich Message таблицами с колонками «Сегодня», «7 дней», «30 дней», «Всё время». События хранятся по Telegram user id в отдельной SQLite/app/data/analytics.sqlite3на persistent volume; текст/медиа пользователей не сохраняются. В режиме генерации учитываются генерации постов, идеи, правки и типы сообщений; в обучении — только типы сообщений. Групповой роутер ограничен аналитическим топиком; доступ к отчёту ограничиваетсяOWNER_USER_ID, если переменная задана.
Архитектура кода
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-логи, подписки
analytics.py — отдельная SQLite-аналитика по Telegram user id; события активности и отчёты
raw_updates.py — отдельная SQLite-БД сырых входящих Telegram Update (работает и в production)
support.py — поддержка: личка ↔ персональный топик в группе операторов
keyboards.py — клавиатуры главного меню и режимов; маппинг кнопок на команды
help_message.py — Rich Message: `WELCOME_RICH_HTML` (онбординг) + `HELP_SECTIONS`/`help_view()` (справка с кнопками), plain fallback
VERSION — версия релиза
Основные потоки
Текстовый запрос:
handle_text (только в режиме генерации) → _classify_and_respond
├─ _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 (пока всё идёт)
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
Скриншоты (ToV):
update_tone_from_image → _handle_image_message
├─ _collect_media_group (собирает галерею, ждёт 1 сек)
├─ extract_posts_from_images (APInex vision, inline base64)
├─ add_post_samples (накапливает по пользователю)
└─ create_tone_profile → save_tone_of_voice → показывает стиль
Ключевые функции (bot.py)
| Функция | Что делает |
|---|---|
start(message, state) |
Одно welcome-сообщение, главное меню и создание триала |
handle_command_text |
Роутинг глобальных переключателей и профильных команд по режиму |
_classify_and_respond |
Ядро: проверка подписки → классификация → генерация |
_check_subscription |
Проверяет активную подписку; если нет — показывает статус и блокирует |
_generate_post_from_training |
/post и кнопка «✍️ Придумать пост»: пост по обученному профилю, с проверкой is_user_trained |
_show_subscription_info |
Показывает тип периода, остаток дней и inline-кнопку продления в режиме подписки |
balance_command |
/balance — статус подписки |
subscribe_command |
/subscribe — отправляет Stars-инвойс на 30 дней (подписка активируется только после оплаты) |
handle_pre_checkout |
Подтверждает только свой инвойс в XTR на 100 звёзд, всё остальное отклоняет |
handle_successful_payment |
Активирует подписку после реальной оплаты; telegram_payment_charge_id пишется в payments для защиты от повторного начисления |
clear_command |
/clear и кнопка «Сбросить данные» показывают inline-подтверждение; сброс только по confirm в режиме обучения |
help_command |
/help — меню справки (Rich Message с обычной inline-клавиатурой) + fallback на plain text |
help_navigation |
Callback help:<section> → edit_message_text(rich_message=...) — переключение разделов |
edit_rich_message |
Редактирование текущего rich-сообщения на новый раздел |
typing_status |
Индикатор «печатает» во время обработки |
OperationStatus |
Статус операции: в приватном чате — живой драфт с ротацией фраз, иначе — сообщение с edit_text |
_draft_supported |
Драфты доступны только в приватных чатах |
Ключевые функции (activity_phrases.py)
| Функция | Что делает |
|---|---|
next_phrase(kind, exclude) |
Случайная фраза из пула операции, не повторяет только что показанную |
random_interval() |
Случайная пауза между сменами фраз, ACTIVITY_MIN_SECONDS..ACTIVITY_MAX_SECONDS |
report_error |
Понятная ошибка пользователю, секреты вырезаны |
Ключевые функции (user_store.py)
| Функция | Что делает |
|---|---|
init_db |
Создаёт таблицы: users, user_post_samples, generated_posts, generated_ideas, api_call_logs, subscriptions |
ensure_subscription |
Создаёт запись подписки с триалом, если нет (INSERT OR IGNORE) |
activate_subscription |
Активирует/продлевает подписку на 30 дней от момента оплаты |
deactivate_subscription |
is_active = 0 |
is_subscribed |
Проверяет: запись есть, is_active = 1, срок не истёк |
subscription_row |
Возвращает строку подписки |
is_user_trained |
Флаг users.is_trained: обучен ли профиль (гейт для «Придумать пост») |
_days_until |
Сколько дней осталось (триал — trial_started_at + 3 дня; подписка — last_paid_at); -1 если истёкло/отключено |
Подписка и монетизация
Модель: триал 3 дня → подписка 30 дней за 100 звёзд (Telegram Stars).
Константы (user_store.py):
TRIAL_DAYS = 3
SUBSCRIPTION_MONTH_DAYS = 30
SUBSCRIPTION_PRICE_STARS = 100
Таблица subscriptions:
telegram_user_id(PK)trial_started_at— момент создания (SQLiteCURRENT_TIMESTAMP, UTC-нейтральный)last_paid_at— момент/конец оплаченного периода;NULL= ещё триалis_active— 1/0monthly_days— длительность периода (30)updated_at
Жизненный цикл:
/start→ensure_subscription→ триал 3 дня,is_active = 1.- Генерация доступна, пока
_days_until > 0. - Триал кончился →
_check_subscriptionблокирует, предлагает/subscribe. /subscribe→sendInvoice(XTR, 100 звёзд) →pre_checkout_query(проверка суммы/валюты/payload) →successful_payment→activate_subscription(months=1)→ +30 дней./clearочищает данные обучения и историю; подписка и триал не изменяются.
Оплата реальная: payments.py проводит настоящий платёж Telegram Stars. Подписка активируется только в successful_payment — сам /subscribe её не продлевает. telegram_payment_charge_id каждого платежа пишется в таблицу payments, поэтому один платёж не может активировать подписку дважды; по этому же id делается возврат через refundStarPayment. Платится валюта XTR, provider_token пустой (цифровой товар).
Часовые пояса: SQLite CURRENT_TIMESTAMP возвращает UTC без tz. Код аккуратно приводит все значения к naive UTC перед сравнением, чтобы не было сдвигов.
Данные и хранилище
- SQLite
/app/data/social_writer.sqlite3(persistent Coolify volume) — бизнес-данные пользователей, подписки и платежи. - SQLite
/app/data/analytics.sqlite3(тот же persistent volume) — append-only события поtelegram_user_id, режиму, действию и типу входящего сообщения; без пользовательского текста и медиа. Общий отчёт агрегируется запросами, отдельные дублирующие сводки не хранятся. - Команда
/statsработает в форуме группы «Мастер постинга: Админка», топик5, и показывает таблицы за сегодня, последние 7 дней, последние 30 дней и всё время. Счётчики генерации: посты, идеи, правки, сообщения и типы сообщений; обучение — типы входящих сообщений. - Постоянное хранилище dev: volume
d32u5zcq2g4odmdpq9hw91yn-social-post-writer-dev-dataсмонтирован в/app/dataконтейнера на сервере Coolifyspb-easy-dale(31.129.103.242). Сервер приложения и сервер управления Coolify могут быть разными: сначала смотри назначенный приложению Server в записи Coolify, затем проверяй контейнер именно на нём. Не ищи контейнер только на хосте Coolify. - Где смотреть ошибки: подключись к
spb-easy-dale, найди контейнер по имениd32u5zcq2g4odmdpq9hw91yn-*командойdocker ps, затем смотри его stdout/stderr черезdocker logs --since 24h <container>. После деплоя ротационный файл приложения будет доступен как/app/data/bot.log(до 5 MiB и 3 архива); он сохраняется в указанном persistent volume при пересоздании контейнера. До деплоя изменения логирования ещё не действуют. - API-журнал ошибки: открой
/app/data/social_writer.sqlite3, таблицуapi_call_logs; записи связываются поoperation_idи времени. Файл/БД находятся в volume на сервере приложения, не в локальном checkout и не на сервере Coolify, если это другой хост. - Сырые Telegram Update (все окружения, включая production): отдельная БД
/app/data/bot_updates.sqlite3, таблицаraw_updates; хранится JSON входящего Update,update_id, тип события и время получения. Поиск поupdate_id:SELECT payload FROM raw_updates WHERE update_id = ?. Записи старше 30 дней удаляются при старте бота и при сохранении новых апдейтов. Чувствительные значения маскируются; Update могут содержать тексты и персональные данные пользователей. Middleware подключается всегда: в production это единственный способ посмотреть события из групп администрирования и поддержки. - Обращения в поддержку: таблица
support_topicsв той же рабочей БД —telegram_user_id(PK),thread_idтопика в группе операторов,is_active, время создания и обновления. Один пользователь — один топик; при завершении запись не удаляется, только снимаетсяis_active, поэтому повторное обращение продолжается с сохранённой историей. Если топик удалили вручную в Telegram, при следующем сообщении он создаётся заново. - Per-user данные: профиль аккаунта/ToV, текстовые обучающие заметки, образцы постов со скриншотов, история сгенерированных постов и идеи. В «Обучении» текст/голос/скриншоты накапливают и обновляют профиль, а не запускают генерацию. История диалогов и исходные изображения не сохраняются.
- Пустые ответы APInex: при пустом
contentбот делает до двух повторов; третий запрос добавляетreasoning_effort: low. Если ответ всё ещё пустой, операция завершается ошибкой и каждая попытка попадает в API-журнал. Ошибки HTTP автоматически не повторяются. APInex документируетlow, но не режим отключения reasoning; поддержка параметра зависит от модели. - Починка формата JSON: при синтаксически невалидном JSON выполняется не более одной отдельной попытки модели
APINEX_RESPONSE_REPAIR_MODEL; затем исходные проверки структуры/схемы запускаются заново. Ошибки API, пустые ответы и JSON с неверной схемой автоматически не исправляются. В Coolify development заданаAPINEX_RESPONSE_REPAIR_MODEL. - Обрезанный JSON: если модель вернула валидную схему, но текст поста обрывается на середине (незакрытая строка/массив), бот доотправки не требует:
_extract_complete_post_stringsдостаёт из ответа завершённые строки и возвращает три полных варианта. Если целых строк меньше трёх или они не найдены — операция завершается ошибкой. Лимитmax_tokensу вызовов LLM снят: модель пишет столько, сколько нужно, без искусственного обрезания. - API-логи: JSON запросов/ответов APInex, тело ответа провайдера при ошибке и ошибки разбора хранятся в SQLite 30 дней. Записи связаны
operation_idи содержат этап, модель, HTTP-статус и длительность. Системные промпты не дублируются — только имя и SHA-256; секреты и base64 изображений маскируются, бинарное аудио не пишется. Пользовательские тексты в запросах/ответах — приватные данные. - Логи контейнера: ошибки операций содержат
operation_id, Telegram user id, этап и полный traceback; ошибки APInex дополнительно показывают модель, статус, длительность и диагностическую структуру ответа без вывода текста пользовательского запроса. Ищи все связанные записи по одномуoperation_id. /clear(только режим «Обучение») удаляет пользовательские данные (ToV, скриншоты, посты, идеи, настройки), не меняя подписку и триал; API-логи сохраняются.
Статистика и аналитика
/statsдоступна в админском форуме, топик «Статистика» (chat_id=-1008001427913,message_thread_id=5). Отчёт уходит в этот же топик Rich Message-таблицами.- По умолчанию
/statsпоказывает данные за всё время; отчёт содержит сопоставимые колонки «Сегодня», «7 дней», «30 дней», «Всё время». - Таблицы: активные пользователи; генерации постов, идеи, правки и сообщения; распределение типов сообщений в генерации; сообщения и типы сообщений в обучении.
- Периоды рассчитываются как текущие 24 часа, последние 7×24 часа, последние 30×24 часа и весь срок хранения соответственно.
- События хранятся в
/app/data/analytics.sqlite3, отдельной SQLite на persistent volume. Запись привязана кtelegram_user_id; общие значения агрегируются запросом без дублирующих таблиц. - Сохраняются только режим, тип действия, тип входящего сообщения и время. Содержимое сообщений, изображений и аудио аналитикой не сохраняется.
- В генерации учитываются действие генерации поста, идеи, правки и входящие сообщения по типам. В обучении — только входящие сообщения по типам.
- Вход в отчёт проверяет точные
chat_idиmessage_thread_id; при заданномOWNER_USER_IDкоманда доступна только этому Telegram-пользователю.
Команды
| Команда | Что делает | Режим |
|---|---|---|
/start |
Возврат в главное меню | Любой |
/training, /generate, /subscription, /help |
Переключение режима | Любой |
/style, /clear |
Профиль аккаунта; сброс накопленного обучения | Обучение |
/post, /idea |
Самостоятельный пост по обученному профилю; идея | Генерация; для генерации нужна подписка |
/balance, /subscribe |
Статус или оплата Stars | Подписка |
/support |
Написать в поддержку | Любой |
/stats |
Отчёт аналитики в админском топике | Топик «Статистика» |
В режиме «Генерация» русские фразы («придумай идею», «о чём написать?») работают как /idea.
Деплой и конфигурация
- Gitea:
admin/social-post-writer-bot, веткаdev. Push → автодеплой. - Coolify: проект
Social Post Writer Bot, окружениеdevelopment, приложениеsocial-post-writer-bot-development(d32u5zcq2g4odmdpq9hw91yn) наspb-easy-dale. - Деплой:
bash /root/.hermes/scripts/gitea_deploy.sh dev(POST/api/v1/deploy?uuid=...&force=false). - Окружение:
DEPLOY_ENVвключает dev-функции только при точном значенииdevelopment; production, пустое и неизвестное значение отключают их.BOT_ENVотдельно выбирает Telegram Bot API и не используется для включения dev-поведения. - VERSION: файл
VERSION(0.1.0+64). ПриDEPLOY_ENV=developmentи изменении версии бот шлёт уведомление о версии вDEV_CHAT_ID. - Переменные Coolify:
BOT_TOKEN,APINEX_API_KEY,APINEX_STT_MODEL,APINEX_INTENT_CLASSIFIER_MODEL,APINEX_INJECTION_CLASSIFIER_MODEL,APINEX_POST_WRITER_MODEL,APINEX_IMAGE_TONE_MODEL,APINEX_RESPONSE_REPAIR_MODEL(optional; default uses the standard model),BOT_DB_PATH,ANALYTICS_DB_PATH(optional; default/app/data/analytics.sqlite3),ANALYTICS_CHAT_ID(default-1008001427913),ANALYTICS_TOPIC_ID(default5),OWNER_USER_ID(optional; when set,/statsis restricted to this Telegram user),RAW_UPDATES_DB_PATH(optional; default/app/data/bot_updates.sqlite3),DEPLOY_ENV,DEV_CHAT_ID,BOT_LOG_PATH(optional; default/app/data/bot.log),ACTIVITY_MIN_SECONDS/ACTIVITY_MAX_SECONDS(optional; default 3 / 8 — границы паузы между сменами фраз в драфте),ACTIVITY_MAX_ROTATIONS(optional; default 12 — предохранитель от бесконечной ротации),ADMIN_GROUP_ID(id админской супергруппы с темами; production:-1004459624166— «Писатель постов: Админка»),SUPPORT_GROUP_ID(id супергруппы с темами для обращений; production:-1003957663233— «Писатель постов: Поддержка»; без неё поддержка выключена),SUPPORT_NOTE_PREFIX(optional; default//— префикс заметки, которая не уходит пользователю),SUPPORT_GREETINGS(optional; пул приветствий оператора, разделитель|),SUPPORT_TOPIC_EMOJI_ID(optional; custom emoji id на топике обращения). - Docker:
python:3.12-slim, копирует исходники бота, включаяraw_updates.py,support.pyиactivity_phrases.py.
Тесты
python -m unittest discover -s tests -v
Покрытие: STT, LLM, vision, ToV, reply-поля, изоляция пользователей, API-логи, raw update log (сохранение/чтение, выключение вне development, очистка по retention), режимные клавиатуры и роутинг (переключатели, возврат в главное меню), платежи (Stars, защита от повторов, dev-подтверждение), ошибки, справка, onboarding, подписки (статус при входе, продление, оплата, баланс, блокировка, сохранение подписки при /clear), самостоятельная генерация поста (гейт по флагу обучения, контекст из профиля), восстановление обрезанного JSON, поддержка (кнопка и клавиатура обращения, support=True оставляет только «Главное меню»).
Запускай через виртуальное окружение проекта: .venv/bin/python -m unittest discover -s tests (вне его aiogram может быть не установлен).