Почистить комментарии от упоминаний процесса
Удалены <remarks>, <summary> сжаты до короткой фразы, вырезаны ссылки на Task/Ruling/этап/python/прототип; //-комментарии со ссылками на процесс удалены; то же в .proto. Правила обновлены в docs/spec/Код-стайл-Дейл.md. Строк комментариев 27210 -> ~19100.
This commit is contained in:
@@ -37,52 +37,38 @@ using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
using CookieOptions = Deal.Api.Configuration.CookieOptions;
|
||||
|
||||
const string cookiesSectionName = "Cookies";
|
||||
// Секция настроек куки оператора (Ruling 1): имя deal_operator_session, срок 12 ч, Secure из конфига.
|
||||
const string operatorCookiesSectionName = "OperatorCookies";
|
||||
// Имя CORS-политики (план Task 12, Ruling 10): одна политика, режим зависит от Security:AllowedOrigins.
|
||||
const string corsPolicyName = "cors";
|
||||
// Секции конфигурации интеграций (Ruling 6): Services:Ml / Services:Ai / Services:Telegram → {UseLocal, Endpoint}.
|
||||
const string servicesSectionName = "Services:Ml";
|
||||
const string aiServicesSectionName = "Services:Ai";
|
||||
const string telegramServicesSectionName = "Services:Telegram";
|
||||
// Имя истории tenant-миграций без схемы (схема — через search_path; миграции применяет
|
||||
// TenantProvisioningService на старте, runtime-контекст их не выполняет).
|
||||
const string tenantMigrationsHistoryTable = "__TenantMigrationsHistory";
|
||||
// Порт gRPC-ингресса по умолчанию (Ruling 7): :5082; переопределяется env GRPC_INGRESS_PORT.
|
||||
const int defaultIngressPort = 5082;
|
||||
const string ingressPortEnvKey = "GRPC_INGRESS_PORT";
|
||||
// Ключ конфигурации адресов основного HTTP-эндпоинта (--urls/ASPNETCORE_URLS/launchSettings).
|
||||
const string serverUrlsKey = "urls";
|
||||
// Фолбэк основного HTTP-адреса при отсутствии явных URL (дефолт ASP.NET Core http://localhost:5000).
|
||||
const string defaultHttpUrl = "http://localhost:5000";
|
||||
// Ключ env-переопределения дефолт-бюджета нового тенанта (Ruling 3; токенов в месяц, период — всегда month).
|
||||
const string defaultAiBudgetEnvKey = "DEAL_DEFAULT_AI_BUDGET";
|
||||
// Секция настроек rate limiting (план Task 11, Ruling 5): RateLimit:Enabled=false в dev/тестах по
|
||||
// умолчанию (curl-приёмки не режутся); PROD включает env-переопределением RateLimit__Enabled=true
|
||||
// (compose-prod, Task 14).
|
||||
const string rateLimitSectionName = "RateLimit";
|
||||
// Секция авто-очистки данных (этап 12, пакет B): DataRetention:AuditRetentionDays (дефолт 180) + Enabled —
|
||||
// фоновый цикл DataRetentionScheduler чистит audit_log по retention, лимиты/счётчики прошедших окон.
|
||||
const string dataRetentionSectionName = "DataRetention";
|
||||
// Секция настроек безопасности HTTP (план Task 12, Ruling 10): Security:AllowedOrigins — явный allowlist
|
||||
// Origin-проверки/CORS (пусто — dev-режим «свой origin» + CORS-любой; PROD — домены фронта, Ruling 9).
|
||||
const string securitySectionName = "Security";
|
||||
// Секция доверия прокси-заголовкам (план Task 12; замечание ревью T4/T11): ForwardedHeaders:KnownProxies/
|
||||
// KnownNetworks — доверенные прокси (Caddy в PROD); dev-дефолт в appsettings — loopback.
|
||||
const string forwardedHeadersSectionName = "ForwardedHeaders";
|
||||
|
||||
// Имя процесса для rolling-файла логов (Ruling 7, Task 14): data/logs/deal-core-<дата>.json.
|
||||
const string coreProcessName = "core";
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Структурированные логи Serilog (Ruling 7, план Task 14): консоль JSON в prod / текст в dev +
|
||||
// rolling-файл data/logs/deal-core-*.json (data — volume контейнера). Уровень/каталог — env
|
||||
// DEAL_LOG_LEVEL/DEAL_LOGS_DIR (см. DealLogging). Регистрируется до остальных сервисов: логирование
|
||||
// заменяет провайдеры Microsoft при builder.Build().
|
||||
DealLogging.Configure(builder, coreProcessName);
|
||||
|
||||
// Метрики OpenTelemetry → Prometheus (этап 12, пакет A): эндпоинт /metrics в формате Prometheus на
|
||||
// отдельном HTTP/1.1 Kestrel-эндпоинте (порт 9464/env METRICS_PORT) + инструментация входящих HTTP-
|
||||
// запросов и исходящих HTTP-клиентов; прикладные метрики — SharedKernel/Observability/DealMetrics.
|
||||
int metricsPort = DealMetricsHosting.ResolveMetricsPort(DealMetricsHosting.DefaultMetricsPort);
|
||||
@@ -96,9 +82,6 @@ builder.Services.AddDbContext<DealDbContext>(options => options.UseNpgsql(connec
|
||||
builder.Services.AddSingleton<ITenantContext, TenantContext>();
|
||||
builder.Services.AddSingleton<ConnectionStringProvider>();
|
||||
|
||||
// mTLS внутреннего gRPC-транспорта (Ruling 6, план Task 13): env DEAL_MTLS_* — флаг и пути/пароли
|
||||
// сертификатов (только env, Ruling 13). Dev-дефолт — выключено: сервисы и ингресс остаются на plaintext +
|
||||
// service-token (Ruling 2 этапа 6); compose-prod (Task 14) передаёт env и монтирует deploy/certs
|
||||
// (генерация — scripts/mtls-certs.sh). При DEAL_MTLS_ENABLED=1 сертификаты грузятся сразу (fail-fast на
|
||||
// битые пути/пароли) — один экземпляр используют и Kestrel-ингресс ниже, и gRPC-клиенты
|
||||
// (Ml/Ai/Telegram-каналы + ServiceHealthProbe).
|
||||
@@ -111,9 +94,7 @@ if (mtlsCertificates is not null)
|
||||
|
||||
// Kestrel: основной HTTP/1.1-эндпоинт из URL-конфигурации (как раньше — --urls/ASPNETCORE_URLS/
|
||||
// launchSettings) + второй endpoint gRPC-ингресса telegram-service (:5082, HTTP/2, env GRPC_INGRESS_PORT;
|
||||
// план Task 12, Ruling 7). Явные Listen заменяют URL-биндинг Kestrel, поэтому основной эндпоинт
|
||||
// пере-биндим адресами конфигурации "urls" явно (см. BindMainHttpEndpoints ниже). Ingress слушает все
|
||||
// интерфейсы (AnyIP): в dev к нему ходит telegram-service из compose-сети через host.docker.internal (Ruling 12).
|
||||
builder.WebHost.ConfigureKestrel(kestrel =>
|
||||
{
|
||||
BindMainHttpEndpoints(kestrel, builder.Configuration[serverUrlsKey]);
|
||||
@@ -123,9 +104,7 @@ builder.WebHost.ConfigureKestrel(kestrel =>
|
||||
listen.Protocols = HttpProtocols.Http2;
|
||||
if (mtlsCertificates is not null)
|
||||
{
|
||||
// mTLS-ингресс (Ruling 6, Task 13): HTTPS с серверным сертификатом core + обязательный клиентский
|
||||
// сертификат (цепочка до CA из DEAL_MTLS_CA_PEM). Основной HTTP :5080 остаётся http — TLS наружу
|
||||
// терминирует Caddy (Ruling 9, compose-prod Task 14).
|
||||
listen.UseHttps(https =>
|
||||
{
|
||||
https.ServerCertificate = mtlsCertificates.ServerCertificate;
|
||||
@@ -160,10 +139,8 @@ builder.Services.AddDbContext<TenantDbContext>(
|
||||
contextLifetime: ServiceLifetime.Scoped,
|
||||
optionsLifetime: ServiceLifetime.Scoped);
|
||||
|
||||
// Модуль Tenants и его EF-адаптеры («port & adapter», Ruling 1).
|
||||
builder.Services.AddTenantsModule();
|
||||
|
||||
// Лимиты ИИ-бюджета (Ruling 3, Task 8): дефолт-бюджет лениво создаваемой строки public.tenant_limits —
|
||||
// env DEAL_DEFAULT_AI_BUDGET (токенов в месяц) с фолбэком на константу модуля TokenBudgetDefaults (10 000 000);
|
||||
// период нового тенанта — month (константа). Значение читается один раз на старте и передаётся адаптеру
|
||||
// TenantLimitStore (GetOrCreateAsync при первом чтении/списании, задачи 7/10 list-путь тоже закрыт).
|
||||
@@ -171,104 +148,74 @@ TokenLimitDefaults tenantLimitDefaults = new(
|
||||
ResolveDefaultAiBudget(builder.Configuration), TokenBudgetDefaults.DefaultPeriod);
|
||||
builder.Services.AddDealPersistence(tenantLimitDefaults);
|
||||
|
||||
// Шифрование секретов (Ruling 2): ISecretCipher — AES-256-GCM; ключ из DEAL_ENCRYPTION_KEY
|
||||
// либо файла data/encryption.key под ContentRoot (dev). Ключ разрешается на старте —
|
||||
// невалидный env-ключ останавливает запуск.
|
||||
builder.Services.AddDealSecurity(builder.Environment.ContentRootPath);
|
||||
|
||||
// Внешние интеграции (Tasks 9/16/15, Rulings 4/5/6/9): IMlClient — детерминированная заглушка LocalMlClient
|
||||
// (Services:Ml:UseLocal=true, default; обучение — этап 3) либо gRPC-клиент GrpcMlClient (UseLocal=false,
|
||||
// ml-service :5103). Local-адаптеры читают KV-настройки тенанта через ISettingsStore — scoped (вне
|
||||
// tenant-запроса не разрешимы). IColumnSuggester — LocalColumnSuggester (эвристика, Self-Review L525–527).
|
||||
MlServiceOptions mlOptions = builder.Configuration.GetSection(servicesSectionName).Get<MlServiceOptions>() ?? new MlServiceOptions();
|
||||
builder.Services.AddSingleton(mlOptions);
|
||||
// AI-интеграция (план Task 15, Ruling 6/9): IAiClassifier/IAiTools — Local-адаптеры (Services:Ai:UseLocal=true,
|
||||
// default; локальный разбор ядра / инструменты не поддерживаются) либо декораторы бюджетного гейта поверх
|
||||
// gRPC-клиентов ai-service (UseLocal=false, ai-service :5102; AddDealIntegrations регистрирует GrpcAiClassifier/
|
||||
// GrpcAiTools + транспорт AiGrpcConnection — fail-fast, как MlGrpcConnection — и оборачивает их в
|
||||
// BudgetedAiClassifier/BudgetedAiTools, Ruling 3/Task 9).
|
||||
AiServiceOptions aiOptions = builder.Configuration.GetSection(aiServicesSectionName).Get<AiServiceOptions>() ?? new AiServiceOptions();
|
||||
builder.Services.AddSingleton(aiOptions);
|
||||
// Telegram-гейт (план Task 14, Ruling 6/7): LocalTelegramGateway (Services:Telegram:UseLocal=true, default —
|
||||
// нейтральный 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);
|
||||
|
||||
// Health-проба автономных сервисов для операторского health (план Task 10, Ruling 3/6/9): grpc.health.v1
|
||||
// к Services:*:Endpoint с дедлайном 3 с (ServiceHealthProbe). Stateless, singleton — пробы строят
|
||||
// короткоживущие каналы на каждый вызов (mTLS-каналы — при включённом флаге, Task 13/Ruling 6).
|
||||
builder.Services.AddSingleton(new ServiceHealthProbe(mtlsCertificates));
|
||||
|
||||
// Файловое хранилище вложений проектных карточек (Ruling 4, Task 6): LocalFileStorage (data/attachments
|
||||
// под ContentRoot) — dev/curl/unit по умолчанию; MinioFileStorage регистрируется, только когда сконфигурирован
|
||||
// MinIO (секция Storage:Minio либо env-алиасы DEAL_MINIO_*; compose-сервис deal-minio, порты 9000/9001).
|
||||
// Singleton: хранилище не привязано к схеме тенанта (объекты — в едином бакете/каталоге, мульти-аренда
|
||||
// объектного хранилища — этап 7 SaaS). Режим логируется на старте (см. ниже) — приёмка Task 6.
|
||||
builder.Services.AddDealFileStorage(builder.Configuration, builder.Environment.ContentRootPath);
|
||||
|
||||
// Модуль Settings (сервис настроек тенанта); адаптеры ISettingsStore/ISecretCipher уже
|
||||
// зарегистрированы AddDealPersistence/AddDealSecurity выше (см. Task 4).
|
||||
builder.Services.AddSettingsModule();
|
||||
|
||||
// Модуль Kanban — единый домен карточки (Ruling 12): регистратор сервисов карточек/контейнеров/тиков;
|
||||
// порт-адаптер ICardStore → KanbanStore уже зарегистрирован AddDealPersistence.
|
||||
builder.Services.AddKanbanModule();
|
||||
|
||||
// Модуль Pipeline (Ruling 10, Task 9): приём/обработка/воркер и ядра разбора этапа 4. Порт-адаптер
|
||||
// IPipelineStore → PipelineStore и внешние порты (IMlClient/IAiClassifier) уже зарегистрированы
|
||||
// AddDealPersistence/AddDealIntegrations выше; сервисы модуля вызывают из эндпоинтов /api/pipeline/*
|
||||
// и gRPC-ингресса telegram-service, pump — admin/tick (Task 10) и фоновый цикл (Task 11).
|
||||
builder.Services.AddPipelineModule();
|
||||
|
||||
// Модуль Telegram (план Task 13, Ruling 7): сервис каталога диалогов (DialogsService) — владелец таблиц
|
||||
// Dialogs/TgMessages схемы тенанта (миграция TenantTelegram). Порт-адаптеры ITelegramStore → TelegramStore и
|
||||
// ITelegramGateway → LocalTelegramGateway/GrpcTelegramClient зарегистрированы AddDealPersistence/AddDealIntegrations
|
||||
// выше; сервис зовут gRPC-ингресс (SyncDialogs/PushMessage) и эндпоинты /api/tg (Task 14).
|
||||
builder.Services.AddTelegramModule();
|
||||
|
||||
// Модуль Discovery (план Task 17/18, Ruling 9/10): сервисы задач/кандидатов/чёрного списка/лога, план-бюджет
|
||||
// и воркер (оценка/бан-гард/паузы). Порт-адаптер IDiscoveryStore → DiscoveryStore зарегистрирован
|
||||
// AddDealPersistence; внешние порты (ITelegramGateway/IAiTools/IMlClient) — AddDealIntegrations выше. Эндпоинты
|
||||
// /api/discovery добавляет Task 19; фоновый цикл воркера — DiscoveryWorkerScheduler ниже.
|
||||
builder.Services.AddDiscoveryModule();
|
||||
|
||||
// Статус/ключи вкладки Telegram (план Task 14, Ruling 8): сборка GET /api/tg/status (гейт + KV tgAccount +
|
||||
// счётчик мониторящихся + keysSet) и чтение глобальных ключей приложения (telegramKeys в public.global_settings,
|
||||
// расшифровка apiHash — задаёт оператор, ТЗ §4.1/§8.1). Scoped: зависимости — ISettingsStore/ITelegramStore
|
||||
// на TenantDbContext схемы тенанта запроса, IGlobalSettingsStore — на системном DealDbContext.
|
||||
builder.Services.AddScoped<TgStatusService>();
|
||||
builder.Services.AddScoped<TelegramKeysService>();
|
||||
|
||||
// Фоновые спуски перечитывания каналов (план Task 14, Ruling 8; аналог python-_spawn роутеров): эндпоинты
|
||||
// мониторинга/«Перечитать» отвечают сразу, тяжёлый разбор идёт в отдельном scope с захваченным tenant-контекстом.
|
||||
builder.Services.AddSingleton<TelegramBackfillScheduler>();
|
||||
|
||||
// Оркестратор ручного тика (план Task 10, Rulings 8/9): тик правил хранения (Kanban) + очистка отсева
|
||||
// Pipeline + один проход pump + SSE-публикации (тосты/new_card) для POST /api/admin/tick (StorageEndpoints).
|
||||
// Scoped: зависимости живут в рамках tenant-запроса (scoped-сервисы модулей на TenantDbContext схемы).
|
||||
builder.Services.AddScoped<AdminTickOrchestrator>();
|
||||
|
||||
// Обслуживание FTS-индексов схемы тенанта (Ruling 6, план Task 10): POST /api/admin/fts/rebuild —
|
||||
// CREATE INDEX IF NOT EXISTS + ANALYZE (FtsMaintenance) на TenantDbContext запроса (scoped, как адаптеры).
|
||||
builder.Services.AddScoped<FtsMaintenance>();
|
||||
|
||||
// SSE-брокер этапа (Ruling 5): singleton per-tenant каналов событий; подписка — GET /api/events,
|
||||
// публикации — из эндпоинтов Api после вызова сервисов (Tasks 10/13/14).
|
||||
builder.Services.AddSingleton<SseBroker>();
|
||||
|
||||
// SSE-тосты статистики тика правил хранения (Ruling 8): единый хелпер для POST /api/admin/tick
|
||||
// (StorageEndpoints) и фонового StorageTickScheduler (Task 11) — без дублирования текстов/иконок.
|
||||
builder.Services.AddSingleton<StorageToastPublisher>();
|
||||
|
||||
// Общий воркер-гейт pump тенанта (план Task 11, Ruling 8; аналог asyncio.Lock pipeline.py L38–42):
|
||||
// POST /api/admin/tick (AdminTickOrchestrator) и фоновый цикл разбора очереди (PipelineWorkerScheduler)
|
||||
// не разбирают очередь одного тенанта одновременно (singleton per-tenant флагов, Interlocked).
|
||||
builder.Services.AddSingleton<PipelinePumpGate>();
|
||||
|
||||
// Rate limiting и защита входа (план Task 11, Ruling 5; этап 12, пакет B — хранилище на Postgres): секция
|
||||
// "RateLimit" (appsettings.json + env RateLimit__*). Enabled=false в dev/тестах — политики/middleware/
|
||||
// интерцептор не регистрируются вовсе (Ruling 5: «в dev выключено — curl-приёмки не режутся»);
|
||||
// LoginAttemptGuard (окно ip|login 5 неудач/15 мин в public.rate_limit_counters) регистрируется всегда,
|
||||
// но активен только при Enabled.
|
||||
RateLimitOptions rateLimitOptions = builder.Configuration
|
||||
@@ -283,19 +230,16 @@ if (rateLimitOptions.Enabled)
|
||||
builder.Services.AddDealRateLimiter(rateLimitOptions);
|
||||
}
|
||||
|
||||
// gRPC-ингресс telegram-service (план Task 12, Ruling 1/7): сервер Deal.Grpc.Telegram.IngressService
|
||||
// на отдельном 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 =>
|
||||
{
|
||||
// Access-лог RPC ингресса (Ruling 7, Task 14): ПЕРВЫМ в цепочке — логируются и отклонённые
|
||||
// вызовы (401/429); gRPC-health не логируется (см. RpcCallLoggingInterceptor).
|
||||
grpc.Interceptors.Add<RpcCallLoggingInterceptor>();
|
||||
grpc.Interceptors.Add<IngressServiceTokenInterceptor>();
|
||||
if (rateLimitOptions.Enabled)
|
||||
{
|
||||
// Лимит входящего потока по tenant-id (план Task 11, Ruling 5): окно считает общий
|
||||
// singleton-лимитер (CreateLimiter) — экземпляры интерцептора общий PartitionedRateLimiter
|
||||
// разделяют; health-методы освобождены (см. IngressRateLimitInterceptor).
|
||||
grpc.Interceptors.Add<IngressRateLimitInterceptor>();
|
||||
@@ -311,80 +255,59 @@ if (rateLimitOptions.Enabled)
|
||||
|
||||
builder.Services.AddScoped<TelegramIngressService>();
|
||||
|
||||
// gRPC-health ингресса (план Task 20, Ruling 12): healthcheck контейнера core в docker compose.
|
||||
// grpc.health.v1.Health интерцептор токеном не проверяет (инфраструктурный liveness, как в сервисах
|
||||
// этапа T2–T4); регистрируется явная проверка "ready" — без неё health-сервис отвечает UNKNOWN.
|
||||
// Живучесть интеграций (ml/ai/telegram) health не проверяет — недоступность сервиса это UNAVAILABLE
|
||||
// на RPC и фолбэк Local-адаптеров, а не падение хоста (Ruling 6).
|
||||
builder.Services
|
||||
.AddGrpcHealthChecks()
|
||||
.AddCheck("ready", () => HealthCheckResult.Healthy("хост Deal.Api готов"));
|
||||
|
||||
// Проверка подключения AI-провайдера (Task 6, Ruling 7): порт модуля IAiConnectionChecker →
|
||||
// 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));
|
||||
|
||||
// Курсы валют (Task 8, Ruling 6): порт модуля IRatesSource → HTTP-адаптер ЦБ с собственным
|
||||
// HttpClient (таймаут 15 с, как httpx timeout=15 в rates.py). URL — фиксированная константа
|
||||
// адаптера (SSRF-allowlist), источник тенантом не настраивается. Типизированный клиент
|
||||
// регистрируется transient и живёт в рамках scope запроса (как IAiConnectionChecker, Task 6).
|
||||
builder.Services.AddHttpClient<IRatesSource, CbrRateSource>(
|
||||
client => client.Timeout = TimeSpan.FromSeconds(CbrRateSource.RequestTimeoutSeconds));
|
||||
|
||||
// Фоновое обновление кэша курсов вне запроса (Ruling 6): PATCH rateSource и лениво на GET —
|
||||
// собственный scope + in-flight guard (см. RatesRefreshScheduler).
|
||||
builder.Services.AddSingleton<RatesRefreshScheduler>();
|
||||
|
||||
// Bootstrap при старте (Ruling 8): дефолтный тенант + admin, провижининг схем всех тенантов.
|
||||
builder.Services.AddHostedService<TenantBootstrapService>();
|
||||
|
||||
// Bootstrap оператора при старте (Ruling 1 этапа 7): env DEAL_OPERATOR_LOGIN/DEAL_OPERATOR_PASSWORD,
|
||||
// dev-дефолт operator/operator в Development; в Production без env — warning и пропуск. Идёт после
|
||||
// TenantBootstrapService: операторские public-таблицы не зависят от провижининга схем тенантов.
|
||||
builder.Services.AddHostedService<OperatorBootstrapHostedService>();
|
||||
|
||||
// Фоновый цикл правил хранения (план Task 11, Ruling 8; аналог _storage_loop main.py L43–53): каждые
|
||||
// 30 с тикает ВСЕ тенанты (StorageTickService + автоочистка отсева пайплайна 3 суток) и публикует
|
||||
// SSE-тосты. Регистрируется после Bootstrap — первый проход стартует уже после провижининга схем.
|
||||
builder.Services.AddHostedService<StorageTickScheduler>();
|
||||
|
||||
// Фоновый цикл SSE-алертов ИИ-бюджета (план Task 9, Ruling 3; эталон StorageTickScheduler): каждые 60 с
|
||||
// проверяет ВСЕ тенанты и публикует в канал тенанта тост при пересечении порогов 80/100% (TryMark*-CAS —
|
||||
// один тост на порог за период). Идёт после Bootstrap: реестр тенантов провижинен до первого прохода.
|
||||
builder.Services.AddHostedService<BudgetAlertScheduler>();
|
||||
|
||||
// Фоновый цикл разбора очереди входящих (план Task 11, Ruling 8/11; аналог _pipeline_loop main.py L79–88):
|
||||
// каждые 2 с pump'ит ВСЕ тенанты (PipelineWorkerService.PumpOnceAsync под общим PipelinePumpGate) и
|
||||
// публикует SSE new_card по созданным карточкам — ingest разбирается без ручного tick (приёмка Task 11).
|
||||
// После StorageTickScheduler: очередь цикла — 2 с, первый проход сразу после старта.
|
||||
builder.Services.AddHostedService<PipelineWorkerScheduler>();
|
||||
|
||||
// Фоновый флашер очереди обучения ML (план Task 16, Ruling 6; аналог _ml_sync_loop main.py): каждые 10 с
|
||||
// выгружает MlOutbox тенантов в ml-service (TrainBatch, порции по 10, ≤100/цикл; удаление после успеха).
|
||||
// Регистрируется только в gRPC-режиме (UseLocal=false) — Local-режиму (этапы 2–5) сервис не нужен, очередь
|
||||
// копится до подключения ml-service (python L56–82); MlGrpcConnection создан в AddDealIntegrations (fail-fast).
|
||||
if (!mlOptions.UseLocal)
|
||||
{
|
||||
builder.Services.AddHostedService<MlOutboxFlushScheduler>();
|
||||
}
|
||||
|
||||
// Фоновый цикл Discovery-воркера (план Task 18, Ruling 10; аналог _discovery_loop main.py): каждые 5 с
|
||||
// делает ОДИН шаг (поиск/оценка/авто-вступление/done) для каждой running-задачи всех тенантов. Работает
|
||||
// всегда: в Local-режиме гейт нейтрален (поиск пуст/история недоступна), реальные действия — при
|
||||
// подключённом telegram-service (UseLocal=false). После Bootstrap: первый проход стартует после провижининга.
|
||||
builder.Services.AddHostedService<DiscoveryWorkerScheduler>();
|
||||
|
||||
// Сборщик глубин очередей/сессий (этап 12, §10.2): общий источник для метрик и операторского health.
|
||||
builder.Services.AddSingleton<RuntimeDepthsCollector>();
|
||||
|
||||
// Фоновый сборщик gauge-метрик (этап 12, пакет A): каждые 15 с публикует глубины очередей (пайплайн,
|
||||
// MlOutbox) по всем тенантам и число активных сессий в meter Deal (callback /metrics отдаёт их Prometheus).
|
||||
// Регистрируется последним из фоновых: после Bootstrap (реестр тенантов провижинен до первого прохода).
|
||||
builder.Services.AddHostedService<DealMetricsCollector>();
|
||||
|
||||
// Фоновый цикл авто-очистки данных (этап 12, пакет B; эталон DealMetricsCollector): раз в сутки удаляет
|
||||
// записи audit_log старше DataRetention:AuditRetentionDays (дефолт 180 дней), сбрасывает накопительные
|
||||
// поля лимитов прошедших периодов и убирает завершившиеся окна распределённых счётчиков. Регистрируется
|
||||
// последним из фоновых: после Bootstrap (реестр тенантов провижинен до первого прохода).
|
||||
@@ -397,15 +320,12 @@ builder.Services.AddHostedService<DataRetentionScheduler>();
|
||||
// Кука сессии: имя/срок/Secure из секции "Cookies" (appsettings.json + env Cookies__*).
|
||||
builder.Services.Configure<CookieOptions>(builder.Configuration.GetSection(cookiesSectionName));
|
||||
|
||||
// Кука операторской сессии (Ruling 1 этапа 7): имя deal_operator_session/срок/Secure из секции
|
||||
// "OperatorCookies" (appsettings.json + env OperatorCookies__*) — отдельная от тенантной deal_session.
|
||||
builder.Services.Configure<OperatorCookieOptions>(builder.Configuration.GetSection(operatorCookiesSectionName));
|
||||
|
||||
// Ответы JSON — как в прототипе FastAPI: без \u-экранирования не-ASCII символов.
|
||||
builder.Services.ConfigureHttpJsonOptions(options =>
|
||||
options.SerializerOptions.Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping);
|
||||
|
||||
// Безопасность HTTP (план Task 12, Ruling 10(2)/9): Security:AllowedOrigins — явный allowlist Origin-
|
||||
// проверки мутаций и CORS (пусто — dev-режим «свой origin», см. AddCors ниже; PROD — домен фронта в
|
||||
// compose-prod). Инстанс регистрируется в DI: значение читается один раз на старте (политики формируются
|
||||
// при старте хоста), OriginGuardMiddleware получает его конструктором.
|
||||
@@ -414,20 +334,14 @@ SecurityOptions securityOptions = builder.Configuration
|
||||
.Get<SecurityOptions>() ?? new SecurityOptions();
|
||||
builder.Services.AddSingleton(securityOptions);
|
||||
|
||||
// Доверие прокси-заголовкам (план Task 12; замечание ревью T4/T11): ForwardedHeaders — UseForwardedHeaders
|
||||
// включается env-переопределением (ForwardedHeaders__Enabled=true в PROD за Caddy, compose-prod Task 14);
|
||||
// dev-дефолт — false (прокси в dev-стеке нет, compose.dev публикует core напрямую).
|
||||
ForwardedHeadersConfig forwardedHeadersConfig = builder.Configuration
|
||||
.GetSection(forwardedHeadersSectionName)
|
||||
.Get<ForwardedHeadersConfig>() ?? new ForwardedHeadersConfig();
|
||||
builder.Services.AddSingleton(forwardedHeadersConfig);
|
||||
|
||||
// CORS (план Task 12, Ruling 10(2)/9): пустой Security:AllowedOrigins — dev-режим «как в прототипе»
|
||||
// (любой origin/method/header, credentials=true; AllowAnyOrigin + AllowCredentials несовместимы — любой
|
||||
// origin разрешается предикатом). Непустой список (PROD, Ruling 9) — строгий allowlist + credentials.
|
||||
// Security-заголовки ответов (nosniff/X-Frame-Options/Referrer-Policy; CSP/HSTS) — на edge (Caddyfile,
|
||||
// Task 14): наружу статику и /api отдаёт Caddy, core отвечает JSON — на core не дублируются
|
||||
// (пересмотр Ruling 10(3), см. task-12-report).
|
||||
builder.Services.AddCors(options =>
|
||||
options.AddPolicy(corsPolicyName, cors =>
|
||||
{
|
||||
@@ -446,7 +360,6 @@ builder.Services.AddCors(options =>
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Эндпоинт метрик /metrics (HTTP/1.1 на отдельном порту): формат Prometheus (этап 12, пакет A).
|
||||
DealMetricsHosting.MapDealMetrics(app);
|
||||
|
||||
// Fail-closed для Production (Security review): дефолты кода рассчитаны на dev/тесты (rate limit выключен,
|
||||
@@ -467,46 +380,36 @@ if (app.Environment.IsProduction())
|
||||
}
|
||||
}
|
||||
|
||||
// Стартовый лог выбранного режима файлового хранилища (приёмка Task 6: запуск Api — LocalFileStorage
|
||||
// с путём data/attachments; при сконфигурированном MinIO — MinioFileStorage с endpoint/бакетом).
|
||||
app.Logger.LogInformation("Файловое хранилище: {FileStorage}", app.Services.GetRequiredService<IFileStorage>());
|
||||
|
||||
// Стартовый лог режима ML-интеграции (приёмка Task 16): Local-заглушка либо gRPC-клиент ml-service.
|
||||
app.Logger.LogInformation(
|
||||
"ML-интеграция: {Mode} ({Endpoint})",
|
||||
mlOptions.UseLocal ? "Local-заглушка (MlOutbox накапливается)" : "gRPC-клиент ml-service",
|
||||
mlOptions.Endpoint);
|
||||
|
||||
// Стартовый лог режима AI-интеграции (приёмка Task 15): локальный разбор ядра либо gRPC-клиент ai-service
|
||||
// под декоратором бюджетного гейта (Task 9: исчерпано/приостановлено → локальный разбор, приём не блокируется).
|
||||
app.Logger.LogInformation(
|
||||
"AI-интеграция: {Mode} ({Endpoint})",
|
||||
aiOptions.UseLocal ? "Local-адаптеры (разбор ядра/инструменты выключены)" : "gRPC-клиент ai-service",
|
||||
aiOptions.Endpoint);
|
||||
|
||||
// Стартовый лог режима Telegram-гейта (приёмка Task 14): Local-заглушка либо gRPC-клиент telegram-service.
|
||||
app.Logger.LogInformation(
|
||||
"Telegram-гейт: {Mode} ({Endpoint})",
|
||||
telegramOptions.UseLocal ? "Local-заглушка (idle/не подключён)" : "gRPC-клиент telegram-service",
|
||||
telegramOptions.Endpoint);
|
||||
|
||||
// Стартовый лог транспорта внутреннего gRPC (приёмка Task 13, Ruling 6): dev — plaintext + service-token,
|
||||
// PROD (DEAL_MTLS_ENABLED=1) — mTLS. Пути/пароли не логируются (Ruling 13).
|
||||
app.Logger.LogInformation(
|
||||
"Транспорт внутреннего gRPC: {Transport}",
|
||||
mtlsOptions.Enabled ? "mTLS (DEAL_MTLS_ENABLED=1, сертификаты из DEAL_MTLS_*)" : "plaintext + service-token (dev)");
|
||||
|
||||
if (forwardedHeadersConfig.Enabled)
|
||||
{
|
||||
// Прокси-заголовки (план Task 12; замечание ревью T4/T11): X-Forwarded-For/X-Forwarded-Proto доверяются
|
||||
// только клиентам из ForwardedHeaders:KnownProxies/KnownNetworks (конфиг; appsettings — loopback для dev).
|
||||
// Middleware — ПЕРВЫЙ в конвейере: RemoteIpAddress/Scheme читают слои ниже (CORS, Session/OperatorSession —
|
||||
// audit-IP эндпоинтов, RateLimiter — ключи по IP, LoginAttemptGuard). Без него за Caddy (compose-prod,
|
||||
// Task 14) RemoteIpAddress всех запросов = IP Caddy, и audit-IP + rate-limit-по-IP схлопываются в один бакет.
|
||||
app.UseForwardedHeaders(BuildForwardedHeadersOptions(forwardedHeadersConfig));
|
||||
}
|
||||
|
||||
// Access-лог HTTP (Ruling 7, Task 14): первый в конвейере (после ForwardedHeaders) — длительность
|
||||
// и статус всего пути обработки. gRPC-ингресс (Content-Type application/grpc) middleware пропускает —
|
||||
// его логирует интерцептор RpcCallLoggingInterceptor (см. HttpAccessLogMiddleware).
|
||||
app.UseMiddleware<HttpAccessLogMiddleware>();
|
||||
@@ -516,34 +419,27 @@ app.UseMiddleware<SessionMiddleware>();
|
||||
app.UseMiddleware<OperatorSessionMiddleware>();
|
||||
if (rateLimitOptions.Enabled)
|
||||
{
|
||||
// Порядок middleware — Ruling 5: Session → Operator → RateLimiter (политика "api" ключует по
|
||||
// CurrentUser.TenantId либо IP анонима; сессии уже разрешены). При Enabled=false лимитер не
|
||||
// регистрируется (dev-прогон не режет curl-приёмки; OriginGuard Task 12 встанет после).
|
||||
app.UseRateLimiter();
|
||||
}
|
||||
|
||||
// Origin-проверка мутаций /api (план Task 12, Ruling 10(2)): порядок Ruling 5 — Session → Operator →
|
||||
// RateLimiter → OriginGuard (сессии разрешены, 429 важнее 403). Проверяются не-GET/HEAD/OPTIONS запросы
|
||||
// с заголовком Origin: Origin == «свой» origin (схема + Host; за Caddy — https из X-Forwarded-Proto)
|
||||
// либо входит в Security:AllowedOrigins; иначе 403 {detail}. Без Origin (curl/сервер-сервер) пропускаются;
|
||||
// SameSite=Lax куки остаётся первым рубежом CSRF (фиксируется в техдок §10, Task 16).
|
||||
app.UseMiddleware<OriginGuardMiddleware>();
|
||||
|
||||
app.MapGet("/api/health", () => Results.Ok(new { ok = true, service = "deal" }));
|
||||
app.MapAuthEndpoints();
|
||||
app.MapOperatorAuthEndpoints();
|
||||
app.MapOperatorAuditEndpoints();
|
||||
// Операторская аналитика (план этапа 10, T3): read-only сводка/расход токенов/лента действий.
|
||||
app.MapOperatorAnalyticsEndpoints();
|
||||
app.MapOperatorInvitesEndpoints();
|
||||
app.MapOperatorTenantsEndpoints();
|
||||
// Операторские лимиты/health (план Task 10, Ruling 3/11): сводка и смена бюджета по тенанту (GET/PATCH
|
||||
// .../limit, аудит tenant_limit_changed) + health ядра/БД и сервисов ml/ai/telegram.
|
||||
app.MapOperatorLimitsEndpoints();
|
||||
app.MapOperatorHealthEndpoints();
|
||||
// Глобальные настройки оператора (ТЗ §4.1/§8.1): ключи приложения Telegram — чтение (маска) и смена.
|
||||
app.MapOperatorSettingsEndpoints();
|
||||
// Обслуживание (этап 12, пакет C): идемпотентная пакетная миграция схем всех тенантов реестра
|
||||
// (ограниченный параллелизм + логирование прогресса) — для SaaS с сотнями/тысячами схем.
|
||||
app.MapOperatorMaintenanceEndpoints();
|
||||
app.MapJoinEndpoint();
|
||||
@@ -559,14 +455,10 @@ app.MapStorageEndpoints();
|
||||
app.MapEventsEndpoint();
|
||||
app.MapAiSuggestEndpoints();
|
||||
app.MapPipelineEndpoints();
|
||||
// Эндпоинты /api/tg (план Task 14, Ruling 8): реальный контракт вкладки «Каналы» вместо boot-заглушки
|
||||
// (BootStubEndpoints удалён); qr-image — отдельным файлом.
|
||||
app.MapTelegramEndpoints();
|
||||
app.MapTelegramQrImageEndpoint();
|
||||
// Эндпоинты /api/discovery (план Task 19, Ruling 11): задачи поиска/кандидаты/чёрный список/лог/generate-keywords
|
||||
// (DiscoveryEndpoints, 1:1 api-map §3.8) — модуль Discovery зарегистрирован AddDiscoveryModule выше.
|
||||
app.MapDiscoveryEndpoints();
|
||||
// gRPC-ингресс и его health освобождены от HTTP-политик rate limiter (план Task 11, Ruling 5): лимит
|
||||
// входящего потока считает IngressRateLimitInterceptor по tenant-id из metadata (иначе общее окно на IP
|
||||
// telegram-service резало бы весь ингресс раньше интерцептора); health — инфраструктурный liveness.
|
||||
app.MapGrpcService<TelegramIngressService>().DisableRateLimiting();
|
||||
@@ -639,9 +531,7 @@ static List<Uri> ParseHttpAddresses(string? urlsConfig)
|
||||
static int? ParsePort(string? rawValue)
|
||||
=> int.TryParse(rawValue, out int parsedPort) ? parsedPort : null;
|
||||
|
||||
// Дефолт-бюджет нового тенанта из env DEAL_DEFAULT_AI_BUDGET (Ruling 3, Task 8): нечисловое/неположительное
|
||||
// значение (пустая переменная, опечатка) — константа модуля TokenBudgetDefaults.DefaultBudgetTokens. Период
|
||||
// всегда month — оператор меняет бюджет/период позже через PATCH лимита (Task 10).
|
||||
long ResolveDefaultAiBudget(IConfiguration configuration)
|
||||
{
|
||||
string? rawValue = configuration[defaultAiBudgetEnvKey];
|
||||
@@ -653,11 +543,7 @@ long ResolveDefaultAiBudget(IConfiguration configuration)
|
||||
public partial class Program
|
||||
{
|
||||
/// <summary>
|
||||
/// Строит опции UseForwardedHeaders из ForwardedHeadersConfig (план Task 12; замечание ревью
|
||||
/// T4/T11): обрабатываются X-Forwarded-For/X-Forwarded-Proto ровно одного доверенного hop'а. Списки
|
||||
/// доверия — строго из конфига KnownProxies/KnownNetworks (appsettings-дефолт — loopback); невалидный
|
||||
/// IP/CIDR — InvalidOperationException (fail-fast: опечатка в настройке доверия не должна молча
|
||||
/// отключать обработку). Публичный: unit-тесты опций (ForwardedHeadersHttpTests).
|
||||
/// Строит опции UseForwardedHeaders из ForwardedHeadersConfig
|
||||
/// </summary>
|
||||
/// <param name="config">Секция ForwardedHeaders конфигурации.</param>
|
||||
/// <returns>Опции для app.UseForwardedHeaders.</returns>
|
||||
|
||||
Reference in New Issue
Block a user