主要问题:未处理不完整的消息块:
你的代码 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 } 选项可以确保字符不会被错误地解码。
网友回复


