Вынести магические строки статусов, видов и ключей в каталоги
Литералы статусов/видов/фаз и 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:
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user