Убрать неиспользуемые using по код-стайлу
Прогон dotnet format (IDE0005) по 4 решениям: удалены лишние using, оставшиеся после миграции namespace (676 файлов).
This commit is contained in:
@@ -1,173 +1,166 @@
|
||||
using System.Text.Json.Nodes;
|
||||
using Deal.Grpc.Ai;
|
||||
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.Infrastructure.Integrations.Abstractions;
|
||||
using Deal.Infrastructure.Integrations.Exceptions;
|
||||
using Deal.Infrastructure.Integrations.Extensions;
|
||||
using Deal.Infrastructure.Integrations.Models;
|
||||
using Deal.Infrastructure.Integrations.Options;
|
||||
|
||||
namespace Deal.Infrastructure.Integrations.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Собирает конфиг активного ИИ-провайдера для запросов ai-service (Ruling 5: ядро расшифровывает
|
||||
/// aiConfigs и передаёт ProviderConfig в теле каждого запроса; сервис настроек тенанта не знает).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Эффективный конфиг 1:1 с python <c>ai.py _cfg()</c> L25–33 и формой ProviderConfig (ai.proto L70–85):
|
||||
/// активный провайдер — настройка <c>aiProvider</c> (дефолт «deepseek»), каталог — <see cref="AiProviders"/>
|
||||
/// (fallback на первый — deepseek, как python L28); из переопределения <c>aiConfigs</c> берутся
|
||||
/// apiKey/baseUrl/model, отсутствующие поля дополняются дефолтами каталога (base провайдера, первая модель);
|
||||
/// apiKey расшифровывается (значения <c>enc:</c>+… через <see cref="ISecretCipher"/>; незашифрованные ранних
|
||||
/// версий — как есть, python crypto.decrypt_text L52–61); api_style провайдера — из каталога (Anthropic —
|
||||
/// «anthropic», остальные — пусто = OpenAI-совместимый). Scoped: читает KV-настройки тенанта (ISettingsStore →
|
||||
/// scoped TenantDbContext запроса), как LocalAiClassifier/GrpcMlClient.
|
||||
/// </remarks>
|
||||
public sealed class AiProviderConfigBuilder
|
||||
{
|
||||
// Ключ aiConfigs: поле apiKey переопределения провайдера.
|
||||
private const string ApiKeyField = "apiKey";
|
||||
|
||||
// Ключ aiConfigs: поле baseUrl переопределения провайдера.
|
||||
private const string BaseUrlField = "baseUrl";
|
||||
|
||||
// Ключ aiConfigs: поле model переопределения провайдера.
|
||||
private const string ModelField = "model";
|
||||
|
||||
// Префикс зашифрованного значения apiKey (crypto.py L49: enc: + Base64(nonce‖ct‖tag)).
|
||||
private const string EncryptedPrefix = "enc:";
|
||||
|
||||
private readonly ISettingsStore _store;
|
||||
private readonly ISecretCipher _secretCipher;
|
||||
|
||||
/// <summary>
|
||||
/// Создаёт сборщик конфига провайдера.
|
||||
/// </summary>
|
||||
/// <param name="store">KV-хранилище настроек тенанта (aiProvider/aiConfigs).</param>
|
||||
/// <param name="secretCipher">Расшифровка секрета aiConfigs.apiKey (AES-GCM, Ruling 2).</param>
|
||||
public AiProviderConfigBuilder(ISettingsStore store, ISecretCipher secretCipher)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(store);
|
||||
ArgumentNullException.ThrowIfNull(secretCipher);
|
||||
_store = store;
|
||||
_secretCipher = secretCipher;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Собирает ProviderConfig активного провайдера для тела запроса ai-service.
|
||||
/// </summary>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
/// <returns>Конфиг: provider_id/base/model/api_key (расшифрованный)/api_style (см. ai.proto).</returns>
|
||||
public async Task<ProviderConfig> BuildAsync(CancellationToken ct)
|
||||
{
|
||||
string providerId = await ReadProviderIdAsync(ct);
|
||||
AiProviderDefinition meta = AiProviders.All.FirstOrDefault(provider => provider.Id == providerId)
|
||||
?? AiProviders.All[0]; // неизвестный id — дефолтный провайдер (python L28)
|
||||
|
||||
JsonObject? overrides = await ReadAiConfigsOverrideAsync(ct);
|
||||
JsonObject? raw = overrides?[meta.Id] as JsonObject;
|
||||
|
||||
string apiKey = ReadField(raw, ApiKeyField);
|
||||
if (apiKey.StartsWith(EncryptedPrefix, StringComparison.Ordinal))
|
||||
{
|
||||
apiKey = _secretCipher.Decrypt(apiKey);
|
||||
}
|
||||
|
||||
string baseUrl = ReadField(raw, BaseUrlField);
|
||||
if (baseUrl.Length == 0)
|
||||
{
|
||||
baseUrl = meta.Base; // python L90: cfg.baseUrl or meta.base
|
||||
}
|
||||
|
||||
string model = ReadField(raw, ModelField);
|
||||
if (model.Length == 0)
|
||||
{
|
||||
model = meta.Models.FirstOrDefault() ?? string.Empty; // python L91: cfg.model or models[0]
|
||||
}
|
||||
|
||||
var config = new ProviderConfig
|
||||
{
|
||||
ProviderId = meta.Id,
|
||||
BaseUrl = baseUrl,
|
||||
Model = model,
|
||||
};
|
||||
if (apiKey.Length > 0)
|
||||
{
|
||||
config.ApiKey = apiKey;
|
||||
}
|
||||
|
||||
if (meta.ApiStyle is { Length: > 0 } apiStyle)
|
||||
{
|
||||
config.ApiStyle = apiStyle;
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
// Активный провайдер из настройки aiProvider (дефолт «deepseek», python L26).
|
||||
// ct: Токен отмены.
|
||||
// Возвращает: Id провайдера (каталога AiProviders).
|
||||
private async Task<string> ReadProviderIdAsync(CancellationToken ct)
|
||||
{
|
||||
SettingValue? row = await _store.GetAsync(SettingsKeys.AiProvider, ct);
|
||||
if (row is null)
|
||||
{
|
||||
return SettingsDefaults.AiProvider;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
JsonNode? value = JsonNode.Parse(row.ValueJson);
|
||||
if (value is JsonValue scalar && scalar.TryGetValue<string>(out string? providerId)
|
||||
&& !string.IsNullOrWhiteSpace(providerId))
|
||||
{
|
||||
return providerId;
|
||||
}
|
||||
}
|
||||
catch (System.Text.Json.JsonException)
|
||||
{
|
||||
// Повреждённая строка — дефолт (мягкая семантика, как в SettingsService).
|
||||
}
|
||||
|
||||
return SettingsDefaults.AiProvider;
|
||||
}
|
||||
|
||||
// Переопределение aiConfigs тенанта (JSON-объект «id провайдера → конфиг»); null — дефолты.
|
||||
// ct: Токен отмены.
|
||||
// Возвращает: Объект переопределения или null.
|
||||
private async Task<JsonObject?> ReadAiConfigsOverrideAsync(CancellationToken ct)
|
||||
{
|
||||
SettingValue? row = await _store.GetAsync(SettingsKeys.AiConfigs, ct);
|
||||
if (row is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return JsonNode.Parse(row.ValueJson) as JsonObject;
|
||||
}
|
||||
catch (System.Text.Json.JsonException)
|
||||
{
|
||||
// Повреждённая строка — дефолты (мягкая семантика, как в SettingsService).
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Строковое поле конфига провайдера (отсутствие/null/не-строка → пустая строка).
|
||||
// config: Объект конфига провайдера (может быть null — дефолты).
|
||||
// field: Имя поля (apiKey/baseUrl/model).
|
||||
// Возвращает: Значение строкой или пустая строка.
|
||||
private static string ReadField(JsonObject? config, string field)
|
||||
{
|
||||
if (config is null || !config.TryGetPropertyValue(field, out JsonNode? node) || node is not JsonValue value)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return value.TryGetValue<string>(out string? text) ? text ?? string.Empty : string.Empty;
|
||||
}
|
||||
}
|
||||
using System.Text.Json.Nodes;
|
||||
using Deal.Grpc.Ai;
|
||||
using Deal.Modules.Settings.Application.Abstractions;
|
||||
using Deal.Modules.Settings.Application.Models;
|
||||
|
||||
namespace Deal.Infrastructure.Integrations.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Собирает конфиг активного ИИ-провайдера для запросов ai-service (Ruling 5: ядро расшифровывает
|
||||
/// aiConfigs и передаёт ProviderConfig в теле каждого запроса; сервис настроек тенанта не знает).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Эффективный конфиг 1:1 с python <c>ai.py _cfg()</c> L25–33 и формой ProviderConfig (ai.proto L70–85):
|
||||
/// активный провайдер — настройка <c>aiProvider</c> (дефолт «deepseek»), каталог — <see cref="AiProviders"/>
|
||||
/// (fallback на первый — deepseek, как python L28); из переопределения <c>aiConfigs</c> берутся
|
||||
/// apiKey/baseUrl/model, отсутствующие поля дополняются дефолтами каталога (base провайдера, первая модель);
|
||||
/// apiKey расшифровывается (значения <c>enc:</c>+… через <see cref="ISecretCipher"/>; незашифрованные ранних
|
||||
/// версий — как есть, python crypto.decrypt_text L52–61); api_style провайдера — из каталога (Anthropic —
|
||||
/// «anthropic», остальные — пусто = OpenAI-совместимый). Scoped: читает KV-настройки тенанта (ISettingsStore →
|
||||
/// scoped TenantDbContext запроса), как LocalAiClassifier/GrpcMlClient.
|
||||
/// </remarks>
|
||||
public sealed class AiProviderConfigBuilder
|
||||
{
|
||||
// Ключ aiConfigs: поле apiKey переопределения провайдера.
|
||||
private const string ApiKeyField = "apiKey";
|
||||
|
||||
// Ключ aiConfigs: поле baseUrl переопределения провайдера.
|
||||
private const string BaseUrlField = "baseUrl";
|
||||
|
||||
// Ключ aiConfigs: поле model переопределения провайдера.
|
||||
private const string ModelField = "model";
|
||||
|
||||
// Префикс зашифрованного значения apiKey (crypto.py L49: enc: + Base64(nonce‖ct‖tag)).
|
||||
private const string EncryptedPrefix = "enc:";
|
||||
|
||||
private readonly ISettingsStore _store;
|
||||
private readonly ISecretCipher _secretCipher;
|
||||
|
||||
/// <summary>
|
||||
/// Создаёт сборщик конфига провайдера.
|
||||
/// </summary>
|
||||
/// <param name="store">KV-хранилище настроек тенанта (aiProvider/aiConfigs).</param>
|
||||
/// <param name="secretCipher">Расшифровка секрета aiConfigs.apiKey (AES-GCM, Ruling 2).</param>
|
||||
public AiProviderConfigBuilder(ISettingsStore store, ISecretCipher secretCipher)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(store);
|
||||
ArgumentNullException.ThrowIfNull(secretCipher);
|
||||
_store = store;
|
||||
_secretCipher = secretCipher;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Собирает ProviderConfig активного провайдера для тела запроса ai-service.
|
||||
/// </summary>
|
||||
/// <param name="ct">Токен отмены.</param>
|
||||
/// <returns>Конфиг: provider_id/base/model/api_key (расшифрованный)/api_style (см. ai.proto).</returns>
|
||||
public async Task<ProviderConfig> BuildAsync(CancellationToken ct)
|
||||
{
|
||||
string providerId = await ReadProviderIdAsync(ct);
|
||||
AiProviderDefinition meta = AiProviders.All.FirstOrDefault(provider => provider.Id == providerId)
|
||||
?? AiProviders.All[0]; // неизвестный id — дефолтный провайдер (python L28)
|
||||
|
||||
JsonObject? overrides = await ReadAiConfigsOverrideAsync(ct);
|
||||
JsonObject? raw = overrides?[meta.Id] as JsonObject;
|
||||
|
||||
string apiKey = ReadField(raw, ApiKeyField);
|
||||
if (apiKey.StartsWith(EncryptedPrefix, StringComparison.Ordinal))
|
||||
{
|
||||
apiKey = _secretCipher.Decrypt(apiKey);
|
||||
}
|
||||
|
||||
string baseUrl = ReadField(raw, BaseUrlField);
|
||||
if (baseUrl.Length == 0)
|
||||
{
|
||||
baseUrl = meta.Base; // python L90: cfg.baseUrl or meta.base
|
||||
}
|
||||
|
||||
string model = ReadField(raw, ModelField);
|
||||
if (model.Length == 0)
|
||||
{
|
||||
model = meta.Models.FirstOrDefault() ?? string.Empty; // python L91: cfg.model or models[0]
|
||||
}
|
||||
|
||||
var config = new ProviderConfig
|
||||
{
|
||||
ProviderId = meta.Id,
|
||||
BaseUrl = baseUrl,
|
||||
Model = model,
|
||||
};
|
||||
if (apiKey.Length > 0)
|
||||
{
|
||||
config.ApiKey = apiKey;
|
||||
}
|
||||
|
||||
if (meta.ApiStyle is { Length: > 0 } apiStyle)
|
||||
{
|
||||
config.ApiStyle = apiStyle;
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
// Активный провайдер из настройки aiProvider (дефолт «deepseek», python L26).
|
||||
// ct: Токен отмены.
|
||||
// Возвращает: Id провайдера (каталога AiProviders).
|
||||
private async Task<string> ReadProviderIdAsync(CancellationToken ct)
|
||||
{
|
||||
SettingValue? row = await _store.GetAsync(SettingsKeys.AiProvider, ct);
|
||||
if (row is null)
|
||||
{
|
||||
return SettingsDefaults.AiProvider;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
JsonNode? value = JsonNode.Parse(row.ValueJson);
|
||||
if (value is JsonValue scalar && scalar.TryGetValue<string>(out string? providerId)
|
||||
&& !string.IsNullOrWhiteSpace(providerId))
|
||||
{
|
||||
return providerId;
|
||||
}
|
||||
}
|
||||
catch (System.Text.Json.JsonException)
|
||||
{
|
||||
// Повреждённая строка — дефолт (мягкая семантика, как в SettingsService).
|
||||
}
|
||||
|
||||
return SettingsDefaults.AiProvider;
|
||||
}
|
||||
|
||||
// Переопределение aiConfigs тенанта (JSON-объект «id провайдера → конфиг»); null — дефолты.
|
||||
// ct: Токен отмены.
|
||||
// Возвращает: Объект переопределения или null.
|
||||
private async Task<JsonObject?> ReadAiConfigsOverrideAsync(CancellationToken ct)
|
||||
{
|
||||
SettingValue? row = await _store.GetAsync(SettingsKeys.AiConfigs, ct);
|
||||
if (row is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return JsonNode.Parse(row.ValueJson) as JsonObject;
|
||||
}
|
||||
catch (System.Text.Json.JsonException)
|
||||
{
|
||||
// Повреждённая строка — дефолты (мягкая семантика, как в SettingsService).
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Строковое поле конфига провайдера (отсутствие/null/не-строка → пустая строка).
|
||||
// config: Объект конфига провайдера (может быть null — дефолты).
|
||||
// field: Имя поля (apiKey/baseUrl/model).
|
||||
// Возвращает: Значение строкой или пустая строка.
|
||||
private static string ReadField(JsonObject? config, string field)
|
||||
{
|
||||
if (config is null || !config.TryGetPropertyValue(field, out JsonNode? node) || node is not JsonValue value)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return value.TryGetValue<string>(out string? text) ? text ?? string.Empty : string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user