使用 .NET MAUI 开发 ChatGPT 客户端

时间:2022-12-21 08:54:08

最近 chatgpt 很火,由于网页版本限制了 ip,还得必须开代理,用起来比较麻烦,所以我尝试用 maui 开发一个聊天小应用,结合 chatgpt 的开放 api 来实现(很多客户端使用网页版本接口用 cookie 的方式,有很多限制(如下图)总归不是很正规)。

使用 .NET MAUI 开发 ChatGPT 客户端

效果如下

使用 .NET MAUI 开发 ChatGPT 客户端

mac 端由于需要升级 macos13 才能开发调试,这部分我还没有完成,不过 maui 的控件是跨平台的,放在后续我升级系统再说。

开发实战

我是设想开发一个类似 jetbrains 的 ToolBox 应用一样,启动程序在桌面右下角出现托盘图标,点击图标弹出应用(风格在 windows mac 平台保持一致)

需要实现的功能一览

  • 托盘图标(右键点击有 menu)
  • webview(js 和 csharp 互相调用)
  • 聊天 SPA 页面(react 开发,build 后让 webview 展示)

新建一个 maui 工程(vs2022)

使用 .NET MAUI 开发 ChatGPT 客户端

坑一:默认编译出来的 exe 是直接双击打不开的

使用 .NET MAUI 开发 ChatGPT 客户端

工程文件加上这个配置

<WindowsPackageType>None</WindowsPackageType>
<WindowsAppSDKSelfContained Condition="'$(IsUnpackaged)' == 'true'">true</WindowsAppSDKSelfContained>
<SelfContained Condition="'$(IsUnpackaged)' == 'true'">true</SelfContained>

以上修改后,编译出来的 exe 双击就可以打开了

托盘图标(右键点击有 menu)

启动时设置窗口不能改变大小,隐藏 titlebar, 让 Webview 控件占满整个窗口

使用 .NET MAUI 开发 ChatGPT 客户端

这里要根据平台不同实现不同了,windows 平台采用 winAPI 调用,具体看工程代码吧!

WebView

在 MainPage.xaml 添加控件

使用 .NET MAUI 开发 ChatGPT 客户端

对应的静态 html 等文件放在工程的 Resource\Raw 文件夹下 (整个文件夹里面默认是作为内嵌资源打包的,工程文件里面的如下配置起的作用)

<!-- Raw Assets (also remove the "Resources\Raw" prefix) -->
<MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
使用 .NET MAUI 开发 ChatGPT 客户端

【重点】js 和 csharp 互相调用

这部分我找了很多资料,最终参考了这个 demo,然后改进了下。

主要原理是:

  • js 调用 csharp 方法前先把数据存储在 localstorage 里
  • 然后 windows.location 切换特定的 url 发起调用,返回一个 promise,等待 csharp 的事件
  • csharp 端监听 webview 的 Navigating 事件,异步进行下面处理
  • 根据 url 解析出来 localstorage 的 key
  • 然后 csharp 端调用 excutescript 根据 key 拿到 localstorage 的 value
  • 进行逻辑处理后返回通过事件分发到 js 端

js 的调用封装如下:

// 调用csharp的方法封装
export default class CsharpMethod {
  constructor(command, data) {
    this.RequestPrefix = "request_csharp_";
    this.ResponsePrefix = "response_csharp_";
    // 唯一
    this.dataId = this.RequestPrefix + new Date().getTime();
    // 调用csharp的命令
    this.command = command;
    // 参数
    this.data = { command: command, data: !data ? '' : JSON.stringify(data), key: this.dataId }
  }

  // 调用csharp 返回promise
  call() {
    // 把data存储到localstorage中 目的是让csharp端获取参数
    localStorage.setItem(this.dataId, this.utf8_to_b64(JSON.stringify(this.data)));
    let eventKey = this.dataId.replace(this.RequestPrefix, this.ResponsePrefix);
    let that = this;
    const promise = new Promise(function (resolve, reject) {
      const eventHandler = function (e) {
        window.removeEventListener(eventKey, eventHandler);
        let resp = e.newValue;
        if (resp) {
          // 从base64转换
          let realData = that.b64_to_utf8(resp);
          if (realData.startsWith('err:')) {
            reject(realData.substr(4));
          } else {
            resolve(realData);
          }
        } else {
          reject("unknown error :" + eventKey);
        }
      };
      // 注册监听回调(csharp端处理完发起的)
      window.addEventListener(eventKey, eventHandler);
    });
    // 改变location 发送给csharp端
    window.location = "/api/" + this.dataId;
    return promise;
  }

