乐趣区

关于.net:使用-NET-MAUI-开发-ChatGPT-客户端

最近 chatgpt 很火,因为网页版本限度了 ip,还得必须开代理,用起来比拟麻烦,所以我尝试用 maui 开发一个聊天小利用,联合 chatgpt 的凋谢 api 来实现(很多客户端应用网页版本接口用 cookie 的形式,有很多限度 (如下图) 总归不是很正规)。

成果如下

mac 端因为须要降级 macos13 能力开发调试,这部分我还没有实现,不过 maui 的控件是跨平台的,放在后续我降级零碎再说。

开发实战

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

须要实现的性能一览

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

新建一个 maui 工程(vs2022)

坑一:默认编译进去的 exe 是间接双击打不开的

工程文件加上这个配置

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

以上批改后,编译进去的 exe 双击就能够关上了

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

启动时设置窗口不能扭转大小,暗藏 titlebar,让 Webview 控件占满整个窗口

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

WebView

在 MainPage.xaml 增加控件

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

<!-- Raw Assets (also remove the "Resources\Raw" prefix) -->
<MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />

【重点】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 端的解决:

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

chatgpt 的凋谢 api 调用

注册好 chatgpt 后能够申请一个 APIKEY。

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 发问,答复的效率很快,举荐大家试试看!

点我理解更多 MAUI 相干材料~

退出移动版