赞
踩
最近由于公众号用户太多,我就在思考有啥方式能给微信公众号的粉丝提供更多的更好的服务?这个时候我就想是否可以给公众号接入一下AI?让用户跟微信公众号对话,然后还能回到用户的问题,并且我提供一些资料让AI帮我回复用户的信息?
这个时候刚刚好我们的FastWiki
项目满足了部分需求,然后我们就顺便加入了微信公众号,下面我们也会解析我们如何给公众号实现接入FastWiki
的!
FastWiki
实现接入微信公众号在FastWiki.Service项目中的Service目录创建WeChatService
用于实现微信公众号接入功能,具体代码如下,
由于微信公众号的限制,没有实现微信公众号的微信认证,您的公众号是无法主动向用户发送信息,并且你的接口必须在5s内回复用户的信息,还得是xml格式(非常想吐槽!!!),在其中,我们将用户对话和AI回复使用Channel去分离我们的业务, AI通过读取Channel的对话信息,然后进行提问,并且调用了知识库服务提供的接口,还可以在知识库搜索相关prompt信息,然后得到大模型响应的内容,然后将响应的内容添加到内存缓存中,并且设置过期时间(防止用户提问以后不在继续造成内存溢出),然后当用户发送1
提取AI的回复的时候获取内存的响应内容,然后直接返回给用户,然后删除内存缓存中的数据,这样就避免接口超过5s导致接口响应异常!
if (context.Request.Method != "POST")
{
context.Request.Query.TryGetValue("signature", out var signature);
context.Request.Query.TryGetValue("timestamp", out var timestamp);
context.Request.Query.TryGetValue("nonce", out var nonce);
context.Request.Query.TryGetValue("echostr", out var echostr);
await context.Response.WriteAsync(echostr);
return;
}
/// <summary> /// 微信服务 /// </summary> public class WeChatService { static WeChatService() { Task.Run(AIChatAsync); } private static readonly Channel<WeChatAI> Channel = System.Threading.Channels.Channel.CreateUnbounded<WeChatAI>(); private const string OutputTemplate = """ 您好,欢迎关注FastWiki! 由于微信限制,我们无法立即回复您的消息,但是您的消息已经收到,我们会尽快回复您! 如果获取消息结果,请输入1。 如果您有其他问题,可以直接回复,我们会尽快回复您! """; public static async Task AIChatAsync() { using var scope = MasaApp.RootServiceProvider.CreateScope(); var eventBus = scope.ServiceProvider.GetRequiredService<IEventBus>(); var wikiMemoryService = scope.ServiceProvider.GetRequiredService<WikiMemoryService>(); var memoryCache = scope.ServiceProvider.GetRequiredService<IMemoryCache>(); while (await Channel.Reader.WaitToReadAsync()) { var content = await Channel.Reader.ReadAsync(); await SendMessageAsync(content, eventBus, wikiMemoryService, memoryCache); } } /// <summary> /// 微信AI对话 /// </summary> /// <param name="chatAi"></param> /// <param name="eventBus"></param> /// <param name="wikiMemoryService"></param> /// <param name="memoryCache"></param> public static async Task SendMessageAsync(WeChatAI chatAi, IEventBus eventBus, WikiMemoryService wikiMemoryService, IMemoryCache memoryCache) { var chatShareInfoQuery = new ChatShareInfoQuery(chatAi.SharedId); await eventBus.PublishAsync(chatShareInfoQuery); // 如果chatShareId不存在则返回让下面扣款 var chatShare = chatShareInfoQuery.Result; var chatApplicationQuery = new ChatApplicationInfoQuery(chatShareInfoQuery.Result.ChatApplicationId); await eventBus.PublishAsync(chatApplicationQuery); var chatApplication = chatApplicationQuery?.Result; if (chatApplication == null) { return; } int requestToken = 0; var module = new ChatCompletionDto<ChatCompletionRequestMessage>() { messages = [ new() { content = chatAi.Content, role = "user", } ] }; var chatHistory = new ChatHistory(); // 如果设置了Prompt,则添加 if (!chatApplication.Prompt.IsNullOrEmpty()) { chatHistory.AddSystemMessage(chatApplication.Prompt); } // 保存对话提问 var createChatRecordCommand = new CreateChatRecordCommand(chatApplication.Id, chatAi.Content); await eventBus.PublishAsync(createChatRecordCommand); var sourceFile = new List<FileStorage>(); var memoryServerless = wikiMemoryService.CreateMemoryServerless(chatApplication.ChatModel); // 如果为空则不使用知识库 if (chatApplication.WikiIds.Count != 0) { var success = await OpenAIService.WikiPrompt(chatApplication, memoryServerless, chatAi.Content, eventBus, sourceFile, module); if (!success) { return; } } var output = new StringBuilder(); // 添加用户输入,并且计算请求token数量 module.messages.ForEach(x => { if (x.content.IsNullOrEmpty()) return; requestToken += TokenHelper.ComputeToken(x.content); chatHistory.Add(new ChatMessageContent(new AuthorRole(x.role), x.content)); }); if (chatShare != null) { // 如果token不足则返回,使用token和当前request总和大于可用token,则返回 if (chatShare.AvailableToken != -1 && (chatShare.UsedToken + requestToken) >= chatShare.AvailableToken) { output.Append("Token不足"); return; } // 如果没有过期则继续 if (chatShare.Expires != null && chatShare.Expires < DateTimeOffset.Now) { output.Append("Token已过期"); return; } } try { await foreach (var item in OpenAIService.SendChatMessageAsync(chatApplication, eventBus, wikiMemoryService, chatHistory)) { if (string.IsNullOrEmpty(item)) { continue; } output.Append(item); } //对于对话扣款 if (chatShare != null) { var updateChatShareCommand = new DeductTokenCommand(chatShare.Id, requestToken); await eventBus.PublishAsync(updateChatShareCommand); } } catch (NotModelException notModelException) { output.Clear(); output.Append(notModelException.Message); } catch (InvalidOperationException invalidOperationException) { output.Clear(); output.Append("对话异常:" + invalidOperationException.Message); } catch (ArgumentException argumentException) { output.Clear(); output.Append("对话异常:" + argumentException.Message); } catch (Exception e) { output.Clear(); output.Append("对话异常,请联系管理员"); } finally { memoryCache.Set(chatAi.MessageId, output.ToString(), TimeSpan.FromMinutes(5)); } } /// <summary> /// 接收消息 /// </summary> /// <param name="context"></param> public static async Task ReceiveMessageAsync(HttpContext context, string? id, IMemoryCache memoryCache) { if (context.Request.Method != "POST") { context.Request.Query.TryGetValue("signature", out var signature); context.Request.Query.TryGetValue("timestamp", out var timestamp); context.Request.Query.TryGetValue("nonce", out var nonce); context.Request.Query.TryGetValue("echostr", out var echostr); await context.Response.WriteAsync(echostr); return; } using var reader = new StreamReader(context.Request.Body); // xml解析 var body = await reader.ReadToEndAsync(); var doc = new XmlDocument(); doc.LoadXml(body); var root = doc.DocumentElement; var input = new WeChatMessageInput { ToUserName = root.SelectSingleNode("ToUserName")?.InnerText, FromUserName = root.SelectSingleNode("FromUserName")?.InnerText, CreateTime = long.Parse(root.SelectSingleNode("CreateTime")?.InnerText ?? "0"), MsgType = root.SelectSingleNode("MsgType")?.InnerText, Content = root.SelectSingleNode("Content")?.InnerText, MsgId = long.Parse(root.SelectSingleNode("MsgId")?.InnerText ?? "0") }; var output = new WehCahtMe { ToUserName = input.ToUserName, FromUserName = input.FromUserName, CreateTime = input.CreateTime, MsgType = input.MsgType, Content = input.Content }; if (output.Content.IsNullOrEmpty()) { return; } if (id == null) { context.Response.ContentType = "application/xml"; await context.Response.WriteAsync(GetOutputXml(output, "参数错误,请联系管理员!code:id_null")); return; } var messageId = GetMessageId(output); // 从缓存中获取,如果有则返回 memoryCache.TryGetValue(messageId, out var value); // 如果value有值则,但是value为空,则返回提示,防止重复提问! if (value is string str && str.IsNullOrEmpty()) { context.Response.ContentType = "application/xml"; await context.Response.WriteAsync(GetOutputXml(output, "暂无消息,请稍后再试!code:no_message")); return; } else if (value is string v && !v.IsNullOrEmpty()) { context.Response.ContentType = "application/xml"; await context.Response.WriteAsync(GetOutputXml(output, v)); return; } if (output.Content == "1") { if (value is string v && !v.IsNullOrEmpty()) { memoryCache.Remove(messageId); context.Response.ContentType = "application/xml"; await context.Response.WriteAsync(GetOutputXml(output, v)); return; } context.Response.ContentType = "application/xml"; await context.Response.WriteAsync(GetOutputXml(output, "暂无消息,请稍后再试!code:no_message")); return; } // 先写入channel,等待后续处理 Channel.Writer.TryWrite(new WeChatAI() { Content = output.Content, SharedId = id, MessageId = messageId }); // 等待4s await Task.Delay(4500); // 尝试从缓存中获取 memoryCache.TryGetValue(messageId, out var outputTemplate); if (outputTemplate is string outValue && !outValue.IsNullOrEmpty()) { context.Response.ContentType = "application/xml"; await context.Response.WriteAsync(GetOutputXml(output, outValue)); return; } context.Response.ContentType = "application/xml"; await context.Response.WriteAsync(GetOutputXml(output, OutputTemplate)); // 写入缓存,5分钟过期 memoryCache.Set(messageId, OutputTemplate, TimeSpan.FromMinutes(5)); } private static string GetMessageId(WehCahtMe output) { return output.FromUserName + output.ToUserName; } /// <summary> /// 获取返回的xml /// </summary> /// <param name="output"></param> /// <param name="content"></param> /// <returns></returns> public static string GetOutputXml(WehCahtMe output, string content) { var createTime = DateTimeOffset.Now.ToUnixTimeSeconds(); var xml = $@" <xml> <ToUserName><![CDATA[{output.FromUserName}]]></ToUserName> <FromUserName><![CDATA[{output.ToUserName}]]></FromUserName> <CreateTime>{createTime}</CreateTime> <MsgType><![CDATA[text]]></MsgType> <Content><![CDATA[{content}]]></Content> </xml> "; return xml; } public class WeChatMessageInput { public string URL { get; set; } public string ToUserName { get; set; } public string FromUserName { get; set; } public long CreateTime { get; set; } public string MsgType { get; set; } public string Content { get; set; } public long MsgId { get; set; } } public class WehCahtMe { public string ToUserName { get; set; } public string FromUserName { get; set; } public long CreateTime { get; set; } public string MsgType { get; set; } public string Content { get; set; } } }
上面是接口的具体实现,然后我们在Program
中将我们的WeChatService
对外提供API(Get是用于提供给微信公众号验证),{id}
则绑定我们的接口的string id
参数,以便动态设置。
app.MapGet("/api/v1/WeChatService/ReceiveMessage/{id}", WeChatService.ReceiveMessageAsync)
.WithTags("WeChat")
.WithGroupName("WeChat")
.WithDescription("微信消息验证")
.WithOpenApi();
app.MapPost("/api/v1/WeChatService/ReceiveMessage/{id}", WeChatService.ReceiveMessageAsync)
.WithTags("WeChat")
.WithGroupName("WeChat")
.WithDescription("微信消息接收")
.WithOpenApi();
目前我们的FastWiki部署了免费体验的示例网站,也可以用于测试自己公众号的接入(但是不保证稳定性!)
体验地址:FastWki
进入地址以后创建账号然后登录:然后点击应用->创建一个应用
然后进入应用
然后点击发布应用
发布完成以后选择复制微信公众号对接地址
然后打开我们的微信公众号,然后找到基本配置,
然后点击修改配置:
然后将我们刚刚复制的地址放到这个URL中,然后保存,保存的时候会校验URL地址。
记得保存以后需要启动配置才能生效!然后就可以去微信公众号对话了!
Github开源地址:https://github.com/AIDotNet/fast-wiki
技术交流群加微信:wk28u9123456789
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。