給公眾號接入`FastWiki`智慧AI知識庫,讓您的公眾號加入智慧行列

tokengo發表於2024-05-17

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

相關文章