后台搭建:
添加依赖:
<dependency>
<groupId>io.springboot.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
<version>1.0.3</version>
</dependency>
添加配置:
spring:
ai:
openai:
api-key: 你的APi key
model: gpt-3.5-turbo
base-url: https://api.xty.app/
chat:
options:
temperature: 0.7
controller:
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.openai.OpenAiChatClient;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
@RestController
@Slf4j
public class OpenAiController {
private final OpenAiChatClient chatClient;
public OpenAiController(OpenAiChatClient chatClient) {
this.chatClient = chatClient; }
/**
* 同步方式实现聊天功能
*
* @param prompt 提示词
*/
@GetMapping("/chat")
public String chat(String prompt) {
return chatClient.call(prompt);
}
/***
* 流式方式实现聊天功能
* @param prompt 提示词
*/
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> stream(String prompt) {
log.info("prompt: {}", prompt);
return chatClient.stream(prompt);
}
}
uiapp代码:
<template>
<view class="message-content">
<scroll-view class="message-wrapper" :scroll-into-view="bottomId" :scroll-y="true"
:scroll-with-animation="true">
<block v-for="(item,index) in messageList">
<block v-if="item.from=='robot'">
<view class="message-item-box">
<view class="message-item-avatar-box">
<img class="avatar" src="@/static/tqb-tabbar/add.png" />
</view>
<view class="message-item-content-box">
<view class="message-item-content-message">{{item.message}}</view>
</view>
</view>
</block>
<block v-else>
<view class="message-item-box message-item-box-right">
<view class="message-item-content-box">
<view class="message-item-content-message">{{item.message}}</view>
</view>
<view class="message-item-avatar-box">
<img class="avatar" src="@/static/tqb-tabbar/add2.png" />
</view>
</view>
</block>
</block>
<block v-if="disabled && resultText.length<1">
<view class="message-item-box">
<view class="message-item-avatar-box">
<img class="avatar" src="@/static/tqb-tabbar/add.png" />
</view>
<view class="message-item-content-box">
<view class="message-item-content-message">
<view class="loading">
<view class="span"></view>
<view class="span"></view>
<view class="span"></view>
<view class="span"></view>
<view class="span"></view>
<view class="span"></view>
<view class="span"></view>
</view>
</view>
</view>
</view>
</block>
<block v-if="resultText">
<view class="message-item-box">
<view class="message-item-avatar-box">
<img class="avatar" src="@/static/tqb-tabbar/add.png" />
</view>
<view class="message-item-content-box">
<view class="message-item-content-message">{{resultText}}</view>
</view>
</view>
</block>
<view id="bottom" class="bottom"></view>
</scroll-view>
</view>
<view class="message-clean-box" @click="cleanMessage">
<view class="message-clean-icon icon-qingkong"></view>
<view class="message-clean-label">清除对话</view>
</view>
<view class="tqb-tabbar-body">
<input placeholder="请输入您想问的内容···" v-model="prompt" />
<block v-if="prompt.length && !disabled">
<view class="send-button iconfont icon-paper" @click="sendMessage">
</view>
</block>
<block v-else>
<view class="send-button iconfont icon-paper send-disabled">
</view>
</block>
</view>
</template>
style:
<style lang="scss" scoped>
.tqb-tabbar-body {
border-radius: 80rpx;
height: 120rpx;
display: flex;
justify-content: space-between;
align-items: center;
background: linear-gradient(to right, #ffffff, #F1EDFF);
position: fixed;
bottom: 68rpx;
width: 704rpx;
left: 24rpx;
z-index: 1001;
box-shadow: 0px 0px 22rpx 9rpx rgba(0, 0, 0, 0.1);
gap: 30rpx;
}
input {
display: inline-block;
flex: 1;
margin-left: 30rpx;
height: 80rpx;
font-size: 32rpx;
}
.send-button {
display: inline-block;
color: #ffffff;
border-radius: 50%;
width: 80rpx;
height: 80rpx;
background: #270ef4;
margin-right: 30rpx;
text-align: center;
line-height: 80rpx;
}
.send-disabled {
color: #fafafa !important;
background: #add8e6 !important;
}
input::-webkit-input-placeholder {
color: #BBBBBB !important;
font-size: 34rpx;
}
.message-content {
width: 100vw;
height: 100vh;
overflow: hidden;
}
.message-wrapper {
width: 100vw;
height: 100vh;
max-width: 100vw;
max-height: 100vh;
background: #add8e6;
.message-item-box:last-child {
padding-bottom: 300rpx !important;
}
}
.message-clean-box {
position: fixed;
align-items: center;
gap: 6rpx;
z-index: 1001;
left: 30rpx;
bottom: 200rpx;
display: flex;
background: linear-gradient(to right, #ffffff, #F1EDFF);
box-shadow: 0px 0px 10rpx 6rpx rgba(0, 0, 0, 0.1);
padding: 10rpx 20rpx;
border-radius: 40rpx;
font-size: 24rpx;
color: #270ef4;
.message-clean-label {
letter-spacing: 6rpx;
}
}
.message-item-box {
padding: 20rpx;
color: #270ef4;
font-size: 30rpx;
display: flex;
align-items: flex-start;
gap: 20rpx;
.message-item-avatar-box {
width: 80rpx;
height: 80rpx;
.avatar {
width: 80rpx;
height: 80rpx;
}
}
.message-item-content-box {
.message-item-content-message {
font-size: 30rpx;
word-break: break-all;
background: #FFFFFF;
max-width: 480rpx;
padding: 20rpx;
border-radius: 0 12rpx 12rpx 12rpx;
}
}
}
.message-item-box-right {
justify-content: flex-end;
color: #FFFFFF;
.message-item-content-box {
.message-item-content-message {
background: #270ef4;
border-radius: 12rpx 0 12rpx 12rpx;
}
}
}
.bottom {
padding-bottom: 300rpx;
}
.loading .span {
display: inline-block;
vertical-align: middle;
width: 6rpx;
height: 20rpx;
margin: 6rpx;
background: #add8e6;
border-radius: 2rpx;
animation: loading-5118fab1 1s infinite alternate;
}
.loading .span:nth-of-type(2) {
background: #008FB2;
animation-delay: 0.2s;
}
.loading .span:nth-of-type(3) {
background: #009B9E;
animation-delay: 0.4s;
}
.loading .span:nth-of-type(4) {
background: #00A77D;
animation-delay: 0.6s;
}
.loading .span:nth-of-type(5) {
background: #00B247;
animation-delay: 0.8s;
}
.loading .span:nth-of-type(6) {
background: #5AB027;
animation-delay: 1.0s;
}
.loading .span:nth-of-type(7) {
background: #270ef4;
animation-delay: 1.2s;
}
@keyframes loading {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
</style>
javaScript:
<script>
import BASE_API from '@/api/config.js'
export default {
components: {
},
data() {
return {
bottomId: "bottom",
disabled: false,
resultText: "",
prompt: "",
messageList: [
]
}
},
onLoad() {
},
methods: {
cleanMessage() {
let that = this;
setTimeout(function() {
that.messageList = [];
that.bottomId = "bottom";
}, 100);
},
scrollBottom() {
let that = this;
that.bottomId = "";
setTimeout(function() {
that.bottomId = "bottom";
}, 100);
},
sendMessage() {
let that = this;
let prompt = that.prompt;
that.messageList.push({
"from": "",
"message": prompt
});
that.disabled = true;
that.prompt = '';
that.scrollBottom();
const requestTask = uni.request({
url: BASE_API + '/stream',
timeout: 15000,
responseType: 'text',
method: 'GET',
enableChunked: true,
data: {
prompt: that.prompt
},
header: {
"Accept": "text/event-stream",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"Content-type": "application/json"
},
success: response => {
console.log("success");
console.log(response);
that.messageList.push({
"from": "robot",
"message": that.resultText
});
that.disabled = false;
that.resultText = "";
that.scrollBottom();
},
fail: error => {
that.disabled = false;
console.log("error");
console.log(error)
}
})
requestTask.onHeadersReceived(function(res) {
that.disabled = true;
that.prompt = '';
})
requestTask.onChunkReceived((res) => {
const uint8Array = new Uint8Array(res.data);
let text = String.fromCharCode.apply(null, uint8Array);
text = decodeURIComponent(escape(text));
if (text) {
let cleanedContent = text.replaceAll("data:", '');
let splitContent = cleanedContent.replace(/^,/, '').replace(/\r?\n/g, '');;
that.resultText += splitContent;
that.scrollBottom();
}
})
}
}
}
</script>
本人是后端,前端不太懂,最近也是别学别写,希望大佬看了嘴下留情,效果如下:
(因为白嫖的国内gpt,用了几次就没费用,也没充值,所以所问非所答(有条件的可以自己注册一个或者搭建个ollama服务)
总结:
1.text/event-stream 是一种用于服务器向客户端推送事件的媒体类型(Media Type)。它是基于 HTTP 协议的一种流式传输技术,也被称为 Server-Sent Events(SSE)。允许客户端发起一起请求,服务端分批响应给客户端,客户端一次解析数据。SSE 响应解析流程:
-
先使用 \n\n 进行分割
-
分割的每个元素使用 \n 进行分割,从而得到 event 和 data
-
对 data 进行序列化解析
后端影响sse如下:
小程序请求如下:
小程序端解析如图: