搭建网页版ChatGPT(三)
搭建网页版 ChatGPT(三)
前言
我们继续上一篇的内容,前面我们完成了对话功能,本小节主要介绍如何请求 chatGPT 以及如何监听 chatGPT 的响应,包括前端和后端的处理。
前端
由于项目一开始就引入了 Axios,于是就尝试看看 Axios 能不能处理 stream,于是写出如下代码:
export async function requestChatStream(
req: ChatRequest,
options?: {
onMessage: (message: string, done: boolean) => void;
onError: (error: Error, statusCode?: number) => void;
}
) {
console.log('[Request] ', req);
return request(`/chat/${req.type}/legacy`, {
method: 'POST',
data: { message: req.content },
headers: {
'Content-Type': 'application/json',
},
responseType: 'stream',
}).then((response) => {
console.log('response', response);
});
}
但是发现返回的结果却是这样的:
这里的 data 是等了一会然后返回整个数据,这并不是我们想要的,我们想要的是实时返回数据。
然后尝试使用 fetch,代码如下:
export async function requestChatStream(
req: ChatRequest,
options?: {
onMessage: (message: string, done: boolean) => void;
onError: (error: Error, statusCode?: number) => void;
}
) {
try {
const res = await fetch(`${baseURL}/chat/ask`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ message: req.message }),
});
let responseText = '';
if (res.ok) {
const reader = res.body?.getReader();
const decoder = new TextDecoder();
while (true) {
const content = await reader?.read();
if (!content || !content.value) {
break;
}
const text = decoder.decode(content.value, { stream: true });
responseText += text;
const done = content.done;
options?.onMessage(responseText, false);
if (done) {
break;
}
}
options?.onMessage(responseText, true);
} else {
console.error('Stream Error', res.body);
options?.onError(new Error('Stream Error'), res.status);
}
} catch (err) {
console.error('NetWork Error', err);
options?.onError(err as Error);
}
}
发现 fetch 是满足我们需求的,但是后面发现 EventSource
才是专门处理 stream 的,但是 fetch 现在也没发现什么问题,所以就先用着吧。
后端
以下是后端的处理逻辑,主要是 SseEmitter
的处理。
@Slf4j
@RestController
@RequestMapping("/chat")
public class OfficialChatController {
@Autowired
private OfficialChatService chatService;
@PostMapping("/ask")
public SseEmitter legacy(@RequestBody LegacyReq request) {
return chatService.legacyAsk(request);
}
}
这里要注意,如果我们使用默认的 SseEmitter
很可能会出现乱码,所以我做了如下处理
/**
* 解决乱码
*/
public class SseEmitterUTF8 extends SseEmitter {
public SseEmitterUTF8(Long timeout) {
super(timeout);
}
@Override
protected void extendResponse(ServerHttpResponse outputMessage) {
super.extendResponse(outputMessage);
HttpHeaders headers = outputMessage.getHeaders();
headers.setContentType(new MediaType(MediaType.TEXT_EVENT_STREAM, StandardCharsets.UTF_8));
}
}
以下是 OfficialChatService
@Slf4j
@Service
public class OfficialChatServiceImpl implements OfficialChatService {
private final CustomOkHttpExecutor httpExecutor;
public OfficialChatServiceImpl(CustomOkHttpExecutor httpExecutor) {
this.httpExecutor = httpExecutor;
}
@Override
public SseEmitter legacyAsk(LegacyReq request) {
SseEmitter emitter = new SseEmitterUTF8(0L);
EmitterHandler emitterHandler = new EmitterHandler(emitter, httpExecutor);
return emitterHandler.handle(request);
}
}
以下是 EmitterHandler
@Slf4j
public class EmitterHandler {
private final SseEmitter emitter;
private final CustomOkHttpExecutor httpExecutor;
public EmitterHandler(SseEmitter emitter, CustomOkHttpExecutor httpExecutor) {
this.emitter = emitter;
this.httpExecutor = httpExecutor;
}
public SseEmitter handle(LegacyReq request) {
askOpenAi(request);
return emitter;
}
private void askOpenAi(LegacyReq request) {
HttpRequest httpRequest = buildHttpRequest(request);
Response chatResponse;
try {
chatResponse = httpExecutor.chat(httpRequest);
} catch (HttpException e) {
emitter.complete();
e.printStackTrace();
return;
}
try (ResponseBody responseBody = chatResponse.body();
InputStream inputStream = responseBody.byteStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream))) {
String line;
boolean first = true;
while ((line = bufferedReader.readLine()) != null) {
if (StringUtils.hasLength(line)) {
if (!printMessage(!first ? "\n" + line: line)) {
break;
}
}
first = false;
}
} catch (IOException e) {
log.error("ResponseBody读取错误");
e.printStackTrace();
} finally {
emitter.complete();
}
}
private boolean printMessage(String reply) {
try {
char[] messages = reply.toCharArray();
for (char message : messages) {
TimeUnit.MILLISECONDS.sleep(20);
if (!sendData2Client(String.valueOf(message))){
return false;
}
}
return true;
} catch (InterruptedException e) {
log.error("ResponseBody读取错误");
e.printStackTrace();
return false;
}
}
private boolean sendData2Client(String data) {
try {
String text = "{" + data + "}";
emitter.send(SseEmitter.event().data(text));
return true;
} catch (IOException e) {
log.error("向客户端发送消息时出现异常");
e.printStackTrace();
}
return false;
}
protected HttpRequest buildHttpRequest(LegacyReq chatRequest) {
HttpRequest httpRequest = new HttpRequest();
httpRequest.setUrl(URL);
httpRequest.setMethod(HttpConstant.Method.POST.name());
httpRequest.setData(JSON.toJSON(chatRequest));
//...
return httpRequest;
}
}
最后
如果喜欢我的文章,欢迎关注我,同时也欢迎关注我的个人网站,里面有多个我一直迭代的项目,
还有我的 b 站,我会定期在 b 站分享一些你意想不到的知识。