Fetch流式请求

87 阅读2分钟
let controllerFetchList = [];
const getSummary = async (data) => {
  if (isStream.value) {
    let controller = new AbortController();
    controllerFetchList.push(controller);
    const { signal } = controller;
    const response = await apiFetch(`${interfaceUrl}/API-AIP/chat/analyze`, {
      signal,
      method: 'POST',
      headers: {
        'Content-Type': 'application/json;charset=utf-8',
      },
      body: JSON.stringify({
        stream: true,
        conversation_id: chatId.value,
        data: JSON.stringify(data),
      }),
    });
    // 3. 从响应体中获取 ReadableStream 的读取器
    const reader = response.body.getReader();
    // 4. 创建 TextDecoder 用于将 Uint8Array 解码成字符串
    const decoder = new TextDecoder('utf-8');
    // 1. 创建一个缓冲区来存储不完整的数据
    let buffer = '';
    // 5. 循环读取流中的数据
    // eslint-disable-next-line no-constant-condition
    while (true) {
      const { done, value } = await reader.read();
      if (done) {
        console.log('数据流接收完毕。');
        getLastChat.value.summaryLoading = false;
        isLoading.value = false;
        getLastChat.value.loading = false;
        //更新消息
        getLastChat.value.noSummary = true;
        await updateMsgApi(getLastChat.value.dataId, { content: JSON.stringify(getLastChat.value) });
        scrollBottom();
        break; // 退出循环
      }
      // 将接收到的 Uint8Array 数据块解码为字符串
      const chunk = decoder.decode(value, { stream: true });
      buffer += chunk;
      while (buffer.includes('\n')) {
        const separatorIndex = buffer.indexOf('\n');
        // 提取一条完整的消息
        const message = buffer.slice(0, separatorIndex);
        // 从缓冲区中移除已处理的消息
        buffer = buffer.slice(separatorIndex + 1);
        const lines = message.split('\n');
        for (const line of lines) {
          if (line.startsWith('data:')) {
            // 提取 'data:' 后面的内容,并去除前导空格
            const jsonString = line.substring(5).trim();
            if (jsonString) {
              try {
                //  解析 JSON 字符串
                const dataObject = JSON.parse(jsonString);
                if (dataObject.type === 'content') {
                  getLastChat.value.summary += dataObject.content;
                }
                //  成功取值!
                console.log('成功解析对象:', dataObject);
              } catch (error) {
                console.error('解析JSON失败:', error, '原始JSON字符串:', jsonString);
              }
            }
          }
        }
      }
    }
  }
};

缓冲区与消息边界处理 (最核心的逻辑)

JavaScript

let buffer = '';
// ...
buffer += chunk;
while (buffer.includes('\n\n')) {
    const separatorIndex = buffer.indexOf('\n\n');
    const message = buffer.slice(0, separatorIndex);
    buffer = buffer.slice(separatorIndex + 2);
    // ...
}
  • buffer 的作用: 这是一个临时存储区,用来解决网络数据传输的不确定性。AI的流式输出可能很长,会被切成很多小的数据块(chunk)发送。buffer 的任务就是将这些小块拼接起来,直到形成一个或多个完整的消息。
  • while (buffer.includes('\n\n')) : 这是整个流式处理的“规则引擎”。它基于一个约定:服务器每发完一条完整的消息(data: {...}),就会在后面跟上两个换行符 \n\n 作为分隔符。这个 while 循环不断检查缓冲区里是否出现了这个分隔符。
  • 提取与消耗 (slice) : 一旦找到分隔符,代码就精确地提取出一条完整的消息 (message),然后立刻将这条消息连同分隔符从 buffer 的开头移除 (buffer.slice(separatorIndex + 2))。这一步至关重要,它确保了程序不会重复处理同一条消息,并让 buffer 为暂存下一条不完整的消息做好了准备。