主要问题:未处理不完整的消息块:
你的代码 const chunk = decoder.decode(value); const lines = chunk.split('\n'); 是在拿到一个数据块后立即按换行符分割。
如果一个数据块是 data: {"choices":[{"delta":{"content":"我是一个 (注意,JSON 在这里被截断了),你的代码会尝试解析 {"choices":[{"delta":{"content":"我是一个,这必然导致 JSON.parse 失败,然后这个 "我是一个" 的内容就永久丢失了。
次要问题:对 [DONE] 的处理逻辑有误:
在你的 for 循环中,当遇到 data: [DONE] 时,你使用了 break;
修复后
try { // 3. 发送请求到后端 const response = await fetch(resdata.url, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(resdata.postdata), }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } // 4. 处理流式响应 const reader = response.body.getReader(); const decoder = new TextDecoder("utf-8"); let assistantResponse = ''; assistantMessageDiv.innerHTML = ''; // 清除“思考中”动画 // --- 新增代码:缓冲区 --- let buffer = ''; while (true) { const { value, done } = await reader.read(); // 当流结束时,done 为 true if (done) { // 在退出前,检查缓冲区是否还有剩余的、未处理的数据 if (buffer.length > 0) { // 这种情况不常见,但为了健壮性最好加上 console.warn('Stream ended with unprocessed data in buffer:', buffer); } break; // 正常退出循环 } // 将新收到的数据块解码并追加到缓冲区 // 使用 { stream: true } 可以更好地处理多字节字符(如中文)被截断的情况 const chunk = decoder.decode(value, { stream: true }); buffer += chunk; // --- 修改代码:处理缓冲区中的完整行 --- let boundary; // 只要缓冲区中还包含换行符,就持续处理 while ((boundary = buffer.indexOf('\n')) !== -1) { // 提取出一行完整的数据(到第一个换行符为止) const line = buffer.substring(0, boundary); // 从缓冲区中移除已经处理过的行 buffer = buffer.substring(boundary + 1); if (line.startsWith('data: ')) { const jsonStr = line.substring(6).trim(); // 正确处理 [DONE] 信号 if (jsonStr === '[DONE]') { // 当收到 [DONE] 时,我们认为流已经从服务端结束了。 // 我们可以选择在这里直接 return 或 break 整个 while 循环。 // 为了确保所有资源被正确关闭,我们在这里跳出循环,让后面的 finally 逻辑执行。 // 调用 reader.cancel() 是一个好习惯,可以提前释放资源 reader.cancel(); // 使用一个 labeled break 或者 return 来跳出外层循环 // 但这里因为 [DONE] 之后 done 很快会变 true,所以简单 break 也可以 // 为了清晰,我们直接 return // 将最终完整的AI回复添加到历史记录 chatHistory.push({ role: 'assistant', content: assistantResponse }); // 重新启用按钮 sendButton.disabled = false; messageInput.focus(); return; // 直接结束函数执行 } // 如果是空数据,则跳过 if (jsonStr === "") continue; try { const parsed = JSON.parse(jsonStr); const content = parsed.choices[0]?.delta?.content || ''; if (content) { assistantResponse += content; // 使用 innerText 很好,可以防止 XSS assistantMessageDiv.innerText = assistantResponse; scrollToBottom(); } } catch (e) { console.error('Error parsing stream JSON:', e, 'Raw JSON string:', jsonStr); } } } } // 将最终完整的AI回复添加到历史记录(如果流是自然结束而不是通过 [DONE]) chatHistory.push({ role: 'assistant', content: assistantResponse }); } catch (error) { console.error('Fetch error:', error); if (error.name !== 'AbortError') { // AbortError 是 reader.cancel() 引起的,是正常行为 assistantMessageDiv.innerText = '抱歉,我好像出错了。请稍后再试。'; } } finally { // 重新启用按钮 sendButton.disabled = false; messageInput.focus(); }代码变更解释
引入缓冲区 buffer: 在 while 循环外定义一个空字符串 let buffer = '';。
数据块追加: 每次 reader.read() 拿到 chunk 后,不再直接处理,而是先追加到 buffer 中 (buffer += chunk;)。
循环处理完整行: 使用 while ((boundary = buffer.indexOf('\n')) !== -1) 来检查缓冲区中是否存在完整的行(以 \n 结尾)。
line = buffer.substring(0, boundary): 提取出这一行。
buffer = buffer.substring(boundary + 1): 将已处理的行从缓冲区移除,留下可能不完整的部分,等待下一个数据块拼接。
正确处理 [DONE]: 当解析到 [DONE] 时,我们直接 return 或者使用 break 跳出整个 while 循环,并调用 reader.cancel() 来提前关闭读取器,这是一种更干净的退出方式。
健壮的解码: decoder.decode(value, { stream: true }) 是一个推荐的实践。当一个多字节字符(比如一个汉字)的字节码恰好在两个 chunk 之间被分割时,{ stream: true } 选项可以确保字符不会被错误地解码。
网友回复