Commit Graph
89 Commits
Author SHA1 Message Date
admin 61c66def3f feat(training): summarize learned profile and add info button 2026-10-29 20:05:42 +02:00
admin 26b533716a fix(subscription): grant one day for one-star plan 2026-10-29 20:00:26 +02:00
admin 60dc55a9b3 fix(subscription): allow zero-day dev confirmation 2026-10-29 13:31:24 +02:00
admin dc2ec6344b fix(subscription): process zero-day one-star payment 2026-10-29 13:03:55 +02:00
admin 0dc54a26b9 fix: use one-star subscription probe 2026-10-21 14:27:23 +02:00
admin a5389e9927 fix(subscription): restore subscription:plan callback decorator
Коммит 3819230 случайно удалил декоратор @router.callback_query(F.data.startswith("subscription:plan:"))
у subscription_plan — хендлер перестал регистрироваться, и все кнопки выбора
тарифа (0/1/10/30 дней) отвечали молча: callback ничем не обрабатывался,
а catch-all @router.message() его не видит.
2026-10-21 13:54:50 +02:00
admin 3819230b3e test: add zero-star invoice probe 2026-10-21 12:56:50 +02:00
admin 13230e3fe2 fix: simplify subscription payment messages 2026-10-20 21:29:44 +02:00
admin c32702c402 fix: simplify subscription payment messages 2026-10-20 21:13:51 +02:00
admin 1ff4012e2f feat: show analytics across periods in tables 2026-10-20 18:35:39 +02:00
admin e4b1b1e510 feat: render full analytics as tables 2026-10-20 17:05:52 +02:00
admin 112c3247e0 feat: add per-user analytics and stats report 2026-10-20 16:42:30 +02:00
admin aa59e30776 docs: update onboarding documentation 2026-10-19 18:47:33 +02:00
admin c1f49ededa feat: update welcome onboarding message 2026-10-19 17:53:39 +02:00
admin 9c37a05139 fix: simplify user-facing bot messages 2026-10-19 15:15:53 +02:00
admin 073c7d2c81 fix: use reply keyboard for subscription purchase 2026-10-12 21:14:58 +02:00
hermes aba1eb4ced fix: allow help navigation outside help mode 2026-10-12 20:40:59 +02:00
hermes a11b5aac64 feat: перевести навигацию справки на inline-клавиатуру 2026-10-11 22:55:17 +02:00
hermes da0da91479 feat: уплотнить кнопки справки 2026-10-11 22:11:57 +02:00
hermes a73d8f403d docs: синхронизация версии 0.1.0+59 в README и IDEA.MD 2026-10-11 21:11:29 +02:00
hermes 21f4adf0b1 feat: кнопка «Ответить» на сообщении оператора
Оператор мог ответить, когда пользователь уже вышел из режима поддержки.
Сообщение приходило, но ответить было нечем: снова идти в «Помощь →
Поддержка», а про прежний контекст уже ничего не напоминало.

- К сообщению оператора добавляется инлайн-кнопка «↩️ Ответить», только
  если обращение закрыто (при активном пользователь и так пишет оператору).
- Кнопка ставит is_active=1, убирает себя с сообщения и показывает
  клавиатуру поддержки (только «Главное меню»).
- Callback проверяет владельца: support:reply:<user_id> сработает только
  для того пользователя, кому пришло сообщение.
- Роутер support.router не затронут фильтром основного роутера
  (F.message.chat.type == "private") — он на него не распространяется.

Тесты: 251 OK (7 новых). Проверено, что aiogram вызывает bot(SendMessage(...)),
а не bot.send_message — asserts адаптированы, иначе тест проходил вхолостую.

VERSION: 0.1.0+58 -> 0.1.0+59
2026-10-11 20:40:06 +02:00
hermes 175d618d62 feat: подпись имени пользователя на сообщении в топике
В топике было непонятно, от кого сообщение: текст приходил без автора.

- Текст уходит в топик как «<Имя>:\n<текст>»; имени нет — «<id>:\n<текст>»
  (user_signature()).
- Caption медиа уходит текстом с той же подписью, само медиа копируется.
- Всё остальное (фото, документы, голосовые без подписи) копируется как есть,
  без подписи — как и просили «если не текст, то пофиг».

