From b8262901a0869f8531db739241ac0e81721f89c4 Mon Sep 17 00:00:00 2001 From: StarInk Date: Sun, 9 Feb 2025 08:23:14 +0200 Subject: [PATCH 01/60] =?UTF-8?q?=F0=9F=92=BE=20Feat:=20Workflow=20UI=20Ba?= =?UTF-8?q?se?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Workflow/WorkflowInfo.cs | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 KitX Shared/KitX.Shared.CSharp/Workflow/WorkflowInfo.cs diff --git a/KitX Shared/KitX.Shared.CSharp/Workflow/WorkflowInfo.cs b/KitX Shared/KitX.Shared.CSharp/Workflow/WorkflowInfo.cs new file mode 100644 index 0000000..ac3ee1e --- /dev/null +++ b/KitX Shared/KitX.Shared.CSharp/Workflow/WorkflowInfo.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; + +namespace KitX.Shared.CSharp.Workflow; + +public class WorkflowInfo +{ + public string Name { get; set; } = string.Empty; + + public string Version { get; set; } = string.Empty; + + public Dictionary DisplayName { get; set; } = []; + + public string AuthorName { get; set; } = string.Empty; + + public string AuthorLink { get; set; } = string.Empty; + + public string PublisherName { get; set; } = string.Empty; + + public string PublisherLink { get; set; } = string.Empty; + + public Dictionary SimpleDescription { get; set; } = []; + + public Dictionary ComplexDescription { get; set; } = []; + + public Dictionary TotalDescriptionInMarkdown { get; set; } = []; + + public string IconInBase64 { get; set; } = string.Empty; + + public DateTime PublishDate { get; set; } + + public DateTime LastUpdateDate { get; set; } + + public bool IsMarketVersion { get; set; } + + public string OriginalFilePath { get; set; } = string.Empty; +} From 81b32a336db67616877370e56f1c5b166d8fb39d Mon Sep 17 00:00:00 2001 From: StarInk Date: Sun, 9 Feb 2025 23:51:43 +0200 Subject: [PATCH 02/60] =?UTF-8?q?=F0=9F=92=BE=20Feat(Kscript.CSharp):=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=8E=A5=E5=8F=A3=E5=92=8C=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E7=B1=BB=E4=BB=A5=E6=94=AF=E6=8C=81=E8=AE=BE=E5=A4=87=E5=92=8C?= =?UTF-8?q?=E6=8F=92=E4=BB=B6=E5=8A=9F=E8=83=BD-=E5=88=9D=E5=A7=8B?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Kscript.CSharp/Interfaces/IComposer.cs | 15 +++++ .../Kscript.CSharp/Interfaces/IDevice.cs | 15 +++++ .../Kscript.CSharp/Interfaces/IFunction.cs | 13 ++++ .../Kscript.CSharp/Interfaces/IPlugin.cs | 16 +++++ .../Kscript.CSharp/Kscript.CSharp.csproj | 24 +++++++ .../Kscript.CSharp/Services/Composer.cs | 67 +++++++++++++++++++ KitX Script/Kscript.CSharp/Services/Device.cs | 56 ++++++++++++++++ .../Kscript.CSharp/Services/Function.cs | 60 +++++++++++++++++ .../Services/NetworkConnector.cs | 67 +++++++++++++++++++ KitX Script/Kscript.CSharp/Services/Plugin.cs | 62 +++++++++++++++++ 10 files changed, 395 insertions(+) create mode 100644 KitX Script/Kscript.CSharp/Interfaces/IComposer.cs create mode 100644 KitX Script/Kscript.CSharp/Interfaces/IDevice.cs create mode 100644 KitX Script/Kscript.CSharp/Interfaces/IFunction.cs create mode 100644 KitX Script/Kscript.CSharp/Interfaces/IPlugin.cs create mode 100644 KitX Script/Kscript.CSharp/Kscript.CSharp.csproj create mode 100644 KitX Script/Kscript.CSharp/Services/Composer.cs create mode 100644 KitX Script/Kscript.CSharp/Services/Device.cs create mode 100644 KitX Script/Kscript.CSharp/Services/Function.cs create mode 100644 KitX Script/Kscript.CSharp/Services/NetworkConnector.cs create mode 100644 KitX Script/Kscript.CSharp/Services/Plugin.cs diff --git a/KitX Script/Kscript.CSharp/Interfaces/IComposer.cs b/KitX Script/Kscript.CSharp/Interfaces/IComposer.cs new file mode 100644 index 0000000..0216730 --- /dev/null +++ b/KitX Script/Kscript.CSharp/Interfaces/IComposer.cs @@ -0,0 +1,15 @@ +using KitX.Shared.CSharp.Device; + +namespace Kscript.CSharp.Interfaces +{ + public interface IComposer + { + Task RequestLocalDevice(); + Task RequestMainController(); + Task RequestRandomDesktopDevice(); + Task RequestRandomMobileDevice(); + Task RequestDeviceByFilter(Func filter); + Task RequestUserSelectedDevice(IEnumerable candidates); + Task> RequestDeviceList(); + } +} diff --git a/KitX Script/Kscript.CSharp/Interfaces/IDevice.cs b/KitX Script/Kscript.CSharp/Interfaces/IDevice.cs new file mode 100644 index 0000000..a430d6c --- /dev/null +++ b/KitX Script/Kscript.CSharp/Interfaces/IDevice.cs @@ -0,0 +1,15 @@ +using KitX.Shared.CSharp.Device; +using KitX.Shared.CSharp.Plugin; + +namespace Kscript.CSharp.Interfaces +{ + public interface IDevice + { + DeviceInfo Info { get; } + Task RequestPlugin(string idOrName); + Task> RequestPluginsByType(string type); + Task> GetPluginList(); + bool HasPlugin(string idOrName); + Task CreatePluginInstance(PluginInfo info); + } +} diff --git a/KitX Script/Kscript.CSharp/Interfaces/IFunction.cs b/KitX Script/Kscript.CSharp/Interfaces/IFunction.cs new file mode 100644 index 0000000..48026af --- /dev/null +++ b/KitX Script/Kscript.CSharp/Interfaces/IFunction.cs @@ -0,0 +1,13 @@ +using KitX.Shared.CSharp.Plugin; + +namespace Kscript.CSharp.Interfaces +{ + public interface IFunction + { + Function Info { get; } + PluginInfo AssociatedPlugin { get; } + Task Invoke(params object[] parameters); + bool ValidateParameters(object[] parameters); + Type GetReturnType(); + } +} diff --git a/KitX Script/Kscript.CSharp/Interfaces/IPlugin.cs b/KitX Script/Kscript.CSharp/Interfaces/IPlugin.cs new file mode 100644 index 0000000..d66fd94 --- /dev/null +++ b/KitX Script/Kscript.CSharp/Interfaces/IPlugin.cs @@ -0,0 +1,16 @@ +using KitX.Shared.CSharp.Device; +using KitX.Shared.CSharp.Plugin; + +namespace Kscript.CSharp.Interfaces +{ + public interface IPlugin + { + PluginInfo Info { get; } + DeviceInfo AssociatedDevice { get; } + Task RequestFunction(string idOrName); + Task> GetFunctionList(); + Task GetFunctionByType(string type); + Task ExecuteFunction(string functionName, params object[] parameters); + bool HasFunction(string idOrName); + } +} diff --git a/KitX Script/Kscript.CSharp/Kscript.CSharp.csproj b/KitX Script/Kscript.CSharp/Kscript.CSharp.csproj new file mode 100644 index 0000000..a2d8d7c --- /dev/null +++ b/KitX Script/Kscript.CSharp/Kscript.CSharp.csproj @@ -0,0 +1,24 @@ + + + + net8.0 + enable + enable + True + + + + $(Version) + $(Version) + 24.10.$([System.DateTime]::UtcNow.Date.Subtract($([System.DateTime]::Parse("2024-02-07"))).TotalDays).$([System.Math]::Floor($([System.DateTime]::UtcNow.TimeOfDay.TotalMinutes))) + + + + Kscript.CSharp + StarDustSeemsInk,Dynesshely + Crequency + KitX Script CSharp + AGPL-3.0-only + True + + diff --git a/KitX Script/Kscript.CSharp/Services/Composer.cs b/KitX Script/Kscript.CSharp/Services/Composer.cs new file mode 100644 index 0000000..d37475d --- /dev/null +++ b/KitX Script/Kscript.CSharp/Services/Composer.cs @@ -0,0 +1,67 @@ +using KitX.Shared.CSharp.Device; +using Kscript.CSharp.Interfaces; + +namespace Kscript.CSharp.Services +{ + public class Composer : IComposer + { + private readonly NetworkConnector _connector = NetworkConnector.Instance; + private IEnumerable _cachedDeviceList; + + public async Task RequestLocalDevice() + { + var deviceInfo = await _connector.GetLocalDeviceInfo(); + return new Device(deviceInfo); + } + + public async Task RequestMainController() + { + var devices = await RequestDeviceList(); + var mainController = devices.FirstOrDefault(x => x.IsMainController); + return mainController != null ? new Device(mainController) : null; + } + + public async Task RequestRandomDesktopDevice() + { + var devices = await RequestDeviceList(); + var desktopDevices = devices.Where(x => x.IsDesktopDevice).ToList(); + if (!desktopDevices.Any()) return null; + + var random = new Random(); + var randomDevice = desktopDevices[random.Next(desktopDevices.Count)]; + return new Device(randomDevice); + } + + public async Task RequestRandomMobileDevice() + { + var devices = await RequestDeviceList(); + var mobileDevices = devices.Where(x => x.IsMobileDevice).ToList(); + if (!mobileDevices.Any()) return null; + + var random = new Random(); + var randomDevice = mobileDevices[random.Next(mobileDevices.Count)]; + return new Device(randomDevice); + } + + public async Task RequestDeviceByFilter(Func filter) + { + var devices = await RequestDeviceList(); + var matchedDevice = devices.FirstOrDefault(filter); + return matchedDevice != null ? new Device(matchedDevice) : null; + } + + public async Task RequestUserSelectedDevice(IEnumerable candidates) + { + // 这里需要调用本地UI让用户选择设备 + // 简化实现,返回第一个设备 + var device = candidates.FirstOrDefault(); + return device != null ? new Device(device) : null; + } + + public async Task> RequestDeviceList() + { + _cachedDeviceList = await _connector.GetDeviceList(); + return _cachedDeviceList; + } + } +} diff --git a/KitX Script/Kscript.CSharp/Services/Device.cs b/KitX Script/Kscript.CSharp/Services/Device.cs new file mode 100644 index 0000000..1a7f7d6 --- /dev/null +++ b/KitX Script/Kscript.CSharp/Services/Device.cs @@ -0,0 +1,56 @@ +using KitX.Shared.CSharp.Device; +using KitX.Shared.CSharp.Plugin; +using Kscript.CSharp.Interfaces; + +namespace Kscript.CSharp.Services +{ + public class Device : IDevice + { + private readonly NetworkConnector _connector = NetworkConnector.Instance; + private Dictionary _pluginCache = new(); + + public DeviceInfo Info { get; } + + public Device(DeviceInfo info) + { + Info = info; + } + + public async Task RequestPlugin(string idOrName) + { + var plugins = await GetPluginList(); + var pluginInfo = plugins.FirstOrDefault(p => + p.Id.Equals(idOrName, StringComparison.OrdinalIgnoreCase) || + p.Name.Equals(idOrName, StringComparison.OrdinalIgnoreCase)); + + return pluginInfo != null ? await CreatePluginInstance(pluginInfo) : null; + } + + public async Task> RequestPluginsByType(string type) + { + var plugins = await GetPluginList(); + var matchedPlugins = plugins.Where(p => p.Type.Equals(type, StringComparison.OrdinalIgnoreCase)); + var tasks = matchedPlugins.Select(CreatePluginInstance); + return await Task.WhenAll(tasks); + } + + public async Task> GetPluginList() + { + var plugins = await _connector.GetPluginList(Info.Id); + _pluginCache = plugins.ToDictionary(p => p.Id); + return plugins; + } + + public bool HasPlugin(string idOrName) + { + return _pluginCache.Values.Any(p => + p.Id.Equals(idOrName, StringComparison.OrdinalIgnoreCase) || + p.Name.Equals(idOrName, StringComparison.OrdinalIgnoreCase)); + } + + public async Task CreatePluginInstance(PluginInfo info) + { + return new Plugin(info, Info, _connector); + } + } +} diff --git a/KitX Script/Kscript.CSharp/Services/Function.cs b/KitX Script/Kscript.CSharp/Services/Function.cs new file mode 100644 index 0000000..315f59c --- /dev/null +++ b/KitX Script/Kscript.CSharp/Services/Function.cs @@ -0,0 +1,60 @@ +using KitX.Shared.CSharp.Device; +using KitX.Shared.CSharp.Plugin; +using Kscript.CSharp.Interfaces; + +namespace Kscript.CSharp.Services +{ + public class Function : IFunction + { + private readonly NetworkConnector _connector; + private readonly DeviceInfo _deviceInfo; + + public Function Info { get; } + public PluginInfo AssociatedPlugin { get; } + + public Function(KitX.Shared.CSharp.Plugin.Function info, PluginInfo pluginInfo, + DeviceInfo deviceInfo, NetworkConnector connector) + { + Info = info; + AssociatedPlugin = pluginInfo; + _deviceInfo = deviceInfo; + _connector = connector; + } + + public async Task Invoke(params object[] parameters) + { + if (!ValidateParameters(parameters)) + { + throw new ArgumentException("Invalid parameters"); + } + + return await _connector.ExecuteFunction( + _deviceInfo.Id, + AssociatedPlugin.Id, + Info.Name, + parameters + ); + } + + public bool ValidateParameters(object[] parameters) + { + if (parameters.Length != Info.Parameters.Length) + return false; + + for (int i = 0; i < parameters.Length; i++) + { + if (!Info.Parameters[i].ParameterType.IsInstanceOfType(parameters[i])) + { + return false; + } + } + + return true; + } + + public Type GetReturnType() + { + return Type.GetType(Info.ReturnType) ?? typeof(object); + } + } +} diff --git a/KitX Script/Kscript.CSharp/Services/NetworkConnector.cs b/KitX Script/Kscript.CSharp/Services/NetworkConnector.cs new file mode 100644 index 0000000..2c43911 --- /dev/null +++ b/KitX Script/Kscript.CSharp/Services/NetworkConnector.cs @@ -0,0 +1,67 @@ +using KitX.Shared.CSharp.WebCommand; +using KitX.Shared.CSharp.Device; +using KitX.Shared.CSharp.Plugin; + +namespace Kscript.CSharp.Services +{ + internal class NetworkConnector + { + private static readonly NetworkConnector _instance = new(); + private readonly Dictionary _deviceCache = new(); + private readonly Connector _connector = new(); + + public static NetworkConnector Instance => _instance; + + private NetworkConnector() + { + // 初始化连接器 + _connector.Connect(); + } + + public async Task GetLocalDeviceInfo() + { + var request = new RequestBuilder() + .SetCommandType(CommandType.GetLocalDeviceInfo) + .Build(); + var response = await _connector.SendRequest(request); + return response.As(); + } + + public async Task> GetDeviceList() + { + var request = new RequestBuilder() + .SetCommandType(CommandType.GetDeviceList) + .Build(); + var response = await _connector.SendRequest(request); + var devices = response.As>(); + foreach (var device in devices) + { + _deviceCache[device.Id] = device; + } + return devices; + } + + public async Task> GetPluginList(string deviceId) + { + var request = new RequestBuilder() + .SetCommandType(CommandType.GetPluginList) + .SetTarget(deviceId) + .Build(); + var response = await _connector.SendRequest(request); + return response.As>(); + } + + public async Task ExecuteFunction(string deviceId, string pluginId, string functionName, object[] parameters) + { + var request = new RequestBuilder() + .SetCommandType(CommandType.ExecuteFunction) + .SetTarget(deviceId) + .AddParameter("pluginId", pluginId) + .AddParameter("functionName", functionName) + .AddParameter("parameters", parameters) + .Build(); + var response = await _connector.SendRequest(request); + return response.Result; + } + } +} diff --git a/KitX Script/Kscript.CSharp/Services/Plugin.cs b/KitX Script/Kscript.CSharp/Services/Plugin.cs new file mode 100644 index 0000000..6ca7896 --- /dev/null +++ b/KitX Script/Kscript.CSharp/Services/Plugin.cs @@ -0,0 +1,62 @@ +using KitX.Shared.CSharp.Device; +using KitX.Shared.CSharp.Plugin; +using Kscript.CSharp.Interfaces; + +namespace Kscript.CSharp.Services +{ + public class Plugin : IPlugin + { + private readonly NetworkConnector _connector; + private readonly Dictionary _functionCache = new(); + + public PluginInfo Info { get; } + public DeviceInfo AssociatedDevice { get; } + + public Plugin(PluginInfo info, DeviceInfo deviceInfo, NetworkConnector connector) + { + Info = info; + AssociatedDevice = deviceInfo; + _connector = connector; + } + + public async Task RequestFunction(string idOrName) + { + var functions = await GetFunctionList(); + var function = functions.FirstOrDefault(f => + f.Info.Id.Equals(idOrName, StringComparison.OrdinalIgnoreCase) || + f.Info.Name.Equals(idOrName, StringComparison.OrdinalIgnoreCase)); + return function; + } + + public async Task> GetFunctionList() + { + var functions = Info.Functions.Select(f => + new Function(f, Info, AssociatedDevice, _connector) as IFunction); + return await Task.FromResult(functions); + } + + public async Task GetFunctionByType(string type) + { + var functions = await GetFunctionList(); + return functions.FirstOrDefault(f => + f.Info.ReturnType.Equals(type, StringComparison.OrdinalIgnoreCase)); + } + + public async Task ExecuteFunction(string functionName, params object[] parameters) + { + return await _connector.ExecuteFunction( + AssociatedDevice.Id, + Info.Id, + functionName, + parameters + ); + } + + public bool HasFunction(string idOrName) + { + return Info.Functions.Any(f => + f.Id.Equals(idOrName, StringComparison.OrdinalIgnoreCase) || + f.Name.Equals(idOrName, StringComparison.OrdinalIgnoreCase)); + } + } +} From c5e413e8829dcac9421b51fce4c2792584613cd5 Mon Sep 17 00:00:00 2001 From: StarInk Date: Mon, 10 Feb 2025 01:41:14 +0200 Subject: [PATCH 03/60] =?UTF-8?q?=F0=9F=92=BE=20=F0=9F=90=9BFeat,=20Bug(Ks?= =?UTF-8?q?cript.CSharp):=20=E6=9B=B4=E6=96=B0=E6=8E=A5=E5=8F=A3=E4=BB=A5?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E6=96=B0=E5=8A=9F=E8=83=BD=EF=BC=8C=E7=A7=BB?= =?UTF-8?q?=E9=99=A4=E4=B8=8D=E5=BF=85=E8=A6=81=E7=9A=84=E7=BD=91=E7=BB=9C?= =?UTF-8?q?=E8=BF=9E=E6=8E=A5=E5=99=A8=EF=BC=8C=E6=9C=AA=E6=89=BE=E5=88=B0?= =?UTF-8?q?=E5=90=88=E9=80=82=E7=9A=84=E8=8E=B7=E5=8F=96=E7=BD=91=E7=BB=9C?= =?UTF-8?q?=E5=B9=BF=E6=92=AD=E4=BF=A1=E6=81=AF=E7=9A=84=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Kscript.CSharp/Interfaces/IComposer.cs | 12 ++-- .../Kscript.CSharp/Interfaces/IDevice.cs | 3 +- .../Kscript.CSharp/Interfaces/IFunction.cs | 5 +- .../Kscript.CSharp/Interfaces/IPlugin.cs | 4 +- .../Kscript.CSharp/Kscript.CSharp.csproj | 4 ++ .../Kscript.CSharp/Services/Composer.cs | 52 +++++++------- KitX Script/Kscript.CSharp/Services/Device.cs | 24 ++----- .../Kscript.CSharp/Services/Function.cs | 54 ++++++--------- .../Services/NetworkConnector.cs | 67 ------------------- KitX Script/Kscript.CSharp/Services/Plugin.cs | 37 ++++++---- 10 files changed, 92 insertions(+), 170 deletions(-) delete mode 100644 KitX Script/Kscript.CSharp/Services/NetworkConnector.cs diff --git a/KitX Script/Kscript.CSharp/Interfaces/IComposer.cs b/KitX Script/Kscript.CSharp/Interfaces/IComposer.cs index 0216730..c7c11ef 100644 --- a/KitX Script/Kscript.CSharp/Interfaces/IComposer.cs +++ b/KitX Script/Kscript.CSharp/Interfaces/IComposer.cs @@ -1,15 +1,15 @@ -using KitX.Shared.CSharp.Device; +using KitX.Shared.CSharp.Device; namespace Kscript.CSharp.Interfaces { public interface IComposer { Task RequestLocalDevice(); - Task RequestMainController(); - Task RequestRandomDesktopDevice(); - Task RequestRandomMobileDevice(); - Task RequestDeviceByFilter(Func filter); - Task RequestUserSelectedDevice(IEnumerable candidates); + Task RequestMainController(); + Task RequestRandomDesktopDevice(); + Task RequestRandomMobileDevice(); + Task RequestDeviceByFilter(Func filter); + Task RequestUserSelectedDevice(IEnumerable candidates); Task> RequestDeviceList(); } } diff --git a/KitX Script/Kscript.CSharp/Interfaces/IDevice.cs b/KitX Script/Kscript.CSharp/Interfaces/IDevice.cs index a430d6c..029b635 100644 --- a/KitX Script/Kscript.CSharp/Interfaces/IDevice.cs +++ b/KitX Script/Kscript.CSharp/Interfaces/IDevice.cs @@ -1,4 +1,4 @@ -using KitX.Shared.CSharp.Device; +using KitX.Shared.CSharp.Device; using KitX.Shared.CSharp.Plugin; namespace Kscript.CSharp.Interfaces @@ -7,7 +7,6 @@ public interface IDevice { DeviceInfo Info { get; } Task RequestPlugin(string idOrName); - Task> RequestPluginsByType(string type); Task> GetPluginList(); bool HasPlugin(string idOrName); Task CreatePluginInstance(PluginInfo info); diff --git a/KitX Script/Kscript.CSharp/Interfaces/IFunction.cs b/KitX Script/Kscript.CSharp/Interfaces/IFunction.cs index 48026af..3f71963 100644 --- a/KitX Script/Kscript.CSharp/Interfaces/IFunction.cs +++ b/KitX Script/Kscript.CSharp/Interfaces/IFunction.cs @@ -1,4 +1,4 @@ -using KitX.Shared.CSharp.Plugin; +using KitX.Shared.CSharp.Plugin; namespace Kscript.CSharp.Interfaces { @@ -6,8 +6,7 @@ public interface IFunction { Function Info { get; } PluginInfo AssociatedPlugin { get; } - Task Invoke(params object[] parameters); - bool ValidateParameters(object[] parameters); + Task Invoke(params string[] parameters); Type GetReturnType(); } } diff --git a/KitX Script/Kscript.CSharp/Interfaces/IPlugin.cs b/KitX Script/Kscript.CSharp/Interfaces/IPlugin.cs index d66fd94..d6d7b37 100644 --- a/KitX Script/Kscript.CSharp/Interfaces/IPlugin.cs +++ b/KitX Script/Kscript.CSharp/Interfaces/IPlugin.cs @@ -1,4 +1,4 @@ -using KitX.Shared.CSharp.Device; +using KitX.Shared.CSharp.Device; using KitX.Shared.CSharp.Plugin; namespace Kscript.CSharp.Interfaces @@ -9,7 +9,7 @@ public interface IPlugin DeviceInfo AssociatedDevice { get; } Task RequestFunction(string idOrName); Task> GetFunctionList(); - Task GetFunctionByType(string type); + Task GetFunctionByType(string type); Task ExecuteFunction(string functionName, params object[] parameters); bool HasFunction(string idOrName); } diff --git a/KitX Script/Kscript.CSharp/Kscript.CSharp.csproj b/KitX Script/Kscript.CSharp/Kscript.CSharp.csproj index a2d8d7c..c2dce81 100644 --- a/KitX Script/Kscript.CSharp/Kscript.CSharp.csproj +++ b/KitX Script/Kscript.CSharp/Kscript.CSharp.csproj @@ -21,4 +21,8 @@ AGPL-3.0-only True + + + + diff --git a/KitX Script/Kscript.CSharp/Services/Composer.cs b/KitX Script/Kscript.CSharp/Services/Composer.cs index d37475d..77f59bb 100644 --- a/KitX Script/Kscript.CSharp/Services/Composer.cs +++ b/KitX Script/Kscript.CSharp/Services/Composer.cs @@ -1,41 +1,41 @@ -using KitX.Shared.CSharp.Device; +using KitX.Shared.CSharp.Device; +using KitX.Shared.CSharp.WebCommand; using Kscript.CSharp.Interfaces; namespace Kscript.CSharp.Services { public class Composer : IComposer { - private readonly NetworkConnector _connector = NetworkConnector.Instance; - private IEnumerable _cachedDeviceList; + private readonly Connector _connector = Connector.Instance; + private IEnumerable? _cachedDeviceList; - public async Task RequestLocalDevice() - { - var deviceInfo = await _connector.GetLocalDeviceInfo(); - return new Device(deviceInfo); - } + public async Task RequestLocalDevice() => throw new NotImplementedException(); - public async Task RequestMainController() + public async Task RequestMainController() { var devices = await RequestDeviceList(); - var mainController = devices.FirstOrDefault(x => x.IsMainController); + var mainController = devices.FirstOrDefault(x => x.IsMainDevice); return mainController != null ? new Device(mainController) : null; } - public async Task RequestRandomDesktopDevice() + public async Task RequestRandomDesktopDevice() { var devices = await RequestDeviceList(); - var desktopDevices = devices.Where(x => x.IsDesktopDevice).ToList(); + var desktopDevices = devices.Where(x => x.DeviceOSType == OperatingSystems.Windows + || x.DeviceOSType == OperatingSystems.MacOS + || x.DeviceOSType == OperatingSystems.Linux).ToList(); if (!desktopDevices.Any()) return null; - + var random = new Random(); var randomDevice = desktopDevices[random.Next(desktopDevices.Count)]; return new Device(randomDevice); } - public async Task RequestRandomMobileDevice() + public async Task RequestRandomMobileDevice() { var devices = await RequestDeviceList(); - var mobileDevices = devices.Where(x => x.IsMobileDevice).ToList(); + var mobileDevices = devices.Where(x => x.DeviceOSType == OperatingSystems.Android + || x.DeviceOSType == OperatingSystems.IOS).ToList(); if (!mobileDevices.Any()) return null; var random = new Random(); @@ -43,25 +43,29 @@ public async Task RequestRandomMobileDevice() return new Device(randomDevice); } - public async Task RequestDeviceByFilter(Func filter) + public async Task RequestDeviceByFilter(Func filter) { var devices = await RequestDeviceList(); var matchedDevice = devices.FirstOrDefault(filter); return matchedDevice != null ? new Device(matchedDevice) : null; } - public async Task RequestUserSelectedDevice(IEnumerable candidates) + // todo: implement this method + public Task RequestUserSelectedDevice(IEnumerable candidates) => throw new NotImplementedException(); + + public async Task> RequestDeviceList() => throw new NotImplementedException(); + + private async Task HandleResponse(Request response) { - // 这里需要调用本地UI让用户选择设备 - // 简化实现,返回第一个设备 - var device = candidates.FirstOrDefault(); - return device != null ? new Device(device) : null; + response.Match( + response.GetContent(content => content), // 这里需要提供实际的解密函数 + matchCommand: ProcessCommandResponse + ); } - public async Task> RequestDeviceList() + private void ProcessCommandResponse(string content) { - _cachedDeviceList = await _connector.GetDeviceList(); - return _cachedDeviceList; + // 处理命令响应 } } } diff --git a/KitX Script/Kscript.CSharp/Services/Device.cs b/KitX Script/Kscript.CSharp/Services/Device.cs index 1a7f7d6..d4e41f3 100644 --- a/KitX Script/Kscript.CSharp/Services/Device.cs +++ b/KitX Script/Kscript.CSharp/Services/Device.cs @@ -1,12 +1,13 @@ -using KitX.Shared.CSharp.Device; +using KitX.Shared.CSharp.Device; using KitX.Shared.CSharp.Plugin; +using KitX.Shared.CSharp.WebCommand; using Kscript.CSharp.Interfaces; namespace Kscript.CSharp.Services { public class Device : IDevice { - private readonly NetworkConnector _connector = NetworkConnector.Instance; + private readonly Connector _connector = Connector.Instance; private Dictionary _pluginCache = new(); public DeviceInfo Info { get; } @@ -20,37 +21,22 @@ public async Task RequestPlugin(string idOrName) { var plugins = await GetPluginList(); var pluginInfo = plugins.FirstOrDefault(p => - p.Id.Equals(idOrName, StringComparison.OrdinalIgnoreCase) || p.Name.Equals(idOrName, StringComparison.OrdinalIgnoreCase)); return pluginInfo != null ? await CreatePluginInstance(pluginInfo) : null; } - public async Task> RequestPluginsByType(string type) - { - var plugins = await GetPluginList(); - var matchedPlugins = plugins.Where(p => p.Type.Equals(type, StringComparison.OrdinalIgnoreCase)); - var tasks = matchedPlugins.Select(CreatePluginInstance); - return await Task.WhenAll(tasks); - } - - public async Task> GetPluginList() - { - var plugins = await _connector.GetPluginList(Info.Id); - _pluginCache = plugins.ToDictionary(p => p.Id); - return plugins; - } + public async Task> GetPluginList() => throw new NotImplementedException(); public bool HasPlugin(string idOrName) { return _pluginCache.Values.Any(p => - p.Id.Equals(idOrName, StringComparison.OrdinalIgnoreCase) || p.Name.Equals(idOrName, StringComparison.OrdinalIgnoreCase)); } public async Task CreatePluginInstance(PluginInfo info) { - return new Plugin(info, Info, _connector); + return new Plugin(info, Info); } } } diff --git a/KitX Script/Kscript.CSharp/Services/Function.cs b/KitX Script/Kscript.CSharp/Services/Function.cs index 315f59c..db23572 100644 --- a/KitX Script/Kscript.CSharp/Services/Function.cs +++ b/KitX Script/Kscript.CSharp/Services/Function.cs @@ -1,60 +1,48 @@ -using KitX.Shared.CSharp.Device; +using KitX.Shared.CSharp.Device; using KitX.Shared.CSharp.Plugin; +using KitX.Shared.CSharp.WebCommand; using Kscript.CSharp.Interfaces; namespace Kscript.CSharp.Services { public class Function : IFunction { - private readonly NetworkConnector _connector; + private readonly Connector _connector = Connector.Instance; private readonly DeviceInfo _deviceInfo; - public Function Info { get; } + public KitX.Shared.CSharp.Plugin.Function Info { get; } public PluginInfo AssociatedPlugin { get; } - public Function(KitX.Shared.CSharp.Plugin.Function info, PluginInfo pluginInfo, - DeviceInfo deviceInfo, NetworkConnector connector) + public Function(KitX.Shared.CSharp.Plugin.Function info, PluginInfo pluginInfo, DeviceInfo deviceInfo) { Info = info; AssociatedPlugin = pluginInfo; _deviceInfo = deviceInfo; - _connector = connector; } - public async Task Invoke(params object[] parameters) + public async Task Invoke(params string[] parameters) { - if (!ValidateParameters(parameters)) - { - throw new ArgumentException("Invalid parameters"); - } - - return await _connector.ExecuteFunction( - _deviceInfo.Id, - AssociatedPlugin.Id, - Info.Name, - parameters - ); - } - - public bool ValidateParameters(object[] parameters) - { - if (parameters.Length != Info.Parameters.Length) - return false; - - for (int i = 0; i < parameters.Length; i++) - { - if (!Info.Parameters[i].ParameterType.IsInstanceOfType(parameters[i])) + _connector.Request() + .UpdateCommand(cmd => + { + cmd.FunctionName = Info.Name; + cmd.FunctionArgs = parameters.Select(p => new Parameter { Value = p }).ToList(); + return cmd; + }) + .UpdateRequest(req => { - return false; - } - } + req.Target = _deviceInfo.Device; + return req; + }) + .Send(); - return true; + // 等待并处理响应 + return null; // 需要处理实际响应 } public Type GetReturnType() { - return Type.GetType(Info.ReturnType) ?? typeof(object); + return Type.GetType(Info.ReturnValueType) ?? typeof(object); } } } diff --git a/KitX Script/Kscript.CSharp/Services/NetworkConnector.cs b/KitX Script/Kscript.CSharp/Services/NetworkConnector.cs deleted file mode 100644 index 2c43911..0000000 --- a/KitX Script/Kscript.CSharp/Services/NetworkConnector.cs +++ /dev/null @@ -1,67 +0,0 @@ -using KitX.Shared.CSharp.WebCommand; -using KitX.Shared.CSharp.Device; -using KitX.Shared.CSharp.Plugin; - -namespace Kscript.CSharp.Services -{ - internal class NetworkConnector - { - private static readonly NetworkConnector _instance = new(); - private readonly Dictionary _deviceCache = new(); - private readonly Connector _connector = new(); - - public static NetworkConnector Instance => _instance; - - private NetworkConnector() - { - // 初始化连接器 - _connector.Connect(); - } - - public async Task GetLocalDeviceInfo() - { - var request = new RequestBuilder() - .SetCommandType(CommandType.GetLocalDeviceInfo) - .Build(); - var response = await _connector.SendRequest(request); - return response.As(); - } - - public async Task> GetDeviceList() - { - var request = new RequestBuilder() - .SetCommandType(CommandType.GetDeviceList) - .Build(); - var response = await _connector.SendRequest(request); - var devices = response.As>(); - foreach (var device in devices) - { - _deviceCache[device.Id] = device; - } - return devices; - } - - public async Task> GetPluginList(string deviceId) - { - var request = new RequestBuilder() - .SetCommandType(CommandType.GetPluginList) - .SetTarget(deviceId) - .Build(); - var response = await _connector.SendRequest(request); - return response.As>(); - } - - public async Task ExecuteFunction(string deviceId, string pluginId, string functionName, object[] parameters) - { - var request = new RequestBuilder() - .SetCommandType(CommandType.ExecuteFunction) - .SetTarget(deviceId) - .AddParameter("pluginId", pluginId) - .AddParameter("functionName", functionName) - .AddParameter("parameters", parameters) - .Build(); - var response = await _connector.SendRequest(request); - return response.Result; - } - } -} diff --git a/KitX Script/Kscript.CSharp/Services/Plugin.cs b/KitX Script/Kscript.CSharp/Services/Plugin.cs index 6ca7896..d105fed 100644 --- a/KitX Script/Kscript.CSharp/Services/Plugin.cs +++ b/KitX Script/Kscript.CSharp/Services/Plugin.cs @@ -1,29 +1,28 @@ -using KitX.Shared.CSharp.Device; +using KitX.Shared.CSharp.Device; using KitX.Shared.CSharp.Plugin; +using KitX.Shared.CSharp.WebCommand; using Kscript.CSharp.Interfaces; namespace Kscript.CSharp.Services { public class Plugin : IPlugin { - private readonly NetworkConnector _connector; + private readonly Connector _connector = Connector.Instance; private readonly Dictionary _functionCache = new(); public PluginInfo Info { get; } public DeviceInfo AssociatedDevice { get; } - public Plugin(PluginInfo info, DeviceInfo deviceInfo, NetworkConnector connector) + public Plugin(PluginInfo info, DeviceInfo deviceInfo) { Info = info; AssociatedDevice = deviceInfo; - _connector = connector; } public async Task RequestFunction(string idOrName) { var functions = await GetFunctionList(); var function = functions.FirstOrDefault(f => - f.Info.Id.Equals(idOrName, StringComparison.OrdinalIgnoreCase) || f.Info.Name.Equals(idOrName, StringComparison.OrdinalIgnoreCase)); return function; } @@ -35,27 +34,37 @@ public async Task> GetFunctionList() return await Task.FromResult(functions); } - public async Task GetFunctionByType(string type) + public async Task GetFunctionByType(string type) { var functions = await GetFunctionList(); return functions.FirstOrDefault(f => - f.Info.ReturnType.Equals(type, StringComparison.OrdinalIgnoreCase)); + f.Info.ReturnValueType.Equals(type, StringComparison.OrdinalIgnoreCase)); } public async Task ExecuteFunction(string functionName, params object[] parameters) { - return await _connector.ExecuteFunction( - AssociatedDevice.Id, - Info.Id, - functionName, - parameters - ); + _connector.Request() + .UpdateCommand(cmd => + { + cmd.PluginConnectionId = Info.Id; + cmd.FunctionName = functionName; + cmd.FunctionArgs = parameters.Select(p => new Parameter { Value = p }).ToList(); + return cmd; + }) + .UpdateRequest(req => + { + req.Target = new DeviceLocator { Id = AssociatedDevice.Id }; + return req; + }) + .Send(); + + // 处理响应 + return null; // 需要处理实际响应 } public bool HasFunction(string idOrName) { return Info.Functions.Any(f => - f.Id.Equals(idOrName, StringComparison.OrdinalIgnoreCase) || f.Name.Equals(idOrName, StringComparison.OrdinalIgnoreCase)); } } From 9ba9d8ee722f5be52f4ef63db6e5112946582a55 Mon Sep 17 00:00:00 2001 From: StarInk Date: Mon, 10 Feb 2025 02:21:48 +0200 Subject: [PATCH 04/60] =?UTF-8?q?=F0=9F=94=A7=20Fix(Kscript.CSharp):=20?= =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=8E=A5=E5=8F=A3=E4=BB=A5=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E5=AD=97=E7=AC=A6=E4=B8=B2=E6=95=B0=E7=BB=84=E4=BD=9C=E4=B8=BA?= =?UTF-8?q?=E5=8F=82=E6=95=B0=EF=BC=8C=E9=87=8D=E5=91=BD=E5=90=8D=E6=96=B9?= =?UTF-8?q?=E6=B3=95=E5=8F=82=E6=95=B0=E4=BB=A5=E6=8F=90=E9=AB=98=E5=8F=AF?= =?UTF-8?q?=E8=AF=BB=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- KitX Script/Kscript.CSharp/Interfaces/IPlugin.cs | 2 +- KitX Script/Kscript.CSharp/Services/Device.cs | 8 ++++---- KitX Script/Kscript.CSharp/Services/Plugin.cs | 16 +++++++--------- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/KitX Script/Kscript.CSharp/Interfaces/IPlugin.cs b/KitX Script/Kscript.CSharp/Interfaces/IPlugin.cs index d6d7b37..4ab8b6e 100644 --- a/KitX Script/Kscript.CSharp/Interfaces/IPlugin.cs +++ b/KitX Script/Kscript.CSharp/Interfaces/IPlugin.cs @@ -10,7 +10,7 @@ public interface IPlugin Task RequestFunction(string idOrName); Task> GetFunctionList(); Task GetFunctionByType(string type); - Task ExecuteFunction(string functionName, params object[] parameters); + Task ExecuteFunction(string functionName, params string[] parameters); bool HasFunction(string idOrName); } } diff --git a/KitX Script/Kscript.CSharp/Services/Device.cs b/KitX Script/Kscript.CSharp/Services/Device.cs index d4e41f3..888ece1 100644 --- a/KitX Script/Kscript.CSharp/Services/Device.cs +++ b/KitX Script/Kscript.CSharp/Services/Device.cs @@ -17,21 +17,21 @@ public Device(DeviceInfo info) Info = info; } - public async Task RequestPlugin(string idOrName) + public async Task RequestPlugin(string Name) { var plugins = await GetPluginList(); var pluginInfo = plugins.FirstOrDefault(p => - p.Name.Equals(idOrName, StringComparison.OrdinalIgnoreCase)); + p.Name.Equals(Name, StringComparison.OrdinalIgnoreCase)); return pluginInfo != null ? await CreatePluginInstance(pluginInfo) : null; } public async Task> GetPluginList() => throw new NotImplementedException(); - public bool HasPlugin(string idOrName) + public bool HasPlugin(string Name) { return _pluginCache.Values.Any(p => - p.Name.Equals(idOrName, StringComparison.OrdinalIgnoreCase)); + p.Name.Equals(Name, StringComparison.OrdinalIgnoreCase)); } public async Task CreatePluginInstance(PluginInfo info) diff --git a/KitX Script/Kscript.CSharp/Services/Plugin.cs b/KitX Script/Kscript.CSharp/Services/Plugin.cs index d105fed..2e3ae92 100644 --- a/KitX Script/Kscript.CSharp/Services/Plugin.cs +++ b/KitX Script/Kscript.CSharp/Services/Plugin.cs @@ -8,7 +8,6 @@ namespace Kscript.CSharp.Services public class Plugin : IPlugin { private readonly Connector _connector = Connector.Instance; - private readonly Dictionary _functionCache = new(); public PluginInfo Info { get; } public DeviceInfo AssociatedDevice { get; } @@ -19,18 +18,18 @@ public Plugin(PluginInfo info, DeviceInfo deviceInfo) AssociatedDevice = deviceInfo; } - public async Task RequestFunction(string idOrName) + public async Task RequestFunction(string Name) { var functions = await GetFunctionList(); var function = functions.FirstOrDefault(f => - f.Info.Name.Equals(idOrName, StringComparison.OrdinalIgnoreCase)); + f.Info.Name.Equals(Name, StringComparison.OrdinalIgnoreCase)); return function; } public async Task> GetFunctionList() { var functions = Info.Functions.Select(f => - new Function(f, Info, AssociatedDevice, _connector) as IFunction); + new Function(f, Info, AssociatedDevice) as IFunction); return await Task.FromResult(functions); } @@ -41,19 +40,18 @@ public async Task> GetFunctionList() f.Info.ReturnValueType.Equals(type, StringComparison.OrdinalIgnoreCase)); } - public async Task ExecuteFunction(string functionName, params object[] parameters) + public async Task ExecuteFunction(string functionName, params string[] parameters) { _connector.Request() .UpdateCommand(cmd => { - cmd.PluginConnectionId = Info.Id; cmd.FunctionName = functionName; cmd.FunctionArgs = parameters.Select(p => new Parameter { Value = p }).ToList(); return cmd; }) .UpdateRequest(req => { - req.Target = new DeviceLocator { Id = AssociatedDevice.Id }; + req.Target = AssociatedDevice.Device; return req; }) .Send(); @@ -62,10 +60,10 @@ public async Task ExecuteFunction(string functionName, params object[] p return null; // 需要处理实际响应 } - public bool HasFunction(string idOrName) + public bool HasFunction(string Name) { return Info.Functions.Any(f => - f.Name.Equals(idOrName, StringComparison.OrdinalIgnoreCase)); + f.Name.Equals(Name, StringComparison.OrdinalIgnoreCase)); } } } From 93b834ddb0b37bdbb5bd5b456ab6eca3e5ac47ff Mon Sep 17 00:00:00 2001 From: StarInk Date: Wed, 12 Feb 2025 01:29:03 +0200 Subject: [PATCH 05/60] =?UTF-8?q?=F0=9F=92=BE=20Feat(Kscript.CSharp):=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=AF=B9=20System.Text.Json=20=E7=9A=84?= =?UTF-8?q?=E6=94=AF=E6=8C=81=EF=BC=8C=E6=9B=B4=E6=96=B0=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E4=BB=A5=E8=BF=94=E5=9B=9E=E5=8F=AF=E7=A9=BA=E7=B1=BB=E5=9E=8B?= =?UTF-8?q?=EF=BC=8C=E4=BC=98=E5=8C=96=E6=8F=92=E4=BB=B6=E5=88=97=E8=A1=A8?= =?UTF-8?q?=E8=8E=B7=E5=8F=96=E9=80=BB=E8=BE=91-=E4=B8=8D=E5=8F=AF?= =?UTF-8?q?=E7=94=A8=EF=BC=8C=E5=BE=85=E8=AE=A8=E8=AE=BA-01?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Kscript.CSharp/Interfaces/IComposer.cs | 2 +- .../Kscript.CSharp/Interfaces/IPlugin.cs | 2 +- .../Kscript.CSharp/Services/Composer.cs | 153 ++++++++++++++++-- KitX Script/Kscript.CSharp/Services/Device.cs | 89 +++++++++- .../Kscript.CSharp/Services/Function.cs | 113 ++++++++++++- KitX Script/Kscript.CSharp/Services/Plugin.cs | 114 ++++++++++++- .../KitX.Shared.CSharp.csproj | 4 + 7 files changed, 457 insertions(+), 20 deletions(-) diff --git a/KitX Script/Kscript.CSharp/Interfaces/IComposer.cs b/KitX Script/Kscript.CSharp/Interfaces/IComposer.cs index c7c11ef..d051b2e 100644 --- a/KitX Script/Kscript.CSharp/Interfaces/IComposer.cs +++ b/KitX Script/Kscript.CSharp/Interfaces/IComposer.cs @@ -4,7 +4,7 @@ namespace Kscript.CSharp.Interfaces { public interface IComposer { - Task RequestLocalDevice(); + Task RequestLocalDevice(); Task RequestMainController(); Task RequestRandomDesktopDevice(); Task RequestRandomMobileDevice(); diff --git a/KitX Script/Kscript.CSharp/Interfaces/IPlugin.cs b/KitX Script/Kscript.CSharp/Interfaces/IPlugin.cs index 4ab8b6e..2e290d0 100644 --- a/KitX Script/Kscript.CSharp/Interfaces/IPlugin.cs +++ b/KitX Script/Kscript.CSharp/Interfaces/IPlugin.cs @@ -7,7 +7,7 @@ public interface IPlugin { PluginInfo Info { get; } DeviceInfo AssociatedDevice { get; } - Task RequestFunction(string idOrName); + Task RequestFunction(string idOrName); Task> GetFunctionList(); Task GetFunctionByType(string type); Task ExecuteFunction(string functionName, params string[] parameters); diff --git a/KitX Script/Kscript.CSharp/Services/Composer.cs b/KitX Script/Kscript.CSharp/Services/Composer.cs index 77f59bb..b911ac8 100644 --- a/KitX Script/Kscript.CSharp/Services/Composer.cs +++ b/KitX Script/Kscript.CSharp/Services/Composer.cs @@ -1,6 +1,8 @@ using KitX.Shared.CSharp.Device; +using KitX.Shared.CSharp.Plugin; using KitX.Shared.CSharp.WebCommand; using Kscript.CSharp.Interfaces; +using System.Text.Json; namespace Kscript.CSharp.Services { @@ -9,7 +11,16 @@ public class Composer : IComposer private readonly Connector _connector = Connector.Instance; private IEnumerable? _cachedDeviceList; - public async Task RequestLocalDevice() => throw new NotImplementedException(); + public async Task RequestLocalDevice() + { + var devices = await RequestDeviceList(); + var localDevice = devices.FirstOrDefault(x => + (x.Device.IPv4?.Equals("127.0.0.1") ?? false) || // Check localhost + (x.Device.IPv4?.Equals("::1") ?? false)); // Check IPv6 localhost + // todo: fixme: 这个检查方案会有问题。改进建议:检查IP地址是否与本地设备的IP地址有交集。因为广播出来的地址可能是本机地址的外网地址 + + return localDevice != null ? new Device(localDevice) : null; + } public async Task RequestMainController() { @@ -24,7 +35,7 @@ public class Composer : IComposer var desktopDevices = devices.Where(x => x.DeviceOSType == OperatingSystems.Windows || x.DeviceOSType == OperatingSystems.MacOS || x.DeviceOSType == OperatingSystems.Linux).ToList(); - if (!desktopDevices.Any()) return null; + if (desktopDevices.Count == 0) return null; var random = new Random(); var randomDevice = desktopDevices[random.Next(desktopDevices.Count)]; @@ -36,7 +47,7 @@ public class Composer : IComposer var devices = await RequestDeviceList(); var mobileDevices = devices.Where(x => x.DeviceOSType == OperatingSystems.Android || x.DeviceOSType == OperatingSystems.IOS).ToList(); - if (!mobileDevices.Any()) return null; + if (mobileDevices.Count == 0) return null; var random = new Random(); var randomDevice = mobileDevices[random.Next(mobileDevices.Count)]; @@ -50,22 +61,144 @@ public class Composer : IComposer return matchedDevice != null ? new Device(matchedDevice) : null; } - // todo: implement this method - public Task RequestUserSelectedDevice(IEnumerable candidates) => throw new NotImplementedException(); + public async Task RequestUserSelectedDevice(IEnumerable candidates) + { + var tcs = new TaskCompletionSource(); + + void OnResponse(Request response) + { + try + { + response.Match( + response.GetContent(content => // 这里返回的是用户选择的设备的 MAC 地址 + { + // Parse selected device from response + if (string.IsNullOrEmpty(content)) + { + tcs.SetResult(null); + return content; + } + + var selectedDevice = candidates.FirstOrDefault(d => + d.Device.MacAddress.ToString().Equals(content, StringComparison.OrdinalIgnoreCase)); + tcs.SetResult(selectedDevice); + return content; + }), + matchCommand: ProcessCommandResponse + ); + } + catch (Exception ex) + { + tcs.SetException(ex); + } + } - public async Task> RequestDeviceList() => throw new NotImplementedException(); + // Send candidates to local device for selection + _connector.Request() + .UpdateCommand(cmd => + { + cmd.FunctionName = "SelectDevice"; + cmd.FunctionArgs = candidates.Select(d => new Parameter { Value = d.ToString() }).ToList(); + return cmd; + }) + .UpdateRequest(req => + { + req.Type = RequestTypes.Command; + req.Version = RequestVersions.V1; + req.Target = null; // Send to local device + return req; + }) + .SetSender(OnResponse) + .Send(); - private async Task HandleResponse(Request response) + var selectedDevice = await tcs.Task; + return selectedDevice != null ? new Device(selectedDevice) : null; + } + + public async Task> RequestDeviceList() { + if (_cachedDeviceList != null) + return _cachedDeviceList; + + var tcs = new TaskCompletionSource>(); + + void OnResponse(Request response) + { + try + { + var devices = HandleDeviceListResponse(response); + _cachedDeviceList = devices; + tcs.SetResult(devices); + } + catch (Exception ex) + { + tcs.SetException(ex); + } + } + + // Request device list from network + _connector.Request() + .UpdateCommand(cmd => + { + cmd.FunctionName = "GetDeviceList"; + return cmd; + }) + .UpdateRequest(req => + { + req.Type = RequestTypes.Command; + req.Version = RequestVersions.V1; + return req; + }) + .SetSender(OnResponse) + .Send(); + + return await tcs.Task; + } + + private IEnumerable HandleDeviceListResponse(Request response) + { + var devices = new List(); + response.Match( - response.GetContent(content => content), // 这里需要提供实际的解密函数 - matchCommand: ProcessCommandResponse + response.GetContent(content => + { + // Parse device list from content + // This should be replaced with proper deserialization based on response format + // For now returning empty list as placeholder + return content; + }), + matchCommand: content => + { + ProcessCommandResponse(content); + } ); + + return devices; } private void ProcessCommandResponse(string content) { - // 处理命令响应 + if (string.IsNullOrEmpty(content)) + return; + + try + { + // Handle different command responses based on content + if (content.StartsWith("DeviceList:")) + { + var deviceListJson = content["DeviceList:".Length..]; + _cachedDeviceList = JsonSerializer.Deserialize>(deviceListJson); + } + else if (content.StartsWith("Error:")) + { + var error = content["Error:".Length..]; + throw new InvalidOperationException($"Command error: {error}"); + } + } + catch (Exception ex) + { + throw new InvalidOperationException($"Failed to process command response: {ex.Message}", ex); + } } } } diff --git a/KitX Script/Kscript.CSharp/Services/Device.cs b/KitX Script/Kscript.CSharp/Services/Device.cs index 888ece1..b8ed769 100644 --- a/KitX Script/Kscript.CSharp/Services/Device.cs +++ b/KitX Script/Kscript.CSharp/Services/Device.cs @@ -1,6 +1,7 @@ -using KitX.Shared.CSharp.Device; +using KitX.Shared.CSharp.Device; using KitX.Shared.CSharp.Plugin; using KitX.Shared.CSharp.WebCommand; +using System.Text.Json; using Kscript.CSharp.Interfaces; namespace Kscript.CSharp.Services @@ -26,12 +27,92 @@ public async Task RequestPlugin(string Name) return pluginInfo != null ? await CreatePluginInstance(pluginInfo) : null; } - public async Task> GetPluginList() => throw new NotImplementedException(); + public async Task> GetPluginList() + { + if (_pluginCache.Any()) + return _pluginCache.Values; + + var tcs = new TaskCompletionSource>(); + + void OnResponse(Request response) + { + try + { + response.Match( + response.GetContent(content => + { + if (string.IsNullOrEmpty(content)) + { + tcs.SetResult(Array.Empty()); + return content; + } + + // With the following code + var plugins = JsonSerializer.Deserialize>(content); + + // Update cache + _pluginCache = plugins.ToDictionary( + p => p.Name, + p => p, + StringComparer.OrdinalIgnoreCase + ); + + tcs.SetResult(plugins); + return content; + }), + matchCommand: content => + { + if (content.StartsWith("Error:")) + { + var error = content.Substring("Error:".Length); + tcs.SetException(new InvalidOperationException($"Failed to get plugin list: {error}")); + } + else if (content.StartsWith("PluginList:")) + { + var pluginListJson = content.Substring("PluginList:".Length); + var plugins = JsonSerializer.Deserialize>(pluginListJson); + + // Update cache + _pluginCache = plugins.ToDictionary( + p => p.Name, + p => p, + StringComparer.OrdinalIgnoreCase + ); + + tcs.SetResult(plugins); + } + } + ); + } + catch (Exception ex) + { + tcs.SetException(ex); + } + } + + // Request plugin list from device + _connector.Request() + .UpdateCommand(cmd => + { + cmd.FunctionName = "GetPluginList"; + return cmd; + }) + .UpdateRequest(req => + { + req.Type = RequestTypes.Command; + req.Version = RequestVersions.V1; + req.Target = Info.Device; + return req; + }) + .SetSender(OnResponse) + .Send(); + + return await tcs.Task; + } public bool HasPlugin(string Name) { - return _pluginCache.Values.Any(p => - p.Name.Equals(Name, StringComparison.OrdinalIgnoreCase)); + return _pluginCache.ContainsKey(Name); } public async Task CreatePluginInstance(PluginInfo info) diff --git a/KitX Script/Kscript.CSharp/Services/Function.cs b/KitX Script/Kscript.CSharp/Services/Function.cs index db23572..0ea316f 100644 --- a/KitX Script/Kscript.CSharp/Services/Function.cs +++ b/KitX Script/Kscript.CSharp/Services/Function.cs @@ -36,13 +36,120 @@ public async Task Invoke(params string[] parameters) }) .Send(); - // 等待并处理响应 - return null; // 需要处理实际响应 + var tcs = new TaskCompletionSource(); + + void OnResponse(Request response) + { + try + { + response.Match( + response.GetContent(content => + { + if (string.IsNullOrEmpty(content)) + { + tcs.SetResult(null); + return content; + } + + // Parse response based on return type + var returnType = GetReturnType(); + var result = ParseResponse(content, returnType); + tcs.SetResult(result); + return content; + }), + matchCommand: content => + { + if (content.StartsWith("Error:")) + { + var error = content.Substring("Error:".Length); + tcs.SetException(new InvalidOperationException($"Function execution failed: {error}")); + } + else if (content.StartsWith("Result:")) + { + var resultJson = content.Substring("Result:".Length); + var returnType = GetReturnType(); + var result = ParseResponse(resultJson, returnType); + tcs.SetResult(result); + } + } + ); + } + catch (Exception ex) + { + tcs.SetException(ex); + } + } + + _connector.Request() + .SetSender(OnResponse); + + return await tcs.Task; } public Type GetReturnType() { - return Type.GetType(Info.ReturnValueType) ?? typeof(object); + // Try to get the type from the assembly first + var type = Type.GetType(Info.ReturnValueType); + if (type != null) + return type; + + // Handle common type names that might not be fully qualified + switch (Info.ReturnValueType.ToLower()) + { + case "string": + return typeof(string); + case "int": + case "int32": + return typeof(int); + case "long": + case "int64": + return typeof(long); + case "float": + case "single": + return typeof(float); + case "double": + return typeof(double); + case "bool": + case "boolean": + return typeof(bool); + case "void": + return typeof(void); + default: + return typeof(object); + } + } + + private object ParseResponse(string content, Type returnType) + { + if (string.IsNullOrEmpty(content)) + return null; + + try + { + if (returnType == typeof(void)) + return null; + + // Handle primitive types + if (returnType == typeof(string)) + return content; + if (returnType == typeof(int)) + return int.Parse(content); + if (returnType == typeof(long)) + return long.Parse(content); + if (returnType == typeof(float)) + return float.Parse(content); + if (returnType == typeof(double)) + return double.Parse(content); + if (returnType == typeof(bool)) + return bool.Parse(content); + + // For complex types, deserialize from JSON + return System.Text.Json.JsonSerializer.Deserialize(content, returnType); + } + catch (Exception ex) + { + throw new InvalidOperationException($"Failed to parse response as {returnType.Name}: {ex.Message}", ex); + } } } } diff --git a/KitX Script/Kscript.CSharp/Services/Plugin.cs b/KitX Script/Kscript.CSharp/Services/Plugin.cs index 2e3ae92..8038730 100644 --- a/KitX Script/Kscript.CSharp/Services/Plugin.cs +++ b/KitX Script/Kscript.CSharp/Services/Plugin.cs @@ -57,7 +57,119 @@ public async Task ExecuteFunction(string functionName, params string[] p .Send(); // 处理响应 - return null; // 需要处理实际响应 + var tcs = new TaskCompletionSource(); + + void OnResponse(Request response) + { + try + { + response.Match( + response.GetContent(content => + { + var returnType = GetFunctionReturnType(functionName); + var result = ParseFunctionResponse(content, returnType); + tcs.SetResult(result); + return content; + }), + matchCommand: content => + { + if (content.StartsWith("Error:")) + { + var error = content.Substring("Error:".Length); + tcs.SetException(new InvalidOperationException($"Function execution failed: {error}")); + } + else if (content.StartsWith("Result:")) + { + var resultJson = content.Substring("Result:".Length); + var returnType = GetFunctionReturnType(functionName); + var result = ParseFunctionResponse(resultJson, returnType); + tcs.SetResult(result); + } + } + ); + } + catch (Exception ex) + { + tcs.SetException(ex); + } + } + + _connector.Request() + .SetSender(OnResponse); + + return await tcs.Task; + } + + private Type GetFunctionReturnType(string functionName) + { + var function = Info.Functions.FirstOrDefault(f => + f.Name.Equals(functionName, StringComparison.OrdinalIgnoreCase)); + + if (function == null) + throw new InvalidOperationException($"Function '{functionName}' not found in plugin '{Info.Name}'"); + + // Try to get the type from the assembly first + var type = Type.GetType(function.ReturnValueType); + if (type != null) + return type; + + // Handle common type names that might not be fully qualified + switch (function.ReturnValueType.ToLower()) + { + case "string": + return typeof(string); + case "int": + case "int32": + return typeof(int); + case "long": + case "int64": + return typeof(long); + case "float": + case "single": + return typeof(float); + case "double": + return typeof(double); + case "bool": + case "boolean": + return typeof(bool); + case "void": + return typeof(void); + default: + return typeof(object); + } + } + + private object ParseFunctionResponse(string content, Type returnType) + { + if (string.IsNullOrEmpty(content)) + return null; + + try + { + if (returnType == typeof(void)) + return null; + + // Handle primitive types + if (returnType == typeof(string)) + return content; + if (returnType == typeof(int)) + return int.Parse(content); + if (returnType == typeof(long)) + return long.Parse(content); + if (returnType == typeof(float)) + return float.Parse(content); + if (returnType == typeof(double)) + return double.Parse(content); + if (returnType == typeof(bool)) + return bool.Parse(content); + + // For complex types, deserialize from JSON + return System.Text.Json.JsonSerializer.Deserialize(content, returnType); + } + catch (Exception ex) + { + throw new InvalidOperationException($"Failed to parse response as {returnType.Name}: {ex.Message}", ex); + } } public bool HasFunction(string Name) diff --git a/KitX Shared/KitX.Shared.CSharp/KitX.Shared.CSharp.csproj b/KitX Shared/KitX.Shared.CSharp/KitX.Shared.CSharp.csproj index 03d1926..6246e52 100644 --- a/KitX Shared/KitX.Shared.CSharp/KitX.Shared.CSharp.csproj +++ b/KitX Shared/KitX.Shared.CSharp/KitX.Shared.CSharp.csproj @@ -37,4 +37,8 @@ + + + + From c759c6ac16ee6faf6b7c5c661bf0795849d67281 Mon Sep 17 00:00:00 2001 From: StarInk Date: Sat, 15 Feb 2025 10:25:41 +0200 Subject: [PATCH 06/60] =?UTF-8?q?=F0=9F=92=BE=20Feat(Kscript.CSharp):=20?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=9C=8D=E5=8A=A1=E7=B1=BB=E4=BB=A5=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E5=8F=AF=E9=80=89=E7=9A=84=20Connector=20=E5=8F=82?= =?UTF-8?q?=E6=95=B0=EF=BC=8C=E4=BC=98=E5=8C=96=E6=8F=92=E4=BB=B6=E5=92=8C?= =?UTF-8?q?=E8=AE=BE=E5=A4=87=E5=AE=9E=E4=BE=8B=E5=8C=96=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- KitX Script/Kscript.CSharp/Services/Composer.cs | 9 +++++++-- KitX Script/Kscript.CSharp/Services/Device.cs | 7 ++++--- KitX Script/Kscript.CSharp/Services/Function.cs | 5 +++-- KitX Script/Kscript.CSharp/Services/Plugin.cs | 11 ++++++----- 4 files changed, 20 insertions(+), 12 deletions(-) diff --git a/KitX Script/Kscript.CSharp/Services/Composer.cs b/KitX Script/Kscript.CSharp/Services/Composer.cs index b911ac8..a707c2f 100644 --- a/KitX Script/Kscript.CSharp/Services/Composer.cs +++ b/KitX Script/Kscript.CSharp/Services/Composer.cs @@ -8,7 +8,12 @@ namespace Kscript.CSharp.Services { public class Composer : IComposer { - private readonly Connector _connector = Connector.Instance; + private readonly Connector _connector; + + public Composer(Connector? connector = null) + { + _connector = connector ?? Connector.Instance; + } private IEnumerable? _cachedDeviceList; public async Task RequestLocalDevice() @@ -98,7 +103,7 @@ void OnResponse(Request response) .UpdateCommand(cmd => { cmd.FunctionName = "SelectDevice"; - cmd.FunctionArgs = candidates.Select(d => new Parameter { Value = d.ToString() }).ToList(); + cmd.FunctionArgs = candidates.Select(d => new Parameter { Value = d.ToString() }).ToList(); // todo: 改为传入一个函数用于筛选设备 return cmd; }) .UpdateRequest(req => diff --git a/KitX Script/Kscript.CSharp/Services/Device.cs b/KitX Script/Kscript.CSharp/Services/Device.cs index b8ed769..52d222d 100644 --- a/KitX Script/Kscript.CSharp/Services/Device.cs +++ b/KitX Script/Kscript.CSharp/Services/Device.cs @@ -8,14 +8,15 @@ namespace Kscript.CSharp.Services { public class Device : IDevice { - private readonly Connector _connector = Connector.Instance; + private readonly Connector _connector; private Dictionary _pluginCache = new(); public DeviceInfo Info { get; } - public Device(DeviceInfo info) + public Device(DeviceInfo info, Connector? connector = null) { Info = info; + _connector = connector ?? Connector.Instance; } public async Task RequestPlugin(string Name) @@ -117,7 +118,7 @@ public bool HasPlugin(string Name) public async Task CreatePluginInstance(PluginInfo info) { - return new Plugin(info, Info); + return new Plugin(info, Info, _connector); } } } diff --git a/KitX Script/Kscript.CSharp/Services/Function.cs b/KitX Script/Kscript.CSharp/Services/Function.cs index 0ea316f..f9aefba 100644 --- a/KitX Script/Kscript.CSharp/Services/Function.cs +++ b/KitX Script/Kscript.CSharp/Services/Function.cs @@ -7,17 +7,18 @@ namespace Kscript.CSharp.Services { public class Function : IFunction { - private readonly Connector _connector = Connector.Instance; + private readonly Connector _connector; private readonly DeviceInfo _deviceInfo; public KitX.Shared.CSharp.Plugin.Function Info { get; } public PluginInfo AssociatedPlugin { get; } - public Function(KitX.Shared.CSharp.Plugin.Function info, PluginInfo pluginInfo, DeviceInfo deviceInfo) + public Function(KitX.Shared.CSharp.Plugin.Function info, PluginInfo pluginInfo, DeviceInfo deviceInfo, Connector? connector = null) { Info = info; AssociatedPlugin = pluginInfo; _deviceInfo = deviceInfo; + _connector = connector ?? Connector.Instance; } public async Task Invoke(params string[] parameters) diff --git a/KitX Script/Kscript.CSharp/Services/Plugin.cs b/KitX Script/Kscript.CSharp/Services/Plugin.cs index 8038730..b3f9a45 100644 --- a/KitX Script/Kscript.CSharp/Services/Plugin.cs +++ b/KitX Script/Kscript.CSharp/Services/Plugin.cs @@ -1,4 +1,4 @@ -using KitX.Shared.CSharp.Device; +using KitX.Shared.CSharp.Device; using KitX.Shared.CSharp.Plugin; using KitX.Shared.CSharp.WebCommand; using Kscript.CSharp.Interfaces; @@ -7,15 +7,16 @@ namespace Kscript.CSharp.Services { public class Plugin : IPlugin { - private readonly Connector _connector = Connector.Instance; + private readonly Connector _connector; public PluginInfo Info { get; } public DeviceInfo AssociatedDevice { get; } - public Plugin(PluginInfo info, DeviceInfo deviceInfo) + public Plugin(PluginInfo info, DeviceInfo deviceInfo, Connector? connector = null) { Info = info; AssociatedDevice = deviceInfo; + _connector = connector ?? Connector.Instance; } public async Task RequestFunction(string Name) @@ -29,7 +30,7 @@ public async Task RequestFunction(string Name) public async Task> GetFunctionList() { var functions = Info.Functions.Select(f => - new Function(f, Info, AssociatedDevice) as IFunction); + new Function(f, Info, AssociatedDevice, _connector) as IFunction); return await Task.FromResult(functions); } @@ -105,7 +106,7 @@ private Type GetFunctionReturnType(string functionName) var function = Info.Functions.FirstOrDefault(f => f.Name.Equals(functionName, StringComparison.OrdinalIgnoreCase)); - if (function == null) + if (function.Equals(default(Function))) throw new InvalidOperationException($"Function '{functionName}' not found in plugin '{Info.Name}'"); // Try to get the type from the assembly first From d72cb557019b56461dc31e9e492cfb9a47840383 Mon Sep 17 00:00:00 2001 From: StarInk Date: Sat, 15 Feb 2025 21:53:18 +0200 Subject: [PATCH 07/60] =?UTF-8?q?=F0=9F=92=BE=20Feat(Kscript.CSharp):=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E8=AE=BE=E5=A4=87=E4=BA=8B=E4=BB=B6=E7=9B=91?= =?UTF-8?q?=E5=90=AC=E5=99=A8=E6=8E=A5=E5=8F=A3=E5=92=8C=E7=BD=91=E7=BB=9C?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E7=B1=BB=EF=BC=8C=E6=8F=90=E4=BE=9B=E8=AE=BE?= =?UTF-8?q?=E5=A4=87=E7=8A=B6=E6=80=81=E6=9B=B4=E6=96=B0=E5=92=8C=E7=BD=91?= =?UTF-8?q?=E7=BB=9C=E5=9C=B0=E5=9D=80=E6=A3=80=E6=9F=A5=E5=8A=9F=E8=83=BD?= =?UTF-8?q?-=E6=83=B3=E9=87=8D=E6=9E=84=E5=AE=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Interfaces/IDeviceEventListener.cs | 34 ++ .../Kscript.CSharp/Services/Composer.cs | 315 ++++++++++-------- .../Kscript.CSharp/Utils/DeviceCache.cs | 193 +++++++++++ .../Utils/DeviceRequestBuilder.cs | 141 ++++++++ .../Kscript.CSharp/Utils/NetworkUtils.cs | 93 ++++++ .../Kscript.CSharp/Utils/ResponseHandler.cs | 124 +++++++ 6 files changed, 770 insertions(+), 130 deletions(-) create mode 100644 KitX Script/Kscript.CSharp/Interfaces/IDeviceEventListener.cs create mode 100644 KitX Script/Kscript.CSharp/Utils/DeviceCache.cs create mode 100644 KitX Script/Kscript.CSharp/Utils/DeviceRequestBuilder.cs create mode 100644 KitX Script/Kscript.CSharp/Utils/NetworkUtils.cs create mode 100644 KitX Script/Kscript.CSharp/Utils/ResponseHandler.cs diff --git a/KitX Script/Kscript.CSharp/Interfaces/IDeviceEventListener.cs b/KitX Script/Kscript.CSharp/Interfaces/IDeviceEventListener.cs new file mode 100644 index 0000000..3071b78 --- /dev/null +++ b/KitX Script/Kscript.CSharp/Interfaces/IDeviceEventListener.cs @@ -0,0 +1,34 @@ +using KitX.Shared.CSharp.Device; + +namespace Kscript.CSharp.Interfaces +{ + /// + /// 设备事件监听器接口 + /// + public interface IDeviceEventListener + { + /// + /// 当设备列表更新时触发 + /// + /// 更新后的设备列表 + void OnDeviceListUpdated(IEnumerable devices); + + /// + /// 当有新设备添加时触发 + /// + /// 新添加的设备 + void OnDeviceAdded(DeviceInfo device); + + /// + /// 当设备被移除时触发 + /// + /// 被移除的设备 + void OnDeviceRemoved(DeviceInfo device); + + /// + /// 当设备状态变化时触发 + /// + /// 状态发生变化的设备 + void OnDeviceStatusChanged(DeviceInfo device); + } +} \ No newline at end of file diff --git a/KitX Script/Kscript.CSharp/Services/Composer.cs b/KitX Script/Kscript.CSharp/Services/Composer.cs index a707c2f..0f1f520 100644 --- a/KitX Script/Kscript.CSharp/Services/Composer.cs +++ b/KitX Script/Kscript.CSharp/Services/Composer.cs @@ -2,207 +2,262 @@ using KitX.Shared.CSharp.Plugin; using KitX.Shared.CSharp.WebCommand; using Kscript.CSharp.Interfaces; +using Kscript.CSharp.Utils; using System.Text.Json; namespace Kscript.CSharp.Services { - public class Composer : IComposer + /// + /// 设备管理器 + /// + public class Composer : IComposer, IDisposable { private readonly Connector _connector; + private readonly DeviceCache _deviceCache; + private readonly DeviceRequestBuilder _requestBuilder; + private readonly HashSet _listeners = new(); + private bool _isDisposed; + + private readonly object _lockObject = new(); + private Dictionary _lastKnownDevices = new(); + + // 设备离线判断阈值(超过这个时间没有收到更新则认为设备离线) + private static readonly TimeSpan DeviceOfflineThreshold = TimeSpan.FromMinutes(2); public Composer(Connector? connector = null) { _connector = connector ?? Connector.Instance; + _deviceCache = new DeviceCache(); + _requestBuilder = new DeviceRequestBuilder(_connector); + } + + /// + /// 添加设备事件监听器 + /// + public void AddListener(IDeviceEventListener listener) + { + if (_isDisposed) + throw new ObjectDisposedException(nameof(Composer)); + + lock (_lockObject) + { + _listeners.Add(listener); + } } - private IEnumerable? _cachedDeviceList; + /// + /// 移除设备事件监听器 + /// + public void RemoveListener(IDeviceEventListener listener) + { + if (_isDisposed) + return; + + lock (_lockObject) + { + _listeners.Remove(listener); + } + } + + /// + /// 获取本地设备 + /// public async Task RequestLocalDevice() { var devices = await RequestDeviceList(); - var localDevice = devices.FirstOrDefault(x => - (x.Device.IPv4?.Equals("127.0.0.1") ?? false) || // Check localhost - (x.Device.IPv4?.Equals("::1") ?? false)); // Check IPv6 localhost - // todo: fixme: 这个检查方案会有问题。改进建议:检查IP地址是否与本地设备的IP地址有交集。因为广播出来的地址可能是本机地址的外网地址 - - return localDevice != null ? new Device(localDevice) : null; + var localDevice = devices.FirstOrDefault(NetworkUtils.IsLocalDevice); + return localDevice != null ? new Device(localDevice, _connector) : null; } + /// + /// 获取主控制器设备 + /// public async Task RequestMainController() { var devices = await RequestDeviceList(); var mainController = devices.FirstOrDefault(x => x.IsMainDevice); - return mainController != null ? new Device(mainController) : null; + return mainController != null ? new Device(mainController, _connector) : null; } + /// + /// 获取随机桌面设备 + /// public async Task RequestRandomDesktopDevice() { var devices = await RequestDeviceList(); var desktopDevices = devices.Where(x => x.DeviceOSType == OperatingSystems.Windows - || x.DeviceOSType == OperatingSystems.MacOS - || x.DeviceOSType == OperatingSystems.Linux).ToList(); - if (desktopDevices.Count == 0) return null; + || x.DeviceOSType == OperatingSystems.MacOS + || x.DeviceOSType == OperatingSystems.Linux) + .ToList(); + + if (!desktopDevices.Any()) + return null; var random = new Random(); var randomDevice = desktopDevices[random.Next(desktopDevices.Count)]; - return new Device(randomDevice); + return new Device(randomDevice, _connector); } + /// + /// 获取随机移动设备 + /// public async Task RequestRandomMobileDevice() { var devices = await RequestDeviceList(); var mobileDevices = devices.Where(x => x.DeviceOSType == OperatingSystems.Android - || x.DeviceOSType == OperatingSystems.IOS).ToList(); - if (mobileDevices.Count == 0) return null; + || x.DeviceOSType == OperatingSystems.IOS) + .ToList(); + if (!mobileDevices.Any()) + return null; + var random = new Random(); var randomDevice = mobileDevices[random.Next(mobileDevices.Count)]; - return new Device(randomDevice); + return new Device(randomDevice, _connector); } + /// + /// 根据过滤器获取设备 + /// public async Task RequestDeviceByFilter(Func filter) { var devices = await RequestDeviceList(); var matchedDevice = devices.FirstOrDefault(filter); - return matchedDevice != null ? new Device(matchedDevice) : null; + return matchedDevice != null ? new Device(matchedDevice, _connector) : null; } + /// + /// 获取用户选择的设备 + /// public async Task RequestUserSelectedDevice(IEnumerable candidates) { - var tcs = new TaskCompletionSource(); - - void OnResponse(Request response) - { - try + var response = await _requestBuilder + .WithFunction("SelectDevice", candidates) + .ExecuteAsync(async response => { - response.Match( - response.GetContent(content => // 这里返回的是用户选择的设备的 MAC 地址 - { - // Parse selected device from response - if (string.IsNullOrEmpty(content)) - { - tcs.SetResult(null); - return content; - } - - var selectedDevice = candidates.FirstOrDefault(d => - d.Device.MacAddress.ToString().Equals(content, StringComparison.OrdinalIgnoreCase)); - tcs.SetResult(selectedDevice); - return content; - }), - matchCommand: ProcessCommandResponse - ); - } - catch (Exception ex) - { - tcs.SetException(ex); - } - } + var result = await ResponseHandler.HandleStringResponse(response); + if (string.IsNullOrEmpty(result)) + return null; - // Send candidates to local device for selection - _connector.Request() - .UpdateCommand(cmd => - { - cmd.FunctionName = "SelectDevice"; - cmd.FunctionArgs = candidates.Select(d => new Parameter { Value = d.ToString() }).ToList(); // todo: 改为传入一个函数用于筛选设备 - return cmd; - }) - .UpdateRequest(req => - { - req.Type = RequestTypes.Command; - req.Version = RequestVersions.V1; - req.Target = null; // Send to local device - return req; - }) - .SetSender(OnResponse) - .Send(); - - var selectedDevice = await tcs.Task; - return selectedDevice != null ? new Device(selectedDevice) : null; + return candidates.FirstOrDefault(d => + d.Device.MacAddress.ToString().Equals(result, StringComparison.OrdinalIgnoreCase)); + }); + + return response != null ? new Device(response, _connector) : null; } + /// + /// 获取设备列表 + /// public async Task> RequestDeviceList() { - if (_cachedDeviceList != null) - return _cachedDeviceList; + return await _deviceCache.GetOrAdd("devices", async () => + { + var response = await _requestBuilder + .WithFunction("GetDeviceList") + .ExecuteAsync(async response => + { + var result = await ResponseHandler.HandlePrefixedResponse>( + response, + "DeviceList:"); + + if (result != null) + UpdateDeviceStates(result); - var tcs = new TaskCompletionSource>(); + return result ?? new List(); + }); - void OnResponse(Request response) + return response; + }); + } + + private void UpdateDeviceStates(IEnumerable newDevices) + { + if (_isDisposed) + return; + + lock (_lockObject) { - try - { - var devices = HandleDeviceListResponse(response); - _cachedDeviceList = devices; - tcs.SetResult(devices); - } - catch (Exception ex) + var newDevicesDict = newDevices.ToDictionary(d => d.Device.MacAddress.ToString()); + + // Find removed devices + var removedDevices = _lastKnownDevices.Keys + .Except(newDevicesDict.Keys) + .Select(key => _lastKnownDevices[key]) + .ToList(); + + // Find added devices + var addedDevices = newDevicesDict.Keys + .Except(_lastKnownDevices.Keys) + .Select(key => newDevicesDict[key]) + .ToList(); + + // Find changed devices + var changedDevices = _lastKnownDevices.Keys + .Intersect(newDevicesDict.Keys) + .Where(key => HasDeviceChanged(_lastKnownDevices[key], newDevicesDict[key])) + .Select(key => newDevicesDict[key]) + .ToList(); + + // Update last known devices + _lastKnownDevices = newDevicesDict; + + // Notify listeners + var listeners = _listeners.ToList(); + foreach (var listener in listeners) { - tcs.SetException(ex); + try + { + foreach (var device in removedDevices) + listener.OnDeviceRemoved(device); + + foreach (var device in addedDevices) + listener.OnDeviceAdded(device); + + foreach (var device in changedDevices) + listener.OnDeviceStatusChanged(device); + + listener.OnDeviceListUpdated(newDevices); + } + catch (Exception ex) + { + // Log error but continue with other listeners + System.Diagnostics.Debug.WriteLine($"Error in device listener: {ex.Message}"); + } } } - - // Request device list from network - _connector.Request() - .UpdateCommand(cmd => - { - cmd.FunctionName = "GetDeviceList"; - return cmd; - }) - .UpdateRequest(req => - { - req.Type = RequestTypes.Command; - req.Version = RequestVersions.V1; - return req; - }) - .SetSender(OnResponse) - .Send(); - - return await tcs.Task; } - private IEnumerable HandleDeviceListResponse(Request response) + private bool HasDeviceChanged(DeviceInfo oldDevice, DeviceInfo newDevice) { - var devices = new List(); - - response.Match( - response.GetContent(content => - { - // Parse device list from content - // This should be replaced with proper deserialization based on response format - // For now returning empty list as placeholder - return content; - }), - matchCommand: content => - { - ProcessCommandResponse(content); - } - ); + // 检查设备在线状态(基于最后一次发送时间) + var oldDeviceOnline = DateTime.UtcNow - oldDevice.SendTime < DeviceOfflineThreshold; + var newDeviceOnline = DateTime.UtcNow - newDevice.SendTime < DeviceOfflineThreshold; - return devices; + if (oldDeviceOnline != newDeviceOnline) + return true; + + // 检查关键属性变化 + return oldDevice.PluginsCount != newDevice.PluginsCount || + oldDevice.Device.IPv4 != newDevice.Device.IPv4 || + oldDevice.Device.IPv6 != newDevice.Device.IPv6 || + oldDevice.PluginsServerPort != newDevice.PluginsServerPort || + oldDevice.DevicesServerPort != newDevice.DevicesServerPort || + oldDevice.IsMainDevice != newDevice.IsMainDevice || + oldDevice.DeviceOSVersion != newDevice.DeviceOSVersion; } - private void ProcessCommandResponse(string content) + public void Dispose() { - if (string.IsNullOrEmpty(content)) + if (_isDisposed) return; - try - { - // Handle different command responses based on content - if (content.StartsWith("DeviceList:")) - { - var deviceListJson = content["DeviceList:".Length..]; - _cachedDeviceList = JsonSerializer.Deserialize>(deviceListJson); - } - else if (content.StartsWith("Error:")) - { - var error = content["Error:".Length..]; - throw new InvalidOperationException($"Command error: {error}"); - } - } - catch (Exception ex) + _isDisposed = true; + _deviceCache.Dispose(); + lock (_lockObject) { - throw new InvalidOperationException($"Failed to process command response: {ex.Message}", ex); + _listeners.Clear(); + _lastKnownDevices.Clear(); } } } diff --git a/KitX Script/Kscript.CSharp/Utils/DeviceCache.cs b/KitX Script/Kscript.CSharp/Utils/DeviceCache.cs new file mode 100644 index 0000000..7598a04 --- /dev/null +++ b/KitX Script/Kscript.CSharp/Utils/DeviceCache.cs @@ -0,0 +1,193 @@ +using System.Collections.Concurrent; +using KitX.Shared.CSharp.Device; + +namespace Kscript.CSharp.Utils +{ + /// + /// 设备信息缓存管理器 + /// + public class DeviceCache : IDisposable + { + private class CacheEntry + { + public T Value { get; set; } + public DateTime Expiration { get; set; } + public DateTime CreatedAt { get; } = DateTime.UtcNow; + private int _accessCount; + + public bool IsExpired => DateTime.UtcNow > Expiration; + + public int GetAccessCount() + { + return Interlocked.CompareExchange(ref _accessCount, 0, 0); + } + + public void IncrementAccess() + { + Interlocked.Increment(ref _accessCount); + } + } + + private readonly ConcurrentDictionary>> _cache = new(); + private readonly TimeSpan _defaultExpiration; + private readonly Timer _cleanupTimer; + private volatile bool _isDisposed; + + /// + /// 初始化设备缓存 + /// + /// 默认的缓存过期时间 + /// 缓存清理的时间间隔 + public DeviceCache( + TimeSpan? defaultExpiration = null, + TimeSpan? cleanupInterval = null) + { + _defaultExpiration = defaultExpiration ?? TimeSpan.FromMinutes(1); + var interval = cleanupInterval ?? TimeSpan.FromMinutes(5); + + _cleanupTimer = new Timer( + CleanupCallback, + null, + interval, + interval + ); + } + + /// + /// 设置缓存项 + /// + public void Set(string key, IEnumerable value, TimeSpan? expiration = null) + { + var entry = new CacheEntry> + { + Value = value, + Expiration = DateTime.UtcNow.Add(expiration ?? _defaultExpiration) + }; + + _cache.AddOrUpdate(key, _ => entry, (_, __) => entry); + } + + /// + /// 尝试获取缓存项 + /// + public IEnumerable? Get(string key) + { + var entry = _cache.GetOrAdd(key, _ => null); + if (entry == null || entry.IsExpired) + { + _cache.TryRemove(key, out _); + return null; + } + + entry.IncrementAccess(); + return entry.Value; + } + + /// + /// 检查缓存项是否存在且有效 + /// + public bool IsValid(string key) + { + var entry = _cache.GetOrAdd(key, _ => null); + return entry != null && !entry.IsExpired; + } + + /// + /// 获取缓存项,如果不存在或已过期则使用提供的函数获取新值 + /// + public async Task> GetOrAdd( + string key, + Func>> valueFactory, + TimeSpan? expiration = null) + { + var existingValue = Get(key); + if (existingValue != null) + return existingValue; + + var newValue = await valueFactory(); + Set(key, newValue, expiration); + return newValue; + } + + /// + /// 移除缓存项 + /// + public bool Remove(string key) + { + return _cache.TryRemove(key, out _); + } + + /// + /// 清除所有缓存 + /// + public void Clear() + { + _cache.Clear(); + } + + /// + /// 获取缓存统计信息 + /// + public CacheStatistics GetStatistics() + { + var now = DateTime.UtcNow; + var entries = _cache.ToArray(); + + return new CacheStatistics + { + TotalItems = entries.Length, + ExpiredItems = entries.Count(x => x.Value.IsExpired), + TotalAccesses = entries.Sum(x => x.Value.GetAccessCount()), + AverageAccessesPerItem = entries.Length > 0 + ? entries.Average(x => x.Value.GetAccessCount()) + : 0, + OldestItemAge = entries.Length > 0 + ? (now - entries.Min(x => x.Value.CreatedAt)) + : TimeSpan.Zero, + AverageItemAge = entries.Length > 0 + ? TimeSpan.FromTicks((long)entries.Average(x => + (now - x.Value.CreatedAt).Ticks)) + : TimeSpan.Zero + }; + } + + private void CleanupCallback(object? state) + { + if (_isDisposed) + return; + + var expiredKeys = _cache + .Where(kvp => kvp.Value.IsExpired) + .Select(kvp => kvp.Key) + .ToList(); + + foreach (var key in expiredKeys) + { + _cache.TryRemove(key, out _); + } + } + + public void Dispose() + { + if (_isDisposed) + return; + + _isDisposed = true; + _cleanupTimer.Dispose(); + Clear(); + } + } + + /// + /// 缓存统计信息 + /// + public class CacheStatistics + { + public int TotalItems { get; set; } + public int ExpiredItems { get; set; } + public int TotalAccesses { get; set; } + public double AverageAccessesPerItem { get; set; } + public TimeSpan OldestItemAge { get; set; } + public TimeSpan AverageItemAge { get; set; } + } +} \ No newline at end of file diff --git a/KitX Script/Kscript.CSharp/Utils/DeviceRequestBuilder.cs b/KitX Script/Kscript.CSharp/Utils/DeviceRequestBuilder.cs new file mode 100644 index 0000000..8ffa998 --- /dev/null +++ b/KitX Script/Kscript.CSharp/Utils/DeviceRequestBuilder.cs @@ -0,0 +1,141 @@ +using System.Text.Json; +using KitX.Shared.CSharp.WebCommand; +using KitX.Shared.CSharp.Device; +using KitX.Shared.CSharp.Plugin; + +namespace Kscript.CSharp.Utils +{ + /// + /// 设备请求构建器 + /// + public class DeviceRequestBuilder + { + private readonly Connector _connector; + private Command _command; + private Request _request; + + public DeviceRequestBuilder(Connector connector) + { + _connector = connector; + _command = new Command(); + _request = new Request + { + Type = RequestTypes.Command, + Version = RequestVersions.V1 + }; + } + + /// + /// 设置请求的函数名和参数 + /// + public DeviceRequestBuilder WithFunction(string name, params object[] args) + { + _command.FunctionName = name; + _command.FunctionArgs = args.Select(a => new Parameter + { + Value = JsonSerializer.Serialize(a), + Type = a.GetType().Name + }).ToList(); + return this; + } + + /// + /// 设置请求的目标设备 + /// + public DeviceRequestBuilder WithTarget(DeviceLocator? target) + { + _request.Target = target; + return this; + } + + /// + /// 设置请求的目标设备信息 + /// + public DeviceRequestBuilder WithTargetDevice(DeviceInfo device) + { + return WithTarget(device.Device); + } + + /// + /// 执行请求并处理响应 + /// + public async Task ExecuteAsync(Func> responseHandler) + { + var tcs = new TaskCompletionSource(); + + void OnResponse(Request response) + { + try + { + var result = responseHandler(response); + result.ContinueWith(t => + { + if (t.IsFaulted) + tcs.SetException(t.Exception ?? new Exception("Unknown error")); + else if (t.IsCanceled) + tcs.SetCanceled(); + else + tcs.SetResult(t.Result); + }); + } + catch (Exception ex) + { + tcs.SetException(ex); + } + } + + _connector.Request() + .UpdateCommand(cmd => + { + cmd.FunctionName = _command.FunctionName; + cmd.FunctionArgs = _command.FunctionArgs; + return cmd; + }) + .UpdateRequest(req => + { + req.Type = _request.Type; + req.Version = _request.Version; + req.Target = _request.Target; + return req; + }) + .SetSender(OnResponse) + .Send(); + + return await tcs.Task; + } + + /// + /// 重置构建器状态 + /// + public DeviceRequestBuilder Reset() + { + _command = new Command(); + _request = new Request + { + Type = RequestTypes.Command, + Version = RequestVersions.V1 + }; + return this; + } + + /// + /// 创建请求克隆 + /// + public DeviceRequestBuilder Clone() + { + var clone = new DeviceRequestBuilder(_connector); + clone._command = new Command + { + FunctionName = _command.FunctionName, + FunctionArgs = _command.FunctionArgs?.ToList() + }; + clone._request = new Request + { + Type = _request.Type, + Version = _request.Version, + Target = _request.Target + }; + return clone; + } + } +} \ No newline at end of file diff --git a/KitX Script/Kscript.CSharp/Utils/NetworkUtils.cs b/KitX Script/Kscript.CSharp/Utils/NetworkUtils.cs new file mode 100644 index 0000000..f5c0eac --- /dev/null +++ b/KitX Script/Kscript.CSharp/Utils/NetworkUtils.cs @@ -0,0 +1,93 @@ +using System.Net.NetworkInformation; +using KitX.Shared.CSharp.Device; + +namespace Kscript.CSharp.Utils +{ + /// + /// 提供网络相关的工具方法 + /// + public static class NetworkUtils + { + /// + /// 检查给定的设备是否是本地设备 + /// + /// 要检查的设备信息 + /// 如果是本地设备返回true,否则返回false + public static bool IsLocalDevice(DeviceInfo device) + { + if (device.Device.IPv4 == null) + return false; + + // 获取本机所有IP地址 + var localIPs = GetLocalIPAddresses(); + + // 检查设备IP是否与本机IP匹配 + return localIPs.Contains(device.Device.IPv4) || + IsLocalNetworkAddress(device.Device.IPv4); + } + + /// + /// 获取本机所有网络接口的IP地址 + /// + private static HashSet GetLocalIPAddresses() + { + return new HashSet( + NetworkInterface.GetAllNetworkInterfaces() + .Where(n => n.OperationalStatus == OperationalStatus.Up) + .SelectMany(n => n.GetIPProperties().UnicastAddresses) + .Select(a => a.Address.ToString()) + ); + } + + /// + /// 检查给定的IP地址是否为本地网络地址 + /// + private static bool IsLocalNetworkAddress(string ip) + { + return ip.StartsWith("127.") || // Loopback + ip.StartsWith("169.254.") || // Link-local + ip.StartsWith("192.168.") || // 私有网络 + ip.StartsWith("10.") || // 私有网络 + ip.StartsWith("172.") || // 私有网络 + ip.Equals("::1"); // IPv6 loopback + } + + /// + /// 检查给定的IP地址是否为私有网络地址 + /// + public static bool IsPrivateNetworkAddress(string ip) + { + return ip.StartsWith("192.168.") || // Class C private network + ip.StartsWith("10.") || // Class A private network + (ip.StartsWith("172.") && // Class B private network + TryGetSecondOctet(ip, out var secondOctet) && + secondOctet >= 16 && secondOctet <= 31); + } + + private static bool TryGetSecondOctet(string ip, out int octet) + { + octet = 0; + var parts = ip.Split('.'); + if (parts.Length < 2) return false; + return int.TryParse(parts[1], out octet); + } + + /// + /// 判断两个IP地址是否在同一子网内 + /// + public static bool IsInSameSubnet(string ip1, string ip2, string subnetMask) + { + var ip1Parts = ip1.Split('.').Select(byte.Parse).ToArray(); + var ip2Parts = ip2.Split('.').Select(byte.Parse).ToArray(); + var maskParts = subnetMask.Split('.').Select(byte.Parse).ToArray(); + + for (int i = 0; i < 4; i++) + { + if ((ip1Parts[i] & maskParts[i]) != (ip2Parts[i] & maskParts[i])) + return false; + } + + return true; + } + } +} \ No newline at end of file diff --git a/KitX Script/Kscript.CSharp/Utils/ResponseHandler.cs b/KitX Script/Kscript.CSharp/Utils/ResponseHandler.cs new file mode 100644 index 0000000..bba69b5 --- /dev/null +++ b/KitX Script/Kscript.CSharp/Utils/ResponseHandler.cs @@ -0,0 +1,124 @@ +using System.Text.Json; +using KitX.Shared.CSharp.WebCommand; + +namespace Kscript.CSharp.Utils +{ + /// + /// 统一处理网络响应的工具类 + /// + public static class ResponseHandler + { + /// + /// 处理网络响应并返回指定类型的结果 + /// + /// 响应结果的类型 + /// 网络响应 + /// 处理响应内容的函数 + /// 处理命令响应的函数 + /// 处理后的结果 + public static async Task HandleResponse( + Request response, + Func contentHandler, + Func commandHandler) + { + return await Task.Run(() => + { + try + { + T? result = default; + response.Match( + response.GetContent(content => + { + result = contentHandler(content); + return content; + }), + matchCommand: command => + { + result = commandHandler(command); + } + ); + return result; + } + catch (Exception ex) + { + throw new InvalidOperationException( + $"Failed to handle response: {ex.Message}", ex); + } + }); + } + + /// + /// 处理JSON响应并反序列化为指定类型 + /// + public static async Task HandleJsonResponse( + Request response, + JsonSerializerOptions? options = null) + { + return await HandleResponse( + response, + content => string.IsNullOrEmpty(content) ? default : + JsonSerializer.Deserialize(content, options), + command => + { + if (command.StartsWith("Error:")) + { + var error = command.Substring("Error:".Length); + throw new InvalidOperationException($"Command error: {error}"); + } + return default; + } + ); + } + + /// + /// 处理带前缀的命令响应 + /// + public static async Task HandlePrefixedResponse( + Request response, + string prefix, + JsonSerializerOptions? options = null) + { + return await HandleResponse( + response, + content => string.IsNullOrEmpty(content) ? default : + JsonSerializer.Deserialize(content, options), + command => + { + if (command.StartsWith("Error:")) + { + var error = command.Substring("Error:".Length); + throw new InvalidOperationException($"Command error: {error}"); + } + else if (command.StartsWith(prefix)) + { + var json = command.Substring(prefix.Length); + return JsonSerializer.Deserialize(json, options); + } + return default; + } + ); + } + + /// + /// 处理简单的字符串响应 + /// + public static async Task HandleStringResponse( + Request response, + bool throwOnError = true) + { + return await HandleResponse( + response, + content => content ?? string.Empty, + command => + { + if (throwOnError && command.StartsWith("Error:")) + { + var error = command.Substring("Error:".Length); + throw new InvalidOperationException($"Command error: {error}"); + } + return command; + } + ); + } + } +} \ No newline at end of file From 090432d8f1b2de1ff1e9bc31a41a826d5b4b8ab9 Mon Sep 17 00:00:00 2001 From: StarInk Date: Tue, 29 Jul 2025 14:38:47 +0800 Subject: [PATCH 08/60] =?UTF-8?q?=F0=9F=92=BE=20Feat(Kscript.CSharp.Parser?= =?UTF-8?q?):=20=E5=88=9B=E5=BB=BA=E5=B9=B6=E5=88=9D=E6=AD=A5=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=E4=BB=8E=E6=8F=92=E4=BB=B6=E4=BF=A1=E6=81=AF=E5=88=97?= =?UTF-8?q?=E8=A1=A8Json=E6=96=87=E4=BB=B6=E7=94=9F=E6=88=90=E6=8F=92?= =?UTF-8?q?=E4=BB=B6=E9=9D=99=E6=80=81API=EF=BC=8C=E4=BD=BFC#=E8=84=9A?= =?UTF-8?q?=E6=9C=AC=E8=83=BD=E6=97=A0=E6=84=9F=E8=B0=83=E7=94=A8=E6=8F=92?= =?UTF-8?q?=E4=BB=B6=E5=8F=8A=E5=85=B6=E5=8A=9F=E8=83=BD=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CodeGen/AssemblyCache.cs | 242 +++++++++++++++ .../CodeGen/MethodEmitter.cs | 267 ++++++++++++++++ .../CodeGen/TypeMapper.cs | 212 +++++++++++++ .../Core/IPluginManager.cs | 38 +++ .../Core/MockPluginManager.cs | 140 +++++++++ .../Examples/BasicUsageExample.cs | 235 ++++++++++++++ .../Kscript.CSharp.Parser/Examples/Program.cs | 39 +++ .../Exceptions/ParserException.cs | 43 +++ .../Kscript.CSharp.Parser/GlobalUsings.cs | 6 + .../Kscript.CSharp.Parser.csproj | 21 ++ .../Kscript.CSharp.Parser.sln | 24 ++ .../Kscript.CSharp.Parser/LLMDocs/InitPlan.md | 76 +++++ .../Models/PluginCallInfo.cs | 44 +++ KitX Script/Kscript.CSharp.Parser/Parser.cs | 271 ++++++++++++++++ KitX Script/Kscript.CSharp.Parser/README.md | 202 ++++++++++++ KitX Script/Kscript.CSharp.Parser/USAGE.md | 292 ++++++++++++++++++ .../Kscript.CSharp.Parser/example.json | 154 +++++++++ KitX Script/Kscript.CSharp/Kscript.CSharp.sln | 24 ++ 18 files changed, 2330 insertions(+) create mode 100644 KitX Script/Kscript.CSharp.Parser/CodeGen/AssemblyCache.cs create mode 100644 KitX Script/Kscript.CSharp.Parser/CodeGen/MethodEmitter.cs create mode 100644 KitX Script/Kscript.CSharp.Parser/CodeGen/TypeMapper.cs create mode 100644 KitX Script/Kscript.CSharp.Parser/Core/IPluginManager.cs create mode 100644 KitX Script/Kscript.CSharp.Parser/Core/MockPluginManager.cs create mode 100644 KitX Script/Kscript.CSharp.Parser/Examples/BasicUsageExample.cs create mode 100644 KitX Script/Kscript.CSharp.Parser/Examples/Program.cs create mode 100644 KitX Script/Kscript.CSharp.Parser/Exceptions/ParserException.cs create mode 100644 KitX Script/Kscript.CSharp.Parser/GlobalUsings.cs create mode 100644 KitX Script/Kscript.CSharp.Parser/Kscript.CSharp.Parser.csproj create mode 100644 KitX Script/Kscript.CSharp.Parser/Kscript.CSharp.Parser.sln create mode 100644 KitX Script/Kscript.CSharp.Parser/LLMDocs/InitPlan.md create mode 100644 KitX Script/Kscript.CSharp.Parser/Models/PluginCallInfo.cs create mode 100644 KitX Script/Kscript.CSharp.Parser/Parser.cs create mode 100644 KitX Script/Kscript.CSharp.Parser/README.md create mode 100644 KitX Script/Kscript.CSharp.Parser/USAGE.md create mode 100644 KitX Script/Kscript.CSharp.Parser/example.json create mode 100644 KitX Script/Kscript.CSharp/Kscript.CSharp.sln diff --git a/KitX Script/Kscript.CSharp.Parser/CodeGen/AssemblyCache.cs b/KitX Script/Kscript.CSharp.Parser/CodeGen/AssemblyCache.cs new file mode 100644 index 0000000..26e4ce0 --- /dev/null +++ b/KitX Script/Kscript.CSharp.Parser/CodeGen/AssemblyCache.cs @@ -0,0 +1,242 @@ +using System.Collections.Concurrent; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; + +namespace Kscript.CSharp.Parser.CodeGen; + +/// +/// 程序集缓存管理器,避免重复生成相同的程序集 +/// +public static class AssemblyCache +{ + /// + /// 程序集缓存字典,Key 为插件清单的哈希值,Value 为生成的程序集 + /// + private static readonly ConcurrentDictionary _assemblyCache = new(); + + /// + /// 程序集引用计数,用于清理不再使用的程序集 + /// + private static readonly ConcurrentDictionary _referenceCount = new(); + + /// + /// 缓存锁,确保并发安全 + /// + private static readonly object _cacheLock = new(); + + /// + /// 获取或生成程序集 + /// + /// 插件信息列表 + /// 程序集名称 + /// 插件管理器实例 + /// 缓存的或新生成的程序集 + public static Assembly GetOrCreateAssembly(List plugins, string assemblyName = "DynamicPluginAssembly", Core.IPluginManager? pluginManager = null) + { + var cacheKey = GenerateCacheKey(plugins, assemblyName); + + // 尝试从缓存获取 + if (_assemblyCache.TryGetValue(cacheKey, out var cachedAssembly)) + { + IncrementReference(cacheKey); + return cachedAssembly; + } + + lock (_cacheLock) + { + // 双重检查锁定模式 + if (_assemblyCache.TryGetValue(cacheKey, out cachedAssembly)) + { + IncrementReference(cacheKey); + return cachedAssembly; + } + + // 生成新的程序集 + var newAssembly = MethodEmitter.GenerateAssembly(plugins, assemblyName, pluginManager); + + // 添加到缓存 + _assemblyCache[cacheKey] = newAssembly; + _referenceCount[cacheKey] = 1; + + return newAssembly; + } + } + + /// + /// 生成插件清单的缓存键 + /// + /// 插件信息列表 + /// 程序集名称 + /// 缓存键字符串 + private static string GenerateCacheKey(List plugins, string assemblyName) + { + try + { + // 创建用于哈希的数据对象 + var cacheData = new + { + AssemblyName = assemblyName, + Plugins = plugins.Select(p => new + { + p.Name, + p.Version, + Functions = p.Functions.Select(f => new + { + f.Name, + f.ReturnValueType, + Parameters = f.Parameters.Select(param => new + { + param.Name, + param.Type, + param.IsOptional, + param.Value + }).OrderBy(param => param.Name) + }).OrderBy(f => f.Name) + }).OrderBy(p => p.Name) + }; + + // 序列化为JSON + var json = JsonSerializer.Serialize(cacheData, new JsonSerializerOptions + { + WriteIndented = false, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }); + + // 计算SHA256哈希 + using var sha256 = SHA256.Create(); + var hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(json)); + return Convert.ToHexString(hashBytes); + } + catch (Exception ex) + { + // 如果哈希计算失败,使用基础信息生成简单键 + var fallbackKey = $"{assemblyName}_{plugins.Count}_{string.Join("_", plugins.Select(p => $"{p.Name}_{p.Functions.Count}"))}_{DateTime.UtcNow.Ticks}"; + Console.WriteLine($"[AssemblyCache] 哈希计算失败,使用回退键: {ex.Message}"); + return fallbackKey; + } + } + + /// + /// 增加引用计数 + /// + /// 缓存键 + private static void IncrementReference(string cacheKey) + { + _referenceCount.AddOrUpdate(cacheKey, 1, (key, count) => count + 1); + } + + /// + /// 减少引用计数 + /// + /// 缓存键 + public static void DecrementReference(string cacheKey) + { + if (_referenceCount.TryGetValue(cacheKey, out var count)) + { + var newCount = count - 1; + if (newCount <= 0) + { + // 引用计数为0,从缓存中移除 + _assemblyCache.TryRemove(cacheKey, out _); + _referenceCount.TryRemove(cacheKey, out _); + } + else + { + _referenceCount[cacheKey] = newCount; + } + } + } + + /// + /// 清除所有缓存 + /// + public static void ClearCache() + { + lock (_cacheLock) + { + _assemblyCache.Clear(); + _referenceCount.Clear(); + } + } + + /// + /// 获取缓存统计信息 + /// + /// 缓存统计信息 + public static CacheStatistics GetStatistics() + { + return new CacheStatistics + { + CachedAssemblyCount = _assemblyCache.Count, + TotalReferences = _referenceCount.Values.Sum(), + CacheKeys = _assemblyCache.Keys.ToList() + }; + } + + /// + /// 检查是否存在缓存 + /// + /// 插件信息列表 + /// 程序集名称 + /// 是否存在缓存 + public static bool HasCache(List plugins, string assemblyName = "DynamicPluginAssembly") + { + var cacheKey = GenerateCacheKey(plugins, assemblyName); + return _assemblyCache.ContainsKey(cacheKey); + } + + /// + /// 强制重新生成程序集(绕过缓存) + /// + /// 插件信息列表 + /// 程序集名称 + /// 插件管理器实例 + /// 新生成的程序集 + public static Assembly ForceRegenerate(List plugins, string assemblyName = "DynamicPluginAssembly", Core.IPluginManager? pluginManager = null) + { + var cacheKey = GenerateCacheKey(plugins, assemblyName); + + lock (_cacheLock) + { + // 移除现有缓存 + _assemblyCache.TryRemove(cacheKey, out _); + _referenceCount.TryRemove(cacheKey, out _); + + // 生成新的程序集 + var newAssembly = MethodEmitter.GenerateAssembly(plugins, assemblyName, pluginManager); + + // 添加到缓存 + _assemblyCache[cacheKey] = newAssembly; + _referenceCount[cacheKey] = 1; + + return newAssembly; + } + } +} + +/// +/// 缓存统计信息 +/// +public class CacheStatistics +{ + /// + /// 缓存的程序集数量 + /// + public int CachedAssemblyCount { get; set; } + + /// + /// 总引用数量 + /// + public int TotalReferences { get; set; } + + /// + /// 缓存键列表 + /// + public List CacheKeys { get; set; } = new(); + + public override string ToString() + { + return $"缓存统计: {CachedAssemblyCount} 个程序集, {TotalReferences} 个引用"; + } +} diff --git a/KitX Script/Kscript.CSharp.Parser/CodeGen/MethodEmitter.cs b/KitX Script/Kscript.CSharp.Parser/CodeGen/MethodEmitter.cs new file mode 100644 index 0000000..f4921fd --- /dev/null +++ b/KitX Script/Kscript.CSharp.Parser/CodeGen/MethodEmitter.cs @@ -0,0 +1,267 @@ +using Kscript.CSharp.Parser.Core; +using Kscript.CSharp.Parser.Exceptions; +using Kscript.CSharp.Parser.Models; + +namespace Kscript.CSharp.Parser.CodeGen; + +/// +/// IL 方法生成器,负责生成插件调用的静态方法 +/// +public static class MethodEmitter +{ + /// + /// 为插件生成动态程序集 + /// + /// 插件信息列表 + /// 程序集名称 + /// 插件管理器实例 + /// 生成的动态程序集 + public static Assembly GenerateAssembly(List plugins, string assemblyName = "DynamicPluginAssembly", IPluginManager? pluginManager = null) + { + try + { + // 创建动态程序集 + var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly( + new AssemblyName(assemblyName), + AssemblyBuilderAccess.Run); + + var moduleBuilder = assemblyBuilder.DefineDynamicModule($"{assemblyName}.dll"); + + // 为每个插件生成静态类 + foreach (var plugin in plugins) + { + GeneratePluginClass(moduleBuilder, plugin, pluginManager); + } + + return assemblyBuilder; + } + catch (Exception ex) + { + throw ParserException.AssemblyGenerationError(assemblyName, ex); + } + } + + /// + /// 为单个插件生成静态类 + /// + private static void GeneratePluginClass(ModuleBuilder moduleBuilder, PluginInfo plugin, IPluginManager? pluginManager) + { + try + { + // 创建静态类 + var typeBuilder = moduleBuilder.DefineType( + plugin.Name, + TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.Abstract | TypeAttributes.Sealed); + + // 添加插件管理器字段 + var pluginManagerField = typeBuilder.DefineField( + "_pluginManager", + typeof(IPluginManager), + FieldAttributes.Private | FieldAttributes.Static); + + // 生成静态构造函数来初始化插件管理器 + GenerateStaticConstructor(typeBuilder, pluginManagerField, pluginManager); + + // 为每个功能生成静态方法 + foreach (var function in plugin.Functions) + { + GeneratePluginMethod(typeBuilder, plugin.Name, function, pluginManagerField); + } + + // 创建类型 + typeBuilder.CreateType(); + } + catch (Exception ex) + { + throw ParserException.ILGenerationError($"{plugin.Name} 类", ex); + } + } + + /// + /// 生成静态构造函数 + /// + private static void GenerateStaticConstructor(TypeBuilder typeBuilder, FieldBuilder pluginManagerField, IPluginManager? pluginManager) + { + var constructorBuilder = typeBuilder.DefineConstructor( + MethodAttributes.Private | MethodAttributes.Static | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName, + CallingConventions.Standard, + Type.EmptyTypes); + + var il = constructorBuilder.GetILGenerator(); + + if (pluginManager != null) + { + // 如果提供了插件管理器实例,直接使用 + il.Emit(OpCodes.Ldtoken, pluginManager.GetType()); + il.Emit(OpCodes.Call, typeof(Type).GetMethod("GetTypeFromHandle")!); + il.Emit(OpCodes.Call, typeof(Activator).GetMethod("CreateInstance", new[] { typeof(Type) })!); + il.Emit(OpCodes.Castclass, typeof(IPluginManager)); + } + else + { + // 使用默认的 MockPluginManager + il.Emit(OpCodes.Newobj, typeof(MockPluginManager).GetConstructor(Type.EmptyTypes)!); + } + + il.Emit(OpCodes.Stsfld, pluginManagerField); + il.Emit(OpCodes.Ret); + } + + /// + /// 为插件功能生成静态方法 + /// + private static void GeneratePluginMethod(TypeBuilder typeBuilder, string pluginName, Function function, FieldBuilder pluginManagerField) + { + try + { + // 解析参数类型 + var parameterTypes = function.Parameters + .Select(p => TypeMapper.MapType(p.Type)) + .ToArray(); + + // 解析返回值类型 + var returnType = TypeMapper.MapType(function.ReturnValueType); + + // 定义方法 + var methodBuilder = typeBuilder.DefineMethod( + function.Name, + MethodAttributes.Public | MethodAttributes.Static, + returnType, + parameterTypes); + + // 设置参数名称 + for (int i = 0; i < function.Parameters.Count; i++) + { + var param = function.Parameters[i]; + var paramBuilder = methodBuilder.DefineParameter(i + 1, ParameterAttributes.None, param.Name); + + // 如果参数是可选的,设置默认值 + if (param.IsOptional && !string.IsNullOrEmpty(param.Value)) + { + var defaultValue = ConvertDefaultValue(param.Value, parameterTypes[i]); + if (defaultValue != null) + { + paramBuilder.SetConstant(defaultValue); + } + } + } + + // 生成方法体IL代码 + GenerateMethodBody(methodBuilder, pluginName, function, parameterTypes, returnType, pluginManagerField); + } + catch (Exception ex) + { + throw ParserException.ILGenerationError($"{pluginName}.{function.Name}", ex); + } + } + + /// + /// 生成方法体IL代码 + /// + private static void GenerateMethodBody(MethodBuilder methodBuilder, string pluginName, Function function, + Type[] parameterTypes, Type returnType, FieldBuilder pluginManagerField) + { + var il = methodBuilder.GetILGenerator(); + + // 声明局部变量 + var callInfoLocal = il.DeclareLocal(typeof(PluginCallInfo)); + var parametersArrayLocal = il.DeclareLocal(typeof(object[])); + var parameterTypesArrayLocal = il.DeclareLocal(typeof(Type[])); + LocalBuilder? resultLocal = null; + + if (returnType != typeof(void)) + { + resultLocal = il.DeclareLocal(returnType); + } + + // 创建参数数组 + il.Emit(OpCodes.Ldc_I4, parameterTypes.Length); + il.Emit(OpCodes.Newarr, typeof(object)); + il.Emit(OpCodes.Stloc, parametersArrayLocal); + + // 创建参数类型数组 + il.Emit(OpCodes.Ldc_I4, parameterTypes.Length); + il.Emit(OpCodes.Newarr, typeof(Type)); + il.Emit(OpCodes.Stloc, parameterTypesArrayLocal); + + // 填充参数数组和类型数组 + for (int i = 0; i < parameterTypes.Length; i++) + { + // 填充参数值 + il.Emit(OpCodes.Ldloc, parametersArrayLocal); + il.Emit(OpCodes.Ldc_I4, i); + il.Emit(OpCodes.Ldarg, i); + + // 如果是值类型,需要装箱 + if (parameterTypes[i].IsValueType) + { + il.Emit(OpCodes.Box, parameterTypes[i]); + } + + il.Emit(OpCodes.Stelem_Ref); + + // 填充参数类型 + il.Emit(OpCodes.Ldloc, parameterTypesArrayLocal); + il.Emit(OpCodes.Ldc_I4, i); + il.Emit(OpCodes.Ldtoken, parameterTypes[i]); + il.Emit(OpCodes.Call, typeof(Type).GetMethod("GetTypeFromHandle")!); + il.Emit(OpCodes.Stelem_Ref); + } + + // 创建 PluginCallInfo 实例 + il.Emit(OpCodes.Ldstr, pluginName); + il.Emit(OpCodes.Ldstr, function.Name); + il.Emit(OpCodes.Ldloc, parametersArrayLocal); + il.Emit(OpCodes.Ldloc, parameterTypesArrayLocal); + il.Emit(OpCodes.Newobj, typeof(PluginCallInfo).GetConstructor(new[] + { + typeof(string), typeof(string), typeof(object[]), typeof(Type[]) + })!); + il.Emit(OpCodes.Stloc, callInfoLocal); + + // 调用插件管理器 + il.Emit(OpCodes.Ldsfld, pluginManagerField); + il.Emit(OpCodes.Ldloc, callInfoLocal); + + if (returnType == typeof(void)) + { + // 调用无返回值的方法 + var voidCallMethod = typeof(IPluginManager).GetMethods() + .First(m => m.Name == "Call" && !m.IsGenericMethod && m.GetParameters().Length == 1); + il.Emit(OpCodes.Callvirt, voidCallMethod); + } + else + { + // 调用有返回值的泛型方法 + var genericCallMethod = typeof(IPluginManager).GetMethods() + .First(m => m.Name == "Call" && m.IsGenericMethod && m.GetParameters().Length == 1) + .MakeGenericMethod(returnType); + il.Emit(OpCodes.Callvirt, genericCallMethod); + il.Emit(OpCodes.Stloc, resultLocal!); + il.Emit(OpCodes.Ldloc, resultLocal!); + } + + il.Emit(OpCodes.Ret); + } + + /// + /// 转换默认值 + /// + private static object? ConvertDefaultValue(string value, Type targetType) + { + try + { + if (targetType == typeof(string)) + return value; + + if (string.IsNullOrEmpty(value)) + return null; + + return Convert.ChangeType(value, targetType); + } + catch + { + return null; + } + } +} diff --git a/KitX Script/Kscript.CSharp.Parser/CodeGen/TypeMapper.cs b/KitX Script/Kscript.CSharp.Parser/CodeGen/TypeMapper.cs new file mode 100644 index 0000000..061a31f --- /dev/null +++ b/KitX Script/Kscript.CSharp.Parser/CodeGen/TypeMapper.cs @@ -0,0 +1,212 @@ +using System.Collections.Concurrent; +using System.Text.RegularExpressions; + +namespace Kscript.CSharp.Parser.CodeGen; + +/// +/// 类型映射器,将字符串类型名映射到 System.Type +/// +public static class TypeMapper +{ + /// + /// 基础类型映射表 + /// + private static readonly Dictionary _basicTypeMap = new() + { + { "void", typeof(void) }, + { "bool", typeof(bool) }, + { "byte", typeof(byte) }, + { "sbyte", typeof(sbyte) }, + { "short", typeof(short) }, + { "ushort", typeof(ushort) }, + { "int", typeof(int) }, + { "uint", typeof(uint) }, + { "long", typeof(long) }, + { "ulong", typeof(ulong) }, + { "float", typeof(float) }, + { "double", typeof(double) }, + { "decimal", typeof(decimal) }, + { "char", typeof(char) }, + { "string", typeof(string) }, + { "object", typeof(object) } + }; + + /// + /// 自定义类型映射表 + /// + private static readonly ConcurrentDictionary _customTypeMap = new(); + + /// + /// 类型缓存 + /// + private static readonly ConcurrentDictionary _typeCache = new(); + + /// + /// 泛型类型正则表达式 + /// + private static readonly Regex _genericTypeRegex = new(@"^(\w+)<(.+)>$", RegexOptions.Compiled); + + /// + /// 映射字符串类型名到 System.Type + /// + /// 类型名字符串 + /// 对应的 Type 对象,如果找不到则返回 typeof(object) + public static Type MapType(string typeName) + { + if (string.IsNullOrWhiteSpace(typeName)) + return typeof(object); + + // 先检查缓存 + if (_typeCache.TryGetValue(typeName, out var cachedType)) + return cachedType; + + Type? resolvedType = null; + + try + { + // 1. 检查基础类型 + if (_basicTypeMap.TryGetValue(typeName, out resolvedType)) + { + _typeCache[typeName] = resolvedType; + return resolvedType; + } + + // 2. 检查自定义映射 + if (_customTypeMap.TryGetValue(typeName, out resolvedType)) + { + _typeCache[typeName] = resolvedType; + return resolvedType; + } + + // 3. 处理泛型类型 + var genericMatch = _genericTypeRegex.Match(typeName); + if (genericMatch.Success) + { + resolvedType = ResolveGenericType(genericMatch); + if (resolvedType != null) + { + _typeCache[typeName] = resolvedType; + return resolvedType; + } + } + + // 4. 尝试直接解析类型 + resolvedType = Type.GetType(typeName); + if (resolvedType != null) + { + _typeCache[typeName] = resolvedType; + return resolvedType; + } + + // 5. 回退到 object 类型 + resolvedType = typeof(object); + _typeCache[typeName] = resolvedType; + return resolvedType; + } + catch + { + // 发生异常时回退到 object 类型 + resolvedType = typeof(object); + _typeCache[typeName] = resolvedType; + return resolvedType; + } + } + + /// + /// 解析泛型类型 + /// + private static Type? ResolveGenericType(Match genericMatch) + { + var genericTypeName = genericMatch.Groups[1].Value; + var genericArgs = genericMatch.Groups[2].Value; + + // 解析泛型参数 + var argTypes = ParseGenericArguments(genericArgs); + if (argTypes.Length == 0) + return null; + + // 处理常见的泛型类型 + return genericTypeName switch + { + "List" => typeof(List<>).MakeGenericType(argTypes), + "Dictionary" when argTypes.Length == 2 => typeof(Dictionary<,>).MakeGenericType(argTypes), + "Array" => argTypes[0].MakeArrayType(), + "Nullable" => typeof(Nullable<>).MakeGenericType(argTypes), + _ => null + }; + } + + /// + /// 解析泛型参数 + /// + private static Type[] ParseGenericArguments(string argsString) + { + var args = SplitGenericArguments(argsString); + return args.Select(MapType).ToArray(); + } + + /// + /// 分割泛型参数字符串,处理嵌套泛型 + /// + private static string[] SplitGenericArguments(string argsString) + { + var args = new List(); + var current = ""; + var depth = 0; + + foreach (char c in argsString) + { + if (c == '<') + { + depth++; + current += c; + } + else if (c == '>') + { + depth--; + current += c; + } + else if (c == ',' && depth == 0) + { + args.Add(current.Trim()); + current = ""; + } + else + { + current += c; + } + } + + if (!string.IsNullOrEmpty(current)) + args.Add(current.Trim()); + + return args.ToArray(); + } + + /// + /// 注册自定义类型映射 + /// + /// 类型名字符串 + /// 对应的 Type 对象 + public static void RegisterCustomType(string typeName, Type type) + { + _customTypeMap[typeName] = type; + _typeCache.TryRemove(typeName, out _); // 清除缓存 + } + + /// + /// 清除类型缓存 + /// + public static void ClearCache() + { + _typeCache.Clear(); + } + + /// + /// 获取所有已注册的自定义类型 + /// + public static IReadOnlyDictionary GetCustomTypes() + { + return _customTypeMap.AsReadOnly(); + } +} diff --git a/KitX Script/Kscript.CSharp.Parser/Core/IPluginManager.cs b/KitX Script/Kscript.CSharp.Parser/Core/IPluginManager.cs new file mode 100644 index 0000000..e66e9ea --- /dev/null +++ b/KitX Script/Kscript.CSharp.Parser/Core/IPluginManager.cs @@ -0,0 +1,38 @@ +using Kscript.CSharp.Parser.Models; + +namespace Kscript.CSharp.Parser.Core; + +/// +/// 插件管理器接口 - 模拟接口,实际实现由外部项目提供 +/// +public interface IPluginManager +{ + /// + /// 调用插件方法 + /// + /// 返回值类型 + /// 调用信息 + /// 插件方法的返回值 + T Call(PluginCallInfo callInfo); + + /// + /// 调用插件方法(无返回值) + /// + /// 调用信息 + void Call(PluginCallInfo callInfo); + + /// + /// 检查插件是否存在 + /// + /// 插件名称 + /// 插件是否存在 + bool IsPluginExists(string pluginName); + + /// + /// 检查插件方法是否存在 + /// + /// 插件名称 + /// 方法名称 + /// 方法是否存在 + bool IsMethodExists(string pluginName, string methodName); +} diff --git a/KitX Script/Kscript.CSharp.Parser/Core/MockPluginManager.cs b/KitX Script/Kscript.CSharp.Parser/Core/MockPluginManager.cs new file mode 100644 index 0000000..083c602 --- /dev/null +++ b/KitX Script/Kscript.CSharp.Parser/Core/MockPluginManager.cs @@ -0,0 +1,140 @@ +using Kscript.CSharp.Parser.Models; + +namespace Kscript.CSharp.Parser.Core; + +/// +/// 模拟插件管理器 - 用于调试和测试 +/// +public class MockPluginManager : IPluginManager +{ + /// + /// 模拟的插件数据,用于验证调用 + /// + private readonly Dictionary> _mockData = new() + { + { + "SampleCalculator", new Dictionary + { + { "Add", (int a, int b) => a + b }, + { "Divide", (double numerator, double denominator, int decimals = 2) => Math.Round(numerator / denominator, decimals) } + } + }, + { + "StringToolkit", new Dictionary + { + { "Reverse", (string text) => new string(text.Reverse().ToArray()) }, + { "ToUpper", (string text) => text.ToUpperInvariant() } + } + } + }; + + /// + /// 调用插件方法 + /// + public T Call(PluginCallInfo callInfo) + { + Console.WriteLine($"[MockPluginManager] 调用插件方法: {callInfo}"); + Console.WriteLine($"[MockPluginManager] 参数类型: [{string.Join(", ", callInfo.ParameterTypes.Select(t => t.Name))}]"); + Console.WriteLine($"[MockPluginManager] 期望返回类型: {typeof(T).Name}"); + + // 简单的模拟实现 + if (_mockData.TryGetValue(callInfo.PluginName, out var plugin) && + plugin.TryGetValue(callInfo.MethodName, out var method)) + { + try + { + var result = InvokeMockMethod(callInfo, method); + Console.WriteLine($"[MockPluginManager] 返回结果: {result}"); + + if (result is T typedResult) + return typedResult; + + // 尝试类型转换 + if (result != null && typeof(T) != typeof(void)) + { + var convertedResult = Convert.ChangeType(result, typeof(T)); + return (T)convertedResult; + } + } + catch (Exception ex) + { + Console.WriteLine($"[MockPluginManager] 调用异常: {ex.Message}"); + } + } + else + { + Console.WriteLine($"[MockPluginManager] 未找到插件方法: {callInfo.PluginName}.{callInfo.MethodName}"); + } + + // 返回默认值 + if (typeof(T) == typeof(void)) + return default(T)!; + + return (T)GetDefaultValue(typeof(T))!; + } + + /// + /// 调用插件方法(无返回值) + /// + public void Call(PluginCallInfo callInfo) + { + Call(callInfo); + } + + /// + /// 检查插件是否存在 + /// + public bool IsPluginExists(string pluginName) + { + var exists = _mockData.ContainsKey(pluginName); + Console.WriteLine($"[MockPluginManager] 检查插件 '{pluginName}' 是否存在: {exists}"); + return exists; + } + + /// + /// 检查插件方法是否存在 + /// + public bool IsMethodExists(string pluginName, string methodName) + { + var exists = _mockData.TryGetValue(pluginName, out var plugin) && + plugin.ContainsKey(methodName); + Console.WriteLine($"[MockPluginManager] 检查方法 '{pluginName}.{methodName}' 是否存在: {exists}"); + return exists; + } + + /// + /// 调用模拟方法 + /// + private object? InvokeMockMethod(PluginCallInfo callInfo, object method) + { + // 根据插件和方法名进行简单的模拟计算 + return (callInfo.PluginName, callInfo.MethodName) switch + { + ("SampleCalculator", "Add") when callInfo.Parameters.Length >= 2 => + Convert.ToInt32(callInfo.Parameters[0]) + Convert.ToInt32(callInfo.Parameters[1]), + + ("SampleCalculator", "Divide") when callInfo.Parameters.Length >= 2 => + callInfo.Parameters.Length >= 3 + ? Math.Round(Convert.ToDouble(callInfo.Parameters[0]) / Convert.ToDouble(callInfo.Parameters[1]), Convert.ToInt32(callInfo.Parameters[2])) + : Math.Round(Convert.ToDouble(callInfo.Parameters[0]) / Convert.ToDouble(callInfo.Parameters[1]), 2), + + ("StringToolkit", "Reverse") when callInfo.Parameters.Length >= 1 => + new string(callInfo.Parameters[0].ToString()?.Reverse().ToArray() ?? Array.Empty()), + + ("StringToolkit", "ToUpper") when callInfo.Parameters.Length >= 1 => + callInfo.Parameters[0].ToString()?.ToUpperInvariant() ?? string.Empty, + + _ => $"MockResult_{callInfo.MethodName}" + }; + } + + /// + /// 获取类型的默认值 + /// + private static object? GetDefaultValue(Type type) + { + if (type.IsValueType) + return Activator.CreateInstance(type); + return null; + } +} diff --git a/KitX Script/Kscript.CSharp.Parser/Examples/BasicUsageExample.cs b/KitX Script/Kscript.CSharp.Parser/Examples/BasicUsageExample.cs new file mode 100644 index 0000000..f2e6fdc --- /dev/null +++ b/KitX Script/Kscript.CSharp.Parser/Examples/BasicUsageExample.cs @@ -0,0 +1,235 @@ +using System.Text.Json; +using Kscript.CSharp.Parser; +using Kscript.CSharp.Parser.Core; + +namespace Kscript.CSharp.Parser.Examples; + +/// +/// 基础用法示例 +/// +public static class BasicUsageExample +{ + /// + /// 演示基本功能 + /// + public static async Task RunExample() + { + Console.WriteLine("=== KitX.CSharp.Parser 基础用法示例 ===\n"); + + try + { + // 1. 从现有的 example.json 文件加载插件清单 + Console.WriteLine("1. 加载插件清单..."); + var exampleJsonPath = Path.Combine(".", "example.json"); + if (!File.Exists(exampleJsonPath)) + { + Console.WriteLine($" 警告: example.json 文件不存在于 {exampleJsonPath}"); + Console.WriteLine(" 将使用内置示例数据"); + RunWithBuiltInData(); + return; + } + + var assembly = await Parser.GenerateFromFileAsync(exampleJsonPath, "ExamplePluginAssembly"); + Console.WriteLine($" ✓ 成功生成程序集: {assembly.FullName}"); + + // 2. 获取生成的插件类型 + Console.WriteLine("\n2. 分析生成的插件类型..."); + var pluginTypes = Parser.GetPluginTypes(assembly); + Console.WriteLine($" 发现 {pluginTypes.Count} 个插件类:"); + + foreach (var type in pluginTypes) + { + Console.WriteLine($" - {type.Name}"); + var methods = Parser.GetPluginMethods(type); + foreach (var method in methods) + { + var parameters = string.Join(", ", method.GetParameters().Select(p => $"{p.ParameterType.Name} {p.Name}")); + Console.WriteLine($" └── {method.ReturnType.Name} {method.Name}({parameters})"); + } + } + + // 3. 动态调用插件方法 + Console.WriteLine("\n3. 动态调用插件方法..."); + DynamicInvokeExample(assembly); + + // 4. 缓存统计 + Console.WriteLine("\n4. 缓存统计信息..."); + var stats = Parser.GetCacheStatistics(); + Console.WriteLine($" {stats}"); + + // 5. 演示缓存效果 + Console.WriteLine("\n5. 测试缓存效果..."); + var assembly2 = await Parser.GenerateFromFileAsync(exampleJsonPath, "ExamplePluginAssembly"); + Console.WriteLine($" 第二次生成是否使用缓存: {assembly == assembly2}"); + + } + catch (Exception ex) + { + Console.WriteLine($"❌ 示例执行失败: {ex.Message}"); + Console.WriteLine($"详细信息: {ex}"); + } + } + + /// + /// 使用内置数据运行示例 + /// + private static void RunWithBuiltInData() + { + // 创建示例插件数据 + var plugins = new List + { + new PluginInfo + { + Name = "TestCalculator", + Version = "1.0.0", + Functions = new List + { + new Function + { + Name = "Add", + ReturnValueType = "int", + Parameters = new List + { + new Parameter { Name = "a", Type = "int", IsOptional = false }, + new Parameter { Name = "b", Type = "int", IsOptional = false } + } + }, + new Function + { + Name = "Multiply", + ReturnValueType = "double", + Parameters = new List + { + new Parameter { Name = "x", Type = "double", IsOptional = false }, + new Parameter { Name = "y", Type = "double", IsOptional = false } + } + } + } + } + }; + + Console.WriteLine(" 使用内置示例数据..."); + var assembly = Parser.Generate(plugins, "BuiltInExampleAssembly"); + Console.WriteLine($" ✓ 成功生成程序集: {assembly.FullName}"); + + DynamicInvokeExample(assembly); + } + + /// + /// 动态调用示例 + /// + private static void DynamicInvokeExample(Assembly assembly) + { + var pluginTypes = Parser.GetPluginTypes(assembly); + + foreach (var type in pluginTypes) + { + Console.WriteLine($"\n 调用插件: {type.Name}"); + var methods = Parser.GetPluginMethods(type); + + foreach (var method in methods) + { + try + { + object? result = null; + var parameters = method.GetParameters(); + + // 根据方法名和参数生成测试数据 + object[] args = GenerateTestArguments(method.Name, parameters); + + Console.WriteLine($" 调用 {method.Name}({string.Join(", ", args)})"); + + // 动态调用方法 + result = method.Invoke(null, args); + + Console.WriteLine($" → 结果: {result}"); + } + catch (Exception ex) + { + Console.WriteLine($" → 调用失败: {ex.Message}"); + } + } + } + } + + /// + /// 生成测试参数 + /// + private static object[] GenerateTestArguments(string methodName, ParameterInfo[] parameters) + { + var args = new object[parameters.Length]; + + for (int i = 0; i < parameters.Length; i++) + { + var param = parameters[i]; + + // 根据参数类型和方法名生成合适的测试值 + args[i] = param.ParameterType.Name switch + { + "Int32" => methodName switch + { + "Add" => i == 0 ? 10 : 20, + "Divide" when param.Name == "decimals" => 2, + _ => i + 1 + }, + "Double" => methodName switch + { + "Divide" => i == 0 ? 10.0 : 3.0, + "Multiply" => i == 0 ? 2.5 : 4.0, + _ => (i + 1) * 1.5 + }, + "String" => methodName switch + { + "Reverse" => "Hello", + "ToUpper" => "world", + _ => $"test{i}" + }, + _ => GetDefaultValue(param.ParameterType) + }; + } + + return args; + } + + /// + /// 获取类型默认值 + /// + private static object GetDefaultValue(Type type) + { + if (type.IsValueType) + return Activator.CreateInstance(type)!; + + return type == typeof(string) ? "default" : null!; + } + + /// + /// 演示高级功能 + /// + public static void RunAdvancedExample() + { + Console.WriteLine("\n=== 高级功能演示 ===\n"); + + try + { + // 1. 注册自定义类型 + Console.WriteLine("1. 注册自定义类型映射..."); + Parser.RegisterCustomType("CustomType", typeof(DateTime)); + Console.WriteLine(" ✓ 已注册 CustomType -> DateTime"); + + // 2. 清除缓存 + Console.WriteLine("\n2. 清除所有缓存..."); + Parser.ClearCache(); + Console.WriteLine(" ✓ 缓存已清除"); + + // 3. 设置自定义插件管理器 + Console.WriteLine("\n3. 设置自定义插件管理器..."); + Parser.SetDefaultPluginManager(new MockPluginManager()); + Console.WriteLine(" ✓ 已设置默认插件管理器"); + + } + catch (Exception ex) + { + Console.WriteLine($"❌ 高级功能演示失败: {ex.Message}"); + } + } +} diff --git a/KitX Script/Kscript.CSharp.Parser/Examples/Program.cs b/KitX Script/Kscript.CSharp.Parser/Examples/Program.cs new file mode 100644 index 0000000..5598f27 --- /dev/null +++ b/KitX Script/Kscript.CSharp.Parser/Examples/Program.cs @@ -0,0 +1,39 @@ +using Kscript.CSharp.Parser.Examples; + +namespace Kscript.CSharp.Parser.Examples; + +/// +/// 演示程序入口 +/// +public static class Program +{ + /// + /// 主入口方法 + /// + public static async Task Main(string[] args) + { + Console.OutputEncoding = System.Text.Encoding.UTF8; + Console.WriteLine("🚀 欢迎使用 KitX.CSharp.Parser 演示程序!"); + Console.WriteLine("==================================================\n"); + + try + { + // 运行基础用法示例 + await BasicUsageExample.RunExample(); + + // 运行高级功能示例 + BasicUsageExample.RunAdvancedExample(); + + Console.WriteLine("\n=================================================="); + Console.WriteLine("✅ 所有示例运行完成!"); + } + catch (Exception ex) + { + Console.WriteLine($"\n❌ 程序运行失败: {ex.Message}"); + Console.WriteLine($"详细错误信息:\n{ex}"); + } + + Console.WriteLine("\n按任意键退出..."); + Console.ReadKey(); + } +} diff --git a/KitX Script/Kscript.CSharp.Parser/Exceptions/ParserException.cs b/KitX Script/Kscript.CSharp.Parser/Exceptions/ParserException.cs new file mode 100644 index 0000000..5ba8106 --- /dev/null +++ b/KitX Script/Kscript.CSharp.Parser/Exceptions/ParserException.cs @@ -0,0 +1,43 @@ +namespace Kscript.CSharp.Parser.Exceptions; + +/// +/// 解析器异常 +/// +public class ParserException : Exception +{ + public ParserException() : base() + { + } + + public ParserException(string message) : base(message) + { + } + + public ParserException(string message, Exception innerException) : base(message, innerException) + { + } + + /// + /// 创建类型映射异常 + /// + public static ParserException TypeMappingError(string typeName, Exception? innerException = null) + { + return new ParserException($"无法映射类型: {typeName}", innerException!); + } + + /// + /// 创建IL生成异常 + /// + public static ParserException ILGenerationError(string methodName, Exception? innerException = null) + { + return new ParserException($"生成IL代码失败: {methodName}", innerException!); + } + + /// + /// 创建程序集生成异常 + /// + public static ParserException AssemblyGenerationError(string assemblyName, Exception? innerException = null) + { + return new ParserException($"生成程序集失败: {assemblyName}", innerException!); + } +} diff --git a/KitX Script/Kscript.CSharp.Parser/GlobalUsings.cs b/KitX Script/Kscript.CSharp.Parser/GlobalUsings.cs new file mode 100644 index 0000000..3708735 --- /dev/null +++ b/KitX Script/Kscript.CSharp.Parser/GlobalUsings.cs @@ -0,0 +1,6 @@ +global using System; +global using System.Collections.Generic; +global using System.Linq; +global using System.Reflection; +global using System.Reflection.Emit; +global using KitX.Shared.CSharp.Plugin; diff --git a/KitX Script/Kscript.CSharp.Parser/Kscript.CSharp.Parser.csproj b/KitX Script/Kscript.CSharp.Parser/Kscript.CSharp.Parser.csproj new file mode 100644 index 0000000..e873ef7 --- /dev/null +++ b/KitX Script/Kscript.CSharp.Parser/Kscript.CSharp.Parser.csproj @@ -0,0 +1,21 @@ + + + + net8.0 + 12 + enable + enable + Kscript.CSharp.Parser + Exe + Kscript.CSharp.Parser.Examples.Program + + + + + + + + + + + diff --git a/KitX Script/Kscript.CSharp.Parser/Kscript.CSharp.Parser.sln b/KitX Script/Kscript.CSharp.Parser/Kscript.CSharp.Parser.sln new file mode 100644 index 0000000..2b571ef --- /dev/null +++ b/KitX Script/Kscript.CSharp.Parser/Kscript.CSharp.Parser.sln @@ -0,0 +1,24 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.2.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kscript.CSharp.Parser", "Kscript.CSharp.Parser.csproj", "{17B35B70-0BC3-479B-83D6-ED1513D6C4A6}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {17B35B70-0BC3-479B-83D6-ED1513D6C4A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {17B35B70-0BC3-479B-83D6-ED1513D6C4A6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {17B35B70-0BC3-479B-83D6-ED1513D6C4A6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {17B35B70-0BC3-479B-83D6-ED1513D6C4A6}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {C88E6290-4CEE-41DA-B062-81147E9E898E} + EndGlobalSection +EndGlobal diff --git a/KitX Script/Kscript.CSharp.Parser/LLMDocs/InitPlan.md b/KitX Script/Kscript.CSharp.Parser/LLMDocs/InitPlan.md new file mode 100644 index 0000000..b3ca63c --- /dev/null +++ b/KitX Script/Kscript.CSharp.Parser/LLMDocs/InitPlan.md @@ -0,0 +1,76 @@ +现在让我为你详细规划 KitX.CSharp.Parser 项目的架构和实现方案: + +## 🏗️ 项目架构概览 + +```mermaid +graph TD + A[JSON 插件清单] --> B[Parser.Generate] + B --> C[PluginInfo 模型解析] + C --> D[TypeMapper 类型映射] + D --> E[MethodEmitter IL生成] + E --> F[AssemblyCache 缓存管理] + F --> G[动态程序集] + G --> H[脚本调用: Plugin.Method] + + subgraph "核心组件" + I[Parser - 主入口] + J[TypeMapper - 类型映射] + K[MethodEmitter - IL生成] + L[AssemblyCache - 缓存] + M[PluginManager - 模拟调用] + end +``` + +## 📁 详细目录结构 + +``` +KitX.CSharp.Parser/ +├─ src/ +│ ├─ Parser.cs # 主入口静态类 +│ ├─ Models/ +│ │ ├─ PluginInfo.cs # 插件信息模型 +│ │ ├─ FunctionInfo.cs # 功能信息模型 +│ │ ├─ ParameterInfo.cs # 参数信息模型 +│ │ └─ PluginCallInfo.cs # 插件调用信息 +│ ├─ CodeGen/ +│ │ ├─ TypeMapper.cs # 字符串到Type的映射 +│ │ ├─ MethodEmitter.cs # IL方法生成器 +│ │ └─ AssemblyCache.cs # 程序集缓存管理 +│ ├─ Core/ +│ │ ├─ IPluginManager.cs # 插件管理器接口 +│ │ └─ MockPluginManager.cs # 模拟实现 +│ └─ Exceptions/ +│ └─ ParserException.cs # 自定义异常 +├─ examples/ +│ └─ ConsoleDemo/ # 演示程序 +├─ KitX.CSharp.Parser.csproj # 项目文件 +└─ GlobalUsings.cs # 全局using声明 +``` + +## 🔧 核心技术要点 + +1. **类型映射策略**: + - 基础类型:`int` → `System.Int32` + - 泛型类型:`List` → `List` + - 自定义类型回退机制 + +2. **IL 生成模式**: + ```csharp + // 生成的方法体伪代码 + public static T MethodName(params...) + { + var callInfo = new PluginCallInfo(pluginName, methodName, parameters); + return PluginManager.Call(callInfo); + } + ``` + +3. **缓存机制**: + - 按插件清单 Hash 缓存程序集 + - 支持热更新时的缓存失效 + +## 🎯 关键实现亮点 + +- **类型安全**:编译时类型检查,运行时类型转换 +- **性能优化**:程序集缓存避免重复生成 +- **扩展性**:支持自定义类型映射注册 +- **调试友好**:可选的磁盘程序集输出 diff --git a/KitX Script/Kscript.CSharp.Parser/Models/PluginCallInfo.cs b/KitX Script/Kscript.CSharp.Parser/Models/PluginCallInfo.cs new file mode 100644 index 0000000..3cc0cc6 --- /dev/null +++ b/KitX Script/Kscript.CSharp.Parser/Models/PluginCallInfo.cs @@ -0,0 +1,44 @@ +namespace Kscript.CSharp.Parser.Models; + +/// +/// 插件调用信息,用于传递给 PluginManager.Call 的参数(模拟,可能根据外部实现发生更改) +/// +public class PluginCallInfo +{ + /// + /// 插件名称 + /// + public string PluginName { get; set; } = string.Empty; + + /// + /// 方法名称 + /// + public string MethodName { get; set; } = string.Empty; + + /// + /// 方法参数数组 + /// + public object[] Parameters { get; set; } = Array.Empty(); + + /// + /// 参数类型数组 + /// + public Type[] ParameterTypes { get; set; } = Array.Empty(); + + public PluginCallInfo() + { + } + + public PluginCallInfo(string pluginName, string methodName, object[] parameters, Type[] parameterTypes) + { + PluginName = pluginName; + MethodName = methodName; + Parameters = parameters; + ParameterTypes = parameterTypes; + } + + public override string ToString() + { + return $"{PluginName}.{MethodName}({string.Join(", ", Parameters)})"; + } +} diff --git a/KitX Script/Kscript.CSharp.Parser/Parser.cs b/KitX Script/Kscript.CSharp.Parser/Parser.cs new file mode 100644 index 0000000..4d88ce5 --- /dev/null +++ b/KitX Script/Kscript.CSharp.Parser/Parser.cs @@ -0,0 +1,271 @@ +using System.Text.Json; +using Kscript.CSharp.Parser.CodeGen; +using Kscript.CSharp.Parser.Core; +using Kscript.CSharp.Parser.Exceptions; + +namespace Kscript.CSharp.Parser; + +/// +/// KitX.CSharp.Parser 主入口类 +/// 将插件清单 JSON 即时编译成可直接调用的 C# 静态 API +/// +public static class Parser +{ + /// + /// 默认插件管理器 + /// + private static IPluginManager? _defaultPluginManager; + + /// + /// 设置默认插件管理器 + /// + /// 插件管理器实例 + public static void SetDefaultPluginManager(IPluginManager pluginManager) + { + _defaultPluginManager = pluginManager; + } + + /// + /// 从插件信息列表生成动态程序集 + /// + /// 插件信息列表 + /// 程序集名称 + /// 插件管理器实例(可选,默认使用 MockPluginManager) + /// 是否使用缓存 + /// 生成的动态程序集 + public static Assembly Generate(List plugins, string assemblyName = "DynamicPluginAssembly", + IPluginManager? pluginManager = null, bool useCache = true) + { + if (plugins == null || plugins.Count == 0) + { + throw new ArgumentException("插件列表不能为空", nameof(plugins)); + } + + try + { + var manager = pluginManager ?? _defaultPluginManager ?? new MockPluginManager(); + + if (useCache) + { + return AssemblyCache.GetOrCreateAssembly(plugins, assemblyName, manager); + } + else + { + return MethodEmitter.GenerateAssembly(plugins, assemblyName, manager); + } + } + catch (Exception ex) when (!(ex is ParserException)) + { + throw ParserException.AssemblyGenerationError(assemblyName, ex); + } + } + + /// + /// 从 JSON 字符串生成动态程序集 + /// + /// 插件清单 JSON 字符串 + /// 程序集名称 + /// 插件管理器实例 + /// 是否使用缓存 + /// 生成的动态程序集 + public static Assembly GenerateFromJson(string jsonString, string assemblyName = "DynamicPluginAssembly", + IPluginManager? pluginManager = null, bool useCache = true) + { + if (string.IsNullOrWhiteSpace(jsonString)) + { + throw new ArgumentException("JSON 字符串不能为空", nameof(jsonString)); + } + + try + { + var plugins = JsonSerializer.Deserialize>(jsonString, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + AllowTrailingCommas = true + }); + + if (plugins == null) + { + throw new ParserException("JSON 反序列化失败,结果为 null"); + } + + return Generate(plugins, assemblyName, pluginManager, useCache); + } + catch (JsonException ex) + { + throw new ParserException($"JSON 格式错误: {ex.Message}", ex); + } + } + + /// + /// 从 JSON 文件生成动态程序集 + /// + /// JSON 文件路径 + /// 程序集名称 + /// 插件管理器实例 + /// 是否使用缓存 + /// 生成的动态程序集 + public static async Task GenerateFromFileAsync(string jsonFilePath, string assemblyName = "DynamicPluginAssembly", + IPluginManager? pluginManager = null, bool useCache = true) + { + if (string.IsNullOrWhiteSpace(jsonFilePath)) + { + throw new ArgumentException("文件路径不能为空", nameof(jsonFilePath)); + } + + if (!File.Exists(jsonFilePath)) + { + throw new FileNotFoundException($"文件不存在: {jsonFilePath}"); + } + + try + { + var jsonString = await File.ReadAllTextAsync(jsonFilePath); + return GenerateFromJson(jsonString, assemblyName, pluginManager, useCache); + } + catch (Exception ex) when (!(ex is ParserException)) + { + throw new ParserException($"读取文件失败: {jsonFilePath}", ex); + } + } + + /// + /// 强制重新生成程序集(绕过缓存) + /// + /// 插件信息列表 + /// 程序集名称 + /// 插件管理器实例 + /// 新生成的动态程序集 + public static Assembly Regenerate(List plugins, string assemblyName = "DynamicPluginAssembly", + IPluginManager? pluginManager = null) + { + if (plugins == null || plugins.Count == 0) + { + throw new ArgumentException("插件列表不能为空", nameof(plugins)); + } + + try + { + var manager = pluginManager ?? _defaultPluginManager ?? new MockPluginManager(); + return AssemblyCache.ForceRegenerate(plugins, assemblyName, manager); + } + catch (Exception ex) when (!(ex is ParserException)) + { + throw ParserException.AssemblyGenerationError(assemblyName, ex); + } + } + + /// + /// 注册自定义类型映射 + /// + /// 类型名字符串 + /// 对应的 Type 对象 + public static void RegisterCustomType(string typeName, Type type) + { + TypeMapper.RegisterCustomType(typeName, type); + } + + /// + /// 清除所有缓存 + /// + public static void ClearCache() + { + AssemblyCache.ClearCache(); + TypeMapper.ClearCache(); + } + + /// + /// 获取缓存统计信息 + /// + /// 缓存统计信息 + public static CacheStatistics GetCacheStatistics() + { + return AssemblyCache.GetStatistics(); + } + + /// + /// 检查插件清单是否已有缓存 + /// + /// 插件信息列表 + /// 程序集名称 + /// 是否存在缓存 + public static bool HasCache(List plugins, string assemblyName = "DynamicPluginAssembly") + { + return AssemblyCache.HasCache(plugins, assemblyName); + } + + /// + /// 将程序集保存到磁盘(用于调试) + /// + /// 要保存的程序集 + /// 保存路径 + /// 是否覆盖现有文件 + public static void SaveAssemblyToFile(Assembly assembly, string filePath, bool overwrite = false) + { + if (assembly == null) + throw new ArgumentNullException(nameof(assembly)); + + if (string.IsNullOrWhiteSpace(filePath)) + throw new ArgumentException("文件路径不能为空", nameof(filePath)); + + if (File.Exists(filePath) && !overwrite) + throw new InvalidOperationException($"文件已存在且不允许覆盖: {filePath}"); + + try + { + // 注意:在 .NET 8.0 中,动态程序集默认是不可保存的 + // 这个方法主要用于调试目的,实际保存功能需要特殊处理 + Console.WriteLine($"[Parser] 注意: .NET 8.0 中动态程序集无法直接保存到磁盘"); + Console.WriteLine($"[Parser] 程序集信息: {assembly.FullName}"); + Console.WriteLine($"[Parser] 包含类型: {string.Join(", ", assembly.GetTypes().Select(t => t.Name))}"); + } + catch (Exception ex) + { + throw new ParserException($"保存程序集失败: {filePath}", ex); + } + } + + /// + /// 获取程序集中的所有插件类型 + /// + /// 程序集 + /// 插件类型列表 + public static List GetPluginTypes(Assembly assembly) + { + if (assembly == null) + throw new ArgumentNullException(nameof(assembly)); + + try + { + return assembly.GetTypes() + .Where(t => t.IsClass && t.IsAbstract && t.IsSealed) // 静态类 + .ToList(); + } + catch (Exception ex) + { + throw new ParserException("获取插件类型失败", ex); + } + } + + /// + /// 获取插件类型的所有方法信息 + /// + /// 插件类型 + /// 方法信息列表 + public static List GetPluginMethods(Type pluginType) + { + if (pluginType == null) + throw new ArgumentNullException(nameof(pluginType)); + + try + { + return pluginType.GetMethods(BindingFlags.Public | BindingFlags.Static) + .Where(m => !m.IsSpecialName) // 排除特殊方法如属性访问器 + .ToList(); + } + catch (Exception ex) + { + throw new ParserException($"获取插件方法失败: {pluginType.Name}", ex); + } + } +} diff --git a/KitX Script/Kscript.CSharp.Parser/README.md b/KitX Script/Kscript.CSharp.Parser/README.md new file mode 100644 index 0000000..2530fdd --- /dev/null +++ b/KitX Script/Kscript.CSharp.Parser/README.md @@ -0,0 +1,202 @@ +# KitX.CSharp.Parser +> 将插件清单 JSON 即时编译成可直接调用的 C# 静态 API + +--- + +## 🌱 项目定位 +KitX.CSharp.Parser 是 **KitX 工作流子系统** 的第一环。 +它只做一件事: +**把插件清单(JSON)→ 运行时静态类/方法**, +让后续脚本引擎(Roslyn、CSharpScript、Eval-etc.)可以像调用普通静态方法一样使用插件功能。 + +``` + JSON 清单 ─→ Parser ─→ 内存程序集(DynamicAssembly) + │ + └─ 脚本侧: SamplePlugin.Run(…) +``` + +--- + +## 🎯 边界与职责 +| 范围 | 属于本库 | 不属于本库 | +|---|---|---| +| 读取 JSON | ✅ 反序列化为 `PluginInfo` | ❌ 网络/磁盘 IO | +| 类型映射 | ✅ `"int"` → `System.Int32` | ❌ 复杂自定义类型序列化 | +| 生成 IL | ✅ 用 `Reflection.Emit` 生成静态类/方法 | ❌ 脚本执行、调试 | +| 生命周期 | ✅ 生成后缓存 `Assembly` | ❌ 热重载(由上层调用 `Regenerate()`) | +| 错误处理 | ✅ 语法/类型错误抛出 `ParserException` | ❌ 插件运行时异常 | + +--- + +## 🧩 整体流程 +1. **输入** + `List`(已由外部从 JSON 反序列化好)。 + +2. **映射** + 把 `Function.ReturnValueType` / `Parameter.Type` 字符串映射到 `System.Type`。 + +3. **构建** + - 每个插件 → 一个 `static class`(`TypeBuilder`) + - 每个功能 → 一个 `public static` 方法(`MethodBuilder`) + +4. **Emit** + 方法体内构造 `PluginCallInfo`,调用 `PluginManager.Call()`, + 返回值拆箱后 `ret`。 + +5. **输出** + 程序集保存到内存(或可选磁盘 DLL), + 脚本侧通过 `PluginNamespace.PluginName.Func(...)` 直接调用。 + +--- + +## 🗂️ 目录结构(初始) +``` +KitX.CSharp.Parser/ + ├─ src/ + │ ├─ Parser.cs // 对外静态入口 + │ ├─ CodeGen/ // IL 生成实现 + │ │ ├─ TypeMapper.cs // 字符串 → Type + │ │ ├─ MethodEmitter.cs // IL 指令 + │ │ └─ AssemblyCache.cs // 已生成 Assembly 缓存 + │ └─ Models/ + │ └─ PluginInfo.cs // 复用现有结构 + ├─ tests/ + │ └─ Parser.Tests/ + └─ README.md +``` + +--- + +## 🚀 快速开始 + +### 基本用法 + +```csharp +using Kscript.CSharp.Parser; + +// 1. 从 JSON 文件生成程序集 +var assembly = await Parser.GenerateFromFileAsync("example.json"); + +// 2. 或从插件信息列表生成 +var plugins = new List { /* ... */ }; +var assembly = Parser.Generate(plugins); + +// 3. 脚本中直接调用生成的方法 +int sum = SampleCalculator.Add(10, 20); +string reversed = StringToolkit.Reverse("Hello"); +``` + +### 运行演示 + +```bash +# 构建项目 +dotnet build + +# 运行演示程序 +dotnet run +``` + +--- + +## 📁 项目结构 + +``` +KitX.CSharp.Parser/ +├─ src/ +│ ├─ Parser.cs # 主入口静态类 +│ ├─ Models/ +│ │ └─ PluginCallInfo.cs # 插件调用信息模型 +│ ├─ CodeGen/ +│ │ ├─ TypeMapper.cs # 字符串 → Type 映射 +│ │ ├─ MethodEmitter.cs # IL 方法生成器 +│ │ └─ AssemblyCache.cs # 程序集缓存管理 +│ ├─ Core/ +│ │ ├─ IPluginManager.cs # 插件管理器接口 +│ │ └─ MockPluginManager.cs # 模拟实现 +│ └─ Exceptions/ +│ └─ ParserException.cs # 自定义异常 +├─ Examples/ +│ ├─ BasicUsageExample.cs # 基础用法示例 +│ └─ Program.cs # 演示程序入口 +├─ example.json # 插件清单示例 +├─ README.md # 项目说明 +└─ USAGE.md # 详细使用指南 +``` + +--- + +## ⚙️ 主要特性 + +- ✅ **动态 IL 生成** - 使用 `Reflection.Emit` 生成高性能静态方法 +- ✅ **智能类型映射** - 支持基础类型、泛型类型和自定义类型 +- ✅ **程序集缓存** - 避免重复生成,提升性能 +- ✅ **扩展性设计** - 支持自定义插件管理器和类型映射 +- ✅ **异常处理** - 完善的错误处理机制 +- ✅ **调试友好** - 详细的日志输出和统计信息 + +--- + +## 📐 类型映射支持 + +| JSON 字符串 | CLR 类型 | 示例 | +|---|---|---| +| `"int"` | `System.Int32` | 整数参数 | +| `"double"` | `System.Double` | 浮点数参数 | +| `"string"` | `System.String` | 字符串参数 | +| `"bool"` | `System.Boolean` | 布尔参数 | +| `"void"` | `System.Void` | 无返回值 | +| `"List"` | `List` | 泛型集合 | +| `"Dictionary"` | `Dictionary` | 字典类型 | +| 自定义 | 通过 `RegisterCustomType()` 注册 | 扩展类型 | + +--- + +## 🎯 运行结果示例 + +``` +🚀 欢迎使用 KitX.CSharp.Parser 演示程序! + +=== KitX.CSharp.Parser 基础用法示例 === + +1. 加载插件清单... + ✓ 成功生成程序集: ExamplePluginAssembly + +2. 分析生成的插件类型... + 发现 2 个插件类: + - SampleCalculator + └── Int32 Add(Int32 a, Int32 b) + └── Double Divide(Double numerator, Double denominator, Int32 decimals) + - StringToolkit + └── String Reverse(String text) + └── String ToUpper(String text) + +3. 动态调用插件方法... + 调用 Add(10, 20) → 结果: 30 + 调用 Divide(10, 3, 2) → 结果: 3.33 + 调用 Reverse(Hello) → 结果: olleH + 调用 ToUpper(world) → 结果: WORLD + +4. 缓存统计信息... + 缓存统计: 1 个程序集, 1 个引用 +``` + +--- + +## 📋 TODO / 未来计划 + +### 🔄 项目重构 +- [ ] **转换为纯库项目** - 移除命令行功能,专注于提供API +- [ ] **移除演示程序** - 将 `Examples/` 目录移至独立的演示项目 +- [ ] **调整项目配置** - 移除 `OutputType=Exe` 配置 + +### 🔌 插件管理器集成 +- [ ] **替换 Mock 实现** - 当 KitX 主项目完成后,集成真实的 `PluginManager` +- [ ] **接口适配** - 根据实际需求调整 `IPluginManager` 接口定义 +- [ ] **调用机制优化** - 优化插件调用的性能和稳定性 + +### 🛠️ 功能增强(暂无计划) +### 📈 性能优化(暂无计划) +### 🧪 测试覆盖(暂无计划) + +> **注意**: 当前版本使用 `MockPluginManager` 进行功能验证。在 KitX 生态系统的其他组件完成后,需要将其替换为真实的插件调用实现。 + diff --git a/KitX Script/Kscript.CSharp.Parser/USAGE.md b/KitX Script/Kscript.CSharp.Parser/USAGE.md new file mode 100644 index 0000000..22983bb --- /dev/null +++ b/KitX Script/Kscript.CSharp.Parser/USAGE.md @@ -0,0 +1,292 @@ +# KitX.CSharp.Parser 使用指南 + +## 📖 概述 + +KitX.CSharp.Parser 是 KitX 工作流子系统的核心组件,它能够将插件清单 JSON 即时编译成可直接调用的 C# 静态 API。通过动态 IL 生成技术,让脚本引擎可以像调用普通静态方法一样使用插件功能。 + +## 🚀 快速开始 + +### 基本用法 + +```csharp +using Kscript.CSharp.Parser; + +// 1. 从 JSON 字符串生成程序集 +var jsonString = File.ReadAllText("plugins.json"); +var assembly = Parser.GenerateFromJson(jsonString); + +// 2. 从文件生成程序集 +var assembly = await Parser.GenerateFromFileAsync("plugins.json"); + +// 3. 从插件信息列表生成程序集 +var plugins = new List { /* ... */ }; +var assembly = Parser.Generate(plugins); +``` + +### 动态调用生成的方法 + +```csharp +// 获取生成的插件类型 +var pluginTypes = Parser.GetPluginTypes(assembly); + +foreach (var type in pluginTypes) +{ + var methods = Parser.GetPluginMethods(type); + foreach (var method in methods) + { + // 动态调用方法 + var result = method.Invoke(null, new object[] { /* 参数 */ }); + Console.WriteLine($"结果: {result}"); + } +} +``` + +## 🔧 API 参考 + +### Parser 静态类 + +主入口类,提供所有核心功能。 + +#### 生成方法 + +| 方法 | 描述 | 参数 | +|------|------|------| +| `Generate()` | 从插件信息列表生成程序集 | `plugins`, `assemblyName`, `pluginManager`, `useCache` | +| `GenerateFromJson()` | 从 JSON 字符串生成程序集 | `jsonString`, `assemblyName`, `pluginManager`, `useCache` | +| `GenerateFromFileAsync()` | 从 JSON 文件异步生成程序集 | `jsonFilePath`, `assemblyName`, `pluginManager`, `useCache` | +| `Regenerate()` | 强制重新生成程序集(绕过缓存) | `plugins`, `assemblyName`, `pluginManager` | + +#### 配置方法 + +| 方法 | 描述 | +|------|------| +| `SetDefaultPluginManager()` | 设置默认插件管理器 | +| `RegisterCustomType()` | 注册自定义类型映射 | +| `ClearCache()` | 清除所有缓存 | + +#### 分析方法 + +| 方法 | 描述 | +|------|------| +| `GetPluginTypes()` | 获取程序集中的插件类型 | +| `GetPluginMethods()` | 获取插件类型的方法信息 | +| `GetCacheStatistics()` | 获取缓存统计信息 | +| `HasCache()` | 检查是否存在缓存 | + +### 类型映射系统 + +#### 支持的基础类型 + +| JSON 字符串 | CLR 类型 | +|-------------|----------| +| `"void"` | `System.Void` | +| `"bool"` | `System.Boolean` | +| `"int"` | `System.Int32` | +| `"double"` | `System.Double` | +| `"string"` | `System.String` | +| `"object"` | `System.Object` | + +#### 泛型类型支持 + +```csharp +// 支持的泛型类型 +"List" → List +"Dictionary" → Dictionary +"Nullable" → int? +"Array" → string[] +``` + +#### 自定义类型映射 + +```csharp +// 注册自定义类型 +Parser.RegisterCustomType("DateTime", typeof(DateTime)); +Parser.RegisterCustomType("MyCustomType", typeof(MyClass)); +``` + +## 🏗️ 架构设计 + +### 核心组件 + +1. **Parser** - 主入口类,统一API接口 +2. **TypeMapper** - 类型映射器,处理字符串到Type的转换 +3. **MethodEmitter** - IL方法生成器,核心IL生成逻辑 +4. **AssemblyCache** - 程序集缓存,提升性能 +5. **PluginManager** - 插件管理器接口,处理实际调用 + +### 工作流程 + +```mermaid +graph TD + A[JSON 插件清单] --> B[Parser.Generate] + B --> C[TypeMapper 类型映射] + C --> D[MethodEmitter IL生成] + D --> E[AssemblyCache 缓存] + E --> F[动态程序集] + F --> G[脚本调用] +``` + +## 📝 插件清单格式 + +### 基本结构 + +```json +[ + { + "Name": "PluginName", + "Version": "1.0.0", + "Functions": [ + { + "Name": "MethodName", + "ReturnValueType": "int", + "Parameters": [ + { + "Name": "paramName", + "Type": "string", + "IsOptional": false, + "Value": "defaultValue" + } + ] + } + ] + } +] +``` + +### 完整示例 + +参考项目根目录的 `example.json` 文件,包含完整的插件定义示例。 + +## ⚡ 性能优化 + +### 缓存机制 + +- **程序集缓存**: 相同插件清单只生成一次 +- **类型映射缓存**: 避免重复类型解析 +- **智能哈希**: 基于插件内容生成缓存键 + +### 最佳实践 + +```csharp +// 1. 复用程序集 +var assembly = Parser.Generate(plugins, useCache: true); + +// 2. 批量注册自定义类型 +Parser.RegisterCustomType("DateTime", typeof(DateTime)); +Parser.RegisterCustomType("Guid", typeof(Guid)); + +// 3. 合理使用强制重新生成 +if (pluginChanged) +{ + assembly = Parser.Regenerate(plugins); +} +``` + +## 🔍 调试功能 + +### 日志输出 + +使用 `MockPluginManager` 时会输出详细的调用日志: + +``` +[MockPluginManager] 调用插件方法: SampleCalculator.Add(10, 20) +[MockPluginManager] 参数类型: [Int32, Int32] +[MockPluginManager] 期望返回类型: Int32 +[MockPluginManager] 返回结果: 30 +``` + +### 缓存统计 + +```csharp +var stats = Parser.GetCacheStatistics(); +Console.WriteLine($"缓存统计: {stats.CachedAssemblyCount} 个程序集"); +``` + +## 🚨 错误处理 + +### 异常类型 + +- `ParserException` - 解析器相关异常 +- `ArgumentException` - 参数错误 +- `FileNotFoundException` - 文件不存在 + +### 异常处理示例 + +```csharp +try +{ + var assembly = Parser.GenerateFromJson(jsonString); +} +catch (ParserException ex) +{ + Console.WriteLine($"解析失败: {ex.Message}"); +} +catch (JsonException ex) +{ + Console.WriteLine($"JSON 格式错误: {ex.Message}"); +} +``` + +## 🔌 扩展点 + +### 自定义插件管理器 + +```csharp +public class CustomPluginManager : IPluginManager +{ + public T Call(PluginCallInfo callInfo) + { + // 实现自定义调用逻辑 + return default(T); + } + + // 实现其他接口方法... +} + +// 设置自定义管理器 +Parser.SetDefaultPluginManager(new CustomPluginManager()); +``` + +### 自定义类型映射 + +```csharp +// 扩展类型映射 +TypeMapper.RegisterCustomType("Vector3", typeof(UnityEngine.Vector3)); +TypeMapper.RegisterCustomType("Color", typeof(System.Drawing.Color)); +``` + +## 🔄 与其他系统集成 + +### 脚本引擎集成 + +生成的程序集可直接用于各种 C# 脚本引擎: + +- **Roslyn** - Microsoft.CodeAnalysis +- **CSharpScript** - Microsoft.CodeAnalysis.CSharp.Scripting +- **CS-Script** - 第三方脚本引擎 + +### 示例集成代码 + +```csharp +// 在脚本中使用生成的API +var script = @" + int result = SampleCalculator.Add(10, 20); + string reversed = StringToolkit.Reverse(""Hello""); + return result; +"; + +// 通过脚本引擎执行 +var assembly = Parser.Generate(plugins); +var scriptResult = ExecuteScript(script, assembly); +``` + +## 📚 更多示例 + +完整的使用示例请参考: +- `Examples/BasicUsageExample.cs` - 基础功能演示 +- `Examples/Program.cs` - 控制台演示程序 + +## 🔗 相关项目 + +- **KitX.Shared.CSharp** - 共享数据模型 +- **KitX 主项目** - 完整的插件生态系统 diff --git a/KitX Script/Kscript.CSharp.Parser/example.json b/KitX Script/Kscript.CSharp.Parser/example.json new file mode 100644 index 0000000..60ed38c --- /dev/null +++ b/KitX Script/Kscript.CSharp.Parser/example.json @@ -0,0 +1,154 @@ +[ + { + "Name": "SampleCalculator", + "Version": "1.0.0", + "DisplayName": { + "en": "Calculator", + "zh": "计算器" + }, + "AuthorName": "KitX Devs", + "AuthorLink": "https://github.com/Crequency/KitX", + "PublisherName": "Crequency", + "PublisherLink": "https://kitx.cc", + "SimpleDescription": { + "en": "A simple calculator plugin.", + "zh": "简易计算器插件" + }, + "ComplexDescription": {}, + "TotalDescriptionInMarkdown": {}, + "IconInBase64": "", + "PublishDate": "2025-07-29T00:00:00Z", + "LastUpdateDate": "2025-07-29T00:00:00Z", + "IsMarketVersion": true, + "Tags": { + "en": "math,utility", + "zh": "数学,工具" + }, + "Functions": [ + { + "Name": "Add", + "DisplayNames": { + "en": "Add two integers", + "zh": "整数相加" + }, + "Parameters": [ + { + "Name": "a", + "DisplayNames": { + "en": "First number", + "zh": "第一个数" + }, + "Type": "int", + "Value": "0", + "IsOptional": false + }, + { + "Name": "b", + "DisplayNames": { + "en": "Second number", + "zh": "第二个数" + }, + "Type": "int", + "Value": "0", + "IsOptional": false + } + ], + "ReturnValue": "Sum of a and b", + "ReturnValueType": "int" + }, + { + "Name": "Divide", + "DisplayNames": { + "en": "Divide with optional precision", + "zh": "带可选精度的除法" + }, + "Parameters": [ + { + "Name": "numerator", + "Type": "double", + "Value": "0", + "IsOptional": false + }, + { + "Name": "denominator", + "Type": "double", + "Value": "1", + "IsOptional": false + }, + { + "Name": "decimals", + "Type": "int", + "Value": "2", + "IsOptional": true + } + ], + "ReturnValue": "Rounded quotient", + "ReturnValueType": "double" + } + ], + "RootStartupFileName": "SampleCalculator.dll" + }, + { + "Name": "StringToolkit", + "Version": "1.2.0", + "DisplayName": { + "en": "String Toolkit", + "zh": "字符串工具箱" + }, + "AuthorName": "Community", + "AuthorLink": "https://example.com", + "PublisherName": "KitX", + "PublisherLink": "https://kitx.cc", + "SimpleDescription": { + "en": "Utility functions for string manipulation.", + "zh": "字符串处理工具集" + }, + "ComplexDescription": {}, + "TotalDescriptionInMarkdown": {}, + "IconInBase64": "", + "PublishDate": "2025-07-29T00:00:00Z", + "LastUpdateDate": "2025-07-29T00:00:00Z", + "IsMarketVersion": false, + "Tags": { + "en": "string,utility,text", + "zh": "字符串,工具,文本" + }, + "Functions": [ + { + "Name": "Reverse", + "DisplayNames": { + "en": "Reverse string", + "zh": "反转字符串" + }, + "Parameters": [ + { + "Name": "text", + "Type": "string", + "Value": "", + "IsOptional": false + } + ], + "ReturnValue": "Reversed text", + "ReturnValueType": "string" + }, + { + "Name": "ToUpper", + "DisplayNames": { + "en": "Convert to upper case", + "zh": "转大写" + }, + "Parameters": [ + { + "Name": "text", + "Type": "string", + "Value": "", + "IsOptional": false + } + ], + "ReturnValue": "Uppercase text", + "ReturnValueType": "string" + } + ], + "RootStartupFileName": "StringToolkit.dll" + } + ] diff --git a/KitX Script/Kscript.CSharp/Kscript.CSharp.sln b/KitX Script/Kscript.CSharp/Kscript.CSharp.sln new file mode 100644 index 0000000..bf8c8ad --- /dev/null +++ b/KitX Script/Kscript.CSharp/Kscript.CSharp.sln @@ -0,0 +1,24 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.2.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kscript.CSharp", "Kscript.CSharp.csproj", "{F935BEC1-159C-96F1-83A8-DE0F8509777F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F935BEC1-159C-96F1-83A8-DE0F8509777F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F935BEC1-159C-96F1-83A8-DE0F8509777F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F935BEC1-159C-96F1-83A8-DE0F8509777F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F935BEC1-159C-96F1-83A8-DE0F8509777F}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {CFC1FA4A-256C-4413-BD89-42FCAB741EBB} + EndGlobalSection +EndGlobal From 9c6eda2013fd13080f65d7692a0747a966e02b37 Mon Sep 17 00:00:00 2001 From: StarInk Date: Fri, 7 Nov 2025 19:57:00 +0100 Subject: [PATCH 09/60] =?UTF-8?q?=F0=9F=92=BE=20Feat(Kscript.CSharp.Parser?= =?UTF-8?q?):=20=E5=B0=9D=E8=AF=95=E9=80=82=E9=85=8DDashboard=E7=9A=84?= =?UTF-8?q?=E6=8F=92=E4=BB=B6=E7=AE=A1=E7=90=86=E5=99=A8=EF=BC=88=E6=9C=AA?= =?UTF-8?q?=E5=AE=8C=E6=88=90=EF=BC=8C=E5=BE=85=E4=BC=98=E5=8C=96=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ARCHITECTURE_ANALYSIS.md | 285 ++++++++++++++ .../Kscript.CSharp.Parser/CLASS_DIAGRAM.md | 343 +++++++++++++++++ .../CodeGen/MethodEmitter.cs | 22 +- .../Core/RealPluginManager.cs | 353 ++++++++++++++++++ .../Kscript.CSharp.Parser/Examples/Program.cs | 7 + .../Examples/RealPluginManagerExample.cs | 131 +++++++ .../INTEGRATION_GUIDE.md | 163 ++++++++ .../Kscript.CSharp.Parser.csproj | 1 + KitX Script/Kscript.CSharp.Parser/Parser.cs | 25 +- KitX Script/Kscript.CSharp.Parser/README.md | 56 +++ 10 files changed, 1381 insertions(+), 5 deletions(-) create mode 100644 KitX Script/Kscript.CSharp.Parser/ARCHITECTURE_ANALYSIS.md create mode 100644 KitX Script/Kscript.CSharp.Parser/CLASS_DIAGRAM.md create mode 100644 KitX Script/Kscript.CSharp.Parser/Core/RealPluginManager.cs create mode 100644 KitX Script/Kscript.CSharp.Parser/Examples/RealPluginManagerExample.cs create mode 100644 KitX Script/Kscript.CSharp.Parser/INTEGRATION_GUIDE.md diff --git a/KitX Script/Kscript.CSharp.Parser/ARCHITECTURE_ANALYSIS.md b/KitX Script/Kscript.CSharp.Parser/ARCHITECTURE_ANALYSIS.md new file mode 100644 index 0000000..b78fc4b --- /dev/null +++ b/KitX Script/Kscript.CSharp.Parser/ARCHITECTURE_ANALYSIS.md @@ -0,0 +1,285 @@ +# KitX.CSharp.Parser 架构分析与重构建议 + +## 🚨 当前架构问题分析 + +### 1. 循环依赖风险 + +```mermaid +graph TD + Parser --> MethodEmitter + MethodEmitter --> IPluginManager + Parser --> IPluginManager + MethodEmitter -.-> Parser +``` + +**问题描述:** +- [`Parser`](KitX Standard/KitX Script/Kscript.CSharp.Parser/Parser.cs:12) 设置插件管理器,但 [`MethodEmitter`](KitX Standard/KitX Script/Kscript.CSharp.Parser/CodeGen/MethodEmitter.cs:10) 也需要访问 +- [`MethodEmitter.SetStaticPluginManager()`](KitX Standard/KitX Script/Kscript.CSharp.Parser/CodeGen/MethodEmitter.cs:21) 创建了隐式依赖 + +### 2. 职责边界模糊 + +| 类 | 当前职责 | 问题 | +|------|----------|------| +| **Parser** | 入口+配置+协调+缓存管理 | 职责过多,违反单一职责原则 | +| **MethodEmitter** | IL生成+插件管理器管理 | 混合了生成逻辑和运行时逻辑 | +| **AssemblyCache** | 缓存+直接调用MethodEmitter | 缓存层不应该知道生成细节 | + +### 3. 跨层调用问题 + +```mermaid +graph LR + Parser[Parser 表现层] --> AssemblyCache[缓存层] + AssemblyCache --> MethodEmitter[生成层] + MethodEmitter --> TypeMapper[解析层] +``` + +**问题:** 缓存层直接调用生成层,破坏了分层架构的纯粹性 + +## 🎯 重构方案:单向数据流架构 + +### 核心设计原则 + +1. **数据只能向下流动**:上层 → 下层 +2. **配置只能横向注入**:配置层 → 各层 +3. **事件只能向上冒泡**:下层 → 上层(异常、状态) +4. **禁止跨层调用**:只能调用相邻层 + +### 📋 重构后的分层架构 + +```mermaid +graph TD + %% 输入层 + A[JSON 输入] --> B[Parser 输入层] + + %% 解析层 + B --> C[PluginInfo 解析层] + C --> D[TypeMapper 类型映射] + + %% 生成层 + D --> E[AssemblyBuilder 程序集构建器] + E --> F[MethodEmitter IL生成器] + + %% 缓存层 + F --> G[AssemblyCache 缓存层] + + %% 执行层 + G --> H[PluginManager 插件管理器] + H --> I[PluginCallInfo 调用信息] + + %% 输出层 + I --> J[动态程序集输出] + + %% 配置层(独立) + K[Configuration 配置层] -.-> B + K -.-> D + K -.-> G + K -.-> H + + %% 样式 + classDef inputLayer fill:#e1f5fe + classDef parseLayer fill:#f3e5f5 + classDef generateLayer fill:#e8f5e8 + classDef cacheLayer fill:#fff3e0 + classDef executeLayer fill:#fce4ec + classDef outputLayer fill:#e0f2f1 + classDef configLayer fill:#f1f8e9 + + class A,B inputLayer + class C,D parseLayer + class E,F generateLayer + class G cacheLayer + class H,I executeLayer + class J outputLayer + class K configLayer +``` + +### 🔧 重构后的类职责 + +| 层级 | 类名 | 职责 | 输入 | 输出 | +|------|------|------|------|------| +| **输入层** | Parser | 统一入口,参数验证 | JSON/PluginInfo | 验证后的数据 | +| **解析层** | TypeMapper | 类型映射,验证 | 类型字符串 | System.Type | +| **生成层** | AssemblyBuilder | 协调生成过程 | PluginInfo | IL代码 | +| | MethodEmitter | 纯IL生成 | 类型信息 | IL指令 | +| **缓存层** | AssemblyCache | 纯缓存管理 | 程序集 | 缓存的程序集 | +| **执行层** | PluginManager | 插件调用 | 调用信息 | 执行结果 | +| **配置层** | Configuration | 配置管理 | 配置数据 | 配置实例 | + +## 🛠️ 具体重构策略 + +### 1. 引入依赖注入容器 + +```csharp +// 替代静态依赖 +public class ServiceContainer +{ + public void ConfigureServices() + { + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + } +} +``` + +### 2. 接口隔离原则 + +```csharp +// 明确的接口定义 +public interface IAssemblyBuilder +{ + Assembly BuildFromPlugins(List plugins, string assemblyName); +} + +public interface IMethodEmitter +{ + void EmitMethod(TypeBuilder typeBuilder, Function function); +} + +public interface ITypeMapper +{ + Type MapType(string typeName); + void RegisterCustomType(string typeName, Type type); +} +``` + +### 3. 事件驱动通信 + +```csharp +// 替代直接调用 +public class AssemblyBuilder +{ + public event EventHandler AssemblyGenerated; + public event EventHandler MethodGenerated; + + public Assembly Build(List plugins) + { + // 生成逻辑 + foreach (var plugin in plugins) + { + foreach (var function in plugin.Functions) + { + MethodGenerated?.Invoke(this, new MethodGeneratedEventArgs(function)); + } + } + + var assembly = // ... 生成程序集 + AssemblyGenerated?.Invoke(this, new AssemblyGeneratedEventArgs(assembly)); + return assembly; + } +} +``` + +### 4. 配置管理分离 + +```csharp +public class ParserConfiguration +{ + public IPluginManager PluginManager { get; set; } + public bool UseCache { get; set; } = true; + public string DefaultAssemblyName { get; set; } = "DynamicPluginAssembly"; + public TimeSpan CacheTimeout { get; set; } = TimeSpan.FromHours(1); +} + +public class ConfigurationManager +{ + public static ParserConfiguration GetConfiguration() + { + // 从配置文件、环境变量等读取配置 + return new ParserConfiguration + { + UseCache = bool.Parse(Environment.GetEnvironmentVariable("KSCRIPT_CACHE") ?? "true"), + DefaultAssemblyName = Environment.GetEnvironmentVariable("KSCRIPT_ASSEMBLY_NAME") ?? "DynamicPluginAssembly" + }; + } +} +``` + +## 🔄 重构后的数据流 + +### 程序集生成流程 + +```mermaid +sequenceDiagram + participant Client + participant Parser + participant AssemblyBuilder + participant TypeMapper + participant MethodEmitter + participant AssemblyCache + participant PluginManager + + Client->>Parser: Generate(plugins) + Parser->>Parser: ValidateInput() + Parser->>AssemblyBuilder: Build(plugins) + + AssemblyBuilder->>TypeMapper: MapType(typeName) + TypeMapper-->>AssemblyBuilder: Type + + AssemblyBuilder->>MethodEmitter: EmitMethod() + MethodEmitter-->>AssemblyBuilder: IL Instructions + + AssemblyBuilder->>AssemblyCache: GetOrCreate(assembly) + AssemblyCache-->>AssemblyBuilder: Cached Assembly + + AssemblyBuilder-->>Parser: Assembly + Parser->>PluginManager: SetContext(assembly) + Parser-->>Client: Assembly +``` + +### 插件调用流程 + +```mermaid +sequenceDiagram + participant Script + participant GeneratedMethod + participant PluginManager + participant PluginConnector + participant PluginProcess + + Script->>GeneratedMethod: Plugin.Method(args) + GeneratedMethod->>PluginManager: Call(PluginCallInfo) + + PluginManager->>PluginConnector: FindConnector() + PluginManager->>PluginConnector: SendRequest() + PluginConnector->>PluginProcess: WebSocket Communication + PluginProcess-->>PluginConnector: Response + PluginConnector-->>PluginManager: HandleResponse() + PluginManager-->>GeneratedMethod: Result + GeneratedMethod-->>Script: Result +``` + +## ✅ 重构收益 + +1. **清晰的职责分离** - 每个类只负责一个明确的功能 +2. **单向数据流** - 避免循环依赖和混乱的调用关系 +3. **易于测试** - 每层可以独立测试 +4. **易于维护** - 修改一层不会影响其他层 +5. **可扩展性** - 新功能可以通过添加新层实现 +6. **配置灵活性** - 通过依赖注入支持不同的运行环境 + +## 🎯 实施步骤 + +1. **第一阶段:接口定义** + - 定义各层的接口 + - 确定数据契约 + +2. **第二阶段:依赖注入** + - 引入DI容器 + - 重构静态依赖 + +3. **第三阶段:事件驱动** + - 替换直接调用为事件 + - 实现观察者模式 + +4. **第四阶段:配置管理** + - 分离配置逻辑 + - 支持多环境配置 + +5. **第五阶段:测试验证** + - 单元测试各层 + - 集成测试整体流程 + +这样的重构将显著提升 KitX.CSharp.Parser 项目的可维护性和可扩展性,同时保持现有功能的完整性。 diff --git a/KitX Script/Kscript.CSharp.Parser/CLASS_DIAGRAM.md b/KitX Script/Kscript.CSharp.Parser/CLASS_DIAGRAM.md new file mode 100644 index 0000000..1ccf714 --- /dev/null +++ b/KitX Script/Kscript.CSharp.Parser/CLASS_DIAGRAM.md @@ -0,0 +1,343 @@ +# KitX.CSharp.Parser 类图与调用关系 + +## 📊 整体架构类图 + +```mermaid +classDiagram + %% 外部依赖 + class KitXDashboard { + +PluginsServer.Instance + +WorkflowScriptService + } + + class KitXShared { + <> + +PluginInfo + +Function + +Parameter + +Connector + +Request + } + + %% 主入口类 + class Parser { + <> + -_defaultPluginManager: IPluginManager + -_pluginManagerFactory: Func~IPluginManager~ + +Generate(plugins, assemblyName, pluginManager, useCache): Assembly + +GenerateFromJson(jsonString, assemblyName, pluginManager, useCache): Assembly + +GenerateFromFileAsync(jsonFilePath, assemblyName, pluginManager, useCache): Task~Assembly~ + +Regenerate(plugins, assemblyName, pluginManager): Assembly + +SetDefaultPluginManager(pluginManager): void + +SetPluginManagerFactory(factory): void + +RegisterCustomType(typeName, type): void + +ClearCache(): void + +GetCacheStatistics(): CacheStatistics + +HasCache(plugins, assemblyName): bool + +GetPluginTypes(assembly): List~Type~ + +GetPluginMethods(pluginType): List~MethodInfo~ + } + + %% 核心接口 + class IPluginManager { + <> + +Call~T~(callInfo): T + +Call(callInfo): void + +IsPluginExists(pluginName): bool + +IsMethodExists(pluginName, methodName): bool + } + + %% 插件管理器实现 + class MockPluginManager { + -_mockData: Dictionary~string, Dictionary~ + +Call~T~(callInfo): T + +Call(callInfo): void + +IsPluginExists(pluginName): bool + +IsMethodExists(pluginName, methodName): bool + -InvokeMockMethod(callInfo, method): object + -GetDefaultValue(type): object + } + + class RealPluginManager { + -_pluginsServer: object + -_infoLogger: Action~string~ + -_errorLogger: Action~string~ + -_pendingRequests: ConcurrentDictionary~string, TaskCompletionSource~ + +Call~T~(callInfo): T + +Call(callInfo): void + +IsPluginExists(pluginName): bool + +IsMethodExists(pluginName, methodName): bool + -FindPluginInfo(pluginName): PluginInfo + -FindPluginConnector(pluginInfo): object + -SendPluginRequest~T~(connector, callInfo): Task~T~ + -SendRequestToConnector(connector, request): Task + +HandlePluginResponse(requestId, responseJson): void + -GetDefaultResult~T~(): T + } + + %% 代码生成组件 + class MethodEmitter { + <> + -_staticPluginManager: IPluginManager + +SetStaticPluginManager(pluginManager): void + +GenerateAssembly(plugins, assemblyName, pluginManager): Assembly + -GeneratePluginClass(moduleBuilder, plugin, pluginManager): void + -GenerateStaticConstructor(typeBuilder, pluginManagerField, pluginManager): void + -GeneratePluginMethod(typeBuilder, pluginName, function, pluginManagerField): void + -GenerateMethodBody(methodBuilder, pluginName, function, parameterTypes, returnType, pluginManagerField): void + -ConvertDefaultValue(value, targetType): object + } + + class TypeMapper { + <> + -_basicTypeMap: Dictionary~string, Type~ + -_customTypeMap: ConcurrentDictionary~string, Type~ + -_typeCache: ConcurrentDictionary~string, Type~ + -_genericTypeRegex: Regex + +MapType(typeName): Type + -ResolveGenericType(genericMatch): Type + -ParseGenericArguments(argsString): Type[] + -SplitGenericArguments(argsString): string[] + +RegisterCustomType(typeName, type): void + +ClearCache(): void + +GetCustomTypes(): IReadOnlyDictionary~string, Type~ + } + + class AssemblyCache { + <> + -_assemblyCache: ConcurrentDictionary~string, Assembly~ + -_referenceCount: ConcurrentDictionary~string, int~ + -_cacheLock: object + +GetOrCreateAssembly(plugins, assemblyName, pluginManager): Assembly + -GenerateCacheKey(plugins, assemblyName): string + -IncrementReference(cacheKey): void + +DecrementReference(cacheKey): void + +ClearCache(): void + +GetStatistics(): CacheStatistics + +HasCache(plugins, assemblyName): bool + +ForceRegenerate(plugins, assemblyName, pluginManager): Assembly + } + + %% 数据模型 + class PluginCallInfo { + +PluginName: string + +MethodName: string + +Parameters: object[] + +ParameterTypes: Type[] + +ToString(): string + } + + class CacheStatistics { + +CachedAssemblyCount: int + +TotalReferences: int + +CacheKeys: List~string~ + +ToString(): string + } + + %% 异常类 + class ParserException { + +ParserException() + +ParserException(message) + +ParserException(message, innerException) + +TypeMappingError(typeName, innerException): ParserException + +ILGenerationError(methodName, innerException): ParserException + +AssemblyGenerationError(assemblyName, innerException): ParserException + } + + %% 示例类 + class BasicUsageExample { + <> + +RunExample(): Task + -RunWithBuiltInData(): void + -DynamicInvokeExample(assembly): void + -GenerateTestArguments(methodName, parameters): object[] + -GetDefaultValue(type): object + +RunAdvancedExample(): void + } + + class RealPluginManagerExample { + <> + +RunExample(): void + +DashboardIntegrationExample(): void + } + + %% 关系定义 + Parser --> IPluginManager : 使用 + Parser --> TypeMapper : 使用 + Parser --> AssemblyCache : 使用 + Parser --> MethodEmitter : 调用 + Parser --> CacheStatistics : 返回 + + IPluginManager <|.. MockPluginManager : 实现 + IPluginManager <|.. RealPluginManager : 实现 + + MockPluginManager --> PluginCallInfo : 使用 + RealPluginManager --> PluginCallInfo : 使用 + RealPluginManager --> KitXDashboard : 依赖 + RealPluginManager --> KitXShared : 使用 + + MethodEmitter --> IPluginManager : 使用 + MethodEmitter --> TypeMapper : 使用 + MethodEmitter --> PluginCallInfo : 使用 + MethodEmitter --> ParserException : 抛出 + + TypeMapper --> ParserException : 抛出 + + AssemblyCache --> MethodEmitter : 调用 + AssemblyCache --> CacheStatistics : 返回 + + BasicUsageExample --> Parser : 使用 + RealPluginManagerExample --> Parser : 使用 + RealPluginManagerExample --> RealPluginManager : 使用 + + KitXDashboard --> RealPluginManager : 创建 +``` + +## 🔄 调用时序图 + +### 基本生成流程 + +```mermaid +sequenceDiagram + participant Client + participant Parser + participant AssemblyCache + participant MethodEmitter + participant TypeMapper + participant IPluginManager + + Client->>Parser: Generate(plugins) + Parser->>AssemblyCache: GetOrCreateAssembly() + + alt 缓存命中 + AssemblyCache-->>Parser: 返回缓存的Assembly + else 缓存未命中 + AssemblyCache->>MethodEmitter: GenerateAssembly() + + loop 对每个插件 + MethodEmitter->>TypeMapper: MapType(typeName) + TypeMapper-->>MethodEmitter: 返回Type + + MethodEmitter->>MethodEmitter: GeneratePluginClass() + MethodEmitter->>MethodEmitter: GenerateStaticConstructor() + + loop 对每个功能 + MethodEmitter->>MethodEmitter: GeneratePluginMethod() + MethodEmitter->>MethodEmitter: GenerateMethodBody() + MethodEmitter->>IPluginManager: SetStaticPluginManager() + end + end + + MethodEmitter-->>AssemblyCache: 返回新Assembly + AssemblyCache-->>Parser: 返回Assembly + end + + Parser-->>Client: 返回Assembly +``` + +### 真实插件调用流程 + +```mermaid +sequenceDiagram + participant Script + participant GeneratedMethod + participant RealPluginManager + participant PluginConnector + participant PluginProcess + participant KitXDashboard + + Script->>GeneratedMethod: SampleCalculator.Add(10, 20) + GeneratedMethod->>RealPluginManager: Call(PluginCallInfo) + + RealPluginManager->>KitXDashboard: FindPluginInfo() + KitXDashboard-->>RealPluginManager: PluginInfo + + RealPluginManager->>KitXDashboard: FindPluginConnector() + KitXDashboard-->>RealPluginManager: PluginConnector + + RealPluginManager->>PluginConnector: SendRequest(Request) + PluginConnector->>PluginProcess: WebSocket 通信 + PluginProcess-->>PluginConnector: Response + PluginConnector-->>RealPluginManager: HandleResponse() + + RealPluginManager-->>GeneratedMethod: 返回结果 + GeneratedMethod-->>Script: 返回结果 +``` + +## 📋 类职责说明 + +### 核心类 + +| 类名 | 职责 | 关键方法 | +|------|------|----------| +| **Parser** | 主入口,统一API | `Generate()`, `GenerateFromJson()`, `GenerateFromFileAsync()` | +| **IPluginManager** | 插件管理器接口 | `Call()`, `IsPluginExists()` | +| **MockPluginManager** | 模拟插件管理器 | `Call()`, `InvokeMockMethod()` | +| **RealPluginManager** | 真实插件管理器 | `Call()`, `SendPluginRequest()` | + +### 代码生成类 + +| 类名 | 职责 | 关键方法 | +|------|------|----------| +| **MethodEmitter** | IL代码生成 | `GenerateAssembly()`, `GeneratePluginClass()` | +| **TypeMapper** | 类型映射 | `MapType()`, `RegisterCustomType()` | +| **AssemblyCache** | 程序集缓存 | `GetOrCreateAssembly()`, `ForceRegenerate()` | + +### 数据模型类 + +| 类名 | 职责 | 关键属性 | +|------|------|----------| +| **PluginCallInfo** | 插件调用信息 | `PluginName`, `MethodName`, `Parameters` | +| **CacheStatistics** | 缓存统计信息 | `CachedAssemblyCount`, `TotalReferences` | + +## 🔧 设计模式应用 + +### 1. 工厂模式 +- `Parser.SetPluginManagerFactory()` - 创建插件管理器实例 +- `MethodEmitter.GenerateAssembly()` - 创建动态程序集 + +### 2. 策略模式 +- `IPluginManager` 接口 - 不同的插件管理器实现策略 +- `MockPluginManager` vs `RealPluginManager` - 测试vs生产策略 + +### 3. 单例模式 +- `AssemblyCache` - 静态缓存管理 +- `TypeMapper` - 静态类型映射 + +### 4. 建造者模式 +- `MethodEmitter` 中的 IL 生成过程 +- `Connector` 的请求构建过程 + +### 5. 模板方法模式 +- `Parser.Generate()` 方法的通用流程 +- 插件方法生成的标准流程 + +## 🎯 关键调用路径 + +### 1. 程序集生成路径 +``` +Parser.Generate() +→ AssemblyCache.GetOrCreateAssembly() +→ MethodEmitter.GenerateAssembly() +→ TypeMapper.MapType() +→ IL生成 +``` + +### 2. 插件调用路径 +``` +生成的静态方法 +→ IPluginManager.Call() +→ RealPluginManager.SendPluginRequest() +→ WebSocket通信 +→ 插件进程 +``` + +### 3. 缓存管理路径 +``` +AssemblyCache.GenerateCacheKey() +→ SHA256哈希计算 +→ 缓存查找/存储 +→ 引用计数管理 +``` + +这个类图展示了 KitX.CSharp.Parser 项目的完整架构,包括各个类之间的依赖关系、调用顺序和设计模式的应用。 diff --git a/KitX Script/Kscript.CSharp.Parser/CodeGen/MethodEmitter.cs b/KitX Script/Kscript.CSharp.Parser/CodeGen/MethodEmitter.cs index f4921fd..7e9f713 100644 --- a/KitX Script/Kscript.CSharp.Parser/CodeGen/MethodEmitter.cs +++ b/KitX Script/Kscript.CSharp.Parser/CodeGen/MethodEmitter.cs @@ -9,6 +9,19 @@ namespace Kscript.CSharp.Parser.CodeGen; /// public static class MethodEmitter { + /// + /// 静态插件管理器实例,用于在生成的程序集中使用 + /// + private static IPluginManager? _staticPluginManager; + + /// + /// 设置静态插件管理器实例 + /// + /// 插件管理器实例 + public static void SetStaticPluginManager(IPluginManager pluginManager) + { + _staticPluginManager = pluginManager; + } /// /// 为插件生成动态程序集 /// @@ -21,7 +34,7 @@ public static Assembly GenerateAssembly(List plugins, string assembl try { // 创建动态程序集 - var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly( + var assemblyBuilder = System.Reflection.Emit.AssemblyBuilder.DefineDynamicAssembly( new AssemblyName(assemblyName), AssemblyBuilderAccess.Run); @@ -89,10 +102,13 @@ private static void GenerateStaticConstructor(TypeBuilder typeBuilder, FieldBuil var il = constructorBuilder.GetILGenerator(); - if (pluginManager != null) + // 优先使用静态插件管理器实例 + var managerToUse = _staticPluginManager ?? pluginManager; + + if (managerToUse != null) { // 如果提供了插件管理器实例,直接使用 - il.Emit(OpCodes.Ldtoken, pluginManager.GetType()); + il.Emit(OpCodes.Ldtoken, managerToUse.GetType()); il.Emit(OpCodes.Call, typeof(Type).GetMethod("GetTypeFromHandle")!); il.Emit(OpCodes.Call, typeof(Activator).GetMethod("CreateInstance", new[] { typeof(Type) })!); il.Emit(OpCodes.Castclass, typeof(IPluginManager)); diff --git a/KitX Script/Kscript.CSharp.Parser/Core/RealPluginManager.cs b/KitX Script/Kscript.CSharp.Parser/Core/RealPluginManager.cs new file mode 100644 index 0000000..3626efa --- /dev/null +++ b/KitX Script/Kscript.CSharp.Parser/Core/RealPluginManager.cs @@ -0,0 +1,353 @@ +using System.Text.Json; +using Kscript.CSharp.Parser.Models; +using KitX.Shared.CSharp.Plugin; +using KitX.Shared.CSharp.WebCommand; + +using Serilog; +using System.Collections.Concurrent; + +namespace Kscript.CSharp.Parser.Core; + +/// +/// 实际的插件管理器 - 使用 KitX Dashboard 的插件系统进行真实调用 +/// +public class RealPluginManager : IPluginManager +{ + private readonly object _pluginsServer; + private readonly Action _infoLogger; + private readonly Action _errorLogger; + + private readonly JsonSerializerOptions _serializerOptions = new() + { + WriteIndented = true, + IncludeFields = true, + PropertyNameCaseInsensitive = true, + }; + + // 用于存储等待响应的请求 + private readonly ConcurrentDictionary> _pendingRequests = new(); + + /// + /// 构造函数 + /// + /// PluginsServer 实例 + /// 信息日志记录器 + /// 错误日志记录器 + public RealPluginManager(object pluginsServer, Action? infoLogger = null, Action? errorLogger = null) + { + _pluginsServer = pluginsServer ?? throw new ArgumentNullException(nameof(pluginsServer)); + _infoLogger = infoLogger ?? (message => Console.WriteLine(message)); + _errorLogger = errorLogger ?? (message => Console.WriteLine(message)); + + _infoLogger("[WorkflowScriptService] 正在初始化 RealPluginManager..."); + } + + /// + /// 调用插件方法 + /// + public T Call(PluginCallInfo callInfo) + { + try + { + _infoLogger($"[RealPluginManager] 开始调用插件方法: {callInfo}"); + + // 查找插件信息 + var pluginInfo = FindPluginInfo(callInfo.PluginName); + if (pluginInfo == null) + { + _infoLogger($"[RealPluginManager] 未找到插件: {callInfo.PluginName}"); + return GetDefaultResult(); + } + + // 查找插件连接器 + var connector = FindPluginConnector(pluginInfo); + if (connector == null) + { + _infoLogger($"[RealPluginManager] 插件 {callInfo.PluginName} 未连接"); + return GetDefaultResult(); + } + + // 创建并发送请求 + var result = SendPluginRequest(connector, callInfo).GetAwaiter().GetResult(); + + _infoLogger($"[RealPluginManager] 插件调用完成: {callInfo} -> 结果: {result}"); + return result; + } + catch (Exception ex) + { + _errorLogger($"[RealPluginManager] 调用插件方法失败: {callInfo} - 异常: {ex.Message}"); + return GetDefaultResult(); + } + } + + /// + /// 调用插件方法(无返回值) + /// + public void Call(PluginCallInfo callInfo) + { + Call(callInfo); + } + + /// + /// 检查插件是否存在 + /// + public bool IsPluginExists(string pluginName) + { + try + { + var pluginInfo = FindPluginInfo(pluginName); + var exists = pluginInfo != null; + + _infoLogger($"[RealPluginManager] 检查插件 '{pluginName}' 是否存在: {exists}"); + return exists; + } + catch (Exception ex) + { + _errorLogger($"[RealPluginManager] 检查插件存在性失败: {pluginName} - 异常: {ex.Message}"); + return false; + } + } + + /// + /// 检查插件方法是否存在 + /// + public bool IsMethodExists(string pluginName, string methodName) + { + try + { + var pluginInfo = FindPluginInfo(pluginName); + var exists = pluginInfo?.Functions.Any(f => f.Name == methodName) ?? false; + + _infoLogger($"[RealPluginManager] 检查方法 '{pluginName}.{methodName}' 是否存在: {exists}"); + return exists; + } + catch (Exception ex) + { + _errorLogger($"[RealPluginManager] 检查方法存在性失败: {pluginName}.{methodName} - 异常: {ex.Message}"); + return false; + } + } + + /// + /// 查找插件信息 + /// + private PluginInfo? FindPluginInfo(string pluginName) + { + try + { + // 通过传入的 PluginsServer 实例查找插件信息 + var pluginsServerType = _pluginsServer.GetType(); + var pluginConnectorsProperty = pluginsServerType.GetProperty("PluginConnectors"); + + if (pluginConnectorsProperty != null) + { + var pluginConnectors = pluginConnectorsProperty.GetValue(_pluginsServer) as System.Collections.IList; + if (pluginConnectors != null) + { + foreach (var connector in pluginConnectors) + { + if (connector != null) + { + var connectorType = connector.GetType(); + var pluginInfoProperty = connectorType.GetProperty("PluginInfo"); + if (pluginInfoProperty != null) + { + var pluginInfo = pluginInfoProperty.GetValue(connector) as PluginInfo; + if (pluginInfo != null && pluginInfo.Name == pluginName) + { + return pluginInfo; + } + } + } + } + } + } + } + catch (Exception ex) + { + _errorLogger($"[RealPluginManager] 查找插件信息失败: {pluginName} - 异常: {ex.Message}"); + } + + return null; + } + + /// + /// 查找插件连接器 + /// + private object? FindPluginConnector(PluginInfo pluginInfo) + { + try + { + var pluginsServerType = _pluginsServer.GetType(); + var findConnectorMethod = pluginsServerType.GetMethod("FindConnector", new[] { typeof(PluginInfo) }); + + if (findConnectorMethod != null) + { + return findConnectorMethod.Invoke(_pluginsServer, new object[] { pluginInfo }); + } + } + catch (Exception ex) + { + _errorLogger($"[RealPluginManager] 查找插件连接器失败: {pluginInfo.Name} - 异常: {ex.Message}"); + } + + return null; + } + + /// + /// 发送插件请求 + /// + private async Task SendPluginRequest(object connector, PluginCallInfo callInfo) + { + try + { + // 创建 Connector 实例 + var connectorInstance = new Connector() + .SetSerializer(x => JsonSerializer.Serialize(x, _serializerOptions)) + .SetSender(request => { + SendRequestToConnector(connector, request).ConfigureAwait(false); + }); + + // 构建参数列表 + var functionArgs = new List(); + for (int i = 0; i < callInfo.Parameters.Length; i++) + { + var paramType = callInfo.ParameterTypes[i]; + var paramValue = callInfo.Parameters[i]; + + functionArgs.Add(new Parameter + { + Name = $"param{i}", + Type = paramType.Name, + Value = paramValue?.ToString() ?? string.Empty, + IsOptional = false + }); + } + + // 生成唯一的请求ID + var requestId = Guid.NewGuid().ToString(); + + // 创建任务完成源,用于等待响应 + var tcs = new TaskCompletionSource(); + _pendingRequests[requestId] = tcs; + + // 发送请求 + var request = connectorInstance + .Request() + .ReceiveCommand() + .UpdateCommand(cmd => + { + cmd.FunctionName = callInfo.MethodName; + cmd.FunctionArgs = functionArgs; + cmd.PluginConnectionId = callInfo.PluginName; + cmd.Tags = new Dictionary + { + ["RequestId"] = requestId + }; + return cmd; + }) + .Send(); + + // 等待响应,设置超时时间为30秒 + var timeout = TimeSpan.FromSeconds(30); + var completedTask = await Task.WhenAny(tcs.Task, Task.Delay(timeout)); + + if (completedTask == tcs.Task) + { + // 获取到响应 + var responseJson = tcs.Task.Result; + _infoLogger($"[RealPluginManager] 收到插件响应: {responseJson}"); + + // 尝试反序列化响应 + try + { + if (typeof(T) == typeof(string)) + { + return (T)(object)responseJson; + } + else if (typeof(T) == typeof(void)) + { + return default(T)!; + } + else + { + return JsonSerializer.Deserialize(responseJson, _serializerOptions) ?? GetDefaultResult(); + } + } + catch (JsonException ex) + { + _errorLogger($"[RealPluginManager] 反序列化响应失败: {ex.Message}"); + return GetDefaultResult(); + } + } + else + { + // 超时 + _errorLogger($"[RealPluginManager] 插件调用超时: {callInfo.PluginName}.{callInfo.MethodName}"); + _pendingRequests.TryRemove(requestId, out _); + return GetDefaultResult(); + } + } + catch (Exception ex) + { + _errorLogger($"[RealPluginManager] 发送插件请求失败: {callInfo.PluginName}.{callInfo.MethodName} - 异常: {ex.Message}"); + return GetDefaultResult(); + } + } + + /// + /// 向连接器发送请求 + /// + private async Task SendRequestToConnector(object connector, Request request) + { + try + { + var connectorType = connector.GetType(); + var requestMethod = connectorType.GetMethod("Request", new[] { typeof(Request) }); + if (requestMethod != null) + { + var result = requestMethod.Invoke(connector, new object[] { request }); + if (result is Task task) + { + await task; + } + } + } + catch (Exception ex) + { + _errorLogger($"[RealPluginManager] 向连接器发送请求失败 - 异常: {ex.Message}"); + } + } + + /// + /// 处理插件响应 + /// + public void HandlePluginResponse(string requestId, string responseJson) + { + if (_pendingRequests.TryRemove(requestId, out var tcs)) + { + tcs.SetResult(responseJson); + } + else + { + _infoLogger($"[RealPluginManager] 收到未知请求的响应: {requestId}"); + } + } + + /// + /// 获取默认结果 + /// + private T GetDefaultResult() + { + if (typeof(T) == typeof(void)) + return default(T)!; + + if (typeof(T) == typeof(string)) + return (T)(object)string.Empty; + + if (typeof(T).IsValueType) + return (T)Activator.CreateInstance(typeof(T))!; + + return default(T)!; + } +} diff --git a/KitX Script/Kscript.CSharp.Parser/Examples/Program.cs b/KitX Script/Kscript.CSharp.Parser/Examples/Program.cs index 5598f27..51b3d0f 100644 --- a/KitX Script/Kscript.CSharp.Parser/Examples/Program.cs +++ b/KitX Script/Kscript.CSharp.Parser/Examples/Program.cs @@ -24,6 +24,13 @@ public static async Task Main(string[] args) // 运行高级功能示例 BasicUsageExample.RunAdvancedExample(); + // 运行实际插件管理器示例 + Console.WriteLine("\n" + new string('=', 60)); + RealPluginManagerExample.RunExample(); + + Console.WriteLine("\n" + new string('=', 60)); + RealPluginManagerExample.DashboardIntegrationExample(); + Console.WriteLine("\n=================================================="); Console.WriteLine("✅ 所有示例运行完成!"); } diff --git a/KitX Script/Kscript.CSharp.Parser/Examples/RealPluginManagerExample.cs b/KitX Script/Kscript.CSharp.Parser/Examples/RealPluginManagerExample.cs new file mode 100644 index 0000000..06a7070 --- /dev/null +++ b/KitX Script/Kscript.CSharp.Parser/Examples/RealPluginManagerExample.cs @@ -0,0 +1,131 @@ +using Kscript.CSharp.Parser; +using Kscript.CSharp.Parser.Core; +using System.Text.Json; + +namespace Kscript.CSharp.Parser.Examples; + +/// +/// 实际插件管理器使用示例 +/// +public static class RealPluginManagerExample +{ + /// + /// 演示如何使用 RealPluginManager + /// + public static void RunExample() + { + Console.WriteLine("=== KitX.CSharp.Parser 实际插件管理器使用示例 ===\n"); + + try + { + // 1. 设置插件管理器工厂函数 + Console.WriteLine("1. 设置插件管理器工厂函数..."); + + // 这里应该传入实际的 PluginsServer 实例和日志记录器 + // 由于这是一个示例,我们使用 null 作为占位符 + Parser.SetPluginManagerFactory(() => + { + // 在实际使用中,这里应该传入真实的 PluginsServer 实例和日志记录器 + // 例如: + // var pluginsServer = KitX.Dashboard.Network.PluginsNetwork.PluginsServer.Instance; + // var logger = Serilog.Log.Logger; + // return new RealPluginManager(pluginsServer, message => logger.Information(message)); + + Console.WriteLine(" 创建 RealPluginManager 实例..."); + return null; // 示例中返回 null,实际使用时返回真实实例 + }); + + Console.WriteLine(" ✓ 插件管理器工厂函数已设置"); + + // 2. 创建示例插件数据 + Console.WriteLine("\n2. 创建示例插件数据..."); + var examplePlugins = new List + { + new PluginInfo + { + Name = "TestPlugin", + Version = "1.0.0", + Functions = new List + { + new Function + { + Name = "TestMethod", + ReturnValueType = "string", + Parameters = new List + { + new Parameter { Name = "input", Type = "string", IsOptional = false } + } + } + } + } + }; + + Console.WriteLine(" ✓ 示例插件数据已创建"); + + // 3. 生成动态程序集 + Console.WriteLine("\n3. 生成动态程序集..."); + var assembly = Parser.Generate(examplePlugins, "RealPluginManagerExampleAssembly"); + Console.WriteLine($" ✓ 成功生成程序集: {assembly.FullName}"); + + // 4. 获取生成的插件类型 + Console.WriteLine("\n4. 分析生成的插件类型..."); + var pluginTypes = Parser.GetPluginTypes(assembly); + Console.WriteLine($" 发现 {pluginTypes.Count} 个插件类:"); + + foreach (var type in pluginTypes) + { + Console.WriteLine($" - {type.Name}"); + var methods = Parser.GetPluginMethods(type); + foreach (var method in methods) + { + var parameters = string.Join(", ", method.GetParameters().Select(p => $"{p.ParameterType.Name} {p.Name}")); + Console.WriteLine($" └── {method.ReturnType.Name} {method.Name}({parameters})"); + } + } + + Console.WriteLine("\n=== 示例完成 ==="); + Console.WriteLine("\n注意:在实际使用中,您需要:"); + Console.WriteLine("1. 确保 KitX Dashboard 正在运行"); + Console.WriteLine("2. 传入真实的 PluginsServer.Instance"); + Console.WriteLine("3. 传入真实的日志记录器实例"); + Console.WriteLine("4. 确保插件已正确连接到 Dashboard"); + } + catch (Exception ex) + { + Console.WriteLine($"❌ 示例执行失败: {ex.Message}"); + Console.WriteLine($"详细信息: {ex}"); + } + } + + /// + /// 演示如何在 KitX Dashboard 中集成 + /// + public static void DashboardIntegrationExample() + { + Console.WriteLine("=== KitX Dashboard 集成示例 ===\n"); + + Console.WriteLine("在 KitX Dashboard 中使用 RealPluginManager 的步骤:\n"); + + Console.WriteLine("1. 在 Dashboard 启动时设置插件管理器工厂:"); + Console.WriteLine("```csharp"); + Console.WriteLine("// 在 Dashboard 的初始化代码中"); + Console.WriteLine("Parser.SetPluginManagerFactory(() => new RealPluginManager("); + Console.WriteLine(" KitX.Dashboard.Network.PluginsNetwork.PluginsServer.Instance,"); + Console.WriteLine(" message => Log.Information(message)"); + Console.WriteLine("));"); + Console.WriteLine("```"); + + Console.WriteLine("\n2. 生成插件程序集:"); + Console.WriteLine("```csharp"); + Console.WriteLine("var assembly = Parser.Generate(plugins, \"DashboardPluginAssembly\");"); + Console.WriteLine("```"); + + Console.WriteLine("\n3. 在脚本中使用生成的 API:"); + Console.WriteLine("```csharp"); + Console.WriteLine("// 现在可以在脚本中直接调用"); + Console.WriteLine("var result = TestPlugin.TestMethod(\"Hello World\");"); + Console.WriteLine("```"); + + Console.WriteLine("\n这样,脚本中的插件调用将通过真实的 KitX Dashboard 插件系统执行!"); + } +} diff --git a/KitX Script/Kscript.CSharp.Parser/INTEGRATION_GUIDE.md b/KitX Script/Kscript.CSharp.Parser/INTEGRATION_GUIDE.md new file mode 100644 index 0000000..4fd92b9 --- /dev/null +++ b/KitX Script/Kscript.CSharp.Parser/INTEGRATION_GUIDE.md @@ -0,0 +1,163 @@ +# KitX.CSharp.Parser 与 KitX Dashboard 集成指南 + +## 🎯 集成概述 + +本文档说明如何将 KitX.CSharp.Parser 的实际插件管理器集成到 KitX Dashboard 中,实现从 MockPluginManager 到真实插件调用的升级。 + +## 📋 完成的集成工作 + +### 1. 创建 RealPluginManager +- ✅ **文件**: [`Core/RealPluginManager.cs`](../Core/RealPluginManager.cs:1-225) +- ✅ **功能**: 实现了 `IPluginManager` 接口,支持真实的 KitX Dashboard 插件调用 +- ✅ **设计**: 采用依赖注入模式,接受外部传入的 `PluginsServer` 实例和日志记录器 +- ✅ **兼容性**: 完全兼容现有的 MockPluginManager 接口 + +### 2. 扩展 MethodEmitter +- ✅ **静态支持**: 在 [`CodeGen/MethodEmitter.cs`](../CodeGen/MethodEmitter.cs:13-21) 中添加了静态插件管理器支持 +- ✅ **工厂模式**: 实现了 `SetStaticPluginManager()` 方法 +- ✅ **灵活配置**: 支持多种插件管理器实现方式 + +### 3. 更新 Parser 主入口 +- ✅ **工厂函数**: 在 [`Parser.cs`](../Parser.cs:23-32) 中添加了 `SetPluginManagerFactory()` 方法 +- ✅ **向后兼容**: 保持与现有 `SetDefaultPluginManager()` 的完全兼容 +- ✅ **自动设置**: 在生成程序集时自动设置静态插件管理器 + +### 4. 集成到 Dashboard +- ✅ **WorkflowScriptService**: 修改了 [`Services/WorkflowScriptService.cs`](../../KitX%20Clients/KitX%20Dashboard/KitX%20Dashboard/Services/WorkflowScriptService.cs:17-52) +- ✅ **静态构造**: 在静态构造函数中设置 RealPluginManager 工厂 +- ✅ **实例传递**: 将PluginManager实例传递给 Parser +- ✅ **错误处理**: 添加了完善的异常处理和日志记录 + +## 🔧 集成架构 + +``` +┌─────────────────────────────────────────────────┐ +│ KitX Dashboard │ +│ ┌─────────────────────────────────────┐ │ +│ │ WorkflowScriptService │ │ +│ │ ┌─ SetPluginManagerFactory │ │ +│ │ │ │ │ │ +│ │ │ ▼ │ │ +│ │ │ RealPluginManager │ │ +│ │ │ ┌─ PluginsServer │ │ +│ │ │ │ ┌─ FindConnector │ │ +│ │ │ │ │ │ │ +│ │ │ │ ▼ │ │ +│ │ │ │ PluginConnector │ │ +│ │ │ │ ┌─ Request │ │ +│ │ │ │ │ │ │ +│ │ │ │ ▼ │ │ +│ │ │ │ WebSocket │ │ +│ │ │ └─ Plugin Process │ │ +│ │ └─────────────────────────────┘ │ +│ │ │ │ +│ │ ▼ Generated Assembly │ │ +│ │ ┌─ MethodEmitter │ │ +│ │ │ │ ┌─ Static Constructor │ │ +│ │ │ │ │ │ │ +│ │ │ │ ▼ │ │ +│ │ │ │ │ SetStaticPluginManager │ │ +│ │ │ │ └─────────────────────┘ │ +│ │ │ │ │ +│ │ ▼ Script Engine │ │ +│ │ └─ CSharpScriptEngine │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ └─────────────────────────────────┘ │ +│ │ +│ Workflow Script Execution │ +│ ────────────────────────┘ +└─────────────────────────────────────────┘ +``` + +## 🚀 使用方法 + +### 在 KitX Dashboard 中启用 RealPluginManager + +在 KitX Dashboard 启动时,`WorkflowScriptService` 的静态构造函数会自动执行以下操作: + +1. **获取 PluginsServer 实例**: 获取 `KitX.Dashboard.Network.PluginsNetwork.PluginsServer.Instance` +2. **设置工厂函数**: 调用 `Parser.SetPluginManagerFactory()` +3. **创建 RealPluginManager**: 传入 `PluginsServer` 实例和日志记录器 +4. **自动集成**: 后续的所有 Workflow Script 执行都将使用真实的插件系统 + +### 验证集成 + +集成成功后,您可以通过以下方式验证: + +1. **编译 Dashboard**: + ```bash + cd "KitX Clients/KitX Dashboard" + dotnet build + ``` + +2. **运行 Dashboard**: 启动后检查Logger输出 + ``` + [WorkflowScriptService] RealPluginManager 工厂函数已设置 + ``` + +3. **测试 Workflow Script**: 创建并执行包含插件调用的脚本 + +## 📝 代码示例 + +### 基本集成 +```csharp +// 在 Dashboard 的初始化代码中(通常在 App.xaml.cs 或 Program.cs) +// WorkflowScriptService 的静态构造函数会自动执行此代码 + +// 手动设置(可选) +Parser.SetPluginManagerFactory(() => new RealPluginManager( + KitX.Dashboard.Network.PluginsNetwork.PluginsServer.Instance, + message => Log.Information(message) // 使用 Serilog +)); +``` + +### 脚本中使用 +```csharp +// 现在脚本中可以直接调用,将通过真实的 KitX Dashboard 插件系统执行 +var result = SampleCalculator.Add(10, 20); +var text = StringToolkit.Reverse("Hello World"); +``` + +## ⚙️ 配置选项 + +### 工厂函数 vs 默认管理器 +- **工厂函数** (`SetPluginManagerFactory`): 推荐用于生产环境 +- **默认管理器** (`SetDefaultPluginManager`): 适用于简单场景或测试 + +### 日志记录器 +- **开发阶段**: 使用 `Console.WriteLine` 或 `Debug.WriteLine` +- **生产阶段**: 使用 `Serilog.Log.Information` 或其他日志框架 + +## 🔍 故障排除 + +### 常见问题 + +1. **编译错误**: 确保已正确引用 `Kscript.CSharp.Parser` 项目 +2. **运行时错误**: 检查 `PluginsServer.Instance` 是否可用 +3. **反射异常**: 确保 Dashboard 程序集已正确加载 + +### 调试技巧 + +1. **启用详细日志**: 在 `RealPluginManager` 构造函数中添加更多日志 +2. **检查插件状态**: 验证插件是否正确连接到 Dashboard +3. **测试单独组件**: 分别测试 `RealPluginManager` 和 `PluginsServer` 连接 + +## 📈 性能考虑 + +- ✅ **延迟初始化**: RealPluginManager 只在首次使用时创建 +- ✅ **缓存机制**: 保持原有的程序集缓存功能 +- ✅ **内存效率**: 静态插件管理器实例,避免重复创建 + +## 🎉 总结 + +通过这次集成,KitX.CSharp.Parser 现在完全支持: + +1. **真实的插件调用**: 通过 KitX Dashboard 的插件系统 +2. **灵活的配置**: 支持多种插件管理器实现 +3. **向后兼容性**: 不影响现有代码 +4. **生产就绪**: 具备完整的错误处理和日志记录 + +现在 Workflow Script 中的插件调用将直接通过 KitX Dashboard 的真实插件系统执行,实现了从模拟到真实的完整升级! diff --git a/KitX Script/Kscript.CSharp.Parser/Kscript.CSharp.Parser.csproj b/KitX Script/Kscript.CSharp.Parser/Kscript.CSharp.Parser.csproj index e873ef7..03feb01 100644 --- a/KitX Script/Kscript.CSharp.Parser/Kscript.CSharp.Parser.csproj +++ b/KitX Script/Kscript.CSharp.Parser/Kscript.CSharp.Parser.csproj @@ -11,6 +11,7 @@ + diff --git a/KitX Script/Kscript.CSharp.Parser/Parser.cs b/KitX Script/Kscript.CSharp.Parser/Parser.cs index 4d88ce5..c07ec35 100644 --- a/KitX Script/Kscript.CSharp.Parser/Parser.cs +++ b/KitX Script/Kscript.CSharp.Parser/Parser.cs @@ -16,6 +16,11 @@ public static class Parser /// private static IPluginManager? _defaultPluginManager; + /// + /// 插件管理器工厂函数 + /// + private static Func? _pluginManagerFactory; + /// /// 设置默认插件管理器 /// @@ -25,6 +30,15 @@ public static void SetDefaultPluginManager(IPluginManager pluginManager) _defaultPluginManager = pluginManager; } + /// + /// 设置插件管理器工厂函数 + /// + /// 插件管理器工厂函数 + public static void SetPluginManagerFactory(Func factory) + { + _pluginManagerFactory = factory; + } + /// /// 从插件信息列表生成动态程序集 /// @@ -43,7 +57,10 @@ public static Assembly Generate(List plugins, string assemblyName = try { - var manager = pluginManager ?? _defaultPluginManager ?? new MockPluginManager(); + var manager = pluginManager ?? _defaultPluginManager ?? _pluginManagerFactory?.Invoke() ?? new MockPluginManager(); + + // 设置静态插件管理器实例 + MethodEmitter.SetStaticPluginManager(manager); if (useCache) { @@ -146,7 +163,11 @@ public static Assembly Regenerate(List plugins, string assemblyName try { - var manager = pluginManager ?? _defaultPluginManager ?? new MockPluginManager(); + var manager = pluginManager ?? _defaultPluginManager ?? _pluginManagerFactory?.Invoke() ?? new MockPluginManager(); + + // 设置静态插件管理器实例 + MethodEmitter.SetStaticPluginManager(manager); + return AssemblyCache.ForceRegenerate(plugins, assemblyName, manager); } catch (Exception ex) when (!(ex is ParserException)) diff --git a/KitX Script/Kscript.CSharp.Parser/README.md b/KitX Script/Kscript.CSharp.Parser/README.md index 2530fdd..730f4c7 100644 --- a/KitX Script/Kscript.CSharp.Parser/README.md +++ b/KitX Script/Kscript.CSharp.Parser/README.md @@ -96,6 +96,62 @@ dotnet build dotnet run ``` +## 🔌 实际插件管理器集成 + +### 概述 + +KitX.CSharp.Parser 现在支持使用实际的插件管理器替换默认的 MockPluginManager,实现与 KitX Dashboard 的真实插件调用集成。 + +### 集成步骤 + +1. **设置插件管理器工厂** + ```csharp + // 在 KitX Dashboard 启动时设置 + Parser.SetPluginManagerFactory(() => new RealPluginManager( + KitX.Dashboard.Network.PluginsNetwork.PluginsServer.Instance, + message => Log.Information(message) + )); + ``` + +2. **生成插件程序集** + ```csharp + var assembly = Parser.Generate(plugins, "DashboardPluginAssembly"); + ``` + +3. **在脚本中使用** + ```csharp + // 现在可以直接调用,将通过真实的 KitX Dashboard 插件系统执行 + var result = SampleCalculator.Add(10, 20); + ``` + +### API 参考 + +#### 新增方法 + +| 方法 | 描述 | 参数 | +|------|------|------| +| `SetPluginManagerFactory()` | 设置插件管理器工厂函数 | `Func factory` | + +#### RealPluginManager 构造函数 + +| 参数 | 类型 | 描述 | +|------|------|------| +| `pluginsServer` | `object` | PluginsServer 实例 | +| `logger` | `Action` | 日志记录器 | + +### 使用示例 + +详细的使用示例请参考: +- `Examples/RealPluginManagerExample.cs` - 完整的使用示例 +- `Examples/Program.cs` - 演示程序入口 + +### 注意事项 + +- ✅ **依赖注入设计**:通过工厂函数模式实现松耦合 +- ✅ **日志统一**:支持外部传入日志记录器 +- ✅ **向后兼容**:仍支持 MockPluginManager 作为默认选项 +- ✅ **实际调用**:通过 KitX Dashboard 的插件系统进行真实调用 + --- ## 📁 项目结构 From fdbbfef2da2a4f6f430a72b40d8c2050104706a8 Mon Sep 17 00:00:00 2001 From: StarInk Date: Sat, 8 Nov 2025 02:17:25 +0100 Subject: [PATCH 10/60] =?UTF-8?q?=F0=9F=A7=A9=20Refactor(Parser):=20?= =?UTF-8?q?=E5=A4=A7=E5=B9=85=E4=BC=98=E5=8C=96=E4=BB=A3=E7=A0=81=E7=BB=93?= =?UTF-8?q?=E6=9E=84=EF=BC=8C=E5=88=A0=E9=99=A4=E4=B8=8D=E5=BF=85=E8=A6=81?= =?UTF-8?q?=E9=80=BB=E8=BE=91=E5=B9=B6=E4=BF=AE=E5=A4=8D=E6=BD=9C=E5=9C=A8?= =?UTF-8?q?=E9=9A=90=E6=82=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ARCHITECTURE_ANALYSIS.md | 285 --------------- .../Kscript.CSharp.Parser/CLASS_DIAGRAM.md | 343 ------------------ .../CodeGen/AssemblyCache.cs | 151 ++------ .../CodeGen/MethodEmitter.cs | 49 +-- .../CodeGen/TypeMapper.cs | 148 +------- .../Examples/BasicUsageExample.cs | 50 ++- .../Kscript.CSharp.Parser/Examples/Program.cs | 118 ++++++ .../Examples/RealPluginManagerExample.cs | 34 +- .../Exceptions/ParserException.cs | 24 -- .../INTEGRATION_GUIDE.md | 163 --------- KitX Script/Kscript.CSharp.Parser/Parser.cs | 112 +++--- 11 files changed, 266 insertions(+), 1211 deletions(-) delete mode 100644 KitX Script/Kscript.CSharp.Parser/ARCHITECTURE_ANALYSIS.md delete mode 100644 KitX Script/Kscript.CSharp.Parser/CLASS_DIAGRAM.md delete mode 100644 KitX Script/Kscript.CSharp.Parser/INTEGRATION_GUIDE.md diff --git a/KitX Script/Kscript.CSharp.Parser/ARCHITECTURE_ANALYSIS.md b/KitX Script/Kscript.CSharp.Parser/ARCHITECTURE_ANALYSIS.md deleted file mode 100644 index b78fc4b..0000000 --- a/KitX Script/Kscript.CSharp.Parser/ARCHITECTURE_ANALYSIS.md +++ /dev/null @@ -1,285 +0,0 @@ -# KitX.CSharp.Parser 架构分析与重构建议 - -## 🚨 当前架构问题分析 - -### 1. 循环依赖风险 - -```mermaid -graph TD - Parser --> MethodEmitter - MethodEmitter --> IPluginManager - Parser --> IPluginManager - MethodEmitter -.-> Parser -``` - -**问题描述:** -- [`Parser`](KitX Standard/KitX Script/Kscript.CSharp.Parser/Parser.cs:12) 设置插件管理器,但 [`MethodEmitter`](KitX Standard/KitX Script/Kscript.CSharp.Parser/CodeGen/MethodEmitter.cs:10) 也需要访问 -- [`MethodEmitter.SetStaticPluginManager()`](KitX Standard/KitX Script/Kscript.CSharp.Parser/CodeGen/MethodEmitter.cs:21) 创建了隐式依赖 - -### 2. 职责边界模糊 - -| 类 | 当前职责 | 问题 | -|------|----------|------| -| **Parser** | 入口+配置+协调+缓存管理 | 职责过多,违反单一职责原则 | -| **MethodEmitter** | IL生成+插件管理器管理 | 混合了生成逻辑和运行时逻辑 | -| **AssemblyCache** | 缓存+直接调用MethodEmitter | 缓存层不应该知道生成细节 | - -### 3. 跨层调用问题 - -```mermaid -graph LR - Parser[Parser 表现层] --> AssemblyCache[缓存层] - AssemblyCache --> MethodEmitter[生成层] - MethodEmitter --> TypeMapper[解析层] -``` - -**问题:** 缓存层直接调用生成层,破坏了分层架构的纯粹性 - -## 🎯 重构方案:单向数据流架构 - -### 核心设计原则 - -1. **数据只能向下流动**:上层 → 下层 -2. **配置只能横向注入**:配置层 → 各层 -3. **事件只能向上冒泡**:下层 → 上层(异常、状态) -4. **禁止跨层调用**:只能调用相邻层 - -### 📋 重构后的分层架构 - -```mermaid -graph TD - %% 输入层 - A[JSON 输入] --> B[Parser 输入层] - - %% 解析层 - B --> C[PluginInfo 解析层] - C --> D[TypeMapper 类型映射] - - %% 生成层 - D --> E[AssemblyBuilder 程序集构建器] - E --> F[MethodEmitter IL生成器] - - %% 缓存层 - F --> G[AssemblyCache 缓存层] - - %% 执行层 - G --> H[PluginManager 插件管理器] - H --> I[PluginCallInfo 调用信息] - - %% 输出层 - I --> J[动态程序集输出] - - %% 配置层(独立) - K[Configuration 配置层] -.-> B - K -.-> D - K -.-> G - K -.-> H - - %% 样式 - classDef inputLayer fill:#e1f5fe - classDef parseLayer fill:#f3e5f5 - classDef generateLayer fill:#e8f5e8 - classDef cacheLayer fill:#fff3e0 - classDef executeLayer fill:#fce4ec - classDef outputLayer fill:#e0f2f1 - classDef configLayer fill:#f1f8e9 - - class A,B inputLayer - class C,D parseLayer - class E,F generateLayer - class G cacheLayer - class H,I executeLayer - class J outputLayer - class K configLayer -``` - -### 🔧 重构后的类职责 - -| 层级 | 类名 | 职责 | 输入 | 输出 | -|------|------|------|------|------| -| **输入层** | Parser | 统一入口,参数验证 | JSON/PluginInfo | 验证后的数据 | -| **解析层** | TypeMapper | 类型映射,验证 | 类型字符串 | System.Type | -| **生成层** | AssemblyBuilder | 协调生成过程 | PluginInfo | IL代码 | -| | MethodEmitter | 纯IL生成 | 类型信息 | IL指令 | -| **缓存层** | AssemblyCache | 纯缓存管理 | 程序集 | 缓存的程序集 | -| **执行层** | PluginManager | 插件调用 | 调用信息 | 执行结果 | -| **配置层** | Configuration | 配置管理 | 配置数据 | 配置实例 | - -## 🛠️ 具体重构策略 - -### 1. 引入依赖注入容器 - -```csharp -// 替代静态依赖 -public class ServiceContainer -{ - public void ConfigureServices() - { - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - } -} -``` - -### 2. 接口隔离原则 - -```csharp -// 明确的接口定义 -public interface IAssemblyBuilder -{ - Assembly BuildFromPlugins(List plugins, string assemblyName); -} - -public interface IMethodEmitter -{ - void EmitMethod(TypeBuilder typeBuilder, Function function); -} - -public interface ITypeMapper -{ - Type MapType(string typeName); - void RegisterCustomType(string typeName, Type type); -} -``` - -### 3. 事件驱动通信 - -```csharp -// 替代直接调用 -public class AssemblyBuilder -{ - public event EventHandler AssemblyGenerated; - public event EventHandler MethodGenerated; - - public Assembly Build(List plugins) - { - // 生成逻辑 - foreach (var plugin in plugins) - { - foreach (var function in plugin.Functions) - { - MethodGenerated?.Invoke(this, new MethodGeneratedEventArgs(function)); - } - } - - var assembly = // ... 生成程序集 - AssemblyGenerated?.Invoke(this, new AssemblyGeneratedEventArgs(assembly)); - return assembly; - } -} -``` - -### 4. 配置管理分离 - -```csharp -public class ParserConfiguration -{ - public IPluginManager PluginManager { get; set; } - public bool UseCache { get; set; } = true; - public string DefaultAssemblyName { get; set; } = "DynamicPluginAssembly"; - public TimeSpan CacheTimeout { get; set; } = TimeSpan.FromHours(1); -} - -public class ConfigurationManager -{ - public static ParserConfiguration GetConfiguration() - { - // 从配置文件、环境变量等读取配置 - return new ParserConfiguration - { - UseCache = bool.Parse(Environment.GetEnvironmentVariable("KSCRIPT_CACHE") ?? "true"), - DefaultAssemblyName = Environment.GetEnvironmentVariable("KSCRIPT_ASSEMBLY_NAME") ?? "DynamicPluginAssembly" - }; - } -} -``` - -## 🔄 重构后的数据流 - -### 程序集生成流程 - -```mermaid -sequenceDiagram - participant Client - participant Parser - participant AssemblyBuilder - participant TypeMapper - participant MethodEmitter - participant AssemblyCache - participant PluginManager - - Client->>Parser: Generate(plugins) - Parser->>Parser: ValidateInput() - Parser->>AssemblyBuilder: Build(plugins) - - AssemblyBuilder->>TypeMapper: MapType(typeName) - TypeMapper-->>AssemblyBuilder: Type - - AssemblyBuilder->>MethodEmitter: EmitMethod() - MethodEmitter-->>AssemblyBuilder: IL Instructions - - AssemblyBuilder->>AssemblyCache: GetOrCreate(assembly) - AssemblyCache-->>AssemblyBuilder: Cached Assembly - - AssemblyBuilder-->>Parser: Assembly - Parser->>PluginManager: SetContext(assembly) - Parser-->>Client: Assembly -``` - -### 插件调用流程 - -```mermaid -sequenceDiagram - participant Script - participant GeneratedMethod - participant PluginManager - participant PluginConnector - participant PluginProcess - - Script->>GeneratedMethod: Plugin.Method(args) - GeneratedMethod->>PluginManager: Call(PluginCallInfo) - - PluginManager->>PluginConnector: FindConnector() - PluginManager->>PluginConnector: SendRequest() - PluginConnector->>PluginProcess: WebSocket Communication - PluginProcess-->>PluginConnector: Response - PluginConnector-->>PluginManager: HandleResponse() - PluginManager-->>GeneratedMethod: Result - GeneratedMethod-->>Script: Result -``` - -## ✅ 重构收益 - -1. **清晰的职责分离** - 每个类只负责一个明确的功能 -2. **单向数据流** - 避免循环依赖和混乱的调用关系 -3. **易于测试** - 每层可以独立测试 -4. **易于维护** - 修改一层不会影响其他层 -5. **可扩展性** - 新功能可以通过添加新层实现 -6. **配置灵活性** - 通过依赖注入支持不同的运行环境 - -## 🎯 实施步骤 - -1. **第一阶段:接口定义** - - 定义各层的接口 - - 确定数据契约 - -2. **第二阶段:依赖注入** - - 引入DI容器 - - 重构静态依赖 - -3. **第三阶段:事件驱动** - - 替换直接调用为事件 - - 实现观察者模式 - -4. **第四阶段:配置管理** - - 分离配置逻辑 - - 支持多环境配置 - -5. **第五阶段:测试验证** - - 单元测试各层 - - 集成测试整体流程 - -这样的重构将显著提升 KitX.CSharp.Parser 项目的可维护性和可扩展性,同时保持现有功能的完整性。 diff --git a/KitX Script/Kscript.CSharp.Parser/CLASS_DIAGRAM.md b/KitX Script/Kscript.CSharp.Parser/CLASS_DIAGRAM.md deleted file mode 100644 index 1ccf714..0000000 --- a/KitX Script/Kscript.CSharp.Parser/CLASS_DIAGRAM.md +++ /dev/null @@ -1,343 +0,0 @@ -# KitX.CSharp.Parser 类图与调用关系 - -## 📊 整体架构类图 - -```mermaid -classDiagram - %% 外部依赖 - class KitXDashboard { - +PluginsServer.Instance - +WorkflowScriptService - } - - class KitXShared { - <> - +PluginInfo - +Function - +Parameter - +Connector - +Request - } - - %% 主入口类 - class Parser { - <> - -_defaultPluginManager: IPluginManager - -_pluginManagerFactory: Func~IPluginManager~ - +Generate(plugins, assemblyName, pluginManager, useCache): Assembly - +GenerateFromJson(jsonString, assemblyName, pluginManager, useCache): Assembly - +GenerateFromFileAsync(jsonFilePath, assemblyName, pluginManager, useCache): Task~Assembly~ - +Regenerate(plugins, assemblyName, pluginManager): Assembly - +SetDefaultPluginManager(pluginManager): void - +SetPluginManagerFactory(factory): void - +RegisterCustomType(typeName, type): void - +ClearCache(): void - +GetCacheStatistics(): CacheStatistics - +HasCache(plugins, assemblyName): bool - +GetPluginTypes(assembly): List~Type~ - +GetPluginMethods(pluginType): List~MethodInfo~ - } - - %% 核心接口 - class IPluginManager { - <> - +Call~T~(callInfo): T - +Call(callInfo): void - +IsPluginExists(pluginName): bool - +IsMethodExists(pluginName, methodName): bool - } - - %% 插件管理器实现 - class MockPluginManager { - -_mockData: Dictionary~string, Dictionary~ - +Call~T~(callInfo): T - +Call(callInfo): void - +IsPluginExists(pluginName): bool - +IsMethodExists(pluginName, methodName): bool - -InvokeMockMethod(callInfo, method): object - -GetDefaultValue(type): object - } - - class RealPluginManager { - -_pluginsServer: object - -_infoLogger: Action~string~ - -_errorLogger: Action~string~ - -_pendingRequests: ConcurrentDictionary~string, TaskCompletionSource~ - +Call~T~(callInfo): T - +Call(callInfo): void - +IsPluginExists(pluginName): bool - +IsMethodExists(pluginName, methodName): bool - -FindPluginInfo(pluginName): PluginInfo - -FindPluginConnector(pluginInfo): object - -SendPluginRequest~T~(connector, callInfo): Task~T~ - -SendRequestToConnector(connector, request): Task - +HandlePluginResponse(requestId, responseJson): void - -GetDefaultResult~T~(): T - } - - %% 代码生成组件 - class MethodEmitter { - <> - -_staticPluginManager: IPluginManager - +SetStaticPluginManager(pluginManager): void - +GenerateAssembly(plugins, assemblyName, pluginManager): Assembly - -GeneratePluginClass(moduleBuilder, plugin, pluginManager): void - -GenerateStaticConstructor(typeBuilder, pluginManagerField, pluginManager): void - -GeneratePluginMethod(typeBuilder, pluginName, function, pluginManagerField): void - -GenerateMethodBody(methodBuilder, pluginName, function, parameterTypes, returnType, pluginManagerField): void - -ConvertDefaultValue(value, targetType): object - } - - class TypeMapper { - <> - -_basicTypeMap: Dictionary~string, Type~ - -_customTypeMap: ConcurrentDictionary~string, Type~ - -_typeCache: ConcurrentDictionary~string, Type~ - -_genericTypeRegex: Regex - +MapType(typeName): Type - -ResolveGenericType(genericMatch): Type - -ParseGenericArguments(argsString): Type[] - -SplitGenericArguments(argsString): string[] - +RegisterCustomType(typeName, type): void - +ClearCache(): void - +GetCustomTypes(): IReadOnlyDictionary~string, Type~ - } - - class AssemblyCache { - <> - -_assemblyCache: ConcurrentDictionary~string, Assembly~ - -_referenceCount: ConcurrentDictionary~string, int~ - -_cacheLock: object - +GetOrCreateAssembly(plugins, assemblyName, pluginManager): Assembly - -GenerateCacheKey(plugins, assemblyName): string - -IncrementReference(cacheKey): void - +DecrementReference(cacheKey): void - +ClearCache(): void - +GetStatistics(): CacheStatistics - +HasCache(plugins, assemblyName): bool - +ForceRegenerate(plugins, assemblyName, pluginManager): Assembly - } - - %% 数据模型 - class PluginCallInfo { - +PluginName: string - +MethodName: string - +Parameters: object[] - +ParameterTypes: Type[] - +ToString(): string - } - - class CacheStatistics { - +CachedAssemblyCount: int - +TotalReferences: int - +CacheKeys: List~string~ - +ToString(): string - } - - %% 异常类 - class ParserException { - +ParserException() - +ParserException(message) - +ParserException(message, innerException) - +TypeMappingError(typeName, innerException): ParserException - +ILGenerationError(methodName, innerException): ParserException - +AssemblyGenerationError(assemblyName, innerException): ParserException - } - - %% 示例类 - class BasicUsageExample { - <> - +RunExample(): Task - -RunWithBuiltInData(): void - -DynamicInvokeExample(assembly): void - -GenerateTestArguments(methodName, parameters): object[] - -GetDefaultValue(type): object - +RunAdvancedExample(): void - } - - class RealPluginManagerExample { - <> - +RunExample(): void - +DashboardIntegrationExample(): void - } - - %% 关系定义 - Parser --> IPluginManager : 使用 - Parser --> TypeMapper : 使用 - Parser --> AssemblyCache : 使用 - Parser --> MethodEmitter : 调用 - Parser --> CacheStatistics : 返回 - - IPluginManager <|.. MockPluginManager : 实现 - IPluginManager <|.. RealPluginManager : 实现 - - MockPluginManager --> PluginCallInfo : 使用 - RealPluginManager --> PluginCallInfo : 使用 - RealPluginManager --> KitXDashboard : 依赖 - RealPluginManager --> KitXShared : 使用 - - MethodEmitter --> IPluginManager : 使用 - MethodEmitter --> TypeMapper : 使用 - MethodEmitter --> PluginCallInfo : 使用 - MethodEmitter --> ParserException : 抛出 - - TypeMapper --> ParserException : 抛出 - - AssemblyCache --> MethodEmitter : 调用 - AssemblyCache --> CacheStatistics : 返回 - - BasicUsageExample --> Parser : 使用 - RealPluginManagerExample --> Parser : 使用 - RealPluginManagerExample --> RealPluginManager : 使用 - - KitXDashboard --> RealPluginManager : 创建 -``` - -## 🔄 调用时序图 - -### 基本生成流程 - -```mermaid -sequenceDiagram - participant Client - participant Parser - participant AssemblyCache - participant MethodEmitter - participant TypeMapper - participant IPluginManager - - Client->>Parser: Generate(plugins) - Parser->>AssemblyCache: GetOrCreateAssembly() - - alt 缓存命中 - AssemblyCache-->>Parser: 返回缓存的Assembly - else 缓存未命中 - AssemblyCache->>MethodEmitter: GenerateAssembly() - - loop 对每个插件 - MethodEmitter->>TypeMapper: MapType(typeName) - TypeMapper-->>MethodEmitter: 返回Type - - MethodEmitter->>MethodEmitter: GeneratePluginClass() - MethodEmitter->>MethodEmitter: GenerateStaticConstructor() - - loop 对每个功能 - MethodEmitter->>MethodEmitter: GeneratePluginMethod() - MethodEmitter->>MethodEmitter: GenerateMethodBody() - MethodEmitter->>IPluginManager: SetStaticPluginManager() - end - end - - MethodEmitter-->>AssemblyCache: 返回新Assembly - AssemblyCache-->>Parser: 返回Assembly - end - - Parser-->>Client: 返回Assembly -``` - -### 真实插件调用流程 - -```mermaid -sequenceDiagram - participant Script - participant GeneratedMethod - participant RealPluginManager - participant PluginConnector - participant PluginProcess - participant KitXDashboard - - Script->>GeneratedMethod: SampleCalculator.Add(10, 20) - GeneratedMethod->>RealPluginManager: Call(PluginCallInfo) - - RealPluginManager->>KitXDashboard: FindPluginInfo() - KitXDashboard-->>RealPluginManager: PluginInfo - - RealPluginManager->>KitXDashboard: FindPluginConnector() - KitXDashboard-->>RealPluginManager: PluginConnector - - RealPluginManager->>PluginConnector: SendRequest(Request) - PluginConnector->>PluginProcess: WebSocket 通信 - PluginProcess-->>PluginConnector: Response - PluginConnector-->>RealPluginManager: HandleResponse() - - RealPluginManager-->>GeneratedMethod: 返回结果 - GeneratedMethod-->>Script: 返回结果 -``` - -## 📋 类职责说明 - -### 核心类 - -| 类名 | 职责 | 关键方法 | -|------|------|----------| -| **Parser** | 主入口,统一API | `Generate()`, `GenerateFromJson()`, `GenerateFromFileAsync()` | -| **IPluginManager** | 插件管理器接口 | `Call()`, `IsPluginExists()` | -| **MockPluginManager** | 模拟插件管理器 | `Call()`, `InvokeMockMethod()` | -| **RealPluginManager** | 真实插件管理器 | `Call()`, `SendPluginRequest()` | - -### 代码生成类 - -| 类名 | 职责 | 关键方法 | -|------|------|----------| -| **MethodEmitter** | IL代码生成 | `GenerateAssembly()`, `GeneratePluginClass()` | -| **TypeMapper** | 类型映射 | `MapType()`, `RegisterCustomType()` | -| **AssemblyCache** | 程序集缓存 | `GetOrCreateAssembly()`, `ForceRegenerate()` | - -### 数据模型类 - -| 类名 | 职责 | 关键属性 | -|------|------|----------| -| **PluginCallInfo** | 插件调用信息 | `PluginName`, `MethodName`, `Parameters` | -| **CacheStatistics** | 缓存统计信息 | `CachedAssemblyCount`, `TotalReferences` | - -## 🔧 设计模式应用 - -### 1. 工厂模式 -- `Parser.SetPluginManagerFactory()` - 创建插件管理器实例 -- `MethodEmitter.GenerateAssembly()` - 创建动态程序集 - -### 2. 策略模式 -- `IPluginManager` 接口 - 不同的插件管理器实现策略 -- `MockPluginManager` vs `RealPluginManager` - 测试vs生产策略 - -### 3. 单例模式 -- `AssemblyCache` - 静态缓存管理 -- `TypeMapper` - 静态类型映射 - -### 4. 建造者模式 -- `MethodEmitter` 中的 IL 生成过程 -- `Connector` 的请求构建过程 - -### 5. 模板方法模式 -- `Parser.Generate()` 方法的通用流程 -- 插件方法生成的标准流程 - -## 🎯 关键调用路径 - -### 1. 程序集生成路径 -``` -Parser.Generate() -→ AssemblyCache.GetOrCreateAssembly() -→ MethodEmitter.GenerateAssembly() -→ TypeMapper.MapType() -→ IL生成 -``` - -### 2. 插件调用路径 -``` -生成的静态方法 -→ IPluginManager.Call() -→ RealPluginManager.SendPluginRequest() -→ WebSocket通信 -→ 插件进程 -``` - -### 3. 缓存管理路径 -``` -AssemblyCache.GenerateCacheKey() -→ SHA256哈希计算 -→ 缓存查找/存储 -→ 引用计数管理 -``` - -这个类图展示了 KitX.CSharp.Parser 项目的完整架构,包括各个类之间的依赖关系、调用顺序和设计模式的应用。 diff --git a/KitX Script/Kscript.CSharp.Parser/CodeGen/AssemblyCache.cs b/KitX Script/Kscript.CSharp.Parser/CodeGen/AssemblyCache.cs index 26e4ce0..50f8d8a 100644 --- a/KitX Script/Kscript.CSharp.Parser/CodeGen/AssemblyCache.cs +++ b/KitX Script/Kscript.CSharp.Parser/CodeGen/AssemblyCache.cs @@ -1,7 +1,5 @@ using System.Collections.Concurrent; -using System.Security.Cryptography; using System.Text; -using System.Text.Json; namespace Kscript.CSharp.Parser.CodeGen; @@ -15,16 +13,6 @@ public static class AssemblyCache /// private static readonly ConcurrentDictionary _assemblyCache = new(); - /// - /// 程序集引用计数,用于清理不再使用的程序集 - /// - private static readonly ConcurrentDictionary _referenceCount = new(); - - /// - /// 缓存锁,确保并发安全 - /// - private static readonly object _cacheLock = new(); - /// /// 获取或生成程序集 /// @@ -32,35 +20,23 @@ public static class AssemblyCache /// 程序集名称 /// 插件管理器实例 /// 缓存的或新生成的程序集 - public static Assembly GetOrCreateAssembly(List plugins, string assemblyName = "DynamicPluginAssembly", Core.IPluginManager? pluginManager = null) + public static Assembly GetOrCreateAssembly(List plugins, string assemblyName, Core.IPluginManager pluginManager) { var cacheKey = GenerateCacheKey(plugins, assemblyName); // 尝试从缓存获取 if (_assemblyCache.TryGetValue(cacheKey, out var cachedAssembly)) { - IncrementReference(cacheKey); return cachedAssembly; } - lock (_cacheLock) - { - // 双重检查锁定模式 - if (_assemblyCache.TryGetValue(cacheKey, out cachedAssembly)) - { - IncrementReference(cacheKey); - return cachedAssembly; - } - - // 生成新的程序集 - var newAssembly = MethodEmitter.GenerateAssembly(plugins, assemblyName, pluginManager); + // 生成新的程序集 + var newAssembly = MethodEmitter.GenerateAssembly(plugins, assemblyName, pluginManager); - // 添加到缓存 - _assemblyCache[cacheKey] = newAssembly; - _referenceCount[cacheKey] = 1; + // 添加到缓存 + _assemblyCache.TryAdd(cacheKey, newAssembly); - return newAssembly; - } + return newAssembly; } /// @@ -71,81 +47,21 @@ public static Assembly GetOrCreateAssembly(List plugins, string asse /// 缓存键字符串 private static string GenerateCacheKey(List plugins, string assemblyName) { - try - { - // 创建用于哈希的数据对象 - var cacheData = new - { - AssemblyName = assemblyName, - Plugins = plugins.Select(p => new - { - p.Name, - p.Version, - Functions = p.Functions.Select(f => new - { - f.Name, - f.ReturnValueType, - Parameters = f.Parameters.Select(param => new - { - param.Name, - param.Type, - param.IsOptional, - param.Value - }).OrderBy(param => param.Name) - }).OrderBy(f => f.Name) - }).OrderBy(p => p.Name) - }; - - // 序列化为JSON - var json = JsonSerializer.Serialize(cacheData, new JsonSerializerOptions - { - WriteIndented = false, - PropertyNamingPolicy = JsonNamingPolicy.CamelCase - }); - - // 计算SHA256哈希 - using var sha256 = SHA256.Create(); - var hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(json)); - return Convert.ToHexString(hashBytes); - } - catch (Exception ex) - { - // 如果哈希计算失败,使用基础信息生成简单键 - var fallbackKey = $"{assemblyName}_{plugins.Count}_{string.Join("_", plugins.Select(p => $"{p.Name}_{p.Functions.Count}"))}_{DateTime.UtcNow.Ticks}"; - Console.WriteLine($"[AssemblyCache] 哈希计算失败,使用回退键: {ex.Message}"); - return fallbackKey; - } - } - - /// - /// 增加引用计数 - /// - /// 缓存键 - private static void IncrementReference(string cacheKey) - { - _referenceCount.AddOrUpdate(cacheKey, 1, (key, count) => count + 1); - } + // 使用轻量级字符串哈希,避免复杂的JSON序列化和SHA256计算 + var keyBuilder = new StringBuilder(); + keyBuilder.Append(assemblyName); + keyBuilder.Append($"|{plugins.Count}"); - /// - /// 减少引用计数 - /// - /// 缓存键 - public static void DecrementReference(string cacheKey) - { - if (_referenceCount.TryGetValue(cacheKey, out var count)) + foreach (var plugin in plugins.OrderBy(p => p.Name)) { - var newCount = count - 1; - if (newCount <= 0) - { - // 引用计数为0,从缓存中移除 - _assemblyCache.TryRemove(cacheKey, out _); - _referenceCount.TryRemove(cacheKey, out _); - } - else + keyBuilder.Append($"|{plugin.Name}:{plugin.Version}:{plugin.Functions.Count}"); + foreach (var function in plugin.Functions.OrderBy(f => f.Name)) { - _referenceCount[cacheKey] = newCount; + keyBuilder.Append($":{function.Name}:{function.ReturnValueType}:{function.Parameters.Count}"); } } + + return keyBuilder.ToString().GetHashCode().ToString("X"); } /// @@ -153,11 +69,7 @@ public static void DecrementReference(string cacheKey) /// public static void ClearCache() { - lock (_cacheLock) - { - _assemblyCache.Clear(); - _referenceCount.Clear(); - } + _assemblyCache.Clear(); } /// @@ -169,7 +81,6 @@ public static CacheStatistics GetStatistics() return new CacheStatistics { CachedAssemblyCount = _assemblyCache.Count, - TotalReferences = _referenceCount.Values.Sum(), CacheKeys = _assemblyCache.Keys.ToList() }; } @@ -193,25 +104,20 @@ public static bool HasCache(List plugins, string assemblyName = "Dyn /// 程序集名称 /// 插件管理器实例 /// 新生成的程序集 - public static Assembly ForceRegenerate(List plugins, string assemblyName = "DynamicPluginAssembly", Core.IPluginManager? pluginManager = null) + public static Assembly ForceRegenerate(List plugins, Core.IPluginManager pluginManager, string assemblyName = "DynamicPluginAssembly") { var cacheKey = GenerateCacheKey(plugins, assemblyName); - lock (_cacheLock) - { - // 移除现有缓存 - _assemblyCache.TryRemove(cacheKey, out _); - _referenceCount.TryRemove(cacheKey, out _); + // 移除现有缓存 + _assemblyCache.TryRemove(cacheKey, out _); - // 生成新的程序集 - var newAssembly = MethodEmitter.GenerateAssembly(plugins, assemblyName, pluginManager); + // 生成新的程序集 + var newAssembly = MethodEmitter.GenerateAssembly(plugins, assemblyName, pluginManager); - // 添加到缓存 - _assemblyCache[cacheKey] = newAssembly; - _referenceCount[cacheKey] = 1; + // 添加到缓存 + _assemblyCache.TryAdd(cacheKey, newAssembly); - return newAssembly; - } + return newAssembly; } } @@ -225,11 +131,6 @@ public class CacheStatistics /// public int CachedAssemblyCount { get; set; } - /// - /// 总引用数量 - /// - public int TotalReferences { get; set; } - /// /// 缓存键列表 /// @@ -237,6 +138,6 @@ public class CacheStatistics public override string ToString() { - return $"缓存统计: {CachedAssemblyCount} 个程序集, {TotalReferences} 个引用"; + return $"缓存统计: {CachedAssemblyCount} 个程序集"; } } diff --git a/KitX Script/Kscript.CSharp.Parser/CodeGen/MethodEmitter.cs b/KitX Script/Kscript.CSharp.Parser/CodeGen/MethodEmitter.cs index 7e9f713..1fe2cc8 100644 --- a/KitX Script/Kscript.CSharp.Parser/CodeGen/MethodEmitter.cs +++ b/KitX Script/Kscript.CSharp.Parser/CodeGen/MethodEmitter.cs @@ -9,19 +9,6 @@ namespace Kscript.CSharp.Parser.CodeGen; /// public static class MethodEmitter { - /// - /// 静态插件管理器实例,用于在生成的程序集中使用 - /// - private static IPluginManager? _staticPluginManager; - - /// - /// 设置静态插件管理器实例 - /// - /// 插件管理器实例 - public static void SetStaticPluginManager(IPluginManager pluginManager) - { - _staticPluginManager = pluginManager; - } /// /// 为插件生成动态程序集 /// @@ -29,12 +16,13 @@ public static void SetStaticPluginManager(IPluginManager pluginManager) /// 程序集名称 /// 插件管理器实例 /// 生成的动态程序集 - public static Assembly GenerateAssembly(List plugins, string assemblyName = "DynamicPluginAssembly", IPluginManager? pluginManager = null) + public static Assembly GenerateAssembly(List plugins, + string assemblyName, IPluginManager pluginManager) { try { // 创建动态程序集 - var assemblyBuilder = System.Reflection.Emit.AssemblyBuilder.DefineDynamicAssembly( + var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly( new AssemblyName(assemblyName), AssemblyBuilderAccess.Run); @@ -50,14 +38,14 @@ public static Assembly GenerateAssembly(List plugins, string assembl } catch (Exception ex) { - throw ParserException.AssemblyGenerationError(assemblyName, ex); + throw new ParserException($"生成程序集失败: {assemblyName}", ex); } } /// /// 为单个插件生成静态类 /// - private static void GeneratePluginClass(ModuleBuilder moduleBuilder, PluginInfo plugin, IPluginManager? pluginManager) + private static void GeneratePluginClass(ModuleBuilder moduleBuilder, PluginInfo plugin, IPluginManager pluginManager) { try { @@ -86,14 +74,14 @@ private static void GeneratePluginClass(ModuleBuilder moduleBuilder, PluginInfo } catch (Exception ex) { - throw ParserException.ILGenerationError($"{plugin.Name} 类", ex); + throw new ParserException($"生成IL代码失败: {plugin.Name} 类", ex); } } /// /// 生成静态构造函数 /// - private static void GenerateStaticConstructor(TypeBuilder typeBuilder, FieldBuilder pluginManagerField, IPluginManager? pluginManager) + private static void GenerateStaticConstructor(TypeBuilder typeBuilder, FieldBuilder pluginManagerField, IPluginManager pluginManager) { var constructorBuilder = typeBuilder.DefineConstructor( MethodAttributes.Private | MethodAttributes.Static | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName, @@ -102,22 +90,11 @@ private static void GenerateStaticConstructor(TypeBuilder typeBuilder, FieldBuil var il = constructorBuilder.GetILGenerator(); - // 优先使用静态插件管理器实例 - var managerToUse = _staticPluginManager ?? pluginManager; - - if (managerToUse != null) - { - // 如果提供了插件管理器实例,直接使用 - il.Emit(OpCodes.Ldtoken, managerToUse.GetType()); - il.Emit(OpCodes.Call, typeof(Type).GetMethod("GetTypeFromHandle")!); - il.Emit(OpCodes.Call, typeof(Activator).GetMethod("CreateInstance", new[] { typeof(Type) })!); - il.Emit(OpCodes.Castclass, typeof(IPluginManager)); - } - else - { - // 使用默认的 MockPluginManager - il.Emit(OpCodes.Newobj, typeof(MockPluginManager).GetConstructor(Type.EmptyTypes)!); - } + // 使用提供的插件管理器实例 + il.Emit(OpCodes.Ldtoken, pluginManager.GetType()); + il.Emit(OpCodes.Call, typeof(Type).GetMethod("GetTypeFromHandle")!); + il.Emit(OpCodes.Call, typeof(Activator).GetMethod("CreateInstance", new[] { typeof(Type) })!); + il.Emit(OpCodes.Castclass, typeof(IPluginManager)); il.Emit(OpCodes.Stsfld, pluginManagerField); il.Emit(OpCodes.Ret); @@ -167,7 +144,7 @@ private static void GeneratePluginMethod(TypeBuilder typeBuilder, string pluginN } catch (Exception ex) { - throw ParserException.ILGenerationError($"{pluginName}.{function.Name}", ex); + throw new ParserException($"生成IL代码失败: {pluginName}.{function.Name}", ex); } } diff --git a/KitX Script/Kscript.CSharp.Parser/CodeGen/TypeMapper.cs b/KitX Script/Kscript.CSharp.Parser/CodeGen/TypeMapper.cs index 061a31f..1810ffb 100644 --- a/KitX Script/Kscript.CSharp.Parser/CodeGen/TypeMapper.cs +++ b/KitX Script/Kscript.CSharp.Parser/CodeGen/TypeMapper.cs @@ -1,5 +1,4 @@ using System.Collections.Concurrent; -using System.Text.RegularExpressions; namespace Kscript.CSharp.Parser.CodeGen; @@ -31,21 +30,11 @@ public static class TypeMapper { "object", typeof(object) } }; - /// - /// 自定义类型映射表 - /// - private static readonly ConcurrentDictionary _customTypeMap = new(); - /// /// 类型缓存 /// private static readonly ConcurrentDictionary _typeCache = new(); - /// - /// 泛型类型正则表达式 - /// - private static readonly Regex _genericTypeRegex = new(@"^(\w+)<(.+)>$", RegexOptions.Compiled); - /// /// 映射字符串类型名到 System.Type /// @@ -60,138 +49,31 @@ public static Type MapType(string typeName) if (_typeCache.TryGetValue(typeName, out var cachedType)) return cachedType; - Type? resolvedType = null; + // 检查基础类型 + if (_basicTypeMap.TryGetValue(typeName, out var basicType)) + { + _typeCache[typeName] = basicType; + return basicType; + } + // 尝试直接解析类型 try { - // 1. 检查基础类型 - if (_basicTypeMap.TryGetValue(typeName, out resolvedType)) - { - _typeCache[typeName] = resolvedType; - return resolvedType; - } - - // 2. 检查自定义映射 - if (_customTypeMap.TryGetValue(typeName, out resolvedType)) - { - _typeCache[typeName] = resolvedType; - return resolvedType; - } - - // 3. 处理泛型类型 - var genericMatch = _genericTypeRegex.Match(typeName); - if (genericMatch.Success) - { - resolvedType = ResolveGenericType(genericMatch); - if (resolvedType != null) - { - _typeCache[typeName] = resolvedType; - return resolvedType; - } - } - - // 4. 尝试直接解析类型 - resolvedType = Type.GetType(typeName); + var resolvedType = Type.GetType(typeName); if (resolvedType != null) { _typeCache[typeName] = resolvedType; return resolvedType; } - - // 5. 回退到 object 类型 - resolvedType = typeof(object); - _typeCache[typeName] = resolvedType; - return resolvedType; } catch { // 发生异常时回退到 object 类型 - resolvedType = typeof(object); - _typeCache[typeName] = resolvedType; - return resolvedType; } - } - - /// - /// 解析泛型类型 - /// - private static Type? ResolveGenericType(Match genericMatch) - { - var genericTypeName = genericMatch.Groups[1].Value; - var genericArgs = genericMatch.Groups[2].Value; - - // 解析泛型参数 - var argTypes = ParseGenericArguments(genericArgs); - if (argTypes.Length == 0) - return null; - // 处理常见的泛型类型 - return genericTypeName switch - { - "List" => typeof(List<>).MakeGenericType(argTypes), - "Dictionary" when argTypes.Length == 2 => typeof(Dictionary<,>).MakeGenericType(argTypes), - "Array" => argTypes[0].MakeArrayType(), - "Nullable" => typeof(Nullable<>).MakeGenericType(argTypes), - _ => null - }; - } - - /// - /// 解析泛型参数 - /// - private static Type[] ParseGenericArguments(string argsString) - { - var args = SplitGenericArguments(argsString); - return args.Select(MapType).ToArray(); - } - - /// - /// 分割泛型参数字符串,处理嵌套泛型 - /// - private static string[] SplitGenericArguments(string argsString) - { - var args = new List(); - var current = ""; - var depth = 0; - - foreach (char c in argsString) - { - if (c == '<') - { - depth++; - current += c; - } - else if (c == '>') - { - depth--; - current += c; - } - else if (c == ',' && depth == 0) - { - args.Add(current.Trim()); - current = ""; - } - else - { - current += c; - } - } - - if (!string.IsNullOrEmpty(current)) - args.Add(current.Trim()); - - return args.ToArray(); - } - - /// - /// 注册自定义类型映射 - /// - /// 类型名字符串 - /// 对应的 Type 对象 - public static void RegisterCustomType(string typeName, Type type) - { - _customTypeMap[typeName] = type; - _typeCache.TryRemove(typeName, out _); // 清除缓存 + // 回退到 object 类型 + _typeCache[typeName] = typeof(object); + return typeof(object); } /// @@ -201,12 +83,4 @@ public static void ClearCache() { _typeCache.Clear(); } - - /// - /// 获取所有已注册的自定义类型 - /// - public static IReadOnlyDictionary GetCustomTypes() - { - return _customTypeMap.AsReadOnly(); - } } diff --git a/KitX Script/Kscript.CSharp.Parser/Examples/BasicUsageExample.cs b/KitX Script/Kscript.CSharp.Parser/Examples/BasicUsageExample.cs index f2e6fdc..3a35e74 100644 --- a/KitX Script/Kscript.CSharp.Parser/Examples/BasicUsageExample.cs +++ b/KitX Script/Kscript.CSharp.Parser/Examples/BasicUsageExample.cs @@ -18,6 +18,14 @@ public static async Task RunExample() try { + // 0. 初始化插件管理器 + Console.WriteLine("0. 初始化插件管理器..."); + if (!Parser.IsInitialized) + { + Parser.SetPluginManager(new MockPluginManager()); + Console.WriteLine(" ✓ 已设置 MockPluginManager"); + } + // 1. 从现有的 example.json 文件加载插件清单 Console.WriteLine("1. 加载插件清单..."); var exampleJsonPath = Path.Combine(".", "example.json"); @@ -59,9 +67,28 @@ public static async Task RunExample() // 5. 演示缓存效果 Console.WriteLine("\n5. 测试缓存效果..."); + + // 5.1 先检查是否存在缓存 + var plugins = JsonSerializer.Deserialize>(await File.ReadAllTextAsync(exampleJsonPath), new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + AllowTrailingCommas = true + }); + var hasCacheBefore = Parser.HasCache(plugins!, "ExamplePluginAssembly"); + Console.WriteLine($" 生成前检查缓存存在: {hasCacheBefore}"); + + // 5.2 第二次生成(应该使用缓存) var assembly2 = await Parser.GenerateFromFileAsync(exampleJsonPath, "ExamplePluginAssembly"); Console.WriteLine($" 第二次生成是否使用缓存: {assembly == assembly2}"); + // 5.3 强制重新生成(应该绕过缓存) + var assembly3 = Parser.Generate(plugins!, "ExamplePluginAssembly", useCache: false); + Console.WriteLine($" 强制重新生成是否使用新程序集: {assembly3 != assembly2}"); + + // 5.4 检查强制重新生成后的缓存状态 + var hasCacheAfterRegenerate = Parser.HasCache(plugins!, "ExamplePluginAssembly"); + Console.WriteLine($" 重新生成后缓存存在: {hasCacheAfterRegenerate}"); + } catch (Exception ex) { @@ -75,6 +102,12 @@ public static async Task RunExample() /// private static void RunWithBuiltInData() { + // 确保插件管理器已初始化 + if (!Parser.IsInitialized) + { + Parser.SetPluginManager(new MockPluginManager()); + } + // 创建示例插件数据 var plugins = new List { @@ -211,20 +244,15 @@ public static void RunAdvancedExample() try { - // 1. 注册自定义类型 - Console.WriteLine("1. 注册自定义类型映射..."); - Parser.RegisterCustomType("CustomType", typeof(DateTime)); - Console.WriteLine(" ✓ 已注册 CustomType -> DateTime"); - - // 2. 清除缓存 - Console.WriteLine("\n2. 清除所有缓存..."); + // 1. 清除缓存 + Console.WriteLine("1. 清除所有缓存..."); Parser.ClearCache(); Console.WriteLine(" ✓ 缓存已清除"); - // 3. 设置自定义插件管理器 - Console.WriteLine("\n3. 设置自定义插件管理器..."); - Parser.SetDefaultPluginManager(new MockPluginManager()); - Console.WriteLine(" ✓ 已设置默认插件管理器"); + // 2. 重新设置插件管理器 + Console.WriteLine("\n2. 重新设置插件管理器..."); + Parser.SetPluginManager(new MockPluginManager()); + Console.WriteLine(" ✓ 已重新设置插件管理器"); } catch (Exception ex) diff --git a/KitX Script/Kscript.CSharp.Parser/Examples/Program.cs b/KitX Script/Kscript.CSharp.Parser/Examples/Program.cs index 51b3d0f..142351e 100644 --- a/KitX Script/Kscript.CSharp.Parser/Examples/Program.cs +++ b/KitX Script/Kscript.CSharp.Parser/Examples/Program.cs @@ -1,4 +1,5 @@ using Kscript.CSharp.Parser.Examples; +using Kscript.CSharp.Parser.Core; namespace Kscript.CSharp.Parser.Examples; @@ -18,6 +19,12 @@ public static async Task Main(string[] args) try { + // 首先运行简化后的功能测试 + Console.WriteLine("=== 测试简化后的 Parser 功能 ==="); + TestSimplifiedParser(); + + Console.WriteLine("\n" + new string('=', 60)); + // 运行基础用法示例 await BasicUsageExample.RunExample(); @@ -43,4 +50,115 @@ public static async Task Main(string[] args) Console.WriteLine("\n按任意键退出..."); Console.ReadKey(); } + + /// + /// 测试简化后的 Parser 功能 + /// + private static void TestSimplifiedParser() + { + Console.WriteLine("1. 测试默认插件管理器设置..."); + + // 测试使用 MockPluginManager 中已有的插件 + var testPlugins = new List + { + new PluginInfo + { + Name = "SampleCalculator", + Version = "1.0.0", + Functions = new List + { + new Function + { + Name = "Add", + ReturnValueType = "int", + Parameters = new List + { + new Parameter { Name = "a", Type = "int", IsOptional = false }, + new Parameter { Name = "b", Type = "int", IsOptional = false } + } + } + } + } + }; + + try + { + // 先设置插件管理器 + Parser.SetPluginManager(new MockPluginManager()); + + // 使用 MockPluginManager 生成程序集 + var assembly = Parser.Generate(testPlugins, "TestAssembly"); + Console.WriteLine(" ✓ 默认 MockPluginManager 工作正常"); + + // 获取生成的类型 + var types = Parser.GetPluginTypes(assembly); + Console.WriteLine($" ✓ 生成了 {types.Count} 个插件类型"); + + foreach (var type in types) + { + var methods = Parser.GetPluginMethods(type); + Console.WriteLine($" - {type.Name}: {methods.Count} 个方法"); + } + } + catch (Exception ex) + { + Console.WriteLine($" ❌ 测试失败: {ex.Message}"); + } + + Console.WriteLine("2. 测试自定义插件管理器设置..."); + try + { + // 设置新的插件管理器 + Parser.SetPluginManager(new MockPluginManager()); + var assembly2 = Parser.Generate(testPlugins, "TestAssembly2"); + Console.WriteLine(" ✓ 自定义插件管理器设置工作正常"); + } + catch (Exception ex) + { + Console.WriteLine($" ❌ 自定义设置测试失败: {ex.Message}"); + } + + Console.WriteLine("3. 测试空参数处理..."); + try + { + // 测试 null 参数应该抛出异常 + Parser.SetPluginManager(null); + Console.WriteLine(" ❌ 空参数测试失败: 应该抛出异常但没有"); + } + catch (ArgumentNullException) + { + Console.WriteLine(" ✓ 空参数正确抛出 ArgumentNullException"); + } + catch (Exception ex) + { + Console.WriteLine($" ❌ 空参数测试失败: {ex.Message}"); + } + + Console.WriteLine("4. 测试插件存在性检测..."); + try + { + var mockManager = new MockPluginManager(); + Parser.SetPluginManager(mockManager); + + // 测试存在的插件 + var existsSampleCalculator = mockManager.IsPluginExists("SampleCalculator"); + Console.WriteLine($" ✓ SampleCalculator 存在: {existsSampleCalculator}"); + + var existsAddMethod = mockManager.IsMethodExists("SampleCalculator", "Add"); + Console.WriteLine($" ✓ SampleCalculator.Add 方法存在: {existsAddMethod}"); + + // 测试不存在的插件 + var existsNonExistent = mockManager.IsPluginExists("NonExistentPlugin"); + Console.WriteLine($" ✓ NonExistentPlugin 存在: {existsNonExistent}"); + + var existsNonExistentMethod = mockManager.IsMethodExists("SampleCalculator", "NonExistentMethod"); + Console.WriteLine($" ✓ SampleCalculator.NonExistentMethod 方法存在: {existsNonExistentMethod}"); + + Console.WriteLine(" ✓ 插件存在性检测功能工作正常"); + } + catch (Exception ex) + { + Console.WriteLine($" ❌ 插件存在性检测测试失败: {ex.Message}"); + } + } } diff --git a/KitX Script/Kscript.CSharp.Parser/Examples/RealPluginManagerExample.cs b/KitX Script/Kscript.CSharp.Parser/Examples/RealPluginManagerExample.cs index 06a7070..937a3ad 100644 --- a/KitX Script/Kscript.CSharp.Parser/Examples/RealPluginManagerExample.cs +++ b/KitX Script/Kscript.CSharp.Parser/Examples/RealPluginManagerExample.cs @@ -18,24 +18,21 @@ public static void RunExample() try { - // 1. 设置插件管理器工厂函数 - Console.WriteLine("1. 设置插件管理器工厂函数..."); + // 1. 设置插件管理器 + Console.WriteLine("1. 设置插件管理器..."); // 这里应该传入实际的 PluginsServer 实例和日志记录器 // 由于这是一个示例,我们使用 null 作为占位符 - Parser.SetPluginManagerFactory(() => - { - // 在实际使用中,这里应该传入真实的 PluginsServer 实例和日志记录器 - // 例如: - // var pluginsServer = KitX.Dashboard.Network.PluginsNetwork.PluginsServer.Instance; - // var logger = Serilog.Log.Logger; - // return new RealPluginManager(pluginsServer, message => logger.Information(message)); + // 在实际使用中,这里应该传入真实的 PluginsServer 实例和日志记录器 + // 例如: + // var pluginsServer = KitX.Dashboard.Network.PluginsNetwork.PluginsServer.Instance; + // var logger = Serilog.Log.Logger; + // Parser.SetPluginManager(new RealPluginManager(pluginsServer, message => logger.Information(message))); - Console.WriteLine(" 创建 RealPluginManager 实例..."); - return null; // 示例中返回 null,实际使用时返回真实实例 - }); + Console.WriteLine(" 使用 MockPluginManager 进行演示..."); + Parser.SetPluginManager(new MockPluginManager()); - Console.WriteLine(" ✓ 插件管理器工厂函数已设置"); + Console.WriteLine(" ✓ 插件管理器已设置"); // 2. 创建示例插件数据 Console.WriteLine("\n2. 创建示例插件数据..."); @@ -43,17 +40,18 @@ public static void RunExample() { new PluginInfo { - Name = "TestPlugin", + Name = "SampleCalculator", Version = "1.0.0", Functions = new List { new Function { - Name = "TestMethod", - ReturnValueType = "string", + Name = "Add", + ReturnValueType = "int", Parameters = new List { - new Parameter { Name = "input", Type = "string", IsOptional = false } + new Parameter { Name = "a", Type = "int", IsOptional = false }, + new Parameter { Name = "b", Type = "int", IsOptional = false } } } } @@ -123,7 +121,7 @@ public static void DashboardIntegrationExample() Console.WriteLine("\n3. 在脚本中使用生成的 API:"); Console.WriteLine("```csharp"); Console.WriteLine("// 现在可以在脚本中直接调用"); - Console.WriteLine("var result = TestPlugin.TestMethod(\"Hello World\");"); + Console.WriteLine("var result = SampleCalculator.Add(10, 20);"); Console.WriteLine("```"); Console.WriteLine("\n这样,脚本中的插件调用将通过真实的 KitX Dashboard 插件系统执行!"); diff --git a/KitX Script/Kscript.CSharp.Parser/Exceptions/ParserException.cs b/KitX Script/Kscript.CSharp.Parser/Exceptions/ParserException.cs index 5ba8106..a46417e 100644 --- a/KitX Script/Kscript.CSharp.Parser/Exceptions/ParserException.cs +++ b/KitX Script/Kscript.CSharp.Parser/Exceptions/ParserException.cs @@ -16,28 +16,4 @@ public ParserException(string message) : base(message) public ParserException(string message, Exception innerException) : base(message, innerException) { } - - /// - /// 创建类型映射异常 - /// - public static ParserException TypeMappingError(string typeName, Exception? innerException = null) - { - return new ParserException($"无法映射类型: {typeName}", innerException!); - } - - /// - /// 创建IL生成异常 - /// - public static ParserException ILGenerationError(string methodName, Exception? innerException = null) - { - return new ParserException($"生成IL代码失败: {methodName}", innerException!); - } - - /// - /// 创建程序集生成异常 - /// - public static ParserException AssemblyGenerationError(string assemblyName, Exception? innerException = null) - { - return new ParserException($"生成程序集失败: {assemblyName}", innerException!); - } } diff --git a/KitX Script/Kscript.CSharp.Parser/INTEGRATION_GUIDE.md b/KitX Script/Kscript.CSharp.Parser/INTEGRATION_GUIDE.md deleted file mode 100644 index 4fd92b9..0000000 --- a/KitX Script/Kscript.CSharp.Parser/INTEGRATION_GUIDE.md +++ /dev/null @@ -1,163 +0,0 @@ -# KitX.CSharp.Parser 与 KitX Dashboard 集成指南 - -## 🎯 集成概述 - -本文档说明如何将 KitX.CSharp.Parser 的实际插件管理器集成到 KitX Dashboard 中,实现从 MockPluginManager 到真实插件调用的升级。 - -## 📋 完成的集成工作 - -### 1. 创建 RealPluginManager -- ✅ **文件**: [`Core/RealPluginManager.cs`](../Core/RealPluginManager.cs:1-225) -- ✅ **功能**: 实现了 `IPluginManager` 接口,支持真实的 KitX Dashboard 插件调用 -- ✅ **设计**: 采用依赖注入模式,接受外部传入的 `PluginsServer` 实例和日志记录器 -- ✅ **兼容性**: 完全兼容现有的 MockPluginManager 接口 - -### 2. 扩展 MethodEmitter -- ✅ **静态支持**: 在 [`CodeGen/MethodEmitter.cs`](../CodeGen/MethodEmitter.cs:13-21) 中添加了静态插件管理器支持 -- ✅ **工厂模式**: 实现了 `SetStaticPluginManager()` 方法 -- ✅ **灵活配置**: 支持多种插件管理器实现方式 - -### 3. 更新 Parser 主入口 -- ✅ **工厂函数**: 在 [`Parser.cs`](../Parser.cs:23-32) 中添加了 `SetPluginManagerFactory()` 方法 -- ✅ **向后兼容**: 保持与现有 `SetDefaultPluginManager()` 的完全兼容 -- ✅ **自动设置**: 在生成程序集时自动设置静态插件管理器 - -### 4. 集成到 Dashboard -- ✅ **WorkflowScriptService**: 修改了 [`Services/WorkflowScriptService.cs`](../../KitX%20Clients/KitX%20Dashboard/KitX%20Dashboard/Services/WorkflowScriptService.cs:17-52) -- ✅ **静态构造**: 在静态构造函数中设置 RealPluginManager 工厂 -- ✅ **实例传递**: 将PluginManager实例传递给 Parser -- ✅ **错误处理**: 添加了完善的异常处理和日志记录 - -## 🔧 集成架构 - -``` -┌─────────────────────────────────────────────────┐ -│ KitX Dashboard │ -│ ┌─────────────────────────────────────┐ │ -│ │ WorkflowScriptService │ │ -│ │ ┌─ SetPluginManagerFactory │ │ -│ │ │ │ │ │ -│ │ │ ▼ │ │ -│ │ │ RealPluginManager │ │ -│ │ │ ┌─ PluginsServer │ │ -│ │ │ │ ┌─ FindConnector │ │ -│ │ │ │ │ │ │ -│ │ │ │ ▼ │ │ -│ │ │ │ PluginConnector │ │ -│ │ │ │ ┌─ Request │ │ -│ │ │ │ │ │ │ -│ │ │ │ ▼ │ │ -│ │ │ │ WebSocket │ │ -│ │ │ └─ Plugin Process │ │ -│ │ └─────────────────────────────┘ │ -│ │ │ │ -│ │ ▼ Generated Assembly │ │ -│ │ ┌─ MethodEmitter │ │ -│ │ │ │ ┌─ Static Constructor │ │ -│ │ │ │ │ │ │ -│ │ │ │ ▼ │ │ -│ │ │ │ │ SetStaticPluginManager │ │ -│ │ │ │ └─────────────────────┘ │ -│ │ │ │ │ -│ │ ▼ Script Engine │ │ -│ │ └─ CSharpScriptEngine │ │ -│ │ │ │ -│ │ │ │ -│ │ │ │ -│ │ │ │ -│ └─────────────────────────────────┘ │ -│ │ -│ Workflow Script Execution │ -│ ────────────────────────┘ -└─────────────────────────────────────────┘ -``` - -## 🚀 使用方法 - -### 在 KitX Dashboard 中启用 RealPluginManager - -在 KitX Dashboard 启动时,`WorkflowScriptService` 的静态构造函数会自动执行以下操作: - -1. **获取 PluginsServer 实例**: 获取 `KitX.Dashboard.Network.PluginsNetwork.PluginsServer.Instance` -2. **设置工厂函数**: 调用 `Parser.SetPluginManagerFactory()` -3. **创建 RealPluginManager**: 传入 `PluginsServer` 实例和日志记录器 -4. **自动集成**: 后续的所有 Workflow Script 执行都将使用真实的插件系统 - -### 验证集成 - -集成成功后,您可以通过以下方式验证: - -1. **编译 Dashboard**: - ```bash - cd "KitX Clients/KitX Dashboard" - dotnet build - ``` - -2. **运行 Dashboard**: 启动后检查Logger输出 - ``` - [WorkflowScriptService] RealPluginManager 工厂函数已设置 - ``` - -3. **测试 Workflow Script**: 创建并执行包含插件调用的脚本 - -## 📝 代码示例 - -### 基本集成 -```csharp -// 在 Dashboard 的初始化代码中(通常在 App.xaml.cs 或 Program.cs) -// WorkflowScriptService 的静态构造函数会自动执行此代码 - -// 手动设置(可选) -Parser.SetPluginManagerFactory(() => new RealPluginManager( - KitX.Dashboard.Network.PluginsNetwork.PluginsServer.Instance, - message => Log.Information(message) // 使用 Serilog -)); -``` - -### 脚本中使用 -```csharp -// 现在脚本中可以直接调用,将通过真实的 KitX Dashboard 插件系统执行 -var result = SampleCalculator.Add(10, 20); -var text = StringToolkit.Reverse("Hello World"); -``` - -## ⚙️ 配置选项 - -### 工厂函数 vs 默认管理器 -- **工厂函数** (`SetPluginManagerFactory`): 推荐用于生产环境 -- **默认管理器** (`SetDefaultPluginManager`): 适用于简单场景或测试 - -### 日志记录器 -- **开发阶段**: 使用 `Console.WriteLine` 或 `Debug.WriteLine` -- **生产阶段**: 使用 `Serilog.Log.Information` 或其他日志框架 - -## 🔍 故障排除 - -### 常见问题 - -1. **编译错误**: 确保已正确引用 `Kscript.CSharp.Parser` 项目 -2. **运行时错误**: 检查 `PluginsServer.Instance` 是否可用 -3. **反射异常**: 确保 Dashboard 程序集已正确加载 - -### 调试技巧 - -1. **启用详细日志**: 在 `RealPluginManager` 构造函数中添加更多日志 -2. **检查插件状态**: 验证插件是否正确连接到 Dashboard -3. **测试单独组件**: 分别测试 `RealPluginManager` 和 `PluginsServer` 连接 - -## 📈 性能考虑 - -- ✅ **延迟初始化**: RealPluginManager 只在首次使用时创建 -- ✅ **缓存机制**: 保持原有的程序集缓存功能 -- ✅ **内存效率**: 静态插件管理器实例,避免重复创建 - -## 🎉 总结 - -通过这次集成,KitX.CSharp.Parser 现在完全支持: - -1. **真实的插件调用**: 通过 KitX Dashboard 的插件系统 -2. **灵活的配置**: 支持多种插件管理器实现 -3. **向后兼容性**: 不影响现有代码 -4. **生产就绪**: 具备完整的错误处理和日志记录 - -现在 Workflow Script 中的插件调用将直接通过 KitX Dashboard 的真实插件系统执行,实现了从模拟到真实的完整升级! diff --git a/KitX Script/Kscript.CSharp.Parser/Parser.cs b/KitX Script/Kscript.CSharp.Parser/Parser.cs index c07ec35..d9b701a 100644 --- a/KitX Script/Kscript.CSharp.Parser/Parser.cs +++ b/KitX Script/Kscript.CSharp.Parser/Parser.cs @@ -12,31 +12,52 @@ namespace Kscript.CSharp.Parser; public static class Parser { /// - /// 默认插件管理器 + /// 插件管理器实例 /// - private static IPluginManager? _defaultPluginManager; + private static IPluginManager? _pluginManager; /// - /// 插件管理器工厂函数 + /// 获取Parser初始化状态 /// - private static Func? _pluginManagerFactory; + public static bool IsInitialized => _pluginManager != null; /// - /// 设置默认插件管理器 + /// 设置插件管理器 /// /// 插件管理器实例 - public static void SetDefaultPluginManager(IPluginManager pluginManager) + public static void SetPluginManager(IPluginManager pluginManager) { - _defaultPluginManager = pluginManager; + _pluginManager = pluginManager ?? throw new ArgumentNullException(nameof(pluginManager)); } /// - /// 设置插件管理器工厂函数 + /// 确保Parser已正确初始化 /// - /// 插件管理器工厂函数 - public static void SetPluginManagerFactory(Func factory) + /// 当Parser未初始化时抛出 + private static void EnsureInitialized() { - _pluginManagerFactory = factory; + if (_pluginManager == null) + { + throw new InvalidOperationException("Parser 未初始化。请先调用 SetPluginManager() 设置插件管理器。"); + } + } + + /// + /// 确保所需插件已加载 + /// + /// 插件信息列表 + /// 当所需插件未加载时抛出 + private static void EnsurePluginsLoaded(List plugins) + { + foreach (var plugin in plugins) + { + if (!_pluginManager!.IsPluginExists(plugin.Name)) + { + // TODO: 实现插件动态加载逻辑 + // 目前先抛出异常,待后续实现插件加载功能 + throw new InvalidOperationException($"所需插件未加载: {plugin.Name}。请确保插件已正确安装并启动。"); + } + } } /// @@ -44,36 +65,32 @@ public static void SetPluginManagerFactory(Func factory) /// /// 插件信息列表 /// 程序集名称 - /// 插件管理器实例(可选,默认使用 MockPluginManager) /// 是否使用缓存 /// 生成的动态程序集 - public static Assembly Generate(List plugins, string assemblyName = "DynamicPluginAssembly", - IPluginManager? pluginManager = null, bool useCache = true) + public static Assembly Generate(List plugins, string assemblyName = "DynamicPluginAssembly", bool useCache = true) { if (plugins == null || plugins.Count == 0) { throw new ArgumentException("插件列表不能为空", nameof(plugins)); } + EnsureInitialized(); + EnsurePluginsLoaded(plugins); + try { - var manager = pluginManager ?? _defaultPluginManager ?? _pluginManagerFactory?.Invoke() ?? new MockPluginManager(); - - // 设置静态插件管理器实例 - MethodEmitter.SetStaticPluginManager(manager); - if (useCache) { - return AssemblyCache.GetOrCreateAssembly(plugins, assemblyName, manager); + return AssemblyCache.GetOrCreateAssembly(plugins, assemblyName, _pluginManager!); } else { - return MethodEmitter.GenerateAssembly(plugins, assemblyName, manager); + return AssemblyCache.ForceRegenerate(plugins, _pluginManager!, assemblyName); } } catch (Exception ex) when (!(ex is ParserException)) { - throw ParserException.AssemblyGenerationError(assemblyName, ex); + throw new ParserException($"生成程序集失败: {assemblyName}", ex); } } @@ -82,11 +99,9 @@ public static Assembly Generate(List plugins, string assemblyName = /// /// 插件清单 JSON 字符串 /// 程序集名称 - /// 插件管理器实例 /// 是否使用缓存 /// 生成的动态程序集 - public static Assembly GenerateFromJson(string jsonString, string assemblyName = "DynamicPluginAssembly", - IPluginManager? pluginManager = null, bool useCache = true) + public static Assembly GenerateFromJson(string jsonString, string assemblyName = "DynamicPluginAssembly", bool useCache = true) { if (string.IsNullOrWhiteSpace(jsonString)) { @@ -106,7 +121,7 @@ public static Assembly GenerateFromJson(string jsonString, string assemblyName = throw new ParserException("JSON 反序列化失败,结果为 null"); } - return Generate(plugins, assemblyName, pluginManager, useCache); + return Generate(plugins, assemblyName, useCache); } catch (JsonException ex) { @@ -119,11 +134,9 @@ public static Assembly GenerateFromJson(string jsonString, string assemblyName = /// /// JSON 文件路径 /// 程序集名称 - /// 插件管理器实例 /// 是否使用缓存 /// 生成的动态程序集 - public static async Task GenerateFromFileAsync(string jsonFilePath, string assemblyName = "DynamicPluginAssembly", - IPluginManager? pluginManager = null, bool useCache = true) + public static async Task GenerateFromFileAsync(string jsonFilePath, string assemblyName = "DynamicPluginAssembly", bool useCache = true) { if (string.IsNullOrWhiteSpace(jsonFilePath)) { @@ -138,7 +151,7 @@ public static async Task GenerateFromFileAsync(string jsonFilePath, st try { var jsonString = await File.ReadAllTextAsync(jsonFilePath); - return GenerateFromJson(jsonString, assemblyName, pluginManager, useCache); + return GenerateFromJson(jsonString, assemblyName, useCache); } catch (Exception ex) when (!(ex is ParserException)) { @@ -146,45 +159,6 @@ public static async Task GenerateFromFileAsync(string jsonFilePath, st } } - /// - /// 强制重新生成程序集(绕过缓存) - /// - /// 插件信息列表 - /// 程序集名称 - /// 插件管理器实例 - /// 新生成的动态程序集 - public static Assembly Regenerate(List plugins, string assemblyName = "DynamicPluginAssembly", - IPluginManager? pluginManager = null) - { - if (plugins == null || plugins.Count == 0) - { - throw new ArgumentException("插件列表不能为空", nameof(plugins)); - } - - try - { - var manager = pluginManager ?? _defaultPluginManager ?? _pluginManagerFactory?.Invoke() ?? new MockPluginManager(); - - // 设置静态插件管理器实例 - MethodEmitter.SetStaticPluginManager(manager); - - return AssemblyCache.ForceRegenerate(plugins, assemblyName, manager); - } - catch (Exception ex) when (!(ex is ParserException)) - { - throw ParserException.AssemblyGenerationError(assemblyName, ex); - } - } - - /// - /// 注册自定义类型映射 - /// - /// 类型名字符串 - /// 对应的 Type 对象 - public static void RegisterCustomType(string typeName, Type type) - { - TypeMapper.RegisterCustomType(typeName, type); - } /// /// 清除所有缓存 From 010e095c4e745140a734bc35607be3fe877f36fd Mon Sep 17 00:00:00 2001 From: StarInk Date: Sat, 8 Nov 2025 02:18:57 +0100 Subject: [PATCH 11/60] =?UTF-8?q?=F0=9F=93=84=20Docs(README,=20USAGE):=20?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=96=87=E6=A1=A3=E9=80=82=E9=85=8D=E6=9C=80?= =?UTF-8?q?=E6=96=B0=E6=9E=B6=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- KitX Script/Kscript.CSharp.Parser/README.md | 268 +++++++++----------- KitX Script/Kscript.CSharp.Parser/USAGE.md | 147 +++++------ 2 files changed, 177 insertions(+), 238 deletions(-) diff --git a/KitX Script/Kscript.CSharp.Parser/README.md b/KitX Script/Kscript.CSharp.Parser/README.md index 730f4c7..fff54a1 100644 --- a/KitX Script/Kscript.CSharp.Parser/README.md +++ b/KitX Script/Kscript.CSharp.Parser/README.md @@ -4,66 +4,25 @@ --- ## 🌱 项目定位 -KitX.CSharp.Parser 是 **KitX 工作流子系统** 的第一环。 -它只做一件事: -**把插件清单(JSON)→ 运行时静态类/方法**, -让后续脚本引擎(Roslyn、CSharpScript、Eval-etc.)可以像调用普通静态方法一样使用插件功能。 -``` - JSON 清单 ─→ Parser ─→ 内存程序集(DynamicAssembly) - │ - └─ 脚本侧: SamplePlugin.Run(…) -``` +KitX.CSharp.Parser 是 **KitX 工作流子系统** 的核心组件。它专注于一个核心功能: ---- +**把插件清单(JSON)→ 运行时静态类/方法**,让脚本引擎可以像调用普通静态方法一样使用插件功能。 -## 🎯 边界与职责 -| 范围 | 属于本库 | 不属于本库 | -|---|---|---| -| 读取 JSON | ✅ 反序列化为 `PluginInfo` | ❌ 网络/磁盘 IO | -| 类型映射 | ✅ `"int"` → `System.Int32` | ❌ 复杂自定义类型序列化 | -| 生成 IL | ✅ 用 `Reflection.Emit` 生成静态类/方法 | ❌ 脚本执行、调试 | -| 生命周期 | ✅ 生成后缓存 `Assembly` | ❌ 热重载(由上层调用 `Regenerate()`) | -| 错误处理 | ✅ 语法/类型错误抛出 `ParserException` | ❌ 插件运行时异常 | +``` +JSON 清单 ─→ Parser ─→ 内存程序集 ─→ 脚本侧: SamplePlugin.Method(...) +``` --- -## 🧩 整体流程 -1. **输入** - `List`(已由外部从 JSON 反序列化好)。 - -2. **映射** - 把 `Function.ReturnValueType` / `Parameter.Type` 字符串映射到 `System.Type`。 - -3. **构建** - - 每个插件 → 一个 `static class`(`TypeBuilder`) - - 每个功能 → 一个 `public static` 方法(`MethodBuilder`) - -4. **Emit** - 方法体内构造 `PluginCallInfo`,调用 `PluginManager.Call()`, - 返回值拆箱后 `ret`。 +## 🎯 核心特性 -5. **输出** - 程序集保存到内存(或可选磁盘 DLL), - 脚本侧通过 `PluginNamespace.PluginName.Func(...)` 直接调用。 - ---- - -## 🗂️ 目录结构(初始) -``` -KitX.CSharp.Parser/ - ├─ src/ - │ ├─ Parser.cs // 对外静态入口 - │ ├─ CodeGen/ // IL 生成实现 - │ │ ├─ TypeMapper.cs // 字符串 → Type - │ │ ├─ MethodEmitter.cs // IL 指令 - │ │ └─ AssemblyCache.cs // 已生成 Assembly 缓存 - │ └─ Models/ - │ └─ PluginInfo.cs // 复用现有结构 - ├─ tests/ - │ └─ Parser.Tests/ - └─ README.md -``` +- ✅ **动态 IL 生成** - 使用 `Reflection.Emit` 生成高性能静态方法 +- ✅ **智能类型映射** - 支持基础类型和自定义类型 +- ✅ **程序集缓存** - 避免重复生成,提升性能 +- ✅ **简化设计** - 直接依赖注入,无复杂抽象层 +- ✅ **异常处理** - 完善的错误处理机制 +- ✅ **调试友好** - 详细的日志输出和统计信息 --- @@ -74,14 +33,17 @@ KitX.CSharp.Parser/ ```csharp using Kscript.CSharp.Parser; -// 1. 从 JSON 文件生成程序集 -var assembly = await Parser.GenerateFromFileAsync("example.json"); +// 1. 设置插件管理器(必须) +Parser.SetPluginManager(new MockPluginManager()); + +// 2. 从 JSON 文件生成程序集 +var assembly = await Parser.GenerateFromFileAsync("plugins.json"); -// 2. 或从插件信息列表生成 +// 3. 从插件信息列表生成程序集 var plugins = new List { /* ... */ }; var assembly = Parser.Generate(plugins); -// 3. 脚本中直接调用生成的方法 +// 4. 脚本中直接调用生成的方法 int sum = SampleCalculator.Add(10, 20); string reversed = StringToolkit.Reverse("Hello"); ``` @@ -96,81 +58,66 @@ dotnet build dotnet run ``` -## 🔌 实际插件管理器集成 - -### 概述 - -KitX.CSharp.Parser 现在支持使用实际的插件管理器替换默认的 MockPluginManager,实现与 KitX Dashboard 的真实插件调用集成。 - -### 集成步骤 - -1. **设置插件管理器工厂** - ```csharp - // 在 KitX Dashboard 启动时设置 - Parser.SetPluginManagerFactory(() => new RealPluginManager( - KitX.Dashboard.Network.PluginsNetwork.PluginsServer.Instance, - message => Log.Information(message) - )); - ``` - -2. **生成插件程序集** - ```csharp - var assembly = Parser.Generate(plugins, "DashboardPluginAssembly"); - ``` +--- -3. **在脚本中使用** - ```csharp - // 现在可以直接调用,将通过真实的 KitX Dashboard 插件系统执行 - var result = SampleCalculator.Add(10, 20); - ``` +## 🔌 API 参考 -### API 参考 +### Parser 静态类 -#### 新增方法 +#### 生成方法 | 方法 | 描述 | 参数 | |------|------|------| -| `SetPluginManagerFactory()` | 设置插件管理器工厂函数 | `Func factory` | +| `Generate()` | 从插件信息列表生成程序集 | `plugins`, `assemblyName`, `useCache` | +| `GenerateFromJson()` | 从 JSON 字符串生成程序集 | `jsonString`, `assemblyName`, `useCache` | +| `GenerateFromFileAsync()` | 从 JSON 文件异步生成程序集 | `jsonFilePath`, `assemblyName`, `useCache` | -#### RealPluginManager 构造函数 +#### 配置方法 -| 参数 | 类型 | 描述 | -|------|------|------| -| `pluginsServer` | `object` | PluginsServer 实例 | -| `logger` | `Action` | 日志记录器 | +| 方法 | 描述 | +|------|------| +| `SetPluginManager()` | 设置插件管理器实例 | `IPluginManager pluginManager` | +| `ClearCache()` | 清除所有缓存 | +| `GetCacheStatistics()` | 获取缓存统计信息 | -### 使用示例 +#### 分析方法 -详细的使用示例请参考: -- `Examples/RealPluginManagerExample.cs` - 完整的使用示例 -- `Examples/Program.cs` - 演示程序入口 +| 方法 | 描述 | +|------|------| +| `GetPluginTypes()` | 获取程序集中的所有插件类型 | +| `GetPluginMethods()` | 获取插件类型的所有方法信息 | -### 注意事项 +--- + +## 📊 类型映射支持 -- ✅ **依赖注入设计**:通过工厂函数模式实现松耦合 -- ✅ **日志统一**:支持外部传入日志记录器 -- ✅ **向后兼容**:仍支持 MockPluginManager 作为默认选项 -- ✅ **实际调用**:通过 KitX Dashboard 的插件系统进行真实调用 +| JSON 字符串 | CLR 类型 | 示例 | +|---|---|---| +| `"void"` | `System.Void` | 无返回值 | +| `"bool"` | `System.Boolean` | 布尔参数 | +| `"int"` | `System.Int32` | 整数参数 | +| `"double"` | `System.Double` | 浮点数参数 | +| `"string"` | `System.String` | 字符串参数 | +| `"object"` | `System.Object` | 对象参数 | --- -## 📁 项目结构 +## 🏗️ 项目结构 ``` KitX.CSharp.Parser/ -├─ src/ -│ ├─ Parser.cs # 主入口静态类 -│ ├─ Models/ -│ │ └─ PluginCallInfo.cs # 插件调用信息模型 -│ ├─ CodeGen/ -│ │ ├─ TypeMapper.cs # 字符串 → Type 映射 -│ │ ├─ MethodEmitter.cs # IL 方法生成器 -│ │ └─ AssemblyCache.cs # 程序集缓存管理 -│ ├─ Core/ -│ │ ├─ IPluginManager.cs # 插件管理器接口 -│ │ └─ MockPluginManager.cs # 模拟实现 -│ └─ Exceptions/ -│ └─ ParserException.cs # 自定义异常 +├─ Parser.cs # 主入口静态类 +├─ Models/ +│ └─ PluginCallInfo.cs # 插件调用信息模型 +├─ CodeGen/ +│ ├─ TypeMapper.cs # 字符串 → Type 映射 +│ ├─ MethodEmitter.cs # IL 方法生成器 +│ └─ AssemblyCache.cs # 程序集缓存管理 +├─ Core/ +│ ├─ IPluginManager.cs # 插件管理器接口 +│ └─ MockPluginManager.cs # 模拟实现 +├─ Exceptions/ +│ └─ ParserException.cs # 自定义异常 ├─ Examples/ │ ├─ BasicUsageExample.cs # 基础用法示例 │ └─ Program.cs # 演示程序入口 @@ -181,39 +128,29 @@ KitX.CSharp.Parser/ --- -## ⚙️ 主要特性 - -- ✅ **动态 IL 生成** - 使用 `Reflection.Emit` 生成高性能静态方法 -- ✅ **智能类型映射** - 支持基础类型、泛型类型和自定义类型 -- ✅ **程序集缓存** - 避免重复生成,提升性能 -- ✅ **扩展性设计** - 支持自定义插件管理器和类型映射 -- ✅ **异常处理** - 完善的错误处理机制 -- ✅ **调试友好** - 详细的日志输出和统计信息 +## 🎯 运行结果示例 ---- +``` +🚀 欢迎使用 KitX.CSharp.Parser 演示程序! -## 📐 类型映射支持 +=== 测试简化后的 Parser 功能 === +1. 测试默认插件管理器设置... + ✓ 默认 MockPluginManager 工作正常 -| JSON 字符串 | CLR 类型 | 示例 | -|---|---|---| -| `"int"` | `System.Int32` | 整数参数 | -| `"double"` | `System.Double` | 浮点数参数 | -| `"string"` | `System.String` | 字符串参数 | -| `"bool"` | `System.Boolean` | 布尔参数 | -| `"void"` | `System.Void` | 无返回值 | -| `"List"` | `List` | 泛型集合 | -| `"Dictionary"` | `Dictionary` | 字典类型 | -| 自定义 | 通过 `RegisterCustomType()` 注册 | 扩展类型 | +2. 测试自定义插件管理器设置... + ✓ 自定义插件管理器设置工作正常 ---- +3. 测试空参数处理... + ✓ 空参数正确抛出 ArgumentNullException -## 🎯 运行结果示例 - -``` -🚀 欢迎使用 KitX.CSharp.Parser 演示程序! +4. 测试插件存在性检测... + ✓ SampleCalculator 存在: True + ✓ SampleCalculator.Add 方法存在: True + ✓ NonExistentPlugin 存在: False + ✓ SampleCalculator.NonExistentMethod 方法存在: False + ✓ 插件存在性检测功能工作正常 === KitX.CSharp.Parser 基础用法示例 === - 1. 加载插件清单... ✓ 成功生成程序集: ExamplePluginAssembly @@ -233,26 +170,49 @@ KitX.CSharp.Parser/ 调用 ToUpper(world) → 结果: WORLD 4. 缓存统计信息... - 缓存统计: 1 个程序集, 1 个引用 + 缓存统计: 1 个程序集 + +5. 测试缓存效果... + 第二次生成是否使用缓存: True + 强制重新生成是否使用新程序集: True + 重新生成后缓存存在: True + +✅ 所有示例运行完成! ``` --- -## 📋 TODO / 未来计划 +## 🔧 实际插件管理器集成 + +### 设置真实插件管理器 -### 🔄 项目重构 -- [ ] **转换为纯库项目** - 移除命令行功能,专注于提供API -- [ ] **移除演示程序** - 将 `Examples/` 目录移至独立的演示项目 -- [ ] **调整项目配置** - 移除 `OutputType=Exe` 配置 +```csharp +// 在 KitX Dashboard 启动时设置 +Parser.SetPluginManager(new RealPluginManager( + KitX.Dashboard.Network.PluginsNetwork.PluginsServer.Instance, + message => Log.Information(message) +)); +``` -### 🔌 插件管理器集成 -- [ ] **替换 Mock 实现** - 当 KitX 主项目完成后,集成真实的 `PluginManager` -- [ ] **接口适配** - 根据实际需求调整 `IPluginManager` 接口定义 -- [ ] **调用机制优化** - 优化插件调用的性能和稳定性 +### 在脚本中使用 -### 🛠️ 功能增强(暂无计划) -### 📈 性能优化(暂无计划) -### 🧪 测试覆盖(暂无计划) +```csharp +// 现在可以直接调用,将通过真实的 KitX Dashboard 插件系统执行 +var result = SampleCalculator.Add(10, 20); +``` + +--- + +## 📝 注意事项 + +- ✅ **简化设计**:采用直接依赖注入,移除了复杂的工厂函数模式 +- ✅ **必须初始化**:使用前必须调用 `SetPluginManager()` 设置插件管理器 +- ✅ **插件验证**:生成程序集前会检查所需插件是否存在 +- ✅ **缓存优化**:相同插件清单只生成一次,提升性能 +- ✅ **异常安全**:完善的参数验证和错误处理机制 + +--- -> **注意**: 当前版本使用 `MockPluginManager` 进行功能验证。在 KitX 生态系统的其他组件完成后,需要将其替换为真实的插件调用实现。 +## 🎉 总结 +KitX.CSharp.Parser 提供了一个简洁、高效、易维护的解决方案,用于将插件清单转换为可直接调用的 C# API。通过简化的设计和完善的错误处理,确保了在生产环境中的稳定性和可靠性。 diff --git a/KitX Script/Kscript.CSharp.Parser/USAGE.md b/KitX Script/Kscript.CSharp.Parser/USAGE.md index 22983bb..7e0f2e7 100644 --- a/KitX Script/Kscript.CSharp.Parser/USAGE.md +++ b/KitX Script/Kscript.CSharp.Parser/USAGE.md @@ -11,98 +11,75 @@ KitX.CSharp.Parser 是 KitX 工作流子系统的核心组件,它能够将插 ```csharp using Kscript.CSharp.Parser; -// 1. 从 JSON 字符串生成程序集 +// 1. 设置插件管理器(必须) +Parser.SetPluginManager(new MockPluginManager()); + +// 2. 从 JSON 字符串生成程序集 var jsonString = File.ReadAllText("plugins.json"); var assembly = Parser.GenerateFromJson(jsonString); -// 2. 从文件生成程序集 +// 3. 从文件生成程序集 var assembly = await Parser.GenerateFromFileAsync("plugins.json"); -// 3. 从插件信息列表生成程序集 +// 4. 从插件信息列表生成程序集 var plugins = new List { /* ... */ }; var assembly = Parser.Generate(plugins); + +// 5. 脚本中直接调用生成的方法 +int sum = SampleCalculator.Add(10, 20); +string reversed = StringToolkit.Reverse("Hello"); ``` -### 动态调用生成的方法 +### 运行演示 -```csharp -// 获取生成的插件类型 -var pluginTypes = Parser.GetPluginTypes(assembly); +```bash +# 构建项目 +dotnet build -foreach (var type in pluginTypes) -{ - var methods = Parser.GetPluginMethods(type); - foreach (var method in methods) - { - // 动态调用方法 - var result = method.Invoke(null, new object[] { /* 参数 */ }); - Console.WriteLine($"结果: {result}"); - } -} +# 运行演示程序 +dotnet run ``` ## 🔧 API 参考 ### Parser 静态类 -主入口类,提供所有核心功能。 - #### 生成方法 | 方法 | 描述 | 参数 | |------|------|------| -| `Generate()` | 从插件信息列表生成程序集 | `plugins`, `assemblyName`, `pluginManager`, `useCache` | -| `GenerateFromJson()` | 从 JSON 字符串生成程序集 | `jsonString`, `assemblyName`, `pluginManager`, `useCache` | -| `GenerateFromFileAsync()` | 从 JSON 文件异步生成程序集 | `jsonFilePath`, `assemblyName`, `pluginManager`, `useCache` | -| `Regenerate()` | 强制重新生成程序集(绕过缓存) | `plugins`, `assemblyName`, `pluginManager` | +| `Generate()` | 从插件信息列表生成程序集 | `plugins`, `assemblyName`, `useCache` | +| `GenerateFromJson()` | 从 JSON 字符串生成程序集 | `jsonString`, `assemblyName`, `useCache` | +| `GenerateFromFileAsync()` | 从 JSON 文件异步生成程序集 | `jsonFilePath`, `assemblyName`, `useCache` | #### 配置方法 | 方法 | 描述 | |------|------| -| `SetDefaultPluginManager()` | 设置默认插件管理器 | -| `RegisterCustomType()` | 注册自定义类型映射 | +| `SetPluginManager()` | 设置插件管理器实例 | `IPluginManager pluginManager` | | `ClearCache()` | 清除所有缓存 | +| `GetCacheStatistics()` | 获取缓存统计信息 | #### 分析方法 | 方法 | 描述 | |------|------| -| `GetPluginTypes()` | 获取程序集中的插件类型 | -| `GetPluginMethods()` | 获取插件类型的方法信息 | -| `GetCacheStatistics()` | 获取缓存统计信息 | +| `GetPluginTypes()` | 获取程序集中的所有插件类型 | +| `GetPluginMethods()` | 获取插件类型的所有方法信息 | | `HasCache()` | 检查是否存在缓存 | -### 类型映射系统 - -#### 支持的基础类型 - -| JSON 字符串 | CLR 类型 | -|-------------|----------| -| `"void"` | `System.Void` | -| `"bool"` | `System.Boolean` | -| `"int"` | `System.Int32` | -| `"double"` | `System.Double` | -| `"string"` | `System.String` | -| `"object"` | `System.Object` | - -#### 泛型类型支持 - -```csharp -// 支持的泛型类型 -"List" → List -"Dictionary" → Dictionary -"Nullable" → int? -"Array" → string[] -``` +## 📊 类型映射系统 -#### 自定义类型映射 +### 支持的基础类型 -```csharp -// 注册自定义类型 -Parser.RegisterCustomType("DateTime", typeof(DateTime)); -Parser.RegisterCustomType("MyCustomType", typeof(MyClass)); -``` +| JSON 字符串 | CLR 类型 | 示例 | +|---|---|---| +| `"void"` | `System.Void` | 无返回值 | +| `"bool"` | `System.Boolean` | 布尔参数 | +| `"int"` | `System.Int32` | 整数参数 | +| `"double"` | `System.Double` | 浮点数参数 | +| `"string"` | `System.String` | 字符串参数 | +| `"object"` | `System.Object` | 对象参数 | ## 🏗️ 架构设计 @@ -178,30 +155,10 @@ Parser.RegisterCustomType("Guid", typeof(Guid)); // 3. 合理使用强制重新生成 if (pluginChanged) { - assembly = Parser.Regenerate(plugins); + assembly = Parser.Generate(plugins, useCache: false); } ``` -## 🔍 调试功能 - -### 日志输出 - -使用 `MockPluginManager` 时会输出详细的调用日志: - -``` -[MockPluginManager] 调用插件方法: SampleCalculator.Add(10, 20) -[MockPluginManager] 参数类型: [Int32, Int32] -[MockPluginManager] 期望返回类型: Int32 -[MockPluginManager] 返回结果: 30 -``` - -### 缓存统计 - -```csharp -var stats = Parser.GetCacheStatistics(); -Console.WriteLine($"缓存统计: {stats.CachedAssemblyCount} 个程序集"); -``` - ## 🚨 错误处理 ### 异常类型 @@ -209,6 +166,7 @@ Console.WriteLine($"缓存统计: {stats.CachedAssemblyCount} 个程序集"); - `ParserException` - 解析器相关异常 - `ArgumentException` - 参数错误 - `FileNotFoundException` - 文件不存在 +- `InvalidOperationException` - 初始化状态错误 ### 异常处理示例 @@ -227,6 +185,26 @@ catch (JsonException ex) } ``` +## 🔍 调试功能 + +### 日志输出 + +使用 `MockPluginManager` 时会输出详细的调用日志: + +``` +[MockPluginManager] 调用插件方法: SampleCalculator.Add(10, 20) +[MockPluginManager] 参数类型: [Int32, Int32] +[MockPluginManager] 期望返回类型: Int32 +[MockPluginManager] 返回结果: 30 +``` + +### 缓存统计 + +```csharp +var stats = Parser.GetCacheStatistics(); +Console.WriteLine($"缓存统计: {stats.CachedAssemblyCount} 个程序集"); +``` + ## 🔌 扩展点 ### 自定义插件管理器 @@ -244,10 +222,10 @@ public class CustomPluginManager : IPluginManager } // 设置自定义管理器 -Parser.SetDefaultPluginManager(new CustomPluginManager()); +Parser.SetPluginManager(new CustomPluginManager()); ``` -### 自定义类型映射 +### 自定义类型映射(已移除,未来可能会用更好的方案重新添加回来,取决于上游协议) ```csharp // 扩展类型映射 @@ -269,7 +247,7 @@ TypeMapper.RegisterCustomType("Color", typeof(System.Drawing.Color)); ```csharp // 在脚本中使用生成的API -var script = @" +var Script = @" int result = SampleCalculator.Add(10, 20); string reversed = StringToolkit.Reverse(""Hello""); return result; @@ -277,16 +255,17 @@ var script = @" // 通过脚本引擎执行 var assembly = Parser.Generate(plugins); -var scriptResult = ExecuteScript(script, assembly); +var ScriptResult = ExecuteScript(Script, assembly); ``` ## 📚 更多示例 完整的使用示例请参考: + - `Examples/BasicUsageExample.cs` - 基础功能演示 - `Examples/Program.cs` - 控制台演示程序 +- `RealPluginManagerExample.cs` - "真实"插件管理器示例 -## 🔗 相关项目 +## 🎯 总结 -- **KitX.Shared.CSharp** - 共享数据模型 -- **KitX 主项目** - 完整的插件生态系统 +KitX.CSharp.Parser 提供了一个简洁、高效、易维护的解决方案,用于将插件清单转换为可直接调用的 C# API。通过简化的设计和完善的错误处理,确保了在生产环境中的稳定性和可靠性。 From 6fa09868f34d5ba76af76010f0eb6e16caa1b49c Mon Sep 17 00:00:00 2001 From: StarInk Date: Sat, 8 Nov 2025 10:35:17 +0100 Subject: [PATCH 12/60] =?UTF-8?q?=F0=9F=A7=A9=20Refactor(Examples):=20?= =?UTF-8?q?=E5=B0=86=E5=9F=BA=E7=A1=80=E7=94=A8=E6=B3=95=E7=A4=BA=E4=BE=8B?= =?UTF-8?q?=E5=92=8C=E5=AE=9E=E9=99=85=E6=8F=92=E4=BB=B6=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E5=99=A8=E4=BD=BF=E7=94=A8=E7=A4=BA=E4=BE=8B=E6=8B=86=E5=87=BA?= =?UTF-8?q?=E4=B8=BA=E7=8B=AC=E7=AB=8B=E7=A8=8B=E5=BA=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Examples/BasicUsageExample.cs | 265 ++++++++++++++++++ .../Examples/Program.cs | 165 +++++++++++ .../Examples/RealPluginManagerExample.cs | 130 +++++++++ .../Kscript.CSharp.Parser.Examples.csproj | 22 ++ 4 files changed, 582 insertions(+) create mode 100644 KitX Script/Kscript.CSharp.Parser.Examples/Examples/BasicUsageExample.cs create mode 100644 KitX Script/Kscript.CSharp.Parser.Examples/Examples/Program.cs create mode 100644 KitX Script/Kscript.CSharp.Parser.Examples/Examples/RealPluginManagerExample.cs create mode 100644 KitX Script/Kscript.CSharp.Parser.Examples/Kscript.CSharp.Parser.Examples.csproj diff --git a/KitX Script/Kscript.CSharp.Parser.Examples/Examples/BasicUsageExample.cs b/KitX Script/Kscript.CSharp.Parser.Examples/Examples/BasicUsageExample.cs new file mode 100644 index 0000000..6d15ce9 --- /dev/null +++ b/KitX Script/Kscript.CSharp.Parser.Examples/Examples/BasicUsageExample.cs @@ -0,0 +1,265 @@ +using System.Reflection; +using System.Text.Json; +using KitX.Shared.CSharp.Plugin; +using Kscript.CSharp.Parser; +using Kscript.CSharp.Parser.Core; + +namespace Kscript.CSharp.Parser.Examples; + +/// +/// 基础用法示例 +/// +public static class BasicUsageExample +{ + /// + /// 演示基本功能 + /// + public static async Task RunExample() + { + Console.WriteLine("=== KitX.CSharp.Parser 基础用法示例 ===\n"); + + try + { + // 0. 初始化插件管理器 + Console.WriteLine("0. 初始化插件管理器..."); + if (!Parser.IsInitialized) + { + Parser.SetPluginManager(new MockPluginManager()); + Console.WriteLine(" ✓ 已设置 MockPluginManager"); + } + + // 1. 从现有的 example.json 文件加载插件清单 + Console.WriteLine("1. 加载插件清单..."); + var exampleJsonPath = Path.Combine(".", "example.json"); + if (!File.Exists(exampleJsonPath)) + { + Console.WriteLine($" 警告: example.json 文件不存在于 {exampleJsonPath}"); + Console.WriteLine(" 将使用内置示例数据"); + RunWithBuiltInData(); + return; + } + + var assembly = await Parser.GenerateFromFileAsync(exampleJsonPath, "ExamplePluginAssembly"); + Console.WriteLine($" ✓ 成功生成程序集: {assembly.FullName}"); + + // 2. 获取生成的插件类型 + Console.WriteLine("\n2. 分析生成的插件类型..."); + var pluginTypes = Parser.GetPluginTypes(assembly); + Console.WriteLine($" 发现 {pluginTypes.Count} 个插件类:"); + + foreach (var type in pluginTypes) + { + Console.WriteLine($" - {type.Name}"); + var methods = Parser.GetPluginMethods(type); + foreach (var method in methods) + { + var parameters = string.Join(", ", method.GetParameters().Select(p => $"{p.ParameterType.Name} {p.Name}")); + Console.WriteLine($" └── {method.ReturnType.Name} {method.Name}({parameters})"); + } + } + + // 3. 动态调用插件方法 + Console.WriteLine("\n3. 动态调用插件方法..."); + DynamicInvokeExample(assembly); + + // 4. 缓存统计 + Console.WriteLine("\n4. 缓存统计信息..."); + var stats = Parser.GetCacheStatistics(); + Console.WriteLine($" {stats}"); + + // 5. 演示缓存效果 + Console.WriteLine("\n5. 测试缓存效果..."); + + // 5.1 先检查是否存在缓存 + var plugins = JsonSerializer.Deserialize>(await File.ReadAllTextAsync(exampleJsonPath), new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + AllowTrailingCommas = true + }); + var hasCacheBefore = Parser.HasCache(plugins!, "ExamplePluginAssembly"); + Console.WriteLine($" 生成前检查缓存存在: {hasCacheBefore}"); + + // 5.2 第二次生成(应该使用缓存) + var assembly2 = await Parser.GenerateFromFileAsync(exampleJsonPath, "ExamplePluginAssembly"); + Console.WriteLine($" 第二次生成是否使用缓存: {assembly == assembly2}"); + + // 5.3 强制重新生成(应该绕过缓存) + var assembly3 = Parser.Generate(plugins!, "ExamplePluginAssembly", useCache: false); + Console.WriteLine($" 强制重新生成是否使用新程序集: {assembly3 != assembly2}"); + + // 5.4 检查强制重新生成后的缓存状态 + var hasCacheAfterRegenerate = Parser.HasCache(plugins!, "ExamplePluginAssembly"); + Console.WriteLine($" 重新生成后缓存存在: {hasCacheAfterRegenerate}"); + + } + catch (Exception ex) + { + Console.WriteLine($"❌ 示例执行失败: {ex.Message}"); + Console.WriteLine($"详细信息: {ex}"); + } + } + + /// + /// 使用内置数据运行示例 + /// + private static void RunWithBuiltInData() + { + // 确保插件管理器已初始化 + if (!Parser.IsInitialized) + { + Parser.SetPluginManager(new MockPluginManager()); + } + + // 创建示例插件数据 + var plugins = new List + { + new PluginInfo + { + Name = "TestCalculator", + Version = "1.0.0", + Functions = new List + { + new Function + { + Name = "Add", + ReturnValueType = "int", + Parameters = new List + { + new Parameter { Name = "a", Type = "int", IsOptional = false }, + new Parameter { Name = "b", Type = "int", IsOptional = false } + } + }, + new Function + { + Name = "Multiply", + ReturnValueType = "double", + Parameters = new List + { + new Parameter { Name = "x", Type = "double", IsOptional = false }, + new Parameter { Name = "y", Type = "double", IsOptional = false } + } + } + } + } + }; + + Console.WriteLine(" 使用内置示例数据..."); + var assembly = Parser.Generate(plugins, "BuiltInExampleAssembly"); + Console.WriteLine($" ✓ 成功生成程序集: {assembly.FullName}"); + + DynamicInvokeExample(assembly); + } + + /// + /// 动态调用示例 + /// + private static void DynamicInvokeExample(Assembly assembly) + { + var pluginTypes = Parser.GetPluginTypes(assembly); + + foreach (var type in pluginTypes) + { + Console.WriteLine($"\n 调用插件: {type.Name}"); + var methods = Parser.GetPluginMethods(type); + + foreach (var method in methods) + { + try + { + object? result = null; + var parameters = method.GetParameters(); + + // 根据方法名和参数生成测试数据 + object[] args = GenerateTestArguments(method.Name, parameters); + + Console.WriteLine($" 调用 {method.Name}({string.Join(", ", args)})"); + + // 动态调用方法 + result = method.Invoke(null, args); + + Console.WriteLine($" → 结果: {result}"); + } + catch (Exception ex) + { + Console.WriteLine($" → 调用失败: {ex.Message}"); + } + } + } + } + + /// + /// 生成测试参数 + /// + private static object[] GenerateTestArguments(string methodName, ParameterInfo[] parameters) + { + var args = new object[parameters.Length]; + + for (int i = 0; i < parameters.Length; i++) + { + var param = parameters[i]; + + // 根据参数类型和方法名生成合适的测试值 + args[i] = param.ParameterType.Name switch + { + "Int32" => methodName switch + { + "Add" => i == 0 ? 10 : 20, + "Divide" when param.Name == "decimals" => 2, + _ => i + 1 + }, + "Double" => methodName switch + { + "Divide" => i == 0 ? 10.0 : 3.0, + "Multiply" => i == 0 ? 2.5 : 4.0, + _ => (i + 1) * 1.5 + }, + "String" => methodName switch + { + "Reverse" => "Hello", + "ToUpper" => "world", + _ => $"test{i}" + }, + _ => GetDefaultValue(param.ParameterType) + }; + } + + return args; + } + + /// + /// 获取类型默认值 + /// + private static object GetDefaultValue(Type type) + { + if (type.IsValueType) + return Activator.CreateInstance(type)!; + + return type == typeof(string) ? "default" : null!; + } + + /// + /// 演示高级功能 + /// + public static void RunAdvancedExample() + { + Console.WriteLine("\n=== 高级功能演示 ===\n"); + + try + { + // 1. 清除缓存 + Console.WriteLine("1. 清除所有缓存..."); + Parser.ClearCache(); + Console.WriteLine(" ✓ 缓存已清除"); + + // 2. 重新设置插件管理器 + Console.WriteLine("\n2. 重新设置插件管理器..."); + Parser.SetPluginManager(new MockPluginManager()); + Console.WriteLine(" ✓ 已重新设置插件管理器"); + + } + catch (Exception ex) + { + Console.WriteLine($"❌ 高级功能演示失败: {ex.Message}"); + } + } +} diff --git a/KitX Script/Kscript.CSharp.Parser.Examples/Examples/Program.cs b/KitX Script/Kscript.CSharp.Parser.Examples/Examples/Program.cs new file mode 100644 index 0000000..d9ae99c --- /dev/null +++ b/KitX Script/Kscript.CSharp.Parser.Examples/Examples/Program.cs @@ -0,0 +1,165 @@ +using Kscript.CSharp.Parser.Examples; +using Kscript.CSharp.Parser.Core; +using KitX.Shared.CSharp.Plugin; + +namespace Kscript.CSharp.Parser.Examples; + +/// +/// 演示程序入口 +/// +public static class Program +{ + /// + /// 主入口方法 + /// + public static async Task Main(string[] args) + { + Console.OutputEncoding = System.Text.Encoding.UTF8; + Console.WriteLine("🚀 欢迎使用 KitX.CSharp.Parser 演示程序!"); + Console.WriteLine("==================================================\n"); + + try + { + // 首先运行简化后的功能测试 + Console.WriteLine("=== 测试简化后的 Parser 功能 ==="); + TestSimplifiedParser(); + + Console.WriteLine("\n" + new string('=', 60)); + + // 运行基础用法示例 + await BasicUsageExample.RunExample(); + + // 运行高级功能示例 + BasicUsageExample.RunAdvancedExample(); + + // 运行实际插件管理器示例 + Console.WriteLine("\n" + new string('=', 60)); + RealPluginManagerExample.RunExample(); + + Console.WriteLine("\n" + new string('=', 60)); + RealPluginManagerExample.DashboardIntegrationExample(); + + Console.WriteLine("\n=================================================="); + Console.WriteLine("✅ 所有示例运行完成!"); + } + catch (Exception ex) + { + Console.WriteLine($"\n❌ 程序运行失败: {ex.Message}"); + Console.WriteLine($"详细错误信息:\n{ex}"); + } + + Console.WriteLine("\n按任意键退出..."); + Console.ReadKey(); + } + + /// + /// 测试简化后的 Parser 功能 + /// + private static void TestSimplifiedParser() + { + Console.WriteLine("1. 测试默认插件管理器设置..."); + + // 测试使用 MockPluginManager 中已有的插件 + var testPlugins = new List + { + new PluginInfo + { + Name = "SampleCalculator", + Version = "1.0.0", + Functions = new List + { + new Function + { + Name = "Add", + ReturnValueType = "int", + Parameters = new List + { + new Parameter { Name = "a", Type = "int", IsOptional = false }, + new Parameter { Name = "b", Type = "int", IsOptional = false } + } + } + } + } + }; + + try + { + // 先设置插件管理器 + Parser.SetPluginManager(new MockPluginManager()); + + // 使用 MockPluginManager 生成程序集 + var assembly = Parser.Generate(testPlugins, "TestAssembly"); + Console.WriteLine(" ✓ 默认 MockPluginManager 工作正常"); + + // 获取生成的类型 + var types = Parser.GetPluginTypes(assembly); + Console.WriteLine($" ✓ 生成了 {types.Count} 个插件类型"); + + foreach (var type in types) + { + var methods = Parser.GetPluginMethods(type); + Console.WriteLine($" - {type.Name}: {methods.Count} 个方法"); + } + } + catch (Exception ex) + { + Console.WriteLine($" ❌ 测试失败: {ex.Message}"); + } + + Console.WriteLine("2. 测试自定义插件管理器设置..."); + try + { + // 设置新的插件管理器 + Parser.SetPluginManager(new MockPluginManager()); + var assembly2 = Parser.Generate(testPlugins, "TestAssembly2"); + Console.WriteLine(" ✓ 自定义插件管理器设置工作正常"); + } + catch (Exception ex) + { + Console.WriteLine($" ❌ 自定义设置测试失败: {ex.Message}"); + } + + Console.WriteLine("3. 测试空参数处理..."); + try + { + // 测试 null 参数应该抛出异常 + Parser.SetPluginManager(null!); + Console.WriteLine(" ❌ 空参数测试失败: 应该抛出异常但没有"); + } + catch (ArgumentNullException) + { + Console.WriteLine(" ✓ 空参数正确抛出 ArgumentNullException"); + } + catch (Exception ex) + { + Console.WriteLine($" ❌ 空参数测试失败: {ex.Message}"); + } + + Console.WriteLine("4. 测试插件存在性检测..."); + try + { + var mockManager = new MockPluginManager(); + Parser.SetPluginManager(mockManager); + + // 测试存在的插件 + var existsSampleCalculator = mockManager.IsPluginExists("SampleCalculator"); + Console.WriteLine($" ✓ SampleCalculator 存在: {existsSampleCalculator}"); + + var existsAddMethod = mockManager.IsMethodExists("SampleCalculator", "Add"); + Console.WriteLine($" ✓ SampleCalculator.Add 方法存在: {existsAddMethod}"); + + // 测试不存在的插件 + var existsNonExistent = mockManager.IsPluginExists("NonExistentPlugin"); + Console.WriteLine($" ✓ NonExistentPlugin 存在: {existsNonExistent}"); + + var existsNonExistentMethod = mockManager.IsMethodExists("SampleCalculator", "NonExistentMethod"); + Console.WriteLine($" ✓ SampleCalculator.NonExistentMethod 方法存在: {existsNonExistentMethod}"); + + Console.WriteLine(" ✓ 插件存在性检测功能工作正常"); + } + catch (Exception ex) + { + Console.WriteLine($" ❌ 插件存在性检测测试失败: {ex.Message}"); + } + } +} diff --git a/KitX Script/Kscript.CSharp.Parser.Examples/Examples/RealPluginManagerExample.cs b/KitX Script/Kscript.CSharp.Parser.Examples/Examples/RealPluginManagerExample.cs new file mode 100644 index 0000000..b44528e --- /dev/null +++ b/KitX Script/Kscript.CSharp.Parser.Examples/Examples/RealPluginManagerExample.cs @@ -0,0 +1,130 @@ +using KitX.Shared.CSharp.Plugin; +using Kscript.CSharp.Parser; +using Kscript.CSharp.Parser.Core; +using System.Text.Json; + +namespace Kscript.CSharp.Parser.Examples; + +/// +/// 实际插件管理器使用示例 +/// +public static class RealPluginManagerExample +{ + /// + /// 演示如何使用 RealPluginManager + /// + public static void RunExample() + { + Console.WriteLine("=== KitX.CSharp.Parser 实际插件管理器使用示例 ===\n"); + + try + { + // 1. 设置插件管理器 + Console.WriteLine("1. 设置插件管理器..."); + + // 这里应该传入实际的 PluginsServer 实例和日志记录器 + // 由于这是一个示例,我们使用 null 作为占位符 + // 在实际使用中,这里应该传入真实的 PluginsServer 实例和日志记录器 + // 例如: + // var pluginsServer = KitX.Dashboard.Network.PluginsNetwork.PluginsServer.Instance; + // var logger = Serilog.Log.Logger; + // Parser.SetPluginManager(new RealPluginManager(pluginsServer, message => logger.Information(message))); + + Console.WriteLine(" 使用 MockPluginManager 进行演示..."); + Parser.SetPluginManager(new MockPluginManager()); + + Console.WriteLine(" ✓ 插件管理器已设置"); + + // 2. 创建示例插件数据 + Console.WriteLine("\n2. 创建示例插件数据..."); + var examplePlugins = new List + { + new PluginInfo + { + Name = "SampleCalculator", + Version = "1.0.0", + Functions = new List + { + new Function + { + Name = "Add", + ReturnValueType = "int", + Parameters = new List + { + new Parameter { Name = "a", Type = "int", IsOptional = false }, + new Parameter { Name = "b", Type = "int", IsOptional = false } + } + } + } + } + }; + + Console.WriteLine(" ✓ 示例插件数据已创建"); + + // 3. 生成动态程序集 + Console.WriteLine("\n3. 生成动态程序集..."); + var assembly = Parser.Generate(examplePlugins, "RealPluginManagerExampleAssembly"); + Console.WriteLine($" ✓ 成功生成程序集: {assembly.FullName}"); + + // 4. 获取生成的插件类型 + Console.WriteLine("\n4. 分析生成的插件类型..."); + var pluginTypes = Parser.GetPluginTypes(assembly); + Console.WriteLine($" 发现 {pluginTypes.Count} 个插件类:"); + + foreach (var type in pluginTypes) + { + Console.WriteLine($" - {type.Name}"); + var methods = Parser.GetPluginMethods(type); + foreach (var method in methods) + { + var parameters = string.Join(", ", method.GetParameters().Select(p => $"{p.ParameterType.Name} {p.Name}")); + Console.WriteLine($" └── {method.ReturnType.Name} {method.Name}({parameters})"); + } + } + + Console.WriteLine("\n=== 示例完成 ==="); + Console.WriteLine("\n注意:在实际使用中,您需要:"); + Console.WriteLine("1. 确保 KitX Dashboard 正在运行"); + Console.WriteLine("2. 传入真实的 PluginsServer.Instance"); + Console.WriteLine("3. 传入真实的日志记录器实例"); + Console.WriteLine("4. 确保插件已正确连接到 Dashboard"); + } + catch (Exception ex) + { + Console.WriteLine($"❌ 示例执行失败: {ex.Message}"); + Console.WriteLine($"详细信息: {ex}"); + } + } + + /// + /// 演示如何在 KitX Dashboard 中集成 + /// + public static void DashboardIntegrationExample() + { + Console.WriteLine("=== KitX Dashboard 集成示例 ===\n"); + + Console.WriteLine("在 KitX Dashboard 中使用 RealPluginManager 的步骤:\n"); + + Console.WriteLine("1. 在 Dashboard 启动时设置插件管理器工厂:"); + Console.WriteLine("```csharp"); + Console.WriteLine("// 在 Dashboard 的初始化代码中"); + Console.WriteLine("Parser.SetPluginManagerFactory(() => new RealPluginManager("); + Console.WriteLine(" KitX.Dashboard.Network.PluginsNetwork.PluginsServer.Instance,"); + Console.WriteLine(" message => Log.Information(message)"); + Console.WriteLine("));"); + Console.WriteLine("```"); + + Console.WriteLine("\n2. 生成插件程序集:"); + Console.WriteLine("```csharp"); + Console.WriteLine("var assembly = Parser.Generate(plugins, \"DashboardPluginAssembly\");"); + Console.WriteLine("```"); + + Console.WriteLine("\n3. 在脚本中使用生成的 API:"); + Console.WriteLine("```csharp"); + Console.WriteLine("// 现在可以在脚本中直接调用"); + Console.WriteLine("var result = SampleCalculator.Add(10, 20);"); + Console.WriteLine("```"); + + Console.WriteLine("\n这样,脚本中的插件调用将通过真实的 KitX Dashboard 插件系统执行!"); + } +} diff --git a/KitX Script/Kscript.CSharp.Parser.Examples/Kscript.CSharp.Parser.Examples.csproj b/KitX Script/Kscript.CSharp.Parser.Examples/Kscript.CSharp.Parser.Examples.csproj new file mode 100644 index 0000000..64343b5 --- /dev/null +++ b/KitX Script/Kscript.CSharp.Parser.Examples/Kscript.CSharp.Parser.Examples.csproj @@ -0,0 +1,22 @@ + + + + net10.0 + preview + enable + enable + Kscript.CSharp.Parser.Examples + Exe + Kscript.CSharp.Parser.Examples.Program + + + + + + + + + + + + From a1ad5e77b681d8c4ec74582a821f3d4426171ea1 Mon Sep 17 00:00:00 2001 From: StarInk Date: Sat, 8 Nov 2025 10:35:42 +0100 Subject: [PATCH 13/60] =?UTF-8?q?=F0=9F=92=BE=20Feat(MethodEmitter,=20Asse?= =?UTF-8?q?mblyCache):=20=E6=B7=BB=E5=8A=A0=E5=8F=AF=E6=94=B6=E9=9B=86?= =?UTF-8?q?=E7=9A=84=E7=A8=8B=E5=BA=8F=E9=9B=86=E5=8A=A0=E8=BD=BD=E4=B8=8A?= =?UTF-8?q?=E4=B8=8B=E6=96=87=EF=BC=8C=E6=94=AF=E6=8C=81=E5=8A=A8=E6=80=81?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E9=9B=86=E7=9A=84=E5=8D=B8=E8=BD=BD=E4=B8=8E?= =?UTF-8?q?=E6=B8=85=E7=90=86=EF=BC=9B=E6=9B=B4=E6=96=B0Mock=E6=8F=92?= =?UTF-8?q?=E4=BB=B6=E7=AE=A1=E7=90=86=E5=99=A8=E4=BB=A5=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E6=96=B0=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CodeGen/AssemblyCache.cs | 4 +- .../CodeGen/MethodEmitter.cs | 115 +++++++++++++++++- .../Core/MockPluginManager.cs | 61 +++++++--- .../Kscript.CSharp.Parser.csproj | 6 +- 4 files changed, 159 insertions(+), 27 deletions(-) diff --git a/KitX Script/Kscript.CSharp.Parser/CodeGen/AssemblyCache.cs b/KitX Script/Kscript.CSharp.Parser/CodeGen/AssemblyCache.cs index 50f8d8a..4e0e193 100644 --- a/KitX Script/Kscript.CSharp.Parser/CodeGen/AssemblyCache.cs +++ b/KitX Script/Kscript.CSharp.Parser/CodeGen/AssemblyCache.cs @@ -70,6 +70,8 @@ private static string GenerateCacheKey(List plugins, string assembly public static void ClearCache() { _assemblyCache.Clear(); + // 清除所有程序集加载上下文 + MethodEmitter.ClearAllAssemblyContexts(); } /// @@ -111,7 +113,7 @@ public static Assembly ForceRegenerate(List plugins, Core.IPluginMan // 移除现有缓存 _assemblyCache.TryRemove(cacheKey, out _); - // 生成新的程序集 + // 生成新的程序集(这会自动卸载旧的程序集加载上下文) var newAssembly = MethodEmitter.GenerateAssembly(plugins, assemblyName, pluginManager); // 添加到缓存 diff --git a/KitX Script/Kscript.CSharp.Parser/CodeGen/MethodEmitter.cs b/KitX Script/Kscript.CSharp.Parser/CodeGen/MethodEmitter.cs index 1fe2cc8..e732fb4 100644 --- a/KitX Script/Kscript.CSharp.Parser/CodeGen/MethodEmitter.cs +++ b/KitX Script/Kscript.CSharp.Parser/CodeGen/MethodEmitter.cs @@ -1,14 +1,37 @@ using Kscript.CSharp.Parser.Core; using Kscript.CSharp.Parser.Exceptions; using Kscript.CSharp.Parser.Models; +using System.Reflection.Emit; +using System.Runtime.Loader; +using System.Collections.Concurrent; namespace Kscript.CSharp.Parser.CodeGen; +/// +/// 可收集的程序集加载上下文,支持程序集卸载 +/// +public class CollectibleAssemblyLoadContext : AssemblyLoadContext +{ + public CollectibleAssemblyLoadContext(string name) : base(name, isCollectible: true) + { + } + + protected override Assembly Load(AssemblyName assemblyName) + { + // 让默认上下文处理核心程序集加载 + return null; + } +} + /// /// IL 方法生成器,负责生成插件调用的静态方法 /// public static class MethodEmitter { + /// + /// 程序集加载上下文缓存,用于隔离和卸载程序集 + /// + private static readonly ConcurrentDictionary _loadContexts = new(); /// /// 为插件生成动态程序集 /// @@ -21,12 +44,22 @@ public static Assembly GenerateAssembly(List plugins, { try { - // 创建动态程序集 - var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly( + // 如果已存在相同名称的程序集加载上下文,先卸载它 + if (_loadContexts.TryGetValue(assemblyName, out var existingContext)) + { + UnloadAssemblyContext(assemblyName, existingContext); + } + + // 创建新的可卸载的程序集加载上下文 + var loadContext = new CollectibleAssemblyLoadContext(assemblyName); + _loadContexts.TryAdd(assemblyName, loadContext); + + // 使用PersistedAssemblyBuilder创建可保存的动态程序集 + var persistedAssemblyBuilder = new PersistedAssemblyBuilder( new AssemblyName(assemblyName), - AssemblyBuilderAccess.Run); + typeof(object).Assembly); - var moduleBuilder = assemblyBuilder.DefineDynamicModule($"{assemblyName}.dll"); + var moduleBuilder = persistedAssemblyBuilder.DefineDynamicModule($"{assemblyName}.dll"); // 为每个插件生成静态类 foreach (var plugin in plugins) @@ -34,7 +67,41 @@ public static Assembly GenerateAssembly(List plugins, GeneratePluginClass(moduleBuilder, plugin, pluginManager); } - return assemblyBuilder; + // 创建所有类型 + foreach (var plugin in plugins) + { + // 确保所有类型都已创建 + // 类型在GeneratePluginClass中已经创建 + } + + // 保存到临时文件并重新加载为可引用的程序集 + var tempPath = Path.Combine(Path.GetTempPath(), $"{assemblyName}_{Guid.NewGuid()}.dll"); + + using (var fileStream = File.Create(tempPath)) + { + persistedAssemblyBuilder.Save(fileStream); + } + + // 使用自定义加载上下文从文件加载程序集 + var assembly = loadContext.LoadFromAssemblyPath(tempPath); + + Console.WriteLine($"[MethodEmitter] 使用CollectibleAssemblyLoadContext生成并加载程序集: {assemblyName} -> {tempPath}"); + + // 注册临时文件删除(在程序退出时) + AppDomain.CurrentDomain.ProcessExit += (s, e) => + { + try + { + if (File.Exists(tempPath)) + File.Delete(tempPath); + } + catch + { + // 忽略删除错误 + } + }; + + return assembly; } catch (Exception ex) { @@ -42,6 +109,44 @@ public static Assembly GenerateAssembly(List plugins, } } + /// + /// 卸载程序集加载上下文 + /// + /// 程序集名称 + /// 要卸载的加载上下文 + private static void UnloadAssemblyContext(string assemblyName, CollectibleAssemblyLoadContext context) + { + try + { + Console.WriteLine($"[MethodEmitter] 卸载程序集加载上下文: {assemblyName}"); + + // 从缓存中移除 + _loadContexts.TryRemove(assemblyName, out _); + + // 卸载上下文 + context.Unload(); + + // 等待卸载完成 + GC.Collect(); + GC.WaitForPendingFinalizers(); + } + catch (Exception ex) + { + Console.WriteLine($"[MethodEmitter] 卸载程序集加载上下文失败: {assemblyName}, 错误: {ex.Message}"); + } + } + + /// + /// 清除所有程序集加载上下文 + /// + public static void ClearAllAssemblyContexts() + { + foreach (var kvp in _loadContexts) + { + UnloadAssemblyContext(kvp.Key, kvp.Value); + } + } + /// /// 为单个插件生成静态类 /// diff --git a/KitX Script/Kscript.CSharp.Parser/Core/MockPluginManager.cs b/KitX Script/Kscript.CSharp.Parser/Core/MockPluginManager.cs index 083c602..ecc1710 100644 --- a/KitX Script/Kscript.CSharp.Parser/Core/MockPluginManager.cs +++ b/KitX Script/Kscript.CSharp.Parser/Core/MockPluginManager.cs @@ -16,6 +16,7 @@ public class MockPluginManager : IPluginManager "SampleCalculator", new Dictionary { { "Add", (int a, int b) => a + b }, + { "Multiply", (double x, double y) => x * y }, { "Divide", (double numerator, double denominator, int decimals = 2) => Math.Round(numerator / denominator, decimals) } } }, @@ -23,7 +24,14 @@ public class MockPluginManager : IPluginManager "StringToolkit", new Dictionary { { "Reverse", (string text) => new string(text.Reverse().ToArray()) }, - { "ToUpper", (string text) => text.ToUpperInvariant() } + { "ToUpper", (string text) => text.ToUpperInvariant() }, + { "Concat", (string str1, string str2) => str1 + str2 } + } + }, + { + "KitXWF", new Dictionary + { + { "Print", (string message) => Console.WriteLine(message) } } } }; @@ -108,24 +116,43 @@ public bool IsMethodExists(string pluginName, string methodName) private object? InvokeMockMethod(PluginCallInfo callInfo, object method) { // 根据插件和方法名进行简单的模拟计算 - return (callInfo.PluginName, callInfo.MethodName) switch + switch (callInfo.PluginName) { - ("SampleCalculator", "Add") when callInfo.Parameters.Length >= 2 => - Convert.ToInt32(callInfo.Parameters[0]) + Convert.ToInt32(callInfo.Parameters[1]), - - ("SampleCalculator", "Divide") when callInfo.Parameters.Length >= 2 => - callInfo.Parameters.Length >= 3 - ? Math.Round(Convert.ToDouble(callInfo.Parameters[0]) / Convert.ToDouble(callInfo.Parameters[1]), Convert.ToInt32(callInfo.Parameters[2])) - : Math.Round(Convert.ToDouble(callInfo.Parameters[0]) / Convert.ToDouble(callInfo.Parameters[1]), 2), - - ("StringToolkit", "Reverse") when callInfo.Parameters.Length >= 1 => - new string(callInfo.Parameters[0].ToString()?.Reverse().ToArray() ?? Array.Empty()), - - ("StringToolkit", "ToUpper") when callInfo.Parameters.Length >= 1 => - callInfo.Parameters[0].ToString()?.ToUpperInvariant() ?? string.Empty, + case "SampleCalculator": + switch (callInfo.MethodName) + { + case "Add" when callInfo.Parameters.Length >= 2: + return Convert.ToInt32(callInfo.Parameters[0]) + Convert.ToInt32(callInfo.Parameters[1]); + case "Multiply" when callInfo.Parameters.Length >= 2: + return Convert.ToDouble(callInfo.Parameters[0]) * Convert.ToDouble(callInfo.Parameters[1]); + case "Divide" when callInfo.Parameters.Length >= 2: + return callInfo.Parameters.Length >= 3 + ? Math.Round(Convert.ToDouble(callInfo.Parameters[0]) / Convert.ToDouble(callInfo.Parameters[1]), Convert.ToInt32(callInfo.Parameters[2])) + : Math.Round(Convert.ToDouble(callInfo.Parameters[0]) / Convert.ToDouble(callInfo.Parameters[1]), 2); + } + break; + case "KitXWF": + switch (callInfo.MethodName) + { + case "Print" when callInfo.Parameters.Length >= 1: + Console.WriteLine($"[KitXWF] {callInfo.Parameters[0]?.ToString() ?? ""}"); + return null; + } + break; + case "StringToolkit": + switch (callInfo.MethodName) + { + case "Reverse" when callInfo.Parameters.Length >= 1: + return new string(callInfo.Parameters[0].ToString()?.Reverse().ToArray() ?? Array.Empty()); + case "ToUpper" when callInfo.Parameters.Length >= 1: + return callInfo.Parameters[0].ToString()?.ToUpperInvariant() ?? string.Empty; + case "Concat" when callInfo.Parameters.Length >= 2: + return callInfo.Parameters[0].ToString() + callInfo.Parameters[1].ToString(); + } + break; + } - _ => $"MockResult_{callInfo.MethodName}" - }; + return $"MockResult_{callInfo.MethodName}"; } /// diff --git a/KitX Script/Kscript.CSharp.Parser/Kscript.CSharp.Parser.csproj b/KitX Script/Kscript.CSharp.Parser/Kscript.CSharp.Parser.csproj index 03feb01..fe53ec8 100644 --- a/KitX Script/Kscript.CSharp.Parser/Kscript.CSharp.Parser.csproj +++ b/KitX Script/Kscript.CSharp.Parser/Kscript.CSharp.Parser.csproj @@ -1,13 +1,11 @@  - net8.0 - 12 + net10.0 + preview enable enable Kscript.CSharp.Parser - Exe - Kscript.CSharp.Parser.Examples.Program From 92fa4d1f0ad670cc2036ee92cd9f06089ff2b71f Mon Sep 17 00:00:00 2001 From: StarInk Date: Sat, 8 Nov 2025 10:36:40 +0100 Subject: [PATCH 14/60] =?UTF-8?q?=F0=9F=92=BE=20Feat(Examples):=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=AB=AF=E5=88=B0=E7=AB=AF=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E5=92=8C=E8=84=9A=E6=9C=AC=E6=89=A7=E8=A1=8C=E7=A4=BA=E4=BE=8B?= =?UTF-8?q?=EF=BC=8C=E9=AA=8C=E8=AF=81=E6=8F=92=E4=BB=B6=E8=B0=83=E7=94=A8?= =?UTF-8?q?=E9=93=BE=E5=92=8C=E8=84=9A=E6=9C=AC=E6=89=A7=E8=A1=8C=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Examples/EndToEndTest.cs | 392 ++++++++++++++++++ .../Examples/ScriptExecutionExample.cs | 259 ++++++++++++ .../Kscript.CSharp.Compiler.csproj | 22 + .../Kscript.CSharp.Compiler/Program.cs | 44 ++ .../Kscript.CSharp.Compiler/ScriptExecutor.cs | 262 ++++++++++++ 5 files changed, 979 insertions(+) create mode 100644 KitX Script/Kscript.CSharp.Compiler/Examples/EndToEndTest.cs create mode 100644 KitX Script/Kscript.CSharp.Compiler/Examples/ScriptExecutionExample.cs create mode 100644 KitX Script/Kscript.CSharp.Compiler/Kscript.CSharp.Compiler.csproj create mode 100644 KitX Script/Kscript.CSharp.Compiler/Program.cs create mode 100644 KitX Script/Kscript.CSharp.Compiler/ScriptExecutor.cs diff --git a/KitX Script/Kscript.CSharp.Compiler/Examples/EndToEndTest.cs b/KitX Script/Kscript.CSharp.Compiler/Examples/EndToEndTest.cs new file mode 100644 index 0000000..1cc63e8 --- /dev/null +++ b/KitX Script/Kscript.CSharp.Compiler/Examples/EndToEndTest.cs @@ -0,0 +1,392 @@ +using Kscript.CSharp.Parser; +using Kscript.CSharp.Parser.Core; +using Kscript.CSharp.Compiler; +using Kscript.CSharp.Parser.Models; +using KitX.Shared.CSharp.Plugin; +using System.Reflection; + +namespace Kscript.CSharp.Compiler.Examples; + +/// +/// 端到端测试:用户脚本 → Compiler → Parser → PluginManager调用 +/// 验证完整的调用链是否正常工作 +/// +public static class EndToEndTest +{ + /// + /// 运行端到端测试 + /// + public static async Task RunTest() + { + Console.WriteLine("=== KitX.CSharp.Compiler 端到端测试 ===\n"); + + try + { + // 1. 初始化插件管理器 + Console.WriteLine("1. 初始化插件管理器..."); + Parser.Parser.SetPluginManager(new MockPluginManager()); + + // 2. 创建测试插件清单 + Console.WriteLine("2. 创建测试插件清单..."); + var plugins = CreateTestPlugins(); + + // 3. 生成动态程序集 + Console.WriteLine("3. 生成动态程序集..."); + var assembly = Parser.Parser.Generate(plugins, "EndToEndTestAssembly"); + Console.WriteLine($" ✓ 成功生成程序集: {assembly.FullName}"); + + // 4. 验证生成的程序集 + Console.WriteLine("\n4. 验证生成的程序集..."); + await ValidateGeneratedAssembly(assembly); + + // 5. 创建脚本执行器并测试 + Console.WriteLine("\n5. 创建脚本执行器并测试..."); + await TestScriptExecution(assembly); + + // 6. 测试错误处理 + Console.WriteLine("\n6. 测试错误处理..."); + await TestErrorHandling(assembly); + + // 7. 性能测试 + Console.WriteLine("\n7. 性能测试..."); + await PerformanceTest(assembly); + + Console.WriteLine("\n=== 端到端测试完成 ==="); + Console.WriteLine("✅ 所有测试通过,C#脚本可以成功调用生成的插件方法!"); + } + catch (Exception ex) + { + Console.WriteLine($"❌ 端到端测试失败: {ex.Message}"); + Console.WriteLine($"详细错误信息:\n{ex}"); + } + } + + /// + /// 创建测试插件数据 + /// + /// 插件信息列表 + private static List CreateTestPlugins() + { + return new List + { + new PluginInfo + { + Name = "SampleCalculator", + Version = "1.0.0", + Functions = new List + { + new Function + { + Name = "Add", + ReturnValueType = "int", + Parameters = new List + { + new Parameter { Name = "a", Type = "int", IsOptional = false }, + new Parameter { Name = "b", Type = "int", IsOptional = false } + } + }, + new Function + { + Name = "Multiply", + ReturnValueType = "double", + Parameters = new List + { + new Parameter { Name = "x", Type = "double", IsOptional = false }, + new Parameter { Name = "y", Type = "double", IsOptional = false } + } + }, + new Function + { + Name = "Divide", + ReturnValueType = "double", + Parameters = new List + { + new Parameter { Name = "numerator", Type = "double", IsOptional = false }, + new Parameter { Name = "denominator", Type = "double", IsOptional = false }, + new Parameter { Name = "decimals", Type = "int", IsOptional = true, Value = "2" } + } + } + } + }, + new PluginInfo + { + Name = "StringToolkit", + Version = "1.0.0", + Functions = new List + { + new Function + { + Name = "Reverse", + ReturnValueType = "string", + Parameters = new List + { + new Parameter { Name = "text", Type = "string", IsOptional = false } + } + }, + new Function + { + Name = "Concat", + ReturnValueType = "string", + Parameters = new List + { + new Parameter { Name = "str1", Type = "string", IsOptional = false }, + new Parameter { Name = "str2", Type = "string", IsOptional = false } + } + } + } + }, + new PluginInfo + { + Name = "KitXWF", + Version = "1.0.0", + Functions = new List + { + new Function + { + Name = "Print", + ReturnValueType = "void", + Parameters = new List + { + new Parameter { Name = "message", Type = "string", IsOptional = false } + } + } + } + } + }; + } + + /// + /// 验证生成的程序集 + /// + /// 生成的程序集 + private static async Task ValidateGeneratedAssembly(Assembly assembly) + { + Console.WriteLine(" 验证程序集结构..."); + + // 获取所有插件类型 + var types = Parser.Parser.GetPluginTypes(assembly); + Console.WriteLine($" 发现 {types.Count} 个插件类型:"); + + foreach (var type in types) + { + Console.WriteLine($" - {type.Name}"); + + // 获取所有方法 + var methods = Parser.Parser.GetPluginMethods(type); + Console.WriteLine($" 包含 {methods.Count} 个方法:"); + + foreach (var method in methods) + { + var parameters = method.GetParameters(); + var paramList = string.Join(", ", parameters.Select(p => $"{p.ParameterType.Name} {p.Name}")); + Console.WriteLine($" - {method.ReturnType.Name} {method.Name}({paramList})"); + } + } + + Console.WriteLine(" ✓ 程序集结构验证完成"); + } + + /// + /// 测试脚本执行 + /// + /// 生成的程序集 + private static async Task TestScriptExecution(Assembly assembly) + { + using var executor = new ScriptExecutor(); + + // 添加程序集引用 + executor.AddAssemblyReference(assembly); + Console.WriteLine(" ✓ 已添加程序集引用到脚本执行器"); + + // 测试基本计算脚本 + Console.WriteLine("\n 测试基本计算脚本..."); + string basicScript = @" + // 测试加法 + var sum = SampleCalculator.Add(10, 20); + KitXWF.Print(string.Format(""加法结果: {0}"", sum)); + + // 测试乘法 + var product = SampleCalculator.Multiply(3.5, 2.8); + KitXWF.Print(string.Format(""乘法结果: {0}"", product)); + + // 测试字符串反转 + var reversed = StringToolkit.Reverse(""Hello KitX""); + KitXWF.Print(string.Format(""字符串反转结果: {0}"", reversed)); + + return new { Sum = sum, Product = product, Reversed = reversed }; + "; + + var basicResult = await executor.ExecuteAsync(basicScript, "BasicTest"); + Console.WriteLine($" ✓ 基本脚本执行成功: Sum={basicResult.Sum}, Product={basicResult.Product}, Reversed={basicResult.Reversed}"); + + // 测试复杂业务逻辑脚本 + Console.WriteLine("\n 测试复杂业务逻辑脚本..."); + string complexScript = @" + // 模拟订单处理 + var orderItems = new[] + { + new { Name = ""商品A"", Price = 10.5, Quantity = 2 }, + new { Name = ""商品B"", Price = 25.0, Quantity = 1 }, + new { Name = ""商品C"", Price = 5.75, Quantity = 3 } + }; + + var totalAmount = 0.0; + var itemCount = orderItems.Length; + + foreach (var item in orderItems) + { + var itemTotal = SampleCalculator.Multiply(item.Price, (double)item.Quantity); + totalAmount = totalAmount + itemTotal; + KitXWF.Print(string.Format(""处理商品: {0}, 小计: {1}"", item.Name, itemTotal)); + } + + // 计算平均价格 + var averagePrice = SampleCalculator.Divide(totalAmount, (double)itemCount, 2); + + // 生成订单摘要 + var summary = StringToolkit.Concat(string.Format(""订单总额: {0}, 平均价格: {1}"", totalAmount, averagePrice), "" (已处理)""); + + return new { + TotalAmount = totalAmount, + ItemCount = itemCount, + AveragePrice = averagePrice, + Summary = summary + }; + "; + + var complexResult = await executor.ExecuteAsync(complexScript, "ComplexTest"); + Console.WriteLine($" ✓ 复杂脚本执行成功: Total={complexResult.TotalAmount}, Items={complexResult.ItemCount}, Avg={complexResult.AveragePrice}"); + + // 测试脚本验证功能 + Console.WriteLine("\n 测试脚本验证功能..."); + var validScript = "var x = 10; return x * 2;"; + var invalidScript = "var x = 10; return x *; // 语法错误"; + + var validValidation = executor.ValidateScript(validScript); + var invalidValidation = executor.ValidateScript(invalidScript); + + Console.WriteLine($" ✓ 有效脚本验证: {validValidation.IsValid} - {validValidation.Message}"); + Console.WriteLine($" ✓ 无效脚本验证: {!invalidValidation.IsValid} - {invalidValidation.Message}"); + + // 显示执行器状态 + var status = executor.GetStatus(); + Console.WriteLine($" 执行器状态: {status.ReferencedAssembliesCount} 个程序集, {status.GlobalVariablesCount} 个全局变量"); + } + + /// + /// 测试错误处理 + /// + /// 生成的程序集 + private static async Task TestErrorHandling(Assembly assembly) + { + using var executor = new ScriptExecutor(); + executor.AddAssemblyReference(assembly); + + // 测试编译错误 + Console.WriteLine(" 测试编译错误处理..."); + try + { + var syntaxErrorScript = "var x = 10; var y = ; return x + y;"; // 故意语法错误 + await executor.ExecuteAsync(syntaxErrorScript); + Console.WriteLine(" ❌ 应该捕获编译错误但没有"); + } + catch (ScriptExecutionException ex) + { + Console.WriteLine($" ✓ 成功捕获编译错误: {ex.Message}"); + } + + // 测试运行时错误 + Console.WriteLine("\n 测试运行时错误处理..."); + try + { + var runtimeErrorScript = @" + var result = SampleCalculator.Divide(10, 0); // 除零错误 + return result; + "; + + await executor.ExecuteAsync(runtimeErrorScript); + Console.WriteLine(" ❌ 应该捕获运行时错误但没有"); + } + catch (ScriptExecutionException ex) + { + Console.WriteLine($" ✓ 成功捕获运行时错误: {ex.Message}"); + } + + // 测试插件方法不存在错误 + Console.WriteLine("\n 测试插件方法不存在错误..."); + try + { + var methodNotFoundScript = "var result = SampleCalculator.NonExistentMethod(10, 20); return result;"; + await executor.ExecuteAsync(methodNotFoundScript); + Console.WriteLine(" ❌ 应该捕获方法不存在错误但没有"); + } + catch (ScriptExecutionException ex) + { + Console.WriteLine($" ✓ 成功捕获方法不存在错误: {ex.Message}"); + } + } + + /// + /// 性能测试 + /// + /// 生成的程序集 + private static async Task PerformanceTest(Assembly assembly) + { + using var executor = new ScriptExecutor(); + executor.AddAssemblyReference(assembly); + + Console.WriteLine(" 执行性能测试..."); + + var testScript = @" + using System.Collections.Generic; + var results = new List(); + for (int i = 0; i < 10; i++) + { + var sum = SampleCalculator.Add(i, i * 2); + results.Add(sum); + } + return results.Count; + "; + + var stopwatch = System.Diagnostics.Stopwatch.StartNew(); + + // 执行多次取平均值 + const int iterations = 10; + var times = new List(); + + for (int i = 0; i < iterations; i++) + { + stopwatch.Restart(); + await executor.ExecuteAsync(testScript, $"PerfTest_{i}"); + times.Add(stopwatch.ElapsedMilliseconds); + } + + var avgTime = times.Average(); + var minTime = times.Min(); + var maxTime = times.Max(); + + Console.WriteLine($" 性能测试结果 ({iterations} 次执行):"); + Console.WriteLine($" - 平均执行时间: {avgTime:F2} ms"); + Console.WriteLine($" - 最快执行时间: {minTime} ms"); + Console.WriteLine($" - 最慢执行时间: {maxTime} ms"); + Console.WriteLine($" - 性能评级: {GetPerformanceRating(avgTime)}"); + } + + /// + /// 获取性能评级 + /// + /// 平均执行时间 + /// 性能评级 + private static string GetPerformanceRating(double avgTime) + { + return avgTime switch + { + < 10 => "优秀", + < 50 => "良好", + < 100 => "一般", + < 200 => "较差", + _ => "很差" + }; + } +} + diff --git a/KitX Script/Kscript.CSharp.Compiler/Examples/ScriptExecutionExample.cs b/KitX Script/Kscript.CSharp.Compiler/Examples/ScriptExecutionExample.cs new file mode 100644 index 0000000..149319a --- /dev/null +++ b/KitX Script/Kscript.CSharp.Compiler/Examples/ScriptExecutionExample.cs @@ -0,0 +1,259 @@ +using Kscript.CSharp.Parser; +using Kscript.CSharp.Parser.Core; +using Kscript.CSharp.Compiler; +using Kscript.CSharp.Parser.Models; +using System.Text.Json; +using System.Reflection; +using KitX.Shared.CSharp.Plugin; + +namespace Kscript.CSharp.Compiler.Examples; + +/// +/// C#脚本执行示例 +/// +public static class ScriptExecutionExample +{ + /// + /// 运行脚本执行示例 + /// + public static async Task RunExample() + { + Console.WriteLine("=== Kscript.CSharp.Compiler 脚本执行示例 ===\n"); + + try + { + // 1. 初始化插件管理器 + Console.WriteLine("1. 初始化插件管理器..."); + Parser.Parser.SetPluginManager(new MockPluginManager()); + + // 2. 创建示例插件数据 + var plugins = new List + { + new PluginInfo + { + Name = "SampleCalculator", + Version = "1.0.0", + Functions = new List + { + new Function + { + Name = "Add", + ReturnValueType = "int", + Parameters = new List + { + new Parameter { Name = "a", Type = "int", IsOptional = false }, + new Parameter { Name = "b", Type = "int", IsOptional = false } + } + }, + new Function + { + Name = "Multiply", + ReturnValueType = "double", + Parameters = new List + { + new Parameter { Name = "x", Type = "double", IsOptional = false }, + new Parameter { Name = "y", Type = "double", IsOptional = false } + } + } + } + }, + new PluginInfo + { + Name = "StringToolkit", + Version = "1.0.0", + Functions = new List + { + new Function + { + Name = "Reverse", + ReturnValueType = "string", + Parameters = new List + { + new Parameter { Name = "text", Type = "string", IsOptional = false } + } + }, + new Function + { + Name = "ToUpper", + ReturnValueType = "string", + Parameters = new List + { + new Parameter { Name = "text", Type = "string", IsOptional = false } + } + } + } + }, + new PluginInfo + { + Name = "KitXWF", + Version = "1.0.0", + Functions = new List + { + new Function + { + Name = "Print", + ReturnValueType = "void", + Parameters = new List + { + new Parameter { Name = "message", Type = "string", IsOptional = false } + } + } + } + } + }; + + // 3. 生成动态程序集 + Console.WriteLine("2. 生成动态程序集..."); + var assembly = Parser.Parser.Generate(plugins, "ScriptExampleAssembly"); + Console.WriteLine($" ✓ 成功生成程序集: {assembly.FullName}"); + + // 4. 创建脚本执行器 + Console.WriteLine("\n3. 创建脚本执行器..."); + using var executor = new ScriptExecutor(); + + // 5. 添加程序集引用 + executor.AddAssemblyReference(assembly); + Console.WriteLine(" ✓ 已添加程序集引用"); + + // 6. 设置全局变量 + executor.SetGlobalVariable("PI", Math.PI); + executor.SetGlobalVariable("AppName", "KitX Script Engine"); + Console.WriteLine(" ✓ 已设置全局变量"); + + // 7. 执行简单计算脚本 + Console.WriteLine("\n4. 执行简单计算脚本..."); + string simpleScript = @" + var sum = SampleCalculator.Add(15, 25); + var product = SampleCalculator.Multiply(3.5, 2.8); + return new { Sum = sum, Product = product }; + "; + + var simpleResult = await executor.ExecuteAsync(simpleScript); + Console.WriteLine($" 计算结果: Sum = {simpleResult.Sum}, Product = {simpleResult.Product}"); + + // 8. 执行字符串处理脚本 + Console.WriteLine("\n5. 执行字符串处理脚本..."); + string stringScript = @" + var reversed = StringToolkit.Reverse(""Hello World""); + var upper = StringToolkit.ToUpper(""kitx script""); + return new { Original = ""Hello World"", Reversed = reversed, Upper = upper }; + "; + + var stringResult = await executor.ExecuteAsync(stringScript); + Console.WriteLine($" 字符串处理结果:"); + Console.WriteLine($" Original: {stringResult.Original}"); + Console.WriteLine($" Reversed: {stringResult.Reversed}"); + Console.WriteLine($" Upper: {stringResult.Upper}"); + + // 9. 执行复杂业务逻辑脚本 + Console.WriteLine("\n6. 执行复杂业务逻辑脚本..."); + string complexScript = @" + // 模拟一个简单的业务场景:计算订单总价 + var orderItems = new[] + { + new { Name = ""商品A"", Price = 10.5, Quantity = 2 }, + new { Name = ""商品B"", Price = 25.0, Quantity = 1 }, + new { Name = ""商品C"", Price = 5.75, Quantity = 3 } + }; + + var total = 0.0; + foreach (var item in orderItems) + { + var itemTotal = SampleCalculator.Multiply(item.Price, item.Quantity); + total = total + itemTotal; // 直接使用double加法 + KitXWF.Print(string.Format(""商品: {0}, 单价: {1}, 数量: {2}, 小计: {3}"", item.Name, item.Price, item.Quantity, itemTotal)); + } + + // 使用字符串处理生成订单摘要 + var summary = StringToolkit.ToUpper(string.Format(""订单总价: {0}"", total)); + return new { Total = total, Summary = summary, ItemCount = orderItems.Length }; + "; + + var complexResult = await executor.ExecuteAsync(complexScript); + Console.WriteLine($" 复杂业务逻辑结果:"); + Console.WriteLine($" 订单总价: {complexResult.Total}"); + Console.WriteLine($" 订单摘要: {complexResult.Summary}"); + Console.WriteLine($" 商品数量: {complexResult.ItemCount}"); + + // 10. 显示执行器状态 + Console.WriteLine("\n7. 脚本执行器状态:"); + var status = executor.GetStatus(); + Console.WriteLine($" 引用程序集数量: {status.ReferencedAssembliesCount}"); + Console.WriteLine($" 全局变量数量: {status.GlobalVariablesCount}"); + Console.WriteLine($" 命名空间数量: {status.UsingsCount}"); + Console.WriteLine($" 程序集列表: {string.Join("", "", status.AssemblyNames)}"); + + // 11. 脚本验证示例 + Console.WriteLine("\n8. 脚本验证示例:"); + var validScript = "var x = 10; var y = 20; return x + y;"; + var invalidScript = "var x = 10; var y = ; return x + y;"; // 故意的语法错误 + + var validResult = executor.ValidateScript(validScript); + Console.WriteLine($" 有效脚本验证: {validResult.IsValid} - {validResult.Message}"); + + var invalidResult = executor.ValidateScript(invalidScript); + Console.WriteLine($" 无效脚本验证: {invalidResult.IsValid} - {invalidResult.Message}"); + } + catch (Exception ex) + { + Console.WriteLine($"❌ 示例执行失败: {ex.Message}"); + Console.WriteLine($"详细错误信息:\n{ex}"); + } + + Console.WriteLine("\n=== 脚本执行示例完成 ==="); + } + + /// + /// 演示脚本安全性和错误处理 + /// + public static async Task RunSecurityExample() + { + Console.WriteLine("=== 脚本安全性示例 ===\n"); + + using var executor = new ScriptExecutor(); + + // 1. 测试无限循环检测 + Console.WriteLine("1. 测试无限循环检测..."); + string infiniteLoopScript = @" + while (true) + { + // 这会导致无限循环 + } + "; + + try + { + // 设置超时机制(CSharpScript本身不支持,但可以通过其他方式实现) + var task = executor.ExecuteAsync(infiniteLoopScript); + var timeoutTask = Task.Delay(5000); // 5秒超时 + + var completedTask = await Task.WhenAny(task, timeoutTask); + if (completedTask == timeoutTask) + { + Console.WriteLine(" ✓ 检测到潜在的超时/无限循环"); + } + } + catch (Exception ex) + { + Console.WriteLine($" ✓ 安全机制生效: {ex.Message}"); + } + + // 2. 测试恶意代码检测 + Console.WriteLine("\n2. 测试恶意代码检测..."); + string maliciousScript = @" + // 尝试访问文件系统 + System.IO.File.WriteAllText(""malicious.txt"", ""This is malicious""); + return ""File created""; + "; + + try + { + var result = await executor.ExecuteAsync(maliciousScript); + Console.WriteLine(" ⚠️ 警告: 脚本执行成功,可能存在安全风险"); + } + catch (Exception ex) + { + Console.WriteLine($" ✓ 安全机制阻止了恶意操作: {ex.Message}"); + } + } +} diff --git a/KitX Script/Kscript.CSharp.Compiler/Kscript.CSharp.Compiler.csproj b/KitX Script/Kscript.CSharp.Compiler/Kscript.CSharp.Compiler.csproj new file mode 100644 index 0000000..1cadee8 --- /dev/null +++ b/KitX Script/Kscript.CSharp.Compiler/Kscript.CSharp.Compiler.csproj @@ -0,0 +1,22 @@ + + + + net10.0 + preview + enable + enable + Kscript.CSharp.Compiler + Exe + + + + + + + + + + + + + diff --git a/KitX Script/Kscript.CSharp.Compiler/Program.cs b/KitX Script/Kscript.CSharp.Compiler/Program.cs new file mode 100644 index 0000000..75319a1 --- /dev/null +++ b/KitX Script/Kscript.CSharp.Compiler/Program.cs @@ -0,0 +1,44 @@ +using Kscript.CSharp.Compiler.Examples; + +namespace Kscript.CSharp.Compiler; + +/// +/// Kscript.CSharp.Compiler 主程序入口 +/// +public static class Program +{ + /// + /// 主入口方法 + /// + /// 命令行参数 + public static async Task Main(string[] args) + { + Console.OutputEncoding = System.Text.Encoding.UTF8; + Console.WriteLine("🚀 欢迎使用 KitX.CSharp.Compiler!"); + Console.WriteLine("==================================================\n"); + + try + { + // 运行脚本执行示例 + await ScriptExecutionExample.RunExample(); + + Console.WriteLine("\n" + new string('=', 60)); + + // 运行端到端测试 + await EndToEndTest.RunTest(); + + Console.WriteLine("\n=================================================="); + Console.WriteLine("✅ 所有示例运行完成!"); + Console.WriteLine("\n按任意键退出..."); + Console.ReadKey(); + } + catch (Exception ex) + { + Console.WriteLine($"\n❌ 程序运行失败: {ex.Message}"); + Console.WriteLine($"详细错误信息:\n{ex}"); + + Console.WriteLine("\n按任意键退出..."); + Console.ReadKey(); + } + } +} diff --git a/KitX Script/Kscript.CSharp.Compiler/ScriptExecutor.cs b/KitX Script/Kscript.CSharp.Compiler/ScriptExecutor.cs new file mode 100644 index 0000000..6ec1c98 --- /dev/null +++ b/KitX Script/Kscript.CSharp.Compiler/ScriptExecutor.cs @@ -0,0 +1,262 @@ +using Microsoft.CodeAnalysis.CSharp.Scripting; +using Microsoft.CodeAnalysis.Scripting; +using System.Reflection; +using System.IO; +using System.Reflection.Emit; +using Kscript.CSharp.Parser.Core; +using Kscript.CSharp.Parser.Models; + +namespace Kscript.CSharp.Compiler; + +/// +/// C#脚本执行器,用于执行用户编写的C#脚本并调用生成的插件方法 +/// +public class ScriptExecutor : IDisposable +{ + private readonly Dictionary _globalVariables = new(); + private readonly List _referencedAssemblies = new(); + private readonly List _usings = new(); + private ScriptOptions? _scriptOptions; + private bool _disposed = false; + + /// + /// 构造函数 + /// + public ScriptExecutor() + { + _scriptOptions = ScriptOptions.Default + .WithImports("System", "System.Math", "System.Collections.Generic", "System.Console", "System.Linq") + .WithEmitDebugInformation(true); + } + + /// + /// 添加程序集引用 + /// + /// 要引用的程序集 + public void AddAssemblyReference(Assembly assembly) + { + if (assembly == null) + throw new ArgumentNullException(nameof(assembly)); + + if (!_referencedAssemblies.Contains(assembly)) + { + _referencedAssemblies.Add(assembly); + Console.WriteLine($"[ScriptExecutor] 已添加程序集引用: {assembly.FullName}"); + } + } + + /// + /// 添加命名空间引用 + /// + /// 命名空间 + public void AddUsing(string @namespace) + { + if (string.IsNullOrEmpty(@namespace)) + throw new ArgumentException("命名空间不能为空", nameof(@namespace)); + + if (!_usings.Contains(@namespace)) + { + _usings.Add(@namespace); + Console.WriteLine($"[ScriptExecutor] 已添加命名空间引用: {@namespace}"); + } + } + + /// + /// 设置全局变量 + /// + /// 变量名 + /// 变量值 + public void SetGlobalVariable(string name, object value) + { + if (string.IsNullOrEmpty(name)) + throw new ArgumentException("变量名不能为空", nameof(name)); + + _globalVariables[name] = value; + Console.WriteLine($"[ScriptExecutor] 已设置全局变量: {name} = {value}"); + } + + /// + /// 执行C#脚本 + /// + /// 返回值类型 + /// 脚本代码 + /// 程序集名称(用于调试) + /// 执行结果 + public async Task ExecuteAsync(string script, string assemblyName = "ScriptAssembly") + { + if (string.IsNullOrEmpty(script)) + throw new ArgumentException("脚本代码不能为空", nameof(script)); + + try + { + Console.WriteLine($"[ScriptExecutor] 开始执行脚本: {assemblyName}"); + Console.WriteLine($"[ScriptExecutor] 脚本代码: {script.Substring(0, Math.Min(script.Length, 100))}..."); + + // 更新脚本选项,包含所有引用 + var options = ScriptOptions.Default + .WithReferences(_referencedAssemblies) + .WithImports(_usings.Concat(new[] { "System.Console" })) + .WithEmitDebugInformation(true); + + // 创建脚本对象 + var csharpScript = CSharpScript.Create(script, options); + + // 执行脚本(不使用全局变量) + var result = await csharpScript.RunAsync(null); + + Console.WriteLine($"[ScriptExecutor] 脚本执行成功,结果: {result.ReturnValue}"); + return result.ReturnValue; + } + catch (CompilationErrorException ex) + { + Console.WriteLine($"[ScriptExecutor] 脚本编译错误: {ex.Message}"); + throw new ScriptExecutionException($"脚本编译失败: {ex.Message}", ex); + } + catch (Exception ex) + { + Console.WriteLine($"[ScriptExecutor] 脚本执行异常: {ex.Message}"); + throw new ScriptExecutionException($"脚本执行失败: {ex.Message}", ex); + } + } + + /// + /// 执行脚本(无返回值) + /// + /// 脚本代码 + /// 程序集名称 + public async Task ExecuteAsync(string script, string assemblyName = "ScriptAssembly") + { + await ExecuteAsync(script, assemblyName); + } + + /// + /// 验证脚本语法 + /// + /// 脚本代码 + /// 验证结果 + public ScriptValidationResult ValidateScript(string script) + { + if (string.IsNullOrEmpty(script)) + return new ScriptValidationResult(false, "脚本代码不能为空"); + + try + { + // 更新脚本选项,包含所有引用 + var options = ScriptOptions.Default + .WithReferences(_referencedAssemblies) + .WithImports(_usings.Concat(new[] { "System.Console" })) + .WithEmitDebugInformation(true); + + var csharpScript = CSharpScript.Create(script, options); + + // 尝试编译以检查语法 + var diagnostics = csharpScript.Compile(); + + if (!diagnostics.Any()) + { + return new ScriptValidationResult(true, "脚本语法正确"); + } + else + { + var errors = string.Join("\n", diagnostics.Select(d => d.ToString())); + return new ScriptValidationResult(false, $"脚本语法错误:\n{errors}"); + } + } + catch (Exception ex) + { + return new ScriptValidationResult(false, $"脚本验证异常: {ex.Message}"); + } + } + + /// + /// 获取当前状态信息 + /// + /// 状态信息 + public ScriptExecutorStatus GetStatus() + { + return new ScriptExecutorStatus + { + ReferencedAssembliesCount = _referencedAssemblies.Count, + GlobalVariablesCount = _globalVariables.Count, + UsingsCount = _usings.Count, + AssemblyNames = _referencedAssemblies.Select(a => a.GetName().Name).ToList() + }; + } + + /// + /// 清除所有引用和变量 + /// + public void Clear() + { + _referencedAssemblies.Clear(); + _globalVariables.Clear(); + _usings.Clear(); + Console.WriteLine("[ScriptExecutor] 已清除所有引用和变量"); + } + + /// + /// 释放资源 + /// + public void Dispose() + { + if (!_disposed) + { + Clear(); + _disposed = true; + Console.WriteLine("[ScriptExecutor] 资源已释放"); + } + } +} + +/// +/// 脚本全局变量容器 +/// +public class ScriptGlobals +{ + private readonly Dictionary _variables; + + public ScriptGlobals(Dictionary variables) + { + _variables = variables ?? new Dictionary(); + } + + /// + /// 动态属性访问器 + /// + public object this[string name] + { + get + { + if (_variables.TryGetValue(name, out var value)) + return value; + throw new KeyNotFoundException($"未找到全局变量: {name}"); + } + } +} + +/// +/// 脚本验证结果 +/// +public record ScriptValidationResult(bool IsValid, string Message); + +/// +/// 脚本执行器状态 +/// +public record ScriptExecutorStatus +{ + public int ReferencedAssembliesCount { get; init; } + public int GlobalVariablesCount { get; init; } + public int UsingsCount { get; init; } + public List AssemblyNames { get; init; } = new(); +} + +/// +/// 脚本执行异常 +/// +public class ScriptExecutionException : Exception +{ + public ScriptExecutionException(string message, Exception? innerException = null) + : base(message, innerException) + { + } +} From e80de9d7837165b2b00dd23bba9307fba979da75 Mon Sep 17 00:00:00 2001 From: StarInk Date: Sat, 8 Nov 2025 10:37:40 +0100 Subject: [PATCH 15/60] =?UTF-8?q?=F0=9F=94=A7=20Fix(Examples):=20=E7=A7=BB?= =?UTF-8?q?=E9=99=A4=E5=9F=BA=E7=A1=80=E7=94=A8=E6=B3=95=E7=A4=BA=E4=BE=8B?= =?UTF-8?q?=E5=92=8C=E5=AE=9E=E9=99=85=E6=8F=92=E4=BB=B6=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E5=99=A8=E7=A4=BA=E4=BE=8B=EF=BC=8C=E5=A4=8D=E5=88=B6=E7=A4=BA?= =?UTF-8?q?=E4=BE=8B=E6=95=B0=E6=8D=AE=E8=87=B3=E6=96=B0=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example.json | 154 ++++++++++ .../Examples/BasicUsageExample.cs | 263 ------------------ .../Kscript.CSharp.Parser/Examples/Program.cs | 164 ----------- .../Examples/RealPluginManagerExample.cs | 129 --------- 4 files changed, 154 insertions(+), 556 deletions(-) create mode 100644 KitX Script/Kscript.CSharp.Parser.Examples/example.json delete mode 100644 KitX Script/Kscript.CSharp.Parser/Examples/BasicUsageExample.cs delete mode 100644 KitX Script/Kscript.CSharp.Parser/Examples/Program.cs delete mode 100644 KitX Script/Kscript.CSharp.Parser/Examples/RealPluginManagerExample.cs diff --git a/KitX Script/Kscript.CSharp.Parser.Examples/example.json b/KitX Script/Kscript.CSharp.Parser.Examples/example.json new file mode 100644 index 0000000..60ed38c --- /dev/null +++ b/KitX Script/Kscript.CSharp.Parser.Examples/example.json @@ -0,0 +1,154 @@ +[ + { + "Name": "SampleCalculator", + "Version": "1.0.0", + "DisplayName": { + "en": "Calculator", + "zh": "计算器" + }, + "AuthorName": "KitX Devs", + "AuthorLink": "https://github.com/Crequency/KitX", + "PublisherName": "Crequency", + "PublisherLink": "https://kitx.cc", + "SimpleDescription": { + "en": "A simple calculator plugin.", + "zh": "简易计算器插件" + }, + "ComplexDescription": {}, + "TotalDescriptionInMarkdown": {}, + "IconInBase64": "", + "PublishDate": "2025-07-29T00:00:00Z", + "LastUpdateDate": "2025-07-29T00:00:00Z", + "IsMarketVersion": true, + "Tags": { + "en": "math,utility", + "zh": "数学,工具" + }, + "Functions": [ + { + "Name": "Add", + "DisplayNames": { + "en": "Add two integers", + "zh": "整数相加" + }, + "Parameters": [ + { + "Name": "a", + "DisplayNames": { + "en": "First number", + "zh": "第一个数" + }, + "Type": "int", + "Value": "0", + "IsOptional": false + }, + { + "Name": "b", + "DisplayNames": { + "en": "Second number", + "zh": "第二个数" + }, + "Type": "int", + "Value": "0", + "IsOptional": false + } + ], + "ReturnValue": "Sum of a and b", + "ReturnValueType": "int" + }, + { + "Name": "Divide", + "DisplayNames": { + "en": "Divide with optional precision", + "zh": "带可选精度的除法" + }, + "Parameters": [ + { + "Name": "numerator", + "Type": "double", + "Value": "0", + "IsOptional": false + }, + { + "Name": "denominator", + "Type": "double", + "Value": "1", + "IsOptional": false + }, + { + "Name": "decimals", + "Type": "int", + "Value": "2", + "IsOptional": true + } + ], + "ReturnValue": "Rounded quotient", + "ReturnValueType": "double" + } + ], + "RootStartupFileName": "SampleCalculator.dll" + }, + { + "Name": "StringToolkit", + "Version": "1.2.0", + "DisplayName": { + "en": "String Toolkit", + "zh": "字符串工具箱" + }, + "AuthorName": "Community", + "AuthorLink": "https://example.com", + "PublisherName": "KitX", + "PublisherLink": "https://kitx.cc", + "SimpleDescription": { + "en": "Utility functions for string manipulation.", + "zh": "字符串处理工具集" + }, + "ComplexDescription": {}, + "TotalDescriptionInMarkdown": {}, + "IconInBase64": "", + "PublishDate": "2025-07-29T00:00:00Z", + "LastUpdateDate": "2025-07-29T00:00:00Z", + "IsMarketVersion": false, + "Tags": { + "en": "string,utility,text", + "zh": "字符串,工具,文本" + }, + "Functions": [ + { + "Name": "Reverse", + "DisplayNames": { + "en": "Reverse string", + "zh": "反转字符串" + }, + "Parameters": [ + { + "Name": "text", + "Type": "string", + "Value": "", + "IsOptional": false + } + ], + "ReturnValue": "Reversed text", + "ReturnValueType": "string" + }, + { + "Name": "ToUpper", + "DisplayNames": { + "en": "Convert to upper case", + "zh": "转大写" + }, + "Parameters": [ + { + "Name": "text", + "Type": "string", + "Value": "", + "IsOptional": false + } + ], + "ReturnValue": "Uppercase text", + "ReturnValueType": "string" + } + ], + "RootStartupFileName": "StringToolkit.dll" + } + ] diff --git a/KitX Script/Kscript.CSharp.Parser/Examples/BasicUsageExample.cs b/KitX Script/Kscript.CSharp.Parser/Examples/BasicUsageExample.cs deleted file mode 100644 index 3a35e74..0000000 --- a/KitX Script/Kscript.CSharp.Parser/Examples/BasicUsageExample.cs +++ /dev/null @@ -1,263 +0,0 @@ -using System.Text.Json; -using Kscript.CSharp.Parser; -using Kscript.CSharp.Parser.Core; - -namespace Kscript.CSharp.Parser.Examples; - -/// -/// 基础用法示例 -/// -public static class BasicUsageExample -{ - /// - /// 演示基本功能 - /// - public static async Task RunExample() - { - Console.WriteLine("=== KitX.CSharp.Parser 基础用法示例 ===\n"); - - try - { - // 0. 初始化插件管理器 - Console.WriteLine("0. 初始化插件管理器..."); - if (!Parser.IsInitialized) - { - Parser.SetPluginManager(new MockPluginManager()); - Console.WriteLine(" ✓ 已设置 MockPluginManager"); - } - - // 1. 从现有的 example.json 文件加载插件清单 - Console.WriteLine("1. 加载插件清单..."); - var exampleJsonPath = Path.Combine(".", "example.json"); - if (!File.Exists(exampleJsonPath)) - { - Console.WriteLine($" 警告: example.json 文件不存在于 {exampleJsonPath}"); - Console.WriteLine(" 将使用内置示例数据"); - RunWithBuiltInData(); - return; - } - - var assembly = await Parser.GenerateFromFileAsync(exampleJsonPath, "ExamplePluginAssembly"); - Console.WriteLine($" ✓ 成功生成程序集: {assembly.FullName}"); - - // 2. 获取生成的插件类型 - Console.WriteLine("\n2. 分析生成的插件类型..."); - var pluginTypes = Parser.GetPluginTypes(assembly); - Console.WriteLine($" 发现 {pluginTypes.Count} 个插件类:"); - - foreach (var type in pluginTypes) - { - Console.WriteLine($" - {type.Name}"); - var methods = Parser.GetPluginMethods(type); - foreach (var method in methods) - { - var parameters = string.Join(", ", method.GetParameters().Select(p => $"{p.ParameterType.Name} {p.Name}")); - Console.WriteLine($" └── {method.ReturnType.Name} {method.Name}({parameters})"); - } - } - - // 3. 动态调用插件方法 - Console.WriteLine("\n3. 动态调用插件方法..."); - DynamicInvokeExample(assembly); - - // 4. 缓存统计 - Console.WriteLine("\n4. 缓存统计信息..."); - var stats = Parser.GetCacheStatistics(); - Console.WriteLine($" {stats}"); - - // 5. 演示缓存效果 - Console.WriteLine("\n5. 测试缓存效果..."); - - // 5.1 先检查是否存在缓存 - var plugins = JsonSerializer.Deserialize>(await File.ReadAllTextAsync(exampleJsonPath), new JsonSerializerOptions - { - PropertyNameCaseInsensitive = true, - AllowTrailingCommas = true - }); - var hasCacheBefore = Parser.HasCache(plugins!, "ExamplePluginAssembly"); - Console.WriteLine($" 生成前检查缓存存在: {hasCacheBefore}"); - - // 5.2 第二次生成(应该使用缓存) - var assembly2 = await Parser.GenerateFromFileAsync(exampleJsonPath, "ExamplePluginAssembly"); - Console.WriteLine($" 第二次生成是否使用缓存: {assembly == assembly2}"); - - // 5.3 强制重新生成(应该绕过缓存) - var assembly3 = Parser.Generate(plugins!, "ExamplePluginAssembly", useCache: false); - Console.WriteLine($" 强制重新生成是否使用新程序集: {assembly3 != assembly2}"); - - // 5.4 检查强制重新生成后的缓存状态 - var hasCacheAfterRegenerate = Parser.HasCache(plugins!, "ExamplePluginAssembly"); - Console.WriteLine($" 重新生成后缓存存在: {hasCacheAfterRegenerate}"); - - } - catch (Exception ex) - { - Console.WriteLine($"❌ 示例执行失败: {ex.Message}"); - Console.WriteLine($"详细信息: {ex}"); - } - } - - /// - /// 使用内置数据运行示例 - /// - private static void RunWithBuiltInData() - { - // 确保插件管理器已初始化 - if (!Parser.IsInitialized) - { - Parser.SetPluginManager(new MockPluginManager()); - } - - // 创建示例插件数据 - var plugins = new List - { - new PluginInfo - { - Name = "TestCalculator", - Version = "1.0.0", - Functions = new List - { - new Function - { - Name = "Add", - ReturnValueType = "int", - Parameters = new List - { - new Parameter { Name = "a", Type = "int", IsOptional = false }, - new Parameter { Name = "b", Type = "int", IsOptional = false } - } - }, - new Function - { - Name = "Multiply", - ReturnValueType = "double", - Parameters = new List - { - new Parameter { Name = "x", Type = "double", IsOptional = false }, - new Parameter { Name = "y", Type = "double", IsOptional = false } - } - } - } - } - }; - - Console.WriteLine(" 使用内置示例数据..."); - var assembly = Parser.Generate(plugins, "BuiltInExampleAssembly"); - Console.WriteLine($" ✓ 成功生成程序集: {assembly.FullName}"); - - DynamicInvokeExample(assembly); - } - - /// - /// 动态调用示例 - /// - private static void DynamicInvokeExample(Assembly assembly) - { - var pluginTypes = Parser.GetPluginTypes(assembly); - - foreach (var type in pluginTypes) - { - Console.WriteLine($"\n 调用插件: {type.Name}"); - var methods = Parser.GetPluginMethods(type); - - foreach (var method in methods) - { - try - { - object? result = null; - var parameters = method.GetParameters(); - - // 根据方法名和参数生成测试数据 - object[] args = GenerateTestArguments(method.Name, parameters); - - Console.WriteLine($" 调用 {method.Name}({string.Join(", ", args)})"); - - // 动态调用方法 - result = method.Invoke(null, args); - - Console.WriteLine($" → 结果: {result}"); - } - catch (Exception ex) - { - Console.WriteLine($" → 调用失败: {ex.Message}"); - } - } - } - } - - /// - /// 生成测试参数 - /// - private static object[] GenerateTestArguments(string methodName, ParameterInfo[] parameters) - { - var args = new object[parameters.Length]; - - for (int i = 0; i < parameters.Length; i++) - { - var param = parameters[i]; - - // 根据参数类型和方法名生成合适的测试值 - args[i] = param.ParameterType.Name switch - { - "Int32" => methodName switch - { - "Add" => i == 0 ? 10 : 20, - "Divide" when param.Name == "decimals" => 2, - _ => i + 1 - }, - "Double" => methodName switch - { - "Divide" => i == 0 ? 10.0 : 3.0, - "Multiply" => i == 0 ? 2.5 : 4.0, - _ => (i + 1) * 1.5 - }, - "String" => methodName switch - { - "Reverse" => "Hello", - "ToUpper" => "world", - _ => $"test{i}" - }, - _ => GetDefaultValue(param.ParameterType) - }; - } - - return args; - } - - /// - /// 获取类型默认值 - /// - private static object GetDefaultValue(Type type) - { - if (type.IsValueType) - return Activator.CreateInstance(type)!; - - return type == typeof(string) ? "default" : null!; - } - - /// - /// 演示高级功能 - /// - public static void RunAdvancedExample() - { - Console.WriteLine("\n=== 高级功能演示 ===\n"); - - try - { - // 1. 清除缓存 - Console.WriteLine("1. 清除所有缓存..."); - Parser.ClearCache(); - Console.WriteLine(" ✓ 缓存已清除"); - - // 2. 重新设置插件管理器 - Console.WriteLine("\n2. 重新设置插件管理器..."); - Parser.SetPluginManager(new MockPluginManager()); - Console.WriteLine(" ✓ 已重新设置插件管理器"); - - } - catch (Exception ex) - { - Console.WriteLine($"❌ 高级功能演示失败: {ex.Message}"); - } - } -} diff --git a/KitX Script/Kscript.CSharp.Parser/Examples/Program.cs b/KitX Script/Kscript.CSharp.Parser/Examples/Program.cs deleted file mode 100644 index 142351e..0000000 --- a/KitX Script/Kscript.CSharp.Parser/Examples/Program.cs +++ /dev/null @@ -1,164 +0,0 @@ -using Kscript.CSharp.Parser.Examples; -using Kscript.CSharp.Parser.Core; - -namespace Kscript.CSharp.Parser.Examples; - -/// -/// 演示程序入口 -/// -public static class Program -{ - /// - /// 主入口方法 - /// - public static async Task Main(string[] args) - { - Console.OutputEncoding = System.Text.Encoding.UTF8; - Console.WriteLine("🚀 欢迎使用 KitX.CSharp.Parser 演示程序!"); - Console.WriteLine("==================================================\n"); - - try - { - // 首先运行简化后的功能测试 - Console.WriteLine("=== 测试简化后的 Parser 功能 ==="); - TestSimplifiedParser(); - - Console.WriteLine("\n" + new string('=', 60)); - - // 运行基础用法示例 - await BasicUsageExample.RunExample(); - - // 运行高级功能示例 - BasicUsageExample.RunAdvancedExample(); - - // 运行实际插件管理器示例 - Console.WriteLine("\n" + new string('=', 60)); - RealPluginManagerExample.RunExample(); - - Console.WriteLine("\n" + new string('=', 60)); - RealPluginManagerExample.DashboardIntegrationExample(); - - Console.WriteLine("\n=================================================="); - Console.WriteLine("✅ 所有示例运行完成!"); - } - catch (Exception ex) - { - Console.WriteLine($"\n❌ 程序运行失败: {ex.Message}"); - Console.WriteLine($"详细错误信息:\n{ex}"); - } - - Console.WriteLine("\n按任意键退出..."); - Console.ReadKey(); - } - - /// - /// 测试简化后的 Parser 功能 - /// - private static void TestSimplifiedParser() - { - Console.WriteLine("1. 测试默认插件管理器设置..."); - - // 测试使用 MockPluginManager 中已有的插件 - var testPlugins = new List - { - new PluginInfo - { - Name = "SampleCalculator", - Version = "1.0.0", - Functions = new List - { - new Function - { - Name = "Add", - ReturnValueType = "int", - Parameters = new List - { - new Parameter { Name = "a", Type = "int", IsOptional = false }, - new Parameter { Name = "b", Type = "int", IsOptional = false } - } - } - } - } - }; - - try - { - // 先设置插件管理器 - Parser.SetPluginManager(new MockPluginManager()); - - // 使用 MockPluginManager 生成程序集 - var assembly = Parser.Generate(testPlugins, "TestAssembly"); - Console.WriteLine(" ✓ 默认 MockPluginManager 工作正常"); - - // 获取生成的类型 - var types = Parser.GetPluginTypes(assembly); - Console.WriteLine($" ✓ 生成了 {types.Count} 个插件类型"); - - foreach (var type in types) - { - var methods = Parser.GetPluginMethods(type); - Console.WriteLine($" - {type.Name}: {methods.Count} 个方法"); - } - } - catch (Exception ex) - { - Console.WriteLine($" ❌ 测试失败: {ex.Message}"); - } - - Console.WriteLine("2. 测试自定义插件管理器设置..."); - try - { - // 设置新的插件管理器 - Parser.SetPluginManager(new MockPluginManager()); - var assembly2 = Parser.Generate(testPlugins, "TestAssembly2"); - Console.WriteLine(" ✓ 自定义插件管理器设置工作正常"); - } - catch (Exception ex) - { - Console.WriteLine($" ❌ 自定义设置测试失败: {ex.Message}"); - } - - Console.WriteLine("3. 测试空参数处理..."); - try - { - // 测试 null 参数应该抛出异常 - Parser.SetPluginManager(null); - Console.WriteLine(" ❌ 空参数测试失败: 应该抛出异常但没有"); - } - catch (ArgumentNullException) - { - Console.WriteLine(" ✓ 空参数正确抛出 ArgumentNullException"); - } - catch (Exception ex) - { - Console.WriteLine($" ❌ 空参数测试失败: {ex.Message}"); - } - - Console.WriteLine("4. 测试插件存在性检测..."); - try - { - var mockManager = new MockPluginManager(); - Parser.SetPluginManager(mockManager); - - // 测试存在的插件 - var existsSampleCalculator = mockManager.IsPluginExists("SampleCalculator"); - Console.WriteLine($" ✓ SampleCalculator 存在: {existsSampleCalculator}"); - - var existsAddMethod = mockManager.IsMethodExists("SampleCalculator", "Add"); - Console.WriteLine($" ✓ SampleCalculator.Add 方法存在: {existsAddMethod}"); - - // 测试不存在的插件 - var existsNonExistent = mockManager.IsPluginExists("NonExistentPlugin"); - Console.WriteLine($" ✓ NonExistentPlugin 存在: {existsNonExistent}"); - - var existsNonExistentMethod = mockManager.IsMethodExists("SampleCalculator", "NonExistentMethod"); - Console.WriteLine($" ✓ SampleCalculator.NonExistentMethod 方法存在: {existsNonExistentMethod}"); - - Console.WriteLine(" ✓ 插件存在性检测功能工作正常"); - } - catch (Exception ex) - { - Console.WriteLine($" ❌ 插件存在性检测测试失败: {ex.Message}"); - } - } -} diff --git a/KitX Script/Kscript.CSharp.Parser/Examples/RealPluginManagerExample.cs b/KitX Script/Kscript.CSharp.Parser/Examples/RealPluginManagerExample.cs deleted file mode 100644 index 937a3ad..0000000 --- a/KitX Script/Kscript.CSharp.Parser/Examples/RealPluginManagerExample.cs +++ /dev/null @@ -1,129 +0,0 @@ -using Kscript.CSharp.Parser; -using Kscript.CSharp.Parser.Core; -using System.Text.Json; - -namespace Kscript.CSharp.Parser.Examples; - -/// -/// 实际插件管理器使用示例 -/// -public static class RealPluginManagerExample -{ - /// - /// 演示如何使用 RealPluginManager - /// - public static void RunExample() - { - Console.WriteLine("=== KitX.CSharp.Parser 实际插件管理器使用示例 ===\n"); - - try - { - // 1. 设置插件管理器 - Console.WriteLine("1. 设置插件管理器..."); - - // 这里应该传入实际的 PluginsServer 实例和日志记录器 - // 由于这是一个示例,我们使用 null 作为占位符 - // 在实际使用中,这里应该传入真实的 PluginsServer 实例和日志记录器 - // 例如: - // var pluginsServer = KitX.Dashboard.Network.PluginsNetwork.PluginsServer.Instance; - // var logger = Serilog.Log.Logger; - // Parser.SetPluginManager(new RealPluginManager(pluginsServer, message => logger.Information(message))); - - Console.WriteLine(" 使用 MockPluginManager 进行演示..."); - Parser.SetPluginManager(new MockPluginManager()); - - Console.WriteLine(" ✓ 插件管理器已设置"); - - // 2. 创建示例插件数据 - Console.WriteLine("\n2. 创建示例插件数据..."); - var examplePlugins = new List - { - new PluginInfo - { - Name = "SampleCalculator", - Version = "1.0.0", - Functions = new List - { - new Function - { - Name = "Add", - ReturnValueType = "int", - Parameters = new List - { - new Parameter { Name = "a", Type = "int", IsOptional = false }, - new Parameter { Name = "b", Type = "int", IsOptional = false } - } - } - } - } - }; - - Console.WriteLine(" ✓ 示例插件数据已创建"); - - // 3. 生成动态程序集 - Console.WriteLine("\n3. 生成动态程序集..."); - var assembly = Parser.Generate(examplePlugins, "RealPluginManagerExampleAssembly"); - Console.WriteLine($" ✓ 成功生成程序集: {assembly.FullName}"); - - // 4. 获取生成的插件类型 - Console.WriteLine("\n4. 分析生成的插件类型..."); - var pluginTypes = Parser.GetPluginTypes(assembly); - Console.WriteLine($" 发现 {pluginTypes.Count} 个插件类:"); - - foreach (var type in pluginTypes) - { - Console.WriteLine($" - {type.Name}"); - var methods = Parser.GetPluginMethods(type); - foreach (var method in methods) - { - var parameters = string.Join(", ", method.GetParameters().Select(p => $"{p.ParameterType.Name} {p.Name}")); - Console.WriteLine($" └── {method.ReturnType.Name} {method.Name}({parameters})"); - } - } - - Console.WriteLine("\n=== 示例完成 ==="); - Console.WriteLine("\n注意:在实际使用中,您需要:"); - Console.WriteLine("1. 确保 KitX Dashboard 正在运行"); - Console.WriteLine("2. 传入真实的 PluginsServer.Instance"); - Console.WriteLine("3. 传入真实的日志记录器实例"); - Console.WriteLine("4. 确保插件已正确连接到 Dashboard"); - } - catch (Exception ex) - { - Console.WriteLine($"❌ 示例执行失败: {ex.Message}"); - Console.WriteLine($"详细信息: {ex}"); - } - } - - /// - /// 演示如何在 KitX Dashboard 中集成 - /// - public static void DashboardIntegrationExample() - { - Console.WriteLine("=== KitX Dashboard 集成示例 ===\n"); - - Console.WriteLine("在 KitX Dashboard 中使用 RealPluginManager 的步骤:\n"); - - Console.WriteLine("1. 在 Dashboard 启动时设置插件管理器工厂:"); - Console.WriteLine("```csharp"); - Console.WriteLine("// 在 Dashboard 的初始化代码中"); - Console.WriteLine("Parser.SetPluginManagerFactory(() => new RealPluginManager("); - Console.WriteLine(" KitX.Dashboard.Network.PluginsNetwork.PluginsServer.Instance,"); - Console.WriteLine(" message => Log.Information(message)"); - Console.WriteLine("));"); - Console.WriteLine("```"); - - Console.WriteLine("\n2. 生成插件程序集:"); - Console.WriteLine("```csharp"); - Console.WriteLine("var assembly = Parser.Generate(plugins, \"DashboardPluginAssembly\");"); - Console.WriteLine("```"); - - Console.WriteLine("\n3. 在脚本中使用生成的 API:"); - Console.WriteLine("```csharp"); - Console.WriteLine("// 现在可以在脚本中直接调用"); - Console.WriteLine("var result = SampleCalculator.Add(10, 20);"); - Console.WriteLine("```"); - - Console.WriteLine("\n这样,脚本中的插件调用将通过真实的 KitX Dashboard 插件系统执行!"); - } -} From ea85a5a7fd432e62d6c08b99b8948d4ede95ba69 Mon Sep 17 00:00:00 2001 From: StarInk Date: Sat, 8 Nov 2025 11:34:45 +0100 Subject: [PATCH 16/60] =?UTF-8?q?=F0=9F=94=A7=20Fix(ScriptExecutor):=20?= =?UTF-8?q?=E7=A7=BB=E9=99=A4=E4=B8=8D=E5=BF=85=E8=A6=81=E7=9A=84=E5=91=BD?= =?UTF-8?q?=E5=90=8D=E7=A9=BA=E9=97=B4=E5=BC=95=E7=94=A8=E2=80=9CSystem.Co?= =?UTF-8?q?nsole=E2=80=9D=E4=BB=A5=E7=AE=80=E5=8C=96=E8=84=9A=E6=9C=AC?= =?UTF-8?q?=E9=80=89=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- KitX Script/Kscript.CSharp.Compiler/ScriptExecutor.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/KitX Script/Kscript.CSharp.Compiler/ScriptExecutor.cs b/KitX Script/Kscript.CSharp.Compiler/ScriptExecutor.cs index 6ec1c98..e648cf9 100644 --- a/KitX Script/Kscript.CSharp.Compiler/ScriptExecutor.cs +++ b/KitX Script/Kscript.CSharp.Compiler/ScriptExecutor.cs @@ -25,7 +25,7 @@ public class ScriptExecutor : IDisposable public ScriptExecutor() { _scriptOptions = ScriptOptions.Default - .WithImports("System", "System.Math", "System.Collections.Generic", "System.Console", "System.Linq") + .WithImports("System", "System.Math", "System.Collections.Generic", "System.Linq") .WithEmitDebugInformation(true); } @@ -144,7 +144,6 @@ public ScriptValidationResult ValidateScript(string script) // 更新脚本选项,包含所有引用 var options = ScriptOptions.Default .WithReferences(_referencedAssemblies) - .WithImports(_usings.Concat(new[] { "System.Console" })) .WithEmitDebugInformation(true); var csharpScript = CSharpScript.Create(script, options); From 3512cc53aef7af2f90d95aa7f1b3936b300c82ba Mon Sep 17 00:00:00 2001 From: StarInk Date: Sat, 14 Feb 2026 00:30:07 +0100 Subject: [PATCH 17/60] =?UTF-8?q?=F0=9F=92=BE=20Feat(RealPluginManager,=20?= =?UTF-8?q?IPluginServiceProvider):=20=E9=87=8D=E6=9E=84=E6=8F=92=E4=BB=B6?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=99=A8=E4=BB=A5=E4=BD=BF=E7=94=A8=E6=8F=92?= =?UTF-8?q?=E4=BB=B6=E6=9C=8D=E5=8A=A1=E6=8F=90=E4=BE=9B=E8=80=85=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=EF=BC=8C=E6=B7=BB=E5=8A=A0=E6=8F=92=E4=BB=B6=E5=93=8D?= =?UTF-8?q?=E5=BA=94=E8=AE=A2=E9=98=85=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Core/IPluginServiceProvider.cs | 44 +++++++ .../Core/RealPluginManager.cs | 119 +++--------------- 2 files changed, 63 insertions(+), 100 deletions(-) create mode 100644 KitX Script/Kscript.CSharp.Parser/Core/IPluginServiceProvider.cs diff --git a/KitX Script/Kscript.CSharp.Parser/Core/IPluginServiceProvider.cs b/KitX Script/Kscript.CSharp.Parser/Core/IPluginServiceProvider.cs new file mode 100644 index 0000000..badc3e9 --- /dev/null +++ b/KitX Script/Kscript.CSharp.Parser/Core/IPluginServiceProvider.cs @@ -0,0 +1,44 @@ +using KitX.Shared.CSharp.Plugin; + +namespace Kscript.CSharp.Parser.Core; + +/// +/// 插件服务提供者接口 - 由Dashboard实现 +/// 用于解耦Parser和Dashboard,避免直接依赖和反射 +/// +public interface IPluginServiceProvider +{ + /// + /// 获取所有运行中的插件信息 + /// + /// 运行中的插件信息集合 + IEnumerable GetRunningPlugins(); + + /// + /// 根据插件名称查找插件信息 + /// + /// 插件名称 + /// 插件信息,如果未找到则返回null + PluginInfo? FindPlugin(string pluginName); + + /// + /// 查找插件连接器 + /// + /// 插件信息 + /// 插件连接器对象,如果未找到则返回null + object? FindConnector(PluginInfo pluginInfo); + + /// + /// 向插件连接器发送请求 + /// + /// 插件连接器 + /// 请求对象 + /// 异步任务 + Task SendRequestAsync(object connector, object request); + + /// + /// 订阅插件响应事件 + /// + /// 响应处理器,接收(requestId, responseJson) + void SubscribeToResponses(Action responseHandler); +} diff --git a/KitX Script/Kscript.CSharp.Parser/Core/RealPluginManager.cs b/KitX Script/Kscript.CSharp.Parser/Core/RealPluginManager.cs index 3626efa..1f7d94c 100644 --- a/KitX Script/Kscript.CSharp.Parser/Core/RealPluginManager.cs +++ b/KitX Script/Kscript.CSharp.Parser/Core/RealPluginManager.cs @@ -13,7 +13,7 @@ namespace Kscript.CSharp.Parser.Core; /// public class RealPluginManager : IPluginManager { - private readonly object _pluginsServer; + private readonly IPluginServiceProvider _serviceProvider; private readonly Action _infoLogger; private readonly Action _errorLogger; @@ -30,16 +30,24 @@ public class RealPluginManager : IPluginManager /// /// 构造函数 /// - /// PluginsServer 实例 + /// 插件服务提供者实例 /// 信息日志记录器 /// 错误日志记录器 - public RealPluginManager(object pluginsServer, Action? infoLogger = null, Action? errorLogger = null) + public RealPluginManager( + IPluginServiceProvider serviceProvider, + Action? infoLogger = null, + Action? errorLogger = null) { - _pluginsServer = pluginsServer ?? throw new ArgumentNullException(nameof(pluginsServer)); + _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); _infoLogger = infoLogger ?? (message => Console.WriteLine(message)); _errorLogger = errorLogger ?? (message => Console.WriteLine(message)); - _infoLogger("[WorkflowScriptService] 正在初始化 RealPluginManager..."); + _infoLogger("[RealPluginManager] 正在初始化 RealPluginManager..."); + + // 订阅插件响应事件 + _serviceProvider.SubscribeToResponses(HandlePluginResponse); + + _infoLogger("[RealPluginManager] RealPluginManager 初始化完成"); } /// @@ -52,7 +60,7 @@ public T Call(PluginCallInfo callInfo) _infoLogger($"[RealPluginManager] 开始调用插件方法: {callInfo}"); // 查找插件信息 - var pluginInfo = FindPluginInfo(callInfo.PluginName); + var pluginInfo = _serviceProvider.FindPlugin(callInfo.PluginName); if (pluginInfo == null) { _infoLogger($"[RealPluginManager] 未找到插件: {callInfo.PluginName}"); @@ -60,7 +68,7 @@ public T Call(PluginCallInfo callInfo) } // 查找插件连接器 - var connector = FindPluginConnector(pluginInfo); + var connector = _serviceProvider.FindConnector(pluginInfo); if (connector == null) { _infoLogger($"[RealPluginManager] 插件 {callInfo.PluginName} 未连接"); @@ -95,7 +103,7 @@ public bool IsPluginExists(string pluginName) { try { - var pluginInfo = FindPluginInfo(pluginName); + var pluginInfo = _serviceProvider.FindPlugin(pluginName); var exists = pluginInfo != null; _infoLogger($"[RealPluginManager] 检查插件 '{pluginName}' 是否存在: {exists}"); @@ -115,7 +123,7 @@ public bool IsMethodExists(string pluginName, string methodName) { try { - var pluginInfo = FindPluginInfo(pluginName); + var pluginInfo = _serviceProvider.FindPlugin(pluginName); var exists = pluginInfo?.Functions.Any(f => f.Name == methodName) ?? false; _infoLogger($"[RealPluginManager] 检查方法 '{pluginName}.{methodName}' 是否存在: {exists}"); @@ -128,72 +136,6 @@ public bool IsMethodExists(string pluginName, string methodName) } } - /// - /// 查找插件信息 - /// - private PluginInfo? FindPluginInfo(string pluginName) - { - try - { - // 通过传入的 PluginsServer 实例查找插件信息 - var pluginsServerType = _pluginsServer.GetType(); - var pluginConnectorsProperty = pluginsServerType.GetProperty("PluginConnectors"); - - if (pluginConnectorsProperty != null) - { - var pluginConnectors = pluginConnectorsProperty.GetValue(_pluginsServer) as System.Collections.IList; - if (pluginConnectors != null) - { - foreach (var connector in pluginConnectors) - { - if (connector != null) - { - var connectorType = connector.GetType(); - var pluginInfoProperty = connectorType.GetProperty("PluginInfo"); - if (pluginInfoProperty != null) - { - var pluginInfo = pluginInfoProperty.GetValue(connector) as PluginInfo; - if (pluginInfo != null && pluginInfo.Name == pluginName) - { - return pluginInfo; - } - } - } - } - } - } - } - catch (Exception ex) - { - _errorLogger($"[RealPluginManager] 查找插件信息失败: {pluginName} - 异常: {ex.Message}"); - } - - return null; - } - - /// - /// 查找插件连接器 - /// - private object? FindPluginConnector(PluginInfo pluginInfo) - { - try - { - var pluginsServerType = _pluginsServer.GetType(); - var findConnectorMethod = pluginsServerType.GetMethod("FindConnector", new[] { typeof(PluginInfo) }); - - if (findConnectorMethod != null) - { - return findConnectorMethod.Invoke(_pluginsServer, new object[] { pluginInfo }); - } - } - catch (Exception ex) - { - _errorLogger($"[RealPluginManager] 查找插件连接器失败: {pluginInfo.Name} - 异常: {ex.Message}"); - } - - return null; - } - /// /// 发送插件请求 /// @@ -205,7 +147,7 @@ private async Task SendPluginRequest(object connector, PluginCallInfo call var connectorInstance = new Connector() .SetSerializer(x => JsonSerializer.Serialize(x, _serializerOptions)) .SetSender(request => { - SendRequestToConnector(connector, request).ConfigureAwait(false); + _serviceProvider.SendRequestAsync(connector, request).ConfigureAwait(false); }); // 构建参数列表 @@ -219,6 +161,7 @@ private async Task SendPluginRequest(object connector, PluginCallInfo call { Name = $"param{i}", Type = paramType.Name, + // TODO: 等待KitX数据传输标准制定完成后,替换为标准序列化方法 Value = paramValue?.ToString() ?? string.Empty, IsOptional = false }); @@ -295,30 +238,6 @@ private async Task SendPluginRequest(object connector, PluginCallInfo call } } - /// - /// 向连接器发送请求 - /// - private async Task SendRequestToConnector(object connector, Request request) - { - try - { - var connectorType = connector.GetType(); - var requestMethod = connectorType.GetMethod("Request", new[] { typeof(Request) }); - if (requestMethod != null) - { - var result = requestMethod.Invoke(connector, new object[] { request }); - if (result is Task task) - { - await task; - } - } - } - catch (Exception ex) - { - _errorLogger($"[RealPluginManager] 向连接器发送请求失败 - 异常: {ex.Message}"); - } - } - /// /// 处理插件响应 /// From 4c0b44d2d80d6ace5f3b047bc299a6ab50ec4bd2 Mon Sep 17 00:00:00 2001 From: StarInk Date: Fri, 20 Feb 2026 19:57:06 +0100 Subject: [PATCH 18/60] =?UTF-8?q?=EF=BB=BF=F0=9F=92=BE=20Feat(KitX.Core.Co?= =?UTF-8?q?ntract):=20Add=20KitX.Core.Contract=20to=20provide=20standard?= =?UTF-8?q?=20interfaces=20for=20the=20separation=20of=20front-end=20and?= =?UTF-8?q?=20back-end=20in=20the=20Dashboard.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Activity/IActivityService.cs | 93 ++++ .../Announcement/IAnnouncementService.cs | 109 +++++ .../Configuration/IConfigService.cs | 407 ++++++++++++++++++ .../Device/IDeviceService.cs | 215 +++++++++ .../KitX.Core.Contract/Event/IEventService.cs | 58 +++ .../Events/CommonEventArgs.cs | 42 ++ .../FileWatcher/IFileWatcherService.cs | 28 ++ .../Hotkey/IKeyHookService.cs | 32 ++ .../KitX-Background-ani.png | Bin 0 -> 211184 bytes .../KitX.Core.Contract.csproj | 49 +++ .../Plugin/IPluginService.cs | 255 +++++++++++ .../KitX.Core.Contract/README.md | 121 ++++++ .../Security/ISecurityService.cs | 90 ++++ .../Statistics/IStatisticsService.cs | 46 ++ .../KitX.Core.Contract/Tasks/ITasksService.cs | 25 ++ .../Workflow/IWorkflowService.cs | 150 +++++++ .../CodeGen/AssemblyCache.cs | 2 +- .../Device/DeviceInfoExtensions.cs | 75 ++++ 18 files changed, 1796 insertions(+), 1 deletion(-) create mode 100644 KitX Core Contracts/KitX.Core.Contract/Activity/IActivityService.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Announcement/IAnnouncementService.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Configuration/IConfigService.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Device/IDeviceService.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Event/IEventService.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Events/CommonEventArgs.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/FileWatcher/IFileWatcherService.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Hotkey/IKeyHookService.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/KitX-Background-ani.png create mode 100644 KitX Core Contracts/KitX.Core.Contract/KitX.Core.Contract.csproj create mode 100644 KitX Core Contracts/KitX.Core.Contract/Plugin/IPluginService.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/README.md create mode 100644 KitX Core Contracts/KitX.Core.Contract/Security/ISecurityService.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Statistics/IStatisticsService.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Tasks/ITasksService.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowService.cs create mode 100644 KitX Shared/KitX.Shared.CSharp/Device/DeviceInfoExtensions.cs diff --git a/KitX Core Contracts/KitX.Core.Contract/Activity/IActivityService.cs b/KitX Core Contracts/KitX.Core.Contract/Activity/IActivityService.cs new file mode 100644 index 0000000..fae97e4 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Activity/IActivityService.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Threading.Tasks; + +namespace KitX.Core.Contract.Activity; + +/// +/// Activity management service interface +/// +public interface IActivityService +{ + /// + /// Records app start event + /// + void RecordAppStart(); + + /// + /// Records app exit event + /// + void RecordAppExit(); + + /// + /// Records an activity + /// + /// The activity type + /// Optional details + void RecordActivity(string type, Dictionary? details = null); + + /// + /// Gets activities + /// + /// Optional start date + /// Optional end date + /// Maximum number of activities to return + /// List of activities + IList GetActivities(DateTime? startDate = null, DateTime? endDate = null, int limit = 100); + + /// + /// Gets activity statistics + /// + /// Start date + /// End date + /// Activity statistics + IActivityStatistics GetStatistics(DateTime startDate, DateTime endDate); + + /// + /// Event raised when activities are updated + /// + event EventHandler? ActivitiesUpdated; +} + +/// +/// Activity interface +/// +public interface IActivity +{ + /// + /// Gets the activity ID + /// + string Id { get; } + + /// + /// Gets the activity type + /// + string Type { get; } + + /// + /// Gets the timestamp + /// + DateTime Timestamp { get; } + + /// + /// Gets the details + /// + Dictionary Details { get; } +} + +/// +/// Activity statistics interface +/// +public interface IActivityStatistics +{ + /// + /// Gets the total number of activities + /// + int TotalActivities { get; } + + /// + /// Gets the activities grouped by type + /// + Dictionary ActivitiesByType { get; } +} diff --git a/KitX Core Contracts/KitX.Core.Contract/Announcement/IAnnouncementService.cs b/KitX Core Contracts/KitX.Core.Contract/Announcement/IAnnouncementService.cs new file mode 100644 index 0000000..ec5ff4c --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Announcement/IAnnouncementService.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Threading.Tasks; +using KitX.Core.Contract.Configuration; + +namespace KitX.Core.Contract.Announcement; + +/// +/// Announcement service interface +/// +public interface IAnnouncementService +{ + /// + /// Gets the announcement configuration + /// + IAnnouncementConfig AnnouncementConfig { get; } + + /// + /// Checks for new announcements + /// + /// List of new announcements + Task> CheckNewAnnouncementsAsync(); + + /// + /// Marks an announcement as read + /// + /// The announcement ID + void MarkAsRead(string announcementId); + + /// + /// Gets all read announcement IDs + /// + /// List of read announcement IDs + IReadOnlyList GetReadAnnouncementIds(); + + /// + /// Saves the announcement configuration + /// + void SaveAnnouncementConfig(); + + /// + /// Event raised when new announcements are available + /// + event EventHandler? NewAnnouncementsAvailable; +} + +/// +/// Announcement interface +/// +public interface IAnnouncement +{ + /// + /// Gets the announcement ID + /// + string Id { get; } + + /// + /// Gets the announcement title + /// + string Title { get; } + + /// + /// Gets the announcement content + /// + string Content { get; } + + /// + /// Gets the publish date + /// + DateTime PublishDate { get; } + + /// + /// Gets the version + /// + string Version { get; } +} + +/// +/// New announcements event arguments +/// +public class NewAnnouncementsEventArgs : EventArgs +{ + /// + /// Gets or sets the announcements + /// + public IReadOnlyList Announcements { get; set; } = Array.Empty(); + + /// + /// Gets or sets announcements as dictionary (date -> content) + /// + public Dictionary? AnnouncementsDict { get; set; } +} + +/// +/// Announcement error event arguments +/// +public class AnnouncementErrorEventArgs : EventArgs +{ + /// + /// Gets or sets the error message + /// + public string ErrorMessage { get; set; } = string.Empty; + + /// + /// Gets or sets the stack trace + /// + public string StackTrace { get; set; } = string.Empty; +} diff --git a/KitX Core Contracts/KitX.Core.Contract/Configuration/IConfigService.cs b/KitX Core Contracts/KitX.Core.Contract/Configuration/IConfigService.cs new file mode 100644 index 0000000..c70bca0 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Configuration/IConfigService.cs @@ -0,0 +1,407 @@ +using System; +using System.Collections.Generic; +using Common.BasicHelper.Graphics.Screen; +using KitX.Shared.CSharp.Device; +using KitX.Shared.CSharp.Loader; +using KitX.Shared.CSharp.Plugin; +using Serilog.Events; + +namespace KitX.Core.Contract.Configuration; + +/// +/// Window state enumeration (mirrors Avalonia.WindowState) +/// +public enum WindowState +{ + Normal, + Minimized, + Maximized, + FullScreen, + NonInteractive +} + +/// +/// Configuration management service interface +/// +public interface IConfigService +{ + /// + /// Gets the application configuration + /// + IAppConfig AppConfig { get; } + + /// + /// Gets the plugins configuration + /// + IPluginsConfig PluginsConfig { get; } + + /// + /// Gets the security configuration + /// + ISecurityConfig SecurityConfig { get; } + + /// + /// Loads all configurations from files + /// + void Load(); + + /// + /// Saves all configurations to files + /// + void SaveAll(); + + /// + /// Reloads all configurations from files + /// + void Reload(); + + /// + /// Event raised when configuration changes + /// + event EventHandler? ConfigChanged; +} + +/// +/// Application configuration interface (complete structure) +/// +public interface IAppConfig +{ + /// + /// Gets or sets the application configuration + /// + IAppConf App { get; set; } + + /// + /// Gets or sets the windows configuration + /// + IWindowsConf Windows { get; set; } + + /// + /// Gets or sets the pages configuration + /// + IPagesConf Pages { get; set; } + + /// + /// Gets or sets the web configuration + /// + IWebConf Web { get; set; } + + /// + /// Gets or sets the log configuration + /// + ILogConf Log { get; set; } + + /// + /// Gets or sets the IO configuration + /// + IIOConf IO { get; set; } + + /// + /// Gets or sets the activity configuration + /// + IActivityConf Activity { get; set; } + + /// + /// Gets or sets the loaders configuration + /// + ILoadersConf Loaders { get; set; } +} + +/// +/// Application configuration section +/// +public interface IAppConf +{ + string IconFileName { get; set; } + string CoverIconFileName { get; set; } + string AppLanguage { get; set; } + string Theme { get; set; } + string ThemeColor { get; set; } + Dictionary SurpportLanguages { get; set; } + string LocalPluginsFileFolder { get; set; } + string LocalPluginsDataFolder { get; set; } + bool DeveloperSetting { get; set; } + bool ShowAnnouncementWhenStart { get; set; } + ulong RanTime { get; set; } + int LastBreakAfterExit { get; set; } +} + +/// +/// Windows configuration section +/// +public interface IWindowsConf +{ + IMainWindowConf MainWindow { get; set; } + IAnnouncementWindowConf AnnouncementWindow { get; set; } +} + +/// +/// Main window configuration +/// +public interface IMainWindowConf +{ + Resolution Size { get; set; } + Distances Location { get; set; } + WindowState WindowState { get; set; } + bool IsHidden { get; set; } + Dictionary Tags { get; set; } + bool EnabledMica { get; set; } + int GreetingTextCount_Morning { get; set; } + int GreetingTextCount_Noon { get; set; } + int GreetingTextCount_AfterNoon { get; set; } + int GreetingTextCount_Evening { get; set; } + int GreetingTextCount_Night { get; set; } + int GreetingUpdateInterval { get; set; } +} + +/// +/// Announcement window configuration +/// +public interface IAnnouncementWindowConf +{ + Resolution Size { get; set; } + Distances Location { get; set; } +} + +/// +/// Pages configuration section +/// +public interface IPagesConf +{ + IHomePageConf Home { get; set; } + IDevicePageConf Device { get; set; } + IMarketPageConf Market { get; set; } + ISettingsPageConf Settings { get; set; } +} + +/// +/// Home page configuration +/// +public interface IHomePageConf +{ + NavigationViewPaneDisplayMode NavigationViewPaneDisplayMode { get; set; } + string SelectedViewName { get; set; } + bool IsNavigationViewPaneOpened { get; set; } + bool UseAreaExpanded { get; set; } +} + +/// +/// Device page configuration +/// +public interface IDevicePageConf { } + +/// +/// Market page configuration +/// +public interface IMarketPageConf { } + +/// +/// Settings page configuration +/// +public interface ISettingsPageConf +{ + NavigationViewPaneDisplayMode NavigationViewPaneDisplayMode { get; set; } + string SelectedViewName { get; set; } + bool PaletteAreaExpanded { get; set; } + bool WebRelatedAreaExpanded { get; set; } + bool WebRelatedAreaOfNetworkInterfacesExpanded { get; set; } + bool LogRelatedAreaExpanded { get; set; } + bool UpdateRelatedAreaExpanded { get; set; } + bool AboutAreaExpanded { get; set; } + bool AuthorsAreaExpanded { get; set; } + bool LinksAreaExpanded { get; set; } + bool ThirdPartyLicensesAreaExpanded { get; set; } + bool IsNavigationViewPaneOpened { get; set; } +} + +/// +/// Web configuration section +/// +public interface IWebConf +{ + double DelayStartSeconds { get; set; } + string ApiServer { get; set; } + string ApiPath { get; set; } + int DevicesViewRefreshDelay { get; set; } + List? AcceptedNetworkInterfaces { get; set; } + int? UserSpecifiedDevicesServerPort { get; set; } + int? UserSpecifiedPluginsServerPort { get; set; } + int UdpPortSend { get; set; } + int UdpPortReceive { get; set; } + int UdpSendFrequency { get; set; } + string UdpBroadcastAddress { get; set; } + string IPFilter { get; set; } + int SocketBufferSize { get; set; } + int DeviceInfoTTLSeconds { get; set; } + bool DisableRemovingOfflineDeviceCard { get; set; } + string UpdateServer { get; set; } + string UpdatePath { get; set; } + string UpdateDownloadPath { get; set; } + string UpdateChannel { get; set; } + string UpdateSource { get; set; } + int DebugServicesServerPort { get; set; } +} + +/// +/// Log configuration section +/// +public interface ILogConf +{ + long LogFileSingleMaxSize { get; set; } + string LogFilePath { get; set; } + string LogTemplate { get; set; } + int LogFileMaxCount { get; set; } + int LogFileFlushInterval { get; set; } + public LogEventLevel LogLevel { get; set; } +} + +/// +/// IO configuration section +/// +public interface IIOConf +{ + int UpdatingCheckPerThreadFilesCount { get; set; } + int OperatingSystemVersionUpdateInterval { get; set; } +} + +/// +/// Activity configuration section +/// +public interface IActivityConf +{ + int TotalRecorded { get; set; } +} + +/// +/// Loaders configuration section +/// +public interface ILoadersConf +{ + string InstallPath { get; set; } +} + +public interface IAnnouncementConfig +{ + /// + /// Gets or sets the list of accepted announcement IDs + /// + List Accepted { get; set; } + + /// + /// Gets or sets the config file location + /// + string? ConfigFileLocation { get; set; } +} + +/// +/// Plugins configuration interface +/// +public interface IPluginsConfig +{ + /// + /// Gets or sets the list of plugin installations + /// + IList Plugins { get; set; } +} + +/// +/// Security configuration interface +/// +public interface ISecurityConfig +{ + /// + /// Gets or sets the device keys dictionary + /// + IDictionary DeviceKeys { get; set; } +} + +/// +/// Plugin installation interface +/// +public interface IPluginInstallation +{ + /// + /// Gets the installation path + /// + string? InstallPath { get; } + + /// + /// Gets or sets the plugin information + /// + PluginInfo? PluginInfo { get; set; } + + /// + /// Gets or sets the loader information + /// + LoaderInfo? LoaderInfo { get; set; } + + /// + /// Gets or sets the list of installed devices + /// + IList InstalledDevices { get; set; } +} + +/// +/// Device key interface +/// +public interface IDeviceKey +{ + /// + /// Gets the MAC address + /// + string MacAddress { get; } + + /// + /// Gets the device name + /// + string DeviceName { get; } + + /// + /// Gets the public key + /// + string PublicKey { get; } + + /// + /// Gets the time when the key was added + /// + DateTime AddedAt { get; } +} + +/// +/// Navigation view pane display mode enum +/// +public enum NavigationViewPaneDisplayMode +{ + Auto = 0, + Left = 1, + Top = 2, + LeftCompact = 3, + LeftMinimal = 4 +} + +/// +/// Configuration changed event arguments +/// +public class ConfigChangedEventArgs : EventArgs +{ + /// + /// Gets or sets the configuration type (e.g., "App", "Plugins", "Security") + /// + public string ConfigType { get; set; } = string.Empty; + + /// + /// Gets or sets the property name that changed + /// + public string PropertyName { get; set; } = string.Empty; + + /// + /// Gets or sets the old value + /// + public object? OldValue { get; set; } + + /// + /// Gets or sets the new value + /// + public object? NewValue { get; set; } +} + diff --git a/KitX Core Contracts/KitX.Core.Contract/Device/IDeviceService.cs b/KitX Core Contracts/KitX.Core.Contract/Device/IDeviceService.cs new file mode 100644 index 0000000..6e3f930 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Device/IDeviceService.cs @@ -0,0 +1,215 @@ +using System; +using System.ComponentModel; +using System.Threading.Tasks; +using System.Collections.Generic; +using KitX.Shared.CSharp.Device; + +namespace KitX.Core.Contract.Device; + +/// +/// Device management service interface +/// +public interface IDeviceService +{ + /// + /// Gets the discovered devices list + /// + IReadOnlyList DiscoveredDevices { get; } + + /// + /// Gets the authorized devices list + /// + IReadOnlyList AuthorizedDevices { get; } + + /// + /// Gets the self device information + /// + DeviceInfo SelfDeviceInfo { get; } + + /// + /// Gets a value indicating whether this device is the main device + /// + bool IsMainDevice { get; } + + /// + /// Authorizes a device + /// + /// The device ID + /// The device key + /// True if authorization was successful + Task AuthorizeDeviceAsync(string deviceId, string deviceKey); + + /// + /// Unauthorizes a device + /// + /// The device ID + /// True if unauthorization was successful + Task UnauthorizeDeviceAsync(string deviceId); + + /// + /// Connects to a device + /// + /// The device ID + /// True if connection was successful + Task ConnectToDeviceAsync(string deviceId); + + /// + /// Event raised when a device is discovered + /// + event EventHandler? DeviceDiscovered; + + /// + /// Event raised when a device goes offline + /// + event EventHandler? DeviceOffline; + + /// + /// Event raised when the main device changes + /// + event EventHandler? MainDeviceChanged; +} + +/// +/// Device discovery service interface +/// +public interface IDeviceDiscoveryService +{ + /// + /// Gets the default device information + /// + DeviceInfo DefaultDeviceInfo { get; } + + /// + /// Gets the port the discovery service is running on + /// + int? Port { get; } + + /// + /// Starts the device discovery service + /// + /// The service instance + IDeviceDiscoveryService Run(); + + /// + /// Stops the device discovery service + /// + void Stop(); + + /// + /// Event raised when a device is discovered + /// + event EventHandler? DeviceDiscovered; + + /// + /// Event raised when a device goes offline + /// + event EventHandler? DeviceOffline; +} + +/// +/// Device server interface for HTTP API +/// +public interface IDeviceServer +{ + /// + /// Gets the port the server is running on + /// + int? Port { get; } + + /// + /// Starts the device server + /// + /// The server instance + IDeviceServer Run(); + + /// + /// Stops the device server + /// + void Stop(); +} + +/// +/// Devices organizer interface +/// +public interface IDevicesOrganizer +{ + /// + /// Updates the source and adds device cards + /// + /// The device info + void UpdateSourceAndAddCards(DeviceInfo deviceInfo); + + /// + /// Event raised when a device is discovered + /// + event EventHandler? DeviceDiscovered; +} + +/// +/// Device case interface +/// +public interface IDeviceCase +{ + /// + /// Gets or sets the device information + /// + DeviceInfo DeviceInfo { get; set; } + + /// + /// Gets a value indicating whether the device is authorized + /// + bool IsAuthorized { get; } + + /// + /// Gets a value indicating whether this is the main device + /// + bool IsMainDevice { get; } + + /// + /// Gets a value indicating whether the device is online + /// + bool IsOnline { get; } + + /// + /// Gets the last seen time + /// + DateTime LastSeen { get; } +} + +/// +/// Device discovered event arguments +/// +public class DeviceDiscoveredEventArgs : EventArgs +{ + /// + /// Gets or sets the device information + /// + public DeviceInfo? DeviceInfo { get; set; } +} + +/// +/// Device offline event arguments +/// +public class DeviceOfflineEventArgs : EventArgs +{ + /// + /// Gets or sets the device ID + /// + public string DeviceId { get; set; } = string.Empty; +} + +/// +/// Main device changed event arguments +/// +public class MainDeviceChangedEventArgs : EventArgs +{ + /// + /// Gets or sets the old main device ID + /// + public string OldMainDeviceId { get; set; } = string.Empty; + + /// + /// Gets or sets the new main device ID + /// + public string NewMainDeviceId { get; set; } = string.Empty; +} diff --git a/KitX Core Contracts/KitX.Core.Contract/Event/IEventService.cs b/KitX Core Contracts/KitX.Core.Contract/Event/IEventService.cs new file mode 100644 index 0000000..843b86a --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Event/IEventService.cs @@ -0,0 +1,58 @@ +using System; +using System.ComponentModel; + +namespace KitX.Core.Contract.Event; + +/// +/// Event service interface for global event bus +/// +public interface IEventService +{ + /// + /// Subscribes to an event + /// + /// The event name + /// The event handler + void Subscribe(string eventName, EventHandler handler); + + /// + /// Unsubscribes from an event + /// + /// The event name + /// The event handler + void Unsubscribe(string eventName, EventHandler handler); + + /// + /// Publishes an event + /// + /// The event name + /// The event arguments + void Publish(string eventName, EventArgs args); + + /// + /// Subscribes to a typed event + /// + /// The event args type + /// The event name + /// The event handler + void Subscribe(string eventName, EventHandler handler) + where TEventArgs : EventArgs; + + /// + /// Unsubscribes from a typed event + /// + /// The event args type + /// The event name + /// The event handler + void Unsubscribe(string eventName, EventHandler handler) + where TEventArgs : EventArgs; + + /// + /// Publishes a typed event + /// + /// The event args type + /// The event name + /// The event arguments + void Publish(string eventName, TEventArgs args) + where TEventArgs : EventArgs; +} diff --git a/KitX Core Contracts/KitX.Core.Contract/Events/CommonEventArgs.cs b/KitX Core Contracts/KitX.Core.Contract/Events/CommonEventArgs.cs new file mode 100644 index 0000000..2e70a4d --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Events/CommonEventArgs.cs @@ -0,0 +1,42 @@ +using System; +using System.ComponentModel; + +namespace KitX.Core.Contract.Events; + +/// +/// Base class for event arguments +/// +public abstract class BaseEventArgs : EventArgs +{ + /// + /// Gets or sets the timestamp when the event occurred + /// + public DateTime Timestamp { get; set; } = DateTime.UtcNow; +} + +/// +/// Generic event arguments for simple event data +/// +public class GenericEventArgs : BaseEventArgs +{ + /// + /// Gets or sets the event data + /// + public object? Data { get; set; } +} + +/// +/// Generic event arguments with type parameter +/// +public class GenericEventArgs : BaseEventArgs +{ + /// + /// Gets or sets the event data + /// + public T? Data { get; set; } + + /// + /// Gets or sets the event type + /// + public string? EventType { get; set; } +} diff --git a/KitX Core Contracts/KitX.Core.Contract/FileWatcher/IFileWatcherService.cs b/KitX Core Contracts/KitX.Core.Contract/FileWatcher/IFileWatcherService.cs new file mode 100644 index 0000000..037bc27 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/FileWatcher/IFileWatcherService.cs @@ -0,0 +1,28 @@ +using System.IO; +using System.ComponentModel; + +namespace KitX.Core.Contract.FileWatcher; + +/// +/// File watcher service interface +/// +public interface IFileWatcherService +{ + /// + /// Registers a file watcher + /// + /// The file path to watch + /// The callback when file changes + void RegisterWatcher(string filePath, FileSystemEventHandler onChanged); + + /// + /// Unregisters a file watcher + /// + /// The file path to stop watching + void UnregisterWatcher(string filePath); + + /// + /// Clears all file watchers + /// + void Clear(); +} diff --git a/KitX Core Contracts/KitX.Core.Contract/Hotkey/IKeyHookService.cs b/KitX Core Contracts/KitX.Core.Contract/Hotkey/IKeyHookService.cs new file mode 100644 index 0000000..9e1dddb --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Hotkey/IKeyHookService.cs @@ -0,0 +1,32 @@ +using System; + +namespace KitX.Core.Contract.Hotkey; + +/// +/// Key hook service interface for global hotkeys +/// +public interface IKeyHookService +{ + /// + /// Starts the key hook + /// + void StartHook(); + + /// + /// Stops the key hook + /// + void StopHook(); + + /// + /// Registers a hotkey handler + /// + /// The keys sequence + /// The handler + void RegisterHotKeyHandler(string keysSequence, Action handler); + + /// + /// Unregisters a hotkey handler + /// + /// The keys sequence + void UnregisterHotKeyHandler(string keysSequence); +} diff --git a/KitX Core Contracts/KitX.Core.Contract/KitX-Background-ani.png b/KitX Core Contracts/KitX.Core.Contract/KitX-Background-ani.png new file mode 100644 index 0000000000000000000000000000000000000000..7abdbc3680d197674c8729ea4c0eab5ca71d03c8 GIT binary patch literal 211184 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

n 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%0A%6YC$6s&yL)Ry1p9BAwZ@>9Jee=zW>Ca{P zho|$IZ6!`33%LLbu^@O)Q$Xhw#i~)KVIK|eI6Yht=+MnfBoI|%?}^G`udyS ze*5v`v#5!7Vdr-JkKTX&5C6$O1^M+~{~CPXZ{2XSt$PW2w+hBO_gJY*gfn=a^Hd0u z;H)4@eDmhS(KJ9v$Os*!W4aH-B(R1rGKFc9c^9NUsI@?c2*OA)Aojsh{2|kx48#g> z0(*kIrDBxMp0y2EH-8@8@5-GtQ=LUvJz0{+qA=Z_gi< zTuzT)e)*F>`q`hXZ=Pi37r*5sUqMP>3KTNoENw;FR&<=BK zkrawZo`w|a&OO~+UNc4SAWZFQdQm?|0j9mHcCww$r_;m3+xLI^=l}RG{^H-+zWMI| z@qhh~|K@N0QeZzlt^eac|MO*g;caWZpJ0vV#dX~-j({~Yy9W&Kd8~OeYyKq5STbyx zGwJH6se2)t*;W0$nb9BA1Wgt|{_5k!V1M%XaQb*XTYG5h4fuMyJZpc0<aaVF2NUfgPSqqb^2-Rsaa(6!sOA=K84{6t?*$W-~S0fSVbBCvu^D zH2u&`Nr)^*@^C&4zI>X{Cv*WfBm{ z3&tohRFMjgWcZmUndGFNE5i(EG>E8(F7ShxVA{8S)!w&$F*}>SU*uW!J>-v<^-0>B z*1FlnlitihG1)b{Ws@~f9#xNP=oa7jlgtGkMCNfDKWw^i%e#Y6SNY>SOw@wo9aG zSe+%)AyA+}H-m)*pbB=oLd~o-5!LP178tH(XNdj1|L{Nf=AZp%PqzKg^{1!P(|TIh z!u`s{aHx9UApw`to?CJ?}h7Ogr{rvH< zy}W$>=H0LU>@WWK@BRGr_RLeeb-mthUA3!SZ~b=Jw(CuAH>frmw9{0e=v&`zwrzwq zq7^t=BzFY>07M>p>%T;N{Q`goCAwi#^Qg|&wVU?dySiD4x}Fra_4M%O;qB+2|M4IF z^cR2n^#0v&;8*%s->b6Y=qIK%AyyYu6>*UMMmJxaR}Cpnqf z+4#ivc3EAuci^i1YPtd4GDj-8-L<|)@Fp1;vP9uRu9QXCVu3ayJnjQ^HH8VR8J4hl zjfM_9n{MPY@Xua8{$0O4uIuUH&2m~#=hNxw5vyqAhI)1`Jsp8&wr!%aa#>{6W%Gq0 zPrN5kXH*T%Wg%C^4q=L>P`Y?2V1~Y#hnF04-OOns(weZf)+mc-&u^+K)_wO3Noluj zYp^eu<-hU87hkG97#=ArYyoa+XD`JKGs}Ha<&k7?znkgspR5hrI(Z=N{cd(M+sr^) zhzIiX*4{14qqN0W*G{cl|6c8T?JKyb*=F|f!$vj(ZXKRT)=kxVhse?zWm((916xBo z$+}*@{jS+|K0m&Ee38>d){_UtD0xbS1DV0J`{EFou;8((auE_8!CK%B*k<5ccmV_` zzA8R>a8`+=tWudNDigMk*Z6QrAKFIP$R^tz%wr^@enIMw7MXa@SSq!!)EVwDaM$jDee5WK7=xSTm?@N!o z*1(1!XAJUDR;X+(s>jfS7;^EDo{OO-9Z_X-a3pTNC zJ+hN1s@A*thHGjQU7LqH)cni?m@N_Cca+sIMU!+9+4u4nOa+iJUWdoqrZ2bc`L^9Q zz4e}^dSHi|Y6r}e(#^KMscr^+e0==u{pYgT^S8fi*O#;4r*F=u%jMK{QS}@|Xm1rN z2o#Hehxfz)kC=6!591&V&2-CSNk#{GA+VKLfu|-9YzxpRXCZx^xykpc-Jq(!`tpxf zw3pxfUGF#Eu1!0)D@@xWt*yFkdb!;$8)+h9_VKoD*YAI5ZD}hyprH1yfdpA7F`&sz zP1%Vy@NNp{kMNL9nCPT%_3ZgiYI@tGNuEJJzz0Y(3Yb;}UVDx2;;9QJ_o_b4m^%RX zft6`)C+e2-vUh@WAXDdZFn5<_pC%PjcOYB<%biMTE|z;p2f!qpW-eG*j?M{Qc^@jO zoglj2K7O;D9^~}!lYjp|_@jsa%@>px@*bpn8$}u;tD9*Aw$$ zh*Pb)_Dy^5*1cSk`Vv#5bcT6B-`v`W7OU?eXKB()xT5}HDO+E#8Vn0FY)0X#pdXO& za=l(ZJge$=AHFxUmHc9DYS)GRd=hNk)973^GlaYhiLhfVjhVsU6U!#pPHgT`s*D1; zMq3DowMEv}VAfTIY)cbQ@9VZKq9>jnmhZK{Tb4%qe3dr~S3#$BRZ>o414_KKF*ctW zdP+LGszHFGD%GX&!}*;TliZDy3Uou4k&mnKRWOayM| zP>3cxOKd?iVB6Tcm`cRjmab7$17sW92R*>-!3k;NtGek{372s7$V@lf)GoK%%jJ5x z-fq47fvalhsvlo2mu=fLoW-u4us3g?e)cCn|C68oeEZ?@m;ds^$Cr=4+4|P?%VpJm z^YlGeA`umt6IhTMCbT>Km`{y}P@+&7Rq41y+&@nR^`!+u)X+f!o1u3#qC+-g2gvsB z-Dmp!2YvpYRPmtMrh4uDc7+;ULrbG>x>>(mw#(&q*-QlMs^D$AE!!%7_E$GmuZKwO z9leL0TCsbIm5&g-oVl*C<*5L(d;+sb>WvDDVZG@{4yvW8iH5J5|H+6TISzzL>)R%{ z_7+T8xk|ol5ry8<87SK=x-mI06QbMD9*3UUB<1%SA{W)%yciBs;jD)4AMTJZ`CFXq zXPwqfdt1&Rx7&BZO&6Yj`6rJjxxH-f&!ERGY8?9bF7p*kfzIc=P$wySGTYwbK8K9$T0KZ zr`J;!fzj*w2~9e*d%=9Ef{__N5kCmIMLe-=e|J$mDj&)<>L=cA;>4-5w_j$Gb4M8K zZdn-&CbU(!HUMnuG$LuxF2pLl7%sP)n}=U4?SWWegmqX4R_~WHZE%+PMk^y4(7`^L zKau8FfvwSh2qjQya)E0ULazR}tfq6LUJaY!W_Houz!POXJz`lX<~3A>fctA4vh0te z8Jn1?ZP+&5w!Zb=qZqE{LnF4}y`e7!HEB(nG;!Rg?=mc{mj6^u)z4XO>$0v8X`r0W z=MOKJ%Xag%U+Mk)aQ@ zrnj4QrGoZBV~4tKTkpB-uYFVNY64^L-8?54A^8CxpruV_WMNryjGp1KAVW-*<96`&?s>TxEIw=6C$7Oh}N3YXhz%G+Ca6YW>;I*uguuD&)RuisH2)$ zSQyMokU4$DZ($QTxxxy!s^I4Pl_T<==3e&K$w9=Pc`-C&E_*+&r=NWG$L;Y^&L^Wv zYrY&y(biUhXz#{uYJyyAM${5hLu+Uh6M>4SJWHbM(7W^?e%YqG)qr3)k$GE_P9=-u zpabz-K`}1Emg7_Fn-Fi`ygx0=6Ya~jJ)O7^wi(12<(jpu{!)s`fNs`Q>G@DwMK+}4 zl`NLVKvae_k_`~XHf=&-RaxF#{92PHrhWU(40DZm|#`#be~nzf=`ikvfl^%TZZS+;jf?UlJ`s!5aJQekA8yu7Pph0hdouC~ zDnzWU@Kqa_DXee3dwLQ9rJ41uU$4{%`cuR~QiQ5*5SF!QVLu|4Z3+u_2 zYu0L`A21-2vHF;J-ydosg=bE_WcQ720m`Ke< z;IRcg2NhXo#pD2htsA-t%(phU$eAjITpGlL`d1%cUd&eVhu%-O?N8sl0lM$^aU_-4 z6cNeDuIh7tYo*`?C=wX?&KXImak&O9RqU~XC~@VIdo;Bio6Fg1Xi#C)toaL3ItCdD+3@1P$zyeqK!%;IeGwr>r zl3do6(i-`=wDs}f3UuSvRfHz&YM^a>JFlnZwtT)UrzSK!NNeUp$oNJYNClR&Nl2g# zt(x; zj&S$EMx6!%VrJW>?S|IK#o%Dmhw-JeEw9!M5+$SdhBIA%jP*@}AHN1NtORL}L*}b} zV^KEg#F=J+kU=lP&*PgZIGqWc7kvVtQ%m`brE_jY`_dQ#_v*26sg0QmMG$8Dh#Hml za~TZv7><3xTWX)&mjJKql;%Gp^(Kq>`sN`6OckPCH<10d{p#mme)HkEFLHt207HjG zG!`^#S?)Q#P4@ssp{e52irks6|BeYsie@G|JrHRUaS)?Gnl?jg^3b@RS6v#GCL*?} zC)Y_MRXn<;06et?o^2p$nEAqjK~d$J)A7jvQbb6Bw}{FX_h zFwd%rDg1kgefCR+{^L89eetkQy*5G#y9i;PkrdJh1oB+5oWWK%r}#fY}T zTED8IOYd3Fr=@0v`hu9zsAclu>G0e2f~*RSY{;5 zIj+GeF(;v{Ev&p?GZ7DODAAUMfBydI-+q5-uqUBV7Mhs#+?m^brZ8fPme7wxAC`&= znk9A);*T#%&!G%!?{b(xHUf=+dsS3hh*N7#h}J0DL=3G*Sji(_3O}GHo-OV)0`Mew zOF>QpW~N`5g&dZ1O)2vX6(%!8kIi5f$3YT+(AjmeFN6XrNipAp!FF0sjXaSwTQahj zvOjirut<}S3!83Qv+!$`@mGCWT$I)i~L09Qb$ zzlf8mMhPD;JL9`UjlKuVq|qphM3~w9k=m)qiGnEf7rshdAmc{*pAwoS(khh!M6waJqLy7^A zrSmx5odihdo^EnR*J~{t&|Z65CaZFWIaf96i8Dr+DUDv8wp;JEefGD zZnh~KTa$;EGv}lHlI%f?;yTL-iGv#@fHckN_rc<(Qa-Q$e zi$$VTTm}g32h%sIr><`nX=aN^YfZgUyLydFs}lh|Xy8mZE2FVS!Xi+r$MKfq1Ex)Z zD)d-$MMiXpbY~Q{X@9xCyj{Lfv8DOJIn9mzI+bNgR)7vMBM|ZEM!=PY zC&Mb3F|(HCV^1q_XMfXd5i0C_Ufa^tE39-a%(YA`ocibO2ndS7=%+`R=5{#%5++B4 zfk*ckrc+8W!)x;Gkx77xZ{&z#RXgU04J}ObRBQ;dFGM7}nlqIuryGjhUOcV{( zHSo9wZlgalw+>nsTf|>GGbea0+Mf-iof#1FpPldn9H9nk7@;v>aeWN}qa1#;Hhgz@ z41++dY#9{>>of|qDe&%Bzc@dfK3=vB*33iHBSX#-;OM&veJ`8>q>3LIkv*DXc9<3g z;@K&6BMhIQmr~}#R+_cex@ile&;<0fd8pA%T?^jSuBIKnyPZI6q=~4iNbXR}U`lS} zgD{tYch*)~4599Z0g;_fE6b9z6zco7Xx$Hl-25n1c{)EV*Y6r|y>2&(nECN45tcfb z59PiY;L?GBwTX~^fg2&kbvv`cGUgJ{;O-`KPF(2RQ389{57+Ht=&+C47ZpT}eomt{ zFH`43&e)2RVi({k8K2lX1dk!3$(zajxy}b>Zn390FfA%CeB=v1x9vi(wI)rdIu+*< zLNf~KiIXEef0|txHgXC06j;de+xY{r9)ZHm+HO`w@uoP2njmt<#6s>Z7|$m(E+P-jJ(q4`V6=j=@AIvKaa_L)qrQg~fKM&3>+Qu3;LKCNDZ+5_%$w`g$+ zZHYuQZh%pu2&Q9;p_^)_#GyoGg{2QN3^NBq+ct#pK9C#5_R|E#`>tu9Wq?8izN;h8 zN0}}CVJ77~I~}41tq|aX493`ae27z;Pz4k_aT}~Ze(f(wx(DH=n=BNd4=feKkn`Y; zh#A$yR*SyNKEnk7axo;4B%`olm$)$pGcUlU9lC*h`q@t(9#6k{zI6?|Um@KR%N407 zr>qqJRpO5^Sx(2S6c%e2B(t=c!E=Kq-J1Mr%})*>9?s{>vLs-+ahG|SmZzYo&eQ-r zIj>`FmU8^kxlje2k~&6&<>nPH(}d1&`FNCM!Shev)8 z0MD1r>*}oIi8gh7HU=gZn@pz}XbKmGXK!O3hJ}+NpMCbzj4c*3VB2O~`1Z8C6*@G& zXzyy9^=56>;@wp~fIQun+2pR@I|FpsIF9c~Em8(uv5Z9!D=wb6!i?60@7wZ5mQ9*P z;V`e=XRrt{z|+rsG0eQ`TVYQsy`;Pa^CIDOQfO)P??z%!570wF@vQ_#fqQe&?WhFd zTiGx&5HJ`WJg*NTVrtvg(Yq-`Z8{152rj6wc+$Mv?t;XVVbG%#L)uTFT;a8+FDl ziq%ydKUV~-o`5(!D#$X8*EF^Q1Ox5zabmrjxsf)yngC}2uv9P-WuJupuDFW97}Y(e zk%x{q?Cjr33jHC&II{;E? zIJ(YHX)`}CT-w9=&D+Pn`px&A?_)-bDXSATh+&si$y^Sd%(N5;k;tI6$YsK~WsgWB zdSn6Y09HTC*dHKXD%-mtO`4fPx`nUkI$R9CAG151g9QP&LpXV5feM!S3(-vz-ye!@ zj3_eFOqo<3!|={vfe+WglP@YDj zW#s0sv|lERD6~QuC4&KC0w!PEN;Dbu<^)(8KR+!el^YbKuf2;bhNvYugtSzL4{}5T zQIJvoUigHig+g$(cWO{D^gx{-5Onavh=!pR(-Y?%FwGPshPrwJNm9C&b|8it?=N{h@I4oaWNB-d2RVT?#3! zEkaT4mu)*ovJxvO%84EaAaNZ_TJu8dskyQkQrTcNL8uKmlCoUsO_sPouz}t5S^K&! zzBvmN-8#q7NS;bd7_|e?TuV@2T0$UIX>c8|<6;^X9g_VDL|GFQ*1vNT36TgR3=#xZ zPJU++U*O5%!Sp*FKWr0b?A4bc=BAf%Vcl9hT@zZ4_O1uXr}6MtHNY5gcFxLmNJS=~ zc{%%9x|xh6OaCCbc~UoNlUinO&2Z??;)s`8{O#LXL6b!u-@jQJdpE9`L}No_nZ;*X z0SbKImLhG9V8P6{XJXTs_*5RDzL2>kp@=;?4$@9S(VJ~I^uDZZ6=Fjy{+dlA8ww<- zS8`G4N@N9^&2eefrj!rFEPP12bs4yk6Hwfn*fmK)GE`(8?REA+10dBq z#A0 zho!WORV`we`hoH&>7j>PO2@@$LQP4YTY_5nBrr1WbBZ|tkHo3*%_^H83-5&&`-VD0 zbyg^4G|sH$yWKTKne^=CBB&+kM}_pEk&A9LAh#lo$;jwRtSor^Sw}pfhe(iWIY_IeOT+%rp4#>DcvZR?} zUD{i6i4$7Tn08p6n&SPf`7=xANOoo=Nf%$&+)N|I=yAd15Y54=Wh^9ReAHsn+R|E6 zHm}cI$#X^l{p3d>BIgqtGvib15E06r(;Q52RparAPQGTieH2Q=FbHPoPum)qj#N4f zN}*UYHmaQ;-q>>Db{kSQWOCx-GGhx2EauG#C>Z#-+P)Ld>SD@FV=- zht}SF_PK8%*G=6%Bpvr1<>-)4G7|BybC5b!@pxFF-ORwX)HOeOnj@smXRuK5 zemv38KxQ@0LDbRvgFdbGvz;7GzmhEJI>3zD$p)G%%hMlyaa!8Rvx?1Nq}S_!+aSqR zBkWg^Th@9|!GUYZh>;^!zn!Ir3L3d<3A%)kowf*0jmu&ps(Q0c#nTc(VKIsxkQvwU zwK&Fs*kjs59BN6x0_W!-UPjUe#mFcM#~qc^_D?rj2rAM>or1!EfX#?@ek04nVjmZF z0|e0^pcI8{LW99Kvyo(zlwoMn73hyRvKZb8d1;&!yIaW@1#|-IN%q z2BwkBGWmSqmfPu~M{$)e-;F9jEYs1m7?}qnu-yC@nKP=zdrh=xMxdS1PM3?Bg?&wP zl#VRhnwTN%L0Lr>N=iK)APPc^&_sIZ z`~IhD1mJlrk@_egM5IEBsd6i{fzjG9F(;re7j2YoOU>wWEi#r@QS&jg<@c z%u!iDn45d7kJ4=!+NsFw;g>K;(&QgD#@tu7fW@Hzf_uO`hBi(6B5Sqa=KnCJCY;Lw zg=w0{6q>S8mgmi`&HP|388qQEpmbV>br)P*_E0ADBe*2}g|cRli+Xh`Y^;n|kS$dq z8H?%8OwX4qfd?VU$0l^4o^0j)l-hWkzwR#OD{l)5%73WOCbQgTq5z0EAeO4@L;mBNI{h2SE8qQ;a><@RFDc;*=nTxOzB{6uC7ly#bRtq`JQ zH(t7Zd%fLG0<)-I?Q3y$U(Dx@bZPZj4gfYyN6JOmj>t(YTXAQ~?k8>2ezm@eh&0ACYKKrcTiXzGU6^nM)s{Jn)sUVWoD{_+Vc=x z(rxA)`VQgfL+>CfE*B57g5*+=LAYfG`y^(Vdp50~gn?qoE~%P!I6t~L&1IqISTmSV zGoILC; z1U6!0qKPQ$jXbS@%+vouF=fn0q+-7FI>abmkQS)B&jwM-+RpTtX-6RM7)%y$3KG@w zK$;H;ZISI6MuUgC%e7)MRl)-R=Ai{eI?`2YsDYF@cQhY_C!vvVm!+G>f2Ev_JKo-Z z6rvmW`YLxGn%D`n1;LOZ+gVeXN3j6|R+01479nZswZVP5CvYh~^PHD(m^nuUF=`}- z!{?8)QUk2#j}ax|qMBAAel7yn0U2Q#WS3IGFz~&xEo|$v>c`FAi=;;zQC77uPZD#A zPbZtPD92olxudNm=?WHDa5(16$!})5sl8bjX|3Nn7Ed*9Vg3PO>Z+=0CfsInwJ{EP zFY{#PfB*p$zgZ?~2mZzmva)9qO(rUpK;~N%*)4_dHyH?2y@m`y|_S8k|l$Co@HRrwE`Fmai{o^>;Qs>ps1gKL^1k+m?uL(X!Y#_g?pYu(Ya$-S3 zRYRvNZ9TpJ;@!3Vt&A118Kb%z5eS z*cxq8Bz-t7E75?~cos1;q4Yh+>|TY6w~tXGNA+v%r!=)#fN>TMGmIgGW;t8o5E&w(@i` z@v?;(HW3kuQn__#?kUb}yNbwX0EkoPQcj8~l5vn{zy%=(3YRcuDZhh#JUAy?N@^KU z)?W_4Y=77CjIE4mhXTH&6gxzh>NwI6FN(4rR)aZ&-Z*Ip%aLM8FaYdQ;KrOy9~CMM zln#v*-b)c+PQXi^U_xpQg4xR8%j5?``e6$7u;lXe+1rOxlit%@+4XRS{y^sXH*+2H zt^ldYf#ZI_NYwTBLeVt4MJN<^=!DjH1db^MvkHkD~s<8J$SLygss@;@n`6)5ZZG_}Ijpy1Gcp`eYw;t3oMEmxg&J zGgA7>CKPyq)ZhV9qWf05lv0wU4xZ4zw^q)kc?>}=S1jg zcE^2_rGAhp#__r&a#0ea`JqiF6`aH^Rg{2?k_mrM7=nEJ<>z0o4Y0oT$)c;fBN?=i zDjt*5RYqRU^DyTozL+e33}rmzGMUmFw6GTxR*9n%J;|sK;f_Ku2v9npIy11w#00&;g1-THKzC(x@#wf%*QG_;BRT*tFfMJ-i zzI0?>ASZJ-bHILleVl_Rgb5TP0>SA_HM^*t%o=Inrsm_HqNJhU?Z}!HCK{YP3FRc) z6Hg2-WuohV5He4baVq;>=tit$^ZHjoUQhfA*7O3Dv#JGhy@%DU!u*j?gdwR`O0FV8 z74b-+lbH5W!8z^4-@}6&GU$5?o3cq;{p=u_Xf@Df58aap?^l86n-b0M?wwkk&@AUM zf{_O6zy>=leljtkhU5}8({8$Tw;+5sOO!@8)1Lcstb{qxuS8{{d4P8IVF#o`FR2=% zG;bO9W8h^5MsxMy7;nyIUP0s`ehGyx+Nw~mT_^A(A>Ab2qcg`dr{SRhviN?sn*5^s zb+qvQE9^Oqim7reJ6}WajN56(gQFs+CB_sc)`Ybf1eo}sG=RfIz5(q0FMrwA#kTHt z)mIIg;6QM zGv5(o3sNNRap}O21@o);F0;qMT8QLNdhJb^ewfXr-q6OqIm@ic!>n)4E{FM07t_U$trMaIWsoI zs3xfTo1E9bF9U9VP{bdb2ToJ1Amp#Se!UMD7B~98#N?j0RaTILPWN4rNx?nU~Rj z`^#UxegF9V%e9deeTYVxC|LZSB0wPanoe*t&^|E}!-WlK3ftS)9I_&wb8Ktk8+yzv z54DdlCk~~4^XXb0|ea^Y}zWY8RGBUHWi=wJi z>M9x#EttRni7^ufOpq9W#E1b?gqR{RV2Y4Hj1hkU113GB)Dm>}phfjZbyv}uotcpt z5ijDsd(Sz0xfuidaPzg+IrqNE$gGS|uXu6p+H3i8H#ax?nYsCw>z~nRt5Y;Yqwvf#CGcm4uk6#n?2=gHpxP*fuM+Er4=Eq}(TiPy(l;)H)6{0KzGyR4j7$ zC0z$&X6(zH;Q_#76~h2TWk8_D3qcgyw3=nWO>kfkQic|aZWMK*k|1qc$X>orR(s#R zdc>LB%49&5x+oKi5~PzK=xIAa5!i<-Z5WUMjT143d#=NHf(S7JY0OeBo6%AFf`use z0_qe15Nt|qedvKj8#hw5^&d=V0NbI4s#xQg#2G1)TlGO@wl%CHJ4Fvgr;l7SDZ=#O z(U{qYu?`|4>bCa);o*8ECOaq)3OgFZ<(m!ty8^T{2%&dJ0r9yo-V+rKOc+vH{FbRk zYM{m`T{ibb1&@R+pYpAOG|hWkN5IL}V5 z2FHe4asG5l#!yiNGltR;#knSKj}s*TthRv4WZxDWgx1A8#?-Ij>{QW9Ax`W(2;Lr~ z^dJ@c7O-T-1%*OyU0Rn;(xOca5qyOpaoHD($c(EN{rAUl2oOaRha(V74D^6z3k0un zxQI|7F6$dH^GwCcHdme)jlje#s+)?8;hSipG6vw#d$0$=vrI%O8!o8j3mvRQNfH%x z#P*$f-o1I?rTGL-p9;dZkDG2HTXzkE5}~yQkscu+f|VPn~q9>ox>pcI&znmvJgy%RF@!5mCq{SuX!z#NEeVM z|4?g7)y&5~`LPS3ouC+}HN|UlafXLYj-s^BXIvxXWV0q!h3_8P-AYW2NQ%CMu=|hf z2W0a`@MZwYs7sT)&$V7&-+yck3D z&Ly2OR!1v45Xe5VZ7c#%2VylkghI-*CYGikJH4za(7>C@LRi?N=ZXH-Y05Pn#RL|q zqm%Xv>`HqORdX9}2 zik;u_IHww^z#`+G9bK5>Ywc^B?2Z|5 zqju?(hKy^nKS5N}Q60^Z5NF25PpeV}RHQE{>O1bx{Ac1?TB79iK%y|xepQ)qD@VSh zu3|DVZ?_FZ3%7S~-VniJXj;a2EmRH=wWhaCo^K`q;@ew=N1+N4;%0;}@=bVvlo7pn zp5zbrDh->aPdDUc;Br_86Gq+Tv{Q*}4EEG1eff%MsX9}PPGRZJN6D87YI_OaU%J1u zQDKEr`F5hL+z?7^Y`RP$j+*uoJuWgdzEmLsFYEf|zBNYdSV^S;(wG$Y-_gIi@8)0) zrd0f&&@B2BMA?l^xooP7ZYxRW*bQgwq2L~ai)pkVkkh?~3`IecN05WRfnx@CV<$mP zze}^<5E>Uf2(`VRVHnQ71H1zOo1@nNTQ0c}t>GC(y`7li1C^)1D7!qG1?-5Y(W)aJ zHuW+AcO&V^`! z!VWSXSp<+q4#OpmA#9Bqf#fcKHXH%2fS3mOk_CYYs=>wB_LXel$HgnLfdHt_W&^x` z$OPp^vCyB?M1o1jMpI#$DLHZ0F|7X6(6GeMk`uz;8uO<$oC`wO1forqiLT4?<=0>9 z)^TB|c6IR+pCk!Y?a#Nqb%-#tBRMw$w2P`RH(NYxv#or5WiT@hJ5iln5U3T}H)^c8 zO*losG@7SY-dR4yGtPeH2Y%k0OrZMGolgMevFE5box|4Oqn91gvEWp5jkDqYv*?fb9br&#uc)80i?cwMOXZQF>Hmj*z#4uF?N zAjO7`)*7?<{X!r+Oc>Rx4zjF|8LFSO@9+zQ_AvrLp(1WXf)D8zqR3RBmxj>kFodqO z;eM)}37nBYyZ30qpd~+pJCzPNI@-M&szdvgb}XH@*OF%+F*}&v_Bwr0zuoh3RB5~_ zUlP0(h=}R>^_OjJ%Cxx+8qQF_C?bqjg)6Esd3hRGpA2+dqL_g&+K$R}M#2KY?TPy% z;TC`|JXcj~8pibtgjx z6Q&3#$G+><2{3*#FU;JC)<#WyUtjjsHz4F(OC7^DdqHpqPV;9O@28OeYyx-jMyrqR zSLq+_%isVY-&@&JQ-(xN{F*W9Hg1jrmMC8Zwl+X^>Y~Wu?-3z4lV!yML4PJfTUP0! zXvEDIelXZnN>ns<8%GTGr=tN2G?R|*p~j4a)D#NACF4x-y{ZTlV{S~=@F7idY*yHi zY}>$;TU&&<5jAGpvJ5^#>w$M2bqB%(Y0>4twKB(H$NYd?Q>_SlyD#Xg+{vxf!u-oy z@3zCLEqHXF-_He!>QZYgAXW9yKN-!W+(Ma+Wo*-P<7^oW+A+d%F1o{x9!03sJn|nE z+ha?(H6_{`-JIHbhNZLTOa#S}79InPJm`D7Y8;L1{>UI_ePU zot|EN2O3YtE31x%;7s0-gR23-@O15Ui&e3smP$>kyueq#z8l_WbpYO-e;%~^2X3Gz0M zVZ7*F4_(agA;u8HXq4pn=&I--&i;1_%~J~xB@g`c^OI(WSdBm*!#oSc|4>xR>GeZ*MEM-kv79hM{}0;XL`Js#*E2Ee`k23m+03-bD4 z1^H=OLfqEt%57=1Xt(K1zPoBRUf5KPW(!#vyxaZRUzCEC(mtFQ9kf#kLZw^~vjr@$ z#4I6>jua!NQaT=oo!k|Fq>Sdf(GQ7dRH!)Q;Zb?2@BNVCuFQO)CeTi?-ojB+8Poyz zz>%8*jU_V(XVV8beZ)Kdx>)!c?o6EGPD5a`laSKk8W1t=*dM~|DMHKyP-@(+u};u- zXChUTB)}%-&&$MsJ;%p2p+D8ObwNs5jyk{=C_q58iHM>D50}fbH0@n`S87D8)Kye^ zw}apz05xa_?8GF&)=6}%E3p9pQHWSKzc?}_QW(}*MPm;xOpbYxNzSxQTc_Wv!6Sz8= zgW^hC!g7=y@oOrdlArjj3Kf;ED%U^!uiky}@c7ej3nzv#qtFQRt%qoPAru{gX;^zg z|71m0sj3=7I~dwjnsim=^+LFc%F-5UO$n-^s#|C6o5Bl)*j^Ai_WubNkYW?xbfvR+ z3-I{#3AgAzU};}+v?E1zV>E=|p=9ftwCL=QxR(*M5gsmg))ieBS_G`hKHVhkXD}kA zbT1(;*wH5AB?aGe_Fs%rA>=t?If+z7k~|&TZU%nNIuXSnCPjoKN~LN8Xy2^+YmiMT z>D)ad?3S4RmkC^Gaqq&|YDqz>h={|Ystrrbs#BszHl<+d{OCPFsR(!=qwK{pLS z)o|Fq*q>CNM(YMZrX_nZ868lqBtP1+FpoFMWx%;jMRwwZ3Cn6 z!o(^z>Kps&q+Fz@S-T!7N<=V&ff!EQ<7J{)2>kGB0=pfZPcPd4b)pE!eHDg4E`ebo zB9^^NphON4S+qvd7{i!)Xz9bv^_i-3B>LC|D4&}QL_3D7o~W6a<6#SOubfo5cWiikdTi@L zbWxSHH82ZEAtJ&=u~4D=d3zcvqX2A6WI+mi2GEjQ6iz?fA532?JAetr*2q42({}Yz zkMlUEGU|{uWfIb7+3vaBM4z7J%R6qqm9ei4Eo-KS!WeEzvWY^_L*xhVvrl7zf-||9 zZqo*&KpH1(yCRzlNxt=WpkS1Z*p-d)*cPL(sMgVOn_GQ<6F;U&9v*H9lWT%!FGaSk zgBNNSl^Zj-k@_y0Ea-}E&MoN%F)sRfL0zN2=l|DKc8ypGGQO7HgMoX(O)_+79u4RY zEQr;=G51u{ppY_k5d|seX4??h>2Dwgnr1s8Mi++&)_8UpqA)}GAbX9aXRz}p?A|s} zRFzx5-L&6b*LGcXtO8TByWALH5$(PXz@wb4F+jE>gt3VU#7s@KM}foJ){u~xapk5V zQg~-QoD;Zx2%*STqJ#cmex%)9Iwhzdn4&2UI52)KW4=w^0Di!jbPFZxV5HG4;k*Xd zhh*%p{nHtu(=(mDBrOkIFGh3EQgD{NR#~Xin@qwrRG5eW>5t#Bs6v;eJ>RxSPuUg} znEq1jrWQ&E$~hVHk#r~`F!y^1fU<)7*1B%nwq7m`mqxlWb)~KlJ6yv?F0|WRycqin z_uC5~Qj0aZVrfhr2%%H88P4A7PHRXYFgV8UMY((|gpv$GD5^G~328qKKXRCySA;;}GAT88NlR*k0p$4d(rc0!Lcri;v z{H-Z$BU$STsKW^RoYDoU~aqR+vgSWZF#@kwy%Sk@)Gz$q)1AdCbJVJI@J zS^#7xZPf@wcsjD9m|2u*mi%gZ7hxj>IVUN|3YT-5d3|;j6hC-K0hR|?ozdR|*hNwH zL;uJ6BL=OCOM&t`mRdlSR6H2StzB1&dA=pfgb#|LQd161l^9g-h$z!)jctXLKwV8n zKt);f3VwUNqvb-n-Yv^Qc;4(BQdRBg5s*cAV`p@C<~^0DipNw)$(D(#s%;i+LjVyn zs?Zt4#X<>K7^&HY&!dQ>@FwiLsdu3;n182MDS9L`FWo$?Q5*+nWU zkkS5|G!~2M3WyR@YwdF37V{xkK}ubrKBrZE3X6(6_X6L}9(Ksm&;v*e1CTA~@}3z9 zh>2tWCZ?DXrP-GcuBbuf0257fU|)%lDub5Z<@t8I=S7J(6%QB%t1S;J(7YDO`6baO zPqrr0W`+f#5~8C^4p}B;maRI;U8He))9CHH_quIZ?iH~8K1Ec!tq(F6ocp%fXfP8| zTcT|oJeZ)l!)ZWH$Z;?tsL*igohS_FfDq4P5X?jI6mi__ILD|?4SJU=7?Fae4pz#O zcp3#ux_iOhG2G<=23vn?YLYX8EVWLOTQP75+U_Nl_eilit;PZB=Y&ItihN8Y4FACy zIciCO3i`7uz$-1w^5&tdolg`JMpR3f5$S|vO4&Dru18x0Jqn+L!){8A!ETUNq9(Gc zf)_yp8v7n3!eWa|(VzlhTW>F0wFhBDT*~Oh^q!f(Day5(osdkCuMd{zVSP07B^< zyZ%=TK48N}hL@OU;g8&>Asfwu4h$J)09ndF_NYJ!nP;I?pX|xkktp<40kRVl9V!;k z1=P_Xv@X{bLJ;)L@I=Jc{u2B7aYPnpi%a@VwA19-#M|PI1LWIL`sPB3piz?u1RHa6 zs5kt}S|=f}DhpV6=>ieCUas#R?pkX;-JU3>Rm7rU*GkL6#bv_&(A?rx``>~25w9G8 zN`Zz%UG-Vz%I(q?fnKle?U!Hn+cV$2(XHE*e;#J8KDX0~8h0hgARFVWbl>bJ9c{z8BDNxLk*-^BR(A z0n$K$9A}%TuEnBQs7XL3Xip~~zd!*5+9yX?zQP40*6YZ#SSdvJw3sWHcZ^ODo1wD2 zeek&=L#`%5+zP`A(jD*XD!O6i=ED=l&>N|EpFOtwQ6*p@eSY39==XQacbihzzICRy z5=rAmun8dy>9*IJkTA@{69|b25x9Xs%<6z8(4I~|!2_7djbVaFipCQWHFA(|%nL6d z7Ne9IttoSNj*+s3qPswr6>pYy$G7{HS0c8>F9+;Y=Lu*C873~ibj{{IfI5Z2!=3JA zdN4e5C)YY6jM*wEtsama9mxBPzrvNh|LfsGLMVJ?ZljT&?F6CK#N z5WI*;8LV&U12N$js;KXr(``~@*PGs+`nKJkFZXwESe0-kYUB|a@~*V?0H7A5*S&Jh ztezk?8+Gd#lTWF7};xZ_0c7Huj|<5nTjO z8xgSCSAnhBmPNY#(gB1iiP_Ptf}b#%{rvERia4R>b2J`9&hn?jc#`IZ$P!I^0S%UB z$T}IQ0x5p)P^rDPYUG80m(1O7$kl14J~1|@C!}H)<@6j?4>ivMCQeO~VuWW5!o6G& zE7AJpm&Cq7UpV>UT;>?Z7<4}4d(xQ7gz{}+g1hCSgy>?4+I++-0j&3nJc6&+CQsaz z86XiAD7BT8SldF7G`IM0TZrI9Ib4lve5ExTioG4kJKfn;O%>#^E%MZe1){CZ4V4c)!qhd>UQ1Il z30h|f%&vXuo&>gOnKWX ziMl!UB<|RnT2o`QjT(lMnYnReYCg1S=K=|s8|+**B4vt$2Q`BNR1~+Xh}$KiPMZp~ zmHP9q{^Bp_FaPx6c3ZBr5UKZH9kM4N2=F^-IOTX%0xqHlc>vvf5)O`lm@d3Dq7S#H zk9~WCCQl!6+myt%@6dFQOf)pl3HPjQ1zUP-Sz3C!Y2|CET~%VHY#{OQT~jhE@XYc! zuyH)ESUl<+6pV1k9s!55Zx6!&)UkFAYK$_;aY8Cw9yw~It6m52E&ljW#8C1XBg^-0 zw35UAGQJyuFg=p+!ZmD+f<%=?*Z1$avE&B*1O0;xbDpSBv=>uXbv@R3SVAh1pM0*W zXao?hz)yerSMt-pq%VK6sdh&1KAqxO^!e6rTkl)%+s(GK6@`dc)K{P#x`~Us5}2^C zGDM{JuGyp-uq!ciz_67C9nu-fak@xr>(lerZ?f=uSuRYCxlvnjxx2Ns^U|4bTfc3a z^xpO9>FLAMFaJ{i@?U)ai$A@Sr`wfdzkoEG12=1zOfd$&rd07^0=|2pA#BUm1 zMSt=0zkK`gvs;q+TwmtT~kpfD+6Vx}w?qsKp$825| zsM=wfewG`kciC>Y+tbt2slMqCLF#_vUo-iFMG$hI*N*|GK`B&9*$j6#XaiB%!AL;fko zr!%iQY&Ah&fNm620*VY6tP~5tU%vVAa$UtnH{B6)u9N^)P+ZG8Sf8Qr zdp=Pv#t%u2P0AI@)VS&G`KN#Kr+@zM{JVF5{NH%^(H}1Bot3HJo9N@VefaM7^!TxF z&nlbyk>nf16R|RLV*=fJe|mcA+Y^%zLD7kMT^1(Q-eucFx=8Qawr$V-=Kn!9(hVxw zW!tvCZGJ9U7u;^TJ@@SffViPuaec$rFZlXSh($VdBiexHMZRwzf40btq1_@+Hj+RX z+7*l%mcbm%;WU_;#Q@VZ0I_Nd9$A`uyc(Pz;6OuSDMV}%Si&s6FI*!F3LLLb!v<6- z7`G!eJT?b-B{(IajOb7t&Ur_C*ZVL2{9oMu**9;0|BoKNcuR`s=iA2*A3l8l@p+SH z!N=$4r`t_cmvvn(D-&go$gc*&wce!=(1#}|XH*7b#-JYMH zwx`Ftr*FAGUzl#9H(>S20mE@FJf^V4?jlsDv&e{Jr&hg&A|{JRZ_)OyBM?-Cfu{6! z;h$~tq+Oue7V8B^As!=eTjY+WQDReogkoQbf@uL0oU-h$p4Di0l3ADZfm}YQ4(d|9 zDX``DT8kd$Cv4cR96m#7PqYl^kLSf=ISEn3E~LMgbq%6)N_OZoOpLJ7=8pJIIfrjl za-r!TM3FM8J{^}Ro-B-B2<@FxCEbHa&?oDe;-)?eyf^0To zMXj~AEX!rRT$iPR+t%g7$M3)Y)h~YWtG{}B`kt4@7uL01?(f%Sp}sN14jxlc*|yvB z)Aszd-JbgPtkU~->)R&V4P6%Il^1<_zJ2^2&rjTM+Ba2U;-xLTUS+-8mg@#m=+gAE zpwY&1yW5`MVf)dAA1<~h9P(VvxzUgwO{h%~kkQm7C_oX1CfAMlT$8osyZm2T87j~* z2H0Lk|1jZ{hNXBfFFHV(f{t>L-ijNtqBR14S_A-QT@MYs9=RD=(`O z6F25YeQVp}vOSRsciFb*`|ZO!)(0gf?SRoN({SIU={jb8}5P(^v5+og| z%?AJzH`WCz(r=LUvhe*q+G?}<)#g=e__;Px-fiKH9c}=?R{T!pJ*MOhD9gCtaMfq& z@Lr$CLfl80!9A+&&a20}95sePNYSIg$Na9&o%e|AaANF3>j6|Rd!lv3Y5faBPth?; zV!CD{H>t=dB$Y)65TX$3j7|(g7mtSNoRh)-=_VW+F{P6hU+mFBcxIJI4_j=d1Meo0ObGO{1 zE2J;Xmt|?(;QO14swl}ueM9eZQ`rdH_H=vRI^X2>_>udzN^dGx;7TO@`SJNfCt6y2 zvs^Aqd%Lzb%7hN>Uo`r!+%5m$p?$TY*#cXxo<%lg{4HzLDxZfWpY-qco=!EwLq=Y? zPElWXAJ*RBK7mf39bVSn0%D7fP)dh`NVh8h0RR9=L_t&~&D2&^BEVK9iK+^9qPu1J z5%b%p4}bcX|Ix?4{7-KxWMMd7E=&YGt337o4BCL3+?07)ue7%1x-NHjASNXSFU-wH zw5UTj=^~=N_igK4pp8^jx5wM}t0-;H`utJ0=L_)FvizvEuiE-{S)YNwlK#CsEzjG% zJ}Jl6CdJV#+J}ztBqa zgW4F5U!nD?=-MB92hoqe{Bpg2qr`D^E~(h&;H0*IIwlgP`yjphxxk3y=jKv1V*N&W zh_uPKPrjO5k|L=d0UJ5esCw2VW@wg5S70o!iUiV0GS7dRz^lemst-Q>>*y7BvIev& z@@90le05a0j4C-J&a}2btXA&4pyHK~NmX^Q%uA#U0(Dw0?Xud|n^VRN~jfsx_OY20fcs zC(GrZtk2){?NM*P?ESl~cU6hWZKA@`8)$9ILU>Z3Uv#^C|NXY<22g8jqsB~^wp`l6 zt=S2Pq}uy!+ithp^YhcA-7jtZ!!N$+vZ3D=l^-wbzj62QuPv7!E$f@M5c5WKgEAAC zHp0y&ACPp~1k6O&Mt6iODT#K`ClQf;CAkvuMl9TjRb$n_z)0H;0GU8$zo?GASc~}N z?A;YUUz*V|0FXc!tW{C%KAI^v?L_U}dVTlqOSyHryTWXMkEBT~cMoyOd_}ZNIN_8l zwXvU97ye@s5h6l~bBWzY7uVX~=3Uhwd6{ms2cuExs#2NR^#3ua0T8eh)Sf@}mbO@a6uHr`SM-A!-{mj5&CxcM2#WQ@lTkfx5(jFBJ z?AIa$1~j9d8Q=-eer$aKvyaXP32-R`OOT#NklUEBzJGi9Z@;_yZxIQxpox;0nIdkW zg{XNz5#DJfQbE7<$H$v=ect;2?^oabzkl`Jf4Xg*`1jW3XZ`7){K4=2kN;QyYkc*i z`@1*P@rz%5FI}mtY}@nW7w3XYX@wM?nt=XyoB1U7qkYW?<8UgqmsyXrI z27LOTTwglb47aPA+jWM1R?*e!9F1&uPs5AZ0r#G$)Qr*iLEe<{J`c?q6Evzrqb7k2 zrH4L@i4DtJ&_YzC_Z9lD-`)Rlqs!8W`SI4DyF80Nce%;--FEx)+tXk6?K`Np_A}L= z>+KJJ{73)zzw_Vkm%F?BxA)7cz5mI-{Fguft3T`8#=LO5h|2Bu{NdxTe)-+cf7O4% zO|I+RAAI?T|BL_0|MJ6M{Imb-|Ly<$@%H>c{0D9M^XjGaX^YU3|#eHemDVJ^-lMOl)FMYj0Q9u0-?)@8A5RZy)~M zpMU!wJU(^O|K#!ZC;$1M{m;Mpa{0;E)R;DD56eOebwwB51ky>Bn=&??(*e@ud-cL846f{80*rO1?E|DBLbxRsqof2 zvM`q;pQfCUTh2Y&MhnM&3wsoTILFt~MOxhT@s9yg5cKzAy`A=?7ABY}rxBv9% z@#BB-FaLYr{Al^=%cp*83f|-&{Tu&!(?7nuUWpfxzV*(Hm~pwTcbALYw)dCIPrmx` zU;Gz;a{twvpFe*8B=Ysr{^8~R9pN2wQxcmHXdzGmY}@7PAj(y(O*1nj~}<|-SzUt7g*MQ z+a!vlAyydyRv=bfFc}_5_dQuDcC{P3)&<>xmK0dc#XrnIQT$EZi}TjN0pp`)Gy3nU zi?l#_tX}P5jI{_QFN(c@rjcGa+^4Pp=t;^FUPg~!%{^Mo2*T?TR`LfY3yZqCC z`XBt`|L()vFW&)eS+7hC7HX7FsX7E)c!-=Z{S53!g(X#&W^`7N+PmAvW8d%#^Rfw6W6KqtTiG-lqyZ3j6$WAH{5fP1$YmM>1 z7DR%eN-}iV@lXaIUT-DjCv!WcG6Rand};jr{A8oP&)@$Fw+<3!7C|bi#wQu?P*v@n zREgP!WSN+`K?J>f|DsrsgrNY39OI%$P|-gG!xTRwNHJ183l$aAn1w0?m4LdLiZRz& zF=oSgmP@%$FDo38o$;NPwN4t4aOlO%O)#VVOSqih>Rh+_2}Mn#S3zf~c53LGKvcdL zassT0+w%5}0Bz*&RdX+(AUxD-{>a)&cFTt{F86Ld&l%&yIVe zxv{OD#F5lugz1`&Xk=G??p;+MsC_T}qsW5x)!Q$>djIy_WqrQ&>*cPsX4bhi+kRXL z!rmk8U2Y$~2OgiV>;3n?_{Bf{=l|c8==Zq&|HWPrvwfdEaPV({)wY{wGZZD&jN$d<8tYJ5LNEk&Z3^h&eES1oDhS3cwEm4fFBZ4!W&Le<;Y9~CqhsN2YBbv-8oIb` zC-b1j_v2qe%1=tmluxA3)6`E4%$`Ep{m}iZunpYS_g@tIXXMl{V1m9LGRAmgZ5%of zg_wJ{@%Jg0J_P) z_x*SO{6GBn|IxqoU%%eJyIk&i7ZsaF1T@jsAwm$QPE2nXc}MHVg}3cj3$FjV zm8e0X(uf;?2nA2RsOd?;D&94N-t%Z#Unb1sjQCZKOAO z-{{Mwy?y`X{o8w8u59q!&;V8;Ra%-#C+)TYy7b2D-J)H3-?p~g;j2IVqo4h=|M=TY z8n7-{S1b**X=8v%8A?)wmfUa#2WoJz81D2MPFfK~`729n^LnN_|Nh&x2Rqq?qw_IA zzMWp}U)L&x#}%%^wA(Twlwh;W7}SUx(^dJNiDX-dzIylmt8d=DeRJQ1Nwt9(N?laC z_O8-NZluD%!rIujYvt!RcTMk=_=D)L7~l22TxelxOb|4vt%34`fS|}YU1<=FbFnCP zc$RoiUx6S^+ z&5uU5pu9%-%?EzjTS#M~zB@X(RxJlC>v+ISF2ZCM>px)I_+$kZsEV6nYX_%h^v{3y z9nJW;1cVJ*2XV1@%HNJLVV&&K=mhMdHxzH@(s$wDL%cq6am1d&F;!YJ*<>ojgZ6MBuEpl!!&thmMn+lBKj9uUEdcrQBlW?wpon6KRAY(ju(&?; zIF5i+0!VH1M?lc11uPJqM-Uthhjqx z0QXJ;rgnvHyteK3@%_K{M~}cSx(H;wEXzVm^NOg?LIMG#5wc8!R-1ZY|I~Q(nv6r< zmf?eFQrds=l1KD6f9>QwI>|%$qQxlX#%6-2sL-%trE3{Zhd`C0yTW-vNGPEgy8!@SLz2057N7{%xK){9RZdn>B zvrkNnhE4Lt$7(=uMKPy`vTu_x?BapkRE#thg6XI-q|5>cSkjopKXLn&%egcQnF2SF zr)?9WAAj+_trzu!3IhGbPSs@7U)jtoXb3YAF%&A@`uLiqCCd7y%{DL|D?xX(YSwE| zTB{UtKmjdu0L@)ZY5J{^d_ZeDP!v+6AX3#OuxpeYUUSee{O%I%8J&U5p9bZ%in|`b zWM+oI?-?|1<}{VsQ&ybZpARV?Q_*@^*5<*MbZCu+-Q1n&R%zm)0YRa$*Z|j!^l*|% zR$!X);SHG148A)0Gi)%qt9ID#Rcx#4nUz^vLsux%Lj2Xk`^N3*!}HqLWo2G$7M?%_ zB9N|-bj=#G5`&vI7I44bfANQZc(*hVK5X4+W3r{C@UacbIdi_BOpspA>xxbO9oQ=4 zL(Yc}^wL#*D4lM1H~)&SgRE(I2p$Z?m}-(q<{KY(5;%}3ipSubcb|x&8yPrC_m{gr z_{mSU=jVrqRke$61gk_U)CrrQ1DzNIvO$4`p@Mc<)-T@v=&Rpb{>6X%1@l)+yIY7X zEN*jvGLq$!HNv$mOJ&sjClh#gYiO!l*Z@j%7lI;wY{juD_g>^ntgb0i7Q2eMDYxFA zx^xipY5_4%o4MPFuCrfjPOg1xe?-vkX0ExaY`7_d{Gj zSCwd?FnfG=zpPDC$!1D3_uA6Kw^v0y4!TmLqcAG03m1tpm72EaWC>6npyZtvj$dL& z3ixN%Pz(ihAzf{fr_!6Hy~D!qzI^j=e_^ny_8ZWIi68_=7zO~FtI?(VGRK8!Bc|)! z*FX7#ukY^O$a4ptMOe8j7TX;vk8I5;6Xa^aNxd6AOtAkl_{};%*%FuruSyeP|INS3 zYtr$QaH_N#DAxXG^z-XCP&?cq1+>VH51;1{LIfX}(UjM=ytzKSd;4~6dUv7QCSp4= zshu8d3rkc~MATMsiMy`S8eQML{fGbHAKv}@|E~w)hsK-A{j$J2c!3Wlb_yU4863n{ z4h#`5P3$8VPBTDi`?EPO>XezWisF+^9Ay}DJ(eGj%{(!24;j5&*0wg-Bu=sn4Eqsd z6GS#YTLnbQHla<^PooU*LY-J50HQ)Fs(m`AbolJuC0qnn`>L$I%8Bz627R6+9&!C0#Q9%$8ndceH<(sT}47~Ov@eS z4NQY1|6_x$$iaVBU0BzKt_0A+q+S2=mp^}ed|cMHtb{I5s2F(oCZHyuwhB^w+}r8m zcDeiFi{F3$#pPE&-xN=xP0W1ALN*=->ql3=-V}S(&2o%fwXXJj$tx=SuB<93iGc~)13wFNOf z20)HesIP%clR^I0J%Mo&gDOG-^OV8b;Ab4BbFsCvG#ar zC40!n3n-9gHkJW^6j09qQ+r6DHaWj53rNjY*g;1eU%%KmPpB|LpeoQADrn6#(L#6qCf5?BsnF3n|zU z0n7_uAO7I?{-A&VEm6cGe2l{S)wYi?5u;fESfB(6r1Rmm(ZxO!wb0V{WVi zy9$S2zy1a-L6Y&gWsM$VXm1THTHqK^h_TJuhLpUCWHeHg>z+!=F+eKM+mq^cxn5M! z8WXE@pNM9Y07NL}?R)fER99+X+P3HC+h+Mbs&?q3$OQ~lR)8R5D}09`7ge`nG~gjL zefTVZ+BQ~3xky-)X@)dSRXZ?s-xG9F4hpIz7X^p`RRK|Be*48cD0o?XXjh@b*qArZ zBUI}~M2eMG8xKISegIq(RJBV9-5>l~@&#oRLRHkMX@JQ?mqBFAyE%A%rC8CA@4Tj)fwcNI~5A z{_T2yc@#}7j$}9^dQ{UyqbP%4C4Q4|oc zIIr=G2oEdYZ~ezt{_0^xzo9SfdIxT?N1`GR2NoIzfS3%OTcft{QZ|>eb+-+SOftj@t zL_0_Y+R#8`Tk?zeoE9hnn+T*oKi@9IUz5ITd{bfR3?d2ti^kLkrBR2<7D9Ty$nDjm z&ja5aD=K7eKy``IU$7p-G>BS4*~5{0vBAC1Y}0(Yq7iq(H`g~m`oq5uBJd*pCJ_WS zBMUOm#Jyj#iL>g#SXV6@y>^pVo7_e}iY%wNTXS;8N5fGap&pkOg+>n|fKbJ8J~V(v ze16!ZnJ-fYgP8eRKtslIRAY|a#H$ls7)&_HyFl`pFCINE*ND**2?v9ebT=b&)c4FC z952rLEkSQOOQ}01(A?I$%fr1Zohqs=d{yby^^ig%GEzZ>Gj;7)aAmgUiH~V?%rhUN z8FnP&LfJ#eQcMV$6pZUa?;3vo`2ElS{9ginyr;ZT|2;@NbZuj~%cQXCO;|pf(N&35!2m-Uj@(?nxiArW-yR8lSuX3k z*hn%dwI&;Px^?1B#TJ4S>DH*rwrwH;YRmHQ@aBtUUAsK1K8dUza~Q@K=^N*Kkvx(y zXdbYvlUB2xbu+SvZtj>6F?_C3^T33?6qN4o`9*GQH)vD7YwL&p45sC>@@4JTx1z%E z5I0+R?cL-aw#*0IISGlK4vWi1r7oJNs*wY`k+6I`)d(v4{}d(}ej??$bEJ`MSK(kt z%uOnoRBEXJP1`|-&8zLD^^3yh{Bh!o?TJjgK7V`oEK`o!4(salRg@n|XCWy$dL;5l zA^9{=_p9x1TiRu1CZZ81oif9VtahKALb(M#@i-6RaoL283m0I^qAlQ>lZYj8TwBSghIy%u#=h=Au>p{E4Jdqv)eVmNe}wy`uuHON5^P> zIew?FTTt&zMjb{IcGcbCzS@hLF*7blO4Ez94=ns7?#F1!*14m_ zsX0Uc82mhN-KIp$?_50^@xuDyw*C3XjhB_$LQ2j#Q46poEiR@Rhl%iRD5Kb>aAdo) z1mczs%7j#b>IIO2u%A?t;4PafFF>>Od#F;2z%!BlO4S2{Wh*_b()L=^5h1^N#&P>oPN$26Y^0Biz; zZEM<99~yu0=8c{8+SZlZV&lvdHijT93P!V;<-$T>#pQCjTrRhpJiL3iUaqt~GDCgG zT(2G5Dxt2Bum%pz7G#Qm5xG{^*2v}`bNxq?+5gY9+`gV2=hxfhuXE8!3Wc!Y7`a|Mtav0Sa*7+*g`z1F7tg}} zr@Sq#5CgR%v`P4Qd)}nqUDhVjKR)4lPfR`x70Nb5kca@u(0|~7Zv{jQ5s0X73I{|8 zB}W5;xZ>*s(!0Oyd@*$jH1RDN5z@_x&i<-Ni;x;NTt|-F=OO2m-l5H(0|-p{9za<*m@SC<$6U- zsxDHkUfn`lin6mkbIO@)$slqs$~y_c9!is!9H^@7oCrtgEb^s54kJ>s#|!|0YzI-x zqXQsri}gUs9SPSC4r{>7Dn8x3Ev>b7Syx$aZ@zrbmuv5oW=K?RzX$2cj7Fqt5d;c; zMJM0e)Qy*B*V2sK5T>)JU1|SG9?x3!U(Izw!6{KgR%g$|9eM($u6(*gnGVUu@CN26 zintt`oiKJX*tTzI?3+w7p%alX*|8K#qO{n+E-j1FB52yN5?_c2coN?me>MR~*(@RX zklUib`Nk;+;3G%7^`5CtMdSe#k9xY!46F{z8L4b=D&B4)n{)s_xql#tJU`KL%>a;k z_sV)xu1irN1k;INAyQI9e~Tdbd|X54bWYv9IdNmG?~{SpFqTH?h}`6Kvm92i9!Ypc zTG?d73{;pHWQ=Ay*vUEwoP>@}dTA#h&`P1x>B1Ds?JN6E9i|!P(+0kjENYS?PQ@pf zy@WZ}^WXKhCNm5a0o!HO79(La85dtj zeK$B{oM&G@Dg~NPP&$Ovh!m|+V_JysuJ`NZ(w1hP%dqpLV1-R|1_3nY7!v`+t+i!& z`0|V8dfy*Oi9q8>Jg>CD2UCoIq)(WGd0}%+*z38jqi0zw_tO3A*RS{0=VyK@*wb}_ z{quJv+;L1hEI@Gy3QUrZrzUS>)rg~ivUBBjcmL3qi}FI$h}aG%f(_dp#Ko0vum&C34;Mz zBd`*(=*o3%ZR_%(Z#MracKY?rOf`D{p5d|0H5$dXQ3-@rv84y$>t$LEPu}2!9MKHg zV@_u@aG}~AB|CT^904Nx>7bPBPkmegWBmcdJOL^PH&T*bJ&IIwSyZW6k7OalPk2e;0bO{jdtP% zYDm*bDClqbI#6G&<7?^WtG=^Q*Jve^Mh%Q7LWbrkgY1Zo9YBqj%k{o37vJ*Vq(N08 zltTbF7V%DjfIzS_8U-YMBVZ-E5H||HWRL=%2k)-OOgw=>HtWWI({NGt`oCjL%n+|O z@5mcE+`P_CGS)OAh5-Erd3Ra(dQsaMNu0M}bBVYR6v8|l7OPT)YWGm16K-`RQ>R$t z;W8fs$Z7+0b6h?$&frhZ8i~#aXpj)EqD9oJ5Jr%O<)Mj*0qFiPHiO%_r4D4a$fG!x zCTlT1GFgCpJArd)bj@cJJPl8@=#18HGdoHyRNGJ~ny-Sh4p6xgCDUq{bHgzuRlWbc zKR^uW3{QUN^Bf8(^k@u-X&KWQ^qq9XT%^(%DB0*<@=I%fZrI|%fmt3w5i?{k)5=7u za(e<|)XMxZ9|R}2s@X@e6{5;c!z8S(wp+JJuAH$k0hL*vUj z0;~ljMzS*|4uD94vJomk8p_zIH3Sv0(|~{S^*77i-RfQm*^`fOn?G#f+0gQfr>8eADU?PGD-7{hV}n(HB17}Y|&j!@bmeW_x3G9u$6 z%UYLRYS>N)mv4TwUf0{lr>xJ^u{n-83`kD?>40-o%b4v_fi5gGf!(|-tbZ8_t;Zrm zy+$88ip$NqOu&fge&GkEH`irZ8aHmOk@fT?Sqkg^0zO~gs?4_ip#_K6wJnQM?5P-D z08z1Ml+|?_b0MIXYG*hrNH6W^YB8F)-tZY8ruO*TLiAfI5&6ys9%V^TQizJ60ptfdUYJ&*)klrM zHo5@7r}8Ey3}7@$(}fI2GR4!t(yC8Ez~uFhT&S|}$kC~>2lNk~j647=Le`j~8NTnwsO%t*zo#aEZR%WdL9HT$pDv9Knor~Err32<4BWIp$& zvSpVK1gwe`rDE7Q6?N0mJzmWd)6`j2NYhN!?biC5c4UdD)G8tlz z(fI=gqC2Y;F<$|QMgNg;$bhT5f{B=Xk#}1b;>N8JHHAQwefw_Gp4r1!We@;W+4@Gz z^!Cl$D|3U|DWsf|24sJvqIuD8b8cy>5weOybKNzV+^5lvpvf*b;8kACkbAfd}eHtnK5@h@hJ#$rng$)>Kl(X|y~Hfm6M=EXgYXb<-0U|2@i zWR5kH5im%~6KTl@vIRMoGFB8ZqPuS4y?0jsl5g)jG)yUSy1 z!dp&SP~<9E7v!nT>9>y2>&nR;E)}ecfZPb|?lP~@yv&yfGSq|sKSbPKjafj1Mu5ok z(?`T91VbOA&0~TYWrtTp7`gxpu&Iy_Fp!OCi-S^*`Nlls_z6woiZ`a=?G^DnWn>|( zTKkX>3Ozc#UNIBDiL18uYgXatA&jRxheT_bmNI1?1?j~Cz9W6~4(I-81OgSdM4&Am z{kO2Pr`MSJW$Vi&&X~5dP2|!@7+ZhdRG86*ml8@f=<^_==dWY{LD4VF1%PP_6k%{? zj_kE*w77jWBr6#7db>`-t$-2cO~LD@zm8^R3z0xv1Ix#f-w1MWEeM?qGpDOq>JzUp3n z2HAeA<1fO!omgN!jIJuOJyTowvb2SH7jZDcvN6wML0R&isKDi_4TIM-e_uG4ykr}Q zBR__N1Kh3G&k?{c?U$_pf-xN_b%JkNkOWtN}}8tHbKaJh3c3l zUxD0)A{6f+3<2I$?G+803MWqCwWQ21j!i))cYmFWcE4WDHFZa)-=59rZ-f?2PlW@CskI2R5Unlka`73B!Rv@Fc9jmX@z2eEs%(DdEkG*I&reXS-svB~ z$Q11fO)`Q}8m2aLB{4@;U*Yu`bN5)V!>{^x-{ZM6 zv$^30W2%rY+9i4b!CxLI6fD8j<`>i+6)2F0o1~k4Iv9B)6JK!2&$djXek=Sp0~Ey(g3;i#KQ^{N2w53 z^N*0cmTxAqkDgZ8?BcAax$%8)RD`%K5BKgjS!of%*qJ%hYSYMnRJ#m`o=LUKLh>L# zHbL-in|n>8(z%OUuCJ|Z8pbQ5)rV-1q-PM3o>U+pR%wx0^-( zYZ!#0yX;iyWQtSN3(|#J#CTEi#_D$l4U}5@vY80Q+%deWS?y|)EBdC=x7+u!{pkAk z{rfL&zx*X0$c6zd3yYBkCPW*6w2O*@*-wqnc2ZlIm+nitH576E(LRG)M95PlRgr6O zD!AL7h}SH4o+!DXfI=DHH9|)C$*5ATbWdHX#anMUw|msK(?Uwc669sTX_?L)B~3kp zqRl12xY5GYN@~2;ApEG==kl_n2ctMG&KaV<(t!<)L40}lW(+uLv}t-i5e>{L)rAsM zoe*PXVT&lrPxY88Q{XJw5mV9(q&@zc?i`WN6!ataG$I4u*$06dGlRD08&ui$L-h4F zd+V$j078j8il_)NYikPf_0qerCW;Lo0!k4Jn~d<#E%#t@j*D52A*{|tO0C+l>cxvb zE7cFXUWKGruTz}UwAInz;SVIThl8Z)DG)&F$3VIpwjY=B254<*%VO(QC1WU~Yi2s& z!5{1Q*&coj1>w67-{(&H9>@zS28xrG0x2+=I1Z{ThUtVEMbFD1*4b4!?{SjyCJG_< zVMe!O3~@*NQgPS#f$6V4e%~&uw#G!_hm)y?TC5%x>&)ZenV1?I6K%MlHP~LEwi6;} zkcA4R0Fbjk?MDIF4El#0>|N)4?8lQuP6IhLmDvHGRF&MKL>d~*Ei*Pe8-;HLQ4G)s zmhX%S;s{-Y2`PSiID_YHINFPL@sX*~3$9j!Sb=J`65~S}9IM|(cVNh!rspw$SQY55 z4^Y0ppEkqTl{lCEf;oz!BT!mI@$8eKK9d%A3_b3fhOi_dYiI5+9S{Nd>_L;UR6lS4Se1`J~#mxryU>HNb=ynqfb@n`=$ zRb;Hcr(lwfPiS}y;hy^<01#wDXKu^=y|%^IZf#vvZIO&Jt3X7c(B5-FHIYvawRg2jfnb!$p2^3pFnE26X zvSDWG_hEl*eL&>l7K37m+6g{mkEc@-nx&jvBXU+%5)dWTeD}56mz{DN&7&GMjh}#O z_bt&>`0i@qm*@s$%2~sAHALD@%_AC0*G!|DwLW9k{fZnBx($bdu2QVwTc_9_3CwNLR4R?iR zrX_^b$mcL?8S|K%Iy0^ruj`?MLA!(N@Z&>a(-um;LV+- zCdJu&5F%!#+zZGf#?j?Y^(K36;GBda14R2~bC63I40q+2%>V9#ds&+@;|i12_nOp< zi$@TX(4%DrA7RR86sah68wy_Dz9D8CjN_Ts=);}c9Gz}QGi%P3%(+&&`zD-YXQnAy z&Ok1(ck^j3d}3ONSZgNHzC|MQa{oqJ6Wt8l z;nh-%I8`vZqI0r}CNMNvO#_fA^G|KjsXG=KQ2jkD&)@7dYE#hp@N!%lj@fy3FZmPk zmZ-)Yvlty@425#X9)w2p#ohhV77+#O;PRML5`mKJq5&W<+YV=R>)mz|;MkASTP&(V z2g<{255=pDni8iczgOGAm0#-`-(_h=j<|m(0%GR(Z{L#w%pewvfyGFx>lu-3J%&G& z)MrIEhj{X>7evszrm+bwrzU#ih`-ZHoDQ?DD1Gs%BGn&2D(^s4EuxW*)$0&jM-c!Z z*Axs}VSg2A7shi11dtG!taO$D8NfN#&EXe0A^eVn-wZ z2&AiaK$f>}sS$C^)jd$XcRyabR7Ydzt}Z7=AifzdHNM){O4B@q&DP495$Y9aV`9SX z>G9)tzocnC-NZN)Zj;YIAlRNF05LE3Z?LRVMvw!=9YkC;Oo3=}I4OtJ6L%0G|TtvxNZA zta1d{{892aA97TpsBF&fH1rw+nq@*bvaKcrIeGu0+E$ugsP_Jv!w(vtcNI|))uk=> z%jMmhH@e;UvY@f+kHrqVQX2}{_2EjA>MLC6^g1Y{PYv+ih9yD+o* z4zZ2EPW(R@GKQzKZ?{9dml2j;d^vwU!S)y&9!6x4sj9Mm`MzEJ%&d7CZUMd+M@=zS z_j;;9#{BK6D2GEn_!2(f-i8h(AC(z>v$Pd49|BP?0#v2<=MNuL^Msbrf6XEaOL&Cm z#j|be{hOy{)o1F0wX|W?(Wyly>9lnLAqa2`N)zG)<-Sv&)wH`uMfYB@%75K!Up|$l zeW}<_CZO`)%a_}OP?#EBj3)$9-9X$LaT7&Do)uWp!3IoyF3$iNKnX7oe*&yyr9R=1RywfD!Zf8RG1SuQJIF4$CAm7A#8 z@nkWLKFnhvZ77kD!n{MKW`=J>)F@q`1ja#COs$9%4yjdl0s)f+;T28TTviyAWP?AR z1MObWp0ggH)Hv&>y!L~}?ZnG!RPA}nYQ!1iznLblD_JpA@+cyI1nRf#=O2D~6UmbXa@}{(ceH{ux_y}@2ZIijhfYlLnz}Lc z`Gt0Tg-3L_u}I~@EGi$i=O^iRmj)4RomAPj@+ZpKZ_-6YD3(wl;Y}38)D{Av3+znb z0fsh;RflVn5OV9eZpzhKijo?YbW z>B)zHRq0SLMJ9lqayJo7p!M!vxcNLVk0k|FDK>`+O;s!wZN4Myn62}V6MBz1{0(2U zr=Op0POsAuNnay4iCLUOn#lG`&N_^dz&HV~GJ^#F>{!2AYtM#4k$r{TB? zL?BQZ;xDLu&RQXgInq>Y17dt2HY=V!9$0WzullN>%vltOPUq7V;xJ&w6Y6(_ zkC7b;s-CgOFGd7whw1fQe=HA;SBJ7pk_P23c{$~T6rP7H7etM6Wti1;Ss z(gq{5E!Xnn<&1_+KY#Tbx?a$txxN%*8K090&HwG(A3Fa2RF*_g)vgMn^?J{3F__d8 z5QRxc4#NP$b!R51Kzn0exmovr;8T=z=%ko7#25qZX|JiPkQL$mgqW1VMpUt6Ja|-R zIr(nO7RFU=z)Ar0S!BJt7g1^rW&FY>@-SQUjJdI2tr1h5>^}nDnq&<|AiXFAe+vUB ze5HY}v~TM3D+BO@kqEFT4KT55|tuO=)O&og~dvjKA?CkP)oj@I;?G?`Mb zVo)Z6bwFQvePE96{ykn*zGiyjyD^L$a4L$fx~Xnx?egwTYX*X2Mty$k4$q-Z6I+fi z=Ja%Aj8>wxn1pDOF}Pb=D1rv19`<^G;_r+xas(wh9uFZgZba_{~QSjP+B1 zM}y~lhcjCpAB{AzjG!isS937`2%8aLo5oxwi!h|Hn#!8T;U_V2B-Ypdgp-(JVw1s$ z28g(J`lxaqr2ZpZY!*Z5q7fVt??D_#1myPW~2(Ej`=&mX04 z%hITM1qamb#xjDmDiM_zR7idqyr_`l{#u%<9h?u7P$T(T$O}!+s!|3HW43bfwow`@ z=CTF-<*`cd>)l;j)}%*AjrMLq5Iemb(TEBA=NlcBo?f3{sR}1!FURW>nf)49g}2!X zO$aey7U7d7@%b?S7U&!bUih@zVK1tdJSN-mbzQkF5$LC30wS->d&GiNIVgGVn*|wa zC@^%v2rAYLlS8DH6%%Y9ZNaAobf;@Vy~zM)j>Yk?R}#*hh&)T*RKL2vU)BrSqUz^4 z+EVCQy-8K2OTXP@+hQgjMUN`oGjcTdkH!wTLoE^eycdK}xcMxGpkiqi>sp^gLmJKI zH&q7@-f6aS{T z=b;oG$Qr|~p0D;UeUrYm%Z1Drnlh-~c&c;Mi;hw7$sX)vcSqyVHLf?~zhW2$a!)8; zZXydNx~|LRa#bJscPl;_0h{-h0b;MHS6H^bUN5|^V=8RsKia}uM1xtSN0`|2qTQaK zz7Q@O&c+Xcj^F%6FZ~F`nvi__lgUCJsy#?SqD=Cd-{61C!i$ktTQAfaepO_#4dP^b!qJ!I7w4 z@BGw}B4BoD{iIwG^JW!&+HMV4Z3l%?FAJ>b)L@>cmYdlz@FZtVx~DA}G$%>)FHA`m z9dnp}r8}~z5a`P{ZH%+VQ3%7X)%&ucGqoW*cRiz!_;%s3DxjqX?wgxb!w8%Vq~vOv=n4RrJdj@0eNgeFEGZ0*V~8 zZigR)%#pgCtlJVksu|hiS@!D|d;4npZ>5+km4xIr9h6%K>Bpz%U;g4}(dn$SY7(n6 zdm45pS*d7^dASq{6)49&8v5svL&ZbUh+FI~MS1=zq@TbT3S;MIdQAB@b46LH(J5HF zHV*|M(3syd(2U005E&DyF$6{F9=u{2vt^Ws+Nu+D*DK=VrK^$gX*)EutE_}I<~d@N zY;)zN_EgjNg#q{HZ9y47C~gO)x>UPQVKJx} z+EU-j0WEwPl3JaLY6fX)RVBECP#UaUe}qZ>0@KX7ASFHuRtIjWyr zuOso)IIrYU;{pdb_&aqKS*FvcMG}&k2?)OBI;IH-qum`YjxJN=ZU;v#my4A^=^p?{ z(HU(x!am*yXiP$+x`39_cbsx?k1sC{>Ts2-aB^1KfaG>ygxacrlwo9lz+IkgZ zH|+D?z5NupP?7oiWd3e8KeYP-u;8UCsWS0G)IbY(;WuCMW!-87!{j75J7?p-H2Uve ze@bDYgfL3QbS;`6R*n?~&A0YEb*~Vx!xyAb0Sn`ffZHR~W17jU3>^f`85WqNf{2J( zyWA=9WI3W@wSGugpi%M6A3G{Pc^zBr@9Z^@oN6oVVpc~xmbNY82#(O<%;tx4w(-1* zxe{HL_08KaMr*hbPi|C*W&%Gkb8FnVK_IGJrUube+C)RGQwl;hPO&VCvaCO ziuoI(hIGag4BR=(CNhg3TG%jbLjhGSeAwj!a9vR`EU;=E`HP6ezto`|!%Fe{bFSl@ zz$jd&r!RPh)iNH)(%pSRcKR(zfD$pNfftts5Vd7__sw#5-2lu=RPT;T9nXu}7G20d zZy5c%n1#W63`PAsOy8$Ee)V_EHc4 zaKEnCH*XaHHGpDrqwAMy9*AV0abTgshk_3fe?)3ZZwGUA4M%jJ9tEjCAz4{~ooYsZ#igZ3>+nnm|*I zdzGk^lPF#^%}(E-GKPA> zPeP9TrGe0iZ-X<&bPN_JA^u|lblaqNthJ21&kf=#^3=TsK*Y3M5c;scibnuHUVZ|N| zFuocfAKNO1D{P1pjW;@gJQ|lqJ%&}Anm(H}r}fbEqM-lu#jz!)9z=3SOJ`u$&6#UD zXED-bhi+&7G`bENzJI3F>@@hfxN>{e?MCauY$r*0+z;b_le4Pj?+cxSvnw0)wfCuC zJDssxI{l(-%tV=x0lD2odhNr~^p7XCE5blXsUjj?FG9H|{JvN9qI*EIj?-BqR#Wr4 zeI1+pr6==qJtWB{F6s&*ERF8(E`)l(GaTb)Y2CZss;c_^x8E|b5D~x!>W(rq0R(-H z2E5kLKggul%2XUF`M6Sdk*>H~*5!HypuKCiEuxX&4&M=UM}&rh$Yb5~$huTenr^9~R11q(_XW8p55q}WuPQ5>m^Bx8j?%|F(oRO>j+C#Tz zy3=Z{v`syMdJVkJx+DSh>siW<*Z5+rdp%3Gn}*;}2Or_zbbCSsNz{-1^j(ZJIT3}? zo~X~pug{V3or^*HN7ZH@A$@ue5U2rc``MoQm^6OH)Q{R^<_R!O(?IKWX3s;?r@yZ@ z;=zdgAT;=0zFv~8gN;xoY~q8JZo|{K4oh%QX!ke))sNr(s-h8)KLljwNc(}ww9YRoQ3t@PK3ZDS$T;QVCDmIOVZ8ZP|Y_RVMQ9?*# z{(bm@fn46P&N06i4L%Kjbcg=c+)w>6+WFXI0bd{cK@!MBt!Zq>Ifft06e*mx?*UCu z(u@)DsZFTdPumQXFDuf&dZnNK3TeX(oO(+;Gm|z@;XOex36oN5Xib+jH`65)$(>dM zr1NmB#!FJ|VEAic+owL7Swcl2mZR61?i5xzXT+9Jg1<;<6Q-=lPEU+AKI z4zB-zq`wuHy9{U7srf!n^a8s2E!k;JXpP^#eXG$e%h4;-F#SaC4eqHc<%{26j-@uA$ z-7G}GK#>5{#Pp#p!_k>MC#PW_oCs7oDB)Qf??fcx@}+5}e>`UGG}&E=1bV?fOP`O1 zv=p`ji3|#-rHkpGUSA8d?`Wg5kz1O+DQ&}+=_s^zP7uZr;Ud^X zAZmPh;5Tn>64RTatWZG>rx% z{`0a(h*ucGmqvz8>l!i0vw9D|>38TlxVzK!L%o25kPSZ`z73joXj_&qzy8K%g@?wt z1;~JqTkFt1^1PIYe)^YxL8@h|`!l&45B5&j*>$XErTm!k*E9Qdj5I0-*aQeW_x+nM zzFaP=?e(w33ZL*U(p7|sZR`Z8^BF%A>jyM4)YKR}K)UBtzB?eN)s|*tt39xjDpRB% zYn4!5_=RZsx(o7ZT2Z(Gghst)7t(TggO{l`;!IT>U=yJW2TuglE)tyQGzAC3z^&fr* zualbZ4fr{n zZNtA_^L~~ZfNb%XiXy;}68*UWepYZqVF&<3b?deuAWcIJ z(eEt5=vNhnXFZNSBaNq3^M~8(4LUrt3TOpjOQ+9wjuA(86;bIBQL&AWls)Gc&2fQM zohmTK9R>>4od8}1I0DA8iut(~6F?JKSEqecbmpWz^i_Suu4b;;S=*Zj_}SNB5IaD& zWm%>Hl9C$O^`#Hhdrew@Sp4S*5ow`mmq2F`N){3kRG*$6pPoJzcI`E~27e-O8ZJA5 z2zPJas`00Yqurg=h%(!$yrWe+Good3$#2#5nIN|_0d)E*pX~jyf*qRNw;w_TZtLYT zf1+vcN0)sBiA+MEs?7ZK{A3eA5u)~@mHM@s=XICq(;vTnC1wI@n_H@#Yt3)g%fq`b z0DxFQN?efCU1kgNn5R$hqwz-E9FF*dlF-A{nhb*$3ou4p5wk=cNoWl3MKlRjLr8bZ zwdeO9WCu74V7q}tM9Wt6S>I(G(4tFHR+yadqVwqPWTaf;c5jD&QU78@pQb!>Gmc1w zT!Y5tVb*l=dyWu%SU^4TrKkzbVi+}v;UZP#%epQMf*p)^4)=Q6B3gf)Qc_u$%6PWr z$;%@~d2_B3l*Fm;?y2a#ZyQ%U)-ltJT#RGLYE(@rwgtX_^TrH7wsUF8qmwvSLtoqb zw|b>NI;tJmxB0%K@yK_Y_K9EP^EWl5HXfaw&n(_>x)C=oq9JvE?358#Z# zlMt<{p4VcC`d_aDu=8B@`(f1hkH5(I{0!=l8$fMQ1+@j*nH$umV+#l>st{EW^H2s5 zL{*@yP}?iaVxYsfav4lW*9?L?&ejPPVG)o(pmkE61I^2%!My(uT6yLf$4ZlMDAaY5Hd;*k#e_@+yJ7qh2&h& z2{&@=p$4X+gxJ3Lb@wAWj@tY3aL)@fljzW$%<>0cpYhQT^NgteQ}HC{ZBPc82xtI9 z`?i(UFXX9<7}coPj8Lj7%X;Z%-|85{(Fu6**Xi|w&HgVh0K4w(fk|5Ww@`FD#f6!d zraNT>au@(PED(8`%WqKie0yGiMu|qntptuuTDQzSQ2py%lP~(IYoDSZma6)(#;PCt zCPdeVH$F>-4G42x4aGr!1QGPu0$KqJ{e!f3KR2cv&Nchy4qs#9U~zge%)m6SaSn5& zZ=G?ajN0vdhTwSB9-0V?{K&U275Du^dF`AV&NG37aV4mfCJgE{LDdcgFB`p8ASixx zar#|u(p#C_g|IYPt|%zB)10Gch177CYx`3X^}&=`buvHBe3F&ql~m}qUBCIUGIv4K zNEjcd5#L`sdS1JzlC*ExGYlOzIBS^b?rrpP@dN^0m{sPnmU)}TjJZsm1gA<>v0g6L zasEX3>##xKgd+3xI>SE0e*E3K)IHGG2(O|aKH*6|!y!nunZ1O3XqqiPG|B7r%B{Kb zr6;C34!IpNU)(G*=ZUs$TdAxAm|7^^(c$!!btfk^|LcwG9C$MzPOx)mF6@M1qauMVvJ-67qlk`EZsvxf!9iHMkRu^z13^6X8emonm*Q#dF@NmbjrQfsLy z$5O|WG8OC!#Dr&&*xb&dJvl}|VG5OK zY<^^e0nE*iklNxSg@jxPV2V;bOvG4PB9tXBka=X@ZV2`mb{~)q)|-!5f;HEt2MYNT%X_UFR=w7{-Uy}JY3ex z!yPgCwlE?hIYFliDBLu_i~!kQss3EkTB52(;C^^pp$SDdOE@hL?8qFlwSt(zUK*2H za@=7`PveU@aGo~M%|?tf4#ftrykt#~BcIl#Pe)h>Wl;D=89{*!eK?+v)))vuS{(bZ zQIycUs~gB&ZuddU$uffsRPyqZC^xgJ39^U5UenRFgF>a>u-%>>pO~l-<(bh3R*kH~ zwjHHWU-|I!k_^jyEPzrr=JN$+Lq`PF?e_h*Kev5P%tBbP<&>ZBTqsV&a8u&1cwMM9 zJvd-E1NK5X!K9*g{*D~@RQ-R)u1`1k*>&nBm;i|CCb~hdcB)H>Lzh{B8a)G)orB#D7Ubi&b?ysLr zKMo)pB0g$ZiZB5&r$6kLLb|HBFmq8HszP+RlYSj-ibqsU?h%`Pl7 zm^*|k%gi(ym6IqpzdP_EI9jYw%w7(o5u-0>8l+*`2fI=1oVvj@?B8wxXjzx*rTfCL z(Q2RLxV@kYJOB4YiC)b%A$#}zoX-mw+emf0ef-rgr1$jcJ=Y8z&b`qL|9GZEZDDRr zM$;yK3{uYdL$Car9oeV@LYjVu4Z=%ROM|H89)&@v0HHz&T-cx<#h!z=*#k)Aitwzb z(ZXzleh&9=rgQIb7W5DG8|eBe-{-G!4Q|ZA5U&+M2|h4Rytek`ySKz-le}~3MB0vs z=_e1=cTEe-NenaHz!(WDCX(1@$5gMHlz8uUC?%sDIKLm2Qhg zwn&8fZNvpetrF^)h7zPDh+hD=#>*1sdQWqe<#U=}rS|Nb-&yegM8#%*Iw2f8Cl2|< zg9nlSJn^6mA576gqTmiVLZVh{!@{ejdu}=wAsM9`S4gPH$e%^o6UC5>Pdx zWx00{ApqD^pHxMqtqT!nWZ12usEPpKX@fWacLGuCA(tsbbd2Md4#~*HeXQ6{&ZDTo zIITHN^+_Gg`~ygbwT{t-s1)p_@r@j zbZkaP!{+CjCtgOYvUDwp)#vlfQW{q7Mc--k&@{Xv2lOly46qX2zyIRyx)$4U)V?Eg zi#rgt3UW`>UQg~$Unl$b$RB7u5+&m^iP27~q|v3Ny>Fe}08{*73C0bGiLwVUjnnM% zIzF0eF(`g1ijuCn`1ld^v|-vr%8so#EX93Oq^QRCL?)w#l|Z1T<<7Rq0*3 zIX0!b1E~nUf6nB<$Jl_m!ikVf+`I>-3AgD;1@;cBj6nZ=1ss!yDo{Ag3@%ezV#dd? z1MzS+!~Qz6x6hY>!qrX}_y8S{DC<^JmW>FKuJT5Y)Ex4sVcZTY{SeN2t;tkeRLSc$ zI!Do*UVDY*P6~dyu&F$&0QBbW(w`oMF5m@dNCp@hpMcI0_+YCKVpeCM zkHwVIKu6cApnXF0u*jO}&p^Rns?}y^?c8)Y^6&JDXrj7snx;(r;wkMErI{}(X%23P zMi41RI`W%=6SGx5Ug^A#*k&?`sx`4ph>|~b+K^|0QBKA>hxJPj$bAKSK>$Nol|)FD zzy8Pn=EKAFwn-A5qYrzAzI>fj^)-)2$G@*7^U_Df>F~EacV|MCY)`1+hP&I3mcSPEmGe z$Msa>6xeUMLzO0boFSdi?lMl2nn*c~67`ji8%J$S-xdMYv(tpsrFz6|N^Rpke8l1u zF(!pc6i-Ev5DD_>?W(HUxBg5^Ti(6DyT99I`+}H9J54iY3kxghXQKoom;LQjU=8df z-j9&|DHH1RzLEJaW893&$p8`fK}yju18f6jHU_Xa@i=mY>Rb`ZKgSx!;L)#l{SF#} z#Et;{)7L;{z1rT6pry4h-hXMhHVIxDkP+aWl@zL~{dwC&A~4|}p33VGn!SxWlKg`Z z|I^x0yvTD;z(kq}EoSgD;rix{9r+F7*yO?X|0fWO4}XYuaaOwYXCy3aWgy)=NDwX) z5ykXS-)?$*Q5BVDwA?9G`A7Fno=R!9$02RUh?2*?LEJU)$kI|A+K3sQB2*2kc=sn! zm`ZmkbX_T%CxZ<8dbor})xBHguDw;#=LqHJ$tH~@jKh~7o-L-rBE3P95jLVDK05Cx z@Y_U;DQ8t(X#Ld60Wtm-xA`6bvjxmQ~%luS9T14`tAt#%f|4 zYB+0*QT+yy7)CJqBQ2(kC>x@S) zrZfgx-o{q>5hA76b+G3b?KsrF+P zQ61?UI}gO0fdP7hJY24Czy2n6egG8OyMh2FW(8D~VqhTL(O3pCU@sl{vZ+o+O%wSB z#XcVh08~=5cMGd2d?1=0PSvgt$y+9L69A24$VWi1emJR3b~Fgl1E8|?cijVgW`F&A zjJr&p@BD3v$*E!*4W67B6sgOdnsWQPtV-GRv${z-mzxG-_P|G1qBuZe=YuL#y4-Fy zRw~aoBD!4GG9)^*s>0w4k^RY2*}eERt|Ev2v)a0;!6s7(#6d^6J*x^$1Idv`ba+RE zC~n4R0P}jiZpG=)Qn^W0UaI+v*JpPEem&m(tzNGgDSGX-PXFv)?0meOyD$#iF!gP3 z-n}(^_}DJT)3Pz?aGZnmW7 z;d+00cmrlM7TC7JNnQVg5C~)AUHo zlb<}iz3WmXZTe?-mAJ~N!BpId9@p!P z$tT6U{@|Q8I}@hgo>in!X~3R={)mYlvhsKk^Y#8=^Qq8oSa&FYa13_W=PLZ~uBU%q zvrmp{4DYLI%U_2FYVxy)DKm-LrDNcYKs<3byB%K8fIRGj%~M0Xbj7ZiBVI4N z6g#gjM5nye#PKu39!+fmIBgSQ0~B`b4izZL=~m8^A*pj!e0Jiny->UmvYeQeA~tl$ z9tlAkYePqWY7YOP5#qfclQyySNyDF0W|-m7n0RFn{KlzsQ}RF zkdIM|lsfn3rzvlKUG5+H7^6Rs`-w|m{W+>p(d4XZKfL7MlIzs}d--y_^KVs!s9mm? z>z!h@!!hiY|3JfyosG!$^u*8wzMsv3x;+j@w_OhjXE@vu{TsNd;iD?N;er|uNE9DM z0FX#qTI_ZKo7~s8F1^F{c(Cu|xL_bL8|fq2;BcZ9A~fnT`69XLOIZT=*s&uXe3Zrg zDodovpzy_O zeok8Ms9-f?m2^t#Y5ua3m;aVH&6L*xl--ZD(g~vI&JDg?ukXJIB%CvwWZs*()boT5 zI{kB==B1A+;fUd+U2kN1h&?}uzyNxma*Wm;;z|+Fj06>o6;PsXJ9dZ27G-}C(D(PH zdNWqMdezS2Z~rw3Vy)3x?b4e}M+*7Gb5ntK(0X^@E|;#DkNT+2&W!$Aa#wx+_z|kh z827BC&ZJczN3)3YioEWoyin>M~#%RsI0dzo4z0MbX?CAOo-wBJFLT=1$47wE`z z$ELkl(f(~@xR{Ggwpo819ht5ph;rKbKu|>)s~i%c!n4isdN~^xihRN|fwCG=(d4I>R zfZ0^Cmy#e9D4ci!DYrN8zgpG>!^P4`3li^Dd?~CRTwNfF$BetdJzR*=tLZhd=|26lzp+bx4YuwhYu><&V_M|uc}Nwm)Y;L zCIqq}5W;eI?LZejVf>-ogE-C>;FXkg#=773OJDu@`iHs~Fe(ycYStCcif4eiabwCh zbTG;2KSqxLf>>y3hW_LC-vPQ1!zT{ZA_vvFfBgB?{tYzj;76f@QX9fgZ$zrnwO<>( zefJK`;Kg@3ffAJ2j(lN1CO&*xigDNC=LZ~_{Q!m)!NEvOzV9@NF-CTYZH2z~r( z__lN-c@0d}QW6zsWG#yMYU?;Mv$uZGkxZm9TryaFmTk3wXCUNWihA25vJ@*f^*4rX zoo&0@i@Mdu_=-Y;u_HD=)@dFzP&>ZW9F)0WA|icR8!y%y9{kA8Pwfa+NvGYvh>u6j zPT~JEP+?Sc|2RFiQ!*B)JU?UG*eIadnnV7gD-c3gEg50GUOTAI7AiqOU#QfpwL0`Q zi|Ltb@w<9`z9~qLbN`hpo<+JUn3;WXOablCiok!-ocQeG3?e4__~AQ(od>DBLzkV4 zb5@D4OnW$Z$sO8XpVQK3o$i<%Ghq_}P<@hqZOgm&U#W<48)Lk_3c!KR{q*bMmJ=Nd=*1i{i1#EIhrQT)~VLdChw(-4u7XIX#ZSQiEv@M zlA>?H_OjEVw^WtzDVuPxs9mnMY*Im5R-#lhUB&REid56QP`8uMj#>T?-26Lv9poQp z56<4jP3oux<<{2TJUle!X^!q8i;Mz=A`C&eJv}vGCE@`>r*`<$%s089qoB_2%gcK3 za;~S3vinLI#0P<%M1*L$TtQ6DHaSg{N3|U}USbnB5%BLez`XIam{It-FL%)J@-YC; zeGdRY#TFu&U#8(L!w7ZBpAKjlSOpt8BC5XM$@u+|tNx1jp}Ww|0LZX1nt*?l+Z47U zn6F(_=+T)tIs}vJ3|Y#75+6#5=owI{L(Y9V$V+yFhc$3|QxjB3I;8jKr@P<(2`x?i z9LzD@X>aBZ94X}gI)jxb_T{Bi&X+V#lLwTHZbS^dJwAT?)wiP$2AW*|Ddri%AMY$D zZmSYU=&i2LC^mN{zBKb^qQ;9qe><;TB|gc7VAxA;LfHy<&X9VHEdt1KMT-5*2N0*n zW9j^aTp@z?-hjpDJ)efa&ue&aIR8e>^VA>R5fFR)R~L1Y-hqepvMlQet+)?p&^h`p zB2r+e!YS2~clz$m-mwRwnRA^`6&((19Oi^Ea2&Sk=QV})u^Xp&9zb;5b>^HoFR;wy zC9@Z_+}qK>Ja7#DA$F;8<0hLoPgh^1BQa&#Lfpr3+B z!=S1#x}6NOeEBt9*4|^o&|!D|>ohr_(&GE;lO9h=kxbruyDzy)$K#nI&pM5hK&q_W zR{GRVOh|W_(X;@G{vp?@pk)!FJl*KTFF(*%YxokT?I|CM^gCe!UcFpWYoi}rve$T) zTY*Ab<2Uc#y2F&QN|eUl!e+E_wVappI=%O zAyizUx{2IW?=RQIP6DWf>Zp8)<__hKr!~l*CMMF-1n-}qe6)+!XOuC9Z0QV^b9@p6 zhO8>dL zJSA|44l%a>n$1BCIPpxgb_@O?^8 z7y%qx|LM>FY7-GvBCxqVpU3Da&EcC*i0h=DpK+Zrh{CiKLf=dmSvz*5DB*mb!Yh*7!@pR>B2iD6naaIk|tiHOhsj|h)%GHQEN6iD$o?-IQ^ z9h*cu&`l^ksI$jGrRMWY%lX03_X+)JxRNd~I@qIGbJ{na*0(jca6e`Uo)=YU*kG+b zsgvf+B7jtU7es&zw}(eN@M#wgO`}qsq*`qy6_u_4+S2Z>8n`VQrB(2oGoFk=ZH>y6 zPlU)`yHeQb@H}dMNFB|3yKURkb0K|XW(J{15jnVc_z_0I06mP9CF0~2q4@(dg=5`vMTWBf+>97I zQ<)-TR%FD&p-?dMx-c_0uDd;zp7SYpXOTPzt{#(p>Uv4`jUyEfvO&@$46#-1x97|& z-iC|ft-1Fj5Ij@W>%*J2t{v`6>50vGk?g%c@JfYfBIM<~@G@imj$XR^_t*MrBh!%pJ`6#n84=O4>^TAS|Q=1>ei-QLh;+^th zdKoiC=@mb2}woEm5b5)J%Okq&XfMJPcdjQ1I&ew#| za(Cs%0`{2}LwD@|!QukDrL%?c*65%q@o(UwLxb4k{c~by1N3$yDCgjSZ;ov!Vm$yY znNEbN>gE1^SuYBREgPSMb|a9wFsW^AArS)la-S7pv(<&H4CxJlqdeo?5X_WfQ1y?q{Xz z?(u7z#q_KK#pUjfmX&x3M3Rg_sE8f%b+U>rM_TQw!Ll-E!WBj}=S+-0gE>1o$CMH+ z;|~N7+oJp27pRUvFboRyL}_$t;9{pLI3M^*HVt$jHJ!O$;p-kMLj@(Mq>OS41!1+z z=z@pz$$MU^V+uo7#(W#iaR%p-%YvK7q?|!S+JHhn~HUa1&^K`;OPe z*9&HcVQ?iYxNn!K^Zlxmb0Y8itfpTxDi6M364=onGe$Nd64{%GKTV+hQsvDVl||GI;+LNQrn| z+p_E@1&(Xy{wn~8O2Df0{_*(LjhtX|XXW#j)xp%Rw^Ei-<9U?7(VJ7)S!n}o_D5G5{eF|3G+cu@*pU+t0nq&0l) z+-||9-%89s^4t)pRE)6hNcWnL#!6IHpVxM%>==B=I+;SGX4nhYAYxTr-+i$u#O6Nc z)T`1FsqMXUhz_sQ3eL!Sy!-T3G;LSJbQ!MN>@$VP_Vl>(45HajRB4y1ic(`XUm*$8B}S3r|azrLcv- z%|QKeltJgu-!X;EDQs|TPwk%3bB^?$5h{|~fRo=?t zhj0Ybi>h?Rgoz^;bo%E|*)mERgEaa7OUG z4z7B$64gCpE8Y1+Pkv6GuiK5NSN0ahYPyorZvr^;+q48egfmP?wIns{GH~3Cw#NF1 z>X8(Cp!-#U)ySwKuV}sli+gs5M0dckZq;t{}jne(yi;8l@{*%ka z!qSknv`Bw`P93N&Oab0~ro;OrfO)yxbx_8LfO4bzuMZqT!T%qE=C6X_Z{ziey1Z-< zifbA_i2w*d%fjnYTq{pO3G~AnCvZl0T=DqbcPhP+EgL+!KF4wYB)Y$z;B~glb>EwU ztP=wb=+ZlsxwXrc+6skc@E;&1+w+t&s-Ff=L_8n^X4_lC!p_A6_)YRP73$b=s%Bz% zONRXf_^~i!2h|bPwsuVIn_8S=+Iq2&ZtWwZefejWydDbwvawG;I&9+<0uMlOYI3&? z+iLX$R>HulZ$LfpC@Kad@KkV7cVe`z`?I0*>Nf9uQuX+ncaMgp!ZaWNjSmCdRJLao z*+gz#Rm;wK(HqN-+|e)5d_7k_psWx$zx&+Fd2y;yI;V;fMdfx=^|9GH4iGmd_8P8R z@q1aem?Vm_a63Dnb2dM?TAsnnIPt@B`&@VPZ{+&53)dI!{dcIHwE_d8(t&olwq=pA zlr$lKAk~~(xSCKRx_x|Xz?Fyz=^j&fmxpinWWCVlvlm#h#b@$IPkz2c$?!l_pQU%; zn>TNl>lMtUFrP;rLnULd%Latw5g|?ktP!D8=P2#R%aqSv8^_i)>Ep+9B92Z>prTua z;%^1X!=%S}Kv)YKF;1Yag;S;GXz344v#-X$lNy@z3X}uP!U-;^(q0VtRbO7^pkHEh zSx~WjGXXSZ8zyOr1EsU$rie12Hnd?-6`#XLh~#!t1-HiD^%_<8G=uc4Re7+7|Ca>* z%%RCwvwOYEare70sM-c+;>vVcmh!BAU%Z-0LW19BCW+eJo#f6)MHY9Lp4`pJstElW zCgO))1<>jfnt#6X<)^Qd_!l447cbwv^kC>WhU_W`5I~5o4-aiwG`0#RDz(e%j`6zU zbpZe#zyE%LUYV&(i7#%#sjF~w?@lW5LzpxGK7dh~mQSn=iiP)`%An zS2spun6Uc(p11WCEG{pBESjYia z!y&a^PjLteW=wUlf&W3zu=Jp*6%p(QRBa)cY^q{Q0l*~1Hk@!P$e#&#-P1W})j6ua z_;JDPS5iB3nP_2Jn1;0OYLM|l&z%ZUp(;e{-Gdo`5q1nW7GFUtZyxG=C_Sh_JiXdcOQ z4Ep%}cPr7Fld8k*tS-e&hH+}VKg`R0T_{eUo|(O~X08rP%AvZag08x7d$(M^{`zZX z(6Rz7bz23R{z+?6rcjNUX#vHK8Z$98=0apFD8^fg28B5;V1?ZNVYs9RV4O}IAp4|L z!aM+qtTYJJa6iUq@hQi5>j2I#oxa((P_+X&a1^Y`%0w+D%`e_x#8QA9qtq;Tbj#){ z%q?_Q*C0Qz8#%@j<0RvY2>pXdIl75p5~3ae=ri08Weg3WBZpqNvU|Swl3s=#a*~OV z%e)EXgF-nrZsL&s5&71ueUqo}zlR9#iHv-QCI?BGGGd*}^$yG`_R}SI2NL-Pd*L}{ z&XnYYfBHF7{_7as!6L>_ElHAa43!-}Ly~4zoLnYY!_#$X8}?kIh2w57Lgm+~Y&m(8 zFIGPGYTZ%gx|ork^S`Mc*0d1O=L(Wx&tqt+jT!N?Y}LBluS6mb*f=`;lN_1uR=7 zi=o6$(E(~oWyByYpZXbl)eaS#6<9yjyL2WTT8IMqsJ84F4*&wk3RC=+hRH_!52LP= z>`We)o(}fxKMKK4w%25ZK-D;;g{J48MWT2#8O`4g(?|6t-FMBt*vK!Ejr;7f?ZQ(v zNzDJ<*$WzZcn)o$^~%>PHI8y>sWq0qha0b25sdlexz|pZlGEWy6|H1qiDlAwcsa0ST-y+>k`$05T&I3Zu4NAe7 zZ69&FJwG+rfxuutIc4euP$6k2lBZ8zs_ogY&vu)#-W5vHlUD_(F+=r9`li}%Hv*9o zksT$%P+y80!+7R6mMx%%CY1KepQDOVcm_g82xekH*cgObg@|dI3c^_HL^#>o6my$T z=r<9CL(qS=8M~%AH2^w^bAFWh#?M{-ZTo*xxl zpUovwDX^0t0+_H8HQ88Yj1N*$Fh~gpf@)5Y5igfJGXNGRE@aRY|B6Z~W=(s!nv*!3 zavOfNIx>|rXTzQD%wwmg_QaUhb}U^7y1%FR>jUYaJXN@a%#Su1ywJWp6?t0&=~oYR*qdiePO`wO0rL z)9T|fB;X%Sa$TNhrUA7R-v+;EaQ2K-`75!z)bjvgweUG7DoV5)ehsSUoaV1AWAPE+ zY0HU7qfym7zhbUvgpO!1YZ$81T^K}r8Xw*XD?GSf`jiR=FYDWPq9eYkA-BUA97luq ze6ApJWG*IFoxt2lgY39F3s6o}*7ZuQhQ4BS@--Xk@aa0uJlGDhElHVT2BJ+ z=j{>nrb;)6O8rR5kk^prE?KiVQAe8UtHtawt!E;ckx^G@0L%P=zW4rI#gkx43 zSmIz8l_U{W(rxSAw#Vg}g$nP-9aXu?_P`Z)QcBb8y*s5zKR1R%y)#!wYdHb0Segg(^{uhdMweQNCxpg1zdGe(e616*P zO9H_c=aK630a4d(M8*+wIvy6=4jsII66u z4%?2hGc3^~@9Nv+*2@NO;!}0@5`(iKg#w$3f|j%#nAs$5go2Vx(paW3wmJ?IP;)g zn-rpSUVmGNr&&OxQ zl(?VsR;bo|7w;Th= z4_vx2-)94P6bJwj(VH*7l-6Vrki{fNqgz}nnNpB{=E`bmsUaph{wX<*H8KZ&PDxIb z#9~w8m06hpG^2OUWoGLRb+eGna!8?Sz6n{^az9l$AQmwq^@l zXST6;_8l?DGYE*7Au2xn%fluR6EPU9+*$)EGfVFSkle=j2}v?6 zVfA)Qd5W&5au-Ml{_LIB5eQPPn2-|TY{|EK6GMXt6J+mv5eNd_6wkLCH=^Yt&*}^L zH3$~9x!rYC3I4r%c_xIh=rOv*6M&txS^fyIN;~5}2QuZ!C%SY;lQp&jK6x6COa)0b4r%rfpLbvRr^I5cA^D0OGpV5lAXSb7_pcp2hKvHUnY z89_ir)$lpeN{oyWnX2WrHk56<|K{t<|lJF^Gfo*$aAGih@o*m zir#C;3*}eQ2LVhZM6|AekMyRq@Ab7Ipz!>&afzul+x$&${YHpgfKsZeo0_6g>M_$b z*j>1xM=q5c&6`A;GhXgnx0FcvUH}jnbQ4*aKRkXfA0J8Fm33EAk>Eb+G%y~+tibyL z1_eVavoA93x*Txqz2Q&^^A1b8(Q;GQp)%7RPcuppy~OcJ zbj@f~CTknpKdkTG^RkHS!F6~IwSwa|CWj78_ds^>blsrtkU~> zjhfN|wYd2WSGa>t{PTaO?90C5!F_?Hng9(Q&q~jB*LVP?Q9e9?lE%g5W+CP3T%Amz? ze4IMvm1oo~A2dYSru__a9KriC!7D>WQ<}N5Ro{BBEU`_A%Hwqt=b^1#aW_Bm#3PJZ@FFX+q&pn_o5On+u_2%v`d=xJ|+vuA2Pqg`#IBc zbLe#y{2+jsxbecYGC{^s(-C@!+DvXAjVA=Cigbl8>$+Z6_?a-8W+|GsbYa}_5Aw3d z17IdLAB4pm-eC5M=60(6y@~)+K&-#UemmUB${X+g`S~9NTpF)LUFFfY`yNL@xf?f{ z=7jsT5l;+nbUE_lH0r_okvjgX(;ZLj{f-!o50=(0m&@h;fry#&Xw~%h%?8*EpqLFx zervX2d)jW>*2X~}l@BMaG;<A01b=2p};k33ZsDhuIf z&mX&Nsw!KDC@}-EoX726Y?+B*)REb$F>RU(HUw{8Gwwx1Vy#xut;dyWN!{Zw-Tay$OwQ5%28EV@z!$B^Cp6f?{@m2*Hcl@yvI z4TkgfJjQkMK`>wZSOlnxL`vL5eSY96vNMr8mn0tnj{zo-Wa$RmMC<>bu|HXsELpY# zv88HzyPp%08F`!61BEKos3;tfa}GG6aL#|?-|#mmLJmox2n&z`0Tl4MdvA8po#Vbu zl>=>U`-%JD85!Ze8*9{5mo}zmb{X4lmXfyYrG;wLt9$#?;K`q)&QzT?gy3XL9!vCU z#hYP_7XEUIIrddVh@8U*;dptufBke7AsV1+Z%Z!gpb!5U5q^k(=?+qYhzdJ1ERA=o zUkQ}Q4LT{|EiOc8{h>^zYU(2hMrqQkqUEh>cSe0KK>c~AgCabDwi-YoGR@%KeF{XA zc~%ysXv8Uc7zO3f{y8wn*myqJYKaLA$F?L3X?TaFeqq$?d6bZA(ED|)!9>9FS?j7eQI0un*1;1N;EHy!}4zrlt%|#4+Rg{ zl@vs3^nrCrWin(ZynBAewj0E#NLt#yFbfJX+8bjyGzT6A+xt9-^Iin>v(4Qi40<%Z zeNrkh^vG^5A4gHgS1MHP$fD9hzSM?>nVt{6(*R&_>cTdG3ALsT~=W|{%0 zZ_q1;>#;RdF+^X>RB*W<9c7yc76I|ys>0kkffF(ajxe%;m4K1&%|wLx`tthmm-i}! z0mWihn5J|;H~-jWVw2tJALT!TUt9V zCb1o4WWiQOed?Q#n#XERseGCLwr^VsRoEr04jLMZbKN7~2XzTS65AoLyW# zA)K_rI#Lr;5PHq@&n*DnqEc!AdV^x8UuUHoRR@=H`n10^=@+GSgblF9VxrZzZb9MvS&&na6pquy z0Bj4I^ml_!@x%Yuj3ou+n0hR>TVTpnwQJAE0E=;Y1id4~ij3{bn8N%a{rqow!qPx0v zKl>2xN4FziB=Ipqyv!st4|)9zl@#G7nMwM7k*EdFiJ7;Jww(rRaAq@K39X?EifDmH ztle37w=Dcdjjjr^S8WjzWqD0=MpJ`8rD9YG5QP97>SO{UdZ;RtgR{*R3BQ2i?>=(K zVMe4RCAD+n6domr=z{tSUBNye-NMxQv)ewwoyG42Y|NL-PR;2yG07?4?gW@7fcgvp z#iF&`0PfsNhYwI6)AgAeXe@N=4|G6WZK#}9T{{03$?JBbvyC%>phO2i#5;kjms94} zH$E=Ni{WK}>&&y&_2nf!570y3R3`vpM#(ZIutrfGU7H&5?olfJo7CUZe+el5}i$j(J9t^)h(e91EWO;DD~ z;^{JxB&#F-;pyqa55F6iU5Hdg;~1kQ9WnxbN%Bxc8k(?$o&28%Y;0_}3|R{LTMA9_ z!0KI+X?bnofQF%e2KwtoO=xMOVl7bJ7N~kt-y+mT*;LU%D^e@-qQ9*j{WDy@pg_Ql zyN?>Vvy{;n@AG4`vap^>wf0+ECHYiP@u9Z_h`5~8JvCQxY?~D_2I-;5*`zAdzh&-k z&?e(ct)e%+o?*^ybSKDhKfe5`^U$GY@o5dj^0BCyI`t^m z&k{3j8#Bv{IG|FF;aX5gZt3!cR*2eL*0K&+5?pQ`TNx;72*X%ujURX@)oI_7tu_)@qxAu&t;ZC_-VAm6B4gQ`5Z5_$3qBsdN%& zUFNKIyl$r08k;u844#b~ycL|ar_jEI0tGCZDsIto4^(QZPEVKZs&5+-BqLQ^o}VTl zQ+I-_oUBW!K1yn^Y)*ufcdBn-N zgf`-xtI8^C+?&s|Cf77c|4;(#ihZg0lDvzQ#uQJe+Kw&3U*u$A=Hq^UmAPTNfBmfY zgZ510(Rwiz8j*+t88rhpp6fu?@t)4>)0$>NFku)tO=tAtHEe-`qehna87+-|ejEB* zL#iBaq{bW##ZM4`(g}l0_st<|z%?(`YCX{WJU@psTu3UUm!{r`yL}L(9T6cq=UVTwxV@8?b9lg8nd-``mc3Nd}L`Aq_(g=bX-tB21?z3 zRuM~mu^2IRHVCB_votO?TvL8a1SsEtJ7&w!Ca%SSyTuo#T;67e<%`lHpKuJ!>=pv4 z%F9D6|s!B{_-y?1MZSE&0Ur}7!aMo*ywbb?9!{?#nDSfq1 z01`)l)Fv60>Smm(9Fvu{G2UIaXIl#dOp%?39cSPf<6RzlbAgW7y=fI9F(qKrMS)jT z?TB4)n`sz#w1vUIHSwS+`rYR;zu{1w3g)4rY0tXqYyN_<05LNUkqPo$|F>u9<0ErNW!-rL7Jk4!qY>Jq1%5EE8AGL?c@o#;)mA*_l^p35cA}O6hxN3+;2##s6 z*m$R`g*OWZJZD&gg-r0*^8gB+tS0dW_{X=3jkJ$UnMf+Rll34QF-xO*reR>SbBm^? z0s`>U8ll)g$Gi20X>G52z@gZz^iR^iL?~DkO>28ON@h$kjSXx&B*S;tvd5BtGtV>L zaPuqe+^oK0o+mAxf|p)KVs9Jz;;jY#8_#IJkrjwTug=#OIqnuGfbxQ%x$#N-4i)0# zBcg+dw_*Fu=+YF8^cinXTF0dcLl4B@x)ns>*ebMJ87PI(*(PHGU3_;AHPDOADSZ$eYO`Ub9BON1`lDlq&40)#w8WrhT8VHbf zMbQ{i7g3_j+XEsV(t*A-w{wPfK3b3}q5+G~)ukXBgp=toRfTSBDa)H@m00f)`#J#H z&I4v@iK?2+sTE439Kf|HZ+rra-}Hf&FMV{fV{jsb22B-Ip7R?(ztrq+d@D1i=x-sQ zQ#%8@JT9MwD*66p71ju0P7|BbZRmj8dRL563|$u~)j(8KT~`$aiH~GQe+@;H_^*u& zA+&m;2!+d+;M}2ARUyC>5n(#!1Q8EbnTRP$dzQOfs|wp{Uo$4DWP1#xE8{i?(V6Z+ zuO%weWDb=ao8poKQ2knZ82_l67q2S!uV0VXmt94bi&Uh5(G$Iq#}hQh^~M6cA{H z(w1In@$uVY5f)LTRbCO^%jJEm-#vMM-o z=}e2C(9<=Vbs9|g<;OpWh;fR>iU(0o6A(c9L}?73x@+0{Q>YvReCmEe1)5jeLT!yK z6~T0l@}{qBnU;n>f2b(%-Sa!%cI6GkL~JhbDFOu>bM?Iq!_JOwcs8&qh~F|^(WqKw z*1y@in(KV5%dWsYEC+HM_LsWDl++_0}r3|a#t1Q4$T>nwP^kdJ^+ohf<&J+EKB-oAXiifBX^ z&hc88(hR4j=?9sDjlrXgObXj7m?~VS!mZXoyL$qoCQA=GQEO-l5ei2tE{rfV(FQ0? z`DPSEJCt}R=l~RSs2-|&4E%~*8rd|)6WLzp$kgRjT{(ATf&U!7w4S{l_tF-g%yPpc zp&(R3p{NRQs7|q6KC->h;YBiXtS#IG0Hpf$^GAY+uanRSg;BcS2|&&gstO3m=E@MK zt+*TnHxxpg2lnmDCdirF6;P=&{7K2t(ayGkYLpGbHg@KLZCB#5$AZa-d|#lEiA zf7&H# zcXK7c$=j(a%A(3jcJ3+>6OHris5d{H(D08G05M2vSQAk+vO!A1SG-O#R9RSPnRF~= zOD@xHkP<*(BJqv&Rzx zN;c4j(RD_0bjEZ}quIoxV4d=YJ`zne6oV7i2h)#q0|*owWge=-4ovnAK;l5eqB#MU zELqJ5rKRp9KsBCv%Nrb~hF-nRyycu~59Q|6RyebtU}JWB@vrFs5RusNMJ8gQ3B!$Y ziD>8#hMg9m0A6pe`5T*Q^Vi%U0QF~bplRt*zEPN%wI`|G%KLJz^B7(Da3$ab zhRI@1rhq`qJf}=mW_tJZJocTpJqjZ739r!BN+PzjpF=5xktd~tIQi97dpd zzt7uOB2S!%T9-izB^V?pxNWEqRVA7Z_%_8EeQa$jP;drUjms|8;n8#QV*}1*2z~CT zSymY)YsXG@XR_PY>e)Os&F8I(X$(fhNJ%k^-C{&d!x52UsvtI`ST)w8GD=gZ z+eE>}Ij?85vyh~To$1mX*}xbZQ;sBd7V&Nq?PDV%mA<-CotNFt&5u(iw}=*%FJHe} zd@n~$ph`Xo)+Q&Mh7+nv(!?&0AZY-m)HHlSNicL~3ZM|v=^O+pejtigxkjY{(_kiX zYJ(yohp2)E59?p)u|;MPu3HR<2-&t6LF!=4nA!5-eFJ~kFv2|%spld`dW5UImP)}VUoT@o# z8Ms;rJyjhFDLxgj#Hvq4ha78>wOc7!MFklE4Kqr`;@;cSbtktNK z%xnib!SVNFz@%<=U=A?HWz`waBYN9z_v^_ z{4})(-)>j*2!}t+pwvAY;JL&xRO8R;1LWN^pBqWTj_+d?4?G5CTYGXa4DsWC0Ddfr zDnUi3s7~lz^$tkRY82!*`~*p`qagTP^PuP!sfk9$WR7AbueRwms+2;_IJZyOkfnb_ z&xj~e5Xe{dnV?k&7tWqJH{sA9pso-*0@*cyHC9yb$6Zxzbua=jBRXOcc!zCqzD``V zl{zFKQTXz*7@Lsn8n$jKWS~lmf7&o>EeTv45X)y&%_dn}%pthXtw!Nl+C!Ds9wpK39jhkUo*Y=oEZ|a6+wm`## zTA~v$yf0%vE!brgL*O*Xt1EGk&<`+FM0D)?1g2WH0hu8dhDz&J3^av=^e%_>K;G5N z!0ZzMy@gZ+YF(s|#zdX+7g_*jnfL3Z&iiry`en|^+rp(3aFY&~st_k88v3h#g!ea=_a zooE|8;c*Xk-zd+22xb46vT=-rHZZ?x1L6LE=e-Pr`hL!1HH8ijrabZ?$viT#TOnt*;F*`==u_w#94E-ppiI_3%V4 z{XJ<0t#ORWqUH_jo6V&vKs(BJH`-lKt3tJ+=Gj1xQUrlcUctBe+~iBEQd4D49`eaf5!)+R{a_XdR7AzMfUQQROVr`sOKXa-A!gHXZv-_% zq)rlJOc-y}=I%jx)r|6!bEG|AXhcw8Dl?63{N~;Bl|EgGwr$f`gB*btod)?t$RjD5 zcxr3iIy2XQrJ>ZX`Rh(DiV!CSUy!ppRb5*DWT-JX(Ml3kwygeaWQwyZ05J~$5n0cJ zI_8v@*PHRgAlk`te^lax6b*~8biBK{>G8xi43Ue+kNgcPS6~s@2f!*9JBHmxyuiU5 z&>tB@fDVEZn8)SmJ!tsY7dJV@J&!DiZt4sGjYt_Xl>$XHK`KY zx$wtNfBfU$&NO{m12=;&%MBa|Nn z*tWPuCiKN(Jqrd>2hqqrYGazPPwvU&q0DN*bY{wDtQBMDzx@4&-~QJhZ=x!)4b7c! zywyY?lFEs6`k#D}07mL<>BvXV49aiW6cx#2p##{}d zX!!@_OZp&%=n%*@oGCFTQexlIj%+>4V7?#sDe`5`-(RlR=XbpAp2}Cs_SBdhEQ-@G z$<_yyk6*#896n859hA;~#nflIW;Glh_3XXY<$*UpeS@oSedzIl6Sh?A*Uy&sg4w7) z-qj|65H3kAO^B7F%dj@Zq=5zX&>N$n32PQ;bEHQfI`WV& zmAlU|_ikkn5jXROpdco7>rjN}{OM1BJdWdHZ7R>IjhlJHBCTHX(p0zM$4U*07z}e= zLTj~1V`=ijk`}0H*u9ueg_h2cru`FBQiFE94uMkNt+kq7?XhQh3?@~PFP~rk;dVTe zd{6vtyLb|%YQ<|>!-&$5Q1}ws_)~EYYW9w+twni7fz|79Pc(kkYSF{J9zNeRD_7We0lwPd;M*!tQ|yyFdX;Z z&PO!Kwxj@PDmR6XQ}a`zhXJINWLKQSL)yTOU0cf7uuAzUdHcJL_jHx68&fcH=N;sy4l%J|gd?-%*mp~m4Jo5nL zE+CGiOL-@FF-NT%Xa3oi$0^WP#H-Y%^qjCAfc*p zMrpAkWyb@21^(>AutwsBKS)hJ6| ziaIK+Y$aK!fxt|Im{@EXB{9)Mc5E@gI<^!7MI0RtMsfs=;jI8+Yo%tmtB)|^X;O+~ z5v_Y7sydlShikSes#E^(%a@--h?%E|;yy(rvY1ujnT~F@i7Yk_Eoq>sE^7KK|K>#e z3=!r)Rl76oA(B%(gr%v+2_5%L@pwpLALDoL-hcn$hkfjXZoGpdj47&^Q;$O-L^Oz@ zBnk$7{PCAxUcUYn(fe)eOzr^$`d4~So03&^+ge&r*XW3b{_1+VDgEqY7?C;05ES+< zHe=4|cS@0kLR9bbxX>+Npy&YDTP~ zeNJX4o$ma3^zV3IM0L^>^gWW?ku#)&s3c^Oqj+oU<=4POu%o>~fKP)tzN9fup%Sf@ zl*h2*mk4IFjuAvgf*GBVy6cMmWPc*^QDVTd()1k=m>eYO6i!|ws@6Hqeig)Pe7?ETdi4DF4!+-~Vz@Lae`KC#dcIl>dG$zkDsM}HDzs(g>L)|r{?RJ?p!-2x+m&O`N@p_3#TkHMfpogP~h zZ%Vnj|33W$L@FVHB2d40YB3Bo{65v`6jm@Xv9QDCJVd7KgH`me#u%V4Q~vq&_38R_ zsNuC9B1E=b3{fmE(uQRe^0&C1fnbShHbMy%989Mfi>e)qT;MeLu~VKPW^Ya+bOc!QV zkhnh0wQni95~;LZvxlJ_rgR7vLERcr^bt>SRWCe|IicD1?aLe?d)GOI-Fc^}YHL>J z6cPC}?_Z%$*C*nk(^HcW+su5qTB-CqUDhIX?Lmf@EUQn}w$pP4qJM%ocPb{-!8@y2lF^H-+=!5!94nxG-R59b=W zaXv`wj90!bVu8LIoZ!|WhzM*uLc1h0fmEl=_e47p6t5zH-UT-~n1-k@>rgN=Ak$Bh z_ldDV+BTjrP*FF%qb*%SBtg8H89b(Jaj-{xVV|`K?Lcg28b>IIiIl+`#>RZR&wu{( z={^rI#!kaTS1kHyQA;76a@fNBlf zzX@q^Zv*RwIbp=uQ?%NR{U9L9P@9|DX@>cMJ;m}`P(&bAnKHl19NYHcyWcQxHi@qy z$)~AAnq>k2DoTtvA5Aq~tj1fEjN1=DOdo%d=2o&U*C`m_&wX_HngQCkY{%rcKsHTk z$IT{1kCki>*Yx~I_9L7oZx%U|QSW(gr!6V7jt^aP0r=l2aCeDlC0l9TsrH z1Xs2J2eI(U@3<7EWCh$)vL$sWf-4z!0xBv&1ev0f=KYxWqvlFu4M^&Elkxk@*D>D{ z_MFIOVo?$rRu6M3ELD(Fi$wL$f+ENUB5bbwEW?E`FpNB1IW*R81)k~*yJRg~1ggRy zW**FgXa_Mc0f3nBSw(e{>INd3DwEBL0tu7=R_w%sFccuLTLOO9RdYACdY7oFc+I5{X`6AR#KWGBJ%Qj`}xP8{zj&Vo$EruRZ1$PI*|PKmYo-z(xeZT~rmkF7ypybKGysfHhF6fdJ(OubL?g*=4GwQZBjiR+1|s z{XP|M>y;^VXXyY9tYh#^j=M|&E<7&RXWsU0-*~XxrE}K8C>pyS74y$2S13?ALm!KC5TO-aI+&;)wns&%^RaqM>*0K0u~CvE!T_De<`L$lg0@{8JIM!UHSS&#!At zLIB94$!;jJ3b|D!Q)McN9C6?0R@LvwmkJx%^;rae(j1^@R-swiTlFa+Q`ejeFC`@VdMz6UVpV{d znJ4I`dRN@1Op6tf-d&SRCeYgYKdpXfMrz>TH$=sE8bwvblI|p z+LSsmk(u!#dQ2hE)8N1S?)?uRzW2o%17Y}^4iJ%f+-}GHCJdzk)s4sNt0*bxc=`0} z(Bqlu!bHHKs%p&NfK79`MvpeS2}Hiz=R=xj{Im3?6C~L-n4<>23?>SGag94bLqS9W zV(7FufZ$M>pzD6!uFt$(#(n`fElGsTQHBEy{Vhr)3V>KGt05&95l*CF>~8nNZGTRw zMS8m_^YjMQ-uUR_9a`@$?}mYs^gS=4QqMkF3(ZIj0MHf+ZLK~9G|A?i*?Ogt!OgM) z3~5rUhZ1T5>p{5)bhPD>RT3d={~iqp==O}0l~;mvZnhGNo``85le#i)n~E%< ziE;@_sswBdna4l>@lQYf^vB=Hzg6uCCrTtReLXg)6E3xkk3QMP&|`{B z<^6Jb1qAfOJeh6EcPE~)Em|eYDu`mKQE;IET&$Lug-Dl-T?GQ*G65zYj7B=3#H0v$ zOV_@-r4qXqAtYRPh8*)BfBos}%co=Nl_yF>N z|M0J_Py7AL6xCr0QuBZ(-vZbH&;n7~1S-{BX=$v(MXgWyU-D`n91U;Qr-dnV1Y#T& z8`0312L|B#)bo^iSN*E`zy0AapWc5@+eMV*n9f^i|M_7#RP1P`g9@={)P-Hh0cSoy zVVjuzX(F#-R`+2$k|t^1z_ZT4-~J>Lzm5K3R3#bRlFNj9O`V*gDCPK zD`Jv4&|fq8!%vhe@Yg=XF@%}qPO*iDW=zyU-8D)Y_qr4Sf2VO_+K8*ePzoa+k2ef@ z)q&K>QRC|K{wmN(5UBFyqC_INO}&&pv)Bo_Yuv&<*#rqw!q(k4%wFl9V|gMv(fNAX zCU0ONC=fVs-%ZHDG#C>YDlfN}PoF>f1aK4@(H12rlj`xR_ZNZ;CMIQIW4Z&X*!KPF zeaa?|N!isL;r3O6;HtqrHs+!brHw4Ho!S1k*83)ul12!H+#xS@nyCdy8l$9$_#O~( z)B&l|1gY|-{QUGJM2BE!{;GNt6~#vUZj76Ft~1dH!vqQ)c9?o0TK3!^&qFjEF@iMa zvrU9h;;E%+8O@{~JMO3(SMJZKfiQyx)26sX2k9hz_x%rJA3SV=BSZA=0Nh_+R0SBK zdd#`ev2Rx>2IT(v<9*JJh)9_U#7Yy^c-29qL^e+pD*%b?*c?OZ%%sV-dtlO&mVyPS zX|*NDxmd17Ac{%!pdT495ZHF>3n?=LWXhZ}Uqycv`Q2~+>JWYR{4B>Y?{mB{qJ9SliZLs}|7$zM&6ONPD-IPw}Z62B%m0CtnY4r}f^s0ZDBm&$-yv zZ2-9&fX0m#vYV%q|9San4|^G6sS5d&4|2Vh6{|kdXo=;70INm`D>Md{dj<>V&Y2q)bqPT)zKq5&@Wil5+)xGy@(&w4`Ek?dCO$(g5NV zEvM|_1fb|)UoOGNIw6?(uup>W;8?Bnv6G}|8(v2$ij|1m|K9#zs1{{$U-M>nR8 z`F58>yj4N`#+WJ_VW)Zj_{;Vee~EplOd=YL%RZi-FL#k+PQ@tpg2SGgsTzBgDHExJ zF)UTXMK!n4_lF}CwSa;D-_f6f0o^lCBd4NSnNX-cuTNsR{S-Uhh(qWg8lV>i0ay04 zep^?uvrqAcV*|H zL3H1)+x6L#hn$H*3#!U-=>2uOjInQbKxRfAX>9w4?|!pcP2@f*6N8>%4vcm+yLBnTxDCxL}o4v;6Lx&k-(d`fgUxyANBJda%04kgZ zgIpOBu?Bp3u)7@Ed_H`JR2u3b4P$u?UG7~--~-6j2!Kqr(8&2oNz?GdH3HN0?y~>> zw|_~*3Yv~1kU09$N*0*|^Vfg=uiyXGUtQk41N6{?AlsnpWeDLmMbsA<`+$RpNIcp@ zK3IS)*kspBHb>1NDx~6>?GQd~Vdv08!Db?Q*9 zPUDBMy&M9?P2{d}!60G+@1dQ8nTlnF|J|f5ipnWljKTZ9u^xQ9Ug$efBE{gLV<>=m!*L&k z!L*Mtn3x%(Onc{9GrfKO41QJ&g)Nr0f9Qb1?toIXPbvF;DRg{$xT(nC`lN=%;w&?7 zAM~nnsC<$+RcP@3{KNwh$_nRk1L}h5Dd*aZ!!!aKA&3wYI6EPCx|GI(Dv$EBND;(f)f>r6G+@(BsQcVGqgM^<%&-w@`h^BN{(Dj zAC}Vsv}KE)W@`Czk?0;+MNDie+%~$B`+ZIk%O%taQyb|)Ox62weE#zF)7P&*ef{bE zAOGR6{`J4vn2!6j9nv88*EwJ2aU8p8LS|MD$Pkb%qaoIqm``19*O7*ug4EI92Px+E zOR*DCE#RS=NOqO3)VM?aK%(jn01_KrRSqx^vi{%1V`?holj254D}-ql9S;Xktr3H~{U%mZCqu79G=_-Clx>Xt>FM@* zcadSYQ4lfhYB)8>LUkb&Ek*PP>-gJ)g@JuLf{vuR{L`OUfq9LUx7VAU@bK_j-8ozw zFFdGQ0CgTf-7r9P*rs)W?e9X}7}I!+!YauqOafH2O;YQscp*XYIeE9Bi5I_*W zAi!~a`uXP#ni&t$l;oTuQ{?%bJb(B3)8{|__{R^w{mr}QXA&JMB6Fi-io6{2b{D-c zfiQ^F7Z)YFF_GBL`zju<@buK!m)@C0#5AvwBG{^UEA0sn1a%?lPK#yVBJ^-^OxhGO z7z8F3#e@=XA1=>IpC@qmIBsxa%MH73yNb%3 zL+10w?`XbbV#3DEfJ&w@`>6vGIgpX{>xz+=BSg{}6PpcxYTX$&d-RxOWz3~2IHr95+UCcpAc2?37RShfi-bVz&yP;UznEi>i|9M!CbVH^R{Kb=qKvrzvcvRj-e5S$w6gj`rSUB>Ha)usK8Hm%2=CDmD?10 z{{Hzd|MvMl$9B1W{qjV@V;@jGZX4!Z=I8r-xgGlcYC95`>hn1cjpR*%LYXl#bI|=f zvGgD!VsTH#&|iEZpYxRAE>R$GqO+Uj8j?(ndaLmj6dFWi>o(Xd1RDG0yX(_VJXHnw zJm(Y7jYrTC6z*?hL1#-KfxSl#bS@2M1cfPypagx5;78k!MqQ#tK^x@UH3m7<<~0OW0v^Ln zh=_Yk@vBc|f<#35a{2i6ppsbB9e+xO2F7nDfmNFzNOZb>*n6oM#h?v2EGYD%87 z7^bFzNC(lz*ai*em|=vF_saACBgK;#Jj_A0{VOoWpnU^q-VdF?MhqHu$|ciIm@hAX z`p^G<*U2*M3_M#rEh?5fG?@AF^qb%P<-TnnzI*q)kDUaV10X%#KMhrV5&e1-0S_3Y zG7YeG#FpTL%(fqcm_7emrcUu$2LwVWjMglQLv!P07f)g^dtznK$QeO_wvUksi6;LV zlH^@x21t}xY)cOSG??Fy@#Aq!=%0@J`>`<{7apb!>(ni0cgMnvhc3qd`U%$o02pj3 z?Ungs>X`jd2iRRG$0Te-7equvrhjK!aEzJ7H&n(%<~mUt`Kfmc^KMj1DHo z2F19%fBylzeVLzMw*5L31ljbs5*;recjmj!SCPwRVf;$SkYEo2j9aEpm6+az{n<7;??L`1`4T7lj+I@hru0B_@d+^#hKVqE?o zfB&EUkJs<-U+?4T8S^lU!WF6mfa6t5fJq+{4is%9T`2_m$K+6p*Hp+OqF9{?a8?eF zypoSJ-(kH+>vM~O3iMtJL@4j-Ssmu&1G;r<`xLlv`@@MX@Lqw0S zU;fJxr44ep7zZfJ~&d^Ju$gC^EQn}QgP`S9KIWgBDg1Qe{cg9=0MA_67S z0Xbye=RDMQP2j;8n1`(%RGm7>r#(a=ps6@i?o)45?;=xdg4fN(jc72LMjRXy95u05 zrk3Lfe0c$dovI223vEPWyId_ycl-P@=W%6LU_w92@$=7R3P(4Q6I$Ff%a=LFZJ}(Hxv9IG~4Vxhb#5WWsmj z*&NsVJVcLIk%Py7_pkrW@$$;go6ae7s!X+AU5ylDBNgmV`$gXC;IVB4)w#=kKkAd1D6dZ^l>VqD7)RkNMw#=a& zospPB>n^9V0hU7GX+Ktv0;H@$G!CUN^Z5OC{pq;l(BuAvXtGY3$6$7%wg7Z&>}0O{ z`YCfZxyuy*0e-lI)^jZuo(@9*_MeCTHy>!VYvb)d^TuiouS%kf_ijh~?X6l}-K|Fg z*}V))KIjHlYmGM7gK%nPP$<--1vV`V%uz|{(}2TA^lv&aA40?w5y5OP1fi@bm#fx> z2i0?l`GBTJU>Z_%LUpRr@%hUW(;&PY^OyT%ef_X)&xaU)6Eg-e(>8eXTwyUWS3p#! zLX@_o-7MU`t>5a@?iC;=e)~2L=RvPZ@8<~I_VUuH1nIc1F6P>D_ zsP96}Am(woTo|gX2g$MF(5atZzP!4Iob&bdM)Qg>mF}vS!GmbC&0Y!M0Ujbgg`l341lNYf6Ev<)5gb5WZHTlB|TW@V3Om0dwJof=gaf^5AUDm>(@_r z`T67PhwJvDbKdp!^_YT}IdAv5lfK)>7=!ms$9S3g<@LB7a!fs@#z7*az*O8s?<$Ar zA?>tE00bu5mU#1Z8)3z(oAiUgX0KM=~@J*G8FT_uSub{7TTx5>RK6HG($w%J4`|V%fUk|<9 zbl!D}xjZBif0^k{y6u?4P?BRD$8q}tOvTgR{Qmbp{n!8X^Or9#Q>Nm84jiH+po@;3 z=y}kEaOG_>9l%ZXuv`g|yUZ7nuOia~iiuH*e`=s zNT(m<959o?9A8I}ND(;U)>p*b+iY}#nVzEiONQ$S{o@_{H+&K(QPz4nmU($CD}oiJ zgueBu0v-8O5nDlor%O6L`w78r6W6|M3kir}=k( zCey!Tml7Le0{T#aRMh87J<3Hz3M15<#Kgomk>j}k_y3#!em`#Gr zZ~JB2FZ;f+h`inpjTJ-y%{J%6Oxrdt`@W6X?boD5{jgRZyzSe5*)P|9zihsESmr$D zF)dI;yfF_R3{p96w|PHQWxs6K=gW58_opkyIPQ9Xz02$C>rcP@{L7b@yYAQh0KJI5 ziaw9=4?q3%>woxn*MI$Q^(MlbFk_>!Z`j5ma({UdIWPbc;9xsTQs({sx-)Gi`8quwM==p7M9gDs zV;kd2Y?Bgf5V?(g-}ikRo7>q2_;xO_SeVE(x;YJT!$@U{bBytHeY#$*+rA%D=Kc2i z^~>{@m%sS!?-=6~{^8Rvzf2L|VvI=$FM7NxcH$f8lgvT$E77jBO_z_m>dnH?HThK& zO9mVc_rx37EG6&68<=f!NR&u7C_yiS-(4=hdwzGhUM`H!pFe%Q-?uTYV~~QzQ1Db7 zbKdW-Q|ADjKx4mT{wfFG4wd86oO4{?z5mVq^Jl!<0E}(ZIf3b{uR=6vunKf2#?X0? z3J)HGSutPka-Z+_%Z*<)0tpV$y9m=Dq6vix3n{UZ+$Ii{Y3>3@fg9nK_!WAG9;)|a z-f+LG&T_IXPGq(pIZePu6CqM&@OBy7Him6SI)N9}S6iyMU7j|`ys64>u21iHyN>O{ z``>){?l{8BjC^3ET^gqR5C(9kKvtDfO>(dKZDA2`WoA$G(FPoo~5J*z4EPk63U}%Rk31o z+oc|(fL?nylC0HN(jl#BpGGk`j=ZoaNO4vh%RDiK=^bu!o8U|6*P61h2Ch`0$fRG% zVfe4M!FSTid+F<5Bd}z5maw4o33m&;G7pt`-0t81?ce_0?d8L-pSNv5R3W82i&@?3>$>0a&Ur3+z!0JFGRDm=vc-86l{O9tR+Z zL;i?#-#6ZPyX>?L;$4*>GQWQPt6zWp`0E!sbQ|>ZFQ0$><(2pC@1FNR{D1ym?|%9P z+w;MjZWrZopYu@tdd$zqotQ5>%l*^+^N;WK^@;8(2W|WF_4@z*zyCi!{qp&u%1npi z^IZVC6DtOUv5o6Kwx`RujDz&+eSZ9W|M_Kp5jw^`_Ho^|ZH#SGb823n966>)%Nb&F4wVNnTG&!yU*L}*O%9u@)(y7 zfA!ZN{{8>=KmYvmcE4Xmr{W_Nz<1mD&E@((zW@H+^_|S)<@Wk|+^4DngZMHw_BI9c z#nK;JBR0m^c1mjRz05-l~kj&)>hheE%Jo<$hX4!OVHWs0cC zH0fg62c{mMzurE6{_*es%l!K5yX$rx{PN=;|L~vw!~gsr|KphQv|m|t2<}q_aua-- z$6b|y3)4=#5nXs(cwBj0n2A^iLv|iIL}bb#6RO|w_HNt0zg*t!`}4kE{RC0k#>Ruk z7~8gQ+cw7FvGEvVjIr&!@64OdW8PkFFE4Vx5z)9@uJ7Lu!tK+qx7XLp)4TW2-;c3x zmnRY#(6Mbq^qar=oB!_RfBN5j{Pgk5m+$}bZ?^%~JErp3ubcz%Eu!LTrCYfVSbUlrF0R!qBU@HBa9&y1JigL;MIYL@yszawl)dj;L3nov z(AeI(NmSQBtv2&`BVMfu$hwfgYphsK&IO(NW5>J{RWg3#e>d}9k%~A{cr*q~`?Hw< zrSK@fpmc{-D&>5fN62J)icZ zWmJ1(lf*HNmQ`py&}kgD3TG-|s4Wo@A!b}`5t`U}-%ur0Hgi+h(!ZhDpWgH3I^Mm= zA^TT2mw))_2Hfxe@E`u&_78tQMQ_Ie(6;Yldm|IShT>VSA``!@D{jEy*Qk8SRE+j!e-hC=T5dB5%No-W^i$AjnVtK444 zO|Kp$j@H&-${YC%be;wQ3 z-4wUax39l`IbQGgFQ1Rs*Lld;_UqF-oyW&N{o$8C{^Q4=|A6DmPJqlWUp{|*eYt>s zc>jaW$pR>jL*|ri>=@g=Z5t0&QBj6&gpIHf5wU;{9ZLJyuG`pm9^2r{zF+oju-ejM zn^IB{QL(ui9%I}0ZQsYfkNqXo}Mq)EB)nP$T6Y3 zjf>olc}z18#bRFG)* z{^|erf34Oj5W5=Saz?Xu~63+g{cau>M@V|)P3Ks*ZJks?U$d#lHM%=hqq>< z`yU?6gRPD>8Q|S_#Z^5`lG#*t^{DAYA2LH1Uo|G8>L*>0c}tR;ds6xzM%CBX+dSsr zq4)cr{>wlAmw){EEBN3|$4;u3F)o9N$6fX7?Rc5v&!P|bh zJUu->J-vH+db;fUw(+3ISx;$1CSKSI2xG|N=5oM`C*BgR@FXIUmO{KmdE0Th%wxX( z`s>%9e){tH^VqM~ckiHjOeJDFCgHf>0UgX^4BJx(AQ9Ux8WcvO2efkgN>lS=@HTkc z%2l8=0mv6>1d41=PjuN|KYco0zLHNc5P`;i;p^2HgSU-_PnTj0dHs6)^v9q7(|`K; z%hylix_$rM^}FxE+i{;?Uv8wtG9hx@Zu2-U`-KRfKmGjkFF)Szub06@bf0q@W8W`N z*XQ5==C@-Erm^i;V!k{*UA}ui<$k=r%5e-D^EmW4^nM(-n<^3S7+2b#w(I--@{Wk~ ze%Iqha<}Pah74w#4t6~`n8%usuD2NN#r?n<^cz_eicL z5@i+J31!YHz_?y%zkK@Xr{nhJ-S2--W1lZKBI0eQv4fdxZGWJQ2OU&m4UQ!e#8z>( zV}_+y7j(rq7R#MM@=XsQShw5+U2nv<7Gin(b!xAeA$0al`hCArHa#uCh*}61jS9?; zK8KdmaCFzUQO2zh#;$=ZWl(EvTtINSBe86y?w<&g%U;oZkWdlv;JPW24->gYsM@?c zYKW=*g4hPPq)^`;A(WG<-fjY5%CjA!ucl#j19VEopZTsQ<({F$O!`Ls( ztb!juetZ(SK3#2zrrwVGr{m>LpKkig%U$WPe6Odg{IFd{*p>U>jfpMkUSew4T~Z~O z8zV|B4=;m2lp?Tt&0$m^EN-ZnE1~`}5tGilzE0-x{)g}1y?_4jdb{23$9)2{S$c;k zkMZt@?`>k4nJpD%v13lDBpNEgJeR?~0>ZU@a$!2E8sjhbFQ4@*j^nT-ej#_qr+n3~ zUu8}r?AIs0TtFls+Ad5gD%TJ1fB$@a_xkermp{C0{(8TQ(%82B>FM%x<*^-84%U6=+kClyx<7sQ-S>a_7yEvl_v8NMi{6jp z>(}Ex1+09b%e(#gJ03d$I`8{4;q|~>8MN^=({N#FV$G|A0-{cz!88W%8*Q5fMLm^V zd>?-~4yed|idrIo=O!pbL_rK@;f)4k@SLy2$J6)k^tj3G4jSOica>G}Ab@gz_7Kfj zI^q&aKpwJ9F{(8|NHyAtq<_ZlU+mQWNuG$`>7zJ$K1?e-*f<^i6R6kwg&-j=i>^}L zLWVTjnJdrXOQ;>PwL05lNpZ7^bf4%X8hIZ)I_wERQ2|XIRVIhf(TRPm{Uq!5_$AjpgN~aaf{QU*Z=^L zNxr9gY1S6*w-F*Y6}`P~|C+mEZI^Oj{JQVurnWB?XbL7lg}@lNPx<)qv&>I}`StaF zeY*VS!@JAV{{H<%4lr-TLqQUb*PL=3cR=p<`!SFGvS0Rbd%geq@yn;z*B?K={JTH= za`~@+`kNoV|Ka`faX*e@?%Q@8$L)TAAc85_x69ZsH__X1-@chza|nrO*OqtUR$`h|&?k~5? zKDI#{VK8k>e7#r~b&4GK<2Y=NN@JS`Kl#DD0UkNc6Szmj44Y9S$Z;IEyY22Ii2X6( z1_BLRTgP_7r^qpHFJm9fBqHD)G*lr|?ToJ$Hb!j862_=1;*4w4;u=d86Tv94C2i@) zT+%l_<6HQ$CPspuHy)j)Lu>?sFje z;B1)Y@Ofg@X8PMHYd&kYf^}Lw+685e`M`}PXkE$PWTYu*hV)!aU6qXX-pWD}EJ}rl z0Z|cHct;}i1VH?BKe^v^PGeB9Gvo&Qi8c*?L~DB#irvF$mZy^T1X9aq7f|h@1UPCm z18hZ&f(5P(%0kl^ORS`cHRE&`hF0<++Y6b?|=RNx_@`QLiM_Dzx(k0 z*slUUrrret-Vb@NdS$+DV4p@+_atEOB`&4p5?S-swyIlNJa0k{4Fpscqj?c%-~xD=rXwDpImC`z2J!X zm^^rzHO1r^X**0&G!7M=4pI{&3LWE^N+NR}^dcB$nGuc4>&px7x6Ac;T=v^t1)zN! z7-sewe=#u)-a+@9Uv795Dz}) z@p{Y~@8G-TSLmD@>viMHHuf=O8`tY)>>HpWqEp0DeLb_8v%dtg^HYf?O$qAw`ezUU z2Qk~Y7&2>Ga%I`U&g@Z!0Hn5&lIe93*dY$d0Zd@>J&I6q2wN*hO;N|*J}0yN0s3+N z{0r!h2$i1;KD!!@0Rh7Q(uWYn^cxS;0xoYou$^1!?Tj#bZCyq|wsk9vD8JB_P^(?T zviFDEJ9TAojFSSI<~!WZDugfcuWm+L@SC`pYLN=>U3r!hWgrdsxLo#a8%i`)4^gscg3xx^XpDK>c^lXL`65?^-tWh!PoKYj zdA%R9jr|GZ{gvN6UEV!EZ9D+%`$d7{m@?;_dY`Hw8iS$pkn6TRUoX$ww#O>DZO|Z) zC(sYj@Vzv0hQ#A2d8}3lWX4*Zs5N)uT;@hdJKjeTdqs!>gc+VZS*XE=%?>_B@r*rX zTPoG5gid$nOfaTwSTM2MiMDxD9eGKu`86XH0V7tBPMgfH;}&C08|D;X1L-{GJYJ`W zg0_8odiUM+`ZQlYLv+9G1A*6C4dbf@JvI7?YQ}tDZZi5KKwu)=BDRP&}q(y=~ zw>=9ZUSu|v&gsWAW%wvJ8Vt#gyhe2Z(rII6S}#s*h%?;sO`}FwYO*aQU!66T7YPS8URSBo9aKGG^tsmib z>2@0LT6!{{a=$+Mp^0DZN&;EYNb|9HB~?kd2CX69o760=sD=&c*tBOBJVu27TJQ&M z@yQHV9cIgE*@b3Kx_w2`L@JBtO(#RavSZu@zh%7+<->U7*LIUO(^e%7wII+ zy$Nju8IX-FJ$$I>ald{2`uci5o}Zs>nI#mP&00^Hudnmv<@S0zj;W!=h-fIie|lzS zRjA1QcKiBzd!?IPo|py*W54i@F_h;dfp2`kmhrc5&;i)Fl2#@YjctsvQJ(GUOPE6q zG>4!hB;5me;sD|V3W6uzcVjAeTE&;H26q$JtTPQT0~3PSCM3{`eG867)I{^K3`tp) z5yEQM3-X#c*B3)Kb`~IFVhn<^^Q&+7r-QIvu1rvwvTeuh^?rN3efs+4mmdi<#@Kip z+YVo;#sphXCs4KH5M|1|-4!%=jO{wc1xn0x9QWh?%KJ9&f@?xjd9tZccnSa zhH&Fbre1d0cwDxzL1^AdMa9xZ2XxzLV@v4weaMpt`B#|Rivj>8(0j^@O{Ov2Ay$eL zoSF&g^dy^R&_Nht0V7*)K)D^4o=??t{^^(&7M}k}6kWr!F@lT1T1a$)>8OrQxYus= z#{%0jfw8^cK85zH20H)F8cvpSec#&EWM7TFoLXKd^?(hXS`hIcsvu0N$^gN&?gHz? zdPi$YbV-Khj%BXc)X6l|1yNQh~e42TLZ&%~$wpjdEJ(W&SN1%HHY_a(Iol{jpR zLNv^=CeJrz+k0~w-fd$L(;=!nbh}ItrjlX|CL$g~ZHFgg8(@r`$7Wk&y1o)ok>i-x z{X*jc8Z>so1d$K}WS%40uG8)vPa3=tZ4g!=7+n}PzP1nbR3(89KSIq_ImIAFv=#)( z?R-d6cLy7*z{bN&tQ2<_z?gJF*hy}I`QYc_G@*qMgQh;mS79G$D+Vd+gg&Che#wDn zK0Tp-SP97>?Xor&hkJ(u4~0raIaE z7X>^^-E>3%ekN=CkFF13gQW)6C*f|IAQb`Z%pEZvjKPlQsx4t4bR|bvP;9j=3-@UqI0zi z7doFaNT4h#u>E#`yMo5x{X*MrIK7P_$8p^6A~KId8DrmBVJn;GaVWsd$NeBRdy+xe zpuFwd^$H$yPGYx~&8R03Y(i5Ns_ff*`G$5rXW=%lC$@*(rF|KUYGOv=CCYVofG+tr zR{-Vf5s@T zrYZM6mt#f?5yAygB8#j;X<)km08O387)-=tzYJnIrpkm0^I#%9Y=N|h3P9WSvh7zQ zmU%$LHaXw+O>m`c+b&m(A);f?Q+P>LWu_ny10=!(l8HHCyUW53Q4S?#BRYmHkcb5w zIlb!g5(D6L3h?pK^9j2Mp%qn;cxt*F<$(p&nOVsW`x#*dU@5^NBGKQtE60aC5S}|B zV4-FnT&ZsMAx!6mUZxUKQ}{$%Ba8Y9xbP1E z_!gqZ12v&&DMIIlS~JMMMpz;UMxomgp&O+>@Lo*ik0^OER%laWQa=+Q;0&SKh+-uM zK)$*yLRlIvvnrIBx1GmOuuL&O-MC#;WX`E7Q||X;LNEKy`>sSHlbBg}-Val0xBKyW zo8RwOvqVWn?vqXZ$OOY;z@FPNeAhM+_01*S>h?o#M8#AhdycHQlScsu%Xiep=H-D% zd>1Kn;@W7Gy`~-qa*|04D0G`46ls`gIgOtV8Lg5U4A+c4N|!pDi@aOwX)-dBR;|0N zp*uu<+8G2>DR2SN27m|I zTL3}Xg67ttW2$({JgMS1VEc>`@wS=fw8S3Q0hIrST)uQslAbB;Z=6Dj*GeO;*AOKn`28@VxFu zBlWu18wvNC=vpmm+Qx20T)f69v{Wpq(-WywEjuArftYzx`oQ;&ijIo{(u?r!s{*!P zdnp_jgS=a4kcz$HJe|&~Di2u-h9mVOzBo(@kCrtcDsOB~nNz0h`@Zd$!2_aTB_cT{ z0le)33}(6Abk6JLx?iu`Hi#i}@EH5$N#?X=VYl1;yZ7%em#f_8pqL4nN`Q*^UY*t+ z@jZD!3F0npZn^1nYWu?Jm3O-Ggp@cdwGj`DreI!`70o02Lbb{+g&7xuzpUnnSfuS@ zT$kyCJtU#UXx`fJA0vjmpaLn+kCsIf@Hqp&lcUw$ zjFv+i%D_W9fv}dG4win-FkoJv2~92_C3!X^BpQ#USWR!<4;Eq%bH` zD=i3ZtI8<(QzXsD*!BXDIgi`xJm$Xb%p2QMc!3_`DXab*G10zVcx+?rP+yh9gV?sd z;qAEJL3(|9cYS(CN)Q>_HhA-KGsCd6P6`3-{P6$$+5VG?T#@N^*Z>+!|PXN_oLmIpd&`?!kCIw?;Xv)=z zuWfc)#am{b6;%~i>&(1g;0B%3kmAJ{L`u6mRDol3v)LQ{0(|3ENNt8%DC*HLB9S-z zPv9yLlRI8kFTCuz)((xgsoB)4aj=*s?UN9cNVeGI@oHm@^SdVlczNWF3eQaW=9AuY z4`2uLotfHi^zEQ2P=(qYNrZR`p{U4lZm(7rkp^p|O)61jBD%(lUBAE<0cKb@6Dd!m zkY>bJ{Hj^{Ap}YNJ_xGGOu=)kY1eD$z@ClkV3IA#=c?hY0f3*Yq<+-l+WOPKf#_u! zk*m^NEulb6Q-^>AF}?nT6e9%rd>=uX01Xh0oyd3AgxIzgM4$l> zGi^H)ZG(j{B=1ExBK5@9HdB-Swm_gPqFNXCX1+ls130uZTzg=P`xP+ zTFjD~yMI?aAJZC%NN>fXX9v#i%!>J$vfm~TGNzu4OGae3h{IW z+a{sf1ngtLrdE7~s|Vh^EuyN*cKV#EPY{t02H9MPduF-3${HKSyV1ec6@q+9S`mp` zA1!1*urWC|bH2 zv5gkeY1UH%*2Sa(;H#C&sCU__=i8T1J6X4}HPZHz)*$Ctt>&G?vTw0eiboxutj)Im zTPcGno%Ye*7I*drMliu^57ukZWxL{bOqd)&2P;qjUnbCLT^5EGNJUWFgC1Uk0x>^6 zh&=jYVzX`A80p6PPMP7TE67r#cnlDeVvGT(NUXmc4CFBd#2fp0fTn=vOb(8NoE8Sx z>U&ta`HWK}Lj`Fp+BbF;f@TU@kR<4o?4FR-hpeVCRzk06>j5>5ZDsllOSm- zx*FcqnrS2meO^41U*cDY5QS}+Zi@fI%pRLpVIpiB0YtWvb_5#)tEgqH=3+v>>r7(g z&t!bWG1}poP!1c7^aGsqXfyAT>E;6*L7vnF2_(yKQN;H8^&-8tPA|iusgTtN(G=J+ zY~6ZD=R{X?sSRfvRYI741$JKNnO~42)*4RBH@i{S>3PyA4pksXpx;RZBYaU^>*2k#Q1_QY(G7*;*|G9pSr8Tu)al`>xWxc_c#E>ocB+LPBkY;-T=b?KgcUs<0eT^{O<8tnh_q zfe(4tg}(TsVsz0$*8#CdUUf0dMEI5fVM5l+Sr&!p10t0P2t_Y7yO%C~(y8G7K#iNjoWX$cq_Y^7=k zl&Zo5U){1cI@{TnzB-w5K)R>|x0i_>)Qxo4P}{BmN)2@LAIS${0yXLtz))ZgffiB7 z%t8R9eeJo3QHhq^o?f6+ z85EF^I1*b}#!WoDzg&C6D)G;@Vt1f_25J*>wXK75lNc={ z-H(egE5>4fnszTd3KC86p<6}Kb$Fj+SWr4Fd1p6u?h^MZYESI!PPz#}U0<4zgo_$G zwTo{`^C|g%ZCvw?|1Kw^=xg^S$0)=+*r&y!G>%GfpB-r9m_oIfG|VL85637pfli@R z*D>*;LmS6{8`Uaob@KQt#*y6=I~UDWO>?@;U!c%=!Bzs#hfgZUI&tz$v>Y`XuI~J= zK6OXrm58Mzza>YhLQb-Nej~iRvR}`&CId7Y`A8vT$LSHL5XX^=2)jkeb~3E?P)HPo z9EZyG*7(W`5tT{s^ck{mzBz;41pr?~p>1$hfC?}RPALtmVMwJWF5p_|*wt0wB*e!IJ*3lx1`Q5cdjza4|JV#+tCi66VrZ;Q-G`(`FI{nd7G)B`zdVo!kB!SpVOIV4;GC4;P<+yDG0`myNnw^Mcse2^E_Th0X1tIO` zhE0woqChOq)(pi_ZdVH;W#8T?*h=g5>a;u20hH={E&C(l>%35X4J$5i_kqGun<6Ha z9;jW88kOd2n60$f2@yBQA5~f97)0wrhXiWjJjTV#)hG0)mQRUB$y9Ga9~*sq)n7zUu?^K$oD}HaFwG$* z9(LEYolnpt+kk+Aq0$ULwK;Cp#kO-1TnyrmU5**B*l!K}PZ(3FIFRdh9>!t`rNH9ty76ry%LdYa?a3)R-^ zwT{>h+E?>do@#4uNAMD~_&-dvW{V&%X_Og!7RaN42sN$iBs7<-I(%}cJe93ZZ@S^)Q4tu40}*7LZ8$+a*8GvUImKsYV0&p2MrY# ztZ5Vq6a}=eGw}D*9(VF;C^3C_p!BsN?upM4_8}8-pa(%jWO|;7A9fZR*Q!eGi*kG_ zHbjMY1-}&J(Nvy`g0=IU(=!N^SvCwXaxKcPJ>E&%NZlWK(`Y(Lbe7e4r9GnOLe6D2 z73KtS-4YT-2I{VFbE+lD61He`G%(#K1`xh6L7QSXibi4>*%3IL&}+p-elfI|ER|!$ z@oQ$qN0ST0{TbU+FJFBiLTP|D{i6|vX=>)IdrX{HscLl28?sJ2YkweK(0(h0`{Z0M ziav*^Urjv^U0G`Nni7sn&-C{6N6o5^!)e@Lb@W%y=Rkq+sYeSKWXWF97Q7K#j*oZL4Rgth>A^LOj5&9p&bWrpQ*qQ_#z=6x-{7V^=@2Wx;*fuGSb(HX1GQ zH1tb!K7G_iybcI~iD*3IgkP1ecNIX(@aiP7vA6h_i|j(;UTd#0v`5&6maV5igj=%3 zjjpj_)u70lqZsaZtqZGf+p7u{A&?)U#>+POE2$A+mi0Ro&GqCkGJ{nc zo8+siU=I8WcfWa@+GrKEQ}A*Mvj&My#AyRqqm~0j=O=5Rx-dA4q(b@z9`)ZRHQz8j z?pGux+ZI{7w zp!Q~01yK(z)TPIcB-=Lp&B&gXT&1Q$+ti|!{f1t#&(`2Ln7)vpgeGn8rt92LC|X2E zJ-X+HWh7};{3KbBLL`G_yF9miS>GI7gbHYiI#vDIzk0kzDGi=c+HciBw!m3_%QFk4 z`ngc`TwcJ3W6_HK3DztBq_ai=aj;{??1yh%Q2RsxPVE!spOCH%)j}Fq05#Z|%bFq( zW}~wjXZAso$)Z6KC`)D7Xp}6LIPH3oG}4G{*_w4dR;PK?3X0tEoub)r?nzOKofkqY z0EqcU3YE}@>qM_rGFqN@yFvMHnFMd(<7sY(mVa_I$~K@Zr&aI}gDNZ2!!mv_gdJw! za36HgVvgRx!Zv4ax7M3A?Jp6CXw*{p3Wz(4YP6FQCS5&S)47yEQvlT`>bOf;@%qJT zO&D&ZsN3;pLC?@12!>k-nwlWJjcz-ho@w7Zl=LxvBjgZ3t#3TuykEmoFn%TmB2#=#! zm#R<|i9%+xAbJoEE{NO_s1BEY$_6H_x^$j!F;v<~dp+QI^uA_9gOVu*=z_fr9|h%! z8YeY_BoyAtKPjSXlMP{gNlvMg2BqPU z@A{xxfiR*1--!^zXZ`3#=o$UBz7!058;zel9$R50YK5UWeDKOs&0zVIj#(I~z?VuZ z&BV zs+k=Y7p1D%vZjiR50ost8*dd0X?H<2@f^X7#it5|AFUE$Am<8E^(;Ihy-#;fsa(=k@I0Gz3T!q$Z zxKI=cjT9{nWG_Ju8SZru60#duXR9bgnc=~AO63;qX{NVSRtO>VR+JgR0g6*o(&p$O z5(8Y#i}72fy(2>Y&YOK47cmdAt~P)_cG3qYR_TM4!rNaztD*i<4rcu9o14b}9sr6V zY!H#dWWp6>N#&R8=Gs`l)wSSIJ9oj$aB2m$b7O=>S~P15{EeVZ${%ukVSg97aw33Q z`{Mr^tnwb>u@H*>S_?Y8LGTCsUvKvHw~wDbq?M)o)??LaZNJ{CGs~p)BFdaZXxBG5 z&?%$p;Zs{_RIsj^3vHv!#ZGNFSD#o15Mx)Vsxc!A&j}8EK?V*Y1(%2@)&u!XOVzNI zoRRmfajsTIY|6NfUWNkkWDnHn`v|CijJ-0NBoesAttYDvs6Y{F;z3I!x{~l{Miclx zwoOqZltv%)E_j{V4YT0vyQq5VUmfu^XZeUAOgbg!)uYg8ZQ*TWpn!&s|9Xn_m(jq%CDv+*U}oHkQ8e6T6YV=^hoTg^$PXzSVy8;ilw0uXH~^x-pEvr zXUoR_>3o>HaFXGaCy&84rU;8SvGMV3?;*6A4$+zwE+TB$R23?kk=iu-8g@x)3peGJ zG?$av!)Db~%*IB6staLjr{*a-v6we63TYf zW7&DDit7lu;Or_jJtiOFM5*Z zZ&ZfVdVbGJs>A@NB~eT{xN)dwu6je=`k*_zCP+qZR+Fg^?3~oA;7$s>qe}(;3jIqA zYF5x4_z3nP1>L`fUJXO0$|_7>aJ(fflD;)?uC2Ub3s3f7Odw{%rB#dzi&IQ#rKt9Y zfaOaOXNf=*gs*zB;6HG=)I;$^)3m-3tAj(qXJyj|EaXH?GbK`*!nf=dVQ&3voJ#E#q~145bEhI2*z=tXV(j zM+#sX_G)`L8qoXOTizF7RSmA*!d>s&s-XxWDY5YgwIUZ43Op_Z>q^o1t#SbrN;P+? zj;P??jS#(Df1?@Vuh{##ptXd&+zz8i-}STPH87RBzR0MT>ddO0?(5t|4ptowW+-dy zPEGejlX}x@L@0vrA&-k(#8VG|e&V0#SARQ=z&`2ijb%%mS!|IUEni{=MMrxIX^-}= zuoL`IjJY`J>P$pP+bp7usS=Vs09U})ctLc+eY9{H>+gkFaiPc-lH#TlV&0@U+v$_7 zx2Cf@PgzFCdZN=R&oV)+C(Xk3(QSfUL~HLwEruX*wT%4^1}mT^EQj~LYK|bw66na_ zuls0J0`5`~PJ5VfHk6T^4<}A}Z;f9Aj#UArg>bF{&Io8jX<4MM33%xO;JZl?L3AZs zsi5i|!ldYKwYR)$vCt05*S+MhVXKX3=mMALczFff1^`%YyzyBCH;C@i_{}%E3RV8r{Pw^ZePc(&8MYeR+*}xHlvLdhJ)TCGka8eAVEge4H%vDV$(mbQNTh-C4GmEIR z5+!P8kOvXLNUJT34~MSRn2&|=nsi?3rH=UVq09r(){`K#N*Kq1_ugeHXC+Irx}z}^ zDIC;C2dC%z&jlL%f8Q5ES2DVEw#I;np_&1&%+)C_Z_Y?TngV&sd+@|ph3Ok43WbQ{BgL7R4E&ib?0B0pl-R;vnwMd|PmJLpXaJEVM zmhDeyU&=Oo;}gQ!U^AcWE?<69S>~ha9Mur+B+&x)G+d?bFE2(L$_@& z9Fc1Z3oM(d4MCb&lxjE>kCEABIhRfr(-tm6rV!;xut*F^x`%&20r?RiN`)6>XFeYRqdI_Y9aOr?7^eIm7me|kh9fwNT1QahULmnukR zF3?*Vsj!XZ$);R*oib4ct8L=skOS4&PS|_QaZs#Vi`80nA#6&bs1T6WGK7ejIaUa# zP6tJbdf4hcjux#T(CPG^C^MisUZSAC9S4zI55(Fx4^yZUJ0L?At|g(sbJSv!3?mo&$-NYwZoyTm-a5oxRmi8=qTb#}B)}F!Mb){h z5S8jhaCG_{f!-bdK(>U~2e!bDTGm#Fm^Pp_dN6CVbuvoNpVgZRSyT~Pw0MjPUB8AsgU!dR=cSXkD2SC8}%e;EWQq(B$xGs}sEl%U(bQV9{bbp41Hp zMWC;W_^yq;JO}ZBa97@KwU&+d6f}`UgW9%|19eq_wALzfB1k7FRDjwcI+wT3NU9dpki1je8@I;E%h{VC=XZ8p~;u{v-WVb=$* z20a_sDEheqF>1Ob0|aFg6r+~RB?6(v>udB*GDbjbv=zgbOy+`?ybrQ<57ca;RGkbv z78qa?+VG&FIn)}WP(Z@YivLqgvV)YcVhuWJ2-h(LD2)=au$7~tBw&pK+~oH*QiRLM z5eICEW&z(>VfO&YJ8qcLfhV94c~JwXl9C9zsrMq$GhLksZBa^#pC_0KzztJ7ciikt zJF&}m1WY}IHscaDxLJZQa15&|`96`6(sV}5?1#>4T$;>S(od?f-T9EU;%XQ7~P%s0g7IMFLrwWKLW&R4Z{a-hkZ_qv9cI<576JG7{zKL^;pqN)PKNt>{vN zc7mh$+*;M<9W?0jh=Fs7_st zaj}q2Fiz=?Hqjhdnk{!N=*ZV-+sLIrP6GS$R%{NuyTuxTW{RVrG;C`n8iC;f#}v;* zG6KSW))-M$Q4ty=A3qaZH5L=Z@KM%_x?Tt?#eOl0NXx;Snni_@ARzM1wE!j-**pyL zsfjU8T}wXjN*t=93fpWelvDg0%~Q2Nwjb$A!8uJgsRwl>9W6iw!mTyQL91=o6Jo+< z>wQXruh(O+93CkVG~`y}@vM-@SN*WtyvP|xiAe+Y<7ZFmDuPdm>Ik79Ft^21@GlnpRci%GXeS(UgN#Ku$c}|2hkgUx_#2oRB#Mag7 zNBI0My(WVcrbBqH5r-{!sP30{OAf991PX*WBV&c>R5vuCO6?3B(9n3Tu@tw1Ekp%G z3_G8on9sUQr87OS70#4ng*3;lTf~9gRTILiTN@PPkH^mn;*U@0n^%|k`nUB!yAuj5 z7nwst_6Tk=HN4GARJ%iP0vI2#sI7*~FVND?haOT+n3x)+(7Rbpp+VmZNSrBx)ENRT z!e|$Gh6+?5H7>z5j#I61!RW3h83dg|%u5%%F7-d1;JS8FSMr=_+~J!-2Zo`e^(ClE zlp&00>^k*ZHMX(pqvDTfvHSy{sH#p$Pf#(3xtZ@)RB2H#b~8;s(c?k3Sy>J;d`#eo2KF#8C!F@%)?jR-sPDV*HhJk&E<27gaxd6W`(b&s9Cn~&6B$xp~+k{UrFBFR*J=_PX8cyj&Ol= z-bl?CjrZ_E&?^0?N0iEN3x#{1xUdX|k3ciLs8}ircUJM%sX%iXUV;XS8Jl0CE@;+G zJL0BwOcz&G*VLH|4J=JvTitFJtk@`oz^HC07zxJ!<%Z#6`~^S zxRbvd&k!JX_SOmqg?Vbc_1FZD>Y%qX;8ZD=KdKY8WWK>fgVX$9RS;XtdLT+$mD%}6 z7RfGWSTme1gtVvOGQj063FLL_t)}mib|B)<!jMiASQ# zL*rMoCs(@NY5G{WG>mQ>zLg}75y zG$wu{)Pq)F_U%E&^D)2Pxem} zqOL`ZupE=Nnt~ryQ6zmf;MU}q67nr|YSORoC=n!Vq|{_C1>!Inu}BAnWJWt&ShO?* zSqC(zso~*}TN>fMe}m4wr8T4(VTG#AKr}PCi+VTG7vZbGSZOlg0Y$R+QD-kHV{DBV z@=W7;K7aSA{9sNjV0Zh^eB)EfX#}PDHkov-_Y43T7c% zi4ulVBXTLE{IRGwR{)BV@(5x_tC~L?tf`?V8gXeES9e5DN~nuY*A8WYEvi+M%yG;9 zC!g7JPat2@r4R&F#bRZx#@6{$t4p(m1jZ<&<4iy;6lm^^Q>_l}^N~t(UQ;U4h6*Pu z(Yn(j{;t?TC73uc@CosFBfB}IJ<-!kv3vyGCAEXFiPTyL1x$=mCWz_t2VmA=SQ zRRM0Z$_<}Y)7WtZkm1J|vtR>inJ`sD_vh?l%!t$gXP+AM$EiFE71|_NXs9Y9phK6p zvgACID~qD!_-azt6&Y2|sm0cPyz8P0C=%qNJsAd$9H!Kf#SbokeFmdSExpyRtsQ&g z&KqjuU-)#8r>33~$JTgCMNAyt?XhYkPYs zA<6%o;|3aqwsEc3PY!&xCfHSTiFOgn30%}bx9_r!prYkLFIo^b#I^A%kTf*%3N5}i zVyvSvs|)1+SS{4}ujAdBD$eZGnpWeHDLVcChS~m8nuK;8<+eg-rCzH78THYOp;Ip+ zjgRVNvxJ@J(!uSJZ>D%DYf#9R?loHJpEQha@r%%*#Mq__SETm)Y}^RJzBE(8E0xlJG(M@ z#P+w|`qqcueDp-e4m9vqxAujZ*M@Lyf;Ph_VF)=>n-2%*oQIZTLcLxOtakF%wTj&K z;f%r6Oat}CR68j})Kd}x(=#m0?GcL+Sn6BCw}Wk?OE?Ry*QEn;AJ%Xv+$b%}eIb`O z&Sg6lOm?u)l!79OYbOoS6q13Z1byj|YhoNDj8$V9fs4`sjkJmJXlKwr@_``{g*S&n zg+@;JP9OPt_`v02HGB>b^?+c`$~4`a1OU$3vA$_1jl*{TgDi##-RdM4EuHpJ>?R+A z#ayrP>T2yYLfH`Nx~j`x1Ef}xJI1wEr8((p3xy~rgjY_XO4e9fYNhun>lIcW+WqGH zG4}N(!(0nD*Dr&OZhox&Pke3LH7x7$3;}3Gq^wV~Tb1MA#JmvU1ZbV)_S^fi6{8n# zPz2#Zmdm+7hL0YI*ao>#B+}T2dxRW6TL_0qrsiHfha)_^hkZ#$hx2;PF+hN?#PK$AN9~;8MWVK zo&*Lod5hs+m4qv|rpMa3J+QGzlGQ7r7^ssj#-%;1jd)Fwo@70!Y{q}sSpb>?oI>NqH5Jn+>^QV~ z%~~eXIEPNGqUI=tbD6Gf%j^uTp*>r#m)QW5nvjAzjk6onhpL%H;jQJ|u2#2dRBRH=Lt7gra@Ur+Jg`#@Gc6YWQ4r z5%Jd2&}d1M?mpOypjcA`&E9=P`YN+qd7bC-dNk$~ZLx2q;jAAbwoO9NpmU^F6>E&u zni?vxU!)G)$$j1`A{*EUlrO{PW<~RcWBcry&|dsH>qW$CPHmxuwji=yDyfjJI!g~Q z-)kX%Kka=D0Gu=C$6S)bB}~Ul2!QW**I!?Z6{W%u@=M zh=;2Jg)xuA%Ok$GmMn7mpc3AAnU+`U$2V*F#xvIHX2vwM@Z>#r*o==F?4kX7XmO(nw7Mrxv`VXiB zkV1gAhgg3iTV-lM3xnDNZ$ka*jf4OOlKE=;(rb>7#>R3E{w?j-g2lqC!uQ(9tmv}# zs6kT~+wKGP>meAuB~@7J0z>7d_89+V~ zOT?l<%>?M?YyRI8L2Fh3X{ahdq8U$CQv;>=HC5X3$x_%?HRrH}os`y$mX?C%0x@Xp zPnW4J1yvMMeH!Rx5Y>|y+#7a=aiohzs)Cfj>3(&U5@YN|p%Qu`JHoLezmmVUh>&ImTjiqWjf$eW zS?$LA>Xe1$>9Xd8@jt<}HOOHbH6uL++nrnuW#cFlBV!C9;R&q-J&ST$C$0a@LOeZL z$Z|4JsnK0;SQyv-a;F3jAB1(d8d3t))l`N>`?|7Bw%U zWsWsqRc~ivK&Ak9;i%9tHST&tHLRb8{!1Teed5$TUkS1ZXX=X7F4e}8R20jkG*AFt zK2nvRdUTOr)$R3YW?sc$cl z;IHT{h>nF0lxU1^8Rkb%tM3$Fe|%hNeH5zuM~X%6`4(o@|o%~Y-`oqA7Lf?AF5&L2(X9yRn~AkyVPFTL zB9*a2=%*s<0?OL^lBby$wIij)q0&pE41Jg03?Zupt|+uKIgt`zti3f zg;I2I-4$)X=~-<+?^koUb0AxYE6WV*3T4@NxQ%tT(RWKFL;r;o4d_yi7bF2<9e@&Y zpC2=BY}9xWv9RGC%k7ylYSpC&e`JYZ!Ue}}hN@^5gfp`_146J((0Y2AqjMyWN2rBd zS0yYp<}+ivh?VLfY}nSZg8pzyL+-nXR#3b8RO6Z4(}J_@(^}FHD+YupyYm(d#kik= z@j<7o6;a6if-1#D&8*ghZ$j{+q5S2*qn zLb1=LH(O71m3XpE5=DDd#f*;FxTf@6l=L9<1-$O70=Yksb8z6Eh$_*>#LR#yF9PVS z7r>YOEA3DmWatC-^^V!S@M`VF#cA%{J6zy5EGz0q| zDlS+!8Kx&h?;DhqwVW%vK}h=wjpEC4*W#oih4<_Ol7-!;i$kE!Fn4OQxz#`fW# ztkOi`2K0BSO77f5p`)|gj`GnKEjv0x(=~WY$1`I%;nNtrW!IIKGJ;cqZwi*SUYgR< zbI-Fc+Iybi{2tFR{Bz_*D6xomHKkqzK7SnV`+RG>LZ_!G5I$;ZY}ua4uU@HdS!&@R z3snUbGUO6%Q!LVF{j*^XtKtnt4JEJ!_gBwlaUZ?1030ps4{LepdMDiy#y%Sy%Nq~0 z4stbc!TRcA@;oY$1+dp!VT`sT{(^-Uq-)r7x*Wq85h0*>yLR|!&7>+O>D+`ctI>z? zvfJAdW;?@&Z?qc?CAo&3VFXbkt$qfOPJNr$k|^oYdWk zH))y|7c5^1&~$ywV>~jn)@yvDa4jxO%~^H>YUoL)oi$D&Pu zgE_*@${mpvsr(YhGmL|fpU?pa;0vCnRFI3)Is25Fu^WnC{ zACB&{64GdlA@e?~W2%n}K$=@-bmCRBTU@2Jzafe9Z9*7x&sMc#v=p`~K1Dw`!`$LG zN3%5W3Pt2^Ho;3vuL*y(AP~cHt`+c&9Q;;j zFr`}f9+p~HVx1~UB<=8*XpoFwfd{`wS;eV6dJfxHO|8aN<+Z2i^bmEV3(Gye2ky6$ zY^1`Gw+pM41OVi=UuqmIh8h&Umm1=9vJT=^76lPtY_+njx+!$(@}h^6EY_W1sd60> z4zLVc4cS7zwWn{q31@pD|5^=5N1^m(pd6h=H+_^&)z*p_IV*RWF}L0smqnOn!3tex zM0#*D>+T}a4awq&6EzyYQAdwX|BU)}U9d_^SRw8~$fASJ`Z((ins|qECyr)!8%sNE zP!9VW`XNew{1%j=q+L^}C}Mi=7A4U^yD?U!Z(@~#QbP!kTgNGIqjOGaAUB$pJlU#% z5^y?;ZP9RvF$Bfl-bqj+aJnKbCg-s3b~KXzQ!U%(_2L4O;z)o5FSv0Rky%Us&E=5+ z2UOJdOuiid7>l4mxh&~}7i(*=RLAK8cr4`=9h=Q=-P9_-jdu=HnAjQ7C=rNI*Bq%1B4vehW8*pAF*=R5qYw?Y$%-!r+e#$ zDHDz)-BXv1e6DoziX%{Lc^5^Nxe!{(wyp}2L+vbFlUQor!h$1Bl@c2Jr&V$SK}R-) zD27)+XnUJm<2=K!e>8iA49TU+h*MFCDE1I-?Q28r(=DLja+I5p?yRUKh-Tlk-%XzA z{Ft9q;6bZmD5|!c#H3|Xw8G0LUdk_YjLx78MHJd)q8?sTQ0pV`?u) zJtZ4xYO$k#NBaf=DKd2>y%YG(@6jCsfg}FY+d=&`lpwW6xkOVEGYSmZT9!(MLQ08D z=*#lg2vkaFwB61PHXVlsg>9tDHbCKi#J!@&!E!_#i4w?l(H4LrVg(@=J2JT)wN~^H z~$y>r^X zJPJi8Lg&UVAL@`vNS;_9lG_` zuQwWWn<&kM?AA$Fc4PBA>}N=Rcr4jBf-s6ViC!$zai=-rILS*$^cw=>cfIjB-SVcG zbjEp`*ODYipO*5khhm#{2sH+0>Gp2PQ*)jQt9qYT8Y?O%sWh>7lv=+XNSka>(bGVy zfVHGMQ2j)sCB##`NaH4-ScuU~t=xs#rGk<)1-xx%Z0ZdvexmxHvW{=2xt#iSqLs^F z(W?MhV;W{H1+6hD9tsnGRA+!f7V48dspqoCp^UQzt;f8=>{wq9LG4Lbi%SO=bciS) za#vT6H}o9J{$3}fzNH0I%jJ}H+Pq!kCP%C?pU4sZ*E)6?8E)P03T5f4hBj;IDkahi z&|CPRZd$yhi1LxG`r!zv@h}O$52W>%G8IYkyru{)v&ekg@UI(FOu0{na;1T*C~1yk;IJXp6I0lQSive3dceZ z63XRG8hDEeSEF~mujfyLiskXvsL;fTt_i^nV9(yh8k9S&b|6odS)SS2HlzY2c;vzWKSsPd7^ZE2{3=E@wvFZD zQdE!?QT_IY>RIoeXjhKkTOtQVIhSwqJi1u9NH*I6U9~htRvE?G3yPQG+E!207)BUl? z_ND?p30!L1?<^Cn@h2g=`CDn>K~*TCM`3OGV5n-#kBF)*3M3N36$z(yM^b)M%IdYH zLvO%GRdFVuo=6uF`l8;;b$n#y)C)6<|j6*@>SB+=Rc& z5y~uG>OXb8vqrvL{sdw=Gm=U zk%OE4YGY5tZT7J0b>l1K<$?NEDl}{%R1aFnqP?N$N}BR->b1YkRUOP1qw_)9HSwgL z)izt!v=PkrwGu>tmK~gNyT$c?17uI|>GWN?y|6O%*ZaG!T2lU7MWAd5<#2!+;GJoN z0ucMJ8v)^uB8Roz1!ZL!@`}`foY`uvvziDI@bO~n-G$^CuMY(*vbGy7B`3K(>dA_! zvkY~DnHZIF=;Z+C^%!vVMWIsG047ddMQWb&<-8Vc%{D(81W|9+;ou(*y-J2XitL1D ztC|nc0$_i3MHW^jq)_8AHo5fjG)a$hp>JJz`+;m+mQzsl;1MhyUcdf$k=`DZ4T_8@ zRkA^nm?8}e9dIg+nb$q06VZJSL^3eR)|v(F!k?C0T^@Tpq=YCSz6iOL3_8qdO$jxR zpeAkd#WbR=E95w?ofhX%*+Rb%=MZ*vKC_A*$sez-*Rx-xsB#Y688m}nF^{=$6@a$; zX$o(W=2T(B#&4+Y_!SlUy<~SF?q!6EzhbW{)23(+xn*wcpJE!p%>mz{l>#f zb@|bUiq;CE3#UM5d?yWH4;AV??a*YZ)tg6X0W_eQD2)tBlz6iAXkMYyOReq6n2!V~ zG1KL8Hw)Hs>J$B3{46c8?S-7}*($C~7JrpMTPVg;v4l7PPfwoIL*dpXpAYbNq3{rX zOY9+lbo%~6w$v-K6d2*;1c3;#9wFAH<>F|c+19>!d`G&Kcrk36n-J)753WboF^g%8 z6#j0E1n*SHs2)3S?Hste(yk4(1}xaFp-R31eGCMMa)<93k7iu(dxjNZ*C1ARCxcIRsa&CX2Ltvz)Po>=&O$PSR3hb9Q#y}{L3=^#i zrZCIOO+xe3gfgkSTwUa^iqLzrwJiLNdacrF+ZsWO=;O< zOHK&Yml~fwMV<{kMH*~8?vr4Qg^j$9c}+FBb18#UuwYm;#T@F zMSfYlT2)hDxal@@JP?n#c-mI!%;dcZyUf`r2;Wk`q2>qleSXO6aaW*#vsdl|T;4-h zWL*#xv4A*f(L;gDgZWjmjgs9^0t(K6gwzj8)z;=0peis$}ufU#XUK#UZe1dn#ze|Gt#`Io-V z9ah)q6D2eu6!t^KC0D8O8{FGWo_mj{7vGq-J;!%iJ8))<^K-hs@K!Uw_0jf3buq6x zRXfAIHRp`Z3~N&eb|hrFlr4Oz0268_xO_u32vVn{25SU)!zvO}1`&n>IJAf^QPE%o zbrg!Qoo)XdzC|tza9l0VuCKRm!nUG+{*AZk?qS3?f`Wd#Jm-FNN%sSgBU8EQk406x z1L>*Ix4h}c9@P$6r8`bV#~W|*L4ZE0=Erw~!&(xUk3A6x+~S1UPN!Swk1WTCQRK+- zQwPLlO6B!H{1vaeHC*I*XpeDGsFX$Py)6qVSIv?10G(26>w|z-BUW5J&7^lZUb<`n zqD)m>@S>|J3a8UO=|;LteOB}hBUk8I4fCcFA3yZ)r4LGS_5&848L}52v6<2VF>G`y z=Oi-CPH&?_F;kjApNLikv=E7E$ShLDwj)Jc6=}YKa4CCx!d1!+of?}&e~YSbLCU$8 zi%jZrOP7TH6w$S`MR%UdIe(n~TN)5>8Kg89%;GRMjRX)g&r|&>r1ttX?cq(x$VO;; z3KDriRvQP6l^j}HyeNt!+fm3fH^_q}#nGCiw+oa40i1@hf@v_zu6Ji4X{sh~F4AK# ztG8jOpJN<>S7#?r#Qjz>Zt=?!bETjMJsVdfP79z~aK6&9PDqC3 zLV(H7F)lsdMoS}l7gP5XjGa0WDOYLaJBASyDz((0Q8W~tPjA}M%`Tk^f>Q1CQ#m>f zxYM)^$SSU{Cz6I;HJnGr3#a9}E^gBL=`XBy2G#gio${ zs~T4cRb=Rugo$D*U7NmO@Y^K#Rtpr5M0>NG21u>SGu}5VtpVUSRS?2PSSsE^>z!iY zXc|GGs^xrD37&Brzob;Xs_pM@aeqOX9;6y6ZdO+Xb|T8389QKw=xo`0UA!{) zT#+e=OVlA`I?*Je*bgJ{4&r3USnNC6rzvQAIBjU&+EU*(D&>Nf0I4u7Nv>KKj3u z_K6qbwnm%5rr}{pn_)H>vqDbjX(AdZpX2O*@oDX-2GcywdxWghn*FIkyY_TX)as>r z-w|=tPQ?Yd9zPw1vmAFT6?oX%vnh_{O1gjHN4q#Rtx$c;$CHucp9K;YDm5U`wZ;9@ zMSxB)fKsb<{ZVGLI;W-PTfK^c#qjO2}>* zc6C7Stk*0g#46@sdbc+?Qtby18STq}vVQc10)%}q{3!y{bBbV#T- zU!c%+LpTlIoPXEm|m7jNEo^_@>Q>)N~pITXChjN%?f5YK7yR2b(+=ak2p#d*WxR zWx}lw30Vq1f>UV{vH1bo*toS#p{fFKWOkiANk@Kl!iCE7*;-#6NtD6@c5)hJ=Zscz zCr_kTAf_mS)G?9Mnbg0yfS(dY*6>hqtS)8bukx->p~jBqt;n-&S>w1U#Ir(LDIM2i znA+S-*s5z>P@@L;lDH_=0ex-A+VSMMiu!03ur#rH^e?nu-@5gvCsyfOXqw!Y(TA21 z@P@j1K!5bkI#O=v1L~|$3jsiY1dc@T9uz@|q-RJlPPM5Yc~2>kDQ=o-R&)5e0<%0M zGoL&h!q&pjOGKcByV46M#A&T1R(4ghqxN8$@G;0;ohK~j=4g$$*8bLpHWS)NAI3v= z7pWj3me9q~$2Ami=DH@Bm*7SABdX-ITbIEIUP&4mNUhIQznd7X|0?jtI!~-^TJNMf z3ddztGrB%ijmZ#3VEjpqrEi*j_n2*H1(ZTsd$`tezHMdKFh)q|;guCZD+c8K>47v# z-F9lmG8xv`wDn5UX()YwVCt^$!oEqdw1JK>&-nvy4>Uy}1*#g>VSZUp>&~&~2ey$} zUN4OjJYAfRdb$wx>>6s-TNJH!YF6PJifezb!9c4=TgNh=urYo6H*JvyiyfGjcUGEV zDWZ|!8=sK)_N`AexF76JvAPLguia-t!;FI*-k?h@|wRh09?ssX z7TR^1#*VBUMnM`hp&m#RER$7yjbH5-2xsy?zc0%1PB85{L5(`gjq zTl(XrBYQwJQ90SkyM}CP;CgfX7P%_5kYwzQTjaTTPtP4+)ximmlkJipQgs6rhFWY! z^5JO4dWNdom?jrTM)ZoU8Tcx#uyuITh}(3%Tz#Z~60sGq(IXdO3*#)f1K1j2=YKuh zJ2hx4xfXq`c})XmiB;r;uuIiUDynnF3>%>>uvT4mt36l$r~%%r!rCV7EcKJCt0?U9 zE0lChUR|fuYfZf4g`Ud`UH~4EsE<>3NJ`&TLZJoj+dc_7zx>9W+o@Nj3-WtonWhs% z<`5dIO2`K*Z>0fsTVZ6-?1XB$Bd$%BCR!UZM>C+RJ*1j#i>{`#1!5x4JhT6_LEEEZ zFr|hzm@6T?2NY&%K`V;#ef+55EQ)TFGvP$UIWa~VhOK7|so6t4;NmqRbAU_qgXnbL zrzM!If?15{CgEh8chZNe(Yw`tP@NZ79MBMLwJ_c&eMNI1R`s{W_4ldfrB+*^xFL#L zIQdjWYUUo@4X9lX|?eM7|U{U!5+ ztaQz5NCt`$ELkw9KyjW|IDHDl7EcIPbL36QcQg*}FS2w7i+J{1G-#dGY~f;owQxlS z>le|_QrbL2n1X2~RnniWj9sNSz_~2Tb|SV$+fQ=|~gQf#Kin5r0x>j2o-DlE?fHLp;kXA<3w&otf8 z)+JCjK4|u6(s$!v9XaaNegiA3N$GvJD(d>_dGwB=W5n9P1XMkvf0xS6-YXEi#obl* zsi@?smRk7sCq>VHggFy2zt9#r=l0N}RUw>>m;6@=gpE8UMqAD_RpCKN=Ua1HnQBKJ zySr?n8SK8QdrdXo=xe`-0HT8DYGOWFd<=_e9I z>6t2Te`v8is(oF~((!)t6O=i})+yVdQAk(ae(P6Z#H!rT`tFU$2u2)AR0b#86Y6Y!x$EFxOm z(rEZYOM`K1>ZOPE0wq8A;l%byrg;MkN4^$u9ycn3vzNN4{7Q}Vm%-v%M_i-e9jOAyL@*5(PXqJE+1JV{%S!|_Glk) z2Z{!0^>KF;Y!W-h)5Wppoc!9uWV^s3&a1AiSepE4Vzo+91;MP41iVJ~iF?~yY`XsvbmdH)8P!&X*Aq@@PDoBv1Ti4m zoLlwn#5(*4h6=<8q#jM?ynd;;0Z`5Mx$h4~g_LtTwv*XojKE^o$aHZ{Azw(A=+Yie z-;Ytbjm+wufzVbm6l+~)<*uZIesYE#^*k$piZk<>N3!LI-A1w+i1ZM4>GL=WTKW*{!^|r#%a(558E^sSG^mvovt% zQIL+cGukoid|B|8$o@xvGHPEj#`n@F%LdZRlpn@}gw_{QU-e>oPhQ7(yNONQEC~Jj z+5D-QmUt-ye!(M&R_o;YxB_WJtf6k^#SVhEes&|NE;9Ok01c%hl6Ga{j>1m?2mxs4 z1g8z#4Yol?AirQCWe5Xjp)$r2SYocRFl@BPkUQH1>*j2cQ=1tFK;59UOE|YURTNE$ zX+NY$Q@?^R$%x3tRe)*YXiT)kN;i78G?9QhdtiqdADAi|TbEiEF+!b|e=mlAGJ8=e zTF!}R8PUtP49(Fv_uj_*jYE~0+-nmBYLVcrVgx%uMQNrvoWe{Bg| za#9iJk1~-t)f@evmH)B{CulVOKOjEcI!QvCuQyy5j7)5-X?a;~$p9)0wyY7I{Ej+uqI?DnqPX~>9WTz`UnH}d%%5krb|A{R@Ta74l-j5+4I(w> z28i}!$pQ$L4sqaUk_#AG2p4@rL`I#{(_-=SWIPdnK(yIbs24M6%^JB_@}%BaIttKP zbkh={=BBai%T!lEzWF?!}UbJ~@Yx(wl38iu~npXl6to}Qh^%uOd-HJw)ka>t&u z*O0O1y}<$9AQ#c|0dZxL+7k(#mBBZUlYWeilbRtty;2JozEgH|Nr*7fWXxofb1r@Z zgN|Gmm+e31bON;KuhYjhe+PO3S5C(?7nYz`2ee-&^jYq+rSZ&CyFcu$Uja8s->L{}-cl8%D{-T6%Vmj#7uUeS3XWQ$ z+)$t_f#vlu$0?@_p@<@dfg=!DE-qEznKub1q*F~aCx;w6qv|?^dgbG4HQmjT<$A}`IPKuthDli zTbJ!SkNEl_Q?gspFR5s5-2MP_)~OfKE%Aq_SSW_2=U{%vr|7qO_|2IurMLQ3HLI!$ zOf<>^Ox}t{ot?wwxB-BFFwTepKL`y#etBc9fSbKZODr7%R}J3doSI$h6SD*>5N&xN zRtS-C@E%JRiXkjdC9H~}Vg*xY1PR2;@_Z&^p~*D4_T87Q&{$9XWd{$O^wUc^@^+)L z`Xz&q6QuR%W-19RTocaht*WqtCxK2{t#hj1F)S-GIOM1i`SHT@LYbn&86gEs*QuaZ zl2a`t>;&rXi+)zkw=enla$bErXtV}H{;3&BgMbeyKKd=a`7VQ#3#d$js~OugWv4{n z*vlm2v=|8_XGq@-&v-7Mh^ljlIw!0Y!+kQ<&af5BmK|>_n|3Rpx6tT4NUuvS34S`l z=HDIpzqPM^HB}!DDp9ES@%KZc3$@>O0{d)cmIATRCrftM`1C2enZ-aPU+g#eb+ZL} zJ~aIsm_N|VPn5p^Rc*Y{#xpm-{=`(xC&qavaN}iM$S(%{oxrKz2h4wZMupOaXl}|l zvkFY~+MeR(26mxo91o7!&R5pHeU8f!SxAzn9?knw(!2w&aB@l(=94F?O(Z zQ%gmPY|Q}|+(i>v$-qiR%^ z33+G?h$8O$n~VV$i{f$}ew9Czg`84+$7`?gS&yPTLL5Sb&(b{vs z0S1H{eWzy_+8WjJO~w6Mq^cdx-h~z3AjCgwp!At*1soh=1{6{VYEf$i! zhu{2}v#kHK;LT`?nI1j0JDI*PN)aMh&}S;n*n_PIA% zAFPO8M)5qEasH;@!|j8n`!4w~=9yjREv+mR>Lt7BB`fesbj5H42$OV12DVrYVza!o z`<-~%jyE`~I3b~}updYc6Fb+Jf)S9)f+F6>>42@4rFFDR}qx$JWH e|LsIXME(cwwz-{8LzPSb0000 + + + net10.0 + enable + True + + + + $(Version) + $(Version) + 24.10.$([System.DateTime]::UtcNow.Date.Subtract($([System.DateTime]::Parse("2024-02-07"))).TotalDays).$([System.Math]::Floor($([System.DateTime]::UtcNow.TimeOfDay.TotalMinutes))) + + + + KitX.Core.Contract.CSharp + Dynesshely + Crequency + Core service contracts for KitX Dashboard written in C# + AGPL-3.0-only + True + KitX-Background-ani.png + README.md + https://github.com/Crequency/KitX/ + https://github.com/Crequency/KitX-Standard/ + + + + + True + \ + + + True + \ + + + + + + + + + + + + + + diff --git a/KitX Core Contracts/KitX.Core.Contract/Plugin/IPluginService.cs b/KitX Core Contracts/KitX.Core.Contract/Plugin/IPluginService.cs new file mode 100644 index 0000000..60561f7 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Plugin/IPluginService.cs @@ -0,0 +1,255 @@ +using System; +using KitX.Core.Contract.Configuration; +using KitX.Shared.CSharp.Plugin; +using System.Collections.Generic; +using System.ComponentModel; +using System.Threading.Tasks; + +namespace KitX.Core.Contract.Plugin; + +///

+/// Plugin management service interface +/// +public interface IPluginService +{ + /// + /// Gets all installed plugins + /// + IReadOnlyList GetInstalledPlugins(); + + /// + /// Gets a plugin by its ID + /// + /// The plugin ID + /// The plugin installation or null if not found + IPluginInstallation? GetPlugin(Guid pluginId); + + /// + /// Imports a plugin package (.kxp file) + /// + /// Path to the .kxp file + /// True if import was successful + Task ImportPluginAsync(string kxpFilePath); + + /// + /// Removes a plugin + /// + /// The plugin ID + /// True if removal was successful + Task RemovePluginAsync(Guid pluginId); + + /// + /// Starts a plugin + /// + /// The plugin ID + /// True if start was successful + Task StartPluginAsync(Guid pluginId); + + /// + /// Stops a plugin + /// + /// The plugin ID + /// True if stop was successful + Task StopPluginAsync(Guid pluginId); + + /// + /// Calls a plugin function + /// + /// The plugin ID + /// The function name + /// Optional parameters + /// The function result + Task CallPluginFunctionAsync(Guid pluginId, string functionName, Dictionary? parameters = null); + + /// + /// Event raised when plugin status changes + /// + event EventHandler? PluginStatusChanged; +} + +/// +/// Plugin server interface for managing plugin connections +/// +public interface IPluginServer +{ + /// + /// Gets the port the server is running on + /// + int? Port { get; } + + /// + /// Starts the plugin server + /// + /// The server instance + IPluginServer Run(); + + /// + /// Stops the plugin server + /// + void Stop(); + + /// + /// Finds a connector for a specific plugin + /// + /// The plugin info + /// The plugin connector or null if not found + IPluginConnector? FindConnector(PluginInfo pluginInfo); + + /// + /// Event raised when server port changes + /// + event EventHandler? PortChanged; + + /// + /// Event raised when a plugin registers with the server + /// + event EventHandler? PluginRegistered; + + /// + /// Event raised when a plugin unregisters/disconnects from the server + /// + event EventHandler? PluginUnregistered; +} + +/// +/// Plugin connector interface for managing individual plugin connections +/// +public interface IPluginConnector +{ + /// + /// Gets the connection ID + /// + string ConnectionId { get; } + + /// + /// Gets the plugin info + /// + PluginInfo? PluginInfo { get; } + + /// + /// Sends a request to the plugin + /// + /// The request to send + void Request(object request); + + /// + /// Event raised when a plugin response is received + /// + event EventHandler? PluginResponse; + + /// + /// Event raised when plugin reports status + /// + event EventHandler? StatusReport; +} + +/// +/// Plugin status enumeration +/// +public enum PluginStatus +{ + /// + /// Unknown status + /// + Unknown, + + /// + /// Plugin is installed but not running + /// + Installed, + + /// + /// Plugin is running + /// + Running, + + /// + /// Plugin was running but is now stopped + /// + Stopped, + + /// + /// Plugin encountered an error + /// + Error +} + +/// +/// Plugin status changed event arguments +/// +public class PluginStatusChangedEventArgs : EventArgs +{ + /// + /// Gets or sets the plugin ID + /// + public Guid PluginId { get; set; } + + /// + /// Gets or sets the plugin name + /// + public string PluginName { get; set; } = string.Empty; + + /// + /// Gets or sets the old status + /// + public PluginStatus OldStatus { get; set; } + + /// + /// Gets or sets the new status + /// + public PluginStatus NewStatus { get; set; } +} + +/// +/// Plugin response event arguments +/// +public class PluginResponseEventArgs : EventArgs +{ + /// + /// Gets or sets the request ID + /// + public string RequestId { get; set; } = string.Empty; + + /// + /// Gets or sets the response content + /// + public string Content { get; set; } = string.Empty; +} + +/// +/// Plugin status report event arguments +/// +public class PluginStatusReportEventArgs : EventArgs +{ + /// + /// Gets or sets the connection ID + /// + public string ConnectionId { get; set; } = string.Empty; + + /// + /// Gets or sets the status message + /// + public string Status { get; set; } = string.Empty; +} + +/// +/// Plugin registered event arguments +/// +public class PluginRegisteredEventArgs : EventArgs +{ + /// + /// Gets or sets the plugin info + /// + public PluginInfo? PluginInfo { get; set; } +} + +/// +/// Plugin unregistered event arguments +/// +public class PluginUnregisteredEventArgs : EventArgs +{ + /// + /// Gets or sets the plugin info + /// + public PluginInfo? PluginInfo { get; set; } +} diff --git a/KitX Core Contracts/KitX.Core.Contract/README.md b/KitX Core Contracts/KitX.Core.Contract/README.md new file mode 100644 index 0000000..a35b30d --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/README.md @@ -0,0 +1,121 @@ +# KitX.Core.Contract.CSharp + +Core service contracts for KitX Dashboard written in C#. + +## Overview + +This project defines the service interfaces that separate the UI layer (Dashboard) from the core business logic layer. These interfaces enable: + +- **Dependency Injection**: Core services can be injected into ViewModels +- **Testability**: Core logic can be tested independently of the UI +- **Flexibility**: Multiple frontends (Dashboard, CLI, etc.) can use the same core services +- **Maintainability**: Clear boundaries between UI and business logic + +## Architecture + +``` +UI Layer (Dashboard, CLI, etc.) + ↓ depends on +Core Service Contracts (interfaces) + ↓ implemented by +Core Service Implementations (Managers) +``` + +## Service Interfaces + +### Configuration +- `IConfigService` - Configuration management service +- `IAppConfig` - Application configuration +- `IPluginsConfig` - Plugins configuration +- `ISecurityConfig` - Security configuration + +### Plugin Management +- `IPluginService` - Plugin lifecycle management +- `IPluginServer` - WebSocket server for plugin connections +- `IPluginConnector` - Individual plugin connection handler + +### Device Management +- `IDeviceService` - Device discovery and management +- `IDeviceDiscoveryService` - UDP broadcast device discovery +- `IDeviceServer` - HTTP API server for device communication +- `IDevicesOrganizer` - Device organization and tracking + +### Security +- `ISecurityService` - Encryption, decryption, and device key management + +### Activity Logging +- `IActivityService` - Activity recording and statistics + +### Statistics +- `IStatisticsService` - Application usage statistics + +### Workflow +- `IWorkflowService` - Workflow script execution +- `IPluginServiceProvider` - Plugin integration for workflow scripts + +### Event System +- `IEventService` - Global event bus for component communication + +### Task Management +- `ITasksService` - Background task execution + +### File Watching +- `IFileWatcherService` - File system monitoring for hot reload + +### Hotkeys +- `IKeyHookService` - Global hotkey registration and handling + +### Announcements +- `IAnnouncementService` - Announcement fetching and display + +## Usage Example + +```csharp +using KitX.Core.Contract.Configuration; +using KitX.Core.Contract.Plugin; + +public class MyViewModel +{ + private readonly IConfigService _configService; + private readonly IPluginService _pluginService; + + public MyViewModel( + IConfigService configService, + IPluginService pluginService) + { + _configService = configService; + _pluginService = pluginService; + + // Subscribe to events + _pluginService.PluginStatusChanged += OnPluginStatusChanged; + } + + private void OnPluginStatusChanged(object? sender, PluginStatusChangedEventArgs e) + { + // Handle plugin status change + } + + public async Task ImportPlugin(string filePath) + { + var success = await _pluginService.ImportPluginAsync(filePath); + if (success) + { + _configService.SaveAll(); + } + } +} +``` + +## Dependencies + +- .NET Standard 2.0/2.1 +- KitX.Shared.CSharp - Shared data models + +## License + +AGPL-3.0-only + +## Links + +- [KitX Repository](https://github.com/Crequency/KitX/) +- [KitX Standard Repository](https://github.com/Crequency/KitX-Standard/) diff --git a/KitX Core Contracts/KitX.Core.Contract/Security/ISecurityService.cs b/KitX Core Contracts/KitX.Core.Contract/Security/ISecurityService.cs new file mode 100644 index 0000000..4cd6f23 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Security/ISecurityService.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Threading.Tasks; +using KitX.Shared.CSharp.Device; + +namespace KitX.Core.Contract.Security; + +/// +/// Security management service interface +/// +public interface ISecurityService +{ + /// + /// Gets all device keys + /// + IReadOnlyList GetDeviceKeys(); + + /// + /// Adds a device key + /// + /// The MAC address + /// The device name + /// The public key + /// True if addition was successful + bool AddDeviceKey(string macAddress, string deviceName, string publicKey); + + /// + /// Removes a device key + /// + /// The MAC address + /// True if removal was successful + bool RemoveDeviceKey(string macAddress); + + /// + /// Checks if a device is authorized + /// + /// The device locator + /// True if the device is authorized + bool IsDeviceAuthorized(DeviceLocator device); + + /// + /// Encrypts a string + /// + /// The content to encrypt + /// The target device MAC address + /// The encrypted content + Task EncryptStringAsync(string content, string targetDeviceMacAddress); + + /// + /// Decrypts a string + /// + /// The encrypted content + /// The source device MAC address + /// The decrypted content + Task DecryptStringAsync(string encryptedContent, string sourceDeviceMacAddress); + + /// + /// Computes a hash + /// + /// The content to hash + /// The hash + string ComputeHash(string content); +} + +/// +/// Device key interface +/// +public interface IDeviceKey +{ + /// + /// Gets the MAC address + /// + string MacAddress { get; } + + /// + /// Gets the device name + /// + string DeviceName { get; } + + /// + /// Gets the public key + /// + string PublicKey { get; } + + /// + /// Gets the time when the key was added + /// + DateTime AddedAt { get; } +} diff --git a/KitX Core Contracts/KitX.Core.Contract/Statistics/IStatisticsService.cs b/KitX Core Contracts/KitX.Core.Contract/Statistics/IStatisticsService.cs new file mode 100644 index 0000000..13d0310 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Statistics/IStatisticsService.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Threading.Tasks; + +namespace KitX.Core.Contract.Statistics; + +/// +/// Statistics service interface +/// +public interface IStatisticsService +{ + /// + /// Starts statistics collection + /// + void Start(); + + /// + /// Stops statistics collection + /// + void Stop(); + + /// + /// Gets usage statistics + /// + /// Start date + /// End date + /// Usage statistics + IUsageStatistics GetUsageStatistics(DateTime startDate, DateTime endDate); +} + +/// +/// Usage statistics interface +/// +public interface IUsageStatistics +{ + /// + /// Gets the total usage in seconds + /// + double TotalUsageSeconds { get; } + + /// + /// Gets the daily usage dictionary + /// + Dictionary DailyUsage { get; } +} diff --git a/KitX Core Contracts/KitX.Core.Contract/Tasks/ITasksService.cs b/KitX Core Contracts/KitX.Core.Contract/Tasks/ITasksService.cs new file mode 100644 index 0000000..1959f30 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Tasks/ITasksService.cs @@ -0,0 +1,25 @@ +using System; +using System.Threading.Tasks; + +namespace KitX.Core.Contract.Tasks; + +/// +/// Tasks service interface for background task management +/// +public interface ITasksService +{ + /// + /// Runs a synchronous task + /// + /// The task to run + /// Optional task name + void RunTask(Action task, string? taskName = null); + + /// + /// Runs an asynchronous task + /// + /// The task to run + /// Optional task name + /// Task representing the async operation + Task RunTaskAsync(Func task, string? taskName = null); +} diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowService.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowService.cs new file mode 100644 index 0000000..930b699 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowService.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using KitX.Shared.CSharp.Plugin; + +namespace KitX.Core.Contract.Workflow; + +/// +/// Workflow service interface +/// +public interface IWorkflowService +{ + /// + /// Gets the workflow list + /// + IReadOnlyList GetWorkflows(); + + /// + /// Adds a workflow + /// + /// The workflow to add + void AddWorkflow(IWorkflowCase workflow); + + /// + /// Removes a workflow + /// + /// The workflow ID + void RemoveWorkflow(string workflowId); + + /// + /// Runs a workflow + /// + /// The workflow ID + /// True if run was successful + Task RunWorkflowAsync(string workflowId); + + /// + /// Stops a workflow + /// + /// The workflow ID + /// True if stop was successful + Task StopWorkflowAsync(string workflowId); + + /// + /// Executes a workflow script + /// + /// The script content + /// Optional parameters + /// The execution result + Task ExecuteScriptAsync(string script, Dictionary? parameters = null); + + /// + /// Executes workflow script codes with plugin dependencies + /// + /// The code to execute + /// Required plugins for this script + /// Whether to include timestamp in result + /// Cancellation token + /// Execution result as string + Task ExecuteCodesAsync( + string code, + List? requiredPlugins = null, + bool includeTimestamp = true, + System.Threading.CancellationToken cancellationToken = default); + + /// + /// Initializes the plugin manager + /// + void InitializePluginManager(); + + /// + /// Updates the available plugins list + /// + /// Plugin list + void UpdateAvailablePlugins(List plugins); +} + +/// +/// Plugin service provider interface for workflow integration +/// +public interface IPluginServiceProvider +{ + /// + /// Gets running plugins + /// + IEnumerable GetRunningPlugins(); + + /// + /// Finds a plugin by name + /// + /// The plugin name + /// The plugin info or null if not found + PluginInfo? FindPlugin(string pluginName); + + /// + /// Finds a connector for a plugin + /// + /// The plugin info + /// The connector or null if not found + object? FindConnector(PluginInfo pluginInfo); + + /// + /// Sends a request asynchronously + /// + /// The connector + /// The request + Task SendRequestAsync(object connector, object request); + + /// + /// Subscribes to plugin responses + /// + /// The response handler + void SubscribeToResponses(Action responseHandler); +} + +/// +/// Workflow case interface +/// +public interface IWorkflowCase +{ + /// + /// Gets the workflow ID + /// + string Id { get; } + + /// + /// Gets the workflow name + /// + string Name { get; } + + /// + /// Gets the workflow description + /// + string Description { get; } + + /// + /// Gets the icon path + /// + string IconPath { get; } + + /// + /// Gets or sets a value indicating whether the workflow is running + /// + bool IsRunning { get; set; } + + /// + /// Gets or sets the script file path + /// + string? ScriptPath { get; set; } +} diff --git a/KitX Script/Kscript.CSharp.Parser/CodeGen/AssemblyCache.cs b/KitX Script/Kscript.CSharp.Parser/CodeGen/AssemblyCache.cs index 4e0e193..01dcdfc 100644 --- a/KitX Script/Kscript.CSharp.Parser/CodeGen/AssemblyCache.cs +++ b/KitX Script/Kscript.CSharp.Parser/CodeGen/AssemblyCache.cs @@ -1,4 +1,4 @@ -using System.Collections.Concurrent; +using System.Collections.Concurrent; using System.Text; namespace Kscript.CSharp.Parser.CodeGen; diff --git a/KitX Shared/KitX.Shared.CSharp/Device/DeviceInfoExtensions.cs b/KitX Shared/KitX.Shared.CSharp/Device/DeviceInfoExtensions.cs new file mode 100644 index 0000000..c4e23ec --- /dev/null +++ b/KitX Shared/KitX.Shared.CSharp/Device/DeviceInfoExtensions.cs @@ -0,0 +1,75 @@ +using System; + +namespace KitX.Shared.CSharp.Device; + +/// +/// Extension methods for DeviceInfo +/// +public static class DeviceInfoExtensions +{ + /// + /// Checks if a device is offline (not seen within TTL period) + /// + /// The device info + /// Time-to-live in seconds + /// True if the device is offline + public static bool IsOffline(this DeviceInfo info, int ttlSeconds) => + DateTime.UtcNow - info.SendTime.ToUniversalTime() > TimeSpan.FromSeconds(ttlSeconds); + + /// + /// Checks if this device is the current device + /// + /// The device info + /// The current device info + /// True if this is the current device + public static bool IsCurrentDevice(this DeviceInfo info, DeviceInfo selfDeviceInfo) => + info.IsSameDevice(selfDeviceInfo); + + /// + /// Checks if two device infos represent the same device + /// + /// The first device info + /// The target device info + /// True if they are the same device + public static bool IsSameDevice(this DeviceInfo info, DeviceInfo target) => + info.Device.IsSameDevice(target.Device); + + /// + /// Updates this device info with data from another device info + /// + /// The device info to update + /// The source device info + public static void UpdateTo(this DeviceInfo info, DeviceInfo target) + { + var type = typeof(DeviceInfo); + + var fields = type.GetFields(); + + foreach (var field in fields) + { + var firstValue = field.GetValue(info); + var secondValue = field.GetValue(target); + + if (firstValue?.Equals(secondValue) ?? false) + continue; + + field.SetValue(info, secondValue); + } + + var properties = type.GetProperties(); + + foreach (var property in properties) + { + if (!property.CanWrite) + continue; + + object? firstValue = property.GetValue(info); + object? secondValue = property.GetValue(target); + + if (firstValue?.Equals(secondValue) ?? false) + continue; + + property.SetValue(info, secondValue); + } + } +} From be9302d5679b69f47cde4d67470dfb7d264e6652 Mon Sep 17 00:00:00 2001 From: StarInk Date: Wed, 25 Feb 2026 03:46:55 +0100 Subject: [PATCH 19/60] =?UTF-8?q?=F0=9F=92=BE=20=F0=9F=93=9D=20Feat(IPlugi?= =?UTF-8?q?nInstallation):=20Added=20Id=20parameter=20to=20IPluginInstalla?= =?UTF-8?q?tion.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../KitX.Core.Contract/Configuration/IConfigService.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/KitX Core Contracts/KitX.Core.Contract/Configuration/IConfigService.cs b/KitX Core Contracts/KitX.Core.Contract/Configuration/IConfigService.cs index c70bca0..987875e 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Configuration/IConfigService.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Configuration/IConfigService.cs @@ -320,6 +320,11 @@ public interface ISecurityConfig ///
public interface IPluginInstallation { + /// + /// Gets the unique identifier for this plugin installation + /// + Guid Id { get; } + /// /// Gets the installation path /// From dd21efb1f7ba9257746d1a9e97df0b733a84de9e Mon Sep 17 00:00:00 2001 From: StarInk Date: Tue, 10 Mar 2026 10:59:16 +0100 Subject: [PATCH 20/60] =?UTF-8?q?=F0=9F=92=BE=20=F0=9F=A7=A9=20Feat,=20Ref?= =?UTF-8?q?actor:=20=E8=BF=81=E7=A7=BB=E5=B9=B6=E6=95=B4=E7=90=86=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E4=B8=8E=E8=AE=BE=E5=A4=87=E5=AE=89=E5=85=A8=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E5=8A=9F=E8=83=BD=E7=9A=84=E6=8E=A5=E5=8F=A3=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Configuration/ConfigChangedEventArgs.cs | 29 ++ .../Configuration/IActivityConfig.cs | 9 + .../Configuration/IAnnouncementConfig.cs | 16 + .../Configuration/IAppConfig.cs | 68 ++++ .../Configuration/IConfigService.cs | 370 +----------------- .../Configuration/IConfigWithMetadata.cs | 24 ++ .../Configuration/IIOConfig.cs | 10 + .../Configuration/ILoadersConfig.cs | 9 + .../Configuration/ILogConfig.cs | 16 + .../Configuration/IPagesConfig.cs | 52 +++ .../Configuration/IPluginsConfig.cs | 49 +++ .../Configuration/ISecurityConfig.cs | 41 ++ .../Configuration/IWebConfig.cs | 31 ++ .../Configuration/IWindowsConfig.cs | 41 ++ .../NavigationViewPaneDisplayMode.cs | 13 + .../Configuration/WindowState.cs | 15 + .../Security/ISecurityService.cs | 91 ++++- 17 files changed, 496 insertions(+), 388 deletions(-) create mode 100644 KitX Core Contracts/KitX.Core.Contract/Configuration/ConfigChangedEventArgs.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Configuration/IActivityConfig.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Configuration/IAnnouncementConfig.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Configuration/IAppConfig.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Configuration/IConfigWithMetadata.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Configuration/IIOConfig.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Configuration/ILoadersConfig.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Configuration/ILogConfig.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Configuration/IPagesConfig.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Configuration/IPluginsConfig.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Configuration/ISecurityConfig.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Configuration/IWebConfig.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Configuration/IWindowsConfig.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Configuration/NavigationViewPaneDisplayMode.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Configuration/WindowState.cs diff --git a/KitX Core Contracts/KitX.Core.Contract/Configuration/ConfigChangedEventArgs.cs b/KitX Core Contracts/KitX.Core.Contract/Configuration/ConfigChangedEventArgs.cs new file mode 100644 index 0000000..9e05817 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Configuration/ConfigChangedEventArgs.cs @@ -0,0 +1,29 @@ +using System; + +namespace KitX.Core.Contract.Configuration; + +/// +/// Configuration changed event arguments +/// +public class ConfigChangedEventArgs : EventArgs +{ + /// + /// Gets or sets the configuration type (e.g., "App", "Plugins", "Security") + /// + public string ConfigType { get; set; } = string.Empty; + + /// + /// Gets or sets the property name that changed + /// + public string PropertyName { get; set; } = string.Empty; + + /// + /// Gets or sets the old value + /// + public object? OldValue { get; set; } + + /// + /// Gets or sets the new value + /// + public object? NewValue { get; set; } +} diff --git a/KitX Core Contracts/KitX.Core.Contract/Configuration/IActivityConfig.cs b/KitX Core Contracts/KitX.Core.Contract/Configuration/IActivityConfig.cs new file mode 100644 index 0000000..8c15a61 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Configuration/IActivityConfig.cs @@ -0,0 +1,9 @@ +namespace KitX.Core.Contract.Configuration; + +/// +/// Activity configuration section +/// +public interface IActivityConf +{ + int TotalRecorded { get; set; } +} diff --git a/KitX Core Contracts/KitX.Core.Contract/Configuration/IAnnouncementConfig.cs b/KitX Core Contracts/KitX.Core.Contract/Configuration/IAnnouncementConfig.cs new file mode 100644 index 0000000..845ee4a --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Configuration/IAnnouncementConfig.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; + +namespace KitX.Core.Contract.Configuration; + +public interface IAnnouncementConfig +{ + /// + /// Gets or sets the list of accepted announcement IDs + /// + List Accepted { get; set; } + + /// + /// Gets or sets the config file location + /// + string? ConfigFileLocation { get; set; } +} diff --git a/KitX Core Contracts/KitX.Core.Contract/Configuration/IAppConfig.cs b/KitX Core Contracts/KitX.Core.Contract/Configuration/IAppConfig.cs new file mode 100644 index 0000000..7dc51f5 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Configuration/IAppConfig.cs @@ -0,0 +1,68 @@ +using System.Collections.Generic; + +namespace KitX.Core.Contract.Configuration; + +/// +/// Application configuration interface (complete structure) +/// +public interface IAppConfig +{ + /// + /// Gets or sets the application configuration + /// + IAppConf App { get; set; } + + /// + /// Gets or sets the windows configuration + /// + IWindowsConf Windows { get; set; } + + /// + /// Gets or sets the pages configuration + /// + IPagesConf Pages { get; set; } + + /// + /// Gets or sets the web configuration + /// + IWebConf Web { get; set; } + + /// + /// Gets or sets the log configuration + /// + ILogConf Log { get; set; } + + /// + /// Gets or sets the IO configuration + /// + IIOConf IO { get; set; } + + /// + /// Gets or sets the activity configuration + /// + IActivityConf Activity { get; set; } + + /// + /// Gets or sets the loaders configuration + /// + ILoadersConf Loaders { get; set; } +} + +/// +/// Application configuration section +/// +public interface IAppConf +{ + string IconFileName { get; set; } + string CoverIconFileName { get; set; } + string AppLanguage { get; set; } + string Theme { get; set; } + string ThemeColor { get; set; } + Dictionary SurpportLanguages { get; set; } + string LocalPluginsFileFolder { get; set; } + string LocalPluginsDataFolder { get; set; } + bool DeveloperSetting { get; set; } + bool ShowAnnouncementWhenStart { get; set; } + ulong RanTime { get; set; } + int LastBreakAfterExit { get; set; } +} diff --git a/KitX Core Contracts/KitX.Core.Contract/Configuration/IConfigService.cs b/KitX Core Contracts/KitX.Core.Contract/Configuration/IConfigService.cs index 987875e..4c15a2a 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Configuration/IConfigService.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Configuration/IConfigService.cs @@ -1,25 +1,7 @@ -using System; -using System.Collections.Generic; -using Common.BasicHelper.Graphics.Screen; -using KitX.Shared.CSharp.Device; -using KitX.Shared.CSharp.Loader; -using KitX.Shared.CSharp.Plugin; -using Serilog.Events; +using System; namespace KitX.Core.Contract.Configuration; -/// -/// Window state enumeration (mirrors Avalonia.WindowState) -/// -public enum WindowState -{ - Normal, - Minimized, - Maximized, - FullScreen, - NonInteractive -} - /// /// Configuration management service interface /// @@ -60,353 +42,3 @@ public interface IConfigService /// event EventHandler? ConfigChanged; } - -/// -/// Application configuration interface (complete structure) -/// -public interface IAppConfig -{ - /// - /// Gets or sets the application configuration - /// - IAppConf App { get; set; } - - /// - /// Gets or sets the windows configuration - /// - IWindowsConf Windows { get; set; } - - /// - /// Gets or sets the pages configuration - /// - IPagesConf Pages { get; set; } - - /// - /// Gets or sets the web configuration - /// - IWebConf Web { get; set; } - - /// - /// Gets or sets the log configuration - /// - ILogConf Log { get; set; } - - /// - /// Gets or sets the IO configuration - /// - IIOConf IO { get; set; } - - /// - /// Gets or sets the activity configuration - /// - IActivityConf Activity { get; set; } - - /// - /// Gets or sets the loaders configuration - /// - ILoadersConf Loaders { get; set; } -} - -/// -/// Application configuration section -/// -public interface IAppConf -{ - string IconFileName { get; set; } - string CoverIconFileName { get; set; } - string AppLanguage { get; set; } - string Theme { get; set; } - string ThemeColor { get; set; } - Dictionary SurpportLanguages { get; set; } - string LocalPluginsFileFolder { get; set; } - string LocalPluginsDataFolder { get; set; } - bool DeveloperSetting { get; set; } - bool ShowAnnouncementWhenStart { get; set; } - ulong RanTime { get; set; } - int LastBreakAfterExit { get; set; } -} - -/// -/// Windows configuration section -/// -public interface IWindowsConf -{ - IMainWindowConf MainWindow { get; set; } - IAnnouncementWindowConf AnnouncementWindow { get; set; } -} - -/// -/// Main window configuration -/// -public interface IMainWindowConf -{ - Resolution Size { get; set; } - Distances Location { get; set; } - WindowState WindowState { get; set; } - bool IsHidden { get; set; } - Dictionary Tags { get; set; } - bool EnabledMica { get; set; } - int GreetingTextCount_Morning { get; set; } - int GreetingTextCount_Noon { get; set; } - int GreetingTextCount_AfterNoon { get; set; } - int GreetingTextCount_Evening { get; set; } - int GreetingTextCount_Night { get; set; } - int GreetingUpdateInterval { get; set; } -} - -/// -/// Announcement window configuration -/// -public interface IAnnouncementWindowConf -{ - Resolution Size { get; set; } - Distances Location { get; set; } -} - -/// -/// Pages configuration section -/// -public interface IPagesConf -{ - IHomePageConf Home { get; set; } - IDevicePageConf Device { get; set; } - IMarketPageConf Market { get; set; } - ISettingsPageConf Settings { get; set; } -} - -/// -/// Home page configuration -/// -public interface IHomePageConf -{ - NavigationViewPaneDisplayMode NavigationViewPaneDisplayMode { get; set; } - string SelectedViewName { get; set; } - bool IsNavigationViewPaneOpened { get; set; } - bool UseAreaExpanded { get; set; } -} - -/// -/// Device page configuration -/// -public interface IDevicePageConf { } - -/// -/// Market page configuration -/// -public interface IMarketPageConf { } - -/// -/// Settings page configuration -/// -public interface ISettingsPageConf -{ - NavigationViewPaneDisplayMode NavigationViewPaneDisplayMode { get; set; } - string SelectedViewName { get; set; } - bool PaletteAreaExpanded { get; set; } - bool WebRelatedAreaExpanded { get; set; } - bool WebRelatedAreaOfNetworkInterfacesExpanded { get; set; } - bool LogRelatedAreaExpanded { get; set; } - bool UpdateRelatedAreaExpanded { get; set; } - bool AboutAreaExpanded { get; set; } - bool AuthorsAreaExpanded { get; set; } - bool LinksAreaExpanded { get; set; } - bool ThirdPartyLicensesAreaExpanded { get; set; } - bool IsNavigationViewPaneOpened { get; set; } -} - -/// -/// Web configuration section -/// -public interface IWebConf -{ - double DelayStartSeconds { get; set; } - string ApiServer { get; set; } - string ApiPath { get; set; } - int DevicesViewRefreshDelay { get; set; } - List? AcceptedNetworkInterfaces { get; set; } - int? UserSpecifiedDevicesServerPort { get; set; } - int? UserSpecifiedPluginsServerPort { get; set; } - int UdpPortSend { get; set; } - int UdpPortReceive { get; set; } - int UdpSendFrequency { get; set; } - string UdpBroadcastAddress { get; set; } - string IPFilter { get; set; } - int SocketBufferSize { get; set; } - int DeviceInfoTTLSeconds { get; set; } - bool DisableRemovingOfflineDeviceCard { get; set; } - string UpdateServer { get; set; } - string UpdatePath { get; set; } - string UpdateDownloadPath { get; set; } - string UpdateChannel { get; set; } - string UpdateSource { get; set; } - int DebugServicesServerPort { get; set; } -} - -/// -/// Log configuration section -/// -public interface ILogConf -{ - long LogFileSingleMaxSize { get; set; } - string LogFilePath { get; set; } - string LogTemplate { get; set; } - int LogFileMaxCount { get; set; } - int LogFileFlushInterval { get; set; } - public LogEventLevel LogLevel { get; set; } -} - -/// -/// IO configuration section -/// -public interface IIOConf -{ - int UpdatingCheckPerThreadFilesCount { get; set; } - int OperatingSystemVersionUpdateInterval { get; set; } -} - -/// -/// Activity configuration section -/// -public interface IActivityConf -{ - int TotalRecorded { get; set; } -} - -/// -/// Loaders configuration section -/// -public interface ILoadersConf -{ - string InstallPath { get; set; } -} - -public interface IAnnouncementConfig -{ - /// - /// Gets or sets the list of accepted announcement IDs - /// - List Accepted { get; set; } - - /// - /// Gets or sets the config file location - /// - string? ConfigFileLocation { get; set; } -} - -/// -/// Plugins configuration interface -/// -public interface IPluginsConfig -{ - /// - /// Gets or sets the list of plugin installations - /// - IList Plugins { get; set; } -} - -/// -/// Security configuration interface -/// -public interface ISecurityConfig -{ - /// - /// Gets or sets the device keys dictionary - /// - IDictionary DeviceKeys { get; set; } -} - -/// -/// Plugin installation interface -/// -public interface IPluginInstallation -{ - /// - /// Gets the unique identifier for this plugin installation - /// - Guid Id { get; } - - /// - /// Gets the installation path - /// - string? InstallPath { get; } - - /// - /// Gets or sets the plugin information - /// - PluginInfo? PluginInfo { get; set; } - - /// - /// Gets or sets the loader information - /// - LoaderInfo? LoaderInfo { get; set; } - - /// - /// Gets or sets the list of installed devices - /// - IList InstalledDevices { get; set; } -} - -/// -/// Device key interface -/// -public interface IDeviceKey -{ - /// - /// Gets the MAC address - /// - string MacAddress { get; } - - /// - /// Gets the device name - /// - string DeviceName { get; } - - /// - /// Gets the public key - /// - string PublicKey { get; } - - /// - /// Gets the time when the key was added - /// - DateTime AddedAt { get; } -} - -/// -/// Navigation view pane display mode enum -/// -public enum NavigationViewPaneDisplayMode -{ - Auto = 0, - Left = 1, - Top = 2, - LeftCompact = 3, - LeftMinimal = 4 -} - -/// -/// Configuration changed event arguments -/// -public class ConfigChangedEventArgs : EventArgs -{ - /// - /// Gets or sets the configuration type (e.g., "App", "Plugins", "Security") - /// - public string ConfigType { get; set; } = string.Empty; - - /// - /// Gets or sets the property name that changed - /// - public string PropertyName { get; set; } = string.Empty; - - /// - /// Gets or sets the old value - /// - public object? OldValue { get; set; } - - /// - /// Gets or sets the new value - /// - public object? NewValue { get; set; } -} - diff --git a/KitX Core Contracts/KitX.Core.Contract/Configuration/IConfigWithMetadata.cs b/KitX Core Contracts/KitX.Core.Contract/Configuration/IConfigWithMetadata.cs new file mode 100644 index 0000000..b43f4f3 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Configuration/IConfigWithMetadata.cs @@ -0,0 +1,24 @@ +using System; + +namespace KitX.Core.Contract.Configuration; + +/// +/// Interface for configurations with metadata fields +/// +public interface IConfigWithMetadata +{ + /// + /// Gets or sets the configuration file location + /// + string? ConfigFileLocation { get; set; } + + /// + /// Gets or sets the configuration file watcher name + /// + string? ConfigFileWatcherName { get; set; } + + /// + /// Gets or sets the configuration generated time + /// + DateTime? ConfigGeneratedTime { get; set; } +} diff --git a/KitX Core Contracts/KitX.Core.Contract/Configuration/IIOConfig.cs b/KitX Core Contracts/KitX.Core.Contract/Configuration/IIOConfig.cs new file mode 100644 index 0000000..bf766f8 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Configuration/IIOConfig.cs @@ -0,0 +1,10 @@ +namespace KitX.Core.Contract.Configuration; + +/// +/// IO configuration section +/// +public interface IIOConf +{ + int UpdatingCheckPerThreadFilesCount { get; set; } + int OperatingSystemVersionUpdateInterval { get; set; } +} diff --git a/KitX Core Contracts/KitX.Core.Contract/Configuration/ILoadersConfig.cs b/KitX Core Contracts/KitX.Core.Contract/Configuration/ILoadersConfig.cs new file mode 100644 index 0000000..c1ba09b --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Configuration/ILoadersConfig.cs @@ -0,0 +1,9 @@ +namespace KitX.Core.Contract.Configuration; + +/// +/// Loaders configuration section +/// +public interface ILoadersConf +{ + string InstallPath { get; set; } +} diff --git a/KitX Core Contracts/KitX.Core.Contract/Configuration/ILogConfig.cs b/KitX Core Contracts/KitX.Core.Contract/Configuration/ILogConfig.cs new file mode 100644 index 0000000..4dbb731 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Configuration/ILogConfig.cs @@ -0,0 +1,16 @@ +using Serilog.Events; + +namespace KitX.Core.Contract.Configuration; + +/// +/// Log configuration section +/// +public interface ILogConf +{ + long LogFileSingleMaxSize { get; set; } + string LogFilePath { get; set; } + string LogTemplate { get; set; } + int LogFileMaxCount { get; set; } + int LogFileFlushInterval { get; set; } + public LogEventLevel LogLevel { get; set; } +} diff --git a/KitX Core Contracts/KitX.Core.Contract/Configuration/IPagesConfig.cs b/KitX Core Contracts/KitX.Core.Contract/Configuration/IPagesConfig.cs new file mode 100644 index 0000000..c73e68e --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Configuration/IPagesConfig.cs @@ -0,0 +1,52 @@ +namespace KitX.Core.Contract.Configuration; + +/// +/// Pages configuration section +/// +public interface IPagesConf +{ + IHomePageConf Home { get; set; } + IDevicePageConf Device { get; set; } + IMarketPageConf Market { get; set; } + ISettingsPageConf Settings { get; set; } +} + +/// +/// Home page configuration +/// +public interface IHomePageConf +{ + NavigationViewPaneDisplayMode NavigationViewPaneDisplayMode { get; set; } + string SelectedViewName { get; set; } + bool IsNavigationViewPaneOpened { get; set; } + bool UseAreaExpanded { get; set; } +} + +/// +/// Device page configuration +/// +public interface IDevicePageConf { } + +/// +/// Market page configuration +/// +public interface IMarketPageConf { } + +/// +/// Settings page configuration +/// +public interface ISettingsPageConf +{ + NavigationViewPaneDisplayMode NavigationViewPaneDisplayMode { get; set; } + string SelectedViewName { get; set; } + bool PaletteAreaExpanded { get; set; } + bool WebRelatedAreaExpanded { get; set; } + bool WebRelatedAreaOfNetworkInterfacesExpanded { get; set; } + bool LogRelatedAreaExpanded { get; set; } + bool UpdateRelatedAreaExpanded { get; set; } + bool AboutAreaExpanded { get; set; } + bool AuthorsAreaExpanded { get; set; } + bool LinksAreaExpanded { get; set; } + bool ThirdPartyLicensesAreaExpanded { get; set; } + bool IsNavigationViewPaneOpened { get; set; } +} diff --git a/KitX Core Contracts/KitX.Core.Contract/Configuration/IPluginsConfig.cs b/KitX Core Contracts/KitX.Core.Contract/Configuration/IPluginsConfig.cs new file mode 100644 index 0000000..92f9c2e --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Configuration/IPluginsConfig.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using KitX.Shared.CSharp.Device; +using KitX.Shared.CSharp.Loader; +using KitX.Shared.CSharp.Plugin; + +namespace KitX.Core.Contract.Configuration; + +/// +/// Plugins configuration interface +/// +public interface IPluginsConfig +{ + /// + /// Gets or sets the list of plugin installations + /// + IList Plugins { get; set; } +} + +/// +/// Plugin installation interface +/// +public interface IPluginInstallation +{ + /// + /// Gets the unique identifier for this plugin installation + /// + Guid Id { get; } + + /// + /// Gets the installation path + /// + string? InstallPath { get; } + + /// + /// Gets or sets the plugin information + /// + PluginInfo? PluginInfo { get; set; } + + /// + /// Gets or sets the loader information + /// + LoaderInfo? LoaderInfo { get; set; } + + /// + /// Gets or sets the list of installed devices + /// + IList InstalledDevices { get; set; } +} diff --git a/KitX Core Contracts/KitX.Core.Contract/Configuration/ISecurityConfig.cs b/KitX Core Contracts/KitX.Core.Contract/Configuration/ISecurityConfig.cs new file mode 100644 index 0000000..b2b7a12 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Configuration/ISecurityConfig.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; + +namespace KitX.Core.Contract.Configuration; + +/// +/// Security configuration interface +/// +public interface ISecurityConfig +{ + /// + /// Gets or sets the device keys list + /// + IList DeviceKeys { get; set; } +} + +/// +/// Device key interface +/// +public interface IDeviceKey +{ + /// + /// Gets the MAC address + /// + string MacAddress { get; } + + /// + /// Gets the device name + /// + string DeviceName { get; } + + /// + /// Gets the public key + /// + string PublicKey { get; } + + /// + /// Gets the time when the key was added + /// + DateTime AddedAt { get; } +} diff --git a/KitX Core Contracts/KitX.Core.Contract/Configuration/IWebConfig.cs b/KitX Core Contracts/KitX.Core.Contract/Configuration/IWebConfig.cs new file mode 100644 index 0000000..ceb0887 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Configuration/IWebConfig.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; + +namespace KitX.Core.Contract.Configuration; + +/// +/// Web configuration section +/// +public interface IWebConf +{ + double DelayStartSeconds { get; set; } + string ApiServer { get; set; } + string ApiPath { get; set; } + int DevicesViewRefreshDelay { get; set; } + List? AcceptedNetworkInterfaces { get; set; } + int? UserSpecifiedDevicesServerPort { get; set; } + int? UserSpecifiedPluginsServerPort { get; set; } + int UdpPortSend { get; set; } + int UdpPortReceive { get; set; } + int UdpSendFrequency { get; set; } + string UdpBroadcastAddress { get; set; } + string IPFilter { get; set; } + int SocketBufferSize { get; set; } + int DeviceInfoTTLSeconds { get; set; } + bool DisableRemovingOfflineDeviceCard { get; set; } + string UpdateServer { get; set; } + string UpdatePath { get; set; } + string UpdateDownloadPath { get; set; } + string UpdateChannel { get; set; } + string UpdateSource { get; set; } + int DebugServicesServerPort { get; set; } +} diff --git a/KitX Core Contracts/KitX.Core.Contract/Configuration/IWindowsConfig.cs b/KitX Core Contracts/KitX.Core.Contract/Configuration/IWindowsConfig.cs new file mode 100644 index 0000000..7d05b26 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Configuration/IWindowsConfig.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using Common.BasicHelper.Graphics.Screen; + +namespace KitX.Core.Contract.Configuration; + +/// +/// Windows configuration section +/// +public interface IWindowsConf +{ + IMainWindowConf MainWindow { get; set; } + IAnnouncementWindowConf AnnouncementWindow { get; set; } +} + +/// +/// Main window configuration +/// +public interface IMainWindowConf +{ + Resolution Size { get; set; } + Distances Location { get; set; } + WindowState WindowState { get; set; } + bool IsHidden { get; set; } + Dictionary Tags { get; set; } + bool EnabledMica { get; set; } + int GreetingTextCount_Morning { get; set; } + int GreetingTextCount_Noon { get; set; } + int GreetingTextCount_AfterNoon { get; set; } + int GreetingTextCount_Evening { get; set; } + int GreetingTextCount_Night { get; set; } + int GreetingUpdateInterval { get; set; } +} + +/// +/// Announcement window configuration +/// +public interface IAnnouncementWindowConf +{ + Resolution Size { get; set; } + Distances Location { get; set; } +} diff --git a/KitX Core Contracts/KitX.Core.Contract/Configuration/NavigationViewPaneDisplayMode.cs b/KitX Core Contracts/KitX.Core.Contract/Configuration/NavigationViewPaneDisplayMode.cs new file mode 100644 index 0000000..605e73f --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Configuration/NavigationViewPaneDisplayMode.cs @@ -0,0 +1,13 @@ +namespace KitX.Core.Contract.Configuration; + +/// +/// Navigation view pane display mode enum +/// +public enum NavigationViewPaneDisplayMode +{ + Auto = 0, + Left = 1, + Top = 2, + LeftCompact = 3, + LeftMinimal = 4 +} diff --git a/KitX Core Contracts/KitX.Core.Contract/Configuration/WindowState.cs b/KitX Core Contracts/KitX.Core.Contract/Configuration/WindowState.cs new file mode 100644 index 0000000..8ffc493 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Configuration/WindowState.cs @@ -0,0 +1,15 @@ +using System; + +namespace KitX.Core.Contract.Configuration; + +/// +/// Window state enumeration (mirrors Avalonia.WindowState) +/// +public enum WindowState +{ + Normal, + Minimized, + Maximized, + FullScreen, + NonInteractive +} diff --git a/KitX Core Contracts/KitX.Core.Contract/Security/ISecurityService.cs b/KitX Core Contracts/KitX.Core.Contract/Security/ISecurityService.cs index 4cd6f23..ba4b890 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Security/ISecurityService.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Security/ISecurityService.cs @@ -3,6 +3,8 @@ using System.ComponentModel; using System.Threading.Tasks; using KitX.Shared.CSharp.Device; +using KitX.Shared.CSharp.Security; +using KitXIDeviceKey = KitX.Core.Contract.Configuration.IDeviceKey; namespace KitX.Core.Contract.Security; @@ -14,7 +16,7 @@ public interface ISecurityService /// /// Gets all device keys /// - IReadOnlyList GetDeviceKeys(); + IReadOnlyList GetDeviceKeys(); /// /// Adds a device key @@ -32,6 +34,21 @@ public interface ISecurityService /// True if removal was successful bool RemoveDeviceKey(string macAddress); + /// + /// Searches for a device key by device locator + /// + /// The device locator + /// The device key if found, otherwise null + DeviceKey? SearchDeviceKey(DeviceLocator locator); + + /// + /// Checks if a device key is correct + /// + /// The device locator + /// The device key to verify + /// True if the key is correct + bool IsDeviceKeyCorrect(DeviceLocator locator, DeviceKey key); + /// /// Checks if a device is authorized /// @@ -39,6 +56,12 @@ public interface ISecurityService /// True if the device is authorized bool IsDeviceAuthorized(DeviceLocator device); + /// + /// Gets the private device key for local device + /// + /// The private device key, or null if not available + DeviceKey? GetPrivateDeviceKey(); + /// /// Encrypts a string /// @@ -56,35 +79,65 @@ public interface ISecurityService Task DecryptStringAsync(string encryptedContent, string sourceDeviceMacAddress); /// - /// Computes a hash + /// Encrypts a string using RSA with a specific device's public key /// - /// The content to hash - /// The hash - string ComputeHash(string content); -} + /// The device key containing the public key + /// The data to encrypt + /// The encrypted data as Base64 string + string? RsaEncryptString(DeviceKey key, string data); -/// -/// Device key interface -/// -public interface IDeviceKey -{ /// - /// Gets the MAC address + /// Decrypts a string using RSA with a specific device's private key /// - string MacAddress { get; } + /// The device key containing the private key + /// The encrypted data as Base64 string + /// The decrypted data + string? RsaDecryptString(DeviceKey key, string encryptedData); /// - /// Gets the device name + /// Encrypts content using RSA+AES hybrid encryption /// - string DeviceName { get; } + /// The device key + /// The content to encrypt + /// The encrypted content + EncryptedContent RsaEncryptContent(DeviceKey key, string content); + + /// + /// Decrypts content using RSA+AES hybrid decryption + /// + /// The device key + /// The encrypted content + /// The decrypted content + string RsaDecryptContent(DeviceKey key, EncryptedContent content); + + /// + /// Encrypts a string with AES + /// + /// The source string + /// The encryption key + /// The encrypted string + string AesEncrypt(string source, string key); + + /// + /// Decrypts a string with AES + /// + /// The source string + /// The decryption key + /// Whether the source is in Base64 + /// The decrypted string + string AesDecrypt(string source, string key, bool isSourceInBase64 = true); /// - /// Gets the public key + /// Computes a hash /// - string PublicKey { get; } + /// The content to hash + /// The hash + string ComputeHash(string content); /// - /// Gets the time when the key was added + /// Computes SHA1 hash of a string /// - DateTime AddedAt { get; } + /// The data to hash + /// The SHA1 hash string + string GetSHA1(string data); } From 05cc000900c0dd6ba6a0a451084f28e9a19d93e5 Mon Sep 17 00:00:00 2001 From: StarInk Date: Sun, 15 Mar 2026 14:28:39 +0100 Subject: [PATCH 21/60] =?UTF-8?q?=F0=9F=92=BE=20Feat(Kscript.CSharp.Parser?= =?UTF-8?q?):=20=E4=BD=BF=E7=94=A8=E7=94=B1=E6=8F=92=E4=BB=B6=E5=8E=9F?= =?UTF-8?q?=E5=A7=8B=E8=AE=BE=E7=BD=AE=E7=9A=84=E5=8F=82=E6=95=B0=E5=90=8D?= =?UTF-8?q?=E7=A7=B0=E8=80=8C=E4=B8=8D=E6=98=AF=E7=B4=A2=E5=BC=95=E4=BD=9C?= =?UTF-8?q?=E4=B8=BA=E5=8F=82=E6=95=B0=E5=90=8D=E7=A7=B0=EF=BC=9B=E9=80=82?= =?UTF-8?q?=E9=85=8D=E5=8F=82=E6=95=B0=E7=9A=84IsOptional=E5=AD=97?= =?UTF-8?q?=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CodeGen/MethodEmitter.cs | 42 ++++++++++++++++--- .../Core/MockPluginManager.cs | 14 +++++-- .../Core/RealPluginManager.cs | 12 +++--- .../Models/PluginCallInfo.cs | 12 +++++- 4 files changed, 62 insertions(+), 18 deletions(-) diff --git a/KitX Script/Kscript.CSharp.Parser/CodeGen/MethodEmitter.cs b/KitX Script/Kscript.CSharp.Parser/CodeGen/MethodEmitter.cs index e732fb4..a044907 100644 --- a/KitX Script/Kscript.CSharp.Parser/CodeGen/MethodEmitter.cs +++ b/KitX Script/Kscript.CSharp.Parser/CodeGen/MethodEmitter.cs @@ -1,6 +1,7 @@ using Kscript.CSharp.Parser.Core; using Kscript.CSharp.Parser.Exceptions; using Kscript.CSharp.Parser.Models; +using KitX.Shared.CSharp.Plugin; using System.Reflection.Emit; using System.Runtime.Loader; using System.Collections.Concurrent; @@ -105,6 +106,7 @@ public static Assembly GenerateAssembly(List plugins, } catch (Exception ex) { + Console.WriteLine($"[MethodEmitter] 生成程序集失败: {assemblyName}, 详细错误: {ex}"); throw new ParserException($"生成程序集失败: {assemblyName}", ex); } } @@ -227,13 +229,26 @@ private static void GeneratePluginMethod(TypeBuilder typeBuilder, string pluginN returnType, parameterTypes); - // 设置参数名称 + // 设置参数名称和属性 for (int i = 0; i < function.Parameters.Count; i++) { var param = function.Parameters[i]; - var paramBuilder = methodBuilder.DefineParameter(i + 1, ParameterAttributes.None, param.Name); - // 如果参数是可选的,设置默认值 + // 根据 IsOptional 属性确定参数属性 + var paramAttributes = ParameterAttributes.None; + if (param.IsOptional) + { + paramAttributes |= ParameterAttributes.Optional; + // 如果有默认值,设置 HasDefault 标志 + if (!string.IsNullOrEmpty(param.Value)) + { + paramAttributes |= ParameterAttributes.HasDefault; + } + } + + var paramBuilder = methodBuilder.DefineParameter(i + 1, paramAttributes, param.Name); + + // 如果参数是可选的且有默认值,设置默认值 if (param.IsOptional && !string.IsNullOrEmpty(param.Value)) { var defaultValue = ConvertDefaultValue(param.Value, parameterTypes[i]); @@ -265,6 +280,7 @@ private static void GenerateMethodBody(MethodBuilder methodBuilder, string plugi var callInfoLocal = il.DeclareLocal(typeof(PluginCallInfo)); var parametersArrayLocal = il.DeclareLocal(typeof(object[])); var parameterTypesArrayLocal = il.DeclareLocal(typeof(Type[])); + var parameterNamesArrayLocal = il.DeclareLocal(typeof(string[])); LocalBuilder? resultLocal = null; if (returnType != typeof(void)) @@ -272,7 +288,7 @@ private static void GenerateMethodBody(MethodBuilder methodBuilder, string plugi resultLocal = il.DeclareLocal(returnType); } - // 创建参数数组 + // 创建参数值数组 il.Emit(OpCodes.Ldc_I4, parameterTypes.Length); il.Emit(OpCodes.Newarr, typeof(object)); il.Emit(OpCodes.Stloc, parametersArrayLocal); @@ -282,9 +298,16 @@ private static void GenerateMethodBody(MethodBuilder methodBuilder, string plugi il.Emit(OpCodes.Newarr, typeof(Type)); il.Emit(OpCodes.Stloc, parameterTypesArrayLocal); - // 填充参数数组和类型数组 + // 创建参数名称数组 + il.Emit(OpCodes.Ldc_I4, parameterTypes.Length); + il.Emit(OpCodes.Newarr, typeof(string)); + il.Emit(OpCodes.Stloc, parameterNamesArrayLocal); + + // 填充参数数组、类型数组和名称数组 for (int i = 0; i < parameterTypes.Length; i++) { + var param = function.Parameters[i]; + // 填充参数值 il.Emit(OpCodes.Ldloc, parametersArrayLocal); il.Emit(OpCodes.Ldc_I4, i); @@ -304,6 +327,12 @@ private static void GenerateMethodBody(MethodBuilder methodBuilder, string plugi il.Emit(OpCodes.Ldtoken, parameterTypes[i]); il.Emit(OpCodes.Call, typeof(Type).GetMethod("GetTypeFromHandle")!); il.Emit(OpCodes.Stelem_Ref); + + // 填充参数名称 + il.Emit(OpCodes.Ldloc, parameterNamesArrayLocal); + il.Emit(OpCodes.Ldc_I4, i); + il.Emit(OpCodes.Ldstr, param.Name ?? $"param{i}"); + il.Emit(OpCodes.Stelem_Ref); } // 创建 PluginCallInfo 实例 @@ -311,9 +340,10 @@ private static void GenerateMethodBody(MethodBuilder methodBuilder, string plugi il.Emit(OpCodes.Ldstr, function.Name); il.Emit(OpCodes.Ldloc, parametersArrayLocal); il.Emit(OpCodes.Ldloc, parameterTypesArrayLocal); + il.Emit(OpCodes.Ldloc, parameterNamesArrayLocal); il.Emit(OpCodes.Newobj, typeof(PluginCallInfo).GetConstructor(new[] { - typeof(string), typeof(string), typeof(object[]), typeof(Type[]) + typeof(string), typeof(string), typeof(object[]), typeof(Type[]), typeof(string[]) })!); il.Emit(OpCodes.Stloc, callInfoLocal); diff --git a/KitX Script/Kscript.CSharp.Parser/Core/MockPluginManager.cs b/KitX Script/Kscript.CSharp.Parser/Core/MockPluginManager.cs index ecc1710..2fadfa8 100644 --- a/KitX Script/Kscript.CSharp.Parser/Core/MockPluginManager.cs +++ b/KitX Script/Kscript.CSharp.Parser/Core/MockPluginManager.cs @@ -1,4 +1,5 @@ using Kscript.CSharp.Parser.Models; +using KitX.Shared.CSharp.Plugin; namespace Kscript.CSharp.Parser.Core; @@ -42,7 +43,12 @@ public class MockPluginManager : IPluginManager public T Call(PluginCallInfo callInfo) { Console.WriteLine($"[MockPluginManager] 调用插件方法: {callInfo}"); - Console.WriteLine($"[MockPluginManager] 参数类型: [{string.Join(", ", callInfo.ParameterTypes.Select(t => t.Name))}]"); + var paramInfo = callInfo.Parameters.Select((p, i) => { + var name = callInfo.ParameterNames?.Length > i ? callInfo.ParameterNames[i] : $"param{i}"; + var type = callInfo.ParameterTypes?.Length > i ? callInfo.ParameterTypes[i].Name : "object"; + return $"{name}:{type}={p}"; + }); + Console.WriteLine($"[MockPluginManager] 参数: [{string.Join(", ", paramInfo)}]"); Console.WriteLine($"[MockPluginManager] 期望返回类型: {typeof(T).Name}"); // 简单的模拟实现 @@ -143,11 +149,11 @@ public bool IsMethodExists(string pluginName, string methodName) switch (callInfo.MethodName) { case "Reverse" when callInfo.Parameters.Length >= 1: - return new string(callInfo.Parameters[0].ToString()?.Reverse().ToArray() ?? Array.Empty()); + return new string(callInfo.Parameters[0]?.ToString()?.Reverse().ToArray() ?? Array.Empty()); case "ToUpper" when callInfo.Parameters.Length >= 1: - return callInfo.Parameters[0].ToString()?.ToUpperInvariant() ?? string.Empty; + return callInfo.Parameters[0]?.ToString()?.ToUpperInvariant() ?? string.Empty; case "Concat" when callInfo.Parameters.Length >= 2: - return callInfo.Parameters[0].ToString() + callInfo.Parameters[1].ToString(); + return callInfo.Parameters[0]?.ToString() + callInfo.Parameters[1]?.ToString(); } break; } diff --git a/KitX Script/Kscript.CSharp.Parser/Core/RealPluginManager.cs b/KitX Script/Kscript.CSharp.Parser/Core/RealPluginManager.cs index 1f7d94c..e2b330c 100644 --- a/KitX Script/Kscript.CSharp.Parser/Core/RealPluginManager.cs +++ b/KitX Script/Kscript.CSharp.Parser/Core/RealPluginManager.cs @@ -154,15 +154,15 @@ private async Task SendPluginRequest(object connector, PluginCallInfo call var functionArgs = new List(); for (int i = 0; i < callInfo.Parameters.Length; i++) { - var paramType = callInfo.ParameterTypes[i]; - var paramValue = callInfo.Parameters[i]; + var paramValue = callInfo.Parameters[i]?.ToString() ?? string.Empty; + var paramName = callInfo.ParameterNames?.Length > i ? callInfo.ParameterNames[i] : $"param{i}"; + var paramType = callInfo.ParameterTypes?.Length > i ? callInfo.ParameterTypes[i].Name : "string"; functionArgs.Add(new Parameter { - Name = $"param{i}", - Type = paramType.Name, - // TODO: 等待KitX数据传输标准制定完成后,替换为标准序列化方法 - Value = paramValue?.ToString() ?? string.Empty, + Name = paramName, + Type = paramType, + Value = paramValue, IsOptional = false }); } diff --git a/KitX Script/Kscript.CSharp.Parser/Models/PluginCallInfo.cs b/KitX Script/Kscript.CSharp.Parser/Models/PluginCallInfo.cs index 3cc0cc6..ee55d67 100644 --- a/KitX Script/Kscript.CSharp.Parser/Models/PluginCallInfo.cs +++ b/KitX Script/Kscript.CSharp.Parser/Models/PluginCallInfo.cs @@ -1,3 +1,5 @@ +using KitX.Shared.CSharp.Plugin; + namespace Kscript.CSharp.Parser.Models; /// @@ -16,7 +18,7 @@ public class PluginCallInfo public string MethodName { get; set; } = string.Empty; /// - /// 方法参数数组 + /// 方法参数值数组 /// public object[] Parameters { get; set; } = Array.Empty(); @@ -25,16 +27,22 @@ public class PluginCallInfo /// public Type[] ParameterTypes { get; set; } = Array.Empty(); + /// + /// 参数名称数组 + /// + public string[] ParameterNames { get; set; } = Array.Empty(); + public PluginCallInfo() { } - public PluginCallInfo(string pluginName, string methodName, object[] parameters, Type[] parameterTypes) + public PluginCallInfo(string pluginName, string methodName, object[] parameters, Type[] parameterTypes, string[] parameterNames) { PluginName = pluginName; MethodName = methodName; Parameters = parameters; ParameterTypes = parameterTypes; + ParameterNames = parameterNames; } public override string ToString() From bc08809aa9e37c84a289193eb32237c9c7e48683 Mon Sep 17 00:00:00 2001 From: StarInk Date: Wed, 18 Mar 2026 19:27:00 +0100 Subject: [PATCH 22/60] =?UTF-8?q?=F0=9F=92=BE=20Feat(Workflow):=20?= =?UTF-8?q?=E8=BF=9B=E4=B8=80=E6=AD=A5=E5=AE=9E=E7=8E=B0Workflow=E8=84=9A?= =?UTF-8?q?=E6=9C=AC=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Workflow/IWorkflowService.cs | 41 +++++ .../Workflow/KcsFileFormat.cs | 142 ++++++++++++++++++ 2 files changed, 183 insertions(+) create mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/KcsFileFormat.cs diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowService.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowService.cs index 930b699..c74fcb7 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowService.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowService.cs @@ -73,6 +73,47 @@ public interface IWorkflowService /// /// Plugin list void UpdateAvailablePlugins(List plugins); + + /// + /// 从代码中解析常量 + /// + /// 代码内容 + /// 常量列表 + List ParseConstantsFromCode(string code); + + /// + /// 应用常量到代码 + /// + /// 原始代码 + /// 常量列表 + /// 应用常量后的代码 + string ApplyConstantsToCode(string code, List constants); + + /// + /// 合并辅助函数到代码 + /// + /// 主程序代码 + /// 辅助函数列表 + /// 合并后的完整代码 + string MergeHelperFunctions(string mainCode, List helperFunctions); + + /// + /// 执行KCS代码 - 包含代码分析、常量应用、辅助函数合并 + /// + /// 主程序代码 + /// 辅助函数列表 + /// 可变常量列表 + /// 需要的插件 + /// 是否包含时间戳 + /// 取消令牌 + /// 执行结果 + Task ExecuteKcsCodesAsync( + string mainCode, + List helperFunctions, + List constants, + List? requiredPlugins = null, + bool includeTimestamp = true, + System.Threading.CancellationToken cancellationToken = default); } /// diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/KcsFileFormat.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/KcsFileFormat.cs new file mode 100644 index 0000000..81c2648 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/KcsFileFormat.cs @@ -0,0 +1,142 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace KitX.Core.Contract.Workflow; + +/// +/// KCS (KitX Code Script) 文件格式定义 +/// +public class KcsFileFormat +{ + /// + /// 主程序代码 + /// + public string MainProgram { get; set; } = string.Empty; + + /// + /// 辅助函数列表 + /// + public List HelperFunctions { get; set; } = []; + + /// + /// 可变常量及其用户修改后的值 + /// + public Dictionary VariableConstants { get; set; } = []; +} + +/// +/// 辅助函数参数定义 +/// +public class HelperFunctionParameter +{ + /// + /// 参数名称 + /// + public string Name { get; set; } = string.Empty; + + /// + /// 参数类型 + /// + public string Type { get; set; } = "object"; +} + +/// +/// 辅助函数定义 +/// +public class HelperFunction +{ + /// + /// 函数名称 + /// + public string Name { get; set; } = string.Empty; + + /// + /// 函数参数列表 + /// + public List Parameters { get; set; } = []; + + /// + /// 返回值类型 + /// + public string ReturnType { get; set; } = "object"; + + /// + /// 函数体代码(不包括函数签名) + /// + public string Code { get; set; } = string.Empty; +} + +/// +/// 可变常量(用于UI显示) +/// +public class VariableConstant +{ + /// + /// 常量名称 + /// + public string Name { get; set; } = string.Empty; + + /// + /// 默认值 + /// + public object? DefaultValue { get; set; } + + /// + /// 用户修改后的值 + /// + public object? UserValue { get; set; } + + /// + /// 数据类型 + /// + public string Type { get; set; } = "string"; +} + +/// +/// 主程序代码分析结果 +/// +public class MainProgramAnalysisResult +{ + /// + /// 是否有效 + /// + public bool IsValid { get; set; } = true; + + /// + /// 禁止原因 + /// + public string ForbiddenReason { get; set; } = string.Empty; +} + +/// +/// KCS文件服务接口 - 仅负责KCS文件的读写 +/// +public interface IKcsFileService +{ + /// + /// 加载KCS文件 + /// + /// 文件路径 + /// KCS文件内容 + Task LoadKcsFileAsync(string filePath); + + /// + /// 保存KCS文件 + /// + /// 文件路径 + /// KCS文件内容 + Task SaveKcsFileAsync(string filePath, KcsFileFormat kcs); +} + +/// +/// 主程序代码分析器接口 +/// +public interface IMainProgramAnalyzer +{ + /// + /// 分析主程序代码 + /// + /// 要分析的代码 + /// 分析结果 + MainProgramAnalysisResult Analyze(string code); +} From 13ae5c66502b25833b18fb474380b7fb6a39a3bb Mon Sep 17 00:00:00 2001 From: StarInk Date: Thu, 19 Mar 2026 07:01:42 +0100 Subject: [PATCH 23/60] =?UTF-8?q?=F0=9F=92=BE=20Feat(Workflow):=20?= =?UTF-8?q?=E4=B8=BAblock=20script=E7=89=B9=E6=80=A7=E9=A2=84=E8=AE=BE?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../KitX.Core.Contract/Tasks/ITasksService.cs | 10 + .../Workflow/BlockScriptModels.cs | 383 ++++++++++++++++++ .../Workflow/IBlockScriptParser.cs | 133 ++++++ .../Workflow/KcsFileFormat.cs | 13 + 4 files changed, 539 insertions(+) create mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/BlockScriptModels.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/IBlockScriptParser.cs diff --git a/KitX Core Contracts/KitX.Core.Contract/Tasks/ITasksService.cs b/KitX Core Contracts/KitX.Core.Contract/Tasks/ITasksService.cs index 1959f30..e2b3f3a 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Tasks/ITasksService.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Tasks/ITasksService.cs @@ -1,4 +1,5 @@ using System; +using System.Threading; using System.Threading.Tasks; namespace KitX.Core.Contract.Tasks; @@ -22,4 +23,13 @@ public interface ITasksService /// Optional task name /// Task representing the async operation Task RunTaskAsync(Func task, string? taskName = null); + + /// + /// Runs an asynchronous task with cancellation support + /// + /// The task to run + /// Cancellation token + /// Optional task name + /// Task representing the async operation + Task RunTaskAsync(Func task, CancellationToken cancellationToken, string? taskName = null); } diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/BlockScriptModels.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/BlockScriptModels.cs new file mode 100644 index 0000000..57dcb5b --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/BlockScriptModels.cs @@ -0,0 +1,383 @@ +using System; +using System.Collections.Generic; + +namespace KitX.Core.Contract.Workflow; + +/// +/// Block type enumeration +/// +public enum BlockType +{ + /// + /// Constants block - variables are globally scoped and read-only + /// + ConstBlock, + + /// + /// Main block - entry point, local scope + /// + MainBlock, + + /// + /// Named block - local scope, can be called by name + /// + NamedBlock, + + /// + /// Public variable block - globally scoped and writable, but not exposed in UI editor + /// + PubVarBlock +} + +/// +/// Represents a single block definition in the script +/// +public class BlockDefinition +{ + /// + /// Block type + /// + public BlockType Type { get; set; } + + /// + /// Block name (for NamedBlock) + /// + public string Name { get; set; } = string.Empty; + + /// + /// Variable declarations in this block + /// + public List Variables { get; set; } = []; + + /// + /// Statements in this block + /// + public List Statements { get; set; } = []; + + /// + /// Line number in source where this block starts + /// + public int LineNumber { get; set; } +} + +/// +/// Variable declaration +/// +public class VariableDeclaration +{ + /// + /// Variable name + /// + public string Name { get; set; } = string.Empty; + + /// + /// Variable type as string + /// + public string Type { get; set; } = "object"; + + /// + /// Initial value expression as string (for evaluation at parse time or execution time) + /// + public string? InitialValueExpression { get; set; } + + /// + /// Default value (pre-evaluated for const block) + /// + public object? DefaultValue { get; set; } +} + +/// +/// Base class for statements within a block +/// +public abstract class BlockStatement +{ + /// + /// Line number in source + /// + public int LineNumber { get; set; } + + /// + /// Original source code for this statement + /// + public string SourceCode { get; set; } = string.Empty; +} + +/// +/// Expression statement +/// +public class ExpressionStatement : BlockStatement +{ + /// + /// The expression to execute + /// + public string Expression { get; set; } = string.Empty; +} + +/// +/// Variable declaration statement +/// +public class VariableDeclarationStatement : BlockStatement +{ + /// + /// The variable declaration + /// + public VariableDeclaration Declaration { get; set; } = new(); +} + +/// +/// Flow control statement types +/// +public enum FlowControlType +{ + /// + /// Branch to another block based on condition + /// + Branch, + + /// + /// Loop back to a block while condition is true + /// + Loop, + + /// + /// Return from script execution + /// + Return, + + /// + /// Break from current loop + /// + Break, + + /// + /// Continue to next iteration + /// + Continue +} + +/// +/// Flow control statement +/// +public class FlowControlStatement : BlockStatement +{ + /// + /// Type of flow control + /// + public FlowControlType ControlType { get; set; } + + /// + /// Condition expression (for Branch/Loop) + /// + public string ConditionExpression { get; set; } = string.Empty; + + /// + /// Target block name when condition is true (for Branch) + /// + public string TrueBlockName { get; set; } = string.Empty; + + /// + /// Target block name when condition is false (for Branch) + /// + public string FalseBlockName { get; set; } = string.Empty; + + /// + /// Loop body block name (for Loop) + /// + public string? LoopBlockName { get; set; } +} + +/// +/// Parsed block script container +/// +public class BlockScript +{ + /// + /// Global constants block (ConstBlock) + /// + public BlockDefinition? ConstBlock { get; set; } + + /// + /// Public variables block (PubVarBlock) - optional + /// + public BlockDefinition? PubVarBlock { get; set; } + + /// + /// Main entry block (MainBlock) + /// + public BlockDefinition? MainBlock { get; set; } + + /// + /// Named blocks dictionary by name + /// + public Dictionary NamedBlocks { get; set; } = []; + + /// + /// All blocks in order of appearance + /// + public List AllBlocks { get; set; } = []; + + /// + /// Raw source code + /// + public string SourceCode { get; set; } = string.Empty; +} + +/// +/// Result of parsing operation +/// +public class BlockScriptParseResult +{ + /// + /// Whether parsing was successful + /// + public bool IsSuccess { get; set; } + + /// + /// Error message if parsing failed + /// + public string? ErrorMessage { get; set; } + + /// + /// Line number where error occurred + /// + public int ErrorLine { get; set; } + + /// + /// The parsed script if successful + /// + public BlockScript? Script { get; set; } +} + +/// +/// Validation result for block scripts +/// +public class BlockScriptValidationResult +{ + /// + /// Whether the script is valid + /// + public bool IsValid { get; set; } = true; + + /// + /// List of errors found + /// + public List Errors { get; set; } = []; + + /// + /// List of warnings + /// + public List Warnings { get; set; } = []; + + /// + /// Adds an error + /// + public void AddError(string error) => Errors.Add(error); + + /// + /// Adds a warning + /// + public void AddWarning(string warning) => Warnings.Add(warning); +} + +/// +/// Execution result for block scripts +/// +public class BlockScriptExecutionResult +{ + /// + /// Whether execution was successful + /// + public bool IsSuccess { get; set; } + + /// + /// Return value from script (if any) + /// + public object? ReturnValue { get; set; } + + /// + /// Error message if execution failed + /// + public string? ErrorMessage { get; set; } + + /// + /// Output from Print() calls + /// + public List Output { get; set; } = []; + + /// + /// Number of blocks executed + /// + public int ExecutedBlockCount { get; set; } + + /// + /// Execution time in milliseconds + /// + public long ExecutionTimeMs { get; set; } +} + +/// +/// Flow control result returned by built-in functions +/// +public class FlowResult +{ + /// + /// Type of flow result + /// + public FlowResultType Type { get; set; } + + /// + /// Target block name for jumps + /// + public string? TargetBlock { get; set; } + + /// + /// Optional value (for return) + /// + public object? Value { get; set; } + + /// + /// Whether execution should continue + /// + public bool ShouldContinue { get; set; } = true; + + /// + /// Continue to next statement + /// + public static FlowResult Continue() => new() { Type = FlowResultType.Continue, ShouldContinue = true }; + + /// + /// Return with a value + /// + public static FlowResult Return(object? value = null) => new() { Type = FlowResultType.Return, Value = value, ShouldContinue = false }; + + /// + /// Break from loop + /// + public static FlowResult Break() => new() { Type = FlowResultType.Break, ShouldContinue = false }; +} + +/// +/// Flow result types +/// +public enum FlowResultType +{ + /// + /// Continue to next statement + /// + Continue, + + /// + /// Return from script + /// + Return, + + /// + /// Break from loop + /// + Break, + + /// + /// Continue to next loop iteration + /// + ContinueLoop +} diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/IBlockScriptParser.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/IBlockScriptParser.cs new file mode 100644 index 0000000..b371106 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/IBlockScriptParser.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace KitX.Core.Contract.Workflow; + +/// +/// Block script parser interface - parses C# scripts with block attributes +/// +public interface IBlockScriptParser +{ + /// + /// Parses a block-based script from source code + /// + /// The C# source code with block attributes + /// Parsed block script result + BlockScriptParseResult Parse(string sourceCode); + + /// + /// Parses a block-based script from source code asynchronously + /// + Task ParseAsync(string sourceCode); + + /// + /// Validates block script syntax and structure + /// + BlockScriptValidationResult Validate(string sourceCode); +} + +/// +/// Block script executor interface - executes parsed block scripts +/// +public interface IBlockScriptExecutor +{ + /// + /// Executes a block script + /// + /// The parsed block script + /// Input parameters + /// Cancellation token + /// Execution result + Task ExecuteAsync( + BlockScript script, + Dictionary? parameters = null, + CancellationToken cancellationToken = default); + + /// + /// Executes a specific block by name + /// + Task ExecuteBlockAsync( + BlockScript script, + string blockName, + Dictionary? parameters = null, + CancellationToken cancellationToken = default); + + /// + /// Validates a block script + /// + BlockScriptValidationResult Validate(BlockScript script); +} + +/// +/// Block scope manager interface - manages variable scoping +/// +public interface IBlockScopeManager +{ + /// + /// Gets the global (ConstBlock) scope + /// + IBlockScope GlobalScope { get; } + + /// + /// Creates a new local scope for a block + /// + IBlockScope CreateLocalScope(string blockName); + + /// + /// Resolves a variable name to its value (searches local then global) + /// + object? ResolveVariable(string name); + + /// + /// Sets a variable value in the appropriate scope + /// + void SetVariable(string name, object? value, bool global = false); + + /// + /// Checks if a variable exists in any scope + /// + bool HasVariable(string name); + + /// + /// Clears all local scopes (called between executions) + /// + void ClearLocalScopes(); +} + +/// +/// Variable scope interface +/// +public interface IBlockScope +{ + /// + /// Name of the block this scope belongs to + /// + string BlockName { get; } + + /// + /// Whether this is the global scope + /// + bool IsGlobal { get; } + + /// + /// Gets a variable value + /// + object? GetVariable(string name); + + /// + /// Sets a variable value + /// + void SetVariable(string name, object? value); + + /// + /// Checks if a variable exists in this scope + /// + bool HasVariable(string name); + + /// + /// Gets all variables in this scope + /// + Dictionary GetAllVariables(); +} diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/KcsFileFormat.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/KcsFileFormat.cs index 81c2648..13786fe 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/KcsFileFormat.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/KcsFileFormat.cs @@ -22,6 +22,19 @@ public class KcsFileFormat /// 可变常量及其用户修改后的值 /// public Dictionary VariableConstants { get; set; } = []; + + /// + /// 是否使用块脚本模式 + /// + /// + /// 当为 true 时,使用 BlockScriptSource 作为脚本内容 + /// + public bool UseBlockMode { get; set; } = false; + + /// + /// 块脚本源代码(当 UseBlockMode 为 true 时使用) + /// + public string? BlockScriptSource { get; set; } } /// From eba7ee42dae17b00ca35490dbf69de9752dceea0 Mon Sep 17 00:00:00 2001 From: StarInk Date: Thu, 19 Mar 2026 11:08:32 +0100 Subject: [PATCH 24/60] =?UTF-8?q?=F0=9F=92=BE=20Feat(Workflow):=20?= =?UTF-8?q?=E4=B8=BAblock=20script=E7=89=B9=E6=80=A7=E9=A2=84=E8=AE=BE?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Workflow/IWorkflowService.cs | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowService.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowService.cs index c74fcb7..e980b52 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowService.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowService.cs @@ -114,6 +114,51 @@ public interface IWorkflowService List? requiredPlugins = null, bool includeTimestamp = true, System.Threading.CancellationToken cancellationToken = default); + + // ========== Block Script Methods ========== + + /// + /// 解析块脚本 + /// + /// 块脚本源代码 + /// 解析结果 + BlockScriptParseResult ParseBlockScript(string sourceCode); + + /// + /// 异步解析块脚本 + /// + Task ParseBlockScriptAsync(string sourceCode); + + /// + /// 验证块脚本 + /// + /// 块脚本源代码 + /// 验证结果 + BlockScriptValidationResult ValidateBlockScript(string sourceCode); + + /// + /// 执行块脚本 + /// + /// 解析后的块脚本 + /// 输入参数 + /// 取消令牌 + /// 执行结果 + Task ExecuteBlockScriptAsync( + BlockScript script, + Dictionary? parameters = null, + System.Threading.CancellationToken cancellationToken = default); + + /// + /// 从块脚本源代码执行 + /// + /// 块脚本源代码 + /// 输入参数 + /// 取消令牌 + /// 执行结果 + Task ExecuteBlockScriptAsync( + string sourceCode, + Dictionary? parameters = null, + System.Threading.CancellationToken cancellationToken = default); } /// From 314b184152ba065c18da81985a37b738a5eb06eb Mon Sep 17 00:00:00 2001 From: StarInk Date: Tue, 24 Mar 2026 23:41:29 +0100 Subject: [PATCH 25/60] =?UTF-8?q?=F0=9F=92=BE=20Feat(Workflow):=20?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=9D=97=E8=84=9A=E6=9C=AC=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E5=92=8C=E6=8E=A5=E5=8F=A3=EF=BC=8C=E6=B7=BB=E5=8A=A0=E5=BE=AA?= =?UTF-8?q?=E7=8E=AF=E5=9D=97=E6=94=AF=E6=8C=81=E5=8F=8A=E6=89=A7=E8=A1=8C?= =?UTF-8?q?=E7=BB=93=E6=9E=9C=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Workflow/BlockScriptModels.cs | 146 ++++++++++------- .../Workflow/IBlockScriptParser.cs | 14 -- .../Workflow/IWorkflowService.cs | 148 ++++++------------ 3 files changed, 142 insertions(+), 166 deletions(-) diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/BlockScriptModels.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/BlockScriptModels.cs index 57dcb5b..9b2ef40 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/BlockScriptModels.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/BlockScriptModels.cs @@ -26,7 +26,12 @@ public enum BlockType /// /// Public variable block - globally scoped and writable, but not exposed in UI editor /// - PubVarBlock + PubVarBlock, + + /// + /// Loop block - auto-generated block containing a Loop statement + /// + LoopBlock } /// @@ -50,7 +55,7 @@ public class BlockDefinition public List Variables { get; set; } = []; /// - /// Statements in this block + /// Statements in this block (excluding Loop statements, which are separated) /// public List Statements { get; set; } = []; @@ -58,6 +63,17 @@ public class BlockDefinition /// Line number in source where this block starts /// public int LineNumber { get; set; } + + /// + /// Name of the next block to execute when this block ends naturally + /// (i.e., not ended by Branch/Loop/LoopBodyEnd) + /// + public string? NextBlockName { get; set; } + + /// + /// For LoopBlock: the block name containing this loop (i.e., the parent block) + /// + public string? ParentBlockName { get; set; } } /// @@ -135,7 +151,7 @@ public enum FlowControlType Branch, /// - /// Loop back to a block while condition is true + /// Loop while condition is true (Loop has three args: condition, trueBlock, falseBlock) /// Loop, @@ -150,9 +166,9 @@ public enum FlowControlType Break, /// - /// Continue to next iteration + /// Loop body end - marks the end of a loop body and returns to loop condition /// - Continue + LoopBodyEnd } /// @@ -171,19 +187,20 @@ public class FlowControlStatement : BlockStatement public string ConditionExpression { get; set; } = string.Empty; /// - /// Target block name when condition is true (for Branch) + /// Target block name when condition is true (for Branch/Loop) /// public string TrueBlockName { get; set; } = string.Empty; /// - /// Target block name when condition is false (for Branch) + /// Target block name when condition is false (for Branch/Loop) + /// For Loop: this is the loop exit block /// public string FalseBlockName { get; set; } = string.Empty; /// - /// Loop body block name (for Loop) + /// For LoopBodyEnd: the block name containing the Loop statement to return to /// - public string? LoopBlockName { get; set; } + public string? LoopBodyEndReturnTo { get; set; } } /// @@ -217,9 +234,50 @@ public class BlockScript public List AllBlocks { get; set; } = []; /// - /// Raw source code + /// Loop blocks dictionary by parent block name + /// (e.g., "MainBlock" -> LoopBlock for MainBlock's Loop statement) + /// + public Dictionary LoopBlocks { get; set; } = []; + + /// + /// Raw source code (parsed input, may not include helper functions) /// public string SourceCode { get; set; } = string.Empty; + + /// + /// Full source code including merged helper functions (for execution) + /// + public string FullSourceCode { get; set; } = string.Empty; + + /// + /// Helper functions to be made available in script execution context + /// + public List HelperFunctions { get; set; } = []; + + + /// + /// Gets a block by name (checks NamedBlocks, MainBlock, ConstBlock, PubVarBlock, then LoopBlocks) + /// Note: LoopBlocks is checked last because its keys are parent block names (e.g., "MainBlock") + /// which would otherwise shadow the actual MainBlock when querying by name. + /// + public BlockDefinition? GetBlockByName(string name) + { + // First check NamedBlocks (user-defined blocks) + if (NamedBlocks.TryGetValue(name, out var namedBlock)) + return namedBlock; + // Then check the standard blocks by name match + if (MainBlock?.Name == name) + return MainBlock; + if (ConstBlock?.Name == name) + return ConstBlock; + if (PubVarBlock?.Name == name) + return PubVarBlock; + // Finally check LoopBlocks - these are internal and should not shadow standard blocks + // LoopBlocks keys are parent block names (e.g., "MainBlock" -> LoopBlock for that parent) + if (LoopBlocks.TryGetValue(name, out var loopBlock)) + return loopBlock; + return null; + } } /// @@ -316,68 +374,48 @@ public class BlockScriptExecutionResult } /// -/// Flow control result returned by built-in functions +/// Result of executing a block - used by state machine for flow control /// -public class FlowResult +public class BlockExecutionResult { /// - /// Type of flow result - /// - public FlowResultType Type { get; set; } - - /// - /// Target block name for jumps - /// - public string? TargetBlock { get; set; } - - /// - /// Optional value (for return) - /// - public object? Value { get; set; } - - /// - /// Whether execution should continue + /// Whether execution should continue to next block /// public bool ShouldContinue { get; set; } = true; /// - /// Continue to next statement - /// - public static FlowResult Continue() => new() { Type = FlowResultType.Continue, ShouldContinue = true }; - - /// - /// Return with a value + /// Name of the next block to execute (null means end of script) /// - public static FlowResult Return(object? value = null) => new() { Type = FlowResultType.Return, Value = value, ShouldContinue = false }; + public string? NextBlockName { get; set; } /// - /// Break from loop + /// Whether this is a return (end of entire script) /// - public static FlowResult Break() => new() { Type = FlowResultType.Break, ShouldContinue = false }; -} + public bool IsReturn { get; set; } -/// -/// Flow result types -/// -public enum FlowResultType -{ /// - /// Continue to next statement + /// Return value if IsReturn is true /// - Continue, - - /// - /// Return from script - /// - Return, + public object? ReturnValue { get; set; } /// - /// Break from loop + /// Create a result for continuing to next block /// - Break, + public static BlockExecutionResult ContinueTo(string? nextBlockName) => new() + { + ShouldContinue = true, + NextBlockName = nextBlockName, + IsReturn = false + }; /// - /// Continue to next loop iteration + /// Create a result for end of script /// - ContinueLoop + public static BlockExecutionResult Return(object? value = null) => new() + { + ShouldContinue = false, + NextBlockName = null, + IsReturn = true, + ReturnValue = value + }; } diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/IBlockScriptParser.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/IBlockScriptParser.cs index b371106..24ed4ce 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/IBlockScriptParser.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/IBlockScriptParser.cs @@ -45,15 +45,6 @@ Task ExecuteAsync( Dictionary? parameters = null, CancellationToken cancellationToken = default); - /// - /// Executes a specific block by name - /// - Task ExecuteBlockAsync( - BlockScript script, - string blockName, - Dictionary? parameters = null, - CancellationToken cancellationToken = default); - /// /// Validates a block script /// @@ -70,11 +61,6 @@ public interface IBlockScopeManager /// IBlockScope GlobalScope { get; } - /// - /// Creates a new local scope for a block - /// - IBlockScope CreateLocalScope(string blockName); - /// /// Resolves a variable name to its value (searches local then global) /// diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowService.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowService.cs index e980b52..d0d9e80 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowService.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowService.cs @@ -6,9 +6,9 @@ namespace KitX.Core.Contract.Workflow; /// -/// Workflow service interface +/// Workflow management interface /// -public interface IWorkflowService +public interface IWorkflowManagementService { /// /// Gets the workflow list @@ -18,51 +18,60 @@ public interface IWorkflowService /// /// Adds a workflow /// - /// The workflow to add void AddWorkflow(IWorkflowCase workflow); /// /// Removes a workflow /// - /// The workflow ID void RemoveWorkflow(string workflowId); /// /// Runs a workflow /// - /// The workflow ID - /// True if run was successful Task RunWorkflowAsync(string workflowId); /// /// Stops a workflow /// - /// The workflow ID - /// True if stop was successful Task StopWorkflowAsync(string workflowId); +} +/// +/// Script execution interface +/// +public interface IScriptExecutionService +{ /// /// Executes a workflow script /// - /// The script content - /// Optional parameters - /// The execution result Task ExecuteScriptAsync(string script, Dictionary? parameters = null); /// /// Executes workflow script codes with plugin dependencies /// - /// The code to execute - /// Required plugins for this script - /// Whether to include timestamp in result - /// Cancellation token - /// Execution result as string Task ExecuteCodesAsync( string code, List? requiredPlugins = null, bool includeTimestamp = true, System.Threading.CancellationToken cancellationToken = default); + /// + /// Executes KCS codes + /// + Task ExecuteKcsCodesAsync( + string mainCode, + List helperFunctions, + List constants, + List? requiredPlugins = null, + bool includeTimestamp = true, + System.Threading.CancellationToken cancellationToken = default); +} + +/// +/// Plugin service interface for workflow constant and helper function handling +/// +public interface IWorkflowPluginService +{ /// /// Initializes the plugin manager /// @@ -71,132 +80,75 @@ public interface IWorkflowService /// /// Updates the available plugins list /// - /// Plugin list void UpdateAvailablePlugins(List plugins); /// - /// 从代码中解析常量 + /// Parses constants from code /// - /// 代码内容 - /// 常量列表 List ParseConstantsFromCode(string code); /// - /// 应用常量到代码 + /// Applies constants to code /// - /// 原始代码 - /// 常量列表 - /// 应用常量后的代码 string ApplyConstantsToCode(string code, List constants); /// - /// 合并辅助函数到代码 + /// Merges helper functions into code /// - /// 主程序代码 - /// 辅助函数列表 - /// 合并后的完整代码 string MergeHelperFunctions(string mainCode, List helperFunctions); +} +/// +/// Block script service interface +/// +public interface IBlockScriptService +{ /// - /// 执行KCS代码 - 包含代码分析、常量应用、辅助函数合并 - /// - /// 主程序代码 - /// 辅助函数列表 - /// 可变常量列表 - /// 需要的插件 - /// 是否包含时间戳 - /// 取消令牌 - /// 执行结果 - Task ExecuteKcsCodesAsync( - string mainCode, - List helperFunctions, - List constants, - List? requiredPlugins = null, - bool includeTimestamp = true, - System.Threading.CancellationToken cancellationToken = default); - - // ========== Block Script Methods ========== - - /// - /// 解析块脚本 + /// Parses a block script /// - /// 块脚本源代码 - /// 解析结果 BlockScriptParseResult ParseBlockScript(string sourceCode); /// - /// 异步解析块脚本 + /// Parses a block script asynchronously /// Task ParseBlockScriptAsync(string sourceCode); /// - /// 验证块脚本 + /// Validates a block script /// - /// 块脚本源代码 - /// 验证结果 BlockScriptValidationResult ValidateBlockScript(string sourceCode); /// - /// 执行块脚本 + /// Executes a block script /// - /// 解析后的块脚本 - /// 输入参数 - /// 取消令牌 - /// 执行结果 Task ExecuteBlockScriptAsync( BlockScript script, Dictionary? parameters = null, System.Threading.CancellationToken cancellationToken = default); /// - /// 从块脚本源代码执行 + /// Executes a block script from source code /// - /// 块脚本源代码 - /// 输入参数 - /// 取消令牌 - /// 执行结果 Task ExecuteBlockScriptAsync( string sourceCode, Dictionary? parameters = null, System.Threading.CancellationToken cancellationToken = default); + + /// + /// Executes a block script from source code with helper functions + /// + Task ExecuteBlockScriptAsync( + string sourceCode, + List helperFunctions, + System.Threading.CancellationToken cancellationToken = default); } /// -/// Plugin service provider interface for workflow integration +/// Workflow service interface - composite interface for backward compatibility /// -public interface IPluginServiceProvider +public interface IWorkflowService : IWorkflowManagementService, IScriptExecutionService, + IWorkflowPluginService, IBlockScriptService { - /// - /// Gets running plugins - /// - IEnumerable GetRunningPlugins(); - - /// - /// Finds a plugin by name - /// - /// The plugin name - /// The plugin info or null if not found - PluginInfo? FindPlugin(string pluginName); - - /// - /// Finds a connector for a plugin - /// - /// The plugin info - /// The connector or null if not found - object? FindConnector(PluginInfo pluginInfo); - - /// - /// Sends a request asynchronously - /// - /// The connector - /// The request - Task SendRequestAsync(object connector, object request); - - /// - /// Subscribes to plugin responses - /// - /// The response handler - void SubscribeToResponses(Action responseHandler); } /// From 691241f1572d4bd780772a8b637eae1a60a49fb5 Mon Sep 17 00:00:00 2001 From: StarInk Date: Mon, 30 Mar 2026 07:09:21 +0200 Subject: [PATCH 26/60] =?UTF-8?q?=F0=9F=92=BE=20Feat(Blueprint):=20?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=9D=97=E8=84=9A=E6=9C=AC=E8=8E=B7=E5=8F=96?= =?UTF-8?q?=E9=80=BB=E8=BE=91=EF=BC=8C=E4=BC=98=E5=85=88=E6=A3=80=E6=9F=A5?= =?UTF-8?q?=E5=BE=AA=E7=8E=AF=E5=9D=97=EF=BC=9B=E6=B7=BB=E5=8A=A0=E8=93=9D?= =?UTF-8?q?=E5=9B=BE=E5=8F=AF=E8=A7=86=E5=8C=96=E6=95=B0=E6=8D=AE=E6=94=AF?= =?UTF-8?q?=E6=8C=81=EF=BC=9B=E6=96=B0=E5=A2=9E=E8=8A=82=E7=82=B9=E6=A8=A1?= =?UTF-8?q?=E6=9D=BF=E6=8E=A5=E5=8F=A3=E5=8F=8A=E5=AE=9A=E4=B9=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Workflow/BlockScriptModels.cs | 24 +- .../Workflow/BlueprintModels.cs | 742 ++++++++++++++++++ .../Workflow/IBlueprintConverter.cs | 79 ++ .../Workflow/INodeTemplateProvider.cs | 57 ++ .../Workflow/KcsFileFormat.cs | 6 + 5 files changed, 900 insertions(+), 8 deletions(-) create mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/BlueprintModels.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/IBlueprintConverter.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/INodeTemplateProvider.cs diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/BlockScriptModels.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/BlockScriptModels.cs index 9b2ef40..c9375a7 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/BlockScriptModels.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/BlockScriptModels.cs @@ -256,15 +256,25 @@ public class BlockScript /// - /// Gets a block by name (checks NamedBlocks, MainBlock, ConstBlock, PubVarBlock, then LoopBlocks) - /// Note: LoopBlocks is checked last because its keys are parent block names (e.g., "MainBlock") - /// which would otherwise shadow the actual MainBlock when querying by name. + /// Gets a block by name (checks LoopBlocks first by block name, then NamedBlocks, then standard blocks) + /// IMPORTANT: LoopBlocks are checked FIRST because LoopBlock names (like "LoopBody") should NOT be + /// shadowed by user-defined NamedBlocks with the same name. /// public BlockDefinition? GetBlockByName(string name) { - // First check NamedBlocks (user-defined blocks) + // First check LoopBlocks by the block's own name (not parent block name) + // This is critical because LoopBlocks are created for "NextBlock = Loop(...)" statements + // and their names (like "LoopBody") should take precedence over user-defined blocks + foreach (var kvp in LoopBlocks) + { + if (kvp.Value.Name == name) + return kvp.Value; + } + + // Then check NamedBlocks (user-defined blocks) if (NamedBlocks.TryGetValue(name, out var namedBlock)) return namedBlock; + // Then check the standard blocks by name match if (MainBlock?.Name == name) return MainBlock; @@ -272,10 +282,7 @@ public class BlockScript return ConstBlock; if (PubVarBlock?.Name == name) return PubVarBlock; - // Finally check LoopBlocks - these are internal and should not shadow standard blocks - // LoopBlocks keys are parent block names (e.g., "MainBlock" -> LoopBlock for that parent) - if (LoopBlocks.TryGetValue(name, out var loopBlock)) - return loopBlock; + return null; } } @@ -419,3 +426,4 @@ public class BlockExecutionResult ReturnValue = value }; } + diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/BlueprintModels.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/BlueprintModels.cs new file mode 100644 index 0000000..9daf844 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/BlueprintModels.cs @@ -0,0 +1,742 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace KitX.Core.Contract.Workflow; + +/// +/// Simple 2D point structure for view positioning +/// +public struct ViewPoint +{ + public double X { get; set; } + public double Y { get; set; } + + public ViewPoint(double x, double y) + { + X = x; + Y = y; + } + + public static implicit operator (double X, double Y)(ViewPoint p) => (p.X, p.Y); + public static implicit operator ViewPoint((double X, double Y) p) => new(p.X, p.Y); +} + +/// +/// Blueprint node types +/// +public enum BlueprintNodeType +{ + /// + /// Entry point node - triggered by Run button + /// + Entry, + + /// + /// Conditional branch node + /// + Branch, + + /// + /// Loop node + /// + Loop, + + /// + /// Break from loop node + /// + Break, + + /// + /// Constant value node + /// + Const, + + /// + /// Plugin function call node + /// + Call, + + /// + /// Helper function call node + /// + CallHelper, + + /// + /// Get variable value node - reads a PubVar + /// + Get, + + /// + /// Set variable value node - writes to a PubVar + /// + Set, + + /// + /// Print output node + /// + Print, + + /// + /// Pause execution node + /// + Pause +} + +/// +/// Pin direction +/// +public enum PinDirection +{ + Input, + Output +} + +/// +/// Pin type for data flow +/// +public enum PinType +{ + /// + /// Execution flow pin - green + /// + Execution, + + /// + /// Boolean pin - cyan + /// + Boolean, + + /// + /// Integer pin - orange + /// + Integer, + + /// + /// Double pin - purple + /// + Double, + + /// + /// String pin - yellow + /// + String, + + /// + /// Any type pin - white + /// + Any +} + +/// +/// Pin on a blueprint node +/// +public class BlueprintPin +{ + /// + /// Unique identifier + /// + public string Id { get; set; } = Guid.NewGuid().ToString(); + + /// + /// Pin name (e.g., "Exec", "Condition", "True", "Value") + /// + public string Name { get; set; } = string.Empty; + + /// + /// Pin direction + /// + public PinDirection Direction { get; set; } + + /// + /// Pin data type + /// + public PinType Type { get; set; } = PinType.Any; + + /// + /// Default value for input pins + /// + public string? DefaultValue { get; set; } + + /// + /// Connected pin ID (for runtime connections) + /// + public string? ConnectedPinId { get; set; } +} + +/// +/// Base class for all blueprint nodes +/// +public abstract class BlueprintNode +{ + /// + /// Unique identifier + /// + public string Id { get; set; } = Guid.NewGuid().ToString(); + + /// + /// Node type + /// + public BlueprintNodeType NodeType { get; set; } + + /// + /// Display name + /// + public string Name { get; set; } = string.Empty; + + /// + /// X position on canvas + /// + public double X { get; set; } + + /// + /// Y position on canvas + /// + public double Y { get; set; } + + /// + /// Node width + /// + public double Width { get; set; } = 200; + + /// + /// Node height + /// + public double Height { get; set; } = 100; + + /// + /// Whether node is selected + /// + public bool IsSelected { get; set; } + + /// + /// Input pins + /// + public List InputPins { get; set; } = []; + + /// + /// Output pins + /// + public List OutputPins { get; set; } = []; + + /// + /// Parent blueprint reference (set when node is added to blueprint) + /// + public Blueprint? Blueprint { get; set; } + + /// + /// Get pin by ID + /// + public BlueprintPin? GetPinById(string pinId) + { + foreach (var pin in InputPins) + if (pin.Id == pinId) return pin; + foreach (var pin in OutputPins) + if (pin.Id == pinId) return pin; + return null; + } + + /// + /// Get all pins + /// + public IEnumerable GetAllPins() + { + foreach (var pin in InputPins) + yield return pin; + foreach (var pin in OutputPins) + yield return pin; + } +} + +// Node-specific classes can be defined for additional properties +// Currently using the abstract base with runtime-added properties via dynamics or dedicated subclasses + +/// +/// Entry node - execution entry point +/// +public class EntryNode : BlueprintNode +{ + public EntryNode() + { + NodeType = BlueprintNodeType.Entry; + Name = "Entry"; + OutputPins.Add(new BlueprintPin + { + Name = "Exec", + Direction = PinDirection.Output, + Type = PinType.Execution + }); + } +} + +/// +/// Branch node - conditional execution +/// +public class BranchNode : BlueprintNode +{ + public BranchNode() + { + NodeType = BlueprintNodeType.Branch; + Name = "Branch"; + InputPins.Add(new BlueprintPin + { + Name = "Exec", + Direction = PinDirection.Input, + Type = PinType.Execution + }); + InputPins.Add(new BlueprintPin + { + Name = "Condition", + Direction = PinDirection.Input, + Type = PinType.Boolean + }); + OutputPins.Add(new BlueprintPin + { + Name = "True", + Direction = PinDirection.Output, + Type = PinType.Execution + }); + OutputPins.Add(new BlueprintPin + { + Name = "False", + Direction = PinDirection.Output, + Type = PinType.Execution + }); + } +} + +/// +/// Loop node - iterative execution +/// +public class LoopNode : BlueprintNode +{ + public LoopNode() + { + NodeType = BlueprintNodeType.Loop; + Name = "Loop"; + InputPins.Add(new BlueprintPin + { + Name = "Exec", + Direction = PinDirection.Input, + Type = PinType.Execution + }); + InputPins.Add(new BlueprintPin + { + Name = "Condition", + Direction = PinDirection.Input, + Type = PinType.Boolean + }); + OutputPins.Add(new BlueprintPin + { + Name = "LoopBody", + Direction = PinDirection.Output, + Type = PinType.Execution + }); + OutputPins.Add(new BlueprintPin + { + Name = "LoopEnd", + Direction = PinDirection.Output, + Type = PinType.Execution + }); + } +} + +/// +/// Break node - exit loop +/// +public class BreakNode : BlueprintNode +{ + public BreakNode() + { + NodeType = BlueprintNodeType.Break; + Name = "Break"; + InputPins.Add(new BlueprintPin + { + Name = "Exec", + Direction = PinDirection.Input, + Type = PinType.Execution + }); + } +} + +/// +/// Const node - constant value +/// +public class ConstNode : BlueprintNode +{ + /// + /// Constant name + /// + public string ConstName { get; set; } = string.Empty; + + /// + /// Constant type + /// + public string ConstType { get; set; } = "string"; + + /// + /// Constant value + /// + public string? ConstValue { get; set; } + + public ConstNode() + { + NodeType = BlueprintNodeType.Const; + Name = "Const"; + OutputPins.Add(new BlueprintPin + { + Name = "Value", + Direction = PinDirection.Output, + Type = PinType.Any + }); + } +} + +/// +/// Call node - plugin function call +/// +public class CallNode : BlueprintNode +{ + /// + /// Plugin name + /// + public string PluginName { get; set; } = string.Empty; + + /// + /// Function name + /// + public string FunctionName { get; set; } = string.Empty; + + public CallNode() + { + NodeType = BlueprintNodeType.Call; + Name = "Call"; + InputPins.Add(new BlueprintPin + { + Name = "Exec", + Direction = PinDirection.Input, + Type = PinType.Execution + }); + OutputPins.Add(new BlueprintPin + { + Name = "Exec", + Direction = PinDirection.Output, + Type = PinType.Execution + }); + OutputPins.Add(new BlueprintPin + { + Name = "Return", + Direction = PinDirection.Output, + Type = PinType.Any + }); + } +} + +/// +/// CallHelper node - helper function call +/// +public class CallHelperNode : BlueprintNode +{ + /// + /// Helper function name reference + /// + public string HelperFunctionName { get; set; } = string.Empty; + + public CallHelperNode() + { + NodeType = BlueprintNodeType.CallHelper; + Name = "CallHelper"; + InputPins.Add(new BlueprintPin + { + Name = "Exec", + Direction = PinDirection.Input, + Type = PinType.Execution + }); + OutputPins.Add(new BlueprintPin + { + Name = "Exec", + Direction = PinDirection.Output, + Type = PinType.Execution + }); + OutputPins.Add(new BlueprintPin + { + Name = "Return", + Direction = PinDirection.Output, + Type = PinType.Any + }); + } +} + +/// +/// Get variable value node - reads a PubVar +/// +public class GetNode : BlueprintNode +{ + /// + /// Variable name to read + /// + public string VarName { get; set; } = string.Empty; + + public GetNode() + { + NodeType = BlueprintNodeType.Get; + Name = "Get"; + InputPins.Add(new BlueprintPin + { + Name = "Exec", + Direction = PinDirection.Input, + Type = PinType.Execution + }); + OutputPins.Add(new BlueprintPin + { + Name = "Exec", + Direction = PinDirection.Output, + Type = PinType.Execution + }); + OutputPins.Add(new BlueprintPin + { + Name = "Value", + Direction = PinDirection.Output, + Type = PinType.Any + }); + } +} + +/// +/// Set variable value node - writes to a PubVar +/// +public class SetNode : BlueprintNode +{ + /// + /// Variable name to write + /// + public string VarName { get; set; } = string.Empty; + + public SetNode() + { + NodeType = BlueprintNodeType.Set; + Name = "Set"; + InputPins.Add(new BlueprintPin + { + Name = "Exec", + Direction = PinDirection.Input, + Type = PinType.Execution + }); + InputPins.Add(new BlueprintPin + { + Name = "Value", + Direction = PinDirection.Input, + Type = PinType.Any + }); + OutputPins.Add(new BlueprintPin + { + Name = "Exec", + Direction = PinDirection.Output, + Type = PinType.Execution + }); + } +} + +/// +/// Print node - output information +/// +public class PrintNode : BlueprintNode +{ + public PrintNode() + { + NodeType = BlueprintNodeType.Print; + Name = "Print"; + InputPins.Add(new BlueprintPin + { + Name = "Exec", + Direction = PinDirection.Input, + Type = PinType.Execution + }); + InputPins.Add(new BlueprintPin + { + Name = "Value", + Direction = PinDirection.Input, + Type = PinType.Any + }); + OutputPins.Add(new BlueprintPin + { + Name = "Exec", + Direction = PinDirection.Output, + Type = PinType.Execution + }); + } +} + +/// +/// Pause node - pause execution +/// +public class PauseNode : BlueprintNode +{ + public PauseNode() + { + NodeType = BlueprintNodeType.Pause; + Name = "Pause"; + InputPins.Add(new BlueprintPin + { + Name = "Exec", + Direction = PinDirection.Input, + Type = PinType.Execution + }); + InputPins.Add(new BlueprintPin + { + Name = "Milliseconds", + Direction = PinDirection.Input, + Type = PinType.Integer + }); + OutputPins.Add(new BlueprintPin + { + Name = "Exec", + Direction = PinDirection.Output, + Type = PinType.Execution + }); + } +} + +/// +/// Connection between two pins +/// +public class BlueprintConnection +{ + /// + /// Unique identifier + /// + public string Id { get; set; } = Guid.NewGuid().ToString(); + + /// + /// Source node ID + /// + public string SourceNodeId { get; set; } = string.Empty; + + /// + /// Source pin ID + /// + public string SourcePinId { get; set; } = string.Empty; + + /// + /// Target node ID + /// + public string TargetNodeId { get; set; } = string.Empty; + + /// + /// Target pin ID + /// + public string TargetPinId { get; set; } = string.Empty; + + /// + /// Corresponding PubVar name for data flow connections (optional) + /// + public string? PubVarName { get; set; } +} + +/// +/// Blueprint document container +/// +public class Blueprint +{ + /// + /// Unique identifier + /// + public string Id { get; set; } = Guid.NewGuid().ToString(); + + /// + /// Document name + /// + public string Name { get; set; } = "Untitled"; + + /// + /// Creation timestamp + /// + public DateTime CreatedAt { get; set; } = DateTime.Now; + + /// + /// Last modified timestamp + /// + public DateTime ModifiedAt { get; set; } = DateTime.Now; + + /// + /// All nodes in this blueprint + /// + public List Nodes { get; set; } = []; + + /// + /// All connections in this blueprint + /// + public List Connections { get; set; } = []; + + /// + /// View zoom level (0.1 to 5.0) + /// + public double ZoomLevel { get; set; } = 1.0; + + /// + /// View pan offset + /// + public ViewPoint PanOffset { get; set; } = new(0, 0); + + /// + /// Helper functions available in this blueprint + /// + public List HelperFunctions { get; set; } = []; + + /// + /// PubVar variable names (invisible in Blueprint, used for data flow) + /// + public List PubVarNames { get; set; } = []; + + /// + /// Constant values (from ConstBlock) + /// + public List ConstValues { get; set; } = []; + + /// + /// Get node by ID + /// + public BlueprintNode? GetNodeById(string nodeId) + { + foreach (var node in Nodes) + if (node.Id == nodeId) return node; + return null; + } + + /// + /// Get connections from a node + /// + public IEnumerable GetConnectionsFrom(string nodeId) + { + foreach (var conn in Connections) + if (conn.SourceNodeId == nodeId) yield return conn; + } + + /// + /// Get connections to a node + /// + public IEnumerable GetConnectionsTo(string nodeId) + { + foreach (var conn in Connections) + if (conn.TargetNodeId == nodeId) yield return conn; + } + + /// + /// Add a node to this blueprint, automatically setting the back-reference + /// + /// Node to add + public void AddNode(BlueprintNode node) + { + node.Blueprint = this; + Nodes.Add(node); + } + + /// + /// Add a connection to this blueprint + /// + /// Connection to add + public void AddConnection(BlueprintConnection connection) + { + Connections.Add(connection); + } +} diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/IBlueprintConverter.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/IBlueprintConverter.cs new file mode 100644 index 0000000..01cf3ca --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/IBlueprintConverter.cs @@ -0,0 +1,79 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace KitX.Core.Contract.Workflow; + +/// +/// Interface for converting BlockScript to Blueprint +/// +public interface IBlockScriptToBlueprintConverter +{ + /// + /// Convert BlockScript source code to Blueprint + /// + /// BlockScript source code + /// Helper functions available + /// Converted Blueprint + Blueprint Convert(string sourceCode, List? helperFunctions = null); + + /// + /// Convert parsed BlockScript to Blueprint + /// + /// Parsed BlockScript + /// Converted Blueprint + Blueprint Convert(BlockScript script); +} + +/// +/// Interface for converting Blueprint to BlockScript +/// +public interface IBlueprintToBlockScriptConverter +{ + /// + /// Convert Blueprint to BlockScript source code + /// + /// Blueprint to convert + /// BlockScript source code + string Convert(Blueprint blueprint); + + /// + /// Convert Blueprint to parsed BlockScript + /// + /// Blueprint to convert + /// Parsed BlockScript + BlockScript ConvertToBlockScript(Blueprint blueprint); +} + +/// +/// Interface for Blueprint service +/// +public interface IBlueprintService +{ + /// + /// Create a new empty blueprint + /// + /// New blueprint + Blueprint CreateBlueprint(); + + /// + /// Import blueprint from BlockScript + /// + /// BlockScript source code + /// Helper functions + /// Imported blueprint, or null if conversion failed + Blueprint? ImportFromBlockScript(string sourceCode, List? helperFunctions = null); + + /// + /// Export blueprint to BlockScript + /// + /// Blueprint to export + /// BlockScript source code + string ExportToBlockScript(Blueprint blueprint); + + /// + /// Execute blueprint by converting to BlockScript and running + /// + /// Blueprint to execute + /// Execution result + Task ExecuteBlueprintAsync(Blueprint blueprint); +} diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/INodeTemplateProvider.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/INodeTemplateProvider.cs new file mode 100644 index 0000000..1a0b30b --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/INodeTemplateProvider.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; + +namespace KitX.Core.Contract.Workflow; + +/// +/// 节点模板接口 - 统一手动和自动创建的节点定义 +/// +public interface INodeTemplateProvider +{ + /// + /// 创建指定类型的节点 + /// + BlueprintNode CreateNode(BlueprintNodeType type); + + /// + /// 获取节点模板 + /// + IReadOnlyDictionary GetTemplates(); + + /// + /// 获取节点尺寸 + /// + (double Width, double Height) GetNodeSize(BlueprintNodeType type); + + /// + /// 获取输入引脚的相对 Y 坐标 + /// + double GetInputPinY(BlueprintNodeType nodeType, string pinName); + + /// + /// 获取输出引脚的相对 Y 坐标 + /// + double GetOutputPinY(BlueprintNodeType nodeType, string pinName); +} + +/// +/// 节点模板定义 +/// +public class NodeTemplate +{ + public BlueprintNodeType NodeType { get; set; } + public string Name { get; set; } = string.Empty; + public double Width { get; set; } = 120; + public double Height { get; set; } = 60; + public List InputPins { get; set; } = []; + public List OutputPins { get; set; } = []; +} + +/// +/// 引脚模板定义 +/// +public class PinTemplate +{ + public string Name { get; set; } = string.Empty; + public PinType Type { get; set; } = PinType.Any; + public double RelativeY { get; set; } +} diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/KcsFileFormat.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/KcsFileFormat.cs index 13786fe..7bf1d4c 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/KcsFileFormat.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/KcsFileFormat.cs @@ -35,6 +35,12 @@ public class KcsFileFormat /// 块脚本源代码(当 UseBlockMode 为 true 时使用) /// public string? BlockScriptSource { get; set; } + + /// + /// 蓝图可视化数据(包含节点位置、连接关系、视图状态等) + /// 当 UseBlockMode=true 且此字段非空时,表示该脚本有对应的蓝图编辑状态 + /// + public Blueprint? BlueprintData { get; set; } } /// From cc7f85e65b0a234c02e1026a7159dce6fd2635f2 Mon Sep 17 00:00:00 2001 From: StarInk Date: Wed, 1 Apr 2026 05:44:49 +0200 Subject: [PATCH 27/60] =?UTF-8?q?=F0=9F=92=BE=20Feat(Blueprint):=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E8=8A=82=E7=82=B9=E5=88=9B=E5=BB=BA=E5=92=8C?= =?UTF-8?q?=E5=B8=83=E5=B1=80=E6=9C=8D=E5=8A=A1=E6=8E=A5=E5=8F=A3=EF=BC=9B?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E8=93=9D=E5=9B=BE=E8=BF=9E=E6=8E=A5=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E9=80=BB=E8=BE=91=E4=BB=A5=E9=81=BF=E5=85=8D=E9=87=8D?= =?UTF-8?q?=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Workflow/BlueprintModels.cs | 9 ++- .../Workflow/ILayoutService.cs | 9 +++ .../Workflow/INodeCreationService.cs | 62 +++++++++++++++++++ 3 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/ILayoutService.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/INodeCreationService.cs diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/BlueprintModels.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/BlueprintModels.cs index 9daf844..5d3c4a0 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/BlueprintModels.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/BlueprintModels.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; namespace KitX.Core.Contract.Workflow; @@ -732,11 +733,15 @@ public void AddNode(BlueprintNode node) } /// - /// Add a connection to this blueprint + /// Add a connection to this blueprint (deduplicates by source/target/pin) /// /// Connection to add public void AddConnection(BlueprintConnection connection) { - Connections.Add(connection); + var alreadyExists = Connections.Any(c => + c.SourceNodeId == connection.SourceNodeId && c.SourcePinId == connection.SourcePinId && + c.TargetNodeId == connection.TargetNodeId && c.TargetPinId == connection.TargetPinId); + if (!alreadyExists) + Connections.Add(connection); } } diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/ILayoutService.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/ILayoutService.cs new file mode 100644 index 0000000..c146bbe --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/ILayoutService.cs @@ -0,0 +1,9 @@ +namespace KitX.Core.Contract.Workflow; + +/// +/// Interface for layout service +/// +public interface ILayoutService +{ + void LayoutNodes(Blueprint blueprint); +} diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/INodeCreationService.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/INodeCreationService.cs new file mode 100644 index 0000000..d3854c6 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/INodeCreationService.cs @@ -0,0 +1,62 @@ +namespace KitX.Core.Contract.Workflow; + +/// +/// Interface for node creation service +/// +public interface INodeCreationService +{ + /// + /// Creates a constant node + /// + ConstNode CreateConstNode(string name, string type, string? defaultValue); + + /// + /// Creates an entry node + /// + EntryNode CreateEntryNode(double x, double y); + + /// + /// Creates a get variable node + /// + GetNode CreateGetNode(string varName); + + /// + /// Creates a set variable node + /// + SetNode CreateSetNode(string varName); + + /// + /// Creates a print node + /// + PrintNode CreatePrintNode(); + + /// + /// Creates a pause node + /// + PauseNode CreatePauseNode(); + + /// + /// Creates a call function node + /// + CallNode CreateCallNode(string functionName); + + /// + /// Creates a call helper function node + /// + CallHelperNode CreateCallHelperNode(string helperFunctionName); + + /// + /// Creates a branch node + /// + BranchNode CreateBranchNode(); + + /// + /// Creates a loop node + /// + LoopNode CreateLoopNode(); + + /// + /// Creates a break node + /// + BreakNode CreateBreakNode(); +} From 104637811039d9755a3b80c22304feaa645d6126 Mon Sep 17 00:00:00 2001 From: StarInk Date: Sat, 4 Apr 2026 17:39:23 +0200 Subject: [PATCH 28/60] =?UTF-8?q?=F0=9F=92=BE=20Feat(Blueprint):=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E8=93=9D=E5=9B=BE=E6=B8=B2=E6=9F=93=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E7=B1=BB=E5=8F=8A=E6=9C=8D=E5=8A=A1=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=EF=BC=8C=E7=94=A8=E4=BA=8E=E5=88=86=E7=B1=BB=E6=89=A7=E8=A1=8C?= =?UTF-8?q?=E5=92=8C=E6=95=B0=E6=8D=AE=E8=BF=9E=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Workflow/BlueprintRenderData.cs | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/BlueprintRenderData.cs diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/BlueprintRenderData.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/BlueprintRenderData.cs new file mode 100644 index 0000000..fb05a36 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/BlueprintRenderData.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; + +namespace KitX.Core.Contract.Workflow; + +/// +/// Pre-classified rendering data for three-phase blueprint rendering: +/// Phase 1: AllNodes, Phase 2: ExecConnections, Phase 3: DataConnections +/// +public class BlueprintRenderData +{ + /// + /// All nodes in the blueprint + /// + public List AllNodes { get; set; } = []; + + /// + /// Execution flow connections (source pin is Execution type) + /// + public List ExecConnections { get; set; } = []; + + /// + /// Data flow connections (source pin is non-Execution type) + /// + public List DataConnections { get; set; } = []; +} + +/// +/// Service that classifies blueprint connections into Exec and Data groups +/// for phased rendering in the frontend. +/// +public interface IBlueprintRenderDataService +{ + /// + /// Splits a Blueprint's connections into Exec and Data categories + /// + BlueprintRenderData GetRenderData(Blueprint blueprint); +} From 56934260baa68c2116fd96bc01236b6388e08eff Mon Sep 17 00:00:00 2001 From: StarInk Date: Sun, 5 Apr 2026 22:18:49 +0200 Subject: [PATCH 29/60] =?UTF-8?q?=F0=9F=A7=A9=20Refactor(Blueprint):=20?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=BB=A3=E7=A0=81=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../KitX.Core.Contract/Workflow/BlueprintModels.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/BlueprintModels.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/BlueprintModels.cs index 5d3c4a0..89fe7cd 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/BlueprintModels.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/BlueprintModels.cs @@ -158,11 +158,6 @@ public class BlueprintPin /// Default value for input pins /// public string? DefaultValue { get; set; } - - /// - /// Connected pin ID (for runtime connections) - /// - public string? ConnectedPinId { get; set; } } /// From 46442b16d697b298c57e8ae551c9467b292596f7 Mon Sep 17 00:00:00 2001 From: StarInk Date: Mon, 6 Apr 2026 02:58:43 +0200 Subject: [PATCH 30/60] =?UTF-8?q?=F0=9F=A7=A9=20Refactor(Blueprint):=20?= =?UTF-8?q?=E5=BC=95=E5=85=A5=E8=8A=82=E7=82=B9=E6=B3=A8=E5=86=8C=E8=A1=A8?= =?UTF-8?q?=E4=B8=8E=E7=AD=96=E7=95=A5=E6=A8=A1=E5=BC=8F=EF=BC=8C=E6=B6=88?= =?UTF-8?q?=E9=99=A4(=E6=B7=BB=E5=8A=A0=E6=96=B0=E5=86=85=E7=BD=AE?= =?UTF-8?q?=E8=8A=82=E7=82=B9=E6=97=B6=E7=9A=84)=E6=95=A3=E5=BC=B9?= =?UTF-8?q?=E5=BC=8F=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Workflow/BlueprintModels.cs | 153 ++++++++++++++++++ .../Workflow/INodeCreationService.cs | 62 ------- .../Workflow/INodeRegistry.cs | 28 ++++ .../Workflow/INodeTemplateProvider.cs | 57 ------- 4 files changed, 181 insertions(+), 119 deletions(-) delete mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/INodeCreationService.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/INodeRegistry.cs delete mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/INodeTemplateProvider.cs diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/BlueprintModels.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/BlueprintModels.cs index 89fe7cd..15e8709 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/BlueprintModels.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/BlueprintModels.cs @@ -129,6 +129,28 @@ public enum PinType Any } +/// +/// Describes a pin's layout within a node template. +/// Used for self-describing node pin configurations. +/// +public record PinDescriptor( + string Name, + PinType Type, + double RelativeY +); + +/// +/// Describes a node type's layout and pin configuration. +/// Each node subclass provides its own descriptor via GetDescriptor(). +/// +public record NodeDescriptor( + double Width, + double Height, + IReadOnlyList InputPins, + IReadOnlyList OutputPins, + string DisplayName +); + /// /// Pin on a blueprint node /// @@ -242,6 +264,19 @@ public IEnumerable GetAllPins() foreach (var pin in OutputPins) yield return pin; } + + /// + /// Returns the layout descriptor for this node type. + /// Each node subclass must define its own pin layout, width, and height. + /// Used by the node registry and UI rendering to avoid external switch statements. + /// + public abstract NodeDescriptor GetDescriptor(); + + /// + /// Returns the display title for UI rendering (e.g., "Call: Plugin.Func"). + /// Default implementation returns Name; subclasses override for richer display. + /// + public virtual string GetDisplayTitle() => Name; } // Node-specific classes can be defined for additional properties @@ -263,6 +298,13 @@ public EntryNode() Type = PinType.Execution }); } + + public override NodeDescriptor GetDescriptor() => new( + Width: 120, Height: 60, + InputPins: [], + OutputPins: [new PinDescriptor("Exec", PinType.Execution, 30)], + DisplayName: "Entry" + ); } /// @@ -299,6 +341,19 @@ public BranchNode() Type = PinType.Execution }); } + + public override NodeDescriptor GetDescriptor() => new( + Width: 120, Height: 80, + InputPins: [ + new PinDescriptor("Exec", PinType.Execution, 30), + new PinDescriptor("Condition", PinType.Boolean, 50) + ], + OutputPins: [ + new PinDescriptor("True", PinType.Execution, 30), + new PinDescriptor("False", PinType.Execution, 50) + ], + DisplayName: "Branch" + ); } /// @@ -335,6 +390,19 @@ public LoopNode() Type = PinType.Execution }); } + + public override NodeDescriptor GetDescriptor() => new( + Width: 120, Height: 80, + InputPins: [ + new PinDescriptor("Exec", PinType.Execution, 30), + new PinDescriptor("Condition", PinType.Boolean, 50) + ], + OutputPins: [ + new PinDescriptor("LoopBody", PinType.Execution, 30), + new PinDescriptor("LoopEnd", PinType.Execution, 50) + ], + DisplayName: "Loop" + ); } /// @@ -353,6 +421,13 @@ public BreakNode() Type = PinType.Execution }); } + + public override NodeDescriptor GetDescriptor() => new( + Width: 100, Height: 40, + InputPins: [new PinDescriptor("Exec", PinType.Execution, 20)], + OutputPins: [], + DisplayName: "Break" + ); } /// @@ -386,6 +461,15 @@ public ConstNode() Type = PinType.Any }); } + + public override NodeDescriptor GetDescriptor() => new( + Width: 120, Height: 50, + InputPins: [], + OutputPins: [new PinDescriptor("Value", PinType.Any, 25)], + DisplayName: "Const" + ); + + public override string GetDisplayTitle() => $"Const: {ConstName}"; } /// @@ -426,6 +510,19 @@ public CallNode() Type = PinType.Any }); } + + public override NodeDescriptor GetDescriptor() => new( + Width: 140, Height: 60, + InputPins: [new PinDescriptor("Exec", PinType.Execution, 20)], + OutputPins: [ + new PinDescriptor("Exec", PinType.Execution, 20), + new PinDescriptor("Return", PinType.Any, 40) + ], + DisplayName: "Call" + ); + + public override string GetDisplayTitle() + => string.IsNullOrEmpty(PluginName) ? $"Call: {FunctionName}" : $"Call: {PluginName}.{FunctionName}"; } /// @@ -461,6 +558,18 @@ public CallHelperNode() Type = PinType.Any }); } + + public override NodeDescriptor GetDescriptor() => new( + Width: 130, Height: 50, + InputPins: [new PinDescriptor("Exec", PinType.Execution, 25)], + OutputPins: [ + new PinDescriptor("Exec", PinType.Execution, 25), + new PinDescriptor("Return", PinType.Any, 40) + ], + DisplayName: "CallHelper" + ); + + public override string GetDisplayTitle() => $"Helper: {HelperFunctionName}"; } /// @@ -496,6 +605,18 @@ public GetNode() Type = PinType.Any }); } + + public override NodeDescriptor GetDescriptor() => new( + Width: 120, Height: 60, + InputPins: [new PinDescriptor("Exec", PinType.Execution, 20)], + OutputPins: [ + new PinDescriptor("Exec", PinType.Execution, 20), + new PinDescriptor("Value", PinType.Any, 40) + ], + DisplayName: "Get" + ); + + public override string GetDisplayTitle() => $"Get: {VarName}"; } /// @@ -531,6 +652,18 @@ public SetNode() Type = PinType.Execution }); } + + public override NodeDescriptor GetDescriptor() => new( + Width: 120, Height: 60, + InputPins: [ + new PinDescriptor("Exec", PinType.Execution, 20), + new PinDescriptor("Value", PinType.Any, 40) + ], + OutputPins: [new PinDescriptor("Exec", PinType.Execution, 20)], + DisplayName: "Set" + ); + + public override string GetDisplayTitle() => $"Set: {VarName}"; } /// @@ -561,6 +694,16 @@ public PrintNode() Type = PinType.Execution }); } + + public override NodeDescriptor GetDescriptor() => new( + Width: 100, Height: 50, + InputPins: [ + new PinDescriptor("Exec", PinType.Execution, 20), + new PinDescriptor("Value", PinType.Any, 35) + ], + OutputPins: [new PinDescriptor("Exec", PinType.Execution, 25)], + DisplayName: "Print" + ); } /// @@ -591,6 +734,16 @@ public PauseNode() Type = PinType.Execution }); } + + public override NodeDescriptor GetDescriptor() => new( + Width: 100, Height: 50, + InputPins: [ + new PinDescriptor("Exec", PinType.Execution, 20), + new PinDescriptor("Milliseconds", PinType.Integer, 35) + ], + OutputPins: [new PinDescriptor("Exec", PinType.Execution, 25)], + DisplayName: "Pause" + ); } /// diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/INodeCreationService.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/INodeCreationService.cs deleted file mode 100644 index d3854c6..0000000 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/INodeCreationService.cs +++ /dev/null @@ -1,62 +0,0 @@ -namespace KitX.Core.Contract.Workflow; - -/// -/// Interface for node creation service -/// -public interface INodeCreationService -{ - /// - /// Creates a constant node - /// - ConstNode CreateConstNode(string name, string type, string? defaultValue); - - /// - /// Creates an entry node - /// - EntryNode CreateEntryNode(double x, double y); - - /// - /// Creates a get variable node - /// - GetNode CreateGetNode(string varName); - - /// - /// Creates a set variable node - /// - SetNode CreateSetNode(string varName); - - /// - /// Creates a print node - /// - PrintNode CreatePrintNode(); - - /// - /// Creates a pause node - /// - PauseNode CreatePauseNode(); - - /// - /// Creates a call function node - /// - CallNode CreateCallNode(string functionName); - - /// - /// Creates a call helper function node - /// - CallHelperNode CreateCallHelperNode(string helperFunctionName); - - /// - /// Creates a branch node - /// - BranchNode CreateBranchNode(); - - /// - /// Creates a loop node - /// - LoopNode CreateLoopNode(); - - /// - /// Creates a break node - /// - BreakNode CreateBreakNode(); -} diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/INodeRegistry.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/INodeRegistry.cs new file mode 100644 index 0000000..40d24f5 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/INodeRegistry.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; + +namespace KitX.Core.Contract.Workflow; + +/// +/// Unified registry for node type creation and metadata. +/// Replaces INodeCreationService and INodeTemplateProvider with a single, +/// self-describing approach where each node type carries its own descriptor. +/// +public interface INodeRegistry +{ + /// + /// Creates a new node instance of the given type with default pin configuration. + /// + BlueprintNode Create(BlueprintNodeType type); + + /// + /// Gets the descriptor (layout + pin info) for a node type. + /// Delegates to the node's GetDescriptor() method. + /// Results are cached for performance. + /// + NodeDescriptor GetDescriptor(BlueprintNodeType type); + + /// + /// Returns all registered node types. + /// + IReadOnlySet RegisteredTypes { get; } +} diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/INodeTemplateProvider.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/INodeTemplateProvider.cs deleted file mode 100644 index 1a0b30b..0000000 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/INodeTemplateProvider.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System.Collections.Generic; - -namespace KitX.Core.Contract.Workflow; - -/// -/// 节点模板接口 - 统一手动和自动创建的节点定义 -/// -public interface INodeTemplateProvider -{ - /// - /// 创建指定类型的节点 - /// - BlueprintNode CreateNode(BlueprintNodeType type); - - /// - /// 获取节点模板 - /// - IReadOnlyDictionary GetTemplates(); - - /// - /// 获取节点尺寸 - /// - (double Width, double Height) GetNodeSize(BlueprintNodeType type); - - /// - /// 获取输入引脚的相对 Y 坐标 - /// - double GetInputPinY(BlueprintNodeType nodeType, string pinName); - - /// - /// 获取输出引脚的相对 Y 坐标 - /// - double GetOutputPinY(BlueprintNodeType nodeType, string pinName); -} - -/// -/// 节点模板定义 -/// -public class NodeTemplate -{ - public BlueprintNodeType NodeType { get; set; } - public string Name { get; set; } = string.Empty; - public double Width { get; set; } = 120; - public double Height { get; set; } = 60; - public List InputPins { get; set; } = []; - public List OutputPins { get; set; } = []; -} - -/// -/// 引脚模板定义 -/// -public class PinTemplate -{ - public string Name { get; set; } = string.Empty; - public PinType Type { get; set; } = PinType.Any; - public double RelativeY { get; set; } -} From 302d65fe3c446ca62c50607022fb50115d316ee7 Mon Sep 17 00:00:00 2001 From: StarInk Date: Tue, 7 Apr 2026 11:44:12 +0200 Subject: [PATCH 31/60] =?UTF-8?q?=F0=9F=92=BE=20Feat(Workflow):=20Implemen?= =?UTF-8?q?t=20Blueprint=20service=20-=20bp2bs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Workflow/BlueprintModels.cs | 54 ++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/BlueprintModels.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/BlueprintModels.cs index 15e8709..4b8c8ee 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/BlueprintModels.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/BlueprintModels.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json.Serialization; using System.Threading.Tasks; namespace KitX.Core.Contract.Workflow; @@ -238,8 +239,10 @@ public abstract class BlueprintNode public List OutputPins { get; set; } = []; /// - /// Parent blueprint reference (set when node is added to blueprint) + /// Parent blueprint reference (set when node is added to blueprint). + /// Ignored during JSON serialization to prevent circular reference. /// + [JsonIgnore] public Blueprint? Blueprint { get; set; } /// @@ -782,6 +785,49 @@ public class BlueprintConnection public string? PubVarName { get; set; } } +/// +/// Records a named block scope within a Blueprint. +/// Captures which nodes belong to a logical block, +/// preserving BlockScript block boundaries for reverse conversion. +/// +public class BlueprintBlockScope +{ + /// + /// Block name (stable across round-trips, e.g. "MainBlock", "LoopBody", "SuccessLogic"). + /// + public string Name { get; set; } = string.Empty; + + /// + /// Ordered list of node IDs that belong to this block. + /// + public List NodeIds { get; set; } = []; + + /// + /// Name of the next block to execute when this block ends naturally + /// (i.e., not ended by Branch/Loop/LoopBodyEnd). Null if the block ends + /// with a control-flow statement or is terminal. + /// + public string? NextBlockName { get; set; } + + /// + /// For sub-blocks: the node ID of the Branch/Loop node that created this scope. + /// Null for the main block scope. + /// + public string? OwnerNodeId { get; set; } + + /// + /// For sub-blocks: which output arm of the owner node leads into this scope. + /// E.g., "True", "False" for Branch; "LoopBody", "LoopEnd" for Loop. + /// Null for the main block scope. + /// + public string? OwnerArmName { get; set; } + + /// + /// Whether this block scope represents the main entry block. + /// + public bool IsMainBlock { get; set; } +} + /// /// Blueprint document container /// @@ -842,6 +888,12 @@ public class Blueprint /// public List ConstValues { get; set; } = []; + /// + /// Named block scopes recording which nodes belong to which logical block. + /// Empty for legacy blueprints that predate this field. + /// + public List BlockScopes { get; set; } = []; + /// /// Get node by ID /// From 0740abece8fd6655a2a7155bcba2ddb18031c560 Mon Sep 17 00:00:00 2001 From: StarInk Date: Tue, 7 Apr 2026 22:29:15 +0200 Subject: [PATCH 32/60] =?UTF-8?q?=F0=9F=92=BE=20Feat(Blueprint):=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=BA=90=E4=BB=A3=E7=A0=81=E9=87=8D=E7=94=9F?= =?UTF-8?q?=E6=96=B9=E6=B3=95=E5=B9=B6=E4=BC=98=E5=8C=96=E8=8A=82=E7=82=B9?= =?UTF-8?q?=E6=9E=84=E9=80=A0=E5=87=BD=E6=95=B0=E4=BB=A5=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E6=8F=8F=E8=BF=B0=E7=AC=A6=E5=88=9D=E5=A7=8B=E5=8C=96=E5=BC=95?= =?UTF-8?q?=E8=84=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Workflow/BlockScriptModels.cs | 18 ++ .../Workflow/BlueprintModels.cs | 199 +++--------------- 2 files changed, 43 insertions(+), 174 deletions(-) diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/BlockScriptModels.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/BlockScriptModels.cs index c9375a7..fe47e70 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/BlockScriptModels.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/BlockScriptModels.cs @@ -201,6 +201,24 @@ public class FlowControlStatement : BlockStatement /// For LoopBodyEnd: the block name containing the Loop statement to return to /// public string? LoopBodyEndReturnTo { get; set; } + + /// + /// Regenerates SourceCode from current field values. + /// Call after updating TrueBlockName/FalseBlockName/etc. to keep SourceCode in sync. + /// + public void RegenerateSourceCode() + { + SourceCode = ControlType switch + { + FlowControlType.Branch => $"NextBlock = Branch({ConditionExpression}, \"{TrueBlockName}\", \"{FalseBlockName}\");", + FlowControlType.Loop => $"NextBlock = Loop({ConditionExpression}, \"{TrueBlockName}\", \"{FalseBlockName}\");", + FlowControlType.LoopBodyEnd => LoopBodyEndReturnTo != null + ? $"NextBlock = LoopBodyEnd(\"{LoopBodyEndReturnTo}\");" + : "LoopBodyEnd();", + FlowControlType.Break => "Break();", + _ => SourceCode + }; + } } /// diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/BlueprintModels.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/BlueprintModels.cs index 4b8c8ee..1a1da31 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/BlueprintModels.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/BlueprintModels.cs @@ -280,6 +280,20 @@ public IEnumerable GetAllPins() /// Default implementation returns Name; subclasses override for richer display. /// public virtual string GetDisplayTitle() => Name; + + /// + /// Initializes InputPins and OutputPins from GetDescriptor(). + /// Subclasses should call this in their constructor instead of manually adding pins. + /// This ensures the descriptor is the single source of truth for pin layout. + /// + protected void InitializePinsFromDescriptor() + { + var desc = GetDescriptor(); + foreach (var pd in desc.InputPins) + InputPins.Add(new BlueprintPin { Name = pd.Name, Direction = PinDirection.Input, Type = pd.Type }); + foreach (var pd in desc.OutputPins) + OutputPins.Add(new BlueprintPin { Name = pd.Name, Direction = PinDirection.Output, Type = pd.Type }); + } } // Node-specific classes can be defined for additional properties @@ -294,12 +308,7 @@ public EntryNode() { NodeType = BlueprintNodeType.Entry; Name = "Entry"; - OutputPins.Add(new BlueprintPin - { - Name = "Exec", - Direction = PinDirection.Output, - Type = PinType.Execution - }); + InitializePinsFromDescriptor(); } public override NodeDescriptor GetDescriptor() => new( @@ -319,30 +328,7 @@ public BranchNode() { NodeType = BlueprintNodeType.Branch; Name = "Branch"; - InputPins.Add(new BlueprintPin - { - Name = "Exec", - Direction = PinDirection.Input, - Type = PinType.Execution - }); - InputPins.Add(new BlueprintPin - { - Name = "Condition", - Direction = PinDirection.Input, - Type = PinType.Boolean - }); - OutputPins.Add(new BlueprintPin - { - Name = "True", - Direction = PinDirection.Output, - Type = PinType.Execution - }); - OutputPins.Add(new BlueprintPin - { - Name = "False", - Direction = PinDirection.Output, - Type = PinType.Execution - }); + InitializePinsFromDescriptor(); } public override NodeDescriptor GetDescriptor() => new( @@ -368,30 +354,7 @@ public LoopNode() { NodeType = BlueprintNodeType.Loop; Name = "Loop"; - InputPins.Add(new BlueprintPin - { - Name = "Exec", - Direction = PinDirection.Input, - Type = PinType.Execution - }); - InputPins.Add(new BlueprintPin - { - Name = "Condition", - Direction = PinDirection.Input, - Type = PinType.Boolean - }); - OutputPins.Add(new BlueprintPin - { - Name = "LoopBody", - Direction = PinDirection.Output, - Type = PinType.Execution - }); - OutputPins.Add(new BlueprintPin - { - Name = "LoopEnd", - Direction = PinDirection.Output, - Type = PinType.Execution - }); + InitializePinsFromDescriptor(); } public override NodeDescriptor GetDescriptor() => new( @@ -417,12 +380,7 @@ public BreakNode() { NodeType = BlueprintNodeType.Break; Name = "Break"; - InputPins.Add(new BlueprintPin - { - Name = "Exec", - Direction = PinDirection.Input, - Type = PinType.Execution - }); + InitializePinsFromDescriptor(); } public override NodeDescriptor GetDescriptor() => new( @@ -457,12 +415,7 @@ public ConstNode() { NodeType = BlueprintNodeType.Const; Name = "Const"; - OutputPins.Add(new BlueprintPin - { - Name = "Value", - Direction = PinDirection.Output, - Type = PinType.Any - }); + InitializePinsFromDescriptor(); } public override NodeDescriptor GetDescriptor() => new( @@ -494,24 +447,7 @@ public CallNode() { NodeType = BlueprintNodeType.Call; Name = "Call"; - InputPins.Add(new BlueprintPin - { - Name = "Exec", - Direction = PinDirection.Input, - Type = PinType.Execution - }); - OutputPins.Add(new BlueprintPin - { - Name = "Exec", - Direction = PinDirection.Output, - Type = PinType.Execution - }); - OutputPins.Add(new BlueprintPin - { - Name = "Return", - Direction = PinDirection.Output, - Type = PinType.Any - }); + InitializePinsFromDescriptor(); } public override NodeDescriptor GetDescriptor() => new( @@ -542,24 +478,7 @@ public CallHelperNode() { NodeType = BlueprintNodeType.CallHelper; Name = "CallHelper"; - InputPins.Add(new BlueprintPin - { - Name = "Exec", - Direction = PinDirection.Input, - Type = PinType.Execution - }); - OutputPins.Add(new BlueprintPin - { - Name = "Exec", - Direction = PinDirection.Output, - Type = PinType.Execution - }); - OutputPins.Add(new BlueprintPin - { - Name = "Return", - Direction = PinDirection.Output, - Type = PinType.Any - }); + InitializePinsFromDescriptor(); } public override NodeDescriptor GetDescriptor() => new( @@ -589,24 +508,7 @@ public GetNode() { NodeType = BlueprintNodeType.Get; Name = "Get"; - InputPins.Add(new BlueprintPin - { - Name = "Exec", - Direction = PinDirection.Input, - Type = PinType.Execution - }); - OutputPins.Add(new BlueprintPin - { - Name = "Exec", - Direction = PinDirection.Output, - Type = PinType.Execution - }); - OutputPins.Add(new BlueprintPin - { - Name = "Value", - Direction = PinDirection.Output, - Type = PinType.Any - }); + InitializePinsFromDescriptor(); } public override NodeDescriptor GetDescriptor() => new( @@ -636,24 +538,7 @@ public SetNode() { NodeType = BlueprintNodeType.Set; Name = "Set"; - InputPins.Add(new BlueprintPin - { - Name = "Exec", - Direction = PinDirection.Input, - Type = PinType.Execution - }); - InputPins.Add(new BlueprintPin - { - Name = "Value", - Direction = PinDirection.Input, - Type = PinType.Any - }); - OutputPins.Add(new BlueprintPin - { - Name = "Exec", - Direction = PinDirection.Output, - Type = PinType.Execution - }); + InitializePinsFromDescriptor(); } public override NodeDescriptor GetDescriptor() => new( @@ -678,24 +563,7 @@ public PrintNode() { NodeType = BlueprintNodeType.Print; Name = "Print"; - InputPins.Add(new BlueprintPin - { - Name = "Exec", - Direction = PinDirection.Input, - Type = PinType.Execution - }); - InputPins.Add(new BlueprintPin - { - Name = "Value", - Direction = PinDirection.Input, - Type = PinType.Any - }); - OutputPins.Add(new BlueprintPin - { - Name = "Exec", - Direction = PinDirection.Output, - Type = PinType.Execution - }); + InitializePinsFromDescriptor(); } public override NodeDescriptor GetDescriptor() => new( @@ -718,24 +586,7 @@ public PauseNode() { NodeType = BlueprintNodeType.Pause; Name = "Pause"; - InputPins.Add(new BlueprintPin - { - Name = "Exec", - Direction = PinDirection.Input, - Type = PinType.Execution - }); - InputPins.Add(new BlueprintPin - { - Name = "Milliseconds", - Direction = PinDirection.Input, - Type = PinType.Integer - }); - OutputPins.Add(new BlueprintPin - { - Name = "Exec", - Direction = PinDirection.Output, - Type = PinType.Execution - }); + InitializePinsFromDescriptor(); } public override NodeDescriptor GetDescriptor() => new( From dd7a5549228d06be1466e36fb285d9e4aa1fa97f Mon Sep 17 00:00:00 2001 From: StarInk Date: Thu, 9 Apr 2026 02:26:26 +0200 Subject: [PATCH 33/60] =?UTF-8?q?=F0=9F=92=BE=20Feat(Blueprint):=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=A4=9A=E6=80=81=20JSON=20=E5=BA=8F?= =?UTF-8?q?=E5=88=97=E5=8C=96=E6=94=AF=E6=8C=81=EF=BC=8C=E4=BB=A5=E7=A1=AE?= =?UTF-8?q?=E4=BF=9D=E5=85=B7=E4=BD=93=E8=8A=82=E7=82=B9=E7=B1=BB=E5=9E=8B?= =?UTF-8?q?=E9=80=9A=E8=BF=87=20System.Text.Json=20=E6=AD=A3=E7=A1=AE?= =?UTF-8?q?=E5=9B=9E=E4=BC=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Workflow/BlueprintModels.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/BlueprintModels.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/BlueprintModels.cs index 1a1da31..67a3a8a 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/BlueprintModels.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/BlueprintModels.cs @@ -184,8 +184,22 @@ public class BlueprintPin } /// -/// Base class for all blueprint nodes +/// Base class for all blueprint nodes. +/// Uses polymorphic JSON serialization so that concrete node types +/// round-trip correctly through System.Text.Json. /// +[JsonPolymorphic(UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(EntryNode), "Entry")] +[JsonDerivedType(typeof(BranchNode), "Branch")] +[JsonDerivedType(typeof(LoopNode), "Loop")] +[JsonDerivedType(typeof(BreakNode), "Break")] +[JsonDerivedType(typeof(ConstNode), "Const")] +[JsonDerivedType(typeof(CallNode), "Call")] +[JsonDerivedType(typeof(CallHelperNode), "CallHelper")] +[JsonDerivedType(typeof(GetNode), "Get")] +[JsonDerivedType(typeof(SetNode), "Set")] +[JsonDerivedType(typeof(PrintNode), "Print")] +[JsonDerivedType(typeof(PauseNode), "Pause")] public abstract class BlueprintNode { /// From 1788c03405428dd6744a7cb1a116f7e98cc2e177 Mon Sep 17 00:00:00 2001 From: StarInk Date: Thu, 9 Apr 2026 15:38:57 +0200 Subject: [PATCH 34/60] =?UTF-8?q?=F0=9F=92=BE=20Feat(Workflow):=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20IWorkflowEditorBridge=20=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E4=BB=A5=E6=94=AF=E6=8C=81=20BlueprintEditor=20=E5=92=8C=20Wor?= =?UTF-8?q?kflowEditor=20=E4=B9=8B=E9=97=B4=E7=9A=84=E9=80=9A=E4=BF=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Workflow/IWorkflowEditorBridge.cs | 36 +++++++++++++++++++ .../Workflow/IWorkflowService.cs | 6 ++++ 2 files changed, 42 insertions(+) create mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowEditorBridge.cs diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowEditorBridge.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowEditorBridge.cs new file mode 100644 index 0000000..98065ce --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowEditorBridge.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; + +namespace KitX.Core.Contract.Workflow; + +/// +/// Bridge interface for communication between BlueprintEditor and WorkflowEditor. +/// BlueprintEditor always runs in association with a WorkflowEditor instance. +/// +public interface IWorkflowEditorBridge +{ + /// + /// Gets the current BlockScript source code from the WorkflowEditor's main program area + /// + string? GetCurrentScript(); + + /// + /// Gets the current helper functions list from the WorkflowEditor + /// + List? GetHelperFunctions(); + + /// + /// Writes BlockScript source code back to the WorkflowEditor's main program area + /// and triggers UI update + /// + void SetScript(string sourceCode, List? helpers); + + /// + /// Appends output text to the WorkflowEditor's Output panel + /// + void AppendOutput(string output); + + /// + /// Triggers script execution in the WorkflowEditor + /// + void TriggerExecution(); +} diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowService.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowService.cs index d0d9e80..2346e13 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowService.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowService.cs @@ -118,6 +118,12 @@ public interface IBlockScriptService /// BlockScriptValidationResult ValidateBlockScript(string sourceCode); + /// + /// Parses constants from a BlockScript source's #ConstBlock section. + /// Only returns variables that have initial values (DefaultValue != null). + /// + List ParseConstantsFromBlockScript(string sourceCode); + /// /// Executes a block script /// From ff4aa0dd6623d0796702bbcda0e503afe1ada21b Mon Sep 17 00:00:00 2001 From: StarInk Date: Thu, 9 Apr 2026 19:40:53 +0200 Subject: [PATCH 35/60] =?UTF-8?q?=F0=9F=92=BE=20Feat(Blueprint):=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20VariableNode=20=E7=B1=BB=E5=9E=8B=E4=B8=8E?= =?UTF-8?q?=E5=B8=B8=E9=87=8F=E8=A6=86=E7=9B=96=E6=89=A7=E8=A1=8C=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 Variable 枚举值与 VariableNode 类(无端口浮游节点,用于无初始值变量声明) - 注册 JsonDerivedType 支持多态序列化 - IWorkflowService 新增带 constantOverrides 参数的 ExecuteBlockScriptAsync 重载 --- .../Workflow/BlueprintModels.cs | 43 ++++++++++++++++++- .../Workflow/IWorkflowService.cs | 10 +++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/BlueprintModels.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/BlueprintModels.cs index 67a3a8a..e2c23ae 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/BlueprintModels.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/BlueprintModels.cs @@ -82,7 +82,13 @@ public enum BlueprintNodeType /// /// Pause execution node /// - Pause + Pause, + + /// + /// Variable declaration node (ConstBlock variables without initial values). + /// A floating node with no ports — users can only change the data type. + /// + Variable } /// @@ -200,6 +206,7 @@ public class BlueprintPin [JsonDerivedType(typeof(SetNode), "Set")] [JsonDerivedType(typeof(PrintNode), "Print")] [JsonDerivedType(typeof(PauseNode), "Pause")] +[JsonDerivedType(typeof(VariableNode), "Variable")] public abstract class BlueprintNode { /// @@ -614,6 +621,40 @@ public PauseNode() ); } +/// +/// Variable node - ConstBlock variable without initial value. +/// A floating node with no input/output ports. Users can change the data type +/// but not the initial value. Type changes propagate to Get/Set nodes. +/// +public class VariableNode : BlueprintNode +{ + /// + /// Variable name + /// + public string VarName { get; set; } = string.Empty; + + /// + /// Variable type (e.g., "int", "double", "string", "bool") + /// + public string VarType { get; set; } = "int"; + + public VariableNode() + { + NodeType = BlueprintNodeType.Variable; + Name = "Variable"; + InitializePinsFromDescriptor(); + } + + public override NodeDescriptor GetDescriptor() => new( + Width: 120, Height: 50, + InputPins: [], + OutputPins: [], + DisplayName: "Variable" + ); + + public override string GetDisplayTitle() => $"Var: {VarName}"; +} + /// /// Connection between two pins /// diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowService.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowService.cs index 2346e13..9ef2bd7 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowService.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowService.cs @@ -147,6 +147,16 @@ Task ExecuteBlockScriptAsync( string sourceCode, List helperFunctions, System.Threading.CancellationToken cancellationToken = default); + + /// + /// Executes a block script from source code with helper functions and constant overrides. + /// Constant overrides replace the DefaultValue on ConstBlock variables before execution. + /// + Task ExecuteBlockScriptAsync( + string sourceCode, + List helperFunctions, + Dictionary? constantOverrides, + System.Threading.CancellationToken cancellationToken = default); } /// From 27bbe787977fc04a15a33aeef112161f5fcee23e Mon Sep 17 00:00:00 2001 From: StarInk Date: Fri, 10 Apr 2026 03:14:52 +0200 Subject: [PATCH 36/60] =?UTF-8?q?=F0=9F=92=BE=20Feat(Contract):=20IPluginS?= =?UTF-8?q?erver=20=E6=8E=A5=E5=8F=A3=E6=96=B0=E5=A2=9E=20Connections=20?= =?UTF-8?q?=E5=B1=9E=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 IReadOnlyList Connections { get; } - 允许 UI 层通过 DI 接口访问已连接插件的 PluginInfo.Functions --- .../KitX.Core.Contract/Plugin/IPluginService.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/KitX Core Contracts/KitX.Core.Contract/Plugin/IPluginService.cs b/KitX Core Contracts/KitX.Core.Contract/Plugin/IPluginService.cs index 60561f7..d09a441 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Plugin/IPluginService.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Plugin/IPluginService.cs @@ -77,6 +77,11 @@ public interface IPluginServer /// int? Port { get; } + /// + /// Gets the list of currently connected plugins + /// + IReadOnlyList Connections { get; } + /// /// Starts the plugin server /// From 639b79de393e7585b1b38d4adce844697a3e03fd Mon Sep 17 00:00:00 2001 From: StarInk Date: Fri, 10 Apr 2026 18:22:54 +0200 Subject: [PATCH 37/60] =?UTF-8?q?=F0=9F=92=BE=20Feat(Blueprint):=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20BuiltinFunctionNode=20=E7=B1=BB=E5=9E=8B?= =?UTF-8?q?=E5=8F=8A=E5=85=B6=E5=9C=A8=20INodeRegistry=20=E4=B8=AD?= =?UTF-8?q?=E7=9A=84=E5=88=9B=E5=BB=BA=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Workflow/BlueprintModels.cs | 49 ++++++++++++++++++- .../Workflow/INodeRegistry.cs | 10 ++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/BlueprintModels.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/BlueprintModels.cs index e2c23ae..695cdbf 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/BlueprintModels.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/BlueprintModels.cs @@ -88,7 +88,13 @@ public enum BlueprintNodeType /// Variable declaration node (ConstBlock variables without initial values). /// A floating node with no ports — users can only change the data type. /// - Variable + Variable, + + /// + /// 通用内置函数节点。通过 BuiltinFunctionNode.FunctionName 区分具体函数。 + /// 新的内置函数统一使用此类型,无需为每个函数创建专用 enum 值。 + /// + BuiltinFunction } /// @@ -207,6 +213,7 @@ public class BlueprintPin [JsonDerivedType(typeof(PrintNode), "Print")] [JsonDerivedType(typeof(PauseNode), "Pause")] [JsonDerivedType(typeof(VariableNode), "Variable")] +[JsonDerivedType(typeof(BuiltinFunctionNode), "BuiltinFunction")] public abstract class BlueprintNode { /// @@ -655,6 +662,46 @@ public VariableNode() public override string GetDisplayTitle() => $"Var: {VarName}"; } +/// +/// 通用内置函数节点。通过 区分具体函数。 +/// 引脚布局由 驱动, +/// 消除了为每个内置函数创建专用节点子类的需要。 +/// +public class BuiltinFunctionNode : BlueprintNode +{ + /// + /// BlockScript 函数名(如 "Flip"),作为具体函数的唯一标识 + /// + public string FunctionName { get; set; } = string.Empty; + + /// + /// 额外属性字典,用于特殊节点(如 Set 的 VarName、Get 的 VarName) + /// + public Dictionary Properties { get; set; } = []; + + private NodeDescriptor? _descriptor; + + /// + /// 由 BuiltinFunctionRegistry 在创建节点时设置,基于 IBuiltinFunctionDefinition 的引脚描述 + /// + public void SetDescriptor(NodeDescriptor descriptor) => _descriptor = descriptor; + + public override NodeDescriptor GetDescriptor() => _descriptor ?? new( + Width: 120, Height: 60, + InputPins: [], + OutputPins: [], + DisplayName: FunctionName + ); + + public BuiltinFunctionNode() + { + NodeType = BlueprintNodeType.BuiltinFunction; + Name = "BuiltinFunction"; + } + + public override string GetDisplayTitle() => FunctionName; +} + /// /// Connection between two pins /// diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/INodeRegistry.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/INodeRegistry.cs index 40d24f5..eaa259e 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/INodeRegistry.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/INodeRegistry.cs @@ -25,4 +25,14 @@ public interface INodeRegistry /// Returns all registered node types. /// IReadOnlySet RegisteredTypes { get; } + + /// + /// Creates a pre-configured from an + /// . The node's pins, dimensions, + /// and display name are all driven by the definition. + /// + /// The built-in function name (must be registered in BuiltinFunctionRegistry) + /// A fully configured BuiltinFunctionNode + /// Thrown when the function name is not registered + BlueprintNode CreateBuiltinFunctionNode(string functionName); } From 4dead353c2048e17f8a45f4b329c4c588ae71b1b Mon Sep 17 00:00:00 2001 From: StarInk Date: Sat, 11 Apr 2026 03:11:11 +0200 Subject: [PATCH 38/60] =?UTF-8?q?=F0=9F=94=A7=20Fix(Kscript):=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=20Device/Function/Plugin=20=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E7=B1=BB=E7=9A=84=E5=8F=AF=E7=A9=BA=E5=BC=95=E7=94=A8=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E7=BC=96=E8=AF=91=E8=AD=A6=E5=91=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CodeGen/MethodEmitter.cs | 2 +- .../Kscript.CSharp.Parser.csproj | 1 - KitX Script/Kscript.CSharp/Services/Device.cs | 18 +++++++++--------- .../Kscript.CSharp/Services/Function.cs | 8 ++++---- KitX Script/Kscript.CSharp/Services/Plugin.cs | 10 +++++----- .../Kscript.CSharp/Utils/DeviceCache.cs | 8 +++----- .../Utils/DeviceRequestBuilder.cs | 2 +- .../Kscript.CSharp/Utils/ResponseHandler.cs | 2 +- 8 files changed, 24 insertions(+), 27 deletions(-) diff --git a/KitX Script/Kscript.CSharp.Parser/CodeGen/MethodEmitter.cs b/KitX Script/Kscript.CSharp.Parser/CodeGen/MethodEmitter.cs index a044907..e850f3b 100644 --- a/KitX Script/Kscript.CSharp.Parser/CodeGen/MethodEmitter.cs +++ b/KitX Script/Kscript.CSharp.Parser/CodeGen/MethodEmitter.cs @@ -20,7 +20,7 @@ public CollectibleAssemblyLoadContext(string name) : base(name, isCollectible: t protected override Assembly Load(AssemblyName assemblyName) { // 让默认上下文处理核心程序集加载 - return null; + return null!; } } diff --git a/KitX Script/Kscript.CSharp.Parser/Kscript.CSharp.Parser.csproj b/KitX Script/Kscript.CSharp.Parser/Kscript.CSharp.Parser.csproj index fe53ec8..37863bf 100644 --- a/KitX Script/Kscript.CSharp.Parser/Kscript.CSharp.Parser.csproj +++ b/KitX Script/Kscript.CSharp.Parser/Kscript.CSharp.Parser.csproj @@ -10,7 +10,6 @@ - diff --git a/KitX Script/Kscript.CSharp/Services/Device.cs b/KitX Script/Kscript.CSharp/Services/Device.cs index 52d222d..3aef2b5 100644 --- a/KitX Script/Kscript.CSharp/Services/Device.cs +++ b/KitX Script/Kscript.CSharp/Services/Device.cs @@ -22,10 +22,10 @@ public Device(DeviceInfo info, Connector? connector = null) public async Task RequestPlugin(string Name) { var plugins = await GetPluginList(); - var pluginInfo = plugins.FirstOrDefault(p => + var pluginInfo = plugins.FirstOrDefault(p => p.Name.Equals(Name, StringComparison.OrdinalIgnoreCase)); - - return pluginInfo != null ? await CreatePluginInstance(pluginInfo) : null; + + return pluginInfo != null ? await CreatePluginInstance(pluginInfo) : null!; } public async Task> GetPluginList() @@ -50,15 +50,15 @@ void OnResponse(Request response) // With the following code var plugins = JsonSerializer.Deserialize>(content); - + // Update cache - _pluginCache = plugins.ToDictionary( + _pluginCache = plugins!.ToDictionary( p => p.Name, p => p, StringComparer.OrdinalIgnoreCase ); - tcs.SetResult(plugins); + tcs.SetResult(plugins!); return content; }), matchCommand: content => @@ -72,15 +72,15 @@ void OnResponse(Request response) { var pluginListJson = content.Substring("PluginList:".Length); var plugins = JsonSerializer.Deserialize>(pluginListJson); - + // Update cache - _pluginCache = plugins.ToDictionary( + _pluginCache = plugins!.ToDictionary( p => p.Name, p => p, StringComparer.OrdinalIgnoreCase ); - tcs.SetResult(plugins); + tcs.SetResult(plugins!); } } ); diff --git a/KitX Script/Kscript.CSharp/Services/Function.cs b/KitX Script/Kscript.CSharp/Services/Function.cs index f9aefba..a50a068 100644 --- a/KitX Script/Kscript.CSharp/Services/Function.cs +++ b/KitX Script/Kscript.CSharp/Services/Function.cs @@ -48,14 +48,14 @@ void OnResponse(Request response) { if (string.IsNullOrEmpty(content)) { - tcs.SetResult(null); + tcs.SetResult(null!); return content; } // Parse response based on return type var returnType = GetReturnType(); var result = ParseResponse(content, returnType); - tcs.SetResult(result); + tcs.SetResult(result!); return content; }), matchCommand: content => @@ -70,7 +70,7 @@ void OnResponse(Request response) var resultJson = content.Substring("Result:".Length); var returnType = GetReturnType(); var result = ParseResponse(resultJson, returnType); - tcs.SetResult(result); + tcs.SetResult(result!); } } ); @@ -120,7 +120,7 @@ public Type GetReturnType() } } - private object ParseResponse(string content, Type returnType) + private object? ParseResponse(string content, Type returnType) { if (string.IsNullOrEmpty(content)) return null; diff --git a/KitX Script/Kscript.CSharp/Services/Plugin.cs b/KitX Script/Kscript.CSharp/Services/Plugin.cs index b3f9a45..0ab0a82 100644 --- a/KitX Script/Kscript.CSharp/Services/Plugin.cs +++ b/KitX Script/Kscript.CSharp/Services/Plugin.cs @@ -19,10 +19,10 @@ public Plugin(PluginInfo info, DeviceInfo deviceInfo, Connector? connector = nul _connector = connector ?? Connector.Instance; } - public async Task RequestFunction(string Name) + public async Task RequestFunction(string Name) { var functions = await GetFunctionList(); - var function = functions.FirstOrDefault(f => + var function = functions.FirstOrDefault(f => f.Info.Name.Equals(Name, StringComparison.OrdinalIgnoreCase)); return function; } @@ -69,7 +69,7 @@ void OnResponse(Request response) { var returnType = GetFunctionReturnType(functionName); var result = ParseFunctionResponse(content, returnType); - tcs.SetResult(result); + tcs.SetResult(result!); return content; }), matchCommand: content => @@ -84,7 +84,7 @@ void OnResponse(Request response) var resultJson = content.Substring("Result:".Length); var returnType = GetFunctionReturnType(functionName); var result = ParseFunctionResponse(resultJson, returnType); - tcs.SetResult(result); + tcs.SetResult(result!); } } ); @@ -140,7 +140,7 @@ private Type GetFunctionReturnType(string functionName) } } - private object ParseFunctionResponse(string content, Type returnType) + private object? ParseFunctionResponse(string content, Type returnType) { if (string.IsNullOrEmpty(content)) return null; diff --git a/KitX Script/Kscript.CSharp/Utils/DeviceCache.cs b/KitX Script/Kscript.CSharp/Utils/DeviceCache.cs index 7598a04..3f8989e 100644 --- a/KitX Script/Kscript.CSharp/Utils/DeviceCache.cs +++ b/KitX Script/Kscript.CSharp/Utils/DeviceCache.cs @@ -10,7 +10,7 @@ public class DeviceCache : IDisposable { private class CacheEntry { - public T Value { get; set; } + public required T Value { get; set; } public DateTime Expiration { get; set; } public DateTime CreatedAt { get; } = DateTime.UtcNow; private int _accessCount; @@ -72,8 +72,7 @@ public void Set(string key, IEnumerable value, TimeSpan? expiration /// public IEnumerable? Get(string key) { - var entry = _cache.GetOrAdd(key, _ => null); - if (entry == null || entry.IsExpired) + if (!_cache.TryGetValue(key, out var entry) || entry.IsExpired) { _cache.TryRemove(key, out _); return null; @@ -88,8 +87,7 @@ public void Set(string key, IEnumerable value, TimeSpan? expiration /// public bool IsValid(string key) { - var entry = _cache.GetOrAdd(key, _ => null); - return entry != null && !entry.IsExpired; + return _cache.TryGetValue(key, out var entry) && !entry.IsExpired; } /// diff --git a/KitX Script/Kscript.CSharp/Utils/DeviceRequestBuilder.cs b/KitX Script/Kscript.CSharp/Utils/DeviceRequestBuilder.cs index 8ffa998..54b3ea0 100644 --- a/KitX Script/Kscript.CSharp/Utils/DeviceRequestBuilder.cs +++ b/KitX Script/Kscript.CSharp/Utils/DeviceRequestBuilder.cs @@ -127,7 +127,7 @@ public DeviceRequestBuilder Clone() clone._command = new Command { FunctionName = _command.FunctionName, - FunctionArgs = _command.FunctionArgs?.ToList() + FunctionArgs = _command.FunctionArgs?.ToList() ?? new() }; clone._request = new Request { diff --git a/KitX Script/Kscript.CSharp/Utils/ResponseHandler.cs b/KitX Script/Kscript.CSharp/Utils/ResponseHandler.cs index bba69b5..6bfa4ac 100644 --- a/KitX Script/Kscript.CSharp/Utils/ResponseHandler.cs +++ b/KitX Script/Kscript.CSharp/Utils/ResponseHandler.cs @@ -37,7 +37,7 @@ public static async Task HandleResponse( result = commandHandler(command); } ); - return result; + return result!; } catch (Exception ex) { From 88f7e0ae75fe93775e595966d11bfcd53477609a Mon Sep 17 00:00:00 2001 From: StarInk Date: Sat, 11 Apr 2026 18:50:11 +0200 Subject: [PATCH 39/60] =?UTF-8?q?=F0=9F=92=BE=20Feat(Workflow):=20?= =?UTF-8?q?=E6=89=A9=E5=B1=95=20IWorkflowCase=20=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=EF=BC=8CIconPath=20=E6=94=B9=E4=B8=BA=20Author=EF=BC=8C?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=97=B6=E9=97=B4=E6=88=B3=E4=B8=8E=E8=A7=A6?= =?UTF-8?q?=E5=8F=91=E7=B1=BB=E5=9E=8B=E5=AD=97=E6=AE=B5=EF=BC=9B=E6=89=A9?= =?UTF-8?q?=E5=B1=95=20KcsFileFormat=20=E5=B5=8C=E5=85=A5=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E6=B5=81=E5=85=83=E6=95=B0=E6=8D=AE=EF=BC=9B=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=20IWorkflowStorageService=20=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E5=AD=98=E5=82=A8=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Workflow/IWorkflowService.cs | 27 ++++++-- .../Workflow/IWorkflowStorageService.cs | 63 +++++++++++++++++++ .../Workflow/KcsFileFormat.cs | 36 +++++++++++ 3 files changed, 120 insertions(+), 6 deletions(-) create mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowStorageService.cs diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowService.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowService.cs index 9ef2bd7..6e028f1 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowService.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowService.cs @@ -178,19 +178,19 @@ public interface IWorkflowCase string Id { get; } /// - /// Gets the workflow name + /// Gets or sets the workflow name /// - string Name { get; } + string Name { get; set; } /// - /// Gets the workflow description + /// Gets or sets the workflow description /// - string Description { get; } + string Description { get; set; } /// - /// Gets the icon path + /// Gets or sets the author name /// - string IconPath { get; } + string Author { get; set; } /// /// Gets or sets a value indicating whether the workflow is running @@ -201,4 +201,19 @@ public interface IWorkflowCase /// Gets or sets the script file path /// string? ScriptPath { get; set; } + + /// + /// Gets the creation time + /// + DateTime CreatedTime { get; } + + /// + /// Gets or sets the last modified time + /// + DateTime LastModifiedTime { get; set; } + + /// + /// Gets or sets the trigger type (e.g., "Manual", "Scheduled", "Event") + /// + string TriggerType { get; set; } } diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowStorageService.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowStorageService.cs new file mode 100644 index 0000000..31bd4d2 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowStorageService.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace KitX.Core.Contract.Workflow; + +/// +/// Workflow storage service interface - manages workflow file persistence +/// +public interface IWorkflowStorageService +{ + /// + /// Gets the storage directory path (e.g., ./Data/Workflows/) + /// + string StorageDirectory { get; } + + /// + /// Creates a new workflow with a default BlockScript template + /// + /// Workflow name + /// Optional description + /// The created workflow case + Task CreateWorkflowAsync(string name, string? description = null); + + /// + /// Loads workflow data from a .kcs file + /// + /// Workflow ID + /// The loaded KCS file data, or null if not found + Task LoadWorkflowDataAsync(string workflowId); + + /// + /// Saves workflow data to a .kcs file + /// + /// Workflow ID + /// The KCS file data to save + Task SaveWorkflowDataAsync(string workflowId, KcsFileFormat data); + + /// + /// Deletes a workflow and its .kcs file + /// + /// Workflow ID + Task DeleteWorkflowAsync(string workflowId); + + /// + /// Renames a workflow + /// + /// Workflow ID + /// New name + Task RenameWorkflowAsync(string workflowId, string newName); + + /// + /// Discovers all stored workflows by scanning the storage directory + /// + /// List of discovered workflow cases + Task> DiscoverWorkflowsAsync(); + + /// + /// Gets the file path for a workflow + /// + /// Workflow ID + /// Full file path + string GetWorkflowFilePath(string workflowId); +} diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/KcsFileFormat.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/KcsFileFormat.cs index 7bf1d4c..c6a4642 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/KcsFileFormat.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/KcsFileFormat.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Threading.Tasks; @@ -8,6 +9,41 @@ namespace KitX.Core.Contract.Workflow; /// public class KcsFileFormat { + /// + /// 工作流唯一标识符 + /// + public string Id { get; set; } = string.Empty; + + /// + /// 工作流名称 + /// + public string Name { get; set; } = "Untitled Workflow"; + + /// + /// 工作流描述 + /// + public string Description { get; set; } = string.Empty; + + /// + /// 作者名称 + /// + public string Author { get; set; } = string.Empty; + + /// + /// 创建时间 + /// + public DateTime CreatedTime { get; set; } = DateTime.UtcNow; + + /// + /// 最后修改时间 + /// + public DateTime LastModifiedTime { get; set; } = DateTime.UtcNow; + + /// + /// 触发类型("Manual", "Scheduled", "Event" 等) + /// + public string TriggerType { get; set; } = "Manual"; + /// /// 主程序代码 /// From 4c0b204999111ddc77f0b3ddf5aa40fa74784a64 Mon Sep 17 00:00:00 2001 From: StarInk Date: Sun, 12 Apr 2026 01:09:13 +0200 Subject: [PATCH 40/60] =?UTF-8?q?=F0=9F=92=BE=20Feat(Event):=20=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=20WorkflowRenamedEventArgs=20=E5=92=8C=20WorkflowSave?= =?UTF-8?q?dEventArgs=20=E4=BA=8B=E4=BB=B6=E5=8F=82=E6=95=B0=E7=B1=BB?= =?UTF-8?q?=EF=BC=8C=E7=94=A8=E4=BA=8E=E5=A4=84=E7=90=86=E5=B7=A5=E4=BD=9C?= =?UTF-8?q?=E6=B5=81=E9=87=8D=E5=91=BD=E5=90=8D=E5=92=8C=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E4=BF=9D=E5=AD=98=E4=BA=8B=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Event/WorkflowEventArgs.cs | 47 +++++++++++++++++++ .../Workflow/BlockScriptModels.cs | 16 +++---- .../Workflow/BlueprintModels.cs | 2 +- 3 files changed, 56 insertions(+), 9 deletions(-) create mode 100644 KitX Core Contracts/KitX.Core.Contract/Event/WorkflowEventArgs.cs diff --git a/KitX Core Contracts/KitX.Core.Contract/Event/WorkflowEventArgs.cs b/KitX Core Contracts/KitX.Core.Contract/Event/WorkflowEventArgs.cs new file mode 100644 index 0000000..ed9035b --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Event/WorkflowEventArgs.cs @@ -0,0 +1,47 @@ +using System; + +namespace KitX.Core.Contract.Event; + +/// +/// Event arguments for workflow rename events +/// +public class WorkflowRenamedEventArgs : EventArgs +{ + /// + /// The workflow ID that was renamed + /// + public string WorkflowId { get; } + + /// + /// The new name of the workflow + /// + public string NewName { get; } + + public WorkflowRenamedEventArgs(string workflowId, string newName) + { + WorkflowId = workflowId; + NewName = newName; + } +} + +/// +/// Event arguments for workflow data saved events +/// +public class WorkflowSavedEventArgs : EventArgs +{ + /// + /// The workflow ID that was saved + /// + public string WorkflowId { get; } + + /// + /// The workflow name at time of save + /// + public string WorkflowName { get; } + + public WorkflowSavedEventArgs(string workflowId, string workflowName) + { + WorkflowId = workflowId; + WorkflowName = workflowName; + } +} diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/BlockScriptModels.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/BlockScriptModels.cs index fe47e70..8f8ee14 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/BlockScriptModels.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/BlockScriptModels.cs @@ -66,7 +66,7 @@ public class BlockDefinition /// /// Name of the next block to execute when this block ends naturally - /// (i.e., not ended by Branch/Loop/LoopBodyEnd) + /// (i.e., not ended by Branch/Loop/ToLoopCond) /// public string? NextBlockName { get; set; } @@ -166,9 +166,9 @@ public enum FlowControlType Break, /// - /// Loop body end - marks the end of a loop body and returns to loop condition + /// To loop condition - marks the end of a loop body and returns to loop condition /// - LoopBodyEnd + ToLoopCond } /// @@ -198,9 +198,9 @@ public class FlowControlStatement : BlockStatement public string FalseBlockName { get; set; } = string.Empty; /// - /// For LoopBodyEnd: the block name containing the Loop statement to return to + /// For ToLoopCond: the block name containing the Loop statement to return to /// - public string? LoopBodyEndReturnTo { get; set; } + public string? ToLoopCondReturnTo { get; set; } /// /// Regenerates SourceCode from current field values. @@ -212,9 +212,9 @@ public void RegenerateSourceCode() { FlowControlType.Branch => $"NextBlock = Branch({ConditionExpression}, \"{TrueBlockName}\", \"{FalseBlockName}\");", FlowControlType.Loop => $"NextBlock = Loop({ConditionExpression}, \"{TrueBlockName}\", \"{FalseBlockName}\");", - FlowControlType.LoopBodyEnd => LoopBodyEndReturnTo != null - ? $"NextBlock = LoopBodyEnd(\"{LoopBodyEndReturnTo}\");" - : "LoopBodyEnd();", + FlowControlType.ToLoopCond => ToLoopCondReturnTo != null + ? $"NextBlock = ToLoopCond(\"{ToLoopCondReturnTo}\");" + : "ToLoopCond();", FlowControlType.Break => "Break();", _ => SourceCode }; diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/BlueprintModels.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/BlueprintModels.cs index 695cdbf..8325f34 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/BlueprintModels.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/BlueprintModels.cs @@ -757,7 +757,7 @@ public class BlueprintBlockScope /// /// Name of the next block to execute when this block ends naturally - /// (i.e., not ended by Branch/Loop/LoopBodyEnd). Null if the block ends + /// (i.e., not ended by Branch/Loop/ToLoopCond). Null if the block ends /// with a control-flow statement or is terminal. /// public string? NextBlockName { get; set; } From 1ce4ddf7a8a8fcbf5761ef81772cd21804fffb49 Mon Sep 17 00:00:00 2001 From: StarInk Date: Sun, 12 Apr 2026 03:27:24 +0200 Subject: [PATCH 41/60] =?UTF-8?q?=F0=9F=92=BE=20Feat(Workflow):=20?= =?UTF-8?q?=E6=89=A9=E5=B1=95=20WorkflowSavedEventArgs=20=E6=94=AF?= =?UTF-8?q?=E6=8C=81=20Description=20=E4=B8=8E=20Author=20=E5=AD=97?= =?UTF-8?q?=E6=AE=B5=E5=90=8C=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../KitX.Core.Contract/Event/WorkflowEventArgs.cs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/KitX Core Contracts/KitX.Core.Contract/Event/WorkflowEventArgs.cs b/KitX Core Contracts/KitX.Core.Contract/Event/WorkflowEventArgs.cs index ed9035b..1af3d1a 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Event/WorkflowEventArgs.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Event/WorkflowEventArgs.cs @@ -39,9 +39,22 @@ public class WorkflowSavedEventArgs : EventArgs /// public string WorkflowName { get; } - public WorkflowSavedEventArgs(string workflowId, string workflowName) + /// + /// The workflow description at time of save + /// + public string Description { get; } + + /// + /// The workflow author at time of save + /// + public string Author { get; } + + public WorkflowSavedEventArgs(string workflowId, string workflowName, + string description = "", string author = "") { WorkflowId = workflowId; WorkflowName = workflowName; + Description = description; + Author = author; } } From aeb466d2b996d865204cd5d0d6d1f6cbde789df5 Mon Sep 17 00:00:00 2001 From: StarInk Date: Mon, 13 Apr 2026 03:27:31 +0200 Subject: [PATCH 42/60] =?UTF-8?q?=F0=9F=92=BE=20Feat(Trigger):=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E6=8F=92=E4=BB=B6=E8=A7=A6=E5=8F=91=E5=99=A8=E6=94=AF?= =?UTF-8?q?=E6=8C=81=EF=BC=8C=E6=96=B0=E5=A2=9E=20TriggerConfig=20?= =?UTF-8?q?=E7=B1=BB=E5=8F=8A=E7=9B=B8=E5=85=B3=E4=BA=8B=E4=BB=B6=E5=8F=82?= =?UTF-8?q?=E6=95=B0=EF=BC=8C=E6=89=A9=E5=B1=95=E5=B7=A5=E4=BD=9C=E6=B5=81?= =?UTF-8?q?=E6=89=A7=E8=A1=8C=E7=BB=93=E6=9E=9C=E5=92=8C=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../KitX.Contract.CSharp/TriggerHelper.cs | 44 +++++++++++++++++++ .../Event/WorkflowEventArgs.cs | 28 ++++++++++++ .../Workflow/BlueprintModels.cs | 39 ++++++++++++++++ .../Workflow/IWorkflowService.cs | 17 ++++++- .../Workflow/KcsFileFormat.cs | 7 ++- .../Workflow/TriggerConfig.cs | 25 +++++++++++ .../KitX.Shared.CSharp/Plugin/PluginInfo.cs | 5 +++ .../WebCommand/Infos/CommandRequestInfo.cs | 2 + .../WebCommand/RequestBuilder.cs | 17 +++++++ 9 files changed, 182 insertions(+), 2 deletions(-) create mode 100644 KitX Contracts/KitX.Contract.CSharp/TriggerHelper.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/TriggerConfig.cs diff --git a/KitX Contracts/KitX.Contract.CSharp/TriggerHelper.cs b/KitX Contracts/KitX.Contract.CSharp/TriggerHelper.cs new file mode 100644 index 0000000..7854215 --- /dev/null +++ b/KitX Contracts/KitX.Contract.CSharp/TriggerHelper.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using KitX.Shared.CSharp.WebCommand; +using KitX.Shared.CSharp.WebCommand.Infos; + +namespace KitX.Contract.CSharp; + +/// +/// 插件触发器辅助工具。插件通过此类发送触发信号到 Dashboard。 +/// 触发器是纯信号,不携带业务数据。工作流被触发后通过调用插件函数获取数据。 +/// +public static class TriggerHelper +{ + private static readonly JsonSerializerOptions _options = new() + { + WriteIndented = false, + IncludeFields = true, + PropertyNameCaseInsensitive = true, + }; + + /// + /// 触发一个信号事件 + /// + /// SetSendCommandAction 提供的发送回调 + /// 触发器名称 + public static void FireTrigger(Action? sendAction, string triggerName) + { + if (sendAction is null) return; + + var request = new Request + { + Type = RequestTypes.Command, + Version = RequestVersions.V1, + Content = JsonSerializer.Serialize(new Command + { + Request = CommandRequestInfo.TriggerFired, + Tags = new Dictionary { { "TriggerName", triggerName } } + }, _options) + }; + + sendAction.Invoke(request); + } +} diff --git a/KitX Core Contracts/KitX.Core.Contract/Event/WorkflowEventArgs.cs b/KitX Core Contracts/KitX.Core.Contract/Event/WorkflowEventArgs.cs index 1af3d1a..abb8655 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Event/WorkflowEventArgs.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Event/WorkflowEventArgs.cs @@ -58,3 +58,31 @@ public WorkflowSavedEventArgs(string workflowId, string workflowName, Author = author; } } + +/// +/// Event arguments for workflow execution result events +/// +public class WorkflowExecutionResultEventArgs : EventArgs +{ + /// + /// The workflow ID that was executed + /// + public string WorkflowId { get; } + + /// + /// Whether the execution succeeded + /// + public bool IsSuccess { get; } + + /// + /// Error message if execution failed + /// + public string? ErrorMessage { get; } + + public WorkflowExecutionResultEventArgs(string workflowId, bool isSuccess, string? errorMessage = null) + { + WorkflowId = workflowId; + IsSuccess = isSuccess; + ErrorMessage = errorMessage; + } +} diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/BlueprintModels.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/BlueprintModels.cs index 8325f34..f4e5a62 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/BlueprintModels.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/BlueprintModels.cs @@ -34,6 +34,12 @@ public enum BlueprintNodeType /// Entry, + /// + /// Plugin event trigger node - alternative entry point activated by plugin triggers. + /// Has same pin structure as Entry (0 input, 1 Exec output) but carries PluginName/TriggerName metadata. + /// + PluginTrigger, + /// /// Conditional branch node /// @@ -214,6 +220,7 @@ public class BlueprintPin [JsonDerivedType(typeof(PauseNode), "Pause")] [JsonDerivedType(typeof(VariableNode), "Variable")] [JsonDerivedType(typeof(BuiltinFunctionNode), "BuiltinFunction")] +[JsonDerivedType(typeof(PluginTriggerNode), "PluginTrigger")] public abstract class BlueprintNode { /// @@ -347,6 +354,38 @@ public EntryNode() ); } +/// +/// Plugin trigger entry node - activated when a specific plugin fires a specific trigger signal. +/// Has same pin structure as Entry (0 input, 1 Exec output) but carries PluginName/TriggerName metadata. +/// +public class PluginTriggerNode : BlueprintNode +{ + /// The plugin name to listen for + public string PluginName { get; set; } = string.Empty; + + /// The trigger name to listen for + public string TriggerName { get; set; } = string.Empty; + + public PluginTriggerNode() + { + NodeType = BlueprintNodeType.PluginTrigger; + Name = "PluginTrigger"; + InitializePinsFromDescriptor(); + } + + public override NodeDescriptor GetDescriptor() => new( + Width: 160, Height: 60, + InputPins: [], + OutputPins: [new PinDescriptor("Exec", PinType.Execution, 30)], + DisplayName: "PluginTrigger" + ); + + public override string GetDisplayTitle() + => string.IsNullOrEmpty(PluginName) + ? $"Trigger: {TriggerName}" + : $"Trigger: {PluginName}.{TriggerName}"; +} + /// /// Branch node - conditional execution /// diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowService.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowService.cs index 6e028f1..850b352 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowService.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowService.cs @@ -197,6 +197,16 @@ public interface IWorkflowCase /// bool IsRunning { get; set; } + /// + /// Gets or sets a value indicating whether the workflow is in an error state + /// + bool IsError { get; set; } + + /// + /// Gets or sets the error message if the workflow is in an error state + /// + string? ErrorMessage { get; set; } + /// /// Gets or sets the script file path /// @@ -213,7 +223,12 @@ public interface IWorkflowCase DateTime LastModifiedTime { get; set; } /// - /// Gets or sets the trigger type (e.g., "Manual", "Scheduled", "Event") + /// Gets or sets the trigger type (e.g., "Manual", "PluginEvent") /// string TriggerType { get; set; } + + /// + /// Gets or sets the trigger configuration + /// + TriggerConfig? TriggerConfig { get; set; } } diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/KcsFileFormat.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/KcsFileFormat.cs index c6a4642..a383fc7 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/KcsFileFormat.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/KcsFileFormat.cs @@ -40,10 +40,15 @@ public class KcsFileFormat public DateTime LastModifiedTime { get; set; } = DateTime.UtcNow; /// - /// 触发类型("Manual", "Scheduled", "Event" 等) + /// 触发类型("Manual", "PluginEvent" 等) /// public string TriggerType { get; set; } = "Manual"; + /// + /// 触发器配置 + /// + public TriggerConfig? TriggerConfig { get; set; } + /// /// 主程序代码 /// diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/TriggerConfig.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/TriggerConfig.cs new file mode 100644 index 0000000..8321910 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/TriggerConfig.cs @@ -0,0 +1,25 @@ +namespace KitX.Core.Contract.Workflow; + +/// +/// 工作流触发器配置 +/// +public class TriggerConfig +{ + /// + /// 触发器类型: + /// - "Manual": 手动触发(默认) + /// - "PluginEvent": 插件事件触发 + /// Cron/FileWatcher/Webhook 等场景由专门插件通过 PluginEvent 实现 + /// + public string TriggerType { get; set; } = "Manual"; + + /// + /// PluginEvent 类型:监听的插件名称 + /// + public string? PluginName { get; set; } + + /// + /// PluginEvent 类型:监听的触发器名称(null = 该插件的所有触发器) + /// + public string? TriggerName { get; set; } +} diff --git a/KitX Shared/KitX.Shared.CSharp/Plugin/PluginInfo.cs b/KitX Shared/KitX.Shared.CSharp/Plugin/PluginInfo.cs index cd75f08..b66f96e 100644 --- a/KitX Shared/KitX.Shared.CSharp/Plugin/PluginInfo.cs +++ b/KitX Shared/KitX.Shared.CSharp/Plugin/PluginInfo.cs @@ -37,5 +37,10 @@ public class PluginInfo public List Functions { get; set; } = []; + /// + /// 此插件可触发的触发器名称列表 + /// + public List SupportedTriggers { get; set; } = []; + public string RootStartupFileName { get; set; } = string.Empty; } diff --git a/KitX Shared/KitX.Shared.CSharp/WebCommand/Infos/CommandRequestInfo.cs b/KitX Shared/KitX.Shared.CSharp/WebCommand/Infos/CommandRequestInfo.cs index bb03a2e..9c69e45 100644 --- a/KitX Shared/KitX.Shared.CSharp/WebCommand/Infos/CommandRequestInfo.cs +++ b/KitX Shared/KitX.Shared.CSharp/WebCommand/Infos/CommandRequestInfo.cs @@ -13,4 +13,6 @@ public static class CommandRequestInfo public const string RequestCommand = "RequestCommand"; public const string ReceiveCommand = "ReceiveCommand"; + + public const string TriggerFired = "TriggerFired"; } diff --git a/KitX Shared/KitX.Shared.CSharp/WebCommand/RequestBuilder.cs b/KitX Shared/KitX.Shared.CSharp/WebCommand/RequestBuilder.cs index fa3128f..8446b21 100644 --- a/KitX Shared/KitX.Shared.CSharp/WebCommand/RequestBuilder.cs +++ b/KitX Shared/KitX.Shared.CSharp/WebCommand/RequestBuilder.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using KitX.Shared.CSharp.WebCommand.Infos; namespace KitX.Shared.CSharp.WebCommand; @@ -155,4 +156,20 @@ public static RequestBuilder ReceiveCommand(this RequestBuilder builder) return builder; } + + public static RequestBuilder TriggerFired(this RequestBuilder builder, string triggerName) + { + builder = builder.UpdateCommand(cmd => + { + cmd.Request = CommandRequestInfo.TriggerFired; + + cmd.Tags ??= new Dictionary(); + + cmd.Tags["TriggerName"] = triggerName; + + return cmd; + }); + + return builder; + } } From 8f2ee17923cc1d54025520ab268e351e181289e2 Mon Sep 17 00:00:00 2001 From: StarInk Date: Tue, 14 Apr 2026 17:56:09 +0200 Subject: [PATCH 43/60] =?UTF-8?q?=F0=9F=92=BE=20Feat(Plugin):=20=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=20IPluginManager=20=E6=8E=A5=E5=8F=A3=E5=8F=8A=20Plug?= =?UTF-8?q?inCallInfo=20=E7=B1=BB=EF=BC=8C=E6=94=AF=E6=8C=81=E6=8F=92?= =?UTF-8?q?=E4=BB=B6=E8=B0=83=E7=94=A8=E5=92=8C=E5=8F=82=E6=95=B0=E4=BC=A0?= =?UTF-8?q?=E9=80=92=EF=BC=9B=E6=89=A9=E5=B1=95=20IWorkflowService=20?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E4=BB=A5=E7=BC=96=E8=AF=91=E5=92=8C=E6=8C=81?= =?UTF-8?q?=E4=B9=85=E5=8C=96=E5=B7=A5=E4=BD=9C=E6=B5=81=E8=84=9A=E6=9C=AC?= =?UTF-8?q?=EF=BC=9B=E6=89=A9=E5=B1=95=20IWorkflowStorageService=20?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E4=BB=A5=E9=A2=84=E5=8A=A0=E8=BD=BD=E5=B7=B2?= =?UTF-8?q?=E7=BC=96=E8=AF=91=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Workflow/IPluginManager.cs | 39 ++++++++++++++ .../Workflow/IWorkflowService.cs | 8 +++ .../Workflow/IWorkflowStorageService.cs | 7 +++ .../Workflow/PluginCallInfo.cs | 54 +++++++++++++++++++ 4 files changed, 108 insertions(+) create mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/IPluginManager.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/PluginCallInfo.cs diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/IPluginManager.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/IPluginManager.cs new file mode 100644 index 0000000..9b4c62c --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/IPluginManager.cs @@ -0,0 +1,39 @@ +namespace KitX.Core.Contract.Workflow; + +/// +/// 插件管理器接口 - BlockScripting 使用此接口调用插件方法。 +/// Copy of Kscript.CSharp.Parser.Core.IPluginManager for BlockScripting use +/// without depending on the KCS parser assembly. +/// 实际实现由外部项目(RealPluginManager)提供。 +/// +public interface IPluginManager +{ + /// + /// 调用插件方法 + /// + /// 返回值类型 + /// 调用信息 + /// 插件方法的返回值 + T Call(PluginCallInfo callInfo); + + /// + /// 调用插件方法(无返回值) + /// + /// 调用信息 + void Call(PluginCallInfo callInfo); + + /// + /// 检查插件是否存在 + /// + /// 插件名称 + /// 插件是否存在 + bool IsPluginExists(string pluginName); + + /// + /// 检查插件方法是否存在 + /// + /// 插件名称 + /// 方法名称 + /// 方法是否存在 + bool IsMethodExists(string pluginName, string methodName); +} \ No newline at end of file diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowService.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowService.cs index 850b352..a6c18e3 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowService.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowService.cs @@ -165,6 +165,14 @@ Task ExecuteBlockScriptAsync( public interface IWorkflowService : IWorkflowManagementService, IScriptExecutionService, IWorkflowPluginService, IBlockScriptService { + /// + /// Compiles a workflow's BlockScript into a persisted assembly on disk. + /// The compiled assembly is saved under Data/CompiledScripts/{workflowId}/ + /// and will be reused on subsequent runs instead of recompiling. + /// + /// The workflow ID to compile and persist. + /// True if compilation and persistence succeeded. + Task CompileAndPersistWorkflowAsync(string workflowId); } /// diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowStorageService.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowStorageService.cs index 31bd4d2..07e5ddb 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowStorageService.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowStorageService.cs @@ -60,4 +60,11 @@ public interface IWorkflowStorageService /// Workflow ID /// Full file path string GetWorkflowFilePath(string workflowId); + + /// + /// Preloads all persisted compiled scripts for discovered workflows from disk + /// into the in-memory cache. Called at startup to enable fast first-run execution. + /// + /// Number of scripts successfully loaded. + Task PreloadCompiledScriptsAsync(); } diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/PluginCallInfo.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/PluginCallInfo.cs new file mode 100644 index 0000000..90c3d96 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/PluginCallInfo.cs @@ -0,0 +1,54 @@ +using System; + +namespace KitX.Core.Contract.Workflow; + +/// +/// 插件调用信息,用于传递给 IPluginManager.Call 的参数。 +/// Copy of Kscript.CSharp.Parser.Models.PluginCallInfo for BlockScripting use +/// without depending on the KCS parser assembly. +/// +public class PluginCallInfo +{ + /// + /// 插件名称 + /// + public string PluginName { get; set; } = string.Empty; + + /// + /// 方法名称 + /// + public string MethodName { get; set; } = string.Empty; + + /// + /// 方法参数值数组 + /// + public object[] Parameters { get; set; } = Array.Empty(); + + /// + /// 参数类型数组 + /// + public Type[] ParameterTypes { get; set; } = Array.Empty(); + + /// + /// 参数名称数组 + /// + public string[] ParameterNames { get; set; } = Array.Empty(); + + public PluginCallInfo() + { + } + + public PluginCallInfo(string pluginName, string methodName, object[] parameters, Type[] parameterTypes, string[] parameterNames) + { + PluginName = pluginName; + MethodName = methodName; + Parameters = parameters; + ParameterTypes = parameterTypes; + ParameterNames = parameterNames; + } + + public override string ToString() + { + return $"{PluginName}.{MethodName}({string.Join(", ", Parameters)})"; + } +} \ No newline at end of file From 7d38df8ebe06054755fe678e4a5027520791abee Mon Sep 17 00:00:00 2001 From: StarInk Date: Sat, 18 Apr 2026 20:35:09 +0200 Subject: [PATCH 44/60] =?UTF-8?q?=F0=9F=92=BE=20Feat(CallNode):=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20TargetDevice=20=E5=B1=9E=E6=80=A7=E4=BB=A5?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E8=B7=A8=E8=AE=BE=E5=A4=87=E8=B0=83=E7=94=A8?= =?UTF-8?q?=EF=BC=9B=E6=9B=B4=E6=96=B0=20GetDisplayTitle=20=E6=96=B9?= =?UTF-8?q?=E6=B3=95=E4=BB=A5=E6=98=BE=E7=A4=BA=E7=9B=AE=E6=A0=87=E8=AE=BE?= =?UTF-8?q?=E5=A4=87=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../KitX.Core.Contract/Workflow/BlueprintModels.cs | 11 ++++++++++- .../KitX.Core.Contract/Workflow/PluginCallInfo.cs | 5 +++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/BlueprintModels.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/BlueprintModels.cs index f4e5a62..bcefed0 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/BlueprintModels.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/BlueprintModels.cs @@ -510,6 +510,12 @@ public class CallNode : BlueprintNode /// public string FunctionName { get; set; } = string.Empty; + /// + /// Target device name for cross-device calls. + /// If null or empty, call is routed locally via PluginCall. + /// + public string? TargetDevice { get; set; } + public CallNode() { NodeType = BlueprintNodeType.Call; @@ -528,7 +534,10 @@ public CallNode() ); public override string GetDisplayTitle() - => string.IsNullOrEmpty(PluginName) ? $"Call: {FunctionName}" : $"Call: {PluginName}.{FunctionName}"; + { + var baseTitle = string.IsNullOrEmpty(PluginName) ? $"Call: {FunctionName}" : $"Call: {PluginName}.{FunctionName}"; + return string.IsNullOrEmpty(TargetDevice) ? baseTitle : $"{baseTitle} @ {TargetDevice}"; + } } /// diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/PluginCallInfo.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/PluginCallInfo.cs index 90c3d96..3f19863 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/PluginCallInfo.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/PluginCallInfo.cs @@ -34,6 +34,11 @@ public class PluginCallInfo /// public string[] ParameterNames { get; set; } = Array.Empty(); + /// + /// 目标设备名称(远程调用时使用)。如果为空或 null,则为本地调用。 + /// + public string? TargetDevice { get; set; } + public PluginCallInfo() { } From 7ff6a86725dbb808ed548dfc6306723d4881c487 Mon Sep 17 00:00:00 2001 From: StarInk Date: Sun, 19 Apr 2026 02:02:21 +0200 Subject: [PATCH 45/60] =?UTF-8?q?=F0=9F=A7=A9Refactor:=20=E6=9E=B6?= =?UTF-8?q?=E6=9E=84=E8=A7=84=E8=8C=83=E6=80=A7=E6=A2=B3=E7=90=86=E5=92=8C?= =?UTF-8?q?God=20Classes=E6=8B=86=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Configuration/IConfigLoader.cs | 21 + .../Configuration/IConfigSaver.cs | 16 + .../Configuration/IPagesConfig.cs | 14 +- .../Device/IDeviceService.cs | 5 + .../Plugin/Events/PluginEventArgs.cs | 84 ++ .../Plugin/IPluginConnector.cs | 37 + .../Plugin/IPluginServer.cs | 55 ++ .../Plugin/IPluginService.cs | 200 +---- .../KitX.Core.Contract/Plugin/PluginStatus.cs | 32 + .../Security/IDeviceKeyService.cs | 60 ++ ...curityService.cs => IEncryptionService.cs} | 73 +- .../Workflow/BlockDefinition.cs | 45 ++ .../Workflow/BlockScript.cs | 87 +++ .../Workflow/BlockScriptModels.cs | 417 +--------- .../Workflow/BlueprintModels.cs | 730 +----------------- .../Workflow/FlowControlType.cs | 32 + .../Workflow/INodeExportStrategy.cs | 74 ++ .../Workflow/IWorkflowService.cs | 30 +- .../Workflow/Nodes/BlueprintNode.cs | 136 ++++ .../Workflow/Nodes/BlueprintNodeType.cs | 80 ++ .../Workflow/Nodes/BlueprintPin.cs | 34 + .../Workflow/Nodes/BranchNode.cs | 27 + .../Workflow/Nodes/BreakNode.cs | 21 + .../Workflow/Nodes/BuiltinFunctionNode.cs | 43 ++ .../Workflow/Nodes/CallHelperNode.cs | 31 + .../Workflow/Nodes/CallNode.cs | 46 ++ .../Workflow/Nodes/ConstNode.cs | 38 + .../Workflow/Nodes/EntryNode.cs | 21 + .../Workflow/Nodes/GetNode.cs | 31 + .../Workflow/Nodes/LoopNode.cs | 27 + .../Workflow/Nodes/NodeDescriptor.cs | 15 + .../Workflow/Nodes/PauseNode.cs | 24 + .../Workflow/Nodes/PinDescriptor.cs | 11 + .../Workflow/Nodes/PinDirection.cs | 10 + .../Workflow/Nodes/PinType.cs | 37 + .../Workflow/Nodes/PluginTriggerNode.cs | 33 + .../Workflow/Nodes/PrintNode.cs | 24 + .../Workflow/Nodes/SetNode.cs | 31 + .../Workflow/Nodes/VariableNode.cs | 35 + .../Workflow/Results/BlockExecutionResult.cs | 48 ++ .../Results/BlockScriptExecutionResult.cs | 39 + .../Results/BlockScriptParseResult.cs | 27 + .../Results/BlockScriptValidationResult.cs | 34 + .../Workflow/Statements/BlockStatement.cs | 17 + .../Statements/ExpressionStatement.cs | 12 + .../Statements/FlowControlStatement.cs | 51 ++ .../VariableDeclarationStatement.cs | 12 + .../Workflow/VariableDeclaration.cs | 27 + 48 files changed, 1603 insertions(+), 1431 deletions(-) create mode 100644 KitX Core Contracts/KitX.Core.Contract/Configuration/IConfigLoader.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Configuration/IConfigSaver.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Plugin/Events/PluginEventArgs.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Plugin/IPluginConnector.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Plugin/IPluginServer.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Plugin/PluginStatus.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Security/IDeviceKeyService.cs rename KitX Core Contracts/KitX.Core.Contract/Security/{ISecurityService.cs => IEncryptionService.cs} (53%) create mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/BlockDefinition.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/BlockScript.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/FlowControlType.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/INodeExportStrategy.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/BlueprintNode.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/BlueprintNodeType.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/BlueprintPin.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/BranchNode.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/BreakNode.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/BuiltinFunctionNode.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/CallHelperNode.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/CallNode.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/ConstNode.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/EntryNode.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/GetNode.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/LoopNode.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/NodeDescriptor.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/PauseNode.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/PinDescriptor.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/PinDirection.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/PinType.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/PluginTriggerNode.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/PrintNode.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/SetNode.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/VariableNode.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/Results/BlockExecutionResult.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/Results/BlockScriptExecutionResult.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/Results/BlockScriptParseResult.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/Results/BlockScriptValidationResult.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/Statements/BlockStatement.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/Statements/ExpressionStatement.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/Statements/FlowControlStatement.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/Statements/VariableDeclarationStatement.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/VariableDeclaration.cs diff --git a/KitX Core Contracts/KitX.Core.Contract/Configuration/IConfigLoader.cs b/KitX Core Contracts/KitX.Core.Contract/Configuration/IConfigLoader.cs new file mode 100644 index 0000000..8f6d570 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Configuration/IConfigLoader.cs @@ -0,0 +1,21 @@ +namespace KitX.Core.Contract.Configuration; + +/// +/// Loads configuration from files +/// +public interface IConfigLoader +{ + /// + /// Loads a config file from the specified location + /// + /// Config type + /// Directory path + /// File name + /// The loaded config or default + T Load(string location, string fileName) where T : class, new(); + + /// + /// Loads SecurityConfig with special handling + /// + ISecurityConfig LoadSecurityConfig(string location); +} diff --git a/KitX Core Contracts/KitX.Core.Contract/Configuration/IConfigSaver.cs b/KitX Core Contracts/KitX.Core.Contract/Configuration/IConfigSaver.cs new file mode 100644 index 0000000..e6b8df7 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Configuration/IConfigSaver.cs @@ -0,0 +1,16 @@ +namespace KitX.Core.Contract.Configuration; + +/// +/// Saves configuration to files +/// +public interface IConfigSaver +{ + /// + /// Saves a config to the specified location + /// + /// Config type + /// Config to save + /// Directory path + /// File name + void Save(T config, string location, string fileName) where T : class; +} diff --git a/KitX Core Contracts/KitX.Core.Contract/Configuration/IPagesConfig.cs b/KitX Core Contracts/KitX.Core.Contract/Configuration/IPagesConfig.cs index c73e68e..39dff37 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Configuration/IPagesConfig.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Configuration/IPagesConfig.cs @@ -6,8 +6,8 @@ namespace KitX.Core.Contract.Configuration; public interface IPagesConf { IHomePageConf Home { get; set; } - IDevicePageConf Device { get; set; } - IMarketPageConf Market { get; set; } + object? Device { get; set; } + object? Market { get; set; } ISettingsPageConf Settings { get; set; } } @@ -22,16 +22,6 @@ public interface IHomePageConf bool UseAreaExpanded { get; set; } } -/// -/// Device page configuration -/// -public interface IDevicePageConf { } - -/// -/// Market page configuration -/// -public interface IMarketPageConf { } - /// /// Settings page configuration /// diff --git a/KitX Core Contracts/KitX.Core.Contract/Device/IDeviceService.cs b/KitX Core Contracts/KitX.Core.Contract/Device/IDeviceService.cs index 6e3f930..3a141b6 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Device/IDeviceService.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Device/IDeviceService.cs @@ -143,6 +143,11 @@ public interface IDevicesOrganizer /// Event raised when a device is discovered /// event EventHandler? DeviceDiscovered; + + /// + /// Event raised when a device goes offline + /// + event EventHandler? DeviceOffline; } /// diff --git a/KitX Core Contracts/KitX.Core.Contract/Plugin/Events/PluginEventArgs.cs b/KitX Core Contracts/KitX.Core.Contract/Plugin/Events/PluginEventArgs.cs new file mode 100644 index 0000000..8249577 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Plugin/Events/PluginEventArgs.cs @@ -0,0 +1,84 @@ +using System; +using KitX.Shared.CSharp.Plugin; + +namespace KitX.Core.Contract.Plugin.Events; + +/// +/// Plugin status changed event arguments +/// +public class PluginStatusChangedEventArgs : EventArgs +{ + /// + /// Gets or sets the plugin ID + /// + public Guid PluginId { get; set; } + + /// + /// Gets or sets the plugin name + /// + public string PluginName { get; set; } = string.Empty; + + /// + /// Gets or sets the old status + /// + public PluginStatus OldStatus { get; set; } + + /// + /// Gets or sets the new status + /// + public PluginStatus NewStatus { get; set; } +} + +/// +/// Plugin response event arguments +/// +public class PluginResponseEventArgs : EventArgs +{ + /// + /// Gets or sets the request ID + /// + public string RequestId { get; set; } = string.Empty; + + /// + /// Gets or sets the response content + /// + public string Content { get; set; } = string.Empty; +} + +/// +/// Plugin status report event arguments +/// +public class PluginStatusReportEventArgs : EventArgs +{ + /// + /// Gets or sets the connection ID + /// + public string ConnectionId { get; set; } = string.Empty; + + /// + /// Gets or sets the status message + /// + public string Status { get; set; } = string.Empty; +} + +/// +/// Plugin registered event arguments +/// +public class PluginRegisteredEventArgs : EventArgs +{ + /// + /// Gets or sets the plugin info + /// + public PluginInfo? PluginInfo { get; set; } +} + +/// +/// Plugin unregistered event arguments +/// +public class PluginUnregisteredEventArgs : EventArgs +{ + /// + /// Gets or sets the plugin info + /// + public PluginInfo? PluginInfo { get; set; } +} \ No newline at end of file diff --git a/KitX Core Contracts/KitX.Core.Contract/Plugin/IPluginConnector.cs b/KitX Core Contracts/KitX.Core.Contract/Plugin/IPluginConnector.cs new file mode 100644 index 0000000..0a326eb --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Plugin/IPluginConnector.cs @@ -0,0 +1,37 @@ +using System; +using KitX.Shared.CSharp.Plugin; +using KitX.Core.Contract.Plugin.Events; + +namespace KitX.Core.Contract.Plugin; + +/// +/// Plugin connector interface for managing individual plugin connections +/// +public interface IPluginConnector +{ + /// + /// Gets the connection ID + /// + string ConnectionId { get; } + + /// + /// Gets the plugin info + /// + PluginInfo? PluginInfo { get; } + + /// + /// Sends a request to the plugin + /// + /// The request to send + void Request(object request); + + /// + /// Event raised when a plugin response is received + /// + event EventHandler? PluginResponse; + + /// + /// Event raised when plugin reports status + /// + event EventHandler? StatusReport; +} \ No newline at end of file diff --git a/KitX Core Contracts/KitX.Core.Contract/Plugin/IPluginServer.cs b/KitX Core Contracts/KitX.Core.Contract/Plugin/IPluginServer.cs new file mode 100644 index 0000000..3f9d20f --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Plugin/IPluginServer.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using KitX.Shared.CSharp.Plugin; +using KitX.Core.Contract.Plugin.Events; + +namespace KitX.Core.Contract.Plugin; + +/// +/// Plugin server interface for managing plugin connections +/// +public interface IPluginServer +{ + /// + /// Gets the port the server is running on + /// + int? Port { get; } + + /// + /// Gets the list of currently connected plugins + /// + IReadOnlyList Connections { get; } + + /// + /// Starts the plugin server + /// + /// The server instance + IPluginServer Run(); + + /// + /// Stops the plugin server + /// + void Stop(); + + /// + /// Finds a connector for a specific plugin + /// + /// The plugin info + /// The plugin connector or null if not found + IPluginConnector? FindConnector(PluginInfo pluginInfo); + + /// + /// Event raised when server port changes + /// + event EventHandler? PortChanged; + + /// + /// Event raised when a plugin registers with the server + /// + event EventHandler? PluginRegistered; + + /// + /// Event raised when a plugin unregisters/disconnects from the server + /// + event EventHandler? PluginUnregistered; +} \ No newline at end of file diff --git a/KitX Core Contracts/KitX.Core.Contract/Plugin/IPluginService.cs b/KitX Core Contracts/KitX.Core.Contract/Plugin/IPluginService.cs index d09a441..495e5a2 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Plugin/IPluginService.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Plugin/IPluginService.cs @@ -1,9 +1,9 @@ using System; -using KitX.Core.Contract.Configuration; -using KitX.Shared.CSharp.Plugin; using System.Collections.Generic; -using System.ComponentModel; using System.Threading.Tasks; +using KitX.Core.Contract.Configuration; +using KitX.Shared.CSharp.Plugin; +using KitX.Core.Contract.Plugin.Events; namespace KitX.Core.Contract.Plugin; @@ -65,196 +65,4 @@ public interface IPluginService /// Event raised when plugin status changes /// event EventHandler? PluginStatusChanged; -} - -/// -/// Plugin server interface for managing plugin connections -/// -public interface IPluginServer -{ - /// - /// Gets the port the server is running on - /// - int? Port { get; } - - /// - /// Gets the list of currently connected plugins - /// - IReadOnlyList Connections { get; } - - /// - /// Starts the plugin server - /// - /// The server instance - IPluginServer Run(); - - /// - /// Stops the plugin server - /// - void Stop(); - - /// - /// Finds a connector for a specific plugin - /// - /// The plugin info - /// The plugin connector or null if not found - IPluginConnector? FindConnector(PluginInfo pluginInfo); - - /// - /// Event raised when server port changes - /// - event EventHandler? PortChanged; - - /// - /// Event raised when a plugin registers with the server - /// - event EventHandler? PluginRegistered; - - /// - /// Event raised when a plugin unregisters/disconnects from the server - /// - event EventHandler? PluginUnregistered; -} - -/// -/// Plugin connector interface for managing individual plugin connections -/// -public interface IPluginConnector -{ - /// - /// Gets the connection ID - /// - string ConnectionId { get; } - - /// - /// Gets the plugin info - /// - PluginInfo? PluginInfo { get; } - - /// - /// Sends a request to the plugin - /// - /// The request to send - void Request(object request); - - /// - /// Event raised when a plugin response is received - /// - event EventHandler? PluginResponse; - - /// - /// Event raised when plugin reports status - /// - event EventHandler? StatusReport; -} - -/// -/// Plugin status enumeration -/// -public enum PluginStatus -{ - /// - /// Unknown status - /// - Unknown, - - /// - /// Plugin is installed but not running - /// - Installed, - - /// - /// Plugin is running - /// - Running, - - /// - /// Plugin was running but is now stopped - /// - Stopped, - - /// - /// Plugin encountered an error - /// - Error -} - -/// -/// Plugin status changed event arguments -/// -public class PluginStatusChangedEventArgs : EventArgs -{ - /// - /// Gets or sets the plugin ID - /// - public Guid PluginId { get; set; } - - /// - /// Gets or sets the plugin name - /// - public string PluginName { get; set; } = string.Empty; - - /// - /// Gets or sets the old status - /// - public PluginStatus OldStatus { get; set; } - - /// - /// Gets or sets the new status - /// - public PluginStatus NewStatus { get; set; } -} - -/// -/// Plugin response event arguments -/// -public class PluginResponseEventArgs : EventArgs -{ - /// - /// Gets or sets the request ID - /// - public string RequestId { get; set; } = string.Empty; - - /// - /// Gets or sets the response content - /// - public string Content { get; set; } = string.Empty; -} - -/// -/// Plugin status report event arguments -/// -public class PluginStatusReportEventArgs : EventArgs -{ - /// - /// Gets or sets the connection ID - /// - public string ConnectionId { get; set; } = string.Empty; - - /// - /// Gets or sets the status message - /// - public string Status { get; set; } = string.Empty; -} - -/// -/// Plugin registered event arguments -/// -public class PluginRegisteredEventArgs : EventArgs -{ - /// - /// Gets or sets the plugin info - /// - public PluginInfo? PluginInfo { get; set; } -} - -/// -/// Plugin unregistered event arguments -/// -public class PluginUnregisteredEventArgs : EventArgs -{ - /// - /// Gets or sets the plugin info - /// - public PluginInfo? PluginInfo { get; set; } -} +} \ No newline at end of file diff --git a/KitX Core Contracts/KitX.Core.Contract/Plugin/PluginStatus.cs b/KitX Core Contracts/KitX.Core.Contract/Plugin/PluginStatus.cs new file mode 100644 index 0000000..0586cfe --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Plugin/PluginStatus.cs @@ -0,0 +1,32 @@ +namespace KitX.Core.Contract.Plugin; + +/// +/// Plugin status enumeration +/// +public enum PluginStatus +{ + /// + /// Unknown status + /// + Unknown, + + /// + /// Plugin is installed but not running + /// + Installed, + + /// + /// Plugin is running + /// + Running, + + /// + /// Plugin was running but is now stopped + /// + Stopped, + + /// + /// Plugin encountered an error + /// + Error +} \ No newline at end of file diff --git a/KitX Core Contracts/KitX.Core.Contract/Security/IDeviceKeyService.cs b/KitX Core Contracts/KitX.Core.Contract/Security/IDeviceKeyService.cs new file mode 100644 index 0000000..bf770dd --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Security/IDeviceKeyService.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using KitX.Shared.CSharp.Device; +using KitXIDeviceKey = KitX.Core.Contract.Configuration.IDeviceKey; + +namespace KitX.Core.Contract.Security; + +/// +/// Device key management service interface +/// +public interface IDeviceKeyService +{ + /// + /// Gets all device keys + /// + IReadOnlyList GetDeviceKeys(); + + /// + /// Adds a device key + /// + /// The MAC address + /// The device name + /// The public key + /// True if addition was successful + bool AddDeviceKey(string macAddress, string deviceName, string publicKey); + + /// + /// Removes a device key + /// + /// The MAC address + /// True if removal was successful + bool RemoveDeviceKey(string macAddress); + + /// + /// Searches for a device key by device locator + /// + /// The device locator + /// The device key if found, otherwise null + DeviceKey? SearchDeviceKey(DeviceLocator locator); + + /// + /// Checks if a device key is correct + /// + /// The device locator + /// The device key to verify + /// True if the key is correct + bool IsDeviceKeyCorrect(DeviceLocator locator, DeviceKey key); + + /// + /// Checks if a device is authorized + /// + /// The device locator + /// True if the device is authorized + bool IsDeviceAuthorized(DeviceLocator device); + + /// + /// Gets the private device key for local device + /// + /// The private device key, or null if not available + DeviceKey? GetPrivateDeviceKey(); +} diff --git a/KitX Core Contracts/KitX.Core.Contract/Security/ISecurityService.cs b/KitX Core Contracts/KitX.Core.Contract/Security/IEncryptionService.cs similarity index 53% rename from KitX Core Contracts/KitX.Core.Contract/Security/ISecurityService.cs rename to KitX Core Contracts/KitX.Core.Contract/Security/IEncryptionService.cs index ba4b890..4027014 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Security/ISecurityService.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Security/IEncryptionService.cs @@ -1,67 +1,13 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; using System.Threading.Tasks; -using KitX.Shared.CSharp.Device; using KitX.Shared.CSharp.Security; -using KitXIDeviceKey = KitX.Core.Contract.Configuration.IDeviceKey; namespace KitX.Core.Contract.Security; /// -/// Security management service interface +/// Encryption service interface /// -public interface ISecurityService +public interface IEncryptionService { - /// - /// Gets all device keys - /// - IReadOnlyList GetDeviceKeys(); - - /// - /// Adds a device key - /// - /// The MAC address - /// The device name - /// The public key - /// True if addition was successful - bool AddDeviceKey(string macAddress, string deviceName, string publicKey); - - /// - /// Removes a device key - /// - /// The MAC address - /// True if removal was successful - bool RemoveDeviceKey(string macAddress); - - /// - /// Searches for a device key by device locator - /// - /// The device locator - /// The device key if found, otherwise null - DeviceKey? SearchDeviceKey(DeviceLocator locator); - - /// - /// Checks if a device key is correct - /// - /// The device locator - /// The device key to verify - /// True if the key is correct - bool IsDeviceKeyCorrect(DeviceLocator locator, DeviceKey key); - - /// - /// Checks if a device is authorized - /// - /// The device locator - /// True if the device is authorized - bool IsDeviceAuthorized(DeviceLocator device); - - /// - /// Gets the private device key for local device - /// - /// The private device key, or null if not available - DeviceKey? GetPrivateDeviceKey(); - /// /// Encrypts a string /// @@ -84,7 +30,7 @@ public interface ISecurityService /// The device key containing the public key /// The data to encrypt /// The encrypted data as Base64 string - string? RsaEncryptString(DeviceKey key, string data); + string? RsaEncryptString(Shared.CSharp.Device.DeviceKey key, string data); /// /// Decrypts a string using RSA with a specific device's private key @@ -92,7 +38,7 @@ public interface ISecurityService /// The device key containing the private key /// The encrypted data as Base64 string /// The decrypted data - string? RsaDecryptString(DeviceKey key, string encryptedData); + string? RsaDecryptString(Shared.CSharp.Device.DeviceKey key, string encryptedData); /// /// Encrypts content using RSA+AES hybrid encryption @@ -100,7 +46,7 @@ public interface ISecurityService /// The device key /// The content to encrypt /// The encrypted content - EncryptedContent RsaEncryptContent(DeviceKey key, string content); + EncryptedContent RsaEncryptContent(Shared.CSharp.Device.DeviceKey key, string content); /// /// Decrypts content using RSA+AES hybrid decryption @@ -108,7 +54,7 @@ public interface ISecurityService /// The device key /// The encrypted content /// The decrypted content - string RsaDecryptContent(DeviceKey key, EncryptedContent content); + string RsaDecryptContent(Shared.CSharp.Device.DeviceKey key, EncryptedContent content); /// /// Encrypts a string with AES @@ -127,13 +73,6 @@ public interface ISecurityService /// The decrypted string string AesDecrypt(string source, string key, bool isSourceInBase64 = true); - /// - /// Computes a hash - /// - /// The content to hash - /// The hash - string ComputeHash(string content); - /// /// Computes SHA1 hash of a string /// diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/BlockDefinition.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/BlockDefinition.cs new file mode 100644 index 0000000..9dff4d6 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/BlockDefinition.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; + +namespace KitX.Core.Contract.Workflow; + +/// +/// Represents a single block definition in the script +/// +public class BlockDefinition +{ + /// + /// Block type + /// + public BlockType Type { get; set; } + + /// + /// Block name (for NamedBlock) + /// + public string Name { get; set; } = string.Empty; + + /// + /// Variable declarations in this block + /// + public List Variables { get; set; } = []; + + /// + /// Statements in this block (excluding Loop statements, which are separated) + /// + public List Statements { get; set; } = []; + + /// + /// Line number in source where this block starts + /// + public int LineNumber { get; set; } + + /// + /// Name of the next block to execute when this block ends naturally + /// (i.e., not ended by Branch/Loop/ToLoopCond) + /// + public string? NextBlockName { get; set; } + + /// + /// For LoopBlock: the block name containing this loop (i.e., the parent block) + /// + public string? ParentBlockName { get; set; } +} \ No newline at end of file diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/BlockScript.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/BlockScript.cs new file mode 100644 index 0000000..95a0182 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/BlockScript.cs @@ -0,0 +1,87 @@ +using System.Collections.Generic; + +namespace KitX.Core.Contract.Workflow; + +/// +/// Parsed block script container +/// +public class BlockScript +{ + /// + /// Global constants block (ConstBlock) + /// + public BlockDefinition? ConstBlock { get; set; } + + /// + /// Public variables block (PubVarBlock) - optional + /// + public BlockDefinition? PubVarBlock { get; set; } + + /// + /// Main entry block (MainBlock) + /// + public BlockDefinition? MainBlock { get; set; } + + /// + /// Named blocks dictionary by name + /// + public Dictionary NamedBlocks { get; set; } = []; + + /// + /// All blocks in order of appearance + /// + public List AllBlocks { get; set; } = []; + + /// + /// Loop blocks dictionary by parent block name + /// (e.g., "MainBlock" -> LoopBlock for MainBlock's Loop statement) + /// + public Dictionary LoopBlocks { get; set; } = []; + + /// + /// Raw source code (parsed input, may not include helper functions) + /// + public string SourceCode { get; set; } = string.Empty; + + /// + /// Full source code including merged helper functions (for execution) + /// + public string FullSourceCode { get; set; } = string.Empty; + + /// + /// Helper functions to be made available in script execution context + /// + public List HelperFunctions { get; set; } = []; + + + /// + /// Gets a block by name (checks LoopBlocks first by block name, then NamedBlocks, then standard blocks) + /// IMPORTANT: LoopBlocks are checked FIRST because LoopBlock names (like "LoopBody") should NOT be + /// shadowed by user-defined NamedBlocks with the same name. + /// + public BlockDefinition? GetBlockByName(string name) + { + // First check LoopBlocks by the block's own name (not parent block name) + // This is critical because LoopBlocks are created for "NextBlock = Loop(...)" statements + // and their names (like "LoopBody") should take precedence over user-defined blocks + foreach (var kvp in LoopBlocks) + { + if (kvp.Value.Name == name) + return kvp.Value; + } + + // Then check NamedBlocks (user-defined blocks) + if (NamedBlocks.TryGetValue(name, out var namedBlock)) + return namedBlock; + + // Then check the standard blocks by name match + if (MainBlock?.Name == name) + return MainBlock; + if (ConstBlock?.Name == name) + return ConstBlock; + if (PubVarBlock?.Name == name) + return PubVarBlock; + + return null; + } +} \ No newline at end of file diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/BlockScriptModels.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/BlockScriptModels.cs index 8f8ee14..860c8a2 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/BlockScriptModels.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/BlockScriptModels.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; - namespace KitX.Core.Contract.Workflow; /// @@ -32,416 +29,4 @@ public enum BlockType /// Loop block - auto-generated block containing a Loop statement /// LoopBlock -} - -/// -/// Represents a single block definition in the script -/// -public class BlockDefinition -{ - /// - /// Block type - /// - public BlockType Type { get; set; } - - /// - /// Block name (for NamedBlock) - /// - public string Name { get; set; } = string.Empty; - - /// - /// Variable declarations in this block - /// - public List Variables { get; set; } = []; - - /// - /// Statements in this block (excluding Loop statements, which are separated) - /// - public List Statements { get; set; } = []; - - /// - /// Line number in source where this block starts - /// - public int LineNumber { get; set; } - - /// - /// Name of the next block to execute when this block ends naturally - /// (i.e., not ended by Branch/Loop/ToLoopCond) - /// - public string? NextBlockName { get; set; } - - /// - /// For LoopBlock: the block name containing this loop (i.e., the parent block) - /// - public string? ParentBlockName { get; set; } -} - -/// -/// Variable declaration -/// -public class VariableDeclaration -{ - /// - /// Variable name - /// - public string Name { get; set; } = string.Empty; - - /// - /// Variable type as string - /// - public string Type { get; set; } = "object"; - - /// - /// Initial value expression as string (for evaluation at parse time or execution time) - /// - public string? InitialValueExpression { get; set; } - - /// - /// Default value (pre-evaluated for const block) - /// - public object? DefaultValue { get; set; } -} - -/// -/// Base class for statements within a block -/// -public abstract class BlockStatement -{ - /// - /// Line number in source - /// - public int LineNumber { get; set; } - - /// - /// Original source code for this statement - /// - public string SourceCode { get; set; } = string.Empty; -} - -/// -/// Expression statement -/// -public class ExpressionStatement : BlockStatement -{ - /// - /// The expression to execute - /// - public string Expression { get; set; } = string.Empty; -} - -/// -/// Variable declaration statement -/// -public class VariableDeclarationStatement : BlockStatement -{ - /// - /// The variable declaration - /// - public VariableDeclaration Declaration { get; set; } = new(); -} - -/// -/// Flow control statement types -/// -public enum FlowControlType -{ - /// - /// Branch to another block based on condition - /// - Branch, - - /// - /// Loop while condition is true (Loop has three args: condition, trueBlock, falseBlock) - /// - Loop, - - /// - /// Return from script execution - /// - Return, - - /// - /// Break from current loop - /// - Break, - - /// - /// To loop condition - marks the end of a loop body and returns to loop condition - /// - ToLoopCond -} - -/// -/// Flow control statement -/// -public class FlowControlStatement : BlockStatement -{ - /// - /// Type of flow control - /// - public FlowControlType ControlType { get; set; } - - /// - /// Condition expression (for Branch/Loop) - /// - public string ConditionExpression { get; set; } = string.Empty; - - /// - /// Target block name when condition is true (for Branch/Loop) - /// - public string TrueBlockName { get; set; } = string.Empty; - - /// - /// Target block name when condition is false (for Branch/Loop) - /// For Loop: this is the loop exit block - /// - public string FalseBlockName { get; set; } = string.Empty; - - /// - /// For ToLoopCond: the block name containing the Loop statement to return to - /// - public string? ToLoopCondReturnTo { get; set; } - - /// - /// Regenerates SourceCode from current field values. - /// Call after updating TrueBlockName/FalseBlockName/etc. to keep SourceCode in sync. - /// - public void RegenerateSourceCode() - { - SourceCode = ControlType switch - { - FlowControlType.Branch => $"NextBlock = Branch({ConditionExpression}, \"{TrueBlockName}\", \"{FalseBlockName}\");", - FlowControlType.Loop => $"NextBlock = Loop({ConditionExpression}, \"{TrueBlockName}\", \"{FalseBlockName}\");", - FlowControlType.ToLoopCond => ToLoopCondReturnTo != null - ? $"NextBlock = ToLoopCond(\"{ToLoopCondReturnTo}\");" - : "ToLoopCond();", - FlowControlType.Break => "Break();", - _ => SourceCode - }; - } -} - -/// -/// Parsed block script container -/// -public class BlockScript -{ - /// - /// Global constants block (ConstBlock) - /// - public BlockDefinition? ConstBlock { get; set; } - - /// - /// Public variables block (PubVarBlock) - optional - /// - public BlockDefinition? PubVarBlock { get; set; } - - /// - /// Main entry block (MainBlock) - /// - public BlockDefinition? MainBlock { get; set; } - - /// - /// Named blocks dictionary by name - /// - public Dictionary NamedBlocks { get; set; } = []; - - /// - /// All blocks in order of appearance - /// - public List AllBlocks { get; set; } = []; - - /// - /// Loop blocks dictionary by parent block name - /// (e.g., "MainBlock" -> LoopBlock for MainBlock's Loop statement) - /// - public Dictionary LoopBlocks { get; set; } = []; - - /// - /// Raw source code (parsed input, may not include helper functions) - /// - public string SourceCode { get; set; } = string.Empty; - - /// - /// Full source code including merged helper functions (for execution) - /// - public string FullSourceCode { get; set; } = string.Empty; - - /// - /// Helper functions to be made available in script execution context - /// - public List HelperFunctions { get; set; } = []; - - - /// - /// Gets a block by name (checks LoopBlocks first by block name, then NamedBlocks, then standard blocks) - /// IMPORTANT: LoopBlocks are checked FIRST because LoopBlock names (like "LoopBody") should NOT be - /// shadowed by user-defined NamedBlocks with the same name. - /// - public BlockDefinition? GetBlockByName(string name) - { - // First check LoopBlocks by the block's own name (not parent block name) - // This is critical because LoopBlocks are created for "NextBlock = Loop(...)" statements - // and their names (like "LoopBody") should take precedence over user-defined blocks - foreach (var kvp in LoopBlocks) - { - if (kvp.Value.Name == name) - return kvp.Value; - } - - // Then check NamedBlocks (user-defined blocks) - if (NamedBlocks.TryGetValue(name, out var namedBlock)) - return namedBlock; - - // Then check the standard blocks by name match - if (MainBlock?.Name == name) - return MainBlock; - if (ConstBlock?.Name == name) - return ConstBlock; - if (PubVarBlock?.Name == name) - return PubVarBlock; - - return null; - } -} - -/// -/// Result of parsing operation -/// -public class BlockScriptParseResult -{ - /// - /// Whether parsing was successful - /// - public bool IsSuccess { get; set; } - - /// - /// Error message if parsing failed - /// - public string? ErrorMessage { get; set; } - - /// - /// Line number where error occurred - /// - public int ErrorLine { get; set; } - - /// - /// The parsed script if successful - /// - public BlockScript? Script { get; set; } -} - -/// -/// Validation result for block scripts -/// -public class BlockScriptValidationResult -{ - /// - /// Whether the script is valid - /// - public bool IsValid { get; set; } = true; - - /// - /// List of errors found - /// - public List Errors { get; set; } = []; - - /// - /// List of warnings - /// - public List Warnings { get; set; } = []; - - /// - /// Adds an error - /// - public void AddError(string error) => Errors.Add(error); - - /// - /// Adds a warning - /// - public void AddWarning(string warning) => Warnings.Add(warning); -} - -/// -/// Execution result for block scripts -/// -public class BlockScriptExecutionResult -{ - /// - /// Whether execution was successful - /// - public bool IsSuccess { get; set; } - - /// - /// Return value from script (if any) - /// - public object? ReturnValue { get; set; } - - /// - /// Error message if execution failed - /// - public string? ErrorMessage { get; set; } - - /// - /// Output from Print() calls - /// - public List Output { get; set; } = []; - - /// - /// Number of blocks executed - /// - public int ExecutedBlockCount { get; set; } - - /// - /// Execution time in milliseconds - /// - public long ExecutionTimeMs { get; set; } -} - -/// -/// Result of executing a block - used by state machine for flow control -/// -public class BlockExecutionResult -{ - /// - /// Whether execution should continue to next block - /// - public bool ShouldContinue { get; set; } = true; - - /// - /// Name of the next block to execute (null means end of script) - /// - public string? NextBlockName { get; set; } - - /// - /// Whether this is a return (end of entire script) - /// - public bool IsReturn { get; set; } - - /// - /// Return value if IsReturn is true - /// - public object? ReturnValue { get; set; } - - /// - /// Create a result for continuing to next block - /// - public static BlockExecutionResult ContinueTo(string? nextBlockName) => new() - { - ShouldContinue = true, - NextBlockName = nextBlockName, - IsReturn = false - }; - - /// - /// Create a result for end of script - /// - public static BlockExecutionResult Return(object? value = null) => new() - { - ShouldContinue = false, - NextBlockName = null, - IsReturn = true, - ReturnValue = value - }; -} - +} \ No newline at end of file diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/BlueprintModels.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/BlueprintModels.cs index bcefed0..11aec19 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/BlueprintModels.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/BlueprintModels.cs @@ -24,740 +24,14 @@ public ViewPoint(double x, double y) public static implicit operator ViewPoint((double X, double Y) p) => new(p.X, p.Y); } -/// -/// Blueprint node types -/// -public enum BlueprintNodeType -{ - /// - /// Entry point node - triggered by Run button - /// - Entry, - - /// - /// Plugin event trigger node - alternative entry point activated by plugin triggers. - /// Has same pin structure as Entry (0 input, 1 Exec output) but carries PluginName/TriggerName metadata. - /// - PluginTrigger, - - /// - /// Conditional branch node - /// - Branch, - - /// - /// Loop node - /// - Loop, - - /// - /// Break from loop node - /// - Break, - - /// - /// Constant value node - /// - Const, - - /// - /// Plugin function call node - /// - Call, - - /// - /// Helper function call node - /// - CallHelper, - - /// - /// Get variable value node - reads a PubVar - /// - Get, - - /// - /// Set variable value node - writes to a PubVar - /// - Set, - - /// - /// Print output node - /// - Print, - - /// - /// Pause execution node - /// - Pause, - - /// - /// Variable declaration node (ConstBlock variables without initial values). - /// A floating node with no ports — users can only change the data type. - /// - Variable, - - /// - /// 通用内置函数节点。通过 BuiltinFunctionNode.FunctionName 区分具体函数。 - /// 新的内置函数统一使用此类型,无需为每个函数创建专用 enum 值。 - /// - BuiltinFunction -} - -/// -/// Pin direction -/// -public enum PinDirection -{ - Input, - Output -} - -/// -/// Pin type for data flow -/// -public enum PinType -{ - /// - /// Execution flow pin - green - /// - Execution, - - /// - /// Boolean pin - cyan - /// - Boolean, - - /// - /// Integer pin - orange - /// - Integer, - - /// - /// Double pin - purple - /// - Double, - - /// - /// String pin - yellow - /// - String, - - /// - /// Any type pin - white - /// - Any -} - -/// -/// Describes a pin's layout within a node template. -/// Used for self-describing node pin configurations. -/// -public record PinDescriptor( - string Name, - PinType Type, - double RelativeY -); - -/// -/// Describes a node type's layout and pin configuration. -/// Each node subclass provides its own descriptor via GetDescriptor(). -/// -public record NodeDescriptor( - double Width, - double Height, - IReadOnlyList InputPins, - IReadOnlyList OutputPins, - string DisplayName -); - -/// -/// Pin on a blueprint node -/// -public class BlueprintPin -{ - /// - /// Unique identifier - /// - public string Id { get; set; } = Guid.NewGuid().ToString(); - - /// - /// Pin name (e.g., "Exec", "Condition", "True", "Value") - /// - public string Name { get; set; } = string.Empty; - - /// - /// Pin direction - /// - public PinDirection Direction { get; set; } - - /// - /// Pin data type - /// - public PinType Type { get; set; } = PinType.Any; - - /// - /// Default value for input pins - /// - public string? DefaultValue { get; set; } -} - -/// -/// Base class for all blueprint nodes. -/// Uses polymorphic JSON serialization so that concrete node types -/// round-trip correctly through System.Text.Json. -/// -[JsonPolymorphic(UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(EntryNode), "Entry")] -[JsonDerivedType(typeof(BranchNode), "Branch")] -[JsonDerivedType(typeof(LoopNode), "Loop")] -[JsonDerivedType(typeof(BreakNode), "Break")] -[JsonDerivedType(typeof(ConstNode), "Const")] -[JsonDerivedType(typeof(CallNode), "Call")] -[JsonDerivedType(typeof(CallHelperNode), "CallHelper")] -[JsonDerivedType(typeof(GetNode), "Get")] -[JsonDerivedType(typeof(SetNode), "Set")] -[JsonDerivedType(typeof(PrintNode), "Print")] -[JsonDerivedType(typeof(PauseNode), "Pause")] -[JsonDerivedType(typeof(VariableNode), "Variable")] -[JsonDerivedType(typeof(BuiltinFunctionNode), "BuiltinFunction")] -[JsonDerivedType(typeof(PluginTriggerNode), "PluginTrigger")] -public abstract class BlueprintNode -{ - /// - /// Unique identifier - /// - public string Id { get; set; } = Guid.NewGuid().ToString(); - - /// - /// Node type - /// - public BlueprintNodeType NodeType { get; set; } - - /// - /// Display name - /// - public string Name { get; set; } = string.Empty; - - /// - /// X position on canvas - /// - public double X { get; set; } - - /// - /// Y position on canvas - /// - public double Y { get; set; } - - /// - /// Node width - /// - public double Width { get; set; } = 200; - - /// - /// Node height - /// - public double Height { get; set; } = 100; - - /// - /// Whether node is selected - /// - public bool IsSelected { get; set; } - - /// - /// Input pins - /// - public List InputPins { get; set; } = []; - - /// - /// Output pins - /// - public List OutputPins { get; set; } = []; - - /// - /// Parent blueprint reference (set when node is added to blueprint). - /// Ignored during JSON serialization to prevent circular reference. - /// - [JsonIgnore] - public Blueprint? Blueprint { get; set; } - - /// - /// Get pin by ID - /// - public BlueprintPin? GetPinById(string pinId) - { - foreach (var pin in InputPins) - if (pin.Id == pinId) return pin; - foreach (var pin in OutputPins) - if (pin.Id == pinId) return pin; - return null; - } - - /// - /// Get all pins - /// - public IEnumerable GetAllPins() - { - foreach (var pin in InputPins) - yield return pin; - foreach (var pin in OutputPins) - yield return pin; - } - - /// - /// Returns the layout descriptor for this node type. - /// Each node subclass must define its own pin layout, width, and height. - /// Used by the node registry and UI rendering to avoid external switch statements. - /// - public abstract NodeDescriptor GetDescriptor(); - - /// - /// Returns the display title for UI rendering (e.g., "Call: Plugin.Func"). - /// Default implementation returns Name; subclasses override for richer display. - /// - public virtual string GetDisplayTitle() => Name; - - /// - /// Initializes InputPins and OutputPins from GetDescriptor(). - /// Subclasses should call this in their constructor instead of manually adding pins. - /// This ensures the descriptor is the single source of truth for pin layout. - /// - protected void InitializePinsFromDescriptor() - { - var desc = GetDescriptor(); - foreach (var pd in desc.InputPins) - InputPins.Add(new BlueprintPin { Name = pd.Name, Direction = PinDirection.Input, Type = pd.Type }); - foreach (var pd in desc.OutputPins) - OutputPins.Add(new BlueprintPin { Name = pd.Name, Direction = PinDirection.Output, Type = pd.Type }); - } -} - -// Node-specific classes can be defined for additional properties -// Currently using the abstract base with runtime-added properties via dynamics or dedicated subclasses - -/// -/// Entry node - execution entry point -/// -public class EntryNode : BlueprintNode -{ - public EntryNode() - { - NodeType = BlueprintNodeType.Entry; - Name = "Entry"; - InitializePinsFromDescriptor(); - } - - public override NodeDescriptor GetDescriptor() => new( - Width: 120, Height: 60, - InputPins: [], - OutputPins: [new PinDescriptor("Exec", PinType.Execution, 30)], - DisplayName: "Entry" - ); -} - -/// -/// Plugin trigger entry node - activated when a specific plugin fires a specific trigger signal. -/// Has same pin structure as Entry (0 input, 1 Exec output) but carries PluginName/TriggerName metadata. -/// -public class PluginTriggerNode : BlueprintNode -{ - /// The plugin name to listen for - public string PluginName { get; set; } = string.Empty; - - /// The trigger name to listen for - public string TriggerName { get; set; } = string.Empty; - - public PluginTriggerNode() - { - NodeType = BlueprintNodeType.PluginTrigger; - Name = "PluginTrigger"; - InitializePinsFromDescriptor(); - } - - public override NodeDescriptor GetDescriptor() => new( - Width: 160, Height: 60, - InputPins: [], - OutputPins: [new PinDescriptor("Exec", PinType.Execution, 30)], - DisplayName: "PluginTrigger" - ); - - public override string GetDisplayTitle() - => string.IsNullOrEmpty(PluginName) - ? $"Trigger: {TriggerName}" - : $"Trigger: {PluginName}.{TriggerName}"; -} - -/// -/// Branch node - conditional execution -/// -public class BranchNode : BlueprintNode -{ - public BranchNode() - { - NodeType = BlueprintNodeType.Branch; - Name = "Branch"; - InitializePinsFromDescriptor(); - } - - public override NodeDescriptor GetDescriptor() => new( - Width: 120, Height: 80, - InputPins: [ - new PinDescriptor("Exec", PinType.Execution, 30), - new PinDescriptor("Condition", PinType.Boolean, 50) - ], - OutputPins: [ - new PinDescriptor("True", PinType.Execution, 30), - new PinDescriptor("False", PinType.Execution, 50) - ], - DisplayName: "Branch" - ); -} - -/// -/// Loop node - iterative execution -/// -public class LoopNode : BlueprintNode -{ - public LoopNode() - { - NodeType = BlueprintNodeType.Loop; - Name = "Loop"; - InitializePinsFromDescriptor(); - } - - public override NodeDescriptor GetDescriptor() => new( - Width: 120, Height: 80, - InputPins: [ - new PinDescriptor("Exec", PinType.Execution, 30), - new PinDescriptor("Condition", PinType.Boolean, 50) - ], - OutputPins: [ - new PinDescriptor("LoopBody", PinType.Execution, 30), - new PinDescriptor("LoopEnd", PinType.Execution, 50) - ], - DisplayName: "Loop" - ); -} - -/// -/// Break node - exit loop -/// -public class BreakNode : BlueprintNode -{ - public BreakNode() - { - NodeType = BlueprintNodeType.Break; - Name = "Break"; - InitializePinsFromDescriptor(); - } - - public override NodeDescriptor GetDescriptor() => new( - Width: 100, Height: 40, - InputPins: [new PinDescriptor("Exec", PinType.Execution, 20)], - OutputPins: [], - DisplayName: "Break" - ); -} - -/// -/// Const node - constant value -/// -public class ConstNode : BlueprintNode -{ - /// - /// Constant name - /// - public string ConstName { get; set; } = string.Empty; - - /// - /// Constant type - /// - public string ConstType { get; set; } = "string"; - - /// - /// Constant value - /// - public string? ConstValue { get; set; } - - public ConstNode() - { - NodeType = BlueprintNodeType.Const; - Name = "Const"; - InitializePinsFromDescriptor(); - } - - public override NodeDescriptor GetDescriptor() => new( - Width: 120, Height: 50, - InputPins: [], - OutputPins: [new PinDescriptor("Value", PinType.Any, 25)], - DisplayName: "Const" - ); - - public override string GetDisplayTitle() => $"Const: {ConstName}"; -} - -/// -/// Call node - plugin function call -/// -public class CallNode : BlueprintNode -{ - /// - /// Plugin name - /// - public string PluginName { get; set; } = string.Empty; - - /// - /// Function name - /// - public string FunctionName { get; set; } = string.Empty; - - /// - /// Target device name for cross-device calls. - /// If null or empty, call is routed locally via PluginCall. - /// - public string? TargetDevice { get; set; } - - public CallNode() - { - NodeType = BlueprintNodeType.Call; - Name = "Call"; - InitializePinsFromDescriptor(); - } - - public override NodeDescriptor GetDescriptor() => new( - Width: 140, Height: 60, - InputPins: [new PinDescriptor("Exec", PinType.Execution, 20)], - OutputPins: [ - new PinDescriptor("Exec", PinType.Execution, 20), - new PinDescriptor("Return", PinType.Any, 40) - ], - DisplayName: "Call" - ); - - public override string GetDisplayTitle() - { - var baseTitle = string.IsNullOrEmpty(PluginName) ? $"Call: {FunctionName}" : $"Call: {PluginName}.{FunctionName}"; - return string.IsNullOrEmpty(TargetDevice) ? baseTitle : $"{baseTitle} @ {TargetDevice}"; - } -} - -/// -/// CallHelper node - helper function call -/// -public class CallHelperNode : BlueprintNode -{ - /// - /// Helper function name reference - /// - public string HelperFunctionName { get; set; } = string.Empty; - - public CallHelperNode() - { - NodeType = BlueprintNodeType.CallHelper; - Name = "CallHelper"; - InitializePinsFromDescriptor(); - } - - public override NodeDescriptor GetDescriptor() => new( - Width: 130, Height: 50, - InputPins: [new PinDescriptor("Exec", PinType.Execution, 25)], - OutputPins: [ - new PinDescriptor("Exec", PinType.Execution, 25), - new PinDescriptor("Return", PinType.Any, 40) - ], - DisplayName: "CallHelper" - ); - - public override string GetDisplayTitle() => $"Helper: {HelperFunctionName}"; -} - -/// -/// Get variable value node - reads a PubVar -/// -public class GetNode : BlueprintNode -{ - /// - /// Variable name to read - /// - public string VarName { get; set; } = string.Empty; - - public GetNode() - { - NodeType = BlueprintNodeType.Get; - Name = "Get"; - InitializePinsFromDescriptor(); - } - - public override NodeDescriptor GetDescriptor() => new( - Width: 120, Height: 60, - InputPins: [new PinDescriptor("Exec", PinType.Execution, 20)], - OutputPins: [ - new PinDescriptor("Exec", PinType.Execution, 20), - new PinDescriptor("Value", PinType.Any, 40) - ], - DisplayName: "Get" - ); - - public override string GetDisplayTitle() => $"Get: {VarName}"; -} - -/// -/// Set variable value node - writes to a PubVar -/// -public class SetNode : BlueprintNode -{ - /// - /// Variable name to write - /// - public string VarName { get; set; } = string.Empty; - - public SetNode() - { - NodeType = BlueprintNodeType.Set; - Name = "Set"; - InitializePinsFromDescriptor(); - } - - public override NodeDescriptor GetDescriptor() => new( - Width: 120, Height: 60, - InputPins: [ - new PinDescriptor("Exec", PinType.Execution, 20), - new PinDescriptor("Value", PinType.Any, 40) - ], - OutputPins: [new PinDescriptor("Exec", PinType.Execution, 20)], - DisplayName: "Set" - ); - - public override string GetDisplayTitle() => $"Set: {VarName}"; -} - -/// -/// Print node - output information -/// -public class PrintNode : BlueprintNode -{ - public PrintNode() - { - NodeType = BlueprintNodeType.Print; - Name = "Print"; - InitializePinsFromDescriptor(); - } - - public override NodeDescriptor GetDescriptor() => new( - Width: 100, Height: 50, - InputPins: [ - new PinDescriptor("Exec", PinType.Execution, 20), - new PinDescriptor("Value", PinType.Any, 35) - ], - OutputPins: [new PinDescriptor("Exec", PinType.Execution, 25)], - DisplayName: "Print" - ); -} - -/// -/// Pause node - pause execution -/// -public class PauseNode : BlueprintNode -{ - public PauseNode() - { - NodeType = BlueprintNodeType.Pause; - Name = "Pause"; - InitializePinsFromDescriptor(); - } - - public override NodeDescriptor GetDescriptor() => new( - Width: 100, Height: 50, - InputPins: [ - new PinDescriptor("Exec", PinType.Execution, 20), - new PinDescriptor("Milliseconds", PinType.Integer, 35) - ], - OutputPins: [new PinDescriptor("Exec", PinType.Execution, 25)], - DisplayName: "Pause" - ); -} - -/// -/// Variable node - ConstBlock variable without initial value. -/// A floating node with no input/output ports. Users can change the data type -/// but not the initial value. Type changes propagate to Get/Set nodes. -/// -public class VariableNode : BlueprintNode -{ - /// - /// Variable name - /// - public string VarName { get; set; } = string.Empty; - - /// - /// Variable type (e.g., "int", "double", "string", "bool") - /// - public string VarType { get; set; } = "int"; - - public VariableNode() - { - NodeType = BlueprintNodeType.Variable; - Name = "Variable"; - InitializePinsFromDescriptor(); - } - - public override NodeDescriptor GetDescriptor() => new( - Width: 120, Height: 50, - InputPins: [], - OutputPins: [], - DisplayName: "Variable" - ); - - public override string GetDisplayTitle() => $"Var: {VarName}"; -} - -/// -/// 通用内置函数节点。通过 区分具体函数。 -/// 引脚布局由 驱动, -/// 消除了为每个内置函数创建专用节点子类的需要。 -/// -public class BuiltinFunctionNode : BlueprintNode -{ - /// - /// BlockScript 函数名(如 "Flip"),作为具体函数的唯一标识 - /// - public string FunctionName { get; set; } = string.Empty; - - /// - /// 额外属性字典,用于特殊节点(如 Set 的 VarName、Get 的 VarName) - /// - public Dictionary Properties { get; set; } = []; - - private NodeDescriptor? _descriptor; - - /// - /// 由 BuiltinFunctionRegistry 在创建节点时设置,基于 IBuiltinFunctionDefinition 的引脚描述 - /// - public void SetDescriptor(NodeDescriptor descriptor) => _descriptor = descriptor; - - public override NodeDescriptor GetDescriptor() => _descriptor ?? new( - Width: 120, Height: 60, - InputPins: [], - OutputPins: [], - DisplayName: FunctionName - ); - - public BuiltinFunctionNode() - { - NodeType = BlueprintNodeType.BuiltinFunction; - Name = "BuiltinFunction"; - } - - public override string GetDisplayTitle() => FunctionName; -} - /// /// Connection between two pins /// public class BlueprintConnection { /// - /// Unique identifier - /// +/// Unique identifier +/// public string Id { get; set; } = Guid.NewGuid().ToString(); /// diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/FlowControlType.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/FlowControlType.cs new file mode 100644 index 0000000..8cfdd0d --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/FlowControlType.cs @@ -0,0 +1,32 @@ +namespace KitX.Core.Contract.Workflow; + +/// +/// Flow control statement types +/// +public enum FlowControlType +{ + /// + /// Branch to another block based on condition + /// + Branch, + + /// + /// Loop while condition is true (Loop has three args: condition, trueBlock, falseBlock) + /// + Loop, + + /// + /// Return from script execution + /// + Return, + + /// + /// Break from current loop + /// + Break, + + /// + /// To loop condition - marks the end of a loop body and returns to loop condition + /// + ToLoopCond +} \ No newline at end of file diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/INodeExportStrategy.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/INodeExportStrategy.cs new file mode 100644 index 0000000..3072c5b --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/INodeExportStrategy.cs @@ -0,0 +1,74 @@ +using System.Collections.Generic; + +namespace KitX.Core.Contract.Workflow; + +/// +/// Helper service providing data resolution utilities for node export strategies. +/// Implemented by BlueprintToBlockScriptConverter. +/// +public interface INodeExportHelper +{ + /// +/// Resolves the input value for a pin by tracing data connections. +/// Returns ConstName for const references, PubVarName for pubvar references, +/// or the pin's default value. +/// + string GetInputValue(BlueprintNode node, string pinName); + + /// +/// Resolves all non-Exec input arguments for a node, returning them as a comma-separated string. +/// + string GetInputArgs(BlueprintNode node); + + /// +/// The blueprint being converted. +/// + Blueprint Blueprint { get; } +} + +/// +/// Strategy for converting a specific node type to a BlockScript statement. +/// Each node type that participates in reverse conversion provides an implementation, +/// eliminating the need for switch-based dispatch in the converter. +/// +public interface INodeExportStrategy +{ + /// +/// The node type this strategy handles. +/// + BlueprintNodeType NodeType { get; } + + /// +/// Whether this node type represents a control flow construct (Branch, Loop, etc.). +/// Used by the converter to determine main flow termination and sub-graph processing. +/// + bool IsControlFlow { get; } + + /// +/// Converts the node to a BlockScript statement, or null if the node should be skipped. +/// + BlockStatement? ToStatement(BlueprintNode node, INodeExportHelper helper); + + /// +/// For control flow nodes: returns the output arm configuration. +/// Each arm defines an output pin name and whether it represents a loopback. +/// Non-control-flow strategies return an empty collection. +/// + IEnumerable GetOutputArms(BlueprintNode node); +} + +/// +/// Describes a single output arm of a control flow node (e.g., Branch's True/False, Loop's LoopBody/LoopEnd). +/// +public struct OutputArmDescriptor +{ + /// +/// The output pin name (e.g., Pins.True, Pins.False, Pins.LoopBody, Pins.LoopEnd) +/// + public string PinName { get; set; } + + /// +/// Whether this arm loops back to a parent node (LoopBody loops back to the Loop node) +/// + public bool IsLoopback { get; set; } +} diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowService.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowService.cs index a6c18e3..a6b389f 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowService.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowService.cs @@ -34,6 +34,13 @@ public interface IWorkflowManagementService /// Stops a workflow /// Task StopWorkflowAsync(string workflowId); + + /// + /// Compiles a workflow's BlockScript into a persisted assembly on disk. + /// + /// The workflow ID to compile and persist. + /// True if compilation and persistence succeeded. + Task CompileAndPersistWorkflowAsync(string workflowId); } /// @@ -157,22 +164,21 @@ Task ExecuteBlockScriptAsync( List helperFunctions, Dictionary? constantOverrides, System.Threading.CancellationToken cancellationToken = default); -} -/// -/// Workflow service interface - composite interface for backward compatibility -/// -public interface IWorkflowService : IWorkflowManagementService, IScriptExecutionService, - IWorkflowPluginService, IBlockScriptService -{ /// - /// Compiles a workflow's BlockScript into a persisted assembly on disk. - /// The compiled assembly is saved under Data/CompiledScripts/{workflowId}/ - /// and will be reused on subsequent runs instead of recompiling. + /// Compiles a BlockScript and persists the compiled assembly to disk. /// - /// The workflow ID to compile and persist. + /// The parsed BlockScript to compile. + /// The workflow ID for assembly naming. /// True if compilation and persistence succeeded. - Task CompileAndPersistWorkflowAsync(string workflowId); + Task CompileAndPersistAsync(BlockScript script, string workflowId); + + /// + /// Preloads all persisted compiled scripts for a workflow from disk. + /// + /// Workflow ID to preload scripts for. + /// Number of scripts loaded from disk. + int PreloadCompiledScripts(string workflowId); } /// diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/BlueprintNode.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/BlueprintNode.cs new file mode 100644 index 0000000..64df1ed --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/BlueprintNode.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace KitX.Core.Contract.Workflow; + +// Forward declaration - Node types are in their own files +/// +/// Base class for all blueprint nodes. +/// Uses polymorphic JSON serialization so that concrete node types +/// round-trip correctly through System.Text.Json. +/// +[JsonPolymorphic(UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(EntryNode), "Entry")] +[JsonDerivedType(typeof(BranchNode), "Branch")] +[JsonDerivedType(typeof(LoopNode), "Loop")] +[JsonDerivedType(typeof(BreakNode), "Break")] +[JsonDerivedType(typeof(ConstNode), "Const")] +[JsonDerivedType(typeof(CallNode), "Call")] +[JsonDerivedType(typeof(CallHelperNode), "CallHelper")] +[JsonDerivedType(typeof(GetNode), "Get")] +[JsonDerivedType(typeof(SetNode), "Set")] +[JsonDerivedType(typeof(PrintNode), "Print")] +[JsonDerivedType(typeof(PauseNode), "Pause")] +[JsonDerivedType(typeof(VariableNode), "Variable")] +[JsonDerivedType(typeof(BuiltinFunctionNode), "BuiltinFunction")] +[JsonDerivedType(typeof(PluginTriggerNode), "PluginTrigger")] +public abstract partial class BlueprintNode +{ + /// + /// Unique identifier + /// + public string Id { get; set; } = Guid.NewGuid().ToString(); + + /// + /// Node type + /// + public BlueprintNodeType NodeType { get; set; } + + /// + /// Display name + /// + public string Name { get; set; } = string.Empty; + + /// + /// X position on canvas + /// + public double X { get; set; } + + /// + /// Y position on canvas + /// + public double Y { get; set; } + + /// + /// Node width + /// + public double Width { get; set; } = 200; + + /// + /// Node height + /// + public double Height { get; set; } = 100; + + /// + /// Whether node is selected + /// + public bool IsSelected { get; set; } + + /// + /// Input pins + /// + public List InputPins { get; set; } = []; + + /// + /// Output pins + /// + public List OutputPins { get; set; } = []; + + /// + /// Parent blueprint reference (set when node is added to blueprint). + /// Ignored during JSON serialization to prevent circular reference. + /// + [JsonIgnore] + public Blueprint? Blueprint { get; set; } + + /// + /// Get pin by ID + /// + public BlueprintPin? GetPinById(string pinId) + { + foreach (var pin in InputPins) + if (pin.Id == pinId) return pin; + foreach (var pin in OutputPins) + if (pin.Id == pinId) return pin; + return null; + } + + /// + /// Get all pins + /// + public IEnumerable GetAllPins() + { + foreach (var pin in InputPins) + yield return pin; + foreach (var pin in OutputPins) + yield return pin; + } + + /// + /// Returns the layout descriptor for this node type. + /// Each node subclass must define its own pin layout, width, and height. + /// Used by the node registry and UI rendering to avoid external switch statements. + /// + public abstract NodeDescriptor GetDescriptor(); + + /// + /// Returns the display title for UI rendering (e.g., "Call: Plugin.Func"). + /// Default implementation returns Name; subclasses override for richer display. + /// + public virtual string GetDisplayTitle() => Name; + + /// + /// Initializes InputPins and OutputPins from GetDescriptor(). + /// Subclasses should call this in their constructor instead of manually adding pins. + /// This ensures the descriptor is the single source of truth for pin layout. + /// + protected void InitializePinsFromDescriptor() + { + var desc = GetDescriptor(); + foreach (var pd in desc.InputPins) + InputPins.Add(new BlueprintPin { Name = pd.Name, Direction = PinDirection.Input, Type = pd.Type }); + foreach (var pd in desc.OutputPins) + OutputPins.Add(new BlueprintPin { Name = pd.Name, Direction = PinDirection.Output, Type = pd.Type }); + } +} \ No newline at end of file diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/BlueprintNodeType.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/BlueprintNodeType.cs new file mode 100644 index 0000000..fda9986 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/BlueprintNodeType.cs @@ -0,0 +1,80 @@ +namespace KitX.Core.Contract.Workflow; + +/// +/// Blueprint node types +/// +public enum BlueprintNodeType +{ + /// + /// Entry point node - triggered by Run button + /// + Entry, + + /// + /// Plugin event trigger node - alternative entry point activated by plugin triggers. + /// Has same pin structure as Entry (0 input, 1 Exec output) but carries PluginName/TriggerName metadata. + /// + PluginTrigger, + + /// + /// Conditional branch node + /// + Branch, + + /// + /// Loop node + /// + Loop, + + /// + /// Break from loop node + /// + Break, + + /// + /// Constant value node + /// + Const, + + /// + /// Plugin function call node + /// + Call, + + /// + /// Helper function call node + /// + CallHelper, + + /// + /// Get variable value node - reads a PubVar + /// + Get, + + /// + /// Set variable value node - writes to a PubVar + /// + Set, + + /// + /// Print output node + /// + Print, + + /// + /// Pause execution node + /// + Pause, + + /// + /// Variable declaration node (ConstBlock variables without initial values). + /// A floating node with no ports — users can only change the data type. + /// + Variable, + + /// + /// 通用内置函数节点。通过 BuiltinFunctionNode.FunctionName 区分具体函数。 + /// 新的内置函数统一使用此类型,无需为每个函数创建专用 enum 值。 + /// + BuiltinFunction +} \ No newline at end of file diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/BlueprintPin.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/BlueprintPin.cs new file mode 100644 index 0000000..f1ba9ba --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/BlueprintPin.cs @@ -0,0 +1,34 @@ +using System; + +namespace KitX.Core.Contract.Workflow; + +/// +/// Pin on a blueprint node +/// +public class BlueprintPin +{ + /// + /// Unique identifier + /// + public string Id { get; set; } = Guid.NewGuid().ToString(); + + /// + /// Pin name (e.g., "Exec", "Condition", "True", "Value") + /// + public string Name { get; set; } = string.Empty; + + /// + /// Pin direction + /// + public PinDirection Direction { get; set; } + + /// + /// Pin data type + /// + public PinType Type { get; set; } = PinType.Any; + + /// + /// Default value for input pins + /// + public string? DefaultValue { get; set; } +} \ No newline at end of file diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/BranchNode.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/BranchNode.cs new file mode 100644 index 0000000..495dcf0 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/BranchNode.cs @@ -0,0 +1,27 @@ +namespace KitX.Core.Contract.Workflow; + +/// +/// Branch node - conditional execution +/// +public class BranchNode : BlueprintNode +{ + public BranchNode() + { + NodeType = BlueprintNodeType.Branch; + Name = "Branch"; + InitializePinsFromDescriptor(); + } + + public override NodeDescriptor GetDescriptor() => new( + Width: 120, Height: 80, + InputPins: [ + new PinDescriptor("Exec", PinType.Execution, 30), + new PinDescriptor("Condition", PinType.Boolean, 50) + ], + OutputPins: [ + new PinDescriptor("True", PinType.Execution, 30), + new PinDescriptor("False", PinType.Execution, 50) + ], + DisplayName: "Branch" + ); +} \ No newline at end of file diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/BreakNode.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/BreakNode.cs new file mode 100644 index 0000000..97db11e --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/BreakNode.cs @@ -0,0 +1,21 @@ +namespace KitX.Core.Contract.Workflow; + +/// +/// Break node - exit loop +/// +public class BreakNode : BlueprintNode +{ + public BreakNode() + { + NodeType = BlueprintNodeType.Break; + Name = "Break"; + InitializePinsFromDescriptor(); + } + + public override NodeDescriptor GetDescriptor() => new( + Width: 100, Height: 40, + InputPins: [new PinDescriptor("Exec", PinType.Execution, 20)], + OutputPins: [], + DisplayName: "Break" + ); +} \ No newline at end of file diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/BuiltinFunctionNode.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/BuiltinFunctionNode.cs new file mode 100644 index 0000000..209d2d1 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/BuiltinFunctionNode.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; + +namespace KitX.Core.Contract.Workflow; + +/// +/// 通用内置函数节点。通过 区分具体函数。 +/// 引脚布局由 驱动, +/// 消除了为每个内置函数创建专用节点子类的需要。 +/// +public class BuiltinFunctionNode : BlueprintNode +{ + /// + /// BlockScript 函数名(如 "Flip"),作为具体函数的唯一标识 + /// + public string FunctionName { get; set; } = string.Empty; + + /// + /// 额外属性字典,用于特殊节点(如 Set 的 VarName、Get 的 VarName) + /// + public Dictionary Properties { get; set; } = []; + + private NodeDescriptor? _descriptor; + + /// + /// 由 BuiltinFunctionRegistry 在创建节点时设置,基于 IBuiltinFunctionDefinition 的引脚描述 + /// + public void SetDescriptor(NodeDescriptor descriptor) => _descriptor = descriptor; + + public override NodeDescriptor GetDescriptor() => _descriptor ?? new( + Width: 120, Height: 60, + InputPins: [], + OutputPins: [], + DisplayName: FunctionName + ); + + public BuiltinFunctionNode() + { + NodeType = BlueprintNodeType.BuiltinFunction; + Name = "BuiltinFunction"; + } + + public override string GetDisplayTitle() => FunctionName; +} \ No newline at end of file diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/CallHelperNode.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/CallHelperNode.cs new file mode 100644 index 0000000..00014d6 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/CallHelperNode.cs @@ -0,0 +1,31 @@ +namespace KitX.Core.Contract.Workflow; + +/// +/// CallHelper node - helper function call +/// +public class CallHelperNode : BlueprintNode +{ + /// + /// Helper function name reference + /// + public string HelperFunctionName { get; set; } = string.Empty; + + public CallHelperNode() + { + NodeType = BlueprintNodeType.CallHelper; + Name = "CallHelper"; + InitializePinsFromDescriptor(); + } + + public override NodeDescriptor GetDescriptor() => new( + Width: 130, Height: 50, + InputPins: [new PinDescriptor("Exec", PinType.Execution, 25)], + OutputPins: [ + new PinDescriptor("Exec", PinType.Execution, 25), + new PinDescriptor("Return", PinType.Any, 40) + ], + DisplayName: "CallHelper" + ); + + public override string GetDisplayTitle() => $"Helper: {HelperFunctionName}"; +} \ No newline at end of file diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/CallNode.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/CallNode.cs new file mode 100644 index 0000000..fb31e75 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/CallNode.cs @@ -0,0 +1,46 @@ +namespace KitX.Core.Contract.Workflow; + +/// +/// Call node - plugin function call +/// +public class CallNode : BlueprintNode +{ + /// + /// Plugin name + /// + public string PluginName { get; set; } = string.Empty; + + /// + /// Function name + /// + public string FunctionName { get; set; } = string.Empty; + + /// + /// Target device name for cross-device calls. + /// If null or empty, call is routed locally via PluginCall. + /// + public string? TargetDevice { get; set; } + + public CallNode() + { + NodeType = BlueprintNodeType.Call; + Name = "Call"; + InitializePinsFromDescriptor(); + } + + public override NodeDescriptor GetDescriptor() => new( + Width: 140, Height: 60, + InputPins: [new PinDescriptor("Exec", PinType.Execution, 20)], + OutputPins: [ + new PinDescriptor("Exec", PinType.Execution, 20), + new PinDescriptor("Return", PinType.Any, 40) + ], + DisplayName: "Call" + ); + + public override string GetDisplayTitle() + { + var baseTitle = string.IsNullOrEmpty(PluginName) ? $"Call: {FunctionName}" : $"Call: {PluginName}.{FunctionName}"; + return string.IsNullOrEmpty(TargetDevice) ? baseTitle : $"{baseTitle} @ {TargetDevice}"; + } +} \ No newline at end of file diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/ConstNode.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/ConstNode.cs new file mode 100644 index 0000000..85ce4fd --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/ConstNode.cs @@ -0,0 +1,38 @@ +namespace KitX.Core.Contract.Workflow; + +/// +/// Const node - constant value +/// +public class ConstNode : BlueprintNode +{ + /// + /// Constant name + /// + public string ConstName { get; set; } = string.Empty; + + /// + /// Constant type + /// + public string ConstType { get; set; } = "string"; + + /// + /// Constant value + /// + public string? ConstValue { get; set; } + + public ConstNode() + { + NodeType = BlueprintNodeType.Const; + Name = "Const"; + InitializePinsFromDescriptor(); + } + + public override NodeDescriptor GetDescriptor() => new( + Width: 120, Height: 50, + InputPins: [], + OutputPins: [new PinDescriptor("Value", PinType.Any, 25)], + DisplayName: "Const" + ); + + public override string GetDisplayTitle() => $"Const: {ConstName}"; +} \ No newline at end of file diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/EntryNode.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/EntryNode.cs new file mode 100644 index 0000000..fe96faa --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/EntryNode.cs @@ -0,0 +1,21 @@ +namespace KitX.Core.Contract.Workflow; + +/// +/// Entry node - execution entry point +/// +public class EntryNode : BlueprintNode +{ + public EntryNode() + { + NodeType = BlueprintNodeType.Entry; + Name = "Entry"; + InitializePinsFromDescriptor(); + } + + public override NodeDescriptor GetDescriptor() => new( + Width: 120, Height: 60, + InputPins: [], + OutputPins: [new PinDescriptor("Exec", PinType.Execution, 30)], + DisplayName: "Entry" + ); +} \ No newline at end of file diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/GetNode.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/GetNode.cs new file mode 100644 index 0000000..f53c857 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/GetNode.cs @@ -0,0 +1,31 @@ +namespace KitX.Core.Contract.Workflow; + +/// +/// Get variable value node - reads a PubVar +/// +public class GetNode : BlueprintNode +{ + /// + /// Variable name to read + /// + public string VarName { get; set; } = string.Empty; + + public GetNode() + { + NodeType = BlueprintNodeType.Get; + Name = "Get"; + InitializePinsFromDescriptor(); + } + + public override NodeDescriptor GetDescriptor() => new( + Width: 120, Height: 60, + InputPins: [new PinDescriptor("Exec", PinType.Execution, 20)], + OutputPins: [ + new PinDescriptor("Exec", PinType.Execution, 20), + new PinDescriptor("Value", PinType.Any, 40) + ], + DisplayName: "Get" + ); + + public override string GetDisplayTitle() => $"Get: {VarName}"; +} \ No newline at end of file diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/LoopNode.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/LoopNode.cs new file mode 100644 index 0000000..bffe2b5 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/LoopNode.cs @@ -0,0 +1,27 @@ +namespace KitX.Core.Contract.Workflow; + +/// +/// Loop node - iterative execution +/// +public class LoopNode : BlueprintNode +{ + public LoopNode() + { + NodeType = BlueprintNodeType.Loop; + Name = "Loop"; + InitializePinsFromDescriptor(); + } + + public override NodeDescriptor GetDescriptor() => new( + Width: 120, Height: 80, + InputPins: [ + new PinDescriptor("Exec", PinType.Execution, 30), + new PinDescriptor("Condition", PinType.Boolean, 50) + ], + OutputPins: [ + new PinDescriptor("LoopBody", PinType.Execution, 30), + new PinDescriptor("LoopEnd", PinType.Execution, 50) + ], + DisplayName: "Loop" + ); +} \ No newline at end of file diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/NodeDescriptor.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/NodeDescriptor.cs new file mode 100644 index 0000000..a981e6c --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/NodeDescriptor.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +namespace KitX.Core.Contract.Workflow; + +/// +/// Describes a node type's layout and pin configuration. +/// Each node subclass provides its own descriptor via GetDescriptor(). +/// +public record NodeDescriptor( + double Width, + double Height, + IReadOnlyList InputPins, + IReadOnlyList OutputPins, + string DisplayName +); \ No newline at end of file diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/PauseNode.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/PauseNode.cs new file mode 100644 index 0000000..2b0b279 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/PauseNode.cs @@ -0,0 +1,24 @@ +namespace KitX.Core.Contract.Workflow; + +/// +/// Pause node - pause execution +/// +public class PauseNode : BlueprintNode +{ + public PauseNode() + { + NodeType = BlueprintNodeType.Pause; + Name = "Pause"; + InitializePinsFromDescriptor(); + } + + public override NodeDescriptor GetDescriptor() => new( + Width: 100, Height: 50, + InputPins: [ + new PinDescriptor("Exec", PinType.Execution, 20), + new PinDescriptor("Milliseconds", PinType.Integer, 35) + ], + OutputPins: [new PinDescriptor("Exec", PinType.Execution, 25)], + DisplayName: "Pause" + ); +} \ No newline at end of file diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/PinDescriptor.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/PinDescriptor.cs new file mode 100644 index 0000000..de8599a --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/PinDescriptor.cs @@ -0,0 +1,11 @@ +namespace KitX.Core.Contract.Workflow; + +/// +/// Describes a pin's layout within a node template. +/// Used for self-describing node pin configurations. +/// +public record PinDescriptor( + string Name, + PinType Type, + double RelativeY +); \ No newline at end of file diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/PinDirection.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/PinDirection.cs new file mode 100644 index 0000000..15eb9a2 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/PinDirection.cs @@ -0,0 +1,10 @@ +namespace KitX.Core.Contract.Workflow; + +/// +/// Pin direction +/// +public enum PinDirection +{ + Input, + Output +} \ No newline at end of file diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/PinType.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/PinType.cs new file mode 100644 index 0000000..9ac29c8 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/PinType.cs @@ -0,0 +1,37 @@ +namespace KitX.Core.Contract.Workflow; + +/// +/// Pin type for data flow +/// +public enum PinType +{ + /// + /// Execution flow pin - green + /// + Execution, + + /// + /// Boolean pin - cyan + /// + Boolean, + + /// + /// Integer pin - orange + /// + Integer, + + /// + /// Double pin - purple + /// + Double, + + /// + /// String pin - yellow + /// + String, + + /// + /// Any type pin - white + /// + Any +} \ No newline at end of file diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/PluginTriggerNode.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/PluginTriggerNode.cs new file mode 100644 index 0000000..c242636 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/PluginTriggerNode.cs @@ -0,0 +1,33 @@ +namespace KitX.Core.Contract.Workflow; + +/// +/// Plugin trigger entry node - activated when a specific plugin fires a specific trigger signal. +/// Has same pin structure as Entry (0 input, 1 Exec output) but carries PluginName/TriggerName metadata. +/// +public class PluginTriggerNode : BlueprintNode +{ + /// The plugin name to listen for + public string PluginName { get; set; } = string.Empty; + + /// The trigger name to listen for + public string TriggerName { get; set; } = string.Empty; + + public PluginTriggerNode() + { + NodeType = BlueprintNodeType.PluginTrigger; + Name = "PluginTrigger"; + InitializePinsFromDescriptor(); + } + + public override NodeDescriptor GetDescriptor() => new( + Width: 160, Height: 60, + InputPins: [], + OutputPins: [new PinDescriptor("Exec", PinType.Execution, 30)], + DisplayName: "PluginTrigger" + ); + + public override string GetDisplayTitle() + => string.IsNullOrEmpty(PluginName) + ? $"Trigger: {TriggerName}" + : $"Trigger: {PluginName}.{TriggerName}"; +} \ No newline at end of file diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/PrintNode.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/PrintNode.cs new file mode 100644 index 0000000..799a606 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/PrintNode.cs @@ -0,0 +1,24 @@ +namespace KitX.Core.Contract.Workflow; + +/// +/// Print node - output information +/// +public class PrintNode : BlueprintNode +{ + public PrintNode() + { + NodeType = BlueprintNodeType.Print; + Name = "Print"; + InitializePinsFromDescriptor(); + } + + public override NodeDescriptor GetDescriptor() => new( + Width: 100, Height: 50, + InputPins: [ + new PinDescriptor("Exec", PinType.Execution, 20), + new PinDescriptor("Value", PinType.Any, 35) + ], + OutputPins: [new PinDescriptor("Exec", PinType.Execution, 25)], + DisplayName: "Print" + ); +} \ No newline at end of file diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/SetNode.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/SetNode.cs new file mode 100644 index 0000000..f20941c --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/SetNode.cs @@ -0,0 +1,31 @@ +namespace KitX.Core.Contract.Workflow; + +/// +/// Set variable value node - writes to a PubVar +/// +public class SetNode : BlueprintNode +{ + /// + /// Variable name to write + /// + public string VarName { get; set; } = string.Empty; + + public SetNode() + { + NodeType = BlueprintNodeType.Set; + Name = "Set"; + InitializePinsFromDescriptor(); + } + + public override NodeDescriptor GetDescriptor() => new( + Width: 120, Height: 60, + InputPins: [ + new PinDescriptor("Exec", PinType.Execution, 20), + new PinDescriptor("Value", PinType.Any, 40) + ], + OutputPins: [new PinDescriptor("Exec", PinType.Execution, 20)], + DisplayName: "Set" + ); + + public override string GetDisplayTitle() => $"Set: {VarName}"; +} \ No newline at end of file diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/VariableNode.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/VariableNode.cs new file mode 100644 index 0000000..24fa7f7 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/VariableNode.cs @@ -0,0 +1,35 @@ +namespace KitX.Core.Contract.Workflow; + +/// +/// Variable node - ConstBlock variable without initial value. +/// A floating node with no input/output ports. Users can change the data type +/// but not the initial value. Type changes propagate to Get/Set nodes. +/// +public class VariableNode : BlueprintNode +{ + /// + /// Variable name + /// + public string VarName { get; set; } = string.Empty; + + /// + /// Variable type (e.g., "int", "double", "string", "bool") + /// + public string VarType { get; set; } = "int"; + + public VariableNode() + { + NodeType = BlueprintNodeType.Variable; + Name = "Variable"; + InitializePinsFromDescriptor(); + } + + public override NodeDescriptor GetDescriptor() => new( + Width: 120, Height: 50, + InputPins: [], + OutputPins: [], + DisplayName: "Variable" + ); + + public override string GetDisplayTitle() => $"Var: {VarName}"; +} \ No newline at end of file diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/Results/BlockExecutionResult.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/Results/BlockExecutionResult.cs new file mode 100644 index 0000000..056f78d --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/Results/BlockExecutionResult.cs @@ -0,0 +1,48 @@ +namespace KitX.Core.Contract.Workflow; + +/// +/// Result of executing a block - used by state machine for flow control +/// +public class BlockExecutionResult +{ + /// + /// Whether execution should continue to next block + /// + public bool ShouldContinue { get; set; } = true; + + /// + /// Name of the next block to execute (null means end of script) + /// + public string? NextBlockName { get; set; } + + /// + /// Whether this is a return (end of entire script) + /// + public bool IsReturn { get; set; } + + /// + /// Return value if IsReturn is true + /// + public object? ReturnValue { get; set; } + + /// + /// Create a result for continuing to next block + /// + public static BlockExecutionResult ContinueTo(string? nextBlockName) => new() + { + ShouldContinue = true, + NextBlockName = nextBlockName, + IsReturn = false + }; + + /// + /// Create a result for end of script + /// + public static BlockExecutionResult Return(object? value = null) => new() + { + ShouldContinue = false, + NextBlockName = null, + IsReturn = true, + ReturnValue = value + }; +} \ No newline at end of file diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/Results/BlockScriptExecutionResult.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/Results/BlockScriptExecutionResult.cs new file mode 100644 index 0000000..9c48436 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/Results/BlockScriptExecutionResult.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; + +namespace KitX.Core.Contract.Workflow; + +/// +/// Execution result for block scripts +/// +public class BlockScriptExecutionResult +{ + /// + /// Whether execution was successful + /// + public bool IsSuccess { get; set; } + + /// + /// Return value from script (if any) + /// + public object? ReturnValue { get; set; } + + /// + /// Error message if execution failed + /// + public string? ErrorMessage { get; set; } + + /// + /// Output from Print() calls + /// + public List Output { get; set; } = []; + + /// + /// Number of blocks executed + /// + public int ExecutedBlockCount { get; set; } + + /// + /// Execution time in milliseconds + /// + public long ExecutionTimeMs { get; set; } +} \ No newline at end of file diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/Results/BlockScriptParseResult.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/Results/BlockScriptParseResult.cs new file mode 100644 index 0000000..594eee5 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/Results/BlockScriptParseResult.cs @@ -0,0 +1,27 @@ +namespace KitX.Core.Contract.Workflow; + +/// +/// Result of parsing operation +/// +public class BlockScriptParseResult +{ + /// + /// Whether parsing was successful + /// + public bool IsSuccess { get; set; } + + /// + /// Error message if parsing failed + /// + public string? ErrorMessage { get; set; } + + /// + /// Line number where error occurred + /// + public int ErrorLine { get; set; } + + /// + /// The parsed script if successful + /// + public BlockScript? Script { get; set; } +} \ No newline at end of file diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/Results/BlockScriptValidationResult.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/Results/BlockScriptValidationResult.cs new file mode 100644 index 0000000..b76fb4e --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/Results/BlockScriptValidationResult.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; + +namespace KitX.Core.Contract.Workflow; + +/// +/// Validation result for block scripts +/// +public class BlockScriptValidationResult +{ + /// + /// Whether the script is valid + /// + public bool IsValid { get; set; } = true; + + /// + /// List of errors found + /// + public List Errors { get; set; } = []; + + /// + /// List of warnings + /// + public List Warnings { get; set; } = []; + + /// + /// Adds an error + /// + public void AddError(string error) => Errors.Add(error); + + /// + /// Adds a warning + /// + public void AddWarning(string warning) => Warnings.Add(warning); +} \ No newline at end of file diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/Statements/BlockStatement.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/Statements/BlockStatement.cs new file mode 100644 index 0000000..177a147 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/Statements/BlockStatement.cs @@ -0,0 +1,17 @@ +namespace KitX.Core.Contract.Workflow; + +/// +/// Base class for statements within a block +/// +public abstract class BlockStatement +{ + /// + /// Line number in source + /// + public int LineNumber { get; set; } + + /// + /// Original source code for this statement + /// + public string SourceCode { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/Statements/ExpressionStatement.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/Statements/ExpressionStatement.cs new file mode 100644 index 0000000..2d32e60 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/Statements/ExpressionStatement.cs @@ -0,0 +1,12 @@ +namespace KitX.Core.Contract.Workflow; + +/// +/// Expression statement +/// +public class ExpressionStatement : BlockStatement +{ + /// + /// The expression to execute + /// + public string Expression { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/Statements/FlowControlStatement.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/Statements/FlowControlStatement.cs new file mode 100644 index 0000000..728e0ca --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/Statements/FlowControlStatement.cs @@ -0,0 +1,51 @@ +namespace KitX.Core.Contract.Workflow; + +/// +/// Flow control statement +/// +public class FlowControlStatement : BlockStatement +{ + /// + /// Type of flow control + /// + public FlowControlType ControlType { get; set; } + + /// + /// Condition expression (for Branch/Loop) + /// + public string ConditionExpression { get; set; } = string.Empty; + + /// + /// Target block name when condition is true (for Branch/Loop) + /// + public string TrueBlockName { get; set; } = string.Empty; + + /// + /// Target block name when condition is false (for Branch/Loop) + /// For Loop: this is the loop exit block + /// + public string FalseBlockName { get; set; } = string.Empty; + + /// + /// For ToLoopCond: the block name containing the Loop statement to return to + /// + public string? ToLoopCondReturnTo { get; set; } + + /// + /// Regenerates SourceCode from current field values. + /// Call after updating TrueBlockName/FalseBlockName/etc. to keep SourceCode in sync. + /// + public void RegenerateSourceCode() + { + SourceCode = ControlType switch + { + FlowControlType.Branch => $"NextBlock = Branch({ConditionExpression}, \"{TrueBlockName}\", \"{FalseBlockName}\");", + FlowControlType.Loop => $"NextBlock = Loop({ConditionExpression}, \"{TrueBlockName}\", \"{FalseBlockName}\");", + FlowControlType.ToLoopCond => ToLoopCondReturnTo != null + ? $"NextBlock = ToLoopCond(\"{ToLoopCondReturnTo}\");" + : "ToLoopCond();", + FlowControlType.Break => "Break();", + _ => SourceCode + }; + } +} \ No newline at end of file diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/Statements/VariableDeclarationStatement.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/Statements/VariableDeclarationStatement.cs new file mode 100644 index 0000000..4f6c4ef --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/Statements/VariableDeclarationStatement.cs @@ -0,0 +1,12 @@ +namespace KitX.Core.Contract.Workflow; + +/// +/// Variable declaration statement +/// +public class VariableDeclarationStatement : BlockStatement +{ + /// + /// The variable declaration + /// + public VariableDeclaration Declaration { get; set; } = new(); +} \ No newline at end of file diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/VariableDeclaration.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/VariableDeclaration.cs new file mode 100644 index 0000000..492007a --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/VariableDeclaration.cs @@ -0,0 +1,27 @@ +namespace KitX.Core.Contract.Workflow; + +/// +/// Variable declaration +/// +public class VariableDeclaration +{ + /// + /// Variable name + /// + public string Name { get; set; } = string.Empty; + + /// + /// Variable type as string + /// + public string Type { get; set; } = "object"; + + /// + /// Initial value expression as string (for evaluation at parse time or execution time) + /// + public string? InitialValueExpression { get; set; } + + /// + /// Default value (pre-evaluated for const block) + /// + public object? DefaultValue { get; set; } +} \ No newline at end of file From 9495389969c49dc9b9ba23e6278abce392ec0931 Mon Sep 17 00:00:00 2001 From: StarInk Date: Mon, 20 Apr 2026 22:40:00 +0200 Subject: [PATCH 46/60] =?UTF-8?q?=F0=9F=92=BE=20Feat(CallNode):=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20TargetDevice=20=E5=B1=9E=E6=80=A7=E4=BB=A5?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E8=B7=A8=E8=AE=BE=E5=A4=87=E8=B0=83=E7=94=A8?= =?UTF-8?q?=EF=BC=9B=E6=9B=B4=E6=96=B0=20GetDisplayTitle=20=E6=96=B9?= =?UTF-8?q?=E6=B3=95=E4=BB=A5=E6=98=BE=E7=A4=BA=E7=9B=AE=E6=A0=87=E8=AE=BE?= =?UTF-8?q?=E5=A4=87=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../KitX.Core.Contract/Workflow/Nodes/CallNode.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/CallNode.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/CallNode.cs index fb31e75..43d5b14 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/CallNode.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/CallNode.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; + namespace KitX.Core.Contract.Workflow; /// @@ -21,6 +23,13 @@ public class CallNode : BlueprintNode /// public string? TargetDevice { get; set; } + /// + /// Extra arguments beyond plugin name, method name, and target device. + /// Used by PluginCallWithTarget and similar functions with variable arguments. + /// Stored as raw argument expressions (e.g., "cityId", "__pubVar1"). + /// + public List ExtraArguments { get; set; } = new(); + public CallNode() { NodeType = BlueprintNodeType.Call; From 6cf48eb598d68030a9e3013bd37119e9e1a9c6f8 Mon Sep 17 00:00:00 2001 From: StarInk Date: Mon, 27 Apr 2026 15:33:15 +0200 Subject: [PATCH 47/60] =?UTF-8?q?=F0=9F=92=BE=20Feat(ISecurityConfig):=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20DeviceLocator=20=E5=92=8C=20RsaPublicKeyPe?= =?UTF-8?q?m=20=E5=B1=9E=E6=80=A7=E4=BB=A5=E5=A2=9E=E5=BC=BA=E8=AE=BE?= =?UTF-8?q?=E5=A4=87=E5=AF=86=E9=92=A5=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Configuration/ISecurityConfig.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/KitX Core Contracts/KitX.Core.Contract/Configuration/ISecurityConfig.cs b/KitX Core Contracts/KitX.Core.Contract/Configuration/ISecurityConfig.cs index b2b7a12..c100ce7 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Configuration/ISecurityConfig.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Configuration/ISecurityConfig.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using KitX.Shared.CSharp.Device; namespace KitX.Core.Contract.Configuration; @@ -19,6 +20,16 @@ public interface ISecurityConfig /// public interface IDeviceKey { + /// + /// Gets the device locator + /// + DeviceLocator Device { get; } + + /// + /// Gets the RSA public key in PEM format + /// + string? RsaPublicKeyPem { get; } + /// /// Gets the MAC address /// @@ -38,4 +49,4 @@ public interface IDeviceKey /// Gets the time when the key was added /// DateTime AddedAt { get; } -} +} \ No newline at end of file From f8509d2b2531958dfd2307a04ec32e38b8f0f6c6 Mon Sep 17 00:00:00 2001 From: StarInk Date: Sat, 2 May 2026 15:28:23 +0200 Subject: [PATCH 48/60] =?UTF-8?q?=F0=9F=92=BE=20Feat(DeviceService):=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E8=AE=BE=E5=A4=87=E7=99=BB=E5=BD=95=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E6=A3=80=E6=9F=A5=E3=80=81=E8=8E=B7=E5=8F=96=E8=AE=BE?= =?UTF-8?q?=E5=A4=87=E4=BB=A4=E7=89=8C=E5=8F=8A=E5=B7=B2=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E8=AE=BE=E5=A4=87=E5=88=97=E8=A1=A8=E5=8A=9F=E8=83=BD=EF=BC=9B?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=20ServerStatus=20=E6=9E=9A=E4=B8=BE=20?= =?UTF-8?q?=F0=9F=92=BE=20Feat(Plugin):=20=E6=96=B0=E5=A2=9E=E6=8F=92?= =?UTF-8?q?=E4=BB=B6=E8=BF=9E=E6=8E=A5=E6=8E=A5=E5=8F=A3=E5=8F=8A=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E4=BA=8B=E4=BB=B6=EF=BC=8C=E6=94=AF=E6=8C=81=E6=8F=92?= =?UTF-8?q?=E4=BB=B6=E8=BF=9E=E6=8E=A5=E7=8A=B6=E6=80=81=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E5=92=8C=E6=B6=88=E6=81=AF=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Device/IDeviceService.cs | 20 ++++++++ .../KitX.Core.Contract/Device/ServerStatus.cs | 13 +++++ .../Plugin/Events/PluginEventArgs.cs | 38 +++++++++++++++ .../Plugin/IPluginConnection.cs | 48 +++++++++++++++++++ .../Plugin/IPluginConnector.cs | 2 +- .../Plugin/IPluginServer.cs | 29 ++++++++++- 6 files changed, 148 insertions(+), 2 deletions(-) create mode 100644 KitX Core Contracts/KitX.Core.Contract/Device/ServerStatus.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Plugin/IPluginConnection.cs diff --git a/KitX Core Contracts/KitX.Core.Contract/Device/IDeviceService.cs b/KitX Core Contracts/KitX.Core.Contract/Device/IDeviceService.cs index 3a141b6..cd709c2 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Device/IDeviceService.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Device/IDeviceService.cs @@ -126,6 +126,26 @@ public interface IDeviceServer /// Stops the device server /// void Stop(); + + /// + /// Checks if a device is signed in + /// + /// The device locator + /// True if the device is signed in + bool IsDeviceSignedIn(KitX.Shared.CSharp.Device.DeviceLocator locator); + + /// + /// Gets the signed device token for a device locator + /// + /// The device locator + /// The token or null if not found + string? GetDeviceToken(KitX.Shared.CSharp.Device.DeviceLocator locator); + + /// + /// Gets all signed-in device locators + /// + /// Read-only list of signed-in device locators + System.Collections.Generic.IReadOnlyList GetSignedInDevices(); } /// diff --git a/KitX Core Contracts/KitX.Core.Contract/Device/ServerStatus.cs b/KitX Core Contracts/KitX.Core.Contract/Device/ServerStatus.cs new file mode 100644 index 0000000..c41bda0 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Device/ServerStatus.cs @@ -0,0 +1,13 @@ +namespace KitX.Core.Contract.Device; + +/// +/// Server status enumeration +/// +public enum ServerStatus +{ + Pending, + Starting, + Running, + Stopping, + Errored +} diff --git a/KitX Core Contracts/KitX.Core.Contract/Plugin/Events/PluginEventArgs.cs b/KitX Core Contracts/KitX.Core.Contract/Plugin/Events/PluginEventArgs.cs index 8249577..068086b 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Plugin/Events/PluginEventArgs.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Plugin/Events/PluginEventArgs.cs @@ -81,4 +81,42 @@ public class PluginUnregisteredEventArgs : EventArgs /// Gets or sets the plugin info /// public PluginInfo? PluginInfo { get; set; } +} + +/// +/// Plugin connected event arguments +/// +public class PluginConnectedEventArgs : EventArgs +{ + /// + /// Gets or sets the connection ID + /// + public string? ConnectionId { get; set; } +} + +/// +/// Plugin disconnected event arguments +/// +public class PluginDisconnectedEventArgs : EventArgs +{ + /// + /// Gets or sets the connection ID + /// + public string? ConnectionId { get; set; } +} + +/// +/// Plugin message received event arguments +/// +public class PluginMessageReceivedEventArgs : EventArgs +{ + /// + /// Gets or sets the connection ID + /// + public string? ConnectionId { get; set; } + + /// + /// Gets or sets the message + /// + public string? Message { get; set; } } \ No newline at end of file diff --git a/KitX Core Contracts/KitX.Core.Contract/Plugin/IPluginConnection.cs b/KitX Core Contracts/KitX.Core.Contract/Plugin/IPluginConnection.cs new file mode 100644 index 0000000..d343993 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Plugin/IPluginConnection.cs @@ -0,0 +1,48 @@ +using System; +using KitX.Shared.CSharp.Plugin; +using KitX.Core.Contract.Device; +using CTask = System.Threading.Tasks.Task; + +namespace KitX.Core.Contract.Plugin; + +/// +/// Plugin connection interface +/// +public interface IPluginConnection : IPluginConnector +{ + /// + /// Gets or sets the plugin info + /// + new PluginInfo? PluginInfo { get; set; } + + /// + /// Gets the connection status + /// + ServerStatus Status { get; } + + /// + /// Event raised when a message is received + /// + event EventHandler? MessageReceived; + + /// + /// Event raised when connection is closed + /// + event EventHandler? Closed; + + /// + /// Initializes the connection + /// + void Initialize(); + + /// + /// Sends a message + /// + /// The message to send + void Send(string message); + + /// + /// Closes the connection + /// + CTask CloseAsync(); +} diff --git a/KitX Core Contracts/KitX.Core.Contract/Plugin/IPluginConnector.cs b/KitX Core Contracts/KitX.Core.Contract/Plugin/IPluginConnector.cs index 0a326eb..5f63eff 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Plugin/IPluginConnector.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Plugin/IPluginConnector.cs @@ -12,7 +12,7 @@ public interface IPluginConnector /// /// Gets the connection ID /// - string ConnectionId { get; } + string? ConnectionId { get; } /// /// Gets the plugin info diff --git a/KitX Core Contracts/KitX.Core.Contract/Plugin/IPluginServer.cs b/KitX Core Contracts/KitX.Core.Contract/Plugin/IPluginServer.cs index 3f9d20f..871277a 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Plugin/IPluginServer.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Plugin/IPluginServer.cs @@ -18,7 +18,7 @@ public interface IPluginServer /// /// Gets the list of currently connected plugins /// - IReadOnlyList Connections { get; } + IReadOnlyList Connections { get; } /// /// Starts the plugin server @@ -38,11 +38,33 @@ public interface IPluginServer /// The plugin connector or null if not found IPluginConnector? FindConnector(PluginInfo pluginInfo); + /// + /// Finds a connection by connection ID + /// + /// The connection ID + /// The plugin connection or null if not found + IPluginConnection? FindConnection(string connectionId); + /// /// Event raised when server port changes /// event EventHandler? PortChanged; + /// + /// Event raised when a plugin connects + /// + event EventHandler? PluginConnected; + + /// + /// Event raised when a plugin disconnects + /// + event EventHandler? PluginDisconnected; + + /// + /// Event raised when a plugin message is received + /// + event EventHandler? PluginMessageReceived; + /// /// Event raised when a plugin registers with the server /// @@ -52,4 +74,9 @@ public interface IPluginServer /// Event raised when a plugin unregisters/disconnects from the server /// event EventHandler? PluginUnregistered; + + /// + /// Event raised when a plugin sends a response (has RequestId) + /// + event EventHandler? PluginResponse; } \ No newline at end of file From ddd94f7efd13cc6b60bf2109047c4b8134dfa2e6 Mon Sep 17 00:00:00 2001 From: StarInk Date: Tue, 5 May 2026 22:14:38 +0200 Subject: [PATCH 49/60] =?UTF-8?q?=F0=9F=A7=A9Refactor:=20=E7=A7=BB?= =?UTF-8?q?=E9=99=A4=E4=B8=8D=E5=86=8D=E4=BD=BF=E7=94=A8=E7=9A=84=E8=84=9A?= =?UTF-8?q?=E6=9C=AC=E6=89=A7=E8=A1=8C=E6=8E=A5=E5=8F=A3=E5=92=8C=E4=B8=BB?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=88=86=E6=9E=90=E7=BB=93=E6=9E=9C=E7=B1=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Workflow/IWorkflowService.cs | 31 ------------------- .../Workflow/KcsFileFormat.cs | 27 +--------------- 2 files changed, 1 insertion(+), 57 deletions(-) diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowService.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowService.cs index a6b389f..192b49b 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowService.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowService.cs @@ -43,37 +43,6 @@ public interface IWorkflowManagementService Task CompileAndPersistWorkflowAsync(string workflowId); } -/// -/// Script execution interface -/// -public interface IScriptExecutionService -{ - /// - /// Executes a workflow script - /// - Task ExecuteScriptAsync(string script, Dictionary? parameters = null); - - /// - /// Executes workflow script codes with plugin dependencies - /// - Task ExecuteCodesAsync( - string code, - List? requiredPlugins = null, - bool includeTimestamp = true, - System.Threading.CancellationToken cancellationToken = default); - - /// - /// Executes KCS codes - /// - Task ExecuteKcsCodesAsync( - string mainCode, - List helperFunctions, - List constants, - List? requiredPlugins = null, - bool includeTimestamp = true, - System.Threading.CancellationToken cancellationToken = default); -} - /// /// Plugin service interface for workflow constant and helper function handling /// diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/KcsFileFormat.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/KcsFileFormat.cs index a383fc7..ba1a1bc 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/KcsFileFormat.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/KcsFileFormat.cs @@ -152,22 +152,6 @@ public class VariableConstant public string Type { get; set; } = "string"; } -/// -/// 主程序代码分析结果 -/// -public class MainProgramAnalysisResult -{ - /// - /// 是否有效 - /// - public bool IsValid { get; set; } = true; - - /// - /// 禁止原因 - /// - public string ForbiddenReason { get; set; } = string.Empty; -} - /// /// KCS文件服务接口 - 仅负责KCS文件的读写 /// @@ -189,14 +173,5 @@ public interface IKcsFileService } /// -/// 主程序代码分析器接口 +/// KCS 文件服务接口 /// -public interface IMainProgramAnalyzer -{ - /// - /// 分析主程序代码 - /// - /// 要分析的代码 - /// 分析结果 - MainProgramAnalysisResult Analyze(string code); -} From 8d1243f4ade2eaf3e6a59f5624702c43191579d5 Mon Sep 17 00:00:00 2001 From: StarInk Date: Sun, 17 May 2026 06:52:18 +0200 Subject: [PATCH 50/60] =?UTF-8?q?=F0=9F=92=BE=F0=9F=A7=A9=20Feat,=20Refact?= =?UTF-8?q?or(Contract):=20=E6=89=A9=E5=B1=95CFG=E8=B0=83=E8=AF=95?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E4=B8=8EAST=E8=8A=82=E7=82=B9=E8=BA=AB?= =?UTF-8?q?=E4=BB=BD=E8=BF=BD=E8=B8=AA=E8=83=BD=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - BlockStatement 新增 StatementId 属性,默认空串,用于BP↔CFG↔BS双向保留节点身份 - BlockScript 新增 DebugNodeMapping 字段,存储调试用StatementId→NodeId映射 - BlockScriptExecutionResult 新增 DebugNodeMapping 字段,供执行结果返回调试映射 - IBlueprintService 新增 GetDebugNodeMapping(Blueprint) 方法 - IBlockScriptExecutor 新增 SetDebugger(IBlueprintDebugController?) 方法 - IBlueprintDebugController 新增 UpdateVariableSnapshot(Dictionary) 方法 - INodeExportHelper 新增 GetOutputPubVar(node, pinName) 与 IsOutputConsumed(node, pinName) --- .../Workflow/BlockScript.cs | 6 +++ .../Workflow/IBlockScriptParser.cs | 6 +++ .../Workflow/IBlueprintConverter.cs | 8 ++++ .../Workflow/IBlueprintDebugController.cs | 43 +++++++++++++++++++ .../Workflow/INodeExportStrategy.cs | 14 +++++- .../Results/BlockScriptExecutionResult.cs | 6 +++ .../Workflow/Statements/BlockStatement.cs | 7 +++ 7 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/IBlueprintDebugController.cs diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/BlockScript.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/BlockScript.cs index 95a0182..919ab65 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/BlockScript.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/BlockScript.cs @@ -48,6 +48,12 @@ public class BlockScript /// public string FullSourceCode { get; set; } = string.Empty; + /// + /// Debug mapping from CFG statement IDs to Blueprint node IDs. + /// Populated during BP→BS conversion for use by the debug execution pipeline. + /// + public Dictionary? DebugNodeMapping { get; set; } + /// /// Helper functions to be made available in script execution context /// diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/IBlockScriptParser.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/IBlockScriptParser.cs index 24ed4ce..b911579 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/IBlockScriptParser.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/IBlockScriptParser.cs @@ -49,6 +49,12 @@ Task ExecuteAsync( /// Validates a block script /// BlockScriptValidationResult Validate(BlockScript script); + + /// + /// Sets an optional debug controller for interactive execution (breakpoints, step, slow). + /// Pass null to disable debug mode. + /// + void SetDebugger(IBlueprintDebugController? debugger); } /// diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/IBlueprintConverter.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/IBlueprintConverter.cs index 01cf3ca..b7a46d0 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/IBlueprintConverter.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/IBlueprintConverter.cs @@ -76,4 +76,12 @@ public interface IBlueprintService /// Blueprint to execute /// Execution result Task ExecuteBlueprintAsync(Blueprint blueprint); + + /// + /// Build a mapping from CFG statement IDs to Blueprint node IDs for debug highlighting. + /// Key = statementId (also equals BlueprintNode.Id), Value = BlueprintNode.Id + /// + /// Blueprint to analyze + /// StatementId → NodeId mapping, or empty if conversion failed + Dictionary GetDebugNodeMapping(Blueprint blueprint); } diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/IBlueprintDebugController.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/IBlueprintDebugController.cs new file mode 100644 index 0000000..1057f78 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/IBlueprintDebugController.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace KitX.Core.Contract.Workflow; + +public enum ExecutionSpeed +{ + RealTime, + Slow, + StepByStep +} + +public interface IBlueprintDebugController +{ + event Action? NodeExecuting; + event Action? NodeExecuted; + event Action? BlockEntered; + event Action? VariableChanged; + event Action? ExecutionPaused; + event Action? ExecutionResumed; + + void SetBreakpoint(string nodeId); + void RemoveBreakpoint(string nodeId); + void ClearBreakpoints(); + bool HasBreakpoint(string nodeId); + + void Pause(); + void StepNext(); + void Continue(); + void SetSpeed(ExecutionSpeed speed); + + ExecutionSpeed Speed { get; } + bool IsPaused { get; } + + IReadOnlyDictionary CurrentVariableSnapshot { get; } + + void UpdateVariableSnapshot(Dictionary variables); + + Task CheckpointAsync( + string statementId, string? blockName, + System.Threading.CancellationToken cancellationToken); +} diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/INodeExportStrategy.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/INodeExportStrategy.cs index 3072c5b..918a998 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/INodeExportStrategy.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/INodeExportStrategy.cs @@ -21,9 +21,19 @@ public interface INodeExportHelper string GetInputArgs(BlueprintNode node); /// -/// The blueprint being converted. -/// + /// The blueprint being converted. + /// Blueprint Blueprint { get; } + + /// + /// Returns the PubVar name assigned to the given output pin, or null if no data connection exists. + /// + string? GetOutputPubVar(BlueprintNode node, string pinName); + + /// + /// Returns true if the given output pin is consumed by at least one data connection. + /// + bool IsOutputConsumed(BlueprintNode node, string pinName); } /// diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/Results/BlockScriptExecutionResult.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/Results/BlockScriptExecutionResult.cs index 9c48436..cfe0251 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/Results/BlockScriptExecutionResult.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/Results/BlockScriptExecutionResult.cs @@ -36,4 +36,10 @@ public class BlockScriptExecutionResult /// Execution time in milliseconds /// public long ExecutionTimeMs { get; set; } + + /// + /// Debug mapping: CFG statementId → Blueprint nodeId, for highlighting during debug. + /// Populated when executing with a debugger attached. + /// + public Dictionary? DebugNodeMapping { get; set; } } \ No newline at end of file diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/Statements/BlockStatement.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/Statements/BlockStatement.cs index 177a147..57c1e35 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/Statements/BlockStatement.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/Statements/BlockStatement.cs @@ -5,6 +5,13 @@ namespace KitX.Core.Contract.Workflow; /// public abstract class BlockStatement { + /// + /// Debug statement ID — preserved through BP⇄BS round-trip. + /// Set by CFG→BS conversion; consumed by BS→CFG conversion. + /// Empty means "unset; generate a new ID". + /// + public string StatementId { get; set; } = string.Empty; + /// /// Line number in source /// From 51da1348a5cc9455adb94dd38a684ba41bdf63e2 Mon Sep 17 00:00:00 2001 From: StarInk Date: Mon, 18 May 2026 05:29:06 +0200 Subject: [PATCH 51/60] =?UTF-8?q?=F0=9F=A7=A9Refactor:=20=E7=A7=BB?= =?UTF-8?q?=E9=99=A4=E4=B8=8D=E5=86=8D=E4=BD=BF=E7=94=A8=E7=9A=84=E8=8A=82?= =?UTF-8?q?=E7=82=B9=E7=B1=BB=E5=9E=8B=EF=BC=88BranchNode=E3=80=81BreakNod?= =?UTF-8?q?e=E3=80=81GetNode=E3=80=81LoopNode=E3=80=81PauseNode=E3=80=81Pr?= =?UTF-8?q?intNode=E3=80=81SetNode=EF=BC=89=E4=BB=A5=E7=AE=80=E5=8C=96?= =?UTF-8?q?=E8=93=9D=E5=9B=BE=E8=8A=82=E7=82=B9=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Workflow/Nodes/BlueprintNode.cs | 7 ---- .../Workflow/Nodes/BlueprintNodeType.cs | 39 +------------------ .../Workflow/Nodes/BranchNode.cs | 27 ------------- .../Workflow/Nodes/BreakNode.cs | 21 ---------- .../Workflow/Nodes/GetNode.cs | 31 --------------- .../Workflow/Nodes/LoopNode.cs | 27 ------------- .../Workflow/Nodes/PauseNode.cs | 24 ------------ .../Workflow/Nodes/PrintNode.cs | 24 ------------ .../Workflow/Nodes/SetNode.cs | 31 --------------- 9 files changed, 2 insertions(+), 229 deletions(-) delete mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/BranchNode.cs delete mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/BreakNode.cs delete mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/GetNode.cs delete mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/LoopNode.cs delete mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/PauseNode.cs delete mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/PrintNode.cs delete mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/SetNode.cs diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/BlueprintNode.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/BlueprintNode.cs index 64df1ed..34ee425 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/BlueprintNode.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/BlueprintNode.cs @@ -12,16 +12,9 @@ namespace KitX.Core.Contract.Workflow; /// [JsonPolymorphic(UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] [JsonDerivedType(typeof(EntryNode), "Entry")] -[JsonDerivedType(typeof(BranchNode), "Branch")] -[JsonDerivedType(typeof(LoopNode), "Loop")] -[JsonDerivedType(typeof(BreakNode), "Break")] [JsonDerivedType(typeof(ConstNode), "Const")] [JsonDerivedType(typeof(CallNode), "Call")] [JsonDerivedType(typeof(CallHelperNode), "CallHelper")] -[JsonDerivedType(typeof(GetNode), "Get")] -[JsonDerivedType(typeof(SetNode), "Set")] -[JsonDerivedType(typeof(PrintNode), "Print")] -[JsonDerivedType(typeof(PauseNode), "Pause")] [JsonDerivedType(typeof(VariableNode), "Variable")] [JsonDerivedType(typeof(BuiltinFunctionNode), "BuiltinFunction")] [JsonDerivedType(typeof(PluginTriggerNode), "PluginTrigger")] diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/BlueprintNodeType.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/BlueprintNodeType.cs index fda9986..5b2f1d9 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/BlueprintNodeType.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/BlueprintNodeType.cs @@ -16,21 +16,6 @@ public enum BlueprintNodeType /// PluginTrigger, - /// - /// Conditional branch node - /// - Branch, - - /// - /// Loop node - /// - Loop, - - /// - /// Break from loop node - /// - Break, - /// /// Constant value node /// @@ -46,26 +31,6 @@ public enum BlueprintNodeType /// CallHelper, - /// - /// Get variable value node - reads a PubVar - /// - Get, - - /// - /// Set variable value node - writes to a PubVar - /// - Set, - - /// - /// Print output node - /// - Print, - - /// - /// Pause execution node - /// - Pause, - /// /// Variable declaration node (ConstBlock variables without initial values). /// A floating node with no ports — users can only change the data type. @@ -74,7 +39,7 @@ public enum BlueprintNodeType /// /// 通用内置函数节点。通过 BuiltinFunctionNode.FunctionName 区分具体函数。 - /// 新的内置函数统一使用此类型,无需为每个函数创建专用 enum 值。 + /// 所有内置函数统一使用此类型,无需为每个函数创建专用 enum 值。 /// BuiltinFunction -} \ No newline at end of file +} diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/BranchNode.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/BranchNode.cs deleted file mode 100644 index 495dcf0..0000000 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/BranchNode.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace KitX.Core.Contract.Workflow; - -/// -/// Branch node - conditional execution -/// -public class BranchNode : BlueprintNode -{ - public BranchNode() - { - NodeType = BlueprintNodeType.Branch; - Name = "Branch"; - InitializePinsFromDescriptor(); - } - - public override NodeDescriptor GetDescriptor() => new( - Width: 120, Height: 80, - InputPins: [ - new PinDescriptor("Exec", PinType.Execution, 30), - new PinDescriptor("Condition", PinType.Boolean, 50) - ], - OutputPins: [ - new PinDescriptor("True", PinType.Execution, 30), - new PinDescriptor("False", PinType.Execution, 50) - ], - DisplayName: "Branch" - ); -} \ No newline at end of file diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/BreakNode.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/BreakNode.cs deleted file mode 100644 index 97db11e..0000000 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/BreakNode.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace KitX.Core.Contract.Workflow; - -/// -/// Break node - exit loop -/// -public class BreakNode : BlueprintNode -{ - public BreakNode() - { - NodeType = BlueprintNodeType.Break; - Name = "Break"; - InitializePinsFromDescriptor(); - } - - public override NodeDescriptor GetDescriptor() => new( - Width: 100, Height: 40, - InputPins: [new PinDescriptor("Exec", PinType.Execution, 20)], - OutputPins: [], - DisplayName: "Break" - ); -} \ No newline at end of file diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/GetNode.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/GetNode.cs deleted file mode 100644 index f53c857..0000000 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/GetNode.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace KitX.Core.Contract.Workflow; - -/// -/// Get variable value node - reads a PubVar -/// -public class GetNode : BlueprintNode -{ - /// - /// Variable name to read - /// - public string VarName { get; set; } = string.Empty; - - public GetNode() - { - NodeType = BlueprintNodeType.Get; - Name = "Get"; - InitializePinsFromDescriptor(); - } - - public override NodeDescriptor GetDescriptor() => new( - Width: 120, Height: 60, - InputPins: [new PinDescriptor("Exec", PinType.Execution, 20)], - OutputPins: [ - new PinDescriptor("Exec", PinType.Execution, 20), - new PinDescriptor("Value", PinType.Any, 40) - ], - DisplayName: "Get" - ); - - public override string GetDisplayTitle() => $"Get: {VarName}"; -} \ No newline at end of file diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/LoopNode.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/LoopNode.cs deleted file mode 100644 index bffe2b5..0000000 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/LoopNode.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace KitX.Core.Contract.Workflow; - -/// -/// Loop node - iterative execution -/// -public class LoopNode : BlueprintNode -{ - public LoopNode() - { - NodeType = BlueprintNodeType.Loop; - Name = "Loop"; - InitializePinsFromDescriptor(); - } - - public override NodeDescriptor GetDescriptor() => new( - Width: 120, Height: 80, - InputPins: [ - new PinDescriptor("Exec", PinType.Execution, 30), - new PinDescriptor("Condition", PinType.Boolean, 50) - ], - OutputPins: [ - new PinDescriptor("LoopBody", PinType.Execution, 30), - new PinDescriptor("LoopEnd", PinType.Execution, 50) - ], - DisplayName: "Loop" - ); -} \ No newline at end of file diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/PauseNode.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/PauseNode.cs deleted file mode 100644 index 2b0b279..0000000 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/PauseNode.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace KitX.Core.Contract.Workflow; - -/// -/// Pause node - pause execution -/// -public class PauseNode : BlueprintNode -{ - public PauseNode() - { - NodeType = BlueprintNodeType.Pause; - Name = "Pause"; - InitializePinsFromDescriptor(); - } - - public override NodeDescriptor GetDescriptor() => new( - Width: 100, Height: 50, - InputPins: [ - new PinDescriptor("Exec", PinType.Execution, 20), - new PinDescriptor("Milliseconds", PinType.Integer, 35) - ], - OutputPins: [new PinDescriptor("Exec", PinType.Execution, 25)], - DisplayName: "Pause" - ); -} \ No newline at end of file diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/PrintNode.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/PrintNode.cs deleted file mode 100644 index 799a606..0000000 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/PrintNode.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace KitX.Core.Contract.Workflow; - -/// -/// Print node - output information -/// -public class PrintNode : BlueprintNode -{ - public PrintNode() - { - NodeType = BlueprintNodeType.Print; - Name = "Print"; - InitializePinsFromDescriptor(); - } - - public override NodeDescriptor GetDescriptor() => new( - Width: 100, Height: 50, - InputPins: [ - new PinDescriptor("Exec", PinType.Execution, 20), - new PinDescriptor("Value", PinType.Any, 35) - ], - OutputPins: [new PinDescriptor("Exec", PinType.Execution, 25)], - DisplayName: "Print" - ); -} \ No newline at end of file diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/SetNode.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/SetNode.cs deleted file mode 100644 index f20941c..0000000 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/SetNode.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace KitX.Core.Contract.Workflow; - -/// -/// Set variable value node - writes to a PubVar -/// -public class SetNode : BlueprintNode -{ - /// - /// Variable name to write - /// - public string VarName { get; set; } = string.Empty; - - public SetNode() - { - NodeType = BlueprintNodeType.Set; - Name = "Set"; - InitializePinsFromDescriptor(); - } - - public override NodeDescriptor GetDescriptor() => new( - Width: 120, Height: 60, - InputPins: [ - new PinDescriptor("Exec", PinType.Execution, 20), - new PinDescriptor("Value", PinType.Any, 40) - ], - OutputPins: [new PinDescriptor("Exec", PinType.Execution, 20)], - DisplayName: "Set" - ); - - public override string GetDisplayTitle() => $"Set: {VarName}"; -} \ No newline at end of file From 6d12db2687a6c2d07728dbe44702fd12cfec39ee Mon Sep 17 00:00:00 2001 From: StarInk Date: Mon, 15 Jun 2026 21:30:59 +0200 Subject: [PATCH 52/60] =?UTF-8?q?=F0=9F=A7=A9=20Refactor(Workflow):=20?= =?UTF-8?q?=E7=98=A6=E8=BA=AB=20Core.Contract=20=E8=87=B3=20Dashboard=20?= =?UTF-8?q?=E5=85=AC=E5=85=B1=E5=A5=91=E7=BA=A6=E9=9D=A2-=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=20ITriggerManager/IRealPluginManagerBridge/WorkflowEv?= =?UTF-8?q?entNames=E3=80=81IDeviceHttpClient=20=E6=8F=90=E5=8D=87?= =?UTF-8?q?=E8=87=B3=20Contract=E3=80=81=E6=8B=86=E5=88=86=20IBlockScriptS?= =?UTF-8?q?ervice=20=E7=A7=BB=E9=99=A4=E5=86=85=E9=83=A8=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E4=BE=9D=E8=B5=96=E3=80=81=E8=BF=81=E7=A7=BB=2012=20=E4=B8=AA?= =?UTF-8?q?=E5=86=85=E9=83=A8=E6=95=B0=E6=8D=AE=E6=A8=A1=E5=9E=8B=E4=B8=8E?= =?UTF-8?q?=209=20=E4=B8=AA=E5=86=85=E9=83=A8=E6=8E=A5=E5=8F=A3=E8=87=B3?= =?UTF-8?q?=20KitX.Workflow=20=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Device/IDeviceService.cs | 25 ++++ .../Event/WorkflowEventNames.cs | 21 +++ .../Workflow/BlockDefinition.cs | 45 ------- .../Workflow/BlockScript.cs | 93 ------------- .../Workflow/BlockScriptModels.cs | 32 ----- .../Workflow/FlowControlType.cs | 32 ----- .../Workflow/IBlockScriptParser.cs | 125 ------------------ .../Workflow/IBlueprintConverter.cs | 48 +------ .../Workflow/ILayoutService.cs | 9 -- .../Workflow/INodeExportStrategy.cs | 84 ------------ .../Workflow/IPluginManager.cs | 39 ------ .../Workflow/IRealPluginManagerBridge.cs | 17 +++ .../Workflow/ITriggerManager.cs | 34 +++++ .../Workflow/IWorkflowService.cs | 34 +---- .../Workflow/PluginCallInfo.cs | 59 --------- .../Workflow/Results/BlockExecutionResult.cs | 48 ------- .../Results/BlockScriptParseResult.cs | 27 ---- .../Workflow/Statements/BlockStatement.cs | 24 ---- .../Statements/ExpressionStatement.cs | 12 -- .../Statements/FlowControlStatement.cs | 51 ------- .../VariableDeclarationStatement.cs | 12 -- .../Workflow/VariableDeclaration.cs | 27 ---- 22 files changed, 110 insertions(+), 788 deletions(-) create mode 100644 KitX Core Contracts/KitX.Core.Contract/Event/WorkflowEventNames.cs delete mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/BlockDefinition.cs delete mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/BlockScript.cs delete mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/BlockScriptModels.cs delete mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/FlowControlType.cs delete mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/IBlockScriptParser.cs delete mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/ILayoutService.cs delete mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/INodeExportStrategy.cs delete mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/IPluginManager.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/IRealPluginManagerBridge.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/ITriggerManager.cs delete mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/PluginCallInfo.cs delete mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/Results/BlockExecutionResult.cs delete mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/Results/BlockScriptParseResult.cs delete mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/Statements/BlockStatement.cs delete mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/Statements/ExpressionStatement.cs delete mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/Statements/FlowControlStatement.cs delete mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/Statements/VariableDeclarationStatement.cs delete mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/VariableDeclaration.cs diff --git a/KitX Core Contracts/KitX.Core.Contract/Device/IDeviceService.cs b/KitX Core Contracts/KitX.Core.Contract/Device/IDeviceService.cs index cd709c2..790c82c 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Device/IDeviceService.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Device/IDeviceService.cs @@ -1,5 +1,7 @@ using System; using System.ComponentModel; +using System.Net.Http; +using System.Threading; using System.Threading.Tasks; using System.Collections.Generic; using KitX.Shared.CSharp.Device; @@ -238,3 +240,26 @@ public class MainDeviceChangedEventArgs : EventArgs /// public string NewMainDeviceId { get; set; } = string.Empty; } + +/// +/// Device HTTP client interface — sends requests to remote DevicesServer instances. +/// Used for cross-device plugin invocation via the /Api/V1/Plugin/Invoke endpoint. +/// (Moved to Contract so the Workflow library can depend on the abstraction without +/// referencing KitX.Core.) +/// +public interface IDeviceHttpClient +{ + /// + /// Invokes a plugin method on a remote device via HTTP POST to /Api/V1/Plugin/Invoke. + /// + /// Target device info (contains IPv4 and DevicesServerPort) + /// Valid session token for the target device + /// The Request object to send + /// Cancellation token + /// HTTP response from remote device, or null on network error + Task InvokePluginAsync( + DeviceInfo targetDevice, + string token, + KitX.Shared.CSharp.WebCommand.Request request, + CancellationToken ct = default); +} diff --git a/KitX Core Contracts/KitX.Core.Contract/Event/WorkflowEventNames.cs b/KitX Core Contracts/KitX.Core.Contract/Event/WorkflowEventNames.cs new file mode 100644 index 0000000..b7985bb --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Event/WorkflowEventNames.cs @@ -0,0 +1,21 @@ +namespace KitX.Core.Contract.Event; + +/// +/// Event-bus channel names shared between the Workflow library and its host +/// (KitX.Core / KitX.Dashboard). Centralized here so the workflow library can +/// publish/subscribe on the bus without referencing +/// KitX.Core's own EventNames table. +/// +public static class WorkflowEventNames +{ + /// + /// Plugin response event (carries a RequestId so callers can correlate + /// a fire-and-forget plugin invocation with its reply). + /// + public const string PluginResponse = "PluginResponse"; + + /// + /// Workflow execution result event (success or failure of a workflow run). + /// + public const string WorkflowExecutionResult = "WorkflowExecutionResult"; +} diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/BlockDefinition.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/BlockDefinition.cs deleted file mode 100644 index 9dff4d6..0000000 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/BlockDefinition.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.Collections.Generic; - -namespace KitX.Core.Contract.Workflow; - -/// -/// Represents a single block definition in the script -/// -public class BlockDefinition -{ - /// - /// Block type - /// - public BlockType Type { get; set; } - - /// - /// Block name (for NamedBlock) - /// - public string Name { get; set; } = string.Empty; - - /// - /// Variable declarations in this block - /// - public List Variables { get; set; } = []; - - /// - /// Statements in this block (excluding Loop statements, which are separated) - /// - public List Statements { get; set; } = []; - - /// - /// Line number in source where this block starts - /// - public int LineNumber { get; set; } - - /// - /// Name of the next block to execute when this block ends naturally - /// (i.e., not ended by Branch/Loop/ToLoopCond) - /// - public string? NextBlockName { get; set; } - - /// - /// For LoopBlock: the block name containing this loop (i.e., the parent block) - /// - public string? ParentBlockName { get; set; } -} \ No newline at end of file diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/BlockScript.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/BlockScript.cs deleted file mode 100644 index 919ab65..0000000 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/BlockScript.cs +++ /dev/null @@ -1,93 +0,0 @@ -using System.Collections.Generic; - -namespace KitX.Core.Contract.Workflow; - -/// -/// Parsed block script container -/// -public class BlockScript -{ - /// - /// Global constants block (ConstBlock) - /// - public BlockDefinition? ConstBlock { get; set; } - - /// - /// Public variables block (PubVarBlock) - optional - /// - public BlockDefinition? PubVarBlock { get; set; } - - /// - /// Main entry block (MainBlock) - /// - public BlockDefinition? MainBlock { get; set; } - - /// - /// Named blocks dictionary by name - /// - public Dictionary NamedBlocks { get; set; } = []; - - /// - /// All blocks in order of appearance - /// - public List AllBlocks { get; set; } = []; - - /// - /// Loop blocks dictionary by parent block name - /// (e.g., "MainBlock" -> LoopBlock for MainBlock's Loop statement) - /// - public Dictionary LoopBlocks { get; set; } = []; - - /// - /// Raw source code (parsed input, may not include helper functions) - /// - public string SourceCode { get; set; } = string.Empty; - - /// - /// Full source code including merged helper functions (for execution) - /// - public string FullSourceCode { get; set; } = string.Empty; - - /// - /// Debug mapping from CFG statement IDs to Blueprint node IDs. - /// Populated during BP→BS conversion for use by the debug execution pipeline. - /// - public Dictionary? DebugNodeMapping { get; set; } - - /// - /// Helper functions to be made available in script execution context - /// - public List HelperFunctions { get; set; } = []; - - - /// - /// Gets a block by name (checks LoopBlocks first by block name, then NamedBlocks, then standard blocks) - /// IMPORTANT: LoopBlocks are checked FIRST because LoopBlock names (like "LoopBody") should NOT be - /// shadowed by user-defined NamedBlocks with the same name. - /// - public BlockDefinition? GetBlockByName(string name) - { - // First check LoopBlocks by the block's own name (not parent block name) - // This is critical because LoopBlocks are created for "NextBlock = Loop(...)" statements - // and their names (like "LoopBody") should take precedence over user-defined blocks - foreach (var kvp in LoopBlocks) - { - if (kvp.Value.Name == name) - return kvp.Value; - } - - // Then check NamedBlocks (user-defined blocks) - if (NamedBlocks.TryGetValue(name, out var namedBlock)) - return namedBlock; - - // Then check the standard blocks by name match - if (MainBlock?.Name == name) - return MainBlock; - if (ConstBlock?.Name == name) - return ConstBlock; - if (PubVarBlock?.Name == name) - return PubVarBlock; - - return null; - } -} \ No newline at end of file diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/BlockScriptModels.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/BlockScriptModels.cs deleted file mode 100644 index 860c8a2..0000000 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/BlockScriptModels.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace KitX.Core.Contract.Workflow; - -/// -/// Block type enumeration -/// -public enum BlockType -{ - /// - /// Constants block - variables are globally scoped and read-only - /// - ConstBlock, - - /// - /// Main block - entry point, local scope - /// - MainBlock, - - /// - /// Named block - local scope, can be called by name - /// - NamedBlock, - - /// - /// Public variable block - globally scoped and writable, but not exposed in UI editor - /// - PubVarBlock, - - /// - /// Loop block - auto-generated block containing a Loop statement - /// - LoopBlock -} \ No newline at end of file diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/FlowControlType.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/FlowControlType.cs deleted file mode 100644 index 8cfdd0d..0000000 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/FlowControlType.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace KitX.Core.Contract.Workflow; - -/// -/// Flow control statement types -/// -public enum FlowControlType -{ - /// - /// Branch to another block based on condition - /// - Branch, - - /// - /// Loop while condition is true (Loop has three args: condition, trueBlock, falseBlock) - /// - Loop, - - /// - /// Return from script execution - /// - Return, - - /// - /// Break from current loop - /// - Break, - - /// - /// To loop condition - marks the end of a loop body and returns to loop condition - /// - ToLoopCond -} \ No newline at end of file diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/IBlockScriptParser.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/IBlockScriptParser.cs deleted file mode 100644 index b911579..0000000 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/IBlockScriptParser.cs +++ /dev/null @@ -1,125 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -namespace KitX.Core.Contract.Workflow; - -/// -/// Block script parser interface - parses C# scripts with block attributes -/// -public interface IBlockScriptParser -{ - /// - /// Parses a block-based script from source code - /// - /// The C# source code with block attributes - /// Parsed block script result - BlockScriptParseResult Parse(string sourceCode); - - /// - /// Parses a block-based script from source code asynchronously - /// - Task ParseAsync(string sourceCode); - - /// - /// Validates block script syntax and structure - /// - BlockScriptValidationResult Validate(string sourceCode); -} - -/// -/// Block script executor interface - executes parsed block scripts -/// -public interface IBlockScriptExecutor -{ - /// - /// Executes a block script - /// - /// The parsed block script - /// Input parameters - /// Cancellation token - /// Execution result - Task ExecuteAsync( - BlockScript script, - Dictionary? parameters = null, - CancellationToken cancellationToken = default); - - /// - /// Validates a block script - /// - BlockScriptValidationResult Validate(BlockScript script); - - /// - /// Sets an optional debug controller for interactive execution (breakpoints, step, slow). - /// Pass null to disable debug mode. - /// - void SetDebugger(IBlueprintDebugController? debugger); -} - -/// -/// Block scope manager interface - manages variable scoping -/// -public interface IBlockScopeManager -{ - /// - /// Gets the global (ConstBlock) scope - /// - IBlockScope GlobalScope { get; } - - /// - /// Resolves a variable name to its value (searches local then global) - /// - object? ResolveVariable(string name); - - /// - /// Sets a variable value in the appropriate scope - /// - void SetVariable(string name, object? value, bool global = false); - - /// - /// Checks if a variable exists in any scope - /// - bool HasVariable(string name); - - /// - /// Clears all local scopes (called between executions) - /// - void ClearLocalScopes(); -} - -/// -/// Variable scope interface -/// -public interface IBlockScope -{ - /// - /// Name of the block this scope belongs to - /// - string BlockName { get; } - - /// - /// Whether this is the global scope - /// - bool IsGlobal { get; } - - /// - /// Gets a variable value - /// - object? GetVariable(string name); - - /// - /// Sets a variable value - /// - void SetVariable(string name, object? value); - - /// - /// Checks if a variable exists in this scope - /// - bool HasVariable(string name); - - /// - /// Gets all variables in this scope - /// - Dictionary GetAllVariables(); -} diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/IBlueprintConverter.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/IBlueprintConverter.cs index b7a46d0..9107a75 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/IBlueprintConverter.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/IBlueprintConverter.cs @@ -4,48 +4,12 @@ namespace KitX.Core.Contract.Workflow; /// -/// Interface for converting BlockScript to Blueprint -/// -public interface IBlockScriptToBlueprintConverter -{ - /// - /// Convert BlockScript source code to Blueprint - /// - /// BlockScript source code - /// Helper functions available - /// Converted Blueprint - Blueprint Convert(string sourceCode, List? helperFunctions = null); - - /// - /// Convert parsed BlockScript to Blueprint - /// - /// Parsed BlockScript - /// Converted Blueprint - Blueprint Convert(BlockScript script); -} - -/// -/// Interface for converting Blueprint to BlockScript -/// -public interface IBlueprintToBlockScriptConverter -{ - /// - /// Convert Blueprint to BlockScript source code - /// - /// Blueprint to convert - /// BlockScript source code - string Convert(Blueprint blueprint); - - /// - /// Convert Blueprint to parsed BlockScript - /// - /// Blueprint to convert - /// Parsed BlockScript - BlockScript ConvertToBlockScript(Blueprint blueprint); -} - -/// -/// Interface for Blueprint service +/// Interface for Blueprint service. +/// +/// Kept in the public Contract surface (Dashboard consumes it). The lower-level +/// converters (IBlockScriptToBlueprintConverter, +/// IBlueprintToBlockScriptConverter) are internal to the workflow pipeline +/// and live in the KitX.Workflow library. /// public interface IBlueprintService { diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/ILayoutService.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/ILayoutService.cs deleted file mode 100644 index c146bbe..0000000 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/ILayoutService.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace KitX.Core.Contract.Workflow; - -/// -/// Interface for layout service -/// -public interface ILayoutService -{ - void LayoutNodes(Blueprint blueprint); -} diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/INodeExportStrategy.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/INodeExportStrategy.cs deleted file mode 100644 index 918a998..0000000 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/INodeExportStrategy.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System.Collections.Generic; - -namespace KitX.Core.Contract.Workflow; - -/// -/// Helper service providing data resolution utilities for node export strategies. -/// Implemented by BlueprintToBlockScriptConverter. -/// -public interface INodeExportHelper -{ - /// -/// Resolves the input value for a pin by tracing data connections. -/// Returns ConstName for const references, PubVarName for pubvar references, -/// or the pin's default value. -/// - string GetInputValue(BlueprintNode node, string pinName); - - /// -/// Resolves all non-Exec input arguments for a node, returning them as a comma-separated string. -/// - string GetInputArgs(BlueprintNode node); - - /// - /// The blueprint being converted. - /// - Blueprint Blueprint { get; } - - /// - /// Returns the PubVar name assigned to the given output pin, or null if no data connection exists. - /// - string? GetOutputPubVar(BlueprintNode node, string pinName); - - /// - /// Returns true if the given output pin is consumed by at least one data connection. - /// - bool IsOutputConsumed(BlueprintNode node, string pinName); -} - -/// -/// Strategy for converting a specific node type to a BlockScript statement. -/// Each node type that participates in reverse conversion provides an implementation, -/// eliminating the need for switch-based dispatch in the converter. -/// -public interface INodeExportStrategy -{ - /// -/// The node type this strategy handles. -/// - BlueprintNodeType NodeType { get; } - - /// -/// Whether this node type represents a control flow construct (Branch, Loop, etc.). -/// Used by the converter to determine main flow termination and sub-graph processing. -/// - bool IsControlFlow { get; } - - /// -/// Converts the node to a BlockScript statement, or null if the node should be skipped. -/// - BlockStatement? ToStatement(BlueprintNode node, INodeExportHelper helper); - - /// -/// For control flow nodes: returns the output arm configuration. -/// Each arm defines an output pin name and whether it represents a loopback. -/// Non-control-flow strategies return an empty collection. -/// - IEnumerable GetOutputArms(BlueprintNode node); -} - -/// -/// Describes a single output arm of a control flow node (e.g., Branch's True/False, Loop's LoopBody/LoopEnd). -/// -public struct OutputArmDescriptor -{ - /// -/// The output pin name (e.g., Pins.True, Pins.False, Pins.LoopBody, Pins.LoopEnd) -/// - public string PinName { get; set; } - - /// -/// Whether this arm loops back to a parent node (LoopBody loops back to the Loop node) -/// - public bool IsLoopback { get; set; } -} diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/IPluginManager.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/IPluginManager.cs deleted file mode 100644 index 9b4c62c..0000000 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/IPluginManager.cs +++ /dev/null @@ -1,39 +0,0 @@ -namespace KitX.Core.Contract.Workflow; - -/// -/// 插件管理器接口 - BlockScripting 使用此接口调用插件方法。 -/// Copy of Kscript.CSharp.Parser.Core.IPluginManager for BlockScripting use -/// without depending on the KCS parser assembly. -/// 实际实现由外部项目(RealPluginManager)提供。 -/// -public interface IPluginManager -{ - /// - /// 调用插件方法 - /// - /// 返回值类型 - /// 调用信息 - /// 插件方法的返回值 - T Call(PluginCallInfo callInfo); - - /// - /// 调用插件方法(无返回值) - /// - /// 调用信息 - void Call(PluginCallInfo callInfo); - - /// - /// 检查插件是否存在 - /// - /// 插件名称 - /// 插件是否存在 - bool IsPluginExists(string pluginName); - - /// - /// 检查插件方法是否存在 - /// - /// 插件名称 - /// 方法名称 - /// 方法是否存在 - bool IsMethodExists(string pluginName, string methodName); -} \ No newline at end of file diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/IRealPluginManagerBridge.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/IRealPluginManagerBridge.cs new file mode 100644 index 0000000..1b2c90e --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/IRealPluginManagerBridge.cs @@ -0,0 +1,17 @@ +namespace KitX.Core.Contract.Workflow; + +/// +/// Marker/bridge interface for the concrete plugin manager implementation +/// used by the workflow subsystem. +/// +/// Dashboard resolves this interface once at startup (via +/// GetRequiredService<IRealPluginManagerBridge>()) purely to force eager +/// singleton construction — the concrete RealPluginManager subscribes to plugin +/// message events in its constructor, so the instance must exist before plugins +/// connect. The interface itself carries no members; callers that need to +/// invoke plugin methods depend on the workflow library's own IPluginManager +/// contract instead. +/// +public interface IRealPluginManagerBridge +{ +} diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/ITriggerManager.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/ITriggerManager.cs new file mode 100644 index 0000000..2431557 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/ITriggerManager.cs @@ -0,0 +1,34 @@ +namespace KitX.Core.Contract.Workflow; + +/// +/// Routes plugin trigger signals to the workflows that subscribed to them. +/// +/// A trigger is a pure signal (equivalent to pressing the "Run" button); it carries +/// no business payload. Plugins fire triggers via the TriggerFired command; the +/// matches the firing plugin/trigger against registered +/// subscriptions and runs each matching workflow. +/// +public interface ITriggerManager +{ + /// + /// Registers a workflow's trigger configuration so that matching + /// TriggerFired events from plugins will run the workflow. + /// Only PluginEvent triggers with a non-empty plugin name take effect. + /// + /// The workflow identifier. + /// The trigger configuration. + void RegisterWorkflowTrigger(string workflowId, TriggerConfig config); + + /// + /// Removes the trigger subscription for a workflow (e.g. on delete/rename). + /// + /// The workflow identifier. + void UnregisterWorkflowTrigger(string workflowId); + + /// + /// Scans persisted workflow files and re-subscribes all configured triggers. + /// Called once after DI initialization completes, before plugins connect, so + /// that TriggerFired events arriving early can still be routed. + /// + void InitializeFromPersistedWorkflows(); +} diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowService.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowService.cs index 192b49b..9bfa7cb 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowService.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowService.cs @@ -75,20 +75,16 @@ public interface IWorkflowPluginService } /// -/// Block script service interface +/// Block script service interface — the public surface consumed by Dashboard. +/// +/// Only methods that take primitive/source parameters (string, List<HelperFunction>, ...) +/// or return Dashboard-visible result types are kept here. Pipeline-internal methods that +/// deal in the parsed BlockScript / BlockScriptParseResult models live on the +/// workflow library's internal IBlockScriptPipelineService instead, so that those +/// internal models need not be exposed through Contract. /// public interface IBlockScriptService { - /// - /// Parses a block script - /// - BlockScriptParseResult ParseBlockScript(string sourceCode); - - /// - /// Parses a block script asynchronously - /// - Task ParseBlockScriptAsync(string sourceCode); - /// /// Validates a block script /// @@ -100,14 +96,6 @@ public interface IBlockScriptService /// List ParseConstantsFromBlockScript(string sourceCode); - /// - /// Executes a block script - /// - Task ExecuteBlockScriptAsync( - BlockScript script, - Dictionary? parameters = null, - System.Threading.CancellationToken cancellationToken = default); - /// /// Executes a block script from source code /// @@ -134,14 +122,6 @@ Task ExecuteBlockScriptAsync( Dictionary? constantOverrides, System.Threading.CancellationToken cancellationToken = default); - /// - /// Compiles a BlockScript and persists the compiled assembly to disk. - /// - /// The parsed BlockScript to compile. - /// The workflow ID for assembly naming. - /// True if compilation and persistence succeeded. - Task CompileAndPersistAsync(BlockScript script, string workflowId); - /// /// Preloads all persisted compiled scripts for a workflow from disk. /// diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/PluginCallInfo.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/PluginCallInfo.cs deleted file mode 100644 index 3f19863..0000000 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/PluginCallInfo.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; - -namespace KitX.Core.Contract.Workflow; - -/// -/// 插件调用信息,用于传递给 IPluginManager.Call 的参数。 -/// Copy of Kscript.CSharp.Parser.Models.PluginCallInfo for BlockScripting use -/// without depending on the KCS parser assembly. -/// -public class PluginCallInfo -{ - /// - /// 插件名称 - /// - public string PluginName { get; set; } = string.Empty; - - /// - /// 方法名称 - /// - public string MethodName { get; set; } = string.Empty; - - /// - /// 方法参数值数组 - /// - public object[] Parameters { get; set; } = Array.Empty(); - - /// - /// 参数类型数组 - /// - public Type[] ParameterTypes { get; set; } = Array.Empty(); - - /// - /// 参数名称数组 - /// - public string[] ParameterNames { get; set; } = Array.Empty(); - - /// - /// 目标设备名称(远程调用时使用)。如果为空或 null,则为本地调用。 - /// - public string? TargetDevice { get; set; } - - public PluginCallInfo() - { - } - - public PluginCallInfo(string pluginName, string methodName, object[] parameters, Type[] parameterTypes, string[] parameterNames) - { - PluginName = pluginName; - MethodName = methodName; - Parameters = parameters; - ParameterTypes = parameterTypes; - ParameterNames = parameterNames; - } - - public override string ToString() - { - return $"{PluginName}.{MethodName}({string.Join(", ", Parameters)})"; - } -} \ No newline at end of file diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/Results/BlockExecutionResult.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/Results/BlockExecutionResult.cs deleted file mode 100644 index 056f78d..0000000 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/Results/BlockExecutionResult.cs +++ /dev/null @@ -1,48 +0,0 @@ -namespace KitX.Core.Contract.Workflow; - -/// -/// Result of executing a block - used by state machine for flow control -/// -public class BlockExecutionResult -{ - /// - /// Whether execution should continue to next block - /// - public bool ShouldContinue { get; set; } = true; - - /// - /// Name of the next block to execute (null means end of script) - /// - public string? NextBlockName { get; set; } - - /// - /// Whether this is a return (end of entire script) - /// - public bool IsReturn { get; set; } - - /// - /// Return value if IsReturn is true - /// - public object? ReturnValue { get; set; } - - /// - /// Create a result for continuing to next block - /// - public static BlockExecutionResult ContinueTo(string? nextBlockName) => new() - { - ShouldContinue = true, - NextBlockName = nextBlockName, - IsReturn = false - }; - - /// - /// Create a result for end of script - /// - public static BlockExecutionResult Return(object? value = null) => new() - { - ShouldContinue = false, - NextBlockName = null, - IsReturn = true, - ReturnValue = value - }; -} \ No newline at end of file diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/Results/BlockScriptParseResult.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/Results/BlockScriptParseResult.cs deleted file mode 100644 index 594eee5..0000000 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/Results/BlockScriptParseResult.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace KitX.Core.Contract.Workflow; - -/// -/// Result of parsing operation -/// -public class BlockScriptParseResult -{ - /// - /// Whether parsing was successful - /// - public bool IsSuccess { get; set; } - - /// - /// Error message if parsing failed - /// - public string? ErrorMessage { get; set; } - - /// - /// Line number where error occurred - /// - public int ErrorLine { get; set; } - - /// - /// The parsed script if successful - /// - public BlockScript? Script { get; set; } -} \ No newline at end of file diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/Statements/BlockStatement.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/Statements/BlockStatement.cs deleted file mode 100644 index 57c1e35..0000000 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/Statements/BlockStatement.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace KitX.Core.Contract.Workflow; - -/// -/// Base class for statements within a block -/// -public abstract class BlockStatement -{ - /// - /// Debug statement ID — preserved through BP⇄BS round-trip. - /// Set by CFG→BS conversion; consumed by BS→CFG conversion. - /// Empty means "unset; generate a new ID". - /// - public string StatementId { get; set; } = string.Empty; - - /// - /// Line number in source - /// - public int LineNumber { get; set; } - - /// - /// Original source code for this statement - /// - public string SourceCode { get; set; } = string.Empty; -} \ No newline at end of file diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/Statements/ExpressionStatement.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/Statements/ExpressionStatement.cs deleted file mode 100644 index 2d32e60..0000000 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/Statements/ExpressionStatement.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace KitX.Core.Contract.Workflow; - -/// -/// Expression statement -/// -public class ExpressionStatement : BlockStatement -{ - /// - /// The expression to execute - /// - public string Expression { get; set; } = string.Empty; -} \ No newline at end of file diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/Statements/FlowControlStatement.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/Statements/FlowControlStatement.cs deleted file mode 100644 index 728e0ca..0000000 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/Statements/FlowControlStatement.cs +++ /dev/null @@ -1,51 +0,0 @@ -namespace KitX.Core.Contract.Workflow; - -/// -/// Flow control statement -/// -public class FlowControlStatement : BlockStatement -{ - /// - /// Type of flow control - /// - public FlowControlType ControlType { get; set; } - - /// - /// Condition expression (for Branch/Loop) - /// - public string ConditionExpression { get; set; } = string.Empty; - - /// - /// Target block name when condition is true (for Branch/Loop) - /// - public string TrueBlockName { get; set; } = string.Empty; - - /// - /// Target block name when condition is false (for Branch/Loop) - /// For Loop: this is the loop exit block - /// - public string FalseBlockName { get; set; } = string.Empty; - - /// - /// For ToLoopCond: the block name containing the Loop statement to return to - /// - public string? ToLoopCondReturnTo { get; set; } - - /// - /// Regenerates SourceCode from current field values. - /// Call after updating TrueBlockName/FalseBlockName/etc. to keep SourceCode in sync. - /// - public void RegenerateSourceCode() - { - SourceCode = ControlType switch - { - FlowControlType.Branch => $"NextBlock = Branch({ConditionExpression}, \"{TrueBlockName}\", \"{FalseBlockName}\");", - FlowControlType.Loop => $"NextBlock = Loop({ConditionExpression}, \"{TrueBlockName}\", \"{FalseBlockName}\");", - FlowControlType.ToLoopCond => ToLoopCondReturnTo != null - ? $"NextBlock = ToLoopCond(\"{ToLoopCondReturnTo}\");" - : "ToLoopCond();", - FlowControlType.Break => "Break();", - _ => SourceCode - }; - } -} \ No newline at end of file diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/Statements/VariableDeclarationStatement.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/Statements/VariableDeclarationStatement.cs deleted file mode 100644 index 4f6c4ef..0000000 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/Statements/VariableDeclarationStatement.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace KitX.Core.Contract.Workflow; - -/// -/// Variable declaration statement -/// -public class VariableDeclarationStatement : BlockStatement -{ - /// - /// The variable declaration - /// - public VariableDeclaration Declaration { get; set; } = new(); -} \ No newline at end of file diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/VariableDeclaration.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/VariableDeclaration.cs deleted file mode 100644 index 492007a..0000000 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/VariableDeclaration.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace KitX.Core.Contract.Workflow; - -/// -/// Variable declaration -/// -public class VariableDeclaration -{ - /// - /// Variable name - /// - public string Name { get; set; } = string.Empty; - - /// - /// Variable type as string - /// - public string Type { get; set; } = "object"; - - /// - /// Initial value expression as string (for evaluation at parse time or execution time) - /// - public string? InitialValueExpression { get; set; } - - /// - /// Default value (pre-evaluated for const block) - /// - public object? DefaultValue { get; set; } -} \ No newline at end of file From c1671fa964ca435501b5d8bc3e81f3d767978a1d Mon Sep 17 00:00:00 2001 From: StarInk Date: Wed, 17 Jun 2026 20:17:27 +0200 Subject: [PATCH 53/60] =?UTF-8?q?=F0=9F=A7=A9=20Refactor(Contract):=20?= =?UTF-8?q?=E5=88=A0=E9=99=A4=20IWorkflowEditorBridge=20=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 双窗口编辑器架构已被统一的 WorkflowEditorWindow 取代,该跨窗口数据管道接口 (GetCurrentScript/GetHelperFunctions/SetScript/AppendOutput/TriggerExecution) 的唯一实现者(Dashboard 的 WorkflowEditorBridge)与唯一消费者 (BlueprintEditorViewModel 的 bridge 字段)均已随遗留窗口清理删除,接口成为死代码。 --- .../Workflow/IWorkflowEditorBridge.cs | 36 ------------------- 1 file changed, 36 deletions(-) delete mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowEditorBridge.cs diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowEditorBridge.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowEditorBridge.cs deleted file mode 100644 index 98065ce..0000000 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowEditorBridge.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Collections.Generic; - -namespace KitX.Core.Contract.Workflow; - -/// -/// Bridge interface for communication between BlueprintEditor and WorkflowEditor. -/// BlueprintEditor always runs in association with a WorkflowEditor instance. -/// -public interface IWorkflowEditorBridge -{ - /// - /// Gets the current BlockScript source code from the WorkflowEditor's main program area - /// - string? GetCurrentScript(); - - /// - /// Gets the current helper functions list from the WorkflowEditor - /// - List? GetHelperFunctions(); - - /// - /// Writes BlockScript source code back to the WorkflowEditor's main program area - /// and triggers UI update - /// - void SetScript(string sourceCode, List? helpers); - - /// - /// Appends output text to the WorkflowEditor's Output panel - /// - void AppendOutput(string output); - - /// - /// Triggers script execution in the WorkflowEditor - /// - void TriggerExecution(); -} From 7d256fca55ecde81fa115dd16c2fce030c01a803 Mon Sep 17 00:00:00 2001 From: StarInk Date: Fri, 19 Jun 2026 18:47:04 +0200 Subject: [PATCH 54/60] =?UTF-8?q?=F0=9F=92=BE=20Feat(Workflow):=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20RunWorkflowWithDetailsAsync=20=E6=96=B9?= =?UTF-8?q?=E6=B3=95=E4=BB=A5=E8=BF=94=E5=9B=9E=E5=AE=8C=E6=95=B4=E6=89=A7?= =?UTF-8?q?=E8=A1=8C=E7=BB=93=E6=9E=9C=E5=92=8C=20Print()=20=E8=BE=93?= =?UTF-8?q?=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../KitX.Core.Contract/Event/WorkflowEventArgs.cs | 12 +++++++++++- .../Workflow/IWorkflowService.cs | 14 ++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/KitX Core Contracts/KitX.Core.Contract/Event/WorkflowEventArgs.cs b/KitX Core Contracts/KitX.Core.Contract/Event/WorkflowEventArgs.cs index abb8655..cda2807 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Event/WorkflowEventArgs.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Event/WorkflowEventArgs.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace KitX.Core.Contract.Event; @@ -79,10 +80,19 @@ public class WorkflowExecutionResultEventArgs : EventArgs /// public string? ErrorMessage { get; } - public WorkflowExecutionResultEventArgs(string workflowId, bool isSuccess, string? errorMessage = null) + /// + /// Lines of Print() output produced during execution (null if not captured). + /// Surfaced to the Debug activity log so users can see what the workflow printed + /// without opening the editor's output panel. + /// + public IReadOnlyList? Output { get; } + + public WorkflowExecutionResultEventArgs(string workflowId, bool isSuccess, + string? errorMessage = null, IReadOnlyList? output = null) { WorkflowId = workflowId; IsSuccess = isSuccess; ErrorMessage = errorMessage; + Output = output; } } diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowService.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowService.cs index 9bfa7cb..05bf609 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowService.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowService.cs @@ -30,6 +30,12 @@ public interface IWorkflowManagementService /// Task RunWorkflowAsync(string workflowId); + /// + /// Runs a workflow and returns the full execution result including Print() output. + /// Use this when the caller needs the execution output (e.g. the Debug activity log). + /// + Task RunWorkflowWithDetailsAsync(string workflowId); + /// /// Stops a workflow /// @@ -195,3 +201,11 @@ public interface IWorkflowCase /// TriggerConfig? TriggerConfig { get; set; } } + +/// +/// Result of a workflow run, including the Print() output lines captured during execution. +/// +public record WorkflowRunResult( + bool IsSuccess, + string? ErrorMessage, + IReadOnlyList? Output); From d9996ce8cae8e58bff9c37d8b5d61ea4a9ed2672 Mon Sep 17 00:00:00 2001 From: StarInk Date: Sat, 20 Jun 2026 01:41:13 +0200 Subject: [PATCH 55/60] =?UTF-8?q?=F0=9F=A7=A9=20Refactor(Contract):=20?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=20BranchArm=20=E4=B8=8E=20VariadicPinSpec,?= =?UTF-8?q?=E6=89=A9=E5=B1=95=20NodeDescriptor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 BranchArm:通用控制流臂模型(PinName/TargetBlockName/IsLoopback), 位于 Contract 层供 Workflow 与 Contract 共享 - 新增 VariadicPinSpec record:声明节点的变长端口增长规则 (BasePinName/StartIndex/PinType),供编辑器泛化"连一个长一个"机制 - NodeDescriptor 新增可选 InputVariadic/OutputVariadic 字段(默认 null=固定) --- .../KitX.Core.Contract/Workflow/BranchArm.cs | 28 +++++++++++++++++++ .../Workflow/Nodes/NodeDescriptor.cs | 8 ++++-- .../Workflow/Nodes/VariadicPinSpec.cs | 22 +++++++++++++++ 3 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/BranchArm.cs create mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/VariadicPinSpec.cs diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/BranchArm.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/BranchArm.cs new file mode 100644 index 0000000..ce79d0f --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/BranchArm.cs @@ -0,0 +1,28 @@ +namespace KitX.Core.Contract.Workflow; + +/// +/// A single outgoing arm of a control-flow statement (Branch / Loop / ToLoopCond / Switch). +/// Replaces the former fixed TrueBlockName/FalseBlockName/ToLoopCondReturnTo +/// triple, generalising the model to any number of arms so that an N-way Switch +/// (and future control-flow builtins) can be expressed without positional hacks. +/// +public class BranchArm +{ + /// + /// The output pin name this arm corresponds to on the Blueprint node + /// (e.g. "True", "False", "LoopBody", "LoopEnd", + /// "Exec", "Default", or "0".."N-1" for Switch). + /// + public string PinName { get; set; } = string.Empty; + + /// + /// The target block name this arm transfers control to. + /// + public string TargetBlockName { get; set; } = string.Empty; + + /// + /// Whether this arm is a loopback edge (used by ToLoopCond, which returns + /// to the parent loop's condition block). Drives CFGEdgeType.LoopbackToCondition. + /// + public bool IsLoopback { get; set; } +} diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/NodeDescriptor.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/NodeDescriptor.cs index a981e6c..2de3522 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/NodeDescriptor.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/NodeDescriptor.cs @@ -6,10 +6,14 @@ namespace KitX.Core.Contract.Workflow; /// Describes a node type's layout and pin configuration. /// Each node subclass provides its own descriptor via GetDescriptor(). /// +/// Optional variadic-growth spec for input pins. Null = fixed. +/// Optional variadic-growth spec for output pins. Null = fixed. public record NodeDescriptor( double Width, double Height, IReadOnlyList InputPins, IReadOnlyList OutputPins, - string DisplayName -); \ No newline at end of file + string DisplayName, + VariadicPinSpec? InputVariadic = null, + VariadicPinSpec? OutputVariadic = null +); diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/VariadicPinSpec.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/VariadicPinSpec.cs new file mode 100644 index 0000000..73a9c35 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/VariadicPinSpec.cs @@ -0,0 +1,22 @@ +namespace KitX.Core.Contract.Workflow; + +/// +/// Declares a variadic (auto-growing) pin group on a node descriptor. +/// +/// When a node declares InputVariadic or OutputVariadic, the Blueprint editor +/// appends a fresh pin of whenever the last pin of that group +/// gets connected — so the user can chain more inputs/outputs without manually adding pins. +/// +/// +/// is the title prefix for new pins; is the +/// starting number appended to it. The editor derives each new pin's name from the node's +/// current pin count (not by mutating this record), so every node instance counts independently +/// and survives save/load round-trips. +/// +/// Examples: +/// +/// StringConcat input: new("Input ", 3, PinType.String) → "Input 3", "Input 4", ... +/// Switch output: new("", 1, PinType.Execution) → "1", "2", ... +/// +/// +public record VariadicPinSpec(string BasePinName, int StartIndex, PinType PinType); From 03f800feb2f0a3d1c3335534bb91c90fc59f7ed6 Mon Sep 17 00:00:00 2001 From: StarInk Date: Sat, 20 Jun 2026 03:25:23 +0200 Subject: [PATCH 56/60] =?UTF-8?q?=F0=9F=A7=A9=20Refactor(Workflow):=20Node?= =?UTF-8?q?Descriptor=E5=8E=BB=E6=AD=BB=E5=AD=97=E6=AE=B5-KISS=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E9=98=B6=E6=AE=B5L0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 从 NodeDescriptor record 删除 Width/Height 两个零消费字段。 LayoutService 读取的是节点实例的 BlueprintNode.Width/Height(默认 200/100), 从未读取 descriptor 级尺寸;InitializePinsFromDescriptor 也只读 InputPins/OutputPins。 同步移除 7 个节点子类(Entry/Const/PluginTrigger/Call/CallHelper/Variable/BuiltinFunction) GetDescriptor() 里的 Width:/Height: 命名参数。 # KitX Workflow KISS 深度重构 L0 阶段:删除纯死代码 # Date: 2026-06-19 # Author: StarInk --- .../Workflow/Nodes/BuiltinFunctionNode.cs | 1 - .../KitX.Core.Contract/Workflow/Nodes/CallHelperNode.cs | 1 - .../KitX.Core.Contract/Workflow/Nodes/CallNode.cs | 1 - .../KitX.Core.Contract/Workflow/Nodes/ConstNode.cs | 1 - .../KitX.Core.Contract/Workflow/Nodes/EntryNode.cs | 1 - .../KitX.Core.Contract/Workflow/Nodes/NodeDescriptor.cs | 7 ++++--- .../KitX.Core.Contract/Workflow/Nodes/PluginTriggerNode.cs | 1 - .../KitX.Core.Contract/Workflow/Nodes/VariableNode.cs | 1 - 8 files changed, 4 insertions(+), 10 deletions(-) diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/BuiltinFunctionNode.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/BuiltinFunctionNode.cs index 209d2d1..1a075db 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/BuiltinFunctionNode.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/BuiltinFunctionNode.cs @@ -27,7 +27,6 @@ public class BuiltinFunctionNode : BlueprintNode public void SetDescriptor(NodeDescriptor descriptor) => _descriptor = descriptor; public override NodeDescriptor GetDescriptor() => _descriptor ?? new( - Width: 120, Height: 60, InputPins: [], OutputPins: [], DisplayName: FunctionName diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/CallHelperNode.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/CallHelperNode.cs index 00014d6..6aa0b48 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/CallHelperNode.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/CallHelperNode.cs @@ -18,7 +18,6 @@ public CallHelperNode() } public override NodeDescriptor GetDescriptor() => new( - Width: 130, Height: 50, InputPins: [new PinDescriptor("Exec", PinType.Execution, 25)], OutputPins: [ new PinDescriptor("Exec", PinType.Execution, 25), diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/CallNode.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/CallNode.cs index 43d5b14..6869419 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/CallNode.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/CallNode.cs @@ -38,7 +38,6 @@ public CallNode() } public override NodeDescriptor GetDescriptor() => new( - Width: 140, Height: 60, InputPins: [new PinDescriptor("Exec", PinType.Execution, 20)], OutputPins: [ new PinDescriptor("Exec", PinType.Execution, 20), diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/ConstNode.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/ConstNode.cs index 85ce4fd..16745fd 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/ConstNode.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/ConstNode.cs @@ -28,7 +28,6 @@ public ConstNode() } public override NodeDescriptor GetDescriptor() => new( - Width: 120, Height: 50, InputPins: [], OutputPins: [new PinDescriptor("Value", PinType.Any, 25)], DisplayName: "Const" diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/EntryNode.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/EntryNode.cs index fe96faa..505e692 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/EntryNode.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/EntryNode.cs @@ -13,7 +13,6 @@ public EntryNode() } public override NodeDescriptor GetDescriptor() => new( - Width: 120, Height: 60, InputPins: [], OutputPins: [new PinDescriptor("Exec", PinType.Execution, 30)], DisplayName: "Entry" diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/NodeDescriptor.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/NodeDescriptor.cs index 2de3522..d658eb9 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/NodeDescriptor.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/NodeDescriptor.cs @@ -3,14 +3,15 @@ namespace KitX.Core.Contract.Workflow; /// -/// Describes a node type's layout and pin configuration. +/// Describes a node type's pin configuration. /// Each node subclass provides its own descriptor via GetDescriptor(). +/// Width/Height are intentionally not part of the descriptor: node instances carry +/// their own layout dimensions (BlueprintNode.Width/Height defaults), and no consumer +/// ever reads descriptor-level dimensions. /// /// Optional variadic-growth spec for input pins. Null = fixed. /// Optional variadic-growth spec for output pins. Null = fixed. public record NodeDescriptor( - double Width, - double Height, IReadOnlyList InputPins, IReadOnlyList OutputPins, string DisplayName, diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/PluginTriggerNode.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/PluginTriggerNode.cs index c242636..2ae7ca5 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/PluginTriggerNode.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/PluginTriggerNode.cs @@ -20,7 +20,6 @@ public PluginTriggerNode() } public override NodeDescriptor GetDescriptor() => new( - Width: 160, Height: 60, InputPins: [], OutputPins: [new PinDescriptor("Exec", PinType.Execution, 30)], DisplayName: "PluginTrigger" diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/VariableNode.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/VariableNode.cs index 24fa7f7..6b6de71 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/VariableNode.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/VariableNode.cs @@ -25,7 +25,6 @@ public VariableNode() } public override NodeDescriptor GetDescriptor() => new( - Width: 120, Height: 50, InputPins: [], OutputPins: [], DisplayName: "Variable" From 829afd3f81e5f55cf376e63bbc63d8a65dc86d39 Mon Sep 17 00:00:00 2001 From: StarInk Date: Sat, 20 Jun 2026 03:29:28 +0200 Subject: [PATCH 57/60] =?UTF-8?q?=F0=9F=A7=A9=20Refactor(Workflow):=20Bran?= =?UTF-8?q?chArm.Clone-KISS=E4=BC=98=E5=8C=96=E9=98=B6=E6=AE=B5L1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为 BranchArm 新增 Clone() 方法,集中 arm 深拷贝逻辑。替换 BS2CFG / CFG2BS / BP2CFG / CFGConditionDuplicator 四处字符级相同的 Select(a => new BranchArm { ... }).ToList() 为 Select(a => a.Clone()).ToList()。 直接构造新 arm 的两处(非克隆)保持不变。 # KitX Workflow KISS 深度重构 L1 阶段:机械去重(子模块部分) # Date: 2026-06-19 # Author: StarInk --- .../KitX.Core.Contract/Workflow/BranchArm.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/BranchArm.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/BranchArm.cs index ce79d0f..0392ad4 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/BranchArm.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/BranchArm.cs @@ -25,4 +25,16 @@ public class BranchArm /// to the parent loop's condition block). Drives CFGEdgeType.LoopbackToCondition. /// public bool IsLoopback { get; set; } + + /// + /// Deep copy of this arm. Centralised so the four converters (BS2CFG / CFG2BS / + /// BP2CFG / CFGConditionDuplicator) share one clone path instead of four + /// character-identical Select(a => new BranchArm { ... }) blocks. + /// + public BranchArm Clone() => new() + { + PinName = PinName, + TargetBlockName = TargetBlockName, + IsLoopback = IsLoopback + }; } From da48fc4cb063dc99760b90cbe590b3f6a148016a Mon Sep 17 00:00:00 2001 From: StarInk Date: Sat, 20 Jun 2026 06:11:53 +0200 Subject: [PATCH 58/60] =?UTF-8?q?=F0=9F=A7=A9=20Refactor(Workflow):=20?= =?UTF-8?q?=E6=8A=BD=E5=8F=96ControlFlowArms=E5=85=B1=E4=BA=AB=E5=80=BC?= =?UTF-8?q?=E5=AF=B9=E8=B1=A1-KISS=E4=BC=98=E5=8C=96=E9=98=B6=E6=AE=B5L5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 ControlFlowArms 值对象(Contract 层),集中控制流 arm 的访问逻辑: Arms/TrueBlockName/FalseBlockName/LoopbackTarget/SetArm。消除 CFGStatement 与 FlowControlStatement 两处约 80 行逐字/近字重复,并修复一个隐藏的 null-vs-string.Empty 不一致(TrueBlockName/FalseBlockName 便利访问器在两层 返回不同类型的"空")。 ControlFlowArms 统一空值约定为 string.Empty(经 SetArm 归一化),避免 null 泄漏进 SourceCode 拼接。 # KitX Workflow KISS 深度重构 L5 阶段:抽取共享值对象(子模块部分) # Date: 2026-06-19 # Author: StarInk --- .../Workflow/ControlFlowArms.cs | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 KitX Core Contracts/KitX.Core.Contract/Workflow/ControlFlowArms.cs diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/ControlFlowArms.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/ControlFlowArms.cs new file mode 100644 index 0000000..cf6b89c --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/ControlFlowArms.cs @@ -0,0 +1,73 @@ +using System.Collections.Generic; + +namespace KitX.Core.Contract.Workflow; + +/// +/// Shared control-flow arms model embedded by both CFGStatement (CFG layer) and +/// FlowControlStatement (BlockScript layer). Eliminates the ~80 lines of +/// character-identical Arms/TrueBlockName/FalseBlockName/ +/// LoopbackTarget/SetArm duplication that had drifted between the two layers +/// (including a hidden null-vs-string.Empty inconsistency in the convenience accessors). +/// +/// Both layers expose this via their own delegating properties (Arms, +/// TrueBlockName, FalseBlockName, LoopbackTarget) so existing call +/// sites (stmt.Arms, flow.TrueBlockName, ...) are unchanged. +/// +/// +/// Branch: ["True", "False"] +/// Loop: ["LoopBody", "LoopEnd"] +/// ToLoopCond: ["Exec" with =true] +/// Switch: ["Default", "0", "1", ... , "N-1"] +/// +/// +public class ControlFlowArms +{ + /// + /// Outgoing arms of this control-flow statement. Generalised model replacing the former + /// fixed TrueBlockName/FalseBlockName/ToLoopCondReturnTo triple. + /// See for the per-arm layout of each control-flow kind. + /// + public List Arms { get; set; } = []; + + /// Convenience accessor: the true-branch / loop-body target (Arms[0]). + public string? TrueBlockName + { + get => Arms.Count > 0 ? Arms[0].TargetBlockName : null; + set => SetArm(0, "True", value); + } + + /// Convenience accessor: the false-branch / loop-exit target (Arms[1]). + public string? FalseBlockName + { + get => Arms.Count > 1 ? Arms[1].TargetBlockName : null; + set => SetArm(1, "False", value); + } + + /// + /// The ToLoopCond loopback target — the loop condition block this statement returns to. + /// Unified into [0] (PinName="Exec", IsLoopback=true) so ToLoopCond is + /// treated uniformly with Branch/Loop/Switch: all control-flow targets live in Arms. + /// The former standalone ToLoopCondReturnTo field was a patch over an Arms[0] + /// collision that no longer exists; Loop statements no longer carry this metadata at all + /// (it was dead — never read for control flow, only copied and debug-printed). + /// + public string? LoopbackTarget + { + get => Arms.Count > 0 ? Arms[0].TargetBlockName : null; + set => SetArm(0, "Exec", value, isLoopback: true); + } + + /// + /// Sets the arm at , growing with blank arms + /// as needed. Normalises a null to string.Empty so the + /// convenience accessors never surface null into SourceCode concatenation. + /// + public void SetArm(int index, string pinName, string? value, bool isLoopback = false) + { + while (Arms.Count <= index) + Arms.Add(new BranchArm()); + Arms[index].PinName = pinName; + Arms[index].TargetBlockName = value ?? string.Empty; + Arms[index].IsLoopback = isLoopback; + } +} \ No newline at end of file From 2b8270229dcf690c0d24aa40943c156026f89431 Mon Sep 17 00:00:00 2001 From: StarInk Date: Sat, 20 Jun 2026 10:46:21 +0200 Subject: [PATCH 59/60] =?UTF-8?q?=F0=9F=A7=A9=20Refactor(Workflow):=20?= =?UTF-8?q?=E7=A7=BB=E9=99=A4=20IWorkflowPluginService=20=E5=92=8C=20IWork?= =?UTF-8?q?flowCase=20=E4=B8=AD=E7=9A=84=E5=86=97=E4=BD=99=E5=AD=97?= =?UTF-8?q?=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../KitX.Core.Contract/Workflow/IWorkflowService.cs | 10 ---------- .../KitX.Core.Contract/Workflow/KcsFileFormat.cs | 7 +------ 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowService.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowService.cs index 05bf609..702cc0b 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowService.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowService.cs @@ -54,11 +54,6 @@ public interface IWorkflowManagementService /// public interface IWorkflowPluginService { - /// - /// Initializes the plugin manager - /// - void InitializePluginManager(); - /// /// Updates the available plugins list /// @@ -191,11 +186,6 @@ public interface IWorkflowCase /// DateTime LastModifiedTime { get; set; } - /// - /// Gets or sets the trigger type (e.g., "Manual", "PluginEvent") - /// - string TriggerType { get; set; } - /// /// Gets or sets the trigger configuration /// diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/KcsFileFormat.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/KcsFileFormat.cs index ba1a1bc..f81840a 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/KcsFileFormat.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/KcsFileFormat.cs @@ -40,12 +40,7 @@ public class KcsFileFormat public DateTime LastModifiedTime { get; set; } = DateTime.UtcNow; /// - /// 触发类型("Manual", "PluginEvent" 等) - /// - public string TriggerType { get; set; } = "Manual"; - - /// - /// 触发器配置 + /// 触发器配置(包含触发类型、插件名、触发器名等结构化字段) /// public TriggerConfig? TriggerConfig { get; set; } From 82762139dc002a8cde6c5d5db84385414ee02eaf Mon Sep 17 00:00:00 2001 From: StarInk Date: Sat, 20 Jun 2026 13:12:53 +0200 Subject: [PATCH 60/60] =?UTF-8?q?=F0=9F=A7=A9=20Refactor(Workflow.Contract?= =?UTF-8?q?):=20=E7=A7=BB=E9=99=A4=E6=97=A0=E6=B6=88=E8=B4=B9=E8=80=85?= =?UTF-8?q?=E7=9A=84=E5=B7=A5=E4=BD=9C=E6=B5=81=E6=8E=A5=E5=8F=A3=E6=88=90?= =?UTF-8?q?=E5=91=98-=E6=AD=BB=E4=BB=A3=E7=A0=81=E6=B8=85=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - IWorkflowManagementService: 删除 AddWorkflow/GetWorkflows/RemoveWorkflow(Dashboard 直接走 IWorkflowStorageService) - IWorkflowPluginService: 删除 UpdateAvailablePlugins/ApplyConstantsToCode/MergeHelperFunctions(无运行时调用) - IWorkflowStorageService: 删除 RenameWorkflowAsync/PreloadCompiledScriptsAsync(Dashboard 用事件而非方法) - IBlockScriptService: 删除单参 Dictionary 重载(无调用方) - IKcsFileService: 删除整个接口(DI 注册但从未被解析)+ 删除孤儿 KcsFileFormat 末尾悬空注释 经全仓 grep 验证: 11 个接口方法 0 外部调用者, 无反射/字符串派发 BREAKING CHANGE: 公共契约 API 表面缩减, 外部插件若依赖被删成员需迁移 Date: 2026-06-20 Author: StarInk --- .../Workflow/IWorkflowService.cs | 38 ------------------- .../Workflow/IWorkflowStorageService.cs | 14 ------- .../Workflow/KcsFileFormat.cs | 23 ----------- 3 files changed, 75 deletions(-) diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowService.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowService.cs index 702cc0b..bff4f8d 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowService.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowService.cs @@ -10,21 +10,6 @@ namespace KitX.Core.Contract.Workflow; /// public interface IWorkflowManagementService { - /// - /// Gets the workflow list - /// - IReadOnlyList GetWorkflows(); - - /// - /// Adds a workflow - /// - void AddWorkflow(IWorkflowCase workflow); - - /// - /// Removes a workflow - /// - void RemoveWorkflow(string workflowId); - /// /// Runs a workflow /// @@ -54,25 +39,10 @@ public interface IWorkflowManagementService /// public interface IWorkflowPluginService { - /// - /// Updates the available plugins list - /// - void UpdateAvailablePlugins(List plugins); - /// /// Parses constants from code /// List ParseConstantsFromCode(string code); - - /// - /// Applies constants to code - /// - string ApplyConstantsToCode(string code, List constants); - - /// - /// Merges helper functions into code - /// - string MergeHelperFunctions(string mainCode, List helperFunctions); } /// @@ -97,14 +67,6 @@ public interface IBlockScriptService /// List ParseConstantsFromBlockScript(string sourceCode); - /// - /// Executes a block script from source code - /// - Task ExecuteBlockScriptAsync( - string sourceCode, - Dictionary? parameters = null, - System.Threading.CancellationToken cancellationToken = default); - /// /// Executes a block script from source code with helper functions /// diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowStorageService.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowStorageService.cs index 07e5ddb..03ac755 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowStorageService.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowStorageService.cs @@ -41,13 +41,6 @@ public interface IWorkflowStorageService /// Workflow ID Task DeleteWorkflowAsync(string workflowId); - /// - /// Renames a workflow - /// - /// Workflow ID - /// New name - Task RenameWorkflowAsync(string workflowId, string newName); - /// /// Discovers all stored workflows by scanning the storage directory /// @@ -60,11 +53,4 @@ public interface IWorkflowStorageService /// Workflow ID /// Full file path string GetWorkflowFilePath(string workflowId); - - /// - /// Preloads all persisted compiled scripts for discovered workflows from disk - /// into the in-memory cache. Called at startup to enable fast first-run execution. - /// - /// Number of scripts successfully loaded. - Task PreloadCompiledScriptsAsync(); } diff --git a/KitX Core Contracts/KitX.Core.Contract/Workflow/KcsFileFormat.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/KcsFileFormat.cs index f81840a..f38a4a0 100644 --- a/KitX Core Contracts/KitX.Core.Contract/Workflow/KcsFileFormat.cs +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/KcsFileFormat.cs @@ -147,26 +147,3 @@ public class VariableConstant public string Type { get; set; } = "string"; } -/// -/// KCS文件服务接口 - 仅负责KCS文件的读写 -/// -public interface IKcsFileService -{ - /// - /// 加载KCS文件 - /// - /// 文件路径 - /// KCS文件内容 - Task LoadKcsFileAsync(string filePath); - - /// - /// 保存KCS文件 - /// - /// 文件路径 - /// KCS文件内容 - Task SaveKcsFileAsync(string filePath, KcsFileFormat kcs); -} - -/// -/// KCS 文件服务接口 -///