diff --git a/src/Infrastructure/BotSharp.Abstraction/Plugins/IBotSharpAppPlugin.cs b/src/Infrastructure/BotSharp.Abstraction/Plugins/IBotSharpAppPlugin.cs new file mode 100644 index 00000000..d4106b7a --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Plugins/IBotSharpAppPlugin.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Builder; +using System; +using System.Collections.Generic; +using System.Text; + +namespace BotSharp.Abstraction.Plugins +{ + public interface IBotSharpAppPlugin: IBotSharpPlugin + { + void Configure(IApplicationBuilder app); + } +} diff --git a/src/Infrastructure/BotSharp.Core/BotSharpServiceCollectionExtensions.cs b/src/Infrastructure/BotSharp.Core/BotSharpServiceCollectionExtensions.cs index c8604051..2ae1a0c7 100644 --- a/src/Infrastructure/BotSharp.Core/BotSharpServiceCollectionExtensions.cs +++ b/src/Infrastructure/BotSharp.Core/BotSharpServiceCollectionExtensions.cs @@ -37,6 +37,8 @@ public static class BotSharpServiceCollectionExtensions throw new ArgumentNullException(nameof(app)); } + app.ApplicationServices.GetRequiredService().Configure(app); + return app; } @@ -74,5 +76,7 @@ public static class BotSharpServiceCollectionExtensions var loader = new PluginLoader(services, config, pluginSettings); loader.Load(); + + services.AddSingleton(loader); } } diff --git a/src/Infrastructure/BotSharp.Core/Plugins/PluginLoader.cs b/src/Infrastructure/BotSharp.Core/Plugins/PluginLoader.cs index 79cdd134..516b906f 100644 --- a/src/Infrastructure/BotSharp.Core/Plugins/PluginLoader.cs +++ b/src/Infrastructure/BotSharp.Core/Plugins/PluginLoader.cs @@ -1,3 +1,4 @@ +using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; using System.Drawing; using System.IO; @@ -13,7 +14,7 @@ public class PluginLoader private readonly PluginLoaderSettings _settings; private static List _modules = new List(); - public PluginLoader(IServiceCollection services, + public PluginLoader(IServiceCollection services, IConfiguration config, PluginLoaderSettings settings) { @@ -59,4 +60,23 @@ public class PluginLoader } }); } + + public void Configure(IApplicationBuilder app) + { + if(_modules.Count == 0) + { + Console.WriteLine($"No plugin loaded. Please check whether the Load() method is called.", Color.Yellow); + } + + _modules.ForEach(module => + { + if (module.GetType().GetInterface(nameof(IBotSharpAppPlugin)) != null) + { + if (_settings.Plugins.Contains(module.GetType().Name)) + { + (module as IBotSharpAppPlugin).Configure(app); + } + } + }); + } } diff --git a/src/Plugins/BotSharp.Plugin.WeChat/BotSharp.Plugin.WeChat.csproj b/src/Plugins/BotSharp.Plugin.WeChat/BotSharp.Plugin.WeChat.csproj index d1ee4dc0..611146dd 100644 --- a/src/Plugins/BotSharp.Plugin.WeChat/BotSharp.Plugin.WeChat.csproj +++ b/src/Plugins/BotSharp.Plugin.WeChat/BotSharp.Plugin.WeChat.csproj @@ -20,7 +20,12 @@ - + + + + + + diff --git a/src/Plugins/BotSharp.Plugin.WeChat/BotSharpMessageHandler.cs b/src/Plugins/BotSharp.Plugin.WeChat/BotSharpMessageHandler.cs new file mode 100644 index 00000000..d12a6f45 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.WeChat/BotSharpMessageHandler.cs @@ -0,0 +1,53 @@ +using BotSharp.Abstraction.Conversations; +using BotSharp.Abstraction.Infrastructures.ContentTransmitters; +using BotSharp.Abstraction.Models; +using Microsoft.Extensions.DependencyInjection; +using Senparc.NeuChar.App.AppStore; +using Senparc.NeuChar.Entities; +using Senparc.Weixin.MP.Entities; +using Senparc.Weixin.MP.Entities.Request; +using Senparc.Weixin.MP.MessageContexts; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using System.Xml.Linq; + +namespace BotSharp.Plugin.WeChat +{ + public class BotSharpMessageHandler : Senparc.Weixin.MP.MessageHandlers.MessageHandler + { + public static Func GenerateMessageHandler = (stream, postModel, maxRecordCount, serviceProvider) + => new BotSharpMessageHandler(stream, postModel, maxRecordCount, false /* 是否只允许处理加密消息,以提高安全性 */, serviceProvider: serviceProvider); + + public BotSharpMessageHandler(Stream inputStream, PostModel postModel, int maxRecordCount = 0, bool onlyAllowEncryptMessage = false, DeveloperInfo developerInfo = null, IServiceProvider serviceProvider = null) : base(inputStream, postModel, maxRecordCount, onlyAllowEncryptMessage, developerInfo, serviceProvider) + { + } + + public BotSharpMessageHandler(XDocument requestDocument, PostModel postModel, int maxRecordCount = 0, bool onlyAllowEncryptMessage = false, DeveloperInfo developerInfo = null, IServiceProvider serviceProvider = null) : base(requestDocument, postModel, maxRecordCount, onlyAllowEncryptMessage, developerInfo, serviceProvider) + { + } + + public BotSharpMessageHandler(RequestMessageBase requestMessageBase, PostModel postModel, int maxRecordCount = 0, bool onlyAllowEncryptMessage = false, DeveloperInfo developerInfo = null, IServiceProvider serviceProvider = null) : base(requestMessageBase, postModel, maxRecordCount, onlyAllowEncryptMessage, developerInfo, serviceProvider) + { + } + + public override IResponseMessageBase DefaultResponseMessage(IRequestMessageBase requestMessage) + { + return null; + } + + public async override Task OnTextRequestAsync(RequestMessageText requestMessage) + { + var messageQueue = ServiceProvider.GetRequiredService(); + await messageQueue.EnqueueAsync(new WeChatMessage() + { + OpenId = OpenId, + Message = requestMessage.Content, + Type = "text" + }); + return await base.OnTextRequestAsync(requestMessage); + } + } +} diff --git a/src/Plugins/BotSharp.Plugin.WeChat/IMessageQueue.cs b/src/Plugins/BotSharp.Plugin.WeChat/IMessageQueue.cs new file mode 100644 index 00000000..b7d31cf6 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.WeChat/IMessageQueue.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace BotSharp.Plugin.WeChat +{ + public interface IMessageQueue + { + Task EnqueueAsync(WeChatMessage message); + } +} diff --git a/src/Plugins/BotSharp.Plugin.WeChat/WeChatBackgroundService.cs b/src/Plugins/BotSharp.Plugin.WeChat/WeChatBackgroundService.cs new file mode 100644 index 00000000..09aac562 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.WeChat/WeChatBackgroundService.cs @@ -0,0 +1,89 @@ +using BotSharp.Abstraction.Conversations; +using BotSharp.Abstraction.Infrastructures.ContentTransmitters; +using BotSharp.Abstraction.Models; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; + +namespace BotSharp.Plugin.WeChat +{ + public class WeChatBackgroundService : BackgroundService, IMessageQueue + { + private readonly Channel _queue; + private readonly IServiceProvider _service; + private readonly ILogger _logger; + + public WeChatBackgroundService( + IServiceProvider service, + ILogger logger) + { + + this._service = service; + this._logger = logger; + this._queue = Channel.CreateUnbounded(); + } + + private async Task HandleTextMessageAsync(string openid, string message) + { + var scoped = _service.CreateScope().ServiceProvider; + var conversationService = scoped.GetRequiredService(); + var contentTransfer = scoped.GetRequiredService(); + + var conversations = conversationService.GetDialogHistory(openid); + conversations.Add(new RoleDialogModel + { + Role = "User", + Text = message, + }); + + var container = new ContentContainer + { + Conversations = conversations + }; + + var result = await contentTransfer.Transport(container); + + if (result.IsSuccess) + { + var output = container.Output.Text.Trim(); + await ReplyTextMessageAsync(openid, output); + conversationService.AddDialog(new RoleDialogModel() + { + Role = "Assistant", + Text = output, + }); + } + } + + private async Task ReplyTextMessageAsync(string openid, string content) + { + var appId = Senparc.Weixin.Config.SenparcWeixinSetting.WeixinAppId; + await Senparc.Weixin.MP.AdvancedAPIs.CustomApi.SendTextAsync(appId, openid, content); + } + + public async Task EnqueueAsync(WeChatMessage message) + { + await _queue.Writer.WriteAsync(message); + } + + protected override async Task ExecuteAsync(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + try + { + var message = await _queue.Reader.ReadAsync(cancellationToken); + await HandleTextMessageAsync(message.OpenId, message.Message); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error occurred Handle Message"); + } + } + } + } +} diff --git a/src/Plugins/BotSharp.Plugin.WeChat/WeChatMessage.cs b/src/Plugins/BotSharp.Plugin.WeChat/WeChatMessage.cs new file mode 100644 index 00000000..944c2fce --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.WeChat/WeChatMessage.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace BotSharp.Plugin.WeChat +{ + public class WeChatMessage + { + public string OpenId { get; set; } + public string Type { get; set; } + public string Message { get; set; } + } +} diff --git a/src/Plugins/BotSharp.Plugin.WeChat/WeChatPlugin.cs b/src/Plugins/BotSharp.Plugin.WeChat/WeChatPlugin.cs new file mode 100644 index 00000000..38c6592f --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.WeChat/WeChatPlugin.cs @@ -0,0 +1,66 @@ +using BotSharp.Abstraction.Plugins; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using Senparc.CO2NET.AspNet; +using Senparc.CO2NET; +using Senparc.Weixin.RegisterServices; +using System; +using System.Collections.Generic; +using System.Text; +using Senparc.Weixin; +using Senparc.Weixin.MP; +using Senparc.Weixin.MP.MessageHandlers.Middleware; +using Senparc.Weixin.Entities; +using Senparc.CO2NET.RegisterServices; +using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.Http; + +namespace BotSharp.Plugin.WeChat +{ + + public class WeChatPlugin : IBotSharpAppPlugin + { + public void RegisterDI(IServiceCollection services, IConfiguration config) + { + services.AddMemoryCache(); + + services.Configure(config.GetSection("WeChat")); + + if (!Senparc.CO2NET.RegisterServices.RegisterServiceExtension.SenparcGlobalServicesRegistered) + { + services = services.AddSenparcGlobalServices(config); + } + + services.AddSingleton(); + + services.AddHostedService(s => s.GetRequiredService()); + + services.TryAddSingleton(s => s.GetRequiredService()); + } + + public void Configure(IApplicationBuilder app) + { + var env = app.ApplicationServices.GetRequiredService(); + var logger = app.ApplicationServices.GetRequiredService>(); + + var register = app.UseSenparcGlobal(env); + register.UseSenparcWeixin(null, (svc, settings) => + { + svc.RegisterMpAccount(settings, "WeChat"); + }, app.ApplicationServices); + + app.UseMessageHandlerForMp("/WeChatAsync", BotSharpMessageHandler.GenerateMessageHandler, options => + { + options.AccountSettingFunc = context => Senparc.Weixin.Config.SenparcWeixinSetting; + options.EnbleResponseLog = false; + options.EnableRequestLog = false; + }); + + logger.LogInformation("WeChat Message Handler is running on /WeChatAsync."); + + } + } +} diff --git a/src/WebStarter/WebStarter.csproj b/src/WebStarter/WebStarter.csproj index 7991ff98..e2abd70d 100644 --- a/src/WebStarter/WebStarter.csproj +++ b/src/WebStarter/WebStarter.csproj @@ -42,6 +42,7 @@ + diff --git a/src/WebStarter/appsettings.json b/src/WebStarter/appsettings.json index c7e7480d..10509e05 100644 --- a/src/WebStarter/appsettings.json +++ b/src/WebStarter/appsettings.json @@ -60,6 +60,13 @@ "ApiKey": "" }, + "WeChat": { + "Token": "#{Token}#", + "EncodingAESKey": "#{EncodingAESKey}#", + "WeixinAppId": "#{WeixinAppId}#", + "WeixinAppSecret": "#{WeixinAppSecret}#" + }, + "KnowledgeBase": { "VectorDb": "MemVecDbProvider" }, @@ -70,7 +77,8 @@ "BotSharp.Plugin.AzureOpenAI", "BotSharp.Plugin.MetaAI", "BotSharp.Plugin.Qdrant", - "BotSharp.Plugin.PaddleSharp" + "BotSharp.Plugin.PaddleSharp", + "BotSharp.Plugin.WeChat" ], "Plugins": [ "KnowledgeBasePlugin", @@ -79,7 +87,8 @@ "AzureOpenAiPlugin", "MetaAiPlugin", "QdrantPlugin", - "PaddleSharpPlugin" + "PaddleSharpPlugin", + "WeChatPlugin" ] } }