using System.Globalization; using System.Text.Json; using Deal.Modules.Settings.Application.Models; using Deal.SharedKernel.Utilities; namespace Deal.Modules.Settings.Application.Services; public sealed partial class SettingsService { private async Task> LoadStoredOverridesAsync(CancellationToken ct) { var result = new Dictionary(StringComparer.Ordinal); IReadOnlyCollection rows = await store.GetAllAsync(ct); foreach (SettingValue row in rows) { try { using JsonDocument document = JsonDocument.Parse(row.ValueJson); result[row.Key] = document.RootElement.Clone(); } catch (JsonException) { // Повреждённая строка не должна ронять снимок: значение трактуется как отсутствующее (дефолт). } } return result; } private static bool TryReadInt(JsonElement element, out int value) { long wide; if (element.ValueKind == JsonValueKind.Number) { if (!element.TryGetInt64(out wide)) { value = 0; return false; } } else if (element.ValueKind == JsonValueKind.String) { if (!long.TryParse(element.GetString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out wide)) { value = 0; return false; } } else { value = 0; return false; } // Диапазон long шире int: сжимаем (клампы PATCH всё равно ограничены диапазоном меньше int32). value = (int)Math.Clamp(wide, int.MinValue, int.MaxValue); return true; } private static bool TryReadBool(JsonElement element, out bool value) { if (element.ValueKind is JsonValueKind.True or JsonValueKind.False) { value = element.GetBoolean(); return true; } value = false; return false; } private static bool TryReadText(JsonElement element, out string? text) { switch (element.ValueKind) { case JsonValueKind.String: text = element.GetString(); return true; case JsonValueKind.Number: text = element.GetRawText(); return true; case JsonValueKind.True: text = BoolText.True; return true; case JsonValueKind.False: text = BoolText.False; return true; default: text = null; return false; } } // Читает строковое поле объекта (JSON null / отсутствие / не-скаляр → False). // obj: JSON-объект. // name: Имя поля. // text: Значение поля. // Возвращает: True — поле присутствует и скалярно. private static bool TryReadFieldText( JsonElement obj, string name, out string text) { if (obj.TryGetProperty(name, out JsonElement element) && TryReadText(element, out string? value) && value is not null) { text = value; return true; } text = string.Empty; return false; } // Кламп целого в диапазон. // value: Исходное значение. // min: Нижняя граница. // max: Верхняя граница. // Возвращает: Значение в диапазоне [min..max]. private static int Clamp( int value, int min, int max) => Math.Max(min, Math.Min(max, value)); // Объединяет дефолт и переопределение целочисленного ключа. private static int MergeInt( Dictionary overrides, string key, int defaultValue) { return overrides.TryGetValue(key, out JsonElement element) && TryReadInt(element, out int value) ? value : defaultValue; } // Объединяет дефолт и переопределение булева ключа. private static bool MergeBool( Dictionary overrides, string key, bool defaultValue) { return overrides.TryGetValue(key, out JsonElement element) && TryReadBool(element, out bool value) ? value : defaultValue; } // Объединяет дефолт и переопределение строкового ключа. private static string MergeString( Dictionary overrides, string key, string defaultValue) { return overrides.TryGetValue(key, out JsonElement element) && TryReadText(element, out string? value) && value is not null ? value : defaultValue; } // Объединяет дефолт и переопределение списка строк. private static IReadOnlyList MergeStringList( Dictionary overrides, string key, IReadOnlyList defaultValue) { if (overrides.TryGetValue(key, out JsonElement element) && element.ValueKind == JsonValueKind.Array) { var items = new List(); foreach (JsonElement item in element.EnumerateArray()) { if (TryReadText(item, out string? text) && text is not null) { items.Add(text); } } return items; } return defaultValue; } // Объединяет дефолт и переопределение словаря (colState): значения сохраняются как JsonElement. private static IReadOnlyDictionary MergeDict(Dictionary overrides, string key) { if (overrides.TryGetValue(key, out JsonElement element) && element.ValueKind == JsonValueKind.Object) { var result = new Dictionary(StringComparer.Ordinal); foreach (JsonProperty property in element.EnumerateObject()) { result[property.Name] = property.Value.Clone(); } return result; } return new Dictionary(StringComparer.Ordinal); } // Объединяет дефолты aiConfigs и сохранённое переопределение (переопределение — на провайдера). // defaults: Дефолтные конфиги на всех провайдеров каталога. // overrides: Сохранённые переопределения. // Возвращает: Эффективные конфиги: провайдеры дефолтов + перекрытия из хранилища (неизвестные пропускаются). private static IReadOnlyDictionary MergeAiConfigs(IReadOnlyDictionary defaults, Dictionary overrides) { var merged = defaults.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.Ordinal); if (overrides.TryGetValue(SettingsKeys.AiConfigs, out JsonElement element) && element.ValueKind == JsonValueKind.Object) { foreach (JsonProperty provider in element.EnumerateObject()) { // «Только существующие провайдеры»: неизвестные id из хранилища не подмешиваются. if (!merged.ContainsKey(provider.Name) || provider.Value.ValueKind != JsonValueKind.Object) { continue; } string apiKey = TryReadFieldText(provider.Value, SettingsFieldKeys.ApiKey, out string storedKey) ? storedKey : string.Empty; string baseUrl = TryReadFieldText(provider.Value, SettingsFieldKeys.BaseUrl, out string storedBase) ? storedBase : string.Empty; string model = TryReadFieldText(provider.Value, SettingsFieldKeys.Model, out string storedModel) ? storedModel : string.Empty; merged[provider.Name] = new AiConfigSetting(apiKey, baseUrl, model); } } return merged; } // Читает сохранённые «Мои промпты»; повреждённое значение → пустой список (дефолт). // overrides: Сохранённые переопределения. // Возвращает: Список промптов из хранилища или дефолт (пустой). private static IReadOnlyList MergeMyPrompts(Dictionary overrides) { if (overrides.TryGetValue(SettingsKeys.MyPrompts, out JsonElement element) && element.ValueKind == JsonValueKind.Array) { try { return JsonSerializer.Deserialize>(element.GetRawText(), JsonOptions) ?? new List(); } catch (JsonException) { return Array.Empty(); } } return SettingsDefaults.MyPrompts; } }