From 79c931d88ebc4cc4447c46e1f07fec3c09073b17 Mon Sep 17 00:00:00 2001 From: Rustam Khalimov Date: Fri, 11 Sep 2026 13:49:26 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=BE=D1=87=D0=B8=D1=81=D1=82=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D0=BA=D0=BE=D0=BC=D0=BC=D0=B5=D0=BD=D1=82=D0=B0?= =?UTF-8?q?=D1=80=D0=B8=D0=B8=20=D0=BE=D1=82=20=D1=81=D1=81=D1=8B=D0=BB?= =?UTF-8?q?=D0=BE=D0=BA=20=D0=BD=D0=B0=20=D0=A2=D0=97=20=D0=B8=20=D0=BE?= =?UTF-8?q?=D0=B1=D1=80=D1=8B=D0=B2=D0=BA=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Удаление целых //-блоков со ссылками (Task/Ruling/этап/ТЗ/§/ дизайн-док/api-map/python/прототип) вместо построчного вырезания — без обрывков фраз; снят боилерплейт /; то же в .proto. --- .../_slim_comments.cpython-312.pyc | Bin 9349 -> 10715 bytes src/ai-service/Deal.Ai.Tests/Ai/AiTestHost.cs | 6 -- .../Deal.Ai.Tests/Ai/FakeProviderClient.cs | 2 - .../Deal.Ai.Tests/Grpc/AiServiceHostTests.cs | 5 - .../Support/StubHttpMessageHandler.cs | 1 - src/ai-service/Deal.Ai/AiServiceHost.cs | 4 - src/ai-service/Deal.Ai/AiServiceImpl.cs | 14 --- src/ai-service/Deal.Ai/Llm/JsonExtractor.cs | 2 - src/ai-service/Deal.Ai/Llm/LlmHttpClient.cs | 12 --- src/ai-service/Deal.Ai/Program.cs | 11 -- src/contracts/telegram.proto | 4 - .../Deal.Api/Endpoints/AiCheckEndpoint.cs | 5 - .../Deal.Api/Endpoints/AiSuggestEndpoints.cs | 3 - src/core/Deal.Api/Endpoints/AuthEndpoints.cs | 4 - .../Endpoints/CardDetailsEndpoints.cs | 2 - src/core/Deal.Api/Endpoints/CardsEndpoints.cs | 10 -- .../Deal.Api/Endpoints/DiscoveryEndpoints.cs | 14 --- .../Deal.Api/Endpoints/MlCandidatesRequest.cs | 2 +- .../Endpoints/OperatorAnalyticsEndpoints.cs | 7 -- .../Endpoints/OperatorAuditEndpoints.cs | 12 --- .../Endpoints/OperatorAuthEndpoints.cs | 4 - .../Endpoints/OperatorTenantsEndpoints.cs | 1 - .../Deal.Api/Endpoints/PipelineEndpoints.cs | 1 - src/core/Deal.Api/Endpoints/RatesEndpoints.cs | 1 - .../Deal.Api/Endpoints/SettingsEndpoints.cs | 4 - .../Deal.Api/Endpoints/StorageEndpoints.cs | 5 - .../Deal.Api/Endpoints/TelegramEndpoints.cs | 5 - src/core/Deal.Api/Extensions/AuthHelpers.cs | 5 - .../Deal.Api/Hosting/BudgetAlertScheduler.cs | 1 - .../Hosting/MlOutboxFlushScheduler.cs | 6 -- .../Deal.Api/Hosting/StorageTickScheduler.cs | 10 -- .../Hosting/TenantBootstrapService.cs | 3 - src/core/Deal.Api/Logging/DealLogging.cs | 11 -- .../Middleware/HttpAccessLogMiddleware.cs | 1 - .../Middleware/OperatorSessionMiddleware.cs | 1 - .../Middleware/OriginGuardMiddleware.cs | 1 - .../Deal.Api/Middleware/RateLimitPolicies.cs | 4 - .../Deal.Api/Middleware/SessionMiddleware.cs | 1 - src/core/Deal.Api/Program.cs | 102 ------------------ .../Services/AdminTickOrchestrator.cs | 8 -- src/core/Deal.Api/Services/AuditAppender.cs | 2 - .../Deal.Api/Services/SessionCookieWriter.cs | 1 - .../Telegram/TelegramIngressService.cs | 15 --- src/core/Deal.Api/Telegram/TgStatusService.cs | 6 +- .../Abstractions/IAiClassifier.cs | 2 +- .../Abstractions/ITelegramGateway.cs | 2 +- .../Data/ConnectionStringProvider.cs | 1 - .../Services/AiConnectionChecker.cs | 11 -- .../Services/AiProviderConfigBuilder.cs | 2 - .../Services/BudgetedAiClassifier.cs | 4 - .../Integrations/Services/BudgetedAiTools.cs | 2 - .../Integrations/Services/CbrRateSource.cs | 3 - .../Integrations/Services/GrpcAiClassifier.cs | 11 -- .../Integrations/Services/GrpcAiTools.cs | 9 -- .../Integrations/Services/GrpcMlClient.cs | 11 -- .../Services/GrpcTelegramClient.cs | 16 --- .../Services/LocalColumnSuggester.cs | 11 -- .../Integrations/Services/LocalMlClient.cs | 3 - .../Integrations/Services/MlOutboxQueue.cs | 7 -- .../Services/TokenUsageRecorder.cs | 4 - .../Storage/Services/FileStorageRegistrar.cs | 3 - .../Storage/Services/LocalFileStorage.cs | 5 - .../Storage/Services/MinioFileStorage.cs | 4 - .../Configurations/CardConfiguration.cs | 1 - .../Persistence/Repositories/AuditLogStore.cs | 2 - .../Persistence/Repositories/InviteStore.cs | 1 - .../Repositories/KanbanStore.Cards.cs | 8 -- .../Repositories/KanbanStore.Comments.cs | 3 - .../Repositories/KanbanStore.Selected.cs | 3 - .../Repositories/KanbanStore.StorageRules.cs | 3 - .../Persistence/Repositories/KanbanStore.cs | 1 - .../Persistence/Repositories/PipelineStore.cs | 9 -- .../Persistence/Repositories/TelegramStore.cs | 4 - .../Repositories/TenantLimitStore.cs | 10 -- .../ServiceCollectionExtensions.cs | 47 -------- .../Deal.Infrastructure/Services/CardMover.cs | 2 - .../Abstractions/IContainerRules.cs | 4 +- .../Application/Abstractions/ITzCard.cs | 4 +- .../Application/Models/Card.cs | 1 - .../Registrars/DiscoveryModuleRegistrar.cs | 4 - .../Application/Services/DiscoveryBanGuard.cs | 3 - .../Services/DiscoveryCandidatesService.cs | 6 -- .../Services/DiscoveryTasksService.cs | 4 - .../DiscoveryWorkerService.Constants.cs | 1 - .../DiscoveryWorkerService.Evaluate.cs | 13 --- .../DiscoveryWorkerService.Helpers.cs | 9 -- .../Services/DiscoveryWorkerService.Join.cs | 10 -- .../Services/DiscoveryWorkerService.Search.cs | 5 - .../Services/DiscoveryWorkerService.cs | 1 - .../Application/ColumnRules/AmountParser.cs | 7 -- .../Application/ColumnRules/ColumnMatcher.cs | 2 - .../ColumnRules/MatchHitBuilder.cs | 12 --- .../Application/ColumnRules/RulesDescriber.cs | 2 - .../Application/ColumnRules/TypeAliases.cs | 2 +- .../Registrars/KanbanModuleRegistrar.cs | 3 - .../Application/Services/BudgetNormalizer.cs | 4 - .../Services/CardsService.Files.cs | 4 - .../Services/CardsService.Helpers.cs | 22 ---- .../Services/CardsService.Reminders.cs | 2 - .../Services/CardsService.Selected.cs | 3 - .../Application/Services/FileKindDetector.cs | 2 - .../Services/StorageTickService.cs | 4 - .../Application/Services/SuggestHeuristics.cs | 7 -- .../Abstractions/IPipelineStore.cs | 2 +- .../Models/GlobalExcludeSettings.cs | 2 +- .../Application/Models/MlApplyResult.cs | 2 +- .../Application/Models/MlCandidateDto.cs | 2 +- .../Models/MlCandidatePredictionDto.cs | 2 +- .../Models/PipelineRejectConstants.cs | 2 +- .../Application/Models/RejectRecord.cs | 2 +- .../Application/Models/RejectedPageDto.cs | 2 +- .../Application/Parse/ContactsQualifier.cs | 9 -- .../Application/Parse/DedupHasher.cs | 2 - .../Application/Parse/LocalFieldsParser.cs | 14 --- .../Parse/MessageListNormalizer.cs | 6 -- .../Application/Parse/MessageTextCleaner.cs | 14 --- .../Application/Parse/SummaryComposer.cs | 5 - .../Registrars/PipelineModuleRegistrar.cs | 8 -- .../Services/AiClassifyContextBuilder.cs | 7 -- .../Application/Services/AiRawCardMapper.cs | 26 ----- .../Application/Services/CardComposer.cs | 7 -- .../Application/Services/CardReclassifier.cs | 10 -- .../Services/GlobalExclusionRules.cs | 2 +- .../Application/Services/MlReviewService.cs | 2 +- .../Services/PipelineProcessingService.cs | 3 +- .../Services/PipelineWorkerService.Checks.cs | 23 ---- .../PipelineWorkerService.Decisions.cs | 4 - .../PipelineWorkerService.Learning.cs | 15 --- .../Services/PipelineWorkerService.Pump.cs | 17 --- .../PipelineWorkerService.Rejections.cs | 12 --- .../PipelineWorkerService.Settings.cs | 21 ---- .../Application/Models/IncomingRules.cs | 21 ---- .../Application/Models/MyPromptDto.cs | 2 +- .../Application/Models/PublicSettingsDto.cs | 2 +- .../Application/Models/SettingsDefaults.cs | 2 - .../Application/Models/SettingsKeys.cs | 2 +- .../Registrars/SettingsModuleRegistrar.cs | 2 - .../Application/Services/RatesService.cs | 5 - .../SettingsService.PatchMyPrompts.cs | 2 - .../SettingsService.PatchScalarKeys.cs | 5 - .../Services/SettingsService.PatchSecrets.cs | 4 - .../Services/SettingsService.PublicForms.cs | 5 - .../Services/SettingsService.ReadMerge.cs | 12 --- .../Application/Services/SettingsService.cs | 10 +- .../Application/DialogsService.cs | 11 +- .../Application/ITelegramStore.cs | 2 +- .../Models/TelegramMonitorAllDto.cs | 2 +- .../Application/TelegramModuleRegistrar.cs | 2 - .../Application/Models/AuditEvents.cs | 2 +- .../Registrars/TenantModuleRegistrar.cs | 2 - .../Application/Services/AuditService.cs | 1 - .../Application/Services/AuthService.cs | 4 - .../Application/Services/InvitesService.cs | 3 - .../Application/Services/JoinService.cs | 2 - .../Services/OperatorAuthService.cs | 2 - .../Contracts/BudgetedAiClassifierTests.cs | 2 - .../Contracts/CardsServiceTests.cs | 2 - .../Contracts/FakeTelegramStore.cs | 2 - .../Contracts/IntegrationsDiTests.cs | 3 - .../Contracts/PipelineWorkerGrpcAiTests.cs | 9 -- .../Grpc/RecordingAiService.cs | 2 - .../Grpc/RecordingMlService.cs | 2 - .../Infrastructure/TenantLimitStoreTests.cs | 3 - .../Modules/Cards/FakeKanjStore.cs | 9 -- .../Modules/Kanban/FakePipelineStore.cs | 5 - .../Modules/Kanban/SuggestHeuristicsTests.cs | 1 - .../Modules/Tenants/FakeInviteStore.cs | 1 - .../Modules/Tenants/FakeTenantLimitStore.cs | 1 - .../Modules/Tenants/JoinFlowTests.cs | 2 - .../Tenants/SuspiciousActivityServiceTests.cs | 2 +- .../Support/AdminTickOrchestratorTests.cs | 1 - .../Deal.Tests.Unit/Support/AiGrpcTestHost.cs | 6 -- .../Support/BudgetAlertSchedulerTests.cs | 3 - .../Support/CardsServiceSelectedTests.cs | 1 - .../Support/ConversionRecomputerTests.cs | 4 - .../Support/ForwardedHeadersHttpTests.cs | 2 - .../Support/GrpcAiClassifierTests.cs | 4 - .../Support/IncomingRulesTests.cs | 2 - .../Support/LocalColumnSuggesterTests.cs | 5 - .../Support/LocalFileStorageTests.cs | 1 - .../Support/LocalMlClientTests.cs | 1 - .../Support/MessageParseCoreTests.cs | 1 - .../Deal.Tests.Unit/Support/MlGrpcTestHost.cs | 5 - .../Support/OperatorAuthHttpHost.cs | 18 ---- .../OperatorHealthEndpointsHttpTests.cs | 1 - .../Support/OriginGuardHttpTests.cs | 1 - .../Support/PipelineWorkerSchedulerTests.cs | 1 - .../Support/PipelineWorkerServiceTests.cs | 6 -- .../Support/RateLimitHttpTests.cs | 1 - .../Support/RatesServiceTests.cs | 2 - .../Support/ServiceHealthProbeTests.cs | 1 - .../Support/SettingsServiceTests.cs | 1 - .../Support/StorageTickSchedulerTests.cs | 10 -- .../Support/TelegramGrpcTestHost.cs | 5 - .../Support/TelegramIngressTestHost.cs | 7 -- .../tests/Deal.Tests.Unit/Support/TestPort.cs | 6 -- .../Deal.Ml.Tests/Grpc/MlTestHost.cs | 3 - .../Deal.Ml.Tests/Ml/LearningData.cs | 2 - .../Ml/TenantModelLearningTests.cs | 1 - src/ml-service/Deal.Ml/MlServiceHost.cs | 3 - src/ml-service/Deal.Ml/MlServiceImpl.cs | 3 - .../Deal.Ml/Model/OnlineNaiveBayes.cs | 16 --- src/ml-service/Deal.Ml/Model/TenantModel.cs | 11 -- src/ml-service/Deal.Ml/Program.cs | 10 -- src/ml-service/Deal.Ml/Storage/MlDb.cs | 8 -- .../Grpc/FakeIngressServer.cs | 3 - .../Deal.Telegram.Tests/Grpc/TestDoubles.cs | 4 - .../Telegram/FakeSessionClient.cs | 2 - .../Telegram/SessionHarness.cs | 2 - .../Deal.Telegram/Core/CoreIngressClient.cs | 3 - .../Deal.Telegram/Dialogs/BackfillService.cs | 3 - .../Deal.Telegram/Dialogs/DialogCatalog.cs | 2 - .../Deal.Telegram/Dialogs/RealtimeSweep.cs | 4 - .../Deal.Telegram/Discovery/DiscoveryOps.cs | 2 - src/telegram-service/Deal.Telegram/Program.cs | 9 -- .../Deal.Telegram/Sessions/TenantSession.cs | 6 -- .../Deal.Telegram/Telegram/TlMessageMapper.cs | 3 - .../Telegram/WTelegramSessionClient.cs | 36 ------- .../Deal.Telegram/TelegramServiceHost.cs | 7 -- .../Deal.Telegram/TelegramServiceImpl.cs | 11 -- 220 files changed, 32 insertions(+), 1259 deletions(-) diff --git a/scripts/__pycache__/_slim_comments.cpython-312.pyc b/scripts/__pycache__/_slim_comments.cpython-312.pyc index f0ce1595c0b47731df239ddfc1f8ce4b21e70643..843531d144012dadda2193a3a98fe07c77450ce1 100644 GIT binary patch delta 2168 zcmb7EZA@EL7(S=HZEruIz5Sqt7AaC#%NWRJH#aAUqaY}g;oO!6hJ`i`K?e60W#l5T z%*nI~bjJufmu+hHk@h`i!_nqkz49Wa7KHq4P~}%gn{B#~ zI-}EoiOgGJX2h(Z?g;W{2eSb6!E8W7P!Grj4S>dwDaduZ z5#m`H3K=h^5qy(e9hZb(<9IcG(MGd3pO0D${N zP+yg1qz}O$U6*E@(nqmT=|=3Rbcxz4q}Qb>ask)dhRpljtwO2CT~Xn#F0Al)+~uVn z*T7A(f`9Q*FY)7ZU~dhscUNy~Er++KRFpoD=A_F&e?_{!0R%`u4D@wvcq-5*)-*Sy z-Ys=IQgNl^JJ}oRfetvwexM*fo2(^%3P0-vgL5lB#0H=`NU-h{j zJDlZhf(BS28o(l)V2xuR@s!{ynnGd(rjLHipO$N{tq56@aTZ2=yq<}bZ+U$ ztQ=9B$-_!8XUD1hu#Ifuyf}BZi&J7;Guvkz#6)3!8fO#HT$Qh>_0`n2H`lb(G$oaR zzP?azFv+HLExz4k-W<`?!GWsb5xQshTFzpff5}oDvlO2WELz+|W4%lJ(!Qzv(fy}u z7A+-YoArefukw>DW5d17r)p~>QvR*@Ebz$TxpkBwKUvqSWPC{&Doy6OJo255BlWf) zR1u zxEb&n2IXqxuriz(K{4}V3CdDmdQsSNdjk|?jv%^A`nylZ3!I3Fp}=eqnY0R{Nm)ck z%JS~3mqkiM{3vXQQ=2}6Jc^w>R0Q!33TlAAHGu&Ij3c^5JNpp)T&9qS+H%PWu59FG zJcVIu?F>4E-j+XuJ$R=_#?un;l>o3VgI5xF2y_FdlebY*Ilz#!k3ZNM5`^}iuHKMP zzP;yQXP`%@D&PJ@s5dm&$5(~HOUMKG5&bcWq_U>@ft!VI?RmHDLfc&B(uVuy2mTxp zXmW=O3b9Aa7s8TXPr*sS3y@TX2E#C-JWV~xJlwF;$KOefw1i2fr%MPY<$>Nq9u>cZ zs(loSDO6DhMF%9+%{S1ss;f5`>J1Bm3MA+1#S^vNtZ4>C2RuTOtg{~<(ds9*9ose; zny!f+SkxB0S$nSWOyl{1nOzqKT@49y&bTjOG))GM z7r$0}s_~V^(*x&sof%wk-1Wik%l`NLHx)Mn^K9JPu;hIt=6ytJ-WB(@EP40Ey!#fs z{)E9YrHksOA3I(7%Dx3MVz-R^Pm)Ei|2HMF+OPQH_6M%`X8Lbu(SM?8$x#t=RLtN- z$HqBd%>F<;XUBN`*LvgRmKVk8{x9{;w@fkGnRr>#f~P5AFimNqnjO>L1w-NYI&*^6 zPH2v4CcV>K^qvLQkzm#1yN)Tx@$x#P)r|)t#mjD_(~ma(xuQpo9avC89Y5W&{=q7C zw#D%UgPV!7uu>TUEx(2AEo@Qr!*Y1`ox;mFWKOEu+k*!?+uQlase_rqUJA4ud?!Fs z)p_u#zOJ6o>RdcTRq_^J-RgvL)a`Bxn6mxSRZ5RD4H5#f|dM4~-2;Mo(N-I?607Rf;;iEJv7GHWRnYvo(=r9dDWNAM;DL v;ds`wqnd=)GQN>ciIh_v*WN$Mt|(2oV5LlpEh`Qu*00oI8{lSgz?J_O())$Y delta 1233 zcmZ9KTWlLe6ozMZ?e*HbYiE6l?bIfAn;36Wa3jS^Q(B^mHmxI6r3s)CLZT$=3YvwkNC7J%D7+aDx8bZS-)br)#iCk-+)#%jYJKF+Q|G7rF*SsV5~ zZHlwYxFa{XJ7tm0SHKu2TQGTzK0Uc#|n$p`e!%0+#n@}_>Ha;b7jFGU^&Q zj)CX>xZ?eKOmn5z)mYQIO8;+7J33|G0~^%^x618S_n=&J9D31)Jqp{(f7%%ckCgv* zq@aJ%ga19DU&l^;q2JJN1sX=&B(m~0O1jQN9{uWi+{8@H&&OYu(ec(C-WmXp;Yd!E z@3ejdg^`sr+8OQt|REG=K%WF^HwOpd?napA;Z^ZcdVP4d2sW1830BcG>$*6 zP$=c?rlHb>hT9_Ow)Y+sP=N7!X%(oH7pFAsOB+?Aa#l5>pF5nWz**2|K0ke8x6k!6 z3wE5|XbD$S{r6RwrojrMQZltHU~{S5Un;!jJHQlRu7#-n3lx@JDjWrp@svWYPVr$Z zJ14^9$u21ziG&A&5lIR^+9h=;W;`WvN|2;=69fo)F=|XawSfInh*ye}U@rmLnc^Vm z!H`T!l;|f2Itf}2W7dq*=jLW+l@6i}({u6?8f_0+Vz@hjBe@T~)_&~*Z_S^~p446Y zKZ%!)e)_@>d|w7`b9_Nuacl0Ph}wP>gB3BjCGM*_oGX1=U$F-r${f3EbLG?7bn&Sz z+umx6SU9cW(^p$ORo<2#$_^D4i>I~J79XhcLP5+9W{z(2&MM30Em=z;S`@W|Tdc3j znlor1$T&Lz$7iN;o%ImlZ5M}>HoU#^+2HpE7(>0G9)@gJiK5xih%td>s{Cc>bNGx{ zGfhs)b2F2Z%CjV)Vb3s8&k>Ac)J!vTFU`MrF5d99MvojHylzx z=%;Xp`xPw7OH00sQKun9XxlH}fD9I$<7KL=eqz5ulC N3_>?%FWQj&{{pxINK^m- diff --git a/src/ai-service/Deal.Ai.Tests/Ai/AiTestHost.cs b/src/ai-service/Deal.Ai.Tests/Ai/AiTestHost.cs index 0ee5f0b..4210a3e 100644 --- a/src/ai-service/Deal.Ai.Tests/Ai/AiTestHost.cs +++ b/src/ai-service/Deal.Ai.Tests/Ai/AiTestHost.cs @@ -12,10 +12,6 @@ using Microsoft.Extensions.DependencyInjection; namespace Deal.Ai.Tests.Ai; -// в процессе теста на эфемерном порту и через configureServices-хук подменяет LLM-фасад фейком -// (FakeProviderClient, без сети) и функцию паузы ретраев (мгновенная) — сценарии не -// ждут 0.8/2 с между попытками. Регистрация, добавленная харнессом после дефолтных, побеждает -// (DI резолвит последнюю). Исходное значение env DEAL_SERVICE_TOKEN восстанавливается. internal static class AiTestHost { /// @@ -101,7 +97,6 @@ internal static class AiTestHost /// /// Подменяет HTTP-фасад вызовов модели фейком сценария /// - /// Коллекция сервисов хоста. /// Фейк-провайдер сценария. public static void UseFakeProvider(IServiceCollection services, FakeProviderClient fake) => services.AddSingleton(fake); @@ -109,7 +104,6 @@ internal static class AiTestHost /// /// Делает паузы ретраев мгновенными /// - /// Коллекция сервисов хоста. public static void DisableRetryDelays(IServiceCollection services) => services.AddSingleton>(static (_, _) => Task.CompletedTask); diff --git a/src/ai-service/Deal.Ai.Tests/Ai/FakeProviderClient.cs b/src/ai-service/Deal.Ai.Tests/Ai/FakeProviderClient.cs index 5e2397f..4434d17 100644 --- a/src/ai-service/Deal.Ai.Tests/Ai/FakeProviderClient.cs +++ b/src/ai-service/Deal.Ai.Tests/Ai/FakeProviderClient.cs @@ -8,8 +8,6 @@ namespace Deal.Ai.Tests.Ai; // Config: Конфиг провайдера вызова. internal sealed record FakeProviderCall(string SystemPrompt, string UserText, LlmConfig Config); -// usage либо сбой), каждый вызов записывается в Calls — тесты проверяют и собранные -// RPC-слоем промпты, и число попыток ретраев. internal sealed class FakeProviderClient : IProviderClient { private readonly Func> _handler; diff --git a/src/ai-service/Deal.Ai.Tests/Grpc/AiServiceHostTests.cs b/src/ai-service/Deal.Ai.Tests/Grpc/AiServiceHostTests.cs index b7ace63..3b87edd 100644 --- a/src/ai-service/Deal.Ai.Tests/Grpc/AiServiceHostTests.cs +++ b/src/ai-service/Deal.Ai.Tests/Grpc/AiServiceHostTests.cs @@ -119,11 +119,6 @@ public sealed class AiServiceHostTests channel => AssertRejectedAsync(channel, tokenHeader, expected)); } - // Вызывает Classify и проверяет, что сервер ответил ожидаемым кодом статуса. - // Запрос несёт полный metadata (service-token + tenant-id), чтобы верный токен доходил - // channel: Канал к хосту ai-service. - // tokenHeader: Значение metadata «service-token» либо null (нет заголовка). - // expected: Ожидаемый StatusCode. private static async Task AssertRejectedAsync( GrpcChannel channel, string? tokenHeader, diff --git a/src/ai-service/Deal.Ai.Tests/Support/StubHttpMessageHandler.cs b/src/ai-service/Deal.Ai.Tests/Support/StubHttpMessageHandler.cs index 75cc353..db55682 100644 --- a/src/ai-service/Deal.Ai.Tests/Support/StubHttpMessageHandler.cs +++ b/src/ai-service/Deal.Ai.Tests/Support/StubHttpMessageHandler.cs @@ -13,7 +13,6 @@ internal sealed record CapturedHttpRequest( IReadOnlyDictionary Headers, string? Body); -// запросы (URL/заголовки/тело) и отвечает по сценарию; опциональная задержка — для теста таймаута. internal sealed class StubHttpMessageHandler : HttpMessageHandler { private readonly Func _responder; diff --git a/src/ai-service/Deal.Ai/AiServiceHost.cs b/src/ai-service/Deal.Ai/AiServiceHost.cs index a4b30bb..2ae225d 100644 --- a/src/ai-service/Deal.Ai/AiServiceHost.cs +++ b/src/ai-service/Deal.Ai/AiServiceHost.cs @@ -26,15 +26,11 @@ public static class AiServiceHost { WebApplicationBuilder builder = WebApplication.CreateBuilder(args ?? []); - // Общая серверная обвязка (Deal.Grpc.Hosting, C31): mTLS env DEAL_MTLS_* — загрузка - // сертификатов сразу с fail-fast (compose-prod монтирует deploy/certs, scripts/mtls-certs.sh); MtlsCertificates? mtlsCertificates = GrpcServer.LoadMtlsCertificates(builder); GrpcServer.ConfigureKestrelHttp2Endpoint(builder, grpcPort, mtlsCertificates); builder.Services.AddDealGrpcServer(); builder.Services.AddReadyHealthCheck("хост ai-service готов"); - // попытки управляется внутри — 90 с OpenAI / 60 с Anthropic; клиент без общего таймаута) и - // оркестратор ретраев/JSON/usage поверх него. Ключи API — в конфиге запроса, не в DI/логах. builder.Services.AddHttpClient(static httpClient => httpClient.Timeout = Timeout.InfiniteTimeSpan); builder.Services.AddSingleton( diff --git a/src/ai-service/Deal.Ai/AiServiceImpl.cs b/src/ai-service/Deal.Ai/AiServiceImpl.cs index e6a130c..b9d94f0 100644 --- a/src/ai-service/Deal.Ai/AiServiceImpl.cs +++ b/src/ai-service/Deal.Ai/AiServiceImpl.cs @@ -275,7 +275,6 @@ public sealed class AiServiceImpl : AiService.AiServiceBase } } - // context: Контекст вызова. private static string RequireTenantId(ServerCallContext context) { string? tenantId = context.RequestHeaders.GetValue(TenantIdMetadataKey); @@ -349,10 +348,6 @@ public sealed class AiServiceImpl : AiService.AiServiceBase providerConfig.ApiStyle); } - // method: Имя RPC для аудита. - // tenantId: Id тенанта. - // config: Конфиг провайдера вызова. - // callError: Итоговая ошибка фасада. private void LogAiUnavailable( string method, string tenantId, @@ -365,14 +360,9 @@ public sealed class AiServiceImpl : AiService.AiServiceBase config.DisplayName, callError.Kind); - // callError: Итоговая ошибка фасада. private static RpcException ToUnavailable(LlmCallException callError) => new(new Status(StatusCode.Unavailable, callError.Message)); - // Читает булево поле JSON-ответа модели: bool как есть; строка — ложь только для значений из - // json: Корневой объект ответа модели. - // fieldName: Имя поля. - // defaultValue: Значение при отсутствии/неразбираемости поля. private static bool ReadBoolField( JsonObject json, string fieldName, @@ -416,9 +406,6 @@ public sealed class AiServiceImpl : AiService.AiServiceBase return trimmed.Length == 0 ? null : trimmed; } - // Причина решения fit: поле reason модели, при отсутствии — «подходит»/«не подходит» - // json: Корневой объект ответа модели. - // fit: Решение модели. private static string ReadFitReason(JsonObject json, bool fit) { string reason = ReadStringField(json, ReasonFieldName) ?? (fit ? FitReasonDefault : NotFitReasonDefault); @@ -447,7 +434,6 @@ public sealed class AiServiceImpl : AiService.AiServiceBase } } - // keywords: Ключи задачи. private static string JoinKeywords(IEnumerable keywords) => string.Join( ", ", diff --git a/src/ai-service/Deal.Ai/Llm/JsonExtractor.cs b/src/ai-service/Deal.Ai/Llm/JsonExtractor.cs index c3eeda7..16ad6ff 100644 --- a/src/ai-service/Deal.Ai/Llm/JsonExtractor.cs +++ b/src/ai-service/Deal.Ai/Llm/JsonExtractor.cs @@ -46,8 +46,6 @@ public static class JsonExtractor } } - // между открывающей и закрывающей обёртками; обёртки нет/незакрыта — null. - // raw: Текст ответа (уже обрезанный). private static string? UnwrapFence(string raw) { int open = raw.IndexOf(CodeFence, StringComparison.Ordinal); diff --git a/src/ai-service/Deal.Ai/Llm/LlmHttpClient.cs b/src/ai-service/Deal.Ai/Llm/LlmHttpClient.cs index 636a3f6..ea88e16 100644 --- a/src/ai-service/Deal.Ai/Llm/LlmHttpClient.cs +++ b/src/ai-service/Deal.Ai/Llm/LlmHttpClient.cs @@ -97,10 +97,6 @@ public sealed class LlmHttpClient : IProviderClient } } - // Собирает запрос OpenAI-совместимого чата: {base}/chat/completions, Bearer при заданном ключе, - // config: Конфиг провайдера. - // systemPrompt: Системный промпт. - // userText: Пользовательское сообщение. private static HttpRequestMessage BuildOpenAiRequest( LlmConfig config, string systemPrompt, @@ -126,10 +122,6 @@ public sealed class LlmHttpClient : IProviderClient return request; } - // Собирает запрос Anthropic Messages API: {base}/v1/messages, x-api-key + anthropic-version, - // config: Конфиг провайдера. - // systemPrompt: Системный промпт. - // userText: Пользовательское сообщение. private static HttpRequestMessage BuildAnthropicRequest( LlmConfig config, string systemPrompt, @@ -171,7 +163,6 @@ public sealed class LlmHttpClient : IProviderClient return isAnthropic ? ReadAnthropicBody(body) : ReadOpenAiBody(body); } - // body: Тело ответа. private static ProviderChatResult ReadOpenAiBody(string body) { JsonObject? payload = ParseObjectOrThrow(body); @@ -193,7 +184,6 @@ public sealed class LlmHttpClient : IProviderClient return new ProviderChatResult(content ?? string.Empty, ReadOpenAiUsage(payload["usage"])); } - // body: Тело ответа. private static ProviderChatResult ReadAnthropicBody(string body) { JsonObject? payload = ParseObjectOrThrow(body); @@ -265,7 +255,6 @@ public sealed class LlmHttpClient : IProviderClient throw UnexpectedApiResponse("тело не является JSON-объектом"); } - // failureKind: Короткая причина (без тела ответа и секретов). private static LlmHttpException UnexpectedApiResponse(string failureKind) => new($"Неожиданный ответ ИИ-провайдера: {failureKind}"); @@ -310,6 +299,5 @@ public sealed class LlmHttpClient : IProviderClient return result; } - // baseUrl: Базовый URL из конфига. private static string NormalizeBaseUrl(string baseUrl) => baseUrl.TrimEnd('/'); } diff --git a/src/ai-service/Deal.Ai/Program.cs b/src/ai-service/Deal.Ai/Program.cs index a72e627..7c2ffda 100644 --- a/src/ai-service/Deal.Ai/Program.cs +++ b/src/ai-service/Deal.Ai/Program.cs @@ -1,11 +1,4 @@ // -// Kestrel HTTP/2 на порту 5102 (env GRPC_PORT, затем PORT) + AddGrpc с интерцепторами service-token -// и access-лога + стандартный gRPC-health (grpc.health.v1.Health). Транспорт: dev — plaintext -// Production без mTLS не стартует (GrpcHostEnvironment.RequireMtlsInProduction). -// Серверная обвязка (Kestrel/AddGrpc/health) — общий Deal.Grpc.Hosting (C31): хост-фабрика -// AiServiceHost.Create используется и интеграционными тестами (Deal.Ai.Tests), которые поднимают -// его в своём процессе на эфемерном порту. Методы AiService (Filter/Classify/GenerateKeywords/ -// задачи 7–8): фасад живёт в Deal.Ai/Llm. using Deal.Ai; using Deal.Grpc.Hosting.Interceptors; @@ -13,16 +6,12 @@ using Deal.Grpc.Hosting.Models; using Deal.Grpc.Hosting.Options; using Deal.Grpc.Hosting.Services; -// или PORT (общий конвенциональный env хостинг-платформ) — см. GrpcHostEnvironment.ResolveGrpcPort. const int defaultGrpcPort = 5102; const string aiProcessName = "ai"; int grpcPort = GrpcHostEnvironment.ResolveGrpcPort(defaultGrpcPort); int metricsPort = DealMetricsHosting.ResolveMetricsPort(DealMetricsHosting.DefaultMetricsPort); -// конфигурируется production-точкой входа через configureBuilder-хук хоста (тесты хост поднимают -// без Serilog, DealLogging.Configure в AiServiceHost/Create вызывается только здесь). Метрики -// (OTel → Prometheus, /metrics) — тем же хуком до builder.Build(). WebApplication app = AiServiceHost.Create( grpcPort, configureBuilder: builder => diff --git a/src/contracts/telegram.proto b/src/contracts/telegram.proto index 83b3e95..4d8cea7 100644 --- a/src/contracts/telegram.proto +++ b/src/contracts/telegram.proto @@ -41,7 +41,6 @@ service TelegramService { // само. Нет сессии — FAILED_PRECONDITION «Telegram не подключён». rpc GetStatus(GetStatusRequest) returns (GetStatusReply); - // api_id/api_hash — глобальные ключи приложения Telegram, задаёт оператор (ТЗ §4.1/§8.1), // не заданы оператором» до вызова. Ответ: новая фаза ("code"). rpc StartPhone(StartPhoneRequest) returns (StartPhoneReply); @@ -73,7 +72,6 @@ service TelegramService { rpc Backfill(BackfillRequest) returns (BackfillReply); // свежие из Telegram; признак lead и фолбэк на БД добавляет ядро - // (у сервиса нет БД тенанта). limit 1..50 (api-map /dialogs/preview). rpc ReadRecent(ReadRecentRequest) returns (ReadRecentReply); // Пауза анти-бана после поиска — внутри сервиса. Личные чаты/боты ядро @@ -208,7 +206,6 @@ message SetMonitorAllRequest { message SetMonitorAllReply { bool ok = 1; - // Сколько диалогов в каталоге тенанта (api-map /monitor-all → count). int32 count = 2; bool enabled = 3; } @@ -228,7 +225,6 @@ message BackfillReply { message ReadRecentRequest { // Id диалога. string dialog_id = 1; - // Сколько последних сообщений (1..50; api-map /dialogs/preview limit 1..50). int32 limit = 2; } diff --git a/src/core/Deal.Api/Endpoints/AiCheckEndpoint.cs b/src/core/Deal.Api/Endpoints/AiCheckEndpoint.cs index 16bf566..cd1f27e 100644 --- a/src/core/Deal.Api/Endpoints/AiCheckEndpoint.cs +++ b/src/core/Deal.Api/Endpoints/AiCheckEndpoint.cs @@ -48,11 +48,6 @@ public static class AiCheckEndpoint return Results.Ok(result); } - // store: KV-хранилище настроек тенанта. - // secretCipher: Шифр секретов (расшифровка apiKey). - // ct: Токен отмены. - // Возвращает: Запрос проверки: id провайдера + эффективные base/model + расшифрованный ключ. - // Эффективные значения = дефолты SettingsDefaults, перекрытые сохранёнными private static async Task BuildActiveCheckRequestAsync( ISettingsStore store, ISecretCipher secretCipher, diff --git a/src/core/Deal.Api/Endpoints/AiSuggestEndpoints.cs b/src/core/Deal.Api/Endpoints/AiSuggestEndpoints.cs index 1577fae..197edc7 100644 --- a/src/core/Deal.Api/Endpoints/AiSuggestEndpoints.cs +++ b/src/core/Deal.Api/Endpoints/AiSuggestEndpoints.cs @@ -38,8 +38,6 @@ public static class AiSuggestEndpoints return app; } - // POST /api/ai/suggest-columns: анализ «Неразобранного» и создание колонок-предложений. - // SSE-toast), {ok:false, reason} (+ cooldown) — мягкая причина (HTTP 200). Кулдаун/«мало карточек»/ private static async Task SuggestColumnsAsync(HttpContext context, CancellationToken ct) { if (!context.HasUser()) @@ -61,7 +59,6 @@ public static class AiSuggestEndpoints return Results.Ok(result); } - // POST /api/ai/suggest-keywords: слова-маркеры сферы по карточкам (настройки «Сфера и ключи»). private static async Task SuggestKeywordsAsync(HttpContext context, CancellationToken ct) { if (!context.HasUser()) diff --git a/src/core/Deal.Api/Endpoints/AuthEndpoints.cs b/src/core/Deal.Api/Endpoints/AuthEndpoints.cs index 8cf15f8..26e47b3 100644 --- a/src/core/Deal.Api/Endpoints/AuthEndpoints.cs +++ b/src/core/Deal.Api/Endpoints/AuthEndpoints.cs @@ -31,7 +31,6 @@ public static class AuthEndpoints { var group = app.MapGroup(AuthGroupPrefix).WithTags(AuthOpenApiTag); - // ручки входа; остальные ручки группы — под глобальной API-политикой (по тенанту/IP). group.MapPost("/login", LoginAsync).RequireRateLimiting(RateLimitPolicies.AuthPolicy); group.MapPost("/logout", LogoutAsync); group.MapGet("/me", MeAsync); @@ -51,7 +50,6 @@ public static class AuthEndpoints { string? attemptedLogin = NormalizeLogin(body.Login); - // 429 «Слишком много попыток входа…» (в dev при RateLimit:Enabled=false гвард выключен). if (await loginAttemptGuard.IsBlockedAsync(ClientIp(context), attemptedLogin, ct)) { return EndpointResults.TooManyRequests(LoginAttemptGuard.BlockedDetail); @@ -74,7 +72,6 @@ public static class AuthEndpoints if (result.Login is null || result.Token is null) { - // счётчик неудач гварда растёт там же — пустые логины ключа не имеют (блокирует только auth-политика). if (attemptedLogin is not null) { await loginAttemptGuard.RecordFailureAsync(ClientIp(context), attemptedLogin, ct); @@ -104,7 +101,6 @@ public static class AuthEndpoints return Results.Ok(new { ok = true, login = result.Login }); } - // POST /api/auth/logout: удаление сессии по токену из куки и очистка куки (всегда ok). private static async Task LogoutAsync( AuthService authService, AuditService auditService, diff --git a/src/core/Deal.Api/Endpoints/CardDetailsEndpoints.cs b/src/core/Deal.Api/Endpoints/CardDetailsEndpoints.cs index 6420992..2adcd02 100644 --- a/src/core/Deal.Api/Endpoints/CardDetailsEndpoints.cs +++ b/src/core/Deal.Api/Endpoints/CardDetailsEndpoints.cs @@ -425,8 +425,6 @@ public static class CardDetailsEndpoints // Возвращает: Имя, безопасное для заголовка. private static string ToDownloadFileName(string name) => name.Replace(FileNameQuoteCharacter, string.Empty); - // body: Тело запроса. - // Возвращает: DTO модуля для CardsService.CreateLocalCardAsync. private static CardLocalCreateDto ToCreateLocalDto(CreateCardRequest body) { return new CardLocalCreateDto( diff --git a/src/core/Deal.Api/Endpoints/CardsEndpoints.cs b/src/core/Deal.Api/Endpoints/CardsEndpoints.cs index 1f20ad2..f7f18c9 100644 --- a/src/core/Deal.Api/Endpoints/CardsEndpoints.cs +++ b/src/core/Deal.Api/Endpoints/CardsEndpoints.cs @@ -158,7 +158,6 @@ public static class CardsEndpoints if (body.Col is null) { - // Пустая/отсутствующая col попала бы в mark_seen как «не задана» и сняла бы «новое» со ВСЕХ return EndpointResults.BadRequest(UnknownColumnDetail); } @@ -167,10 +166,6 @@ public static class CardsEndpoints return Results.Ok(new { ok = true }); } - // Маршрутизацию цели (стадия «Выбранных» vs дашборд-контейнер) и побочные эффекты выполняет единый - // доменный механизм перехода ICardMover: стадия — запись истории и сброс напоминания - // (move_stage), дашборд-контейнер — журнал/обучение ML. Ответ — обновлённая карточка; 400 при - // несуществующем контейнере, 404 — карточки нет. private static async Task MoveAsync( string cardId, MoveBody body, @@ -380,11 +375,6 @@ public static class CardsEndpoints reason = result.Reason, }; - // Публикуется только когда проход реально выполнен и что-то изменил (started и - // reclassified > 0): пустой inbox/всё пропущено не меняют доску — событие не шлём. Нагрузка - // минимальная: сколько обработано и перемещено (фронт перечитывает доску). Без подписчиков — no-op. - // context: Контекст запроса (тенант-канал сессии). - // result: Итог прохода. private static void PublishReclassified(HttpContext context, ReclassifyResultDto result) { if (!result.Started || result.Reclassified == 0) diff --git a/src/core/Deal.Api/Endpoints/DiscoveryEndpoints.cs b/src/core/Deal.Api/Endpoints/DiscoveryEndpoints.cs index 51e4414..b6c9e02 100644 --- a/src/core/Deal.Api/Endpoints/DiscoveryEndpoints.cs +++ b/src/core/Deal.Api/Endpoints/DiscoveryEndpoints.cs @@ -210,9 +210,6 @@ public static class DiscoveryEndpoints return task is null ? EndpointResults.NotFound(TaskNotFoundDetail) : Results.Ok(task); } - // POST /api/discovery/tasks/{task_id}/generate-keywords: ИИ-ключи по описанию задачи - // символов, дедуп casefold); описание режет до 4000 сам адаптер (GrpcAiTools.MaxDescriptionCodePoints). - // Локальный режим (LocalAiTools, UseLocal=true) — NotSupportedException → та же мягкая ветка с текстом причины. private static async Task GenerateKeywordsAsync( string task_id, HttpContext context, @@ -259,7 +256,6 @@ public static class DiscoveryEndpoints } } - // GET /api/discovery/tasks/{task_id}/candidates?status=: кандидаты задачи с фильтром private static async Task ListCandidatesAsync( string task_id, string? status, @@ -283,8 +279,6 @@ public static class DiscoveryEndpoints return Results.Ok(new { items }); } - // RPC Join → строка каталога Dialogs (монитор on) + зеркало → фоновый первый разбор → снятие чёрного списка → - // mark_joined(auto:false). Ошибка Telegram → 400 с текстом причины. private static async Task JoinCandidateAsync( string dialog_id, HttpContext context, @@ -319,11 +313,9 @@ public static class DiscoveryEndpoints return EndpointResults.BadRequest(string.Format(JoinFailedFormat, username, reason)); } - // Источник в каталоге (монитор on, backfilled=false) + монитор-зеркало telegram-service DialogsService dialogs = context.RequestServices.GetRequiredService(); await dialogs.AddDiscoveredMonitoredAsync(dialog_id, row.Name, username, row.Kind, row.Hue, ct); - // Догон последних сообщений — в фоне: join из UI не должен висеть на паузах backfill context.RequestServices.GetRequiredService().ScheduleFirstBackfill(dialog_id); DiscoveryBlacklistService blacklist = context.RequestServices.GetRequiredService(); @@ -340,7 +332,6 @@ public static class DiscoveryEndpoints } } - // POST /api/discovery/candidates/{dialog_id}/reject: отклонить кандидата — в чёрный список private static async Task RejectCandidateAsync( string dialog_id, HttpContext context, @@ -462,9 +453,6 @@ public static class DiscoveryEndpoints private const string ServiceUnavailableText = "ИИ недоступен — повторите попытку через несколько секунд"; - // Читает настройку aiEnabled (KV; отсутствие строки — дефолт SettingsDefaults). - // settings: KV-хранилище настроек тенанта. - // ct: Токен отмены. private static async Task ReadAiEnabledAsync(ISettingsStore settings, CancellationToken ct) { SettingValue? row = await settings.GetAsync(SettingsKeys.AiEnabled, ct); @@ -505,8 +493,6 @@ public static class DiscoveryEndpoints }; } - // body: Тело запроса (wire-поля camelCase). - // Возвращает: Патч задачи (DiscoveryTaskPatch). private static DiscoveryTaskPatch ToPatch(DiscoveryTaskPatchBody body) { return new DiscoveryTaskPatch diff --git a/src/core/Deal.Api/Endpoints/MlCandidatesRequest.cs b/src/core/Deal.Api/Endpoints/MlCandidatesRequest.cs index 2fd69e8..0478842 100644 --- a/src/core/Deal.Api/Endpoints/MlCandidatesRequest.cs +++ b/src/core/Deal.Api/Endpoints/MlCandidatesRequest.cs @@ -3,6 +3,6 @@ namespace Deal.Api.Endpoints; /// /// Тело POST /api/ml/candidates. /// -/// Id диалога/канала Telegram; пусто — выборка по всем источникам тенанта (§8). +/// Id диалога/канала Telegram; пусто — выборка по всем источникам тенанта. /// Сколько последних сообщений вернуть (кламп 1..60, дефолт 10). public sealed record MlCandidatesRequest(string DialogId, int Limit); diff --git a/src/core/Deal.Api/Endpoints/OperatorAnalyticsEndpoints.cs b/src/core/Deal.Api/Endpoints/OperatorAnalyticsEndpoints.cs index e919b8b..85d7f84 100644 --- a/src/core/Deal.Api/Endpoints/OperatorAnalyticsEndpoints.cs +++ b/src/core/Deal.Api/Endpoints/OperatorAnalyticsEndpoints.cs @@ -36,13 +36,6 @@ public static class OperatorAnalyticsEndpoints return app; } - // GET /api/operator/analytics/suspicious?from=&to=: находки детектора подозрительной активности (§10.5). - // from: Начало окна анализа (включительно; ISO-8601); null — последние 24 часа. - // to: Конец окна анализа (включительно; ISO-8601); null — «сейчас». - // context: Контекст запроса. - // suspiciousService: Детектор подозрительной активности (scoped). - // ct: Токен отмены. - // Возвращает: 200 сводка находок или 401 без операторской сессии. private static async Task SuspiciousAsync( DateTimeOffset? from, DateTimeOffset? to, diff --git a/src/core/Deal.Api/Endpoints/OperatorAuditEndpoints.cs b/src/core/Deal.Api/Endpoints/OperatorAuditEndpoints.cs index 4d567b1..9d55690 100644 --- a/src/core/Deal.Api/Endpoints/OperatorAuditEndpoints.cs +++ b/src/core/Deal.Api/Endpoints/OperatorAuditEndpoints.cs @@ -28,18 +28,6 @@ public static class OperatorAuditEndpoints return app; } - // GET /api/operator/audit?eventType=&actorType=&actorId=&tenantId=&from=&to=&limit=&offset=: лента аудита. - // eventType: Фильтр по типу события (равенство; пусто — без фильтра). - // actorType: Фильтр по типу актора operator|tenant|system (равенство). - // actorId: Фильтр по идентификатору актора (равенство). - // tenantId: Фильтр по тенанту (равенство). - // from: Нижняя граница At (включительно; ISO-8601). - // to: Верхняя граница At (включительно; ISO-8601). - // limit: Размер выборки (дефолт 100, клампится 1..500). - // context: Контекст запроса. - // auditService: Сервис аудита (scoped). - // ct: Токен отмены. - // Возвращает: 200 {items:[...], total} или 401 без операторской сессии. private static async Task ListAsync( string? eventType, string? actorType, diff --git a/src/core/Deal.Api/Endpoints/OperatorAuthEndpoints.cs b/src/core/Deal.Api/Endpoints/OperatorAuthEndpoints.cs index bf297e5..ba3095f 100644 --- a/src/core/Deal.Api/Endpoints/OperatorAuthEndpoints.cs +++ b/src/core/Deal.Api/Endpoints/OperatorAuthEndpoints.cs @@ -29,7 +29,6 @@ public static class OperatorAuthEndpoints { var group = app.MapGroup(OperatorAuthGroupPrefix).WithTags(OperatorAuthOpenApiTag); - // входа оператора; остальные ручки группы — под глобальной API-политикой (по тенанту/IP). group.MapPost("/login", LoginAsync).RequireRateLimiting(RateLimitPolicies.AuthPolicy); group.MapPost("/logout", LogoutAsync); group.MapGet("/me", MeAsync); @@ -48,7 +47,6 @@ public static class OperatorAuthEndpoints { string? attemptedLogin = NormalizeLogin(body.Login); - // ip|login до проверки учётных данных (в dev при RateLimit:Enabled=false гвард выключен). if (await loginAttemptGuard.IsBlockedAsync(ClientIp(context), attemptedLogin, ct)) { return EndpointResults.TooManyRequests(LoginAttemptGuard.BlockedDetail); @@ -57,8 +55,6 @@ public static class OperatorAuthEndpoints var result = await operatorAuthService.LoginAsync(body.Login, body.Password, ct); if (result.Login is null || result.Token is null) { - // Неверные учётные данные оператора — одно сообщение (зеркало AuthEndpoints). - // счётчик неудач гварда растёт там же (пустые логины ключа не имеют). if (attemptedLogin is not null) { await loginAttemptGuard.RecordFailureAsync(ClientIp(context), attemptedLogin, ct); diff --git a/src/core/Deal.Api/Endpoints/OperatorTenantsEndpoints.cs b/src/core/Deal.Api/Endpoints/OperatorTenantsEndpoints.cs index 8b77967..215cea4 100644 --- a/src/core/Deal.Api/Endpoints/OperatorTenantsEndpoints.cs +++ b/src/core/Deal.Api/Endpoints/OperatorTenantsEndpoints.cs @@ -81,7 +81,6 @@ public static class OperatorTenantsEndpoints return Results.Ok(new { items }); } - // POST /api/operator/tenants: создание тенанта (Status active + провижининг схемы); аудит tenant_created. private static async Task CreateAsync( OperatorTenantCreateRequest body, HttpContext context, diff --git a/src/core/Deal.Api/Endpoints/PipelineEndpoints.cs b/src/core/Deal.Api/Endpoints/PipelineEndpoints.cs index 0db71cc..916eeec 100644 --- a/src/core/Deal.Api/Endpoints/PipelineEndpoints.cs +++ b/src/core/Deal.Api/Endpoints/PipelineEndpoints.cs @@ -84,7 +84,6 @@ public static class PipelineEndpoints return Results.Ok(new { items, counts, rejected }); } - // первыми; offset ≥ 0, limit 1..500 (clamp в сервисе), значения эхом в ответе {items,total,offset,limit}. private static async Task RejectedAsync( string? q, int? offset, diff --git a/src/core/Deal.Api/Endpoints/RatesEndpoints.cs b/src/core/Deal.Api/Endpoints/RatesEndpoints.cs index b1f6c1b..afd9874 100644 --- a/src/core/Deal.Api/Endpoints/RatesEndpoints.cs +++ b/src/core/Deal.Api/Endpoints/RatesEndpoints.cs @@ -46,7 +46,6 @@ public static class RatesEndpoints RatesDto current = await ratesService.GetAsync(ct); - // фоновый RefreshAsync в отдельном scope; ответ — текущий кэш. if (await ratesService.ShouldFetchAsync(ct)) { context.RequestServices.GetRequiredService().Schedule(); diff --git a/src/core/Deal.Api/Endpoints/SettingsEndpoints.cs b/src/core/Deal.Api/Endpoints/SettingsEndpoints.cs index a5fd33e..1545384 100644 --- a/src/core/Deal.Api/Endpoints/SettingsEndpoints.cs +++ b/src/core/Deal.Api/Endpoints/SettingsEndpoints.cs @@ -53,7 +53,6 @@ public static class SettingsEndpoints return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail); } - // Произвольный JSON-объект: ключи тела — публичные ключи §4.6 (как их шлёт фронт). Dictionary? body; try { @@ -64,7 +63,6 @@ public static class SettingsEndpoints } catch (JsonException) { - // Не-JSON или не-объект целиком — ошибка запроса: 400 + detail return EndpointResults.BadRequest(InvalidBodyDetail); } @@ -86,8 +84,6 @@ public static class SettingsEndpoints return Results.Ok(result); } - // body: Тело PATCH — публичные ключи §4.6. - // Возвращает: True — поле rateSource передано «правдивым» значением (не null/пустая строка). private static bool ShouldScheduleRatesRefresh(Dictionary body) { if (!body.TryGetValue(SettingsKeys.RateSource, out JsonElement element)) diff --git a/src/core/Deal.Api/Endpoints/StorageEndpoints.cs b/src/core/Deal.Api/Endpoints/StorageEndpoints.cs index 1790c00..6379c14 100644 --- a/src/core/Deal.Api/Endpoints/StorageEndpoints.cs +++ b/src/core/Deal.Api/Endpoints/StorageEndpoints.cs @@ -30,9 +30,6 @@ public static class StorageEndpoints return app; } - // Весь состав тика — AdminTickOrchestrator (вынесен из эндпоинта для unit-тестов логики и - // переиспользования): тик StorageTickService (Kanban) → PurgeExpiredAsync (отсев 3 суток, merge в - // storage.purgedRejected) → тосты статистики (включая «Отсев очищен: N записей (3 дн.)») → CheckDueAsync private static async Task AdminTickAsync(HttpContext context, CancellationToken ct) { if (!context.HasUser()) @@ -45,8 +42,6 @@ public static class StorageEndpoints return Results.Ok(await orchestrator.TickAsync(tenantId, ct)); } - // SearchTsv — генерируемые STORED-колонки Cards/RejectedItems: авто-актуальны, «пересборка» = создание - // отсутствующих GIN-индексов (CREATE INDEX IF NOT EXISTS) + ANALYZE таблиц (FtsMaintenance.RebuildAsync). private static async Task FtsRebuildAsync(HttpContext context, CancellationToken ct) { if (!context.HasUser()) diff --git a/src/core/Deal.Api/Endpoints/TelegramEndpoints.cs b/src/core/Deal.Api/Endpoints/TelegramEndpoints.cs index 2a88bb1..6b72878 100644 --- a/src/core/Deal.Api/Endpoints/TelegramEndpoints.cs +++ b/src/core/Deal.Api/Endpoints/TelegramEndpoints.cs @@ -55,13 +55,11 @@ public static class TelegramEndpoints // Путь включения/выключения мониторинга одного диалога (POST, параметр dialog_id). private const string DialogMonitorPath = "/dialogs/{dialog_id}/monitor"; - // Путь разбора одного диалога (POST, параметр dialog_id; сервер-only, api-map п.9). private const string DialogBackfillPath = "/dialogs/{dialog_id}/backfill"; // Деталь 400: сохранённый api_id не является корректным числом (Security review: int.TryParse). private const string InvalidApiIdDetail = "Некорректный api_id — проверьте глобальные ключи Telegram"; - // Деталь 400 без ключей приложения: оператор не задал глобальные ключи Telegram (ТЗ §4.1/§8.1). private const string NoKeysDetail = "Ключи Telegram не заданы оператором"; // Деталь недоступного telegram-service/неподключённого аккаунта (глобальная строка контракта). @@ -296,7 +294,6 @@ public static class TelegramEndpoints DialogsService dialogs = context.RequestServices.GetRequiredService(); ITelegramGateway gateway = context.RequestServices.GetRequiredService(); - // {ok:false, reason:"not-connected", count:0} HTTP 200 — refresh не ошибка запроса. bool connected; try { @@ -456,8 +453,6 @@ public static class TelegramEndpoints return keys.KeysSet ? keys : null; } - // exception: Исключение вызова гейта (RpcException домена/транспорта, прочее). - // Возвращает: 400-ответ с текстом причины. private static IResult GatewayError(Exception exception) { return EndpointResults.BadRequest(GatewayErrorText(exception)); diff --git a/src/core/Deal.Api/Extensions/AuthHelpers.cs b/src/core/Deal.Api/Extensions/AuthHelpers.cs index a958a9d..6958fb0 100644 --- a/src/core/Deal.Api/Extensions/AuthHelpers.cs +++ b/src/core/Deal.Api/Extensions/AuthHelpers.cs @@ -30,7 +30,6 @@ public static class AuthHelpers /// /// Кладёт пользователя в HttpContext.Items. /// - /// Контекст запроса. /// Пользователь, разрешённый по сессии. public static void SetCurrentUser(this HttpContext context, CurrentUser user) => context.Items[CurrentUserItemKey] = user; @@ -38,7 +37,6 @@ public static class AuthHelpers /// /// Возвращает текущего пользователя запроса или null, если сессия не разрешена. /// - /// Контекст запроса. /// Пользователь или null. public static CurrentUser? GetCurrentUser(this HttpContext context) => context.Items[CurrentUserItemKey] as CurrentUser; @@ -46,14 +44,12 @@ public static class AuthHelpers /// /// Проверяет, разрешена ли для запроса пользовательская сессия. /// - /// Контекст запроса. /// True — текущий пользователь установлен. public static bool HasUser(this HttpContext context) => context.GetCurrentUser() is not null; /// /// Кладёт оператора в HttpContext.Items. /// - /// Контекст запроса. /// Оператор, разрешённый по операторской сессии. public static void SetCurrentOperator(this HttpContext context, CurrentOperator operatorIdentity) => context.Items[CurrentOperatorItemKey] = operatorIdentity; @@ -61,7 +57,6 @@ public static class AuthHelpers /// /// Возвращает текущего оператора запроса или null, если операторская сессия не разрешена. /// - /// Контекст запроса. /// Оператор или null (ручка /api/operator/* отвечает 401). public static CurrentOperator? GetCurrentOperator(this HttpContext context) => context.Items[CurrentOperatorItemKey] as CurrentOperator; diff --git a/src/core/Deal.Api/Hosting/BudgetAlertScheduler.cs b/src/core/Deal.Api/Hosting/BudgetAlertScheduler.cs index 9cc9976..266b549 100644 --- a/src/core/Deal.Api/Hosting/BudgetAlertScheduler.cs +++ b/src/core/Deal.Api/Hosting/BudgetAlertScheduler.cs @@ -158,7 +158,6 @@ public sealed class BudgetAlertScheduler : IHostedService { ITenantLimitStore limitStore = tenantScope.ServiceProvider.GetRequiredService(); - // TryMark* — атомарная установка флага: true только в момент первого наблюдения порога за период if (await limitStore.TryMarkWarnedAsync(tenant.Id, ct)) { _logger.LogDebug("Бюджет тенанта {TenantId}: порог 80% пересечён — SSE-тост", tenant.Id); diff --git a/src/core/Deal.Api/Hosting/MlOutboxFlushScheduler.cs b/src/core/Deal.Api/Hosting/MlOutboxFlushScheduler.cs index 1210ee0..2d723eb 100644 --- a/src/core/Deal.Api/Hosting/MlOutboxFlushScheduler.cs +++ b/src/core/Deal.Api/Hosting/MlOutboxFlushScheduler.cs @@ -140,11 +140,6 @@ public sealed class MlOutboxFlushScheduler : IHostedService } } - // Выгрузка очереди одного тенанта в собственном scope: SetTenant → порции по 10 до ≤100/цикл. - // цикл тенанта завершается (следующая попытка — следующий тик). Сбой хранилища тенанта не валит проход: - // ошибка логируется, остальные тенанты обрабатываются; отмена (OCE) пробрасывается наверх. - // tenant: Тенант реестра (Id в формате Guid; схема — tenant_<N>). - // ct: Токен отмены прохода. private async Task FlushTenantAsync(TenantRecordDto tenant, CancellationToken ct) { await using AsyncServiceScope tenantScope = _scopeFactory.CreateAsyncScope(); @@ -176,7 +171,6 @@ public sealed class MlOutboxFlushScheduler : IHostedService } catch (Exception exception) { - // попытка — на следующем тике; флашер не роняет проход цикла. _logger.LogWarning( exception, "Флашер ML-outbox: отправка {RowCount} строк тенанта {TenantId} не удалась — строки остались", diff --git a/src/core/Deal.Api/Hosting/StorageTickScheduler.cs b/src/core/Deal.Api/Hosting/StorageTickScheduler.cs index a127a8e..bb7dd97 100644 --- a/src/core/Deal.Api/Hosting/StorageTickScheduler.cs +++ b/src/core/Deal.Api/Hosting/StorageTickScheduler.cs @@ -143,13 +143,6 @@ public sealed class StorageTickScheduler : IHostedService } } - // Тик одного тенанта в собственном scope: SetTenant → Kanban-тик → purge отсева → SSE-тосты → - // тосты → напоминания). - // Контекст AsyncLocal сбрасывается в finally, чтобы не переживать scope тенанта (как - // SessionMiddleware). Тик одного тенанта не валит проход: ошибка ветки/тенанта логируется, остальные - // тенанты обрабатываются; отмена (OCE) пробрасывается наверх — проход завершается. - // tenant: Тенант реестра (Id в формате Guid; схема — tenant_<N>). - // ct: Токен отмены прохода. private async Task TickTenantAsync(TenantRecordDto tenant, CancellationToken ct) { await using AsyncServiceScope tenantScope = _scopeFactory.CreateAsyncScope(); @@ -162,15 +155,12 @@ public sealed class StorageTickScheduler : IHostedService StorageTickService tickService = tenantScope.ServiceProvider.GetRequiredService(); StorageTickStatsDto stats = await tickService.TickAsync(ct); - // (AdminTickOrchestrator), тост «Отсев очищен: N записей (3 дн.)» публикуется этой же веткой. PipelineProcessingService processing = tenantScope.ServiceProvider.GetRequiredService(); int purgedRejected = await processing.PurgeExpiredAsync(ct); StorageTickStatsDto mergedStats = stats with { PurgedRejected = purgedRejected }; _toastPublisher.PublishTickToasts(tenant.Id, mergedStats); - // возвращает их {id,title,containerId}. Сбой проверки НЕ роняет тик тенанта/проход: лог-предупреждение, - // остальные тенанты обрабатываются (паттерн ветки AdminTickOrchestrator). SSE reminder_due по каждой IReadOnlyList dueReminders; try { diff --git a/src/core/Deal.Api/Hosting/TenantBootstrapService.cs b/src/core/Deal.Api/Hosting/TenantBootstrapService.cs index eb97c75..b171fe1 100644 --- a/src/core/Deal.Api/Hosting/TenantBootstrapService.cs +++ b/src/core/Deal.Api/Hosting/TenantBootstrapService.cs @@ -38,7 +38,6 @@ public sealed class TenantBootstrapService(IServiceScopeFactory scopeFactory) : var password = configuration[BootstrapPasswordEnvKey] ?? DefaultAdminPassword; var environment = scope.ServiceProvider.GetRequiredService(); - // в Development или при DEAL_BOOTSTRAP_DEFAULT_TENANT=1. В Production тенантов заводит оператор. var seedDefaultTenant = environment.IsDevelopment() || configuration[DefaultTenantBootstrapEnvKey] == DefaultTenantBootstrapEnabledValue; if (seedDefaultTenant) @@ -65,8 +64,6 @@ public sealed class TenantBootstrapService(IServiceScopeFactory scopeFactory) : } } - // что созданного в dev: пакетная идемпотентная миграция (CREATE SCHEMA IF NOT EXISTS + Migrate, - // применяющий только неприменённые миграции) с ограниченным параллелизмом и логированием прогресса — var tenantSchemaMigrationService = scope.ServiceProvider.GetRequiredService(); await tenantSchemaMigrationService.MigrateAllAsync(ct); } diff --git a/src/core/Deal.Api/Logging/DealLogging.cs b/src/core/Deal.Api/Logging/DealLogging.cs index 2e09e59..b6d7853 100644 --- a/src/core/Deal.Api/Logging/DealLogging.cs +++ b/src/core/Deal.Api/Logging/DealLogging.cs @@ -4,17 +4,6 @@ using Serilog.Formatting.Compact; namespace Deal.Api.Logging; -// Все четыре процесса (core + telegram/ai/ml-сервисы) логируют через Serilog: консоль — JSON в -// prod-стиле (CompactJsonFormatter: одна JSON-строка на событие, поля @t/@mt/@l — парсинг -// Loki/Promtail) либо текст в Development; плюс rolling-файл data/logs/deal-<процесс>.json -// (под ContentRoot; у core каталог data смонтирован volume-ом, compose.prod). Уровень/каталог -// переопределяются env: DEAL_LOG_LEVEL, DEAL_LOGS_DIR. -// Конфигурация кодом, а не секцией appsettings: у трёх сервисов appsettings.json нет (весь конфиг — -// стек: Serilog-логи → docker-логи → Promtail → Loki → Grafana. -// Вызов — из Program.cs процесса (entry point): DealLogging.Configure(builder, "…") ДО -// builder.Build(). Интеграционные тесты сервисов поднимают хост через *ServiceHost.Create -// БЕЗ этого вызова (конфигурация логирования — забота production-точки входа), поэтому тесты не пишут -// файлы-логи и не меняют своё логирование. internal static class DealLogging { // Env-ключ минимального уровня Serilog (Debug/Information/Warning/Error; дефолт Information). diff --git a/src/core/Deal.Api/Middleware/HttpAccessLogMiddleware.cs b/src/core/Deal.Api/Middleware/HttpAccessLogMiddleware.cs index 499bdb7..4bfede3 100644 --- a/src/core/Deal.Api/Middleware/HttpAccessLogMiddleware.cs +++ b/src/core/Deal.Api/Middleware/HttpAccessLogMiddleware.cs @@ -29,7 +29,6 @@ public sealed class HttpAccessLogMiddleware /// /// Обрабатывает запрос /// - /// Контекст запроса. public async Task InvokeAsync(HttpContext context) { if (context.Request.ContentType?.StartsWith(GrpcContentType, StringComparison.OrdinalIgnoreCase) == true) diff --git a/src/core/Deal.Api/Middleware/OperatorSessionMiddleware.cs b/src/core/Deal.Api/Middleware/OperatorSessionMiddleware.cs index e617ec4..9f8e6c7 100644 --- a/src/core/Deal.Api/Middleware/OperatorSessionMiddleware.cs +++ b/src/core/Deal.Api/Middleware/OperatorSessionMiddleware.cs @@ -24,7 +24,6 @@ public sealed class OperatorSessionMiddleware /// /// Обрабатывает запрос /// - /// Контекст запроса. public async Task InvokeAsync(HttpContext context) { var cookieName = _cookieOptions.CurrentValue.Name; diff --git a/src/core/Deal.Api/Middleware/OriginGuardMiddleware.cs b/src/core/Deal.Api/Middleware/OriginGuardMiddleware.cs index a952f66..0dfc24c 100644 --- a/src/core/Deal.Api/Middleware/OriginGuardMiddleware.cs +++ b/src/core/Deal.Api/Middleware/OriginGuardMiddleware.cs @@ -30,7 +30,6 @@ public sealed class OriginGuardMiddleware /// /// Обрабатывает запрос /// - /// Контекст запроса. public async Task InvokeAsync(HttpContext context) { HttpRequest request = context.Request; diff --git a/src/core/Deal.Api/Middleware/RateLimitPolicies.cs b/src/core/Deal.Api/Middleware/RateLimitPolicies.cs index 7627d8a..6c9a434 100644 --- a/src/core/Deal.Api/Middleware/RateLimitPolicies.cs +++ b/src/core/Deal.Api/Middleware/RateLimitPolicies.cs @@ -49,9 +49,7 @@ public static class RateLimitPolicies /// /// Регистрирует AddRateLimiter с политиками "auth"/"api" и глобальным лимитером API-партиции. /// - /// Коллекция сервисов хоста. /// Настройки rate limiting (секция RateLimit). - /// Коллекция сервисов для цепочки вызовов. public static IServiceCollection AddDealRateLimiter(this IServiceCollection services, RateLimitOptions options) { ArgumentNullException.ThrowIfNull(services); @@ -126,8 +124,6 @@ public static class RateLimitPolicies return string.IsNullOrEmpty(ip) ? UnknownClientKey : IpKeyPrefix + ip; } - // context: Контекст отклонённого запроса. - // cancellationToken: Токен отмены ответа. private static async ValueTask OnRejectedAsync(OnRejectedContext context, CancellationToken cancellationToken) { context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests; diff --git a/src/core/Deal.Api/Middleware/SessionMiddleware.cs b/src/core/Deal.Api/Middleware/SessionMiddleware.cs index fc85f89..12a16f0 100644 --- a/src/core/Deal.Api/Middleware/SessionMiddleware.cs +++ b/src/core/Deal.Api/Middleware/SessionMiddleware.cs @@ -31,7 +31,6 @@ public sealed class SessionMiddleware /// /// Обрабатывает запрос /// - /// Контекст запроса. public async Task InvokeAsync(HttpContext context) { try diff --git a/src/core/Deal.Api/Program.cs b/src/core/Deal.Api/Program.cs index 33d7a81..7182d58 100644 --- a/src/core/Deal.Api/Program.cs +++ b/src/core/Deal.Api/Program.cs @@ -52,25 +52,17 @@ const string serverUrlsKey = "urls"; // Фолбэк основного HTTP-адреса при отсутствии явных URL (дефолт ASP.NET Core http://localhost:5000). const string defaultHttpUrl = "http://localhost:5000"; const string defaultAiBudgetEnvKey = "DEAL_DEFAULT_AI_BUDGET"; -// умолчанию (curl-приёмки не режутся); PROD включает env-переопределением RateLimit__Enabled=true const string rateLimitSectionName = "RateLimit"; -// фоновый цикл DataRetentionScheduler чистит audit_log по retention, лимиты/счётчики прошедших окон. const string dataRetentionSectionName = "DataRetention"; const string securitySectionName = "Security"; -// KnownNetworks — доверенные прокси (Caddy в PROD); dev-дефолт в appsettings — loopback. const string forwardedHeadersSectionName = "ForwardedHeaders"; const string coreProcessName = "core"; var builder = WebApplication.CreateBuilder(args); -// rolling-файл data/logs/deal-core-*.json (data — volume контейнера). Уровень/каталог — env -// DEAL_LOG_LEVEL/DEAL_LOGS_DIR (см. DealLogging). Регистрируется до остальных сервисов: логирование -// заменяет провайдеры Microsoft при builder.Build(). DealLogging.Configure(builder, coreProcessName); -// отдельном HTTP/1.1 Kestrel-эндпоинте (порт 9464/env METRICS_PORT) + инструментация входящих HTTP- -// запросов и исходящих HTTP-клиентов; прикладные метрики — SharedKernel/Observability/DealMetrics. int metricsPort = DealMetricsHosting.ResolveMetricsPort(DealMetricsHosting.DefaultMetricsPort); DealMetricsHosting.AddDealMetrics(builder, metricsPort); @@ -82,9 +74,6 @@ builder.Services.AddDbContext(options => options.UseNpgsql(connec builder.Services.AddSingleton(); builder.Services.AddSingleton(); -// (генерация — scripts/mtls-certs.sh). При DEAL_MTLS_ENABLED=1 сертификаты грузятся сразу (fail-fast на -// битые пути/пароли) — один экземпляр используют и Kestrel-ингресс ниже, и gRPC-клиенты -// (Ml/Ai/Telegram-каналы + ServiceHealthProbe). MtlsOptions mtlsOptions = MtlsOptions.FromConfiguration(builder.Configuration); MtlsCertificates? mtlsCertificates = MtlsCertificates.Load(mtlsOptions); if (mtlsCertificates is not null) @@ -92,9 +81,6 @@ if (mtlsCertificates is not null) builder.Services.AddSingleton(mtlsCertificates); } -// Kestrel: основной HTTP/1.1-эндпоинт из URL-конфигурации (как раньше — --urls/ASPNETCORE_URLS/ -// launchSettings) + второй endpoint gRPC-ингресса telegram-service (:5082, HTTP/2, env GRPC_INGRESS_PORT; -// пере-биндим адресами конфигурации "urls" явно (см. BindMainHttpEndpoints ниже). Ingress слушает все builder.WebHost.ConfigureKestrel(kestrel => { BindMainHttpEndpoints(kestrel, builder.Configuration[serverUrlsKey]); @@ -104,7 +90,6 @@ builder.WebHost.ConfigureKestrel(kestrel => listen.Protocols = HttpProtocols.Http2; if (mtlsCertificates is not null) { - // сертификат (цепочка до CA из DEAL_MTLS_CA_PEM). Основной HTTP :5080 остаётся http — TLS наружу listen.UseHttps(https => { https.ServerCertificate = mtlsCertificates.ServerCertificate; @@ -141,83 +126,49 @@ builder.Services.AddDbContext( builder.Services.AddTenantsModule(); -// env DEAL_DEFAULT_AI_BUDGET (токенов в месяц) с фолбэком на константу модуля TokenBudgetDefaults (10 000 000); -// период нового тенанта — month (константа). Значение читается один раз на старте и передаётся адаптеру -// TenantLimitStore (GetOrCreateAsync при первом чтении/списании, задачи 7/10 list-путь тоже закрыт). TokenLimitDefaults tenantLimitDefaults = new( ResolveDefaultAiBudget(builder.Configuration), TokenBudgetDefaults.DefaultPeriod); builder.Services.AddDealPersistence(tenantLimitDefaults); -// либо файла data/encryption.key под ContentRoot (dev). Ключ разрешается на старте — -// невалидный env-ключ останавливает запуск. builder.Services.AddDealSecurity(builder.Environment.ContentRootPath); -// ml-service :5103). Local-адаптеры читают KV-настройки тенанта через ISettingsStore — scoped (вне MlServiceOptions mlOptions = builder.Configuration.GetSection(servicesSectionName).Get() ?? new MlServiceOptions(); builder.Services.AddSingleton(mlOptions); -// default; локальный разбор ядра / инструменты не поддерживаются) либо декораторы бюджетного гейта поверх -// gRPC-клиентов ai-service (UseLocal=false, ai-service :5102; AddDealIntegrations регистрирует GrpcAiClassifier/ -// GrpcAiTools + транспорт AiGrpcConnection — fail-fast, как MlGrpcConnection — и оборачивает их в AiServiceOptions aiOptions = builder.Configuration.GetSection(aiServicesSectionName).Get() ?? new AiServiceOptions(); builder.Services.AddSingleton(aiOptions); -// нейтральный no-op/idle) либо gRPC-клиент GrpcTelegramClient (UseLocal=false, telegram-service :5101). TelegramServiceOptions telegramOptions = builder.Configuration.GetSection(telegramServicesSectionName).Get() ?? new TelegramServiceOptions(); builder.Services.AddSingleton(telegramOptions); builder.Services.AddDealIntegrations(mlOptions, aiOptions, telegramOptions, mtlsCertificates); -// к Services:*:Endpoint с дедлайном 3 с (ServiceHealthProbe). Stateless, singleton — пробы строят builder.Services.AddSingleton(new ServiceHealthProbe(mtlsCertificates)); -// под ContentRoot) — dev/curl/unit по умолчанию; MinioFileStorage регистрируется, только когда сконфигурирован -// MinIO (секция Storage:Minio либо env-алиасы DEAL_MINIO_*; compose-сервис deal-minio, порты 9000/9001). -// Singleton: хранилище не привязано к схеме тенанта (объекты — в едином бакете/каталоге, мульти-аренда builder.Services.AddDealFileStorage(builder.Configuration, builder.Environment.ContentRootPath); -// Модуль Settings (сервис настроек тенанта); адаптеры ISettingsStore/ISecretCipher уже builder.Services.AddSettingsModule(); -// порт-адаптер ICardStore → KanbanStore уже зарегистрирован AddDealPersistence. builder.Services.AddKanbanModule(); -// IPipelineStore → PipelineStore и внешние порты (IMlClient/IAiClassifier) уже зарегистрированы -// AddDealPersistence/AddDealIntegrations выше; сервисы модуля вызывают из эндпоинтов /api/pipeline/* builder.Services.AddPipelineModule(); -// Dialogs/TgMessages схемы тенанта (миграция TenantTelegram). Порт-адаптеры ITelegramStore → TelegramStore и -// ITelegramGateway → LocalTelegramGateway/GrpcTelegramClient зарегистрированы AddDealPersistence/AddDealIntegrations builder.Services.AddTelegramModule(); -// и воркер (оценка/бан-гард/паузы). Порт-адаптер IDiscoveryStore → DiscoveryStore зарегистрирован -// AddDealPersistence; внешние порты (ITelegramGateway/IAiTools/IMlClient) — AddDealIntegrations выше. Эндпоинты builder.Services.AddDiscoveryModule(); -// счётчик мониторящихся + keysSet) и чтение глобальных ключей приложения (telegramKeys в public.global_settings, -// расшифровка apiHash — задаёт оператор, ТЗ §4.1/§8.1). Scoped: зависимости — ISettingsStore/ITelegramStore -// на TenantDbContext схемы тенанта запроса, IGlobalSettingsStore — на системном DealDbContext. builder.Services.AddScoped(); builder.Services.AddScoped(); -// мониторинга/«Перечитать» отвечают сразу, тяжёлый разбор идёт в отдельном scope с захваченным tenant-контекстом. builder.Services.AddSingleton(); -// Pipeline + один проход pump + SSE-публикации (тосты/new_card) для POST /api/admin/tick (StorageEndpoints). -// Scoped: зависимости живут в рамках tenant-запроса (scoped-сервисы модулей на TenantDbContext схемы). builder.Services.AddScoped(); -// CREATE INDEX IF NOT EXISTS + ANALYZE (FtsMaintenance) на TenantDbContext запроса (scoped, как адаптеры). builder.Services.AddScoped(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); -// POST /api/admin/tick (AdminTickOrchestrator) и фоновый цикл разбора очереди (PipelineWorkerScheduler) -// не разбирают очередь одного тенанта одновременно (singleton per-tenant флагов, Interlocked). builder.Services.AddSingleton(); -// "RateLimit" (appsettings.json + env RateLimit__*). Enabled=false в dev/тестах — политики/middleware/ -// LoginAttemptGuard (окно ip|login 5 неудач/15 мин в public.rate_limit_counters) регистрируется всегда, -// но активен только при Enabled. RateLimitOptions rateLimitOptions = builder.Configuration .GetSection(rateLimitSectionName) .Get() ?? new RateLimitOptions(); @@ -230,18 +181,12 @@ if (rateLimitOptions.Enabled) builder.Services.AddDealRateLimiter(rateLimitOptions); } -// на отдельном Kestrel-endpoint (:5082, HTTP/2, см. ConfigureKestrel выше) в том же процессе. Token -// из metadata «service-token» проверяет интерцептор (fail-closed, DEAL_SERVICE_TOKEN); AddAuthentication -// не нужен — пользовательская сессия HTTP ингрессом не используется (tenant-id из metadata → SetTenant). builder.Services.AddGrpc(grpc => { - // вызовы (401/429); gRPC-health не логируется (см. RpcCallLoggingInterceptor). grpc.Interceptors.Add(); grpc.Interceptors.Add(); if (rateLimitOptions.Enabled) { - // singleton-лимитер (CreateLimiter) — экземпляры интерцептора общий PartitionedRateLimiter - // разделяют; health-методы освобождены (см. IngressRateLimitInterceptor). grpc.Interceptors.Add(); } }); @@ -255,62 +200,39 @@ if (rateLimitOptions.Enabled) builder.Services.AddScoped(); -// grpc.health.v1.Health интерцептор токеном не проверяет (инфраструктурный liveness, как в сервисах -// Живучесть интеграций (ml/ai/telegram) health не проверяет — недоступность сервиса это UNAVAILABLE builder.Services .AddGrpcHealthChecks() .AddCheck("ready", () => HealthCheckResult.Healthy("хост Deal.Api готов")); -// HTTP-адаптер Infrastructure с собственным HttpClient (фабрика AddHttpClient, таймаут 12 с). -// HTTP наружу ходит только по действию Settings-экрана (POST /api/ai/check) — GET {base}/models. builder.Services.AddHttpClient( client => client.Timeout = TimeSpan.FromSeconds(AiConnectionChecker.RequestTimeoutSeconds)); -// адаптера (SSRF-allowlist), источник тенантом не настраивается. Типизированный клиент builder.Services.AddHttpClient( client => client.Timeout = TimeSpan.FromSeconds(CbrRateSource.RequestTimeoutSeconds)); -// собственный scope + in-flight guard (см. RatesRefreshScheduler). builder.Services.AddSingleton(); builder.Services.AddHostedService(); -// dev-дефолт operator/operator в Development; в Production без env — warning и пропуск. Идёт после -// TenantBootstrapService: операторские public-таблицы не зависят от провижининга схем тенантов. builder.Services.AddHostedService(); -// 30 с тикает ВСЕ тенанты (StorageTickService + автоочистка отсева пайплайна 3 суток) и публикует -// SSE-тосты. Регистрируется после Bootstrap — первый проход стартует уже после провижининга схем. builder.Services.AddHostedService(); -// проверяет ВСЕ тенанты и публикует в канал тенанта тост при пересечении порогов 80/100% (TryMark*-CAS — -// один тост на порог за период). Идёт после Bootstrap: реестр тенантов провижинен до первого прохода. builder.Services.AddHostedService(); -// каждые 2 с pump'ит ВСЕ тенанты (PipelineWorkerService.PumpOnceAsync под общим PipelinePumpGate) и -// После StorageTickScheduler: очередь цикла — 2 с, первый проход сразу после старта. builder.Services.AddHostedService(); -// выгружает MlOutbox тенантов в ml-service (TrainBatch, порции по 10, ≤100/цикл; удаление после успеха). if (!mlOptions.UseLocal) { builder.Services.AddHostedService(); } -// делает ОДИН шаг (поиск/оценка/авто-вступление/done) для каждой running-задачи всех тенантов. Работает -// всегда: в Local-режиме гейт нейтрален (поиск пуст/история недоступна), реальные действия — при -// подключённом telegram-service (UseLocal=false). После Bootstrap: первый проход стартует после провижининга. builder.Services.AddHostedService(); builder.Services.AddSingleton(); -// MlOutbox) по всем тенантам и число активных сессий в meter Deal (callback /metrics отдаёт их Prometheus). -// Регистрируется последним из фоновых: после Bootstrap (реестр тенантов провижинен до первого прохода). builder.Services.AddHostedService(); -// записи audit_log старше DataRetention:AuditRetentionDays (дефолт 180 дней), сбрасывает накопительные -// поля лимитов прошедших периодов и убирает завершившиеся окна распределённых счётчиков. Регистрируется -// последним из фоновых: после Bootstrap (реестр тенантов провижинен до первого прохода). DataRetentionOptions dataRetentionOptions = builder.Configuration .GetSection(dataRetentionSectionName) .Get() ?? new DataRetentionOptions(); @@ -320,28 +242,21 @@ builder.Services.AddHostedService(); // Кука сессии: имя/срок/Secure из секции "Cookies" (appsettings.json + env Cookies__*). builder.Services.Configure(builder.Configuration.GetSection(cookiesSectionName)); -// "OperatorCookies" (appsettings.json + env OperatorCookies__*) — отдельная от тенантной deal_session. builder.Services.Configure(builder.Configuration.GetSection(operatorCookiesSectionName)); builder.Services.ConfigureHttpJsonOptions(options => options.SerializerOptions.Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping); -// проверки мутаций и CORS (пусто — dev-режим «свой origin», см. AddCors ниже; PROD — домен фронта в -// compose-prod). Инстанс регистрируется в DI: значение читается один раз на старте (политики формируются -// при старте хоста), OriginGuardMiddleware получает его конструктором. SecurityOptions securityOptions = builder.Configuration .GetSection(securitySectionName) .Get() ?? new SecurityOptions(); builder.Services.AddSingleton(securityOptions); -// dev-дефолт — false (прокси в dev-стеке нет, compose.dev публикует core напрямую). ForwardedHeadersConfig forwardedHeadersConfig = builder.Configuration .GetSection(forwardedHeadersSectionName) .Get() ?? new ForwardedHeadersConfig(); builder.Services.AddSingleton(forwardedHeadersConfig); -// (любой origin/method/header, credentials=true; AllowAnyOrigin + AllowCredentials несовместимы — любой -// Security-заголовки ответов (nosniff/X-Frame-Options/Referrer-Policy; CSP/HSTS) — на edge (Caddyfile, builder.Services.AddCors(options => options.AddPolicy(corsPolicyName, cors => { @@ -380,7 +295,6 @@ if (app.Environment.IsProduction()) } } -// с путём data/attachments; при сконфигурированном MinIO — MinioFileStorage с endpoint/бакетом). app.Logger.LogInformation("Файловое хранилище: {FileStorage}", app.Services.GetRequiredService()); app.Logger.LogInformation( @@ -404,14 +318,9 @@ app.Logger.LogInformation( if (forwardedHeadersConfig.Enabled) { - // только клиентам из ForwardedHeaders:KnownProxies/KnownNetworks (конфиг; appsettings — loopback для dev). - // Middleware — ПЕРВЫЙ в конвейере: RemoteIpAddress/Scheme читают слои ниже (CORS, Session/OperatorSession — - // audit-IP эндпоинтов, RateLimiter — ключи по IP, LoginAttemptGuard). Без него за Caddy (compose-prod, app.UseForwardedHeaders(BuildForwardedHeadersOptions(forwardedHeadersConfig)); } -// и статус всего пути обработки. gRPC-ингресс (Content-Type application/grpc) middleware пропускает — -// его логирует интерцептор RpcCallLoggingInterceptor (см. HttpAccessLogMiddleware). app.UseMiddleware(); app.UseCors(corsPolicyName); @@ -419,13 +328,9 @@ app.UseMiddleware(); app.UseMiddleware(); if (rateLimitOptions.Enabled) { - // CurrentUser.TenantId либо IP анонима; сессии уже разрешены). При Enabled=false лимитер не app.UseRateLimiter(); } -// RateLimiter → OriginGuard (сессии разрешены, 429 важнее 403). Проверяются не-GET/HEAD/OPTIONS запросы -// с заголовком Origin: Origin == «свой» origin (схема + Host; за Caddy — https из X-Forwarded-Proto) -// либо входит в Security:AllowedOrigins; иначе 403 {detail}. Без Origin (curl/сервер-сервер) пропускаются; app.UseMiddleware(); app.MapGet("/api/health", () => Results.Ok(new { ok = true, service = "deal" })); @@ -435,12 +340,9 @@ app.MapOperatorAuditEndpoints(); app.MapOperatorAnalyticsEndpoints(); app.MapOperatorInvitesEndpoints(); app.MapOperatorTenantsEndpoints(); -// .../limit, аудит tenant_limit_changed) + health ядра/БД и сервисов ml/ai/telegram. app.MapOperatorLimitsEndpoints(); app.MapOperatorHealthEndpoints(); -// Глобальные настройки оператора (ТЗ §4.1/§8.1): ключи приложения Telegram — чтение (маска) и смена. app.MapOperatorSettingsEndpoints(); -// (ограниченный параллелизм + логирование прогресса) — для SaaS с сотнями/тысячами схем. app.MapOperatorMaintenanceEndpoints(); app.MapJoinEndpoint(); app.MapSettingsEndpoints(); @@ -455,12 +357,9 @@ app.MapStorageEndpoints(); app.MapEventsEndpoint(); app.MapAiSuggestEndpoints(); app.MapPipelineEndpoints(); -// (BootStubEndpoints удалён); qr-image — отдельным файлом. app.MapTelegramEndpoints(); app.MapTelegramQrImageEndpoint(); app.MapDiscoveryEndpoints(); -// входящего потока считает IngressRateLimitInterceptor по tenant-id из metadata (иначе общее окно на IP -// telegram-service резало бы весь ингресс раньше интерцептора); health — инфраструктурный liveness. app.MapGrpcService().DisableRateLimiting(); app.MapGrpcHealthChecksService().DisableRateLimiting(); @@ -531,7 +430,6 @@ static List ParseHttpAddresses(string? urlsConfig) static int? ParsePort(string? rawValue) => int.TryParse(rawValue, out int parsedPort) ? parsedPort : null; -// значение (пустая переменная, опечатка) — константа модуля TokenBudgetDefaults.DefaultBudgetTokens. Период long ResolveDefaultAiBudget(IConfiguration configuration) { string? rawValue = configuration[defaultAiBudgetEnvKey]; diff --git a/src/core/Deal.Api/Services/AdminTickOrchestrator.cs b/src/core/Deal.Api/Services/AdminTickOrchestrator.cs index 3c2e62e..794babc 100644 --- a/src/core/Deal.Api/Services/AdminTickOrchestrator.cs +++ b/src/core/Deal.Api/Services/AdminTickOrchestrator.cs @@ -51,7 +51,6 @@ public sealed class AdminTickOrchestrator( toastPublisher.PublishTickToasts(tenantId, mergedStorage); - // проверки НЕ роняет тик: лог + reminders ответа пуст (очередь/хранение продолжают работать). IReadOnlyList dueReminders; try { @@ -68,13 +67,11 @@ public sealed class AdminTickOrchestrator( dueReminders = Array.Empty(); } - // шлём — у фронта модалка ReminderNotice; без подписчиков канала публикация — no-op). После MarkFired foreach (CardReminderDueDto due in dueReminders) { broker.Publish(tenantId, ReminderDueEventType, due); } - // (5) Один проход pump под гейтом тенанта; сбой не роняет тик: pipeline={}, очередь дождётся PipelinePumpResult? pump = await PumpOnceSafelyAsync(tenantId, ct); if (pump is not null) @@ -94,11 +91,8 @@ public sealed class AdminTickOrchestrator( queueCounts.Total); } - // ct: Токен отмены запроса. - // Возвращает: Результат прохода либо null — гейт занят другим воркером/pump упал (pipeline ответа пуст). private async Task PumpOnceSafelyAsync(Guid tenantId, CancellationToken ct) { - // разбирают очередь тенанта одновременно. Гейт занят (фоновый цикл уже pump'ит) — проход пропускаем, if (!pumpGate.TryEnter(tenantId)) { return null; @@ -124,8 +118,6 @@ public sealed class AdminTickOrchestrator( } } - // wire не выходят — они ушли отдельными SSE new_card). - // pump: Результат успешного прохода pump. private static IReadOnlyDictionary ToPipelineWireDict(PipelinePumpResult pump) { int[] counters = diff --git a/src/core/Deal.Api/Services/AuditAppender.cs b/src/core/Deal.Api/Services/AuditAppender.cs index 46076fc..2700a50 100644 --- a/src/core/Deal.Api/Services/AuditAppender.cs +++ b/src/core/Deal.Api/Services/AuditAppender.cs @@ -13,7 +13,6 @@ public static class AuditAppender /// /// Пишет событие действия пользователя тенанта /// - /// Контекст запроса (источник актора и IP). /// Тип события — константа . /// Минимальные детали события (обычно анонимный объект) или null. public static async Task AppendTenantAsync( @@ -43,7 +42,6 @@ public static class AuditAppender /// /// Пишет событие действия оператора /// - /// Контекст запроса (источник актора и IP). /// Тип события — константа . /// Минимальные детали события (обычно анонимный объект) или null. public static async Task AppendOperatorAsync( diff --git a/src/core/Deal.Api/Services/SessionCookieWriter.cs b/src/core/Deal.Api/Services/SessionCookieWriter.cs index 7babf74..8b9533c 100644 --- a/src/core/Deal.Api/Services/SessionCookieWriter.cs +++ b/src/core/Deal.Api/Services/SessionCookieWriter.cs @@ -11,7 +11,6 @@ public static class SessionCookieWriter /// /// Выставляет httpOnly-куку сессии на текущем ответе. /// - /// Контекст запроса. /// Настройки куки из конфигурации (секция Cookies). /// Raw-токен сессии. public static void Append( diff --git a/src/core/Deal.Api/Telegram/TelegramIngressService.cs b/src/core/Deal.Api/Telegram/TelegramIngressService.cs index 4a42cd7..97721ee 100644 --- a/src/core/Deal.Api/Telegram/TelegramIngressService.cs +++ b/src/core/Deal.Api/Telegram/TelegramIngressService.cs @@ -107,7 +107,6 @@ public sealed class TelegramIngressService( } catch (Exception exception) { - // Сбой схемы/БД тенанта (напр. схема ещё не провижинена): RPC не падает — reply not-accepted logger.LogWarning(exception, "Аудит: PushMessage {TenantId} → не принято (сбой схемы/БД)", tenant.Id); return new PushMessageReply(); } @@ -226,11 +225,6 @@ public sealed class TelegramIngressService( } } - // Разрешает тенанта запроса: metadata tenant-id → реестр public.tenants. - // Отсутствующий/пустой tenant-id — RPC-отказ UNAUTHENTICATED (README: tenant-id обязателен). - // Id не Guid либо записи нет в реестре — неизвестный тенант: лог аудита и null (RPC отвечает не-принято, - // context: Контекст вызова. - // Возвращает: Запись тенанта реестра либо null (тенант неизвестен). private async Task ResolveTenantAsync(ServerCallContext context) { string tenantId = RequireTenantIdMetadata(context); @@ -265,12 +259,6 @@ public sealed class TelegramIngressService( return tenantId; } - // Пишет превью принятого сообщения (TgMessages + «последнее сообщение» каталога) без влияния на приём. - // не роняет RPC и не меняет accepted — очередь уже записана, упущенное догонит realtime-sweep (как - // tenantScope: Scope тенанта (TenantDbContext построен на схеме тенанта). - // tenant: Тенант канала (для лога аудита). - // request: Сообщение PushMessage. - // ct: Токен отмены. private async Task SavePreviewSafelyAsync( AsyncServiceScope tenantScope, TenantRecordDto tenant, @@ -325,9 +313,6 @@ public sealed class TelegramIngressService( } } - // tenantId: Тенант-получатель. - // text: Текст тоста. - // icon: Иконка тоста (набор Icon.vue фронта). private void PublishToast( Guid tenantId, string text, diff --git a/src/core/Deal.Api/Telegram/TgStatusService.cs b/src/core/Deal.Api/Telegram/TgStatusService.cs index 43c6086..28ab356 100644 --- a/src/core/Deal.Api/Telegram/TgStatusService.cs +++ b/src/core/Deal.Api/Telegram/TgStatusService.cs @@ -31,7 +31,7 @@ public sealed class TgStatusService( }; /// - /// Форма GET /api/tg/status текущего тенанта + /// Форма GET /api/tg/status текущего тенанта. /// /// Полный статус вкладки Telegram. public async Task GetAsync(CancellationToken ct) @@ -52,8 +52,6 @@ public sealed class TgStatusService( QrUrl: live.QrUrl); } - // ct: Токен отмены. - // Возвращает: Статус гейта либо idle-поля. private async Task ReadLiveAsync(CancellationToken ct) { try @@ -66,8 +64,6 @@ public sealed class TgStatusService( } } - // ct: Токен отмены. - // Возвращает: Аккаунт или пустая строка. private async Task ReadAccountAsync(CancellationToken ct) { SettingValue? row = await settings.GetAsync(SettingsKeys.TgAccount, ct).ConfigureAwait(false); diff --git a/src/core/Deal.Contracts/Integrations/Abstractions/IAiClassifier.cs b/src/core/Deal.Contracts/Integrations/Abstractions/IAiClassifier.cs index 19ea94d..a26b349 100644 --- a/src/core/Deal.Contracts/Integrations/Abstractions/IAiClassifier.cs +++ b/src/core/Deal.Contracts/Integrations/Abstractions/IAiClassifier.cs @@ -15,7 +15,7 @@ public interface IAiClassifier public Task FilterAsync(string text, CancellationToken ct); /// - /// Полный разбор карточки — структура классификации ТЗ §5 и classify. + /// Полный разбор карточки — структура классификации и classify. /// /// Текст сообщения (как в очереди, уже обрезанный до 6000 при приёме). /// Структурированный разбор карточки (поля карточки + решение о типе/колонке). diff --git a/src/core/Deal.Contracts/Integrations/Abstractions/ITelegramGateway.cs b/src/core/Deal.Contracts/Integrations/Abstractions/ITelegramGateway.cs index 0f6acb4..23d661b 100644 --- a/src/core/Deal.Contracts/Integrations/Abstractions/ITelegramGateway.cs +++ b/src/core/Deal.Contracts/Integrations/Abstractions/ITelegramGateway.cs @@ -96,7 +96,7 @@ public interface ITelegramGateway /// Последние сообщения диалога для превью, свежие из Telegram. /// /// Id диалога. - /// Сколько последних сообщений (1..50; api-map /dialogs/preview). + /// Сколько последних сообщений. /// Сообщения от новых к старым; признак lead и фолбэк на БД добавляет ядро. public Task> ReadRecentAsync( string dialogId, diff --git a/src/core/Deal.Infrastructure/Data/ConnectionStringProvider.cs b/src/core/Deal.Infrastructure/Data/ConnectionStringProvider.cs index e640c34..2b961c1 100644 --- a/src/core/Deal.Infrastructure/Data/ConnectionStringProvider.cs +++ b/src/core/Deal.Infrastructure/Data/ConnectionStringProvider.cs @@ -15,7 +15,6 @@ public sealed class ConnectionStringProvider { _baseConnectionString = configuration.GetConnectionString("DealPostgres") ?? throw new InvalidOperationException("ConnectionStrings:DealPostgres не задан"); - // Опциональная роль мигратора: задаётся только в проде (см. техдок §10/§13). _migratorConnectionString = configuration.GetConnectionString("DealMigrator"); } diff --git a/src/core/Deal.Infrastructure/Integrations/Services/AiConnectionChecker.cs b/src/core/Deal.Infrastructure/Integrations/Services/AiConnectionChecker.cs index 33b65c1..e48e29c 100644 --- a/src/core/Deal.Infrastructure/Integrations/Services/AiConnectionChecker.cs +++ b/src/core/Deal.Infrastructure/Integrations/Services/AiConnectionChecker.cs @@ -151,11 +151,6 @@ public sealed class AiConnectionChecker : IAiConnectionChecker } } - // request: Запрос проверки (поля статуса провайдера). - // name: Имя провайдера из каталога AiProviders. - // ok: Результат подключения. - // message: Сообщение ветки. - // Возвращает: DTO ответа (наружу — camelCase). private static AiCheckResultDto BuildResult( AiCheckRequest request, string name, @@ -174,8 +169,6 @@ public sealed class AiConnectionChecker : IAiConnectionChecker KeyMasked: MaskKey(request.ApiKey)); } - // key: Ключ открытым текстом. - // Возвращает: Маскированная строка. private static string MaskKey(string key) { if (string.IsNullOrEmpty(key)) @@ -191,10 +184,6 @@ public sealed class AiConnectionChecker : IAiConnectionChecker return string.Concat(key.AsSpan(0, 4), "…", key.AsSpan(key.Length - 4)); } - // baseUrl: Эффективный базовый URL из конфигурации провайдера. - // apiStyle: Стиль API провайдера (null — OpenAI-совместимый). - // modelsUri: URL списка моделей (валиден только при возврате true). - // Возвращает: True — URL построен; False — base URL не абсолютный http(s) (SSRF-гейт). private static bool TryBuildModelsUri( string baseUrl, string? apiStyle, diff --git a/src/core/Deal.Infrastructure/Integrations/Services/AiProviderConfigBuilder.cs b/src/core/Deal.Infrastructure/Integrations/Services/AiProviderConfigBuilder.cs index bfa422a..a85a7de 100644 --- a/src/core/Deal.Infrastructure/Integrations/Services/AiProviderConfigBuilder.cs +++ b/src/core/Deal.Infrastructure/Integrations/Services/AiProviderConfigBuilder.cs @@ -87,8 +87,6 @@ public sealed class AiProviderConfigBuilder return config; } - // ct: Токен отмены. - // Возвращает: Id провайдера (каталога AiProviders). private async Task ReadProviderIdAsync(CancellationToken ct) { SettingValue? row = await _store.GetAsync(SettingsKeys.AiProvider, ct); diff --git a/src/core/Deal.Infrastructure/Integrations/Services/BudgetedAiClassifier.cs b/src/core/Deal.Infrastructure/Integrations/Services/BudgetedAiClassifier.cs index ad0de46..9a3c746 100644 --- a/src/core/Deal.Infrastructure/Integrations/Services/BudgetedAiClassifier.cs +++ b/src/core/Deal.Infrastructure/Integrations/Services/BudgetedAiClassifier.cs @@ -62,7 +62,6 @@ public sealed class BudgetedAiClassifier : IAiClassifier return await _paidClassifier.FilterAsync(text, ct); } - // Запрет гейта — фильтр через Local-реализацию {pass:true, skipped:true} (семантика «фильтр недоступен», _logger.LogDebug( "ИИ-фильтр: {Reason} — Local-пропуск (тенант {TenantId})", GateDeniedLogText, TenantIdForLog()); return await _localClassifier.FilterAsync(text, ct); @@ -76,14 +75,11 @@ public sealed class BudgetedAiClassifier : IAiClassifier return await _paidClassifier.ClassifyAsync(text, ct); } - // без платного ИИ, приём не блокируется. _logger.LogDebug( "ИИ-классификация: {Reason} — Local-разбор (тенант {TenantId})", GateDeniedLogText, TenantIdForLog()); return await _localClassifier.ClassifyAsync(text, ct); } - // ct: Токен отмены. - // Возвращает: True — можно звать платного исполнителя. private async Task IsPaidAllowedAsync(CancellationToken ct) { BudgetStateDto state = await _tenantLimits.GetStateAsync(RequireTenantId(), ct); diff --git a/src/core/Deal.Infrastructure/Integrations/Services/BudgetedAiTools.cs b/src/core/Deal.Infrastructure/Integrations/Services/BudgetedAiTools.cs index 6369794..df30c05 100644 --- a/src/core/Deal.Infrastructure/Integrations/Services/BudgetedAiTools.cs +++ b/src/core/Deal.Infrastructure/Integrations/Services/BudgetedAiTools.cs @@ -90,8 +90,6 @@ public sealed class BudgetedAiTools : IAiTools state.Status == TenantStatuses.Suspended ? SuspendedFitError : ExhaustedFitError); } - // ct: Токен отмены. - // Возвращает: Состояние бюджета тенанта на сейчас. private async Task GateStateAsync(CancellationToken ct) => await _tenantLimits.GetStateAsync(RequireTenantId(), ct); diff --git a/src/core/Deal.Infrastructure/Integrations/Services/CbrRateSource.cs b/src/core/Deal.Infrastructure/Integrations/Services/CbrRateSource.cs index aba1332..4b3bccd 100644 --- a/src/core/Deal.Infrastructure/Integrations/Services/CbrRateSource.cs +++ b/src/core/Deal.Infrastructure/Integrations/Services/CbrRateSource.cs @@ -100,9 +100,6 @@ public sealed class CbrRateSource : IRatesSource return rates; } - // currency: Пара «код валюты → объект {Value, Nominal}». - // rate: Курс единицы валюты к рублю (валиден при возврате true). - // Возвращает: True — запись распознана; False — повреждённая запись (весь fetch — сбой). private static bool TryParseCurrency(JsonProperty currency, out double rate) { rate = 0; diff --git a/src/core/Deal.Infrastructure/Integrations/Services/GrpcAiClassifier.cs b/src/core/Deal.Infrastructure/Integrations/Services/GrpcAiClassifier.cs index e75b33b..e71e6fd 100644 --- a/src/core/Deal.Infrastructure/Integrations/Services/GrpcAiClassifier.cs +++ b/src/core/Deal.Infrastructure/Integrations/Services/GrpcAiClassifier.cs @@ -89,7 +89,6 @@ public sealed class GrpcAiClassifier : IAiClassifier CallOptions(tenantId.Value, ct)); await _usageRecorder.AddAsync(reply.Usage, providerConfig.ProviderId, providerConfig.Model, ct); - // Фильтр применён (воркер звал его только при aiFilterEnabled и не force) — skipped=false return new AiFilterResultDto( Pass: reply.Pass, Reason: reply.HasReason ? reply.Reason : null, @@ -142,7 +141,6 @@ public sealed class GrpcAiClassifier : IAiClassifier await _usageRecorder.AddAsync(reply.Usage, providerConfig.ProviderId, providerConfig.Model, ct); if (!reply.Ok) { - // Модель не вернула разбираемый JSON после ретраев — контрактная ok=false (README ai.proto): _logger.LogWarning("ИИ-классификация: ok=false (тенант {TenantId})", tenantId.Value); throw new AiUnavailableException(NoJsonAnswerText); } @@ -167,27 +165,18 @@ public sealed class GrpcAiClassifier : IAiClassifier ?? throw new InvalidOperationException( "GrpcAiClassifier запрошен вне tenant-контекста (ITenantContext.TenantId == null)."); - // tenantId: Id тенанта (формат N). - // ct: Токен отмены вызова. - // Возвращает: Опции вызова с заголовками, deadline и отменой. private CallOptions CallOptions(string tenantId, CancellationToken ct) => new( headers: _connection.CreateMetadata(tenantId), deadline: DateTime.UtcNow.Add(TimeSpan.FromSeconds(RpcDeadlineSeconds)), cancellationToken: ct); - // секреты/тела ответов не логируются и в текст не попадают). - // exception: Исключение RPC-вызова. - // Возвращает: Текст ошибки. private static string ErrorText(RpcException exception) { string detail = exception.Status.Detail?.Trim() ?? string.Empty; return detail.Length > 0 ? detail : ServiceUnavailableText; } - // text: Строка. - // max: Лимит. - // Возвращает: Усечённая строка. private static string SliceCodePoints(string text, int max) { return text.Length <= max ? text : SliceByCodePoints(text, max); diff --git a/src/core/Deal.Infrastructure/Integrations/Services/GrpcAiTools.cs b/src/core/Deal.Infrastructure/Integrations/Services/GrpcAiTools.cs index e639492..6202dbe 100644 --- a/src/core/Deal.Infrastructure/Integrations/Services/GrpcAiTools.cs +++ b/src/core/Deal.Infrastructure/Integrations/Services/GrpcAiTools.cs @@ -158,27 +158,18 @@ public sealed class GrpcAiTools : IAiTools ?? throw new InvalidOperationException( "GrpcAiTools запрошен вне tenant-контекста (ITenantContext.TenantId == null)."); - // tenantId: Id тенанта (формат N). - // ct: Токен отмены вызова. - // Возвращает: Опции вызова с заголовками, deadline и отменой. private CallOptions CallOptions(string tenantId, CancellationToken ct) => new( headers: _connection.CreateMetadata(tenantId), deadline: DateTime.UtcNow.Add(TimeSpan.FromSeconds(RpcDeadlineSeconds)), cancellationToken: ct); - // секреты/тела ответов не логируются и в текст не попадают). - // exception: Исключение RPC-вызова. - // Возвращает: Текст ошибки. private static string ErrorText(RpcException exception) { string detail = exception.Status.Detail?.Trim() ?? string.Empty; return detail.Length > 0 ? detail : ServiceUnavailableText; } - // text: Строка. - // max: Лимит. - // Возвращает: Усечённая строка. private static string SliceCodePoints(string text, int max) { if (text.Length <= max) diff --git a/src/core/Deal.Infrastructure/Integrations/Services/GrpcMlClient.cs b/src/core/Deal.Infrastructure/Integrations/Services/GrpcMlClient.cs index e85bc9a..020725d 100644 --- a/src/core/Deal.Infrastructure/Integrations/Services/GrpcMlClient.cs +++ b/src/core/Deal.Infrastructure/Integrations/Services/GrpcMlClient.cs @@ -133,7 +133,6 @@ public sealed class GrpcMlClient : IMlClient, IMlTrainClient new PredictRequest { Text = text ?? string.Empty }, CallOptions(tenantId.Value, TimeSpan.FromSeconds(PredictDeadlineSeconds), ct)); - // бюджет/lifetime AI-счётчик не затрагиваются (локальная модель бесплатна). await _usageRecorder.AddEstimatedAsync(text, TokenUsageSources.Local, TokenUsageSources.Ml, ct); return MapPredict(reply); } @@ -246,8 +245,6 @@ public sealed class GrpcMlClient : IMlClient, IMlTrainClient } } - // reply: Ответ ml-service. - // Возвращает: DTO статуса модели. private static MlServiceStatusDto MapStatus(StatusReply reply) { return new MlServiceStatusDto( @@ -260,8 +257,6 @@ public sealed class GrpcMlClient : IMlClient, IMlTrainClient Accuracy: reply.Eval?.Accuracy ?? 0.0)); } - // reply: Ответ ml-service. - // Возвращает: DTO предсказания. private static MlPredictResultDto MapPredict(PredictReply reply) { return new MlPredictResultDto( @@ -289,10 +284,6 @@ public sealed class GrpcMlClient : IMlClient, IMlTrainClient Margin: decision.Margin); } - // tenantId: Id тенанта (формат N). - // deadline: Лимит времени вызова. - // ct: Токен отмены вызова. - // Возвращает: Опции вызова с заголовками, deadline и отменой. private CallOptions CallOptions( string tenantId, TimeSpan deadline, @@ -327,8 +318,6 @@ public sealed class GrpcMlClient : IMlClient, IMlTrainClient Learned: 0, Eval: new MlEvalDto(Count: 0, Correct: 0, Accuracy: 0.0)); - // ct: Токен отмены. - // Возвращает: True, если ключ отсутствует, повреждён или хранит JSON-true. private async Task ReadMlEnabledAsync(CancellationToken ct) { SettingValue? row = await _store.GetAsync(SettingsKeys.MlEnabled, ct); diff --git a/src/core/Deal.Infrastructure/Integrations/Services/GrpcTelegramClient.cs b/src/core/Deal.Infrastructure/Integrations/Services/GrpcTelegramClient.cs index a76c4eb..a5a7de3 100644 --- a/src/core/Deal.Infrastructure/Integrations/Services/GrpcTelegramClient.cs +++ b/src/core/Deal.Infrastructure/Integrations/Services/GrpcTelegramClient.cs @@ -398,11 +398,6 @@ public sealed class GrpcTelegramClient : ITelegramGateway ?? throw new InvalidOperationException( "GrpcTelegramClient запрошен вне tenant-контекста (ITenantContext.TenantId == null)."); - // TReply: Тип ответа RPC. - // tenantId: Id тенанта (формат N). - // ct: Токен отмены вызова. - // call: Вызов клиента (принимает клиент и CallOptions). - // Возвращает: Ответ RPC. private async Task CallAsync( TenantId tenantId, TimeSpan deadline, @@ -418,13 +413,6 @@ public sealed class GrpcTelegramClient : ITelegramGateway return await call(client, options); } - // Нормализует транспортные сбои в RpcException «Telegram не подключён»; RpcException домена — как есть. - // Отмена по токену вызывающего пробрасывается без нормализации (не сбой сервиса). Доменные - // RPC-ошибки (INVALID_ARGUMENT/FAILED_PRECONDITION/…) несут канонический detail — их трогать нельзя: - // exception: Исключение вызова. - // tenantId: Id тенанта (лог). - // operation: Имя RPC (лог-аудит). - // Возвращает: Исключение для проброса: транспортный сбой — нормализованный RpcException. private Exception TranslateTransportFailure( Exception exception, TenantId tenantId, @@ -436,8 +424,6 @@ public sealed class GrpcTelegramClient : ITelegramGateway return exception; } - // Доменная RPC-ошибка сервиса (INVALID_ARGUMENT/FAILED_PRECONDITION/NOT_FOUND…) несёт канонический - // Unavailable с detail (сервис сам ответил причиной) — тоже как есть. if (exception is RpcException rpc && (rpc.StatusCode != StatusCode.Unavailable || !string.IsNullOrEmpty(rpc.Status.Detail))) { @@ -448,8 +434,6 @@ public sealed class GrpcTelegramClient : ITelegramGateway return new RpcException(new Status(StatusCode.Unavailable, NotConnectedDetail)); } - // entries: Записи каталога telegram-service. - // Возвращает: Записи в форме контракта (username → handle). private static IReadOnlyList MapEntries(Google.Protobuf.Collections.RepeatedField entries) { return entries diff --git a/src/core/Deal.Infrastructure/Integrations/Services/LocalColumnSuggester.cs b/src/core/Deal.Infrastructure/Integrations/Services/LocalColumnSuggester.cs index fa9befb..737a7d5 100644 --- a/src/core/Deal.Infrastructure/Integrations/Services/LocalColumnSuggester.cs +++ b/src/core/Deal.Infrastructure/Integrations/Services/LocalColumnSuggester.cs @@ -84,7 +84,6 @@ ContainersService containersService) : IColumnSuggester /// public async Task SuggestKeywordsAsync(CancellationToken ct) { - // свежие 40 (ListCardsAsync(null) = «все, кроме taken», ORDER BY received_at DESC). IReadOnlyList cards = await store.ListCardsAsync(new CardsQuery(null), ct); List texts = cards .Where(card => card.Col != CardIds.Trash @@ -107,12 +106,6 @@ ContainersService containersService) : IColumnSuggester return new SuggestKeywordsResultDto(Ok: true, Keywords: keywords, Reason: null); } - // inbox: Снимок «Неразобранного» (карточки планов берутся из него). - // plans: Планы колонок (SuggestHeuristics.PlanColumns, ≤4). - // ct: Токен отмены. - // Возвращает: Сколько досок реально создано (0 — все откатаны из-за разобранных карточек). - // Каждая доска — suggested=true c правилами {mode:"any", keywords:[тема]} и note-обоснованием. - // Перед раскладкой перечитывается «Неразобранное»: карточки, ушедшие из inbox между снимком и private async Task StoreSuggestedColumnsAsync( IReadOnlyList inbox, IReadOnlyList plans, @@ -176,9 +169,6 @@ ContainersService containersService) : IColumnSuggester return created; } - // Сработал ли кулдаун: с последнего успешного предложения прошло меньше 20 минут. - // ct: Токен отмены. - // Возвращает: True — повторный вызов слишком рано (ответ {ok:false, reason, cooldown:true}). private async Task WithinCooldownAsync(CancellationToken ct) { SettingValue? row = await settings.GetAsync(SettingsKeys.LastSuggestAt, ct); @@ -204,7 +194,6 @@ ContainersService containersService) : IColumnSuggester } } - // ct: Токен отмены. private Task WriteLastSuggestAtAsync(CancellationToken ct) => settings.SetAsync( SettingsKeys.LastSuggestAt, diff --git a/src/core/Deal.Infrastructure/Integrations/Services/LocalMlClient.cs b/src/core/Deal.Infrastructure/Integrations/Services/LocalMlClient.cs index 6ef2d57..cd0ba7d 100644 --- a/src/core/Deal.Infrastructure/Integrations/Services/LocalMlClient.cs +++ b/src/core/Deal.Infrastructure/Integrations/Services/LocalMlClient.cs @@ -75,12 +75,9 @@ public sealed class LocalMlClient(ISettingsStore store, IMlLearningStore learnin double delta, CancellationToken ct) { - // пустые — тихий no-op, text[:6000], id mle_+hex) — в MlOutboxQueue, общем для Local/Grpc-адаптеров. await MlOutboxQueue.PushAsync(learningStore, text, label, delta, ct); } - // ct: Токен отмены. - // Возвращает: True, если ключ отсутствует, повреждён или хранит JSON-true. private async Task ReadMlEnabledAsync(CancellationToken ct) { SettingValue? row = await store.GetAsync(SettingsKeys.MlEnabled, ct); diff --git a/src/core/Deal.Infrastructure/Integrations/Services/MlOutboxQueue.cs b/src/core/Deal.Infrastructure/Integrations/Services/MlOutboxQueue.cs index 4ffa670..0369c00 100644 --- a/src/core/Deal.Infrastructure/Integrations/Services/MlOutboxQueue.cs +++ b/src/core/Deal.Infrastructure/Integrations/Services/MlOutboxQueue.cs @@ -4,8 +4,6 @@ using Deal.Modules.Kanban.Application.Models; namespace Deal.Infrastructure.Integrations.Services; -// тихий no-op, text обрезается до 6000 символов (без разрыва суррогатной пары), id — mle_ + -// и в Local-, и в gRPC-режиме сигнал сначала пишется в outbox, отправку в ml-service делает фоновый internal static class MlOutboxQueue { internal const int MaxLearningTextLength = 6000; @@ -42,14 +40,9 @@ internal static class MlOutboxQueue ct); } - // Возвращает: Короткий id записи очереди. private static string NewOutboxId() => KanbanIdPrefixes.MlOutbox + Convert.ToHexString(RandomNumberGenerator.GetBytes(OutboxIdRandomBytes)).ToLowerInvariant(); - // Обрезает текст до MaxLearningTextLength символов, не разбивая суррогатную пару на конце. - // text: Текст (уже trim-нут). - // Возвращает: Первые 6000 символов (или весь текст, если короче). - // (text[:6000]) режет по code points — хвостовой high-surrogate убираем, чтобы в БД не ушла «битая» пара. private static string TruncateText(string text) { if (text.Length <= MaxLearningTextLength) diff --git a/src/core/Deal.Infrastructure/Integrations/Services/TokenUsageRecorder.cs b/src/core/Deal.Infrastructure/Integrations/Services/TokenUsageRecorder.cs index 5033fa0..9defcba 100644 --- a/src/core/Deal.Infrastructure/Integrations/Services/TokenUsageRecorder.cs +++ b/src/core/Deal.Infrastructure/Integrations/Services/TokenUsageRecorder.cs @@ -76,7 +76,6 @@ public sealed class TokenUsageRecorder } await AddToLifetimeAsync(usage, ct); - // token_usage_events (без tenantId в метках). DealMetrics.RecordAiUsage(usage.Prompt, usage.Completion); await RecordEventAsync( provider, @@ -102,7 +101,6 @@ public sealed class TokenUsageRecorder CancellationToken ct) { long promptTokens = EstimateTokens(text); - // что и событие token_usage_events, kind=ml). DealMetrics.RecordMlUsage(promptTokens); await RecordEventAsync( provider, @@ -170,8 +168,6 @@ public sealed class TokenUsageRecorder return id; } - // usage: Оценка токенов ответа. - // ct: Токен отмены. private async Task AddToLifetimeAsync(Usage usage, CancellationToken ct) { JsonObject? current = await ReadAsync(ct); diff --git a/src/core/Deal.Infrastructure/Integrations/Storage/Services/FileStorageRegistrar.cs b/src/core/Deal.Infrastructure/Integrations/Storage/Services/FileStorageRegistrar.cs index 9f57e1c..8c318bc 100644 --- a/src/core/Deal.Infrastructure/Integrations/Storage/Services/FileStorageRegistrar.cs +++ b/src/core/Deal.Infrastructure/Integrations/Storage/Services/FileStorageRegistrar.cs @@ -38,10 +38,8 @@ public static class FileStorageRegistrar /// /// Регистрирует IFileStorage — LocalFileStorage или MinioFileStorage по конфигурации. /// - /// Коллекция сервисов. /// Конфигурация приложения (секция Storage + env-алиасы DEAL_MINIO_*). /// ContentRoot приложения — база для дефолтного корня data/attachments. - /// Коллекция сервисов для цепочки вызовов. public static IServiceCollection AddDealFileStorage( this IServiceCollection services, IConfiguration configuration, @@ -51,7 +49,6 @@ public static class FileStorageRegistrar StorageOptions options = ReadOptions(configuration); - // недоступен» — dev/curl/unit по умолчанию работают на LocalFileStorage без MinIO). if (options.Minio.IsConfigured()) { services.AddSingleton(serviceProvider => diff --git a/src/core/Deal.Infrastructure/Integrations/Storage/Services/LocalFileStorage.cs b/src/core/Deal.Infrastructure/Integrations/Storage/Services/LocalFileStorage.cs index b5e9afc..3a75e94 100644 --- a/src/core/Deal.Infrastructure/Integrations/Storage/Services/LocalFileStorage.cs +++ b/src/core/Deal.Infrastructure/Integrations/Storage/Services/LocalFileStorage.cs @@ -40,8 +40,6 @@ public sealed class LocalFileStorage : IFileStorage { ArgumentNullException.ThrowIfNull(content); - // может быть прочитан эндпоинтом раньше; перемотаемые потоки сбрасываем (неперемотаемые читаются - // с текущей позиции, как есть). Выравнивание с Minio-адаптером PutAsync. if (content.CanSeek && content.Position != 0) { content.Position = 0; @@ -95,9 +93,6 @@ public sealed class LocalFileStorage : IFileStorage return Task.CompletedTask; } - // objectKey: Ключ объекта (сегменты по '/', без «.»/«..»). - // Возвращает: Полный путь файла под root. - // Исключение ArgumentException: objectKey пуст либо содержит обходные сегменты «.»/«..». private string ResolvePath(string objectKey) { if (string.IsNullOrWhiteSpace(objectKey)) diff --git a/src/core/Deal.Infrastructure/Integrations/Storage/Services/MinioFileStorage.cs b/src/core/Deal.Infrastructure/Integrations/Storage/Services/MinioFileStorage.cs index 6730772..54a3254 100644 --- a/src/core/Deal.Infrastructure/Integrations/Storage/Services/MinioFileStorage.cs +++ b/src/core/Deal.Infrastructure/Integrations/Storage/Services/MinioFileStorage.cs @@ -77,7 +77,6 @@ public sealed class MinioFileStorage : IFileStorage await EnsureBucketAsync(ct); - // (Content-Length), поэтому поток буферизуется — Local-адаптер буферизации не требует. if (content.CanSeek && content.Position != 0) { content.Position = 0; @@ -134,7 +133,6 @@ public sealed class MinioFileStorage : IFileStorage { try { - // → null-семантика порта). Иные ошибки (MinIO недоступен) уходят вызывающему — он мапит их в 404. ObjectStat stat = await _client.StatObjectAsync( new StatObjectArgs().WithBucket(_bucket).WithObject(objectKey), ct); @@ -167,7 +165,6 @@ public sealed class MinioFileStorage : IFileStorage } } - // ct: Токен отмены. private async Task EnsureBucketAsync(CancellationToken ct) { if (_bucketChecked) @@ -193,7 +190,6 @@ public sealed class MinioFileStorage : IFileStorage } catch (MinioException exception) { - // Бакет не проверить/создать (MinIO недоступен и т.п.): put продолжит и упадёт с понятной _logger.LogWarning( exception, "Не удалось проверить/создать бакет MinIO «{Bucket}»: {Message}", diff --git a/src/core/Deal.Infrastructure/Persistence/Configurations/CardConfiguration.cs b/src/core/Deal.Infrastructure/Persistence/Configurations/CardConfiguration.cs index fa572d5..d03e178 100644 --- a/src/core/Deal.Infrastructure/Persistence/Configurations/CardConfiguration.cs +++ b/src/core/Deal.Infrastructure/Persistence/Configurations/CardConfiguration.cs @@ -36,7 +36,6 @@ public sealed class CardConfiguration : IEntityTypeConfiguration // Сортировка пространства «Выбранные» — updated_at DESC. builder.HasIndex(x => x.UpdatedAt).IsDescending(); - // Полнотекстовый вектор карточки (russian): Title+Summary+SourceMsg+Contact — вычисляемая STORED- builder.Property(x => x.SearchTsv) .HasComputedColumnSql( "to_tsvector('russian', coalesce(\"Title\",'')||' '||coalesce(\"Summary\",'')||' '||coalesce(\"SourceMsg\",'')||' '||coalesce(\"Contact\",''))", diff --git a/src/core/Deal.Infrastructure/Persistence/Repositories/AuditLogStore.cs b/src/core/Deal.Infrastructure/Persistence/Repositories/AuditLogStore.cs index 5c4d912..00df77d 100644 --- a/src/core/Deal.Infrastructure/Persistence/Repositories/AuditLogStore.cs +++ b/src/core/Deal.Infrastructure/Persistence/Repositories/AuditLogStore.cs @@ -107,8 +107,6 @@ public sealed class AuditLogStore(DealDbContext dbContext) : IAuditLogStore return query; } - // limit: Запрошенный размер. - // Возвращает: Клампированное значение. private static int ClampLimit(int limit) => Math.Max(1, Math.Min(AuditService.MaxQueryLimit, limit)); private static AuditRecordDto ToAuditRecordDto(AuditLogEntity entity) => diff --git a/src/core/Deal.Infrastructure/Persistence/Repositories/InviteStore.cs b/src/core/Deal.Infrastructure/Persistence/Repositories/InviteStore.cs index 1ab9003..d3857a3 100644 --- a/src/core/Deal.Infrastructure/Persistence/Repositories/InviteStore.cs +++ b/src/core/Deal.Infrastructure/Persistence/Repositories/InviteStore.cs @@ -72,7 +72,6 @@ public sealed class InviteStore(DealDbContext dbContext) : IInviteStore DateTimeOffset activatedAt, CancellationToken ct) { - // иначе параллельный отзыв/активация не перезаписываются (ExecuteUpdate выполняется одним оператором SQL). int affected = await dbContext.Invites .Where(i => i.Code == code && i.Status == InviteStatuses.Pending) .ExecuteUpdateAsync(s => s diff --git a/src/core/Deal.Infrastructure/Persistence/Repositories/KanbanStore.Cards.cs b/src/core/Deal.Infrastructure/Persistence/Repositories/KanbanStore.Cards.cs index 9dde964..e8e66d0 100644 --- a/src/core/Deal.Infrastructure/Persistence/Repositories/KanbanStore.Cards.cs +++ b/src/core/Deal.Infrastructure/Persistence/Repositories/KanbanStore.Cards.cs @@ -15,7 +15,6 @@ public sealed partial class KanbanStore IQueryable queryable = _dbContext.Cards.AsNoTracking(); if (query.Col is null) { - // в работу, живёт в том же пространстве «Выбранных», а не на дашборде. queryable = queryable.Where(card => !SelectedStageIds.Contains(card.Col)); } else @@ -41,9 +40,6 @@ public sealed partial class KanbanStore return Array.Empty(); } - // исключены, порядок — ts_rank DESC, ReceivedAt DESC, limit 12. plainto_tsquery со стоп-словами даёт - // пустой tsquery — совпадений нет (не ошибка), LIKE ниже всё равно отработает. Всё параметризовано - // (никакой конкатенации пользовательского ввода); LIKE-паттерн — как в отсев-поиске PipelineStore. string pattern = $"%{query}%"; List entities = await _dbContext.Cards .FromSqlInterpolated( @@ -133,7 +129,6 @@ public sealed partial class KanbanStore /// public async Task UpdateColumnAsync(CardColumnUpdateDto update, CancellationToken ct) { - // через ExecuteUpdate потребовало бы двух запросов, чтение со слежением проще и точнее. CardEntity? entity = await _dbContext.Cards.SingleOrDefaultAsync(card => card.Id == update.CardId, ct); if (entity is null) { @@ -155,7 +150,6 @@ public sealed partial class KanbanStore /// public async Task ApplyReclassificationAsync(CardReclassificationDto update, CancellationToken ct) { - // валюты и отсутствие конверсии очищают поля (BudgetCur/ConvCur = ""), PrevCol/ArchivedAt не трогаются. CardEntity? entity = await _dbContext.Cards.SingleOrDefaultAsync(card => card.Id == update.CardId, ct); if (entity is null) { @@ -205,7 +199,6 @@ public sealed partial class KanbanStore /// public async Task DeleteForeverAsync(string cardId, CancellationToken ct) { - // «сирота» заблокирует повторное создание карточки при перечитывании канала. await using var transaction = await _dbContext.Database.BeginTransactionAsync(ct); await _dbContext.DedupEntries .Where(entry => entry.LeadId == cardId) @@ -219,7 +212,6 @@ public sealed partial class KanbanStore /// public async Task ClearColAsync(string col, CancellationToken ct) { - // корзины/архива тоже должна снимать «сирот», иначе текст из удалённой карточки не пройдёт дедуп. await using var transaction = await _dbContext.Database.BeginTransactionAsync(ct); await _dbContext.DedupEntries .Where(entry => _dbContext.Cards.Any(card => card.Col == col && card.Id == entry.LeadId)) diff --git a/src/core/Deal.Infrastructure/Persistence/Repositories/KanbanStore.Comments.cs b/src/core/Deal.Infrastructure/Persistence/Repositories/KanbanStore.Comments.cs index 0bca7ea..34179d6 100644 --- a/src/core/Deal.Infrastructure/Persistence/Repositories/KanbanStore.Comments.cs +++ b/src/core/Deal.Infrastructure/Persistence/Repositories/KanbanStore.Comments.cs @@ -30,8 +30,6 @@ public sealed partial class KanbanStore string text, CancellationToken ct) { - // комментарий без карточки не запишется (404-семантику несуществующей карточки отдаёт сервис, - // прочитав карточку перед добавлением, — адаптеру возвращать нечего: порт void). _dbContext.LeadComments.Add(new LeadCommentEntity { Id = commentId, @@ -64,7 +62,6 @@ public sealed partial class KanbanStore /// public async Task> GetAiMarkupExamplesAsync(int limit, CancellationToken ct) { - // действия move/restore, цель не служебная (trash/archive), source_msg непустой, свежие первыми. return await _dbContext.CardMoves .AsNoTracking() .Where(move => move.ToCol != null && move.ToCol != CardIds.Trash && move.ToCol != CardIds.Archive) diff --git a/src/core/Deal.Infrastructure/Persistence/Repositories/KanbanStore.Selected.cs b/src/core/Deal.Infrastructure/Persistence/Repositories/KanbanStore.Selected.cs index 3a1d8eb..cc74042 100644 --- a/src/core/Deal.Infrastructure/Persistence/Repositories/KanbanStore.Selected.cs +++ b/src/core/Deal.Infrastructure/Persistence/Repositories/KanbanStore.Selected.cs @@ -34,7 +34,6 @@ public sealed partial class KanbanStore CardPatch patch, CancellationToken ct) { - // Точечная правка по присутствующим полям патча: null-поле не меняется, JSON-поля заменяются CardEntity? entity = await _dbContext.Cards .SingleOrDefaultAsync(card => card.Id == cardId, ct); if (entity is null) @@ -152,7 +151,6 @@ public sealed partial class KanbanStore long atMs, CancellationToken ct) { - // Смена контейнера-стадии: Col=containerId, сброс напоминания, updated_at=atMs, история + запись CardEntity? entity = await _dbContext.Cards .AsNoTracking() .SingleOrDefaultAsync(card => card.Id == cardId, ct); @@ -206,7 +204,6 @@ public sealed partial class KanbanStore /// public async Task ClearStageAsync(string containerId, CancellationToken ct) { - // снимает каскад FK LeadComments → Cards. return await _dbContext.Cards .Where(card => card.Col == containerId) .ExecuteDeleteAsync(ct); diff --git a/src/core/Deal.Infrastructure/Persistence/Repositories/KanbanStore.StorageRules.cs b/src/core/Deal.Infrastructure/Persistence/Repositories/KanbanStore.StorageRules.cs index a2b182d..71851ba 100644 --- a/src/core/Deal.Infrastructure/Persistence/Repositories/KanbanStore.StorageRules.cs +++ b/src/core/Deal.Infrastructure/Persistence/Repositories/KanbanStore.StorageRules.cs @@ -13,7 +13,6 @@ public sealed partial class KanbanStore /// public async Task> ListArchiveCandidatesAsync(DateTimeOffset receivedBeforeUtc, CancellationToken ct) { - // Кандидаты автоархива: карточки пользовательских колонок (kind=board) и «Неразобранного» List boardIds = await _dbContext.Containers .AsNoTracking() .Where(container => container.Kind == ContainerKinds.Board) @@ -34,7 +33,6 @@ public sealed partial class KanbanStore DateTimeOffset archivedAt, CancellationToken ct) { - // UpdateColumnAsync автоархива: col=archive, is_new=false, archived_at=now, matchHits пусто; if (cardIds.Count == 0) { return 0; @@ -94,7 +92,6 @@ public sealed partial class KanbanStore /// public async Task> ListCardsForConversionAsync(CancellationToken ct) { - // пересчёт читает только бюджетные/conv-поля, комментарии не нужны. List entities = await _dbContext.Cards .AsNoTracking() .Where(card => card.BudgetCur != string.Empty diff --git a/src/core/Deal.Infrastructure/Persistence/Repositories/KanbanStore.cs b/src/core/Deal.Infrastructure/Persistence/Repositories/KanbanStore.cs index de6fcfa..dc99096 100644 --- a/src/core/Deal.Infrastructure/Persistence/Repositories/KanbanStore.cs +++ b/src/core/Deal.Infrastructure/Persistence/Repositories/KanbanStore.cs @@ -204,7 +204,6 @@ public sealed partial class KanbanStore : ICardStore private static CardCommentDto ToCommentDto(LeadCommentEntity entity) => new(entity.Id, entity.By, entity.Text, HumanAge(entity.CreatedAt)); - // timestamp: Время события (получения сообщения/добавления комментария). private static string HumanAge(DateTimeOffset timestamp) { long deltaMs = Math.Max(0, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - timestamp.ToUnixTimeMilliseconds()); diff --git a/src/core/Deal.Infrastructure/Persistence/Repositories/PipelineStore.cs b/src/core/Deal.Infrastructure/Persistence/Repositories/PipelineStore.cs index c6e95ee..b6f11f3 100644 --- a/src/core/Deal.Infrastructure/Persistence/Repositories/PipelineStore.cs +++ b/src/core/Deal.Infrastructure/Persistence/Repositories/PipelineStore.cs @@ -90,8 +90,6 @@ public sealed class PipelineStore(TenantDbContext dbContext) : IPipelineStore string id = record.DeterministicId ?? PrefixId.New(PipelineIdPrefixes.Rejected); DateTimeOffset rejectedAt = DateTimeOffset.UtcNow; - // Upsert по детерминированному id: повторное отбрасывание того же сообщения (dialog+msgId) - // аудит возврата (returned/returnedAt/returnReason) при повторном отсеве переживает. await dbContext.Database.ExecuteSqlInterpolatedAsync( $""" INSERT INTO "RejectedItems" ("Id", "DialogId", "MsgId", "ChannelName", "ChannelHandle", "ChannelHue", "Text", "Stage", "Reason", "Kw", "Source", "MsgAt", "RejectedAt", "Returned", "ReturnReason") @@ -139,7 +137,6 @@ public sealed class PipelineStore(TenantDbContext dbContext) : IPipelineStore return Array.Empty(); } - // plainto_tsquery со стоп-словами даёт пустой tsquery — совпадений нет (не ошибка), LIKE ниже всё List fts = await dbContext.RejectedItems .FromSqlInterpolated( $""" @@ -154,7 +151,6 @@ public sealed class PipelineStore(TenantDbContext dbContext) : IPipelineStore var rows = fts.Select(ToRejectedItemDto).ToList(); var seen = new HashSet(fts.Select(item => item.Id), StringComparer.Ordinal); - // последнего перестроения вектора и слова, которых нет в словаре морфологии. limitLike — как string pattern = $"%{query}%"; List like = await dbContext.RejectedItems .AsNoTracking() @@ -236,8 +232,6 @@ public sealed class PipelineStore(TenantDbContext dbContext) : IPipelineStore /// public async Task ClaimAsync(string hash, CancellationToken ct) { - // NOTHING — параллельные дубли одного текста не проходят (сырой SQL: EF-вставка упала бы на PK). - // Число вставленных строк — результат: 1 = заявка за этим вызовом, 0 = хэш уже заявлен другим int inserted = await dbContext.Database.ExecuteSqlInterpolatedAsync( $""" INSERT INTO "DedupEntries" ("Hash", "LeadId", "CreatedAt") @@ -251,7 +245,6 @@ public sealed class PipelineStore(TenantDbContext dbContext) : IPipelineStore /// public async Task DeleteClaimAsync(string hash, CancellationToken ct) { - // (LeadId IS NOT NULL), не трогаем — иначе повторный текст прошёл бы дедуп после отсева. await dbContext.DedupEntries .Where(entry => entry.Hash == hash && entry.LeadId == null) .ExecuteDeleteAsync(ct); @@ -271,8 +264,6 @@ public sealed class PipelineStore(TenantDbContext dbContext) : IPipelineStore /// public async Task DeleteByCardAsync(string cardId, CancellationToken ct) { - // дублируется тем же DELETE (см. KanbanStore.DeleteForeverAsync/PurgeAsync/ClearColAsync) — - // чтобы Kanban-адаптер не зависел от порта Pipeline; этот метод — для потребителей модуля. await dbContext.DedupEntries .Where(entry => entry.LeadId == cardId) .ExecuteDeleteAsync(ct); diff --git a/src/core/Deal.Infrastructure/Persistence/Repositories/TelegramStore.cs b/src/core/Deal.Infrastructure/Persistence/Repositories/TelegramStore.cs index b5ec200..617088f 100644 --- a/src/core/Deal.Infrastructure/Persistence/Repositories/TelegramStore.cs +++ b/src/core/Deal.Infrastructure/Persistence/Repositories/TelegramStore.cs @@ -146,8 +146,6 @@ public sealed class TelegramStore(TenantDbContext dbContext) : ITelegramStore string hue, CancellationToken ct) { - // discovery-вступления — метаданные источника, monitor=TRUE, backfilled=FALSE (разбор подхватит - // первый фоновый Backfill). Значения нормализованы вызывающим (DialogsService). DialogEntity? existing = await dbContext.Dialogs.FirstOrDefaultAsync(dialog => dialog.Id == dialogId, ct); DateTimeOffset now = DateTimeOffset.UtcNow; if (existing is null) @@ -234,8 +232,6 @@ public sealed class TelegramStore(TenantDbContext dbContext) : ITelegramStore row.Id, row.Text, row.MsgAt.ToUnixTimeMilliseconds(), row.LeadId is not null)).ToList(); } - // row: Строка Dialogs. - // Возвращает: Форма §4.8: type = kind (EN-канон), on = monitor, last {text, time}. private static TelegramDialogDto ToDialogDto(DialogEntity row) { long? lastAtMs = row.LastAt?.ToUnixTimeMilliseconds(); diff --git a/src/core/Deal.Infrastructure/Persistence/Repositories/TenantLimitStore.cs b/src/core/Deal.Infrastructure/Persistence/Repositories/TenantLimitStore.cs index 5768446..7da6eeb 100644 --- a/src/core/Deal.Infrastructure/Persistence/Repositories/TenantLimitStore.cs +++ b/src/core/Deal.Infrastructure/Persistence/Repositories/TenantLimitStore.cs @@ -164,10 +164,6 @@ public sealed class TenantLimitStore : ITenantLimitStore return true; } - // tenantId: Тенант. - // defaults: Дефолт-параметры создаваемой строки. - // ct: Токен отмены. - // Возвращает: Отслеживаемая строка лимита. private async Task LoadOrCreateAsync( Guid tenantId, TokenLimitDefaults defaults, @@ -196,8 +192,6 @@ public sealed class TenantLimitStore : ITenantLimitStore return created; } - // entity: Отслеживаемая строка лимита. - // ct: Токен отмены. private async Task ResetIfPeriodExpiredAsync(TenantLimitEntity entity, CancellationToken ct) { if (ResetIfExpired(entity, _utcNow())) @@ -249,12 +243,8 @@ public sealed class TenantLimitStore : ITenantLimitStore return reset; } - // entity: Строка лимита (после ленивого reset). - // ct: Токен отмены. - // Возвращает: Состояние бюджета тенанта. private async Task ToStateDtoAsync(TenantLimitEntity entity, CancellationToken ct) { - // Статус тенанта — из public.tenants тем же контекстом; отсутствие строки трактуем suspended string? tenantStatus = await _dbContext.Tenants .AsNoTracking() .Where(t => t.Id == entity.TenantId) diff --git a/src/core/Deal.Infrastructure/ServiceCollectionExtensions.cs b/src/core/Deal.Infrastructure/ServiceCollectionExtensions.cs index 475c2d4..831daeb 100644 --- a/src/core/Deal.Infrastructure/ServiceCollectionExtensions.cs +++ b/src/core/Deal.Infrastructure/ServiceCollectionExtensions.cs @@ -30,62 +30,44 @@ public static class ServiceCollectionExtensions /// /// Регистрирует EF-адаптеры портов модулей /// - /// Коллекция сервисов. /// Дефолт-бюджет лениво создаваемых строк tenant_limits. Передаётся из конфигурации/env DEAL_DEFAULT_AI_BUDGET в Program.cs. - /// Коллекция сервисов для цепочки вызовов. public static IServiceCollection AddDealPersistence(this IServiceCollection services, TokenLimitDefaults? tenantLimitDefaults = null) { services.AddScoped(); - // схемы public — отдельный порт от IAuthStore (оператор ≠ пользователь тенанта). services.AddScoped(); services.AddScoped(); services.AddScoped(); - // + агрегаты для аналитики; порт без Update/Delete. services.AddScoped(); services.AddScoped(); - // дефолт-бюджет из конфигурации (Program.cs) — ленивый GetOrCreate новой строки использует его; - // DealDbContext уже зарегистрирован в Api (AddDbContext до AddDealPersistence). services.AddScoped(provider => new TenantLimitStore( provider.GetRequiredService(), tenantLimitDefaults ?? TokenBudgetDefaults.Default)); - // общее хранилище распределённого rate limiting (auth/api/gRPC) и учёта попыток входа. Scoped - // (DealDbContext); лимитер резолвит его в собственном scope на каждое приобретение. services.AddScoped(); - // контекст — scoped TenantDbContext запроса (см. AddDbContext в Deal.Api). services.AddScoped(); - // KV-хранилище глобальных (системных) настроек оператора (ТЗ §4.1/§8.1): таблица - // public.global_settings, контекст — системный DealDbContext (public-схема). Ключи Telegram - // задаёт оператор, ядро читает их для команд входа (TelegramKeysService). services.AddScoped(); services.AddScoped(); - // KanbanStore при жёстком удалении карточки чистит DedupEntries напрямую тем же TenantDbContext services.AddScoped(); services.AddScoped(); services.AddScoped(); - // тенанта. Проверки «уже мониторится» читают таблицу Dialogs (владелец — Telegram) тем же TenantDbContext. services.AddScoped(); - // ConnectionStringProvider — он регистрируется в Deal.Api (Program.cs) как singleton. services.AddScoped(); - // параллелизм + логирование прогресса поверх ITenantRepository и ITenantProvisioner. services.AddScoped(); - // Выбранных vs дашборд-контейнер» живёт в адаптере, а не в эндпоинте. Scoped — - // композирует scoped-сервис карточек в рамках tenant-запроса. services.AddScoped(); return services; } @@ -93,11 +75,9 @@ public static class ServiceCollectionExtensions /// /// Регистрирует адаптеры внешних интеграций /// - /// Коллекция сервисов. /// Конфигурация секции Services:Ml — выбор реализации IMlClient. /// Конфигурация секции Services:Ai — выбор реализации IAiClassifier/IAiTools. /// Конфигурация секции Services:Telegram — выбор реализации ITelegramGateway. - /// Коллекция сервисов для цепочки вызовов. public static IServiceCollection AddDealIntegrations( this IServiceCollection services, MlServiceOptions mlOptions, @@ -105,9 +85,6 @@ public static class ServiceCollectionExtensions TelegramServiceOptions telegramOptions, MtlsCertificates? mtlsCertificates = null) { - // остаются plaintext + service-token (dev); при включённом флаге каждый транспорт подписывает запрос - // клиентским сертификатом и проверяет CA сервера (fail-fast на загрузку — в MtlsCertificates.Load). - // событий. Регистрируется независимо от AI-режима — им пользуется и gRPC ML-клиент (событие kind=ml). services.AddScoped(); if (mlOptions.UseLocal) @@ -116,8 +93,6 @@ public static class ServiceCollectionExtensions } else { - // gRPC-клиент ml-service: транспорт создаётся сразу (fail-fast: пустой endpoint/токен останавливают - // KV/таблицы тенанта (как LocalMlClient) и ходит в сервис по metadata tenant-id/service-token. services.AddSingleton(new MlGrpcConnection(mlOptions, mtlsCertificates)); services.AddSingleton(); services.AddScoped(); @@ -125,18 +100,8 @@ public static class ServiceCollectionExtensions services.AddScoped(provider => provider.GetRequiredService()); } - // LocalColumnSuggester читает карточки через ICardStore, считает группы ядром SuggestHeuristics - // (модуль Kanban) и создаёт доски suggested=true через ContainersService; scoped — его зависимости - // gRPC-клиентом ai-service с тем же контрактом. services.AddScoped(); - // (UseLocal=true) либо gRPC-адаптер ai-service (UseLocal=false). LocalAiClassifier разбирает сообщение - // ядром LocalFieldsParser модуля Pipeline (маркерная гипотеза типа: is_vacancy_known=false, board=null — - // «смысловые колонки до ИИ не назначаем») и всегда пропускает ИИ-фильтр {pass:true, skipped:true}; - // scoped — LocalFieldsParser читает маркеры hireMarkers/levelTerms из KV-настроек тенанта (ISettingsStore). - // GrpcAiClassifier строит промпты/контекст классификации (AiClassifyContextBuilder модуля Pipeline, - // регистрируется AddPipelineModule), ходит в ai-service с ProviderConfig из настроек (расшифровка apiKey) - // и копит usage в KV aiTokenUsage; недоступность/ok=false → AiUnavailableException — воркер падает в if (aiOptions.UseLocal) { services.AddScoped(); @@ -144,16 +109,10 @@ public static class ServiceCollectionExtensions } else { - // gRPC-клиент ai-service: транспорт создаётся сразу (fail-fast: пустой endpoint/токен останавливают - // в сервис по metadata tenant-id/service-token; usage ответов списывает TokenUsageRecorder services.AddSingleton(new AiGrpcConnection(aiOptions, mtlsCertificates)); services.AddScoped(); services.AddScoped(); - // исчерпанном бюджете/приостановке уводит вызов на детерминированный бесплатный локальный разбор - // (LocalAiClassifier; LocalFieldsParser регистрирует AddPipelineModule в Program.cs). Инструменты локального services.AddScoped(); - // Декораторы бюджетного гейта (порядок Grpc → Budgeted → наружу): перед каждым платным вызовом - // GetStateAsync (ITenantLimitStore, scoped) — исчерпано/приостановлено → Local-классификатор либо services.AddScoped(provider => new BudgetedAiClassifier( provider.GetRequiredService(), provider.GetRequiredService(), @@ -168,10 +127,6 @@ public static class ServiceCollectionExtensions provider.GetRequiredService>())); } - // локальная заглушка dev LocalTelegramGateway (нейтральный no-op/idle — сервис не поднят); при - // UseLocal=false — gRPC-клиент GrpcTelegramClient (telegram.proto) + синглтон-транспорт - // TelegramGrpcConnection (fail-fast, как MlGrpcConnection). Scoped-адаптер: tenant-id для metadata - // берёт из ITenantContext (AsyncLocal) scope вызова (HTTP-запрос/tenant-циклы ингресса). if (telegramOptions.UseLocal) { services.AddSingleton(); @@ -189,9 +144,7 @@ public static class ServiceCollectionExtensions /// /// Регистрирует сервисы шифрования секретов /// - /// Коллекция сервисов. /// ContentRoot приложения — каталог по умолчанию для файла-ключа data/encryption.key. - /// Коллекция сервисов для цепочки вызовов. public static IServiceCollection AddDealSecurity(this IServiceCollection services, string contentRootPath) { EncryptionKeyProvider keyProvider = new(contentRootPath); diff --git a/src/core/Deal.Infrastructure/Services/CardMover.cs b/src/core/Deal.Infrastructure/Services/CardMover.cs index c5c1a7b..5739095 100644 --- a/src/core/Deal.Infrastructure/Services/CardMover.cs +++ b/src/core/Deal.Infrastructure/Services/CardMover.cs @@ -19,8 +19,6 @@ public sealed class CardMover(CardsService cardsService) : ICardMover TransitionContext ctx, CancellationToken ct) { - // Контекст перехода (инициатор/обучение) учтён внутри маршрутов: дашборд-перенос обучает ML по - // цели пользователя, переход по стадии пишет историю и сбрасывает напоминание. Отдельного CardResultDto result = CardsDefaultContainers.Contains(toContainerId) ? await cardsService.MoveStageCardAsync(cardId, toContainerId, ct) : await cardsService.MoveDashboardCardAsync(cardId, toContainerId, ct); diff --git a/src/core/Deal.Modules.Cards/Application/Abstractions/IContainerRules.cs b/src/core/Deal.Modules.Cards/Application/Abstractions/IContainerRules.cs index ac5abf0..b4c7ebb 100644 --- a/src/core/Deal.Modules.Cards/Application/Abstractions/IContainerRules.cs +++ b/src/core/Deal.Modules.Cards/Application/Abstractions/IContainerRules.cs @@ -33,7 +33,7 @@ public interface IContainerRules public IReadOnlyList Grades { get; } /// - /// Уровень (отдельная группа §6.3; пусто — не участвует). + /// Уровень. /// public IReadOnlyList Levels { get; } @@ -53,7 +53,7 @@ public interface IContainerRules public CardBudget? Budget { get; } /// - /// Диапазон цены + /// Диапазон цены. /// public CardBudget? Prices { get; } diff --git a/src/core/Deal.Modules.Cards/Application/Abstractions/ITzCard.cs b/src/core/Deal.Modules.Cards/Application/Abstractions/ITzCard.cs index 32948fd..ecd8dc6 100644 --- a/src/core/Deal.Modules.Cards/Application/Abstractions/ITzCard.cs +++ b/src/core/Deal.Modules.Cards/Application/Abstractions/ITzCard.cs @@ -1,12 +1,12 @@ namespace Deal.Modules.Cards.Application.Abstractions; /// -/// Модуль «ТЗ»: текст технического задания/заметки-задания по карточке. +/// Модуль « »: текст технического задания/заметки-задания по карточке. /// public interface ITzCard { /// - /// Текст ТЗ (пусто — ТЗ нет). + /// Текст. /// public string TzText { get; } } diff --git a/src/core/Deal.Modules.Cards/Application/Models/Card.cs b/src/core/Deal.Modules.Cards/Application/Models/Card.cs index 33881ea..6ed171e 100644 --- a/src/core/Deal.Modules.Cards/Application/Models/Card.cs +++ b/src/core/Deal.Modules.Cards/Application/Models/Card.cs @@ -66,7 +66,6 @@ public sealed class Card : /// public IReadOnlyList Files { get; init; } = Array.Empty(); - // ── Модуль «ТЗ» ── /// public string TzText { get; init; } = string.Empty; diff --git a/src/core/Deal.Modules.Discovery/Application/Registrars/DiscoveryModuleRegistrar.cs b/src/core/Deal.Modules.Discovery/Application/Registrars/DiscoveryModuleRegistrar.cs index 09468ce..1da58be 100644 --- a/src/core/Deal.Modules.Discovery/Application/Registrars/DiscoveryModuleRegistrar.cs +++ b/src/core/Deal.Modules.Discovery/Application/Registrars/DiscoveryModuleRegistrar.cs @@ -13,8 +13,6 @@ public static class DiscoveryModuleRegistrar /// /// Регистрирует сервисы модуля Discovery в контейнере. /// - /// Коллекция сервисов. - /// Коллекция сервисов для цепочки вызовов. public static IServiceCollection AddDiscoveryModule(this IServiceCollection services) { services.AddScoped(); @@ -23,8 +21,6 @@ public static class DiscoveryModuleRegistrar services.AddScoped(); services.AddScoped(); - // (DiscoveryWorkerScheduler); IDiscoverySearchErrorCounter — singleton (счётчик ошибок ключей живёт - // чтобы не резолвить опциональный параметр utcNow (дефолт — DateTimeOffset.UtcNow). services.AddSingleton(); services.AddSingleton(sp => sp.GetRequiredService()); services.AddScoped(); diff --git a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryBanGuard.cs b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryBanGuard.cs index b778575..f4e4ae5 100644 --- a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryBanGuard.cs +++ b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryBanGuard.cs @@ -90,9 +90,6 @@ public sealed class DiscoveryBanGuard private long StartOfDayMs() => StartOfDayUtc().ToUnixTimeMilliseconds(); - // floodDay: Epoch-ms начала суток flood-блокировки (0 — не было). - // startOfDayMs: Начало текущих UTC-суток в epoch-ms. - // Возвращает: True — flood фиксировался сегодня. private static bool IsFloodToday(long floodDay, long startOfDayMs) => floodDay != 0 && floodDay == startOfDayMs; } diff --git a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryCandidatesService.cs b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryCandidatesService.cs index ddec8df..2629914 100644 --- a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryCandidatesService.cs +++ b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryCandidatesService.cs @@ -81,7 +81,6 @@ public sealed class DiscoveryCandidatesService( DiscoveryTaskDto? task = await store.GetTaskAsync(taskId, ct).ConfigureAwait(false); if (task is null) { - // для существующих running-задач; строки нет → источник просто не добавляется). return null; } @@ -274,8 +273,6 @@ public sealed class DiscoveryCandidatesService( return store.DeleteCandidateAsync(dialogId, ct); } - // patch: Патч из запроса. - // Возвращает: Патч с нормализованными значениями (null-поля — «не менять»). private static DiscoveryCandidatePatch NormalizePatch(DiscoveryCandidatePatch patch, DiscoveryCandidateDto current) { return new DiscoveryCandidatePatch @@ -293,9 +290,6 @@ public sealed class DiscoveryCandidatesService( }; } - // value: Значение патча (null — не менялось). - // current: Текущее значение строки. - // Возвращает: Обрезанное значение или null (поле не меняется). private static string? NormalizeKeptOr(string? value, string current) { if (value is null) diff --git a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryTasksService.cs b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryTasksService.cs index 75ad431..ca634a6 100644 --- a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryTasksService.cs +++ b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryTasksService.cs @@ -225,8 +225,6 @@ public sealed class DiscoveryTasksService(IDiscoveryStore store, DiscoveryPlanGu return await store.AdvanceSearchAsync(taskId, nextIndex, searchDone, ct).ConfigureAwait(false); } - // patch: Патч из запроса. - // Возвращает: Патч с нормализованными значениями (null-поля сохраняются как «не менять»). private static DiscoveryTaskPatch NormalizeTaskPatch(DiscoveryTaskPatch patch) { int? threshold = patch.Threshold is int value ? Clamp(value, min: 1, max: 100) : null; @@ -249,8 +247,6 @@ public sealed class DiscoveryTasksService(IDiscoveryStore store, DiscoveryPlanGu }; } - // keywords: Сырые ключи (null — пусто). - // Возвращает: Список непустых ключей. private static IReadOnlyList CleanKeywords(IReadOnlyList? keywords) { return keywords?.Where(keyword => !string.IsNullOrWhiteSpace(keyword)) diff --git a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Constants.cs b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Constants.cs index f5784e0..8ad1bdd 100644 --- a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Constants.cs +++ b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Constants.cs @@ -1,6 +1,5 @@ namespace Deal.Modules.Discovery.Application.Services; -// Часть DiscoveryWorkerService: константы — действия тика, пороги/размеры воркера и метки кандидатов public sealed partial class DiscoveryWorkerService { /// diff --git a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Evaluate.cs b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Evaluate.cs index 260df3c..4a80fe9 100644 --- a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Evaluate.cs +++ b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Evaluate.cs @@ -3,13 +3,8 @@ using Deal.Modules.Discovery.Application.Models; namespace Deal.Modules.Discovery.Application.Services; -// Часть DiscoveryWorkerService: шаг оценки кандидата status='new' — инфо источника, выборка, язык, объём public sealed partial class DiscoveryWorkerService { - // task: Running-задача кандидата. - // candidate: Самый старый кандидат статуса new. - // ct: Токен отмены. - // Возвращает: review — кандидат оценён; skip — пропущен (delete); error — сбой шага. private async Task EvalStepAsync( DiscoveryTaskDto task, DiscoveryCandidateDto candidate, @@ -46,7 +41,6 @@ public sealed partial class DiscoveryWorkerService }; if (resolved) { - // Имя/username обновляем только при успешном резолве: при fallback discovery_info возвращает patch = patch with { Name = string.IsNullOrWhiteSpace(info.Name) ? null : info.Name.Trim(), @@ -195,13 +189,6 @@ public sealed partial class DiscoveryWorkerService return new DiscoveryWorkerOutcome(ActionSkip, taskId); } - // taskId: Id задачи. - // dialogId: Id источника. - // marks: Метки (пустые отфильтровываются). - // langRu: Подтверждённый язык (null — не менять). - // fitRatio: Доля подходящих (null — контент не оценён). - // topics: Темы форума (null — не менять; для не-форумов не заполняется). - // ct: Токен отмены. private async Task FinishReviewAsync( string taskId, string dialogId, diff --git a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Helpers.cs b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Helpers.cs index 9ec51fd..0fbbc0e 100644 --- a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Helpers.cs +++ b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Helpers.cs @@ -7,12 +7,6 @@ namespace Deal.Modules.Discovery.Application.Services; // источника (KindCode) и тексты сообщений для оценки (GroupTexts). public sealed partial class DiscoveryWorkerService { - // Flood-ли это исключение гейта: RpcException RESOURCE_EXHAUSTED c detail-префиксом «flood:» - // текст исключения: RpcException.Message кодирует Status как - // Status(StatusCode="ResourceExhausted", Detail="flood: FLOOD_WAIT_…") — ищем маркер «flood:» - // (префикс detail, которым сервис помечает FloodWait; обычные ошибки его не несут). - // exception: Исключение вызова гейта. - // Возвращает: True — FloodWait Telegram (стоп до конца суток, note_flood). internal static bool IsFlood(Exception exception) { string message = exception.Message ?? string.Empty; @@ -21,9 +15,6 @@ public sealed partial class DiscoveryWorkerService || message.Contains("flood:", StringComparison.OrdinalIgnoreCase); } - // kind: Kind из гейта (EN-канон) либо русская форма каталога. - // isForum: True — мегагруппа-форум (темы) → forum. - // Возвращает: Код кандидата: channel/group/forum (неизвестное → group). private static string KindCode(string kind, bool isForum = false) { if (isForum) diff --git a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Join.cs b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Join.cs index 16b1606..b52f8b7 100644 --- a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Join.cs +++ b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Join.cs @@ -3,13 +3,8 @@ using Deal.Modules.Discovery.Application.Models; namespace Deal.Modules.Discovery.Application.Services; -// Часть DiscoveryWorkerService: шаг авто-вступления кандидата status='review' — повторные проверки, public sealed partial class DiscoveryWorkerService { - // task: Running-задача с autoJoin. - // candidate: Самый старый кандидат статуса review. - // ct: Токен отмены. - // Возвращает: join/reject/flood/error/skip — действие; none — выход без вступления (изменения за паузу). private async Task JoinStepAsync( DiscoveryTaskDto task, DiscoveryCandidateDto candidate, @@ -37,8 +32,6 @@ public sealed partial class DiscoveryWorkerService await _pacer.WaitJoinDelayAsync(ct).ConfigureAwait(false); - // За время паузы задача/кандидат/состояние BanGuard могли измениться: вступаем только если кандидат - // всё ещё есть и в review, задача ещё running с autoJoin, мы не состоим и авто-вступления разрешены DiscoveryTaskDto? taskNow = await _tasks.GetAsync(taskId, ct).ConfigureAwait(false); DiscoveryCandidateDto? fresh = await _store.GetCandidateAsync(dialogId, ct).ConfigureAwait(false); if (fresh is null @@ -71,7 +64,6 @@ public sealed partial class DiscoveryWorkerService return new DiscoveryWorkerOutcome(ActionFlood, taskId); } - // Между паузой и неудачным join кандидата могли отклонить/удалить: счётчик и удаление трогаем DiscoveryCandidateDto? rowNow = await _store.GetCandidateAsync(dialogId, ct).ConfigureAwait(false); if (rowNow is null || rowNow.Status != DiscoveryCandidateStatuses.Review) { @@ -100,7 +92,6 @@ public sealed partial class DiscoveryWorkerService return new DiscoveryWorkerOutcome(ActionError, taskId); } - // Вступление состоялось: joined(auto) + каталог/зеркало (монитор on) + фоновый разбор последних await _candidates.MarkJoinedAsync(dialogId, auto: true, ct).ConfigureAwait(false); try @@ -129,7 +120,6 @@ public sealed partial class DiscoveryWorkerService } catch (Exception exception) { - // но фиксируется в DiscLog, иначе «немое» глотание скрывает регулярные сбои. await _log.AddAsync(taskId, DiscoveryLogEvents.Error, $"фоновый разбор {dialogId}: {exception.Message}", ct) .ConfigureAwait(false); } diff --git a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Search.cs b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Search.cs index bc3a26d..39d3572 100644 --- a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Search.cs +++ b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Search.cs @@ -3,12 +3,8 @@ using Deal.Modules.Discovery.Application.Models; namespace Deal.Modules.Discovery.Application.Services; -// Часть DiscoveryWorkerService: шаг поиска — один ключ keywords[SearchIdx] → кандидаты + advance_search public sealed partial class DiscoveryWorkerService { - // task: Running-задача с незавершённым проходом. - // ct: Токен отмены. - // Возвращает: search — ключ обработан; flood/error — сбой (индекс ключа/счётчик ошибок по сценарию). private async Task SearchStepAsync(DiscoveryTaskDto task, CancellationToken ct) { string taskId = task.Id; @@ -40,7 +36,6 @@ public sealed partial class DiscoveryWorkerService int errors = _searchErrors.Next(taskId); if (errors >= SearchErrorsToSkip) { - // 3 ошибки подряд одного ключа: пропускаем (битый ключ не должен зацикливать поиск и _searchErrors.Reset(taskId); await _log.AddAsync( taskId, diff --git a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.cs b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.cs index 50ca516..1648bec 100644 --- a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.cs +++ b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.cs @@ -81,7 +81,6 @@ public sealed partial class DiscoveryWorkerService return DiscoveryWorkerOutcome.None; } - // 1. План достигнут — закрываем задачу (важно до поиска/оценки/join: задачу с выполненным планом foreach (DiscoveryTaskDto task in running) { if (task.Joined >= task.PlanJoins) diff --git a/src/core/Deal.Modules.Kanban/Application/ColumnRules/AmountParser.cs b/src/core/Deal.Modules.Kanban/Application/ColumnRules/AmountParser.cs index 48bffe0..59e5263 100644 --- a/src/core/Deal.Modules.Kanban/Application/ColumnRules/AmountParser.cs +++ b/src/core/Deal.Modules.Kanban/Application/ColumnRules/AmountParser.cs @@ -138,9 +138,6 @@ public static class AmountParser return result; } - // Число из строки суммы: пробелы и неразрывные пробелы убираем, «,» — десятичный разделитель, - // raw: Сырая цифровая часть из regex (может содержать «к»/«К» на конце). - // Возвращает: Значение суммы или null при ошибке парсинга. private static double? NormalizeAmount(string raw) { string s = raw.Replace("\u00a0", " ").Replace(" ", string.Empty).Replace(",", "."); @@ -160,10 +157,6 @@ public static class AmountParser : null; } - // Код валюты рядом с суммой: символ/слово сразу после позиции либо символ за ≤3 символа до неё - // text: Весь текст сообщения. - // position: Позиция сразу после конца суммы (m.end). - // Возвращает: Код валюты или null, если валюты рядом нет. private static string? CurFromTail(string text, int position) { int tailLength = Math.Min(12, text.Length - position); diff --git a/src/core/Deal.Modules.Kanban/Application/ColumnRules/ColumnMatcher.cs b/src/core/Deal.Modules.Kanban/Application/ColumnRules/ColumnMatcher.cs index 94187be..8ecc1ef 100644 --- a/src/core/Deal.Modules.Kanban/Application/ColumnRules/ColumnMatcher.cs +++ b/src/core/Deal.Modules.Kanban/Application/ColumnRules/ColumnMatcher.cs @@ -192,8 +192,6 @@ public static class ColumnMatcher return count; } - // terms: Термы как сохранены. - // Возвращает: Непустые термы в нижнем регистре. private static IReadOnlyList NormalizeTerms(IReadOnlyList? terms) { var result = new List(); diff --git a/src/core/Deal.Modules.Kanban/Application/ColumnRules/MatchHitBuilder.cs b/src/core/Deal.Modules.Kanban/Application/ColumnRules/MatchHitBuilder.cs index 7d694e5..84c6216 100644 --- a/src/core/Deal.Modules.Kanban/Application/ColumnRules/MatchHitBuilder.cs +++ b/src/core/Deal.Modules.Kanban/Application/ColumnRules/MatchHitBuilder.cs @@ -18,19 +18,15 @@ public static class MatchHitBuilder // Метка группы «грейд/уровень». private const string GradeLabel = "Грейд/уровень"; - // Метка отдельной группы «уровень» (§6.3). private const string LevelsLabel = "Уровень"; - // Метка группы «локация» (§6.3). private const string LocationsLabel = "Локация"; - // Метка группы «тип» (§6.3). private const string TypesLabel = "Тип"; // Метка группы «бюджет». private const string BudgetLabel = "Бюджет"; - // Метка отдельной группы «цена» (§6.3). private const string PricesLabel = "Цена"; /// @@ -130,10 +126,6 @@ public static class MatchHitBuilder } } - // rawTerms: Термы как сохранены. - // label: Метка группы. - // lower: Текст в нижнем регистре. - // Возвращает: Hits по каждому совпавшему терму (term сохраняет регистр пользователя). private static IEnumerable MatchedTermHits( IReadOnlyList? rawTerms, string label, @@ -154,8 +146,6 @@ public static class MatchHitBuilder } } - // budget: Бюджет правил. - // Возвращает: «от X до Y CUR» / «до X CUR» / «от X CUR» / «бюджет». private static string DescribeRange(BudgetRangeDto budget) { string cur = (budget.Cur ?? string.Empty).ToUpperInvariant(); @@ -177,8 +167,6 @@ public static class MatchHitBuilder return "бюджет"; } - // value: Число. - // Возвращает: Строка по инвариантной культуре. private static string FormatAmount(double value) { return value.ToString("G6", System.Globalization.CultureInfo.InvariantCulture).Replace('E', 'e'); diff --git a/src/core/Deal.Modules.Kanban/Application/ColumnRules/RulesDescriber.cs b/src/core/Deal.Modules.Kanban/Application/ColumnRules/RulesDescriber.cs index 93dd4be..aa963c0 100644 --- a/src/core/Deal.Modules.Kanban/Application/ColumnRules/RulesDescriber.cs +++ b/src/core/Deal.Modules.Kanban/Application/ColumnRules/RulesDescriber.cs @@ -106,8 +106,6 @@ public static class RulesDescriber parts.Add($"{title}: {lo}–{hi} {cur}".Replace("– ", "–").Replace(" –", "–")); } - // terms: Термы группы. - // limit: Максимум термов. private static string JoinLimited(IReadOnlyList terms, int limit) { int count = Math.Min(terms.Count, limit); diff --git a/src/core/Deal.Modules.Kanban/Application/ColumnRules/TypeAliases.cs b/src/core/Deal.Modules.Kanban/Application/ColumnRules/TypeAliases.cs index 3d082ad..72cf90f 100644 --- a/src/core/Deal.Modules.Kanban/Application/ColumnRules/TypeAliases.cs +++ b/src/core/Deal.Modules.Kanban/Application/ColumnRules/TypeAliases.cs @@ -1,7 +1,7 @@ namespace Deal.Modules.Kanban.Application.ColumnRules; /// -/// Синонимы типов заявки для группы types правил колонки и глобальных исключений +/// Синонимы типов заявки для группы types правил колонки и глобальных исключений. /// public static class TypeAliases { diff --git a/src/core/Deal.Modules.Kanban/Application/Registrars/KanbanModuleRegistrar.cs b/src/core/Deal.Modules.Kanban/Application/Registrars/KanbanModuleRegistrar.cs index 35f65ab..087e2a6 100644 --- a/src/core/Deal.Modules.Kanban/Application/Registrars/KanbanModuleRegistrar.cs +++ b/src/core/Deal.Modules.Kanban/Application/Registrars/KanbanModuleRegistrar.cs @@ -12,15 +12,12 @@ public static class KanbanModuleRegistrar /// /// Регистрирует сервисы модуля Kanban в контейнере. /// - /// Коллекция сервисов. - /// Коллекция сервисов для цепочки вызовов. public static IServiceCollection AddKanbanModule(this IServiceCollection services) { services.AddScoped(); services.AddScoped(); services.AddScoped(); - // оповещают его после записи кэша курсов / смены targetCurrency|conversionOn (список может быть пуст). services.AddScoped(); return services; } diff --git a/src/core/Deal.Modules.Kanban/Application/Services/BudgetNormalizer.cs b/src/core/Deal.Modules.Kanban/Application/Services/BudgetNormalizer.cs index 1acba93..8eb7ef8 100644 --- a/src/core/Deal.Modules.Kanban/Application/Services/BudgetNormalizer.cs +++ b/src/core/Deal.Modules.Kanban/Application/Services/BudgetNormalizer.cs @@ -100,8 +100,6 @@ public static class BudgetNormalizer return new CardBudgetDto(convFrom, convTo, target); } - // value: Значение границы из бюджета. - // Возвращает: Значение или null при 0. private static double? NormalizeBound(double? value) { if (value is null || value.Value == 0) @@ -112,8 +110,6 @@ public static class BudgetNormalizer return value.Value; } - // raw: Валюта как пришла («рублей», «$», «usd», …). - // Возвращает: Код валюты (USD/RUB/…) или null, если не распознана. private static string? NormalizeCurrency(string? raw) { string s = (raw ?? string.Empty).Trim().ToUpperInvariant(); diff --git a/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Files.cs b/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Files.cs index 5726d13..3bb8261 100644 --- a/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Files.cs +++ b/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Files.cs @@ -120,10 +120,6 @@ public sealed partial class CardsService ?? throw new InvalidOperationException("Карточка не прочиталась после удаления файла: " + cardId); } - // cardId: Id карточки (c_...). - // fileId: Id записи файла (pf_...; уникальный суффикс ключа). - // name: Имя файла как прислано (в ключ идёт санитизированная часть). - // Возвращает: Ключ объекта (opaque для хранилища). private static string BuildObjectKey( string cardId, string fileId, diff --git a/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Helpers.cs b/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Helpers.cs index 14de2f4..f8c543d 100644 --- a/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Helpers.cs +++ b/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Helpers.cs @@ -15,17 +15,12 @@ public sealed partial class CardsService { // ── Внутреннее ───────────────────────────────────────────────────────── - // card: Карточка. private static string LearningText(CardDto card) { string source = card.SourceMsg.Trim(); return source.Length > 0 ? source : card.Title; } - // card: Карточка ДО переноса (для prev_col/from_col журнала). - // toCol: Новая колонка. - // action: Действие журнала: move/trash. - // ct: Токен отмены. private async Task MoveToColumnAsync( CardDto card, string toCol, @@ -43,11 +38,6 @@ public sealed partial class CardsService await LogMoveAsync(card.Id, action, card.Col, toCol, ct); } - // cardId: Id карточки. - // action: Действие: move/trash/restore/comment. - // fromCol: Прежняя колонка (для comment — null). - // toCol: Новая колонка (для comment — null). - // ct: Токен отмены. private async Task LogMoveAsync( string cardId, string action, @@ -63,10 +53,6 @@ public sealed partial class CardsService toCol), ct); } - // boardId: Id доски (b_...). - // text: Текст карточки для правил (source_msg или title). - // ct: Токен отмены. - // Возвращает: Список совпавших критериев; доски нет/правил нет → пусто. private async Task> ComputeHitsForBoardAsync( string boardId, string text, @@ -78,11 +64,6 @@ public sealed partial class CardsService : await ComputeHitsAsync(board.Rules, text, ct); } - // Совпавшие критерии правил колонки-доски (ColumnRules.ComputeHits с курсами для бюджета). - // Резолв имени — через алиас KanbanColumnRules: одноимённые класс и namespace ColumnRules в одном модуле. - // text: Текст карточки для правил. - // ct: Токен отмены. - // Возвращает: Совпавшие критерии (label/term[/word]); нет активных правил → пусто. private async Task> ComputeHitsAsync( ContainerRulesDto? rules, string text, @@ -94,9 +75,6 @@ public sealed partial class CardsService return KanbanColumnRules.ComputeHits(rules, text, rates); } - // prevCol: Сохранённая prev_col карточки. - // ct: Токен отмены. - // Возвращает: Колонка возврата (inbox/доска). private async Task ResolveReturnColAsync(string prevCol, CancellationToken ct) { if (prevCol == CardIds.Inbox) diff --git a/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Reminders.cs b/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Reminders.cs index c871a8f..e0fb89a 100644 --- a/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Reminders.cs +++ b/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Reminders.cs @@ -102,8 +102,6 @@ public sealed partial class CardsService return due; } - // ct: Токен отмены. - // Возвращает: Значение настройки; отсутствие/повреждённый JSON → дефолт SettingsDefaults.RemindersEnabled. private async Task ReadRemindersEnabledAsync(CancellationToken ct) { return (await TenantSettingsSnapshot.LoadAsync(_settings, ct)) diff --git a/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Selected.cs b/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Selected.cs index 6030090..7eba535 100644 --- a/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Selected.cs +++ b/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Selected.cs @@ -247,9 +247,6 @@ public sealed partial class CardsService return _store.ClearStageAsync(RejectedStage, ct); } - // Переводит тело PATCH в точечный патч хранилища: ключ присутствует → поле меняется; - // body: Тело PATCH (ключ → значение). - // Возвращает: Патч со значениями присутствующих ключей; остальные поля null («не менять»). private static CardPatch ResolvePatch(IReadOnlyDictionary body) { string? title = null; diff --git a/src/core/Deal.Modules.Kanban/Application/Services/FileKindDetector.cs b/src/core/Deal.Modules.Kanban/Application/Services/FileKindDetector.cs index 576f173..2331a8b 100644 --- a/src/core/Deal.Modules.Kanban/Application/Services/FileKindDetector.cs +++ b/src/core/Deal.Modules.Kanban/Application/Services/FileKindDetector.cs @@ -103,8 +103,6 @@ public static class FileKindDetector return new CardFileKind(kind, LabelsByKind[kind]); } - // name: Имя файла. - // Возвращает: Расширение без точки; пустая строка, если точки в имени нет (или имя заканчивается точкой). private static string GetExtension(string name) { int lastDot = name.LastIndexOf('.'); diff --git a/src/core/Deal.Modules.Kanban/Application/Services/StorageTickService.cs b/src/core/Deal.Modules.Kanban/Application/Services/StorageTickService.cs index 4b40589..a8c1e42 100644 --- a/src/core/Deal.Modules.Kanban/Application/Services/StorageTickService.cs +++ b/src/core/Deal.Modules.Kanban/Application/Services/StorageTickService.cs @@ -30,7 +30,6 @@ public sealed class StorageTickService(ICardStore store, ISettingsStore settings int archived = 0; if (autoArchive) { - // UPDATE (архивация как системный перенос: col=archive, is_new=false, archived_at=now, matchHits IReadOnlyList candidates = await store.ListArchiveCandidatesAsync( now.AddDays(-archiveAfterDays), ct); archived = candidates.Count == 0 ? 0 : await store.ArchiveAsync(candidates, now, ct); @@ -45,9 +44,6 @@ public sealed class StorageTickService(ICardStore store, ISettingsStore settings return new StorageTickStatsDto(archived, purgedArchive, purgedTrash, PurgedRejected: 0); } - // cardIds: Id карточек-кандидатов из выборки хранилища. - // ct: Токен отмены. - // Возвращает: Сколько карточек реально удалено (0 — кандидатов не было). private async Task PurgeAsync(IReadOnlyList cardIds, CancellationToken ct) { return cardIds.Count == 0 ? 0 : await store.PurgeAsync(cardIds, ct); diff --git a/src/core/Deal.Modules.Kanban/Application/Services/SuggestHeuristics.cs b/src/core/Deal.Modules.Kanban/Application/Services/SuggestHeuristics.cs index ad92020..a052605 100644 --- a/src/core/Deal.Modules.Kanban/Application/Services/SuggestHeuristics.cs +++ b/src/core/Deal.Modules.Kanban/Application/Services/SuggestHeuristics.cs @@ -95,7 +95,6 @@ public static class SuggestHeuristics /// Планы колонок (≤4, каждая ≥2 карточки); пусто — мало карточек/нечего сгруппировать. public static IReadOnlyList PlanColumns(IReadOnlyList inbox, IReadOnlyList existingBoardNames) { - // детерминизма: адаптер уже отдаёт received_at DESC, но вход не должен влиять на выход). List window = inbox .Where(card => card.SourceMsg.Trim().Length > 0) .OrderByDescending(card => card.ReceivedAtMs) @@ -233,10 +232,6 @@ public static class SuggestHeuristics terms.Add(term); } - // Похоже ли слово-тема на существующую доску: равенство или вхождение имени в слово/наоборот - // term: Слово-кандидат (нижний регистр). - // existingLowered: Имена существующих досок в нижнем регистре. - // Возвращает: True — тема уже покрыта существующей колонкой (кандидат пропускается). private static bool SimilarToExisting(string term, List existingLowered) { foreach (string name in existingLowered) @@ -250,7 +245,5 @@ public static class SuggestHeuristics return false; } - // term: Слово-тема в нижнем регистре (непустое). - // Возвращает: Слово с заглавной первой буквой. private static string Capitalize(string term) => char.ToUpperInvariant(term[0]) + term[1..]; } diff --git a/src/core/Deal.Modules.Pipeline/Application/Abstractions/IPipelineStore.cs b/src/core/Deal.Modules.Pipeline/Application/Abstractions/IPipelineStore.cs index a290249..4d3c49b 100644 --- a/src/core/Deal.Modules.Pipeline/Application/Abstractions/IPipelineStore.cs +++ b/src/core/Deal.Modules.Pipeline/Application/Abstractions/IPipelineStore.cs @@ -55,7 +55,7 @@ public interface IPipelineStore CancellationToken ct); /// - /// Удаляет строку очереди безвозвратно + /// Удаляет строку очереди безвозвратно. /// /// Id строки (p_...). public Task RemoveAsync(string id, CancellationToken ct); diff --git a/src/core/Deal.Modules.Pipeline/Application/Models/GlobalExcludeSettings.cs b/src/core/Deal.Modules.Pipeline/Application/Models/GlobalExcludeSettings.cs index bd3e0be..e9865dc 100644 --- a/src/core/Deal.Modules.Pipeline/Application/Models/GlobalExcludeSettings.cs +++ b/src/core/Deal.Modules.Pipeline/Application/Models/GlobalExcludeSettings.cs @@ -1,7 +1,7 @@ namespace Deal.Modules.Pipeline.Application.Models; /// -/// Снимок глобальных исключений тенанта +/// Снимок глобальных исключений тенанта. /// /// Ключевые слова/фразы/технологии (пусто — группа выключена). /// Локации/языки (пусто — группа выключена). diff --git a/src/core/Deal.Modules.Pipeline/Application/Models/MlApplyResult.cs b/src/core/Deal.Modules.Pipeline/Application/Models/MlApplyResult.cs index 78e99f0..a7821f9 100644 --- a/src/core/Deal.Modules.Pipeline/Application/Models/MlApplyResult.cs +++ b/src/core/Deal.Modules.Pipeline/Application/Models/MlApplyResult.cs @@ -1,7 +1,7 @@ namespace Deal.Modules.Pipeline.Application.Models; /// -/// Результат ручного решения по сообщению +/// Результат ручного решения по сообщению. /// /// Текст ошибки 400 (неизвестная доска/действие); null — решение применено. /// True — решение принято. diff --git a/src/core/Deal.Modules.Pipeline/Application/Models/MlCandidateDto.cs b/src/core/Deal.Modules.Pipeline/Application/Models/MlCandidateDto.cs index 729dd3d..15ff178 100644 --- a/src/core/Deal.Modules.Pipeline/Application/Models/MlCandidateDto.cs +++ b/src/core/Deal.Modules.Pipeline/Application/Models/MlCandidateDto.cs @@ -1,7 +1,7 @@ namespace Deal.Modules.Pipeline.Application.Models; /// -/// Сообщение-кандидат для ручной проверки ML +/// Сообщение-кандидат для ручной проверки ML. /// public sealed record MlCandidateDto { diff --git a/src/core/Deal.Modules.Pipeline/Application/Models/MlCandidatePredictionDto.cs b/src/core/Deal.Modules.Pipeline/Application/Models/MlCandidatePredictionDto.cs index 7671c24..9f533fe 100644 --- a/src/core/Deal.Modules.Pipeline/Application/Models/MlCandidatePredictionDto.cs +++ b/src/core/Deal.Modules.Pipeline/Application/Models/MlCandidatePredictionDto.cs @@ -1,7 +1,7 @@ namespace Deal.Modules.Pipeline.Application.Models; /// -/// Мнение ML по сообщению-кандидату +/// Мнение ML по сообщению-кандидату. /// /// Модель «взяла бы» сообщение (готова и уверена). /// Метка решения (id доски либо spam); null — модель не уверена/не готова. diff --git a/src/core/Deal.Modules.Pipeline/Application/Models/PipelineRejectConstants.cs b/src/core/Deal.Modules.Pipeline/Application/Models/PipelineRejectConstants.cs index ab3444d..f4fbddf 100644 --- a/src/core/Deal.Modules.Pipeline/Application/Models/PipelineRejectConstants.cs +++ b/src/core/Deal.Modules.Pipeline/Application/Models/PipelineRejectConstants.cs @@ -46,7 +46,7 @@ public static class PipelineRejectConstants }; /// - /// Подпись отсева + /// Подпись отсева: словарь, иначе сам stage; пустой stage — «отсев». /// /// Этап отсева. /// Подпись для UI. diff --git a/src/core/Deal.Modules.Pipeline/Application/Models/RejectRecord.cs b/src/core/Deal.Modules.Pipeline/Application/Models/RejectRecord.cs index b10ae95..b22e319 100644 --- a/src/core/Deal.Modules.Pipeline/Application/Models/RejectRecord.cs +++ b/src/core/Deal.Modules.Pipeline/Application/Models/RejectRecord.cs @@ -46,7 +46,7 @@ public sealed record RejectRecord public string Source { get; init; } = string.Empty; /// - /// Этап отсева: length|stop|resume|type|budget|stale|spam_ml|spam_ai|filter_ai|dup + /// Этап отсева: length|stop|resume|type|budget|stale|spam_ml|spam_ai|filter_ai|dup. /// public string Stage { get; init; } = string.Empty; diff --git a/src/core/Deal.Modules.Pipeline/Application/Models/RejectedPageDto.cs b/src/core/Deal.Modules.Pipeline/Application/Models/RejectedPageDto.cs index c32b152..dbf6b2e 100644 --- a/src/core/Deal.Modules.Pipeline/Application/Models/RejectedPageDto.cs +++ b/src/core/Deal.Modules.Pipeline/Application/Models/RejectedPageDto.cs @@ -3,7 +3,7 @@ namespace Deal.Modules.Pipeline.Application.Models; /// /// Страница отсева — тело GET /api/pipeline/rejected. /// -/// Записи страницы (полные item'ы отсева §4.5, порядок RejectedAt DESC). +/// Записи страницы. /// Всего записей по условию поиска. /// Сдвиг от начала страницы (эхо запроса). /// Размер страницы (эхо запроса; ≤500). diff --git a/src/core/Deal.Modules.Pipeline/Application/Parse/ContactsQualifier.cs b/src/core/Deal.Modules.Pipeline/Application/Parse/ContactsQualifier.cs index 41e6d64..5bf96ad 100644 --- a/src/core/Deal.Modules.Pipeline/Application/Parse/ContactsQualifier.cs +++ b/src/core/Deal.Modules.Pipeline/Application/Parse/ContactsQualifier.cs @@ -238,8 +238,6 @@ public static class ContactsQualifier return result.Count > MaxTextCandidates ? result.Take(MaxTextCandidates).ToList() : result; } - // phone: Телефон как встретился в тексте. - // Возвращает: Нормализованный телефон (≤MaxNormalizedPhoneLength символов). internal static string NormalizePhone(string phone) { string normalized = phone.Replace(" ", string.Empty) @@ -252,16 +250,11 @@ public static class ContactsQualifier : normalized; } - // type: Тип контакта (tg/phone/whatsapp/email/linkedin/site). - // Возвращает: Приоритет (меньше — важнее); неизвестный тип — UnknownTypePriority. private static int OrderOf(string type) { return PrimaryOrder.TryGetValue(type, out int order) ? order : UnknownTypePriority; } - // candidates: Сырые кандидаты. - // text: Текст сообщения для извлечения кандидатов, если список пуст. - // Возвращает: Квалифицированные контакты без дублей (≤MaxContacts). private static IReadOnlyList BuildFromCandidates(List candidates, string? text) { if (candidates.Count == 0) @@ -295,8 +288,6 @@ public static class ContactsQualifier return result; } - // result: Список-накопитель. - // values: Найденные совпадения. private static void AddUnique(List result, IEnumerable values) { foreach (string value in values) diff --git a/src/core/Deal.Modules.Pipeline/Application/Parse/DedupHasher.cs b/src/core/Deal.Modules.Pipeline/Application/Parse/DedupHasher.cs index 82f2709..fa83f3d 100644 --- a/src/core/Deal.Modules.Pipeline/Application/Parse/DedupHasher.cs +++ b/src/core/Deal.Modules.Pipeline/Application/Parse/DedupHasher.cs @@ -20,8 +20,6 @@ public static class DedupHasher return Convert.ToHexString(hash).ToLowerInvariant(); } - // text: Исходный текст. - // Возвращает: Нормализованная строка без регистра, пробелов и пунктуации. private static string Normalize(string text) { var builder = new StringBuilder(text.Length); diff --git a/src/core/Deal.Modules.Pipeline/Application/Parse/LocalFieldsParser.cs b/src/core/Deal.Modules.Pipeline/Application/Parse/LocalFieldsParser.cs index 2d6d8d8..d4c8708 100644 --- a/src/core/Deal.Modules.Pipeline/Application/Parse/LocalFieldsParser.cs +++ b/src/core/Deal.Modules.Pipeline/Application/Parse/LocalFieldsParser.cs @@ -213,8 +213,6 @@ public sealed class LocalFieldsParser(ISettingsStore store) return new LocalParsedFields(title, summary, resultStack, resultGrades, budget, contactsText, isVacancy); } - // line: Строка текста (очищенная). - // Возвращает: Категория поля и значение метки; null — это не метка-поле. internal static (string Category, string Value)? FieldOf(string line) { Match match = LabelRe.Match(line); @@ -241,8 +239,6 @@ public sealed class LocalFieldsParser(ISettingsStore store) return null; } - // value: Значение метки («Стек: Java, Kotlin» → «Java, Kotlin»). - // Возвращает: Токены-технологии (до MaxPickTokens). private static IReadOnlyList PickStackTokens(string value) { var tokens = new List(); @@ -264,8 +260,6 @@ public sealed class LocalFieldsParser(ISettingsStore store) return tokens; } - // value: Строка значения. - // Возвращает: Все совпадения токенов. private static IEnumerable TokensOf(string value) { foreach (Match match in TokenRe.Matches(value)) @@ -274,8 +268,6 @@ public sealed class LocalFieldsParser(ISettingsStore store) } } - // source: Текст (значение метки «Бюджет:» либо всё тело). - // Возвращает: Бюджет формы хранения (валюта-код) или null — суммы с валютой нет. private static BudgetRangeDto? FirstAmount(string source) { IReadOnlyList amounts = AmountParser.Parse(source); @@ -288,8 +280,6 @@ public sealed class LocalFieldsParser(ISettingsStore store) return new BudgetRangeDto(first.From, first.To, first.Cur); } - // markers: Список маркеров как сохранён (или дефолт); null — пусто. - // Возвращает: Нормализованный набор (порядок исходного, без дублей). private static IReadOnlySet NormalizeMarkers(IReadOnlyCollection? markers) { var result = new HashSet(StringComparer.Ordinal); @@ -310,9 +300,6 @@ public sealed class LocalFieldsParser(ISettingsStore store) return result; } - // lower: Текст в нижнем регистре. - // markers: Нормализованные маркеры. - // Возвращает: True — найден хотя бы один маркер. private static bool ContainsAny(string lower, IReadOnlySet markers) { foreach (string marker in markers) @@ -337,7 +324,6 @@ public sealed class LocalFieldsParser(ISettingsStore store) // Категория поля «Бюджет:». Константа-строка (свитч по категориям меток). private const string FieldCategoryBudget = "budget"; - // Возвращает: Список: категория → синонимы меток (нижний регистр). private static IReadOnlyList<(string Category, IReadOnlySet Synonyms)> BuildFieldLabels() { return new List<(string, IReadOnlySet)> diff --git a/src/core/Deal.Modules.Pipeline/Application/Parse/MessageListNormalizer.cs b/src/core/Deal.Modules.Pipeline/Application/Parse/MessageListNormalizer.cs index bd8a98c..1e3a596 100644 --- a/src/core/Deal.Modules.Pipeline/Application/Parse/MessageListNormalizer.cs +++ b/src/core/Deal.Modules.Pipeline/Application/Parse/MessageListNormalizer.cs @@ -68,9 +68,6 @@ public static class MessageListNormalizer /// public static IReadOnlySet StackStopWords => StopWordsSet; - // Очищает части списка: обрезка, снятие обрамляющих *`#, висящих запятых/точек, - // parts: Сырые части (после разбиения или элементы списка). - // Возвращает: Очищенный список. private static IReadOnlyList CleanParts(IEnumerable parts) { var result = new List(); @@ -88,8 +85,6 @@ public static class MessageListNormalizer return result; } - // items: Нормализованные элементы списка. - // Возвращает: Стек ≤12 элементов длиной ≥2. private static IReadOnlyList TakeStack(IReadOnlyList items) { var stack = new List(Math.Min(items.Count, MaxStackItems)); @@ -110,7 +105,6 @@ public static class MessageListNormalizer return stack; } - // Возвращает: Набор слов в нижнем регистре. private static IReadOnlySet BuildStopWords() { return new HashSet(StringComparer.Ordinal) diff --git a/src/core/Deal.Modules.Pipeline/Application/Parse/MessageTextCleaner.cs b/src/core/Deal.Modules.Pipeline/Application/Parse/MessageTextCleaner.cs index e57fb4c..90de883 100644 --- a/src/core/Deal.Modules.Pipeline/Application/Parse/MessageTextCleaner.cs +++ b/src/core/Deal.Modules.Pipeline/Application/Parse/MessageTextCleaner.cs @@ -103,8 +103,6 @@ public static class MessageTextCleaner return LineEdgesRe.Replace(line ?? string.Empty, string.Empty).Trim(); } - // value: Строка (не null). - // Возвращает: Число кодовых точек. internal static int CountCodePoints(string value) { int count = 0; @@ -120,10 +118,6 @@ public static class MessageTextCleaner return count; } - // суррогатных пар). - // value: Строка. - // max: Максимум кодовых точек; ≤0 или ≥ длины — строка как есть. - // Возвращает: Усечённая строка. internal static string SliceCodePoints(string value, int max) { if (max <= 0) @@ -156,10 +150,6 @@ public static class MessageTextCleaner return builder.ToString(); } - // 1F000–1FAFF (включая региональные флаги 1F1E6–1F1FF), разные символы 2600–27BF, стрелки 2B00–2BFF - // и variation selector FE0F. - // value: Текст после снятия markdown-разметки. - // Возвращает: Текст без эмодзи-диапазонов (суррогатные пары удаляются целиком). private static string RemoveEmojiCodePoints(string value) { var builder = new StringBuilder(value.Length); @@ -212,10 +202,6 @@ public static class MessageTextCleaner return current; } - // Обрезка по границе последнего переноса/пробела в первых limit кодовых точках - // value: Текст длиннее лимита. - // limit: Лимит кодовых точек. - // Возвращает: Усечённый текст с многоточием в конце. private static string CutByBoundary(string value, int limit) { string cut = SliceCodePoints(value, limit); diff --git a/src/core/Deal.Modules.Pipeline/Application/Parse/SummaryComposer.cs b/src/core/Deal.Modules.Pipeline/Application/Parse/SummaryComposer.cs index 34ba04a..7bdc8a1 100644 --- a/src/core/Deal.Modules.Pipeline/Application/Parse/SummaryComposer.cs +++ b/src/core/Deal.Modules.Pipeline/Application/Parse/SummaryComposer.cs @@ -19,7 +19,6 @@ public static class SummaryComposer private const int MaxSummaryLength = 600; - // Сколько первых строк списка пропускает локальная суть по умолчанию — первая строка это заголовок private const int DefaultSkipFirst = 1; private static readonly IReadOnlySet TrivialWords = new HashSet(StringComparer.Ordinal) @@ -79,7 +78,6 @@ public static class SummaryComposer return string.Join("\n", blocks); } - // Структурированных полей нет. «summary» разбора часто является копией исходника/шумом — если в нём есть string legacy = MessageTextCleaner.CleanShort(source.Summary); if (legacy.Length > 0 && !legacy.ContainsFooterHint()) { @@ -163,9 +161,6 @@ public static class SummaryComposer return MessageTextCleaner.SliceCodePoints(summary, MaxSummaryLength); } - // Нормализует пункты блока: элементы списка чистятся как в normalize_list(list), затем каждый — - // values: Сырые пункты разбора (может быть null). - // Возвращает: Очищенные непустые пункты. private static IReadOnlyList ContentItems(IReadOnlyList? values) { var result = new List(); diff --git a/src/core/Deal.Modules.Pipeline/Application/Registrars/PipelineModuleRegistrar.cs b/src/core/Deal.Modules.Pipeline/Application/Registrars/PipelineModuleRegistrar.cs index a344c24..f5c6abe 100644 --- a/src/core/Deal.Modules.Pipeline/Application/Registrars/PipelineModuleRegistrar.cs +++ b/src/core/Deal.Modules.Pipeline/Application/Registrars/PipelineModuleRegistrar.cs @@ -12,8 +12,6 @@ public static class PipelineModuleRegistrar /// /// Регистрирует сервисы модуля Pipeline в контейнере. /// - /// Коллекция сервисов. - /// Коллекция сервисов для цепочки вызовов. public static IServiceCollection AddPipelineModule(this IServiceCollection services) { // Ядра разбора задачи 4: статические (MessageTextCleaner/…/SummaryComposer) не регистрируются; @@ -23,14 +21,11 @@ public static class PipelineModuleRegistrar services.AddScoped(); services.AddScoped(); - // оба читают настройки тенанта/доски (ISettingsStore/ICardStore scoped, как IncomingRules). services.AddScoped(); services.AddScoped(); services.AddScoped(); - // и доски/журнал тенанта — scoped, как CardComposer. Потребитель — gRPC-адаптер GrpcAiClassifier - // (Infrastructure, реализует порт IAiClassifier); локальный путь контекст не строит. services.AddScoped(); // Этап 12, пакет D: ручная переклассификация карточек (POST /api/cards/reclassify|{id}/reclassify). @@ -39,9 +34,6 @@ public static class PipelineModuleRegistrar services.AddSingleton(); services.AddScoped(); - // Этап 12, добивка по ТЗ §8: ручная проверка/разметка ML на сообщениях канала — кандидаты и применение - // решения (обучение ML + перенос/корзина/отсев через существующие сервисы). Scoped — как остальные - // сервисы модуля (EF-контекст tenant-запроса). services.AddScoped(); return services; } diff --git a/src/core/Deal.Modules.Pipeline/Application/Services/AiClassifyContextBuilder.cs b/src/core/Deal.Modules.Pipeline/Application/Services/AiClassifyContextBuilder.cs index 79e1c60..a5b7489 100644 --- a/src/core/Deal.Modules.Pipeline/Application/Services/AiClassifyContextBuilder.cs +++ b/src/core/Deal.Modules.Pipeline/Application/Services/AiClassifyContextBuilder.cs @@ -108,8 +108,6 @@ public sealed class AiClassifyContextBuilder(ISettingsStore settings, ICardStore return context.ToString(); } - // ct: Токен отмены. - // Возвращает: Список строк «- id: имя (критерии) — описание»; пусто — фраза «колонок нет». private async Task BuildBoardMapAsync(CancellationToken ct) { IReadOnlyList containers = await kanjStore.ListContainersAsync(ContainerSpaces.Dashboard, ct); @@ -127,8 +125,6 @@ public sealed class AiClassifyContextBuilder(ISettingsStore settings, ICardStore return lines.Count == 0 ? NoBoardsLine : string.Join("\n", lines); } - // container: Колонка (non-suggested, kind=board). - // Возвращает: Однострочное описание колонки для промпта. private static string BuildBoardLine(ContainerDto container) { string line = "- " + container.Id + ": " + container.Name; @@ -146,9 +142,6 @@ public sealed class AiClassifyContextBuilder(ISettingsStore settings, ICardStore return line; } - // defaultValue: Дефолт из SettingsDefaults (когда переопределения нет). - // settingsSnapshot: Типизированный снимок настроек тенанта (промпты/«Сфера и ключи»). - // Возвращает: Текст промпта с подставленными {domain}/{keywords}. private static string FillPrompt( string key, string defaultValue, diff --git a/src/core/Deal.Modules.Pipeline/Application/Services/AiRawCardMapper.cs b/src/core/Deal.Modules.Pipeline/Application/Services/AiRawCardMapper.cs index 20cf0da..e4760e6 100644 --- a/src/core/Deal.Modules.Pipeline/Application/Services/AiRawCardMapper.cs +++ b/src/core/Deal.Modules.Pipeline/Application/Services/AiRawCardMapper.cs @@ -73,9 +73,6 @@ public static class AiRawCardMapper Board: board); } - // root: Корневой объект ответа модели. - // field: Имя поля. - // Возвращает: Значение строкой (отсутствие/null → пустая строка). private static string ReadRawString(JsonObject root, string field) { if (!root.TryGetPropertyValue(field, out JsonNode? node) || node is null) @@ -86,18 +83,12 @@ public static class AiRawCardMapper return node is JsonValue value ? ValueToString(value) : string.Empty; } - // root: Корневой объект ответа модели. - // field: Имя поля. - // Возвращает: Значение или null. private static string? ReadNullableString(JsonObject root, string field) { string value = ReadRawString(root, field); return value.Length == 0 ? null : value; } - // root: Корневой объект ответа модели. - // field: Имя поля. - // Возвращает: True/False; отсутствие/неразбираемое → false. private static bool ReadBool(JsonObject root, string field) { if (!root.TryGetPropertyValue(field, out JsonNode? node) || node is null) @@ -129,9 +120,6 @@ public static class AiRawCardMapper return false; } - // root: Корневой объект ответа модели. - // field: Имя поля. - // Возвращает: Очищенные пункты; отсутствие/null → null (блока нет), пустой список → пустой список. private static IReadOnlyList? ReadItems(JsonObject root, string field) { if (!root.TryGetPropertyValue(field, out JsonNode? node) || node is null) @@ -157,8 +145,6 @@ public static class AiRawCardMapper return result; } - // root: Корневой объект ответа модели. - // Возвращает: Нормализованный стек. private static IReadOnlyList ReadStack(JsonObject root) { if (!root.TryGetPropertyValue("stack", out JsonNode? node) || node is null) @@ -173,8 +159,6 @@ public static class AiRawCardMapper : Array.Empty(); } - // root: Корневой объект ответа модели. - // Возвращает: Нормализованный бюджет контракта или null (нет объекта/валюты/границ). private static AiBudgetDto? ReadBudget(JsonObject root) { if (!root.TryGetPropertyValue("budget", out JsonNode? node) || node is not JsonObject budget) @@ -196,9 +180,6 @@ public static class AiRawCardMapper : new AiBudgetDto(normalized.From, normalized.To, normalized.Cur); } - // budget: Объект бюджета ответа модели. - // field: Имя границы (from/to). - // Возвращает: Число или null — поле отсутствует/пусто/не разбирается. private static double? ParseBudgetBound(JsonObject budget, string field) { if (!budget.TryGetPropertyValue(field, out JsonNode? node) || node is null) @@ -242,9 +223,6 @@ public static class AiRawCardMapper return result == 0 ? null : result; } - // root: Корневой объект ответа модели. - // text: Исходный текст сообщения (fallback-кандидаты, если контактов в ответе нет). - // Возвращает: Квалифицированные контакты (≤6, дедуп по casefold-значению). private static IReadOnlyList ReadContacts(JsonObject root, string text) { List candidates = new(); @@ -295,13 +273,9 @@ public static class AiRawCardMapper return result; } - // Значение JsonNode строкой: JsonValue — как ValueToString, прочее/отсутствие — пустая строка. - // node: Узел JSON (элемент массива). private static string NodeToString(JsonNode? node) => node is JsonValue value ? ValueToString(value) : string.Empty; - // value: JSON-значение. - // Возвращает: Строковое представление. private static string ValueToString(JsonValue value) { if (value.TryGetValue(out string? text)) diff --git a/src/core/Deal.Modules.Pipeline/Application/Services/CardComposer.cs b/src/core/Deal.Modules.Pipeline/Application/Services/CardComposer.cs index 41a71d0..1ae1861 100644 --- a/src/core/Deal.Modules.Pipeline/Application/Services/CardComposer.cs +++ b/src/core/Deal.Modules.Pipeline/Application/Services/CardComposer.cs @@ -128,11 +128,6 @@ public sealed class CardComposer(ICardStore kanjStore, ISettingsStore settings) }; } - // Бюджет карточки: нормализация из разбора, иначе fallback первой суммы по исходнику/«О заявке» - // parsedBudget: Бюджет разбора (форма контракта; null — разбор не выделил сумму). - // text: Текст исходного сообщения (первый источник fallback). - // summary: Блок «О заявке» (второй источник fallback — сумма часто уходит в «Условия»). - // Возвращает: Нормализованный бюджет либо null — суммы с валютой нет ни в разборе, ни в тексте. private static CardBudgetDto? ComposeBudget( AiBudgetDto? parsedBudget, string? text, @@ -147,8 +142,6 @@ public sealed class CardComposer(ICardStore kanjStore, ISettingsStore settings) return fallback is null ? null : BudgetNormalizer.Normalize(fallback); } - // parsed: Разбор (пустые/отсутствующие поля блок не дают — SummaryComposer). - // Возвращает: Структура для SummaryComposer.Compose. private static ParsedCardContent ToParsedContent(AiParsedCardDto parsed) => new( Company: parsed.Company, Format: parsed.Format, diff --git a/src/core/Deal.Modules.Pipeline/Application/Services/CardReclassifier.cs b/src/core/Deal.Modules.Pipeline/Application/Services/CardReclassifier.cs index d1fb123..68a86a3 100644 --- a/src/core/Deal.Modules.Pipeline/Application/Services/CardReclassifier.cs +++ b/src/core/Deal.Modules.Pipeline/Application/Services/CardReclassifier.cs @@ -195,10 +195,6 @@ public sealed class CardReclassifier( await AiCardLearning.PushSignalsAsync(store, mlClient, snapshot.Col, parsed, text, MlLearningLabels.AiPushWeight, ct); } - // card: Карточка. - // text: Исходный текст (обучающий пример). - // pass: Накопители прохода. - // ct: Токен отмены. private async Task TrashAsync( CardDto card, string text, @@ -253,9 +249,6 @@ public sealed class CardReclassifier( string cur) => cur.Length == 0 ? null : new CardBudgetDto(from, to, cur); - // card: Карточка до переклассификации (старый контакт). - // computed: Контакт, собранный из нового разбора. - // Возвращает: Значение основного контакта либо пустая строка. private static string ResolveContact(CardDto card, string computed) { if (computed.Length > 0) @@ -267,9 +260,6 @@ public sealed class CardReclassifier( return oldContact.Length > 0 && ContactsQualifier.Qualify(oldContact) is not null ? oldContact : string.Empty; } - // inbox: Карточки «Неразобранного» (порядок хранилища). - // ids: Опциональный фильтр id. - // Возвращает: Целевые карточки в порядке хранилища. private static List SelectTarget(IReadOnlyList inbox, IReadOnlyList? ids) { if (ids is null || ids.Count == 0) diff --git a/src/core/Deal.Modules.Pipeline/Application/Services/GlobalExclusionRules.cs b/src/core/Deal.Modules.Pipeline/Application/Services/GlobalExclusionRules.cs index 3c48c6f..ec32b0a 100644 --- a/src/core/Deal.Modules.Pipeline/Application/Services/GlobalExclusionRules.cs +++ b/src/core/Deal.Modules.Pipeline/Application/Services/GlobalExclusionRules.cs @@ -4,7 +4,7 @@ using Deal.Modules.Pipeline.Application.Models; namespace Deal.Modules.Pipeline.Application.Services; /// -/// Глобальные исключения тенанта — стоп-фильтр ДО ML/ИИ +/// Глобальные исключения тенанта — стоп-фильтр ДО ML/ИИ. /// public static class GlobalExclusionRules { diff --git a/src/core/Deal.Modules.Pipeline/Application/Services/MlReviewService.cs b/src/core/Deal.Modules.Pipeline/Application/Services/MlReviewService.cs index 7fb2fa0..564ed0b 100644 --- a/src/core/Deal.Modules.Pipeline/Application/Services/MlReviewService.cs +++ b/src/core/Deal.Modules.Pipeline/Application/Services/MlReviewService.cs @@ -10,7 +10,7 @@ using Deal.Modules.Pipeline.Application.Models; namespace Deal.Modules.Pipeline.Application.Services; /// -/// Ручная проверка/разметка ML на сообщениях канала +/// Ручная проверка/разметка ML на сообщениях канала. /// public sealed class MlReviewService( IPipelineStore pipelineStore, diff --git a/src/core/Deal.Modules.Pipeline/Application/Services/PipelineProcessingService.cs b/src/core/Deal.Modules.Pipeline/Application/Services/PipelineProcessingService.cs index 5a9a88f..0e4e8d0 100644 --- a/src/core/Deal.Modules.Pipeline/Application/Services/PipelineProcessingService.cs +++ b/src/core/Deal.Modules.Pipeline/Application/Services/PipelineProcessingService.cs @@ -91,7 +91,7 @@ public sealed class PipelineProcessingService( /// Сырые сообщения очереди в порядке постановки /// /// Максимум строк; clamp 1..500 — запрашивается больше — вернётся не больше 500. - /// Строки очереди (item'ы §4.5; внутренний Force в JSON не выходит). + /// Строки очереди. public async Task> ListQueueAsync(int limit, CancellationToken ct) { int clamped = Math.Clamp(limit, 1, MaxPageSize); @@ -213,7 +213,6 @@ public sealed class PipelineProcessingService( } else { - // Старые записи (до сохранения dialog_id/msg_id): текст сохранился — возвращаем без ссылки var item = new QueueItemDto { Id = PrefixId.New(PipelineIdPrefixes.Queue), diff --git a/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Checks.cs b/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Checks.cs index 4ef47dd..06e239e 100644 --- a/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Checks.cs +++ b/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Checks.cs @@ -12,11 +12,6 @@ public sealed partial class PipelineWorkerService { // ── Проверки воркера ─────────────────────────────────────────────────── - // включён и msg_at старше archiveAfterDays суток → устарело. msg_at=0/пусто — не устарело. Флаги — из - // снимка прохода (читаются один раз на pump, не на каждое сообщение). - // row: Строка очереди (время исходного сообщения). - // run: Снимок настроек воркера (autoArchive/archiveAfterDays). - // Возвращает: True — сообщение старше срока до автоархива (не заводим в систему). private bool IsStaleAsync(QueueItemDto row, WorkerRunSettings run) { if (row.MsgAtMs == 0) @@ -39,14 +34,6 @@ public sealed partial class PipelineWorkerService return nowMs - row.MsgAtMs > maxAgeMs; } - // соответствующий флаг (budgetRequiredHire/Order) и суммы нет ни в разборе, ни в тексте → карточку не создаём. - // Суммой считаем нормализованный бюджет разбора либо сумму с валютой в тексте (AmountParser.Parse, - // rules_svc.extract_amounts). Claim дедупа при отсеве снимается — после выключения фильтра сообщение можно - // parsed: Разбор сообщения (тип заявки и бюджет). - // text: Исходный текст сообщения (fallback-источник суммы). - // run: Снимок настроек воркера (флаги budgetRequiredHire/Order). - // ct: Токен отмены. - // Возвращает: True — фильтр требует сумму, а её нет (сообщение пропускаем). private Task SkipNoBudgetAsync( AiParsedCardDto parsed, string text, @@ -72,10 +59,6 @@ public sealed partial class PipelineWorkerService return Task.FromResult(AmountParser.Parse(text).Count == 0); } - // ведущей «~», не стоп-слова стека, не дубликаты — до MaxMlTermsAdded терминов. - // terms: Термины класса из ответа ML (dec.terms). - // stack: Стек локального разбора (к нему добавляем; лимит 12 доберёт CardComposer). - // Возвращает: Стек с добавленными терминами ML. private static IReadOnlyList MergeMlTerms(IReadOnlyList terms, IReadOnlyList stack) { var merged = new List(stack); @@ -110,9 +93,6 @@ public sealed partial class PipelineWorkerService return merged; } - // text: Текст сообщения. - // ct: Токен отмены. - // Возвращает: Решение модели либо фиксированный «не готов/не уверен». private async Task PredictSafelyAsync(string text, CancellationToken ct) { try @@ -125,9 +105,6 @@ public sealed partial class PipelineWorkerService } } - // text: Текст сообщения. - // ct: Токен отмены. - // Возвращает: Решение фильтра либо пропуск при сбое. private async Task FilterSafelyAsync(string text, CancellationToken ct) { try diff --git a/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Decisions.cs b/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Decisions.cs index d83c6af..af52578 100644 --- a/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Decisions.cs +++ b/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Decisions.cs @@ -9,10 +9,6 @@ namespace Deal.Modules.Pipeline.Application.Services; public sealed partial class PipelineWorkerService { - // Инкрементирует KV-счётчики решений после pump: mlDecisions = mlStored+mlDrop, aiDecisions = aiStored+aiDrop. - // Read-modify-write через ISettingsStore (те же ключи читает LocalMlClient.StatusAsync — - // state: Накопители результата pump. - // ct: Токен отмены. private async Task TrackDecisionsAsync(PumpState state, CancellationToken ct) { int ml = state.MlStored + state.MlDrop; diff --git a/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Learning.cs b/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Learning.cs index c88cd50..e0a6d14 100644 --- a/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Learning.cs +++ b/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Learning.cs @@ -11,15 +11,6 @@ public sealed partial class PipelineWorkerService { // ── Карточка и обучение ML ──────────────────────────────────────────── - // Создаёт карточку (CardComposer + PipelineCardWriter: AddCard → LinkDedup → чтение) и снимает строку очереди. - // Dedup-claim НЕ удаляется: он уже связан с карточкой (IPipelineStore.LinkAsync в писателе; - // state: Накопители результата pump. - // parsed: Разбор сообщения (поля карточки + назначенная колонка). - // row: Строка очереди (метаданные канала/текст/время). - // digest: SHA1-hex текста (claim уже заявлен и будет связан с карточкой). - // isAiPath: true — ИИ/локальный путь (aiStored); false — ML-ветка (mlStored). - // ct: Токен отмены. - // Возвращает: Созданная карточка (полный CardDto, для CreatedCards/обучения). private async Task CreateCardAndDropAsync( PumpState state, AiParsedCardDto parsed, @@ -43,12 +34,6 @@ public sealed partial class PipelineWorkerService return card; } - // без активных правил и не-suggested) и тип (t:hire/t:order при is_vacancy_known). - // «Не знаю» (inbox) и служебные колонки не учим; доска с активными правилами/ИИ-предложение — тоже - // card: Созданная карточка (реальная колонка после ContainerAccepts-страховки). - // parsed: Разбор, на котором собрана карточка (тип/спам из классификатора). - // text: Текст сообщения (обучающий пример — как source_msg карточки). - // ct: Токен отмены. private Task LearnFromAiCardAsync( CardDto card, AiParsedCardDto parsed, diff --git a/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Pump.cs b/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Pump.cs index c75c634..86175d0 100644 --- a/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Pump.cs +++ b/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Pump.cs @@ -21,8 +21,6 @@ public sealed partial class PipelineWorkerService /// Сводка прохода: счётчики решений + созданные карточки (SSE new_card). public async Task PumpOnceAsync(CancellationToken ct) { - // значения/дефолты резолвятся в снимок, который передаётся во все проверки сообщений батча и - // LocalFieldsParser (раньше настройки читались на каждое сообщение — до 16 чтений таблицы за pump). WorkerRunSettings runSettings = await LoadRunSettingsAsync(_settings, ct); var state = new PumpState(); await PumpNewPassAsync(state, runSettings, ct); @@ -32,10 +30,6 @@ public sealed partial class PipelineWorkerService } - // Батч сообщений статуса new: устарело/правила/повтор отсекаются, остальные — ML или в filtered. - // Настройки (сроки/флаги ML/ИИ/бюджета) — из снимка прохода: читаются один раз, не на каждое сообщение. - // state: Накопители результата pump (счётчики/карточки). - // ct: Токен отмены. private async Task PumpNewPassAsync( PumpState state, WorkerRunSettings run, @@ -62,8 +56,6 @@ public sealed partial class PipelineWorkerService continue; } - // Глобальные исключения тенанта (§5.14): стоп ДО дедупа/ML/ИИ, чтобы не тратить токены; - // причина называет конкретное сработавшее исключение (source=stop). GlobalExclusionResult? exclusion = GlobalExclusionRules.Match(row.Text, run.GlobalExclusions); if (exclusion is not null) { @@ -81,8 +73,6 @@ public sealed partial class PipelineWorkerService continue; } - // Claim атомарен (INSERT … ON CONFLICT DO NOTHING): false — хэш уже заявлен ПАРАЛЛЕЛЬНЫМ проходом - // pump после нашей проверки ExistsAsync. Карточку не создаём (иначе два прохода завели бы две) — if (!await _store.ClaimAsync(digest, ct)) { await RejectRowAsync(row, SourceDup, StageDup, DupReason, string.Empty, ct); @@ -190,10 +180,6 @@ public sealed partial class PipelineWorkerService } - // Батч сообщений статуса filtered: локальный путь (aiEnabled=false) или фильтр+классификация ИИ. - // Настройки — из снимка прохода (читаются один раз, не на каждое сообщение). - // state: Накопители результата pump (счётчики/карточки). - // ct: Токен отмены. private async Task PumpFilteredPassAsync( PumpState state, WorkerRunSettings run, @@ -253,15 +239,12 @@ public sealed partial class PipelineWorkerService parsed = null; } - // эвристикой). Стемп ставится ДО создания карточки (карточка получает is_vacancy_known=true) и - // переживает force-отмену вердикта «спам»; локальный разбор aiFail/aiEnabled=false стемпа не имеет. if (parsed is not null) { parsed = parsed with { IsVacancyKnown = true }; } } - // отменяет вердикт — пользователь уже подтвердил релевантность, карточку создаём без обучения «спаму». bool isSpam = !filter.Pass || (parsed?.IsSpam ?? false); if (force && filter.Pass && parsed?.IsSpam == true) { diff --git a/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Rejections.cs b/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Rejections.cs index 60c2111..7d0cf5c 100644 --- a/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Rejections.cs +++ b/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Rejections.cs @@ -10,11 +10,6 @@ public sealed partial class PipelineWorkerService { // ── Отсев / удаление строк ───────────────────────────────────────────── - // row: Строка очереди — источник полей записи (текст/канал/время). - // source: Источник решения: stop|ml|ai|stale|dup. - // stage: Этап отсева: length|stop|resume|type|budget|stale|spam_ml|spam_ai|filter_ai|dup. - // kw: Сработавшая фраза/маркер правила (пусто — не правило). - // ct: Токен отмены. private Task RejectRowAsync( QueueItemDto row, string source, @@ -39,10 +34,6 @@ public sealed partial class PipelineWorkerService }, ct); } - // row: Строка очереди. - // digest: Хэш текста (для снятия незанятого dedup-claim). - // withDedup: Снять claim дедупа (true — сообщение можно обработать заново после отсева). - // ct: Токен отмены. private async Task DropRowAsync( QueueItemDto row, string digest, @@ -57,9 +48,6 @@ public sealed partial class PipelineWorkerService await _store.RemoveAsync(row.Id, ct); } - // row: Строка очереди со старым msg_at. - // run: Снимок настроек воркера (срок до автоархива уже прочитан). - // ct: Токен отмены. private async Task DropStaleAsync( QueueItemDto row, WorkerRunSettings run, diff --git a/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Settings.cs b/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Settings.cs index d871067..2bc9fd6 100644 --- a/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Settings.cs +++ b/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Settings.cs @@ -10,19 +10,6 @@ namespace Deal.Modules.Pipeline.Application.Services; public sealed partial class PipelineWorkerService { - // Снимок настроек воркера на один проход pump: значения прочитаны ОДИН раз (LoadRunSettingsAsync) - // и передаются во все проверки сообщений батча и в LocalFieldsParser — раньше настройки читались на каждое - // сообщение (ReadBool/ReadInt + GetAllAsync парсера — до 16 чтений таблицы за pump). Значения — из - // типизированного снимка TenantSettingsSnapshot (C30). - // Settings: Типизированный снимок переопределений тенанта (передаётся LocalFieldsParser). - // AutoArchive: Флаг автоархива (срок актуальности сообщения). - // ArchiveAfterDays: Срок до автоархива, сутки (stale/причина отсева). - // MlEnabled: ML-слот включён (не false — прогноз/решения). - // AiEnabled: ИИ-слот включён (классификация/фильтр). - // BudgetRequiredHire: Фильтр «без суммы» для найма. - // BudgetRequiredOrder: Фильтр «без суммы» для разовых заказов. - // WantedType: Тип заявок «что собираем» (both|vacancy|freelance). - // GlobalExclusions: Снимок глобальных исключений (стоп до ML/ИИ, §5.14). private sealed record WorkerRunSettings( TenantSettingsSnapshot Settings, bool AutoArchive, @@ -56,11 +43,6 @@ public sealed partial class PipelineWorkerService LoadGlobalExclusions(settings)); } - // Собирает снимок глобальных исключений тенанта (§5.14): списки слов/локаций/типов и границы бюджета. - // Граница бюджета 0 в настройках трактуется как «не задана» (null); сравнение сумм — числовое, - // без конвертации валют (у настройки нет валюты). - // settings: Типизированный снимок настроек тенанта. - // Возвращает: Снимок исключений для чистой проверки сообщений батча. private static GlobalExcludeSettings LoadGlobalExclusions(TenantSettingsSnapshot settings) { int from = settings.GetInt(SettingsKeys.ExcludeBudgetFrom, SettingsDefaults.ExcludeBudgetFrom); @@ -73,9 +55,6 @@ public sealed partial class PipelineWorkerService BudgetTo: to > 0 ? to : null); } - // ствующая/повреждённая строка → дефолт «both» (даёт GetString с дефолтом + нормализация). - // value: Значение настройки wantedType (JSON-строка либо дефолт из SettingsDefaults). - // Возвращает: Значение в нижнем регистре (пустое хранимое значение остаётся пустым — как wantedType=""). private static string NormalizeWantedType(string value) { return value.Trim().ToLowerInvariant(); diff --git a/src/core/Deal.Modules.Settings/Application/Models/IncomingRules.cs b/src/core/Deal.Modules.Settings/Application/Models/IncomingRules.cs index b64fdee..d2da8e4 100644 --- a/src/core/Deal.Modules.Settings/Application/Models/IncomingRules.cs +++ b/src/core/Deal.Modules.Settings/Application/Models/IncomingRules.cs @@ -65,12 +65,10 @@ public sealed class IncomingRules(ISettingsStore store) /// Вердикт: {pass, reason, stage:1, kind, kw}. public async Task CheckAsync(string? text, CancellationToken ct) { - // Читаем переопределения одним запросом (GetAllAsync) и считаем чистой функцией над снимком RulesSettings settings = ReadSettings(await TenantSettingsSnapshot.LoadAsync(store, ct)); return Evaluate(text, settings); } - // Возвращает: Вердикт: причина/правило/фраза либо проход. private static IncomingRulesResult Evaluate(string? text, RulesSettings settings) { string trimmed = (text ?? string.Empty).Trim(); @@ -124,12 +122,6 @@ public sealed class IncomingRules(ISettingsStore store) return new IncomingRulesResult(Pass: true, Reason: null, Stage, KindPass, Kw: string.Empty); } - // lower: Текст, приведённый к нижнему регистру (casefold). - // resumeMarkers: Нормализованные маркеры резюме (в порядке списка настроек). - // hireMarkers: Нормализованные маркеры найма (для guard). - // Возвращает: Найденный маркер (нормализованный) или null — текст не похож на резюме. - // работодателя («…вакансия…, присылайте резюме»), а не резюме соискателя — пропускаем маркер - // порядок произвольный). private static string? FindResumeMarker( string lower, IReadOnlyList resumeMarkers, @@ -172,9 +164,6 @@ public sealed class IncomingRules(ISettingsStore store) return false; } - // Собирает вердикт-отсев: stage всегда 1, причина фиксированная. - // kind: Правило, сработавшее на тексте (length|stop|resume|type). - // reason: Причина для UI. private static IncomingRulesResult Fail( string kind, string reason, @@ -183,8 +172,6 @@ public sealed class IncomingRules(ISettingsStore store) return new IncomingRulesResult(Pass: false, Reason: reason, Stage, kind, Kw); } - // settings: Снимок настроек тенанта (дефолты, перекрытые сохранёнными). - // Возвращает: Снимок значений для чистой проверки. private static RulesSettings ReadSettings(TenantSettingsSnapshot settings) { return new RulesSettings( @@ -208,8 +195,6 @@ public sealed class IncomingRules(ISettingsStore store) return wanted.Length > 0 ? wanted : WantedTypeBoth; } - // markers: Список маркеров как сохранён (или дефолт). - // Возвращает: Нормализованный список в порядке исходного (детерминированно). private static IReadOnlyList NormalizeMarkers(IReadOnlyList markers) { var result = new List(markers.Count); @@ -225,12 +210,6 @@ public sealed class IncomingRules(ISettingsStore store) return result; } - // MinLen: Минимальная длина текста (minLen). - // StopPhrases: Стоп-фразы как сохранены (kw отдаётся исходным списком). - // BlockResumes: True — резюме соискателей отсекаем. - // ResumeMarkers: Нормализованные маркеры резюме (для FindResumeMarker). - // HireMarkers: Нормализованные маркеры найма (guard «резюме» + тип заявки). - // WantedType: Тип собираемых заявок (both|vacancy|freelance). private sealed record RulesSettings( int MinLen, IReadOnlyList StopPhrases, diff --git a/src/core/Deal.Modules.Settings/Application/Models/MyPromptDto.cs b/src/core/Deal.Modules.Settings/Application/Models/MyPromptDto.cs index b2937b6..c4ef1f7 100644 --- a/src/core/Deal.Modules.Settings/Application/Models/MyPromptDto.cs +++ b/src/core/Deal.Modules.Settings/Application/Models/MyPromptDto.cs @@ -1,7 +1,7 @@ namespace Deal.Modules.Settings.Application.Models; /// -/// Элемент «Моих промптов» +/// Элемент «Моих промптов». /// /// Идентификатор промпта (начинается с pp_). /// Название (как показывается пользователю). diff --git a/src/core/Deal.Modules.Settings/Application/Models/PublicSettingsDto.cs b/src/core/Deal.Modules.Settings/Application/Models/PublicSettingsDto.cs index 59ba100..435b8ae 100644 --- a/src/core/Deal.Modules.Settings/Application/Models/PublicSettingsDto.cs +++ b/src/core/Deal.Modules.Settings/Application/Models/PublicSettingsDto.cs @@ -1,7 +1,7 @@ namespace Deal.Modules.Settings.Application.Models; /// -/// Публичный снимок настроек тенанта — тело ответа GET/PATCH /api/settings +/// Публичный снимок настроек тенанта — тело ответа GET/PATCH /api/settings. /// public sealed record PublicSettingsDto { diff --git a/src/core/Deal.Modules.Settings/Application/Models/SettingsDefaults.cs b/src/core/Deal.Modules.Settings/Application/Models/SettingsDefaults.cs index e4c9781..5aecc27 100644 --- a/src/core/Deal.Modules.Settings/Application/Models/SettingsDefaults.cs +++ b/src/core/Deal.Modules.Settings/Application/Models/SettingsDefaults.cs @@ -122,7 +122,6 @@ public static class SettingsDefaults /// public const bool BlockResumes = true; - // ── Глобальные исключения (стоп-уровень, §5.14) ── /// /// Дефолт «excludeKeywords» @@ -257,7 +256,6 @@ public static class SettingsDefaults /// public const bool DiscPaused = false; - // Возвращает: Словарь: id провайдера → конфиг с пустым ключом, базовым URL и первой моделью. private static IReadOnlyDictionary BuildDefaultAiConfigs() { var result = new Dictionary(AiProviders.All.Count); diff --git a/src/core/Deal.Modules.Settings/Application/Models/SettingsKeys.cs b/src/core/Deal.Modules.Settings/Application/Models/SettingsKeys.cs index f47dd32..4ea4d97 100644 --- a/src/core/Deal.Modules.Settings/Application/Models/SettingsKeys.cs +++ b/src/core/Deal.Modules.Settings/Application/Models/SettingsKeys.cs @@ -331,7 +331,7 @@ public static class SettingsKeys /// /// Возвращает категорию публичного ключа или null, если ключ не в каталоге. /// - /// Имя ключа (camelCase, как в api-map). + /// Имя ключа. /// Категория ключа или null для неизвестного/внутреннего ключа. public static SettingKind? FindPublicKind(string key) { diff --git a/src/core/Deal.Modules.Settings/Application/Registrars/SettingsModuleRegistrar.cs b/src/core/Deal.Modules.Settings/Application/Registrars/SettingsModuleRegistrar.cs index 14d6eba..4d06ef9 100644 --- a/src/core/Deal.Modules.Settings/Application/Registrars/SettingsModuleRegistrar.cs +++ b/src/core/Deal.Modules.Settings/Application/Registrars/SettingsModuleRegistrar.cs @@ -12,8 +12,6 @@ public static class SettingsModuleRegistrar /// /// Регистрирует сервисы модуля Settings в контейнере. /// - /// Коллекция сервисов. - /// Коллекция сервисов для цепочки вызовов. public static IServiceCollection AddSettingsModule(this IServiceCollection services) { services.AddScoped(); diff --git a/src/core/Deal.Modules.Settings/Application/Services/RatesService.cs b/src/core/Deal.Modules.Settings/Application/Services/RatesService.cs index e6bbf07..c32f5cd 100644 --- a/src/core/Deal.Modules.Settings/Application/Services/RatesService.cs +++ b/src/core/Deal.Modules.Settings/Application/Services/RatesService.cs @@ -73,8 +73,6 @@ public sealed class RatesService( return true; } - // ct: Токен отмены. - // пустой список — no-op. При сбое источника (кэш не записан) слушатели НЕ вызываются. private async Task NotifyRatesChangedAsync(CancellationToken ct) { foreach (IRatesChangedListener listener in listeners) @@ -166,10 +164,7 @@ public sealed class RatesService( await store.SetAsync(SettingsKeys.RatesCache, JsonSerializer.Serialize(cache, JsonOptions), ct); } - // Нормализует значение настройки источника к известным source кэша (mock|cbr). - // setting: Значение настройки rateSource. private static string NormalizeSource(string setting) => setting == MockRateSource ? MockSource : CbrSource; - // Возвращает: Количество миллисекунд с 1970-01-01 UTC. private static long UtcNowMs() => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); } diff --git a/src/core/Deal.Modules.Settings/Application/Services/SettingsService.PatchMyPrompts.cs b/src/core/Deal.Modules.Settings/Application/Services/SettingsService.PatchMyPrompts.cs index 9bd1bc2..8a766cd 100644 --- a/src/core/Deal.Modules.Settings/Application/Services/SettingsService.PatchMyPrompts.cs +++ b/src/core/Deal.Modules.Settings/Application/Services/SettingsService.PatchMyPrompts.cs @@ -8,8 +8,6 @@ namespace Deal.Modules.Settings.Application.Services; // и чтение полей элемента: ReadTrimmedField/ReadPromptId). colState — passthrough в главном ApplyPatchAsync. public sealed partial class SettingsService { - // array: JSON-массив из тела PATCH. - // Возвращает: JSON-строка списка {id,name,description,prompt} (camelCase). private static string SerializeCleanMyPrompts(JsonElement array) { var clean = new List(); diff --git a/src/core/Deal.Modules.Settings/Application/Services/SettingsService.PatchScalarKeys.cs b/src/core/Deal.Modules.Settings/Application/Services/SettingsService.PatchScalarKeys.cs index 3cc21aa..d40fec3 100644 --- a/src/core/Deal.Modules.Settings/Application/Services/SettingsService.PatchScalarKeys.cs +++ b/src/core/Deal.Modules.Settings/Application/Services/SettingsService.PatchScalarKeys.cs @@ -7,9 +7,6 @@ namespace Deal.Modules.Settings.Application.Services; // int/string-ключи и списки строк (ApplyIntKey/ApplyStringKey/SerializeStringList). public sealed partial class SettingsService { - // body: Тело PATCH (не мутируется). - // overrides: Сохранённые переопределения (для «другого конца» интервала). - // Возвращает: Ключ → уже склампированное значение; ключи в результате пропускаются общим циклом. private static Dictionary PrepareDelayClamps(Dictionary body, Dictionary overrides) { var result = new Dictionary(StringComparer.Ordinal); @@ -122,8 +119,6 @@ public sealed partial class SettingsService writes[key] = JsonSerializer.Serialize(text); } - // array: JSON-массив из тела PATCH. - // Возвращает: JSON-строка массива строк. private static string SerializeStringList(JsonElement array) { var items = new List(); diff --git a/src/core/Deal.Modules.Settings/Application/Services/SettingsService.PatchSecrets.cs b/src/core/Deal.Modules.Settings/Application/Services/SettingsService.PatchSecrets.cs index 91c5a2d..2d42920 100644 --- a/src/core/Deal.Modules.Settings/Application/Services/SettingsService.PatchSecrets.cs +++ b/src/core/Deal.Modules.Settings/Application/Services/SettingsService.PatchSecrets.cs @@ -7,10 +7,6 @@ namespace Deal.Modules.Settings.Application.Services; // apiKey), включая удаление переопределения, вернувшегося к дефолту (EqualsDefault). public sealed partial class SettingsService { - // value: JSON-объект {id провайдера → конфиг} из тела PATCH. - // overrides: Сохранённые переопределения. - // writes: Накопитель записей (key → valueJson). - // removals: Список ключей на удаление (переопределение вернулось к дефолту). private void ApplyAiConfigsKey( JsonElement value, Dictionary overrides, diff --git a/src/core/Deal.Modules.Settings/Application/Services/SettingsService.PublicForms.cs b/src/core/Deal.Modules.Settings/Application/Services/SettingsService.PublicForms.cs index b6435d9..13c893a 100644 --- a/src/core/Deal.Modules.Settings/Application/Services/SettingsService.PublicForms.cs +++ b/src/core/Deal.Modules.Settings/Application/Services/SettingsService.PublicForms.cs @@ -4,8 +4,6 @@ namespace Deal.Modules.Settings.Application.Services; public sealed partial class SettingsService { - // config: Сохранённая конфигурация (apiKey — enc: или пустая строка). - // Возвращает: DTO с флагом keySet и маской keyMasked. private AiConfigPublicDto ToPublic(AiConfigSetting config) { string plainKey = secretCipher.Decrypt(config.ApiKey); @@ -16,9 +14,6 @@ public sealed partial class SettingsService KeyMasked: MaskSecret(plainKey)); } - // Маска СЕКРЕТА (apiKey ИИ): всегда скрывает, кроме пустого — короткие значения не раскрываются - // value: Открытый секрет (не null). - // Возвращает: Маскированная строка. private static string MaskSecret(string value) { return value.Length switch diff --git a/src/core/Deal.Modules.Settings/Application/Services/SettingsService.ReadMerge.cs b/src/core/Deal.Modules.Settings/Application/Services/SettingsService.ReadMerge.cs index 61169bf..8446961 100644 --- a/src/core/Deal.Modules.Settings/Application/Services/SettingsService.ReadMerge.cs +++ b/src/core/Deal.Modules.Settings/Application/Services/SettingsService.ReadMerge.cs @@ -4,11 +4,8 @@ using Deal.Modules.Settings.Application.Models; namespace Deal.Modules.Settings.Application.Services; -// TryRead* и Merge* по категориям ключей (канон мягкого чтения для GET /api/settings). public sealed partial class SettingsService { - // ct: Токен отмены. - // Возвращает: Словарь: ключ → JsonElement (клоны, живут после вызова). Повреждённые JSON-строки пропускаются. private async Task> LoadStoredOverridesAsync(CancellationToken ct) { var result = new Dictionary(StringComparer.Ordinal); @@ -30,9 +27,6 @@ public sealed partial class SettingsService return result; } - // element: JSON-значение. - // value: Прочитанное число (диапазон long сжимается до int). - // Возвращает: True — значение прочитано; иначе False (нечисловое — пропуск ключа). private static bool TryReadInt(JsonElement element, out int value) { long wide; @@ -63,9 +57,6 @@ public sealed partial class SettingsService return true; } - // element: JSON-значение. - // value: Прочитанное значение. - // Возвращает: True — значение вида true/false. private static bool TryReadBool(JsonElement element, out bool value) { if (element.ValueKind is JsonValueKind.True or JsonValueKind.False) @@ -78,9 +69,6 @@ public sealed partial class SettingsService return false; } - // element: JSON-значение. - // text: Текст; null — если значение не скаляр (объект/массив/null). - // Возвращает: True — значение скалярное. private static bool TryReadText(JsonElement element, out string? text) { switch (element.ValueKind) diff --git a/src/core/Deal.Modules.Settings/Application/Services/SettingsService.cs b/src/core/Deal.Modules.Settings/Application/Services/SettingsService.cs index be6799b..8038b4f 100644 --- a/src/core/Deal.Modules.Settings/Application/Services/SettingsService.cs +++ b/src/core/Deal.Modules.Settings/Application/Services/SettingsService.cs @@ -84,7 +84,7 @@ public sealed partial class SettingsService( /// /// Публичный снимок настроек /// - /// Полный public-снимок §4.6 (тело GET/PATCH /api/settings). + /// Полный public-снимок (тело GET/PATCH /api/settings). public async Task GetPublicAsync(CancellationToken ct) { Dictionary overrides = await LoadStoredOverridesAsync(ct); @@ -94,7 +94,7 @@ public sealed partial class SettingsService( /// /// Частичное обновление настроек /// - /// Тело PATCH — произвольный JSON-объект из публичных ключей §4.6. + /// Тело PATCH — произвольный JSON-объект из публичных ключей. /// Полный public-снимок после применения (как GET). public async Task ApplyPatchAsync(Dictionary body, CancellationToken ct) { @@ -102,7 +102,6 @@ public sealed partial class SettingsService( Dictionary overrides = await LoadStoredOverridesAsync(ct); - // клампы 5..600 и swap при min > max; при одном конце — кламп относительно сохранённого другого. Dictionary preparedDelays = PrepareDelayClamps(body, overrides); var writes = new Dictionary(StringComparer.Ordinal); @@ -180,7 +179,6 @@ public sealed partial class SettingsService( await store.RemoveAsync(key, ct); } - // (реализация порта — ConversionRecomputer модуля Kanban; пустой список — no-op). if (AffectsConversion(body)) { await NotifyRatesChangedAsync(ct); @@ -189,8 +187,6 @@ public sealed partial class SettingsService( return await GetPublicAsync(ct); } - // body: Тело PATCH. - // Возвращает: True — нужен пересчёт конверсий после применения. private static bool AffectsConversion(Dictionary body) { return HasNonNull(body, SettingsKeys.TargetCurrency) || HasNonNull(body, SettingsKeys.ConversionOn); @@ -205,7 +201,6 @@ public sealed partial class SettingsService( return body.TryGetValue(key, out JsonElement value) && value.ValueKind != JsonValueKind.Null; } - // ct: Токен отмены. private async Task NotifyRatesChangedAsync(CancellationToken ct) { foreach (IRatesChangedListener listener in listeners) @@ -252,7 +247,6 @@ public sealed partial class SettingsService( ResumeMarkers = MergeStringList(overrides, SettingsKeys.ResumeMarkers, SettingsDefaults.ResumeMarkers), BlockResumes = MergeBool(overrides, SettingsKeys.BlockResumes, SettingsDefaults.BlockResumes), - // Глобальные исключения (§5.14) ExcludeKeywords = MergeStringList(overrides, SettingsKeys.ExcludeKeywords, SettingsDefaults.ExcludeKeywords), ExcludeLocations = MergeStringList(overrides, SettingsKeys.ExcludeLocations, SettingsDefaults.ExcludeLocations), ExcludeTypes = MergeStringList(overrides, SettingsKeys.ExcludeTypes, SettingsDefaults.ExcludeTypes), diff --git a/src/core/Deal.Modules.Telegram/Application/DialogsService.cs b/src/core/Deal.Modules.Telegram/Application/DialogsService.cs index 1bd52f8..0265b17 100644 --- a/src/core/Deal.Modules.Telegram/Application/DialogsService.cs +++ b/src/core/Deal.Modules.Telegram/Application/DialogsService.cs @@ -27,7 +27,7 @@ public sealed class DialogsService( /// /// Список диалогов каталога для вкладки «Каналы». /// - /// Диалоги (включённые мониторингом первыми) в форме §4.8. + /// Диалоги (включённые мониторингом первыми) в форме. public Task> ListAsync(CancellationToken ct) => store.ListAsync(ct); /// @@ -152,7 +152,6 @@ public sealed class DialogsService( } catch (Exception exception) when (exception is not OperationCanceledException || !ct.IsCancellationRequested) { - // канал остаётся неразобранным — следующий «Перечитать»/первое включение попробует снова. logger.LogWarning(exception, "Перечитывание канала {DialogId} не удалось", dialogId); } } @@ -186,8 +185,8 @@ public sealed class DialogsService( /// Последние сообщения диалога для превью. /// /// Id диалога. - /// Сколько последних сообщений (1..50; api-map /dialogs/preview limit 1..50). - /// Сообщения от новых к старым в форме §4.11 ({id, text, time, lead}). + /// Сколько последних сообщений. + /// Сообщения от новых к старым в форме ({id, text, time, lead}). public async Task> PreviewAsync( string dialogId, int limit, @@ -209,7 +208,6 @@ public sealed class DialogsService( return await store.ListMessagesAsync(dialogId, limit, ct).ConfigureAwait(false); } - // lead свежих сообщений — по строке TgMessages «m__» (признак уже принятого/карточки): Dictionary existingRows = (await store.ListMessagesAsync(dialogId, limit, ct).ConfigureAwait(false)) .ToDictionary(message => message.Id, StringComparer.Ordinal); var items = new List(fresh.Count); @@ -255,9 +253,6 @@ public sealed class DialogsService( await store.SavePreviewAsync(previewId, dialogId, Truncate(text, PreviewTextMaxLength), at, ct).ConfigureAwait(false); } - // value: Исходная строка. - // maxLength: Потолок длины. - // Возвращает: Строка не длиннее maxLength. private static string Truncate(string value, int maxLength) => value.Length <= maxLength ? value : value[..maxLength]; } diff --git a/src/core/Deal.Modules.Telegram/Application/ITelegramStore.cs b/src/core/Deal.Modules.Telegram/Application/ITelegramStore.cs index 728ad9f..6e81fd4 100644 --- a/src/core/Deal.Modules.Telegram/Application/ITelegramStore.cs +++ b/src/core/Deal.Modules.Telegram/Application/ITelegramStore.cs @@ -46,7 +46,7 @@ public interface ITelegramStore /// Включает/выключает мониторинг всех диалогов каталога. /// /// True — мониторить все, false — снять мониторинг со всех. - /// Сколько диалогов в каталоге (api-map /monitor-all → count). + /// Сколько диалогов в каталоге. public Task SetMonitorAllAsync(bool enabled, CancellationToken ct); /// diff --git a/src/core/Deal.Modules.Telegram/Application/Models/TelegramMonitorAllDto.cs b/src/core/Deal.Modules.Telegram/Application/Models/TelegramMonitorAllDto.cs index b0cf9d0..bc9fcf1 100644 --- a/src/core/Deal.Modules.Telegram/Application/Models/TelegramMonitorAllDto.cs +++ b/src/core/Deal.Modules.Telegram/Application/Models/TelegramMonitorAllDto.cs @@ -3,6 +3,6 @@ namespace Deal.Modules.Telegram.Application.Models; /// /// Результат включения/выключения мониторинга всех диалогов — ответ SetMonitorAllAsync. /// -/// Сколько диалогов в каталоге тенанта (api-map /monitor-all → count). +/// Сколько диалогов в каталоге тенанта. /// Id неразобранных диалогов при enabled=true (пусто при выключении). public sealed record TelegramMonitorAllDto(int Count, IReadOnlyList BackfillNeededIds); diff --git a/src/core/Deal.Modules.Telegram/Application/TelegramModuleRegistrar.cs b/src/core/Deal.Modules.Telegram/Application/TelegramModuleRegistrar.cs index 40761c1..2e378e7 100644 --- a/src/core/Deal.Modules.Telegram/Application/TelegramModuleRegistrar.cs +++ b/src/core/Deal.Modules.Telegram/Application/TelegramModuleRegistrar.cs @@ -10,8 +10,6 @@ public static class TelegramModuleRegistrar /// /// Регистрирует сервисы модуля Telegram в контейнере. /// - /// Коллекция сервисов. - /// Коллекция сервисов для цепочки вызовов. public static IServiceCollection AddTelegramModule(this IServiceCollection services) { services.AddScoped(); diff --git a/src/core/Deal.Modules.Tenants/Application/Models/AuditEvents.cs b/src/core/Deal.Modules.Tenants/Application/Models/AuditEvents.cs index b90dd50..518e87c 100644 --- a/src/core/Deal.Modules.Tenants/Application/Models/AuditEvents.cs +++ b/src/core/Deal.Modules.Tenants/Application/Models/AuditEvents.cs @@ -151,7 +151,7 @@ public static class AuditEvents public const string TelegramLinked = "telegram_linked"; /// - /// Оператор изменил глобальные ключи Telegram api_id/api_hash + /// Оператор изменил глобальные ключи Telegram api_id/api_hash. /// public const string TelegramKeysChanged = "telegram_keys_changed"; } diff --git a/src/core/Deal.Modules.Tenants/Application/Registrars/TenantModuleRegistrar.cs b/src/core/Deal.Modules.Tenants/Application/Registrars/TenantModuleRegistrar.cs index 7bf15c6..f512953 100644 --- a/src/core/Deal.Modules.Tenants/Application/Registrars/TenantModuleRegistrar.cs +++ b/src/core/Deal.Modules.Tenants/Application/Registrars/TenantModuleRegistrar.cs @@ -12,8 +12,6 @@ public static class TenantModuleRegistrar /// /// Регистрирует сервисы модуля Tenants в контейнере. /// - /// Коллекция сервисов. - /// Коллекция сервисов для цепочки вызовов. public static IServiceCollection AddTenantsModule(this IServiceCollection services) { services.AddSingleton(); diff --git a/src/core/Deal.Modules.Tenants/Application/Services/AuditService.cs b/src/core/Deal.Modules.Tenants/Application/Services/AuditService.cs index 86f96ab..ac82d82 100644 --- a/src/core/Deal.Modules.Tenants/Application/Services/AuditService.cs +++ b/src/core/Deal.Modules.Tenants/Application/Services/AuditService.cs @@ -30,7 +30,6 @@ public sealed class AuditService(IAuditLogStore store) public async Task AppendAsync(AuditRecordDto record, CancellationToken ct) { await store.AppendAsync(record with { At = DateTimeOffset.UtcNow }, ct); - // (низкокардинальные метки — без tenantId/actorId). DealMetrics.RecordAuditEvent(record.EventType, record.ActorType); } diff --git a/src/core/Deal.Modules.Tenants/Application/Services/AuthService.cs b/src/core/Deal.Modules.Tenants/Application/Services/AuthService.cs index 9c1d29d..413e6c7 100644 --- a/src/core/Deal.Modules.Tenants/Application/Services/AuthService.cs +++ b/src/core/Deal.Modules.Tenants/Application/Services/AuthService.cs @@ -123,7 +123,6 @@ public sealed class AuthService( return new ChangePasswordResultDto(Ok: false, Error: ChangePasswordResultDto.ErrorTooShort, NewToken: null); } - // сессии, обновляем хэш и выдаём свежую — её raw-токен вернёт HTTP-слой в куке. await authStore.DeleteSessionsByUserIdAsync(user.Id, ct); await authStore.UpdatePasswordHashAsync(user.Id, passwordHasher.Hash(newPassword), ct); var token = await CreateSessionForUserAsync(user.Id, user.Login, impersonatedByOperatorId: null, ct); @@ -202,9 +201,6 @@ public sealed class AuthService( if (user is not null) { - // каждом разрешении сессии, поэтому активные сессии suspended-тенанта перестают работать - // храним: при resume доступ возвращается тем же путём (login не блокирует активные сессии). - // Касается и impersonation-сессий (та же tenant-сессия с маркером оператора). var tenant = await tenantRepository.FindByIdAsync(user.TenantId, ct); if (tenant is not null && tenant.Status == TenantStatuses.Suspended) { diff --git a/src/core/Deal.Modules.Tenants/Application/Services/InvitesService.cs b/src/core/Deal.Modules.Tenants/Application/Services/InvitesService.cs index 4f53895..66d531c 100644 --- a/src/core/Deal.Modules.Tenants/Application/Services/InvitesService.cs +++ b/src/core/Deal.Modules.Tenants/Application/Services/InvitesService.cs @@ -43,9 +43,6 @@ public sealed partial class InvitesService(IInviteStore inviteStore) return new InviteCreateResultDto(Ok: false, Error: InviteCreateResultDto.ErrorInvalidEmail, Invite: null); } - // pending уже истекло (статус ещё не переведён), сначала помечаем expired — иначе partial unique-индекс - // по pending не пустит новую строку; затем создаём новое приглашение. Отозванный/активированный/ - // истёкший email свободен (глобальную уникальность регистрации держит unique-индекс users.Login). var active = await inviteStore.FindActiveByEmailAsync(normalizedEmail, ct); if (active is not null) { diff --git a/src/core/Deal.Modules.Tenants/Application/Services/JoinService.cs b/src/core/Deal.Modules.Tenants/Application/Services/JoinService.cs index 5e282aa..d4438f4 100644 --- a/src/core/Deal.Modules.Tenants/Application/Services/JoinService.cs +++ b/src/core/Deal.Modules.Tenants/Application/Services/JoinService.cs @@ -34,7 +34,6 @@ public sealed class JoinService( string normalizedEmail = InvitesService.NormalizeEmail(email); string normalizedCode = code?.Trim() ?? string.Empty; - // поэтому вернувшийся pending гарантированно не истёк; отдельная проверка кода не нужна — отвечает статус. var invite = await invitesService.GetByCodeAsync(normalizedCode, ct); if (invite is null) { @@ -57,7 +56,6 @@ public sealed class JoinService( return Failed(JoinResultDto.ErrorPasswordTooShort); } - // email не создавал тенанта и не расходовал инвайт (остаётся pending — оператор видит/отзывает его). var existingUser = await authStore.FindUserByLoginAsync(normalizedEmail, ct); if (existingUser is not null) { diff --git a/src/core/Deal.Modules.Tenants/Application/Services/OperatorAuthService.cs b/src/core/Deal.Modules.Tenants/Application/Services/OperatorAuthService.cs index de2a72c..61540a9 100644 --- a/src/core/Deal.Modules.Tenants/Application/Services/OperatorAuthService.cs +++ b/src/core/Deal.Modules.Tenants/Application/Services/OperatorAuthService.cs @@ -74,8 +74,6 @@ public sealed class OperatorAuthService(IOperatorAuthStore operatorAuthStore, IP OperatorIdentityDto? operatorIdentity = null; if (session is not null && session.ExpiresAt > DateTimeOffset.UtcNow) { - // Оператора ищем по денормализованному в сессию логину: он уникален и не меняется. - // или приостановленный оператор при живой сессии получает null — 401 на HTTP-слое. var operatorRecord = await operatorAuthStore.FindByLoginAsync(session.Login, ct); if (operatorRecord is not null && operatorRecord.Status == ActiveStatus) { diff --git a/src/core/tests/Deal.Tests.Unit/Contracts/BudgetedAiClassifierTests.cs b/src/core/tests/Deal.Tests.Unit/Contracts/BudgetedAiClassifierTests.cs index 86c6008..b4448fa 100644 --- a/src/core/tests/Deal.Tests.Unit/Contracts/BudgetedAiClassifierTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Contracts/BudgetedAiClassifierTests.cs @@ -77,7 +77,6 @@ public sealed class BudgetedAiClassifierTests [Fact] public async Task Classify_LargeBudgetLimit_CallsPaidAndReturnsItsResult() { - // (10 000 000/месяц, расход 0) → Allowed=true → вызов уходит на платного исполнителя. Context ctx = Create(); ctx.Paid.ClassifyResult = PaidParsedLead; @@ -90,7 +89,6 @@ public sealed class BudgetedAiClassifierTests [Fact] public async Task Classify_SuspendedTenant_UsesLocalParseAndDoesNotCallPaid() { - // даже при неисчерпанном бюджете. Context ctx = Create( limits => limits.Preload( TenantGuid, budgetTokens: 1000, TenantLimitPeriods.Month, PeriodStart, usedTokens: 100, diff --git a/src/core/tests/Deal.Tests.Unit/Contracts/CardsServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Contracts/CardsServiceTests.cs index bbd0308..d169eb0 100644 --- a/src/core/tests/Deal.Tests.Unit/Contracts/CardsServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Contracts/CardsServiceTests.cs @@ -267,7 +267,6 @@ public sealed class CardsServiceTests CardResultDto result = await service.MoveDashboardCardAsync("l_1", "b_py", CancellationToken.None); - // restore_lead) — из archive/trash карточку выводит только «Вернуть» (кнопка restore). Assert.Equal(CardsService.MoveSourceRestrictedDetail, result.Error); Assert.Null(result.Card); Assert.Equal(KanbanColumns.Archive, store.CardDtos.Single().Col); // карточка не тронута @@ -284,7 +283,6 @@ public sealed class CardsServiceTests CardResultDto result = await service.MoveDashboardCardAsync("l_1", "b_py", CancellationToken.None); - // Из корзины карточка возвращается только кнопкой «Вернуть» (restore снимает спам-обучение): Assert.Equal(CardsService.MoveSourceRestrictedDetail, result.Error); Assert.Null(result.Card); Assert.Equal(KanbanColumns.Trash, store.CardDtos.Single().Col); diff --git a/src/core/tests/Deal.Tests.Unit/Contracts/FakeTelegramStore.cs b/src/core/tests/Deal.Tests.Unit/Contracts/FakeTelegramStore.cs index 747078c..12c6ed7 100644 --- a/src/core/tests/Deal.Tests.Unit/Contracts/FakeTelegramStore.cs +++ b/src/core/tests/Deal.Tests.Unit/Contracts/FakeTelegramStore.cs @@ -244,8 +244,6 @@ public sealed class FakeTelegramStore : ITelegramStore return Task.FromResult(items); } - // row: Строка Dialogs фейка. - // Возвращает: Форма §4.8: type = kind, on = monitor, last {text, time}. private static TelegramDialogDto ToDialogDto(FakeTelegramDialogRow row) { long? lastAtMs = row.LastAt?.ToUnixTimeMilliseconds(); diff --git a/src/core/tests/Deal.Tests.Unit/Contracts/IntegrationsDiTests.cs b/src/core/tests/Deal.Tests.Unit/Contracts/IntegrationsDiTests.cs index 22d537f..cc455c0 100644 --- a/src/core/tests/Deal.Tests.Unit/Contracts/IntegrationsDiTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Contracts/IntegrationsDiTests.cs @@ -58,8 +58,6 @@ public sealed class IntegrationsDiTests new TelegramServiceOptions { UseLocal = false }); await using AsyncServiceScope scope = provider.CreateAsyncScope(); - // gRPC-адаптеры: один scoped-экземпляр GrpcMlClient реализует IMlClient и IMlTrainClient (для - // флашера); наружу IAiClassifier → BudgetedAiClassifier (внутри GrpcAiClassifier), IAiTools → IMlClient mlClient = scope.ServiceProvider.GetRequiredService(); IMlTrainClient trainClient = scope.ServiceProvider.GetRequiredService(); Assert.IsType(mlClient); @@ -150,7 +148,6 @@ public sealed class IntegrationsDiTests services.AddScoped(_ => new FakeSecretCipher()); services.AddScoped(_ => new FakeKanjStore()); services.AddScoped(_ => new FakeMlLearningStore()); - // ITenantLimitStore регистрируется AddDealPersistence поверх DealDbContext — здесь не нужен). services.AddScoped(_ => new FakeTenantLimitStore()); services.AddScoped(_ => new TokenUsageEventService(new FakeTokenUsageEventStore())); services.AddScoped(); diff --git a/src/core/tests/Deal.Tests.Unit/Contracts/PipelineWorkerGrpcAiTests.cs b/src/core/tests/Deal.Tests.Unit/Contracts/PipelineWorkerGrpcAiTests.cs index 07b62b6..5d8ad53 100644 --- a/src/core/tests/Deal.Tests.Unit/Contracts/PipelineWorkerGrpcAiTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Contracts/PipelineWorkerGrpcAiTests.cs @@ -97,7 +97,6 @@ public sealed class PipelineWorkerGrpcAiTests PipelinePumpResult result = await ctx.Worker.PumpOnceAsync(CancellationToken.None); - // Недоступность ИИ: фильтр — «пропустить», классификация — локальный разбор (aiFail), карточка жива Assert.Equal(1, result.AiFail); Assert.Equal(1, result.AiStored); Assert.Single(result.CreatedCards); @@ -109,8 +108,6 @@ public sealed class PipelineWorkerGrpcAiTests [Fact] public async Task Pump_BudgetExhausted_GateUsesLocalClassifierWithoutPaidRpc() { - // декоратор BudgetedAiClassifier поверх реального GrpcAiClassifier не пускает к платному ИИ (бюджет - // исчерпан), фильтр/классификацию выполняет Local-реализация, pump завершается карточкой. await AiGrpcTestHost.RunAsync(AiGrpcTestHost.DefaultToken, new RecordingAiService(), async (port, service) => { // Реальный ai-service ответил бы — но гейт (бюджет исчерпан) не должен пустить к нему ни одного RPC. @@ -140,7 +137,6 @@ public sealed class PipelineWorkerGrpcAiTests Assert.Equal(0, service.ClassifyCalls); Assert.Empty(service.RequestTenantIds); - // aiFail не засчитан (классификатор вернул разбор, а не упал). Расход не списывался. Assert.Equal(1, result.AiStored); Assert.Equal(0, result.AiFail); Assert.Single(result.CreatedCards); @@ -161,11 +157,6 @@ public sealed class PipelineWorkerGrpcAiTests FakeSettingsStore Settings, FakeTenantLimitStore Limits); - // Собирает воркер с реальным GrpcAiClassifier к in-proc ai-service (остальные порты — фейки). - // port: Порт хоста-фейка ai-service. - // limits: Фейк лимитов (списание usage/гейт); null — собственный экземпляр. - // budgeted: True — классификатор обёрнут декоратором BudgetedAiClassifier - // Возвращает: Воркер и фейки для проверок. private static Context CreateContext( int port, FakeTenantLimitStore? limits = null, diff --git a/src/core/tests/Deal.Tests.Unit/Grpc/RecordingAiService.cs b/src/core/tests/Deal.Tests.Unit/Grpc/RecordingAiService.cs index b27986f..5e7e83a 100644 --- a/src/core/tests/Deal.Tests.Unit/Grpc/RecordingAiService.cs +++ b/src/core/tests/Deal.Tests.Unit/Grpc/RecordingAiService.cs @@ -154,14 +154,12 @@ public sealed class RecordingAiService : AiService.AiServiceBase return Task.FromResult(EvaluateFitReply); } - // context: Контекст вызова (metadata запроса). private void RecordMetadata(ServerCallContext context) { RequestTenantIds.Add(context.RequestHeaders.GetValue("tenant-id") ?? string.Empty); RequestTokens.Add(context.RequestHeaders.GetValue("service-token") ?? string.Empty); } - // Возвращает: Исключение RPC UNAVAILABLE. private static RpcException Unavailable() => new(new Status(StatusCode.Unavailable, "ИИ (DeepSeek) не ответил корректно — повторите попытку через несколько секунд")); } diff --git a/src/core/tests/Deal.Tests.Unit/Grpc/RecordingMlService.cs b/src/core/tests/Deal.Tests.Unit/Grpc/RecordingMlService.cs index c0e794b..33f0b8d 100644 --- a/src/core/tests/Deal.Tests.Unit/Grpc/RecordingMlService.cs +++ b/src/core/tests/Deal.Tests.Unit/Grpc/RecordingMlService.cs @@ -131,14 +131,12 @@ public sealed class RecordingMlService : MlService.MlServiceBase return Task.FromResult(new TrainBatchReply { Learned = request.Items.Count }); } - // context: Контекст вызова (metadata запроса). private void RecordMetadata(ServerCallContext context) { RequestTenantIds.Add(context.RequestHeaders.GetValue("tenant-id") ?? string.Empty); RequestTokens.Add(context.RequestHeaders.GetValue("service-token") ?? string.Empty); } - // Возвращает: Исключение RPC UNAVAILABLE. private static RpcException Unavailable() => new(new Status(StatusCode.Unavailable, "ml-service недоступен (тест)")); } diff --git a/src/core/tests/Deal.Tests.Unit/Infrastructure/TenantLimitStoreTests.cs b/src/core/tests/Deal.Tests.Unit/Infrastructure/TenantLimitStoreTests.cs index 8a929d6..99ccd36 100644 --- a/src/core/tests/Deal.Tests.Unit/Infrastructure/TenantLimitStoreTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Infrastructure/TenantLimitStoreTests.cs @@ -106,8 +106,6 @@ public sealed class TenantLimitStoreTests [Fact] public async Task AddUsageAsync_Crossing80Percent_DoesNotSetWarned80() { - // Warned80 — флаг ставит только TryMark* (планировщик алертов в момент фактического перехода), иначе - // списание «съедало» бы переход и тост при естественном расходе не вышел бы никогда. var tenantId = Guid.NewGuid(); (DealDbContext db, TenantLimitStore store) = CreateStore(clock: () => Now); AddTenant(db, tenantId, TenantStatuses.Active); @@ -124,7 +122,6 @@ public sealed class TenantLimitStoreTests [Fact] public async Task AddUsageAsync_Exhaustion_DoesNotSetFlagsButDisallows() { - // TryMark*); Allowed=false при этом считается от used/budget (1050 ≥ 1000), а не от флагов. var tenantId = Guid.NewGuid(); (DealDbContext db, TenantLimitStore store) = CreateStore(clock: () => Now); AddTenant(db, tenantId, TenantStatuses.Active); diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Cards/FakeKanjStore.cs b/src/core/tests/Deal.Tests.Unit/Modules/Cards/FakeKanjStore.cs index c224c64..5f9a02f 100644 --- a/src/core/tests/Deal.Tests.Unit/Modules/Cards/FakeKanjStore.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Cards/FakeKanjStore.cs @@ -164,9 +164,6 @@ public class FakeKanjStore : ICardStore int limit, CancellationToken ct) { - // недостижима — реализуется LIKE-дополнение (lower title/summary/contact/source_msg), порядок - // ReceivedAt DESC, лимит. Вызов записывается (SearchCalls) — тесты сервиса видят - // делегирование с q/лимитом. _searchCalls.Add((q, limit)); string lowered = q.Trim().ToLowerInvariant(); if (lowered.Length == 0) @@ -217,7 +214,6 @@ public class FakeKanjStore : ICardStore throw new InvalidOperationException("Тестовый сбой записи карточки (FailAddCard)."); } - // свежей карточки — «только что», как HumanAge(now)); стартовые комментарии/история — из снимка. _cards.Add(new CardDto { Id = snapshot.Id, @@ -257,8 +253,6 @@ public class FakeKanjStore : ICardStore /// public Task UpdateColumnAsync(CardColumnUpdateDto update, CancellationToken ct) { - // обнуляет, возврат из архива/корзины); нет карточки — no-op (валидирует сервис). ArchivedAt наружу - // не выходит (его нет в CardDto) — храним рядом в archivedAtById. int index = _cards.FindIndex(card => card.Id == update.CardId); if (index < 0) { @@ -287,7 +281,6 @@ public class FakeKanjStore : ICardStore /// public Task ApplyReclassificationAsync(CardReclassificationDto update, CancellationToken ct) { - // не трогаются; нет карточки — false (404-семантика сервиса). int index = _cards.FindIndex(card => card.Id == update.CardId); if (index < 0) { @@ -455,7 +448,6 @@ public class FakeKanjStore : ICardStore DateTimeOffset archivedAt, CancellationToken ct) { - // archived_at=archivedAt, matchHits пусто; prev_col не трогается). Возврат — число архивированных. if (cardIds.Count == 0) { return Task.FromResult(0); @@ -567,7 +559,6 @@ public class FakeKanjStore : ICardStore string convCur, CancellationToken ct) { - // convCur пуст → конверсия снята (Converted = null, как маппинг адаптера ConvCur == ""). int index = _cards.FindIndex(card => card.Id == cardId); if (index >= 0) { diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Kanban/FakePipelineStore.cs b/src/core/tests/Deal.Tests.Unit/Modules/Kanban/FakePipelineStore.cs index 05c6345..41429e3 100644 --- a/src/core/tests/Deal.Tests.Unit/Modules/Kanban/FakePipelineStore.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Kanban/FakePipelineStore.cs @@ -158,7 +158,6 @@ public class FakePipelineStore : IPipelineStore int limitLike, CancellationToken ct) { - // регистру, q НЕ приводим — если сервис забыл lower, совпадений не будет (тест ловит нормализацию). string query = q.Trim(); IReadOnlyList rows = _rejected .Where(item => LikeField(item.Text, query) @@ -295,10 +294,6 @@ public class FakePipelineStore : IPipelineStore return field.ToLowerInvariant().Contains(query, StringComparison.Ordinal); } - // record: Команда записи отсева. - // id: Id записи (детерминированный либо r_+hex). - // rejectedAtMs: Момент отсева (epoch-ms). - // Возвращает: Полная запись отсева. private static RejectedItemDto ToItem( RejectRecord record, string id, diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Kanban/SuggestHeuristicsTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Kanban/SuggestHeuristicsTests.cs index 22a325e..826d3df 100644 --- a/src/core/tests/Deal.Tests.Unit/Modules/Kanban/SuggestHeuristicsTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Kanban/SuggestHeuristicsTests.cs @@ -136,7 +136,6 @@ public sealed class SuggestHeuristicsTests [Fact] public void PlanColumns_MoreThanFourThemes_LimitsToFourColumns() { - // лексикографический: crm, mvp, php, sql (vue остаётся за лимитом). List inbox = [ InboxCard("l_c1", "нужен crm"), diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeInviteStore.cs b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeInviteStore.cs index a9b79f4..8ec25bf 100644 --- a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeInviteStore.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeInviteStore.cs @@ -67,7 +67,6 @@ public class FakeInviteStore : IInviteStore DateTimeOffset activatedAt, CancellationToken ct) { - // активирована/истекла) — false без изменений, как условный UPDATE EF-адаптера. int index = _invites.FindIndex(i => i.Code == code && i.Status == InviteStatuses.Pending); if (index < 0) { diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeTenantLimitStore.cs b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeTenantLimitStore.cs index f69bfe0..2be5a27 100644 --- a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeTenantLimitStore.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeTenantLimitStore.cs @@ -139,7 +139,6 @@ public sealed class FakeTenantLimitStore : ITenantLimitStore ResetIfPeriodExpired(row); if (tokens > 0) { - // иначе планировщик алертов не увидел бы переход порога (TryMark* = false после списания). row.UsedTokens += tokens; } diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/JoinFlowTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/JoinFlowTests.cs index c9f88b0..06c5f3b 100644 --- a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/JoinFlowTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/JoinFlowTests.cs @@ -261,7 +261,6 @@ public sealed class JoinFlowTests [Fact] public async Task ActivateAsync_SecondActivationWithSameCode_ReturnsUsedWithoutDuplicates() { - // вторая попытка видит статус activated (переход pending→activated выполнился ровно один раз). var inviteStore = new FakeInviteStore(); inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null)); var provisioner = new FakeTenantProvisioner(); @@ -283,7 +282,6 @@ public sealed class JoinFlowTests [Fact] public async Task ActivateAsync_WhenParallelRevokeWinsBeforeCas_ReturnsRevokedWithoutSideEffects() { - // проигравшая активация не создаёт пользователя/тенанта и не перезаписывает revoked. var inviteStore = new RacingInviteStore(statusBeforeActivate: InviteStatuses.Revoked); inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null)); var tenantStore = new FakeTenantStore(); diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/SuspiciousActivityServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/SuspiciousActivityServiceTests.cs index b578e5e..e56de32 100644 --- a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/SuspiciousActivityServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/SuspiciousActivityServiceTests.cs @@ -4,7 +4,7 @@ using Deal.Modules.Tenants.Application.Services; namespace Deal.Tests.Unit.Modules.Tenants; /// -/// Юнит-тесты детектора подозрительной активности +/// Юнит-тесты детектора подозрительной активности на фейковом хранилище аудита. /// public sealed class SuspiciousActivityServiceTests { diff --git a/src/core/tests/Deal.Tests.Unit/Support/AdminTickOrchestratorTests.cs b/src/core/tests/Deal.Tests.Unit/Support/AdminTickOrchestratorTests.cs index d7e501a..09e5147 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/AdminTickOrchestratorTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/AdminTickOrchestratorTests.cs @@ -100,7 +100,6 @@ public sealed class AdminTickOrchestratorTests Context ctx = CreateContext(withThrowingQueueRead: true); ctx.PipelineStore.SeedQueue(QueueRow("p_1", "Вакансия: разработчик в команду, удалённая работа, оплата 2000$")); - // Сбой чтения очереди (ListAsync бросает): pump не выполнился — тик возвращает storage/purge и AdminTickResultDto result = await ctx.Orchestrator.TickAsync(TenantA, CancellationToken.None); Assert.Equal(0, result.Storage.Archived); diff --git a/src/core/tests/Deal.Tests.Unit/Support/AiGrpcTestHost.cs b/src/core/tests/Deal.Tests.Unit/Support/AiGrpcTestHost.cs index 6e18b90..5ec5b3c 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/AiGrpcTestHost.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/AiGrpcTestHost.cs @@ -8,12 +8,6 @@ using Microsoft.Extensions.DependencyInjection; namespace Deal.Tests.Unit.Support; -// эталон MlGrpcTestHost). -// RecordingAiService (серверная сторона ai.proto) и передаёт сценарию порт + сервис-фейк: -// адаптеры (GrpcAiClassifier/GrpcAiTools) строятся на реальном канале к этому порту, поэтому проверяются -// metadata tenant-id/service-token, deadline и маппинг DTO↔proto «по проводу». DEAL_SERVICE_TOKEN задаётся -// env на время сценария (транспорт AiGrpcConnection читает его при создании); исходное значение -// восстанавливается. internal static class AiGrpcTestHost { /// diff --git a/src/core/tests/Deal.Tests.Unit/Support/BudgetAlertSchedulerTests.cs b/src/core/tests/Deal.Tests.Unit/Support/BudgetAlertSchedulerTests.cs index 603ab18..c227a05 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/BudgetAlertSchedulerTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/BudgetAlertSchedulerTests.cs @@ -93,8 +93,6 @@ public sealed class BudgetAlertSchedulerTests [Fact] public async Task RunCycle_NaturalSpendCrossing80Then100_PublishesOneToastPerThreshold() { - // 80% → ближайший проход публикует РОВНО один тост; повторный проход — без тоста; пересечение 100% (ещё - // расход) → ещё ровно один тост (100%), 80% не дублируется. FakeTenantLimitStore limits = new(); limits.Preload(TenantA, budgetTokens: 1000, TenantLimitPeriods.Month, Now(), usedTokens: 0); await using ServiceProvider provider = BuildProvider(new FakeTenantRepository(Tenant(TenantA)), limits); @@ -103,7 +101,6 @@ public sealed class BudgetAlertSchedulerTests SseSubscription subscriptionA = broker.Subscribe(TenantA); BudgetAlertScheduler scheduler = CreateScheduler(provider); - // переход остаётся непомеченным, и TryMark* на проходе вернёт true ровно один раз. await limits.AddUsageAsync(TenantA, tokens: 850, CancellationToken.None); await scheduler.RunCycleAsync(CancellationToken.None); diff --git a/src/core/tests/Deal.Tests.Unit/Support/CardsServiceSelectedTests.cs b/src/core/tests/Deal.Tests.Unit/Support/CardsServiceSelectedTests.cs index b0246d5..78845c0 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/CardsServiceSelectedTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/CardsServiceSelectedTests.cs @@ -250,7 +250,6 @@ public sealed class CardsServiceSelectedTests PatchBody(("title", null), ("summary", "Новое описание")), CancellationToken.None); - // ключа — title не тронут, summary обновлён. Assert.NotNull(card); Assert.Equal("Старый", card!.Title); Assert.Equal("Новое описание", card.Summary); diff --git a/src/core/tests/Deal.Tests.Unit/Support/ConversionRecomputerTests.cs b/src/core/tests/Deal.Tests.Unit/Support/ConversionRecomputerTests.cs index b764165..eec9445 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/ConversionRecomputerTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/ConversionRecomputerTests.cs @@ -63,7 +63,6 @@ public sealed class ConversionRecomputerTests public async Task Recompute_UsdtBudget_TreatedAsUsdRate() { (ConversionRecomputer service, FakeKanjStore store, FakeSettingsStore settings) = Create(); - // Собственный курс USDT (90) отличается от USD (100): приравнивание USDT=USD даёт 100 * 100 = 10000, var rates = new Dictionary { ["RUB"] = 1.0, ["USD"] = 100.0, ["USDT"] = 90.0 }; settings.Preload(SettingsKeys.RatesCache, CacheJson(rates)); store.SeedCard(Card("l_board", "b_1", from: 100, to: 100, cur: "USDT")); @@ -129,7 +128,6 @@ public sealed class ConversionRecomputerTests [Fact] public async Task Recompute_MissingCurrencyInRates_SkipsCardKeepingOldConversion() { - // — старые conv-поля сохраняются как были (не обнуляются). (ConversionRecomputer service, FakeKanjStore store, FakeSettingsStore settings) = Create(); settings.Preload(SettingsKeys.RatesCache, CacheJson(new Dictionary { ["RUB"] = 1.0, ["USD"] = 92.5 })); store.SeedCard(Card("l_board", "b_1", from: 100, to: 100, cur: "XXX") with @@ -149,7 +147,6 @@ public sealed class ConversionRecomputerTests [Fact] public async Task Recompute_NoRatesCache_ReturnsZeroAndKeepsCardsUntouched() { - // читается из строки rates таблицы напрямую), карточки не трогаются. (ConversionRecomputer service, FakeKanjStore store, _) = Create(); store.SeedCard(Card("l_board", "b_1", from: 100, to: 100, cur: "USD")); @@ -231,7 +228,6 @@ public sealed class ConversionRecomputerTests Budget = new CardBudgetDto(from, to, cur), }; - // rates: Курсы к рублю. private static string CacheJson(IReadOnlyDictionary rates) { var cache = new RatesCacheValue(rates, "mock", UpdatedAtMs: 1_752_000_000_000); diff --git a/src/core/tests/Deal.Tests.Unit/Support/ForwardedHeadersHttpTests.cs b/src/core/tests/Deal.Tests.Unit/Support/ForwardedHeadersHttpTests.cs index 3c422d4..e75ff22 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/ForwardedHeadersHttpTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/ForwardedHeadersHttpTests.cs @@ -13,7 +13,6 @@ namespace Deal.Tests.Unit.Support; /// public sealed class ForwardedHeadersHttpTests { - // Конечный клиент за прокси (TEST-NET-3, RFC 5737 — не маршрутизируется). private const string ForwardedClientIp = "203.0.113.7"; // Путь read-only эндпоинта, возвращающего адрес/схему, которые видит приложение. @@ -123,7 +122,6 @@ public sealed class ForwardedHeadersHttpTests [Fact] public async Task UnknownClient_ForwardedHeadersAreIgnored() { - // TEST-NET-1 (RFC 5737): адрес «другого прокси» в конфиге; реальный клиент сценария — loopback. var config = new ForwardedHeadersConfig { KnownProxies = ["192.0.2.10"] }; await RunAsync(config, async (baseAddress, client) => diff --git a/src/core/tests/Deal.Tests.Unit/Support/GrpcAiClassifierTests.cs b/src/core/tests/Deal.Tests.Unit/Support/GrpcAiClassifierTests.cs index 112c51c..109e0ea 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/GrpcAiClassifierTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/GrpcAiClassifierTests.cs @@ -308,10 +308,6 @@ public sealed class GrpcAiClassifierTests // Возвращает: JSON для Preload. private static string Json(object value) => JsonSerializer.Serialize(value); - // settings: KV-хранилище тенанта. - // prompt: Ожидаемые токены запроса. - // completion: Ожидаемые токены ответа. - // total: Ожидаемые суммарные токены. private static async Task AssertUsageAsync( FakeSettingsStore settings, uint prompt, diff --git a/src/core/tests/Deal.Tests.Unit/Support/IncomingRulesTests.cs b/src/core/tests/Deal.Tests.Unit/Support/IncomingRulesTests.cs index 856e2ac..37648e3 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/IncomingRulesTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/IncomingRulesTests.cs @@ -61,7 +61,6 @@ public sealed class IncomingRulesTests [Fact] public async Task CheckAsync_LongCleanTextWithDefaults_Passes() { - // Дефолты (stopPhrases «взаимный пиар/резюме/ищу работу/набор в команду», blockResumes, IncomingRulesResult result = await CreateRules(new FakeSettingsStore()).CheckAsync( "Заработок на крипте 300% в месяц! Подпишись на канал и получи бесплатный курс по трейдингу", CancellationToken.None); @@ -163,7 +162,6 @@ public sealed class IncomingRulesTests store.Preload(SettingsKeys.StopPhrases, Json(Array.Empty())); IncomingRules rules = CreateRules(store); - // «…вакансия…, присылайте резюме» — объявление работодателя: hire-маркер ДО «резюме» IncomingRulesResult result = await rules.CheckAsync( "Открыта вакансия senior python разработчика в офис, зарплата по итогам собеседования, " + "присылайте резюме на почту hr@example.com, рассматриваем удалённо и в гибриде", diff --git a/src/core/tests/Deal.Tests.Unit/Support/LocalColumnSuggesterTests.cs b/src/core/tests/Deal.Tests.Unit/Support/LocalColumnSuggesterTests.cs index eff3b9b..848d9bb 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/LocalColumnSuggesterTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/LocalColumnSuggesterTests.cs @@ -24,10 +24,6 @@ public sealed class LocalColumnSuggesterTests return (store, settings, suggester); } - // id: Id карточки. - // text: Исходное сообщение. - // store: Хранилище канбана. - // receivedAtMs: Время получения, epoch-ms. private static void SeedInbox( string id, string text, @@ -229,7 +225,6 @@ public sealed class LocalColumnSuggesterTests (FakeKanjStore store, _, LocalColumnSuggester suggester) = CreateContext(); SeedInbox("l_1", "нужен python", store); SeedInbox("l_2", "нужен vue", store); - // текстов вне мусора только 2 → «мало карточек». store.SeedCard(new CardDto { Id = "l_t1", Col = KanbanColumns.Trash, SourceMsg = "ищу такси" }); store.SeedCard(new CardDto { Id = "l_t2", Col = KanbanColumns.Trash, SourceMsg = "ищу такси" }); store.SeedCard(new CardDto { Id = "l_a1", Col = KanbanColumns.Archive, SourceMsg = "ищу такси" }); diff --git a/src/core/tests/Deal.Tests.Unit/Support/LocalFileStorageTests.cs b/src/core/tests/Deal.Tests.Unit/Support/LocalFileStorageTests.cs index a3d39ea..39113fa 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/LocalFileStorageTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/LocalFileStorageTests.cs @@ -71,7 +71,6 @@ public sealed class LocalFileStorageTests : IDisposable [Fact] public async Task Put_StreamAtNonZeroPosition_StoresWholeContentFromStart() { - // в начало, в файл пишется весь SampleContent, а не «хвост» от текущей позиции (как в Minio-адаптере). using MemoryStream content = new(SampleContent) { Position = SampleContent.Length / 2 }; const string objectKey = "projects/pr_1/1710000000000_full.bin"; diff --git a/src/core/tests/Deal.Tests.Unit/Support/LocalMlClientTests.cs b/src/core/tests/Deal.Tests.Unit/Support/LocalMlClientTests.cs index 7e9019a..a01ae01 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/LocalMlClientTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/LocalMlClientTests.cs @@ -320,7 +320,6 @@ public sealed class LocalMlClientTests string json = JsonSerializer.Serialize(dto, CamelCaseOptions); - // порядок объявления. Неготовая модель: take/label/scores/hits/ready/margin/terms/type. Assert.Equal( "{\"take\":false,\"label\":null,\"scores\":{},\"hits\":0,\"ready\":false," + "\"margin\":null,\"terms\":[],\"type\":null}", diff --git a/src/core/tests/Deal.Tests.Unit/Support/MessageParseCoreTests.cs b/src/core/tests/Deal.Tests.Unit/Support/MessageParseCoreTests.cs index 5e9f24a..13a7af9 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/MessageParseCoreTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/MessageParseCoreTests.cs @@ -120,7 +120,6 @@ public sealed class MessageParseCoreTests [Fact] public void NormalizeList_CleansItemsAndDropsTrash() { - // остаётся «C» — одиночная буква отбрасывается), висящие запятые/одиночные буквы/дубли — тоже. Assert.Equal( new[] { "Java", "Kotlin" }, MessageListNormalizer.NormalizeList("**Java**; Kotlin,; A; C#; Kotlin")); diff --git a/src/core/tests/Deal.Tests.Unit/Support/MlGrpcTestHost.cs b/src/core/tests/Deal.Tests.Unit/Support/MlGrpcTestHost.cs index 225d946..46618be 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/MlGrpcTestHost.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/MlGrpcTestHost.cs @@ -7,11 +7,6 @@ using Microsoft.Extensions.DependencyInjection; namespace Deal.Tests.Unit.Support; -// RecordingMlService (серверная сторона ml.proto) и передаёт сценарию порт + сервис-фейк: -// клиент (GrpcMlClient/флашер) строится на реальном канале к этому порту, поэтому проверяются metadata -// tenant-id/service-token, deadline и маппинг DTO↔proto «по проводу». DEAL_SERVICE_TOKEN задаётся env на -// время сценария (транспорт клиента MlGrpcConnection читает его при создании); исходное значение -// восстанавливается. internal static class MlGrpcTestHost { /// diff --git a/src/core/tests/Deal.Tests.Unit/Support/OperatorAuthHttpHost.cs b/src/core/tests/Deal.Tests.Unit/Support/OperatorAuthHttpHost.cs index 7014bd0..415f3da 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/OperatorAuthHttpHost.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/OperatorAuthHttpHost.cs @@ -24,16 +24,6 @@ using TenantCookieOptions = Deal.Api.Configuration.CookieOptions; namespace Deal.Tests.Unit.Support; -// OperatorSessionMiddleware и обеими auth-группами (/api/auth и /api/operator/auth) на фейк-хранилищах. -// Эталон in-process-хостов тестов (MlGrpcTestHost/TelegramIngressTestHost). Поднимается HTTP/1.1 -// (основной эндпоинт core, как :5080) на эфемерном loopback-порту; пароли — FakePasswordHasher -// (детерминированный, без Argon2). Сценарий сам создаёт HttpClient'ы (у каждого — свой CookieContainer), -// что позволяет проверять изоляцию кук deal_session/deal_operator_session в одном сценарии. -// (FakeAuditLogStore), если сценарий не передал собственный; GET /api/operator/audit также -// смонтирован (MapOperatorAuditEndpoints), чтобы HTTP-сценарии покрывали чтение ленты оператором. -// MapOperatorLimitsEndpoints и MapOperatorHealthEndpoints; для health-ручки зарегистрированы DealDbContext -// (Npgsql к dev-Postgres :5433 — SELECT 1 ручки сам переживает недоступность, см. OperatorHealthEndpoints), -// опции Services:Ml|Ai|Telegram (по умолчанию UseLocal=true — Local-режим dev) и ServiceHealthProbe. internal static class OperatorAuthHttpHost { // Строка подключения health-ручки в тестах: dev-Postgres deal-postgres (:5433), Timeout=3 — @@ -201,30 +191,22 @@ internal static class OperatorAuthHttpHost builder.Services.AddSingleton(effectiveInviteStore); builder.Services.AddSingleton(effectiveTenantStore); builder.Services.AddSingleton(effectiveLimitStore); - // Глобальные настройки оператора (ТЗ §4.1/§8.1): фейк public.global_settings + шифр секретов. builder.Services.AddSingleton(effectiveGlobalSettingsStore); builder.Services.AddSingleton(new FakeSecretCipher()); // Сервис глобальных ключей Telegram (операторские ручки /api/operator/settings/telegram-keys). builder.Services.AddScoped(); builder.Services.AddSingleton(effectiveTokenUsageStore); - // Провижининг схем тенантов — фейк (реальный TenantProvisioningService требует Postgres; нужен builder.Services.AddSingleton(); - // реестра/провижинера — проверяются авторизация и форма сводки ручки maintenance. builder.Services.AddScoped(); // Опции кук — по умолчанию (deal_session/deal_operator_session, без конфиг-секции в тесте). builder.Services.AddOptions(); builder.Services.AddOptions(); - // (SELECT 1 в try/catch с таймаутом, см. OperatorHealthEndpoints); опции сервисов — dev-default - // (UseLocal=true → services помечаются local); проба grpc.health.v1 stateless-синглтон. builder.Services.AddDbContext(options => options.UseNpgsql(TestDatabaseConnectionString)); builder.Services.AddSingleton(new MlServiceOptions()); builder.Services.AddSingleton(new AiServiceOptions()); builder.Services.AddSingleton(new TelegramServiceOptions()); builder.Services.AddSingleton(); - // Глубины очередей/сессий для health (§10.2): тот же сборщик, что и метрики в продакшене. builder.Services.AddSingleton(); - // его параметром DI); активен только при Enabled в переданных опциях (по умолчанию — выключен). - // Счётчики попыток входа — общее фейк-хранилище (public.rate_limit_counters в проде). builder.Services.AddSingleton(effectiveRateLimitOptions); builder.Services.AddSingleton(new FakeRateLimitCounterStore()); builder.Services.AddScoped(); diff --git a/src/core/tests/Deal.Tests.Unit/Support/OperatorHealthEndpointsHttpTests.cs b/src/core/tests/Deal.Tests.Unit/Support/OperatorHealthEndpointsHttpTests.cs index 236344d..8300975 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/OperatorHealthEndpointsHttpTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/OperatorHealthEndpointsHttpTests.cs @@ -54,7 +54,6 @@ public sealed class OperatorHealthEndpointsHttpTests AssertServiceLocal(FindService(services, "ai")); AssertServiceLocal(FindService(services, "telegram")); - // Глубины очередей и активные сессии (§10.2): числовые поля присутствуют всегда (0 при пустой БД). JsonElement queues = body.GetProperty("queues"); Assert.True(queues.GetProperty("pipeline").TryGetInt64(out _)); Assert.True(queues.GetProperty("mlOutbox").TryGetInt64(out _)); diff --git a/src/core/tests/Deal.Tests.Unit/Support/OriginGuardHttpTests.cs b/src/core/tests/Deal.Tests.Unit/Support/OriginGuardHttpTests.cs index cb607fc..27b45a8 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/OriginGuardHttpTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/OriginGuardHttpTests.cs @@ -17,7 +17,6 @@ public sealed class OriginGuardHttpTests // Путь мутирующего эндпоинта сценария (POST /api/...). private const string MutateProbePath = "/api/mutate"; - // Чужой Origin: cross-site атакующий сайт (тест-домен RFC 2606). private const string ForeignOrigin = "http://evil.example"; // Origin из явного allowlist конфига (Security:AllowedOrigins). diff --git a/src/core/tests/Deal.Tests.Unit/Support/PipelineWorkerSchedulerTests.cs b/src/core/tests/Deal.Tests.Unit/Support/PipelineWorkerSchedulerTests.cs index 10f0275..9d0afa0 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/PipelineWorkerSchedulerTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/PipelineWorkerSchedulerTests.cs @@ -120,7 +120,6 @@ public sealed class PipelineWorkerSchedulerTests ctx.PipelineA.SeedQueue(QueueRow("p_a_1", VacancyText, "d_a")); ctx.PipelineB.SeedQueue(QueueRow("p_b_1", VacancyText, "d_b")); - // Очередь тенанта A уже разбирает другой воркер (ручной POST /api/admin/tick): гейт занят — цикл Assert.True(ctx.PumpGate.TryEnter(TenantA)); await ctx.Scheduler.RunCycleAsync(CancellationToken.None); diff --git a/src/core/tests/Deal.Tests.Unit/Support/PipelineWorkerServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Support/PipelineWorkerServiceTests.cs index a5662f8..dcf6a70 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/PipelineWorkerServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/PipelineWorkerServiceTests.cs @@ -115,7 +115,6 @@ public sealed class PipelineWorkerServiceTests Force = force, }; - // Возвращает: Фиксированный «не готов/не уверен». private static MlPredictResultDto NotReadyPrediction() => new( Take: false, Label: null, @@ -198,7 +197,6 @@ public sealed class PipelineWorkerServiceTests Assert.Empty(result.CreatedCards); } - // ─── (2a) Глобальное исключение по слову → отсев до ML/ИИ (§5.14) ────── [Fact] public async Task Pump_GlobalExclusionKeyword_RejectsBeforeMlAndAi() @@ -298,7 +296,6 @@ public sealed class PipelineWorkerServiceTests PipelinePumpResult result = await ctx.Worker.PumpOnceAsync(default); - // Другой проход pump заявил хэш между нашей ExistsAsync-проверкой и ClaimAsync (INSERT … ON CONFLICT RacingClaimPipelineStore store = Assert.IsType(ctx.PipelineStore); Assert.Equal(1, store.ClaimCalls); // конфликт claim'а обработан, а не пропущен молча RejectedItemDto rejected = Assert.Single(ctx.PipelineStore.Rejected); @@ -512,7 +509,6 @@ public sealed class PipelineWorkerServiceTests PipelinePumpResult result = await ctx.Worker.PumpOnceAsync(default); - // Возврат из отсева: пользователь подтвердил релевантность — правила/устарело/ML не пересматриваем CardDto card = Assert.Single(ctx.KanjStore.CardDtos); Assert.Equal(KanbanColumns.Inbox, card.Col); Assert.Equal(1, result.AiStored); @@ -621,7 +617,6 @@ public sealed class PipelineWorkerServiceTests ctx.AiClassifier.ClassifyResult = Parsed("Разработчик", isVacancy: true); ctx.PipelineStore.SeedQueue(QueueRow("p_1", text)); - // продолжит следующим тиком) — сообщение не отсеивается и не теряется. await Assert.ThrowsAsync(() => ctx.Worker.PumpOnceAsync(default)); QueueItemDto row = Assert.Single(ctx.PipelineStore.Queue); @@ -659,7 +654,6 @@ public sealed class PipelineWorkerServiceTests Context ctx = CreateContext(); ctx.KanjStore.SeedBoard(new ContainerDto { Id = "b_py" }); const string text = "Вакансия: python-разработчик в команду, удалённая работа, оплата 2000$ в месяц"; - // Классификатор сам вернул is_vacancy_known=false (детерминированный LocalAiClassifier) — воркер на ctx.AiClassifier.ClassifyResult = Parsed( "Python-разработчик", budget: new AiBudgetDto(2000, 2000, "USD"), isVacancy: true, board: "b_py"); ctx.PipelineStore.SeedQueue(QueueRow("p_1", text)); diff --git a/src/core/tests/Deal.Tests.Unit/Support/RateLimitHttpTests.cs b/src/core/tests/Deal.Tests.Unit/Support/RateLimitHttpTests.cs index da19e29..5ff2ee5 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/RateLimitHttpTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/RateLimitHttpTests.cs @@ -161,7 +161,6 @@ public sealed class RateLimitHttpTests if (options.Enabled) { - // Счётчики окон — общее фейк-хранилище (public.rate_limit_counters в проде): лимиты видны builder.Services.AddSingleton(new FakeRateLimitCounterStore()); builder.Services.AddDealRateLimiter(options); } diff --git a/src/core/tests/Deal.Tests.Unit/Support/RatesServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Support/RatesServiceTests.cs index f12309f..2164dc1 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/RatesServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/RatesServiceTests.cs @@ -407,8 +407,6 @@ public sealed class RatesServiceTests return CreateService(source, Array.Empty()); } - // source: Фейк-источник курсов. - // listeners: Слушатели смены курсов. private RatesService CreateService(FakeRatesSource source, IReadOnlyList listeners) { return new RatesService(_store, source, listeners); diff --git a/src/core/tests/Deal.Tests.Unit/Support/ServiceHealthProbeTests.cs b/src/core/tests/Deal.Tests.Unit/Support/ServiceHealthProbeTests.cs index 5025948..0ddb741 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/ServiceHealthProbeTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/ServiceHealthProbeTests.cs @@ -58,7 +58,6 @@ public sealed class ServiceHealthProbeTests [Fact] public async Task Probe_SlowServer_ExceedsDeadline_ReturnsNotReachable() { - // Проверка висит дольше дедлайна пробы (3 с, ServiceHealthProbe.HealthTimeoutSeconds) — сервис жив, await RunWithHealthServerAsync( async () => { diff --git a/src/core/tests/Deal.Tests.Unit/Support/SettingsServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Support/SettingsServiceTests.cs index 63f1232..2e01aa0 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/SettingsServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/SettingsServiceTests.cs @@ -189,7 +189,6 @@ public sealed class SettingsServiceTests Assert.Equal(9999, snapshot.ArchiveClearDays); } - // ─── PATCH/GET: глобальные исключения (§5.14) ──────────────────────────── [Fact] public async Task ApplyPatchAsync_GlobalExclusions_RoundTrips() diff --git a/src/core/tests/Deal.Tests.Unit/Support/StorageTickSchedulerTests.cs b/src/core/tests/Deal.Tests.Unit/Support/StorageTickSchedulerTests.cs index 8232ca8..4921499 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/StorageTickSchedulerTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/StorageTickSchedulerTests.cs @@ -171,7 +171,6 @@ public sealed class StorageTickSchedulerTests await scheduler.RunCycleAsync(CancellationToken.None); - // 3 суток удалена безвозвратно, свежая пережила; тост «Отсев очищен» — только в канал тенанта A Assert.Equal("r_fresh", Assert.Single(pipelineStoreA.Rejected).Id); (string Text, string Icon)[] rejectedPurgeToast = [(Text: "Отсев очищен: 1 записей (3 дн.)", Icon: "trash")]; Assert.Equal(rejectedPurgeToast, ReadToasts(subscriptionA)); @@ -182,7 +181,6 @@ public sealed class StorageTickSchedulerTests [Fact] public async Task RunCycle_DueReminder_PublishesReminderDueToTenantChannelAndMarksFired() { - // Тенант A: hold-карточка с напоминанием в прошлом («выстреливает») и в будущем + не-hold с прошлым var cardStoreA = new FakeKanjStore(); cardStoreA.SeedCard(HoldCard("c_a_past", title: "Отложенный бот", reminderAtMs: NowMs() - 60_000)); cardStoreA.SeedCard(HoldCard("c_a_future", title: "Будущий", reminderAtMs: NowMs() + 60_000)); @@ -203,7 +201,6 @@ public sealed class StorageTickSchedulerTests await scheduler.RunCycleAsync(CancellationToken.None); - // тика нулевые), в канал только тенанта A (у B due нет). (string Type, string Json) dueEvent = Assert.Single(ReadEvents(subscriptionA)); Assert.Equal("reminder_due", dueEvent.Type); using JsonDocument payload = JsonDocument.Parse(dueEvent.Json); @@ -330,8 +327,6 @@ public sealed class StorageTickSchedulerTests services.AddScoped(); services.AddScoped(); services.AddScoped(); - // тенанта (как в AdminTickOrchestratorTests); сбой ветки имитируется подклассом FakeKanjStore с - // падающим ListDueRemindersAsync. services.AddScoped(); return services.BuildServiceProvider(); } @@ -379,11 +374,6 @@ public sealed class StorageTickSchedulerTests // Текущее время в epoch-мс (UTC) — для посева напоминаний в прошлом/будущем. private static long NowMs() => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); - // Карточка hold с напоминанием (как строка Cards после маппинга адаптера). - // id: Id карточки (c_...). - // title: Заголовок (ушёл в SSE reminder_due). - // reminderAtMs: Время напоминания, epoch-ms (прошлое — «выстрелит» на проходе). - // Возвращает: Карточка как DTO хранилища. private static CardDto HoldCard( string id, string title, diff --git a/src/core/tests/Deal.Tests.Unit/Support/TelegramGrpcTestHost.cs b/src/core/tests/Deal.Tests.Unit/Support/TelegramGrpcTestHost.cs index 8b19a5b..b28b869 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/TelegramGrpcTestHost.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/TelegramGrpcTestHost.cs @@ -7,11 +7,6 @@ using Microsoft.Extensions.DependencyInjection; namespace Deal.Tests.Unit.Support; -// RecordingTelegramService (серверная сторона telegram.proto) и передаёт сценарию порт + сервис-фейк: -// клиент (GrpcTelegramClient) строится на реальном канале к этому порту, поэтому проверяются metadata -// tenant-id/service-token, deadline и маппинг DTO↔proto «по проводу». DEAL_SERVICE_TOKEN задаётся env на время -// сценария (транспорт клиента TelegramGrpcConnection читает его при создании); исходное значение -// восстанавливается. internal static class TelegramGrpcTestHost { /// diff --git a/src/core/tests/Deal.Tests.Unit/Support/TelegramIngressTestHost.cs b/src/core/tests/Deal.Tests.Unit/Support/TelegramIngressTestHost.cs index 0e55a4d..dc8d258 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/TelegramIngressTestHost.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/TelegramIngressTestHost.cs @@ -22,10 +22,6 @@ using Microsoft.Extensions.Diagnostics.HealthChecks; namespace Deal.Tests.Unit.Support; -// Program.cs (AddGrpc + IngressServiceTokenInterceptor + TelegramIngressService), и tenant-фейками вместо -// БД: реестр (FakeTenantRegistry) и tenant-scoped адаптеры (IPipelineStore/ISettingsStore) выбирает -// сценарий. DEAL_SERVICE_TOKEN задаётся env на время сценария (интерцептор читает IConfiguration -// хоста); исходное значение восстанавливается. internal static class TelegramIngressTestHost { /// @@ -95,7 +91,6 @@ internal static class TelegramIngressTestHost }); if (rateLimitOptions is { Enabled: true }) { - // Счётчики окон — общий фейк-хранилище (public.rate_limit_counters в проде); лимитер — builder.Services.AddSingleton(new FakeRateLimitCounterStore()); builder.Services.AddSingleton(provider => IngressRateLimitInterceptor.CreateLimiter( @@ -107,8 +102,6 @@ internal static class TelegramIngressTestHost .AddCheck("ready", () => HealthCheckResult.Healthy("хост готов")); } - // превью). Фейки tenant-адаптеров по умолчанию — сценарий переопределяет их в configureServices - // (как ISettingsStore/IPipelineStore ниже): последняя регистрация побеждает. builder.Services.AddScoped(_ => new FakeSettingsStore()); builder.Services.AddScoped(_ => new FakeTelegramStore()); builder.Services.AddScoped(_ => new FakeTelegramGateway()); diff --git a/src/core/tests/Deal.Tests.Unit/Support/TestPort.cs b/src/core/tests/Deal.Tests.Unit/Support/TestPort.cs index 1b29d68..38d865b 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/TestPort.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/TestPort.cs @@ -4,12 +4,6 @@ using System.Net.Sockets; namespace Deal.Tests.Unit.Support; -// Заменяет размноженные копии FreeTcpPort() из тест-хостов (AiGrpcTestHost, OperatorAuthHttpHost, -// ForwardedHeadersHttpTests и т. п.). Наивная связка «биндинг 127.0.0.1:0 → освобождение» даёт редкую -// гонку при параллельном прогоне: ОС может выдать один и тот же освобождённый эфемерный порт двум тестам, и -// второй бинд падает с System.Net.Sockets.SocketException AddressInUse. Хелпер запоминает уже -// выданные в процессе порты и при коллизии повторяет биндинг порта 0 — выданные порты гарантированно не -// пересекаются между тестами. Порт после выдачи освобождается: тест биндит его сам (Kestrel/gRPC-хост). internal static class TestPort { // Максимум повторов биндинга порта 0 при коллизии с уже выданным портом. diff --git a/src/ml-service/Deal.Ml.Tests/Grpc/MlTestHost.cs b/src/ml-service/Deal.Ml.Tests/Grpc/MlTestHost.cs index 4c9ad86..2aaf3b8 100644 --- a/src/ml-service/Deal.Ml.Tests/Grpc/MlTestHost.cs +++ b/src/ml-service/Deal.Ml.Tests/Grpc/MlTestHost.cs @@ -11,9 +11,6 @@ using Microsoft.AspNetCore.Builder; namespace Deal.Ml.Tests.Grpc; -// процессе теста на эфемерном порту и направляет каталог моделей (DEAL_ML_DATA_DIR) во временную -// папку — файлы data/ml/<tenant>.sqlite тестов не попадают в репозиторий. Исходные значения -// env восстанавливаются; каталог удаляется после сценария (шаблон TelegramTestHost). internal static class MlTestHost { /// diff --git a/src/ml-service/Deal.Ml.Tests/Ml/LearningData.cs b/src/ml-service/Deal.Ml.Tests/Ml/LearningData.cs index 2a71517..fefdfb3 100644 --- a/src/ml-service/Deal.Ml.Tests/Ml/LearningData.cs +++ b/src/ml-service/Deal.Ml.Tests/Ml/LearningData.cs @@ -2,8 +2,6 @@ using Deal.Ml.Model; namespace Deal.Ml.Tests.Ml; -// намеренно не пересекаются: колонка/тип разработки (Dev), разовая сделка (Order) и спам — -// разные термины, чтобы предсказания были детерминированы порогами, а не шумом пересечений. internal static class LearningData { /// diff --git a/src/ml-service/Deal.Ml.Tests/Ml/TenantModelLearningTests.cs b/src/ml-service/Deal.Ml.Tests/Ml/TenantModelLearningTests.cs index 1834403..e508100 100644 --- a/src/ml-service/Deal.Ml.Tests/Ml/TenantModelLearningTests.cs +++ b/src/ml-service/Deal.Ml.Tests/Ml/TenantModelLearningTests.cs @@ -96,7 +96,6 @@ public sealed class TenantModelLearningTests TenantModel model = pool.GetOrCreate("tenant-canonical"); List items = LearningData.CanonicalTrainItems(); - // затем «реальные действия» (4) — они и дают строки самооценки. int learnedFirst = model.LearnBatch(items.Take(24).ToList()); int learnedSecond = model.LearnBatch(items.Skip(24).ToList()); diff --git a/src/ml-service/Deal.Ml/MlServiceHost.cs b/src/ml-service/Deal.Ml/MlServiceHost.cs index 5254c5f..2468153 100644 --- a/src/ml-service/Deal.Ml/MlServiceHost.cs +++ b/src/ml-service/Deal.Ml/MlServiceHost.cs @@ -27,14 +27,11 @@ public static class MlServiceHost { WebApplicationBuilder builder = WebApplication.CreateBuilder(args ?? []); - // Общая серверная обвязка (Deal.Grpc.Hosting, C31): mTLS env DEAL_MTLS_* — загрузка - // сертификатов сразу с fail-fast (compose-prod монтирует deploy/certs, scripts/mtls-certs.sh); MtlsCertificates? mtlsCertificates = GrpcServer.LoadMtlsCertificates(builder); GrpcServer.ConfigureKestrelHttp2Endpoint(builder, grpcPort, mtlsCertificates); builder.Services.AddDealGrpcServer(); builder.Services.AddReadyHealthCheck("хост ml-service готов"); - // (env DEAL_ML_DATA_DIR; по умолчанию data/ml под ContentRoot) + пул моделей per-tenant. MlOptions mlOptions = MlOptions.FromConfiguration(builder.Configuration, builder.Environment); builder.Services.AddSingleton(mlOptions); builder.Services.AddSingleton(); diff --git a/src/ml-service/Deal.Ml/MlServiceImpl.cs b/src/ml-service/Deal.Ml/MlServiceImpl.cs index d792fe3..9e1890b 100644 --- a/src/ml-service/Deal.Ml/MlServiceImpl.cs +++ b/src/ml-service/Deal.Ml/MlServiceImpl.cs @@ -182,7 +182,6 @@ public sealed class MlServiceImpl : MlService.MlServiceBase } } - // context: Контекст вызова. private static string RequireTenantId(ServerCallContext context) { string? tenantId = context.RequestHeaders.GetValue(TenantIdMetadataKey); @@ -208,7 +207,6 @@ public sealed class MlServiceImpl : MlService.MlServiceBase } } - // result: Результат предсказания. private static PredictReply ToPredictReply(MlPredictResult result) { var reply = new PredictReply @@ -249,7 +247,6 @@ public sealed class MlServiceImpl : MlService.MlServiceBase return reply; } - // result: Результат статуса. private static StatusReply ToStatusReply(MlStatusResult result) { var reply = new StatusReply diff --git a/src/ml-service/Deal.Ml/Model/OnlineNaiveBayes.cs b/src/ml-service/Deal.Ml/Model/OnlineNaiveBayes.cs index 0fa6469..6670d65 100644 --- a/src/ml-service/Deal.Ml/Model/OnlineNaiveBayes.cs +++ b/src/ml-service/Deal.Ml/Model/OnlineNaiveBayes.cs @@ -68,7 +68,6 @@ public static class OnlineNaiveBayes return NotReady(Ready(state)); } - // Модель «включается» только с опытом: пока примеров мало, она ничего не решает if (!Ready(state)) { return NotReady(ready: false); @@ -177,15 +176,9 @@ public static class OnlineNaiveBayes return new MlStatusResult(ready, classes, learned, EvalWindow(state)); } - // weight: Вес (count) термина в классе. private static double TermScore(double weight) => weight < 1.0 ? 1.0 : 1.0 + (weight - 1.0) / (weight + 1.0); - // опытом ≥ MIN_TYPE_WINNER и отрывом best_total − second_total ≥ margin. - // state: Состояние модели. - // scores: Веса классов по тексту (label → score). - // prior: Априорные доли классов. - // margin: Адаптивный порог отрыва. private static MlTypeDecision? DecideType( ModelState state, IReadOnlyDictionary scores, @@ -226,10 +219,6 @@ public static class OnlineNaiveBayes Margin: Round(margin, ModelConstants.MarginPrecision)); } - // для структурирования карточки без ИИ. Хвостовые «~»-термины исключаются; до 8 по весу. - // state: Состояние модели. - // label: Класс-победитель (не spam). - // tokens: Термины текста (в порядке появления). private static IReadOnlyList MatchTerms( ModelState state, string label, @@ -262,7 +251,6 @@ public static class OnlineNaiveBayes .ToArray(); } - // state: Состояние модели. private static MlEvalInfo EvalWindow(ModelState state) { int count = Math.Min(state.EvalLog.Count, ModelConstants.EvalWindowSize); @@ -274,8 +262,6 @@ public static class OnlineNaiveBayes return new MlEvalInfo(count, correct, accuracy); } - // state: Состояние модели. - // label: Метка класса. private static Dictionary? TryGetTerms(ModelState state, string label) => state.TermsByLabel.TryGetValue(label, out Dictionary? terms) ? terms : null; @@ -306,7 +292,5 @@ public static class OnlineNaiveBayes Terms: Array.Empty(), Type: typeDecision); - // value: Значение. - // digits: Число знаков после запятой. private static double Round(double value, int digits) => Math.Round(value, digits); } diff --git a/src/ml-service/Deal.Ml/Model/TenantModel.cs b/src/ml-service/Deal.Ml/Model/TenantModel.cs index ecfb2c9..e8c4da8 100644 --- a/src/ml-service/Deal.Ml/Model/TenantModel.cs +++ b/src/ml-service/Deal.Ml/Model/TenantModel.cs @@ -89,7 +89,6 @@ public sealed class TenantModel : IDisposable return 0; } - // Сначала персистентность (одна транзакция; при сбое память не менялась), затем — _db.ApplyLearnBatch(applied, evalRows); foreach ((string label, double delta, string[] tokens) in applied) { @@ -117,11 +116,6 @@ public sealed class TenantModel : IDisposable /// public void Dispose() => _db.Dispose(); - // сверяем его с действием и пишем строку журнала. Гипотезы ИИ (delta<1) и «разучивание» - // (delta<0) не оцениваются. - // label: Метка действия пользователя. - // text: Текст примера. - // delta: Вес сигнала. private EvalEntry? TrySelfEval( string label, string text, @@ -147,10 +141,6 @@ public sealed class TenantModel : IDisposable string.Equals(prediction.Label, label, StringComparison.Ordinal)); } - // обновляются до применения удаления «обнулённых» строк при delta < 0. Удалённый класс - // label: Метка класса. - // delta: Вес сигнала. - // tokens: Термины текста (повторы — повторы инкрементов). private void ApplyToMemory( string label, double delta, @@ -194,7 +184,6 @@ public sealed class TenantModel : IDisposable } } - // rows: Новые строки (уже в порядке накопления). private void AppendEvalToMemory(IReadOnlyList rows) { if (rows.Count == 0) diff --git a/src/ml-service/Deal.Ml/Program.cs b/src/ml-service/Deal.Ml/Program.cs index abf43b5..815e527 100644 --- a/src/ml-service/Deal.Ml/Program.cs +++ b/src/ml-service/Deal.Ml/Program.cs @@ -1,10 +1,4 @@ // -// Kestrel HTTP/2 на порту 5103 (env GRPC_PORT, затем PORT) + AddGrpc с интерцепторами service-token -// и access-лога + стандартный gRPC-health (grpc.health.v1.Health). Транспорт: dev — plaintext -// Production без mTLS не стартует (GrpcHostEnvironment.RequireMtlsInProduction). -// Серверная обвязка (Kestrel/AddGrpc/health) — общий Deal.Grpc.Hosting (C31): хост-фабрика -// MlServiceHost.Create используется и интеграционными тестами (Deal.Ml.Tests), которые поднимают -// в Model/, хранилище в Storage/, gRPC Predict/Status/Reset/TrainBatch — MlServiceImpl. using Deal.Grpc.Hosting.Interceptors; using Deal.Grpc.Hosting.Models; @@ -12,16 +6,12 @@ using Deal.Grpc.Hosting.Options; using Deal.Grpc.Hosting.Services; using Deal.Ml; -// или PORT (общий конвенциональный env хостинг-платформ) — см. GrpcHostEnvironment.ResolveGrpcPort. const int defaultGrpcPort = 5103; const string mlProcessName = "ml"; int grpcPort = GrpcHostEnvironment.ResolveGrpcPort(defaultGrpcPort); int metricsPort = DealMetricsHosting.ResolveMetricsPort(DealMetricsHosting.DefaultMetricsPort); -// конфигурируется production-точкой входа через configureBuilder-хук хоста (тесты хост поднимают -// без Serilog, DealLogging.Configure в MlServiceHost/Create вызывается только здесь). Метрики -// (OTel → Prometheus, /metrics) — тем же хуком до builder.Build(). WebApplication app = MlServiceHost.Create( grpcPort, configureBuilder: builder => diff --git a/src/ml-service/Deal.Ml/Storage/MlDb.cs b/src/ml-service/Deal.Ml/Storage/MlDb.cs index 86a5e0b..42bba98 100644 --- a/src/ml-service/Deal.Ml/Storage/MlDb.cs +++ b/src/ml-service/Deal.Ml/Storage/MlDb.cs @@ -177,10 +177,6 @@ public sealed class MlDb : IDisposable /// public void Dispose() => DisposeConnection(); - // пакетные upsert терминов; при delta < 0 — удаление строк count ≤ 0 и классов n ≤ 0. - // transaction: Транзакция батча. - // label: Метка класса. - // delta: Вес сигнала (знак — учить/разучивать). private void ApplyExample( SqliteTransaction transaction, string label, @@ -260,10 +256,6 @@ public sealed class MlDb : IDisposable } } - // Python удаляет по created_at и при одинаковых миллисекундах может оставить больше EVAL_KEEP; - // здесь прунинг детерминирован по порядку вставки (rowid) — память модели хранит те же самые - // последние EVAL_KEEP строк, поэтому состояние памяти и файла не расходится после перезапуска. - // transaction: Транзакция батча. private void PruneEvalLog(SqliteTransaction transaction) { using SqliteCommand command = _connection!.CreateCommand(); diff --git a/src/telegram-service/Deal.Telegram.Tests/Grpc/FakeIngressServer.cs b/src/telegram-service/Deal.Telegram.Tests/Grpc/FakeIngressServer.cs index 151f92c..1b330d4 100644 --- a/src/telegram-service/Deal.Telegram.Tests/Grpc/FakeIngressServer.cs +++ b/src/telegram-service/Deal.Telegram.Tests/Grpc/FakeIngressServer.cs @@ -9,8 +9,6 @@ using Microsoft.Extensions.DependencyInjection; namespace Deal.Telegram.Tests.Grpc; -// Фейковая реализация IngressService (сервер ядра) для in-proc проверки клиента ингресса -// (tenant-id/service-token), отвечает по протоколу без логики ядра. internal sealed class RecordingIngressService : IngressService.IngressServiceBase { /// @@ -56,7 +54,6 @@ internal sealed class RecordingIngressService : IngressService.IngressServiceBas return Task.FromResult(reply); } - // context: Контекст вызова gRPC. private void Record(ServerCallContext context) { Tenants.Add(context.RequestHeaders.GetValue("tenant-id") ?? string.Empty); diff --git a/src/telegram-service/Deal.Telegram.Tests/Grpc/TestDoubles.cs b/src/telegram-service/Deal.Telegram.Tests/Grpc/TestDoubles.cs index b8bdcc9..73aaf73 100644 --- a/src/telegram-service/Deal.Telegram.Tests/Grpc/TestDoubles.cs +++ b/src/telegram-service/Deal.Telegram.Tests/Grpc/TestDoubles.cs @@ -4,9 +4,6 @@ using Deal.Telegram.Dialogs; namespace Deal.Telegram.Tests.Grpc; -// Фейк исходящего канала в ядро (ICoreIngressClient) для unit-тестов служб каталога: записывает -// PushMessage/SyncDialogs без сети; SyncDialogs возвращает настраиваемый monitored-набор (роль ядра, -// in-proc фейк-сервера ингресса (FakeIngressServer). internal sealed class FakeIngress : ICoreIngressClient { /// @@ -75,7 +72,6 @@ internal sealed class FakeIngress : ICoreIngressClient } } -// Фейк анти-бан-пейсера: паузы не ждёт, а записывает запрошенные диапазоны (сек) — проверка internal sealed class RecordingPacer : IBackfillPacer { /// diff --git a/src/telegram-service/Deal.Telegram.Tests/Telegram/FakeSessionClient.cs b/src/telegram-service/Deal.Telegram.Tests/Telegram/FakeSessionClient.cs index 59f153c..e48a21d 100644 --- a/src/telegram-service/Deal.Telegram.Tests/Telegram/FakeSessionClient.cs +++ b/src/telegram-service/Deal.Telegram.Tests/Telegram/FakeSessionClient.cs @@ -2,8 +2,6 @@ using Deal.Telegram.Telegram; namespace Deal.Telegram.Tests.Telegram; -// абстракции ISessionClient; без сети). Поведение задаётся скриптом: ошибки/результаты кода/пароля, -// QR-вход ждёт явного «сканирования» (CompleteQrScanAsync) — тест управляет моментом авторизации. internal sealed class FakeSessionClient : ISessionClient { /// diff --git a/src/telegram-service/Deal.Telegram.Tests/Telegram/SessionHarness.cs b/src/telegram-service/Deal.Telegram.Tests/Telegram/SessionHarness.cs index 78828b6..c1ab0d4 100644 --- a/src/telegram-service/Deal.Telegram.Tests/Telegram/SessionHarness.cs +++ b/src/telegram-service/Deal.Telegram.Tests/Telegram/SessionHarness.cs @@ -3,8 +3,6 @@ using Microsoft.Extensions.Logging.Abstractions; namespace Deal.Telegram.Tests.Telegram; -// фейковая фабрика клиентов и сессия, возобновлённая из «сохранённой авторизованной сессии» -// (TryResumeAsync → фаза ready, без сети и QR-флоу). Фейк-клиент после создания настраивается тестом. internal sealed class SessionHarness : IAsyncDisposable { /// diff --git a/src/telegram-service/Deal.Telegram/Core/CoreIngressClient.cs b/src/telegram-service/Deal.Telegram/Core/CoreIngressClient.cs index 8f15a3a..21347dd 100644 --- a/src/telegram-service/Deal.Telegram/Core/CoreIngressClient.cs +++ b/src/telegram-service/Deal.Telegram/Core/CoreIngressClient.cs @@ -109,7 +109,6 @@ public sealed class CoreIngressClient : ICoreIngressClient } } - // tenantId: Id тенанта. private CallOptions CallOptions(string tenantId) { var metadata = new Metadata @@ -120,8 +119,6 @@ public sealed class CoreIngressClient : ICoreIngressClient return new CallOptions(metadata, deadline: DateTime.UtcNow.AddSeconds(CoreIngressOptions.RpcTimeoutSeconds)); } - // Переводит сбой вызова в SessionException (UNAVAILABLE) со структурированным логом. - // exception: Исключение вызова. private SessionException Fail(string action, Exception exception) { _logger.LogWarning(exception, "Аудит: ингресс ядра {Action} → сбой ({Endpoint})", action, _options.IngressEndpoint); diff --git a/src/telegram-service/Deal.Telegram/Dialogs/BackfillService.cs b/src/telegram-service/Deal.Telegram/Dialogs/BackfillService.cs index 3ee111c..c0ae0fd 100644 --- a/src/telegram-service/Deal.Telegram/Dialogs/BackfillService.cs +++ b/src/telegram-service/Deal.Telegram/Dialogs/BackfillService.cs @@ -122,8 +122,6 @@ public sealed class BackfillService } } - // tenantId: Id тенанта. - // cancellationToken: Отмена операции. private async Task EnsureDialogSpacingAsync(string tenantId, CancellationToken cancellationToken) { DateTime? lastFinishUtc; @@ -181,7 +179,6 @@ public sealed class BackfillService await _pacer.WaitAsync(MinPerMessageDelaySeconds, MaxPerMessageDelaySeconds, cancellationToken).ConfigureAwait(false); } - // не выполняется: неотправленное останется непрочитанным и догонится realtime_sweep. await _sessionFarm.MarkReadAsync(tenantId, dialogId, cancellationToken).ConfigureAwait(false); return processed; } diff --git a/src/telegram-service/Deal.Telegram/Dialogs/DialogCatalog.cs b/src/telegram-service/Deal.Telegram/Dialogs/DialogCatalog.cs index 0766c12..fa12baa 100644 --- a/src/telegram-service/Deal.Telegram/Dialogs/DialogCatalog.cs +++ b/src/telegram-service/Deal.Telegram/Dialogs/DialogCatalog.cs @@ -171,7 +171,6 @@ public sealed class DialogCatalog } } - // tenantId: Id тенанта. private HashSet KnownSet(string tenantId) { if (!_knownByTenant.TryGetValue(tenantId, out HashSet? known)) @@ -183,7 +182,6 @@ public sealed class DialogCatalog return known; } - // tenantId: Id тенанта. private HashSet MonitoredSet(string tenantId) { if (!_monitoredByTenant.TryGetValue(tenantId, out HashSet? monitored)) diff --git a/src/telegram-service/Deal.Telegram/Dialogs/RealtimeSweep.cs b/src/telegram-service/Deal.Telegram/Dialogs/RealtimeSweep.cs index e7d31fb..db8b4b0 100644 --- a/src/telegram-service/Deal.Telegram/Dialogs/RealtimeSweep.cs +++ b/src/telegram-service/Deal.Telegram/Dialogs/RealtimeSweep.cs @@ -130,9 +130,6 @@ public sealed class RealtimeSweep } } - // tenantId: Id тенанта. - // dialog: Диалог с unread_count > 0. - // cancellationToken: Отмена операции. private async Task SweepDialogAsync( string tenantId, TelegramDialog dialog, @@ -166,7 +163,6 @@ public sealed class RealtimeSweep added); } - // цикл увидит unread снова и дочитает то, что не ушло в ядро. await _sessionFarm.MarkReadAsync(tenantId, dialog.Id, cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) diff --git a/src/telegram-service/Deal.Telegram/Discovery/DiscoveryOps.cs b/src/telegram-service/Deal.Telegram/Discovery/DiscoveryOps.cs index 42642d0..d7e3c36 100644 --- a/src/telegram-service/Deal.Telegram/Discovery/DiscoveryOps.cs +++ b/src/telegram-service/Deal.Telegram/Discovery/DiscoveryOps.cs @@ -142,8 +142,6 @@ public sealed class DiscoveryOps public static string NormalizeUsername(string? username) => (username ?? string.Empty).Trim().TrimStart('@'); - // found: Результат поиска сессии (chats затем users). - // limit: Верхняя граница числа записей. private static IReadOnlyList DedupeAndCap(IReadOnlyList found, int limit) { var seen = new HashSet(StringComparer.Ordinal); diff --git a/src/telegram-service/Deal.Telegram/Program.cs b/src/telegram-service/Deal.Telegram/Program.cs index 3e86b03..aa430b7 100644 --- a/src/telegram-service/Deal.Telegram/Program.cs +++ b/src/telegram-service/Deal.Telegram/Program.cs @@ -1,9 +1,4 @@ // -// Kestrel HTTP/2 на порту 5101 (env GRPC_PORT, затем PORT) + AddGrpc с интерцепторами service-token -// и access-лога + стандартный gRPC-health (grpc.health.v1.Health). Транспорт: dev — plaintext -// Production без mTLS не стартует (GrpcHostEnvironment.RequireMtlsInProduction). -// Серверная обвязка (Kestrel/AddGrpc/health) — общий Deal.Grpc.Hosting (C31): хост-фабрика -// TelegramServiceHost.Create используется и интеграционными тестами (Deal.Telegram.Tests), которые using Deal.Grpc.Hosting.Interceptors; using Deal.Grpc.Hosting.Models; @@ -11,16 +6,12 @@ using Deal.Grpc.Hosting.Options; using Deal.Grpc.Hosting.Services; using Deal.Telegram; -// или PORT (общий конвенциональный env хостинг-платформ) — см. GrpcHostEnvironment.ResolveGrpcPort. const int defaultGrpcPort = 5101; const string telegramProcessName = "telegram"; int grpcPort = GrpcHostEnvironment.ResolveGrpcPort(defaultGrpcPort); int metricsPort = DealMetricsHosting.ResolveMetricsPort(DealMetricsHosting.DefaultMetricsPort); -// конфигурируется production-точкой входа через configureBuilder-хук хоста (тесты хост поднимают -// без Serilog, DealLogging.Configure в TelegramServiceHost/Create вызывается только здесь). Метрики -// (OTel → Prometheus, /metrics) — тем же хуком до builder.Build(). WebApplication app = TelegramServiceHost.Create( grpcPort, configureBuilder: builder => diff --git a/src/telegram-service/Deal.Telegram/Sessions/TenantSession.cs b/src/telegram-service/Deal.Telegram/Sessions/TenantSession.cs index 6c841d2..4988ffc 100644 --- a/src/telegram-service/Deal.Telegram/Sessions/TenantSession.cs +++ b/src/telegram-service/Deal.Telegram/Sessions/TenantSession.cs @@ -197,7 +197,6 @@ public sealed class TenantSession : IAsyncDisposable _qrUrl = null; } - // LoginWithQRCode продолжала бы авторизацию «скрыто» после отмены RPC (замечание code-review). CancelQrFlow(); throw; @@ -698,7 +697,6 @@ public sealed class TenantSession : IAsyncDisposable try { - // Собственный лимит попытки: linked-токен с CancelAfter на время попытки переподключения. using CancellationTokenSource attemptTimeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); attemptTimeout.CancelAfter(_reconnectAttemptTimeout); @@ -838,8 +836,6 @@ public sealed class TenantSession : IAsyncDisposable AttachClientMessages(_client); } - // Сбой get_me не отменяет готовность — сохраняем сессию без account (готовность важнее имени). - // cancellationToken: Отмена операции. private async Task CompleteAuthorizationAsync(CancellationToken cancellationToken) { if (_client is null) @@ -1015,8 +1011,6 @@ public sealed class TenantSession : IAsyncDisposable } } - // Обработка ошибки QR после того, как URL уже был выдан (RPC вернулся): фаза idle + текст ошибки - // exception: Ошибка QR-входа. private async Task FailQrUnderGateAsync(SessionException exception) { await _gate.WaitAsync().ConfigureAwait(false); diff --git a/src/telegram-service/Deal.Telegram/Telegram/TlMessageMapper.cs b/src/telegram-service/Deal.Telegram/Telegram/TlMessageMapper.cs index 0d8f729..9e48792 100644 --- a/src/telegram-service/Deal.Telegram/Telegram/TlMessageMapper.cs +++ b/src/telegram-service/Deal.Telegram/Telegram/TlMessageMapper.cs @@ -179,8 +179,6 @@ public static class TlMessageMapper _ => throw new SessionException(StatusCode.InvalidArgument, SessionErrorMessages.InvalidDialogId), }; - // date: Дата сообщения. - // now: Текущее время (фолбэк). private static long ToEpochMsOrNow(DateTime date, DateTimeOffset now) => date == default ? now.ToUnixTimeMilliseconds() : ToEpochMs(date); @@ -201,7 +199,6 @@ public static class TlMessageMapper private static UserBase? FindUser(Peer peer, IReadOnlyDictionary users) => peer is PeerUser user && users.TryGetValue(user.user_id, out User? regularUser) ? regularUser : null; - // date: Время сообщения. private static long ToEpochMs(DateTime date) => new DateTimeOffset(DateTime.SpecifyKind(date, DateTimeKind.Utc)).ToUnixTimeMilliseconds(); } diff --git a/src/telegram-service/Deal.Telegram/Telegram/WTelegramSessionClient.cs b/src/telegram-service/Deal.Telegram/Telegram/WTelegramSessionClient.cs index b1d0567..15a4cfa 100644 --- a/src/telegram-service/Deal.Telegram/Telegram/WTelegramSessionClient.cs +++ b/src/telegram-service/Deal.Telegram/Telegram/WTelegramSessionClient.cs @@ -9,7 +9,6 @@ using RpcException = TL.RpcException; namespace Deal.Telegram.Telegram; #pragma warning disable CS0618 // Auth_SendCode/Auth_SignIn используются осознанно: ручной веб-вход 1:1 с прототипом -// LoginUserIfNeeded, который умеет только интерактивный конфиг-ввод, а не наш пошаговый API. /// /// Реальная реализация поверх WTelegramClient. @@ -269,7 +268,6 @@ public sealed class WTelegramSessionClient : ISessionClient public async Task MarkReadAsync(string dialogId, CancellationToken cancellationToken) { InputPeer peer = await ResolvePeerAsync(dialogId, cancellationToken).ConfigureAwait(false); - // Generic-хелпер библиотеки: для канала — channels.readHistory, иначе — messages.readHistory; await RunTlCallAsync(() => _client.ReadHistory(peer), cancellationToken).ConfigureAwait(false); } @@ -301,7 +299,6 @@ public sealed class WTelegramSessionClient : ISessionClient /// public async Task GetInfoAsync(string dialogId, CancellationToken cancellationToken) { - // чат дают инфо по умолчанию (name=id, kind пуст, participants пуст), сбой участников не роняет TelegramSourceInfo unknown = DefaultSourceInfo(dialogId); if (!TryParseSignedId(dialogId, out bool isChannel, out bool isChat, out bool isUser, out long rawId)) { @@ -409,11 +406,6 @@ public sealed class WTelegramSessionClient : ISessionClient await RunTlCallAsync(() => _client.Channels_LeaveChannel(channel), cancellationToken).ConfigureAwait(false); } - // Инфо о канале/супергруппе: entity из кэша/полного чата, участники — GetFullChannel best-effort - // dialogId: Подписанный id (для имени по умолчанию). - // rawId: Raw id канала. - // unknown: Инфо по умолчанию (сущность недоступна). - // cancellationToken: Отмена операции. private async Task GetChannelInfoAsync( string dialogId, long rawId, @@ -446,9 +438,6 @@ public sealed class WTelegramSessionClient : ISessionClient return DescribeChannel(dialogId, channel, fullParticipants); } - // dialogId: Подписанный id. - // rawId: Raw id канала. - // cancellationToken: Отмена операции. private async Task TryFetchChannelParticipantsAsync( string dialogId, long rawId, @@ -505,8 +494,6 @@ public sealed class WTelegramSessionClient : ISessionClient return DescribeGroup(dialogId, group, fullParticipants); } - // rawId: Raw id группы. - // cancellationToken: Отмена операции. private async Task TryFetchBasicParticipantsAsync(long rawId, CancellationToken cancellationToken) { try @@ -539,9 +526,6 @@ public sealed class WTelegramSessionClient : ISessionClient participants, isForum: false); - // Инфо о личном чате/боте: имя из кэша сущности (полного чата у людей нет). - // dialogId: Подписанный id. - // rawId: Raw id пользователя. private TelegramSourceInfo GetUserInfo( string dialogId, long rawId, @@ -568,10 +552,6 @@ public sealed class WTelegramSessionClient : ISessionClient return new TelegramSourceInfo(dialogId, name, username, DialogKinds.Chat, participants: null, isForum: false); } - // peer: Peer форума. - // limit: Размер выборки (раскладывается по темам). - // cancellationToken: Отмена операции. - // Возвращает: Плоский список сообщений тем (пуст — темы не прочитались; фолбэк на ленту делает вызывающий). private async Task> ReadForumTopicsAsync( InputPeer peer, int limit, @@ -663,15 +643,12 @@ public sealed class WTelegramSessionClient : ISessionClient participants, isForum: (channel.flags & Channel.Flags.forum) != 0); - // dialogId: Подписанный id (фолбэк имени). - // chat: Сущность чата/канала. private static string DisplayName(string dialogId, ChatBase chat) { string title = chat.Title ?? string.Empty; return title.Length > 0 ? title : dialogId; } - // rawId: Raw id канала. private bool IsForumChannel(long rawId) { lock (_entityCacheGate) @@ -687,15 +664,6 @@ public sealed class WTelegramSessionClient : ISessionClient private const int ForumMessagesPerTopicCap = 10; - // последовательно на каждое обновление в правильном порядке (без пропусков/дублей по pts). - // Все типы новых сообщений библиотека нормализует в TL.UpdateNewMessage: - // * UpdateNewChannelMessage (каналы/супергруппы) — подкласс UpdateNewMessage; - // * UpdateShortMessage/UpdateShortChatMessage — синтез UpdateNewMessage в UpdateList (TL.Xtended); - // * UpdateShortSentMessage (собственные исходящие) менеджер не поднимает. - // Ниже — только фильтр «новое входящее текстовое сообщение» и подъём события MessageReceived; - // фильтр по зеркалу мониторинга и read-ack делает служба каталога (единый downstream). - // Ошибки отдельного события не роняют цикл (подписчиков изолирует TenantSession). - // update: Одно нормализованное обновление. private async Task OnSingleUpdateAsync(Update update) { try @@ -800,10 +768,6 @@ public sealed class WTelegramSessionClient : ISessionClient } } - // Переводит RpcException Telegram в SessionException: FloodWait — RESOURCE_EXHAUSTED - // (detail с префиксом "flood", контракт telegram.proto); 400-ошибки входных данных — - // остальные серверные/сетевые сбои — UNAVAILABLE «Telegram недоступен…» (безопасный повтор). - // exception: Исключение RPC Telegram. private static SessionException MapRpcException(RpcException exception) { if (exception.Message.StartsWith("FLOOD_WAIT_", StringComparison.Ordinal)) diff --git a/src/telegram-service/Deal.Telegram/TelegramServiceHost.cs b/src/telegram-service/Deal.Telegram/TelegramServiceHost.cs index 18f5584..c4a59c6 100644 --- a/src/telegram-service/Deal.Telegram/TelegramServiceHost.cs +++ b/src/telegram-service/Deal.Telegram/TelegramServiceHost.cs @@ -32,18 +32,11 @@ public static class TelegramServiceHost { WebApplicationBuilder builder = WebApplication.CreateBuilder(args ?? []); - // Общая серверная обвязка (Deal.Grpc.Hosting, C31): mTLS env DEAL_MTLS_* — загрузка - // сертификатов сразу с fail-fast (compose-prod монтирует deploy/certs, scripts/mtls-certs.sh); - // Один экземпляр mtlsCertificates используют и Kestrel ниже, и исходящий канал в ядро - // (CoreIngressClient). MtlsCertificates? mtlsCertificates = GrpcServer.LoadMtlsCertificates(builder); GrpcServer.ConfigureKestrelHttp2Endpoint(builder, grpcPort, mtlsCertificates); builder.Services.AddDealGrpcServer(); builder.Services.AddReadyHealthCheck("хост telegram-service готов"); - // файлов data/sessions/.session (AES-GCM, ключ из env), пул 1 аккаунт/тенант и - // фоновый цикл auto_resume/heartbeat (30 с). DEAL_TELEGRAM_SESSION_KEY обязателен — - // иначе хост не стартует (сессии не могут храниться в открытом виде). TgOptions sessionOptions = TgOptions.FromConfiguration(builder.Configuration, builder.Environment); builder.Services.AddSingleton(sessionOptions); builder.Services.AddSingleton(); diff --git a/src/telegram-service/Deal.Telegram/TelegramServiceImpl.cs b/src/telegram-service/Deal.Telegram/TelegramServiceImpl.cs index 4aee187..6e0227d 100644 --- a/src/telegram-service/Deal.Telegram/TelegramServiceImpl.cs +++ b/src/telegram-service/Deal.Telegram/TelegramServiceImpl.cs @@ -205,7 +205,6 @@ public sealed class TelegramServiceImpl : TelegramService.TelegramServiceBase List entries = dialogs.Select(DialogProtoMapper.ToEntry).ToList(); _catalog.ReplaceKnown(tenantId, dialogs.Select(dialog => dialog.Id).ToList()); - // ответ — зеркало догонит realtime_sweep следующим циклом. await SyncCatalogSilentlyAsync(tenantId, entries, ct).ConfigureAwait(false); var result = new RefreshDialogsReply(); @@ -405,7 +404,6 @@ public sealed class TelegramServiceImpl : TelegramService.TelegramServiceBase return new LeaveReply { Ok = true }; } - // context: Контекст вызова. private static string RequireTenantId(ServerCallContext context) { string? tenantId = context.RequestHeaders.GetValue(TenantIdMetadataKey); @@ -447,10 +445,6 @@ public sealed class TelegramServiceImpl : TelegramService.TelegramServiceBase } } - // Best-effort-синхронизация зеркала мониторинга с ядром (SyncDialogs): сбой (ядро недоступно) - // tenantId: Id тенанта. - // entries: Актуальный каталог диалогов. - // cancellationToken: Отмена операции. private async Task SyncCatalogSilentlyAsync( string tenantId, IReadOnlyList entries, @@ -467,11 +461,6 @@ public sealed class TelegramServiceImpl : TelegramService.TelegramServiceBase } } - // Исполняет операцию сессии с единым переводом ошибок: SessionException → RPC-статус контракта, - // TResult: Тип результата операции. - // tenantId: Id тенанта (для лога аудита). - // operation: Операция сессии. - // context: Контекст вызова gRPC. private async Task ExecuteAsync( string tenantId, Func> operation,