Bootstrap

若依前后端分离版集成AI(通义千问)

1.前言

本文主要介绍如何在若依框架前后端分离版实现AI对话。由于目前AI非常多,我以通义千问为例进行介绍。文章会详细介绍前后端如何进行代码设计,后端调用AI接口后,采用推流的方式返回前端,并且将对话内容保存在数据库中。并不会和现在大多数文章一样,前端介绍草草结束,而是设计了完整的对话窗口,并且完成了流数据拼接和格式转换。也就是说,哪怕不用通义千问这个AI,也可以将前端页面集成到自己的系统里面。但是需要注意点是,我只集成了单轮对话,并且通过文生文的方式进调用。对于多轮对话和上传文件等功能没有实现,但是在对话窗口中预留了文件上传功能,能够正常保存,只是没有实现此功能罢了。

2.后端配置

1.对于简单的若依框架如何启动和配置,我这里就不介绍了,如果有需要可以查看我的主页文章。需要去阿里云获取通义千问的应用id和key,具体可以去阿里云官网查看首次调用通义千问API

2.根据下面的sql,生成表py_message。

CREATE TABLE `py_message` (
  `id` varchar(32) CHARACTER SET utf8 NOT NULL COMMENT 'id',
  `owner` varchar(30) CHARACTER SET utf8 DEFAULT NULL COMMENT '所属人',
  `file_path` varchar(511) CHARACTER SET utf8 DEFAULT NULL COMMENT '文件路径',
  `bot_flag` char(1) CHARACTER SET utf8 DEFAULT '0' COMMENT '是否为机器人消息(0代表否,2代表是)',
  `text` text CHARACTER SET utf8 COMMENT '内容',
  `del_flag` char(1) CHARACTER SET utf8 DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)',
  `create_by` varchar(64) CHARACTER SET utf8 DEFAULT '' COMMENT '创建者',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_by` varchar(64) CHARACTER SET utf8 DEFAULT '' COMMENT '更新者',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  `remark` varchar(500) CHARACTER SET utf8 DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB COMMENT='消息信息表';

3.表单操作我使用的mybatis-plus,如果没有集成,可以查看我的文章《若依前后端分离版创建Mybatis-Plus代码生成器》。如果已经根据我的文章集成了mybatis-plus代码生成器,需要修改的地方很少。为了兼顾所有的小伙伴,我会介绍每个类。打开src/main/resources/application.yml文件,替换成自己的通义千问应用的id和key。

#ai配置
ai:
  # 通义千问配置
  tyQw:
    id: 
    key: 

4.根据包和类结构,将下面代码替换到对应位置的类中。

4.1src/main/java/com/ruoyi/domain/BasePojo.java代码如下:

package com.ruoyi.domain;


import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;

import java.util.Date;
@Data
public class BasePojo {
    //创建者id
    @TableField("create_by")
    private String createBy;

    //创建时间
    @TableField("create_time")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date createTime;

    //更新者id
    @TableField("update_by")
    private String updateBy;

    //更新时间
    @TableField("update_time")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date updateTime;

    //是否可用,2代表可用,0代表删除
    @TableField("del_flag")
    @TableLogic(value = "0", delval = "2")
    private String delFlag;
}

4.2src/main/java/com/ruoyi/util/ConfigUtil.java代码如下:

package com.ruoyi.util;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;

/**
 * @author py
 * @date 2025/3/10 16:51
 * @description 由于静态变量无法获取配置中的,通过此类中转下
 */
@Component
public class ConfigUtil {
    // 通义千问id
    @Value("${ai.tyQw.id}")
    private String tyQwId;

    // 通义千问key
    @Value("${ai.tyQw.key}")
    private String tyQwKey;

    // 类注入容器时执行
    @PostConstruct
    public void init() {
        AiUtil.init(tyQwId, tyQwKey);
    }

}

4.3src/main/java/com/ruoyi/util/AiUtil.java代码如下:

package com.ruoyi.util;

import com.alibaba.dashscope.app.Application;
import com.alibaba.dashscope.app.ApplicationParam;
import com.alibaba.dashscope.app.ApplicationResult;
import com.alibaba.dashscope.exception.InputRequiredException;
import com.alibaba.dashscope.exception.NoApiKeyException;
import io.reactivex.Flowable;


/**
 * @author py
 * @date 2025/3/10 15:59
 * @description ai工具类
*/
public class AiUtil {
    // 通义千问id
    private static String tyQwId;

    // 通义千问key
    private static String tyQwKey;

    // 获取初始值
    public static void init(String tyQwId, String tyQwKey) {
        AiUtil.tyQwId = tyQwId;
        AiUtil.tyQwKey = tyQwKey;
    }

    /**
     * 获取通义千问消息
     * @param prompt    提示词
     * @return
     * @throws NoApiKeyException
     * @throws InputRequiredException
     */
    public static String getTyQwMessage(String prompt) throws NoApiKeyException, InputRequiredException {
        ApplicationParam param = ApplicationParam.builder()
                .apiKey(tyQwKey)
                .appId(tyQwId)
                .prompt(prompt)
                .build();

        Application application = new Application();
        ApplicationResult result = application.call(param);
        return result.getOutput().getText();
    }

    public static Flowable<ApplicationResult> getTyQwFluxMessage(String prompt) throws NoApiKeyException, InputRequiredException {
        ApplicationParam param = ApplicationParam.builder()
                .apiKey(tyQwKey)
                .appId(tyQwId)
                .prompt(prompt)
                .incrementalOutput(true)
                .build();

        Application application = new Application();
        Flowable<ApplicationResult> result = application.streamCall(param);
        return result;
    }




}

4.4src/main/java/com/ruoyi/util/QueryWrapperUtil.java代码如下:

package com.ruoyi.util;

import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.ruoyi.domain.BasePojo;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Map;

public class QueryWrapperUtil {
    public static <T extends BasePojo> QueryWrapper<T> getWrapper(T entity) {
        QueryWrapper<T> wrapper = new QueryWrapper<>();
        Class<? extends BasePojo> entityClass = entity.getClass();
        Field[] declaredFields = getAllDeclaredFields(entityClass);
        for (Field declaredField : declaredFields) {
            String name = declaredField.getName();
            if (!name.equals("serialVersionUID")) {
                String getName = StrUtil.upperFirstAndAddPre(name, "get");
                Method method = null;
                try {
                    method = entityClass.getMethod(getName);
                    Object obj = method.invoke(entity);
                    String column = StrUtil.toSymbolCase(name, '_');
                    wrapper.eq(ObjectUtil.isNotEmpty(obj), column, obj);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }

        }
        return wrapper;
    }

    public static <T extends BasePojo> QueryWrapper<T> getWrapper(T entity, Map<String, String> map) {
        QueryWrapper<T> wrapper = new QueryWrapper<>();
        Class<? extends BasePojo> entityClass = entity.getClass();
        Field[] declaredFields = getAllDeclaredFields(entityClass);
        for (Field declaredField : declaredFields) {
            String name = declaredField.getName();
            if (!name.equals("serialVersionUID")) {
                String getName = StrUtil.upperFirstAndAddPre(name, "get");
                Method method = null;
                try {
                    method = entityClass.getMethod(getName);
                    Object obj = method.invoke(entity);
                    String column = StrUtil.toSymbolCase(name, '_');
                    String mybatisValue = map.get(name);
                    if (StrUtil.isNotEmpty(mybatisValue)) {
                        switch (mybatisValue) {
                            case "isNull":
                                wrapper.isNull(column);
                                break;
                            case "isNotNull":
                                wrapper.isNotNull(column);
                                break;
                            case "eq":
                                wrapper.eq(ObjectUtil.isNotEmpty(obj), column, obj);
                                break;
                            case "ne":
                                wrapper.ne(ObjectUtil.isNotEmpty(obj), column, obj);
                                break;
                            case "like":
                                wrapper.like(ObjectUtil.isNotEmpty(obj), column, obj);
                                break;
                            case "notLike":
                                wrapper.notLike(ObjectUtil.isNotEmpty(obj), column, obj);
                                break;
                            case "likeRight":
                                wrapper.likeRight(ObjectUtil.isNotEmpty(obj), column, obj);
                                break;
                            case "likeLeft":
                                wrapper.likeLeft(ObjectUtil.isNotEmpty(obj), column, obj);
                                break;
                            case "lt":
                                wrapper.lt(ObjectUtil.isNotEmpty(obj), column, obj);
                                break;
                            case "le":
                                wrapper.le(ObjectUtil.isNotEmpty(obj), column, obj);
                                break;
                            case "gt":
                                wrapper.gt(ObjectUtil.isNotEmpty(obj), column, obj);
                                break;
                            case "ge":
                                wrapper.ge(ObjectUtil.isNotEmpty(obj), column, obj);
                                break;
                            case "orderByAsc":
                                wrapper.orderByAsc(column);
                                break;
                            case "orderByDesc":
                                wrapper.orderByDesc(column);
                                break;
                            default:
                                break;
                        }
                    }

                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }

        }
        return wrapper;
    }

    // 获取当前类及其父类的所有字段
    public static Field[] getAllDeclaredFields(Class<?> clazz) {
        if (clazz == null) {
            return new Field[0]; // 没有父类时返回空数组
        }

        // 获取当前类的所有声明字段
        Field[] fields = clazz.getDeclaredFields();

        // 获取父类的字段并与当前类的字段合并
        Field[] parentFields = getAllDeclaredFields(clazz.getSuperclass());

        // 合并当前类字段与父类字段
        Field[] allFields = new Field[fields.length + parentFields.length];
        System.arraycopy(fields, 0, allFields, 0, fields.length);
        System.arraycopy(parentFields, 0, allFields, fields.length, parentFields.length);

        return allFields;
    }
}


注意:这个查询工具类,进行了修改。以前的查询工具类无法匹配继承的属性,现在的工具类是可以匹配继承的属性,具体可以查看我的文章《若依前后端分离版创建Mybatis-Plus代码生成器》。

4.5src/main/java/com/ruoyi/data/message/domain/PyMessage.java代码如下:

package com.ruoyi.data.message.domain;

import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.ruoyi.domain.BasePojo;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Data;
import lombok.EqualsAndHashCode;

/**
 * @author py
 * @date 2025-03-08
 * @description 消息信息的基础类,对应数据库py_message表中的数据
 */
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("py_message")
public class PyMessage extends BasePojo {

    private static final long serialVersionUID = 1L;

    // id
    @TableId(value = "id", type = IdType.ASSIGN_UUID)
    private String id;

    // 所属人
    @TableField("owner")
    private String owner;

    // 是否为机器人消息(0代表否,2代表是)
    @TableField("bot_flag")
    private String botFlag;

    // 文件路径
    @TableField("file_path")
    private String filePath;

    // 内容
    // 不让springboot转义,防止html标签丢失
    @TableField("text")
    private String text;

    // 备注
    @TableField("remark")
    private String remark;

}

4.6src/main/java/com/ruoyi/data/message/mapper/PyMessageMapper.java代码如下:

package com.ruoyi.data.message.mapper;

import com.ruoyi.data.message.domain.PyMessage;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;

/**
 * @author py
 * @date 2025-03-08
 * @description 消息信息对应的mapper
 */
public interface PyMessageMapper extends BaseMapper<PyMessage> {

}

4.7src/main/java/com/ruoyi/data/message/service/PyMessageService.java代码如下:

package com.ruoyi.data.message.service;

import com.alibaba.dashscope.exception.InputRequiredException;
import com.alibaba.dashscope.exception.NoApiKeyException;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.data.message.domain.PyMessage;
import com.baomidou.mybatisplus.extension.service.IService;
import reactor.core.publisher.Flux;

/**
 * @author py
 * @date 2025-03-08
 * @description 消息信息对应的Service层接口
 */
public interface PyMessageService extends IService<PyMessage> {
    AjaxResult sendMessageAndGetJsonMessage(PyMessage pyMessage);

    Flux<String> sendMessageAndGetFluxMessage(PyMessage pyMessage) throws NoApiKeyException, InputRequiredException;

}

4.8src/main/java/com/ruoyi/data/message/service/impl/PyMessageServiceImpl.java代码如下:

package com.ruoyi.data.message.service.impl;

import com.alibaba.dashscope.app.ApplicationResult;
import com.alibaba.dashscope.exception.InputRequiredException;
import com.alibaba.dashscope.exception.NoApiKeyException;

import com.ruoyi.common.core.domain.AjaxResult;

import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.data.message.domain.PyMessage;
import com.ruoyi.data.message.mapper.PyMessageMapper;
import com.ruoyi.data.message.service.PyMessageService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;

import com.ruoyi.util.AiUtil;

import io.reactivex.Flowable;

import org.springframework.stereotype.Service;

import reactor.core.publisher.Flux;
import reactor.core.scheduler.Schedulers;



import java.util.ArrayList;
import java.util.Date;


/**
 * @author py
 * @date 2025-03-08
 * @description 消息信息对应的Service层实现类
 */
@Service("pyMessageService")
public class PyMessageServiceImpl extends ServiceImpl<PyMessageMapper, PyMessage> implements PyMessageService {

    /**
     * 发送消息,并且获取json消息
     *
     * @param pyMessage 消息
     * @return 获取的json消息
     */
    @Override
    public AjaxResult sendMessageAndGetJsonMessage(PyMessage pyMessage) {
        ArrayList<PyMessage> list = new ArrayList<>();
        list.add(pyMessage);
        // 机器人消息
        PyMessage botMessage = new PyMessage();
        botMessage.setOwner(pyMessage.getOwner());
        botMessage.setCreateBy(pyMessage.getCreateBy());
        Date date = new Date();
        botMessage.setCreateTime(date);
        botMessage.setBotFlag("2");
        String text = "";

        try {
            // 如果没有文件,调用一般文生文接口
            if (StringUtils.isEmpty(pyMessage.getFilePath())) {
                text = AiUtil.getTyQwMessage(pyMessage.getText());
            }

        } catch (Exception e) {
            text = "机器人回复异常";
        } finally {
            botMessage.setText(text);
            list.add(botMessage);
            if (saveBatch(list)) {
                return AjaxResult.success(botMessage);
            }
            return AjaxResult.error("保存消息失败");
        }
    }

    /**
     * 通过流发送消息
     * @param pyMessage 消息
     * @return
     * @throws NoApiKeyException
     * @throws InputRequiredException
     */
    @Override
    public Flux<String> sendMessageAndGetFluxMessage(PyMessage pyMessage) throws NoApiKeyException, InputRequiredException {
        // 保存消息到本地
        save(pyMessage);
        // 机器人消息
        Flowable<ApplicationResult> result = AiUtil.getTyQwFluxMessage(pyMessage.getText());
        return Flux.from(result)
                .map(data -> data.getOutput().getText())
                .publishOn(Schedulers.boundedElastic());
    }

}

4.9src/main/java/com/ruoyi/data/message/controller/PyMessageController.java代码如下:

package com.ruoyi.data.message.controller;

import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import java.util.Date;
import javax.servlet.http.HttpServletResponse;


import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.util.QueryWrapperUtil;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.data.message.domain.PyMessage;
import com.ruoyi.data.message.service.PyMessageService;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.common.core.page.TableDataInfo;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import reactor.core.publisher.Flux;


/**
 * @author py
 * @date 2025-03-08
 * @description 消息信息对应的controller层
 */
@RestController
@RequestMapping("/py/message")
public class PyMessageController extends BaseController {
    @Autowired
    private PyMessageService pyMessageService;


    /**
     * 分页查询消息信息列表
     */
    @GetMapping("/list")
    @PreAuthorize("@ss.hasPermi('py:message:message:list')")
    public TableDataInfo list(PyMessage pyMessage) {
        startPage();
        Wrapper<PyMessage> wrapper = QueryWrapperUtil.getWrapper(pyMessage);
        List<PyMessage> list = pyMessageService.list(wrapper);
        return getDataTable(list);
    }

    /**
     * 查询消息信息列表
     */
    @GetMapping("/getList")
    @PreAuthorize("@ss.hasPermi('py:message:message:list')")
    public AjaxResult getList(PyMessage pyMessage) {
        pyMessage.setOwner(getUsername());
        QueryWrapper<PyMessage> wrapper = QueryWrapperUtil.getWrapper(pyMessage);
        wrapper.orderByAsc("create_time");
        wrapper.orderByAsc("bot_flag");
        List<PyMessage> list = pyMessageService.list(wrapper);
        return success(list);
    }

    /**
     * 导出消息信息列表
     */
    @PostMapping("/export")
    @PreAuthorize("@ss.hasPermi('py:message:message:export')")
    @Log(title = "消息信息", businessType = BusinessType.EXPORT)
    public void export(HttpServletResponse response, PyMessage pyMessage) {
        Wrapper<PyMessage> wrapper = QueryWrapperUtil.getWrapper(pyMessage);
        List<PyMessage> list = pyMessageService.list(wrapper);
        ExcelUtil<PyMessage> util = new ExcelUtil<PyMessage>(PyMessage.class);
        util.exportExcel(response, list, "消息信息数据");
    }

    /**
     * 获取消息信息详细信息
     */
    @PreAuthorize("@ss.hasPermi('py:message:message:query')")
    @GetMapping(value = "/{messageId}")
    public AjaxResult getInfo(@PathVariable String messageId) {
        return success(pyMessageService.getById(messageId));
    }

    /**
     * 新增消息信息,字符串方式,并返回机器人消息
     */
    @PreAuthorize("@ss.hasPermi('py:message:message:add')")
    @PostMapping("/send")
    public AjaxResult send(@RequestBody PyMessage pyMessage) {
        String username = getUsername();
        pyMessage.setCreateBy(username);
        pyMessage.setOwner(username);
        return pyMessageService.sendMessageAndGetJsonMessage(pyMessage);
    }

    /**
     * 添加消息
     *
     * @param pyMessage 消息
     * @return
     */
    @PreAuthorize("@ss.hasPermi('py:message:message:add')")
    @PostMapping
    public AjaxResult add(@RequestBody PyMessage pyMessage) {
        try {
            pyMessage.setText(URLDecoder.decode(pyMessage.getText(), String.valueOf(StandardCharsets.UTF_8)));
        } catch (UnsupportedEncodingException e) {
            return AjaxResult.error();
        }
        String username = getUsername();
        pyMessage.setCreateBy(username);
        pyMessage.setOwner(username);
        return toAjax(pyMessageService.save(pyMessage));
    }

    /**
     * 新增消息,通过流的方式获取机器人消息
     *
     * @param pyMessage
     * @return
     */
    @PreAuthorize("@ss.hasPermi('py:message:message:add')")
    @PostMapping(value = "/getFlux", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<String> getFlux(@RequestBody PyMessage pyMessage) {
        String username = getUsername();
        pyMessage.setCreateBy(username);
        pyMessage.setOwner(username);
        try {
            Flux<String> flux = pyMessageService.sendMessageAndGetFluxMessage(pyMessage);
            return flux;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }


    /**
     * 修改消息信息
     */
    @PreAuthorize("@ss.hasPermi('py:message:message:edit')")
    @Log(title = "消息信息", businessType = BusinessType.UPDATE)
    @PutMapping
    public AjaxResult edit(@RequestBody PyMessage pyMessage) {
        pyMessage.setUpdateBy(getUsername());
        pyMessage.setUpdateTime(new Date());
        return toAjax(pyMessageService.updateById(pyMessage));
    }

    /**
     * 删除消息信息
     */
    @PreAuthorize("@ss.hasPermi('py:message:message:remove')")
    @Log(title = "消息信息", businessType = BusinessType.DELETE)
    @DeleteMapping("/{messageIds}")
    public AjaxResult remove(@PathVariable String[] messageIds) {
        return toAjax(pyMessageService.removeByIds(Arrays.asList(messageIds)));
    }
}

10.启动后端。

3.前端配置

1.根据文件夹结构,新建src\py\api\message\message.js文件,并用以下代码替换。

import request from '@/utils/request';
let messageUrl = "/py/message";

// 分页查询消息信息列表
export function listMessage(query) {
  return request({
    url: messageUrl + '/list',
    method: 'get',
    params: query
  })
}

// 不分页查询消息信息列表
export function getMessageList(query) {
  return request({
    url: messageUrl + '/getList',
    method: 'get',
    params: query
  })
}

// 查询消息信息详细
export function getMessage(id) {
  return request({
    url: messageUrl + '/' +id,
    method: 'get'
  })
}

// 发送消息并获取json机器人小
export function sendMessage(data) {
  return request({
    url: messageUrl + '/send',
    method: 'post',
    data: data
  })
}

// 新增消息信息
export function addMessage(data) {
  return request({
    url: messageUrl,
    method: 'post',
    data: data
  })
}

// 修改消息信息
export function updateMessage(data) {
  return request({
    url: messageUrl,
    method: 'put',
    data: data
  })
}

// 删除消息信息
export function delMessage(ids) {
  return request({
    url: messageUrl + '/' + ids,
    method: 'delete'
  })
}

2.根据文件夹结构,新建src\components\py\tooltip\index.vue文件,并用以下代码替换。

<template>
    <el-tooltip ref="tooltip" :popper-class="currentPopperClass" :content="content" :placement="placement"
        :value="currentValue" @input="updateValue" :disabled="disabled" :offset="offset" :transition="transition"
        :visible-arrow="visibleArrow" :popper-options="popperOptions" :open-delay="openDelay" :manual="manual"
        :enterable="enterable" :hide-after="hideAfter" :tabindex="tabindex">
        <slot @mouseenter="showTooltip" @mouseleave="hideTooltip"></slot>
    </el-tooltip>
</template>
<script>
    export default {
        props: {
            // 新增属性
            // 样式,可以设置设置下面三个值
            popperStyle: {
                type: Object,
                default() {
                    return {
                        // 背景色
                        backgroundColor: "#409eff",
                        // 字体颜色
                        color: "#fff",
                        // 字体大小
                        fontSize: "14px"
                    }
                }
            },


            // el-tooltip原有属性
            value: Boolean,
            openDelay: {
                type: Number,
                default: 0
            },
            disabled: Boolean,
            manual: Boolean,
            // 删除属性,通过上面新增属性,样式设置
            // effect: {
            //     type: String,
            //     default: 'dark'
            // },
            arrowOffset: {
                type: Number,
                default: 0
            },
            popperClass: String,
            content: String,
            visibleArrow: {
                default: true
            },
            transition: {
                type: String,
                default: 'el-fade-in-linear'
            },
            popperOptions: {
                default() {
                    return {
                        boundariesPadding: 10,
                        gpuAcceleration: false
                    };
                }
            },
            enterable: {
                type: Boolean,
                default: true
            },
            hideAfter: {
                type: Number,
                default: 0
            },
            tabindex: {
                type: Number,
                default: 0
            },
            offset: {
                default: 0
            },
            placement: {
                type: String,
                default: 'bottom'
            },
        },
        data() {
            return {
                uniqueId: `py-tooltip-${Math.random().toString(36).substr(2, 9)}`
            }
        },
        computed: {
            // 当前值
            currentValue: {
                get() {
                    return this.value;
                },
                set(val) {
                    this.updateValue(val);
                }
            },

            // 当前样式
            currentPopperStyle() {
                return {
                    backgroundColor: this.popperStyle.backgroundColor || "#409eff",
                    color: this.popperStyle.color || "#fff",
                    fontSize: this.popperStyle.fontSize || "14px"
                };
            },

            // 当前样式
            currentPopperClass() {
                return this.popperClass ? (this.uniqueId + ' py-tooltip-popper ' + this.popperClass) : (this.uniqueId + ' py-tooltip-popper');
            }
        },
        watch: {
            value(newVal, oldVal) {
                this.currentValue = newVal;
            },

            placement(newVal) {
                // 获取所有 <style> 标签
                const styleTags = document.head.getElementsByTagName("style");

                let hasStyle = false;

                // 遍历所有 <style> 标签
                for (let styleTag of styleTags) {
                    if (styleTag.textContent.includes(`.${this.uniqueId}[x-placement^="${this.placement}"] .popper__arrow::after`)) {
                        hasStyle = true;
                        break;
                    }
                }

                // 如果没有新增
                if (!hasStyle) {
                    const style = document.createElement("style");
                    style.innerHTML = `
      .${this.uniqueId}[x-placement^="${this.placement}"] .popper__arrow::after {
        border-${this.placement}-color: ${this.currentPopperStyle.backgroundColor} !important;
      }
      .${this.uniqueId}[x-placement^="${this.placement}"] .popper__arrow {
        border-${this.placement}-color: ${this.currentPopperStyle.backgroundColor} !important;
      }
    `;
                    document.head.appendChild(style);
                }
            }
        },
        mounted() {
            // 在组件挂载后,将样式动态插入到 style 标签中
            const style = document.createElement("style");
            style.innerHTML = `
      .${this.uniqueId} {
        background-color: ${this.currentPopperStyle.backgroundColor} !important;
        color: ${this.currentPopperStyle.color} !important;
        font-size: ${this.currentPopperStyle.fontSize} !important;
      }
      .${this.uniqueId}[x-placement^="${this.placement}"] .popper__arrow::after {
        border-${this.placement}-color: ${this.currentPopperStyle.backgroundColor} !important;
      }

      .${this.uniqueId}[x-placement^="${this.placement}"] .popper__arrow {
        border-${this.placement}-color: ${this.currentPopperStyle.backgroundColor} !important;
      }
    `;
            document.head.appendChild(style);
        },
        methods: {
            showTooltip() {
                this.currentValue = true; // 鼠标进入时显示提示
            },
            hideTooltip() {
                this.currentValue = false; // 鼠标离开时隐藏提示框
            },
            // // 更新值
            updateValue(val) {
                this.$emit("input", val);
            },
        },
    }
</script>
<style>
    /* 统一样式 */
    .py-tooltip-popper {
        /* 内边距 */
        padding: 8px 12px !important;
        /* 圆角 */
        border-radius: 6px !important;
        /* 阴影 */
        box-shadow: 0 2px 12px rgba(0, 0, 0, 0.2) !important;
    }
</style>

注意:

由于原来的el-tooltip不太好看,封装了个py-tooltip组件,如果有需要可以使用此组件。此组件会操作document,请根据实际情况使用,这也是没办法,因为el-tooltip使用vue-popper,只能操作document设置样式。并且,为了防止样式的相互干扰,每使用一个py-tooltip会动态生成一个样式。因此,使用py-tooltip最好通过v-show来控制显示和隐藏,避免产生过多的样式。

3.根据文件夹结构,新建src\views\py\chat\index.vue文件,并用以下代码替换。

<template>
    <div class="py-chat">
        <!-- 悬浮按钮,打开聊天框 -->
        <py-tooltip content="智能客服" placement="left">
            <el-button v-show="!chatVisible" class="chat-button" type="primary" icon="el-icon-message" circle
                @click="toggleChat"></el-button>
        </py-tooltip>

        <!-- 自定义聊天对话框 -->
        <el-dialog :visible.sync="chatVisible" :fullscreen="isFullScreen" :modal-append-to-body="false"
            class="chat-dialog" :show-close="false">

            <!-- 自定义标题栏 -->
            <template slot="title">
                <div class="chat-header">
                    <span class="chat-title">AI 智能客服</span>
                    <div class="chat-controls">
                        <!-- :placement="isFullScreen ? 'bottom' : 'top'" -->
                        <py-tooltip :content="isFullScreen ? '最小化' : '全屏'" :placement="isFullScreen ? 'bottom' : 'top'">
                            <el-button type="text" @click="toggleFullScreen">
                                <i :class="isFullScreen ? 'el-icon-minus' : 'el-icon-full-screen'"></i>
                            </el-button>
                        </py-tooltip>
                        <py-tooltip content="关闭" :placement="isFullScreen ? 'bottom' : 'top'">
                            <el-button type="text" @click="toggleChat">
                                <i class="el-icon-close"></i>
                            </el-button>
                        </py-tooltip>
                    </div>
                </div>
            </template>

            <!-- 聊天内容 -->
            <div class="chat-content" ref="chatContent" :style="{height: chatContentHeight}">
                <div v-for="(message, index) in messages" :key="index" class="chat-message"
                    :class="message.botFlag == '2' ? 'bot' : 'user'">
                    <!-- AI 头像 -->
                    <img v-if="message.botFlag == '2'" class="chat-avatar" :src="botAvatar" alt="AI">
                    <!-- 聊天气泡 -->
                    <div class="chat-bubble">
                        <div class="chat-text" v-html="message.text">
                        </div>
                        <div class="chat-time" v-if="message.createTime">
                            {{message.createTime}}
                        </div>
                    </div>
                    <!-- 用户头像 -->
                    <img v-if="message.botFlag == '0'" class="chat-avatar" :src="avatar" alt="用户">
                </div>
            </div>


            <!-- 输入框 -->
            <div class="chat-input">
                <!-- 文件上传按钮 -->
                <el-upload class="upload-container" :action="upload.url" :headers="upload.headers"
                    :disabled="upload.isUploading" :show-file-list="false" :on-progress="handleUploadProgress"
                    :on-success="handleUploadSuccess" :before-upload="beforeUpload" multiple>
                    <el-button type="primary" icon="el-icon-upload" class="custom-upload-btn">上传文件</el-button>
                </el-upload>

                <!-- 显示上传文件及进度 -->
                <div class="file-list">
                    <div v-for="file in fileList" :key="file.uid" class="file-item">
                        <span class="file-name">{{ file.name }}</span>
                        <el-progress v-if="file.progress < 100" :percentage="file.progress" type="circle" :width="40">
                        </el-progress>
                        <span v-else class="file-list-operation">
                            <py-tooltip content="删除" placement="top">
                                <i class="el-icon-close file-remove" @click="handleUploadRemove(file)"></i>
                            </py-tooltip>
                        </span>

                    </div>
                </div>
                <el-input v-model="userInput" type="textarea" clearable
                    :autosize="{ minRows: 4, maxRows: 6 }"></el-input>
                <div class="chat-input-button">
                    <el-button type="info" plain icon="el-icon-circle-close" @click="chatVisible=false;">取消</el-button>
                    <el-button type="primary" plain icon="el-icon-position" @click="sendMessage">发送</el-button>
                </div>

            </div>

        </el-dialog>
    </div>
</template>

<script>
    import { mapGetters } from 'vuex';
    import PyTooltip from "@/components/py/tooltip";
    import { addMessage, getMessageList, sendMessage } from "@/py/api/message/message";
    import { getToken } from "@/utils/auth";
    import { tansParams } from "@/utils/ruoyi";
    import axios from 'axios';
    export default {
        data() {
            return {
                // 智能按钮提示框是否显示
                chatButtonTooltipShow: false,
                // 改变大小按钮提示框是否显示
                resizeButtonTooltipShow: false,
                // 关闭按钮提示框是否显示
                closeButtonTooltipShow: false,

                chatVisible: false, // 是否显示聊天框
                isFullScreen: false, // 是否全屏
                userInput: "", // 用户输入
                // ai头像
                botAvatar: require("@/assets/py/images/ai.png"),

                // 聊天内容高度
                chatContentHeight: "300px",

                messages: [],

                // 上传参数
                upload: {
                    // 是否禁用上传
                    isUploading: false,
                    // 设置上传的请求头部
                    headers: { Authorization: "Bearer " + getToken() },
                    // 上传的地址
                    url: process.env.VUE_APP_BASE_API + "/common/upload"
                },

                // 文件列表
                fileList: [],

                // 流内容中断标志
                fluxInterrupt: false
            };
        },
        created() {
            // 获取所有消息
            this.getList();
        },
        components: {
            PyTooltip
        },
        computed: {
            ...mapGetters([
                // 用户头像
                'avatar'
            ]),
        },
        methods: {
            // 获取所有消息
            getList() {
                getMessageList().then(response => {
                    this.messages = response.data;
                    if (!this.messages.length) {
                        this.messages.push({ botFlag: "2", text: "你好!我是智能客服,有什么可以帮助你的吗?" });
                    }
                })
            },
            // 显示/隐藏聊天框
            toggleChat() {
                this.chatVisible = !this.chatVisible;
                // 如果打开聊天框,切换到最下方
                if (this.chatVisible) {
                    this.scrollToBottom();
                }
            },

            // 一次性返回所有数据的方式
            // 等在时间太长,不推荐使用
            // 只支持一问一答的方式,不支持文件上传等操作
            // 开始测试使用,后面功能不会开发,需要自己参考流的方式进行功能完善
            // sendMessage() {
            //     if (!this.userInput.trim()) return;
            //     let userInput = this.userInput;
            //     this.userInput = "";
            //     let currentDate = new Date();
            //     let createTime = this.parseTime(currentDate);
            //     this.messages.push({ createTime: createTime, botFlag: '0', text: userInput });
            //     this.scrollToBottom();
            //     this.messages.push({ botFlag: '2', text: "正在思考中..." });
            //     sendMessage({ createTime: createTime, text: userInput }).then(response => {
            //         this.$set(this.messages, this.messages.length - 1, response.data);
            //     })
            // },

            // 优化消息显示
            optimizeMessage() {

                let messages = this.messages;
                let messagesLength = messages.length;
                let currentMessage = messages[messagesLength - 1];
                let currentMessageText = currentMessage.text;
                currentMessageText = currentMessageText
                    .replace(/---/g, "<hr>") // 替换横线
                    .replace(/(#{3,})([^\*]*)\*\*([^\*]*)\*\*/g, "<h3><strong>$2$3</strong></h3>") // 替换h3
                    .replace(/-\s*\*\*([^\*]*)\*\*:/g, "<strong>$1</strong><br>") // 加粗,并换行
                    .replace(/-\s*\*\*([^\*]*):\*\*/g, "<strong>$1</strong><br>") // 加粗,并换行
                    .replace(/\*\*([^\n\*]+)\*\*/g, "<strong>$1</strong>") // 非换行加粗
                    .replace(/。(<strong>[^<\n\s]+<\/strong><br>)/g, "。<br>$1") // 在句号后面的加粗语句添加换行
                    // .replace(/- ((?:(?!<br>)[^-])+)/g, "<li>$1</li>") // 添加li
                    // .replace(/<\/li><li>/g, "</li><br><li>") // 两个li之间添加换行
                    .replace(/(。)\s+(<strong>)/g, "$1<br>$2") // 特殊情况,分行失败重新分行
                    .replace(/#{3,} (总结)/g, "<h3><strong>$1</strong></h3>") // 特殊处理,如果有总结,添加h3和加粗
                    .replace(/\n/g, "<br>"); // 换行
                this.$set(currentMessage, "text", currentMessageText);
            },

            // 获取所有消息之后优化
            optimizeMessageAfterEnd() {
                let messages = this.messages;
                let messagesLength = messages.length;
                let currentMessage = messages[messagesLength - 1];
                let currentMessageText = currentMessage.text;
                currentMessageText = currentMessageText
                    .replace(/- ((?:(?!<br>)(?!<hr>)(?!<h3>)(?!- ).)+)/g, "<li>$1</li>") // 添加li
                    .replace(/<\/li><li>/g, "</li><br><li>") // 两个li之间添加换行
                    .replace(/(。)\s*(<li>)/g, "$1<br>$2") // 增加li和内容之间的换行
                    .replace(/(<br><strong>(?:(?!(<\/strong>)).)*<\/strong>)/g, "<br>$1") // 在换行加粗前面,再增加一个换行
                    .replace(/(<strong>(?:(?!(<\/strong>)).)*<\/strong><br>)/g, "$1<br>") // 在加粗并换行后面,再添加一个换行
                    .replace(/(。)(<br>)(?:(?!<br>)(?!<))/g, "$1<br>$2"); // 再次检查一下分行失败的情况
                    
                this.$set(currentMessage, "text", currentMessageText);
            },

            // 获取句号后面回车数量
            getFullStopAfterEnterCount(text) {
                let count = 0;
                let lastFullStopIndex = text.lastIndexOf("。");
                let nextEnterIndex = -1;
                if (text[lastFullStopIndex + 1] == "\n") {
                    nextEnterIndex = lastFullStopIndex + 1;
                } else {
                    return count;
                }

                // 如果回车后面还有内容直接返回
                if (text.substring(nextEnterIndex + 1).match(/[^\s\n]/g)) {
                    return count;
                }
                while (nextEnterIndex != -1) {
                    count++;
                    if ((nextEnterIndex < text.length - 1) && (text[nextEnterIndex + 1] == "\n")) {
                        nextEnterIndex = nextEnterIndex + 1;
                    } else {
                        nextEnterIndex = -1;
                    }
                }
                return count;
            },

            // 获取开始的回车数量
            getFirstEnterCount(text) {
                let count = 0;
                let nextEnterIndex = -1;
                if (text[0] == "\n") {
                    nextEnterIndex = 0;
                } else {
                    return count;
                }
                while (nextEnterIndex != -1) {
                    count++;
                    if ((nextEnterIndex < text.length - 1) && (text[nextEnterIndex + 1] == "\n")) {
                        nextEnterIndex = nextEnterIndex + 1;
                    } else {
                        nextEnterIndex = -1;
                    }
                }
                return count;
            },

            // 通过流的方式获取数据,不需要等待
            // 生成数据后,再次调用后端,才能保存。
            async sendMessage() {
                this.fluxInterrupt = false;
                if (!this.userInput.trim()) return;
                let userInput = this.userInput;
                this.userInput = "";
                let currentDate = new Date();
                let createTime = this.parseTime(currentDate);
                // 替换掉emoji表情,因为我mysql版本较低,不支持保存此类表情
                userInput = userInput.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g, '');
                this.messages.push({ createTime, botFlag: '0', text: userInput });
                let filePath = this.fileList.map(item => item.filePath).join(",");
                this.fileList = [];
                this.scrollToBottom();

                this.messages.push({ botFlag: '2', text: "" });
                let obj = {
                    createTime,
                    text: userInput,
                    filePath
                }
                let headers = {
                    "Authorization": "Bearer " + getToken(),
                    "Content-type": "application/json;charset=utf-8"
                }
                try {
                    const response = await fetch(process.env.VUE_APP_BASE_API + "/py/message/getFlux",
                        {
                            method: "post",
                            responseType: "stream",
                            headers: headers,
                            body: JSON.stringify(obj),
                        }
                    )
                    const reader = response.body.getReader();
                    const decoder = new TextDecoder();
                    // 是否有句号
                    let hasFullStop = false;
                    // 句号后面的回车数量
                    let fullStopAfterEnterCount = 0;
                    let notOptimizeText = "";
                    while (true) {
                        const { done, value } = await reader.read();
                        if (done) break;
                        let messages = this.messages;
                        let messagesLength = messages.length;
                        let currentMessage = messages[messagesLength - 1];
                        let currentMessageText = currentMessage.text;
                        let currentText = decoder.decode(value, { stream: true });

                        let firstEnterCount = 0;
                        let addEnter = false;
                        // 如果上段内容有句号,判断是否需要加回车。
                        if (hasFullStop) {
                            firstEnterCount = this.getFirstEnterCount(currentText);
                            if (fullStopAfterEnterCount + firstEnterCount > 2) {
                                addEnter = true;
                            }
                            // 重置
                            fullStopAfterEnterCount = 0;
                            hasFullStop = false;
                        }

                        // 如果有句号
                        if (currentText.includes("。")) {
                            fullStopAfterEnterCount = this.getFullStopAfterEnterCount(currentText);
                            if (fullStopAfterEnterCount > 2) {
                                addEnter = true;
                                fullStopAfterEnterCount = 0;
                            } else {
                                hasFullStop = true;
                            }
                        }
                        // 有实际数据时,才添加
                        if (currentText.match(/[^\n\s]/g)) {

                            // 特殊情况"data:。"连在一起时,替换为换行
                            currentText = currentText.replace("data:。", "。<br>");

                            // 删除所有的回车
                            currentText = currentText.replace(/\n/g, "");

                            // 删除标志
                            currentText = currentText.replace(/data:/g, "");
                            if (addEnter) {
                                currentText += "<br>";
                            }

                            if (currentText) {
                                notOptimizeText += currentText;
                                currentMessage.text = currentMessageText + currentText;
                                this.$set(this.messages, messagesLength - 1, currentMessage);
                            }
                        } else if (addEnter) {
                            currentMessage.text = currentMessageText + "<br>";
                            this.$set(this.messages, messagesLength - 1, currentMessage);
                        }
                        // 优化内容显示
                        this.optimizeMessage();
                        this.scrollToBottom();

                    }
                    console.log(notOptimizeText);
                    // 获取所有消息之后优化内容
                    this.optimizeMessageAfterEnd();
                    this.scrollToBottom();
                    let messages = this.messages;
                    let messagesLength = messages.length;
                    let currentBotMessage = messages[messagesLength - 1];

                    // 替换掉emoji表情,因为我mysql版本较低,不支持保存此类表情
                    currentBotMessage.text = currentBotMessage.text.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g, '');
                    currentBotMessage["createTime"] = this.parseTime(new Date());
                    console.log(currentBotMessage.text);
                    this.$set(this.messages, messagesLength - 1, currentBotMessage);


                    // 防止后端将html标签转义
                    let copyCurrentBotMessage = JSON.parse(JSON.stringify(currentBotMessage));
                    copyCurrentBotMessage.text = encodeURIComponent(copyCurrentBotMessage.text)
                    // 保存数据
                    addMessage(copyCurrentBotMessage);
                } catch (error) {
                    console.log(error);
                    this.$modal.msgError("生成消息失败")
                }
            },
            // 滚动到底部
            scrollToBottom() {
                this.$nextTick(() => {
                    const chatContent = this.$refs.chatContent;
                    chatContent.scrollTop = chatContent.scrollHeight;
                });
            },
            // 切换全屏
            toggleFullScreen() {
                this.isFullScreen = !this.isFullScreen;
                if (this.isFullScreen) {
                    this.chatContentHeight = "60vh";
                } else {
                    this.chatContentHeight = "300px";
                }
                this.scrollToBottom();
            },

            // 上传前,添加文件到列表
            beforeUpload(file) {
                this.fileList.push({
                    uid: file.uid,
                    name: file.name,
                    progress: 0
                });
                return true; // 允许上传
            },

            // 处理上传进度
            handleUploadProgress(event, file) {
                this.upload.isUploading = true;
                const targetFile = this.fileList.find(f => f.uid === file.uid);
                if (targetFile) {
                    targetFile.progress = Math.round((event.loaded / event.total) * 100);
                }
            },

            // 上传成功
            handleUploadSuccess(response, file) {
                this.upload.isUploading = false;
                const targetFile = this.fileList.find(f => f.uid === file.uid);
                if (targetFile) {
                    targetFile.progress = 100; // 上传成功,进度变为100%
                    // 保存文件上传路径
                    if (response.code == 200) {
                        targetFile["filePath"] = response.fileName;
                    }
                }
            },

            // 处理上传列表删除操作
            handleUploadRemove(file) {
                this.fileList = this.fileList.filter(item => item.uid != file.uid);
            }
        }
    };
</script>

<style lang="scss" scoped>
    .py-chat {

        /* 悬浮按钮 */
        .chat-button {
            position: fixed;
            bottom: 20px;
            right: 20px;
            width: 50px;
            height: 50px;
            font-size: 22px;
            z-index: 1000;
        }

        /* 聊天对话框 */
        .chat-dialog {

            /* 自定义标题栏 */
            .chat-header {
                display: flex;
                justify-content: space-between;
                align-items: center;
                width: 100%;

                /* 聊天标题 */
                .chat-title {
                    font-size: 18px;
                    font-weight: bold;
                    text-align: center;
                    flex-grow: 1;
                }

                /* 控制按钮 */
                .chat-controls {
                    display: flex;

                    .el-button {
                        font-size: 18px;
                    }
                }



            }

            /* 聊天内容 */
            .chat-content {
                height: 300px;
                overflow-y: auto;
                padding: 10px;
                background: #f5f5f5;
                border-radius: 5px;
                font-size: 16px;

                /* 聊天消息气泡 */
                .chat-message {
                    display: flex;
                    margin: 10px 0;

                    /* 所有聊天气泡样式 */
                    .chat-bubble {
                        padding: 8px 15px;
                        border-radius: 8px;
                        background: #409eff;
                        color: #fff;
                        max-width: 70%;
                        word-wrap: break-word;

                        /* 聊天内容 */
                        .chat-text {
                            white-space: pre-wrap;
                            text-align: left;
                            /* 字过长自动换行 */
                            word-break: break-word;
                        }

                        /* 聊天时间 */
                        .chat-time {
                            margin-top: 10px;
                            font-size: 12px;
                            color: #F0F0ED;
                        }
                    }

                    /* 机器人聊天样式 */
                    &.bot {
                        justify-content: flex-start;

                        /* 聊天气泡 */
                        .chat-bubble {
                            background: #e0e0e0;
                            color: #333;

                            /* 聊天时间 */
                            .chat-time {
                                color: #606266;
                            }
                        }
                    }

                    /* 用户聊天样式 */
                    &.user {
                        justify-content: flex-end;
                    }

                    /* 头像样式 */
                    .chat-avatar {
                        width: 30px;
                        height: 30px;
                        border-radius: 50%;
                        margin: 0 10px;
                    }
                }
            }

            /* 输入框 */
            .chat-input {
                margin-top: 10px;

                /* 自定义上传按钮 */
                .custom-upload-btn {
                    background-color: #409eff;
                    color: white;
                    border-radius: 20px;
                    padding: 10px 20px;
                }

                /* 上传文件列表 */
                .file-list {
                    margin-top: 10px;

                    .file-item {
                        display: flex;
                        align-items: center;
                        margin-bottom: 10px;

                        .file-name {
                            flex: 1;
                            font-size: 14px;
                        }

                        /* 文件列表操作 */
                        .file-list-operation {
                            font-size: 20px;

                            /* 文件删除 */
                            .file-remove {
                                cursor: pointer;
                                color: #F56C6C;
                                margin-right: 10px;
                            }

                            /* 文件完成 */
                            .file-done {
                                color: #67C23A;
                            }
                        }


                        /* 进度条样式 */
                        .el-progress {
                            margin-left: 10px;
                        }
                    }


                }

                .el-textarea {
                    margin-right: 10px;
                }

                .chat-input-button {
                    display: flex;
                    justify-content: flex-end;
                    margin-top: 10px;
                }
            }
        }
    }
</style>

4.打开src\layout\components\AppMain.vue文件,添加聊天组件,这样能保证所有页面,都显示聊天按钮。

import PyChat from "@/views/py/chat/index"

4.功能介绍

1.通过流的方式获取数据,自动转换格式。

2.样式转换分为两种,一种是获取流数据过程中转换样式。另一种是数据获取完成后,对数据进行统一转换。样式转化使用添加html标签的方式,如果有特殊样式需求,可以根据实际情况修改。

3.支持关闭、最小化和全屏三种状态。关闭状态和全屏状态上面已经显示,下面是最小化状态。

4.支持文件上传和删除,虽然没有使用到文件。但是能将文件地址保存到数据库中,多文件通过“,”分割。如果不需要此功能,可以将相关代码删除。

5.头像和用户头像一致,修改后会同步修改。机器人头像指向src\assets\py\images\ai.png文件,如果有需要可以修改。

5.代码说明

1.注释掉的sendMessage函数是一次性获取数据后,再推给前端。此方式等待时间较长,不推荐使用。如果有此需求,可以去掉注释。

2.由于我的mysql版本较低,数据库不支持报错emoji表情,因此我将表情相关的内容替换掉。如果数据库版本支持emoji表情,可以将这部分代码去掉。

3.由于直接将html字符串传到后端,会将html的标签进行转义。为了防止html标签被转义,先前端加密,再后端解密。

4.当数据较多时,通义千问会自动将内容进行分条。一旦分条后,就要涉及样式转换,通过optimizeMessage和optimizeMessageAfterEnd两个函数完成。optimizeMessage函数用于正在获取数据过程中的样式转换,optimizeMessageAfterEnd用于数据获取完成后的样式转换。这是没办法通过一个函数完成的,因为li样式就用-表示,但是有些样式也包含-,如果都在获取数据过程中完成,就容易把很多内容当成li,导致样式混乱。因此,第一个函数处理获取数据过程中的样式,不至于非常难看;第二个函数处理数据完成后的样式,也是最终样式。

5.控制台会打印准换之前和转换后数据,用于判断是否转换出错。因为通过流获取数据,很容易出现问题。我已经测试了很多数据,发现样式没有大改变。如果有特殊情况,请根据打印信息修改上面两个函数。如果不需要打印,可以删除相关代码。

6.由于类按照类静态属性加载、配置加载和类注入的顺序依次完成,静态变量直接取yml配置一定为空。因此,新增了ConfigUtil工具类,处理静态属性获取yml配置问题。原理是,ConfigUtil工具类非静态属性取到yml配置信息,然后通过@PostConstruct注解,实现类注入时自动调静态变量赋值方法完成上述功能。

6.总结

如果有细心的小伙伴可以发现,集成ai的过程中,发送和接收数据并不麻烦。主要是接收数据后,对数据进行处理。不论是为了减少代码还是提高效率,都需要用到正则表达式。就那几行替换代码,才是文章的精髓之处。虽然这次复制粘贴就可以完成功能,但是下次再用到正则表达式又不知道怎么办了。为了防止上面问题出现,我后续会发布一篇关于正则表达式的文章,说实话,我以前也看到这玩意头疼,一般都是用现成的。但是,真正会用了后,发现这东西也没有那么难,后续,我会详细介绍下如何使用正则表达式。
如果本文章对您有帮助并且条件允许的话,可以给我打赏下。不打赏也没关系,您可以给我点赞支持下,您的支持就是我最大的动力。我会不定时发布一些关于若依框架、java、Vue、uniapp等方面的内容,如果感兴趣的话,可以关注我。如果您需要前后端分离版的文件管理系统、流程管理系统或其他以上四方面涉及的内容,查看我的主页一定不后悔。

;