最近 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 相干材料~