Java后端
pom.xml添加ChatGPT的依赖
<dependency>
<groupId>com.unfbx</groupId>
<artifactId>chatgpt-java</artifactId>
<version>1.0.10</version>
<!--排除子依赖 slf4j-simple 不然会有冲突 -->
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
</exclusion>
</exclusions>
</dependency>
SpringBootApplication启动文件配置
@SpringBootApplication
public class NotesApplication implements CommandLineRunner {
public static void main(String[] args) {
SpringApplication.run(NotesApplication.class, args);
}
@Override
public void run(String... args) throws Exception {
}
@Value("${chatgpt.apiKey}")
private List<String> apiKey;
@Value("${chatgpt.apiHost}")
private String apiHost;
@Bean
public OpenAiStreamClientSub openAiStreamClient() {
//本地开发调试条件: 1. 配置科学上网 2.配置IP代理
Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("127.0.0.1", 17890));
HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor(new OpenAILogger());
//!!!!!!测试或者发布到服务器千万不要配置Level == BODY!!!!
httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.HEADERS);
OkHttpClient okHttpClient = new OkHttpClient
.Builder()
.proxy(proxy)
.addInterceptor(httpLoggingInterceptor)
.connectTimeout(30, TimeUnit.SECONDS)
.writeTimeout(60, TimeUnit.SECONDS)
.readTimeout(600, TimeUnit.SECONDS)
.build();
System.out.println("使用的key:" + apiKey);
return OpenAiStreamClientSub
.builder()
.apiHost(apiHost)
.apiKey(apiKey)
//自定义key使用策略 默认随机策略
.keyStrategy(new KeyRandomStrategy())
.okHttpClient(okHttpClient)
.build();
}
}
注意本地开发配置条件:.配置IP代理 很关键,缺一不可
控制器Controller
/**
* AI助手端 SSE 服务端 推送 客户端
* @param headers
*/
@RequestMapping("/v1/chat/completions")
@CrossOrigin
public SseEmitter sseCompletions(@RequestHeader Map<String, String> headers) {
String uuid = headers.get("uuid");
//解码 内容部分
String promptContent = headers.get("promptcontent");
String content = null;
try {
content = URLDecoder.decode(promptContent, "utf-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
log.info("content:{}", content);
//默认30秒超时,设置为0L则永不超时
SseEmitter sseEmitter = new SseEmitter(0L);
long startTime = System.currentTimeMillis();
customChatGpt.sseCompletions(sseEmitter, uuid, content);
long endTime = System.currentTimeMillis();
log.info("请求耗时:{} ms", (endTime-startTime)/1000);
return sseEmitter;
}
- 需要配置 @CrossOrigin 注解: 全称"跨域资源共享"(Cross-origin resource sharing)
解决跨域问题,实现CORS的关键在于服务器,只要服务器实现CORS接口,就可以实现跨域通信
返回值为SseEmitter ,此处采用SSE推送技术实现,SseEmitter简介:SpringMVC提供的一种技术,可以实现服务端向客户端实时推送数据的功能,用法在Contorller中提供一个接口,返回SseEmitter对象,发送数据可以在另一个接口调用其send方法发送数据,SpringBoot已经集成这个功能,因此直接使用
生成ChatGPT喜欢的聊天列表,用于ChatGPT更好的理解与用户聊天的上下文
public void sseCompletions(SseEmitter sseEmitter, String uuid, String question) {
String key = String.format(messagesKey, uuid);
List<Message> messages = redisCache.getCacheObject(key);
if (StringUtils.isNotNull(messages)) {
if (messages.size() >= 5) {
messages = messages.subList(1, 5);
}
Message currentMessage = Message.builder().content(question).role(Message.Role.USER).build();
messages.add(currentMessage);
} else {
messages = new ArrayList<>();
Message currentMessage = Message.builder().content(question).role(Message.Role.USER).build();
messages.add(currentMessage);
}
OpenAISSEEventSourceListener eventSourceListener = new OpenAISSEEventSourceListener(sseEmitter);
openAiStreamClientSub.streamChatCompletion(messages, eventSourceListener);
redisCache.setCacheObject(key, messages, 50, TimeUnit.MINUTES );
}
1.3 重载 EventSourceListener 监听器 重写 onEvent 回调接口
@SneakyThrows
@Override
public void onEvent(EventSource eventSource, String id, String type, String data) {
log.info("OpenAI返回数据:{}", data);
if (data.equals("[DONE]")) {
log.info("OpenAI返回数据结束了");
sseEmitter.send(SseEmitter.event()
.id("[DONE]")
.data("[DONE]")
.reconnectTime(3000));
return;
}
ObjectMapper mapper = new ObjectMapper();
// 读取Json
ChatCompletionResponse completionResponse = mapper.readValue(data, ChatCompletionResponse.class);
sseEmitter.send(SseEmitter.event()
.id(completionResponse.getId())
.data(completionResponse.getChoices().get(0).getDelta())
.reconnectTime(3000));
}
构造http请求 v1/chat/completions 接口,发起对话
public void streamChatCompletion(ChatCompletion chatCompletion, EventSourceListener eventSourceListener) {
if (Objects.isNull(eventSourceListener)) {
log.error("参数异常:EventSourceListener不能为空,可以参考:com.unfbx.chatgpt.sse.ConsoleEventSourceListener");
throw new BaseException(CommonError.PARAM_ERROR);
} else {
if (!chatCompletion.isStream()) {
chatCompletion.setStream(true);
}
try {
EventSource.Factory factory = EventSources.createFactory(this.okHttpClient);
ObjectMapper mapper = new ObjectMapper();
String requestBody = mapper.writeValueAsString(chatCompletion);
Request request = (new okhttp3.Request.Builder()).url(this.apiHost + "v1/chat/completions").post(RequestBody.create(MediaType.parse(ContentType.JSON.getValue()), requestBody)).build();
factory.newEventSource(request, eventSourceListener);
} catch (JsonProcessingException var8) {
log.error("请求参数解析异常:{}", var8);
var8.printStackTrace();
} catch (Exception var9) {
log.error("请求参数解析异常:{}", var9);
var9.printStackTrace();
}
}
}
等待ChatGPT流式回答,效果如下
H5前端
下载依赖: npm install event-source-polyfill
创建SSE连接
createSSE(item){
let that = this
const itemAss = {
type: 'chat',
content: '回答中...',
my: false,
loading: true,
};
that.messages.push(itemAss)
that.disableInput()
that.setScroll()
if(window.EventSource){
this.eventSource = new EventSourcePolyfill(
//${config.baseUrl}/sse/openApi/chatGPT/v1/chat/sseCompletions
`http://localhost:7091/openApi/chatGPT/v1/chat/completions`, {
// 设置重连时间
heartbeatTimeout: 60 * 60 * 1000,
// 添加token
headers: {
'Authorization': `Bearer `,
'uuid': that.userName,
'promptcontent': encodeURIComponent(item.content)
},
});
this.eventSource.onopen = (e) => {
console.log("已建立SSE连接~")
}
this.eventSource.onmessage = (e) => {
//隐藏加载框
// uni.hideLoading();
let item = that.messages[that.messages.length - 1]
console.log("消息入栈:", e.data)
let result = e.data
//数据流开始标记
if(result === '{"role":"assistant"}'){
item.content = ''
}
//数据流结束标记
if(result === '[DONE]'){
uni.setStorageSync('isOpen', true)
that.closeSSE()
}
//内容
if(result.includes('content')){
let jsonResult = JSON.parse(result)
//内容进行拼接
item.content += jsonResult.content
that.setScroll()
}
}
this.eventSource.onerror = (e) => {
if (e.readyState == EventSource.CLOSED) {
console.log("SSE连接关闭", JSON.stringify(this.eventSource))
} else if (this.eventSource.readyState == EventSource.CONNECTING) {
console.log("SSE正在重连", JSON.stringify(this.eventSource))
} else {
console.log('error', e);
}
};
} else {
console.log("你的浏览器不支持SSE~")
}
},
问题及解决办法
配置本地代理