使用 .NET MAUI 開發(fā) ChatGPT 客戶端的流程
最近 chatgpt 很火,由于網(wǎng)頁(yè)版本限制了 ip,還得必須開代理,用起來(lái)比較麻煩,所以我嘗試用 maui 開發(fā)一個(gè)聊天小應(yīng)用,結(jié)合 chatgpt 的開放 api 來(lái)實(shí)現(xiàn)(很多客戶端使用網(wǎng)頁(yè)版本接口用 cookie 的方式,有很多限制(如下圖)總歸不是很正規(guī))。
效果如下
mac 端由于需要升級(jí) macos13 才能開發(fā)調(diào)試,這部分我還沒(méi)有完成,不過(guò) maui 的控件是跨平臺(tái)的,放在后續(xù)我升級(jí)系統(tǒng)再說(shuō)。
開發(fā)實(shí)戰(zhàn)
我是設(shè)想開發(fā)一個(gè)類似 jetbrains 的 ToolBox 應(yīng)用一樣,啟動(dòng)程序在桌面右下角出現(xiàn)托盤圖標(biāo),點(diǎn)擊圖標(biāo)彈出應(yīng)用(風(fēng)格在 windows mac 平臺(tái)保持一致)
需要實(shí)現(xiàn)的功能一覽
- 托盤圖標(biāo)(右鍵點(diǎn)擊有 menu)
- webview(js 和 csharp 互相調(diào)用)
- 聊天 SPA 頁(yè)面(react 開發(fā),build 后讓 webview 展示)
新建一個(gè) maui 工程(vs2022)
坑一:默認(rèn)編譯出來(lái)的 exe 是直接雙擊打不開的
工程文件加上這個(gè)配置
<WindowsPackageType>None</WindowsPackageType> <WindowsAppSDKSelfContained Condition="'$(IsUnpackaged)' == 'true'">true</WindowsAppSDKSelfContained> <SelfContained Condition="'$(IsUnpackaged)' == 'true'">true</SelfContained>
以上修改后,編譯出來(lái)的 exe 雙擊就可以打開了
托盤圖標(biāo)(右鍵點(diǎn)擊有 menu)
啟動(dòng)時(shí)設(shè)置窗口不能改變大小,隱藏 titlebar, 讓 Webview 控件占滿整個(gè)窗口
這里要根據(jù)平臺(tái)不同實(shí)現(xiàn)不同了,windows 平臺(tái)采用 winAPI 調(diào)用,具體看工程代碼吧!
WebView
在 MainPage.xaml 添加控件
對(duì)應(yīng)的靜態(tài) html 等文件放在工程的 Resource\Raw 文件夾下 (整個(gè)文件夾里面默認(rèn)是作為內(nèi)嵌資源打包的,工程文件里面的如下配置起的作用)
<!-- Raw Assets (also remove the "Resources\Raw" prefix) --> <MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
【重點(diǎn)】js 和 csharp 互相調(diào)用
這部分我找了很多資料,最終參考了這個(gè) demo,然后改進(jìn)了下。
主要原理是:
- js 調(diào)用 csharp 方法前先把數(shù)據(jù)存儲(chǔ)在 localstorage 里
- 然后 windows.location 切換特定的 url 發(fā)起調(diào)用,返回一個(gè) promise,等待 csharp 的事件
- csharp 端監(jiān)聽 webview 的 Navigating 事件,異步進(jìn)行下面處理
- 根據(jù) url 解析出來(lái) localstorage 的 key
- 然后 csharp 端調(diào)用 excutescript 根據(jù) key 拿到 localstorage 的 value
- 進(jìn)行邏輯處理后返回通過(guò)事件分發(fā)到 js 端
js 的調(diào)用封裝如下:
// 調(diào)用csharp的方法封裝 export default class CsharpMethod { constructor(command, data) { this.RequestPrefix = "request_csharp_"; this.ResponsePrefix = "response_csharp_"; // 唯一 this.dataId = this.RequestPrefix + new Date().getTime(); // 調(diào)用csharp的命令 this.command = command; // 參數(shù) this.data = { command: command, data: !data ? '' : JSON.stringify(data), key: this.dataId } } // 調(diào)用csharp 返回promise call() { // 把data存儲(chǔ)到localstorage中 目的是讓csharp端獲取參數(shù) 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轉(zhuǎn)換 let realData = that.b64_to_utf8(resp); if (realData.startsWith('err:')) { reject(realData.substr(4)); } else { resolve(realData); } } else { reject("unknown error :" + eventKey); } }; // 注冊(cè)監(jiān)聽回調(diào)(csharp端處理完發(fā)起的) window.addEventListener(eventKey, eventHandler); }); // 改變location 發(fā)送給csharp端 window.location = "/api/" + this.dataId; return promise; } // 轉(zhuǎn)成base64 解決中文亂碼 utf8_to_b64(str) { return window.btoa(unescape(encodeURIComponent(str))); } // 從base64轉(zhuǎn)過(guò)來(lái) 解決中文亂碼 b64_to_utf8(str) { return decodeURIComponent(escape(window.atob(str))); } }
前端的使用方式
import CsharpMethod from '../../services/api' // 發(fā)起調(diào)用csharp的chat事件函數(shù) 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 的互相調(diào)用就很方便了。
chatgpt 的開放 api 調(diào)用
注冊(cè)好 chatgpt 后可以申請(qǐng)一個(gè) 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); }
調(diào)用方式
var reply = await ChatService.GetResponseDataAsync('xxxxxxxxxx');
在學(xué)習(xí) maui 的過(guò)程中,遇到問(wèn)題我在 Microsoft Learn 提問(wèn),回答的效率很快,推薦大家試試看!
到此這篇關(guān)于使用 .NET MAUI 開發(fā) ChatGPT 客戶端的文章就介紹到這了,更多相關(guān).NET MAUI 開發(fā) ChatGPT 內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Microsoft SQL Server 2005 Express 遠(yuǎn)程訪問(wèn)設(shè)置詳述,100%成功篇
Microsoft SQL Server 2005 Express Edition是Microsoft數(shù)據(jù)庫(kù)的低端解決方案,是免費(fèi)的,并且可以隨軟件免費(fèi)發(fā)布,而就其數(shù)據(jù)庫(kù)功能對(duì)于一般的企業(yè)級(jí)應(yīng)用已足夠了。但 默認(rèn)安裝時(shí)只允許本地訪問(wèn),而不能遠(yuǎn)程訪問(wèn)。2009-03-03Asp.net利用JQuery AJAX實(shí)現(xiàn)無(wú)刷新評(píng)論思路與代碼
Asp.net利用JQuery AJAX實(shí)現(xiàn)無(wú)刷新評(píng)論,此功能是每一個(gè)從事asp.net開發(fā)者的朋友都希望實(shí)現(xiàn)的,本文利用閑暇時(shí)間整理了一些,有需要的朋友可以參考下2012-12-12c# Random快速連續(xù)產(chǎn)生相同隨機(jī)數(shù)的解決方案
在寫數(shù)獨(dú)基類的時(shí)候?yàn)榱水a(chǎn)生隨機(jī)數(shù)的時(shí)候遇到奇怪的問(wèn)題2009-03-03基于.NET程序默認(rèn)啟動(dòng)線程數(shù)講解
本篇文章小編為大家介紹,基于.NET程序默認(rèn)啟動(dòng)線程數(shù)講解。需要的朋友參考下2013-04-04asp.net SqlDataReader綁定Repeater
asp.net SqlDataReader綁定Repeater2009-04-04基于Fiddler實(shí)現(xiàn)修改接口返回?cái)?shù)據(jù)進(jìn)行測(cè)試
這篇文章主要介紹了基于Fiddler實(shí)現(xiàn)修改接口返回?cái)?shù)據(jù)進(jìn)行測(cè)試,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-08-08Asp.net頁(yè)面中調(diào)用soapheader進(jìn)行驗(yàn)證的操作步驟
這篇文章主要介紹了Asp.net頁(yè)面中調(diào)用soapheader進(jìn)行驗(yàn)證的操作步驟,感興趣的小伙伴們可以參考一下2016-04-04寫一個(gè)含數(shù)字,拼音,漢字的驗(yàn)證碼生成類
本文和大家分享的是一個(gè)集成1:小寫拼音;2:大寫拼音;3:數(shù)字;4:漢字的驗(yàn)證碼生成類。本章例子也會(huì)有一個(gè)mvc使用驗(yàn)證碼校驗(yàn)的場(chǎng)景。具有一定的參考價(jià)值,下面跟著小編一起來(lái)看下吧2017-01-01