SpringBoot + 通义千问 + 自定义React组件,支持EventStream数据解析!

news2024/12/31 4:59:45

一、前言

大家好!我是sum墨,一个一线的底层码农,平时喜欢研究和思考一些技术相关的问题并整理成文,限于本人水平,如果文章和代码有表述不当之处,还请不吝赐教。

最近ChatGPT非常受欢迎,尤其是在编写代码方面,我每天都在使用。随着使用时间的增长,我开始对其原理产生了一些兴趣。虽然我无法完全理解这些AI大型模型的算法和模型,但我认为可以研究一下其中的交互逻辑。特别是,我想了解它是如何实现在发送一个问题后不需要等待答案完全生成,而是通过不断追加的方式实现实时回复的。

F12打开控制台后,我发现在点击发送后,它会发送一个普通的请求。但是回复的方式却不同,它的类型是eventsource。一次请求会不断地获取数据,然后前端的聊天组件会动态地显示回复内容,回复的内容是用Markdown格式来展示的。

在了解了前面的这些东西后我就萌生了自己写一个小demo的想法。起初,我打算使用openai的接口,并写一个小型的UI组件。然而,由于openai账号申请复杂且存在网络问题,很多人估计搞不定,所以我最终选择了通义千问。通义千问有两个优点:一是它是国内的且目前调用是免费的,二是它提供了Java-SDK和API文档,开发起来容易。

作为后端开发人员,按照API文档调用模型并不难,但真正难到我的是前端UI组件的编写。我原以为市面上会有很多支持EventStream的现成组件,但事实上并没有。不知道是因为这个功能太容易还是太难,总之,对接通义千问只花了不到一小时,而编写一个UI对话组件却花了整整两天的时间!接下来,我将分享一些我之前的经验,希望可以帮助大家少走坑。

首先展示一下我的成品效果
在这里插入图片描述

二、通义千问开发Key申请

1. 登录阿里云,搜索通义千问

2. 点击"开通DashScope"

3. 创建一个API-KEY

4. 对接流程

(1)API文档地址

https://help.aliyun.com/zh/dashscope/developer-reference/api-details

(2)Java-SDK依赖

<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>dashscope-sdk-java</artifactId>
  <version>2.8.2</version>
</dependency>

三、支持EventStream格式的接口

1. 什么是EventStream

EventStream是一种流式数据格式,用于实时传输事件数据。它是基于HTTP协议的,但与传统的请求-响应模型不同,它是一个持续的、单向的数据流。它可用于推送实时数据、日志、通知等,所以EventStream很适合这种对话式的场景。在Spring Boot中,主要有以下框架和模块支持EventStream格式:

  • Spring WebFlux:Spring WebFlux是Spring框架的一部分,用于构建反应式Web应用程序。
  • Reactor:Reactor是一个基于响应式流标准的库,是Spring WebFlux的核心组件。
  • Spring Cloud Stream:Spring Cloud Stream是一个用于构建消息驱动的微服务应用的框架。

这次我使用的是reactor-core框架。

2. 写一个例子

maven依赖

<!-- Reactor Core -->
<dependency>
  <groupId>io.projectreactor</groupId>
  <artifactId>reactor-core</artifactId>
  <version>3.4.6</version>
</dependency>

代码如下

import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;

import java.time.Duration;
import java.time.LocalTime;

@RestController
@RequestMapping("/event-stream")
public class EventStreamController {

    @GetMapping(produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<String> getEventStream() {
        return Flux.interval(Duration.ofSeconds(1))
                .map(sequence -> "Event " + sequence + " at " + LocalTime.now());
    }
}

调用一下接口后就可以看到浏览器上在不断地打印时间戳了

四、项目实现

这个就不BB了,直接贴代码!

1. 项目结构

2. pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.17</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.chatrobot</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <!-- 通义千问SDK -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>dashscope-sdk-java</artifactId>
            <version>2.8.2</version>
        </dependency>

        <!-- Reactor Core -->
        <dependency>
            <groupId>io.projectreactor</groupId>
            <artifactId>reactor-core</artifactId>
            <version>3.4.6</version>
        </dependency>

        <!-- Web组件 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <exclusions>
                <exclusion>
                    <artifactId>logback-classic</artifactId>
                    <groupId>ch.qos.logback</groupId>
                </exclusion>
            </exclusions>
        </dependency>

    </dependencies>

</project>

3. 代码

(1)后端代码

DemoApplication.java
package com.chatrobot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

}
EventController.java
package com.chatrobot.controller;

import java.time.Duration;
import java.time.LocalTime;
import java.util.Arrays;

