关于解决 ChatGPT 流数据处理的bug

前言

我们废话不多说,今天主要和大家分享下我昨天修复的关于 chatGPT 流数据处理的一个bug,这个 bug 其实是一个偶现的 bug,因此一开始我就没管,(又不是不能用对吧?),但是后面发现这个 bug 出现的频率还比较高,所以还是决定解决一下,下面是解决过程。

复现 bug

下面是这个 bug 的截图。首先做下知识铺垫,使用 SseEmitter 给浏览器发送消息时,浏览器拿到的数据是会带着 data: 你发的内容,比如你发送的内容是 'hello',那么浏览器收到的消息就是 'data:hello'.

因为为了实现打字机的效果,后端是一个字一个字返回的,前端也是一个字一个字处理,下面是具体的代码。

后端逐字发送代码如下:

java 复制代码
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;
    }
}

具体使用 SseEmitter 的发送代码如下,这个代码后面有用,会细讲,大家先混个眼熟。

java 复制代码
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;
}

前端逐字处理代码:

js 复制代码
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/${req.type}/legacy`, {  
            method: 'POST',  
            headers: {  
                'Content-Type': 'application/json',  
            },  
            body: JSON.stringify({  
                userId: `${Cookies.get('openbytecode-chat-id')}`,  
            }),  
        });  

        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 });  
                console.log('sssssssssssssss', text);
                const value = extractData(text);
                responseText += value;  
                const done = content.done;  
                options?.onMessage(responseText, false);  
                if (done) {  
                    break;  
                }  
            }   
        } else if (res.status === 401) {  
            console.error('Unauthorized');  
            options?.onError(new Error('Unauthorized'), res.status);  
        } 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);  
    }  
}

下面是抽取 data 的代码,对应后端的 String text = "{" + data + "}";,至于为什么后端要在返回数据中加入 {},现在你只需要记住这个 {} 有大用处,我们一会再说。

js 复制代码
/**  
* 抽取 data  
* @param content  
*/  
export const extractData = (content: string) => {  
    let index1 = content.indexOf('{');  
    let index2 = content.lastIndexOf('}');  
    return content.substring(index1 + 1, index2);  
};

所以接下来就是复现这个的 bug,通过日志来找出问题。下面是通过打印日志得到的截图。

分析 bug

根据我们输出的日志,可以明显发现,decoder.decode(content.value, { stream: true }) 这玩意得到的内容并不是我们想的那样每次解析只有一个字,而是有时会出现多个字一起返回,这不就是出现了粘包嘛,由于后端数据的粘包,导致前端在处理时只截取了开头和结尾的 {}, 所以就出现了这个 bug,好了,问题找到了,下面我们就考虑如何解决。

解决 bug

其实,这个是后端粘包的问题,但是,我要说但是了,我们前端也能处理(因为后端还不知道该如何处理,头大),前端处理其实也比较简单,因为我们知道后端有时会出现粘包,那我们就将解码之后将内容拆成数组,代码如下。

js 复制代码
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/${req.type}/legacy`, {  
            method: 'POST',  
            headers: {  
                'Content-Type': 'application/json',  
            },  
            body: JSON.stringify({  
                userId: `${Cookies.get('openbytecode-chat-id')}`,  
            }),  
        });  

        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 });  
                const values = text.split('data:').filter((v) => v);  
  
                for (let i = 0; i < values.length; i++) {  
                    const data = extractData(values[i]);  
                    responseText += data;  
                }

                const done = content.done;  
                options?.onMessage(responseText, false);  
                if (done) {  
                    break;  
                }  
            }   
        } else if (res.status === 401) {  
            console.error('Unauthorized');  
            options?.onError(new Error('Unauthorized'), res.status);  
        } 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-event-source,因为通过查看 fetch-event-source 的源码,他底层使用的也是 fetch,索性我们也想到了解决办法,所以就先不换了,还有人可能会问,为什么不用 EventSource,因为 EventSource 不支持 post 请求😂。

最后

如果喜欢我的文章,欢迎关注我,同时也欢迎关注我的个人网站,里面有多个我一直迭代的项目, 还有我的 b 站,我会定期在 b 站分享一些你意想不到的知识。

相关推荐
abigale0321 分钟前
webpack+vite前端构建工具 -11实战中的配置技巧
前端·webpack·node.js
专注API从业者41 分钟前
构建淘宝评论监控系统:API 接口开发与实时数据采集教程
大数据·前端·数据库·oracle
Joker`s smile1 小时前
Chrome安装老版本、不同版本,自制便携版本用于前端调试
前端·chrome
weixin_416639971 小时前
爬虫工程师Chrome开发者工具简单介绍
前端·chrome·爬虫
我是如子啊1 小时前
【解决“此扩展可能损坏”】Edge浏览器(chrome系列通杀))扩展损坏?一招保留数据快速修复
前端·chrome·edge
灵性花火1 小时前
Qt的前端和后端过于耦合(0/7)
开发语言·前端·qt
孤水寒月5 小时前
基于HTML的悬窗可拖动记事本
前端·css·html
祝余呀5 小时前
html初学者第一天
前端·html
耶啵奶膘7 小时前
uniapp+firstUI——上传视频组件fui-upload-video
前端·javascript·uni-app
视频砖家8 小时前
移动端Html5播放器按钮变小的问题解决方法
前端·javascript·viewport功能