第1章:效果演示与环境介绍
效果演示:见视频地址,上某站搜索菩提老师免费看视频
环境介绍:
后端:Spring Boot3.4.2+JDK21
前端:Vue3.3.4+Vite4.4.6
第2章:从零搭建SpringBoot3 Web服务
1、创建Spring Boot3空的Web项目
选择两个依赖项:
2、修改pom.xml文件
1、删除暂时无用的标签
2、增加pom属性
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>21</java.version>
</properties>
3、添加依赖:
<dependencies>
<!-- web服务-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 这里由于后期需要把前端项目不分离的融合到后端,所以引入Thymeleaf模板引擎-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- 阿里云百炼大模型的SDK-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dashscope-sdk-java</artifactId>
<version>2.18.2</version>
</dependency>
<!-- lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
4、修改build打包标签
<build>
<finalName>AiApp</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
5、完整的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>3.4.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.ai</groupId>
<artifactId>ai-admin</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>ai-admin</name>
<description>ai-admin</description>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>21</java.version>
</properties>
<dependencies>
<!-- web服务-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 这里由于后期需要把前端项目不分离的融合到后端,所以引入Thymeleaf模板引擎-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- 阿里云百炼大模型的SDK-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dashscope-sdk-java</artifactId>
<version>2.18.2</version>
</dependency>
<!-- lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.32</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<finalName>AiApp</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
3、编写AI控制器,先写一个空白页面试试效果
思考这里为什么不是@RestController注解?
@Controller
@Slf4j
public class AiController {
@RequestMapping("/")
String index() {
return "index";
}
}
再编写一个index.html文件,内容随便写,比如哈哈哈。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>hello Ai</h1>
</body>
</html>
注意:index.html文件需要放到resources资源目录下的templates目录下,关于Thymeleaf模板引擎不了解的可以去看菩提老师的文档或课程
4、修改application.properties文件
把application.properties改为application.yml,不改也行,但为什么要改?那就需要你知道yml文件比properties文件的优势了。
至于里边的配置,不改也罢,端口改为80,方便开发,浏览器懒得输入端口号。
5、启动主启动程序,访问http://localhost
6、修改项目的编译为JDK21
7、在AI控制器中增加/sse接口
sse技术叫(Server Event Send)服务端单向推送消息给客户端,跟webSocket双向长连接有些区别,当然也可以使用WebSocket,这里我们就使用SSE技术。
@Controller
@Slf4j
public class AiController {
SseEmitter emitter = new SseEmitter();
@RequestMapping("/")
String index() {
return "index";
}
@GetMapping(value = "/sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
@ResponseBody
public SseEmitter streamEvents() {
emitter = new SseEmitter();
return emitter;
}
}
由于这里需要返回一个SseEmitter 对象,所以这里需要加@ResponseBody
创建的这个SseEmitter 对象,是用来给前端不断推送大模型消息的,所以应该把他存起来,当前端连接这个sse接口的时候,就会产生一个新的SseEmitter对象,因此,需要把这个对象保存起来。这里为了简单,我们就连接一次就创建一个唯一的,覆盖之前的,实际是每一个前端连接都会创建一个专属于它的连接对象,所以应该按照Map把每个连接对象存起来,到时候再按照key取出前端自己的SseEmitter对象进行消息的推送。
8、获取阿里百炼大模型的API-Key
登录阿里云控制台,找到大模型服务平台百炼
创建一个apKey,等会备用
9、准备一个接受请求的请求对象AiReq:
package com.ai.aiadmin.pojo.req;
import com.alibaba.dashscope.common.Message;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
@Data
public class AiReq implements Serializable {
private String mode;// 调用哪种大模型,比如deepseek-r1、qianwen-plus等
private List<Message> msgs;// 与对话的消息和用户输入的消息
private boolean incrementalOutput;// 是否增量输出
}
10、准备一个ResponseDTO对象,统一给前端的返回对象
package com.ai.aiadmin.pojo.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ResponseDTO<T> implements Serializable {
/**
* 返回的消息
*/
private String msg;
/**
* 返回成功的消息
*/
private static String successMsg = "操作成功";
/**
* 返回错误的消息
*/
private static String errorMsg = "操作失败";
/**
* 返回的状态
*/
private Integer status = 200;
/**
* 返回的结果
*/
private boolean res = true;
/**
* 返回的错误码
*/
private Integer errorCode;
/**
* 返回的数据
*/
private T data;
/**
* 返回成功的Code
*/
private Integer okCode;
/**
* 返回为成功的构造函数
*
* @param msg
* @param data
*/
public ResponseDTO(String msg, T data) {
this.msg = msg;
this.data = data;
}
public ResponseDTO(String msg, T data, Integer okCode) {
this.msg = msg;
this.data = data;
this.okCode = okCode;
}
/**
* 构造失败的回调结果
*
* @param msg
* @param data
* @param errorCode
* @param status
*/
public ResponseDTO(String msg, T data, Integer errorCode, Integer status) {
this.msg = msg;
this.data = data;
this.errorCode = errorCode;
this.status = status;
this.res = false;
}
public ResponseDTO(String msg, int status, Boolean res, T data) {
this.msg = msg;
this.res = res;
this.status = status;
this.data = data;
}
/**
* 调用成功的时候,返回成功的状态
*
* @param msg
* @param data
* @return
*/
public static <T> ResponseDTO<T> ok(String msg, T data) {
return new ResponseDTO(msg, data);
}
/**
* 调用成功的时候,返回成功的状态
*
* @param msg
* @param data
* @return
*/
public static <T> ResponseDTO<T> ok(String msg, T data, Integer okCode) {
return new ResponseDTO(msg, data, okCode);
}
/**
* 调用成功的时候,返回成功的状态
*
* @return
*/
public static <T> ResponseDTO<T> get(boolean res) {
if (res) {
return ResponseDTO.ok(successMsg);
} else {
return ResponseDTO.fail(errorMsg);
}
}
public static <T> ResponseDTO<T> get(T data) {
if (data != null) {
return ResponseDTO.ok(successMsg, data);
} else {
return ResponseDTO.fail(errorMsg);
}
}
public static <T> ResponseDTO<T> ok(String msg) {
return new ResponseDTO(msg, null);
}
public static <T> ResponseDTO<T> ok() {
return new ResponseDTO("操作成功", null);
}
/**
* 调用失败的时候,返回失败的状态
*
* @param msg
* @param data
* @return
*/
public static <T> ResponseDTO<T> fail(String msg, T data, Integer errorCode, Integer status) {
return new ResponseDTO(msg, data, errorCode, status);
}
/**
* 调用失败的时候,返回失败的状态
*/
public static <T> ResponseDTO<T> fail(String msg) {
return new ResponseDTO(msg, null, null, null);
}
public static <T> ResponseDTO<T> fail(String msg,T data) {
return new ResponseDTO(msg, data, null, null);
}
public static <T> ResponseDTO<T> fail() {
return new ResponseDTO(errorMsg, null, null, null);
}
}
11、在AiController编写一个接口,接受用户点击发送时候传递的请求数据
// 接受前端send的请求数据
@RequestMapping("/ai")
@ResponseBody
public ResponseDTO<Boolean> ai(@RequestBody AiReq aiReq) {
CompletableFuture.runAsync(() -> {
GenerationParam param = buildGenerationParam(aiReq);
Flowable<GenerationResult> result = null;
try {
result = new Generation().streamCall(param);
} catch (NoApiKeyException e) {
throw new RuntimeException(e);
} catch (InputRequiredException e) {
throw new RuntimeException(e);
}
result.blockingForEach(this::handleGenerationResult);
});
return ResponseDTO.ok();
}
// 生成大模型的参数
private static GenerationParam buildGenerationParam(AiReq aiReq) {
return GenerationParam.builder()
// 若没有配置环境变量,请用百炼API Key将下行替换为:.apiKey("sk-xxx")
.apiKey("第八步的API-KEY")
.model(aiReq.getMode())
.messages(aiReq.getMsgs())
.resultFormat(GenerationParam.ResultFormat.MESSAGE)
.incrementalOutput(true)
.build();
}
// 处理大模型调用返回的结果
private void handleGenerationResult(GenerationResult message) {
log.debug(":{}",message);
Message msg = message.getOutput().getChoices().get(0).getMessage();
try {
// 用emitter给前端推送消息
emitter.send(SseEmitter.event()
.data(msg)
.id(message.getRequestId()));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
12、配置跨域请求和前端静态资源代理
package com.ai.aiadmin.config;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* creator:菩提老师
/**
* webMVC的配置类
* 这个就是MVC核心接口:能够配置拦截器,能够配置跨域访问,还能配置静态资源,还能配置很多东西
*/
@SpringBootConfiguration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/assets/**")
.addResourceLocations("classpath:/templates/dist/assets/");
registry.addResourceHandler("/config.js")
.addResourceLocations("classpath:/templates/dist/");
registry.addResourceHandler("/favicon.ico")
.addResourceLocations("classpath:/templates/dist/");
}
/**
* 配置跨域请求
*
* @param registry
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(false).maxAge(3600)
.allowedOrigins("*")
;
}
}
这里跨域请求主要是为了支持开发环境时前后端分离的请求
配置静态资源代理是为了前端编写好之后,放到resources/templates目录下做静态资源配置用的
🚩需要注意的是,vue项目打包之后,只有一个index.html文件,并且他的目录结构是这样的:
所以为了方便我们直接复制dist目录,于是在WebMvcConfig配置类中是那样写,同时,AIController中需要跳转到dist目录下的index.html文件:
@RequestMapping("/")
String index() {
return "dist/index";
}
这样才能正确展示
后端先告一段落。
第3章:编写Vue3前端聊天界面:
1、创建Vue3空项目
简单说一下从零创建Vue3.x项目,会的直接跳过,也可以全面更详细的去看菩提老师讲的Vue3.x+Vite4企业实战。
首先安装Nodejs:Node.js — Run JavaScript Everywhere,Nodejs是对JavaScript进行编译的一个工具,这样就可以在没有浏览器的情况下,也可以使用JS,好比Java的JDK。
查看安装的版本
C:\Users\23737>node -v
v18.17.1
C:\Users\23737>npm -v
9.6.7
看到以上的信息,表示nodejs安装好了,接下来配置淘宝镜像:
1.通过cnpm使用淘宝镜像:npm install -g cnpm --registry=http://registry.npm.taobao.org
2.将npm设置为淘宝镜像:npm config set registry http://registry.npm.taobao.org
3.将cnpm设置为淘宝镜像:cnpm config set registry http://registry.npm.taobao.org
4.查看npm镜像设置:npm config get registry
5.查看cnpm镜像设置:cnpm config get registry
开发前端项目的常用或者热门开发用具IDEA(VS Code(免费)、WebStorm(idea一个公司( JetBrains),收费)、Idea(可以用来开发前端项目) )
目前Vue官网的是 vue3.7.3+Vite4+
2、前端要新,后端要稳
cnpm init vue@latest(注意:需要先把Nodejs安装上,你可以理解为类似Maven工具,下载依赖用的,类似Spring Boot Web项目))
选择各种选项:
✔ Project name: … <your-project-name> #项目名称 默认就是文件夹名称
✔ Add TypeScript? … No / Yes 是否增加TypeScript语法,(就是js带类型)选择(NO)
✔ Add JSX Support? … No / Yes JS语法扩展选择(NO)
✔ Add Vue Router for Single Page Application development? … No / Yes (Vue路由:实现单页应用)选择(Yes)
✔ Add Pinia for state management? … No / Yes (状态管理 早的时候是VueX,现在是pinia)选择(Yes)
✔ Add Vitest for Unit testing? … No / Yes 选择(测试?NO)
✔ Add Cypress for both Unit and End-to-End testing? … No / Yes 选择(No)
✔ Add ESLint for code quality? … No / Yes 选择(No)
✔ Add Prettier for code formatting? … No / Yes 选择(NO)
Scaffolding project in ./<your-project-name>...
Done.
进入到项目所在的目录
cd
安装依赖:
cnpm install
运行:cnpm run dev
打包:cnpm run build
OK,前端项目搭建完毕!
3、封装前端请求工具:RequestUtil.ts
import axios from 'axios';
import {message} from "ant-design-vue";
import router from "@/router/router";
// 构建axios实例
export const http = axios.create({
baseURL: import.meta.env.BASE_URL, // 该处url会根据开发环境进行变化(开发/发布)
timeout: 2000000, // 设置请求超时连接时间
})
let running = false;
export const tokenName: string = 'lzket.token';
console.log('域名:', import.meta.env.VITE_domain)
// 域名配置:重要的!
export const domainConfig = {
https: import.meta.env.MODE == 'development' ? window.config.dev.https : window.config.pro.https,
domain: import.meta.env.MODE == 'development' ? window.config.dev.domain : window.config.pro.domain,
getApiPrefix() {
return this.getUrlPrefix()
},
getUrlPrefix() {
return this.https ? "https://" : "http://" + this.domain
}
}
http.interceptors.request.use(
async (config: any) => {
config.url = domainConfig.getApiPrefix() + config.url;
return config // 对config处理完后返回,下一步将向后端发送请求
},
error => { // 当发生错误时,执行该部分代码
console.log(error); //调试用
return Promise.reject(error)
}
)
http.interceptors.response.use(
(response: any) => { // 该处为后端返回整个内容
let res = response.data; // 该处可将后端数据取出,提前做一个处理
// console.log('后端返回对象', response);
let token = response.headers.token;// 后端返回的token
if (response.headers.token) {
localStorage.setItem(tokenName, token);
localStorage.setItem("tokenexpiration", response.headers.tokenexpiration);
}
if (res.errorCode == 401) {
// session.showLoginModal = true;
message.warn(res.msg);
console.log('goBackUrl', window.location.href);
localStorage.setItem('goBackUrl', window.location.href)
router.push('/loginPage')
return Promise.reject('401未登录')
} else {
return res;// 默认都返回
}
},
error => {
// console.log(error);
message.error(error.message)
return Promise.reject(error)
}
)
4、编写消息组件
<script setup lang="ts">
import {LoadingOutlined} from '@ant-design/icons-vue'
let p = defineProps(['msg']);
</script>
<template>
<div class="out-ai-msg">
<div v-if="p.msg.role=='assistant'">
<div style="display: flex">
<div>
<a-tag color="success">{{ p.msg.agent }}:</a-tag>
</div>
<div style="flex: 1;text-align: left">
<div class="ponder" v-if="p.msg.reasoningContent" v-html="p.msg.reasoningContent"></div>
<LoadingOutlined v-show="p.msg.loading"/>
<div v-html="p.msg.markedContent"></div>
</div>
</div>
</div>
<div v-else-if="p.msg.role=='user'" class="out-msg">
<div class="user-msg">
<div>{{ p.msg.content }}</div>
</div>
</div>
</div>
</template>
<style scoped lang="less">
.out-ai-msg {
.ponder {
color: #8b8b8b;
white-space: pre-wrap;
margin: 0;
padding: 0 0 10px 13px;
line-height: 26px;
position: relative;
border-left: solid 2px lightgray;
}
.out-msg {
justify-content: flex-end;
margin-bottom: 16px;
padding-bottom: 32px;
display: flex;
.user-msg {
display: inline-block;
background: #eff6ff;
padding: 8px 20px;
font-size: var(--ds-font-size-l);
line-height: var(--ds-line-height-l);
color: #262626;
box-sizing: border-box;
white-space: pre-wrap;
word-break: break-word;
border-radius: 14px;
max-width: calc(100% - 48px);
position: relative;
}
}
}
</style>
5、把静态资源,比如图片字体拷贝到assets目录下去
6、注意事项:
由于大模型返回的是markedown文档格式的文字,所以我们需要把大模型返回的文本转换成markedown文档,因此需要安装marked依赖包在前端:
npm install marked
此外,我们为了记录每个消息,包括用户发送的消息的唯一性,安装uuid依赖,在前端生成消息的唯一id,后端返回的大模型消息是自带uuid的,因此只需要在前端发送消息的时候生成uuid作为msg的唯一id即可
npm install uuid
7、编写聊天界面:index-main.vue
<template>
<a-layout class="cm-ai">
<a-layout-sider class="sider" width="260">
<div class="logo-div">
AI Assistant
</div>
<div>
<a-space>
<a-avatar :src="putiSrc" />
<div style="text-align: left">
菩提老师<br>WX:zhihoubuhuo
</div>
</a-space>
</div>
<div>
<a-list size="small" :data-source="questionItems">
<template #renderItem="{ item }">
<a-list-item @click="viewHistory(item)"
:style="{background:d.currentItemKey==item.key?'#e6f4ff': '',color:d.currentItemKey==item.key?'#3a8cff':'',border:'none'}">
<div>
{{ item.label }}
</div>
<template #actions>
<a-popconfirm
title="Are you sure delete this history?"
ok-text="Yes"
cancel-text="No"
@confirm="deleteHistory(item)"
>
<DeleteOutlined/>
</a-popconfirm>
</template>
</a-list-item>
</template>
<template #header>
<div>question history</div>
</template>
</a-list>
</div>
</a-layout-sider>
<a-layout>
<!-- <a-layout-header :style="headerStyle">FAB · AI Tool Solution</a-layout-header>-->
<a-layout-content :style="contentStyle">
<div
style="align-items: center;justify-content: center;display: flex;padding: 30px 10px 0 0;height: calc(100vh - 260px)">
<div ref="msgDiv" style="width: 800px;height: 100%;overflow-y: auto;">
<msg v-for="msg in d.msgs" :msg="msg"></msg>
</div>
</div>
</a-layout-content>
<a-layout-footer :style="footerStyle">
<div style="align-items: center;justify-content: center;display: flex;">
<div style="width: 800px">
<a-card size="small" style="max-width: 798px;width: 100%;min-width: 200px">
<!-- <template #extra v-if="d.mode=='deepseek-r1'">-->
<!-- <a-switch v-model:checked="d.incrementalOutput" checked-children="思考过程-开" un-checked-children="思考过程-关" />-->
<!-- </template>-->
<template #title>
<a-space>
model:
<a-select
ref="select"
v-model:value="d.mode"
style="min-width: 200px"
>
<a-select-option value="cm-model" disabled>CM-Model (And So On)</a-select-option>
<a-select-option value="deepseek-r1">DeepSeek R1</a-select-option>
<a-select-option value="qwen-plus">千问-Plus</a-select-option>
<a-select-option value="llama3.3-70b-instruct">Llama3.3</a-select-option>
<a-select-option value="abab6.5s-chat">MiniMax大模型</a-select-option>
</a-select>
</a-space>
</template>
<a-textarea @keydown="(e)=>{if(e.keyCode==13&&!e.shiftKey){sendMsg()}}" v-model:value="d.content"
placeholder="Send messages to AI" :rows="d.rows"
:autoSize="{ minRows: 2, maxRows: 6 }"/>
<div>
<a-button type="primary" :disabled="!d.canSend" @click="sendMsg">Send</a-button>
</div>
</a-card>
</div>
</div>
Content is generated by AI, please check it carefully
</a-layout-footer>
</a-layout>
</a-layout>
</template>
<script lang="ts" setup>
import {computed, type CSSProperties, onMounted, reactive, ref} from 'vue';
import Msg from "@/components/msg.vue";
import {domainConfig, http} from "@/util/RequestUtil";
import {message} from "ant-design-vue";
import {v4} from 'uuid';
import {DeleteOutlined} from '@ant-design/icons-vue'
const msgDiv = ref(null);
import {marked} from 'marked';
import putiSrc from '@/assets/images/puti.jpg'
import wxImg from '@/assets/my_wx.png'
import codeImg from '@/assets/wx_puclic_cod.jpg'
let questionItems = computed(() => {
return d.questions.map(q => ({
key: q.userMsg.id,
label: q.userMsg.content,
question: q
}))
})
let deleteHistory = (item) => {
d.questions.splice(d.questions.findIndex(e => e.userMsg.id == item.key), 1);
localStorage.setItem("cm-questions", JSON.stringify(d.questions));
}
let loadLocalStorageQuestions = () => {
let questions = localStorage.getItem("cm-questions");
if (questions == null) {
questions = JSON.stringify([]);
}
d.questions = JSON.parse(questions);
}
let d = reactive({
questions: [],
currentItemKey: '',
selectedKeys: [],
incrementalOutput: true,
content: '',
userCurrentHistoryContent: '',
mode: 'deepseek-r1',
rows: 2,
msgs: [],
canSend: true,
})
let viewHistory = (item) => {
d.msgs = [];
d.msgs[0] = item.question.userMsg
d.msgs[1] = item.question.aiMsg
d.currentItemKey = item.question.userMsg.id
}
onMounted(() => {
loadLocalStorageQuestions();
let eventSource = new EventSource(domainConfig.getApiPrefix()+'/sse');
eventSource.onmessage = function (event) {
console.log(event)
let msg = JSON.parse(event.data);
msg.id = event.lastEventId;
msg.agent = d.mode;
console.log('msg:', msg)
let old = d.msgs.find(e => e.id === msg.id);
if (old) {
let loading1 = true;
let loading2 = true;
if (msg.reasoningContent) {
loading1 = true;
old.reasoningContent += msg.reasoningContent;
} else {
loading1 = false;
}
if (msg.content) {
loading2 = true;
old.content += msg.content;
old.markedContent = marked.parse(old.content);
} else {
loading2 = false;
}
old.loading = loading1 || loading2;// loading为true表示在编写中,false表示本次对话结束
if (!old.loading) {
d.canSend = true;
let q = {
userMsg: {role: 'user', content: d.userCurrentHistoryContent, id: v4()},
aiMsg: old
}
d.questions.push(q);
localStorage.setItem("cm-questions", JSON.stringify(d.questions));
} else {
d.canSend = false
}
} else {
msg.loading = true;
msg.markedContent = marked.parse(msg.content);
d.msgs.push(msg);
}
scrollMsgDivToBottom();
};
eventSource.onerror = function (err) {
console.error("EventSource failed:", err);
eventSource.close();
};
})
const scrollMsgDivToBottom = () => {
if (msgDiv.value) {
// 检查内容高度是否超过可见高度
if (msgDiv.value.scrollHeight > msgDiv.value.clientHeight+5) {
msgDiv.value.scrollTop = msgDiv.value.scrollHeight;
}
}
};
let sendMsg = () => {
if (d.content && d.content.trim().length > 0) {
d.canSend = false
d.userCurrentHistoryContent=d.content
d.msgs.push({role: 'user', content: d.content})
http.post('/ai', {msgs: d.msgs, mode: d.mode}).then(res => {
message.success('success');
d.content='';
})
} else {
message.warn('The input cannot be empty !')
}
}
const headerStyle: CSSProperties = {
textAlign: 'center',
color: '#fff',
height: 64,
paddingInline: 50,
lineHeight: '64px',
};
const contentStyle: CSSProperties = {
textAlign: 'center',
minHeight: 120,
backgroundColor: '#ffffff',
};
const footerStyle: CSSProperties = {
textAlign: 'center',
color: 'lightgray',
backgroundColor: '#ffffff',
};
</script>
<style lang="less">
.cm-ai {
.logo-div {
line-height: 50px;
font-weight: bold;
font-size: 20px
}
.sider {
text-align: center;
height: 100vh;
background-color: #f9fbff;
width: 300px;
}
.ant-card-body {
padding: 2px 2px 6px 2px;
}
.ant-input {
border-radius: 6px;
border: none;
//border: solid red 1px;
}
// 去掉 textarea 聚焦时的边框
.ant-input:focus {
border-color: transparent;
box-shadow: none;
}
}
</style>
8、启动前后端,进行联调:
发现是可以的,灰色部分是深度DeepSeek R1的思考过程,黑色字是正式回答:
但是我们发现了一个警告:
这个是后端异步调用大模型超时的问题,解决办法,在yml文件中增加配置:增加异步超时时间为1小时,重新启动
server: port: 80 # 端口改为80,方便开发 spring: mvc: async: request-timeout: 3600000 # 1h 超时时间为1个小时
看来还是比较聪明的,真是个大聪明,希望能帮助到你。
第四章:项目打包、外置API-KEY。
项目写完了,需要打包,并且API-KEY写到代码中写死,很不科学,如果要给别人使用,别人要用自己的API-KEY,又不修改代码,那么API-KEY就需要从配置文件读取。
于是在application.yml文件中增加配置名称:
server:
port: 80 # 端口改为80,方便开发
spring:
mvc:
async:
request-timeout: 3600000 # 1h
apiKey: '' # 启动的时候指定 apiKey 自定义的名称
1、修改AIController代码:
@Value("${apiKey:}")
String apiKey;
// 生成大模型的参数
private GenerationParam buildGenerationParam(AiReq aiReq) {
return GenerationParam.builder()
// 若没有配置环境变量,请用百炼API Key将下行替换为:.apiKey("sk-xxx")
.apiKey(apiKey)
.model(aiReq.getMode())
.messages(aiReq.getMsgs())
.resultFormat(GenerationParam.ResultFormat.MESSAGE)
.incrementalOutput(true)
.build();
}
2、把前端打包后的产物放到src/main/resources/templates目录下
3、启动项目自动打开浏览器的localhost地址:
修改主启动了:
package com.ai.aiadmin;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import java.io.IOException;
@SpringBootApplication
public class AiAdminApplication {
public static void main(String[] args) {
SpringApplication.run(AiAdminApplication.class, args);
try {
Runtime.getRuntime().exec("rundll32 url.dll,FileProtocolHandler http://localhost" );
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
4、打包项目:
5、构建产品
在桌面新建一个文件夹,比如ai-app
在文件夹中放入jdk21,放入刚才大号的Spring Boot jar包:
在写一个config.perperties文件,里边编写:
像这样:
6、编写启动脚本:start-app.bat
set aiapp=AiApp.jar
for /f "tokens=1" %%i in ('.\jdk-21\bin\jps -l ^| findstr %aiapp%') do (
taskkill /F /PID %%i
)
:: 读取 config.properties 文件中的 apiKey 值
for /f "tokens=2 delims==" %%a in ('findstr /b "apiKey=" config.properties') do set apiKey=%%a
:: 打印获取到的 apiKey 值
echo 获取到的 apiKey 值: %apiKey%
chcp 65001
.\jdk-21\bin\java -jar %aiapp% --apiKey=%apiKey%
pause
其实源码已经展示完了,粘贴就行,如果想需要完整项目的,call我吧。
7、双击脚本启动试试:
8、自动打开聊天对话窗口:
9、把产品文件夹压缩,发给别人使用吧。
注意:如果不想别人使用你的API-KEY,那就把config.properties文件中的值删除再发给别人