From da0da9147943fb689e9c27539cb02518dfad6777 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sun, 11 Oct 2026 22:11:57 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20=D1=83=D0=BF=D0=BB=D0=BE=D1=82=D0=BD?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D0=BA=D0=BD=D0=BE=D0=BF=D0=BA=D0=B8=20?= =?UTF-8?q?=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- IDEA.MD | 6 +++--- README.md | 10 ++++++++-- VERSION | 2 +- help_message.py | 32 ++++++++++++++++++++------------ tests/test_release_help.py | 25 +++++++++++++++++++------ 5 files changed, 51 insertions(+), 24 deletions(-) diff --git a/IDEA.MD b/IDEA.MD index f28bc36..5fc5a81 100644 --- a/IDEA.MD +++ b/IDEA.MD @@ -24,7 +24,7 @@ Telegram-бот, который по запросу пользователя г - **Подписка** — при входе показывает активный период и остаток дней; inline-кнопка продления сразу открывает Stars-инвойс (`/balance`, `/subscribe`). - **Помощь** — справка и навигация по её разделам (`/help`). -`/start` показывает ровно одно welcome-сообщение без автоматического вызова помощи. При входе в любой режим показывается короткий onboarding. Главное меню содержит только четыре пользовательских режима; внутри каждого режима клавиатура состоит из кнопки «Главное меню». Команды `/training`, `/generate`, `/subscription`, `/help` доступны из любого режима; профильные действия — только в своём режиме. +`/start` показывает ровно одно welcome-сообщение без автоматического вызова помощи. При входе в любой режим показывается короткий onboarding. Главное меню содержит только четыре пользовательских режима; внутри каждого режима клавиатура состоит из кнопки «Главное меню». В справке `/help` кнопки разделов расположены последовательно, переносятся целиком по мере заполнения строки; отдельной кнопки «В меню» нет, из раздела доступны остальные разделы. Команды `/training`, `/generate`, `/subscription`, `/help` доступны из любого режима; профильные действия — только в своём режиме. ## Первая версия - Принимать запрос обычным сообщением. @@ -104,9 +104,9 @@ 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+59` +- Версия разработки: `0.1.0+60` - [x] Статус долгой операции стримится живым драфтом `sendMessageDraft` в приватном чате и каждые 3–8 сек меняет случайную фразу из пула операции (`activity_phrases.py`); в группах и топиках работает прежний `edit_text` -- [x] Справка бота (`/help`) описывает `/balance` и `/subscribe` +- [x] Справка `/help`: кнопки разделов идут последовательно и переносятся на новую строку целиком; отдельной кнопки «В меню» нет, из раздела доступны остальные разделы - [x] Проект Gitea «Social Post Writer Bot Roadmap» с задачами (выполненные — в «Готово», будущие — в Backlog) - [x] Ротационный файл лога `/app/data/bot.log` (5 MiB × 3 архива) на persistent volume - [x] Кнопка «✍️ Придумать пост» и `/post`: самостоятельный пост по обученному профилю, гейт по `users.is_trained` diff --git a/README.md b/README.md index e14e127..37696ba 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Telegram-бот для генерации постов для соцсетей. - [Деплой и конфигурация](#деплой-и-конфигурация) - [Тесты](#тесты) -Версия разработки: `0.1.0+59`. +Версия разработки: `0.1.0+60`. --- @@ -60,6 +60,12 @@ onboarding. В «Подписке» сразу отображаются тип 1. Отправляет ровно одно `WELCOME_RICH_HTML` с примерами и описанием режимов. 2. Показывает клавиатуру главного меню; справка открывается отдельно кнопкой или командой `/help`. +### Справка (`/help`) + +Меню разделов и страницы справки используют последовательную раскладку inline-кнопок: кнопки +идут друг за другом, а на следующую строку переносятся целиком, когда не помещаются. Из раздела +можно перейти в любой другой; отдельной кнопки «В меню» нет, текущий раздел отключён. + ### Поддержка (`support.py`) Отдельного FSM-режима нет: обращение — расширение режима «Помощь». Пользователь нажимает @@ -347,7 +353,7 @@ SUBSCRIPTION_PRICE_STARS = 100 - **Coolify:** проект `Social Post Writer Bot`, окружение `development`, приложение `social-post-writer-bot-development` (`d32u5zcq2g4odmdpq9hw91yn`) на `spb-easy-dale`. - **Деплой:** `bash /root/.hermes/scripts/gitea_deploy.sh dev` (POST `/api/v1/deploy?uuid=...&force=false`). - **Окружение:** `DEPLOY_ENV` включает dev-функции только при точном значении `development`; production, пустое и неизвестное значение отключают их. `BOT_ENV` отдельно выбирает Telegram Bot API и не используется для включения dev-поведения. -- **VERSION:** файл `VERSION` (`0.1.0+59`). При `DEPLOY_ENV=development` и изменении версии бот шлёт уведомление о версии в `DEV_CHAT_ID`. +- **VERSION:** файл `VERSION` (`0.1.0+60`). При `DEPLOY_ENV=development` и изменении версии бот шлёт уведомление о версии в `DEV_CHAT_ID`. - **Переменные Coolify:** `BOT_TOKEN`, `APINEX_API_KEY`, `APINEX_STT_MODEL`, `APINEX_INTENT_CLASSIFIER_MODEL`, `APINEX_INJECTION_CLASSIFIER_MODEL`, `APINEX_POST_WRITER_MODEL`, `APINEX_IMAGE_TONE_MODEL`, `APINEX_RESPONSE_REPAIR_MODEL` (optional; default uses the standard model), `BOT_DB_PATH`, `RAW_UPDATES_DB_PATH` (optional; default `/app/data/bot_updates.sqlite3`), `DEPLOY_ENV`, `DEV_CHAT_ID`, `BOT_LOG_PATH` (optional; default `/app/data/bot.log`), `ACTIVITY_MIN_SECONDS` / `ACTIVITY_MAX_SECONDS` (optional; default 3 / 8 — границы паузы между сменами фраз в драфте), `ACTIVITY_MAX_ROTATIONS` (optional; default 12 — предохранитель от бесконечной ротации), `SUPPORT_GROUP_ID` (id супергруппы с темами для обращений; без неё поддержка выключена), `SUPPORT_NOTE_PREFIX` (optional; default `//` — префикс заметки, которая не уходит пользователю), diff --git a/VERSION b/VERSION index a2bea9e..2d5d1a2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.1.0+59 \ No newline at end of file +0.1.0+60 \ No newline at end of file diff --git a/help_message.py b/help_message.py index 1c19db1..c36aeeb 100644 --- a/help_message.py +++ b/help_message.py @@ -64,11 +64,11 @@ HELP_SECTIONS: dict[str, tuple[str, str]] = { } HELP_SECTION_ORDER: tuple[str, ...] = tuple(HELP_SECTIONS) -HELP_MENU_BUTTON = "⌂ В меню" +HELP_BUTTON_ROW_WIDTH = 24 def _menu_buttons(current: str | None = None) -> str: - rows = [] + buttons: list[tuple[str, str]] = [] for section in HELP_SECTION_ORDER: title = HELP_SECTIONS[section][0] if section == current: @@ -78,18 +78,26 @@ def _menu_buttons(current: str | None = None) -> str: f'' f"{title}" ) - rows.append(f'{button}') + buttons.append((title, button)) + + rows = [] + row: list[str] = [] + row_width = 0 + for title, button in buttons: + button_width = len(title) + separator_width = 1 if row else 0 + if row and row_width + separator_width + button_width > HELP_BUTTON_ROW_WIDTH: + rows.append(f'{"".join(row)}') + row = [] + row_width = 0 + separator_width = 0 + row.append(button) + row_width += separator_width + button_width + if row: + rows.append(f'{"".join(row)}') return "".join(rows) -def _menu_button_row() -> str: - return ( - f'' - f'' - f"{HELP_MENU_BUTTON}" - ) - - def help_menu_html(current: str | None = None) -> str: return ( f"

