diff --git a/Developer-Guide.md b/Developer-Guide.md deleted file mode 100644 index 657ddf7..0000000 --- a/Developer-Guide.md +++ /dev/null @@ -1,167 +0,0 @@ -# Разработчику - -## Структура - -``` -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 — версия релиза -Dockerfile — python:3.12-slim -requirements.txt — зависимости -tests/ — юнит-тесты (unittest) -``` - ---- - -## Потоки обработки - -### Текстовый запрос -``` -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 -``` - -### Скриншоты (Tone of Voice) -``` -update_tone_from_image → _handle_image_message - ├─ _collect_media_group (собирает галерею, ждёт MEDIA_GROUP_COLLECT_SECONDS = 1.0) - ├─ 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` | Индикатор «печатает», обновляется каждые 4 сек | -| `report_error` | Понятная ошибка пользователю; секреты и URL вырезаются | -| `split_text` | Разбивает длинный текст по лимиту (4000) по границам слов | - -### `user_store.py` - -| Функция | Что делает | -|---|---| -| `init_db` | Создаёт таблицы (см. ниже) | -| `ensure_subscription` | Создаёт запись подписки с триалом (`INSERT OR IGNORE`) | -| `activate_subscription` | Активирует/продлевает подписку на 30 дней | -| `deactivate_subscription` | `is_active = 0` | -| `is_subscribed` | Проверяет: запись есть, `is_active = 1`, срок не истёк | -| `subscription_row` | Возвращает строку подписки | -| `_days_until` | Сколько дней осталось; `-1` если истёкло/отключено | -| `add_post_samples` | Накапливает образцы постов (без дублей) | -| `save_generated_posts` | Сохраняет сгенерированные посты с `origin` | -| `save_idea` / `get_idea_history` | История идей | -| `save_api_call_log` | Пишет JSON лог вызова APInex | -| `clear_user_data` | Полный сброс данных пользователя | - -### `apinex_client.py` - -| Функция | Что делает | -|---|---| -| `classify_intent` | Определяет намерение: `write_post` / `revise_post` / `generate_ideas` / `help` | -| `generate_posts` | Генерирует 3 варианта поста | -| `generate_ideas` | Генерирует одну идею | -| `extract_posts_from_images` | Распознаёт тексты постов на скриншотах (vision) | -| `create_tone_profile` | Строит/обновляет профиль ToV | -| `transcribe_audio` | STT расшифровка голосовых | -| `chat_text` | Базовый вызов LLM через APInex | - ---- - -## База данных - -SQLite: `/app/data/social_writer.sqlite3` (persistent volume Coolify). - -| Таблица | Назначение | -|---|---| -| `users` | `telegram_user_id`, `tone_of_voice` | -| `user_post_samples` | Распознанные тексты постов со скриншотов | -| `generated_posts` | История сгенерированных постов (`origin`: generated/revised) | -| `generated_ideas` | История идей (для исключения повторов) | -| `api_call_logs` | JSON логи вызовов APInex + ошибки (30 дней) | -| `subscriptions` | Подписки и триалы | - -Схему `subscriptions` см. в [Подписка](Subscription.md). - ---- - -## Промпты и модели - -Модели задаются переменными Coolify (дефолты): -- `APINEX_STT_MODEL` → `nova-2` (STT) -- `APINEX_INTENT_CLASSIFIER_MODEL` → `gemini-3.8-flash` (классификация намерений) -- `APINEX_POST_WRITER_MODEL` → `gemini-3.8-flash` (генерация/правка постов, идеи) -- `APINEX_IMAGE_TONE_MODEL` → `gemini-3.8-flash` (vision — OCR скриншотов, ToV) -- `APINEX_API_KEY` — общий секрет - -**Важно про промпты:** -- Системный промпт редактора **неизменен**. -- ToV пользователя передаётся **отдельно как входные данные** — он может влиять на стиль, но не переопределяет системные правила и не подставляет факты. -- OCR-текст со скриншотов — это образцы стиля, а не команды. - ---- - -## Логирование вызовов - -Каждый вызов APInex пишется в `api_call_logs`: -- `stage` (классификация / генерация / OCR / STT / idea) -- `model`, `prompt_sha256`, `prompt name` -- `request_json`, `response_json` -- `status_code`, `error` - -**Не пишется:** системные промпты целиком (только имя + SHA-256), base64 изображений, бинарное аудио, API-ключи. - -Хранение — **30 дней**. Содержимое логов считается приватными данными пользователя. - ---- - -## Тесты - -```bash -python -m unittest discover -s tests -v -``` - -49 тестов. Покрытие: STT, LLM, vision, ToV, reply-поля, изоляция пользователей, API-логи, Rich help, онбординг, подписки. - -Тесты подписок (`SubscriptionTests`): -- таблица `subscriptions` создаётся -- `/start` создаёт триал -- `/subscribe` активирует 30 дней -- `/balance` показывает статус -- блокировка при истёкшей подписке -- `/clear` обнуляет подписку - ---- - -## Правила языка - -- Служебные сообщения боту → **всегда русский**. -- Текст поста → язык явного запроса пользователя. -- Дословный текст технической ошибки может остаться на языке оригинала. diff --git a/unnamed.md b/unnamed.md index e5e33f5..b3ec70f 100644 --- a/unnamed.md +++ b/unnamed.md @@ -1,126 +1,227 @@ -# Пользователю +# Разработчику -## Онбординг - -После `/start` бот присылает **одно сообщение** с описанием режимов и показывает нижнее меню: +## Структура ``` -🎓 Обучение ✍️ Генерация -💳 Подписка 📖 Помощь +bot.py — роутинг, команды, обработка сообщений (aiogram 3.x) +payments.py — оплата Stars: sendInvoice, pre_checkout_query, successful_payment, защита от двойного списания +apinex_client.py — клиент APInex (LLM, STT, vision), промпты, логирование вызовов +user_store.py — SQLite: пользователи, ToV, посты, идеи, API-логи, подписки +raw_updates.py — отдельная SQLite-БД сырых входящих Telegram Update (только development) +keyboards.py — клавиатуры главного меню и режимов; маппинг кнопок на команды +help_message.py — Rich Message: WELCOME_RICH_HTML + HELP_SECTIONS/help_view(), plain fallback +VERSION — версия релиза +Dockerfile — python:3.12-slim +requirements.txt — зависимости +tests/ — юнит-тесты (unittest) ``` -Одновременно `/start` создаёт пробный период на **3 дня**. Справка автоматически не открывается — она доступна кнопкой «📖 Помощь» или командой `/help`. +--- -Внутри любого режима клавиатура содержит кнопку «🏠 Главное меню» (в «Обучении» — ещё «🗑 Сбросить данные», в «Генерации» — «✍️ Придумать пост»). +## Режимы (FSM) + +`BotMode` содержит `lobby`, `training`, `generation`, `subscription`, `help`. «Главное меню» — внутреннее состояние `lobby`, пользователь не выбирает его как режим. + +- `current_mode(state)` / `enter_mode(...)` / `require_mode(...)` — проверка и переключение. +- Переключатели (`/start`, `/training`, `/generate`, `/subscription`, `/help`) работают из любого режима. +- Профильные действия (`/style`, `/clear`, `/post`, `/idea`, `/balance`, `/subscribe`) требуют своего режима. --- -## Основные сценарии +## Потоки обработки -### Написать пост -Перейди в режим «Генерация» и напиши или надиктуй прямую просьбу: -- «Напиши пост про запуск нового продукта» -- «Сделай пост-напоминание о вебинаре в официальном тоне» -- «Пост для Telegram: анонс скидки, коротко, с эмодзи» +### Текстовый запрос +``` +handle_text (только режим generation) → _classify_and_respond + ├─ classify_prompt_injection → блокирует попытку изменить правила + ├─ _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 (индикатор «печатает», обновление каждые 4 сек) -Можно указать площадку, аудиторию, факты, длину и цель — чем подробнее ТЗ, тем точнее результат. +handle_command_text → переключатели доступны всегда; действия проходят режимную проверку +``` -**Что получишь:** три отдельных сообщения, в каждом — только текст поста (без нумерации и вступлений, с абзацами и уместными эмодзи). +### Самостоятельный пост (`/post` или кнопка «✍️ Придумать пост») +``` +handle_command_text → _generate_post_from_training + ├─ _check_subscription + ├─ is_user_trained? нет → просьба обучить, модель НЕ вызывается + ├─ gather(get_tone_of_voice, get_post_samples, get_training_notes) → контекст ≤ 8000 + └─ generate_posts(requirements = контекст как недоверенные данные) → _send_three_posts +``` -### Пост по изображению -В режиме «Генерация» отправь изображение или галерею **с подписью** — подпись считается ТЗ, и бот вернёт три варианта поста по изображениям. Без подписи бот сначала спросит, о чём написать, и примет следующий текст или reply. Видео, видеокружки и GIF/анимации не поддерживаются. +### Голосовое +``` +transcribe_voice → _process_voice → transcribe_audio (APInex STT) → _classify_and_respond +``` -### Изменить пост -Ответь **реплаем** на сообщение с вариантом и напиши, что изменить: -- «Сделай короче и добавь призыв» -- «Добавь юмора» -- «Сделай официальный тон» +### Изображения +- В режиме **training**: `_handle_image_message` → извлечение текстов → накопление образцов → пересборка ToV. +- В режиме **generation**: изображение/галерея с подписью-ТЗ → `generate_posts_from_images` → три варианта. Без подписи бот запрашивает ТЗ. +- Видео, видеокружки, GIF/анимации отклоняются во всех режимах. -Бот учтёт исходный текст и вернёт три новых варианта. +### Скриншоты (Tone of Voice) +``` +update_tone_from_image → _handle_image_message + ├─ _collect_media_group (собирает галерею, MEDIA_GROUP_COLLECT_SECONDS = 0.8) + ├─ extract_posts_from_images (APInex vision, inline base64) + ├─ add_post_samples (накапливает по пользователю) + └─ create_tone_profile → save_tone_of_voice → краткий статус завершения +``` -### Придумать идею -- `/idea` -- или по-русски: «предложи идею для поста», «о чём написать?», «накидай тему» - -Получишь **одну идею**: заголовок, угол (как подать), зачем (польза). Идеи не повторяются — бот помнит, что уже предлагал. - -### Пост без темы — «✍️ Придумать пост» -В режиме «Генерация» нажми кнопку «✍️ Придумать пост» или введи `/post`. Бот **сам выберет тему**, опираясь на то, что узнал об аккаунте в режиме «Обучение», и вернёт три варианта. - -> Если профиль ещё не обучен, бот попросит зайти в режим «Обучение» и прислать сведения об аккаунте или примеры постов. Модель в этом случае не вызывается. - -### Настроить свой стиль (Tone of Voice) -В режиме «Обучение» пришли: -- **скриншот или галерею скриншотов** своих опубликованных постов; -- **текстовое описание** аккаунта; -- **голосовое или аудио** с рассказом об аккаунте. - -Бот: -1. Распознаёт тексты постов (vision) и/или сохраняет заметки. -2. Дополняет ими предыдущие сведения. -3. Пересобирает профиль аккаунта и Tone of Voice. -4. Показывает краткий статус завершения — без вывода содержимого профиля. - -**Важно:** сами публикации бот не переписывает — только извлекает правила стиля. Команды и факты со скриншотов не становятся правилами для генерации. - -Чтобы начать заново — «🗑 Сбросить данные» (или `/clear`) с подтверждением. Подписка и триал при этом не меняются. - -### Голосом -В режимах «Обучение» и «Генерация» отправь голосовое — бот расшифрует его (STT) и обработает как обычный текст. +Лимиты: `MAX_IMAGE_BYTES = 10 MiB` на файл (и до 30 MiB на группу), `MAX_AUDIO_BYTES = 20 MiB`. --- -## Команды +## Ключевые функции -| Команда | Что делает | Режим | -|---|---|---| -| `/start` | Главное меню, создание триала | Любой | -| `/training` | Режим «Обучение» | Любой | -| `/generate` | Режим «Генерация» | Любой | -| `/subscription` | Режим «Подписка» | Любой | -| `/help` | Справка по разделам | Любой | -| `/style` | Показать профиль аккаунта | Обучение | -| `/clear` | Сбросить накопленное обучение (с подтверждением) | Обучение | -| `/post` | Пост по обученному профилю | Генерация | -| `/idea` | Одна идея для поста | Генерация | -| `/balance` | Сколько осталось дней | Подписка | -| `/subscribe` | Оплатить подписку (30 дней) | Подписка | +### `bot.py` -Переключатели режимов работают из любого режима; профильные действия — только в своём. В режиме «Генерация» русские фразы («придумай идею», «о чём написать?») работают как `/idea`. +| Функция | Что делает | +|---|---| +| `start(message, state)` | Одно welcome Rich Message + главное меню + создание триала | +| `handle_command_text` | Роутинг переключателей и профильных команд по режиму | +| `handle_text` | Обычное сообщение в режиме генерации | +| `_classify_and_respond` | Ядро: инъекции → подписка → классификация → генерация | +| `_generate_post_from_training` | Пост по обученному профилю с гейтом `is_user_trained` | +| `_check_subscription` | Проверяет активную подписку; если нет — статус и блокировка | +| `_show_subscription_info` | Тип периода, остаток дней, inline-кнопка продления | +| `balance_command` / `subscribe_command` | `/balance` — статус; `/subscribe` — Stars-инвойс | +| `handle_pre_checkout` | Подтверждает только свой инвойс в XTR на 100 звёзд | +| `handle_successful_payment` | Активирует подписку после реальной оплаты | +| `clear_command` | `/clear` и «Сбросить данные» — inline-подтверждение, сброс только в режиме обучения | +| `style_command` | `/style` — профиль аккаунта | +| `help_command` / `help_navigation` | Справка Rich Message + переключение разделов | +| `notify_development_deploy` | Dev-уведомление о новой версии в `DEV_CHAT_ID` | +| `typing_status` | Индикатор «печатает» | +| `report_error` | Понятная ошибка пользователю; секреты вырезаны | +| `is_development_environment` | `DEPLOY_ENV == "development"` — гейт всех dev-ветвей | +| `configure_raw_update_logging` | Подключает `RawUpdateMiddleware` только в development | + +### `user_store.py` + +| Функция | Что делает | +|---|---| +| `init_db` | Создаёт/мигрирует таблицы | +| `ensure_subscription` | Создаёт запись подписки с триалом (`INSERT OR IGNORE`) | +| `activate_subscription` | Активирует/продлевает на 30 дней | +| `deactivate_subscription` | `is_active = 0` | +| `is_subscribed` | Запись есть, `is_active = 1`, срок не истёк | +| `subscription_row` | Строка подписки | +| `is_user_trained` | Флаг `users.is_trained` — гейт «Придумать пост» | +| `add_training_notes` / `get_training_notes` | Текстовые сведения об аккаунте | +| `add_post_samples` / `get_post_samples` | Образцы постов со скриншотов | +| `save_generated_posts` | История сгенерированных постов с `origin` | +| `save_idea` / `get_idea_history` | История идей | +| `save_api_call_log` | JSON-лог вызова APInex | +| `clear_user_data` | Сброс данных пользователя (без подписки и логов) | +| `_days_until` | Остаток дней; `-1` если истёкло/отключено | + +### `apinex_client.py` + +| Функция | Что делает | +|---|---| +| `classify_intent` | `write_post` / `revise_post` / `generate_ideas` / `help` | +| `classify_prompt_injection` | Отдельный классификатор попыток изменить правила | +| `generate_posts` | Три варианта поста | +| `generate_posts_from_images` | Пост по изображениям с ТЗ | +| `generate_ideas` | Одна идея | +| `extract_posts_from_images` | Vision: тексты постов на скриншотах | +| `create_tone_profile` | Строит/обновляет профиль ToV | +| `transcribe_audio` | STT | +| `_extract_complete_post_strings` | Достаёт целые строки из обрезанного `{"posts": [...]}` | +| `chat_text` | Базовый вызов LLM | --- -## Подписка +## База данных -- **Триал:** 3 дня бесплатно с момента первого `/start`. -- **Дальше:** 30 дней за **100 звёзд** (Telegram Stars). -- **Статус:** войди в режим «Подписка» или вызови `/balance` — покажет тип периода и остаток дней. Кнопка «Продлить подписку» сразу открывает счёт в звёздах. +Рабочая БД: `/app/data/social_writer.sqlite3` (persistent volume Coolify). -Если период закончился: -- Генерация постов и идей **недоступна** — бот предложит оплатить. -- Переходы между режимами, `/help`, `/balance`, `/subscribe` работают **всегда**. +| Таблица | Назначение | +|---|---| +| `users` | `telegram_user_id`, `tone_of_voice`, `is_trained` | +| `user_post_samples` | Распознанные тексты постов со скриншотов | +| `generated_posts` | История сгенерированных постов (`origin`: generated/revised) | +| `generated_ideas` | История идей (для исключения повторов) | +| `api_call_logs` | JSON-логи вызовов APInex + ошибки (30 дней) | +| `subscriptions` | Подписки и триалы | +| `payments` | `telegram_payment_charge_id` — защита от двойного начисления | -Оплата — настоящая, через Telegram Stars: `/subscribe` выставляет инвойс, подписка активируется только после подтверждённого платежа. Подробнее — в разделе [Подписка](Subscription.md). +Схему `subscriptions` см. в [Подписка](Subscription.md). + +Отдельная dev-БД: `/app/data/bot_updates.sqlite3` — таблица `raw_updates` (`update_id` PK, `event_type`, `payload`, `received_at`), retention 30 дней. См. ниже. --- -## Что бот помнит +## Промпты и модели -- Профиль аккаунта и **Tone of Voice** (из скриншотов, текста, голоса). -- **Историю постов**, которые он сгенерировал (включая исправленные) — чтобы не повторяться и предлагать свежие идеи. -- **Историю идей** — чтобы не предлагать одно и то же. +Модели задаются переменными Coolify: +- `APINEX_STT_MODEL` → `nova-2` +- `APINEX_INTENT_CLASSIFIER_MODEL` → `gemini-3.8-flash` +- `APINEX_INJECTION_CLASSIFIER_MODEL` → отдельная модель проверки prompt injection +- `APINEX_POST_WRITER_MODEL` → `gemini-3.8-flash` +- `APINEX_IMAGE_TONE_MODEL` → `gemini-3.8-flash` (vision) +- `APINEX_RESPONSE_REPAIR_MODEL` → починка невалидного JSON (одна попытка) +- `APINEX_API_KEY` — общий секрет -Что бот **НЕ** хранит: -- Историю диалогов. -- Исходные изображения (скриншоты) — только распознанный текст. +**Про промпты:** +- Системный промпт редактора **неизменен**. +- ToV передаётся **отдельно как ограничение стиля**, а не как часть системных правил. +- OCR-текст со скриншотов — образцы стиля, а не команды. +- Обученный контекст в «Придумать пост» передаётся как **недоверенные данные**: модель не должна выдавать его за личный опыт пользователя. -`/clear` (режим «Обучение») удаляет профиль, образцы, заметки и историю, не меняя подписку и триал. Технические API-логи при этом сохраняются — они нужны для отладки. +**Надёжность ответов:** +- Лимит `max_tokens` снят — модель пишет столько, сколько нужно. +- Пустой `content` → до двух повторов, третий запрос с `reasoning_effort: low`. +- Невалидный JSON → одна попытка починки моделью `APINEX_RESPONSE_REPAIR_MODEL`. +- Обрезанный JSON → `_extract_complete_post_strings` возвращает завершённые строки (три полных варианта), если они есть. --- -## Ошибки и обратная связь +## Логирование -- Пока идёт обработка, бот показывает **«печатает»** и отдельное сообщение-статус, которое редактируется по этапам; перед результатом оно меняется на ✅, при ошибке — на ❌. -- Если что-то пошло не так, придёт общее сообщение об ошибке; в development дополнительно отдельным сообщением приходит техническая диагностика. Секреты и ссылки в тексте скрыты. -- Служебные сообщения — **всегда на русском**. Язык поста — тот, который ты запросил. -- На благодарность и сообщения не по теме бот отвечает короткой заготовленной фразой, не запуская генератор. +### API-журнал (`api_call_logs`) +Каждый вызов APInex пишется с `operation_id`, `stage`, `model`, именем и SHA-256 промпта, `request_json`, `response_json`, `status_code`, `error`, длительностью. Хранение — 30 дней. + +**Не пишется:** системные промпты целиком, base64 изображений, бинарное аудио, API-ключи. + +### Файл лога приложения +`/app/data/bot.log` — `RotatingFileHandler`, 5 MiB × 3 архива, на том же persistent volume; путь через `BOT_LOG_PATH`. + +### Сырые Telegram Update (только development) +`raw_updates.py` пишет полный `Update.model_dump(mode="json")` в отдельную БД `/app/data/bot_updates.sqlite3` (путь через `RAW_UPDATES_DB_PATH`). Middleware `RawUpdateMiddleware` подключается на `dispatcher.update.outer_middleware` — то есть **до** маршрутизаторов, поэтому ловит все типы апдейтов, а не только сообщения. + +```sql +SELECT payload FROM raw_updates WHERE update_id = ?; +``` + +- Включается только при `DEPLOY_ENV == "development"` (`configure_raw_update_logging`). +- Retention 30 дней: чистка при старте и при каждой записи. +- Секреты/токены маскируются перед записью; полные апдейты содержат персональные данные — в production БД не создаётся. + +### Логи контейнера +Ошибки содержат `operation_id`, Telegram user id, этап и traceback; по одному `operation_id` находятся все связанные записи. + +--- + +## Тесты + +```bash +.venv/bin/python -m unittest discover -s tests -v +``` + +155 тестов (на версии `0.1.0+48`). Покрытие: STT, LLM, vision, ToV, reply-поля, изоляция пользователей, API-логи, **raw update log**, режимные клавиатуры и роутинг, платежи (Stars, защита от повторов, dev-подтверждение), ошибки, справка, онбординг, подписки, самостоятельная генерация поста, восстановление обрезанного JSON, индикатор «печатает». + +> Запускай через `.venv` проекта: вне виртуального окружения `aiogram` может быть не установлен. + +--- + +## Правила языка + +- Служебные сообщения боту → **всегда русский**. +- Текст поста → язык явного запроса пользователя. +- Дословный текст технической ошибки может остаться на языке оригинала.