# ТЕХНИЧЕСКОЕ ЗАДАНИЕ ## **Система мониторинга, AI-классификации и управления IT-лидами (LeadRadar) | Спецификация V1.2 (Production)** **Архитектура:** Self-Hosted / High-Perf **База данных:** DuckDB (Embedded OLAP) **AI Engine:** DeepSeek v4 Flash **Интерфейс:** SPA / Custom Dashboard ## **1\. Введение и назначение системы** LeadRadar — это автономная программная платформа для автоматического перехвата, аналитической классификации, дедупликации и трекинга заявок/лидов **в любой сфере** (IT и не-IT: вакансии и найм, разовые заказы и услуги, товары, недвижимость и т.д.). Система разворачивается на выделенном сервере и полностью управляется через отзывчивый веб\-интерфейс, исключая необходимость взаимодействия через командную строку. ## **2\. Анализ применимости базы данных** Выбранная СУБД (DuckDB) идеально подходит для поставленной задачи, сочетая преимущества встраиваемой архитектуры (отсутствие внешних зависимостей, работа в одном файле) и колоночной аналитики: > * **Скорость аналитики:** мгновенная фильтрация по десяткам тысяч записей, стеку технологий, временным диапазонам и доскам с векторизованным выполнением запросов. > * **Нативная поддержка структурированных данных:** списки технологий и параметров хранятся без деградации скорости доступа. > * **Полнотекстовый поиск (FTS):** встроенное расширение позволяет мгновенно искать по ключевым словам и фразам во всей истории сообщений без использования сторонних поисковых движков. > * **Легковесность:** потребляет минимум системных ресурсов и не требует администрирования отдельного сервиса СУБД. ## **3\. Выбор оптимального технологического стека** | Уровень | Технология | Обоснование | | :---- | :---- | :---- | | Backend Runtime | Python 3.12+ / FastAPI | Асинхронное ядро, минимальные накладные расходы, нативная поддержка реалтайм-событий. | | Database Core | DuckDB | Встраиваемая колоночная СУБД, быстрые агрегации, векторные выборки, работа в одном файле. | | Telegram Engine | Telethon (MTProto API) | Поддержка Client API, веб\-авторизация, полный доступ к диалогам и истории. | | AI Classifier | DeepSeek v4 Flash | Высокая точность в IT-терминологии, оптимальная себестоимость анализа, строгая структуризация ответов. | | Frontend Stack | Vue 3 \+ Tailwind CSS | Максимальная кастомизация, высокая скорость рендеринга, реактивность интерфейса. | | Realtime Transport | Server-Sent Events (SSE) | Однонаправленный поток событий, мгновенные пуш-уведомления, автоматическое переподключение. | ## **4\. Архитектура и функциональные требования** ### **4.1. Авторизация в дашборд** > * Вход в веб-интерфейс по логину и паролю; учетные данные по умолчанию: **admin / admin**. > * Смена пароля — через интерфейс настроек. > * Серверная сессия действительна **30 дней** (месяц): повторная авторизация в течение этого срока не требуется. ### **4.2. Авторизация Telegram через Web-интерфейс** > * Ключи Telegram API (**api_id / api_hash**) вводятся один раз в интерфейсе настроек, а не через переменные окружения. > * Ввод номера телефона в модальном окне интерфейса. > * Поддержка ввода кода верификации и облачного пароля (2FA). > * Опциональная генерация QR-кода для быстрой авторизации камерой смартфона. > * Надежное сохранение сессии в файловой системе (без необходимости повторных входов). > * Индикация статуса подключения и возможных ошибок. ### **4.3. Менеджер каналов и диалогов** > * Отдельный экран или боковая панель со списком всех каналов и групп. > * Отображение метаданных: название, аватар, системное имя, тип (чат/канал). > * Быстрый предпросмотр последних сообщений в один клик без перехода в приложение мессенджера. > * Индивидуальный переключатель для каждого источника: режим активного мониторинга или игнорирования. > * **Кнопка «Включить все» / «Выключить все»** в шапке списка каналов — массовое переключение мониторинга для всех источников разом. При первом включении канала (и при массовом включении) выполняется последовательный разбор последних 10 сообщений с паузами (анти-бан), фоново, без блокировки UI. > * **Кнопка «Перечитать»** в шапке списка каналов — ручная догонялка: перечитывает последние ~10 сообщений **всех включённых** каналов в фоне (с паузами анти-бан), даже если канал уже разобран ранее. Повторные карточки не создаются (защита дедупликации по тексту). После этого система реагирует только на новые сообщения в реальном времени; страховочный цикл (~30 с) догоняет потерянные события (рестарт/разрыв соединения). > * **Автосинхронизация списка:** при входе на вкладку и фоновым циклом актуальный список чатов/каналов сверяется с аккаунтом Telegram — новые появляются, переименованные обновляются, покинутые/удалённые исчезают (мониторинг по ним прекращается). > * **Новые чаты:** при включённой настройке «новые чаты — сразу в мониторинг» (`autoMonitorNew`) любой появившийся чат включается в мониторинг автоматически; при выключенной — появляется отключённым, пользователь включает вручную. > * **Прочитанность:** полученные/перечитанные сообщения сразу помечаются прочитанными в Telegram (read-ack в realtime, при «Перечитать» и ручном предпросмотре) — в других клиентах они не висят «новыми». > * В пункте меню «Каналы» выводится счётчик числа каналов, находящихся в мониторинге. ### **4.4. Кастомные колонки и стилизация** > * **По умолчанию колонок в системе нет.** Колонки создаются пользователем (кнопка «Новая колонка») либо предлагаются ИИ по результатам анализа «Неразобранного». > * **Создание и настройка — единый диалог «Новая колонка / Настройки колонки»**: название, **описание колонки** (для пользователя и подсказки ИИ/ML), цветовой акцент и набор фильтров. Диалог открывается сразу при создании и в любой момент из меню колонки (⋮). > * **Колонка — это не отдельный навык, а смысловой набор фильтров** (каждый опционален, набор можно менять в любой момент): направление/тема задач, ключевые технологии и стек, ключевые слова, **грейд/уровень** (junior/middle/senior/lead/…, с распознаванием синонимов: джун/мидл/сеньор/mid и т.п.), бюджетный диапазон с валютой (от–до). Пустые группы не участвуют. Режим комбинирования — «все условия» или «любое из условий» («любое» = хотя бы одна **заданная** (непустая) группа совпала; пустые группы результат не искажают). Сами правила **не раскладывают входящие «словарно» до ИИ** (словесный матч не понимает смысл и ловит ложные совпадения из дайджестов и футеров): они служат (1) критериями для ИИ-классификатора, (2) **проверкой-страховкой на бэкенде** и (3) обоснованием «почему карточка в колонке» (см. п. 5.4). Колонка, у которой заданы активные фильтры, **не принимает карточки, не прошедшие её правила, ни от ИИ, ни от ML** (страховка на бэкенде) — такая карточка остаётся в «Неразобранном». > * **ИИ-предложения** (статус `suggested`) появляются в списке колонок с пометкой «ИИ» и **обоснованием**: по какому направлению, стеку, грейду и ключевым словам собрана, сколько похожих карточек в выборке. Пользователь открывает колонку, просматривает карточки и решает: **принять** (колонка становится обычной — можно переименовать, дополнить описание и поправить фильтры) или **отклонить** (карточки возвращаются в «Неразобранное»). > * **Причина попадания в колонку:** при совпадении с фильтром у карточки фиксируется, **какие именно критерии совпали** — группа фильтра (направление/слова/стек/грейд/бюджет) и сами совпавшие термины. Совпадение ищется по всему тексту сообщения, включая списки стека, требований и «будет плюсом» (и наоборот: термин карточки ищется по всем группам фильтра). В подробном виде карточки есть блок **«Попала в колонку по фильтру — совпало»** с перечнем совпавших критериев. При ручном переносе карточки совпадения пересчитываются для новой колонки. > * Для каждой колонки настраиваются: название, описание, цветовой акцент, ширина, сворачивание в виджет, **набор фильтров попадания карточек**, индивидуальный системный промпт, видимость полей (бюджет, стек, контакты, источник). Описание и набор фильтров колонки передаются в промпт ИИ-классификатора при выборе колонки. > * **Любую колонку можно удалить** — находящиеся в ней карточки возвращаются в «Неразобранное» (с пометкой новых). ### **4.5. Буфер «Неклассифицированное» (Inbox)** > * Изолированный раздел для входящих заявок, не подошедших под критерии активных досок. > * Счетчик непрочитанных элементов с визуальным уведомлением. > * Удобный ручной перенос лида на нужную доску в один клик. > * Функция пакетной повторной классификации через нейросеть. ### **4.6. Настройки ключей и интеграций** > * Все API-ключи (**Telegram api_id/api_hash**, **ключи AI-провайдеров**) задаются и заменяются через веб-интерфейс. > * Секреты не хранятся в переменных окружения и конфигурационных файлах (по договорённости в env исключения — ключ шифрования БД и креды MinIO, см. п. 8). > * Сохраненные ключи не отображаются в открытом виде: доступны только факт наличия и замена значения. > * Настройка целевой валюты отображения и источника курсов (вкладка «Валюта и курсы»). > * Источник курсов — **ЦБ РФ (cbr.ru)**: официальные курсы к рублю, запрос выполняется 4 раза в сутки (каждые 6 часов); до подключения сервиса используются мок-курсы. > * AI-классификатор возвращает бюджет как число или диапазон с указанием валюты (USD/EUR/RUB/USDT и др.). > * AI-классификатор работает через **выбираемого провайдера — активен только один**: DeepSeek, OpenAI, OpenRouter, Anthropic Claude или локальный OpenAI-совместимый сервер (Ollama, LM Studio, vLLM и т.п.). Для каждого провайдера настраиваются base URL, модель и API-ключ (локальным ключ не нужен); системный промпт общий. ### **4.7. Дашборд: рабочие колонки и карточки** > * Основной рабочий экран — **дашборд** из колонок (как пользовательских, так и ИИ-предложений, см. п. 4.4); каждая колонка имеет свой цвет-акцент. По умолчанию колонок нет — создаются пользователем или предлагаются ИИ. > * Служебные колонки: **«Неразобранное»** (буфер Inbox, см. п. 4.5) и **«Корзина»** (отложенное удаление карточек). > * Колонки прокручиваются по вертикали независимо; при нехватке ширины область дашборда прокручивается горизонтально. > * Пользователь настраивает рабочее пространство: **количество колонок, их ширину и порядок** (перетаскиванием), отображение на **пол-экрана или весь экран**. > * Любую колонку можно свернуть в **виджет-счетчик** (компактная плашка с числом ожидающих карточек) и развернуть обратно в один клик. > * **Карточки интерактивные**: быстрые действия без открытия — копирование контакта, перенос в другую колонку, добавление комментария, перемещение в корзину; подробное описание карточки открывается по центру экрана в модальной панели. > * На карточке показывается **структурированная суть «О заявке»** — не голый текст исходника (он виден только в подробном виде под спойлером). «О заявке» у всех карточек собирается из **одинаковых логических блоков единой структуры** (Компания → Формат → О задаче → Требования → Будет плюсом → Условия), длина блоков разная; недостающие блоки пропускаются. Как заполнять поля блока задаёт отдельный **«Промпт структуры карточки»** (см. п. 5.6), а единый вид текста гарантирует серверная сборка из структурированных полей. > * **Заголовок карточки** — короткий (4–9 слов), без эмодзи/хэштегов и **без ссылок** (markdown-ссылки и URL вычищаются и при сохранении, и при отображении), визуально обрезается до двух строк. > * **Текст в карточках переносится по словам** (длинные ссылки/токены не выходят за границы карточки): включён перенос строк и сохранение структуры (список параметров от ИИ не схлопывается). > * На карточке источник (название канала) не показывается — он виден только в подробном описании лида. > * Бюджет может быть числом или диапазоном «от–до» в любой валюте. > * Тип заявки (найм/занятость или разовая сделка/заказ) определяется **по контексту ИИ** (ML учится этому же); маркерная эвристика без ИИ не является решающей. Подписи типов настраиваются в «Сфере и ключах» (по умолчанию — **«вакансия»** и **«фриланс»**); бейдж типа выводится, когда тип подтверждён по контексту. > * Пересчёт сумм в целевую валюту (по умолчанию — рубли) выполняется один раз — в момент поступления заявки, по курсу на тот день; исходная сумма в первоначальной валюте всегда сохраняется и отображается первой (в карточке и в подробном описании). > * Служебная колонка **«Архив»**: карточки, находящиеся в системе дольше настраиваемого срока (интервал настройки 1–30 дней), автоматически переносятся в архив. > * **Отсев устаревших на входе:** сообщение, опубликованное раньше срока до архива (например, канал молчал, и «Перечитать» подтянуло старые посты), в систему **не попадает вообще** — ни карточкой, ни в архив, ни в корзину. Работает, когда автоархив включён: возраст сообщения считается от даты публикации в канале до текущего момента. > * **Архив** очищается автоматически через 90 дней после помещения, **корзина** — каждые 7 дней. Возврат карточек из архива и корзины возможен только на канбан (доски / «Неразобранное»), пока карточка не очищена автоматически. Помимо автоправил, **корзину и архив можно очистить вручную полностью** (безвозвратно, с подтверждением). > * Карточки с основного канбана можно «взять в работу» — они попадают в отдельный дашборд «Выбранные» (см. п. 4.8). > * К карточке можно добавить **комментарий** (внутренняя заметка), он сохраняется вместе с лидом. > * Действия пользователя (ручной перенос, корзина, возврат, перенос в «Выбранные») фиксируются как **обучающие примеры и всегда передаются в ML-модель** (обучение идёт постоянно, независимо от того, используется ли ML в пайплайне). Дополнительно ML учится на каждом попадании карточки в колонку по правилам. ИИ-предложения колонок анализируются и учитываются пользователем до превращения в постоянные. ### **4.8. «Выбранные» — второй дашборд (проектный канбан)** > * Отдельный экран для отобранных лидов: свой канбан с перетаскиванием карточек по стадиям. > * Стадии по умолчанию: **Запланировано → Отклик → Согласование → В работе → Проверка → Готово**, плюс **Отложено** (пауза) и финальные статусы **«Выполнено»** и **«Отклонено»**. > * Карточка попадает сюда кнопкой «Взять в работу» из лида на канбане, либо **создается вручную** кнопкой «Новая карточка» с тем же набором полей — такие карточки помечаются как **«локальные»** (созданы вручную, без лида-источника). > * Карточка не может находиться одновременно на дашборде и в «Выбранных»: при взятии в работу лид **уходит с дашборда безвозвратно** (в архиве, корзине и поиске не участвует) — обратно на дашборд он не возвращается. > * У проектной карточки ведется **история движения**: с момента добавления в «Выбранные» фиксируется каждая смена стадии (статус, дата и время); для локальных карточек первая запись — «Создана локально». История показывается в подробном виде под спойлером «История движения». > * У проектной карточки редактируются: **сумма (число или диапазон), валюта, стек, контакты, комментарии**; можно **прикреплять ссылки и ТЗ**. > * К карточке прикрепляются **файлы — медиа и документы**; система автоматически определяет тип файла (по MIME и расширению). На самой карточке значками показывается количество прикрепленных **файлов** и **ссылок**. > * Файлы хранятся в объектном хранилище **MinIO (S3-совместимое, креды в env)**: в БД — метаданные и ключ объекта. Если MinIO не настроен (локальный запуск без docker-compose) — те же ключи сохраняются в локальную папку `data/attachments`, API и карточки не меняются. Тип файла определяется автоматически (MIME + расширение): изображение/видео/аудио/архив/документ. > * Карточки «Выбранных» **не попадают в архив и корзину** — у дашборда собственные финальные сущности «Выполнено» и «Отклонено». Стадию «Отклонено» можно **очистить полностью вручную** (безвозвратно, с подтверждением). > * Стадия «Отложено» поддерживает **напоминания**: при переносе карточки открывается окно настройки — напомнить **через N дней (1–30)** или **в конкретную дату (календарь)** и в какое время; по наступлению срока всплывает оповещение с действиями «Открыть карточку / Позже / Снять». > * Общий переключатель напоминаний вынесен в **настройки уведомлений**: если он выключен, окно настройки при переносе в «Отложено» не показывается и уже установленные напоминания не срабатывают. Список активных напоминаний виден там же. > * Напоминание автоматически снимается, когда карточка покидает стадию «Отложено». ### **4.9. Поиск и подключение каналов (Discovery)** > * Подвкладка **«Поиск»** на экране «Каналы» — поиск и подключение новых источников (каналы, группы, форумы), в которых мы ещё **не состоим**. Система ищет кандидатов, оценивает их (метаданные, язык, содержимое) и показывает человеку список «на рассмотрение». > * **Задача поиска** — конфиг с описанием цели (что ищем): пользователь описывает цель → система генерирует **поисковые ключи ИИ** (редактируются перед стартом; запуск возможен только после их подтверждения) → по ключам выполняется **каскад фильтров** по нарастающей стоимости (при первом «нет» источник пропускается): поиск и дедупликация кандидатов → глобальный фильтр «мы не состоим» → число участников (минимум; 0 = не важно) → язык (`ru`/`any`) → оценка содержимого выборки сообщений. Задач может быть несколько. > * **Глобальное правило «мы не состоим» — безусловное, для всех задач:** источник, в котором мы уже состоим (вступили/мониторим), находящийся в чёрном списке или уже обрабатываемый/вступивший в другой задаче, отбрасывается сразу и на любом этапе (поиск → оценка → вступление) — независимо от запроса, ключей и настроек задачи. Проверка повторяется непосредственно перед вступлением (между оценкой и join'ом источник мог быть добавлен вручную). > * **Метки кандидатов** — человекочитаемые пометки: «закрытая группа/канал», «форум», «не прочитано», «участники не подтверждены», «язык не подтверждён», «мало сообщений», «есть проходные темы». Метки «не подтверждено» — не ошибка и не пропуск, а сигнал человеку на экране рассмотрения. > * **Оценка содержимого:** сообщение проходит те же правила, что в основном пайплайне (этап 1 → ML → ИИ), но с **профилем задачи** (описание + ключи задачи), без создания карточек/очереди/обучения ML; при выключенном ИИ — локальный разбор/ML. Каналы и открытые группы: читается выборка до `sampleSize` последних сообщений (по умолчанию 10), доля подходящих ≥ порога (по умолчанию 40%) → «на рассмотрение»; открытая группа без чтения → «на рассмотрение» с меткой. > * **Оценка по темам (форумы):** группа раскладывается по темам (`reply_to_top_id`), выборка читается по активным темам и оценивается **по темам** («тема: подходит X из N»); группа подходит при ≥1 проходной теме, в превью — список тем с пометками проходная/нет (имена тем подставляются сниппетом первого сообщения, если API не отдаёт их без членства). Для оценки нужно ≥3 содержательных сообщений в выборке; если их меньше — кандидат идёт «на рассмотрение» с меткой «мало сообщений». Закрытые группы (история скрыта) — сразу «на рассмотрение» с меткой «закрытая группа/канал». > * **«На рассмотрение» и действия человека:** у кандидата показываются тип, число участников, метки, соответствие «подходит X из N», **почему подошло** (перечень подходящих сообщений/тем, как блок «попала по фильтру») и превью. Действия: **«Вступить и мониторить»** — вступление, добавление в список каналов с `monitor=1` и догон последних ~10 сообщений; **«Отклонить»** — источник уходит в **чёрный список** (исключается из поиска всех задач; снимается вручную); для закрытых групп вместо авто-вступления — кнопка-ссылка `t.me/`, факт вступления система замечает при синхронизации диалогов и предлагает добавить источник в мониторинг. > * **План задач и правило создания:** у задачи задаётся план вступлений 1–50; **сумма планов всех активных задач (статус не `done/failed`) ≤ суточного лимита вступлений** (по умолчанию 50) — задача с планом 50 не даёт создать другую, план 25 оставляет не более 25. У активной задачи план нельзя увеличить сверх свободного бюджета. > * **Авто-вступление и квоты:** авто-режим включается на задачу (`autoJoin`) — подходящие кандидаты вступают сами, по одному действию, со **случайной паузой 50–70 с**. Суточный лимит — **50 авто-вступлений, общий на все задачи** (считаются только автоматические); **ручные вступления — без квот и ограничений**. Задача «выполнена» при достижении плана; при упоре в общий суточный бюджет авто-режим продолжает на следующий день (новый суточный бюджет). > * **Анти-бан (BanGuard):** единый менеджер квот и пауз для всех действий поиска и для всех задач; поиск/чтение — мягкие паузы (единицы секунд + джиттер). При `FloodWaitError` — пауза по секундам из ответа + запас, авто-вступления останавливаются до следующего дня; есть общий **«стоп-кран»** — ручная пауза всего discovery. Лимит, интервалы и размеры выборки — настройки в UI. ## **5. Спецификация пайплайна обработки данных** > 1. **Перехват события и очередь:** Система фиксирует новые сообщения мониторящихся каналов в реальном времени и кладёт их в **очередь обработки** (на диске); очередь разбирается фоновым воркером. В момент получения сообщению присваивается строгая метка локального серверного времени и сохраняется идентификатор исходного сообщения (для «открыть исходник»). > 2. **Этап 1 — без ИИ:** Отбрасываются короткие неинформативные тексты, сообщения со стоп-фразами и (настраиваемо) **резюме соискателей**: если включён тумблер «Отсев резюме» (`blockResumes`), текст с любым маркером резюме/соискателя (`resumeMarkers`, список редактируется в «Сфере и ключах») удаляется сразу — **до правил колонок и ИИ** (раньше резюме могло залететь в колонку по стеку, минуя ИИ-фильтр). Слово «резюме» имеет контекстный guard: если перед ним в тексте есть маркер найма (например, «…вакансия…, присылайте резюме») — это объявление работодателя, оно не блокируется. Тумблер выключен — резюме собираются как обычные лиды (полезно, когда система настроена на поиск сотрудников). Дополнительно фильтр «собирать только найм / только разовые заказы» (`wantedType`) и опциональные фильтры «не создавать карточку без суммы» (отдельно для найма и разовых заказов: `budgetRequiredHire`/`budgetRequiredOrder`) также отрабатывают на этапе 1/без ИИ. **Отсев устаревших:** если автоархив включён и сообщение опубликовано раньше, чем за `archiveAfterDays` дней до текущего момента, оно удаляется сразу (в БД не попадает — ни карточкой, ни в архив/корзину). Не прошедшее карточкой не становится: отброс фиксируется в мониторинге «Отсев» с причиной и конкретным словом/фразой (см. п. 5.12) и живёт там до автоочистки (3 суток). > 3. **Дедупликация:** По нормализованному тексту (регистр, спецсимволы) вычисляется идентификатор; повторное сообщение игнорируется. > 4. **Правила колонок — не «словарный» роутер до ИИ:** смысловые колонки НЕ назначаются словарным матчем до ML/ИИ — он не понимает смысл и ловит ложные совпадения (дайджест из нескольких ролей, «desktop» в URL/футере и т.п.). Правила (направление, стек, слова, грейд, бюджет; режим «все/любое», исключения) используются как: (1) критерии, передаваемые ИИ-классификатору при выборе колонки; (2) страховка-проверка после выбора ИИ/ML (карточка попадает в колонку с активными правилами только если текст прошёл их); (3) обоснование «почему карточка здесь» (блок «Попала по фильтру — совпало»). Во всех «быстрых» путях (ML без ИИ / ИИ выключен) карточка **структурируется локальным разбором без ИИ**: заголовок (первая строка без markdown-мусора), стек/грейд/контакты/бюджет по меткам вида «Стек: …» и регулярным выражениям (суммы и валюты), признак вакансии. > 5. **ML-слой (обучаемый, отдельный сервис):** Между стоп-листом и ИИ работает локальная ML-модель, обучаемая **на реальных действиях пользователя** (перенос на доску, корзина, возврат, возврат из отсева) и на решениях ИИ. Если ML уверен — решает сам (спам уходит в отсев, колонка назначается) без обращения к ИИ. **ML не назначает колонки, у которых заданы активные правила, и ИИ-предложения** — такие колонки наполняются ИИ (с проверкой правил) или ручным выбором пользователя, чтобы нерелевантное не попадало в «отфильтрованные» колонки. Использование ML в пайплайне включается настройкой; **обучение идёт всегда**. > 5.1 **Полный выключатель ИИ** (`aiEnabled`, вкладка настроек «AI-классификатор»): при выключенном ИИ карточки собирает локальный разбор без провайдера, смысловую раскладку по колонкам берёт на себя ML (когда готова). ML в этом режиме обучается только вручную — действиями пользователя: переносы карточек, корзина, возвраты, возврат ошибочного отсева, ручная разметка в «ML-лаборатории». Кнопки «Предложить колонки» и «Переклассифицировать» при выключенном ИИ недоступны (с понятным сообщением). > 5.2 **Самооценка ML (индикатор на вкладке ИИ):** каждое реальное действие пользователя (delta=1.0: перенос на доску, корзина, возврат, ручная разметка) сверяется с текущим предсказанием модели до обучения на нём; если модель уверена — фиксируется «верно/ошибка». В статусе ML отдаётся окно последних 50 подтверждённых решений (верно/всего/точность). На вкладке «AI-классификатор» показывается прогресс проверки, а когда накоплено ≥ 50 решений с точностью ≥ 90% — зелёная подсказка **«ML справляется — ИИ можно отключить»** с кнопкой выключения ИИ. > 6. **ИИ-этап (если ML не уверен или выключен):** Сообщение проходит ИИ-фильтр с отдельным промптом (не пропускать простые сообщения, рекламу, скам и прочее; этап отключаемый), затем — ИИ-классификацию. Классификатору передаётся карта **только принятых колонок вместе с их критериями** (направление, стек, ключевые слова, грейд, бюджет, описание); колонка выбирается по совпадению критериев, а не по названию; если ни одна не подходит — `null` (в «Неразобранное»). Даже если ИИ/ML вернули колонку с активными правилами, бэкенд **проверяет текст правилами колонки** и при несоответствии оставляет карточку в «Неразобранном» (страховка от засорения отфильтрованных колонок). Классификатор возвращает структурированную карточку (заголовок, поля блока «О заявке» — company/format/task/requirements/plus/conditions, стек, бюджет с валютой, контакты, признак вакансии); при сбое ИИ карточка структурируется локальным разбором и не теряется. **Ручная переклассификация «Неразобранного» повторяет конвейер ИИ** с теми же страховками правил и обучающими сигналами для ML. > 6.1 **Промпт структуры карточки** (`cardPrompt`): отдельный настраиваемый промпт (вкладка AI-классификатора), который задаёт, какие поля блока «О заявке» и как заполнять (компания, формат, задача, требования, «будет плюсом», условия; для не-IT сфер «стек/требования» = материалы, услуги, навыки, инструменты). Добавляется к промпту классификатора отдельным блоком; текст «О заявке» сервер собирает из этих полей сам — поэтому у всех карточек одинаковая структура и разная длина. Поля заполняются только фактами из сообщения. > 6.2 **Бюджет карточки (fallback и распознавание):** если ИИ не выделил бюджет отдельным полем, но сумма с валютой есть в исходнике или в структурированной «О заявке» (часто уходит в «Условия») — она добирается автоматически (тот же источник, что и фильтр «не создавать без суммы»). Распознаются «2к»/«1.5к»/«$2к», «2000р/₽/руб», диапазоны «от X до Y»/«X–Y», «до X»; названия валют нормализуются (руб/рублей/₽, долларов/$/бакс, евро и т.п. → коды USD/EUR/RUB/…). Если ИИ вернул бюджет строкой с суффиксом («2к») или «р/₽» — значение тоже нормализуется. > * **Стек — всегда список строк**, а не строка: ответы ИИ нормализуются (строка «Java, Kotlin» превращается в массив), названия сохраняются слитно (`.NET`, `C#`, `Node.js`), одиночные символы отбрасываются. > * **Контакты** собираются списком и квалифицируются (см. п. 6 UI): телефон, email, @username, LinkedIn, WhatsApp, сайт; боты, каналы/группы и ссылки на посты/вакансии отбрасываются; при отсутствии в ответе ИИ контакты добываются из текста сообщения. > * У каждой карточки из сообщения есть **переход к исходному сообщению**: в подробном виде — явная кнопка «Открыть исходник» (ссылка на сообщение в Telegram по сохранённым id диалога и сообщения), текст исходника — под спойлером. > 7. **ИИ-предложения колонок:** По накопленным карточкам «Неразобранного» (от ~6) ИИ периодически (кулдаун ~20 мин) или по кнопке предлагает 2–4 смысловые колонки с обоснованием и критериями; карточки раскладываются по предложениям, пользователь принимает решение (см. п. 4.4). > 8. **Запись и триггер уведомлений:** Сохранение результата в базу (в БД оседает только прошедшее фильтры; исходные сообщения карточек хранятся с идентификатором для «исходника»). Сервер инициирует событие, фронтенд плавно добавляет карточку в верх соответствующей колонки (либо в «Неразобранное»), с визуальной и звуковой индикацией. > 9. **Обучение на действиях пользователя:** Действия с карточкой (ручной перенос, корзина, возврат, взятие в работу) всегда записываются в очередь обучения ML и учитываются при последующих обработках — система со временем точнее раскладывает похожие заявки или оставляет их в «Неразобранном». > 10. **Универсальность (не только IT) и настройка «Сфера и ключи»:** Система работает для любой сферы — разработка, дизайн, недвижимость, стройка/кровля, услуги и т.п. Распознающие паттерны **не зашиты в код**: в настройках (вкладка «Сфера и ключи») задаются: > * **Описание сферы** (`domainDescription`) — что для пользователя является заявкой/лидом; > * **Общие ключевые слова-маркеры** (`domainKeywords`) — слова/фразы, по которым сообщение опознаётся как заявка; кнопка **«Предложить ИИ»** анализирует накопленные карточки и предлагает список ключей (пользователь правит и сохраняет); > * Маркеры «найма» (`hireMarkers`), уровней (`levelTerms`) и резюме/соискателей (`resumeMarkers`, тумблер `blockResumes`) — используются этапом 1 и локальным разбором без ИИ; дефолты покрывают IT-найм, но редактируются под любую сферу и язык. > Промпты ИИ (классификатор, фильтр, предложение колонок/ключей) содержат плейсхолдеры `{domain}` и `{keywords}`, которые подставляются из этих настроек при каждом вызове — поэтому фильтрация спама и смысловые поля (заголовок, «о чём заявка», цена, контакты: телефон/@username/email) настраиваются под конкретный бизнес без правки кода. > 11. **Библиотека промптов и «Мои промпты»:** В настройках AI есть **библиотека готовых промптов** (на русском) для разных специальностей, разбитая на категории (IT/разработка, дизайн, недвижимость, стройка/ремонт, бытовые услуги, красота/здоровье, обучение и базовые) с **поиском по списку**. В «Базовых» есть универсальные **варианты поведения**: нейтральный, строгий (только явные заявки с деталями), гибкий (ничего не упускать), **«только найм (вакансии)»** и **«только заказы (услуги)»**. Шаблон можно посмотреть (превью текста), изменить под себя и **применить в редактор** (активным становится только после кнопки «Сохранить промпт» — текущий сохранённый промпт не меняется автоматически). Любой текст можно **сохранить в личный раздел «Мои промпты»** с собственным названием и описанием; оттуда промпт можно снова применить или удалить. Кнопка «Базовый промпт» возвращает универсальный дефолт. > 12. **Мониторинг обработки — вкладка «Обработка» (отдельный экран).** Прозрачность пайплайна: что сейчас в очереди, что и почему отсеяно. > * **Очередь** — сырые сообщения из каналов, ожидающие разбора: этап 1 (стоп-лист/длина, без ИИ) и ожидающие ИИ; список живой (обновляется по SSE + поллингом), у каждого сообщения — канал, статус этапа и время в очереди. Экран показывает, что воркер делает прямо сейчас (полезно при «Перечитать»/первичном разборе). > * **Отсев** — сообщения, не прошедшие любой этап, с пометкой **почему**: этап и причина (стоп-фраза, резюме соискателя, тип заявки, нет суммы, устарело, ML, ИИ-фильтр/спам, повтор), **конкретное слово/фраза** стоп-списка (если отсев по нему) и **чьё решение** — правила (без ИИ) / ML / ИИ / система. Повторное отбрасывание того же сообщения (перечитывание каналов) обновляет запись, а не плодит дубликаты. > * **Полнотекстовый поиск по отсеву** (FTS + LIKE по тексту, причине, слову и каналу), пагинация «показать ещё». > * **Очистка отсева:** вручную (кнопка «Очистить», с подтверждением; можно удалить отдельную запись) и автоматически — **раз в 3 суток** записи старше 3 дней удаляются безвозвратно. > * В боковом меню у пункта «Обработка» показывается **только счётчик сообщений в очереди**; отсев виден внутри вкладки. > * **Возврат из отсева в обработку:** у записи отсева есть действие «Вернуть в обработку» (кроме «повторов» — карточка уже существует). Открывается окно с полем «Почему обработано некорректно? (необязательно)». Возвращённое сообщение снова кладётся в очередь с пометкой force: **причины отсева для него игнорируются** (стоп-лист/резюме/тип/без суммы/устарело, ML и ИИ-отсев «спам») — оно проходит ML/ИИ и создаёт карточку; если ИИ снова скажет «спам», вердикт отменяется (карточка создаётся). ML обучается на действии (снятие веса «спама» за текст), а решение ИИ по возвращённому сообщению снова учит ML. Запись в отсеве не удаляется, а помечается «возвращено» с причиной (аудит); автоочистка через 3 суток действует как обычно. > * Сброс карточек/прогона (admin wipe / clear-cards) очищает и отсев, чтобы повторные тестовые прогоны не смешивались со старой историей. ## **6\. Требования к UI/UX и дизайну** > * **Цветовая палитра:** Глубокие темные оттенки фона с яркими акцентами через пользовательские цвета досок. > * **Звуковая обратная связь:** Деликатный синтезированный сигнал при поступлении приоритетного лида. > * **Анимации:** Плавное появление новой карточки, микро-индикаторы пульсации онлайн-статуса системы. > * **Удобство отклика:** Контакты заказчика выводятся крупно в отдельном блоке с кнопкой быстрого копирования и прямой ссылкой на диалог. Контакты **квалифицируются по типу** (Telegram/@username, телефон, почта, LinkedIn, WhatsApp, сайт): в подробном виде каждый контакт показан с меткой типа, кнопкой «открыть» (t.me/tel/mailto/ссылка) и копированием. Боты (@…bot), каналы/группы и «постовые» ссылки (teletype, формы, job-агрегаторы) контактами не считаются. > * **Навигация:** Боковое меню сворачивается в узкую панель иконок и разворачивается обратно. > * **Время:** Текущее время в 24-часовом формате (часы:минуты, без секунд) отображается в шапке дашборда рядом с днем недели и числом. > * **Сортировка:** В пределах колонки — безусловная хронологическая иерархия по времени получения (самые свежие заявки всегда сверху). > * **Счётчики:** бейджи и счётчики (Выбранные, Обработка, Каналы, Архив, Корзина, колонки-доски) отображаются только при значении > 0 — нулевые показатели нигде не выводятся. > * **Подтверждения и уведомления — только встроенные окна в стиле приложения:** единый модальный диалог подтверждения (очистка корзины/архива/«Отклонено»/отсева, удаление или отклонение колонки, сброс ML) и окна с полями (причина при возврате из отсева). Системные окна браузера (confirm/alert/prompt) не используются. > * **Отображение суммы на карточке:** одна сумма → «2 500 ₽»; только верхняя граница → «до 3 000 ₽»; диапазон → «2 500–3 000 ₽». «От 0» не выводится: «от 0 до X» отображается как «до X». ## **7\. План этапов разработки (Roadmap)** | Этап | Модуль | Ключевой результат | | :---- | :---- | :---- | | Спринт 1 | Ядро, авторизация дашборда и Telegram | Инициализация базы, авторизация в дашборд (admin/admin, сессия 30 дней), ввод Telegram api_id/api_hash, модуль веб-авторизации Telegram, веб\-интерфейс каналов с предпросмотром сообщений. | | Спринт 2 | AI & Маршрутизация | Пайплайн дедупликации, AI-классификатор, распределение на доски и в буфер неклассифицированного. | | Спринт 3 | UI/UX, Кастомизация | Дашборд-колонки с интерактивными карточками, выбор цветов, настройка полей, комментарии и корзина, звуковые уведомления, виджеты-счетчики, обучение на действиях пользователя. | | Спринт 4 | Деплой & Полировка | Настройка процессов развертывания системы, стресс-тесты под нагрузкой, финальная оптимизация. | ## **8\. Развертывание (Deployment)** > * Запуск системы — через **Docker Compose** (в том числе локально). > * В переменные окружения выносятся неконфиденциальные параметры: порты, пути к файлу БД и данным, пути хранения сессий. По договорённости в env также лежат **ключ шифрования секретов БД** (`LEADRADAR_ENCRYPTION_KEY`; при локальной разработке без env — файл `data/encryption.key`) и **креды MinIO** (`LEADRADAR_MINIO_ENDPOINT/_ACCESS_KEY/_SECRET_KEY/_BUCKET`). > * Секреты (Telegram **api_id/api_hash**, **ключи AI-провайдеров**, пароль дашборда) в env не передаются — задаются через веб-интерфейс (см. п. 4.1 и 4.6) и хранятся в БД в зашифрованном виде.