@lobehub/chat 0.156.1 → 0.157.0

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.
Files changed (115) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/Dockerfile +4 -1
  3. package/package.json +3 -2
  4. package/src/config/modelProviders/anthropic.ts +3 -0
  5. package/src/config/modelProviders/google.ts +3 -0
  6. package/src/config/modelProviders/groq.ts +5 -1
  7. package/src/config/modelProviders/minimax.ts +10 -7
  8. package/src/config/modelProviders/mistral.ts +1 -0
  9. package/src/config/modelProviders/moonshot.ts +3 -0
  10. package/src/config/modelProviders/zhipu.ts +2 -6
  11. package/src/config/server/provider.ts +1 -1
  12. package/src/database/client/core/db.ts +32 -0
  13. package/src/database/client/core/schemas.ts +9 -0
  14. package/src/database/client/models/__tests__/message.test.ts +2 -2
  15. package/src/database/client/schemas/message.ts +8 -1
  16. package/src/features/AgentSetting/store/action.ts +15 -6
  17. package/src/features/Conversation/Actions/Tool.tsx +16 -0
  18. package/src/features/Conversation/Actions/index.ts +2 -2
  19. package/src/features/Conversation/Messages/Assistant/ToolCalls/index.tsx +78 -0
  20. package/src/features/Conversation/Messages/Assistant/ToolCalls/style.ts +25 -0
  21. package/src/features/Conversation/Messages/Assistant/index.tsx +47 -0
  22. package/src/features/Conversation/Messages/Default.tsx +4 -1
  23. package/src/features/Conversation/{Plugins → Messages/Tool}/Inspector/index.tsx +34 -35
  24. package/src/features/Conversation/Messages/Tool/index.tsx +44 -0
  25. package/src/features/Conversation/Messages/index.ts +3 -2
  26. package/src/features/Conversation/Plugins/Render/StandaloneType/Iframe.tsx +1 -1
  27. package/src/features/Conversation/components/SkeletonList.tsx +2 -2
  28. package/src/features/Conversation/index.tsx +2 -3
  29. package/src/libs/agent-runtime/BaseAI.ts +2 -9
  30. package/src/libs/agent-runtime/anthropic/index.test.ts +195 -0
  31. package/src/libs/agent-runtime/anthropic/index.ts +71 -15
  32. package/src/libs/agent-runtime/azureOpenai/index.ts +12 -13
  33. package/src/libs/agent-runtime/bedrock/index.ts +24 -18
  34. package/src/libs/agent-runtime/google/index.test.ts +154 -0
  35. package/src/libs/agent-runtime/google/index.ts +91 -10
  36. package/src/libs/agent-runtime/groq/index.test.ts +41 -72
  37. package/src/libs/agent-runtime/groq/index.ts +7 -0
  38. package/src/libs/agent-runtime/minimax/index.test.ts +2 -2
  39. package/src/libs/agent-runtime/minimax/index.ts +14 -37
  40. package/src/libs/agent-runtime/mistral/index.test.ts +0 -53
  41. package/src/libs/agent-runtime/mistral/index.ts +1 -0
  42. package/src/libs/agent-runtime/moonshot/index.test.ts +1 -71
  43. package/src/libs/agent-runtime/ollama/index.test.ts +197 -0
  44. package/src/libs/agent-runtime/ollama/index.ts +3 -3
  45. package/src/libs/agent-runtime/openai/index.test.ts +0 -53
  46. package/src/libs/agent-runtime/openrouter/index.test.ts +1 -53
  47. package/src/libs/agent-runtime/perplexity/index.test.ts +0 -71
  48. package/src/libs/agent-runtime/perplexity/index.ts +2 -3
  49. package/src/libs/agent-runtime/togetherai/__snapshots__/index.test.ts.snap +886 -0
  50. package/src/libs/agent-runtime/togetherai/fixtures/models.json +8111 -0
  51. package/src/libs/agent-runtime/togetherai/index.test.ts +16 -54
  52. package/src/libs/agent-runtime/types/chat.ts +19 -3
  53. package/src/libs/agent-runtime/utils/anthropicHelpers.test.ts +120 -1
  54. package/src/libs/agent-runtime/utils/anthropicHelpers.ts +67 -4
  55. package/src/libs/agent-runtime/utils/debugStream.test.ts +70 -0
  56. package/src/libs/agent-runtime/utils/debugStream.ts +39 -9
  57. package/src/libs/agent-runtime/utils/openaiCompatibleFactory/index.test.ts +521 -0
  58. package/src/libs/agent-runtime/utils/openaiCompatibleFactory/index.ts +76 -5
  59. package/src/libs/agent-runtime/utils/response.ts +12 -0
  60. package/src/libs/agent-runtime/utils/streams/anthropic.test.ts +197 -0
  61. package/src/libs/agent-runtime/utils/streams/anthropic.ts +91 -0
  62. package/src/libs/agent-runtime/utils/streams/bedrock/claude.ts +21 -0
  63. package/src/libs/agent-runtime/utils/streams/bedrock/common.ts +32 -0
  64. package/src/libs/agent-runtime/utils/streams/bedrock/index.ts +3 -0
  65. package/src/libs/agent-runtime/utils/streams/bedrock/llama.test.ts +196 -0
  66. package/src/libs/agent-runtime/utils/streams/bedrock/llama.ts +51 -0
  67. package/src/libs/agent-runtime/utils/streams/google-ai.test.ts +97 -0
  68. package/src/libs/agent-runtime/utils/streams/google-ai.ts +68 -0
  69. package/src/libs/agent-runtime/utils/streams/index.ts +7 -0
  70. package/src/libs/agent-runtime/utils/streams/minimax.ts +39 -0
  71. package/src/libs/agent-runtime/utils/streams/ollama.test.ts +77 -0
  72. package/src/libs/agent-runtime/utils/streams/ollama.ts +38 -0
  73. package/src/libs/agent-runtime/utils/streams/openai.test.ts +263 -0
  74. package/src/libs/agent-runtime/utils/streams/openai.ts +79 -0
  75. package/src/libs/agent-runtime/utils/streams/protocol.ts +100 -0
  76. package/src/libs/agent-runtime/zeroone/index.test.ts +1 -53
  77. package/src/libs/agent-runtime/zhipu/index.test.ts +1 -1
  78. package/src/libs/agent-runtime/zhipu/index.ts +3 -2
  79. package/src/locales/default/plugin.ts +3 -4
  80. package/src/migrations/FromV4ToV5/fixtures/from-v1-to-v5-output.json +245 -0
  81. package/src/migrations/FromV4ToV5/fixtures/function-input-v4.json +96 -0
  82. package/src/migrations/FromV4ToV5/fixtures/function-output-v5.json +120 -0
  83. package/src/migrations/FromV4ToV5/index.ts +58 -0
  84. package/src/migrations/FromV4ToV5/migrations.test.ts +49 -0
  85. package/src/migrations/FromV4ToV5/types/v4.ts +21 -0
  86. package/src/migrations/FromV4ToV5/types/v5.ts +27 -0
  87. package/src/migrations/index.ts +8 -1
  88. package/src/services/__tests__/chat.test.ts +10 -20
  89. package/src/services/chat.ts +78 -65
  90. package/src/store/chat/slices/enchance/action.ts +15 -10
  91. package/src/store/chat/slices/message/action.test.ts +36 -86
  92. package/src/store/chat/slices/message/action.ts +70 -79
  93. package/src/store/chat/slices/message/reducer.ts +18 -1
  94. package/src/store/chat/slices/message/selectors.test.ts +38 -68
  95. package/src/store/chat/slices/message/selectors.ts +1 -22
  96. package/src/store/chat/slices/plugin/action.test.ts +147 -203
  97. package/src/store/chat/slices/plugin/action.ts +96 -82
  98. package/src/store/chat/slices/share/action.test.ts +3 -3
  99. package/src/store/chat/slices/share/action.ts +1 -1
  100. package/src/store/chat/slices/topic/action.ts +7 -2
  101. package/src/store/tool/selectors/tool.ts +6 -24
  102. package/src/store/tool/slices/builtin/action.test.ts +90 -0
  103. package/src/types/llm.ts +1 -1
  104. package/src/types/message/index.ts +9 -4
  105. package/src/types/message/tools.ts +57 -0
  106. package/src/types/openai/chat.ts +6 -0
  107. package/src/utils/fetch.test.ts +245 -1
  108. package/src/utils/fetch.ts +120 -44
  109. package/src/utils/toolCall.ts +21 -0
  110. package/src/features/Conversation/Messages/Assistant.tsx +0 -26
  111. package/src/features/Conversation/Messages/Function.tsx +0 -35
  112. package/src/libs/agent-runtime/ollama/stream.ts +0 -31
  113. /package/src/features/Conversation/{Plugins → Messages/Tool}/Inspector/PluginResultJSON.tsx +0 -0
  114. /package/src/features/Conversation/{Plugins → Messages/Tool}/Inspector/Settings.tsx +0 -0
  115. /package/src/features/Conversation/{Plugins → Messages/Tool}/Inspector/style.ts +0 -0
