峥嵘南方 發表於 2023-3-23 08:36:00

使用C#开发微信公众号对接ChatGPT和DALL-E

<p>本人是一家小公司的技术总监,工作包括写市场分析、工作汇报、产品推广文案及代码开发等。在ChatGPT推出之后本人一直在工作中使用,在头脑风暴、大纲生成、语句优化、代码生成方面很有效果。但ChatGPT在一些常识性生成方面并不理想,比如某个省有哪些旅游景点、三角数学公式推算等,大家使用中一定要注意并仔细甄别。ChatGPT的一些使用场景可以参考用 ChatGPT 来了解 ChatGPT。<br>
由于我之前一直是在电脑浏览器中使用,且经常碰见超时需要重新进入的情况,遂产生想法把ChatGPT和DALL-E(OpenAI开发的图片生成模型)集成到本人的公众号上,使用更加方便和稳定。二话不说,先上效果图和代码。<br>
<img src="https://img2023.cnblogs.com/blog/1371469/202303/1371469-20230322224608131-989645732.png"></p>
<p><strong>代码</strong><br>
开发中主要使用了两个库:<br>
OpenAI-DotNet:一个简单的C# .NET客户端库,用于通过RESTful API使用chat-gpt、GPT-4、GPT-3.5-Turbo和Dall-E。这是一个独立开发的库,不是官方库,需要OpenAI API帐户。<br>
Senparc.Weixin —— 微信 .NET SDK:使用 Senparc.Weixin,您可以方便快速地开发微信全平台的应用(包括微信公众号、小程序、小游戏、企业号、开放平台、微信支付、JS-SDK、微信硬件/蓝牙,等等。</p>
<ol>
<li>新建一个空的.net core WebAPI项目并引入Nuget包如下:</li>
</ol>
<pre><code>Install-Package OpenAI-DotNet

Install-Package Senparc.Weixin.AspNet
Install-Package Senparc.Weixin.MP.Middleware
</code></pre>
<ol start="2">
<li>在Program.cs中引入微信配置代码</li>
</ol>
<pre><code>public static void Main(string[] args)
{
    ...
   
    builder.Services.AddMemoryCache();//使用本地缓存必须添加

    //Senparc.Weixin 微信注册
    builder.Services.AddSenparcWeixinServices(builder.Configuration);

    ...
   
    //启用微信配置
    var registerService = app.UseSenparcWeixin(app.Environment, null, null,
      register =&gt; { /* CO2NET 全局配置 */ },
      (register, weixinSetting) =&gt;
      {
            register.RegisterMpAccount(weixinSetting);
      });

    //启用微信消息处理中间件
    app.UseMessageHandlerForMp("/WeixinAsync", CustomMessageHandler.GenerateMessageHandler, options =&gt;
    {
      options.AccountSettingFunc = context =&gt; Senparc.Weixin.Config.SenparcWeixinSetting;
    });
   
    //Nginx部署使用
    app.UseForwardedHeaders(new ForwardedHeadersOptions
    {
      ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
    });
   
      ...

}
</code></pre>
<ol start="3">
<li>新建一个类CustomMessageHandler用于处理微信消息</li>
</ol>
<pre><code>/// &lt;summary&gt;
/// 自定义消息处理器
/// &lt;/summary&gt;
public class CustomMessageHandler : MessageHandler&lt;DefaultMpMessageContext&gt;
{
    private readonly ILogger _logger;
    private readonly string _openAPI_Key;
    private string appId = Config.SenparcWeixinSetting.MpSetting.WeixinAppId;

    /// &lt;summary&gt;
    /// 为中间件提供生成当前类的委托
    /// &lt;/summary&gt;
    public static Func&lt;Stream, PostModel, int, IServiceProvider, CustomMessageHandler&gt; GenerateMessageHandler = (stream, postModel, maxRecordCount, serviceProvider)
                     =&gt; new CustomMessageHandler(stream, postModel, maxRecordCount, serviceProvider: serviceProvider);

    public CustomMessageHandler(Stream inputStream, PostModel postModel, int maxRecordCount, IServiceProvider serviceProvider)
      : base(inputStream, postModel, maxRecordCount, false, null, serviceProvider)
    {
      GlobalMessageContext.ExpireMinutes = 3;
      GlobalMessageContext.MaxRecordCount = 3;
      OmitRepeatedMessage = true; //启用消息去重功能
      _logger = serviceProvider.GetService&lt;ILogger&lt;CustomMessageHandler&gt;&gt;();
      _openAPI_Key = serviceProvider.GetService&lt;IConfiguration&gt;()["OpenAIKey"];
    }

    /// &lt;summary&gt;
    /// 回复以文字形式发送的信息
    /// &lt;/summary&gt;
    public override async Task&lt;IResponseMessageBase&gt; OnTextRequestAsync(RequestMessageText requestMessage)
    {
      #region 由于微信回复时长5秒限制,这里采用异步推送客服信息,后面直接返回空消息。

      _ = Task.Factory.StartNew(async () =&gt;
      {
            OpenAIClient api = new OpenAIClient(_openAPI_Key);

            if (requestMessage.Content.Contains("图片", StringComparison.OrdinalIgnoreCase)
                || requestMessage.Content.Contains("图像", StringComparison.OrdinalIgnoreCase)
                || requestMessage.Content.Contains("照片", StringComparison.OrdinalIgnoreCase)
                || requestMessage.Content.Contains("Image", StringComparison.OrdinalIgnoreCase)
                || requestMessage.Content.Contains("Photo", StringComparison.OrdinalIgnoreCase)) //DALL-E 模型
            {
                var imageRequest = requestMessage.Content.Replace("图片", "").Replace("图像", "").Replace("照片", "").Replace("Image", "").Replace("Photo", "").Trim();
                var results = await api.ImagesEndPoint.GenerateImageAsync(imageRequest, 1, ImageSize.Small); //调用DALL-E模型接口生成图片
                if (results.Count &gt; 0)
                {
                  var imageRemoteUrl = results;

                  #region 临时保存图片到本地
                  var imageLocalUrl = ServerUtility.ContentRootMapPath($"~/App_Data/TempImages/{Guid.NewGuid()}.png");
                  var client = new HttpClient();
                  client.Timeout = TimeSpan.FromSeconds(30);
                  byte[] bytes = await client.GetByteArrayAsync(imageRemoteUrl);
                  if (bytes.Length&gt;0)
                  {
                        FileStream fs = new FileStream(imageLocalUrl, FileMode.Create);
                        BinaryWriter w = new BinaryWriter(fs);

                        try
                        {
                            w.Write(bytes);
                        }
                        finally
                        {
                            fs.Close();
                            w.Close();
                        }
                  }
                  #endregion

                  var uploadResult = MediaApi.UploadTemporaryMedia(appId, UploadMediaFileType.image, imageLocalUrl);
                  await CustomApi.SendImageAsync(appId, OpenId, uploadResult.media_id);
                }
            }
            else //GPT3_5_Turbo 模型
            {
                var currentMessageContext = await base.GetCurrentMessageContext(); //使用StorageData保存对话上下文
                if (currentMessageContext.StorageData == null || !(currentMessageContext.StorageData is List&lt;ChatPrompt&gt;))
                {
                  currentMessageContext.StorageData = new List&lt;ChatPrompt&gt;();
                }
                var chatPrompts = (List&lt;ChatPrompt&gt;)currentMessageContext.StorageData;

                chatPrompts.Add(new ChatPrompt("user", requestMessage.Content));

                var chatRequest = new ChatRequest(chatPrompts, model: Model.GPT3_5_Turbo);
                var result = await api.ChatEndpoint.GetCompletionAsync(chatRequest); //调用GPT3_5_Turbo模型接口生成对话
                var firstChoice = result.FirstChoice.ToString().Trim();
                await CustomApi.SendTextAsync(appId, OpenId, firstChoice);

                chatPrompts.Add(new ChatPrompt("assistant", firstChoice));
                currentMessageContext.StorageData = chatPrompts;
                GlobalMessageContext.UpdateMessageContext(currentMessageContext);//储存到缓存
            }

      }).ContinueWith(async task =&gt;
      {
            if (task.Exception != null)
            {
                await CustomApi.SendTextAsync(appId, OpenId, $"生成失败,请稍后再试。");
                _logger.LogError($"生成失败,错误信息:{task.Exception.InnerException}。");
            }
      });

      return new SuccessResponseMessage();

      #endregion
    }

    /// &lt;summary&gt;
    /// 默认消息
    /// &lt;/summary&gt;
    public override IResponseMessageBase DefaultResponseMessage(IRequestMessageBase requestMessage)
    {
      var responseMessage = base.CreateResponseMessage&lt;ResponseMessageText&gt;();
      responseMessage.Content = $"欢迎来到车骑数说,该公众号已集成【ChatGPT 对话生成】和【DALL-E 图片生成】功能:\r\n1、如若使用ChatGPT 对话生成功能请直接输入想问的问题。\r\n1、如若使用DALL-E 图片生成请在描述文字后以“图片”结尾,比如“一匹马在月球上图片”。";
      return responseMessage;
    }
}
</code></pre>
<ol start="4">
<li>在配置文件appsettings.json里面添加对应配置<br>
<img src="https://img2023.cnblogs.com/blog/1371469/202303/1371469-20230322224735960-352958594.png"></li>
<li>运行代码,在url后加上/WeixinAsync后缀,如果出现以下页面即代表开发成功。<br>
<img src="https://img2023.cnblogs.com/blog/1371469/202303/1371469-20230322224740368-1163760499.png"></li>
</ol>
<p><strong>OpenAI接口账号</strong>:<br>
注册完OpenAI账号后(具体注册过程请自行百度),打开https://platform.openai.com/,在用户管理里面的View API Keys里面创建一个New Secret Key,并保存下来用于API调用。每个用户有18美元的免费额度用于调用。具体使用额度可以在Usage页面查看。<br>
<img src="https://img2023.cnblogs.com/blog/1371469/202303/1371469-20230322224820605-821448212.png"></p>
<p><strong>后端部署</strong><br>
由于OpenAI及接口不允许中国地区访问,所以在部署代码的时候有两种方案:</p>
<ul>
<li>国内服务器+安装国外代理</li>
<li>国外服务器</li>
</ul>
<p>同时对接微信公众号一定是需要域名的,目前域名和国外服务器绑定不需要备案,而和国内服务器绑定需要备案,如果考量备案时间关系建议使用国外服务器,而且部分国外服务器并不贵,这里推荐RackNerd,RackNerd是一家美国VPS服务商,其公司于2015年注册,可选机房有圣何塞、芝加哥、达拉斯、亚特兰大、纽约、阿什本等10个机房。一直以价格低廉、守候到位、服务器稳定而火爆市场。付款方式支持支付宝。<br>
促销款便宜套餐入口:套餐1 套餐2 套餐32023年最新优惠码:15OFFDEDI官方网站:racknerd<br>
本人购买的是套餐2,一年大概170元。</p>
<p>我是部署在Ubuntu 20.4的操作系统上,主要有三步:</p>
<ol>
<li>安装运行时 aspnetcore-runtime</li>
</ol>
<p>添加 Microsoft 包存储库</p>
<pre><code>wget https://packages.microsoft.com/config/ubuntu/20.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb
sudo dpkg -i packages-microsoft-prod.deb
rm packages-microsoft-prod.deb
</code></pre>
<p>安装运行时</p>
<pre><code>sudo apt-get update &amp;&amp; \
sudo apt-get install -y aspnetcore-runtime-6.0
</code></pre>
<ol start="2">
<li>Nginx 安装配置</li>
</ol>
<p>由于请求通过反向代理转发,因此使用 Microsoft.AspNetCore.HttpOverrides 包中的转接头中间件。 此中间件使用 X-Forwarded-Proto 标头来更新<br>
Request.Scheme,使重定向 URI 和其他安全策略能够正常工作。</p>
<pre><code>using Microsoft.AspNetCore.HttpOverrides;

...

app.UseForwardedHeaders(new ForwardedHeadersOptions
{
    ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});

app.UseAuthentication();
</code></pre>
<p>安装 Nginx<br>
<code>sudo apt-get install nginx</code><br>
首次安装 Nginx,通过运行以下命令显式启动后确认浏览器显示 Nginx 的默认登陆页。<br>
<code>sudo service nginx start</code><br>
配置 Nginx<br>
修改 /etc/nginx/sites-available/default。 在文本编辑器中打开它,并将内容替换为以下代码片段,Nginx 将匹配的请求转发到 http://127.0.0.1:5000 中的 Kestrel上运行的 ASP.NET Core 应用。</p>
<pre><code>server {
    listen      80;
    #server_name   example.com *.example.com;
    location / {
      proxy_pass         http://127.0.0.1:5000;
      proxy_http_version 1.1;
      proxy_set_header   Upgrade $http_upgrade;
      proxy_set_header   Connection keep-alive;
      proxy_set_header   Host $host;
      proxy_cache_bypass $http_upgrade;
      proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header   X-Forwarded-Proto $scheme;
    }
}
</code></pre>
<p>完成配置 Nginx 后,运行 sudo nginx -t 来验证配置文件的语法。 如果配置文件测试成功,可以通过运行 sudo nginx -s reload 强制 Nginx 选取更改。</p>
<ol start="3">
<li>设置服务自启动</li>
</ol>
<p>systemd 可用于创建服务文件以启动和监视基础 Web 应用。 systemd 是一个初始系统,可以提供启动、停止和管理进程的许多强大的功能。<br>
创建服务定义文件:<br>
<code>sudo nano /etc/systemd/system/wx-ai.service</code><br>
并填充以下内容</p>
<pre><code>
Description=wx-ai service


WorkingDirectory=/var/www/wx-ai
ExecStart=/usr/bin/dotnet /var/www/wx-ai/OpenAI.Examples.WX.dll
Restart=always
# Restart service after 90 seconds if the dotnet service crashes:
RestartSec=90
KillSignal=SIGINT
SyslogIdentifier=wx-ai
User=www-data
Environment=ASPNETCORE_ENVIRONMENT=Production
Environment=DOTNET_PRINT_TELEMETRY_MESSAGE=false


WantedBy=multi-user.target
</code></pre>
<p>systemd常用命令如下:</p>
<pre><code>sudo systemctl enable wx-ai.service#开机启动
sudo systemctl start wx-ai.service#启动服务
sudo systemctl stop wx-ai.service #停止服务
sudo systemctl status wx-ai.service #查看服务状态
sudo systemctl restart wx-ai.service#重新启动服务
</code></pre>
<p>查看日志命令如下:<br>
<code>sudo journalctl -fu wx-ai.service --since "2016-10-18" --until "2016-10-18 04:00"</code></p>
<p>至此部署完成,最后需要申请域名绑定服务器IP。<br>
<img src="https://img2023.cnblogs.com/blog/1371469/202303/1371469-20230322225739684-1736114665.png"></p>
<p><strong>微信集成</strong><br>
这里以微信测试公众号为例子,打开https://mp.weixin.qq.com/debug/cgi-bin/sandboxinfo?action=showinfo&amp;t=sandbox/index,找到appID和appsecret放在ASP.NET Core 应用的配置文件里,把ASP.NET Core应用部署的地址+/WeixinAsync填入接口配置信息,并填写自定义Token,保证Token在微信配置和ASP.NET Core 应用的配置文件里一直。接下来就可以正式测试了。<br>
<img src="https://img2023.cnblogs.com/blog/1371469/202303/1371469-20230322225746045-237828983.png"><br>
正式公众号配置类似,但这里有个比较尴尬的地方,ChatGPT和DALL-E和接口数据返回时间一般比较长,大于微信公众号普通回复接口5秒的时长限制,所以最好使用客服接口进行结果回复。而目前客服接口只适用于通过微信认知的企业公众号而非个人公众号。所以本人的个人公众号【车骑数说】无法接入,目前自己也只能在个人测试号上使用。不过我也正在开发小程序版本,后续开发完成后会及时更新。</p><br><br>
来源:https://www.cnblogs.com/royzshare/p/17246149.html
頁: [1]
查看完整版本: 使用C#开发微信公众号对接ChatGPT和DALL-E