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

102次阅读

共计 4881 个字符,预计需要花费 13 分钟才能阅读完成。

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

正文完
 0