微软跨平台maui开发chatgpt客户端

作者: cnpim CNPIM 2023年06月09日

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><WindowsAppSDKSelfContainedCondition="'$(IsUnpackaged)'=='true'">true</WindowsAppSDKSelfContained><SelfContainedCondition="'$(IsUnpackaged)'=='true'">true</SelfContained>

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

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

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

image
image

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

WebView

在MainPage.xaml 添加控件

image
image

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

<!--RawAssets(alsoremovethe"ResourcesRaw"prefix)--><MauiAssetInclude="ResourcesRaw**"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的方法封装exportdefaultclassCsharpMethod{constructor(command,data){this.RequestPrefix="request_csharp_";this.ResponsePrefix="response_csharp_";//唯一this.dataId=this.RequestPrefix+newDate().getTime();//调用csharp的命令this.command=command;//参数this.data={command:command,data:!data?'':JSON.stringify(data),key:this.dataId}}//调用csharp返回promisecall(){//把data存储到localstorage中目的是让csharp端获取参数localStorage.setItem(this.dataId,this.utf8_to_b64(JSON.stringify(this.data)));leteventKey=this.dataId.replace(this.RequestPrefix,this.ResponsePrefix);letthat=this;constpromise=newPromise(function(resolve,reject){consteventHandler=function(e){window.removeEventListener(eventKey,eventHandler);letresp=e.newValue;if(resp){//从base64转换letrealData=that.b64_to_utf8(resp);if(realData.startsWith('err:')){reject(realData.substr(4));}else{resolve(realData);}}else{reject("unknownerror:"+eventKey);}};//注册监听回调(csharp端处理完发起的)window.addEventListener(eventKey,eventHandler);});//改变location发送给csharp端window.location="/api/"+this.dataId;returnpromise;}//转成base64解决中文乱码utf8_to_b64(str){returnwindow.btoa(unescape(encodeURIComponent(str)));}//从base64转过来解决中文乱码b64_to_utf8(str){returndecodeURIComponent(escape(window.atob(str)));}}

前端的使用方式

importCsharpMethodfrom'../../services/api'//发起调用csharp的chat事件函数constmethod=newCsharpMethod("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封装:

publicstaticasyncTask<CompletionsResponse>GetResponseDataAsync(stringprompt){//SetuptheAPIURLandAPIkeystringapiUrl="https://api.openai.com/v1/completions";//GettherequestbodyJSONdecimaltemperature=decimal.Parse(Setting.Temperature,CultureInfo.InvariantCulture);intmaxTokens=int.Parse(Setting.MaxTokens,CultureInfo.InvariantCulture);stringrequestBodyJson=GetRequestBodyJson(prompt,temperature,maxTokens);//SendtheAPIrequestandgettheresponsedatareturnawaitSendApiRequestAsync(apiUrl,Setting.ApiKey,requestBodyJson);}privatestaticstringGetRequestBodyJson(stringprompt,decimaltemperature,intmaxTokens){//SetuptherequestbodyvarrequestBody=newCompletionsRequestBody{Model="text-davinci-003",Prompt=prompt,Temperature=temperature,MaxTokens=maxTokens,TopP=1.0m,FrequencyPenalty=0.0m,PresencePenalty=0.0m,N=1,Stop="[END]",};//CreateanewJsonSerializerOptionsobjectwiththeIgnoreNullValuesandIgnoreReadOnlyPropertiespropertiessettotruevarserializerOptions=newJsonSerializerOptions{IgnoreNullValues=true,IgnoreReadOnlyProperties=true,};//SerializetherequestbodytoJSONusingtheJsonSerializer.SerializemethodoverloadthattakesaJsonSerializerOptionsparameterreturnJsonSerializer.Serialize(requestBody,serializerOptions);}privatestaticasyncTask<CompletionsResponse>SendApiRequestAsync(stringapiUrl,stringapiKey,stringrequestBodyJson){//CreateanewHttpClientformakingtheAPIrequestusingHttpClientclient=newHttpClient();//SettheAPIkeyintherequestheadersclient.DefaultRequestHeaders.Add("Authorization","Bearer"+apiKey);//CreateanewStringContentobjectwiththeJSONpayloadandthecorrectcontenttypeStringContentcontent=newStringContent(requestBodyJson,Encoding.UTF8,"application/json");//SendtheAPIrequestandgettheresponseHttpResponseMessageresponse=awaitclient.PostAsync(apiUrl,content);//DeserializetheresponsevarresponseBody=awaitresponse.Content.ReadAsStringAsync();//ReturntheresponsedatareturnJsonSerializer.Deserialize<CompletionsResponse>(responseBody);}

调用方式

varreply=awaitChatService.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


本文阅读量:

声明:本信息来源于网络,仅用于学习和技术交流,如有侵权或其他问题,请联系本站处理。

技术支持:CNPIM.COM