From ea0f7ee27cb3f1a8f57c2a7803f9759545e79854 Mon Sep 17 00:00:00 2001 From: admin Date: Fri, 9 Oct 2026 16:52:32 +0200 Subject: [PATCH] fix: keep training completion status concise --- IDEA.MD | 4 ++-- README.md | 6 +++--- VERSION | 2 +- bot.py | 19 ++++++++++--------- tests/test_keyboards.py | 14 ++++++++++++++ 5 files changed, 30 insertions(+), 15 deletions(-) diff --git a/IDEA.MD b/IDEA.MD index c4365bf..83f077e 100644 --- a/IDEA.MD +++ b/IDEA.MD @@ -43,7 +43,7 @@ Telegram-бот, который по запросу пользователя г - Служебные ответы пользователю — всегда на русском; тексты постов — на языке явного запроса. В production исключения не показываются; техническая диагностика приходит отдельным сообщением только в development. - Настройки пользователей и распознанные тексты публикаций для накопления Tone of Voice хранятся отдельно по Telegram user id; история диалогов и исходные изображения не сохраняются. - На явную просьбу поста и на реплай-правку бот возвращает 3 отдельных варианта. -- Во время обучения, чтения изображений, распознавания голосовых и генерации бот редактирует одно сообщение-статус по этапам; перед результатом показывает ✅, при ошибке — ❌. +- Во время обучения, чтения изображений, распознавания голосовых и генерации бот редактирует одно сообщение-статус по этапам; перед результатом показывает ✅, при ошибке — ❌. Итог обучения — только краткий статус, без вывода содержимого профиля. ## Подписка и монетизация - Модель: триал 3 дня, затем подписка 30 дней за 100 звёзд (Telegram Stars). @@ -97,7 +97,7 @@ Telegram-бот, который по запросу пользователя г - [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+39` +- Версия разработки: `0.1.0+40` - [x] Справка бота (`/help`) описывает `/balance` и `/subscribe` - [x] Проект Gitea «Social Post Writer Bot Roadmap» с задачами (выполненные — в «Готово», будущие — в Backlog) diff --git a/README.md b/README.md index 92fc218..d908bfe 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Telegram-бот для генерации постов для соцсетей. - [Деплой и конфигурация](#деплой-и-конфигурация) - [Тесты](#тесты) -Версия разработки: `0.1.0+39`. +Версия разработки: `0.1.0+40`. --- @@ -65,13 +65,13 @@ onboarding. В «Подписке» сразу отображаются тип | Написать пост | В режиме «Генерация» пишет «напиши пост про запуск продукта» | Индикатор «печатает», затем **3 отдельных сообщения** | | Изменить пост | В режиме «Генерация» отвечает реплаем на вариант бота | Три новых варианта с учётом исходного текста | | Придумать идею | В режиме «Генерация» пишет «предложи тему» или `/idea` | Одна идея одним сообщением | -| Настроить стиль | В режиме «Обучение» присылает скриншоты постов | Бот распознаёт тексты, накапливает их и обновляет ToV | +| Настроить стиль | В режиме «Обучение» присылает скриншоты постов | Бот распознаёт тексты, накапливает их, обновляет профиль и показывает только статус завершения | | Голосом | В режиме «Генерация» отправляет голосовое | STT расшифровывает → обычный текстовый запрос | | Узнать статус / продлить | Входит в режим «Подписка» и нажимает «Продлить подписку» | Текущий тип периода, остаток дней и Stars-инвойс на 100 звёзд | | Очистить данные | В режиме «Обучение» нажимает «Сбросить данные» или вводит `/clear`, затем подтверждает | Очищает стиль и историю, не меняя подписку или триал | ### Индикаторы и обратная связь -- Пока **идёт обработка** (классификация, расшифровка, чтение скриншота, генерация) — бот показывает «печатает» (`typing_status`, обновление каждые 4 секунды) и отдельное сообщение-статус, которое редактируется по этапам; перед результатом оно меняется на ✅, при ошибке — на ❌. +- Пока **идёт обработка** (классификация, расшифровка, чтение скриншота, генерация) — бот показывает «печатает» (`typing_status`, обновление каждые 4 секунды) и отдельное сообщение-статус, которое редактируется по этапам; перед результатом оно меняется на ✅, при ошибке — на ❌. После обучения в чате остаётся только краткий статус завершения, без вывода содержимого профиля. - При **ошибке** пользователь получает общий безопасный ответ. Только в development отдельным сообщением дополнительно отправляется диагностика; production не раскрывает текст исключения. - Служебные тексты — **всегда на русском**. Язык самого поста определяется языком запроса. diff --git a/VERSION b/VERSION index 445aad9..4db3d44 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.1.0+39 +0.1.0+40 diff --git a/bot.py b/bot.py index caaea38..58ef1fa 100644 --- a/bot.py +++ b/bot.py @@ -473,7 +473,7 @@ async def style_command(message: Message, state: FSMContext) -> None: await message.answer(f"Накоплено постов: {len(samples)}. Дополнений профиля: {len(notes)}.") -async def _rebuild_account_profile(user_id: int, message: Message, *, new_note: str = "", new_posts: list[str] | None = None, status=None) -> None: +async def _rebuild_account_profile(user_id: int, message: Message, *, new_note: str = "", new_posts: list[str] | None = None, status: "OperationStatus | None" = None) -> None: previous_profile = await asyncio.to_thread(get_tone_of_voice, user_id) if new_note.strip(): notes = await asyncio.to_thread(add_training_note, user_id, new_note) @@ -494,15 +494,16 @@ async def _rebuild_account_profile(user_id: int, message: Message, *, new_note: await message.answer("Не нашла достаточно материалов для обновления профиля.") return await asyncio.to_thread(save_tone_of_voice, user_id, profile) - if status: + if status is None: + rich_html = ( + "

