Files
social-post-writer-bot/IDEA.MD
T

208 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 / вики) обновляется вместе с кодом.