@lobehub/chat 1.66.3 → 1.66.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md
CHANGED
@@ -2,6 +2,31 @@
|
|
2
2
|
|
3
3
|
# Changelog
|
4
4
|
|
5
|
+
### [Version 1.66.4](https://github.com/lobehub/lobe-chat/compare/v1.66.3...v1.66.4)
|
6
|
+
|
7
|
+
<sup>Released on **2025-02-28**</sup>
|
8
|
+
|
9
|
+
#### 💄 Styles
|
10
|
+
|
11
|
+
- **misc**: Optimize smooth output.
|
12
|
+
|
13
|
+
<br/>
|
14
|
+
|
15
|
+
<details>
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
17
|
+
|
18
|
+
#### Styles
|
19
|
+
|
20
|
+
- **misc**: Optimize smooth output, closes [#5824](https://github.com/lobehub/lobe-chat/issues/5824) ([7a84ad9](https://github.com/lobehub/lobe-chat/commit/7a84ad9))
|
21
|
+
|
22
|
+
</details>
|
23
|
+
|
24
|
+
<div align="right">
|
25
|
+
|
26
|
+
[](#readme-top)
|
27
|
+
|
28
|
+
</div>
|
29
|
+
|
5
30
|
### [Version 1.66.3](https://github.com/lobehub/lobe-chat/compare/v1.66.2...v1.66.3)
|
6
31
|
|
7
32
|
<sup>Released on **2025-02-27**</sup>
|
package/changelog/v1.json
CHANGED
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@lobehub/chat",
|
3
|
-
"version": "1.66.
|
3
|
+
"version": "1.66.4",
|
4
4
|
"description": "Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
|
5
5
|
"keywords": [
|
6
6
|
"framework",
|
@@ -193,14 +193,10 @@ const Google: ModelProviderCard = {
|
|
193
193
|
sdkType: 'google',
|
194
194
|
showModelFetcher: true,
|
195
195
|
smoothing: {
|
196
|
-
speed:
|
196
|
+
speed: 50,
|
197
197
|
text: true,
|
198
198
|
},
|
199
199
|
},
|
200
|
-
smoothing: {
|
201
|
-
speed: 2,
|
202
|
-
text: true,
|
203
|
-
},
|
204
200
|
url: 'https://ai.google.dev',
|
205
201
|
};
|
206
202
|
|
@@ -142,9 +142,24 @@ describe('fetchSSE', () => {
|
|
142
142
|
smoothing: true,
|
143
143
|
});
|
144
144
|
|
145
|
-
|
146
|
-
|
147
|
-
|
145
|
+
const expectedMessages = [
|
146
|
+
{ text: 'H', type: 'text' },
|
147
|
+
{ text: 'e', type: 'text' },
|
148
|
+
{ text: 'l', type: 'text' },
|
149
|
+
{ text: 'l', type: 'text' },
|
150
|
+
{ text: 'o', type: 'text' },
|
151
|
+
{ text: ' ', type: 'text' },
|
152
|
+
{ text: 'W', type: 'text' },
|
153
|
+
{ text: 'o', type: 'text' },
|
154
|
+
{ text: 'r', type: 'text' },
|
155
|
+
{ text: 'l', type: 'text' },
|
156
|
+
{ text: 'd', type: 'text' },
|
157
|
+
];
|
158
|
+
|
159
|
+
expectedMessages.forEach((message, index) => {
|
160
|
+
expect(mockOnMessageHandle).toHaveBeenNthCalledWith(index + 1, message);
|
161
|
+
});
|
162
|
+
|
148
163
|
// more assertions for each character...
|
149
164
|
expect(mockOnFinish).toHaveBeenCalledWith('Hello World', {
|
150
165
|
observationId: null,
|
@@ -186,77 +201,6 @@ describe('fetchSSE', () => {
|
|
186
201
|
type: 'done',
|
187
202
|
});
|
188
203
|
});
|
189
|
-
|
190
|
-
it('should handle reasoning event with smoothing correctly', async () => {
|
191
|
-
const mockOnMessageHandle = vi.fn();
|
192
|
-
const mockOnFinish = vi.fn();
|
193
|
-
|
194
|
-
(fetchEventSource as any).mockImplementationOnce(
|
195
|
-
async (url: string, options: FetchEventSourceInit) => {
|
196
|
-
options.onopen!({ clone: () => ({ ok: true, headers: new Headers() }) } as any);
|
197
|
-
options.onmessage!({ event: 'reasoning', data: JSON.stringify('Hello') } as any);
|
198
|
-
await sleep(100);
|
199
|
-
options.onmessage!({ event: 'reasoning', data: JSON.stringify(' World') } as any);
|
200
|
-
await sleep(100);
|
201
|
-
options.onmessage!({ event: 'text', data: JSON.stringify('hi') } as any);
|
202
|
-
},
|
203
|
-
);
|
204
|
-
|
205
|
-
await fetchSSE('/', {
|
206
|
-
onMessageHandle: mockOnMessageHandle,
|
207
|
-
onFinish: mockOnFinish,
|
208
|
-
smoothing: true,
|
209
|
-
});
|
210
|
-
|
211
|
-
expect(mockOnMessageHandle).toHaveBeenNthCalledWith(1, { text: 'Hell', type: 'reasoning' });
|
212
|
-
expect(mockOnMessageHandle).toHaveBeenNthCalledWith(2, { text: 'o', type: 'reasoning' });
|
213
|
-
expect(mockOnMessageHandle).toHaveBeenNthCalledWith(3, { text: ' Wor', type: 'reasoning' });
|
214
|
-
// more assertions for each character...
|
215
|
-
expect(mockOnFinish).toHaveBeenCalledWith('hi', {
|
216
|
-
observationId: null,
|
217
|
-
toolCalls: undefined,
|
218
|
-
reasoning: { content: 'Hello World' },
|
219
|
-
traceId: null,
|
220
|
-
type: 'done',
|
221
|
-
});
|
222
|
-
});
|
223
|
-
it('should handle reasoning with signature', async () => {
|
224
|
-
const mockOnMessageHandle = vi.fn();
|
225
|
-
const mockOnFinish = vi.fn();
|
226
|
-
|
227
|
-
(fetchEventSource as any).mockImplementationOnce(
|
228
|
-
async (url: string, options: FetchEventSourceInit) => {
|
229
|
-
options.onopen!({ clone: () => ({ ok: true, headers: new Headers() }) } as any);
|
230
|
-
options.onmessage!({ event: 'reasoning', data: JSON.stringify('Hello') } as any);
|
231
|
-
await sleep(100);
|
232
|
-
options.onmessage!({ event: 'reasoning', data: JSON.stringify(' World') } as any);
|
233
|
-
options.onmessage!({
|
234
|
-
event: 'reasoning_signature',
|
235
|
-
data: JSON.stringify('abcbcd'),
|
236
|
-
} as any);
|
237
|
-
await sleep(100);
|
238
|
-
options.onmessage!({ event: 'text', data: JSON.stringify('hi') } as any);
|
239
|
-
},
|
240
|
-
);
|
241
|
-
|
242
|
-
await fetchSSE('/', {
|
243
|
-
onMessageHandle: mockOnMessageHandle,
|
244
|
-
onFinish: mockOnFinish,
|
245
|
-
smoothing: true,
|
246
|
-
});
|
247
|
-
|
248
|
-
expect(mockOnMessageHandle).toHaveBeenNthCalledWith(1, { text: 'Hell', type: 'reasoning' });
|
249
|
-
expect(mockOnMessageHandle).toHaveBeenNthCalledWith(2, { text: 'o', type: 'reasoning' });
|
250
|
-
expect(mockOnMessageHandle).toHaveBeenNthCalledWith(3, { text: ' Wor', type: 'reasoning' });
|
251
|
-
// more assertions for each character...
|
252
|
-
expect(mockOnFinish).toHaveBeenCalledWith('hi', {
|
253
|
-
observationId: null,
|
254
|
-
toolCalls: undefined,
|
255
|
-
reasoning: { content: 'Hello World', signature: 'abcbcd' },
|
256
|
-
traceId: null,
|
257
|
-
type: 'done',
|
258
|
-
});
|
259
|
-
});
|
260
204
|
});
|
261
205
|
|
262
206
|
it('should handle grounding event', async () => {
|
@@ -281,7 +225,7 @@ describe('fetchSSE', () => {
|
|
281
225
|
grounding: 'Hello',
|
282
226
|
type: 'grounding',
|
283
227
|
});
|
284
|
-
|
228
|
+
|
285
229
|
expect(mockOnFinish).toHaveBeenCalledWith('hi', {
|
286
230
|
observationId: null,
|
287
231
|
toolCalls: undefined,
|
@@ -376,9 +320,23 @@ describe('fetchSSE', () => {
|
|
376
320
|
smoothing: true,
|
377
321
|
});
|
378
322
|
|
379
|
-
|
380
|
-
|
381
|
-
|
323
|
+
const expectedMessages = [
|
324
|
+
{ text: 'H', type: 'text' },
|
325
|
+
{ text: 'e', type: 'text' },
|
326
|
+
{ text: 'l', type: 'text' },
|
327
|
+
{ text: 'l', type: 'text' },
|
328
|
+
{ text: 'o', type: 'text' },
|
329
|
+
{ text: ' ', type: 'text' },
|
330
|
+
{ text: 'W', type: 'text' },
|
331
|
+
{ text: 'o', type: 'text' },
|
332
|
+
{ text: 'r', type: 'text' },
|
333
|
+
{ text: 'l', type: 'text' },
|
334
|
+
{ text: 'd', type: 'text' },
|
335
|
+
];
|
336
|
+
|
337
|
+
expectedMessages.forEach((message, index) => {
|
338
|
+
expect(mockOnMessageHandle).toHaveBeenNthCalledWith(index + 1, message);
|
339
|
+
});
|
382
340
|
|
383
341
|
expect(mockOnFinish).toHaveBeenCalledWith('Hello World', {
|
384
342
|
type: 'done',
|
@@ -64,9 +64,9 @@ export interface FetchSSEOptions {
|
|
64
64
|
smoothing?: SmoothingParams | boolean;
|
65
65
|
}
|
66
66
|
|
67
|
-
const START_ANIMATION_SPEED =
|
67
|
+
const START_ANIMATION_SPEED = 10; // 默认起始速度
|
68
68
|
|
69
|
-
const END_ANIMATION_SPEED =
|
69
|
+
const END_ANIMATION_SPEED = 16;
|
70
70
|
|
71
71
|
const createSmoothMessage = (params: {
|
72
72
|
onTextUpdate: (delta: string, text: string) => void;
|
@@ -75,12 +75,14 @@ const createSmoothMessage = (params: {
|
|
75
75
|
const { startSpeed = START_ANIMATION_SPEED } = params;
|
76
76
|
|
77
77
|
let buffer = '';
|
78
|
-
// why use queue: https://shareg.pt/GLBrjpK
|
79
78
|
let outputQueue: string[] = [];
|
80
79
|
let isAnimationActive = false;
|
81
80
|
let animationFrameId: number | null = null;
|
81
|
+
let lastFrameTime = 0;
|
82
|
+
let accumulatedTime = 0;
|
83
|
+
let currentSpeed = startSpeed;
|
84
|
+
let lastQueueLength = 0; // 记录上一帧的队列长度
|
82
85
|
|
83
|
-
// when you need to stop the animation, call this function
|
84
86
|
const stopAnimation = () => {
|
85
87
|
isAnimationActive = false;
|
86
88
|
if (animationFrameId !== null) {
|
@@ -89,48 +91,72 @@ const createSmoothMessage = (params: {
|
|
89
91
|
}
|
90
92
|
};
|
91
93
|
|
92
|
-
|
93
|
-
|
94
|
-
const startAnimation = (speed = startSpeed) =>
|
95
|
-
new Promise<void>((resolve) => {
|
94
|
+
const startAnimation = (speed = startSpeed) => {
|
95
|
+
return new Promise<void>((resolve) => {
|
96
96
|
if (isAnimationActive) {
|
97
97
|
resolve();
|
98
98
|
return;
|
99
99
|
}
|
100
100
|
|
101
101
|
isAnimationActive = true;
|
102
|
+
lastFrameTime = performance.now();
|
103
|
+
accumulatedTime = 0;
|
104
|
+
currentSpeed = speed;
|
105
|
+
lastQueueLength = 0; // 重置上一帧队列长度
|
102
106
|
|
103
|
-
const updateText = () => {
|
104
|
-
// 如果动画已经不再激活,则停止更新文本
|
107
|
+
const updateText = (timestamp: number) => {
|
105
108
|
if (!isAnimationActive) {
|
106
|
-
|
107
|
-
|
109
|
+
if (animationFrameId !== null) {
|
110
|
+
cancelAnimationFrame(animationFrameId);
|
111
|
+
}
|
108
112
|
resolve();
|
109
113
|
return;
|
110
114
|
}
|
111
115
|
|
112
|
-
|
113
|
-
|
116
|
+
const frameDuration = timestamp - lastFrameTime;
|
117
|
+
lastFrameTime = timestamp;
|
118
|
+
accumulatedTime += frameDuration;
|
119
|
+
|
120
|
+
let charsToProcess = 0;
|
114
121
|
if (outputQueue.length > 0) {
|
115
|
-
//
|
116
|
-
const
|
117
|
-
|
122
|
+
// 更平滑的速度调整
|
123
|
+
const targetSpeed = Math.max(speed, outputQueue.length);
|
124
|
+
// 根据队列长度变化调整速度变化率
|
125
|
+
const speedChangeRate = Math.abs(outputQueue.length - lastQueueLength) * 0.0008 + 0.005;
|
126
|
+
currentSpeed += (targetSpeed - currentSpeed) * speedChangeRate;
|
127
|
+
|
128
|
+
charsToProcess = Math.floor((accumulatedTime * currentSpeed) / 1000);
|
129
|
+
}
|
130
|
+
|
131
|
+
if (charsToProcess > 0) {
|
132
|
+
accumulatedTime -= (charsToProcess * 1000) / currentSpeed;
|
133
|
+
|
134
|
+
let actualChars = Math.min(charsToProcess, outputQueue.length);
|
135
|
+
// actualChars = Math.min(speed, actualChars); // 速度上限
|
118
136
|
|
119
|
-
//
|
137
|
+
// if (actualChars * 2 < outputQueue.length && /[\dA-Za-z]/.test(outputQueue[actualChars])) {
|
138
|
+
// actualChars *= 2;
|
139
|
+
// }
|
140
|
+
|
141
|
+
const charsToAdd = outputQueue.splice(0, actualChars).join('');
|
142
|
+
buffer += charsToAdd;
|
120
143
|
params.onTextUpdate(charsToAdd, buffer);
|
144
|
+
}
|
145
|
+
|
146
|
+
lastQueueLength = outputQueue.length; // 更新上一帧的队列长度
|
147
|
+
|
148
|
+
if (outputQueue.length > 0 && isAnimationActive) {
|
149
|
+
animationFrameId = requestAnimationFrame(updateText);
|
121
150
|
} else {
|
122
|
-
// 当所有字符都显示完毕时,清除动画状态
|
123
151
|
isAnimationActive = false;
|
124
152
|
animationFrameId = null;
|
125
153
|
resolve();
|
126
|
-
return;
|
127
154
|
}
|
128
|
-
|
129
|
-
animationFrameId = requestAnimationFrame(updateText);
|
130
155
|
};
|
131
156
|
|
132
157
|
animationFrameId = requestAnimationFrame(updateText);
|
133
158
|
});
|
159
|
+
};
|
134
160
|
|
135
161
|
const pushToQueue = (text: string) => {
|
136
162
|
outputQueue.push(...text.split(''));
|
@@ -435,7 +461,7 @@ export const fetchSSE = async (url: string, options: RequestInit & FetchSSEOptio
|
|
435
461
|
const observationId = response.headers.get(LOBE_CHAT_OBSERVATION_ID);
|
436
462
|
|
437
463
|
if (textController.isTokenRemain()) {
|
438
|
-
await textController.startAnimation(
|
464
|
+
await textController.startAnimation(smoothingSpeed);
|
439
465
|
}
|
440
466
|
|
441
467
|
if (toolCallsController.isTokenRemain()) {
|