  // 转成base64 解决中文乱码
  utf8_to_b64(str) {
    return window.btoa(unescape(encodeURIComponent(str)));
  }
  // 从base64转过来 解决中文乱码
  b64_to_utf8(str) {
    return decodeURIComponent(escape(window.atob(str)));
  }

}

前端的使用方式

import CsharpMethod from '../../services/api'

// 发起调用csharp的chat事件函数
const method = new CsharpMethod("chat", {msg: message});
method.call() // call返回promise
.then(data =>{
  // 拿到csharp端的返回后展示
  onMessageHandler({
    message: data,
    username: 'Robot',
    type: 'chat_message'
  });
}).catch(err =>  {
    alert(err);
});

csharp 端的处理:

使用 .NET MAUI 开发 ChatGPT 客户端

这么封装后,js 和 csharp 的互相调用就很方便了。

chatgpt 的开放 api 调用

注册好 chatgpt 后可以申请一个 APIKEY。

使用 .NET MAUI 开发 ChatGPT 客户端

API 封装:

  public static async Task<CompletionsResponse> GetResponseDataAsync(string prompt)
        {
            // Set up the API URL and API key
            string apiUrl = "https://api.openai.com/v1/completions";

            // Get the request body JSON
            decimal temperature = decimal.Parse(Setting.Temperature, CultureInfo.InvariantCulture);
            int maxTokens = int.Parse(Setting.MaxTokens, CultureInfo.InvariantCulture);
            string requestBodyJson = GetRequestBodyJson(prompt, temperature, maxTokens);

            // Send the API request and get the response data
            return await SendApiRequestAsync(apiUrl, Setting.ApiKey, requestBodyJson);
        }

        private static string GetRequestBodyJson(string prompt, decimal temperature, int maxTokens)
        {
            // Set up the request body
            var requestBody = new CompletionsRequestBody
            {
                Model = "text-davinci-003",
                Prompt = prompt,
                Temperature = temperature,
                MaxTokens = maxTokens,
                TopP = 1.0m,
                FrequencyPenalty = 0.0m,
                PresencePenalty = 0.0m,
                N = 1,
                Stop = "[END]",
            };

            // Create a new JsonSerializerOptions object with the IgnoreNullValues and IgnoreReadOnlyProperties properties set to true
            var serializerOptions = new JsonSerializerOptions
            {
                IgnoreNullValues = true,
                IgnoreReadOnlyProperties = true,
            };

            // Serialize the request body to JSON using the JsonSerializer.Serialize method overload that takes a JsonSerializerOptions parameter
            return JsonSerializer.Serialize(requestBody, serializerOptions);
        }

        private static async Task<CompletionsResponse> SendApiRequestAsync(string apiUrl, string apiKey, string requestBodyJson)
        {
            // Create a new HttpClient for making the API request
            using HttpClient client = new HttpClient();

            // Set the API key in the request headers
            client.DefaultRequestHeaders.Add("Authorization", "Bearer " + apiKey);

            // Create a new StringContent object with the JSON payload and the correct content type
            StringContent content = new StringContent(requestBodyJson, Encoding.UTF8, "application/json");

            // Send the API request and get the response
            HttpResponseMessage response = await client.PostAsync(apiUrl, content);

            // Deserialize the response
            var responseBody = await response.Content.ReadAsStringAsync();

            // Return the response data
            return JsonSerializer.Deserialize<CompletionsResponse>(responseBody);
        }

调用方式

 var reply = await ChatService.GetResponseDataAsync('xxxxxxxxxx');

完整代码参考~

在学习 maui 的过程中,遇到问题我在 Microsoft Learn 提问,回答的效率很快,推荐大家试试看!

使用 .NET MAUI 开发 ChatGPT 客户端

点我了解更多 MAUI 相关资料~