微软跨平台maui开发chatgpt客户端
阅读原文时间:2023年07月09日阅读:3

image

什么是maui

.NET 多平台应用 UI (.NET MAUI) 是一个跨平台框架,用于使用 C# 和 XAML 创建本机移动(ios,andriod)和桌面(windows,mac)应用。

image

chagpt

最近这玩意很火,由于网页版本限制了ip,还得必须开代理, 用起来比较麻烦,所以我尝试用maui开发一个聊天小应用 结合 chatgpt的开放api来实现(很多客户端使用网页版本接口用cookie的方式,有很多限制(如下图)总归不是很正规)

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

坑一: 默认编译出来的exe是直接双击打不开的

image

工程文件加上这个配置

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

以上修改后,编译出来的exe双击就可以打开了

托盘图标(右键点击有menu)

启动时设置窗口不能改变大小,隐藏titlebar, 让Webview控件占满整个窗口

image

这里要根据平台不同实现不同了,windows平台采用winAPI调用,具体看工程代码吧

WebView

在MainPage.xaml 添加控件

image

对应的静态html等文件放在工程的 Resource\Raw文件夹下 (整个文件夹里面默认是作为内嵌资源打包的,工程文件里面的如下配置起的作用)

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

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的调用封装如下:

//&nbsp;调用csharp的方法封装
export&nbsp;default&nbsp;class&nbsp;CsharpMethod&nbsp;{
&nbsp;&nbsp;constructor(command,&nbsp;data)&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;this.RequestPrefix&nbsp;=&nbsp;"request_csharp_";
&nbsp;&nbsp;&nbsp;&nbsp;this.ResponsePrefix&nbsp;=&nbsp;"response_csharp_";
&nbsp;&nbsp;&nbsp;&nbsp;//&nbsp;唯一
&nbsp;&nbsp;&nbsp;&nbsp;this.dataId&nbsp;=&nbsp;this.RequestPrefix&nbsp;+&nbsp;new&nbsp;Date().getTime();
&nbsp;&nbsp;&nbsp;&nbsp;//&nbsp;调用csharp的命令
&nbsp;&nbsp;&nbsp;&nbsp;this.command&nbsp;=&nbsp;command;
&nbsp;&nbsp;&nbsp;&nbsp;//&nbsp;参数
&nbsp;&nbsp;&nbsp;&nbsp;this.data&nbsp;=&nbsp;{&nbsp;command:&nbsp;command,&nbsp;data:&nbsp;!data&nbsp;?&nbsp;''&nbsp;:&nbsp;JSON.stringify(data),&nbsp;key:&nbsp;this.dataId&nbsp;}
&nbsp;&nbsp;}

&nbsp;&nbsp;//&nbsp;调用csharp&nbsp;返回promise
&nbsp;&nbsp;call()&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;//&nbsp;把data存储到localstorage中&nbsp;目的是让csharp端获取参数
&nbsp;&nbsp;&nbsp;&nbsp;localStorage.setItem(this.dataId,&nbsp;this.utf8_to_b64(JSON.stringify(this.data)));
&nbsp;&nbsp;&nbsp;&nbsp;let&nbsp;eventKey&nbsp;=&nbsp;this.dataId.replace(this.RequestPrefix,&nbsp;this.ResponsePrefix);
&nbsp;&nbsp;&nbsp;&nbsp;let&nbsp;that&nbsp;=&nbsp;this;
&nbsp;&nbsp;&nbsp;&nbsp;const&nbsp;promise&nbsp;=&nbsp;new&nbsp;Promise(function&nbsp;(resolve,&nbsp;reject)&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;const&nbsp;eventHandler&nbsp;=&nbsp;function&nbsp;(e)&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;window.removeEventListener(eventKey,&nbsp;eventHandler);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;let&nbsp;resp&nbsp;=&nbsp;e.newValue;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if&nbsp;(resp)&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//&nbsp;从base64转换
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;let&nbsp;realData&nbsp;=&nbsp;that.b64_to_utf8(resp);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if&nbsp;(realData.startsWith('err:'))&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;reject(realData.substr(4));
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}&nbsp;else&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;resolve(realData);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}&nbsp;else&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;reject("unknown&nbsp;error&nbsp;:&nbsp;"&nbsp;+&nbsp;eventKey);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;};
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//&nbsp;注册监听回调(csharp端处理完发起的)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;window.addEventListener(eventKey,&nbsp;eventHandler);
&nbsp;&nbsp;&nbsp;&nbsp;});
&nbsp;&nbsp;&nbsp;&nbsp;//&nbsp;改变location&nbsp;发送给csharp端
&nbsp;&nbsp;&nbsp;&nbsp;window.location&nbsp;=&nbsp;"/api/"&nbsp;+&nbsp;this.dataId;
&nbsp;&nbsp;&nbsp;&nbsp;return&nbsp;promise;
&nbsp;&nbsp;}

