diff --git a/app/MindWork AI Studio/Layout/MainLayout.razor.cs b/app/MindWork AI Studio/Layout/MainLayout.razor.cs index 32d9e981..1145c830 100644 --- a/app/MindWork AI Studio/Layout/MainLayout.razor.cs +++ b/app/MindWork AI Studio/Layout/MainLayout.razor.cs @@ -82,8 +82,19 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, IDis // Ensure that all settings are loaded: await this.SettingsManager.LoadSettings(); - // Ensure that all internal plugins are present: - await PluginFactory.EnsureInternalPlugins(); + // + // We cannot process the plugins before the settings are loaded, + // and we know our data directory. + // + if(PreviewFeatures.PRE_PLUGINS_2025.IsEnabled(this.SettingsManager)) + { + // Ensure that all internal plugins are present: + await PluginFactory.EnsureInternalPlugins(); + + // Load (but not start) all plugins: + var pluginLoadingTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + await PluginFactory.LoadAll(pluginLoadingTimeout.Token); + } // Register this component with the message bus: this.MessageBus.RegisterComponent(this); diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs index f60ff6f7..7b158af2 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs @@ -1,3 +1,5 @@ +using System.Text; + using AIStudio.Settings; using Lua; @@ -9,10 +11,81 @@ public static partial class PluginFactory private static readonly ILogger LOG = Program.LOGGER_FACTORY.CreateLogger("PluginFactory"); private static readonly string DATA_DIR = SettingsManager.DataDirectory!; private static readonly string PLUGINS_ROOT = Path.Join(DATA_DIR, "plugins"); + private static readonly Dictionary AVAILABLE_PLUGINS = new(); + private static readonly SettingsManager SETTINGS = Program.SERVICE_PROVIDER.GetRequiredService(); - public static async Task LoadAll() + /// + /// A list of all available plugins. + /// + public static IEnumerable AvailablePlugins => AVAILABLE_PLUGINS.Keys; + + /// + /// A list of all enabled plugins. + /// + public static IEnumerable EnabledPlugins => AVAILABLE_PLUGINS.Keys.Where(x => SETTINGS.ConfigurationData.EnabledPlugins.Contains(x.Id)); + + /// + /// A list of all disabled plugins. + /// + public static IEnumerable DisabledPlugins => AVAILABLE_PLUGINS.Keys.Where(x => !SETTINGS.ConfigurationData.EnabledPlugins.Contains(x.Id)); + + /// + /// Try to load all plugins from the plugins directory. + /// + /// + /// Loading plugins means:
+ /// - Parsing and checking the plugin code
+ /// - Check for forbidden plugins
+ /// - Creating a new instance of the allowed plugin
+ /// - Read the plugin metadata
+ ///
+ /// Loading a plugin does not mean to start the plugin, though. + ///
+ public static async Task LoadAll(CancellationToken cancellationToken = default) { + LOG.LogInformation("Start loading plugins."); + if (!Directory.Exists(PLUGINS_ROOT)) + { + LOG.LogInformation("No plugins found."); + return; + } + // + // The easiest way to load all plugins is to find all `plugin.lua` files and load them. + // By convention, each plugin is enforced to have a `plugin.lua` file. + // + var pluginMainFiles = Directory.EnumerateFiles(PLUGINS_ROOT, "plugin.lua", SearchOption.AllDirectories); + foreach (var pluginMainFile in pluginMainFiles) + { + if (cancellationToken.IsCancellationRequested) + break; + + LOG.LogInformation($"Try to load plugin: {pluginMainFile}"); + var code = await File.ReadAllTextAsync(pluginMainFile, Encoding.UTF8, cancellationToken); + var plugin = await Load(pluginMainFile, code, cancellationToken); + + switch (plugin) + { + case NoPlugin noPlugin when noPlugin.Issues.Any(): + LOG.LogError($"Was not able to load plugin: '{pluginMainFile}'. Reason: {noPlugin.Issues.First()}"); + continue; + + case NoPlugin: + LOG.LogError($"Was not able to load plugin: '{pluginMainFile}'. Reason: Unknown."); + continue; + + case { IsValid: false }: + LOG.LogError($"Was not able to load plugin '{pluginMainFile}', because the Lua code is not a valid AI Studio plugin. There are {plugin.Issues.Count()} issues to fix."); + continue; + + case { IsMaintained: false }: + LOG.LogWarning($"The plugin '{pluginMainFile}' is not maintained anymore. Please consider to disable it."); + break; + } + + LOG.LogInformation($"Successfully loaded plugin: '{pluginMainFile}' (Id='{plugin.Id}', Type='{plugin.Type}', Name='{plugin.Name}', Version='{plugin.Version}', Authors='{string.Join(", ", plugin.Authors)}')"); + AVAILABLE_PLUGINS.Add(new PluginMetadata(plugin), pluginMainFile); + } } public static async Task Load(string path, string code, CancellationToken cancellationToken = default) @@ -30,6 +103,10 @@ public static partial class PluginFactory { return new NoPlugin($"Was not able to parse the plugin: {e.Message}"); } + catch (LuaRuntimeException e) + { + return new NoPlugin($"Was not able to run the plugin: {e.Message}"); + } if (!state.Environment["TYPE"].TryRead(out var typeText)) return new NoPlugin("TYPE does not exist or is not a valid string.");