feat: онбординг после /start — welcome Rich Message + вызов help_command

- help_message.py: добавлен WELCOME_RICH_HTML (приветствие, примеры, скриншоты)
- bot.py: send_rich_help -> send_rich_html(html, stage) + send_rich_message с plain-fallback
- bot.py: start теперь шлёт welcome Rich Message, пауза 0.8с, затем help_command
- help_command вызывает общий send_rich_message — правки HELP_RICH_HTML меняют и онбординг
- тесты: OnboardingTests (3), README/IDEA/VERSION 0.1.0+22
This commit is contained in:
admin committed 2026-10-06 22:57:47 +02:00
1 parent d618fd8a61
commit 0ff0e1abe7
6 files changed
+78 -44

No files matched your search

+2 -2
View File
@@ -44,7 +44,7 @@ Telegram-бот, который по запросу пользователя г
- Coolify приложение: `social-post-writer-bot-development` (`d32u5zcq2g4odmdpq9hw91yn`) на `spb-easy-dale`.
- SQLite: `/app/data/social_writer.sqlite3`; persistent volume `d32u5zcq2g4odmdpq9hw91yn-social-post-writer-dev-data` монтируется в `/app/data`.
- Секреты хранятся в переменных окружения приложения Coolify; не сохраняются в репозитории.
- `DEPLOY_ENV=development`, `DEV_CHAT_ID` — для уведомления о новой версии; версия в файле `VERSION` (сейчас `0.1.0+19`).
- `DEPLOY_ENV=development`, `DEV_CHAT_ID` — для уведомления о новой версии; версия в файле `VERSION` (сейчас `0.1.0+22`).
- Вики Gitea: страницы `Home`, `User-Guide`, `Developer-Guide`, `Subscription`, `Deployment`.
## Статус
@@ -69,7 +69,7 @@ Telegram-бот, который по запросу пользователя г
- [x] Служебные ответы по-русски, язык текстов постов определяется запросом пользователя
- [x] Help command uses new Rich Message, with a plain-text fallback.
- [x] Development deployment/version notice and user-visible error details.
- [x] Онбординг после `/start`: три сообщения (что делаю → примеры → стиль и идеи), golden path, остальное в `/help`
- [x] Онбординг после `/start`: welcome Rich Message (`WELCOME_RICH_HTML`) → пауза → вызов `help_command`; правки справки автоматически меняют онбординг
- [x] Подписка: триал 3 дня, `/subscribe` за 100 звёзд (30 дней), `/balance`, блокировка генерации без активной подписки
- [x] Справка бота (`/help`) описывает `/balance` и `/subscribe`
- [x] Проект Gitea «Social Post Writer Bot Roadmap» с задачами (выполненные — в «Готово», будущие — в Backlog)
+9 -10
View File
@@ -18,15 +18,14 @@ Telegram-бот для генерации постов для соцсетей.
## Как работает бот (UX)
### Онбординг (`/start`)
Первое сообщение пользователя запускает онбординг — **три сообщения с паузой 0.8 сек** между ними:
Первое сообщение пользователя запускает онбординг — **два сообщения с паузой 0.8 сек** между ними:
1. **Что делаю** — «Привет! Я помогаю писать посты для соцсетей. Расскажи, о чём нужен пост — а я верну три варианта: короткий, развёрнутый и с призывом к действию.»
2. **Примеры запросов** — три сценария: написать пост, официальный тон, реплай-правка.
3. **Стиль и идеи** — «пришли скриншоты своих постов, чтобы я запомнил стиль. Хочешь идею — напиши «предложи тему». Подробно — /help»
1. **Welcome (Rich Message)** — `WELCOME_RICH_HTML`: приветствие, примеры запросов, упоминание скриншотов для Tone of Voice.
2. **Вызов `help_command`** — пауза 0.8 сек, затем тот же Rich Message справки, что и по команде `/help`.
Одновременно `/start` создаёт запись триала на 3 дня.
**Логика:** golden path (основной путь) показываем сразу, всё остальное (тон, голоса, правки, `/clear`, `/idea`) — в `/help`. Пользователь не тонет в тексте.
**Логика:** welcome содержит только golden path, всё остальное (тон, голоса, правки, `/clear`, `/idea`) — в `/help`. Онбординг не дублирует текст справки, а **вызывает `help_command`**, поэтому правки `HELP_RICH_HTML` автоматически меняют и онбординг.
### Основные сценарии
@@ -59,8 +58,8 @@ Telegram-бот для генерации постов для соцсетей.
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)
help_message.py — Rich Message: `WELCOME_RICH_HTML` (онбординг) + `HELP_RICH_HTML` (справка), plain fallback
VERSION — версия релиза (сейчас 0.1.0+22)
```
### Основные потоки
@@ -95,7 +94,7 @@ update_tone_from_image → _handle_image_message
| Функция | Что делает |
|---|---|
| `start(message)` | Онбординг (3 сообщения) + создание триала |
| `start(message)` | Онбординг (welcome Rich Message + вызов `help_command`) + создание триала |
| `handle_command_text` | Роутинг команд: `/help`, `/start`, `/clear`, `/idea`, `/balance`, `/subscribe` |
| `_classify_and_respond` | Ядро: проверка подписки → классификация → генерация |
| `_check_subscription` | Проверяет активную подписку; если нет — показывает статус и блокирует |
@@ -166,7 +165,7 @@ SUBSCRIPTION_PRICE_STARS = 100
| Команда | Что делает | Доступна без подписки |
|---|---|---|
| `/start` | Онбординг (3 сообщения) + создание триала | ✅ |
| `/start` | Онбординг (welcome Rich Message + справка) + создание триала | ✅ |
| `/help` | Подробная справка (Rich Message + plain fallback) | ✅ |
| `/balance` | Статус подписки / триала | ✅ |
| `/subscribe` | Активация подписки (100 звёзд, 30 дней) | ✅ |
@@ -194,4 +193,4 @@ SUBSCRIPTION_PRICE_STARS = 100
python -m unittest discover -s tests -v
```
Покрытие: STT, LLM, vision, ToV, reply-поля, изоляция пользователей, API-логи, Rich help, **подписки** (6 тестов: таблица, триал после `/start`, оплата, баланс, блокировка, обнуление через `/clear`).
Покрытие: STT, LLM, vision, ToV, reply-поля, изоляция пользователей, API-логи, Rich help, **онбординг** (welcome → вызов `help_command`), **подписки** (6 тестов: таблица, триал после `/start`, оплата, баланс, блокровка, обнуление через `/clear`).
+1 -1
View File
@@ -1 +1 @@
0.1.0+21
0.1.0+22
+21 -31
View File
@@ -18,7 +18,7 @@ from aiogram.types import Message
import user_store
from apinex_client import APINEX_STT_URL, POST_HISTORY_CHAR_LIMIT, POST_HISTORY_LIMIT, classify_intent, create_tone_profile, extract_posts_from_images, extract_transcript, generate_ideas, generate_posts, transcribe_audio
from help_message import HELP_RICH_HTML, rich_to_plain
from help_message import HELP_RICH_HTML, WELCOME_RICH_HTML, rich_to_plain
from user_store import (
add_post_samples,
clear_user_data,
@@ -114,9 +114,9 @@ async def notify_development_deploy(bot: Bot) -> None:
logger.info("Development deploy notice sent: version=%s", VERSION)
async def send_rich_help(message: Message) -> bool:
async def send_rich_html(message: Message, rich_html: str, stage: str = "помощи") -> bool:
bot = message.bot
payload = {"chat_id": message.chat.id, "rich_message": {"html": HELP_RICH_HTML}}
payload = {"chat_id": message.chat.id, "rich_message": {"html": rich_html}}
if message.message_thread_id is not None:
payload["message_thread_id"] = message.message_thread_id
url = bot.session.api.api_url(token=bot.token, method="sendRichMessage")
@@ -130,13 +130,26 @@ async def send_rich_help(message: Message) -> bool:
data = await response.json(content_type=None)
if response.status == 200 and data.get("ok"):
return True
logger.warning("Rich help rejected with HTTP %s", response.status)
logger.warning("Rich %s rejected with HTTP %s", stage, response.status)
return False
except Exception as exc:
logger.warning("Rich help delivery error (%s): %s", type(exc).__name__, str(exc)[:160])
logger.warning("Rich %s delivery error (%s): %s", stage, type(exc).__name__, str(exc)[:160])
return False
async def send_rich_message(message: Message, rich_html: str, stage: str) -> None:
try:
sent = await send_rich_html(message, rich_html, stage)
except Exception as exc:
sent = False
logger.warning("Rich %s send failed (%s): %s", stage, type(exc).__name__, str(exc)[:180])
if DEPLOY_ENV == "development":
await report_error(message, f"отправка Rich Message: {stage}", exc)
if not sent:
if DEPLOY_ENV == "development":
await report_error(message, f"отправка Rich Message: {stage}", RuntimeError("sendRichMessage returned false"))
await message.answer(rich_to_plain(rich_html))
@router.message(CommandStart())
async def start(message: Message) -> None:
@@ -145,22 +158,9 @@ async def start(message: Message) -> None:
return
await notify_development_deploy(message.bot)
await asyncio.to_thread(ensure_subscription, message.from_user.id)
await message.answer(
"Привет! Я помогаю писать посты для соцсетей. Расскажи, о чём нужен пост — "
"а я верну три варианта: короткий, развёрнутый и с призывом к действию."
)
await send_rich_message(message, WELCOME_RICH_HTML, "приветствия")
await asyncio.sleep(0.8)
await message.answer(
"Примеры запросов:\n"
"• «Напиши пост про запуск нового продукта»\n"
"• «Сделай пост-напоминание о вебинаре в официальном тоне»\n"
"• «Обнови этот вариант, добавь эмодзи» (реплаем на мой текст)"
)
await asyncio.sleep(0.8)
await message.answer(
"Также я умею запоминать твой стиль: пришли скриншоты своих постов. "
"Хочешь идею — напиши «предложи тему». Подробно — /help"
)
await help_command(message)
async def _show_subscription_info(message: Message, sub: sqlite3.Row | None) -> None:
@@ -234,17 +234,7 @@ async def clear_command(message: Message) -> None:
async def help_command(message: Message) -> None:
try:
rich_sent = await send_rich_help(message)
except Exception as exc:
rich_sent = False
logger.warning("Rich help send failed (%s): %s", type(exc).__name__, str(exc)[:180])
if DEPLOY_ENV == "development":
await report_error(message, "отправка Rich Message помощи", exc)
if not rich_sent:
if DEPLOY_ENV == "development":
await report_error(message, "отправка Rich Message помощи", RuntimeError("sendRichMessage returned false"))
await message.answer(rich_to_plain(HELP_RICH_HTML))
await send_rich_message(message, HELP_RICH_HTML, "помощи")
+11
View File
@@ -1,6 +1,17 @@
from __future__ import annotations
import html
WELCOME_RICH_HTML = """<h1>Привет! Я — бот для написания постов</h1>
<p>Помогаю писать и редактировать посты для соцсетей. Расскажи, о чём нужен пост, а я верну три варианта: короткий, развёрнутый и с призывом к действию.</p>
<h2>Примеры запросов</h2>
<ul>
<li>«Напиши пост про запуск нового продукта»</li>
<li>«Сделай пост-напоминание о вебинаре в официальном тоне»</li>
<li>«Обнови этот вариант, добавь эмодзи» — реплаем на мой текст</li>
</ul>
<p>Также я умею запоминать твой стиль: пришли скриншоты своих постов. Хочешь идею — напиши «предложи тему».</p>
<p>Ниже — подробная инструкция по работе с ботом.</p>
"""
HELP_RICH_HTML = """<h1>Помощь по боту</h1>
<p>Я помогаю писать и редактировать посты для социальных сетей. Генерирую посты только по явной просьбе.</p>
<hr/>
+34
View File
@@ -62,5 +62,39 @@ class HelpRoutingTests(unittest.TestCase):
self.assertIn(term, text)
class OnboardingTests(unittest.IsolatedAsyncioTestCase):
async def test_start_sends_welcome_rich_then_help_command(self):
message = AsyncMock()
message.from_user.id = 42
message.message_thread_id = None
calls: list[str] = []
async def fake_rich(_message, html, _stage):
calls.append(html)
with (
patch.object(bot, "notify_development_deploy", new=AsyncMock()),
patch.object(bot, "ensure_subscription"),
patch.object(bot.asyncio, "to_thread", new=AsyncMock()),
patch.object(bot, "send_rich_message", new=fake_rich),
):
await bot.start(message)
self.assertEqual(calls, [bot.WELCOME_RICH_HTML, bot.HELP_RICH_HTML])
async def test_start_help_step_reuses_help_command(self):
from pathlib import Path
source = Path(bot.__file__).read_text(encoding="utf-8")
start_body = source.split("async def start(message: Message) -> None:")[1].split("\nasync def ")[0]
self.assertIn("await help_command(message)", start_body)
def test_welcome_message_has_examples_and_help_handoff(self):
from help_message import WELCOME_RICH_HTML, rich_to_plain
text = rich_to_plain(WELCOME_RICH_HTML)
for term in ("Привет", "Напиши пост", "скриншот"):
self.assertIn(term, text)
if __name__ == "__main__":
unittest.main()