Почистить комментарии от упоминаний процесса
Удалены <remarks>, <summary> сжаты до короткой фразы, вырезаны ссылки на Task/Ruling/этап/python/прототип; //-комментарии со ссылками на процесс удалены; то же в .proto. Правила обновлены в docs/spec/Код-стайл-Дейл.md. Строк комментариев 27210 -> ~19100.
This commit is contained in:
@@ -4,93 +4,65 @@ using Deal.Modules.Kanban.Application.Models;
|
||||
namespace Deal.Modules.Pipeline.Application.Parse;
|
||||
|
||||
/// <summary>
|
||||
/// Квалификация контактов из разбора/текста сообщения (pipeline.py L344–430, _norm_phone L661–663,
|
||||
/// _contacts_from L666–679).
|
||||
/// Квалификация контактов из разбора/текста сообщения.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Тип контакта — tg|phone|email|linkedin|whatsapp|site, формат записи — <see cref="CardContactDto"/> карточки
|
||||
/// Kanban ({type, value}, Ruling 4). Отбрасываются боты (<c>@…bot</c>), сервисные t.me-ссылки
|
||||
/// (joinchat/+/s/c/…), «постовые» сайты (teletype, google-формы, youtube и т.п.). <see cref="Build"/> собирает
|
||||
/// до 6 записей с дедупликацией по значению (casefold); при отсутствии контактов в разборе пытается вытащить
|
||||
/// кандидатов из текста (<c>_contacts_from</c>: @username, email, телефон). <see cref="Primary"/> — основной
|
||||
/// контакт карточки для быстрого действия (tg → phone → whatsapp → email → linkedin → site, python L424–430).
|
||||
/// </remarks>
|
||||
public static class ContactsQualifier
|
||||
{
|
||||
/// <summary>
|
||||
/// Максимум записей контактов карточки (python build_contacts L419: ≤6, Ruling 4).
|
||||
/// Максимум записей контактов карточки.
|
||||
/// </summary>
|
||||
public const int MaxContacts = 6;
|
||||
|
||||
// Максимум кандидатов из текста за один вызов (python _contacts_from L679: [:4]).
|
||||
internal const int MaxTextCandidates = 4;
|
||||
|
||||
// Максимум символов сырого контакта (python qualify_contact L358: len > 300 → None).
|
||||
private const int MaxContactLength = 300;
|
||||
|
||||
// Минимальная длина никнейма telegram (python L362/L367: {4,32}).
|
||||
private const int MinTgNameLength = 4;
|
||||
|
||||
// Максимальная длина никнейма telegram (python L362/L367: {4,32}).
|
||||
private const int MaxTgNameLength = 32;
|
||||
|
||||
// Ограничение нормализованного телефона (python _norm_phone L663: [:18]).
|
||||
internal const int MaxNormalizedPhoneLength = 18;
|
||||
|
||||
// Подсказка бота в конце ника: @…bot отбрасывается (python _TG_BOT_HINTS L345).
|
||||
private const string TgBotSuffix = "bot";
|
||||
|
||||
// Сервисные t.me-ссылки: не контакты людей (python _TG_SERVICE_NAMES L346).
|
||||
private static readonly IReadOnlySet<string> TgServiceNames = new HashSet<string>(StringComparer.Ordinal)
|
||||
{
|
||||
"joinchat", "share", "s", "c", "addstickers", "addtheme", "proxy", "bg", "login",
|
||||
};
|
||||
|
||||
// «Постовые» сайты-агрегаторы: не контакты (python _SKIP_SITE_HOSTS L347).
|
||||
private static readonly IReadOnlySet<string> SkipSiteHosts = new HashSet<string>(StringComparer.Ordinal)
|
||||
{
|
||||
"teletype.in", "forms.gle", "docs.google.com", "youtube.com", "youtu.be", "clck.ru",
|
||||
};
|
||||
|
||||
// Ник в telegram: @имя (python L362).
|
||||
private static readonly Regex TelegramNameRe = new(@"\A[A-Za-z0-9_]{4,32}\z", RegexOptions.CultureInvariant);
|
||||
|
||||
// t.me-ссылка на профиль (python L366).
|
||||
private static readonly Regex TelegramLinkRe = new(
|
||||
@"\Ahttps?://(?:www\.)?t\.me/([A-Za-z0-9_]{4,32})/?\z",
|
||||
RegexOptions.CultureInvariant);
|
||||
|
||||
// E-mail (python L372).
|
||||
private static readonly Regex EmailRe = new(
|
||||
@"\A[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}\z",
|
||||
RegexOptions.CultureInvariant);
|
||||
|
||||
// Телефон целиком: цифры/пробелы/дефисы/скобки, опциональный «+» (python L375).
|
||||
private static readonly Regex PhoneRe = new(@"\A\+?[\d\s\-()]{6,20}\z", RegexOptions.CultureInvariant);
|
||||
|
||||
// Ссылка LinkedIn на профиль (python L377).
|
||||
private static readonly Regex LinkedinRe = new(@"linkedin\.com/in/", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||
|
||||
// Ссылка WhatsApp (python L379).
|
||||
private static readonly Regex WhatsappRe = new(@"wa\.me|api\.whatsapp\.com", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||
|
||||
// Хост сайта из URL (python L382).
|
||||
private static readonly Regex SiteHostRe = new(@"https?://(?:www\.)?", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||
|
||||
// Никнеймы в тексте: @имя (python _CONTACT_RE L601).
|
||||
private static readonly Regex AtNameRe = new(@"@[A-Za-z0-9_]{3,}", RegexOptions.CultureInvariant);
|
||||
|
||||
// E-mail в тексте (python _EMAIL_RE L602).
|
||||
private static readonly Regex EmailInTextRe = new(
|
||||
@"[A-Za-z0-9._%+\-]+@[A-Za-z0-9\-]+(?:\.[A-Za-z0-9\-]+)+",
|
||||
RegexOptions.CultureInvariant);
|
||||
|
||||
// Телефон в тексте (python _PHONE_RE L603).
|
||||
private static readonly Regex PhoneInTextRe = new(
|
||||
@"(?:\+7|8|7)[\s\-()]*\d{3}[\s\-()]*\d{3}[\s\-]*\d{2}[\s\-]*\d{2}",
|
||||
RegexOptions.CultureInvariant);
|
||||
|
||||
// Приоритеты основного контакта: tg → phone → whatsapp → email → linkedin → site (python L426).
|
||||
private static readonly IReadOnlyDictionary<string, int> PrimaryOrder = new Dictionary<string, int>(StringComparer.Ordinal)
|
||||
{
|
||||
["tg"] = 0,
|
||||
@@ -101,17 +73,13 @@ public static class ContactsQualifier
|
||||
["site"] = 5,
|
||||
};
|
||||
|
||||
// Приоритет неизвестного типа (не встречается — fallback на всякий случай, python L429: 9).
|
||||
private const int UnknownTypePriority = 9;
|
||||
|
||||
/// <summary>
|
||||
/// Классифицирует один сырой контакт → {type, value} или null (python qualify_contact L350–386).
|
||||
/// Классифицирует один сырой контакт → {type, value} или null.
|
||||
/// </summary>
|
||||
/// <param name="raw">Сырое значение контакта («@user», «https://t.me/x», телефон, email, ссылка).</param>
|
||||
/// <returns>
|
||||
/// Квалифицированный контакт <see cref="CardContactDto"/> либо null — пусто/мусор/бот/сервисная ссылка/
|
||||
/// «постовый» сайт (python: None).
|
||||
/// </returns>
|
||||
/// <returns>Квалифицированный контакт <see cref="CardContactDto"/> либо null — пусто/мусор/бот/сервисная ссылка/ «постовый» сайт.</returns>
|
||||
public static CardContactDto? Qualify(string? raw)
|
||||
{
|
||||
string s = (raw ?? string.Empty).Trim();
|
||||
@@ -183,7 +151,7 @@ public static class ContactsQualifier
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Собирает квалифицированные контакты из разбора/текста (python build_contacts L389–421).
|
||||
/// Собирает квалифицированные контакты из разбора/текста.
|
||||
/// </summary>
|
||||
/// <param name="contacts">Сырые контакты разбора: строка со значениями через <c>;</c>/<c>|</c>/перенос.</param>
|
||||
/// <param name="text">Исходный текст сообщения — кандидаты, если в разборе контактов нет.</param>
|
||||
@@ -193,8 +161,6 @@ public static class ContactsQualifier
|
||||
var candidates = new List<string>();
|
||||
if (contacts is not null)
|
||||
{
|
||||
// python build_contacts L396–397: строка разбивается по разделителям; пустая строка даёт [''] —
|
||||
// «кандидаты есть», текст НЕ извлекаем (1:1, L406: if not cands and text).
|
||||
candidates.AddRange(Regex.Split(contacts, @"[;|\n]+", RegexOptions.CultureInvariant));
|
||||
if (candidates.Count == 0)
|
||||
{
|
||||
@@ -206,8 +172,7 @@ public static class ContactsQualifier
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Собирает квалифицированные контакты из списка значений разбора (python build_contacts L396–405:
|
||||
/// элементы строки могут нести разделители — разбиваются).
|
||||
/// Собирает квалифицированные контакты из списка значений разбора.
|
||||
/// </summary>
|
||||
/// <param name="contactValues">Список сырых значений контактов (пустой/null — извлечение из текста).</param>
|
||||
/// <param name="text">Исходный текст сообщения — кандидаты, если список пуст.</param>
|
||||
@@ -224,7 +189,6 @@ public static class ContactsQualifier
|
||||
continue;
|
||||
}
|
||||
|
||||
// python: элемент списка-строки разбивается разделителями (L398).
|
||||
candidates.AddRange(Regex.Split(value, @"[;|\n]+", RegexOptions.CultureInvariant));
|
||||
}
|
||||
}
|
||||
@@ -233,7 +197,7 @@ public static class ContactsQualifier
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Основной контакт карточки — значение с наименьшим приоритетом (python primary_contact L424–430).
|
||||
/// Основной контакт карточки — значение с наименьшим приоритетом.
|
||||
/// </summary>
|
||||
/// <param name="contacts">Квалифицированные контакты (см. <see cref="Build"/>).</param>
|
||||
/// <returns>Значение основного контакта или пустая строка, если контактов нет.</returns>
|
||||
@@ -260,7 +224,7 @@ public static class ContactsQualifier
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Извлекает кандидатов контактов из текста: @username, e-mail, телефон (python _contacts_from L666–679).
|
||||
/// Извлекает кандидатов контактов из текста
|
||||
/// </summary>
|
||||
/// <param name="body">Текст сообщения или значение метки «Контакты: …».</param>
|
||||
/// <returns>До <see cref="MaxTextCandidates"/> сырых кандидатов в порядке появления (телефон — нормализован).</returns>
|
||||
@@ -274,7 +238,6 @@ public static class ContactsQualifier
|
||||
return result.Count > MaxTextCandidates ? result.Take(MaxTextCandidates).ToList() : result;
|
||||
}
|
||||
|
||||
// Нормализует телефон: убирает пробелы/неразрывные пробелы/дефисы/скобки (python _norm_phone L661–663).
|
||||
// phone: Телефон как встретился в тексте.
|
||||
// Возвращает: Нормализованный телефон (≤MaxNormalizedPhoneLength символов).
|
||||
internal static string NormalizePhone(string phone)
|
||||
@@ -289,7 +252,6 @@ public static class ContactsQualifier
|
||||
: normalized;
|
||||
}
|
||||
|
||||
// Приоритет типа контакта для Primary (python L426–428).
|
||||
// type: Тип контакта (tg/phone/whatsapp/email/linkedin/site).
|
||||
// Возвращает: Приоритет (меньше — важнее); неизвестный тип — UnknownTypePriority.
|
||||
private static int OrderOf(string type)
|
||||
@@ -297,7 +259,6 @@ public static class ContactsQualifier
|
||||
return PrimaryOrder.TryGetValue(type, out int order) ? order : UnknownTypePriority;
|
||||
}
|
||||
|
||||
// Квалифицирует кандидатов и собирает результат: дедуп по casefold-значению, лимит (python L408–420).
|
||||
// candidates: Сырые кандидаты.
|
||||
// text: Текст сообщения для извлечения кандидатов, если список пуст.
|
||||
// Возвращает: Квалифицированные контакты без дублей (≤MaxContacts).
|
||||
@@ -334,7 +295,6 @@ public static class ContactsQualifier
|
||||
return result;
|
||||
}
|
||||
|
||||
// Добавляет в список только отсутствующие значения (python «if m not in out» L669–678).
|
||||
// result: Список-накопитель.
|
||||
// values: Найденные совпадения.
|
||||
private static void AddUnique(List<string> result, IEnumerable<string> values)
|
||||
|
||||
Reference in New Issue
Block a user