什么是maui
.NET 多平台应用 UI (.NET MAUI) 是一个跨平台框架,用于使用 C# 和 XAML 创建本机移动(ios,andriod)和桌面(windows,mac)应用。
chagpt
最近这玩意很火,由于网页版本限制了ip,还得必须开代理, 用起来比较麻烦,所以我尝试用maui开发一个聊天小应用 结合 chatgpt的开放api来实现(很多客户端使用网页版本接口用cookie的方式,有很多限制(如下图)总归不是很正规)
效果如下
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)
坑一: 默认编译出来的exe是直接双击打不开的
工程文件加上这个配置
<WindowsPackageType>None</WindowsPackageType> <WindowsAppSDKSelfContained Condition="'$(IsUnpackaged)' == 'true'">true</WindowsAppSDKSelfContained> <SelfContained Condition="'$(IsUnpackaged)' == 'true'">true</SelfContained>
以上修改后,编译出来的exe双击就可以打开了
托盘图标(右键点击有menu)
启动时设置窗口不能改变大小,隐藏titlebar, 让Webview控件占满整个窗口
这里要根据平台不同实现不同了,windows平台采用winAPI调用,具体看工程代码吧
WebView
在MainPage.xaml 添加控件
对应的静态html等文件放在工程的 ResourceRaw文件夹下 (整个文件夹里面默认是作为内嵌资源打包的,工程文件里面的如下配置起的作用)
<!-- Raw Assets (also remove the "ResourcesRaw" prefix) --> <MauiAsset Include="ResourcesRaw**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
【重点】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 返回promise call() { // 把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端的处理:
这么封装后,js和csharp的互相调用就很方便了
chatgpt的开放api调用
注册号chatgpt后可以申请一个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); }
调用方式
var reply = await ChatService.GetResponseDataAsync('xxxxxxxxxx');
完整代码参考 https://github.com/yuzd/maui_chatgpt
在学习maui的过程中,遇到问题我在microsoft learn提问,回答的效率很快,推荐大家试试看
关于我
微软最有价值专家是微软公司授予第三方技术专业人士的一个全球奖项。27年来,世界各地的技术社区领导者,因其在线上和线下的技术社区中分享专业知识和经验而获得此奖项。
MVP是经过严格挑选的专家团队,他们代表着技术最精湛且最具智慧的人,是对社区投入极大的热情并乐于助人的专家。MVP致力于通过演讲、论坛问答、创建网站、撰写博客、分享视频、开源项目、组织会议等方式来帮助他人,并最大程度地帮助微软技术社区用户使用Microsoft技术。
更多详情请登录官方网站https://mvp.microsoft.com/zh-cn