@lobehub/chat 0.119.0 → 0.119.2
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 +42 -0
- package/README.md +8 -8
- package/README.zh-CN.md +8 -8
- package/docs/Home.md +1 -1
- package/docs/_Sidebar.md +1 -1
- package/package.json +6 -6
- package/src/database/models/__tests__/topic.test.ts +2 -2
- package/src/store/chat/slices/message/action.ts +97 -4
- package/src/utils/fetch.test.ts +131 -0
- package/src/utils/fetch.ts +26 -10
- /package/docs/Development/{Steup-Development.md → Setup-Development.md} +0 -0
- /package/docs/Development/{Steup-Development.zh-CN.md → Setup-Development.zh-CN.md} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,48 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
### [Version 0.119.2](https://github.com/lobehub/lobe-chat/compare/v0.119.1...v0.119.2)
|
|
6
|
+
|
|
7
|
+
<sup>Released on **2024-01-05**</sup>
|
|
8
|
+
|
|
9
|
+
#### 🐛 Bug Fixes
|
|
10
|
+
|
|
11
|
+
- **misc**: Fix function call error with smooth animation.
|
|
12
|
+
|
|
13
|
+
<br/>
|
|
14
|
+
|
|
15
|
+
<details>
|
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
17
|
+
|
|
18
|
+
#### What's fixed
|
|
19
|
+
|
|
20
|
+
- **misc**: Fix function call error with smooth animation, closes [#946](https://github.com/lobehub/lobe-chat/issues/946) ([7242aee](https://github.com/lobehub/lobe-chat/commit/7242aee))
|
|
21
|
+
|
|
22
|
+
</details>
|
|
23
|
+
|
|
24
|
+
<div align="right">
|
|
25
|
+
|
|
26
|
+
[](#readme-top)
|
|
27
|
+
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
### [Version 0.119.1](https://github.com/lobehub/lobe-chat/compare/v0.119.0...v0.119.1)
|
|
31
|
+
|
|
32
|
+
<sup>Released on **2024-01-05**</sup>
|
|
33
|
+
|
|
34
|
+
<br/>
|
|
35
|
+
|
|
36
|
+
<details>
|
|
37
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
38
|
+
|
|
39
|
+
</details>
|
|
40
|
+
|
|
41
|
+
<div align="right">
|
|
42
|
+
|
|
43
|
+
[](#readme-top)
|
|
44
|
+
|
|
45
|
+
</div>
|
|
46
|
+
|
|
5
47
|
## [Version 0.119.0](https://github.com/lobehub/lobe-chat/compare/v0.118.10...v0.119.0)
|
|
6
48
|
|
|
7
49
|
<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
|
|
212
|
-
|
|
|
213
|
-
| [
|
|
214
|
-
| [
|
|
215
|
-
| [
|
|
216
|
-
| [
|
|
217
|
-
|
|
218
|
-
> 📊 Total agents: [<kbd>**
|
|
211
|
+
| Recent Submits | Description |
|
|
212
|
+
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
213
|
+
| [SVG Flowchart Explanation Assistant](https://chat-preview.lobehub.com/market?agent=svg-flowchart-explanation-assistant)<br/><sup>By **[Justin3go](https://github.com/Justin3go)** on **2024-01-05**</sup> | Explain SVG flowcharts by inputting SVG source code and interpreting the flowchart.<br/>`flowchart-explanation` `technical-documentation-writing` `business-knowledge` |
|
|
214
|
+
| [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` |
|
|
215
|
+
| [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` |
|
|
216
|
+
| [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` |
|
|
217
|
+
|
|
218
|
+
> 📊 Total agents: [<kbd>**89**</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
|
-
| [
|
|
190
|
-
| [
|
|
191
|
-
| [
|
|
192
|
-
| [
|
|
193
|
-
|
|
194
|
-
> 📊 Total agents: [<kbd>**
|
|
187
|
+
| 最近新增 | 助手说明 |
|
|
188
|
+
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
189
|
+
| [SVG 流程图解释助手](https://chat-preview.lobehub.com/market?agent=svg-flowchart-explanation-assistant)<br/><sup>By **[Justin3go](https://github.com/Justin3go)** on **2024-01-05**</sup> | SVG 流程图解释,输入 SVG 源代码,解释该流程图<br/>`流程图解释` `技术文档编写` `业务知识` |
|
|
190
|
+
| [三维动画工程师](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` `网页设计` `动画` |
|
|
191
|
+
| [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` |
|
|
192
|
+
| [考试助手](https://chat-preview.lobehub.com/market?agent=generador-examenes)<br/><sup>By **[aitorroma](https://github.com/aitorroma)** on **2024-01-02**</sup> | 我是一个技能总结助手,无法进行互动式考试。但我可以帮助你以清晰简洁的格式总结你的技能和知识。<br/>`考试` `学习` `统计` |
|
|
193
|
+
|
|
194
|
+
> 📊 Total agents: [<kbd>**89**</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/
|
|
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/
|
|
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.
|
|
3
|
+
"version": "0.119.2",
|
|
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.
|
|
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.
|
|
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": "^
|
|
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": "
|
|
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": "
|
|
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,22 +372,44 @@ 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
|
+
// if there is still content not displayed,
|
|
390
|
+
// and the message is not a function call
|
|
391
|
+
// then continue the animation
|
|
392
|
+
if (outputQueue.length > 0 && !isFunctionCall) {
|
|
393
|
+
await startAnimation(15);
|
|
394
|
+
}
|
|
395
|
+
|
|
375
396
|
// update the content after fetch result
|
|
376
397
|
await updateMessageContent(assistantId, content);
|
|
377
398
|
},
|
|
378
399
|
onMessageHandle: async (text) => {
|
|
379
400
|
output += text;
|
|
380
|
-
|
|
381
|
-
await updateMessageContent(assistantId, output);
|
|
401
|
+
outputQueue.push(...text.split(''));
|
|
382
402
|
|
|
383
403
|
// is this message is just a function call
|
|
384
|
-
if (isFunctionMessageAtStart(output))
|
|
404
|
+
if (isFunctionMessageAtStart(output)) {
|
|
405
|
+
stopAnimation();
|
|
406
|
+
isFunctionCall = true;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// if it's the first time to receive the message,
|
|
410
|
+
// and the message is not a function call
|
|
411
|
+
// then start the animation
|
|
412
|
+
if (!isAnimationActive && !isFunctionCall) startAnimation();
|
|
385
413
|
},
|
|
386
414
|
});
|
|
387
415
|
|
|
@@ -416,4 +444,69 @@ export const chatMessage: StateCreator<
|
|
|
416
444
|
window.removeEventListener('beforeunload', preventLeavingFn);
|
|
417
445
|
}
|
|
418
446
|
},
|
|
447
|
+
|
|
448
|
+
createSmoothMessage: (id) => {
|
|
449
|
+
const { dispatchMessage } = get();
|
|
450
|
+
|
|
451
|
+
let buffer = '';
|
|
452
|
+
// why use queue: https://shareg.pt/GLBrjpK
|
|
453
|
+
let outputQueue: string[] = [];
|
|
454
|
+
|
|
455
|
+
// eslint-disable-next-line no-undef
|
|
456
|
+
let animationTimeoutId: NodeJS.Timeout | null = null;
|
|
457
|
+
let isAnimationActive = false;
|
|
458
|
+
|
|
459
|
+
// when you need to stop the animation, call this function
|
|
460
|
+
const stopAnimation = () => {
|
|
461
|
+
isAnimationActive = false;
|
|
462
|
+
if (animationTimeoutId !== null) {
|
|
463
|
+
clearTimeout(animationTimeoutId);
|
|
464
|
+
animationTimeoutId = null;
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
// define startAnimation function to display the text in buffer smooth
|
|
469
|
+
// when you need to start the animation, call this function
|
|
470
|
+
const startAnimation = (speed = 2) =>
|
|
471
|
+
new Promise<void>((resolve) => {
|
|
472
|
+
if (isAnimationActive) {
|
|
473
|
+
resolve();
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
isAnimationActive = true;
|
|
478
|
+
|
|
479
|
+
const updateText = () => {
|
|
480
|
+
// 如果动画已经不再激活,则停止更新文本
|
|
481
|
+
if (!isAnimationActive) {
|
|
482
|
+
clearTimeout(animationTimeoutId!);
|
|
483
|
+
animationTimeoutId = null;
|
|
484
|
+
resolve();
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// 如果还有文本没有显示
|
|
488
|
+
// 检查队列中是否有字符待显示
|
|
489
|
+
if (outputQueue.length > 0) {
|
|
490
|
+
// 从队列中获取前两个字符(如果存在)
|
|
491
|
+
const charsToAdd = outputQueue.splice(0, speed).join('');
|
|
492
|
+
buffer += charsToAdd;
|
|
493
|
+
|
|
494
|
+
// 更新消息内容,这里可能需要结合实际情况调整
|
|
495
|
+
dispatchMessage({ id, key: 'content', type: 'updateMessage', value: buffer });
|
|
496
|
+
|
|
497
|
+
// 设置下一个字符的延迟
|
|
498
|
+
animationTimeoutId = setTimeout(updateText, 16); // 16 毫秒的延迟模拟打字机效果
|
|
499
|
+
} else {
|
|
500
|
+
// 当所有字符都显示完毕时,清除动画状态
|
|
501
|
+
isAnimationActive = false;
|
|
502
|
+
animationTimeoutId = null;
|
|
503
|
+
resolve();
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
updateText();
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
return { startAnimation, stopAnimation, outputQueue, isAnimationActive };
|
|
511
|
+
},
|
|
419
512
|
});
|
|
@@ -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
|
+
});
|
package/src/utils/fetch.ts
CHANGED
|
@@ -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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
};
|
|
File without changes
|
|
File without changes
|