Литералы статусов/видов/фаз и wire-ключей заменены каталогами: значения сущностей и репозиториев — TenantStatuses/TenantLimitPeriods/InviteStatuses/ContainerKinds/ContainerSpaces/CardIds/Discovery*/PipelineQueueStatuses; контакты и виды вложений — CardContactTypes/CardFileKinds; источники и этапы отсева — PipelineRejectStages/PipelineRejectSources; ключи промптов/провайдеров — SettingsFieldKeys; wire-ключи канбана — KanbanWireKeys; режимы правил — ColumnRuleModes; операции/фазы/виды диалогов/контакты Telegram и общий BoolText — в Deal.Contracts/Deal.SharedKernel; имена метрик — DealMetrics. Значения не менялись.
253 lines
9.5 KiB
C#
253 lines
9.5 KiB
C#
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<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 = 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<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, 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<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;
|
|
}
|
|
}
|