@lobehub/chat 1.66.2 → 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,56 @@
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
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
27
+
28
+ </div>
29
+
30
+ ### [Version 1.66.3](https://github.com/lobehub/lobe-chat/compare/v1.66.2...v1.66.3)
31
+
32
+ <sup>Released on **2025-02-27**</sup>
33
+
34
+ #### 🐛 Bug Fixes
35
+
36
+ - **misc**: Fix fetch assistants plugin error.
37
+
38
+ <br/>
39
+
40
+ <details>
41
+ <summary><kbd>Improvements and Fixes</kbd></summary>
42
+
43
+ #### What's fixed
44
+
45
+ - **misc**: Fix fetch assistants plugin error, closes [#6576](https://github.com/lobehub/lobe-chat/issues/6576) ([9669a02](https://github.com/lobehub/lobe-chat/commit/9669a02))
46
+
47
+ </details>
48
+
49
+ <div align="right">
50
+
51
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
52
+
53
+ </div>
54
+
5
55
  ### [Version 1.66.2](https://github.com/lobehub/lobe-chat/compare/v1.66.1...v1.66.2)
6
56
 
7
57
  <sup>Released on **2025-02-27**</sup>
package/changelog/v1.json CHANGED
@@ -1,4 +1,22 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "improvements": [
5
+ "Optimize smooth output."
6
+ ]
7
+ },
8
+ "date": "2025-02-28",
9
+ "version": "1.66.4"
10
+ },
11
+ {
12
+ "children": {
13
+ "fixes": [
14
+ "Fix fetch assistants plugin error."
15
+ ]
16
+ },
17
+ "date": "2025-02-27",
18
+ "version": "1.66.3"
19
+ },
2
20
  {
3
21
  "children": {
4
22
  "fixes": [
@@ -3,6 +3,7 @@ services:
3
3
  network-service:
4
4
  image: alpine
5
5
  container_name: lobe-network
6
+ restart: always
6
7
  ports:
7
8
  - '${MINIO_PORT}:${MINIO_PORT}' # MinIO API
8
9
  - '9001:9001' # MinIO Console
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "1.66.2",
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: 2,
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
 
@@ -53,7 +53,7 @@ export class DiscoverService {
53
53
  const json = await this.assistantStore.getAgentIndex(locale, revalidate);
54
54
 
55
55
  // @ts-expect-error 目前类型不一致,未来要统一
56
- return json.agents;
56
+ return json.agents ?? [];
57
57
  };
58
58
 
59
59
  getAssistantById = async (
@@ -131,21 +131,27 @@ export class DiscoverService {
131
131
  };
132
132
 
133
133
  getPluginList = async (locale: Locales): Promise<DiscoverPlugintem[]> => {
134
- let res = await fetch(this.pluginStore.getPluginIndexUrl(locale), {
135
- next: { revalidate: 12 * revalidate },
136
- });
137
-
138
- if (!res.ok) {
139
- res = await fetch(this.pluginStore.getPluginIndexUrl(DEFAULT_LANG), {
134
+ try {
135
+ let res = await fetch(this.pluginStore.getPluginIndexUrl(locale), {
140
136
  next: { revalidate: 12 * revalidate },
141
137
  });
142
- }
143
138
 
144
- if (!res.ok) return [];
139
+ if (!res.ok) {
140
+ res = await fetch(this.pluginStore.getPluginIndexUrl(DEFAULT_LANG), {
141
+ next: { revalidate: 12 * revalidate },
142
+ });
143
+ }
144
+
145
+ if (!res.ok) return [];
145
146
 
146
- const json = await res.json();
147
+ const json = await res.json();
147
148
 
148
- return json.plugins;
149
+ return json.plugins ?? [];
150
+ } catch (e) {
151
+ console.error('[getPluginListError] failed to fetch plugin list, error detail:');
152
+ console.error(e);
153
+ return [];
154
+ }
149
155
  };
150
156
 
151
157
  getPluginByIds = async (locale: Locales, identifiers: string[]): Promise<DiscoverPlugintem[]> => {
@@ -142,9 +142,24 @@ describe('fetchSSE', () => {
142
142
  smoothing: true,
143
143
  });
144
144
 
145
- expect(mockOnMessageHandle).toHaveBeenNthCalledWith(1, { text: 'Hell', type: 'text' });
146
- expect(mockOnMessageHandle).toHaveBeenNthCalledWith(2, { text: 'o', type: 'text' });
147
- expect(mockOnMessageHandle).toHaveBeenNthCalledWith(3, { text: ' World', type: 'text' });
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
- expect(mockOnMessageHandle).toHaveBeenNthCalledWith(1, { text: 'Hell', type: 'text' });
380
- expect(mockOnMessageHandle).toHaveBeenNthCalledWith(2, { text: 'o', type: 'text' });
381
- expect(mockOnMessageHandle).toHaveBeenNthCalledWith(3, { text: ' World', type: 'text' });
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 = 4;
67
+ const START_ANIMATION_SPEED = 10; // 默认起始速度
68
68
 
69
- const END_ANIMATION_SPEED = 15;
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
- // define startAnimation function to display the text in buffer smooth
93
- // when you need to start the animation, call this function
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
- cancelAnimationFrame(animationFrameId!);
107
- animationFrameId = null;
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
- // 从队列中获取前 n 个字符(如果存在)
116
- const charsToAdd = outputQueue.splice(0, speed).join('');
117
- buffer += charsToAdd;
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(END_ANIMATION_SPEED);
464
+ await textController.startAnimation(smoothingSpeed);
439
465
  }
440
466
 
441
467
  if (toolCallsController.isTokenRemain()) {