Почистить комментарии от ссылок на ТЗ и обрывков

Удаление целых //-блоков со ссылками (Task/Ruling/этап/ТЗ/§/
дизайн-док/api-map/python/прототип) вместо построчного вырезания —
без обрывков фраз; снят боилерплейт <param>/<returns>; то же в
.proto.
This commit is contained in:
Rustam Khalimov
2026-09-11 13:49:26 +03:00
parent b053d58335
commit 79c931d88e
220 changed files with 32 additions and 1259 deletions
@@ -151,11 +151,6 @@ public sealed class AiConnectionChecker : IAiConnectionChecker
}
}
// request: Запрос проверки (поля статуса провайдера).
// name: Имя провайдера из каталога AiProviders.
// ok: Результат подключения.
// message: Сообщение ветки.
// Возвращает: DTO ответа (наружу — camelCase).
private static AiCheckResultDto BuildResult(
AiCheckRequest request,
string name,
@@ -174,8 +169,6 @@ public sealed class AiConnectionChecker : IAiConnectionChecker
KeyMasked: MaskKey(request.ApiKey));
}
// key: Ключ открытым текстом.
// Возвращает: Маскированная строка.
private static string MaskKey(string key)
{
if (string.IsNullOrEmpty(key))
@@ -191,10 +184,6 @@ public sealed class AiConnectionChecker : IAiConnectionChecker
return string.Concat(key.AsSpan(0, 4), "…", key.AsSpan(key.Length - 4));
}
// baseUrl: Эффективный базовый URL из конфигурации провайдера.
// apiStyle: Стиль API провайдера (null — OpenAI-совместимый).
// modelsUri: URL списка моделей (валиден только при возврате true).
// Возвращает: True — URL построен; False — base URL не абсолютный http(s) (SSRF-гейт).
private static bool TryBuildModelsUri(
string baseUrl,
string? apiStyle,
@@ -87,8 +87,6 @@ public sealed class AiProviderConfigBuilder
return config;
}
// ct: Токен отмены.
// Возвращает: Id провайдера (каталога AiProviders).
private async Task<string> ReadProviderIdAsync(CancellationToken ct)
{
SettingValue? row = await _store.GetAsync(SettingsKeys.AiProvider, ct);
@@ -62,7 +62,6 @@ public sealed class BudgetedAiClassifier : IAiClassifier
return await _paidClassifier.FilterAsync(text, ct);
}
// Запрет гейта — фильтр через Local-реализацию {pass:true, skipped:true} (семантика «фильтр недоступен»,
_logger.LogDebug(
"ИИ-фильтр: {Reason} — Local-пропуск (тенант {TenantId})", GateDeniedLogText, TenantIdForLog());
return await _localClassifier.FilterAsync(text, ct);
@@ -76,14 +75,11 @@ public sealed class BudgetedAiClassifier : IAiClassifier
return await _paidClassifier.ClassifyAsync(text, ct);
}
// без платного ИИ, приём не блокируется.
_logger.LogDebug(
"ИИ-классификация: {Reason} — Local-разбор (тенант {TenantId})", GateDeniedLogText, TenantIdForLog());
return await _localClassifier.ClassifyAsync(text, ct);
}
// ct: Токен отмены.
// Возвращает: True — можно звать платного исполнителя.
private async Task<bool> IsPaidAllowedAsync(CancellationToken ct)
{
BudgetStateDto state = await _tenantLimits.GetStateAsync(RequireTenantId(), ct);
@@ -90,8 +90,6 @@ public sealed class BudgetedAiTools : IAiTools
state.Status == TenantStatuses.Suspended ? SuspendedFitError : ExhaustedFitError);
}
// ct: Токен отмены.
// Возвращает: Состояние бюджета тенанта на сейчас.
private async Task<BudgetStateDto> GateStateAsync(CancellationToken ct)
=> await _tenantLimits.GetStateAsync(RequireTenantId(), ct);
@@ -100,9 +100,6 @@ public sealed class CbrRateSource : IRatesSource
return rates;
}
// currency: Пара «код валюты → объект {Value, Nominal}».
// rate: Курс единицы валюты к рублю (валиден при возврате true).
// Возвращает: True — запись распознана; False — повреждённая запись (весь fetch — сбой).
private static bool TryParseCurrency(JsonProperty currency, out double rate)
{
rate = 0;
@@ -89,7 +89,6 @@ public sealed class GrpcAiClassifier : IAiClassifier
CallOptions(tenantId.Value, ct));
await _usageRecorder.AddAsync(reply.Usage, providerConfig.ProviderId, providerConfig.Model, ct);
// Фильтр применён (воркер звал его только при aiFilterEnabled и не force) — skipped=false
return new AiFilterResultDto(
Pass: reply.Pass,
Reason: reply.HasReason ? reply.Reason : null,
@@ -142,7 +141,6 @@ public sealed class GrpcAiClassifier : IAiClassifier
await _usageRecorder.AddAsync(reply.Usage, providerConfig.ProviderId, providerConfig.Model, ct);
if (!reply.Ok)
{
// Модель не вернула разбираемый JSON после ретраев — контрактная ok=false (README ai.proto):
_logger.LogWarning("ИИ-классификация: ok=false (тенант {TenantId})", tenantId.Value);
throw new AiUnavailableException(NoJsonAnswerText);
}
@@ -167,27 +165,18 @@ public sealed class GrpcAiClassifier : IAiClassifier
?? throw new InvalidOperationException(
"GrpcAiClassifier запрошен вне tenant-контекста (ITenantContext.TenantId == null).");
// tenantId: Id тенанта (формат N).
// ct: Токен отмены вызова.
// Возвращает: Опции вызова с заголовками, deadline и отменой.
private CallOptions CallOptions(string tenantId, CancellationToken ct)
=> new(
headers: _connection.CreateMetadata(tenantId),
deadline: DateTime.UtcNow.Add(TimeSpan.FromSeconds(RpcDeadlineSeconds)),
cancellationToken: ct);
// секреты/тела ответов не логируются и в текст не попадают).
// exception: Исключение RPC-вызова.
// Возвращает: Текст ошибки.
private static string ErrorText(RpcException exception)
{
string detail = exception.Status.Detail?.Trim() ?? string.Empty;
return detail.Length > 0 ? detail : ServiceUnavailableText;
}
// text: Строка.
// max: Лимит.
// Возвращает: Усечённая строка.
private static string SliceCodePoints(string text, int max)
{
return text.Length <= max ? text : SliceByCodePoints(text, max);
@@ -158,27 +158,18 @@ public sealed class GrpcAiTools : IAiTools
?? throw new InvalidOperationException(
"GrpcAiTools запрошен вне tenant-контекста (ITenantContext.TenantId == null).");
// tenantId: Id тенанта (формат N).
// ct: Токен отмены вызова.
// Возвращает: Опции вызова с заголовками, deadline и отменой.
private CallOptions CallOptions(string tenantId, CancellationToken ct)
=> new(
headers: _connection.CreateMetadata(tenantId),
deadline: DateTime.UtcNow.Add(TimeSpan.FromSeconds(RpcDeadlineSeconds)),
cancellationToken: ct);
// секреты/тела ответов не логируются и в текст не попадают).
// exception: Исключение RPC-вызова.
// Возвращает: Текст ошибки.
private static string ErrorText(RpcException exception)
{
string detail = exception.Status.Detail?.Trim() ?? string.Empty;
return detail.Length > 0 ? detail : ServiceUnavailableText;
}
// text: Строка.
// max: Лимит.
// Возвращает: Усечённая строка.
private static string SliceCodePoints(string text, int max)
{
if (text.Length <= max)
@@ -133,7 +133,6 @@ public sealed class GrpcMlClient : IMlClient, IMlTrainClient
new PredictRequest { Text = text ?? string.Empty },
CallOptions(tenantId.Value, TimeSpan.FromSeconds(PredictDeadlineSeconds), ct));
// бюджет/lifetime AI-счётчик не затрагиваются (локальная модель бесплатна).
await _usageRecorder.AddEstimatedAsync(text, TokenUsageSources.Local, TokenUsageSources.Ml, ct);
return MapPredict(reply);
}
@@ -246,8 +245,6 @@ public sealed class GrpcMlClient : IMlClient, IMlTrainClient
}
}
// reply: Ответ ml-service.
// Возвращает: DTO статуса модели.
private static MlServiceStatusDto MapStatus(StatusReply reply)
{
return new MlServiceStatusDto(
@@ -260,8 +257,6 @@ public sealed class GrpcMlClient : IMlClient, IMlTrainClient
Accuracy: reply.Eval?.Accuracy ?? 0.0));
}
// reply: Ответ ml-service.
// Возвращает: DTO предсказания.
private static MlPredictResultDto MapPredict(PredictReply reply)
{
return new MlPredictResultDto(
@@ -289,10 +284,6 @@ public sealed class GrpcMlClient : IMlClient, IMlTrainClient
Margin: decision.Margin);
}
// tenantId: Id тенанта (формат N).
// deadline: Лимит времени вызова.
// ct: Токен отмены вызова.
// Возвращает: Опции вызова с заголовками, deadline и отменой.
private CallOptions CallOptions(
string tenantId,
TimeSpan deadline,
@@ -327,8 +318,6 @@ public sealed class GrpcMlClient : IMlClient, IMlTrainClient
Learned: 0,
Eval: new MlEvalDto(Count: 0, Correct: 0, Accuracy: 0.0));
// ct: Токен отмены.
// Возвращает: True, если ключ отсутствует, повреждён или хранит JSON-true.
private async Task<bool> ReadMlEnabledAsync(CancellationToken ct)
{
SettingValue? row = await _store.GetAsync(SettingsKeys.MlEnabled, ct);
@@ -398,11 +398,6 @@ public sealed class GrpcTelegramClient : ITelegramGateway
?? throw new InvalidOperationException(
"GrpcTelegramClient запрошен вне tenant-контекста (ITenantContext.TenantId == null).");
// TReply: Тип ответа RPC.
// tenantId: Id тенанта (формат N).
// ct: Токен отмены вызова.
// call: Вызов клиента (принимает клиент и CallOptions).
// Возвращает: Ответ RPC.
private async Task<TReply> CallAsync<TReply>(
TenantId tenantId,
TimeSpan deadline,
@@ -418,13 +413,6 @@ public sealed class GrpcTelegramClient : ITelegramGateway
return await call(client, options);
}
// Нормализует транспортные сбои в RpcException «Telegram не подключён»; RpcException домена — как есть.
// Отмена по токену вызывающего пробрасывается без нормализации (не сбой сервиса). Доменные
// RPC-ошибки (INVALID_ARGUMENT/FAILED_PRECONDITION/…) несут канонический detail — их трогать нельзя:
// exception: Исключение вызова.
// tenantId: Id тенанта (лог).
// operation: Имя RPC (лог-аудит).
// Возвращает: Исключение для проброса: транспортный сбой — нормализованный RpcException.
private Exception TranslateTransportFailure(
Exception exception,
TenantId tenantId,
@@ -436,8 +424,6 @@ public sealed class GrpcTelegramClient : ITelegramGateway
return exception;
}
// Доменная RPC-ошибка сервиса (INVALID_ARGUMENT/FAILED_PRECONDITION/NOT_FOUND…) несёт канонический
// Unavailable с detail (сервис сам ответил причиной) — тоже как есть.
if (exception is RpcException rpc &&
(rpc.StatusCode != StatusCode.Unavailable || !string.IsNullOrEmpty(rpc.Status.Detail)))
{
@@ -448,8 +434,6 @@ public sealed class GrpcTelegramClient : ITelegramGateway
return new RpcException(new Status(StatusCode.Unavailable, NotConnectedDetail));
}
// entries: Записи каталога telegram-service.
// Возвращает: Записи в форме контракта (username → handle).
private static IReadOnlyList<TelegramDialogEntryDto> MapEntries(Google.Protobuf.Collections.RepeatedField<DialogEntry> entries)
{
return entries
@@ -84,7 +84,6 @@ ContainersService containersService) : IColumnSuggester
/// <inheritdoc />
public async Task<SuggestKeywordsResultDto> SuggestKeywordsAsync(CancellationToken ct)
{
// свежие 40 (ListCardsAsync(null) = «все, кроме taken», ORDER BY received_at DESC).
IReadOnlyList<CardDto> cards = await store.ListCardsAsync(new CardsQuery(null), ct);
List<string> texts = cards
.Where(card => card.Col != CardIds.Trash
@@ -107,12 +106,6 @@ ContainersService containersService) : IColumnSuggester
return new SuggestKeywordsResultDto(Ok: true, Keywords: keywords, Reason: null);
}
// inbox: Снимок «Неразобранного» (карточки планов берутся из него).
// plans: Планы колонок (SuggestHeuristics.PlanColumns, ≤4).
// ct: Токен отмены.
// Возвращает: Сколько досок реально создано (0 — все откатаны из-за разобранных карточек).
// Каждая доска — suggested=true c правилами {mode:"any", keywords:[тема]} и note-обоснованием.
// Перед раскладкой перечитывается «Неразобранное»: карточки, ушедшие из inbox между снимком и
private async Task<int> StoreSuggestedColumnsAsync(
IReadOnlyList<CardDto> inbox,
IReadOnlyList<SuggestedColumnPlan> plans,
@@ -176,9 +169,6 @@ ContainersService containersService) : IColumnSuggester
return created;
}
// Сработал ли кулдаун: с последнего успешного предложения прошло меньше 20 минут.
// ct: Токен отмены.
// Возвращает: True — повторный вызов слишком рано (ответ {ok:false, reason, cooldown:true}).
private async Task<bool> WithinCooldownAsync(CancellationToken ct)
{
SettingValue? row = await settings.GetAsync(SettingsKeys.LastSuggestAt, ct);
@@ -204,7 +194,6 @@ ContainersService containersService) : IColumnSuggester
}
}
// ct: Токен отмены.
private Task WriteLastSuggestAtAsync(CancellationToken ct) =>
settings.SetAsync(
SettingsKeys.LastSuggestAt,
@@ -75,12 +75,9 @@ public sealed class LocalMlClient(ISettingsStore store, IMlLearningStore learnin
double delta,
CancellationToken ct)
{
// пустые — тихий no-op, text[:6000], id mle_+hex) — в MlOutboxQueue, общем для Local/Grpc-адаптеров.
await MlOutboxQueue.PushAsync(learningStore, text, label, delta, ct);
}
// ct: Токен отмены.
// Возвращает: True, если ключ отсутствует, повреждён или хранит JSON-true.
private async Task<bool> ReadMlEnabledAsync(CancellationToken ct)
{
SettingValue? row = await store.GetAsync(SettingsKeys.MlEnabled, ct);
@@ -4,8 +4,6 @@ using Deal.Modules.Kanban.Application.Models;
namespace Deal.Infrastructure.Integrations.Services;
// тихий no-op, text обрезается до 6000 символов (без разрыва суррогатной пары), id — mle_ +
// и в Local-, и в gRPC-режиме сигнал сначала пишется в outbox, отправку в ml-service делает фоновый
internal static class MlOutboxQueue
{
internal const int MaxLearningTextLength = 6000;
@@ -42,14 +40,9 @@ internal static class MlOutboxQueue
ct);
}
// Возвращает: Короткий id записи очереди.
private static string NewOutboxId()
=> KanbanIdPrefixes.MlOutbox + Convert.ToHexString(RandomNumberGenerator.GetBytes(OutboxIdRandomBytes)).ToLowerInvariant();
// Обрезает текст до MaxLearningTextLength символов, не разбивая суррогатную пару на конце.
// text: Текст (уже trim-нут).
// Возвращает: Первые 6000 символов (или весь текст, если короче).
// (text[:6000]) режет по code points — хвостовой high-surrogate убираем, чтобы в БД не ушла «битая» пара.
private static string TruncateText(string text)
{
if (text.Length <= MaxLearningTextLength)
@@ -76,7 +76,6 @@ public sealed class TokenUsageRecorder
}
await AddToLifetimeAsync(usage, ct);
// token_usage_events (без tenantId в метках).
DealMetrics.RecordAiUsage(usage.Prompt, usage.Completion);
await RecordEventAsync(
provider,
@@ -102,7 +101,6 @@ public sealed class TokenUsageRecorder
CancellationToken ct)
{
long promptTokens = EstimateTokens(text);
// что и событие token_usage_events, kind=ml).
DealMetrics.RecordMlUsage(promptTokens);
await RecordEventAsync(
provider,
@@ -170,8 +168,6 @@ public sealed class TokenUsageRecorder
return id;
}
// usage: Оценка токенов ответа.
// ct: Токен отмены.
private async Task AddToLifetimeAsync(Usage usage, CancellationToken ct)
{
JsonObject? current = await ReadAsync(ct);