@lobehub/chat 0.117.5 → 0.118.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,56 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ### [Version 0.118.1](https://github.com/lobehub/lobe-chat/compare/v0.118.0...v0.118.1)
6
+
7
+ <sup>Released on **2023-12-30**</sup>
8
+
9
+ #### 🐛 Bug Fixes
10
+
11
+ - **misc**: Fix dalle image download error.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### What's fixed
19
+
20
+ - **misc**: Fix dalle image download error, closes [#868](https://github.com/lobehub/lobe-chat/issues/868) ([5b6d11f](https://github.com/lobehub/lobe-chat/commit/5b6d11f))
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
+
30
+ ## [Version 0.118.0](https://github.com/lobehub/lobe-chat/compare/v0.117.5...v0.118.0)
31
+
32
+ <sup>Released on **2023-12-29**</sup>
33
+
34
+ #### ✨ Features
35
+
36
+ - **misc**: Support markdown type plugin.
37
+
38
+ <br/>
39
+
40
+ <details>
41
+ <summary><kbd>Improvements and Fixes</kbd></summary>
42
+
43
+ #### What's improved
44
+
45
+ - **misc**: Support markdown type plugin, closes [#865](https://github.com/lobehub/lobe-chat/issues/865) ([2791166](https://github.com/lobehub/lobe-chat/commit/2791166))
46
+
47
+ </details>
48
+
49
+ <div align="right">
50
+
51
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
52
+
53
+ </div>
54
+
5
55
  ### [Version 0.117.5](https://github.com/lobehub/lobe-chat/compare/v0.117.4...v0.117.5)
6
56
 
7
57
  <sup>Released on **2023-12-29**</sup>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "0.117.5",
3
+ "version": "0.118.1",
4
4
  "description": "Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
5
5
  "keywords": [
6
6
  "framework",
@@ -151,7 +151,7 @@
151
151
  "@types/ua-parser-js": "^0.7",
152
152
  "@types/uuid": "^9",
153
153
  "@umijs/lint": "^4",
154
- "@vitest/coverage-v8": "0.34.6",
154
+ "@vitest/coverage-v8": "^1",
155
155
  "commitlint": "^18",
156
156
  "consola": "^3",
157
157
  "dpdm": "^3",
@@ -175,7 +175,7 @@
175
175
  "typescript": "^5",
176
176
  "unified": "^11",
177
177
  "unist-util-visit": "^5",
178
- "vitest": "0.34.6",
178
+ "vitest": "^1",
179
179
  "vitest-canvas-mock": "^0.3.3"
180
180
  },
181
181
  "publishConfig": {
@@ -11,7 +11,7 @@ const PluginSchema = z.object({
11
11
  identifier: z.string(),
12
12
  arguments: z.string(),
13
13
  apiName: z.string(),
14
- type: z.enum(['default', 'standalone', 'builtin']).default('default'),
14
+ type: z.enum(['default', 'markdown', 'standalone', 'builtin']).default('default'),
15
15
  });
16
16
 
17
17
  export const DB_MessageSchema = z.object({
@@ -1,5 +1,4 @@
1
1
  import { memo } from 'react';
2
- import { Flexbox } from 'react-layout-kit';
3
2
 
4
3
  import { BuiltinToolsRenders } from '@/tools/renders';
5
4
 
@@ -17,13 +16,7 @@ const BuiltinType = memo<BuiltinTypeProps>(({ content, id, identifier, loading }
17
16
  const { isJSON, data } = useParseContent(content);
18
17
 
19
18
  if (!isJSON) {
20
- return (
21
- loading && (
22
- <Flexbox gap={8}>
23
- <Loading />
24
- </Flexbox>
25
- )
26
- );
19
+ return loading && <Loading />;
27
20
  }
28
21
 
29
22
  const Render = BuiltinToolsRenders[identifier || ''];
@@ -1,7 +1,6 @@
1
1
  import { Skeleton } from 'antd';
2
2
  import dynamic from 'next/dynamic';
3
3
  import { Suspense, memo } from 'react';
4
- import { Flexbox } from 'react-layout-kit';
5
4
 
6
5
  import { useToolStore } from '@/store/tool';
7
6
  import { pluginSelectors } from '@/store/tool/selectors';
@@ -24,13 +23,7 @@ const PluginDefaultType = memo<PluginDefaultTypeProps>(({ content, name, loading
24
23
  const { isJSON, data } = useParseContent(content);
25
24
 
26
25
  if (!isJSON) {
27
- return (
28
- loading && (
29
- <Flexbox gap={8}>
30
- <Loading />
31
- </Flexbox>
32
- )
33
- );
26
+ return loading && <Loading />;
34
27
  }
35
28
 
36
29
  if (!manifest?.ui) return;
@@ -0,0 +1,17 @@
1
+ import { Markdown } from '@lobehub/ui';
2
+ import { memo } from 'react';
3
+
4
+ import Loading from '../Loading';
5
+
6
+ export interface PluginMarkdownTypeProps {
7
+ content: string;
8
+ loading?: boolean;
9
+ }
10
+
11
+ const PluginMarkdownType = memo<PluginMarkdownTypeProps>(({ content, loading }) => {
12
+ if (loading) return <Loading />;
13
+
14
+ return <Markdown>{content}</Markdown>;
15
+ });
16
+
17
+ export default PluginMarkdownType;
@@ -9,10 +9,12 @@ import { pluginSelectors } from '@/store/tool/selectors';
9
9
 
10
10
  import { useOnPluginReadyForInteraction } from '../utils/iframeOnReady';
11
11
  import {
12
+ useOnPluginCreateAssistantMessage,
12
13
  useOnPluginFetchMessage,
13
14
  useOnPluginFetchPluginSettings,
14
15
  useOnPluginFetchPluginState,
15
16
  useOnPluginFillContent,
17
+ useOnPluginTriggerAIMessage,
16
18
  } from '../utils/listenToPlugin';
17
19
  import { useOnPluginSettingsUpdate } from '../utils/pluginSettings';
18
20
  import { useOnPluginStateUpdate } from '../utils/pluginState';
@@ -118,6 +120,21 @@ const IFrameRender = memo<IFrameRenderProps>(({ url, id, payload, width = 600, h
118
120
  updatePluginSettings(payload?.identifier, value);
119
121
  });
120
122
 
123
+ // when plugin want to trigger AI message
124
+ const triggerAIMessage = useChatStore((s) => s.triggerAIMessage);
125
+ useOnPluginTriggerAIMessage((messageId) => {
126
+ // we need to know which message to trigger
127
+ if (messageId !== id) return;
128
+
129
+ triggerAIMessage(id);
130
+ });
131
+
132
+ // when plugin want to create an assistant message
133
+ const createAssistantMessage = useChatStore((s) => s.createAssistantMessageByPlugin);
134
+ useOnPluginCreateAssistantMessage((content) => {
135
+ createAssistantMessage(content, id);
136
+ });
137
+
121
138
  return (
122
139
  <>
123
140
  {loading && <Skeleton active style={{ maxWidth: '100%', width }} />}
@@ -5,6 +5,7 @@ import { LobeToolRenderType } from '@/types/tool';
5
5
 
6
6
  import BuiltinType from '././BuiltinType';
7
7
  import DefaultType from './DefaultType';
8
+ import Markdown from './MarkdownType';
8
9
  import Standalone from './StandaloneType';
9
10
 
10
11
  export interface PluginRenderProps {
@@ -27,6 +28,10 @@ const PluginRender = memo<PluginRenderProps>(
27
28
  return <BuiltinType content={content} id={id} identifier={identifier} loading={loading} />;
28
29
  }
29
30
 
31
+ case 'markdown': {
32
+ return <Markdown content={content} loading={loading} />;
33
+ }
34
+
30
35
  default: {
31
36
  return <DefaultType content={content} loading={loading} name={identifier} />;
32
37
  }
@@ -3,10 +3,12 @@ import { renderHook } from '@testing-library/react';
3
3
  import { afterEach, describe, expect, it, vi } from 'vitest';
4
4
 
5
5
  import {
6
+ useOnPluginCreateAssistantMessage,
6
7
  useOnPluginFetchMessage,
7
8
  useOnPluginFetchPluginSettings,
8
9
  useOnPluginFetchPluginState,
9
10
  useOnPluginFillContent,
11
+ useOnPluginTriggerAIMessage,
10
12
  } from './listenToPlugin';
11
13
 
12
14
  afterEach(() => {
@@ -102,3 +104,61 @@ describe('useOnPluginFetchPluginSettings', () => {
102
104
  expect(mockOnRequest).toHaveBeenCalled();
103
105
  });
104
106
  });
107
+
108
+ describe('useOnPluginTriggerAIMessage', () => {
109
+ it('calls callback with id when a triggerAIMessage is received', () => {
110
+ const mockCallback = vi.fn();
111
+ renderHook(() => useOnPluginTriggerAIMessage(mockCallback));
112
+
113
+ const testId = 'testId';
114
+ const event = new MessageEvent('message', {
115
+ data: { type: PluginChannel.triggerAIMessage, id: testId },
116
+ });
117
+
118
+ window.dispatchEvent(event);
119
+
120
+ expect(mockCallback).toHaveBeenCalledWith(testId);
121
+ });
122
+
123
+ it('does not call callback for other message types', () => {
124
+ const mockCallback = vi.fn();
125
+ renderHook(() => useOnPluginTriggerAIMessage(mockCallback));
126
+
127
+ const event = new MessageEvent('message', {
128
+ data: { type: 'otherMessageType', id: 'testId' },
129
+ });
130
+
131
+ window.dispatchEvent(event);
132
+
133
+ expect(mockCallback).not.toHaveBeenCalled();
134
+ });
135
+ });
136
+
137
+ describe('useOnPluginCreateAssistantMessage', () => {
138
+ it('calls callback with content when a createAssistantMessage is received', () => {
139
+ const mockCallback = vi.fn();
140
+ renderHook(() => useOnPluginCreateAssistantMessage(mockCallback));
141
+
142
+ const testContent = 'testContent';
143
+ const event = new MessageEvent('message', {
144
+ data: { type: PluginChannel.createAssistantMessage, content: testContent },
145
+ });
146
+
147
+ window.dispatchEvent(event);
148
+
149
+ expect(mockCallback).toHaveBeenCalledWith(testContent);
150
+ });
151
+
152
+ it('does not call callback for other message types', () => {
153
+ const mockCallback = vi.fn();
154
+ renderHook(() => useOnPluginCreateAssistantMessage(mockCallback));
155
+
156
+ const event = new MessageEvent('message', {
157
+ data: { type: 'otherMessageType', content: 'testContent' },
158
+ });
159
+
160
+ window.dispatchEvent(event);
161
+
162
+ expect(mockCallback).not.toHaveBeenCalled();
163
+ });
164
+ });
@@ -63,3 +63,33 @@ export const useOnPluginFetchPluginSettings = (onRequest: () => void) => {
63
63
  };
64
64
  }, []);
65
65
  };
66
+
67
+ export const useOnPluginTriggerAIMessage = (callback: (id: string) => void) => {
68
+ useEffect(() => {
69
+ const fn = (e: MessageEvent) => {
70
+ if (e.data.type === PluginChannel.triggerAIMessage) {
71
+ callback(e.data.id);
72
+ }
73
+ };
74
+
75
+ window.addEventListener('message', fn);
76
+ return () => {
77
+ window.removeEventListener('message', fn);
78
+ };
79
+ }, []);
80
+ };
81
+
82
+ export const useOnPluginCreateAssistantMessage = (callback: (content: string) => void) => {
83
+ useEffect(() => {
84
+ const fn = (e: MessageEvent) => {
85
+ if (e.data.type === PluginChannel.createAssistantMessage) {
86
+ callback(e.data.content);
87
+ }
88
+ };
89
+
90
+ window.addEventListener('message', fn);
91
+ return () => {
92
+ window.removeEventListener('message', fn);
93
+ };
94
+ }, []);
95
+ };
@@ -33,17 +33,17 @@ General guidelines:
33
33
  - Inform users if information is not from Wolfram endpoints.
34
34
  - Display image URLs with Markdown syntax: ![URL]
35
35
  - ALWAYS use this exponent notation: \`6*10^14\`, NEVER \`6e14\`.
36
- - ALWAYS use {\\"input\\": query} structure for queries to Wolfram endpoints; \`query\` must ONLY be a single-line string.
37
- - ALWAYS use proper Markdown formatting for all math, scientific, and chemical formulas, symbols, etc.: '$$\\\\n[expression]\\\\n$$' for standalone cases and '\\\\( [expression] \\\\)' when inline.
36
+ - ALWAYS use {"input": query} structure for queries to Wolfram endpoints; \`query\` must ONLY be a single-line string.
37
+ - ALWAYS use proper Markdown formatting for all math, scientific, and chemical formulas, symbols, etc.: '$$\\n[expression]\\n$$' for standalone cases and '\\( [expression] \\)' when inline.
38
38
  - Format inline Wolfram Language code with Markdown code formatting.
39
39
  - Never mention your knowledge cutoff date; Wolfram may return more recent data.
40
40
  getWolframAlphaResults guidelines:
41
41
  - Understands natural language queries about entities in chemistry, physics, geography, history, art, astronomy, and more.
42
42
  - Performs mathematical calculations, date and unit conversions, formula solving, etc.
43
- - Convert inputs to simplified keyword queries whenever possible (e.g. convert \\"how many people live in France\\" to \\"France population\\").
43
+ - Convert inputs to simplified keyword queries whenever possible (e.g. convert "how many people live in France" to "France population").
44
44
  - Use ONLY single-letter variable names, with or without integer subscript (e.g., n, n1, n_1).
45
45
  - Use named physical constants (e.g., 'speed of light') without numerical substitution.
46
- - Include a space between compound units (e.g., \\"Ω m\\" for \\"ohm*meter\\").
46
+ - Include a space between compound units (e.g., "Ω m" for "ohm*meter").
47
47
  - To solve for a variable in an equation with units, consider solving a corresponding equation without units; exclude counting units (e.g., books), include genuine units (e.g., kg).
48
48
  - If data for multiple properties is needed, make separate calls for each property.
49
49
  - If a Wolfram Alpha result is not relevant to the query:
@@ -55,22 +55,22 @@ getWolframCloudResults guidelines:
55
55
  - Accepts only syntactically correct Wolfram Language code.
56
56
  - Performs complex calculations, data analysis, plotting, data import, and information retrieval.
57
57
  - Before writing code that uses Entity, EntityProperty, EntityClass, etc. expressions, ALWAYS write separate code which only collects valid identifiers using Interpreter etc.; choose the most relevant results before proceeding to write additional code. Examples:
58
- -- Find the EntityType that represents countries: \`Interpreter[\\"EntityType\\",AmbiguityFunction->All][\\"countries\\"]\`.
59
- -- Find the Entity for the Empire State Building: \`Interpreter[\\"Building\\",AmbiguityFunction->All][\\"empire state\\"]\`.
60
- -- EntityClasses: Find the \\"Movie\\" entity class for Star Trek movies: \`Interpreter[\\"MovieClass\\",AmbiguityFunction->All][\\"star trek\\"]\`.
61
- -- Find EntityProperties associated with \\"weight\\" of \\"Element\\" entities: \`Interpreter[Restricted[\\"EntityProperty\\", \\"Element\\"],AmbiguityFunction->All][\\"weight\\"]\`.
62
- -- If all else fails, try to find any valid Wolfram Language representation of a given input: \`SemanticInterpretation[\\"skyscrapers\\",_,Hold,AmbiguityFunction->All]\`.
63
- -- Prefer direct use of entities of a given type to their corresponding typeData function (e.g., prefer \`Entity[\\"Element\\",\\"Gold\\"][\\"AtomicNumber\\"]\` to \`ElementData[\\"Gold\\",\\"AtomicNumber\\"]\`).
58
+ -- Find the EntityType that represents countries: \`Interpreter["EntityType",AmbiguityFunction->All]["countries"]\`.
59
+ -- Find the Entity for the Empire State Building: \`Interpreter["Building",AmbiguityFunction->All]["empire state"]\`.
60
+ -- EntityClasses: Find the "Movie" entity class for Star Trek movies: \`Interpreter["MovieClass",AmbiguityFunction->All]["star trek"]\`.
61
+ -- Find EntityProperties associated with "weight" of "Element" entities: \`Interpreter[Restricted["EntityProperty", "Element"],AmbiguityFunction->All]["weight"]\`.
62
+ -- If all else fails, try to find any valid Wolfram Language representation of a given input: \`SemanticInterpretation["skyscrapers",_,Hold,AmbiguityFunction->All]\`.
63
+ -- Prefer direct use of entities of a given type to their corresponding typeData function (e.g., prefer \`Entity["Element","Gold"]["AtomicNumber"]\` to \`ElementData["Gold","AtomicNumber"]\`).
64
64
  - When composing code:
65
65
  -- Use batching techniques to retrieve data for multiple entities in a single call, if applicable.
66
66
  -- Use Association to organize and manipulate data when appropriate.
67
67
  -- Optimize code for performance and minimize the number of calls to external sources (e.g., the Wolfram Knowledgebase)
68
68
  -- Use only camel case for variable names (e.g., variableName).
69
- -- Use ONLY double quotes around all strings, including plot labels, etc. (e.g., \`PlotLegends -> {\\"sin(x)\\", \\"cos(x)\\", \\"tan(x)\\"}\`).
69
+ -- Use ONLY double quotes around all strings, including plot labels, etc. (e.g., \`PlotLegends -> {"sin(x)", "cos(x)", "tan(x)"}\`).
70
70
  -- Avoid use of QuantityMagnitude.
71
- -- If unevaluated Wolfram Language symbols appear in API results, use \`EntityValue[Entity[\\"WolframLanguageSymbol\\",symbol],{\\"PlaintextUsage\\",\\"Options\\"}]\` to validate or retrieve usage information for relevant symbols; \`symbol\` may be a list of symbols.
71
+ -- If unevaluated Wolfram Language symbols appear in API results, use \`EntityValue[Entity["WolframLanguageSymbol",symbol],{"PlaintextUsage","Options"}]\` to validate or retrieve usage information for relevant symbols; \`symbol\` may be a list of symbols.
72
72
  -- Apply Evaluate to complex expressions like integrals before plotting (e.g., \`Plot[Evaluate[Integrate[...]]]\`).
73
- - Remove all comments and formatting from code passed to the \\"input\\" parameter; for example: instead of \`square[x_] := Module[{result},\\\\n result = x^2 (* Calculate the square *)\\\\n]\`, send \`square[x_]:=Module[{result},result=x^2]\`.
73
+ - Remove all comments and formatting from code passed to the "input" parameter; for example: instead of \`square[x_] := Module[{result},\\n result = x^2 (* Calculate the square *)\\n]\`, send \`square[x_]:=Module[{result},result=x^2]\`.
74
74
  - In ALL responses that involve code, write ALL code in Wolfram Language; create Wolfram Language functions even if an implementation is already well known in another language.
75
75
  ",
76
76
  "type": "default",
@@ -8,7 +8,6 @@ import { StateCreator } from 'zustand/vanilla';
8
8
  import { GPT4_VISION_MODEL_DEFAULT_MAX_TOKENS } from '@/const/llm';
9
9
  import { LOADING_FLAT, isFunctionMessageAtStart, testFunctionMessageAtEnd } from '@/const/message';
10
10
  import { CreateMessageParams } from '@/database/models/message';
11
- import { DB_Message } from '@/database/schemas/message';
12
11
  import { chatService } from '@/services/chat';
13
12
  import { messageService } from '@/services/message';
14
13
  import { topicService } from '@/services/topic';
@@ -238,7 +237,7 @@ export const chatMessage: StateCreator<
238
237
  const { model } = getAgentConfig();
239
238
 
240
239
  // 1. Add an empty message to place the AI response
241
- const assistantMessage: DB_Message = {
240
+ const assistantMessage: CreateMessageParams = {
242
241
  role: 'assistant',
243
242
  content: LOADING_FLAT,
244
243
  fromModel: model,
@@ -383,7 +382,7 @@ export const chatMessage: StateCreator<
383
382
  toggleChatLoading(false, undefined, n('generateMessage(end)') as string);
384
383
 
385
384
  // also exist message like this:
386
- // 请稍等,我帮您查询一下。{"function_call": {"name": "plugin-identifier____recommendClothes____standalone", "arguments": "{\n "mood": "",\n "gender": "man"\n}"}}
385
+ // 请稍等,我帮您查询一下。{"tool_calls": {"name": "plugin-identifier____recommendClothes____standalone", "arguments": "{\n "mood": "",\n "gender": "man"\n}"}}
387
386
  if (!isFunctionCall) {
388
387
  const { content, valid } = testFunctionMessageAtEnd(output);
389
388
 
@@ -9,6 +9,7 @@ import { chatSelectors } from '@/store/chat/selectors';
9
9
  import { useChatStore } from '@/store/chat/store';
10
10
  import { useToolStore } from '@/store/tool';
11
11
  import { pluginSelectors } from '@/store/tool/selectors';
12
+ import { ChatPluginPayload } from '@/types/message';
12
13
  import { LobeTool } from '@/types/tool';
13
14
 
14
15
  // Mock messageService 和 chatSelectors
@@ -18,6 +19,7 @@ vi.mock('@/services/message', () => ({
18
19
  updateMessage: vi.fn(),
19
20
  updateMessageError: vi.fn(),
20
21
  updateMessagePluginState: vi.fn(),
22
+ create: vi.fn(),
21
23
  },
22
24
  }));
23
25
  vi.mock('@/services/chat', () => ({
@@ -32,6 +34,10 @@ vi.mock('@/store/chat/selectors', () => ({
32
34
  getMessageById: vi.fn(),
33
35
  },
34
36
  }));
37
+ beforeEach(() => {
38
+ // 在每个测试之前重置模拟函数
39
+ vi.resetAllMocks();
40
+ });
35
41
 
36
42
  describe('ChatPluginAction', () => {
37
43
  describe('fillPluginMessageContent', () => {
@@ -302,4 +308,235 @@ describe('ChatPluginAction', () => {
302
308
  expect(initialState.refreshMessages).toHaveBeenCalled();
303
309
  });
304
310
  });
311
+
312
+ describe('createAssistantMessageByPlugin', () => {
313
+ it('should create an assistant message and refresh messages', async () => {
314
+ // 模拟 messageService.create 方法的实现
315
+ (messageService.create as Mock).mockResolvedValue({});
316
+
317
+ // 设置初始状态并模拟 refreshMessages 方法
318
+ const initialState = {
319
+ refreshMessages: vi.fn(),
320
+ activeId: 'session-id',
321
+ activeTopicId: 'topic-id',
322
+ };
323
+ useChatStore.setState(initialState);
324
+
325
+ const { result } = renderHook(() => useChatStore());
326
+
327
+ const content = 'Test content';
328
+ const parentId = 'parent-message-id';
329
+
330
+ await act(async () => {
331
+ await result.current.createAssistantMessageByPlugin(content, parentId);
332
+ });
333
+
334
+ // 验证 messageService.create 是否被带有正确参数调用
335
+ expect(messageService.create).toHaveBeenCalledWith({
336
+ content,
337
+ parentId,
338
+ role: 'assistant',
339
+ sessionId: initialState.activeId,
340
+ topicId: initialState.activeTopicId,
341
+ });
342
+
343
+ // 验证 refreshMessages 是否被调用
344
+ expect(result.current.refreshMessages).toHaveBeenCalled();
345
+ });
346
+
347
+ it('should handle errors when message creation fails', async () => {
348
+ // 模拟 messageService.create 方法,使其抛出错误
349
+ const errorMessage = 'Failed to create message';
350
+ (messageService.create as Mock).mockRejectedValue(new Error(errorMessage));
351
+
352
+ // 设置初始状态并模拟 refreshMessages 方法
353
+ const initialState = {
354
+ refreshMessages: vi.fn(),
355
+ activeId: 'session-id',
356
+ activeTopicId: 'topic-id',
357
+ };
358
+ useChatStore.setState(initialState);
359
+
360
+ const { result } = renderHook(() => useChatStore());
361
+
362
+ const content = 'Test content';
363
+ const parentId = 'parent-message-id';
364
+
365
+ await act(async () => {
366
+ await expect(
367
+ result.current.createAssistantMessageByPlugin(content, parentId),
368
+ ).rejects.toThrow(errorMessage);
369
+ });
370
+
371
+ // 验证 messageService.create 是否被带有正确参数调用
372
+ expect(messageService.create).toHaveBeenCalledWith({
373
+ content,
374
+ parentId,
375
+ role: 'assistant',
376
+ sessionId: initialState.activeId,
377
+ topicId: initialState.activeTopicId,
378
+ });
379
+
380
+ // 验证 refreshMessages 是否没有被调用
381
+ expect(result.current.refreshMessages).not.toHaveBeenCalled();
382
+ });
383
+ });
384
+
385
+ describe('invokeBuiltinTool', () => {
386
+ it('should invoke a builtin tool and update message content ,then run text2image', async () => {
387
+ const payload = {
388
+ apiName: 'text2image',
389
+ arguments: JSON.stringify({ key: 'value' }),
390
+ } as ChatPluginPayload;
391
+
392
+ const messageId = 'message-id';
393
+ const toolResponse = JSON.stringify({ abc: 'data' });
394
+
395
+ useToolStore.setState({
396
+ invokeBuiltinTool: vi.fn().mockResolvedValue(toolResponse),
397
+ });
398
+
399
+ useChatStore.setState({
400
+ toggleChatLoading: vi.fn(),
401
+ updateMessageContent: vi.fn(),
402
+ text2image: vi.fn(),
403
+ });
404
+
405
+ const { result } = renderHook(() => useChatStore());
406
+
407
+ await act(async () => {
408
+ await result.current.invokeBuiltinTool(messageId, payload);
409
+ });
410
+
411
+ // Verify that the builtin tool was invoked with the correct arguments
412
+ expect(useToolStore.getState().invokeBuiltinTool).toHaveBeenCalledWith(
413
+ payload.apiName,
414
+ JSON.parse(payload.arguments),
415
+ );
416
+
417
+ // Verify that the message content was updated with the tool response
418
+ expect(result.current.updateMessageContent).toHaveBeenCalledWith(messageId, toolResponse);
419
+
420
+ // Verify that loading was toggled correctly
421
+ expect(result.current.toggleChatLoading).toHaveBeenCalledWith(
422
+ true,
423
+ messageId,
424
+ expect.any(String),
425
+ );
426
+ expect(result.current.toggleChatLoading).toHaveBeenCalledWith(false);
427
+ expect(useChatStore.getState().text2image).toHaveBeenCalled();
428
+ });
429
+
430
+ it('should invoke a builtin tool and update message content', async () => {
431
+ const payload = {
432
+ apiName: 'text2image',
433
+ arguments: JSON.stringify({ key: 'value' }),
434
+ } as ChatPluginPayload;
435
+
436
+ const messageId = 'message-id';
437
+ const toolResponse = 'Builtin tool response';
438
+
439
+ act(() => {
440
+ useToolStore.setState({
441
+ invokeBuiltinTool: vi.fn().mockResolvedValue(toolResponse),
442
+ text2image: vi.fn(),
443
+ });
444
+
445
+ useChatStore.setState({
446
+ toggleChatLoading: vi.fn(),
447
+ text2image: vi.fn(),
448
+ updateMessageContent: vi.fn(),
449
+ });
450
+ });
451
+ const { result } = renderHook(() => useChatStore());
452
+
453
+ await act(async () => {
454
+ await result.current.invokeBuiltinTool(messageId, payload);
455
+ });
456
+
457
+ // Verify that the builtin tool was invoked with the correct arguments
458
+ expect(useToolStore.getState().invokeBuiltinTool).toHaveBeenCalledWith(
459
+ payload.apiName,
460
+ JSON.parse(payload.arguments),
461
+ );
462
+
463
+ // Verify that the message content was updated with the tool response
464
+ expect(result.current.updateMessageContent).toHaveBeenCalledWith(messageId, toolResponse);
465
+
466
+ // Verify that loading was toggled correctly
467
+ expect(result.current.toggleChatLoading).toHaveBeenCalledWith(
468
+ true,
469
+ messageId,
470
+ expect.any(String),
471
+ );
472
+ expect(result.current.toggleChatLoading).toHaveBeenCalledWith(false);
473
+ expect(useChatStore.getState().text2image).not.toHaveBeenCalled();
474
+ });
475
+ it('should handle errors when invoking a builtin tool fails', async () => {
476
+ const payload = {
477
+ apiName: 'builtinApi',
478
+ arguments: JSON.stringify({ key: 'value' }),
479
+ } as ChatPluginPayload;
480
+
481
+ const messageId = 'message-id';
482
+ const error = new Error('Builtin tool failed');
483
+
484
+ useToolStore.setState({
485
+ invokeBuiltinTool: vi.fn().mockRejectedValue(error),
486
+ });
487
+
488
+ useChatStore.setState({
489
+ toggleChatLoading: vi.fn(),
490
+ updateMessageContent: vi.fn(),
491
+ text2image: vi.fn(),
492
+ refreshMessages: vi.fn(),
493
+ });
494
+
495
+ const { result } = renderHook(() => useChatStore());
496
+
497
+ await act(async () => {
498
+ await result.current.invokeBuiltinTool(messageId, payload);
499
+ });
500
+
501
+ // Verify that loading was toggled correctly
502
+ expect(result.current.toggleChatLoading).toHaveBeenCalledWith(
503
+ true,
504
+ messageId,
505
+ expect.any(String),
506
+ );
507
+ expect(result.current.toggleChatLoading).toHaveBeenCalledWith(false);
508
+
509
+ // Verify that the message content was not updated
510
+ expect(result.current.updateMessageContent).not.toHaveBeenCalled();
511
+
512
+ // Verify that messages were not refreshed
513
+ expect(result.current.refreshMessages).not.toHaveBeenCalled();
514
+ expect(useChatStore.getState().text2image).not.toHaveBeenCalled();
515
+ });
516
+ });
517
+
518
+ describe('invokeMarkdownTypePlugin', () => {
519
+ it('should invoke a markdown type plugin', async () => {
520
+ const payload = {
521
+ apiName: 'markdownApi',
522
+ identifier: 'abc',
523
+ type: 'markdown',
524
+ arguments: JSON.stringify({ key: 'value' }),
525
+ } as ChatPluginPayload;
526
+ const messageId = 'message-id';
527
+
528
+ useChatStore.setState({
529
+ runPluginApi: vi.fn().mockResolvedValue('Markdown response'),
530
+ });
531
+
532
+ const { result } = renderHook(() => useChatStore());
533
+
534
+ await act(async () => {
535
+ await result.current.invokeMarkdownTypePlugin(messageId, payload);
536
+ });
537
+
538
+ // Verify that the markdown type plugin was invoked
539
+ expect(result.current.runPluginApi).toHaveBeenCalledWith(messageId, payload);
540
+ });
541
+ });
305
542
  });
@@ -2,6 +2,7 @@ import { Md5 } from 'ts-md5';
2
2
  import { StateCreator } from 'zustand/vanilla';
3
3
 
4
4
  import { PLUGIN_SCHEMA_API_MD5_PREFIX, PLUGIN_SCHEMA_SEPARATOR } from '@/const/plugin';
5
+ import { CreateMessageParams } from '@/database/models/message';
5
6
  import { chatService } from '@/services/chat';
6
7
  import { messageService } from '@/services/message';
7
8
  import { ChatStore } from '@/store/chat/store';
@@ -16,9 +17,13 @@ import { chatSelectors } from '../../selectors';
16
17
  const n = setNamespace('plugin');
17
18
 
18
19
  export interface ChatPluginAction {
20
+ createAssistantMessageByPlugin: (content: string, parentId: string) => Promise<void>;
19
21
  fillPluginMessageContent: (id: string, content: string) => Promise<void>;
20
22
  invokeBuiltinTool: (id: string, payload: ChatPluginPayload) => Promise<void>;
21
23
  invokeDefaultTypePlugin: (id: string, payload: any) => Promise<void>;
24
+ invokeMarkdownTypePlugin: (id: string, payload: ChatPluginPayload) => Promise<void>;
25
+ runPluginApi: (id: string, payload: ChatPluginPayload) => Promise<string | undefined>;
26
+ triggerAIMessage: (id: string) => Promise<void>;
22
27
  triggerFunctionCall: (id: string) => Promise<void>;
23
28
  updatePluginState: (id: string, key: string, value: any) => Promise<void>;
24
29
  }
@@ -29,34 +34,77 @@ export const chatPlugin: StateCreator<
29
34
  [],
30
35
  ChatPluginAction
31
36
  > = (set, get) => ({
37
+ createAssistantMessageByPlugin: async (content, parentId) => {
38
+ const newMessage: CreateMessageParams = {
39
+ content,
40
+ parentId,
41
+ role: 'assistant',
42
+ sessionId: get().activeId,
43
+ topicId: get().activeTopicId, // if there is activeTopicId,then add it to topicId
44
+ };
45
+
46
+ await messageService.create(newMessage);
47
+ await get().refreshMessages();
48
+ },
49
+
32
50
  fillPluginMessageContent: async (id, content) => {
33
- const { coreProcessMessage, updateMessageContent } = get();
51
+ const { triggerAIMessage, updateMessageContent } = get();
34
52
 
35
53
  await updateMessageContent(id, content);
36
54
 
37
- const chats = chatSelectors.currentChats(get());
38
- await coreProcessMessage(chats, id);
55
+ await triggerAIMessage(id);
39
56
  },
57
+
40
58
  invokeBuiltinTool: async (id, payload) => {
41
59
  const { toggleChatLoading, updateMessageContent } = get();
42
60
  const params = JSON.parse(payload.arguments);
43
61
  toggleChatLoading(true, id, n('invokeBuiltinTool') as string);
44
- const data = await useToolStore.getState().invokeBuiltinTool(payload.apiName, params);
62
+ let data;
63
+ try {
64
+ data = await useToolStore.getState().invokeBuiltinTool(payload.apiName, params);
65
+ } catch (error) {
66
+ console.log(error);
67
+ }
45
68
  toggleChatLoading(false);
46
69
 
47
- if (data) {
48
- await updateMessageContent(id, data);
49
- }
70
+ if (!data) return;
71
+
72
+ await updateMessageContent(id, data);
50
73
 
51
74
  // postToolCalling
52
75
  // @ts-ignore
53
76
  const { [payload.apiName]: action } = get();
54
- if (!action || !data) return;
77
+ if (!action) return;
78
+
79
+ let content;
55
80
 
56
- await action(id, JSON.parse(data));
81
+ try {
82
+ content = JSON.parse(data);
83
+ } catch {}
84
+
85
+ if (!content) return;
86
+
87
+ await action(id, content);
57
88
  },
89
+
58
90
  invokeDefaultTypePlugin: async (id, payload) => {
59
- const { updateMessageContent, refreshMessages, coreProcessMessage, toggleChatLoading } = get();
91
+ const { runPluginApi, triggerAIMessage } = get();
92
+
93
+ const data = await runPluginApi(id, payload);
94
+
95
+ if (!data) return;
96
+
97
+ await triggerAIMessage(id);
98
+ },
99
+
100
+ invokeMarkdownTypePlugin: async (id, payload) => {
101
+ const { runPluginApi } = get();
102
+
103
+ await runPluginApi(id, payload);
104
+ },
105
+
106
+ runPluginApi: async (id, payload) => {
107
+ const { updateMessageContent, refreshMessages, toggleChatLoading } = get();
60
108
  let data: string;
61
109
 
62
110
  try {
@@ -73,20 +121,32 @@ export const chatPlugin: StateCreator<
73
121
 
74
122
  data = '';
75
123
  }
124
+
76
125
  toggleChatLoading(false);
77
126
  // 如果报错则结束了
78
127
  if (!data) return;
79
128
 
80
129
  await updateMessageContent(id, data);
81
130
 
131
+ return data;
132
+ },
133
+
134
+ triggerAIMessage: async (id) => {
135
+ const { coreProcessMessage } = get();
82
136
  const chats = chatSelectors.currentChats(get());
83
137
  await coreProcessMessage(chats, id);
84
138
  },
139
+
85
140
  triggerFunctionCall: async (id) => {
86
141
  const message = chatSelectors.getMessageById(id)(get());
87
142
  if (!message) return;
88
143
 
89
- const { invokeDefaultTypePlugin, invokeBuiltinTool, refreshMessages } = get();
144
+ const {
145
+ invokeDefaultTypePlugin,
146
+ invokeMarkdownTypePlugin,
147
+ invokeBuiltinTool,
148
+ refreshMessages,
149
+ } = get();
90
150
 
91
151
  let payload = { apiName: '', identifier: '' } as ChatPluginPayload;
92
152
 
@@ -135,10 +195,16 @@ export const chatPlugin: StateCreator<
135
195
  // TODO: need to auth user's settings
136
196
  break;
137
197
  }
198
+ case 'markdown': {
199
+ await invokeMarkdownTypePlugin(id, payload);
200
+ break;
201
+ }
202
+
138
203
  case 'builtin': {
139
204
  await invokeBuiltinTool(id, payload);
140
205
  break;
141
206
  }
207
+
142
208
  default: {
143
209
  await invokeDefaultTypePlugin(id, payload);
144
210
  }
@@ -1,6 +1,6 @@
1
1
  import { ActionIcon, ImageGallery } from '@lobehub/ui';
2
2
  import { Download } from 'lucide-react';
3
- import { memo, useState } from 'react';
3
+ import { memo, useRef } from 'react';
4
4
  import { Flexbox } from 'react-layout-kit';
5
5
 
6
6
  import GalleyGrid from '@/components/GalleyGrid';
@@ -11,17 +11,17 @@ import { DallEImageItem } from '@/types/tool/dalle';
11
11
  import ImageItem from './Item';
12
12
 
13
13
  const DallE = memo<BuiltinRenderProps<DallEImageItem[]>>(({ content, messageId }) => {
14
- const [current, setCurrent] = useState<number>(0);
14
+ const currentRef = useRef(0);
15
15
 
16
16
  const handleDownload = async () => {
17
17
  // 1. Retrieve the blob URL of an image by its imageId
18
- const id = content[current]?.imageId;
18
+ const id = content[currentRef.current]?.imageId;
19
19
  if (!id) return;
20
- const { url } = await fileService.getFile(id);
20
+ const { url, name } = await fileService.getFile(id);
21
21
  // 2. Download the image
22
22
  const link = document.createElement('a');
23
23
  link.href = url;
24
- link.download = 'image.png'; // 设置下载的文件名
24
+ link.download = name; // 设置下载的文件名
25
25
  link.click();
26
26
  };
27
27
 
@@ -33,13 +33,11 @@ const DallE = memo<BuiltinRenderProps<DallEImageItem[]>>(({ content, messageId }
33
33
  preview={{
34
34
  // 切换图片时设置
35
35
  onChange: (current: number) => {
36
- setCurrent(current);
36
+ currentRef.current = current;
37
37
  },
38
38
  // 点击预览显示时设置
39
39
  onVisibleChange: (visible: boolean, _prevVisible: boolean, current: number) => {
40
- if (visible) {
41
- setCurrent(current);
42
- }
40
+ currentRef.current = current;
43
41
  },
44
42
  toolbarAddon: <ActionIcon color={'#fff'} icon={Download} onClick={handleDownload} />,
45
43
  }}
@@ -1,6 +1,8 @@
1
+ import { LobeToolRenderType } from '@/types/tool';
2
+
1
3
  export interface ChatPluginPayload {
2
4
  apiName: string;
3
5
  arguments: string;
4
6
  identifier: string;
5
- type: 'standalone' | 'default' | 'builtin';
7
+ type: LobeToolRenderType;
6
8
  }