import com.alibaba.dashscope.aigc.generation.Generation;
import com.alibaba.dashscope.aigc.generation.GenerationResult;
import com.alibaba.dashscope.aigc.generation.models.QwenParam;
import com.alibaba.dashscope.common.Message;
import com.alibaba.dashscope.common.Role;
import com.alibaba.dashscope.exception.ApiException;
import com.alibaba.dashscope.exception.InputRequiredException;
import com.alibaba.dashscope.exception.NoApiKeyException;

import io.reactivex.Flowable;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;

@RestController
@RequestMapping("/events")
@CrossOrigin
public class EventController {

    @Value("${api.key}")
    private String apiKey;

    @GetMapping(value = "/streamAsk", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<ServerSentEvent<String>> streamAsk(String q) throws Exception {

        Generation gen = new Generation();

        // 创建用户消息对象
        Message userMsg = Message
            .builder()
            .role(Role.USER.getValue())
            .content(q)
            .build();

        // 创建QwenParam对象,设置参数
        QwenParam param = QwenParam.builder()
            .model(Generation.Models.QWEN_PLUS)
            .messages(Arrays.asList(userMsg))
            .resultFormat(QwenParam.ResultFormat.MESSAGE)
            .topP(0.8)
            .enableSearch(true)
            .apiKey(apiKey)
            // get streaming output incrementally
            .incrementalOutput(true)
            .build();

        // 调用生成接口,获取Flowable对象
        Flowable<GenerationResult> result = gen.streamCall(param);

        // 将Flowable转换成Flux<ServerSentEvent<String>>并进行处理
        return Flux.from(result)
            // add delay between each event
            .delayElements(Duration.ofMillis(1000))
            .map(message -> {
                String output = message.getOutput().getChoices().get(0).getMessage().getContent();
                System.out.println(output); // print the output
                return ServerSentEvent.<String>builder()
                    .data(output)
                    .build();
            })
            .concatWith(Flux.just(ServerSentEvent.<String>builder().comment("").build()))
            .doOnError(e -> {
                if (e instanceof NoApiKeyException) {
                    // 处理 NoApiKeyException
                } else if (e instanceof InputRequiredException) {
                    // 处理 InputRequiredException
                } else if (e instanceof ApiException) {
                    // 处理其他 ApiException
                } else {
                    // 处理其他异常
                }
            });
    }

    @GetMapping(value = "test", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<String> testEventStream() {
        return Flux.interval(Duration.ofSeconds(1))
            .map(sequence -> "Event " + sequence + " at " + LocalTime.now());
    }
}

(2)前端代码

chat.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>ChatBot</title>
    <style>
        body {
            background: #f9f9f9;
            /* 替换为您想要的背景颜色或图片 */
        }

        .chat-bot {
            display: flex;
            flex-direction: column;
            width: 100%;
            max-width: 800px;
            margin: 50px auto;
            box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
            border-radius: 8px;
            overflow: hidden;
            font-family: "Roboto", sans-serif;
            background: #f5f5f5;
        }

        .chat-bot-header {
            background: linear-gradient(to right, #1791ee, #9fdbf1);
            color: white;
            text-align: center;
            padding: 15px;
            font-size: 24px;
            font-weight: 500;
        }

        .chat-bot-messages {
            flex: 1;
            padding: 20px;
            min-height: 400px;
            overflow-y: auto;
        }

        .userName {
            margin: 0 10px;
        }

        .message-wrapper {
            display: flex;
            align-items: flex-start;
            margin-bottom: 10px;
            border-radius: 20px;
        }

        .message-wrapper.user {
            justify-content: flex-end;
            border-radius: 20px;
        }

        .message-avatar {
            width: 30px;
            height: 30px;
            border-radius: 50%;
            background-color: #ccc;
            margin-right: 10px;
            margin-bottom: 10px;
            /* 添加这一行 */
            order: -1;
            /* 添加这一行 */
            text-align: right;
        }

        .message-avatar.user {
            background-color: transparent;
            display: flex;
            justify-content: flex-end;
            width: 100%;
            margin-right: 0;
            align-items: center;
        }

        .message-avatar.bot {
            background-color: transparent;
            display: flex;
            justify-content: flex-start;
            width: 100%;
            margin-right: 0;
            align-items: center;
        }

        .message-avatar-inner.user {
            background-image: url("./luge.jpeg");
            background-size: cover;
            background-position: center;
            width: 30px;
            height: 30px;
            border-radius: 50%;
        }

        .message-avatar-inner.bot {
            background-image: url("./logo.svg");
            background-size: cover;
            background-position: center;
            width: 30px;
            height: 30px;
            border-radius: 50%;
        }

        .message {
            padding: 10px 20px;
            border-radius: 15px;
            font-size: 16px;
            background-color: #d9edf7;
            order: 1;
            /* 添加这一行 */
        }

        .bot {
            background-color: #e9eff5;
            /* 添加这一行 */
        }

        .user {
            background-color: #d9edf7;
            color: #111111;
            order: 1;
            /* 添加这一行 */
        }

        .chat-bot-input {
            display: flex;
            align-items: center;
            border-top: 1px solid #ccc;
            padding: 10px;
            background-color: #fff;
        }

        .chat-bot-input input {
            flex: 1;
            padding: 10px 15px;
            border: none;
            font-size: 16px;
            outline: none;
        }

        .chat-bot-input button {
            padding: 10px 20px;
            background-color: #007bff;
            border: none;
            border-radius: 50px;
            color: white;
            font-weight: 500;
            cursor: pointer;
            transition: background-color 0.3s;
        }

        .chat-bot-input button:hover {
            background-color: #0056b3;
        }

        @media (max-width: 768px) {
            .chat-bot {
                margin: 20px;
            }

            .chat-bot-header {
                font-size: 20px;
            }

            .message {
                font-size: 14px;
            }
        }

        @keyframes spin {
            0% {
                transform: rotate(0deg);
            }
            100% {
                transform: rotate(360deg);
            }
        }

        .loading-spinner {
            width: 15px;
            height: 15px;
            border-radius: 50%;
            border: 2px solid #d9edf7;
            border-top-color: transparent;
            animation: spin 1s infinite linear;
        }
    </style>
</head>
<body>
<div class="chat-bot">
    <div class="chat-bot-header">
        <img src="./logo.svg" alt="Logo" class="logo" />
        通义千问
    </div>
    <div class="chat-bot-messages"></div>
    <div class="chat-bot-input">
        <input type="text" placeholder="输入你想问的问题" />
        <button id="sendButton">Send</button>
    </div>
</div>
<script
        src="https://cdnjs.cloudflare.com/ajax/libs/markdown-it/13.0.2/markdown-it.min.js"
        integrity="sha512-ohlWmsCxOu0bph1om5eDL0jm/83eH09fvqLDhiEdiqfDeJbEvz4FSbeY0gLJSVJwQAp0laRhTXbUQG+ZUuifUQ=="
        crossorigin="anonymous"
        referrerpolicy="no-referrer"
></script>
<script>
    const userName = "summo";

    document.addEventListener("DOMContentLoaded", function () {
        const input = document.querySelector(".chat-bot-input input");
        const messagesContainer = document.querySelector(".chat-bot-messages");
        const sendButton = document.getElementById("sendButton");

        function appendToMessage(messageTxt, sender, md, message) {
            let messageElement = messagesContainer.querySelector(
                `.message-wrapper.${sender}:last-child .message`
            );

            if (!messageElement) {
                if (sender === "bot") {
                    messageElement = document.createElement("div");
                    messageElement.classList.add("message-avatar", sender);
                    messageElement.innerHTML = `<div class="message-avatar-inner ${sender}"></div><div class="userName">通义千问</div>`;
                    messagesContainer.appendChild(messageElement);
                } else {
                    messageElement = document.createElement("div");
                    messageElement.classList.add("message-avatar", sender);
                    messageElement.innerHTML = `<div class="message-avatar-inner ${sender}"></div><div class="userName"">${userName}</div>`;
                    messagesContainer.appendChild(messageElement);
                }
                messageElement = document.createElement("div");
                messageElement.classList.add("message-wrapper", sender);
                messageElement.innerHTML = `<div class="message ${sender}"></div>`;
                messagesContainer.appendChild(messageElement);

                messageElement = messageElement.querySelector(".message");
            }
            // messageElement.textContent += messageTxt; // 追加文本
            // messagesContainer.scrollTop = messagesContainer.scrollHeight; // 滚动到底部
            let result = (message += messageTxt);
            const html = md.renderInline(messageTxt);
            messageElement.innerHTML += html;
            messagesContainer.scrollTop = messagesContainer.scrollHeight;
        }

        function handleSend() {
            const inputValue = input.value.trim();
            if (inputValue) {
                input.disabled = true;
                sendButton.disabled = true;
                sendButton.innerHTML = '<div class="loading-spinner"></div>';
                const md = new markdownit();
                // 修改按钮文本内容为"Loading..."
                let message = "";
                appendToMessage(inputValue, "user", md, message);
                input.value = "";
                const eventSource = new EventSource(
                    `http://localhost:8080/events/streamAsk?q=${encodeURIComponent(
                        inputValue
                    )}`
                );
                eventSource.onmessage = function (event) {
                    console.log(event.data);
                    appendToMessage(event.data, "bot", md, message);
                };
                eventSource.onerror = function () {
                    eventSource.close();
                    input.disabled = false;
                    sendButton.disabled = false;
                    sendButton.innerHTML = "Send";
                };
            }
        }

        document
            .querySelector(".chat-bot-input button")
            .addEventListener("click", handleSend);
        input.addEventListener("input", function () {
            sendButton.disabled = input.value.trim() === "";
        });

        input.addEventListener("keypress", function (event) {
            if (event.key === "Enter" && !sendButton.disabled) {
                handleSend();
            }
        });
    });
</script>
</body>
</html>

另外还有两个头像,大家可以替换成自己喜欢的,好了文章到这里也就结束了,再秀一下我的成品👉

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

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

相关文章

接口传参数list的时候,items里面放个​​​​​​​list

item里面放个list 先定义一个 list&#xff0c;循环add加入参数

如何用CHAT解释文章含义?

问CHAT&#xff1a;解释“ 本身乐善好施&#xff0c;令名远近共钦&#xff0c;待等二十左右&#xff0c;定有高亲可攀&#xff1b;而且四德俱备&#xff0c;帮夫之缘亦有。主持家事不紊&#xff0c;上下亦无闲言。但四十交进&#xff0c;家内谨防口舌&#xff0c;须安家堂&…

基于C#实现Bitmap算法

在所有具有性能优化的数据结构中&#xff0c;我想大家使用最多的就是 hash 表&#xff0c;是的&#xff0c;在具有定位查找上具有 O(1)的常量时间&#xff0c;多么的简洁优美&#xff0c;但是在特定的场合下&#xff1a; ①&#xff1a;对 10 亿个不重复的整数进行排序。 ②&am…

2023年度openGauss标杆应用实践案例征集

标杆应用实践案例征集 2023 openGauss 数据库作为企业IT系统的核心组成部分&#xff0c;是数字基础设施建设的关键&#xff0c;是实现数据安全稳定的保障。openGauss顺应开源发展趋势&#xff0c;强化核心技术突破&#xff0c;着力打造自主根社区&#xff0c;携手产业伙伴共同…

手把手教你通过CODESYS V3进行PLC编程(二)

教程背景 在上一期教程中&#xff0c;我们已经完成了控制器设备的连接和配置。接下来的教程将继续以宏集MC-Prime为例&#xff0c;假设控制器已经配置并连接到开发者的PC上&#xff0c;为您演示如何为控制器安装合适的CODESYS V3版本并创建第一个程序。 一、安装CODESYS &…

冷链运输车辆GPS定位及温湿度管理案例

1.项目背景 项目名称&#xff1a;山西冷链运输车辆GPS定位及温湿度管理案例 项目需求&#xff1a;随着经济发展带动物流行业快速发展&#xff0c;运输规模逐步扩大&#xff0c;集团为了适应高速发展的行业现象&#xff0c;物流管理系统的完善成了现阶段发展的重中之重。因此&…

FDG6306P PowerTrench® MOSFET P沟道 特点及其应用详解

关于PowerTrench MOSFET&#xff1f; 它是一种MOS场效应晶体管&#xff0c;可以提高系统效率和功率密度。该技术采用了屏蔽栅极技术&#xff0c;可以减少开关损耗和导通损耗&#xff0c;从而提高了系统效率。此外&#xff0c;PowerTrench MOSFET还具有低导通电阻和高开关速度的…

史上最细教程-一台服务器上搭建2个MySQL实例

史上最细教程-一台服务器上搭建2个MySQL实例 文章目录 史上最细教程-一台服务器上搭建2个MySQL实例环境准备&#xff1a;操作步骤&#xff1a;1.安装MySQL2.配置搭建3306、3307实例3.初始化3306、3307实例、远程连接访问支持 环境准备&#xff1a; 服务器&#xff1a;阿里云Ce…

斯坦福大学引入FlashFFTConv来优化机器学习中长序列的FFT卷积

斯坦福大学的FlashFFTConv优化了扩展序列的快速傅里叶变换(FFT)卷积。该方法引入Monarch分解&#xff0c;在FLOP和I/O成本之间取得平衡&#xff0c;提高模型质量和效率。并且优于PyTorch和FlashAttention-v2。它可以处理更长的序列&#xff0c;并在人工智能应用程序中打开新的可…

【网络】数据链路层协议

数据链路层协议 一、链路层解决的问题二、以太网协议1、局域网技术2、令牌环网&#xff08;了解&#xff09;3、以太网通信原理4、 MAC地址5、以太网帧格式6、碰撞避免7、最大传输单元MTU 二、ARP协议1、ARP数据的格式2、ARP协议的工作流程3、ARP缓存表4、ARP协议中的一些问题7…

力控软件与多台PLC之间ModbusTCP/IP无线通信

Modbus TCP/IP 是对成熟的 Modbus 协议的改编&#xff0c; 因其开放性、简单性和广泛接受性而在工业自动化系统中发挥着举足轻重的作用。它作为连接各种工业设备的通用通信协议&#xff0c;包括可编程逻辑控制器 (PLC)、远程终端单元 (RTU) 和传感器。它提供标准化的 TCP 接口&…

从零开始,用Docker-compose打造SkyWalking、Elasticsearch和Spring Cloud的完美融合

&#x1f38f;&#xff1a;你只管努力&#xff0c;剩下的交给时间 &#x1f3e0; &#xff1a;小破站 "从零开始&#xff0c;用Docker-compose打造SkyWalking、Elasticsearch和Spring Cloud的完美融合 前言准备工作编写docker-compose.yml文件为什么使用本机ip为什么skywa…

VL06O报表添加增强字段

业务描述 用户需要在VL06O事务代码下进行批量交货过账&#xff0c;现有的筛选条件不太适用当前公司的业务&#xff0c;需要在报表中新增三个交货单增强字段&#xff0c;方便其筛选&#xff08;选择屏幕没有加&#xff0c;用户在报表里用标准按钮功能自己筛选&#xff09; 效果…

百战python01-初识python_turtle绘图

文章目录 简介练习1.简易的进度条学习使用turtle在屏幕上绘制图形注:需要对python有基本了解,可查看本作者python基础专栏,有任何问题欢迎私信或评论(本专栏每章内容都将不定期进行内容扩充与更新) 简介 python简介及方向+pycharm安装使用请转 练习 注:尝试练习。了解…

视频云存储EasyCVR平台国标接入获取通道设备未回复是什么原因?该如何解决?

安防视频监控/视频集中存储/云存储/磁盘阵列EasyCVR平台可拓展性强、视频能力灵活、部署轻快&#xff0c;可支持的主流标准协议有国标GB28181、RTSP/Onvif、RTMP等&#xff0c;以及支持厂家私有协议与SDK接入&#xff0c;包括海康Ehome、海大宇等设备的SDK等。平台既具备传统安…

Windows平台Unity下实现camera场景推送RTMP|轻量级RTSP服务|实时录像

技术背景 我们在对接Unity平台camera场景采集的时候&#xff0c;除了常规的RTMP推送、录像外&#xff0c;还有一些开发者&#xff0c;需要能实现轻量级RTSP服务&#xff0c;对外提供个拉流的RTSP URL。 目前我们在Windows平台Unity下数据源可采集到以下部分&#xff1a; 采集…

如何满足BMW EDI项目的PKT需求?

近期宝马BMW&#xff08;以下简称BMW&#xff09;在其部分供应商之间试点推进PKT项目&#xff0c;BMW为什么要启动 PKT 计划呢&#xff1f; 业务系统全面升级统一全球所有宝马工厂的流程 宝马内部的物流供货流程 近期BMW PKT需求主要针对其内部物流供货流程展开&#xff1a; …

linux下流媒体压力测试工具的使用

前言 因为领导要求做linux的推拉流时服务器压力测试&#xff0c;于是在网上找了找。一顿操作下来&#xff0c;发现很多软件盗用一款名为srs-bench的开源软件。 该代码仓库有详细的使用说明&#xff0c;而且可以在issues中找到可能会遇到的问题的解决办法 需要下载该仓库的源…

网页小游戏的开发流程

网页小游戏的开发流程可以分为几个关键步骤。这只是一个一般性的流程概述&#xff0c;具体的步骤可能会根据项目的规模和要求而有所不同。此外&#xff0c;还要考虑法律和版权问题&#xff0c;确保你的游戏开发过程是合法的。下面是一个简要的概述&#xff0c;希望对大家有所帮…

Centos Download

前言 CentOS Linux 是一个社区支持的发行版&#xff0c;源自 CentOS git for Red Hat Enterprise Linux &#xff08;RHEL&#xff09; 上免费提供给公众的源代码。因此&#xff0c;CentOS Linux 的目标是在功能上与 RHEL 兼容。CentOS 计划主要更改组件以删除上游供应商的品牌…