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

89e85cef9592e48366e13e1e9f60740e.png

image

什么是maui

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

f237fc7c179851dd45c34c32d5889583.png
image

chagpt

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

1e40f7182a038a2bd9792cf06f30c2a4.png
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)

074130d40dc684c6ffba06a54e355c0e.png
image

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

1a4f8f41edebead54f4cde6794b2b2d4.png
image

工程文件加上这个配置

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

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

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

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

a001e9262446610770db17045c93818d.png
image

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

WebView

在MainPage.xaml 添加控件

026c2ebee8101f666b5e83b4b278285b.png
image

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

<!-- Raw Assets (also remove the "Resources\Raw" prefix) -->
<MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
7cd8c14ed449840bc3101f3084699d81.png
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 返回promisecall() {// 把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端的处理:

d4954f57970b1949c40d59653783a729.png
image

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

chatgpt的开放api调用

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

08037ac37c1c5eb819935e4d064172d4.png
image

API封装:

public static async Task<CompletionsResponse> GetResponseDataAsync(string prompt){// Set up the API URL and API keystring apiUrl = "https://api.openai.com/v1/completions";// Get the request body JSONdecimal 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 datareturn await SendApiRequestAsync(apiUrl, Setting.ApiKey, requestBodyJson);}private static string GetRequestBodyJson(string prompt, decimal temperature, int maxTokens){// Set up the request bodyvar 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 truevar serializerOptions = new JsonSerializerOptions{IgnoreNullValues = true,IgnoreReadOnlyProperties = true,};// Serialize the request body to JSON using the JsonSerializer.Serialize method overload that takes a JsonSerializerOptions parameterreturn JsonSerializer.Serialize(requestBody, serializerOptions);}private static async Task<CompletionsResponse> SendApiRequestAsync(string apiUrl, string apiKey, string requestBodyJson){// Create a new HttpClient for making the API requestusing HttpClient client = new HttpClient();// Set the API key in the request headersclient.DefaultRequestHeaders.Add("Authorization", "Bearer " + apiKey);// Create a new StringContent object with the JSON payload and the correct content typeStringContent content = new StringContent(requestBodyJson, Encoding.UTF8, "application/json");// Send the API request and get the responseHttpResponseMessage response = await client.PostAsync(apiUrl, content);// Deserialize the responsevar responseBody = await response.Content.ReadAsStringAsync();// Return the response datareturn JsonSerializer.Deserialize<CompletionsResponse>(responseBody);}

调用方式

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

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

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

975ccd35aacfae2a0eaae5090c371a41.png
image

关于我

942047c70d6d119ffe6383e313d923f5.png
image

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

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/280115.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

在Xshell 6开NumLock时按小键盘上的数字键并不能输入数字

小键盘问题 在Xshell 6上用vi的时候&#xff0c;开NumLock时按小键盘上的数字键并不能输入数字&#xff0c;而是出现一个字母然后换行&#xff08;实际上是命令模式上对应上下左右的键&#xff09;。解决方法 选项Terminal->Features里&#xff0c;找到Disable application …

WebP 在减少图片体积和流量上的效果如何?—— WebP 技术实践分享

作者 | Jackson编辑 | 尾尾 不论是 PC 还是移动端&#xff0c;图片一直占据着页面流量的大头&#xff0c;在图片的大小和质量之间如何权衡&#xff0c;成为了长期困扰开发者们的问题。而 WebP 技术的出现&#xff0c;为解决该问题提供了好的方案。本文将为大家详细介绍 WebP 技…

chrome 固定缩放比例_您如何调整Google Chrome浏览器的用户界面缩放比例?

chrome 固定缩放比例Everything can be going along nicely until a program gets a new update that suddenly turns everything into a visual mess, like scaling up the UI, for example. Is there a simple solution? Today’s SuperUser Q&A post has some helpful …

树莓派 Raspberry Pi 更换国内源

http://www.shumeipaiba.com/wanpai/jiaocheng/16.html转载于:https://www.cnblogs.com/Baronboy/p/9185849.html

优雅告别 2022 年,2023 年主题:敢想,就敢做!

自从工作之后&#xff0c;每年春节我都会花一天时间&#xff0c;一个人待在一个小房间&#xff0c;思考自己今年做了什么具备阶段性成果的事情。然后&#xff0c;写下明年需要执行的计划。会写在一个 XMind 文件里&#xff0c;记录每一年将要执行的计划&#xff0c;且未完成的计…

纯js上传文件 很好用

<!DOCTYPE html> <html> <head> <meta charset"utf-8"> <meta name"viewport" content"widthdevice-width, initial-scale1.0"> <title>参数设置</title> <meta name"keywords&q…

买台电脑,不行!去旅游一下,不行!论程序员怎么实现财务自由!

“最近读了一本不是编程的程序员技能书《软技能-代码之外的生存指南》&#xff0c;全书分为 7 个篇章&#xff0c;分别是职业、自我营销、学习、生产力、理财、健身和精神。在读完职业、自我营销和理财这三个篇章后&#xff0c;让我感触很深&#xff0c;也让我很意外。本来以为…

java发送gmail_如何在Gmail中轻松通过电子邮件发送人群

java发送gmailMailing lists are an old tool in the email arsenal, but their implementation in Gmail isn’t immediately intuitive. Read on as we show you how to email groups using your Gmail account. 邮件列表是电子邮件库中的一个旧工具&#xff0c;但是在Gmail中…

移动web开发相关笔记(三)

1.推荐以sublime插件的排名官网:https://packagecontrol.io/&#xff08;sublime插件官网&#xff09;2.时间算法【//总秒数var totalSecond 3671;//获取里面的小时var hoursMath.floor(totalSecond/3600);//获取剩下的分钟var minuteMath.floor(totalSecond%3600/60);//获取剩…

互联网和IT行业越来越严峻,前景几何?

楔子新冠疫情反反复复&#xff0c;互联网和IT行业一路下滑。硅谷裁员高达10万人。前景该何处何从呢&#xff1f;春江水暖猪先知IT行业如此的不景气&#xff0c;自然是一些在风口上被吹起来的猪首先感受到了。他们进行的自救其实就一条:裁员&#xff0c;大量的裁员。裁员可以解决…

Asp.net MVC使用Model Binding解除Session, Cookie等依赖

上篇文章"Asp.net MVC使用Filter解除Session, Cookie等依赖"介绍了如何使用Filter来解除对于Session, Cookie的依赖。其实这个也可以通过Model Binding来达到同样的效果。 什么是Model Binding? Model Binding的作用就是将Request请求中包含的散乱参数&#xff0c;根…

C++回声服务器_4-UDP connect版本客户端

针对UDP套接字调用connect函数不会与对方UDP套接字建立连接&#xff0c;只是向UDP套接字注册目标IP和端口信息。 修改客户端代码 服务器代码不需要修改&#xff0c;只需修改客户端代码。调用connect函数之后&#xff0c;可以调用write函数和read函数来发送、接收数据&#xff0…

如何在路由绑定中使用 IParsable

IParsable 是 .Net 7 中新增的接口&#xff0c;它可以将字符串转换为对应的实体。在 Controller 的 Route 绑定中可以使用 IParsable 来绑定复杂的实体。实验背景 假设有一个需要将 route "report/{month}-{day}" 绑定到 MyDate 对象上的场景。在 .Net 7 之前&#x…

火狐 新增标签 一直加载_在Firefox的新标签页中加载最后标签页的URL

火狐 新增标签 一直加载Yeah, you’re pretty sure that you’re the master of all things Firefox. I mean, why else would you be reading this article? So, we’ve got to ask, have you ever seen this one before? 是的&#xff0c;您很确定自己是Firefox的所有人。 …

ptyhon【递归练习】

转载于:https://www.cnblogs.com/LTEF/p/9187287.html

Iterator 和 for...of 循环

本系列属于阮一峰老师所著的ECMAScript 6 入门学习笔记 Iterator(遍历器) JavaScript表示“集合”的数据结构&#xff0c;除了Array 、Object &#xff0c;ES6又新增了Map 和Set 。 遍历器&#xff08;Iterator&#xff09;是一种统一的接口机制&#xff0c;用来处理所有不同的…

JAVA常量

2019独角兽企业重金招聘Python工程师标准>>> 常量就是一个固定值。它们不需要计算&#xff0c;直接代表相应的值。 常量指不能改变的量。 在Java中用final标志&#xff0c;声明方式和变量类似&#xff1a; final double PI 3.1415927; 虽然常量名也可以用小写&…

基于Docker托管Azure DevOps代理

Azure DevOps非常好用&#xff0c;但是为代理准备单独的服务器经常会显得性价比不高&#xff1a;配置低了&#xff0c;前端构建时会教会你做人&#xff0c;配置太高又有点浪费资源&#xff0c;代理数量少了各团队构建要打架。对于既想享受DevOps的美妙之处但是资源捉襟见肘的小…

微软 word转换pdf_如何将行转换为Microsoft Word表中的列

微软 word转换pdfYou’ve created a table in Word and started to enter your data. Then, you realize that the table should be transposed, meaning the rows should be columns and vice versa. Rather than recreating the table and manually entering the data again,…

pycharm中如何正确配置pyqt5

网上看了几个文章&#xff0c;不成功。这样做才是正确姿势&#xff1a; /Users/mac/anaconda3/bin/Designer.app /Users/mac/anaconda3/bin$ProjectFileDir$ pyuic5 $FileName$ -o $FileNameWithoutExtension$.py $ProjectFileDir$ 其它细节懒得说。 转载于:https://www.cnblog…