&nbsp;&nbsp;//&nbsp;转成base64&nbsp;解决中文乱码
&nbsp;&nbsp;utf8_to_b64(str)&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;return&nbsp;window.btoa(unescape(encodeURIComponent(str)));
&nbsp;&nbsp;}
&nbsp;&nbsp;//&nbsp;从base64转过来&nbsp;解决中文乱码
&nbsp;&nbsp;b64_to_utf8(str)&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;return&nbsp;decodeURIComponent(escape(window.atob(str)));
&nbsp;&nbsp;}

}

前端的使用方式

import&nbsp;CsharpMethod&nbsp;from&nbsp;'../../services/api'

//&nbsp;发起调用csharp的chat事件函数
const&nbsp;method&nbsp;=&nbsp;new&nbsp;CsharpMethod("chat",&nbsp;{msg:&nbsp;message});
method.call()&nbsp;//&nbsp;call返回promise
.then(data&nbsp;=>{
&nbsp;&nbsp;//&nbsp;拿到csharp端的返回后展示
&nbsp;&nbsp;onMessageHandler({
&nbsp;&nbsp;&nbsp;&nbsp;message:&nbsp;data,
&nbsp;&nbsp;&nbsp;&nbsp;username:&nbsp;'Robot',
&nbsp;&nbsp;&nbsp;&nbsp;type:&nbsp;'chat_message'
&nbsp;&nbsp;});
}).catch(err&nbsp;=>&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;alert(err);
});

csharp端的处理:

image

这么封装后,js和csharp的互相调用就很方便了

chatgpt的开放api调用

注册号chatgpt后可以申请一个APIKEY

image

API封装:

