fix: keep training completion status concise

This commit is contained in:
admin committed 2026-10-09 16:52:32 +02:00
1 parent 3e33a9deb5
commit ea0f7ee27c
5 files changed
+30 -15

No files matched your search

+2 -2
View File
@@ -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)
+3 -3
View File
@@ -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 не раскрывает текст исключения.
- Служебные тексты — **всегда на русском**. Язык самого поста определяется языком запроса.
+1 -1
View File
@@ -1 +1 @@
0.1.0+39
0.1.0+40
+10 -9
View File
@@ -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 = (
"<h2>✅ Профиль аккаунта обновлён</h2>"
f"<p>Накоплено постов: {len(samples)}. Дополнений профиля: {len(notes)}.</p>"
f"<p>{html.escape(profile)}</p>"
"<p>Присылай новые посты или сведения в режиме «Обучение» — профиль будет дополняться.</p>"
)
await send_rich_message(message, rich_html, "обновления профиля", reply_markup=mode_keyboard("training"))
else:
await status.finish("✅ Обучение завершено")
rich_html = (
"<h2>✅ Профиль аккаунта обновлён</h2>"
f"<p>Накоплено постов: {len(samples)}. Дополнений профиля: {len(notes)}.</p>"
f"<p>{html.escape(profile)}</p>"
"<p>Присылай новые посты или сведения в режиме «Обучение» — профиль будет дополняться.</p>"
)
await send_rich_message(message, rich_html, "обновления профиля", reply_markup=mode_keyboard("training"))
async def process_training_text(message: Message, text: str) -> None:
+14
View File
@@ -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")