最近 chatgpt 很火,由于网页版本限制了 ip,还得必须开代理,用起来比较麻烦,所以我尝试用 maui 开发一个聊天小应用,结合 chatgpt 的开放 api 来实现(很多客户端使用网页版本接口用 cookie 的方式,有很多限制(如下图)总归不是很正规)。
效果如下
mac 端由于需要升级 macos13 才能开发调试,这部分我还没有完成,不过 maui 的控件是跨平台的,放在后续我升级系统再说。
开发实战
我是设想开发一个类似 jetbrains 的 toolbox 应用一样,启动程序在桌面右下角出现托盘图标,点击图标弹出应用(风格在 windows mac 平台保持一致)
需要实现的功能一览
- 托盘图标(右键点击有 menu)
- webview(js 和 csharp 互相调用)
- 聊天 spa 页面(react 开发,build 后让 webview 展示)
新建一个 maui 工程(vs2022)
坑一:默认编译出来的 exe 是直接双击打不开的
工程文件加上这个配置
none true true
以上修改后,编译出来的 exe 双击就可以打开了
托盘图标(右键点击有 menu)
启动时设置窗口不能改变大小,隐藏 titlebar, 让 webview 控件占满整个窗口
这里要根据平台不同实现不同了,windows 平台采用 winapi 调用,具体看工程代码吧!
webview
在 mainpage.xaml 添加控件
对应的静态 html 等文件放在工程的 resource\raw 文件夹下 (整个文件夹里面默认是作为内嵌资源打包的,工程文件里面的如下配置起的作用)
【重点】js 和 csharp 互相调用
这部分我找了很多资料,最终参考了这个 ,然后改进了下。
主要原理是:
- 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 taskgetresponsedataasync(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 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 (responsebody); }
调用方式
var reply = await chatservice.getresponsedataasync('xxxxxxxxxx');
在学习 maui 的过程中,遇到问题我在 microsoft learn 提问,回答的效率很快,推荐大家试试看!