Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63242775ee | ||
|
|
beaf20df42 | ||
|
|
4526532b1a | ||
|
|
79793a7635 | ||
|
|
e81f1ebf30 | ||
|
|
babfbf8006 | ||
|
|
b8570e3197 | ||
|
|
0d2204219a |
@@ -271,7 +271,8 @@
|
|||||||
- **Явная реализация интерфейсов — по умолчанию** (`Task ICardStore.GetAsync(...)`). **[изм. 2026-09-11,
|
- **Явная реализация интерфейсов — по умолчанию** (`Task ICardStore.GetAsync(...)`). **[изм. 2026-09-11,
|
||||||
решение владельца]** Классы напрямую не вызываются — только через интерфейсы; исключения: DTO/модели
|
решение владельца]** Классы напрямую не вызываются — только через интерфейсы; исключения: DTO/модели
|
||||||
(напр. `Card` и семейство `I*Card`), хелперы, extension-классы. Весь прод-код уже переведён на явные
|
(напр. `Card` и семейство `I*Card`), хелперы, extension-классы. Весь прод-код уже переведён на явные
|
||||||
реализации (codemod `scripts/make_explicit.py`, идемпотентный).
|
реализации (codemod'ы `scripts/make_explicit.py` и `scripts/strip_implementation_docs.py` — идемпотентны,
|
||||||
|
`--apply` применяет правки, без флага — dry-run-отчёт).
|
||||||
- Один публичный тип интерфейса = один файл (как и для классов); имя файла = имя типа.
|
- Один публичный тип интерфейса = один файл (как и для классов); имя файла = имя типа.
|
||||||
- **Маркерные классы не используются** — если нужен маркер, это маркерный интерфейс
|
- **Маркерные классы не используются** — если нужен маркер, это маркерный интерфейс
|
||||||
(`IKanbanModule`, `ISharedKernel` и т.п.). **[изм. 2026-09-11]**
|
(`IKanbanModule`, `ISharedKernel` и т.п.). **[изм. 2026-09-11]**
|
||||||
|
|||||||
@@ -4,9 +4,6 @@ using System.Text.Json.Nodes;
|
|||||||
|
|
||||||
namespace Deal.Ai.Llm;
|
namespace Deal.Ai.Llm;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// HTTP-реализация <see cref="IProviderClient"/>
|
|
||||||
/// </summary>
|
|
||||||
public sealed class LlmHttpClient : IProviderClient
|
public sealed class LlmHttpClient : IProviderClient
|
||||||
{
|
{
|
||||||
// Относительный путь OpenAI-совместимого эндпоинта (база уже без хвостового «/»).
|
// Относительный путь OpenAI-совместимого эндпоинта (база уже без хвостового «/»).
|
||||||
@@ -58,13 +55,6 @@ public sealed class LlmHttpClient : IProviderClient
|
|||||||
_anthropicCallTimeout = anthropicCallTimeout;
|
_anthropicCallTimeout = anthropicCallTimeout;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Выполняет один вызов модели по выбранной схеме API.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="config">Конфиг провайдера (стиль — <c>ApiStyle</c>).</param>
|
|
||||||
/// <param name="systemPrompt">Системный промпт.</param>
|
|
||||||
/// <param name="userText">Пользовательское сообщение/контекст.</param>
|
|
||||||
/// <returns>Текст ответа и usage API-ответа (null при его отсутствии).</returns>
|
|
||||||
async Task<ProviderChatResult> IProviderClient.ChatAsync(
|
async Task<ProviderChatResult> IProviderClient.ChatAsync(
|
||||||
LlmConfig config,
|
LlmConfig config,
|
||||||
string systemPrompt,
|
string systemPrompt,
|
||||||
|
|||||||
@@ -3,22 +3,17 @@ using Deal.SharedKernel.Tenants.Models;
|
|||||||
|
|
||||||
namespace Deal.Infrastructure.Data;
|
namespace Deal.Infrastructure.Data;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Контекст тенанта на AsyncLocal
|
|
||||||
/// </summary>
|
|
||||||
public sealed class TenantContext : ITenantContext
|
public sealed class TenantContext : ITenantContext
|
||||||
{
|
{
|
||||||
private static readonly AsyncLocal<TenantId?> Current = new();
|
private static readonly AsyncLocal<TenantId?> Current = new();
|
||||||
|
|
||||||
public TenantId? TenantId => Current.Value;
|
TenantId? ITenantContext.TenantId => Current.Value;
|
||||||
|
|
||||||
public bool HasTenant => Current.Value is not null;
|
bool ITenantContext.HasTenant => Current.Value is not null;
|
||||||
|
|
||||||
public string? SchemaName => Current.Value?.SchemaName;
|
string? ITenantContext.SchemaName => Current.Value?.SchemaName;
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
void ITenantContext.SetTenant(TenantId tenantId) => Current.Value = tenantId;
|
void ITenantContext.SetTenant(TenantId tenantId) => Current.Value = tenantId;
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
void ITenantContext.Reset() => Current.Value = null;
|
void ITenantContext.Reset() => Current.Value = null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
using Deal.SharedKernel.Resilience;
|
||||||
|
using Grpc.Core;
|
||||||
|
|
||||||
|
namespace Deal.Infrastructure.Integrations.Resilience;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Повтор транзиентных gRPC-сбоев клиентов автономных сервисов.
|
||||||
|
/// </summary>
|
||||||
|
public static class GrpcRetry
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Число повторов после первой попытки.
|
||||||
|
/// </summary>
|
||||||
|
public const int RetryCount = 2;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Базовая задержка повтора (далее — экспоненциально с джиттером).
|
||||||
|
/// </summary>
|
||||||
|
public static readonly TimeSpan BaseDelay = TimeSpan.FromMilliseconds(200);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Выполняет gRPC-вызов с повтором транзиентных сбоев.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="operation">Вызов (принимает токен отмены).</param>
|
||||||
|
/// <param name="cancellationToken">Токен отмены.</param>
|
||||||
|
/// <returns>Ответ вызова.</returns>
|
||||||
|
public static Task<TResult> ExecuteAsync<TResult>(
|
||||||
|
Func<CancellationToken, Task<TResult>> operation,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
=> ExecuteAsync(operation, DefaultDelayAsync, cancellationToken);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Выполняет gRPC-вызов с повтором и заданной паузой между попытками.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="operation">Вызов (принимает токен отмены).</param>
|
||||||
|
/// <param name="delayAsync">Пауза между попытками (в тестах — мгновенная).</param>
|
||||||
|
/// <param name="cancellationToken">Токен отмены.</param>
|
||||||
|
/// <returns>Ответ вызова.</returns>
|
||||||
|
public static Task<TResult> ExecuteAsync<TResult>(
|
||||||
|
Func<CancellationToken, Task<TResult>> operation,
|
||||||
|
Func<TimeSpan, CancellationToken, Task> delayAsync,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
=> RetryExecutor.ExecuteAsync(
|
||||||
|
operation,
|
||||||
|
RetryCount,
|
||||||
|
BaseDelay,
|
||||||
|
IsTransient,
|
||||||
|
delayAsync,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Признак транзиентного сбоя транспорта (недоступность/дедлайн).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="exception">Исключение вызова.</param>
|
||||||
|
/// <returns>True — сбой имеет смысл повторить.</returns>
|
||||||
|
public static bool IsTransient(Exception exception)
|
||||||
|
=> exception is RpcException rpc
|
||||||
|
&& rpc.StatusCode is StatusCode.Unavailable or StatusCode.DeadlineExceeded;
|
||||||
|
|
||||||
|
// Экспоненциальная задержка с джиттером 0.5–1.5× (сглаживает синхронные ретраи воркеров).
|
||||||
|
private static Task DefaultDelayAsync(TimeSpan delay, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
double factor = 0.5 + Random.Shared.NextDouble();
|
||||||
|
return Task.Delay(TimeSpan.FromMilliseconds(delay.TotalMilliseconds * factor), cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,9 +6,6 @@ using Deal.Modules.Settings.Application.Models;
|
|||||||
|
|
||||||
namespace Deal.Infrastructure.Integrations.Services;
|
namespace Deal.Infrastructure.Integrations.Services;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// HTTP-реализация проверки подключения к AI-провайдеру.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class AiConnectionChecker : IAiConnectionChecker
|
public sealed class AiConnectionChecker : IAiConnectionChecker
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -78,7 +75,6 @@ public sealed class AiConnectionChecker : IAiConnectionChecker
|
|||||||
_httpClient = httpClient;
|
_httpClient = httpClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
async Task<AiCheckResultDto> IAiConnectionChecker.CheckAsync(AiCheckRequest request, CancellationToken ct)
|
async Task<AiCheckResultDto> IAiConnectionChecker.CheckAsync(AiCheckRequest request, CancellationToken ct)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(request);
|
ArgumentNullException.ThrowIfNull(request);
|
||||||
|
|||||||
@@ -9,9 +9,6 @@ using Microsoft.Extensions.Logging;
|
|||||||
|
|
||||||
namespace Deal.Infrastructure.Integrations.Services;
|
namespace Deal.Infrastructure.Integrations.Services;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Декоратор бюджетного гейта порта <see cref="IAiClassifier"/>
|
|
||||||
/// </summary>
|
|
||||||
public sealed class BudgetedAiClassifier : IAiClassifier
|
public sealed class BudgetedAiClassifier : IAiClassifier
|
||||||
{
|
{
|
||||||
// Текст ошибки вызова вне tenant-контекста (гейт читает лимиты по тенанту).
|
// Текст ошибки вызова вне tenant-контекста (гейт читает лимиты по тенанту).
|
||||||
@@ -54,7 +51,6 @@ public sealed class BudgetedAiClassifier : IAiClassifier
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
async Task<AiFilterResultDto> IAiClassifier.FilterAsync(string text, CancellationToken ct)
|
async Task<AiFilterResultDto> IAiClassifier.FilterAsync(string text, CancellationToken ct)
|
||||||
{
|
{
|
||||||
if (await IsPaidAllowedAsync(ct))
|
if (await IsPaidAllowedAsync(ct))
|
||||||
@@ -67,7 +63,6 @@ public sealed class BudgetedAiClassifier : IAiClassifier
|
|||||||
return await _localClassifier.FilterAsync(text, ct);
|
return await _localClassifier.FilterAsync(text, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
async Task<AiParsedCardDto> IAiClassifier.ClassifyAsync(string text, CancellationToken ct)
|
async Task<AiParsedCardDto> IAiClassifier.ClassifyAsync(string text, CancellationToken ct)
|
||||||
{
|
{
|
||||||
if (await IsPaidAllowedAsync(ct))
|
if (await IsPaidAllowedAsync(ct))
|
||||||
|
|||||||
@@ -9,9 +9,6 @@ using Microsoft.Extensions.Logging;
|
|||||||
|
|
||||||
namespace Deal.Infrastructure.Integrations.Services;
|
namespace Deal.Infrastructure.Integrations.Services;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Декоратор бюджетного гейта порта <see cref="IAiTools"/>
|
|
||||||
/// </summary>
|
|
||||||
public sealed class BudgetedAiTools : IAiTools
|
public sealed class BudgetedAiTools : IAiTools
|
||||||
{
|
{
|
||||||
private const string ExhaustedKeywordsError = "ИИ-бюджет исчерпан — генерация ключевых слов недоступна";
|
private const string ExhaustedKeywordsError = "ИИ-бюджет исчерпан — генерация ключевых слов недоступна";
|
||||||
@@ -50,7 +47,6 @@ public sealed class BudgetedAiTools : IAiTools
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
async Task<AiGenerateKeywordsResultDto> IAiTools.GenerateKeywordsAsync(string description, CancellationToken ct)
|
async Task<AiGenerateKeywordsResultDto> IAiTools.GenerateKeywordsAsync(string description, CancellationToken ct)
|
||||||
{
|
{
|
||||||
BudgetStateDto state = await GateStateAsync(ct);
|
BudgetStateDto state = await GateStateAsync(ct);
|
||||||
@@ -69,7 +65,6 @@ public sealed class BudgetedAiTools : IAiTools
|
|||||||
Error: state.Status == TenantStatuses.Suspended ? SuspendedKeywordsError : ExhaustedKeywordsError);
|
Error: state.Status == TenantStatuses.Suspended ? SuspendedKeywordsError : ExhaustedKeywordsError);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
async Task<AiEvaluateFitResultDto> IAiTools.EvaluateFitAsync(
|
async Task<AiEvaluateFitResultDto> IAiTools.EvaluateFitAsync(
|
||||||
string text,
|
string text,
|
||||||
string description,
|
string description,
|
||||||
|
|||||||
@@ -5,9 +5,6 @@ using Microsoft.Extensions.Logging;
|
|||||||
|
|
||||||
namespace Deal.Infrastructure.Integrations.Services;
|
namespace Deal.Infrastructure.Integrations.Services;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// HTTP-источник курсов ЦБ РФ
|
|
||||||
/// </summary>
|
|
||||||
public sealed class CbrRateSource : IRatesSource
|
public sealed class CbrRateSource : IRatesSource
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -47,7 +44,6 @@ public sealed class CbrRateSource : IRatesSource
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
async Task<Dictionary<string, double>?> IRatesSource.FetchAsync(CancellationToken ct)
|
async Task<Dictionary<string, double>?> IRatesSource.FetchAsync(CancellationToken ct)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using Deal.Contracts.Integrations.Models;
|
|||||||
using Deal.Grpc.Ai;
|
using Deal.Grpc.Ai;
|
||||||
using Deal.Infrastructure.Integrations.Exceptions;
|
using Deal.Infrastructure.Integrations.Exceptions;
|
||||||
using Deal.Infrastructure.Integrations.Models;
|
using Deal.Infrastructure.Integrations.Models;
|
||||||
|
using Deal.Infrastructure.Integrations.Resilience;
|
||||||
using Deal.Modules.Pipeline.Application.Services;
|
using Deal.Modules.Pipeline.Application.Services;
|
||||||
using Deal.SharedKernel.Tenants.Abstractions;
|
using Deal.SharedKernel.Tenants.Abstractions;
|
||||||
using Deal.SharedKernel.Tenants.Models;
|
using Deal.SharedKernel.Tenants.Models;
|
||||||
@@ -11,9 +12,6 @@ using Microsoft.Extensions.Logging;
|
|||||||
|
|
||||||
namespace Deal.Infrastructure.Integrations.Services;
|
namespace Deal.Infrastructure.Integrations.Services;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// gRPC-адаптер порта <see cref="IAiClassifier"/> к автономному ai-service.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class GrpcAiClassifier : IAiClassifier
|
public sealed class GrpcAiClassifier : IAiClassifier
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -70,7 +68,6 @@ public sealed class GrpcAiClassifier : IAiClassifier
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
async Task<AiFilterResultDto> IAiClassifier.FilterAsync(string text, CancellationToken ct)
|
async Task<AiFilterResultDto> IAiClassifier.FilterAsync(string text, CancellationToken ct)
|
||||||
{
|
{
|
||||||
TenantId tenantId = RequireTenant();
|
TenantId tenantId = RequireTenant();
|
||||||
@@ -79,14 +76,16 @@ public sealed class GrpcAiClassifier : IAiClassifier
|
|||||||
string prompt = await _contextBuilder.BuildFilterPromptAsync(ct);
|
string prompt = await _contextBuilder.BuildFilterPromptAsync(ct);
|
||||||
ProviderConfig providerConfig = await _providerConfigBuilder.BuildAsync(ct);
|
ProviderConfig providerConfig = await _providerConfigBuilder.BuildAsync(ct);
|
||||||
AiService.AiServiceClient client = _connection.CreateClient();
|
AiService.AiServiceClient client = _connection.CreateClient();
|
||||||
FilterReply reply = await client.FilterAsync(
|
FilterReply reply = await GrpcRetry.ExecuteAsync(
|
||||||
new FilterRequest
|
token => client.FilterAsync(
|
||||||
{
|
new FilterRequest
|
||||||
Prompt = prompt,
|
{
|
||||||
Text = SliceCodePoints(text, MaxFilterTextCodePoints), // python L193: text[:4000]
|
Prompt = prompt,
|
||||||
ProviderConfig = providerConfig,
|
Text = SliceCodePoints(text, MaxFilterTextCodePoints), // python L193: text[:4000]
|
||||||
},
|
ProviderConfig = providerConfig,
|
||||||
CallOptions(tenantId.Value, ct));
|
},
|
||||||
|
CallOptions(tenantId.Value, token)).ResponseAsync,
|
||||||
|
ct);
|
||||||
await _usageRecorder.AddAsync(reply.Usage, providerConfig.ProviderId, providerConfig.Model, ct);
|
await _usageRecorder.AddAsync(reply.Usage, providerConfig.ProviderId, providerConfig.Model, ct);
|
||||||
|
|
||||||
return new AiFilterResultDto(
|
return new AiFilterResultDto(
|
||||||
@@ -106,7 +105,6 @@ public sealed class GrpcAiClassifier : IAiClassifier
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
async Task<AiParsedCardDto> IAiClassifier.ClassifyAsync(string text, CancellationToken ct)
|
async Task<AiParsedCardDto> IAiClassifier.ClassifyAsync(string text, CancellationToken ct)
|
||||||
{
|
{
|
||||||
TenantId tenantId = RequireTenant();
|
TenantId tenantId = RequireTenant();
|
||||||
@@ -118,14 +116,16 @@ public sealed class GrpcAiClassifier : IAiClassifier
|
|||||||
ClassifyReply reply;
|
ClassifyReply reply;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
reply = await client.ClassifyAsync(
|
reply = await GrpcRetry.ExecuteAsync(
|
||||||
new ClassifyRequest
|
token => client.ClassifyAsync(
|
||||||
{
|
new ClassifyRequest
|
||||||
SystemPrompt = systemPrompt,
|
{
|
||||||
UserContext = userContext,
|
SystemPrompt = systemPrompt,
|
||||||
ProviderConfig = providerConfig,
|
UserContext = userContext,
|
||||||
},
|
ProviderConfig = providerConfig,
|
||||||
CallOptions(tenantId.Value, ct));
|
},
|
||||||
|
CallOptions(tenantId.Value, token)).ResponseAsync,
|
||||||
|
ct);
|
||||||
}
|
}
|
||||||
catch (RpcException exception)
|
catch (RpcException exception)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using Deal.Contracts.Integrations.Models;
|
|||||||
using Deal.Grpc.Ai;
|
using Deal.Grpc.Ai;
|
||||||
using Deal.Infrastructure.Integrations.Exceptions;
|
using Deal.Infrastructure.Integrations.Exceptions;
|
||||||
using Deal.Infrastructure.Integrations.Models;
|
using Deal.Infrastructure.Integrations.Models;
|
||||||
|
using Deal.Infrastructure.Integrations.Resilience;
|
||||||
using Deal.SharedKernel.Tenants.Abstractions;
|
using Deal.SharedKernel.Tenants.Abstractions;
|
||||||
using Deal.SharedKernel.Tenants.Models;
|
using Deal.SharedKernel.Tenants.Models;
|
||||||
using Grpc.Core;
|
using Grpc.Core;
|
||||||
@@ -10,9 +11,6 @@ using Microsoft.Extensions.Logging;
|
|||||||
|
|
||||||
namespace Deal.Infrastructure.Integrations.Services;
|
namespace Deal.Infrastructure.Integrations.Services;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// gRPC-адаптер порта <see cref="IAiTools"/> к автономному ai-service.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class GrpcAiTools : IAiTools
|
public sealed class GrpcAiTools : IAiTools
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -70,7 +68,6 @@ public sealed class GrpcAiTools : IAiTools
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
async Task<AiGenerateKeywordsResultDto> IAiTools.GenerateKeywordsAsync(string description, CancellationToken ct)
|
async Task<AiGenerateKeywordsResultDto> IAiTools.GenerateKeywordsAsync(string description, CancellationToken ct)
|
||||||
{
|
{
|
||||||
TenantId tenantId = RequireTenant();
|
TenantId tenantId = RequireTenant();
|
||||||
@@ -78,13 +75,15 @@ public sealed class GrpcAiTools : IAiTools
|
|||||||
{
|
{
|
||||||
AiService.AiServiceClient client = _connection.CreateClient();
|
AiService.AiServiceClient client = _connection.CreateClient();
|
||||||
ProviderConfig providerConfig = await _providerConfigBuilder.BuildAsync(ct);
|
ProviderConfig providerConfig = await _providerConfigBuilder.BuildAsync(ct);
|
||||||
GenerateKeywordsReply reply = await client.GenerateKeywordsAsync(
|
GenerateKeywordsReply reply = await GrpcRetry.ExecuteAsync(
|
||||||
new GenerateKeywordsRequest
|
token => client.GenerateKeywordsAsync(
|
||||||
{
|
new GenerateKeywordsRequest
|
||||||
Description = SliceCodePoints(description ?? string.Empty, MaxDescriptionCodePoints),
|
{
|
||||||
ProviderConfig = providerConfig,
|
Description = SliceCodePoints(description ?? string.Empty, MaxDescriptionCodePoints),
|
||||||
},
|
ProviderConfig = providerConfig,
|
||||||
CallOptions(tenantId.Value, ct));
|
},
|
||||||
|
CallOptions(tenantId.Value, token)).ResponseAsync,
|
||||||
|
ct);
|
||||||
await _usageRecorder.AddAsync(reply.Usage, providerConfig.ProviderId, providerConfig.Model, ct);
|
await _usageRecorder.AddAsync(reply.Usage, providerConfig.ProviderId, providerConfig.Model, ct);
|
||||||
return new AiGenerateKeywordsResultDto(
|
return new AiGenerateKeywordsResultDto(
|
||||||
Ok: true,
|
Ok: true,
|
||||||
@@ -104,7 +103,6 @@ public sealed class GrpcAiTools : IAiTools
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
async Task<AiEvaluateFitResultDto> IAiTools.EvaluateFitAsync(
|
async Task<AiEvaluateFitResultDto> IAiTools.EvaluateFitAsync(
|
||||||
string text,
|
string text,
|
||||||
string description,
|
string description,
|
||||||
@@ -130,7 +128,9 @@ public sealed class GrpcAiTools : IAiTools
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
EvaluateFitReply reply = await client.EvaluateFitAsync(request, CallOptions(tenantId.Value, ct));
|
EvaluateFitReply reply = await GrpcRetry.ExecuteAsync(
|
||||||
|
token => client.EvaluateFitAsync(request, CallOptions(tenantId.Value, token)).ResponseAsync,
|
||||||
|
ct);
|
||||||
await _usageRecorder.AddAsync(reply.Usage, providerConfig.ProviderId, providerConfig.Model, ct);
|
await _usageRecorder.AddAsync(reply.Usage, providerConfig.ProviderId, providerConfig.Model, ct);
|
||||||
return new AiEvaluateFitResultDto(
|
return new AiEvaluateFitResultDto(
|
||||||
Fit: reply.Fit,
|
Fit: reply.Fit,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using Deal.Contracts.Integrations.Models;
|
|||||||
using Deal.Grpc.Ml;
|
using Deal.Grpc.Ml;
|
||||||
using Deal.Infrastructure.Integrations.Abstractions;
|
using Deal.Infrastructure.Integrations.Abstractions;
|
||||||
using Deal.Infrastructure.Integrations.Models;
|
using Deal.Infrastructure.Integrations.Models;
|
||||||
|
using Deal.Infrastructure.Integrations.Resilience;
|
||||||
using Deal.Modules.Kanban.Application.Abstractions;
|
using Deal.Modules.Kanban.Application.Abstractions;
|
||||||
using Deal.Modules.Kanban.Application.Models;
|
using Deal.Modules.Kanban.Application.Models;
|
||||||
using Deal.Modules.Settings.Application.Abstractions;
|
using Deal.Modules.Settings.Application.Abstractions;
|
||||||
@@ -16,9 +17,6 @@ using Microsoft.Extensions.Logging;
|
|||||||
|
|
||||||
namespace Deal.Infrastructure.Integrations.Services;
|
namespace Deal.Infrastructure.Integrations.Services;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// gRPC-адаптер порта IMlClient к автономному ml-service.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class GrpcMlClient : IMlClient, IMlTrainClient
|
public sealed class GrpcMlClient : IMlClient, IMlTrainClient
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -97,7 +95,6 @@ public sealed class GrpcMlClient : IMlClient, IMlTrainClient
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
async Task<MlStatusResponseDto> IMlClient.StatusAsync(CancellationToken ct)
|
async Task<MlStatusResponseDto> IMlClient.StatusAsync(CancellationToken ct)
|
||||||
{
|
{
|
||||||
TenantId tenantId = RequireTenant();
|
TenantId tenantId = RequireTenant();
|
||||||
@@ -122,16 +119,17 @@ public sealed class GrpcMlClient : IMlClient, IMlTrainClient
|
|||||||
return new MlStatusResponseDto(Enabled: enabled, Service: snapshot.Service, Reachable: snapshot.Reachable, Stats: stats);
|
return new MlStatusResponseDto(Enabled: enabled, Service: snapshot.Service, Reachable: snapshot.Reachable, Stats: stats);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
async Task<MlPredictResultDto> IMlClient.PredictAsync(string text, CancellationToken ct)
|
async Task<MlPredictResultDto> IMlClient.PredictAsync(string text, CancellationToken ct)
|
||||||
{
|
{
|
||||||
TenantId tenantId = RequireTenant();
|
TenantId tenantId = RequireTenant();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
MlService.MlServiceClient client = _connection.CreateClient();
|
MlService.MlServiceClient client = _connection.CreateClient();
|
||||||
PredictReply reply = await client.PredictAsync(
|
PredictReply reply = await GrpcRetry.ExecuteAsync(
|
||||||
new PredictRequest { Text = text ?? string.Empty },
|
token => client.PredictAsync(
|
||||||
CallOptions(tenantId.Value, TimeSpan.FromSeconds(PredictDeadlineSeconds), ct));
|
new PredictRequest { Text = text ?? string.Empty },
|
||||||
|
CallOptions(tenantId.Value, TimeSpan.FromSeconds(PredictDeadlineSeconds), token)).ResponseAsync,
|
||||||
|
ct);
|
||||||
|
|
||||||
await _usageRecorder.AddEstimatedAsync(text, TokenUsageSources.Local, TokenUsageSources.Ml, ct);
|
await _usageRecorder.AddEstimatedAsync(text, TokenUsageSources.Local, TokenUsageSources.Ml, ct);
|
||||||
return MapPredict(reply);
|
return MapPredict(reply);
|
||||||
@@ -143,7 +141,6 @@ public sealed class GrpcMlClient : IMlClient, IMlTrainClient
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
async Task<MlResetResultDto> IMlClient.ResetAsync(CancellationToken ct)
|
async Task<MlResetResultDto> IMlClient.ResetAsync(CancellationToken ct)
|
||||||
{
|
{
|
||||||
TenantId tenantId = RequireTenant();
|
TenantId tenantId = RequireTenant();
|
||||||
@@ -171,7 +168,6 @@ public sealed class GrpcMlClient : IMlClient, IMlTrainClient
|
|||||||
return new MlResetResultDto(Ok: true, Error: null);
|
return new MlResetResultDto(Ok: true, Error: null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
async Task IMlClient.PushAsync(
|
async Task IMlClient.PushAsync(
|
||||||
string text,
|
string text,
|
||||||
string label,
|
string label,
|
||||||
@@ -181,7 +177,6 @@ public sealed class GrpcMlClient : IMlClient, IMlTrainClient
|
|||||||
await MlOutboxQueue.PushAsync(_learningStore, text, label, delta, ct);
|
await MlOutboxQueue.PushAsync(_learningStore, text, label, delta, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
async Task<int> IMlTrainClient.TrainBatchAsync(IReadOnlyList<MlOutboxEntryDto> items, CancellationToken ct)
|
async Task<int> IMlTrainClient.TrainBatchAsync(IReadOnlyList<MlOutboxEntryDto> items, CancellationToken ct)
|
||||||
{
|
{
|
||||||
TenantId tenantId = RequireTenant();
|
TenantId tenantId = RequireTenant();
|
||||||
@@ -228,9 +223,11 @@ public sealed class GrpcMlClient : IMlClient, IMlTrainClient
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
MlService.MlServiceClient client = _connection.CreateClient();
|
MlService.MlServiceClient client = _connection.CreateClient();
|
||||||
StatusReply reply = await client.StatusAsync(
|
StatusReply reply = await GrpcRetry.ExecuteAsync(
|
||||||
new StatusRequest(),
|
token => client.StatusAsync(
|
||||||
CallOptions(tenantId.Value, TimeSpan.FromSeconds(StatusDeadlineSeconds), ct));
|
new StatusRequest(),
|
||||||
|
CallOptions(tenantId.Value, TimeSpan.FromSeconds(StatusDeadlineSeconds), token)).ResponseAsync,
|
||||||
|
ct);
|
||||||
MlServiceStatusDto service = MapStatus(reply);
|
MlServiceStatusDto service = MapStatus(reply);
|
||||||
_statusCache.Set(tenantId.Value, service, reachable: true);
|
_statusCache.Set(tenantId.Value, service, reachable: true);
|
||||||
return _statusCache.TryGet(tenantId.Value, out MlStatusCache.Snapshot updated)
|
return _statusCache.TryGet(tenantId.Value, out MlStatusCache.Snapshot updated)
|
||||||
|
|||||||
@@ -9,9 +9,6 @@ using Microsoft.Extensions.Logging;
|
|||||||
|
|
||||||
namespace Deal.Infrastructure.Integrations.Services;
|
namespace Deal.Infrastructure.Integrations.Services;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// gRPC-адаптер порта <see cref="ITelegramGateway"/> к автономному telegram-service.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class GrpcTelegramClient : ITelegramGateway
|
public sealed class GrpcTelegramClient : ITelegramGateway
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -58,7 +55,6 @@ public sealed class GrpcTelegramClient : ITelegramGateway
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
async Task<TelegramAccountStatusDto> ITelegramGateway.StatusAsync(CancellationToken ct)
|
async Task<TelegramAccountStatusDto> ITelegramGateway.StatusAsync(CancellationToken ct)
|
||||||
{
|
{
|
||||||
TenantId tenantId = RequireTenant();
|
TenantId tenantId = RequireTenant();
|
||||||
@@ -81,7 +77,6 @@ public sealed class GrpcTelegramClient : ITelegramGateway
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
async Task<TelegramAuthResultDto> ITelegramGateway.StartPhoneAsync(
|
async Task<TelegramAuthResultDto> ITelegramGateway.StartPhoneAsync(
|
||||||
string phone,
|
string phone,
|
||||||
int apiId,
|
int apiId,
|
||||||
@@ -104,7 +99,6 @@ public sealed class GrpcTelegramClient : ITelegramGateway
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
async Task<TelegramAuthResultDto> ITelegramGateway.StartQrAsync(
|
async Task<TelegramAuthResultDto> ITelegramGateway.StartQrAsync(
|
||||||
int apiId,
|
int apiId,
|
||||||
string apiHash,
|
string apiHash,
|
||||||
@@ -128,7 +122,6 @@ public sealed class GrpcTelegramClient : ITelegramGateway
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
async Task<string> ITelegramGateway.SendCodeAsync(string code, CancellationToken ct)
|
async Task<string> ITelegramGateway.SendCodeAsync(string code, CancellationToken ct)
|
||||||
{
|
{
|
||||||
TenantId tenantId = RequireTenant();
|
TenantId tenantId = RequireTenant();
|
||||||
@@ -146,7 +139,6 @@ public sealed class GrpcTelegramClient : ITelegramGateway
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
async Task<string> ITelegramGateway.SendPasswordAsync(string password, CancellationToken ct)
|
async Task<string> ITelegramGateway.SendPasswordAsync(string password, CancellationToken ct)
|
||||||
{
|
{
|
||||||
TenantId tenantId = RequireTenant();
|
TenantId tenantId = RequireTenant();
|
||||||
@@ -164,7 +156,6 @@ public sealed class GrpcTelegramClient : ITelegramGateway
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
async Task ITelegramGateway.LogoutAsync(CancellationToken ct)
|
async Task ITelegramGateway.LogoutAsync(CancellationToken ct)
|
||||||
{
|
{
|
||||||
TenantId tenantId = RequireTenant();
|
TenantId tenantId = RequireTenant();
|
||||||
@@ -180,7 +171,6 @@ public sealed class GrpcTelegramClient : ITelegramGateway
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
async Task<IReadOnlyList<TelegramDialogEntryDto>> ITelegramGateway.RefreshDialogsAsync(CancellationToken ct)
|
async Task<IReadOnlyList<TelegramDialogEntryDto>> ITelegramGateway.RefreshDialogsAsync(CancellationToken ct)
|
||||||
{
|
{
|
||||||
TenantId tenantId = RequireTenant();
|
TenantId tenantId = RequireTenant();
|
||||||
@@ -197,7 +187,6 @@ public sealed class GrpcTelegramClient : ITelegramGateway
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
async Task ITelegramGateway.SetMonitorAsync(
|
async Task ITelegramGateway.SetMonitorAsync(
|
||||||
string dialogId,
|
string dialogId,
|
||||||
bool enabled,
|
bool enabled,
|
||||||
@@ -217,7 +206,6 @@ public sealed class GrpcTelegramClient : ITelegramGateway
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
async Task ITelegramGateway.SetMonitorAllAsync(bool enabled, CancellationToken ct)
|
async Task ITelegramGateway.SetMonitorAllAsync(bool enabled, CancellationToken ct)
|
||||||
{
|
{
|
||||||
TenantId tenantId = RequireTenant();
|
TenantId tenantId = RequireTenant();
|
||||||
@@ -234,7 +222,6 @@ public sealed class GrpcTelegramClient : ITelegramGateway
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
async Task<int> ITelegramGateway.BackfillAsync(
|
async Task<int> ITelegramGateway.BackfillAsync(
|
||||||
string dialogId,
|
string dialogId,
|
||||||
bool force,
|
bool force,
|
||||||
@@ -255,7 +242,6 @@ public sealed class GrpcTelegramClient : ITelegramGateway
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
async Task<IReadOnlyList<TelegramRecentMessageDto>> ITelegramGateway.ReadRecentAsync(
|
async Task<IReadOnlyList<TelegramRecentMessageDto>> ITelegramGateway.ReadRecentAsync(
|
||||||
string dialogId,
|
string dialogId,
|
||||||
int limit,
|
int limit,
|
||||||
@@ -278,7 +264,6 @@ public sealed class GrpcTelegramClient : ITelegramGateway
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
async Task<TelegramSourceContentDto> ITelegramGateway.ReadSourceAsync(
|
async Task<TelegramSourceContentDto> ITelegramGateway.ReadSourceAsync(
|
||||||
string dialogId,
|
string dialogId,
|
||||||
long msgId,
|
long msgId,
|
||||||
@@ -302,7 +287,6 @@ public sealed class GrpcTelegramClient : ITelegramGateway
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
async Task<IReadOnlyList<TelegramDialogEntryDto>> ITelegramGateway.SearchAsync(
|
async Task<IReadOnlyList<TelegramDialogEntryDto>> ITelegramGateway.SearchAsync(
|
||||||
string query,
|
string query,
|
||||||
int limit,
|
int limit,
|
||||||
@@ -323,7 +307,6 @@ public sealed class GrpcTelegramClient : ITelegramGateway
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
async Task<TelegramChannelInfoDto> ITelegramGateway.InfoAsync(string dialogId, CancellationToken ct)
|
async Task<TelegramChannelInfoDto> ITelegramGateway.InfoAsync(string dialogId, CancellationToken ct)
|
||||||
{
|
{
|
||||||
TenantId tenantId = RequireTenant();
|
TenantId tenantId = RequireTenant();
|
||||||
@@ -349,7 +332,6 @@ public sealed class GrpcTelegramClient : ITelegramGateway
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
async Task<TelegramEvalReadDto> ITelegramGateway.ReadForEvalAsync(
|
async Task<TelegramEvalReadDto> ITelegramGateway.ReadForEvalAsync(
|
||||||
string dialogId,
|
string dialogId,
|
||||||
int limit,
|
int limit,
|
||||||
@@ -380,7 +362,6 @@ public sealed class GrpcTelegramClient : ITelegramGateway
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
async Task ITelegramGateway.JoinAsync(string username, CancellationToken ct)
|
async Task ITelegramGateway.JoinAsync(string username, CancellationToken ct)
|
||||||
{
|
{
|
||||||
TenantId tenantId = RequireTenant();
|
TenantId tenantId = RequireTenant();
|
||||||
@@ -397,7 +378,6 @@ public sealed class GrpcTelegramClient : ITelegramGateway
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
async Task ITelegramGateway.LeaveAsync(string dialogId, CancellationToken ct)
|
async Task ITelegramGateway.LeaveAsync(string dialogId, CancellationToken ct)
|
||||||
{
|
{
|
||||||
TenantId tenantId = RequireTenant();
|
TenantId tenantId = RequireTenant();
|
||||||
|
|||||||
@@ -3,20 +3,15 @@ using Deal.Contracts.Integrations.Models;
|
|||||||
|
|
||||||
namespace Deal.Infrastructure.Integrations.Services;
|
namespace Deal.Infrastructure.Integrations.Services;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Локальная реализация <see cref="IAiTools"/> без внешнего ИИ-сервиса.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class LocalAiTools : IAiTools
|
public sealed class LocalAiTools : IAiTools
|
||||||
{
|
{
|
||||||
// Сообщение исключения методов (локальный режим = ai-service не подключён).
|
// Сообщение исключения методов (локальный режим = ai-service не подключён).
|
||||||
private const string NotSupportedMessage =
|
private const string NotSupportedMessage =
|
||||||
"ИИ-инструменты доступны только при подключённом ai-service (Services:Ai:UseLocal=false).";
|
"ИИ-инструменты доступны только при подключённом ai-service (Services:Ai:UseLocal=false).";
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
Task<AiGenerateKeywordsResultDto> IAiTools.GenerateKeywordsAsync(string description, CancellationToken ct)
|
Task<AiGenerateKeywordsResultDto> IAiTools.GenerateKeywordsAsync(string description, CancellationToken ct)
|
||||||
=> throw new NotSupportedException(NotSupportedMessage);
|
=> throw new NotSupportedException(NotSupportedMessage);
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
Task<AiEvaluateFitResultDto> IAiTools.EvaluateFitAsync(
|
Task<AiEvaluateFitResultDto> IAiTools.EvaluateFitAsync(
|
||||||
string text,
|
string text,
|
||||||
string description,
|
string description,
|
||||||
|
|||||||
@@ -3,21 +3,16 @@ using Deal.Contracts.Integrations.Models;
|
|||||||
|
|
||||||
namespace Deal.Infrastructure.Integrations.Services;
|
namespace Deal.Infrastructure.Integrations.Services;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Локальная заглушка <see cref="ITelegramGateway"/> без telegram-service.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class LocalTelegramGateway : ITelegramGateway
|
public sealed class LocalTelegramGateway : ITelegramGateway
|
||||||
{
|
{
|
||||||
// Фаза idle-формы (аккаунт не подключён — сервиса нет).
|
// Фаза idle-формы (аккаунт не подключён — сервиса нет).
|
||||||
private const string IdlePhase = "idle";
|
private const string IdlePhase = "idle";
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
Task<TelegramAccountStatusDto> ITelegramGateway.StatusAsync(CancellationToken ct)
|
Task<TelegramAccountStatusDto> ITelegramGateway.StatusAsync(CancellationToken ct)
|
||||||
{
|
{
|
||||||
return Task.FromResult(new TelegramAccountStatusDto(IdlePhase, false, false, string.Empty, null, null));
|
return Task.FromResult(new TelegramAccountStatusDto(IdlePhase, false, false, string.Empty, null, null));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
Task<TelegramAuthResultDto> ITelegramGateway.StartPhoneAsync(
|
Task<TelegramAuthResultDto> ITelegramGateway.StartPhoneAsync(
|
||||||
string phone,
|
string phone,
|
||||||
int apiId,
|
int apiId,
|
||||||
@@ -25,76 +20,61 @@ public sealed class LocalTelegramGateway : ITelegramGateway
|
|||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
=> Task.FromResult(new TelegramAuthResultDto(IdlePhase, null));
|
=> Task.FromResult(new TelegramAuthResultDto(IdlePhase, null));
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
Task<TelegramAuthResultDto> ITelegramGateway.StartQrAsync(
|
Task<TelegramAuthResultDto> ITelegramGateway.StartQrAsync(
|
||||||
int apiId,
|
int apiId,
|
||||||
string apiHash,
|
string apiHash,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
=> Task.FromResult(new TelegramAuthResultDto(IdlePhase, null));
|
=> Task.FromResult(new TelegramAuthResultDto(IdlePhase, null));
|
||||||
|
|
||||||
/// <inheritdoc />
|
Task<string> ITelegramGateway.SendCodeAsync(string code, CancellationToken ct) => Task.FromResult(IdlePhase);
|
||||||
public Task<string> SendCodeAsync(string code, CancellationToken ct) => Task.FromResult(IdlePhase);
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
Task<string> ITelegramGateway.SendPasswordAsync(string password, CancellationToken ct) => Task.FromResult(IdlePhase);
|
||||||
public Task<string> SendPasswordAsync(string password, CancellationToken ct) => Task.FromResult(IdlePhase);
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
Task ITelegramGateway.LogoutAsync(CancellationToken ct) => Task.CompletedTask;
|
Task ITelegramGateway.LogoutAsync(CancellationToken ct) => Task.CompletedTask;
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
Task<IReadOnlyList<TelegramDialogEntryDto>> ITelegramGateway.RefreshDialogsAsync(CancellationToken ct)
|
Task<IReadOnlyList<TelegramDialogEntryDto>> ITelegramGateway.RefreshDialogsAsync(CancellationToken ct)
|
||||||
=> Task.FromResult<IReadOnlyList<TelegramDialogEntryDto>>([]);
|
=> Task.FromResult<IReadOnlyList<TelegramDialogEntryDto>>([]);
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
Task ITelegramGateway.SetMonitorAsync(
|
Task ITelegramGateway.SetMonitorAsync(
|
||||||
string dialogId,
|
string dialogId,
|
||||||
bool enabled,
|
bool enabled,
|
||||||
CancellationToken ct) => Task.CompletedTask;
|
CancellationToken ct) => Task.CompletedTask;
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
Task ITelegramGateway.SetMonitorAllAsync(bool enabled, CancellationToken ct) => Task.CompletedTask;
|
Task ITelegramGateway.SetMonitorAllAsync(bool enabled, CancellationToken ct) => Task.CompletedTask;
|
||||||
|
|
||||||
/// <inheritdoc />
|
Task<int> ITelegramGateway.BackfillAsync(
|
||||||
public Task<int> BackfillAsync(
|
|
||||||
string dialogId,
|
string dialogId,
|
||||||
bool force,
|
bool force,
|
||||||
CancellationToken ct) => Task.FromResult(0);
|
CancellationToken ct) => Task.FromResult(0);
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
Task<IReadOnlyList<TelegramRecentMessageDto>> ITelegramGateway.ReadRecentAsync(
|
Task<IReadOnlyList<TelegramRecentMessageDto>> ITelegramGateway.ReadRecentAsync(
|
||||||
string dialogId,
|
string dialogId,
|
||||||
int limit,
|
int limit,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
=> Task.FromResult<IReadOnlyList<TelegramRecentMessageDto>>([]);
|
=> Task.FromResult<IReadOnlyList<TelegramRecentMessageDto>>([]);
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
Task<TelegramSourceContentDto> ITelegramGateway.ReadSourceAsync(
|
Task<TelegramSourceContentDto> ITelegramGateway.ReadSourceAsync(
|
||||||
string dialogId,
|
string dialogId,
|
||||||
long msgId,
|
long msgId,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
=> Task.FromResult(new TelegramSourceContentDto(false, null, null));
|
=> Task.FromResult(new TelegramSourceContentDto(false, null, null));
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
Task<IReadOnlyList<TelegramDialogEntryDto>> ITelegramGateway.SearchAsync(
|
Task<IReadOnlyList<TelegramDialogEntryDto>> ITelegramGateway.SearchAsync(
|
||||||
string query,
|
string query,
|
||||||
int limit,
|
int limit,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
=> Task.FromResult<IReadOnlyList<TelegramDialogEntryDto>>([]);
|
=> Task.FromResult<IReadOnlyList<TelegramDialogEntryDto>>([]);
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
Task<TelegramChannelInfoDto> ITelegramGateway.InfoAsync(string dialogId, CancellationToken ct)
|
Task<TelegramChannelInfoDto> ITelegramGateway.InfoAsync(string dialogId, CancellationToken ct)
|
||||||
=> Task.FromResult(new TelegramChannelInfoDto(dialogId, string.Empty, string.Empty, string.Empty, SourceDefaults.DefaultHue, null, false));
|
=> Task.FromResult(new TelegramChannelInfoDto(dialogId, string.Empty, string.Empty, string.Empty, SourceDefaults.DefaultHue, null, false));
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
Task<TelegramEvalReadDto> ITelegramGateway.ReadForEvalAsync(
|
Task<TelegramEvalReadDto> ITelegramGateway.ReadForEvalAsync(
|
||||||
string dialogId,
|
string dialogId,
|
||||||
int limit,
|
int limit,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
=> Task.FromResult(new TelegramEvalReadDto(false, "no_history", []));
|
=> Task.FromResult(new TelegramEvalReadDto(false, "no_history", []));
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
Task ITelegramGateway.JoinAsync(string username, CancellationToken ct) => Task.CompletedTask;
|
Task ITelegramGateway.JoinAsync(string username, CancellationToken ct) => Task.CompletedTask;
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
Task ITelegramGateway.LeaveAsync(string dialogId, CancellationToken ct) => Task.CompletedTask;
|
Task ITelegramGateway.LeaveAsync(string dialogId, CancellationToken ct) => Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,6 @@ using Deal.Contracts.Integrations.Models;
|
|||||||
|
|
||||||
namespace Deal.Infrastructure.Integrations.Storage.Services;
|
namespace Deal.Infrastructure.Integrations.Storage.Services;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Локальное файловое хранилище вложений — каталог на диске.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class LocalFileStorage : IFileStorage
|
public sealed class LocalFileStorage : IFileStorage
|
||||||
{
|
{
|
||||||
// Размер буфера чтения при скачивании (async FileStream).
|
// Размер буфера чтения при скачивании (async FileStream).
|
||||||
@@ -31,7 +28,6 @@ public sealed class LocalFileStorage : IFileStorage
|
|||||||
/// <returns>Строка вида <c>LocalFileStorage (root: …)</c>.</returns>
|
/// <returns>Строка вида <c>LocalFileStorage (root: …)</c>.</returns>
|
||||||
public override string ToString() => $"LocalFileStorage (root: {_rootPath})";
|
public override string ToString() => $"LocalFileStorage (root: {_rootPath})";
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
async Task<string> IFileStorage.PutAsync(
|
async Task<string> IFileStorage.PutAsync(
|
||||||
string objectKey,
|
string objectKey,
|
||||||
Stream content,
|
Stream content,
|
||||||
@@ -55,7 +51,6 @@ public sealed class LocalFileStorage : IFileStorage
|
|||||||
return objectKey;
|
return objectKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
Task<Stream?> IFileStorage.GetAsync(string objectKey, CancellationToken ct)
|
Task<Stream?> IFileStorage.GetAsync(string objectKey, CancellationToken ct)
|
||||||
{
|
{
|
||||||
string path = ResolvePath(objectKey);
|
string path = ResolvePath(objectKey);
|
||||||
@@ -68,7 +63,6 @@ public sealed class LocalFileStorage : IFileStorage
|
|||||||
return Task.FromResult<Stream?>(stream);
|
return Task.FromResult<Stream?>(stream);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
Task<FileMeta?> IFileStorage.StatAsync(string objectKey, CancellationToken ct)
|
Task<FileMeta?> IFileStorage.StatAsync(string objectKey, CancellationToken ct)
|
||||||
{
|
{
|
||||||
string path = ResolvePath(objectKey);
|
string path = ResolvePath(objectKey);
|
||||||
@@ -81,7 +75,6 @@ public sealed class LocalFileStorage : IFileStorage
|
|||||||
return Task.FromResult<FileMeta?>(new FileMeta(objectKey, info.Length, string.Empty));
|
return Task.FromResult<FileMeta?>(new FileMeta(objectKey, info.Length, string.Empty));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
Task IFileStorage.DeleteAsync(string objectKey, CancellationToken ct)
|
Task IFileStorage.DeleteAsync(string objectKey, CancellationToken ct)
|
||||||
{
|
{
|
||||||
string path = ResolvePath(objectKey);
|
string path = ResolvePath(objectKey);
|
||||||
|
|||||||
@@ -9,9 +9,6 @@ using Minio.Exceptions;
|
|||||||
|
|
||||||
namespace Deal.Infrastructure.Integrations.Storage.Services;
|
namespace Deal.Infrastructure.Integrations.Storage.Services;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Хранилище вложений на MinIO
|
|
||||||
/// </summary>
|
|
||||||
public sealed class MinioFileStorage : IFileStorage
|
public sealed class MinioFileStorage : IFileStorage
|
||||||
{
|
{
|
||||||
private const string DefaultContentType = "application/octet-stream";
|
private const string DefaultContentType = "application/octet-stream";
|
||||||
@@ -66,7 +63,6 @@ public sealed class MinioFileStorage : IFileStorage
|
|||||||
/// <returns>Строка вида <c>MinioFileStorage (endpoint: …; bucket: …)</c>.</returns>
|
/// <returns>Строка вида <c>MinioFileStorage (endpoint: …; bucket: …)</c>.</returns>
|
||||||
public override string ToString() => $"MinioFileStorage (endpoint: {_endpoint}; bucket: {_bucket})";
|
public override string ToString() => $"MinioFileStorage (endpoint: {_endpoint}; bucket: {_bucket})";
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
async Task<string> IFileStorage.PutAsync(
|
async Task<string> IFileStorage.PutAsync(
|
||||||
string objectKey,
|
string objectKey,
|
||||||
Stream content,
|
Stream content,
|
||||||
@@ -98,7 +94,6 @@ public sealed class MinioFileStorage : IFileStorage
|
|||||||
return objectKey;
|
return objectKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
async Task<Stream?> IFileStorage.GetAsync(string objectKey, CancellationToken ct)
|
async Task<Stream?> IFileStorage.GetAsync(string objectKey, CancellationToken ct)
|
||||||
{
|
{
|
||||||
MemoryStream buffer = new();
|
MemoryStream buffer = new();
|
||||||
@@ -128,7 +123,6 @@ public sealed class MinioFileStorage : IFileStorage
|
|||||||
return buffer;
|
return buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
async Task<FileMeta?> IFileStorage.StatAsync(string objectKey, CancellationToken ct)
|
async Task<FileMeta?> IFileStorage.StatAsync(string objectKey, CancellationToken ct)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -144,7 +138,6 @@ public sealed class MinioFileStorage : IFileStorage
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
async Task IFileStorage.DeleteAsync(string objectKey, CancellationToken ct)
|
async Task IFileStorage.DeleteAsync(string objectKey, CancellationToken ct)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -5,9 +5,6 @@ using Deal.Modules.Discovery.Application.Models;
|
|||||||
|
|
||||||
namespace Deal.Infrastructure.Persistence.Repositories;
|
namespace Deal.Infrastructure.Persistence.Repositories;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// EF-адаптер хранилища Discovery
|
|
||||||
/// </summary>
|
|
||||||
public sealed partial class DiscoveryStore : IDiscoveryStore
|
public sealed partial class DiscoveryStore : IDiscoveryStore
|
||||||
{
|
{
|
||||||
private readonly TenantDbContext _dbContext;
|
private readonly TenantDbContext _dbContext;
|
||||||
|
|||||||
@@ -9,9 +9,6 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
|
|
||||||
namespace Deal.Infrastructure.Persistence.Repositories;
|
namespace Deal.Infrastructure.Persistence.Repositories;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// EF-адаптер хранилища карточек и контейнеров
|
|
||||||
/// </summary>
|
|
||||||
public sealed partial class KanbanStore : ICardStore
|
public sealed partial class KanbanStore : ICardStore
|
||||||
{
|
{
|
||||||
private readonly TenantDbContext _dbContext;
|
private readonly TenantDbContext _dbContext;
|
||||||
|
|||||||
@@ -6,9 +6,6 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
|
|
||||||
namespace Deal.Infrastructure.Persistence.Repositories;
|
namespace Deal.Infrastructure.Persistence.Repositories;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// EF-адаптер хранилища лимитов ИИ-бюджета
|
|
||||||
/// </summary>
|
|
||||||
public sealed class TenantLimitStore : ITenantLimitStore
|
public sealed class TenantLimitStore : ITenantLimitStore
|
||||||
{
|
{
|
||||||
private readonly DealDbContext _dbContext;
|
private readonly DealDbContext _dbContext;
|
||||||
@@ -58,7 +55,6 @@ public sealed class TenantLimitStore : ITenantLimitStore
|
|||||||
_utcNow = utcNow;
|
_utcNow = utcNow;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
async Task<TenantLimitDto> ITenantLimitStore.GetOrCreateAsync(
|
async Task<TenantLimitDto> ITenantLimitStore.GetOrCreateAsync(
|
||||||
Guid tenantId,
|
Guid tenantId,
|
||||||
CancellationToken ct,
|
CancellationToken ct,
|
||||||
@@ -68,7 +64,6 @@ public sealed class TenantLimitStore : ITenantLimitStore
|
|||||||
return ToLimitDto(entity);
|
return ToLimitDto(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
async Task<BudgetStateDto> ITenantLimitStore.GetStateAsync(Guid tenantId, CancellationToken ct)
|
async Task<BudgetStateDto> ITenantLimitStore.GetStateAsync(Guid tenantId, CancellationToken ct)
|
||||||
{
|
{
|
||||||
TenantLimitEntity entity = await LoadOrCreateAsync(tenantId, _defaults, ct);
|
TenantLimitEntity entity = await LoadOrCreateAsync(tenantId, _defaults, ct);
|
||||||
@@ -76,7 +71,6 @@ public sealed class TenantLimitStore : ITenantLimitStore
|
|||||||
return await ToStateDtoAsync(entity, ct);
|
return await ToStateDtoAsync(entity, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
async Task<BudgetStateDto> ITenantLimitStore.AddUsageAsync(
|
async Task<BudgetStateDto> ITenantLimitStore.AddUsageAsync(
|
||||||
Guid tenantId,
|
Guid tenantId,
|
||||||
long tokens,
|
long tokens,
|
||||||
@@ -109,7 +103,6 @@ public sealed class TenantLimitStore : ITenantLimitStore
|
|||||||
return await ToStateDtoAsync(entity, ct);
|
return await ToStateDtoAsync(entity, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
async Task<BudgetStateDto> ITenantLimitStore.UpdateBudgetAsync(
|
async Task<BudgetStateDto> ITenantLimitStore.UpdateBudgetAsync(
|
||||||
Guid tenantId,
|
Guid tenantId,
|
||||||
long budgetTokens,
|
long budgetTokens,
|
||||||
@@ -132,7 +125,6 @@ public sealed class TenantLimitStore : ITenantLimitStore
|
|||||||
return await ToStateDtoAsync(entity, ct);
|
return await ToStateDtoAsync(entity, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
async Task<bool> ITenantLimitStore.TryMarkWarnedAsync(Guid tenantId, CancellationToken ct)
|
async Task<bool> ITenantLimitStore.TryMarkWarnedAsync(Guid tenantId, CancellationToken ct)
|
||||||
{
|
{
|
||||||
TenantLimitEntity entity = await LoadOrCreateAsync(tenantId, _defaults, ct);
|
TenantLimitEntity entity = await LoadOrCreateAsync(tenantId, _defaults, ct);
|
||||||
@@ -148,7 +140,6 @@ public sealed class TenantLimitStore : ITenantLimitStore
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
async Task<bool> ITenantLimitStore.TryMarkNotifiedExhaustedAsync(Guid tenantId, CancellationToken ct)
|
async Task<bool> ITenantLimitStore.TryMarkNotifiedExhaustedAsync(Guid tenantId, CancellationToken ct)
|
||||||
{
|
{
|
||||||
TenantLimitEntity entity = await LoadOrCreateAsync(tenantId, _defaults, ct);
|
TenantLimitEntity entity = await LoadOrCreateAsync(tenantId, _defaults, ct);
|
||||||
@@ -219,7 +210,6 @@ public sealed class TenantLimitStore : ITenantLimitStore
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
async Task<int> ITenantLimitStore.ResetExpiredPeriodsAsync(DateTimeOffset now, CancellationToken ct)
|
async Task<int> ITenantLimitStore.ResetExpiredPeriodsAsync(DateTimeOffset now, CancellationToken ct)
|
||||||
{
|
{
|
||||||
// Трогаем только строки с накоплениями (расход/флаги): строки без накоплений чистить нечего.
|
// Трогаем только строки с накоплениями (расход/флаги): строки без накоплений чистить нечего.
|
||||||
|
|||||||
@@ -4,9 +4,6 @@ using Deal.Modules.Settings.Application.Abstractions;
|
|||||||
|
|
||||||
namespace Deal.Infrastructure.Security;
|
namespace Deal.Infrastructure.Security;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// AES-256-GCM-шифр секретов
|
|
||||||
/// </summary>
|
|
||||||
public sealed class AesGcmSecretCipher : ISecretCipher
|
public sealed class AesGcmSecretCipher : ISecretCipher
|
||||||
{
|
{
|
||||||
// Префикс зашифрованного значения (маркер формата в хранилище).
|
// Префикс зашифрованного значения (маркер формата в хранилище).
|
||||||
@@ -39,7 +36,6 @@ public sealed class AesGcmSecretCipher : ISecretCipher
|
|||||||
_key = key;
|
_key = key;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
string ISecretCipher.Encrypt(string plainText)
|
string ISecretCipher.Encrypt(string plainText)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(plainText))
|
if (string.IsNullOrEmpty(plainText))
|
||||||
@@ -65,7 +61,6 @@ public sealed class AesGcmSecretCipher : ISecretCipher
|
|||||||
return EncryptedPrefix + Convert.ToBase64String(payload);
|
return EncryptedPrefix + Convert.ToBase64String(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
string ISecretCipher.Decrypt(string cipherText)
|
string ISecretCipher.Decrypt(string cipherText)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(cipherText) || !cipherText.StartsWith(EncryptedPrefix, StringComparison.Ordinal))
|
if (string.IsNullOrEmpty(cipherText) || !cipherText.StartsWith(EncryptedPrefix, StringComparison.Ordinal))
|
||||||
|
|||||||
@@ -3,9 +3,6 @@ using Deal.Modules.Discovery.Application.Abstractions;
|
|||||||
|
|
||||||
namespace Deal.Modules.Discovery.Application.Services;
|
namespace Deal.Modules.Discovery.Application.Services;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Потокобезопасная реализация <see cref="IDiscoverySearchErrorCounter"/>
|
|
||||||
/// </summary>
|
|
||||||
public sealed class DiscoverySearchErrorCounter : IDiscoverySearchErrorCounter
|
public sealed class DiscoverySearchErrorCounter : IDiscoverySearchErrorCounter
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -43,7 +40,6 @@ public sealed class DiscoverySearchErrorCounter : IDiscoverySearchErrorCounter
|
|||||||
_utcNow = utcNow;
|
_utcNow = utcNow;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
int IDiscoverySearchErrorCounter.Next(string taskId)
|
int IDiscoverySearchErrorCounter.Next(string taskId)
|
||||||
{
|
{
|
||||||
EvictExpired();
|
EvictExpired();
|
||||||
@@ -55,7 +51,6 @@ public sealed class DiscoverySearchErrorCounter : IDiscoverySearchErrorCounter
|
|||||||
return fresh.Count;
|
return fresh.Count;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
void IDiscoverySearchErrorCounter.Reset(string taskId)
|
void IDiscoverySearchErrorCounter.Reset(string taskId)
|
||||||
{
|
{
|
||||||
EvictExpired();
|
EvictExpired();
|
||||||
|
|||||||
@@ -3,14 +3,9 @@ using Isopoh.Cryptography.Argon2;
|
|||||||
|
|
||||||
namespace Deal.Modules.Tenants.Application.Services;
|
namespace Deal.Modules.Tenants.Application.Services;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Реализация <see cref="IPasswordHasher"/> на Argon2id.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class DefaultPasswordHasher : IPasswordHasher
|
public sealed class DefaultPasswordHasher : IPasswordHasher
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
string IPasswordHasher.Hash(string password) => Argon2.Hash(password);
|
||||||
public string Hash(string password) => Argon2.Hash(password);
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
bool IPasswordHasher.Verify(string password, string encodedHash) => Argon2.Verify(encodedHash, password);
|
||||||
public bool Verify(string password, string encodedHash) => Argon2.Verify(encodedHash, password);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
namespace Deal.SharedKernel.Resilience;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Повтор операции при транзиентном сбое.
|
||||||
|
/// </summary>
|
||||||
|
public static class RetryExecutor
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Выполняет операцию, повторяя её при транзиентном сбое с задержкой.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="operation">Операция (принимает токен отмены).</param>
|
||||||
|
/// <param name="retryCount">Число повторов после первой попытки.</param>
|
||||||
|
/// <param name="baseDelay">Базовая задержка; для повтора N — baseDelay * 2^N.</param>
|
||||||
|
/// <param name="shouldRetry">Предикат транзиентности сбоя.</param>
|
||||||
|
/// <param name="delayAsync">Пауза между попытками (в тестах — мгновенная).</param>
|
||||||
|
/// <param name="cancellationToken">Токен отмены.</param>
|
||||||
|
/// <returns>Результат первой успешной попытки.</returns>
|
||||||
|
public static async Task<TResult> ExecuteAsync<TResult>(
|
||||||
|
Func<CancellationToken, Task<TResult>> operation,
|
||||||
|
int retryCount,
|
||||||
|
TimeSpan baseDelay,
|
||||||
|
Func<Exception, bool> shouldRetry,
|
||||||
|
Func<TimeSpan, CancellationToken, Task> delayAsync,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(operation);
|
||||||
|
ArgumentOutOfRangeException.ThrowIfNegative(retryCount);
|
||||||
|
ArgumentNullException.ThrowIfNull(shouldRetry);
|
||||||
|
ArgumentNullException.ThrowIfNull(delayAsync);
|
||||||
|
|
||||||
|
for (int attempt = 0; ; attempt++)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await operation(cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception exception) when (attempt < retryCount
|
||||||
|
&& shouldRetry(exception)
|
||||||
|
&& !cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
await delayAsync(BackoffDelay(baseDelay, attempt), cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Задержка повтора с экспоненциальным ростом от базовой.
|
||||||
|
private static TimeSpan BackoffDelay(TimeSpan baseDelay, int attempt)
|
||||||
|
=> TimeSpan.FromMilliseconds(baseDelay.TotalMilliseconds * Math.Pow(2, attempt));
|
||||||
|
}
|
||||||
@@ -39,7 +39,7 @@ public sealed class DiscoveryWorkerSchedulerTests
|
|||||||
TestDiscoveryStore StoreB,
|
TestDiscoveryStore StoreB,
|
||||||
TestDiscoveryGateway GatewayA,
|
TestDiscoveryGateway GatewayA,
|
||||||
TestDiscoveryGateway GatewayB,
|
TestDiscoveryGateway GatewayB,
|
||||||
TenantContext TenantContext,
|
ITenantContext TenantContext,
|
||||||
ListLogger Logs);
|
ListLogger Logs);
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -91,7 +91,7 @@ public sealed class DiscoveryWorkerSchedulerTests
|
|||||||
private static Context CreateContext()
|
private static Context CreateContext()
|
||||||
{
|
{
|
||||||
var tenants = new TestTenantRepository(Tenant(TenantA), Tenant(TenantB));
|
var tenants = new TestTenantRepository(Tenant(TenantA), Tenant(TenantB));
|
||||||
var tenantContext = new TenantContext();
|
ITenantContext tenantContext = new TenantContext();
|
||||||
var storeA = new TestDiscoveryStore();
|
var storeA = new TestDiscoveryStore();
|
||||||
var storeB = new TestDiscoveryStore();
|
var storeB = new TestDiscoveryStore();
|
||||||
var settingsA = new TestSettingsStore();
|
var settingsA = new TestSettingsStore();
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ public sealed class MlOutboxFlushSchedulerTests
|
|||||||
{
|
{
|
||||||
var store = new TestMlLearningStore();
|
var store = new TestMlLearningStore();
|
||||||
SeedRows(store, count: 25, prefix: "a");
|
SeedRows(store, count: 25, prefix: "a");
|
||||||
var tenantContext = new TenantContext();
|
ITenantContext tenantContext = new TenantContext();
|
||||||
await using ServiceProvider provider = BuildProvider(
|
await using ServiceProvider provider = BuildProvider(
|
||||||
port,
|
port,
|
||||||
new TestTenantRepository(Tenant(TenantA)),
|
new TestTenantRepository(Tenant(TenantA)),
|
||||||
@@ -67,7 +67,7 @@ public sealed class MlOutboxFlushSchedulerTests
|
|||||||
service.TrainUnavailable = true;
|
service.TrainUnavailable = true;
|
||||||
var store = new TestMlLearningStore();
|
var store = new TestMlLearningStore();
|
||||||
SeedRows(store, count: 5, prefix: "a");
|
SeedRows(store, count: 5, prefix: "a");
|
||||||
var tenantContext = new TenantContext();
|
ITenantContext tenantContext = new TenantContext();
|
||||||
await using ServiceProvider provider = BuildProvider(
|
await using ServiceProvider provider = BuildProvider(
|
||||||
port,
|
port,
|
||||||
new TestTenantRepository(Tenant(TenantA)),
|
new TestTenantRepository(Tenant(TenantA)),
|
||||||
@@ -95,7 +95,7 @@ public sealed class MlOutboxFlushSchedulerTests
|
|||||||
SeedRows(storeA, count: 12, prefix: "a");
|
SeedRows(storeA, count: 12, prefix: "a");
|
||||||
var storeB = new TestMlLearningStore();
|
var storeB = new TestMlLearningStore();
|
||||||
SeedRows(storeB, count: 3, prefix: "b");
|
SeedRows(storeB, count: 3, prefix: "b");
|
||||||
var tenantContext = new TenantContext();
|
ITenantContext tenantContext = new TenantContext();
|
||||||
await using ServiceProvider provider = BuildProvider(
|
await using ServiceProvider provider = BuildProvider(
|
||||||
port,
|
port,
|
||||||
new TestTenantRepository(Tenant(TenantA), Tenant(TenantB)),
|
new TestTenantRepository(Tenant(TenantA), Tenant(TenantB)),
|
||||||
@@ -121,7 +121,7 @@ public sealed class MlOutboxFlushSchedulerTests
|
|||||||
{
|
{
|
||||||
var store = new TestMlLearningStore();
|
var store = new TestMlLearningStore();
|
||||||
SeedRows(store, count: 105, prefix: "a");
|
SeedRows(store, count: 105, prefix: "a");
|
||||||
var tenantContext = new TenantContext();
|
ITenantContext tenantContext = new TenantContext();
|
||||||
await using ServiceProvider provider = BuildProvider(
|
await using ServiceProvider provider = BuildProvider(
|
||||||
port,
|
port,
|
||||||
new TestTenantRepository(Tenant(TenantA)),
|
new TestTenantRepository(Tenant(TenantA)),
|
||||||
@@ -148,7 +148,7 @@ public sealed class MlOutboxFlushSchedulerTests
|
|||||||
private static ServiceProvider BuildProvider(
|
private static ServiceProvider BuildProvider(
|
||||||
int port,
|
int port,
|
||||||
TestTenantRepository tenants,
|
TestTenantRepository tenants,
|
||||||
TenantContext tenantContext,
|
ITenantContext tenantContext,
|
||||||
Dictionary<Guid, TestMlLearningStore> storesByTenant)
|
Dictionary<Guid, TestMlLearningStore> storesByTenant)
|
||||||
{
|
{
|
||||||
var services = new ServiceCollection();
|
var services = new ServiceCollection();
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using Deal.Infrastructure.Data;
|
|||||||
using Deal.Infrastructure.Integrations.Abstractions;
|
using Deal.Infrastructure.Integrations.Abstractions;
|
||||||
using Deal.Infrastructure.Integrations.Models;
|
using Deal.Infrastructure.Integrations.Models;
|
||||||
using Deal.Infrastructure.Integrations.Options;
|
using Deal.Infrastructure.Integrations.Options;
|
||||||
|
using Deal.Infrastructure.Integrations.Resilience;
|
||||||
using Deal.Infrastructure.Integrations.Services;
|
using Deal.Infrastructure.Integrations.Services;
|
||||||
using Deal.Modules.Kanban.Application.Models;
|
using Deal.Modules.Kanban.Application.Models;
|
||||||
using Deal.Modules.Settings.Application.Models;
|
using Deal.Modules.Settings.Application.Models;
|
||||||
@@ -91,7 +92,8 @@ public sealed class GrpcMlClientTests
|
|||||||
Assert.Null(result.Margin);
|
Assert.Null(result.Margin);
|
||||||
Assert.Empty(result.Terms);
|
Assert.Empty(result.Terms);
|
||||||
Assert.Null(result.Type);
|
Assert.Null(result.Type);
|
||||||
Assert.Single(service.RequestTenantIds);
|
// Недоступность транспорта повторяется — на сервер приходит первая попытка и повторы.
|
||||||
|
Assert.Equal(GrpcRetry.RetryCount + 1, service.RequestTenantIds.Count);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,7 +150,8 @@ public sealed class GrpcMlClientTests
|
|||||||
Assert.False(down.Reachable);
|
Assert.False(down.Reachable);
|
||||||
Assert.False(down.Service.Ready);
|
Assert.False(down.Service.Ready);
|
||||||
Assert.False(down.Stats.Reachable);
|
Assert.False(down.Stats.Reachable);
|
||||||
Assert.Equal(1, service.StatusCalls);
|
// При недоступности транспорта идёт повтор — считаем все попытки.
|
||||||
|
Assert.Equal(GrpcRetry.RetryCount + 1, service.StatusCalls);
|
||||||
|
|
||||||
// «Поднялся»: после TTL 15 с следующий StatusAsync обновляет кэш (ready=true, reachable=true).
|
// «Поднялся»: после TTL 15 с следующий StatusAsync обновляет кэш (ready=true, reachable=true).
|
||||||
service.StatusUnavailable = false;
|
service.StatusUnavailable = false;
|
||||||
@@ -165,7 +168,7 @@ public sealed class GrpcMlClientTests
|
|||||||
Assert.True(up.Reachable);
|
Assert.True(up.Reachable);
|
||||||
Assert.True(up.Service.Ready);
|
Assert.True(up.Service.Ready);
|
||||||
Assert.Equal(3, up.Service.Learned);
|
Assert.Equal(3, up.Service.Learned);
|
||||||
Assert.Equal(2, service.StatusCalls);
|
Assert.Equal(GrpcRetry.RetryCount + 2, service.StatusCalls);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using Deal.Grpc.Ai;
|
|||||||
using Deal.Infrastructure.Data;
|
using Deal.Infrastructure.Data;
|
||||||
using Deal.Infrastructure.Integrations.Models;
|
using Deal.Infrastructure.Integrations.Models;
|
||||||
using Deal.Infrastructure.Integrations.Options;
|
using Deal.Infrastructure.Integrations.Options;
|
||||||
|
using Deal.Infrastructure.Integrations.Resilience;
|
||||||
using Deal.Infrastructure.Integrations.Services;
|
using Deal.Infrastructure.Integrations.Services;
|
||||||
using Deal.Modules.Kanban.Application.Models;
|
using Deal.Modules.Kanban.Application.Models;
|
||||||
using Deal.Modules.Pipeline.Application.Models;
|
using Deal.Modules.Pipeline.Application.Models;
|
||||||
@@ -101,8 +102,9 @@ public sealed class PipelineWorkerGrpcAiTests
|
|||||||
Assert.Equal(1, result.AiFail);
|
Assert.Equal(1, result.AiFail);
|
||||||
Assert.Equal(1, result.AiStored);
|
Assert.Equal(1, result.AiStored);
|
||||||
Assert.Single(result.CreatedCards);
|
Assert.Single(result.CreatedCards);
|
||||||
Assert.Equal(1, service.FilterCalls);
|
// Недоступность транспорта повторяется — на сервер приходит первая попытка и повторы.
|
||||||
Assert.Equal(1, service.ClassifyCalls);
|
Assert.Equal(GrpcRetry.RetryCount + 1, service.FilterCalls);
|
||||||
|
Assert.Equal(GrpcRetry.RetryCount + 1, service.ClassifyCalls);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
using Deal.Infrastructure.Integrations.Resilience;
|
||||||
|
using Grpc.Core;
|
||||||
|
|
||||||
|
namespace Deal.Tests.Unit.Support;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Тесты <see cref="GrpcRetry"/> — повтор транзиентных gRPC-сбоев.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GrpcRetryTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData(StatusCode.Unavailable)]
|
||||||
|
[InlineData(StatusCode.DeadlineExceeded)]
|
||||||
|
public void IsTransient_TransportFailures_True(StatusCode statusCode)
|
||||||
|
{
|
||||||
|
Assert.True(GrpcRetry.IsTransient(new RpcException(new Status(statusCode, "сбой"))));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(StatusCode.NotFound)]
|
||||||
|
[InlineData(StatusCode.InvalidArgument)]
|
||||||
|
[InlineData(StatusCode.Internal)]
|
||||||
|
public void IsTransient_ApplicationFailures_False(StatusCode statusCode)
|
||||||
|
{
|
||||||
|
Assert.False(GrpcRetry.IsTransient(new RpcException(new Status(statusCode, "сбой"))));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsTransient_NonRpcException_False()
|
||||||
|
{
|
||||||
|
Assert.False(GrpcRetry.IsTransient(new InvalidOperationException("сбой")));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ExecuteAsync_TransientThenSuccess_Retries()
|
||||||
|
{
|
||||||
|
int calls = 0;
|
||||||
|
|
||||||
|
string result = await GrpcRetry.ExecuteAsync(
|
||||||
|
_ =>
|
||||||
|
{
|
||||||
|
calls++;
|
||||||
|
return calls < 2
|
||||||
|
? Task.FromException<string>(new RpcException(new Status(StatusCode.Unavailable, "down")))
|
||||||
|
: Task.FromResult("ok");
|
||||||
|
},
|
||||||
|
InstantDelayAsync,
|
||||||
|
CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.Equal("ok", result);
|
||||||
|
Assert.Equal(2, calls);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ExecuteAsync_TransientExhausted_ThrowsRpcException()
|
||||||
|
{
|
||||||
|
int calls = 0;
|
||||||
|
|
||||||
|
RpcException thrown = await Assert.ThrowsAsync<RpcException>(
|
||||||
|
() => GrpcRetry.ExecuteAsync(
|
||||||
|
_ =>
|
||||||
|
{
|
||||||
|
calls++;
|
||||||
|
return Task.FromException<string>(new RpcException(new Status(StatusCode.Unavailable, "down")));
|
||||||
|
},
|
||||||
|
InstantDelayAsync,
|
||||||
|
CancellationToken.None));
|
||||||
|
|
||||||
|
Assert.Equal(StatusCode.Unavailable, thrown.StatusCode);
|
||||||
|
Assert.Equal(GrpcRetry.RetryCount + 1, calls);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ExecuteAsync_ApplicationFailure_NotRetried()
|
||||||
|
{
|
||||||
|
int calls = 0;
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<RpcException>(
|
||||||
|
() => GrpcRetry.ExecuteAsync(
|
||||||
|
_ =>
|
||||||
|
{
|
||||||
|
calls++;
|
||||||
|
return Task.FromException<string>(new RpcException(new Status(StatusCode.InvalidArgument, "bad")));
|
||||||
|
},
|
||||||
|
InstantDelayAsync,
|
||||||
|
CancellationToken.None));
|
||||||
|
|
||||||
|
Assert.Equal(1, calls);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Task InstantDelayAsync(TimeSpan delay, CancellationToken cancellationToken)
|
||||||
|
=> Task.CompletedTask;
|
||||||
|
}
|
||||||
@@ -47,7 +47,7 @@ public sealed class PipelineWorkerSchedulerTests
|
|||||||
TestKanjStore KanjB,
|
TestKanjStore KanjB,
|
||||||
SseSubscription SubscriptionB,
|
SseSubscription SubscriptionB,
|
||||||
PipelinePumpGate PumpGate,
|
PipelinePumpGate PumpGate,
|
||||||
TenantContext TenantContext,
|
ITenantContext TenantContext,
|
||||||
ListLogger Logs);
|
ListLogger Logs);
|
||||||
|
|
||||||
// ─── Цикл: pump каждого тенанта в собственном scope + new_card ─────────
|
// ─── Цикл: pump каждого тенанта в собственном scope + new_card ─────────
|
||||||
@@ -149,7 +149,7 @@ public sealed class PipelineWorkerSchedulerTests
|
|||||||
private static Context CreateContext(bool withThrowingQueueReadA = false)
|
private static Context CreateContext(bool withThrowingQueueReadA = false)
|
||||||
{
|
{
|
||||||
var tenants = new TestTenantRepository(Tenant(TenantA), Tenant(TenantB));
|
var tenants = new TestTenantRepository(Tenant(TenantA), Tenant(TenantB));
|
||||||
var tenantContext = new TenantContext();
|
ITenantContext tenantContext = new TenantContext();
|
||||||
var pipelineA = new TestPipelineStore(throwOnList: withThrowingQueueReadA);
|
var pipelineA = new TestPipelineStore(throwOnList: withThrowingQueueReadA);
|
||||||
var pipelineB = new TestPipelineStore();
|
var pipelineB = new TestPipelineStore();
|
||||||
var kanjA = new TestKanjStore();
|
var kanjA = new TestKanjStore();
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
using Deal.SharedKernel.Resilience;
|
||||||
|
|
||||||
|
namespace Deal.Tests.Unit.Support;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Тесты <see cref="RetryExecutor"/> — повтор транзиентных сбоев.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class RetryExecutorTests
|
||||||
|
{
|
||||||
|
private const int RetryCount = 2;
|
||||||
|
private static readonly TimeSpan BaseDelay = TimeSpan.FromMilliseconds(10);
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task FirstAttemptSucceeds_NoRetry()
|
||||||
|
{
|
||||||
|
var delays = new List<TimeSpan>();
|
||||||
|
int calls = 0;
|
||||||
|
|
||||||
|
string result = await ExecuteAsync(
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
calls++;
|
||||||
|
return Task.FromResult("ok");
|
||||||
|
},
|
||||||
|
shouldRetry: _ => true,
|
||||||
|
delays);
|
||||||
|
|
||||||
|
Assert.Equal("ok", result);
|
||||||
|
Assert.Equal(1, calls);
|
||||||
|
Assert.Empty(delays);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task TransientFailureThenSuccess_RetriesUntilSuccess()
|
||||||
|
{
|
||||||
|
var delays = new List<TimeSpan>();
|
||||||
|
int calls = 0;
|
||||||
|
|
||||||
|
string result = await ExecuteAsync(
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
calls++;
|
||||||
|
return calls < 3
|
||||||
|
? throw new InvalidOperationException("транзиент")
|
||||||
|
: Task.FromResult("ok");
|
||||||
|
},
|
||||||
|
shouldRetry: _ => true,
|
||||||
|
delays);
|
||||||
|
|
||||||
|
Assert.Equal("ok", result);
|
||||||
|
Assert.Equal(3, calls);
|
||||||
|
Assert.Equal(2, delays.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RetriesExhausted_ThrowsLastFailure()
|
||||||
|
{
|
||||||
|
var delays = new List<TimeSpan>();
|
||||||
|
int calls = 0;
|
||||||
|
|
||||||
|
InvalidOperationException thrown = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||||
|
() => ExecuteAsync<string>(
|
||||||
|
async () =>
|
||||||
|
{
|
||||||
|
calls++;
|
||||||
|
await Task.Yield();
|
||||||
|
throw new InvalidOperationException($"сбой {calls}");
|
||||||
|
},
|
||||||
|
shouldRetry: _ => true,
|
||||||
|
delays));
|
||||||
|
|
||||||
|
Assert.Equal(3, calls); // первая попытка + 2 повтора
|
||||||
|
Assert.Equal("сбой 3", thrown.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task NonTransientFailure_NotRetried()
|
||||||
|
{
|
||||||
|
var delays = new List<TimeSpan>();
|
||||||
|
int calls = 0;
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<InvalidOperationException>(
|
||||||
|
() => ExecuteAsync<string>(
|
||||||
|
async () =>
|
||||||
|
{
|
||||||
|
calls++;
|
||||||
|
await Task.Yield();
|
||||||
|
throw new InvalidOperationException("не транзиент");
|
||||||
|
},
|
||||||
|
shouldRetry: _ => false,
|
||||||
|
delays));
|
||||||
|
|
||||||
|
Assert.Equal(1, calls);
|
||||||
|
Assert.Empty(delays);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Cancellation_DoesNotRetry()
|
||||||
|
{
|
||||||
|
var delays = new List<TimeSpan>();
|
||||||
|
int calls = 0;
|
||||||
|
using var cts = new CancellationTokenSource();
|
||||||
|
cts.Cancel();
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<InvalidOperationException>(
|
||||||
|
() => ExecuteAsync<string>(
|
||||||
|
async () =>
|
||||||
|
{
|
||||||
|
calls++;
|
||||||
|
await Task.Yield();
|
||||||
|
throw new InvalidOperationException("сбой");
|
||||||
|
},
|
||||||
|
shouldRetry: _ => true,
|
||||||
|
delays,
|
||||||
|
cts.Token));
|
||||||
|
|
||||||
|
Assert.Equal(1, calls);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Backoff_GrowsExponentially()
|
||||||
|
{
|
||||||
|
var delays = new List<TimeSpan>();
|
||||||
|
int calls = 0;
|
||||||
|
|
||||||
|
await ExecuteAsync(
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
calls++;
|
||||||
|
return calls < 3
|
||||||
|
? throw new InvalidOperationException("транзиент")
|
||||||
|
: Task.FromResult(1);
|
||||||
|
},
|
||||||
|
shouldRetry: _ => true,
|
||||||
|
delays);
|
||||||
|
|
||||||
|
Assert.Equal(2, delays.Count);
|
||||||
|
Assert.Equal(BaseDelay, delays[0]);
|
||||||
|
Assert.Equal(BaseDelay * 2, delays[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Task<TResult> ExecuteAsync<TResult>(
|
||||||
|
Func<Task<TResult>> operation,
|
||||||
|
Func<Exception, bool> shouldRetry,
|
||||||
|
List<TimeSpan> delays,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
=> RetryExecutor.ExecuteAsync(
|
||||||
|
_ => operation(),
|
||||||
|
RetryCount,
|
||||||
|
BaseDelay,
|
||||||
|
shouldRetry,
|
||||||
|
(delay, _) =>
|
||||||
|
{
|
||||||
|
delays.Add(delay);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
},
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
@@ -46,7 +46,7 @@ public sealed class StorageTickSchedulerTests
|
|||||||
TestKanjStore storeB = StoreWithExpiredInboxCard("l_b_old");
|
TestKanjStore storeB = StoreWithExpiredInboxCard("l_b_old");
|
||||||
var settings = new TestSettingsStore();
|
var settings = new TestSettingsStore();
|
||||||
var tenants = new TestTenantRepository(Tenant(TenantA), Tenant(TenantB));
|
var tenants = new TestTenantRepository(Tenant(TenantA), Tenant(TenantB));
|
||||||
var tenantContext = new TenantContext();
|
ITenantContext tenantContext = new TenantContext();
|
||||||
await using ServiceProvider provider = BuildProvider(tenants, tenantContext, storeA, storeB, settings);
|
await using ServiceProvider provider = BuildProvider(tenants, tenantContext, storeA, storeB, settings);
|
||||||
|
|
||||||
SseBroker broker = provider.GetRequiredService<SseBroker>();
|
SseBroker broker = provider.GetRequiredService<SseBroker>();
|
||||||
@@ -74,7 +74,7 @@ public sealed class StorageTickSchedulerTests
|
|||||||
var storeB = new TestKanjStore();
|
var storeB = new TestKanjStore();
|
||||||
storeB.SeedCard(Card("l_b_fresh", KanbanColumns.Inbox, ReceivedAtMsAgo(TimeSpan.FromHours(1))));
|
storeB.SeedCard(Card("l_b_fresh", KanbanColumns.Inbox, ReceivedAtMsAgo(TimeSpan.FromHours(1))));
|
||||||
var settings = new TestSettingsStore();
|
var settings = new TestSettingsStore();
|
||||||
var tenantContext = new TenantContext();
|
ITenantContext tenantContext = new TenantContext();
|
||||||
await using ServiceProvider provider = BuildProvider(
|
await using ServiceProvider provider = BuildProvider(
|
||||||
new TestTenantRepository(Tenant(TenantA), Tenant(TenantB)),
|
new TestTenantRepository(Tenant(TenantA), Tenant(TenantB)),
|
||||||
tenantContext,
|
tenantContext,
|
||||||
@@ -100,7 +100,7 @@ public sealed class StorageTickSchedulerTests
|
|||||||
{
|
{
|
||||||
// У тенанта A настройки падают (имитация сбоя схемы/БД) — тик A логирует ошибку, B обрабатывается.
|
// У тенанта A настройки падают (имитация сбоя схемы/БД) — тик A логирует ошибку, B обрабатывается.
|
||||||
TestKanjStore storeB = StoreWithExpiredInboxCard("l_b_old");
|
TestKanjStore storeB = StoreWithExpiredInboxCard("l_b_old");
|
||||||
var tenantContext = new TenantContext();
|
ITenantContext tenantContext = new TenantContext();
|
||||||
var settingsByTenant = new Dictionary<Guid, ISettingsStore>
|
var settingsByTenant = new Dictionary<Guid, ISettingsStore>
|
||||||
{
|
{
|
||||||
[TenantA] = new ThrowingSettingsStore(),
|
[TenantA] = new ThrowingSettingsStore(),
|
||||||
@@ -133,7 +133,7 @@ public sealed class StorageTickSchedulerTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task RunCycle_TenantListFailure_DoesNotThrow()
|
public async Task RunCycle_TenantListFailure_DoesNotThrow()
|
||||||
{
|
{
|
||||||
var tenantContext = new TenantContext();
|
ITenantContext tenantContext = new TenantContext();
|
||||||
await using ServiceProvider provider = BuildProvider(
|
await using ServiceProvider provider = BuildProvider(
|
||||||
new ThrowingTenantRepository(),
|
new ThrowingTenantRepository(),
|
||||||
tenantContext,
|
tenantContext,
|
||||||
@@ -152,7 +152,7 @@ public sealed class StorageTickSchedulerTests
|
|||||||
{
|
{
|
||||||
var kanjStore = new TestKanjStore();
|
var kanjStore = new TestKanjStore();
|
||||||
var settings = new TestSettingsStore();
|
var settings = new TestSettingsStore();
|
||||||
var tenantContext = new TenantContext();
|
ITenantContext tenantContext = new TenantContext();
|
||||||
var pipelineStoreA = new TestPipelineStore();
|
var pipelineStoreA = new TestPipelineStore();
|
||||||
long nowMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
long nowMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||||
pipelineStoreA.SeedRejected(Rejected("r_old_a", (long)(nowMs - TimeSpan.FromDays(4).TotalMilliseconds)));
|
pipelineStoreA.SeedRejected(Rejected("r_old_a", (long)(nowMs - TimeSpan.FromDays(4).TotalMilliseconds)));
|
||||||
@@ -186,7 +186,7 @@ public sealed class StorageTickSchedulerTests
|
|||||||
cardStoreA.SeedCard(HoldCard("c_a_future", title: "Будущий", reminderAtMs: NowMs() + 60_000));
|
cardStoreA.SeedCard(HoldCard("c_a_future", title: "Будущий", reminderAtMs: NowMs() + 60_000));
|
||||||
cardStoreA.SeedCard(HoldCard("c_a_work", stage: "work", title: "В работе", reminderAtMs: NowMs() - 60_000));
|
cardStoreA.SeedCard(HoldCard("c_a_work", stage: "work", title: "В работе", reminderAtMs: NowMs() - 60_000));
|
||||||
var cardStoreB = new TestKanjStore();
|
var cardStoreB = new TestKanjStore();
|
||||||
var tenantContext = new TenantContext();
|
ITenantContext tenantContext = new TenantContext();
|
||||||
await using ServiceProvider provider = BuildProvider(
|
await using ServiceProvider provider = BuildProvider(
|
||||||
new TestTenantRepository(Tenant(TenantA), Tenant(TenantB)).Repository,
|
new TestTenantRepository(Tenant(TenantA), Tenant(TenantB)).Repository,
|
||||||
tenantContext,
|
tenantContext,
|
||||||
@@ -221,7 +221,7 @@ public sealed class StorageTickSchedulerTests
|
|||||||
cardStoreA.SeedCard(HoldCard("c_past", title: "Старое", reminderAtMs: NowMs() - 60_000));
|
cardStoreA.SeedCard(HoldCard("c_past", title: "Старое", reminderAtMs: NowMs() - 60_000));
|
||||||
var settingsA = new TestSettingsStore();
|
var settingsA = new TestSettingsStore();
|
||||||
settingsA.Preload(SettingsKeys.RemindersEnabled, "false");
|
settingsA.Preload(SettingsKeys.RemindersEnabled, "false");
|
||||||
var tenantContext = new TenantContext();
|
ITenantContext tenantContext = new TenantContext();
|
||||||
await using ServiceProvider provider = BuildProvider(
|
await using ServiceProvider provider = BuildProvider(
|
||||||
new TestTenantRepository(Tenant(TenantA)).Repository,
|
new TestTenantRepository(Tenant(TenantA)).Repository,
|
||||||
tenantContext,
|
tenantContext,
|
||||||
@@ -247,7 +247,7 @@ public sealed class StorageTickSchedulerTests
|
|||||||
// и тик A завершается, тенант B обрабатывается (автоархив + тост) — проход жив.
|
// и тик A завершается, тенант B обрабатывается (автоархив + тост) — проход жив.
|
||||||
var cardStoreA = new TestKanjStore(throwOnDueReminders: true);
|
var cardStoreA = new TestKanjStore(throwOnDueReminders: true);
|
||||||
TestKanjStore storeB = StoreWithExpiredInboxCard("l_b_old");
|
TestKanjStore storeB = StoreWithExpiredInboxCard("l_b_old");
|
||||||
var tenantContext = new TenantContext();
|
ITenantContext tenantContext = new TenantContext();
|
||||||
await using ServiceProvider provider = BuildProvider(
|
await using ServiceProvider provider = BuildProvider(
|
||||||
new TestTenantRepository(Tenant(TenantA), Tenant(TenantB)).Repository,
|
new TestTenantRepository(Tenant(TenantA), Tenant(TenantB)).Repository,
|
||||||
tenantContext,
|
tenantContext,
|
||||||
@@ -280,7 +280,7 @@ public sealed class StorageTickSchedulerTests
|
|||||||
// Возвращает: Провайдер с зарегистрированными сервисами теста.
|
// Возвращает: Провайдер с зарегистрированными сервисами теста.
|
||||||
private static ServiceProvider BuildProvider(
|
private static ServiceProvider BuildProvider(
|
||||||
TestTenantRepository tenants,
|
TestTenantRepository tenants,
|
||||||
TenantContext tenantContext,
|
ITenantContext tenantContext,
|
||||||
TestKanjStore storeA,
|
TestKanjStore storeA,
|
||||||
TestKanjStore storeB,
|
TestKanjStore storeB,
|
||||||
TestSettingsStore settings)
|
TestSettingsStore settings)
|
||||||
@@ -301,7 +301,7 @@ public sealed class StorageTickSchedulerTests
|
|||||||
// Возвращает: Провайдер с зарегистрированными сервисами теста.
|
// Возвращает: Провайдер с зарегистрированными сервисами теста.
|
||||||
private static ServiceProvider BuildProvider(
|
private static ServiceProvider BuildProvider(
|
||||||
ITenantRepository tenants,
|
ITenantRepository tenants,
|
||||||
TenantContext tenantContext,
|
ITenantContext tenantContext,
|
||||||
Dictionary<Guid, TestKanjStore> storesByTenant,
|
Dictionary<Guid, TestKanjStore> storesByTenant,
|
||||||
Dictionary<Guid, ISettingsStore> settingsByTenant,
|
Dictionary<Guid, ISettingsStore> settingsByTenant,
|
||||||
Dictionary<Guid, TestPipelineStore>? pipelineStoresByTenant = null)
|
Dictionary<Guid, TestPipelineStore>? pipelineStoresByTenant = null)
|
||||||
|
|||||||
@@ -11,9 +11,6 @@ using Deal.Telegram.Core;
|
|||||||
|
|
||||||
namespace Deal.Telegram.Core;
|
namespace Deal.Telegram.Core;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Исходящий gRPC-канал в ядро
|
|
||||||
/// </summary>
|
|
||||||
public sealed class CoreIngressClient : ICoreIngressClient
|
public sealed class CoreIngressClient : ICoreIngressClient
|
||||||
{
|
{
|
||||||
public const string TenantIdMetadataKey = "tenant-id";
|
public const string TenantIdMetadataKey = "tenant-id";
|
||||||
@@ -44,7 +41,6 @@ public sealed class CoreIngressClient : ICoreIngressClient
|
|||||||
_mtlsCertificates = mtlsCertificates;
|
_mtlsCertificates = mtlsCertificates;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
async Task<PushSourceReply> ICoreIngressClient.PushSourceAsync(
|
async Task<PushSourceReply> ICoreIngressClient.PushSourceAsync(
|
||||||
string tenantId,
|
string tenantId,
|
||||||
PushSourceRequest request,
|
PushSourceRequest request,
|
||||||
@@ -61,7 +57,6 @@ public sealed class CoreIngressClient : ICoreIngressClient
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
async Task<IReadOnlyList<string>> ICoreIngressClient.SyncDialogsAsync(
|
async Task<IReadOnlyList<string>> ICoreIngressClient.SyncDialogsAsync(
|
||||||
string tenantId,
|
string tenantId,
|
||||||
IReadOnlyList<DialogEntry> entries,
|
IReadOnlyList<DialogEntry> entries,
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
using Deal.Telegram.Dialogs;
|
using Deal.Telegram.Dialogs;
|
||||||
namespace Deal.Telegram.Dialogs;
|
namespace Deal.Telegram.Dialogs;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Реальная реализация <see cref="IBackfillPacer"/>
|
|
||||||
/// </summary>
|
|
||||||
public sealed class RandomBackfillPacer : IBackfillPacer
|
public sealed class RandomBackfillPacer : IBackfillPacer
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
async Task IBackfillPacer.WaitAsync(
|
async Task IBackfillPacer.WaitAsync(
|
||||||
double minSeconds,
|
double minSeconds,
|
||||||
double maxSeconds,
|
double maxSeconds,
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
using Deal.Telegram.Telegram;
|
using Deal.Telegram.Telegram;
|
||||||
namespace Deal.Telegram.Telegram;
|
namespace Deal.Telegram.Telegram;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Фабрика реальных клиентов WTelegramClient.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class ClientFactory : ITelegramClientFactory
|
public sealed class ClientFactory : ITelegramClientFactory
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
ISessionClient ITelegramClientFactory.Create(
|
ISessionClient ITelegramClientFactory.Create(
|
||||||
int apiId,
|
int apiId,
|
||||||
string apiHash,
|
string apiHash,
|
||||||
|
|||||||
@@ -11,9 +11,6 @@ namespace Deal.Telegram.Telegram;
|
|||||||
|
|
||||||
#pragma warning disable CS0618 // Auth_SendCode/Auth_SignIn используются осознанно: ручной веб-вход 1:1 с прототипом
|
#pragma warning disable CS0618 // Auth_SendCode/Auth_SignIn используются осознанно: ручной веб-вход 1:1 с прототипом
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Реальная реализация <see cref="ISessionClient"/> поверх WTelegramClient.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class WTelegramSessionClient : ISessionClient
|
public sealed class WTelegramSessionClient : ISessionClient
|
||||||
{
|
{
|
||||||
private readonly Client _client;
|
private readonly Client _client;
|
||||||
@@ -67,25 +64,19 @@ public sealed class WTelegramSessionClient : ISessionClient
|
|||||||
_updateManager = new UpdateManager(_client, OnSingleUpdateAsync);
|
_updateManager = new UpdateManager(_client, OnSingleUpdateAsync);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
bool ISessionClient.IsAuthorized => _client.UserId != 0;
|
||||||
public bool IsAuthorized => _client.UserId != 0;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
bool ISessionClient.IsConnected => _connected && !_client.Disconnected;
|
||||||
public bool IsConnected => _connected && !_client.Disconnected;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
int ISessionClient.ApiId => _apiId;
|
||||||
public int ApiId => _apiId;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
string ISessionClient.ApiHash => _apiHash;
|
||||||
public string ApiHash => _apiHash;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
byte[]? ISessionClient.SessionBytes => Volatile.Read(ref _latestSessionBytes);
|
||||||
public byte[]? SessionBytes => Volatile.Read(ref _latestSessionBytes);
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
async Task ISessionClient.ConnectAsync(CancellationToken cancellationToken)
|
async Task ISessionClient.ConnectAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (IsConnected)
|
if (((ISessionClient)this).IsConnected)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -94,7 +85,6 @@ public sealed class WTelegramSessionClient : ISessionClient
|
|||||||
_connected = true;
|
_connected = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
async Task ISessionClient.RequestCodeAsync(string phone, CancellationToken cancellationToken)
|
async Task ISessionClient.RequestCodeAsync(string phone, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
_phone = phone;
|
_phone = phone;
|
||||||
@@ -118,7 +108,6 @@ public sealed class WTelegramSessionClient : ISessionClient
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
async Task<string?> ISessionClient.SubmitCodeAsync(string code, CancellationToken cancellationToken)
|
async Task<string?> ISessionClient.SubmitCodeAsync(string code, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (_phoneAlreadyAuthorized)
|
if (_phoneAlreadyAuthorized)
|
||||||
@@ -158,7 +147,6 @@ public sealed class WTelegramSessionClient : ISessionClient
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
async Task ISessionClient.SubmitPasswordAsync(string password, CancellationToken cancellationToken)
|
async Task ISessionClient.SubmitPasswordAsync(string password, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -178,7 +166,6 @@ public sealed class WTelegramSessionClient : ISessionClient
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
async Task ISessionClient.StartQrAsync(Action<string> onQrUrl, CancellationToken cancellationToken)
|
async Task ISessionClient.StartQrAsync(Action<string> onQrUrl, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -196,13 +183,11 @@ public sealed class WTelegramSessionClient : ISessionClient
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
async Task ISessionClient.LogOutAsync(CancellationToken cancellationToken)
|
async Task ISessionClient.LogOutAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
await _client.Auth_LogOut().WaitAsync(cancellationToken).ConfigureAwait(false);
|
await _client.Auth_LogOut().WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
async Task<string> ISessionClient.GetAccountAsync(CancellationToken cancellationToken)
|
async Task<string> ISessionClient.GetAccountAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
UserBase[] users = await _client.Users_GetUsers(InputUser.Self).WaitAsync(cancellationToken).ConfigureAwait(false);
|
UserBase[] users = await _client.Users_GetUsers(InputUser.Self).WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||||
@@ -221,7 +206,6 @@ public sealed class WTelegramSessionClient : ISessionClient
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public event Func<TelegramMessage, Task>? MessageReceived;
|
public event Func<TelegramMessage, Task>? MessageReceived;
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
async Task<IReadOnlyList<TelegramDialog>> ISessionClient.GetDialogsAsync(int limit, CancellationToken cancellationToken)
|
async Task<IReadOnlyList<TelegramDialog>> ISessionClient.GetDialogsAsync(int limit, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
Messages_DialogsBase result = await RunTlCallAsync(() => _client.Messages_GetDialogs(limit: limit), cancellationToken).ConfigureAwait(false);
|
Messages_DialogsBase result = await RunTlCallAsync(() => _client.Messages_GetDialogs(limit: limit), cancellationToken).ConfigureAwait(false);
|
||||||
@@ -241,7 +225,6 @@ public sealed class WTelegramSessionClient : ISessionClient
|
|||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
async Task<IReadOnlyList<TelegramMessage>> ISessionClient.GetMessagesAsync(
|
async Task<IReadOnlyList<TelegramMessage>> ISessionClient.GetMessagesAsync(
|
||||||
string dialogId,
|
string dialogId,
|
||||||
int limit,
|
int limit,
|
||||||
@@ -265,7 +248,6 @@ public sealed class WTelegramSessionClient : ISessionClient
|
|||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
async Task<TelegramMessage?> ISessionClient.GetMessageAsync(
|
async Task<TelegramMessage?> ISessionClient.GetMessageAsync(
|
||||||
string dialogId,
|
string dialogId,
|
||||||
long msgId,
|
long msgId,
|
||||||
@@ -295,7 +277,6 @@ public sealed class WTelegramSessionClient : ISessionClient
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
async Task ISessionClient.MarkReadAsync(string dialogId, CancellationToken cancellationToken)
|
async Task ISessionClient.MarkReadAsync(string dialogId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
InputPeer peer = await ResolvePeerAsync(dialogId, cancellationToken).ConfigureAwait(false);
|
InputPeer peer = await ResolvePeerAsync(dialogId, cancellationToken).ConfigureAwait(false);
|
||||||
@@ -303,7 +284,6 @@ public sealed class WTelegramSessionClient : ISessionClient
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
async Task<IReadOnlyList<TelegramDialog>> ISessionClient.SearchAsync(
|
async Task<IReadOnlyList<TelegramDialog>> ISessionClient.SearchAsync(
|
||||||
string query,
|
string query,
|
||||||
int limit,
|
int limit,
|
||||||
@@ -327,7 +307,6 @@ public sealed class WTelegramSessionClient : ISessionClient
|
|||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
async Task<TelegramSourceInfo> ISessionClient.GetInfoAsync(string dialogId, CancellationToken cancellationToken)
|
async Task<TelegramSourceInfo> ISessionClient.GetInfoAsync(string dialogId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
TelegramSourceInfo unknown = DefaultSourceInfo(dialogId);
|
TelegramSourceInfo unknown = DefaultSourceInfo(dialogId);
|
||||||
@@ -361,7 +340,6 @@ public sealed class WTelegramSessionClient : ISessionClient
|
|||||||
return unknown;
|
return unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
async Task<DiscoveryReadResult> ISessionClient.ReadForEvalAsync(
|
async Task<DiscoveryReadResult> ISessionClient.ReadForEvalAsync(
|
||||||
string dialogId,
|
string dialogId,
|
||||||
int limit,
|
int limit,
|
||||||
@@ -410,7 +388,6 @@ public sealed class WTelegramSessionClient : ISessionClient
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
async Task ISessionClient.JoinAsync(string username, CancellationToken cancellationToken)
|
async Task ISessionClient.JoinAsync(string username, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
Contacts_ResolvedPeer resolved = await RunTlCallAsync(() => _client.Contacts_ResolveUsername(username), cancellationToken).ConfigureAwait(false);
|
Contacts_ResolvedPeer resolved = await RunTlCallAsync(() => _client.Contacts_ResolveUsername(username), cancellationToken).ConfigureAwait(false);
|
||||||
@@ -425,7 +402,6 @@ public sealed class WTelegramSessionClient : ISessionClient
|
|||||||
await RunTlCallAsync(() => _client.Channels_JoinChannel(new InputChannel(channel.id, channel.access_hash)), cancellationToken).ConfigureAwait(false);
|
await RunTlCallAsync(() => _client.Channels_JoinChannel(new InputChannel(channel.id, channel.access_hash)), cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
async Task ISessionClient.LeaveAsync(string dialogId, CancellationToken cancellationToken)
|
async Task ISessionClient.LeaveAsync(string dialogId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (!TryParseSignedId(dialogId, out bool isChannel, out _, out _, out long rawId) || !isChannel)
|
if (!TryParseSignedId(dialogId, out bool isChannel, out _, out _, out long rawId) || !isChannel)
|
||||||
|
|||||||
Reference in New Issue
Block a user