Симметрично подписи оператора «Оператор поддержки:» на ответах.

Тесты: 244 OK (8 новых — подпись имени, fallback на id, медиа с caption,
медиа без текста копируется без подписи).

VERSION: 0.1.0+57 -> 0.1.0+58
2026-10-11 20:22:34 +02:00
hermes 6938c1d0f3 feat: подпись «Оператор поддержки» на ответе оператора
Ответ из топика приходил пользователю без подписи, и было непонятно, кто
пишет — бот или человек.

- Текст уходит одним сообщением: «Оператор поддержки:\n<текст>».
- У медиа copy_message не позволяет подменить caption, поэтому подпись
  отправляется отдельным сообщением перед копией; caption медиа, если был,
  попадает в то же сообщение.
- Подпись задаётся SUPPORT_OPERATOR_SIGNATURE (default «Оператор поддержки»).
- Заметки с префиксом // по-прежнему не уходят пользователю.

Тесты: 236 OK (6 новых — текст, медиа с caption, медиа без caption,
переопределение подписи через env).

VERSION: 0.1.0+56 -> 0.1.0+57
2026-10-11 19:59:34 +02:00
hermes 3dd2924d5e fix: вход в любой режим закрывает обращение поддержки
Деактивация была только в /start и «Главное меню». После перехода в
«Обучение» флаг is_active оставался, и текст уезжал оператору, а не в
обучение — сообщение уходило не туда.

Правило теперь: ушёл из контекста поддержки — обращение неактивно.
_close_support_case() вызывается из enter_mode() (любой режим),
show_main_menu() и start(). «🆘 Поддержка» — не режим, флаг не снимает.
Топик и история сохраняются, следующее обращение продолжает переписку.

Три точки вызова сведены в один helper вместо трёх копий try/except.

Тесты: 230 OK (3 новых). Регрессия проверена на старом коде: после входа
в «Обучение» is_active=True и relay=1 (текст ушёл оператору); с фиксом
is_active=False, relay=0 (текст обработал бот).

VERSION: 0.1.0+55 -> 0.1.0+56
2026-10-11 19:48:57 +02:00
hermes 12ef2c6b44 fix: /start закрывает обращение в поддержку
/start возвращает в лобби, но не снимал is_active в support_topics: FSM
сбрасывался, а обращение в БД оставалось открытым. Пользователь оказывался
в лобби, а следующее сообщение всё равно уходило оператору. Дополнительно
enter_mode() блокировал вход в режим сообщением «Идёт обращение в поддержку»
— после /start это выглядело как необъяснимый отказ.

- start() вызывает support.deactivate() перед сбросом FSM.
- Блокировка из enter_mode() удалена: выход в любой режим из открытого
  обращения разрешён — пользователя нельзя запирать. Проверка дублировала
  middleware иConflictовала с /start.

Тесты: 227 OK (3 новых). Регрессия проверена на старом коде: /start оставляет
is_active=True (баг), с фиксом — False. Топик при этом сохраняется, поэтому
следующее обращение продолжает ту же переписку.

VERSION: 0.1.0+54 -> 0.1.0+55
2026-10-11 18:34:14 +02:00
hermes 562442b979 fix: развести приватные и групповые чаты по роутерам
Сообщение оператора в топике поддержки не доходило до пользователя: бот
отвечал прямо в группу «Выбери режим работы кнопкой ниже». Причина — порядок
регистрации роутеров: основной router с catch-all @router.message() (без
фильтра по чату) включался ПЕРВЫМ, а support.router — вторым. У aiogram
побеждает первый подходящий, поэтому до поддержки очередь не доходила.
Комментарий в main() утверждал обратное.

- support.router регистрируется до основного router.
- Основной роутер отфильтрован на F.chat.type == "private" (роль «работник»);
  групповые сценарии обслуживаются своими роутерами. Это первый шаг к ролям:
  работник / помощник поддержки / аналитик / почтальон.
- Роутер поддержки дополнительно отсекает сообщения от ботов.

