Перенести настройку ИИ-провайдера в глобальные настройки оператора
Провайдера, модель, baseUrl и API-ключ задаёт оператор: конфигурация хранится в public.global_settings (ключ aiConfig, шифрование enc:), читается общей для всех тенантов и используется AiProviderConfigBuilder во всех ИИ-вызовах. Настройки тенанта больше не содержат aiProvider/aiConfigs, пользовательский POST /api/ai/check удалён. Добавлены операторские ручки GET/PUT /api/operator/settings/ai-config и POST .../check с аудитом ai_config_changed.
This commit is contained in:
@@ -3,9 +3,9 @@ namespace Deal.Modules.Settings.Application.Models;
|
||||
/// <summary>
|
||||
/// Данные проверки подключения AI-провайдера.
|
||||
/// </summary>
|
||||
/// <param name="ProviderId">Идентификатор провайдера (id из каталога <c>AiProviders</c>, ключ в aiConfigs).</param>
|
||||
/// <param name="BaseUrl">Эффективный базовый URL: переопределение aiConfigs или дефолт каталога.</param>
|
||||
/// <param name="Model">Активная модель: переопределение aiConfigs или первая модель каталога.</param>
|
||||
/// <param name="ProviderId">Идентификатор провайдера (id из каталога <c>AiProviders</c>).</param>
|
||||
/// <param name="BaseUrl">Эффективный базовый URL провайдера.</param>
|
||||
/// <param name="Model">Активная модель.</param>
|
||||
/// <param name="ApiKey">API-ключ открытым текстом (пустая строка — ключ не задан).</param>
|
||||
/// <param name="IsLocal">True — локальный сервер (Ollama/LM Studio): ключ не требуется, HTTP не выполняется.</param>
|
||||
/// <param name="ApiStyle">Стиль API: null — OpenAI-совместимый, <c>"anthropic"</c> — Messages API.</param>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
namespace Deal.Modules.Settings.Application.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Результат проверки подключения AI-провайдера — тело POST /api/ai/check.
|
||||
/// Результат проверки подключения AI-провайдера — тело проверки связи операторской конфигурации ИИ.
|
||||
/// </summary>
|
||||
/// <param name="Ok">True — подключение успешно (включая локальные серверы).</param>
|
||||
/// <param name="Message">Сообщение для UI.</param>
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
namespace Deal.Modules.Settings.Application.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Публичная форма конфигурации AI-провайдера в <c>aiConfigs</c>.
|
||||
/// </summary>
|
||||
/// <param name="BaseUrl">Базовый URL API провайдера.</param>
|
||||
/// <param name="Model">Активная модель.</param>
|
||||
/// <param name="KeySet">True — API-ключ задан (после расшифровки не пустой).</param>
|
||||
/// <param name="KeyMasked">Маскированный ключ: пусто, как есть (len ≤ 8) или «1234…5678».</param>
|
||||
public sealed record AiConfigPublicDto(string BaseUrl, string Model, bool KeySet, string KeyMasked);
|
||||
@@ -1,9 +0,0 @@
|
||||
namespace Deal.Modules.Settings.Application.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Сохранённая конфигурация AI-провайдера в настройке <c>aiConfigs</c>
|
||||
/// </summary>
|
||||
/// <param name="ApiKey">API-ключ: пустая строка или зашифрованное значение с префиксом <c>enc:</c>.</param>
|
||||
/// <param name="BaseUrl">Базовый URL API (переопределение дефолта провайдера).</param>
|
||||
/// <param name="Model">Активная модель.</param>
|
||||
public sealed record AiConfigSetting(string ApiKey, string BaseUrl, string Model);
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace Deal.Modules.Settings.Application.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Маскированная форма глобальной конфигурации ИИ — тело ответа операторской ручки GET/PUT ai-config.
|
||||
/// </summary>
|
||||
/// <param name="ProviderId">Id провайдера из каталога <see cref="AiProviders"/>; пусто — конфигурация не задана.</param>
|
||||
/// <param name="BaseUrl">Базовый URL API.</param>
|
||||
/// <param name="Model">Активная модель.</param>
|
||||
/// <param name="KeySet">True — API-ключ задан (после расшифровки не пустой).</param>
|
||||
/// <param name="KeyMasked">Маскированный ключ: пусто, как есть (len ≤ 8) или «1234…5678».</param>
|
||||
/// <param name="Providers">Каталог провайдеров для выбора оператором.</param>
|
||||
public sealed record AiGlobalConfigMaskedDto(
|
||||
string ProviderId,
|
||||
string BaseUrl,
|
||||
string Model,
|
||||
bool KeySet,
|
||||
string KeyMasked,
|
||||
IReadOnlyList<AiProviderPublicDto> Providers);
|
||||
@@ -0,0 +1,29 @@
|
||||
namespace Deal.Modules.Settings.Application.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Снимок глобальной конфигурации ИИ-провайдера с расшифрованным ключом (только для внутреннего использования).
|
||||
/// </summary>
|
||||
/// <param name="ProviderId">Id провайдера из каталога <see cref="AiProviders"/>; пусто — конфигурация не задана.</param>
|
||||
/// <param name="BaseUrl">Базовый URL API (эффективный: сохранённый или дефолт каталога).</param>
|
||||
/// <param name="Model">Активная модель (эффективная: сохранённая или первая из каталога).</param>
|
||||
/// <param name="ApiKey">API-ключ открытым текстом (пусто — не задан; локальным провайдерам не нужен).</param>
|
||||
public sealed record AiGlobalConfigSnapshot(
|
||||
string ProviderId,
|
||||
string BaseUrl,
|
||||
string Model,
|
||||
string ApiKey)
|
||||
{
|
||||
/// <summary>
|
||||
/// Провайдер каталога по <see cref="ProviderId"/> или null для неизвестного/незаданного id
|
||||
/// </summary>
|
||||
public AiProviderDefinition? Meta => AiProviders.All.FirstOrDefault(provider => provider.Id == ProviderId);
|
||||
|
||||
/// <summary>
|
||||
/// Конфигурация готова к вызовам ИИ: провайдер известен, адрес и модель непустые,
|
||||
/// ключ задан (или не нужен локальному провайдеру)
|
||||
/// </summary>
|
||||
public bool Configured => Meta is not null
|
||||
&& BaseUrl.Length > 0
|
||||
&& Model.Length > 0
|
||||
&& (Meta.Local || ApiKey.Length > 0);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace Deal.Modules.Settings.Application.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Сериализуемая форма значения глобальной конфигурации ИИ (ключ — зашифрованный).
|
||||
/// </summary>
|
||||
/// <param name="ProviderId">Id провайдера.</param>
|
||||
/// <param name="ApiKey">Зашифрованный ключ (префикс <c>enc:</c>) или пусто.</param>
|
||||
/// <param name="BaseUrl">Базовый URL.</param>
|
||||
/// <param name="Model">Модель.</param>
|
||||
public sealed record AiGlobalConfigValue(
|
||||
string ProviderId,
|
||||
string ApiKey,
|
||||
string BaseUrl,
|
||||
string Model);
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace Deal.Modules.Settings.Application.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Провайдер ИИ для выбора оператором
|
||||
/// </summary>
|
||||
/// <param name="Id">Идентификатор провайдера из каталога.</param>
|
||||
/// <param name="Name">Человекочитаемое имя.</param>
|
||||
/// <param name="Base">Базовый URL API по умолчанию.</param>
|
||||
/// <param name="Local">True — локальный сервер (ключ не нужен).</param>
|
||||
/// <param name="Models">Доступные модели каталога; пусто — оператор задаёт модель вручную.</param>
|
||||
public sealed record AiProviderPublicDto(
|
||||
string Id,
|
||||
string Name,
|
||||
string Base,
|
||||
bool Local,
|
||||
IReadOnlyList<string> Models);
|
||||
@@ -6,4 +6,9 @@ namespace Deal.Modules.Settings.Application.Models;
|
||||
public static class GlobalSettingsKeys
|
||||
{
|
||||
public const string TelegramKeys = "telegramKeys";
|
||||
|
||||
/// <summary>
|
||||
/// Глобальная конфигурация ИИ-провайдера (задаётся оператором, общая для всех тенантов)
|
||||
/// </summary>
|
||||
public const string AiConfig = "aiConfig";
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
namespace Deal.Modules.Settings.Application.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Провайдер ИИ в public-снимке настроек.
|
||||
/// </summary>
|
||||
/// <param name="Id">Идентификатор провайдера.</param>
|
||||
/// <param name="Name">Человекочитаемое имя.</param>
|
||||
/// <param name="Base">Базовый URL API.</param>
|
||||
/// <param name="Local">True — локальный сервер (ключ не нужен).</param>
|
||||
/// <param name="Models">Доступные модели.</param>
|
||||
public sealed record ProviderPublicDto(string Id, string Name, string Base, bool Local, IReadOnlyList<string> Models);
|
||||
@@ -210,20 +210,4 @@ public sealed record PublicSettingsDto
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, object?> ColState { get; init; } =
|
||||
new Dictionary<string, object?>();
|
||||
|
||||
/// <summary>
|
||||
/// Активный AI-провайдер
|
||||
/// </summary>
|
||||
public string AiProvider { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Публичные конфигурации AI-провайдеров
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, AiConfigPublicDto> AiConfigs { get; init; } =
|
||||
new Dictionary<string, AiConfigPublicDto>();
|
||||
|
||||
/// <summary>
|
||||
/// Статический список AI-провайдеров.
|
||||
/// </summary>
|
||||
public IReadOnlyList<ProviderPublicDto> Providers { get; init; } = Array.Empty<ProviderPublicDto>();
|
||||
}
|
||||
|
||||
@@ -35,11 +35,6 @@ public enum SettingKind
|
||||
/// </summary>
|
||||
MyPrompts,
|
||||
|
||||
/// <summary>
|
||||
/// Конфигурации AI-провайдеров с ключами
|
||||
/// </summary>
|
||||
AiConfigs,
|
||||
|
||||
/// <summary>
|
||||
/// Внутренний (непубличный) ключ
|
||||
/// </summary>
|
||||
|
||||
@@ -207,18 +207,6 @@ public static class SettingsDefaults
|
||||
/// </summary>
|
||||
public static readonly IReadOnlyDictionary<string, object?> ColState = new Dictionary<string, object?>();
|
||||
|
||||
// ── ИИ ──
|
||||
|
||||
/// <summary>
|
||||
/// Дефолт «aiProvider»
|
||||
/// </summary>
|
||||
public const string AiProvider = "deepseek";
|
||||
|
||||
/// <summary>
|
||||
/// Дефолт «aiConfigs»
|
||||
/// </summary>
|
||||
public static readonly IReadOnlyDictionary<string, AiConfigSetting> AiConfigs = BuildDefaultAiConfigs();
|
||||
|
||||
// ── Telegram / Discovery ──
|
||||
|
||||
/// <summary>
|
||||
@@ -255,16 +243,4 @@ public static class SettingsDefaults
|
||||
/// Дефолт «discPaused»
|
||||
/// </summary>
|
||||
public const bool DiscPaused = false;
|
||||
|
||||
private static IReadOnlyDictionary<string, AiConfigSetting> BuildDefaultAiConfigs()
|
||||
{
|
||||
var result = new Dictionary<string, AiConfigSetting>(AiProviders.All.Count);
|
||||
foreach (AiProviderDefinition provider in AiProviders.All)
|
||||
{
|
||||
string firstModel = provider.Models.FirstOrDefault() ?? string.Empty;
|
||||
result.Add(provider.Id, new AiConfigSetting(ApiKey: "", BaseUrl: provider.Base, Model: firstModel));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,8 +104,6 @@ public static class SettingsKeys
|
||||
|
||||
public const string RateSource = "rateSource";
|
||||
|
||||
public const string AiProvider = "aiProvider";
|
||||
|
||||
public const string AiPrompt = "aiPrompt";
|
||||
|
||||
public const string AiFilterPrompt = "aiFilterPrompt";
|
||||
@@ -147,8 +145,6 @@ public static class SettingsKeys
|
||||
|
||||
public const string MyPrompts = "myPrompts";
|
||||
|
||||
public const string AiConfigs = "aiConfigs";
|
||||
|
||||
/// <summary>
|
||||
/// Каталог публичных ключей
|
||||
/// </summary>
|
||||
@@ -181,7 +177,6 @@ public static class SettingsKeys
|
||||
// String
|
||||
[TargetCurrency] = SettingKind.String,
|
||||
[RateSource] = SettingKind.String,
|
||||
[AiProvider] = SettingKind.String,
|
||||
[AiPrompt] = SettingKind.String,
|
||||
[AiFilterPrompt] = SettingKind.String,
|
||||
[CardPrompt] = SettingKind.String,
|
||||
@@ -202,7 +197,6 @@ public static class SettingsKeys
|
||||
[ColState] = SettingKind.Dict,
|
||||
// special
|
||||
[MyPrompts] = SettingKind.MyPrompts,
|
||||
[AiConfigs] = SettingKind.AiConfigs,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user