✅ Профиль аккаунта обновлён

" + f"

Накоплено постов: {len(samples)}. Дополнений профиля: {len(notes)}.

" + f"

{html.escape(profile)}

" + "

Присылай новые посты или сведения в режиме «Обучение» — профиль будет дополняться.

" + ) + await send_rich_message(message, rich_html, "обновления профиля", reply_markup=mode_keyboard("training")) + else: await status.finish("✅ Обучение завершено") - rich_html = ( - "

✅ Профиль аккаунта обновлён

" - f"

Накоплено постов: {len(samples)}. Дополнений профиля: {len(notes)}.

" - f"

{html.escape(profile)}

" - "

Присылай новые посты или сведения в режиме «Обучение» — профиль будет дополняться.

" - ) - await send_rich_message(message, rich_html, "обновления профиля", reply_markup=mode_keyboard("training")) async def process_training_text(message: Message, text: str) -> None: diff --git a/tests/test_keyboards.py b/tests/test_keyboards.py index 609d902..5fd1957 100644 --- a/tests/test_keyboards.py +++ b/tests/test_keyboards.py @@ -238,6 +238,20 @@ class ModeRoutingTests(unittest.IsolatedAsyncioTestCase): self.assertEqual(call.kwargs["new_note"], "Кофейня в Калининграде для семей с детьми") self.assertIsInstance(call.kwargs["status"], bot.OperationStatus) + async def test_training_profile_rebuild_does_not_send_profile_details_when_status_exists(self): + status = AsyncMock() + message = _StubMessage() + with ( + patch.object(bot.asyncio, "to_thread", new=AsyncMock(side_effect=["old", ["note"], ["post"], None])), + patch.object(bot, "create_tone_profile", new=AsyncMock(return_value="new profile")), + patch.object(bot, "send_rich_message", new=AsyncMock()) as rich_send, + ): + await bot._rebuild_account_profile(42, cast(Any, message), new_note="note", status=status) + + status.finish.assert_awaited_once_with("✅ Обучение завершено") + rich_send.assert_not_awaited() + self.assertEqual(message.answers, []) + async def test_plain_text_is_only_sent_to_generator_in_generation_mode(self): message = _StubMessage("Напиши пост про запуск продукта") state = _StubState("generation")