# Xfee1.0 **Repository Path**: Lapper/Xfee1.0 ## Basic Information - **Project Name**: Xfee1.0 - **Description**: 人工智能小助手的模板前端代码 - **Primary Language**: JavaScript - **License**: AGPL-3.0 - **Default Branch**: orgin - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2024-05-08 - **Last Updated**: 2024-09-19 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Xfee人工智能助手 开发文档 #版权归属 @tomdoger QQ:656277239 侵权必究 ## 需求分析: ​ 随着人工智能的不断发展,ai语言大模型逐渐走入人们的生活,并向医疗,交通各行各业渗透,希望能够设计一款gpt为财经校园专用,帮助同学们以及学校的教职工解决平时遇到的各种问题,便捷学校师生的生活。 > 主要 可以为学校提供 1. 学术支持: - 文献检索:GPT可以提供学生和教师快速准确的文献检索服务,帮助他们找到所需的学术资料。 - 论文写作辅助:GPT可以提供写作建议、论文结构指导等支持,帮助学生更好地完成学术论文。 - 学科知识普及:GPT可以提供各学科知识的普及和解释,帮助学生更好地理解学科知识。 1. 教学辅助: - 智能教学助手:GPT可以为教师提供教学建议、课程设计支持等,帮助他们更好地进行教学。 - 个性化学习推荐:GPT可以根据学生的学习情况和需求,推荐个性化的学习资源和课程,提高学生学习效率。 1. 校园管理: - 学生信息管理:GPT可以帮助学校管理学生信息,包括学籍管理、成绩管理、考勤管理等。 - 课程安排优化:GPT可以根据学生选课情况和教师排课需求,优化课程安排,避免课程冲突。 - 校园安全监控:GPT可以用于校园安全监控,包括视频监控、事件预警等,保障校园安全。 1. 学生服务: - 校园活动信息发布:GPT可以帮助学校发布校园活动信息,提醒学生参与校园活动。 - 心理健康咨询:GPT可以提供心理健康咨询服务,帮助学生解决心理问题。 - 就业指导:GPT可以提供就业信息和指导,帮助学生顺利就业。 1. 沟通互动: - 在线答疑:GPT可以提供在线答疑服务,帮助学生解决学习问题。 - 课程反馈:GPT可以收集学生对课程的反馈意见,帮助教师改进教学质量。 - 校园新闻发布:GPT可以帮助学校发布校园新闻,及时传达校园信息。 ## 系统设计 > ### 功能设计 - 登录与注册功能 - 个人信息管理功能 - 文章发布功能 - 新手指引功能 - 技术教程查看功能 - AI助手功能【核心】 - 热度搜索词功能【如果时间来得及】 ### 技术选型 > 前端:HTML CSS JavaScript > > 前端框架:Vue3 Element-plus Node.js-v16.20 > > 后端: Java Springboot3 > > 数据库:Mysql[存储不常用的数据] > > Nosql:Redis[登录验证缓存] > > Sdk: 阿里云oss 讯飞星火认知大模型 ### 数据库设计 > 用户表 ``` create table user ( id int unsigned auto_increment comment 'ID' primary key, username varchar(20) not null comment '用户名', password varchar(32) null comment '密码', nickname varchar(10) default '' null comment '昵称', email varchar(128) default '' null comment '邮箱', user_pic varchar(128) default '' null comment '头像', create_time datetime not null comment '创建时间', update_time datetime not null comment '修改时间', constraint username unique (username) ) comment '用户表'; ``` > 文章相关表 ``` create table article ( id int unsigned auto_increment comment 'ID' primary key, title varchar(30) not null comment '文章标题', content varchar(10000) not null comment '文章内容', cover_img varchar(128) not null comment '文章封面', state varchar(3) default '草稿' null comment '文章状态: 只能是[已发布] 或者 [草稿]', category_id int unsigned null comment '文章分类ID', create_user int unsigned not null comment '创建人ID', create_time datetime not null comment '创建时间', update_time datetime not null comment '修改时间', allcategory_id int not null, constraint fk_article_category foreign key (category_id) references category (id), constraint fk_article_user foreign key (create_user) references user (id) ); ``` ### 前后端统一响应码: > [!CAUTION] > > { > > code:0,1 //0表示成功 1 表示失败 > > message:"" > > data:{} > > } ``` package org.Xfee.PoJo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; //统一响应结果 @AllArgsConstructor @Data @NoArgsConstructor public class Result { private Integer code;//业务状态码 0-成功 1-失败 private String message;//提示信息 private T data;//响应数据 //快速返回操作成功响应结果(带响应数据) public static Result success(E data) { return new Result<>(0, "操作成功", data); } //快速返回操作成功响应结果 public static Result success() { return new Result(0, "操作成功", null); } public static Result error(String message) { return new Result(1, message, null); } } ``` ### 前后端统一拦截: #### 前端: axios利用响应码进行拦截 加请求头以及过滤请求响应 ``` axios.interceptors.request.use(config => { // 请求拦截器 if (localStorage.getItem("token")!=null) { // 每一个请求头携带token console.log("前端请求拦截加头"+localStorage.getItem("token")) config.headers.Authorization = localStorage.getItem("token"); } return config }, error => Promise.reject(error)); //响应拦截 axios.interceptors.response.use(result=>{ if (result.data.code==0) { //成功就放行 return result } if (result.data.code==1){ return ElNotification({ title: '消息', message: h('i', { style: 'color: teal' },"操作失败!"), }) } }, err =>{ if (err.response.status === 401) { ElMessage.error('主人,请先登录吧') router.push('/login') } else { ElMessage.error('服务异常') } return Promise.reject(err);//异步的状态转化成失败的状态 } ) ``` #### 后端: 利用Springboot的拦截器 实现了请求过滤以及登录鉴别 主要是对于前端的token进行鉴别 通过与redis缓存的token解密比对 一致则放行授权 不一致则拒绝授权 ``` @Configuration public class WebConfig implements WebMvcConfigurer { @Autowired //注入全局拦截器 private org.Xfee.Interceptors.LoginIntercepter loginIntercepter; @Override public void addInterceptors(InterceptorRegistry registry) { //放行登录和注册 /* * addInterceptor:需要一个实现HandlerInterceptor接口的拦截器实例 addPathPatterns:用于设置拦截器的过滤路径规则;addPathPatterns("/**")对所有请求都拦截 :用于设置不需要拦截的过滤规则 * */ registry.addInterceptor(loginIntercepter).excludePathPatterns("/user/register","/user/login"); } } package org.Xfee.Interceptors; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.Xfee.Util.JwtUtil; import org.Xfee.Util.ThreadLocalUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.ValueOperations; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; import java.util.Enumeration; import java.util.Map; @Component public class LoginIntercepter implements HandlerInterceptor { @Autowired private StringRedisTemplate stringRedisTemplate; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { try { // 从请求头中获取token Enumeration headerNames = request.getHeaderNames(); while (headerNames.hasMoreElements()){ String name = headerNames.nextElement(); String value = request.getHeader(name); System.out.println(name+":"+value); } System.out.println("分界线----------------------------------请求"); if (request.getMethod().equals("OPTIONS")) { response.setStatus(HttpServletResponse.SC_OK); return true; // 如果不是,我们就把token拿到,用来做判断 } String token = request.getHeader("Authorization"); System.out.println("Token: " + token); // 从Redis中获取存储的token ValueOperations ops = stringRedisTemplate.opsForValue(); String redisToken = ops.get("token"); // 判断token是否有效 if (redisToken == null || !redisToken.equals(token)) { throw new RuntimeException("Token is invalid or expired"); } // 解密JWT token获取有效载荷 Map payload = JwtUtil.praseToken(token); // 从有效载荷中获取用户信息并存储到线程本地变量 Map userInfo = (Map) payload.get("subjects"); ThreadLocalUtil.set(userInfo); return true; } catch (Exception e) { // 记录日志或返回详细错误信息 response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return false; } } } ``` ### 全局统一异常处理: 开发过程中可能有大量的异常抛出,一个一个进行catch捕获太繁琐,利用Springboot的全局异常处理进行异常的处理 ``` package org.Xfee.Eception; import org.Xfee.PoJo.Result; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; /** * 作者:Lapper * QQ656277239 * vx:JavaAdmin123 */ @RestControllerAdvice //全局异常拦截处理 //搭配ExceptionHandler public class GlobalEceptionHandler { @ExceptionHandler(Exception.class) public Result Eception(Exception E){ //异常打印以及处理 E.printStackTrace(); return Result.error(StringUtils.hasLength(E.getMessage())?E.getMessage():"操作失败"); } } ``` ## 功能开发 ### 1.注册登录功能 #### 注册 > 注册功能: > > 1.封装UserBean 用于封装用户 完成基础的增删改查 > > 2.通过获取前端的用户与密码 > > 后端首先通过正则表达式检验前端传入的数据是否合规 SpringBoot Validation校验 > > 再次判断数据库是否存在用户 存在泽调用业务层 对UserBean进行增加用户操作 > > 若不存在 则返回操作失败 ``` public Result register(@Pattern(regexp = "^.{5,16}$") String username, @Pattern(regexp = "^.{5,16}$")String password){ User user= userService.finfByUserName(username); //查询内容 if (user==null){ //可以注册 userService.register(username,password); return Result.success(null); }else{ return Result.error("操作失败"); } } @Override public void register(String username, String password) { String Md5String= Md5Util.getMD5String(password); userMapper.AddUser(username,Md5String); } ``` #### 登录 > 登录功能 > > 后端首先通过正则表达式检验前端传入的数据是否合规 SpringBoot Validation校验 > > 首先查看数据库是否有当前用户 没有则返回登录失败 > > 如果数据库里面有当前用户 就对数据库的密码进行解密 与前端传入的做比对 > > 比对通过则登录成功 并且将token缓存到redis做失效判断 不通过则返回登录失败 ``` public Result Login(@Pattern(regexp = "^.{5,16}$") String username, @Pattern(regexp = "^.{5,16}$")String password){ //先判断用户是否存在 User user= userService.finfByUserName(username); if (user==null){ return Result.error("登陆失败"); } if (Md5Util.getMD5String(password).equals(user.getPassword())){ //JWT令牌校验 Map Claims= new HashMap<>(); Claims.put("id",user.getId()); Claims.put("username",user.getUsername()); String token= JwtUtil.genToken(Claims); //存放到redis 令牌主动失效机制 ValueOperations op = stringRedisTemplate.opsForValue(); op.set("token",token,1, TimeUnit.HOURS); return Result.success(token); } return Result.error("密码错误"); } ``` ### 2.个人信息管理功能 #### 1.查看用户信息 #### 2.修改用户信息 #### 3.修改密码 #### 4.上传头像 > 这一部分属于是基础的增删改 不做过多介绍 详情可以看代码 ``` @GetMapping("/userInfo") public Result userInfo(/*@RequestHeader(name = "Authorization") String token*/){ //用户信息查询 try { Map map=ThreadLocalUtil.get(); String username = (String) map.get("username"); User user= userService.finfByUserName(username); System.out.println(user); System.out.println("访问到这"); return Result.success(user); }catch (Exception e){ return Result.error("查询失败"); } } @PutMapping("/update") public Result update(@RequestBody User user ){ //用户信息更新 System.out.println(user); user.setUpdateTime(LocalDateTime.now()); userService.updateUser(user); return Result.success("更新成功"); } @PatchMapping("/updateAvatar") //用户头像修改 public Result FileUpload(MultipartFile file) throws IOException { String OriginalFileName=file.getOriginalFilename(); String filename= UUID.randomUUID().toString()+OriginalFileName.substring(OriginalFileName.lastIndexOf(".")); //调用阿里云sdk String url= AliossUtil.main(filename,file.getInputStream()); Map map=ThreadLocalUtil.get(); Integer id= (Integer) map.get("id"); userService.updateAvatar(url,id); return Result.success("更新成功"); } @PatchMapping("/updatePwd") //修改用户密码 public Result updatePwd(@RequestBody Map prams,@RequestHeader("Authorization") String token){ System.out.println(prams); String oldPwd = prams.get("old_pwd"); String newPwd = prams.get("new_pwd"); String rePwd = prams.get("re_pwd"); //拿到前端的数据 Boolean checkPwd=new PasswordCheck().Check(prams); //检测参数是否合法 if (checkPwd){ //参数合法 Map map=ThreadLocalUtil.get(); //读取线程里面存放的map //拿到用户名 User user=userService.finfByUserName((String) map.get("username")); if(user.getPassword().equals(Md5Util.getMD5String(oldPwd))){ //如果老的密码比对通过 userService.updatePwd(Md5Util.getMD5String(newPwd)); ValueOperations op = stringRedisTemplate.opsForValue(); op.getOperations().delete("token"); }else { return Result.error("更新失败 旧密码不一致"); } }else { return Result.error("更新失败"); } return Result.success("更新成功"); } } ``` ### 3.图片上传接口 > 这里调用了阿里云工具类对于上传的文件 返回文件地址 ``` package org.Xfee.Controller; import org.Xfee.PoJo.Result; import org.Xfee.Util.AliossUtil; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; import java.util.UUID; @RestController @RequestMapping("/upload") public class Upload { @PostMapping public Result FileUpload(MultipartFile file) throws IOException { String OriginalFileName=file.getOriginalFilename(); String filename= UUID.randomUUID().toString()+OriginalFileName.substring(OriginalFileName.lastIndexOf(".")); //调用阿里云sdk String url= AliossUtil.main(filename,file.getInputStream()); return Result.success(url); } } ``` ### 4.阿里云文件上传工具 > 工具类是百度的 但是有些功能可能因为年代太久失效了 是本人重新修复了一部分 觉得拉跨可以自己改 ``` package org.Xfee.Util; import com.aliyun.oss.ClientException; import com.aliyun.oss.OSS; import com.aliyun.oss.OSSClientBuilder; import com.aliyun.oss.OSSException; import com.aliyun.oss.model.PutObjectRequest; import com.aliyun.oss.model.PutObjectResult; import java.io.InputStream; /** * 作者:Lapper * QQ656277239 * vx:JavaAdmin123 */ public class AliossUtil { // Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。 private static final String Endpoint = "https://oss-cn-beijing.aliyuns.com"; // 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。 // EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider(); // 填写Bucket名称,例如examplebucket。 private static final String ACCESS_KEY_ID="LTAI5tKu2nQCswYkxNEXNzrE"; private static final String ACCESS_KEY_SECRET="WEsjf7KiexNwZlbTgiWO5aCyfTwrt1"; private static final String BucketName = "bigeven01"; // 填写Object完整路径,完整路径中不能包含Bucket名称,例如exampledir/exampleobject.txt。 public static String main(String filename, InputStream in) { String objectName =filename; String url=""; // 创建OSSClient实例。 OSS ossClient = new OSSClientBuilder().build(Endpoint,ACCESS_KEY_ID,ACCESS_KEY_SECRET); try { // 填写字符串。 // 创建PutObjectRequest对象。 PutObjectRequest putObjectRequest = new PutObjectRequest(BucketName, objectName,in); // 如果需要上传时设置存储类型和访问权限,请参考以下示例代码。 // ObjectMetadata metadata = new ObjectMetadata(); // metadata.setHeader(OSSHeaders.OSS_STORAGE_CLASS, StorageClass.Standard.toString()); // metadata.setObjectAcl(CannedAccessControlList.Private); // putObjectRequest.setMetadata(metadata); // 上传字符串。 PutObjectResult result = ossClient.putObject(putObjectRequest); url="https://"+BucketName+"."+Endpoint.substring(Endpoint.lastIndexOf("/")+1)+"/"+objectName; } catch ( OSSException oe) { System.out.println("Caught an OSSException, which means your request made it to OSS, " + "but was rejected with an error response for some reason."); System.out.println("Error Message:" + oe.getErrorMessage()); System.out.println("Error Code:" + oe.getErrorCode()); System.out.println("Request ID:" + oe.getRequestId()); System.out.println("Host ID:" + oe.getHostId()); } catch ( ClientException ce) { System.out.println("Caught an ClientException, which means the client encountered " + "a serious internal problem while trying to communicate with OSS, " + "such as not being able to access the network."); System.out.println("Error Message:" + ce.getMessage()); } finally { if (ossClient != null) { ossClient.shutdown(); } } return url; } } ``` 5.JWT工具类 > 工具类是百度的 但是有些功能可能因为年代太久失效了 是本人重新修复了一部分 觉得拉跨可以自己改 ``` package org.Xfee.Util; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import javax.crypto.spec.SecretKeySpec; import java.security.Key; import java.util.Base64; import java.util.Date; import java.util.Map; import static io.jsonwebtoken.SignatureAlgorithm.HS256; public class JwtUtil { //JWt令牌工具 private static final String SECRET_KEY = "+yoursecretkey"; private static final long EXPIRATION_TIME = 3600000; // 1 hour in milliseconds public static String genToken(Map subjects) { byte[] apiKeySecretBytes = Base64.getDecoder().decode(SECRET_KEY); Key signingKey = new SecretKeySpec(apiKeySecretBytes, HS256.getJcaName()); Date expirationDate = new Date(System.currentTimeMillis() + EXPIRATION_TIME); String jwtToken = Jwts.builder() .setExpiration(expirationDate) .claim("subjects", subjects) .signWith(HS256, signingKey) .compact(); return jwtToken; } public static boolean JudgeToken(String token) { try { Jwts.parser().setSigningKey(Base64.getDecoder().decode(SECRET_KEY)).parseClaimsJws(token); return true; } catch (io.jsonwebtoken.SignatureException e) { return false; } } public static Map praseToken(String token) { byte[] apiKeySecretBytes = Base64.getDecoder().decode(SECRET_KEY); Key signingKey = new SecretKeySpec(apiKeySecretBytes, HS256.getJcaName()); Claims claims = Jwts.parser() .setSigningKey(signingKey) .parseClaimsJws(token) .getBody(); return claims; } } ``` ### 5.文章教程功能 > 由于这类基础增删改比较简单 这里只给出代码 不作详细说明了 > > 分页查询 使用的是Mybatis的插件 PageHelper ``` package org.Xfee.Service.Impl; import com.github.pagehelper.Page; import com.github.pagehelper.PageHelper; import org.Xfee.Mapper.ArticleMapper; import org.Xfee.PoJo.Article; import org.Xfee.PoJo.PageBean; import org.Xfee.Service.ArticleService; import org.Xfee.Util.ThreadLocalUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.time.LocalDateTime; import java.util.List; import java.util.Map; /** * 作者:Lapper * QQ656277239 * vx:JavaAdmin123 */ @Service public class ArticleServiceImpl implements ArticleService { @Autowired private ArticleMapper articleMapper; @Override //新增文章 public void Add(Article article) { article.setCreateTime(LocalDateTime.now()); article.setUpdateTime(LocalDateTime.now()); Map map=ThreadLocalUtil.get(); article.setCreateUser((Integer) map.get("id")); articleMapper.Add(article); } @Override //分页条件查询 public PageBean
List(Integer pageNum, Integer pageSize, Integer categoryId, String state) { PageBean
pageBean = new PageBean<>(); Map map = ThreadLocalUtil.get(); Integer userId= (Integer) map.get("id"); PageHelper.startPage(pageNum,pageSize); List
articleList= articleMapper.List(userId,categoryId,state); Page
page = (Page
) articleList; pageBean.setTotal(page.getTotal()); pageBean.setItems(page.getResult()); return pageBean; } @Override public List
queryByKey(String keywords, Integer allcategoryId) { List
List=articleMapper.selectByKey(keywords,allcategoryId); return List; } } ``` ### 6.AI讯飞星火大模型 > 这里首先注册了讯飞星火模型 然后进行配置 > > 这里借鉴了github大佬 @狐狸半面舔 的讯飞星火1.5连接解决方案 这个版本并没有官方Sdk 只有一些大佬自己写的文章。 > > 目前讯飞已经推出了3.0 并且开放了Java的调用实例 > > 这里先获取用户输入的内容 对于用户输入的内容进行鉴别 > > 如果内容 符合规则 创建消息对象 创建监听器 发送给大模型 开启监听 > > 这里拿到数据后返回给前端 #### 请求接口: ``` package org.Xfee.Controller; import cn.hutool.core.util.StrUtil; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import okhttp3.WebSocket; import org.Xfee.Config.XfXhConfig; import org.Xfee.Dao.MsgDTO; import org.Xfee.Listen.XfXhWebSocketListener; import org.Xfee.PoJo.Result; import org.Xfee.PoJo.XfXhStreamClient; import org.springframework.web.bind.annotation.*; import java.util.Collections; import java.util.UUID; @RestController @RequestMapping("/xfee") @Slf4j public class TestController { @Resource private XfXhStreamClient xfXhStreamClient; @Resource private XfXhConfig xfXhConfig; /** * 发送问题 * * @param question 问题 * @return 星火大模型的回答 */ @GetMapping("/ask") public Result sendQuestion(@RequestParam String question) { // 如果是无效字符串,则不对大模型进行请求 if (StrUtil.isBlank(question)) { return Result.error("无效问题,请重新输入"); } // 获取连接令牌 if (!xfXhStreamClient.operateToken(XfXhStreamClient.GET_TOKEN_STATUS)) { return Result.error("当前模型链接人数过多!"); } // 创建消息对象 MsgDTO msgDTO = MsgDTO.createUserMsg(question); // 创建监听器 XfXhWebSocketListener listener = new XfXhWebSocketListener(); // 发送问题给大模型,生成 websocket 连接 WebSocket webSocket = xfXhStreamClient.sendMsg(UUID.randomUUID().toString().substring(0, 10), Collections.singletonList(msgDTO), listener); if (webSocket == null) { // 归还令牌 xfXhStreamClient.operateToken(XfXhStreamClient.BACK_TOKEN_STATUS); return Result.error("系统内部错误!"); } try { int count = 0; // 为了避免死循环,设置循环次数来定义超时时长 int maxCount = xfXhConfig.getMaxResponseTime() * 5; while (count <= maxCount) { Thread.sleep(200); if (listener.isWsCloseFlag()) { break; } count++; } if (count > maxCount) { return Result.error("响应超时请联系管理!"); } // 响应大模型的答案 return Result.success(listener.getAnswer().toString()); } catch (InterruptedException e) { log.error("错误:" + e.getMessage()); return Result.error("系统内部错误!请联系管理!"); } finally { // 关闭 websocket 连接 webSocket.close(1000, ""); // 归还令牌 xfXhStreamClient.operateToken(XfXhStreamClient.BACK_TOKEN_STATUS); } } } ``` #### 封装请求的数据 > 功能参考@狐狸半面舔 ``` package org.Xfee.Dao; import com.alibaba.fastjson.annotation.JSONField; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.util.List; /** * 请求参数 * 对应生成的 JSON 结构参考 resources/demo-json/request.json * * @author 狐狸半面添 * @create 2023-09-15 0:42 */ @NoArgsConstructor @Data public class RequestDTO { @JsonProperty("header") private HeaderDTO header; @JsonProperty("parameter") private ParameterDTO parameter; @JsonProperty("payload") private PayloadDTO payload; @NoArgsConstructor @Data @AllArgsConstructor public static class HeaderDTO { /** * 应用appid,从开放平台控制台创建的应用中获取 */ @JSONField(name = "app_id") private String appId; /** * 每个用户的id,用于区分不同用户,最大长度32 */ @JSONField(name = "uid") private String uid; } @NoArgsConstructor @Data @AllArgsConstructor public static class ParameterDTO { private ChatDTO chat; @NoArgsConstructor @Data @AllArgsConstructor public static class ChatDTO { /** * 指定访问的领域,general指向V1.5版本 generalv2指向V2版本。注意:不同的取值对应的url也不一样! */ @JsonProperty("domain") private String domain; /** * 核采样阈值。用于决定结果随机性,取值越高随机性越强即相同的问题得到的不同答案的可能性越高 */ @JsonProperty("temperature") private Float temperature; /** * 模型回答的tokens的最大长度 */ @JSONField(name = "max_tokens") private Integer maxTokens; } } @NoArgsConstructor @Data @AllArgsConstructor public static class PayloadDTO { @JsonProperty("message") private MessageDTO message; @NoArgsConstructor @Data @AllArgsConstructor public static class MessageDTO { @JsonProperty("text") private List text; } } } ``` #### 封装响应的数据: ``` package org.Xfee.Dao; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; import lombok.NoArgsConstructor; import java.util.List; /** * 返回参数 * 对应生成的 JSON 结构参考 resources/demo-json/response.json * * @author 狐狸半面添 * @create 2023-09-15 0:42 */ @NoArgsConstructor @Data public class ResponseDTO { @JsonProperty("header") private HeaderDTO header; @JsonProperty("payload") private PayloadDTO payload; @NoArgsConstructor @Data public static class HeaderDTO { /** * 错误码,0表示正常,非0表示出错 */ @JsonProperty("code") private Integer code; /** * 会话是否成功的描述信息 */ @JsonProperty("message") private String message; /** * 会话的唯一id,用于讯飞技术人员查询服务端会话日志使用,出现调用错误时建议留存该字段 */ @JsonProperty("sid") private String sid; /** * 会话状态,取值为[0,1,2];0代表首次结果;1代表中间结果;2代表最后一个结果 */ @JsonProperty("status") private Integer status; } @NoArgsConstructor @Data public static class PayloadDTO { @JsonProperty("choices") private ChoicesDTO choices; /** * 在最后一次结果返回 */ @JsonProperty("usage") private UsageDTO usage; @NoArgsConstructor @Data public static class ChoicesDTO { /** * 文本响应状态,取值为[0,1,2]; 0代表首个文本结果;1代表中间文本结果;2代表最后一个文本结果 */ @JsonProperty("status") private Integer status; /** * 返回的数据序号,取值为[0,9999999] */ @JsonProperty("seq") private Integer seq; /** * 响应文本 */ @JsonProperty("text") private List text; } @NoArgsConstructor @Data public static class UsageDTO { @JsonProperty("text") private TextDTO text; @NoArgsConstructor @Data public static class TextDTO { /** * 保留字段,可忽略 */ @JsonProperty("question_tokens") private Integer questionTokens; /** * 包含历史问题的总tokens大小 */ @JsonProperty("prompt_tokens") private Integer promptTokens; /** * 回答的tokens大小 */ @JsonProperty("completion_tokens") private Integer completionTokens; /** * prompt_tokens和completion_tokens的和,也是本次交互计费的tokens大小 */ @JsonProperty("total_tokens") private Integer totalTokens; } } } } ``` #### 调用接口与配置 ``` package org.Xfee.PoJo; import com.alibaba.fastjson.JSONObject; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import okhttp3.*; import org.Xfee.Config.XfXhConfig; import org.Xfee.Dao.MsgDTO; import org.Xfee.Dao.RequestDTO; import org.springframework.stereotype.Component; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.net.URL; import java.nio.charset.StandardCharsets; import java.text.SimpleDateFormat; import java.util.*; @Component @Slf4j public class XfXhStreamClient { @Resource private XfXhConfig xfXhConfig; // @Value("${xfxh.QPS}") private int connectionTokenCount=2; /** * 获取令牌 */ public static int GET_TOKEN_STATUS = 0; /** * 归还令牌 */ public static int BACK_TOKEN_STATUS = 1; /** * 操作令牌 * * @param status 0-获取令牌 1-归还令牌 * @return 是否操作成功 */ public synchronized boolean operateToken(int status) { if (status == GET_TOKEN_STATUS) { // 获取令牌 if (connectionTokenCount != 0) { // 说明还有令牌,将令牌数减一 connectionTokenCount -= 1; return true; } else { return false; } } else { // 放回令牌 connectionTokenCount += 1; return true; } } /** * 发送消息 * * @param uid 每个用户的id,用于区分不同用户 * @param msgList 发送给大模型的消息,可以包含上下文内容 * @return 获取websocket连接,以便于我们在获取完整大模型回复后手动关闭连接 */ public WebSocket sendMsg(String uid, List msgList, WebSocketListener listener) { // 获取鉴权url String authUrl = this.getAuthUrl(); // 鉴权方法生成失败,直接返回 null if (authUrl == null) { return null; } OkHttpClient okHttpClient = new OkHttpClient.Builder().build(); // 将 https/http 连接替换为 ws/wss 连接 String url = authUrl.replace("http://", "ws://").replace("https://", "wss://"); Request request = new Request.Builder().url(url).build(); // 建立 wss 连接 WebSocket webSocket = okHttpClient.newWebSocket(request, listener); // 组装请求参数 RequestDTO requestDTO = getRequestParam(uid, msgList); // 发送请求 webSocket.send(JSONObject.toJSONString(requestDTO)); return webSocket; } /** * 生成鉴权方法,具体实现不用关心,这是讯飞官方定义的鉴权方式 * * @return 鉴权访问大模型的路径 */ public String getAuthUrl() { try { URL url = new URL(xfXhConfig.getHostUrl()); // 时间 SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US); format.setTimeZone(TimeZone.getTimeZone("GMT")); String date = format.format(new Date()); // 拼接 String preStr = "host: " + url.getHost() + "\n" + "date: " + date + "\n" + "GET " + url.getPath() + " HTTP/1.1"; // SHA256加密 Mac mac = Mac.getInstance("hmacsha256"); SecretKeySpec spec = new SecretKeySpec(xfXhConfig.getApiSecret().getBytes(StandardCharsets.UTF_8), "hmacsha256"); mac.init(spec); byte[] hexDigits = mac.doFinal(preStr.getBytes(StandardCharsets.UTF_8)); // Base64加密 String sha = Base64.getEncoder().encodeToString(hexDigits); // 拼接 String authorizationOrigin = String.format("api_key=\"%s\", algorithm=\"%s\", headers=\"%s\", signature=\"%s\"", xfXhConfig.getApiKey(), "hmac-sha256", "host date request-line", sha); // 拼接地址 HttpUrl httpUrl = Objects.requireNonNull(HttpUrl.parse("https://" + url.getHost() + url.getPath())).newBuilder(). addQueryParameter("authorization", Base64.getEncoder().encodeToString(authorizationOrigin.getBytes(StandardCharsets.UTF_8))). addQueryParameter("date", date). addQueryParameter("host", url.getHost()). build(); return httpUrl.toString(); } catch (Exception e) { log.error("鉴权方法中发生错误:" + e.getMessage()); return null; } } /** * 获取请求参数 * * @param uid 每个用户的id,用于区分不同用户 * @param msgList 发送给大模型的消息,可以包含上下文内容 * @return 请求DTO,该 DTO 转 json 字符串后生成的格式参考 resources/demo-json/request.json */ public RequestDTO getRequestParam(String uid, List msgList) { RequestDTO requestDTO = new RequestDTO(); requestDTO.setHeader(new RequestDTO.HeaderDTO(xfXhConfig.getAppId(), uid)); requestDTO.setParameter(new RequestDTO.ParameterDTO(new RequestDTO.ParameterDTO.ChatDTO(xfXhConfig.getDomain(), xfXhConfig.getTemperature(), xfXhConfig.getMaxTokens()))); requestDTO.setPayload(new RequestDTO.PayloadDTO(new RequestDTO.PayloadDTO.MessageDTO(msgList))); return requestDTO; } } ``` ### 前端开发: > 前端主要采用vue3 采用组合式API开发 vue2的很多东西会有版本冲突-------不要随便引入 > 页面布局统一采用 Element-plus框架的 row col 以及布局容器 来实现布局 > > 下面对核心的功能进行讲解 小的部分就不提及了 > > 由于大模型的响应数据量会很大 直接展现给用户 会影响用户体验 > > 所以需要动态的操作dom 通过异步函数 体现出打字效果 【**当然这里可以用aysnc和await关键字去做流式数据传输 或者是sse技术** 可能需要手写请求 比较麻烦 直接用了axios 本次1.5暂时采用异步动态操作dom来实现类似效果 】 #### Ai大模型页面 ```

Xfee人工智能助手

欢迎您使用Xfee星火人工智能小助手

V1.5

亲爱的用户您好,我是Xfee小助手

我可以陪您写文案,聊天,答疑解惑,计算题等等 点击下方的按钮快来试试吧~
您可以问我:例如:如何写一封表白性?感冒发烧怎么办?100+1100*2=?以及生活学业工作所遇到的各种问题^^
询问
``` #### 原生js实现前端流式输出效果 > 观察了一下chatgpt 的流式输出数据 > > 一些大佬给出的方案是 用js的aysnc await 关键字通过获取 read对象来流式传输数据 也有是说用SSE技术 > > 空闲下来接入3.0版本的api将采用 aysnc await关键字去处理 > > 这里用了js动态操作dom 并且持续监听高度 实现了流式的打字效果 > > 首先对于按钮开启了禁用 防止多次提交 > > 同时开启了动态监听窗口的高度 随着数据的加载 不断更新到底部 优化用户的体验感 ``` const SparkDesk=ref("") const Answer=ref("") //讯飞的接口 const xfee = (question) => { //防止二次提交 openoff.value=true //拿到用户的问题 const sparkask = SparkDesk.value; const outputElement2 = document.getElementById('text'); // 创建一个span元素并设置样式为简约颜色和金色边框 用户输入的部分 const spanElement = document.createElement('span'); spanElement.style.color = 'black'; // 设置字体颜色为navy,可以根据需要更改颜色 spanElement.style.fontWeight = 'bold'; spanElement.style.width='100%'// 设置边框为2像素宽的金色实线 const img1 = document.createElement('img'); img1.src = "https://bpic.588ku.com/element_pic/23/04/23/cd5a7195c75b1ac6b25ea6be211dec53.png!/fw/253/quality/90/unsharp/true/compress/true"; img1.style.borderRadius = "50%"; img1.style.width = "6%"; img1.style.height = "6%"; img1.style.alignItems = "left"; img1.size='small' spanElement.appendChild(img1); spanElement.appendChild(document.createTextNode( sparkask)); outputElement2.appendChild(spanElement); outputElement2.scrollTop=outputElement2.scrollHeight const hr1 = document.createElement("hr"); const br1 = document.createElement("br"); outputElement2.appendChild(hr1); outputElement2.appendChild(br1); const outputElement1 = document.getElementById('text'); const img = document.createElement('img'); img.src = "https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png"; img.style.borderRadius = "50%"; img.style.width = "5%"; img.style.height = "5%"; img.style.alignItems = "left"; outputElement1.appendChild(img); SparkDesk.value="" axios.get("/xfee/ask", {params:{ question:question }}).then(result => { //拿到结果开启加载 fullscreenLoading.value = true //打开 按钮开关 setTimeout(()=>{ fullscreenLoading.value=false },500) if (result.data.code==0){ //拿到回答 Answer.value = result.data.data; const text = Answer.value; let index = 0; const outputElement = document.getElementById('text'); function printText() { if (index < text.length) { outputElement.append(document.createTextNode(text.charAt(index))); index++; setTimeout(printText, 50); // 控制打印速度,单位为毫秒 //持续设置高度 outputElement.scrollTop=outputElement.scrollHeight } else { const elDivider = document.createElement('br'); outputElement.appendChild(elDivider); const hr = document.createElement('hr'); outputElement.appendChild(hr); } } printText(); } if (result.data.code==1){ return ElNotification({ title: '消息', message: h('i', { style: 'color: teal' },result.data.data), }) } }).catch(error => { openoff.value=false return ElNotification({ title: '消息', message: h('i', { style: 'color: teal' },error.message), }) }).finally(()=>{ openoff.value=false }); } ```