@@ -0,0 +1,49 @@
1
+ import { describe } from 'vitest';
2
+
3
+ import { MigrationData, VersionController } from '@/migrations/VersionController';
4
+
5
+ import { MigrationV1ToV2 } from '../FromV1ToV2';
6
+ import inputV1Data from '../FromV1ToV2/fixtures/input-v1-session.json';
7
+ import { MigrationV2ToV3 } from '../FromV2ToV3';
8
+ import { MigrationV3ToV4 } from '../FromV3ToV4';
9
+ import outputDataFromV1ToV5 from './fixtures/from-v1-to-v5-output.json';
10
+ import functionInputV4 from './fixtures/function-input-v4.json';
11
+ import functionOutputV5 from './fixtures/function-output-v5.json';
12
+ import { MigrationV4ToV5 } from './index';
13
+
14
+ describe('MigrationV4ToV5', () => {
15
+ let migrations;
16
+ let versionController: VersionController<any>;
17
+
18
+ beforeEach(() => {
19
+ migrations = [MigrationV4ToV5];
20
+ versionController = new VersionController(migrations, 5);
21
+ });
22
+
23
+ describe('should migrate data correctly from previous versions', () => {
24
+ it('role=function', () => {
25
+ const data: MigrationData = functionInputV4;
26
+
27
+ const migratedData = versionController.migrate(data);
28
+
29
+ expect(migratedData.version).toEqual(functionOutputV5.version);
30
+ expect(migratedData.state.messages).toEqual(functionOutputV5.state.messages);
31
+ });
32
+ });
33
+
34
+ it('should work correct from v1 to v5', () => {
35
+ const data: MigrationData = inputV1Data;
36
+
37
+ versionController = new VersionController(
38
+ [MigrationV4ToV5, MigrationV3ToV4, MigrationV2ToV3, MigrationV1ToV2],
39
+ 5,
40
+ );
41
+
42
+ const migratedData = versionController.migrate(data);
43
+
44
+ expect(migratedData.version).toEqual(outputDataFromV1ToV5.version);
45
+ expect(migratedData.state.messages).toEqual(outputDataFromV1ToV5.state.messages);
46
+ expect(migratedData.state.sessions).toEqual(outputDataFromV1ToV5.state.sessions);
47
+ expect(migratedData.state.topics).toEqual(outputDataFromV1ToV5.state.topics);
48
+ });
49
+ });
@@ -0,0 +1,21 @@
1
+ import { LobeToolRenderType } from '@/types/tool';
2
+
3
+ export interface V4ChatPluginPayload {
4
+ apiName: string;
5
+ arguments: string;
6
+ identifier: string;
7
+ type: LobeToolRenderType;
8
+ }
9
+
10
+ export interface V4Message {
11
+ content: string;
12
+ createdAt: number;
13
+ id: string;
14
+ plugin?: V4ChatPluginPayload;
15
+ role: 'user' | 'system' | 'assistant' | 'function';
16
+ updatedAt: number;
17
+ }
18
+
19
+ export interface V4ConfigState {
20
+ messages: V4Message[];
21
+ }
@@ -0,0 +1,27 @@
1
+ import { LobeToolRenderType } from '@/types/tool';
2
+
3
+ import { V4ChatPluginPayload } from './v4';
4
+
5
+ interface ChatToolPayload {
6
+ apiName: string;
7
+ arguments: string;
8
+ id: string;
9
+ identifier: string;
10
+ type: LobeToolRenderType;
11
+ }
12
+
13
+ export interface V5Message {
14
+ content: string;
15
+ createdAt: number;
16
+ id: string;
17
+ parentId?: string;
18
+ plugin?: V4ChatPluginPayload;
19
+ role: 'user' | 'system' | 'assistant' | 'tool';
20
+ tool_call_id?: string;
21
+ tools?: ChatToolPayload[];
22
+ updatedAt: number;
23
+ }
24
+
25
+ export interface V5ConfigState {
26
+ messages: V5Message[];
27
+ }
@@ -5,12 +5,19 @@ import { ConfigStateAll } from '@/types/exportConfig';
5
5
  import { MigrationV0ToV1 } from './FromV0ToV1';
