Вынести магические строки статусов, видов и ключей в каталоги
Литералы статусов/видов/фаз и wire-ключей заменены каталогами: значения сущностей и репозиториев — TenantStatuses/TenantLimitPeriods/InviteStatuses/ContainerKinds/ContainerSpaces/CardIds/Discovery*/PipelineQueueStatuses; контакты и виды вложений — CardContactTypes/CardFileKinds; источники и этапы отсева — PipelineRejectStages/PipelineRejectSources; ключи промптов/провайдеров — SettingsFieldKeys; wire-ключи канбана — KanbanWireKeys; режимы правил — ColumnRuleModes; операции/фазы/виды диалогов/контакты Telegram и общий BoolText — в Deal.Contracts/Deal.SharedKernel; имена метрик — DealMetrics. Значения не менялись.
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -105,15 +105,15 @@ public static class CardsEndpoints
|
||||
CardsService cardsService = context.RequestServices.GetRequiredService<CardsService>();
|
||||
CardCountsDto counts = await cardsService.CountsAsync(ct);
|
||||
|
||||
var wire = new Dictionary<string, object> { ["new"] = counts.New };
|
||||
var wire = new Dictionary<string, object> { [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);
|
||||
}
|
||||
|
||||
|
||||
@@ -288,12 +288,12 @@ public static class ContainersEndpoints
|
||||
var wire = new Dictionary<string, object>();
|
||||
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;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -12,6 +12,10 @@ public sealed class TelegramBackfillScheduler(
|
||||
IHostApplicationLifetime applicationLifetime,
|
||||
ILogger<TelegramBackfillScheduler> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -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).
|
||||
|
||||
@@ -21,6 +21,11 @@ public sealed class SourceIngressGrpcService(
|
||||
IngressTenantResolver tenants,
|
||||
ILogger<SourceIngressGrpcService> logger) : SourceIngressService.SourceIngressServiceBase
|
||||
{
|
||||
// Исходы приёма источника для лога аудита.
|
||||
private const string DuplicateOutcome = "duplicate";
|
||||
private const string NoOpOutcome = "no-op";
|
||||
private const string QueuedOutcome = "queued";
|
||||
|
||||
/// <summary>
|
||||
/// PushSource — запись источника в очередь пайплайна тенанта.
|
||||
/// </summary>
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
namespace Deal.Contracts.Integrations.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Типы контактов заказчика
|
||||
/// </summary>
|
||||
public static class CardContactTypes
|
||||
{
|
||||
/// <summary>
|
||||
/// Telegram (@username)
|
||||
/// </summary>
|
||||
public const string Telegram = "tg";
|
||||
|
||||
/// <summary>
|
||||
/// Телефон
|
||||
/// </summary>
|
||||
public const string Phone = "phone";
|
||||
|
||||
/// <summary>
|
||||
/// Email
|
||||
/// </summary>
|
||||
public const string Email = "email";
|
||||
|
||||
/// <summary>
|
||||
/// WhatsApp
|
||||
/// </summary>
|
||||
public const string WhatsApp = "whatsapp";
|
||||
|
||||
/// <summary>
|
||||
/// LinkedIn
|
||||
/// </summary>
|
||||
public const string LinkedIn = "linkedin";
|
||||
|
||||
/// <summary>
|
||||
/// Сайт
|
||||
/// </summary>
|
||||
public const string Site = "site";
|
||||
|
||||
/// <summary>
|
||||
/// Прочее (не опознано)
|
||||
/// </summary>
|
||||
public const string Other = "other";
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
namespace Deal.Contracts.Integrations.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Фазы входа в аккаунт Telegram
|
||||
/// </summary>
|
||||
public static class TelegramAuthPhases
|
||||
{
|
||||
/// <summary>
|
||||
/// Аккаунт не подключён
|
||||
/// </summary>
|
||||
public const string Idle = "idle";
|
||||
|
||||
/// <summary>
|
||||
/// Ожидание номера телефона
|
||||
/// </summary>
|
||||
public const string Phone = "phone";
|
||||
|
||||
/// <summary>
|
||||
/// Ожидание кода
|
||||
/// </summary>
|
||||
public const string Code = "code";
|
||||
|
||||
/// <summary>
|
||||
/// Ожидание облачного пароля
|
||||
/// </summary>
|
||||
public const string Password = "password";
|
||||
|
||||
/// <summary>
|
||||
/// Ожидание сканирования QR
|
||||
/// </summary>
|
||||
public const string Qr = "qr";
|
||||
|
||||
/// <summary>
|
||||
/// Аккаунт авторизован
|
||||
/// </summary>
|
||||
public const string Ready = "ready";
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
namespace Deal.Contracts.Integrations.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Виды диалогов Telegram
|
||||
/// </summary>
|
||||
public static class TelegramDialogKinds
|
||||
{
|
||||
/// <summary>
|
||||
/// Канал
|
||||
/// </summary>
|
||||
public const string Channel = "channel";
|
||||
|
||||
/// <summary>
|
||||
/// Группа
|
||||
/// </summary>
|
||||
public const string Group = "group";
|
||||
|
||||
/// <summary>
|
||||
/// Форум (темы внутри группы)
|
||||
/// </summary>
|
||||
public const string Forum = "forum";
|
||||
|
||||
/// <summary>
|
||||
/// Личный чат
|
||||
/// </summary>
|
||||
public const string Chat = "chat";
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
namespace Deal.Contracts.Integrations.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Имена операций Telegram-гейта для логов и диагностики
|
||||
/// </summary>
|
||||
public static class TelegramOperations
|
||||
{
|
||||
/// <summary>
|
||||
/// Статус аккаунта
|
||||
/// </summary>
|
||||
public const string Status = "status";
|
||||
|
||||
/// <summary>
|
||||
/// Вход по номеру телефона
|
||||
/// </summary>
|
||||
public const string StartPhone = "start_phone";
|
||||
|
||||
/// <summary>
|
||||
/// Вход по QR
|
||||
/// </summary>
|
||||
public const string StartQr = "start_qr";
|
||||
|
||||
/// <summary>
|
||||
/// Подтверждение кода
|
||||
/// </summary>
|
||||
public const string SendCode = "send_code";
|
||||
|
||||
/// <summary>
|
||||
/// Подтверждение облачного пароля
|
||||
/// </summary>
|
||||
public const string SendPassword = "send_password";
|
||||
|
||||
/// <summary>
|
||||
/// Выход из аккаунта
|
||||
/// </summary>
|
||||
public const string Logout = "logout";
|
||||
|
||||
/// <summary>
|
||||
/// Обновление каталога диалогов
|
||||
/// </summary>
|
||||
public const string RefreshDialogs = "refresh_dialogs";
|
||||
|
||||
/// <summary>
|
||||
/// Включение/выключение мониторинга диалога
|
||||
/// </summary>
|
||||
public const string SetMonitor = "set_monitor";
|
||||
|
||||
/// <summary>
|
||||
/// Включение/выключение мониторинга всех диалогов
|
||||
/// </summary>
|
||||
public const string SetMonitorAll = "set_monitor_all";
|
||||
|
||||
/// <summary>
|
||||
/// Перечитать последние сообщения
|
||||
/// </summary>
|
||||
public const string Backfill = "backfill";
|
||||
|
||||
/// <summary>
|
||||
/// Чтение последних сообщений
|
||||
/// </summary>
|
||||
public const string ReadRecent = "read_recent";
|
||||
|
||||
/// <summary>
|
||||
/// Чтение исходного сообщения
|
||||
/// </summary>
|
||||
public const string ReadSource = "read_source";
|
||||
|
||||
/// <summary>
|
||||
/// Поиск источников
|
||||
/// </summary>
|
||||
public const string Search = "search";
|
||||
|
||||
/// <summary>
|
||||
/// Информация об источнике
|
||||
/// </summary>
|
||||
public const string GetInfo = "get_info";
|
||||
|
||||
/// <summary>
|
||||
/// Чтение сообщений для оценки
|
||||
/// </summary>
|
||||
public const string ReadForEval = "read_for_eval";
|
||||
|
||||
/// <summary>
|
||||
/// Вступление в источник
|
||||
/// </summary>
|
||||
public const string Join = "join";
|
||||
|
||||
/// <summary>
|
||||
/// Выход из источника
|
||||
/// </summary>
|
||||
public const string Leave = "leave";
|
||||
|
||||
/// <summary>
|
||||
/// Информация об источнике (Discovery)
|
||||
/// </summary>
|
||||
public const string DiscoveryInfo = "discovery_info";
|
||||
|
||||
/// <summary>
|
||||
/// Чтение сообщений источника (Discovery)
|
||||
/// </summary>
|
||||
public const string DiscoveryRead = "discovery_read";
|
||||
}
|
||||
@@ -6,6 +6,9 @@ namespace Deal.Infrastructure.Integrations.Extensions;
|
||||
// Расширения Uri для SSRF-гейта интеграций
|
||||
internal static class UriExtensions
|
||||
{
|
||||
// Имя loopback-хоста.
|
||||
private const string LocalhostHost = "localhost";
|
||||
|
||||
/// <summary>
|
||||
/// Проверяет, указывает ли URL на приватный/loopback/link-local адрес
|
||||
/// </summary>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
using Deal.SharedKernel.Utilities;
|
||||
|
||||
namespace Deal.Infrastructure.Integrations.Options;
|
||||
|
||||
/// <summary>
|
||||
@@ -86,13 +88,16 @@ public sealed class MtlsOptions
|
||||
};
|
||||
}
|
||||
|
||||
// Строковое представление включённого флага числом.
|
||||
private const string OneLiteral = "1";
|
||||
|
||||
/// <summary>
|
||||
/// Разбирает значение флага DEAL_MTLS_ENABLED
|
||||
/// </summary>
|
||||
/// <param name="rawValue">Сырое значение env (null/пусто — выключено).</param>
|
||||
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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,8 +39,6 @@ ContainersService containersService) : IColumnSuggester
|
||||
|
||||
private const string KeywordsEmptyReason = "ИИ не смог выделить ключи — попробуйте ещё раз";
|
||||
|
||||
private const string RulesModeAny = "any";
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<SuggestColumnsResultDto> 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<string>(),
|
||||
Keywords: [plan.Word],
|
||||
Stack: Array.Empty<string>(),
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
using Deal.SharedKernel.Utilities;
|
||||
|
||||
namespace Deal.Infrastructure.Integrations.Storage.Extensions;
|
||||
|
||||
// Расширения string для разбора конфигурационных значений.
|
||||
internal static class StringExtensions
|
||||
{
|
||||
// Строковое представление включённого флага числом.
|
||||
private const string OneLiteral = "1";
|
||||
|
||||
/// <summary>
|
||||
/// Разбирает строковое значение как булев флаг конфигурации
|
||||
/// </summary>
|
||||
@@ -10,6 +15,6 @@ internal static class StringExtensions
|
||||
/// <returns>True — значение распознано как включённое.</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
/// <summary>
|
||||
/// Предыдущая колонка
|
||||
/// </summary>
|
||||
public string PrevCol { get; set; } = "inbox";
|
||||
public string PrevCol { get; set; } = CardIds.Inbox;
|
||||
|
||||
/// <summary>
|
||||
/// Время помещения в архив
|
||||
|
||||
@@ -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
|
||||
/// <summary>
|
||||
/// Вид контейнера: board
|
||||
/// </summary>
|
||||
public string Kind { get; set; } = "board";
|
||||
public string Kind { get; set; } = ContainerKinds.Board;
|
||||
|
||||
/// <summary>
|
||||
/// Пространство: dashboard | selected
|
||||
/// </summary>
|
||||
public string Space { get; set; } = "dashboard";
|
||||
public string Space { get; set; } = ContainerSpaces.Dashboard;
|
||||
|
||||
/// <summary>
|
||||
/// Свёрнутость колонки на дашборде
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Deal.Modules.Discovery.Application.Models;
|
||||
|
||||
namespace Deal.Infrastructure.Persistence.Entities;
|
||||
|
||||
/// <summary>
|
||||
@@ -28,7 +30,7 @@ public sealed class DiscCandidateEntity
|
||||
/// <summary>
|
||||
/// Тип источника: channel|group|forum.
|
||||
/// </summary>
|
||||
public string Kind { get; set; } = "channel";
|
||||
public string Kind { get; set; } = DiscoveryCandidateKinds.Channel;
|
||||
|
||||
/// <summary>
|
||||
/// Цвет источника из палитры DIALOG_HUES
|
||||
@@ -63,7 +65,7 @@ public sealed class DiscCandidateEntity
|
||||
/// <summary>
|
||||
/// Статус кандидата
|
||||
/// </summary>
|
||||
public string Status { get; set; } = "new";
|
||||
public string Status { get; set; } = DiscoveryCandidateStatuses.New;
|
||||
|
||||
/// <summary>
|
||||
/// Вступили автоматически
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Deal.Modules.Discovery.Application.Models;
|
||||
|
||||
namespace Deal.Infrastructure.Persistence.Entities;
|
||||
|
||||
/// <summary>
|
||||
@@ -55,7 +57,7 @@ public sealed class DiscTaskEntity
|
||||
/// <summary>
|
||||
/// Статус задачи: draft|running|paused|done|failed.
|
||||
/// </summary>
|
||||
public string Status { get; set; } = "draft";
|
||||
public string Status { get; set; } = DiscoveryTaskStatuses.Draft;
|
||||
|
||||
/// <summary>
|
||||
/// Индекс текущего ключа поиска
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Deal.Modules.Tenants.Application.Models;
|
||||
|
||||
namespace Deal.Infrastructure.Persistence.Entities;
|
||||
|
||||
/// <summary>
|
||||
@@ -20,7 +22,7 @@ public sealed class InviteEntity
|
||||
/// </summary>
|
||||
public Guid? TenantId { get; set; }
|
||||
|
||||
public string Status { get; set; } = "pending";
|
||||
public string Status { get; set; } = InviteStatuses.Pending;
|
||||
|
||||
public DateTimeOffset ExpiresAt { get; set; }
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Deal.Modules.Tenants.Application.Models;
|
||||
|
||||
namespace Deal.Infrastructure.Persistence.Entities;
|
||||
|
||||
/// <summary>
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Deal.Modules.Pipeline.Application.Models;
|
||||
|
||||
namespace Deal.Infrastructure.Persistence.Entities;
|
||||
|
||||
/// <summary>
|
||||
@@ -38,7 +40,7 @@ public sealed class QueueItemEntity
|
||||
/// <summary>
|
||||
/// Статус строки: <c>new</c> — ждёт разбора воркером, <c>filtered</c> — прошла фильтры и ждёт ИИ/ML.
|
||||
/// </summary>
|
||||
public string Status { get; set; } = "new";
|
||||
public string Status { get; set; } = PipelineQueueStatuses.New;
|
||||
|
||||
/// <summary>
|
||||
/// Признак возврата из отсева
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using NpgsqlTypes;
|
||||
|
||||
using Deal.Modules.Pipeline.Application.Models;
|
||||
|
||||
namespace Deal.Infrastructure.Persistence.Entities;
|
||||
|
||||
/// <summary>
|
||||
@@ -50,7 +52,7 @@ public sealed class RejectedItemEntity
|
||||
/// <summary>
|
||||
/// Кто вынес решение
|
||||
/// </summary>
|
||||
public string Source { get; set; } = "stop";
|
||||
public string Source { get; set; } = PipelineRejectSources.Rules;
|
||||
|
||||
/// <summary>
|
||||
/// Время получения исходного сообщения
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Deal.Modules.Tenants.Application.Models;
|
||||
|
||||
namespace Deal.Infrastructure.Persistence.Entities;
|
||||
|
||||
/// <summary>
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Deal.Modules.Tenants.Application.Models;
|
||||
|
||||
namespace Deal.Infrastructure.Persistence.Entities;
|
||||
|
||||
/// <summary>
|
||||
@@ -18,7 +20,7 @@ public sealed class TenantLimitEntity
|
||||
/// <summary>
|
||||
/// Тип периода: month|day.
|
||||
/// </summary>
|
||||
public string Period { get; set; } = "month";
|
||||
public string Period { get; set; } = TenantLimitPeriods.Month;
|
||||
|
||||
/// <summary>
|
||||
/// Начало текущего периода
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Deal.Modules.Tenants.Application.Models;
|
||||
|
||||
namespace Deal.Infrastructure.Persistence.Entities;
|
||||
|
||||
/// <summary>
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<int> IDiscoveryStore.SumActivePlanAsync(string? excludeTaskId, CancellationToken ct)
|
||||
{
|
||||
IQueryable<DiscTaskEntity> 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);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Deal.Contracts.Integrations.Models;
|
||||
|
||||
namespace Deal.Modules.Cards.Application.Models;
|
||||
|
||||
/// <summary>
|
||||
@@ -8,7 +10,7 @@ public sealed record CardContact
|
||||
/// <summary>
|
||||
/// Тип контакта: tg/phone/email/linkedin/whatsapp/site/other.
|
||||
/// </summary>
|
||||
public string Type { get; init; } = "other";
|
||||
public string Type { get; init; } = CardContactTypes.Other;
|
||||
|
||||
/// <summary>
|
||||
/// Значение: @username, +7…, name@mail, url.
|
||||
|
||||
@@ -23,7 +23,7 @@ public sealed record CardFile
|
||||
/// <summary>
|
||||
/// Тип контента: image/video/audio/archive/document/other
|
||||
/// </summary>
|
||||
public string Kind { get; init; } = "other";
|
||||
public string Kind { get; init; } = CardFileKinds.Other;
|
||||
|
||||
public string ObjectKey { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
namespace Deal.Modules.Cards.Application.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Категории вложений карточки
|
||||
/// </summary>
|
||||
public static class CardFileKinds
|
||||
{
|
||||
/// <summary>
|
||||
/// Изображение
|
||||
/// </summary>
|
||||
public const string Image = "image";
|
||||
|
||||
/// <summary>
|
||||
/// Видео
|
||||
/// </summary>
|
||||
public const string Video = "video";
|
||||
|
||||
/// <summary>
|
||||
/// Аудио
|
||||
/// </summary>
|
||||
public const string Audio = "audio";
|
||||
|
||||
/// <summary>
|
||||
/// Документ
|
||||
/// </summary>
|
||||
public const string Document = "document";
|
||||
|
||||
/// <summary>
|
||||
/// Архив
|
||||
/// </summary>
|
||||
public const string Archive = "archive";
|
||||
|
||||
/// <summary>
|
||||
/// Прочее (не опознано)
|
||||
/// </summary>
|
||||
public const string Other = "other";
|
||||
}
|
||||
@@ -19,4 +19,24 @@ public static class DiscoveryCandidateKinds
|
||||
/// Форум (группа с темами; topics заполняются оценкой).
|
||||
/// </summary>
|
||||
public const string Forum = "forum";
|
||||
|
||||
/// <summary>
|
||||
/// Русское написание «канал» (как приходит из Telegram)
|
||||
/// </summary>
|
||||
public const string ChannelAliasRu = "канал";
|
||||
|
||||
/// <summary>
|
||||
/// Русское написание «форум»
|
||||
/// </summary>
|
||||
public const string ForumAliasRu = "форум";
|
||||
|
||||
/// <summary>
|
||||
/// Английское написание «chat»
|
||||
/// </summary>
|
||||
public const string ChatAlias = "chat";
|
||||
|
||||
/// <summary>
|
||||
/// Русское написание «чат»
|
||||
/// </summary>
|
||||
public const string ChatAliasRu = "чат";
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ public sealed record DiscoveryCandidateRow
|
||||
/// <summary>
|
||||
/// Тип источника: channel|group|forum.
|
||||
/// </summary>
|
||||
public string Kind { get; init; } = "channel";
|
||||
public string Kind { get; init; } = DiscoveryCandidateKinds.Channel;
|
||||
|
||||
/// <summary>
|
||||
/// Цвет источника
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace Deal.Modules.Discovery.Application.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Источники решения о соответствии сообщения
|
||||
/// </summary>
|
||||
public static class DiscoveryFitOrigins
|
||||
{
|
||||
/// <summary>
|
||||
/// Эвристика по ключевым словам
|
||||
/// </summary>
|
||||
public const string Heuristic = "heuristic";
|
||||
|
||||
/// <summary>
|
||||
/// Локальная ML-модель
|
||||
/// </summary>
|
||||
public const string Ml = "ml";
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace Deal.Modules.Discovery.Application.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Языки источников Discovery
|
||||
/// </summary>
|
||||
public static class DiscoveryLanguages
|
||||
{
|
||||
/// <summary>
|
||||
/// Русскоязычные источники
|
||||
/// </summary>
|
||||
public const string Russian = "ru";
|
||||
|
||||
/// <summary>
|
||||
/// Любой язык
|
||||
/// </summary>
|
||||
public const string Any = "any";
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -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
|
||||
|
||||
+10
-6
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
+1
-1
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace Deal.Modules.Kanban.Application.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Режимы сопоставления правил колонки
|
||||
/// </summary>
|
||||
public static class ColumnRuleModes
|
||||
{
|
||||
/// <summary>
|
||||
/// Достаточно любого условия
|
||||
/// </summary>
|
||||
public const string Any = "any";
|
||||
|
||||
/// <summary>
|
||||
/// Нужны все условия
|
||||
/// </summary>
|
||||
public const string All = "all";
|
||||
}
|
||||
@@ -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 = "");
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
namespace Deal.Modules.Kanban.Application.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Ключи wire-контракта канбана (ответы API и патчи карточки)
|
||||
/// </summary>
|
||||
public static class KanbanWireKeys
|
||||
{
|
||||
/// <summary>
|
||||
/// Счётчик новых карточек
|
||||
/// </summary>
|
||||
public const string New = "new";
|
||||
|
||||
/// <summary>
|
||||
/// Счётчик в обучении ML
|
||||
/// </summary>
|
||||
public const string Learning = "learning";
|
||||
|
||||
/// <summary>
|
||||
/// Счётчик решённых ML
|
||||
/// </summary>
|
||||
public const string Ml = "ml";
|
||||
|
||||
/// <summary>
|
||||
/// Счётчик решённых ИИ
|
||||
/// </summary>
|
||||
public const string Ai = "ai";
|
||||
|
||||
/// <summary>
|
||||
/// Признак свёрнутой колонки
|
||||
/// </summary>
|
||||
public const string Collapsed = "collapsed";
|
||||
|
||||
/// <summary>
|
||||
/// Ширина колонки
|
||||
/// </summary>
|
||||
public const string Width = "width";
|
||||
|
||||
/// <summary>
|
||||
/// Нижняя граница бюджета
|
||||
/// </summary>
|
||||
public const string From = "from";
|
||||
|
||||
/// <summary>
|
||||
/// Верхняя граница бюджета
|
||||
/// </summary>
|
||||
public const string To = "to";
|
||||
|
||||
/// <summary>
|
||||
/// Валюта бюджета
|
||||
/// </summary>
|
||||
public const string Currency = "cur";
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
namespace Deal.Modules.Pipeline.Application.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Ключи JSON-схемы разбора карточки от ИИ
|
||||
/// </summary>
|
||||
public static class AiRawCardKeys
|
||||
{
|
||||
/// <summary>
|
||||
/// Заголовок
|
||||
/// </summary>
|
||||
public const string Title = "title";
|
||||
|
||||
/// <summary>
|
||||
/// Колонка/доска
|
||||
/// </summary>
|
||||
public const string Board = "board";
|
||||
|
||||
/// <summary>
|
||||
/// Компания
|
||||
/// </summary>
|
||||
public const string Company = "company";
|
||||
|
||||
/// <summary>
|
||||
/// Формат работы
|
||||
/// </summary>
|
||||
public const string Format = "format";
|
||||
|
||||
/// <summary>
|
||||
/// О задаче
|
||||
/// </summary>
|
||||
public const string Task = "task";
|
||||
|
||||
/// <summary>
|
||||
/// Требования
|
||||
/// </summary>
|
||||
public const string Requirements = "requirements";
|
||||
|
||||
/// <summary>
|
||||
/// Плюсы
|
||||
/// </summary>
|
||||
public const string Plus = "plus";
|
||||
|
||||
/// <summary>
|
||||
/// Условия
|
||||
/// </summary>
|
||||
public const string Conditions = "conditions";
|
||||
|
||||
/// <summary>
|
||||
/// Краткое описание
|
||||
/// </summary>
|
||||
public const string Summary = "summary";
|
||||
|
||||
/// <summary>
|
||||
/// Стек
|
||||
/// </summary>
|
||||
public const string Stack = "stack";
|
||||
|
||||
/// <summary>
|
||||
/// Бюджет
|
||||
/// </summary>
|
||||
public const string Budget = "budget";
|
||||
|
||||
/// <summary>
|
||||
/// Контакты
|
||||
/// </summary>
|
||||
public const string Contacts = "contacts";
|
||||
|
||||
/// <summary>
|
||||
/// Признак вакансии
|
||||
/// </summary>
|
||||
public const string IsVacancy = "is_vacancy";
|
||||
|
||||
/// <summary>
|
||||
/// Признак определённого типа заявки
|
||||
/// </summary>
|
||||
public const string IsVacancyKnown = "is_vacancy_known";
|
||||
|
||||
/// <summary>
|
||||
/// Признак спама
|
||||
/// </summary>
|
||||
public const string IsSpam = "is_spam";
|
||||
|
||||
/// <summary>
|
||||
/// Значение контакта
|
||||
/// </summary>
|
||||
public const string Value = "value";
|
||||
|
||||
/// <summary>
|
||||
/// Валюта бюджета
|
||||
/// </summary>
|
||||
public const string Currency = "currency";
|
||||
|
||||
/// <summary>
|
||||
/// Короткий код валюты бюджета
|
||||
/// </summary>
|
||||
public const string CurrencyShort = "cur";
|
||||
|
||||
/// <summary>
|
||||
/// Нижняя граница бюджета
|
||||
/// </summary>
|
||||
public const string From = "from";
|
||||
|
||||
/// <summary>
|
||||
/// Верхняя граница бюджета
|
||||
/// </summary>
|
||||
public const string To = "to";
|
||||
}
|
||||
@@ -16,20 +16,20 @@ public static class PipelineRejectConstants
|
||||
public static readonly IReadOnlyDictionary<string, string> StageLabels =
|
||||
new Dictionary<string, string>(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] = "повтор",
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
@@ -38,11 +38,11 @@ public static class PipelineRejectConstants
|
||||
public static readonly IReadOnlyDictionary<string, string> SourceLabels =
|
||||
new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["stop"] = "правила",
|
||||
["ml"] = "ML",
|
||||
["ai"] = "ИИ",
|
||||
["stale"] = "система",
|
||||
["dup"] = "система",
|
||||
[PipelineRejectSources.Rules] = "правила",
|
||||
[PipelineRejectSources.Ml] = "ML",
|
||||
[PipelineRejectSources.Ai] = "ИИ",
|
||||
[PipelineRejectSources.System] = "система",
|
||||
[PipelineRejectSources.Duplicate] = "система",
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
namespace Deal.Modules.Pipeline.Application.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Источники решения об отсеве
|
||||
/// </summary>
|
||||
public static class PipelineRejectSources
|
||||
{
|
||||
/// <summary>
|
||||
/// Правила входящих (стоп-фразы, тип и т.п.)
|
||||
/// </summary>
|
||||
public const string Rules = "stop";
|
||||
|
||||
/// <summary>
|
||||
/// Локальная ML-модель
|
||||
/// </summary>
|
||||
public const string Ml = "ml";
|
||||
|
||||
/// <summary>
|
||||
/// ИИ-фильтр
|
||||
/// </summary>
|
||||
public const string Ai = "ai";
|
||||
|
||||
/// <summary>
|
||||
/// Система (устаревание, дубликаты)
|
||||
/// </summary>
|
||||
public const string System = "system";
|
||||
|
||||
/// <summary>
|
||||
/// Дубликат
|
||||
/// </summary>
|
||||
public const string Duplicate = "dup";
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
namespace Deal.Modules.Pipeline.Application.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Этапы отсева входящих сообщений
|
||||
/// </summary>
|
||||
public static class PipelineRejectStages
|
||||
{
|
||||
/// <summary>
|
||||
/// Слишком короткое сообщение
|
||||
/// </summary>
|
||||
public const string Length = "length";
|
||||
|
||||
/// <summary>
|
||||
/// Стоп-фраза
|
||||
/// </summary>
|
||||
public const string Stop = "stop";
|
||||
|
||||
/// <summary>
|
||||
/// Резюме соискателя
|
||||
/// </summary>
|
||||
public const string Resume = "resume";
|
||||
|
||||
/// <summary>
|
||||
/// Неподходящий тип заявки
|
||||
/// </summary>
|
||||
public const string Type = "type";
|
||||
|
||||
/// <summary>
|
||||
/// Нет суммы
|
||||
/// </summary>
|
||||
public const string Budget = "budget";
|
||||
|
||||
/// <summary>
|
||||
/// Исключение по словам/технологиям
|
||||
/// </summary>
|
||||
public const string ExcludeKeywords = "exclude_kw";
|
||||
|
||||
/// <summary>
|
||||
/// Исключение по локации/языку
|
||||
/// </summary>
|
||||
public const string ExcludeLocation = "exclude_location";
|
||||
|
||||
/// <summary>
|
||||
/// Исключение по типу
|
||||
/// </summary>
|
||||
public const string ExcludeType = "exclude_type";
|
||||
|
||||
/// <summary>
|
||||
/// Исключение по бюджету
|
||||
/// </summary>
|
||||
public const string ExcludeBudget = "exclude_budget";
|
||||
|
||||
/// <summary>
|
||||
/// Устаревшее сообщение
|
||||
/// </summary>
|
||||
public const string Stale = "stale";
|
||||
|
||||
/// <summary>
|
||||
/// Спам по ML
|
||||
/// </summary>
|
||||
public const string SpamMl = "spam_ml";
|
||||
|
||||
/// <summary>
|
||||
/// Спам по ИИ
|
||||
/// </summary>
|
||||
public const string SpamAi = "spam_ai";
|
||||
|
||||
/// <summary>
|
||||
/// Отсев ИИ-фильтром
|
||||
/// </summary>
|
||||
public const string FilterAi = "filter_ai";
|
||||
|
||||
/// <summary>
|
||||
/// Дубликат
|
||||
/// </summary>
|
||||
public const string Duplicate = "dup";
|
||||
}
|
||||
@@ -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<string, int> PrimaryOrder = new Dictionary<string, int>(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;
|
||||
|
||||
@@ -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<string> FalsyAnswerValues =
|
||||
new HashSet<string>(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<AiContactDto> 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<string>(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<string> 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<string>();
|
||||
}
|
||||
@@ -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<AiContactDto> ReadContacts(JsonObject root, string text)
|
||||
{
|
||||
List<string> 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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<string> SpamStages = new(StringComparer.Ordinal)
|
||||
{
|
||||
"spam_ml",
|
||||
"spam_ai",
|
||||
"filter_ai",
|
||||
PipelineRejectStages.SpamMl,
|
||||
PipelineRejectStages.SpamAi,
|
||||
PipelineRejectStages.FilterAi,
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
namespace Deal.Modules.Settings.Application.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Ключи полей настроек провайдеров/промптов
|
||||
/// </summary>
|
||||
public static class SettingsFieldKeys
|
||||
{
|
||||
/// <summary>
|
||||
/// API-ключ провайдера
|
||||
/// </summary>
|
||||
public const string ApiKey = "apiKey";
|
||||
|
||||
/// <summary>
|
||||
/// Базовый URL провайдера
|
||||
/// </summary>
|
||||
public const string BaseUrl = "baseUrl";
|
||||
|
||||
/// <summary>
|
||||
/// Модель провайдера
|
||||
/// </summary>
|
||||
public const string Model = "model";
|
||||
|
||||
/// <summary>
|
||||
/// Название пользовательского промпта
|
||||
/// </summary>
|
||||
public const string Name = "name";
|
||||
|
||||
/// <summary>
|
||||
/// Текст пользовательского промпта
|
||||
/// </summary>
|
||||
public const string Prompt = "prompt";
|
||||
|
||||
/// <summary>
|
||||
/// Описание пользовательского промпта
|
||||
/// </summary>
|
||||
public const string Description = "description";
|
||||
}
|
||||
+3
-3
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ public sealed class TenantAdminService(
|
||||
Id: ownerUserId,
|
||||
Login: normalizedEmail,
|
||||
TenantId: tenantId,
|
||||
Status: "active",
|
||||
Status: TenantStatuses.Active,
|
||||
PasswordHash: passwordHasher.Hash(initialPassword)),
|
||||
ct);
|
||||
|
||||
|
||||
@@ -54,6 +54,51 @@ public static class DealMetrics
|
||||
/// </summary>
|
||||
public const string AiBudgetUsedRatioName = "deal.ai.budget.used.ratio";
|
||||
|
||||
/// <summary>
|
||||
/// Имя метрики успешных вызовов платного ИИ
|
||||
/// </summary>
|
||||
public const string AiCallsName = "deal.ai.calls";
|
||||
|
||||
/// <summary>
|
||||
/// Имя метрики токенов платного ИИ
|
||||
/// </summary>
|
||||
public const string AiTokensName = "deal.ai.tokens";
|
||||
|
||||
/// <summary>
|
||||
/// Имя метрики вызовов локального ML
|
||||
/// </summary>
|
||||
public const string MlCallsName = "deal.ml.calls";
|
||||
|
||||
/// <summary>
|
||||
/// Имя метрики оценки токенов локального ML
|
||||
/// </summary>
|
||||
public const string MlTokensName = "deal.ml.tokens";
|
||||
|
||||
/// <summary>
|
||||
/// Имя метрики событий аудита
|
||||
/// </summary>
|
||||
public const string AuditEventsName = "deal.audit.events";
|
||||
|
||||
/// <summary>
|
||||
/// Имя метрики событий подозрительной активности
|
||||
/// </summary>
|
||||
public const string SecurityEventsName = "deal.security.suspicious";
|
||||
|
||||
/// <summary>
|
||||
/// Имя метрики глубины очереди пайплайна
|
||||
/// </summary>
|
||||
public const string PipelineQueueDepthName = "deal.pipeline.queue.depth";
|
||||
|
||||
/// <summary>
|
||||
/// Имя метрики глубины очереди обучения ML
|
||||
/// </summary>
|
||||
public const string MlOutboxDepthName = "deal.ml.outbox.depth";
|
||||
|
||||
/// <summary>
|
||||
/// Имя метрики активных сессий
|
||||
/// </summary>
|
||||
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
|
||||
/// Успешные вызовы платного ИИ
|
||||
/// </summary>
|
||||
public static readonly Counter<long> AiCalls =
|
||||
Meter.CreateCounter<long>("deal.ai.calls", description: "Успешные вызовы платного ИИ (ai-service).");
|
||||
Meter.CreateCounter<long>(AiCallsName, description: "Успешные вызовы платного ИИ (ai-service).");
|
||||
|
||||
/// <summary>
|
||||
/// Токены платного ИИ по видам
|
||||
/// </summary>
|
||||
public static readonly Counter<long> AiTokens =
|
||||
Meter.CreateCounter<long>("deal.ai.tokens", description: "Токены платных ИИ-вызовов (prompt/completion).");
|
||||
Meter.CreateCounter<long>(AiTokensName, description: "Токены платных ИИ-вызовов (prompt/completion).");
|
||||
|
||||
/// <summary>
|
||||
/// Вызовы локального ML
|
||||
/// </summary>
|
||||
public static readonly Counter<long> MlCalls =
|
||||
Meter.CreateCounter<long>("deal.ml.calls", description: "Вызовы локального ML-предсказания.");
|
||||
Meter.CreateCounter<long>(MlCallsName, description: "Вызовы локального ML-предсказания.");
|
||||
|
||||
/// <summary>
|
||||
/// Оценка токенов локальных ML-вызовов
|
||||
/// </summary>
|
||||
public static readonly Counter<long> MlTokens =
|
||||
Meter.CreateCounter<long>("deal.ml.tokens", description: "Оценка токенов локальных ML-вызовов.");
|
||||
Meter.CreateCounter<long>(MlTokensName, description: "Оценка токенов локальных ML-вызовов.");
|
||||
|
||||
/// <summary>
|
||||
/// События аудита по типам и акторам
|
||||
/// </summary>
|
||||
public static readonly Counter<long> AuditEvents =
|
||||
Meter.CreateCounter<long>("deal.audit.events", description: "Записи аудита по типам и акторам.");
|
||||
Meter.CreateCounter<long>(AuditEventsName, description: "Записи аудита по типам и акторам.");
|
||||
|
||||
/// <summary>
|
||||
/// События подозрительной активности по видам
|
||||
/// </summary>
|
||||
public static readonly Counter<long> SecurityEvents =
|
||||
Meter.CreateCounter<long>("deal.security.suspicious", description: "События подозрительной активности по видам (rate_limit, login_blocked).");
|
||||
Meter.CreateCounter<long>(SecurityEventsName, description: "События подозрительной активности по видам (rate_limit, login_blocked).");
|
||||
|
||||
/// <summary>
|
||||
/// Суммарная глубина очереди пайплайна
|
||||
/// </summary>
|
||||
public static readonly ObservableGauge<long> PipelineQueueDepth =
|
||||
Meter.CreateObservableGauge(
|
||||
"deal.pipeline.queue.depth",
|
||||
PipelineQueueDepthName,
|
||||
(Func<long>)(() => (long)Interlocked.Read(ref DealMetrics._pipelineQueueDepth)),
|
||||
description: "Суммарная глубина очереди пайплайна (new+filtered) по всем тенантам.");
|
||||
|
||||
@@ -107,7 +152,7 @@ public static class DealMetrics
|
||||
/// </summary>
|
||||
public static readonly ObservableGauge<long> MlOutboxDepth =
|
||||
Meter.CreateObservableGauge(
|
||||
"deal.ml.outbox.depth",
|
||||
MlOutboxDepthName,
|
||||
(Func<long>)(() => (long)Interlocked.Read(ref DealMetrics._mlOutboxDepth)),
|
||||
description: "Суммарная глубина очереди обучения ML (MlOutbox) по всем тенантам.");
|
||||
|
||||
@@ -116,7 +161,7 @@ public static class DealMetrics
|
||||
/// </summary>
|
||||
public static readonly ObservableGauge<long> ActiveSessions =
|
||||
Meter.CreateObservableGauge(
|
||||
"deal.sessions.active",
|
||||
ActiveSessionsName,
|
||||
(Func<long>)(() => (long)Interlocked.Read(ref DealMetrics._activeSessions)),
|
||||
description: "Активные непросроченные сессии пользователей и операторов.");
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace Deal.SharedKernel.Utilities;
|
||||
|
||||
/// <summary>
|
||||
/// Строковые представления булевых значений
|
||||
/// </summary>
|
||||
public static class BoolText
|
||||
{
|
||||
/// <summary>
|
||||
/// Истина
|
||||
/// </summary>
|
||||
public const string True = "true";
|
||||
|
||||
/// <summary>
|
||||
/// Ложь
|
||||
/// </summary>
|
||||
public const string False = "false";
|
||||
}
|
||||
Reference in New Issue
Block a user