Deal — единая кодовая база
ci / build-test (push) Canceled after 0s

SaaS-мониторинг Telegram: ядро (модули Cards/Kanban/Pipeline/Tenants/Settings/
Discovery, Api, Infrastructure), сервисы telegram/ai/ml/storage, фронт Vue,
контракты и grpc-hosting, деплой-конфиги (dev/prod/observability/CI-раннер),
Gitea Actions CI, документация (ТЗ, техдок, api-map, код-стайл, планы, бэклог).

Текущее состояние: все этапы роадмапа 0–12 закрыты, сборка 5 sln 0/0,
тесты 1340/130/52/38/9 зелёные.
This commit is contained in:
Rustam Khalimov
2026-09-11 23:56:47 +03:00
commit 27c7831910
1383 changed files with 158436 additions and 0 deletions
+276
View File
@@ -0,0 +1,276 @@
# Дейл — код-стайл (действующие правила)
> Единый свод правил стиля кода для всего репозитория (core, telegram/ai/ml-сервисы, тесты).
> Составлен на основе исходного `Стиль_кода.docx` (перенесён в `archive/style-guide-original/`),
> дополнен действующими правилами проекта и `.editorconfig`. Правила обязательны для нового кода;
> приведение существующего — в `docs/superpowers/backlog.md` (`TD-COMMENTS-IFACE`, `TD-PROTO-COMMENTS`).
Пометки:
- **[изм.]** — правило дополнено/уточнено относительно исходного документа.
- **[отмена]** — правило исходного документа, которое в этом проекте не применяется.
---
## 1. Именование
Используются стандартные соглашения .NET. Венгерская нотация и префиксы типов в именах не применяются.
- **Классы** — Pascal: `User`.
- **Интерфейсы** — Pascal с префиксом `I`: `IDisposable`, `ICardStore`.
- **Generic-параметры** — Pascal с `T`: `T`, `TKey`, `TValue`.
- **Публичные функции/методы** — Pascal: `Authenticate`.
- **Приватные функции/методы** — тоже Pascal: `Authenticate` (не camel).
- **Параметры функций** — camel: `userId`.
- **Свойства (public/private)** — Pascal: `FirstName`.
- **Public-поля** — Pascal: `FirstName`. **[изм.]** Публичное состояние — свойство (§4); публичное поле допускается
только для данных-контейнеров без логики и именуется Pascal.
- **Private-поля — обязательный префикс `_` + camelCase: `_firstName`.** **[изм.]** Без `_` запрещено.
Исключения — только для константоподобных полей: `const` и `static readonly` именуются PascalCase
(`MaxRetryCount`, `DefaultTimeout`).
- **Локальные переменные** — camel: `user`.
- **Константы** — Pascal: `MaxRetryCount` (приватные `const` и `static readonly` — тоже Pascal, без `_`).
- **Enum** — Pascal: `UserStatus`; **значения enum** — Pascal: `Active`.
- **Exception** — Pascal с суффиксом `Exception`: `UserAuthenticationException`.
- **Event** — Pascal: `StatusChanged`.
- **Namespace** — Pascal.
Не использовать сокращения, кроме общепринятых (`id`, `ui`, `http`, `grpc`, `json`, `api`).
## 2. Организация кода и файлов
- Один публичный тип — один файл; имя файла = имя типа. **[изм.]** Правило усилено: смешивать типы в
одном файле нельзя (небольшие вспомогательные private-классы — исключение).
- **`namespace` строго соответствует пути папки** (для тестов — тоже). Файлы группируются по назначению:
`Abstractions` (интерфейсы `I*`), `Services` (сервисы/воркеры/исполнители), `Models` (доменные типы,
enum/статусы/константные реестры), `Dtos` (`*Dto`/`*Request`/`*Response`/`*Patch`), `Extensions`
(`*Extensions`), `Options` (`*Options`), `Exceptions` (`*Exception`), `Registrars` (`*ModuleRegistrar`),
`Configurations` (EF-конфигурации), `Entities`, `Repositories`. Feature-папки допустимы и сохраняются
(`Endpoints`, `Middleware`, `Hosting`, `Parsing`, `ColumnRules` и т.п.).
- **Тестовые проекты** группируются по областям (`Modules/<X>`, `Api`, `Infrastructure`, `Contracts`,
`Grpc`, …), общие фейки/хелперы — в `Support`; `namespace` = `<ПроектТестов>.<Область>`.
- В одном файле — один `namespace`. File-scoped namespace допустим.
- Все `using` — в начале файла; сначала системные, затем сторонние/project.
- `using` внутри `namespace` не используются (внешние `using`).
- Порядок членов внутри типа: константы → поля → конструкторы → свойства → методы. Члены группируются
по назначению.
- **[отмена]** Регионы (`#region`) **не используются** — вместо них осмысленный порядок и декомпозиция.
- Если у свойства есть backing-поле, поле объявляется **над** свойством:
```csharp
private User _user;
public User User { get; set; }
```
## 3. Форматирование
- Стандартные настройки форматирования Visual Studio / `.editorconfig`.
- Фигурные скобки — всегда на отдельной строке (Allman).
- В `if`/`else` фигурные скобки используются **всегда**, даже для одной инструкции.
- Отступ — 4 пробела (символ табуляции в историческом документе; в проекте — пробелы).
- Длина строки — желательно не более 100 символов; при переносе продолжение сдвигается вправо на один
уровень отступа.
- Каждая переменная объявляется на отдельной строке.
- Если `get`/`set` свойства состоит из одной операции, допускается размещение на одной строке:
```csharp
public User
{
get { return user; }
}
```
- Модификаторы доступа указываются **всегда**, включая явный `private`.
## 4. Проектные соглашения .NET
Машиночитаемая часть правил форматирования/анализа — в `.editorconfig` и `Directory.Build.props`
(`Nullable=enable`, `TreatWarningsAsErrors=true`, `EnforceCodeStyleInBuild=true`). Ниже — соглашения
уровня кода, которые этими файлами не выражаются.
- **Публичные члены — только свойства** (`{ get; init; }` / `{ get; set; }`), **не публичные поля**.
**[изм.]** Отменяет исходное правило о публичных полях: публичное состояние — свойство.
- Приватное/внутреннее состояние без дополнительной логики — поле (см. §6); с логикой — свойство.
- Зависимости — через конструктор (DI). Настройки — через `IOptions<T>` / `IOptionsSnapshot<T>`;
прямое чтение `IConfiguration` в бизнес-коде не допускается.
- `var` не использовать для встроенных типов и когда тип неочевиден — предпочитать явный тип
(см. `.editorconfig`, `csharp_style_var_* = false`).
- **Время**: `DateTimeOffset` в UTC внутри домена; на wire — epoch-миллисекунды. Локальное время —
только на границе представления (UI).
- **JSON на wire** — camelCase; ошибки API — объект `{ "detail": ... }`.
- **Идентификаторы** — с префиксом сущности/типа (напр. `card_...`, `board_...`), без «сырых» чисел.
- `this.` для обращения к членам **запрещён** (`dotnet_style_qualification_* = false:warning`).
К приватным полям обращаемся по имени с `_` (`_logger.Info(...)`), к свойствам/методам — без
квалификации. Запрет распространяется на поля, свойства, методы и события. **[изм.]**
- Асинхронность: суффикс `Async`, `CancellationToken` пробрасывать до конца; `.Result` / `.Wait()`
запрещены — только `await`.
## 5. Комментирование кода
Все комментарии — на русском языке.
- **Комментируем то, что видно снаружи.** XML-doc (`///`) — на **public/protected** члены, типы и
интерфейсы. **[изм.]** Приватные/внутренние детали реализации комментариями не «обвешиваем» — только
там, где неочевидна причина/ограничение (короткий обычный комментарий).
- **Кратко.** Комментарий объясняет **зачем и что**, а не пересказывает код. Не дублировать имя и
сигнатуру словами.
- **`<summary>` — короткое описание (одна фраза).** Это назначение типа/члена, а **не** «как оно
работает» и не пояснения/детали реализации. Несколько предложений в summary не пишем.
- **`<remarks>` не используем** — подробные пояснения «как устроено» не нужны; rationale — только если
поведение действительно неочевидно, коротким обычным комментарием.
- **Никаких упоминаний процесса:** в комментариях запрещены ссылки на таски/этапы/рулинги/планы и
прототип (`Task N`, `Ruling N`, `этап N`, `python L…`, `main.py`, `прототип`, `LEADRADAR_*` и т.п.).
- **Внутренние `//`-комментарии — только для неочевидного поведения** (причина, ограничение, подвох).
Пересказ кода, пошаговая навигация и «что делает следующая строка» — удалять.
- **`<param>`/`<returns>`** — только если смысл не очевиден из имени/типа; не переписывать сигнатуру.
- **`<summary>` — только блочный.** Открывающий `<summary>` и закрывающий `</summary>` — **каждый на
своей строке**; запись в одну строку (`/// <summary>текст</summary>`) **не допускается**. **[изм.]**
Правильно:
```csharp
/// <summary>
/// Краткое описание назначения.
/// </summary>
public void DoWork() { }
```
Неправильно:
```csharp
/// <summary>Краткое описание.</summary>
public void DoWork() { }
```
- Прочие теги (`<param>`, `<returns>`, `<remarks>`, `<inheritdoc/>`) — по необходимости; `<param>`/`<returns>`
можно однострочно, `<remarks>` — блоком.
- Для функций, создающих исключения, возможные исключения указывать в `<exception>`.
- Для примеров использования — `<example>`, `<remarks>`, `<code>`.
- Для ссылок в документации — `<see cref="..."/>`, `<seeAlso cref="..."/>`.
- Спецсимволы XML в тексте комментария — через `CDATA`.
- Для сложных/неочевидных алгоритмов — пояснение каждого шага прямо в коде.
- **[изм.]** При изменении критичных участков/ядра — комментарий: кто, когда, почему.
- Временные заплатки — с `//TODO:` и указанием, что и когда должно быть исправлено.
- Неочевидные межкомпонентные зависимости (не ловятся компилятором) — описывать подробно.
## 6. Переменные и типы
- Свойство использовать только когда есть смысл. Если при получении/сохранении дополнительной логики
нет — использовать поле.
- Использовать максимально простой достаточный тип (`int`, а не `long`, когда `int` хватает).
- Константы — только для простых типов; для сложных — `static readonly`-поля.
- `object` — только когда действительно необходимо; в остальных случаях generic-и. `Hashtable` → `Dictionary<>`,
`ArrayList` → `List<>`.
- Boxing/unboxing value-типов — только при необходимости.
- При задании нецелых значений — минимум одна цифра до и после точки.
- Использовать имена типов C# (`int`, `string`), а не CTS (`Int32`, `String`).
- Поля и переменные инициализировать при объявлении, когда возможно.
- Конструктор по умолчанию, если класс требует параметров инициализации, делать `private`, чтобы клиент
не создал неинициализированный объект.
- Magic numbers для статусов/состояний запрещены — только константы/enum:
```csharp
// плохо
public User GetUserByStatus(int statusId);
// хорошо
public User GetUserByStatus(UserStatus userStatus);
```
- Если `get`/`set` содержит сложные вычисления, преобразование, побочный эффект или долго выполняется —
заменить свойством на метод.
- Свойство не должно менять значение от вызова к вызову при неизменном состоянии объекта.
- Внутри `get`/`set` не должно быть обращений к коду, не связанному напрямую с получением/сохранением значения.
- Настройки, влияющие на работу приложения, не хардкодить — выносить в конфигурацию. Значения по умолчанию
прописывать; если default невозможен и ключ отсутствует — выбрасывать исключение.
## 7. Функции
- Функции, возвращающие массив/коллекцию, всегда возвращают массив/коллекцию: если данных нет — пустой
экземпляр, но не `null`.
- Не более 7 параметров у функции. Больше — объединять в класс/DTO.
- **Перенос параметров:** если параметров **больше двух** — каждый на **отдельной строке** (открывающая `(` — в конце первой строки, закрывающая `)` — на отдельной строке с отступом объявления); если **два или меньше** — все параметры **в одну строку**.
Больше двух:
```csharp
public async Task<CardMoveResultDto> MoveAsync(
string cardId,
string toContainerId,
TransitionContext ctx,
CancellationToken ct)
```
Два или меньше:
```csharp
public User FindUser(string login, CancellationToken ct) { }
```
## 8. Управление выполнением программы
- При `foreach` по коллекции саму коллекцию модифицировать нельзя (не добавлять и не удалять элементы).
- Если задача решается и рекурсией, и циклом — предпочитать цикл; рекурсия — только когда цикл сложнее.
- Тернарный оператор — только для простых проверок; сложные условия — через `if`/`else`.
- Сложные составные условия разбивать на простые, сохраняя промежуточные результаты в `bool`-переменные.
- Типы, реализующие `IDisposable`, создавать в `using`:
```csharp
using (SqlConnection sqlConnection = new SqlConnection(...)) { }
```
## 9. События, делегаты, потоки
- Перед вызовом делегата/события — всегда проверка на `null`.
- Для простых event-ов использовать `EventHandler`/`EventArgs`.
- Для сложных event-ов — наследники `EventArgs`.
- Для блокировок использовать `lock`, а не класс `Monitor`.
## 10. Исключения и их обработка
- `try-catch` — только для непредвиденных ошибок, не для управления ходом программы.
- При пробрасывании выше — `throw;`, а **не** `throw ex;`.
- Свои исключения наследовать от `Exception`.
- Исключение создавать всегда, когда функция не может быть выполнена (неверные параметры, нет доступа к
БД, неизвестные идентификаторы и т.п.).
- Все исключения должны быть залогированы или показаны пользователю; пустые `catch` запрещены.
- В лог об ошибке, как правило, писать `StackTrace`.
## 11. Интерфейсы
- **Не дублировать `<summary>` интерфейса в реализации.** Если член объявлен в интерфейсе с XML-doc,
в классе-реализации достаточно `/// <inheritdoc/>` (или вообще ничего, если doc наследуется настройкой).
Текст описания пишется **один раз** — у интерфейса.
- **Явная реализация интерфейсов — по умолчанию** (`Task ICardStore.GetAsync(...)`). **[изм. 2026-09-11,
решение владельца]** Классы напрямую не вызываются — только через интерфейсы; исключения: DTO/модели
(напр. `Card` и семейство `I*Card`), хелперы, extension-классы. Весь прод-код уже переведён на явные
реализации (codemod `scripts/make_explicit.py`, идемпотентный).
- Один публичный тип интерфейса = один файл (как и для классов); имя файла = имя типа.
- **Маркерные классы не используются** — если нужен маркер, это маркерный интерфейс
(`IKanbanModule`, `ISharedKernel` и т.п.). **[изм. 2026-09-11]**
- **Тесты: моки — через NSubstitute** (`Substitute.For<IPasswordHasher>()`), тестовые переменные
типизируются интерфейсом. Новые hand-written фейк-классы не заводить; существующие мигрируются
поэтапно (план — `backlog.md`, `TD-TESTS-NSUBSTITUTE`). **[изм. 2026-09-11]**
## 12. Приложение: сводная таблица правил именования
| Идентификатор | Регистр | Пример |
| --- | --- | --- |
| Класс | Pascal | `User` |
| Локальная переменная | camel | `user` |
| Интерфейс | Pascal (`I`) | `IDisposable` |
| Generic | Pascal (`T`) | `T`, `TKey`, `TValue` |
| Публичная функция | Pascal | `Authenticate` |
| Приватная функция | Pascal | `Authenticate` |
| Параметр функции | camel | `userId` |
| Публичное свойство | Pascal | `FirstName` |
| Приватное свойство | Pascal | `FirstName` |
| Публичное поле | Pascal | `FirstName` |
| Приватное поле | `_` + camel | `_firstName` |
| Приватное `const` / `static readonly` | Pascal | `MaxRetryCount` |
| Константа | Pascal | `MaxRetryCount` |
| Enum | Pascal | `UserStatus` |
| Значение enum | Pascal | `Active` |
| Exception | Pascal (+`Exception`) | `UserAuthenticationException` |
| Event | Pascal | `StatusChanged` |
| Namespace | Pascal | `Deal.Core.Cards` |
## 13. Автоматизация
- **Служебные скрипты (codemod'ы, скрипты сборки/тестов/бэкапов) в репозиторий не входят** —
правило владельца: в репе только код. Актуальные копии живут локально, вне кода.
- **Проверка на новом коде**: правила `<summary>`-блока и «комментарии только на public» проверяемы
статически; задел — линтер (по аналогии с `scripts/i18n-lint.mjs`) и/или анализаторы Roslyn/StyleCop в
`Directory.Build.props`.
- Открытые пункты аудита и решения по ним — `docs/spec/Код-стайл-аудит-2026-09-11.md`.
@@ -0,0 +1,62 @@
# Аудит кода на соответствие код-стайлу «Дейл» (2026-09-11)
> Отчёт прохода по всему C#-коду (`src/**/*.cs`, 928 файлов, без `bin/obj`).
> Правила — `docs/spec/Код-стайл-Дейл.md`. Проверка: сборка 4 решений + все тесты.
## 1. Исправлено (применено и проверено)
| Пункт | Правило | Было | Стало | Инструмент |
| --- | --- | --- | --- | --- |
| Блочный `<summary>` | §5 | 5286 однострочных/инлайн (833 файла) | 0 | `scripts/fix_summary_blocks.py --apply` |
| XML-док на private/internal | §5 | 2028 блоков (359 файлов) | 0 (понижены до `//`) | `scripts/fix_private_docs.py --apply` |
| Квалификация `this.` | §4 | 124 | **0** | разовый Roslyn-инструмент (семантический) |
| Приватные instance-поля | §1 | camelCase (`logger`) | `_camelCase` (`_logger`) | разовый Roslyn-инструмент |
| Приватные `static readonly`/`const` | §1 | — | Pascal (`DefaultTimeout`) | разовый Roslyn-инструмент |
Дополнительно в `.editorconfig` включены машинные правила, теперь ломающие сборку при нарушении
(`TreatWarningsAsErrors=true`, `EnforceCodeStyleInBuild=true`):
- `dotnet_style_qualification_for_{field,property,method,event} = false:warning` — запрет `this.`;
- правила именования `IDE1006`: приватные instance-поля `_camelCase`, `const`/`static readonly` — Pascal.
Также проверено и **не требует правок**: `#region` нет; trailing whitespace нет; все файлы заканчиваются
переводом строки; кодировка UTF-8; настоящих public-полей нет (публичные члены — свойства); явные
модификаторы доступа соблюдены.
### Проверка после правок
- `dotnet build``Deal.sln`, `Deal.Telegram.sln`, `Deal.Ai.sln`, `Deal.Ml.sln`: 0 ошибок / 0 предупреждений.
- Тесты: core **1275/1275**, telegram **125/125**, ai **52/52**, ml **38/38** — все пройдены.
- Повторный прогон renamer: `this.` — 0, полей к переименованию — 0 (идемпотентно).
## 2. Остатки — решения (закрыто 2026-09-11, вечер)
1. **`var` — закрыто.** В `.editorconfig` включён ломающий сборку гейт `csharp_style_var_for_built_in_types = false:warning`
(запрет только для встроенных типов — как в §4); режимы «очевидный тип» и «прочие» оставлены `silent`
осознанно: правка субъективна и потребовала бы ~1600 механических замен. Остаток встроенных типов
выправлен `dotnet format style --diagnostics IDE0008` по всем 5 решениям (51 файл); сборка 5 sln — 0/0.
2. **Явная реализация интерфейсов (§11) — выполнено (вечер, решение владельца, вариант A).** 161 член
в 30 прод-файлах конвертирован codemod'ом `scripts/make_explicit.py` (частичные классы и многострочные
сигнатуры учтены); потребители конкретных типов перетипизированы на интерфейсы (8 мест в проде,
17 тест-файлов); `Card`/семейство `I*Card` оставлены implicit — это DTO, их члены и есть публичный API.
Правило закреплено в §11 код-стайла: классы напрямую не вызываем (DTO/хелперы/экстеншены — исключения).
3. **Дедупликация `<summary>` через `<inheritdoc/>` — закрыто: дублей нет.** Проверено двумя независимыми
сканами (сопоставление по тексту и по имени члена интерфейса: 39 интерфейсов, 229 задокументированных
членов) — реализаций, дублирующих summary интерфейсного члена, в продакшн-коде нет; случаев
«`<param>` + дублирующий summary» не существует.
4. **Переводы строк — решено: LF.** Обоснование: инструменты проекта (Python/Node-скрипты, codemod'ы) пишут LF;
shell-скрипты с CRLF не работают на Linux CI (`sh scripts/ci.sh` в GitHub Actions); фактическое большинство
файлов уже было LF. Применено: `.gitattributes` (`* text=auto eol=lf` + бинарные исключения),
`.editorconfig``end_of_line = lf`, конвертировано 1029 трекаемых файлов, `git add --renormalize`.
Побочный эффект: починены 42 CRLF-.sh (9 в `scripts/` — до этого первый удалённый прогон CI падал бы).
### Попутно исправлено (2026-09-11, вечер)
- Повторный прогон `scripts/fix_private_docs.py --apply`: понижено 12 XML-доков на private/internal (extension-файлы).
- Добавлены недостающие `<summary>`: `IContainerRules.Keywords`/`Stack`, `ITenantContext.TenantId`/`HasTenant`.
- Переведены на русский англоязычные `//`-комментарии (3 шт. из 18 найденных; остальные — имена
сущностей/заголовки секций тестов, не англоязычный текст).
- Из индекса убраны случайно закоммиченные `archive/**/__pycache__/*.pyc` (2 шт., уже в `.gitignore`).
- STATUS.md: удалён устаревший блок «Осталось (в backlog)» в шапке (пункты закрыты generic-контрактом источника).
- Дочистка по контрольному скану краткости: удалены 73 очевидных `<param name="ct|cancellationToken">`
(«Токен отмены.» — пересказ сигнатуры, §5) в 17 файлах; ужаты 3 summary (2 многосентенционных, 1 длинное).
Контроль: `<remarks>` — 0, inline-`<summary>` — 0, многосентенционных summary — 0, TODO — 0.
@@ -0,0 +1,257 @@
# Дейл (Deal) — Техническое задание на новую архитектуру
> Версия: 1.0 (отражает этапы 0–12)
> Дата: 2026-09-10
> Связанные документы: `docs/architecture/2026-09-05-deal-architecture-design.md`,
> `docs/architecture/2026-09-10-unified-api-contract.md`,
> `docs/architecture/2026-09-10-operator-analytics-contract.md`,
> исходное ТЗ прототипа LeadRadar V1.2 — `archive/leadradar-legacy/ТЗ-LeadRadar-v1.2.md`.
---
## 1. О продукте
«Дейл» — SaaS-сервис мониторинга Telegram-каналов и групп. Клиент подключает свой
Telegram-аккаунт, выбирает каналы/группы для мониторинга, а система:
1. получает сообщения из источников в реальном времени;
2. отсеивает мусор: рекламу, скам, служебные сообщения, дубликаты, устаревшее;
3. структурирует оставшееся в **карточки** (заказ/вакансия/услуга) по профилю клиента
(сфера, стек, бюджет, локация);
4. раскладывает карточки по **колонкам-фильтрам** клиента;
5. обучается на действиях клиента (ML) и всё больше обрабатывает поток сама;
6. помогает искать и подключать новые источники (Discovery).
**Целевая аудитория:** специалисты и мастера в разных сферах (разработчики, дизайнеры,
риелторы, строители и т.д.), которые ищут реальные заказы и клиентов в Telegram.
**Ключевая ценность:** видеть реальные заказы и клиентов, а не кучу дубликатов и рекламы.
---
## 2. Термины
- **Тенант** — клиент SaaS. Владеет схемой БД, настройками обработки, ML-моделью.
- **Аккаунт (Telegram)** — личный Telegram-аккаунт тенанта, подключённый к системе.
- **Источник** — откуда система получает записи. Сейчас это Telegram-канал/группа/чат (тема форума);
контракт источника универсален, поэтому позже сюда добавляются другие сервисы (WhatsApp, сайты,
файлы/таблицы) без изменения ядра.
- **Сырое сообщение** — оригинальное сообщение из источника до обработки (входные данные пайплайна).
- **Карточка** — единая сущность системы: ядро (id, заголовок, источник) + опциональные модули
(содержимое, бюджет, контакты, атрибуты, комментарии, ссылки, файлы, ТЗ, история, напоминания,
размещение в контейнере). Создаётся из прошедшего фильтры сообщения либо вручную; переезжает между
дашбордами/контейнерами без смены сущности. Термин «лид» не используется — это лишь входное сообщение.
- **Источник (Source)** — откуда пришла карточка: локально/вручную, ссылка на сайт, файл, Telegram
(канал/группа/чат, тема форума), колонка импортированных данных, внешний API, ИИ (провайдер+модель),
составной «первоисточник + цепочка обработки».
- **Контейнер** — общая база колонок/стадий/зон: пользовательские колонки дашборда (набор фильтров),
стадии «Выбранных», «Неразобранное», архив, корзина, терминальные зоны. У каждого контейнера —
политика (что можно/нельзя, автоочистка, терминальность).
- **Отсев** — сообщения, отклонённые пайплайном (с причиной).
---
## 3. Роли и доступ
| Роль | Возможности |
|---|---|
| **Оператор (владелец SaaS)** | Создаёт тенантов и инвайты; управляет лимитами; видит health; impersonation с аудитом |
| **Тенант (клиент)** | Входит по инвайту, задаёт пароль; подключает свой Telegram-аккаунт; настраивает обработку; работает с дашбордом |
- Регистрация — **только по инвайту** (ссылка/код от оператора).
- Логин: email + пароль; email уникален в масштабе SaaS; `tenantId` — в сессии/JWT.
- Вход оператора — отдельный, изолированный от тенантов.
---
## 4. Подключение Telegram-аккаунта
1. Оператор один раз задаёт ключи приложения Telegram (`api_id`/`api_hash`) — глобально.
2. Тенант в UI: «Добавить аккаунт» → QR-код (или телефон + код подтверждения).
3. Система сохраняет сессию аккаунта (в telegram-service) и показывает статус подключения.
4. **1 аккаунт на тенанта** на старте (схема допускает расширение).
5. При первом подключении система подтягивает список диалогов аккаунта (каналы/группы/чаты)
и обновляет его при каждом входе на экран каналов и в фоне (появление/исчезновение
источников отслеживается автоматически).
### Мониторинг источников
- Тенант включает/выключает мониторинг по каждому источнику из списка его диалогов.
- Настройка «новый чат → мониторинг автоматически» (вкл/выкл).
- Источники, удалённые/покинутые вне системы, исчезают из списка.
- Кнопка «Перечитать»: догон последних ~10 сообщений всех включённых источников
(с паузами, анти-бан).
- Полученные сообщения **сразу помечаются прочитанными** в Telegram.
### Discovery (поиск и подключение источников)
- Тенант создаёт **задачу поиска**: описание цели → ИИ генерирует ключевые слова.
- Система ищет каналы/группы/форумы, в которых аккаунт **не состоит** (глобальное правило).
- Каскад фильтров: участники → язык → содержание (по темам, порог ≥40%).
- Кандидаты показываются «на рассмотрение» с метаданными (тип, участники, fit «X из N»,
темы форума, метки: закрытая группа и т.п.).
- Действия: «Вступить и мониторить» (вручную) или авто-вступление с квотами
(50/сутки общий, паузы 50–70 с), «Отклонить» → чёрный список.
- Чёрный список исключает источник во всех задачах; снимается вручную.
---
## 5. Обработка входящих (пайплайн)
Путь сообщения: **источник → очередь → стоп-лист → дедуп → ML → ИИ → карточка**.
Всё, что отсеяно, — в «Отсеве» с причиной. Настройки обработки — **per-tenant**.
### Этап 1 (без ИИ, дёшево)
1. Минимальная длина текста.
2. **Стоп-фразы** (настраиваемый список).
3. Отсев резюме соискателей (настройка).
4. Тип заявки (только вакансии / только заказы) по контексту.
5. **Дедуп**: одинаковый текст (нормализованный хэш) уже в системе → отсев «повтор».
6. Устаревшее сообщение (старше срока архивации) → отсев.
### ML-слой
- Если ML-модель тенанта уверена — решает сама: спам → отсев; колонка → карточка сразу.
- Не уверена → сообщение уходит на ИИ.
- Возврат из отсева (force) идёт мимо ML к ИИ-классификации.
### ИИ-слой (если включён)
- ИИ-фильтр: сообщение не про заявки/интересы тенанта → отсев.
- Классификация: структурированный разбор (компания, формат, о задаче, требования,
плюсы, условия, бюджет, стек, контакты, тип заявки).
- Назначение колонки с проверкой её правил.
### Глобальные фильтры
- «Не создавать карточку без суммы» — отдельно для вакансий и для заказов.
- Исключения по ключевым словам/технологиям/бюджету/локации (стоп на уровне фильтров).
### Карточка
- Единая сущность: ядро (id, заголовок, источник) + опциональные модули. Вид карточки — композиция
модулей, не отдельный класс/таблица; третий дашборд работает с той же карточкой.
- Реализация (этап 9): карточка — **одна строка одной таблицы `Cards`** во всех дашбордах; таблица
`ProjectCards` упразднена. Модули — колонки той же строки (`StackJson`/`LinksJson`/`FilesJson`/
`HistoryJson`/`TzText`/напоминание), комментарии — общая таблица `LeadComments`. Колонки/стадии/зоны —
единый реестр контейнеров; пространства не пересекаются (карточка не может быть одновременно
в дашборде и в «Выбранных»), «взять в работу» — смена контейнера, а не клон.
- Модули: содержимое (единая структура «О заявке»: Компания → Формат → О задаче → Требования →
Будет плюсом → Условия), бюджет (from/to/валюта), контакты (квалифицированные: tg/phone/email/
linkedin/site), атрибуты (стек/грейд/локация/сроки — настраиваются тенантом в UI, не зашиты),
комментарии, ссылки, файлы, ТЗ, история движения, напоминание, размещение в контейнере.
- Исходное сообщение карточки хранится и доступно: текст структурируется и показывается в карточке,
вложения/ссылки/контакты — отдельными блоками; кнопка «Обновить из источника» догружает оригинал
у сервиса-владельца источника (для Telegram — по id сообщения), если он доступен.
---
## 6. Дашборд (канбан)
- Колонки: «Неразобранное», пользовательские колонки (набор фильтров), «Архив», «Корзина».
- Пользовательские колонки создаёт пользователь; ИИ может **предлагать** колонки с
обоснованием (по каким критериям), пользователь принимает/отклоняет/переименовывает.
- Колонка = сложный набор опциональных фильтров: ключевые слова/стек/грейд/уровень/цена/
бюджет/локация/тип + отрицательные фильтры («чтобы не попадало»).
- При помещении карточки в колонку указывается, **по каким критериям** она попала.
- Карточки в колонке: свежие сверху. Drag&drop между колонками (с обучением ML).
- Быстрые действия на карточке: комментарий, корзина, контакт, «открыть исходник».
- Виджеты-счётчики свёрнутых колонок; колонки можно двигать, менять размер/ширину.
- **Архив**: карточки старше N дней (настройка 1–30); очистка архива через 90 дней.
- **Корзина**: очистка раз в 7 дней; из архива/корзины карточку можно вернуть (на канбан).
### «Выбранные» (пространство стадий)
- То же пространство карточек: **те же карточки** в контейнерах-стадиях
(Запланировано → Отклик → Согласование → В работе → Проверка → Готово / Отложено и др.).
«Взять в работу» — переход карточки в контейнер, а не создание второй сущности.
- У карточки наполняются модули работы: комментарии, изменение суммы/стека/контактов,
прикрепление ссылок, ТЗ, **файлов** (медиа/документы; тип определяется автоматически;
хранение в S3/MinIO; на карточке значки количества файлов и ссылок).
- Отложенные: напоминания (через срок + в заданное время, выбор даты в календаре);
настройка в общих настройках; если напоминания выключены — окно не показывается и
установленные не срабатывают.
- История движения карточки (статус, дата, время) — под спойлером в карточке.
- Ручное создание карточки с тем же набором полей (пометка «создано локально»).
- В архив/корзину дашборда карточки «Выбранных» не попадают; свои терминальные зоны:
«Отклонено», «Выполнено» (политики контейнеров).
---
## 7. Вкладка «Обработка»
- **Очередь**: сырые сообщения, ожидающие обработки (этап 1 / ожидают ИИ), с автопрокруткой.
- **Отсев**: отклонённые сообщения с причиной и источником решения
(правила / ML / ИИ / система), включая конкретное стоп-слово/фразу.
- У записи: метаданные (источник, подпись, вид, время), «показать исходное сообщение» с форматированием,
кнопка обновления исходника у сервиса-владельца источника.
- Поиск по отсеву — полнотекстовый.
- Возврат из отсева в обработку: причины отсева игнорируются, ML/ИИ обучаются на решении;
можно указать причину возврата.
- Автоочистка отсева: раз в 3 дня; ручная очистка.
- Вкладка показывает счётчик обработки (в боковой панели отсев не показывается).
---
## 8. Настройки тенанта
- Telegram: ключи приложения (оператор), подключение аккаунта, авто-мониторинг новых.
- ИИ: провайдер (один; включая локальные), модель, ключ (хранится зашифрованно),
промпты (базовый + свой), библиотека готовых промптов по сферам + «мои промпты»,
вкл/выкл ИИ, вкл/выкл ИИ-фильтр.
- ML: вкл/выкл, обучение на действиях, проверка на сообщении/канале, сброс, самооценка
(«ML справляется с последними N сообщениями — ИИ можно отключить»).
- Обработка: стоп-фразы, длина, резюме, тип заявки, домен/ключи, маркеры найма/заказа.
- Проверка текста: сухой прогон по цепочке (стоп-правила → глобальные исключения → ML → ИИ →
«без суммы») без создания карточки — показывает этапы, причину отсева и куда попала бы карточка.
- Колонки: набор, правила, отрицательные фильтры, исключения.
- Валюта: целевая валюта отображения, источник курсов (4 запроса/сутки), конвертация
при приходе данных + пересчёт старых карточек (кроме архива/корзины); USDT = USD.
- Хранение: срок архивации (1–30 дней), очистка архива/корзины.
- Уведомления и напоминания (общие; отложенные — отдельно).
- Звук, внешний вид.
---
## 9. Лимиты (бюджет токенов)
- Каждый тенант имеет **бюджет токенов** на LLM-вызовы (период — настраивается).
- ai-service оценивает каждый вызов в токенах и списывает с бюджета.
- При исчерпании: AI-обработка переключается на fallback (ML/локальный разбор),
тенант получает уведомление; приём и базовая обработка сообщений не блокируются.
- Оператор видит расход по тенантам в админке и может менять бюджет.
---
## 10. Админка оператора
- Тенанты: создание, инвайты, статус, лимиты/бюджеты, приостановка.
- Health всех сервисов и очередей.
- Аудит: входы/выходы, инвайты, impersonation, действия оператора и пользователей тенанта
(создание/перенос/удаление карточек, комментарии, контейнеры, настройки, каналы).
- Аналитика: расход токенов (по дню/тенанту/провайдеру/модели) и лента действий с фильтрами.
- Подозрительная активность (по логам безопасности) и метрики сервисов (Prometheus/Grafana).
- UI: оператор-консоль (`#/operator`) и страница активации инвайта (`#/join`).
---
## 11. Нефункциональные требования
- **Безопасность**: TLS, mTLS между сервисами, параметризованный SQL, защита от
IDOR/XSS/SSRF/CSRF, Argon2id, rate limiting (прокси + приложение; счётчики — распределённые,
в БД, работают при нескольких инстансах), Cloudflare.
- **Надёжность**: ежедневные бэкапы (Postgres, файлы, сессии), outbox для событий;
авто-очистки (retention аудита, лимитов, окон rate-limit); мгновенный разлогин suspended-сессий.
- **Наблюдаемость**: структурированные логи → Loki, метрики (OpenTelemetry → Prometheus) → Grafana
+ правила алертов; история расхода токенов (`token_usage_events`).
- **Масштабируемость**: модульный монолит + отдельные сервисы (ml/ai/telegram);
горизонтальное масштабирование сервисов; k8s — позже.
- **Производительность**: пайплайн обрабатывает поток без потерь; анти-бан-паузы
Telegram не блокируют обработку.
- **Локализация (i18n)**: весь интерфейс — на русском; все пользовательские строки вынесены в ресурсы
(без хардкода в компонентах), включая тексты ошибок; фолбэк — русский. Переключатель языка и второй
язык — **в бэклоге**: делаем, когда возникнет потребность (основа в ресурсах уже готова).
Область — основное приложение и оператор-консоль. (Этап 11 roadmap.)
---
## 12. Ограничения и допущения
- Фронтенд (Vue 3 + Vite + Tailwind) переезжает из LeadRadar; с этапа 9 контракт карточек/колонок — единый
(`/api/cards` + `/api/containers`, см. `docs/architecture/2026-09-10-unified-api-contract.md`).
- Данные текущего LeadRadar тестовые — не мигрируются.
- Kafka, k8s, биллинг-провайдер, саморегистрация — вне рамок текущего этапа.
- 1 Telegram-аккаунт на тенанта; несколько аккаунтов — позже (схема готова).