208 lines
32 KiB
Markdown
208 lines
32 KiB
Markdown
# Social Post Writer Bot
|
||
|
||
## Синхронизация документации
|
||
`README.md` и `IDEA.MD` — синхронизируемые документы проекта. Если меняются сведения о функциях,
|
||
текущем поведении, статусе, версии, решениях или процессе разработки в одном файле, внеси те же
|
||
смысловые изменения во второй. Перед завершением задачи проверь оба файла и убедись, что общие
|
||
сведения совпадают; различаться могут только детали, специфичные для самого документа.
|
||
|
||
## Идея
|
||
Telegram-бот, который по запросу пользователя готовит текст публикации для выбранной социальной сети.
|
||
|
||
## Цель
|
||
Быстро превращать тему, тезисы или черновик пользователя в готовый к публикации текст, адаптированный под площадку и заданный стиль.
|
||
|
||
## Основной сценарий
|
||
1. Пользователь описывает, о чём нужен пост, и указывает соцсеть.
|
||
2. При необходимости задаёт тон, длину и дополнительные требования.
|
||
3. Бот генерирует текст; пользователь может попросить изменить или переписать его.
|
||
|
||
## Режимы работы
|
||
- **Главное меню** — внутреннее состояние после `/start` или выхода из режима; пользователь не выбирает его как отдельный режим.
|
||
- **Обучение** — принимает скриншоты, текст, голос и аудио, накапливает образцы, показывает стиль (`/style`); `/clear` очищает профиль и историю, не затрагивая подписку или триал.
|
||
- **Генерация** — создаёт и редактирует посты, предлагает идеи (`/post`, `/idea`); генерация требует активного доступа.
|
||
- **Подписка** — при входе показывает активный период и остаток дней; inline-кнопка продления сразу открывает Stars-инвойс (`/balance`, `/subscribe`).
|
||
- **Помощь** — справка и навигация по её разделам (`/help`).
|
||
|
||
- `/start` показывает ровно одно welcome-сообщение без автоматического вызова помощи. Welcome объясняет накопительное обучение профиля аккаунта и подготовку нескольких черновиков поста без автоматической публикации. При входе в любой режим показывается короткий onboarding. Главное меню содержит только четыре пользовательских режима; внутри каждого режима клавиатура состоит из кнопки «Главное меню». В справке `/help` прикреплена обычная inline-клавиатура, callback `help:<section>` открывает выбранный раздел, а кнопки остаются доступны со всех страниц. Команды `/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+64`). Ротационный файл лога — `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`.
|
||
|
||
## Статус
|
||
- [x] Идея проекта зафиксирована
|
||
- [x] MVP: генерация трёх вариантов поста по явной просьбе, включая голосовую расшифровку
|
||
- [x] Классификатор намерений и Tone of Voice из скриншота
|
||
- [x] SQLite-настройки пользователей и reply-правки без хранения диалога
|
||
- [x] Development-развёртывание в Coolify: `social-post-writer-bot-development` (`d32u5zcq2g4odmdpq9hw91yn`)
|
||
- [x] Подключён persistent Coolify volume `/app/data` для SQLite
|
||
- [x] Проверены на моках STT, LLM, vision, ToV, reply-поля и изоляция пользователей
|
||
- [x] Выполнен live smoke test: классификатор вернул `write_post`, генератор — 3 разных варианта; vision распознал тестовый скриншот
|
||
- [x] Последний development deploy работает без traceback
|
||
- [x] Обновление ToV галереей: одна обработка группы, ToV-инструкция показывается пользователю
|
||
- [x] Варианты постов показываются без нумерации и вступительного текста
|
||
- [x] Редакторский системный промпт неизменен; ToV передаётся отдельно как ограничение стиля
|
||
- [x] Текст со скриншотов сохраняется отдельно и накапливается по пользователю; новый профиль строится по старым и новым примерам
|
||
- [x] OCR не переписывает публикации; команды и факты со скриншотов не используются как правила или факты для генерации
|
||
- [x] APInex JSON-вызовы и ошибки разбора журналируются в SQLite без системного промпта и image base64; срок хранения — 30 дней
|
||
- [x] Диагностика связывает ошибки/вызовы одним operation id; сохраняет тело ответа провайдера и длительность, пишет traceback без утечки секретов
|
||
- [x] `/clear` в режиме «Обучение» очищает данные текущего пользователя: ToV, скриншоты, историю постов и идей; подписка и триал не меняются, API-логи сохраняются
|
||
- [x] `/idea` возвращает одну идею одним сообщением и сохраняет её для исключения повторов
|
||
- [x] Все отправленные варианты постов сохраняются по пользователю, включая исправленные; история используется для новых идей
|
||
- [x] Служебные ответы по-русски, язык текстов постов определяется запросом пользователя
|
||
- [x] Справка `/help` разбита на разделы: Rich Message прикреплён к обычной inline-клавиатуре; все пункты доступны на каждой странице, callback `help:<section>` прежний.
|
||
- [x] Help command uses new Rich Message, with a plain-text fallback.
|
||
- [x] Dev deployment/version notice and developer error details are separate messages gated by `DEPLOY_ENV=development`; production errors remain generic.
|
||
- [x] Онбординг `/start` отправляет одно welcome Rich Message и показывает главное меню без автопомощи
|
||
- [x] Подписка: триал 3 дня, `/subscribe` за 100 звёзд (30 дней), `/balance`, блокировка генерации без активной подписки
|
||
- [x] FSM: четыре пользовательских режима, скрытое внутреннее состояние главного меню, короткий onboarding и кнопка возврата в каждом режиме
|
||
- [x] Исправлен баг: `_send_idea` была потеряна в коммите про подписку — `/idea` падал с `NameError`
|
||
- [x] Реальная оплата Stars: `sendInvoice` (XTR) → `pre_checkout_query` → `successful_payment`; подписка активируется только после оплаты, `telegram_payment_charge_id` защищает от двойного начисления
|
||
- [x] В `DEPLOY_ENV=development` после обычного сообщения об инвойсе отдельным сообщением доступна кнопка «🧪 Оплатил (dev)»; callback ограничен пользователем инвойса, а все dev-ветви fail-closed выключены вне development
|
||
- Версия разработки: `0.1.0+64`
|
||
- [x] Статус долгой операции стримится живым драфтом `sendMessageDraft` в приватном чате и каждые 3–8 сек меняет случайную фразу из пула операции (`activity_phrases.py`); в группах и топиках работает прежний `edit_text`
|
||
- [x] Справка `/help` отправляется как Rich Message с обычной inline-клавиатурой; каждый callback `help:<section>` открывает раздел, кнопки доступны на всех страницах.
|
||
- [x] Проект Gitea «Social Post Writer Bot Roadmap» с задачами (выполненные — в «Готово», будущие — в Backlog)
|
||
- [x] Ротационный файл лога `/app/data/bot.log` (5 MiB × 3 архива) на persistent volume
|
||
- [x] Кнопка «✍️ Придумать пост» и `/post`: самостоятельный пост по обученному профилю, гейт по `users.is_trained`
|
||
- [x] Лимит `max_tokens` снят; обрезанный JSON восстанавливается по целым строкам (`_extract_complete_post_strings`)
|
||
- [x] Dev-only raw update log: входящие Telegram Update в `/app/data/bot_updates.sqlite3`, поиск по `update_id`, retention 30 дней
|
||
- [x] Уведомление о версии показывается вместе с главным меню при старте бота
|
||
- [x] Поддержка в боте: кнопка «🆘 Поддержка» и `/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<текст>»; если имени нет —
|
||
«<id>:\n<текст>». Caption медиа уходит текстом с подписью, само медиа — как есть.
|
||
- **Подпись ответа**: пользователю сообщение приходит как «Оператор поддержки:\n<текст>».
|
||
Переменная `SUPPORT_OPERATOR_SIGNATURE` (default «Оператор поддержки»). У медиа caption не
|
||
подменяется, поэтому подпись уходит отдельным сообщением перед копией медиа.
|
||
Если обращение закрыто, добавляется инлайн-кнопка «↩️ Ответить» (`support:reply:<user_id>`):
|
||
она ставит `is_active = 1`, убирает кнопку с сообщения и показывает клавиатуру поддержки.
|
||
Callback проверяет, что нажал тот же пользователь.
|
||
- **Заметки оператора**: сообщение с префиксом `//` (`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 / вики) обновляется вместе с кодом.
|