Убрать неиспользуемые using по код-стайлу
Прогон dotnet format (IDE0005) по 4 решениям: удалены лишние using, оставшиеся после миграции namespace (676 файлов).
This commit is contained in:
@@ -1,176 +1,172 @@
|
||||
using System.Text.Json;
|
||||
using Deal.Api.Extensions;
|
||||
using Deal.Api.Models;
|
||||
using Deal.Api.Services;
|
||||
using Deal.Modules.Settings.Application.Abstractions;
|
||||
using Deal.Modules.Settings.Application.Models;
|
||||
using Deal.Modules.Settings.Application.Registrars;
|
||||
using Deal.Modules.Settings.Application.Services;
|
||||
using Deal.Api.Dtos;
|
||||
|
||||
namespace Deal.Api.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// Эндпоинт проверки подключения AI-провайдера: POST /api/ai/check (Ruling 7/8, api-map §4.10).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// «Только для Settings-экрана» (Ruling 8): фронт жмёт «Проверить подключение» (store.js
|
||||
/// checkAiConnection) БЕЗ тела — сервер читает АКТИВНУЮ конфигурацию провайдера тенанта
|
||||
/// (настройки <c>aiProvider</c> + <c>aiConfigs</c> с расшифровкой ключа через <see cref="ISecretCipher"/>;
|
||||
/// 1:1 с ai_svc._cfg(), ai.py L25–33), вызывает порт <see cref="IAiConnectionChecker"/> и отдаёт
|
||||
/// {ok, message} + статус провайдера. Требует сессию: 401 {detail} (формат прототипа).
|
||||
/// Резолв scoped-зависимостей — через RequestServices ПОСЛЕ проверки сессии (как SettingsEndpoints:
|
||||
/// DI-биндинг параметров выполняется до тела, а ISettingsStore требует tenant-контекст запроса).
|
||||
/// </remarks>
|
||||
public static class AiCheckEndpoint
|
||||
{
|
||||
// Префикс группы API (общий для эндпоинтов этапа, Ruling 8).
|
||||
private const string ApiGroupPrefix = "/api";
|
||||
|
||||
// Путь проверки подключения AI-провайдера.
|
||||
private const string AiCheckPath = "/ai/check";
|
||||
|
||||
// OpenAPI-тег группы (в прототипе роутер settings — settings_routes.py).
|
||||
private const string OpenApiTag = "settings";
|
||||
|
||||
/// <summary>
|
||||
/// Регистрирует POST /api/ai/check.
|
||||
/// </summary>
|
||||
/// <param name="app">Построитель маршрутов приложения.</param>
|
||||
/// <returns>Построитель маршрутов для цепочки вызовов.</returns>
|
||||
public static IEndpointRouteBuilder MapAiCheckEndpoint(this IEndpointRouteBuilder app)
|
||||
{
|
||||
var group = app.MapGroup(ApiGroupPrefix).WithTags(OpenApiTag);
|
||||
group.MapPost(AiCheckPath, CheckAsync);
|
||||
return app;
|
||||
}
|
||||
|
||||
// POST /api/ai/check: проверка соединения с активным AI-провайдером тенанта.
|
||||
private static async Task<IResult> CheckAsync(HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (context.GetCurrentUser() is null)
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
|
||||
// Резолв после 401-гейта: ISettingsStore — scoped на TenantDbContext (tenant-контекст запроса).
|
||||
ISettingsStore store = context.RequestServices.GetRequiredService<ISettingsStore>();
|
||||
ISecretCipher secretCipher = context.RequestServices.GetRequiredService<ISecretCipher>();
|
||||
IAiConnectionChecker checker = context.RequestServices.GetRequiredService<IAiConnectionChecker>();
|
||||
|
||||
AiCheckRequest checkRequest = await BuildActiveCheckRequestAsync(store, secretCipher, ct);
|
||||
AiCheckResultDto result = await checker.CheckAsync(checkRequest, ct);
|
||||
return Results.Ok(result);
|
||||
}
|
||||
|
||||
// Собирает запрос проверки из активной конфигурации провайдера (1:1 с ai_svc._cfg()).
|
||||
// store: KV-хранилище настроек тенанта.
|
||||
// secretCipher: Шифр секретов (расшифровка apiKey).
|
||||
// ct: Токен отмены.
|
||||
// Возвращает: Запрос проверки: id провайдера + эффективные base/model + расшифрованный ключ.
|
||||
// Эффективные значения = дефолты SettingsDefaults, перекрытые сохранёнными
|
||||
// переопределениями (Ruling 1); пустое переопределение base/model → дефолт каталога
|
||||
// (семантика «cfg.get(...) or meta[...]» прототипа).
|
||||
private static async Task<AiCheckRequest> BuildActiveCheckRequestAsync(
|
||||
ISettingsStore store,
|
||||
ISecretCipher secretCipher,
|
||||
CancellationToken ct)
|
||||
{
|
||||
string providerId = await ReadActiveProviderIdAsync(store, ct);
|
||||
AiProviderDefinition? meta = AiProviders.All.FirstOrDefault(provider => provider.Id == providerId);
|
||||
|
||||
// Неизвестный id (ручное вмешательство в БД — PATCH-гейт SettingsService не даёт сохранить):
|
||||
// HTTP не выполняется — ответит SSRF-гейт checker (allowlist).
|
||||
if (meta is null)
|
||||
{
|
||||
return new AiCheckRequest(providerId, string.Empty, string.Empty, string.Empty, IsLocal: false, ApiStyle: null);
|
||||
}
|
||||
|
||||
AiConfigSetting config = await ReadEffectiveConfigAsync(store, providerId, ct);
|
||||
string apiKey = secretCipher.Decrypt(config.ApiKey);
|
||||
string baseUrl = string.IsNullOrEmpty(config.BaseUrl) ? meta.Base : config.BaseUrl;
|
||||
string model = string.IsNullOrEmpty(config.Model)
|
||||
? meta.Models.FirstOrDefault() ?? string.Empty
|
||||
: config.Model;
|
||||
|
||||
return new AiCheckRequest(providerId, baseUrl, model, apiKey, meta.Local, meta.ApiStyle);
|
||||
}
|
||||
|
||||
// Читает активный провайдер: сохранённый aiProvider или дефолт (повреждённое значение — дефолт).
|
||||
// store: KV-хранилище настроек тенанта.
|
||||
// ct: Токен отмены.
|
||||
// Возвращает: id провайдера.
|
||||
private static async Task<string> ReadActiveProviderIdAsync(ISettingsStore store, CancellationToken ct)
|
||||
{
|
||||
SettingValue? row = await store.GetAsync(SettingsKeys.AiProvider, ct);
|
||||
if (row is null)
|
||||
{
|
||||
return SettingsDefaults.AiProvider;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using JsonDocument document = JsonDocument.Parse(row.ValueJson);
|
||||
if (document.RootElement.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
return document.RootElement.GetString() ?? SettingsDefaults.AiProvider;
|
||||
}
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
// Повреждённая строка — дефолт (мягкая семантика, как в SettingsService).
|
||||
}
|
||||
|
||||
return SettingsDefaults.AiProvider;
|
||||
}
|
||||
|
||||
// Эффективный конфиг провайдера: дефолт SettingsDefaults, перекрытый сохранённым aiConfigs.
|
||||
// store: KV-хранилище настроек тенанта.
|
||||
// providerId: Активный провайдер (id из каталога).
|
||||
// ct: Токен отмены.
|
||||
// Возвращает: Конфиг {apiKey, baseUrl, model}; повреждённая строка aiConfigs — дефолт.
|
||||
private static async Task<AiConfigSetting> ReadEffectiveConfigAsync(
|
||||
ISettingsStore store,
|
||||
string providerId,
|
||||
CancellationToken ct)
|
||||
{
|
||||
AiConfigSetting defaults = SettingsDefaults.AiConfigs[providerId];
|
||||
SettingValue? row = await store.GetAsync(SettingsKeys.AiConfigs, ct);
|
||||
if (row is null)
|
||||
{
|
||||
return defaults;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using JsonDocument document = JsonDocument.Parse(row.ValueJson);
|
||||
JsonElement root = document.RootElement;
|
||||
if (root.ValueKind == JsonValueKind.Object
|
||||
&& root.TryGetProperty(providerId, out JsonElement entry)
|
||||
&& entry.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
return new AiConfigSetting(
|
||||
ApiKey: ReadField(entry, "apiKey") ?? defaults.ApiKey,
|
||||
BaseUrl: ReadField(entry, "baseUrl") ?? defaults.BaseUrl,
|
||||
Model: ReadField(entry, "model") ?? defaults.Model);
|
||||
}
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
// Повреждённая строка — дефолт (не роняем проверку).
|
||||
}
|
||||
|
||||
return defaults;
|
||||
}
|
||||
|
||||
// Читает строковое поле объекта конфигурации (имена полей camelCase, как пишет SettingsService).
|
||||
// entry: JSON-объект конфигурации провайдера.
|
||||
// field: Имя поля (apiKey/baseUrl/model).
|
||||
// Возвращает: Значение или null, если поле отсутствует/не строка.
|
||||
private static string? ReadField(JsonElement entry, string field)
|
||||
{
|
||||
return entry.TryGetProperty(field, out JsonElement value) && value.ValueKind == JsonValueKind.String
|
||||
? value.GetString()
|
||||
: null;
|
||||
}
|
||||
}
|
||||
using System.Text.Json;
|
||||
using Deal.Api.Extensions;
|
||||
using Deal.Api.Services;
|
||||
using Deal.Modules.Settings.Application.Abstractions;
|
||||
using Deal.Modules.Settings.Application.Models;
|
||||
|
||||
namespace Deal.Api.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// Эндпоинт проверки подключения AI-провайдера: POST /api/ai/check (Ruling 7/8, api-map §4.10).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// «Только для Settings-экрана» (Ruling 8): фронт жмёт «Проверить подключение» (store.js
|
||||
/// checkAiConnection) БЕЗ тела — сервер читает АКТИВНУЮ конфигурацию провайдера тенанта
|
||||
/// (настройки <c>aiProvider</c> + <c>aiConfigs</c> с расшифровкой ключа через <see cref="ISecretCipher"/>;
|
||||
/// 1:1 с ai_svc._cfg(), ai.py L25–33), вызывает порт <see cref="IAiConnectionChecker"/> и отдаёт
|
||||
/// {ok, message} + статус провайдера. Требует сессию: 401 {detail} (формат прототипа).
|
||||
/// Резолв scoped-зависимостей — через RequestServices ПОСЛЕ проверки сессии (как SettingsEndpoints:
|
||||
/// DI-биндинг параметров выполняется до тела, а ISettingsStore требует tenant-контекст запроса).
|
||||
/// </remarks>
|
||||
public static class AiCheckEndpoint
|
||||
{
|
||||
// Префикс группы API (общий для эндпоинтов этапа, Ruling 8).
|
||||
private const string ApiGroupPrefix = "/api";
|
||||
|
||||
// Путь проверки подключения AI-провайдера.
|
||||
private const string AiCheckPath = "/ai/check";
|
||||
|
||||
// OpenAPI-тег группы (в прототипе роутер settings — settings_routes.py).
|
||||
private const string OpenApiTag = "settings";
|
||||
|
||||
/// <summary>
|
||||
/// Регистрирует POST /api/ai/check.
|
||||
/// </summary>
|
||||
/// <param name="app">Построитель маршрутов приложения.</param>
|
||||
/// <returns>Построитель маршрутов для цепочки вызовов.</returns>
|
||||
public static IEndpointRouteBuilder MapAiCheckEndpoint(this IEndpointRouteBuilder app)
|
||||
{
|
||||
var group = app.MapGroup(ApiGroupPrefix).WithTags(OpenApiTag);
|
||||
group.MapPost(AiCheckPath, CheckAsync);
|
||||
return app;
|
||||
}
|
||||
|
||||
// POST /api/ai/check: проверка соединения с активным AI-провайдером тенанта.
|
||||
private static async Task<IResult> CheckAsync(HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (context.GetCurrentUser() is null)
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
|
||||
// Резолв после 401-гейта: ISettingsStore — scoped на TenantDbContext (tenant-контекст запроса).
|
||||
ISettingsStore store = context.RequestServices.GetRequiredService<ISettingsStore>();
|
||||
ISecretCipher secretCipher = context.RequestServices.GetRequiredService<ISecretCipher>();
|
||||
IAiConnectionChecker checker = context.RequestServices.GetRequiredService<IAiConnectionChecker>();
|
||||
|
||||
AiCheckRequest checkRequest = await BuildActiveCheckRequestAsync(store, secretCipher, ct);
|
||||
AiCheckResultDto result = await checker.CheckAsync(checkRequest, ct);
|
||||
return Results.Ok(result);
|
||||
}
|
||||
|
||||
// Собирает запрос проверки из активной конфигурации провайдера (1:1 с ai_svc._cfg()).
|
||||
// store: KV-хранилище настроек тенанта.
|
||||
// secretCipher: Шифр секретов (расшифровка apiKey).
|
||||
// ct: Токен отмены.
|
||||
// Возвращает: Запрос проверки: id провайдера + эффективные base/model + расшифрованный ключ.
|
||||
// Эффективные значения = дефолты SettingsDefaults, перекрытые сохранёнными
|
||||
// переопределениями (Ruling 1); пустое переопределение base/model → дефолт каталога
|
||||
// (семантика «cfg.get(...) or meta[...]» прототипа).
|
||||
private static async Task<AiCheckRequest> BuildActiveCheckRequestAsync(
|
||||
ISettingsStore store,
|
||||
ISecretCipher secretCipher,
|
||||
CancellationToken ct)
|
||||
{
|
||||
string providerId = await ReadActiveProviderIdAsync(store, ct);
|
||||
AiProviderDefinition? meta = AiProviders.All.FirstOrDefault(provider => provider.Id == providerId);
|
||||
|
||||
// Неизвестный id (ручное вмешательство в БД — PATCH-гейт SettingsService не даёт сохранить):
|
||||
// HTTP не выполняется — ответит SSRF-гейт checker (allowlist).
|
||||
if (meta is null)
|
||||
{
|
||||
return new AiCheckRequest(providerId, string.Empty, string.Empty, string.Empty, IsLocal: false, ApiStyle: null);
|
||||
}
|
||||
|
||||
AiConfigSetting config = await ReadEffectiveConfigAsync(store, providerId, ct);
|
||||
string apiKey = secretCipher.Decrypt(config.ApiKey);
|
||||
string baseUrl = string.IsNullOrEmpty(config.BaseUrl) ? meta.Base : config.BaseUrl;
|
||||
string model = string.IsNullOrEmpty(config.Model)
|
||||
? meta.Models.FirstOrDefault() ?? string.Empty
|
||||
: config.Model;
|
||||
|
||||
return new AiCheckRequest(providerId, baseUrl, model, apiKey, meta.Local, meta.ApiStyle);
|
||||
}
|
||||
|
||||
// Читает активный провайдер: сохранённый aiProvider или дефолт (повреждённое значение — дефолт).
|
||||
// store: KV-хранилище настроек тенанта.
|
||||
// ct: Токен отмены.
|
||||
// Возвращает: id провайдера.
|
||||
private static async Task<string> ReadActiveProviderIdAsync(ISettingsStore store, CancellationToken ct)
|
||||
{
|
||||
SettingValue? row = await store.GetAsync(SettingsKeys.AiProvider, ct);
|
||||
if (row is null)
|
||||
{
|
||||
return SettingsDefaults.AiProvider;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using JsonDocument document = JsonDocument.Parse(row.ValueJson);
|
||||
if (document.RootElement.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
return document.RootElement.GetString() ?? SettingsDefaults.AiProvider;
|
||||
}
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
// Повреждённая строка — дефолт (мягкая семантика, как в SettingsService).
|
||||
}
|
||||
|
||||
return SettingsDefaults.AiProvider;
|
||||
}
|
||||
|
||||
// Эффективный конфиг провайдера: дефолт SettingsDefaults, перекрытый сохранённым aiConfigs.
|
||||
// store: KV-хранилище настроек тенанта.
|
||||
// providerId: Активный провайдер (id из каталога).
|
||||
// ct: Токен отмены.
|
||||
// Возвращает: Конфиг {apiKey, baseUrl, model}; повреждённая строка aiConfigs — дефолт.
|
||||
private static async Task<AiConfigSetting> ReadEffectiveConfigAsync(
|
||||
ISettingsStore store,
|
||||
string providerId,
|
||||
CancellationToken ct)
|
||||
{
|
||||
AiConfigSetting defaults = SettingsDefaults.AiConfigs[providerId];
|
||||
SettingValue? row = await store.GetAsync(SettingsKeys.AiConfigs, ct);
|
||||
if (row is null)
|
||||
{
|
||||
return defaults;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using JsonDocument document = JsonDocument.Parse(row.ValueJson);
|
||||
JsonElement root = document.RootElement;
|
||||
if (root.ValueKind == JsonValueKind.Object
|
||||
&& root.TryGetProperty(providerId, out JsonElement entry)
|
||||
&& entry.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
return new AiConfigSetting(
|
||||
ApiKey: ReadField(entry, "apiKey") ?? defaults.ApiKey,
|
||||
BaseUrl: ReadField(entry, "baseUrl") ?? defaults.BaseUrl,
|
||||
Model: ReadField(entry, "model") ?? defaults.Model);
|
||||
}
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
// Повреждённая строка — дефолт (не роняем проверку).
|
||||
}
|
||||
|
||||
return defaults;
|
||||
}
|
||||
|
||||
// Читает строковое поле объекта конфигурации (имена полей camelCase, как пишет SettingsService).
|
||||
// entry: JSON-объект конфигурации провайдера.
|
||||
// field: Имя поля (apiKey/baseUrl/model).
|
||||
// Возвращает: Значение или null, если поле отсутствует/не строка.
|
||||
private static string? ReadField(JsonElement entry, string field)
|
||||
{
|
||||
return entry.TryGetProperty(field, out JsonElement value) && value.ValueKind == JsonValueKind.String
|
||||
? value.GetString()
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user