&nbsp;&nbsp;public&nbsp;static&nbsp;async&nbsp;Task<CompletionsResponse>&nbsp;GetResponseDataAsync(string&nbsp;prompt)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//&nbsp;Set&nbsp;up&nbsp;the&nbsp;API&nbsp;URL&nbsp;and&nbsp;API&nbsp;key
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;string&nbsp;apiUrl&nbsp;=&nbsp;"https://api.openai.com/v1/completions";

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//&nbsp;Get&nbsp;the&nbsp;request&nbsp;body&nbsp;JSON
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;decimal&nbsp;temperature&nbsp;=&nbsp;decimal.Parse(Setting.Temperature,&nbsp;CultureInfo.InvariantCulture);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;int&nbsp;maxTokens&nbsp;=&nbsp;int.Parse(Setting.MaxTokens,&nbsp;CultureInfo.InvariantCulture);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;string&nbsp;requestBodyJson&nbsp;=&nbsp;GetRequestBodyJson(prompt,&nbsp;temperature,&nbsp;maxTokens);

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//&nbsp;Send&nbsp;the&nbsp;API&nbsp;request&nbsp;and&nbsp;get&nbsp;the&nbsp;response&nbsp;data
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return&nbsp;await&nbsp;SendApiRequestAsync(apiUrl,&nbsp;Setting.ApiKey,&nbsp;requestBodyJson);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;private&nbsp;static&nbsp;string&nbsp;GetRequestBodyJson(string&nbsp;prompt,&nbsp;decimal&nbsp;temperature,&nbsp;int&nbsp;maxTokens)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//&nbsp;Set&nbsp;up&nbsp;the&nbsp;request&nbsp;body
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;var&nbsp;requestBody&nbsp;=&nbsp;new&nbsp;CompletionsRequestBody
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Model&nbsp;=&nbsp;"text-davinci-003",
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Prompt&nbsp;=&nbsp;prompt,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Temperature&nbsp;=&nbsp;temperature,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;MaxTokens&nbsp;=&nbsp;maxTokens,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;TopP&nbsp;=&nbsp;1.0m,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;FrequencyPenalty&nbsp;=&nbsp;0.0m,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;PresencePenalty&nbsp;=&nbsp;0.0m,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;N&nbsp;=&nbsp;1,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Stop&nbsp;=&nbsp;"[END]",
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;};

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//&nbsp;Create&nbsp;a&nbsp;new&nbsp;JsonSerializerOptions&nbsp;object&nbsp;with&nbsp;the&nbsp;IgnoreNullValues&nbsp;and&nbsp;IgnoreReadOnlyProperties&nbsp;properties&nbsp;set&nbsp;to&nbsp;true
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;var&nbsp;serializerOptions&nbsp;=&nbsp;new&nbsp;JsonSerializerOptions
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;IgnoreNullValues&nbsp;=&nbsp;true,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;IgnoreReadOnlyProperties&nbsp;=&nbsp;true,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;};

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//&nbsp;Serialize&nbsp;the&nbsp;request&nbsp;body&nbsp;to&nbsp;JSON&nbsp;using&nbsp;the&nbsp;JsonSerializer.Serialize&nbsp;method&nbsp;overload&nbsp;that&nbsp;takes&nbsp;a&nbsp;JsonSerializerOptions&nbsp;parameter
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return&nbsp;JsonSerializer.Serialize(requestBody,&nbsp;serializerOptions);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;private&nbsp;static&nbsp;async&nbsp;Task<CompletionsResponse>&nbsp;SendApiRequestAsync(string&nbsp;apiUrl,&nbsp;string&nbsp;apiKey,&nbsp;string&nbsp;requestBodyJson)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//&nbsp;Create&nbsp;a&nbsp;new&nbsp;HttpClient&nbsp;for&nbsp;making&nbsp;the&nbsp;API&nbsp;request
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;using&nbsp;HttpClient&nbsp;client&nbsp;=&nbsp;new&nbsp;HttpClient();

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//&nbsp;Set&nbsp;the&nbsp;API&nbsp;key&nbsp;in&nbsp;the&nbsp;request&nbsp;headers
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;client.DefaultRequestHeaders.Add("Authorization",&nbsp;"Bearer&nbsp;"&nbsp;+&nbsp;apiKey);

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//&nbsp;Create&nbsp;a&nbsp;new&nbsp;StringContent&nbsp;object&nbsp;with&nbsp;the&nbsp;JSON&nbsp;payload&nbsp;and&nbsp;the&nbsp;correct&nbsp;content&nbsp;type
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;StringContent&nbsp;content&nbsp;=&nbsp;new&nbsp;StringContent(requestBodyJson,&nbsp;Encoding.UTF8,&nbsp;"application/json");

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//&nbsp;Send&nbsp;the&nbsp;API&nbsp;request&nbsp;and&nbsp;get&nbsp;the&nbsp;response
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;HttpResponseMessage&nbsp;response&nbsp;=&nbsp;await&nbsp;client.PostAsync(apiUrl,&nbsp;content);

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//&nbsp;Deserialize&nbsp;the&nbsp;response
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;var&nbsp;responseBody&nbsp;=&nbsp;await&nbsp;response.Content.ReadAsStringAsync();

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//&nbsp;Return&nbsp;the&nbsp;response&nbsp;data
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return&nbsp;JsonSerializer.Deserialize<CompletionsResponse>(responseBody);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}

调用方式

&nbsp;&nbsp;var&nbsp;reply&nbsp;=&nbsp;await&nbsp;ChatService.GetResponseDataAsync('xxxxxxxxxx');

完整代码参考 https://github.com/yuzd/maui_chatgpt

在学习maui的过程中,遇到问题我在microsoft learn提问,回答的效率很快,推荐大家试试看

image

关于我

image

微软最有价值专家是微软公司授予第三方技术专业人士的一个全球奖项。27年来,世界各地的技术社区领导者,因其在线上和线下的技术社区中分享专业知识和经验而获得此奖项。

MVP是经过严格挑选的专家团队,他们代表着技术最精湛且最具智慧的人,是对社区投入极大的热情并乐于助人的专家。MVP致力于通过演讲、论坛问答、创建网站、撰写博客、分享视频、开源项目、组织会议等方式来帮助他人,并最大程度地帮助微软技术社区用户使用Microsoft技术。

更多详情请登录官方网站https://mvp.microsoft.com/zh-cn

手机扫一扫

移动阅读更方便

阿里云服务器
腾讯云服务器
七牛云服务器

你可能感兴趣的文章