Инициализировать репозиторий «Дейл»

Первый коммит: модульный монолит ядра (.NET 10) и gRPC-сервисы
ai/ml/telegram, фронтенд Vue 3/Vite/Tailwind, документация (ТЗ,
инструкция пользователя, техдокументация, код-стайл), бэклог,
скрипты развёртывания и архив прототипа LeadRadar.
This commit is contained in:
Rustam Khalimov
2026-09-11 02:50:17 +03:00
commit 9e07568ddd
1402 changed files with 177470 additions and 0 deletions
+85
View File
@@ -0,0 +1,85 @@
using Deal.Grpc.Hosting;
namespace Deal.Ai;
/// <summary>
/// Собирает WebApplication gRPC-хоста ai-service (план Task 4/7/8; Ruling 1/2/5/12).
///
/// Продакшн-точка входа вызывает <see cref="Create"/> из Program.cs (порт из env GRPC_PORT/PORT);
/// интеграционные тесты (Deal.Ai.Tests) — из своего процесса на эфемерном порту, поэтому
/// конфигурация хоста живёт здесь один раз и не дублируется в тестах.
/// Транспорт/AddGrpc/health — общая серверная обвязка <see cref="GrpcServer"/> (Deal.Grpc.Hosting,
/// C31): mTLS (env DEAL_MTLS_*, Ruling 6/Task 13), Kestrel HTTP/2, интерцепторы service-token и
/// access-лога, gRPC-health; здесь — только регистрации логики ai-service.
/// Регистрации логики (план Task 7/8, Ruling 5): LLM-фасад провайдеров — HTTP-клиент
/// (<see cref="Llm.LlmHttpClient"/>, OpenAI-совместимые chat/completions + Anthropic Messages API,
/// таймауты 90/60 с) как <see cref="Llm.IProviderClient"/> и оркестратор вызовов
/// (<see cref="Llm.ProviderCaller"/>: ретраи 2 с паузами 0.8/2 с, извлечение JSON из markdown,
/// usage API или оценка по символам). Сервис без БД и настроек (ядро передаёт заполненные промпты
/// и конфиг провайдера в теле запроса); configureServices-хук — seam для фейков тестов
/// (подмена IProviderClient и функции паузы ретраев).
/// </summary>
public static class AiServiceHost
{
/// <summary>
/// Создаёт (не запускает) хост: общая обвязка GrpcServer (Kestrel HTTP/2 на 0.0.0.0:grpcPort,
/// dev — plaintext + service-token, Ruling 2; при DEAL_MTLS_ENABLED=1 — HTTPS с серверным
/// сертификатом и требованием клиентского, Ruling 6/Task 13), затем LLM-фасад (Task 7)
/// и маппинг <see cref="AiServiceImpl"/> (Task 8).
/// </summary>
/// <param name="grpcPort">TCP-порт Kestrel.</param>
/// <param name="args">Аргументы командной строки (Program.cs); в тестах не нужны.</param>
/// <param name="configureServices">
/// Опциональный хук DI для тестов (подмена LLM-фасада фейками: регистрация, добавленная здесь,
/// побеждает — DI резолвит последнюю; см. AiServiceHostTests/AiRpcTests).
/// </param>
/// <param name="configureBuilder">
/// Опциональный хук конфигурации билдера для production-точки входа (Program.cs): Serilog
/// (DealLogging.Configure, Ruling 7/Task 14). Тесты хост поднимают БЕЗ этого хука — логирование
/// файлов/консоли тестам не нужно.
/// </param>
/// <returns>Собранный хост; запуск — StartAsync/RunAsync у вызывающего.</returns>
public static WebApplication Create(
int grpcPort,
string[]? args = null,
Action<IServiceCollection>? configureServices = null,
Action<WebApplicationBuilder>? configureBuilder = null)
{
WebApplicationBuilder builder = WebApplication.CreateBuilder(args ?? []);
// Общая серверная обвязка (Deal.Grpc.Hosting, C31): mTLS env DEAL_MTLS_* — загрузка
// сертификатов сразу с fail-fast (compose-prod монтирует deploy/certs, scripts/mtls-certs.sh);
// Kestrel HTTP/2 (dev — plaintext + обязательный service-token, Ruling 2); AddGrpc
// (access-лог первым, затем service-token, потолок сообщения) и gRPC-health (Ruling 12).
MtlsCertificates? mtlsCertificates = GrpcServer.LoadMtlsCertificates(builder);
GrpcServer.ConfigureKestrelHttp2Endpoint(builder, grpcPort, mtlsCertificates);
builder.Services.AddDealGrpcServer();
builder.Services.AddReadyHealthCheck("хост ai-service готов");
// LLM-фасад провайдеров (план Task 7, Ruling 5): HTTP-клиент одной попытки вызова (таймаут
// попытки управляется внутри — 90 с OpenAI / 60 с Anthropic; клиент без общего таймаута) и
// оркестратор ретраев/JSON/usage поверх него. Ключи API — в конфиге запроса, не в DI/логах.
builder.Services.AddHttpClient<Llm.LlmHttpClient>(static httpClient =>
httpClient.Timeout = Timeout.InfiniteTimeSpan);
builder.Services.AddSingleton<Llm.IProviderClient>(
static sp => sp.GetRequiredService<Llm.LlmHttpClient>());
builder.Services.AddSingleton<Func<TimeSpan, CancellationToken, Task>>(DefaultRetryDelayAsync);
builder.Services.AddSingleton<Llm.ProviderCaller>();
configureServices?.Invoke(builder.Services);
configureBuilder?.Invoke(builder);
WebApplication app = builder.Build();
app.MapGrpcService<AiServiceImpl>();
app.MapGrpcHealthChecksService();
return app;
}
// Пауза между ретраями по умолчанию (Task.Delay; тесты подменяют через DI).
// delay: Длительность паузы.
// cancellationToken: Токен отмены.
private static Task DefaultRetryDelayAsync(TimeSpan delay, CancellationToken cancellationToken)
=> Task.Delay(delay, cancellationToken);
}