给公众号接入`FastWiki`智能AI知识库,让您的公众号加入智能行列

fastwiki,ai · 浏览次数 : 0

小编点评

**代码功能:** 该代码是一个微信公众号接入的API,它接收微信公众号发送的聊天信息并将其写入一个XML文件中。 **步骤:** 1. **获取请求参数:** - 从请求中获取 `FromUserName`、`ToUserName`、`CreateTime`、`MsgType` 和 `Content` 等参数。 - 检查 `Content` 是否为空。 2. **从缓存中获取 XML 模板:** - 检查 `messageId` 是否存在于缓存中。 - 如果存在,检查 `outputTemplate` 是否为空。 - 如果 `outputTemplate` 存在,返回其值。 3. **写入 XML 模板:** - 创建 XML 模板,包含聊天信息。 - 设置 `CreateTime`、`FromUserName`、`MsgType` 和 `Content` 的值。 4. **写入缓存:** - 将 XML 模板缓存到 `memoryCache` 中,有效期 5 分钟。 5. **返回 XML 字符串:** - 如果缓存中存在 XML 模板,返回其。 - 如果缓存中没有模板,返回错误。 **主要函数:** - `GetMessageId`:获取微信公众号发送的聊天信息中的 `ToUserName` 和 `FromUserName`。 - `getOutputXml`:将微信聊天信息构建成 XML 字符串。 - `ReceiveMessageAsync`:接收微信公众号发送的聊天信息并写入 XML 文件。 **使用方法:** 1. 在应用程序中添加 `FastWiki` 示例网站。 2. 在示例网站中创建账号并登录。 3. 在应用中添加微信公众号对接地址。 4. 在应用配置中设置微信公众号的聊天机器人 ID。 5. 保存配置并启动应用。 **注意:** - 该代码仅用于示例,可能需要根据实际情况进行修改。 - 缓存的有效期可根据需求调整。

正文

最近由于公众号用户太多,我就在思考有啥方式能给微信公众号的粉丝提供更多的更好的服务?这个时候我就想是否可以给公众号接入一下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知识库,让您的公众号加入智能行列相似的内容:

给公众号接入`FastWiki`智能AI知识库,让您的公众号加入智能行列

最近由于公众号用户太多,我就在思考有啥方式能给微信公众号的粉丝提供更多的更好的服务?这个时候我就想是否可以给公众号接入一下AI?让用户跟微信公众号对话,然后还能回到用户的问题,并且我提供一些资料让AI帮我回复用户的信息? 这个时候刚刚好我们的FastWiki项目满足了部分需求,然后我们就顺便加入了微

[转帖] jq实现json文本对比

原创:打码日记(微信公众号ID:codelogs),欢迎分享,转载请保留出处。 简介# 近期,为了给一个核心系统减负,组内决定将一些调用量大的查询接口迁移到另一个系统,由于接口逻辑比较复杂,为了保证接口逻辑一致,我们决定将一周内的请求参数在两个接口重放,并用脚本校验两边接口的响应结果。接口返回数据是

[转帖]程序员版本的八荣八耻,爱了

https://juejin.cn/post/7105604721028628511 前言 大家好,我是捡田螺的小男孩。 最近整理了一个关于程序员版本的八荣八耻,还挺有意思的。给大家娱乐一下,哈哈~ 公众号:捡田螺的小男孩 我的github地址,感谢给个star 1. 以接口兼容为荣,以接口裸奔为耻

Excel快速调整单元格行高和列宽

之前使用的是鼠标双击的方法,但是只适用于少量调整时。 今天给同事编辑公众号文章,有一大篇表格在word中,直接从word中复制到公众号的话,格式会有一定程度的错位。 于是先粘贴到excel中处理,但到excel中行高和列宽默认都很不合适。 有一个小技巧,Ctrl + A 全选表格以后, 格式 ->

DevOps|AGI : 智能时代研发效能平台新引擎(上)

AGI 的出现,给了我们一个新视角去审视我们做过的系统,尤其是研发效能平台。研发效能平台作为一个工具平台,本质就是提高公司整体产研的效率。AGI 的快速进步大家已经有目共睹,本文就是在项目协同,代码管理、测试、AIOps等方面来探讨 AGI 可以给研发效能平台带来的巨大变化效率提升。拥抱 AGI,吸

阿里云香港节点全面故障给我们的启示

2022年12月18日上午,阿里云发布《阿里云香港可用区C某机房设备异常》公告。“阿里云监控发现香港地域某机房设备异常,影响香港地域可用区C的云服务器ECS、云数据库PolarDB等云产品使用,阿里云工程师已在紧急处理中。” 在这个寒冷的冬天,一个炸雷给业界惊起了一个大波浪。很多人不解的是说好的高可

DevOps|研发效能解决的是企业效率问题

研发效能并不能解决企业效益问题 它不是利润中心,不能给你带来直接收入(研发效能相关工具厂商做咨询、出方案、卖工具除外)。想要解决企业效益问题,依赖于企业战略、业务/产品、组织、运营、创新等其他方面。 研发效能解决的是企业效率问题 研发效能解决的是企业内部「产研运协作效率」的问题。 企业最需要两种涉及

DevOps | 企业内源(内部开源)适合什么样的公司

框架类是否适合企业内源? 框架类都由公司早期来的一些大佬们负责(相当于技术委员会),更新频率非常低。给框架类提MR的人,多数本身就在技术委员会。 如果公司的人员众多,类似BAT级别,几万人使用的框架,大家一起添砖加瓦也许是合适的,尤其适合那些公司本来已经开源到开源社区的框架。但做之前肯定有大量的学习

【原创】基于Scrum框架产研团队运作20问

学习完了 Scrum,实际使用中,是否遇到/思考过下面的问题? Product Owner的老板是谁、谁来给 Product Owner打绩效、考核的标准是啥? Scrum Master 的老板是谁、谁来给Scrum Master打绩效、考核的标准是啥? Scrum Master 是教练,团队成员表

wxdown 公众号离线文章保存(GO语言开发)

简介 本来一开始用 nodejs 写的,考虑大小、易操作、高性能、跨平台以及环境等问题,我就想能不能搞个不需依赖开发语言环境就能运行的。所以我就选择 go并且它本身就具备以上优点。作者本身是java开发,第一次使用 go所以过程也是比较艰难,好在 GPT 在学习一门新的开发语言方面还是相当给力!