Вторая поломка того же корня: при активном обращении «📖 Помощь» и
«🎓 Обучение» уезжали оператору как обычный текст, режим не переключался и
бот в личке молчал — пользователь был заперт в обращении. Middleware теперь
пропускает все команды и все кнопки reply-клавиатуры через _is_bot_control(),
а не только «Поддержка» и «Главное меню».

Тесты: 224 OK (10 новых). Регрессия проверена: на старой логике «Помощь»
уходит оператору (relay=1), на новой обрабатывается ботом (relay=0).
Заглушки заменены на настоящие Message — middleware проверяет
isinstance(event, Message), поэтому SimpleNamespace проходил вхолостую.
is_support_active вызывается через to_thread — AsyncMock-заглушка возвращала
truthy-корутину и делала проверку фиктивной.

VERSION: 0.1.0+53 -> 0.1.0+54
2026-10-11 18:16:11 +02:00
hermes 4076765618 fix: показывать причину сбоя поддержки в development
Создание топика падало с «not enough rights to create a topic» (бот был
обычным участником группы, а не администратором), но пользователь видел
только «Поддержка временно недоступна. Попробуй позже» — по такому сообщению
невозможно понять, что чинить.

- ensure_topic возвращает (thread_id, причина) вместо одного thread_id.
- Текст Telegram логируется на ERROR: по типу исключения причина нечитаема.
- В DEPLOY_ENV=development причина добавляется в сообщение пользователю;
  в production остаётся общая заглушка. HTML экранируется.

Проверено: 214 тестов OK (7 новых на причину и её выдачу).
Живая проверка на dev: после выдачи боту прав администратора с «Управление
темами» create_forum_topic создал топик (thread_id 18, пробный удалён).

VERSION: 0.1.0+52 -> 0.1.0+53
2026-10-11 17:04:33 +02:00
hermes 21db563f28 feat: поддержка в боте — топик, сводка профиля, приветствие оператора
Обращение открывается кнопкой «🆘 Поддержка» или /support в режиме «Помощь».
Отдельного FSM-режима нет: пока обращение активно, внизу остаётся только
«Главное меню», а переключение режимов отклоняется.

- support.py: личка пользователя ↔ персональный топик группы SUPPORT_GROUP_ID.
  Топик создаётся один раз на пользователя и переиспользуется, поэтому
  повторные обращения продолжают ту же переписку. Пересылаются все типы
  контента через copy_message; ответ оператора из топика уходит в личку;
  сообщение с префиксом // остаётся внутренней заметкой. Функция выключена
  без SUPPORT_GROUP_ID.
- Карточка профиля уходит первым сообщением топика: аватарка, имя, username,
  id и сводка — сколько дней пользуется ботом, сколько продлений подписки,
  сколько дней осталось, обучен ли бот (user_store.support_profile_summary).
- Бот не объясняет пользователю, что «передаёт оператору»: в ответ приходит
  случайная фраза из пула SUPPORT_GREETINGS (activity_phrases.support_greeting).
  Формулировки безличные, без «я/бот/оператор»; пул переопределяется через
  SUPPORT_GREETINGS без деплоя.

Проверено: 207 тестов OK (30 новых в tests/test_support.py).
VERSION: 0.1.0+51 -> 0.1.0+52
2026-10-11 16:48:04 +02:00
admin 7a630a974f fix: push the final text into the draft before sending it as a message
Драфт с последней фразой висел в чате ещё несколько секунд после того, как
уже ушло сообщение «✅ Обучение завершено»: содержимое драфта не совпадало
с финальным сообщением, и клиент не мог их слить.

Финальный текст теперь сначала пушится в драфт тем же draft_id, и только
потом уходит реальным сообщением с тем же текстом — драфт заменяется без
задержки. finish() без текста гасит драфт пустым пушем.

Проверено: 198 тестов OK (2 новых на совпадение текста драфта и сообщения).

VERSION: 0.1.0+50 -> 0.1.0+51
2026-10-11 15:34:15 +02:00
admin 635b78cb86 fix: always close the operation status so the draft stops hanging
Драфт исчезает только когда бот отправляет реальное сообщение. Ряд путей
завершения операции не закрывал статус, из-за чего последняя показанная
фраза висела в чате до конца TTL драфта и выглядела неактуальной.

