diff --git a/KitX Dashboard/Docs/Architecture/Detiled-Plan.md b/KitX Dashboard/Docs/Architecture/Detiled-Plan.md new file mode 100644 index 0000000..905660e --- /dev/null +++ b/KitX Dashboard/Docs/Architecture/Detiled-Plan.md @@ -0,0 +1,372 @@ +# KitX Dashboard 前后端分离详细重构分析报告 + +## 1. 项目现状总结 + +基于对整个KitX Dashboard项目的深入分析,我发现这是一个典型的单体应用架构,所有业务逻辑、数据管理、网络通信和UI逻辑都耦合在一个项目中。 + +### 1.1 核心架构问题 +- **高度耦合**: UI逻辑与业务逻辑混杂在ViewModels中 +- **直接依赖**: ViewModels直接调用Manager类和静态实例 +- **配置混乱**: 前端UI配置与后端业务配置混合在同一配置文件中 +- **网络通信**: 所有网络服务都运行在前端应用进程中 + +## 2. 需要分离到后端(Core项目)的逻辑 + +### 2.1 管理器层 (`Managers/`) - 完全迁移 + +**核心业务管理器:** +- [`ManagerBase.cs`](KitX Dashboard/Managers/ManagerBase.cs:5) - 需重构为服务基类 +- [`PluginsManager.cs`](KitX Dashboard/Managers/PluginsManager.cs:16) - 插件生命周期管理 + - `ImportPlugin()` 方法 (第20行) - 插件导入逻辑 + - 插件安装和卸载功能 +- [`SecurityManager.cs`](KitX Dashboard/Managers/SecurityManager.cs:15) - 安全管理核心 + - RSA加密解密 (`EncryptString`, `DecryptString`) + - 设备密钥管理 (`AddDeviceKey`, `RemoveDeviceKey`) + - 设备授权验证 (`IsDeviceAuthorized`) +- [`ConfigManager.cs`](KitX Dashboard/Managers/ConfigManager.cs:12) - 配置管理系统 + - 配置文件热重载机制 + - 多配置文件管理 (`LoadConfigFile()`) +- [`ActivityManager.cs`](KitX Dashboard/Managers/ActivityManager.cs:12) - 活动日志管理 + - LiteDB数据库操作 + - 活动记录和查询功能 +- [`StatisticsManager.cs`](KitX Dashboard/Managers/StatisticsManager.cs:13) - 统计数据处理 + - 使用统计数据收集 + - 数据持久化存储 +- [`WebManager.cs`](KitX Dashboard/Managers/WebManager.cs:11) - 网络服务管理 + - 服务器启动/停止控制 + - 网络服务协调 +- [`TasksManager.cs`](KitX Dashboard/Managers/TasksManager.cs:7) - 任务调度 +- [`AnnouncementManager.cs`](KitX Dashboard/Managers/AnnouncementManager.cs:16) - 公告管理 + - HTTP API调用逻辑 (`CheckNewAnnouncements`) + +### 2.2 网络通信层 (`Network/`) - 完全迁移 + +**设备网络通信:** +- [`DevicesServer.cs`](KitX Dashboard/Network/DevicesNetwork/DevicesServer.cs:20) - ASP.NET Core Web API服务器 + - 设备连接管理 + - JWT令牌管理 + - API控制器集成 +- [`DevicesDiscoveryServer.cs`](KitX Dashboard/Network/DevicesNetwork/DevicesDiscoveryServer.cs:20) - UDP广播服务 + - 多播网络发现 (`MultiDevicesBroadCastSend`, `MultiDevicesBroadCastReceive`) + - 网络接口管理 (`FindSupportNetworkInterfaces`) +- [`DevicesOrganizer.cs`](KitX Dashboard/Network/DevicesNetwork/DevicesOrganizer.cs) - 设备组织管理 +- API控制器 (`DevicesServerControllers/V1/`) + +**插件网络通信:** +- [`PluginsServer.cs`](KitX Dashboard/Network/PluginsNetwork/PluginsServer.cs:13) - WebSocket服务器 + - Fleck WebSocket实现 + - 插件连接管理 +- [`PluginConnector.cs`](KitX Dashboard/Network/PluginsNetwork/PluginConnector.cs:16) - 插件连接器 + - 插件注册处理 + - 消息协议处理 + +**网络工具:** +- [`NetworkHelper.cs`](KitX Dashboard/Network/NetworkHelper.cs:17) - 网络工具类 + - IP地址获取 (`GetInterNetworkIPv4`, `GetInterNetworkIPv6`) + - 设备信息构建 (`GetDeviceInfo`) + - MAC地址获取 (`TryGetDeviceMacAddress`) + +### 2.3 数据模型层 (`Models/`) - 部分迁移 + +**业务实体模型:** +- [`PluginInstallation.cs`](KitX Dashboard/Models/PluginInstallation.cs:9) - 插件安装信息 +- [`WorkflowCase.cs`](KitX Dashboard/Models/WorkflowCase.cs:5) - 工作流定义 +- [`Component.cs`](KitX Dashboard/Models/Component.cs:3) - 组件信息 + +**注意**: [`DeviceCase.cs`](KitX Dashboard/Models/DeviceCase.cs:22) 需要重构,其中包含大量UI逻辑和业务逻辑的混合 + +### 2.4 配置管理层 (`Configuration/`) - 部分迁移 + +**核心配置基础设施:** +- [`ConfigBase.cs`](KitX Dashboard/Configuration/ConfigBase.cs:16) - 配置基类和序列化扩展 +- [`ConfigFetcher.cs`](KitX Dashboard/Configuration/ConfigFetcher.cs:5) - 配置获取器基类 + +**业务配置 (需迁移到Core):** +- [`SecurityConfig.cs`](KitX Dashboard/Configuration/SecurityConfig.cs) - 安全配置 +- [`PluginsConfig.cs`](KitX Dashboard/Configuration/PluginsConfig.cs) - 插件配置 +- [`MarketConfig.cs`](KitX Dashboard/Configuration/MarketConfig.cs) - 市场配置 +- [`AnnouncementConfig.cs`](KitX Dashboard/Configuration/AnnouncementConfig.cs) - 公告配置 + +**UI配置 (保留在前端):** +- [`AppConfig.cs`](KitX Dashboard/Configuration/AppConfig.cs:10) 中的UI相关部分: + - `Config_Windows` (第65行) - 窗口配置 + - `Config_Pages` (第106行) - 页面配置 + - 主题和语言设置 + +### 2.5 服务层 (`Services/`) - 部分迁移 + +**后端服务 (迁移到Core):** +- [`DebugService.cs`](KitX Dashboard/Services/DebugService.cs:13) 中的代码执行逻辑 +- [`EventService.cs`](KitX Dashboard/Services/EventService.cs:7) 中的业务事件部分 + +## 3. 保留在前端(Dashboard项目)的逻辑 + +### 3.1 视图层 (`Views/`) - 完全保留 +- 所有AXAML视图文件 +- 视图代码后置文件 +- UI控件和样式 + +### 3.2 视图模型层 (`ViewModels/`) - 重构保留 + +**需要重构的ViewModels:** +- [`ViewModelBase.cs`](KitX Dashboard/ViewModels/ViewModelBase.cs:10) - 移除直接的ConfigManager依赖 (第47行) +- [`AppViewModel.cs`](KitX Dashboard/ViewModels/AppViewModel.cs:13) - 移除对AnnouncementManager的直接调用 (第44行) +- [`MainWindowViewModel.cs`](KitX Dashboard/ViewModels/MainWindowViewModel.cs:8) - 基本结构可保留 +- [`DevicesPageViewModel.cs`](KitX Dashboard/ViewModels/Pages/DevicesPageViewModel.cs:10) - 移除对WebManager的直接依赖 (第23行) +- [`HomePageViewModel.cs`](KitX Dashboard/ViewModels/Pages/HomePageViewModel.cs:9) - 移除对ConfigManager的直接访问 (第38行) + +### 3.3 UI专用组件 +- 转换器 (`Converters/`) +- 样式和主题 (`Styles/`) +- 语言资源 (`Languages/`) +- UI相关的工具类 + +## 4. 详细重构实施计划 + +### 阶段1: Core项目基础架构重构 + +**1.1 项目类型转换** +```xml + + + + net8.0 + Library + + +``` + +**1.2 创建服务接口层** +``` +KitX Dashboard Core/ +├── Interfaces/ +│ ├── IPluginService.cs // 插件管理服务接口 +│ ├── ISecurityService.cs // 安全管理服务接口 +│ ├── IConfigurationService.cs // 配置管理服务接口 +│ ├── IActivityService.cs // 活动日志服务接口 +│ ├── IStatisticsService.cs // 统计服务接口 +│ ├── INetworkService.cs // 网络服务接口 +│ ├── IDeviceService.cs // 设备管理服务接口 +│ └── IAnnouncementService.cs // 公告服务接口 +``` + +**1.3 依赖注入容器配置** +```csharp +// ServiceCollectionExtensions.cs +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddKitXCore(this IServiceCollection services) + { + // 注册核心服务 + services.AddSingleton(); + services.AddSingleton(); + // ... 其他服务注册 + return services; + } +} +``` + +### 阶段2: 业务逻辑迁移重构 + +**2.1 Manager to Service 重构模式** + +以PluginsManager为例: +```csharp +// 原 PluginsManager.cs (第16行) +internal class PluginsManager +{ + internal static void ImportPlugin(string[] kxpfiles, bool inGraphic = false) + { + // 业务逻辑... + } +} + +// 重构后的 IPluginService.cs +public interface IPluginService +{ + Task ImportPluginAsync(string[] kxpFiles, bool inGraphic = false); + Task> GetInstalledPluginsAsync(); + Task UninstallPluginAsync(string pluginId); +} + +// 重构后的 PluginService.cs +public class PluginService : IPluginService +{ + private readonly IConfigurationService _configService; + private readonly ILogger _logger; + + public async Task ImportPluginAsync(string[] kxpFiles, bool inGraphic = false) + { + // 从原PluginsManager.ImportPlugin迁移的逻辑 + } +} +``` + +**2.2 配置管理重构** + +分离业务配置和UI配置: +```csharp +// Core项目中的业务配置 +public class CoreAppConfig +{ + public PluginConfig Plugins { get; set; } + public SecurityConfig Security { get; set; } + public NetworkConfig Network { get; set; } + public LoggingConfig Logging { get; set; } +} + +// Dashboard项目中的UI配置 +public class UIConfig +{ + public WindowConfig Windows { get; set; } + public ThemeConfig Theme { get; set; } + public LanguageConfig Language { get; set; } +} +``` + +**2.3 网络服务重构** + +```csharp +// INetworkService.cs +public interface INetworkService +{ + Task StartDevicesServerAsync(); + Task StopDevicesServerAsync(); + Task StartPluginsServerAsync(); + Task StopPluginsServerAsync(); + Task GetLocalDeviceInfoAsync(); +} +``` + +### 阶段3: 前端架构重构 + +**3.1 Service Proxy层创建** +``` +KitX Dashboard/ +├── ServiceProxies/ +│ ├── IServiceProxy.cs // 服务代理基接口 +│ ├── PluginServiceProxy.cs // 插件服务代理 +│ ├── SecurityServiceProxy.cs // 安全服务代理 +│ ├── DeviceServiceProxy.cs // 设备服务代理 +│ └── ConfigServiceProxy.cs // 配置服务代理 +``` + +**3.2 ViewModels重构模式** + +以DevicesPageViewModel为例: +```csharp +// 重构前 (第23行直接依赖WebManager) +if (Instances.WebManager is null) return; +await Instances.WebManager.RestartAsync(...) + +// 重构后 (通过服务代理) +public class DevicesPageViewModel : ViewModelBase +{ + private readonly IDeviceServiceProxy _deviceService; + + public DevicesPageViewModel(IDeviceServiceProxy deviceService) + { + _deviceService = deviceService; + } + + private async Task RestartDevicesServerAsync() + { + await _deviceService.RestartDevicesServerAsync(); + } +} +``` + +### 阶段4: 通信层设计 + +**4.1 API接口设计** +```csharp +// REST API 设计 +[ApiController] +[Route("api/v1/[controller]")] +public class PluginsController : ControllerBase +{ + [HttpGet] + public async Task GetInstalledPlugins() + + [HttpPost("import")] + public async Task ImportPlugin([FromBody] ImportPluginRequest request) + + [HttpDelete("{id}")] + public async Task UninstallPlugin(string id) +} +``` + +**4.2 实时通信 (SignalR)** +```csharp +// 用于实时状态更新 +public class DashboardHub : Hub +{ + public async Task JoinGroup(string groupName) + public async Task LeaveGroup(string groupName) +} + +// 事件推送 +public interface IDashboardEventNotifier +{ + Task NotifyPluginInstalled(PluginInfo plugin); + Task NotifyDeviceConnected(DeviceInfo device); + Task NotifyConfigurationChanged(string configType); +} +``` + +## 5. 关键重构要点 + +### 5.1 配置管理分离策略 +- **业务配置** → Core项目 (插件、安全、网络配置) +- **UI配置** → Dashboard项目 (窗口、主题、语言配置) +- **共享配置** → 通过API同步 + +### 5.2 事件系统重构 +- 将[`EventService.cs`](KitX Dashboard/Services/EventService.cs:7)分离为: + - **Core事件**: 业务逻辑事件 (插件安装、设备连接等) + - **UI事件**: 界面交互事件 (主题变更、语言切换等) + +### 5.3 数据持久化分离 +- **LiteDB数据库** → Core项目管理 +- **UI状态缓存** → Dashboard项目管理 + +### 5.4 安全架构重构 +- [`SecurityManager.cs`](KitX Dashboard/Managers/SecurityManager.cs:15) → Core项目 +- 设备密钥管理完全在后端处理 +- 前端只处理UI层的身份验证 + +## 6. 实施建议 + +### 6.1 分阶段实施 +1. **第一阶段**: 完成Core项目基础架构搭建 +2. **第二阶段**: 迁移核心业务逻辑 (插件管理、安全管理) +3. **第三阶段**: 迁移网络通信层 +4. **第四阶段**: 重构前端ViewModels +5. **第五阶段**: 完善API接口和测试 + +### 6.2 向下兼容策略 +- 保持现有配置文件格式兼容 +- 逐步迁移现有数据 +- 提供配置迁移工具 + +### 6.3 测试策略 +- **单元测试**: Core项目的业务逻辑 +- **集成测试**: API接口测试 +- **UI测试**: 前端功能测试 +- **端到端测试**: 完整流程测试 + +## 7. 预期收益 + +### 7.1 架构优势 +- **职责分离**: 前后端各司其职 +- **可扩展性**: 支持多种前端实现 +- **可维护性**: 业务逻辑集中管理 +- **可测试性**: 独立的业务逻辑测试 + +### 7.2 技术优势 +- **性能优化**: 后端可独立优化 +- **并发处理**: 更好的并发控制 +- **资源管理**: 独立的资源管理 +- **部署灵活**: 前后端独立部署 + +这个详细的重构分析为KitX Dashboard的前后端分离提供了清晰的路线图,确保重构过程的系统性和可执行性。建议按照分阶段实施的策略,逐步完成整个重构过程,以降低风险并保证系统的稳定性。 diff --git a/KitX Dashboard/Docs/Architecture/Separation-Plan.md b/KitX Dashboard/Docs/Architecture/Separation-Plan.md new file mode 100644 index 0000000..fca74f9 --- /dev/null +++ b/KitX Dashboard/Docs/Architecture/Separation-Plan.md @@ -0,0 +1,83 @@ +# KitX Dashboard 前后端分离架构设计文档 + +## 1. 概述 + +本方案将KitX Dashboard拆分为: +- **前端**:KitX Dashboard (UI层) +- **后端**:KitX Dashboard Core (业务逻辑层) + +## 2. 架构设计 + +```mermaid +graph TD + subgraph "KitX Dashboard (前端)" + A[Views] --> B[ViewModels] + B --> C[Service Interfaces] + C --> D[Core API] + end + + subgraph "KitX Dashboard Core (后端)" + D --> E[Business Services] + E --> F[Managers] + F --> G[Data Models] + end +``` + +## 3. 职责划分 + +### 后端(Core项目) +- 所有业务逻辑处理 +- 数据管理和持久化 +- 网络通信 +- 插件管理 +- 安全配置 +- 统计和日志 + +### 前端(Dashboard项目) +- 用户界面展示 +- 用户交互处理 +- 视图状态管理 +- 通过接口调用后端服务 + +## 4. 实施步骤 + +1. **重构Core项目** + - 改为类库项目 + - 设计服务接口层 + - 建立DI容器 + +2. **代码迁移** + - 迁移Models、Managers、Services等 + +3. **接口设计** + - 定义服务接口 + - 实现服务类 + - 创建服务工厂 + +4. **前端适配** + - 重构ViewModels + - 实现服务代理 + - 更新DI配置 + +## 5. 目录结构 + +``` +KitX Dashboard Core/ +├── Interfaces/ +├── Services/ +├── Models/ +├── Managers/ +└── Extensions/ + +KitX Dashboard/ +├── Views/ +├── ViewModels/ +├── ServiceProxies/ +└── Locators/ +``` + +## 6. 后续计划 + +- 按阶段实施迁移 +- 逐步测试验证 +- 完善文档 diff --git a/KitX Dashboard/KitX.Dashboard.csproj b/KitX Dashboard/KitX.Dashboard.csproj index 513fbd8..18d66fc 100644 --- a/KitX Dashboard/KitX.Dashboard.csproj +++ b/KitX Dashboard/KitX.Dashboard.csproj @@ -1,7 +1,7 @@  WinExe - net8.0 + net10.0 enable true Assets\KitX-Icon-256x.ico @@ -113,6 +113,7 @@ + diff --git a/KitX Dashboard/Models/WorkflowCase.cs b/KitX Dashboard/Models/WorkflowCase.cs index 6296f96..19c77a8 100644 --- a/KitX Dashboard/Models/WorkflowCase.cs +++ b/KitX Dashboard/Models/WorkflowCase.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace KitX.Dashboard.Models { @@ -8,5 +8,6 @@ public class WorkflowCase public required string Description { get; set; } // 简介信息 public required string IconPath { get; set; } // 图标路径 public bool IsRunning { get; set; } // 运行状态 + public string ScriptPath { get; set; } = string.Empty; // 脚本文件路径 } -} +} \ No newline at end of file diff --git a/KitX Dashboard/Network/PluginsNetwork/PluginConnector.cs b/KitX Dashboard/Network/PluginsNetwork/PluginConnector.cs index 9ed015f..d2a3fdd 100644 --- a/KitX Dashboard/Network/PluginsNetwork/PluginConnector.cs +++ b/KitX Dashboard/Network/PluginsNetwork/PluginConnector.cs @@ -38,6 +38,11 @@ public class PluginConnector public event PluginStatusUpdatedHandler PluginStatusUpdated = new(() => { }); + /// + /// 插件响应事件 - 当接收到插件功能调用的响应时触发 + /// + public static event Action? OnPluginResponse; + public PluginConnector() { } public PluginConnector(IWebSocketConnection socket) @@ -125,6 +130,15 @@ public PluginConnector Run() var command = JsonSerializer.Deserialize(kwc.Content, serializerOptions); + // 检查是否是功能调用的响应(通过RequestId标签) + if (command.Tags != null && + command.Tags.TryGetValue("RequestId", out var requestId)) + { + // 触发插件响应事件 + OnPluginResponse?.Invoke(requestId, kwc.Content); + return; + } + switch (command.Request) { case CommandRequestInfo.RegisterPlugin: diff --git a/KitX Dashboard/Services/PluginServiceProvider.cs b/KitX Dashboard/Services/PluginServiceProvider.cs new file mode 100644 index 0000000..ab229e4 --- /dev/null +++ b/KitX Dashboard/Services/PluginServiceProvider.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using KitX.Dashboard.Network.PluginsNetwork; +using KitX.Dashboard.Views; +using KitX.Shared.CSharp.Plugin; +using KitX.Shared.CSharp.WebCommand; +using Kscript.CSharp.Parser.Core; +using Serilog; + +namespace KitX.Dashboard.Services; + +/// +/// 插件服务提供者实现 - 连接Dashboard和Parser +/// +public class PluginServiceProvider : IPluginServiceProvider +{ + private readonly PluginsServer _pluginsServer; + + /// + /// 构造函数 + /// + /// 插件服务器实例 + public PluginServiceProvider(PluginsServer pluginsServer) + { + _pluginsServer = pluginsServer ?? throw new ArgumentNullException(nameof(pluginsServer)); + } + + /// + /// 获取所有运行中的插件信息 + /// + public IEnumerable GetRunningPlugins() + { + return ViewInstances.PluginInfos; + } + + /// + /// 根据插件名称查找插件信息 + /// + public PluginInfo? FindPlugin(string pluginName) + { + try + { + return ViewInstances.PluginInfos + .FirstOrDefault(p => p.Name == pluginName); + } + catch (Exception ex) + { + Log.Error($"[PluginServiceProvider] 查找插件失败: {pluginName} - {ex.Message}"); + return null; + } + } + + /// + /// 查找插件连接器 + /// + public object? FindConnector(PluginInfo pluginInfo) + { + try + { + return _pluginsServer.FindConnector(pluginInfo); + } + catch (Exception ex) + { + Log.Error($"[PluginServiceProvider] 查找连接器失败: {pluginInfo.Name} - {ex.Message}"); + return null; + } + } + + /// + /// 向插件连接器发送请求 + /// + public async Task SendRequestAsync(object connector, object request) + { + try + { + if (connector is PluginConnector pluginConnector && request is Request req) + { + // PluginConnector.Request 是 async void 方法,不能直接 await + // 我们需要使用 Task.Run 来包装它 + await Task.Run(() => pluginConnector.Request(req)); + } + else + { + Log.Warning($"[PluginServiceProvider] SendRequestAsync 参数类型不匹配"); + } + } + catch (Exception ex) + { + Log.Error($"[PluginServiceProvider] 发送请求失败 - {ex.Message}"); + } + } + + /// + /// 订阅插件响应事件 + /// + public void SubscribeToResponses(Action responseHandler) + { + try + { + // 订阅PluginConnector的响应事件 + PluginConnector.OnPluginResponse += (requestId, response) => + { + try + { + responseHandler(requestId, response); + } + catch (Exception ex) + { + Log.Error($"[PluginServiceProvider] 响应处理器异常: {ex.Message}"); + } + }; + + Log.Information("[PluginServiceProvider] 已订阅插件响应事件"); + } + catch (Exception ex) + { + Log.Error($"[PluginServiceProvider] 订阅响应事件失败: {ex.Message}"); + } + } +} diff --git a/KitX Dashboard/Services/WorkflowScriptService.cs b/KitX Dashboard/Services/WorkflowScriptService.cs new file mode 100644 index 0000000..4644418 --- /dev/null +++ b/KitX Dashboard/Services/WorkflowScriptService.cs @@ -0,0 +1,377 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Reflection; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Linq; +using Csharpell.Core; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Scripting; +using Kscript.CSharp.Parser; +using KitX.Shared.CSharp.Plugin; +using Serilog; +using KitX.Dashboard.Network.PluginsNetwork; +using KitX.Dashboard.Models; +using KitX.Dashboard.Managers; +using KitX.Dashboard.Configuration; +using KitX.Dashboard.Network.DevicesNetwork; +using Common.BasicHelper.Utils.Extensions; + +namespace KitX.Dashboard.Services; + +public static class WorkflowScriptService +{ + private static CSharpScriptEngine Engine => new(); + + // 存储当前可用的插件列表,用于生成API + private static List AvailablePlugins { get; set; } = new(); + + /// + /// 初始化插件管理器 + /// + public static void InitializePluginManager() + { + if (Parser.IsInitialized) return; // 已经初始化过了 + + try + { + // 创建PluginServiceProvider + var serviceProvider = new PluginServiceProvider(PluginsServer.Instance); + + // 设置到Parser + Parser.SetPluginManager(new Kscript.CSharp.Parser.Core.RealPluginManager( + serviceProvider, + Log.Information, + Log.Error + )); + + Log.Information("[WorkflowScriptService] RealPluginManager 已设置"); + } + catch (Exception ex) + { + Log.Error($"[WorkflowScriptService] 设置插件管理器失败: {ex.Message}"); + // 如果设置失败,使用默认的 MockPluginManager + Parser.SetPluginManager(new Kscript.CSharp.Parser.Core.MockPluginManager()); + } + } + + /// + /// 更新可用插件列表 + /// + /// 插件列表 + public static void UpdateAvailablePlugins(List plugins) + { + AvailablePlugins = plugins ?? new List(); + } + + /// + /// 确保工作流所需的插件已加载并运行 + /// + /// 需要的插件列表 + /// 取消令牌 + /// 是否所有插件都已就绪 + private static async Task EnsurePluginsReadyAsync( + List requiredPlugins, + CancellationToken cancellationToken = default) + { + var success = true; + + foreach (var requiredPlugin in requiredPlugins) + { + // 1. 检查插件是否已安装 + var installedPlugin = PluginsManager.Plugins + .FirstOrDefault(p => p.PluginInfo.Name == requiredPlugin.Name); + + if (installedPlugin == null) + { + Log.Error($"[WorkflowScriptService] 插件 {requiredPlugin.Name} 未安装"); + success = false; + continue; + } + + // 2. 检查插件是否正在运行 + var isRunning = KitX.Dashboard.Views.ViewInstances.PluginInfos + .Any(p => p.Name == requiredPlugin.Name); + + if (!isRunning) + { + Log.Information($"[WorkflowScriptService] 启动插件: {requiredPlugin.Name}"); + + // 3. 启动插件 + StartPlugin(installedPlugin); + + // 4. 等待插件连接(最多10秒) + var connected = await WaitForPluginConnectionAsync( + requiredPlugin.Name, + TimeSpan.FromSeconds(10), + cancellationToken + ); + + if (!connected) + { + Log.Error($"[WorkflowScriptService] 插件 {requiredPlugin.Name} 启动超时"); + success = false; + } + } + else + { + Log.Information($"[WorkflowScriptService] 插件 {requiredPlugin.Name} 已运行"); + } + } + + return success; + } + + /// + /// 启动插件 + /// + /// 插件安装信息 + private static void StartPlugin(PluginInstallation plugin) + { + try + { + var pd = plugin.PluginInfo; + var loaderName = plugin.LoaderInfo.LoaderName; + var loaderVersion = plugin.LoaderInfo.LoaderVersion; + + // 构建插件文件路径 + var pluginPath = $"{plugin.InstallPath}/{pd.RootStartupFileName}"; + var pluginFile = pluginPath.GetFullPath(); + + // 构建 WebSocket 连接字符串 + var connectStr = "ws://" + + $"{DevicesDiscoveryServer.Instance.DefaultDeviceInfo.Device.IPv4}" + + $":{ConstantTable.PluginsServerPort}/"; + + if (plugin.LoaderInfo.SelfLoad) + { + // 自加载插件 + Process.Start(pluginFile, $"--connect {connectStr}"); + Log.Information($"[WorkflowScriptService] 启动自加载插件: {pd.Name}"); + } + else + { + // 通过加载器启动 + var loadersInstallPath = ConfigManager.Instance.AppConfig.Loaders.InstallPath; + var loaderFile = $"{loadersInstallPath}/{loaderName}/{loaderVersion}/{loaderName}"; + + if (OperatingSystem.IsWindows()) + loaderFile += ".exe"; + + var arg = $"--load \"{pluginFile}\" --connect {connectStr}"; + Process.Start(loaderFile, arg); + + Log.Information($"[WorkflowScriptService] 通过加载器启动插件: {pd.Name}"); + } + } + catch (Exception ex) + { + Log.Error($"[WorkflowScriptService] 启动插件失败: {plugin.PluginInfo.Name} - {ex.Message}"); + } + } + + /// + /// 等待插件连接 + /// + /// 插件名称 + /// 超时时间 + /// 取消令牌 + /// 是否连接成功 + private static async Task WaitForPluginConnectionAsync( + string pluginName, + TimeSpan timeout, + CancellationToken cancellationToken) + { + var startTime = DateTime.Now; + var checkInterval = TimeSpan.FromMilliseconds(500); + + while (DateTime.Now - startTime < timeout) + { + if (cancellationToken.IsCancellationRequested) + return false; + + // 检查插件是否已连接 + var isConnected = KitX.Dashboard.Views.ViewInstances.PluginInfos + .Any(p => p.Name == pluginName); + + if (isConnected) + { + Log.Information($"[WorkflowScriptService] 插件 {pluginName} 连接成功"); + return true; + } + + // 等待一段时间再检查 + await Task.Delay(checkInterval, cancellationToken); + } + + return false; + } + + /// + /// 执行工作流脚本代码 + /// + /// 要执行的代码 + /// 需要的插件列表(可选) + /// 是否包含时间戳 + /// 取消令牌 + /// 执行结果 + public static async Task ExecuteCodesAsync( + string code, + List? requiredPlugins = null, + bool includeTimestamp = true, + CancellationToken cancellationToken = default) + { + var sw = new Stopwatch(); + var begin = DateTime.Now; + sw.Start(); + + try + { + // 如果有需要的插件,先确保它们已加载 + if (requiredPlugins != null && requiredPlugins.Any()) + { + // 确保插件管理器已初始化 + if (!Parser.IsInitialized) + InitializePluginManager(); + + // 确保插件已加载并运行 + var pluginsReady = await EnsurePluginsReadyAsync(requiredPlugins, cancellationToken); + + if (!pluginsReady) + { + Log.Warning("[WorkflowScriptService] 部分插件未能成功启动,但继续执行脚本"); + } + + // 生成包含插件API的程序集 + Assembly? pluginApiAssembly = null; + try + { + pluginApiAssembly = Parser.Generate(requiredPlugins, "KitXWorkflowPlugins"); + Log.Information($"[WorkflowScriptService] 成功生成插件API,包含 {requiredPlugins.Count} 个插件"); + } + catch (Exception ex) + { + // 如果插件API生成失败,记录错误但继续执行脚本 + var error = $"Failed to generate plugin API: {ex.Message}"; + Log.Error(error); + } + + // 执行脚本 + var result = ( + await Engine.ExecuteAsync( + code, + options => + { + options = options + .WithReferences(Assembly.GetExecutingAssembly()) + .WithImports( + "KitX", + "KitX.Dashboard", + "KitX.Shared.CSharp.Plugin", + "System", + "System.Collections.Generic", + "System.Threading.Tasks" + ) + .WithLanguageVersion(LanguageVersion.Preview); + + // 如果有插件API程序集,添加为引用 + if (pluginApiAssembly != null) + { + options = options.WithReferences(pluginApiAssembly); + } + + return options; + }, + addDefaultImports: true, + runInReplMode: false, + cancellationToken: cancellationToken + ) + )?.ToString(); + + sw.Stop(); + + return includeTimestamp + ? new StringBuilder() + .AppendLine($"[{begin:yyyy-MM-dd HH:mm:ss}] [I] Workflow script posted.") + .AppendLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] [I] Script ended, took {sw.ElapsedMilliseconds} ms.") + .AppendLine(result) + .ToString() + : result; + } + else + { + // 没有插件依赖,直接执行 + return await ExecuteCodesWithoutPluginsAsync(code, includeTimestamp, cancellationToken); + } + } + catch (Exception e) + { + sw.Stop(); + + return includeTimestamp + ? new StringBuilder() + .AppendLine($"[{begin:yyyy-MM-dd HH:mm:ss}] [I] Workflow script posted.") + .AppendLine( + $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] [E] Exception caught after {sw.ElapsedMilliseconds} ms, Message: {e.Message}" + ) + .AppendLine(e.StackTrace) + .ToString() + : e.StackTrace; + } + } + + /// + /// 执行无插件依赖的脚本 + /// + private static async Task ExecuteCodesWithoutPluginsAsync( + string code, + bool includeTimestamp, + CancellationToken cancellationToken) + { + var sw = new Stopwatch(); + var begin = DateTime.Now; + sw.Start(); + + try + { + var result = ( + await Engine.ExecuteAsync( + code, + options => options + .WithReferences(Assembly.GetExecutingAssembly()) + .WithImports("System", "System.Collections.Generic", "System.Threading.Tasks") + .WithLanguageVersion(LanguageVersion.Preview), + addDefaultImports: true, + runInReplMode: false, + cancellationToken: cancellationToken + ) + )?.ToString(); + + sw.Stop(); + + return includeTimestamp + ? new StringBuilder() + .AppendLine($"[{begin:yyyy-MM-dd HH:mm:ss}] [I] Workflow script posted.") + .AppendLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] [I] Script ended, took {sw.ElapsedMilliseconds} ms.") + .AppendLine(result) + .ToString() + : result; + } + catch (Exception e) + { + sw.Stop(); + + return includeTimestamp + ? new StringBuilder() + .AppendLine($"[{begin:yyyy-MM-dd HH:mm:ss}] [I] Workflow script posted.") + .AppendLine( + $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] [E] Exception caught after {sw.ElapsedMilliseconds} ms, Message: {e.Message}" + ) + .AppendLine(e.StackTrace) + .ToString() + : e.StackTrace; + } + } +} diff --git a/KitX Dashboard/ViewModels/Pages/WorkflowPageViewModel.cs b/KitX Dashboard/ViewModels/Pages/WorkflowPageViewModel.cs index 78b682f..6cf226f 100644 --- a/KitX Dashboard/ViewModels/Pages/WorkflowPageViewModel.cs +++ b/KitX Dashboard/ViewModels/Pages/WorkflowPageViewModel.cs @@ -1,4 +1,4 @@ -using System.Collections.ObjectModel; +using System.Collections.ObjectModel; using System.Reactive; using System.Threading.Tasks; using Avalonia.Metadata; @@ -61,6 +61,14 @@ public sealed override void InitCommands() var messageBoxStandardWindow = MessageBoxManager.GetMessageBoxStandard("C", "XK", icon: Icon.Error).ShowWindowAsync(); return Task.CompletedTask; }); + + // 添加打开工作流脚本编辑器的命令 + OpenWorkflowEditorCommand = ReactiveCommand.Create(() => + { + var editorWindow = new WorkflowScriptEditorWindow(); + editorWindow.Show(); + return Task.CompletedTask; + }); } public sealed override void InitEvents() @@ -104,5 +112,8 @@ internal double NoWorkflow_TipHeight internal ReactiveCommand? RunWorkflowCommand { get; set; } internal ReactiveCommand? StopWorkflowCommand { get; set; } + + // 新增命令:打开工作流脚本编辑器 + internal ReactiveCommand? OpenWorkflowEditorCommand { get; set; } } } diff --git a/KitX Dashboard/ViewModels/WorkflowScriptEditorWindowViewModel.cs b/KitX Dashboard/ViewModels/WorkflowScriptEditorWindowViewModel.cs new file mode 100644 index 0000000..2a423ef --- /dev/null +++ b/KitX Dashboard/ViewModels/WorkflowScriptEditorWindowViewModel.cs @@ -0,0 +1,101 @@ +using System; +using System.Reactive; +using System.Threading; +using System.Threading.Tasks; +using Avalonia.Threading; +using AvaloniaEdit.Document; +using KitX.Dashboard.Services; +using ReactiveUI; +using System.Reflection; +using System.Collections.Generic; +using Kscript.CSharp.Parser; +using KitX.Shared.CSharp.Plugin; + +namespace KitX.Dashboard.ViewModels; + +internal class WorkflowScriptEditorWindowViewModel : ViewModelBase +{ + private CancellationTokenSource? _cancellationTokenSource; + + private readonly IDisposable _codeDocumentSubscription; + + public WorkflowScriptEditorWindowViewModel() + { + InitCommands(); + InitEvents(); + + // 订阅CodeDocument属性变化 + _codeDocumentSubscription = this.WhenAnyValue(x => x.CodeDocument) + .Subscribe(document => + { + // 在这里处理CodeDocument变化的逻辑 + if (document != null) + { + // 例如:可以在这里触发代码分析或保存操作 + // SubmitCodes(document); + } + }); + } + + public sealed override void InitCommands() + { + CancelExecutionCommand = ReactiveCommand.Create(() => _cancellationTokenSource?.Cancel()); + } + + public sealed override void InitEvents() { } + + internal void SubmitCodes(IDocument doc) + { + IsExecuting = true; + + var code = doc.Text; + + var tokenSource = new CancellationTokenSource(); + + _cancellationTokenSource = tokenSource; + + Task.Run( + async () => + { + var result = await WorkflowScriptService.ExecuteCodesAsync(code, cancellationToken: tokenSource.Token); + + tokenSource.Dispose(); + + _cancellationTokenSource = null; + + Dispatcher.UIThread.Invoke(() => + { + ExecutionResult = result ?? string.Empty; + + IsExecuting = false; + }); + }, + tokenSource.Token + ); + } + + internal void CancelExecution() + { + _cancellationTokenSource?.Cancel(); + } + + private string _executionResult = string.Empty; + + public string ExecutionResult + { + get => _executionResult; + set => this.RaiseAndSetIfChanged(ref _executionResult, value); + } + + private bool _isExecuting; + + public bool IsExecuting + { + get => _isExecuting; + set => this.RaiseAndSetIfChanged(ref _isExecuting, value); + } + + internal IDocument? CodeDocument { get; set; } + + internal ReactiveCommand? CancelExecutionCommand { get; set; } +} \ No newline at end of file diff --git a/KitX Dashboard/Views/Pages/WorkflowPage.axaml b/KitX Dashboard/Views/Pages/WorkflowPage.axaml index 58bda2f..70b101f 100644 --- a/KitX Dashboard/Views/Pages/WorkflowPage.axaml +++ b/KitX Dashboard/Views/Pages/WorkflowPage.axaml @@ -1,4 +1,4 @@ - + + +