使用 .NET MAUI 开发 ChatGPT 客户端

news2025/2/4 11:01:25

最近 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 提问,回答的效率很快,推荐大家试试看!

点我了解更多 MAUI 相关资料~

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

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

相关文章

含辞未吐,声若幽兰,史上最强免费人工智能AI语音合成TTS服务微软Azure(Python3.10接入)

所谓文无第一&#xff0c;武无第二&#xff0c;云原生人工智能技术目前呈现三足鼎立的态势&#xff0c;微软&#xff0c;谷歌以及亚马逊三大巨头各擅胜场&#xff0c;不分伯仲&#xff0c;但目前微软Azure平台不仅仅只是一个PaaS平台&#xff0c;相比AWS&#xff0c;以及GAE&am…

【云原生】k8s的pod基础

内容预知 1.pod的相关知识 1.1 pod的基础概念 1.2 k8s中pod的两种使用方式 1.3 pod 容器的常规使用流程 1.4 k8s中pod结构设计的巧妙用意 通常把Pod分为两类 2. 容器的分类 2.1 pause基础容器&#xff08;infrastructure container&#xff09; &#xff08;1&#xf…

安装ZIMG 图片服务器

简介&#xff1a;zimg是图像存储和处理服务器。您可以使用URL参数从zimg获取压缩和缩放的图像。 zimg的并发I / O&#xff0c;分布式存储和时间处理能力非常出色。 您不再需要在图像服务器中使用nginx。在基准测试中&#xff0c;zimg可以在高并发级别上处理每秒3000图像下载任务…

【性能篇】29 # 怎么给Canvas绘制加速?

说明 【跟月影学可视化】学习笔记。 方法一&#xff1a;优化 Canvas 指令 例子&#xff1a;实现一些位置随机的多边形&#xff0c;并且不断刷新这些图形的形状和位置 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"…

openGauss的WDR报告解读

文章目录1.执行以下SQL命令,查询已经生成的快照信息。2.生成WDR报告。3.手工创建快照信息4.WDR涉及的数据表5.WDR报告解读在Oralce数据库中&#xff0c;遇到性能问题&#xff0c;我们通常会查看有无对应时间段的快照&#xff0c;生成awr报告并进一步分析&#xff08;AWR是Autom…

海量数据小内存!只出现两次的数以及中位数怎么找

文章目录题目一题目二实际上类似的题目类似的解法在之前已经有介绍过海量数据小内存&#xff01;如何找到高频数 海量数据小内存&#xff01;从未出现过的数在哪里 题目一 如何在 40 亿个无符号整数中找到出现次数只有两次的那些数&#xff0c;在只提供 1 G 内存的条件下 解…

Map集合概述、API 遍历方式(键值对集合)

注意&#xff1a; Map集合和Collection集合是两个不同类型的集合 Map集合体系特点&#xff1a; 常用API&#xff1a; 根据键找出值&#xff1a; map.get(key); 取所有键的集合和取所有值得集合&#xff1a; 因为key是无序不重复无索引&#xff0c;所以放入set集合&#xff…

锂电池电压和电量的关系

锂电池电压和电量之间,有一定的对应关系,通过对开路电压的测量,可以大致得出电池的剩余电量。不过用电压测量电量的方式有一定的不稳定性,例如放电电流、环境温度、循环、放电平台、电极材料等,都会给最后结果的准确与否带来影响。 电压和电量的对应关系是: 100%----4.…

【算法】常用查找算法(顺序查找、二分查找、插值查找、斐波那契查找)

目录查找算法1.线性(顺序)查找(1)思路(2)代码实现(java)2.二分(折半)查找(1)思路(2)代码实现(java)3.插值查找(1)思路(2)代码实现(java)4.斐波那契(黄金分割法)查找(1)思路(2)代码实现(java)查找算法 1.线性(顺序)查找 (1)思路 判断序列中是否包含某个元素&#xff0c;找到提…

Vue3引入Lottie动画以及遇到的坑

之所以写这个问题是因为原本我认为非常小的一件事却困扰了我一整天&#xff0c;所以我打算写一个博客记录一番。 国外动画网址&#xff1a;Lottie 将来用到的lottie组件库网址&#xff1a; Vue3-lottie 我目前用的第二个&#xff1a; Vue3-lottiejs 1. 我在引入Lottie的时…

