@lobehub/chat 0.119.0 → 0.119.1

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,23 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ### [Version 0.119.1](https://github.com/lobehub/lobe-chat/compare/v0.119.0...v0.119.1)
6
+
7
+ <sup>Released on **2024-01-05**</sup>
8
+
9
+ <br/>
10
+
11
+ <details>
12
+ <summary><kbd>Improvements and Fixes</kbd></summary>
13
+
14
+ </details>
15
+
16
+ <div align="right">
17
+
18
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
19
+
20
+ </div>
21
+
5
22
  ## [Version 0.119.0](https://github.com/lobehub/lobe-chat/compare/v0.118.10...v0.119.0)
6
23
 
7
24
  <sup>Released on **2024-01-04**</sup>
package/README.md CHANGED
@@ -208,14 +208,14 @@ Our marketplace is not just a showcase platform but also a collaborative space.
208
208
 
209
209
  <!-- AGENT LIST -->
210
210
 
211
- | Recent Submits | Description |
212
- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
213
- | [Amazon Title Assistant](https://chat-preview.lobehub.com/market?agent=amazon)<br/><sup>By **[cm2457618290](https://github.com/cm2457618290)** on **2024-01-02**</sup> | Provide product keywords or product links to automatically write titles and product introductions<br/>`assistant` |
214
- | [Exam Assistant](https://chat-preview.lobehub.com/market?agent=generador-examenes)<br/><sup>By **[aitorroma](https://github.com/aitorroma)** on **2024-01-02**</sup> | I am a skills summary assistant and cannot conduct interactive exams. However, I can help you summarize your skills and knowledge in a clear and concise format.<br/>`exam` `learning` `statistics` |
215
- | [Teaching Mentor](https://chat-preview.lobehub.com/market?agent=ljrwwjl-development)<br/><sup>By **[ljr1314](https://github.com/ljr1314)** on **2024-01-02**</sup> | A friendly and helpful mentor who customizes explanations and examples based on the user's learning level and interests to ensure clarity. Ask 4 questions, then provide explanations, examples, and analogies, and ensure understanding through questioning. Finally, have the user explain the topic in their own words and provide an example. End on a positive note and encourage further learning.<br/>`mentor` `education` `explanation` `communication` `learning` |
216
- | [MidjourneyGPT](https://chat-preview.lobehub.com/market?agent=prompt-composition)<br/><sup>By **[richards199999](https://github.com/richards199999)** on **2023-12-30**</sup> | Write perfect and beautiful prompts for Midjourney. (Including V6!)<br/>`midjourney` `prompt` `ai` |
217
-
218
- > 📊 Total agents: [<kbd>**87**</kbd> ](https://github.com/lobehub/lobe-chat-agents)
211
+ | Recent Submits | Description |
212
+ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
213
+ | [3D Animation Engineer](https://chat-preview.lobehub.com/market?agent=react-three-3-d-expert)<br/><sup>By **[arvinxx](https://github.com/arvinxx)** on **2024-01-03**</sup> | Proficient in React, Three.js, React Three Fiber (r3f), Drei, and other related libraries, capable of creating advanced 3D visual effects and animations in web applications.<br/>`3-d-animation` `react` `three-js` `web-design` `animation` |
214
+ | [Amazon Title Assistant](https://chat-preview.lobehub.com/market?agent=amazon)<br/><sup>By **[cm2457618290](https://github.com/cm2457618290)** on **2024-01-02**</sup> | Provide product keywords or product links to automatically write titles and product introductions<br/>`assistant` |
215
+ | [Exam Assistant](https://chat-preview.lobehub.com/market?agent=generador-examenes)<br/><sup>By **[aitorroma](https://github.com/aitorroma)** on **2024-01-02**</sup> | I am a skills summary assistant and cannot conduct interactive exams. However, I can help you summarize your skills and knowledge in a clear and concise format.<br/>`exam` `learning` `statistics` |
216
+ | [Teaching Mentor](https://chat-preview.lobehub.com/market?agent=ljrwwjl-development)<br/><sup>By **[ljr1314](https://github.com/ljr1314)** on **2024-01-02**</sup> | A friendly and helpful mentor who customizes explanations and examples based on the user's learning level and interests to ensure clarity. Ask 4 questions, then provide explanations, examples, and analogies, and ensure understanding through questioning. Finally, have the user explain the topic in their own words and provide an example. End on a positive note and encourage further learning.<br/>`mentor` `education` `explanation` `communication` `learning` |
217
+
218
+ > 📊 Total agents: [<kbd>**88**</kbd> ](https://github.com/lobehub/lobe-chat-agents)
219
219
 
220
220
  <!-- AGENT LIST -->
221
221
 
package/README.zh-CN.md CHANGED
@@ -184,14 +184,14 @@ LobeChat 的插件生态系统是其核心功能的重要扩展,它极大地
184
184
 
185
185
  <!-- AGENT LIST -->
186
186
 
187
- | 最近新增 | 助手说明 |
188
- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
189
- | [Amazon Title Assistant](https://chat-preview.lobehub.com/market?agent=amazon)<br/><sup>By **[cm2457618290](https://github.com/cm2457618290)** on **2024-01-02**</sup> | Provide product keywords or product links to automatically write titles and product introductions<br/>`assistant` |
190
- | [考试助手](https://chat-preview.lobehub.com/market?agent=generador-examenes)<br/><sup>By **[aitorroma](https://github.com/aitorroma)** on **2024-01-02**</sup> | 我是一个技能总结助手,无法进行互动式考试。但我可以帮助你以清晰简洁的格式总结你的技能和知识。<br/>`考试` `学习` `统计` |
191
- | [教学导师](https://chat-preview.lobehub.com/market?agent=ljrwwjl-development)<br/><sup>By **[ljr1314](https://github.com/ljr1314)** on **2024-01-02**</sup> | 友好乐于助人的导师,根据用户的学习水平和兴趣定制解释和示例,确保简洁明了。问 4 个问题,然后提供解释、例子和类比,并通过提问确保用户理解。最后让用户用自己的语言解释主题,并给出一个例子。以积极正面的方式结束,并鼓励用户深入学习。<br/>`导师` `教育` `解释` `交流` `学习` |
192
- | [MidjourneyGPT](https://chat-preview.lobehub.com/market?agent=prompt-composition)<br/><sup>By **[richards199999](https://github.com/richards199999)** on **2023-12-30**</sup> | 为 Midjourney 编写完美而美丽的提示。(包括 V6!)<br/>`midjourney` `prompt` `ai` |
193
-
194
- > 📊 Total agents: [<kbd>**87**</kbd> ](https://github.com/lobehub/lobe-chat-agents)
187
+ | 最近新增 | 助手说明 |
188
+ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
189
+ | [三维动画工程师](https://chat-preview.lobehub.com/market?agent=react-three-3-d-expert)<br/><sup>By **[arvinxx](https://github.com/arvinxx)** on **2024-01-03**</sup> | 擅长 React、Three.js、React Three Fiber(r3f)、Drei 等库,能够在网页应用中创造高级别的三维视觉效果和动画。<br/>`3-d动画` `react` `three-js` `网页设计` `动画` |
190
+ | [Amazon Title Assistant](https://chat-preview.lobehub.com/market?agent=amazon)<br/><sup>By **[cm2457618290](https://github.com/cm2457618290)** on **2024-01-02**</sup> | Provide product keywords or product links to automatically write titles and product introductions<br/>`assistant` |
191
+ | [考试助手](https://chat-preview.lobehub.com/market?agent=generador-examenes)<br/><sup>By **[aitorroma](https://github.com/aitorroma)** on **2024-01-02**</sup> | 我是一个技能总结助手,无法进行互动式考试。但我可以帮助你以清晰简洁的格式总结你的技能和知识。<br/>`考试` `学习` `统计` |
192
+ | [教学导师](https://chat-preview.lobehub.com/market?agent=ljrwwjl-development)<br/><sup>By **[ljr1314](https://github.com/ljr1314)** on **2024-01-02**</sup> | 友好乐于助人的导师,根据用户的学习水平和兴趣定制解释和示例,确保简洁明了。问 4 个问题,然后提供解释、例子和类比,并通过提问确保用户理解。最后让用户用自己的语言解释主题,并给出一个例子。以积极正面的方式结束,并鼓励用户深入学习。<br/>`导师` `教育` `解释` `交流` `学习` |
193
+
194
+ > 📊 Total agents: [<kbd>**88**</kbd> ](https://github.com/lobehub/lobe-chat-agents)
195
195
 
196
196
  <!-- AGENT LIST -->
197
197
 
package/docs/Home.md CHANGED
@@ -36,7 +36,7 @@ LobeChat is a open-source, extensible ([Function Calling][fc-url]), high-perform
36
36
  ### ⌨️ Development
37
37
 
38
38
  - [Technical Development Getting Started Guide](https://github.com/lobehub/lobe-chat/wiki/index) | [技术开发上手指南](https://github.com/lobehub/lobe-chat/wiki/index.zh-CN)
39
- - [Environment Setup Guide](https://github.com/lobehub/lobe-chat/wiki/Steup-Development) | [开发环境设置](https://github.com/lobehub/lobe-chat/wiki/Steup-Development.zh-CN)
39
+ - [Environment Setup Guide](https://github.com/lobehub/lobe-chat/wiki/Setup-Development) | [开发环境设置](https://github.com/lobehub/lobe-chat/wiki/Setup-Development.zh-CN)
40
40
  - [Architecture Design](https://github.com/lobehub/lobe-chat/wiki/Architecture) | [架构设计](https://github.com/lobehub/lobe-chat/wiki/Architecture.zh-CN)
41
41
  - [Directory Structure](https://github.com/lobehub/lobe-chat/wiki/Folder-Structure) | [目录架构](https://github.com/lobehub/lobe-chat/wiki/Folder-Structure.zh-CN)
42
42
  - [Data Store Selector](https://github.com/lobehub/lobe-chat/wiki/Data-Store-Selectors) | [数据存储取数模块](https://github.com/lobehub/lobe-chat/wiki/Data-Store-Selectors.zh-CN)
package/docs/_Sidebar.md CHANGED
@@ -24,7 +24,7 @@
24
24
  #### ⌨️ Development
25
25
 
26
26
  - [Technical Development Getting Started Guide](https://github.com/lobehub/lobe-chat/wiki/index) | [技术开发上手指南](https://github.com/lobehub/lobe-chat/wiki/index.zh-CN)
27
- - [Environment Setup Guide](https://github.com/lobehub/lobe-chat/wiki/Steup-Development) | [开发环境设置](https://github.com/lobehub/lobe-chat/wiki/Steup-Development.zh-CN)
27
+ - [Environment Setup Guide](https://github.com/lobehub/lobe-chat/wiki/Setup-Development) | [开发环境设置](https://github.com/lobehub/lobe-chat/wiki/Setup-Development.zh-CN)
28
28
  - [Architecture Design](https://github.com/lobehub/lobe-chat/wiki/Architecture) | [架构设计](https://github.com/lobehub/lobe-chat/wiki/Architecture.zh-CN)
29
29
  - [Directory Structure](https://github.com/lobehub/lobe-chat/wiki/Folder-Structure) | [目录架构](https://github.com/lobehub/lobe-chat/wiki/Folder-Structure.zh-CN)
30
30
  - [Data Store Selector](https://github.com/lobehub/lobe-chat/wiki/Data-Store-Selectors) | [数据存储取数模块](https://github.com/lobehub/lobe-chat/wiki/Data-Store-Selectors.zh-CN)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "0.119.0",
3
+ "version": "0.119.1",
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",
@@ -81,7 +81,7 @@
81
81
  "@vercel/analytics": "^1",
82
82
  "ahooks": "^3",
83
83
  "ai": "^2.2.22",
84
- "antd": "5.12.5",
84
+ "antd": "^5.12.7",
85
85
  "antd-style": "^3",
86
86
  "brotli-wasm": "^1",
87
87
  "chroma-js": "^2",
@@ -107,7 +107,7 @@
107
107
  "query-string": "^8",
108
108
  "react": "^18",
109
109
  "react-dom": "^18",
110
- "react-hotkeys-hook": "4.4.1",
110
+ "react-hotkeys-hook": "^4.4.3",
111
111
  "react-i18next": "^14.0.0",
112
112
  "react-intersection-observer": "^9",
113
113
  "react-layout-kit": "^1",
@@ -133,7 +133,7 @@
133
133
  },
134
134
  "devDependencies": {
135
135
  "@commitlint/cli": "^18",
136
- "@ducanh2912/next-pwa": "^9",
136
+ "@ducanh2912/next-pwa": "^10",
137
137
  "@lobehub/i18n-cli": "latest",
138
138
  "@lobehub/lint": "latest",
139
139
  "@next/bundle-analyzer": "^14",
@@ -152,7 +152,7 @@
152
152
  "@types/ua-parser-js": "^0.7",
153
153
  "@types/uuid": "^9",
154
154
  "@umijs/lint": "^4",
155
- "@vitest/coverage-v8": "^1",
155
+ "@vitest/coverage-v8": "1.1.1",
156
156
  "commitlint": "^18",
157
157
  "consola": "^3",
158
158
  "dpdm": "^3",
@@ -176,7 +176,7 @@
176
176
  "typescript": "^5",
177
177
  "unified": "^11",
178
178
  "unist-util-visit": "^5",
179
- "vitest": "^1",
179
+ "vitest": "1.1.1",
180
180
  "vitest-canvas-mock": "^0.3.3"
181
181
  },
182
182
  "publishConfig": {
@@ -395,9 +395,9 @@ describe('TopicModel', () => {
395
395
  it('should fail if the database transaction fails', async () => {
396
396
  // 强制数据库事务失败,例如通过在复制过程中抛出异常
397
397
  const dbTransactionFailedError = new Error('DB transaction failed');
398
- vi.spyOn(TopicModel['db'], 'transaction').mockImplementation(async () => {
398
+ vi.spyOn(TopicModel['db'], 'transaction').mockImplementation((async () => {
399
399
  throw dbTransactionFailedError;
400
- });
400
+ }) as any);
401
401
 
402
402
  // 尝试复制主题并捕捉期望的错误
403
403
  await expect(TopicModel.duplicateTopic(originalTopic.id)).rejects.toThrow(
@@ -71,6 +71,12 @@ export interface ChatMessageAction {
71
71
  action?: string,
72
72
  ) => AbortController | undefined;
73
73
  refreshMessages: () => Promise<void>;
74
+ createSmoothMessage: (id: string) => {
75
+ startAnimation: (speed?: number) => Promise<void>;
76
+ stopAnimation: () => void;
77
+ outputQueue: string[];
78
+ isAnimationActive: boolean;
79
+ };
74
80
  }
75
81
 
76
82
  const getAgentConfig = () => agentSelectors.currentAgentConfig(useSessionStore.getState());
@@ -297,7 +303,7 @@ export const chatMessage: StateCreator<
297
303
  set({ messages }, false, n(`dispatchMessage/${payload.type}`, payload));
298
304
  },
299
305
  fetchAIChatMessage: async (messages, assistantId) => {
300
- const { toggleChatLoading, refreshMessages, updateMessageContent } = get();
306
+ const { toggleChatLoading, refreshMessages, updateMessageContent, createSmoothMessage } = get();
301
307
 
302
308
  const abortController = toggleChatLoading(
303
309
  true,
@@ -366,19 +372,35 @@ export const chatMessage: StateCreator<
366
372
  let functionCallAtEnd = false;
367
373
  let functionCallContent = '';
368
374
 
375
+ const { startAnimation, stopAnimation, outputQueue, isAnimationActive } =
376
+ createSmoothMessage(assistantId);
377
+
369
378
  await fetchSSE(fetcher, {
370
379
  onErrorHandle: async (error) => {
371
380
  await messageService.updateMessageError(assistantId, error);
372
381
  await refreshMessages();
373
382
  },
383
+ onAbort: async () => {
384
+ stopAnimation();
385
+ },
374
386
  onFinish: async (content) => {
387
+ stopAnimation();
388
+
389
+ // 如果还有内容没有显示,则继续动画
390
+ if (outputQueue.length > 0) {
391
+ await startAnimation(15);
392
+ }
393
+
394
+ // 如果没有剩余内容,直接更新消息内容
375
395
  // update the content after fetch result
376
396
  await updateMessageContent(assistantId, content);
377
397
  },
378
398
  onMessageHandle: async (text) => {
379
399
  output += text;
400
+ outputQueue.push(...text.split(''));
380
401
 
381
- await updateMessageContent(assistantId, output);
402
+ // 如果是第一次接收到消息,启动打字动画
403
+ if (!isAnimationActive) startAnimation();
382
404
 
383
405
  // is this message is just a function call
384
406
  if (isFunctionMessageAtStart(output)) isFunctionCall = true;
@@ -416,4 +438,69 @@ export const chatMessage: StateCreator<
416
438
  window.removeEventListener('beforeunload', preventLeavingFn);
417
439
  }
418
440
  },
441
+
442
+ createSmoothMessage: (id) => {
443
+ const { dispatchMessage } = get();
444
+
445
+ let buffer = '';
446
+ // why use queue: https://shareg.pt/GLBrjpK
447
+ let outputQueue: string[] = [];
448
+
449
+ // eslint-disable-next-line no-undef
450
+ let animationTimeoutId: NodeJS.Timeout | null = null;
451
+ let isAnimationActive = false;
452
+
453
+ // when you need to stop the animation, call this function
454
+ const stopAnimation = () => {
455
+ isAnimationActive = false;
456
+ if (animationTimeoutId !== null) {
457
+ clearTimeout(animationTimeoutId);
458
+ animationTimeoutId = null;
459
+ }
460
+ };
461
+
462
+ // define startAnimation function to display the text in buffer smooth
463
+ // when you need to start the animation, call this function
464
+ const startAnimation = (speed = 2) =>
465
+ new Promise<void>((resolve) => {
466
+ if (isAnimationActive) {
467
+ resolve();
468
+ return;
469
+ }
470
+
471
+ isAnimationActive = true;
472
+
473
+ const updateText = () => {
474
+ // 如果动画已经不再激活,则停止更新文本
475
+ if (!isAnimationActive) {
476
+ clearTimeout(animationTimeoutId!);
477
+ animationTimeoutId = null;
478
+ resolve();
479
+ }
480
+
481
+ // 如果还有文本没有显示
482
+ // 检查队列中是否有字符待显示
483
+ if (outputQueue.length > 0) {
484
+ // 从队列中获取前两个字符(如果存在)
485
+ const charsToAdd = outputQueue.splice(0, speed).join('');
486
+ buffer += charsToAdd;
487
+
488
+ // 更新消息内容,这里可能需要结合实际情况调整
489
+ dispatchMessage({ id, key: 'content', type: 'updateMessage', value: buffer });
490
+
491
+ // 设置下一个字符的延迟
492
+ animationTimeoutId = setTimeout(updateText, 16); // 16 毫秒的延迟模拟打字机效果
493
+ } else {
494
+ // 当所有字符都显示完毕时,清除动画状态
495
+ isAnimationActive = false;
496
+ animationTimeoutId = null;
497
+ resolve();
498
+ }
499
+ };
500
+
501
+ updateText();
502
+ });
503
+
504
+ return { startAnimation, stopAnimation, outputQueue, isAnimationActive };
505
+ },
419
506
  });
@@ -0,0 +1,131 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ import { ErrorResponse, ErrorType } from '@/types/fetch';
4
+
5
+ import { fetchAIFactory, fetchSSE, getMessageError } from './fetch';
6
+
7
+ // 模拟 i18next
8
+ vi.mock('i18next', () => ({
9
+ t: vi.fn((key) => `translated_${key}`),
10
+ }));
11
+
12
+ // 模拟 Response
13
+ const createMockResponse = (body: any, ok: boolean, status: number = 200) => ({
14
+ ok,
15
+ status,
16
+ json: vi.fn(async () => body),
17
+ clone: vi.fn(function () {
18
+ // @ts-ignore
19
+ return this;
20
+ }),
21
+ text: vi.fn(async () => JSON.stringify(body)),
22
+ body: {
23
+ getReader: () => {
24
+ let done = false;
25
+ return {
26
+ read: () => {
27
+ if (!done) {
28
+ done = true;
29
+ return Promise.resolve({
30
+ value: new TextEncoder().encode(JSON.stringify(body)),
31
+ done: false,
32
+ });
33
+ } else {
34
+ return Promise.resolve({ done: true });
35
+ }
36
+ },
37
+ };
38
+ },
39
+ },
40
+ });
41
+
42
+ // 在每次测试后清理所有模拟
43
+ afterEach(() => {
44
+ vi.restoreAllMocks();
45
+ });
46
+
47
+ describe('getMessageError', () => {
48
+ it('should handle business error correctly', async () => {
49
+ const mockErrorResponse: ErrorResponse = {
50
+ body: 'Error occurred',
51
+ errorType: 'InvalidAccessCode',
52
+ };
53
+ const mockResponse = createMockResponse(mockErrorResponse, false, 400);
54
+
55
+ const error = await getMessageError(mockResponse as any);
56
+
57
+ expect(error).toEqual({
58
+ body: mockErrorResponse.body,
59
+ message: 'translated_response.InvalidAccessCode',
60
+ type: mockErrorResponse.errorType,
61
+ });
62
+ expect(mockResponse.json).toHaveBeenCalled();
63
+ });
64
+
65
+ it('should handle regular error correctly', async () => {
66
+ const mockResponse = createMockResponse({}, false, 500);
67
+ mockResponse.json.mockImplementationOnce(() => {
68
+ throw new Error('Failed to parse');
69
+ });
70
+
71
+ const error = await getMessageError(mockResponse as any);
72
+
73
+ expect(error).toEqual({
74
+ message: 'translated_response.500',
75
+ type: 500,
76
+ });
77
+ expect(mockResponse.json).toHaveBeenCalled();
78
+ });
79
+ });
80
+
81
+ describe('fetchAIFactory', () => {
82
+ it('should handle successful response', async () => {
83
+ const fetcher = async (params: any, options: any) =>
84
+ new Response('AI response', { status: 200 });
85
+ const params = {
86
+ /* mock params */
87
+ };
88
+ const onMessageHandle = vi.fn();
89
+ const onFinish = vi.fn();
90
+ const onError = vi.fn();
91
+ const onLoadingChange = vi.fn();
92
+ const abortController = new AbortController();
93
+
94
+ const fetchAIFn = fetchAIFactory(fetcher);
95
+ const result = await fetchAIFn({
96
+ params,
97
+ onMessageHandle,
98
+ onFinish,
99
+ onError,
100
+ onLoadingChange,
101
+ abortController,
102
+ });
103
+
104
+ expect(result).toBe('AI response');
105
+ expect(onMessageHandle).toHaveBeenCalled();
106
+ expect(onFinish).toHaveBeenCalled();
107
+ expect(onError).not.toHaveBeenCalled();
108
+ expect(onLoadingChange).toHaveBeenCalledTimes(2);
109
+ });
110
+
111
+ it('should handle error response', async () => {
112
+ const fetcher = async (params: any, options: any) =>
113
+ new Response(null, { status: 404, statusText: 'Not Found' });
114
+ const params = {
115
+ /* mock params */
116
+ };
117
+ const onError = vi.fn();
118
+ const onLoadingChange = vi.fn();
119
+ const abortController = new AbortController();
120
+
121
+ const fetchAIFn = fetchAIFactory(fetcher);
122
+ await fetchAIFn({ params, onError, onLoadingChange, abortController });
123
+
124
+ expect(onError).toHaveBeenCalledWith(expect.any(Error), {
125
+ message: 'translated_response.404',
126
+ type: 404,
127
+ });
128
+ expect(onLoadingChange).toHaveBeenCalledTimes(3);
129
+ expect(onLoadingChange.mock.lastCall).toEqual([false]);
130
+ });
131
+ });
@@ -11,13 +11,13 @@ export const getMessageError = async (response: Response) => {
11
11
  const data = (await response.json()) as ErrorResponse;
12
12
  chatMessageError = {
13
13
  body: data.body,
14
- message: t(`response.${data.errorType}`),
14
+ message: t(`response.${data.errorType}` as any, { ns: 'error' }),
15
15
  type: data.errorType,
16
16
  };
17
17
  } catch {
18
18
  // 如果无法正常返回,说明是常规报错
19
19
  chatMessageError = {
20
- message: t(`response.${response.status}`),
20
+ message: t(`response.${response.status}` as any, { ns: 'error' }),
21
21
  type: response.status as ErrorType,
22
22
  };
23
23
  }
@@ -25,9 +25,12 @@ export const getMessageError = async (response: Response) => {
25
25
  return chatMessageError;
26
26
  };
27
27
 
28
+ type SSEFinishType = 'done' | 'error' | 'abort';
29
+
28
30
  export interface FetchSSEOptions {
31
+ onAbort?: (text: string) => Promise<void>;
29
32
  onErrorHandle?: (error: ChatMessageError) => void;
30
- onFinish?: (text: string) => Promise<void>;
33
+ onFinish?: (text: string, type: SSEFinishType) => Promise<void>;
31
34
  onMessageHandle?: (text: string) => void;
32
35
  }
33
36
 
@@ -57,17 +60,30 @@ export const fetchSSE = async (fetchFn: () => Promise<Response>, options: FetchS
57
60
  const decoder = new TextDecoder();
58
61
 
59
62
  let done = false;
63
+ let finishedType: SSEFinishType = 'done';
60
64
 
61
65
  while (!done) {
62
- const { value, done: doneReading } = await reader.read();
63
- done = doneReading;
64
- const chunkValue = decoder.decode(value, { stream: true });
65
-
66
- output += chunkValue;
67
- options.onMessageHandle?.(chunkValue);
66
+ try {
67
+ const { value, done: doneReading } = await reader.read();
68
+ done = doneReading;
69
+ const chunkValue = decoder.decode(value, { stream: true });
70
+
71
+ output += chunkValue;
72
+ options.onMessageHandle?.(chunkValue);
73
+ } catch (error) {
74
+ done = true;
75
+
76
+ if ((error as TypeError).name === 'AbortError') {
77
+ finishedType = 'abort';
78
+ options?.onAbort?.(output);
79
+ } else {
80
+ finishedType = 'error';
81
+ console.error(error);
82
+ }
83
+ }
68
84
  }
69
85
 
70
- await options?.onFinish?.(output);
86
+ await options?.onFinish?.(output, finishedType);
71
87
 
72
88
  return returnRes;
73
89
  };