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

Удаление целых //-блоков со ссылками (Task/Ruling/этап/ТЗ/§/
дизайн-док/api-map/python/прототип) вместо построчного вырезания —
без обрывков фраз; снят боилерплейт <param>/<returns>; то же в
.proto.
This commit is contained in:
Rustam Khalimov
2026-09-11 13:49:26 +03:00
parent b053d58335
commit 79c931d88e
220 changed files with 32 additions and 1259 deletions
-102
View File
@@ -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<DealDbContext>(options => options.UseNpgsql(connec
builder.Services.AddSingleton<ITenantContext, TenantContext>();
builder.Services.AddSingleton<ConnectionStringProvider>();
// (генерация — 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<TenantDbContext>(
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<MlServiceOptions>() ?? 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<AiServiceOptions>() ?? new AiServiceOptions();
builder.Services.AddSingleton(aiOptions);
// нейтральный no-op/idle) либо gRPC-клиент GrpcTelegramClient (UseLocal=false, telegram-service :5101).
TelegramServiceOptions telegramOptions = builder.Configuration.GetSection(telegramServicesSectionName).Get<TelegramServiceOptions>() ?? 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<TgStatusService>();
builder.Services.AddScoped<TelegramKeysService>();
// мониторинга/«Перечитать» отвечают сразу, тяжёлый разбор идёт в отдельном scope с захваченным tenant-контекстом.
builder.Services.AddSingleton<TelegramBackfillScheduler>();
// Pipeline + один проход pump + SSE-публикации (тосты/new_card) для POST /api/admin/tick (StorageEndpoints).
// Scoped: зависимости живут в рамках tenant-запроса (scoped-сервисы модулей на TenantDbContext схемы).
builder.Services.AddScoped<AdminTickOrchestrator>();
// CREATE INDEX IF NOT EXISTS + ANALYZE (FtsMaintenance) на TenantDbContext запроса (scoped, как адаптеры).
builder.Services.AddScoped<FtsMaintenance>();
builder.Services.AddSingleton<SseBroker>();
builder.Services.AddSingleton<StorageToastPublisher>();
// POST /api/admin/tick (AdminTickOrchestrator) и фоновый цикл разбора очереди (PipelineWorkerScheduler)
// не разбирают очередь одного тенанта одновременно (singleton per-tenant флагов, Interlocked).
builder.Services.AddSingleton<PipelinePumpGate>();
// "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<RateLimitOptions>() ?? 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<RpcCallLoggingInterceptor>();
grpc.Interceptors.Add<IngressServiceTokenInterceptor>();
if (rateLimitOptions.Enabled)
{
// singleton-лимитер (CreateLimiter) — экземпляры интерцептора общий PartitionedRateLimiter
// разделяют; health-методы освобождены (см. IngressRateLimitInterceptor).
grpc.Interceptors.Add<IngressRateLimitInterceptor>();
}
});
@@ -255,62 +200,39 @@ if (rateLimitOptions.Enabled)
builder.Services.AddScoped<TelegramIngressService>();
// 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<IAiConnectionChecker, AiConnectionChecker>(
client => client.Timeout = TimeSpan.FromSeconds(AiConnectionChecker.RequestTimeoutSeconds));
// адаптера (SSRF-allowlist), источник тенантом не настраивается. Типизированный клиент
builder.Services.AddHttpClient<IRatesSource, CbrRateSource>(
client => client.Timeout = TimeSpan.FromSeconds(CbrRateSource.RequestTimeoutSeconds));
// собственный scope + in-flight guard (см. RatesRefreshScheduler).
builder.Services.AddSingleton<RatesRefreshScheduler>();
builder.Services.AddHostedService<TenantBootstrapService>();
// dev-дефолт operator/operator в Development; в Production без env — warning и пропуск. Идёт после
// TenantBootstrapService: операторские public-таблицы не зависят от провижининга схем тенантов.
builder.Services.AddHostedService<OperatorBootstrapHostedService>();
// 30 с тикает ВСЕ тенанты (StorageTickService + автоочистка отсева пайплайна 3 суток) и публикует
// SSE-тосты. Регистрируется после Bootstrap — первый проход стартует уже после провижининга схем.
builder.Services.AddHostedService<StorageTickScheduler>();
// проверяет ВСЕ тенанты и публикует в канал тенанта тост при пересечении порогов 80/100% (TryMark*-CAS —
// один тост на порог за период). Идёт после Bootstrap: реестр тенантов провижинен до первого прохода.
builder.Services.AddHostedService<BudgetAlertScheduler>();
// каждые 2 с pump'ит ВСЕ тенанты (PipelineWorkerService.PumpOnceAsync под общим PipelinePumpGate) и
// После StorageTickScheduler: очередь цикла — 2 с, первый проход сразу после старта.
builder.Services.AddHostedService<PipelineWorkerScheduler>();
// выгружает MlOutbox тенантов в ml-service (TrainBatch, порции по 10, ≤100/цикл; удаление после успеха).
if (!mlOptions.UseLocal)
{
builder.Services.AddHostedService<MlOutboxFlushScheduler>();
}
// делает ОДИН шаг (поиск/оценка/авто-вступление/done) для каждой running-задачи всех тенантов. Работает
// всегда: в Local-режиме гейт нейтрален (поиск пуст/история недоступна), реальные действия — при
// подключённом telegram-service (UseLocal=false). После Bootstrap: первый проход стартует после провижининга.
builder.Services.AddHostedService<DiscoveryWorkerScheduler>();
builder.Services.AddSingleton<RuntimeDepthsCollector>();
// MlOutbox) по всем тенантам и число активных сессий в meter Deal (callback /metrics отдаёт их Prometheus).
// Регистрируется последним из фоновых: после Bootstrap (реестр тенантов провижинен до первого прохода).
builder.Services.AddHostedService<DealMetricsCollector>();
// записи audit_log старше DataRetention:AuditRetentionDays (дефолт 180 дней), сбрасывает накопительные
// поля лимитов прошедших периодов и убирает завершившиеся окна распределённых счётчиков. Регистрируется
// последним из фоновых: после Bootstrap (реестр тенантов провижинен до первого прохода).
DataRetentionOptions dataRetentionOptions = builder.Configuration
.GetSection(dataRetentionSectionName)
.Get<DataRetentionOptions>() ?? new DataRetentionOptions();
@@ -320,28 +242,21 @@ builder.Services.AddHostedService<DataRetentionScheduler>();
// Кука сессии: имя/срок/Secure из секции "Cookies" (appsettings.json + env Cookies__*).
builder.Services.Configure<CookieOptions>(builder.Configuration.GetSection(cookiesSectionName));
// "OperatorCookies" (appsettings.json + env OperatorCookies__*) — отдельная от тенантной deal_session.
builder.Services.Configure<OperatorCookieOptions>(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<SecurityOptions>() ?? new SecurityOptions();
builder.Services.AddSingleton(securityOptions);
// dev-дефолт — false (прокси в dev-стеке нет, compose.dev публикует core напрямую).
ForwardedHeadersConfig forwardedHeadersConfig = builder.Configuration
.GetSection(forwardedHeadersSectionName)
.Get<ForwardedHeadersConfig>() ?? 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<IFileStorage>());
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<HttpAccessLogMiddleware>();
app.UseCors(corsPolicyName);
@@ -419,13 +328,9 @@ app.UseMiddleware<SessionMiddleware>();
app.UseMiddleware<OperatorSessionMiddleware>();
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<OriginGuardMiddleware>();
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<TelegramIngressService>().DisableRateLimiting();
app.MapGrpcHealthChecksService().DisableRateLimiting();
@@ -531,7 +430,6 @@ static List<Uri> 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];