Отформатировать списки параметров по код-стайлу
Больше двух параметров — каждый на отдельной строке (закрывающая скобка в конце последнего); два и меньше — в одну строку. Правило добавлено в docs/spec/Код-стайл-Дейл.md; применено к 628 сигнатурам в 253 файлах.
This commit is contained in:
@@ -165,6 +165,21 @@
|
||||
- Функции, возвращающие массив/коллекцию, всегда возвращают массив/коллекцию: если данных нет — пустой
|
||||
экземпляр, но не `null`.
|
||||
- Не более 7 параметров у функции. Больше — объединять в класс/DTO.
|
||||
- **Перенос параметров:** если параметров **больше двух** — каждый на **отдельной строке** (открывающая `(` — в конце первой строки, закрывающая `)` — на отдельной строке с отступом объявления); если **два или меньше** — все параметры **в одну строку**.
|
||||
|
||||
Больше двух:
|
||||
```csharp
|
||||
public async Task<CardMoveResultDto> MoveAsync(
|
||||
string cardId,
|
||||
string toContainerId,
|
||||
TransitionContext ctx,
|
||||
CancellationToken ct)
|
||||
```
|
||||
|
||||
Два или меньше:
|
||||
```csharp
|
||||
public User FindUser(string login, CancellationToken ct) { }
|
||||
```
|
||||
|
||||
## 8. Управление выполнением программы
|
||||
|
||||
|
||||
@@ -128,7 +128,10 @@ public sealed class AiServiceHostTests
|
||||
// serviceToken: Ожидаемый токен хоста (env DEAL_SERVICE_TOKEN).
|
||||
// tokenHeader: Значение metadata «service-token» запроса либо null (нет заголовка).
|
||||
// expected: Ожидаемый StatusCode ответа.
|
||||
private static async Task AssertDealRpcRejectedAsync(string? serviceToken, string? tokenHeader, StatusCode expected)
|
||||
private static async Task AssertDealRpcRejectedAsync(
|
||||
string? serviceToken,
|
||||
string? tokenHeader,
|
||||
StatusCode expected)
|
||||
{
|
||||
await RunHostScenarioAsync(
|
||||
serviceToken,
|
||||
@@ -141,7 +144,10 @@ public sealed class AiServiceHostTests
|
||||
// channel: Канал к хосту ai-service.
|
||||
// tokenHeader: Значение metadata «service-token» либо null (нет заголовка).
|
||||
// expected: Ожидаемый StatusCode.
|
||||
private static async Task AssertRejectedAsync(GrpcChannel channel, string? tokenHeader, StatusCode expected)
|
||||
private static async Task AssertRejectedAsync(
|
||||
GrpcChannel channel,
|
||||
string? tokenHeader,
|
||||
StatusCode expected)
|
||||
{
|
||||
var client = new AiService.AiServiceClient(channel);
|
||||
Metadata metadata = new() { { "tenant-id", TenantId } };
|
||||
|
||||
@@ -52,9 +52,7 @@ internal static class AiTestHost
|
||||
/// </summary>
|
||||
/// <param name="configureServices">Настройка DI сценария (фейк-провайдер, мгновенные ретраи).</param>
|
||||
/// <param name="scenario">Сценарий с gRPC-каналом к хосту.</param>
|
||||
public static Task RunAsync(
|
||||
Action<IServiceCollection> configureServices,
|
||||
Func<GrpcChannel, Task> scenario)
|
||||
public static Task RunAsync(Action<IServiceCollection> configureServices, Func<GrpcChannel, Task> scenario)
|
||||
=> RunAsync(DefaultToken, configureServices, scenario);
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -26,9 +26,7 @@ internal sealed class StubHttpMessageHandler : HttpMessageHandler
|
||||
/// </summary>
|
||||
/// <param name="responder">Ответчик: request → response.</param>
|
||||
/// <param name="delay">Необязательная задержка ответа (тест таймаута).</param>
|
||||
public StubHttpMessageHandler(
|
||||
Func<HttpRequestMessage, HttpResponseMessage> responder,
|
||||
TimeSpan? delay = null)
|
||||
public StubHttpMessageHandler(Func<HttpRequestMessage, HttpResponseMessage> responder, TimeSpan? delay = null)
|
||||
{
|
||||
_responder = responder;
|
||||
_delay = delay;
|
||||
@@ -45,9 +43,7 @@ internal sealed class StubHttpMessageHandler : HttpMessageHandler
|
||||
public CapturedHttpRequest LastRequest => Requests[^1];
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task<HttpResponseMessage> SendAsync(
|
||||
HttpRequestMessage request,
|
||||
CancellationToken cancellationToken)
|
||||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
string? body = request.Content is null
|
||||
? null
|
||||
|
||||
@@ -239,8 +239,7 @@ public sealed class AiServiceImpl : AiService.AiServiceBase
|
||||
/// L36–47 + описание): ответ {keywords}. Очистку (_clean_keywords: ≤30, ≤60 симв., дедуп) и мягкую
|
||||
/// ошибку для UI делает ядро (Ruling 11); недоступность провайдера — UNAVAILABLE.
|
||||
/// </summary>
|
||||
public override async Task<GenerateKeywordsReply> GenerateKeywords(
|
||||
GenerateKeywordsRequest request, ServerCallContext context)
|
||||
public override async Task<GenerateKeywordsReply> GenerateKeywords(GenerateKeywordsRequest request, ServerCallContext context)
|
||||
{
|
||||
string tenantId = RequireTenantId(context);
|
||||
EnsureLengthAtMost(request.Description, MaxDescriptionLength, DescriptionTooLongDetail);
|
||||
@@ -277,8 +276,7 @@ public sealed class AiServiceImpl : AiService.AiServiceBase
|
||||
/// текст + описание + ключи задачи): ответ {fit, reason}. Ядро зовёт только при aiEnabled;
|
||||
/// сбой — фолбэк на эвристику (Ruling 10).
|
||||
/// </summary>
|
||||
public override async Task<EvaluateFitReply> EvaluateFit(
|
||||
EvaluateFitRequest request, ServerCallContext context)
|
||||
public override async Task<EvaluateFitReply> EvaluateFit(EvaluateFitRequest request, ServerCallContext context)
|
||||
{
|
||||
string tenantId = RequireTenantId(context);
|
||||
EnsureLengthAtMost(request.Text, MaxTextLength, MessageTextTooLongDetail);
|
||||
@@ -333,7 +331,10 @@ public sealed class AiServiceImpl : AiService.AiServiceBase
|
||||
// value: Значение поля запроса (в proto строка не бывает null).
|
||||
// maxLength: Допустимый максимум символов.
|
||||
// detail: Текст отказа (detail RPC).
|
||||
private static void EnsureLengthAtMost(string value, int maxLength, string detail)
|
||||
private static void EnsureLengthAtMost(
|
||||
string value,
|
||||
int maxLength,
|
||||
string detail)
|
||||
{
|
||||
if (value.Length > maxLength)
|
||||
{
|
||||
@@ -393,7 +394,11 @@ public sealed class AiServiceImpl : AiService.AiServiceBase
|
||||
// tenantId: Id тенанта.
|
||||
// config: Конфиг провайдера вызова.
|
||||
// callError: Итоговая ошибка фасада.
|
||||
private void LogAiUnavailable(string method, string tenantId, LlmConfig config, LlmCallException callError)
|
||||
private void LogAiUnavailable(
|
||||
string method,
|
||||
string tenantId,
|
||||
LlmConfig config,
|
||||
LlmCallException callError)
|
||||
=> _logger.LogWarning(
|
||||
"Аудит: tenant {TenantId} {Method} → ИИ ({Provider}) недоступен ({Kind})",
|
||||
tenantId,
|
||||
@@ -412,7 +417,10 @@ public sealed class AiServiceImpl : AiService.AiServiceBase
|
||||
// json: Корневой объект ответа модели.
|
||||
// fieldName: Имя поля.
|
||||
// defaultValue: Значение при отсутствии/неразбираемости поля.
|
||||
private static bool ReadBoolField(JsonObject json, string fieldName, bool defaultValue)
|
||||
private static bool ReadBoolField(
|
||||
JsonObject json,
|
||||
string fieldName,
|
||||
bool defaultValue)
|
||||
{
|
||||
if (json[fieldName] is not JsonValue value)
|
||||
{
|
||||
|
||||
@@ -16,7 +16,10 @@ public sealed class LlmCallException : Exception
|
||||
/// <param name="kind">Причина исчерпания (см. <see cref="LlmCallFailureKind"/>).</param>
|
||||
/// <param name="providerDisplayName">Отображаемое имя провайдера (без ключей).</param>
|
||||
/// <param name="usage">Usage последней ответившей попытки (для Kind=AnswerNotJson; иначе null).</param>
|
||||
public LlmCallException(LlmCallFailureKind kind, string providerDisplayName, LlmUsage? usage = null)
|
||||
public LlmCallException(
|
||||
LlmCallFailureKind kind,
|
||||
string providerDisplayName,
|
||||
LlmUsage? usage = null)
|
||||
: base(string.Format(DetailTemplate, providerDisplayName))
|
||||
{
|
||||
Kind = kind;
|
||||
|
||||
@@ -58,7 +58,10 @@ public sealed class LlmHttpClient : IProviderClient
|
||||
// httpClient: HttpClient над фейковым обработчиком.
|
||||
// openAiCallTimeout: Таймаут OpenAI-совместимой попытки.
|
||||
// anthropicCallTimeout: Таймаут Anthropic-попытки.
|
||||
internal LlmHttpClient(HttpClient httpClient, TimeSpan openAiCallTimeout, TimeSpan anthropicCallTimeout)
|
||||
internal LlmHttpClient(
|
||||
HttpClient httpClient,
|
||||
TimeSpan openAiCallTimeout,
|
||||
TimeSpan anthropicCallTimeout)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_openAiCallTimeout = openAiCallTimeout;
|
||||
@@ -110,7 +113,10 @@ public sealed class LlmHttpClient : IProviderClient
|
||||
// config: Конфиг провайдера.
|
||||
// systemPrompt: Системный промпт.
|
||||
// userText: Пользовательское сообщение.
|
||||
private static HttpRequestMessage BuildOpenAiRequest(LlmConfig config, string systemPrompt, string userText)
|
||||
private static HttpRequestMessage BuildOpenAiRequest(
|
||||
LlmConfig config,
|
||||
string systemPrompt,
|
||||
string userText)
|
||||
{
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, NormalizeBaseUrl(config.BaseUrl) + OpenAiCompletionsPath);
|
||||
if (!string.IsNullOrEmpty(config.ApiKey))
|
||||
@@ -137,7 +143,10 @@ public sealed class LlmHttpClient : IProviderClient
|
||||
// config: Конфиг провайдера.
|
||||
// systemPrompt: Системный промпт.
|
||||
// userText: Пользовательское сообщение.
|
||||
private static HttpRequestMessage BuildAnthropicRequest(LlmConfig config, string systemPrompt, string userText)
|
||||
private static HttpRequestMessage BuildAnthropicRequest(
|
||||
LlmConfig config,
|
||||
string systemPrompt,
|
||||
string userText)
|
||||
{
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, NormalizeBaseUrl(config.BaseUrl) + AnthropicMessagesPath);
|
||||
request.Headers.TryAddWithoutValidation(AnthropicApiKeyHeader, config.ApiKey ?? string.Empty);
|
||||
|
||||
@@ -16,7 +16,10 @@ public static class TokenEstimator
|
||||
/// <param name="providerUsage">Usage из API-ответа (null — провайдер его не вернул).</param>
|
||||
/// <param name="promptText">Полный текст запроса (система + пользователь) для оценки.</param>
|
||||
/// <param name="completionText">Текст ответа модели для оценки.</param>
|
||||
public static LlmUsage Resolve(ProviderUsage? providerUsage, string promptText, string completionText)
|
||||
public static LlmUsage Resolve(
|
||||
ProviderUsage? providerUsage,
|
||||
string promptText,
|
||||
string completionText)
|
||||
{
|
||||
if (providerUsage is not null)
|
||||
{
|
||||
|
||||
@@ -128,7 +128,10 @@ public static class AiCheckEndpoint
|
||||
// providerId: Активный провайдер (id из каталога).
|
||||
// ct: Токен отмены.
|
||||
// Возвращает: Конфиг {apiKey, baseUrl, model}; повреждённая строка aiConfigs — дефолт.
|
||||
private static async Task<AiConfigSetting> ReadEffectiveConfigAsync(ISettingsStore store, string providerId, CancellationToken ct)
|
||||
private static async Task<AiConfigSetting> ReadEffectiveConfigAsync(
|
||||
ISettingsStore store,
|
||||
string providerId,
|
||||
CancellationToken ct)
|
||||
{
|
||||
AiConfigSetting defaults = SettingsDefaults.AiConfigs[providerId];
|
||||
SettingValue? row = await store.GetAsync(SettingsKeys.AiConfigs, ct);
|
||||
|
||||
@@ -117,7 +117,10 @@ public static class CardDetailsEndpoints
|
||||
}
|
||||
|
||||
// POST /api/cards: ручное создание «локальной» карточки. Ответ — созданная карточка.
|
||||
private static async Task<IResult> CreateCardAsync(CreateCardRequest body, HttpContext context, CancellationToken ct)
|
||||
private static async Task<IResult> CreateCardAsync(
|
||||
CreateCardRequest body,
|
||||
HttpContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!context.HasUser())
|
||||
{
|
||||
@@ -133,7 +136,10 @@ public static class CardDetailsEndpoints
|
||||
}
|
||||
|
||||
// POST /api/cards/take {cardId}: «взять в работу» — перенос карточки в planned. Ответ — карточка.
|
||||
private static async Task<IResult> TakeCardAsync(TakeCardRequest body, HttpContext context, CancellationToken ct)
|
||||
private static async Task<IResult> TakeCardAsync(
|
||||
TakeCardRequest body,
|
||||
HttpContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!context.HasUser())
|
||||
{
|
||||
@@ -163,7 +169,10 @@ public static class CardDetailsEndpoints
|
||||
// PATCH /api/cards/{cardId}: точечная правка полей (title/summary/contact/tzText/stack/budget).
|
||||
// Тело читается как произвольный JSON-объект (presence-aware): явный null чистящих полей
|
||||
// (budget:null, stack:null) не теряется типизированным биндингом. Ответ — обновлённая карточка.
|
||||
private static async Task<IResult> PatchCardAsync(string cardId, HttpContext context, CancellationToken ct)
|
||||
private static async Task<IResult> PatchCardAsync(
|
||||
string cardId,
|
||||
HttpContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!context.HasUser())
|
||||
{
|
||||
@@ -184,7 +193,11 @@ public static class CardDetailsEndpoints
|
||||
}
|
||||
|
||||
// POST /api/cards/{cardId}/links {name?,url}: добавить ссылку. Ответ — карточка.
|
||||
private static async Task<IResult> AddLinkAsync(string cardId, CardLinkRequest body, HttpContext context, CancellationToken ct)
|
||||
private static async Task<IResult> AddLinkAsync(
|
||||
string cardId,
|
||||
CardLinkRequest body,
|
||||
HttpContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!context.HasUser())
|
||||
{
|
||||
@@ -208,7 +221,11 @@ public static class CardDetailsEndpoints
|
||||
}
|
||||
|
||||
// DELETE /api/cards/{cardId}/links/{linkId}: удалить ссылку. Ответ — карточка.
|
||||
private static async Task<IResult> RemoveLinkAsync(string cardId, string linkId, HttpContext context, CancellationToken ct)
|
||||
private static async Task<IResult> RemoveLinkAsync(
|
||||
string cardId,
|
||||
string linkId,
|
||||
HttpContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!context.HasUser())
|
||||
{
|
||||
@@ -225,7 +242,10 @@ public static class CardDetailsEndpoints
|
||||
// POST /api/cards/{cardId}/files: загрузка вложений (multipart/form-data, поле files).
|
||||
// Ответ — обновлённая карточка (с новыми files). Карточки нет → 404 до записи объектов. Каждый файл:
|
||||
// имя/ContentType/поток/длина → CardsService.AddFileAsync. Ранний null — гонка (404).
|
||||
private static async Task<IResult> UploadFilesAsync(string cardId, HttpContext context, CancellationToken ct)
|
||||
private static async Task<IResult> UploadFilesAsync(
|
||||
string cardId,
|
||||
HttpContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!context.HasUser())
|
||||
{
|
||||
@@ -265,7 +285,11 @@ public static class CardDetailsEndpoints
|
||||
// GET /api/cards/{cardId}/files/{fileId}/download: поток содержимого вложения.
|
||||
// Карточки/записи нет → 404; пустой objectKey → 410; объекта нет в хранилище/сбой → 404. Ответ — поток
|
||||
// с Content-Length/Content-Type из дескриптора; Content-Disposition attachment, имя без кавычек.
|
||||
private static async Task<IResult> DownloadFileAsync(string cardId, string fileId, HttpContext context, CancellationToken ct)
|
||||
private static async Task<IResult> DownloadFileAsync(
|
||||
string cardId,
|
||||
string fileId,
|
||||
HttpContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!context.HasUser())
|
||||
{
|
||||
@@ -314,7 +338,11 @@ public static class CardDetailsEndpoints
|
||||
}
|
||||
|
||||
// DELETE /api/cards/{cardId}/files/{fileId}: открепить файл. Ответ — карточка.
|
||||
private static async Task<IResult> RemoveFileAsync(string cardId, string fileId, HttpContext context, CancellationToken ct)
|
||||
private static async Task<IResult> RemoveFileAsync(
|
||||
string cardId,
|
||||
string fileId,
|
||||
HttpContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!context.HasUser())
|
||||
{
|
||||
@@ -329,7 +357,11 @@ public static class CardDetailsEndpoints
|
||||
}
|
||||
|
||||
// POST /api/cards/{cardId}/reminder {at: epoch-ms}: установить напоминание. Ответ — карточка.
|
||||
private static async Task<IResult> SetReminderAsync(string cardId, ReminderSetRequest body, HttpContext context, CancellationToken ct)
|
||||
private static async Task<IResult> SetReminderAsync(
|
||||
string cardId,
|
||||
ReminderSetRequest body,
|
||||
HttpContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!context.HasUser())
|
||||
{
|
||||
@@ -354,7 +386,10 @@ public static class CardDetailsEndpoints
|
||||
}
|
||||
|
||||
// DELETE /api/cards/{cardId}/reminder: снять напоминание. Ответ — карточка.
|
||||
private static async Task<IResult> ClearReminderAsync(string cardId, HttpContext context, CancellationToken ct)
|
||||
private static async Task<IResult> ClearReminderAsync(
|
||||
string cardId,
|
||||
HttpContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!context.HasUser())
|
||||
{
|
||||
@@ -368,7 +403,10 @@ public static class CardDetailsEndpoints
|
||||
}
|
||||
|
||||
// POST /api/cards/{cardId}/reminder/snooze: «напомнить позже» (now + 24 ч). Ответ — карточка.
|
||||
private static async Task<IResult> SnoozeReminderAsync(string cardId, HttpContext context, CancellationToken ct)
|
||||
private static async Task<IResult> SnoozeReminderAsync(
|
||||
string cardId,
|
||||
HttpContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!context.HasUser())
|
||||
{
|
||||
@@ -386,7 +424,10 @@ public static class CardDetailsEndpoints
|
||||
// cardId: Id карточки.
|
||||
// ct: Токен отмены.
|
||||
// Возвращает: 200 с единой карточкой либо 404.
|
||||
private static async Task<IResult> ReadCardAsync(HttpContext context, string cardId, CancellationToken ct)
|
||||
private static async Task<IResult> ReadCardAsync(
|
||||
HttpContext context,
|
||||
string cardId,
|
||||
CancellationToken ct)
|
||||
{
|
||||
CardsService cardsService = context.RequestServices.GetRequiredService<CardsService>();
|
||||
CardDto? card = await cardsService.GetCardAsync(cardId, ct);
|
||||
|
||||
@@ -100,7 +100,11 @@ public static class CardsEndpoints
|
||||
|
||||
// GET /api/cards?containerId=: карточки контейнера (или все карточки дашборда); 400 «Неизвестный контейнер».
|
||||
// Параметр col принят как алиас containerId (совместимость со старым фронтом).
|
||||
private static async Task<IResult> ListCardsAsync(string? containerId, string? col, HttpContext context, CancellationToken ct)
|
||||
private static async Task<IResult> ListCardsAsync(
|
||||
string? containerId,
|
||||
string? col,
|
||||
HttpContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!context.HasUser())
|
||||
{
|
||||
@@ -143,7 +147,10 @@ public static class CardsEndpoints
|
||||
}
|
||||
|
||||
// GET /api/cards/{cardId}: одна карточка; 404 «Карточка не найдена» (L166–168).
|
||||
private static async Task<IResult> GetCardAsync(string cardId, HttpContext context, CancellationToken ct)
|
||||
private static async Task<IResult> GetCardAsync(
|
||||
string cardId,
|
||||
HttpContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!context.HasUser())
|
||||
{
|
||||
@@ -171,7 +178,10 @@ public static class CardsEndpoints
|
||||
}
|
||||
|
||||
// POST /api/cards/mark-col-seen: снять «новое» с колонки (L187–191); ответ {ok:true}.
|
||||
private static async Task<IResult> MarkColSeenAsync(MarkColBody body, HttpContext context, CancellationToken ct)
|
||||
private static async Task<IResult> MarkColSeenAsync(
|
||||
MarkColBody body,
|
||||
HttpContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!context.HasUser())
|
||||
{
|
||||
@@ -195,7 +205,11 @@ public static class CardsEndpoints
|
||||
// доменный механизм перехода ICardMover: стадия — запись истории и сброс напоминания
|
||||
// (move_stage), дашборд-контейнер — журнал/обучение ML. Ответ — обновлённая карточка; 400 при
|
||||
// несуществующем контейнере, 404 — карточки нет.
|
||||
private static async Task<IResult> MoveAsync(string cardId, MoveBody body, HttpContext context, CancellationToken ct)
|
||||
private static async Task<IResult> MoveAsync(
|
||||
string cardId,
|
||||
MoveBody body,
|
||||
HttpContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!context.HasUser())
|
||||
{
|
||||
@@ -225,7 +239,10 @@ public static class CardsEndpoints
|
||||
}
|
||||
|
||||
// POST /api/cards/{cardId}/trash: в корзину + обучение ML spam (L203–207); ответ {ok:true}; 404.
|
||||
private static async Task<IResult> TrashAsync(string cardId, HttpContext context, CancellationToken ct)
|
||||
private static async Task<IResult> TrashAsync(
|
||||
string cardId,
|
||||
HttpContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!context.HasUser())
|
||||
{
|
||||
@@ -245,7 +262,10 @@ public static class CardsEndpoints
|
||||
}
|
||||
|
||||
// POST /api/cards/{cardId}/restore: возврат из архив/корзины на канбан (L210–214); ответ {ok, col}; 404.
|
||||
private static async Task<IResult> RestoreAsync(string cardId, HttpContext context, CancellationToken ct)
|
||||
private static async Task<IResult> RestoreAsync(
|
||||
string cardId,
|
||||
HttpContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!context.HasUser())
|
||||
{
|
||||
@@ -265,7 +285,10 @@ public static class CardsEndpoints
|
||||
}
|
||||
|
||||
// DELETE /api/cards/{cardId}: удалить навсегда (Cards + комментарии; L217–221); ответ {ok:true}; 404.
|
||||
private static async Task<IResult> DeleteAsync(string cardId, HttpContext context, CancellationToken ct)
|
||||
private static async Task<IResult> DeleteAsync(
|
||||
string cardId,
|
||||
HttpContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!context.HasUser())
|
||||
{
|
||||
@@ -285,7 +308,10 @@ public static class CardsEndpoints
|
||||
}
|
||||
|
||||
// POST /api/cards/clear-col {col}: очистить корзину/архив (L228–235); ответ {ok, cleared}; 400.
|
||||
private static async Task<IResult> ClearColAsync(ClearColBody body, HttpContext context, CancellationToken ct)
|
||||
private static async Task<IResult> ClearColAsync(
|
||||
ClearColBody body,
|
||||
HttpContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!context.HasUser())
|
||||
{
|
||||
@@ -300,7 +326,11 @@ public static class CardsEndpoints
|
||||
}
|
||||
|
||||
// POST /api/cards/{cardId}/comments {text}: добавить комментарий (L238–242); ответ {comments}; 400 «Пустой комментарий»; 404.
|
||||
private static async Task<IResult> AddCommentAsync(string cardId, CommentBody body, HttpContext context, CancellationToken ct)
|
||||
private static async Task<IResult> AddCommentAsync(
|
||||
string cardId,
|
||||
CommentBody body,
|
||||
HttpContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!context.HasUser())
|
||||
{
|
||||
@@ -332,7 +362,10 @@ public static class CardsEndpoints
|
||||
// body: Тело запроса (ids — опционально).
|
||||
// context: Контекст запроса.
|
||||
// ct: Токен отмены.
|
||||
private static async Task<IResult> ReclassifyAsync(ReclassifyBody? body, HttpContext context, CancellationToken ct)
|
||||
private static async Task<IResult> ReclassifyAsync(
|
||||
ReclassifyBody? body,
|
||||
HttpContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!context.HasUser())
|
||||
{
|
||||
@@ -350,7 +383,10 @@ public static class CardsEndpoints
|
||||
// cardId: Id карточки (c_...).
|
||||
// context: Контекст запроса.
|
||||
// ct: Токен отмены.
|
||||
private static async Task<IResult> ReclassifyOneAsync(string cardId, HttpContext context, CancellationToken ct)
|
||||
private static async Task<IResult> ReclassifyOneAsync(
|
||||
string cardId,
|
||||
HttpContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!context.HasUser())
|
||||
{
|
||||
@@ -412,7 +448,10 @@ public static class CardsEndpoints
|
||||
// context: Контекст запроса.
|
||||
// result: Итог прохода.
|
||||
// ct: Токен отмены.
|
||||
private static Task AppendReclassifyAuditAsync(HttpContext context, ReclassifyResultDto result, CancellationToken ct)
|
||||
private static Task AppendReclassifyAuditAsync(
|
||||
HttpContext context,
|
||||
ReclassifyResultDto result,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (result.Reclassified == 0)
|
||||
{
|
||||
@@ -427,7 +466,10 @@ public static class CardsEndpoints
|
||||
}
|
||||
|
||||
// GET /api/search?q=: поиск карточек (FTS + LIKE, Ruling 6/Task 12; dashboard_routes L254–256). Ответ {leads, messages: []}.
|
||||
private static async Task<IResult> SearchAsync(string? q, HttpContext context, CancellationToken ct)
|
||||
private static async Task<IResult> SearchAsync(
|
||||
string? q,
|
||||
HttpContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!context.HasUser())
|
||||
{
|
||||
@@ -448,7 +490,10 @@ public static class CardsEndpoints
|
||||
// context: Контекст запроса (для резолва ContainersService).
|
||||
// ct: Токен отмены.
|
||||
// Возвращает: True — контейнер допустим для фильтра.
|
||||
private static async Task<bool> IsKnownContainerAsync(string col, HttpContext context, CancellationToken ct)
|
||||
private static async Task<bool> IsKnownContainerAsync(
|
||||
string col,
|
||||
HttpContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
ContainersService containers = context.RequestServices.GetRequiredService<ContainersService>();
|
||||
return await containers.GetAsync(col, ct) is not null;
|
||||
|
||||
@@ -69,7 +69,10 @@ public static class ContainersEndpoints
|
||||
}
|
||||
|
||||
// GET /api/containers?space=: список контейнеров пространства (или всех) со счётчиками.
|
||||
private static async Task<IResult> ListContainersAsync(string? space, HttpContext context, CancellationToken ct)
|
||||
private static async Task<IResult> ListContainersAsync(
|
||||
string? space,
|
||||
HttpContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!context.HasUser())
|
||||
{
|
||||
@@ -81,7 +84,10 @@ public static class ContainersEndpoints
|
||||
}
|
||||
|
||||
// POST /api/containers: создать контейнер; ответ {id}.
|
||||
private static async Task<IResult> CreateContainerAsync(ContainerCreateRequest body, HttpContext context, CancellationToken ct)
|
||||
private static async Task<IResult> CreateContainerAsync(
|
||||
ContainerCreateRequest body,
|
||||
HttpContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!context.HasUser())
|
||||
{
|
||||
@@ -112,7 +118,11 @@ public static class ContainersEndpoints
|
||||
}
|
||||
|
||||
// PATCH /api/containers/{id}: частичное обновление; ответ {id}; 404 «Контейнер не найден».
|
||||
private static async Task<IResult> PatchContainerAsync(string containerId, JsonElement body, HttpContext context, CancellationToken ct)
|
||||
private static async Task<IResult> PatchContainerAsync(
|
||||
string containerId,
|
||||
JsonElement body,
|
||||
HttpContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!context.HasUser())
|
||||
{
|
||||
@@ -172,7 +182,10 @@ public static class ContainersEndpoints
|
||||
}
|
||||
|
||||
// POST /api/containers/{id}/accept: принять ИИ-предложение (suggested=false).
|
||||
private static async Task<IResult> AcceptSuggestedAsync(string containerId, HttpContext context, CancellationToken ct)
|
||||
private static async Task<IResult> AcceptSuggestedAsync(
|
||||
string containerId,
|
||||
HttpContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!context.HasUser())
|
||||
{
|
||||
@@ -192,7 +205,10 @@ public static class ContainersEndpoints
|
||||
}
|
||||
|
||||
// DELETE /api/containers/{id}: удалить контейнер; карточки → «Неразобранное» новыми.
|
||||
private static async Task<IResult> DeleteContainerAsync(string containerId, HttpContext context, CancellationToken ct)
|
||||
private static async Task<IResult> DeleteContainerAsync(
|
||||
string containerId,
|
||||
HttpContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!context.HasUser())
|
||||
{
|
||||
@@ -208,7 +224,10 @@ public static class ContainersEndpoints
|
||||
}
|
||||
|
||||
// POST /api/containers/reorder: порядок контейнеров пространства; ответ {ok:true}.
|
||||
private static async Task<IResult> ReorderContainersAsync(OrderBody body, HttpContext context, CancellationToken ct)
|
||||
private static async Task<IResult> ReorderContainersAsync(
|
||||
OrderBody body,
|
||||
HttpContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!context.HasUser())
|
||||
{
|
||||
@@ -246,7 +265,11 @@ public static class ContainersEndpoints
|
||||
}
|
||||
|
||||
// PATCH /api/containers/{id}/state: merge патча в состояние колонки; ответ — состояние этой колонки.
|
||||
private static async Task<IResult> PatchColumnStateAsync(string containerId, ColStateBody body, HttpContext context, CancellationToken ct)
|
||||
private static async Task<IResult> PatchColumnStateAsync(
|
||||
string containerId,
|
||||
ColStateBody body,
|
||||
HttpContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!context.HasUser())
|
||||
{
|
||||
|
||||
@@ -146,7 +146,10 @@ public static class DiscoveryEndpoints
|
||||
}
|
||||
|
||||
// POST /api/discovery/tasks: создать задачу поиска (create_task L146–151; дефолты — в сервисе).
|
||||
private static async Task<IResult> CreateTaskAsync(DiscoveryTaskCreateBody body, HttpContext context, CancellationToken ct)
|
||||
private static async Task<IResult> CreateTaskAsync(
|
||||
DiscoveryTaskCreateBody body,
|
||||
HttpContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!context.HasUser())
|
||||
{
|
||||
@@ -166,7 +169,11 @@ public static class DiscoveryEndpoints
|
||||
}
|
||||
|
||||
// PATCH /api/discovery/tasks/{task_id}: обновить задачу (patch_task L154–161; 404/400).
|
||||
private static async Task<IResult> PatchTaskAsync(string task_id, DiscoveryTaskPatchBody body, HttpContext context, CancellationToken ct)
|
||||
private static async Task<IResult> PatchTaskAsync(
|
||||
string task_id,
|
||||
DiscoveryTaskPatchBody body,
|
||||
HttpContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!context.HasUser())
|
||||
{
|
||||
@@ -186,7 +193,10 @@ public static class DiscoveryEndpoints
|
||||
}
|
||||
|
||||
// DELETE /api/discovery/tasks/{task_id}: удалить задачу с кандидатами и логом (delete_task L164–168).
|
||||
private static async Task<IResult> DeleteTaskAsync(string task_id, HttpContext context, CancellationToken ct)
|
||||
private static async Task<IResult> DeleteTaskAsync(
|
||||
string task_id,
|
||||
HttpContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!context.HasUser())
|
||||
{
|
||||
@@ -199,7 +209,10 @@ public static class DiscoveryEndpoints
|
||||
}
|
||||
|
||||
// POST /api/discovery/tasks/{task_id}/start: запуск поиска (start_task L171–179; пустые ключи → 400).
|
||||
private static async Task<IResult> StartTaskAsync(string task_id, HttpContext context, CancellationToken ct)
|
||||
private static async Task<IResult> StartTaskAsync(
|
||||
string task_id,
|
||||
HttpContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!context.HasUser())
|
||||
{
|
||||
@@ -219,7 +232,10 @@ public static class DiscoveryEndpoints
|
||||
}
|
||||
|
||||
// POST /api/discovery/tasks/{task_id}/pause: пауза поиска (pause_task L181–187).
|
||||
private static async Task<IResult> PauseTaskAsync(string task_id, HttpContext context, CancellationToken ct)
|
||||
private static async Task<IResult> PauseTaskAsync(
|
||||
string task_id,
|
||||
HttpContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!context.HasUser())
|
||||
{
|
||||
@@ -236,7 +252,10 @@ public static class DiscoveryEndpoints
|
||||
// Очистка ключей ответа — CleanKeywords (python _clean_keywords L111–128: ≤30, ≤60
|
||||
// символов, дедуп casefold); описание режет до 4000 сам адаптер (GrpcAiTools.MaxDescriptionCodePoints).
|
||||
// Локальный режим (LocalAiTools, UseLocal=true) — NotSupportedException → та же мягкая ветка с текстом причины.
|
||||
private static async Task<IResult> GenerateKeywordsAsync(string task_id, HttpContext context, CancellationToken ct)
|
||||
private static async Task<IResult> GenerateKeywordsAsync(
|
||||
string task_id,
|
||||
HttpContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!context.HasUser())
|
||||
{
|
||||
@@ -283,7 +302,11 @@ public static class DiscoveryEndpoints
|
||||
|
||||
// GET /api/discovery/tasks/{task_id}/candidates?status=: кандидаты задачи с фильтром
|
||||
// new|review|joined|rejected (list_candidates L216–224; невалидный статус — пустой список).
|
||||
private static async Task<IResult> ListCandidatesAsync(string task_id, string? status, HttpContext context, CancellationToken ct)
|
||||
private static async Task<IResult> ListCandidatesAsync(
|
||||
string task_id,
|
||||
string? status,
|
||||
HttpContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!context.HasUser())
|
||||
{
|
||||
@@ -305,7 +328,10 @@ public static class DiscoveryEndpoints
|
||||
// POST /api/discovery/candidates/{dialog_id}/join: ручное вступление вне квот (join_candidate L226–251).
|
||||
// RPC Join → строка каталога Dialogs (монитор on) + зеркало → фоновый первый разбор → снятие чёрного списка →
|
||||
// mark_joined(auto:false). Ошибка Telegram → 400 с текстом причины.
|
||||
private static async Task<IResult> JoinCandidateAsync(string dialog_id, HttpContext context, CancellationToken ct)
|
||||
private static async Task<IResult> JoinCandidateAsync(
|
||||
string dialog_id,
|
||||
HttpContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!context.HasUser())
|
||||
{
|
||||
@@ -361,7 +387,10 @@ public static class DiscoveryEndpoints
|
||||
|
||||
// POST /api/discovery/candidates/{dialog_id}/reject: отклонить кандидата — в чёрный список
|
||||
// (reject_candidate L253–264; уже вступившего — нельзя, 400).
|
||||
private static async Task<IResult> RejectCandidateAsync(string dialog_id, HttpContext context, CancellationToken ct)
|
||||
private static async Task<IResult> RejectCandidateAsync(
|
||||
string dialog_id,
|
||||
HttpContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!context.HasUser())
|
||||
{
|
||||
@@ -405,7 +434,10 @@ public static class DiscoveryEndpoints
|
||||
}
|
||||
|
||||
// DELETE /api/discovery/blacklist/{dialog_id}: снять источник с чёрного списка (remove_blacklist L274–277).
|
||||
private static async Task<IResult> RemoveBlacklistAsync(string dialog_id, HttpContext context, CancellationToken ct)
|
||||
private static async Task<IResult> RemoveBlacklistAsync(
|
||||
string dialog_id,
|
||||
HttpContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!context.HasUser())
|
||||
{
|
||||
@@ -418,7 +450,10 @@ public static class DiscoveryEndpoints
|
||||
}
|
||||
|
||||
// GET /api/discovery/tasks/{task_id}/log: лог задачи (task_log L282–285), события от новых к старым.
|
||||
private static async Task<IResult> TaskLogAsync(string task_id, HttpContext context, CancellationToken ct)
|
||||
private static async Task<IResult> TaskLogAsync(
|
||||
string task_id,
|
||||
HttpContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!context.HasUser())
|
||||
{
|
||||
|
||||
@@ -110,7 +110,10 @@ public static class EventsEndpoint
|
||||
// response: Ответ (stream уже начат).
|
||||
// frame: Frame протокола SSE.
|
||||
// ct: Токен отмены запроса.
|
||||
private static async Task WriteFrameAsync(HttpResponse response, string frame, CancellationToken ct)
|
||||
private static async Task WriteFrameAsync(
|
||||
HttpResponse response,
|
||||
string frame,
|
||||
CancellationToken ct)
|
||||
{
|
||||
await response.WriteAsync(frame, ct);
|
||||
await response.Body.FlushAsync(ct);
|
||||
|
||||
@@ -47,7 +47,10 @@ public static class FilterTesterEndpoints
|
||||
}
|
||||
|
||||
// POST /api/admin/check-message: этап-1 правила + этап-2 (skipped) для тестера (dashboard_routes.py L267–284).
|
||||
private static async Task<IResult> CheckAsync(CheckMessageRequest body, HttpContext context, CancellationToken ct)
|
||||
private static async Task<IResult> CheckAsync(
|
||||
CheckMessageRequest body,
|
||||
HttpContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!context.HasUser())
|
||||
{
|
||||
|
||||
@@ -104,7 +104,10 @@ public static class MlEndpoints
|
||||
}
|
||||
|
||||
// POST /api/ml/predict: проверка ML на тексте (ml_routes.py L84–90).
|
||||
private static async Task<IResult> PredictAsync(MlPredictRequest body, HttpContext context, CancellationToken ct)
|
||||
private static async Task<IResult> PredictAsync(
|
||||
MlPredictRequest body,
|
||||
HttpContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!context.HasUser())
|
||||
{
|
||||
@@ -139,7 +142,10 @@ public static class MlEndpoints
|
||||
}
|
||||
|
||||
// POST /api/ml/candidates: последние сообщения канала + мнение ML (ml_routes.py L112–134).
|
||||
private static async Task<IResult> CandidatesAsync(MlCandidatesRequest body, HttpContext context, CancellationToken ct)
|
||||
private static async Task<IResult> CandidatesAsync(
|
||||
MlCandidatesRequest body,
|
||||
HttpContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!context.HasUser())
|
||||
{
|
||||
@@ -152,7 +158,10 @@ public static class MlEndpoints
|
||||
}
|
||||
|
||||
// POST /api/ml/apply: ручное решение по сообщению (ml_routes.py L137–171).
|
||||
private static async Task<IResult> ApplyAsync(MlApplyRequest body, HttpContext context, CancellationToken ct)
|
||||
private static async Task<IResult> ApplyAsync(
|
||||
MlApplyRequest body,
|
||||
HttpContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!context.HasUser())
|
||||
{
|
||||
|
||||
@@ -143,7 +143,10 @@ public static class OperatorAuthEndpoints
|
||||
// context: Контекст запроса.
|
||||
// options: Настройки куки из конфигурации (секция OperatorCookies).
|
||||
// rawToken: Raw-токен операторской сессии.
|
||||
private static void SetOperatorSessionCookie(HttpContext context, OperatorCookieOptions options, string rawToken)
|
||||
private static void SetOperatorSessionCookie(
|
||||
HttpContext context,
|
||||
OperatorCookieOptions options,
|
||||
string rawToken)
|
||||
{
|
||||
// MaxAge — OperatorCookies:Hours; код-дефолт значения ссылается на
|
||||
// OperatorAuthService.SessionLifetimeHours (единый источник «12 часов», см. OperatorCookieOptions).
|
||||
|
||||
@@ -93,7 +93,10 @@ public static class PipelineEndpoints
|
||||
// GET /api/pipeline/queue?limit=: сырые сообщения очереди + счётчики + число отсева (processing_routes.py L23–31).
|
||||
// Ответ {items, counts:{new,ai,total}, rejected} 1:1 с list_queue L218–241 + queue_counts L207–215 +
|
||||
// rejected_count L201–202. limit — дефолт 100, clamp 1..500 делает сервис (ListQueueAsync).
|
||||
private static async Task<IResult> QueueAsync(int? limit, HttpContext context, CancellationToken ct)
|
||||
private static async Task<IResult> QueueAsync(
|
||||
int? limit,
|
||||
HttpContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!context.HasUser())
|
||||
{
|
||||
@@ -110,7 +113,12 @@ public static class PipelineEndpoints
|
||||
// GET /api/pipeline/rejected?q=&offset=&limit=: страница отсева (processing_routes.py L34–42, list_rejected L246–312).
|
||||
// q — поиск по тексту/причине/фразе/имени канала (FTS ∪ LIKE, Ruling 6), пустой q — весь отсев свежими
|
||||
// первыми; offset ≥ 0, limit 1..500 (clamp в сервисе), значения эхом в ответе {items,total,offset,limit}.
|
||||
private static async Task<IResult> RejectedAsync(string? q, int? offset, int? limit, HttpContext context, CancellationToken ct)
|
||||
private static async Task<IResult> RejectedAsync(
|
||||
string? q,
|
||||
int? offset,
|
||||
int? limit,
|
||||
HttpContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!context.HasUser())
|
||||
{
|
||||
@@ -137,7 +145,10 @@ public static class PipelineEndpoints
|
||||
|
||||
// DELETE /api/pipeline/rejected/{rejId}: удалить запись отсева; ответ {ok:true} всегда (delete_one L196–198, Ruling 10).
|
||||
// Прототип не проверяет наличие записи — 404 не шлём (план Task 9 L444; Ruling 10 «always ok»).
|
||||
private static async Task<IResult> DeleteAsync(string rejId, HttpContext context, CancellationToken ct)
|
||||
private static async Task<IResult> DeleteAsync(
|
||||
string rejId,
|
||||
HttpContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!context.HasUser())
|
||||
{
|
||||
@@ -153,7 +164,11 @@ public static class PipelineEndpoints
|
||||
// Успех — {id, returned:true, returnedAt} (запись помечается returned, НЕ удаляется — аудит Ruling 10);
|
||||
// причины 400 (уже возвращено/повтор-dup/нет текста) — константы PipelineProcessingService (строки 1:1 с
|
||||
// прототипом); записи нет — 404 «Запись не найдена» (текст 404 — слой эндпоинтов).
|
||||
private static async Task<IResult> ReturnAsync(string rejId, ReturnReasonRequest body, HttpContext context, CancellationToken ct)
|
||||
private static async Task<IResult> ReturnAsync(
|
||||
string rejId,
|
||||
ReturnReasonRequest body,
|
||||
HttpContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!context.HasUser())
|
||||
{
|
||||
|
||||
@@ -140,7 +140,10 @@ public static class TelegramEndpoints
|
||||
}
|
||||
|
||||
// POST /api/tg/start-phone: запросить код по номеру (tg_routes.py L68–74; python L134–147).
|
||||
private static async Task<IResult> StartPhoneAsync(TgStartPhoneRequest body, HttpContext context, CancellationToken ct)
|
||||
private static async Task<IResult> StartPhoneAsync(
|
||||
TgStartPhoneRequest body,
|
||||
HttpContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!context.HasUser())
|
||||
{
|
||||
@@ -209,7 +212,10 @@ public static class TelegramEndpoints
|
||||
}
|
||||
|
||||
// POST /api/tg/send-code: отправить SMS-код (tg_routes.py L86–94; python submit_code L149–166).
|
||||
private static async Task<IResult> SendCodeAsync(TgSendCodeRequest body, HttpContext context, CancellationToken ct)
|
||||
private static async Task<IResult> SendCodeAsync(
|
||||
TgSendCodeRequest body,
|
||||
HttpContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!context.HasUser())
|
||||
{
|
||||
@@ -235,7 +241,10 @@ public static class TelegramEndpoints
|
||||
}
|
||||
|
||||
// POST /api/tg/send-password: облачный пароль 2FA (tg_routes.py L97–103; python submit_password L168–176).
|
||||
private static async Task<IResult> SendPasswordAsync(TgSendPasswordRequest body, HttpContext context, CancellationToken ct)
|
||||
private static async Task<IResult> SendPasswordAsync(
|
||||
TgSendPasswordRequest body,
|
||||
HttpContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!context.HasUser())
|
||||
{
|
||||
@@ -355,7 +364,10 @@ public static class TelegramEndpoints
|
||||
}
|
||||
|
||||
// POST /api/tg/dialogs/monitor-all: мониторинг всех каналов (tg_routes.py L125–129; L548–567).
|
||||
private static async Task<IResult> MonitorAllAsync(TgMonitorBody body, HttpContext context, CancellationToken ct)
|
||||
private static async Task<IResult> MonitorAllAsync(
|
||||
TgMonitorBody body,
|
||||
HttpContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!context.HasUser())
|
||||
{
|
||||
@@ -406,7 +418,11 @@ public static class TelegramEndpoints
|
||||
}
|
||||
|
||||
// POST /api/tg/dialogs/{dialog_id}/monitor: вкл/выкл мониторинг канала (tg_routes.py L139–142; L536–546).
|
||||
private static async Task<IResult> SetMonitorAsync(string dialog_id, TgMonitorBody body, HttpContext context, CancellationToken ct)
|
||||
private static async Task<IResult> SetMonitorAsync(
|
||||
string dialog_id,
|
||||
TgMonitorBody body,
|
||||
HttpContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!context.HasUser())
|
||||
{
|
||||
@@ -439,7 +455,10 @@ public static class TelegramEndpoints
|
||||
|
||||
// POST /api/tg/dialogs/{dialog_id}/backfill: разбор одного диалога (tg_routes.py L145–148; L349–390).
|
||||
// Сервер-only эндпоинт (фронт не вызывает, api-map §3.3 L139/п.9): первый разбор/догон одного канала.
|
||||
private static async Task<IResult> BackfillDialogAsync(string dialog_id, HttpContext context, CancellationToken ct)
|
||||
private static async Task<IResult> BackfillDialogAsync(
|
||||
string dialog_id,
|
||||
HttpContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!context.HasUser())
|
||||
{
|
||||
@@ -459,7 +478,10 @@ public static class TelegramEndpoints
|
||||
}
|
||||
|
||||
// POST /api/tg/dialogs/preview: последние сообщения диалога (tg_routes.py L151–153; dialog_messages L583–620).
|
||||
private static async Task<IResult> PreviewAsync(TgPreviewBody body, HttpContext context, CancellationToken ct)
|
||||
private static async Task<IResult> PreviewAsync(
|
||||
TgPreviewBody body,
|
||||
HttpContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!context.HasUser())
|
||||
{
|
||||
|
||||
@@ -96,7 +96,10 @@ public sealed class SseBroker
|
||||
/// <param name="tenantId">Тенант-получатель; без подписчиков — no-op, не падает.</param>
|
||||
/// <param name="eventType">Тип события (new_card/toast этапа 3; api.js L78–79).</param>
|
||||
/// <param name="payload">Полезная нагрузка — сериализуется в JSON (camelCase, без \u).</param>
|
||||
public void Publish(Guid tenantId, string eventType, object payload) =>
|
||||
public void Publish(
|
||||
Guid tenantId,
|
||||
string eventType,
|
||||
object payload) =>
|
||||
Publish(tenantId, new SseEvent(eventType, JsonSerializer.Serialize(payload, PublishJsonOptions)));
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -28,7 +28,11 @@ public static class AuditAppender
|
||||
/// <param name="eventType">Тип события — константа <see cref="AuditEvents"/>.</param>
|
||||
/// <param name="details">Минимальные детали события (обычно анонимный объект) или null.</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
public static async Task AppendTenantAsync(HttpContext context, string eventType, object? details, CancellationToken ct)
|
||||
public static async Task AppendTenantAsync(
|
||||
HttpContext context,
|
||||
string eventType,
|
||||
object? details,
|
||||
CancellationToken ct)
|
||||
{
|
||||
CurrentUser? user = context.GetCurrentUser();
|
||||
if (user is null)
|
||||
@@ -55,7 +59,11 @@ public static class AuditAppender
|
||||
/// <param name="eventType">Тип события — константа <see cref="AuditEvents"/>.</param>
|
||||
/// <param name="details">Минимальные детали события (обычно анонимный объект) или null.</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
public static async Task AppendOperatorAsync(HttpContext context, string eventType, object? details, CancellationToken ct)
|
||||
public static async Task AppendOperatorAsync(
|
||||
HttpContext context,
|
||||
string eventType,
|
||||
object? details,
|
||||
CancellationToken ct)
|
||||
{
|
||||
CurrentOperator? operatorIdentity = context.GetCurrentOperator();
|
||||
if (operatorIdentity is null)
|
||||
|
||||
@@ -64,7 +64,10 @@ public sealed class LoginAttemptGuard
|
||||
/// <param name="options">Настройки rate limiting (секция RateLimit).</param>
|
||||
/// <param name="store">Хранилище счётчиков фиксированного окна (public.rate_limit_counters).</param>
|
||||
/// <param name="clock">Источник «сейчас».</param>
|
||||
public LoginAttemptGuard(RateLimitOptions options, IRateLimitCounterStore store, Func<DateTimeOffset> clock)
|
||||
public LoginAttemptGuard(
|
||||
RateLimitOptions options,
|
||||
IRateLimitCounterStore store,
|
||||
Func<DateTimeOffset> clock)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
ArgumentNullException.ThrowIfNull(store);
|
||||
@@ -84,7 +87,10 @@ public sealed class LoginAttemptGuard
|
||||
/// <param name="login">Нормализованный логин; пустой/пробельный — блокировке не подлежит.</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
/// <returns>true — следующий вход ключа отклоняется 429 до проверки учётных данных.</returns>
|
||||
public async Task<bool> IsBlockedAsync(string? ip, string? login, CancellationToken ct)
|
||||
public async Task<bool> IsBlockedAsync(
|
||||
string? ip,
|
||||
string? login,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!_enabled || string.IsNullOrWhiteSpace(login))
|
||||
{
|
||||
@@ -101,7 +107,10 @@ public sealed class LoginAttemptGuard
|
||||
/// <param name="ip">IP клиента (null/пустой — фолбэк unknown).</param>
|
||||
/// <param name="login">Нормализованный логин; пустой/пробельный — не записывается.</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
public async Task RecordFailureAsync(string? ip, string? login, CancellationToken ct)
|
||||
public async Task RecordFailureAsync(
|
||||
string? ip,
|
||||
string? login,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!_enabled || string.IsNullOrWhiteSpace(login))
|
||||
{
|
||||
@@ -118,7 +127,10 @@ public sealed class LoginAttemptGuard
|
||||
/// <param name="ip">IP клиента (null/пустой — фолбэк unknown).</param>
|
||||
/// <param name="login">Логин успешно вошедшего (нормализованный).</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
public async Task ResetAsync(string? ip, string? login, CancellationToken ct)
|
||||
public async Task ResetAsync(
|
||||
string? ip,
|
||||
string? login,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!_enabled || string.IsNullOrWhiteSpace(login))
|
||||
{
|
||||
|
||||
@@ -23,7 +23,10 @@ public static class SessionCookieWriter
|
||||
/// <param name="context">Контекст запроса.</param>
|
||||
/// <param name="options">Настройки куки из конфигурации (секция Cookies).</param>
|
||||
/// <param name="rawToken">Raw-токен сессии.</param>
|
||||
public static void Append(HttpContext context, CookieOptions options, string rawToken)
|
||||
public static void Append(
|
||||
HttpContext context,
|
||||
CookieOptions options,
|
||||
string rawToken)
|
||||
{
|
||||
context.Response.Cookies.Append(
|
||||
options.Name,
|
||||
|
||||
@@ -70,7 +70,10 @@ public sealed class HttpAccessLogMiddleware
|
||||
// context: Контекст запроса (метод/путь/статус ответа).
|
||||
// startedAt: Метка времени старта запроса (Stopwatch.GetTimestamp).
|
||||
// exception: Необработанное исключение (null — запрос завершился штатно).
|
||||
private void LogCall(HttpContext context, long startedAt, Exception? exception)
|
||||
private void LogCall(
|
||||
HttpContext context,
|
||||
long startedAt,
|
||||
Exception? exception)
|
||||
{
|
||||
long elapsedMs = (long)Stopwatch.GetElapsedTime(startedAt).TotalMilliseconds;
|
||||
string method = context.Request.Method;
|
||||
|
||||
@@ -30,9 +30,7 @@ public sealed class OperatorSessionMiddleware
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly IOptionsMonitor<OperatorCookieOptions> _cookieOptions;
|
||||
|
||||
public OperatorSessionMiddleware(
|
||||
RequestDelegate next,
|
||||
IOptionsMonitor<OperatorCookieOptions> cookieOptions)
|
||||
public OperatorSessionMiddleware(RequestDelegate next, IOptionsMonitor<OperatorCookieOptions> cookieOptions)
|
||||
{
|
||||
_next = next;
|
||||
_cookieOptions = cookieOptions;
|
||||
|
||||
@@ -102,7 +102,10 @@ public static class RateLimitPolicies
|
||||
// scope: Пространство ключей (имя политики или "global" для глобального лимитера).
|
||||
// permitsPerMinute: Разрешено запросов в минуту на партицию (RateLimit:ApiPerMinute).
|
||||
// Возвращает: Партиция лимитера с ключом tenant:{id} либо IP.
|
||||
private static RateLimitPartition<string> ApiPartition(HttpContext context, string scope, int permitsPerMinute)
|
||||
private static RateLimitPartition<string> ApiPartition(
|
||||
HttpContext context,
|
||||
string scope,
|
||||
int permitsPerMinute)
|
||||
{
|
||||
string partitionKey = context.GetCurrentUser() is { } user
|
||||
? TenantKeyPrefix + user.TenantId.ToString("N")
|
||||
@@ -117,7 +120,10 @@ public static class RateLimitPolicies
|
||||
// permitsPerMinute: Разрешено запросов в минуту.
|
||||
// Возвращает: Партиция store-backed лимитера (ленивое создание и кеш — менеджер партиций).
|
||||
private static RateLimitPartition<string> StorePartition(
|
||||
HttpContext context, string scope, string partitionKey, int permitsPerMinute)
|
||||
HttpContext context,
|
||||
string scope,
|
||||
string partitionKey,
|
||||
int permitsPerMinute)
|
||||
{
|
||||
string storeKey = HttpKeyPrefix + scope + ":" + partitionKey;
|
||||
return RateLimitPartition.Get(storeKey, key => new StoreBackedFixedWindowRateLimiter(
|
||||
|
||||
@@ -102,7 +102,9 @@ public sealed class TelegramBackfillScheduler(
|
||||
|
||||
// «Перечитать» всех включённых каналов в фоне (ReadRecentAsync продолжает при частичном падении).
|
||||
private static async Task RunReadRecentAsync(
|
||||
IServiceProvider provider, ILogger<TelegramBackfillScheduler> runLogger, CancellationToken ct)
|
||||
IServiceProvider provider,
|
||||
ILogger<TelegramBackfillScheduler> runLogger,
|
||||
CancellationToken ct)
|
||||
{
|
||||
DialogsService dialogs = provider.GetRequiredService<DialogsService>();
|
||||
int count = await dialogs.ReadRecentAsync(ct);
|
||||
|
||||
@@ -79,7 +79,10 @@ public sealed class RpcCallLoggingInterceptor : Interceptor
|
||||
// context: Контекст вызова (метод).
|
||||
// startedAt: Метка времени старта вызова (Stopwatch.GetTimestamp).
|
||||
// statusCode: Итоговый gRPC-статус; null — успех (OK).
|
||||
private void LogCall(ServerCallContext context, long startedAt, StatusCode? statusCode)
|
||||
private void LogCall(
|
||||
ServerCallContext context,
|
||||
long startedAt,
|
||||
StatusCode? statusCode)
|
||||
{
|
||||
long elapsedMs = (long)Stopwatch.GetElapsedTime(startedAt).TotalMilliseconds;
|
||||
_logger.LogInformation(
|
||||
|
||||
@@ -328,7 +328,10 @@ public sealed class TelegramIngressService(
|
||||
// request: Сообщение PushMessage.
|
||||
// ct: Токен отмены.
|
||||
private async Task SavePreviewSafelyAsync(
|
||||
AsyncServiceScope tenantScope, TenantRecordDto tenant, PushMessageRequest request, CancellationToken ct)
|
||||
AsyncServiceScope tenantScope,
|
||||
TenantRecordDto tenant,
|
||||
PushMessageRequest request,
|
||||
CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -356,7 +359,10 @@ public sealed class TelegramIngressService(
|
||||
// tenantId: Тенант канала (реестровый Guid).
|
||||
// previous: Предыдущий снимок из KV (null — первый репорт).
|
||||
// current: Текущий снимок репорта.
|
||||
private void PublishStatusEvents(Guid tenantId, TgReportedStatus? previous, TgReportedStatus current)
|
||||
private void PublishStatusEvents(
|
||||
Guid tenantId,
|
||||
TgReportedStatus? previous,
|
||||
TgReportedStatus current)
|
||||
{
|
||||
broker.Publish(tenantId, SystemStatusEventType, current);
|
||||
if (previous is null)
|
||||
@@ -379,7 +385,10 @@ public sealed class TelegramIngressService(
|
||||
// tenantId: Тенант-получатель.
|
||||
// text: Текст тоста.
|
||||
// icon: Иконка тоста (набор Icon.vue фронта).
|
||||
private void PublishToast(Guid tenantId, string text, string icon)
|
||||
private void PublishToast(
|
||||
Guid tenantId,
|
||||
string text,
|
||||
string icon)
|
||||
{
|
||||
broker.Publish(tenantId, ToastEventType, new { text, icon });
|
||||
}
|
||||
|
||||
@@ -96,7 +96,10 @@ public sealed class TelegramKeysService(IGlobalSettingsStore store, ISecretCiphe
|
||||
/// <param name="apiHash">api_hash приложения (непустой секрет).</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
/// <exception cref="ArgumentException">Значения не прошли валидацию (см. <see cref="IsValidApiId"/>/<see cref="IsValidApiHash"/>).</exception>
|
||||
public async Task SaveAsync(string apiId, string apiHash, CancellationToken ct)
|
||||
public async Task SaveAsync(
|
||||
string apiId,
|
||||
string apiHash,
|
||||
CancellationToken ct)
|
||||
{
|
||||
string trimmedApiId = (apiId ?? string.Empty).Trim();
|
||||
string trimmedApiHash = (apiHash ?? string.Empty).Trim();
|
||||
|
||||
@@ -43,5 +43,8 @@ public interface IAiTools
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
/// <returns>Решение fit + краткая причина (потолок причины 200, как _AI_REASON_LIMIT L43).</returns>
|
||||
public Task<AiEvaluateFitResultDto> EvaluateFitAsync(
|
||||
string text, string description, IReadOnlyCollection<string> keywords, CancellationToken ct);
|
||||
string text,
|
||||
string description,
|
||||
IReadOnlyCollection<string> keywords,
|
||||
CancellationToken ct);
|
||||
}
|
||||
|
||||
@@ -39,7 +39,11 @@ public interface IFileStorage
|
||||
/// <param name="contentType">MIME-тип (например, image/png); может быть пустым.</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
/// <returns>Сохранённый objectKey (как передан).</returns>
|
||||
public Task<string> PutAsync(string objectKey, Stream content, string contentType, CancellationToken ct);
|
||||
public Task<string> PutAsync(
|
||||
string objectKey,
|
||||
Stream content,
|
||||
string contentType,
|
||||
CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Возвращает содержимое объекта потоком (get object_store.py L82–93).
|
||||
|
||||
@@ -51,5 +51,9 @@ public interface IMlClient
|
||||
/// <param name="delta">Вес сигнала: 1.0 — действие пользователя, −1.0 — снять метку; ИИ-сигналы (0.4/0.6) — этапы 4/6.</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
/// <returns>Задача завершается после записи строки в outbox (отправка в ML-сервис — фоновый воркер этапа 6).</returns>
|
||||
public Task PushAsync(string text, string label, double delta, CancellationToken ct);
|
||||
public Task PushAsync(
|
||||
string text,
|
||||
string label,
|
||||
double delta,
|
||||
CancellationToken ct);
|
||||
}
|
||||
|
||||
@@ -31,7 +31,11 @@ public interface ITelegramGateway
|
||||
/// <param name="apiHash">api_hash приложения Telegram (глобальные ключи, задаёт оператор).</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
/// <returns>Новая фаза ("code"); нет ключей — ядро отвечает 400 «Ключи Telegram не заданы оператором» до вызова.</returns>
|
||||
public Task<TelegramAuthResultDto> StartPhoneAsync(string phone, int apiId, string apiHash, CancellationToken ct);
|
||||
public Task<TelegramAuthResultDto> StartPhoneAsync(
|
||||
string phone,
|
||||
int apiId,
|
||||
string apiHash,
|
||||
CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Начать QR-вход (qr_start python L286–300).
|
||||
@@ -40,7 +44,10 @@ public interface ITelegramGateway
|
||||
/// <param name="apiHash">api_hash приложения Telegram (глобальные ключи, задаёт оператор).</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
/// <returns>Фаза + qrUrl; аккаунт уже авторизован — фаза "ready", qrUrl пуст.</returns>
|
||||
public Task<TelegramAuthResultDto> StartQrAsync(int apiId, string apiHash, CancellationToken ct);
|
||||
public Task<TelegramAuthResultDto> StartQrAsync(
|
||||
int apiId,
|
||||
string apiHash,
|
||||
CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Отправить SMS-код (submit_code python L149–166).
|
||||
@@ -79,7 +86,10 @@ public interface ITelegramGateway
|
||||
/// <param name="enabled">True — мониторить (сообщения → PushMessage в ядро), false — выключить.</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
/// <returns>Завершается после обновления зеркала сервиса.</returns>
|
||||
public Task SetMonitorAsync(string dialogId, bool enabled, CancellationToken ct);
|
||||
public Task SetMonitorAsync(
|
||||
string dialogId,
|
||||
bool enabled,
|
||||
CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Мониторинг всех диалогов сразу (set_monitor_all python L548–567): зеркало сервиса = каталог.
|
||||
@@ -96,7 +106,10 @@ public interface ITelegramGateway
|
||||
/// <param name="force">True — перечитать, даже если диалог уже разобран (кнопка «Перечитать»).</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
/// <returns>Сколько сообщений отправлено в ядро потоком PushMessage.</returns>
|
||||
public Task<int> BackfillAsync(string dialogId, bool force, CancellationToken ct);
|
||||
public Task<int> BackfillAsync(
|
||||
string dialogId,
|
||||
bool force,
|
||||
CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Последние сообщения диалога для превью (dialog_messages python L583–620), свежие из Telegram.
|
||||
@@ -105,7 +118,10 @@ public interface ITelegramGateway
|
||||
/// <param name="limit">Сколько последних сообщений (1..50; api-map /dialogs/preview).</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
/// <returns>Сообщения от новых к старым; признак lead и фолбэк на БД добавляет ядро (Ruling 7).</returns>
|
||||
public Task<IReadOnlyList<TelegramRecentMessageDto>> ReadRecentAsync(string dialogId, int limit, CancellationToken ct);
|
||||
public Task<IReadOnlyList<TelegramRecentMessageDto>> ReadRecentAsync(
|
||||
string dialogId,
|
||||
int limit,
|
||||
CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Глобальный поиск каналов/групп по ключу (discovery_search python L624–664).
|
||||
@@ -114,7 +130,10 @@ public interface ITelegramGateway
|
||||
/// <param name="limit">Верхняя граница результатов (прототип: default 30).</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
/// <returns>Найденные источники (каналы/группы; личные чаты/ботов отсеивает ядро — Ruling 10).</returns>
|
||||
public Task<IReadOnlyList<TelegramDialogEntryDto>> SearchAsync(string query, int limit, CancellationToken ct);
|
||||
public Task<IReadOnlyList<TelegramDialogEntryDto>> SearchAsync(
|
||||
string query,
|
||||
int limit,
|
||||
CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Инфо об источнике для оценки (discovery_info python L666–716).
|
||||
@@ -131,7 +150,10 @@ public interface ITelegramGateway
|
||||
/// <param name="limit">Размер выборки (прототип discovery_read: limit сообщений/тем).</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
/// <returns>ok + сообщения (форумы — по активным темам) либо ok=false + error="no_history".</returns>
|
||||
public Task<TelegramEvalReadDto> ReadForEvalAsync(string dialogId, int limit, CancellationToken ct);
|
||||
public Task<TelegramEvalReadDto> ReadForEvalAsync(
|
||||
string dialogId,
|
||||
int limit,
|
||||
CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Вступить в канал/группу по @username (discovery_join python L818–839).
|
||||
|
||||
@@ -89,7 +89,10 @@ public sealed class MtlsCertificates
|
||||
/// <param name="certificate">Клиентский сертификат из рукопожатия (null — RequireCertificate не выполнен).</param>
|
||||
/// <param name="chain">Цепочка стандартной проверки (игнорируется — пересобирается на нашу CA).</param>
|
||||
/// <param name="sslPolicyErrors">Ошибки стандартной проверки TLS.</param>
|
||||
public bool ValidateClientCertificate(X509Certificate2? certificate, X509Chain? chain, SslPolicyErrors sslPolicyErrors)
|
||||
public bool ValidateClientCertificate(
|
||||
X509Certificate2? certificate,
|
||||
X509Chain? chain,
|
||||
SslPolicyErrors sslPolicyErrors)
|
||||
{
|
||||
if (certificate is null)
|
||||
{
|
||||
@@ -132,7 +135,11 @@ public sealed class MtlsCertificates
|
||||
// certificate: Сертификат сервера из рукопожатия.
|
||||
// chain: Цепочка стандартной проверки (игнорируется — пересобирается на нашу CA).
|
||||
// sslPolicyErrors: Ошибки стандартной проверки TLS.
|
||||
private bool ValidateServerCertificate(object? sender, X509Certificate? certificate, X509Chain? chain, SslPolicyErrors sslPolicyErrors)
|
||||
private bool ValidateServerCertificate(
|
||||
object? sender,
|
||||
X509Certificate? certificate,
|
||||
X509Chain? chain,
|
||||
SslPolicyErrors sslPolicyErrors)
|
||||
{
|
||||
if (certificate is null)
|
||||
{
|
||||
@@ -222,7 +229,10 @@ public sealed class MtlsCertificates
|
||||
// configuredPath: Путь из env.
|
||||
// envKey: Env-ключ пути (для сообщения об ошибке).
|
||||
// role: Роль сертификата (для сообщения об ошибке).
|
||||
private static string RequireExistingFile(string configuredPath, string envKey, string role)
|
||||
private static string RequireExistingFile(
|
||||
string configuredPath,
|
||||
string envKey,
|
||||
string role)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(configuredPath))
|
||||
{
|
||||
|
||||
@@ -179,7 +179,11 @@ public sealed class AiConnectionChecker : IAiConnectionChecker
|
||||
// ok: Результат подключения.
|
||||
// message: Сообщение ветки.
|
||||
// Возвращает: DTO ответа (наружу — camelCase).
|
||||
private static AiCheckResultDto BuildResult(AiCheckRequest request, string name, bool ok, string message)
|
||||
private static AiCheckResultDto BuildResult(
|
||||
AiCheckRequest request,
|
||||
string name,
|
||||
bool ok,
|
||||
string message)
|
||||
{
|
||||
return new AiCheckResultDto(
|
||||
Ok: ok,
|
||||
@@ -216,7 +220,10 @@ public sealed class AiConnectionChecker : IAiConnectionChecker
|
||||
// apiStyle: Стиль API провайдера (null — OpenAI-совместимый).
|
||||
// modelsUri: URL списка моделей (валиден только при возврате true).
|
||||
// Возвращает: True — URL построен; False — base URL не абсолютный http(s) (SSRF-гейт).
|
||||
private static bool TryBuildModelsUri(string baseUrl, string? apiStyle, [NotNullWhen(true)] out Uri? modelsUri)
|
||||
private static bool TryBuildModelsUri(
|
||||
string baseUrl,
|
||||
string? apiStyle,
|
||||
[NotNullWhen(true)] out Uri? modelsUri)
|
||||
{
|
||||
modelsUri = null;
|
||||
if (!Uri.TryCreate(baseUrl, UriKind.Absolute, out Uri? parsed)
|
||||
@@ -241,7 +248,10 @@ public sealed class AiConnectionChecker : IAiConnectionChecker
|
||||
// httpRequest: Запрос списка моделей.
|
||||
// apiKey: Ключ открытым текстом (непустой — ветка ключа пройдена).
|
||||
// apiStyle: Стиль API провайдера.
|
||||
private static void AddAuthHeaders(HttpRequestMessage httpRequest, string apiKey, string? apiStyle)
|
||||
private static void AddAuthHeaders(
|
||||
HttpRequestMessage httpRequest,
|
||||
string apiKey,
|
||||
string? apiStyle)
|
||||
{
|
||||
if (apiStyle == AnthropicApiStyle)
|
||||
{
|
||||
|
||||
@@ -98,7 +98,10 @@ public sealed class BudgetedAiTools : IAiTools
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<AiEvaluateFitResultDto> EvaluateFitAsync(
|
||||
string text, string description, IReadOnlyCollection<string> keywords, CancellationToken ct)
|
||||
string text,
|
||||
string description,
|
||||
IReadOnlyCollection<string> keywords,
|
||||
CancellationToken ct)
|
||||
{
|
||||
BudgetStateDto state = await GateStateAsync(ct);
|
||||
if (state.Allowed)
|
||||
|
||||
@@ -128,7 +128,10 @@ public sealed class GrpcAiTools : IAiTools
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<AiEvaluateFitResultDto> EvaluateFitAsync(
|
||||
string text, string description, IReadOnlyCollection<string> keywords, CancellationToken ct)
|
||||
string text,
|
||||
string description,
|
||||
IReadOnlyCollection<string> keywords,
|
||||
CancellationToken ct)
|
||||
{
|
||||
TenantId tenantId = RequireTenant();
|
||||
try
|
||||
|
||||
@@ -210,7 +210,11 @@ public sealed class GrpcMlClient : IMlClient, IMlTrainClient
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task PushAsync(string text, string label, double delta, CancellationToken ct)
|
||||
public async Task PushAsync(
|
||||
string text,
|
||||
string label,
|
||||
double delta,
|
||||
CancellationToken ct)
|
||||
{
|
||||
// Обучение гарантированно и локально (Ruling 6): сигнал всегда пишется в MlOutbox, отправку батчами
|
||||
// делает MlOutboxFlushScheduler — и в Local-, и в gRPC-режиме (ml_client.py L6–7).
|
||||
@@ -332,7 +336,10 @@ public sealed class GrpcMlClient : IMlClient, IMlTrainClient
|
||||
// deadline: Лимит времени вызова.
|
||||
// ct: Токен отмены вызова.
|
||||
// Возвращает: Опции вызова с заголовками, deadline и отменой.
|
||||
private CallOptions CallOptions(string tenantId, TimeSpan deadline, CancellationToken ct)
|
||||
private CallOptions CallOptions(
|
||||
string tenantId,
|
||||
TimeSpan deadline,
|
||||
CancellationToken ct)
|
||||
=> new(
|
||||
headers: _connection.CreateMetadata(tenantId),
|
||||
deadline: DateTime.UtcNow.Add(deadline),
|
||||
|
||||
@@ -102,7 +102,11 @@ public sealed class GrpcTelegramClient : ITelegramGateway
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<TelegramAuthResultDto> StartPhoneAsync(string phone, int apiId, string apiHash, CancellationToken ct)
|
||||
public async Task<TelegramAuthResultDto> StartPhoneAsync(
|
||||
string phone,
|
||||
int apiId,
|
||||
string apiHash,
|
||||
CancellationToken ct)
|
||||
{
|
||||
TenantId tenantId = RequireTenant();
|
||||
try
|
||||
@@ -121,7 +125,10 @@ public sealed class GrpcTelegramClient : ITelegramGateway
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<TelegramAuthResultDto> StartQrAsync(int apiId, string apiHash, CancellationToken ct)
|
||||
public async Task<TelegramAuthResultDto> StartQrAsync(
|
||||
int apiId,
|
||||
string apiHash,
|
||||
CancellationToken ct)
|
||||
{
|
||||
TenantId tenantId = RequireTenant();
|
||||
try
|
||||
@@ -211,7 +218,10 @@ public sealed class GrpcTelegramClient : ITelegramGateway
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task SetMonitorAsync(string dialogId, bool enabled, CancellationToken ct)
|
||||
public async Task SetMonitorAsync(
|
||||
string dialogId,
|
||||
bool enabled,
|
||||
CancellationToken ct)
|
||||
{
|
||||
TenantId tenantId = RequireTenant();
|
||||
try
|
||||
@@ -245,7 +255,10 @@ public sealed class GrpcTelegramClient : ITelegramGateway
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<int> BackfillAsync(string dialogId, bool force, CancellationToken ct)
|
||||
public async Task<int> BackfillAsync(
|
||||
string dialogId,
|
||||
bool force,
|
||||
CancellationToken ct)
|
||||
{
|
||||
TenantId tenantId = RequireTenant();
|
||||
try
|
||||
@@ -263,7 +276,10 @@ public sealed class GrpcTelegramClient : ITelegramGateway
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<TelegramRecentMessageDto>> ReadRecentAsync(string dialogId, int limit, CancellationToken ct)
|
||||
public async Task<IReadOnlyList<TelegramRecentMessageDto>> ReadRecentAsync(
|
||||
string dialogId,
|
||||
int limit,
|
||||
CancellationToken ct)
|
||||
{
|
||||
TenantId tenantId = RequireTenant();
|
||||
try
|
||||
@@ -283,7 +299,10 @@ public sealed class GrpcTelegramClient : ITelegramGateway
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<TelegramDialogEntryDto>> SearchAsync(string query, int limit, CancellationToken ct)
|
||||
public async Task<IReadOnlyList<TelegramDialogEntryDto>> SearchAsync(
|
||||
string query,
|
||||
int limit,
|
||||
CancellationToken ct)
|
||||
{
|
||||
TenantId tenantId = RequireTenant();
|
||||
try
|
||||
@@ -327,7 +346,10 @@ public sealed class GrpcTelegramClient : ITelegramGateway
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<TelegramEvalReadDto> ReadForEvalAsync(string dialogId, int limit, CancellationToken ct)
|
||||
public async Task<TelegramEvalReadDto> ReadForEvalAsync(
|
||||
string dialogId,
|
||||
int limit,
|
||||
CancellationToken ct)
|
||||
{
|
||||
TenantId tenantId = RequireTenant();
|
||||
try
|
||||
@@ -404,7 +426,9 @@ public sealed class GrpcTelegramClient : ITelegramGateway
|
||||
// call: Вызов клиента (принимает клиент и CallOptions).
|
||||
// Возвращает: Ответ RPC.
|
||||
private async Task<TReply> CallAsync<TReply>(
|
||||
TenantId tenantId, TimeSpan deadline, CancellationToken ct,
|
||||
TenantId tenantId,
|
||||
TimeSpan deadline,
|
||||
CancellationToken ct,
|
||||
Func<TelegramService.TelegramServiceClient, CallOptions, AsyncUnaryCall<TReply>> call)
|
||||
where TReply : class
|
||||
{
|
||||
@@ -424,7 +448,10 @@ public sealed class GrpcTelegramClient : ITelegramGateway
|
||||
// tenantId: Id тенанта (лог).
|
||||
// operation: Имя RPC (лог-аудит).
|
||||
// Возвращает: Исключение для проброса: транспортный сбой — нормализованный RpcException.
|
||||
private Exception TranslateTransportFailure(Exception exception, TenantId tenantId, string operation)
|
||||
private Exception TranslateTransportFailure(
|
||||
Exception exception,
|
||||
TenantId tenantId,
|
||||
string operation)
|
||||
{
|
||||
// Отмена по токену вызывающего — не ошибка сервиса (пробрасываем как обычно).
|
||||
if (exception is OperationCanceledException)
|
||||
|
||||
@@ -31,6 +31,9 @@ public sealed class LocalAiTools : IAiTools
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<AiEvaluateFitResultDto> EvaluateFitAsync(
|
||||
string text, string description, IReadOnlyCollection<string> keywords, CancellationToken ct)
|
||||
string text,
|
||||
string description,
|
||||
IReadOnlyCollection<string> keywords,
|
||||
CancellationToken ct)
|
||||
=> throw new NotSupportedException(NotSupportedMessage);
|
||||
}
|
||||
|
||||
@@ -103,7 +103,11 @@ public sealed class LocalMlClient(ISettingsStore store, IMlLearningStore learnin
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task PushAsync(string text, string label, double delta, CancellationToken ct)
|
||||
public async Task PushAsync(
|
||||
string text,
|
||||
string label,
|
||||
double delta,
|
||||
CancellationToken ct)
|
||||
{
|
||||
// Обучение гарантированно и локально (ml_client.push L40–49): действие пользователя — строка
|
||||
// очереди MlOutbox (отправку в ML-сервис делает воркер этапа 6). Общая логика (trim text/label,
|
||||
|
||||
@@ -34,11 +34,18 @@ public sealed class LocalTelegramGateway : ITelegramGateway
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<TelegramAuthResultDto> StartPhoneAsync(string phone, int apiId, string apiHash, CancellationToken ct)
|
||||
public Task<TelegramAuthResultDto> StartPhoneAsync(
|
||||
string phone,
|
||||
int apiId,
|
||||
string apiHash,
|
||||
CancellationToken ct)
|
||||
=> Task.FromResult(new TelegramAuthResultDto(IdlePhase, null));
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<TelegramAuthResultDto> StartQrAsync(int apiId, string apiHash, CancellationToken ct)
|
||||
public Task<TelegramAuthResultDto> StartQrAsync(
|
||||
int apiId,
|
||||
string apiHash,
|
||||
CancellationToken ct)
|
||||
=> Task.FromResult(new TelegramAuthResultDto(IdlePhase, null));
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -55,20 +62,32 @@ public sealed class LocalTelegramGateway : ITelegramGateway
|
||||
=> Task.FromResult<IReadOnlyList<TelegramDialogEntryDto>>([]);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task SetMonitorAsync(string dialogId, bool enabled, CancellationToken ct) => Task.CompletedTask;
|
||||
public Task SetMonitorAsync(
|
||||
string dialogId,
|
||||
bool enabled,
|
||||
CancellationToken ct) => Task.CompletedTask;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task SetMonitorAllAsync(bool enabled, CancellationToken ct) => Task.CompletedTask;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<int> BackfillAsync(string dialogId, bool force, CancellationToken ct) => Task.FromResult(0);
|
||||
public Task<int> BackfillAsync(
|
||||
string dialogId,
|
||||
bool force,
|
||||
CancellationToken ct) => Task.FromResult(0);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<TelegramRecentMessageDto>> ReadRecentAsync(string dialogId, int limit, CancellationToken ct)
|
||||
public Task<IReadOnlyList<TelegramRecentMessageDto>> ReadRecentAsync(
|
||||
string dialogId,
|
||||
int limit,
|
||||
CancellationToken ct)
|
||||
=> Task.FromResult<IReadOnlyList<TelegramRecentMessageDto>>([]);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<TelegramDialogEntryDto>> SearchAsync(string query, int limit, CancellationToken ct)
|
||||
public Task<IReadOnlyList<TelegramDialogEntryDto>> SearchAsync(
|
||||
string query,
|
||||
int limit,
|
||||
CancellationToken ct)
|
||||
=> Task.FromResult<IReadOnlyList<TelegramDialogEntryDto>>([]);
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -76,7 +95,10 @@ public sealed class LocalTelegramGateway : ITelegramGateway
|
||||
=> Task.FromResult(new TelegramChannelInfoDto(dialogId, string.Empty, string.Empty, string.Empty, SourceDefaults.DefaultHue, null, false));
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<TelegramEvalReadDto> ReadForEvalAsync(string dialogId, int limit, CancellationToken ct)
|
||||
public Task<TelegramEvalReadDto> ReadForEvalAsync(
|
||||
string dialogId,
|
||||
int limit,
|
||||
CancellationToken ct)
|
||||
=> Task.FromResult(new TelegramEvalReadDto(false, "no_history", []));
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -35,7 +35,12 @@ internal static class MlOutboxQueue
|
||||
/// <param name="delta">Вес сигнала (1.0 — учить, −1.0 — снять метку).</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
/// <returns>Задача завершается после записи строки (отправку делает фоновый флашер).</returns>
|
||||
public static async Task PushAsync(IMlLearningStore learningStore, string text, string label, double delta, CancellationToken ct)
|
||||
public static async Task PushAsync(
|
||||
IMlLearningStore learningStore,
|
||||
string text,
|
||||
string label,
|
||||
double delta,
|
||||
CancellationToken ct)
|
||||
{
|
||||
string trimmedText = (text ?? string.Empty).Trim();
|
||||
string trimmedLabel = (label ?? string.Empty).Trim();
|
||||
|
||||
@@ -91,7 +91,10 @@ public sealed class MlStatusCache
|
||||
/// <param name="tenantId">Id тенанта (формат N).</param>
|
||||
/// <param name="service">Статус модели.</param>
|
||||
/// <param name="reachable">Доступность сервиса.</param>
|
||||
public void Set(string tenantId, MlServiceStatusDto service, bool reachable)
|
||||
public void Set(
|
||||
string tenantId,
|
||||
MlServiceStatusDto service,
|
||||
bool reachable)
|
||||
{
|
||||
_entries[tenantId] = new Snapshot(service, reachable, _utcNow().ToUnixTimeMilliseconds());
|
||||
}
|
||||
|
||||
@@ -89,7 +89,11 @@ public sealed class TokenUsageRecorder
|
||||
/// <param name="provider">Id активного провайдера (deepseek/openai/anthropic/…; событие истории).</param>
|
||||
/// <param name="model">Модель провайдера (событие истории).</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
public async Task AddAsync(Usage? usage, string provider, string model, CancellationToken ct)
|
||||
public async Task AddAsync(
|
||||
Usage? usage,
|
||||
string provider,
|
||||
string model,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (usage is null)
|
||||
{
|
||||
@@ -124,7 +128,11 @@ public sealed class TokenUsageRecorder
|
||||
/// <param name="model">Модель/вид локального ML-вызова (событие истории).</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
/// <returns>Оценка токенов (для тестов/наблюдаемости).</returns>
|
||||
public async Task<long> AddEstimatedAsync(string? text, string provider, string model, CancellationToken ct)
|
||||
public async Task<long> AddEstimatedAsync(
|
||||
string? text,
|
||||
string provider,
|
||||
string model,
|
||||
CancellationToken ct)
|
||||
{
|
||||
long promptTokens = EstimateTokens(text);
|
||||
// Прикладная метрика (этап 12, пакет A): вызов локального ML + оценка токенов (та же точка,
|
||||
|
||||
@@ -47,7 +47,11 @@ public sealed class LocalFileStorage : IFileStorage
|
||||
public override string ToString() => $"LocalFileStorage (root: {_rootPath})";
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<string> PutAsync(string objectKey, Stream content, string contentType, CancellationToken ct)
|
||||
public async Task<string> PutAsync(
|
||||
string objectKey,
|
||||
Stream content,
|
||||
string contentType,
|
||||
CancellationToken ct)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(content);
|
||||
|
||||
|
||||
@@ -83,7 +83,11 @@ public sealed class MinioFileStorage : IFileStorage
|
||||
public override string ToString() => $"MinioFileStorage (endpoint: {_endpoint}; bucket: {_bucket})";
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<string> PutAsync(string objectKey, Stream content, string contentType, CancellationToken ct)
|
||||
public async Task<string> PutAsync(
|
||||
string objectKey,
|
||||
Stream content,
|
||||
string contentType,
|
||||
CancellationToken ct)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(content);
|
||||
|
||||
|
||||
@@ -105,7 +105,10 @@ public sealed class AuthStore(DealDbContext dbContext) : IAuthStore
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task UpdatePasswordHashAsync(Guid userId, string passwordHash, CancellationToken ct)
|
||||
public async Task UpdatePasswordHashAsync(
|
||||
Guid userId,
|
||||
string passwordHash,
|
||||
CancellationToken ct)
|
||||
{
|
||||
await dbContext.Users
|
||||
.Where(u => u.Id == userId)
|
||||
|
||||
@@ -18,7 +18,11 @@ namespace Deal.Infrastructure.Persistence.Repositories;
|
||||
public sealed partial class DiscoveryStore
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task UpsertBlacklistAsync(string dialogId, string name, string reason, CancellationToken ct)
|
||||
public async Task UpsertBlacklistAsync(
|
||||
string dialogId,
|
||||
string name,
|
||||
string reason,
|
||||
CancellationToken ct)
|
||||
{
|
||||
DiscBlacklistEntity? row = await _dbContext.DiscBlacklist
|
||||
.FirstOrDefaultAsync(entry => entry.DialogId == dialogId, ct);
|
||||
|
||||
@@ -19,7 +19,10 @@ namespace Deal.Infrastructure.Persistence.Repositories;
|
||||
public sealed partial class DiscoveryStore
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<DiscoveryCandidateDto>> ListCandidatesAsync(string taskId, string? status, CancellationToken ct)
|
||||
public async Task<IReadOnlyList<DiscoveryCandidateDto>> ListCandidatesAsync(
|
||||
string taskId,
|
||||
string? status,
|
||||
CancellationToken ct)
|
||||
{
|
||||
IQueryable<DiscCandidateEntity> query = _dbContext.DiscCandidates.AsNoTracking().Where(candidate => candidate.TaskId == taskId);
|
||||
if (status is not null)
|
||||
@@ -78,7 +81,10 @@ public sealed partial class DiscoveryStore
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> PatchCandidateAsync(string dialogId, DiscoveryCandidatePatch patch, CancellationToken ct)
|
||||
public async Task<bool> PatchCandidateAsync(
|
||||
string dialogId,
|
||||
DiscoveryCandidatePatch patch,
|
||||
CancellationToken ct)
|
||||
{
|
||||
DiscCandidateEntity? row = await _dbContext.DiscCandidates
|
||||
.FirstOrDefaultAsync(candidate => candidate.DialogId == dialogId, ct);
|
||||
@@ -94,7 +100,10 @@ public sealed partial class DiscoveryStore
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> SetCandidateStatusAsync(string dialogId, string status, CancellationToken ct)
|
||||
public async Task<bool> SetCandidateStatusAsync(
|
||||
string dialogId,
|
||||
string status,
|
||||
CancellationToken ct)
|
||||
{
|
||||
DiscCandidateEntity? row = await _dbContext.DiscCandidates
|
||||
.FirstOrDefaultAsync(candidate => candidate.DialogId == dialogId, ct);
|
||||
@@ -110,7 +119,10 @@ public sealed partial class DiscoveryStore
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> SetCandidateJoinedAsync(string dialogId, bool autoJoined, CancellationToken ct)
|
||||
public async Task<bool> SetCandidateJoinedAsync(
|
||||
string dialogId,
|
||||
bool autoJoined,
|
||||
CancellationToken ct)
|
||||
{
|
||||
DiscCandidateEntity? row = await _dbContext.DiscCandidates
|
||||
.FirstOrDefaultAsync(candidate => candidate.DialogId == dialogId, ct);
|
||||
|
||||
@@ -18,7 +18,12 @@ namespace Deal.Infrastructure.Persistence.Repositories;
|
||||
public sealed partial class DiscoveryStore
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task AddLogAsync(string logId, string taskId, string logEvent, string text, CancellationToken ct)
|
||||
public async Task AddLogAsync(
|
||||
string logId,
|
||||
string taskId,
|
||||
string logEvent,
|
||||
string text,
|
||||
CancellationToken ct)
|
||||
{
|
||||
_dbContext.DiscLog.Add(new DiscLogEntity
|
||||
{
|
||||
@@ -32,14 +37,20 @@ public sealed partial class DiscoveryStore
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<int> CountLogEventAsync(string logEvent, DateTimeOffset sinceUtc, CancellationToken ct)
|
||||
public async Task<int> CountLogEventAsync(
|
||||
string logEvent,
|
||||
DateTimeOffset sinceUtc,
|
||||
CancellationToken ct)
|
||||
{
|
||||
// ban_guard.joins_today_auto L29–35: число событий лога по типу с начала UTC-суток (счётчик квоты).
|
||||
return await _dbContext.DiscLog.CountAsync(row => row.Event == logEvent && row.CreatedAt >= sinceUtc, ct);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<DiscoveryLogDto>> ListTaskLogAsync(string taskId, int limit, CancellationToken ct)
|
||||
public async Task<IReadOnlyList<DiscoveryLogDto>> ListTaskLogAsync(
|
||||
string taskId,
|
||||
int limit,
|
||||
CancellationToken ct)
|
||||
{
|
||||
List<DiscLogEntity> rows = await _dbContext.DiscLog
|
||||
.AsNoTracking()
|
||||
|
||||
@@ -61,7 +61,10 @@ public sealed partial class DiscoveryStore
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> PatchTaskAsync(string taskId, DiscoveryTaskPatch patch, CancellationToken ct)
|
||||
public async Task<bool> PatchTaskAsync(
|
||||
string taskId,
|
||||
DiscoveryTaskPatch patch,
|
||||
CancellationToken ct)
|
||||
{
|
||||
DiscTaskEntity? row = await _dbContext.DiscTasks
|
||||
.FirstOrDefaultAsync(task => task.Id == taskId, ct);
|
||||
@@ -95,7 +98,10 @@ public sealed partial class DiscoveryStore
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> SetTaskRunningAsync(string taskId, bool resetProgress, CancellationToken ct)
|
||||
public async Task<bool> SetTaskRunningAsync(
|
||||
string taskId,
|
||||
bool resetProgress,
|
||||
CancellationToken ct)
|
||||
{
|
||||
DiscTaskEntity? row = await _dbContext.DiscTasks
|
||||
.FirstOrDefaultAsync(task => task.Id == taskId, ct);
|
||||
@@ -155,7 +161,11 @@ public sealed partial class DiscoveryStore
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> BumpTaskCounterAsync(string taskId, DiscoveryCounterField field, int n, CancellationToken ct)
|
||||
public async Task<bool> BumpTaskCounterAsync(
|
||||
string taskId,
|
||||
DiscoveryCounterField field,
|
||||
int n,
|
||||
CancellationToken ct)
|
||||
{
|
||||
DiscTaskEntity? row = await _dbContext.DiscTasks
|
||||
.FirstOrDefaultAsync(task => task.Id == taskId, ct);
|
||||
@@ -189,7 +199,11 @@ public sealed partial class DiscoveryStore
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> AdvanceSearchAsync(string taskId, int nextIndex, bool searchDone, CancellationToken ct)
|
||||
public async Task<bool> AdvanceSearchAsync(
|
||||
string taskId,
|
||||
int nextIndex,
|
||||
bool searchDone,
|
||||
CancellationToken ct)
|
||||
{
|
||||
DiscTaskEntity? row = await _dbContext.DiscTasks
|
||||
.FirstOrDefaultAsync(task => task.Id == taskId, ct);
|
||||
|
||||
@@ -29,7 +29,10 @@ public sealed class GlobalSettingsStore(DealDbContext dbContext) : IGlobalSettin
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task SetAsync(string key, string valueJson, CancellationToken ct)
|
||||
public async Task SetAsync(
|
||||
string key,
|
||||
string valueJson,
|
||||
CancellationToken ct)
|
||||
{
|
||||
// Upsert по ключу (PK): существующая строка обновляется, отсутствующая — добавляется.
|
||||
GlobalSettingEntity? entity = await dbContext.GlobalSettings
|
||||
|
||||
@@ -57,7 +57,11 @@ public sealed class InviteStore(DealDbContext dbContext) : IInviteStore
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> UpdateStatusAsync(string code, string status, DateTimeOffset? activatedAt, CancellationToken ct)
|
||||
public async Task<bool> UpdateStatusAsync(
|
||||
string code,
|
||||
string status,
|
||||
DateTimeOffset? activatedAt,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var entity = await dbContext.Invites.SingleOrDefaultAsync(i => i.Code == code, ct);
|
||||
if (entity is null)
|
||||
@@ -72,7 +76,10 @@ public sealed class InviteStore(DealDbContext dbContext) : IInviteStore
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> TryActivateAsync(string code, DateTimeOffset activatedAt, CancellationToken ct)
|
||||
public async Task<bool> TryActivateAsync(
|
||||
string code,
|
||||
DateTimeOffset activatedAt,
|
||||
CancellationToken ct)
|
||||
{
|
||||
// CAS (Task 6): атомарный условный UPDATE — pending → activated только если статус всё ещё pending,
|
||||
// иначе параллельный отзыв/активация не перезаписываются (ExecuteUpdate выполняется одним оператором SQL).
|
||||
|
||||
@@ -39,7 +39,10 @@ public sealed partial class KanbanStore
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<CardDto>> SearchCardsAsync(string q, int limit, CancellationToken ct)
|
||||
public async Task<IReadOnlyList<CardDto>> SearchCardsAsync(
|
||||
string q,
|
||||
int limit,
|
||||
CancellationToken ct)
|
||||
{
|
||||
string query = q.Trim();
|
||||
if (query.Length == 0)
|
||||
@@ -87,7 +90,10 @@ public sealed partial class KanbanStore
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<CardDto?> GetCardBySourceAsync(string dialogId, long msgId, CancellationToken ct)
|
||||
public async Task<CardDto?> GetCardBySourceAsync(
|
||||
string dialogId,
|
||||
long msgId,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (string.IsNullOrEmpty(dialogId))
|
||||
{
|
||||
@@ -192,7 +198,10 @@ public sealed partial class KanbanStore
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task UpdateSeenAsync(string? cardId, string? col, CancellationToken ct)
|
||||
public async Task UpdateSeenAsync(
|
||||
string? cardId,
|
||||
string? col,
|
||||
CancellationToken ct)
|
||||
{
|
||||
// Три режима leads.py mark_seen L250–256: одна карточка | колонка | все карточки.
|
||||
IQueryable<CardEntity> queryable = _dbContext.Cards;
|
||||
|
||||
@@ -33,7 +33,12 @@ public sealed partial class KanbanStore
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task AddCommentAsync(string commentId, string cardId, string by, string text, CancellationToken ct)
|
||||
public async Task AddCommentAsync(
|
||||
string commentId,
|
||||
string cardId,
|
||||
string by,
|
||||
string text,
|
||||
CancellationToken ct)
|
||||
{
|
||||
// CreatedAt — UTC-now (leads.py add_comment L259–265). Целостность ссылки на карточку держит FK:
|
||||
// комментарий без карточки не запишется (404-семантику несуществующей карточки отдаёт сервис,
|
||||
|
||||
@@ -93,7 +93,10 @@ public sealed partial class KanbanStore
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task ReorderContainersAsync(string space, IReadOnlyList<string> containerIds, CancellationToken ct)
|
||||
public async Task ReorderContainersAsync(
|
||||
string space,
|
||||
IReadOnlyList<string> containerIds,
|
||||
CancellationToken ct)
|
||||
{
|
||||
// Позиции 0..N-1 — одна транзакция: при сбое середины порядок не остаётся частично проставленным.
|
||||
await using var transaction = await _dbContext.Database.BeginTransactionAsync(ct);
|
||||
|
||||
@@ -33,7 +33,10 @@ public sealed partial class KanbanStore
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> PatchCardAsync(string cardId, CardPatch patch, CancellationToken ct)
|
||||
public async Task<bool> PatchCardAsync(
|
||||
string cardId,
|
||||
CardPatch patch,
|
||||
CancellationToken ct)
|
||||
{
|
||||
// Точечная правка по присутствующим полям патча: null-поле не меняется, JSON-поля заменяются
|
||||
// целиком, в конце — bump UpdatedAt (patch_card projects.py L159–187).
|
||||
@@ -71,7 +74,10 @@ public sealed partial class KanbanStore
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> AddLinkAsync(string cardId, CardLinkDto link, CancellationToken ct)
|
||||
public async Task<bool> AddLinkAsync(
|
||||
string cardId,
|
||||
CardLinkDto link,
|
||||
CancellationToken ct)
|
||||
{
|
||||
// Один UPDATE — jsonb-append в конец массива links (add_link L133–143 + bump UpdatedAt).
|
||||
return await ExecuteJsonMutationAsync(
|
||||
@@ -86,7 +92,10 @@ public sealed partial class KanbanStore
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> RemoveLinkAsync(string cardId, string linkId, CancellationToken ct)
|
||||
public async Task<bool> RemoveLinkAsync(
|
||||
string cardId,
|
||||
string linkId,
|
||||
CancellationToken ct)
|
||||
{
|
||||
// Один UPDATE — jsonb-фильтрация массива по id элемента (remove_link L146–150 + bump UpdatedAt).
|
||||
return await ExecuteJsonMutationAsync(
|
||||
@@ -105,7 +114,10 @@ public sealed partial class KanbanStore
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> AddFileAsync(string cardId, CardFileDto file, CancellationToken ct)
|
||||
public async Task<bool> AddFileAsync(
|
||||
string cardId,
|
||||
CardFileDto file,
|
||||
CancellationToken ct)
|
||||
{
|
||||
// Один UPDATE — jsonb-append в конец массива files (files.py add_file L57–75 + bump UpdatedAt).
|
||||
return await ExecuteJsonMutationAsync(
|
||||
@@ -120,7 +132,10 @@ public sealed partial class KanbanStore
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> RemoveFileAsync(string cardId, string fileId, CancellationToken ct)
|
||||
public async Task<bool> RemoveFileAsync(
|
||||
string cardId,
|
||||
string fileId,
|
||||
CancellationToken ct)
|
||||
{
|
||||
// Один UPDATE — jsonb-фильтрация массива files по id записи (files.py remove_file L86–94 + bump).
|
||||
return await ExecuteJsonMutationAsync(
|
||||
@@ -140,7 +155,11 @@ public sealed partial class KanbanStore
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> MoveCardStageAsync(
|
||||
string cardId, string containerId, CardHistoryDto historyEntry, long atMs, CancellationToken ct)
|
||||
string cardId,
|
||||
string containerId,
|
||||
CardHistoryDto historyEntry,
|
||||
long atMs,
|
||||
CancellationToken ct)
|
||||
{
|
||||
// Смена контейнера-стадии: Col=containerId, сброс напоминания, updated_at=atMs, история + запись
|
||||
// (move_stage projects.py L202–216; Ruling 7). Нетрекинговое чтение + ExecuteUpdate.
|
||||
@@ -169,7 +188,10 @@ public sealed partial class KanbanStore
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task SetReminderAsync(string cardId, long atMs, CancellationToken ct)
|
||||
public async Task SetReminderAsync(
|
||||
string cardId,
|
||||
long atMs,
|
||||
CancellationToken ct)
|
||||
{
|
||||
await _dbContext.Cards
|
||||
.Where(card => card.Id == cardId)
|
||||
|
||||
@@ -41,7 +41,10 @@ public sealed partial class KanbanStore
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<int> ArchiveAsync(IReadOnlyList<string> cardIds, DateTimeOffset archivedAt, CancellationToken ct)
|
||||
public async Task<int> ArchiveAsync(
|
||||
IReadOnlyList<string> cardIds,
|
||||
DateTimeOffset archivedAt,
|
||||
CancellationToken ct)
|
||||
{
|
||||
// Автоархив тика пачкой (tick_storage L462–473): один UPDATE вместо N по-карточных — как по-карточный
|
||||
// UpdateColumnAsync автоархива: col=archive, is_new=false, archived_at=now, matchHits пусто;
|
||||
@@ -121,7 +124,12 @@ public sealed partial class KanbanStore
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task UpdateConversionAsync(string cardId, double? convFrom, double? convTo, string convCur, CancellationToken ct)
|
||||
public async Task UpdateConversionAsync(
|
||||
string cardId,
|
||||
double? convFrom,
|
||||
double? convTo,
|
||||
string convCur,
|
||||
CancellationToken ct)
|
||||
{
|
||||
// Пишутся только conv-поля карточки (Ruling 7); convCur пуст — конверсия снята.
|
||||
await _dbContext.Cards
|
||||
|
||||
@@ -29,7 +29,12 @@ public sealed class MlLearningStore(TenantDbContext dbContext) : IMlLearningStor
|
||||
public Task<int> CountOutboxAsync(CancellationToken ct) => dbContext.MlOutbox.CountAsync(ct);
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task AddOutboxAsync(string id, string text, string label, double delta, CancellationToken ct)
|
||||
public async Task AddOutboxAsync(
|
||||
string id,
|
||||
string text,
|
||||
string label,
|
||||
double delta,
|
||||
CancellationToken ct)
|
||||
{
|
||||
dbContext.MlOutbox.Add(new MlOutboxEntity
|
||||
{
|
||||
|
||||
@@ -36,7 +36,10 @@ public sealed class PipelineStore(TenantDbContext dbContext) : IPipelineStore
|
||||
// ── Очередь (QueueItems) ───────────────────────────────────────────────
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<bool> ExistsDuplicateAsync(string dialogId, long? msgId, CancellationToken ct)
|
||||
public Task<bool> ExistsDuplicateAsync(
|
||||
string dialogId,
|
||||
long? msgId,
|
||||
CancellationToken ct)
|
||||
{
|
||||
// Дубль-гвард приёма (Ruling 2, enqueue L70–77): msgId null — проверять нечего.
|
||||
return msgId is null
|
||||
@@ -52,7 +55,10 @@ public sealed class PipelineStore(TenantDbContext dbContext) : IPipelineStore
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<QueueItemDto>> ListAsync(string? status, int limit, CancellationToken ct)
|
||||
public async Task<IReadOnlyList<QueueItemDto>> ListAsync(
|
||||
string? status,
|
||||
int limit,
|
||||
CancellationToken ct)
|
||||
{
|
||||
IQueryable<QueueItemEntity> queryable = dbContext.QueueItems.AsNoTracking();
|
||||
if (status is not null)
|
||||
@@ -72,7 +78,10 @@ public sealed class PipelineStore(TenantDbContext dbContext) : IPipelineStore
|
||||
=> dbContext.QueueItems.CountAsync(item => item.Status == status, ct);
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task SetStatusAsync(string id, string status, CancellationToken ct)
|
||||
public async Task SetStatusAsync(
|
||||
string id,
|
||||
string status,
|
||||
CancellationToken ct)
|
||||
{
|
||||
// Смена статуса строки очереди (new → filtered в воркере): UpdatedAt = UTC-now (порт).
|
||||
await dbContext.QueueItems
|
||||
@@ -131,7 +140,10 @@ public sealed class PipelineStore(TenantDbContext dbContext) : IPipelineStore
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<RejectedItemDto>> ListPageAsync(int offset, int limit, CancellationToken ct)
|
||||
public async Task<IReadOnlyList<RejectedItemDto>> ListPageAsync(
|
||||
int offset,
|
||||
int limit,
|
||||
CancellationToken ct)
|
||||
{
|
||||
List<RejectedItemEntity> entities = await dbContext.RejectedItems
|
||||
.AsNoTracking()
|
||||
@@ -143,7 +155,11 @@ public sealed class PipelineStore(TenantDbContext dbContext) : IPipelineStore
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<RejectedItemDto>> SearchAsync(string q, int limitFts, int limitLike, CancellationToken ct)
|
||||
public async Task<IReadOnlyList<RejectedItemDto>> SearchAsync(
|
||||
string q,
|
||||
int limitFts,
|
||||
int limitLike,
|
||||
CancellationToken ct)
|
||||
{
|
||||
string query = q.Trim();
|
||||
if (query.Length == 0)
|
||||
@@ -231,7 +247,11 @@ public sealed class PipelineStore(TenantDbContext dbContext) : IPipelineStore
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task MarkReturnedAsync(string id, string reason, DateTimeOffset returnedAt, CancellationToken ct)
|
||||
public async Task MarkReturnedAsync(
|
||||
string id,
|
||||
string reason,
|
||||
DateTimeOffset returnedAt,
|
||||
CancellationToken ct)
|
||||
{
|
||||
// Возврат из отсева: аудит-поля записи, строка не удаляется (return_to_queue L156–159).
|
||||
await dbContext.RejectedItems
|
||||
@@ -277,7 +297,10 @@ public sealed class PipelineStore(TenantDbContext dbContext) : IPipelineStore
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task LinkAsync(string hash, string cardId, CancellationToken ct)
|
||||
public async Task LinkAsync(
|
||||
string hash,
|
||||
string cardId,
|
||||
CancellationToken ct)
|
||||
{
|
||||
// Связь заявки с созданной карточкой (Ruling 4, порядок AddCard → LinkDedup L512–513).
|
||||
await dbContext.DedupEntries
|
||||
|
||||
@@ -46,7 +46,11 @@ public sealed class RateLimitCounterStore(DealDbContext dbContext) : IRateLimitC
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<int> IncrementAsync(
|
||||
string key, DateTimeOffset windowStart, DateTimeOffset windowEnd, int amount, CancellationToken ct)
|
||||
string key,
|
||||
DateTimeOffset windowStart,
|
||||
DateTimeOffset windowEnd,
|
||||
int amount,
|
||||
CancellationToken ct)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(key);
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(amount);
|
||||
@@ -94,7 +98,11 @@ public sealed class RateLimitCounterStore(DealDbContext dbContext) : IRateLimitC
|
||||
// ct: Токен отмены.
|
||||
// Возвращает: Значение счётчика после операции.
|
||||
private async Task<int> IncrementNpgsqlAsync(
|
||||
string key, DateTimeOffset windowStart, DateTimeOffset windowEnd, int amount, CancellationToken ct)
|
||||
string key,
|
||||
DateTimeOffset windowStart,
|
||||
DateTimeOffset windowEnd,
|
||||
int amount,
|
||||
CancellationToken ct)
|
||||
{
|
||||
DbConnection connection = dbContext.Database.GetDbConnection();
|
||||
bool openedHere = connection.State != ConnectionState.Open;
|
||||
@@ -127,7 +135,10 @@ public sealed class RateLimitCounterStore(DealDbContext dbContext) : IRateLimitC
|
||||
// command: Команда upsert.
|
||||
// name: Имя параметра (с префиксом @).
|
||||
// value: Значение.
|
||||
private static void AddParameter(DbCommand command, string name, object value)
|
||||
private static void AddParameter(
|
||||
DbCommand command,
|
||||
string name,
|
||||
object value)
|
||||
{
|
||||
DbParameter parameter = command.CreateParameter();
|
||||
parameter.ParameterName = name;
|
||||
@@ -136,7 +147,10 @@ public sealed class RateLimitCounterStore(DealDbContext dbContext) : IRateLimitC
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<int> GetCountAsync(string key, DateTimeOffset windowStart, CancellationToken ct)
|
||||
public async Task<int> GetCountAsync(
|
||||
string key,
|
||||
DateTimeOffset windowStart,
|
||||
CancellationToken ct)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(key);
|
||||
|
||||
|
||||
@@ -41,7 +41,10 @@ public sealed class SettingsStore(TenantDbContext dbContext) : ISettingsStore
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task SetAsync(string key, string valueJson, CancellationToken ct)
|
||||
public async Task SetAsync(
|
||||
string key,
|
||||
string valueJson,
|
||||
CancellationToken ct)
|
||||
{
|
||||
// Upsert по ключу (PK): существующая строка обновляется, отсутствующая — добавляется.
|
||||
TenantSettingEntity? entity = await dbContext.Settings
|
||||
|
||||
@@ -22,7 +22,9 @@ public sealed class TelegramStore(TenantDbContext dbContext) : ITelegramStore
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<int> SyncFromTelegramAsync(
|
||||
IReadOnlyCollection<TelegramDialogEntryDto> entries, bool autoMonitorNew, CancellationToken ct)
|
||||
IReadOnlyCollection<TelegramDialogEntryDto> entries,
|
||||
bool autoMonitorNew,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (entries.Count == 0)
|
||||
{
|
||||
@@ -95,7 +97,10 @@ public sealed class TelegramStore(TenantDbContext dbContext) : ITelegramStore
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task SetMonitorAsync(string dialogId, bool enabled, CancellationToken ct)
|
||||
public Task SetMonitorAsync(
|
||||
string dialogId,
|
||||
bool enabled,
|
||||
CancellationToken ct)
|
||||
{
|
||||
return dbContext.Dialogs
|
||||
.Where(dialog => dialog.Id == dialogId)
|
||||
@@ -148,7 +153,12 @@ public sealed class TelegramStore(TenantDbContext dbContext) : ITelegramStore
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task UpsertDiscoveredMonitoredAsync(
|
||||
string dialogId, string name, string handle, string kind, string hue, CancellationToken ct)
|
||||
string dialogId,
|
||||
string name,
|
||||
string handle,
|
||||
string kind,
|
||||
string hue,
|
||||
CancellationToken ct)
|
||||
{
|
||||
// 1:1 python add_dialog_monitored L850–873 (INSERT/ON CONFLICT DO UPDATE): строка каталога после
|
||||
// discovery-вступления — метаданные источника, monitor=TRUE, backfilled=FALSE (разбор подхватит
|
||||
@@ -185,7 +195,11 @@ public sealed class TelegramStore(TenantDbContext dbContext) : ITelegramStore
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task SavePreviewAsync(
|
||||
string messageId, string dialogId, string text, DateTimeOffset msgAt, CancellationToken ct)
|
||||
string messageId,
|
||||
string dialogId,
|
||||
string text,
|
||||
DateTimeOffset msgAt,
|
||||
CancellationToken ct)
|
||||
{
|
||||
bool exists = await dbContext.TgMessages.AnyAsync(message => message.Id == messageId, ct);
|
||||
if (exists)
|
||||
@@ -205,7 +219,11 @@ public sealed class TelegramStore(TenantDbContext dbContext) : ITelegramStore
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task TouchDialogLastAsync(string dialogId, string text, DateTimeOffset at, CancellationToken ct)
|
||||
public Task TouchDialogLastAsync(
|
||||
string dialogId,
|
||||
string text,
|
||||
DateTimeOffset at,
|
||||
CancellationToken ct)
|
||||
{
|
||||
// python _on_message L270–273: last_text/last_at/updated_at одним моментом приёма.
|
||||
return dbContext.Dialogs
|
||||
@@ -219,7 +237,10 @@ public sealed class TelegramStore(TenantDbContext dbContext) : ITelegramStore
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<TelegramMessageDto>> ListMessagesAsync(string dialogId, int limit, CancellationToken ct)
|
||||
public async Task<IReadOnlyList<TelegramMessageDto>> ListMessagesAsync(
|
||||
string dialogId,
|
||||
int limit,
|
||||
CancellationToken ct)
|
||||
{
|
||||
List<TgMessageEntity> rows = await dbContext.TgMessages
|
||||
.Where(message => message.DialogId == dialogId)
|
||||
|
||||
@@ -75,7 +75,9 @@ public sealed class TenantLimitStore : ITenantLimitStore
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<TenantLimitDto> GetOrCreateAsync(
|
||||
Guid tenantId, CancellationToken ct, TokenLimitDefaults? defaults = null)
|
||||
Guid tenantId,
|
||||
CancellationToken ct,
|
||||
TokenLimitDefaults? defaults = null)
|
||||
{
|
||||
TenantLimitEntity entity = await LoadOrCreateAsync(tenantId, defaults ?? _defaults, ct);
|
||||
return ToLimitDto(entity);
|
||||
@@ -90,7 +92,10 @@ public sealed class TenantLimitStore : ITenantLimitStore
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<BudgetStateDto> AddUsageAsync(Guid tenantId, long tokens, CancellationToken ct)
|
||||
public async Task<BudgetStateDto> AddUsageAsync(
|
||||
Guid tenantId,
|
||||
long tokens,
|
||||
CancellationToken ct)
|
||||
{
|
||||
TenantLimitEntity entity = await LoadOrCreateAsync(tenantId, _defaults, ct);
|
||||
await ResetIfPeriodExpiredAsync(entity, ct);
|
||||
@@ -120,7 +125,11 @@ public sealed class TenantLimitStore : ITenantLimitStore
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<BudgetStateDto> UpdateBudgetAsync(Guid tenantId, long budgetTokens, string period, CancellationToken ct)
|
||||
public async Task<BudgetStateDto> UpdateBudgetAsync(
|
||||
Guid tenantId,
|
||||
long budgetTokens,
|
||||
string period,
|
||||
CancellationToken ct)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(budgetTokens);
|
||||
if (period != TenantLimitPeriods.Month && period != TenantLimitPeriods.Day)
|
||||
@@ -175,7 +184,10 @@ public sealed class TenantLimitStore : ITenantLimitStore
|
||||
// defaults: Дефолт-параметры создаваемой строки.
|
||||
// ct: Токен отмены.
|
||||
// Возвращает: Отслеживаемая строка лимита.
|
||||
private async Task<TenantLimitEntity> LoadOrCreateAsync(Guid tenantId, TokenLimitDefaults defaults, CancellationToken ct)
|
||||
private async Task<TenantLimitEntity> LoadOrCreateAsync(
|
||||
Guid tenantId,
|
||||
TokenLimitDefaults defaults,
|
||||
CancellationToken ct)
|
||||
{
|
||||
TenantLimitEntity? entity = await _dbContext.TenantLimits
|
||||
.SingleOrDefaultAsync(x => x.TenantId == tenantId, ct);
|
||||
|
||||
@@ -49,7 +49,10 @@ public sealed class TenantRepository(DealDbContext dbContext) : ITenantRepositor
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> UpdateStatusAsync(Guid id, string status, CancellationToken ct)
|
||||
public async Task<bool> UpdateStatusAsync(
|
||||
Guid id,
|
||||
string status,
|
||||
CancellationToken ct)
|
||||
{
|
||||
// Отслеживаемая запись + SaveChanges (не ExecuteUpdateAsync): операция редкая (операторская админка),
|
||||
// зато семантика проверяема на InMemory-провайдере в unit-тестах.
|
||||
|
||||
@@ -97,8 +97,7 @@ public sealed class TokenUsageEventStore(DealDbContext dbContext) : ITokenUsageE
|
||||
// source: Отфильтрованный запрос.
|
||||
// ct: Токен отмены.
|
||||
// Возвращает: Строки агрегатов по дням.
|
||||
private static async Task<IReadOnlyList<TokenUsageAggregateDto>> AggregateByDayAsync(
|
||||
IQueryable<TokenUsageEventEntity> source, CancellationToken ct)
|
||||
private static async Task<IReadOnlyList<TokenUsageAggregateDto>> AggregateByDayAsync(IQueryable<TokenUsageEventEntity> source, CancellationToken ct)
|
||||
{
|
||||
var rows = await source
|
||||
.GroupBy(e => new { e.At.Year, e.At.Month, e.At.Day })
|
||||
@@ -131,8 +130,7 @@ public sealed class TokenUsageEventStore(DealDbContext dbContext) : ITokenUsageE
|
||||
// source: Отфильтрованный запрос.
|
||||
// ct: Токен отмены.
|
||||
// Возвращает: Строки агрегатов по тенантам.
|
||||
private static async Task<IReadOnlyList<TokenUsageAggregateDto>> AggregateByTenantAsync(
|
||||
IQueryable<TokenUsageEventEntity> source, CancellationToken ct)
|
||||
private static async Task<IReadOnlyList<TokenUsageAggregateDto>> AggregateByTenantAsync(IQueryable<TokenUsageEventEntity> source, CancellationToken ct)
|
||||
{
|
||||
var rows = await source
|
||||
.GroupBy(e => e.TenantId)
|
||||
|
||||
@@ -62,8 +62,7 @@ public static class ServiceCollectionExtensions
|
||||
/// <param name="tenantLimitDefaults">Дефолт-бюджет лениво создаваемых строк tenant_limits (Ruling 3; null — константа TokenBudgetDefaults).
|
||||
/// Передаётся из конфигурации/env DEAL_DEFAULT_AI_BUDGET в Program.cs (Task 8).</param>
|
||||
/// <returns>Коллекция сервисов для цепочки вызовов.</returns>
|
||||
public static IServiceCollection AddDealPersistence(
|
||||
this IServiceCollection services, TokenLimitDefaults? tenantLimitDefaults = null)
|
||||
public static IServiceCollection AddDealPersistence(this IServiceCollection services, TokenLimitDefaults? tenantLimitDefaults = null)
|
||||
{
|
||||
services.AddScoped<IAuthStore, AuthStore>();
|
||||
|
||||
@@ -168,8 +167,11 @@ public static class ServiceCollectionExtensions
|
||||
/// см. Program.cs (Tasks 6/8).
|
||||
/// </remarks>
|
||||
public static IServiceCollection AddDealIntegrations(
|
||||
this IServiceCollection services, MlServiceOptions mlOptions, AiServiceOptions aiOptions,
|
||||
TelegramServiceOptions telegramOptions, MtlsCertificates? mtlsCertificates = null)
|
||||
this IServiceCollection services,
|
||||
MlServiceOptions mlOptions,
|
||||
AiServiceOptions aiOptions,
|
||||
TelegramServiceOptions telegramOptions,
|
||||
MtlsCertificates? mtlsCertificates = null)
|
||||
{
|
||||
// Сертификаты mTLS-каналов (Ruling 6, Task 13): null (флаг DEAL_MTLS_ENABLED выключен) — каналы
|
||||
// остаются plaintext + service-token (dev); при включённом флаге каждый транспорт подписывает запрос
|
||||
|
||||
@@ -26,7 +26,10 @@ public sealed class CardMover(CardsService cardsService) : ICardMover
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<CardMoveResultDto> MoveAsync(
|
||||
string cardId, string toContainerId, TransitionContext ctx, CancellationToken ct)
|
||||
string cardId,
|
||||
string toContainerId,
|
||||
TransitionContext ctx,
|
||||
CancellationToken ct)
|
||||
{
|
||||
// Контекст перехода (инициатор/обучение) учтён внутри маршрутов: дашборд-перенос обучает ML по
|
||||
// цели пользователя, переход по стадии пишет историю и сбрасывает напоминание. Отдельного
|
||||
|
||||
@@ -52,7 +52,10 @@ public static class DefaultContainerProvisioner
|
||||
/// <param name="db">Контекст схемы тенанта.</param>
|
||||
/// <param name="now">Текущее UTC-время (CreatedAt новых строк).</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
public static async Task EnsureAsync(TenantDbContext db, DateTimeOffset now, CancellationToken ct)
|
||||
public static async Task EnsureAsync(
|
||||
TenantDbContext db,
|
||||
DateTimeOffset now,
|
||||
CancellationToken ct)
|
||||
{
|
||||
HashSet<string> existing = (await db.Containers
|
||||
.AsNoTracking()
|
||||
@@ -104,7 +107,12 @@ public static class DefaultContainerProvisioner
|
||||
// now: Время создания строки.
|
||||
// Возвращает: Строка контейнера.
|
||||
private static ContainerEntity BuildService(
|
||||
string id, string name, string color, int order, ContainerPolicyDto policy, DateTimeOffset now)
|
||||
string id,
|
||||
string name,
|
||||
string color,
|
||||
int order,
|
||||
ContainerPolicyDto policy,
|
||||
DateTimeOffset now)
|
||||
{
|
||||
return new ContainerEntity
|
||||
{
|
||||
@@ -125,7 +133,10 @@ public static class DefaultContainerProvisioner
|
||||
// db: Контекст тенанта.
|
||||
// existing: Множество существующих id (пополняется).
|
||||
// container: Строка контейнера.
|
||||
private static void AddIfMissing(TenantDbContext db, HashSet<string> existing, ContainerEntity container)
|
||||
private static void AddIfMissing(
|
||||
TenantDbContext db,
|
||||
HashSet<string> existing,
|
||||
ContainerEntity container)
|
||||
{
|
||||
if (existing.Add(container.Id))
|
||||
{
|
||||
|
||||
@@ -66,7 +66,10 @@ public sealed class TenantProvisioningService(ConnectionStringProvider connectio
|
||||
// tenantId: Идентификатор тенанта.
|
||||
// schemaName: Имя схемы тенанта (для MigrationsHistoryTable).
|
||||
// ct: Токен отмены.
|
||||
private async Task ApplyTenantMigrationsAsync(TenantId tenantId, string schemaName, CancellationToken ct)
|
||||
private async Task ApplyTenantMigrationsAsync(
|
||||
TenantId tenantId,
|
||||
string schemaName,
|
||||
CancellationToken ct)
|
||||
{
|
||||
// Строка уже с Search Path=tenant_<id> (ForSchemaDdl) — таблицы бессхемной модели TenantDbContext
|
||||
// лягут в схему тенанта. DDL применяется мигратор-ролью при её наличии (см. ConnectionStringProvider).
|
||||
|
||||
@@ -24,5 +24,8 @@ public interface ICardMover
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
/// <returns>Результат: Error (400-текст отказа) | Exists=false (карточки нет, 404) | успех (Exists=true).</returns>
|
||||
public Task<CardMoveResultDto> MoveAsync(
|
||||
string cardId, string toContainerId, TransitionContext ctx, CancellationToken ct);
|
||||
string cardId,
|
||||
string toContainerId,
|
||||
TransitionContext ctx,
|
||||
CancellationToken ct);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,13 @@ public sealed record DefaultContainer
|
||||
/// <param name="order">Позиция в канбане.</param>
|
||||
/// <param name="terminal">Терминальная зона (только ручная очистка, без возврата).</param>
|
||||
/// <param name="restore">Возврат из контейнера разрешён.</param>
|
||||
public DefaultContainer(string id, string name, string color, int order, bool terminal, bool restore)
|
||||
public DefaultContainer(
|
||||
string id,
|
||||
string name,
|
||||
string color,
|
||||
int order,
|
||||
bool terminal,
|
||||
bool restore)
|
||||
{
|
||||
Id = id;
|
||||
Name = name;
|
||||
|
||||
@@ -52,7 +52,10 @@ public interface IDiscoveryStore
|
||||
/// <param name="patch">Изменяемые поля (null — поле не меняется; keywords — полная замена JSON-массива).</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
/// <returns>True — строка обновлена; false — задачи нет (404-семантика сервиса).</returns>
|
||||
public Task<bool> PatchTaskAsync(string taskId, DiscoveryTaskPatch patch, CancellationToken ct);
|
||||
public Task<bool> PatchTaskAsync(
|
||||
string taskId,
|
||||
DiscoveryTaskPatch patch,
|
||||
CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Удаляет задачу вместе с её кандидатами и логом (delete_task L314–318; чёрный список общий — не трогается).
|
||||
@@ -70,7 +73,10 @@ public interface IDiscoveryStore
|
||||
/// счётчики found/evaluated/joined/rejected=0 (L333–339); False — draft/paused: только статус (L342–344).</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
/// <returns>True — задача обновлена; false — строки нет.</returns>
|
||||
public Task<bool> SetTaskRunningAsync(string taskId, bool resetProgress, CancellationToken ct);
|
||||
public Task<bool> SetTaskRunningAsync(
|
||||
string taskId,
|
||||
bool resetProgress,
|
||||
CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Переводит задачу в paused (pause_task L349–356; прогресс поиска сохраняется).
|
||||
@@ -96,7 +102,11 @@ public interface IDiscoveryStore
|
||||
/// <param name="n">Приращение (вызывающий передаёт ≥1; ≤0 — no-op, как max(0, n) python).</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
/// <returns>True — задача обновлена; false — строки нет.</returns>
|
||||
public Task<bool> BumpTaskCounterAsync(string taskId, DiscoveryCounterField field, int n, CancellationToken ct);
|
||||
public Task<bool> BumpTaskCounterAsync(
|
||||
string taskId,
|
||||
DiscoveryCounterField field,
|
||||
int n,
|
||||
CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Продвигает индекс поиска (advance_search L371–380: search_idx = nextIndex, search_done, bump UpdatedAt).
|
||||
@@ -106,7 +116,11 @@ public interface IDiscoveryStore
|
||||
/// <param name="searchDone">search_done = nextIndex ≥ keywords.Count (считает сервис).</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
/// <returns>True — задача обновлена; false — строки нет.</returns>
|
||||
public Task<bool> AdvanceSearchAsync(string taskId, int nextIndex, bool searchDone, CancellationToken ct);
|
||||
public Task<bool> AdvanceSearchAsync(
|
||||
string taskId,
|
||||
int nextIndex,
|
||||
bool searchDone,
|
||||
CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Сумма plan_joins активных задач (python _active_plan_sum L85–91: status NOT IN done/failed).
|
||||
@@ -125,7 +139,10 @@ public interface IDiscoveryStore
|
||||
/// <param name="status">Статус-фильтр (см. <see cref="DiscoveryCandidateStatuses"/>); null — все статусы.</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
/// <returns>Кандидаты задачи (marks/topics — типизированными списками); пусто — кандидатов нет.</returns>
|
||||
public Task<IReadOnlyList<DiscoveryCandidateDto>> ListCandidatesAsync(string taskId, string? status, CancellationToken ct);
|
||||
public Task<IReadOnlyList<DiscoveryCandidateDto>> ListCandidatesAsync(
|
||||
string taskId,
|
||||
string? status,
|
||||
CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Кандидат по dialog_id (python _get_candidate L400–402; ключ — источник, кандидат один).
|
||||
@@ -143,7 +160,10 @@ public interface IDiscoveryStore
|
||||
/// <param name="sinceUtc">Нижняя граница CreatedAt (UTC; бан-гард передаёт начало текущих суток).</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
/// <returns>Число событий с начала суток (0 — событий нет).</returns>
|
||||
public Task<int> CountLogEventAsync(string logEvent, DateTimeOffset sinceUtc, CancellationToken ct);
|
||||
public Task<int> CountLogEventAsync(
|
||||
string logEvent,
|
||||
DateTimeOffset sinceUtc,
|
||||
CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Увеличивает join_failures кандидата, если запись жива и в статусе review (воркер L411–416; bump UpdatedAt).
|
||||
@@ -191,7 +211,10 @@ public interface IDiscoveryStore
|
||||
/// <param name="patch">Изменяемые поля (null — поле не меняется; marks/topics — полная замена JSON).</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
/// <returns>True — строка обновлена; false — кандидата нет.</returns>
|
||||
public Task<bool> PatchCandidateAsync(string dialogId, DiscoveryCandidatePatch patch, CancellationToken ct);
|
||||
public Task<bool> PatchCandidateAsync(
|
||||
string dialogId,
|
||||
DiscoveryCandidatePatch patch,
|
||||
CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Переводит кандидата в new/review (set_candidate_status L497–515: только эти статусы; bump UpdatedAt).
|
||||
@@ -200,7 +223,10 @@ public interface IDiscoveryStore
|
||||
/// <param name="status">Новый статус: new|review (валидирует сервис).</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
/// <returns>True — строка обновлена; false — кандидата нет.</returns>
|
||||
public Task<bool> SetCandidateStatusAsync(string dialogId, string status, CancellationToken ct);
|
||||
public Task<bool> SetCandidateStatusAsync(
|
||||
string dialogId,
|
||||
string status,
|
||||
CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Ставит кандидату joined + auto_joined (mark_joined L531–534; bump UpdatedAt).
|
||||
@@ -209,7 +235,10 @@ public interface IDiscoveryStore
|
||||
/// <param name="autoJoined">True — авто-вступление воркера; false — ручное (join из UI).</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
/// <returns>True — строка обновлена; false — кандидата нет.</returns>
|
||||
public Task<bool> SetCandidateJoinedAsync(string dialogId, bool autoJoined, CancellationToken ct);
|
||||
public Task<bool> SetCandidateJoinedAsync(
|
||||
string dialogId,
|
||||
bool autoJoined,
|
||||
CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Ставит кандидату rejected (mark_rejected L555–558; bump UpdatedAt; счётчик/лог/чёрный список — сервис).
|
||||
@@ -228,7 +257,11 @@ public interface IDiscoveryStore
|
||||
/// <param name="name">Имя источника (вызывающий передаёт нормализованное: пусто → DialogId).</param>
|
||||
/// <param name="reason">Причина добавления.</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
public Task UpsertBlacklistAsync(string dialogId, string name, string reason, CancellationToken ct);
|
||||
public Task UpsertBlacklistAsync(
|
||||
string dialogId,
|
||||
string name,
|
||||
string reason,
|
||||
CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Снимает источник с чёрного списка (remove_blacklist L581–582; нет строки — no-op).
|
||||
@@ -262,7 +295,12 @@ public interface IDiscoveryStore
|
||||
/// <param name="logEvent">Событие (см. <see cref="DiscoveryLogEvents"/>).</param>
|
||||
/// <param name="text">Текст/детали события.</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
public Task AddLogAsync(string logId, string taskId, string logEvent, string text, CancellationToken ct);
|
||||
public Task AddLogAsync(
|
||||
string logId,
|
||||
string taskId,
|
||||
string logEvent,
|
||||
string text,
|
||||
CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Последние события задачи, новые сверху (task_log L601–608: ORDER BY created_at DESC LIMIT).
|
||||
@@ -271,5 +309,8 @@ public interface IDiscoveryStore
|
||||
/// <param name="limit">Сколько последних записей (кламп 1..500 выполняет сервис; дефолт 100).</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
/// <returns>События задачи от новых к старым; пусто — лога нет.</returns>
|
||||
public Task<IReadOnlyList<DiscoveryLogDto>> ListTaskLogAsync(string taskId, int limit, CancellationToken ct);
|
||||
public Task<IReadOnlyList<DiscoveryLogDto>> ListTaskLogAsync(
|
||||
string taskId,
|
||||
int limit,
|
||||
CancellationToken ct);
|
||||
}
|
||||
|
||||
@@ -40,7 +40,10 @@ public sealed class DiscoveryBanGuard
|
||||
/// <param name="settings">KV-настройки тенанта (discJoinLimit/discFloodDay/discPaused).</param>
|
||||
/// <param name="utcNow">Источник текущего времени (UTC; тесты передают фиксированные «часы», эталон
|
||||
/// FakeDiscoveryStore). По умолчанию — <see cref="DateTimeOffset.UtcNow"/>.</param>
|
||||
public DiscoveryBanGuard(IDiscoveryStore store, ISettingsStore settings, Func<DateTimeOffset>? utcNow = null)
|
||||
public DiscoveryBanGuard(
|
||||
IDiscoveryStore store,
|
||||
ISettingsStore settings,
|
||||
Func<DateTimeOffset>? utcNow = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(store);
|
||||
ArgumentNullException.ThrowIfNull(settings);
|
||||
|
||||
@@ -26,7 +26,11 @@ public sealed class DiscoveryBlacklistService(IDiscoveryStore store)
|
||||
/// <param name="reason">Причина добавления («отклонено вручную», метка воркера).</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
/// <returns>Запись чёрного списка (echo после upsert; CreatedAt — первое добавление).</returns>
|
||||
public async Task<DiscoveryBlacklistDto> AddAsync(string dialogId, string name, string reason, CancellationToken ct)
|
||||
public async Task<DiscoveryBlacklistDto> AddAsync(
|
||||
string dialogId,
|
||||
string name,
|
||||
string reason,
|
||||
CancellationToken ct)
|
||||
{
|
||||
string normalizedName = (name ?? string.Empty).Trim();
|
||||
if (normalizedName.Length == 0)
|
||||
|
||||
@@ -58,7 +58,10 @@ public sealed class DiscoveryCandidatesService(
|
||||
/// <param name="status">Статус-фильтр (new|review|joined|rejected); null — все.</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
/// <returns>Кандидаты задачи (marks/topics — списками).</returns>
|
||||
public Task<IReadOnlyList<DiscoveryCandidateDto>> ListAsync(string taskId, string? status, CancellationToken ct)
|
||||
public Task<IReadOnlyList<DiscoveryCandidateDto>> ListAsync(
|
||||
string taskId,
|
||||
string? status,
|
||||
CancellationToken ct)
|
||||
{
|
||||
return store.ListCandidatesAsync(taskId, status, ct);
|
||||
}
|
||||
@@ -91,7 +94,13 @@ public sealed class DiscoveryCandidatesService(
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
/// <returns>Новый кандидат либо null — источник пропущен (в лог записан skip).</returns>
|
||||
public async Task<DiscoveryCandidateDto?> AddAsync(
|
||||
string taskId, string dialogId, string name, string username, string kind, string hue, CancellationToken ct)
|
||||
string taskId,
|
||||
string dialogId,
|
||||
string name,
|
||||
string username,
|
||||
string kind,
|
||||
string hue,
|
||||
CancellationToken ct)
|
||||
{
|
||||
DiscoveryTaskDto? task = await store.GetTaskAsync(taskId, ct).ConfigureAwait(false);
|
||||
if (task is null)
|
||||
@@ -156,7 +165,10 @@ public sealed class DiscoveryCandidatesService(
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
/// <returns>Обновлённый кандидат либо null — задачи/кандидата нет (или кандидат другой задачи).</returns>
|
||||
public async Task<DiscoveryCandidateDto?> SetAsync(
|
||||
string taskId, string dialogId, DiscoveryCandidatePatch patch, CancellationToken ct)
|
||||
string taskId,
|
||||
string dialogId,
|
||||
DiscoveryCandidatePatch patch,
|
||||
CancellationToken ct)
|
||||
{
|
||||
DiscoveryTaskDto? task = await store.GetTaskAsync(taskId, ct).ConfigureAwait(false);
|
||||
DiscoveryCandidateDto? current = await store.GetCandidateAsync(dialogId, ct).ConfigureAwait(false);
|
||||
@@ -190,7 +202,10 @@ public sealed class DiscoveryCandidatesService(
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
/// <returns>Обновлённый кандидат либо null (кандидата нет).</returns>
|
||||
/// <exception cref="DiscoveryValidationException">Статус не new/review.</exception>
|
||||
public async Task<DiscoveryCandidateDto?> SetStatusAsync(string dialogId, string status, CancellationToken ct)
|
||||
public async Task<DiscoveryCandidateDto?> SetStatusAsync(
|
||||
string dialogId,
|
||||
string status,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!DiscoveryCandidateStatuses.IsTransitionAllowed(status))
|
||||
{
|
||||
@@ -223,7 +238,10 @@ public sealed class DiscoveryCandidatesService(
|
||||
/// <param name="auto">True — авто-вступление воркера; false — ручное (join из UI, Task 19).</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
/// <returns>Кандидат в joined либо null (кандидата нет). Повторный вызов для joined — идемпотентен (L528–529).</returns>
|
||||
public async Task<DiscoveryCandidateDto?> MarkJoinedAsync(string dialogId, bool auto, CancellationToken ct)
|
||||
public async Task<DiscoveryCandidateDto?> MarkJoinedAsync(
|
||||
string dialogId,
|
||||
bool auto,
|
||||
CancellationToken ct)
|
||||
{
|
||||
DiscoveryCandidateDto? current = await store.GetCandidateAsync(dialogId, ct).ConfigureAwait(false);
|
||||
if (current is null)
|
||||
@@ -251,7 +269,10 @@ public sealed class DiscoveryCandidatesService(
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
/// <returns>Кандидат в rejected либо null (кандидата нет). Повторный вызов для rejected — идемпотентен (L552–553).</returns>
|
||||
/// <exception cref="DiscoveryValidationException">Источник уже joined — отклонить нельзя.</exception>
|
||||
public async Task<DiscoveryCandidateDto?> MarkRejectedAsync(string dialogId, string reason, CancellationToken ct)
|
||||
public async Task<DiscoveryCandidateDto?> MarkRejectedAsync(
|
||||
string dialogId,
|
||||
string reason,
|
||||
CancellationToken ct)
|
||||
{
|
||||
DiscoveryCandidateDto? current = await store.GetCandidateAsync(dialogId, ct).ConfigureAwait(false);
|
||||
if (current is null)
|
||||
|
||||
@@ -68,7 +68,10 @@ public sealed class DiscoveryEvaluator
|
||||
/// <param name="settings">KV-настройки тенанта (mlEnabled/aiEnabled — ветки каскада).</param>
|
||||
/// <param name="mlClient">ML-порт (спам-отсев при mlEnabled; сбой — «не уверен»).</param>
|
||||
/// <param name="aiTools">ИИ-порт (фит при aiEnabled; сбой — эвристика, Ruling 10).</param>
|
||||
public DiscoveryEvaluator(ISettingsStore settings, IMlClient mlClient, IAiTools aiTools)
|
||||
public DiscoveryEvaluator(
|
||||
ISettingsStore settings,
|
||||
IMlClient mlClient,
|
||||
IAiTools aiTools)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(settings);
|
||||
ArgumentNullException.ThrowIfNull(mlClient);
|
||||
@@ -88,7 +91,9 @@ public sealed class DiscoveryEvaluator
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
/// <returns>Агрегат выборки: fit_count, total, fit_ratio, per-message.</returns>
|
||||
public async Task<DiscoveryEvalSample> EvaluateSampleAsync(
|
||||
DiscoveryTaskDto task, IReadOnlyList<string> texts, CancellationToken ct)
|
||||
DiscoveryTaskDto task,
|
||||
IReadOnlyList<string> texts,
|
||||
CancellationToken ct)
|
||||
{
|
||||
// Флаги веток (mlEnabled/aiEnabled) не меняются в пределах выборки: один типизированный снимок
|
||||
// настроек на выборку (C30) вместо двух GetAsync на каждое сообщение.
|
||||
@@ -121,7 +126,10 @@ public sealed class DiscoveryEvaluator
|
||||
/// <param name="text">Текст сообщения.</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
/// <returns>Вердикт: fit + причина + источник (heuristic|ml|ai).</returns>
|
||||
public async Task<DiscoveryMessageFit> EvaluateMessageAsync(DiscoveryTaskDto task, string text, CancellationToken ct)
|
||||
public async Task<DiscoveryMessageFit> EvaluateMessageAsync(
|
||||
DiscoveryTaskDto task,
|
||||
string text,
|
||||
CancellationToken ct)
|
||||
{
|
||||
TenantSettingsSnapshot settingsSnapshot = await TenantSettingsSnapshot.LoadAsync(_settings, ct).ConfigureAwait(false);
|
||||
return await EvaluateMessageCoreAsync(task, text, settingsSnapshot, ct).ConfigureAwait(false);
|
||||
@@ -134,7 +142,10 @@ public sealed class DiscoveryEvaluator
|
||||
// ct: Токен отмены.
|
||||
// Возвращает: Вердикт: fit + причина + источник (heuristic|ml|ai).
|
||||
private async Task<DiscoveryMessageFit> EvaluateMessageCoreAsync(
|
||||
DiscoveryTaskDto task, string text, TenantSettingsSnapshot settingsSnapshot, CancellationToken ct)
|
||||
DiscoveryTaskDto task,
|
||||
string text,
|
||||
TenantSettingsSnapshot settingsSnapshot,
|
||||
CancellationToken ct)
|
||||
{
|
||||
string raw = text ?? string.Empty;
|
||||
if (raw.Trim().Length < MinTextLength)
|
||||
|
||||
@@ -34,7 +34,11 @@ public sealed class DiscoveryLogService(IDiscoveryStore store)
|
||||
/// <param name="text">Текст/детали события (русская строка 1:1 с прототипом).</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
/// <returns>Завершается после записи.</returns>
|
||||
public Task AddAsync(string taskId, string logEvent, string text, CancellationToken ct)
|
||||
public Task AddAsync(
|
||||
string taskId,
|
||||
string logEvent,
|
||||
string text,
|
||||
CancellationToken ct)
|
||||
{
|
||||
return store.AddLogAsync(DiscoveryIdPrefixes.NewLogId(), taskId, logEvent, text ?? string.Empty, ct);
|
||||
}
|
||||
@@ -57,7 +61,10 @@ public sealed class DiscoveryLogService(IDiscoveryStore store)
|
||||
/// <param name="limit">Запрошенный размер выборки.</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
/// <returns>События задачи от новых к старым.</returns>
|
||||
public Task<IReadOnlyList<DiscoveryLogDto>> TaskLogAsync(string taskId, int limit, CancellationToken ct)
|
||||
public Task<IReadOnlyList<DiscoveryLogDto>> TaskLogAsync(
|
||||
string taskId,
|
||||
int limit,
|
||||
CancellationToken ct)
|
||||
{
|
||||
int clamped = Math.Max(MinLogLimit, Math.Min(MaxLogLimit, limit));
|
||||
return store.ListTaskLogAsync(taskId, clamped, ct);
|
||||
|
||||
@@ -77,7 +77,10 @@ public sealed class DiscoveryPlanGuard(IDiscoveryStore store, ISettingsStore set
|
||||
/// <param name="excludeTaskId">Id задачи, исключаемой из занятого бюджета (patch-рост плана); null — создание.</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
/// <exception cref="DiscoveryValidationException">Бюджет исчерпан (текст python L108–110).</exception>
|
||||
public async Task AssertBudgetAsync(int planJoins, string? excludeTaskId, CancellationToken ct)
|
||||
public async Task AssertBudgetAsync(
|
||||
int planJoins,
|
||||
string? excludeTaskId,
|
||||
CancellationToken ct)
|
||||
{
|
||||
int limit = await PlanLimitAsync(ct).ConfigureAwait(false);
|
||||
int used = await store.SumActivePlanAsync(excludeTaskId, ct).ConfigureAwait(false);
|
||||
|
||||
@@ -128,7 +128,10 @@ public sealed class DiscoveryTasksService(IDiscoveryStore store, DiscoveryPlanGu
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
/// <returns>Обновлённая задача либо null (404 «Задача не найдена»).</returns>
|
||||
/// <exception cref="DiscoveryValidationException">Новый план вне границ / бюджет исчерпан.</exception>
|
||||
public async Task<DiscoveryTaskDto?> PatchAsync(string taskId, DiscoveryTaskPatch patch, CancellationToken ct)
|
||||
public async Task<DiscoveryTaskDto?> PatchAsync(
|
||||
string taskId,
|
||||
DiscoveryTaskPatch patch,
|
||||
CancellationToken ct)
|
||||
{
|
||||
DiscoveryTaskDto? current = await store.GetTaskAsync(taskId, ct).ConfigureAwait(false);
|
||||
if (current is null)
|
||||
@@ -226,7 +229,11 @@ public sealed class DiscoveryTasksService(IDiscoveryStore store, DiscoveryPlanGu
|
||||
/// <param name="n">Приращение (по умолчанию 1).</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
/// <returns>True — счётчик увеличен; false — задачи нет.</returns>
|
||||
public Task<bool> BumpCounterAsync(string taskId, DiscoveryCounterField field, int n, CancellationToken ct)
|
||||
public Task<bool> BumpCounterAsync(
|
||||
string taskId,
|
||||
DiscoveryCounterField field,
|
||||
int n,
|
||||
CancellationToken ct)
|
||||
{
|
||||
return store.BumpTaskCounterAsync(taskId, field, Math.Max(0, n), ct);
|
||||
}
|
||||
@@ -286,7 +293,10 @@ public sealed class DiscoveryTasksService(IDiscoveryStore store, DiscoveryPlanGu
|
||||
}
|
||||
|
||||
// Кламп значения в границы [min, max] (эталон clamp python).
|
||||
private static int Clamp(int value, int min, int max)
|
||||
private static int Clamp(
|
||||
int value,
|
||||
int min,
|
||||
int max)
|
||||
{
|
||||
return Math.Max(min, Math.Min(max, value));
|
||||
}
|
||||
|
||||
+3
-1
@@ -18,7 +18,9 @@ public sealed partial class DiscoveryWorkerService
|
||||
// ct: Токен отмены.
|
||||
// Возвращает: review — кандидат оценён; skip — пропущен (delete); error — сбой шага.
|
||||
private async Task<DiscoveryWorkerOutcome> EvalStepAsync(
|
||||
DiscoveryTaskDto task, DiscoveryCandidateDto candidate, CancellationToken ct)
|
||||
DiscoveryTaskDto task,
|
||||
DiscoveryCandidateDto candidate,
|
||||
CancellationToken ct)
|
||||
{
|
||||
string taskId = task.Id;
|
||||
string dialogId = candidate.DialogId;
|
||||
|
||||
@@ -18,7 +18,9 @@ public sealed partial class DiscoveryWorkerService
|
||||
// ct: Токен отмены.
|
||||
// Возвращает: join/reject/flood/error/skip — действие; none — выход без вступления (изменения за паузу).
|
||||
private async Task<DiscoveryWorkerOutcome> JoinStepAsync(
|
||||
DiscoveryTaskDto task, DiscoveryCandidateDto candidate, CancellationToken ct)
|
||||
DiscoveryTaskDto task,
|
||||
DiscoveryCandidateDto candidate,
|
||||
CancellationToken ct)
|
||||
{
|
||||
string taskId = task.Id;
|
||||
string dialogId = candidate.DialogId;
|
||||
|
||||
@@ -72,7 +72,10 @@ public interface ICardStore
|
||||
/// <param name="space">Пространство переставляемых контейнеров.</param>
|
||||
/// <param name="containerIds">Id контейнеров в новом порядке.</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
public Task ReorderContainersAsync(string space, IReadOnlyList<string> containerIds, CancellationToken ct);
|
||||
public Task ReorderContainersAsync(
|
||||
string space,
|
||||
IReadOnlyList<string> containerIds,
|
||||
CancellationToken ct);
|
||||
|
||||
// ── Карточки ──────────────────────────────────────────────────────────
|
||||
|
||||
@@ -100,7 +103,10 @@ public interface ICardStore
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
/// <returns>Полные карточки (комментарии приложены, time посчитан): FTS-кандидаты по
|
||||
/// убыванию ts_rank (SearchTsv @@ plainto_tsquery), затем LIKE-дополнение, внутри — ReceivedAt DESC.</returns>
|
||||
public Task<IReadOnlyList<CardDto>> SearchCardsAsync(string q, int limit, CancellationToken ct);
|
||||
public Task<IReadOnlyList<CardDto>> SearchCardsAsync(
|
||||
string q,
|
||||
int limit,
|
||||
CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Одна карточка по id (GET /api/cards/{id}, а также перечитывание после переноса).
|
||||
@@ -119,7 +125,10 @@ public interface ICardStore
|
||||
/// <param name="msgId">Id исходного сообщения в Telegram.</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
/// <returns>Карточка или null, если сообщение не становилось карточкой.</returns>
|
||||
public Task<CardDto?> GetCardBySourceAsync(string dialogId, long msgId, CancellationToken ct);
|
||||
public Task<CardDto?> GetCardBySourceAsync(
|
||||
string dialogId,
|
||||
long msgId,
|
||||
CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Создаёт карточку из готового снимка (пайплайн этапа 4, ручное создание; CreatedAt — UTC-now).
|
||||
@@ -135,7 +144,10 @@ public interface ICardStore
|
||||
/// <param name="patch">Изменяемые поля (null — поле не меняется; JSON-поля — полная замена).</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
/// <returns>True — строка обновлена; false — карточки нет (404-семантика сервиса).</returns>
|
||||
public Task<bool> PatchCardAsync(string cardId, CardPatch patch, CancellationToken ct);
|
||||
public Task<bool> PatchCardAsync(
|
||||
string cardId,
|
||||
CardPatch patch,
|
||||
CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Атомарно дописывает ссылку в JSON-массив links ОДНИМ UPDATE (jsonb-append) + bump UpdatedAt.
|
||||
@@ -144,7 +156,10 @@ public interface ICardStore
|
||||
/// <param name="link">Готовая ссылка {id,name,url} (id сгенерирован модулем).</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
/// <returns>True — строка обновлена; false — карточки нет (404-семантика сервиса).</returns>
|
||||
public Task<bool> AddLinkAsync(string cardId, CardLinkDto link, CancellationToken ct);
|
||||
public Task<bool> AddLinkAsync(
|
||||
string cardId,
|
||||
CardLinkDto link,
|
||||
CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Атомарно убирает из JSON-массива links элемент с указанным id ОДНИМ UPDATE (jsonb-фильтрация) + bump UpdatedAt.
|
||||
@@ -153,7 +168,10 @@ public interface ICardStore
|
||||
/// <param name="linkId">Id удаляемой ссылки (<c>pl_...</c>).</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
/// <returns>True — строка обновлена; false — карточки нет (404-семантика сервиса).</returns>
|
||||
public Task<bool> RemoveLinkAsync(string cardId, string linkId, CancellationToken ct);
|
||||
public Task<bool> RemoveLinkAsync(
|
||||
string cardId,
|
||||
string linkId,
|
||||
CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Атомарно дописывает метаданные файла в JSON-массив files ОДНИМ UPDATE (jsonb-append) + bump UpdatedAt.
|
||||
@@ -162,7 +180,10 @@ public interface ICardStore
|
||||
/// <param name="file">Готовые метаданные {id,name,size,kind,label,objectKey} (id сгенерирован модулем).</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
/// <returns>True — строка обновлена; false — карточки нет (404-семантика сервиса).</returns>
|
||||
public Task<bool> AddFileAsync(string cardId, CardFileDto file, CancellationToken ct);
|
||||
public Task<bool> AddFileAsync(
|
||||
string cardId,
|
||||
CardFileDto file,
|
||||
CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Атомарно убирает из JSON-массива files элемент с указанным id ОДНИМ UPDATE (jsonb-фильтрация) + bump UpdatedAt.
|
||||
@@ -171,7 +192,10 @@ public interface ICardStore
|
||||
/// <param name="fileId">Id удаляемой записи файла (<c>pf_...</c>).</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
/// <returns>True — строка обновлена; false — карточки нет (404-семантика сервиса).</returns>
|
||||
public Task<bool> RemoveFileAsync(string cardId, string fileId, CancellationToken ct);
|
||||
public Task<bool> RemoveFileAsync(
|
||||
string cardId,
|
||||
string fileId,
|
||||
CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Смена контейнера-стадии: один UPDATE (col, reminder_at=NULL, reminder_fired=false, updated_at=atMs)
|
||||
@@ -183,7 +207,12 @@ public interface ICardStore
|
||||
/// <param name="atMs">Время переноса, epoch-ms (пишется в updated_at и в запись истории).</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
/// <returns>True — строка обновлена; false — карточки нет (404-семантика сервиса).</returns>
|
||||
public Task<bool> MoveCardStageAsync(string cardId, string containerId, CardHistoryDto historyEntry, long atMs, CancellationToken ct);
|
||||
public Task<bool> MoveCardStageAsync(
|
||||
string cardId,
|
||||
string containerId,
|
||||
CardHistoryDto historyEntry,
|
||||
long atMs,
|
||||
CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Устанавливает напоминание: reminder_at, reminder_fired=false + bump UpdatedAt (set_reminder projects.py L236–243).
|
||||
@@ -191,7 +220,10 @@ public interface ICardStore
|
||||
/// <param name="cardId">Id карточки (<c>c_...</c>).</param>
|
||||
/// <param name="atMs">Время напоминания, epoch-ms.</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
public Task SetReminderAsync(string cardId, long atMs, CancellationToken ct);
|
||||
public Task SetReminderAsync(
|
||||
string cardId,
|
||||
long atMs,
|
||||
CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Снимает напоминание: reminder_at=NULL, reminder_fired=false (clear_reminder projects.py L246–247).
|
||||
@@ -253,7 +285,10 @@ public interface ICardStore
|
||||
/// <param name="cardId">Id карточки либо null.</param>
|
||||
/// <param name="col">Колонка либо null.</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
public Task UpdateSeenAsync(string? cardId, string? col, CancellationToken ct);
|
||||
public Task UpdateSeenAsync(
|
||||
string? cardId,
|
||||
string? col,
|
||||
CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Удаляет карточку навсегда: Cards + комментарии (FK cascade) + строки дедупа карточки
|
||||
@@ -297,7 +332,12 @@ public interface ICardStore
|
||||
/// <param name="by">Автор («Вы» — свои комментарии).</param>
|
||||
/// <param name="text">Текст (непустой — валидирует сервис, 400 «Пустой комментарий»).</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
public Task AddCommentAsync(string commentId, string cardId, string by, string text, CancellationToken ct);
|
||||
public Task AddCommentAsync(
|
||||
string commentId,
|
||||
string cardId,
|
||||
string by,
|
||||
string text,
|
||||
CancellationToken ct);
|
||||
|
||||
// ── Журнал действий (CardMoves = learning_log) ────────────────────────
|
||||
|
||||
@@ -348,7 +388,10 @@ public interface ICardStore
|
||||
/// <param name="archivedAt">Момент архивации (один «now» тика).</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
/// <returns>Сколько карточек реально архивировано (0 — кандидатов не было/уже не в рабочих колонках).</returns>
|
||||
public Task<int> ArchiveAsync(IReadOnlyList<string> cardIds, DateTimeOffset archivedAt, CancellationToken ct);
|
||||
public Task<int> ArchiveAsync(
|
||||
IReadOnlyList<string> cardIds,
|
||||
DateTimeOffset archivedAt,
|
||||
CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Кандидаты на очистку архива: col='archive' и ArchivedAt старше срока (tick_storage L475–478).
|
||||
@@ -392,7 +435,12 @@ public interface ICardStore
|
||||
/// <param name="convTo">Сконвертированная верхняя граница, либо null.</param>
|
||||
/// <param name="convCur">Валюта конверсии (целевая валюта тенанта); пусто — конверсия снята.</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
public Task UpdateConversionAsync(string cardId, double? convFrom, double? convTo, string convCur, CancellationToken ct);
|
||||
public Task UpdateConversionAsync(
|
||||
string cardId,
|
||||
double? convFrom,
|
||||
double? convTo,
|
||||
string convCur,
|
||||
CancellationToken ct);
|
||||
|
||||
// ── Эвристика ИИ-предложений (Ruling 3, Task 14) ───────────────────────
|
||||
|
||||
|
||||
@@ -41,7 +41,12 @@ public interface IMlLearningStore
|
||||
/// <param name="label">Метка обучения: id доски, <c>spam</c> либо <c>t:hire|t:order</c>.</param>
|
||||
/// <param name="delta">Вес сигнала (1.0 — учить, −1.0 — снять метку).</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
public Task AddOutboxAsync(string id, string text, string label, double delta, CancellationToken ct);
|
||||
public Task AddOutboxAsync(
|
||||
string id,
|
||||
string text,
|
||||
string label,
|
||||
double delta,
|
||||
CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Полная очистка очереди обучения: DELETE FROM MlOutbox (ml_client.reset_model L122). Журнал CardMoves не трогается.
|
||||
|
||||
@@ -68,7 +68,10 @@ public static class AmountParser
|
||||
var occupied = new List<(int Start, int End)>();
|
||||
|
||||
// Свободен ли диапазон (не пересекается с уже занятым конструкцией выше) — прототип _free L106–107.
|
||||
static bool IsFree(List<(int Start, int End)> spans, int start, int end)
|
||||
static bool IsFree(
|
||||
List<(int Start, int End)> spans,
|
||||
int start,
|
||||
int end)
|
||||
{
|
||||
foreach ((int os, int oe) in spans)
|
||||
{
|
||||
@@ -81,7 +84,12 @@ public static class AmountParser
|
||||
return true;
|
||||
}
|
||||
|
||||
void Add(double? from, double? to, string? cur, int start, int end)
|
||||
void Add(
|
||||
double? from,
|
||||
double? to,
|
||||
string? cur,
|
||||
int start,
|
||||
int end)
|
||||
{
|
||||
if (cur is not null && (from is not null || to is not null) && IsFree(occupied, start, end))
|
||||
{
|
||||
|
||||
@@ -28,7 +28,10 @@ public static class BudgetInRange
|
||||
/// True — есть сумма, которая после конвертации (при необходимости) ≥ from и ≤ to; обе границы null → True;
|
||||
/// пустые amounts или непереводимая валюта → False.
|
||||
/// </returns>
|
||||
public static bool IsInRange(IReadOnlyList<AmountRange> amounts, BudgetRangeDto budget, IReadOnlyDictionary<string, double>? rates)
|
||||
public static bool IsInRange(
|
||||
IReadOnlyList<AmountRange> amounts,
|
||||
BudgetRangeDto budget,
|
||||
IReadOnlyDictionary<string, double>? rates)
|
||||
{
|
||||
double? lo = budget.From;
|
||||
double? hi = budget.To;
|
||||
|
||||
@@ -24,7 +24,10 @@ public static class ColumnMatcher
|
||||
/// <param name="rates">Курсы для конвертации бюджета (см. <see cref="BudgetInRange"/>); null — если бюджет в
|
||||
/// другой валюте, суммы не конвертируются и группа бюджета не совпадает.</param>
|
||||
/// <returns>True — текст прошёл правила в режиме all/any.</returns>
|
||||
public static bool MatchText(ContainerRulesDto? rules, string? text, IReadOnlyDictionary<string, double>? rates = null)
|
||||
public static bool MatchText(
|
||||
ContainerRulesDto? rules,
|
||||
string? text,
|
||||
IReadOnlyDictionary<string, double>? rates = null)
|
||||
{
|
||||
if (rules is null)
|
||||
{
|
||||
|
||||
@@ -28,7 +28,10 @@ public static class ColumnRules
|
||||
/// False — сработало слово-исключение (veto) либо текст не прошёл активные правила;
|
||||
/// True — правил нет/неактивны или текст прошёл правила.
|
||||
/// </returns>
|
||||
public static bool ContainerAccepts(ContainerRulesDto? rules, string? text, IReadOnlyDictionary<string, double>? rates = null)
|
||||
public static bool ContainerAccepts(
|
||||
ContainerRulesDto? rules,
|
||||
string? text,
|
||||
IReadOnlyDictionary<string, double>? rates = null)
|
||||
{
|
||||
if (ColumnExclusions.IsExcluded(rules, text))
|
||||
{
|
||||
@@ -50,7 +53,10 @@ public static class ColumnRules
|
||||
/// <param name="text">Текст сообщения.</param>
|
||||
/// <param name="rates">Курсы для конвертации бюджета.</param>
|
||||
/// <returns>True — текст прошёл правила в режиме all/any (исключения не учитываются).</returns>
|
||||
public static bool Matches(ContainerRulesDto? rules, string? text, IReadOnlyDictionary<string, double>? rates = null)
|
||||
public static bool Matches(
|
||||
ContainerRulesDto? rules,
|
||||
string? text,
|
||||
IReadOnlyDictionary<string, double>? rates = null)
|
||||
{
|
||||
return ColumnMatcher.MatchText(rules, text, rates);
|
||||
}
|
||||
@@ -75,7 +81,10 @@ public static class ColumnRules
|
||||
/// Список совпавших критериев (MatchHitDto label/term/word); для пустых/null правил и колонок без
|
||||
/// активных правил — [] (Ruling 2). Исключения (veto) в список не входят (прототип hits L271–296).
|
||||
/// </returns>
|
||||
public static IReadOnlyList<MatchHitDto> ComputeHits(ContainerRulesDto? rules, string? text, IReadOnlyDictionary<string, double>? rates = null)
|
||||
public static IReadOnlyList<MatchHitDto> ComputeHits(
|
||||
ContainerRulesDto? rules,
|
||||
string? text,
|
||||
IReadOnlyDictionary<string, double>? rates = null)
|
||||
{
|
||||
if (!ColumnMatcher.HasActiveRules(rules))
|
||||
{
|
||||
|
||||
@@ -50,7 +50,10 @@ public static class MatchHitBuilder
|
||||
/// <param name="rates">Курсы для конвертации бюджета (см. <see cref="BudgetInRange"/>).</param>
|
||||
/// <returns>Список совпадений по группам; порядок — direction → keywords → stack → grade → levels →
|
||||
/// locations → types → budget → prices.</returns>
|
||||
public static IReadOnlyList<MatchHitDto> BuildHits(ContainerRulesDto? rules, string? text, IReadOnlyDictionary<string, double>? rates = null)
|
||||
public static IReadOnlyList<MatchHitDto> BuildHits(
|
||||
ContainerRulesDto? rules,
|
||||
string? text,
|
||||
IReadOnlyDictionary<string, double>? rates = null)
|
||||
{
|
||||
var result = new List<MatchHitDto>();
|
||||
if (rules is null)
|
||||
@@ -144,7 +147,10 @@ public static class MatchHitBuilder
|
||||
// label: Метка группы.
|
||||
// lower: Текст в нижнем регистре.
|
||||
// Возвращает: Hits по каждому совпавшему терму (term сохраняет регистр пользователя).
|
||||
private static IEnumerable<MatchHitDto> MatchedTermHits(IReadOnlyList<string>? rawTerms, string label, string lower)
|
||||
private static IEnumerable<MatchHitDto> MatchedTermHits(
|
||||
IReadOnlyList<string>? rawTerms,
|
||||
string label,
|
||||
string lower)
|
||||
{
|
||||
if (rawTerms is null)
|
||||
{
|
||||
|
||||
@@ -99,7 +99,10 @@ public static class RulesDescriber
|
||||
// parts: Накопитель частей описания.
|
||||
// range: Диапазон группы (null — группа выключена).
|
||||
// title: Подпись группы («бюджет»/«цена»).
|
||||
private static void AddRangePart(List<string> parts, BudgetRangeDto? range, string title)
|
||||
private static void AddRangePart(
|
||||
List<string> parts,
|
||||
BudgetRangeDto? range,
|
||||
string title)
|
||||
{
|
||||
if (range is null || (range.From is null && range.To is null))
|
||||
{
|
||||
|
||||
@@ -174,7 +174,11 @@ public static class BudgetNormalizer
|
||||
// toCurrency: Целевая валюта (код).
|
||||
// rates: Курсы к рублю либо null.
|
||||
// Возвращает: Сумма в целевой валюте или null.
|
||||
private static double? Convert(double amount, string fromCurrency, string toCurrency, IReadOnlyDictionary<string, double>? rates)
|
||||
private static double? Convert(
|
||||
double amount,
|
||||
string fromCurrency,
|
||||
string toCurrency,
|
||||
IReadOnlyDictionary<string, double>? rates)
|
||||
{
|
||||
return rates is null ? null : RatesService.ConvertAmount(amount, fromCurrency, toCurrency, rates);
|
||||
}
|
||||
|
||||
@@ -101,7 +101,10 @@ public sealed partial class CardsService
|
||||
/// <param name="fileId">Id записи файла (<c>pf_...</c>).</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
/// <returns>Метаданные записи файла либо null (карточка/запись не найдены).</returns>
|
||||
public async Task<CardFileDto?> GetFileEntryAsync(string cardId, string fileId, CancellationToken ct)
|
||||
public async Task<CardFileDto?> GetFileEntryAsync(
|
||||
string cardId,
|
||||
string fileId,
|
||||
CancellationToken ct)
|
||||
{
|
||||
CardDto? card = await _store.GetCardAsync(cardId, ct);
|
||||
if (card is null)
|
||||
@@ -124,7 +127,10 @@ public sealed partial class CardsService
|
||||
/// <param name="fileId">Id удаляемой записи файла (<c>pf_...</c>).</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
/// <returns>Карточка после удаления (без записи) либо null — карточки нет (404-семантика).</returns>
|
||||
public async Task<CardDto?> RemoveFileAsync(string cardId, string fileId, CancellationToken ct)
|
||||
public async Task<CardDto?> RemoveFileAsync(
|
||||
string cardId,
|
||||
string fileId,
|
||||
CancellationToken ct)
|
||||
{
|
||||
CardDto? card = await _store.GetCardAsync(cardId, ct);
|
||||
if (card is null)
|
||||
@@ -152,7 +158,10 @@ public sealed partial class CardsService
|
||||
// fileId: Id записи файла (pf_...; уникальный суффикс ключа).
|
||||
// name: Имя файла как прислано (в ключ идёт санитизированная часть).
|
||||
// Возвращает: Ключ объекта (opaque для хранилища).
|
||||
private static string BuildObjectKey(string cardId, string fileId, string name)
|
||||
private static string BuildObjectKey(
|
||||
string cardId,
|
||||
string fileId,
|
||||
string name)
|
||||
{
|
||||
return $"{ObjectRootSegment}/{cardId}/{fileId}_{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}_{SanitizeKeyName(name)}";
|
||||
}
|
||||
|
||||
@@ -42,7 +42,12 @@ public sealed partial class CardsService
|
||||
// hits: matchHits для новой колонки (пересчитаны вызывающим, Ruling 2).
|
||||
// action: Действие журнала: move/trash.
|
||||
// ct: Токен отмены.
|
||||
private async Task MoveToColumnAsync(CardDto card, string toCol, IReadOnlyList<MatchHitDto> hits, string action, CancellationToken ct)
|
||||
private async Task MoveToColumnAsync(
|
||||
CardDto card,
|
||||
string toCol,
|
||||
IReadOnlyList<MatchHitDto> hits,
|
||||
string action,
|
||||
CancellationToken ct)
|
||||
{
|
||||
await _store.UpdateColumnAsync(new CardColumnUpdateDto(
|
||||
CardId: card.Id,
|
||||
@@ -60,7 +65,12 @@ public sealed partial class CardsService
|
||||
// fromCol: Прежняя колонка (для comment — null).
|
||||
// toCol: Новая колонка (для comment — null).
|
||||
// ct: Токен отмены.
|
||||
private async Task LogMoveAsync(string cardId, string action, string? fromCol, string? toCol, CancellationToken ct)
|
||||
private async Task LogMoveAsync(
|
||||
string cardId,
|
||||
string action,
|
||||
string? fromCol,
|
||||
string? toCol,
|
||||
CancellationToken ct)
|
||||
{
|
||||
await _store.AddMoveAsync(new CardMoveDto(
|
||||
PrefixId.New(KanbanIdPrefixes.CardMove),
|
||||
@@ -75,7 +85,10 @@ public sealed partial class CardsService
|
||||
// text: Текст карточки для правил (source_msg или title).
|
||||
// ct: Токен отмены.
|
||||
// Возвращает: Список совпавших критериев; доски нет/правил нет → пусто.
|
||||
private async Task<IReadOnlyList<MatchHitDto>> ComputeHitsForBoardAsync(string boardId, string text, CancellationToken ct)
|
||||
private async Task<IReadOnlyList<MatchHitDto>> ComputeHitsForBoardAsync(
|
||||
string boardId,
|
||||
string text,
|
||||
CancellationToken ct)
|
||||
{
|
||||
ContainerDto? board = await _store.GetContainerAsync(boardId, ct);
|
||||
return board is null
|
||||
@@ -89,7 +102,10 @@ public sealed partial class CardsService
|
||||
// text: Текст карточки для правил.
|
||||
// ct: Токен отмены.
|
||||
// Возвращает: Совпавшие критерии (label/term[/word]); нет активных правил → пусто.
|
||||
private async Task<IReadOnlyList<MatchHitDto>> ComputeHitsAsync(ContainerRulesDto? rules, string text, CancellationToken ct)
|
||||
private async Task<IReadOnlyList<MatchHitDto>> ComputeHitsAsync(
|
||||
ContainerRulesDto? rules,
|
||||
string text,
|
||||
CancellationToken ct)
|
||||
{
|
||||
// Кэш курсов из типизированного снимка настроек (C30): null → мок-курсы (дефолт RatesService).
|
||||
IReadOnlyDictionary<string, double> rates =
|
||||
|
||||
@@ -66,7 +66,10 @@ public sealed partial class CardsService
|
||||
/// <param name="toCol">Цель: <c>inbox</c> либо id доски (<c>b_...</c>).</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
/// <returns>Результат: Error (400) | Card=null (карточки нет, 404) | Card — карточка после переноса.</returns>
|
||||
public async Task<CardResultDto> MoveDashboardCardAsync(string cardId, string toCol, CancellationToken ct)
|
||||
public async Task<CardResultDto> MoveDashboardCardAsync(
|
||||
string cardId,
|
||||
string toCol,
|
||||
CancellationToken ct)
|
||||
{
|
||||
ContainerDto? board = null;
|
||||
if (toCol != CardIds.Inbox)
|
||||
@@ -138,7 +141,10 @@ public sealed partial class CardsService
|
||||
/// <param name="teach">True — писать сигнал «спам» (действие пользователя); false — не писать.</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
/// <returns>Карточка после переноса (при no-op — как была) либо null — карточки нет (404).</returns>
|
||||
public async Task<CardDto?> TrashCardAsync(string cardId, bool teach, CancellationToken ct)
|
||||
public async Task<CardDto?> TrashCardAsync(
|
||||
string cardId,
|
||||
bool teach,
|
||||
CancellationToken ct)
|
||||
{
|
||||
CardDto? card = await _store.GetCardAsync(cardId, ct);
|
||||
if (card is null)
|
||||
@@ -255,7 +261,10 @@ public sealed partial class CardsService
|
||||
/// <param name="text">Текст комментария (непустой после Trim).</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
/// <returns>Результат: Error (400) | Comments=null (404) | Comments — список после добавления.</returns>
|
||||
public async Task<AddCommentResultDto> AddCommentAsync(string cardId, string text, CancellationToken ct)
|
||||
public async Task<AddCommentResultDto> AddCommentAsync(
|
||||
string cardId,
|
||||
string text,
|
||||
CancellationToken ct)
|
||||
{
|
||||
string trimmed = (text ?? string.Empty).Trim();
|
||||
if (trimmed.Length == 0)
|
||||
@@ -284,7 +293,10 @@ public sealed partial class CardsService
|
||||
/// <param name="cardId">Id карточки либо null/пусто.</param>
|
||||
/// <param name="col">Колонка либо null/пусто (используется, когда cardId не задан).</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
public Task MarkSeenAsync(string? cardId, string? col, CancellationToken ct)
|
||||
public Task MarkSeenAsync(
|
||||
string? cardId,
|
||||
string? col,
|
||||
CancellationToken ct)
|
||||
{
|
||||
return _store.UpdateSeenAsync(
|
||||
string.IsNullOrEmpty(cardId) ? null : cardId,
|
||||
|
||||
@@ -48,7 +48,10 @@ public sealed partial class CardsService
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
/// <returns>Результат: Error <see cref="RemindersDisabledDetail"/> (400) | Card=null без Error (404) |
|
||||
/// Card — карточка с напоминанием.</returns>
|
||||
public async Task<CardResultDto> SetReminderAsync(string cardId, long atMs, CancellationToken ct)
|
||||
public async Task<CardResultDto> SetReminderAsync(
|
||||
string cardId,
|
||||
long atMs,
|
||||
CancellationToken ct)
|
||||
{
|
||||
CardDto? card = await _store.GetCardAsync(cardId, ct);
|
||||
if (card is null)
|
||||
|
||||
@@ -168,7 +168,10 @@ public sealed partial class CardsService
|
||||
/// <param name="body">Тело PATCH: ключ → JSON-значение (наличие ключа = поле меняется).</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
/// <returns>Обновлённая карточка или null — карточки нет (404).</returns>
|
||||
public async Task<CardDto?> PatchCardAsync(string cardId, IReadOnlyDictionary<string, JsonElement> body, CancellationToken ct)
|
||||
public async Task<CardDto?> PatchCardAsync(
|
||||
string cardId,
|
||||
IReadOnlyDictionary<string, JsonElement> body,
|
||||
CancellationToken ct)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(body);
|
||||
|
||||
@@ -190,7 +193,11 @@ public sealed partial class CardsService
|
||||
/// <param name="url">URL ссылки (без схемы — добавится https://).</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
/// <returns>Результат: Error (400 «Пустая ссылка») | Card=null без Error (404) | Card — карточка со ссылкой.</returns>
|
||||
public async Task<CardResultDto> AddLinkAsync(string cardId, string name, string url, CancellationToken ct)
|
||||
public async Task<CardResultDto> AddLinkAsync(
|
||||
string cardId,
|
||||
string name,
|
||||
string url,
|
||||
CancellationToken ct)
|
||||
{
|
||||
CardDto? card = await _store.GetCardAsync(cardId, ct);
|
||||
if (card is null)
|
||||
@@ -236,7 +243,10 @@ public sealed partial class CardsService
|
||||
/// <param name="linkId">Id удаляемой ссылки (<c>pl_...</c>).</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
/// <returns>Результат: Card=null без Error (404) | Card — карточка без ссылки.</returns>
|
||||
public async Task<CardResultDto> RemoveLinkAsync(string cardId, string linkId, CancellationToken ct)
|
||||
public async Task<CardResultDto> RemoveLinkAsync(
|
||||
string cardId,
|
||||
string linkId,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!await _store.RemoveLinkAsync(cardId, linkId, ct))
|
||||
{
|
||||
@@ -262,7 +272,10 @@ public sealed partial class CardsService
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
/// <returns>Результат: Error «Неизвестная стадия» (400) | Card=null без Error (карточки нет, 404) |
|
||||
/// Card — карточка после переноса.</returns>
|
||||
public async Task<CardResultDto> MoveStageCardAsync(string cardId, string containerId, CancellationToken ct)
|
||||
public async Task<CardResultDto> MoveStageCardAsync(
|
||||
string cardId,
|
||||
string containerId,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!CardsDefaultContainers.Contains(containerId))
|
||||
{
|
||||
|
||||
@@ -52,7 +52,11 @@ public sealed partial class CardsService
|
||||
/// <param name="settings">KV-хранилище настроек тенанта (курсы, напоминания).</param>
|
||||
/// <param name="mlClient">Клиент ML: PushAsync — обучающий сигнал действия, StatusAsync — счётчики counts.</param>
|
||||
/// <param name="storage">Файловое хранилище вложений карточки (объекты файлов).</param>
|
||||
public CardsService(ICardStore store, ISettingsStore settings, IMlClient mlClient, IFileStorage storage)
|
||||
public CardsService(
|
||||
ICardStore store,
|
||||
ISettingsStore settings,
|
||||
IMlClient mlClient,
|
||||
IFileStorage storage)
|
||||
{
|
||||
_store = store;
|
||||
_settings = settings;
|
||||
|
||||
@@ -122,7 +122,10 @@ public sealed class ContainersService(ICardStore store, ISettingsStore settings)
|
||||
/// <param name="patch">Изменения; null-поле означает «не менять».</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
/// <returns>Контейнер после патча; null — контейнера нет (404 «Контейнер не найден»).</returns>
|
||||
public async Task<ContainerDto?> PatchAsync(string containerId, ContainerPatchDto patch, CancellationToken ct)
|
||||
public async Task<ContainerDto?> PatchAsync(
|
||||
string containerId,
|
||||
ContainerPatchDto patch,
|
||||
CancellationToken ct)
|
||||
{
|
||||
ContainerDto? current = await store.GetContainerAsync(containerId, ct);
|
||||
if (current is null)
|
||||
@@ -171,7 +174,10 @@ public sealed class ContainersService(ICardStore store, ISettingsStore settings)
|
||||
/// <param name="space">Пространство переставляемых контейнеров.</param>
|
||||
/// <param name="containerIds">Id контейнеров в новом порядке.</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
public Task ReorderAsync(string space, IReadOnlyList<string> containerIds, CancellationToken ct)
|
||||
public Task ReorderAsync(
|
||||
string space,
|
||||
IReadOnlyList<string> containerIds,
|
||||
CancellationToken ct)
|
||||
{
|
||||
return store.ReorderContainersAsync(space, containerIds, ct);
|
||||
}
|
||||
@@ -195,7 +201,10 @@ public sealed class ContainersService(ICardStore store, ISettingsStore settings)
|
||||
/// <param name="patch">Изменяемые поля состояния.</param>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
/// <returns>Состояние колонки после merge.</returns>
|
||||
public async Task<ColumnStateDto> PatchColStateAsync(string colId, ColumnStateDto patch, CancellationToken ct)
|
||||
public async Task<ColumnStateDto> PatchColStateAsync(
|
||||
string colId,
|
||||
ColumnStateDto patch,
|
||||
CancellationToken ct)
|
||||
{
|
||||
Dictionary<string, ColumnStateDto> state = await ReadColStateAsync(ct);
|
||||
state.TryGetValue(colId, out ColumnStateDto? current);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user