Инициализировать репозиторий «Дейл»
Первый коммит: модульный монолит ядра (.NET 10) и gRPC-сервисы ai/ml/telegram, фронтенд Vue 3/Vite/Tailwind, документация (ТЗ, инструкция пользователя, техдокументация, код-стайл), бэклог, скрипты развёртывания и архив прототипа LeadRadar.
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
namespace Deal.Modules.Discovery.Application.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Запись чёрного списка Discovery — элемент GET /api/discovery/blacklist (api-map §3.8 L213).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Поля 1:1 с _blacklist_view discovery.py L161–167 и строкой disc_blacklist (db.py L180–185). Список общий для
|
||||
/// всех задач (не привязан к DiscTasks): источники из него пропускаются поиском (add_candidate) и повторной
|
||||
/// проверкой перед авто-вступлением (воркер). Снимается вручную (DELETE …/blacklist/{dialogId}) или при ручном
|
||||
/// join. Повторное добавление того же источника обновляет Name/Reason и сохраняет CreatedAt (ON CONFLICT L572–575).
|
||||
/// </remarks>
|
||||
/// <param name="DialogId">Подписанный id источника (первичный ключ).</param>
|
||||
/// <param name="Name">Имя источника (пусто → DialogId).</param>
|
||||
/// <param name="Reason">Причина добавления («отклонено вручную», метка воркера и т.п.).</param>
|
||||
/// <param name="CreatedAt">Время добавления, epoch-ms.</param>
|
||||
public sealed record DiscoveryBlacklistDto(
|
||||
string DialogId,
|
||||
string Name,
|
||||
string Reason,
|
||||
long CreatedAt);
|
||||
@@ -0,0 +1,44 @@
|
||||
namespace Deal.Modules.Discovery.Application.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Кандидат задачи Discovery — элемент GET …/candidates и ответ join/reject (api-map §4.8 L355).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Поля 1:1 с _candidate_view discovery.py L140–158 и строкой disc_candidates (db.py L159–176). marks/topics —
|
||||
/// JSON-колонки, наружу всегда списки (marks — строки-метки, topics — элементы <see cref="DiscoveryTopicDto"/> для
|
||||
/// форумов). Статус — <see cref="Deal.Modules.Discovery.Application.DiscoveryCandidateStatuses"/>; переводы в
|
||||
/// joined/rejected — только через mark_joined/mark_rejected.
|
||||
/// </remarks>
|
||||
/// <param name="DialogId">Подписанный id источника (первичный ключ кандидата; как Dialogs.Id).</param>
|
||||
/// <param name="TaskId">Id задачи поиска, которой принадлежит кандидат.</param>
|
||||
/// <param name="Name">Отображаемое имя источника (пусто → DialogId).</param>
|
||||
/// <param name="Username">Username (handle) источника; пуст, если нет публичного username.</param>
|
||||
/// <param name="Kind">Тип источника: channel|group|forum (см. <see cref="Deal.Modules.Discovery.Application.DiscoveryCandidateKinds"/>).</param>
|
||||
/// <param name="Hue">Цвет источника из палитры DIALOG_HUES (hex «#rrggbb»; дефолт «#666»).</param>
|
||||
/// <param name="Participants">Число участников (после discovery_info); null — неизвестно.</param>
|
||||
/// <param name="LangRu">Язык источника: true — русский, false — не русский, null — не определён.</param>
|
||||
/// <param name="Marks">Метки оценки («закрытая группа (история скрыта)», «мало сообщений», …).</param>
|
||||
/// <param name="Topics">Оценка тем форума (kind=forum; для каналов/групп пуст).</param>
|
||||
/// <param name="FitRatio">Доля подходящих сообщений оценки (0..1); null — контент не оценён.</param>
|
||||
/// <param name="Status">Статус кандидата (см. <see cref="Deal.Modules.Discovery.Application.DiscoveryCandidateStatuses"/>).</param>
|
||||
/// <param name="AutoJoined">Вступили автоматически (воркером); false — вручную (join из UI).</param>
|
||||
/// <param name="JoinFailures">Неудачные авто-вступления подряд (3 → кандидат удаляется, Task 18).</param>
|
||||
/// <param name="CreatedAt">Время добавления, epoch-ms.</param>
|
||||
/// <param name="UpdatedAt">Время последнего изменения, epoch-ms.</param>
|
||||
public sealed record DiscoveryCandidateDto(
|
||||
string DialogId,
|
||||
string TaskId,
|
||||
string Name,
|
||||
string Username,
|
||||
string Kind,
|
||||
string Hue,
|
||||
int? Participants,
|
||||
bool? LangRu,
|
||||
IReadOnlyList<string> Marks,
|
||||
IReadOnlyList<DiscoveryTopicDto> Topics,
|
||||
double? FitRatio,
|
||||
string Status,
|
||||
bool AutoJoined,
|
||||
int JoinFailures,
|
||||
long CreatedAt,
|
||||
long UpdatedAt);
|
||||
@@ -0,0 +1,64 @@
|
||||
namespace Deal.Modules.Discovery.Application.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Патч кандидата (set_candidate discovery.py L456–494; поля — в нотации кандидата).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// null — поле не меняется; не-null значение записывается (marks/topics — полной заменой JSON-массива,
|
||||
/// пустой список очищает). Пустые после Trim name/username/kind/hue НЕ затирают текущее значение
|
||||
/// (python L480–482: «or row[col]» — оценка воркера не должна стирать имя фолбэком). participants:null из
|
||||
/// патча в python очищает колонку; в .NET null означает «не менять» — очистка не нужна (участников всегда
|
||||
/// присылает discovery_info либо поле не трогается).
|
||||
/// </remarks>
|
||||
public sealed record DiscoveryCandidatePatch
|
||||
{
|
||||
/// <summary>
|
||||
/// Новое имя (после Trim; пустое — не менять).
|
||||
/// </summary>
|
||||
public string? Name { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Новый username (после Trim; пустой — не менять).
|
||||
/// </summary>
|
||||
public string? Username { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Новый тип источника (после Trim; пустой — не менять).
|
||||
/// </summary>
|
||||
public string? Kind { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Новый цвет (после Trim; пустой — не менять).
|
||||
/// </summary>
|
||||
public string? Hue { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Новое число участников (null — не менять).
|
||||
/// </summary>
|
||||
public int? Participants { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Новый признак языка (true/false — меняет; null — не менять).
|
||||
/// </summary>
|
||||
public bool? LangRu { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Новые метки оценки (полная замена; null — не менять).
|
||||
/// </summary>
|
||||
public IReadOnlyList<string>? Marks { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Новые темы форума (полная замена; null — не менять).
|
||||
/// </summary>
|
||||
public IReadOnlyList<DiscoveryTopicDto>? Topics { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Новая доля подходящих сообщений (null — не менять).
|
||||
/// </summary>
|
||||
public double? FitRatio { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Новый флаг авто-вступления (у mark_joined ставится отдельным методом; патч — для воркера).
|
||||
/// </summary>
|
||||
public bool? AutoJoined { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
namespace Deal.Modules.Discovery.Application.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Полная запись нового кандидата задачи (INSERT disc_candidates; discovery.py L435–450).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Write-модель: содержит только заполняемые при добавлении поля. Служебные значения INSERT прототипа
|
||||
/// L436–449 проставляет хранилище: Participants=NULL, LangRu=NULL, Marks=«[]», Topics=«[]», FitRatio=NULL,
|
||||
/// Status=<c>new</c>, AutoJoined=false, JoinFailures=0; CreatedAt/UpdatedAt — UTC-now.
|
||||
/// </remarks>
|
||||
public sealed record DiscoveryCandidateRow
|
||||
{
|
||||
/// <summary>
|
||||
/// Подписанный id источника (первичный ключ DiscCandidates).
|
||||
/// </summary>
|
||||
public string DialogId { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Id задачи поиска.
|
||||
/// </summary>
|
||||
public string TaskId { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Имя источника (пустое → DialogId нормализует сервис).
|
||||
/// </summary>
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Username источника (пуст, если нет публичного username).
|
||||
/// </summary>
|
||||
public string Username { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Тип источника: channel|group|forum.
|
||||
/// </summary>
|
||||
public string Kind { get; init; } = "channel";
|
||||
|
||||
/// <summary>
|
||||
/// Цвет источника (дефолт «#666»).
|
||||
/// </summary>
|
||||
public string Hue { get; init; } = "#666";
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace Deal.Modules.Discovery.Application.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Запись лога задачи Discovery — элемент GET /api/discovery/tasks/{id}/log (api-map §3.8 L215).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Поля 1:1 с _log_view discovery.py L170–177 и строкой disc_log (db.py L189–195). Событие — каталог
|
||||
/// <see cref="Deal.Modules.Discovery.Application.DiscoveryLogEvents"/> (search|skip|review|join_auto|join_manual|
|
||||
/// reject|flood|error|done|…). Список — последние события задачи, новые сверху (ORDER BY created_at DESC).
|
||||
/// </remarks>
|
||||
/// <param name="Id">Короткий id записи (префикс <c>dl_</c>).</param>
|
||||
/// <param name="TaskId">Id задачи поиска.</param>
|
||||
/// <param name="Event">Событие (см. <see cref="Deal.Modules.Discovery.Application.DiscoveryLogEvents"/>).</param>
|
||||
/// <param name="Text">Текст/детали события (русская строка 1:1 с прототипом).</param>
|
||||
/// <param name="CreatedAt">Время события, epoch-ms.</param>
|
||||
public sealed record DiscoveryLogDto(
|
||||
string Id,
|
||||
string TaskId,
|
||||
string Event,
|
||||
string Text,
|
||||
long CreatedAt);
|
||||
@@ -0,0 +1,58 @@
|
||||
namespace Deal.Modules.Discovery.Application.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Создание задачи поиска — вход POST /api/discovery/tasks (DiscoveryTaskDraft: поля с дефолтами сервиса).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 1:1 с TaskCreate discovery_routes.py L50–60 и нормализацией create_task discovery.py L234–282: name — строка
|
||||
/// (обязательная, Trim); description/keywords/minSubscribers/lang/autoJoin имеют дефолты в сервисе;
|
||||
/// threshold/sampleSize/planJoins — null → дефолты (threshold/sampleSize — из настроек discEvalThreshold/
|
||||
/// discEvalSample, planJoins — 1). plan_joins дополнительно проходит план-бюджет (DiscoveryPlanGuard).
|
||||
/// </remarks>
|
||||
public sealed record DiscoveryTaskDraft
|
||||
{
|
||||
/// <summary>
|
||||
/// Название задачи (после Trim непустое — иначе 400 «Укажите название задачи»).
|
||||
/// </summary>
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Описание ниши/цели (может быть пустым).
|
||||
/// </summary>
|
||||
public string Description { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Ключевые слова поиска; null → пустой список (start до добавления ключей — 400).
|
||||
/// </summary>
|
||||
public IReadOnlyList<string>? Keywords { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Минимальное число участников; null → 0.
|
||||
/// </summary>
|
||||
public int? MinSubscribers { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Язык источников: ru|any; null/иное → ru.
|
||||
/// </summary>
|
||||
public string? Lang { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Порог подходящих сообщений, % (кламп 1..100); null → настройка discEvalThreshold.
|
||||
/// </summary>
|
||||
public int? Threshold { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Размер выборки сообщений при оценке (кламп ≥1); null → настройка discEvalSample.
|
||||
/// </summary>
|
||||
public int? SampleSize { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// План авто-вступлений (1..discJoinLimit + бюджет); null → 1.
|
||||
/// </summary>
|
||||
public int? PlanJoins { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Авто-вступления воркером; null → false.
|
||||
/// </summary>
|
||||
public bool? AutoJoin { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
namespace Deal.Modules.Discovery.Application.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Задача поиска Discovery — элемент GET /api/discovery/tasks и ответ всех мутаций (api-map §4.8 L353).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Поля 1:1 с _task_view discovery.py L116–137 и строкой disc_tasks (db.py L136–156). JSON-поле keywords
|
||||
/// наружу всегда список строк; статус — <see cref="Deal.Modules.Discovery.Application.DiscoveryTaskStatuses"/>.
|
||||
/// createdAt/updatedAt — epoch-ms (времена хранятся UTC, на границе переводятся в ms — конвенция проекта).
|
||||
/// </remarks>
|
||||
/// <param name="Id">Короткий id задачи (префикс <c>dt_</c>).</param>
|
||||
/// <param name="Name">Название задачи (обязательное, Trim).</param>
|
||||
/// <param name="Description">Описание ниши/цели (источник для ИИ-генерации ключей, Task 19).</param>
|
||||
/// <param name="Keywords">Ключевые слова поиска (JSON-массив; пусто — start возвращает 400).</param>
|
||||
/// <param name="MinSubscribers">Минимальное число участников (0 — не фильтровать).</param>
|
||||
/// <param name="Lang">Язык источников: <c>ru</c>|<c>any</c>.</param>
|
||||
/// <param name="Threshold">Порог подходящих сообщений оценки, % (1..100; дефолт discEvalThreshold).</param>
|
||||
/// <param name="SampleSize">Размер выборки сообщений при оценке (≥1; дефолт discEvalSample).</param>
|
||||
/// <param name="PlanJoins">План авто-вступлений задачи (1..discJoinLimit; занимает суточный бюджет).</param>
|
||||
/// <param name="AutoJoin">Авто-вступления воркером включены (иначе кандидатов вступает человек).</param>
|
||||
/// <param name="Status">Статус задачи (см. <see cref="Deal.Modules.Discovery.Application.DiscoveryTaskStatuses"/>).</param>
|
||||
/// <param name="SearchIdx">Индекс текущего ключа поиска (прогресс прохода по keywords).</param>
|
||||
/// <param name="SearchDone">Проход по всем ключам завершён (searchIdx ≥ keywords.Count).</param>
|
||||
/// <param name="Found">Найдено кандидатов (счётчик found).</param>
|
||||
/// <param name="Evaluated">Оценено/пропущено кандидатов (счётчик evaluated).</param>
|
||||
/// <param name="Joined">Вступили (счётчик joined; joined ≥ PlanJoins → задача done).</param>
|
||||
/// <param name="Rejected">Отклонено (счётчик rejected).</param>
|
||||
/// <param name="CreatedAt">Время создания, epoch-ms.</param>
|
||||
/// <param name="UpdatedAt">Время последнего изменения, epoch-ms.</param>
|
||||
public sealed record DiscoveryTaskDto(
|
||||
string Id,
|
||||
string Name,
|
||||
string Description,
|
||||
IReadOnlyList<string> Keywords,
|
||||
int MinSubscribers,
|
||||
string Lang,
|
||||
int Threshold,
|
||||
int SampleSize,
|
||||
int PlanJoins,
|
||||
bool AutoJoin,
|
||||
string Status,
|
||||
int SearchIdx,
|
||||
bool SearchDone,
|
||||
int Found,
|
||||
int Evaluated,
|
||||
int Joined,
|
||||
int Rejected,
|
||||
long CreatedAt,
|
||||
long UpdatedAt);
|
||||
@@ -0,0 +1,58 @@
|
||||
namespace Deal.Modules.Discovery.Application.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Патч задачи поиска — вход PATCH /api/discovery/tasks/{id} (все поля optional; TaskPatch discovery_routes.py L62–71).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// null — поле не меняется; не-null значение записывается (JSON-список keywords — полной заменой, пустой
|
||||
/// список очищает ключи). План-бюджет: увеличение PlanJoins относительно текущего значения проверяется
|
||||
/// DiscoveryPlanGuard с исключением самой задачи (patch_task discovery.py L292–298); границы значений
|
||||
/// (threshold/minSubscribers/sampleSize/lang) нормализует сервис как _validate_task_values L213–231.
|
||||
/// </remarks>
|
||||
public sealed record DiscoveryTaskPatch
|
||||
{
|
||||
/// <summary>
|
||||
/// Новое название (после Trim; пустое допустимо на patch — 1:1 прототип).
|
||||
/// </summary>
|
||||
public string? Name { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Новое описание (пустая строка очищает).
|
||||
/// </summary>
|
||||
public string? Description { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Новые ключевые слова (полная замена; null — не менять).
|
||||
/// </summary>
|
||||
public IReadOnlyList<string>? Keywords { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Новый минимум участников (кламп ≥0).
|
||||
/// </summary>
|
||||
public int? MinSubscribers { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Новый язык: ru|any (иное → ru).
|
||||
/// </summary>
|
||||
public string? Lang { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Новый порог оценки, % (кламп 1..100).
|
||||
/// </summary>
|
||||
public int? Threshold { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Новый размер выборки (кламп ≥1).
|
||||
/// </summary>
|
||||
public int? SampleSize { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Новый план авто-вступлений (рост — с проверкой бюджета, см. xml-doc типа).
|
||||
/// </summary>
|
||||
public int? PlanJoins { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Новый флаг авто-вступлений (false — выключить).
|
||||
/// </summary>
|
||||
public bool? AutoJoin { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
namespace Deal.Modules.Discovery.Application.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Полная запись для создания задачи поиска (IProjectStore-стиль write-псевдоним строки DiscTasks).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Write-модель: содержит полное состояние новой задачи, включая готовый id (<c>dt_...</c>, генерирует модуль —
|
||||
/// <see cref="Deal.Modules.Discovery.Application.DiscoveryIdPrefixes"/>). Служебные значения, вычисленные до
|
||||
/// записи: Status=<c>draft</c>, SearchIdx=0, SearchDone=false, счётчики Found/Evaluated/Joined/Rejected=0 —
|
||||
/// как в INSERT прототипа discovery.py L260–280. CreatedAt/UpdatedAt проставляет хранилище (UTC-now);
|
||||
/// keywords адаптер сериализует в JSON при записи.
|
||||
/// </remarks>
|
||||
public sealed record DiscoveryTaskRow
|
||||
{
|
||||
/// <summary>
|
||||
/// Готовый id задачи (префикс <c>dt_</c>), сгенерированный модулем.
|
||||
/// </summary>
|
||||
public string Id { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Название задачи (после Trim; непустое — валидирует сервис).
|
||||
/// </summary>
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Описание ниши/цели (может быть пустым).
|
||||
/// </summary>
|
||||
public string Description { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Ключевые слова поиска (JSON-список строк, очищенный от пустых).
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> Keywords { get; init; } = Array.Empty<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Минимальное число участников (0 — не фильтровать).
|
||||
/// </summary>
|
||||
public int MinSubscribers { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Язык источников: ru|any (дефолт ru).
|
||||
/// </summary>
|
||||
public string Lang { get; init; } = "ru";
|
||||
|
||||
/// <summary>
|
||||
/// Порог подходящих сообщений оценки, % (1..100).
|
||||
/// </summary>
|
||||
public int Threshold { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Размер выборки сообщений при оценке (≥1).
|
||||
/// </summary>
|
||||
public int SampleSize { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// План авто-вступлений (1..discJoinLimit; занимает суточный бюджет).
|
||||
/// </summary>
|
||||
public int PlanJoins { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Авто-вступления воркером включены.
|
||||
/// </summary>
|
||||
public bool AutoJoin { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
namespace Deal.Modules.Discovery.Application.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Элемент topics кандидата-форума — результат оценки темы (api-map §4.8 L355, discovery_worker L336–344).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Форма 1:1 с python discovery_worker._evaluate_content L336–344: {topicId, title, fitCount, total, fitRatio,
|
||||
/// passed}. Заполняется оценкой только для kind=forum (для каналов/групп topics пуст); passed — тема прошла
|
||||
/// порог (eval passed L229–237). Хранится JSON-массивом в DiscCandidates.TopicsJson и наружу отдаётся как есть.
|
||||
/// </remarks>
|
||||
/// <param name="TopicId">Id темы форума (из Telegram).</param>
|
||||
/// <param name="Title">Заголовок темы (пуст — тема без названия).</param>
|
||||
/// <param name="FitCount">Сколько сообщений темы подошли под описание задачи.</param>
|
||||
/// <param name="Total">Всего содержательных сообщений темы в выборке.</param>
|
||||
/// <param name="FitRatio">Доля подходящих сообщений (fitCount / total; 0 при пустой выборке).</param>
|
||||
/// <param name="Passed">Вердикт темы: прошла ли порог задачи (threshold).</param>
|
||||
public sealed record DiscoveryTopicDto(
|
||||
string TopicId,
|
||||
string Title,
|
||||
int FitCount,
|
||||
int Total,
|
||||
double FitRatio,
|
||||
bool Passed);
|
||||
Reference in New Issue
Block a user