feat: внедрение RazorConsole TUI с runtime-управлением MCP-инструментами

- Добавлен RazorConsole.Core для интерактивного TUI-дашборда
- ToolRegistryService: живое включение/отключение модулей и отдельных методов
- InMemoryLogSink: кольцевой буфер логов с фильтрацией по модулю
- TUI: 3 таба (Overview, Logs, Settings)
- IToolModule: generic-интерфейс для легкого добавления новых MCP-модулей
- Guard-проверка TryCheckEnabled() во всех существующих инструментах
This commit is contained in:
2026-04-13 17:31:28 +03:00
parent c117d928b0
commit 879becadfe
24 changed files with 826 additions and 11 deletions

View File

@@ -0,0 +1,96 @@
using System.Collections.Concurrent;
namespace LazyBear.MCP.Services.ToolRegistry;
/// <summary>
/// Singleton. Хранит runtime-состояние включённости модулей и отдельных инструментов.
/// Thread-safe. Уведомляет подписчиков при любом изменении.
/// </summary>
public sealed class ToolRegistryService
{
private readonly ConcurrentDictionary<string, bool> _moduleEnabled = new(StringComparer.OrdinalIgnoreCase);
private readonly ConcurrentDictionary<string, bool> _toolEnabled = new(StringComparer.OrdinalIgnoreCase);
private readonly List<IToolModule> _modules = [];
private readonly Lock _modulesLock = new();
/// <summary>Вызывается при любом изменении состояния.</summary>
public event Action? StateChanged;
// ── Регистрация ──────────────────────────────────────────────────────────
public void RegisterModule(IToolModule module)
{
lock (_modulesLock)
{
if (_modules.Any(m => string.Equals(m.ModuleName, module.ModuleName, StringComparison.OrdinalIgnoreCase)))
return;
_modules.Add(module);
}
_moduleEnabled.TryAdd(module.ModuleName, true);
foreach (var tool in module.ToolNames)
{
_toolEnabled.TryAdd(MakeKey(module.ModuleName, tool), true);
}
}
// ── Запросы состояния ────────────────────────────────────────────────────
public IReadOnlyList<IToolModule> GetModules()
{
lock (_modulesLock)
{
return _modules.ToList();
}
}
public bool IsModuleEnabled(string moduleName) =>
_moduleEnabled.GetValueOrDefault(moduleName, true);
public bool IsToolEnabled(string moduleName, string toolName) =>
IsModuleEnabled(moduleName) &&
_toolEnabled.GetValueOrDefault(MakeKey(moduleName, toolName), true);
// ── Переключение ─────────────────────────────────────────────────────────
public void SetModuleEnabled(string moduleName, bool enabled)
{
_moduleEnabled[moduleName] = enabled;
StateChanged?.Invoke();
}
public void SetToolEnabled(string moduleName, string toolName, bool enabled)
{
_toolEnabled[MakeKey(moduleName, toolName)] = enabled;
StateChanged?.Invoke();
}
public void ToggleModule(string moduleName) =>
SetModuleEnabled(moduleName, !IsModuleEnabled(moduleName));
public void ToggleTool(string moduleName, string toolName) =>
SetToolEnabled(moduleName, toolName, !IsToolEnabled(moduleName, toolName));
// ── Счётчики для Overview ─────────────────────────────────────────────────
public (int Active, int Total) GetToolCounts(string moduleName)
{
lock (_modulesLock)
{
var module = _modules.FirstOrDefault(m =>
string.Equals(m.ModuleName, moduleName, StringComparison.OrdinalIgnoreCase));
if (module is null) return (0, 0);
var total = module.ToolNames.Count;
var active = module.ToolNames.Count(t => IsToolEnabled(moduleName, t));
return (active, total);
}
}
// ── Helpers ───────────────────────────────────────────────────────────────
private static string MakeKey(string module, string tool) => $"{module}::{tool}";
}