+
7
-

回答

主要问题:未处理不完整的消息块:

你的代码 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 } 选项可以确保字符不会被错误地解码。

网友回复

我知道答案,我要回答