Почистить комментарии от ссылок на ТЗ и обрывков
Удаление целых //-блоков со ссылками (Task/Ruling/этап/ТЗ/§/ дизайн-док/api-map/python/прототип) вместо построчного вырезания — без обрывков фраз; снят боилерплейт <param>/<returns>; то же в .proto.
This commit is contained in:
@@ -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];
|
||||
|
||||
Reference in New Issue
Block a user