Files
social-post-writer-bot/README.md
T
admin c85f0a34fe feat: /help разбита на разделы с навигацией инлайн-кнопками (aiogram 3.31.0)
- requirements.txt: aiogram 3.26.0 -> 3.31.0 (нативный send_rich_message + edit_message_text(rich_message=...))
- help_message.py: HELP_RICH_HTML -> HELP_SECTIONS (6 разделов) + help_menu_html/help_section_html/help_view/help_plain_view
- навигация на HTML-тегах <tg-button-row>/<tg-button type="callback_data"> (в меню 2 кнопки в ряд, в разделе «В меню» + «Предыдущий»/«Следующий»)
- bot.py: самописный raw-запрос sendRichMessage заменён на bot.send_rich_message; edit_rich_message + callback help_navigation
- онбординг по-прежнему вызывает help_command — кнопки появляются и там
- тест поймал реальный баг: в help_navigation пропущен await у edit_rich_message
- 58 тестов, VERSION 0.1.0+23
2026-10-06 23:15:47 +02:00

199 lines
14 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 — development
Telegram-бот для генерации постов для соцсетей. Готовит текст по явной просьбе пользователя, адаптируя под его Tone of Voice, и работает по модели подписки (триал 3 дня, затем 100 звёзд в месяц).
---
## Содержание
- [Как работает бот (UX)](#как-работает-бот-ux)
- [Архитектура кода](#архитектура-кода)
- [Подписка и монетизация](#подписка-и-монетизация)
- [Данные и хранилище](#данные-и-хранилище)
- [Команды](#команды)
- [Деплой и конфигурация](#деплой-и-конфигурация)
- [Тесты](#тесты)
---
## Как работает бот (UX)
### Онбординг (`/start`)
Первое сообщение пользователя запускает онбординг — **два сообщения с паузой 0.8 сек** между ними:
1. **Welcome (Rich Message)** — `WELCOME_RICH_HTML`: приветствие, примеры запросов, упоминание скриншотов для Tone of Voice.
2. **Вызов `help_command`** — пауза 0.8 сек, затем меню справки с инлайн-кнопками (тот же хендлер, что и по команде `/help`).
Одновременно `/start` создаёт запись триала на 3 дня.
**Логика:** welcome содержит только golden path, всё остальное (тон, голоса, правки, `/clear`, `/idea`) — в `/help`. Онбординг не дублирует текст справки, а **вызывает `help_command`**, поэтому правки `HELP_RICH_HTML` автоматически меняют и онбординг.
### Основные сценарии
| Что хочет пользователь | Что делает | Что видит |
|---|---|---|
| Написать пост | Пишет «напиши пост про запуск продукта» | Индикатор «печатает», затем **3 отдельных сообщения** — по одному варианту в каждом |
| Изменить пост | Отвечает реплаем на вариант бота: «сделай короче» | Снова 3 новых варианта, с учётом исходного текста |
| Придумать идею | «предложи тему» или `/idea` | Одна идея одним сообщением (заголовок, угол, зачем) |
| Настроить стиль | Присылает скриншот(ы) своих постов | Бот распознаёт тексты, накапливает их и обновляет ToV; показывает итоговый профиль стиля |
| Голосом | Отправляет голосовое | STT расшифровывает → как обычный текстовый запрос |
| Узнать статус | `/balance` | Триал активен (сколько дней) / подписка активна / период истёк |
| Оплатить | `/subscribe` | Просит 100 звёзд, подтверждает активацию на 30 дней |
| Начать заново | `/clear` | Полный сброс данных + обнуление подписки |
### Индикаторы и обратная связь
- Пока **идёт обработка** (классификация, расшифровка, чтение скриншота, генерация) — бот показывает «печатает» (`typing_status`), обновляя каждые 4 секунды.
- При **ошибке** пользователю уходит понятное сообщение: «❌ Не получилось: {этап}. Причина: …» — секреты (ключи, токены) и URL вырезаются.
- Служебные тексты — **всегда на русском**. Язык самого поста определяется языком запроса.
### Блокировка по подписке
Если подписка не активна (триал/подписка истекли или `is_active = 0`):
- Генерация поста и идеи **блокируется**, бот пишет, что период истёк, и предлагает `/subscribe`.
- `/help`, `/start`, `/balance`, `/subscribe` **доступны всегда** — пользователь не остаётся в тупике.
---
## Архитектура кода
```
bot.py — роутинг, команды, обработка сообщений (aiogram 3.x)
apinex_client.py — клиент APInex (LLM, STT, vision), промпты, логирование вызовов
user_store.py — SQLite: пользователи, ToV, посты, идеи, API-логи, подписки
help_message.py — Rich Message: `WELCOME_RICH_HTML` (онбординг) + `HELP_SECTIONS`/`help_view()` (справка с кнопками), plain fallback
VERSION — версия релиза (сейчас 0.1.0+23)
```
### Основные потоки
**Текстовый запрос:**
```
handle_text → _classify_and_respond
├─ /help? → help_command (без проверки подписки)
├─ _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 (пока всё идёт)
```
**Голосовое:**
```
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)` | Онбординг (welcome Rich Message + вызов `help_command`) + создание триала |
| `handle_command_text` | Роутинг команд: `/help`, `/start`, `/clear`, `/idea`, `/balance`, `/subscribe` |
| `_classify_and_respond` | Ядро: проверка подписки → классификация → генерация |
| `_check_subscription` | Проверяет активную подписку; если нет — показывает статус и блокирует |
| `_show_subscription_info` | Формирует сообщение о статусе (триал / подписка / истёк) |
| `balance_command` | `/balance` — статус подписки |
| `subscribe_command` | `/subscribe` — активация подписки (оплата 100 звёзд) |
| `clear_command` | `/clear` — сброс данных + обнуление подписки |
| `help_command` | `/help` — меню справки (Rich Message с кнопками) + fallback на plain text |
| `help_navigation` | Callback `help:<section>` → `edit_message_text(rich_message=...)` — переключение разделов |
| `edit_rich_message` | Редактирование текущего rich-сообщения на новый раздел |
| `typing_status` | Индикатор «печатает» во время обработки |
| `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` | Возвращает строку подписки |
| `_days_until` | Сколько дней осталось (триал — `trial_started_at + 3 дня`; подписка — `last_paid_at`); `-1` если истёкло/отключено |
---
## Подписка и монетизация
**Модель:** триал 3 дня → подписка 30 дней за **100 звёзд** (Telegram Stars).
**Константы** (`user_store.py`):
```python
TRIAL_DAYS = 3
SUBSCRIPTION_MONTH_DAYS = 30
SUBSCRIPTION_PRICE_STARS = 100
```
**Таблица `subscriptions`:**
- `telegram_user_id` (PK)
- `trial_started_at` — момент создания (SQLite `CURRENT_TIMESTAMP`, UTC-нейтральный)
- `last_paid_at` — момент/конец оплаченного периода; `NULL` = ещё триал
- `is_active` — 1/0
- `monthly_days` — длительность периода (30)
- `updated_at`
**Жизненный цикл:**
1. `/start` → `ensure_subscription` → триал 3 дня, `is_active = 1`.
2. Генерация доступна, пока `_days_until > 0`.
3. Триал кончился → `_check_subscription` блокирует, предлагает `/subscribe`.
4. `/subscribe` → `activate_subscription(months=1)` → +30 дней от момента оплаты, `is_active = 1`.
5. `/clear` → `deactivate_subscription` → `is_active = 0`; нужен `/subscribe`.
**Важно про оплату:** приём звёзд — **имитация**. Бот пишет «пришли 100 звёзд» и сразу активирует подписку, не проводя реальный платёж. Для настоящих платежей нужен отдельный invoicing-флоу (Telegram Payments / `sendInvoice` для Stars), что в этой версии не реализовано.
**Часовые пояса:** SQLite `CURRENT_TIMESTAMP` возвращает UTC без tz. Код аккуратно приводит все значения к naive UTC перед сравнением, чтобы не было сдвигов.
---
## Данные и хранилище
- SQLite: `/app/data/social_writer.sqlite3` (persistent Coolify volume).
- **Per-user данные:** ToV, образцы постов со скриншотов, история сгенерированных постов, история идей. История диалогов и исходные изображения **не сохраняются**.
- **API-логи:** JSON запросов/ответов APInex + ошибки разбора, 30 дней. Системные промпты не дублируются — только имя и SHA-256. base64 изображений и бинарное аудио **не пишутся**. Содержимое логов — приватные данные пользователя.
- `/clear` удаляет пользовательские данные (ToV, скриншоты, посты, идеи, настройки) и обнуляет подписку; **API-логи сохраняются**.
---
## Команды
| Команда | Что делает | Доступна без подписки |
|---|---|---|
| `/start` | Онбординг (welcome Rich Message + справка) + создание триала | ✅ |
| `/help` | Меню справки: разделы с инлайн-кнопками + plain fallback | ✅ |
| `/balance` | Статус подписки / триала | ✅ |
| `/subscribe` | Активация подписки (100 звёзд, 30 дней) | ✅ |
| `/idea` | Одна идея для поста | ❌ (нужна подписка) |
| `/clear` | Полный сброс данных + обнуление подписки | ✅ |
Русские фразы («придумай идею», «о чём написать?») работают как `/idea`.
---
## Деплой и конфигурация
- **Gitea:** `admin/social-post-writer-bot`, ветка `dev`. Push → автодеплой.
- **Coolify:** проект `Social Post Writer Bot`, окружение `development`, приложение `social-post-writer-bot-development` (`d32u5zcq2g4odmdpq9hw91yn`) на `spb-easy-dale`.
- **Деплой:** `.hermes/scripts/deploy.sh` (POST `/api/v1/deploy?uuid=...`).
- **VERSION:** файл `VERSION` (`0.1.0+19`). При `DEPLOY_ENV=development` и изменении версии бот шлёт уведомление «🚀 Бот обновлён / Версия: X» в `DEV_CHAT_ID`.
- **Переменные Coolify:** `BOT_TOKEN`, `APINEX_API_KEY`, `APINEX_STT_MODEL`, `APINEX_INTENT_CLASSIFIER_MODEL`, `APINEX_POST_WRITER_MODEL`, `APINEX_IMAGE_TONE_MODEL`, `BOT_DB_PATH`, `DEPLOY_ENV`, `DEV_CHAT_ID`.
- **Docker:** `python:3.12-slim`, копирует `bot.py`, `apinex_client.py`, `user_store.py`, `help_message.py`, `VERSION`; `CMD ["python", "bot.py"]`.
---
## Тесты
```bash
python -m unittest discover -s tests -v
```
Покрытие: STT, LLM, vision, ToV, reply-поля, изоляция пользователей, API-логи, **справка с кнопками** (меню, разделы, callback-навигация, plain fallback), **онбординг** (welcome → вызов `help_command`), **подписки** (6 тестов: таблица, триал после `/start`, оплата, баланс, блокировка, обнуление через `/clear`).