@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 +17 -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 +89 -2
- 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,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
|
+
[](#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
|
|
212
|
-
|
|
|
213
|
-
| [
|
|
214
|
-
| [
|
|
215
|
-
| [
|
|
216
|
-
| [
|
|
217
|
-
|
|
218
|
-
> 📊 Total agents: [<kbd>**
|
|
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
|
-
| [
|
|
190
|
-
| [
|
|
191
|
-
| [
|
|
192
|
-
| [
|
|
193
|
-
|
|
194
|
-
> 📊 Total agents: [<kbd>**
|
|
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/
|
|
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.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.
|
|
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,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
|
-
|
|
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
|
+
});
|
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
|