Browse Source

修复 SSE 流式回答每词独占一行的 bug

master
wanghanlin 1 week ago
parent
commit
a2950be98f
  1. 69
      client/dist/chatbot-sdk.js
  2. 2
      client/dist/chatbot-sdk.js.map
  3. 2
      client/dist/chatbot-sdk.min.js
  4. 2
      client/dist/chatbot-sdk.min.js.map
  5. 65
      client/src/api.ts
  6. 5
      client/src/chat.ts
  7. 69
      src/main/resources/static/sdk/chatbot-sdk.js
  8. 2
      src/main/resources/static/sdk/chatbot-sdk.js.map
  9. 2
      src/main/resources/static/sdk/chatbot-sdk.min.js
  10. 2
      src/main/resources/static/sdk/chatbot-sdk.min.js.map

69
client/dist/chatbot-sdk.js

@ -579,6 +579,7 @@ var ChatbotSDK = (function () {
} }
const decoder = new TextDecoder('utf-8', { stream: true }); const decoder = new TextDecoder('utf-8', { stream: true });
let buffer = ''; let buffer = '';
let eventLines = [];
try { try {
while (true) { while (true) {
const { done, value } = await reader.read(); const { done, value } = await reader.read();
@ -589,38 +590,67 @@ var ChatbotSDK = (function () {
buffer = lines.pop() || ''; buffer = lines.pop() || '';
for (const line of lines) { for (const line of lines) {
const trimmed = line.trim(); const trimmed = line.trim();
if (!trimmed || trimmed.startsWith(':'))
// 空行 = SSE 事件边界,刷新当前事件
if (!trimmed) {
if (eventLines.length > 0) {
const chunk = eventLines.join('\n');
totalText += chunk;
onChunk(chunk);
eventLines = [];
}
continue; continue;
if (trimmed.startsWith('data:')) {
const data = trimmed.substring(5).trim();
if (data) {
totalText += data;
onChunk(data);
} }
// 忽略注释行 (: ...)
if (trimmed.startsWith(':'))
continue;
// [DONE] 信号
if (trimmed === '[DONE]') {
if (eventLines.length > 0) {
const chunk = eventLines.join('\n');
totalText += chunk;
onChunk(chunk);
eventLines = [];
} }
else if (trimmed === '[DONE]') {
break; break;
} }
else if (!trimmed.startsWith('event:') && !trimmed.startsWith('id:') && !trimmed.startsWith('retry:')) {
totalText += trimmed;
onChunk(trimmed);
// 跳过 SSE 协议字段
if (trimmed.startsWith('event:') || trimmed.startsWith('id:') || trimmed.startsWith('retry:'))
continue;
// 提取 content(data: 前缀行去掉前缀,裸行为 Flux 元素内换行的续行)
let content;
if (trimmed.startsWith('data:')) {
content = trimmed.substring('data:'.length);
// 去掉 data: 后面紧跟的一个空格(SSE 标准允许)
if (content.startsWith(' '))
content = content.substring(1);
} }
else {
content = trimmed;
}
eventLines.push(content);
} }
} }
if (buffer.trim()) { if (buffer.trim()) {
const trimmed = buffer.trim(); const trimmed = buffer.trim();
if (!trimmed.startsWith(':')) {
if (trimmed.startsWith('data:')) { if (trimmed.startsWith('data:')) {
const data = trimmed.substring(5).trim();
if (data) {
totalText += data;
onChunk(data);
let content = trimmed.substring('data:'.length);
if (content.startsWith(' '))
content = content.substring(1);
eventLines.push(content);
} }
else if (trimmed !== '[DONE]' && !trimmed.startsWith('event:') && !trimmed.startsWith('id:') && !trimmed.startsWith('retry:')) {
eventLines.push(trimmed);
} }
else if (trimmed !== '[DONE]') {
totalText += trimmed;
onChunk(trimmed);
} }
} }
// 处理缓冲区剩余
if (eventLines.length > 0) {
const chunk = eventLines.join('\n');
totalText += chunk;
onChunk(chunk);
eventLines = [];
}
} }
catch (readErr) { catch (readErr) {
// 用户主动中断:视为正常结束,保留已生成内容 // 用户主动中断:视为正常结束,保留已生成内容
@ -4095,6 +4125,9 @@ var ChatbotSDK = (function () {
let accumulated = ''; let accumulated = '';
let streamStarted = false; let streamStarted = false;
chatSSERequest(text, (chunk) => { chatSSERequest(text, (chunk) => {
// 直接拼接:后端每个 SSE 事件是模型一个原始 token,
// token 内的换行已由 api.ts 的 eventLines.join('\n') 还原,
// chunk 之间不能再加 \n,否则会把单词/短句拆成多行、破坏 Markdown 结构
accumulated += chunk; accumulated += chunk;
if (!streamStarted && messagesContainer$1) { if (!streamStarted && messagesContainer$1) {
if (hideLoadingFn$1) if (hideLoadingFn$1)
@ -4105,7 +4138,7 @@ var ChatbotSDK = (function () {
streamStarted = true; streamStarted = true;
} }
if (bubbleEl) { if (bubbleEl) {
bubbleEl.textContent = accumulated;
bubbleEl.innerHTML = renderMarkdown(accumulated);
appendStreamingCaret(bubbleEl); appendStreamingCaret(bubbleEl);
} }
if (messagesContainer$1) if (messagesContainer$1)

2
client/dist/chatbot-sdk.js.map
File diff suppressed because it is too large
View File

2
client/dist/chatbot-sdk.min.js
File diff suppressed because it is too large
View File

2
client/dist/chatbot-sdk.min.js.map
File diff suppressed because it is too large
View File

65
client/src/api.ts

@ -293,6 +293,7 @@ export async function chatSSERequest(
const decoder = new TextDecoder('utf-8', { stream: true } as TextDecoderOptions); const decoder = new TextDecoder('utf-8', { stream: true } as TextDecoderOptions);
let buffer = ''; let buffer = '';
let eventLines: string[] = [];
try { try {
while (true) { while (true) {
@ -305,29 +306,67 @@ export async function chatSSERequest(
for (const line of lines) { for (const line of lines) {
const trimmed = line.trim(); const trimmed = line.trim();
if (!trimmed || trimmed.startsWith(':')) continue;
if (trimmed.startsWith('data:')) {
const data = trimmed.substring(5).trim();
if (data) { totalText += data; onChunk(data); }
} else if (trimmed === '[DONE]') {
// 空行 = SSE 事件边界,刷新当前事件
if (!trimmed) {
if (eventLines.length > 0) {
const chunk = eventLines.join('\n');
totalText += chunk;
onChunk(chunk);
eventLines = [];
}
continue;
}
// 忽略注释行 (: ...)
if (trimmed.startsWith(':')) continue;
// [DONE] 信号
if (trimmed === '[DONE]') {
if (eventLines.length > 0) {
const chunk = eventLines.join('\n');
totalText += chunk;
onChunk(chunk);
eventLines = [];
}
break; break;
} else if (!trimmed.startsWith('event:') && !trimmed.startsWith('id:') && !trimmed.startsWith('retry:')) {
totalText += trimmed;
onChunk(trimmed);
} }
// 跳过 SSE 协议字段
if (trimmed.startsWith('event:') || trimmed.startsWith('id:') || trimmed.startsWith('retry:')) continue;
// 提取 content(data: 前缀行去掉前缀,裸行为 Flux 元素内换行的续行)
let content: string;
if (trimmed.startsWith('data:')) {
content = trimmed.substring('data:'.length);
// 去掉 data: 后面紧跟的一个空格(SSE 标准允许)
if (content.startsWith(' ')) content = content.substring(1);
} else {
content = trimmed;
}
eventLines.push(content);
} }
} }
if (buffer.trim()) { if (buffer.trim()) {
const trimmed = buffer.trim(); const trimmed = buffer.trim();
if (!trimmed.startsWith(':')) {
if (trimmed.startsWith('data:')) { if (trimmed.startsWith('data:')) {
const data = trimmed.substring(5).trim();
if (data) { totalText += data; onChunk(data); }
} else if (trimmed !== '[DONE]') {
totalText += trimmed;
onChunk(trimmed);
let content = trimmed.substring('data:'.length);
if (content.startsWith(' ')) content = content.substring(1);
eventLines.push(content);
} else if (trimmed !== '[DONE]' && !trimmed.startsWith('event:') && !trimmed.startsWith('id:') && !trimmed.startsWith('retry:')) {
eventLines.push(trimmed);
}
}
} }
// 处理缓冲区剩余
if (eventLines.length > 0) {
const chunk = eventLines.join('\n');
totalText += chunk;
onChunk(chunk);
eventLines = [];
} }
} catch (readErr: unknown) { } catch (readErr: unknown) {
// 用户主动中断:视为正常结束,保留已生成内容 // 用户主动中断:视为正常结束,保留已生成内容

5
client/src/chat.ts

@ -521,6 +521,9 @@ async function sendStreamMessage(text: string, aiTimestamp: number, shouldUseRag
chatSSERequest( chatSSERequest(
text, text,
(chunk: string) => { (chunk: string) => {
// 直接拼接:后端每个 SSE 事件是模型一个原始 token,
// token 内的换行已由 api.ts 的 eventLines.join('\n') 还原,
// chunk 之间不能再加 \n,否则会把单词/短句拆成多行、破坏 Markdown 结构
accumulated += chunk; accumulated += chunk;
if (!streamStarted && messagesContainer) { if (!streamStarted && messagesContainer) {
if (hideLoadingFn) hideLoadingFn(); if (hideLoadingFn) hideLoadingFn();
@ -530,7 +533,7 @@ async function sendStreamMessage(text: string, aiTimestamp: number, shouldUseRag
streamStarted = true; streamStarted = true;
} }
if (bubbleEl) { if (bubbleEl) {
bubbleEl.textContent = accumulated;
bubbleEl.innerHTML = renderMarkdown(accumulated);
appendStreamingCaret(bubbleEl); appendStreamingCaret(bubbleEl);
} }
if (messagesContainer) smartScrollToBottom(); if (messagesContainer) smartScrollToBottom();

69
src/main/resources/static/sdk/chatbot-sdk.js

@ -579,6 +579,7 @@ var ChatbotSDK = (function () {
} }
const decoder = new TextDecoder('utf-8', { stream: true }); const decoder = new TextDecoder('utf-8', { stream: true });
let buffer = ''; let buffer = '';
let eventLines = [];
try { try {
while (true) { while (true) {
const { done, value } = await reader.read(); const { done, value } = await reader.read();
@ -589,38 +590,67 @@ var ChatbotSDK = (function () {
buffer = lines.pop() || ''; buffer = lines.pop() || '';
for (const line of lines) { for (const line of lines) {
const trimmed = line.trim(); const trimmed = line.trim();
if (!trimmed || trimmed.startsWith(':'))
// 空行 = SSE 事件边界,刷新当前事件
if (!trimmed) {
if (eventLines.length > 0) {
const chunk = eventLines.join('\n');
totalText += chunk;
onChunk(chunk);
eventLines = [];
}
continue; continue;
if (trimmed.startsWith('data:')) {
const data = trimmed.substring(5).trim();
if (data) {
totalText += data;
onChunk(data);
} }
// 忽略注释行 (: ...)
if (trimmed.startsWith(':'))
continue;
// [DONE] 信号
if (trimmed === '[DONE]') {
if (eventLines.length > 0) {
const chunk = eventLines.join('\n');
totalText += chunk;
onChunk(chunk);
eventLines = [];
} }
else if (trimmed === '[DONE]') {
break; break;
} }
else if (!trimmed.startsWith('event:') && !trimmed.startsWith('id:') && !trimmed.startsWith('retry:')) {
totalText += trimmed;
onChunk(trimmed);
// 跳过 SSE 协议字段
if (trimmed.startsWith('event:') || trimmed.startsWith('id:') || trimmed.startsWith('retry:'))
continue;
// 提取 content(data: 前缀行去掉前缀,裸行为 Flux 元素内换行的续行)
let content;
if (trimmed.startsWith('data:')) {
content = trimmed.substring('data:'.length);
// 去掉 data: 后面紧跟的一个空格(SSE 标准允许)
if (content.startsWith(' '))
content = content.substring(1);
} }
else {
content = trimmed;
}
eventLines.push(content);
} }
} }
if (buffer.trim()) { if (buffer.trim()) {
const trimmed = buffer.trim(); const trimmed = buffer.trim();
if (!trimmed.startsWith(':')) {
if (trimmed.startsWith('data:')) { if (trimmed.startsWith('data:')) {
const data = trimmed.substring(5).trim();
if (data) {
totalText += data;
onChunk(data);
let content = trimmed.substring('data:'.length);
if (content.startsWith(' '))
content = content.substring(1);
eventLines.push(content);
} }
else if (trimmed !== '[DONE]' && !trimmed.startsWith('event:') && !trimmed.startsWith('id:') && !trimmed.startsWith('retry:')) {
eventLines.push(trimmed);
} }
else if (trimmed !== '[DONE]') {
totalText += trimmed;
onChunk(trimmed);
} }
} }
// 处理缓冲区剩余
if (eventLines.length > 0) {
const chunk = eventLines.join('\n');
totalText += chunk;
onChunk(chunk);
eventLines = [];
}
} }
catch (readErr) { catch (readErr) {
// 用户主动中断:视为正常结束,保留已生成内容 // 用户主动中断:视为正常结束,保留已生成内容
@ -4095,6 +4125,9 @@ var ChatbotSDK = (function () {
let accumulated = ''; let accumulated = '';
let streamStarted = false; let streamStarted = false;
chatSSERequest(text, (chunk) => { chatSSERequest(text, (chunk) => {
// 直接拼接:后端每个 SSE 事件是模型一个原始 token,
// token 内的换行已由 api.ts 的 eventLines.join('\n') 还原,
// chunk 之间不能再加 \n,否则会把单词/短句拆成多行、破坏 Markdown 结构
accumulated += chunk; accumulated += chunk;
if (!streamStarted && messagesContainer$1) { if (!streamStarted && messagesContainer$1) {
if (hideLoadingFn$1) if (hideLoadingFn$1)
@ -4105,7 +4138,7 @@ var ChatbotSDK = (function () {
streamStarted = true; streamStarted = true;
} }
if (bubbleEl) { if (bubbleEl) {
bubbleEl.textContent = accumulated;
bubbleEl.innerHTML = renderMarkdown(accumulated);
appendStreamingCaret(bubbleEl); appendStreamingCaret(bubbleEl);
} }
if (messagesContainer$1) if (messagesContainer$1)

2
src/main/resources/static/sdk/chatbot-sdk.js.map
File diff suppressed because it is too large
View File

2
src/main/resources/static/sdk/chatbot-sdk.min.js
File diff suppressed because it is too large
View File

2
src/main/resources/static/sdk/chatbot-sdk.min.js.map
File diff suppressed because it is too large
View File

Loading…
Cancel
Save