docs: подробная документация (UX + код), фикс дублей, /help без подписки, v0.1.0+19
This commit is contained in:
1 parent
f5d6fa95bd
commit
a2927052f4
5 files changed
+243
-123
No files matched your search
@@ -1,25 +1,197 @@
|
||||
# Social Post Writer Bot — development
|
||||
|
||||
The bot prepares social-media posts only when the user explicitly asks. It transcribes Russian voice notes with APInex STT, classifies intent with an LLM, and uses Gemini 3.8 Flash via APInex to write/revise posts or infer Tone of Voice from a screenshot. A post request/revision returns three separate messages. The bot shows the Telegram typing indicator while classifying, transcribing, reading images, and generating. It stores no conversation history.
|
||||
Telegram-бот для генерации постов для соцсетей. Готовит текст по явной просьбе пользователя, адаптируя под его Tone of Voice, и работает по модели подписки (триал 3 дня, затем 100 звёзд в месяц).
|
||||
|
||||
- Runtime: Python 3.12, aiogram 3.x
|
||||
- Release version is stored in `VERSION` and incremented for each code change; dev sends a version notice on boot.
|
||||
- In development, processing errors are returned to the user with a concise, secret-redacted reason.
|
||||
- Coolify: project `Social Post Writer Bot`, environment `development`, server `spb-easy-dale`
|
||||
- Gitea: `admin/social-post-writer-bot`, branch `dev`
|
||||
- API key and model variables are stored only in Coolify: `APINEX_API_KEY`, `APINEX_STT_MODEL`, `APINEX_INTENT_CLASSIFIER_MODEL`, `APINEX_POST_WRITER_MODEL`, `APINEX_IMAGE_TONE_MODEL`.
|
||||
- APInex call and parsing logs live in SQLite at `/app/data/social_writer.sqlite3` for 30 days. They store user input/output JSON and error details; system prompt text is omitted in favor of a prompt name and SHA-256. Inline image base64 and binary audio are omitted. Treat log contents as private user data.
|
||||
- `/clear` полностью очищает данные текущего пользователя: Tone of Voice, тексты со скриншотов, историю постов, историю идей и настройки; API-логи сохраняются.
|
||||
- `/idea` и русские запросы на идеи возвращают одну идею одним сообщением и сохраняют её для исключения повторов.
|
||||
- User-facing service text is always Russian; generated post variants follow the requested post language. Verbatim technical error detail may remain in its original language.
|
||||
- LLM model env defaults are `gemini-3.8-flash`; STT model defaults to APInex `nova-2`.
|
||||
- Per-user Tone of Voice lives in SQLite at `/app/data/social_writer.sqlite3`; persistent Coolify storage must be mounted at `/app/data`.
|
||||
- The Tone of Voice response shows the extracted style instructions.
|
||||
- Each generated post is sent as its own message containing only the post text; retain the model's line breaks, paragraphs and emojis, without headings or explanations.
|
||||
- A screenshot gallery is processed once as one update. APInex transcribes the visible posts without rewriting; OCR text is appended to per-user SQLite samples, and the style profile is regenerated from accumulated examples plus the prior profile. Duplicate texts are stored once.
|
||||
- The immutable post-writer system prompt applies the writing/editorial brief; the per-user screenshot-derived ToV is passed separately as input data and can tune style only, never overriding system rules or supplying facts.
|
||||
- Telegram screenshots are sent to APInex as inline base64 data URLs; source images are not persisted. Recognized post text is stored separately for each Telegram user to preserve style samples.
|
||||
---
|
||||
|
||||
## Local tests
|
||||
## Содержание
|
||||
- [Как работает бот (UX)](#как-работает-бот-ux)
|
||||
- [Архитектура кода](#архитектура-кода)
|
||||
- [Подписка и монетизация](#подписка-и-монетизация)
|
||||
- [Данные и хранилище](#данные-и-хранилище)
|
||||
- [Команды](#команды)
|
||||
- [Деплой и конфигурация](#деплой-и-конфигурация)
|
||||
- [Тесты](#тесты)
|
||||
|
||||
Run `python -m unittest discover -s tests -v`.
|
||||
---
|
||||
|
||||
## Как работает бот (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`).
|
||||
Reference in new issue
Block a user