6
6
  import { MigrationV1ToV2 } from './FromV1ToV2';
7
7
  import { MigrationV3ToV4 } from './FromV3ToV4';
8
+ import { MigrationV4ToV5 } from './FromV4ToV5';
8
9
 
9
10
  // Current latest version
10
- export const CURRENT_CONFIG_VERSION = 4;
11
+ export const CURRENT_CONFIG_VERSION = 5;
11
12
 
12
13
  // Version migrations module
13
14
  const ConfigMigrations = [
15
+ /**
16
+ * 2024.05.11
17
+ *
18
+ * role=function to role=tool
19
+ */
20
+ MigrationV4ToV5,
14
21
  /**
15
22
  * 2024.04.09
16
23
  * settings migrate the `languageModel`
@@ -126,7 +126,7 @@ describe('ChatService', () => {
126
126
  it('should include image content when with vision model', async () => {
127
127
  const messages = [
128
128
  { content: 'Hello', role: 'user', files: ['file1'] }, // Message with files
129
- { content: 'Hi', role: 'function', plugin: { identifier: 'plugin1' } }, // Message with function role
129
+ { content: 'Hi', role: 'tool', plugin: { identifier: 'plugin1', apiName: 'api1' } }, // Message with tool role
130
130
  { content: 'Hey', role: 'assistant' }, // Regular user message
131
131
  ] as ChatMessage[];
132
132
 
@@ -166,8 +166,8 @@ describe('ChatService', () => {
166
166
  },
167
167
  {
168
168
  content: 'Hi',
169
- name: 'plugin1',
170
- role: 'function',
169
+ name: 'plugin1____api1',
170
+ role: 'tool',
171
171
  },
172
172
  {
173
173
  content: 'Hey',
@@ -183,7 +183,7 @@ describe('ChatService', () => {
183
183
  it('should not include image content when default model', async () => {
184
184
  const messages = [
185
185
  { content: 'Hello', role: 'user', files: ['file1'] }, // Message with files
186
- { content: 'Hi', role: 'function', plugin: { identifier: 'plugin1' } }, // Message with function role
186
+ { content: 'Hi', role: 'tool', plugin: { identifier: 'plugin1', apiName: 'api1' } }, // Message with function role
187
187
  { content: 'Hey', role: 'assistant' }, // Regular user message
188
188
  ] as ChatMessage[];
189
189
 
@@ -212,7 +212,7 @@ describe('ChatService', () => {
212
212
  {
213
213
  messages: [
214
214
  { content: 'Hello', role: 'user' },
215
- { content: 'Hi', name: 'plugin1', role: 'function' },
215
+ { content: 'Hi', name: 'plugin1____api1', role: 'tool' },
216
216
  { content: 'Hey', role: 'assistant' },
217
217
  ],
218
218
  model: 'gpt-3.5-turbo',
@@ -224,7 +224,7 @@ describe('ChatService', () => {
224
224
  it('should not include image with vision models when can not find the image', async () => {
225
225
  const messages = [
226
226
  { content: 'Hello', role: 'user', files: ['file2'] }, // Message with files
227
- { content: 'Hi', role: 'function', plugin: { identifier: 'plugin1' } }, // Message with function role
227
+ { content: 'Hi', role: 'tool', plugin: { identifier: 'plugin1', apiName: 'api1' } }, // Message with function role
228
228
  { content: 'Hey', role: 'assistant' }, // Regular user message
229
229
  ] as ChatMessage[];
230
230
 
@@ -248,19 +248,9 @@ describe('ChatService', () => {
248
248
  expect(getChatCompletionSpy).toHaveBeenCalledWith(
249
249
  {
250
250
  messages: [
251
- {
252
- content: 'Hello',
253
- role: 'user',
254
- },
255
- {
256
- content: 'Hi',
257
- name: 'plugin1',
258
- role: 'function',
259
- },
260
- {
261
- content: 'Hey',
262
- role: 'assistant',
263
- },
251
+ { content: 'Hello', role: 'user' },
252
+ { content: 'Hi', name: 'plugin1____api1', role: 'tool' },
253
+ { content: 'Hey', role: 'assistant' },
264
254
  ],
265
255
  },
266
256
  undefined,
@@ -580,7 +570,7 @@ Get data from users`,
580
570
  body: JSON.stringify(expectedPayload),
581
571
  headers: expect.any(Object),
582
572
  method: 'POST',
583
- signal: undefined,
573
+ signal: expect.any(AbortSignal),
584
574
  });
585
575
  });
586
576
 
@@ -21,16 +21,17 @@ import {
21
21
  userProfileSelectors,
22
22
  } from '@/store/user/selectors';
23
23
  import { ChatErrorType } from '@/types/fetch';
24
- import { ChatMessage } from '@/types/message';
24
+ import { ChatMessage, MessageToolCall } from '@/types/message';
25
25
  import type { ChatStreamPayload, OpenAIChatMessage } from '@/types/openai/chat';
26
26
  import { UserMessageContentPart } from '@/types/openai/chat';
27
- import { FetchSSEOptions, OnFinishHandler, fetchSSE, getMessageError } from '@/utils/fetch';
27
+ import { FetchSSEOptions, fetchSSE, getMessageError } from '@/utils/fetch';
28
+ import { genToolCallingName } from '@/utils/toolCall';
28
29
  import { createTraceHeader, getTraceId } from '@/utils/trace';
29
30
 
30
31
  import { createHeaderWithAuth, getProviderAuthPayload } from './_auth';
31
32
  import { API_ENDPOINTS } from './_url';
32
33
 
33
- interface FetchOptions {
34
+ interface FetchOptions extends FetchSSEOptions {
34
35
  isWelcomeQuestion?: boolean;
35
36
  signal?: AbortSignal | undefined;
36
37
  trace?: TracePayload;
@@ -40,23 +41,14 @@ interface GetChatCompletionPayload extends Partial<Omit<ChatStreamPayload, 'mess
40
41
  messages: ChatMessage[];
41
42
  }
42
43
 
43
- interface FetchAITaskResultParams {
44
+ interface FetchAITaskResultParams extends FetchSSEOptions {
44
45
  abortController?: AbortController;
45
- /**
46
- * 错误处理函数
47
- */
48
46
  onError?: (e: Error, rawError?: any) => void;
49
- onFinish?: OnFinishHandler;
50
47
  /**
51
48
  * 加载状态变化处理函数
52
49
  * @param loading - 是否处于加载状态
53
50
  */
54
51
  onLoadingChange?: (loading: boolean) => void;
55
- /**
56
- * 消息处理函数
57
- * @param text - 消息内容
58
- */
59
- onMessageHandle?: (text: string) => void;
60
52
  /**
61
53
  * 请求对象
62
54
  */
@@ -224,20 +216,15 @@ class ChatService {
224
216
  trace,
225
217
  isWelcomeQuestion,
226
218
  }: CreateAssistantMessageStream) => {
227
- await fetchSSE(
228
- () =>
229
- this.createAssistantMessage(params, {
230
- isWelcomeQuestion,
231
- signal: abortController?.signal,
232
- trace: this.mapTrace(trace, TraceTagMap.Chat),
233
- }),
234
- {
235
- onAbort,
236
- onErrorHandle,
237
- onFinish,
238
- onMessageHandle,
239
- },
240
- );
219
+ await this.createAssistantMessage(params, {
220
+ isWelcomeQuestion,
221
+ onAbort,
222
+ onErrorHandle,
223
+ onFinish,
224
+ onMessageHandle,
225
+ signal: abortController?.signal,
226
+ trace: this.mapTrace(trace, TraceTagMap.Chat),
227
+ });
241
228
  };