【Python机器学习】决策树与随机森林的讲解及决策树在决策决策问题中实战(图文解释 附源码)

需要源码请点赞关注收藏后评论区留言私信~~~ 在生活中人们经常应用决策树的思想来做决定 分类的建模过程与上面做决定的过程相反&#xff0c;事先不知道人们的决策思路&#xff0c;需要通过人们已经做出的大量决定来“揣摩”出其决策思路&#xff0c;也就是通过大量数据来归纳道…

嵌入式分享合集124

一、19个常用的5V转3.3V技巧 01 使用LDO稳压器 标准三端线性稳压器的压差通常是 2.0-3.0V。要把 5V 可靠地转换为 3.3V&#xff0c;就不能使用它们。压差为几百个毫伏的低压降 &#xff08;Low Dropout&#xff0c; LDO&#xff09;稳压器&#xff0c;是此类应用的理想选择。图…

常见的CSS布局方法

常见的CSS布局方法 「1. 单栏布局」 常见的单列布局有两种: header,content 和 footer 等宽的单列布局header 与 footer 等宽,content 略窄的单列布局header,content 和 footer 等宽的单列布局 ​ 先通过对 header,content,footer 统一设置 width:1000px;或者 max-width:1…

DHCP报文

一. 介绍 DHCP&#xff08;Dynamic Host Configuration Protocol&#xff0c;动态主机配置协议&#xff09;是一个局域网的网络协议&#xff0c;使用UDP协议工作&#xff0c;统一使用两个IANA分配的端口&#xff1a;67&#xff08;服务器端&#xff09;&#xff0c;68&#xff…

Django学习Day5

由于前两天核酸阳的&#xff0c;一直发烧&#xff0c;故没有学习&#xff0c;csdn也没有进行更新。今天身体基本恢复&#xff0c;继续Django的学习旅程。也希望各位读者重视个人的身体健康&#xff0c;做好自己健康的第一负责人。 1.关于针对模型类的数据库修改方法补充 在mo…

二苯基环辛炔-氨基;DBCO-NH2科研实验用试剂DBCO-Amine;CAS:1255942-06-3

英文名称&#xff1a;DBCO-Amine DBCO-NH2 中文名称&#xff1a;二苯基环辛炔-氨基 CAS&#xff1a;1255942-06-3 分子式&#xff1a;C18H16N2 分子量&#xff1a;276.3 外观&#xff1a;固体粉末 溶剂&#xff1a;溶于 DMSO, DMF, DCM, THF, Chloroform 储存条件&…

什么是容器安全性,您如何提升自己的安全性?

容器无疑已成为部署应用程序的流行方式。这很棒&#xff0c;因为与部署到虚拟机相比&#xff0c;它们具有大量优势。其中一些优点包括便携、不可变和轻量级。您可以控制运行服务的容器内部的内容&#xff0c;这可以产生清晰、可审计的跟踪。 对于安全专业人员来说&#xff0c;…

模型复杂度与硬件性能的衡量

1. 模型复杂度的衡量 参数数量&#xff08;Params&#xff09;&#xff1a;指模型含有多少参数&#xff0c;直接决定模型的大小&#xff0c;也影响推断时对内存的占用量 单位通常为 M&#xff0c;通常参数用 float32 表示&#xff0c;所以模型大小是参数数量的 4 倍左右参数数…

数据结构C语言版 —— 树和二叉树的概念

树和二叉树 一、树 1. 树的概念 树(Tree)是n(n>0)n(n>0)n(n>0)个节点的有限集&#xff0c;在任意一颗非空树中&#xff1a; (1) 有且仅有一个特定的称为根(Root)的节点&#xff0c;根节点是没有前驱节点的。 &#xff08;2&#xff09;当 n>1n > 1n>1时…

_11LeetCode代码随想录算法训练营第十一天-C++队列的应用

_11LeetCode代码随想录算法训练营第十一天-C队列的应用 239.滑动窗口最大值347.前K个高频元素 239.滑动窗口最大值 整体思路 要实现一个单调递减队列&#xff1a; 对于滑动窗口的滑动&#xff0c;移除前面的元素&#xff0c;加入后面的元素。当移除前面的元素时&#xff0…