From b88759b08d0f3d98cce1cf451d6034019caf1daf Mon Sep 17 00:00:00 2001 From: tomnycool Date: Tue, 4 Mar 2025 19:25:01 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9EPluginCore=E6=8F=92=E4=BB=B6?= =?UTF-8?q?=E5=BA=93=E5=8F=8A=E6=8F=92=E4=BB=B6=E7=A4=BA=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Admin.NET.Application.csproj | 2 + .../App_Data/PluginCore.Config.json | 1 + .../App_Data/plugin.config.json | 1 + .../Admin.NET.Plugin.Pay.Alipay.deps.json | 738 ++++++++++++++++ .../Admin.NET.Plugin.Pay.Alipay.dll | Bin 0 -> 13312 bytes .../Admin.NET.Plugin.Pay.Alipay.pdb | Bin 0 -> 24796 bytes .../Admin.NET.Plugin.Pay.Alipay.xml | 34 + .../Admin.NET.Plugin.Pay.Alipay/README.md | 14 + .../Admin.NET.Plugin.Pay.Alipay/info.json | 8 + .../Admin.NET.Plugin.Pay.Alipay/settings.json | 3 + .../wwwroot/css/main.css | 9 + .../wwwroot/index.html | 17 + .../Admin.NET.Plugin.Pay.Alipay/css/main.css | 9 + .../Admin.NET.Plugin.Pay.Alipay/index.html | 17 + Admin.NET/Admin.NET.sln | 55 ++ .../Abstractions/Pay/IPayPlugin.cs | 20 + .../Admin.NET.Plugin.Core.csproj | 27 + .../Admin.NET.Plugin.Core.xml | 8 + .../Admin.NET.Plugin.Core/GlobalUsing.cs | 1 + .../Admin.NET.Plugin.PluginCoreManager.csproj | 35 + .../Controllers/AppCenterController.cs | 207 +++++ .../Controllers/DebugController.cs | 279 ++++++ .../Controllers/PluginWidgetController.cs | 91 ++ .../Controllers/PluginsController.cs | 680 +++++++++++++++ .../Controllers/UserController.cs | 204 +++++ .../Entity/SysPluginCore.cs | 73 ++ .../GlobalUsings.cs | 25 + .../SeedData/SysMenuSeedData.cs | 39 + .../SeedData/SysRoleMenuSeedData.cs | 39 + .../Service/Plugin/Dto/PluginInput.cs | 43 + .../Service/Plugin/SysPluginCoreService.cs | 729 ++++++++++++++++ .../Startup.cs | 34 + .../StartupServiceComponent.cs | 56 ++ .../PluginCoreAdminUIBuilderExtensions.cs | 95 ++ .../AdminUI/PluginCoreAdminUIMiddleware.cs | 150 ++++ .../AdminUI/PluginCoreAdminUIOptions.cs | 68 ++ .../PluginCoreAdminUIRemoteFileProvider.cs | 132 +++ .../PluginCoreAuthenticationHandler.cs | 72 ++ .../PluginCoreAuthenticationSchemeOptions.cs | 23 + .../Authorization/AccountManager.cs | 149 ++++ .../PluginCoreAdminAuthorizationHandler.cs | 59 ++ .../PluginCoreAdminAuthorizeAttribute.cs | 31 + .../PluginCoreAdminRequirement.cs | 26 + .../BackgroundServicesHelper.cs | 27 + .../PluginTimeJobBackgroundService.cs | 92 ++ .../TimeBackgroundService.cs | 47 + .../PluginCore.AspNetCore/CHANGELOG.md | 280 ++++++ .../Controllers忽略/AppCenterController.cs | 207 +++++ .../Controllers忽略/DebugController.cs | 280 ++++++ .../Controllers忽略/HomeController.cs | 73 ++ .../Controllers忽略/PluginWidgetController.cs | 103 +++ .../Controllers忽略/PluginsController.cs | 688 +++++++++++++++ .../Controllers忽略/UserController.cs | 143 +++ .../Extensions/IServiceProviderExtensions.cs | 152 ++++ .../Extensions/PluginCoreStartupExtensions.cs | 416 +++++++++ .../PluginApplicationBuilder.cs | 350 ++++++++ .../PluginCoreHostingStartup.cs | 62 ++ .../IPluginApplicationBuilderManager.cs | 25 + .../Interfaces/IPluginControllerManager.cs | 25 + .../Middlewares/LanguageMiddleware.cs | 47 + .../PluginContentFilterMiddleware.cs | 150 ++++ .../PluginHttpEndFilterMiddleware.cs | 72 ++ .../PluginHttpStartFilterMiddleware.cs | 72 ++ .../Middlewares/PluginStartupXMiddleware.cs | 72 ++ .../PluginCore.AspNetCore.csproj | 107 +++ .../RequestModel/User/LoginRequestModel.cs | 24 + .../RequestModel/User/UpdateRequestMode.cs | 24 + .../ResponseModel/BaseResponseModel.cs | 22 + .../PluginCore.AspNetCore/cliff.toml | 82 ++ .../lmplements/AspNetCorePluginManager.cs | 82 ++ .../lmplements/AspNetCorePluginManagerV1.cs | 84 ++ .../PluginActionDescriptorChangeProvider.cs | 40 + .../PluginApplicationBuilderManager.cs | 78 ++ .../lmplements/PluginControllerManager.cs | 66 ++ .../PluginCore.AspNetCore/nuget-pack.ps1 | 1 + .../PluginCore.AspNetCore/package-lock.json | 307 +++++++ .../PluginCore.AspNetCore/package.json | 8 + .../PluginCore.AspNetCore/readme.txt | 11 + .../CHANGELOG.md | 54 ++ .../IPlugins/IContentFilterPlugin.cs | 29 + .../IPlugins/IHttpFilterPlugin.cs | 29 + .../IPlugins/IStartupPlugin.cs | 40 + .../IPlugins/IStartupXPlugin.cs | 45 + .../PluginCore.IPlugins.AspNetCore.csproj | 40 + .../PluginCore.IPlugins.AspNetCore/cliff.toml | 82 ++ .../nuget-pack.ps1 | 1 + .../PluginCore.IPlugins/BasePlugin.cs | 67 ++ .../PluginCore.IPlugins/CHANGELOG.md | 141 +++ .../PluginCore.IPlugins/Constants.cs | 26 + .../PluginCore/PluginCore.IPlugins/IPlugin.cs | 57 ++ .../IPlugins/ITimeJobPlugin.cs | 31 + .../IPlugins/IWidgetPlugin.cs | 26 + .../Infrastructure/PluginPathProvider.cs | 216 +++++ .../PluginSettingsModelFactory.cs | 165 ++++ .../Interfaces/IPluginFinder.cs | 79 ++ .../Interfaces/IPluginManager.cs | 30 + .../Models/PluginSettingsModel.cs | 27 + .../PluginCore.IPlugins.csproj | 29 + .../PluginCore/PluginCore.IPlugins/cliff.toml | 82 ++ .../PluginCore.IPlugins/nuget-pack.ps1 | 1 + .../PluginCore/PluginCore/CHANGELOG.md | 396 +++++++++ .../PluginCore/Config/PluginCoreConfig.cs | 50 ++ .../Config/PluginCoreConfigFactory.cs | 70 ++ .../PluginCore/Infrastructure/NupkgService.cs | 148 ++++ .../Infrastructure/PluginServiceProvide.cs | 174 ++++ .../ICollectibleAssemblyLoadContext.cs | 24 + .../PluginCore/Interfaces/IPluginContext.cs | 42 + .../Interfaces/IPluginContextManager.cs | 32 + .../Interfaces/IPluginContextPack.cs | 28 + .../PluginCore/Models/PluginConfigModel.cs | 40 + .../PluginCore/Models/PluginInfoModel.cs | 56 ++ .../Models/PluginInfoResponseModel.cs | 31 + .../PluginCore/Models/PluginReadmeModel.cs | 24 + .../Models/PluginReadmeResponseModel.cs | 21 + .../Models/PluginRegistryResponseModel.cs | 45 + .../Models/PluginSettingsInputModel.cs | 24 + .../PluginCore/PluginConfigModelFactory.cs | 91 ++ .../PluginCore/PluginCore/PluginCore.csproj | 48 + .../PluginCore/PluginInfoModelFactory.cs | 108 +++ .../PluginCore/PluginReadmeModelFactory.cs | 55 ++ .../PluginCore/Utils/DateTimeUtil.cs | 118 +++ .../PluginCore/Utils/DependencySorter.cs | 194 +++++ .../PluginCore/PluginCore/Utils/FileUtil.cs | 178 ++++ .../PluginCore/PluginCore/Utils/LogUtil.cs | 238 +++++ .../PluginCore/PluginCore/Utils/Md5Helper.cs | 42 + .../PluginCore/Utils/RuntimeUtil.cs | 28 + .../PluginCore/PluginCore/Utils/ZipHelper.cs | 445 ++++++++++ .../Plugins/PluginCore/PluginCore/cliff.toml | 82 ++ .../CollectibleAssemblyLoadContext.cs | 45 + .../lmplements/LazyPluginLoadContext.cs | 101 +++ .../lmplements/PluginContextManager.cs | 109 +++ .../lmplements/PluginContextPack.cs | 53 ++ .../lmplements/PluginContextPackV1.cs | 105 +++ .../PluginCore/lmplements/PluginFinder.cs | 42 + .../PluginCore/lmplements/PluginFinderV1.cs | 313 +++++++ .../PluginCore/lmplements/PluginFinderV2.cs | 317 +++++++ .../lmplements/PluginLoadContext.cs | 32 + .../PluginCore/lmplements/PluginManager.cs | 65 ++ .../lmplements/PositivePluginLoadContext.cs | 112 +++ .../PluginCore/PluginCore/nuget-pack.ps1 | 1 + .../Test/Admin.NET.Plugin.Pay.Alipay.zip | Bin 0 -> 27799 bytes .../Admin.NET.Plugin.Pay.Alipay.csproj | 65 ++ .../Admin.NET.Plugin.Pay.Alipay.xml | 34 + .../AliPayPlugin.cs | 56 ++ .../Controllers/AliWebApiController.cs | 29 + .../Controllers/HomeController.cs | 35 + .../GlobalUsing.cs | 2 + .../HelloWorldPlugin.cs | 73 ++ .../Middlewares/AlipaySayHelloMiddleware.cs | 63 ++ .../Admin.NET.Plugin.Pay.Alipay/README.md | 14 + .../Services/AlipayPluginCoreService.cs | 55 ++ .../Admin.NET.Plugin.Pay.Alipay/info.json | 8 + .../Admin.NET.Plugin.Pay.Alipay/settings.json | 3 + .../wwwroot/css/main.css | 9 + .../wwwroot/index.html | 17 + Web/src/api-plugins/plugincore/api.ts | 15 + .../plugincore/apis/sys-plugin-core-api.ts | 823 ++++++++++++++++++ Web/src/api-plugins/plugincore/base.ts | 70 ++ .../api-plugins/plugincore/configuration.ts | 83 ++ Web/src/api-plugins/plugincore/index.ts | 18 + .../models/add-plugin-core-input.ts | 99 +++ ...lt-sql-sugar-paged-list-sys-plugin-core.ts | 57 ++ .../admin-result-sys-plugin-core-readme.ts | 57 ++ .../models/admin-result-sys-plugin-core.ts | 57 ++ .../models/delete-plugin-core-input.ts | 26 + .../api-plugins/plugincore/models/index.ts | 11 + .../models/page-plugin-core-input.ts | 62 ++ .../sql-sugar-paged-list-sys-plugin-core.ts | 63 ++ .../models/sys-plugin-core-readme.ts | 47 + .../plugincore/models/sys-plugin-core.ts | 117 +++ .../models/update-plugin-core-input.ts | 99 +++ .../models/update-plugin-core-setting.ts | 35 + .../component/detailsPluginDialog.vue | 73 ++ .../component/editPluginSetting.vue | 117 +++ Web/src/views/system/plugincore/index.vue | 292 +++++++ 175 files changed, 16730 insertions(+) create mode 100644 Admin.NET/Admin.NET.Web.Entry/App_Data/PluginCore.Config.json create mode 100644 Admin.NET/Admin.NET.Web.Entry/App_Data/plugin.config.json create mode 100644 Admin.NET/Admin.NET.Web.Entry/Plugins/Admin.NET.Plugin.Pay.Alipay/Admin.NET.Plugin.Pay.Alipay.deps.json create mode 100644 Admin.NET/Admin.NET.Web.Entry/Plugins/Admin.NET.Plugin.Pay.Alipay/Admin.NET.Plugin.Pay.Alipay.dll create mode 100644 Admin.NET/Admin.NET.Web.Entry/Plugins/Admin.NET.Plugin.Pay.Alipay/Admin.NET.Plugin.Pay.Alipay.pdb create mode 100644 Admin.NET/Admin.NET.Web.Entry/Plugins/Admin.NET.Plugin.Pay.Alipay/Admin.NET.Plugin.Pay.Alipay.xml create mode 100644 Admin.NET/Admin.NET.Web.Entry/Plugins/Admin.NET.Plugin.Pay.Alipay/README.md create mode 100644 Admin.NET/Admin.NET.Web.Entry/Plugins/Admin.NET.Plugin.Pay.Alipay/info.json create mode 100644 Admin.NET/Admin.NET.Web.Entry/Plugins/Admin.NET.Plugin.Pay.Alipay/settings.json create mode 100644 Admin.NET/Admin.NET.Web.Entry/Plugins/Admin.NET.Plugin.Pay.Alipay/wwwroot/css/main.css create mode 100644 Admin.NET/Admin.NET.Web.Entry/Plugins/Admin.NET.Plugin.Pay.Alipay/wwwroot/index.html create mode 100644 Admin.NET/Admin.NET.Web.Entry/Plugins_wwwroot/Admin.NET.Plugin.Pay.Alipay/css/main.css create mode 100644 Admin.NET/Admin.NET.Web.Entry/Plugins_wwwroot/Admin.NET.Plugin.Pay.Alipay/index.html create mode 100644 Admin.NET/Plugins/Admin.NET.Plugin.Core/Abstractions/Pay/IPayPlugin.cs create mode 100644 Admin.NET/Plugins/Admin.NET.Plugin.Core/Admin.NET.Plugin.Core.csproj create mode 100644 Admin.NET/Plugins/Admin.NET.Plugin.Core/Admin.NET.Plugin.Core.xml create mode 100644 Admin.NET/Plugins/Admin.NET.Plugin.Core/GlobalUsing.cs create mode 100644 Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Admin.NET.Plugin.PluginCoreManager.csproj create mode 100644 Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Controllers/AppCenterController.cs create mode 100644 Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Controllers/DebugController.cs create mode 100644 Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Controllers/PluginWidgetController.cs create mode 100644 Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Controllers/PluginsController.cs create mode 100644 Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Controllers/UserController.cs create mode 100644 Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Entity/SysPluginCore.cs create mode 100644 Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/GlobalUsings.cs create mode 100644 Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/SeedData/SysMenuSeedData.cs create mode 100644 Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/SeedData/SysRoleMenuSeedData.cs create mode 100644 Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Service/Plugin/Dto/PluginInput.cs create mode 100644 Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Service/Plugin/SysPluginCoreService.cs create mode 100644 Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Startup.cs create mode 100644 Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/StartupServiceComponent.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/AdminUI/PluginCoreAdminUIBuilderExtensions.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/AdminUI/PluginCoreAdminUIMiddleware.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/AdminUI/PluginCoreAdminUIOptions.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/AdminUI/PluginCoreAdminUIRemoteFileProvider.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Authentication/PluginCoreAuthenticationHandler.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Authentication/PluginCoreAuthenticationSchemeOptions.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Authorization/AccountManager.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Authorization/PluginCoreAdminAuthorizationHandler.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Authorization/PluginCoreAdminAuthorizeAttribute.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Authorization/PluginCoreAdminRequirement.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/BackgroundServices/BackgroundServicesHelper.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/BackgroundServices/PluginTimeJobBackgroundService.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/BackgroundServices/TimeBackgroundService.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/CHANGELOG.md create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Controllers忽略/AppCenterController.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Controllers忽略/DebugController.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Controllers忽略/HomeController.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Controllers忽略/PluginWidgetController.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Controllers忽略/PluginsController.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Controllers忽略/UserController.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Extensions/IServiceProviderExtensions.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Extensions/PluginCoreStartupExtensions.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Infrastructure/PluginApplicationBuilder.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Infrastructure/PluginCoreHostingStartup.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Interfaces/IPluginApplicationBuilderManager.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Interfaces/IPluginControllerManager.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Middlewares/LanguageMiddleware.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Middlewares/PluginContentFilterMiddleware.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Middlewares/PluginHttpEndFilterMiddleware.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Middlewares/PluginHttpStartFilterMiddleware.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Middlewares/PluginStartupXMiddleware.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/PluginCore.AspNetCore.csproj create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/RequestModel/User/LoginRequestModel.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/RequestModel/User/UpdateRequestMode.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/ResponseModel/BaseResponseModel.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/cliff.toml create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/lmplements/AspNetCorePluginManager.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/lmplements/AspNetCorePluginManagerV1.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/lmplements/PluginActionDescriptorChangeProvider.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/lmplements/PluginApplicationBuilderManager.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/lmplements/PluginControllerManager.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/nuget-pack.ps1 create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/package-lock.json create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/package.json create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/readme.txt create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.IPlugins.AspNetCore/CHANGELOG.md create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.IPlugins.AspNetCore/IPlugins/IContentFilterPlugin.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.IPlugins.AspNetCore/IPlugins/IHttpFilterPlugin.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.IPlugins.AspNetCore/IPlugins/IStartupPlugin.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.IPlugins.AspNetCore/IPlugins/IStartupXPlugin.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.IPlugins.AspNetCore/PluginCore.IPlugins.AspNetCore.csproj create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.IPlugins.AspNetCore/cliff.toml create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.IPlugins.AspNetCore/nuget-pack.ps1 create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.IPlugins/BasePlugin.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.IPlugins/CHANGELOG.md create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.IPlugins/Constants.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.IPlugins/IPlugin.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.IPlugins/IPlugins/ITimeJobPlugin.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.IPlugins/IPlugins/IWidgetPlugin.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.IPlugins/Infrastructure/PluginPathProvider.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.IPlugins/Infrastructure/PluginSettingsModelFactory.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.IPlugins/Interfaces/IPluginFinder.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.IPlugins/Interfaces/IPluginManager.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.IPlugins/Models/PluginSettingsModel.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.IPlugins/PluginCore.IPlugins.csproj create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.IPlugins/cliff.toml create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore.IPlugins/nuget-pack.ps1 create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore/CHANGELOG.md create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore/Config/PluginCoreConfig.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore/Config/PluginCoreConfigFactory.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore/Infrastructure/NupkgService.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore/Infrastructure/PluginServiceProvide.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore/Interfaces/ICollectibleAssemblyLoadContext.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore/Interfaces/IPluginContext.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore/Interfaces/IPluginContextManager.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore/Interfaces/IPluginContextPack.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore/Models/PluginConfigModel.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore/Models/PluginInfoModel.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore/Models/PluginInfoResponseModel.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore/Models/PluginReadmeModel.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore/Models/PluginReadmeResponseModel.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore/Models/PluginRegistryResponseModel.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore/Models/PluginSettingsInputModel.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore/PluginConfigModelFactory.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore/PluginCore.csproj create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore/PluginInfoModelFactory.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore/PluginReadmeModelFactory.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore/Utils/DateTimeUtil.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore/Utils/DependencySorter.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore/Utils/FileUtil.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore/Utils/LogUtil.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore/Utils/Md5Helper.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore/Utils/RuntimeUtil.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore/Utils/ZipHelper.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore/cliff.toml create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore/lmplements/CollectibleAssemblyLoadContext.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore/lmplements/LazyPluginLoadContext.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore/lmplements/PluginContextManager.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore/lmplements/PluginContextPack.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore/lmplements/PluginContextPackV1.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore/lmplements/PluginFinder.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore/lmplements/PluginFinderV1.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore/lmplements/PluginFinderV2.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore/lmplements/PluginLoadContext.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore/lmplements/PluginManager.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore/lmplements/PositivePluginLoadContext.cs create mode 100644 Admin.NET/Plugins/PluginCore/PluginCore/nuget-pack.ps1 create mode 100644 Admin.NET/Plugins/PluginCore/Test/Admin.NET.Plugin.Pay.Alipay.zip create mode 100644 Admin.NET/Plugins/PluginCore/Test/Admin.NET.Plugin.Pay.Alipay/Admin.NET.Plugin.Pay.Alipay.csproj create mode 100644 Admin.NET/Plugins/PluginCore/Test/Admin.NET.Plugin.Pay.Alipay/Admin.NET.Plugin.Pay.Alipay.xml create mode 100644 Admin.NET/Plugins/PluginCore/Test/Admin.NET.Plugin.Pay.Alipay/AliPayPlugin.cs create mode 100644 Admin.NET/Plugins/PluginCore/Test/Admin.NET.Plugin.Pay.Alipay/Controllers/AliWebApiController.cs create mode 100644 Admin.NET/Plugins/PluginCore/Test/Admin.NET.Plugin.Pay.Alipay/Controllers/HomeController.cs create mode 100644 Admin.NET/Plugins/PluginCore/Test/Admin.NET.Plugin.Pay.Alipay/GlobalUsing.cs create mode 100644 Admin.NET/Plugins/PluginCore/Test/Admin.NET.Plugin.Pay.Alipay/HelloWorldPlugin.cs create mode 100644 Admin.NET/Plugins/PluginCore/Test/Admin.NET.Plugin.Pay.Alipay/Middlewares/AlipaySayHelloMiddleware.cs create mode 100644 Admin.NET/Plugins/PluginCore/Test/Admin.NET.Plugin.Pay.Alipay/README.md create mode 100644 Admin.NET/Plugins/PluginCore/Test/Admin.NET.Plugin.Pay.Alipay/Services/AlipayPluginCoreService.cs create mode 100644 Admin.NET/Plugins/PluginCore/Test/Admin.NET.Plugin.Pay.Alipay/info.json create mode 100644 Admin.NET/Plugins/PluginCore/Test/Admin.NET.Plugin.Pay.Alipay/settings.json create mode 100644 Admin.NET/Plugins/PluginCore/Test/Admin.NET.Plugin.Pay.Alipay/wwwroot/css/main.css create mode 100644 Admin.NET/Plugins/PluginCore/Test/Admin.NET.Plugin.Pay.Alipay/wwwroot/index.html create mode 100644 Web/src/api-plugins/plugincore/api.ts create mode 100644 Web/src/api-plugins/plugincore/apis/sys-plugin-core-api.ts create mode 100644 Web/src/api-plugins/plugincore/base.ts create mode 100644 Web/src/api-plugins/plugincore/configuration.ts create mode 100644 Web/src/api-plugins/plugincore/index.ts create mode 100644 Web/src/api-plugins/plugincore/models/add-plugin-core-input.ts create mode 100644 Web/src/api-plugins/plugincore/models/admin-result-sql-sugar-paged-list-sys-plugin-core.ts create mode 100644 Web/src/api-plugins/plugincore/models/admin-result-sys-plugin-core-readme.ts create mode 100644 Web/src/api-plugins/plugincore/models/admin-result-sys-plugin-core.ts create mode 100644 Web/src/api-plugins/plugincore/models/delete-plugin-core-input.ts create mode 100644 Web/src/api-plugins/plugincore/models/index.ts create mode 100644 Web/src/api-plugins/plugincore/models/page-plugin-core-input.ts create mode 100644 Web/src/api-plugins/plugincore/models/sql-sugar-paged-list-sys-plugin-core.ts create mode 100644 Web/src/api-plugins/plugincore/models/sys-plugin-core-readme.ts create mode 100644 Web/src/api-plugins/plugincore/models/sys-plugin-core.ts create mode 100644 Web/src/api-plugins/plugincore/models/update-plugin-core-input.ts create mode 100644 Web/src/api-plugins/plugincore/models/update-plugin-core-setting.ts create mode 100644 Web/src/views/system/plugincore/component/detailsPluginDialog.vue create mode 100644 Web/src/views/system/plugincore/component/editPluginSetting.vue create mode 100644 Web/src/views/system/plugincore/index.vue diff --git a/Admin.NET/Admin.NET.Application/Admin.NET.Application.csproj b/Admin.NET/Admin.NET.Application/Admin.NET.Application.csproj index 46fd9c10..1635185b 100644 --- a/Admin.NET/Admin.NET.Application/Admin.NET.Application.csproj +++ b/Admin.NET/Admin.NET.Application/Admin.NET.Application.csproj @@ -26,7 +26,9 @@ + + diff --git a/Admin.NET/Admin.NET.Web.Entry/App_Data/PluginCore.Config.json b/Admin.NET/Admin.NET.Web.Entry/App_Data/PluginCore.Config.json new file mode 100644 index 00000000..cc4c80c2 --- /dev/null +++ b/Admin.NET/Admin.NET.Web.Entry/App_Data/PluginCore.Config.json @@ -0,0 +1 @@ +{"Admin":{"UserName":"admin","Password":"ABC12345"},"FrontendMode":"LocalEmbedded","RemoteFrontend":"https://cdn.jsdelivr.net/gh/yiyungent/plugincore-admin-frontend@0.3.1/dist-cdn","PluginWidgetDebug":false} \ No newline at end of file diff --git a/Admin.NET/Admin.NET.Web.Entry/App_Data/plugin.config.json b/Admin.NET/Admin.NET.Web.Entry/App_Data/plugin.config.json new file mode 100644 index 00000000..fe43402b --- /dev/null +++ b/Admin.NET/Admin.NET.Web.Entry/App_Data/plugin.config.json @@ -0,0 +1 @@ +{"EnabledPlugins":["Admin.NET.Plugin.Pay.Alipay"]} \ No newline at end of file diff --git a/Admin.NET/Admin.NET.Web.Entry/Plugins/Admin.NET.Plugin.Pay.Alipay/Admin.NET.Plugin.Pay.Alipay.deps.json b/Admin.NET/Admin.NET.Web.Entry/Plugins/Admin.NET.Plugin.Pay.Alipay/Admin.NET.Plugin.Pay.Alipay.deps.json new file mode 100644 index 00000000..1fc448b2 --- /dev/null +++ b/Admin.NET/Admin.NET.Web.Entry/Plugins/Admin.NET.Plugin.Pay.Alipay/Admin.NET.Plugin.Pay.Alipay.deps.json @@ -0,0 +1,738 @@ +{ + "runtimeTarget": { + "name": ".NETCoreApp,Version=v8.0", + "signature": "" + }, + "compilationOptions": {}, + "targets": { + ".NETCoreApp,Version=v8.0": { + "Admin.NET.Plugin.Pay.Alipay/1.0.0": { + "dependencies": { + "Admin.NET.Plugin.Core": "1.0.0", + "Furion.Pure": "4.9.7.3" + }, + "runtime": { + "Admin.NET.Plugin.Pay.Alipay.dll": {} + } + }, + "Ben.Demystifier/0.4.1": { + "dependencies": { + "System.Reflection.Metadata": "7.0.0" + }, + "runtime": { + "lib/netstandard2.1/Ben.Demystifier.dll": { + "assemblyVersion": "0.4.0.0", + "fileVersion": "0.4.0.2" + } + } + }, + "Furion.Pure/4.9.7.3": { + "dependencies": { + "Furion.Pure.Extras.DependencyModel.CodeAnalysis": "4.9.7.3", + "MiniProfiler.AspNetCore.Mvc": "4.5.4", + "Swashbuckle.AspNetCore": "7.2.0" + }, + "runtime": { + "lib/net8.0/Furion.Pure.dll": { + "assemblyVersion": "4.9.7.3", + "fileVersion": "4.9.7.3" + } + } + }, + "Furion.Pure.Extras.DependencyModel.CodeAnalysis/4.9.7.3": { + "dependencies": { + "Ben.Demystifier": "0.4.1", + "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "8.0.11", + "Microsoft.AspNetCore.Razor.Language": "6.0.36", + "Microsoft.CodeAnalysis.CSharp": "4.8.0", + "Microsoft.Extensions.DependencyModel": "8.0.2", + "System.Text.Json": "8.0.5", + "System.Text.RegularExpressions": "4.3.1" + }, + "runtime": { + "lib/net8.0/Furion.Pure.Extras.DependencyModel.CodeAnalysis.dll": { + "assemblyVersion": "4.9.7.3", + "fileVersion": "4.9.7.3" + } + } + }, + "Microsoft.AspNetCore.Http.Abstractions/2.3.0": { + "dependencies": { + "Microsoft.AspNetCore.Http.Features": "2.3.0", + "System.Text.Encodings.Web": "8.0.0" + } + }, + "Microsoft.AspNetCore.Http.Features/2.3.0": { + "dependencies": { + "Microsoft.Extensions.Primitives": "8.0.0" + } + }, + "Microsoft.AspNetCore.JsonPatch/8.0.11": { + "dependencies": { + "Microsoft.CSharp": "4.7.0", + "Newtonsoft.Json": "13.0.3" + }, + "runtime": { + "lib/net8.0/Microsoft.AspNetCore.JsonPatch.dll": { + "assemblyVersion": "8.0.11.0", + "fileVersion": "8.0.1124.52116" + } + } + }, + "Microsoft.AspNetCore.Mvc.NewtonsoftJson/8.0.11": { + "dependencies": { + "Microsoft.AspNetCore.JsonPatch": "8.0.11", + "Newtonsoft.Json": "13.0.3", + "Newtonsoft.Json.Bson": "1.0.2" + }, + "runtime": { + "lib/net8.0/Microsoft.AspNetCore.Mvc.NewtonsoftJson.dll": { + "assemblyVersion": "8.0.11.0", + "fileVersion": "8.0.1124.52116" + } + } + }, + "Microsoft.AspNetCore.Razor.Language/6.0.36": { + "runtime": { + "lib/netstandard2.0/Microsoft.AspNetCore.Razor.Language.dll": { + "assemblyVersion": "6.0.36.0", + "fileVersion": "6.0.3624.51604" + } + } + }, + "Microsoft.CodeAnalysis.Analyzers/3.3.4": {}, + "Microsoft.CodeAnalysis.Common/4.8.0": { + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.3.4", + "System.Collections.Immutable": "7.0.0", + "System.Reflection.Metadata": "7.0.0", + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + }, + "runtime": { + "lib/net7.0/Microsoft.CodeAnalysis.dll": { + "assemblyVersion": "4.8.0.0", + "fileVersion": "4.800.23.55801" + } + }, + "resources": { + "lib/net7.0/cs/Microsoft.CodeAnalysis.resources.dll": { + "locale": "cs" + }, + "lib/net7.0/de/Microsoft.CodeAnalysis.resources.dll": { + "locale": "de" + }, + "lib/net7.0/es/Microsoft.CodeAnalysis.resources.dll": { + "locale": "es" + }, + "lib/net7.0/fr/Microsoft.CodeAnalysis.resources.dll": { + "locale": "fr" + }, + "lib/net7.0/it/Microsoft.CodeAnalysis.resources.dll": { + "locale": "it" + }, + "lib/net7.0/ja/Microsoft.CodeAnalysis.resources.dll": { + "locale": "ja" + }, + "lib/net7.0/ko/Microsoft.CodeAnalysis.resources.dll": { + "locale": "ko" + }, + "lib/net7.0/pl/Microsoft.CodeAnalysis.resources.dll": { + "locale": "pl" + }, + "lib/net7.0/pt-BR/Microsoft.CodeAnalysis.resources.dll": { + "locale": "pt-BR" + }, + "lib/net7.0/ru/Microsoft.CodeAnalysis.resources.dll": { + "locale": "ru" + }, + "lib/net7.0/tr/Microsoft.CodeAnalysis.resources.dll": { + "locale": "tr" + }, + "lib/net7.0/zh-Hans/Microsoft.CodeAnalysis.resources.dll": { + "locale": "zh-Hans" + }, + "lib/net7.0/zh-Hant/Microsoft.CodeAnalysis.resources.dll": { + "locale": "zh-Hant" + } + } + }, + "Microsoft.CodeAnalysis.CSharp/4.8.0": { + "dependencies": { + "Microsoft.CodeAnalysis.Common": "4.8.0" + }, + "runtime": { + "lib/net7.0/Microsoft.CodeAnalysis.CSharp.dll": { + "assemblyVersion": "4.8.0.0", + "fileVersion": "4.800.23.55801" + } + }, + "resources": { + "lib/net7.0/cs/Microsoft.CodeAnalysis.CSharp.resources.dll": { + "locale": "cs" + }, + "lib/net7.0/de/Microsoft.CodeAnalysis.CSharp.resources.dll": { + "locale": "de" + }, + "lib/net7.0/es/Microsoft.CodeAnalysis.CSharp.resources.dll": { + "locale": "es" + }, + "lib/net7.0/fr/Microsoft.CodeAnalysis.CSharp.resources.dll": { + "locale": "fr" + }, + "lib/net7.0/it/Microsoft.CodeAnalysis.CSharp.resources.dll": { + "locale": "it" + }, + "lib/net7.0/ja/Microsoft.CodeAnalysis.CSharp.resources.dll": { + "locale": "ja" + }, + "lib/net7.0/ko/Microsoft.CodeAnalysis.CSharp.resources.dll": { + "locale": "ko" + }, + "lib/net7.0/pl/Microsoft.CodeAnalysis.CSharp.resources.dll": { + "locale": "pl" + }, + "lib/net7.0/pt-BR/Microsoft.CodeAnalysis.CSharp.resources.dll": { + "locale": "pt-BR" + }, + "lib/net7.0/ru/Microsoft.CodeAnalysis.CSharp.resources.dll": { + "locale": "ru" + }, + "lib/net7.0/tr/Microsoft.CodeAnalysis.CSharp.resources.dll": { + "locale": "tr" + }, + "lib/net7.0/zh-Hans/Microsoft.CodeAnalysis.CSharp.resources.dll": { + "locale": "zh-Hans" + }, + "lib/net7.0/zh-Hant/Microsoft.CodeAnalysis.CSharp.resources.dll": { + "locale": "zh-Hant" + } + } + }, + "Microsoft.CSharp/4.7.0": {}, + "Microsoft.Extensions.ApiDescription.Server/6.0.5": {}, + "Microsoft.Extensions.DependencyInjection.Abstractions/9.0.1": { + "runtime": { + "lib/net8.0/Microsoft.Extensions.DependencyInjection.Abstractions.dll": { + "assemblyVersion": "9.0.0.0", + "fileVersion": "9.0.124.61010" + } + } + }, + "Microsoft.Extensions.DependencyModel/8.0.2": { + "runtime": { + "lib/net8.0/Microsoft.Extensions.DependencyModel.dll": { + "assemblyVersion": "8.0.0.2", + "fileVersion": "8.0.1024.46610" + } + } + }, + "Microsoft.Extensions.Logging.Abstractions/9.0.1": { + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", + "System.Diagnostics.DiagnosticSource": "9.0.1" + }, + "runtime": { + "lib/net8.0/Microsoft.Extensions.Logging.Abstractions.dll": { + "assemblyVersion": "9.0.0.0", + "fileVersion": "9.0.124.61010" + } + } + }, + "Microsoft.Extensions.Primitives/8.0.0": {}, + "Microsoft.NETCore.Platforms/1.1.1": {}, + "Microsoft.NETCore.Targets/1.1.3": {}, + "Microsoft.OpenApi/1.6.22": { + "runtime": { + "lib/netstandard2.0/Microsoft.OpenApi.dll": { + "assemblyVersion": "1.6.22.0", + "fileVersion": "1.6.22.0" + } + } + }, + "MiniProfiler.AspNetCore/4.5.4": { + "dependencies": { + "MiniProfiler.Shared": "4.5.4" + }, + "runtime": { + "lib/net8.0/MiniProfiler.AspNetCore.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "4.5.4.47516" + } + } + }, + "MiniProfiler.AspNetCore.Mvc/4.5.4": { + "dependencies": { + "MiniProfiler.AspNetCore": "4.5.4" + }, + "runtime": { + "lib/net8.0/MiniProfiler.AspNetCore.Mvc.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "4.5.4.47516" + } + } + }, + "MiniProfiler.Shared/4.5.4": { + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1" + }, + "runtime": { + "lib/net8.0/MiniProfiler.Shared.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "4.5.4.47516" + } + } + }, + "Newtonsoft.Json/13.0.3": { + "runtime": { + "lib/net6.0/Newtonsoft.Json.dll": { + "assemblyVersion": "13.0.0.0", + "fileVersion": "13.0.3.27908" + } + } + }, + "Newtonsoft.Json.Bson/1.0.2": { + "dependencies": { + "Newtonsoft.Json": "13.0.3" + }, + "runtime": { + "lib/netstandard2.0/Newtonsoft.Json.Bson.dll": { + "assemblyVersion": "1.0.0.0", + "fileVersion": "1.0.2.22727" + } + } + }, + "SharpZipLib/1.4.2": { + "runtime": { + "lib/net6.0/ICSharpCode.SharpZipLib.dll": { + "assemblyVersion": "1.4.2.13", + "fileVersion": "1.4.2.13" + } + } + }, + "Swashbuckle.AspNetCore/7.2.0": { + "dependencies": { + "Microsoft.Extensions.ApiDescription.Server": "6.0.5", + "Swashbuckle.AspNetCore.Swagger": "7.2.0", + "Swashbuckle.AspNetCore.SwaggerGen": "7.2.0", + "Swashbuckle.AspNetCore.SwaggerUI": "7.2.0" + } + }, + "Swashbuckle.AspNetCore.Swagger/7.2.0": { + "dependencies": { + "Microsoft.OpenApi": "1.6.22" + }, + "runtime": { + "lib/net8.0/Swashbuckle.AspNetCore.Swagger.dll": { + "assemblyVersion": "7.2.0.0", + "fileVersion": "7.2.0.956" + } + } + }, + "Swashbuckle.AspNetCore.SwaggerGen/7.2.0": { + "dependencies": { + "Swashbuckle.AspNetCore.Swagger": "7.2.0" + }, + "runtime": { + "lib/net8.0/Swashbuckle.AspNetCore.SwaggerGen.dll": { + "assemblyVersion": "7.2.0.0", + "fileVersion": "7.2.0.956" + } + } + }, + "Swashbuckle.AspNetCore.SwaggerUI/7.2.0": { + "runtime": { + "lib/net8.0/Swashbuckle.AspNetCore.SwaggerUI.dll": { + "assemblyVersion": "7.2.0.0", + "fileVersion": "7.2.0.956" + } + } + }, + "System.Collections.Immutable/7.0.0": {}, + "System.Diagnostics.DiagnosticSource/9.0.1": { + "runtime": { + "lib/net8.0/System.Diagnostics.DiagnosticSource.dll": { + "assemblyVersion": "9.0.0.0", + "fileVersion": "9.0.124.61010" + } + } + }, + "System.Reflection.Metadata/7.0.0": { + "dependencies": { + "System.Collections.Immutable": "7.0.0" + } + }, + "System.Runtime/4.3.1": { + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.1", + "Microsoft.NETCore.Targets": "1.1.3" + } + }, + "System.Runtime.CompilerServices.Unsafe/6.0.0": {}, + "System.Text.Encodings.Web/8.0.0": {}, + "System.Text.Json/8.0.5": {}, + "System.Text.RegularExpressions/4.3.1": { + "dependencies": { + "System.Runtime": "4.3.1" + } + }, + "Admin.NET.Plugin.Core/1.0.0": { + "dependencies": { + "PluginCore.AspNetCore": "1.4.3" + }, + "runtime": { + "Admin.NET.Plugin.Core.dll": { + "assemblyVersion": "1.0.0", + "fileVersion": "1.0.0.0" + } + } + }, + "PluginCore/2.2.5": { + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "9.0.1", + "PluginCore.IPlugins": "0.9.1", + "SharpZipLib": "1.4.2" + }, + "runtime": { + "PluginCore.dll": { + "assemblyVersion": "2.2.5", + "fileVersion": "2.2.5.0" + } + } + }, + "PluginCore.AspNetCore/1.4.3": { + "dependencies": { + "PluginCore": "2.2.5", + "PluginCore.IPlugins.AspNetCore": "0.1.1" + }, + "runtime": { + "PluginCore.AspNetCore.dll": { + "assemblyVersion": "1.4.3", + "fileVersion": "1.4.3.0" + } + } + }, + "PluginCore.IPlugins/0.9.1": { + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", + "System.Text.Json": "8.0.5" + }, + "runtime": { + "PluginCore.IPlugins.dll": { + "assemblyVersion": "0.9.1", + "fileVersion": "0.9.1.0" + } + } + }, + "PluginCore.IPlugins.AspNetCore/0.1.1": { + "dependencies": { + "Microsoft.AspNetCore.Http.Abstractions": "2.3.0", + "PluginCore.IPlugins": "0.9.1" + }, + "runtime": { + "PluginCore.IPlugins.AspNetCore.dll": { + "assemblyVersion": "0.1.1", + "fileVersion": "0.1.1.0" + } + } + } + } + }, + "libraries": { + "Admin.NET.Plugin.Pay.Alipay/1.0.0": { + "type": "project", + "serviceable": false, + "sha512": "" + }, + "Ben.Demystifier/0.4.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-axFeEMfmEORy3ipAzOXG/lE+KcNptRbei3F0C4kQCdeiQtW+qJW90K5iIovITGrdLt8AjhNCwk5qLSX9/rFpoA==", + "path": "ben.demystifier/0.4.1", + "hashPath": "ben.demystifier.0.4.1.nupkg.sha512" + }, + "Furion.Pure/4.9.7.3": { + "type": "package", + "serviceable": true, + "sha512": "sha512-4HAtU6kg3/5cY6ImtbKs+qDa+8BteyD9DUIcCVSq3oo8AWE2ZeMJmXOD9rx+5/3mRQXkt5DIXVa4jIG32FjTHw==", + "path": "furion.pure/4.9.7.3", + "hashPath": "furion.pure.4.9.7.3.nupkg.sha512" + }, + "Furion.Pure.Extras.DependencyModel.CodeAnalysis/4.9.7.3": { + "type": "package", + "serviceable": true, + "sha512": "sha512-v6pMgb+tkTeGFpmG98CY8Mv4QNBfTDiDsZmKGb6DEJxTPhdmr7l8onSdTlBuUlxfo94KXIOf7Gq+czr0RX/qbw==", + "path": "furion.pure.extras.dependencymodel.codeanalysis/4.9.7.3", + "hashPath": "furion.pure.extras.dependencymodel.codeanalysis.4.9.7.3.nupkg.sha512" + }, + "Microsoft.AspNetCore.Http.Abstractions/2.3.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-39r9PPrjA6s0blyFv5qarckjNkaHRA5B+3b53ybuGGNTXEj1/DStQJ4NWjFL6QTRQpL9zt7nDyKxZdJOlcnq+Q==", + "path": "microsoft.aspnetcore.http.abstractions/2.3.0", + "hashPath": "microsoft.aspnetcore.http.abstractions.2.3.0.nupkg.sha512" + }, + "Microsoft.AspNetCore.Http.Features/2.3.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-f10WUgcsKqrkmnz6gt8HeZ7kyKjYN30PO7cSic1lPtH7paPtnQqXPOveul/SIPI43PhRD4trttg4ywnrEmmJpA==", + "path": "microsoft.aspnetcore.http.features/2.3.0", + "hashPath": "microsoft.aspnetcore.http.features.2.3.0.nupkg.sha512" + }, + "Microsoft.AspNetCore.JsonPatch/8.0.11": { + "type": "package", + "serviceable": true, + "sha512": "sha512-l1tFnQm2LtFE3M9YRM/bdwtxxCV50Y5jnN0LjliQH1sqvWsN46++Uu3QCJL9IdOweFvXSf3Shi7DI/Vc1jkdKA==", + "path": "microsoft.aspnetcore.jsonpatch/8.0.11", + "hashPath": "microsoft.aspnetcore.jsonpatch.8.0.11.nupkg.sha512" + }, + "Microsoft.AspNetCore.Mvc.NewtonsoftJson/8.0.11": { + "type": "package", + "serviceable": true, + "sha512": "sha512-XcfFd8e0g2M0mcAKVNgoHJtWYJfKrPntHhgqiZ1Ci37i3AEJbM0GHIa715i0UPSksiKmDxsJWXnM3rg8keF/Zg==", + "path": "microsoft.aspnetcore.mvc.newtonsoftjson/8.0.11", + "hashPath": "microsoft.aspnetcore.mvc.newtonsoftjson.8.0.11.nupkg.sha512" + }, + "Microsoft.AspNetCore.Razor.Language/6.0.36": { + "type": "package", + "serviceable": true, + "sha512": "sha512-n5Mg5D0aRrhHJJ6bJcwKqQydIFcgUq0jTlvuynoJjwA2IvAzh8Aqf9cpYagofQbIlIXILkCP6q6FgbngyVtpYA==", + "path": "microsoft.aspnetcore.razor.language/6.0.36", + "hashPath": "microsoft.aspnetcore.razor.language.6.0.36.nupkg.sha512" + }, + "Microsoft.CodeAnalysis.Analyzers/3.3.4": { + "type": "package", + "serviceable": true, + "sha512": "sha512-AxkxcPR+rheX0SmvpLVIGLhOUXAKG56a64kV9VQZ4y9gR9ZmPXnqZvHJnmwLSwzrEP6junUF11vuc+aqo5r68g==", + "path": "microsoft.codeanalysis.analyzers/3.3.4", + "hashPath": "microsoft.codeanalysis.analyzers.3.3.4.nupkg.sha512" + }, + "Microsoft.CodeAnalysis.Common/4.8.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-/jR+e/9aT+BApoQJABlVCKnnggGQbvGh7BKq2/wI1LamxC+LbzhcLj4Vj7gXCofl1n4E521YfF9w0WcASGg/KA==", + "path": "microsoft.codeanalysis.common/4.8.0", + "hashPath": "microsoft.codeanalysis.common.4.8.0.nupkg.sha512" + }, + "Microsoft.CodeAnalysis.CSharp/4.8.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-+3+qfdb/aaGD8PZRCrsdobbzGs1m9u119SkkJt8e/mk3xLJz/udLtS2T6nY27OTXxBBw10HzAbC8Z9w08VyP/g==", + "path": "microsoft.codeanalysis.csharp/4.8.0", + "hashPath": "microsoft.codeanalysis.csharp.4.8.0.nupkg.sha512" + }, + "Microsoft.CSharp/4.7.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-pTj+D3uJWyN3My70i2Hqo+OXixq3Os2D1nJ2x92FFo6sk8fYS1m1WLNTs0Dc1uPaViH0YvEEwvzddQ7y4rhXmA==", + "path": "microsoft.csharp/4.7.0", + "hashPath": "microsoft.csharp.4.7.0.nupkg.sha512" + }, + "Microsoft.Extensions.ApiDescription.Server/6.0.5": { + "type": "package", + "serviceable": true, + "sha512": "sha512-Ckb5EDBUNJdFWyajfXzUIMRkhf52fHZOQuuZg/oiu8y7zDCVwD0iHhew6MnThjHmevanpxL3f5ci2TtHQEN6bw==", + "path": "microsoft.extensions.apidescription.server/6.0.5", + "hashPath": "microsoft.extensions.apidescription.server.6.0.5.nupkg.sha512" + }, + "Microsoft.Extensions.DependencyInjection.Abstractions/9.0.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-Tr74eP0oQ3AyC24ch17N8PuEkrPbD0JqIfENCYqmgKYNOmL8wQKzLJu3ObxTUDrjnn4rHoR1qKa37/eQyHmCDA==", + "path": "microsoft.extensions.dependencyinjection.abstractions/9.0.1", + "hashPath": "microsoft.extensions.dependencyinjection.abstractions.9.0.1.nupkg.sha512" + }, + "Microsoft.Extensions.DependencyModel/8.0.2": { + "type": "package", + "serviceable": true, + "sha512": "sha512-mUBDZZRgZrSyFOsJ2qJJ9fXfqd/kXJwf3AiDoqLD9m6TjY5OO/vLNOb9fb4juC0487eq4hcGN/M2Rh/CKS7QYw==", + "path": "microsoft.extensions.dependencymodel/8.0.2", + "hashPath": "microsoft.extensions.dependencymodel.8.0.2.nupkg.sha512" + }, + "Microsoft.Extensions.Logging.Abstractions/9.0.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-w2gUqXN/jNIuvqYwX3lbXagsizVNXYyt6LlF57+tMve4JYCEgCMMAjRce6uKcDASJgpMbErRT1PfHy2OhbkqEA==", + "path": "microsoft.extensions.logging.abstractions/9.0.1", + "hashPath": "microsoft.extensions.logging.abstractions.9.0.1.nupkg.sha512" + }, + "Microsoft.Extensions.Primitives/8.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g==", + "path": "microsoft.extensions.primitives/8.0.0", + "hashPath": "microsoft.extensions.primitives.8.0.0.nupkg.sha512" + }, + "Microsoft.NETCore.Platforms/1.1.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-TMBuzAHpTenGbGgk0SMTwyEkyijY/Eae4ZGsFNYJvAr/LDn1ku3Etp3FPxChmDp5HHF3kzJuoaa08N0xjqAJfQ==", + "path": "microsoft.netcore.platforms/1.1.1", + "hashPath": "microsoft.netcore.platforms.1.1.1.nupkg.sha512" + }, + "Microsoft.NETCore.Targets/1.1.3": { + "type": "package", + "serviceable": true, + "sha512": "sha512-3Wrmi0kJDzClwAC+iBdUBpEKmEle8FQNsCs77fkiOIw/9oYA07bL1EZNX0kQ2OMN3xpwvl0vAtOCYY3ndDNlhQ==", + "path": "microsoft.netcore.targets/1.1.3", + "hashPath": "microsoft.netcore.targets.1.1.3.nupkg.sha512" + }, + "Microsoft.OpenApi/1.6.22": { + "type": "package", + "serviceable": true, + "sha512": "sha512-aBvunmrdu/x+4CaA/UP1Jx4xWGwk4kymhoIRnn2Vp+zi5/KOPQJ9EkSXHRUr01WcGKtYl3Au7XfkPJbU1G2sjQ==", + "path": "microsoft.openapi/1.6.22", + "hashPath": "microsoft.openapi.1.6.22.nupkg.sha512" + }, + "MiniProfiler.AspNetCore/4.5.4": { + "type": "package", + "serviceable": true, + "sha512": "sha512-meedJsjpYOeHPhE8H6t+dGQ9zLxcCQVpi4DXzmxmYAXywmTzlo6jv2IASUv5QijTU0CxsROln3FHd8RsTO8Z8A==", + "path": "miniprofiler.aspnetcore/4.5.4", + "hashPath": "miniprofiler.aspnetcore.4.5.4.nupkg.sha512" + }, + "MiniProfiler.AspNetCore.Mvc/4.5.4": { + "type": "package", + "serviceable": true, + "sha512": "sha512-+NqXyCy9aNdroPm6leW5+cpngtCnkCdoyOlJzvVN62uucSx+MYkx8jmKbgAt+aCP6aghADfHBExwrTIldHxapg==", + "path": "miniprofiler.aspnetcore.mvc/4.5.4", + "hashPath": "miniprofiler.aspnetcore.mvc.4.5.4.nupkg.sha512" + }, + "MiniProfiler.Shared/4.5.4": { + "type": "package", + "serviceable": true, + "sha512": "sha512-f8ckFm/xTS8C2Bn4BdVc94dNvg+tRfk0e4XFaETOqRi6r0PUOyn3Z9jTQCVpB3R1pP5WiRsEIrqqxux95BVpTA==", + "path": "miniprofiler.shared/4.5.4", + "hashPath": "miniprofiler.shared.4.5.4.nupkg.sha512" + }, + "Newtonsoft.Json/13.0.3": { + "type": "package", + "serviceable": true, + "sha512": "sha512-HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==", + "path": "newtonsoft.json/13.0.3", + "hashPath": "newtonsoft.json.13.0.3.nupkg.sha512" + }, + "Newtonsoft.Json.Bson/1.0.2": { + "type": "package", + "serviceable": true, + "sha512": "sha512-QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", + "path": "newtonsoft.json.bson/1.0.2", + "hashPath": "newtonsoft.json.bson.1.0.2.nupkg.sha512" + }, + "SharpZipLib/1.4.2": { + "type": "package", + "serviceable": true, + "sha512": "sha512-yjj+3zgz8zgXpiiC3ZdF/iyTBbz2fFvMxZFEBPUcwZjIvXOf37Ylm+K58hqMfIBt5JgU/Z2uoUS67JmTLe973A==", + "path": "sharpziplib/1.4.2", + "hashPath": "sharpziplib.1.4.2.nupkg.sha512" + }, + "Swashbuckle.AspNetCore/7.2.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-vJv19UpWm6OOgnS9QLDnWARNVasXUfj8SFvlG7UVALm4nBnfwRnEky7C0veSDqMUmBeMPC6Ec3d6G1ts/J04Uw==", + "path": "swashbuckle.aspnetcore/7.2.0", + "hashPath": "swashbuckle.aspnetcore.7.2.0.nupkg.sha512" + }, + "Swashbuckle.AspNetCore.Swagger/7.2.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-y27fNDfIh1vGhJjXYynLcZjl7DLOW1bSO2MDsY9wB4Zm1fdxpPsuBSiR4U+0acWlAqLmnuOPKr/OeOgwRUkBlw==", + "path": "swashbuckle.aspnetcore.swagger/7.2.0", + "hashPath": "swashbuckle.aspnetcore.swagger.7.2.0.nupkg.sha512" + }, + "Swashbuckle.AspNetCore.SwaggerGen/7.2.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-pMrTxGVuXM7t4wqft5CNNU8A0++Yw5kTLmYhB6tbEcyBfO8xEF/Y8pkJhO6BZ/2MYONrRYoQTfPFJqu8fOf5WQ==", + "path": "swashbuckle.aspnetcore.swaggergen/7.2.0", + "hashPath": "swashbuckle.aspnetcore.swaggergen.7.2.0.nupkg.sha512" + }, + "Swashbuckle.AspNetCore.SwaggerUI/7.2.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-hgrXeKzyp5OGN8qVvL7A+vhmU7mDJTfGpiMBRL66IcfLOyna8UTLtn3cC3CghamXpRDufcc9ciklTszUGEQK0w==", + "path": "swashbuckle.aspnetcore.swaggerui/7.2.0", + "hashPath": "swashbuckle.aspnetcore.swaggerui.7.2.0.nupkg.sha512" + }, + "System.Collections.Immutable/7.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-dQPcs0U1IKnBdRDBkrCTi1FoajSTBzLcVTpjO4MBCMC7f4pDOIPzgBoX8JjG7X6uZRJ8EBxsi8+DR1JuwjnzOQ==", + "path": "system.collections.immutable/7.0.0", + "hashPath": "system.collections.immutable.7.0.0.nupkg.sha512" + }, + "System.Diagnostics.DiagnosticSource/9.0.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-yOcDWx4P/s1I83+7gQlgQLmhny2eNcU0cfo1NBWi+en4EAI38Jau+/neT85gUW6w1s7+FUJc2qNOmmwGLIREqA==", + "path": "system.diagnostics.diagnosticsource/9.0.1", + "hashPath": "system.diagnostics.diagnosticsource.9.0.1.nupkg.sha512" + }, + "System.Reflection.Metadata/7.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-MclTG61lsD9sYdpNz9xsKBzjsmsfCtcMZYXz/IUr2zlhaTaABonlr1ESeompTgM+Xk+IwtGYU7/voh3YWB/fWw==", + "path": "system.reflection.metadata/7.0.0", + "hashPath": "system.reflection.metadata.7.0.0.nupkg.sha512" + }, + "System.Runtime/4.3.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-abhfv1dTK6NXOmu4bgHIONxHyEqFjW8HwXPmpY9gmll+ix9UNo4XDcmzJn6oLooftxNssVHdJC1pGT9jkSynQg==", + "path": "system.runtime/4.3.1", + "hashPath": "system.runtime.4.3.1.nupkg.sha512" + }, + "System.Runtime.CompilerServices.Unsafe/6.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==", + "path": "system.runtime.compilerservices.unsafe/6.0.0", + "hashPath": "system.runtime.compilerservices.unsafe.6.0.0.nupkg.sha512" + }, + "System.Text.Encodings.Web/8.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-yev/k9GHAEGx2Rg3/tU6MQh4HGBXJs70y7j1LaM1i/ER9po+6nnQ6RRqTJn1E7Xu0fbIFK80Nh5EoODxrbxwBQ==", + "path": "system.text.encodings.web/8.0.0", + "hashPath": "system.text.encodings.web.8.0.0.nupkg.sha512" + }, + "System.Text.Json/8.0.5": { + "type": "package", + "serviceable": true, + "sha512": "sha512-0f1B50Ss7rqxXiaBJyzUu9bWFOO2/zSlifZ/UNMdiIpDYe4cY4LQQicP4nirK1OS31I43rn062UIJ1Q9bpmHpg==", + "path": "system.text.json/8.0.5", + "hashPath": "system.text.json.8.0.5.nupkg.sha512" + }, + "System.Text.RegularExpressions/4.3.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-N0kNRrWe4+nXOWlpLT4LAY5brb8caNFlUuIRpraCVMDLYutKkol1aV079rQjLuSxKMJT2SpBQsYX9xbcTMmzwg==", + "path": "system.text.regularexpressions/4.3.1", + "hashPath": "system.text.regularexpressions.4.3.1.nupkg.sha512" + }, + "Admin.NET.Plugin.Core/1.0.0": { + "type": "project", + "serviceable": false, + "sha512": "" + }, + "PluginCore/2.2.5": { + "type": "project", + "serviceable": false, + "sha512": "" + }, + "PluginCore.AspNetCore/1.4.3": { + "type": "project", + "serviceable": false, + "sha512": "" + }, + "PluginCore.IPlugins/0.9.1": { + "type": "project", + "serviceable": false, + "sha512": "" + }, + "PluginCore.IPlugins.AspNetCore/0.1.1": { + "type": "project", + "serviceable": false, + "sha512": "" + } + } +} \ No newline at end of file diff --git a/Admin.NET/Admin.NET.Web.Entry/Plugins/Admin.NET.Plugin.Pay.Alipay/Admin.NET.Plugin.Pay.Alipay.dll b/Admin.NET/Admin.NET.Web.Entry/Plugins/Admin.NET.Plugin.Pay.Alipay/Admin.NET.Plugin.Pay.Alipay.dll new file mode 100644 index 0000000000000000000000000000000000000000..9a087f5cb4cdc226c4eebd986bcf7628054d869d GIT binary patch literal 13312 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 S{|wyP_$NQoKM(K%7WiM|Hw>%* literal 0 HcmV?d00001 diff --git a/Admin.NET/Admin.NET.Web.Entry/Plugins/Admin.NET.Plugin.Pay.Alipay/Admin.NET.Plugin.Pay.Alipay.pdb b/Admin.NET/Admin.NET.Web.Entry/Plugins/Admin.NET.Plugin.Pay.Alipay/Admin.NET.Plugin.Pay.Alipay.pdb new file mode 100644 index 0000000000000000000000000000000000000000..565448c1edae10bd34ab76adc845f92d426d26df GIT binary patch literal 24796 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 H)=K1mLDqcB literal 0 HcmV?d00001 diff --git a/Admin.NET/Admin.NET.Web.Entry/Plugins/Admin.NET.Plugin.Pay.Alipay/Admin.NET.Plugin.Pay.Alipay.xml b/Admin.NET/Admin.NET.Web.Entry/Plugins/Admin.NET.Plugin.Pay.Alipay/Admin.NET.Plugin.Pay.Alipay.xml new file mode 100644 index 00000000..79a4de2e --- /dev/null +++ b/Admin.NET/Admin.NET.Web.Entry/Plugins/Admin.NET.Plugin.Pay.Alipay/Admin.NET.Plugin.Pay.Alipay.xml @@ -0,0 +1,34 @@ + + + + Admin.NET.Plugin.Pay.Alipay + + + + + 在 Build 时, 将会 new Middleware(), 最终将所有 Middleware 包装为一个 + + + + + + + + + 测试,是否运行时添加的Middleware,是否可以依赖注入 + + + + + 系统动态插件服务 + + + + + 获取动态插件列表 + + + + + + diff --git a/Admin.NET/Admin.NET.Web.Entry/Plugins/Admin.NET.Plugin.Pay.Alipay/README.md b/Admin.NET/Admin.NET.Web.Entry/Plugins/Admin.NET.Plugin.Pay.Alipay/README.md new file mode 100644 index 00000000..5f9d8314 --- /dev/null +++ b/Admin.NET/Admin.NET.Web.Entry/Plugins/Admin.NET.Plugin.Pay.Alipay/README.md @@ -0,0 +1,14 @@ +## 说明文档(可选) + +- [] 这是一个示例插件 +- [x] 感谢使用 + +## API + +- [/SayHello](/SayHello) + - 通过插件在运行时添加 管道Middleware, 并拦截响应 + +- [/api/plugins/AliWebApi](/api/plugins/AliWebApi) + - 通过插件Controller 响应 + + \ No newline at end of file diff --git a/Admin.NET/Admin.NET.Web.Entry/Plugins/Admin.NET.Plugin.Pay.Alipay/info.json b/Admin.NET/Admin.NET.Web.Entry/Plugins/Admin.NET.Plugin.Pay.Alipay/info.json new file mode 100644 index 00000000..ec71bb1f --- /dev/null +++ b/Admin.NET/Admin.NET.Web.Entry/Plugins/Admin.NET.Plugin.Pay.Alipay/info.json @@ -0,0 +1,8 @@ +{ + "PluginId": "Admin.NET.Plugin.Pay.Alipay", + "DisplayName": "Alipay示例", + "Description": "这是支付宝插件", + "Author": "tomny", + "Version": "0.1.0", + "SupportedVersions": [ "0.0.1" ] +} \ No newline at end of file diff --git a/Admin.NET/Admin.NET.Web.Entry/Plugins/Admin.NET.Plugin.Pay.Alipay/settings.json b/Admin.NET/Admin.NET.Web.Entry/Plugins/Admin.NET.Plugin.Pay.Alipay/settings.json new file mode 100644 index 00000000..9a32e4c3 --- /dev/null +++ b/Admin.NET/Admin.NET.Web.Entry/Plugins/Admin.NET.Plugin.Pay.Alipay/settings.json @@ -0,0 +1,3 @@ +{ + "Hello": "哈哈哈哈哈或或或或或或" +} \ No newline at end of file diff --git a/Admin.NET/Admin.NET.Web.Entry/Plugins/Admin.NET.Plugin.Pay.Alipay/wwwroot/css/main.css b/Admin.NET/Admin.NET.Web.Entry/Plugins/Admin.NET.Plugin.Pay.Alipay/wwwroot/css/main.css new file mode 100644 index 00000000..191068ea --- /dev/null +++ b/Admin.NET/Admin.NET.Web.Entry/Plugins/Admin.NET.Plugin.Pay.Alipay/wwwroot/css/main.css @@ -0,0 +1,9 @@ +* { + margin: 0; + padding: 0; +} + +#app { + width: 100%; + color: deepskyblue; +} \ No newline at end of file diff --git a/Admin.NET/Admin.NET.Web.Entry/Plugins/Admin.NET.Plugin.Pay.Alipay/wwwroot/index.html b/Admin.NET/Admin.NET.Web.Entry/Plugins/Admin.NET.Plugin.Pay.Alipay/wwwroot/index.html new file mode 100644 index 00000000..9bb783d2 --- /dev/null +++ b/Admin.NET/Admin.NET.Web.Entry/Plugins/Admin.NET.Plugin.Pay.Alipay/wwwroot/index.html @@ -0,0 +1,17 @@ + + + + + + HelloWorldPlugin + + + + +
+

HelloWorldPlugin!

+

插件的前端文件应当放在 wwwroot 文件夹下

+
+ + + \ No newline at end of file diff --git a/Admin.NET/Admin.NET.Web.Entry/Plugins_wwwroot/Admin.NET.Plugin.Pay.Alipay/css/main.css b/Admin.NET/Admin.NET.Web.Entry/Plugins_wwwroot/Admin.NET.Plugin.Pay.Alipay/css/main.css new file mode 100644 index 00000000..191068ea --- /dev/null +++ b/Admin.NET/Admin.NET.Web.Entry/Plugins_wwwroot/Admin.NET.Plugin.Pay.Alipay/css/main.css @@ -0,0 +1,9 @@ +* { + margin: 0; + padding: 0; +} + +#app { + width: 100%; + color: deepskyblue; +} \ No newline at end of file diff --git a/Admin.NET/Admin.NET.Web.Entry/Plugins_wwwroot/Admin.NET.Plugin.Pay.Alipay/index.html b/Admin.NET/Admin.NET.Web.Entry/Plugins_wwwroot/Admin.NET.Plugin.Pay.Alipay/index.html new file mode 100644 index 00000000..9bb783d2 --- /dev/null +++ b/Admin.NET/Admin.NET.Web.Entry/Plugins_wwwroot/Admin.NET.Plugin.Pay.Alipay/index.html @@ -0,0 +1,17 @@ + + + + + + HelloWorldPlugin + + + + +
+

HelloWorldPlugin!

+

插件的前端文件应当放在 wwwroot 文件夹下

+
+ + + \ No newline at end of file diff --git a/Admin.NET/Admin.NET.sln b/Admin.NET/Admin.NET.sln index 998fd368..af32a0c1 100644 --- a/Admin.NET/Admin.NET.sln +++ b/Admin.NET/Admin.NET.sln @@ -32,6 +32,24 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Admin.NET.Plugin.PaddleOCR" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Admin.NET.Plugin.WorkWeixin", "Plugins\Admin.NET.Plugin.WorkWeixin\Admin.NET.Plugin.WorkWeixin.csproj", "{12998618-A875-4580-B5B1-0CC50CE85F27}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PluginCore", "PluginCore", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PluginCore", "Plugins\PluginCore\PluginCore\PluginCore.csproj", "{D6A36A77-53EB-222C-B7E7-AA2D81F1A9B0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PluginCore.AspNetCore", "Plugins\PluginCore\PluginCore.AspNetCore\PluginCore.AspNetCore.csproj", "{D66359C8-1F82-669F-A515-5A7F6A65DB8B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PluginCore.IPlugins", "Plugins\PluginCore\PluginCore.IPlugins\PluginCore.IPlugins.csproj", "{9E4C9BFE-E657-F410-9283-CAE6C370F264}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PluginCore.IPlugins.AspNetCore", "Plugins\PluginCore\PluginCore.IPlugins.AspNetCore\PluginCore.IPlugins.AspNetCore.csproj", "{BAB9ACF0-5AEE-290C-6D33-30712B6ADF14}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Test", "Test", "{3F5B6AE4-951D-4358-AF94-288C4D484D48}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Admin.NET.Plugin.Pay.Alipay", "Plugins\PluginCore\Test\Admin.NET.Plugin.Pay.Alipay\Admin.NET.Plugin.Pay.Alipay.csproj", "{0A92F217-3412-053B-AD13-E0018B024DEE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Admin.NET.Plugin.PluginCoreManager", "Plugins\Admin.NET.Plugin.PluginCoreManager\Admin.NET.Plugin.PluginCoreManager.csproj", "{07C1B5BD-0A9F-A112-F257-0C6E4D0CC961}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Admin.NET.Plugin.Core", "Plugins\Admin.NET.Plugin.Core\Admin.NET.Plugin.Core.csproj", "{B78CD743-15E4-4295-1759-277929D38DF0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -82,6 +100,34 @@ Global {12998618-A875-4580-B5B1-0CC50CE85F27}.Debug|Any CPU.Build.0 = Debug|Any CPU {12998618-A875-4580-B5B1-0CC50CE85F27}.Release|Any CPU.ActiveCfg = Release|Any CPU {12998618-A875-4580-B5B1-0CC50CE85F27}.Release|Any CPU.Build.0 = Release|Any CPU + {D6A36A77-53EB-222C-B7E7-AA2D81F1A9B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D6A36A77-53EB-222C-B7E7-AA2D81F1A9B0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D6A36A77-53EB-222C-B7E7-AA2D81F1A9B0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D6A36A77-53EB-222C-B7E7-AA2D81F1A9B0}.Release|Any CPU.Build.0 = Release|Any CPU + {D66359C8-1F82-669F-A515-5A7F6A65DB8B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D66359C8-1F82-669F-A515-5A7F6A65DB8B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D66359C8-1F82-669F-A515-5A7F6A65DB8B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D66359C8-1F82-669F-A515-5A7F6A65DB8B}.Release|Any CPU.Build.0 = Release|Any CPU + {9E4C9BFE-E657-F410-9283-CAE6C370F264}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9E4C9BFE-E657-F410-9283-CAE6C370F264}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9E4C9BFE-E657-F410-9283-CAE6C370F264}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9E4C9BFE-E657-F410-9283-CAE6C370F264}.Release|Any CPU.Build.0 = Release|Any CPU + {BAB9ACF0-5AEE-290C-6D33-30712B6ADF14}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BAB9ACF0-5AEE-290C-6D33-30712B6ADF14}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BAB9ACF0-5AEE-290C-6D33-30712B6ADF14}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BAB9ACF0-5AEE-290C-6D33-30712B6ADF14}.Release|Any CPU.Build.0 = Release|Any CPU + {0A92F217-3412-053B-AD13-E0018B024DEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0A92F217-3412-053B-AD13-E0018B024DEE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0A92F217-3412-053B-AD13-E0018B024DEE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0A92F217-3412-053B-AD13-E0018B024DEE}.Release|Any CPU.Build.0 = Release|Any CPU + {07C1B5BD-0A9F-A112-F257-0C6E4D0CC961}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {07C1B5BD-0A9F-A112-F257-0C6E4D0CC961}.Debug|Any CPU.Build.0 = Debug|Any CPU + {07C1B5BD-0A9F-A112-F257-0C6E4D0CC961}.Release|Any CPU.ActiveCfg = Release|Any CPU + {07C1B5BD-0A9F-A112-F257-0C6E4D0CC961}.Release|Any CPU.Build.0 = Release|Any CPU + {B78CD743-15E4-4295-1759-277929D38DF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B78CD743-15E4-4295-1759-277929D38DF0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B78CD743-15E4-4295-1759-277929D38DF0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B78CD743-15E4-4295-1759-277929D38DF0}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -94,6 +140,15 @@ Global {9EB9C39E-E14F-443E-9AA3-EE417ABCBC1D} = {76F70D22-8D53-468E-A3B6-1704666A1D71} {1B106C11-E5BF-44AB-A283-1E948A8BD8C2} = {76F70D22-8D53-468E-A3B6-1704666A1D71} {12998618-A875-4580-B5B1-0CC50CE85F27} = {76F70D22-8D53-468E-A3B6-1704666A1D71} + {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {76F70D22-8D53-468E-A3B6-1704666A1D71} + {D6A36A77-53EB-222C-B7E7-AA2D81F1A9B0} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {D66359C8-1F82-669F-A515-5A7F6A65DB8B} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {9E4C9BFE-E657-F410-9283-CAE6C370F264} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {BAB9ACF0-5AEE-290C-6D33-30712B6ADF14} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {3F5B6AE4-951D-4358-AF94-288C4D484D48} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {0A92F217-3412-053B-AD13-E0018B024DEE} = {3F5B6AE4-951D-4358-AF94-288C4D484D48} + {07C1B5BD-0A9F-A112-F257-0C6E4D0CC961} = {76F70D22-8D53-468E-A3B6-1704666A1D71} + {B78CD743-15E4-4295-1759-277929D38DF0} = {76F70D22-8D53-468E-A3B6-1704666A1D71} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {5CD801D7-984A-4F5C-8FA2-211B7A5EA9F3} diff --git a/Admin.NET/Plugins/Admin.NET.Plugin.Core/Abstractions/Pay/IPayPlugin.cs b/Admin.NET/Plugins/Admin.NET.Plugin.Core/Abstractions/Pay/IPayPlugin.cs new file mode 100644 index 00000000..b8e872c0 --- /dev/null +++ b/Admin.NET/Plugins/Admin.NET.Plugin.Core/Abstractions/Pay/IPayPlugin.cs @@ -0,0 +1,20 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + + +using PluginCore.IPlugins; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Admin.NET.Plugin.Core.Abstractions.Pay; +public interface IPayPlugin : IPlugin +{ + string Name { get; set; } + string Say(); +} diff --git a/Admin.NET/Plugins/Admin.NET.Plugin.Core/Admin.NET.Plugin.Core.csproj b/Admin.NET/Plugins/Admin.NET.Plugin.Core/Admin.NET.Plugin.Core.csproj new file mode 100644 index 00000000..f3688bb2 --- /dev/null +++ b/Admin.NET/Plugins/Admin.NET.Plugin.Core/Admin.NET.Plugin.Core.csproj @@ -0,0 +1,27 @@ + + + + net8.0;net9.0 + 1701;1702;8616;1591;8618;8619;8629;8602;8603;8604;8625;8765 + Admin.NET.Plugin.Core.xml + enable + enable + + + + + + + + + + + + + + + + + + + diff --git a/Admin.NET/Plugins/Admin.NET.Plugin.Core/Admin.NET.Plugin.Core.xml b/Admin.NET/Plugins/Admin.NET.Plugin.Core/Admin.NET.Plugin.Core.xml new file mode 100644 index 00000000..e1f8a56e --- /dev/null +++ b/Admin.NET/Plugins/Admin.NET.Plugin.Core/Admin.NET.Plugin.Core.xml @@ -0,0 +1,8 @@ + + + + Admin.NET.Plugin.Core + + + + diff --git a/Admin.NET/Plugins/Admin.NET.Plugin.Core/GlobalUsing.cs b/Admin.NET/Plugins/Admin.NET.Plugin.Core/GlobalUsing.cs new file mode 100644 index 00000000..19a76f13 --- /dev/null +++ b/Admin.NET/Plugins/Admin.NET.Plugin.Core/GlobalUsing.cs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Admin.NET.Plugin.PluginCoreManager.csproj b/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Admin.NET.Plugin.PluginCoreManager.csproj new file mode 100644 index 00000000..c9629cd6 --- /dev/null +++ b/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Admin.NET.Plugin.PluginCoreManager.csproj @@ -0,0 +1,35 @@ + + + + net8.0;net9.0 + 1701;1702;1591;8632 + enable + disable + True + + + + + + + + + + + + + + + + + + + PreserveNewest + + + + + + + + diff --git a/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Controllers/AppCenterController.cs b/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Controllers/AppCenterController.cs new file mode 100644 index 00000000..be83e4d6 --- /dev/null +++ b/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Controllers/AppCenterController.cs @@ -0,0 +1,207 @@ +//=================================================== +// License: Apache-2.0 +// Contributors: yiyungent@gmail.com +// Project: https://moeci.com/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using PluginCore; +using PluginCore.Models; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using PluginCore.AspNetCore.Authorization; +using PluginCore.AspNetCore.ResponseModel; +//using ResponseModel; + + +namespace PluginCore.AspNetCore.Controllers +{ + /// + /// 应用中心 + /// 插件 + /// + [Route("api/plugincore/admin/[controller]/[action]")] + // [PluginCoreAdminAuthorize] + [ApiController] + [NonUnify] + + public class AppCenterController : ControllerBase + { + #region Fields + + private static Dictionary _pluginDownloadTasks; + + #endregion + + #region Ctor + + static AppCenterController() + { + _pluginDownloadTasks = new Dictionary(); + } + + public AppCenterController() + { + + } + #endregion + + #region Actions + + #region 插件列表 + /// + /// 插件 + /// + /// + /// + [HttpGet, HttpPost] + public async Task> Plugins(string query = "") + { + BaseResponseModel responseDTO = new BaseResponseModel(); + IList pluginRegistryModels = new List(); + try + { + // 1. TODO: 从json文件中读取插件订阅源 registry url + string registryUrl = ""; + // 2. TODO: 向订阅源发送 http get 获取插件列表信息 eg: http://rem-core-plugins-registry.moeci.com/?query=xxx + IList remotePluginIds = new List(); + + // 3. 根据本地已有 PluginId 插件情况 状态赋值 + PluginConfigModel pluginConfigModel = PluginConfigModelFactory.Create(); + // IList localPluginIds = pluginConfigModel.EnabledPlugins.Concat(pluginConfigModel.DisabledPlugins).Concat(pluginConfigModel.UninstalledPlugins).ToList(); + IList localPluginIds = PluginPathProvider.AllPluginFolderName(); + + + + responseDTO.Code = 1; + responseDTO.Message = "获取远程插件数据成功"; + responseDTO.Data = pluginRegistryModels; + } + catch (Exception ex) + { + responseDTO.Code = -1; + responseDTO.Message = "获取远程插件数据失败: " + ex.Message; + responseDTO.Data = pluginRegistryModels; + } + + return await Task.FromResult(responseDTO); + } + #endregion + + #region 下载插件 + [HttpGet, HttpPost] + public async Task> DownloadPlugin(string pluginDownloadUrl = "") + { + BaseResponseModel responseDTO = new BaseResponseModel(); + + #region 效验 + if (string.IsNullOrEmpty(pluginDownloadUrl)) + { + responseDTO.Code = -1; + responseDTO.Message = "插件下载地址不正确"; + return responseDTO; + } + // TODO: 效验是否本地已经存在相同pluginId的插件 + + #endregion + + try + { + // 1.执行下载操作, TODO:存在问题,阻塞对性能不好,但不阻塞又不好通知用户插件下载进度,以及可能存在在插件下载过程中,用户再次点击下载 + WebClient webClient = new WebClient(); + // TODO: 插件下载文件路径 + string pluginDownloadFilePath = ""; + //webClient.DownloadFileAsync(new Uri(pluginDownloadFilePath), ""); + Task task = webClient.DownloadFileTaskAsync(pluginDownloadUrl, pluginDownloadFilePath); + + _pluginDownloadTasks.Add(pluginDownloadUrl, task); + + webClient.DownloadFileCompleted += Plugin_DownloadFileCompleted; + webClient.DownloadProgressChanged += Plugin_DownloadProgressChanged; + webClient.Disposed += WebClient_Disposed; + + responseDTO.Code = 1; + responseDTO.Message = "开始下载插件"; + } + catch (Exception ex) + { + responseDTO.Code = -1; + responseDTO.Message = "下载插件失败: " + ex.Message; + } + + return await Task.FromResult(responseDTO); + } + + + + + #endregion + + #region 获取插件下载进度 + [HttpGet, HttpPost] + public async Task> DownloadPluginProgress() + { + BaseResponseModel responseDTO = new BaseResponseModel(); + try + { + responseDTO.Data = new { }; + + + + responseDTO.Code = 1; + responseDTO.Message = "获取插件下载进度成功"; + } + catch (Exception ex) + { + responseDTO.Code = -1; + responseDTO.Message = "获取插件下载进度失败: " + ex.Message; + } + + return await Task.FromResult(responseDTO); + } + #endregion + + #endregion + + #region Helpers + + /// + /// 插件下载完成 + /// + /// + /// + private void Plugin_DownloadFileCompleted(object sender, System.ComponentModel.AsyncCompletedEventArgs e) + { + Console.WriteLine("插件下载完成"); + // 1.从 _pluginDownloadTasks 中移除 + //_pluginDownloadTasks.Remove(); + // 2. 解压插件 + + } + + private void Plugin_DownloadProgressChanged(object sender, DownloadProgressChangedEventArgs e) + { + Console.WriteLine($"插件下载进度改变: {e.ProgressPercentage}% {e.BytesReceived}/{e.TotalBytesToReceive}"); + } + + private void WebClient_Disposed(object sender, EventArgs e) + { + if (sender is WebClient webClient) + { + Console.WriteLine(webClient.BaseAddress); + } + + Console.WriteLine(nameof(WebClient_Disposed) + ": " + sender.ToString()); + } + + #endregion + + } +} diff --git a/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Controllers/DebugController.cs b/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Controllers/DebugController.cs new file mode 100644 index 00000000..d64d4dff --- /dev/null +++ b/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Controllers/DebugController.cs @@ -0,0 +1,279 @@ +using System.Runtime.CompilerServices; +//=================================================== +// License: Apache-2.0 +// Contributors: yiyungent@gmail.com +// Project: https://moeci.com/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using Microsoft.AspNetCore.Mvc; +using PluginCore.AspNetCore.Authorization; +using PluginCore.AspNetCore.ResponseModel; +using PluginCore.Interfaces; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using System.Linq; +using System.Runtime.Loader; +using Microsoft.Extensions.DependencyInjection; +using System.Collections.Concurrent; +using PluginCore.AspNetCore.Extensions; + +namespace PluginCore.AspNetCore.Controllers +{ + /// + /// [ASP.NET Core — 依赖注入\_啊晚的博客-CSDN博客\_asp.net core 依赖注入](https://blog.csdn.net/weixin_37648525/article/details/127942292) + /// [ASP.NET Core中的依赖注入(3): 服务的注册与提供 - Artech - 博客园](https://www.cnblogs.com/artech/p/asp-net-core-di-register.html) + /// [ASP.NET Core中的依赖注入(5): ServiceProvider实现揭秘 【总体设计 】 - Artech - 博客园](https://www.cnblogs.com/artech/p/asp-net-core-di-service-provider-1.html) + /// [dotnet/ServiceProvider.cs at main · dotnet/dotnet](https://github.com/dotnet/dotnet/blob/main/src/runtime/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceProvider.cs) + /// [Net6 DI源码分析Part2 Engine,ServiceProvider - 一身大膘 - 博客园](https://www.cnblogs.com/hts92/p/15800990.html) + /// [【特别的骚气】asp.net core运行时注入服务,实现类库热插拔 - 四处观察 - 博客园](https://www.cnblogs.com/1996-Chinese-Chen/p/16154218.html) + /// + /// ActivatorUtilities.CreateInstance(serviceProvider, "test"); + /// ActivatorUtilities.GetServiceOrCreateInstance(serviceProvider); + /// + /// + [Route("api/plugincore/admin/[controller]/[action]")] + //[PluginCoreAdminAuthorize] + [ApiController] + [NonUnify] + public class DebugController : ControllerBase + { + #region Fields + private readonly IPluginContextManager _pluginContextManager; + #endregion + + #region Ctor + public DebugController(IPluginContextManager pluginContextManager) + { + _pluginContextManager = pluginContextManager; + } + #endregion + + #region Actions + + [HttpGet, HttpPost] + public async Task> PluginContexts() + { + BaseResponseModel responseModel = new BaseResponseModel(); + try + { + var pluginContextList = _pluginContextManager.All(); + Dictionary> keyValuePairs = new Dictionary>(); + foreach (var pluginContext in pluginContextList) + { + keyValuePairs.Add($"{pluginContext.GetType().ToString()} - {pluginContext.PluginId} - {pluginContext.GetHashCode()}", pluginContext.Assemblies.Select(m => m.FullName).ToList()); + } + + responseModel.Code = 1; + responseModel.Message = "success"; + responseModel.Data = keyValuePairs; + } + catch (Exception ex) + { + responseModel.Code = -1; + responseModel.Message = "error"; + responseModel.Data = ex.ToString(); + } + + return await Task.FromResult(responseModel); + } + + [HttpGet, HttpPost] + public async Task> AssemblyLoadContexts() + { + BaseResponseModel responseModel = new BaseResponseModel(); + try + { + var assemblyLoadContextDefault = AssemblyLoadContext.Default; + var assemblyLoadContextAll = AssemblyLoadContext.All; + var responseDataModel = new AssemblyLoadContextsResponseDataModel(); + responseDataModel.Default = new AssemblyLoadContextsResponseDataModel.AssemblyLoadContextModel + { + Name = assemblyLoadContextDefault.Name, + Type = assemblyLoadContextDefault.GetType().ToString(), + Assemblies = assemblyLoadContextDefault.Assemblies.Select(m => new AssemblyModel { FullName = m.FullName, DefinedTypes = m.DefinedTypes.Select(m => m.FullName).ToList() }).ToList() + }; + responseDataModel.All = assemblyLoadContextAll.Select(item => new AssemblyLoadContextsResponseDataModel.AssemblyLoadContextModel + { + Name = item.Name, + Type = item.GetType().ToString(), + Assemblies = item.Assemblies.Select(m => new AssemblyModel { FullName = m.FullName, DefinedTypes = m.DefinedTypes.Select(m => m.FullName).ToList() }).ToList() + }).ToList(); + + responseModel.Code = 1; + responseModel.Message = "success"; + responseModel.Data = responseDataModel; + } + catch (Exception ex) + { + responseModel.Code = -1; + responseModel.Message = "error"; + responseModel.Data = ex.ToString(); + } + + return await Task.FromResult(responseModel); + } + + [HttpGet, HttpPost] + public async Task> Assemblies() + { + BaseResponseModel responseModel = new BaseResponseModel(); + try + { + var assemblies = AppDomain.CurrentDomain.GetAssemblies(); + List assemblyModels = new List(); + foreach (var item in assemblies) + { + assemblyModels.Add(new AssemblyModel + { + FullName = item.FullName, + DefinedTypes = item.DefinedTypes.Select(m => m.FullName).ToList() + }); + } + + responseModel.Code = 1; + responseModel.Message = "success"; + responseModel.Data = assemblyModels; + } + catch (Exception ex) + { + responseModel.Code = -1; + responseModel.Message = "error"; + responseModel.Data = ex.ToString(); + } + + return await Task.FromResult(responseModel); + } + + [HttpGet, HttpPost] + public async Task> Services([FromServices] IServiceProvider serviceProvider) + { + BaseResponseModel responseModel = new BaseResponseModel(); + try + { + //IServiceProvider serviceProvider = HttpContext.RequestServices; + //var provider = serviceProvider.GetType().GetProperty("RootProvider", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + //var serviceField = provider.GetType().GetField("_realizedServices", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + //var serviceValue = serviceField.GetValue(provider); + //var funcType = serviceField.FieldType.GetGenericArguments()[1].GetGenericArguments()[0]; + //ConcurrentDictionary> realizedServices = (ConcurrentDictionary>)serviceValue; + + // 获取所有已经注册的服务 + var allService = serviceProvider.GetAllServiceDescriptors(); + + List serviceModels = new List(); + foreach (var item in allService) + { + serviceModels.Add(new ServiceModel + { + Type = item.Key.ToString(), + ImplementationType = item.Value.ImplementationType?.ToString() ?? "", + Lifetime = item.Value.Lifetime.ToString(), + TypeAssembly = new AssemblyModel + { + FullName = item.Key.Assembly.FullName, + }, + ImplementationTypeAssembly = new AssemblyModel + { + FullName = item.Value.ImplementationType?.Assembly?.FullName ?? "" + } + }); + } + + responseModel.Code = 1; + responseModel.Message = "success"; + responseModel.Data = serviceModels; + } + catch (Exception ex) + { + responseModel.Code = -1; + responseModel.Message = "error"; + responseModel.Data = ex.ToString(); + } + + return await Task.FromResult(responseModel); + } + + #endregion + + public sealed class AssemblyLoadContextsResponseDataModel + { + public AssemblyLoadContextModel Default + { + get; set; + } + + public List All + { + get; set; + } + + public sealed class AssemblyLoadContextModel + { + public string Name + { + get; set; + } + public string Type + { + get; set; + } + public List Assemblies + { + get; set; + } + } + } + + public sealed class AssembliesResponseDataModel + { + + } + + public sealed class ServiceModel + { + public string Type + { + get; set; + } + + public string ImplementationType + { + get; set; + } + + public string Lifetime + { + get; set; + } + + public AssemblyModel TypeAssembly + { + get; set; + } + + public AssemblyModel ImplementationTypeAssembly + { + get; set; + } + } + + public sealed class AssemblyModel + { + public string FullName + { + get; set; + } + + public List DefinedTypes + { + get; set; + } + } + + } +} diff --git a/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Controllers/PluginWidgetController.cs b/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Controllers/PluginWidgetController.cs new file mode 100644 index 00000000..8267dd3a --- /dev/null +++ b/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Controllers/PluginWidgetController.cs @@ -0,0 +1,91 @@ +//=================================================== +// License: Apache-2.0 +// Contributors: yiyungent@gmail.com +// Project: https://moeci.com/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using PluginCore.AspNetCore.ResponseModel; +using PluginCore.Interfaces; +using PluginCore.IPlugins; + +namespace PluginCore.AspNetCore.Controllers +{ + [Route("api/plugincore/[controller]/[action]")] + [ApiController] + [NonUnify] + public class PluginWidgetController : ControllerBase + { + #region Fields + private readonly IPluginFinder _pluginFinder; + #endregion + + #region Ctor + public PluginWidgetController(IPluginFinder pluginFinder) + { + _pluginFinder = pluginFinder; + } + #endregion + + #region Actions + + #region Widget + /// + /// Widget + /// + /// + [HttpGet, HttpPost] + //public async Task> Widget(string widgetKey, string extraPars = "") + public async Task Widget(string widgetKey, string extraPars = "") + { + BaseResponseModel responseModel = new ResponseModel.BaseResponseModel(); + string responseData = ""; + widgetKey = widgetKey.Trim('"', '\''); + string[] extraParsArr = null; + if (!string.IsNullOrEmpty(extraPars)) + { + extraParsArr = extraPars.Split(",", StringSplitOptions.RemoveEmptyEntries); + extraParsArr = extraParsArr.Select(m => m.Trim('"', '\'')).ToArray(); + } + StringBuilder sb = new StringBuilder(); + sb.AppendLine($""); + try + { + List plugins = this._pluginFinder.EnablePlugins().ToList(); + foreach (var item in plugins) + { + string widgetStr = await item.Widget(widgetKey, extraParsArr); + if (!string.IsNullOrEmpty(widgetStr)) + { + // TODO: 配合 PluginCoreConfig.PluginWidgetDebug + // TODO: PluginCoreConfig 改为 Options 模式, 避免手动反复读取文件 效率低 + //sb.AppendLine($""); + + sb.AppendLine(widgetStr); + } + } + + } + catch (Exception ex) + { + Utils.LogUtil.Error(ex.ToString()); + sb.AppendLine($""); + } + sb.AppendLine($""); + responseData = sb.ToString(); + + responseModel.Code = 1; + responseModel.Message = "Load Widget Success"; + responseModel.Data = responseData; + + //return await Task.FromResult(responseModel); + return Content(responseData, "text/html;charset=utf-8"); + } + #endregion + + #endregion + + } +} diff --git a/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Controllers/PluginsController.cs b/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Controllers/PluginsController.cs new file mode 100644 index 00000000..98fb2553 --- /dev/null +++ b/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Controllers/PluginsController.cs @@ -0,0 +1,680 @@ +//=================================================== +// License: Apache-2.0 +// Contributors: yiyungent@gmail.com +// Project: https://moeci.com/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Generic; +using System.Configuration; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +//using Core.Common; +//using Framework.Authorization; +using PluginCore; +using PluginCore.Models; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using PluginCore.AspNetCore.Authorization; +using PluginCore.Infrastructure; +using PluginCore.IPlugins; +using PluginCore.AspNetCore.ResponseModel; +using PluginCore.Interfaces; +using PluginCore.AspNetCore.Interfaces; + +//using ResponseModel; + +namespace PluginCore.AspNetCore.Controllers +{ + [Route("api/plugincore/admin/[controller]/[action]")] + // [PluginCoreAdminAuthorize] + [ApiController] + [NonUnify] + public class PluginsController : ControllerBase + { + #region Fields + private readonly IPluginManager _pluginManager; + private readonly IPluginFinder _pluginFinder; + private readonly IPluginApplicationBuilderManager _pluginApplicationBuilderManager; + #endregion + + #region Ctor + public PluginsController(IPluginManager pluginManager, IPluginFinder pluginFinder, IPluginApplicationBuilderManager pluginApplicationBuilderManager) + { + _pluginManager = pluginManager; + _pluginFinder = pluginFinder; + _pluginApplicationBuilderManager = pluginApplicationBuilderManager; + } + #endregion + + #region Actions + + #region 插件列表 + /// + /// 加载插件列表 + /// + /// 插件状态 + /// + [HttpGet, HttpPost] + public async Task> List(string status = "all") + { + BaseResponseModel responseData = new ResponseModel.BaseResponseModel(); + var pluginConfigModel = PluginConfigModelFactory.Create(); + + // 获取所有插件信息 + IList pluginInfoModels = PluginInfoModelFactory.CreateAll(); + IList responseModels = new List(); + string[] enablePluginIds = _pluginFinder.EnablePluginIds().ToArray(); + + // 添加插件状态 + responseModels = PluginInfoModelToResponseModel(pluginInfoModels, pluginConfigModel, enablePluginIds); + #region 筛选插件状态 + switch (status.ToLower()) + { + case "all": + break; + case "enabled": + responseModels = responseModels.Where(m => m.Status == PluginStatus.Enabled).ToList(); + break; + case "disabled": + responseModels = responseModels.Where(m => m.Status == PluginStatus.Disabled).ToList(); + break; + default: + break; + } + #endregion + + responseData.Code = 1; + responseData.Message = "加载插件列表成功"; + responseData.Data = responseModels; + + return await Task.FromResult(responseData); + } + #endregion + + #region 卸载插件 + [HttpGet, HttpPost] + public async Task> Uninstall(string pluginId) + { + BaseResponseModel responseData = new BaseResponseModel(); + pluginId = pluginId.Trim(); + var pluginConfigModel = PluginConfigModelFactory.Create(); + // 卸载插件 必须 先禁用插件 + #region 效验 + if (pluginConfigModel.EnabledPlugins.Contains(pluginId)) + { + responseData.Code = -1; + responseData.Message = "卸载失败: 请先禁用此插件"; + return await Task.FromResult(responseData); + } + string pluginDirStr= Path.Combine(PluginPathProvider.PluginsRootPath(), pluginId); + string pluginWwwrootDirStr = Path.Combine(PluginPathProvider.PluginsWwwRootDir(), pluginId); + if (!Directory.Exists(pluginDirStr) && !Directory.Exists(pluginWwwrootDirStr)) + { + responseData.Code = -2; + responseData.Message = "卸载失败: 此插件不存在"; + return await Task.FromResult(responseData); + } + #endregion + + try + { + // PS:卸载插件必须先禁用插件,所以此时插件LoadContext已被移除释放(插件Assemblies已被释放), 此处不需移除LoadContext + + // 1.删除物理文件 + var pluginDir = new DirectoryInfo(pluginDirStr); + if (pluginDir.Exists) { + pluginDir.Delete(true); + } + // 虽然 已禁用 时 pluginWwwrootDirStr/pluginId 已删除, 但为确保, 还是再删除一次 + var pluginWwwrootDir = new DirectoryInfo(pluginWwwrootDirStr); + if (pluginWwwrootDir.Exists) { + pluginWwwrootDir.Delete(true); + } + + responseData.Code = 1; + responseData.Message = "卸载成功"; + } + catch (Exception ex) + { + responseData.Code = -1; + responseData.Message = "卸载失败: " + ex.Message; + } + + return await Task.FromResult(responseData); + } + #endregion + + #region 启用插件 + [HttpGet, HttpPost] + public async Task> Enable(string pluginId) + { + BaseResponseModel responseData = new BaseResponseModel(); + var pluginConfigModel = PluginConfigModelFactory.Create(); + // 效验是否存在于 已禁用插件列表 + #region 效验 + pluginId = pluginId.Trim(); + var pluginDir = new DirectoryInfo(Path.Combine(PluginPathProvider.PluginsRootPath(), pluginId)); + if (pluginDir != null && !pluginDir.Exists) + { + responseData.Code = -1; + responseData.Message = "启用失败: 此插件不存在"; + return await Task.FromResult(responseData); + } + string[] enablePluginIds = _pluginFinder.EnablePluginIds().ToArray(); + if (enablePluginIds.Contains(pluginId)) { + responseData.Code = -2; + responseData.Message = "启用失败: 此插件已启用"; + return await Task.FromResult(responseData); + } + #endregion + + try + { + // 1. 创建插件程序集加载上下文, 添加到 PluginsLoadContexts + _pluginManager.LoadPlugin(pluginId); + // 2. 添加到 pluginConfigModel.EnabledPlugins + pluginConfigModel.EnabledPlugins.Add(pluginId); + // 4.保存到 plugin.config.json + PluginConfigModelFactory.Save(pluginConfigModel); + + // 5. 找到此插件实例 + IPlugin plugin = _pluginFinder.Plugin(pluginId); + if (plugin == null) + { + // 7.启用不成功, 回滚插件状态: (1)释放插件上下文 (2)更新 plugin.config.json + try + { + _pluginManager.UnloadPlugin(pluginId); + } + catch (Exception ex) + { } + + // 从 pluginConfigModel.EnabledPlugins 移除 + pluginConfigModel.EnabledPlugins.Remove(pluginId); + // 保存到 plugin.config.json + PluginConfigModelFactory.Save(pluginConfigModel); + + responseData.Code = -1; + responseData.Message = "启用失败: 此插件不存在"; + return await Task.FromResult(responseData); + } + // 6.调取插件的 AfterEnable(), 插件开发者可在此回收资源 + var pluginEnableResult = plugin.AfterEnable(); + if (!pluginEnableResult.IsSuccess) + { + // 7.启用不成功, 回滚插件状态: (1)释放插件上下文 (2)更新 plugin.config.json + try + { + _pluginManager.UnloadPlugin(pluginId); + } + catch (Exception ex) + { } + + // 从 pluginConfigModel.EnabledPlugins 移除 + pluginConfigModel.EnabledPlugins.Remove(pluginId); + // 保存到 plugin.config.json + PluginConfigModelFactory.Save(pluginConfigModel); + + responseData.Code = -1; + responseData.Message = "启用失败: 来自插件的错误信息: " + pluginEnableResult.Message; + return await Task.FromResult(responseData); + } + + // 7. ReBuild + this._pluginApplicationBuilderManager.ReBuild(); + + // 8. 尝试复制 插件下的 wwwroot 到 Plugins_wwwroot + string wwwRootDir = PluginPathProvider.WwwRootDir(pluginId); + if (Directory.Exists(wwwRootDir)) + { + string targetDir = PluginPathProvider.PluginWwwRootDir(pluginId); + Utils.FileUtil.CopyFolder(wwwRootDir, targetDir); + } + + responseData.Code = 1; + responseData.Message = "启用成功"; + } + catch (Exception ex) + { + responseData.Code = -2; + responseData.Message = "启用失败: " + ex.Message; + } + + return await Task.FromResult(responseData); + } + #endregion + + #region 禁用插件 + [HttpGet, HttpPost] + public async Task> Disable(string pluginId) + { + BaseResponseModel responseData = new BaseResponseModel(); + #region 效验 + pluginId = pluginId.Trim(); + var pluginConfigModel = PluginConfigModelFactory.Create(); + // string[] enablePluginIds = _pluginFinder.EnablePluginIds().ToArray(); + // // 效验是否存在于 已启用插件列表 + // if (!enablePluginIds.Contains(pluginId)) + // { + // responseData.Code = -1; + // responseData.Message = "禁用失败: 此插件不存在, 或未启用"; + // return await Task.FromResult(responseData); + // } + #endregion + + try + { + // 1. 找到此插件实例 + IPlugin plugin = _pluginFinder.Plugin(pluginId); + if (plugin == null) + { + responseData.Code = -1; + responseData.Message = "禁用失败: 此插件不存在, 或未启用"; + return await Task.FromResult(responseData); + } + try + { + // 2.调取插件的 BeforeDisable(), 插件开发者可在此回收资源 + var pluginDisableResult = plugin.BeforeDisable(); + if (!pluginDisableResult.IsSuccess) + { + responseData.Code = -1; + responseData.Message = "禁用失败: 来自插件的错误信息: " + pluginDisableResult.Message; + return await Task.FromResult(responseData); + } + // 3.移除插件对应的程序集加载上下文 + _pluginManager.UnloadPlugin(pluginId); + // 3.1. ReBuild + this._pluginApplicationBuilderManager.ReBuild(); + if (pluginConfigModel.EnabledPlugins.Contains(pluginId)) { + // 4.从 pluginConfigModel.EnabledPlugins 移除 + pluginConfigModel.EnabledPlugins.Remove(pluginId); + // 5.保存到 plugin.config.json + PluginConfigModelFactory.Save(pluginConfigModel); + } + } + catch (Exception ex) + { + Utils.LogUtil.Error(ex.ToString()); + responseData.Code = -1; + responseData.Message = "禁用失败: 此插件不存在, 或未启用"; + return await Task.FromResult(responseData); + } + + // 7. 尝试移除 Plugins_wwwroot/PluginId + string pluginWwwRootDir = PluginPathProvider.PluginWwwRootDir(pluginId); + if (Directory.Exists(pluginWwwRootDir)) + { + Directory.Delete(pluginWwwRootDir, true); + } + + + responseData.Code = 1; + responseData.Message = "禁用成功"; + } + catch (Exception ex) + { + responseData.Code = -2; + responseData.Message = "禁用失败: " + ex.Message; + } + + return await Task.FromResult(responseData); + } + #endregion + + #region 上传插件 + /// + /// 上传插件 + /// + /// 注意: 参数名一定为 file, 对应前端传过来时以 file 为名 + /// + [HttpGet, HttpPost] + public async Task> Upload([FromForm] IFormFile file) + { + BaseResponseModel responseData = new BaseResponseModel(); + + #region 效验 + if (file == null) + { + responseData.Code = -1; + responseData.Message = "上传的文件不能为空"; + return responseData; + } + //文件后缀 + string fileExtension = Path.GetExtension(file.FileName);//获取文件格式,拓展名 + // 类型标记 + UploadFileType uploadFileType = UploadFileType.NoAllowedType; + switch (fileExtension) + { + case ".zip": + uploadFileType = UploadFileType.Zip; + break; + case ".nupkg": + uploadFileType = UploadFileType.Nupkg; + break; + } + + if (fileExtension != ".zip" && fileExtension != ".nupkg") + { + responseData.Code = -1; + // nupkg 其实就是 zip + responseData.Message = "只能上传 zip 或 nupkg 格式文件"; + return responseData; + } + // PluginCore.AspNetCore-v1.0.2 起 不再限制插件上传大小 + //判断文件大小 + //var fileSize = file.Length; + //if (fileSize > 1024 * 1024 * 5) // 5M + //{ + // responseData.Code = -1; + // responseData.Message = "上传的文件不能大于5MB"; + // return responseData; + //} + #endregion + + try + { + // 1.先上传到 临时插件上传目录, 用Guid.zip作为保存文件名 + string tempZipFilePath = Path.Combine(PluginPathProvider.TempPluginUploadDir(), Guid.NewGuid() + ".zip"); + using (var fs = System.IO.File.Create(tempZipFilePath)) + { + file.CopyTo(fs); //将上传的文件文件流,复制到fs中 + fs.Flush();//清空文件流 + } + // 2.解压 + bool isDecomparessSuccess = false; + if (uploadFileType == UploadFileType.Zip) + { + isDecomparessSuccess = Utils.ZipHelper.DecomparessFile(tempZipFilePath, tempZipFilePath.Replace(".zip", "")); + } + else if (uploadFileType == UploadFileType.Nupkg) + { + isDecomparessSuccess = NupkgService.DecomparessFile(tempZipFilePath, tempZipFilePath.Replace(".zip", "")); + } + + // 3.删除原压缩包 + System.IO.File.Delete(tempZipFilePath); + if (!isDecomparessSuccess) + { + responseData.Code = -1; + responseData.Message = "解压插件压缩包失败"; + return responseData; + } + // 4.读取其中的info.json, 获取 PluginId 值 + PluginInfoModel pluginInfoModel = PluginInfoModelFactory.ReadPluginDir(tempZipFilePath.Replace(".zip", "")); + if (pluginInfoModel == null || string.IsNullOrEmpty(pluginInfoModel.PluginId)) + { + // 记得删除已不再需要的临时插件文件夹 + Directory.Delete(tempZipFilePath.Replace(".zip", ""), true); + + responseData.Code = -1; + responseData.Message = "不合法的插件"; + return responseData; + } + string pluginId = pluginInfoModel.PluginId; + // 5.检索 此 PluginId 是否本地插件已存在 + var pluginConfigModel = PluginConfigModelFactory.Create(); + // 本地已经存在的 PluginId + IList localExistPluginIds = PluginPathProvider.AllPluginFolderName(); + if (localExistPluginIds.Contains(pluginId)) + { + // 记得删除已不再需要的临时插件文件夹 + Directory.Delete(tempZipFilePath.Replace(".zip", ""), true); + + responseData.Code = -1; + responseData.Message = $"本地已有此插件 (PluginId: {pluginId}), 请前往插件列表删除后, 再上传"; + return responseData; + } + // 6.本地无此插件 -> 移动插件文件夹到 Plugins 下, 并以 PluginId 为插件文件夹名 + string pluginsRootPath = PluginPathProvider.PluginsRootPath(); + string newPluginDir = Path.Combine(pluginsRootPath, pluginId); + Directory.Move(tempZipFilePath.Replace(".zip", ""), newPluginDir); + + // 7. 放入 Plugins 中, 默认为 已禁用 + + responseData.Code = 1; + responseData.Message = $"上传插件成功 (PluginId: {pluginId})"; + } + catch (Exception ex) + { + responseData.Code = -1; + responseData.Message = "上传插件失败: " + ex.Message; + ex = ex.InnerException; + while (ex != null) + { + responseData.Message += " - " + ex.InnerException.Message; + ex = ex.InnerException; + } + } + + return await Task.FromResult(responseData); + } + #endregion + + #region 查看详细 + [HttpGet, HttpPost] + public async Task> Details(string pluginId) + { + BaseResponseModel responseData = new BaseResponseModel(); + + try + { + #region 效验 + pluginId = pluginId.Trim(); + var pluginConfigModel = PluginConfigModelFactory.Create(); + string[] localPluginIds = PluginPathProvider.AllPluginFolderName().ToArray(); + + if (!localPluginIds.Contains(pluginId)) + { + responseData.Code = -1; + responseData.Message = $"查看详细失败: 不存在 {pluginId} 插件"; + return await Task.FromResult(responseData); + } + + #endregion + + PluginInfoModel pluginInfoModel = PluginInfoModelFactory.Create(pluginId); + string[] enablePluginIds = _pluginFinder.EnablePluginIds().ToArray(); + PluginInfoResponseModel pluginInfoResponseModel = PluginInfoModelToResponseModel(new List() { pluginInfoModel }, pluginConfigModel, enablePluginIds).FirstOrDefault(); + + + responseData.Code = 1; + responseData.Message = "查看详细成功"; + responseData.Data = pluginInfoResponseModel; + } + catch (Exception ex) + { + responseData.Code = -1; + responseData.Message = "查看详细失败: " + ex.Message; + } + + return await Task.FromResult(responseData); + } + #endregion + + #region 查看文档 + [HttpGet, HttpPost] + public async Task> Readme(string pluginId) + { + BaseResponseModel responseData = new BaseResponseModel(); + + try + { + #region 效验 + pluginId = pluginId.Trim(); + // var pluginConfigModel = PluginConfigModelFactory.Create(); + string[] localPluginIds = PluginPathProvider.AllPluginFolderName().ToArray(); + + if (!localPluginIds.Contains(pluginId)) + { + responseData.Code = -1; + responseData.Message = $"查看文档失败: 不存在 {pluginId} 插件"; + return await Task.FromResult(responseData); + } + + #endregion + + PluginReadmeModel readmeModel = PluginReadmeModelFactory.Create(pluginId); + PluginReadmeResponseModel readmeResponseModel = new PluginReadmeResponseModel(); + readmeResponseModel.Content = readmeModel?.Content ?? ""; + readmeResponseModel.PluginId = pluginId; + + responseData.Code = 1; + responseData.Message = "查看文档成功"; + responseData.Data = readmeResponseModel; + } + catch (Exception ex) + { + responseData.Code = -1; + responseData.Message = "查看文档失败: " + ex.Message; + } + + return await Task.FromResult(responseData); + } + #endregion + + #region 设置 + [HttpGet] + public async Task> Settings(string pluginId) + { + BaseResponseModel responseData = new BaseResponseModel(); + + try + { + #region 效验 + pluginId = pluginId.Trim(); + // var pluginConfigModel = PluginConfigModelFactory.Create(); + string[] localPluginIds = PluginPathProvider.AllPluginFolderName().ToArray(); + + if (!localPluginIds.Contains(pluginId)) + { + responseData.Code = -1; + responseData.Message = $"查看设置失败: 不存在 {pluginId} 插件"; + return await Task.FromResult(responseData); + } + + #endregion + + string settingsJsonStr = PluginSettingsModelFactory.Create(pluginId); + + + responseData.Code = 1; + responseData.Message = "查看设置成功"; + responseData.Data = settingsJsonStr ?? "无设置项"; + } + catch (Exception ex) + { + responseData.Code = -1; + responseData.Message = "查看设置失败: " + ex.Message; + } + + return await Task.FromResult(responseData); + } + + [HttpPost] + public async Task> Settings(PluginSettingsInputModel inputModel) + { + BaseResponseModel responseData = new BaseResponseModel(); + + try + { + #region 效验 + inputModel.PluginId = inputModel.PluginId.Trim(); + // var pluginConfigModel = PluginConfigModelFactory.Create(); + string[] localPluginIds = PluginPathProvider.AllPluginFolderName().ToArray(); + + if (!localPluginIds.Contains(inputModel.PluginId)) + { + responseData.Code = -1; + responseData.Message = $"设置失败: 不存在 {inputModel.PluginId} 插件"; + return await Task.FromResult(responseData); + } + + #endregion + + inputModel.Data = inputModel.Data ?? ""; + PluginSettingsModelFactory.Save(pluginSettingsJsonStr: inputModel.Data, pluginId: inputModel.PluginId); + + + responseData.Code = 1; + responseData.Message = "设置成功"; + responseData.Data = inputModel.Data; + } + catch (Exception ex) + { + responseData.Code = -1; + responseData.Message = "设置失败: " + ex.Message; + } + + return await Task.FromResult(responseData); + } + #endregion + + #endregion + + + #region Helpers + + [NonAction] + private IList PluginInfoModelToResponseModel(IList pluginInfoModels, PluginConfigModel pluginConfigModel, string[] enablePluginIds) + { + // 获取 Plugins 下所有插件 + // DirectoryInfo pluginsDir = new DirectoryInfo(PluginPathProvider.PluginsRootPath()); + // List pluginIds = pluginsDir?.GetDirectories()?.Select(m => m.Name)?.ToList() ?? new List(); + + IList responseModels = new List(); + #region 添加插件状态信息 + foreach (var model in pluginInfoModels) + { + PluginInfoResponseModel responseModel = new PluginInfoResponseModel(); + responseModel.Author = model.Author; + responseModel.Description = model.Description; + responseModel.DisplayName = model.DisplayName; + responseModel.PluginId = model.PluginId; + responseModel.SupportedVersions = model.SupportedVersions; + responseModel.Version = model.Version; + responseModel.DependPlugins = model.DependPlugins; + + if (pluginConfigModel.EnabledPlugins.Contains(model.PluginId) && !enablePluginIds.Contains(model.PluginId)) { + // 错误情况: 配置 标识 已启用, 但实际没有启用成功 + pluginConfigModel.EnabledPlugins.Remove(model.PluginId); + PluginConfigModelFactory.Save(pluginConfigModel); + + responseModel.Status = PluginStatus.Disabled; + } else if(!pluginConfigModel.EnabledPlugins.Contains(model.PluginId) && enablePluginIds.Contains(model.PluginId)) + { + // 错误情况: 配置没有标识 已启用, 但实际 已启用 + pluginConfigModel.EnabledPlugins.Add(model.PluginId); + PluginConfigModelFactory.Save(pluginConfigModel); + + responseModel.Status = PluginStatus.Enabled; + } else if (pluginConfigModel.EnabledPlugins.Contains(model.PluginId) && enablePluginIds.Contains(model.PluginId)) + { + responseModel.Status = PluginStatus.Enabled; + } + else + { + responseModel.Status = PluginStatus.Disabled; + } + responseModels.Add(responseModel); + } + #endregion + + return responseModels; + } + + public enum UploadFileType + { + NoAllowedType = 0, + Zip = 1, + Nupkg = 2 + } + + #endregion + } +} diff --git a/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Controllers/UserController.cs b/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Controllers/UserController.cs new file mode 100644 index 00000000..fea34d0b --- /dev/null +++ b/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Controllers/UserController.cs @@ -0,0 +1,204 @@ +//=================================================== +// License: Apache-2.0 +// Contributors: yiyungent@gmail.com +// Project: https://moeci.com/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using PluginCore.AspNetCore.Authorization; +using PluginCore.Config; +using PluginCore.AspNetCore.RequestModel.User; +using PluginCore.AspNetCore.ResponseModel; +using Microsoft.AspNetCore.Authorization; +using Admin.NET.Core.Service; +using Admin.NET.Core; +using Furion.DataEncryption; +using Furion.FriendlyException; +using Lazy.Captcha.Core; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel; + +namespace PluginCore.AspNetCore.Controllers +{ + [Route("api/plugincore/admin/[controller]/[action]")] + [ApiController] + [NonUnify] + public class UserController : ControllerBase + { + + + public string RemoteFronted + { + get + { + return PluginCore.Config.PluginCoreConfigFactory.Create().RemoteFrontend; + } + } + + private readonly IUserManager _userManager; + private readonly SqlSugarRepository _sysUserRep; + private readonly SysOrgService _sysOrgService; + private readonly SysUserExtOrgService _sysUserExtOrgService; + private readonly SysUserRoleService _sysUserRoleService; + private readonly SysConfigService _sysConfigService; + + public UserController(IUserManager userManager, + SqlSugarRepository sysUserRep, + SysOrgService sysOrgService, + SysUserExtOrgService sysUserExtOrgService, + SysUserRoleService sysUserRoleService, + SysConfigService sysConfigService) + { + _userManager = userManager; + _sysUserRep = sysUserRep; + _sysOrgService = sysOrgService; + _sysUserExtOrgService = sysUserExtOrgService; + _sysUserRoleService = sysUserRoleService; + _sysConfigService = sysConfigService; + } + /// + /// 登录系统 + /// + /// + /// 用户名/密码:superadmin/123456 + /// + [AllowAnonymous] + [HttpGet, HttpPost] + [DisplayName("登录系统")] + public async Task> Login([FromBody] LoginRequestModel input) + { + BaseResponseModel responseModel = new BaseResponseModel(); + + + // 账号是否存在 + var user = await _sysUserRep.AsQueryable().Includes(t => t.SysOrg).Filter(null, true).FirstAsync(u => u.Account.Equals(input.UserName)); + _ = user ?? throw Oops.Oh(ErrorCodeEnum.D0009); + + // 账号是否被冻结 + if (user.Status == StatusEnum.Disable) + throw Oops.Oh(ErrorCodeEnum.D1017); + + // 租户是否被禁用 + var tenant = await _sysUserRep.ChangeRepository>().GetFirstAsync(u => u.Id == user.TenantId); + if (tenant != null && tenant.Status == StatusEnum.Disable) + throw Oops.Oh(ErrorCodeEnum.Z1003); + + // 密码是否正确 + if (CryptogramUtil.CryptoType == CryptogramEnum.MD5.ToString()) + { + if (user.Password != MD5Encryption.Encrypt(input.Password)) + throw Oops.Oh(ErrorCodeEnum.D1000); + } + else + { + if (CryptogramUtil.Decrypt(user.Password) != input.Password) + throw Oops.Oh(ErrorCodeEnum.D1000); + } + + + var tokenExpire = await _sysConfigService.GetTokenExpire(); + var refreshTokenExpire = await _sysConfigService.GetRefreshTokenExpire(); + + // 生成Token令牌 + var accessToken = JWTEncryption.Encrypt(new Dictionary + { + { ClaimConst.UserId, user.Id }, + { ClaimConst.TenantId, user.TenantId }, + { ClaimConst.Account, user.Account }, + { ClaimConst.RealName, user.RealName }, + { ClaimConst.AccountType, user.AccountType }, + { ClaimConst.OrgId, user.OrgId }, + { ClaimConst.OrgName, user.SysOrg?.Name }, + { ClaimConst.OrgType, user.SysOrg?.OrgType }, + }, tokenExpire); + + // 生成刷新Token令牌 + var refreshToken = JWTEncryption.GenerateRefreshToken(accessToken, refreshTokenExpire); + + responseModel.Code = 1; + responseModel.Message = "登录成功"; + responseModel.Data = new + { + token = accessToken, + userName = user.NickName, + RefreshToken = refreshToken + }; + return await Task.FromResult(responseModel); + + } + + + [HttpGet, HttpPost] + public async Task> Logout() + { + BaseResponseModel responseModel = new BaseResponseModel() + { + Code = 1, + Message = "退出登录成功" + }; + + return await Task.FromResult(responseModel); + } + + + [HttpGet, HttpPost] + public async Task> Info() + { + BaseResponseModel responseModel = new BaseResponseModel(); + + try + { + string adminUserName = PluginCoreConfigFactory.Create().Admin.UserName; + + responseModel.Code = 1; + responseModel.Message = "成功"; + responseModel.Data = new + { + name = adminUserName, + //avatar = this.RemoteFronted + "/images/avatar.gif" + avatar = "" + }; + } + catch (Exception ex) + { + responseModel.Code = -1; + responseModel.Message = "失败: " + ex.Message; + } + + return await Task.FromResult(responseModel); + } + + + [HttpGet, HttpPost] + public async Task> Update([FromBody] UpdateRequestModel requestModel) + { + BaseResponseModel responseModel = new BaseResponseModel(); + + try + { + PluginCoreConfig pluginCoreConfig = PluginCoreConfigFactory.Create(); + pluginCoreConfig.Admin.UserName = requestModel.UserName; + pluginCoreConfig.Admin.Password = requestModel.Password; + PluginCoreConfigFactory.Save(pluginCoreConfig); + + responseModel.Code = 1; + responseModel.Message = "修改成功, 需要重新登录"; + } + catch (Exception ex) + { + responseModel.Code = -1; + responseModel.Message = "失败: " + ex.Message; + } + + return await Task.FromResult(responseModel); + } + + } +} diff --git a/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Entity/SysPluginCore.cs b/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Entity/SysPluginCore.cs new file mode 100644 index 00000000..899047dd --- /dev/null +++ b/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Entity/SysPluginCore.cs @@ -0,0 +1,73 @@ +// 麻省理工学院许可证 +// +// 版权所有 (c) 2021-2023 zuohuaijun,大名科技(天津)有限公司 联系电话/微信:18020030720 QQ:515096995 +// +// 特此免费授予获得本软件的任何人以处理本软件的权利,但须遵守以下条件:在所有副本或重要部分的软件中必须包括上述版权声明和本许可声明。 +// +// 软件按“原样”提供,不提供任何形式的明示或暗示的保证,包括但不限于对适销性、适用性和非侵权的保证。 +// 在任何情况下,作者或版权持有人均不对任何索赔、损害或其他责任负责,无论是因合同、侵权或其他方式引起的,与软件或其使用或其他交易有关。 + + +namespace Admin.NET.Plugin.PluginCoreManager; + +/// +/// 系统动态插件表 +/// +[SugarTable(null, "系统动态插件表")] +[SysTable] +public class SysPluginCore : EntityTenant +{ + /// + /// 插件ID + /// + [SugarColumn(ColumnDescription = "插件ID", Length = 128)] + [Required] + public virtual string PluginId { get; set; } + + /// + /// 名称 + /// + [SugarColumn(ColumnDescription = "名称", Length = 64)] + [Required, MaxLength(64)] + public virtual string DisplayName { get; set; } + + /// + /// 作者 + /// + [SugarColumn(ColumnDescription = "作者", Length = 64)] + [Required, MaxLength(64)] + public virtual string Author { get; set; } + + /// + /// 版本 + /// + [SugarColumn(ColumnDescription = "版本", Length = 64)] + [Required, MaxLength(64)] + public virtual string Version { get; set; } + + /// + /// 描述 + /// + [SugarColumn(ColumnDescription = "描述", Length = 512)] + [MaxLength(512)] + public string? Description { get; set; } + + /// + /// 排序 + /// + [SugarColumn(ColumnDescription = "排序")] + public int OrderNo { get; set; } = 100; + + /// + /// 状态 + /// + [SugarColumn(ColumnDescription = "状态")] + public StatusEnum Status { get; set; } = StatusEnum.Enable; + + /// + /// 备注 + /// + [SugarColumn(ColumnDescription = "备注", Length = 128)] + [MaxLength(128)] + public string? Remark { get; set; } +} \ No newline at end of file diff --git a/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/GlobalUsings.cs b/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/GlobalUsings.cs new file mode 100644 index 00000000..11d11f46 --- /dev/null +++ b/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/GlobalUsings.cs @@ -0,0 +1,25 @@ +// 麻省理工学院许可证 +// +// 版权所有 (c) 2021-2023 zuohuaijun,大名科技(天津)有限公司 联系电话/微信:18020030720 QQ:515096995 +// +// 特此免费授予获得本软件的任何人以处理本软件的权利,但须遵守以下条件:在所有副本或重要部分的软件中必须包括上述版权声明和本许可声明。 +// +// 软件按“原样”提供,不提供任何形式的明示或暗示的保证,包括但不限于对适销性、适用性和非侵权的保证。 +// 在任何情况下,作者或版权持有人均不对任何索赔、损害或其他责任负责,无论是因合同、侵权或其他方式引起的,与软件或其使用或其他交易有关。 +global using Admin.NET.Core; +global using Admin.NET.Core.Service; +global using Furion; +global using Furion.DependencyInjection; +global using Furion.DynamicApiController; +global using Furion.FriendlyException; +global using Mapster; +global using Microsoft.AspNetCore.Http; +global using Microsoft.AspNetCore.Mvc; +global using Microsoft.Extensions.DependencyInjection; +global using Newtonsoft.Json; +global using SqlSugar; +global using System.ComponentModel; +global using System.ComponentModel.DataAnnotations; +global using System.Data; +global using System.Linq.Dynamic.Core; +global using System.Text; diff --git a/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/SeedData/SysMenuSeedData.cs b/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/SeedData/SysMenuSeedData.cs new file mode 100644 index 00000000..ade0beba --- /dev/null +++ b/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/SeedData/SysMenuSeedData.cs @@ -0,0 +1,39 @@ +// 麻省理工学院许可证 +// +// 版权所有 (c) 2021-2023 zuohuaijun,大名科技(天津)有限公司 联系电话/微信:18020030720 QQ:515096995 +// +// 特此免费授予获得本软件的任何人以处理本软件的权利,但须遵守以下条件:在所有副本或重要部分的软件中必须包括上述版权声明和本许可声明。 +// +// 软件按“原样”提供,不提供任何形式的明示或暗示的保证,包括但不限于对适销性、适用性和非侵权的保证。 +// 在任何情况下,作者或版权持有人均不对任何索赔、损害或其他责任负责,无论是因合同、侵权或其他方式引起的,与软件或其使用或其他交易有关。 + +namespace Admin.NET.Plugin.PluginCoreManager; + +/// +/// 系统菜单表种子数据 +/// +[IncreSeed] +public class SysMenu_PluginCore_SeedData : ISqlSugarEntitySeedData +{ + /// + /// 种子数据 + /// + /// + public IEnumerable HasData() + { + return new[] + { + + // 建议此处Id范围之间放置具体业务应用菜单 + new SysMenu{ Id=1310000000802, Pid=1310000000301, Title="应用插件", Path="/platform/plugincore", Name="plugincore", Component="/system/plugincore/index", Icon="ele-TurnOff", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=210 }, + new SysMenu{ Id=1310000000803, Pid=1310000000802, Title="启用", Permission="sysPluginCore/enable", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=212 }, + new SysMenu{ Id=1310000000804, Pid=1310000000802, Title="卸载", Permission="sysPluginCore/delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=213 }, + new SysMenu{ Id=1310000000805, Pid=1310000000802, Title="禁用", Permission="sysPluginCore/disable", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=214 }, + new SysMenu{ Id=1310000000806, Pid=1310000000802, Title="详细", Permission="sysPluginCore/details", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=215 }, + new SysMenu{ Id=1310000000807, Pid=1310000000802, Title="文档", Permission="sysPluginCore/readme", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=216 }, + new SysMenu{ Id=1310000000808, Pid=1310000000802, Title="设置", Permission="sysPluginCore/setting", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=217 }, + + + }; + } +} \ No newline at end of file diff --git a/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/SeedData/SysRoleMenuSeedData.cs b/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/SeedData/SysRoleMenuSeedData.cs new file mode 100644 index 00000000..6a7b8556 --- /dev/null +++ b/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/SeedData/SysRoleMenuSeedData.cs @@ -0,0 +1,39 @@ +// 麻省理工学院许可证 +// +// 版权所有 (c) 2021-2023 zuohuaijun,大名科技(天津)有限公司 联系电话/微信:18020030720 QQ:515096995 +// +// 特此免费授予获得本软件的任何人以处理本软件的权利,但须遵守以下条件:在所有副本或重要部分的软件中必须包括上述版权声明和本许可声明。 +// +// 软件按“原样”提供,不提供任何形式的明示或暗示的保证,包括但不限于对适销性、适用性和非侵权的保证。 +// 在任何情况下,作者或版权持有人均不对任何索赔、损害或其他责任负责,无论是因合同、侵权或其他方式引起的,与软件或其使用或其他交易有关。 + +namespace Admin.NET.Core; + +/// +/// 系统角色菜单表种子数据 +/// +[IncreSeed] +public class SysRoleMenu_PluginCore_SeedData : ISqlSugarEntitySeedData +{ + /// + /// 种子数据 + /// + /// + public IEnumerable HasData() + { + return new[] + { + + // 应用插件 + + new SysRoleMenu{ Id=1300000000831, RoleId=1300000000101, MenuId=1310000000802 }, + new SysRoleMenu{ Id=1300000000832, RoleId=1300000000101, MenuId=1310000000803 }, + new SysRoleMenu{ Id=1300000000833, RoleId=1300000000101, MenuId=1310000000803 }, + new SysRoleMenu{ Id=1300000000834, RoleId=1300000000101, MenuId=1310000000804 }, + new SysRoleMenu{ Id=1300000000835, RoleId=1300000000101, MenuId=1310000000805 }, + new SysRoleMenu{ Id=1300000000836, RoleId=1300000000101, MenuId=1310000000806 }, + new SysRoleMenu{ Id=1300000000837, RoleId=1300000000101, MenuId=1310000000807 }, + new SysRoleMenu{ Id=1300000000830, RoleId=1300000000101, MenuId=1310000000808 }, + }; + } +} \ No newline at end of file diff --git a/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Service/Plugin/Dto/PluginInput.cs b/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Service/Plugin/Dto/PluginInput.cs new file mode 100644 index 00000000..6530a282 --- /dev/null +++ b/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Service/Plugin/Dto/PluginInput.cs @@ -0,0 +1,43 @@ +// 麻省理工学院许可证 +// +// 版权所有 (c) 2021-2023 zuohuaijun,大名科技(天津)有限公司 联系电话/微信:18020030720 QQ:515096995 +// +// 特此免费授予获得本软件的任何人以处理本软件的权利,但须遵守以下条件:在所有副本或重要部分的软件中必须包括上述版权声明和本许可声明。 +// +// 软件按“原样”提供,不提供任何形式的明示或暗示的保证,包括但不限于对适销性、适用性和非侵权的保证。 +// 在任何情况下,作者或版权持有人均不对任何索赔、损害或其他责任负责,无论是因合同、侵权或其他方式引起的,与软件或其使用或其他交易有关。 + +namespace Admin.NET.Plugin.PluginCoreManager.Service.Plugin.Dto; + +public class PagePluginCoreInput : BasePageInput +{ + /// + /// 名称 + /// + public string Name { get; set; } + + /// + /// 编码 + /// + public string Code { get; set; } +} + +public class AddPluginCoreInput : SysPluginCore +{ + +} + +public class UpdatePluginCoreInput : AddPluginCoreInput +{ +} + +public class DeletePluginCoreInput : BaseIdInput +{ +} +public class EnablePluginCoreInput : BaseIdInput +{ +} +public class UpdatePluginCoreSettingInput : BaseIdInput +{ + public string Data { get; set; } +} \ No newline at end of file diff --git a/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Service/Plugin/SysPluginCoreService.cs b/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Service/Plugin/SysPluginCoreService.cs new file mode 100644 index 00000000..cb94f581 --- /dev/null +++ b/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Service/Plugin/SysPluginCoreService.cs @@ -0,0 +1,729 @@ +// 麻省理工学院许可证 +// +// 版权所有 (c) 2021-2023 zuohuaijun,大名科技(天津)有限公司 联系电话/微信:18020030720 QQ:515096995 +// +// 特此免费授予获得本软件的任何人以处理本软件的权利,但须遵守以下条件:在所有副本或重要部分的软件中必须包括上述版权声明和本许可声明。 +// +// 软件按“原样”提供,不提供任何形式的明示或暗示的保证,包括但不限于对适销性、适用性和非侵权的保证。 +// 在任何情况下,作者或版权持有人均不对任何索赔、损害或其他责任负责,无论是因合同、侵权或其他方式引起的,与软件或其使用或其他交易有关。 + +using Admin.NET.Core; +using Admin.NET.Plugin.PluginCoreManager.Service.Plugin.Dto; +using Furion.VirtualFileServer; +using Minio; +using OnceMi.AspNetCore.OSS; +using Org.BouncyCastle.Asn1.Ocsp; +using PluginCore; +using PluginCore.AspNetCore.Interfaces; +using PluginCore.AspNetCore.ResponseModel; +using PluginCore.Infrastructure; +using PluginCore.Interfaces; +using PluginCore.IPlugins; +using PluginCore.Models; + +namespace Admin.NET.Plugin.PluginCoreManager.Service.Plugin; + +/// +/// 系统动态插件服务 +/// +[ApiDescriptionSettings(Order = 245)] +public class SysPluginCoreService : IDynamicApiController, ITransient +{ + #region Fields + private readonly IPluginManager _pluginManager; + private readonly IPluginFinder _pluginFinder; + private readonly IPluginApplicationBuilderManager _pluginApplicationBuilderManager; + #endregion + private readonly IDynamicApiRuntimeChangeProvider _provider; + private readonly SqlSugarRepository _sysPluginRep; + + public SysPluginCoreService(IPluginManager pluginManager, IPluginFinder pluginFinder, IPluginApplicationBuilderManager pluginApplicationBuilderManager, IDynamicApiRuntimeChangeProvider provider, + SqlSugarRepository sysPluginRep) + { + _pluginManager = pluginManager; + _pluginFinder = pluginFinder; + _pluginApplicationBuilderManager = pluginApplicationBuilderManager; + _provider = provider; + _sysPluginRep = sysPluginRep; + } + + /// + /// 获取动态插件列表 + /// + /// + /// + [DisplayName("获取动态插件列表")] + public async Task> Page(PagePluginInput input) + { + return await _sysPluginRep.AsQueryable() + .WhereIF(!string.IsNullOrWhiteSpace(input.Name), u => u.DisplayName.Contains(input.Name)) + .OrderBy(u => u.OrderNo) + .ToPagedListAsync(input.Page, input.PageSize); + } + + + /// + /// 查看详细 + /// + /// + /// + [ApiDescriptionSettings(Name = "Details"), HttpGet] + [DisplayName("查看详细")] + public async Task Details(long id) + { + var pluginCore = await _sysPluginRep.GetByIdAsync(id); + if (pluginCore == null) throw Oops.Oh("查询插件ID失败"); ; + + // 先移除再添加动态程序集/接口 + + try + { + #region 效验 + var pluginId = pluginCore.PluginId; + var pluginConfigModel = PluginConfigModelFactory.Create(); + string[] localPluginIds = PluginPathProvider.AllPluginFolderName().ToArray(); + + if (!localPluginIds.Contains(pluginId)) + { + throw Oops.Oh($"查看详细失败: 不存在 {pluginId} 插件"); + + } + + #endregion + + PluginInfoModel pluginInfoModel = PluginInfoModelFactory.Create(pluginId); + string[] enablePluginIds = _pluginFinder.EnablePluginIds().ToArray(); + PluginInfoResponseModel pluginInfoResponseModel = PluginInfoModelToResponseModel(new List() { pluginInfoModel }, pluginConfigModel, enablePluginIds).FirstOrDefault(); + + + return pluginInfoResponseModel; + } + catch (Exception ex) + { + throw Oops.Oh("查看详细失败: " + ex.Message); + + } + + } + + #region 查看文档 + /// + /// 查看文档 + /// + /// + /// + [ApiDescriptionSettings(Name = "Readme"), HttpGet] + [DisplayName("查看文档")] + public async Task Readme(long id) + { + var pluginCore = await _sysPluginRep.GetByIdAsync(id); + if (pluginCore == null) throw Oops.Oh("查询插件ID失败"); ; + + + try + { + #region 效验 + var pluginId = pluginCore.PluginId; + var pluginConfigModel = PluginConfigModelFactory.Create(); + string[] localPluginIds = PluginPathProvider.AllPluginFolderName().ToArray(); + + if (!localPluginIds.Contains(pluginId)) + { + throw Oops.Oh($"查看详细失败: 不存在 {pluginId} 插件"); + + } + + #endregion + + PluginReadmeModel readmeModel = PluginReadmeModelFactory.Create(pluginId); + PluginReadmeResponseModel readmeResponseModel = new PluginReadmeResponseModel(); + readmeResponseModel.Content = readmeModel?.Content ?? ""; + readmeResponseModel.PluginId = pluginId; + + + return readmeResponseModel; + + } + catch (Exception ex) + { + throw Oops.Oh("查看详细失败: " + ex.Message); + + } + + + } + #endregion + + /// + /// 卸载动态插件 + /// + /// + /// + [ApiDescriptionSettings(Name = "Uninstall"), HttpPost] + [DisplayName("卸载动态插件")] + public async Task Uninstall(DeletePluginCoreInput input) + { + var pluginCore = await _sysPluginRep.GetByIdAsync(input.Id); + if (pluginCore == null) throw Oops.Oh("查询插件ID失败"); + + + + // 卸载插件 必须 先禁用插件 + #region 效验 + var pluginId = pluginCore.PluginId; + var pluginConfigModel = PluginConfigModelFactory.Create(); + if (pluginConfigModel.EnabledPlugins.Contains(pluginId)) + { + + throw Oops.Oh("卸载失败: 请先禁用此插件"); + } + string pluginDirStr = Path.Combine(PluginPathProvider.PluginsRootPath(), pluginId); + string pluginWwwrootDirStr = Path.Combine(PluginPathProvider.PluginsWwwRootDir(), pluginId); + if (!Directory.Exists(pluginDirStr) && !Directory.Exists(pluginWwwrootDirStr)) + { + + throw Oops.Oh("卸载失败: 此插件不存在"); + } + #endregion + + try + { + // PS:卸载插件必须先禁用插件,所以此时插件LoadContext已被移除释放(插件Assemblies已被释放), 此处不需移除LoadContext + + // 1.删除物理文件 + var pluginDir = new DirectoryInfo(pluginDirStr); + if (pluginDir.Exists) + { + pluginDir.Delete(true); + } + // 虽然 已禁用 时 pluginWwwrootDirStr/pluginId 已删除, 但为确保, 还是再删除一次 + var pluginWwwrootDir = new DirectoryInfo(pluginWwwrootDirStr); + if (pluginWwwrootDir.Exists) + { + pluginWwwrootDir.Delete(true); + } + + await _sysPluginRep.DeleteAsync(u => u.Id == input.Id); + } + catch (Exception ex) + { + + throw Oops.Oh("卸载失败: " + ex.Message); + } + + + + } + + #region 设置 + /// + /// 插件设置设置 + /// + /// + /// + [ApiDescriptionSettings(Name = "Settings"), HttpGet] + [DisplayName("插件设置设置")] + public async Task Settings(long id) + { + var pluginCore = await _sysPluginRep.GetByIdAsync(id); + if (pluginCore == null) throw Oops.Oh("查询插件ID失败"); ; + + try + { + #region 效验 + var pluginId = pluginCore.PluginId; + var pluginConfigModel = PluginConfigModelFactory.Create(); + string[] localPluginIds = PluginPathProvider.AllPluginFolderName().ToArray(); + + if (!localPluginIds.Contains(pluginId)) + { + throw Oops.Oh($"查看详细失败: 不存在 {pluginId} 插件"); + + } + + #endregion + + string settingsJsonStr = PluginSettingsModelFactory.Create(pluginId); + + + + return settingsJsonStr ?? "无设置项"; + } + catch (Exception ex) + { + throw Oops.Oh("查看设置失败: " + ex.Message); + + } + + } + /// + /// 插件设置设置 + /// + /// + /// + [ApiDescriptionSettings(Name = "Settings"), HttpPost] + [DisplayName("插件设置设置")] + public async Task Settings(UpdatePluginCoreSettingInput input) + { + var pluginCore = await _sysPluginRep.GetByIdAsync(input.Id); + if (pluginCore == null) throw Oops.Oh("查询插件ID失败"); ; + + try + { + #region 效验 + var pluginId = pluginCore.PluginId; + var pluginConfigModel = PluginConfigModelFactory.Create(); + string[] localPluginIds = PluginPathProvider.AllPluginFolderName().ToArray(); + + if (!localPluginIds.Contains(pluginId)) + { + throw Oops.Oh($"查看详细失败: 不存在 {pluginId} 插件"); + + } + + #endregion + + input.Data = input.Data ?? ""; + PluginSettingsModelFactory.Save(pluginSettingsJsonStr: input.Data, pluginId: pluginCore.PluginId); + + + + } + catch (Exception ex) + { + throw Oops.Oh("设置失败: " + ex.Message); + + + } + + + } + #endregion + + + /// + /// 启用插件 + /// + /// + /// + [ApiDescriptionSettings(Name = "Enable"), HttpPost] + [DisplayName("启用插件")] + public async Task Enable(EnablePluginCoreInput input) + { + var pluginCore = await _sysPluginRep.GetByIdAsync(input.Id); + if (pluginCore == null) return; + + // 移除动态程序集/接口 + var pluginConfigModel = PluginConfigModelFactory.Create(); + // 效验是否存在于 已禁用插件列表 + #region 效验 + var pluginId = pluginCore.PluginId; + var pluginDir = new DirectoryInfo(Path.Combine(PluginPathProvider.PluginsRootPath(), pluginId)); + if (pluginDir != null && !pluginDir.Exists) + { + throw Oops.Oh("启用失败: 此插件不存在"); + + } + string[] enablePluginIds = _pluginFinder.EnablePluginIds().ToArray(); + if (enablePluginIds.Contains(pluginId)) + { + throw Oops.Oh("启用失败: 此插件已启用"); + + } + #endregion + + try + { + // 1. 创建插件程序集加载上下文, 添加到 PluginsLoadContexts + _pluginManager.LoadPlugin(pluginId); + // 2. 添加到 pluginConfigModel.EnabledPlugins + pluginConfigModel.EnabledPlugins.Add(pluginId); + // 4.保存到 plugin.config.json + PluginConfigModelFactory.Save(pluginConfigModel); + + // 5. 找到此插件实例 + IPlugin plugin = _pluginFinder.Plugin(pluginId); + if (plugin == null) + { + // 7.启用不成功, 回滚插件状态: (1)释放插件上下文 (2)更新 plugin.config.json + try + { + _pluginManager.UnloadPlugin(pluginId); + } + catch (Exception ex) + { } + + // 从 pluginConfigModel.EnabledPlugins 移除 + pluginConfigModel.EnabledPlugins.Remove(pluginId); + // 保存到 plugin.config.json + PluginConfigModelFactory.Save(pluginConfigModel); + + throw Oops.Oh("启用失败: 此插件不存在"); + + + } + // 6.调取插件的 AfterEnable(), 插件开发者可在此回收资源 + var pluginEnableResult = plugin.AfterEnable(); + if (!pluginEnableResult.IsSuccess) + { + // 7.启用不成功, 回滚插件状态: (1)释放插件上下文 (2)更新 plugin.config.json + try + { + _pluginManager.UnloadPlugin(pluginId); + } + catch (Exception ex) + { } + + // 从 pluginConfigModel.EnabledPlugins 移除 + pluginConfigModel.EnabledPlugins.Remove(pluginId); + // 保存到 plugin.config.json + PluginConfigModelFactory.Save(pluginConfigModel); + throw Oops.Oh("启用失败: 来自插件的错误信息: " + pluginEnableResult.Message); + + + } + + // 7. ReBuild + this._pluginApplicationBuilderManager.ReBuild(); + + // 8. 尝试复制 插件下的 wwwroot 到 Plugins_wwwroot + string wwwRootDir = PluginPathProvider.WwwRootDir(pluginId); + if (Directory.Exists(wwwRootDir)) + { + string targetDir = PluginPathProvider.PluginWwwRootDir(pluginId); + PluginCore.Utils.FileUtil.CopyFolder(wwwRootDir, targetDir); + } + + //9.载入Furion动态插件 + var pluginMainAssembly = _pluginManager.GetPluginAssembly(pluginId); + // 将程序集添加进动态 WebAPI 应用部件 + _provider.AddAssembliesWithNotifyChanges(pluginMainAssembly); + + await _sysPluginRep.UpdateAsync(u => new SysPluginCore() { Status = StatusEnum.Enable }, u => u.Id == input.Id); + } + catch (Exception ex) + { + throw Oops.Oh("启用失败: " + ex.Message); + + } + + + } + /// + /// 禁用插件 + /// + /// + /// + [ApiDescriptionSettings(Name = "Disable"), HttpPost] + [DisplayName("禁用插件")] + public async Task Disable(EnablePluginCoreInput input) + { + var pluginCore = await _sysPluginRep.GetByIdAsync(input.Id); + if (pluginCore == null) return; + + // 移除动态程序集/ + #region 效验 + var pluginId = pluginCore.PluginId; + var pluginConfigModel = PluginConfigModelFactory.Create(); + // string[] enablePluginIds = _pluginFinder.EnablePluginIds().ToArray(); + // // 效验是否存在于 已启用插件列表 + // if (!enablePluginIds.Contains(pluginId)) + // { + // responseData.Code = -1; + // responseData.Message = "禁用失败: 此插件不存在, 或未启用"; + // return await Task.FromResult(responseData); + // } + #endregion + + try + { + // 1. 找到此插件实例 + IPlugin plugin = _pluginFinder.Plugin(pluginId); + if (plugin == null) + { + + throw Oops.Oh("禁用失败: 此插件不存在, 或未启用"); + } + try + { + // 2.调取插件的 BeforeDisable(), 插件开发者可在此回收资源 + var pluginDisableResult = plugin.BeforeDisable(); + if (!pluginDisableResult.IsSuccess) + { + throw Oops.Oh("禁用失败: 来自插件的错误信息: " + pluginDisableResult.Message); + + } + // 3.移除插件对应的程序集加载上下文 + _pluginManager.UnloadPlugin(pluginId); + // 3.1. ReBuild + this._pluginApplicationBuilderManager.ReBuild(); + if (pluginConfigModel.EnabledPlugins.Contains(pluginId)) + { + // 4.从 pluginConfigModel.EnabledPlugins 移除 + pluginConfigModel.EnabledPlugins.Remove(pluginId); + // 5.保存到 plugin.config.json + PluginConfigModelFactory.Save(pluginConfigModel); + } + } + catch (Exception ex) + { + PluginCore.Utils.LogUtil.Error(ex.ToString()); + throw Oops.Oh("禁用失败: 此插件不存在, 或未启用"); + + } + + // 7. 尝试移除 Plugins_wwwroot/PluginId + string pluginWwwRootDir = PluginPathProvider.PluginWwwRootDir(pluginId); + if (Directory.Exists(pluginWwwRootDir)) + { + Directory.Delete(pluginWwwRootDir, true); + } + //8.移除Furion动态插件 + + //9.载入Furion动态插件 + var pluginMainAssembly = _pluginManager.GetPluginAssembly(pluginId); + // 将程序集添加进动态 WebAPI 应用部件 + _provider.RemoveAssembliesWithNotifyChanges(pluginMainAssembly); + + + await _sysPluginRep.UpdateAsync(u => new SysPluginCore() { Status = StatusEnum.Disable }, u => u.Id == input.Id); + } + catch (Exception ex) + { + throw Oops.Oh("禁用失败: " + ex.Message); + + } + + + } + /// + /// 上传文件 + /// + /// + /// + /// + [DisplayName("上传文件")] + public async Task UploadFile([Required] IFormFile file, [FromQuery] string? path) + { + var sysFile = await HandleUploadFile(file, path); + //return new FileOutput + //{ + + //}; + } + + /// + /// 上传文件 + /// + /// 文件 + /// 路径 + /// + [NonAction] + private async Task HandleUploadFile(IFormFile file, string savePath) + { + if (file == null) throw Oops.Oh(ErrorCodeEnum.D8000); + + var path = savePath; + + BaseResponseModel responseData = new BaseResponseModel(); + + #region 效验 + if (file == null) + { + throw Oops.Oh("上传的文件不能为空"); + //responseData.Code = -1; + //responseData.Message = "上传的文件不能为空"; + //return responseData; + } + //文件后缀 + string fileExtension = Path.GetExtension(file.FileName);//获取文件格式,拓展名 + // 类型标记 + UploadFileType uploadFileType = UploadFileType.NoAllowedType; + switch (fileExtension) + { + case ".zip": + uploadFileType = UploadFileType.Zip; + break; + case ".nupkg": + uploadFileType = UploadFileType.Nupkg; + break; + } + + if (fileExtension != ".zip" && fileExtension != ".nupkg") + { + throw Oops.Oh("只能上传 zip 或 nupkg 格式文件"); + //responseData.Code = -1; + //// nupkg 其实就是 zip + //responseData.Message = "只能上传 zip 或 nupkg 格式文件"; + //return responseData; + } + // PluginCore.AspNetCore-v1.0.2 起 不再限制插件上传大小 + //判断文件大小 + //var fileSize = file.Length; + //if (fileSize > 1024 * 1024 * 5) // 5M + //{ + // responseData.Code = -1; + // responseData.Message = "上传的文件不能大于5MB"; + // return responseData; + //} + #endregion + + try + { + // 1.先上传到 临时插件上传目录, 用Guid.zip作为保存文件名 + string tempZipFilePath = Path.Combine(PluginPathProvider.TempPluginUploadDir(), Guid.NewGuid() + ".zip"); + using (var fs = System.IO.File.Create(tempZipFilePath)) + { + file.CopyTo(fs); //将上传的文件文件流,复制到fs中 + fs.Flush();//清空文件流 + } + // 2.解压 + bool isDecomparessSuccess = false; + if (uploadFileType == UploadFileType.Zip) + { + isDecomparessSuccess = PluginCore.Utils.ZipHelper.DecomparessFile(tempZipFilePath, tempZipFilePath.Replace(".zip", "")); + } + else if (uploadFileType == UploadFileType.Nupkg) + { + isDecomparessSuccess = NupkgService.DecomparessFile(tempZipFilePath, tempZipFilePath.Replace(".zip", "")); + } + + // 3.删除原压缩包 + System.IO.File.Delete(tempZipFilePath); + if (!isDecomparessSuccess) + { + throw Oops.Oh("解压插件压缩包失败"); + + //responseData.Code = -1; + //responseData.Message = "解压插件压缩包失败"; + //return responseData; + } + // 4.读取其中的info.json, 获取 PluginId 值 + PluginInfoModel pluginInfoModel = PluginInfoModelFactory.ReadPluginDir(tempZipFilePath.Replace(".zip", "")); + if (pluginInfoModel == null || string.IsNullOrEmpty(pluginInfoModel.PluginId)) + { + // 记得删除已不再需要的临时插件文件夹 + Directory.Delete(tempZipFilePath.Replace(".zip", ""), true); + throw Oops.Oh("不合法的插件"); + + //responseData.Code = -1; + //responseData.Message = "不合法的插件"; + //return responseData; + } + string pluginId = pluginInfoModel.PluginId; + // 5.检索 此 PluginId 是否本地插件已存在 + var pluginConfigModel = PluginConfigModelFactory.Create(); + // 本地已经存在的 PluginId + IList localExistPluginIds = PluginPathProvider.AllPluginFolderName(); + if (localExistPluginIds.Contains(pluginId)) + { + // 记得删除已不再需要的临时插件文件夹 + Directory.Delete(tempZipFilePath.Replace(".zip", ""), true); + throw Oops.Oh($"本地已有此插件 (PluginId: {pluginId}), 请前往插件列表删除后, 再上传"); + + //responseData.Code = -1; + //responseData.Message = $"本地已有此插件 (PluginId: {pluginId}), 请前往插件列表删除后, 再上传"; + //return responseData; + } + // 6.本地无此插件 -> 移动插件文件夹到 Plugins 下, 并以 PluginId 为插件文件夹名 + string pluginsRootPath = PluginPathProvider.PluginsRootPath(); + string newPluginDir = Path.Combine(pluginsRootPath, pluginId); + Directory.Move(tempZipFilePath.Replace(".zip", ""), newPluginDir); + + // 7. 放入 Plugins 中, 默认为 已禁用 + + var pluginCore = new SysPluginCore(); + pluginCore.PluginId = pluginId; + pluginCore.DisplayName = pluginInfoModel.DisplayName; + pluginCore.Description = pluginInfoModel.Description; + pluginCore.Author = pluginInfoModel.Author; + pluginCore.Version = pluginInfoModel.Version; + pluginCore.Status = StatusEnum.Disable; + await _sysPluginRep.InsertAsync(pluginCore.Adapt()); + + + responseData.Code = 1; + responseData.Message = $"上传插件成功 (PluginId: {pluginId})"; + } + catch (Exception ex) + { + throw Oops.Oh("上传插件失败: " + ex.Message); + + //responseData.Code = -1; + //responseData.Message = "上传插件失败: " + ex.Message; + //ex = ex.InnerException; + //while (ex != null) + //{ + // responseData.Message += " - " + ex.InnerException.Message; + // ex = ex.InnerException; + //} + } + + + + return responseData; + } + + + #region Helpers + + [NonAction] + private IList PluginInfoModelToResponseModel(IList pluginInfoModels, PluginConfigModel pluginConfigModel, string[] enablePluginIds) + { + // 获取 Plugins 下所有插件 + // DirectoryInfo pluginsDir = new DirectoryInfo(PluginPathProvider.PluginsRootPath()); + // List pluginIds = pluginsDir?.GetDirectories()?.Select(m => m.Name)?.ToList() ?? new List(); + + IList responseModels = new List(); + #region 添加插件状态信息 + foreach (var model in pluginInfoModels) + { + PluginInfoResponseModel responseModel = new PluginInfoResponseModel(); + responseModel.Author = model.Author; + responseModel.Description = model.Description; + responseModel.DisplayName = model.DisplayName; + responseModel.PluginId = model.PluginId; + responseModel.SupportedVersions = model.SupportedVersions; + responseModel.Version = model.Version; + responseModel.DependPlugins = model.DependPlugins; + + if (pluginConfigModel.EnabledPlugins.Contains(model.PluginId) && !enablePluginIds.Contains(model.PluginId)) + { + // 错误情况: 配置 标识 已启用, 但实际没有启用成功 + pluginConfigModel.EnabledPlugins.Remove(model.PluginId); + PluginConfigModelFactory.Save(pluginConfigModel); + + responseModel.Status = PluginStatus.Disabled; + } + else if (!pluginConfigModel.EnabledPlugins.Contains(model.PluginId) && enablePluginIds.Contains(model.PluginId)) + { + // 错误情况: 配置没有标识 已启用, 但实际 已启用 + pluginConfigModel.EnabledPlugins.Add(model.PluginId); + PluginConfigModelFactory.Save(pluginConfigModel); + + responseModel.Status = PluginStatus.Enabled; + } + else if (pluginConfigModel.EnabledPlugins.Contains(model.PluginId) && enablePluginIds.Contains(model.PluginId)) + { + responseModel.Status = PluginStatus.Enabled; + } + else + { + responseModel.Status = PluginStatus.Disabled; + } + responseModels.Add(responseModel); + } + #endregion + + return responseModels; + } + + public enum UploadFileType + { + NoAllowedType = 0, + Zip = 1, + Nupkg = 2 + } + + #endregion +} \ No newline at end of file diff --git a/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Startup.cs b/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Startup.cs new file mode 100644 index 00000000..f62088d5 --- /dev/null +++ b/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Startup.cs @@ -0,0 +1,34 @@ +// 麻省理工学院许可证 +// +// 版权所有 (c) 2021-2023 zuohuaijun,大名科技(天津)有限公司 联系电话/微信:18020030720 QQ:515096995 +// +// 特此免费授予获得本软件的任何人以处理本软件的权利,但须遵守以下条件:在所有副本或重要部分的软件中必须包括上述版权声明和本许可声明。 +// +// 软件按“原样”提供,不提供任何形式的明示或暗示的保证,包括但不限于对适销性、适用性和非侵权的保证。 +// 在任何情况下,作者或版权持有人均不对任何索赔、损害或其他责任负责,无论是因合同、侵权或其他方式引起的,与软件或其使用或其他交易有关。 + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using PluginCore.AspNetCore.Extensions; +using System; + +namespace Admin.NET.Plugin.PluginCoreManager; + +[AppStartup(100)] +public class Startup : AppStartup +{ + public void ConfigureServices(IServiceCollection services) + { + RunOptions.Default + .AddComponent() + .UseComponent(); + + + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + + } +} \ No newline at end of file diff --git a/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/StartupServiceComponent.cs b/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/StartupServiceComponent.cs new file mode 100644 index 00000000..a0ada15a --- /dev/null +++ b/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/StartupServiceComponent.cs @@ -0,0 +1,56 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using PluginCore; +using PluginCore.AspNetCore.Extensions; +using PluginCore.Interfaces; +using PluginCore.Models; +using static SKIT.FlurlHttpClient.Wechat.Api.Models.WxaPluginListResponse.Types; + +namespace Admin.NET.Plugin.PluginCoreManager; + +// 模拟 ConfigureService +public sealed class StartupServiceComponent : IServiceComponent +{ + public void Load(IServiceCollection services, ComponentContext componentContext) + { + // 1. 添加 PluginCore + services.AddPluginCore(); + IPluginManager pluginManager = App.GetService(); + IDynamicApiRuntimeChangeProvider provider = App.GetService(); + #region 获取 PluginConfigModel + PluginConfigModel pluginConfigModel = PluginConfigModelFactory.Create(); + #endregion + + // 已启用的插件 + #region 加载 已启用插件的Assemblies + IList enabledPluginIds = pluginConfigModel.EnabledPlugins; + foreach (var pluginId in enabledPluginIds) + { + //9.载入Furion动态插件 + var pluginMainAssembly = pluginManager.GetPluginAssembly(pluginId); + // 将程序集添加进动态 WebAPI 应用部件 + provider.AddAssembliesWithNotifyChanges(pluginMainAssembly); + } + #endregion + + + + } +} + +// 模拟 Configure +public sealed class StartupApplicationComponent : IApplicationComponent +{ + public void Load(IApplicationBuilder app, IWebHostEnvironment env, ComponentContext componentContext) + { + // 2. 使用 PluginCore + app.UsePluginCore(); + } +} diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/AdminUI/PluginCoreAdminUIBuilderExtensions.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/AdminUI/PluginCoreAdminUIBuilderExtensions.cs new file mode 100644 index 00000000..af9e0f8e --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/AdminUI/PluginCoreAdminUIBuilderExtensions.cs @@ -0,0 +1,95 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Options; + +namespace PluginCore.AspNetCore.AdminUI +{ + public static class PluginCoreAdminUIBuilderExtensions + { + + /// + /// Register the SwaggerUI middleware with provided options + /// + public static IApplicationBuilder UsePluginCoreAdminUI(this IApplicationBuilder app, PluginCoreAdminUIOptions options) + { + #region Old - 区分 + //Config.PluginCoreConfig pluginCoreConfig = Config.PluginCoreConfigFactory.Create(); + + //switch (pluginCoreConfig.FrontendMode?.ToLower()) + //{ + // case "localembedded": + // app.UseMiddleware(options); + // break; + // case "localfolder": + + // #region LocalFolder + // //string contentRootPath = Directory.GetCurrentDirectory(); + + // // https://docs.microsoft.com/zh-CN/aspnet/core/fundamentals/static-files?view=aspnetcore-5.0 + // //var options = new DefaultFilesOptions() + // //{ + // // RequestPath = "/PluginCore/Admin", + // //}; + // //// TODO: 404: 无效, 失败, 改为使用 Controller 手动指定 + // ////options.DefaultFileNames.Add("PluginCoreAdmin/index.html"); + // //app.UseDefaultFiles(options); + + // // 注意: 为了无需重启Web,而更新是否本地前端配置, 因此此项保持常驻开启 + // // 因此, 需要保证 PluginCoreAdmin 文件夹存在 + // string pluginCoreAdminDir = PluginPathProvider.PluginCoreAdminDir(); + // app.UseStaticFiles(new StaticFileOptions + // { + // FileProvider = new PhysicalFileProvider( + // pluginCoreAdminDir), + // RequestPath = "/PluginCore/Admin" + // }); + // #endregion + + // break; + // case "remotecdn": + + // break; + // default: + // app.UseMiddleware(options); + // break; + //} + + //return app; + #endregion + + + return app.UseMiddleware(options); + } + + /// + /// Register the SwaggerUI middleware with optional setup action for DI-injected options + /// + public static IApplicationBuilder UsePluginCoreAdminUI( + this IApplicationBuilder app) + { + PluginCoreAdminUIOptions options = new PluginCoreAdminUIOptions() + { + + }; + + return app.UsePluginCoreAdminUI(options); + } + + + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/AdminUI/PluginCoreAdminUIMiddleware.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/AdminUI/PluginCoreAdminUIMiddleware.cs new file mode 100644 index 00000000..b0890d39 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/AdminUI/PluginCoreAdminUIMiddleware.cs @@ -0,0 +1,150 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.StaticFiles; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace PluginCore.AspNetCore.AdminUI +{ + public class PluginCoreAdminUIMiddleware + { + private const string EmbeddedFileNamespace = "PluginCore.AspNetCore.node_modules.plugincore_admin_frontend.dist"; + + private readonly PluginCoreAdminUIOptions _options; + private readonly StaticFileMiddleware _staticFileMiddleware; + + + public PluginCoreAdminUIMiddleware( + RequestDelegate next, + IWebHostEnvironment hostingEnv, + ILoggerFactory loggerFactory, + PluginCoreAdminUIOptions options) + { + _options = options ?? new PluginCoreAdminUIOptions(); + + _staticFileMiddleware = CreateStaticFileMiddleware(next, hostingEnv, loggerFactory, options); + } + + public async Task Invoke(HttpContext httpContext) + { + var httpMethod = httpContext.Request.Method; + var path = httpContext.Request.Path.Value; + + // If the RoutePrefix is requested (with or without trailing slash), redirect to index URL + if (httpMethod == "GET" && Regex.IsMatch(path, $"^/?{Regex.Escape(_options.RoutePrefix)}/?$", RegexOptions.IgnoreCase)) + { + // Use relative redirect to support proxy environments + var relativeIndexUrl = string.IsNullOrEmpty(path) || path.EndsWith("/") + ? "index.html" + : $"{path.Split('/').Last()}/index.html"; + + RespondWithRedirect(httpContext.Response, relativeIndexUrl); + return; + } + + if (httpMethod == "GET" && Regex.IsMatch(path, $"^/{Regex.Escape(_options.RoutePrefix)}/?index.html$", RegexOptions.IgnoreCase)) + { + await RespondWithIndexHtml(httpContext.Response); + return; + } + + await _staticFileMiddleware.Invoke(httpContext); + } + + private StaticFileMiddleware CreateStaticFileMiddleware( + RequestDelegate next, + IWebHostEnvironment hostingEnv, + ILoggerFactory loggerFactory, + PluginCoreAdminUIOptions options) + { + Config.PluginCoreConfig pluginCoreConfig = Config.PluginCoreConfigFactory.Create(); + IFileProvider fileProvider = null; + switch (pluginCoreConfig.FrontendMode?.ToLower()) + { + case "localembedded": + fileProvider = new EmbeddedFileProvider(typeof(PluginCoreAdminUIMiddleware).GetTypeInfo().Assembly, + EmbeddedFileNamespace); + break; + case "localfolder": + string absoluteRootPath = PluginPathProvider.PluginCoreAdminDir(); + fileProvider = new PhysicalFileProvider(absoluteRootPath); + break; + case "remotecdn": + fileProvider = new PluginCoreAdminUIRemoteFileProvider(pluginCoreConfig.RemoteFrontend); + break; + default: + fileProvider = new EmbeddedFileProvider(typeof(PluginCoreAdminUIMiddleware).GetTypeInfo().Assembly, + EmbeddedFileNamespace); + break; + } + + var staticFileOptions = new StaticFileOptions + { + RequestPath = string.IsNullOrEmpty(options.RoutePrefix) ? string.Empty : $"/{options.RoutePrefix}", + FileProvider = fileProvider, + }; + + return new StaticFileMiddleware(next, hostingEnv, Options.Create(staticFileOptions), loggerFactory); + } + + private void RespondWithRedirect(HttpResponse response, string location) + { + response.StatusCode = 301; + response.Headers["Location"] = location; + } + + private async Task RespondWithIndexHtml(HttpResponse response) + { + response.StatusCode = 200; + response.ContentType = "text/html;charset=utf-8"; + + using (var stream = _options.IndexStream()) + { + // Inject arguments before writing to response + var htmlBuilder = new StringBuilder(new StreamReader(stream).ReadToEnd()); + foreach (var entry in GetIndexArguments()) + { + htmlBuilder.Replace(entry.Key, entry.Value); + } + + await response.WriteAsync(htmlBuilder.ToString(), Encoding.UTF8); + } + } + + private IDictionary GetIndexArguments() + { + return new Dictionary() + { + //{ "%(DocumentTitle)", _options.DocumentTitle }, + //{ "%(HeadContent)", _options.HeadContent }, + //{ "%(ConfigObject)", JsonSerializer.Serialize(_options.ConfigObject, _jsonSerializerOptions) }, + //{ "%(OAuthConfigObject)", JsonSerializer.Serialize(_options.OAuthConfigObject, _jsonSerializerOptions) }, + //{ "%(Interceptors)", JsonSerializer.Serialize(_options.Interceptors) }, + }; + } + + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/AdminUI/PluginCoreAdminUIOptions.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/AdminUI/PluginCoreAdminUIOptions.cs new file mode 100644 index 00000000..9a004237 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/AdminUI/PluginCoreAdminUIOptions.cs @@ -0,0 +1,68 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using System.Reflection; +using System.Text; + +namespace PluginCore.AspNetCore.AdminUI +{ + public class PluginCoreAdminUIOptions + { + /// + /// Gets or sets a route prefix for accessing the swagger-ui + /// + public string RoutePrefix { get; set; } = "PluginCore/Admin"; + + /// + /// Gets or sets a Stream function for retrieving the swagger-ui page + /// + public Func IndexStream + { + get + { + Func funcStream = null; + ; + Config.PluginCoreConfig pluginCoreConfig = Config.PluginCoreConfigFactory.Create(); + switch (pluginCoreConfig.FrontendMode?.ToLower()) + { + case "localembedded": + funcStream = () => typeof(PluginCoreAdminUIOptions).GetTypeInfo().Assembly + .GetManifestResourceStream("PluginCore.AspNetCore.node_modules.plugincore_admin_frontend.dist.index.html"); + break; + case "localfolder": + string absoluteRootPath = PluginPathProvider.PluginCoreAdminDir(); + string indexFilePath = Path.Combine(absoluteRootPath, "index.html"); + + funcStream = () => (Stream)new FileStream(indexFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 1, FileOptions.Asynchronous | FileOptions.SequentialScan); + break; + case "remotecdn": + string remoteFrontendRootPath = pluginCoreConfig.RemoteFrontend; + string indexFileRemotePath = remoteFrontendRootPath + "/" + "index.html"; + + funcStream = () => new HttpClient().GetStreamAsync(indexFileRemotePath).Result; + break; + default: + funcStream = () => typeof(PluginCoreAdminUIOptions).GetTypeInfo().Assembly + .GetManifestResourceStream("PluginCore.AspNetCore.node_modules.plugincore_admin_frontend.dist.index.html"); + break; + } + + return funcStream; + } + } + + + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/AdminUI/PluginCoreAdminUIRemoteFileProvider.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/AdminUI/PluginCoreAdminUIRemoteFileProvider.cs new file mode 100644 index 00000000..1e134dc1 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/AdminUI/PluginCoreAdminUIRemoteFileProvider.cs @@ -0,0 +1,132 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using System.Text; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Primitives; + +namespace PluginCore.AspNetCore.AdminUI +{ + public class PluginCoreAdminUIRemoteFileProvider : IFileProvider + { + protected string RootUrl { get; set; } + + public PluginCoreAdminUIRemoteFileProvider(string rootUrl) + { + this.RootUrl = rootUrl; + } + + public IDirectoryContents GetDirectoryContents(string subpath) + { + return (IDirectoryContents)NotFoundDirectoryContents.Singleton; + } + + public IFileInfo GetFileInfo(string subpath) + { + if (string.IsNullOrEmpty(subpath)) + return (IFileInfo)new NotFoundFileInfo(subpath); + + IFileInfo fileInfo = new PluginCoreAdminUIFileInfo(this.RootUrl, subpath); + + return fileInfo; + } + + public IChangeToken Watch(string filter) + { + throw new NotImplementedException(); + } + + + public class PluginCoreAdminUIFileInfo : IFileInfo + { + protected string RootUrl { get; set; } + + protected string SubPath { get; set; } + + private string _name; + + public PluginCoreAdminUIFileInfo(string rootUrl, string subpath) + { + this.RootUrl = rootUrl; + this.SubPath = subpath; + this._name = this.SubPath.Substring(this.SubPath.LastIndexOf("/") + 1); + } + + public Stream CreateReadStream() + { + HttpClient httpClient = new HttpClient(); + + return httpClient.GetStreamAsync($"{this.RootUrl}/{this.SubPath}").Result; + } + + public bool Exists + { + get + { + bool isExist = false; + if (this.Name == "index.html") + { + isExist = true; + } + + return isExist; + } + } + public bool IsDirectory + { + get + { + return false; + } + } + + public DateTimeOffset LastModified + { + get + { + return new DateTimeOffset(DateTime.Now); + } + } + + public long Length + { + get + { + return 111; + } + } + + public string Name + { + get + { + return this._name; + } + } + + public string PhysicalPath + { + get + { + return ""; + } + } + } + + } + + + +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Authentication/PluginCoreAuthenticationHandler.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Authentication/PluginCoreAuthenticationHandler.cs new file mode 100644 index 00000000..bd9ab972 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Authentication/PluginCoreAuthenticationHandler.cs @@ -0,0 +1,72 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Generic; +using System.Security.Claims; +using System.Text; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using PluginCore.AspNetCore.Authorization; + +namespace PluginCore.AspNetCore.Authentication +{ + /// + /// https://stackoverflow.com/questions/52287542/invalidoperationexception-no-authenticationscheme-was-specified-and-there-was + /// + public class PluginCoreAuthenticationHandler : AuthenticationHandler + { + private readonly AccountManager _accountManager; + + public PluginCoreAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, AccountManager accountManager) : base(options, logger, encoder, clock) + { + this._accountManager = accountManager; + } + + protected override async Task HandleAuthenticateAsync() + { + string token = this._accountManager.CurrentToken(); + if (string.IsNullOrEmpty(token)) + { + return AuthenticateResult.NoResult(); + } + + bool isAdmin = AccountManager.IsAdminToken(token); + + if (!isAdmin) + { + return AuthenticateResult.Fail($"token is not admin"); + } + else + { + var id = new ClaimsIdentity( + // new Claim[] { new Claim("PluginCore.Token", token) }, // not safe , just as an example , should custom claims on your own + claims: new Claim[] { new Claim(type: IPlugins.Constants.AspNetCoreAuthenticationClaimType, value: token) }, // not safe , just as an example , should custom claims on your own + authenticationType: Scheme.Name + ); + ClaimsPrincipal principal = new ClaimsPrincipal(identity: id); + var ticket = new AuthenticationTicket( + principal: principal, + properties: new AuthenticationProperties(), + authenticationScheme: Scheme.Name); + + // Utils.LogUtil.Info($"通过 Authentication: token: {token}"); + Utils.LogUtil.Info($"Authentication Passed"); + + return AuthenticateResult.Success(ticket); + } + + } + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Authentication/PluginCoreAuthenticationSchemeOptions.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Authentication/PluginCoreAuthenticationSchemeOptions.cs new file mode 100644 index 00000000..53fd10a6 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Authentication/PluginCoreAuthenticationSchemeOptions.cs @@ -0,0 +1,23 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.AspNetCore.Authentication; + +namespace PluginCore.AspNetCore.Authentication +{ + public class PluginCoreAuthenticationSchemeOptions : AuthenticationSchemeOptions + { + + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Authorization/AccountManager.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Authorization/AccountManager.cs new file mode 100644 index 00000000..5b3f9870 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Authorization/AccountManager.cs @@ -0,0 +1,149 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.AspNetCore.Http; +using PluginCore.Utils; + +namespace PluginCore.AspNetCore.Authorization +{ + public class AccountManager + { + private readonly IHttpContextAccessor _httpContextAccessor; + + public Microsoft.AspNetCore.Http.HttpContext HttpContext + { + get + { + return this._httpContextAccessor.HttpContext; + } + } + + public AccountManager(IHttpContextAccessor httpContextAccessor) + { + // Exception: IFeatureCollection has been disposed. Object name: 'Collection'. + // https://stackoverflow.com/questions/59963383/session-setstring-method-throws-exception-ifeaturecollection-has-been-disposed + //HttpContext = ((HttpContextAccessor)httpContextAccessor).HttpContext; + //HttpContext = httpContextAccessor.HttpContext; + // 注意: 不要将 HttpContext 保存起来,应当每次都从 httpContextAccessor 取 + _httpContextAccessor = httpContextAccessor; + } + + public static Config.PluginCoreConfig.AdminModel Admin + { + get + { + return Config.PluginCoreConfigFactory.Create().Admin; + } + set + { + var sourceModel = Config.PluginCoreConfigFactory.Create(); + sourceModel.Admin = value; + Config.PluginCoreConfigFactory.Save(sourceModel); + } + } + + public static string CurrentToken(HttpContext httpContext) + { + string token = null; + HttpRequest request = httpContext.Request; + try + { + // header -> cookie + try + { + // header 中找 token + if (request.Headers.ContainsKey("Authorization")) + { + string authHeader = request.Headers["Authorization"]; + if (!string.IsNullOrEmpty(authHeader) && authHeader.StartsWith("Bearer")) + { + token = authHeader.Substring("Bearer ".Length).Trim(); + } + } + } + catch (Exception ex) + { + //throw ex; + } + if (string.IsNullOrEmpty(token)) + { + // cookie 中找 token + //string tokenCookieName = "token"; + // string tokenCookieName = "PluginCore.Admin.Token"; + string tokenCookieName = IPlugins.Constants.AspNetCoreAuthorizationTokenCookieName; + if (request.Cookies.Keys.Contains(tokenCookieName)) + { + if (request.Cookies[tokenCookieName] != null && string.IsNullOrEmpty(request.Cookies[tokenCookieName]) == false) + { + token = request.Cookies[tokenCookieName]; + } + } + } + } + catch (Exception ex) + { + throw ex; + } + + return token; + } + + public string CurrentToken() + { + return CurrentToken(this.HttpContext); + } + + public static string CreateToken() + { + return CreateToken(Admin.UserName, Admin.Password); + } + + public static string CreateToken(string userName, string password) + { + string token = $"UserName={userName}&Password={password}"; + token = Md5Helper.MD5Encrypt32(token); + + return token; + } + + public static bool IsAdminToken(string token) + { + bool isAdmin = false; + isAdmin = CreateToken().Equals(token); + + return isAdmin; + } + + public bool IsAdmin() + { + return IsAdmin(this.HttpContext); + } + + public static bool IsAdmin(HttpContext httpContext) + { + bool isAdmin = false; + try + { + string currentToken = CurrentToken(httpContext); + isAdmin = IsAdminToken(currentToken); + } + catch (Exception ex) + { + throw ex; + } + + return isAdmin; + } + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Authorization/PluginCoreAdminAuthorizationHandler.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Authorization/PluginCoreAdminAuthorizationHandler.cs new file mode 100644 index 00000000..215cef21 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Authorization/PluginCoreAdminAuthorizationHandler.cs @@ -0,0 +1,59 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; + +namespace PluginCore.AspNetCore.Authorization +{ + public class PluginCoreAdminAuthorizationHandler : AuthorizationHandler + { + private readonly AccountManager _accountManager; + + public PluginCoreAdminAuthorizationHandler(AccountManager accountManager) + { + _accountManager = accountManager; + } + + /// + /// 必须在其中呼叫一次 代表满足 ,否则皆为 不满足此 Requirement + /// + /// + /// + /// + protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, + PluginCoreAdminRequirement requirement) + { + bool isAdmin = this._accountManager.IsAdmin(); + if (!isAdmin) + { + context.Fail(); + } + else + { + // 认证通过后, 可通过下面方式获取 token + var identity = context.User.Identity; + + string token = this._accountManager.CurrentToken(); + + // Utils.LogUtil.Info($"通过 Authorization: token: {token}"); + Utils.LogUtil.Info($"Authorization Granted"); + + context.Succeed(requirement); + } + + await Task.CompletedTask; + } + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Authorization/PluginCoreAdminAuthorizeAttribute.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Authorization/PluginCoreAdminAuthorizeAttribute.cs new file mode 100644 index 00000000..dc85f768 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Authorization/PluginCoreAdminAuthorizeAttribute.cs @@ -0,0 +1,31 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.AspNetCore.Authorization; + +namespace PluginCore.AspNetCore.Authorization +{ + /// + /// 注意: PluginCoreAdmin -> PluginCore.Admin + /// + public class PluginCoreAdminAuthorizeAttribute : AuthorizeAttribute + { + // public PluginCoreAdminAuthorizeAttribute() : base("PluginCore.Admin") + public PluginCoreAdminAuthorizeAttribute() : base(policy: IPlugins.Constants.AspNetCoreAuthorizationPolicyName) + { + // 同时明确指定 认证方案 与 授权策略 + AuthenticationSchemes = PluginCore.IPlugins.Constants.AspNetCoreAuthenticationScheme; + } + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Authorization/PluginCoreAdminRequirement.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Authorization/PluginCoreAdminRequirement.cs new file mode 100644 index 00000000..5ee934bc --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Authorization/PluginCoreAdminRequirement.cs @@ -0,0 +1,26 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.AspNetCore.Authorization; + +namespace PluginCore.AspNetCore.Authorization +{ + public class PluginCoreAdminRequirement : IAuthorizationRequirement + { + public PluginCoreAdminRequirement() + { + + } + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/BackgroundServices/BackgroundServicesHelper.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/BackgroundServices/BackgroundServicesHelper.cs new file mode 100644 index 00000000..62644fd4 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/BackgroundServices/BackgroundServicesHelper.cs @@ -0,0 +1,27 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using Microsoft.Extensions.DependencyInjection; + +namespace PluginCore.AspNetCore.BackgroundServices +{ + public static class BackgroundServicesHelper + { + public static IServiceCollection AddBackgroundServices(this IServiceCollection services) + { + //services.AddScoped(typeof(IHostedService), typeof(TimeBackgroundService)); + // AddHostedService: Microsoft.AspNetCore.App + services.AddHostedService(); // 以这种方式注入就是单例 + + return services; + } + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/BackgroundServices/PluginTimeJobBackgroundService.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/BackgroundServices/PluginTimeJobBackgroundService.cs new file mode 100644 index 00000000..0c7c4934 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/BackgroundServices/PluginTimeJobBackgroundService.cs @@ -0,0 +1,92 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Generic; +using System.Text; +using System.Linq; +using System.Threading.Tasks; +using PluginCore.Interfaces; +using PluginCore.IPlugins; +using PluginCore.Utils; + +namespace PluginCore.AspNetCore.BackgroundServices +{ + public class PluginTimeJobBackgroundService : TimeBackgroundService + { + /// + /// 插件与之最近执行时间 + /// 最近执行时间: 10位秒 时间戳 + /// + private readonly Dictionary _pluginAndLastExecuteTimeDic = new Dictionary(); + + private readonly IPluginFinder _pluginFinder; + + private static readonly object _doWorklock = new object(); + + public PluginTimeJobBackgroundService(IPluginFinder pluginFinder) + { + _pluginFinder = pluginFinder; + // 最小间隔 1 秒 + _timerPeriod = TimeSpan.FromSeconds(1); + } + + protected override void DoWork(object state) + { + lock (_doWorklock) + { + //Console.WriteLine("Memory used before collection: {0:N0}", GC.GetTotalMemory(false)); + + var plugins = this._pluginFinder.EnablePlugins().ToList(); + + List enabledPluginKeyList = new List(); + foreach (var item in plugins) + { + string pluginKey = item.GetType().ToString(); + enabledPluginKeyList.Add(pluginKey); + if (this._pluginAndLastExecuteTimeDic.ContainsKey(pluginKey)) + { + long lastExecuteTime = this._pluginAndLastExecuteTimeDic[pluginKey]; + long nowTime = DateTime.Now.ToTimeStamp10(); + if (nowTime - lastExecuteTime >= item.SecondsPeriod) + { + // 调用 + Utils.LogUtil.Info($"{pluginKey}: {nameof(ITimeJobPlugin)}.{nameof(ITimeJobPlugin.ExecuteAsync)}"); + Task task = item?.ExecuteAsync(); + this._pluginAndLastExecuteTimeDic[pluginKey] = DateTime.Now.ToTimeStamp10(); + } + } + else + { + // 调用 + Utils.LogUtil.Info($"{pluginKey}: {nameof(ITimeJobPlugin)}.{nameof(ITimeJobPlugin.ExecuteAsync)}"); + Task task = item?.ExecuteAsync(); + this._pluginAndLastExecuteTimeDic.Add(pluginKey, DateTime.Now.ToTimeStamp10()); + } + } + // 所有插件遍历结束 + // 出现在了 _pluginAndLastExecuteTimeDic 中,但没有出现在 enabledPluginKeyList, 说明为之前启用过,但现在已禁用的插件,需要去除掉 + List keys = this._pluginAndLastExecuteTimeDic.Select(m => m.Key).ToList(); + foreach (string key in keys) + { + if (!enabledPluginKeyList.Contains(key)) + { + this._pluginAndLastExecuteTimeDic.Remove(key); + } + } + + GC.Collect(); + GC.WaitForPendingFinalizers(); + //Console.WriteLine("Memory used after full collection: {0:N0}", GC.GetTotalMemory(true)); + } + } + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/BackgroundServices/TimeBackgroundService.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/BackgroundServices/TimeBackgroundService.cs new file mode 100644 index 00000000..9532e80f --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/BackgroundServices/TimeBackgroundService.cs @@ -0,0 +1,47 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using Microsoft.Extensions.Hosting; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace PluginCore.AspNetCore.BackgroundServices +{ + public abstract class TimeBackgroundService : IHostedService, IDisposable + { + protected Timer _timer; + protected TimeSpan _timerPeriod; + + public virtual Task StartAsync(CancellationToken cancellationToken) + { + _timer = new Timer(DoWork, null, TimeSpan.Zero, _timerPeriod); + + return Task.CompletedTask; + } + + public virtual Task StopAsync(CancellationToken cancellationToken) + { + _timer?.Change(Timeout.Infinite, 0); + + return Task.CompletedTask; + } + + protected abstract void DoWork(object state); + + public virtual void Dispose() + { + _timer?.Dispose(); + } + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/CHANGELOG.md b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/CHANGELOG.md new file mode 100644 index 00000000..4ffa87a0 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/CHANGELOG.md @@ -0,0 +1,280 @@ +# Changelog + +All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. + +--- +## [unreleased] + +### Documentation + +- **(CHANGELOG.md)** update - ([85bf0d9](https://github.com/yiyungent/PluginCore/commit/85bf0d9bd8e2f9efa7e8c0db3127ec8aaa054bc8)) - github-actions[bot] +- **(CHANGELOG.md)** update - ([a329e4e](https://github.com/yiyungent/PluginCore/commit/a329e4e7ec4048ec445e826f4536402abd832e46)) - github-actions[bot] +- **(CHANGELOG.md)** update - ([0bbfc89](https://github.com/yiyungent/PluginCore/commit/0bbfc8955b7f6338db2125c78ec250e9eeeadcce)) - github-actions[bot] +- **(CHANGELOG.md)** update - ([4f6b47b](https://github.com/yiyungent/PluginCore/commit/4f6b47b3f86bfce4a8f660166837a7322c568d78)) - github-actions[bot] + +### Miscellaneous Chores + +- **(src/plugincore.aspnetcore)** cliff.toml, CHANGELOG.md - ([bf95c28](https://github.com/yiyungent/PluginCore/commit/bf95c287ae9902608c4711e4dec05575c2d0e794)) - yiyun + +--- +## [PluginCore.AspNetCore-v1.4.3](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v1.4.2..PluginCore.AspNetCore-v1.4.3) - 2024-08-31 + +### Bug Fixes + +- **(PluginCore.AspNetCore/Controllers/PluginsController)** swagger [FromForm] - ([241d9a7](https://github.com/yiyungent/PluginCore/commit/241d9a72973d9cf1a11c11264a91e9370b2a6cda)) - yiyun + +### Features + +- **(src/plugincore.aspnetcore/extensions/plugincorestartupextensions.cs)** 插件 wwwroot 默认页 指定, 无需再手动 - ([469ae81](https://github.com/yiyungent/PluginCore/commit/469ae81fc31adce0c0b0701e2dfa1d98634ee184)) - yiyun + +### Miscellaneous Chores + +- **(PluginCore.AspNetCore.csproj)** 1.4.2 -> 1.4.3 - ([1924e0c](https://github.com/yiyungent/PluginCore/commit/1924e0c179aaac1a1d1f57a8bdebc7578d0b7650)) - yiyun + +--- +## [PluginCore.AspNetCore-v1.4.2](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v1.4.1..PluginCore.AspNetCore-v1.4.2) - 2024-04-06 + +### Features + +- **(src/plugincore.aspnetcore/backgroundservices/plugintimejobbackgroundservice.cs)** log - ([5da04c2](https://github.com/yiyungent/PluginCore/commit/5da04c20b57a166811ef8af828c5fd0bafab844c)) - yiyun +- **(src/plugincore.aspnetcore/extensions/plugincorestartupextensions.cs)** plugin:startup->appstart - ([71cafe7](https://github.com/yiyungent/PluginCore/commit/71cafe7fa7348b4d8ccb0b342f31ff848c6e77e9)) - yiyun + +### Build + +- **(src/plugincore.aspnetcore/plugincore.aspnetcore.csproj)** 1.4.1 -> 1.4.2 - ([75f17be](https://github.com/yiyungent/PluginCore/commit/75f17becb01b6d833759811e65cf464ea7a745b1)) - yiyun + +--- +## [PluginCore.AspNetCore-v1.4.1](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v1.4.0..PluginCore.AspNetCore-v1.4.1) - 2024-03-14 + +### Bug Fixes + +- **(src/plugincore.aspnetcore/extensions/plugincorestartupextensions.cs)** log, _serviceProvider - ([7d7a904](https://github.com/yiyungent/PluginCore/commit/7d7a904fd1794e26f278655f6f3f286b92bd1492)) - yiyun + +### Build + +- **(plugincore.aspnetcore.csproj)** 1.4.0 -> 1.4.1 - ([393d861](https://github.com/yiyungent/PluginCore/commit/393d861870c45a70f63421b5451df76e4ed9c808)) - yiyun + +--- +## [PluginCore.AspNetCore-v1.4.0](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v1.3.4..PluginCore.AspNetCore-v1.4.0) - 2024-02-15 + +### Features + +- **(src/plugincore.aspnetcore)** authentication & Authorize - ([73a673e](https://github.com/yiyungent/PluginCore/commit/73a673e06d2a8d662a44323cf7401e91d814ae90)) - yiyun +- **(src/plugincore.aspnetcore)** 认证与授权: 优化,分离, PluginCoreStartupExtensions 优化 - ([ebab6d8](https://github.com/yiyungent/PluginCore/commit/ebab6d888a303f221e4a45ddaa28107ff4a012c4)) - yiyun +- **(src/plugincore.aspnetcore)** accountManager 部分方法静态化, 提供 HttpContext 传入方式, 相关引用处更新调用 - ([491f334](https://github.com/yiyungent/PluginCore/commit/491f334baabb28e1736b5cc1bd19a0081c54949c)) - yiyun + +### Miscellaneous Chores + +- **(src/plugincore.aspnetcore/extensions/plugincorestartupextensions.cs)** 代码缩进: 美化 - ([6a03628](https://github.com/yiyungent/PluginCore/commit/6a036289ba6f3ae1592b4e4ed58b4fb1bcdc1306)) - yiyun + +### Build + +- **(plugincore.aspnetcore.csproj)** 1.3.4 -> 1.4.0 - ([f786f5e](https://github.com/yiyungent/PluginCore/commit/f786f5eeb5e19558e3e73890a42f56ce812784cc)) - yiyun + +--- +## [PluginCore.AspNetCore-v1.3.4](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v1.3.3..PluginCore.AspNetCore-v1.3.4) - 2023-12-30 + +### Bug Fixes + +- **(src/plugincore.aspnetcore)** 转向/适配 LogUtil - ([1a7c71d](https://github.com/yiyungent/PluginCore/commit/1a7c71d748fd228f1053de67572a04f466012ab8)) - yiyun +- **(src/plugincore.aspnetcore)** 适配: LogUtil - ([f150d07](https://github.com/yiyungent/PluginCore/commit/f150d07b55326133afdfe2c156464605483feb05)) - yiyun +- **(src/plugincore.aspnetcore)** 适配 LogUtil.Error - ([9c82b16](https://github.com/yiyungent/PluginCore/commit/9c82b161c5507ad1a7b78b1191dba208e8f4be45)) - yiyun +- **(src/plugincore.aspnetcore/middlewares/languagemiddleware.cs)** namespace: 语法降级 - ([a1f83fc](https://github.com/yiyungent/PluginCore/commit/a1f83fce8f1dcf2e5ead3b8a15eae006053af4de)) - yiyun + +### Documentation + +- **(src/plugincore.aspnetcore/readme.txt)** zh -> EN - ([f822b0b](https://github.com/yiyungent/PluginCore/commit/f822b0b79d1afca536bb7e10814226fb65988365)) - yiyun +- **(src/plugincore.aspnetcore/readme.txt)** update - ([0a3210b](https://github.com/yiyungent/PluginCore/commit/0a3210bc073252a1f18d0910dc80aabd1061a66f)) - yiyun + +### Features + +- **(src/**/*.cs)** // License: Apache-2.0 -> // License: GNU LGPLv3 - ([57366d3](https://github.com/yiyungent/PluginCore/commit/57366d3e2afdb8e20e94851aa8a09f1ee61b6d7e)) - yiyun +- **(src/**/*.cs)** // Project: https://moeci.com/PluginCore -> // Project: https://yiyungent.github.io/PluginCore - ([7420480](https://github.com/yiyungent/PluginCore/commit/742048065978c1b8597fab3d52f011db4247fbda)) - yiyun +- **(src/plugincore.aspnetcore)** use Constants - ([6cd128a](https://github.com/yiyungent/PluginCore/commit/6cd128a2ce6da83f8cfee46ae03a7af44380e791)) - yiyun +- **(src/plugincore.aspnetcore)** languageMiddleware: 当前 Language - ([b0d79e7](https://github.com/yiyungent/PluginCore/commit/b0d79e7a0ae469cc295d87ed8a7c97a355cbc7a1)) - yiyun +- **(src/plugincore.aspnetcore)** 适配: LogUtil - ([b09da39](https://github.com/yiyungent/PluginCore/commit/b09da39199d614f40ec533d7a472069c40121fae)) - yiyun +- **(src/plugincore.aspnetcore)** 认证与授权: 日志输出: 中文->英文 - ([1470a99](https://github.com/yiyungent/PluginCore/commit/1470a9913fff6e0df37495d1143f5a55eed995fd)) - yiyun +- **(src/plugincore.aspnetcore/controllers/pluginscontroller.cs)** 启用,禁用: Message: 使用 BasePlugin 源 - ([d6c8a33](https://github.com/yiyungent/PluginCore/commit/d6c8a3361a7573c68031ad6e8bd8aa35c65035d4)) - yiyun +- **(src/plugincore.aspnetcore/extensions/plugincorestartupextensions.cs)** use Constants - ([68952a1](https://github.com/yiyungent/PluginCore/commit/68952a13613d469cefe9fc9fd13ab7525dd93f78)) - yiyun +- **(src/plugincore.aspnetcore/extensions/plugincorestartupextensions.cs)** use constants - ([37798ef](https://github.com/yiyungent/PluginCore/commit/37798efc92f82e5e5f4d696b6de5466809a18d48)) - yiyun +- **(src/plugincore.aspnetcore/extensions/plugincorestartupextensions.cs)** 初始化: Logger - ([6e24a5a](https://github.com/yiyungent/PluginCore/commit/6e24a5a391af60f8ec0ba85e8ea86ea23c49f522)) - yiyun +- **(src/plugincore.aspnetcore/extensions/plugincorestartupextensions.cs)** log 输出 -> 英文 - ([e41b73a](https://github.com/yiyungent/PluginCore/commit/e41b73adda0ca5d9f1f60109ee8b6fe7de39deda)) - yiyun + +### Build + +- **(plugincore.aspnetcore.csproj)** 1.3.3 -> 1.3.4 - ([84550cc](https://github.com/yiyungent/PluginCore/commit/84550cc9159c387cedbc8beda282dfcfea9cdcb1)) - yiyun + +--- +## [PluginCore.AspNetCore-v1.3.3](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v1.3.2..PluginCore.AspNetCore-v1.3.3) - 2023-12-14 + +### Build + +- **(src/plugincore.aspnetcore/plugincore.aspnetcore.csproj)** packageReference update, 1.3.2->1.3.3 - ([6fc3d1a](https://github.com/yiyungent/PluginCore/commit/6fc3d1ad6359a5f8690babfc6949e0821d5bd6c5)) - yiyun + +--- +## [PluginCore.AspNetCore-v1.3.2](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v1.3.1..PluginCore.AspNetCore-v1.3.2) - 2023-08-21 + +### Features + +- **(src/plugincore.aspnetcore/extensions/plugincorestartupextensions.cs)** services: PluginFinder - ([144db05](https://github.com/yiyungent/PluginCore/commit/144db05576001a72f9cbd6beea0b3b5baa6a082c)) - yiyun + +### Build + +- **(src/plugincore.aspnetcore/plugincore.aspnetcore.csproj)** 1.3.2 - ([dfdd17d](https://github.com/yiyungent/PluginCore/commit/dfdd17dbcb2fc636cc102ab76497ff5768c64a4e)) - yiyun + +--- +## [PluginCore.AspNetCore-v1.3.1](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v1.3.0..PluginCore.AspNetCore-v1.3.1) - 2023-02-15 + +### Build + +- **(plugincore.aspnetcore.csproj)** `1.3.1` - ([39e0d03](https://github.com/yiyungent/PluginCore/commit/39e0d037c6f3e0dcf4845b5ca8ae2aa41142a474)) - yiyun + +--- +## [PluginCore.AspNetCore-v1.3.0](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v1.2.0..PluginCore.AspNetCore-v1.3.0) - 2023-02-15 + +### Bug Fixes + +- **(plugincore.aspnetcore,plugincore)** iList EnabledPlugins->List,IList不支持Remove - ([4d5d30e](https://github.com/yiyungent/PluginCore/commit/4d5d30e66c4c28998a7a6ac96bf3ffb25e4872b4)) - yiyun + +### Features + +- **(plugincore.aspnetcore,plugincore.iplugins,plugincore)** 仅保留已启用/已禁用 状态, IPlugin新方法 - ([e843a5b](https://github.com/yiyungent/PluginCore/commit/e843a5ba9fad4e88290c09bb3282b730c44c5a06)) - yiyun + +### Build + +- **(src/plugincore.aspnetcore/plugincore.aspnetcore.csproj)** `1.3.0` - ([64b9775](https://github.com/yiyungent/PluginCore/commit/64b977569312539aa2775235ae1f0fca5517ddcd)) - yiyun + +--- +## [PluginCore.AspNetCore-v1.2.0](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v1.1.0..PluginCore.AspNetCore-v1.2.0) - 2023-02-14 + +### Features + +- **(src/plugincore.aspnetcore/controllers/debugcontroller.cs)** pluginContexts:PluginId - ([f82b7b1](https://github.com/yiyungent/PluginCore/commit/f82b7b199420b15fc6f38e8f26c8094ee4be1b88)) - yiyun + +### Build + +- **(src/plugincore.aspnetcore/plugincore.aspnetcore.csproj)** 1.2.0 - ([fe418e0](https://github.com/yiyungent/PluginCore/commit/fe418e0f67dd8ed1fd2d78dc3f4106c8dc1a396b)) - yiyun + +--- +## [PluginCore.AspNetCore-v1.1.0](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v1.0.4..PluginCore.AspNetCore-v1.1.0) - 2023-02-10 + +### Features + +- **(src/plugincore.aspnetcore/)** update - ([52ee48f](https://github.com/yiyungent/PluginCore/commit/52ee48fe234040fd0cc0a4d21f3c2e1c9483735e)) - yiyun +- **(src/plugincore.aspnetcore/controllers/debugcontroller.cs)** add - ([196823d](https://github.com/yiyungent/PluginCore/commit/196823d6e3b39eb70d1d4bb55d7d4c8433ee64fc)) - yiyun +- **(src/plugincore.aspnetcore/controllers/debugcontroller.cs)** 完成 - ([2a2e213](https://github.com/yiyungent/PluginCore/commit/2a2e2131ee4b25912c16ca0d01ef408702cbfc39)) - yiyun +- **(src/plugincore.aspnetcore/controllers/debugcontroller.cs)** services - ([22edc22](https://github.com/yiyungent/PluginCore/commit/22edc229c1bedc4667a920ef62c11c8e002ba9d2)) - yiyun + +### Build + +- **(src/plugincore.aspnetcore/plugincore.aspnetcore.csproj)** 1.1.0 - ([3bdb176](https://github.com/yiyungent/PluginCore/commit/3bdb17602c3258386711b200917e567c82dc442b)) - yiyun + +--- +## [PluginCore.AspNetCore-v1.0.4](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v1.0.3..PluginCore.AspNetCore-v1.0.4) - 2023-01-12 + +### Build + +- **(plugincore.aspnetcore.csproj)** 1.0.4 - ([46cf5dd](https://github.com/yiyungent/PluginCore/commit/46cf5dd5f955f6b648323e5216af97dd177e5a4b)) - yiyun + +--- +## [PluginCore.AspNetCore-v1.0.3](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v1.0.2..PluginCore.AspNetCore-v1.0.3) - 2022-06-03 + +### Bug Fixes + +- **(backgroundservices/plugintimejobbackgroundservice.cs)** 定时任务:强制GC回收,抑制内存++ - ([434e824](https://github.com/yiyungent/PluginCore/commit/434e82403b6aa2050945eeaa1131ea7965bdbc5f)) - yiyun + +### Build + +- **(plugincore.aspnetcore.csproj)** 1.0.3 - ([7da5638](https://github.com/yiyungent/PluginCore/commit/7da5638672571ca0ef057ebf73493260886b903e)) - yiyun + +--- +## [PluginCore.AspNetCore-v1.0.2](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v1.0.1..PluginCore.AspNetCore-v1.0.2) - 2022-04-19 + +### Features + +- **(plugincore.aspnetcore)** pluginsController: 移除: 插件上传大小限制 - ([90f8d67](https://github.com/yiyungent/PluginCore/commit/90f8d671a840ae8ca3688f4a5cbc22e568a6159e)) - yiyun + +### Style + +- add: copyright: *.cs - ([9643dce](https://github.com/yiyungent/PluginCore/commit/9643dce112861a440d63306cb555accbed3d5111)) - yiyun + +### Build + +- **(plugincore.aspnetcore.csproj)** 1.0.2 - ([b4eb1cc](https://github.com/yiyungent/PluginCore/commit/b4eb1cca9039ff95a516ba6d4000f10d156e03d0)) - yiyun +- **(plugincore.aspnetcore/package.json,package-lock.json)** "plugincore-admin-frontend": "0.3.2" - ([7a70b20](https://github.com/yiyungent/PluginCore/commit/7a70b2003128bf569b4fdd4b7eaa662a92da8ee8)) - yiyun + +--- +## [PluginCore.AspNetCore-v1.0.1](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v1.0.0..PluginCore.AspNetCore-v1.0.1) - 2022-04-17 + +### Build + +- **(plugincore.aspnetcore.csproj)** 1.0.1 - ([ab348ce](https://github.com/yiyungent/PluginCore/commit/ab348ceca937f1b1f4ba8c83f4816b9a0ae7ea3e)) - yiyun + +--- +## [PluginCore.AspNetCore-v1.0.0](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v0.0.5..PluginCore.AspNetCore-v1.0.0) - 2022-04-17 + +### Bug Fixes + +- **(plugincore)** 临时修复由于 PluginContextManager 单例失败 导致的插件信息丢失 - ([fa613b4](https://github.com/yiyungent/PluginCore/commit/fa613b4c46e41c906fe955eb8f62c3f4937795bc)) - yiyun + +### Features + +- **(plugincore,plugincore.aspnetcore)** aspNetCorePluginManagerBeta,PluginLoadContext,PluginFinder - ([9d65a59](https://github.com/yiyungent/PluginCore/commit/9d65a590e3e0850251f6d815c322c7c5d9c7cf3f)) - yiyun +- **(plugincore.aspnetcore)** add:DebugController.PluginContext - ([cd8de63](https://github.com/yiyungent/PluginCore/commit/cd8de636c8d7e2b125e11c6ce4091567cb97d4bc)) - yiyun +- **(plugincore.aspnetcore)** commonResponseModel -> BaseResponseModel - ([1a0e834](https://github.com/yiyungent/PluginCore/commit/1a0e834b7dfdb45c45770d42b1733f1cb449a6ca)) - yiyun + +### Refactoring + +- **(plugincore.aspnetcore,plugincore)** 未完成 - ([a151bcd](https://github.com/yiyungent/PluginCore/commit/a151bcda125cb7e9b5fe11d44e1389afa7a1db5e)) - yiyun +- **(plugincore.aspnetcore,plugincore)** 重构v2: 未测试 - ([53dde31](https://github.com/yiyungent/PluginCore/commit/53dde31116bd6455d33f7d7006b6fd1430f3694b)) - yiyun +- **(plugincore.aspnetcore,plugincore)** 变量名,属性名,类名规范化 - ([eaadabf](https://github.com/yiyungent/PluginCore/commit/eaadabfd759228da245af1d9bd5b86e557540d28)) - yiyun + +### Build + +- **(plugincore.aspnetcore.csproj)** 1.0.0 - ([9b5caf4](https://github.com/yiyungent/PluginCore/commit/9b5caf4c4cdc543025c5493d35275a836cae61a9)) - yiyun + +--- +## [PluginCore.AspNetCore-v0.0.5](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v0.0.4..PluginCore.AspNetCore-v0.0.5) - 2022-04-16 + +### Build + +- **(plugincore.aspnetcore.csproj)** 0.0.5 - ([3e5cb09](https://github.com/yiyungent/PluginCore/commit/3e5cb0921a9faa33f93169c2d983011a1e8c5b5f)) - yiyun + +--- +## [PluginCore.AspNetCore-v0.0.4](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v0.0.3..PluginCore.AspNetCore-v0.0.4) - 2022-04-16 + +### Build + +- **(plugincore.aspnetcore.csproj)** 0.0.4 - ([adffab3](https://github.com/yiyungent/PluginCore/commit/adffab3fecee6696709c1d325b8c11e5a5389031)) - yiyun + +--- +## [PluginCore.AspNetCore-v0.0.3](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v0.0.2..PluginCore.AspNetCore-v0.0.3) - 2022-04-16 + +### Bug Fixes + +- **(plugincore.aspnetcore/extensions/plugincorestartupextensions.cs)** pluginFinder:TryAddTransient - ([03e9235](https://github.com/yiyungent/PluginCore/commit/03e9235c3c7fe68a3209cb1109792869e78aaa4e)) - yiyun +- **(plugincore.aspnetcore/extensions/plugincorestartupextensions.cs)** 注释错误: 在程序启动时加载所有 已安装并启用 的插件 - ([844826a](https://github.com/yiyungent/PluginCore/commit/844826a630f99c2d866a98e6556404d4b1857e52)) - yiyun + +### Build + +- **(plugincore.aspnetcore.csproj)** 0.0.3 - ([ee81a45](https://github.com/yiyungent/PluginCore/commit/ee81a4536af17a85bbebc07bc0c980af8c00ad63)) - yiyun + +--- +## [PluginCore.AspNetCore-v0.0.2](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v0.0.1..PluginCore.AspNetCore-v0.0.2) - 2022-04-16 + +### Refactoring + +- 1.提取出 PluginCore.AspNetCore,PluginCore.IPlugins.AspNetCore 2.提取出更多接口,可自由替换 - ([fffd8d9](https://github.com/yiyungent/PluginCore/commit/fffd8d91c23fd6e4a4d09cbf91975beb3cf7acf0)) - yiyun + +### Build + +- **(plugincore.aspnetcore.csproj)** 0.0.2 - ([ba7b274](https://github.com/yiyungent/PluginCore/commit/ba7b27496a4ba784c271305489b9f2f8e6f74ad3)) - yiyun + +--- +## [PluginCore.AspNetCore-v0.0.1](https://github.com/yiyungent/PluginCore/compare/PluginCore-v0.9.3..PluginCore.AspNetCore-v0.0.1) - 2022-03-26 + +### Features + +- **(plugincore.aspnetcore)** pluginCore.AspNetCore,PluginCore.AspNetCore-nuget-push.yml - ([491f63e](https://github.com/yiyungent/PluginCore/commit/491f63e3362129a2239d87b090aa04cc2e414e9a)) - yiyun + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Controllers忽略/AppCenterController.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Controllers忽略/AppCenterController.cs new file mode 100644 index 00000000..762495f5 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Controllers忽略/AppCenterController.cs @@ -0,0 +1,207 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using PluginCore; +using PluginCore.Models; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using PluginCore.AspNetCore.Authorization; +using PluginCore.AspNetCore.ResponseModel; +//using ResponseModel; + + +namespace PluginCore.AspNetCore.Controllers +{ + /// + /// 应用中心 + /// 插件 + /// + [Route("api/plugincore/admin/[controller]/[action]")] + [PluginCoreAdminAuthorize] + [ApiController] + public class AppCenterController : ControllerBase + { + #region Fields + + private static Dictionary _pluginDownloadTasks; + + #endregion + + #region Ctor + + static AppCenterController() + { + _pluginDownloadTasks = new Dictionary(); + } + + public AppCenterController() + { + + } + #endregion + + #region Actions + + #region 插件列表 + /// + /// 插件 + /// + /// + /// + [HttpGet, HttpPost] + public async Task> Plugins(string query = "") + { + BaseResponseModel responseDTO = new BaseResponseModel(); + IList pluginRegistryModels = new List(); + try + { + // 1. TODO: 从json文件中读取插件订阅源 registry url + string registryUrl = ""; + // 2. TODO: 向订阅源发送 http get 获取插件列表信息 eg: http://rem-core-plugins-registry.moeci.com/?query=xxx + IList remotePluginIds = new List(); + + // 3. 根据本地已有 PluginId 插件情况 状态赋值 + PluginConfigModel pluginConfigModel = PluginConfigModelFactory.Create(); + // IList localPluginIds = pluginConfigModel.EnabledPlugins.Concat(pluginConfigModel.DisabledPlugins).Concat(pluginConfigModel.UninstalledPlugins).ToList(); + IList localPluginIds = PluginPathProvider.AllPluginFolderName(); + + + + responseDTO.Code = 1; + responseDTO.Message = "获取远程插件数据成功"; + responseDTO.Data = pluginRegistryModels; + } + catch (Exception ex) + { + responseDTO.Code = -1; + responseDTO.Message = "获取远程插件数据失败: " + ex.Message; + responseDTO.Data = pluginRegistryModels; + } + + return await Task.FromResult(responseDTO); + } + #endregion + + #region 下载插件 + [HttpGet, HttpPost] + public async Task> DownloadPlugin(string pluginDownloadUrl = "") + { + BaseResponseModel responseDTO = new BaseResponseModel(); + + #region 效验 + if (string.IsNullOrEmpty(pluginDownloadUrl)) + { + responseDTO.Code = -1; + responseDTO.Message = "插件下载地址不正确"; + return responseDTO; + } + // TODO: 效验是否本地已经存在相同pluginId的插件 + + #endregion + + try + { + // 1.执行下载操作, TODO:存在问题,阻塞对性能不好,但不阻塞又不好通知用户插件下载进度,以及可能存在在插件下载过程中,用户再次点击下载 + WebClient webClient = new WebClient(); + // TODO: 插件下载文件路径 + string pluginDownloadFilePath = ""; + //webClient.DownloadFileAsync(new Uri(pluginDownloadFilePath), ""); + Task task = webClient.DownloadFileTaskAsync(pluginDownloadUrl, pluginDownloadFilePath); + + _pluginDownloadTasks.Add(pluginDownloadUrl, task); + + webClient.DownloadFileCompleted += Plugin_DownloadFileCompleted; + webClient.DownloadProgressChanged += Plugin_DownloadProgressChanged; + webClient.Disposed += WebClient_Disposed; + + responseDTO.Code = 1; + responseDTO.Message = "开始下载插件"; + } + catch (Exception ex) + { + responseDTO.Code = -1; + responseDTO.Message = "下载插件失败: " + ex.Message; + } + + return await Task.FromResult(responseDTO); + } + + + + + #endregion + + #region 获取插件下载进度 + [HttpGet, HttpPost] + public async Task> DownloadPluginProgress() + { + BaseResponseModel responseDTO = new BaseResponseModel(); + try + { + responseDTO.Data = new { }; + + + + responseDTO.Code = 1; + responseDTO.Message = "获取插件下载进度成功"; + } + catch (Exception ex) + { + responseDTO.Code = -1; + responseDTO.Message = "获取插件下载进度失败: " + ex.Message; + } + + return await Task.FromResult(responseDTO); + } + #endregion + + #endregion + + #region Helpers + + /// + /// 插件下载完成 + /// + /// + /// + private void Plugin_DownloadFileCompleted(object sender, System.ComponentModel.AsyncCompletedEventArgs e) + { + Utils.LogUtil.Info("插件下载完成"); + // 1.从 _pluginDownloadTasks 中移除 + //_pluginDownloadTasks.Remove(); + // 2. 解压插件 + + } + + private void Plugin_DownloadProgressChanged(object sender, DownloadProgressChangedEventArgs e) + { + Utils.LogUtil.Info($"插件下载进度改变: {e.ProgressPercentage}% {e.BytesReceived}/{e.TotalBytesToReceive}"); + } + + private void WebClient_Disposed(object sender, EventArgs e) + { + if (sender is WebClient webClient) + { + Utils.LogUtil.Info(webClient.BaseAddress); + } + + Utils.LogUtil.Info(nameof(WebClient_Disposed) + ": " + sender.ToString()); + } + + #endregion + + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Controllers忽略/DebugController.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Controllers忽略/DebugController.cs new file mode 100644 index 00000000..a3ef7fc5 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Controllers忽略/DebugController.cs @@ -0,0 +1,280 @@ +using System.Runtime.CompilerServices; +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using Microsoft.AspNetCore.Mvc; +using PluginCore.AspNetCore.Authorization; +using PluginCore.AspNetCore.ResponseModel; +using PluginCore.Interfaces; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using System.Linq; +using System.Runtime.Loader; +using Microsoft.Extensions.DependencyInjection; +using System.Collections.Concurrent; +using PluginCore.AspNetCore.Extensions; + +namespace PluginCore.AspNetCore.Controllers +{ + /// + /// [ASP.NET Core — 依赖注入\_啊晚的博客-CSDN博客\_asp.net core 依赖注入](https://blog.csdn.net/weixin_37648525/article/details/127942292) + /// [ASP.NET Core中的依赖注入(3): 服务的注册与提供 - Artech - 博客园](https://www.cnblogs.com/artech/p/asp-net-core-di-register.html) + /// [ASP.NET Core中的依赖注入(5): ServiceProvider实现揭秘 【总体设计 】 - Artech - 博客园](https://www.cnblogs.com/artech/p/asp-net-core-di-service-provider-1.html) + /// [dotnet/ServiceProvider.cs at main · dotnet/dotnet](https://github.com/dotnet/dotnet/blob/main/src/runtime/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceProvider.cs) + /// [Net6 DI源码分析Part2 Engine,ServiceProvider - 一身大膘 - 博客园](https://www.cnblogs.com/hts92/p/15800990.html) + /// [【特别的骚气】asp.net core运行时注入服务,实现类库热插拔 - 四处观察 - 博客园](https://www.cnblogs.com/1996-Chinese-Chen/p/16154218.html) + /// + /// ActivatorUtilities.CreateInstance(serviceProvider, "test"); + /// ActivatorUtilities.GetServiceOrCreateInstance(serviceProvider); + /// + /// + [Route("api/plugincore/admin/[controller]/[action]")] + [PluginCoreAdminAuthorize] + [ApiController] + public class DebugController : ControllerBase + { + #region Fields + private readonly IPluginContextManager _pluginContextManager; + #endregion + + #region Ctor + public DebugController(IPluginContextManager pluginContextManager) + { + _pluginContextManager = pluginContextManager; + } + #endregion + + #region Actions + + [HttpGet, HttpPost] + public async Task> PluginContexts() + { + BaseResponseModel responseModel = new BaseResponseModel(); + try + { + var pluginContextList = _pluginContextManager.All(); + Dictionary> keyValuePairs = new Dictionary>(); + foreach (var pluginContext in pluginContextList) + { + keyValuePairs.Add($"{pluginContext.GetType().ToString()} - {pluginContext.PluginId} - {pluginContext.GetHashCode()}", pluginContext.Assemblies.Select(m => m.FullName).ToList()); + } + + responseModel.Code = 1; + responseModel.Message = "success"; + responseModel.Data = keyValuePairs; + } + catch (Exception ex) + { + responseModel.Code = -1; + responseModel.Message = "error"; + responseModel.Data = ex.ToString(); + } + + return await Task.FromResult(responseModel); + } + + [HttpGet, HttpPost] + public async Task> AssemblyLoadContexts() + { + BaseResponseModel responseModel = new BaseResponseModel(); + try + { + var assemblyLoadContextDefault = AssemblyLoadContext.Default; + var assemblyLoadContextAll = AssemblyLoadContext.All; + var responseDataModel = new AssemblyLoadContextsResponseDataModel(); + responseDataModel.Default = new AssemblyLoadContextsResponseDataModel.AssemblyLoadContextModel + { + Name = assemblyLoadContextDefault.Name, + Type = assemblyLoadContextDefault.GetType().ToString(), + Assemblies = assemblyLoadContextDefault.Assemblies.Select(m => new AssemblyModel { FullName = m.FullName, DefinedTypes = m.DefinedTypes.Select(m => m.FullName).ToList() }).ToList() + }; + responseDataModel.All = assemblyLoadContextAll.Select(item => new AssemblyLoadContextsResponseDataModel.AssemblyLoadContextModel + { + Name = item.Name, + Type = item.GetType().ToString(), + Assemblies = item.Assemblies.Select(m => new AssemblyModel { FullName = m.FullName, DefinedTypes = m.DefinedTypes.Select(m => m.FullName).ToList() }).ToList() + }).ToList(); + + responseModel.Code = 1; + responseModel.Message = "success"; + responseModel.Data = responseDataModel; + } + catch (Exception ex) + { + responseModel.Code = -1; + responseModel.Message = "error"; + responseModel.Data = ex.ToString(); + } + + return await Task.FromResult(responseModel); + } + + [HttpGet, HttpPost] + public async Task> Assemblies() + { + BaseResponseModel responseModel = new BaseResponseModel(); + try + { + var assemblies = AppDomain.CurrentDomain.GetAssemblies(); + List assemblyModels = new List(); + foreach (var item in assemblies) + { + assemblyModels.Add(new AssemblyModel + { + FullName = item.FullName, + DefinedTypes = item.DefinedTypes.Select(m => m.FullName).ToList() + }); + } + + responseModel.Code = 1; + responseModel.Message = "success"; + responseModel.Data = assemblyModels; + } + catch (Exception ex) + { + responseModel.Code = -1; + responseModel.Message = "error"; + responseModel.Data = ex.ToString(); + } + + return await Task.FromResult(responseModel); + } + + [HttpGet, HttpPost] + public async Task> Services([FromServices] IServiceProvider serviceProvider) + { + BaseResponseModel responseModel = new BaseResponseModel(); + try + { + //IServiceProvider serviceProvider = HttpContext.RequestServices; + //var provider = serviceProvider.GetType().GetProperty("RootProvider", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + //var serviceField = provider.GetType().GetField("_realizedServices", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + //var serviceValue = serviceField.GetValue(provider); + //var funcType = serviceField.FieldType.GetGenericArguments()[1].GetGenericArguments()[0]; + //ConcurrentDictionary> realizedServices = (ConcurrentDictionary>)serviceValue; + + // 获取所有已经注册的服务 + var allService = serviceProvider.GetAllServiceDescriptors(); + + List serviceModels = new List(); + foreach (var item in allService) + { + serviceModels.Add(new ServiceModel + { + Type = item.Key.ToString(), + ImplementationType = item.Value.ImplementationType?.ToString() ?? "", + Lifetime = item.Value.Lifetime.ToString(), + TypeAssembly = new AssemblyModel + { + FullName = item.Key.Assembly.FullName, + }, + ImplementationTypeAssembly = new AssemblyModel + { + FullName = item.Value.ImplementationType?.Assembly?.FullName ?? "" + } + }); + } + + responseModel.Code = 1; + responseModel.Message = "success"; + responseModel.Data = serviceModels; + } + catch (Exception ex) + { + responseModel.Code = -1; + responseModel.Message = "error"; + responseModel.Data = ex.ToString(); + } + + return await Task.FromResult(responseModel); + } + + #endregion + + public sealed class AssemblyLoadContextsResponseDataModel + { + public AssemblyLoadContextModel Default + { + get; set; + } + + public List All + { + get; set; + } + + public sealed class AssemblyLoadContextModel + { + public string Name + { + get; set; + } + public string Type + { + get; set; + } + public List Assemblies + { + get; set; + } + } + } + + public sealed class AssembliesResponseDataModel + { + + } + + public sealed class ServiceModel + { + public string Type + { + get; set; + } + + public string ImplementationType + { + get; set; + } + + public string Lifetime + { + get; set; + } + + public AssemblyModel TypeAssembly + { + get; set; + } + + public AssemblyModel ImplementationTypeAssembly + { + get; set; + } + } + + public sealed class AssemblyModel + { + public string FullName + { + get; set; + } + + public List DefinedTypes + { + get; set; + } + } + + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Controllers忽略/HomeController.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Controllers忽略/HomeController.cs new file mode 100644 index 00000000..8b093c30 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Controllers忽略/HomeController.cs @@ -0,0 +1,73 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc; + +namespace PluginCore.AspNetCore.Controllers +{ + [Controller] + public class HomeController : Controller + { + #region Old + //private readonly IWebHostEnvironment _webHostEnvironment; + + //public bool IsLocalFronted + //{ + // get + // { + // return PluginCore.Config.PluginCoreConfigFactory.Create().IsLocalFrontend; + // } + //} + + //public string RemoteFronted + //{ + // get + // { + // return PluginCore.Config.PluginCoreConfigFactory.Create().RemoteFrontend; + // } + //} + + //public HomeController(IWebHostEnvironment webHostEnvironment) + //{ + // this._webHostEnvironment = webHostEnvironment; + //} + + //[Route("PluginCore/Admin")] + //public async Task Home() + //{ + // if (this.IsLocalFronted) + // { + // var localIndexFilePath = Path.Combine( + // this._webHostEnvironment.ContentRootPath, "PluginCoreAdmin", "index.html"); + + // return PhysicalFile(localIndexFilePath, "text/html"); + // } + // else + // { + // string htmlStr = string.Empty; + // HttpClient httpClient = new HttpClient(); + // htmlStr = await httpClient.GetStringAsync(this.RemoteFronted + "/index.html"); + + // return Content(htmlStr, "text/html", Encoding.UTF8); + // } + //} + #endregion + + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Controllers忽略/PluginWidgetController.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Controllers忽略/PluginWidgetController.cs new file mode 100644 index 00000000..0b00b6d4 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Controllers忽略/PluginWidgetController.cs @@ -0,0 +1,103 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using PluginCore; +using PluginCore.Models; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using PluginCore.AspNetCore.Authorization; +using PluginCore.AspNetCore.ResponseModel; +using PluginCore.IPlugins; +using System.Text; +using PluginCore.Interfaces; + +namespace PluginCore.AspNetCore.Controllers +{ + [Route("api/plugincore/[controller]/[action]")] + [ApiController] + public class PluginWidgetController : ControllerBase + { + #region Fields + private readonly IPluginFinder _pluginFinder; + #endregion + + #region Ctor + public PluginWidgetController(IPluginFinder pluginFinder) + { + _pluginFinder = pluginFinder; + } + #endregion + + #region Actions + + #region Widget + /// + /// Widget + /// + /// + [HttpGet, HttpPost] + //public async Task> Widget(string widgetKey, string extraPars = "") + public async Task Widget(string widgetKey, string extraPars = "") + { + BaseResponseModel responseModel = new ResponseModel.BaseResponseModel(); + string responseData = ""; + widgetKey = widgetKey.Trim('"', '\''); + string[] extraParsArr = null; + if (!string.IsNullOrEmpty(extraPars)) + { + extraParsArr = extraPars.Split(",", StringSplitOptions.RemoveEmptyEntries); + extraParsArr = extraParsArr.Select(m => m.Trim('"', '\'')).ToArray(); + } + StringBuilder sb = new StringBuilder(); + sb.AppendLine($""); + try + { + List plugins = this._pluginFinder.EnablePlugins().ToList(); + foreach (var item in plugins) + { + string widgetStr = await item.Widget(widgetKey, extraParsArr); + if (!string.IsNullOrEmpty(widgetStr)) + { + // TODO: 配合 PluginCoreConfig.PluginWidgetDebug + // TODO: PluginCoreConfig 改为 Options 模式, 避免手动反复读取文件 效率低 + //sb.AppendLine($""); + + sb.AppendLine(widgetStr); + } + } + + } + catch (Exception ex) + { + Utils.LogUtil.Error(ex, ex.Message); + sb.AppendLine($""); + } + sb.AppendLine($""); + responseData = sb.ToString(); + + responseModel.Code = 1; + responseModel.Message = "Load Widget Success"; + responseModel.Data = responseData; + + //return await Task.FromResult(responseModel); + return Content(responseData, "text/html;charset=utf-8"); + } + #endregion + + #endregion + + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Controllers忽略/PluginsController.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Controllers忽略/PluginsController.cs new file mode 100644 index 00000000..d6ddec27 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Controllers忽略/PluginsController.cs @@ -0,0 +1,688 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Generic; +using System.Configuration; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +//using Core.Common; +//using Framework.Authorization; +using PluginCore; +using PluginCore.Models; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using PluginCore.AspNetCore.Authorization; +using PluginCore.Infrastructure; +using PluginCore.IPlugins; +using PluginCore.AspNetCore.ResponseModel; +using PluginCore.Interfaces; +using PluginCore.AspNetCore.Interfaces; + +//using ResponseModel; + +namespace PluginCore.AspNetCore.Controllers +{ + [Route("api/plugincore/admin/[controller]/[action]")] + [PluginCoreAdminAuthorize] + [ApiController] + public class PluginsController : ControllerBase + { + #region Fields + private readonly IPluginManager _pluginManager; + private readonly IPluginFinder _pluginFinder; + private readonly IPluginApplicationBuilderManager _pluginApplicationBuilderManager; + #endregion + + #region Ctor + public PluginsController(IPluginManager pluginManager, IPluginFinder pluginFinder, IPluginApplicationBuilderManager pluginApplicationBuilderManager) + { + _pluginManager = pluginManager; + _pluginFinder = pluginFinder; + _pluginApplicationBuilderManager = pluginApplicationBuilderManager; + } + #endregion + + #region Actions + + #region 插件列表 + /// + /// 加载插件列表 + /// + /// 插件状态 + /// + [HttpGet, HttpPost] + public async Task> List(string status = "all") + { + BaseResponseModel responseData = new ResponseModel.BaseResponseModel(); + var pluginConfigModel = PluginConfigModelFactory.Create(); + + // 获取所有插件信息 + IList pluginInfoModels = PluginInfoModelFactory.CreateAll(); + IList responseModels = new List(); + string[] enablePluginIds = _pluginFinder.EnablePluginIds().ToArray(); + + // 添加插件状态 + responseModels = PluginInfoModelToResponseModel(pluginInfoModels, pluginConfigModel, enablePluginIds); + #region 筛选插件状态 + switch (status.ToLower()) + { + case "all": + break; + case "enabled": + responseModels = responseModels.Where(m => m.Status == PluginStatus.Enabled).ToList(); + break; + case "disabled": + responseModels = responseModels.Where(m => m.Status == PluginStatus.Disabled).ToList(); + break; + default: + break; + } + #endregion + + responseData.Code = 1; + responseData.Message = "加载插件列表成功"; + responseData.Data = responseModels; + + return await Task.FromResult(responseData); + } + #endregion + + #region 卸载插件 + [HttpGet, HttpPost] + public async Task> Uninstall(string pluginId) + { + BaseResponseModel responseData = new BaseResponseModel(); + pluginId = pluginId.Trim(); + var pluginConfigModel = PluginConfigModelFactory.Create(); + // 卸载插件 必须 先禁用插件 + #region 效验 + if (pluginConfigModel.EnabledPlugins.Contains(pluginId)) + { + responseData.Code = -1; + responseData.Message = "卸载失败: 请先禁用此插件"; + return await Task.FromResult(responseData); + } + string pluginDirStr= Path.Combine(PluginPathProvider.PluginsRootPath(), pluginId); + string pluginWwwrootDirStr = Path.Combine(PluginPathProvider.PluginsWwwRootDir(), pluginId); + if (!Directory.Exists(pluginDirStr) && !Directory.Exists(pluginWwwrootDirStr)) + { + responseData.Code = -2; + responseData.Message = "卸载失败: 此插件不存在"; + return await Task.FromResult(responseData); + } + #endregion + + try + { + // PS:卸载插件必须先禁用插件,所以此时插件LoadContext已被移除释放(插件Assemblies已被释放), 此处不需移除LoadContext + + // 1.删除物理文件 + var pluginDir = new DirectoryInfo(pluginDirStr); + if (pluginDir.Exists) { + pluginDir.Delete(true); + } + // 虽然 已禁用 时 pluginWwwrootDirStr/pluginId 已删除, 但为确保, 还是再删除一次 + var pluginWwwrootDir = new DirectoryInfo(pluginWwwrootDirStr); + if (pluginWwwrootDir.Exists) { + pluginWwwrootDir.Delete(true); + } + + responseData.Code = 1; + responseData.Message = "卸载成功"; + } + catch (Exception ex) + { + responseData.Code = -1; + responseData.Message = "卸载失败: " + ex.Message; + } + + return await Task.FromResult(responseData); + } + #endregion + + #region 启用插件 + [HttpGet, HttpPost] + public async Task> Enable(string pluginId) + { + BaseResponseModel responseData = new BaseResponseModel(); + var pluginConfigModel = PluginConfigModelFactory.Create(); + // 效验是否存在于 已禁用插件列表 + #region 效验 + pluginId = pluginId.Trim(); + var pluginDir = new DirectoryInfo(Path.Combine(PluginPathProvider.PluginsRootPath(), pluginId)); + if (pluginDir != null && !pluginDir.Exists) + { + responseData.Code = -1; + responseData.Message = "启用失败: 此插件不存在"; + return await Task.FromResult(responseData); + } + string[] enablePluginIds = _pluginFinder.EnablePluginIds().ToArray(); + if (enablePluginIds.Contains(pluginId)) { + responseData.Code = -2; + responseData.Message = "启用失败: 此插件已启用"; + return await Task.FromResult(responseData); + } + #endregion + + try + { + // 1. 创建插件程序集加载上下文, 添加到 PluginsLoadContexts + _pluginManager.LoadPlugin(pluginId); + // 2. 添加到 pluginConfigModel.EnabledPlugins + pluginConfigModel.EnabledPlugins.Add(pluginId); + // 4.保存到 plugin.config.json + PluginConfigModelFactory.Save(pluginConfigModel); + + // 5. 找到此插件实例 + IPlugin plugin = _pluginFinder.Plugin(pluginId); + if (plugin == null) + { + // 7.启用不成功, 回滚插件状态: (1)释放插件上下文 (2)更新 plugin.config.json + try + { + _pluginManager.UnloadPlugin(pluginId); + } + catch (Exception ex) + { } + + // 从 pluginConfigModel.EnabledPlugins 移除 + pluginConfigModel.EnabledPlugins.Remove(pluginId); + // 保存到 plugin.config.json + PluginConfigModelFactory.Save(pluginConfigModel); + + responseData.Code = -1; + responseData.Message = "启用失败: 此插件不存在"; + return await Task.FromResult(responseData); + } + // 6.调取插件的 AfterEnable(), 插件开发者可在此回收资源 + var pluginEnableResult = plugin.AfterEnable(); + if (!pluginEnableResult.IsSuccess) + { + // 7.启用不成功, 回滚插件状态: (1)释放插件上下文 (2)更新 plugin.config.json + try + { + _pluginManager.UnloadPlugin(pluginId); + } + catch (Exception ex) + { } + + // 从 pluginConfigModel.EnabledPlugins 移除 + pluginConfigModel.EnabledPlugins.Remove(pluginId); + // 保存到 plugin.config.json + PluginConfigModelFactory.Save(pluginConfigModel); + + responseData.Code = -1; + responseData.Message = "启用失败: 来自插件的错误信息: " + pluginEnableResult.Message; + return await Task.FromResult(responseData); + } + + // 7. ReBuild + this._pluginApplicationBuilderManager.ReBuild(); + + // 8. 尝试复制 插件下的 wwwroot 到 Plugins_wwwroot + string wwwRootDir = PluginPathProvider.WwwRootDir(pluginId); + if (Directory.Exists(wwwRootDir)) + { + string targetDir = PluginPathProvider.PluginWwwRootDir(pluginId); + Utils.FileUtil.CopyFolder(wwwRootDir, targetDir); + } + + responseData.Code = 1; + // responseData.Message = "启用成功"; + responseData.Message = pluginEnableResult.Message; + } + catch (Exception ex) + { + responseData.Code = -2; + responseData.Message = "启用失败: " + ex.Message; + Utils.LogUtil.Error(ex, ex.Message); + } + + return await Task.FromResult(responseData); + } + #endregion + + #region 禁用插件 + [HttpGet, HttpPost] + public async Task> Disable(string pluginId) + { + BaseResponseModel responseData = new BaseResponseModel(); + #region 效验 + pluginId = pluginId.Trim(); + var pluginConfigModel = PluginConfigModelFactory.Create(); + // string[] enablePluginIds = _pluginFinder.EnablePluginIds().ToArray(); + // // 效验是否存在于 已启用插件列表 + // if (!enablePluginIds.Contains(pluginId)) + // { + // responseData.Code = -1; + // responseData.Message = "禁用失败: 此插件不存在, 或未启用"; + // return await Task.FromResult(responseData); + // } + #endregion + + try + { + // 1. 找到此插件实例 + IPlugin plugin = _pluginFinder.Plugin(pluginId); + if (plugin == null) + { + responseData.Code = -1; + responseData.Message = "禁用失败: 此插件不存在, 或未启用"; + return await Task.FromResult(responseData); + } + string pluginDisableResultMessage = ""; + try + { + // 2.调取插件的 BeforeDisable(), 插件开发者可在此回收资源 + var pluginDisableResult = plugin.BeforeDisable(); + pluginDisableResultMessage = pluginDisableResult.Message; + if (!pluginDisableResult.IsSuccess) + { + responseData.Code = -1; + responseData.Message = "禁用失败: 来自插件的错误信息: " + pluginDisableResult.Message; + return await Task.FromResult(responseData); + } + // 3.移除插件对应的程序集加载上下文 + _pluginManager.UnloadPlugin(pluginId); + // 3.1. ReBuild + this._pluginApplicationBuilderManager.ReBuild(); + if (pluginConfigModel.EnabledPlugins.Contains(pluginId)) { + // 4.从 pluginConfigModel.EnabledPlugins 移除 + pluginConfigModel.EnabledPlugins.Remove(pluginId); + // 5.保存到 plugin.config.json + PluginConfigModelFactory.Save(pluginConfigModel); + } + } + catch (Exception ex) + { + Utils.LogUtil.Error(ex, ex.Message); + responseData.Code = -1; + responseData.Message = "禁用失败: 此插件不存在, 或未启用"; + return await Task.FromResult(responseData); + } + + // 7. 尝试移除 Plugins_wwwroot/PluginId + string pluginWwwRootDir = PluginPathProvider.PluginWwwRootDir(pluginId); + if (Directory.Exists(pluginWwwRootDir)) + { + Directory.Delete(pluginWwwRootDir, true); + } + + + responseData.Code = 1; + // responseData.Message = "禁用成功"; + responseData.Message = pluginDisableResultMessage; + } + catch (Exception ex) + { + responseData.Code = -2; + responseData.Message = "禁用失败: " + ex.Message; + Utils.LogUtil.Error(ex, ex.Message); + } + + return await Task.FromResult(responseData); + } + #endregion + + #region 上传插件 + /// + /// 上传插件 + /// + /// 注意: 参数名一定为 file, 对应前端传过来时以 file 为名 + /// + [HttpGet, HttpPost] + public async Task> Upload(IFormFile file) + { + BaseResponseModel responseData = new BaseResponseModel(); + + #region 效验 + if (file == null) + { + responseData.Code = -1; + responseData.Message = "上传的文件不能为空"; + return responseData; + } + //文件后缀 + string fileExtension = Path.GetExtension(file.FileName);//获取文件格式,拓展名 + // 类型标记 + UploadFileType uploadFileType = UploadFileType.NoAllowedType; + switch (fileExtension) + { + case ".zip": + uploadFileType = UploadFileType.Zip; + break; + case ".nupkg": + uploadFileType = UploadFileType.Nupkg; + break; + } + + if (fileExtension != ".zip" && fileExtension != ".nupkg") + { + responseData.Code = -1; + // nupkg 其实就是 zip + responseData.Message = "只能上传 zip 或 nupkg 格式文件"; + return responseData; + } + // PluginCore.AspNetCore-v1.0.2 起 不再限制插件上传大小 + //判断文件大小 + //var fileSize = file.Length; + //if (fileSize > 1024 * 1024 * 5) // 5M + //{ + // responseData.Code = -1; + // responseData.Message = "上传的文件不能大于5MB"; + // return responseData; + //} + #endregion + + try + { + // 1.先上传到 临时插件上传目录, 用Guid.zip作为保存文件名 + string tempZipFilePath = Path.Combine(PluginPathProvider.TempPluginUploadDir(), Guid.NewGuid() + ".zip"); + using (var fs = System.IO.File.Create(tempZipFilePath)) + { + file.CopyTo(fs); //将上传的文件文件流,复制到fs中 + fs.Flush();//清空文件流 + } + // 2.解压 + bool isDecomparessSuccess = false; + if (uploadFileType == UploadFileType.Zip) + { + isDecomparessSuccess = Utils.ZipHelper.DecomparessFile(tempZipFilePath, tempZipFilePath.Replace(".zip", "")); + } + else if (uploadFileType == UploadFileType.Nupkg) + { + isDecomparessSuccess = NupkgService.DecomparessFile(tempZipFilePath, tempZipFilePath.Replace(".zip", "")); + } + + // 3.删除原压缩包 + System.IO.File.Delete(tempZipFilePath); + if (!isDecomparessSuccess) + { + responseData.Code = -1; + responseData.Message = "解压插件压缩包失败"; + return responseData; + } + // 4.读取其中的info.json, 获取 PluginId 值 + PluginInfoModel pluginInfoModel = PluginInfoModelFactory.ReadPluginDir(tempZipFilePath.Replace(".zip", "")); + if (pluginInfoModel == null || string.IsNullOrEmpty(pluginInfoModel.PluginId)) + { + // 记得删除已不再需要的临时插件文件夹 + Directory.Delete(tempZipFilePath.Replace(".zip", ""), true); + + responseData.Code = -1; + responseData.Message = "不合法的插件"; + return responseData; + } + string pluginId = pluginInfoModel.PluginId; + // 5.检索 此 PluginId 是否本地插件已存在 + var pluginConfigModel = PluginConfigModelFactory.Create(); + // 本地已经存在的 PluginId + IList localExistPluginIds = PluginPathProvider.AllPluginFolderName(); + if (localExistPluginIds.Contains(pluginId)) + { + // 记得删除已不再需要的临时插件文件夹 + Directory.Delete(tempZipFilePath.Replace(".zip", ""), true); + + responseData.Code = -1; + responseData.Message = $"本地已有此插件 (PluginId: {pluginId}), 请前往插件列表删除后, 再上传"; + return responseData; + } + // 6.本地无此插件 -> 移动插件文件夹到 Plugins 下, 并以 PluginId 为插件文件夹名 + string pluginsRootPath = PluginPathProvider.PluginsRootPath(); + string newPluginDir = Path.Combine(pluginsRootPath, pluginId); + Directory.Move(tempZipFilePath.Replace(".zip", ""), newPluginDir); + + // 7. 放入 Plugins 中, 默认为 已禁用 + + responseData.Code = 1; + responseData.Message = $"上传插件成功 (PluginId: {pluginId})"; + } + catch (Exception ex) + { + responseData.Code = -1; + responseData.Message = "上传插件失败: " + ex.Message; + ex = ex.InnerException; + while (ex != null) + { + responseData.Message += " - " + ex.InnerException.Message; + ex = ex.InnerException; + } + Utils.LogUtil.Error(ex, ex.Message); + } + + return await Task.FromResult(responseData); + } + #endregion + + #region 查看详细 + [HttpGet, HttpPost] + public async Task> Details(string pluginId) + { + BaseResponseModel responseData = new BaseResponseModel(); + + try + { + #region 效验 + pluginId = pluginId.Trim(); + var pluginConfigModel = PluginConfigModelFactory.Create(); + string[] localPluginIds = PluginPathProvider.AllPluginFolderName().ToArray(); + + if (!localPluginIds.Contains(pluginId)) + { + responseData.Code = -1; + responseData.Message = $"查看详细失败: 不存在 {pluginId} 插件"; + return await Task.FromResult(responseData); + } + + #endregion + + PluginInfoModel pluginInfoModel = PluginInfoModelFactory.Create(pluginId); + string[] enablePluginIds = _pluginFinder.EnablePluginIds().ToArray(); + PluginInfoResponseModel pluginInfoResponseModel = PluginInfoModelToResponseModel(new List() { pluginInfoModel }, pluginConfigModel, enablePluginIds).FirstOrDefault(); + + + responseData.Code = 1; + responseData.Message = "查看详细成功"; + responseData.Data = pluginInfoResponseModel; + } + catch (Exception ex) + { + responseData.Code = -1; + responseData.Message = "查看详细失败: " + ex.Message; + } + + return await Task.FromResult(responseData); + } + #endregion + + #region 查看文档 + [HttpGet, HttpPost] + public async Task> Readme(string pluginId) + { + BaseResponseModel responseData = new BaseResponseModel(); + + try + { + #region 效验 + pluginId = pluginId.Trim(); + // var pluginConfigModel = PluginConfigModelFactory.Create(); + string[] localPluginIds = PluginPathProvider.AllPluginFolderName().ToArray(); + + if (!localPluginIds.Contains(pluginId)) + { + responseData.Code = -1; + responseData.Message = $"查看文档失败: 不存在 {pluginId} 插件"; + return await Task.FromResult(responseData); + } + + #endregion + + PluginReadmeModel readmeModel = PluginReadmeModelFactory.Create(pluginId); + PluginReadmeResponseModel readmeResponseModel = new PluginReadmeResponseModel(); + readmeResponseModel.Content = readmeModel?.Content ?? ""; + readmeResponseModel.PluginId = pluginId; + + responseData.Code = 1; + responseData.Message = "查看文档成功"; + responseData.Data = readmeResponseModel; + } + catch (Exception ex) + { + responseData.Code = -1; + responseData.Message = "查看文档失败: " + ex.Message; + } + + return await Task.FromResult(responseData); + } + #endregion + + #region 设置 + [HttpGet] + public async Task> Settings(string pluginId) + { + BaseResponseModel responseData = new BaseResponseModel(); + + try + { + #region 效验 + pluginId = pluginId.Trim(); + // var pluginConfigModel = PluginConfigModelFactory.Create(); + string[] localPluginIds = PluginPathProvider.AllPluginFolderName().ToArray(); + + if (!localPluginIds.Contains(pluginId)) + { + responseData.Code = -1; + responseData.Message = $"查看设置失败: 不存在 {pluginId} 插件"; + return await Task.FromResult(responseData); + } + + #endregion + + string settingsJsonStr = PluginSettingsModelFactory.Create(pluginId); + + + responseData.Code = 1; + responseData.Message = "查看设置成功"; + responseData.Data = settingsJsonStr ?? "无设置项"; + } + catch (Exception ex) + { + responseData.Code = -1; + responseData.Message = "查看设置失败: " + ex.Message; + } + + return await Task.FromResult(responseData); + } + + [HttpPost] + public async Task> Settings(PluginSettingsInputModel inputModel) + { + BaseResponseModel responseData = new BaseResponseModel(); + + try + { + #region 效验 + inputModel.PluginId = inputModel.PluginId.Trim(); + // var pluginConfigModel = PluginConfigModelFactory.Create(); + string[] localPluginIds = PluginPathProvider.AllPluginFolderName().ToArray(); + + if (!localPluginIds.Contains(inputModel.PluginId)) + { + responseData.Code = -1; + responseData.Message = $"设置失败: 不存在 {inputModel.PluginId} 插件"; + return await Task.FromResult(responseData); + } + + #endregion + + inputModel.Data = inputModel.Data ?? ""; + PluginSettingsModelFactory.Save(pluginSettingsJsonStr: inputModel.Data, pluginId: inputModel.PluginId); + + + responseData.Code = 1; + responseData.Message = "设置成功"; + responseData.Data = inputModel.Data; + } + catch (Exception ex) + { + responseData.Code = -1; + responseData.Message = "设置失败: " + ex.Message; + } + + return await Task.FromResult(responseData); + } + #endregion + + #endregion + + + #region Helpers + + [NonAction] + private IList PluginInfoModelToResponseModel(IList pluginInfoModels, PluginConfigModel pluginConfigModel, string[] enablePluginIds) + { + // 获取 Plugins 下所有插件 + // DirectoryInfo pluginsDir = new DirectoryInfo(PluginPathProvider.PluginsRootPath()); + // List pluginIds = pluginsDir?.GetDirectories()?.Select(m => m.Name)?.ToList() ?? new List(); + + IList responseModels = new List(); + #region 添加插件状态信息 + foreach (var model in pluginInfoModels) + { + PluginInfoResponseModel responseModel = new PluginInfoResponseModel(); + responseModel.Author = model.Author; + responseModel.Description = model.Description; + responseModel.DisplayName = model.DisplayName; + responseModel.PluginId = model.PluginId; + responseModel.SupportedVersions = model.SupportedVersions; + responseModel.Version = model.Version; + responseModel.DependPlugins = model.DependPlugins; + + if (pluginConfigModel.EnabledPlugins.Contains(model.PluginId) && !enablePluginIds.Contains(model.PluginId)) { + // 错误情况: 配置 标识 已启用, 但实际没有启用成功 + pluginConfigModel.EnabledPlugins.Remove(model.PluginId); + PluginConfigModelFactory.Save(pluginConfigModel); + + responseModel.Status = PluginStatus.Disabled; + } else if(!pluginConfigModel.EnabledPlugins.Contains(model.PluginId) && enablePluginIds.Contains(model.PluginId)) + { + // 错误情况: 配置没有标识 已启用, 但实际 已启用 + pluginConfigModel.EnabledPlugins.Add(model.PluginId); + PluginConfigModelFactory.Save(pluginConfigModel); + + responseModel.Status = PluginStatus.Enabled; + } else if (pluginConfigModel.EnabledPlugins.Contains(model.PluginId) && enablePluginIds.Contains(model.PluginId)) + { + responseModel.Status = PluginStatus.Enabled; + } + else + { + responseModel.Status = PluginStatus.Disabled; + } + responseModels.Add(responseModel); + } + #endregion + + return responseModels; + } + + public enum UploadFileType + { + NoAllowedType = 0, + Zip = 1, + Nupkg = 2 + } + + #endregion + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Controllers忽略/UserController.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Controllers忽略/UserController.cs new file mode 100644 index 00000000..fa6193ed --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Controllers忽略/UserController.cs @@ -0,0 +1,143 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using PluginCore.AspNetCore.Authorization; +using PluginCore.Config; +using PluginCore.AspNetCore.RequestModel.User; +using PluginCore.AspNetCore.ResponseModel; + +namespace PluginCore.AspNetCore.Controllers +{ + [Route("api/plugincore/admin/[controller]/[action]")] + [ApiController] + public class UserController : ControllerBase + { + private readonly AccountManager _accountManager; + + public string RemoteFronted + { + get + { + return PluginCore.Config.PluginCoreConfigFactory.Create().RemoteFrontend; + } + } + + public UserController(AccountManager accountManager) + { + _accountManager = accountManager; + } + + [HttpGet, HttpPost] + public async Task> Login([FromBody] LoginRequestModel requestModel) + { + BaseResponseModel responseModel = new BaseResponseModel(); + + try + { + string token = AccountManager.CreateToken(requestModel.UserName, requestModel.Password); + bool isAdmin = AccountManager.IsAdminToken(token); + if (!isAdmin) + { + responseModel.Code = -1; + responseModel.Message = "用户名或密码不正确"; + + return await Task.FromResult(responseModel); + } + + responseModel.Code = 1; + responseModel.Message = "登录成功"; + responseModel.Data = new + { + token = token, + userName = requestModel.UserName + }; + } + catch (Exception ex) + { + responseModel.Code = -1; + responseModel.Message = "失败: " + ex.Message; + } + + return await Task.FromResult(responseModel); + } + + [HttpGet, HttpPost] + public async Task> Logout() + { + BaseResponseModel responseModel = new BaseResponseModel() + { + Code = 1, + Message = "退出登录成功" + }; + + return await Task.FromResult(responseModel); + } + + [PluginCoreAdminAuthorize] + [HttpGet, HttpPost] + public async Task> Info() + { + BaseResponseModel responseModel = new BaseResponseModel(); + + try + { + string adminUserName = PluginCoreConfigFactory.Create().Admin.UserName; + + responseModel.Code = 1; + responseModel.Message = "成功"; + responseModel.Data = new + { + name = adminUserName, + //avatar = this.RemoteFronted + "/images/avatar.gif" + avatar = "" + }; + } + catch (Exception ex) + { + responseModel.Code = -1; + responseModel.Message = "失败: " + ex.Message; + } + + return await Task.FromResult(responseModel); + } + + [PluginCoreAdminAuthorize] + [HttpGet, HttpPost] + public async Task> Update([FromBody] UpdateRequestModel requestModel) + { + BaseResponseModel responseModel = new BaseResponseModel(); + + try + { + PluginCoreConfig pluginCoreConfig = PluginCoreConfigFactory.Create(); + pluginCoreConfig.Admin.UserName = requestModel.UserName; + pluginCoreConfig.Admin.Password = requestModel.Password; + PluginCoreConfigFactory.Save(pluginCoreConfig); + + responseModel.Code = 1; + responseModel.Message = "修改成功, 需要重新登录"; + } + catch (Exception ex) + { + responseModel.Code = -1; + responseModel.Message = "失败: " + ex.Message; + } + + return await Task.FromResult(responseModel); + } + + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Extensions/IServiceProviderExtensions.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Extensions/IServiceProviderExtensions.cs new file mode 100644 index 00000000..e75d13c8 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Extensions/IServiceProviderExtensions.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Reflection; +using System.Text; +using Microsoft.Extensions.DependencyInjection; + +namespace PluginCore.AspNetCore.Extensions +{ + public static class IServiceProviderExtensions + { + /// + /// Get all registered + /// + /// + /// + public static Dictionary GetAllServiceDescriptors(this IServiceProvider provider) + { + var serviceProvider = provider.GetPropertyValue("RootProvider"); + var result = new Dictionary(); + + var engine = serviceProvider.GetFieldValue("_engine"); + // var callSiteFactory = engine.GetPropertyValue("CallSiteFactory"); + var callSiteFactory = serviceProvider.GetPropertyValue("CallSiteFactory"); + var descriptorLookup = callSiteFactory.GetFieldValue("_descriptorLookup"); + if (descriptorLookup is IDictionary dictionary) + { + foreach (DictionaryEntry entry in dictionary) + { + result.Add((Type)entry.Key, (ServiceDescriptor)entry.Value.GetPropertyValue("Last")); + } + } + + return result; + + #region Old + //if (provider is ServiceProvider serviceProvider) + //{ + // var result = new Dictionary(); + + // var engine = serviceProvider.GetFieldValue("_engine"); + // var callSiteFactory = engine.GetPropertyValue("CallSiteFactory"); + // var descriptorLookup = callSiteFactory.GetFieldValue("_descriptorLookup"); + // if (descriptorLookup is IDictionary dictionary) + // { + // foreach (DictionaryEntry entry in dictionary) + // { + // result.Add((Type)entry.Key, (ServiceDescriptor)entry.Value.GetPropertyValue("Last")); + // } + // } + + // return result; + //} + + //throw new NotSupportedException($"Type '{provider.GetType()}' is not supported!"); + #endregion + } + } + + public static class ReflectionHelper + { + // ########################################################################################## + // Get / Set Field + // ########################################################################################## + + #region Get / Set Field + + public static object GetFieldValue(this object obj, string fieldName) + { + if (obj == null) + throw new ArgumentNullException(nameof(obj)); + Type objType = obj.GetType(); + var fieldInfo = GetFieldInfo(objType, fieldName); + if (fieldInfo == null) + throw new ArgumentOutOfRangeException(fieldName, + $"Couldn't find field {fieldName} in type {objType.FullName}"); + return fieldInfo.GetValue(obj); + } + + public static void SetFieldValue(this object obj, string fieldName, object val) + { + if (obj == null) + throw new ArgumentNullException(nameof(obj)); + Type objType = obj.GetType(); + var fieldInfo = GetFieldInfo(objType, fieldName); + if (fieldInfo == null) + throw new ArgumentOutOfRangeException(fieldName, + $"Couldn't find field {fieldName} in type {objType.FullName}"); + fieldInfo.SetValue(obj, val); + } + + private static FieldInfo GetFieldInfo(Type type, string fieldName) + { + FieldInfo fieldInfo = null; + do + { + fieldInfo = type.GetField(fieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + type = type.BaseType; + } while (fieldInfo == null && type != null); + + return fieldInfo; + } + + #endregion + + // ########################################################################################## + // Get / Set Property + // ########################################################################################## + + #region Get / Set Property + + public static object GetPropertyValue(this object obj, string propertyName) + { + if (obj == null) + throw new ArgumentNullException(nameof(obj)); + Type objType = obj.GetType(); + var propertyInfo = GetPropertyInfo(objType, propertyName); + if (propertyInfo == null) + throw new ArgumentOutOfRangeException(propertyName, + $"Couldn't find property {propertyName} in type {objType.FullName}"); + return propertyInfo.GetValue(obj, null); + } + + public static void SetPropertyValue(this object obj, string propertyName, object val) + { + if (obj == null) + throw new ArgumentNullException(nameof(obj)); + Type objType = obj.GetType(); + var propertyInfo = GetPropertyInfo(objType, propertyName); + if (propertyInfo == null) + throw new ArgumentOutOfRangeException(propertyName, + $"Couldn't find property {propertyName} in type {objType.FullName}"); + propertyInfo.SetValue(obj, val, null); + } + + private static PropertyInfo GetPropertyInfo(Type type, string propertyName) + { + PropertyInfo propertyInfo = null; + do + { + propertyInfo = type.GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + type = type.BaseType; + } while (propertyInfo == null && type != null); + + return propertyInfo; + } + + #endregion + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Extensions/PluginCoreStartupExtensions.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Extensions/PluginCoreStartupExtensions.cs new file mode 100644 index 00000000..32549a3b --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Extensions/PluginCoreStartupExtensions.cs @@ -0,0 +1,416 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using PluginCore.Models; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.FileProviders; +using PluginCore.AspNetCore.Authorization; +using PluginCore.AspNetCore.AdminUI; +using PluginCore.Infrastructure; +using PluginCore.Interfaces; +using PluginCore.AspNetCore.Middlewares; +using PluginCore.AspNetCore.BackgroundServices; +using PluginCore.IPlugins; +using PluginCore.AspNetCore.Interfaces; +using PluginCore.lmplements; +using PluginCore.AspNetCore.lmplements; + +namespace PluginCore.AspNetCore.Extensions +{ + /// + /// + /// + public static class PluginCoreStartupExtensions + { + private static IWebHostEnvironment _webHostEnvironment; + + private static IServiceCollection _services; + + private static IServiceProvider _serviceProvider; + + /// + /// 若需要替换默认实现, 请在 之前, 若在之后, 则无法影响 主程序启动时默认行为 + /// + /// + public static void AddPluginCore(this IServiceCollection services) + { + services.AddPluginCoreServices(); + + services.AddPluginCoreLog(); + + services.AddPluginCorePlugins(); + + #region PluginCore Admin 权限 + // 1. 先 Authentication (我是谁) 2. 再 Authorization (我能做什么) + + // Authentication + //注销内置认证 by tomny + // services.AddPluginCoreAuthentication(); + //注销内置授权 by tomny + // Authorization + //services.AddPluginCoreAuthorization(); + #endregion + + services.AddPluginCoreStartupPlugin(); + + // AddBackgroundServices + services.AddBackgroundServices(); + + // 一定要在最后 + _services = services; + _serviceProvider = services.BuildServiceProvider(); + } + + public static IApplicationBuilder UsePluginCore(this IApplicationBuilder app) + { + // 一定在 PluginCore 添加的中间件中 第一个 + app.UseMiddleware(); + + app.UsePluginCoreLanguageMiddleware(); + //注销内置管理页面 by tomny + // app.UsePluginCoreAdminUI(); + + app.UsePluginCoreStaticFiles(); + + // 发现 UseAuthentication 认证中间件重复添加, 也只会执行一次 认证 + // 但 UseAuthorization 重复添加2次, 则会执行 2次 授权 + //注销内置认证授权 by tomny + //app.UseAuthentication(); + //app.UseAuthorization(); + + #region Plugin Middleware + // Plugin Middleware + //app.UseMiddleware(); + + + // 一定在 PluginCore 添加的中间件中 最后一个 + app.UseMiddleware(); + #endregion + + app.UsePluginCoreStartupPlugin(); + + app.UsePluginCoreAppStart(); + + // Log + app.UsePluginCoreLog(); + + return app; + } + + + public static void AddPluginCoreServices(this IServiceCollection services) + { + #region 注册服务 + + #region 仅适用于 ASP.NET Core + // start: 仅用于 ASP.NET Core + // 用于添加插件Controller 时,通知Controller.Action发生变化 + services.AddSingleton(PluginActionDescriptorChangeProvider.Instance); + services.AddSingleton(PluginActionDescriptorChangeProvider.Instance); + + services.TryAddTransient(); + services.TryAddTransient(); + + services.TryAddTransient(); + services.TryAddTransient(); + // end: 仅用于 ASP.NET Core + #endregion + + #region 通用 + + // v1 旧版 + //services.TryAddTransient(); + //services.TryAddTransient(); + //services.TryAddTransient(); + //services.TryAddTransient(); + + services.TryAddTransient(); + services.TryAddTransient(); + //注销内置插件管理 by tomny + services.TryAddTransient(); + services.TryAddTransient(); + + services.TryAddTransient(); + services.TryAddTransient(); + services.TryAddTransient(); + services.TryAddTransient(); + + // 注意: 它必须单例 + // TODO: 不知道原因, 单例失败, 每次 获取 IPluginFinder 都会获取新的 IPluginContextManager + services.TryAddSingleton(); + services.TryAddSingleton(); + #endregion + + #endregion + + // ************************* 注意: IServiceCollection 是服务列表, 但由 IServiceProvider 来负责解析, AClass 单例 仅在 AServiceProvider 范围内 + _serviceProvider = services.BuildServiceProvider(); + } + + public static void AddPluginCorePlugins(this IServiceCollection services) + { + #region ASP.NET Core + //IPluginManager pluginManager = services.BuildServiceProvider().GetService(); + IPluginManager pluginManager = _serviceProvider.GetService(); + + // 初始化 PluginCore 相关目录 + PluginPathProvider.PluginsRootPath(); + + // 在程序启动时加载所有 已安装并启用 的插件 + + // 获取 PluginConfigModel + #region 获取 PluginConfigModel + PluginConfigModel pluginConfigModel = PluginConfigModelFactory.Create(); + #endregion + + // 已启用的插件 + #region 加载 已启用插件的Assemblies + IList enabledPluginIds = pluginConfigModel.EnabledPlugins; + foreach (var pluginId in enabledPluginIds) + { + pluginManager.LoadPlugin(pluginId); + } + #endregion + + //注销内置Controller的注册 by tomny + // 将本 Assembly 内的 Controller 添加 + // var ass = Assembly.GetExecutingAssembly(); + //IPluginControllerManager pluginControllerManager = services.BuildServiceProvider().GetService(); + // IPluginControllerManager pluginControllerManager = _serviceProvider.GetService(); + //pluginControllerManager.AddControllers(ass); + + + // IWebHostEnvironment + _webHostEnvironment = services.BuildServiceProvider().GetService(); + #endregion + } + + public static void AddPluginCoreAuthentication(this IServiceCollection services) + { + // fixed: https://github.com/yiyungent/PluginCore/issues/4 + // System.InvalidOperationException: No authenticationScheme was specified, and there was no DefaultChallengeScheme found. The default schemes can be set using either AddAuthentication(string defaultScheme) or AddAuthentication(Action configureOptions). + #region 添加认证 Authentication + // 没通过 Authentication: 401 Unauthorized + // services.AddAuthentication("PluginCore.Authentication") + // .AddScheme("PluginCore.Authentication", "PluginCore.Authentication", + // options => { }); + // 注意: 不要设置 默认 认证名: Constants.AspNetCoreAuthenticationScheme + // services.AddAuthentication(Constants.AspNetCoreAuthenticationScheme) + // 默认认证名: 默认 + services.AddAuthentication() + // 添加一个新的认证方案 + .AddScheme( + authenticationScheme: Constants.AspNetCoreAuthenticationScheme, + displayName: Constants.AspNetCoreAuthenticationScheme, + options => { }); + #endregion + } + + public static void AddPluginCoreAuthorization(this IServiceCollection services) + { + #region 添加授权策略-所有标记 [PluginCoreAdminAuthorize] 都需要权限检查 + // Only Once, Not recommend + //services.AddSingleton(); + + services.AddAuthorization(options => + { + // options.AddPolicy("PluginCore.Admin", policy => + options.AddPolicy(name: Constants.AspNetCoreAuthorizationPolicyName, policy => + { + // 无法满足 下方任何一项:HTTP 403 错误 + // 3.需要 检查是否拥有当前请求资源的权限 + //policy.Requirements.Add(new PluginCoreAdminRequirement()); + policy.AuthenticationSchemes = new string[] { + Constants.AspNetCoreAuthenticationScheme + }; + policy.RequireAuthenticatedUser(); + policy.RequireClaim(claimType: Constants.AspNetCoreAuthenticationClaimType); + // 必须重启才能使得更改密码生效 + string token = AccountManager.CreateToken(); + policy.RequireClaim(claimType: Constants.AspNetCoreAuthenticationClaimType, allowedValues: new string[] { + token + }); + //policy.RequireAssertion(context => + //{ + // return true; + //}); + }); + }); + #endregion + + // AccountManager + services.AddTransient(); + // HttpContext.Current + services.TryAddSingleton(); + //services.AddHttpContextAccessor(); + } + + public static void AddPluginCoreStartupPlugin(this IServiceCollection services) + { + //IPluginFinder pluginFinder = services.BuildServiceProvider().GetService(); + IPluginFinder pluginFinder = _serviceProvider.GetService(); + + #region IStartupPlugin + + var plugins = pluginFinder.EnablePlugins()?.OrderBy(m => m.ConfigureServicesOrder)?.ToList(); + + foreach (var item in plugins) + { + // 调用 + Utils.LogUtil.Info( + categoryName: $"{nameof(PluginCoreStartupExtensions)}.{nameof(AddPluginCoreStartupPlugin)}", + message: $"{item.GetType().ToString()}: {nameof(IStartupPlugin)}.{nameof(IStartupPlugin.ConfigureServices)}" + ); + + item?.ConfigureServices(services); + } + + #endregion + } + + public static void AddPluginCoreLog(this IServiceCollection services) + { + #region Logger + + IServiceScopeFactory serviceScopeFactory = _serviceProvider.GetService(); + Utils.LogUtil.Initialize(serviceScopeFactory); + + #endregion + } + + public static IApplicationBuilder UsePluginCoreStaticFiles(this IApplicationBuilder app) + { + // TODO: 其实由于目前已实现运行时动态新增/删除 HTTP Middleware, 其实可以不用再像下方这么复制插件 wwwroot 目录到 Plugins_wwwroot/{PluginId} + // 而是在运行时配置, 直接指向 `Plugins/{PluginId}/wwwroot`, 而无需复制/删除 + + // 注意:`UseDefaultFiles`必须在`UseStaticFiles`之前进行调用。因为`DefaultFilesMiddleware`仅仅负责重写Url,实际上默认页文件,仍然是通过`StaticFilesMiddleware`来提供的。 + + string pluginwwwrootDir = PluginPathProvider.PluginsWwwRootDir(); + + #region 插件 wwwroot 默认页 + // 设置目录的默认页 + var defaultFilesOptions = new DefaultFilesOptions(); + defaultFilesOptions.DefaultFileNames.Clear(); + // 指定默认页名称 + defaultFilesOptions.DefaultFileNames.Add("index.html"); + // 指定请求路径 + defaultFilesOptions.RequestPath = "/Plugins"; + // 指定默认页所在的目录 + defaultFilesOptions.FileProvider = new PhysicalFileProvider(pluginwwwrootDir); + app.UseDefaultFiles(defaultFilesOptions); + #endregion + + #region 插件 wwwroot + // 由于没办法在运行时, 动态 UseStaticFiles(), 因此不再为每一个插件都 UseStaticFiles(), + // 而是统一在一个文件夹下, 插件启用时, 将插件的wwwroot复制到 Plugins_wwwroot/{PluginId}, 禁用时, 再删除 + app.UseStaticFiles(new StaticFileOptions + { + FileProvider = new PhysicalFileProvider( + pluginwwwrootDir), + RequestPath = "/Plugins" + }); + #endregion + + return app; + } + + public static IApplicationBuilder UsePluginCoreAppStart(this IApplicationBuilder app) + { + //IPluginFinder pluginFinder = _services.BuildServiceProvider().GetService(); + IPluginFinder pluginFinder = _serviceProvider.GetService(); + + #region AppStart + + var plugins = pluginFinder.EnablePluginsFull()?.ToList(); + var dependencySorter = new PluginCore.Utils.DependencySorter(); + dependencySorter.AddObjects(plugins.Select(m => m.PluginInstance).ToArray()); + foreach (var item in plugins) + { + var dependPlugins = plugins.Where(m => item.PluginInstance.AppStartOrderDependPlugins.Contains(m.PluginId)).Select(m => m.PluginInstance).ToArray(); + dependencySorter.SetDependencies(obj: item.PluginInstance, dependsOnObjects: dependPlugins); + } + var sortedPlugins = dependencySorter.Sort(); + foreach (var item in sortedPlugins) + { + // 调用 + //Utils.LogUtil.PluginBehavior(item, typeof(IPlugin), nameof(IPlugin.AppStart)); + Utils.LogUtil.Info( + categoryName: $"{nameof(PluginCoreStartupExtensions)}.{nameof(UsePluginCoreAppStart)}", + message: $"{item.GetType().ToString()}: {nameof(IPlugin)}.{nameof(IPlugin.AppStart)}" + ); + + item?.AppStart(); + } + + #endregion + + return app; + } + + public static IApplicationBuilder UsePluginCoreStartupPlugin(this IApplicationBuilder app) + { + //IPluginFinder pluginFinder = _services.BuildServiceProvider().GetService(); + IPluginFinder pluginFinder = _serviceProvider.GetService(); + + #region IStartupPlugin + + var startupPlugins = pluginFinder.EnablePlugins()?.OrderBy(m => m.ConfigureOrder)?.ToList(); + + foreach (var item in startupPlugins) + { + // 调用 + //Utils.LogUtil.PluginBehavior(item, typeof(IStartupPlugin), nameof(IStartupPlugin.Configure)); + Utils.LogUtil.Info( + categoryName: $"{nameof(PluginCoreStartupExtensions)}.{nameof(UsePluginCoreStartupPlugin)}", + message: $"{item.GetType().ToString()}: {nameof(IStartupPlugin)}.{nameof(IStartupPlugin.Configure)}" + ); + + item?.Configure(app); + } + + #endregion + + + app.UseMiddleware(); + + return app; + } + + public static IApplicationBuilder UsePluginCoreLog(this IApplicationBuilder app) + { + #region 启动 Log + Config.PluginCoreConfig pluginCoreConfig = Config.PluginCoreConfigFactory.Create(); + + Utils.LogUtil.Info(categoryName: $"{nameof(PluginCoreStartupExtensions)}.{nameof(UsePluginCoreLog)}", $"{nameof(PluginCore.AspNetCore)}"); + Utils.LogUtil.Info(categoryName: $"{nameof(PluginCoreStartupExtensions)}.{nameof(UsePluginCoreLog)}", "Started successfully:"); + Utils.LogUtil.Info(categoryName: $"{nameof(PluginCoreStartupExtensions)}.{nameof(UsePluginCoreLog)}", $"Front-end mode: {pluginCoreConfig.FrontendMode}"); + Utils.LogUtil.Info(categoryName: $"{nameof(PluginCoreStartupExtensions)}.{nameof(UsePluginCoreLog)}", $"Notice: Updating the front-end mode requires restarting the site"); + #endregion + + return app; + } + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Infrastructure/PluginApplicationBuilder.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Infrastructure/PluginApplicationBuilder.cs new file mode 100644 index 00000000..01ea979d --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Infrastructure/PluginApplicationBuilder.cs @@ -0,0 +1,350 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Internal; + +namespace PluginCore.AspNetCore.Infrastructure +{ + /// + /// Default implementation for . + /// + /// https://github.com/dotnet/aspnetcore/blob/main/src/Http/Http/src/Builder/ApplicationBuilder.cs + /// + public class PluginApplicationBuilder : IApplicationBuilder + { + private const string ServerFeaturesKey = "server.Features"; + private const string ApplicationServicesKey = "application.Services"; + + private readonly List> _components = new List>(); + + private Action _reachEndAction; + + public Action ReachEndAction + { + get + { + return this._reachEndAction; + } + set + { + this._reachEndAction = value; + } + } + + public PluginApplicationBuilder() + { + + } + + /// + /// Initializes a new instance of . + /// + /// The for application services. + public PluginApplicationBuilder(IServiceProvider serviceProvider, Action reachEndAction) + { + Properties = new Dictionary(StringComparer.Ordinal); + ApplicationServices = serviceProvider; + + _reachEndAction = reachEndAction; + } + + /// + /// Initializes a new instance of . + /// + /// The for application services. + /// The server instance that hosts the application. + public PluginApplicationBuilder(IServiceProvider serviceProvider, Action reachEndAction, object server) + : this(serviceProvider, reachEndAction) + { + SetProperty(ServerFeaturesKey, server); + } + + private PluginApplicationBuilder(PluginApplicationBuilder builder) + { + Properties = new CopyOnWriteDictionary(builder.Properties, StringComparer.Ordinal); + } + + /// + /// Gets the for application services. + /// + public IServiceProvider ApplicationServices + { + get + { + //return GetProperty(ApplicationServicesKey)!; + return null; + } + set + { + SetProperty(ApplicationServicesKey, value); + } + } + + /// + /// Gets the for server features. + /// + public IFeatureCollection ServerFeatures + { + get + { + // ! (null 包容)运算符(C# 参考) + // https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/operators/null-forgiving + // TODO: 报错 + //var result = GetProperty(ServerFeaturesKey)!; + + IFeatureCollection rtn = null; + + return rtn; + } + } + + /// + /// Gets a set of properties for . + /// + public IDictionary Properties { get; } + + //private T? GetProperty(string key) + //{ + + // return Properties.TryGetValue(key, out var value) ? (T?)value : default(T); + //} + + private void SetProperty(string key, T value) + { + Properties[key] = value; + } + + /// + /// Adds the middleware to the application request pipeline. + /// + /// The middleware. + /// An instance of after the operation has completed. + public IApplicationBuilder Use(Func middleware) + { + _components.Add(middleware); + return this; + } + + /// + /// Creates a copy of this application builder. + /// + /// The created clone has the same properties as the current instance, but does not copy + /// the request pipeline. + /// + /// + /// The cloned instance. + public IApplicationBuilder New() + { + return new PluginApplicationBuilder(this); + } + + /// + /// Produces a that executes added middlewares. + /// + /// The . + public RequestDelegate Build() + { + RequestDelegate app = context => + { + #region Old + // If we reach the end of the pipeline, but we have an endpoint, then something unexpected has happened. + // This could happen if user code sets an endpoint, but they forgot to add the UseEndpoint middleware. + //var endpoint = context.GetEndpoint(); + //var endpointRequestDelegate = endpoint?.RequestDelegate; + //if (endpointRequestDelegate != null) + //{ + // var message = + // $"The request reached the end of the pipeline without executing the endpoint: '{endpoint!.DisplayName}'. " + + // $"Please register the EndpointMiddleware using '{nameof(IApplicationBuilder)}.UseEndpoints(...)' if using " + + // $"routing."; + // throw new InvalidOperationException(message); + //} + #endregion + + this._reachEndAction(); + + //context.Response.StatusCode = StatusCodes.Status404NotFound; + return Task.CompletedTask; + }; + + for (var c = _components.Count - 1; c >= 0; c--) + { + app = _components[c](app); + } + + return app; + } + } + + + + /// + /// https://github.com/dotnet/aspnetcore/blob/8b30d862de6c9146f466061d51aa3f1414ee2337/src/Shared/CopyOnWriteDictionary/CopyOnWriteDictionary.cs + /// + /// + /// + internal class CopyOnWriteDictionary : IDictionary where TKey : notnull + { + private readonly IDictionary _sourceDictionary; + private readonly IEqualityComparer _comparer; + private IDictionary? _innerDictionary; + + public CopyOnWriteDictionary( + IDictionary sourceDictionary, + IEqualityComparer comparer) + { + if (sourceDictionary == null) + { + throw new ArgumentNullException(nameof(sourceDictionary)); + } + + if (comparer == null) + { + throw new ArgumentNullException(nameof(comparer)); + } + + _sourceDictionary = sourceDictionary; + _comparer = comparer; + } + + private IDictionary ReadDictionary + { + get + { + return _innerDictionary ?? _sourceDictionary; + } + } + + private IDictionary WriteDictionary + { + get + { + if (_innerDictionary == null) + { + _innerDictionary = new Dictionary(_sourceDictionary, + _comparer); + } + + return _innerDictionary; + } + } + + public virtual ICollection Keys + { + get + { + return ReadDictionary.Keys; + } + } + + public virtual ICollection Values + { + get + { + return ReadDictionary.Values; + } + } + + public virtual int Count + { + get + { + return ReadDictionary.Count; + } + } + + public virtual bool IsReadOnly + { + get + { + return false; + } + } + + public virtual TValue this[TKey key] + { + get + { + return ReadDictionary[key]; + } + set + { + WriteDictionary[key] = value; + } + } + + public virtual bool ContainsKey(TKey key) + { + return ReadDictionary.ContainsKey(key); + } + + public virtual void Add(TKey key, TValue value) + { + WriteDictionary.Add(key, value); + } + + public virtual bool Remove(TKey key) + { + return WriteDictionary.Remove(key); + } + + public virtual bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) + { + return ReadDictionary.TryGetValue(key, out value); + } + + public virtual void Add(KeyValuePair item) + { + WriteDictionary.Add(item); + } + + public virtual void Clear() + { + WriteDictionary.Clear(); + } + + public virtual bool Contains(KeyValuePair item) + { + return ReadDictionary.Contains(item); + } + + public virtual void CopyTo(KeyValuePair[] array, int arrayIndex) + { + ReadDictionary.CopyTo(array, arrayIndex); + } + + public bool Remove(KeyValuePair item) + { + return WriteDictionary.Remove(item); + } + + public virtual IEnumerator> GetEnumerator() + { + return ReadDictionary.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } + + + +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Infrastructure/PluginCoreHostingStartup.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Infrastructure/PluginCoreHostingStartup.cs new file mode 100644 index 00000000..00cfaea4 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Infrastructure/PluginCoreHostingStartup.cs @@ -0,0 +1,62 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using PluginCore.AspNetCore.Extensions; + +[assembly: HostingStartup(typeof(PluginCore.AspNetCore.Infrastructure.PluginCoreHostingStartup))] +namespace PluginCore.AspNetCore.Infrastructure +{ + /// + /// https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/host/platform-specific-configuration?view=aspnetcore-5.0 + /// + public class PluginCoreHostingStartup : IHostingStartup + { + public PluginCoreHostingStartup() + { + + } + + public void Configure(IWebHostBuilder builder) + { + //builder.ConfigureAppConfiguration(config => + //{ + + //}); + + // 注意: 无论是通过 Program.cs 中 webBuilder.UseSetting(WebHostDefaults.HostingStartupAssembliesKey, "PluginCore"); + // 还是 通过环境变量 指定承载启动程序集, 都是先执行 外部的承载启动程序集, 再执行主程序的 Startup.cs, 因此在这时, 有些 service 还没有注册 + + // TODO: 不知道, 重复 Add, Use 会导致什么, 没有做防止重复 + builder.ConfigureServices(services => + { + // fixed: https://github.com/yiyungent/PluginCore/issues/1 + // System.InvalidOperationException: 'Unable to resolve service for type 'Microsoft.AspNetCore.Mvc.ApplicationParts.ApplicationPartManager' + // TODO: 不确定, 这样是否可行, 事实上之后主程序还会 Add 一次, 不知道是否会导致存在多个 实例 + // 失败: 不是一个实例, 导致无法改变 Controller + //Microsoft.AspNetCore.Mvc.ApplicationParts.ApplicationPartManager applicationPartManager = + // new Microsoft.AspNetCore.Mvc.ApplicationParts.ApplicationPartManager(); + //services.AddSingleton(applicationPartManager); + + services.AddPluginCore(); + }); + + builder.Configure(app => + { + app.UsePluginCore(); + }); + } + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Interfaces/IPluginApplicationBuilderManager.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Interfaces/IPluginApplicationBuilderManager.cs new file mode 100644 index 00000000..97a7b5b9 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Interfaces/IPluginApplicationBuilderManager.cs @@ -0,0 +1,25 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using Microsoft.AspNetCore.Http; +using System; +using System.Collections.Generic; +using System.Text; + +namespace PluginCore.AspNetCore.Interfaces +{ + public interface IPluginApplicationBuilderManager + { + void ReBuild(); + + RequestDelegate GetBuildResult(); + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Interfaces/IPluginControllerManager.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Interfaces/IPluginControllerManager.cs new file mode 100644 index 00000000..e89a28ea --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Interfaces/IPluginControllerManager.cs @@ -0,0 +1,25 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Text; + +namespace PluginCore.AspNetCore.Interfaces +{ + public interface IPluginControllerManager + { + void AddControllers(Assembly assembly); + + void RemoveControllers(string pluginId); + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Middlewares/LanguageMiddleware.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Middlewares/LanguageMiddleware.cs new file mode 100644 index 00000000..beb7bdae --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Middlewares/LanguageMiddleware.cs @@ -0,0 +1,47 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using PluginCore.IPlugins; + +namespace PluginCore.AspNetCore.Middlewares +{ + public class LanguageMiddleware + { + private readonly RequestDelegate _next; + + public LanguageMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task Invoke(HttpContext httpContext) + { + // 从 Cookie 中获取语言标识 + string language = httpContext.Request.Cookies[Constants.AspNetCoreLanguageCookieName]; + + // 存储当前语言到 HttpContext.Items + httpContext.Items[Constants.AspNetCoreLanguageKey] = language; + + // 调用下一个中间件 + await _next(httpContext); + } + } + + public static class LanguageMiddlewareExtensions + { + public static IApplicationBuilder UsePluginCoreLanguageMiddleware(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + } +} diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Middlewares/PluginContentFilterMiddleware.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Middlewares/PluginContentFilterMiddleware.cs new file mode 100644 index 00000000..7ade9b06 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Middlewares/PluginContentFilterMiddleware.cs @@ -0,0 +1,150 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using PluginCore.Interfaces; + +namespace PluginCore.AspNetCore.Middlewares +{ + /// + /// TODO: 未测试 + /// + public class PluginContentFilterMiddleware + { + private readonly RequestDelegate _next; + + private readonly IPluginFinder _pluginFinder; + + public PluginContentFilterMiddleware( + RequestDelegate next, + IWebHostEnvironment hostingEnv, + ILoggerFactory loggerFactory, + IPluginFinder pluginFinder) + { + _next = next; + _pluginFinder = pluginFinder; + } + + + public async Task InvokeAsync(HttpContext httpContext) + { + var httpMethod = httpContext.Request.Method; + var path = httpContext.Request.Path.Value; + + // 在请求下一个 middleware 前过滤 + await RequestBodyFilter(httpContext); + + // Call the next delegate/middleware in the pipeline + await _next(httpContext); + + if (httpMethod == "GET" && path.EndsWith(".css") || path.EndsWith(".js") || path.EndsWith(".html")) + { + // middleware 回退时 过滤 + await ResponseBodyFilter(httpContext); + } + } + + private async Task RequestBodyFilter(HttpContext httpContext) + { + string content = string.Empty; + using (var memoryStream = new MemoryStream()) + { + // Response.Body + await httpContext.Request.Body.CopyToAsync(memoryStream); + + long pos = httpContext.Request.Body.Position; + + using (var reader = new StreamReader(memoryStream, Encoding.UTF8)) + { + content = await reader.ReadToEndAsync(); + } + } + + var plugins = this._pluginFinder.EnablePlugins().ToList(); + + foreach (var item in plugins) + { + // 调用 + content = await item?.RequestBodyFilter(httpContext.Request.Path.Value, content); + } + + // 更新 Request.Body + + #region 方式1 + //var requestStream = new MemoryStream(); + //using (StreamWriter writer = new StreamWriter(requestStream, Encoding.UTF8)) + //{ + // await writer.WriteAsync(content); + //} + //httpContext.Request.Body = requestStream; + #endregion + + + #region 方式2 + httpContext.Request.Body.Seek(0, SeekOrigin.Begin); + byte[] buffer = Encoding.UTF8.GetBytes(content); + await httpContext.Request.Body.WriteAsync(buffer, 0, buffer.Length); + #endregion + } + + private async Task ResponseBodyFilter(HttpContext httpContext) + { + string content = string.Empty; + using (var memoryStream = new MemoryStream()) + { + // Response.Body + await httpContext.Response.Body.CopyToAsync(memoryStream); + + long pos = httpContext.Response.Body.Position; + + using (var reader = new StreamReader(memoryStream, Encoding.UTF8)) + { + content = await reader.ReadToEndAsync(); + } + } + + var plugins = this._pluginFinder.EnablePlugins().ToList(); + + foreach (var item in plugins) + { + // 调用 + content = await item?.ReponseBodyFilter(httpContext.Request.Path.Value, content); + } + + // 更新 ResponseBody + + #region 方式1 + //var responseStream = new MemoryStream(); + //using (StreamWriter writer = new StreamWriter(responseStream, Encoding.UTF8)) + //{ + // await writer.WriteAsync(content); + //} + //httpContext.Response.Body = responseStream; + #endregion + + + #region 方式2 + httpContext.Response.Body.Seek(0, SeekOrigin.Begin); + byte[] buffer = Encoding.UTF8.GetBytes(content); + await httpContext.Response.Body.WriteAsync(buffer, 0, buffer.Length); + #endregion + } + + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Middlewares/PluginHttpEndFilterMiddleware.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Middlewares/PluginHttpEndFilterMiddleware.cs new file mode 100644 index 00000000..5131a816 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Middlewares/PluginHttpEndFilterMiddleware.cs @@ -0,0 +1,72 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using PluginCore.Interfaces; + +namespace PluginCore.AspNetCore.Middlewares +{ + /// + /// 一定在 PluginCore 添加的中间件中 最后一个 + /// + public class PluginHttpEndFilterMiddleware + { + private readonly RequestDelegate _next; + + private readonly IPluginFinder _pluginFinder; + + public PluginHttpEndFilterMiddleware( + RequestDelegate next, + IWebHostEnvironment hostingEnv, + ILoggerFactory loggerFactory, + IPluginFinder pluginFinder) + { + _next = next; + _pluginFinder = pluginFinder; + } + + + public async Task InvokeAsync(HttpContext httpContext) + { + var httpMethod = httpContext.Request.Method; + var path = httpContext.Request.Path.Value; + + // 在请求下一个 middleware 前过滤 + + // Call the next delegate/middleware in the pipeline + await _next(httpContext); + + // middleware 回退时 过滤 + await Filter(httpContext); + } + + private async Task Filter(HttpContext httpContext) + { + var plugins = this._pluginFinder.EnablePlugins().ToList(); + + foreach (var item in plugins) + { + // 调用 + await item?.HttpEndFilter(httpContext); + } + } + + + + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Middlewares/PluginHttpStartFilterMiddleware.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Middlewares/PluginHttpStartFilterMiddleware.cs new file mode 100644 index 00000000..5286bcb3 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Middlewares/PluginHttpStartFilterMiddleware.cs @@ -0,0 +1,72 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using PluginCore.Interfaces; + +namespace PluginCore.AspNetCore.Middlewares +{ + /// + /// 一定在 PluginCore 添加的中间件中 第一个 + /// + public class PluginHttpStartFilterMiddleware + { + private readonly RequestDelegate _next; + + private readonly IPluginFinder _pluginFinder; + + public PluginHttpStartFilterMiddleware( + RequestDelegate next, + IWebHostEnvironment hostingEnv, + ILoggerFactory loggerFactory, + IPluginFinder pluginFinder) + { + _next = next; + _pluginFinder = pluginFinder; + } + + + public async Task InvokeAsync(HttpContext httpContext) + { + var httpMethod = httpContext.Request.Method; + var path = httpContext.Request.Path.Value; + + // 在请求下一个 middleware 前过滤 + await Filter(httpContext); + + // Call the next delegate/middleware in the pipeline + await _next(httpContext); + + // middleware 回退时 过滤 + } + + private async Task Filter(HttpContext httpContext) + { + var plugins = this._pluginFinder.EnablePlugins().ToList(); + + foreach (var item in plugins) + { + // 调用 + await item?.HttpStartFilter(httpContext); + } + } + + + + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Middlewares/PluginStartupXMiddleware.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Middlewares/PluginStartupXMiddleware.cs new file mode 100644 index 00000000..335e326b --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Middlewares/PluginStartupXMiddleware.cs @@ -0,0 +1,72 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using PluginCore.AspNetCore.Interfaces; +using PluginCore.Infrastructure; + +namespace PluginCore.AspNetCore.Middlewares +{ + public class PluginStartupXMiddleware + { + private readonly RequestDelegate _next; + + public PluginStartupXMiddleware(RequestDelegate next) + { + _next = next; + } + + public static Action ReachedEndAction { get; set; } = () => { _isReachedEnd = true; }; + + private static bool _isReachedEnd; + + public async Task InvokeAsync(HttpContext httpContext, IPluginApplicationBuilderManager pluginApplicationBuilderManager) + { + //bool isReachedEnd = false; + _isReachedEnd = false; + + try + { + RequestDelegate requestDelegate = pluginApplicationBuilderManager.GetBuildResult(); + + await requestDelegate(httpContext); + } + catch (Exception ex) + { + // InvalidOperationException: The request reached the end of the pipeline without executing the endpoint: 'AspNetCore3_1.Controllers.WeatherForecastController.Get (AspNetCore3_1)'. Please register the EndpointMiddleware using 'IApplicationBuilder.UseEndpoints(...)' if using routing. + Utils.LogUtil.Error(ex, ex.Message); + if (ex.InnerException != null) + { + Utils.LogUtil.Error(ex.InnerException, ex.InnerException.Message); + } + } + + if (_isReachedEnd) + { + // Call the next delegate/middleware in the pipeline + await _next(httpContext); + } + else + { + // 没有抵达 End, 说明在插件的 middleware 中已堵塞, 准备返回 响应 + } + + + } + + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/PluginCore.AspNetCore.csproj b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/PluginCore.AspNetCore.csproj new file mode 100644 index 00000000..cdcec149 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/PluginCore.AspNetCore.csproj @@ -0,0 +1,107 @@ + + + + net8.0;net9.0 + PluginCore.AspNetCore + 1.4.3 + yiyun + yiyun + ASP.NET Core lightweight plugin framework + Copyright (c) 2021-present yiyun + https://github.com/yiyungent/PluginCore + https://github.com/yiyungent/PluginCore/blob/main/LICENSE + PluginCore PluginCore.AspNetCore + true + + + + + + + + + + + + + + + + + + + + + + + + + + bin\Release\netcoreapp3.1\PluginCore.AspNetCore.xml + + + bin\Release\net5.0\PluginCore.AspNetCore.xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + $(Path)$(VsInstallRoot)\Web\External\; + + + + + + + + + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/RequestModel/User/LoginRequestModel.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/RequestModel/User/LoginRequestModel.cs new file mode 100644 index 00000000..0484677e --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/RequestModel/User/LoginRequestModel.cs @@ -0,0 +1,24 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Generic; +using System.Text; + +namespace PluginCore.AspNetCore.RequestModel.User +{ + public class LoginRequestModel + { + public string UserName { get; set; } + + public string Password { get; set; } + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/RequestModel/User/UpdateRequestMode.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/RequestModel/User/UpdateRequestMode.cs new file mode 100644 index 00000000..51617c7c --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/RequestModel/User/UpdateRequestMode.cs @@ -0,0 +1,24 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Generic; +using System.Text; + +namespace PluginCore.AspNetCore.RequestModel.User +{ + public class UpdateRequestModel + { + public string UserName { get; set; } + + public string Password { get; set; } + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/ResponseModel/BaseResponseModel.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/ResponseModel/BaseResponseModel.cs new file mode 100644 index 00000000..6e26b427 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/ResponseModel/BaseResponseModel.cs @@ -0,0 +1,22 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +namespace PluginCore.AspNetCore.ResponseModel +{ + public class BaseResponseModel + { + public int Code { get; set; } + + public string Message { get; set; } + + public object Data { get; set; } + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/cliff.toml b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/cliff.toml new file mode 100644 index 00000000..aa3089f4 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/cliff.toml @@ -0,0 +1,82 @@ +# git-cliff ~ configuration file +# https://git-cliff.org/docs/configuration + +[changelog] +# template for the changelog header +header = """ +# Changelog\n +All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines.\n +""" +# template for the changelog body +# https://keats.github.io/tera/docs/#introduction +body = """ +--- +{% if version %}\ + {% if previous.version %}\ + ## [{{ version | trim_start_matches(pat="v") }}]($REPO/compare/{{ previous.version }}..{{ version }}) - {{ timestamp | date(format="%Y-%m-%d") }} + {% else %}\ + ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} + {% endif %}\ +{% else %}\ + ## [unreleased] +{% endif %}\ +{% for group, commits in commits | group_by(attribute="group") %} + ### {{ group | striptags | trim | upper_first }} + {% for commit in commits + | filter(attribute="scope") + | sort(attribute="scope") %} + - **({{commit.scope}})**{% if commit.breaking %} [**breaking**]{% endif %} \ + {{ commit.message }} - ([{{ commit.id | truncate(length=7, end="") }}]($REPO/commit/{{ commit.id }})) - {{ commit.author.name }} + {%- endfor -%} + {% raw %}\n{% endraw %}\ + {%- for commit in commits %} + {%- if commit.scope -%} + {% else -%} + - {% if commit.breaking %} [**breaking**]{% endif %}\ + {{ commit.message }} - ([{{ commit.id | truncate(length=7, end="") }}]($REPO/commit/{{ commit.id }})) - {{ commit.author.name }} + {% endif -%} + {% endfor -%} +{% endfor %}\n +""" +# template for the changelog footer +footer = """ + +""" +# remove the leading and trailing whitespace from the templates +trim = true +# postprocessors +postprocessors = [ + { pattern = '\$REPO', replace = "https://github.com/yiyungent/PluginCore" }, # replace repository URL +] + +[git] +# parse the commits based on https://www.conventionalcommits.org +conventional_commits = true +# filter out the commits that are not conventional +filter_unconventional = true +# process each line of a commit as an individual commit +split_commits = false +# regex for preprocessing the commit messages +commit_preprocessors = [ + # { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/orhun/git-cliff/issues/${2}))"}, # replace issue numbers +] +# regex for parsing and grouping commits +commit_parsers = [ + { message = "^feat", group = "Features" }, + { message = "^fix", group = "Bug Fixes" }, + { message = "^doc", group = "Documentation" }, + { message = "^perf", group = "Performance" }, + { message = "^refactor", group = "Refactoring" }, + { message = "^style", group = "Style" }, + { message = "^revert", group = "Revert" }, + { message = "^test", group = "Tests" }, + { message = "^chore\\(version\\):", skip = true }, + { message = "^chore", group = "Miscellaneous Chores" }, + { body = ".*security", group = "Security" }, +] +# filter out the commits that are not matched by commit parsers +filter_commits = false +# sort the tags topologically +topo_order = false +# sort the commits inside sections by oldest/newest order +sort_commits = "oldest" diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/lmplements/AspNetCorePluginManager.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/lmplements/AspNetCorePluginManager.cs new file mode 100644 index 00000000..c4a5c19e --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/lmplements/AspNetCorePluginManager.cs @@ -0,0 +1,82 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using PluginCore.AspNetCore.Interfaces; +using PluginCore.Interfaces; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.Loader; +using System.Text; +using PluginCore.lmplements; +using PluginCore.Infrastructure; + +namespace PluginCore.AspNetCore.lmplements +{ + /// + /// 一个插件的所有dll由 一个 管理 + /// 记录管理了所有 插件的 + /// 是对 的封装, 使其更好管理插件加载释放的行为 + /// + public class AspNetCorePluginManager : IPluginManager + { + private readonly IPluginControllerManager _pluginControllerManager; + + public IPluginContextManager PluginContextManager { get; set; } + + public IPluginContextPack PluginContextPack { get; set; } + + public AspNetCorePluginManager(IPluginContextManager pluginContextManager, IPluginContextPack pluginContextPack, IPluginControllerManager pluginControllerManager) + { + this.PluginContextManager = pluginContextManager; + this.PluginContextPack = pluginContextPack; + _pluginControllerManager = pluginControllerManager; + } + + /// + /// 加载插件程序集 + /// + /// + public void LoadPlugin(string pluginId) + { + IPluginContext context = this.PluginContextPack.Pack(pluginId); + Assembly pluginMainAssembly = context.LoadFromAssemblyName(new AssemblyName(pluginId)); + //注销内置Controller的注册 by tomny + // 加载其中的控制器 + //_pluginControllerManager.AddControllers(pluginMainAssembly); + + // 这个插件加载上下文 放入 集合中 + this.PluginContextManager.Add(pluginId, context); + } + + public void UnloadPlugin(string pluginId) + { + this.PluginContextManager.Remove(pluginId); + + // 移除其中的控制器 + _pluginControllerManager.RemoveControllers(pluginId); + } + /// + /// 加载插件程序集 + /// + /// + public Assembly GetPluginAssembly(string pluginId) + { + IPluginContext context = this.PluginContextPack.Pack(pluginId); + Assembly pluginMainAssembly = context.LoadFromAssemblyName(new AssemblyName(pluginId)); + + return pluginMainAssembly; + } + + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/lmplements/AspNetCorePluginManagerV1.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/lmplements/AspNetCorePluginManagerV1.cs new file mode 100644 index 00000000..d4dec70a --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/lmplements/AspNetCorePluginManagerV1.cs @@ -0,0 +1,84 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using PluginCore.AspNetCore.Interfaces; +using PluginCore.Interfaces; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.Loader; +using System.Text; +using PluginCore.lmplements; +using PluginCore.Infrastructure; + +namespace PluginCore.AspNetCore.lmplements +{ + + // ********* 虽然 看上去和 AspNetCorePluginManager 一样, 但特别保留, 防止需要不一样的处理, 后续更新维护方便 + + /// + /// 一个插件的所有dll由 一个 管理 + /// 记录管理了所有 插件的 + /// 是对 的封装, 使其更好管理插件加载释放的行为 + /// + public class AspNetCorePluginManagerV1 : IPluginManager + { + private readonly IPluginControllerManager _pluginControllerManager; + + public IPluginContextManager PluginContextManager { get; set; } + + public IPluginContextPack PluginContextPack { get; set; } + + public AspNetCorePluginManagerV1(IPluginContextManager pluginContextManager, IPluginContextPack pluginContextPack, IPluginControllerManager pluginControllerManager) + { + this.PluginContextManager = pluginContextManager; + this.PluginContextPack = pluginContextPack; + _pluginControllerManager = pluginControllerManager; + } + + /// + /// 加载插件程序集 + /// + /// + public void LoadPlugin(string pluginId) + { + // 此插件的 加载上下文 + IPluginContext context = this.PluginContextPack.Pack(pluginId); + + // TODO: 注意: 未测试: 不清除 对于 旧版加载 dll 方式, 再结合 LoadFromAssemblyName 是否有效 + Assembly pluginMainAssembly = context.LoadFromAssemblyName(new AssemblyName(pluginId)); + + //注销内置Controller的注册 by tomny + // 加载其中的控制器 + //_pluginControllerManager.AddControllers(pluginMainAssembly); + + // 这个插件加载上下文 放入 集合中 + this.PluginContextManager.Add(pluginId, context); + } + + public void UnloadPlugin(string pluginId) + { + this.PluginContextManager.Remove(pluginId); + + // 移除其中的控制器 + _pluginControllerManager.RemoveControllers(pluginId); + } + public Assembly GetPluginAssembly(string pluginId) + { + IPluginContext context = this.PluginContextPack.Pack(pluginId); + Assembly pluginMainAssembly = context.LoadFromAssemblyName(new AssemblyName(pluginId)); + + return pluginMainAssembly; + } + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/lmplements/PluginActionDescriptorChangeProvider.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/lmplements/PluginActionDescriptorChangeProvider.cs new file mode 100644 index 00000000..354f97e5 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/lmplements/PluginActionDescriptorChangeProvider.cs @@ -0,0 +1,40 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.Extensions.Primitives; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; + +namespace PluginCore.AspNetCore.lmplements +{ + /// + /// 目前采用的第一种方案 + /// 方案一: https://www.codetd.com/article/461093 + /// 方案二: https://www.cnblogs.com/artech/p/dynamic-controllers.html + /// + public class PluginActionDescriptorChangeProvider : IActionDescriptorChangeProvider + { + public static PluginActionDescriptorChangeProvider Instance { get; } = new PluginActionDescriptorChangeProvider(); + + public CancellationTokenSource TokenSource { get; private set; } + + public bool HasChanged { get; set; } + + public IChangeToken GetChangeToken() + { + TokenSource = new CancellationTokenSource(); + return new CancellationChangeToken(TokenSource.Token); + } + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/lmplements/PluginApplicationBuilderManager.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/lmplements/PluginApplicationBuilderManager.cs new file mode 100644 index 00000000..2e21730e --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/lmplements/PluginApplicationBuilderManager.cs @@ -0,0 +1,78 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using PluginCore.AspNetCore.Interfaces; +using PluginCore.Interfaces; +using PluginCore.IPlugins; +using PluginCore.AspNetCore.Middlewares; +using PluginCore.AspNetCore.Infrastructure; + +namespace PluginCore.AspNetCore.lmplements +{ + public class PluginApplicationBuilderManager : PluginApplicationBuilderManager + { + public PluginApplicationBuilderManager(IPluginFinder pluginFinder) : base(pluginFinder) + { + } + } + + public class PluginApplicationBuilderManager : IPluginApplicationBuilderManager + where TPluginApplicationBuilder : PluginApplicationBuilder, new() + { + private readonly IPluginFinder _pluginFinder; + + public PluginApplicationBuilderManager(IPluginFinder pluginFinder) + { + _pluginFinder = pluginFinder; + } + + public static RequestDelegate RequestDelegateResult { get; set; } + + + /// + /// 插件 启用, 禁用 时: 重新 Build + /// + public void ReBuild() + { + TPluginApplicationBuilder applicationBuilder = new TPluginApplicationBuilder(); + applicationBuilder.ReachEndAction = PluginStartupXMiddleware.ReachedEndAction; + + var plugins = this._pluginFinder.EnablePlugins()?.OrderBy(m => m.ConfigureOrder)?.ToList(); + foreach (var item in plugins) + { + // 调用 + Utils.LogUtil.Info($"{item.GetType().ToString()} {nameof(IStartupXPlugin)}.{nameof(IStartupXPlugin.Configure)}"); + + item.Configure(applicationBuilder); + } + + RequestDelegateResult = applicationBuilder.Build(); + } + + + public RequestDelegate GetBuildResult() + { + if (RequestDelegateResult == null) + { + ReBuild(); + } + + return RequestDelegateResult; + } + + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/lmplements/PluginControllerManager.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/lmplements/PluginControllerManager.cs new file mode 100644 index 00000000..ef890226 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/lmplements/PluginControllerManager.cs @@ -0,0 +1,66 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using Microsoft.AspNetCore.Mvc.ApplicationParts; +using PluginCore.AspNetCore.Interfaces; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace PluginCore.AspNetCore.lmplements +{ + public class PluginControllerManager : IPluginControllerManager + { + private readonly ApplicationPartManager _applicationPartManager; + + public PluginControllerManager(ApplicationPartManager applicationPartManager) + { + _applicationPartManager = applicationPartManager; + } + + /// + /// 从指定 中获取 Controller, 并添加进来 + /// + /// + public void AddControllers(Assembly assembly) + { + AssemblyPart assemblyPart = new AssemblyPart(assembly); + _applicationPartManager.ApplicationParts.Add(assemblyPart); + + ResetControllActions(); + } + + public void RemoveControllers(string pluginId) + { + ApplicationPart last = _applicationPartManager.ApplicationParts.First(m => m.Name == pluginId); + _applicationPartManager.ApplicationParts.Remove(last); + + ResetControllActions(); + } + + /// + /// 通知应用(主程序)Controller.Action 已发生变化 + /// + private void ResetControllActions() + { + PluginActionDescriptorChangeProvider.Instance.HasChanged = true; + // TokenSource 为 null + // 注意: 在程序刚启动时, 未抵达 Controller 时不会触发 IActionDescriptorChangeProvider.GetChangeToken(), 也就会导致 TokenSource 为 null, 在启动时,同时在启动时,插件Controller.Action和主程序一起被添加,此时无需通知改变 + if (PluginActionDescriptorChangeProvider.Instance.TokenSource != null) + { + // 只有在插件列表控制启用,禁用时才需通知改变 + PluginActionDescriptorChangeProvider.Instance.TokenSource.Cancel(); + } + } + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/nuget-pack.ps1 b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/nuget-pack.ps1 new file mode 100644 index 00000000..c3c10d7a --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/nuget-pack.ps1 @@ -0,0 +1 @@ +dotnet pack -c Release \ No newline at end of file diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/package-lock.json b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/package-lock.json new file mode 100644 index 00000000..741f46cd --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/package-lock.json @@ -0,0 +1,307 @@ +{ + "name": "PluginCore", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "PluginCore", + "version": "1.0.0", + "dependencies": { + "plugincore-admin-frontend": "0.3.2" + } + }, + "node_modules/ansi-styles": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-1.0.0.tgz", + "integrity": "sha512-3iF4FIKdxaVYT3JqQuY3Wat/T2t7TRbbQ94Fu50ZUCbLy4TFbTzr90NOHQodQkNqmeEGCw8WbeP78WNi6SKYUA==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/async-validator": { + "version": "1.8.5", + "resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-1.8.5.tgz", + "integrity": "sha512-tXBM+1m056MAX0E8TL2iCjg8WvSyXu0Zc8LNtYqrVeyoL3+esHRZ4SieE9fKQyyU09uONjnMEjrNBMqT0mbvmA==", + "dependencies": { + "babel-runtime": "6.x" + } + }, + "node_modules/axios": { + "version": "0.18.1", + "resolved": "https://registry.npmmirror.com/axios/-/axios-0.18.1.tgz", + "integrity": "sha512-0BfJq4NSfQXd+SkFdrvFbG7addhYSBA2mQwISr46pD6E5iqkWg02RAs8vyTT/j0RTnoYmeXauBuSv1qKwR179g==", + "deprecated": "Critical security vulnerability fixed in v0.21.1. For more information, see https://github.com/axios/axios/pull/3410", + "dependencies": { + "follow-redirects": "1.5.10", + "is-buffer": "^2.0.2" + } + }, + "node_modules/babel-helper-vue-jsx-merge-props": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-2.0.3.tgz", + "integrity": "sha512-gsLiKK7Qrb7zYJNgiXKpXblxbV5ffSwR0f5whkPAaBAR4fhi6bwRZxX9wBlIc5M/v8CCkXUbXZL4N/nSE97cqg==" + }, + "node_modules/babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmmirror.com/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==", + "dependencies": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + } + }, + "node_modules/babel-runtime/node_modules/core-js": { + "version": "2.6.12", + "resolved": "https://registry.npmmirror.com/core-js/-/core-js-2.6.12.tgz", + "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==", + "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.", + "hasInstallScript": true + }, + "node_modules/chalk": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-0.4.0.tgz", + "integrity": "sha512-sQfYDlfv2DGVtjdoQqxS0cEZDroyG8h6TamA6rvxwlrU5BaSLDx9xhatBYl2pxZ7gmpNaPFVwBtdGdu5rQ+tYQ==", + "dependencies": { + "ansi-styles": "~1.0.0", + "has-color": "~0.1.0", + "strip-ansi": "~0.1.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/codemirror": { + "version": "5.65.2", + "resolved": "https://registry.npmmirror.com/codemirror/-/codemirror-5.65.2.tgz", + "integrity": "sha512-SZM4Zq7XEC8Fhroqe3LxbEEX1zUPWH1wMr5zxiBuiUF64iYOUH/JI88v4tBag8MiBS8B8gRv8O1pPXGYXQ4ErA==" + }, + "node_modules/core-js": { + "version": "3.6.5", + "resolved": "https://registry.npmmirror.com/core-js/-/core-js-3.6.5.tgz", + "integrity": "sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==", + "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.", + "hasInstallScript": true + }, + "node_modules/debug": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/deepmerge": { + "version": "1.5.2", + "resolved": "https://registry.npmmirror.com/deepmerge/-/deepmerge-1.5.2.tgz", + "integrity": "sha512-95k0GDqvBjZavkuvzx/YqVLv/6YYa17fz6ILMSf7neqQITCPbnfEnQvEgMPNjH4kgobe7+WIL0yJEHku+H3qtQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/element-ui": { + "version": "2.13.2", + "resolved": "https://registry.npmmirror.com/element-ui/-/element-ui-2.13.2.tgz", + "integrity": "sha512-r761DRPssMPKDiJZWFlG+4e4vr0cRG/atKr3Eqr8Xi0tQMNbtmYU1QXvFnKiFPFFGkgJ6zS6ASkG+sellcoHlQ==", + "dependencies": { + "async-validator": "~1.8.1", + "babel-helper-vue-jsx-merge-props": "^2.0.0", + "deepmerge": "^1.2.0", + "normalize-wheel": "^1.0.1", + "resize-observer-polyfill": "^1.5.0", + "throttle-debounce": "^1.0.1" + }, + "peerDependencies": { + "vue": "^2.5.17" + } + }, + "node_modules/follow-redirects": { + "version": "1.5.10", + "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.5.10.tgz", + "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", + "dependencies": { + "debug": "=3.1.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/has-color": { + "version": "0.1.7", + "resolved": "https://registry.npmmirror.com/has-color/-/has-color-0.1.7.tgz", + "integrity": "sha512-kaNz5OTAYYmt646Hkqw50/qyxP2vFnTVu5AQ1Zmk22Kk5+4Qx6BpO8+u7IKsML5fOsFk0ZT0AcCJNYwcvaLBvw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "engines": { + "node": ">=4" + } + }, + "node_modules/js-cookie": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/js-cookie/-/js-cookie-2.2.0.tgz", + "integrity": "sha512-7YAJP/LPE/MhDjHIdfIiT665HUSumCwPN2hAmO6OJZ8V3o1mtz2HeQ8BKetEjkh+3nqGxYaq1vPMViUR8kaOXw==" + }, + "node_modules/jsonlint": { + "version": "1.6.3", + "resolved": "https://registry.npmmirror.com/jsonlint/-/jsonlint-1.6.3.tgz", + "integrity": "sha512-jMVTMzP+7gU/IyC6hvKyWpUU8tmTkK5b3BPNuMI9U8Sit+YAWLlZwB6Y6YrdCxfg2kNz05p3XY3Bmm4m26Nv3A==", + "dependencies": { + "JSV": "^4.0.x", + "nomnom": "^1.5.x" + }, + "bin": { + "jsonlint": "lib/cli.js" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/JSV": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/JSV/-/JSV-4.0.2.tgz", + "integrity": "sha512-ZJ6wx9xaKJ3yFUhq5/sk82PJMuUyLk277I8mQeyDgCTjGdjWJIvPfaU5LIXaMuaN2UO1X3kZH4+lgphublZUHw==", + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/nomnom": { + "version": "1.8.1", + "resolved": "https://registry.npmmirror.com/nomnom/-/nomnom-1.8.1.tgz", + "integrity": "sha512-5s0JxqhDx9/rksG2BTMVN1enjWSvPidpoSgViZU4ZXULyTe+7jxcCRLB6f42Z0l1xYJpleCBtSyY6Lwg3uu5CQ==", + "deprecated": "Package no longer supported. Contact support@npmjs.com for more info.", + "dependencies": { + "chalk": "~0.4.0", + "underscore": "~1.6.0" + } + }, + "node_modules/normalize-wheel": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/normalize-wheel/-/normalize-wheel-1.0.1.tgz", + "integrity": "sha512-1OnlAPZ3zgrk8B91HyRj+eVv+kS5u+Z0SCsak6Xil/kmgEia50ga7zfkumayonZrImffAxPU/5WcyGhzetHNPA==" + }, + "node_modules/normalize.css": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/normalize.css/-/normalize.css-7.0.0.tgz", + "integrity": "sha512-LYaFZxj2Q1Q9e1VJ0f6laG46Rt5s9URhKyckNaA2vZnL/0gwQHWhM7ALQkp3WBQKM5sXRLQ5Ehrfkp+E/ZiCRg==" + }, + "node_modules/nprogress": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/nprogress/-/nprogress-0.2.0.tgz", + "integrity": "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==" + }, + "node_modules/path-to-regexp": { + "version": "2.4.0", + "resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-2.4.0.tgz", + "integrity": "sha512-G6zHoVqC6GGTQkZwF4lkuEyMbVOjoBKAEybQUypI1WTkqinCOrq2x6U2+phkJ1XsEMTy4LjtwPI7HW+NVrRR2w==" + }, + "node_modules/plugincore-admin-frontend": { + "version": "0.3.2", + "resolved": "https://registry.npmmirror.com/plugincore-admin-frontend/-/plugincore-admin-frontend-0.3.2.tgz", + "integrity": "sha512-w8t6J+92Ns/gan/3uLW9Uns6vbAEkOzYBzueCJY5b7i+F7wyvmh0TcOHQscm6VmJQW+d73eaTk4psIcUcHbOng==", + "dependencies": { + "axios": "0.18.1", + "codemirror": "^5.57.0", + "core-js": "3.6.5", + "element-ui": "2.13.2", + "js-cookie": "2.2.0", + "jsonlint": "^1.6.3", + "normalize.css": "7.0.0", + "nprogress": "0.2.0", + "path-to-regexp": "2.4.0", + "script-loader": "^0.7.2", + "vue": "2.6.10", + "vue-i18n": "^8.26.7", + "vue-meditor": "^2.1.1", + "vue-router": "3.0.6", + "vuex": "3.1.0" + }, + "engines": { + "node": ">=8.9", + "npm": ">= 3.0.0" + } + }, + "node_modules/raw-loader": { + "version": "0.5.1", + "resolved": "https://registry.npmmirror.com/raw-loader/-/raw-loader-0.5.1.tgz", + "integrity": "sha512-sf7oGoLuaYAScB4VGr0tzetsYlS8EJH6qnTCfQ/WVEa89hALQ4RQfCKt5xCyPQKPDUbVUAIP1QsxAwfAjlDp7Q==" + }, + "node_modules/regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" + }, + "node_modules/resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmmirror.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==" + }, + "node_modules/script-loader": { + "version": "0.7.2", + "resolved": "https://registry.npmmirror.com/script-loader/-/script-loader-0.7.2.tgz", + "integrity": "sha512-UMNLEvgOAQuzK8ji8qIscM3GIrRCWN6MmMXGD4SD5l6cSycgGsCo0tX5xRnfQcoghqct0tjHjcykgI1PyBE2aA==", + "dependencies": { + "raw-loader": "~0.5.1" + } + }, + "node_modules/strip-ansi": { + "version": "0.1.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-0.1.1.tgz", + "integrity": "sha512-behete+3uqxecWlDAm5lmskaSaISA+ThQ4oNNBDTBJt0x2ppR6IPqfZNuj6BLaLJ/Sji4TPZlcRyOis8wXQTLg==", + "bin": { + "strip-ansi": "cli.js" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/throttle-debounce": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/throttle-debounce/-/throttle-debounce-1.1.0.tgz", + "integrity": "sha512-XH8UiPCQcWNuk2LYePibW/4qL97+ZQ1AN3FNXwZRBNPPowo/NRU5fAlDCSNBJIYCKbioZfuYtMhG4quqoJhVzg==", + "engines": { + "node": ">=4" + } + }, + "node_modules/underscore": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/underscore/-/underscore-1.6.0.tgz", + "integrity": "sha512-z4o1fvKUojIWh9XuaVLUDdf86RQiq13AC1dmHbTpoyuu+bquHms76v16CjycCbec87J7z0k//SiQVk0sMdFmpQ==" + }, + "node_modules/vue": { + "version": "2.6.10", + "resolved": "https://registry.npmmirror.com/vue/-/vue-2.6.10.tgz", + "integrity": "sha512-ImThpeNU9HbdZL3utgMCq0oiMzAkt1mcgy3/E6zWC/G6AaQoeuFdsl9nDhTDU3X1R6FK7nsIUuRACVcjI+A2GQ==" + }, + "node_modules/vue-i18n": { + "version": "8.27.1", + "resolved": "https://registry.npmmirror.com/vue-i18n/-/vue-i18n-8.27.1.tgz", + "integrity": "sha512-lWrGm4F25qReJ7XxSnFVb2h3PfW54ldnM4C+YLBGGJ75+Myt/kj4hHSTKqsyDLamvNYpvINMicSOdW+7yuqgIQ==" + }, + "node_modules/vue-meditor": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/vue-meditor/-/vue-meditor-2.1.1.tgz", + "integrity": "sha512-xvLqqvS4LzEJ7TqW2mDSzvueMey/WM92o0jfmqZWBTks1/8EAmn0ehqZDlcq/W+1goQpUmWE4m3yJjzQnoZhQQ==" + }, + "node_modules/vue-router": { + "version": "3.0.6", + "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-3.0.6.tgz", + "integrity": "sha512-Ox0ciFLswtSGRTHYhGvx2L44sVbTPNS+uD2kRISuo8B39Y79rOo0Kw0hzupTmiVtftQYCZl87mwldhh2L9Aquw==" + }, + "node_modules/vuex": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/vuex/-/vuex-3.1.0.tgz", + "integrity": "sha512-mdHeHT/7u4BncpUZMlxNaIdcN/HIt1GsGG5LKByArvYG/v6DvHcOxvDCts+7SRdCoIRGllK8IMZvQtQXLppDYg==" + } + } +} diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/package.json b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/package.json new file mode 100644 index 00000000..2466cb08 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/package.json @@ -0,0 +1,8 @@ +{ + "name": "PluginCore", + "version": "1.0.0", + "private": true, + "dependencies": { + "plugincore-admin-frontend": "0.3.2" + } +} diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/readme.txt b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/readme.txt new file mode 100644 index 00000000..3e8cd20a --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/readme.txt @@ -0,0 +1,11 @@ + + + + + +`PluginCoreAdmin` is the web frontend for the `PluginCore management center (PluginCore Admin)` and should be released together. + +https://github.com/yiyungent/PluginCore + + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins.AspNetCore/CHANGELOG.md b/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins.AspNetCore/CHANGELOG.md new file mode 100644 index 00000000..e7d4ae57 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins.AspNetCore/CHANGELOG.md @@ -0,0 +1,54 @@ +# Changelog + +All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. + +--- +## [unreleased] + +### Documentation + +- **(CHANGELOG.md)** update - ([0bbfc89](https://github.com/yiyungent/PluginCore/commit/0bbfc8955b7f6338db2125c78ec250e9eeeadcce)) - github-actions[bot] +- **(CHANGELOG.md)** update - ([4f6b47b](https://github.com/yiyungent/PluginCore/commit/4f6b47b3f86bfce4a8f660166837a7322c568d78)) - github-actions[bot] + +### Miscellaneous Chores + +- **(cliff.toml)** add - ([5614ef0](https://github.com/yiyungent/PluginCore/commit/5614ef024d644349095e19a0016bb23d989b0c90)) - yiyun + +--- +## [PluginCore.IPlugins.AspNetCore-v0.1.1](https://github.com/yiyungent/PluginCore/compare/PluginCore.IPlugins.AspNetCore-v0.1.0..PluginCore.IPlugins.AspNetCore-v0.1.1) - 2023-12-30 + +### Features + +- **(src/**/*.cs)** // License: Apache-2.0 -> // License: GNU LGPLv3 - ([57366d3](https://github.com/yiyungent/PluginCore/commit/57366d3e2afdb8e20e94851aa8a09f1ee61b6d7e)) - yiyun +- **(src/**/*.cs)** // Project: https://moeci.com/PluginCore -> // Project: https://yiyungent.github.io/PluginCore - ([7420480](https://github.com/yiyungent/PluginCore/commit/742048065978c1b8597fab3d52f011db4247fbda)) - yiyun + +### Build + +- **(plugincore.iplugins.aspnetcore.csproj)** 0.1.0 -> 0.1.1 - ([338e919](https://github.com/yiyungent/PluginCore/commit/338e919c74dcff4199b82c6046a62847cc68beb3)) - yiyun + +--- +## [PluginCore.IPlugins.AspNetCore-v0.1.0](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v1.0.2..PluginCore.IPlugins.AspNetCore-v0.1.0) - 2023-02-15 + +### Build + +- **(plugincore.iplugins.aspnetcore.csproj)** `0.1.0` - ([162d304](https://github.com/yiyungent/PluginCore/commit/162d304f6b74941701b5b799228a3061ef4ea6c2)) - yiyun + +--- +## [PluginCore.AspNetCore-v1.0.2](https://github.com/yiyungent/PluginCore/compare/PluginCore.IPlugins.AspNetCore-v0.0.1..PluginCore.AspNetCore-v1.0.2) - 2022-04-19 + +### Style + +- add: copyright: *.cs - ([9643dce](https://github.com/yiyungent/PluginCore/commit/9643dce112861a440d63306cb555accbed3d5111)) - yiyun + +--- +## [PluginCore.IPlugins.AspNetCore-v0.0.1](https://github.com/yiyungent/PluginCore/compare/PluginCore-v1.0.0..PluginCore.IPlugins.AspNetCore-v0.0.1) - 2022-04-16 + +### Refactoring + +- 1.提取出 PluginCore.AspNetCore,PluginCore.IPlugins.AspNetCore 2.提取出更多接口,可自由替换 - ([fffd8d9](https://github.com/yiyungent/PluginCore/commit/fffd8d91c23fd6e4a4d09cbf91975beb3cf7acf0)) - yiyun + +### Build + +- **(plugincore.iplugins.aspnetcore.csproj)** 0.0.1 - ([b907263](https://github.com/yiyungent/PluginCore/commit/b9072639d894904add2faf46216e29f902ddf32b)) - yiyun + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins.AspNetCore/IPlugins/IContentFilterPlugin.cs b/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins.AspNetCore/IPlugins/IContentFilterPlugin.cs new file mode 100644 index 00000000..5f5fbd33 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins.AspNetCore/IPlugins/IContentFilterPlugin.cs @@ -0,0 +1,29 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + + +namespace PluginCore.IPlugins +{ + /// + /// 目前未有效化,占坑 + /// + public interface IContentFilterPlugin : IPlugin + { + Task RequestBodyFilter(string urlPath, string content); + + Task ReponseBodyFilter(string urlPath, string content); + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins.AspNetCore/IPlugins/IHttpFilterPlugin.cs b/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins.AspNetCore/IPlugins/IHttpFilterPlugin.cs new file mode 100644 index 00000000..57d288a1 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins.AspNetCore/IPlugins/IHttpFilterPlugin.cs @@ -0,0 +1,29 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace PluginCore.IPlugins +{ + /// + /// 实验性: 不确定有效, 发现问题,请 new issue + /// + public interface IHttpFilterPlugin : IPlugin + { + Task HttpEndFilter(HttpContext httpContext); + + Task HttpStartFilter(HttpContext httpContext); + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins.AspNetCore/IPlugins/IStartupPlugin.cs b/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins.AspNetCore/IPlugins/IStartupPlugin.cs new file mode 100644 index 00000000..506f2b22 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins.AspNetCore/IPlugins/IStartupPlugin.cs @@ -0,0 +1,40 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace PluginCore.IPlugins +{ + /// + /// 实验阶段 + /// 无法热插拔: 需要启用插件后, 再 重启站点 + /// + public interface IStartupPlugin : IPlugin + { + /// + /// This method gets called by the runtime. Use this method to add services to the container. + /// + /// + void ConfigureServices(IServiceCollection services); + + int ConfigureServicesOrder { get; } + + /// + /// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + /// + /// + /// + void Configure(IApplicationBuilder app); + + int ConfigureOrder { get; } + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins.AspNetCore/IPlugins/IStartupXPlugin.cs b/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins.AspNetCore/IPlugins/IStartupXPlugin.cs new file mode 100644 index 00000000..dccf5cc7 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins.AspNetCore/IPlugins/IStartupXPlugin.cs @@ -0,0 +1,45 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace PluginCore.IPlugins +{ + /// + /// 实验阶段 + /// 热插拔: 已有效化 + /// + public interface IStartupXPlugin : IPlugin + { + /// + /// This method gets called by the runtime. Use this method to add services to the container. + /// 未有效化 + /// + /// + void ConfigureServices(IServiceCollection services); + + + int ConfigureServicesOrder { get; } + + /// + /// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + /// + /// + /// + void Configure(IApplicationBuilder app); + + int ConfigureOrder { get; } + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins.AspNetCore/PluginCore.IPlugins.AspNetCore.csproj b/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins.AspNetCore/PluginCore.IPlugins.AspNetCore.csproj new file mode 100644 index 00000000..dda157f9 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins.AspNetCore/PluginCore.IPlugins.AspNetCore.csproj @@ -0,0 +1,40 @@ + + + + net8.0;net9.0 + PluginCore.IPlugins.AspNetCore + 0.1.1 + yiyun + yiyun + PluginCore.AspNetCore Plugin Sdk + Copyright (c) 2021-present yiyun + https://github.com/yiyungent/PluginCore + https://github.com/yiyungent/PluginCore/blob/main/LICENSE + PluginCore PluginCore.IPlugins.AspNetCore + true + + + + + + + + + + + + + bin\Release\netstandard2.0\PluginCore.IPlugins.AspNetCore.xml + + + + + + + + + + + + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins.AspNetCore/cliff.toml b/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins.AspNetCore/cliff.toml new file mode 100644 index 00000000..aa3089f4 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins.AspNetCore/cliff.toml @@ -0,0 +1,82 @@ +# git-cliff ~ configuration file +# https://git-cliff.org/docs/configuration + +[changelog] +# template for the changelog header +header = """ +# Changelog\n +All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines.\n +""" +# template for the changelog body +# https://keats.github.io/tera/docs/#introduction +body = """ +--- +{% if version %}\ + {% if previous.version %}\ + ## [{{ version | trim_start_matches(pat="v") }}]($REPO/compare/{{ previous.version }}..{{ version }}) - {{ timestamp | date(format="%Y-%m-%d") }} + {% else %}\ + ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} + {% endif %}\ +{% else %}\ + ## [unreleased] +{% endif %}\ +{% for group, commits in commits | group_by(attribute="group") %} + ### {{ group | striptags | trim | upper_first }} + {% for commit in commits + | filter(attribute="scope") + | sort(attribute="scope") %} + - **({{commit.scope}})**{% if commit.breaking %} [**breaking**]{% endif %} \ + {{ commit.message }} - ([{{ commit.id | truncate(length=7, end="") }}]($REPO/commit/{{ commit.id }})) - {{ commit.author.name }} + {%- endfor -%} + {% raw %}\n{% endraw %}\ + {%- for commit in commits %} + {%- if commit.scope -%} + {% else -%} + - {% if commit.breaking %} [**breaking**]{% endif %}\ + {{ commit.message }} - ([{{ commit.id | truncate(length=7, end="") }}]($REPO/commit/{{ commit.id }})) - {{ commit.author.name }} + {% endif -%} + {% endfor -%} +{% endfor %}\n +""" +# template for the changelog footer +footer = """ + +""" +# remove the leading and trailing whitespace from the templates +trim = true +# postprocessors +postprocessors = [ + { pattern = '\$REPO', replace = "https://github.com/yiyungent/PluginCore" }, # replace repository URL +] + +[git] +# parse the commits based on https://www.conventionalcommits.org +conventional_commits = true +# filter out the commits that are not conventional +filter_unconventional = true +# process each line of a commit as an individual commit +split_commits = false +# regex for preprocessing the commit messages +commit_preprocessors = [ + # { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/orhun/git-cliff/issues/${2}))"}, # replace issue numbers +] +# regex for parsing and grouping commits +commit_parsers = [ + { message = "^feat", group = "Features" }, + { message = "^fix", group = "Bug Fixes" }, + { message = "^doc", group = "Documentation" }, + { message = "^perf", group = "Performance" }, + { message = "^refactor", group = "Refactoring" }, + { message = "^style", group = "Style" }, + { message = "^revert", group = "Revert" }, + { message = "^test", group = "Tests" }, + { message = "^chore\\(version\\):", skip = true }, + { message = "^chore", group = "Miscellaneous Chores" }, + { body = ".*security", group = "Security" }, +] +# filter out the commits that are not matched by commit parsers +filter_commits = false +# sort the tags topologically +topo_order = false +# sort the commits inside sections by oldest/newest order +sort_commits = "oldest" diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins.AspNetCore/nuget-pack.ps1 b/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins.AspNetCore/nuget-pack.ps1 new file mode 100644 index 00000000..c3c10d7a --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins.AspNetCore/nuget-pack.ps1 @@ -0,0 +1 @@ +dotnet pack -c Release \ No newline at end of file diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins/BasePlugin.cs b/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins/BasePlugin.cs new file mode 100644 index 00000000..d7c4c6b6 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins/BasePlugin.cs @@ -0,0 +1,67 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Generic; +using System.Text; + +namespace PluginCore.IPlugins +{ + public abstract class BasePlugin : IPlugin + { + /// + /// 在启用插件之后: 这时插件Assemblies已被加载(插件上下文已load) + /// + /// + /// 当 IsSuccess 为 False, 主程序之后会回滚插件状态: (1)unload插件上下文 (2)更新plugin.config.json,标记为禁用状态 + /// + public virtual (bool IsSuccess, string Message) AfterEnable() + { + return (true, "启用成功"); + } + + /// + /// 在禁用插件之前: 这时插件Assemblies还未被释放(插件上下文未Unload) + /// + /// + /// 只有当 IsSuccess 为 True, 主程序之后才会释放插件上下文, 并标记为已禁用 + /// 当 IsSuccess 为 False, 主程序不会释放插件上下文,也不会标记为禁用, 插件维持原状态 + /// + public virtual (bool IsSuccess, string Message) BeforeDisable() + { + return (true, "禁用成功"); + } + + /// + /// TODO: 更新未做 + /// + /// + /// + /// + public virtual (bool IsSuccess, string Message) Update(string currentVersion, string targetVersion) + { + return (true, "更新成功"); + } + + public virtual void AppStart() + { + + } + + public virtual List AppStartOrderDependPlugins + { + get + { + return new List(); + } + } + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins/CHANGELOG.md b/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins/CHANGELOG.md new file mode 100644 index 00000000..1f548fdf --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins/CHANGELOG.md @@ -0,0 +1,141 @@ +# Changelog + +All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. + +--- +## [unreleased] + +### Documentation + +- **(CHANGELOG.md)** update - ([0bbfc89](https://github.com/yiyungent/PluginCore/commit/0bbfc8955b7f6338db2125c78ec250e9eeeadcce)) - github-actions[bot] +- **(CHANGELOG.md)** update - ([4f6b47b](https://github.com/yiyungent/PluginCore/commit/4f6b47b3f86bfce4a8f660166837a7322c568d78)) - github-actions[bot] + +### Miscellaneous Chores + +- **(cliff.toml)** add - ([5614ef0](https://github.com/yiyungent/PluginCore/commit/5614ef024d644349095e19a0016bb23d989b0c90)) - yiyun + +--- +## [PluginCore.IPlugins-v0.9.1](https://github.com/yiyungent/PluginCore/compare/PluginCore.IPlugins-v0.9.0..PluginCore.IPlugins-v0.9.1) - 2023-12-30 + +### Features + +- **(src/**/*.cs)** // License: Apache-2.0 -> // License: GNU LGPLv3 - ([57366d3](https://github.com/yiyungent/PluginCore/commit/57366d3e2afdb8e20e94851aa8a09f1ee61b6d7e)) - yiyun +- **(src/**/*.cs)** // Project: https://moeci.com/PluginCore -> // Project: https://yiyungent.github.io/PluginCore - ([7420480](https://github.com/yiyungent/PluginCore/commit/742048065978c1b8597fab3d52f011db4247fbda)) - yiyun +- **(src/plugincore.iplugins/constants.cs)** add - ([1278d0f](https://github.com/yiyungent/PluginCore/commit/1278d0f4acaa201869e0eb014156e14c6575cd00)) - yiyun +- **(src/plugincore.iplugins/constants.cs)** add: AspNetCoreAuthenticationScheme - ([697fe42](https://github.com/yiyungent/PluginCore/commit/697fe422408eec364075689c60aa9771113e1bd2)) - yiyun +- **(src/plugincore.iplugins/constants.cs)** add: AspNetCoreLanguageCookieName = "language" - ([e3f1196](https://github.com/yiyungent/PluginCore/commit/e3f119655739a510a6804101c4e5d7067719ff86)) - yiyun +- **(src/plugincore.iplugins/constants.cs)** add: AspNetCoreLanguageKey = "PluginCore.Admin.Language" - ([b50fa81](https://github.com/yiyungent/PluginCore/commit/b50fa81fb9efa87ae8048ab1925d3f79ec7c869c)) - yiyun + +### Miscellaneous Chores + +- **(src/plugincore.iplugins/plugincore.iplugins.csproj)** 0.9.0 -> 0.9.1 - ([d842de1](https://github.com/yiyungent/PluginCore/commit/d842de15552e19f7ba8f75e5ef89c68713ef31a5)) - yiyun + +--- +## [PluginCore.IPlugins-v0.9.0](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v1.0.2..PluginCore.IPlugins-v0.9.0) - 2023-02-15 + +### Features + +- **(plugincore.aspnetcore,plugincore.iplugins,plugincore)** 仅保留已启用/已禁用 状态, IPlugin新方法 - ([e843a5b](https://github.com/yiyungent/PluginCore/commit/e843a5ba9fad4e88290c09bb3282b730c44c5a06)) - yiyun + +### Build + +- **(src/plugincore.iplugins/plugincore.iplugins.csproj)** 0.9.0 - ([4f07e99](https://github.com/yiyungent/PluginCore/commit/4f07e99d176421853e276c2a83e84433592f5112)) - yiyun + +--- +## [PluginCore.AspNetCore-v1.0.2](https://github.com/yiyungent/PluginCore/compare/PluginCore.IPlugins-v0.8.0..PluginCore.AspNetCore-v1.0.2) - 2022-04-19 + +### Style + +- add: copyright: *.cs - ([9643dce](https://github.com/yiyungent/PluginCore/commit/9643dce112861a440d63306cb555accbed3d5111)) - yiyun + +--- +## [PluginCore.IPlugins-v0.8.0](https://github.com/yiyungent/PluginCore/compare/PluginCore.IPlugins-v0.7.0..PluginCore.IPlugins-v0.8.0) - 2022-04-16 + +### Miscellaneous Chores + +- **(plugincore.iplugins.csproj)** 0.8.0 - ([853e638](https://github.com/yiyungent/PluginCore/commit/853e63850940aeecc0492bb12da54c548321e408)) - yiyun + +### Refactoring + +- 1.提取出 PluginCore.AspNetCore,PluginCore.IPlugins.AspNetCore 2.提取出更多接口,可自由替换 - ([fffd8d9](https://github.com/yiyungent/PluginCore/commit/fffd8d91c23fd6e4a4d09cbf91975beb3cf7acf0)) - yiyun + +--- +## [PluginCore.IPlugins-v0.7.0](https://github.com/yiyungent/PluginCore/compare/PluginCore.IPlugins-v0.6.1..PluginCore.IPlugins-v0.7.0) - 2022-02-09 + +### Features + +- **(helloworldplugin.cs,iwidgetplugin.cs,plugincore)** add: Plugin Widget - ([0f010e9](https://github.com/yiyungent/PluginCore/commit/0f010e9cb9b11c4ccda51c40656dc5fd82a16a01)) - yiyun + +### Miscellaneous Chores + +- **(plugincore.iplugins.csproj)** 0.7.0 - ([87eda42](https://github.com/yiyungent/PluginCore/commit/87eda427bae83181559de92abaa8241f6e94199a)) - yiyun + +--- +## [PluginCore.IPlugins-v0.6.1](https://github.com/yiyungent/PluginCore/compare/PluginCore.IPlugins-v0.6.0..PluginCore.IPlugins-v0.6.1) - 2021-09-01 + +### Features + +- **(pluginsettingsmodelfactory.cs,plugincore.iplugins.csproj)** remove: Newtonsoft.Json - ([84db1d3](https://github.com/yiyungent/PluginCore/commit/84db1d3f2bf9bae71320883b4c92f7e0f565bf15)) - yiyun + +### Miscellaneous Chores + +- **(plugincore.iplugins.csproj)** 0.6.1 - ([97e88ed](https://github.com/yiyungent/PluginCore/commit/97e88edeacd7b2526f5899db67d66165eb3f4dc9)) - yiyun + +--- +## [PluginCore.IPlugins-v0.6.0](https://github.com/yiyungent/PluginCore/compare/PluginCore.IPlugins-v0.5.0..PluginCore.IPlugins-v0.6.0) - 2021-08-21 + +### Features + +- **(testtimejobplugin,plugincore.iplugins,plugincore)** timeJobPlugin 相关 - ([55d4f4c](https://github.com/yiyungent/PluginCore/commit/55d4f4ca7ddd9738216b9434ad1c30ef75f06471)) - yiyun + +### Miscellaneous Chores + +- **(plugincore.iplugins.csproj)** 0.6.0 - ([402abb3](https://github.com/yiyungent/PluginCore/commit/402abb38d25c8677b671e8e4ac3aa3f08fb33f51)) - yiyun + +--- +## [PluginCore.IPlugins-v0.5.0](https://github.com/yiyungent/PluginCore/compare/PluginCore.IPlugins-v0.4.0..PluginCore.IPlugins-v0.5.0) - 2021-08-21 + +### Features + +- **(plugins,plugincore.iplugins,plugincore)** add: order, add: PluginApplicationBuilderManager - ([5e4a5f4](https://github.com/yiyungent/PluginCore/commit/5e4a5f46a4eb3aaca5d978fc1e695d0849e11e5c)) - yiyun + +### Miscellaneous Chores + +- **(plugincore.iplugins.csproj)** 0.5.0 - ([60eaa08](https://github.com/yiyungent/PluginCore/commit/60eaa08c68e46668d9a6d83b2b7664c6843fadd3)) - yiyun + +--- +## [PluginCore.IPlugins-v0.4.0](https://github.com/yiyungent/PluginCore/compare/PluginCore.IPlugins-v0.2.0..PluginCore.IPlugins-v0.4.0) - 2021-08-20 + +### Features + +- **(istartupplugin.cs,istartupxplugin.cs)** 添加注释 - ([d4519b5](https://github.com/yiyungent/PluginCore/commit/d4519b54e9df931c6e75d9ca59742edc5f3185ac)) - yiyun +- **(plugincore)** pluginContentFilterMiddleware, IContentFilterPlugin - ([2597e9c](https://github.com/yiyungent/PluginCore/commit/2597e9c054bde134f9f250071347990be59e8d37)) - yiyun +- **(plugincore,/plugincore.iplugins)** pluginHttpEndFilter - ([c0cd458](https://github.com/yiyungent/PluginCore/commit/c0cd4581df72cdb9f4f678a531e7f04980c9695d)) - yiyun +- **(plugincore,plugincore.iplugins,helloworldplugin)** iStartupXPlugin: 运行时 Configure(app) - ([0d18a6f](https://github.com/yiyungent/PluginCore/commit/0d18a6f9949faa1e92f1d20da35689e8e153bac1)) - yiyun +- **(plugincore.iplugins)** iStartupPlugin.cs, PluginCore.IPlugins.csproj - ([4459fbe](https://github.com/yiyungent/PluginCore/commit/4459fbe5e2cbe369519b7010a7b7d6d4600738cf)) - yiyun +- **(plugincore.iplugins.csproj)** 0.3.0 - ([c8000be](https://github.com/yiyungent/PluginCore/commit/c8000bec4800826afa5db37edfb095a945231591)) - yiyun +- 生成注释xml: PluginCore.IPlugins,PluginCore - ([5878148](https://github.com/yiyungent/PluginCore/commit/5878148244344f412e75fe9446824dd99ca2de47)) - yiyun + +### Miscellaneous Chores + +- **(plugincore.iplugins)** 注释 - ([4fe65ce](https://github.com/yiyungent/PluginCore/commit/4fe65ce4e731e1a67d35f2c202239f062fe45adc)) - yiyun +- **(plugincore.iplugins.csproj)** 0.4.0 - ([a6e8851](https://github.com/yiyungent/PluginCore/commit/a6e8851b75dabb8ca68d8e14124a1332e7c13ad7)) - yiyun + +--- +## [PluginCore.IPlugins-v0.2.0](https://github.com/yiyungent/PluginCore/compare/PluginCore.IPlugins-v0.1.0..PluginCore.IPlugins-v0.2.0) - 2021-08-10 + +### Features + +- **(plugincore.iplugins.csproj)** 0.2.0 - ([d7fb02f](https://github.com/yiyungent/PluginCore/commit/d7fb02fe481e1b2d20a7f7b34f0fa50e95240059)) - yiyun +- plugin 支持加载插件 wwwroot 文件夹下的 html前端等 - ([273f9a4](https://github.com/yiyungent/PluginCore/commit/273f9a44c8727675f60d364fcf59a373958b3575)) - yiyun + +--- +## [PluginCore.IPlugins-v0.1.0] - 2021-08-08 + +### Features + +- pluginCore.IPlugins, plugins: HelloWorldPlugin - ([1e81de2](https://github.com/yiyungent/PluginCore/commit/1e81de2107394f527a94ec5d4c2ae6853d2d5526)) - yiyun +- pluginCore, plugins/HelloWorldPlugin - ([5141afd](https://github.com/yiyungent/PluginCore/commit/5141afded8feba94af581d6132fccb87aafa516c)) - yiyun +- nuget config, v0.1.0 - ([fffc419](https://github.com/yiyungent/PluginCore/commit/fffc419480481b632340eb4e42a0b608c5fff144)) - yiyun + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins/Constants.cs b/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins/Constants.cs new file mode 100644 index 00000000..6e2b2f6a --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins/Constants.cs @@ -0,0 +1,26 @@ +using System; + +namespace PluginCore.IPlugins +{ + public class Constants + { + public const string AspNetCoreAuthenticationClaimType = "PluginCore.Admin.Token"; + + public const string AspNetCoreAuthenticationScheme = "PluginCore.Admin.Authentication"; + + public const string AspNetCoreAuthorizationPolicyName = "PluginCore.Admin"; + + public const string AspNetCoreAuthorizationTokenCookieName = "PluginCore.Admin.Token"; + + /// + /// zh + /// en + /// + public const string AspNetCoreLanguageCookieName = "language"; + + /// + /// httpContext.Items[Constants.AspNetCoreLanguageKey] + /// + public const string AspNetCoreLanguageKey = "PluginCore.Admin.Language"; + } +} diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins/IPlugin.cs b/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins/IPlugin.cs new file mode 100644 index 00000000..b5e2de54 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins/IPlugin.cs @@ -0,0 +1,57 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Generic; +using System.Text; + +namespace PluginCore.IPlugins +{ + public interface IPlugin + { + /// + /// 在启用插件之后: 这时插件Assemblies已被加载(插件上下文已load) + /// + /// + /// 当 IsSuccess 为 False, 主程序之后会回滚插件状态: (1)unload插件上下文 (2)更新plugin.config.json,标记为禁用状态 + /// + (bool IsSuccess, string Message) AfterEnable(); + + /// + /// 在禁用插件之前: 这时插件Assemblies还未被释放(插件上下文未Unload) + /// + /// + /// 只有当 IsSuccess 为 True, 主程序之后才会释放插件上下文, 并标记为已禁用 + /// 当 IsSuccess 为 False, 主程序不会释放插件上下文,也不会标记为禁用, 插件维持原状态 + /// + (bool IsSuccess, string Message) BeforeDisable(); + + /// + /// TODO: 更新未做 + /// + /// + /// + /// + (bool IsSuccess, string Message) Update(string currentVersion, string targetVersion); + + /// + /// 主应用程序启动时 + /// + void AppStart(); + + /// + /// 启动顺序: 此插件 所依赖的前置插件 + /// + /// + List AppStartOrderDependPlugins { get; } + + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins/IPlugins/ITimeJobPlugin.cs b/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins/IPlugins/ITimeJobPlugin.cs new file mode 100644 index 00000000..5cb88ec8 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins/IPlugins/ITimeJobPlugin.cs @@ -0,0 +1,31 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace PluginCore.IPlugins +{ + /// + /// 定时任务 + /// + public interface ITimeJobPlugin : IPlugin + { + /// + /// 间隔秒数 + /// + long SecondsPeriod { get; } + + Task ExecuteAsync(); + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins/IPlugins/IWidgetPlugin.cs b/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins/IPlugins/IWidgetPlugin.cs new file mode 100644 index 00000000..fae983bd --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins/IPlugins/IWidgetPlugin.cs @@ -0,0 +1,26 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace PluginCore.IPlugins +{ + public interface IWidgetPlugin : IPlugin + { + // 这种方式限定: 不合适, 一个插件, 可能需要注入多个地方 + //string WidgetKey { get; } + + Task Widget(string widgetKey, params string[] extraPars); + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins/Infrastructure/PluginPathProvider.cs b/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins/Infrastructure/PluginPathProvider.cs new file mode 100644 index 00000000..041025c0 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins/Infrastructure/PluginPathProvider.cs @@ -0,0 +1,216 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Reflection; + +namespace PluginCore +{ + public class PluginPathProvider + { + + static PluginPathProvider() + { + // 初始化插件目录 + string appDataDir = Path.Combine(Directory.GetCurrentDirectory(), "App_Data"); + if (!Directory.Exists(appDataDir)) + { + Directory.CreateDirectory(appDataDir); + } + + string pluginConfigJsonFilePath = Path.Combine(Directory.GetCurrentDirectory(), "App_Data", "plugin.config.json"); + string pluginConfigJson = "{\"EnabledPlugins\":[],\"DisabledPlugins\":[],\"UninstalledPlugins\":[]}"; + if (!File.Exists(pluginConfigJsonFilePath)) + { + File.WriteAllText(pluginConfigJsonFilePath, pluginConfigJson, System.Text.Encoding.UTF8); + } + + string tempPluginUploadDir = TempPluginUploadDir(); + if (!Directory.Exists(tempPluginUploadDir)) + { + Directory.CreateDirectory(tempPluginUploadDir); + } + + string pluginsDir = PluginsRootPath(); + if (!Directory.Exists(pluginsDir)) + { + Directory.CreateDirectory(pluginsDir); + } + + string pluginsWwwRootDir = PluginsWwwRootDir(); + if (!Directory.Exists(pluginsWwwRootDir)) + { + Directory.CreateDirectory(pluginsWwwRootDir); + } + + string pluginCoreAdminDir = PluginCoreAdminDir(); + if (!Directory.Exists(pluginCoreAdminDir)) + { + Directory.CreateDirectory(pluginCoreAdminDir); + } + } + + /// + /// 临时插件上传目录路径 + /// eg: F:\Com\me\Repos\Remember.Core\src\Presentation\WebApi\App_Data\TempPluginUpload + /// + /// + public static string TempPluginUploadDir() + { + string tempPluginUploadDir = Path.Combine(Directory.GetCurrentDirectory(), "App_Data", "TempPluginUpload"); + return tempPluginUploadDir; + } + + /// + /// 获取 Plugins 的路径 + /// + /// + public static string PluginsRootPath() + { + string pluginRootPath = Path.Combine(Directory.GetCurrentDirectory(), "Plugins"); + + return pluginRootPath; + } + + /// + /// 获取目标插件文件夹名 + /// + /// 目标插件完整目录路径 + /// + public static string GetPluginFolderNameByDir(string pluginDir) + { + string pluginRootPath = PluginsRootPath(); + string pluginFolderName = pluginDir.Replace(pluginRootPath + Path.DirectorySeparatorChar, ""); + + return pluginFolderName; + } + + /// + /// 所有插件的完整目录路径 + /// + /// + public static IList AllPluginDir() + { + string pluginRootPath = PluginsRootPath(); + string[] pluginDirs = Directory.GetDirectories(pluginRootPath, "*"); + + return pluginDirs; + } + + /// + /// 所有插件的文件夹名 + /// + /// + public static IList AllPluginFolderName() + { + IList pluginFolderNames = new List(); + IList pluginDirs = AllPluginDir(); + foreach (var dir in pluginDirs) + { + string pluginFolderName = GetPluginFolderNameByDir(dir); + pluginFolderNames.Add(pluginFolderName); + } + + return pluginFolderNames; + } + + /// + /// Plugins/{pluginId}/wwwroot + /// + /// + + public static string WwwRootDir(string pluginId) + { + string wwwrootDir = Path.Combine(PluginsRootPath(), pluginId, "wwwroot"); + + return wwwrootDir; + } + + /// + /// Plugins/{currentPluginId}/wwwroot + /// + /// + //public static string CurrentWwwRootDir() + //{ + // string wwwrootDir = Path.Combine(PluginsRootPath(), CurrentPluginId(), "wwwroot"); + + // return wwwrootDir; + //} + + + /// + /// Plugins_wwwroot + /// + /// + public static string PluginsWwwRootDir() + { + string pluginsWwwRootDir = Path.Combine(Directory.GetCurrentDirectory(), "Plugins_wwwroot"); + + return pluginsWwwRootDir; + } + + /// + /// Plugins_wwwroot/pluginId + /// + /// + /// + public static string PluginWwwRootDir(string pluginId) + { + string pluginWwwRootDir = Path.Combine(PluginsWwwRootDir(), pluginId); + + return pluginWwwRootDir; + } + + /// + /// Plugins_wwwroot/currentPluginId + /// + /// + //public static string CurrentPluginWwwRootDir() + //{ + // string pluginId = CurrentPluginId(); + + // string pluginWwwRootDir = PluginWwwRootDir(pluginId); + + // return pluginWwwRootDir; + //} + + public static string PluginCoreAdminDir() + { + string pluginCoreAdminDir = Path.Combine(Directory.GetCurrentDirectory(), "PluginCoreAdmin"); + + return pluginCoreAdminDir; + } + + + /// + /// 规范: + /// PluginId = 插件程序集名 (PluginId.dll) + /// + /// TODO: 无法获取当前 PluginId + /// + /// + //public static string CurrentPluginId() + //{ + // //var ass = Assembly.GetExecutingAssembly(); // 错误: PluginCore.IPlugins + // //var ass = Assembly.GetCallingAssembly(); // 错误: PluginCore.IPlugins + // //var ass = Assembly.GetEntryAssembly(); // 错误: AspNetCore3_1 + + + // string pluginId = ass.GetName().Name; + + // return pluginId; + //} + + + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins/Infrastructure/PluginSettingsModelFactory.cs b/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins/Infrastructure/PluginSettingsModelFactory.cs new file mode 100644 index 00000000..a4fc4b22 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins/Infrastructure/PluginSettingsModelFactory.cs @@ -0,0 +1,165 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using PluginCore.Models; + + +namespace PluginCore +{ + public class PluginSettingsModelFactory + { + // TODO: Linux 文件名下区分大小写, windows不区分, 目前必须为 README.md + private const string SettingsFile = "settings.json"; + + #region 即时读取 + public static T Create + (string pluginId) + where T : PluginSettingsModel + { + PluginSettingsModel rtnModel = new PluginSettingsModel(); + string pluginDir = Path.Combine(PluginPathProvider.PluginsRootPath(), pluginId); + string pluginSettingsFilePath = Path.Combine(pluginDir, SettingsFile); + + if (!File.Exists(pluginSettingsFilePath)) + { + return null; + } + try + { + string settingsStr = File.ReadAllText(pluginSettingsFilePath, Encoding.UTF8); + rtnModel = System.Text.Json.JsonSerializer.Deserialize(settingsStr); + } + catch + { + rtnModel = null; + } + + return rtnModel as T; + } + + public static string Create + (string pluginId) + { + string rtnStr = string.Empty; + string pluginDir = Path.Combine(PluginPathProvider.PluginsRootPath(), pluginId); + string pluginSettingsFilePath = Path.Combine(pluginDir, SettingsFile); + + if (!File.Exists(pluginSettingsFilePath)) + { + return null; + } + try + { + rtnStr = File.ReadAllText(pluginSettingsFilePath, Encoding.UTF8); + } + catch + { + rtnStr = null; + } + + return rtnStr; + } + #endregion + + #region 保存 + public static void Save(T pluginSettingsModel, string pluginId) + where T : PluginSettingsModel + { + if (pluginSettingsModel == null) + { + throw new ArgumentNullException(nameof(pluginSettingsModel)); + } + try + { + string pluginSettingsJsonStr = System.Text.Json.JsonSerializer.Serialize(pluginSettingsModel); + string pluginSettingsFilePath = Path.Combine(PluginPathProvider.PluginsRootPath(), pluginId, SettingsFile); + //File.WriteAllText(pluginSettingsFilePath, pluginSettingsJsonStr, Encoding.UTF8); + // 写的时候加缩进 + File.WriteAllText(pluginSettingsFilePath, ConvertJsonString(pluginSettingsJsonStr), Encoding.UTF8); + } + catch + { } + + } + + public static void Save(string pluginSettingsJsonStr, string pluginId) + { + if (pluginSettingsJsonStr == null) + { + throw new ArgumentNullException(nameof(pluginSettingsJsonStr)); + } + try + { + string pluginSettingsFilePath = Path.Combine(PluginPathProvider.PluginsRootPath(), pluginId, SettingsFile); + //File.WriteAllText(pluginSettingsFilePath, pluginSettingsJsonStr, Encoding.UTF8); + // 写的时候加缩进 + File.WriteAllText(pluginSettingsFilePath, ConvertJsonString(pluginSettingsJsonStr), Encoding.UTF8); + } + catch + { } + + } + #endregion + + #region 格式化JSON字符串 + private static string ConvertJsonString(string str) + { + //格式化json字符串 + #region 使用Newtonsoft.Json格式化 JSON字符串 + //Newtonsoft.Json.JsonSerializer serializer = new Newtonsoft.Json.JsonSerializer(); + //TextReader tr = new StringReader(str); + //Newtonsoft.Json.JsonTextReader jtr = new Newtonsoft.Json.JsonTextReader(tr); + //object obj = serializer.Deserialize(jtr); + //if (obj != null) + //{ + // StringWriter textWriter = new StringWriter(); + // Newtonsoft.Json.JsonTextWriter jsonWriter = new Newtonsoft.Json.JsonTextWriter(textWriter) + // { + // Formatting = Newtonsoft.Json.Formatting.Indented, + // Indentation = 4, + // IndentChar = ' ' + // }; + // serializer.Serialize(jsonWriter, obj); + // return textWriter.ToString(); + //} + //else + //{ + // return str; + //} + #endregion + + #region 使用 System.Text.Json 格式化 JSON字符串 + // https://blog.csdn.net/essity/article/details/84644510 + System.Text.Json.JsonSerializerOptions options = new System.Text.Json.JsonSerializerOptions(); + // 设置支持中文的unicode编码: 这样就不会自动转码,而是原样展现 + options.Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping; + // 启用缩进设置 + options.WriteIndented = true; + + // 注意: object 不会丢失json数据, 但不能使用 dynamic, 会报编译错误 + object jsonObj = System.Text.Json.JsonSerializer.Deserialize(str); + + // Error CS0656 Missing compiler required member 'Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo.Create' + // dynamic jsonObj = System.Text.Json.JsonSerializer.Deserialize(str); + + string rtnStr = System.Text.Json.JsonSerializer.Serialize(jsonObj, options); + + return rtnStr; + #endregion + } + #endregion + + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins/Interfaces/IPluginFinder.cs b/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins/Interfaces/IPluginFinder.cs new file mode 100644 index 00000000..6b757c65 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins/Interfaces/IPluginFinder.cs @@ -0,0 +1,79 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Generic; +using System.Text; +using PluginCore.IPlugins; + +namespace PluginCore.Interfaces +{ + public interface IPluginFinder + { + #region 实现了指定接口或类型 的启用插件 + + /// + /// 实现了指定接口或类型 的启用插件 + /// + /// 可以是一个接口,一个抽象类,一个普通实现类, 只要实现了 即可 + /// + IEnumerable EnablePlugins() + where TPlugin : IPlugin; // BasePlugin + + /// + /// 实现了指定接口或类型 的启用插件 + /// + /// 可以是一个接口,一个抽象类,一个普通实现类, 只要实现了 即可 + /// + IEnumerable<(TPlugin PluginInstance, string PluginId)> EnablePluginsFull() + where TPlugin : IPlugin; // BasePlugin + + /// + /// 所有启用的插件 的 PluginId + /// + /// + IEnumerable EnablePluginIds() + where TPlugin : IPlugin; // BasePlugin + #endregion + + #region 所有启用的插件 + + /// + /// 所有启用的插件 + /// + /// + IEnumerable EnablePlugins(); + + /// + /// 所有启用的插件 + /// + /// + IEnumerable<(IPlugin PluginInstance, string PluginId)> EnablePluginsFull(); + + /// + /// 所有启用的插件 的 PluginId + /// + /// + IEnumerable EnablePluginIds(); + #endregion + + #region 获取指定 pluginId 的启用插件 + + /// + /// 获取指定 pluginId 的启用插件 + /// + /// + /// 1.插件未启用返回null, 2.找不到此插件上下文返回null 3.找不到插件主dll返回null 4.插件主dll中找不到实现了IPlugin的Type返回null, 5.无法实例化插件返回null + IPlugin Plugin(string pluginId); + #endregion + + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins/Interfaces/IPluginManager.cs b/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins/Interfaces/IPluginManager.cs new file mode 100644 index 00000000..091bd128 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins/Interfaces/IPluginManager.cs @@ -0,0 +1,30 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Text; + +namespace PluginCore.Interfaces +{ + public interface IPluginManager + { + void LoadPlugin(string pluginId); + + void UnloadPlugin(string pluginId); + /// + /// 加载插件程序集 + /// + /// + Assembly GetPluginAssembly(string pluginId); + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins/Models/PluginSettingsModel.cs b/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins/Models/PluginSettingsModel.cs new file mode 100644 index 00000000..7e01447b --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins/Models/PluginSettingsModel.cs @@ -0,0 +1,27 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Generic; +using System.Text; + +namespace PluginCore.Models +{ + /// + /// 插件设置模型 + /// 对应插件目录下 settings.json + /// 插件开发者自己的插件设置模型 必须继承该类 + /// + public class PluginSettingsModel + { + + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins/PluginCore.IPlugins.csproj b/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins/PluginCore.IPlugins.csproj new file mode 100644 index 00000000..ee83e6df --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins/PluginCore.IPlugins.csproj @@ -0,0 +1,29 @@ + + + + net8.0;net9.0 + PluginCore.IPlugins + 0.9.1 + yiyun + yiyun + PluginCore 插件开发包 + Copyright (c) 2021-present yiyun + https://github.com/yiyungent/PluginCore + https://github.com/yiyungent/PluginCore/blob/main/LICENSE + PluginCore PluginCore.IPlugin + true + + + + + bin\Release\netstandard2.0\PluginCore.IPlugins.xml + + + + + + + + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins/cliff.toml b/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins/cliff.toml new file mode 100644 index 00000000..aa3089f4 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins/cliff.toml @@ -0,0 +1,82 @@ +# git-cliff ~ configuration file +# https://git-cliff.org/docs/configuration + +[changelog] +# template for the changelog header +header = """ +# Changelog\n +All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines.\n +""" +# template for the changelog body +# https://keats.github.io/tera/docs/#introduction +body = """ +--- +{% if version %}\ + {% if previous.version %}\ + ## [{{ version | trim_start_matches(pat="v") }}]($REPO/compare/{{ previous.version }}..{{ version }}) - {{ timestamp | date(format="%Y-%m-%d") }} + {% else %}\ + ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} + {% endif %}\ +{% else %}\ + ## [unreleased] +{% endif %}\ +{% for group, commits in commits | group_by(attribute="group") %} + ### {{ group | striptags | trim | upper_first }} + {% for commit in commits + | filter(attribute="scope") + | sort(attribute="scope") %} + - **({{commit.scope}})**{% if commit.breaking %} [**breaking**]{% endif %} \ + {{ commit.message }} - ([{{ commit.id | truncate(length=7, end="") }}]($REPO/commit/{{ commit.id }})) - {{ commit.author.name }} + {%- endfor -%} + {% raw %}\n{% endraw %}\ + {%- for commit in commits %} + {%- if commit.scope -%} + {% else -%} + - {% if commit.breaking %} [**breaking**]{% endif %}\ + {{ commit.message }} - ([{{ commit.id | truncate(length=7, end="") }}]($REPO/commit/{{ commit.id }})) - {{ commit.author.name }} + {% endif -%} + {% endfor -%} +{% endfor %}\n +""" +# template for the changelog footer +footer = """ + +""" +# remove the leading and trailing whitespace from the templates +trim = true +# postprocessors +postprocessors = [ + { pattern = '\$REPO', replace = "https://github.com/yiyungent/PluginCore" }, # replace repository URL +] + +[git] +# parse the commits based on https://www.conventionalcommits.org +conventional_commits = true +# filter out the commits that are not conventional +filter_unconventional = true +# process each line of a commit as an individual commit +split_commits = false +# regex for preprocessing the commit messages +commit_preprocessors = [ + # { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/orhun/git-cliff/issues/${2}))"}, # replace issue numbers +] +# regex for parsing and grouping commits +commit_parsers = [ + { message = "^feat", group = "Features" }, + { message = "^fix", group = "Bug Fixes" }, + { message = "^doc", group = "Documentation" }, + { message = "^perf", group = "Performance" }, + { message = "^refactor", group = "Refactoring" }, + { message = "^style", group = "Style" }, + { message = "^revert", group = "Revert" }, + { message = "^test", group = "Tests" }, + { message = "^chore\\(version\\):", skip = true }, + { message = "^chore", group = "Miscellaneous Chores" }, + { body = ".*security", group = "Security" }, +] +# filter out the commits that are not matched by commit parsers +filter_commits = false +# sort the tags topologically +topo_order = false +# sort the commits inside sections by oldest/newest order +sort_commits = "oldest" diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins/nuget-pack.ps1 b/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins/nuget-pack.ps1 new file mode 100644 index 00000000..c3c10d7a --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore.IPlugins/nuget-pack.ps1 @@ -0,0 +1 @@ +dotnet pack -c Release \ No newline at end of file diff --git a/Admin.NET/Plugins/PluginCore/PluginCore/CHANGELOG.md b/Admin.NET/Plugins/PluginCore/PluginCore/CHANGELOG.md new file mode 100644 index 00000000..58d606ec --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore/CHANGELOG.md @@ -0,0 +1,396 @@ +# Changelog + +All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. + +--- +## [unreleased] + +### Documentation + +- **(CHANGELOG.md)** update - ([0bbfc89](https://github.com/yiyungent/PluginCore/commit/0bbfc8955b7f6338db2125c78ec250e9eeeadcce)) - github-actions[bot] +- **(CHANGELOG.md)** update - ([4f6b47b](https://github.com/yiyungent/PluginCore/commit/4f6b47b3f86bfce4a8f660166837a7322c568d78)) - github-actions[bot] + +### Miscellaneous Chores + +- **(cliff.toml)** add - ([5614ef0](https://github.com/yiyungent/PluginCore/commit/5614ef024d644349095e19a0016bb23d989b0c90)) - yiyun + +### Ci + +- **(changelog.yml)** changelog.yml, PluginCore/CHANGELOG.md - ([34ab464](https://github.com/yiyungent/PluginCore/commit/34ab46467101933f3b4b966fbe9c7a21f510494e)) - yiyun + +--- +## [PluginCore-v2.2.5](https://github.com/yiyungent/PluginCore/compare/PluginCore-v2.2.4..PluginCore-v2.2.5) - 2024-04-06 + +### Bug Fixes + +- **(src/plugincore/lmplements/lazypluginloadcontext.cs)** dll 忽略版本搜索 - ([af8e6e2](https://github.com/yiyungent/PluginCore/commit/af8e6e299726abacdfc0eb125339235a5ee2823d)) - yiyun + +### Build + +- **(src/plugincore/plugincore.csproj)** 2.2.4 -> 2.2.5 - ([f2dad72](https://github.com/yiyungent/PluginCore/commit/f2dad724d336902e64cc5d07c7974fe3d08bb0d7)) - yiyun + +--- +## [PluginCore-v2.2.4](https://github.com/yiyungent/PluginCore/compare/PluginCore-v2.2.3..PluginCore-v2.2.4) - 2024-03-14 + +### Features + +- **(src/plugincore/utils/logutil.cs)** add LogCategoryName - ([9e66363](https://github.com/yiyungent/PluginCore/commit/9e66363b207e1b6b10e11e8e934097530280c37f)) - yiyun + +### Build + +- **(src/plugincore/plugincore.csproj)** 2.2.3 -> 2.2.4 - ([0c20c18](https://github.com/yiyungent/PluginCore/commit/0c20c18c452333540cde8a6036b0c2a8f805b376)) - yiyun + +--- +## [PluginCore-v2.2.3](https://github.com/yiyungent/PluginCore/compare/PluginCore-v2.2.2..PluginCore-v2.2.3) - 2023-12-30 + +### Bug Fixes + +- **(src/plugincore/infrastructure/nupkgservice.cs)** 适配 LogUtil.Error - ([1bbd0ad](https://github.com/yiyungent/PluginCore/commit/1bbd0ad8c2b69accf8f19d28c47857844b42f527)) - yiyun + +### Features + +- **(src/**/*.cs)** // License: Apache-2.0 -> // License: GNU LGPLv3 - ([57366d3](https://github.com/yiyungent/PluginCore/commit/57366d3e2afdb8e20e94851aa8a09f1ee61b6d7e)) - yiyun +- **(src/**/*.cs)** // Project: https://moeci.com/PluginCore -> // Project: https://yiyungent.github.io/PluginCore - ([7420480](https://github.com/yiyungent/PluginCore/commit/742048065978c1b8597fab3d52f011db4247fbda)) - yiyun +- **(src/plugincore)** utils/LogUtil.cs, PluginCore.csproj: 与 ILogger 结合, FrameworkReference - ([d638a76](https://github.com/yiyungent/PluginCore/commit/d638a76fbc5733ec19c7dc24450e18b1c5803f70)) - yiyun +- **(src/plugincore)** 适配: LogUtil - ([7e0de48](https://github.com/yiyungent/PluginCore/commit/7e0de488c570ff869948160c8bee9edfb8f4192f)) - yiyun +- **(src/plugincore/lmplements/plugincontextmanager.cs)** logUtil.Error -> LogUtil.Warn - ([d438fad](https://github.com/yiyungent/PluginCore/commit/d438fadc70749cdcff8d22a50c004541d15cb010)) - yiyun +- **(src/plugincore/utils/logutil.cs)** add 非泛型 - ([e108632](https://github.com/yiyungent/PluginCore/commit/e108632414809f496b9bbc9cd45ac73e58d53d36)) - yiyun +- **(src/plugincore/utils/logutil.cs)** 非泛型: 需指定 categoryName - ([3aa0410](https://github.com/yiyungent/PluginCore/commit/3aa0410b97727651659e15e786572bd7335d5963)) - yiyun +- **(src/plugincore/utils/logutil.cs)** add: Warn, support: (Exception ex, string message) - ([897a910](https://github.com/yiyungent/PluginCore/commit/897a910028fd7dce1410c5ea4ad38b2b2a6a7c03)) - yiyun + +### Build + +- **(plugincore.csproj)** 2.2.2 -> 2.2.3 - ([b0838bc](https://github.com/yiyungent/PluginCore/commit/b0838bc9175c1739972f89fb8c65342d16b112c9)) - yiyun + +--- +## [PluginCore-v2.2.2](https://github.com/yiyungent/PluginCore/compare/PluginCore-v2.2.1..PluginCore-v2.2.2) - 2023-12-14 + +### Bug Fixes + +- **(src/plugincore/lmplements/positivepluginloadcontext.cs)** pluginMainDllFilePath 被打开状态即锁定 - ([4c81402](https://github.com/yiyungent/PluginCore/commit/4c814028fdaa377608cf9eb849bc732a1fcc70cc)) - yiyun + +### Miscellaneous Chores + +- **(src/plugincore/plugincore.csproj)** 2.2.1 -> 2.2.2 - ([dcb3dff](https://github.com/yiyungent/PluginCore/commit/dcb3dffb3062f62ae0ba1e3c8115ae8300aaf9ce)) - yiyun + +--- +## [PluginCore-v2.2.1](https://github.com/yiyungent/PluginCore/compare/PluginCore-v2.2.0..PluginCore-v2.2.1) - 2023-08-21 + +### Features + +- **(src/plugincore/lmplements/pluginfinder*)** pluginFinder.cs, PluginFinderV1.cs - ([721ae36](https://github.com/yiyungent/PluginCore/commit/721ae36750281731f3ae10dfed63c44d013cdc1e)) - yiyun +- **(src/plugincore/lmplements/pluginfinder*)** pluginFinderV2,PluginFinder:PluginFinderV2 - ([6c7ad5f](https://github.com/yiyungent/PluginCore/commit/6c7ad5f56a8649bad33779e6fcc9d2a4aa06271a)) - yiyun + +### Build + +- **(src/plugincore/plugincore.csproj)** 2.2.1 - ([12b322a](https://github.com/yiyungent/PluginCore/commit/12b322a58a148dc908fb3924df9bf8a7fbd129f7)) - yiyun + +--- +## [PluginCore-v2.2.0](https://github.com/yiyungent/PluginCore/compare/PluginCore-v2.1.0..PluginCore-v2.2.0) - 2023-02-15 + +### Bug Fixes + +- **(plugincore.aspnetcore,plugincore)** iList EnabledPlugins->List,IList不支持Remove - ([4d5d30e](https://github.com/yiyungent/PluginCore/commit/4d5d30e66c4c28998a7a6ac96bf3ffb25e4872b4)) - yiyun + +### Features + +- **(plugincore)** pluginInfoModel,PluginConfigModelFactory:前置依赖插件:DependPlugins:建立依赖顺序 - ([f5841ce](https://github.com/yiyungent/PluginCore/commit/f5841ce42c296b93e1e34a6ed9c8a84767beb795)) - yiyun +- **(plugincore.aspnetcore,plugincore.iplugins,plugincore)** 仅保留已启用/已禁用 状态, IPlugin新方法 - ([e843a5b](https://github.com/yiyungent/PluginCore/commit/e843a5ba9fad4e88290c09bb3282b730c44c5a06)) - yiyun + +### Miscellaneous Chores + +- **(src/plugincore/utils/dependencysorter.cs)** // Debug.Assert - ([52421fd](https://github.com/yiyungent/PluginCore/commit/52421fd4d150c8c568c63e8d053fceb66d0a7ff2)) - yiyun + +### Build + +- **(src/plugincore/plugincore.csproj)** `2.2.0` - ([11a2043](https://github.com/yiyungent/PluginCore/commit/11a20435ec261dcbb4b10e7a99ec10d2c80743c7)) - yiyun + +--- +## [PluginCore-v2.1.0](https://github.com/yiyungent/PluginCore/compare/PluginCore-v2.0.2..PluginCore-v2.1.0) - 2023-02-14 + +### Features + +- **(src/plugincore/)** iPluginContext.PluginId - ([45b22e0](https://github.com/yiyungent/PluginCore/commit/45b22e006879043a68dc61ad33800c149784d257)) - yiyun +- **(src/plugincore/lmplements/)** lazyPluginLoadContext,PositivePluginLoadContext - ([d7b9918](https://github.com/yiyungent/PluginCore/commit/d7b9918ff86bded2ad65ae3a2b6251f88a3df185)) - yiyun +- **(src/plugincore/lmplements/)** base(name: pluginId),MainAssemblyName,ReferencedAssemblyNames - ([98be798](https://github.com/yiyungent/PluginCore/commit/98be798bfbd3462d19d96f3dd301d2b916240f70)) - yiyun +- **(src/plugincore/lmplements/pluginloadcontext.cs)** pluginLoadContext : LazyPluginLoadContext - ([8e957ab](https://github.com/yiyungent/PluginCore/commit/8e957ab53b9446eab35e6664f2b4397ff07cc950)) - yiyun + +### Build + +- **(src/plugincore/plugincore.csproj)** 2.1.0 - ([1a24c03](https://github.com/yiyungent/PluginCore/commit/1a24c03b5fa28eb0008e0a82d2e9feaba2d22042)) - yiyun + +--- +## [PluginCore-v2.0.2](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v1.0.2..PluginCore-v2.0.2) - 2023-01-12 + +### Bug Fixes + +- **(pluginloadcontext.cs)** b插件依赖A插件时,B插件无法启用 - ([4eb8dac](https://github.com/yiyungent/PluginCore/commit/4eb8daca89d23602b58b104162fc54910fc39f76)) - yiyun + +### Miscellaneous Chores + +- **(plugincore.csproj)** 2.0.2 - ([b3e5522](https://github.com/yiyungent/PluginCore/commit/b3e5522f02e02f66fcdeed35aed3613b78637ab6)) - yiyun + +--- +## [PluginCore.AspNetCore-v1.0.2](https://github.com/yiyungent/PluginCore/compare/PluginCore-v2.0.1..PluginCore.AspNetCore-v1.0.2) - 2022-04-19 + +### Style + +- add: copyright: *.cs - ([9643dce](https://github.com/yiyungent/PluginCore/commit/9643dce112861a440d63306cb555accbed3d5111)) - yiyun + +--- +## [PluginCore-v2.0.1](https://github.com/yiyungent/PluginCore/compare/PluginCore-v1.0.0..PluginCore-v2.0.1) - 2022-04-17 + +### Bug Fixes + +- **(plugincore)** 临时修复由于 PluginContextManager 单例失败 导致的插件信息丢失 - ([fa613b4](https://github.com/yiyungent/PluginCore/commit/fa613b4c46e41c906fe955eb8f62c3f4937795bc)) - yiyun +- **(plugincore)** pluginLoadContext: LoadFromStream: 使用此方法, 就不会导致dll被锁定 - ([8af287a](https://github.com/yiyungent/PluginCore/commit/8af287a44dc0d61db1cb449b7b3225595ac07f03)) - yiyun + +### Features + +- **(plugincore,plugincore.aspnetcore)** aspNetCorePluginManagerBeta,PluginLoadContext,PluginFinder - ([9d65a59](https://github.com/yiyungent/PluginCore/commit/9d65a590e3e0850251f6d815c322c7c5d9c7cf3f)) - yiyun + +### Refactoring + +- **(plugincore.aspnetcore,plugincore)** 未完成 - ([a151bcd](https://github.com/yiyungent/PluginCore/commit/a151bcda125cb7e9b5fe11d44e1389afa7a1db5e)) - yiyun +- **(plugincore.aspnetcore,plugincore)** 重构v2: 未测试 - ([53dde31](https://github.com/yiyungent/PluginCore/commit/53dde31116bd6455d33f7d7006b6fd1430f3694b)) - yiyun +- **(plugincore.aspnetcore,plugincore)** 变量名,属性名,类名规范化 - ([eaadabf](https://github.com/yiyungent/PluginCore/commit/eaadabfd759228da245af1d9bd5b86e557540d28)) - yiyun + +### Build + +- **(plugincore.csproj)** add:net6.0 ; 2.0.0 - ([3608906](https://github.com/yiyungent/PluginCore/commit/3608906e4860f2514541d7fc0a9d187b2bcd3076)) - yiyun +- **(plugincore.csproj)** 2.0.1 - ([ce4cfa8](https://github.com/yiyungent/PluginCore/commit/ce4cfa87525fb6a2fc4c0f87943213ef13fd34bd)) - yiyun + +--- +## [PluginCore-v1.0.0](https://github.com/yiyungent/PluginCore/compare/PluginCore-v0.9.3..PluginCore-v1.0.0) - 2022-04-16 + +### Refactoring + +- 1.提取出 PluginCore.AspNetCore,PluginCore.IPlugins.AspNetCore 2.提取出更多接口,可自由替换 - ([fffd8d9](https://github.com/yiyungent/PluginCore/commit/fffd8d91c23fd6e4a4d09cbf91975beb3cf7acf0)) - yiyun + +### Build + +- **(plugincore.csproj)** 1.0.0 - ([96f6b02](https://github.com/yiyungent/PluginCore/commit/96f6b028b958f415b8013d9786d361f902fa3bea)) - yiyun + +--- +## [PluginCore-v0.9.3](https://github.com/yiyungent/PluginCore/compare/PluginCore-v0.9.2..PluginCore-v0.9.3) - 2022-03-15 + +### Bug Fixes + +- **(plugincore,docs)** 更新 PluginCore Admin 前端: `plugincore-admin-frontend-v0.3.1` - ([a9772bb](https://github.com/yiyungent/PluginCore/commit/a9772bb971d19ec05b982d3f2ef2ddfcbc377e6e)) - yiyun + +--- +## [PluginCore-v0.9.2](https://github.com/yiyungent/PluginCore/compare/PluginCore-v0.9.1..PluginCore-v0.9.2) - 2022-03-09 + +### Bug Fixes + +- **(authorization/accountmanager.cs)** tokenCookieName = "PluginCore.Admin.Token" - ([c643c3b](https://github.com/yiyungent/PluginCore/commit/c643c3bcee7555118f5004aed64c1f73664ada9c)) - yiyun + +--- +## [PluginCore-v0.9.1](https://github.com/yiyungent/PluginCore/compare/PluginCore-v0.9.0..PluginCore-v0.9.1) - 2022-03-08 + +### Bug Fixes + +- **(readme.md,readme_zh.md,releases.md,plugintimejobbackgroundservice.cs,plugincore.csproj)** lock 锁 - ([d233779](https://github.com/yiyungent/PluginCore/commit/d23377993838cdd4fc616b13823964d043b2a526)) - yiyun + +--- +## [PluginCore-v0.9.0](https://github.com/yiyungent/PluginCore/compare/PluginCore-v0.8.6..PluginCore-v0.9.0) - 2022-02-09 + +### Features + +- **(helloworldplugin.cs,iwidgetplugin.cs,plugincore)** add: Plugin Widget - ([0f010e9](https://github.com/yiyungent/PluginCore/commit/0f010e9cb9b11c4ccda51c40656dc5fd82a16a01)) - yiyun +- **(pluginwidgetcontroller.cs)** 1.widgetKey.Trim('"', '\'') 2.Content:text/html - ([27ae842](https://github.com/yiyungent/PluginCore/commit/27ae8422c6105328635328cd9170e5aa13243ad1)) - yiyun + +### Build + +- **(plugincore.csproj)** 0.9.0 - ([8f1ca64](https://github.com/yiyungent/PluginCore/commit/8f1ca6470c8c55eb63930e418a6866cc29a5005e)) - yiyun + +--- +## [PluginCore-v0.8.6](https://github.com/yiyungent/PluginCore/compare/PluginCore-v0.8.5..PluginCore-v0.8.6) - 2022-02-07 + +### Bug Fixes + +- **(appcentercontroller.cs,pluginscontroller.cs,usercontroller.cs)** add: [HttpGet, HttpPost] - ([85d4f1d](https://github.com/yiyungent/PluginCore/commit/85d4f1d60585173158981dd4f0b2c75dbd43bbe2)) - yiyun + +### Documentation + +- **(readme.md,readme_zh.md,releases.md,src/plugincore/plugincore.csproj)** pluginCore-v0.8.6 - ([345f0f3](https://github.com/yiyungent/PluginCore/commit/345f0f32d3896db03efafc3cad06967fd32d40a2)) - yiyun + +--- +## [PluginCore-v0.8.5](https://github.com/yiyungent/PluginCore/compare/PluginCore-v0.8.4..PluginCore-v0.8.5) - 2021-12-16 + +### Miscellaneous Chores + +- **(examples/aspnetcore3_1,plugincore)** pluginsController.cs,DemoModePlugin.csproj - ([f43286a](https://github.com/yiyungent/PluginCore/commit/f43286a10116217d8cb05e1e7dc875669a1159d3)) - yiyun + +### Build + +- **(plugincore)** plugincore-admin-frontend: v0.3.0; PluginCore-v0.8.5 - ([17bdfdb](https://github.com/yiyungent/PluginCore/commit/17bdfdb6026fd8f983a2b685eb9bf03c9504854c)) - yiyun + +--- +## [PluginCore-v0.8.4](https://github.com/yiyungent/PluginCore/compare/PluginCore-v0.8.3..PluginCore-v0.8.4) - 2021-09-01 + +### Build + +- **(plugincore.csproj)** 0.8.4 ; PackageReference: PluginCore.IPlugins: 0.6.1 - ([d359350](https://github.com/yiyungent/PluginCore/commit/d359350e7fa9cad5dbacee1d0207926291a2d4f7)) - yiyun + +--- +## [PluginCore-v0.8.3](https://github.com/yiyungent/PluginCore/compare/PluginCore-v0.8.2..PluginCore-v0.8.3) - 2021-08-25 + +### Bug Fixes + +- **(plugincore)** pluginManager.cs: SkipDlls: 跳过2: 打包进入1个dll 或 打包进 1个exe - ([c0aa1f5](https://github.com/yiyungent/PluginCore/commit/c0aa1f595e33061f33a1bd6fe89d63dbeadbd0f5)) - yiyun + +### Miscellaneous Chores + +- **(plugincore.csproj)** 0.8.3 - ([0904cdc](https://github.com/yiyungent/PluginCore/commit/0904cdc2267bae2671a9e2bd3b08831a4c2eb4b4)) - yiyun + +--- +## [PluginCore-v0.8.2](https://github.com/yiyungent/PluginCore/compare/PluginCore-v0.8.1..PluginCore-v0.8.2) - 2021-08-23 + +### Miscellaneous Chores + +- **(plugincore.csproj)** 0.8.2 - ([11f7757](https://github.com/yiyungent/PluginCore/commit/11f7757747f8990916d60224a8824d54da08d6ef)) - yiyun + +--- +## [PluginCore-v0.8.1](https://github.com/yiyungent/PluginCore/compare/PluginCore-v0.8.0..PluginCore-v0.8.1) - 2021-08-23 + +### Bug Fixes + +- **(plugincorestartupextensions.cs,logutil.cs)** utils.LogUtil.PluginBehavior, apply: IStartupPlugin - ([00bfa94](https://github.com/yiyungent/PluginCore/commit/00bfa94a4de5de34bda970ed4524718f250d0fdf)) - yiyun +- userController.cs: avatar url error; upgrade: frontend - ([c842cb7](https://github.com/yiyungent/PluginCore/commit/c842cb7cce2967c37b80a891689ebad610ae2d62)) - yiyun + +### Features + +- **(pluginfinder.cs)** activatedPlugins - ([9076b29](https://github.com/yiyungent/PluginCore/commit/9076b290bfe365b3e111d2280349f62c37aa5402)) - yiyun + +### Miscellaneous Chores + +- **(plugincore.csproj)** 0.8.1 - ([9f37bbf](https://github.com/yiyungent/PluginCore/commit/9f37bbf7fd8fffcb626d7bb5bb419dc551e91dcc)) - yiyun + +--- +## [PluginCore-v0.8.0](https://github.com/yiyungent/PluginCore/compare/PluginCore-v0.7.0..PluginCore-v0.8.0) - 2021-08-21 + +### Features + +- **(plugincore)** utils.LogUtil.Info - ([4163a2a](https://github.com/yiyungent/PluginCore/commit/4163a2ac7938ac6fc0741bdf7c1b967d72b55ed0)) - yiyun +- **(testtimejobplugin,plugincore.iplugins,plugincore)** timeJobPlugin 相关 - ([55d4f4c](https://github.com/yiyungent/PluginCore/commit/55d4f4ca7ddd9738216b9434ad1c30ef75f06471)) - yiyun + +### Build + +- **(plugincore.csproj)** pluginCore: 0.8.0; PluginCore.IPlugins: 0.6.0 - ([7213c6d](https://github.com/yiyungent/PluginCore/commit/7213c6d716b8d2191eb102aed268cc3c788a8721)) - yiyun + +--- +## [PluginCore-v0.7.0](https://github.com/yiyungent/PluginCore/compare/PluginCore-v0.6.0..PluginCore-v0.7.0) - 2021-08-21 + +### Features + +- **(plugins,plugincore.iplugins,plugincore)** add: order, add: PluginApplicationBuilderManager - ([5e4a5f4](https://github.com/yiyungent/PluginCore/commit/5e4a5f46a4eb3aaca5d978fc1e695d0849e11e5c)) - yiyun +- **(pluginserviceprovide.cs)** add - ([0eb5a28](https://github.com/yiyungent/PluginCore/commit/0eb5a284f89cdca374660623a937178cfec6ebf1)) - yiyun + +### Build + +- **(plugincore.csproj)** 0.7.0, PluginCore.IPlugins: 0.5.0 - ([2992023](https://github.com/yiyungent/PluginCore/commit/2992023e688016d23f99ccf57e3086ff9c9af338)) - yiyun + +--- +## [PluginCore-v0.6.0](https://github.com/yiyungent/PluginCore/compare/PluginCore-v0.5.1..PluginCore-v0.6.0) - 2021-08-20 + +### Features + +- **(plugincore,plugincore.iplugins,helloworldplugin)** iStartupXPlugin: 运行时 Configure(app) - ([0d18a6f](https://github.com/yiyungent/PluginCore/commit/0d18a6f9949faa1e92f1d20da35689e8e153bac1)) - yiyun + +### Build + +- **(plugincore.csproj)** 0.6.0 - ([98a4c70](https://github.com/yiyungent/PluginCore/commit/98a4c708234b2379cbcd00868b11e209c275baa0)) - yiyun + +--- +## [PluginCore-v0.5.1](https://github.com/yiyungent/PluginCore/compare/PluginCore-v0.4.0..PluginCore-v0.5.1) - 2021-08-19 + +### Bug Fixes + +- **(src/plugincore/controllers/pluginscontroller.cs)** 启用插件: 启用失败时 回滚 - ([49d46c7](https://github.com/yiyungent/PluginCore/commit/49d46c79c3601df3b4899aee38296500c942854b)) - yiyun +- **(src/plugincore/pluginmanager.cs)** 当插件引用dll时, 插件Controller立即使用引用dll时,报错:找不到引用dll - ([d8c79c5](https://github.com/yiyungent/PluginCore/commit/d8c79c5681dc0a2e4a3d36565075c243b6ce44c7)) - yiyun + +### Features + +- **(plugincore)** pluginContentFilterMiddleware, IContentFilterPlugin - ([2597e9c](https://github.com/yiyungent/PluginCore/commit/2597e9c054bde134f9f250071347990be59e8d37)) - yiyun +- **(plugincore,/plugincore.iplugins)** pluginHttpEndFilter - ([c0cd458](https://github.com/yiyungent/PluginCore/commit/c0cd4581df72cdb9f4f678a531e7f04980c9695d)) - yiyun +- localEmbedded: PluginCoreAdmin -> package.json - ([f8be0d2](https://github.com/yiyungent/PluginCore/commit/f8be0d2ce86b26d9f00cf67845daed2853f285f6)) - yiyun +- 生成注释xml: PluginCore.IPlugins,PluginCore - ([5878148](https://github.com/yiyungent/PluginCore/commit/5878148244344f412e75fe9446824dd99ca2de47)) - yiyun +- update: IStartupPlugin success, fix: Plugin.Enable - ([ad950b2](https://github.com/yiyungent/PluginCore/commit/ad950b2802f60da3f950fd3eaf7bf1eee24c84b6)) - yiyun + +### Miscellaneous Chores + +- **(plugincore.csproj)** 0.4.0 -> 0.5.0 - ([658dde4](https://github.com/yiyungent/PluginCore/commit/658dde4156316f340fe4a28df5a8d76c895de872)) - yiyun +- **(src/plugincore/plugincore.csproj)** 0.5.1 - ([f337786](https://github.com/yiyungent/PluginCore/commit/f337786fa11addfbaed948085a946fb1205c4e8e)) - yiyun + +### Build + +- **(plugincore.csproj)** pluginCore.IPlugins: 0.3.0 - ([f29b998](https://github.com/yiyungent/PluginCore/commit/f29b998c926a09fddbfe6c47208b624a87b94b0e)) - yiyun + +--- +## [PluginCore-v0.4.0](https://github.com/yiyungent/PluginCore/compare/PluginCore-v0.3.1..PluginCore-v0.4.0) - 2021-08-16 + +### Features + +- **(plugincore.csproj)** 0.4.0 - ([2ef0a92](https://github.com/yiyungent/PluginCore/commit/2ef0a924d5ca4c21245add4d3c05c13909100551)) - yiyun +- 支持 nupkg 格式插件 - ([1aa1d5f](https://github.com/yiyungent/PluginCore/commit/1aa1d5f45208fe273637548cd69f96a770c32c28)) - yiyun +- 支持嵌入式 前端 (打包进dll) - ([7e08cb3](https://github.com/yiyungent/PluginCore/commit/7e08cb33868890227f11645c2b8d4dd022318c94)) - yiyun + +--- +## [PluginCore-v0.3.1](https://github.com/yiyungent/PluginCore/compare/PluginCore-v0.3.0..PluginCore-v0.3.1) - 2021-08-10 + +### Bug Fixes + +- authentication: 401 - ([01c5d04](https://github.com/yiyungent/PluginCore/commit/01c5d04e0a11e53c402076a25c1780fc728ccbc3)) - yiyun + +### Features + +- **(plugincore.csproj)** 0.3.1 - ([001ef67](https://github.com/yiyungent/PluginCore/commit/001ef674190418354bf964d7bf042d66717a0828)) - yiyun +- **(plugincoreconfig.cs)** @0.1.3/dist-cdn - ([da3fb7d](https://github.com/yiyungent/PluginCore/commit/da3fb7d5986582c1fde3d27e31fd8b135cd881c4)) - yiyun +- logUtil and apply - ([f0ee2e8](https://github.com/yiyungent/PluginCore/commit/f0ee2e8df4ec3ec17048a0f718340b6e0adb7360)) - yiyun + +--- +## [PluginCore-v0.3.0](https://github.com/yiyungent/PluginCore/compare/PluginCore-v0.2.0..PluginCore-v0.3.0) - 2021-08-10 + +### Features + +- **(plugincore.csproj)** 0.3.0 - ([627ef86](https://github.com/yiyungent/PluginCore/commit/627ef866f464edddb1f836fde83e4cac04d6f4a3)) - yiyun +- plugin 支持加载插件 wwwroot 文件夹下的 html前端等 - ([273f9a4](https://github.com/yiyungent/PluginCore/commit/273f9a44c8727675f60d364fcf59a373958b3575)) - yiyun + +--- +## [PluginCore-v0.2.0](https://github.com/yiyungent/PluginCore/compare/PluginCore.IPlugins-v0.1.0..PluginCore-v0.2.0) - 2021-08-09 + +### Bug Fixes + +- pluginCore Admin: avatar url 404: dist-cdn - ([38ca90c](https://github.com/yiyungent/PluginCore/commit/38ca90c5ff8b5138e1751887b9f60a376158eaad)) - yiyun +- fronted -> frontend - ([c41cfdb](https://github.com/yiyungent/PluginCore/commit/c41cfdbfba02fcfe37981d9aa4c8c05b194363de)) - yiyun + +### Features + +- **(plugincore.csproj)** 0.2.0 - ([4a10e5d](https://github.com/yiyungent/PluginCore/commit/4a10e5d5d5a45a3a763569bb6ef2c46d04a373fe)) - yiyun +- **(plugincorehostingstartup)** failure - ([0803d76](https://github.com/yiyungent/PluginCore/commit/0803d7679313ffa0e9c583d0923bff3412f265d5)) - yiyun +- remoteFronted, remove PluginCoreAdmin - ([81f6982](https://github.com/yiyungent/PluginCore/commit/81f698213cee6383da9d8035165d3881b88bc709)) - yiyun +- 保证 PluginCoreAdmin 文件夹存在 - ([2bf2c0e](https://github.com/yiyungent/PluginCore/commit/2bf2c0e42cbafb03af00ff324f1fb637238de441)) - yiyun + +--- +## [PluginCore.IPlugins-v0.1.0] - 2021-08-08 + +### Bug Fixes + +- api url error, config file with init etc - ([9adb655](https://github.com/yiyungent/PluginCore/commit/9adb6551650b8ede28bec086df13023a2b7d9bf6)) - yiyun + +### Features + +- **(authorization)** authorization, Login - ([5b6f9fa](https://github.com/yiyungent/PluginCore/commit/5b6f9fa989d9739c30ef0d1f0186b876cddc5890)) - yiyun +- **(plugincore/plugincoreadmin)** add - ([802ad74](https://github.com/yiyungent/PluginCore/commit/802ad74efd013f34e9c5f7d5ed3eef8574f2c20b)) - yiyun +- **(pluginframeworkstartupextensions.cs)** useStaticFiles: PluginCoreAdmin - ([b0adb8e](https://github.com/yiyungent/PluginCore/commit/b0adb8e1f31912472946c8c76fe05b5ff85a77b4)) - yiyun +- **(pluginframeworkstartupextensions.cs)** pluginFramework -> PluginCore, app.UseDefaultFiles(); - ([17e1587](https://github.com/yiyungent/PluginCore/commit/17e15879076d36d3d2cf6891181cf823fb78c66d)) - yiyun +- add controllers, examples - ([c7e8553](https://github.com/yiyungent/PluginCore/commit/c7e8553b9bbb6d45eac251e1060acb719fd3dac9)) - yiyun +- 自动初始化插件目录 - ([fe3d162](https://github.com/yiyungent/PluginCore/commit/fe3d162e3a5455d308db55f9b260301f10ff4eee)) - yiyun +- pluginCore.IPlugins, plugins: HelloWorldPlugin - ([1e81de2](https://github.com/yiyungent/PluginCore/commit/1e81de2107394f527a94ec5d4c2ae6853d2d5526)) - yiyun +- pluginCoreConfig, PluginCoreConfigFactory - ([6a0dae2](https://github.com/yiyungent/PluginCore/commit/6a0dae2d222d9f0464b8d158eb87f674529af56e)) - yiyun +- pluginCore, plugins/HelloWorldPlugin - ([5141afd](https://github.com/yiyungent/PluginCore/commit/5141afded8feba94af581d6132fccb87aafa516c)) - yiyun +- logout, Login: pretty - ([5fac6a3](https://github.com/yiyungent/PluginCore/commit/5fac6a3939436d58d860e2529be08a26c7a79946)) - yiyun +- nuget config, v0.1.0 - ([fffc419](https://github.com/yiyungent/PluginCore/commit/fffc419480481b632340eb4e42a0b608c5fff144)) - yiyun + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore/Config/PluginCoreConfig.cs b/Admin.NET/Plugins/PluginCore/PluginCore/Config/PluginCoreConfig.cs new file mode 100644 index 00000000..ec9096b1 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore/Config/PluginCoreConfig.cs @@ -0,0 +1,50 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Generic; +using System.Text; + +namespace PluginCore.Config +{ + public class PluginCoreConfig + { + public PluginCoreConfig() + { + this.Admin = new AdminModel(); + } + + public AdminModel Admin { get; set; } + + /// + /// LocalEmbedded + /// LocalFolder + /// RemoteCDN + /// + public string FrontendMode { get; set; } = "LocalEmbedded"; + + public string RemoteFrontend { get; set; } = "https://cdn.jsdelivr.net/gh/yiyungent/plugincore-admin-frontend@0.3.1/dist-cdn"; + + /// + /// 开启后: + /// 1. 页面的 Widget 会显示插件的详细插入点 + /// + public bool PluginWidgetDebug { get; set; } = false; + + public sealed class AdminModel + { + public string UserName { get; set; } = "admin"; + + public string Password { get; set; } = "ABC12345"; + } + + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore/Config/PluginCoreConfigFactory.cs b/Admin.NET/Plugins/PluginCore/PluginCore/Config/PluginCoreConfigFactory.cs new file mode 100644 index 00000000..e42adece --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore/Config/PluginCoreConfigFactory.cs @@ -0,0 +1,70 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Text.Json; +using PluginCore.Models; + +namespace PluginCore.Config +{ + public class PluginCoreConfigFactory + { + private const string FileName = "PluginCore.Config.json"; + + #region 即时读取 + public static PluginCoreConfig Create() + { + PluginCoreConfig pluginCoreConfig = new PluginCoreConfig(); + string pluginCoreConfigFilePath = Path.Combine(Directory.GetCurrentDirectory(), "App_Data", FileName); + string pluginCoreConfigJsonStr = string.Empty; + if (!File.Exists(pluginCoreConfigFilePath)) + { + // 不存在, 则新建初始化默认 + pluginCoreConfigJsonStr = JsonSerializer.Serialize(pluginCoreConfig); + File.WriteAllText(pluginCoreConfigFilePath, pluginCoreConfigJsonStr, Encoding.UTF8); + + return pluginCoreConfig; + } + + pluginCoreConfigJsonStr = File.ReadAllText(pluginCoreConfigFilePath, Encoding.UTF8); + JsonSerializerOptions jsonSerializerOptions = new JsonSerializerOptions(); + jsonSerializerOptions.PropertyNameCaseInsensitive = true; + pluginCoreConfig = JsonSerializer.Deserialize(pluginCoreConfigJsonStr, jsonSerializerOptions); + + return pluginCoreConfig; + } + #endregion + + #region 保存 + public static void Save(PluginCoreConfig pluginCoreConfig) + { + if (pluginCoreConfig == null) + { + throw new ArgumentNullException(nameof(pluginCoreConfig)); + } + try + { + string pluginCoreConfigJsonStr = JsonSerializer.Serialize(pluginCoreConfig); + string pluginCoreConfigFilePath = Path.Combine(Directory.GetCurrentDirectory(), "App_Data", FileName); + File.WriteAllText(pluginCoreConfigFilePath, pluginCoreConfigJsonStr, Encoding.UTF8); + } + catch (Exception ex) + { } + + } + #endregion + + + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore/Infrastructure/NupkgService.cs b/Admin.NET/Plugins/PluginCore/PluginCore/Infrastructure/NupkgService.cs new file mode 100644 index 00000000..5cc174a7 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore/Infrastructure/NupkgService.cs @@ -0,0 +1,148 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace PluginCore.Infrastructure +{ + public class NupkgService + { + /// + /// 将 xxx.nupkg 根据当前环境 解压成 对应 插件目录结构 + /// xxx.zip 形式的插件安装包: 直接解压即可 + /// + /// + /// + /// + /// + public static bool DecomparessFile(string sourceFile, string destinationDirectory = null) + { + bool isDecomparessSuccess = false; + + if (!File.Exists(sourceFile)) + { + throw new FileNotFoundException("要解压的文件不存在", sourceFile); + } + + if (string.IsNullOrWhiteSpace(destinationDirectory)) + { + destinationDirectory = Path.GetDirectoryName(sourceFile); + } + + try + { + // 注意: 之前重命名过: Guid.zip + destinationDirectory = destinationDirectory.Replace(".zip", ""); + + isDecomparessSuccess = Utils.ZipHelper.FastDecomparessFile(sourceFile, destinationDirectory); + if (!isDecomparessSuccess) + { + return isDecomparessSuccess; + } + // 到这里已经解压完成, 开始解析 + + var netVersion = Utils.RuntimeUtil.RuntimeNetVersion; + + // netcoreapp3.1 net5.0 + // dll 文件等 + string libDirPath = Path.Combine(destinationDirectory, "lib"); + string netFolderName = string.Empty; + if (netVersion >= new Version("3.1") && Directory.Exists(Path.Combine(libDirPath, $"netcoreapp{netVersion.Major}.{netVersion.Minor}"))) + { + netFolderName = $"netcoreapp{netVersion.Major}.{netVersion.Minor}"; + } + else if (netVersion.Major >= 5 && Directory.Exists(Path.Combine(libDirPath, $"net{netVersion.Major}.{netVersion.Minor}"))) + { + netFolderName = $"net{netVersion.Major}.{netVersion.Minor}"; + } + else if (Directory.Exists(Path.Combine(libDirPath, "netstandard2.0"))) + { + netFolderName = $"netstandard2.0"; + } + else if (Directory.Exists(Path.Combine(libDirPath, "netstandard2.1"))) + { + netFolderName = $"netstandard2.1"; + } + else + { + throw new Exception("暂不支持 .NET Core 3.1 以下版本, 也不支持 .NET Framework "); + } + // 1. ./lib/netcoreapp3.1 + string libNetDirPath = Path.Combine(libDirPath, netFolderName); + // 移动到插件根目录 + //Directory.Move(libNetDirPath, destinationDirectory); // 错误: 这样移动会导致 包含根目录文件夹名 + Utils.FileUtil.CopyFolder(libNetDirPath, destinationDirectory); + // 只需要这些, 其他删除 + Directory.Delete(libDirPath, true); + + Utils.LogUtil.Info($"加载 nupkg 中 dll: {sourceFile} ; {libNetDirPath}"); + + // 2. 普通文件: 例如 wwwroot + // 2.1 ./content + string contentDirPath = Path.Combine(destinationDirectory, "content"); + bool isExistContentDir = Directory.Exists(contentDirPath); + bool isFinishedContent = false; + if (isExistContentDir) + { + DirectoryInfo dir = new DirectoryInfo(contentDirPath); + bool isExistFile = dir.GetFiles()?.Length >= 1 || dir.GetDirectories()?.Length >= 1; + if (isExistFile) + { + Utils.FileUtil.CopyFolder(contentDirPath, destinationDirectory); + isFinishedContent = true; + + Utils.LogUtil.Info($"加载 nupkg 中 非dll: ./content : {sourceFile} ; {contentDirPath}"); + } + } + else + { + // 2.2 ./contentFiles/any/netFolderName + // 在 ./content 中没有找到文件, 再尝试 此文件夹 + if (!isFinishedContent) + { + string contentFilesDirPath = Path.Combine(destinationDirectory, "contentFiles"); + DirectoryInfo dir = new DirectoryInfo(contentFilesDirPath); + int? childDirLength = dir.GetDirectories()?.Length; + if (childDirLength >= 1) + { + DirectoryInfo anyDir = dir.GetDirectories()[0]; + DirectoryInfo netFolderDir = anyDir.GetDirectories(netFolderName)?.FirstOrDefault(); + if (netFolderDir != null) + { + Utils.FileUtil.CopyFolder(netFolderDir.FullName, destinationDirectory); + + Utils.LogUtil.Info($"加载 nupkg 中 非dll: ./contentFiles/any/netFolderName : {sourceFile} ; {netFolderDir.FullName}"); + } + } + } + } + + + + + + } + catch (Exception ex) + { + Utils.LogUtil.Error(ex, ex.Message); + Utils.LogUtil.Error(ex.InnerException?.ToString() ?? ""); + + throw ex; + } + + return isDecomparessSuccess; + } + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore/Infrastructure/PluginServiceProvide.cs b/Admin.NET/Plugins/PluginCore/PluginCore/Infrastructure/PluginServiceProvide.cs new file mode 100644 index 00000000..fc8ea2be --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore/Infrastructure/PluginServiceProvide.cs @@ -0,0 +1,174 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; + +//namespace PluginCore.Infrastructure +//{ +// /// +// /// The default IServiceProvider. +// /// +// public class PluginServiceProvide : IServiceProvider, IDisposable, IServiceProviderEngineCallback, IAsyncDisposable +// { +// private readonly IServiceProviderEngine _engine; + +// private readonly CallSiteValidator _callSiteValidator; + +// internal ServiceProvider(IEnumerable serviceDescriptors, IServiceProviderEngine engine, ServiceProviderOptions options) +// { +// _engine = engine; + +// if (options.ValidateScopes) +// { +// _engine.InitializeCallback(this); +// _callSiteValidator = new CallSiteValidator(); +// } + +// if (options.ValidateOnBuild) +// { +// List exceptions = null; +// foreach (ServiceDescriptor serviceDescriptor in serviceDescriptors) +// { +// try +// { +// _engine.ValidateService(serviceDescriptor); +// } +// catch (Exception e) +// { +// exceptions = exceptions ?? new List(); +// exceptions.Add(e); +// } +// } + +// if (exceptions != null) +// { +// throw new AggregateException("Some services are not able to be constructed", exceptions.ToArray()); +// } +// } +// } + +// /// +// /// Gets the service object of the specified type. +// /// +// /// The type of the service to get. +// /// The service that was produced. +// public object GetService(Type serviceType) => _engine.GetService(serviceType); + +// /// +// public void Dispose() +// { +// _engine.Dispose(); +// } + +// void IServiceProviderEngineCallback.OnCreate(ServiceCallSite callSite) +// { +// _callSiteValidator.ValidateCallSite(callSite); +// } + +// void IServiceProviderEngineCallback.OnResolve(Type serviceType, IServiceScope scope) +// { +// _callSiteValidator.ValidateResolution(serviceType, scope, _engine.RootScope); +// } + +// /// +// public ValueTask DisposeAsync() +// { +// return _engine.DisposeAsync(); +// } +// } + +// internal interface IServiceProviderEngineCallback +// { +// void OnCreate(ServiceCallSite callSite); +// void OnResolve(Type serviceType, IServiceScope scope); +// } + +// /// +// /// Summary description for ServiceCallSite +// /// +// internal abstract class ServiceCallSite +// { +// protected ServiceCallSite(ResultCache cache) +// { +// Cache = cache; +// } + +// public abstract Type ServiceType { get; } +// public abstract Type ImplementationType { get; } +// public abstract CallSiteKind Kind { get; } +// public ResultCache Cache { get; } + +// public bool CaptureDisposable => +// ImplementationType == null || +// typeof(IDisposable).IsAssignableFrom(ImplementationType) || +// typeof(IAsyncDisposable).IsAssignableFrom(ImplementationType); +// } + + +// internal struct ResultCache +// { +// public static ResultCache None { get; } = new ResultCache(CallSiteResultCacheLocation.None, ServiceCacheKey.Empty); + +// internal ResultCache(CallSiteResultCacheLocation lifetime, ServiceCacheKey cacheKey) +// { +// Location = lifetime; +// Key = cacheKey; +// } + +// public ResultCache(ServiceLifetime lifetime, Type type, int slot) +// { +// Debug.Assert(lifetime == ServiceLifetime.Transient || type != null); + +// switch (lifetime) +// { +// case ServiceLifetime.Singleton: +// Location = CallSiteResultCacheLocation.Root; +// break; +// case ServiceLifetime.Scoped: +// Location = CallSiteResultCacheLocation.Scope; +// break; +// case ServiceLifetime.Transient: +// Location = CallSiteResultCacheLocation.Dispose; +// break; +// default: +// Location = CallSiteResultCacheLocation.None; +// break; +// } +// Key = new ServiceCacheKey(type, slot); +// } + +// public CallSiteResultCacheLocation Location { get; set; } + +// public ServiceCacheKey Key { get; set; } +// } + + +// internal enum CallSiteResultCacheLocation +// { +// Root, +// Scope, +// Dispose, +// None +// } + +// internal interface IServiceProviderEngine : IServiceProvider, IDisposable, IAsyncDisposable +// { +// IServiceScope RootScope { get; } +// void InitializeCallback(IServiceProviderEngineCallback callback); +// void ValidateService(ServiceDescriptor descriptor); +// } + +//} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore/Interfaces/ICollectibleAssemblyLoadContext.cs b/Admin.NET/Plugins/PluginCore/PluginCore/Interfaces/ICollectibleAssemblyLoadContext.cs new file mode 100644 index 00000000..2095032e --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore/Interfaces/ICollectibleAssemblyLoadContext.cs @@ -0,0 +1,24 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Generic; +using System.Text; + +namespace PluginCore.Interfaces +{ + /// + /// 暂时无用, 仅作为约束标记 + /// + public interface ICollectibleAssemblyLoadContext + { + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore/Interfaces/IPluginContext.cs b/Admin.NET/Plugins/PluginCore/PluginCore/Interfaces/IPluginContext.cs new file mode 100644 index 00000000..23535806 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore/Interfaces/IPluginContext.cs @@ -0,0 +1,42 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Runtime.Loader; +using System.Text; + +namespace PluginCore.Interfaces +{ + /// + /// 每个插件的所有 Assembly 打包到此中 + /// + public interface IPluginContext + { + string PluginId { get; } + + IEnumerable Assemblies { get; } + + Assembly LoadFromAssemblyName(AssemblyName assemblyName); + + void Unload(); + + + /// + /// 暂时用不到 + /// + /// + /// + Assembly LoadFromStream(Stream assembly); + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore/Interfaces/IPluginContextManager.cs b/Admin.NET/Plugins/PluginCore/PluginCore/Interfaces/IPluginContextManager.cs new file mode 100644 index 00000000..b6b654e2 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore/Interfaces/IPluginContextManager.cs @@ -0,0 +1,32 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using PluginCore.lmplements; +using System; +using System.Collections.Generic; +using System.Runtime.Loader; +using System.Text; + +namespace PluginCore.Interfaces +{ + public interface IPluginContextManager + { + List All(); + + bool Any(string pluginId); + + void Remove(string pluginId); + + IPluginContext Get(string pluginId); + + void Add(string pluginId, IPluginContext context); + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore/Interfaces/IPluginContextPack.cs b/Admin.NET/Plugins/PluginCore/PluginCore/Interfaces/IPluginContextPack.cs new file mode 100644 index 00000000..5ac7466d --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore/Interfaces/IPluginContextPack.cs @@ -0,0 +1,28 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Generic; +using System.Runtime.Loader; +using System.Text; + +namespace PluginCore.Interfaces +{ + public interface IPluginContextPack + { + /// + /// 将 此插件 打包 到一个 中 + /// + /// + /// + IPluginContext Pack(string pluginId); + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore/Models/PluginConfigModel.cs b/Admin.NET/Plugins/PluginCore/PluginCore/Models/PluginConfigModel.cs new file mode 100644 index 00000000..e25f5b3b --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore/Models/PluginConfigModel.cs @@ -0,0 +1,40 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Generic; +using System.Text; + +namespace PluginCore.Models +{ + /// + /// 所有插件的配置信息模型 + /// 对应 WebApi/App_Data/plugin.config.json + /// ------------- + /// Plugins = 已启用 + 已禁用 + /// 上传放入 Plugins 后, 默认为 已禁用 + /// + public class PluginConfigModel + { + /// + /// 启用的插件列表: PluginID + /// 属于 插件 已安装 + /// + public List EnabledPlugins { get; set; } + + #region ctor + public PluginConfigModel() + { + this.EnabledPlugins = new List(); + } + #endregion + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore/Models/PluginInfoModel.cs b/Admin.NET/Plugins/PluginCore/PluginCore/Models/PluginInfoModel.cs new file mode 100644 index 00000000..a0f92707 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore/Models/PluginInfoModel.cs @@ -0,0 +1,56 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Generic; +using System.Text; + +namespace PluginCore.Models +{ + /// + /// 插件信息模型 + /// 对应插件目录下 info.json + /// 约定: 插件文件夹名=PluginID + /// 约定: 插件文件夹名=插件主程序集(Assembly)名 .dll + /// eg: plugins/payment payment.dll + /// 约定: 插件文件夹下 logo.png 为插件图标 + /// 约定: 插件文件夹下 README.md 为插件说明文件 + /// 约定: 插件文件夹下 settings.json 为插件设置文件 + /// + public class PluginInfoModel + { + public string PluginId { get; set; } + + public string DisplayName { get; set; } + + public string Description { get; set; } + + public string Author { get; set; } + + public string Version { get; set; } + + public IList SupportedVersions { get; set; } + + /// + /// 前置依赖插件 + /// + /// + public IList DependPlugins { get; set; } + + #region Ctor + public PluginInfoModel() + { + this.SupportedVersions = new List(); + this.DependPlugins = new List(); + } + #endregion + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore/Models/PluginInfoResponseModel.cs b/Admin.NET/Plugins/PluginCore/PluginCore/Models/PluginInfoResponseModel.cs new file mode 100644 index 00000000..5cc2114e --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore/Models/PluginInfoResponseModel.cs @@ -0,0 +1,31 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Generic; +using System.Text; + +namespace PluginCore.Models +{ + public class PluginInfoResponseModel : PluginInfoModel + { + /// + /// 插件状态 + /// + public PluginStatus Status { get; set; } + } + + public enum PluginStatus + { + Enabled = 0, + Disabled = 1 + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore/Models/PluginReadmeModel.cs b/Admin.NET/Plugins/PluginCore/PluginCore/Models/PluginReadmeModel.cs new file mode 100644 index 00000000..8c08545b --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore/Models/PluginReadmeModel.cs @@ -0,0 +1,24 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Generic; +using System.Text; + +namespace PluginCore.Models +{ + public class PluginReadmeModel + { + public string PluginId { get; set; } + + public string Content { get; set; } + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore/Models/PluginReadmeResponseModel.cs b/Admin.NET/Plugins/PluginCore/PluginCore/Models/PluginReadmeResponseModel.cs new file mode 100644 index 00000000..7b11a32c --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore/Models/PluginReadmeResponseModel.cs @@ -0,0 +1,21 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Generic; +using System.Text; + +namespace PluginCore.Models +{ + public class PluginReadmeResponseModel : PluginReadmeModel + { + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore/Models/PluginRegistryResponseModel.cs b/Admin.NET/Plugins/PluginCore/PluginCore/Models/PluginRegistryResponseModel.cs new file mode 100644 index 00000000..22462fcc --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore/Models/PluginRegistryResponseModel.cs @@ -0,0 +1,45 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Generic; +using System.Text; + +namespace PluginCore.Models +{ + public class PluginRegistryResponseModel : PluginInfoModel + { + public string DownloadUrl { get; set; } + + /// + /// 此属性值由获取后根据本地插件情况赋值 + /// + public PluginStatus Status { get; set; } + + public enum PluginStatus + { + /// + /// 本地无此 PluginId 的插件 + /// + LocalWithout, + + /// + /// 本地已存在此 PluginId 的插件 + /// + LocalExist + } + } + + public class PluginRegistryDTO + { + + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore/Models/PluginSettingsInputModel.cs b/Admin.NET/Plugins/PluginCore/PluginCore/Models/PluginSettingsInputModel.cs new file mode 100644 index 00000000..65066373 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore/Models/PluginSettingsInputModel.cs @@ -0,0 +1,24 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Generic; +using System.Text; + +namespace PluginCore.Models +{ + public class PluginSettingsInputModel + { + public string PluginId { get; set; } + + public string Data { get; set; } + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore/PluginConfigModelFactory.cs b/Admin.NET/Plugins/PluginCore/PluginCore/PluginConfigModelFactory.cs new file mode 100644 index 00000000..60ba9bfa --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore/PluginConfigModelFactory.cs @@ -0,0 +1,91 @@ +using System.Linq; +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Text.Json; +using PluginCore.Models; + +namespace PluginCore +{ + public class PluginConfigModelFactory + { + #region 即时读取 + public static PluginConfigModel Create() + { + PluginConfigModel pluginConfigModel = new PluginConfigModel(); + string pluginConfigFilePath = Path.Combine(Directory.GetCurrentDirectory(), "App_Data", "plugin.config.json"); + string pluginConfigJsonStr = File.ReadAllText(pluginConfigFilePath, Encoding.UTF8); + JsonSerializerOptions jsonSerializerOptions = new JsonSerializerOptions(); + jsonSerializerOptions.PropertyNameCaseInsensitive = true; + pluginConfigModel = JsonSerializer.Deserialize(pluginConfigJsonStr, jsonSerializerOptions); + pluginConfigModel = EnabledPluginsSort(pluginConfigModel); + + return pluginConfigModel; + } + #endregion + + #region 保存 + public static void Save(PluginConfigModel pluginConfigModel) + { + if (pluginConfigModel == null) + { + throw new ArgumentNullException(nameof(pluginConfigModel)); + } + try + { + pluginConfigModel = EnabledPluginsSort(pluginConfigModel); + string pluginConfigJsonStr = JsonSerializer.Serialize(pluginConfigModel); + string pluginConfigFilePath = Path.Combine(Directory.GetCurrentDirectory(), "App_Data", "plugin.config.json"); + File.WriteAllText(pluginConfigFilePath, pluginConfigJsonStr, Encoding.UTF8); + } + catch (Exception ex) + { } + + } + #endregion + + #region 确保建立正确的依赖顺序 + public static PluginConfigModel EnabledPluginsSort(PluginConfigModel pluginConfigModel) { + var dependencySorter = new PluginCore.Utils.DependencySorter(); + dependencySorter.AddObjects(pluginConfigModel.EnabledPlugins.ToArray()); + foreach (var plugin in pluginConfigModel.EnabledPlugins) + { + try + { + IList dependPlugins = PluginInfoModelFactory.Create(plugin).DependPlugins; + if (dependPlugins != null && dependPlugins.Count >= 1) { + dependencySorter.SetDependencies(obj: plugin, dependsOnObjects: dependPlugins.ToArray()); + } + } + catch (System.Exception ex) + { + } + } + try + { + var sortedPlugins = dependencySorter.Sort(); + if (sortedPlugins != null && sortedPlugins.Length >= 1) { + pluginConfigModel.EnabledPlugins = sortedPlugins.ToList(); + } + } + catch (System.Exception ex) + { + } + + return pluginConfigModel; + } + #endregion + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore/PluginCore.csproj b/Admin.NET/Plugins/PluginCore/PluginCore/PluginCore.csproj new file mode 100644 index 00000000..075f17d3 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore/PluginCore.csproj @@ -0,0 +1,48 @@ + + + + net8.0;net9.0 + PluginCore + 2.2.5 + yiyun + yiyun + Lightweight plugin framework + Copyright (c) 2021-present yiyun + https://github.com/yiyungent/PluginCore + https://github.com/yiyungent/PluginCore/blob/main/LICENSE + PluginCore + true + + + + 9.0 + enable + + + + + bin\Release\netcoreapp3.1\PluginCore.xml + + + bin\Release\net5.0\PluginCore.xml + + + + + + + + + + + + + + + + + + + + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore/PluginInfoModelFactory.cs b/Admin.NET/Plugins/PluginCore/PluginCore/PluginInfoModelFactory.cs new file mode 100644 index 00000000..199216ba --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore/PluginInfoModelFactory.cs @@ -0,0 +1,108 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.Json; +using PluginCore.Models; + +namespace PluginCore +{ + public class PluginInfoModelFactory + { + private const string InfoJson = "info.json"; + + #region 即时读取指定 plugin info.json + public static PluginInfoModel Create(string pluginId) + { + PluginInfoModel pluginInfoModel = new PluginInfoModel(); + string pluginDir = Path.Combine(PluginPathProvider.PluginsRootPath(), pluginId); + string pluginInfoFilePath = Path.Combine(pluginDir, InfoJson); + + if (!File.Exists(pluginInfoFilePath)) + { + return null; + } + try + { + string pluginInfoJsonStr = File.ReadAllText(pluginInfoFilePath, Encoding.UTF8); + JsonSerializerOptions jsonSerializerOptions = new JsonSerializerOptions(); + jsonSerializerOptions.PropertyNameCaseInsensitive = true; + pluginInfoModel = JsonSerializer.Deserialize(pluginInfoJsonStr, jsonSerializerOptions); + } + catch (Exception ex) + { + pluginInfoModel = null; + } + + return pluginInfoModel; + } + #endregion + + #region 即时读取插件目录下所有 plugin info.json + public static IList CreateAll() + { + IList pluginInfoModels = new List(); + IList pluginDirs = PluginPathProvider.AllPluginDir(); + foreach (var dir in pluginDirs) + { + // 从 dir 中解析出 pluginId + // 约定: 插件文件夹名=PluginID=插件主.dll + string pluginId = PluginPathProvider.GetPluginFolderNameByDir(dir); + PluginInfoModel model = Create(pluginId); + pluginInfoModels.Add(model); + } + // 去除为 null: 目标插件信息不存在,或者格式错误的 + pluginInfoModels = pluginInfoModels.Where(m => m != null).ToList(); + + return pluginInfoModels; + } + #endregion + + #region 从指定插件目录读取插件信息 + /// + /// 从指定插件目录读取插件信息 + /// 可以用于读取临时插件上传目录中的插件信息 + /// + /// + /// + public static PluginInfoModel ReadPluginDir(string pluginDir) + { + PluginInfoModel pluginInfoModel = new PluginInfoModel(); + string pluginInfoFilePath = Path.Combine(pluginDir, InfoJson); + + if (!File.Exists(pluginInfoFilePath)) + { + return null; + } + try + { + string pluginInfoJsonStr = File.ReadAllText(pluginInfoFilePath, Encoding.UTF8); + JsonSerializerOptions jsonSerializerOptions = new JsonSerializerOptions(); + jsonSerializerOptions.PropertyNameCaseInsensitive = true; + pluginInfoModel = JsonSerializer.Deserialize(pluginInfoJsonStr, jsonSerializerOptions); + } + catch (Exception ex) + { + pluginInfoModel = null; + } + + return pluginInfoModel; + } + #endregion + + } + + +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore/PluginReadmeModelFactory.cs b/Admin.NET/Plugins/PluginCore/PluginCore/PluginReadmeModelFactory.cs new file mode 100644 index 00000000..5aad837e --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore/PluginReadmeModelFactory.cs @@ -0,0 +1,55 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Text.Json; +using PluginCore.Models; + +namespace PluginCore +{ + /// + /// TODO: 目前这样读取出来的包含了 windows 换行符 \r\n + /// + public class PluginReadmeModelFactory + { + // TODO: Linux 文件名下区分大小写, windows不区分, 目前必须为 README.md + private const string ReadmeFile = "README.md"; + + #region 即时读取 + public static PluginReadmeModel Create(string pluginId) + { + PluginReadmeModel readmeModel = new PluginReadmeModel(); + string pluginDir = Path.Combine(PluginPathProvider.PluginsRootPath(), pluginId); + string pluginReadmeFilePath = Path.Combine(pluginDir, ReadmeFile); + + if (!File.Exists(pluginReadmeFilePath)) + { + return null; + } + try + { + string readmeStr = File.ReadAllText(pluginReadmeFilePath, Encoding.UTF8); + readmeModel.PluginId = pluginId; + readmeModel.Content = readmeStr; + } + catch (Exception ex) + { + readmeModel = null; + } + + return readmeModel; + } + #endregion + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore/Utils/DateTimeUtil.cs b/Admin.NET/Plugins/PluginCore/PluginCore/Utils/DateTimeUtil.cs new file mode 100644 index 00000000..6ad961ad --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore/Utils/DateTimeUtil.cs @@ -0,0 +1,118 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +// Project: SimCaptcha +// https://github.com/yiyungent/SimCaptcha +// Author: yiyun + +/// +/// JavaScript时间戳:是指格林威治时间1970年01月01日00时00分00秒(北京时间1970年01月01日08时00分00秒)起至现在的总毫秒数。(13 位数字) +/// +/// Unix时间戳:是指格林威治时间1970年01月01日00时00分00秒(北京时间1970年01月01日08时00分00秒)起至现在的总秒数。(10 位数字) +/// +namespace PluginCore.Utils +{ + public static class DateTimeUtil + { + public static DateTime DateTime1970 = new DateTime(1970, 1, 1).ToLocalTime(); + + #region Unix 10位时间戳-总秒数 + /// + /// C# DateTime转换为Unix时间戳 + /// + public static long ToTimeStamp10(this DateTime dateTime) + { + // 相差秒数 + long timeStamp = (long)(dateTime.ToLocalTime() - DateTime1970).TotalSeconds; + + return timeStamp; + } + + /// + /// C# DateTime转换为Unix时间戳 + /// + public static long ToTimeStamp10(this DateTime? dateTime) + { + if (dateTime == null) + { + return 0; + } + // 相差秒数 + long timeStamp = ToTimeStamp10((DateTime)dateTime); + + return timeStamp; + } + + /// + /// Unix时间戳转换为C# DateTime + /// + public static DateTime ToDateTime10(this long timeStamp10) + { + DateTime dateTime = DateTime1970.AddSeconds(timeStamp10).ToLocalTime(); + + return dateTime; + } + #endregion + + #region JavaScript 13位时间戳-总毫秒数 + /// + /// C# DateTime转换为JavaScript时间戳 + /// + public static long ToTimeStamp13(this DateTime dateTime) + { + // 相差毫秒数 + long timeStamp = (long)(dateTime.ToLocalTime() - DateTime1970).TotalMilliseconds; + + return timeStamp; + } + + /// + /// C# DateTime转换为JavaScript时间戳 + /// + public static long ToTimeStamp13(this DateTime? dateTime) + { + if (dateTime == null) + { + return 0; + } + // 相差秒数 + long timeStamp = ToTimeStamp13((DateTime)dateTime); + + return timeStamp; + } + + /// + /// JavaScript时间戳转换为C# DateTime + /// + public static DateTime ToDateTime13(this long timeStamp13) + { + DateTime dateTime = DateTime1970.AddMilliseconds(timeStamp13).ToLocalTime(); + + return dateTime; + } + #endregion + + #region 获取当前Unix时间戳 + public static long NowTimeStamp10() + { + return ToTimeStamp10(DateTime.Now); + } + #endregion + + #region 获取当前JavaScript时间戳 + public static long NowTimeStamp13() + { + return ToTimeStamp13(DateTime.Now); + } + #endregion + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore/Utils/DependencySorter.cs b/Admin.NET/Plugins/PluginCore/PluginCore/Utils/DependencySorter.cs new file mode 100644 index 00000000..6281d4e8 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore/Utils/DependencySorter.cs @@ -0,0 +1,194 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + +using System; +using System.Collections.Generic; +using System.Diagnostics; + + +namespace PluginCore.Utils +{ + /// + /// https://stackoverflow.com/questions/4106862/how-to-sort-depended-objects-by-dependency/ + /// + /// Definition: http://en.wikipedia.org/wiki/Topological_sorting + /// Source code credited to: http://tawani.blogspot.com/2009/02/topological-sorting-and-cyclic.html + /// Original Java source code: http://www.java2s.com/Code/Java/Collections-Data-Structure/Topologicalsorting.htm + /// + /// ThangTran + /// + /// 2012.03.21 - ThangTran: rewritten based on . + /// + public class DependencySorter + { + //************************************************** + // + // Private members + // + //************************************************** + + #region Private members + + /// + /// Gets the dependency matrix used by this instance. + /// + private readonly Dictionary> _matrix = new Dictionary>(); + + #endregion + + + //************************************************** + // + // Public methods + // + //************************************************** + + #region Public methods + + /// + /// Adds a list of objects that will be sorted. + /// + public void AddObjects(params T[] objects) + { + // --- Begin parameters checking code ----------------------------- + // Debug.Assert(objects != null); + // Debug.Assert(objects.Length > 0); + // --- End parameters checking code ------------------------------- + + // add to matrix + foreach (T obj in objects) + { + // add to dictionary + _matrix.Add(obj, new Dictionary()); + } + } + + /// + /// Sets dependencies of given object. + /// This means depends on these to run. + /// Please make sure objects given in the and are added first. + /// + public void SetDependencies(T obj, params T[] dependsOnObjects) + { + // --- Begin parameters checking code ----------------------------- + // Debug.Assert(dependsOnObjects != null); + // --- End parameters checking code ------------------------------- + + // set dependencies + Dictionary dependencies = _matrix[obj]; + dependencies.Clear(); + + // for each depended objects, add to dependencies + foreach (T dependsOnObject in dependsOnObjects) + { + dependencies.Add(dependsOnObject, null); + } + } + + /// + /// Sorts objects based on this dependencies. + /// Note: because of the nature of algorithm and memory usage efficiency, this method can be used only one time. + /// + public T[] Sort() + { + // prepare result + List result = new List(_matrix.Count); + + // while there are still object to get + while (_matrix.Count > 0) + { + // get an independent object + T independentObject; + if (!this.GetIndependentObject(out independentObject)) + { + // circular dependency found + throw new CircularReferenceException(); + } + + // add to result + result.Add(independentObject); + + // delete processed object + this.DeleteObject(independentObject); + } + + // return result + return result.ToArray(); + } + + #endregion + + + //************************************************** + // + // Private methods + // + //************************************************** + + #region Private methods + + /// + /// Returns independent object or returns NULL if no independent object is found. + /// + private bool GetIndependentObject(out T result) + { + // for each object + foreach (KeyValuePair> pair in _matrix) + { + // if the object contains any dependency + if (pair.Value.Count > 0) + { + // has dependency, skip it + continue; + } + + // found + result = pair.Key; + return true; + } + + // not found + result = default(T); + return false; + } + + /// + /// Deletes given object from the matrix. + /// + private void DeleteObject(T obj) + { + // delete object from matrix + _matrix.Remove(obj); + + // for each object, remove the dependency reference + foreach (KeyValuePair> pair in _matrix) + { + // if current object depends on deleting object + pair.Value.Remove(obj); + } + } + + + #endregion + } + + /// + /// Represents a circular reference exception when sorting dependency objects. + /// + public class CircularReferenceException : Exception + { + /// + /// Initializes a new instance of the class. + /// + public CircularReferenceException() + : base("Circular reference found.") + { + } + } +} + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore/Utils/FileUtil.cs b/Admin.NET/Plugins/PluginCore/PluginCore/Utils/FileUtil.cs new file mode 100644 index 00000000..5685b446 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore/Utils/FileUtil.cs @@ -0,0 +1,178 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace PluginCore.Utils +{ + public class FileUtil + { + #region 复制大文件 + public static void CopyBigFile(string originalFilePath, string destFilePath) + { + // 定义读文件流 + using (FileStream fsr = new FileStream(originalFilePath, FileMode.Open)) + { + // 定义写文件流 + using (FileStream fsw = new FileStream(destFilePath, FileMode.OpenOrCreate)) + { + // 申请1M内存空间 + byte[] buffer = new byte[1024 * 1024]; + // 无限循环中反复读写,直到读完写完 + while (true) + { + int readCount = fsr.Read(buffer, 0, buffer.Length); + fsw.Write(buffer, 0, readCount); + if (readCount < buffer.Length) + { + break; + } + } + } + } + } + #endregion + + #region 移动大文件 + public static void MoveBigFile(string originalFilePath, string destFilePath) + { + try + { + CopyBigFile(originalFilePath, destFilePath); + File.Delete(originalFilePath); + } + catch (Exception ex) + { + throw ex; + } + } + #endregion + + #region 复制文件夹 + /// + /// 复制文件夹及文件 + /// (不包括源文件夹根目录名称, 只是复制其中内容到目标文件夹) + /// https://www.cnblogs.com/wangjianhui008/p/3234519.html + /// + /// 原文件路径 + /// 目标文件路径 + /// + public static bool CopyFolder(string sourceFolder, string destFolder) + { + try + { + // 如果目标路径不存在,则创建目标路径 + if (!System.IO.Directory.Exists(destFolder)) + { + System.IO.Directory.CreateDirectory(destFolder); + } + // 得到原文件根目录下的所有文件 + string[] files = System.IO.Directory.GetFiles(sourceFolder); + foreach (string file in files) + { + string name = System.IO.Path.GetFileName(file); + string dest = System.IO.Path.Combine(destFolder, name); + System.IO.File.Copy(file, dest);//复制文件 + } + // 得到原文件根目录下的所有文件夹 + string[] folders = System.IO.Directory.GetDirectories(sourceFolder); + foreach (string folder in folders) + { + string name = System.IO.Path.GetFileName(folder); + string dest = System.IO.Path.Combine(destFolder, name); + CopyFolder(folder, dest);//构建目标路径,递归复制文件 + } + return true; + } + catch (Exception e) + { + + return false; + } + + } + #endregion + + #region 计算MD5 + + + /// + /// 获取文件的MD5码 + /// + /// 传入的文件名(含路径及后缀名) + /// + public string GetMD5HashFromFile(string filePath) + { + try + { + FileStream file = new FileStream(filePath, System.IO.FileMode.Open); + System.Security.Cryptography.MD5 md5 = new System.Security.Cryptography.MD5CryptoServiceProvider(); + byte[] retVal = md5.ComputeHash(file); + file.Close(); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < retVal.Length; i++) + { + sb.Append(retVal[i].ToString("x2")); + } + return sb.ToString(); + } + catch (Exception ex) + { + throw new Exception("GetMD5HashFromFile() fail,error:" + ex.Message); + } + } + + + public static string GetFileMD5(string filePath) + { + FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read); + int bufferSize = 1048576; + byte[] buff = new byte[bufferSize]; + System.Security.Cryptography.MD5CryptoServiceProvider md5 = new System.Security.Cryptography.MD5CryptoServiceProvider(); + md5.Initialize(); + long offset = 0; + while (offset < fs.Length) + { + long readSize = bufferSize; + if (offset + readSize > fs.Length) + readSize = fs.Length - offset; + fs.Read(buff, 0, Convert.ToInt32(readSize)); + if (offset + readSize < fs.Length) + md5.TransformBlock(buff, 0, Convert.ToInt32(readSize), buff, 0); + else + md5.TransformFinalBlock(buff, 0, Convert.ToInt32(readSize)); + offset += bufferSize; + } + if (offset >= fs.Length) + { + fs.Close(); + byte[] result = md5.Hash; + md5.Clear(); + StringBuilder sb = new StringBuilder(32); + for (int i = 0; i < result.Length; i++) + sb.Append(result[i].ToString("X2")); + return sb.ToString(); + } + else + { + fs.Close(); + return null; + } + } + + #endregion + + + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore/Utils/LogUtil.cs b/Admin.NET/Plugins/PluginCore/PluginCore/Utils/LogUtil.cs new file mode 100644 index 00000000..35492ca7 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore/Utils/LogUtil.cs @@ -0,0 +1,238 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Text; +using System.Linq; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.DependencyInjection; + +namespace PluginCore.Utils +{ + public class LogUtil + { + public const string LogCategoryName = nameof(PluginCore); + + private static IServiceScopeFactory _serviceScopeFactory; + + public static void Initialize(IServiceScopeFactory serviceScopeFactory) + { + _serviceScopeFactory = serviceScopeFactory; + } + + public static void Info(string message) + { + if (_serviceScopeFactory == null) + { + return; + } + using (var scope = _serviceScopeFactory.CreateScope()) + { + ILogger? service = scope.ServiceProvider.GetService>(); + if (service != null) + { + service.LogInformation($"{LogCategoryName}: {message}"); + } + } + } + + public static void Info(string categoryName, string message) + { + if (_serviceScopeFactory == null) + { + return; + } + using (var scope = _serviceScopeFactory.CreateScope()) + { + // null + // ILogger? service = scope.ServiceProvider.GetService(); + ILogger? service = scope.ServiceProvider.GetService()?.CreateLogger(categoryName: categoryName) ?? null; + if (service != null) + { + service.LogInformation($"{LogCategoryName}: {message}"); + } + } + } + + public static void Warn(string message) + { + if (_serviceScopeFactory == null) + { + return; + } + using (var scope = _serviceScopeFactory.CreateScope()) + { + ILogger? service = scope.ServiceProvider.GetService>(); + if (service != null) + { + service.LogWarning($"{LogCategoryName}: {message}"); + } + } + } + + public static void Warn(string categoryName, string message) + { + if (_serviceScopeFactory == null) + { + return; + } + using (var scope = _serviceScopeFactory.CreateScope()) + { + // null + // ILogger? service = scope.ServiceProvider.GetService(); + ILogger? service = scope.ServiceProvider.GetService()?.CreateLogger(categoryName: categoryName) ?? null; + if (service != null) + { + service.LogWarning($"{LogCategoryName}: {message}"); + } + } + } + + public static void Warn(Exception ex, string message) + { + if (_serviceScopeFactory == null) + { + return; + } + using (var scope = _serviceScopeFactory.CreateScope()) + { + ILogger? service = scope.ServiceProvider.GetService>(); + if (service != null) + { + service.LogWarning(ex, $"{LogCategoryName}: {message}"); + } + } + } + + public static void Warn(string categoryName, Exception ex, string message) + { + if (_serviceScopeFactory == null) + { + return; + } + using (var scope = _serviceScopeFactory.CreateScope()) + { + // null + // ILogger? service = scope.ServiceProvider.GetService(); + ILogger? service = scope.ServiceProvider.GetService()?.CreateLogger(categoryName: categoryName) ?? null; + if (service != null) + { + service.LogWarning(ex, $"{LogCategoryName}: {message}"); + } + } + } + + public static void Error(string message) + { + if (_serviceScopeFactory == null) + { + return; + } + using (var scope = _serviceScopeFactory.CreateScope()) + { + ILogger? service = scope.ServiceProvider.GetService>(); + if (service != null) + { + service.LogError($"{LogCategoryName}: {message}"); + } + } + } + + public static void Error(string categoryName, string message) + { + if (_serviceScopeFactory == null) + { + return; + } + using (var scope = _serviceScopeFactory.CreateScope()) + { + // null + // ILogger? service = scope.ServiceProvider.GetService(); + ILogger? service = scope.ServiceProvider.GetService()?.CreateLogger(categoryName: categoryName) ?? null; + if (service != null) + { + service.LogError($"{LogCategoryName}: {message}"); + } + } + } + + public static void Error(Exception ex, string message) + { + if (_serviceScopeFactory == null) + { + return; + } + using (var scope = _serviceScopeFactory.CreateScope()) + { + ILogger? service = scope.ServiceProvider.GetService>(); + if (service != null) + { + service.LogError(ex, $"{LogCategoryName}: {message}"); + } + } + } + + public static void Error(string categoryName, Exception ex, string message) + { + if (_serviceScopeFactory == null) + { + return; + } + using (var scope = _serviceScopeFactory.CreateScope()) + { + // null + // ILogger? service = scope.ServiceProvider.GetService(); + ILogger? service = scope.ServiceProvider.GetService()?.CreateLogger(categoryName: categoryName) ?? null; + if (service != null) + { + service.LogError(ex, $"{LogCategoryName}: {message}"); + } + } + } + + public static void PluginBehavior(T plugin, Type iplugin, string methodName) + where T : IPlugins.IPlugin + { + if (_serviceScopeFactory == null) + { + return; + } + // TODO: Bug: 无法区别 相同方法名, 参数不同的 重载方法 + MethodInfo behavior = iplugin.GetMethods().FirstOrDefault(m => m.Name == methodName); + + ParameterInfo[] pars = behavior.GetParameters(); + string parsStr = string.Empty; + if (pars != null && pars.Length > 0) + { + var parTypes = pars.OrderBy(m => m.Position).Select(m => m.ParameterType.Name).ToArray(); + parsStr = string.Join(", ", parTypes); + } + + // A程序集.APlugin + string pluginStr = plugin.GetType().ToString(); + // A程序集.AClass.AMethod() + string message = $"{behavior.DeclaringType.ToString()}.{behavior.Name}({parsStr})"; + + // 2. 日志输出 + using (var scope = _serviceScopeFactory.CreateScope()) + { + ILogger? service = scope.ServiceProvider.GetService>(); + if (service != null) + { + service.LogInformation($"{LogCategoryName}: {message}"); + } + } + } + + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore/Utils/Md5Helper.cs b/Admin.NET/Plugins/PluginCore/PluginCore/Utils/Md5Helper.cs new file mode 100644 index 00000000..4edf61f7 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore/Utils/Md5Helper.cs @@ -0,0 +1,42 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Text; + +namespace PluginCore.Utils +{ + public class Md5Helper + { + #region MD5加密为32位16进制字符串 + /// + /// MD5加密为32位16进制字符串 + /// + /// 原输入字符串 + /// 返回加密后的字符串 + public static string MD5Encrypt32(string source) + { + MD5 md5 = MD5.Create(); + byte[] buffer = md5.ComputeHash(Encoding.UTF8.GetBytes(source)); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < buffer.Length; i++) + { + // "x2" 转换为 16进制 + sb.Append(buffer[i].ToString("x2")); + } + return sb.ToString(); + } + #endregion + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore/Utils/RuntimeUtil.cs b/Admin.NET/Plugins/PluginCore/PluginCore/Utils/RuntimeUtil.cs new file mode 100644 index 00000000..af38db43 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore/Utils/RuntimeUtil.cs @@ -0,0 +1,28 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Generic; +using System.Text; + +namespace PluginCore.Utils +{ + public class RuntimeUtil + { + public static Version RuntimeNetVersion + { + get + { + return Environment.Version; + } + } + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore/Utils/ZipHelper.cs b/Admin.NET/Plugins/PluginCore/PluginCore/Utils/ZipHelper.cs new file mode 100644 index 00000000..ad44e058 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore/Utils/ZipHelper.cs @@ -0,0 +1,445 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using ICSharpCode.SharpZipLib.Zip; +using System; +using System.Collections.Generic; +using System.IO; + +namespace PluginCore.Utils +{ + public class ZipHelper + { + /// + /// 缓存字节数 + /// + private const int BufferSize = 4096; + + /// + /// 压缩最小等级 + /// + public const int CompressionLevelMin = 0; + + /// + /// 压缩最大等级 + /// + public const int CompressionLevelMax = 9; + + /// + /// 获取所有文件系统对象 + /// + /// 源路径 + /// 顶级文件夹 + /// 字典中Key为完整路径,Value为文件(夹)名称 + private static Dictionary GetAllFileSystemEntities(string source, string topDirectory) + { + Dictionary entitiesDictionary = new Dictionary(); + entitiesDictionary.Add(source, source.Replace(topDirectory, "")); + + if (Directory.Exists(source)) + { + //一次性获取下级所有目录,避免递归 + string[] directories = Directory.GetDirectories(source, "*.*", SearchOption.AllDirectories); + foreach (string directory in directories) + { + entitiesDictionary.Add(directory, directory.Replace(topDirectory, "")); + } + + string[] files = Directory.GetFiles(source, "*.*", SearchOption.AllDirectories); + foreach (string file in files) + { + entitiesDictionary.Add(file, file.Replace(topDirectory, "")); + } + } + + return entitiesDictionary; + } + + /// + /// 校验压缩等级 + /// + /// + /// + private static int CheckCompressionLevel(int compressionLevel) + { + compressionLevel = compressionLevel < CompressionLevelMin ? CompressionLevelMin : compressionLevel; + compressionLevel = compressionLevel > CompressionLevelMax ? CompressionLevelMax : compressionLevel; + return compressionLevel; + } + + #region 字节压缩与解压 + + /// + /// 压缩字节数组 + /// + /// 源字节数组 + /// 压缩等级 + /// 密码 + /// 压缩后的字节数组 + public static byte[] CompressBytes(byte[] sourceBytes, string password = null, int compressionLevel = 6) + { + byte[] result = new byte[] { }; + + if (sourceBytes.Length > 0) + { + try + { + using (MemoryStream tempStream = new MemoryStream()) + { + using (MemoryStream readStream = new MemoryStream(sourceBytes)) + { + using (ZipOutputStream zipStream = new ZipOutputStream(tempStream)) + { + zipStream.Password = password;//设置密码 + zipStream.SetLevel(CheckCompressionLevel(compressionLevel));//设置压缩等级 + + ZipEntry zipEntry = new ZipEntry("ZipBytes"); + zipEntry.DateTime = DateTime.Now; + zipEntry.Size = sourceBytes.Length; + zipStream.PutNextEntry(zipEntry); + int readLength = 0; + byte[] buffer = new byte[BufferSize]; + + do + { + readLength = readStream.Read(buffer, 0, BufferSize); + zipStream.Write(buffer, 0, readLength); + } while (readLength == BufferSize); + + readStream.Close(); + zipStream.Flush(); + zipStream.Finish(); + result = tempStream.ToArray(); + zipStream.Close(); + } + } + } + } + catch (System.Exception ex) + { + throw new Exception("压缩字节数组发生错误", ex); + } + } + + return result; + } + + /// + /// 解压字节数组 + /// + /// 源字节数组 + /// 密码 + /// 解压后的字节数组 + public static byte[] DecompressBytes(byte[] sourceBytes, string password = null) + { + byte[] result = new byte[] { }; + + if (sourceBytes.Length > 0) + { + try + { + using (MemoryStream tempStream = new MemoryStream(sourceBytes)) + { + using (MemoryStream writeStream = new MemoryStream()) + { + using (ZipInputStream zipStream = new ZipInputStream(tempStream)) + { + zipStream.Password = password; + ZipEntry zipEntry = zipStream.GetNextEntry(); + + if (zipEntry != null) + { + byte[] buffer = new byte[BufferSize]; + int readLength = 0; + + do + { + readLength = zipStream.Read(buffer, 0, BufferSize); + writeStream.Write(buffer, 0, readLength); + } while (readLength == BufferSize); + + writeStream.Flush(); + result = writeStream.ToArray(); + writeStream.Close(); + } + zipStream.Close(); + } + } + } + } + catch (System.Exception ex) + { + throw new Exception("解压字节数组发生错误", ex); + } + } + return result; + } + + #endregion + + #region 文件压缩与解压 + + /// + /// 为压缩准备文件系统对象 + /// + /// + /// + private static Dictionary PrepareFileSystementities(IEnumerable sourceFileEntityPathList) + { + Dictionary fileEntityDictionary = new Dictionary();//文件字典 + string parentDirectoryPath = ""; + foreach (string fileEntityPath in sourceFileEntityPathList) + { + string path = fileEntityPath; + //保证传入的文件夹也被压缩进文件 + if (path.EndsWith(@"\")) + { + path = path.Remove(path.LastIndexOf(@"\")); + } + + parentDirectoryPath = Path.GetDirectoryName(path) + @"\"; + + if (parentDirectoryPath.EndsWith(@":\\"))//防止根目录下把盘符压入的错误 + { + parentDirectoryPath = parentDirectoryPath.Replace(@"\\", @"\"); + } + + //获取目录中所有的文件系统对象 + Dictionary subDictionary = GetAllFileSystemEntities(path, parentDirectoryPath); + + //将文件系统对象添加到总的文件字典中 + foreach (string key in subDictionary.Keys) + { + if (!fileEntityDictionary.ContainsKey(key))//检测重复项 + { + fileEntityDictionary.Add(key, subDictionary[key]); + } + } + } + return fileEntityDictionary; + } + + /// + /// 压缩单个文件/文件夹 + /// + /// 源文件/文件夹路径列表 + /// 压缩文件路径 + /// 注释信息 + /// 压缩密码 + /// 压缩等级,范围从0到9,可选,默认为6 + /// + public static bool CompressFile(string path, string zipFilePath, + string comment = null, string password = null, int compressionLevel = 6) + { + return CompressFile(new string[] { path }, zipFilePath, comment, password, compressionLevel); + } + + /// + /// 压缩多个文件/文件夹 + /// + /// 源文件/文件夹路径列表 + /// 压缩文件路径 + /// 注释信息 + /// 压缩密码 + /// 压缩等级,范围从0到9,可选,默认为6 + /// + public static bool CompressFile(IEnumerable sourceList, string zipFilePath, + string comment = null, string password = null, int compressionLevel = 6) + { + bool result = false; + + try + { + //检测目标文件所属的文件夹是否存在,如果不存在则建立 + string zipFileDirectory = Path.GetDirectoryName(zipFilePath); + if (!Directory.Exists(zipFileDirectory)) + { + Directory.CreateDirectory(zipFileDirectory); + } + + Dictionary dictionaryList = PrepareFileSystementities(sourceList); + + using (ZipOutputStream zipStream = new ZipOutputStream(File.Create(zipFilePath))) + { + zipStream.Password = password;//设置密码 + zipStream.SetComment(comment);//添加注释 + zipStream.SetLevel(CheckCompressionLevel(compressionLevel));//设置压缩等级 + + foreach (string key in dictionaryList.Keys)//从字典取文件添加到压缩文件 + { + if (File.Exists(key))//判断是文件还是文件夹 + { + FileInfo fileItem = new FileInfo(key); + + using (FileStream readStream = fileItem.Open(FileMode.Open, + FileAccess.Read, FileShare.Read)) + { + ZipEntry zipEntry = new ZipEntry(dictionaryList[key]); + zipEntry.DateTime = fileItem.LastWriteTime; + zipEntry.Size = readStream.Length; + zipStream.PutNextEntry(zipEntry); + int readLength = 0; + byte[] buffer = new byte[BufferSize]; + + do + { + readLength = readStream.Read(buffer, 0, BufferSize); + zipStream.Write(buffer, 0, readLength); + } while (readLength == BufferSize); + + readStream.Close(); + } + } + else//对文件夹的处理 + { + ZipEntry zipEntry = new ZipEntry(dictionaryList[key] + "/"); + zipStream.PutNextEntry(zipEntry); + } + } + + zipStream.Flush(); + zipStream.Finish(); + zipStream.Close(); + } + + result = true; + } + catch (System.Exception ex) + { + throw new Exception("压缩文件失败", ex); + } + + return result; + } + + /// + /// 解压文件到指定文件夹 + /// + /// 压缩文件 + /// 目标文件夹,如果为空则解压到当前文件夹下 + /// 密码 + /// + public static bool DecomparessFile(string sourceFile, string destinationDirectory = null, string password = null) + { + bool result = false; + + if (!File.Exists(sourceFile)) + { + throw new FileNotFoundException("要解压的文件不存在", sourceFile); + } + + if (string.IsNullOrWhiteSpace(destinationDirectory)) + { + destinationDirectory = Path.GetDirectoryName(sourceFile); + } + + try + { + if (!Directory.Exists(destinationDirectory)) + { + Directory.CreateDirectory(destinationDirectory); + } + + using (ZipInputStream zipStream = new ZipInputStream(File.Open(sourceFile, FileMode.Open, + FileAccess.Read, FileShare.Read))) + { + zipStream.Password = password; + ZipEntry zipEntry = zipStream.GetNextEntry(); + + while (zipEntry != null) + { + if (zipEntry.IsDirectory)//如果是文件夹则创建 + { + Directory.CreateDirectory(Path.Combine(destinationDirectory, + Path.GetDirectoryName(zipEntry.Name))); + } + else + { + string fileName = Path.GetFileName(zipEntry.Name); + if (!string.IsNullOrEmpty(fileName) && fileName.Trim().Length > 0) + { + FileInfo fileItem = new FileInfo(Path.Combine(destinationDirectory, zipEntry.Name)); + if (fileItem.Directory is null) + continue; + if (!Directory.Exists(fileItem.Directory.FullName)) + Directory.CreateDirectory(fileItem.Directory.FullName); + using (FileStream writeStream = fileItem.Create()) + { + byte[] buffer = new byte[BufferSize]; + int readLength = 0; + + do + { + readLength = zipStream.Read(buffer, 0, BufferSize); + writeStream.Write(buffer, 0, readLength); + } while (readLength == BufferSize); + + writeStream.Flush(); + writeStream.Close(); + } + fileItem.LastWriteTime = zipEntry.DateTime; + } + } + zipEntry = zipStream.GetNextEntry();//获取下一个文件 + } + + zipStream.Close(); + } + result = true; + } + catch (System.Exception ex) + { + throw new Exception("文件解压发生错误", ex); + } + + return result; + } + + + public static bool FastDecomparessFile(string sourceFile, string destinationDirectory = null) + { + bool result = false; + + if (!File.Exists(sourceFile)) + { + throw new FileNotFoundException("要解压的文件不存在", sourceFile); + } + + if (string.IsNullOrWhiteSpace(destinationDirectory)) + { + destinationDirectory = Path.GetDirectoryName(sourceFile); + } + + try + { + if (!Directory.Exists(destinationDirectory)) + { + Directory.CreateDirectory(destinationDirectory); + } + + FastZip fastZip = new FastZip(); + string fileFilter = null; + + fastZip.ExtractZip(sourceFile, destinationDirectory, fileFilter); + + result = true; + } + catch (Exception ex) + { + throw new Exception("文件解压发生错误", ex); + } + + return result; + } + + #endregion + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore/cliff.toml b/Admin.NET/Plugins/PluginCore/PluginCore/cliff.toml new file mode 100644 index 00000000..aa3089f4 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore/cliff.toml @@ -0,0 +1,82 @@ +# git-cliff ~ configuration file +# https://git-cliff.org/docs/configuration + +[changelog] +# template for the changelog header +header = """ +# Changelog\n +All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines.\n +""" +# template for the changelog body +# https://keats.github.io/tera/docs/#introduction +body = """ +--- +{% if version %}\ + {% if previous.version %}\ + ## [{{ version | trim_start_matches(pat="v") }}]($REPO/compare/{{ previous.version }}..{{ version }}) - {{ timestamp | date(format="%Y-%m-%d") }} + {% else %}\ + ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} + {% endif %}\ +{% else %}\ + ## [unreleased] +{% endif %}\ +{% for group, commits in commits | group_by(attribute="group") %} + ### {{ group | striptags | trim | upper_first }} + {% for commit in commits + | filter(attribute="scope") + | sort(attribute="scope") %} + - **({{commit.scope}})**{% if commit.breaking %} [**breaking**]{% endif %} \ + {{ commit.message }} - ([{{ commit.id | truncate(length=7, end="") }}]($REPO/commit/{{ commit.id }})) - {{ commit.author.name }} + {%- endfor -%} + {% raw %}\n{% endraw %}\ + {%- for commit in commits %} + {%- if commit.scope -%} + {% else -%} + - {% if commit.breaking %} [**breaking**]{% endif %}\ + {{ commit.message }} - ([{{ commit.id | truncate(length=7, end="") }}]($REPO/commit/{{ commit.id }})) - {{ commit.author.name }} + {% endif -%} + {% endfor -%} +{% endfor %}\n +""" +# template for the changelog footer +footer = """ + +""" +# remove the leading and trailing whitespace from the templates +trim = true +# postprocessors +postprocessors = [ + { pattern = '\$REPO', replace = "https://github.com/yiyungent/PluginCore" }, # replace repository URL +] + +[git] +# parse the commits based on https://www.conventionalcommits.org +conventional_commits = true +# filter out the commits that are not conventional +filter_unconventional = true +# process each line of a commit as an individual commit +split_commits = false +# regex for preprocessing the commit messages +commit_preprocessors = [ + # { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/orhun/git-cliff/issues/${2}))"}, # replace issue numbers +] +# regex for parsing and grouping commits +commit_parsers = [ + { message = "^feat", group = "Features" }, + { message = "^fix", group = "Bug Fixes" }, + { message = "^doc", group = "Documentation" }, + { message = "^perf", group = "Performance" }, + { message = "^refactor", group = "Refactoring" }, + { message = "^style", group = "Style" }, + { message = "^revert", group = "Revert" }, + { message = "^test", group = "Tests" }, + { message = "^chore\\(version\\):", skip = true }, + { message = "^chore", group = "Miscellaneous Chores" }, + { body = ".*security", group = "Security" }, +] +# filter out the commits that are not matched by commit parsers +filter_commits = false +# sort the tags topologically +topo_order = false +# sort the commits inside sections by oldest/newest order +sort_commits = "oldest" diff --git a/Admin.NET/Plugins/PluginCore/PluginCore/lmplements/CollectibleAssemblyLoadContext.cs b/Admin.NET/Plugins/PluginCore/PluginCore/lmplements/CollectibleAssemblyLoadContext.cs new file mode 100644 index 00000000..c34c02a0 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore/lmplements/CollectibleAssemblyLoadContext.cs @@ -0,0 +1,45 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using PluginCore.Interfaces; +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Runtime.Loader; +using System.Text; + +namespace PluginCore.lmplements +{ + /// + /// 一个可回收的程序集加载上下文 + /// 在整个插件加载上下文的设计上,每个插件都使用一个单独的CollectibleAssemblyLoadContext来加载,所有插件的CollectibleAssemblyLoadContext都放在一个PluginsLoadContext对象中 + /// + public class CollectibleAssemblyLoadContext : AssemblyLoadContext, IPluginContext, ICollectibleAssemblyLoadContext + { + public string PluginId + { + get + { + return this.Name ?? ""; + } + } + + public CollectibleAssemblyLoadContext(string? name) + : base(isCollectible: true, name: name) + { + } + + protected override Assembly Load(AssemblyName name) + { + return null; + } + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore/lmplements/LazyPluginLoadContext.cs b/Admin.NET/Plugins/PluginCore/PluginCore/lmplements/LazyPluginLoadContext.cs new file mode 100644 index 00000000..3cf114a7 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore/lmplements/LazyPluginLoadContext.cs @@ -0,0 +1,101 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using PluginCore.Interfaces; +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Runtime.Loader; +using System.Text; +using System.Linq; + +namespace PluginCore.lmplements +{ + /// + /// 修改自下方 + /// https://docs.microsoft.com/en-us/dotnet/core/tutorials/creating-app-with-plugin-support + /// 此方法依赖于 插件项目生成的 HelloWorldPlugin.deps.json 与 runtimes 文件夹, + /// 虽然官方文档没写, 但最好还带上 HelloWorldPlugin.runtimeconfig.json + /// 插件项目 .csproj 其它注意, 看文档 + /// + public class LazyPluginLoadContext : CollectibleAssemblyLoadContext, IPluginContext + { + private AssemblyDependencyResolver _resolver; + + /// + /// 加了一个可回收 + /// + /// + public LazyPluginLoadContext(string pluginId, string pluginMainDllFilePath) : base(name: pluginId) + { + _resolver = new AssemblyDependencyResolver(pluginMainDllFilePath); + } + + protected override Assembly Load(AssemblyName assemblyName) + { + // 1. 先尝试 从本插件文件夹中搜索 + string assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName); + if (assemblyPath != null) + { + //return LoadFromAssemblyPath(assemblyPath); + using (var fs = new FileStream(assemblyPath, FileMode.Open)) + { + // 使用此方法, 就不会导致dll被锁定 + // 锁定dll 会导致: 1. 无法通过复制粘贴替换 更新 2. 无法删除 + return LoadFromStream(fs); + } + } + // 2. 再尝试 从其他启用的插件文件夹中搜索 + // 实测: 可在下方搜索, 主程序包括的 assemblyName 与 启用插件加载的 assemblyName 都会位于其中 + var assList = AppDomain.CurrentDomain.GetAssemblies(); + var temp = assList.FirstOrDefault(m => m.GetName().FullName == assemblyName.FullName); + if (temp != null) + { + return temp; + } + // 尝试忽略版本搜索 + var temp2 = assList.FirstOrDefault(m => m.GetName().Name == assemblyName.Name); + if (temp2 != null) + { + return temp2; + } + + // 由于是懒加载, 若 A 插件依赖 B 插件, 而此时在 A 插件调用 B.Test(), + // 而 B.Test() 与 C.dll 有关, 而之前 B 插件未触发 C.dll 中类型, 于是这时还没有加载 C.dll, 于是 assList 这时其中无 C.dll + // 由于 B.Test() 需在 A 中调用, 于是为公用, 因此 C.dll 需在 A 中排除, 不然会由于重复加载-类型不一致而 Method Missing + // 在 A 中 C.dll 与 在 B 中 C.dll 会认为其中类型不一致 + // TODO: 主动扫描一次 + //foreach (var pluginContextKeyValue in PluginContextStore.PluginContexts) + //{ + // //pluginContextKeyValue.Value.Load(assemblyName); + // string pluginId = pluginContextKeyValue.Key; + + //} + + // 3. 最后搜索不到, 返回 null, 即代表使用主程序提供, 如果最后几次都为 null, 则会报错 + // 当启用本插件/触碰到本插件中的一些类型时, 而当主程序中没有提供相关的此 assemblyName 时, 也会触发此方法 来尝试加载 + + return null; + } + + protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) + { + string libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName); + if (libraryPath != null) + { + return LoadUnmanagedDllFromPath(libraryPath); + } + + return IntPtr.Zero; + } + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore/lmplements/PluginContextManager.cs b/Admin.NET/Plugins/PluginCore/PluginCore/lmplements/PluginContextManager.cs new file mode 100644 index 00000000..83b77a63 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore/lmplements/PluginContextManager.cs @@ -0,0 +1,109 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using PluginCore.Interfaces; +using PluginCore.lmplements; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Loader; +using System.Text; + +namespace PluginCore.lmplements +{ + /// + /// 启用插件时加载进来, 禁用插件时移除释放 + /// 只有已启用的插件才有上下文 + /// https://www.cnblogs.com/lwqlun/p/11395828.html + /// 1.当加载插件的时候,我们需要将当前插件的程序集加载上下文放到_pluginContexts字典中。字典的key是插件的名称,字典的value是插件的程序集加载上下文。 + /// 2.当移除一个插件的时候,我们需要使用Unload方法,来释放当前的程序集加载上下文。 + /// + public class PluginContextManager : IPluginContextManager + { + #region Fields + + private static int _newCount; + + //private Dictionary + // _pluginContexts; + + private Dictionary _pluginContexts + { + get + { + return PluginContextStore.PluginContexts; + } + } + + #endregion + + #region Ctor + public PluginContextManager() + { + //_pluginContexts = new Dictionary(); + + #region 计数 + _newCount++; + if (_newCount > 1) + { +#if DEBUG + Utils.LogUtil.Warn($"警告: {nameof(PluginContextManager)} 被 new {_newCount} 次"); +#endif + } + #endregion + } + #endregion + + #region Methods + + public List All() + { + return _pluginContexts.Select(p => p.Value).ToList(); + } + + public bool Any(string pluginId) + { + return _pluginContexts.ContainsKey(pluginId); + } + + public void Remove(string pluginId) + { + if (_pluginContexts.ContainsKey(pluginId)) + { + _pluginContexts[pluginId].Unload(); + _pluginContexts.Remove(pluginId); + } + } + + public IPluginContext Get(string pluginId) + { + return _pluginContexts[pluginId]; + } + + public void Add(string pluginId, IPluginContext context) + { + _pluginContexts.Add(pluginId, context); + } + #endregion + + } + + /// + /// fixed: 由于 单例失败, 因此临时解决方案 + /// + public static class PluginContextStore + { + public static Dictionary PluginContexts = new Dictionary(); + } + + + +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore/lmplements/PluginContextPack.cs b/Admin.NET/Plugins/PluginCore/PluginCore/lmplements/PluginContextPack.cs new file mode 100644 index 00000000..7bf6a95d --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore/lmplements/PluginContextPack.cs @@ -0,0 +1,53 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using PluginCore.Interfaces; +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Runtime.Loader; +using System.Text; + +namespace PluginCore.lmplements +{ + public class PluginContextPack : IPluginContextPack + { + public IPluginContext Pack(string pluginId) + { + #region 加载插件主dll + + // 插件的主dll, 不包括插件项目引用的dll + string pluginMainDllFilePath = Path.Combine(PluginPathProvider.PluginsRootPath(), pluginId, $"{pluginId}.dll"); + // 此插件的 加载上下文 + var context = new PluginLoadContext(pluginId, pluginMainDllFilePath); + Assembly pluginMainAssembly; + // 微软文档推荐 LoadFromAssemblyName + pluginMainAssembly = context.LoadFromAssemblyName(new AssemblyName(Path.GetFileNameWithoutExtension(pluginMainDllFilePath))); + + #region 第2种方法: 未在这种情况下测试 + //using (var fs = new FileStream(pluginMainDllFilePath, FileMode.Open)) + //{ + // // 使用此方法, 就不会导致dll被锁定 + // pluginMainAssembly = context.LoadFromStream(fs); + + // // 加载其中的控制器 + // _pluginControllerManager.AddControllers(pluginMainAssembly); + //} + #endregion + + #endregion + + + return context; + } + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore/lmplements/PluginContextPackV1.cs b/Admin.NET/Plugins/PluginCore/PluginCore/lmplements/PluginContextPackV1.cs new file mode 100644 index 00000000..0a7f1616 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore/lmplements/PluginContextPackV1.cs @@ -0,0 +1,105 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using PluginCore.Interfaces; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.Loader; +using System.Text; + +namespace PluginCore.lmplements +{ + public class PluginContextPackV1 : IPluginContextPack + { + public IList SkipDlls { get; set; } + + public IPluginContext Pack(string pluginId) + { + SkipDlls = new List(); + + #region 跳过1: 程序目录下 以单独dll 出现 + // 获取主程序 已经存在的(不许在再加载的 dll) + string basePath = AppContext.BaseDirectory; + //Console.WriteLine($"PluginCore.PluginManager: basePath: {basePath}"); + // { "Core.dll", "Domain.dll", "Framework.dll", "Services.dll", "Repositories.dll", "PluginCore.dll", ... } + SkipDlls = new DirectoryInfo(basePath).GetFiles("*.dll").Select(m => m.Name).ToList(); + #endregion + + #region 跳过2: 打包进入1个dll 或 打包进 1个exe + // 注意: 用户可能将 dll 打包在 一个dll中, 或打包进 exe, 因此 通过此方式 确保跳过 + // 主程序所有 位于 AssemblyLoadContext.Default + List skipAssembliesName = AssemblyLoadContext.Default.Assemblies + .Select(m => m.GetName()) + .Select(m => m.Name).ToList(); + foreach (var name in skipAssembliesName) + { + this.SkipDlls.Add($"{name}.dll"); + } + #endregion + + + // 此插件的 加载上下文 + var context = new CollectibleAssemblyLoadContext(pluginId); + + // TODO:未测试 加载插件引用的dll: 方法二: + //AssemblyName[] referenceAssemblyNames = pluginMainAssembly.GetReferencedAssemblies(); + //foreach (var assemblyName in referenceAssemblyNames) + //{ + // context.LoadFromAssemblyName(assemblyName); + //} + + // 跳过不需要加载的 dll, eg: ASP.NET Core Shared Framework, 主程序中已有dll + string[] skipDlls = SkipDlls.ToArray(); //new string[] { "Core.dll", "Domain.dll", "Framework.dll", "Services.dll", "Repositories.dll", "PluginCore.dll" }; + + // 注意: 先加载插件引用的dll, 因为可能在插件主dll的Controller中立即使用了引用的dll + + #region 加载插件引用的dll + // 加载插件引用的dll + // eg: xxx/Plugins/HelloWorld + string pluginDirPath = Path.Combine(PluginPathProvider.PluginsRootPath(), pluginId); + var pluginDir = new DirectoryInfo(pluginDirPath); + // 插件引用的所有dll (排除 主dll 和 skipDlls ) + // 注意: 主程序中已有dll 必须跳过, 应为这些默认Context中已经加载, 而如果插件Context再次加载, 则认为这两个是不同Assembly, 导致其中的Type转换失败 + // 这里简单来说,意思就是当在一个自定义LoadContext中加载程序集的时候,如果找不到这个程序集,程序会自动去默认LoadContext中查找,如果默认LoadContext中都找不到,就会返回null。 + // 这里我突然想到会不会是因为DemoPlugin1、DemoPlugin2以及主站点的AssemblyLoadContext都加载了Mystique.Core.dll程序集的缘故,虽然他们加载的是同一个程序集,但是因为LoadContext不同,所以系统认为它是2个程序集。 + // 参考: https://www.cnblogs.com/lwqlun/p/12930713.html + var allReferenceFileInfos = pluginDir.GetFiles("*.dll").Where(p => + p.Name != $"{pluginId}.dll" + && + !skipDlls.Contains(p.Name)); + foreach (FileInfo file in allReferenceFileInfos) + { + using (var sr = new StreamReader(file.OpenRead())) + { + context.LoadFromStream(sr.BaseStream); + } + } + #endregion + + #region 加载插件主dll + // 插件的主dll, 不包括插件项目引用的dll + string pluginMainDllFilePath = Path.Combine(PluginPathProvider.PluginsRootPath(), pluginId, $"{pluginId}.dll"); + Assembly pluginMainAssembly; + using (var fs = new FileStream(pluginMainDllFilePath, FileMode.Open)) + { + // 使用此方法, 就不会导致dll被锁定 + pluginMainAssembly = context.LoadFromStream(fs); + } + #endregion + + + return context; + } + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore/lmplements/PluginFinder.cs b/Admin.NET/Plugins/PluginCore/PluginCore/lmplements/PluginFinder.cs new file mode 100644 index 00000000..fd456f23 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore/lmplements/PluginFinder.cs @@ -0,0 +1,42 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.Loader; +using System.Text; +using Microsoft.Extensions.DependencyInjection; +using PluginCore.Infrastructure; +using PluginCore.Interfaces; +using PluginCore.IPlugins; + +namespace PluginCore.lmplements +{ + + /// + /// 插件发现者: 找启用的插件(1.plugin.config.json中启用 2. 有插件上下文) + /// TODO: 其实是没必要再效验plugin.config.json的,因为只有启用的插件才有上下文, 为了保险,暂时这么做 + /// 注意: 这意味着一个启用的插件需同时满足这两个条件 + /// + /// + /// 依赖解析: IServiceScopeFactory + /// + public class PluginFinder : PluginFinderV2 + { + public PluginFinder(IPluginContextManager pluginContextManager, IServiceScopeFactory serviceScopeFactory) : base(pluginContextManager, serviceScopeFactory) + { + } + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore/lmplements/PluginFinderV1.cs b/Admin.NET/Plugins/PluginCore/PluginCore/lmplements/PluginFinderV1.cs new file mode 100644 index 00000000..051e755c --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore/lmplements/PluginFinderV1.cs @@ -0,0 +1,313 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.Loader; +using System.Text; +using Microsoft.Extensions.DependencyInjection; +using PluginCore.Infrastructure; +using PluginCore.Interfaces; +using PluginCore.IPlugins; + +namespace PluginCore.lmplements +{ + + /// + /// 插件发现者: 找启用的插件(1.plugin.config.json中启用 2. 有插件上下文) + /// TODO: 其实是没必要再效验plugin.config.json的,因为只有启用的插件才有上下文, 为了保险,暂时这么做 + /// 注意: 这意味着一个启用的插件需同时满足这两个条件 + /// + /// + /// 依赖解析: IServiceProvider + /// + public class PluginFinderV1 : IPluginFinder + { + #region Fields + + /// + /// 用来解析插件构造函数需要的服务 + /// + private readonly IServiceProvider _serviceProvider; + + /// + /// TODO: 未使用 + /// Key: 实现了 IPlugin 的插件类 + /// Value: 此插件类 允许的 行为(众多实现了 IPlugin 的钩子接口) + /// + private static ConcurrentDictionary> _pluginAllowedBehavior; + + #endregion + + public IPluginContextManager PluginContextManager { get; set; } + + #region Ctor + public PluginFinderV1(IPluginContextManager pluginContextManager, IServiceProvider serviceProvider) + { + this.PluginContextManager = pluginContextManager; + _serviceProvider = serviceProvider; + } + + static PluginFinderV1() + { + // TODO: 初始化: 默认为信任模式: 插件允许所有行为 + _pluginAllowedBehavior = new ConcurrentDictionary>(); + } + #endregion + + + #region 允许激活行为的 启用插件 + /// + /// TODO: 实现了指定接口或类型 的启用插件 并且 允许激活此行为(接口) + /// + /// TODO: 考虑后,还是没办法限制住插件恶意行为, 因为无法限制插件 修改配置文件 + /// + /// + /// + public IEnumerable ActivatedPlugins() + where TPlugin : IPlugin + { + var enablePlugins = EnablePlugins(); + foreach (var plugin in enablePlugins) + { + // TODO: 检查是否允许 此插件行为 + + + yield return plugin; + } + } + #endregion + + #region 实现了指定接口或类型 的启用插件 + /// + /// 实现了指定接口或类型 的启用插件 + /// + /// 可以是一个接口,一个抽象类,一个普通实现类, 只要实现了 即可 + /// + public IEnumerable<(TPlugin PluginInstance, string PluginId)> EnablePluginsFull() + where TPlugin : IPlugin // BasePlugin + { + // TODO: 目前这里还有问题, 不应该写为 BasePlugin, 不利于扩展, 不利于插件开发者自己实现 Install , Uninstall等 + + // 1.所有启用的插件 PluginId + var pluginConfigModel = PluginConfigModelFactory.Create(); + IList enablePluginIds = pluginConfigModel.EnabledPlugins; + foreach (var pluginId in enablePluginIds) + { + if (this.PluginContextManager.Any(pluginId)) + { + // 2.找到插件对应的Context + var context = this.PluginContextManager.Get(pluginId); + // 3.找插件 主 Assembly + // Assembly.FullName: HelloWorld, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null + Assembly pluginMainAssembly = context.Assemblies.Where(m => m.FullName.StartsWith($"{pluginId}, Version=")).FirstOrDefault(); + if (pluginMainAssembly == null) + { + continue; + } + // 4.从插件主Assembly中 找实现了 TPlugin 接口的 Type, 若有多个,只要一个 + Type pluginType = pluginMainAssembly.ExportedTypes.Where(m => + (m.BaseType == typeof(TPlugin) || m.GetInterfaces().Contains(typeof(TPlugin))) + && + !m.IsInterface + && + !m.IsAbstract + ).FirstOrDefault(); + if (pluginType == null) + { + continue; + } + // 5.实例化插件 Type + //(TPlugin)Activator.CreateInstance(pluginType,); + //try to resolve plugin as unregistered service + //object instance = EngineContext.Current.ResolveUnregistered(pluginType); + object instance = ResolveUnregistered(pluginType); + //try to get typed instance + TPlugin typedInstance = (TPlugin)instance; + if (typedInstance == null) + { + continue; + } + + yield return (PluginInstance: typedInstance, PluginId: pluginId); + } + } + + } + + /// + /// 实现了指定接口或类型 的启用插件 + /// + /// 可以是一个接口,一个抽象类,一个普通实现类, 只要实现了 即可 + /// + public IEnumerable EnablePlugins() + where TPlugin : IPlugin // BasePlugin + { + return EnablePluginsFull().Select(m => m.PluginInstance); + } + #endregion + + #region 所有启用的插件 + /// + /// 所有启用的插件 + /// + /// + public IEnumerable EnablePlugins() + { + return EnablePluginsFull().Select(m => m.PluginInstance); + } + + /// + /// 所有启用的插件 + /// + /// + public IEnumerable<(IPlugin PluginInstance, string PluginId)> EnablePluginsFull() + { + return EnablePluginsFull(); + } + + /// + /// 所有启用的插件 的 PluginId + /// + /// + public IEnumerable EnablePluginIds() + where TPlugin : IPlugin // BasePlugin + { + // 1.所有启用的插件 PluginId + var pluginConfigModel = PluginConfigModelFactory.Create(); + IList enablePluginIds = pluginConfigModel.EnabledPlugins; + foreach (var pluginId in enablePluginIds) + { + if (this.PluginContextManager.Any(pluginId)) + { + yield return pluginId; + } + } + } + + /// + /// 所有启用的插件 的 PluginId + /// + /// + public IEnumerable EnablePluginIds() + { + return EnablePluginIds(); + } + #endregion + + #region 获取指定 pluginId 的启用插件 + /// + /// 获取指定 pluginId 的启用插件 + /// + /// + /// 1.插件未启用返回null, 2.找不到此插件上下文返回null 3.找不到插件主dll返回null 4.插件主dll中找不到实现了IPlugin的Type返回null, 5.无法实例化插件返回null + public IPlugin Plugin(string pluginId) + { + // 1.所有启用的插件 PluginId + var pluginConfigModel = PluginConfigModelFactory.Create(); + IList enablePluginIds = pluginConfigModel.EnabledPlugins; + // 插件未启用返回null + if (!enablePluginIds.Contains(pluginId)) + { + return null; + } + + // 找不到此插件上下文返回null + if (!this.PluginContextManager.Any(pluginId)) + { + return null; + } + + // 2.找到插件对应的Context + var context = this.PluginContextManager.Get(pluginId); + // 3.找插件 主 Assembly + // Assembly.FullName: HelloWorld, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null + Assembly pluginMainAssembly = context.Assemblies.Where(m => m.FullName.StartsWith($"{pluginId}, Version=")).FirstOrDefault(); + // 找不到插件主dll返回null + if (pluginMainAssembly == null) + { + return null; + } + // 4.从插件主Assembly中 找实现了 TPlugin 接口的 Type, 若有多个,只要一个 + Type pluginType = pluginMainAssembly.ExportedTypes.Where(m => + (m.BaseType == typeof(IPlugin) || m.GetInterfaces().Contains(typeof(IPlugin))) + && + !m.IsInterface + && + !m.IsAbstract + ).FirstOrDefault(); + // 插件主dll中找不到实现了IPlugin的Type返回null + if (pluginType == null) + { + return null; + } + // 5.实例化插件 Type + //(TPlugin)Activator.CreateInstance(pluginType,); + //try to resolve plugin as unregistered service + //object instance = EngineContext.Current.ResolveUnregistered(pluginType); + object instance = ResolveUnregistered(pluginType); + //try to get typed instance + IPlugin typedInstance = (IPlugin)instance; + // 无法实例化插件返回null + if (typedInstance == null) + { + return null; + } + + return typedInstance; + } + #endregion + + #region 获取未IOC注册的类型实例 + /// + /// 获取未IOC注册的类型实例 + /// 此类型的构造函数可以依赖注入, 将通过ASP.NET Core 自带依赖注入系统进行注入 + /// + /// + /// + protected virtual object ResolveUnregistered(Type type) + { + + Exception innerException = null; + foreach (var constructor in type.GetConstructors()) + { + try + { + //try to resolve constructor parameters + var parameters = constructor.GetParameters().Select(parameter => + { + //var service = Resolve(parameter.ParameterType); + var service = _serviceProvider.GetService(parameter.ParameterType); + if (service == null) + throw new Exception("Unknown dependency"); + return service; + }); + + //all is ok, so create instance + return Activator.CreateInstance(type, parameters.ToArray()); + } + catch (Exception ex) + { + innerException = ex; + } + } + + throw new Exception("No constructor was found that had all the dependencies satisfied.", innerException); + + } + #endregion + + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore/lmplements/PluginFinderV2.cs b/Admin.NET/Plugins/PluginCore/PluginCore/lmplements/PluginFinderV2.cs new file mode 100644 index 00000000..ffd95a9e --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore/lmplements/PluginFinderV2.cs @@ -0,0 +1,317 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.Loader; +using System.Text; +using Microsoft.Extensions.DependencyInjection; +using PluginCore.Infrastructure; +using PluginCore.Interfaces; +using PluginCore.IPlugins; + +namespace PluginCore.lmplements +{ + + /// + /// 插件发现者: 找启用的插件(1.plugin.config.json中启用 2. 有插件上下文) + /// TODO: 其实是没必要再效验plugin.config.json的,因为只有启用的插件才有上下文, 为了保险,暂时这么做 + /// 注意: 这意味着一个启用的插件需同时满足这两个条件 + /// + /// + /// 依赖解析: IServiceScopeFactory + /// + public class PluginFinderV2 : IPluginFinder + { + #region Fields + + /// + /// 用来解析插件构造函数需要的服务 + /// + private readonly IServiceScopeFactory _serviceScopeFactory; + + /// + /// TODO: 未使用 + /// Key: 实现了 IPlugin 的插件类 + /// Value: 此插件类 允许的 行为(众多实现了 IPlugin 的钩子接口) + /// + private static ConcurrentDictionary> _pluginAllowedBehavior; + + #endregion + + public IPluginContextManager PluginContextManager { get; set; } + + #region Ctor + public PluginFinderV2(IPluginContextManager pluginContextManager, IServiceScopeFactory serviceScopeFactory) + { + this.PluginContextManager = pluginContextManager; + _serviceScopeFactory = serviceScopeFactory; + } + + static PluginFinderV2() + { + // TODO: 初始化: 默认为信任模式: 插件允许所有行为 + _pluginAllowedBehavior = new ConcurrentDictionary>(); + } + #endregion + + + #region 允许激活行为的 启用插件 + /// + /// TODO: 实现了指定接口或类型 的启用插件 并且 允许激活此行为(接口) + /// + /// TODO: 考虑后,还是没办法限制住插件恶意行为, 因为无法限制插件 修改配置文件 + /// + /// + /// + public IEnumerable ActivatedPlugins() + where TPlugin : IPlugin + { + var enablePlugins = EnablePlugins(); + foreach (var plugin in enablePlugins) + { + // TODO: 检查是否允许 此插件行为 + + + yield return plugin; + } + } + #endregion + + #region 实现了指定接口或类型 的启用插件 + /// + /// 实现了指定接口或类型 的启用插件 + /// + /// 可以是一个接口,一个抽象类,一个普通实现类, 只要实现了 即可 + /// + public IEnumerable<(TPlugin PluginInstance, string PluginId)> EnablePluginsFull() + where TPlugin : IPlugin // BasePlugin + { + // TODO: 目前这里还有问题, 不应该写为 BasePlugin, 不利于扩展, 不利于插件开发者自己实现 Install , Uninstall等 + + // 1.所有启用的插件 PluginId + var pluginConfigModel = PluginConfigModelFactory.Create(); + IList enablePluginIds = pluginConfigModel.EnabledPlugins; + foreach (var pluginId in enablePluginIds) + { + if (this.PluginContextManager.Any(pluginId)) + { + // 2.找到插件对应的Context + var context = this.PluginContextManager.Get(pluginId); + // 3.找插件 主 Assembly + // Assembly.FullName: HelloWorld, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null + Assembly pluginMainAssembly = context.Assemblies.Where(m => m.FullName.StartsWith($"{pluginId}, Version=")).FirstOrDefault(); + if (pluginMainAssembly == null) + { + continue; + } + // 4.从插件主Assembly中 找实现了 TPlugin 接口的 Type, 若有多个,只要一个 + Type pluginType = pluginMainAssembly.ExportedTypes.Where(m => + (m.BaseType == typeof(TPlugin) || m.GetInterfaces().Contains(typeof(TPlugin))) + && + !m.IsInterface + && + !m.IsAbstract + ).FirstOrDefault(); + if (pluginType == null) + { + continue; + } + // 5.实例化插件 Type + //(TPlugin)Activator.CreateInstance(pluginType,); + //try to resolve plugin as unregistered service + //object instance = EngineContext.Current.ResolveUnregistered(pluginType); + object instance = ResolveUnregistered(pluginType); + //try to get typed instance + TPlugin typedInstance = (TPlugin)instance; + if (typedInstance == null) + { + continue; + } + + yield return (PluginInstance: typedInstance, PluginId: pluginId); + } + } + + } + + /// + /// 实现了指定接口或类型 的启用插件 + /// + /// 可以是一个接口,一个抽象类,一个普通实现类, 只要实现了 即可 + /// + public IEnumerable EnablePlugins() + where TPlugin : IPlugin // BasePlugin + { + return EnablePluginsFull().Select(m => m.PluginInstance); + } + #endregion + + #region 所有启用的插件 + /// + /// 所有启用的插件 + /// + /// + public IEnumerable EnablePlugins() + { + return EnablePluginsFull().Select(m => m.PluginInstance); + } + + /// + /// 所有启用的插件 + /// + /// + public IEnumerable<(IPlugin PluginInstance, string PluginId)> EnablePluginsFull() + { + return EnablePluginsFull(); + } + + /// + /// 所有启用的插件 的 PluginId + /// + /// + public IEnumerable EnablePluginIds() + where TPlugin : IPlugin // BasePlugin + { + // 1.所有启用的插件 PluginId + var pluginConfigModel = PluginConfigModelFactory.Create(); + IList enablePluginIds = pluginConfigModel.EnabledPlugins; + foreach (var pluginId in enablePluginIds) + { + if (this.PluginContextManager.Any(pluginId)) + { + yield return pluginId; + } + } + } + + /// + /// 所有启用的插件 的 PluginId + /// + /// + public IEnumerable EnablePluginIds() + { + return EnablePluginIds(); + } + #endregion + + #region 获取指定 pluginId 的启用插件 + /// + /// 获取指定 pluginId 的启用插件 + /// + /// + /// 1.插件未启用返回null, 2.找不到此插件上下文返回null 3.找不到插件主dll返回null 4.插件主dll中找不到实现了IPlugin的Type返回null, 5.无法实例化插件返回null + public IPlugin Plugin(string pluginId) + { + // 1.所有启用的插件 PluginId + var pluginConfigModel = PluginConfigModelFactory.Create(); + IList enablePluginIds = pluginConfigModel.EnabledPlugins; + // 插件未启用返回null + if (!enablePluginIds.Contains(pluginId)) + { + return null; + } + + // 找不到此插件上下文返回null + if (!this.PluginContextManager.Any(pluginId)) + { + return null; + } + + // 2.找到插件对应的Context + var context = this.PluginContextManager.Get(pluginId); + // 3.找插件 主 Assembly + // Assembly.FullName: HelloWorld, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null + Assembly pluginMainAssembly = context.Assemblies.Where(m => m.FullName.StartsWith($"{pluginId}, Version=")).FirstOrDefault(); + // 找不到插件主dll返回null + if (pluginMainAssembly == null) + { + return null; + } + // 4.从插件主Assembly中 找实现了 TPlugin 接口的 Type, 若有多个,只要一个 + Type pluginType = pluginMainAssembly.ExportedTypes.Where(m => + (m.BaseType == typeof(IPlugin) || m.GetInterfaces().Contains(typeof(IPlugin))) + && + !m.IsInterface + && + !m.IsAbstract + ).FirstOrDefault(); + // 插件主dll中找不到实现了IPlugin的Type返回null + if (pluginType == null) + { + return null; + } + // 5.实例化插件 Type + //(TPlugin)Activator.CreateInstance(pluginType,); + //try to resolve plugin as unregistered service + //object instance = EngineContext.Current.ResolveUnregistered(pluginType); + object instance = ResolveUnregistered(pluginType); + //try to get typed instance + IPlugin typedInstance = (IPlugin)instance; + // 无法实例化插件返回null + if (typedInstance == null) + { + return null; + } + + return typedInstance; + } + #endregion + + #region 获取未IOC注册的类型实例 + /// + /// 获取未IOC注册的类型实例 + /// 此类型的构造函数可以依赖注入, 将通过ASP.NET Core 自带依赖注入系统进行注入 + /// + /// + /// + protected virtual object ResolveUnregistered(Type type) + { + + Exception innerException = null; + foreach (var constructor in type.GetConstructors()) + { + try + { + //try to resolve constructor parameters + var parameters = constructor.GetParameters().Select(parameter => + { + //var service = Resolve(parameter.ParameterType); + // var service = _serviceProvider.GetService(parameter.ParameterType); + using (var scope = _serviceScopeFactory.CreateScope()) + { + var service = scope.ServiceProvider.GetService(parameter.ParameterType); + if (service == null) + throw new Exception("Unknown dependency"); + return service; + } + }); + + //all is ok, so create instance + return Activator.CreateInstance(type, parameters.ToArray()); + } + catch (Exception ex) + { + innerException = ex; + } + } + + throw new Exception("No constructor was found that had all the dependencies satisfied.", innerException); + + } + #endregion + + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore/lmplements/PluginLoadContext.cs b/Admin.NET/Plugins/PluginCore/PluginCore/lmplements/PluginLoadContext.cs new file mode 100644 index 00000000..7a0f154f --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore/lmplements/PluginLoadContext.cs @@ -0,0 +1,32 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using PluginCore.Interfaces; +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Runtime.Loader; +using System.Text; +using System.Linq; + +namespace PluginCore.lmplements +{ + /// + /// LazyPluginLoadContext + /// + public class PluginLoadContext : LazyPluginLoadContext, IPluginContext + { + public PluginLoadContext(string pluginId, string pluginMainDllFilePath) : base(pluginId, pluginMainDllFilePath) + { + } + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore/lmplements/PluginManager.cs b/Admin.NET/Plugins/PluginCore/PluginCore/lmplements/PluginManager.cs new file mode 100644 index 00000000..40a164e7 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore/lmplements/PluginManager.cs @@ -0,0 +1,65 @@ +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using PluginCore.Infrastructure; +using PluginCore.Interfaces; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.Loader; +using System.Text; + +namespace PluginCore.lmplements +{ + /// + /// 一个插件的所有dll由 一个 管理 + /// 记录管理了所有 插件的 + /// 是对 的封装, 使其更好管理插件加载释放的行为 + /// + public class PluginManager : IPluginManager + { + public IPluginContextManager PluginContextManager { get; set; } + + public IPluginContextPack PluginContextPack { get; set; } + + public PluginManager(IPluginContextManager pluginContextManager, IPluginContextPack pluginContextPack) + { + this.PluginContextManager = pluginContextManager; + this.PluginContextPack = pluginContextPack; + } + + /// + /// 加载插件程序集 + /// + /// + public void LoadPlugin(string pluginId) + { + IPluginContext context = this.PluginContextPack.Pack(pluginId); + + // 这个插件加载上下文 放入 集合中 + this.PluginContextManager.Add(pluginId, context); + } + + public void UnloadPlugin(string pluginId) + { + this.PluginContextManager.Remove(pluginId); + } + public Assembly GetPluginAssembly(string pluginId) + { + IPluginContext context = this.PluginContextPack.Pack(pluginId); + Assembly pluginMainAssembly = context.LoadFromAssemblyName(new AssemblyName(pluginId)); + + return pluginMainAssembly; + } + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore/lmplements/PositivePluginLoadContext.cs b/Admin.NET/Plugins/PluginCore/PluginCore/lmplements/PositivePluginLoadContext.cs new file mode 100644 index 00000000..9440ffc9 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore/lmplements/PositivePluginLoadContext.cs @@ -0,0 +1,112 @@ +using System.Reflection.Metadata; +//=================================================== +// License: GNU LGPLv3 +// Contributors: yiyungent@gmail.com +// Project: https://yiyungent.github.io/PluginCore +// GitHub: https://github.com/yiyungent/PluginCore +//=================================================== + + + +using PluginCore.Interfaces; +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Runtime.Loader; +using System.Text; +using System.Linq; + +namespace PluginCore.lmplements +{ + /// + /// 修改自下方 + /// https://docs.microsoft.com/en-us/dotnet/core/tutorials/creating-app-with-plugin-support + /// 此方法依赖于 插件项目生成的 HelloWorldPlugin.deps.json 与 runtimes 文件夹, + /// 虽然官方文档没写, 但最好还带上 HelloWorldPlugin.runtimeconfig.json + /// 插件项目 .csproj 其它注意, 看文档 + /// + public class PositivePluginLoadContext : CollectibleAssemblyLoadContext, IPluginContext + { + private AssemblyDependencyResolver _resolver; + + /// + /// 即 插件的 Entrypoint Assembly + /// + public AssemblyName MainAssemblyName{ get; protected set; } + + public IEnumerable ReferencedAssemblyNames { get; protected set; } + + /// + /// 加了一个可回收 + /// + /// + public PositivePluginLoadContext(string pluginId, string pluginMainDllFilePath) : base(name: pluginId) + { + _resolver = new AssemblyDependencyResolver(pluginMainDllFilePath); + + // 主动加载 存在此插件文件夹的 依赖项 + using (var mainFs = new FileStream(pluginMainDllFilePath, FileMode.Open)) + { + // 使用此方法, 就不会导致dll被锁定 + // 锁定dll 会导致: 1. 无法通过复制粘贴替换 更新 2. 无法删除 + Assembly mainAssembly = LoadFromStream(mainFs); + // 上面 FileStream 处于打开 pluginMainDllFilePath 状态, 不能使用下方方法 + //this.MainAssemblyName = AssemblyName.GetAssemblyName(pluginMainDllFilePath); + this.MainAssemblyName = mainAssembly.GetName(); + AssemblyName[] referencedAssemblies = mainAssembly.GetReferencedAssemblies(); + this.ReferencedAssemblyNames = referencedAssemblies.AsEnumerable(); + + // TODO: + AssemblyFile assemblyFile = new AssemblyFile(); + + foreach (var referencedAssembly in referencedAssemblies) + { + string assemblyPath = _resolver.ResolveAssemblyToPath(referencedAssembly); + if (assemblyPath != null) + { + //return LoadFromAssemblyPath(assemblyPath); + using (var fs = new FileStream(assemblyPath, FileMode.Open)) + { + // 使用此方法, 就不会导致dll被锁定 + // 锁定dll 会导致: 1. 无法通过复制粘贴替换 更新 2. 无法删除 + LoadFromStream(fs); + } + } + } + } + } + + protected override Assembly Load(AssemblyName assemblyName) + { + // 1. 先尝试 从本插件文件夹中搜索 (无需, 因为前面已先主动加载, 再次被调用此方法只能是其它情况: 有不存在于此插件文件夹下的依赖项 被需要) + + // 2. 再尝试 从其他启用的插件文件夹中搜索 + // 实测: 可在下方搜索, 主程序包括的 assemblyName 与 启用插件加载的 assemblyName 都会位于其中 + var assList = AppDomain.CurrentDomain.GetAssemblies(); + var temp = assList.FirstOrDefault(m => m.GetName().FullName == assemblyName.FullName); + if (temp != null) + { + return temp; + } + + // 3. 最后搜索不到, 返回 null, 即代表使用主程序提供, 如果最后几次都为 null, 则会报错 + // 当启用本插件/触碰到本插件中的一些类型时, 而当主程序中没有提供相关的此 assemblyName 时, 也会触发此方法 来尝试加载 + + return null; + } + + protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) + { + string libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName); + if (libraryPath != null) + { + return LoadUnmanagedDllFromPath(libraryPath); + } + + return IntPtr.Zero; + } + } +} + + diff --git a/Admin.NET/Plugins/PluginCore/PluginCore/nuget-pack.ps1 b/Admin.NET/Plugins/PluginCore/PluginCore/nuget-pack.ps1 new file mode 100644 index 00000000..c3c10d7a --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/PluginCore/nuget-pack.ps1 @@ -0,0 +1 @@ +dotnet pack -c Release \ No newline at end of file diff --git a/Admin.NET/Plugins/PluginCore/Test/Admin.NET.Plugin.Pay.Alipay.zip b/Admin.NET/Plugins/PluginCore/Test/Admin.NET.Plugin.Pay.Alipay.zip new file mode 100644 index 0000000000000000000000000000000000000000..31169e0cdf1aa0ff2702603e9a207844d23767e1 GIT binary patch literal 27799 zcmbrk1F+~&uq}9O8~50@ZQHhO+qP}n);+du+t$DD4eHm-fAfCTB-z=WPO7_Wb?-`L zrRAl7K~Ml7{_P63#+m^C(*_BE51=R_AS@$7Yh&!<4h;YZ@*j8q^Q58-3jkh~JYwd5 zFkX$zlL``dSJY}xmv9;xWw&+uv!Nf?-|8|a)$TRE{o%n}v&A-^&E8LoRiKd5OSE3=?5nJd z#Ka`cT=nG2qC4l)RBxuHGXh+FKrpL%<94E#hrKCU5i~%bK zsH28$PQJPl3cj+kmI**x`m~?`@>1XsLF-N2yZ->d0tEPX{a>L5`wulI6K7`&TQetG zOD8+q{|>pTlj8&B2l46gakWN8`G3&?WAa?4hlVD*cUS`chhK&rqB5L+ewqG_|HJP; z;s2E_h<|?F+}s@P?40TTE0?kVXR`ilD=`0)6(c97|4mE(tLj1de^|0Hu&|~5XY#*_ z?f)tyuRgE7R6!OxbTeB+1y9r5l)wO%c%}IC#Kep=xc%h^$9qQ#GBgzb_5a*|8M;MSMehKPw`sV z8k@M&nmgNA|98>4D9A_+@gwlG&Hd7^H=W~mtTm2{fZDy_hmWpVXYHK?U9B{~28N3Z zM1kL*70B0v57aGum}M?y`3=Y{Bpq0oo%XOj?%ce7C0Tlfb%~MH_}G`s)@Zhm7!4Flcs3T@ zBJ=|#<{nLP>Su?I8cgM&-MaMn8?Mh{X4VS5aF2%;ovdC`>%g_)_jK#h2b;V=zCSi0%2xG0`Qe-XnK#qe1??ikH*$W!icPl|23g(~JtRK}=IO21 zqoWWV8B)%gx*LFvY}xwM@eKSQ<*jX>mdpnT0MPPp{MXBhLi0L~8MoCe02xk+RPmCoZ1^?p1)mcuT( z0?x@DRD~d7e`8{&^ZYRLu0+(p@UT886HyCQYtM0{fC`f^*5e8r!O2Hql6fG2=WNca zza*iV?MgfArNK-75qW$0p8n_-ybW#$d*@0>At~L zW1WDUmyb^4(pvd4Gr{RIyW$L$*jf;R0Z{xW%W=(ASkN&nI*TgA+5muQR8=Lb|G4av zos+0=oFF|h+KoiEDGsXO_z&1^f1$YP&{UNRC!Emce6R10!u@&v%FjbxmJ$0@sQz)# zxd82;f)C8Qp&GcZPG?cY6Qs*RzmCS{u+|9JQM51^>ae(x9VcYo^0H;SEyw=gy;{sh)WK_1M(SWLK_i>~Gu>ZU3z+I-%fSSw3q9DG{x(R zcvki^@e~D_YCbO0vlES0OyrgM6`fJ>jx0FOZ0MM7>E3QJnu?UHt{r%6dsFSJ?W6kw zUz&9LeU9iKyTAMvY34&$;%;|Zt}~nN*5=7Fyr7T%<{a~b3+p@ZiyIiae{SH)O$E-( z3+bc5qjcDo!}s&R+mV&n^Wg1#*4ua41*UFejU*c9dc=~@y$J^y!GGc*W^0-_4-+y4 z5N$%d(RVnmYo$mGbw)M3G=-dn|8tle)6Z6<`n}L3ZIP0`UqLLEpqCWm5?|JLf zBZ3bo8ry5P$FbU9QEq`qcTXtvqcPdhi9BE{c)Wp;Ls{oA9?rj76!RAi^4|*^a9s4D zc}SA)Iqa&sshTGO)mz3hckosj*C`ck`+h%6Xh?bSGNP)YjryH1z4d0WU$0W*r8)Wj_M_*@4fzry8((0h zN~eG`um^{?`IC{!WrT7-4K2(`)eWWcRu@|*6h;E_uflumc&zOR{L+$9EL~fiqm_$ zjreiM2oBJJ+hm*2b@w9ZKt@ZohhpE(3 z!!(#ne{}SlWPkPaxz2Y)KCRx9ZuEq|lTJbKea$|V^_f0y#w7zve>t^gN9`zS*P#5O zLUx={F6Ck|r}T__S=xWr&`yV_zm6KN+H&sAK#P_0@Ol^l8%7P@9vcp~d46BhYVjRT zQSRTFRv(1zmVLIoNWH?wx1WL^G~hgrlay@Pf=5&4Zjmu2Y)bYEtK`eKF#n1Wr`PB? zh5cBnxZXBj9u8D&q3+xMJXV;q0{wFLcTwB)p%t0v*qhYSzy6tkz9|#Db?no{I8As~ zjpjYO;u+Z;#N>>Tx^414H6};b)E^D#j(Jw1Ne^56eHeetfeCnx@Z)F{cASfoyTEf( z3~ChYJMqDpojWzK_jsCB1bR^aefR!7cUJ_waU;a};JDl+f=4IH`?dc#%!W^=`4J@P zHqfA5za`oMf8TaG!vp?YSdAms6Uknc?>rFRSf*Q*1m?w6_s;J6j(Bz{-5xG3+!*!F z6(c>Q@33O{JQ@Di_McEXso#I_X` z>=XqGY@`uoBVpAB)Fy-Ul#O9MPq4d0GGbg9vS~%f5PGaApYE;C!2_Y)0iS^IrSG@(5Ie`Y_qGuC2lW zOdzC)A`~@9NRAz-wK*aRsj_)sQ^Hh;HI3D1jsIz~VEdRjrAa>)eMk`Yz*Mz78oFSP z@iZaT=sXXuyhpo;2< z6KaYq6yupS=h_2o`d3&?YwxAkY}x|^2~-;=i}EVm0UREM;UyGX1Ep2-!2hszO$5<_ z$8e54+6Z|2wmaTMhc;i5aMjKvb@*H3TW4vO@vQRTs^4J}yYtz1%P;|u0}rr zg2#scG4%Lee%Eoz06dm;Q{!)udJU^YJi+wy_>JV6MNJL&O2Yld-waxI6@y9LXG!~3 z#+ZSY5v2IO3!qc(nuTC!l1Y^iP1Wa>$T|EXUVrVB3mK20Tl;(Kne!g`r-PY|wRGro zR>Un~@>DIjeAlw7J^P_x(u1|wsocq8M7E?JKqRc-nwI_$g+DD>5zoDUHPsBI=zrJ8 zHt+OIJ5)e-tYD(SfGLXXxA)nt$1A#aOxyKbv?>|a`BRUqlX^~_R$?pN+LjDtIl@&D!e|y{f+a>OTPX(c9hH|d zm3ZZvVO;M!Zt5OR@07ybblU4VD0Qyr;jUQj3YMYSjy2bD)rU34aB{C|9l>Q*EVbMC z(}i7Oi(j-1D!M;r^5?$Y!EJ(L5~wuP5VXYfY6eCwm_j%(6b_jm)1`&Ef^-=J4IQnu zcBIeouZk}4x&b6vm$B7KrL)x?1`?M|pTLE3f?G-;N5RlBY)9!=oT(I1g&eQtTBaI( zUeDVtvjdM;GKddOzi>0I9mZ?iT7PBYXPtu~7IFf6Y4ea=)}+RXuoldY)0u9k8z z8+%v_lGzW5cTK1QIk}Lub*6bmJyV;`Hf<60Ku`7)*G*w7Mi65`d+@r9$gKCS%%wff;4rZ3l^{wrl^VV)DZjbc3;N z5plx!OD2y%N4oI@Rsf*QM^`=Z1E~cGOAu0fGvBIZIwMY@;rqRX>2I@M$DpljTfX?HxlDH)ZB5nf3*_tvQTwi(Nv2G^P;Icu?6 z03IxvMUjR2yaI;hBwD}TcqjoR%F|2_eMQP^DD@2?yJ=$=^tVTK2lmY?ZmX)aH<`l~ zaYo!u&bmeb`lR$3jB)Yf(mw}RllST>MFhw$?2Ir+Ci{Hl1x;TjPHV)u<~ivYX&LRn zy-g}>!xYAel}7#i;#lxAQteL12y!K@(*w*}U{FpGh!SrLeENvr1xVs?W3tsi=rEvp zv*~50`2@ep{HzeC<&C4QM6iC=y*>xu`VT%0L&+*FGBalmnjv2MUp18Qy@I6;`Rt#I zv0JA;bnnI2$g4QdGA_J!;$^%DWECb6(*i3&*3m!aW@7U!O&yucz^$i^v7*hP-oj$+ zwVS1L+X2OVW%%CfVe6oDLNE2SFz&>#u3F$5iVgg;r{Qge?q)j47T!g**)jX+O zMG_DLOfB0{r*MKK?bVxvZQ1?Tp2j^u=Vpz`QPpZ7v%>*SFB4AAph+r}TBPP1kx|w9 zwB|St(?~efwKW?;4|8%Vp6syF$qDeb7c&$deNH7ohej){J0&AYHb&-}3m}Dy=EUr{ z3m4Vl3dE3q1KpVoJWTH@t{sbkeLvAj`UJYQz{ik5ckn&<>`EGWnEDc4pK_JoRL-Jk zPt2P{Wnj69N_nz$-@OL*;WjDamXm~W%321tWFaT7X<#X9DVoD6MlubbWRqmv3YJ_{ zbCe;Y8JXkC673Lqa!pffJej__hW8cYiawux)ptfKP+dk>+rZdX+(K+3l_u zTLf9$ZonY>Ik{oczr3ZeA&TmG;crOCYxg@iON1B~&b365u2xvNv@kyspIU7!7q$s~ zk7&KY8ob~vORBl(K;YDMgHeOpxDmZOXgo826j9rHOqSz=9Pr^n|D=<~P^;3+ZAncZ z7u2@32070gW*rD`(^TK6qB(=)aoN_a;;&={?AVOJ&ZPTCc^=4(V9azV>Jd2{{j_;< zmU5;CJeHjD)>b+4=c+y?D-IB6PUy1OYo%v{Frg!Z3}Gh?S)X$kiGC?Z97Wh79n>=nQSXWHvSSw63b5rv@?k7=Y1@3i|X5J2|cjqtjN` zE$~Mto~->TNWv;EXA0YR(xF5As|x{@sedJk+;yHj04dCdBxS&(_RX@>_0E!lqYp1g zVIzm^;Xit2TQmxrc}fNXx@`HVGnD#qRWv7@I&or1RUS$vgOgI*3MtIV)QWpn~|l8S&_QbbrI#vxbx z;8jgz*%~|d0T;JxU$?)uMEcf`J9T_K{??BMeme* z@Wxornc!58L^-j`1BSGC{Wrwbc-bw4CX*VwPJ-z;R=wEbBl)D8EjP5bvt;b;&#PnJ zDrGjyEJU1K z8qPr+7VWW0OiGS}pXF2KHvy0IeYa-N_sMaE7G}fq_H(J?leo7%1@|b2F~z56GwY^h zg(UVpvT{{eMCP*0h!Jde7>R1!xA1VVnbB_F4(tFWvNl7UI9j72_>Sg6??C4xPa?C?&IspOkpo zEi~py*KotFiyveB{qBLVp;pa{wiDXHLS9>epA!JA0o(62;d*T9dio3Y88?}7WOZ-lvY(1=pNg+2H?x|cwrsd$e5x^s3;U+PSYqz&5++I( zj-Nxs1ZhpF_)7yrYYfkz?EBzUP1y8KuqvnKe(cODh|g2?;Lt`OH-+M70_r{s$9n`F zFk7gnLR~pY!V#Ff)?PUtfyjXRY~0=(9GL@`c~TJVtcfWI|b z`sXz1CtE(Aa?L1;er2uXkV$@PLh;Iwe#EADr^~Bq!AJRizCL$suQh$#m5)>SZgVoY@}v_X^?*VlAz(m4g8^VMiUNochza5$ zIt_=Wr(_sS7~Q(vz{U)%%NH!$wE85iwl`EOtgMz(Ypx}0v}#(kT9<33KDj$pDz)|# z$~J6XE!^^asc&w(Uu8`n0A03!8fI;8k8(eA<6gT@vtPUQ*xC+fZOPCPI~@AHS?3Rp z_0;C@dPN<=PW^bKm+F z7!y6cH=6rNVqECiUzEhAC^@7J5*t--<%c8KO3HB_y;A|mBJ zs%1_mMZDme$bV7ujNm1KkT&U85WO^fqOfex3him!%k@! zv{XMTr4<{BDK~u8mX^VN;qPZX0D%Nv}mZ_%iA){ zZ=0*nb#(zXQ}_2OuRuK{E-48#0kag-W<{!&`~f-9ZRK}}83URGv3aB#1EHWg=0tVE znotytP#JeE?$!AFJ=N$m5I1cWixdPLV3;M6p+myB42oHhCQ4tGGWNlW6x>djJU`N{|T)E0WL^FoeiF zUkGKvCXvlA$f%$~42eFN)!1Yp1BH7WlGX&MAcBWD`3)jEA2*S$Z}2t|E?iA>o|9h6 zhpas{zC01mx_qHf$^!FAR+>+i(WmNAZ^~w`%0{osTCa%p?nJTOajPBfIW|!PYOI}< z&F;xcXSv`QgJQv-BY%*kW67AZebyO5lMnH58NM+GCb`BEhb8Fu)H=drd@+X)j#cL0 zhjrsl5EC0kYZBqB9g^XvK-!diAim-%FwzwM60U>T<$`*{B1R_#{+Ce2eO$=LriG$4 zoWodmLkMBmoIp=gmCh9GHxFB|KrXcmg>p!f=Re673;Ll0O3NvgjKA5_ZrTZ&R7-1? zt+z|v6ze-{SX`WA3>*?3=bvO1SntG34h>F=I%np6(K;D8TzH-is#{vf=5OSK8+#}B zb>!e3YSAs_ZRR0f_fJ@IFfl99(x;?2rE!ltvrCxLIUJ0OrjBh`m-371vzX|J4N~{a zcq-&p%#A~eH~2&GcH5)|ZKIr~SZNL|q-dEWA;qiw;s8*(V2t%;d zUnojIe_ZNS)Dmn&B@vGCtzuf(sa^h9b+0$hX?tw)$n8P9(_W9}-ippjiwgy90?7XsPEiD`f;utp-+0HA0~6uFS$Z z*|>Zklu>JGyW<4whF+W*gtaJ|)kvD_USqlZ$;ab7Z!63--MGZ|RIZ(u7qeyz4bt}7 z10(S8@E$i=GMdeBjN5NL^`mrhckC$ zwXKuXvuC}=ZBZi~GUct6A~?L#nb zS2nnDHGE4>qkwJPf_$fZ=n66f)VZj5R881KMTt5mS=DWcT*%E=7v+No6Up{HHD$k$ z`#5xtIJ+M?H!dH=&c}P}4YFm2X^-4_=z*ynO?#RxSgt+Y|6IP{3E)`5^TgGqR?K*C z;A`tbLSRgEl>ROeD}mF~(5HD`kZR7|L9*U=DhONRaAuUhtSWqvmoIOYHTe)6W1e?9 zfIYL0&n#-Bsf!LOd%m(kqXie7?q#Sqc6 zJK#$btB?0f0L}%jr{mmJbeu4UxL*oR@-nagGj?shREe|<|eCWQ5JA?~(Dmj`1DYTbHb0Dxe?*iW9$PbufWol&5EI6f;4o4^KlaDSFlrA~FtP zf{so?#Q3WUhVc`Wwm-|we1kR1gM8p|#w0-d`*-v>?%lmjYlMqT^T4PR&Com)N?Sk+ z6`OJiMw9Bsb!pzd?Ozx+ z;Hc9VGA<$y46mrkFys;hUn&=s=K}tm@EE;-4KU5?H>Py)`=^Z|lr_>3vGCZiGhM6? zo>u5x;#QKMrCD{0AQx7fCY6$=fL8+-ZFKlyuBK47L$F7Nn@0EX4H*vVgOj#$CoURYe_|hCw`|DjL`=b`!VQ`qnE?mG7w^+jC0#^XaW4Abx^6Myt_PQo zh_%{ycAD6TPX=B&h}OGmeQ3GxU2sIddzXF18K~CBYI(#{%f>To3}5hb-&ev7DLm!W zxt67q2qP13l?bCyE@<{N?^<@#6{vo!4KQ^%jeeid0X=6w)eU>)6;4W$xCOcMu$v;q2dDo$C@9{elf6YH zf0PwvCmW^g*NNJ_BiJJ!2sLccwB6$NG&_rVm5K()4zVBgi~#qdGpORF$t&opN>V@>Yh!fTMl11lc_~0&Vofa4YoboQ z2zC%#!$?M6U7U8`q2?9d z6}ZU}w*8KMX36Y;d1;Zc^7OhaXULj?FJ?>)VmW@F-^m<@KQCQLTX|hlh%>z&E?UQ$ zA#UF)I0cJ23iYPUS&|9c?beezCT?XhIew|vwP~*^_e5oBHG9GIa;YexprDXJ<8!q< z@7_t8Eu@)KR1Hg&f^p38v}|tP7vRZZ#n6!f_N;xa%xwj;+)9xIGGO87j5g-2VN-am zPgx8*z>aa&lsi%s9f`DCVCu|}l$Y3So(wK=Z6Hdahn+l|9nPw;F3a6CPxtIL!aM3h z7FCr?biS;NE_uG(WWA`OPtDB9t zq3DUFI9{Mky0gB4+y;z%yAXOfeU3tidb=|1QxPo7YWcXH3DhHnJ%>YA-hr23J8gd} z-%w;8v=awKLm_2oG%%(PCf}MKLs%tmKJVx$XK60&D8`#)q#SByMHzHr9N+1La5aY% zBz>{sbZB3Na*jE3;ha+9R-8EA+^fd~edBC3l+}SQg>f3?oeiBtL{c$ao~%q+(#i#? zHeoxNH5TlyhHC;+#K*Fi>_~bV(IL=ub#^sZLmgPMc%n_q6>81w;x5khbotnsVl}_` z0dPp;spqQ?&>~~h7u^;O#n@TfrZy7H&Y?I*$Al1U>;xw^-utC)QmazzahR=B3!-z@n(iS_A8Jn>rzTHbiI@2FTtqaFd;TjmaGI)vuqhUX%f$m<750 z`R|b<&8+!0Pt(R28;C*K?dCbiq>SqfUk=m$rG?WJO6RO(Dh9pV+mZGu*xUZIHc9E@ ziei=;`16cEqlLlC*)lC>s)N!d8mNgJqhX(fyEn}1GjKdLIPoP5AL0k!)0jj|+{Q;W z5)hsgh5UgNx4~Dv)TS10Z5{0p=+#JrL5d40HKYVrNR5rDiX6LAByM7wZW6? z)MP3fWCcw9^JSf>!9(fHNTJlxcpnaB;OUDc$6(iPc#7LE-L`OPdsC#wDH%F32ld-FU3t^ONQ;((Ww(w*EUs0lP+{mD)$PPkF|86Qp-Dh8<*^Xpzb zN&KdLv${c&mTkCW=a^EpQc93$oIVAF=Tryz>qp8bQbGZOCt^&s80rz zeYu`6U5}Ye<1rRGOcgyQ>_xC8NEnDA##l>?z?_WkdQg*`&!*5N6k7JGzmL{X_0Gmk z=gNX$W&Bpw6VER%s32DWZ6%DZMkN@pziY}p)7ip~g@)_YVoG7D14taDmHwPt<0sPb ze0}^;J9Wp49j?)8OjgAnlx;>G8k71gOdJNT=ROd|4hoo)VGa0ccN=BJsn&(zW-w(K zaz`*tb15_wHuy&B9m<1PZC4Ez-&ll<&qx}S^c3O9r~ZSTEF*1DpkAe6huo1h(6I*x zf_5@I;D5WOmgFhZHoyZ$N|$?jItG$iED9r?1GjQ7MLOLuJ<-Ld9zM*Db@{fc$WIceWa(;Hiu zB@_)LHg1JueU|MU#6ZJ!Nv(0}lEV?JNk=;}wZIcKtb-!--Nq%{irI#vXDkhV}^mjQk&B$4N!EA(V3R6hv_CnpNkqb3 zzPwzYd8(i2{u+i3PKul&;Zh~EsI9ZR`~=k?Pi?9kLovBNOMVfZ1Y2nmR7YE+)<6A} z4qfu7bIb*O)(KqLxOJ0?@No$fwMa~OO^z5k<){W#Fj);TYRZTv0a0G14k=5OfpW&}_W2vzK#N`!0$kK0cwUljFQucJ5np#?SE?Zd3^JG0pxpU?5;!q9C?<0lO;G7Rp~Jy0+{ zMdiHJbGbB6h8yv*OX0;D$2|TsRQZ2RBbzxZf(1GXsAwG533emKzB)wvE)W592r9;8 z4t5S{xKvS{1{Y2AfMEF^bva}PU2484(Uh%32=>EcVO&?pGth06zLRT3Q(GpM>#{^l zt+yvb_r6m6GgL9{(sS0jx$dAtypA1C(Vff zet9kvBQ$$*$ELx&jy4xb1ukiuhtl0sD=}XeVoISYxgHzndVcj_HcK!V6^6~3h&XLoHxAEWtcvHdM#Gvk7-fp~G z*NZdSjTnknS+8ava0JRlev_EP4bEVi{`lr}^lAS(;v@N*; z=d)%6ttQo{#3TQ4##cO7g>kLc<$&Wa8Mt-{*SGB3PLSF8OESaG!F@*lQMZs+@lGHP zO*@UG_-Etcm0#P3RYNw~^KG&iRF$wDj^$G9wl#cfDkJWTM98e#Ig;FN4vxc;;<*3$mEaf&>aHKi=>4?UU>SjM_5LZx^cRspX? zE1bz4sc>+5u#LA_#|}ba=-xBOLL=|j^-gsDbD==hvK$3HvI28$8)qH*_C%c`1_~Fx^fD3**Xu zVD7ijpOC zY4QNVx()~=93UYiQfU6h3PHM3)@Q+RNvzcbZ#%9<48g&2AriM-wFdyJTg-4#xSIC+ zxr!FV{mFZTz`diir_cNY6CMZlX&sO!x#tDz?FYXLzQ+X@5}2)EZcgV-7kq2C@5Sf+ zDqMsQg$0WVQGg$ffc1bRDiAPejzMx98o1I?ln=0G6Qsj<#i;KF;A(OsD9>^Q!xM}z zwZ<_N3&B+iPL^QbtC;XhIEN&MD84O)!T4~{iySBm|BW7cQWqNU9WJ=VDMDoX)`YOz zL>`F!Lmg;;mrHiX6GhENy1)29P;++B^|g%qXZ_w(IqlTaBJ&6A#{~m4$+{sr@SU3 zw1e2qPn845rLtBrAdBOQCC+Za9s(5)mkqQ4g2N7bZ@6Pm+1egPv?_bF0ba%cS>uls z=pDSI&_9au(dNFt1#@0_)#9ZQ45+FEd|IK8I?iqBiM zwjJM7igg&2^DsSbM#(iUk2y)YrJ55>&z9ozI!=5x@1pfD`DQeDD)0QWD~_S+n~m~4 z%Zmwo?Zgr&JsBEDomopUtETdDd6}K(vA3s?mH8ktu!ME#5mVe*X3;xMlW&)0UxsO` zpK4!GAWJih{Xf2w1*LX)i!R++tSvLe{_U)%mXpHdA){qd^3P~QB8nRD2U z+|@Vq^TOs7Z(2_ZI%3jwEs>aO@iAi7E~F&{RLl-~n~Yo*me-g#g6#7{QJesTGooN> zCKj3irr@|IK!faFohX^edC9M|PeHNoi80K5AMN-sZcE-yX4oR8VLB}jqvGp}`Mka} z9XbM6nluq(ajm%f;HVp8U_AUyZrk^G61Z-MeY&r!1DbJc>*i^Pw35Sv<%aE}KPm{X zV#6bm9Vr}zpfGkEWefXu`ntdtpbaK$g%Y! zZO6l|vqjH&8b2uqs(>3gL(K)qfMNtJ zpF?>#XO_{4nv*}z1$!sPq6i@fCjfkvbV_3Q7x>{j8xcrNqgFs$NKFMF^B^fI%>f^U z!;Oi{FN>6MG+xcjUP^-ug(Hq#TZ<1G(Kl&*TUeb>gg4c}VR1%Xb)4MHS&fHY7pD?X zBw@t3NLRxsY7)vCbuNt%ud(HmSMBbroeDluLuHLX-`(SeP>ogcY&o!!8zz5^3gP+4 zK#1FLd3H>bsI-d3C0WL27%eQXZZA|DGG1H{4bqx1UwFaM4@sS!Kg7A$uH_ke%FGBv zT8{TVqUwOs4U*(m$#i+v80ONhbQ{s+_&m{&}np~kiI)!0CT%PzkkZ2E-UHE^nP6LmHJ0@epz(c1>S5%wI8|Yr#$v5e`j0T z($#uLK_h)4>N&gUn4Kll)`6r4-jO-sHzarrb{=YW`u-3xW?eJ|NgY!Z8njyF^j*;8 zxLOnqMxYI2L!A=slK-}SNDH3?P9i0dN~`atsZT$=&(nLHqn-099bnth-ft#o(5k-Z zkY29`Fx}uWb;f% zLpqt(;@761T^buTo9E8+==FvEg8BB2j+P(}7tH@?g7_hZC&eUfLBe{SZ7cP!`0`5YLw%yc%DMA=dM1gwNrJ4>A|9e)0htUM6`!q#SX-tnO_GwJOcTd(X(scnfK^Hz1p|jhw+vc zd#A=YZn95?4_h$5LKh9x!-xx^$Hg|FehV2_S0)`Q&u2pL-6X#U3Qh^21I_g)$Xuo2 z1vwU!?-#Wma`Oh~-8uK*``FgNp^mWBmxT&0Ay|)tiF3Z1Gx>NPj>m)|v`xOR>aLBG zQg8mfPN{u;Po?J676Oy4CeXcldQv9$<`HI?wN$WXab5F!^C?d*PUQ9zkCG?N>Gv;! zBvkN`P&UNKJ&`KxuXrReC`E;9d#?GDu z1LA?ehl%&oJ;HB?bd<*-Jf-E|8AR|1t$&SsMoqt%m};^*G&A2D7v_u+cr+RuWEl(i zD;x<7%qHA!@{x3A@aGLx!IE_eQVyzfmBWCTB0YGTw=>Pww;T<|jDam)ABu3{2$r6= z-c^!+1k)CGL-q1DqzagE9ktBW$%XHv+%QeMxpp?puV5%qza;!dauZ?|DywFu=7L23 zQM2t*s$rO?3yA=|rO3Uj*Uw;R-W+?J*tU~*#p4S^+R<|xkF{rUFil;Uq)}F8_Jggo z0IF>((+PD>SoPxid79lg>Ba%953k2zr*EzvlhwUy8!Ray^3MM*TBN|W!D$t%5jO?x z2Rewz{p3#iD!La&abXDyqvQGzQG08H^Zv+2)M@0XV4fo}QV8E>-yKW&i(gjPNJ6$+ zCcbccO~02Z8?$9k8ziaA0Hkrb&r`cns*caC?<4DhX-h~$;W0wHbbUXPA21? ztr>KgEnM{Ep1b{?8|y+?;Dad_3=1=S|KRoyZFbV<@!+mIwL!w{@tnn(FPNj?(-vkD zw&RMy>e9F5$x>{L-ywkIS3Leij_)sI`VQYl<7Lq#6Is*XM_A=+k&1>NQ2>iOT#!%Q zh6*`4=yJAsf`b0c@Gv#R!@~I6hZPJMWy;_4X`oJ4Re=vgb9%N~h2^28zc??p-bb~* zyXPYq+WD`rR^%~~%QE@mM^RR>Yx~&Lis*LJ67*a~d| zI_?jvxf4_J1MHsa2-OsekOJ6)FxKVLNI0nte+ZxAiu43ly$)PdnMZ9KO+v+-JAHNZ zHg+Sf#0^e}IXTk%Y8WF1^#;-%?=?Q7CMeCSI^~*+ILD=Kp^cfy=ppZd)5mFb1xli5 z)4wt13W89Kn5NUXh;PLx%5bA~Ug(^yLm?~J2>Eryo^U!J2u@ps8dMy3o;9NG`sn+E zP$DMwq~Kbrn*fMUBOLdnJlDl&Joi>ldsy}&Ndhz0hIX}YCofGfWP(c1!E?2 zj*K6YEaDK0d%8o-Pn5 zUdmiQA&^xko$+b(d8xt5!RgzF?PB9HO1h8kSU(2y=Yp57MGXi1eBG4yR7}V{#O3@Q zUb1y6aPe1k_+)K?`wh}O_Ps0Wfzdbl@Zknht=F#*k&6`^`?W@{0OdhcU}5uovl!?J z$I>N2W|I}18XKFfa)|c9_Zw4DOlT3;V3Uqlrp1EU2Hu99?8U(vN(w0elB!%gP^#|6 zHORI0z^K@Tt!1-QvXsMnrK%2Zgw&ptK|Q%ZfoXk#3HFFbZbA1kyg5PV0h6Zu_R+eA z;r@l$&rcV5TT9Oa3gVP)H*W=|x#w>Ql{Q36ZgCzT=@z1GwsYgH{KtLc>`uVVyqz+W zf#9~zr17Ul>N)Y{0|O++kZ9S@Wg3&fGnZ6uR~oHYE9IAO2S2ZqSu}rp4BgyBx;C}T zai#7V3=|R~OXnjMU|d`eyLxNX_#9$*i^*KMEC^{r)!6&@mos2^TXb06iiTcCu*7=O zyLpM9d?SB74{UBI+-k<(lo#Rv?>aXaBu?$9hVf^O+L{w@mit?GGxx;FDuC*XxY>LF z(y~807PUWcZB_NZ5tm62QUToVNJ)Fp@B~|pa$4^|`V}N2U|595rXF}|Y<-th=+!!h zK@owSjw>FLgl7{Ip4VVOuxZl^6O~tHn=iN+Sw!L1h*-h_cpV=L?}bV^`Q)aVC8U4k zo|__(`Rb`TE>7|oDBk{l-L_F}7HA`yr>4K78m88~LncI#$IK2F92#^;@c9W@p^yEf zVDHjt^m-bm-Z53BpDVFgNK<3@Fep>TNbJ!C9HGyz_>SRY0uJbze@#&{vQK^cJEvy4e{^+K*M0Yo$GV@j)^(c_nJ!`2@gim$nqbk=s!+OWo}Xv1X}0ii{jXIUjCWKK53REIbVwZtK`bs+wBaTfqqyTuJ@GDcoK9ruH>$Ae7=%rm#@PGa;cMeIzI;>&z*NV5x|X@G)7aEH z>9!lu_9%N;wa00KvLDA->9FOL4A`jSg{LVUtKuzara7~|Wyh2iTJ9rdN!}yZ%MYUS z>$aS5nd1P4np}FfHYWAh4ipd>#Km~$DBuK!fGX_ZklTEQiH*;#r?mF7J&KNhew~7P zFrq8xD%K1;`v@s1s9Hw*1P%49p)9duL@P>=yZ5WJ-DQT##w%QjJ1ynrl#V*bdc#oO zOZlueA$$o4N49sV{{km4DdbXxy!Ax%{X{DVdyRSL7?|x!dV8@!C9C*{#!M5R)j@evJ|NqXe%;Dx0Pp^jZ~l8%^ZGlbE;`I_ zvtA*4JJ=oZiu5^LWSnR!@sAFlVdU<$4nG|6W5Q>LWpS~|ZnA&kI~%B$jh(@_fB;y1(JSW0M96+Y8l$^^BG=sT!O-BAhAizFez zU16s#hl&pn)a2jBt40lS0m>I7?dKj6ov-kSX*os)k4TfO&|ZYi6KShEZXk47S8wKW z3!ThO)3B;d8B-}xdXIw(VfgIi{bx-l?}%E*;WLD$tv9ll--~U~s~#I--Z+;N=viF{ z@!1`Gq4JjjK=t}?cbtg4TmQX8>eu^syd3zbCLnwMJ8sC?D>&{AOYA1^EI zOb;?XZeUy|UKkp|i0~^S4x|J{vhHVwtXR~H-K&3dgvzGY)YIcrOk35 z^mfcnHH8oISDk|_cW&MHW?*lov6{8f5JrbN*haiVdiQ=f)NiPw{3L>7&Ubt_y>GrZ zi95>@Hr_i|n^OLDIhY~P11p(<@h4VtT=cgFjbi4p@AlU-*k^Di*-Ed2Z}d;e@JQuB zr-y#lGqp<$>fDy}BPZt}X-)}Aui$uzvbO}Oycn5n=`d~qX=1i!8TSoLs|zH)%m%4b zf){?7lG#toamA}=b$wdxaIqN?{?loc#XOk~Mjdo>K98qFgNqO&vz|4=G&4*Ewd;j9 zsbZmzOf~A5Yfs1!ShISp8RCdRrbRXoJfF&l97e)PKiA`uk68ceK0lvhIK8&%$9h}% zoN!`%lcZGwKd0c5~BgBP1fDc8WOC$kp3fM+2ljFMEJDSCoPhK3ikXF;`} zL?0EZed8))ctD{FC8VagNg&6UO@ztv)`_2tKAP&^=jzW#Z zIr&n!gISr$8zESXO6qkw;lJs-snt(B1&|cwQK-nrH1-06)y{Weee6w_*`r|)mlEcg z!+Z0Oa9Yk8`?r^Ly{G5VGul3Pz+1kgLGQF3UH)7HP*_tWvZ(NDDe& zDUf1$U;X%Nk)Yi2$Iq`$b$a2K)?)^NqzuH|r&RUz$Al(3Bffkvz%B-rH#%(7j{M6r zIzQcU+6bF+&T4aqB6D?aT$2lp2WIWsY-c4aG6T1D*%~b$&-X8_xFj1DM3)ay?|3X} zrLvAbkT?fu3CiPTY=^ai^n9&5XE!+AYc_pBu>t(gc=>tf_7323)$4q;wqh0oxYC7s z^s7{xK%V3ic$1%C4w#{Ww@?@w6_#h*Oot}5CAzm~LE=(}zIq~wr8sSmLXbTB^w~$J z0Fs{!OCf zbdD}?dV8~OYF@f*uup8pHsJ~didMC*D<-C1O+Q&F(LHrB7bo_5(@Y>9bVLy>Eo^lm z=$jXUm(U&F`cr<9d;8PxEWY0_3 zeBo1cRlJF9`}wE)&ZGf3e62Tko(o7I>0=3ovvch=V-qo6Cl@;~#O4%|ohdg_cX4>}JUNzULZQ}a z?1BBlC{_|zdQRBb#ByUeULtcc2A-)Kh3J zU+ohSt|n~v_Im%y6L2dqK)h#Gt{?wn{+5yyk^JD&7gwcXOl#YXn9_UiX?@+#o(2-H zC@VwN2fj!hDxo7hk*+>;A(`uRFk!7?N4Yvjz(v?Rdq=WmU@mlHJthnpz_znEHb?KT zNX26yia_`S)E5bdG!DsYvKaIfhezOE{#bUayN?KL>SA{^Ksx{NioWNkaGoAo6w1)jS4w_M9=#JK8R6jG!Wfy3scM!*pxrSt%Pq(0^@9jrBdOyA6a=J;9sSk;|eY7#Uh= z>-s?5-Wwg4%jw#b*YoDAzCGGlLT^B808qYw5#RPhY2EY~5f5PS0@uwf`|RBJDkN@eIG8=gaFT7s!b;N z40rq}>vklO?Fj4HaOVqU@_xfrY>kPHUfrMd^1FmrHR~dgda)LZ-u5Wh8@P%@JntHL zl4QG3RjCW~)oV1$nwNy(b?T^F7mx&?NhcrGmTy~gRdlE-C=KnKK%dL_O;K1>UL9kY z#d|(qdopO`(BpP|@|Ij9=uAQGMdFgF*oK8!tleUCx_td|4Ap6^xL-|Y`3Sab*RhL3 z5HTWmeV^T6?)#0uGD@LZ=Eh^jdDh9jn3#FMYqeg0fcPy)*Nxnxk12(!dMX7J@@sTG%9+?=@-z_ttvv;Eo?Hs4_Z&YN6*!(Ts9ZLxpHk)(+dh_Vhq*k_)+)nor0fd zmLK$=TQI9Eq@P#n35j!7e(7rCl z!_KMV6ZOt}MF3TV#+`%n>SQL5wApC|4s#ZGUaE;3Ftol$@sqluFK@TcXN zwe{54!yt)B;Jm#4p>~2l0@dI%lw4i!2s63_Kagn>O*n+Ga;-oTu{XdJu25!KZ{F0z z;n!S3l9vbR%_XnP^*o_Y}Wdi;(p zNx*#As#dMkL&(kV0!!bG&<|2-hP+>OdQ$7Uyv7!v%C~^#=sh1Qd>vg#4d50Zx{70* zrxKdl`q6j?P55hht}IeU;r&rXjCu7leJBKHwH{gq_w045nrofkEATf7QJ}(NI9EK~ zh8lkmof?h-ir&QZQ}EYtxHlb=4X=hCj;nMVmtFa189tb-FD2@`0A%GsH_p#w=ZYvc z180XD3KC4^_;90m5LD76t+N=%hBoWBG&M{LM=g1thW6}KNyuSwT>i^z{|5LKQg2NKT{o#uEVts(iw{*ABlBiZz*XYu4iG+fa zPq=}@*xF*1>`q{n<$f_*lMp&i!$IUq?i@H#w%Oc08C*cuqANsh)I$_DU60n3cbSA- ztW%Xt0Ww?}K$K3P0{*d9FaXdm&pz!wS6;efubaUHAife6G`@LNGkyHO6wy)xU*8Yt zT%4acqkZ>RN+vL15~VAT3)r`bHQ&B&{Ybo*+kQAhE;3ADsU74?1E*nw z3gV=X7K(>zoe`*p)D*>@;XH9~X=&=leR6ogXT6nNrvWmLnYewXclaY3WwDnQDb@%- zH+zA@p7?gZRb*LV{9R9-yXban0%(~Ai8|F44vnYoTW}6%aFbwdx4~;GWRAdW+d-jmv&jLnF5A)Zs_Y(MZIa~i zbKx5mV{G1OMr{i`6a-@9Wa?v9VhVwGUBh6nBv00eJXO#Ij;)ogNC7s93_}oJGyz*B zoUh@uez~);=%$jv!TM4tLH5~MQbU6=Fzf5*#9Z6V(H^@2k#SB=GWA`~{qbg4(v@uG zs8e8#1K;QCTNmpmd>hSL*L;wcmlrhCNwwi@B0S31g$44VBf6F>dV%(5ghZ9uhbvh2(!O0$8ehR zHgi#)4kQVGVX(lji^>Ci&*e{)H&kw7L++)KS^!a{{Kz`t-ZV=*vtn}`X|vr6$gvvz zIhyI_N5g@V*=*sP1CWmFetqVQ%!M#?!(u^NeWFEAIn~b0wkR1sbs1bbv0BU2Xwzn1 zjll0Sjkon&TZ68mnB$qQSjcBdKs?Oql%$Gv`|8pJ>qrHF-g2rNb&-X|b)t_sK}gVtp!q}KYdS9x~yFD)MKLV5Rp#Io$JnlC=&%k^y;^64xlhY4Z%Q` z`}9)&2=0^XO`_rcM~nixyJXdo0*Oip%9O;pu2B96ITG1BS6ZRD}bbm z6uqvR2(&@7cj#q3&J)RV1!&UH{J7@)EVNC*^) zFtbL`H_5@xIB1Vy$4=~?w3i+uCtGa5&w4y;#xy$js6m7Cwi-Hz3XNrydQtUV(wkJ)yoZ$IJ;z+;TtKikvfBdM zn~>upmsXB4r(sLxqfk4&zIwD8h`cLe!Vp6WQ=v6GK}nxxbLVAdmz|H zzY}w06pZeJ*pLSYf-j8FLmLV!G>IRl{--gJP z-qKc~ZgM(uk8WC)qi>iM=^GA(jf5gCISWr4ZkK*Cq(bjd`%;}^S-}&Qc+7wgx=lZUU>t+U;aIc^F1w$Y1V*=zuxTfu{XGF%`Yh!Y z(n_HLE` z_Y$M6yVl#c+O;fePl@ZPAv;v0p}8TkQ@aS%V2jT|<0|~mqj0@0rrPwe&-`Gf3Okm| z*4n!GU*eCBv5*QIm$>n>o;@ZS^A~@UF_V!uC8!J#3QC#M?tB-| z-YokeG5$4cqC^G?b^9d~QNHbMj6>isx)($Iao;}2izuQ>Q$S~B0XE$0+gjn??7GEu z*%0;CIGw}vPR#|4xBWLwZeq;T<1*~VV3!?34$TF9PVWgKY(Z%pOui&pF#2fDz({FE zee3Y^TIS)~7~klTo|UAZwW%HilD;}K2?fZ7Z6`-H9{bWVNp_wOu{LC89yJG3Kr^HB zwy^POd~WmSG6lBJcp802(S6(05Lc6?zt24{Ev{f+fl>w-73VZ7n=OG{xR-9(p3`TGtj!RBa8kg2H|^*>T(~#KiUM4ZNHhccy+B z#XU*eGZz96OS1m;oiw7GkKg_JJS;Tom-x`{{qTcAtk3(sGe+Z!0{BVb^vf;GO==oq z?_OYvMG#{H7-Keeqf9zf^1rJ z@ECi52b*nZr2gD}#;(tv6q_DKdatLgS!v!!U5(eb?OWbEj)?V!#gFv6D-+>VR|92P z>-7pAiQZc)fF($>t&_>u^=mZx*;tPpS><)xIwBN zyqfTnZOYz8Rec=@(PRkPVdA9jxBA< zYR$^O=mnQ|Rd>5mZ+n$`t*0Ga+fnh7Oe6OT3Mw6~Ej5-UO-B>Uf9v@%q%4qjH`u(z z{9@gsBt~_a(iYj1BTpexzis=$mKIBnGRgv};^7M(ny$QB&A4hfx|KA#csmut2p<%Q zTLVtcr~`HY34O1kjDK8ZpBwjlYR&M59<(Q@k;^*v#85NXq;{EC8dkDad>r2Zdj25p zgP&D3{!-?GR)+49gs)h-m~892+v5DSd$u&}rTgPo3_YB`!15g`o!B`DYo(h%>-?oW zr~qflN#r+EDA(=ZE&fQB9+KuZ#EQlFB=FAxVu6xbX2J34=v6BWr6yco#F=gn^6b)c zvoT3z0#x968z$`YcJ6pf32mf@>+vx0z1(Dlt+erXEq-pt5$7Iz25IsNs4Zy_RQKP# zd#Lik(QH0*E#F~x3JEuyNJS!hJ`a?f;5WxUs-^P49ZTz38J&+@=1qR#I`BeX#f#m; z;7>Y}D>_XK@x9?6OR7lv1U{BhBRNZqH9aawkf97Wo(#phcUAXE<#_-k%7U#2`Z)Hi zlW;zd0Ua=w%5H%tTRFHJ<4>fYsP)6A2qXKPUvHjI)VkNi_@vYOK58X%%50Yi+Ic?1 z9t9)rA@>y1c&*Vo4h--rZ8ENi3*O(DcjP#%wrd&4`7m6%<=9$AieV(Ei)^1PhzpB+ zqmwc0ey(X0v~rx`vEO(CcOB1B5U})@7}vr2OB-P-p!I|i#*G?@?bUORM+a7oyjt-(jYwBW55v&+S5LwT zks}e=d(F$Zfa>dM{#kWV^+lVEFN5&Rd=YmJT1`-yayB49LRLWq68RE-h} zyI##HO^P*7ul&lhCYS4bg;z0nbQ*S0dpASQScZf?|#H3z*|hB)26>0j7Pq45zk$O;xVN zfG*fW6ZT^c*5LPL9HStF0QTfYZihV1 zcOTkxB$R7EAu5l2pU8}Pnhc>WuP-w|56G@^p=96NaL9w&qYB~k^xYUpW49BBUR5gf z!DXM2W6TX(gaYjZ2B_QFUt<{bj-L9WT2-ze|QZuEi@w zlrQpuPgF5{hAcr$?|jhwiu6K845uOlD-A?WSOkqYsG!RO-{mEF`s>E*2y*?$mLto% zBaItAXcFD#)fb$fMSj!4Y`E*WB&2XuC}$fMW-Ll(JY0tO(2Q4RFvT_I>5dSPH=m5p z=IimEW*{Wlg--L62a2-ZV?T)bw(GFl!{uJQVzjQe)Qj}uXMiHoN`d|YJouDQxd;p_67CN#SWul6CiOFd`r9KDYq3S zEkg8HO<2yQg&|eNu!mKi+JevABMTi&d>wxxj*K%H`X>)B9m4CS`y0D z@||tk`!TaFJ|jy-CXrG=h>z9@-$>wd+hQ~`f?WJqf-J#EX-xQ4>Xv$+J)(!h_@wng zu`~v%sUoFvQ9+gHkkKzxh~Q4+{4XBTHqF^d(ooBO5Zo+vNbQ(LIP;5S8*@csSa2=k z^aHdi-202Ap^Vx+^ z+WbBrZ|!?bz7yV93cp*{*s$U#{1*8p^f{+@uZ7=`9<^aj4e|EOD|mf4qSyj1r*C$uy^W*OA@}N{Q-cwU`sB& z@GQk5Y8j(EXKW3Z+OV@|NlUPSW0_Fo1lL)2wcv$djd;?+x2t)Z&8(|C?fC;8rd~mt z?aw!2X2$b`J+7u@^;swW)&xNrXAJ660}Y|Jv8CyKp-fVse*nM_N!M!KA;!WQ-pA7y2vOne9>yCYuNvc|r?TMa)^ zwVLt)IPW#z^Ql@Jg;jYH6ODGU{V@`D_xFUykYyNXKR!t&*=E`NLZOndFm|yM@(YB1 zM)+S%5n~E{mWHct)2aWDL_E7M*u9w_d3eMBL2UsA!vO!kl%hXLJg^`*A5Z-6qrv~L z>pwG#AVEO?VihUM{9W!(zz*Wyy9vJWzc=|;6WBkP{Ml3g-Ww}TLH^B<^nc_aq5kSN@E_+II0#6-* + + + net8.0;net9.0 + 1701;1702;8616;1591;8618;8619;8629;8602;8603;8604;8625;8765 + Admin.NET.Plugin.Pay.Alipay.xml + enable + + enable + + + + + + + + + + + + + + + + PreserveNewest + + + + + + + runtime + + + + + + + Always + + + Always + + + Always + + + + + + + Always + + + Always + + + + + + + + + + diff --git a/Admin.NET/Plugins/PluginCore/Test/Admin.NET.Plugin.Pay.Alipay/Admin.NET.Plugin.Pay.Alipay.xml b/Admin.NET/Plugins/PluginCore/Test/Admin.NET.Plugin.Pay.Alipay/Admin.NET.Plugin.Pay.Alipay.xml new file mode 100644 index 00000000..79a4de2e --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/Test/Admin.NET.Plugin.Pay.Alipay/Admin.NET.Plugin.Pay.Alipay.xml @@ -0,0 +1,34 @@ + + + + Admin.NET.Plugin.Pay.Alipay + + + + + 在 Build 时, 将会 new Middleware(), 最终将所有 Middleware 包装为一个 + + + + + + + + + 测试,是否运行时添加的Middleware,是否可以依赖注入 + + + + + 系统动态插件服务 + + + + + 获取动态插件列表 + + + + + + diff --git a/Admin.NET/Plugins/PluginCore/Test/Admin.NET.Plugin.Pay.Alipay/AliPayPlugin.cs b/Admin.NET/Plugins/PluginCore/Test/Admin.NET.Plugin.Pay.Alipay/AliPayPlugin.cs new file mode 100644 index 00000000..812c18a9 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/Test/Admin.NET.Plugin.Pay.Alipay/AliPayPlugin.cs @@ -0,0 +1,56 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + + +using Admin.NET.Plugin.Core.Abstractions.Pay; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using PluginCore.IPlugins; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Admin.NET.Plugin.Pay.Alipay +{ + public class AliPayPlugin : BasePlugin, IStartupPlugin, IPayPlugin + { + + public void ConfigureServices(IServiceCollection services) + { + + } + + public void Configure(IApplicationBuilder app) + { + + } + + public int ConfigureOrder + { + get + { + return 2; + } + } + + + public int ConfigureServicesOrder + { + get + { + return 2; + } + } + public string Name { get; set; } = "alipay"; + public string Say() + { + return "支付宝插件"; + } + + } +} diff --git a/Admin.NET/Plugins/PluginCore/Test/Admin.NET.Plugin.Pay.Alipay/Controllers/AliWebApiController.cs b/Admin.NET/Plugins/PluginCore/Test/Admin.NET.Plugin.Pay.Alipay/Controllers/AliWebApiController.cs new file mode 100644 index 00000000..0972d21c --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/Test/Admin.NET.Plugin.Pay.Alipay/Controllers/AliWebApiController.cs @@ -0,0 +1,29 @@ +// Admin.NET ĿİȨ̱ꡢרȨӦɷıʹñĿӦطɷ֤Ҫ +// +// ĿҪѭ MIT ֤ Apache ֤汾 2.0зַʹá֤λԴĿ¼е LICENSE-MIT LICENSE-APACHE ļ +// +// ñĿΣҰȫַ˺ϷȨȷɷֹĻκλڱĿοһзɾ׺ΣDzеκΣ + + + +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System.ComponentModel; + +namespace Admin.NET.Plugin.Pay.Alipay.Controllers +{ + + [Route("api/plugins/[controller]")] + [AllowAnonymous] + public class AliWebApiController : ControllerBase + { + [DisplayName("֧Ϣ")] + [HttpGet] + public IActionResult Get() + { + string str = $"֧Ϣ Hello PluginCore !"; + return Content(str); + } + } + +} diff --git a/Admin.NET/Plugins/PluginCore/Test/Admin.NET.Plugin.Pay.Alipay/Controllers/HomeController.cs b/Admin.NET/Plugins/PluginCore/Test/Admin.NET.Plugin.Pay.Alipay/Controllers/HomeController.cs new file mode 100644 index 00000000..d8b1d1be --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/Test/Admin.NET.Plugin.Pay.Alipay/Controllers/HomeController.cs @@ -0,0 +1,35 @@ + + +using Microsoft.AspNetCore.Mvc; +using PluginCore; +using System.ComponentModel; +using System.Threading.Tasks; + +namespace Admin.NET.Plugin.Pay.Yfzf.Controllers +{ + /// + /// 其实也可以不写这个, 直接访问 Plugins/HelloWorldPlugin/index.html + /// + /// 下面的方法, 是去掉 index.html + /// + /// 若 wwwroot 下有其它需要访问的文件, 如何 css, js, 而你又不想每次新增 action 指定返回, 则 Route 必须 Plugins/{PluginId}, + /// 这样访问 Plugins/HelloWorldPlugin/css/main.css 就会访问到你插件下的 wwwroot/css/main.css + /// + [Route("Plugins/HelloWorldPlugin")] + public class HomeController : Controller + { + + /// + /// 新大陆信息 + /// + /// + [ApiDescriptionSettings(Name = "Index"), HttpGet] + [DisplayName("新大陆信息")] + public async Task Index() + { + string indexFilePath = System.IO.Path.Combine(PluginPathProvider.PluginWwwRootDir("Admin.NET.Plugin.Pay.Yfzf"), "index.html"); + + return PhysicalFile(indexFilePath, "text/html"); + } + } +} diff --git a/Admin.NET/Plugins/PluginCore/Test/Admin.NET.Plugin.Pay.Alipay/GlobalUsing.cs b/Admin.NET/Plugins/PluginCore/Test/Admin.NET.Plugin.Pay.Alipay/GlobalUsing.cs new file mode 100644 index 00000000..6a0f7f4b --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/Test/Admin.NET.Plugin.Pay.Alipay/GlobalUsing.cs @@ -0,0 +1,2 @@ +global using Furion; +global using Microsoft.Extensions.DependencyInjection; diff --git a/Admin.NET/Plugins/PluginCore/Test/Admin.NET.Plugin.Pay.Alipay/HelloWorldPlugin.cs b/Admin.NET/Plugins/PluginCore/Test/Admin.NET.Plugin.Pay.Alipay/HelloWorldPlugin.cs new file mode 100644 index 00000000..c201d754 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/Test/Admin.NET.Plugin.Pay.Alipay/HelloWorldPlugin.cs @@ -0,0 +1,73 @@ + +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! +using Admin.NET.Plugin.Pay.Alipay.Middlewares; +using Microsoft.AspNetCore.Builder; +using PluginCore.IPlugins; + +namespace Admin.NET.Plugin.Pay.Alipay +{ + public class HelloWorldPlugin : BasePlugin, IStartupXPlugin, IWidgetPlugin + { + public override (bool IsSuccess, string Message) AfterEnable() + { + Console.WriteLine($"{nameof(HelloWorldPlugin)}: {nameof(AfterEnable)}"); + return base.AfterEnable(); + } + + public override (bool IsSuccess, string Message) BeforeDisable() + { + Console.WriteLine($"{nameof(HelloWorldPlugin)}: {nameof(BeforeDisable)}"); + return base.BeforeDisable(); + } + + public void ConfigureServices(IServiceCollection services) + { + + } + + public void Configure(IApplicationBuilder app) + { + app.UseMiddleware(); + } + + public int ConfigureOrder + { + get + { + return 2; + } + } + + + public int ConfigureServicesOrder + { + get + { + return 2; + } + } + + public async Task Widget(string widgetKey, params string[] extraPars) + { + string rtnStr = null; + if (widgetKey == "PluginCore.Admin.Footer") + { + if (extraPars != null) + { + Console.WriteLine(string.Join(",", extraPars)); + } + rtnStr = @"
+

HelloWorldPlugin 注入

+
HelloWorldPlugin 挂件
+
"; + + } + + return await Task.FromResult(rtnStr); + } + } +} diff --git a/Admin.NET/Plugins/PluginCore/Test/Admin.NET.Plugin.Pay.Alipay/Middlewares/AlipaySayHelloMiddleware.cs b/Admin.NET/Plugins/PluginCore/Test/Admin.NET.Plugin.Pay.Alipay/Middlewares/AlipaySayHelloMiddleware.cs new file mode 100644 index 00000000..bf0d4f6c --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/Test/Admin.NET.Plugin.Pay.Alipay/Middlewares/AlipaySayHelloMiddleware.cs @@ -0,0 +1,63 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + + + +using Admin.NET.Plugin.Core.Abstractions.Pay; +using Microsoft.AspNetCore.Http; +using PluginCore.Interfaces; +using PluginCore.IPlugins; +using System.Text; + +namespace Admin.NET.Plugin.Pay.Alipay.Middlewares +{ + public class AlipaySayHelloMiddleware + { + private readonly RequestDelegate _next; + + /// + /// 在 Build 时, 将会 new Middleware(), 最终将所有 Middleware 包装为一个 + /// + /// + public AlipaySayHelloMiddleware(RequestDelegate next) + { + _next = next; + } + + + /// + /// + /// + /// + /// 测试,是否运行时添加的Middleware,是否可以依赖注入 + /// + public async Task InvokeAsync(HttpContext httpContext, IPluginFinder pluginFinder) + { + // 测试: 成功 + List plugins = pluginFinder.EnablePlugins()?.ToList(); + // 所有实现了 ITestPlugin 的已启用插件 + var payPlugins = pluginFinder.EnablePlugins().ToList(); + bool isMatch = false; + + isMatch = httpContext.Request.Path.Value.StartsWith("/SayHello"); + + if (isMatch) + { + string sayhello = payPlugins[0].Say(); + await httpContext.Response.WriteAsync($"Hello World! {DateTime.Now:yyyy-MM-dd HH:mm:ss}
" + + $"Say Hello! {sayhello}
" + + $"{httpContext.Request.Path}
" + + $"{httpContext.Request.QueryString.Value}", Encoding.UTF8); + } + else + { + // Call the next delegate/middleware in the pipeline + await _next(httpContext); + } + } + + } +} diff --git a/Admin.NET/Plugins/PluginCore/Test/Admin.NET.Plugin.Pay.Alipay/README.md b/Admin.NET/Plugins/PluginCore/Test/Admin.NET.Plugin.Pay.Alipay/README.md new file mode 100644 index 00000000..5f9d8314 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/Test/Admin.NET.Plugin.Pay.Alipay/README.md @@ -0,0 +1,14 @@ +## 说明文档(可选) + +- [] 这是一个示例插件 +- [x] 感谢使用 + +## API + +- [/SayHello](/SayHello) + - 通过插件在运行时添加 管道Middleware, 并拦截响应 + +- [/api/plugins/AliWebApi](/api/plugins/AliWebApi) + - 通过插件Controller 响应 + + \ No newline at end of file diff --git a/Admin.NET/Plugins/PluginCore/Test/Admin.NET.Plugin.Pay.Alipay/Services/AlipayPluginCoreService.cs b/Admin.NET/Plugins/PluginCore/Test/Admin.NET.Plugin.Pay.Alipay/Services/AlipayPluginCoreService.cs new file mode 100644 index 00000000..2b182674 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/Test/Admin.NET.Plugin.Pay.Alipay/Services/AlipayPluginCoreService.cs @@ -0,0 +1,55 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + + +using Furion.DependencyInjection; +using Furion.DynamicApiController; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using PluginCore.AspNetCore.Interfaces; +using PluginCore.Interfaces; +using System.ComponentModel; + +namespace Admin.NET.Plugin.Pay.Alipay.Service; + +/// +/// 系统动态插件服务 +/// +[ApiDescriptionSettings("自定义插件", Name = "Alipay", Order = 100, Description = "支付宝支付插件")] +[AllowAnonymous] +public class AlipayPluginCoreService : IDynamicApiController, IScoped +{ + #region Fields + private readonly IPluginManager _pluginManager; + private readonly IPluginFinder _pluginFinder; + private readonly IPluginApplicationBuilderManager _pluginApplicationBuilderManager; + #endregion + private readonly IDynamicApiRuntimeChangeProvider _provider; + + + public AlipayPluginCoreService(IPluginManager pluginManager, IPluginFinder pluginFinder, IPluginApplicationBuilderManager pluginApplicationBuilderManager, IDynamicApiRuntimeChangeProvider provider) + { + _pluginManager = pluginManager; + _pluginFinder = pluginFinder; + _pluginApplicationBuilderManager = pluginApplicationBuilderManager; + _provider = provider; + } + + /// + /// 获取动态插件列表 + /// + /// + /// + [DisplayName("获取动态插件列表")] + [HttpGet] + [ApiDescriptionSettings(Name = "Page")] + public Task Page() + { + return Task.FromResult("測試插件"); + } + + +} \ No newline at end of file diff --git a/Admin.NET/Plugins/PluginCore/Test/Admin.NET.Plugin.Pay.Alipay/info.json b/Admin.NET/Plugins/PluginCore/Test/Admin.NET.Plugin.Pay.Alipay/info.json new file mode 100644 index 00000000..ec71bb1f --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/Test/Admin.NET.Plugin.Pay.Alipay/info.json @@ -0,0 +1,8 @@ +{ + "PluginId": "Admin.NET.Plugin.Pay.Alipay", + "DisplayName": "Alipay示例", + "Description": "这是支付宝插件", + "Author": "tomny", + "Version": "0.1.0", + "SupportedVersions": [ "0.0.1" ] +} \ No newline at end of file diff --git a/Admin.NET/Plugins/PluginCore/Test/Admin.NET.Plugin.Pay.Alipay/settings.json b/Admin.NET/Plugins/PluginCore/Test/Admin.NET.Plugin.Pay.Alipay/settings.json new file mode 100644 index 00000000..9a32e4c3 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/Test/Admin.NET.Plugin.Pay.Alipay/settings.json @@ -0,0 +1,3 @@ +{ + "Hello": "哈哈哈哈哈或或或或或或" +} \ No newline at end of file diff --git a/Admin.NET/Plugins/PluginCore/Test/Admin.NET.Plugin.Pay.Alipay/wwwroot/css/main.css b/Admin.NET/Plugins/PluginCore/Test/Admin.NET.Plugin.Pay.Alipay/wwwroot/css/main.css new file mode 100644 index 00000000..191068ea --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/Test/Admin.NET.Plugin.Pay.Alipay/wwwroot/css/main.css @@ -0,0 +1,9 @@ +* { + margin: 0; + padding: 0; +} + +#app { + width: 100%; + color: deepskyblue; +} \ No newline at end of file diff --git a/Admin.NET/Plugins/PluginCore/Test/Admin.NET.Plugin.Pay.Alipay/wwwroot/index.html b/Admin.NET/Plugins/PluginCore/Test/Admin.NET.Plugin.Pay.Alipay/wwwroot/index.html new file mode 100644 index 00000000..9bb783d2 --- /dev/null +++ b/Admin.NET/Plugins/PluginCore/Test/Admin.NET.Plugin.Pay.Alipay/wwwroot/index.html @@ -0,0 +1,17 @@ + + + + + + HelloWorldPlugin + + + + +
+

HelloWorldPlugin!

+

插件的前端文件应当放在 wwwroot 文件夹下

+
+ + + \ No newline at end of file diff --git a/Web/src/api-plugins/plugincore/api.ts b/Web/src/api-plugins/plugincore/api.ts new file mode 100644 index 00000000..5e2deded --- /dev/null +++ b/Web/src/api-plugins/plugincore/api.ts @@ -0,0 +1,15 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * PaddleOCR 图像识别 + *
👮不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + * + * OpenAPI spec version: 1.0.0 + * + * + * NOTE: This class is auto generated by the swagger code generator program. + * https://github.com/swagger-api/swagger-codegen.git + * Do not edit the class manually. + */ +export * from './apis/sys-plugin-core-api'; + diff --git a/Web/src/api-plugins/plugincore/apis/sys-plugin-core-api.ts b/Web/src/api-plugins/plugincore/apis/sys-plugin-core-api.ts new file mode 100644 index 00000000..9685c372 --- /dev/null +++ b/Web/src/api-plugins/plugincore/apis/sys-plugin-core-api.ts @@ -0,0 +1,823 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Admin.NET + * 让 .NET 开发更简单、更通用、更流行。前后端分离架构(.NET6/Vue3),开箱即用紧随前沿技术。
https://gitee.com/zuohuaijun/Admin.NET + * + * OpenAPI spec version: 1.0.0 + * Contact: 515096995@qq.com + * + * NOTE: This class is auto generated by the swagger code generator program. + * https://github.com/swagger-api/swagger-codegen.git + * Do not edit the class manually. + */ +import globalAxios, { AxiosResponse, AxiosInstance, AxiosRequestConfig } from 'axios'; +import { Configuration } from '../configuration'; +// Some imports not used depending on template conditions +// @ts-ignore +import { BASE_PATH, COLLECTION_FORMATS, RequestArgs, BaseAPI, RequiredError } from '../base'; +import { AdminResultSysFile } from '../../../api-services/models'; +import { AdminResultSqlSugarPagedListSysPluginCore } from '../models'; +import { AdminResultString } from '../../../api-services/models'; +import { AdminResultSysPluginCoreReadme } from '../models'; +import { AdminResultSysPluginCore } from '../models'; +import { DeletePluginCoreInput } from '../models'; +import { PagePluginCoreInput } from '../models'; +import { UpdatePluginCoreSettingInput } from '../models'; + +/** + * SysPluginApi - axios parameter creator + * @export + */ +export const SysPluginCoreApiAxiosParamCreator = function (configuration?: Configuration) { + return { + + /** + * + * @summary 查看插件信息 + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + apiSysPluginCoreBaseInfoGet: async (id:number,options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'userId' is not null or undefined + if (id === null || id === undefined) { + throw new RequiredError('id','Required parameter id was null or undefined when calling apiSysPluginCoreBaseInfoGet.'); + } + const localVarPath = `/api/sysPluginCore/details/{pid}` + .replace(`{${"pid"}}`, encodeURIComponent(String(id))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, 'https://example.com'); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + const localVarRequestOptions :AxiosRequestConfig = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication Bearer required + // http bearer authentication required + if (configuration && configuration.accessToken) { + const accessToken = typeof configuration.accessToken === 'function' + ? await configuration.accessToken() + : await configuration.accessToken; + localVarHeaderParameter["Authorization"] = "Bearer " + accessToken; + } + + const query = new URLSearchParams(localVarUrlObj.search); + for (const key in localVarQueryParameter) { + query.set(key, localVarQueryParameter[key]); + } + for (const key in options.params) { + query.set(key, options.params[key]); + } + localVarUrlObj.search = (new URLSearchParams(query)).toString(); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: localVarUrlObj.pathname + localVarUrlObj.search + localVarUrlObj.hash, + options: localVarRequestOptions, + }; + }, + /** + * + * @summary 查看插件文档 + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + apiSysPluginCoreReadmeGet: async (id:number,options: AxiosRequestConfig = {}): Promise => { + if (id === null || id === undefined) { + throw new RequiredError('id','Required parameter id was null or undefined when calling apiSysPluginCoreBaseInfoGet.'); + } + const localVarPath = `/api/sysPluginCore/Readme/{pid}` + .replace(`{${"pid"}}`, encodeURIComponent(String(id))); + + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, 'https://example.com'); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + const localVarRequestOptions :AxiosRequestConfig = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication Bearer required + // http bearer authentication required + if (configuration && configuration.accessToken) { + const accessToken = typeof configuration.accessToken === 'function' + ? await configuration.accessToken() + : await configuration.accessToken; + localVarHeaderParameter["Authorization"] = "Bearer " + accessToken; + } + + const query = new URLSearchParams(localVarUrlObj.search); + for (const key in localVarQueryParameter) { + query.set(key, localVarQueryParameter[key]); + } + for (const key in options.params) { + query.set(key, options.params[key]); + } + localVarUrlObj.search = (new URLSearchParams(query)).toString(); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: localVarUrlObj.pathname + localVarUrlObj.search + localVarUrlObj.hash, + options: localVarRequestOptions, + }; + }, + /** + * + * @summary 查看插件设置 + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + apiSysPluginCoreSettingGet: async (id:number,options: AxiosRequestConfig = {}): Promise => { + if (id === null || id === undefined) { + throw new RequiredError('id','Required parameter id was null or undefined when calling apiSysPluginCoreSettingGet.'); + } + const localVarPath = `/api/sysPluginCore/Settings/{pid}` + .replace(`{${"pid"}}`, encodeURIComponent(String(id))); + + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, 'https://example.com'); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + const localVarRequestOptions :AxiosRequestConfig = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication Bearer required + // http bearer authentication required + if (configuration && configuration.accessToken) { + const accessToken = typeof configuration.accessToken === 'function' + ? await configuration.accessToken() + : await configuration.accessToken; + localVarHeaderParameter["Authorization"] = "Bearer " + accessToken; + } + + const query = new URLSearchParams(localVarUrlObj.search); + for (const key in localVarQueryParameter) { + query.set(key, localVarQueryParameter[key]); + } + for (const key in options.params) { + query.set(key, options.params[key]); + } + localVarUrlObj.search = (new URLSearchParams(query)).toString(); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: localVarUrlObj.pathname + localVarUrlObj.search + localVarUrlObj.hash, + options: localVarRequestOptions, + }; + }, + + /** + * + * @summary 更新动态插件设置 + * @param {UpdatePluginInput} [body] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + apiSysPluginCoreSettingUpdatePost: async (body?: UpdatePluginCoreSettingInput, options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/api/sysPluginCore/settings`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, 'https://example.com'); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + const localVarRequestOptions :AxiosRequestConfig = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication Bearer required + // http bearer authentication required + if (configuration && configuration.accessToken) { + const accessToken = typeof configuration.accessToken === 'function' + ? await configuration.accessToken() + : await configuration.accessToken; + localVarHeaderParameter["Authorization"] = "Bearer " + accessToken; + } + + localVarHeaderParameter['Content-Type'] = 'application/json-patch+json'; + + const query = new URLSearchParams(localVarUrlObj.search); + for (const key in localVarQueryParameter) { + query.set(key, localVarQueryParameter[key]); + } + for (const key in options.params) { + query.set(key, options.params[key]); + } + localVarUrlObj.search = (new URLSearchParams(query)).toString(); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + const needsSerialization = (typeof body !== "string") || localVarRequestOptions.headers['Content-Type'] === 'application/json'; + localVarRequestOptions.data = needsSerialization ? JSON.stringify(body !== undefined ? body : {}) : (body || ""); + + return { + url: localVarUrlObj.pathname + localVarUrlObj.search + localVarUrlObj.hash, + options: localVarRequestOptions, + }; + }, + + /** + * + * @summary 卸载动态插件 + * @param {DeletePluginCoreInput} [body] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + apiSysPluginCoreDeletePost: async (body?: DeletePluginCoreInput, options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/api/sysPluginCore/uninstall`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, 'https://example.com'); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + const localVarRequestOptions :AxiosRequestConfig = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication Bearer required + // http bearer authentication required + if (configuration && configuration.accessToken) { + const accessToken = typeof configuration.accessToken === 'function' + ? await configuration.accessToken() + : await configuration.accessToken; + localVarHeaderParameter["Authorization"] = "Bearer " + accessToken; + } + + localVarHeaderParameter['Content-Type'] = 'application/json-patch+json'; + + const query = new URLSearchParams(localVarUrlObj.search); + for (const key in localVarQueryParameter) { + query.set(key, localVarQueryParameter[key]); + } + for (const key in options.params) { + query.set(key, options.params[key]); + } + localVarUrlObj.search = (new URLSearchParams(query)).toString(); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + const needsSerialization = (typeof body !== "string") || localVarRequestOptions.headers['Content-Type'] === 'application/json'; + localVarRequestOptions.data = needsSerialization ? JSON.stringify(body !== undefined ? body : {}) : (body || ""); + + return { + url: localVarUrlObj.pathname + localVarUrlObj.search + localVarUrlObj.hash, + options: localVarRequestOptions, + }; + }, + /** + * + * @summary 启用插件 + * @param {DeletePluginCoreInput} [body] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + apiSysPluginCoreEnablePost: async (body?: DeletePluginCoreInput, options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/api/sysPluginCore/enable`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, 'https://example.com'); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + const localVarRequestOptions :AxiosRequestConfig = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication Bearer required + // http bearer authentication required + if (configuration && configuration.accessToken) { + const accessToken = typeof configuration.accessToken === 'function' + ? await configuration.accessToken() + : await configuration.accessToken; + localVarHeaderParameter["Authorization"] = "Bearer " + accessToken; + } + + localVarHeaderParameter['Content-Type'] = 'application/json-patch+json'; + + const query = new URLSearchParams(localVarUrlObj.search); + for (const key in localVarQueryParameter) { + query.set(key, localVarQueryParameter[key]); + } + for (const key in options.params) { + query.set(key, options.params[key]); + } + localVarUrlObj.search = (new URLSearchParams(query)).toString(); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + const needsSerialization = (typeof body !== "string") || localVarRequestOptions.headers['Content-Type'] === 'application/json'; + localVarRequestOptions.data = needsSerialization ? JSON.stringify(body !== undefined ? body : {}) : (body || ""); + + return { + url: localVarUrlObj.pathname + localVarUrlObj.search + localVarUrlObj.hash, + options: localVarRequestOptions, + }; + }, + /** + * + * @summary 禁用插件 + * @param {DeletePluginCoreInput} [body] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + apiSysPluginCoreDisablePost: async (body?: DeletePluginCoreInput, options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/api/sysPluginCore/disable`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, 'https://example.com'); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + const localVarRequestOptions :AxiosRequestConfig = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication Bearer required + // http bearer authentication required + if (configuration && configuration.accessToken) { + const accessToken = typeof configuration.accessToken === 'function' + ? await configuration.accessToken() + : await configuration.accessToken; + localVarHeaderParameter["Authorization"] = "Bearer " + accessToken; + } + + localVarHeaderParameter['Content-Type'] = 'application/json-patch+json'; + + const query = new URLSearchParams(localVarUrlObj.search); + for (const key in localVarQueryParameter) { + query.set(key, localVarQueryParameter[key]); + } + for (const key in options.params) { + query.set(key, options.params[key]); + } + localVarUrlObj.search = (new URLSearchParams(query)).toString(); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + const needsSerialization = (typeof body !== "string") || localVarRequestOptions.headers['Content-Type'] === 'application/json'; + localVarRequestOptions.data = needsSerialization ? JSON.stringify(body !== undefined ? body : {}) : (body || ""); + + return { + url: localVarUrlObj.pathname + localVarUrlObj.search + localVarUrlObj.hash, + options: localVarRequestOptions, + }; + }, + /** + * + * @summary 获取动态插件列表 + * @param {PagePluginCoreInput} [body] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + apiSysPluginCorePagePost: async (body?: PagePluginCoreInput, options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/api/sysPluginCore/page`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, 'https://example.com'); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + const localVarRequestOptions :AxiosRequestConfig = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication Bearer required + // http bearer authentication required + if (configuration && configuration.accessToken) { + const accessToken = typeof configuration.accessToken === 'function' + ? await configuration.accessToken() + : await configuration.accessToken; + localVarHeaderParameter["Authorization"] = "Bearer " + accessToken; + } + + localVarHeaderParameter['Content-Type'] = 'application/json-patch+json'; + + const query = new URLSearchParams(localVarUrlObj.search); + for (const key in localVarQueryParameter) { + query.set(key, localVarQueryParameter[key]); + } + for (const key in options.params) { + query.set(key, options.params[key]); + } + localVarUrlObj.search = (new URLSearchParams(query)).toString(); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + const needsSerialization = (typeof body !== "string") || localVarRequestOptions.headers['Content-Type'] === 'application/json'; + localVarRequestOptions.data = needsSerialization ? JSON.stringify(body !== undefined ? body : {}) : (body || ""); + + return { + url: localVarUrlObj.pathname + localVarUrlObj.search + localVarUrlObj.hash, + options: localVarRequestOptions, + }; + }, + + /** + * + * @summary 上传文件 + * @param {Blob} [file] + * @param {string} [path] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + apiSysFileUploadFilePostForm: async (file?: Blob, path?: string, options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/api/sysPluginCore/uploadFile`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, 'https://example.com'); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + const localVarRequestOptions :AxiosRequestConfig = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + const localVarFormParams = new FormData(); + + // authentication Bearer required + // http bearer authentication required + if (configuration && configuration.accessToken) { + const accessToken = typeof configuration.accessToken === 'function' + ? await configuration.accessToken() + : await configuration.accessToken; + localVarHeaderParameter["Authorization"] = "Bearer " + accessToken; + } + + if (path !== undefined) { + localVarQueryParameter['path'] = path; + } + + + if (file !== undefined) { + localVarFormParams.append('file', file as any); + } + + localVarHeaderParameter['Content-Type'] = 'multipart/form-data'; + const query = new URLSearchParams(localVarUrlObj.search); + for (const key in localVarQueryParameter) { + query.set(key, localVarQueryParameter[key]); + } + for (const key in options.params) { + query.set(key, options.params[key]); + } + localVarUrlObj.search = (new URLSearchParams(query)).toString(); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = localVarFormParams; + + return { + url: localVarUrlObj.pathname + localVarUrlObj.search + localVarUrlObj.hash, + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * SysPluginApi - functional programming interface + * @export + */ +export const SysPluginCoreApiFp = function(configuration?: Configuration) { + return { + /** + * + * @summary 查看插件文档 + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async apiSysPluginCoreReadmeGet(id:number,options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise>> { + const localVarAxiosArgs = await SysPluginCoreApiAxiosParamCreator(configuration).apiSysPluginCoreReadmeGet(id,options); + return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => { + const axiosRequestArgs :AxiosRequestConfig = {...localVarAxiosArgs.options, url: basePath + localVarAxiosArgs.url}; + return axios.request(axiosRequestArgs); + }; + }, + /** + * + * @summary 查看插件基本信息 + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async apiSysPluginCoreBaseInfoGet(id:number,options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise>> { + const localVarAxiosArgs = await SysPluginCoreApiAxiosParamCreator(configuration).apiSysPluginCoreBaseInfoGet(id,options); + return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => { + const axiosRequestArgs :AxiosRequestConfig = {...localVarAxiosArgs.options, url: basePath + localVarAxiosArgs.url}; + return axios.request(axiosRequestArgs); + }; + }, + /** + * + * @summary 查看插件基本信息 + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async apiSysPluginCoreSettingGet(id:number,options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise>> { + const localVarAxiosArgs = await SysPluginCoreApiAxiosParamCreator(configuration).apiSysPluginCoreSettingGet(id,options); + return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => { + const axiosRequestArgs :AxiosRequestConfig = {...localVarAxiosArgs.options, url: basePath + localVarAxiosArgs.url}; + return axios.request(axiosRequestArgs); + }; +}, + /** + * + * @summary 更新动态插件 + * @param {UpdatePluginInput} [body] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async apiSysPluginCoreSettingUpdatePost(body?: UpdatePluginCoreSettingInput, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise>> { + const localVarAxiosArgs = await SysPluginCoreApiAxiosParamCreator(configuration).apiSysPluginCoreSettingUpdatePost(body, options); + return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => { + const axiosRequestArgs :AxiosRequestConfig = {...localVarAxiosArgs.options, url: basePath + localVarAxiosArgs.url}; + return axios.request(axiosRequestArgs); + }; +}, + + + /** + * + * @summary 卸载动态插件 + * @param {DeletePluginCoreInput} [body] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async apiSysPluginCoreDeletePost(body?: DeletePluginCoreInput, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise>> { + const localVarAxiosArgs = await SysPluginCoreApiAxiosParamCreator(configuration).apiSysPluginCoreDeletePost(body, options); + return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => { + const axiosRequestArgs :AxiosRequestConfig = {...localVarAxiosArgs.options, url: basePath + localVarAxiosArgs.url}; + return axios.request(axiosRequestArgs); + }; + }, + /** + * + * @summary 卸载动态插件 + * @param {EnablePluginCoreInput} [body] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async apiSysPluginCoreEnablePost(body?: DeletePluginCoreInput, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise>> { + const localVarAxiosArgs = await SysPluginCoreApiAxiosParamCreator(configuration).apiSysPluginCoreEnablePost(body, options); + return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => { + const axiosRequestArgs :AxiosRequestConfig = {...localVarAxiosArgs.options, url: basePath + localVarAxiosArgs.url}; + return axios.request(axiosRequestArgs); + }; + }, + /** + * + * @summary 禁用插件 + * @param {DisablePluginCoreInput} [body] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async apiSysPluginCoreDisablePost(body?: DeletePluginCoreInput, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise>> { + const localVarAxiosArgs = await SysPluginCoreApiAxiosParamCreator(configuration).apiSysPluginCoreDisablePost(body, options); + return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => { + const axiosRequestArgs :AxiosRequestConfig = {...localVarAxiosArgs.options, url: basePath + localVarAxiosArgs.url}; + return axios.request(axiosRequestArgs); + }; + }, + /** + * + * @summary 获取动态插件列表 + * @param {PagePluginCoreInput} [body] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async apiSysPluginCorePagePost(body?: PagePluginCoreInput, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise>> { + const localVarAxiosArgs = await SysPluginCoreApiAxiosParamCreator(configuration).apiSysPluginCorePagePost(body, options); + return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => { + const axiosRequestArgs :AxiosRequestConfig = {...localVarAxiosArgs.options, url: basePath + localVarAxiosArgs.url}; + return axios.request(axiosRequestArgs); + }; + }, + + + /** + * + * @summary 上传文件 + * @param {Blob} [file] + * @param {string} [path] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async apiSysFileUploadFilePostForm(file?: Blob, path?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise>> { + const localVarAxiosArgs = await SysPluginCoreApiAxiosParamCreator(configuration).apiSysFileUploadFilePostForm(file, path, options); + return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => { + const axiosRequestArgs :AxiosRequestConfig = {...localVarAxiosArgs.options, url: basePath + localVarAxiosArgs.url}; + return axios.request(axiosRequestArgs); + }; + }, + } +}; + +/** + * SysPluginApi - factory interface + * @export + */ +export const SysPluginCoreApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + return { + /** + * + * @summary 查看插件文档 + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async apiSysPluginCoreReadmeGet(id:number,options?: AxiosRequestConfig): Promise> { + return SysPluginCoreApiFp(configuration).apiSysPluginCoreReadmeGet(id,options).then((request) => request(axios, basePath)); +}, + + /** + * + * @summary 查看插件基本信息 + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async apiSysPluginCoreBaseInfoGet(id:number,options?: AxiosRequestConfig): Promise> { + return SysPluginCoreApiFp(configuration).apiSysPluginCoreBaseInfoGet(id,options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary 查看插件基本信息 + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async apiSysPluginCoreSettingGet(id:number,options?: AxiosRequestConfig): Promise> { + return SysPluginCoreApiFp(configuration).apiSysPluginCoreSettingGet(id,options).then((request) => request(axios, basePath)); +}, + /** + * + * @summary 更新动态插件 + * @param {UpdatePluginInput} [body] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async apiSysPluginCoreSettingUpdatePost(body?: UpdatePluginCoreSettingInput, options?: AxiosRequestConfig): Promise> { + return SysPluginCoreApiFp(configuration).apiSysPluginCoreSettingUpdatePost(body, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary 卸载动态插件 + * @param {DeletePluginCoreInput} [body] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async apiSysPluginCoreDeletePost(body?: DeletePluginCoreInput, options?: AxiosRequestConfig): Promise> { + return SysPluginCoreApiFp(configuration).apiSysPluginCoreDeletePost(body, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary 启用插件 + * @param {DeletePluginCoreInput} [body] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async apiSysPluginCoreEnablePost(body?: DeletePluginCoreInput, options?: AxiosRequestConfig): Promise> { + return SysPluginCoreApiFp(configuration).apiSysPluginCoreEnablePost(body, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary 禁用插件 + * @param {DeletePluginCoreInput} [body] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async apiSysPluginCoreDisablePost(body?: DeletePluginCoreInput, options?: AxiosRequestConfig): Promise> { + return SysPluginCoreApiFp(configuration).apiSysPluginCoreDisablePost(body, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary 获取动态插件列表 + * @param {PagePluginCoreInput} [body] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async apiSysPluginCorePagePost(body?: PagePluginCoreInput, options?: AxiosRequestConfig): Promise> { + return SysPluginCoreApiFp(configuration).apiSysPluginCorePagePost(body, options).then((request) => request(axios, basePath)); + }, + + /** + * + * @summary 上传文件 + * @param {Blob} [file] + * @param {string} [path] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async apiSysFileUploadFilePostForm(file?: Blob, path?: string, options?: AxiosRequestConfig): Promise> { + return SysPluginCoreApiFp(configuration).apiSysFileUploadFilePostForm(file, path, options).then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * SysPluginApi - object-oriented interface + * @export + * @class SysPluginApi + * @extends {BaseAPI} + */ +export class SysPluginCoreApi extends BaseAPI { + /** + * + * @summary 查看插件文档 + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof SysPluginApi + */ + public async apiSysPluginCoreReadmeGet(id:number,options?: AxiosRequestConfig) : Promise> { + return SysPluginCoreApiFp(this.configuration).apiSysPluginCoreReadmeGet(id,options).then((request) => request(this.axios, this.basePath)); + } + /** + * + * @summary 查看插件基本信息 + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof SysPluginApi + */ + public async apiSysPluginCoreBaseInfoGet(id:number,options?: AxiosRequestConfig) : Promise> { + return SysPluginCoreApiFp(this.configuration).apiSysPluginCoreBaseInfoGet(id,options).then((request) => request(this.axios, this.basePath)); + } + /** + * + * @summary 查看插件设置信息 + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof SysPluginApi + */ + public async apiSysPluginCoreSettingGet(id:number,options?: AxiosRequestConfig) : Promise> { + return SysPluginCoreApiFp(this.configuration).apiSysPluginCoreSettingGet(id,options).then((request) => request(this.axios, this.basePath)); + } + /** + * + * @summary 更新动态插件 + * @param {UpdatePluginInput} [body] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof SysPluginApi + */ + public async apiSysPluginCoreSettingUpdatePost(body?: UpdatePluginCoreSettingInput, options?: AxiosRequestConfig) : Promise> { + return SysPluginCoreApiFp(this.configuration).apiSysPluginCoreSettingUpdatePost(body, options).then((request) => request(this.axios, this.basePath)); + } + /** + * + * @summary 卸载动态插件 + * @param {DeletePluginCoreInput} [body] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof SysPluginApi + */ + public async apiSysPluginCoreDeletePost(body?: DeletePluginCoreInput, options?: AxiosRequestConfig) : Promise> { + return SysPluginCoreApiFp(this.configuration).apiSysPluginCoreDeletePost(body, options).then((request) => request(this.axios, this.basePath)); + } + /** + * + * @summary 启用插件 + * @param {EnablePluginCoreInput} [body] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof SysPluginApi + */ + public async apiSysPluginCoreEnablePost(body?: DeletePluginCoreInput, options?: AxiosRequestConfig) : Promise> { + return SysPluginCoreApiFp(this.configuration).apiSysPluginCoreEnablePost(body, options).then((request) => request(this.axios, this.basePath)); + } + /** + * + * @summary 禁用插件 + * @param {DisablePluginCoreInput} [body] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof SysPluginApi + */ + public async apiSysPluginCoreDisablePost(body?: DeletePluginCoreInput, options?: AxiosRequestConfig) : Promise> { + return SysPluginCoreApiFp(this.configuration).apiSysPluginCoreDisablePost(body, options).then((request) => request(this.axios, this.basePath)); + } + /** + * + * @summary 获取动态插件列表 + * @param {PagePluginCoreInput} [body] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof SysPluginApi + */ + public async apiSysPluginCorePagePost(body?: PagePluginCoreInput, options?: AxiosRequestConfig) : Promise> { + return SysPluginCoreApiFp(this.configuration).apiSysPluginCorePagePost(body, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary 上传文件 + * @param {Blob} [file] + * @param {string} [path] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof SysFileApi + */ + public async apiSysFileUploadFilePostForm(file?: Blob, path?: string, options?: AxiosRequestConfig) : Promise> { + return SysPluginCoreApiFp(this.configuration).apiSysFileUploadFilePostForm(file, path, options).then((request) => request(this.axios, this.basePath)); + } +} diff --git a/Web/src/api-plugins/plugincore/base.ts b/Web/src/api-plugins/plugincore/base.ts new file mode 100644 index 00000000..ce437968 --- /dev/null +++ b/Web/src/api-plugins/plugincore/base.ts @@ -0,0 +1,70 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * PaddleOCR 图像识别 + *
👮不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + * + * OpenAPI spec version: 1.0.0 + * + * + * NOTE: This class is auto generated by the swagger code generator program. + * https://github.com/swagger-api/swagger-codegen.git + * Do not edit the class manually. + */ + +import { Configuration } from "./configuration"; +// Some imports not used depending on template conditions +// @ts-ignore +import globalAxios, { AxiosRequestConfig, AxiosInstance } from 'axios'; + +export const BASE_PATH = "/".replace(/\/+$/, ""); + +/** + * + * @export + */ +export const COLLECTION_FORMATS = { + csv: ",", + ssv: " ", + tsv: "\t", + pipes: "|", +}; + +/** + * + * @export + * @interface RequestArgs + */ +export interface RequestArgs { + url: string; + options: AxiosRequestConfig; +} + +/** + * + * @export + * @class BaseAPI + */ +export class BaseAPI { + protected configuration: Configuration | undefined; + + constructor(configuration?: Configuration, protected basePath: string = BASE_PATH, protected axios: AxiosInstance = globalAxios) { + if (configuration) { + this.configuration = configuration; + this.basePath = configuration.basePath || this.basePath; + } + } +}; + +/** + * + * @export + * @class RequiredError + * @extends {Error} + */ +export class RequiredError extends Error { + name: "RequiredError" = "RequiredError"; + constructor(public field: string, msg?: string) { + super(msg); + } +} diff --git a/Web/src/api-plugins/plugincore/configuration.ts b/Web/src/api-plugins/plugincore/configuration.ts new file mode 100644 index 00000000..ec813c45 --- /dev/null +++ b/Web/src/api-plugins/plugincore/configuration.ts @@ -0,0 +1,83 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * PaddleOCR 图像识别 + *
👮不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + * + * OpenAPI spec version: 1.0.0 + * + * + * NOTE: This class is auto generated by the swagger code generator program. + * https://github.com/swagger-api/swagger-codegen.git + * Do not edit the class manually. + */ + +export interface ConfigurationParameters { + apiKey?: string | Promise | ((name: string) => string) | ((name: string) => Promise); + username?: string; + password?: string; + accessToken?: string | Promise | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise); + basePath?: string; + baseOptions?: any; +} + +export class Configuration { + + /** + * parameter for apiKey security + * + * @param name security name + * @memberof Configuration + */ + apiKey?: string | Promise | ((name: string) => string) | ((name: string) => Promise); + + /** + * parameter for basic security + * + * @type {string} + * @memberof Configuration + */ + username?: string; + + /** + * parameter for basic security + * + * @type {string} + * @memberof Configuration + */ + password?: string; + + /** + * parameter for oauth2 security + * + * @param name security name + * @param scopes oauth2 scope + * @memberof Configuration + */ + accessToken?: string | Promise | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise); + + /** + * override base path + * + * @type {string} + * @memberof Configuration + */ + basePath?: string; + + /** + * base options for axios calls + * + * @type {any} + * @memberof Configuration + */ + baseOptions?: any; + + constructor(param: ConfigurationParameters = {}) { + this.apiKey = param.apiKey; + this.username = param.username; + this.password = param.password; + this.accessToken = param.accessToken; + this.basePath = param.basePath; + this.baseOptions = param.baseOptions; + } +} diff --git a/Web/src/api-plugins/plugincore/index.ts b/Web/src/api-plugins/plugincore/index.ts new file mode 100644 index 00000000..84614ab3 --- /dev/null +++ b/Web/src/api-plugins/plugincore/index.ts @@ -0,0 +1,18 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * PaddleOCR 图像识别 + *
👮不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + * + * OpenAPI spec version: 1.0.0 + * + * + * NOTE: This class is auto generated by the swagger code generator program. + * https://github.com/swagger-api/swagger-codegen.git + * Do not edit the class manually. + */ + +export * from "./api"; +export * from "./configuration"; +export * from "./models"; + diff --git a/Web/src/api-plugins/plugincore/models/add-plugin-core-input.ts b/Web/src/api-plugins/plugincore/models/add-plugin-core-input.ts new file mode 100644 index 00000000..a85e4957 --- /dev/null +++ b/Web/src/api-plugins/plugincore/models/add-plugin-core-input.ts @@ -0,0 +1,99 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Admin.NET + * 让 .NET 开发更简单、更通用、更流行。前后端分离架构(.NET6/Vue3),开箱即用紧随前沿技术。
https://gitee.com/zuohuaijun/Admin.NET + * + * OpenAPI spec version: 1.0.0 + * Contact: 515096995@qq.com + * + * NOTE: This class is auto generated by the swagger code generator program. + * https://github.com/swagger-api/swagger-codegen.git + * Do not edit the class manually. + */ +import { StatusEnum } from '../../../api-services/models/status-enum'; +/** + * + * @export + * @interface AddPluginCoreInput + */ +export interface AddPluginCoreInput { + /** + * 雪花Id + * @type {number} + * @memberof AddPluginCoreInput + */ + id?: number; + /** + * 创建时间 + * @type {Date} + * @memberof AddPluginCoreInput + */ + createTime?: Date | null; + /** + * 更新时间 + * @type {Date} + * @memberof AddPluginCoreInput + */ + updateTime?: Date | null; + /** + * 创建者Id + * @type {number} + * @memberof AddPluginCoreInput + */ + createUserId?: number | null; + /** + * 修改者Id + * @type {number} + * @memberof AddPluginCoreInput + */ + updateUserId?: number | null; + /** + * 软删除 + * @type {boolean} + * @memberof AddPluginCoreInput + */ + isDelete?: boolean; + /** + * 租户Id + * @type {number} + * @memberof AddPluginCoreInput + */ + tenantId?: number | null; + /** + * C#代码 + * @type {string} + * @memberof AddPluginCoreInput + */ + csharpCode: string; + /** + * 程序集名称 + * @type {string} + * @memberof AddPluginCoreInput + */ + assemblyName?: string | null; + /** + * 排序 + * @type {number} + * @memberof AddPluginCoreInput + */ + orderNo?: number; + /** + * + * @type {StatusEnum} + * @memberof AddPluginCoreInput + */ + status?: StatusEnum; + /** + * 备注 + * @type {string} + * @memberof AddPluginCoreInput + */ + remark?: string | null; + /** + * 名称 + * @type {string} + * @memberof AddPluginCoreInput + */ + name: string; +} diff --git a/Web/src/api-plugins/plugincore/models/admin-result-sql-sugar-paged-list-sys-plugin-core.ts b/Web/src/api-plugins/plugincore/models/admin-result-sql-sugar-paged-list-sys-plugin-core.ts new file mode 100644 index 00000000..6c94cdae --- /dev/null +++ b/Web/src/api-plugins/plugincore/models/admin-result-sql-sugar-paged-list-sys-plugin-core.ts @@ -0,0 +1,57 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Admin.NET + * 让 .NET 开发更简单、更通用、更流行。前后端分离架构(.NET6/Vue3),开箱即用紧随前沿技术。
https://gitee.com/zuohuaijun/Admin.NET + * + * OpenAPI spec version: 1.0.0 + * Contact: 515096995@qq.com + * + * NOTE: This class is auto generated by the swagger code generator program. + * https://github.com/swagger-api/swagger-codegen.git + * Do not edit the class manually. + */ +import { SqlSugarPagedListSysPluginCore } from './sql-sugar-paged-list-sys-plugin-core'; +/** + * 全局返回结果 + * @export + * @interface AdminResultSqlSugarPagedListSysPluginCore + */ +export interface AdminResultSqlSugarPagedListSysPluginCore { + /** + * 状态码 + * @type {number} + * @memberof AdminResultSqlSugarPagedListSysPluginCore + */ + code?: number; + /** + * 类型success、warning、error + * @type {string} + * @memberof AdminResultSqlSugarPagedListSysPluginCore + */ + type?: string | null; + /** + * 错误信息 + * @type {string} + * @memberof AdminResultSqlSugarPagedListSysPluginCore + */ + message?: string | null; + /** + * + * @type {SqlSugarPagedListSysPlugin} + * @memberof AdminResultSqlSugarPagedListSysPluginCore + */ + result?: SqlSugarPagedListSysPluginCore; + /** + * 附加数据 + * @type {any} + * @memberof AdminResultSqlSugarPagedListSysPluginCore + */ + extras?: any | null; + /** + * 时间 + * @type {Date} + * @memberof AdminResultSqlSugarPagedListSysPluginCore + */ + time?: Date; +} diff --git a/Web/src/api-plugins/plugincore/models/admin-result-sys-plugin-core-readme.ts b/Web/src/api-plugins/plugincore/models/admin-result-sys-plugin-core-readme.ts new file mode 100644 index 00000000..5eec06e9 --- /dev/null +++ b/Web/src/api-plugins/plugincore/models/admin-result-sys-plugin-core-readme.ts @@ -0,0 +1,57 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Admin.NET + * 让 .NET 开发更简单、更通用、更流行。前后端分离架构(.NET6/Vue3),开箱即用紧随前沿技术。
https://gitee.com/zuohuaijun/Admin.NET + * + * OpenAPI spec version: 1.0.0 + * Contact: 515096995@qq.com + * + * NOTE: This class is auto generated by the swagger code generator program. + * https://github.com/swagger-api/swagger-codegen.git + * Do not edit the class manually. + */ +import { SysPluginCoreReadme } from './sys-plugin-core-readme'; +/** + * 全局返回结果 + * @export + * @interface AdminResultSysPluginCoreReadme + */ +export interface AdminResultSysPluginCoreReadme { + /** + * 状态码 + * @type {number} + * @memberof AdminResultSysPluginCoreReadme + */ + code?: number; + /** + * 类型success、warning、error + * @type {string} + * @memberof AdminResultSysPluginCoreReadme + */ + type?: string | null; + /** + * 错误信息 + * @type {string} + * @memberof AdminResultSysPluginCoreReadme + */ + message?: string | null; + /** + * + * @type {SysPluginCoreReadme} + * @memberof AdminResultSysPluginCoreReadme + */ + result?: SysPluginCoreReadme; + /** + * 附加数据 + * @type {any} + * @memberof AdminResultSysPluginCoreReadme + */ + extras?: any | null; + /** + * 时间 + * @type {Date} + * @memberof AdminResultSysPluginCoreReadme + */ + time?: Date; +} diff --git a/Web/src/api-plugins/plugincore/models/admin-result-sys-plugin-core.ts b/Web/src/api-plugins/plugincore/models/admin-result-sys-plugin-core.ts new file mode 100644 index 00000000..3a53049e --- /dev/null +++ b/Web/src/api-plugins/plugincore/models/admin-result-sys-plugin-core.ts @@ -0,0 +1,57 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Admin.NET + * 让 .NET 开发更简单、更通用、更流行。前后端分离架构(.NET6/Vue3),开箱即用紧随前沿技术。
https://gitee.com/zuohuaijun/Admin.NET + * + * OpenAPI spec version: 1.0.0 + * Contact: 515096995@qq.com + * + * NOTE: This class is auto generated by the swagger code generator program. + * https://github.com/swagger-api/swagger-codegen.git + * Do not edit the class manually. + */ +import { SysPluginCore } from './sys-plugin-core'; +/** + * 全局返回结果 + * @export + * @interface AdminResultSysPluginCore + */ +export interface AdminResultSysPluginCore { + /** + * 状态码 + * @type {number} + * @memberof AdminResultSysPluginCore + */ + code?: number; + /** + * 类型success、warning、error + * @type {string} + * @memberof AdminResultSysPluginCore + */ + type?: string | null; + /** + * 错误信息 + * @type {string} + * @memberof AdminResultSysPluginCore + */ + message?: string | null; + /** + * + * @type {SysPluginCore} + * @memberof AdminResultSysPluginCore + */ + result?: SysPluginCore; + /** + * 附加数据 + * @type {any} + * @memberof AdminResultSysPluginCore + */ + extras?: any | null; + /** + * 时间 + * @type {Date} + * @memberof AdminResultSysPluginCore + */ + time?: Date; +} diff --git a/Web/src/api-plugins/plugincore/models/delete-plugin-core-input.ts b/Web/src/api-plugins/plugincore/models/delete-plugin-core-input.ts new file mode 100644 index 00000000..d16bb8ae --- /dev/null +++ b/Web/src/api-plugins/plugincore/models/delete-plugin-core-input.ts @@ -0,0 +1,26 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Admin.NET + * 让 .NET 开发更简单、更通用、更流行。前后端分离架构(.NET6/Vue3),开箱即用紧随前沿技术。
https://gitee.com/zuohuaijun/Admin.NET + * + * OpenAPI spec version: 1.0.0 + * Contact: 515096995@qq.com + * + * NOTE: This class is auto generated by the swagger code generator program. + * https://github.com/swagger-api/swagger-codegen.git + * Do not edit the class manually. + */ +/** + * + * @export + * @interface DeletePluginCoreInput + */ +export interface DeletePluginCoreInput { + /** + * 主键Id + * @type {number} + * @memberof DeletePluginCoreInput + */ + id: number; +} diff --git a/Web/src/api-plugins/plugincore/models/index.ts b/Web/src/api-plugins/plugincore/models/index.ts new file mode 100644 index 00000000..d76de72d --- /dev/null +++ b/Web/src/api-plugins/plugincore/models/index.ts @@ -0,0 +1,11 @@ +export * from './add-plugin-core-input'; +export * from './admin-result-sql-sugar-paged-list-sys-plugin-core'; +export * from './admin-result-sys-plugin-core-readme'; +export * from './admin-result-sys-plugin-core'; +export * from './delete-plugin-core-input'; +export * from './page-plugin-core-input'; +export * from './sys-plugin-core-readme'; +export * from './sys-plugin-core'; +export * from './update-plugin-core-input'; +export * from './update-plugin-core-setting'; +export * from './sql-sugar-paged-list-sys-plugin-core'; \ No newline at end of file diff --git a/Web/src/api-plugins/plugincore/models/page-plugin-core-input.ts b/Web/src/api-plugins/plugincore/models/page-plugin-core-input.ts new file mode 100644 index 00000000..900e19db --- /dev/null +++ b/Web/src/api-plugins/plugincore/models/page-plugin-core-input.ts @@ -0,0 +1,62 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Admin.NET + * 让 .NET 开发更简单、更通用、更流行。前后端分离架构(.NET6/Vue3),开箱即用紧随前沿技术。
https://gitee.com/zuohuaijun/Admin.NET + * + * OpenAPI spec version: 1.0.0 + * Contact: 515096995@qq.com + * + * NOTE: This class is auto generated by the swagger code generator program. + * https://github.com/swagger-api/swagger-codegen.git + * Do not edit the class manually. + */ +/** + * + * @export + * @interface PagePluginCoreInput + */ +export interface PagePluginCoreInput { + /** + * 当前页码 + * @type {number} + * @memberof PagePluginCoreInput + */ + page?: number; + /** + * 页码容量 + * @type {number} + * @memberof PagePluginCoreInput + */ + pageSize?: number; + /** + * 排序字段 + * @type {string} + * @memberof PagePluginCoreInput + */ + field?: string | null; + /** + * 排序方向 + * @type {string} + * @memberof PagePluginCoreInput + */ + order?: string | null; + /** + * 降序排序 + * @type {string} + * @memberof PagePluginCoreInput + */ + descStr?: string | null; + /** + * 名称 + * @type {string} + * @memberof PagePluginCoreInput + */ + name?: string | null; + /** + * 编码 + * @type {string} + * @memberof PagePluginCoreInput + */ + code?: string | null; +} diff --git a/Web/src/api-plugins/plugincore/models/sql-sugar-paged-list-sys-plugin-core.ts b/Web/src/api-plugins/plugincore/models/sql-sugar-paged-list-sys-plugin-core.ts new file mode 100644 index 00000000..a0c8248d --- /dev/null +++ b/Web/src/api-plugins/plugincore/models/sql-sugar-paged-list-sys-plugin-core.ts @@ -0,0 +1,63 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Admin.NET + * 让 .NET 开发更简单、更通用、更流行。前后端分离架构(.NET6/Vue3),开箱即用紧随前沿技术。
https://gitee.com/zuohuaijun/Admin.NET + * + * OpenAPI spec version: 1.0.0 + * Contact: 515096995@qq.com + * + * NOTE: This class is auto generated by the swagger code generator program. + * https://github.com/swagger-api/swagger-codegen.git + * Do not edit the class manually. + */ +import { SysPluginCore } from './sys-plugin-core'; +/** + * 分页泛型集合 + * @export + * @interface SqlSugarPagedListSysPluginCore + */ +export interface SqlSugarPagedListSysPluginCore { + /** + * 页码 + * @type {number} + * @memberof SqlSugarPagedListSysPluginCore + */ + page?: number; + /** + * 页容量 + * @type {number} + * @memberof SqlSugarPagedListSysPluginCore + */ + pageSize?: number; + /** + * 总条数 + * @type {number} + * @memberof SqlSugarPagedListSysPluginCore + */ + total?: number; + /** + * 总页数 + * @type {number} + * @memberof SqlSugarPagedListSysPluginCore + */ + totalPages?: number; + /** + * 当前页集合 + * @type {Array} + * @memberof SqlSugarPagedListSysPluginCore + */ + items?: Array | null; + /** + * 是否有上一页 + * @type {boolean} + * @memberof SqlSugarPagedListSysPluginCore + */ + hasPrevPage?: boolean; + /** + * 是否有下一页 + * @type {boolean} + * @memberof SqlSugarPagedListSysPluginCore + */ + hasNextPage?: boolean; +} diff --git a/Web/src/api-plugins/plugincore/models/sys-plugin-core-readme.ts b/Web/src/api-plugins/plugincore/models/sys-plugin-core-readme.ts new file mode 100644 index 00000000..8b3b2be1 --- /dev/null +++ b/Web/src/api-plugins/plugincore/models/sys-plugin-core-readme.ts @@ -0,0 +1,47 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Admin.NET + * 让 .NET 开发更简单、更通用、更流行。前后端分离架构(.NET6/Vue3),开箱即用紧随前沿技术。
https://gitee.com/zuohuaijun/Admin.NET + * + * OpenAPI spec version: 1.0.0 + * Contact: 515096995@qq.com + * + * NOTE: This class is auto generated by the swagger code generator program. + * https://github.com/swagger-api/swagger-codegen.git + * Do not edit the class manually. + */ + +/** + * 系统动态插件表 + * @export + * @interface SysPluginCoreReadme + */ +export interface SysPluginCoreReadme { + /** + * 雪花Id + * @type {number} + * @memberof SysPluginCoreReadme + */ + id?: number; + + /** + * 插件ID + * @type {string} + * @memberof SysPluginCoreReadme + */ + pluginId: string; + /** + * 名称 + * @type {string} + * @memberof SysPluginCoreReadme + */ + displayName: string; + + /** + * 备注 + * @type {string} + * @memberof SysPluginCoreReadme + */ + content?: string | null; +} diff --git a/Web/src/api-plugins/plugincore/models/sys-plugin-core.ts b/Web/src/api-plugins/plugincore/models/sys-plugin-core.ts new file mode 100644 index 00000000..dd1ba58d --- /dev/null +++ b/Web/src/api-plugins/plugincore/models/sys-plugin-core.ts @@ -0,0 +1,117 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Admin.NET + * 让 .NET 开发更简单、更通用、更流行。前后端分离架构(.NET6/Vue3),开箱即用紧随前沿技术。
https://gitee.com/zuohuaijun/Admin.NET + * + * OpenAPI spec version: 1.0.0 + * Contact: 515096995@qq.com + * + * NOTE: This class is auto generated by the swagger code generator program. + * https://github.com/swagger-api/swagger-codegen.git + * Do not edit the class manually. + */ +import { StatusEnum } from '../../../api-services/models/status-enum'; +/** + * 系统动态插件表 + * @export + * @interface SysPluginCore + */ +export interface SysPluginCore { + /** + * 雪花Id + * @type {number} + * @memberof SysPluginCore + */ + id?: number; + /** + * 创建时间 + * @type {Date} + * @memberof SysPluginCore + */ + createTime?: Date | null; + /** + * 更新时间 + * @type {Date} + * @memberof SysPluginCore + */ + updateTime?: Date | null; + /** + * 创建者Id + * @type {number} + * @memberof SysPluginCore + */ + createUserId?: number | null; + /** + * 修改者Id + * @type {number} + * @memberof SysPluginCore + */ + updateUserId?: number | null; + /** + * 软删除 + * @type {boolean} + * @memberof SysPluginCore + */ + isDelete?: boolean; + /** + * 租户Id + * @type {number} + * @memberof SysPluginCore + */ + tenantId?: number | null; + /** + * 插件ID + * @type {string} + * @memberof SysPluginCore + */ + PluginId: string; + /** + * 名称 + * @type {string} + * @memberof SysPluginCore + */ + DisplayName: string; + /** + * 作者 + * @type {string} + * @memberof SysPluginCore + */ + Author: string; + /** + * 版本 + * @type {string} + * @memberof SysPluginCore + */ + Version: string; + /** + * C#代码 + * @type {string} + * @memberof SysPluginCore + */ + csharpCode: string; + /** + * 描述 + * @type {string} + * @memberof SysPluginCore + */ + Description?: string | null; + /** + * 排序 + * @type {number} + * @memberof SysPluginCore + */ + orderNo?: number; + /** + * + * @type {StatusEnum} + * @memberof SysPluginCore + */ + status?: StatusEnum; + /** + * 备注 + * @type {string} + * @memberof SysPluginCore + */ + remark?: string | null; +} diff --git a/Web/src/api-plugins/plugincore/models/update-plugin-core-input.ts b/Web/src/api-plugins/plugincore/models/update-plugin-core-input.ts new file mode 100644 index 00000000..541210e3 --- /dev/null +++ b/Web/src/api-plugins/plugincore/models/update-plugin-core-input.ts @@ -0,0 +1,99 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Admin.NET + * 让 .NET 开发更简单、更通用、更流行。前后端分离架构(.NET6/Vue3),开箱即用紧随前沿技术。
https://gitee.com/zuohuaijun/Admin.NET + * + * OpenAPI spec version: 1.0.0 + * Contact: 515096995@qq.com + * + * NOTE: This class is auto generated by the swagger code generator program. + * https://github.com/swagger-api/swagger-codegen.git + * Do not edit the class manually. + */ +import { StatusEnum } from '../../../api-services/models/status-enum'; +/** + * + * @export + * @interface UpdatePluginCoreInput + */ +export interface UpdatePluginCoreInput { + /** + * 雪花Id + * @type {number} + * @memberof UpdatePluginCoreInput + */ + id?: number; + /** + * 创建时间 + * @type {Date} + * @memberof UpdatePluginCoreInput + */ + createTime?: Date | null; + /** + * 更新时间 + * @type {Date} + * @memberof UpdatePluginCoreInput + */ + updateTime?: Date | null; + /** + * 创建者Id + * @type {number} + * @memberof UpdatePluginCoreInput + */ + createUserId?: number | null; + /** + * 修改者Id + * @type {number} + * @memberof UpdatePluginCoreInput + */ + updateUserId?: number | null; + /** + * 软删除 + * @type {boolean} + * @memberof UpdatePluginCoreInput + */ + isDelete?: boolean; + /** + * 租户Id + * @type {number} + * @memberof UpdatePluginCoreInput + */ + tenantId?: number | null; + /** + * C#代码 + * @type {string} + * @memberof UpdatePluginCoreInput + */ + csharpCode: string; + /** + * 程序集名称 + * @type {string} + * @memberof UpdatePluginCoreInput + */ + assemblyName?: string | null; + /** + * 排序 + * @type {number} + * @memberof UpdatePluginCoreInput + */ + orderNo?: number; + /** + * + * @type {StatusEnum} + * @memberof UpdatePluginCoreInput + */ + status?: StatusEnum; + /** + * 备注 + * @type {string} + * @memberof UpdatePluginCoreInput + */ + remark?: string | null; + /** + * 名称 + * @type {string} + * @memberof UpdatePluginCoreInput + */ + name: string; +} diff --git a/Web/src/api-plugins/plugincore/models/update-plugin-core-setting.ts b/Web/src/api-plugins/plugincore/models/update-plugin-core-setting.ts new file mode 100644 index 00000000..0ca85b27 --- /dev/null +++ b/Web/src/api-plugins/plugincore/models/update-plugin-core-setting.ts @@ -0,0 +1,35 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Admin.NET + * 让 .NET 开发更简单、更通用、更流行。前后端分离架构(.NET6/Vue3),开箱即用紧随前沿技术。
https://gitee.com/zuohuaijun/Admin.NET + * + * OpenAPI spec version: 1.0.0 + * Contact: 515096995@qq.com + * + * NOTE: This class is auto generated by the swagger code generator program. + * https://github.com/swagger-api/swagger-codegen.git + * Do not edit the class manually. + */ +import { StatusEnum } from '../../../api-services/models/status-enum'; +/** + * + * @export + * @interface UpdatePluginCoreSettingInput + */ +export interface UpdatePluginCoreSettingInput { + /** + * 雪花Id + * @type {number} + * @memberof UpdatePluginCoreSettingInput + */ + id?: number; + + /** + * C#代码 + * @type {string} + * @memberof UpdatePluginCoreSettingInput + */ + data: string; + +} diff --git a/Web/src/views/system/plugincore/component/detailsPluginDialog.vue b/Web/src/views/system/plugincore/component/detailsPluginDialog.vue new file mode 100644 index 00000000..1bc20a4f --- /dev/null +++ b/Web/src/views/system/plugincore/component/detailsPluginDialog.vue @@ -0,0 +1,73 @@ + + + diff --git a/Web/src/views/system/plugincore/component/editPluginSetting.vue b/Web/src/views/system/plugincore/component/editPluginSetting.vue new file mode 100644 index 00000000..fa38aaea --- /dev/null +++ b/Web/src/views/system/plugincore/component/editPluginSetting.vue @@ -0,0 +1,117 @@ + + + diff --git a/Web/src/views/system/plugincore/index.vue b/Web/src/views/system/plugincore/index.vue new file mode 100644 index 00000000..f8871314 --- /dev/null +++ b/Web/src/views/system/plugincore/index.vue @@ -0,0 +1,292 @@ + + +