242
229
 
243
230
  getChatCompletion = async (params: Partial<ChatStreamPayload>, options?: FetchOptions) => {
@@ -268,28 +255,33 @@ class ChatService {
268
255
  const enableFetchOnClient = modelConfigSelectors.isProviderFetchOnClient(provider)(
269
256
  useUserStore.getState(),
270
257
  );
271
- /**
272
- * Notes:
273
- * 1. Broswer agent runtime will skip auth check if a key and endpoint provided by
274
- * user which will cause abuse of plugins services
275
- * 2. This feature will disabled by default
276
- */
258
+
259
+ let fetcher: typeof fetch | undefined = undefined;
260
+
277
261
  if (enableFetchOnClient) {
278
- try {
279
- return await this.fetchOnClient({ payload, provider, signal });
280
- } catch (e) {
281
- const {
282
- errorType = ChatErrorType.BadRequest,
283
- error: errorContent,
284
- ...res
285
- } = e as ChatCompletionErrorPayload;
286
-
287
- const error = errorContent || e;
288
- // track the error at server side
289
- console.error(`Route: [${provider}] ${errorType}:`, error);
290
-
291
- return createErrorResponse(errorType, { error, ...res, provider });
292
- }
262
+ /**
263
+ * Notes:
264
+ * 1. Browser agent runtime will skip auth check if a key and endpoint provided by
265
+ * user which will cause abuse of plugins services
266
+ * 2. This feature will be disabled by default
267
+ */
268
+ fetcher = async () => {
269
+ try {
270
+ return await this.fetchOnClient({ payload, provider, signal });
271
+ } catch (e) {
272
+ const {
273
+ errorType = ChatErrorType.BadRequest,
274
+ error: errorContent,
275
+ ...res
276
+ } = e as ChatCompletionErrorPayload;
277
+
278
+ const error = errorContent || e;
279
+ // track the error at server side
280
+ console.error(`Route: [${provider}] ${errorType}:`, error);
281
+
282
+ return createErrorResponse(errorType, { error, ...res, provider });
283
+ }
284
+ };
293
285
  }
294
286
 
295
287
  const traceHeader = createTraceHeader({ ...options?.trace });
@@ -299,10 +291,15 @@ class ChatService {
299
291
  provider,
300
292
  });
301
293
 
302
- return fetch(API_ENDPOINTS.chat(provider), {
294
+ return fetchSSE(API_ENDPOINTS.chat(provider), {
303
295
  body: JSON.stringify(payload),
296
+ fetcher: fetcher,
304
297
  headers,
305
298
  method: 'POST',
299
+ onAbort: options?.onAbort,
300
+ onErrorHandle: options?.onErrorHandle,
301
+ onFinish: options?.onFinish,
302
+ onMessageHandle: options?.onMessageHandle,
306
303
  signal,
307
304
  });
308
305
  };
@@ -360,20 +357,15 @@ class ChatService {
360
357
 
361
358
  onLoadingChange?.(true);
362
359
 
363
- const data = await fetchSSE(
364
- () =>
365
- this.getChatCompletion(params, {
366
- signal: abortController?.signal,
367
- trace: this.mapTrace(trace, TraceTagMap.SystemChain),
368
- }),
369
- {
370
- onErrorHandle: (error) => {
371
- errorHandle(new Error(error.message), error);
372
- },
373
- onFinish,
374
- onMessageHandle,
360
+ const data = await this.getChatCompletion(params, {
361
+ onErrorHandle: (error) => {
362
+ errorHandle(new Error(error.message), error);
375
363
  },
376
- ).catch(errorHandle);
364
+ onFinish,
365
+ onMessageHandle,
366
+ signal: abortController?.signal,
367
+ trace: this.mapTrace(trace, TraceTagMap.SystemChain),
368
+ }).catch(errorHandle);
377
369
 
378
370
  onLoadingChange?.(false);
379
371
 
@@ -424,9 +416,30 @@ class ChatService {
424
416
  return { content: getContent(m), role: m.role };
425
417
  }
426
418
 
427
- case 'function': {
428
- const name = m.plugin?.identifier as string;
429
- return { content: m.content, name, role: m.role };
419
+ case 'assistant': {
420
+ return {
421
+ content: m.content,
422
+ role: m.role,
423
+ tool_calls: m.tools?.map(
424
+ (tool): MessageToolCall => ({
425
+ function: {
426
+ arguments: tool.arguments,
427
+ name: genToolCallingName(tool.identifier, tool.apiName, tool.type),
428
+ },
429
+ id: tool.id,
430
+ type: 'function',
431
+ }),
432
+ ),
433
+ };
434
+ }
435
+
436
+ case 'tool': {
437
+ return {
438
+ content: m.content,
439
+ name: genToolCallingName(m.plugin!.identifier, m.plugin!.apiName, m.plugin?.type),
440
+ role: m.role,
441
+ tool_call_id: m.tool_call_id,
442
+ };
430
443
  }
431
444
 
432
445
  default: {
@@ -76,16 +76,21 @@ export const chatEnhance: StateCreator<
76
76
 
77
77
  // translate to target language
78
78
  await chatService.fetchPresetTaskResult({
79
- onMessageHandle: (text) => {
80
- internal_dispatchMessage({
81
- id,
82
- key: 'translate',
83
- type: 'updateMessageExtra',
84
- value: produce({ content: '', from, to: targetLang }, (draft) => {
85
- content += text;
86
- draft.content += content;
87
- }),
88
- });
79
+ onMessageHandle: (chunk) => {
80
+ switch (chunk.type) {
81
+ case 'text': {
82
+ internal_dispatchMessage({
83
+ id,
84
+ key: 'translate',
85
+ type: 'updateMessageExtra',
86
+ value: produce({ content: '', from, to: targetLang }, (draft) => {
87
+ content += chunk.text;
88
+ draft.content += content;
89
+ }),
90
+ });
91
+ break;
92
+ }
93
+ }
89
94
  },
90
95
  params: chainTranslate(message.content, targetLang),
91
96
  trace: get().getCurrentTracePayload({ traceName: TraceNameMap.Translator }),
@@ -19,6 +19,7 @@ vi.stubGlobal(
19
19
  vi.fn(() => Promise.resolve(new Response('mock'))),
20
20
  );
21
21
 
22
+ vi.mock('zustand/traditional');
22
23
  // Mock service
23
24
  vi.mock('@/services/message', () => ({
24
25
  messageService: {
@@ -47,12 +48,6 @@ vi.mock('@/services/chat', async (importOriginal) => {
47
48
  };
48
49
  });
49
50
 
50
- vi.mock('@/store/chat/selectors', () => ({
51
- chatSelectors: {
52
- currentChats: vi.fn(),
53
- },
54
- }));
55
-
56
51
  const realCoreProcessMessage = useChatStore.getState().internal_coreProcessMessage;
57
52
  const realRefreshMessages = useChatStore.getState().refreshMessages;
58
53
  // Mock state
@@ -86,6 +81,9 @@ describe('chatMessage actions', () => {
86
81
  const messageId = 'message-id';
87
82
  const deleteSpy = vi.spyOn(result.current, 'deleteMessage');
88
83
 
84
+ act(() => {
85
+ useChatStore.setState({ messages: [{ id: messageId } as ChatMessage] });
86
+ });
89
87
  await act(async () => {
90
88
  await result.current.deleteMessage(messageId);
91
89
  });
@@ -259,11 +257,6 @@ describe('chatMessage actions', () => {
259
257
  enableAutoCreateTopic,
260
258
  }));
261
259
 
262
- // Mock the currentChats selector to return a list that does not reach the threshold
263
- (chatSelectors.currentChats as Mock).mockReturnValue(
264
- Array.from({ length: autoCreateTopicThreshold + 1 }, (_, i) => ({ id: `msg-${i}` })),
265
- );
266
-
267
260
  // Mock saveToTopic and switchTopic to simulate not being called
268
261
  const saveToTopicMock = vi.fn();
269
262
  const switchTopicMock = vi.fn();
@@ -271,6 +264,10 @@ describe('chatMessage actions', () => {
271
264
  await act(async () => {
272
265
  useChatStore.setState({
273
266
  ...mockState,
267
+ // Mock the currentChats selector to return a list that does not reach the threshold
268
+ messages: Array.from({ length: autoCreateTopicThreshold + 1 }, (_, i) => ({
269
+ id: `msg-${i}`,
270
+ })) as any,
274
271
  activeTopicId: undefined,
275
272
  saveToTopic: saveToTopicMock,
276
273
  switchTopic: switchTopicMock,
@@ -298,11 +295,6 @@ describe('chatMessage actions', () => {
298
295
  // Mock messageService.create to resolve with a message id
299
296
  (messageService.createMessage as Mock).mockResolvedValue('new-message-id');
300
297
 
301
- // Mock the currentChats selector to return a list that reaches the threshold
302
- (chatSelectors.currentChats as Mock).mockReturnValue(
303
- Array.from({ length: autoCreateTopicThreshold }, (_, i) => ({ id: `msg-${i}` })),
304
- );
305
-
306
298
  // Mock saveToTopic to resolve with a topic id and switchTopic to switch to the new topic
307
299
  const saveToTopicMock = vi.fn(() => Promise.resolve('new-topic-id'));
308
300
  const switchTopicMock = vi.fn();
@@ -310,6 +302,9 @@ describe('chatMessage actions', () => {
310
302
  act(() => {
311
303
  useChatStore.setState({
312
304
  ...mockState,
305
+ messages: Array.from({ length: autoCreateTopicThreshold }, (_, i) => ({
306
+ id: `msg-${i}`,
307
+ })) as any,
313
308
  activeTopicId: undefined,
314
309
  saveToTopic: saveToTopicMock,
315
310
  switchTopic: switchTopicMock,
@@ -339,11 +334,6 @@ describe('chatMessage actions', () => {
339
334
  enableAutoCreateTopic,
340
335
  }));
341
336
 
342
- // Mock the currentChats selector to return a list that does not reach the threshold
343
- (chatSelectors.currentChats as Mock).mockReturnValue(
344
- Array.from({ length: autoCreateTopicThreshold - 1 }, (_, i) => ({ id: `msg-${i}` })),
345
- );
346
-
347
337
  // Mock saveToTopic and switchTopic to simulate not being called
348
338
  const saveToTopicMock = vi.fn();
349
339
  const switchTopicMock = vi.fn();
@@ -351,6 +341,10 @@ describe('chatMessage actions', () => {
351
341
  await act(async () => {
352
342
  useChatStore.setState({
353
343
  ...mockState,
344
+ // Mock the currentChats selector to return a list that does not reach the threshold
345
+ messages: Array.from({ length: autoCreateTopicThreshold - 2 }, (_, i) => ({
346
+ id: `msg-${i}`,
347
+ })) as any,
354
348
  activeTopicId: undefined,
355
349
  saveToTopic: saveToTopicMock,
356
350
  switchTopic: switchTopicMock,
@@ -395,12 +389,14 @@ describe('chatMessage actions', () => {
395
389
  const { result } = renderHook(() => useChatStore());
396
390
  const messageId = 'message-id';
397
391
 
398
- // Mock the currentChats selector to return a list that includes the message to be resent
399
- (chatSelectors.currentChats as Mock).mockReturnValue([
400
- // ... other messages
401
- { id: messageId, role: 'user', content: 'Resend this message' },
402
- // ... other messages
403
- ]);
392
+ act(() => {
393
+ useChatStore.setState({
394
+ // Mock the currentChats selector to return a list that includes the message to be resent
395
+ messages: [
396
+ { id: messageId, role: 'user', content: 'Resend this message' } as ChatMessage,
397
+ ],
398
+ });
399
+ });
404
400
 
405
401
  // Mock the internal_coreProcessMessage function to resolve immediately
406
402
  mockState.internal_coreProcessMessage.mockResolvedValue(undefined);
@@ -421,10 +417,12 @@ describe('chatMessage actions', () => {
421
417
  const { result } = renderHook(() => useChatStore());
422
418
  const messageId = 'non-existing-message-id';
423
419
 
424
- // Mock the currentChats selector to return a list that does not include the message to be resent
425
- (chatSelectors.currentChats as Mock).mockReturnValue([
426
- // ... other messages
427
- ]);
420
+ act(() => {
421
+ useChatStore.setState({
422
+ // Mock the currentChats selector to return a list that does not include the message to be resent
423
+ messages: [],
424
+ });
425
+ });
428
426
 
429
427
  await act(async () => {
430
428
  await result.current.internal_resendMessage(messageId);
@@ -461,9 +459,8 @@ describe('chatMessage actions', () => {
461
459
 
462
460
  expect(internal_dispatchMessageSpy).toHaveBeenCalledWith({
463
461
  id: messageId,
464
- key: 'content',
465
- type: 'updateMessage',
466
- value: newContent,
462
+ type: 'updateMessages',
463
+ value: { content: newContent },
467
464
  });
468
465
  });
469
466
 
@@ -645,56 +642,7 @@ describe('chatMessage actions', () => {
645
642
  messages,
646
643
  assistantMessageId,
647
644
  );
648
- expect(response.content).toEqual(aiResponse);
649
645
  expect(response.isFunctionCall).toEqual(false);
650
- expect(response.functionCallAtEnd).toEqual(false);
651
- expect(response.functionCallContent).toEqual('');
652
- });
653
- });
654
-
655
- it('should handle function call message at start of AI response', async () => {
656
- const { result } = renderHook(() => useChatStore());
657
- const messages = [{ id: 'message-id', content: 'Hello', role: 'user' }] as ChatMessage[];
658
- const assistantMessageId = 'assistant-message-id';
659
- const aiResponse =
660
- '{"tool_calls":[{"id":"call_sbca","type":"function","function":{"name":"pluginName____apiName","arguments":{"key":"value"}}}]}';
661
-
662
- // Mock fetch to resolve with AI response containing function call
663
- vi.mocked(fetch).mockResolvedValueOnce(new Response(aiResponse));
664
-
665
- await act(async () => {
666
- const response = await result.current.internal_fetchAIChatMessage(
667
- messages,
668
- assistantMessageId,
669
- );
670
- expect(response.content).toEqual(aiResponse);
671
- expect(response.isFunctionCall).toEqual(true);
672
- expect(response.functionCallAtEnd).toEqual(false);
673
- expect(response.functionCallContent).toEqual('');
674
- });
675
- });
676
-
677
- it('should handle function message at end of AI response', async () => {
678
- const { result } = renderHook(() => useChatStore());
679
- const messages = [{ id: 'message-id', content: 'Hello', role: 'user' }] as ChatMessage[];
680
- const assistantMessageId = 'assistant-message-id';
681
- const aiResponse =
682
- 'Hello, human! {"tool_calls":[{"id":"call_sbca","type":"function","function":{"name":"pluginName____apiName","arguments":{"key":"value"}}}]}';
683
-
684
- // Mock fetch to resolve with AI response containing function call at end
685
- vi.mocked(fetch).mockResolvedValue(new Response(aiResponse));
686
-
687
- await act(async () => {
688
- const response = await result.current.internal_fetchAIChatMessage(
689
- messages,
690
- assistantMessageId,
691
- );
692
- expect(response.content).toEqual(aiResponse);
693
- expect(response.isFunctionCall).toEqual(true);
694
- expect(response.functionCallAtEnd).toEqual(true);
695
- expect(response.functionCallContent).toEqual(
696
- '{"tool_calls":[{"id":"call_sbca","type":"function","function":{"name":"pluginName____apiName","arguments":{"key":"value"}}}]}',
697
- );
698
646
  });
699
647
  });
700
648
 
@@ -708,9 +656,11 @@ describe('chatMessage actions', () => {
708
656
  vi.mocked(fetch).mockRejectedValue(new Error(errorMessage));
709
657
 
710
658
  await act(async () => {
711
- await expect(
712
- result.current.internal_fetchAIChatMessage(messages, assistantMessageId),
713
- ).rejects.toThrow(errorMessage);
659
+ expect(
660
+ await result.current.internal_fetchAIChatMessage(messages, assistantMessageId),
661
+ ).toEqual({
662
+ isFunctionCall: false,
663
+ });
714
664
  });
715
665
  });
716
666
  });