Убрать неиспользуемые using по код-стайлу

Прогон dotnet format (IDE0005) по 4 решениям: удалены лишние using,
оставшиеся после миграции namespace (676 файлов).
This commit is contained in:
Rustam Khalimov
2026-09-11 13:30:57 +03:00
parent 5bfa92a4ac
commit e8b9fab860
675 changed files with 47248 additions and 49911 deletions
@@ -1,235 +1,232 @@
using Deal.Contracts.Integrations.Abstractions;
using Deal.Contracts.Integrations.Models;
using Deal.Grpc.Ai;
using Deal.SharedKernel.Tenants.Abstractions;
using Deal.SharedKernel.Tenants.Models;
using Grpc.Core;
using Microsoft.Extensions.Logging;
using Deal.Infrastructure.Integrations.Abstractions;
using Deal.Infrastructure.Integrations.Exceptions;
using Deal.Infrastructure.Integrations.Extensions;
using Deal.Infrastructure.Integrations.Models;
using Deal.Infrastructure.Integrations.Options;
namespace Deal.Infrastructure.Integrations.Services;
/// <summary>
/// gRPC-адаптер порта <see cref="IAiTools"/> к автономному ai-service (Ruling 9, план Task 15/18/19).
/// </summary>
/// <remarks>
/// Регистрируется вместо Local-реализации при <c>Services:Ai:UseLocal=false</c> (Ruling 6). Поведение 1:1 с
/// ai.proto GenerateKeywords/EvaluateFit и прототипом:
/// <list type="bullet">
/// <item><see cref="GenerateKeywordsAsync"/> — RPC GenerateKeywords (deadline 120 с): конфиг провайдера из
/// настроек, описание ≤4000 (discovery_routes L29); недоступность — мягкий {ok:false, keywords:[], error}
/// (Ruling 11: generate-keywords-эндпоинт отдаёт HTTP 200 {keywords: [], error});</item>
/// <item><see cref="EvaluateFitAsync"/> — RPC EvaluateFit: текст ≤4000 (discovery_eval L41) + описание и ключи
/// задачи; сбой — <see cref="AiUnavailableException"/> (воркер Discovery падает в эвристику, Ruling 10);
/// успех — {fit, reason} (потолок причины 200 задаёт сервис, _AI_REASON_LIMIT L43);</item>
/// <item>usage ответов списывается с бюджета тенанта в tenant_limits и копится в lifetime-KV aiTokenUsage
/// (<see cref="TokenUsageRecorder"/>, Ruling 3 этапа 7), как у классификатора.</item>
/// </list>
/// Каждый вызов несёт metadata tenant-id + service-token (<see cref="AiGrpcConnection"/>, Ruling 1). Scoped:
/// настройки провайдера читаются через scoped-хранилище тенанта (ISettingsStore), как GrpcAiClassifier.
/// Выключатель aiEnabled порт не читает — его отрабатывает вызывающий (воркер/эндпоинт Discovery, Ruling 10/11).
/// </remarks>
public sealed class GrpcAiTools : IAiTools
{
/// <summary>
/// Deadline RPC ai-service — 120 с (README контрактов: провайдер 90/60 с + ретраи 0.8/2 с).
/// </summary>
public const int RpcDeadlineSeconds = 120;
/// <summary>
/// Лимит описания ниши generate-keywords (discovery_routes L29: обрезает до 4000).
/// </summary>
public const int MaxDescriptionCodePoints = 4000;
/// <summary>
/// Лимит текста сообщения evaluate-fit (discovery_eval L41: _AI_TEXT_LIMIT=4000).
/// </summary>
public const int MaxEvalTextCodePoints = 4000;
// Текст фолбэк-ошибки, когда RPC-ошибка не несёт detail (сервис недоступен).
private const string ServiceUnavailableText = "ai-service недоступен — повторите попытку через несколько секунд";
// Причина по умолчанию при fit=true, если сервис причину не вернул (1:1 _AI_REASON_LIMIT L170).
private const string FitReasonDefault = "подходит";
// Причина по умолчанию при fit=false, если сервис причину не вернул (1:1 L170).
private const string NotFitReasonDefault = "не подходит";
private readonly ITenantContext _tenantContext;
private readonly AiGrpcConnection _connection;
private readonly AiProviderConfigBuilder _providerConfigBuilder;
private readonly TokenUsageRecorder _usageRecorder;
private readonly ILogger<GrpcAiTools> _logger;
/// <summary>
/// Создаёт gRPC-адаптер ИИ-инструментов ai-service.
/// </summary>
/// <param name="tenantContext">Контекст текущего тенанта (AsyncLocal, заполняется middleware/циклами).</param>
/// <param name="connection">Транспорт ai-service (singleton-канал + service-token).</param>
/// <param name="providerConfigBuilder">Конфиг активного провайдера из настроек тенанта.</param>
/// <param name="usageRecorder">Списание usage ответов: бюджет периода tenant_limits + lifetime-KV aiTokenUsage.</param>
/// <param name="logger">Логгер сбоев.</param>
public GrpcAiTools(
ITenantContext tenantContext,
AiGrpcConnection connection,
AiProviderConfigBuilder providerConfigBuilder,
TokenUsageRecorder usageRecorder,
ILogger<GrpcAiTools> logger)
{
ArgumentNullException.ThrowIfNull(tenantContext);
ArgumentNullException.ThrowIfNull(connection);
ArgumentNullException.ThrowIfNull(providerConfigBuilder);
ArgumentNullException.ThrowIfNull(usageRecorder);
ArgumentNullException.ThrowIfNull(logger);
_tenantContext = tenantContext;
_connection = connection;
_providerConfigBuilder = providerConfigBuilder;
_usageRecorder = usageRecorder;
_logger = logger;
}
/// <inheritdoc />
public async Task<AiGenerateKeywordsResultDto> GenerateKeywordsAsync(string description, CancellationToken ct)
{
TenantId tenantId = RequireTenant();
try
{
AiService.AiServiceClient client = _connection.CreateClient();
ProviderConfig providerConfig = await _providerConfigBuilder.BuildAsync(ct);
GenerateKeywordsReply reply = await client.GenerateKeywordsAsync(
new GenerateKeywordsRequest
{
Description = SliceCodePoints(description ?? string.Empty, MaxDescriptionCodePoints),
ProviderConfig = providerConfig,
},
CallOptions(tenantId.Value, ct));
await _usageRecorder.AddAsync(reply.Usage, providerConfig.ProviderId, providerConfig.Model, ct);
return new AiGenerateKeywordsResultDto(
Ok: true,
Keywords: reply.Keywords.ToList(),
Error: null);
}
catch (RpcException exception)
{
// Мягкая ошибка для UI (Ruling 11): {ok:false, keywords:[], error} — эндпоинт отвечает HTTP 200.
_logger.LogDebug(exception, "generate-keywords недоступен (тенант {TenantId})", tenantId.Value);
return new AiGenerateKeywordsResultDto(Ok: false, Keywords: Array.Empty<string>(), Error: ErrorText(exception));
}
catch (HttpRequestException exception)
{
_logger.LogDebug(exception, "generate-keywords недоступен (тенант {TenantId})", tenantId.Value);
return new AiGenerateKeywordsResultDto(
Ok: false, Keywords: Array.Empty<string>(), Error: ServiceUnavailableText);
}
}
/// <inheritdoc />
public async Task<AiEvaluateFitResultDto> EvaluateFitAsync(
string text,
string description,
IReadOnlyCollection<string> keywords,
CancellationToken ct)
{
TenantId tenantId = RequireTenant();
try
{
AiService.AiServiceClient client = _connection.CreateClient();
ProviderConfig providerConfig = await _providerConfigBuilder.BuildAsync(ct);
var request = new EvaluateFitRequest
{
Text = SliceCodePoints(text ?? string.Empty, MaxEvalTextCodePoints),
Description = description ?? string.Empty,
ProviderConfig = providerConfig,
};
foreach (string keyword in keywords ?? Array.Empty<string>())
{
if (!string.IsNullOrEmpty(keyword))
{
request.Keywords.Add(keyword);
}
}
EvaluateFitReply reply = await client.EvaluateFitAsync(request, CallOptions(tenantId.Value, ct));
await _usageRecorder.AddAsync(reply.Usage, providerConfig.ProviderId, providerConfig.Model, ct);
return new AiEvaluateFitResultDto(
Fit: reply.Fit,
Reason: reply.HasReason && reply.Reason.Length > 0
? reply.Reason
: (reply.Fit ? FitReasonDefault : NotFitReasonDefault));
}
catch (RpcException exception)
{
// Сбой ИИ-оценки не роняет оценку кандидата — воркер падает в эвристику (Ruling 10, python L191–192).
_logger.LogDebug(exception, "evaluate-fit недоступен (тенант {TenantId})", tenantId.Value);
throw new AiUnavailableException(ErrorText(exception));
}
catch (HttpRequestException exception)
{
_logger.LogDebug(exception, "evaluate-fit недоступен (тенант {TenantId})", tenantId.Value);
throw new AiUnavailableException(ServiceUnavailableText);
}
}
// Текущий тенант scope (без него scoped-хранилища/metadata не имеют смысла).
// Возвращает: Идентификатор тенанта.
// Исключение InvalidOperationException: Вызов вне tenant-контекста (конфигурация).
private TenantId RequireTenant()
=> _tenantContext.TenantId
?? throw new InvalidOperationException(
"GrpcAiTools запрошен вне tenant-контекста (ITenantContext.TenantId == null).");
// CallOptions вызова: metadata tenant-id/service-token + deadline + токен отмены (Ruling 1).
// 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);
// Краткий текст ошибки: detail gRPC-ошибки (дружелюбный текст ai-service) либо фолбэк (Ruling 13:
// секреты/тела ответов не логируются и в текст не попадают).
// exception: Исключение RPC-вызова.
// Возвращает: Текст ошибки.
private static string ErrorText(RpcException exception)
{
string detail = exception.Status.Detail?.Trim() ?? string.Empty;
return detail.Length > 0 ? detail : ServiceUnavailableText;
}
// Первые max кодовых точек строки (python-срез без разрыва суррогатных пар).
// text: Строка.
// max: Лимит.
// Возвращает: Усечённая строка.
private static string SliceCodePoints(string text, int max)
{
if (text.Length <= max)
{
return text;
}
var builder = new System.Text.StringBuilder(max);
int taken = 0;
for (int index = 0; index < text.Length && taken < max; index++)
{
bool pair = char.IsHighSurrogate(text[index])
&& index + 1 < text.Length
&& char.IsLowSurrogate(text[index + 1]);
builder.Append(text[index]);
if (pair)
{
index++;
builder.Append(text[index]);
}
taken++;
}
return builder.ToString();
}
}
using Deal.Contracts.Integrations.Abstractions;
using Deal.Contracts.Integrations.Models;
using Deal.Grpc.Ai;
using Deal.Infrastructure.Integrations.Exceptions;
using Deal.Infrastructure.Integrations.Models;
using Deal.SharedKernel.Tenants.Abstractions;
using Deal.SharedKernel.Tenants.Models;
using Grpc.Core;
using Microsoft.Extensions.Logging;
namespace Deal.Infrastructure.Integrations.Services;
/// <summary>
/// gRPC-адаптер порта <see cref="IAiTools"/> к автономному ai-service (Ruling 9, план Task 15/18/19).
/// </summary>
/// <remarks>
/// Регистрируется вместо Local-реализации при <c>Services:Ai:UseLocal=false</c> (Ruling 6). Поведение 1:1 с
/// ai.proto GenerateKeywords/EvaluateFit и прототипом:
/// <list type="bullet">
/// <item><see cref="GenerateKeywordsAsync"/> — RPC GenerateKeywords (deadline 120 с): конфиг провайдера из
/// настроек, описание ≤4000 (discovery_routes L29); недоступность — мягкий {ok:false, keywords:[], error}
/// (Ruling 11: generate-keywords-эндпоинт отдаёт HTTP 200 {keywords: [], error});</item>
/// <item><see cref="EvaluateFitAsync"/> — RPC EvaluateFit: текст ≤4000 (discovery_eval L41) + описание и ключи
/// задачи; сбой — <see cref="AiUnavailableException"/> (воркер Discovery падает в эвристику, Ruling 10);
/// успех — {fit, reason} (потолок причины 200 задаёт сервис, _AI_REASON_LIMIT L43);</item>
/// <item>usage ответов списывается с бюджета тенанта в tenant_limits и копится в lifetime-KV aiTokenUsage
/// (<see cref="TokenUsageRecorder"/>, Ruling 3 этапа 7), как у классификатора.</item>
/// </list>
/// Каждый вызов несёт metadata tenant-id + service-token (<see cref="AiGrpcConnection"/>, Ruling 1). Scoped:
/// настройки провайдера читаются через scoped-хранилище тенанта (ISettingsStore), как GrpcAiClassifier.
/// Выключатель aiEnabled порт не читает — его отрабатывает вызывающий (воркер/эндпоинт Discovery, Ruling 10/11).
/// </remarks>
public sealed class GrpcAiTools : IAiTools
{
/// <summary>
/// Deadline RPC ai-service — 120 с (README контрактов: провайдер 90/60 с + ретраи 0.8/2 с).
/// </summary>
public const int RpcDeadlineSeconds = 120;
/// <summary>
/// Лимит описания ниши generate-keywords (discovery_routes L29: обрезает до 4000).
/// </summary>
public const int MaxDescriptionCodePoints = 4000;
/// <summary>
/// Лимит текста сообщения evaluate-fit (discovery_eval L41: _AI_TEXT_LIMIT=4000).
/// </summary>
public const int MaxEvalTextCodePoints = 4000;
// Текст фолбэк-ошибки, когда RPC-ошибка не несёт detail (сервис недоступен).
private const string ServiceUnavailableText = "ai-service недоступен — повторите попытку через несколько секунд";
// Причина по умолчанию при fit=true, если сервис причину не вернул (1:1 _AI_REASON_LIMIT L170).
private const string FitReasonDefault = "подходит";
// Причина по умолчанию при fit=false, если сервис причину не вернул (1:1 L170).
private const string NotFitReasonDefault = "не подходит";
private readonly ITenantContext _tenantContext;
private readonly AiGrpcConnection _connection;
private readonly AiProviderConfigBuilder _providerConfigBuilder;
private readonly TokenUsageRecorder _usageRecorder;
private readonly ILogger<GrpcAiTools> _logger;
/// <summary>
/// Создаёт gRPC-адаптер ИИ-инструментов ai-service.
/// </summary>
/// <param name="tenantContext">Контекст текущего тенанта (AsyncLocal, заполняется middleware/циклами).</param>
/// <param name="connection">Транспорт ai-service (singleton-канал + service-token).</param>
/// <param name="providerConfigBuilder">Конфиг активного провайдера из настроек тенанта.</param>
/// <param name="usageRecorder">Списание usage ответов: бюджет периода tenant_limits + lifetime-KV aiTokenUsage.</param>
/// <param name="logger">Логгер сбоев.</param>
public GrpcAiTools(
ITenantContext tenantContext,
AiGrpcConnection connection,
AiProviderConfigBuilder providerConfigBuilder,
TokenUsageRecorder usageRecorder,
ILogger<GrpcAiTools> logger)
{
ArgumentNullException.ThrowIfNull(tenantContext);
ArgumentNullException.ThrowIfNull(connection);
ArgumentNullException.ThrowIfNull(providerConfigBuilder);
ArgumentNullException.ThrowIfNull(usageRecorder);
ArgumentNullException.ThrowIfNull(logger);
_tenantContext = tenantContext;
_connection = connection;
_providerConfigBuilder = providerConfigBuilder;
_usageRecorder = usageRecorder;
_logger = logger;
}
/// <inheritdoc />
public async Task<AiGenerateKeywordsResultDto> GenerateKeywordsAsync(string description, CancellationToken ct)
{
TenantId tenantId = RequireTenant();
try
{
AiService.AiServiceClient client = _connection.CreateClient();
ProviderConfig providerConfig = await _providerConfigBuilder.BuildAsync(ct);
GenerateKeywordsReply reply = await client.GenerateKeywordsAsync(
new GenerateKeywordsRequest
{
Description = SliceCodePoints(description ?? string.Empty, MaxDescriptionCodePoints),
ProviderConfig = providerConfig,
},
CallOptions(tenantId.Value, ct));
await _usageRecorder.AddAsync(reply.Usage, providerConfig.ProviderId, providerConfig.Model, ct);
return new AiGenerateKeywordsResultDto(
Ok: true,
Keywords: reply.Keywords.ToList(),
Error: null);
}
catch (RpcException exception)
{
// Мягкая ошибка для UI (Ruling 11): {ok:false, keywords:[], error} — эндпоинт отвечает HTTP 200.
_logger.LogDebug(exception, "generate-keywords недоступен (тенант {TenantId})", tenantId.Value);
return new AiGenerateKeywordsResultDto(Ok: false, Keywords: Array.Empty<string>(), Error: ErrorText(exception));
}
catch (HttpRequestException exception)
{
_logger.LogDebug(exception, "generate-keywords недоступен (тенант {TenantId})", tenantId.Value);
return new AiGenerateKeywordsResultDto(
Ok: false, Keywords: Array.Empty<string>(), Error: ServiceUnavailableText);
}
}
/// <inheritdoc />
public async Task<AiEvaluateFitResultDto> EvaluateFitAsync(
string text,
string description,
IReadOnlyCollection<string> keywords,
CancellationToken ct)
{
TenantId tenantId = RequireTenant();
try
{
AiService.AiServiceClient client = _connection.CreateClient();
ProviderConfig providerConfig = await _providerConfigBuilder.BuildAsync(ct);
var request = new EvaluateFitRequest
{
Text = SliceCodePoints(text ?? string.Empty, MaxEvalTextCodePoints),
Description = description ?? string.Empty,
ProviderConfig = providerConfig,
};
foreach (string keyword in keywords ?? Array.Empty<string>())
{
if (!string.IsNullOrEmpty(keyword))
{
request.Keywords.Add(keyword);
}
}
EvaluateFitReply reply = await client.EvaluateFitAsync(request, CallOptions(tenantId.Value, ct));
await _usageRecorder.AddAsync(reply.Usage, providerConfig.ProviderId, providerConfig.Model, ct);
return new AiEvaluateFitResultDto(
Fit: reply.Fit,
Reason: reply.HasReason && reply.Reason.Length > 0
? reply.Reason
: (reply.Fit ? FitReasonDefault : NotFitReasonDefault));
}
catch (RpcException exception)
{
// Сбой ИИ-оценки не роняет оценку кандидата — воркер падает в эвристику (Ruling 10, python L191–192).
_logger.LogDebug(exception, "evaluate-fit недоступен (тенант {TenantId})", tenantId.Value);
throw new AiUnavailableException(ErrorText(exception));
}
catch (HttpRequestException exception)
{
_logger.LogDebug(exception, "evaluate-fit недоступен (тенант {TenantId})", tenantId.Value);
throw new AiUnavailableException(ServiceUnavailableText);
}
}
// Текущий тенант scope (без него scoped-хранилища/metadata не имеют смысла).
// Возвращает: Идентификатор тенанта.
// Исключение InvalidOperationException: Вызов вне tenant-контекста (конфигурация).
private TenantId RequireTenant()
=> _tenantContext.TenantId
?? throw new InvalidOperationException(
"GrpcAiTools запрошен вне tenant-контекста (ITenantContext.TenantId == null).");
// CallOptions вызова: metadata tenant-id/service-token + deadline + токен отмены (Ruling 1).
// 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);
// Краткий текст ошибки: detail gRPC-ошибки (дружелюбный текст ai-service) либо фолбэк (Ruling 13:
// секреты/тела ответов не логируются и в текст не попадают).
// exception: Исключение RPC-вызова.
// Возвращает: Текст ошибки.
private static string ErrorText(RpcException exception)
{
string detail = exception.Status.Detail?.Trim() ?? string.Empty;
return detail.Length > 0 ? detail : ServiceUnavailableText;
}
// Первые max кодовых точек строки (python-срез без разрыва суррогатных пар).
// text: Строка.
// max: Лимит.
// Возвращает: Усечённая строка.
private static string SliceCodePoints(string text, int max)
{
if (text.Length <= max)
{
return text;
}
var builder = new System.Text.StringBuilder(max);
int taken = 0;
for (int index = 0; index < text.Length && taken < max; index++)
{
bool pair = char.IsHighSurrogate(text[index])
&& index + 1 < text.Length
&& char.IsLowSurrogate(text[index + 1]);
builder.Append(text[index]);
if (pair)
{
index++;
builder.Append(text[index]);
}
taken++;
}
return builder.ToString();
}
}