SaaS-мониторинг Telegram: ядро (модули Cards/Kanban/Pipeline/Tenants/Settings/ Discovery, Api, Infrastructure), сервисы telegram/ai/ml/storage, фронт Vue, контракты и grpc-hosting, деплой-конфиги (dev/prod/observability/CI-раннер), Gitea Actions CI, документация (ТЗ, техдок, api-map, код-стайл, планы, бэклог). Текущее состояние: все этапы роадмапа 0–12 закрыты, сборка 5 sln 0/0, тесты 1340/130/52/38/9 зелёные.
This commit is contained in:
@@ -0,0 +1,251 @@
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using Deal.Modules.Settings.Application.Models;
|
||||
|
||||
namespace Deal.Modules.Settings.Application.Services;
|
||||
|
||||
public sealed partial class SettingsService
|
||||
{
|
||||
private async Task<Dictionary<string, JsonElement>> LoadStoredOverridesAsync(CancellationToken ct)
|
||||
{
|
||||
var result = new Dictionary<string, JsonElement>(StringComparer.Ordinal);
|
||||
IReadOnlyCollection<SettingValue> 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 = "true";
|
||||
return true;
|
||||
|
||||
case JsonValueKind.False:
|
||||
text = "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<string, JsonElement> overrides,
|
||||
string key,
|
||||
int defaultValue)
|
||||
{
|
||||
return overrides.TryGetValue(key, out JsonElement element) && TryReadInt(element, out int value)
|
||||
? value
|
||||
: defaultValue;
|
||||
}
|
||||
|
||||
// Объединяет дефолт и переопределение булева ключа.
|
||||
private static bool MergeBool(
|
||||
Dictionary<string, JsonElement> overrides,
|
||||
string key,
|
||||
bool defaultValue)
|
||||
{
|
||||
return overrides.TryGetValue(key, out JsonElement element) && TryReadBool(element, out bool value)
|
||||
? value
|
||||
: defaultValue;
|
||||
}
|
||||
|
||||
// Объединяет дефолт и переопределение строкового ключа.
|
||||
private static string MergeString(
|
||||
Dictionary<string, JsonElement> 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<string> MergeStringList(
|
||||
Dictionary<string, JsonElement> overrides,
|
||||
string key,
|
||||
IReadOnlyList<string> defaultValue)
|
||||
{
|
||||
if (overrides.TryGetValue(key, out JsonElement element) && element.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
var items = new List<string>();
|
||||
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<string, object?> MergeDict(Dictionary<string, JsonElement> overrides, string key)
|
||||
{
|
||||
if (overrides.TryGetValue(key, out JsonElement element) && element.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
var result = new Dictionary<string, object?>(StringComparer.Ordinal);
|
||||
foreach (JsonProperty property in element.EnumerateObject())
|
||||
{
|
||||
result[property.Name] = property.Value.Clone();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
return new Dictionary<string, object?>(StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
// Объединяет дефолты aiConfigs и сохранённое переопределение (переопределение — на провайдера).
|
||||
// defaults: Дефолтные конфиги на всех провайдеров каталога.
|
||||
// overrides: Сохранённые переопределения.
|
||||
// Возвращает: Эффективные конфиги: провайдеры дефолтов + перекрытия из хранилища (неизвестные пропускаются).
|
||||
private static IReadOnlyDictionary<string, AiConfigSetting> MergeAiConfigs(IReadOnlyDictionary<string, AiConfigSetting> defaults, Dictionary<string, JsonElement> 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, "apiKey", out string storedKey) ? storedKey : string.Empty;
|
||||
string baseUrl = TryReadFieldText(provider.Value, "baseUrl", out string storedBase) ? storedBase : string.Empty;
|
||||
string model = TryReadFieldText(provider.Value, "model", out string storedModel) ? storedModel : string.Empty;
|
||||
merged[provider.Name] = new AiConfigSetting(apiKey, baseUrl, model);
|
||||
}
|
||||
}
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
// Читает сохранённые «Мои промпты»; повреждённое значение → пустой список (дефолт).
|
||||
// overrides: Сохранённые переопределения.
|
||||
// Возвращает: Список промптов из хранилища или дефолт (пустой).
|
||||
private static IReadOnlyList<MyPromptDto> MergeMyPrompts(Dictionary<string, JsonElement> overrides)
|
||||
{
|
||||
if (overrides.TryGetValue(SettingsKeys.MyPrompts, out JsonElement element) && element.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Deserialize<List<MyPromptDto>>(element.GetRawText(), JsonOptions)
|
||||
?? new List<MyPromptDto>();
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return Array.Empty<MyPromptDto>();
|
||||
}
|
||||
}
|
||||
|
||||
return SettingsDefaults.MyPrompts;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user