最近 chatgpt 很火,由于网页版本限制了 ip,还得必须开代理,用起来比较麻烦,所以我尝试用 maui 开发一个聊天小应用,结合 chatgpt 的开放 api 来实现(很多客户端使用网页版本接口用 cookie 的方式,有很多限制(如下图)总归不是很正规)。
效果如下
mac 端由于需要升级 macos13 才能开发调试,这部分我还没有完成,不过 maui 的控件是跨平台的,放在后续我升级系统再说。
开发实战
我是设想开发一个类似 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 等文件放在工程的 Resource\Raw 文件夹下 (整个文件夹里面默认是作为内嵌资源打包的,工程文件里面的如下配置起的作用)
-
<!-- Raw Assets (also remove the "Resources\Raw" prefix) -->
-
<MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
【重点】js 和 csharp 互相调用
这部分我找了很多资料,最终参考了这个 demo,然后改进了下。
主要原理是:
- 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');
在学习 maui 的过程中,遇到问题我在 Microsoft Learn 提问,回答的效率很快,推荐大家试试看!
转载:https://blog.csdn.net/helendemeng/article/details/128386667