最近由於公眾號使用者太多,我就在思考有啥方式能給微信公眾號的粉絲提供更多的更好的服務?這個時候我就想是否可以給公眾號接入一下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