From 1fbe29a838843d3552b082fb43d958d86833fa1a Mon Sep 17 00:00:00 2001 From: stepan Date: Sun, 13 Sep 2026 20:51:36 +0300 Subject: [PATCH] =?UTF-8?q?=D0=92=D1=8B=D0=BD=D0=B5=D1=81=D1=82=D0=B8=20?= =?UTF-8?q?=D0=BC=D0=B0=D0=B3=D0=B8=D1=87=D0=B5=D1=81=D0=BA=D0=B8=D0=B5=20?= =?UTF-8?q?=D1=81=D1=82=D1=80=D0=BE=D0=BA=D0=B8=20=D1=81=D1=82=D0=B0=D1=82?= =?UTF-8?q?=D1=83=D1=81=D0=BE=D0=B2,=20=D0=B2=D0=B8=D0=B4=D0=BE=D0=B2=20?= =?UTF-8?q?=D0=B8=20=D0=BA=D0=BB=D1=8E=D1=87=D0=B5=D0=B9=20=D0=B2=20=D0=BA?= =?UTF-8?q?=D0=B0=D1=82=D0=B0=D0=BB=D0=BE=D0=B3=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Литералы статусов/видов/фаз и wire-ключей заменены каталогами: значения сущностей и репозиториев — TenantStatuses/TenantLimitPeriods/InviteStatuses/ContainerKinds/ContainerSpaces/CardIds/Discovery*/PipelineQueueStatuses; контакты и виды вложений — CardContactTypes/CardFileKinds; источники и этапы отсева — PipelineRejectStages/PipelineRejectSources; ключи промптов/провайдеров — SettingsFieldKeys; wire-ключи канбана — KanbanWireKeys; режимы правил — ColumnRuleModes; операции/фазы/виды диалогов/контакты Telegram и общий BoolText — в Deal.Contracts/Deal.SharedKernel; имена метрик — DealMetrics. Значения не менялись. --- .../Deal.Api/Endpoints/AiCheckEndpoint.cs | 6 +- src/core/Deal.Api/Endpoints/CardsEndpoints.cs | 8 +- .../Deal.Api/Endpoints/ContainersEndpoints.cs | 4 +- .../Deal.Api/Endpoints/TelegramEndpoints.cs | 13 ++- .../Services/TelegramBackfillScheduler.cs | 8 +- .../Sources/SourceIngressGrpcService.cs | 7 +- .../Integrations/Models/CardContactTypes.cs | 42 +++++++ .../Integrations/Models/TelegramAuthPhases.cs | 37 ++++++ .../Models/TelegramDialogKinds.cs | 27 +++++ .../Integrations/Models/TelegramOperations.cs | 102 +++++++++++++++++ .../Integrations/Extensions/UriExtensions.cs | 5 +- .../Integrations/Options/MtlsOptions.cs | 9 +- .../Services/GrpcTelegramClient.cs | 34 +++--- .../Services/LocalColumnSuggester.cs | 4 +- .../Services/LocalTelegramGateway.cs | 4 +- .../Storage/Extensions/StringExtensions.cs | 7 +- .../Persistence/Entities/CardEntity.cs | 3 +- .../Persistence/Entities/ContainerEntity.cs | 5 +- .../Entities/DiscCandidateEntity.cs | 6 +- .../Persistence/Entities/DiscTaskEntity.cs | 4 +- .../Persistence/Entities/InviteEntity.cs | 4 +- .../Persistence/Entities/OperatorEntity.cs | 4 +- .../Persistence/Entities/QueueItemEntity.cs | 4 +- .../Entities/RejectedItemEntity.cs | 4 +- .../Persistence/Entities/TenantEntity.cs | 4 +- .../Persistence/Entities/TenantLimitEntity.cs | 4 +- .../Persistence/Entities/UserEntity.cs | 4 +- .../Repositories/DiscoveryStore.Candidates.cs | 8 +- .../Repositories/DiscoveryStore.Tasks.cs | 10 +- .../Application/Models/CardContact.cs | 4 +- .../Application/Models/CardFile.cs | 2 +- .../Application/Models/CardFileKinds.cs | 37 ++++++ .../Models/DiscoveryCandidateKinds.cs | 20 ++++ .../Models/DiscoveryCandidateRow.cs | 2 +- .../Application/Models/DiscoveryFitOrigins.cs | 17 +++ .../Application/Models/DiscoveryLanguages.cs | 17 +++ .../Services/DiscoveryEvaluator.cs | 6 +- .../Services/DiscoveryTasksService.cs | 4 +- .../DiscoveryWorkerService.Helpers.cs | 16 ++- .../Services/DiscoveryWorkerService.Search.cs | 2 +- .../Application/ColumnRules/ColumnMatcher.cs | 2 +- .../Application/ColumnRules/RulesDescriber.cs | 2 +- .../Application/Models/ColumnRuleModes.cs | 17 +++ .../Application/Models/ContainerCreateDto.cs | 4 +- .../Application/Models/KanbanWireKeys.cs | 52 +++++++++ .../Services/CardsService.Selected.cs | 6 +- .../Application/Models/AiRawCardKeys.cs | 107 ++++++++++++++++++ .../Models/PipelineRejectConstants.cs | 38 +++---- .../Models/PipelineRejectSources.cs | 32 ++++++ .../Models/PipelineRejectStages.cs | 77 +++++++++++++ .../Application/Parse/ContactsQualifier.cs | 27 ++--- .../Application/Services/AiRawCardMapper.cs | 50 ++++---- .../Application/Services/MlReviewService.cs | 2 +- .../Services/PipelineProcessingService.cs | 8 +- .../Application/Models/SettingsFieldKeys.cs | 37 ++++++ .../SettingsService.PatchMyPrompts.cs | 6 +- .../Services/SettingsService.PatchSecrets.cs | 6 +- .../Services/SettingsService.ReadMerge.cs | 11 +- .../Services/TenantAdminService.cs | 2 +- .../Observability/DealMetrics.cs | 63 +++++++++-- .../Deal.SharedKernel/Utilities/BoolText.cs | 17 +++ 61 files changed, 907 insertions(+), 167 deletions(-) create mode 100644 src/core/Deal.Contracts/Integrations/Models/CardContactTypes.cs create mode 100644 src/core/Deal.Contracts/Integrations/Models/TelegramAuthPhases.cs create mode 100644 src/core/Deal.Contracts/Integrations/Models/TelegramDialogKinds.cs create mode 100644 src/core/Deal.Contracts/Integrations/Models/TelegramOperations.cs create mode 100644 src/core/Deal.Modules.Cards/Application/Models/CardFileKinds.cs create mode 100644 src/core/Deal.Modules.Discovery/Application/Models/DiscoveryFitOrigins.cs create mode 100644 src/core/Deal.Modules.Discovery/Application/Models/DiscoveryLanguages.cs create mode 100644 src/core/Deal.Modules.Kanban/Application/Models/ColumnRuleModes.cs create mode 100644 src/core/Deal.Modules.Kanban/Application/Models/KanbanWireKeys.cs create mode 100644 src/core/Deal.Modules.Pipeline/Application/Models/AiRawCardKeys.cs create mode 100644 src/core/Deal.Modules.Pipeline/Application/Models/PipelineRejectSources.cs create mode 100644 src/core/Deal.Modules.Pipeline/Application/Models/PipelineRejectStages.cs create mode 100644 src/core/Deal.Modules.Settings/Application/Models/SettingsFieldKeys.cs create mode 100644 src/core/Deal.SharedKernel/Utilities/BoolText.cs diff --git a/src/core/Deal.Api/Endpoints/AiCheckEndpoint.cs b/src/core/Deal.Api/Endpoints/AiCheckEndpoint.cs index 6651c19..c459027 100644 --- a/src/core/Deal.Api/Endpoints/AiCheckEndpoint.cs +++ b/src/core/Deal.Api/Endpoints/AiCheckEndpoint.cs @@ -127,9 +127,9 @@ public static class AiCheckEndpoint && entry.ValueKind == JsonValueKind.Object) { return new AiConfigSetting( - ApiKey: ReadField(entry, "apiKey") ?? defaults.ApiKey, - BaseUrl: ReadField(entry, "baseUrl") ?? defaults.BaseUrl, - Model: ReadField(entry, "model") ?? defaults.Model); + ApiKey: ReadField(entry, SettingsFieldKeys.ApiKey) ?? defaults.ApiKey, + BaseUrl: ReadField(entry, SettingsFieldKeys.BaseUrl) ?? defaults.BaseUrl, + Model: ReadField(entry, SettingsFieldKeys.Model) ?? defaults.Model); } } catch (JsonException) diff --git a/src/core/Deal.Api/Endpoints/CardsEndpoints.cs b/src/core/Deal.Api/Endpoints/CardsEndpoints.cs index d27dd52..216139a 100644 --- a/src/core/Deal.Api/Endpoints/CardsEndpoints.cs +++ b/src/core/Deal.Api/Endpoints/CardsEndpoints.cs @@ -105,15 +105,15 @@ public static class CardsEndpoints CardsService cardsService = context.RequestServices.GetRequiredService(); CardCountsDto counts = await cardsService.CountsAsync(ct); - var wire = new Dictionary { ["new"] = counts.New }; + var wire = new Dictionary { [KanbanWireKeys.New] = counts.New }; foreach ((string col, CardColumnCountDto column) in counts.Columns) { wire[col] = column; } - wire["learning"] = counts.Learning; - wire["ml"] = counts.Ml; - wire["ai"] = counts.Ai; + wire[KanbanWireKeys.Learning] = counts.Learning; + wire[KanbanWireKeys.Ml] = counts.Ml; + wire[KanbanWireKeys.Ai] = counts.Ai; return Results.Ok(wire); } diff --git a/src/core/Deal.Api/Endpoints/ContainersEndpoints.cs b/src/core/Deal.Api/Endpoints/ContainersEndpoints.cs index aff2392..5bc70ec 100644 --- a/src/core/Deal.Api/Endpoints/ContainersEndpoints.cs +++ b/src/core/Deal.Api/Endpoints/ContainersEndpoints.cs @@ -288,12 +288,12 @@ public static class ContainersEndpoints var wire = new Dictionary(); if (state.Collapsed is { } collapsed) { - wire["collapsed"] = collapsed; + wire[KanbanWireKeys.Collapsed] = collapsed; } if (state.Width is not null) { - wire["width"] = state.Width; + wire[KanbanWireKeys.Width] = state.Width; } return wire; diff --git a/src/core/Deal.Api/Endpoints/TelegramEndpoints.cs b/src/core/Deal.Api/Endpoints/TelegramEndpoints.cs index d81a796..d049d97 100644 --- a/src/core/Deal.Api/Endpoints/TelegramEndpoints.cs +++ b/src/core/Deal.Api/Endpoints/TelegramEndpoints.cs @@ -67,7 +67,12 @@ public static class TelegramEndpoints private const string NotConnectedReason = "not-connected"; - private const string ReadyPhase = "ready"; + private const string ReadyPhase = TelegramAuthPhases.Ready; + + // Русские подписи видов диалогов для операторского UI. + private const string RussianChannelLabel = "канал"; + private const string RussianGroupLabel = "группа"; + private const string RussianChatLabel = "чат"; private const int PreviewDefaultLimit = 24; @@ -482,9 +487,9 @@ public static class TelegramEndpoints { return kind switch { - "channel" => "канал", - "group" or "forum" => "группа", - "chat" => "чат", + TelegramDialogKinds.Channel => RussianChannelLabel, + TelegramDialogKinds.Group or TelegramDialogKinds.Forum => RussianGroupLabel, + TelegramDialogKinds.Chat => RussianChatLabel, _ => kind, }; } diff --git a/src/core/Deal.Api/Services/TelegramBackfillScheduler.cs b/src/core/Deal.Api/Services/TelegramBackfillScheduler.cs index cb9eca2..d9f5f4c 100644 --- a/src/core/Deal.Api/Services/TelegramBackfillScheduler.cs +++ b/src/core/Deal.Api/Services/TelegramBackfillScheduler.cs @@ -12,6 +12,10 @@ public sealed class TelegramBackfillScheduler( IHostApplicationLifetime applicationLifetime, ILogger logger) { + // Имена фоновых задач (метки запуска). + private const string BackfillMonitoredJob = "backfill_monitored"; + private const string FirstBackfillJob = "first_backfill"; + // Флаг in-flight «Перечитать» всех каналов (Interlocked): повторный вызов не плодит // параллельные полные перечитывания (Security review, как RatesRefreshScheduler). private int _readRecentInProgress; @@ -27,7 +31,7 @@ public sealed class TelegramBackfillScheduler( return; } - RunBackground("backfill_monitored", RunReadRecentAsync); + RunBackground(BackfillMonitoredJob, RunReadRecentAsync); } /// @@ -50,7 +54,7 @@ public sealed class TelegramBackfillScheduler( return; } - RunBackground("first_backfill", (provider, runLogger, ct) => RunFirstBackfillsAsync(provider, runLogger, dialogIds, ct)); + RunBackground(FirstBackfillJob, (provider, runLogger, ct) => RunFirstBackfillsAsync(provider, runLogger, dialogIds, ct)); } // Выполняет работу в собственном scope; любые ошибки — warning в лог (как RatesRefreshScheduler). diff --git a/src/core/Deal.Api/Sources/SourceIngressGrpcService.cs b/src/core/Deal.Api/Sources/SourceIngressGrpcService.cs index 93b3eb1..17c8dd6 100644 --- a/src/core/Deal.Api/Sources/SourceIngressGrpcService.cs +++ b/src/core/Deal.Api/Sources/SourceIngressGrpcService.cs @@ -21,6 +21,11 @@ public sealed class SourceIngressGrpcService( IngressTenantResolver tenants, ILogger logger) : SourceIngressService.SourceIngressServiceBase { + // Исходы приёма источника для лога аудита. + private const string DuplicateOutcome = "duplicate"; + private const string NoOpOutcome = "no-op"; + private const string QueuedOutcome = "queued"; + /// /// PushSource — запись источника в очередь пайплайна тенанта. /// @@ -57,7 +62,7 @@ public sealed class SourceIngressGrpcService( tenant.Id, item.Source.Kind, item.Source.ExternalId ?? "-", - result.Duplicate ? "duplicate" : result.Id is null ? "no-op" : "queued"); + result.Duplicate ? DuplicateOutcome : result.Id is null ? NoOpOutcome : QueuedOutcome); return new PushSourceReply { diff --git a/src/core/Deal.Contracts/Integrations/Models/CardContactTypes.cs b/src/core/Deal.Contracts/Integrations/Models/CardContactTypes.cs new file mode 100644 index 0000000..92b2a57 --- /dev/null +++ b/src/core/Deal.Contracts/Integrations/Models/CardContactTypes.cs @@ -0,0 +1,42 @@ +namespace Deal.Contracts.Integrations.Models; + +/// +/// Типы контактов заказчика +/// +public static class CardContactTypes +{ + /// + /// Telegram (@username) + /// + public const string Telegram = "tg"; + + /// + /// Телефон + /// + public const string Phone = "phone"; + + /// + /// Email + /// + public const string Email = "email"; + + /// + /// WhatsApp + /// + public const string WhatsApp = "whatsapp"; + + /// + /// LinkedIn + /// + public const string LinkedIn = "linkedin"; + + /// + /// Сайт + /// + public const string Site = "site"; + + /// + /// Прочее (не опознано) + /// + public const string Other = "other"; +} diff --git a/src/core/Deal.Contracts/Integrations/Models/TelegramAuthPhases.cs b/src/core/Deal.Contracts/Integrations/Models/TelegramAuthPhases.cs new file mode 100644 index 0000000..265b40f --- /dev/null +++ b/src/core/Deal.Contracts/Integrations/Models/TelegramAuthPhases.cs @@ -0,0 +1,37 @@ +namespace Deal.Contracts.Integrations.Models; + +/// +/// Фазы входа в аккаунт Telegram +/// +public static class TelegramAuthPhases +{ + /// + /// Аккаунт не подключён + /// + public const string Idle = "idle"; + + /// + /// Ожидание номера телефона + /// + public const string Phone = "phone"; + + /// + /// Ожидание кода + /// + public const string Code = "code"; + + /// + /// Ожидание облачного пароля + /// + public const string Password = "password"; + + /// + /// Ожидание сканирования QR + /// + public const string Qr = "qr"; + + /// + /// Аккаунт авторизован + /// + public const string Ready = "ready"; +} diff --git a/src/core/Deal.Contracts/Integrations/Models/TelegramDialogKinds.cs b/src/core/Deal.Contracts/Integrations/Models/TelegramDialogKinds.cs new file mode 100644 index 0000000..88cf1e4 --- /dev/null +++ b/src/core/Deal.Contracts/Integrations/Models/TelegramDialogKinds.cs @@ -0,0 +1,27 @@ +namespace Deal.Contracts.Integrations.Models; + +/// +/// Виды диалогов Telegram +/// +public static class TelegramDialogKinds +{ + /// + /// Канал + /// + public const string Channel = "channel"; + + /// + /// Группа + /// + public const string Group = "group"; + + /// + /// Форум (темы внутри группы) + /// + public const string Forum = "forum"; + + /// + /// Личный чат + /// + public const string Chat = "chat"; +} diff --git a/src/core/Deal.Contracts/Integrations/Models/TelegramOperations.cs b/src/core/Deal.Contracts/Integrations/Models/TelegramOperations.cs new file mode 100644 index 0000000..fd2cac5 --- /dev/null +++ b/src/core/Deal.Contracts/Integrations/Models/TelegramOperations.cs @@ -0,0 +1,102 @@ +namespace Deal.Contracts.Integrations.Models; + +/// +/// Имена операций Telegram-гейта для логов и диагностики +/// +public static class TelegramOperations +{ + /// + /// Статус аккаунта + /// + public const string Status = "status"; + + /// + /// Вход по номеру телефона + /// + public const string StartPhone = "start_phone"; + + /// + /// Вход по QR + /// + public const string StartQr = "start_qr"; + + /// + /// Подтверждение кода + /// + public const string SendCode = "send_code"; + + /// + /// Подтверждение облачного пароля + /// + public const string SendPassword = "send_password"; + + /// + /// Выход из аккаунта + /// + public const string Logout = "logout"; + + /// + /// Обновление каталога диалогов + /// + public const string RefreshDialogs = "refresh_dialogs"; + + /// + /// Включение/выключение мониторинга диалога + /// + public const string SetMonitor = "set_monitor"; + + /// + /// Включение/выключение мониторинга всех диалогов + /// + public const string SetMonitorAll = "set_monitor_all"; + + /// + /// Перечитать последние сообщения + /// + public const string Backfill = "backfill"; + + /// + /// Чтение последних сообщений + /// + public const string ReadRecent = "read_recent"; + + /// + /// Чтение исходного сообщения + /// + public const string ReadSource = "read_source"; + + /// + /// Поиск источников + /// + public const string Search = "search"; + + /// + /// Информация об источнике + /// + public const string GetInfo = "get_info"; + + /// + /// Чтение сообщений для оценки + /// + public const string ReadForEval = "read_for_eval"; + + /// + /// Вступление в источник + /// + public const string Join = "join"; + + /// + /// Выход из источника + /// + public const string Leave = "leave"; + + /// + /// Информация об источнике (Discovery) + /// + public const string DiscoveryInfo = "discovery_info"; + + /// + /// Чтение сообщений источника (Discovery) + /// + public const string DiscoveryRead = "discovery_read"; +} diff --git a/src/core/Deal.Infrastructure/Integrations/Extensions/UriExtensions.cs b/src/core/Deal.Infrastructure/Integrations/Extensions/UriExtensions.cs index 5a38a50..064ade7 100644 --- a/src/core/Deal.Infrastructure/Integrations/Extensions/UriExtensions.cs +++ b/src/core/Deal.Infrastructure/Integrations/Extensions/UriExtensions.cs @@ -6,6 +6,9 @@ namespace Deal.Infrastructure.Integrations.Extensions; // Расширения Uri для SSRF-гейта интеграций internal static class UriExtensions { + // Имя loopback-хоста. + private const string LocalhostHost = "localhost"; + /// /// Проверяет, указывает ли URL на приватный/loopback/link-local адрес /// @@ -14,7 +17,7 @@ internal static class UriExtensions public static bool IsPrivateEndpoint(this Uri uri) { string host = uri.Host; - if (string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(host, LocalhostHost, StringComparison.OrdinalIgnoreCase)) { return true; } diff --git a/src/core/Deal.Infrastructure/Integrations/Options/MtlsOptions.cs b/src/core/Deal.Infrastructure/Integrations/Options/MtlsOptions.cs index b84afec..946fcbd 100644 --- a/src/core/Deal.Infrastructure/Integrations/Options/MtlsOptions.cs +++ b/src/core/Deal.Infrastructure/Integrations/Options/MtlsOptions.cs @@ -1,5 +1,7 @@ using Microsoft.Extensions.Configuration; +using Deal.SharedKernel.Utilities; + namespace Deal.Infrastructure.Integrations.Options; /// @@ -86,13 +88,16 @@ public sealed class MtlsOptions }; } + // Строковое представление включённого флага числом. + private const string OneLiteral = "1"; + /// /// Разбирает значение флага DEAL_MTLS_ENABLED /// /// Сырое значение env (null/пусто — выключено). public static bool IsEnabled(string? rawValue) - => string.Equals(rawValue, "1", StringComparison.Ordinal) - || string.Equals(rawValue, "true", StringComparison.OrdinalIgnoreCase); + => string.Equals(rawValue, OneLiteral, StringComparison.Ordinal) + || string.Equals(rawValue, BoolText.True, StringComparison.OrdinalIgnoreCase); // Обрезает путь конфигурации (env-значения с пробелами/кавычками не передаются в файловые API). // rawValue: Сырое значение env. diff --git a/src/core/Deal.Infrastructure/Integrations/Services/GrpcTelegramClient.cs b/src/core/Deal.Infrastructure/Integrations/Services/GrpcTelegramClient.cs index a51d70c..a7d6ef1 100644 --- a/src/core/Deal.Infrastructure/Integrations/Services/GrpcTelegramClient.cs +++ b/src/core/Deal.Infrastructure/Integrations/Services/GrpcTelegramClient.cs @@ -73,7 +73,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway } catch (Exception exception) { - throw TranslateTransportFailure(exception, tenantId, "status"); + throw TranslateTransportFailure(exception, tenantId, TelegramOperations.Status); } } @@ -95,7 +95,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway } catch (Exception exception) { - throw TranslateTransportFailure(exception, tenantId, "start_phone"); + throw TranslateTransportFailure(exception, tenantId, TelegramOperations.StartPhone); } } @@ -118,7 +118,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway } catch (Exception exception) { - throw TranslateTransportFailure(exception, tenantId, "start_qr"); + throw TranslateTransportFailure(exception, tenantId, TelegramOperations.StartQr); } } @@ -135,7 +135,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway } catch (Exception exception) { - throw TranslateTransportFailure(exception, tenantId, "send_code"); + throw TranslateTransportFailure(exception, tenantId, TelegramOperations.SendCode); } } @@ -152,7 +152,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway } catch (Exception exception) { - throw TranslateTransportFailure(exception, tenantId, "send_password"); + throw TranslateTransportFailure(exception, tenantId, TelegramOperations.SendPassword); } } @@ -167,7 +167,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway } catch (Exception exception) { - throw TranslateTransportFailure(exception, tenantId, "logout"); + throw TranslateTransportFailure(exception, tenantId, TelegramOperations.Logout); } } @@ -183,7 +183,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway } catch (Exception exception) { - throw TranslateTransportFailure(exception, tenantId, "refresh_dialogs"); + throw TranslateTransportFailure(exception, tenantId, TelegramOperations.RefreshDialogs); } } @@ -202,7 +202,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway } catch (Exception exception) { - throw TranslateTransportFailure(exception, tenantId, "set_monitor"); + throw TranslateTransportFailure(exception, tenantId, TelegramOperations.SetMonitor); } } @@ -218,7 +218,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway } catch (Exception exception) { - throw TranslateTransportFailure(exception, tenantId, "set_monitor_all"); + throw TranslateTransportFailure(exception, tenantId, TelegramOperations.SetMonitorAll); } } @@ -238,7 +238,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway } catch (Exception exception) { - throw TranslateTransportFailure(exception, tenantId, "backfill"); + throw TranslateTransportFailure(exception, tenantId, TelegramOperations.Backfill); } } @@ -260,7 +260,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway } catch (Exception exception) { - throw TranslateTransportFailure(exception, tenantId, "read_recent"); + throw TranslateTransportFailure(exception, tenantId, TelegramOperations.ReadRecent); } } @@ -283,7 +283,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway } catch (Exception exception) { - throw TranslateTransportFailure(exception, tenantId, "read_source"); + throw TranslateTransportFailure(exception, tenantId, TelegramOperations.ReadSource); } } @@ -303,7 +303,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway } catch (Exception exception) { - throw TranslateTransportFailure(exception, tenantId, "search"); + throw TranslateTransportFailure(exception, tenantId, TelegramOperations.Search); } } @@ -328,7 +328,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway } catch (Exception exception) { - throw TranslateTransportFailure(exception, tenantId, "get_info"); + throw TranslateTransportFailure(exception, tenantId, TelegramOperations.GetInfo); } } @@ -358,7 +358,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway } catch (Exception exception) { - throw TranslateTransportFailure(exception, tenantId, "read_for_eval"); + throw TranslateTransportFailure(exception, tenantId, TelegramOperations.ReadForEval); } } @@ -374,7 +374,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway } catch (Exception exception) { - throw TranslateTransportFailure(exception, tenantId, "join"); + throw TranslateTransportFailure(exception, tenantId, TelegramOperations.Join); } } @@ -390,7 +390,7 @@ public sealed class GrpcTelegramClient : ITelegramGateway } catch (Exception exception) { - throw TranslateTransportFailure(exception, tenantId, "leave"); + throw TranslateTransportFailure(exception, tenantId, TelegramOperations.Leave); } } diff --git a/src/core/Deal.Infrastructure/Integrations/Services/LocalColumnSuggester.cs b/src/core/Deal.Infrastructure/Integrations/Services/LocalColumnSuggester.cs index d1ad249..83c9f2c 100644 --- a/src/core/Deal.Infrastructure/Integrations/Services/LocalColumnSuggester.cs +++ b/src/core/Deal.Infrastructure/Integrations/Services/LocalColumnSuggester.cs @@ -39,8 +39,6 @@ ContainersService containersService) : IColumnSuggester private const string KeywordsEmptyReason = "ИИ не смог выделить ключи — попробуйте ещё раз"; - private const string RulesModeAny = "any"; - /// public async Task SuggestColumnsAsync(CancellationToken ct) { @@ -121,7 +119,7 @@ ContainersService containersService) : IColumnSuggester foreach (SuggestedColumnPlan plan in plans) { var rules = new ContainerRulesDto( - Mode: RulesModeAny, + Mode: ColumnRuleModes.Any, Direction: Array.Empty(), Keywords: [plan.Word], Stack: Array.Empty(), diff --git a/src/core/Deal.Infrastructure/Integrations/Services/LocalTelegramGateway.cs b/src/core/Deal.Infrastructure/Integrations/Services/LocalTelegramGateway.cs index 6dee6a7..010a30a 100644 --- a/src/core/Deal.Infrastructure/Integrations/Services/LocalTelegramGateway.cs +++ b/src/core/Deal.Infrastructure/Integrations/Services/LocalTelegramGateway.cs @@ -5,6 +5,8 @@ namespace Deal.Infrastructure.Integrations.Services; public sealed class LocalTelegramGateway : ITelegramGateway { + // Причина пустого чтения: сообщений в источнике нет. + private const string NoHistoryReason = "no_history"; // Фаза idle-формы (аккаунт не подключён — сервиса нет). private const string IdlePhase = "idle"; @@ -72,7 +74,7 @@ public sealed class LocalTelegramGateway : ITelegramGateway string dialogId, int limit, CancellationToken ct) - => Task.FromResult(new TelegramEvalReadDto(false, "no_history", [])); + => Task.FromResult(new TelegramEvalReadDto(false, NoHistoryReason, [])); Task ITelegramGateway.JoinAsync(string username, CancellationToken ct) => Task.CompletedTask; diff --git a/src/core/Deal.Infrastructure/Integrations/Storage/Extensions/StringExtensions.cs b/src/core/Deal.Infrastructure/Integrations/Storage/Extensions/StringExtensions.cs index f35320a..072bde7 100644 --- a/src/core/Deal.Infrastructure/Integrations/Storage/Extensions/StringExtensions.cs +++ b/src/core/Deal.Infrastructure/Integrations/Storage/Extensions/StringExtensions.cs @@ -1,8 +1,13 @@ +using Deal.SharedKernel.Utilities; + namespace Deal.Infrastructure.Integrations.Storage.Extensions; // Расширения string для разбора конфигурационных значений. internal static class StringExtensions { + // Строковое представление включённого флага числом. + private const string OneLiteral = "1"; + /// /// Разбирает строковое значение как булев флаг конфигурации /// @@ -10,6 +15,6 @@ internal static class StringExtensions /// True — значение распознано как включённое. public static bool IsTrue(this string raw) { - return string.Equals(raw, "true", StringComparison.OrdinalIgnoreCase) || raw == "1"; + return string.Equals(raw, BoolText.True, StringComparison.OrdinalIgnoreCase) || raw == OneLiteral; } } diff --git a/src/core/Deal.Infrastructure/Persistence/Entities/CardEntity.cs b/src/core/Deal.Infrastructure/Persistence/Entities/CardEntity.cs index a389a7f..59f1cd0 100644 --- a/src/core/Deal.Infrastructure/Persistence/Entities/CardEntity.cs +++ b/src/core/Deal.Infrastructure/Persistence/Entities/CardEntity.cs @@ -1,3 +1,4 @@ +using Deal.Modules.Cards.Application.Models; using NpgsqlTypes; namespace Deal.Infrastructure.Persistence.Entities; @@ -130,7 +131,7 @@ public sealed class CardEntity /// /// Предыдущая колонка /// - public string PrevCol { get; set; } = "inbox"; + public string PrevCol { get; set; } = CardIds.Inbox; /// /// Время помещения в архив diff --git a/src/core/Deal.Infrastructure/Persistence/Entities/ContainerEntity.cs b/src/core/Deal.Infrastructure/Persistence/Entities/ContainerEntity.cs index 95297d4..7f6f0a6 100644 --- a/src/core/Deal.Infrastructure/Persistence/Entities/ContainerEntity.cs +++ b/src/core/Deal.Infrastructure/Persistence/Entities/ContainerEntity.cs @@ -1,3 +1,4 @@ +using Deal.Modules.Kanban.Application.Models; using NpgsqlTypes; namespace Deal.Infrastructure.Persistence.Entities; @@ -35,12 +36,12 @@ public sealed class ContainerEntity /// /// Вид контейнера: board /// - public string Kind { get; set; } = "board"; + public string Kind { get; set; } = ContainerKinds.Board; /// /// Пространство: dashboard | selected /// - public string Space { get; set; } = "dashboard"; + public string Space { get; set; } = ContainerSpaces.Dashboard; /// /// Свёрнутость колонки на дашборде diff --git a/src/core/Deal.Infrastructure/Persistence/Entities/DiscCandidateEntity.cs b/src/core/Deal.Infrastructure/Persistence/Entities/DiscCandidateEntity.cs index 8c88eae..b04f08a 100644 --- a/src/core/Deal.Infrastructure/Persistence/Entities/DiscCandidateEntity.cs +++ b/src/core/Deal.Infrastructure/Persistence/Entities/DiscCandidateEntity.cs @@ -1,3 +1,5 @@ +using Deal.Modules.Discovery.Application.Models; + namespace Deal.Infrastructure.Persistence.Entities; /// @@ -28,7 +30,7 @@ public sealed class DiscCandidateEntity /// /// Тип источника: channel|group|forum. /// - public string Kind { get; set; } = "channel"; + public string Kind { get; set; } = DiscoveryCandidateKinds.Channel; /// /// Цвет источника из палитры DIALOG_HUES @@ -63,7 +65,7 @@ public sealed class DiscCandidateEntity /// /// Статус кандидата /// - public string Status { get; set; } = "new"; + public string Status { get; set; } = DiscoveryCandidateStatuses.New; /// /// Вступили автоматически diff --git a/src/core/Deal.Infrastructure/Persistence/Entities/DiscTaskEntity.cs b/src/core/Deal.Infrastructure/Persistence/Entities/DiscTaskEntity.cs index f738819..ba0bc15 100644 --- a/src/core/Deal.Infrastructure/Persistence/Entities/DiscTaskEntity.cs +++ b/src/core/Deal.Infrastructure/Persistence/Entities/DiscTaskEntity.cs @@ -1,3 +1,5 @@ +using Deal.Modules.Discovery.Application.Models; + namespace Deal.Infrastructure.Persistence.Entities; /// @@ -55,7 +57,7 @@ public sealed class DiscTaskEntity /// /// Статус задачи: draft|running|paused|done|failed. /// - public string Status { get; set; } = "draft"; + public string Status { get; set; } = DiscoveryTaskStatuses.Draft; /// /// Индекс текущего ключа поиска diff --git a/src/core/Deal.Infrastructure/Persistence/Entities/InviteEntity.cs b/src/core/Deal.Infrastructure/Persistence/Entities/InviteEntity.cs index a6d9d23..14c58d9 100644 --- a/src/core/Deal.Infrastructure/Persistence/Entities/InviteEntity.cs +++ b/src/core/Deal.Infrastructure/Persistence/Entities/InviteEntity.cs @@ -1,3 +1,5 @@ +using Deal.Modules.Tenants.Application.Models; + namespace Deal.Infrastructure.Persistence.Entities; /// @@ -20,7 +22,7 @@ public sealed class InviteEntity /// public Guid? TenantId { get; set; } - public string Status { get; set; } = "pending"; + public string Status { get; set; } = InviteStatuses.Pending; public DateTimeOffset ExpiresAt { get; set; } diff --git a/src/core/Deal.Infrastructure/Persistence/Entities/OperatorEntity.cs b/src/core/Deal.Infrastructure/Persistence/Entities/OperatorEntity.cs index 73c28cb..ed686f6 100644 --- a/src/core/Deal.Infrastructure/Persistence/Entities/OperatorEntity.cs +++ b/src/core/Deal.Infrastructure/Persistence/Entities/OperatorEntity.cs @@ -1,3 +1,5 @@ +using Deal.Modules.Tenants.Application.Models; + namespace Deal.Infrastructure.Persistence.Entities; /// @@ -14,7 +16,7 @@ public sealed class OperatorEntity public string PasswordHash { get; set; } = string.Empty; - public string Status { get; set; } = "active"; + public string Status { get; set; } = TenantStatuses.Active; public DateTimeOffset CreatedAt { get; set; } } diff --git a/src/core/Deal.Infrastructure/Persistence/Entities/QueueItemEntity.cs b/src/core/Deal.Infrastructure/Persistence/Entities/QueueItemEntity.cs index 789ab9f..2cae754 100644 --- a/src/core/Deal.Infrastructure/Persistence/Entities/QueueItemEntity.cs +++ b/src/core/Deal.Infrastructure/Persistence/Entities/QueueItemEntity.cs @@ -1,3 +1,5 @@ +using Deal.Modules.Pipeline.Application.Models; + namespace Deal.Infrastructure.Persistence.Entities; /// @@ -38,7 +40,7 @@ public sealed class QueueItemEntity /// /// Статус строки: new — ждёт разбора воркером, filtered — прошла фильтры и ждёт ИИ/ML. /// - public string Status { get; set; } = "new"; + public string Status { get; set; } = PipelineQueueStatuses.New; /// /// Признак возврата из отсева diff --git a/src/core/Deal.Infrastructure/Persistence/Entities/RejectedItemEntity.cs b/src/core/Deal.Infrastructure/Persistence/Entities/RejectedItemEntity.cs index f8b3846..6dece73 100644 --- a/src/core/Deal.Infrastructure/Persistence/Entities/RejectedItemEntity.cs +++ b/src/core/Deal.Infrastructure/Persistence/Entities/RejectedItemEntity.cs @@ -1,5 +1,7 @@ using NpgsqlTypes; +using Deal.Modules.Pipeline.Application.Models; + namespace Deal.Infrastructure.Persistence.Entities; /// @@ -50,7 +52,7 @@ public sealed class RejectedItemEntity /// /// Кто вынес решение /// - public string Source { get; set; } = "stop"; + public string Source { get; set; } = PipelineRejectSources.Rules; /// /// Время получения исходного сообщения diff --git a/src/core/Deal.Infrastructure/Persistence/Entities/TenantEntity.cs b/src/core/Deal.Infrastructure/Persistence/Entities/TenantEntity.cs index e58c8d8..d6b8a2a 100644 --- a/src/core/Deal.Infrastructure/Persistence/Entities/TenantEntity.cs +++ b/src/core/Deal.Infrastructure/Persistence/Entities/TenantEntity.cs @@ -1,3 +1,5 @@ +using Deal.Modules.Tenants.Application.Models; + namespace Deal.Infrastructure.Persistence.Entities; /// @@ -9,7 +11,7 @@ public sealed class TenantEntity public string Name { get; set; } = string.Empty; - public string Status { get; set; } = "active"; + public string Status { get; set; } = TenantStatuses.Active; public DateTimeOffset CreatedAt { get; set; } } diff --git a/src/core/Deal.Infrastructure/Persistence/Entities/TenantLimitEntity.cs b/src/core/Deal.Infrastructure/Persistence/Entities/TenantLimitEntity.cs index ded8a6d..f747ad6 100644 --- a/src/core/Deal.Infrastructure/Persistence/Entities/TenantLimitEntity.cs +++ b/src/core/Deal.Infrastructure/Persistence/Entities/TenantLimitEntity.cs @@ -1,3 +1,5 @@ +using Deal.Modules.Tenants.Application.Models; + namespace Deal.Infrastructure.Persistence.Entities; /// @@ -18,7 +20,7 @@ public sealed class TenantLimitEntity /// /// Тип периода: month|day. /// - public string Period { get; set; } = "month"; + public string Period { get; set; } = TenantLimitPeriods.Month; /// /// Начало текущего периода diff --git a/src/core/Deal.Infrastructure/Persistence/Entities/UserEntity.cs b/src/core/Deal.Infrastructure/Persistence/Entities/UserEntity.cs index a8521b6..2d133eb 100644 --- a/src/core/Deal.Infrastructure/Persistence/Entities/UserEntity.cs +++ b/src/core/Deal.Infrastructure/Persistence/Entities/UserEntity.cs @@ -1,3 +1,5 @@ +using Deal.Modules.Tenants.Application.Models; + namespace Deal.Infrastructure.Persistence.Entities; /// @@ -16,7 +18,7 @@ public sealed class UserEntity public string PasswordHash { get; set; } = string.Empty; - public string Status { get; set; } = "active"; + public string Status { get; set; } = TenantStatuses.Active; public DateTimeOffset CreatedAt { get; set; } } diff --git a/src/core/Deal.Infrastructure/Persistence/Repositories/DiscoveryStore.Candidates.cs b/src/core/Deal.Infrastructure/Persistence/Repositories/DiscoveryStore.Candidates.cs index 57f27fd..6fb4cdd 100644 --- a/src/core/Deal.Infrastructure/Persistence/Repositories/DiscoveryStore.Candidates.cs +++ b/src/core/Deal.Infrastructure/Persistence/Repositories/DiscoveryStore.Candidates.cs @@ -59,7 +59,7 @@ public sealed partial class DiscoveryStore Username = row.Username, Kind = row.Kind, Hue = row.Hue, - Status = "new", + Status = DiscoveryCandidateStatuses.New, CreatedAt = now, UpdatedAt = now, }); @@ -123,7 +123,7 @@ public sealed partial class DiscoveryStore return false; } - row.Status = "joined"; + row.Status = DiscoveryCandidateStatuses.Joined; row.AutoJoined = autoJoined; row.UpdatedAt = DateTimeOffset.UtcNow; await _dbContext.SaveChangesAsync(ct); @@ -135,7 +135,7 @@ public sealed partial class DiscoveryStore { DiscCandidateEntity? row = await _dbContext.DiscCandidates .FirstOrDefaultAsync(candidate => candidate.DialogId == dialogId, ct); - if (row is null || row.Status != "review") + if (row is null || row.Status != DiscoveryCandidateStatuses.Review) { return null; } @@ -156,7 +156,7 @@ public sealed partial class DiscoveryStore return false; } - row.Status = "rejected"; + row.Status = DiscoveryCandidateStatuses.Rejected; row.UpdatedAt = DateTimeOffset.UtcNow; await _dbContext.SaveChangesAsync(ct); return true; diff --git a/src/core/Deal.Infrastructure/Persistence/Repositories/DiscoveryStore.Tasks.cs b/src/core/Deal.Infrastructure/Persistence/Repositories/DiscoveryStore.Tasks.cs index e39bae7..2bf2766 100644 --- a/src/core/Deal.Infrastructure/Persistence/Repositories/DiscoveryStore.Tasks.cs +++ b/src/core/Deal.Infrastructure/Persistence/Repositories/DiscoveryStore.Tasks.cs @@ -45,7 +45,7 @@ public sealed partial class DiscoveryStore SampleSize = row.SampleSize, PlanJoins = row.PlanJoins, AutoJoin = row.AutoJoin, - Status = "draft", + Status = DiscoveryTaskStatuses.Draft, CreatedAt = now, UpdatedAt = now, }); @@ -101,7 +101,7 @@ public sealed partial class DiscoveryStore return false; } - row.Status = "running"; + row.Status = DiscoveryTaskStatuses.Running; if (resetProgress) { row.SearchIdx = 0; @@ -127,7 +127,7 @@ public sealed partial class DiscoveryStore return false; } - row.Status = "paused"; + row.Status = DiscoveryTaskStatuses.Paused; row.UpdatedAt = DateTimeOffset.UtcNow; await _dbContext.SaveChangesAsync(ct); return true; @@ -143,7 +143,7 @@ public sealed partial class DiscoveryStore return false; } - row.Status = "done"; + row.Status = DiscoveryTaskStatuses.Done; row.UpdatedAt = DateTimeOffset.UtcNow; await _dbContext.SaveChangesAsync(ct); return true; @@ -211,7 +211,7 @@ public sealed partial class DiscoveryStore async Task IDiscoveryStore.SumActivePlanAsync(string? excludeTaskId, CancellationToken ct) { IQueryable query = _dbContext.DiscTasks - .Where(task => task.Status != "done" && task.Status != "failed"); + .Where(task => task.Status != DiscoveryTaskStatuses.Done && task.Status != DiscoveryTaskStatuses.Failed); if (excludeTaskId is not null) { query = query.Where(task => task.Id != excludeTaskId); diff --git a/src/core/Deal.Modules.Cards/Application/Models/CardContact.cs b/src/core/Deal.Modules.Cards/Application/Models/CardContact.cs index 693f8b6..ab3ffa5 100644 --- a/src/core/Deal.Modules.Cards/Application/Models/CardContact.cs +++ b/src/core/Deal.Modules.Cards/Application/Models/CardContact.cs @@ -1,3 +1,5 @@ +using Deal.Contracts.Integrations.Models; + namespace Deal.Modules.Cards.Application.Models; /// @@ -8,7 +10,7 @@ public sealed record CardContact /// /// Тип контакта: tg/phone/email/linkedin/whatsapp/site/other. /// - public string Type { get; init; } = "other"; + public string Type { get; init; } = CardContactTypes.Other; /// /// Значение: @username, +7…, name@mail, url. diff --git a/src/core/Deal.Modules.Cards/Application/Models/CardFile.cs b/src/core/Deal.Modules.Cards/Application/Models/CardFile.cs index ba4d9ec..807420e 100644 --- a/src/core/Deal.Modules.Cards/Application/Models/CardFile.cs +++ b/src/core/Deal.Modules.Cards/Application/Models/CardFile.cs @@ -23,7 +23,7 @@ public sealed record CardFile /// /// Тип контента: image/video/audio/archive/document/other /// - public string Kind { get; init; } = "other"; + public string Kind { get; init; } = CardFileKinds.Other; public string ObjectKey { get; init; } = string.Empty; } diff --git a/src/core/Deal.Modules.Cards/Application/Models/CardFileKinds.cs b/src/core/Deal.Modules.Cards/Application/Models/CardFileKinds.cs new file mode 100644 index 0000000..af7bdb8 --- /dev/null +++ b/src/core/Deal.Modules.Cards/Application/Models/CardFileKinds.cs @@ -0,0 +1,37 @@ +namespace Deal.Modules.Cards.Application.Models; + +/// +/// Категории вложений карточки +/// +public static class CardFileKinds +{ + /// + /// Изображение + /// + public const string Image = "image"; + + /// + /// Видео + /// + public const string Video = "video"; + + /// + /// Аудио + /// + public const string Audio = "audio"; + + /// + /// Документ + /// + public const string Document = "document"; + + /// + /// Архив + /// + public const string Archive = "archive"; + + /// + /// Прочее (не опознано) + /// + public const string Other = "other"; +} diff --git a/src/core/Deal.Modules.Discovery/Application/Models/DiscoveryCandidateKinds.cs b/src/core/Deal.Modules.Discovery/Application/Models/DiscoveryCandidateKinds.cs index 25040ce..c531737 100644 --- a/src/core/Deal.Modules.Discovery/Application/Models/DiscoveryCandidateKinds.cs +++ b/src/core/Deal.Modules.Discovery/Application/Models/DiscoveryCandidateKinds.cs @@ -19,4 +19,24 @@ public static class DiscoveryCandidateKinds /// Форум (группа с темами; topics заполняются оценкой). /// public const string Forum = "forum"; + + /// + /// Русское написание «канал» (как приходит из Telegram) + /// + public const string ChannelAliasRu = "канал"; + + /// + /// Русское написание «форум» + /// + public const string ForumAliasRu = "форум"; + + /// + /// Английское написание «chat» + /// + public const string ChatAlias = "chat"; + + /// + /// Русское написание «чат» + /// + public const string ChatAliasRu = "чат"; } diff --git a/src/core/Deal.Modules.Discovery/Application/Models/DiscoveryCandidateRow.cs b/src/core/Deal.Modules.Discovery/Application/Models/DiscoveryCandidateRow.cs index dcdf44d..dbb4680 100644 --- a/src/core/Deal.Modules.Discovery/Application/Models/DiscoveryCandidateRow.cs +++ b/src/core/Deal.Modules.Discovery/Application/Models/DiscoveryCandidateRow.cs @@ -28,7 +28,7 @@ public sealed record DiscoveryCandidateRow /// /// Тип источника: channel|group|forum. /// - public string Kind { get; init; } = "channel"; + public string Kind { get; init; } = DiscoveryCandidateKinds.Channel; /// /// Цвет источника diff --git a/src/core/Deal.Modules.Discovery/Application/Models/DiscoveryFitOrigins.cs b/src/core/Deal.Modules.Discovery/Application/Models/DiscoveryFitOrigins.cs new file mode 100644 index 0000000..14ff0e9 --- /dev/null +++ b/src/core/Deal.Modules.Discovery/Application/Models/DiscoveryFitOrigins.cs @@ -0,0 +1,17 @@ +namespace Deal.Modules.Discovery.Application.Models; + +/// +/// Источники решения о соответствии сообщения +/// +public static class DiscoveryFitOrigins +{ + /// + /// Эвристика по ключевым словам + /// + public const string Heuristic = "heuristic"; + + /// + /// Локальная ML-модель + /// + public const string Ml = "ml"; +} diff --git a/src/core/Deal.Modules.Discovery/Application/Models/DiscoveryLanguages.cs b/src/core/Deal.Modules.Discovery/Application/Models/DiscoveryLanguages.cs new file mode 100644 index 0000000..2cbeab0 --- /dev/null +++ b/src/core/Deal.Modules.Discovery/Application/Models/DiscoveryLanguages.cs @@ -0,0 +1,17 @@ +namespace Deal.Modules.Discovery.Application.Models; + +/// +/// Языки источников Discovery +/// +public static class DiscoveryLanguages +{ + /// + /// Русскоязычные источники + /// + public const string Russian = "ru"; + + /// + /// Любой язык + /// + public const string Any = "any"; +} diff --git a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryEvaluator.cs b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryEvaluator.cs index 5fac7a4..574ba09 100644 --- a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryEvaluator.cs +++ b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryEvaluator.cs @@ -116,7 +116,7 @@ public sealed class DiscoveryEvaluator string raw = text ?? string.Empty; if (raw.Trim().Length < MinTextLength) { - return new DiscoveryMessageFit(false, "слишком короткое", "heuristic"); + return new DiscoveryMessageFit(false, "слишком короткое", DiscoveryFitOrigins.Heuristic); } if (settingsSnapshot.GetBool(SettingsKeys.MlEnabled, SettingsDefaults.MlEnabled)) @@ -167,11 +167,11 @@ public sealed class DiscoveryEvaluator string kw = (keyword ?? string.Empty).Trim(); if (kw.Length > 0 && haystack.Contains(kw.ToLowerInvariant(), StringComparison.Ordinal)) { - return new DiscoveryMessageFit(true, $"совпал ключ \"{kw}\"", "heuristic"); + return new DiscoveryMessageFit(true, $"совпал ключ \"{kw}\"", DiscoveryFitOrigins.Heuristic); } } - return new DiscoveryMessageFit(false, "нет совпадений с ключами", "heuristic"); + return new DiscoveryMessageFit(false, "нет совпадений с ключами", DiscoveryFitOrigins.Heuristic); } /// diff --git a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryTasksService.cs b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryTasksService.cs index 831b150..8c45a04 100644 --- a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryTasksService.cs +++ b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryTasksService.cs @@ -65,7 +65,7 @@ public sealed class DiscoveryTasksService(IDiscoveryStore store, DiscoveryPlanGu await planGuard.AssertPlanAsync(planJoins, ct).ConfigureAwait(false); await planGuard.AssertBudgetAsync(planJoins, excludeTaskId: null, ct).ConfigureAwait(false); - string lang = draft.Lang is "ru" or "any" ? draft.Lang : "ru"; + string lang = draft.Lang is DiscoveryLanguages.Russian or DiscoveryLanguages.Any ? draft.Lang : DiscoveryLanguages.Russian; // Дефолты threshold/sampleSize — из типизированного снимка настроек (C30: один GetAllAsync на создание). TenantSettingsSnapshot settingsSnapshot = await TenantSettingsSnapshot.LoadAsync(settings, ct); @@ -230,7 +230,7 @@ public sealed class DiscoveryTasksService(IDiscoveryStore store, DiscoveryPlanGu int? threshold = patch.Threshold is int value ? Clamp(value, min: 1, max: 100) : null; int? sampleSize = patch.SampleSize is int sample ? Math.Max(1, sample) : null; int? minSubscribers = patch.MinSubscribers is int min ? Math.Max(0, min) : null; - string? lang = patch.Lang is "ru" or "any" ? patch.Lang : patch.Lang is null ? null : "ru"; + string? lang = patch.Lang is DiscoveryLanguages.Russian or DiscoveryLanguages.Any ? patch.Lang : patch.Lang is null ? null : DiscoveryLanguages.Russian; string? name = patch.Name is null ? null : patch.Name.Trim(); string? description = patch.Description; return new DiscoveryTaskPatch diff --git a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Helpers.cs b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Helpers.cs index ccf21d7..5184145 100644 --- a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Helpers.cs +++ b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Helpers.cs @@ -7,12 +7,16 @@ namespace Deal.Modules.Discovery.Application.Services; // источника (KindCode) и тексты сообщений для оценки (GroupTexts). public sealed partial class DiscoveryWorkerService { + // Префиксы сообщения об ошибке Telegram при FloodWait. + private const string FloodPrefix = "flood:"; + private const string FloodSpacePrefix = "flood "; + internal static bool IsFlood(Exception exception) { string message = exception.Message ?? string.Empty; - return message.StartsWith("flood:", StringComparison.OrdinalIgnoreCase) - || message.StartsWith("flood ", StringComparison.OrdinalIgnoreCase) - || message.Contains("flood:", StringComparison.OrdinalIgnoreCase); + return message.StartsWith(FloodPrefix, StringComparison.OrdinalIgnoreCase) + || message.StartsWith(FloodSpacePrefix, StringComparison.OrdinalIgnoreCase) + || message.Contains(FloodPrefix, StringComparison.OrdinalIgnoreCase); } private static string KindCode(string kind, bool isForum = false) @@ -24,9 +28,9 @@ public sealed partial class DiscoveryWorkerService return kind.Trim().ToLowerInvariant() switch { - DiscoveryCandidateKinds.Channel or "канал" => DiscoveryCandidateKinds.Channel, - DiscoveryCandidateKinds.Forum or "форум" => DiscoveryCandidateKinds.Forum, - "chat" or "чат" => DiscoveryCandidateKinds.Group, + DiscoveryCandidateKinds.Channel or DiscoveryCandidateKinds.ChannelAliasRu => DiscoveryCandidateKinds.Channel, + DiscoveryCandidateKinds.Forum or DiscoveryCandidateKinds.ForumAliasRu => DiscoveryCandidateKinds.Forum, + DiscoveryCandidateKinds.ChatAlias or DiscoveryCandidateKinds.ChatAliasRu => DiscoveryCandidateKinds.Group, _ => DiscoveryCandidateKinds.Group, }; } diff --git a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Search.cs b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Search.cs index e94ea57..715044b 100644 --- a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Search.cs +++ b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Search.cs @@ -56,7 +56,7 @@ public sealed partial class DiscoveryWorkerService foreach (TelegramDialogEntryDto item in results) { string rawKind = (item.Kind ?? string.Empty).Trim().ToLowerInvariant(); - if (rawKind == "чат" || rawKind == "chat") + if (rawKind == DiscoveryCandidateKinds.ChatAliasRu || rawKind == DiscoveryCandidateKinds.ChatAlias) { await _log.AddAsync(taskId, DiscoveryLogEvents.Skip, $"{item.Name}: личный чат/бот", ct) .ConfigureAwait(false); diff --git a/src/core/Deal.Modules.Kanban/Application/ColumnRules/ColumnMatcher.cs b/src/core/Deal.Modules.Kanban/Application/ColumnRules/ColumnMatcher.cs index 8ecc1ef..b6195c5 100644 --- a/src/core/Deal.Modules.Kanban/Application/ColumnRules/ColumnMatcher.cs +++ b/src/core/Deal.Modules.Kanban/Application/ColumnRules/ColumnMatcher.cs @@ -56,7 +56,7 @@ public static class ColumnMatcher bool budgetOk = !budgetEnabled || BudgetInRange.IsInRange(AmountParser.Parse(text), rules.Budget!, rates); bool pricesOk = !pricesEnabled || BudgetInRange.IsInRange(AmountParser.Parse(text), rules.Prices!, rates); - if (string.Equals(rules.Mode, "any", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(rules.Mode, ColumnRuleModes.Any, StringComparison.OrdinalIgnoreCase)) { // «любое»: совпасть должна хотя бы одна ВКЛЮЧЁННАЯ (непустая) группа. return (keywords.Count > 0 && keywordsOk) diff --git a/src/core/Deal.Modules.Kanban/Application/ColumnRules/RulesDescriber.cs b/src/core/Deal.Modules.Kanban/Application/ColumnRules/RulesDescriber.cs index aa963c0..ba9106e 100644 --- a/src/core/Deal.Modules.Kanban/Application/ColumnRules/RulesDescriber.cs +++ b/src/core/Deal.Modules.Kanban/Application/ColumnRules/RulesDescriber.cs @@ -69,7 +69,7 @@ public static class RulesDescriber return NoRulesText; } - string mode = string.Equals(rules.Mode, "any", StringComparison.OrdinalIgnoreCase) + string mode = string.Equals(rules.Mode, ColumnRuleModes.Any, StringComparison.OrdinalIgnoreCase) ? AnyModePrefix : AllModePrefix; return mode + " · " + string.Join("; ", parts); diff --git a/src/core/Deal.Modules.Kanban/Application/Models/ColumnRuleModes.cs b/src/core/Deal.Modules.Kanban/Application/Models/ColumnRuleModes.cs new file mode 100644 index 0000000..f629f3e --- /dev/null +++ b/src/core/Deal.Modules.Kanban/Application/Models/ColumnRuleModes.cs @@ -0,0 +1,17 @@ +namespace Deal.Modules.Kanban.Application.Models; + +/// +/// Режимы сопоставления правил колонки +/// +public static class ColumnRuleModes +{ + /// + /// Достаточно любого условия + /// + public const string Any = "any"; + + /// + /// Нужны все условия + /// + public const string All = "all"; +} diff --git a/src/core/Deal.Modules.Kanban/Application/Models/ContainerCreateDto.cs b/src/core/Deal.Modules.Kanban/Application/Models/ContainerCreateDto.cs index 5768b09..65e5b59 100644 --- a/src/core/Deal.Modules.Kanban/Application/Models/ContainerCreateDto.cs +++ b/src/core/Deal.Modules.Kanban/Application/Models/ContainerCreateDto.cs @@ -15,8 +15,8 @@ public sealed record ContainerCreateDto( string Name, string Description = "", string? Color = null, - string Space = "dashboard", - string Kind = "board", + string Space = ContainerSpaces.Dashboard, + string Kind = ContainerKinds.Board, bool Suggested = false, ContainerRulesDto? Rules = null, string Note = ""); diff --git a/src/core/Deal.Modules.Kanban/Application/Models/KanbanWireKeys.cs b/src/core/Deal.Modules.Kanban/Application/Models/KanbanWireKeys.cs new file mode 100644 index 0000000..d4e9ef6 --- /dev/null +++ b/src/core/Deal.Modules.Kanban/Application/Models/KanbanWireKeys.cs @@ -0,0 +1,52 @@ +namespace Deal.Modules.Kanban.Application.Models; + +/// +/// Ключи wire-контракта канбана (ответы API и патчи карточки) +/// +public static class KanbanWireKeys +{ + /// + /// Счётчик новых карточек + /// + public const string New = "new"; + + /// + /// Счётчик в обучении ML + /// + public const string Learning = "learning"; + + /// + /// Счётчик решённых ML + /// + public const string Ml = "ml"; + + /// + /// Счётчик решённых ИИ + /// + public const string Ai = "ai"; + + /// + /// Признак свёрнутой колонки + /// + public const string Collapsed = "collapsed"; + + /// + /// Ширина колонки + /// + public const string Width = "width"; + + /// + /// Нижняя граница бюджета + /// + public const string From = "from"; + + /// + /// Верхняя граница бюджета + /// + public const string To = "to"; + + /// + /// Валюта бюджета + /// + public const string Currency = "cur"; +} diff --git a/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Selected.cs b/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Selected.cs index 34b97ab..5704379 100644 --- a/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Selected.cs +++ b/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Selected.cs @@ -361,9 +361,9 @@ public sealed partial class CardsService return ClearedBudget; } - double? from = ReadNumber(element, "from"); - double? to = ReadNumber(element, "to"); - string cur = ReadText(element, "cur"); + double? from = ReadNumber(element, KanbanWireKeys.From); + double? to = ReadNumber(element, KanbanWireKeys.To); + string cur = ReadText(element, KanbanWireKeys.Currency); return new CardBudgetDto(from, to, cur); } diff --git a/src/core/Deal.Modules.Pipeline/Application/Models/AiRawCardKeys.cs b/src/core/Deal.Modules.Pipeline/Application/Models/AiRawCardKeys.cs new file mode 100644 index 0000000..6bdd196 --- /dev/null +++ b/src/core/Deal.Modules.Pipeline/Application/Models/AiRawCardKeys.cs @@ -0,0 +1,107 @@ +namespace Deal.Modules.Pipeline.Application.Models; + +/// +/// Ключи JSON-схемы разбора карточки от ИИ +/// +public static class AiRawCardKeys +{ + /// + /// Заголовок + /// + public const string Title = "title"; + + /// + /// Колонка/доска + /// + public const string Board = "board"; + + /// + /// Компания + /// + public const string Company = "company"; + + /// + /// Формат работы + /// + public const string Format = "format"; + + /// + /// О задаче + /// + public const string Task = "task"; + + /// + /// Требования + /// + public const string Requirements = "requirements"; + + /// + /// Плюсы + /// + public const string Plus = "plus"; + + /// + /// Условия + /// + public const string Conditions = "conditions"; + + /// + /// Краткое описание + /// + public const string Summary = "summary"; + + /// + /// Стек + /// + public const string Stack = "stack"; + + /// + /// Бюджет + /// + public const string Budget = "budget"; + + /// + /// Контакты + /// + public const string Contacts = "contacts"; + + /// + /// Признак вакансии + /// + public const string IsVacancy = "is_vacancy"; + + /// + /// Признак определённого типа заявки + /// + public const string IsVacancyKnown = "is_vacancy_known"; + + /// + /// Признак спама + /// + public const string IsSpam = "is_spam"; + + /// + /// Значение контакта + /// + public const string Value = "value"; + + /// + /// Валюта бюджета + /// + public const string Currency = "currency"; + + /// + /// Короткий код валюты бюджета + /// + public const string CurrencyShort = "cur"; + + /// + /// Нижняя граница бюджета + /// + public const string From = "from"; + + /// + /// Верхняя граница бюджета + /// + public const string To = "to"; +} diff --git a/src/core/Deal.Modules.Pipeline/Application/Models/PipelineRejectConstants.cs b/src/core/Deal.Modules.Pipeline/Application/Models/PipelineRejectConstants.cs index f4fbddf..fb49820 100644 --- a/src/core/Deal.Modules.Pipeline/Application/Models/PipelineRejectConstants.cs +++ b/src/core/Deal.Modules.Pipeline/Application/Models/PipelineRejectConstants.cs @@ -16,20 +16,20 @@ public static class PipelineRejectConstants public static readonly IReadOnlyDictionary StageLabels = new Dictionary(StringComparer.Ordinal) { - ["length"] = "короткое сообщение", - ["stop"] = "стоп-фраза", - ["resume"] = "резюме соискателя", - ["type"] = "тип заявки", - ["budget"] = "нет суммы", - ["exclude_kw"] = "исключение: слова/технологии", - ["exclude_location"] = "исключение: локация/язык", - ["exclude_type"] = "исключение: тип", - ["exclude_budget"] = "исключение: бюджет", - ["stale"] = "устарело", - ["spam_ml"] = "спам (ML)", - ["spam_ai"] = "спам (ИИ)", - ["filter_ai"] = "ИИ-фильтр", - ["dup"] = "повтор", + [PipelineRejectStages.Length] = "короткое сообщение", + [PipelineRejectStages.Stop] = "стоп-фраза", + [PipelineRejectStages.Resume] = "резюме соискателя", + [PipelineRejectStages.Type] = "тип заявки", + [PipelineRejectStages.Budget] = "нет суммы", + [PipelineRejectStages.ExcludeKeywords] = "исключение: слова/технологии", + [PipelineRejectStages.ExcludeLocation] = "исключение: локация/язык", + [PipelineRejectStages.ExcludeType] = "исключение: тип", + [PipelineRejectStages.ExcludeBudget] = "исключение: бюджет", + [PipelineRejectStages.Stale] = "устарело", + [PipelineRejectStages.SpamMl] = "спам (ML)", + [PipelineRejectStages.SpamAi] = "спам (ИИ)", + [PipelineRejectStages.FilterAi] = "ИИ-фильтр", + [PipelineRejectStages.Duplicate] = "повтор", }; /// @@ -38,11 +38,11 @@ public static class PipelineRejectConstants public static readonly IReadOnlyDictionary SourceLabels = new Dictionary(StringComparer.Ordinal) { - ["stop"] = "правила", - ["ml"] = "ML", - ["ai"] = "ИИ", - ["stale"] = "система", - ["dup"] = "система", + [PipelineRejectSources.Rules] = "правила", + [PipelineRejectSources.Ml] = "ML", + [PipelineRejectSources.Ai] = "ИИ", + [PipelineRejectSources.System] = "система", + [PipelineRejectSources.Duplicate] = "система", }; /// diff --git a/src/core/Deal.Modules.Pipeline/Application/Models/PipelineRejectSources.cs b/src/core/Deal.Modules.Pipeline/Application/Models/PipelineRejectSources.cs new file mode 100644 index 0000000..43d373b --- /dev/null +++ b/src/core/Deal.Modules.Pipeline/Application/Models/PipelineRejectSources.cs @@ -0,0 +1,32 @@ +namespace Deal.Modules.Pipeline.Application.Models; + +/// +/// Источники решения об отсеве +/// +public static class PipelineRejectSources +{ + /// + /// Правила входящих (стоп-фразы, тип и т.п.) + /// + public const string Rules = "stop"; + + /// + /// Локальная ML-модель + /// + public const string Ml = "ml"; + + /// + /// ИИ-фильтр + /// + public const string Ai = "ai"; + + /// + /// Система (устаревание, дубликаты) + /// + public const string System = "system"; + + /// + /// Дубликат + /// + public const string Duplicate = "dup"; +} diff --git a/src/core/Deal.Modules.Pipeline/Application/Models/PipelineRejectStages.cs b/src/core/Deal.Modules.Pipeline/Application/Models/PipelineRejectStages.cs new file mode 100644 index 0000000..2a0a9d4 --- /dev/null +++ b/src/core/Deal.Modules.Pipeline/Application/Models/PipelineRejectStages.cs @@ -0,0 +1,77 @@ +namespace Deal.Modules.Pipeline.Application.Models; + +/// +/// Этапы отсева входящих сообщений +/// +public static class PipelineRejectStages +{ + /// + /// Слишком короткое сообщение + /// + public const string Length = "length"; + + /// + /// Стоп-фраза + /// + public const string Stop = "stop"; + + /// + /// Резюме соискателя + /// + public const string Resume = "resume"; + + /// + /// Неподходящий тип заявки + /// + public const string Type = "type"; + + /// + /// Нет суммы + /// + public const string Budget = "budget"; + + /// + /// Исключение по словам/технологиям + /// + public const string ExcludeKeywords = "exclude_kw"; + + /// + /// Исключение по локации/языку + /// + public const string ExcludeLocation = "exclude_location"; + + /// + /// Исключение по типу + /// + public const string ExcludeType = "exclude_type"; + + /// + /// Исключение по бюджету + /// + public const string ExcludeBudget = "exclude_budget"; + + /// + /// Устаревшее сообщение + /// + public const string Stale = "stale"; + + /// + /// Спам по ML + /// + public const string SpamMl = "spam_ml"; + + /// + /// Спам по ИИ + /// + public const string SpamAi = "spam_ai"; + + /// + /// Отсев ИИ-фильтром + /// + public const string FilterAi = "filter_ai"; + + /// + /// Дубликат + /// + public const string Duplicate = "dup"; +} diff --git a/src/core/Deal.Modules.Pipeline/Application/Parse/ContactsQualifier.cs b/src/core/Deal.Modules.Pipeline/Application/Parse/ContactsQualifier.cs index d496d08..15befbe 100644 --- a/src/core/Deal.Modules.Pipeline/Application/Parse/ContactsQualifier.cs +++ b/src/core/Deal.Modules.Pipeline/Application/Parse/ContactsQualifier.cs @@ -1,4 +1,5 @@ using System.Text.RegularExpressions; +using Deal.Contracts.Integrations.Models; using Deal.Modules.Kanban.Application.Models; namespace Deal.Modules.Pipeline.Application.Parse; @@ -65,12 +66,12 @@ public static class ContactsQualifier private static readonly IReadOnlyDictionary PrimaryOrder = new Dictionary(StringComparer.Ordinal) { - ["tg"] = 0, - ["phone"] = 1, - ["whatsapp"] = 2, - ["email"] = 3, - ["linkedin"] = 4, - ["site"] = 5, + [CardContactTypes.Telegram] = 0, + [CardContactTypes.Phone] = 1, + [CardContactTypes.WhatsApp] = 2, + [CardContactTypes.Email] = 3, + [CardContactTypes.LinkedIn] = 4, + [CardContactTypes.Site] = 5, }; private const int UnknownTypePriority = 9; @@ -93,7 +94,7 @@ public static class ContactsQualifier string name = s[1..].Trim(); if (ProfileHandleRe.IsMatch(name) && !name.EndsWith(BotSuffix, StringComparison.OrdinalIgnoreCase)) { - return new CardContactDto("tg", "@" + name); + return new CardContactDto(CardContactTypes.Telegram, "@" + name); } return null; @@ -106,7 +107,7 @@ public static class ContactsQualifier if (!ReservedProfileNames.Contains(name.ToLowerInvariant()) && !name.EndsWith(BotSuffix, StringComparison.OrdinalIgnoreCase)) { - return new CardContactDto("tg", "@" + name); + return new CardContactDto(CardContactTypes.Telegram, "@" + name); } return null; @@ -114,23 +115,23 @@ public static class ContactsQualifier if (EmailRe.IsMatch(s)) { - return new CardContactDto("email", s.ToLowerInvariant()); + return new CardContactDto(CardContactTypes.Email, s.ToLowerInvariant()); } string digits = new string(s.Where(char.IsDigit).ToArray()); if (PhoneRe.IsMatch(s) && digits.Length is >= 10 and <= 15) { - return new CardContactDto("phone", (s.StartsWith('+') ? "+" : string.Empty) + digits); + return new CardContactDto(CardContactTypes.Phone, (s.StartsWith('+') ? "+" : string.Empty) + digits); } if (LinkedinRe.IsMatch(s)) { - return new CardContactDto("linkedin", s); + return new CardContactDto(CardContactTypes.LinkedIn, s); } if (WhatsappRe.IsMatch(s)) { - return new CardContactDto("whatsapp", s); + return new CardContactDto(CardContactTypes.WhatsApp, s); } if (s.StartsWith("http", StringComparison.OrdinalIgnoreCase)) @@ -144,7 +145,7 @@ public static class ContactsQualifier return null; } - return new CardContactDto("site", s); + return new CardContactDto(CardContactTypes.Site, s); } return null; diff --git a/src/core/Deal.Modules.Pipeline/Application/Services/AiRawCardMapper.cs b/src/core/Deal.Modules.Pipeline/Application/Services/AiRawCardMapper.cs index ff1f111..9e93335 100644 --- a/src/core/Deal.Modules.Pipeline/Application/Services/AiRawCardMapper.cs +++ b/src/core/Deal.Modules.Pipeline/Application/Services/AiRawCardMapper.cs @@ -4,7 +4,9 @@ using System.Text.Json.Nodes; using Deal.Contracts.Integrations.Models; using Deal.Modules.Kanban.Application.Models; using Deal.Modules.Kanban.Application.Services; +using Deal.Modules.Pipeline.Application.Models; using Deal.Modules.Pipeline.Application.Parse; +using Deal.SharedKernel.Utilities; namespace Deal.Modules.Pipeline.Application.Services; @@ -19,9 +21,9 @@ public static class AiRawCardMapper private const string BudgetAmountPattern = @"[0-9.,]+\s*[кkКK]?"; - private const string CurrencyFieldName = "currency"; - - private const string CurrencyFieldNameShort = "cur"; + // Строковые значения bool-полей, которые считаются «ложью». + private static readonly IReadOnlySet FalsyAnswerValues = + new HashSet(StringComparer.Ordinal) { "0", BoolText.False, "no", "нет", "null", "none" }; // Регулярное выражение числа бюджета: число + необязательные «к»/валюта-суффиксы. private static readonly System.Text.RegularExpressions.Regex BudgetNumRe = @@ -39,7 +41,7 @@ public static class AiRawCardMapper JsonObject? root = JsonNode.Parse(json) as JsonObject ?? throw new JsonException("Ответ ИИ-классификатора — не JSON-объект (ok=true, но схема нарушена)."); - string title = MessageTextCleaner.CleanShort(ReadRawString(root, "title"), MaxTitleCodePoints); + string title = MessageTextCleaner.CleanShort(ReadRawString(root, AiRawCardKeys.Title), MaxTitleCodePoints); if (title.Length == 0) { title = MessageTextCleaner.CleanShort(text, MaxFallbackTextCodePoints); // python L455 fallback @@ -49,7 +51,7 @@ public static class AiRawCardMapper AiBudgetDto? budget = ReadBudget(root); IReadOnlyList contacts = ReadContacts(root, text); - string? board = ReadRawString(root, "board").Trim(); + string? board = ReadRawString(root, AiRawCardKeys.Board).Trim(); if (board.Length == 0) { board = null; @@ -57,19 +59,19 @@ public static class AiRawCardMapper return new AiParsedCardDto( Title: title, - Company: ReadNullableString(root, "company"), - Format: ReadNullableString(root, "format"), - Task: ReadNullableString(root, "task"), - Requirements: ReadItems(root, "requirements"), - Plus: ReadItems(root, "plus"), - Conditions: ReadNullableString(root, "conditions"), - Summary: ReadNullableString(root, "summary"), + Company: ReadNullableString(root, AiRawCardKeys.Company), + Format: ReadNullableString(root, AiRawCardKeys.Format), + Task: ReadNullableString(root, AiRawCardKeys.Task), + Requirements: ReadItems(root, AiRawCardKeys.Requirements), + Plus: ReadItems(root, AiRawCardKeys.Plus), + Conditions: ReadNullableString(root, AiRawCardKeys.Conditions), + Summary: ReadNullableString(root, AiRawCardKeys.Summary), Stack: stack, Budget: budget, Contacts: contacts, - IsVacancy: ReadBool(root, "is_vacancy"), - IsVacancyKnown: ReadBool(root, "is_vacancy_known"), // false — воркер стемпит true после успешного ИИ - IsSpam: ReadBool(root, "is_spam"), + IsVacancy: ReadBool(root, AiRawCardKeys.IsVacancy), + IsVacancyKnown: ReadBool(root, AiRawCardKeys.IsVacancyKnown), // false — воркер стемпит true после успешного ИИ + IsSpam: ReadBool(root, AiRawCardKeys.IsSpam), Board: board); } @@ -114,7 +116,7 @@ public static class AiRawCardMapper if (value.TryGetValue(out string? text)) { string lower = (text ?? string.Empty).Trim().ToLowerInvariant(); - return lower.Length > 0 && lower is not ("0" or "false" or "no" or "нет" or "null" or "none"); + return lower.Length > 0 && !FalsyAnswerValues.Contains(lower); } return false; @@ -147,7 +149,7 @@ public static class AiRawCardMapper private static IReadOnlyList ReadStack(JsonObject root) { - if (!root.TryGetPropertyValue("stack", out JsonNode? node) || node is null) + if (!root.TryGetPropertyValue(AiRawCardKeys.Stack, out JsonNode? node) || node is null) { return Array.Empty(); } @@ -161,19 +163,19 @@ public static class AiRawCardMapper private static AiBudgetDto? ReadBudget(JsonObject root) { - if (!root.TryGetPropertyValue("budget", out JsonNode? node) || node is not JsonObject budget) + if (!root.TryGetPropertyValue(AiRawCardKeys.Budget, out JsonNode? node) || node is not JsonObject budget) { return null; } - string currency = ReadRawString(budget, CurrencyFieldName); + string currency = ReadRawString(budget, AiRawCardKeys.Currency); if (currency.Length == 0) { - currency = ReadRawString(budget, CurrencyFieldNameShort); // ai.py L327: currency или cur + currency = ReadRawString(budget, AiRawCardKeys.CurrencyShort); // ai.py L327: currency или cur } - double? from = ParseBudgetBound(budget, "from"); - double? to = ParseBudgetBound(budget, "to"); + double? from = ParseBudgetBound(budget, AiRawCardKeys.From); + double? to = ParseBudgetBound(budget, AiRawCardKeys.To); CardBudgetDto? normalized = BudgetNormalizer.Normalize(new BudgetRangeDto(from, to, currency)); return normalized is null ? null @@ -226,7 +228,7 @@ public static class AiRawCardMapper private static IReadOnlyList ReadContacts(JsonObject root, string text) { List candidates = new(); - if (root.TryGetPropertyValue("contacts", out JsonNode? node) && node is not null) + if (root.TryGetPropertyValue(AiRawCardKeys.Contacts, out JsonNode? node) && node is not null) { if (node is JsonValue) { @@ -245,7 +247,7 @@ public static class AiRawCardMapper break; case JsonObject contactObject: { - string value = ReadRawString(contactObject, "value"); // python L402–403: dict.get("value") + string value = ReadRawString(contactObject, AiRawCardKeys.Value); if (value.Length > 0) { candidates.Add(value); diff --git a/src/core/Deal.Modules.Pipeline/Application/Services/MlReviewService.cs b/src/core/Deal.Modules.Pipeline/Application/Services/MlReviewService.cs index 14c2426..9ba38bf 100644 --- a/src/core/Deal.Modules.Pipeline/Application/Services/MlReviewService.cs +++ b/src/core/Deal.Modules.Pipeline/Application/Services/MlReviewService.cs @@ -220,7 +220,7 @@ public sealed class MlReviewService( { // TrashCardAsync(teach:true) сам шлёт обучающий сигнал «спам» — второй сигнал не нужен. CardDto? trashed = await cards.TrashCardAsync(card.Id, teach: true, ct); - return new MlApplyResult(null, Ok: true, Learned: true, Moved: "trash", LeadId: trashed?.Id ?? card.Id); + return new MlApplyResult(null, Ok: true, Learned: true, Moved: CardIds.Trash, LeadId: trashed?.Id ?? card.Id); } await mlClient.PushAsync(text, MlLearningLabels.Spam, UserPushWeight, ct); diff --git a/src/core/Deal.Modules.Pipeline/Application/Services/PipelineProcessingService.cs b/src/core/Deal.Modules.Pipeline/Application/Services/PipelineProcessingService.cs index 2a79770..5969fc2 100644 --- a/src/core/Deal.Modules.Pipeline/Application/Services/PipelineProcessingService.cs +++ b/src/core/Deal.Modules.Pipeline/Application/Services/PipelineProcessingService.cs @@ -47,16 +47,16 @@ public sealed class PipelineProcessingService( // Вес снятия метки «спам»: реальное действие пользователя «это не спам» (delta=−1.0). private const double SpamUnlearnDelta = -1.0; - private const string DuplicateSource = "dup"; + private const string DuplicateSource = PipelineRejectSources.Duplicate; // Имя сущности для текста ошибки «не найдено». private const string RejectedEntityName = "Запись отсева"; private static readonly HashSet SpamStages = new(StringComparer.Ordinal) { - "spam_ml", - "spam_ai", - "filter_ai", + PipelineRejectStages.SpamMl, + PipelineRejectStages.SpamAi, + PipelineRejectStages.FilterAi, }; diff --git a/src/core/Deal.Modules.Settings/Application/Models/SettingsFieldKeys.cs b/src/core/Deal.Modules.Settings/Application/Models/SettingsFieldKeys.cs new file mode 100644 index 0000000..5586f9f --- /dev/null +++ b/src/core/Deal.Modules.Settings/Application/Models/SettingsFieldKeys.cs @@ -0,0 +1,37 @@ +namespace Deal.Modules.Settings.Application.Models; + +/// +/// Ключи полей настроек провайдеров/промптов +/// +public static class SettingsFieldKeys +{ + /// + /// API-ключ провайдера + /// + public const string ApiKey = "apiKey"; + + /// + /// Базовый URL провайдера + /// + public const string BaseUrl = "baseUrl"; + + /// + /// Модель провайдера + /// + public const string Model = "model"; + + /// + /// Название пользовательского промпта + /// + public const string Name = "name"; + + /// + /// Текст пользовательского промпта + /// + public const string Prompt = "prompt"; + + /// + /// Описание пользовательского промпта + /// + public const string Description = "description"; +} diff --git a/src/core/Deal.Modules.Settings/Application/Services/SettingsService.PatchMyPrompts.cs b/src/core/Deal.Modules.Settings/Application/Services/SettingsService.PatchMyPrompts.cs index 06d379d..e695acf 100644 --- a/src/core/Deal.Modules.Settings/Application/Services/SettingsService.PatchMyPrompts.cs +++ b/src/core/Deal.Modules.Settings/Application/Services/SettingsService.PatchMyPrompts.cs @@ -26,14 +26,14 @@ public sealed partial class SettingsService continue; } - string name = ReadTrimmedField(item, "name", PromptNameMaxLength); - string prompt = ReadTrimmedField(item, "prompt", PromptTextMaxLength); + string name = ReadTrimmedField(item, SettingsFieldKeys.Name, PromptNameMaxLength); + string prompt = ReadTrimmedField(item, SettingsFieldKeys.Prompt, PromptTextMaxLength); if (name.Length == 0 || prompt.Length == 0) { continue; // пустые name/prompt — дроп } - string description = ReadTrimmedField(item, "description", PromptDescriptionMaxLength); + string description = ReadTrimmedField(item, SettingsFieldKeys.Description, PromptDescriptionMaxLength); string id = ReadPromptId(item); clean.Add(new MyPromptDto(id, name, description, prompt)); } diff --git a/src/core/Deal.Modules.Settings/Application/Services/SettingsService.PatchSecrets.cs b/src/core/Deal.Modules.Settings/Application/Services/SettingsService.PatchSecrets.cs index 1576558..975eb36 100644 --- a/src/core/Deal.Modules.Settings/Application/Services/SettingsService.PatchSecrets.cs +++ b/src/core/Deal.Modules.Settings/Application/Services/SettingsService.PatchSecrets.cs @@ -46,19 +46,19 @@ public sealed partial class SettingsService // каталогом — иначе тенант мог бы перенаправить ключ/запрос на произвольный внутренний адрес // (SSRF, Security review) и проверка подключения ушла бы на него. bool baseUrlMutable = meta is { Local: true } || providerId == CustomProviderId; - if (baseUrlMutable && TryReadFieldText(entry, "baseUrl", out string newBaseUrl)) + if (baseUrlMutable && TryReadFieldText(entry, SettingsFieldKeys.BaseUrl, out string newBaseUrl)) { baseUrl = newBaseUrl; } - if (TryReadFieldText(entry, "model", out string newModel)) + if (TryReadFieldText(entry, SettingsFieldKeys.Model, out string newModel)) { model = newModel; } // apiKey: непустой, ≥8 симв., без префикса enc: и без маски (содержит "…") → шифруется; // иначе (пустой/маска) ключ не меняется. - if (TryReadFieldText(entry, "apiKey", out string newKey) + if (TryReadFieldText(entry, SettingsFieldKeys.ApiKey, out string newKey) && newKey.Length >= ApiKeyMinLength && !newKey.StartsWith(EncryptedValuePrefix, StringComparison.Ordinal) && !newKey.Contains(MaskEllipsis, StringComparison.Ordinal)) diff --git a/src/core/Deal.Modules.Settings/Application/Services/SettingsService.ReadMerge.cs b/src/core/Deal.Modules.Settings/Application/Services/SettingsService.ReadMerge.cs index f2865d3..76f5e0f 100644 --- a/src/core/Deal.Modules.Settings/Application/Services/SettingsService.ReadMerge.cs +++ b/src/core/Deal.Modules.Settings/Application/Services/SettingsService.ReadMerge.cs @@ -1,6 +1,7 @@ using System.Globalization; using System.Text.Json; using Deal.Modules.Settings.Application.Models; +using Deal.SharedKernel.Utilities; namespace Deal.Modules.Settings.Application.Services; @@ -82,11 +83,11 @@ public sealed partial class SettingsService return true; case JsonValueKind.True: - text = "true"; + text = BoolText.True; return true; case JsonValueKind.False: - text = "false"; + text = BoolText.False; return true; default: @@ -218,9 +219,9 @@ public sealed partial class SettingsService continue; } - string apiKey = TryReadFieldText(provider.Value, "apiKey", out string storedKey) ? storedKey : string.Empty; - string baseUrl = TryReadFieldText(provider.Value, "baseUrl", out string storedBase) ? storedBase : string.Empty; - string model = TryReadFieldText(provider.Value, "model", out string storedModel) ? storedModel : string.Empty; + string apiKey = TryReadFieldText(provider.Value, SettingsFieldKeys.ApiKey, out string storedKey) ? storedKey : string.Empty; + string baseUrl = TryReadFieldText(provider.Value, SettingsFieldKeys.BaseUrl, out string storedBase) ? storedBase : string.Empty; + string model = TryReadFieldText(provider.Value, SettingsFieldKeys.Model, out string storedModel) ? storedModel : string.Empty; merged[provider.Name] = new AiConfigSetting(apiKey, baseUrl, model); } } diff --git a/src/core/Deal.Modules.Tenants/Application/Services/TenantAdminService.cs b/src/core/Deal.Modules.Tenants/Application/Services/TenantAdminService.cs index c6965b1..dd0fb19 100644 --- a/src/core/Deal.Modules.Tenants/Application/Services/TenantAdminService.cs +++ b/src/core/Deal.Modules.Tenants/Application/Services/TenantAdminService.cs @@ -73,7 +73,7 @@ public sealed class TenantAdminService( Id: ownerUserId, Login: normalizedEmail, TenantId: tenantId, - Status: "active", + Status: TenantStatuses.Active, PasswordHash: passwordHasher.Hash(initialPassword)), ct); diff --git a/src/core/Deal.SharedKernel/Observability/DealMetrics.cs b/src/core/Deal.SharedKernel/Observability/DealMetrics.cs index 137dabe..0d2290d 100644 --- a/src/core/Deal.SharedKernel/Observability/DealMetrics.cs +++ b/src/core/Deal.SharedKernel/Observability/DealMetrics.cs @@ -54,6 +54,51 @@ public static class DealMetrics /// public const string AiBudgetUsedRatioName = "deal.ai.budget.used.ratio"; + /// + /// Имя метрики успешных вызовов платного ИИ + /// + public const string AiCallsName = "deal.ai.calls"; + + /// + /// Имя метрики токенов платного ИИ + /// + public const string AiTokensName = "deal.ai.tokens"; + + /// + /// Имя метрики вызовов локального ML + /// + public const string MlCallsName = "deal.ml.calls"; + + /// + /// Имя метрики оценки токенов локального ML + /// + public const string MlTokensName = "deal.ml.tokens"; + + /// + /// Имя метрики событий аудита + /// + public const string AuditEventsName = "deal.audit.events"; + + /// + /// Имя метрики событий подозрительной активности + /// + public const string SecurityEventsName = "deal.security.suspicious"; + + /// + /// Имя метрики глубины очереди пайплайна + /// + public const string PipelineQueueDepthName = "deal.pipeline.queue.depth"; + + /// + /// Имя метрики глубины очереди обучения ML + /// + public const string MlOutboxDepthName = "deal.ml.outbox.depth"; + + /// + /// Имя метрики активных сессий + /// + public const string ActiveSessionsName = "deal.sessions.active"; + // Meter прикладных метрик (static — один на процесс, как того требует System.Diagnostics.Metrics). private static readonly Meter Meter = new(MeterName); @@ -61,44 +106,44 @@ public static class DealMetrics /// Успешные вызовы платного ИИ /// public static readonly Counter AiCalls = - Meter.CreateCounter("deal.ai.calls", description: "Успешные вызовы платного ИИ (ai-service)."); + Meter.CreateCounter(AiCallsName, description: "Успешные вызовы платного ИИ (ai-service)."); /// /// Токены платного ИИ по видам /// public static readonly Counter AiTokens = - Meter.CreateCounter("deal.ai.tokens", description: "Токены платных ИИ-вызовов (prompt/completion)."); + Meter.CreateCounter(AiTokensName, description: "Токены платных ИИ-вызовов (prompt/completion)."); /// /// Вызовы локального ML /// public static readonly Counter MlCalls = - Meter.CreateCounter("deal.ml.calls", description: "Вызовы локального ML-предсказания."); + Meter.CreateCounter(MlCallsName, description: "Вызовы локального ML-предсказания."); /// /// Оценка токенов локальных ML-вызовов /// public static readonly Counter MlTokens = - Meter.CreateCounter("deal.ml.tokens", description: "Оценка токенов локальных ML-вызовов."); + Meter.CreateCounter(MlTokensName, description: "Оценка токенов локальных ML-вызовов."); /// /// События аудита по типам и акторам /// public static readonly Counter AuditEvents = - Meter.CreateCounter("deal.audit.events", description: "Записи аудита по типам и акторам."); + Meter.CreateCounter(AuditEventsName, description: "Записи аудита по типам и акторам."); /// /// События подозрительной активности по видам /// public static readonly Counter SecurityEvents = - Meter.CreateCounter("deal.security.suspicious", description: "События подозрительной активности по видам (rate_limit, login_blocked)."); + Meter.CreateCounter(SecurityEventsName, description: "События подозрительной активности по видам (rate_limit, login_blocked)."); /// /// Суммарная глубина очереди пайплайна /// public static readonly ObservableGauge PipelineQueueDepth = Meter.CreateObservableGauge( - "deal.pipeline.queue.depth", + PipelineQueueDepthName, (Func)(() => (long)Interlocked.Read(ref DealMetrics._pipelineQueueDepth)), description: "Суммарная глубина очереди пайплайна (new+filtered) по всем тенантам."); @@ -107,7 +152,7 @@ public static class DealMetrics /// public static readonly ObservableGauge MlOutboxDepth = Meter.CreateObservableGauge( - "deal.ml.outbox.depth", + MlOutboxDepthName, (Func)(() => (long)Interlocked.Read(ref DealMetrics._mlOutboxDepth)), description: "Суммарная глубина очереди обучения ML (MlOutbox) по всем тенантам."); @@ -116,7 +161,7 @@ public static class DealMetrics /// public static readonly ObservableGauge ActiveSessions = Meter.CreateObservableGauge( - "deal.sessions.active", + ActiveSessionsName, (Func)(() => (long)Interlocked.Read(ref DealMetrics._activeSessions)), description: "Активные непросроченные сессии пользователей и операторов."); diff --git a/src/core/Deal.SharedKernel/Utilities/BoolText.cs b/src/core/Deal.SharedKernel/Utilities/BoolText.cs new file mode 100644 index 0000000..17e2f64 --- /dev/null +++ b/src/core/Deal.SharedKernel/Utilities/BoolText.cs @@ -0,0 +1,17 @@ +namespace Deal.SharedKernel.Utilities; + +/// +/// Строковые представления булевых значений +/// +public static class BoolText +{ + /// + /// Истина + /// + public const string True = "true"; + + /// + /// Ложь + /// + public const string False = "false"; +}