@lobehub/chat 1.11.4 → 1.11.5
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 +25 -0
- package/locales/ar/error.json +3 -1
- package/locales/bg-BG/error.json +3 -1
- package/locales/de-DE/error.json +3 -1
- package/locales/en-US/error.json +3 -1
- package/locales/es-ES/error.json +3 -1
- package/locales/fr-FR/error.json +3 -1
- package/locales/it-IT/error.json +3 -1
- package/locales/ja-JP/error.json +3 -1
- package/locales/ko-KR/error.json +3 -1
- package/locales/nl-NL/error.json +3 -1
- package/locales/pl-PL/error.json +3 -1
- package/locales/pt-BR/error.json +3 -1
- package/locales/ru-RU/error.json +3 -1
- package/locales/tr-TR/error.json +3 -1
- package/locales/vi-VN/error.json +3 -1
- package/locales/zh-CN/error.json +2 -0
- package/locales/zh-TW/error.json +3 -1
- package/package.json +2 -3
- package/src/app/(main)/chat/(workspace)/@portal/{features → Home}/Artifacts/ArtifactList/index.tsx +1 -1
- package/src/app/(main)/chat/(workspace)/@portal/{features → Home}/Artifacts/index.tsx +2 -2
- package/src/app/(main)/chat/(workspace)/@portal/Home/index.tsx +13 -0
- package/src/app/(main)/chat/(workspace)/@portal/_layout/Desktop.tsx +1 -1
- package/src/app/(main)/chat/(workspace)/@portal/components/SkeletonLoading.tsx +14 -0
- package/src/app/(main)/chat/(workspace)/@portal/default.tsx +6 -6
- package/src/app/(main)/chat/(workspace)/@portal/error.tsx +5 -0
- package/src/app/(main)/chat/(workspace)/@portal/features/Header.tsx +1 -0
- package/src/app/(main)/chat/(workspace)/@portal/loading.tsx +3 -0
- package/src/app/(main)/chat/(workspace)/@portal/router.tsx +19 -0
- package/src/const/message.ts +2 -0
- package/src/features/Conversation/Messages/Assistant/index.tsx +2 -2
- package/src/features/Conversation/Messages/Default.tsx +8 -3
- package/src/libs/agent-runtime/error.ts +1 -0
- package/src/libs/agent-runtime/utils/streams/openai.test.ts +42 -0
- package/src/libs/agent-runtime/utils/streams/openai.ts +63 -40
- package/src/libs/agent-runtime/utils/streams/protocol.ts +1 -1
- package/src/libs/trpc/client/edge.ts +1 -0
- package/src/libs/trpc/middleware/userAuth.ts +1 -0
- package/src/locales/default/error.ts +6 -1
- package/src/services/__tests__/chat.test.ts +0 -1
- package/src/store/agent/slices/chat/action.ts +2 -1
- package/src/store/chat/slices/message/action.test.ts +4 -4
- package/src/store/chat/slices/message/action.ts +2 -2
- package/src/store/session/slices/session/action.ts +2 -1
- package/src/store/tool/slices/plugin/action.ts +2 -1
- package/src/store/user/slices/settings/action.ts +3 -1
- package/src/types/fetch.ts +1 -0
- package/src/utils/downloadFile.ts +18 -0
- package/src/utils/{fetch.test.ts → fetch/__tests__/fetchSSE.test.ts} +168 -258
- package/src/utils/fetch/__tests__/parseError.test.ts +89 -0
- package/src/utils/fetch/__tests__/parseToolCalls.test.ts +123 -0
- package/src/utils/fetch/fetchEventSource/index.ts +110 -0
- package/src/utils/fetch/fetchEventSource/parse.ts +182 -0
- package/src/utils/{fetch.ts → fetch/fetchSSE.ts} +100 -118
- package/src/utils/fetch/index.ts +2 -0
- package/src/utils/fetch/parseError.ts +26 -0
- package/src/utils/fetch/parseToolCalls.ts +25 -0
- package/vitest.config.ts +1 -0
- package/src/app/(main)/chat/(workspace)/@portal/index.tsx +0 -24
- /package/src/app/(main)/chat/(workspace)/@portal/{features/ArtifactUI → Artifacts}/Footer.tsx +0 -0
- /package/src/app/(main)/chat/(workspace)/@portal/{features/ArtifactUI → Artifacts}/ToolRender.tsx +0 -0
- /package/src/app/(main)/chat/(workspace)/@portal/{features/ArtifactUI → Artifacts}/index.tsx +0 -0
- /package/src/app/(main)/chat/(workspace)/@portal/{features → Home}/Artifacts/ArtifactList/Item/index.tsx +0 -0
- /package/src/app/(main)/chat/(workspace)/@portal/{features → Home}/Artifacts/ArtifactList/Item/style.ts +0 -0
|
@@ -1,48 +1,18 @@
|
|
|
1
|
-
import { fetchEventSource } from '@microsoft/fetch-event-source';
|
|
2
|
-
import { FetchEventSourceInit } from '@microsoft/fetch-event-source';
|
|
3
1
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
-
import { ZodError } from 'zod';
|
|
5
2
|
|
|
6
|
-
import {
|
|
3
|
+
import { MESSAGE_CANCEL_FLAT } from '@/const/message';
|
|
4
|
+
import { ChatMessageError } from '@/types/message';
|
|
7
5
|
|
|
8
|
-
import {
|
|
6
|
+
import { FetchEventSourceInit } from '../fetchEventSource';
|
|
7
|
+
import { fetchEventSource } from '../fetchEventSource';
|
|
8
|
+
import { fetchSSE } from '../fetchSSE';
|
|
9
9
|
|
|
10
10
|
// 模拟 i18next
|
|
11
11
|
vi.mock('i18next', () => ({
|
|
12
12
|
t: vi.fn((key) => `translated_${key}`),
|
|
13
13
|
}));
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
const createMockResponse = (body: any, ok: boolean, status: number = 200) => ({
|
|
17
|
-
ok,
|
|
18
|
-
status,
|
|
19
|
-
json: vi.fn(async () => body),
|
|
20
|
-
clone: vi.fn(function () {
|
|
21
|
-
// @ts-ignore
|
|
22
|
-
return this;
|
|
23
|
-
}),
|
|
24
|
-
text: vi.fn(async () => JSON.stringify(body)),
|
|
25
|
-
body: {
|
|
26
|
-
getReader: () => {
|
|
27
|
-
let done = false;
|
|
28
|
-
return {
|
|
29
|
-
read: () => {
|
|
30
|
-
if (!done) {
|
|
31
|
-
done = true;
|
|
32
|
-
return Promise.resolve({
|
|
33
|
-
value: new TextEncoder().encode(JSON.stringify(body)),
|
|
34
|
-
done: false,
|
|
35
|
-
});
|
|
36
|
-
} else {
|
|
37
|
-
return Promise.resolve({ done: true });
|
|
38
|
-
}
|
|
39
|
-
},
|
|
40
|
-
};
|
|
41
|
-
},
|
|
42
|
-
},
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
vi.mock('@microsoft/fetch-event-source', () => ({
|
|
15
|
+
vi.mock('../fetchEventSource', () => ({
|
|
46
16
|
fetchEventSource: vi.fn(),
|
|
47
17
|
}));
|
|
48
18
|
|
|
@@ -51,169 +21,6 @@ afterEach(() => {
|
|
|
51
21
|
vi.restoreAllMocks();
|
|
52
22
|
});
|
|
53
23
|
|
|
54
|
-
describe('getMessageError', () => {
|
|
55
|
-
it('should handle business error correctly', async () => {
|
|
56
|
-
const mockErrorResponse: ErrorResponse = {
|
|
57
|
-
body: 'Error occurred',
|
|
58
|
-
errorType: 'InvalidAccessCode',
|
|
59
|
-
};
|
|
60
|
-
const mockResponse = createMockResponse(mockErrorResponse, false, 400);
|
|
61
|
-
|
|
62
|
-
const error = await getMessageError(mockResponse as any);
|
|
63
|
-
|
|
64
|
-
expect(error).toEqual({
|
|
65
|
-
body: mockErrorResponse.body,
|
|
66
|
-
message: 'translated_response.InvalidAccessCode',
|
|
67
|
-
type: mockErrorResponse.errorType,
|
|
68
|
-
});
|
|
69
|
-
expect(mockResponse.json).toHaveBeenCalled();
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('should handle regular error correctly', async () => {
|
|
73
|
-
const mockResponse = createMockResponse({}, false, 500);
|
|
74
|
-
mockResponse.json.mockImplementationOnce(() => {
|
|
75
|
-
throw new Error('Failed to parse');
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
const error = await getMessageError(mockResponse as any);
|
|
79
|
-
|
|
80
|
-
expect(error).toEqual({
|
|
81
|
-
message: 'translated_response.500',
|
|
82
|
-
type: 500,
|
|
83
|
-
});
|
|
84
|
-
expect(mockResponse.json).toHaveBeenCalled();
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it('should handle timeout error correctly', async () => {
|
|
88
|
-
const mockResponse = createMockResponse(undefined, false, 504);
|
|
89
|
-
const error = await getMessageError(mockResponse as any);
|
|
90
|
-
|
|
91
|
-
expect(error).toEqual({
|
|
92
|
-
message: 'translated_response.504',
|
|
93
|
-
type: 504,
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
describe('parseToolCalls', () => {
|
|
99
|
-
it('should create add new item', () => {
|
|
100
|
-
const chunk = [
|
|
101
|
-
{ index: 0, id: '1', type: 'function', function: { name: 'func', arguments: '' } },
|
|
102
|
-
];
|
|
103
|
-
|
|
104
|
-
const result = parseToolCalls([], chunk);
|
|
105
|
-
expect(result).toEqual([
|
|
106
|
-
{ id: '1', type: 'function', function: { name: 'func', arguments: '' } },
|
|
107
|
-
]);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it('should update arguments if there is a toolCall', () => {
|
|
111
|
-
const origin = [{ id: '1', type: 'function', function: { name: 'func', arguments: '' } }];
|
|
112
|
-
|
|
113
|
-
const chunk1 = [{ index: 0, function: { arguments: '{"lo' } }];
|
|
114
|
-
|
|
115
|
-
const result1 = parseToolCalls(origin, chunk1);
|
|
116
|
-
expect(result1).toEqual([
|
|
117
|
-
{ id: '1', type: 'function', function: { name: 'func', arguments: '{"lo' } },
|
|
118
|
-
]);
|
|
119
|
-
|
|
120
|
-
const chunk2 = [{ index: 0, function: { arguments: 'cation\\": \\"Hangzhou\\"}' } }];
|
|
121
|
-
const result2 = parseToolCalls(result1, chunk2);
|
|
122
|
-
|
|
123
|
-
expect(result2).toEqual([
|
|
124
|
-
{
|
|
125
|
-
id: '1',
|
|
126
|
-
type: 'function',
|
|
127
|
-
function: { name: 'func', arguments: '{"location\\": \\"Hangzhou\\"}' },
|
|
128
|
-
},
|
|
129
|
-
]);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it('should add a new tool call if the index is different', () => {
|
|
133
|
-
const origin = [
|
|
134
|
-
{
|
|
135
|
-
id: '1',
|
|
136
|
-
type: 'function',
|
|
137
|
-
function: { name: 'func', arguments: '{"location\\": \\"Hangzhou\\"}' },
|
|
138
|
-
},
|
|
139
|
-
];
|
|
140
|
-
|
|
141
|
-
const chunk = [
|
|
142
|
-
{
|
|
143
|
-
index: 1,
|
|
144
|
-
id: '2',
|
|
145
|
-
type: 'function',
|
|
146
|
-
function: { name: 'func', arguments: '' },
|
|
147
|
-
},
|
|
148
|
-
];
|
|
149
|
-
|
|
150
|
-
const result1 = parseToolCalls(origin, chunk);
|
|
151
|
-
expect(result1).toEqual([
|
|
152
|
-
{
|
|
153
|
-
id: '1',
|
|
154
|
-
type: 'function',
|
|
155
|
-
function: { name: 'func', arguments: '{"location\\": \\"Hangzhou\\"}' },
|
|
156
|
-
},
|
|
157
|
-
{ id: '2', type: 'function', function: { name: 'func', arguments: '' } },
|
|
158
|
-
]);
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it('should update correct arguments if there are multi tool calls', () => {
|
|
162
|
-
const origin = [
|
|
163
|
-
{
|
|
164
|
-
id: '1',
|
|
165
|
-
type: 'function',
|
|
166
|
-
function: { name: 'func', arguments: '{"location\\": \\"Hangzhou\\"}' },
|
|
167
|
-
},
|
|
168
|
-
{ id: '2', type: 'function', function: { name: 'func', arguments: '' } },
|
|
169
|
-
];
|
|
170
|
-
|
|
171
|
-
const chunk = [{ index: 1, function: { arguments: '{"location\\": \\"Beijing\\"}' } }];
|
|
172
|
-
|
|
173
|
-
const result1 = parseToolCalls(origin, chunk);
|
|
174
|
-
expect(result1).toEqual([
|
|
175
|
-
{
|
|
176
|
-
id: '1',
|
|
177
|
-
type: 'function',
|
|
178
|
-
function: { name: 'func', arguments: '{"location\\": \\"Hangzhou\\"}' },
|
|
179
|
-
},
|
|
180
|
-
{
|
|
181
|
-
id: '2',
|
|
182
|
-
type: 'function',
|
|
183
|
-
function: { name: 'func', arguments: '{"location\\": \\"Beijing\\"}' },
|
|
184
|
-
},
|
|
185
|
-
]);
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
it('should throw error if incomplete tool calls data', () => {
|
|
189
|
-
const origin = [
|
|
190
|
-
{
|
|
191
|
-
id: '1',
|
|
192
|
-
type: 'function',
|
|
193
|
-
function: { name: 'func', arguments: '{"location\\": \\"Hangzhou\\"}' },
|
|
194
|
-
},
|
|
195
|
-
];
|
|
196
|
-
|
|
197
|
-
const chunk = [{ index: 1, id: '2', type: 'function' }];
|
|
198
|
-
|
|
199
|
-
try {
|
|
200
|
-
parseToolCalls(origin, chunk as any);
|
|
201
|
-
} catch (e) {
|
|
202
|
-
expect(e).toEqual(
|
|
203
|
-
new ZodError([
|
|
204
|
-
{
|
|
205
|
-
code: 'invalid_type',
|
|
206
|
-
expected: 'object',
|
|
207
|
-
received: 'undefined',
|
|
208
|
-
path: ['function'],
|
|
209
|
-
message: 'Required',
|
|
210
|
-
},
|
|
211
|
-
]),
|
|
212
|
-
);
|
|
213
|
-
}
|
|
214
|
-
});
|
|
215
|
-
});
|
|
216
|
-
|
|
217
24
|
describe('fetchSSE', () => {
|
|
218
25
|
it('should handle text event correctly', async () => {
|
|
219
26
|
const mockOnMessageHandle = vi.fn();
|
|
@@ -293,65 +100,6 @@ describe('fetchSSE', () => {
|
|
|
293
100
|
});
|
|
294
101
|
});
|
|
295
102
|
|
|
296
|
-
it('should call onAbort when AbortError is thrown', async () => {
|
|
297
|
-
const mockOnAbort = vi.fn();
|
|
298
|
-
|
|
299
|
-
(fetchEventSource as any).mockImplementationOnce(
|
|
300
|
-
(url: string, options: FetchEventSourceInit) => {
|
|
301
|
-
options.onmessage!({ event: 'text', data: JSON.stringify('Hello') } as any);
|
|
302
|
-
options.onerror!({ name: 'AbortError' });
|
|
303
|
-
},
|
|
304
|
-
);
|
|
305
|
-
|
|
306
|
-
await fetchSSE('/', { onAbort: mockOnAbort, smoothing: false });
|
|
307
|
-
|
|
308
|
-
expect(mockOnAbort).toHaveBeenCalledWith('Hello');
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
it('should call onErrorHandle when other error is thrown', async () => {
|
|
312
|
-
const mockOnErrorHandle = vi.fn();
|
|
313
|
-
const mockError = new Error('Unknown error');
|
|
314
|
-
|
|
315
|
-
(fetchEventSource as any).mockImplementationOnce(
|
|
316
|
-
(url: string, options: FetchEventSourceInit) => {
|
|
317
|
-
options.onerror!(mockError);
|
|
318
|
-
},
|
|
319
|
-
);
|
|
320
|
-
|
|
321
|
-
try {
|
|
322
|
-
await fetchSSE('/', { onErrorHandle: mockOnErrorHandle });
|
|
323
|
-
} catch (e) {}
|
|
324
|
-
|
|
325
|
-
expect(mockOnErrorHandle).toHaveBeenCalled();
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
it('should call onErrorHandle when response is not ok', async () => {
|
|
329
|
-
const mockOnErrorHandle = vi.fn();
|
|
330
|
-
|
|
331
|
-
(fetchEventSource as any).mockImplementationOnce(
|
|
332
|
-
async (url: string, options: FetchEventSourceInit) => {
|
|
333
|
-
const res = new Response(JSON.stringify({ errorType: 'SomeError' }), {
|
|
334
|
-
status: 400,
|
|
335
|
-
statusText: 'Error',
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
try {
|
|
339
|
-
await options.onopen!(res as any);
|
|
340
|
-
} catch (e) {}
|
|
341
|
-
},
|
|
342
|
-
);
|
|
343
|
-
|
|
344
|
-
try {
|
|
345
|
-
await fetchSSE('/', { onErrorHandle: mockOnErrorHandle });
|
|
346
|
-
} catch (e) {
|
|
347
|
-
expect(mockOnErrorHandle).toHaveBeenCalledWith({
|
|
348
|
-
body: undefined,
|
|
349
|
-
message: 'translated_response.SomeError',
|
|
350
|
-
type: 'SomeError',
|
|
351
|
-
});
|
|
352
|
-
}
|
|
353
|
-
});
|
|
354
|
-
|
|
355
103
|
it('should call onMessageHandle with full text if no message event', async () => {
|
|
356
104
|
const mockOnMessageHandle = vi.fn();
|
|
357
105
|
const mockOnFinish = vi.fn();
|
|
@@ -531,4 +279,166 @@ describe('fetchSSE', () => {
|
|
|
531
279
|
type: 'error',
|
|
532
280
|
});
|
|
533
281
|
});
|
|
282
|
+
|
|
283
|
+
describe('onAbort', () => {
|
|
284
|
+
it('should call onAbort when AbortError is thrown', async () => {
|
|
285
|
+
const mockOnAbort = vi.fn();
|
|
286
|
+
|
|
287
|
+
(fetchEventSource as any).mockImplementationOnce(
|
|
288
|
+
(url: string, options: FetchEventSourceInit) => {
|
|
289
|
+
options.onmessage!({ event: 'text', data: JSON.stringify('Hello') } as any);
|
|
290
|
+
options.onerror!({ name: 'AbortError' });
|
|
291
|
+
},
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
await fetchSSE('/', { onAbort: mockOnAbort, smoothing: false });
|
|
295
|
+
|
|
296
|
+
expect(mockOnAbort).toHaveBeenCalledWith('Hello');
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('should call onAbort when MESSAGE_CANCEL_FLAT is thrown', async () => {
|
|
300
|
+
const mockOnAbort = vi.fn();
|
|
301
|
+
|
|
302
|
+
(fetchEventSource as any).mockImplementationOnce(
|
|
303
|
+
(url: string, options: FetchEventSourceInit) => {
|
|
304
|
+
options.onmessage!({ event: 'text', data: JSON.stringify('Hello') } as any);
|
|
305
|
+
options.onerror!(MESSAGE_CANCEL_FLAT);
|
|
306
|
+
},
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
await fetchSSE('/', { onAbort: mockOnAbort, smoothing: false });
|
|
310
|
+
|
|
311
|
+
expect(mockOnAbort).toHaveBeenCalledWith('Hello');
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
describe('onErrorHandle', () => {
|
|
316
|
+
it('should call onErrorHandle when Chat Message error is thrown', async () => {
|
|
317
|
+
const mockOnErrorHandle = vi.fn();
|
|
318
|
+
const mockError: ChatMessageError = {
|
|
319
|
+
body: {},
|
|
320
|
+
message: 'StreamChunkError',
|
|
321
|
+
type: 'StreamChunkError',
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
(fetchEventSource as any).mockImplementationOnce(
|
|
325
|
+
(url: string, options: FetchEventSourceInit) => {
|
|
326
|
+
options.onerror!(mockError);
|
|
327
|
+
},
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
try {
|
|
331
|
+
await fetchSSE('/', { onErrorHandle: mockOnErrorHandle });
|
|
332
|
+
} catch (e) {}
|
|
333
|
+
|
|
334
|
+
expect(mockOnErrorHandle).toHaveBeenCalledWith(mockError);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it('should call onErrorHandle when Unknown error is thrown', async () => {
|
|
338
|
+
const mockOnErrorHandle = vi.fn();
|
|
339
|
+
const mockError = new Error('Unknown error');
|
|
340
|
+
|
|
341
|
+
(fetchEventSource as any).mockImplementationOnce(
|
|
342
|
+
(url: string, options: FetchEventSourceInit) => {
|
|
343
|
+
options.onerror!(mockError);
|
|
344
|
+
},
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
try {
|
|
348
|
+
await fetchSSE('/', { onErrorHandle: mockOnErrorHandle });
|
|
349
|
+
} catch (e) {}
|
|
350
|
+
|
|
351
|
+
expect(mockOnErrorHandle).toHaveBeenCalledWith({
|
|
352
|
+
type: 'UnknownChatFetchError',
|
|
353
|
+
message: 'Unknown error',
|
|
354
|
+
body: {
|
|
355
|
+
message: 'Unknown error',
|
|
356
|
+
name: 'Error',
|
|
357
|
+
stack: expect.any(String),
|
|
358
|
+
},
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it('should call onErrorHandle when response is not ok', async () => {
|
|
363
|
+
const mockOnErrorHandle = vi.fn();
|
|
364
|
+
|
|
365
|
+
(fetchEventSource as any).mockImplementationOnce(
|
|
366
|
+
async (url: string, options: FetchEventSourceInit) => {
|
|
367
|
+
const res = new Response(JSON.stringify({ errorType: 'SomeError' }), {
|
|
368
|
+
status: 400,
|
|
369
|
+
statusText: 'Error',
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
try {
|
|
373
|
+
await options.onopen!(res as any);
|
|
374
|
+
} catch (e) {}
|
|
375
|
+
},
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
try {
|
|
379
|
+
await fetchSSE('/', { onErrorHandle: mockOnErrorHandle });
|
|
380
|
+
} catch (e) {
|
|
381
|
+
expect(mockOnErrorHandle).toHaveBeenCalledWith({
|
|
382
|
+
body: undefined,
|
|
383
|
+
message: 'translated_response.SomeError',
|
|
384
|
+
type: 'SomeError',
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it('should call onErrorHandle when stream chunk has error type', async () => {
|
|
390
|
+
const mockOnErrorHandle = vi.fn();
|
|
391
|
+
const mockError = {
|
|
392
|
+
type: 'StreamChunkError',
|
|
393
|
+
message: 'abc',
|
|
394
|
+
body: { message: 'abc', context: {} },
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
(fetchEventSource as any).mockImplementationOnce(
|
|
398
|
+
(url: string, options: FetchEventSourceInit) => {
|
|
399
|
+
options.onmessage!({
|
|
400
|
+
event: 'error',
|
|
401
|
+
data: JSON.stringify(mockError),
|
|
402
|
+
} as any);
|
|
403
|
+
},
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
try {
|
|
407
|
+
await fetchSSE('/', { onErrorHandle: mockOnErrorHandle });
|
|
408
|
+
} catch (e) {}
|
|
409
|
+
|
|
410
|
+
expect(mockOnErrorHandle).toHaveBeenCalledWith(mockError);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it('should call onErrorHandle when stream chunk is not valid json', async () => {
|
|
414
|
+
const mockOnErrorHandle = vi.fn();
|
|
415
|
+
const mockError = 'abc';
|
|
416
|
+
|
|
417
|
+
(fetchEventSource as any).mockImplementationOnce(
|
|
418
|
+
(url: string, options: FetchEventSourceInit) => {
|
|
419
|
+
options.onmessage!({ event: 'text', data: mockError } as any);
|
|
420
|
+
},
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
try {
|
|
424
|
+
await fetchSSE('/', { onErrorHandle: mockOnErrorHandle });
|
|
425
|
+
} catch (e) {}
|
|
426
|
+
|
|
427
|
+
expect(mockOnErrorHandle).toHaveBeenCalledWith({
|
|
428
|
+
body: {
|
|
429
|
+
context: {
|
|
430
|
+
chunk: 'abc',
|
|
431
|
+
error: {
|
|
432
|
+
message: 'Unexpected token a in JSON at position 0',
|
|
433
|
+
name: 'SyntaxError',
|
|
434
|
+
},
|
|
435
|
+
},
|
|
436
|
+
message:
|
|
437
|
+
'chat response streaming chunk parse error, please contact your API Provider to fix it.',
|
|
438
|
+
},
|
|
439
|
+
message: 'parse error',
|
|
440
|
+
type: 'StreamChunkError',
|
|
441
|
+
});
|
|
442
|
+
});
|
|
443
|
+
});
|
|
534
444
|
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { ErrorResponse } from '@/types/fetch';
|
|
4
|
+
|
|
5
|
+
import { getMessageError } from '../parseError';
|
|
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
|
+
it('should handle timeout error correctly', async () => {
|
|
81
|
+
const mockResponse = createMockResponse(undefined, false, 504);
|
|
82
|
+
const error = await getMessageError(mockResponse as any);
|
|
83
|
+
|
|
84
|
+
expect(error).toEqual({
|
|
85
|
+
message: 'translated_response.504',
|
|
86
|
+
type: 504,
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { ZodError } from 'zod';
|
|
3
|
+
|
|
4
|
+
import { parseToolCalls } from '../parseToolCalls';
|
|
5
|
+
|
|
6
|
+
describe('parseToolCalls', () => {
|
|
7
|
+
it('should create add new item', () => {
|
|
8
|
+
const chunk = [
|
|
9
|
+
{ index: 0, id: '1', type: 'function', function: { name: 'func', arguments: '' } },
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
const result = parseToolCalls([], chunk);
|
|
13
|
+
expect(result).toEqual([
|
|
14
|
+
{ id: '1', type: 'function', function: { name: 'func', arguments: '' } },
|
|
15
|
+
]);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should update arguments if there is a toolCall', () => {
|
|
19
|
+
const origin = [{ id: '1', type: 'function', function: { name: 'func', arguments: '' } }];
|
|
20
|
+
|
|
21
|
+
const chunk1 = [{ index: 0, function: { arguments: '{"lo' } }];
|
|
22
|
+
|
|
23
|
+
const result1 = parseToolCalls(origin, chunk1);
|
|
24
|
+
expect(result1).toEqual([
|
|
25
|
+
{ id: '1', type: 'function', function: { name: 'func', arguments: '{"lo' } },
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
const chunk2 = [{ index: 0, function: { arguments: 'cation\\": \\"Hangzhou\\"}' } }];
|
|
29
|
+
const result2 = parseToolCalls(result1, chunk2);
|
|
30
|
+
|
|
31
|
+
expect(result2).toEqual([
|
|
32
|
+
{
|
|
33
|
+
id: '1',
|
|
34
|
+
type: 'function',
|
|
35
|
+
function: { name: 'func', arguments: '{"location\\": \\"Hangzhou\\"}' },
|
|
36
|
+
},
|
|
37
|
+
]);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should add a new tool call if the index is different', () => {
|
|
41
|
+
const origin = [
|
|
42
|
+
{
|
|
43
|
+
id: '1',
|
|
44
|
+
type: 'function',
|
|
45
|
+
function: { name: 'func', arguments: '{"location\\": \\"Hangzhou\\"}' },
|
|
46
|
+
},
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
const chunk = [
|
|
50
|
+
{
|
|
51
|
+
index: 1,
|
|
52
|
+
id: '2',
|
|
53
|
+
type: 'function',
|
|
54
|
+
function: { name: 'func', arguments: '' },
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
const result1 = parseToolCalls(origin, chunk);
|
|
59
|
+
expect(result1).toEqual([
|
|
60
|
+
{
|
|
61
|
+
id: '1',
|
|
62
|
+
type: 'function',
|
|
63
|
+
function: { name: 'func', arguments: '{"location\\": \\"Hangzhou\\"}' },
|
|
64
|
+
},
|
|
65
|
+
{ id: '2', type: 'function', function: { name: 'func', arguments: '' } },
|
|
66
|
+
]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should update correct arguments if there are multi tool calls', () => {
|
|
70
|
+
const origin = [
|
|
71
|
+
{
|
|
72
|
+
id: '1',
|
|
73
|
+
type: 'function',
|
|
74
|
+
function: { name: 'func', arguments: '{"location\\": \\"Hangzhou\\"}' },
|
|
75
|
+
},
|
|
76
|
+
{ id: '2', type: 'function', function: { name: 'func', arguments: '' } },
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
const chunk = [{ index: 1, function: { arguments: '{"location\\": \\"Beijing\\"}' } }];
|
|
80
|
+
|
|
81
|
+
const result1 = parseToolCalls(origin, chunk);
|
|
82
|
+
expect(result1).toEqual([
|
|
83
|
+
{
|
|
84
|
+
id: '1',
|
|
85
|
+
type: 'function',
|
|
86
|
+
function: { name: 'func', arguments: '{"location\\": \\"Hangzhou\\"}' },
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: '2',
|
|
90
|
+
type: 'function',
|
|
91
|
+
function: { name: 'func', arguments: '{"location\\": \\"Beijing\\"}' },
|
|
92
|
+
},
|
|
93
|
+
]);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should throw error if incomplete tool calls data', () => {
|
|
97
|
+
const origin = [
|
|
98
|
+
{
|
|
99
|
+
id: '1',
|
|
100
|
+
type: 'function',
|
|
101
|
+
function: { name: 'func', arguments: '{"location\\": \\"Hangzhou\\"}' },
|
|
102
|
+
},
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
const chunk = [{ index: 1, id: '2', type: 'function' }];
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
parseToolCalls(origin, chunk as any);
|
|
109
|
+
} catch (e) {
|
|
110
|
+
expect(e).toEqual(
|
|
111
|
+
new ZodError([
|
|
112
|
+
{
|
|
113
|
+
code: 'invalid_type',
|
|
114
|
+
expected: 'object',
|
|
115
|
+
received: 'undefined',
|
|
116
|
+
path: ['function'],
|
|
117
|
+
message: 'Required',
|
|
118
|
+
},
|
|
119
|
+
]),
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
});
|