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/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/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/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/IConfigService.cs b/KitX Core Contracts/KitX.Core.Contract/Configuration/IConfigService.cs new file mode 100644 index 0000000..4c15a2a --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Configuration/IConfigService.cs @@ -0,0 +1,44 @@ +using System; + +namespace KitX.Core.Contract.Configuration; + +/// +/// 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; +} 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..39dff37 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Configuration/IPagesConfig.cs @@ -0,0 +1,42 @@ +namespace KitX.Core.Contract.Configuration; + +/// +/// Pages configuration section +/// +public interface IPagesConf +{ + IHomePageConf Home { get; set; } + object? Device { get; set; } + object? 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; } +} + +/// +/// 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..c100ce7 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Configuration/ISecurityConfig.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using KitX.Shared.CSharp.Device; + +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 device locator + /// + DeviceLocator Device { get; } + + /// + /// Gets the RSA public key in PEM format + /// + string? RsaPublicKeyPem { get; } + + /// + /// 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; } +} \ No newline at end of file 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/Device/IDeviceService.cs b/KitX Core Contracts/KitX.Core.Contract/Device/IDeviceService.cs new file mode 100644 index 0000000..790c82c --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Device/IDeviceService.cs @@ -0,0 +1,265 @@ +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; + +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(); + + /// + /// 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(); +} + +/// +/// 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; + + /// + /// Event raised when a device goes offline + /// + event EventHandler? DeviceOffline; +} + +/// +/// 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; +} + +/// +/// 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/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/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/Event/WorkflowEventArgs.cs b/KitX Core Contracts/KitX.Core.Contract/Event/WorkflowEventArgs.cs new file mode 100644 index 0000000..cda2807 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Event/WorkflowEventArgs.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; + +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; } + + /// + /// 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; + } +} + +/// +/// 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; } + + /// + /// 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/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/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 0000000..7abdbc3 Binary files /dev/null and b/KitX Core Contracts/KitX.Core.Contract/KitX-Background-ani.png differ diff --git a/KitX Core Contracts/KitX.Core.Contract/KitX.Core.Contract.csproj b/KitX Core Contracts/KitX.Core.Contract/KitX.Core.Contract.csproj new file mode 100644 index 0000000..7a9a866 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/KitX.Core.Contract.csproj @@ -0,0 +1,49 @@ + + + + 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/Events/PluginEventArgs.cs b/KitX Core Contracts/KitX.Core.Contract/Plugin/Events/PluginEventArgs.cs new file mode 100644 index 0000000..068086b --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Plugin/Events/PluginEventArgs.cs @@ -0,0 +1,122 @@ +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; } +} + +/// +/// 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 new file mode 100644 index 0000000..5f63eff --- /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..871277a --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Plugin/IPluginServer.cs @@ -0,0 +1,82 @@ +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); + + /// + /// 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 + /// + event EventHandler? PluginRegistered; + + /// + /// 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 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..495e5a2 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Plugin/IPluginService.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +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; + +/// +/// 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; +} \ 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/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/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/IEncryptionService.cs b/KitX Core Contracts/KitX.Core.Contract/Security/IEncryptionService.cs new file mode 100644 index 0000000..4027014 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Security/IEncryptionService.cs @@ -0,0 +1,82 @@ +using System.Threading.Tasks; +using KitX.Shared.CSharp.Security; + +namespace KitX.Core.Contract.Security; + +/// +/// Encryption service interface +/// +public interface IEncryptionService +{ + /// + /// 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); + + /// + /// Encrypts a string using RSA with a specific device's public key + /// + /// The device key containing the public key + /// The data to encrypt + /// The encrypted data as Base64 string + string? RsaEncryptString(Shared.CSharp.Device.DeviceKey key, string data); + + /// + /// Decrypts a string using RSA with a specific device's private key + /// + /// The device key containing the private key + /// The encrypted data as Base64 string + /// The decrypted data + string? RsaDecryptString(Shared.CSharp.Device.DeviceKey key, string encryptedData); + + /// + /// Encrypts content using RSA+AES hybrid encryption + /// + /// The device key + /// The content to encrypt + /// The encrypted content + EncryptedContent RsaEncryptContent(Shared.CSharp.Device.DeviceKey key, string content); + + /// + /// Decrypts content using RSA+AES hybrid decryption + /// + /// The device key + /// The encrypted content + /// The decrypted content + string RsaDecryptContent(Shared.CSharp.Device.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); + + /// + /// Computes SHA1 hash of a string + /// + /// The data to hash + /// The SHA1 hash string + string GetSHA1(string data); +} 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..e2b3f3a --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Tasks/ITasksService.cs @@ -0,0 +1,35 @@ +using System; +using System.Threading; +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); + + /// + /// 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/BlueprintModels.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/BlueprintModels.cs new file mode 100644 index 0000000..11aec19 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/BlueprintModels.cs @@ -0,0 +1,222 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Serialization; +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); +} + +/// +/// 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; } +} + +/// +/// 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/ToLoopCond). 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 +/// +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; } = []; + + /// + /// 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 + /// + 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 (deduplicates by source/target/pin) + /// + /// Connection to add + public void AddConnection(BlueprintConnection 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/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); +} 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..0392ad4 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/BranchArm.cs @@ -0,0 +1,40 @@ +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; } + + /// + /// 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 + }; +} 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 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..9107a75 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/IBlueprintConverter.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace KitX.Core.Contract.Workflow; + +/// +/// 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 +{ + /// + /// 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); + + /// + /// 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/INodeRegistry.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/INodeRegistry.cs new file mode 100644 index 0000000..eaa259e --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/INodeRegistry.cs @@ -0,0 +1,38 @@ +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; } + + /// + /// 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); +} 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 new file mode 100644 index 0000000..bff4f8d --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowService.cs @@ -0,0 +1,163 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using KitX.Shared.CSharp.Plugin; + +namespace KitX.Core.Contract.Workflow; + +/// +/// Workflow management interface +/// +public interface IWorkflowManagementService +{ + /// + /// Runs a workflow + /// + 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 + /// + 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); +} + +/// +/// Plugin service interface for workflow constant and helper function handling +/// +public interface IWorkflowPluginService +{ + /// + /// Parses constants from code + /// + List ParseConstantsFromCode(string code); +} + +/// +/// 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 +{ + /// + /// Validates a block script + /// + 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 source code with helper functions + /// + 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); + + /// + /// 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); +} + +/// +/// Workflow case interface +/// +public interface IWorkflowCase +{ + /// + /// Gets the workflow ID + /// + string Id { get; } + + /// + /// Gets or sets the workflow name + /// + string Name { get; set; } + + /// + /// Gets or sets the workflow description + /// + string Description { get; set; } + + /// + /// Gets or sets the author name + /// + string Author { get; set; } + + /// + /// Gets or sets a value indicating whether the workflow is running + /// + 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 + /// + 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 configuration + /// + 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); 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..03ac755 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/IWorkflowStorageService.cs @@ -0,0 +1,56 @@ +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); + + /// + /// 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 new file mode 100644 index 0000000..f38a4a0 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/KcsFileFormat.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace KitX.Core.Contract.Workflow; + +/// +/// KCS (KitX Code Script) 文件格式定义 +/// +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; + + /// + /// 触发器配置(包含触发类型、插件名、触发器名等结构化字段) + /// + public TriggerConfig? TriggerConfig { get; set; } + + /// + /// 主程序代码 + /// + public string MainProgram { get; set; } = string.Empty; + + /// + /// 辅助函数列表 + /// + public List HelperFunctions { get; set; } = []; + + /// + /// 可变常量及其用户修改后的值 + /// + public Dictionary VariableConstants { get; set; } = []; + + /// + /// 是否使用块脚本模式 + /// + /// + /// 当为 true 时,使用 BlockScriptSource 作为脚本内容 + /// + public bool UseBlockMode { get; set; } = false; + + /// + /// 块脚本源代码(当 UseBlockMode 为 true 时使用) + /// + public string? BlockScriptSource { get; set; } + + /// + /// 蓝图可视化数据(包含节点位置、连接关系、视图状态等) + /// 当 UseBlockMode=true 且此字段非空时,表示该脚本有对应的蓝图编辑状态 + /// + public Blueprint? BlueprintData { 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"; +} + 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..34ee425 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/BlueprintNode.cs @@ -0,0 +1,129 @@ +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(ConstNode), "Const")] +[JsonDerivedType(typeof(CallNode), "Call")] +[JsonDerivedType(typeof(CallHelperNode), "CallHelper")] +[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..5b2f1d9 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/BlueprintNodeType.cs @@ -0,0 +1,45 @@ +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, + + /// + /// Constant value node + /// + Const, + + /// + /// Plugin function call node + /// + Call, + + /// + /// Helper function call node + /// + CallHelper, + + /// + /// 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 +} 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/BuiltinFunctionNode.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/BuiltinFunctionNode.cs new file mode 100644 index 0000000..1a075db --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/BuiltinFunctionNode.cs @@ -0,0 +1,42 @@ +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( + 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..6aa0b48 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/CallHelperNode.cs @@ -0,0 +1,30 @@ +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( + 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..6869419 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/CallNode.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; + +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; } + + /// + /// 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; + Name = "Call"; + InitializePinsFromDescriptor(); + } + + public override NodeDescriptor GetDescriptor() => new( + 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..16745fd --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/ConstNode.cs @@ -0,0 +1,37 @@ +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( + 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..505e692 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/EntryNode.cs @@ -0,0 +1,20 @@ +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( + 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/NodeDescriptor.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/NodeDescriptor.cs new file mode 100644 index 0000000..d658eb9 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/NodeDescriptor.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; + +namespace KitX.Core.Contract.Workflow; + +/// +/// 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( + IReadOnlyList InputPins, + IReadOnlyList OutputPins, + string DisplayName, + VariadicPinSpec? InputVariadic = null, + VariadicPinSpec? OutputVariadic = null +); 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..2ae7ca5 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/PluginTriggerNode.cs @@ -0,0 +1,32 @@ +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( + 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/VariableNode.cs b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/VariableNode.cs new file mode 100644 index 0000000..6b6de71 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/Nodes/VariableNode.cs @@ -0,0 +1,34 @@ +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( + 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/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); 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..cfe0251 --- /dev/null +++ b/KitX Core Contracts/KitX.Core.Contract/Workflow/Results/BlockScriptExecutionResult.cs @@ -0,0 +1,45 @@ +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; } + + /// + /// 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/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/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 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..e648cf9 --- /dev/null +++ b/KitX Script/Kscript.CSharp.Compiler/ScriptExecutor.cs @@ -0,0 +1,261 @@ +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.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) + .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) + { + } +} 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 + + + + + + + + + + + + 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/CodeGen/AssemblyCache.cs b/KitX Script/Kscript.CSharp.Parser/CodeGen/AssemblyCache.cs new file mode 100644 index 0000000..01dcdfc --- /dev/null +++ b/KitX Script/Kscript.CSharp.Parser/CodeGen/AssemblyCache.cs @@ -0,0 +1,145 @@ +using System.Collections.Concurrent; +using System.Text; + +namespace Kscript.CSharp.Parser.CodeGen; + +/// +/// 程序集缓存管理器,避免重复生成相同的程序集 +/// +public static class AssemblyCache +{ + /// + /// 程序集缓存字典,Key 为插件清单的哈希值,Value 为生成的程序集 + /// + private static readonly ConcurrentDictionary _assemblyCache = new(); + + /// + /// 获取或生成程序集 + /// + /// 插件信息列表 + /// 程序集名称 + /// 插件管理器实例 + /// 缓存的或新生成的程序集 + public static Assembly GetOrCreateAssembly(List plugins, string assemblyName, Core.IPluginManager pluginManager) + { + var cacheKey = GenerateCacheKey(plugins, assemblyName); + + // 尝试从缓存获取 + if (_assemblyCache.TryGetValue(cacheKey, out var cachedAssembly)) + { + return cachedAssembly; + } + + // 生成新的程序集 + var newAssembly = MethodEmitter.GenerateAssembly(plugins, assemblyName, pluginManager); + + // 添加到缓存 + _assemblyCache.TryAdd(cacheKey, newAssembly); + + return newAssembly; + } + + /// + /// 生成插件清单的缓存键 + /// + /// 插件信息列表 + /// 程序集名称 + /// 缓存键字符串 + private static string GenerateCacheKey(List plugins, string assemblyName) + { + // 使用轻量级字符串哈希,避免复杂的JSON序列化和SHA256计算 + var keyBuilder = new StringBuilder(); + keyBuilder.Append(assemblyName); + keyBuilder.Append($"|{plugins.Count}"); + + foreach (var plugin in plugins.OrderBy(p => p.Name)) + { + keyBuilder.Append($"|{plugin.Name}:{plugin.Version}:{plugin.Functions.Count}"); + foreach (var function in plugin.Functions.OrderBy(f => f.Name)) + { + keyBuilder.Append($":{function.Name}:{function.ReturnValueType}:{function.Parameters.Count}"); + } + } + + return keyBuilder.ToString().GetHashCode().ToString("X"); + } + + /// + /// 清除所有缓存 + /// + public static void ClearCache() + { + _assemblyCache.Clear(); + // 清除所有程序集加载上下文 + MethodEmitter.ClearAllAssemblyContexts(); + } + + /// + /// 获取缓存统计信息 + /// + /// 缓存统计信息 + public static CacheStatistics GetStatistics() + { + return new CacheStatistics + { + CachedAssemblyCount = _assemblyCache.Count, + 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, Core.IPluginManager pluginManager, string assemblyName = "DynamicPluginAssembly") + { + var cacheKey = GenerateCacheKey(plugins, assemblyName); + + // 移除现有缓存 + _assemblyCache.TryRemove(cacheKey, out _); + + // 生成新的程序集(这会自动卸载旧的程序集加载上下文) + var newAssembly = MethodEmitter.GenerateAssembly(plugins, assemblyName, pluginManager); + + // 添加到缓存 + _assemblyCache.TryAdd(cacheKey, newAssembly); + + return newAssembly; + } +} + +/// +/// 缓存统计信息 +/// +public class CacheStatistics +{ + /// + /// 缓存的程序集数量 + /// + public int CachedAssemblyCount { get; set; } + + /// + /// 缓存键列表 + /// + public List CacheKeys { get; set; } = new(); + + public override string ToString() + { + return $"缓存统计: {CachedAssemblyCount} 个程序集"; + } +} 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..e850f3b --- /dev/null +++ b/KitX Script/Kscript.CSharp.Parser/CodeGen/MethodEmitter.cs @@ -0,0 +1,395 @@ +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; + +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(); + /// + /// 为插件生成动态程序集 + /// + /// 插件信息列表 + /// 程序集名称 + /// 插件管理器实例 + /// 生成的动态程序集 + public static Assembly GenerateAssembly(List plugins, + string assemblyName, IPluginManager pluginManager) + { + try + { + // 如果已存在相同名称的程序集加载上下文,先卸载它 + 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), + typeof(object).Assembly); + + var moduleBuilder = persistedAssemblyBuilder.DefineDynamicModule($"{assemblyName}.dll"); + + // 为每个插件生成静态类 + foreach (var plugin in plugins) + { + GeneratePluginClass(moduleBuilder, plugin, pluginManager); + } + + // 创建所有类型 + 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) + { + Console.WriteLine($"[MethodEmitter] 生成程序集失败: {assemblyName}, 详细错误: {ex}"); + throw new ParserException($"生成程序集失败: {assemblyName}", ex); + } + } + + /// + /// 卸载程序集加载上下文 + /// + /// 程序集名称 + /// 要卸载的加载上下文 + 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); + } + } + + /// + /// 为单个插件生成静态类 + /// + 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 new ParserException($"生成IL代码失败: {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(); + + // 使用提供的插件管理器实例 + 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); + } + + /// + /// 为插件功能生成静态方法 + /// + 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]; + + // 根据 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]); + if (defaultValue != null) + { + paramBuilder.SetConstant(defaultValue); + } + } + } + + // 生成方法体IL代码 + GenerateMethodBody(methodBuilder, pluginName, function, parameterTypes, returnType, pluginManagerField); + } + catch (Exception ex) + { + throw new ParserException($"生成IL代码失败: {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[])); + var parameterNamesArrayLocal = il.DeclareLocal(typeof(string[])); + 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); + + // 创建参数名称数组 + 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); + 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); + + // 填充参数名称 + 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 实例 + il.Emit(OpCodes.Ldstr, pluginName); + 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[]) + })!); + 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..1810ffb --- /dev/null +++ b/KitX Script/Kscript.CSharp.Parser/CodeGen/TypeMapper.cs @@ -0,0 +1,86 @@ +using System.Collections.Concurrent; + +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 _typeCache = new(); + + /// + /// 映射字符串类型名到 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; + + // 检查基础类型 + if (_basicTypeMap.TryGetValue(typeName, out var basicType)) + { + _typeCache[typeName] = basicType; + return basicType; + } + + // 尝试直接解析类型 + try + { + var resolvedType = Type.GetType(typeName); + if (resolvedType != null) + { + _typeCache[typeName] = resolvedType; + return resolvedType; + } + } + catch + { + // 发生异常时回退到 object 类型 + } + + // 回退到 object 类型 + _typeCache[typeName] = typeof(object); + return typeof(object); + } + + /// + /// 清除类型缓存 + /// + public static void ClearCache() + { + _typeCache.Clear(); + } +} 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/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/MockPluginManager.cs b/KitX Script/Kscript.CSharp.Parser/Core/MockPluginManager.cs new file mode 100644 index 0000000..2fadfa8 --- /dev/null +++ b/KitX Script/Kscript.CSharp.Parser/Core/MockPluginManager.cs @@ -0,0 +1,173 @@ +using Kscript.CSharp.Parser.Models; +using KitX.Shared.CSharp.Plugin; + +namespace Kscript.CSharp.Parser.Core; + +/// +/// 模拟插件管理器 - 用于调试和测试 +/// +public class MockPluginManager : IPluginManager +{ + /// + /// 模拟的插件数据,用于验证调用 + /// + private readonly Dictionary> _mockData = new() + { + { + "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) } + } + }, + { + "StringToolkit", new Dictionary + { + { "Reverse", (string text) => new string(text.Reverse().ToArray()) }, + { "ToUpper", (string text) => text.ToUpperInvariant() }, + { "Concat", (string str1, string str2) => str1 + str2 } + } + }, + { + "KitXWF", new Dictionary + { + { "Print", (string message) => Console.WriteLine(message) } + } + } + }; + + /// + /// 调用插件方法 + /// + public T Call(PluginCallInfo callInfo) + { + Console.WriteLine($"[MockPluginManager] 调用插件方法: {callInfo}"); + 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}"); + + // 简单的模拟实现 + 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) + { + // 根据插件和方法名进行简单的模拟计算 + switch (callInfo.PluginName) + { + 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; + } + + return $"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/Core/RealPluginManager.cs b/KitX Script/Kscript.CSharp.Parser/Core/RealPluginManager.cs new file mode 100644 index 0000000..e2b330c --- /dev/null +++ b/KitX Script/Kscript.CSharp.Parser/Core/RealPluginManager.cs @@ -0,0 +1,272 @@ +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 IPluginServiceProvider _serviceProvider; + private readonly Action _infoLogger; + private readonly Action _errorLogger; + + private readonly JsonSerializerOptions _serializerOptions = new() + { + WriteIndented = true, + IncludeFields = true, + PropertyNameCaseInsensitive = true, + }; + + // 用于存储等待响应的请求 + private readonly ConcurrentDictionary> _pendingRequests = new(); + + /// + /// 构造函数 + /// + /// 插件服务提供者实例 + /// 信息日志记录器 + /// 错误日志记录器 + public RealPluginManager( + IPluginServiceProvider serviceProvider, + Action? infoLogger = null, + Action? errorLogger = null) + { + _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + _infoLogger = infoLogger ?? (message => Console.WriteLine(message)); + _errorLogger = errorLogger ?? (message => Console.WriteLine(message)); + + _infoLogger("[RealPluginManager] 正在初始化 RealPluginManager..."); + + // 订阅插件响应事件 + _serviceProvider.SubscribeToResponses(HandlePluginResponse); + + _infoLogger("[RealPluginManager] RealPluginManager 初始化完成"); + } + + /// + /// 调用插件方法 + /// + public T Call(PluginCallInfo callInfo) + { + try + { + _infoLogger($"[RealPluginManager] 开始调用插件方法: {callInfo}"); + + // 查找插件信息 + var pluginInfo = _serviceProvider.FindPlugin(callInfo.PluginName); + if (pluginInfo == null) + { + _infoLogger($"[RealPluginManager] 未找到插件: {callInfo.PluginName}"); + return GetDefaultResult(); + } + + // 查找插件连接器 + var connector = _serviceProvider.FindConnector(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 = _serviceProvider.FindPlugin(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 = _serviceProvider.FindPlugin(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 async Task SendPluginRequest(object connector, PluginCallInfo callInfo) + { + try + { + // 创建 Connector 实例 + var connectorInstance = new Connector() + .SetSerializer(x => JsonSerializer.Serialize(x, _serializerOptions)) + .SetSender(request => { + _serviceProvider.SendRequestAsync(connector, request).ConfigureAwait(false); + }); + + // 构建参数列表 + var functionArgs = new List(); + for (int i = 0; i < callInfo.Parameters.Length; 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 = paramName, + Type = paramType, + Value = paramValue, + 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(); + } + } + + /// + /// 处理插件响应 + /// + 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/Exceptions/ParserException.cs b/KitX Script/Kscript.CSharp.Parser/Exceptions/ParserException.cs new file mode 100644 index 0000000..a46417e --- /dev/null +++ b/KitX Script/Kscript.CSharp.Parser/Exceptions/ParserException.cs @@ -0,0 +1,19 @@ +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) + { + } +} 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..37863bf --- /dev/null +++ b/KitX Script/Kscript.CSharp.Parser/Kscript.CSharp.Parser.csproj @@ -0,0 +1,19 @@ + + + + net10.0 + preview + enable + enable + Kscript.CSharp.Parser + + + + + + + + + + + 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..ee55d67 --- /dev/null +++ b/KitX Script/Kscript.CSharp.Parser/Models/PluginCallInfo.cs @@ -0,0 +1,52 @@ +using KitX.Shared.CSharp.Plugin; + +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 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)})"; + } +} diff --git a/KitX Script/Kscript.CSharp.Parser/Parser.cs b/KitX Script/Kscript.CSharp.Parser/Parser.cs new file mode 100644 index 0000000..d9b701a --- /dev/null +++ b/KitX Script/Kscript.CSharp.Parser/Parser.cs @@ -0,0 +1,266 @@ +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? _pluginManager; + + /// + /// 获取Parser初始化状态 + /// + public static bool IsInitialized => _pluginManager != null; + + /// + /// 设置插件管理器 + /// + /// 插件管理器实例 + public static void SetPluginManager(IPluginManager pluginManager) + { + _pluginManager = pluginManager ?? throw new ArgumentNullException(nameof(pluginManager)); + } + + /// + /// 确保Parser已正确初始化 + /// + /// 当Parser未初始化时抛出 + private static void EnsureInitialized() + { + 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}。请确保插件已正确安装并启动。"); + } + } + } + + /// + /// 从插件信息列表生成动态程序集 + /// + /// 插件信息列表 + /// 程序集名称 + /// 是否使用缓存 + /// 生成的动态程序集 + 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 + { + if (useCache) + { + return AssemblyCache.GetOrCreateAssembly(plugins, assemblyName, _pluginManager!); + } + else + { + return AssemblyCache.ForceRegenerate(plugins, _pluginManager!, assemblyName); + } + } + catch (Exception ex) when (!(ex is ParserException)) + { + throw new ParserException($"生成程序集失败: {assemblyName}", ex); + } + } + + /// + /// 从 JSON 字符串生成动态程序集 + /// + /// 插件清单 JSON 字符串 + /// 程序集名称 + /// 是否使用缓存 + /// 生成的动态程序集 + public static Assembly GenerateFromJson(string jsonString, string assemblyName = "DynamicPluginAssembly", 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, useCache); + } + catch (JsonException ex) + { + throw new ParserException($"JSON 格式错误: {ex.Message}", ex); + } + } + + /// + /// 从 JSON 文件生成动态程序集 + /// + /// JSON 文件路径 + /// 程序集名称 + /// 是否使用缓存 + /// 生成的动态程序集 + public static async Task GenerateFromFileAsync(string jsonFilePath, string assemblyName = "DynamicPluginAssembly", 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, useCache); + } + catch (Exception ex) when (!(ex is ParserException)) + { + throw new ParserException($"读取文件失败: {jsonFilePath}", ex); + } + } + + + /// + /// 清除所有缓存 + /// + 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..fff54a1 --- /dev/null +++ b/KitX Script/Kscript.CSharp.Parser/README.md @@ -0,0 +1,218 @@ +# KitX.CSharp.Parser +> 将插件清单 JSON 即时编译成可直接调用的 C# 静态 API + +--- + +## 🌱 项目定位 + +KitX.CSharp.Parser 是 **KitX 工作流子系统** 的核心组件。它专注于一个核心功能: + +**把插件清单(JSON)→ 运行时静态类/方法**,让脚本引擎可以像调用普通静态方法一样使用插件功能。 + +``` +JSON 清单 ─→ Parser ─→ 内存程序集 ─→ 脚本侧: SamplePlugin.Method(...) +``` + +--- + +## 🎯 核心特性 + +- ✅ **动态 IL 生成** - 使用 `Reflection.Emit` 生成高性能静态方法 +- ✅ **智能类型映射** - 支持基础类型和自定义类型 +- ✅ **程序集缓存** - 避免重复生成,提升性能 +- ✅ **简化设计** - 直接依赖注入,无复杂抽象层 +- ✅ **异常处理** - 完善的错误处理机制 +- ✅ **调试友好** - 详细的日志输出和统计信息 + +--- + +## 🚀 快速开始 + +### 基本用法 + +```csharp +using Kscript.CSharp.Parser; + +// 1. 设置插件管理器(必须) +Parser.SetPluginManager(new MockPluginManager()); + +// 2. 从 JSON 文件生成程序集 +var assembly = await Parser.GenerateFromFileAsync("plugins.json"); + +// 3. 从插件信息列表生成程序集 +var plugins = new List { /* ... */ }; +var assembly = Parser.Generate(plugins); + +// 4. 脚本中直接调用生成的方法 +int sum = SampleCalculator.Add(10, 20); +string reversed = StringToolkit.Reverse("Hello"); +``` + +### 运行演示 + +```bash +# 构建项目 +dotnet build + +# 运行演示程序 +dotnet run +``` + +--- + +## 🔌 API 参考 + +### Parser 静态类 + +#### 生成方法 + +| 方法 | 描述 | 参数 | +|------|------|------| +| `Generate()` | 从插件信息列表生成程序集 | `plugins`, `assemblyName`, `useCache` | +| `GenerateFromJson()` | 从 JSON 字符串生成程序集 | `jsonString`, `assemblyName`, `useCache` | +| `GenerateFromFileAsync()` | 从 JSON 文件异步生成程序集 | `jsonFilePath`, `assemblyName`, `useCache` | + +#### 配置方法 + +| 方法 | 描述 | +|------|------| +| `SetPluginManager()` | 设置插件管理器实例 | `IPluginManager pluginManager` | +| `ClearCache()` | 清除所有缓存 | +| `GetCacheStatistics()` | 获取缓存统计信息 | + +#### 分析方法 + +| 方法 | 描述 | +|------|------| +| `GetPluginTypes()` | 获取程序集中的所有插件类型 | +| `GetPluginMethods()` | 获取插件类型的所有方法信息 | + +--- + +## 📊 类型映射支持 + +| JSON 字符串 | CLR 类型 | 示例 | +|---|---|---| +| `"void"` | `System.Void` | 无返回值 | +| `"bool"` | `System.Boolean` | 布尔参数 | +| `"int"` | `System.Int32` | 整数参数 | +| `"double"` | `System.Double` | 浮点数参数 | +| `"string"` | `System.String` | 字符串参数 | +| `"object"` | `System.Object` | 对象参数 | + +--- + +## 🏗️ 项目结构 + +``` +KitX.CSharp.Parser/ +├─ 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 # 详细使用指南 +``` + +--- + +## 🎯 运行结果示例 + +``` +🚀 欢迎使用 KitX.CSharp.Parser 演示程序! + +=== 测试简化后的 Parser 功能 === +1. 测试默认插件管理器设置... + ✓ 默认 MockPluginManager 工作正常 + +2. 测试自定义插件管理器设置... + ✓ 自定义插件管理器设置工作正常 + +3. 测试空参数处理... + ✓ 空参数正确抛出 ArgumentNullException + +4. 测试插件存在性检测... + ✓ SampleCalculator 存在: True + ✓ SampleCalculator.Add 方法存在: True + ✓ NonExistentPlugin 存在: False + ✓ SampleCalculator.NonExistentMethod 方法存在: False + ✓ 插件存在性检测功能工作正常 + +=== 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 个程序集 + +5. 测试缓存效果... + 第二次生成是否使用缓存: True + 强制重新生成是否使用新程序集: True + 重新生成后缓存存在: True + +✅ 所有示例运行完成! +``` + +--- + +## 🔧 实际插件管理器集成 + +### 设置真实插件管理器 + +```csharp +// 在 KitX Dashboard 启动时设置 +Parser.SetPluginManager(new RealPluginManager( + KitX.Dashboard.Network.PluginsNetwork.PluginsServer.Instance, + message => Log.Information(message) +)); +``` + +### 在脚本中使用 + +```csharp +// 现在可以直接调用,将通过真实的 KitX Dashboard 插件系统执行 +var result = SampleCalculator.Add(10, 20); +``` + +--- + +## 📝 注意事项 + +- ✅ **简化设计**:采用直接依赖注入,移除了复杂的工厂函数模式 +- ✅ **必须初始化**:使用前必须调用 `SetPluginManager()` 设置插件管理器 +- ✅ **插件验证**:生成程序集前会检查所需插件是否存在 +- ✅ **缓存优化**:相同插件清单只生成一次,提升性能 +- ✅ **异常安全**:完善的参数验证和错误处理机制 + +--- + +## 🎉 总结 + +KitX.CSharp.Parser 提供了一个简洁、高效、易维护的解决方案,用于将插件清单转换为可直接调用的 C# API。通过简化的设计和完善的错误处理,确保了在生产环境中的稳定性和可靠性。 diff --git a/KitX Script/Kscript.CSharp.Parser/USAGE.md b/KitX Script/Kscript.CSharp.Parser/USAGE.md new file mode 100644 index 0000000..7e0f2e7 --- /dev/null +++ b/KitX Script/Kscript.CSharp.Parser/USAGE.md @@ -0,0 +1,271 @@ +# KitX.CSharp.Parser 使用指南 + +## 📖 概述 + +KitX.CSharp.Parser 是 KitX 工作流子系统的核心组件,它能够将插件清单 JSON 即时编译成可直接调用的 C# 静态 API。通过动态 IL 生成技术,让脚本引擎可以像调用普通静态方法一样使用插件功能。 + +## 🚀 快速开始 + +### 基本用法 + +```csharp +using Kscript.CSharp.Parser; + +// 1. 设置插件管理器(必须) +Parser.SetPluginManager(new MockPluginManager()); + +// 2. 从 JSON 字符串生成程序集 +var jsonString = File.ReadAllText("plugins.json"); +var assembly = Parser.GenerateFromJson(jsonString); + +// 3. 从文件生成程序集 +var assembly = await Parser.GenerateFromFileAsync("plugins.json"); + +// 4. 从插件信息列表生成程序集 +var plugins = new List { /* ... */ }; +var assembly = Parser.Generate(plugins); + +// 5. 脚本中直接调用生成的方法 +int sum = SampleCalculator.Add(10, 20); +string reversed = StringToolkit.Reverse("Hello"); +``` + +### 运行演示 + +```bash +# 构建项目 +dotnet build + +# 运行演示程序 +dotnet run +``` + +## 🔧 API 参考 + +### Parser 静态类 + +#### 生成方法 + +| 方法 | 描述 | 参数 | +|------|------|------| +| `Generate()` | 从插件信息列表生成程序集 | `plugins`, `assemblyName`, `useCache` | +| `GenerateFromJson()` | 从 JSON 字符串生成程序集 | `jsonString`, `assemblyName`, `useCache` | +| `GenerateFromFileAsync()` | 从 JSON 文件异步生成程序集 | `jsonFilePath`, `assemblyName`, `useCache` | + +#### 配置方法 + +| 方法 | 描述 | +|------|------| +| `SetPluginManager()` | 设置插件管理器实例 | `IPluginManager pluginManager` | +| `ClearCache()` | 清除所有缓存 | +| `GetCacheStatistics()` | 获取缓存统计信息 | + +#### 分析方法 + +| 方法 | 描述 | +|------|------| +| `GetPluginTypes()` | 获取程序集中的所有插件类型 | +| `GetPluginMethods()` | 获取插件类型的所有方法信息 | +| `HasCache()` | 检查是否存在缓存 | + +## 📊 类型映射系统 + +### 支持的基础类型 + +| JSON 字符串 | CLR 类型 | 示例 | +|---|---|---| +| `"void"` | `System.Void` | 无返回值 | +| `"bool"` | `System.Boolean` | 布尔参数 | +| `"int"` | `System.Int32` | 整数参数 | +| `"double"` | `System.Double` | 浮点数参数 | +| `"string"` | `System.String` | 字符串参数 | +| `"object"` | `System.Object` | 对象参数 | + +## 🏗️ 架构设计 + +### 核心组件 + +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.Generate(plugins, useCache: false); +} +``` + +## 🚨 错误处理 + +### 异常类型 + +- `ParserException` - 解析器相关异常 +- `ArgumentException` - 参数错误 +- `FileNotFoundException` - 文件不存在 +- `InvalidOperationException` - 初始化状态错误 + +### 异常处理示例 + +```csharp +try +{ + var assembly = Parser.GenerateFromJson(jsonString); +} +catch (ParserException ex) +{ + Console.WriteLine($"解析失败: {ex.Message}"); +} +catch (JsonException ex) +{ + Console.WriteLine($"JSON 格式错误: {ex.Message}"); +} +``` + +## 🔍 调试功能 + +### 日志输出 + +使用 `MockPluginManager` 时会输出详细的调用日志: + +``` +[MockPluginManager] 调用插件方法: SampleCalculator.Add(10, 20) +[MockPluginManager] 参数类型: [Int32, Int32] +[MockPluginManager] 期望返回类型: Int32 +[MockPluginManager] 返回结果: 30 +``` + +### 缓存统计 + +```csharp +var stats = Parser.GetCacheStatistics(); +Console.WriteLine($"缓存统计: {stats.CachedAssemblyCount} 个程序集"); +``` + +## 🔌 扩展点 + +### 自定义插件管理器 + +```csharp +public class CustomPluginManager : IPluginManager +{ + public T Call(PluginCallInfo callInfo) + { + // 实现自定义调用逻辑 + return default(T); + } + + // 实现其他接口方法... +} + +// 设置自定义管理器 +Parser.SetPluginManager(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` - 控制台演示程序 +- `RealPluginManagerExample.cs` - "真实"插件管理器示例 + +## 🎯 总结 + +KitX.CSharp.Parser 提供了一个简洁、高效、易维护的解决方案,用于将插件清单转换为可直接调用的 C# API。通过简化的设计和完善的错误处理,确保了在生产环境中的稳定性和可靠性。 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/Interfaces/IComposer.cs b/KitX Script/Kscript.CSharp/Interfaces/IComposer.cs new file mode 100644 index 0000000..d051b2e --- /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..029b635 --- /dev/null +++ b/KitX Script/Kscript.CSharp/Interfaces/IDevice.cs @@ -0,0 +1,14 @@ +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> GetPluginList(); + bool HasPlugin(string idOrName); + Task CreatePluginInstance(PluginInfo info); + } +} 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/Interfaces/IFunction.cs b/KitX Script/Kscript.CSharp/Interfaces/IFunction.cs new file mode 100644 index 0000000..3f71963 --- /dev/null +++ b/KitX Script/Kscript.CSharp/Interfaces/IFunction.cs @@ -0,0 +1,12 @@ +using KitX.Shared.CSharp.Plugin; + +namespace Kscript.CSharp.Interfaces +{ + public interface IFunction + { + Function Info { get; } + PluginInfo AssociatedPlugin { get; } + 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 new file mode 100644 index 0000000..2e290d0 --- /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 string[] 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..c2dce81 --- /dev/null +++ b/KitX Script/Kscript.CSharp/Kscript.CSharp.csproj @@ -0,0 +1,28 @@ + + + + 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/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 diff --git a/KitX Script/Kscript.CSharp/Services/Composer.cs b/KitX Script/Kscript.CSharp/Services/Composer.cs new file mode 100644 index 0000000..0f1f520 --- /dev/null +++ b/KitX Script/Kscript.CSharp/Services/Composer.cs @@ -0,0 +1,264 @@ +using KitX.Shared.CSharp.Device; +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, 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); + } + } + + /// + /// 移除设备事件监听器 + /// + 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(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, _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.Any()) + return null; + + var random = new Random(); + var randomDevice = desktopDevices[random.Next(desktopDevices.Count)]; + 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.Any()) + return null; + + var random = new Random(); + var randomDevice = mobileDevices[random.Next(mobileDevices.Count)]; + 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, _connector) : null; + } + + /// + /// 获取用户选择的设备 + /// + public async Task RequestUserSelectedDevice(IEnumerable candidates) + { + var response = await _requestBuilder + .WithFunction("SelectDevice", candidates) + .ExecuteAsync(async response => + { + var result = await ResponseHandler.HandleStringResponse(response); + if (string.IsNullOrEmpty(result)) + return 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() + { + 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); + + return result ?? new List(); + }); + + return response; + }); + } + + private void UpdateDeviceStates(IEnumerable newDevices) + { + if (_isDisposed) + return; + + lock (_lockObject) + { + 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) + { + 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}"); + } + } + } + } + + private bool HasDeviceChanged(DeviceInfo oldDevice, DeviceInfo newDevice) + { + // 检查设备在线状态(基于最后一次发送时间) + var oldDeviceOnline = DateTime.UtcNow - oldDevice.SendTime < DeviceOfflineThreshold; + var newDeviceOnline = DateTime.UtcNow - newDevice.SendTime < DeviceOfflineThreshold; + + 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; + } + + public void Dispose() + { + if (_isDisposed) + return; + + _isDisposed = true; + _deviceCache.Dispose(); + lock (_lockObject) + { + _listeners.Clear(); + _lastKnownDevices.Clear(); + } + } + } +} diff --git a/KitX Script/Kscript.CSharp/Services/Device.cs b/KitX Script/Kscript.CSharp/Services/Device.cs new file mode 100644 index 0000000..3aef2b5 --- /dev/null +++ b/KitX Script/Kscript.CSharp/Services/Device.cs @@ -0,0 +1,124 @@ +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 +{ + public class Device : IDevice + { + private readonly Connector _connector; + private Dictionary _pluginCache = new(); + + public DeviceInfo Info { get; } + + public Device(DeviceInfo info, Connector? connector = null) + { + Info = info; + _connector = connector ?? Connector.Instance; + } + + public async Task RequestPlugin(string Name) + { + var plugins = await GetPluginList(); + var pluginInfo = plugins.FirstOrDefault(p => + p.Name.Equals(Name, StringComparison.OrdinalIgnoreCase)); + + return pluginInfo != null ? await CreatePluginInstance(pluginInfo) : null!; + } + + 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.ContainsKey(Name); + } + + 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..a50a068 --- /dev/null +++ b/KitX Script/Kscript.CSharp/Services/Function.cs @@ -0,0 +1,156 @@ +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 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, Connector? connector = null) + { + Info = info; + AssociatedPlugin = pluginInfo; + _deviceInfo = deviceInfo; + _connector = connector ?? Connector.Instance; + } + + public async Task Invoke(params string[] parameters) + { + _connector.Request() + .UpdateCommand(cmd => + { + cmd.FunctionName = Info.Name; + cmd.FunctionArgs = parameters.Select(p => new Parameter { Value = p }).ToList(); + return cmd; + }) + .UpdateRequest(req => + { + req.Target = _deviceInfo.Device; + return req; + }) + .Send(); + + 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() + { + // 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 new file mode 100644 index 0000000..0ab0a82 --- /dev/null +++ b/KitX Script/Kscript.CSharp/Services/Plugin.cs @@ -0,0 +1,182 @@ +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 Connector _connector; + + public PluginInfo Info { get; } + public DeviceInfo AssociatedDevice { get; } + + public Plugin(PluginInfo info, DeviceInfo deviceInfo, Connector? connector = null) + { + Info = info; + AssociatedDevice = deviceInfo; + _connector = connector ?? Connector.Instance; + } + + public async Task RequestFunction(string Name) + { + var functions = await GetFunctionList(); + var function = functions.FirstOrDefault(f => + 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); + return await Task.FromResult(functions); + } + + public async Task GetFunctionByType(string type) + { + var functions = await GetFunctionList(); + return functions.FirstOrDefault(f => + f.Info.ReturnValueType.Equals(type, StringComparison.OrdinalIgnoreCase)); + } + + public async Task ExecuteFunction(string functionName, params string[] parameters) + { + _connector.Request() + .UpdateCommand(cmd => + { + cmd.FunctionName = functionName; + cmd.FunctionArgs = parameters.Select(p => new Parameter { Value = p }).ToList(); + return cmd; + }) + .UpdateRequest(req => + { + req.Target = AssociatedDevice.Device; + return req; + }) + .Send(); + + // 处理响应 + 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.Equals(default(Function))) + 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) + { + return Info.Functions.Any(f => + f.Name.Equals(Name, StringComparison.OrdinalIgnoreCase)); + } + } +} diff --git a/KitX Script/Kscript.CSharp/Utils/DeviceCache.cs b/KitX Script/Kscript.CSharp/Utils/DeviceCache.cs new file mode 100644 index 0000000..3f8989e --- /dev/null +++ b/KitX Script/Kscript.CSharp/Utils/DeviceCache.cs @@ -0,0 +1,191 @@ +using System.Collections.Concurrent; +using KitX.Shared.CSharp.Device; + +namespace Kscript.CSharp.Utils +{ + /// + /// 设备信息缓存管理器 + /// + public class DeviceCache : IDisposable + { + private class CacheEntry + { + public required 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) + { + if (!_cache.TryGetValue(key, out var entry) || entry.IsExpired) + { + _cache.TryRemove(key, out _); + return null; + } + + entry.IncrementAccess(); + return entry.Value; + } + + /// + /// 检查缓存项是否存在且有效 + /// + public bool IsValid(string key) + { + return _cache.TryGetValue(key, out var entry) && !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..54b3ea0 --- /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() ?? new() + }; + 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..6bfa4ac --- /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 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); + } + } +} 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 @@ + + + + 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; + } } 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; +}