@lobehub/chat 1.0.12 → 1.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ### [Version 1.0.13](https://github.com/lobehub/lobe-chat/compare/v1.0.12...v1.0.13)
6
+
7
+ <sup>Released on **2024-06-19**</sup>
8
+
9
+ #### 🐛 Bug Fixes
10
+
11
+ - **misc**: Fix and improve tool calling.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### What's fixed
19
+
20
+ - **misc**: Fix and improve tool calling, closes [#2941](https://github.com/lobehub/lobe-chat/issues/2941) ([ea06536](https://github.com/lobehub/lobe-chat/commit/ea06536))
21
+
22
+ </details>
23
+
24
+ <div align="right">
25
+
26
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
27
+
28
+ </div>
29
+
5
30
  ### [Version 1.0.12](https://github.com/lobehub/lobe-chat/compare/v1.0.11...v1.0.12)
6
31
 
7
32
  <sup>Released on **2024-06-19**</sup>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "1.0.12",
3
+ "version": "1.0.13",
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",
@@ -92,7 +92,7 @@
92
92
  },
93
93
  "dependencies": {
94
94
  "@ant-design/icons": "^5.3.7",
95
- "@anthropic-ai/sdk": "^0.21.0",
95
+ "@anthropic-ai/sdk": "^0.23.0",
96
96
  "@auth/core": "0.28.0",
97
97
  "@aws-sdk/client-bedrock-runtime": "^3.583.0",
98
98
  "@aws-sdk/client-s3": "^3.583.0",
@@ -109,7 +109,7 @@
109
109
  "@lobehub/chat-plugins-gateway": "^1.9.0",
110
110
  "@lobehub/icons": "^1.23.0",
111
111
  "@lobehub/tts": "^1.24.1",
112
- "@lobehub/ui": "^1.143.3",
112
+ "@lobehub/ui": "^1.143.5",
113
113
  "@microsoft/fetch-event-source": "^2.0.1",
114
114
  "@neondatabase/serverless": "^0.9.3",
115
115
  "@next/third-parties": "^14.2.3",
@@ -154,6 +154,7 @@
154
154
  "nuqs": "^1.17.4",
155
155
  "ollama": "^0.5.1",
156
156
  "openai": "^4.47.1",
157
+ "partial-json": "^0.1.7",
157
158
  "pg": "^8.11.5",
158
159
  "pino": "^9.1.0",
159
160
  "polished": "^4.3.1",
@@ -5,7 +5,7 @@ import { Flexbox } from 'react-layout-kit';
5
5
  import { ChatMessageError } from '@/types/message';
6
6
 
7
7
  interface ErrorJSONViewerProps {
8
- error?: ChatMessageError;
8
+ error?: ChatMessageError | null;
9
9
  id: string;
10
10
  }
11
11
 
@@ -1,4 +1,4 @@
1
- import { Avatar, Highlighter, Icon } from '@lobehub/ui';
1
+ import { Avatar, Icon } from '@lobehub/ui';
2
2
  import isEqual from 'fast-deep-equal';
3
3
  import { Loader2, LucideChevronDown, LucideChevronRight, LucideToyBrick } from 'lucide-react';
4
4
  import { CSSProperties, memo, useState } from 'react';
@@ -10,6 +10,7 @@ import { chatSelectors } from '@/store/chat/selectors';
10
10
  import { pluginHelpers, useToolStore } from '@/store/tool';
11
11
  import { toolSelectors } from '@/store/tool/selectors';
12
12
 
13
+ import Arguments from '../../components/Arguments';
13
14
  import { useStyles } from './style';
14
15
 
15
16
  export interface InspectorProps {
@@ -64,7 +65,7 @@ const CallItem = memo<InspectorProps>(
64
65
  </Flexbox>
65
66
  <Icon icon={open ? LucideChevronDown : LucideChevronRight} />
66
67
  </Flexbox>
67
- {(open || loading) && <Highlighter language={'json'}>{requestArgs}</Highlighter>}
68
+ {(open || loading) && <Arguments arguments={requestArgs} />}
68
69
  </Flexbox>
69
70
  );
70
71
  },
@@ -1,4 +1,3 @@
1
- import { Snippet } from '@lobehub/ui';
2
1
  import { memo, useState } from 'react';
3
2
  import { Flexbox } from 'react-layout-kit';
4
3
 
@@ -7,10 +6,11 @@ import { chatSelectors } from '@/store/chat/selectors';
7
6
  import { ChatMessage } from '@/types/message';
8
7
 
9
8
  import PluginRender from '../../Plugins/Render';
9
+ import Arguments from '../components/Arguments';
10
10
  import Inspector from './Inspector';
11
11
 
12
12
  export const ToolMessage = memo<ChatMessage>(({ id, content, plugin }) => {
13
- const loading = useChatStore(chatSelectors.isMessageGenerating(id));
13
+ const loading = useChatStore(chatSelectors.isPluginApiInvoking(id));
14
14
 
15
15
  const [showRender, setShow] = useState(plugin?.type !== 'default');
16
16
 
@@ -35,9 +35,7 @@ export const ToolMessage = memo<ChatMessage>(({ id, content, plugin }) => {
35
35
  type={plugin?.type}
36
36
  />
37
37
  ) : (
38
- <Flexbox>
39
- <Snippet>{plugin?.arguments || ''}</Snippet>
40
- </Flexbox>
38
+ <Arguments arguments={plugin?.arguments} />
41
39
  )}
42
40
  </Flexbox>
43
41
  );
@@ -0,0 +1,20 @@
1
+ import { Highlighter } from '@lobehub/ui';
2
+ import { memo } from 'react';
3
+
4
+ import { useYamlArguments } from '../hooks/useYamlArguments';
5
+
6
+ export interface ArgumentsProps {
7
+ arguments?: string;
8
+ }
9
+
10
+ const Arguments = memo<ArgumentsProps>(({ arguments: args = '' }) => {
11
+ const yaml = useYamlArguments(args);
12
+
13
+ return (
14
+ <Highlighter language={'yaml'} showLanguage={false}>
15
+ {yaml}
16
+ </Highlighter>
17
+ );
18
+ });
19
+
20
+ export default Arguments;
@@ -0,0 +1,11 @@
1
+ import { Allow, parse } from 'partial-json';
2
+ import { stringify } from 'yaml';
3
+
4
+ export const useYamlArguments = (args: string) => {
5
+ try {
6
+ const obj = parse(args, Allow.OBJ);
7
+ return stringify(obj);
8
+ } catch {
9
+ return args;
10
+ }
11
+ };
@@ -22,10 +22,6 @@ beforeEach(() => {
22
22
 
23
23
  // 使用 vi.spyOn 来模拟 chat.completions.create 方法
24
24
  vi.spyOn(instance['client'].messages, 'create').mockReturnValue(new ReadableStream() as any);
25
-
26
- vi.spyOn(instance['client'].beta.tools.messages, 'create').mockReturnValue({
27
- content: [],
28
- } as any);
29
25
  });
30
26
 
31
27
  afterEach(() => {
@@ -260,35 +256,9 @@ describe('LobeAnthropicAI', () => {
260
256
  });
261
257
 
262
258
  // Assert
263
- expect(instance['client'].beta.tools.messages.create).toHaveBeenCalled();
259
+ expect(instance['client'].messages.create).toHaveBeenCalled();
264
260
  expect(spyOn).toHaveBeenCalledWith(tools);
265
261
  });
266
-
267
- it('should handle text and tool_use content correctly in transformResponseToStream', async () => {
268
- // Arrange
269
- const mockResponse = {
270
- content: [
271
- { type: 'text', text: 'Hello' },
272
- { type: 'tool_use', id: 'tool1', name: 'tool1', input: 'input1' },
273
- ],
274
- };
275
- // @ts-ignore
276
- vi.spyOn(instance, 'transformResponseToStream').mockReturnValue(new ReadableStream());
277
- vi.spyOn(instance['client'].beta.tools.messages, 'create').mockResolvedValue(
278
- mockResponse as any,
279
- );
280
-
281
- // Act
282
- await instance.chat({
283
- messages: [{ content: 'Hello', role: 'user' }],
284
- model: 'claude-3-haiku-20240307',
285
- temperature: 0,
286
- tools: [{ function: { name: 'tool1', description: 'desc1' }, type: 'function' }],
287
- });
288
-
289
- // Assert
290
- expect(instance['transformResponseToStream']).toHaveBeenCalledWith(mockResponse);
291
- });
292
262
  });
