使用 .NET MAUI 開(kāi)發(fā) ChatGPT 客戶端的流程
最近 chatgpt 很火,由于網(wǎng)頁(yè)版本限制了 ip,還得必須開(kāi)代理,用起來(lái)比較麻煩,所以我嘗試用 maui 開(kāi)發(fā)一個(gè)聊天小應(yīng)用,結(jié)合 chatgpt 的開(kāi)放 api 來(lái)實(shí)現(xiàn)(很多客戶端使用網(wǎng)頁(yè)版本接口用 cookie 的方式,有很多限制(如下圖)總歸不是很正規(guī))。

效果如下

mac 端由于需要升級(jí) macos13 才能開(kāi)發(fā)調(diào)試,這部分我還沒(méi)有完成,不過(guò) maui 的控件是跨平臺(tái)的,放在后續(xù)我升級(jí)系統(tǒng)再說(shuō)。
開(kāi)發(fā)實(shí)戰(zhàn)
我是設(shè)想開(kāi)發(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 開(kāi)發(fā),build 后讓 webview 展示)
新建一個(gè) maui 工程(vs2022)

坑一:默認(rèn)編譯出來(lái)的 exe 是直接雙擊打不開(kāi)的

工程文件加上這個(gè)配置
<WindowsPackageType>None</WindowsPackageType> <WindowsAppSDKSelfContained Condition="'$(IsUnpackaged)' == 'true'">true</WindowsAppSDKSelfContained> <SelfContained Condition="'$(IsUnpackaged)' == 'true'">true</SelfContained>
以上修改后,編譯出來(lái)的 exe 雙擊就可以打開(kāi)了
托盤圖標(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)聽(tīng) 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)聽(tīng)回調(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 的開(kāi)放 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 開(kāi)發(fā) ChatGPT 客戶端的文章就介紹到這了,更多相關(guān).NET MAUI 開(kāi)發(fā) ChatGPT 內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- chatgpt國(guó)內(nèi)鏡像?pycharm?idea插件使用詳解
- ChatGPT在IDEA中使用的詳細(xì)過(guò)程
- Vscode ChatGPT插件使用(無(wú)需代理注冊(cè))
- 如何在Python里使用ChatGPT及ChatGPT簡(jiǎn)介
- chatGPT使用及注冊(cè)過(guò)程中常見(jiàn)的一些錯(cuò)誤解決方法(所有報(bào)錯(cuò)匯總)
- 使用ChatGPT來(lái)自動(dòng)化Python任務(wù)
- 手把手教你在Python里使用ChatGPT
- vscode使用chatGPT 的方法
- chatgpt-api使用指南詳解教程【官方泄露版】
相關(guān)文章
.NET使用Collections.Pooled提升性能優(yōu)化的方法
這篇文章主要介紹了.NET使用Collections.Pooled性能優(yōu)化的方法,今天要給大家分享類庫(kù)Collections.Pooled,它是通過(guò)池化內(nèi)存來(lái)達(dá)到降低內(nèi)存占用和GC的目的,另外也會(huì)帶大家看看源碼,為什么它會(huì)帶來(lái)這些性能提升,一起通過(guò)本文學(xué)習(xí)下吧2022-05-05
Asp.Net套用母版頁(yè)后元素ID不一致(個(gè)人總結(jié))
這篇文章主要介紹了Asp.Net套用母版頁(yè)后元素ID不一致(個(gè)人總結(jié)),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-11-11
asp.net正則表達(dá)式刪除指定的HTML標(biāo)簽的代碼
抓取某網(wǎng)頁(yè)的數(shù)據(jù)后(比如描述),如果照原樣顯示的話,可能會(huì)因?yàn)樗锩姘瑳](méi)有閉合的HTML標(biāo)簽而打亂了格式,也可能它里面用了比較讓人 費(fèi)解 的HTML標(biāo)簽,把預(yù)訂的格式攪亂.2010-09-09
.NET?Core?Web?APi類庫(kù)內(nèi)嵌運(yùn)行的方法
這篇文章主要介紹了.NET?Core?Web?APi類庫(kù)內(nèi)嵌運(yùn)行的方法,本節(jié)我們重點(diǎn)討論如何內(nèi)嵌運(yùn)行.NET Core Web APi類庫(kù),同時(shí)介紹了兩種激活比如控制器特性方案,需要的朋友可以參考下2022-09-09
.NET高級(jí)調(diào)試之sos命令輸出看不懂的處理方法
.NET高級(jí)調(diào)試屬于一個(gè)偏冷門的領(lǐng)域,國(guó)內(nèi)可觀測(cè)的資料比較少,所以很多東西需要你自己去探究源代碼,然后用各種調(diào)試工具去驗(yàn)證,下面通過(guò)本文給大家分享.NET高級(jí)調(diào)試之sos命令輸出的相關(guān)知識(shí),感興趣的朋友一起看看吧2024-03-03
ASP.NET中Validation驗(yàn)證控件正則表達(dá)式特殊符號(hào)的說(shuō)明
本文介紹asp.net中RegularExpressionValidator控件中的幾種特殊字符串使用規(guī)則,并做了代碼演示,希望對(duì)大家有所幫助。2016-04-04
Asp.Net實(shí)現(xiàn)無(wú)限分類生成表格的方法(后臺(tái)自定義輸出table)
這篇文章主要介紹了Asp.Net實(shí)現(xiàn)無(wú)限分類生成表格的方法,同時(shí)后臺(tái)自定義輸出table表格,詳細(xì)分析了asp.net生成表格的相關(guān)技巧,需要的朋友可以參考下2016-04-04
一文帶你了解.Net基于Threading.Mutex實(shí)現(xiàn)互斥鎖
互斥鎖是一個(gè)互斥的同步對(duì)象,意味著同一時(shí)間有且僅有一個(gè)線程可以獲取它。這篇文章主要介紹了一文帶你了解.Net基于Threading.Mutex實(shí)現(xiàn)互斥鎖,感興趣的可以了解一下2021-06-06
ASP.NET中GridView 重復(fù)表格列合并的實(shí)現(xiàn)方法
本文通過(guò)GridView 和 Repeater 解決有關(guān)表格顯示數(shù)據(jù)重復(fù)的數(shù)據(jù)列和并的方法,非常實(shí)用,感興趣的朋友一起看下吧2016-08-08

