给公众号接入`FastWiki`智能AI知识库,让您的公众号加入智能行列
最近由于公众号用户太多,我就在思考有啥方式能给微信公众号的粉丝提供更多的更好的服务?这个时候我就想是否可以给公众号接入一下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;
}
WeChatService具体实现
/// <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; }
}
}
WeChat提供的API服务
上面是接口的具体实现,然后我们在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
给公众号接入`FastWiki`智能AI知识库,让您的公众号加入智能行列的更多相关文章
- 用java开发微信公众号:公众号接入和access_token管理(二)
本文为原创,原始地址为http://www.cnblogs.com/fengzheng/p/5027630.html 上一篇说了微信开发的准备工作,准备工作完成之后,就要开始步入正题了.其实微信公众号 ...
- 2014-07-23 .NET实现微信公众号接入
今天是在吾索实习的第11天.今天我跟我的实习小组的组员们,解决了关于使用ASP.NET进行微信公众号接入的问题.因为我们小组成员也是刚接触微信公众号的二次开发,所以在解决该问题的工程中也走了不少弯路. ...
- thinkphp5.0 微信公众号接入支付宝支付
---恢复内容开始--- 真是无力吐槽这个需求了,想骂客户,好端端的非要在微信公众号接入支付宝,都知道微信公众号是拒绝支付宝的,屏蔽了支付宝,所以在微信公众号接入支付宝的话就必须手动复制链接跳出微信内 ...
- 使用localtunne一分钟搞定微信公众号接入
记得15年那个刚刚进入工作的时候,公司有个微信公众号的项目,那个时候微信官方没有什么调试工具,也没有什么比较好的本地调试工具.当时有个功能需要调用微信JSSDK里面的扫一扫的功能.由于本地不能调试 ...
- 微信公众号开发C#系列-2、微信公众平台接入指南
概述 微信公众平台消息接口的工作原理大概可以这样理解:从用户端到公众号端一个流程是这样的,用户发送消息到微信服务器,微信服务器将接收到的消息post到用户接入时填写的url中,在url处理程序中,首先 ...
- 微信公众号接入之排序问题小记 Arrays.sort()
微信公众号作为强大的自媒体工具,对接一下是很正常的了.不过这不是本文的方向,本文的方向公众号接入的排序问题. 最近接了一个重构的小项目,需要将原有的php的公众号后台系统,转换为java系统.当然,也 ...
- php 微信公众号接入支付宝支付
真是无力吐槽这个需求了,好端端的非要在微信公众号接入支付宝,都知道微信公众号是拒绝支付宝的,屏蔽了支付宝,所以在微信公众号接入支付宝的话就必须手动复制链接跳出微信内置浏览器,强制性打开web浏览器完成 ...
- 7.智能快递柜(APP及微信公众号)
1.智能快递柜(开篇) 2.智能快递柜(终端篇) 3.智能快递柜(通信篇-HTTP) 4.智能快递柜(通信篇-SOCKET) 5.智能快递柜(通信篇-Server程序) 6.智能快递柜(平台篇) 7. ...
- 第六篇 :微信公众平台开发实战Java版之如何自定义微信公众号菜单
我们来了解一下 自定义菜单创建接口: http请求方式:POST(请使用https协议) https://api.weixin.qq.com/cgi-bin/menu/create?access_to ...
- 【微信开发】微信公众平台接入及绑定提示“请求URL超时”的解决办法
成为微信开发者的第一步--微信公众平台接入 第一步:填写服务器配置 在开发->基本配置处启用服务器配置.修改配置 其中URL是开发者用来接收微信消息和事件的接口URL. Token可由开发者可以 ...
随机推荐
- 在Centos 8 服务器用tmux多开窗口
在 CentOS 服务器上使用 tmux 来多开窗口是一个高效的方式.tmux 是一个终端复用器,它允许你在一个终端窗口中打开多个终端会话,还可以在会话之间轻松切换,非常适合长时间运行程序或多任务操作 ...
- Go 语言数组基础教程 - 数组的声明、初始化和使用方法
数组用于在单个变量中存储相同类型的多个值,而不是为每个值声明单独的变量. 声明数组 在Go中,有两种声明数组的方式: 使用var关键字: 语法 var array_name = [length]dat ...
- Python 集合(Sets)3
Python - 合并集合 在 Python 中,有几种方法可以合并两个或多个集合.您可以使用union()方法,该方法返回一个包含两个集合中所有项的新集合,或使用update()方法,将一个集合中的 ...
- 深度剖析:Dubbo使用Nacos注册中心的坑
2020年笔者在做微服务部件升级时,Dubbo的注册中心从Zookeeper切换到Nacos碰到个问题,最近刷Github又有网友提到类似的问题,就在这篇文章里做个梳理和总结. 1.问题描述 前几年我 ...
- 填报表中也可以添加 html 事件
在实际的项目开发中,填报表的应用十分广泛. 多数情况下,填报表会作为整个项目的一部分配合需求灵活使用,但有时也会受大项目环境的影响,产生一些特别的要求.比如,通常报表单元格的数据类型大多是文本,有时却 ...
- Lattice下载器高速编程器HW-USBN-2B fpga仿真器ispdown烧录器
1.概述 HW-USBN-2B 编程烧录Lattice所有芯片,速度非常快.支持Lattice FPGA芯片在线稳定仿真.烧录.加密,支持Lattice CPLD烧录.支持外部配置FLASH.PROM ...
- 剑指offer53(Java)-在排序数组中查找数字(简单)
题目: 统计一个数字在排序数组中出现的次数. 示例 1: 输入: nums = [5,7,7,8,8,10], target = 8输出: 2示例 2: 输入: nums = [5,7,7,8,8,1 ...
- 云原生事件驱动引擎(RocketMQ-EventBridge)应用场景与技术解析
简介: RocketMQ 给人最大的印象一直是一个消息引擎.那什么是事件驱动引擎?为什么我们这次要推出事件驱动引擎这个产品?他有哪些应用场景,以及对应的技术方案是什么?本文我们就一起来看下. 作者:罗 ...
- 基于Delta lake、Hudi格式的湖仓一体方案
简介: Delta Lake 和 Hudi 是流行的开放格式的存储层,为数据湖同时提供流式和批处理的操作,这允许我们在数据湖上直接运行 BI 等应用,让数据分析师可以即时查询新的实时数据,从而对您的 ...
- Serverless 工程实践 | Serverless 应用开发观念的转变
简介: Serverless 架构带来的除了一种新的架构.一种新的编程范式,还包括思路上的转变,尤其是开发过程中的一些思路转变.有人说要把 Serverless 架构看成一种天然的分布式架构,需要用 ...