Files
social-post-writer-bot/README.md
T

198 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. **Что делаю** — «Привет! Я помогаю писать посты для соцсетей. Расскажи, о чём нужен пост — а я верну три варианта: короткий, развёрнутый и с призывом к действию.»
2. **Примеры запросов** — три сценария: написать пост, официальный тон, реплай-правка.
3. **Стиль и идеи** — «пришли скриншоты своих постов, чтобы я запомнил стиль. Хочешь идею — напиши «предложи тему». Подробно — /help»
Одновременно `/start` создаёт запись триала на 3 дня.
**Логика:** golden path (основной путь) показываем сразу, всё остальное (тон, голоса, правки, `/clear`, `/idea`) — в `/help`. Пользователь не тонет в тексте.
### Основные сценарии
| Что хочет пользователь | Что делает | Что видит |
|---|---|---|
| Написать пост | Пишет «напиши пост про запуск продукта» | Индикатор «печатает», затем **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 помощь (HTML + plain fallback)
VERSION — версия релиза (сейчас 0.1.0+18)
```
### Основные потоки
**Текстовый запрос:**
```
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)` | Онбординг (3 сообщения) + создание триала |
| `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 |
| `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` | Онбординг (3 сообщения) + создание триала | ✅ |
| `/help` | Подробная справка (Rich Message + 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+18`). При `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-логи, Rich help, **подписки** (6 тестов: таблица, триал после `/start`, оплата, баланс, блокировка, обнуление через `/clear`).