- OperationStatus: флаг _closed, finish() принимает None (только снять
  статус), finish_if_open() — идемпотентное закрытие, _finished_draft_id
  сохраняется до обнуления draft_id
- finish_status(status, text) — хелпер, терпит None: закрывает статус на
  любом пути выхода без повторения `if status:`
- закрыты пути без finish: _rebuild_account_profile при переданном статусе,
  _process_voice -> _classify_and_respond, успешные выходы _handle_image_message
  и transcribe_voice через else-ветку, ранние возвраты /help и проверки подписки

Проверено: 176 тестов OK (4 новых на закрытие статуса).

VERSION: 0.1.0+49 -> 0.1.0+50
2026-10-11 15:18:59 +02:00
admin 87e421ae73 feat: stream operation status as a live draft with activity phrases
Статус долгой операции больше не оставляет в чате отдельное сообщение:
в приватном чате он стримится живым драфтом (sendMessageDraft), пока
идет классификация, генерация, чтение скриншотов или распознавание речи.

- activity_phrases.py: пулы фраз по типу операции (generation, training,
  images, voice, ideas), случайный интервал 3-8 сек (ACTIVITY_MIN_SECONDS /
  ACTIVITY_MAX_SECONDS), фраза не повторяет предыдущую
- OperationStatus: драфт со стабильным draft_id, фоновая ротация фраз,
  finish() отправляет финальный статус реальным сообщением, retarget()
  меняет пул на лету (generate_ideas), предохранитель ACTIVITY_MAX_ROTATIONS
- в группах и топиках драфты недоступны: прежний answer + edit_text,
  ротация фраз отключена; при ошибке драфта автоматический откат
- Dockerfile копирует activity_phrases.py и support.py, иначе контейнер
  падает на импорте

Проверено: 172 теста OK,其中包括 14 новых в tests/test_activity_phrases.py.
Тесты нашли и закрыли баг: ротация запускалась и в группах, где каждый тик
был бы реальным редактированием сообщения.

VERSION: 0.1.0+48 -> 0.1.0+49
2026-10-11 14:51:07 +02:00
admin a95ae7ae5b docs: sync README/IDEA.MD with 0.1.0+48 2026-10-11 14:07:17 +02:00
admin ff09b74f03 feat: log raw telegram updates in development 2026-10-11 13:19:39 +02:00
admin 926031713d fix: recover complete posts from truncated json 2026-10-10 16:57:30 +02:00
admin 04b33e708d feat: show main menu with version notice 2026-10-10 14:13:40 +02:00
admin b665bf3394 fix: remove max token limit from llm calls 2026-10-10 14:00:23 +02:00
admin 9f62b35798 feat: persist dev bot logs 2026-10-09 21:17:06 +02:00
admin a43ededa1b feat: gate post generation on training flag 2026-10-09 20:11:41 +02:00
admin e847a4b390 feat: add generate-post button 2026-10-09 19:42:35 +02:00
admin 6f8741cf3c feat: generate posts from images and reject video 2026-10-09 18:18:37 +02:00
admin da70e30960 feat: add canned replies and injection screening 2026-10-09 17:31:50 +02:00
admin ea0f7ee27c fix: keep training completion status concise 2026-10-09 16:52:32 +02:00
admin 3e33a9deb5 feat: show editable operation status 2026-10-09 16:46:11 +02:00
admin 189f6bc1f0 fix: show user id in subscription status message 2026-10-09 15:13:40 +02:00
admin c90af5b4df feat: show user id in subscription message 2026-10-08 23:13:05 +02:00
admin 3bcec7a9a5 feat: add cumulative account profile training 2026-10-08 23:04:32 +02:00
admin 766ba22f17 fix: retry empty model responses with lower reasoning 2026-10-08 22:12:18 +02:00
admin 1dffdcf32d feat: confirm training reset and polish bot messages 2026-10-08 21:21:58 +02:00
admin 92771c9d9f feat: repair malformed model JSON responses 2026-10-08 20:58:47 +02:00
admin 2d743d2220 feat: improve operation diagnostics and API logging 2026-10-08 19:35:31 +02:00