微軟跨平臺maui開發chatgpt使用者端

2022-12-18 12:00:18
image
image

什麼是maui

.NET 多平臺應用 UI (.NET MAUI) 是一個跨平臺框架,用於使用 C# 和 XAML 建立本機移動(ios,andriod)和桌面(windows,mac)應用。

image
image

chagpt

最近這玩意很火,由於網頁版本限制了ip,還得必須開代理, 用起來比較麻煩,所以我嘗試用maui開發一個聊天小應用 結合 chatgpt的開放api來實現(很多使用者端使用網頁版本介面用cookie的方式,有很多限制(如下圖)總歸不是很正規)

image
image

效果如下

image
image

mac端由於需要升級macos13才能開發偵錯,這部分我還沒有完成,不過maui的控制元件是跨平臺的,放在後續我升級系統再說

本專案開源

https://github.com/yuzd/maui_chatgpt

學習maui的老鐵支援給個star

開發實戰

我是設想開發一個類似jetbrains的ToolBox應用一樣,啟動程式在桌面右下角出現托盤圖示,點選圖示彈出應用(風格在windows mac平臺保持一致)

需要實現的功能一覽

  • 托盤圖示(右鍵點選有menu)
  • webview(js和csharp互相呼叫)
  • 聊天SPA頁面(react開發,build後讓webview展示)

新建一個maui工程(vs2022)

image
image

坑一: 預設編譯出來的exe是直接雙擊打不開的

image
image

工程檔案加上這個設定

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

以上修改後,編譯出來的exe雙擊就可以開啟了

托盤圖示(右鍵點選有menu)

啟動時設定視窗不能改變大小,隱藏titlebar, 讓Webview控制元件佔滿整個視窗

image
image

這裡要根據平臺不同實現不同了,windows平臺採用winAPI呼叫,具體看工程程式碼吧

WebView

在MainPage.xaml 新增控制元件

image
image

對應的靜態html等檔案放在工程的 Resource\Raw資料夾下 (整個資料夾裡面預設是作為內嵌資源打包的,工程檔案裡面的如下設定起的作用)

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

【重點】js和csharp互相呼叫

這部分我找了很多資料,最終參考了這個demo,然後改進了下

https://github.com/mahop-net/Maui.HybridWebView

主要原理是:

  • 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端的處理:

image
image

這麼封裝後,js和csharp的互相呼叫就很方便了

chatgpt的開放api呼叫

註冊號chatgpt後可以申請一個APIKEY

image
image

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');

完整程式碼參考 https://github.com/yuzd/maui_chatgpt

在學習maui的過程中,遇到問題我在microsoft learn提問,回答的效率很快,推薦大家試試看

image
image

關於我

image
image

微軟最有價值專家是微軟公司授予第三方技術專業人士的一個全球獎項。27年來,世界各地的技術社群領導者,因其線上上和線下的技術社群中分享專業知識和經驗而獲得此獎項。

MVP是經過嚴格挑選的專家團隊,他們代表著技術最精湛且最具智慧的人,是對社群投入極大的熱情並樂於助人的專家。MVP致力於通過演講、論壇問答、建立網站、撰寫部落格、分享視訊、開源專案、組織會議等方式來幫助他人,並最大程度地幫助微軟技術社群使用者使用Microsoft技術。

更多詳情請登入官方網站https://mvp.microsoft.com/zh-cn