293
263
 
294
264
  describe('Error', () => {
@@ -30,41 +30,20 @@ export class LobeAnthropicAI implements LobeRuntimeAI {
30
30
  async chat(payload: ChatStreamPayload, options?: ChatCompetitionOptions) {
31
31
  try {
32
32
  const anthropicPayload = this.buildAnthropicPayload(payload);
33
-
34
- // if there is no tool, we can use the normal chat API
35
- if (!anthropicPayload.tools || anthropicPayload.tools.length === 0) {
36
- const response = await this.client.messages.create(
37
- { ...anthropicPayload, stream: true },
38
- {
39
- signal: options?.signal,
40
- },
41
- );
42
-
43
- const [prod, debug] = response.tee();
44
-
45
- if (process.env.DEBUG_ANTHROPIC_CHAT_COMPLETION === '1') {
46
- debugStream(debug.toReadableStream()).catch(console.error);
47
- }
48
-
49
- return StreamingResponse(AnthropicStream(prod, options?.callback), {
50
- headers: options?.headers,
51
- });
52
- }
53
-
54
- // or we should call the tool API
55
- const response = await this.client.beta.tools.messages.create(
56
- { ...anthropicPayload, stream: false },
57
- { signal: options?.signal },
33
+ const response = await this.client.messages.create(
34
+ { ...anthropicPayload, stream: true },
35
+ {
36
+ signal: options?.signal,
37
+ },
58
38
  );
59
39
 
40
+ const [prod, debug] = response.tee();
41
+
60
42
  if (process.env.DEBUG_ANTHROPIC_CHAT_COMPLETION === '1') {
61
- console.log('\n[no stream response]\n');
62
- console.log(JSON.stringify(response) + '\n');
43
+ debugStream(debug.toReadableStream()).catch(console.error);
63
44
  }
64
45
 
65
- const stream = this.transformResponseToStream(response);
66
-
67
- return StreamingResponse(AnthropicStream(stream, options?.callback), {
46
+ return StreamingResponse(AnthropicStream(prod, options?.callback), {
68
47
  headers: options?.headers,
69
48
  });
70
49
  } catch (error) {
@@ -118,43 +97,10 @@ export class LobeAnthropicAI implements LobeRuntimeAI {
118
97
  model,
119
98
  system: system_message?.content as string,
120
99
  temperature,
121
- // TODO: Anthropic sdk don't have tools interface currently
122
- // @ts-ignore
123
100
  tools: buildAnthropicTools(tools),
124
101
  top_p,
125
102
  } satisfies Anthropic.MessageCreateParams;
126
103
  }
127
-
128
- private transformResponseToStream = (response: Anthropic.Beta.Tools.ToolsBetaMessage) => {
129
- return new ReadableStream<Anthropic.MessageStreamEvent>({
130
- start(controller) {
131
- response.content.forEach((content) => {
132
- switch (content.type) {
133
- case 'text': {
134
- controller.enqueue({
135
- delta: { text: content.text, type: 'text_delta' },
136
- type: 'content_block_delta',
137
- } as Anthropic.ContentBlockDeltaEvent);
138
- break;
139
- }
140
- case 'tool_use': {
141
- controller.enqueue({
142
- delta: {
143
- tool_use: { id: content.id, input: content.input, name: content.name },
144
- type: 'tool_use',
145
- },
146
- type: 'content_block_delta',
147
- } as any);
148
- }
149
- }
150
- });
151
-
152
- controller.enqueue({ type: 'message_stop' } as Anthropic.MessageStopEvent);
153
-
154
- controller.close();
155
- },
156
- });
157
- };
158
104
  }
159
105
 
160
106
  export default LobeAnthropicAI;
@@ -49,7 +49,7 @@ describe('LobeOpenAI', () => {
49
49
  });
50
50
 
51
51
  describe('Error', () => {
52
- it('should return OpenAIBizError with an openai error response when OpenAI.APIError is thrown', async () => {
52
+ it('should return ProviderBizError with an openai error response when OpenAI.APIError is thrown', async () => {
53
53
  // Arrange
54
54
  const apiError = new OpenAI.APIError(
55
55
  400,
@@ -79,7 +79,7 @@ describe('LobeOpenAI', () => {
79
79
  error: { message: 'Bad Request' },
80
80
  status: 400,
81
81
  },
82
- errorType: 'OpenAIBizError',
82
+ errorType: 'ProviderBizError',
83
83
  provider: 'openai',
84
84
  });
85
85
  }
@@ -89,11 +89,11 @@ describe('LobeOpenAI', () => {
89
89
  try {
90
90
  new LobeOpenAI({});
91
91
  } catch (e) {
92
- expect(e).toEqual({ errorType: 'NoOpenAIAPIKey' });
92
+ expect(e).toEqual({ errorType: 'InvalidProviderAPIKey' });
93
93
  }
94
94
  });
95
95
 
96
- it('should return OpenAIBizError with the cause when OpenAI.APIError is thrown with cause', async () => {
96
+ it('should return ProviderBizError with the cause when OpenAI.APIError is thrown with cause', async () => {
97
97
  // Arrange
98
98
  const errorInfo = {
99
99
  stack: 'abc',
@@ -119,13 +119,13 @@ describe('LobeOpenAI', () => {
119
119
  cause: { message: 'api is undefined' },
120
120
  stack: 'abc',
121
121
  },
122
- errorType: 'OpenAIBizError',
122
+ errorType: 'ProviderBizError',
123
123
  provider: 'openai',
124
124
  });
125
125
  }
126
126
  });
127
127
 
128
- it('should return OpenAIBizError with an cause response with desensitize Url', async () => {
128
+ it('should return ProviderBizError with an cause response with desensitize Url', async () => {
129
129
  // Arrange
130
130
  const errorInfo = {
131
131
  stack: 'abc',
@@ -155,7 +155,7 @@ describe('LobeOpenAI', () => {
155
155
  cause: { message: 'api is undefined' },
156
156
  stack: 'abc',
157
157
  },
158
- errorType: 'OpenAIBizError',
158
+ errorType: 'ProviderBizError',
159
159
  provider: 'openai',
160
160
  });
161
161
  }
@@ -1,4 +1,3 @@
1
- import { AgentRuntimeErrorType } from '../error';
2
1
  import { ModelProvider } from '../types';
3
2
  import { LobeOpenAICompatibleFactory } from '../utils/openaiCompatibleFactory';
4
3
 
@@ -7,10 +6,5 @@ export const LobeOpenAI = LobeOpenAICompatibleFactory({
7
6
  debug: {
8
7
  chatCompletion: () => process.env.DEBUG_OPENAI_CHAT_COMPLETION === '1',
9
8
  },
10
- errorType: {
11
- bizError: AgentRuntimeErrorType.OpenAIBizError,
12
- invalidAPIKey: AgentRuntimeErrorType.NoOpenAIAPIKey,
13
- },
14
-
15
9
  provider: ModelProvider.OpenAI,
16
10
  });
@@ -112,9 +112,9 @@ export const buildAnthropicMessages = (
112
112
 
113
113
  export const buildAnthropicTools = (tools?: OpenAI.ChatCompletionTool[]) =>
114
114
  tools?.map(
115
- (tool): Anthropic.Beta.Tools.Tool => ({
115
+ (tool): Anthropic.Tool => ({
116
116
  description: tool.function.description,
117
- input_schema: tool.function.parameters as Anthropic.Beta.Tools.Tool.InputSchema,
117
+ input_schema: tool.function.parameters as Anthropic.Tool.InputSchema,
118
118
  name: tool.function.name,
119
119
  }),
120
120
  );
@@ -99,27 +99,67 @@ describe('AnthropicStream', () => {
99
99
  });
100
100
 
101
101
  it('should handle tool use event and ReadableStream input', async () => {
102
- const toolUseEvent = {
103
- type: 'content_block_delta',
104
- delta: {
105
- type: 'tool_use',
106
- tool_use: {
107
- id: 'tool_use_1',
108
- name: 'example_tool',
109
- input: { arg1: 'value1' },
102
+ const streams = [
103
+ {
104
+ type: 'message_start',
105
+ message: {
106
+ id: 'msg_017aTuY86wNxth5TE544yqJq',
107
+ type: 'message',
108
+ role: 'assistant',
109
+ model: 'claude-3-sonnet-20240229',
110
+ content: [],
111
+ stop_reason: null,
112
+ stop_sequence: null,
113
+ usage: { input_tokens: 457, output_tokens: 1 },
110
114
  },
111
115
  },
112
- };
116
+ { type: 'content_block_start', index: 0, content_block: { type: 'text', text: '' } },
117
+ { type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: '好' } },
118
+ { type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: '的:' } },
119
+
120
+ { type: 'content_block_stop', index: 0 },
121
+ {
122
+ type: 'content_block_start',
123
+ index: 1,
124
+ content_block: {
125
+ type: 'tool_use',
126
+ id: 'toolu_01WdYWxYFQ8iu5iZq1Dy9Saf',
127
+ name: 'realtime-weather____fetchCurrentWeather',
128
+ input: {},
129
+ },
130
+ },
131
+ {
132
+ type: 'content_block_delta',
133
+ index: 1,
134
+ delta: { type: 'input_json_delta', partial_json: '' },
135
+ },
136
+ {
137
+ type: 'content_block_delta',
138
+ index: 1,
139
+ delta: { type: 'input_json_delta', partial_json: '{"city": "' },
140
+ },
141
+ {
142
+ type: 'content_block_delta',
143
+ index: 1,
144
+ delta: { type: 'input_json_delta', partial_json: '杭' },
145
+ },
146
+ {
147
+ type: 'content_block_delta',
148
+ index: 1,
149
+ delta: { type: 'input_json_delta', partial_json: '州"}' },
150
+ },
151
+ { type: 'content_block_stop', index: 1 },
152
+ {
153
+ type: 'message_delta',
154
+ delta: { stop_reason: 'tool_use', stop_sequence: null },
155
+ usage: { output_tokens: 83 },
156
+ },
157
+ ];
113
158
 
114
159
  const mockReadableStream = new ReadableStream({
115
160
  start(controller) {
116
- controller.enqueue({
117
- type: 'message_start',
118
- message: { id: 'message_1', metadata: {} },
119
- });
120
- controller.enqueue(toolUseEvent);
121
- controller.enqueue({
122
- type: 'message_stop',
161
+ streams.forEach((chunk) => {
162
+ controller.enqueue(chunk);
123
163
  });
124
164
  controller.close();
125
165
  },
@@ -139,19 +179,53 @@ describe('AnthropicStream', () => {
139
179
  chunks.push(decoder.decode(chunk, { stream: true }));
140
180
  }
141
181
 
142
- expect(chunks).toEqual([
143
- 'id: message_1\n',
144
- 'event: data\n',
145
- `data: {"id":"message_1","metadata":{}}\n\n`,
146
- 'id: message_1\n',
147
- 'event: tool_calls\n',
148
- `data: [{"function":{"arguments":"{\\"arg1\\":\\"value1\\"}","name":"example_tool"},"id":"tool_use_1","index":0,"type":"function"}]\n\n`,
149
- 'id: message_1\n',
150
- 'event: stop\n',
151
- `data: "message_stop"\n\n`,
152
- ]);
182
+ expect(chunks).toEqual(
183
+ [
184
+ 'id: msg_017aTuY86wNxth5TE544yqJq',
185
+ 'event: data',
186
+ 'data: {"id":"msg_017aTuY86wNxth5TE544yqJq","type":"message","role":"assistant","model":"claude-3-sonnet-20240229","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":457,"output_tokens":1}}\n',
187
+ 'id: msg_017aTuY86wNxth5TE544yqJq',
188
+ 'event: data',
189
+ 'data: ""\n',
190
+ 'id: msg_017aTuY86wNxth5TE544yqJq',
191
+ 'event: text',
192
+ 'data: "好"\n',
193
+ 'id: msg_017aTuY86wNxth5TE544yqJq',
194
+ 'event: text',
195
+ 'data: "的:"\n',
196
+ 'id: msg_017aTuY86wNxth5TE544yqJq',
197
+ 'event: data',
198
+ 'data: {"type":"content_block_stop","index":0}\n',
199
+ // Tool calls
200
+ 'id: msg_017aTuY86wNxth5TE544yqJq',
201
+ 'event: tool_calls',
202
+ `data: [{"function":{"arguments":"","name":"realtime-weather____fetchCurrentWeather"},"id":"toolu_01WdYWxYFQ8iu5iZq1Dy9Saf","index":0,"type":"function"}]\n`,
203
+ 'id: msg_017aTuY86wNxth5TE544yqJq',
204
+ 'event: tool_calls',
205
+ `data: [{"function":{"arguments":""},"index":0,"type":"function"}]\n`,
206
+ 'id: msg_017aTuY86wNxth5TE544yqJq',
207
+ 'event: tool_calls',
208
+ `data: [{"function":{"arguments":"{\\"city\\": \\""},"index":0,"type":"function"}]\n`,
209
+ 'id: msg_017aTuY86wNxth5TE544yqJq',
210
+ 'event: tool_calls',
211
+
212
+ `data: [{"function":{"arguments":"杭"},"index":0,"type":"function"}]\n`,
213
+ 'id: msg_017aTuY86wNxth5TE544yqJq',
214
+ 'event: tool_calls',
215
+
216
+ `data: [{"function":{"arguments":"州\\"}"},"index":0,"type":"function"}]\n`,
217
+
218
+ 'id: msg_017aTuY86wNxth5TE544yqJq',
219
+ 'event: data',
220
+ 'data: {"type":"content_block_stop","index":1}\n',
221
+
222
+ 'id: msg_017aTuY86wNxth5TE544yqJq',
223
+ 'event: stop',
224
+ 'data: "tool_use"\n',
225
+ ].map((item) => `${item}\n`),
226
+ );
153
227
 
154
- expect(onToolCallMock).toHaveBeenCalledTimes(1);
228
+ expect(onToolCallMock).toHaveBeenCalledTimes(5);
155
229
  });
156
230
 
157
231
  it('should handle ReadableStream input', async () => {
@@ -22,27 +22,39 @@ export const transformAnthropicStream = (
22
22
  stack.id = chunk.message.id;
23
23
  return { data: chunk.message, id: chunk.message.id, type: 'data' };
24
24
  }
25
+ case 'content_block_start': {
26
+ if (chunk.content_block.type === 'tool_use') {
27
+ const toolChunk = chunk.content_block;
25
28
 
26
- // case 'content_block_start': {
27
- // return { data: chunk.content_block.text, id: stack.id, type: 'data' };
28
- // }
29
+ const toolCall: StreamToolCallChunkData = {
30
+ function: {
31
+ arguments: '',
32
+ name: toolChunk.name,
33
+ },
34
+ id: toolChunk.id,
35
+ index: 0,
36
+ type: 'function',
37
+ };
38
+
39
+ stack.tool = { id: toolChunk.id, index: 0, name: toolChunk.name };
40
+
41
+ return { data: [toolCall], id: stack.id, type: 'tool_calls' };
42
+ }
43
+
44
+ return { data: chunk.content_block.text, id: stack.id, type: 'data' };
45
+ }
29
46
 
30
47
  case 'content_block_delta': {
31
- switch (chunk.delta.type as string) {
32
- default:
48
+ switch (chunk.delta.type) {
33
49
  case 'text_delta': {
34
50
  return { data: chunk.delta.text, id: stack.id, type: 'text' };
35
51
  }
36
52
 
37
- // TODO: due to anthropic currently don't support streaming tool calling
38
- // we need to add this new `tool_use` type to support streaming
39
- // and maybe we need to update it when the feature is available
40
- case 'tool_use': {
41
- const delta = (chunk.delta as any).tool_use as Anthropic.Beta.Tools.ToolUseBlock;
53
+ case 'input_json_delta': {
54
+ const delta = chunk.delta.partial_json;
42
55
 
43
56
  const toolCall: StreamToolCallChunkData = {
44
- function: { arguments: JSON.stringify(delta.input), name: delta.name },
45
- id: delta.id,
57
+ function: { arguments: delta },
46
58
  index: 0,
47
59
  type: 'function',
48
60
  };
@@ -53,7 +65,12 @@ export const transformAnthropicStream = (
53
65
  type: 'tool_calls',
54
66
  } as StreamProtocolToolCallChunk;
55
67
  }
68
+
69
+ default: {
70
+ break;
71
+ }
56
72
  }
73
+ return { data: chunk, id: stack.id, type: 'data' };
57
74
  }
58
75
 
59
76
  case 'message_delta': {
@@ -2,6 +2,11 @@ import { ChatStreamCallbacks } from '@/libs/agent-runtime';
2
2
 
3
3
  export interface StreamStack {
4
4
  id: string;
5
+ tool?: {
6
+ id: string;
7
+ index: number;
8
+ name: string;
9
+ };
5
10
  }
6
11
 
7
12
  export interface StreamProtocolChunk {
@@ -15,7 +20,7 @@ export interface StreamToolCallChunkData {
15
20
  arguments?: string;
16
21
  name?: string | null;
17
22
  };
18
- id: string;
23
+ id?: string;
19
24
  index: number;
20
25
  type: 'function' | string;
21
26
  }
@@ -44,6 +44,10 @@ export class ServerService implements IMessageService {
44
44
  return lambdaClient.message.update.mutate({ id, value: { error } });
45
45
  }
46
46
 
47
+ async updateMessagePluginError(id: string, error: ChatMessageError): Promise<any> {
48
+ return lambdaClient.message.update.mutate({ id, value: { pluginError: error } });
49
+ }
50
+
47
51
  updateMessage(id: string, message: Partial<ChatMessage>): Promise<any> {
48
52
  return lambdaClient.message.update.mutate({ id, value: message });
49
53
  }
@@ -17,7 +17,7 @@ export interface CreateMessageParams
17
17
  traceId?: string;
18
18
  topicId?: string;
19
19
  content: string;
20
- error?: ChatMessageError;
20
+ error?: ChatMessageError | null;
21
21
  role: MessageRoleType;
22
22
  }
23
23
 
@@ -19,12 +19,14 @@ import { agentSelectors } from '@/store/agent/selectors';
19
19
  import { chatHelpers } from '@/store/chat/helpers';
20
20
  import { messageMapKey } from '@/store/chat/slices/message/utils';
21
21
  import { ChatStore } from '@/store/chat/store';
22
- import { ChatMessage, MessageToolCall } from '@/types/message';
22
+ import { ChatMessage, ChatMessageError, MessageToolCall } from '@/types/message';
23
23
  import { TraceEventPayloads } from '@/types/trace';
24
24
  import { setNamespace } from '@/utils/storeDebug';
25
25
  import { nanoid } from '@/utils/uuid';
26
26
 
27
+ import type { ChatStoreState } from '../../initialState';
27
28
  import { chatSelectors, topicSelectors } from '../../selectors';
29
+ import { preventLeavingFn, toggleBooleanList } from '../../utils';
28
30
  import { MessageDispatch, messagesReducer } from './reducer';
29
31
 
30
32
  const n = setNamespace('m');
@@ -84,6 +86,12 @@ export interface ChatMessageAction {
84
86
  id?: string,
85
87
  action?: string,
86
88
  ) => AbortController | undefined;
89
+ internal_toggleLoadingArrays: (
90
+ key: keyof ChatStoreState,
91
+ loading: boolean,
92
+ id?: string,
93
+ action?: string,
94
+ ) => AbortController | undefined;
87
95
  internal_toggleToolCallingStreaming: (id: string, streaming: boolean[] | undefined) => void;
88
96
  internal_toggleMessageLoading: (loading: boolean, id: string) => void;
89
97
  /**
@@ -123,6 +131,7 @@ export interface ChatMessageAction {
123
131
  content: string,
124
132
  toolCalls?: MessageToolCall[],
125
133
  ) => Promise<void>;
134
+ internal_updateMessageError: (id: string, error: ChatMessageError | null) => Promise<void>;
126
135
  internal_createMessage: (
127
136
  params: CreateMessageParams,
128
137
  context?: { tempMessageId?: string; skipRefresh?: boolean },
@@ -136,24 +145,6 @@ export interface ChatMessageAction {
136
145
  const getAgentConfig = () => agentSelectors.currentAgentConfig(useAgentStore.getState());
137
146
  const getAgentChatConfig = () => agentSelectors.currentAgentChatConfig(useAgentStore.getState());
138
147
 
139
- const preventLeavingFn = (e: BeforeUnloadEvent) => {
140
- // set returnValue to trigger alert modal
141
- // Note: No matter what value is set, the browser will display the standard text
142
- e.returnValue = '你有正在生成中的请求,确定要离开吗?';
143
- };
144
-
145
- const toggleBooleanList = (ids: string[], id: string, loading: boolean) => {
146
- return produce(ids, (draft) => {
147
- if (loading) {
148
- draft.push(id);
149
- } else {
150
- const index = draft.indexOf(id);
151
-
152
- if (index >= 0) draft.splice(index, 1);
153
- }
154
- });
155
- };
156
-
157
148
  export const chatMessage: StateCreator<
158
149
  ChatStore,
159
150
  [['zustand/devtools', never]],
@@ -591,35 +582,7 @@ export const chatMessage: StateCreator<
591
582
  };
592
583
  },
593
584
  internal_toggleChatLoading: (loading, id, action) => {
594
- if (loading) {
595
- window.addEventListener('beforeunload', preventLeavingFn);
596
-
597
- const abortController = new AbortController();
598
- set(
599
- {
600
- abortController,
601
- chatLoadingIds: toggleBooleanList(get().messageLoadingIds, id!, loading),
602
- },
603
- false,
604
- action,
605
- );
606
-
607
- return abortController;
608
- } else {
609
- if (!id) {
610
- set({ abortController: undefined, chatLoadingIds: [] }, false, action);
611
- } else
612
- set(
613
- {
614
- abortController: undefined,
615
- chatLoadingIds: toggleBooleanList(get().messageLoadingIds, id, loading),
616
- },
617
- false,
618
- action,
619
- );
620
-
621
- window.removeEventListener('beforeunload', preventLeavingFn);
622
- }
585
+ return get().internal_toggleLoadingArrays('chatLoadingIds', loading, id, action);
623
586
  },
624
587
  internal_toggleMessageLoading: (loading, id) => {
625
588
  set(
@@ -684,6 +647,11 @@ export const chatMessage: StateCreator<
684
647
  await internal_coreProcessMessage(contextMessages, latestMsg.id, { traceId });
685
648
  },
686
649
 
650
+ internal_updateMessageError: async (id, error) => {
651
+ get().internal_dispatchMessage({ id, type: 'updateMessages', value: { error } });
652
+ await messageService.updateMessage(id, { error });
653
+ await get().refreshMessages();
654
+ },
687
655
  internal_updateMessageContent: async (id, content, toolCalls) => {
688
656
  const { internal_dispatchMessage, refreshMessages, internal_transformToolCalls } = get();
689
657
 
@@ -762,4 +730,36 @@ export const chatMessage: StateCreator<
762
730
  .catch();
763
731
  }
764
732
  },
733
+
734
+ internal_toggleLoadingArrays: (key, loading, id, action) => {
735
+ if (loading) {
736
+ window.addEventListener('beforeunload', preventLeavingFn);
737
+
738
+ const abortController = new AbortController();
739
+ set(
740
+ {
741
+ abortController,
742
+ [key]: toggleBooleanList(get()[key] as string[], id!, loading),
743
+ },
744
+ false,
745
+ action,
746
+ );
747
+
748
+ return abortController;
749
+ } else {
750
+ if (!id) {
751
+ set({ abortController: undefined, [key]: [] }, false, action);
752
+ } else
753
+ set(
754
+ {
755
+ abortController: undefined,
756
+ [key]: toggleBooleanList(get()[key] as string[], id, loading),
757
+ },
758
+ false,
759
+ action,
760
+ );
761
+
762
+ window.removeEventListener('beforeunload', preventLeavingFn);
763
+ }
764
+ },
765
765
  });
@@ -26,6 +26,7 @@ export interface ChatMessageState {
26
26
  */
27
27
  messagesInit: boolean;
28
28
  messagesMap: Record<string, ChatMessage[]>;
29
+ pluginApiLoadingIds: string[];
29
30
  /**
30
31
  * the tool calling stream ids
31
32
  */
@@ -41,5 +42,6 @@ export const initialMessageState: ChatMessageState = {
41
42
  messageLoadingIds: [],
42
43
  messagesInit: false,
43
44
  messagesMap: {},
45
+ pluginApiLoadingIds: [],
44
46
  toolCallingStreamIds: {},
45
47
  };
@@ -129,6 +129,8 @@ const isHasMessageLoading = (s: ChatStore) => s.messageLoadingIds.length > 0;
129
129
  const isCreatingMessage = (s: ChatStore) => s.isCreatingMessage;
130
130
 
131
131
  const isMessageGenerating = (id: string) => (s: ChatStore) => s.chatLoadingIds.includes(id);
132
+ const isPluginApiInvoking = (id: string) => (s: ChatStore) => s.pluginApiLoadingIds.includes(id);
133
+
132
134
  const isToolCallStreaming = (id: string, index: number) => (s: ChatStore) => {
133
135
  const isLoading = s.toolCallingStreamIds[id];
134
136
 
@@ -155,6 +157,7 @@ export const chatSelectors = {
155
157
  isMessageEditing,
156
158
  isMessageGenerating,
157
159
  isMessageLoading,
160
+ isPluginApiInvoking,
158
161
  isToolCallStreaming,
159
162
  latestMessage,
160
163
  showInboxWelcome,
@@ -112,7 +112,7 @@ describe('ChatPluginAction', () => {
112
112
 
113
113
  vi.spyOn(storeState, 'refreshMessages');
114
114
  vi.spyOn(storeState, 'triggerAIMessage').mockResolvedValue(undefined);
115
- vi.spyOn(storeState, 'internal_toggleChatLoading').mockReturnValue(undefined);
115
+ vi.spyOn(storeState, 'internal_togglePluginApiCalling').mockReturnValue(undefined);
116
116
 
117
117
  const runSpy = vi.spyOn(chatService, 'runPluginApi').mockResolvedValue({
118
118
  text: pluginApiResponse,
@@ -125,7 +125,7 @@ describe('ChatPluginAction', () => {
125
125
  await result.current.invokeDefaultTypePlugin(messageId, pluginPayload);
126
126
  });
127
127
 
128
- expect(storeState.internal_toggleChatLoading).toHaveBeenCalledWith(
128
+ expect(storeState.internal_togglePluginApiCalling).toHaveBeenCalledWith(
129
129
  true,
130
130
  messageId,
131
131
  expect.any(String),
@@ -135,7 +135,7 @@ describe('ChatPluginAction', () => {
135
135
  content: pluginApiResponse,
136
136
  });
137
137
  expect(storeState.refreshMessages).toHaveBeenCalled();
138
- expect(storeState.internal_toggleChatLoading).toHaveBeenCalledWith(
138
+ expect(storeState.internal_togglePluginApiCalling).toHaveBeenCalledWith(
139
139
  false,
140
140
  'message-id',
141
141
  'plugin/fetchPlugin/end',
@@ -150,7 +150,7 @@ describe('ChatPluginAction', () => {
150
150
  const storeState = useChatStore.getState();
151
151
  vi.spyOn(storeState, 'refreshMessages');
152
152
  vi.spyOn(storeState, 'triggerAIMessage').mockResolvedValue(undefined);
153
- vi.spyOn(storeState, 'internal_toggleChatLoading').mockReturnValue(undefined);
153
+ vi.spyOn(storeState, 'internal_togglePluginApiCalling').mockReturnValue(undefined);
154
154
 
155
155
  vi.spyOn(chatService, 'runPluginApi').mockRejectedValue(error);
156
156
 
@@ -159,7 +159,7 @@ describe('ChatPluginAction', () => {
159
159
  await result.current.invokeDefaultTypePlugin(messageId, pluginPayload);
160
160
  });
161
161
 
162
- expect(storeState.internal_toggleChatLoading).toHaveBeenCalledWith(
162
+ expect(storeState.internal_togglePluginApiCalling).toHaveBeenCalledWith(
163
163
  true,
164
164
  messageId,
165
165
  expect.any(String),
@@ -167,7 +167,7 @@ describe('ChatPluginAction', () => {
167
167
  expect(chatService.runPluginApi).toHaveBeenCalledWith(pluginPayload, { trace: {} });
168
168
  expect(messageService.updateMessageError).toHaveBeenCalledWith(messageId, error);
169
169
  expect(storeState.refreshMessages).toHaveBeenCalled();
170
- expect(storeState.internal_toggleChatLoading).toHaveBeenCalledWith(
170
+ expect(storeState.internal_togglePluginApiCalling).toHaveBeenCalledWith(
171
171
  false,
172
172
  'message-id',
173
173
  'plugin/fetchPlugin/end',
@@ -467,7 +467,7 @@ describe('ChatPluginAction', () => {
467
467
  });
468
468
 
469
469
  useChatStore.setState({
470
- internal_toggleChatLoading: vi.fn(),
470
+ internal_togglePluginApiCalling: vi.fn(),
471
471
  internal_updateMessageContent: vi.fn(),
472
472
  text2image: vi.fn(),
473
473
  });
@@ -491,12 +491,12 @@ describe('ChatPluginAction', () => {
491
491
  );
492
492
 
493
493
  // Verify that loading was toggled correctly
494
- expect(result.current.internal_toggleChatLoading).toHaveBeenCalledWith(
494
+ expect(result.current.internal_togglePluginApiCalling).toHaveBeenCalledWith(
495
495
  true,
496
496
  messageId,
497
497
  expect.any(String),
498
498
  );
499
- expect(result.current.internal_toggleChatLoading).toHaveBeenCalledWith(false);
499
+ expect(result.current.internal_togglePluginApiCalling).toHaveBeenCalledWith(false);
500
500
  expect(useChatStore.getState().text2image).toHaveBeenCalled();
501
501
  });
502
502
 
@@ -516,7 +516,7 @@ describe('ChatPluginAction', () => {
516
516
  });
517
517
 
518
518
  useChatStore.setState({
519
- internal_toggleChatLoading: vi.fn(),
519
+ internal_togglePluginApiCalling: vi.fn(),
520
520
  text2image: vi.fn(),
521
521
  internal_updateMessageContent: vi.fn(),
522
522
  });
@@ -540,12 +540,12 @@ describe('ChatPluginAction', () => {
540
540
  );
541
541
 
542
542
  // Verify that loading was toggled correctly
543
- expect(result.current.internal_toggleChatLoading).toHaveBeenCalledWith(
543
+ expect(result.current.internal_togglePluginApiCalling).toHaveBeenCalledWith(
544
544
  true,
545
545
  messageId,
546
546
  expect.any(String),
547
547
  );
548
- expect(result.current.internal_toggleChatLoading).toHaveBeenCalledWith(false);
548
+ expect(result.current.internal_togglePluginApiCalling).toHaveBeenCalledWith(false);
549
549
  expect(useChatStore.getState().text2image).not.toHaveBeenCalled();
550
550
  });
551
551
 
@@ -563,7 +563,7 @@ describe('ChatPluginAction', () => {
563
563
  });
564
564
 
565
565
  useChatStore.setState({
566
- internal_toggleChatLoading: vi.fn(),
566
+ internal_togglePluginApiCalling: vi.fn(),
567
567
  internal_updateMessageContent: vi.fn(),
568
568
  text2image: vi.fn(),
569
569
  refreshMessages: vi.fn(),
@@ -576,12 +576,12 @@ describe('ChatPluginAction', () => {
576
576
  });
577
577
 
578
578
  // Verify that loading was toggled correctly
579
- expect(result.current.internal_toggleChatLoading).toHaveBeenCalledWith(
579
+ expect(result.current.internal_togglePluginApiCalling).toHaveBeenCalledWith(
580
580
  true,
581
581
  messageId,
582
582
  expect.any(String),
583
583
  );
584
- expect(result.current.internal_toggleChatLoading).toHaveBeenCalledWith(false);
584
+ expect(result.current.internal_togglePluginApiCalling).toHaveBeenCalledWith(false);
585
585
 
586
586
  // Verify that the message content was not updated
587
587
  expect(result.current.internal_updateMessageContent).not.toHaveBeenCalled();
@@ -1,3 +1,4 @@
1
+ /* eslint-disable sort-keys-fix/sort-keys-fix, typescript-sort-keys/interface */
1
2
  import { PluginErrorType } from '@lobehub/chat-plugin-sdk';
2
3
  import { t } from 'i18next';
3
4
  import { Md5 } from 'ts-md5';
@@ -25,11 +26,6 @@ export interface ChatPluginAction {
25
26
  triggerAiMessage?: boolean,
26
27
  ) => Promise<void>;
27
28
 
28
- internal_callPluginApi: (id: string, payload: ChatToolPayload) => Promise<string | undefined>;
29
- internal_invokeDifferentTypePlugin: (id: string, payload: ChatToolPayload) => Promise<any>;
30
- internal_transformToolCalls: (toolCalls: MessageToolCall[]) => ChatToolPayload[];
31
- internal_updatePluginError: (id: string, error: any) => Promise<void>;
32
-
33
29
  invokeBuiltinTool: (id: string, payload: ChatToolPayload) => Promise<void>;
34
30
  invokeDefaultTypePlugin: (id: string, payload: any) => Promise<string | undefined>;
35
31
  invokeMarkdownTypePlugin: (id: string, payload: ChatToolPayload) => Promise<void>;
@@ -41,6 +37,16 @@ export interface ChatPluginAction {
41
37
 
42
38
  triggerToolCalls: (id: string) => Promise<void>;
43
39
  updatePluginState: (id: string, key: string, value: any) => Promise<void>;
40
+
41
+ internal_callPluginApi: (id: string, payload: ChatToolPayload) => Promise<string | undefined>;
42
+ internal_invokeDifferentTypePlugin: (id: string, payload: ChatToolPayload) => Promise<any>;
43
+ internal_togglePluginApiCalling: (
44
+ loading: boolean,
45
+ id?: string,
46
+ action?: string,
47
+ ) => AbortController | undefined;
48
+ internal_transformToolCalls: (toolCalls: MessageToolCall[]) => ChatToolPayload[];
49
+ internal_updatePluginError: (id: string, error: any) => Promise<void>;
44
50
  }
45
51
 
46
52
  export const chatPlugin: StateCreator<
@@ -69,124 +75,17 @@ export const chatPlugin: StateCreator<
69
75
 
70
76
  if (triggerAiMessage) await triggerAIMessage({ parentId: id });
71
77
  },
72
- internal_callPluginApi: async (id, payload) => {
73
- const { internal_updateMessageContent, refreshMessages, internal_toggleChatLoading } = get();
74
- let data: string;
75
-
76
- try {
77
- const abortController = internal_toggleChatLoading(
78
- true,
79
- id,
80
- n('fetchPlugin/start') as string,
81
- );
82
-
83
- const message = chatSelectors.getMessageById(id)(get());
84
-
85
- const res = await chatService.runPluginApi(payload, {
86
- signal: abortController?.signal,
87
- trace: { observationId: message?.observationId, traceId: message?.traceId },
88
- });
89
- data = res.text;
90
-
91
- // save traceId
92
- if (res.traceId) {
93
- await messageService.updateMessage(id, { traceId: res.traceId });
94
- }
95
- } catch (error) {
96
- console.log(error);
97
- const err = error as Error;
98
-
99
- // ignore the aborted request error
100
- if (!err.message.includes('The user aborted a request.')) {
101
- await messageService.updateMessageError(id, error as any);
102
- await refreshMessages();
103
- }
104
-
105
- data = '';
106
- }
107
-
108
- internal_toggleChatLoading(false, id, n('fetchPlugin/end') as string);
109
- // 如果报错则结束了
110
- if (!data) return;
111
-
112
- await internal_updateMessageContent(id, data);
113
-
114
- return data;
115
- },
116
-
117
- internal_invokeDifferentTypePlugin: async (id, payload) => {
118
- switch (payload.type) {
119
- case 'standalone': {
120
- return await get().invokeStandaloneTypePlugin(id, payload);
121
- }
122
-
123
- case 'markdown': {
124
- return await get().invokeMarkdownTypePlugin(id, payload);
125
- }
126
-
127
- case 'builtin': {
128
- return await get().invokeBuiltinTool(id, payload);
129
- }
130
-
131
- default: {
132
- return await get().invokeDefaultTypePlugin(id, payload);
133
- }
134
- }
135
- },
136
-
137
- internal_transformToolCalls: (toolCalls) => {
138
- return toolCalls
139
- .map((toolCall): ChatToolPayload | null => {
140
- let payload: ChatToolPayload;
141
-
142
- const [identifier, apiName, type] = toolCall.function.name.split(PLUGIN_SCHEMA_SEPARATOR);
143
-
144
- if (!apiName) return null;
145
-
146
- payload = {
147
- apiName,
148
- arguments: toolCall.function.arguments,
149
- id: toolCall.id,
150
- identifier,
151
- type: (type ?? 'default') as any,
152
- };
153
-
154
- // if the apiName is md5, try to find the correct apiName in the plugins
155
- if (apiName.startsWith(PLUGIN_SCHEMA_API_MD5_PREFIX)) {
156
- const md5 = apiName.replace(PLUGIN_SCHEMA_API_MD5_PREFIX, '');
157
- const manifest = pluginSelectors.getPluginManifestById(identifier)(
158
- useToolStore.getState(),
159
- );
160
-
161
- const api = manifest?.api.find((api) => Md5.hashStr(api.name).toString() === md5);
162
- if (api) {
163
- payload.apiName = api.name;
164
- }
165
- }
166
-
167
- return payload;
168
- })
169
- .filter(Boolean) as ChatToolPayload[];
170
- },
171
-
172
- internal_updatePluginError: async (id, error) => {
173
- const { refreshMessages } = get();
174
-
175
- await messageService.updateMessage(id, { pluginError: error });
176
- await refreshMessages();
177
- },
178
-
179
78
  invokeBuiltinTool: async (id, payload) => {
180
- const { internal_toggleChatLoading, internal_updateMessageContent } = get();
79
+ const { internal_togglePluginApiCalling, internal_updateMessageContent } = get();
181
80
  const params = JSON.parse(payload.arguments);
182
- internal_toggleChatLoading(true, id, n('invokeBuiltinTool') as string);
81
+ internal_togglePluginApiCalling(true, id, n('invokeBuiltinTool') as string);
183
82
  let data;
184
83
  try {
185
84
  data = await useToolStore.getState().invokeBuiltinTool(payload.apiName, params);
186
85
  } catch (error) {
187
86
  console.log(error);
188
87
  }
189
- internal_toggleChatLoading(false);
88
+ internal_togglePluginApiCalling(false);
190
89
 
191
90
  if (!data) return;
192
91
 
@@ -248,6 +147,8 @@ export const chatPlugin: StateCreator<
248
147
  const message = chatSelectors.getMessageById(id)(get());
249
148
  if (!message || message.role !== 'tool' || !message.plugin) return;
250
149
 
150
+ get().internal_updateMessageError(id, null);
151
+
251
152
  const payload: ChatToolPayload = { ...message.plugin, id: message.tool_call_id! };
252
153
 
253
154
  await get().internal_invokeDifferentTypePlugin(id, payload);
@@ -301,4 +202,116 @@ export const chatPlugin: StateCreator<
301
202
  await messageService.updateMessagePluginState(id, { [key]: value });
302
203
  await refreshMessages();
303
204
  },
205
+
206
+ internal_callPluginApi: async (id, payload) => {
207
+ const { internal_updateMessageContent, refreshMessages, internal_togglePluginApiCalling } =
208
+ get();
209
+ let data: string;
210
+
211
+ try {
212
+ const abortController = internal_togglePluginApiCalling(
213
+ true,
214
+ id,
215
+ n('fetchPlugin/start') as string,
216
+ );
217
+
218
+ const message = chatSelectors.getMessageById(id)(get());
219
+
220
+ const res = await chatService.runPluginApi(payload, {
221
+ signal: abortController?.signal,
222
+ trace: { observationId: message?.observationId, traceId: message?.traceId },
223
+ });
224
+ data = res.text;
225
+
226
+ // save traceId
227
+ if (res.traceId) {
228
+ await messageService.updateMessage(id, { traceId: res.traceId });
229
+ }
230
+ } catch (error) {
231
+ console.log(error);
232
+ const err = error as Error;
233
+
234
+ // ignore the aborted request error
235
+ if (!err.message.includes('The user aborted a request.')) {
236
+ await messageService.updateMessageError(id, error as any);
237
+ await refreshMessages();
238
+ }
239
+
240
+ data = '';
241
+ }
242
+
243
+ internal_togglePluginApiCalling(false, id, n('fetchPlugin/end') as string);
244
+ // 如果报错则结束了
245
+ if (!data) return;
246
+
247
+ await internal_updateMessageContent(id, data);
248
+
249
+ return data;
250
+ },
251
+
252
+ internal_invokeDifferentTypePlugin: async (id, payload) => {
253
+ switch (payload.type) {
254
+ case 'standalone': {
255
+ return await get().invokeStandaloneTypePlugin(id, payload);
256
+ }
257
+
258
+ case 'markdown': {
259
+ return await get().invokeMarkdownTypePlugin(id, payload);
260
+ }
261
+
262
+ case 'builtin': {
263
+ return await get().invokeBuiltinTool(id, payload);
264
+ }
265
+
266
+ default: {
267
+ return await get().invokeDefaultTypePlugin(id, payload);
268
+ }
269
+ }
270
+ },
271
+
272
+ internal_togglePluginApiCalling: (loading, id, action) => {
273
+ return get().internal_toggleLoadingArrays('pluginApiLoadingIds', loading, id, action);
274
+ },
275
+
276
+ internal_transformToolCalls: (toolCalls) => {
277
+ return toolCalls
278
+ .map((toolCall): ChatToolPayload | null => {
279
+ let payload: ChatToolPayload;
280
+
281
+ const [identifier, apiName, type] = toolCall.function.name.split(PLUGIN_SCHEMA_SEPARATOR);
282
+
283
+ if (!apiName) return null;
284
+
285
+ payload = {
286
+ apiName,
287
+ arguments: toolCall.function.arguments,
288
+ id: toolCall.id,
289
+ identifier,
290
+ type: (type ?? 'default') as any,
291
+ };
292
+
293
+ // if the apiName is md5, try to find the correct apiName in the plugins
294
+ if (apiName.startsWith(PLUGIN_SCHEMA_API_MD5_PREFIX)) {
295
+ const md5 = apiName.replace(PLUGIN_SCHEMA_API_MD5_PREFIX, '');
296
+ const manifest = pluginSelectors.getPluginManifestById(identifier)(
297
+ useToolStore.getState(),
298
+ );
299
+
300
+ const api = manifest?.api.find((api) => Md5.hashStr(api.name).toString() === md5);
301
+ if (api) {
302
+ payload.apiName = api.name;
303
+ }
304
+ }
305
+
306
+ return payload;
307
+ })
308
+ .filter(Boolean) as ChatToolPayload[];
309
+ },
310
+ internal_updatePluginError: async (id, error) => {
311
+ const { refreshMessages } = get();
312
+
313
+ get().internal_dispatchMessage({ id, type: 'updateMessages', value: { error } });
314
+ await messageService.updateMessage(id, { pluginError: error });
315
+ await refreshMessages();
316
+ },
304
317
  });
@@ -0,0 +1,19 @@
1
+ import { produce } from 'immer';
2
+
3
+ export const preventLeavingFn = (e: BeforeUnloadEvent) => {
4
+ // set returnValue to trigger alert modal
5
+ // Note: No matter what value is set, the browser will display the standard text
6
+ e.returnValue = '你有正在生成中的请求,确定要离开吗?';
7
+ };
8
+
9
+ export const toggleBooleanList = (ids: string[], id: string, loading: boolean) => {
10
+ return produce(ids, (draft) => {
11
+ if (loading) {
12
+ draft.push(id);
13
+ } else {
14
+ const index = draft.indexOf(id);
15
+
16
+ if (index >= 0) draft.splice(index, 1);
17
+ }
18
+ });
19
+ };
@@ -32,7 +32,7 @@ export * from './tools';
32
32
 
33
33
  export interface ChatMessage extends BaseDataModel {
34
34
  content: string;
35
- error?: ChatMessageError;
35
+ error?: ChatMessageError | null;
36
36
  // 扩展字段
37
37
  extra?: {
38
38
  fromModel?: string;