{HELP_MENU_TITLE}

" @@ -102,7 +110,7 @@ def help_menu_html(current: str | None = None) -> str: def help_section_html(section: str) -> str: title, body = HELP_SECTIONS[section] - return f"

{title}

{body}{_menu_button_row()}{_menu_buttons(section)}" + return f"

{title}

{body}{_menu_buttons(section)}" def help_view(section: str | None = None) -> str: diff --git a/tests/test_release_help.py b/tests/test_release_help.py index 49a736a..ba5fbe8 100644 --- a/tests/test_release_help.py +++ b/tests/test_release_help.py @@ -147,25 +147,38 @@ class HelpRoutingTests(unittest.TestCase): class HelpNavigationTests(unittest.TestCase): - def test_menu_lists_every_section_as_full_width_button(self): + def test_menu_packs_buttons_without_truncating_labels(self): from help_message import HELP_SECTIONS, HELP_SECTION_ORDER, help_menu_html menu = help_menu_html() rows = menu.split("'), row) - self.assertEqual(row.count("{title}" in row + ] + self.assertLessEqual( + sum(len(title) for title in labels) + max(0, len(labels) - 1), + 24, + row, + ) + for section, (title, _body) in HELP_SECTIONS.items(): self.assertIn(f'data="help:{section}"', menu) + self.assertIn(f">{title}", menu) self.assertNotIn('data="help:menu"', menu) + self.assertNotIn("В меню", menu) - def test_section_navigates_to_every_other_section(self): + def test_section_navigates_to_other_sections_without_menu_button(self): from help_message import HELP_SECTIONS, help_section_html for section in HELP_SECTIONS: html = help_section_html(section) - self.assertIn('data="help:menu"', html) + self.assertNotIn('data="help:menu"', html) + self.assertNotIn("В меню", html) for other in HELP_SECTIONS: if other == section: continue