@lobehub/chat 0.111.5 → 0.112.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.
- package/.env.example +1 -1
- package/.i18nrc.js +1 -0
- package/CHANGELOG.md +84 -0
- package/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/docs/Deployment/Environment-Variable.md +1 -1
- package/docs/Deployment/Environment-Variable.zh-CN.md +1 -1
- package/docs/Deployment/Upstream-Sync.md +1 -1
- package/docs/Deployment/Upstream-Sync.zh-CN.md +1 -1
- package/docs/Home.md +26 -26
- package/docs/Plugins/Plugin-Development.md +1 -1
- package/docs/Plugins/Plugin-Development.zh-CN.md +1 -1
- package/docs/_Sidebar.md +20 -20
- package/locales/en_US/chat.json +12 -12
- package/locales/en_US/common.json +13 -13
- package/locales/en_US/error.json +16 -14
- package/locales/en_US/market.json +2 -2
- package/locales/en_US/migration.json +4 -4
- package/locales/en_US/plugin.json +75 -38
- package/locales/en_US/setting.json +7 -7
- package/locales/fr_FR/chat.json +11 -11
- package/locales/fr_FR/common.json +14 -14
- package/locales/fr_FR/error.json +12 -10
- package/locales/fr_FR/plugin.json +42 -5
- package/locales/fr_FR/setting.json +12 -12
- package/locales/fr_FR/welcome.json +3 -3
- package/locales/ja_JP/chat.json +1 -1
- package/locales/ja_JP/common.json +3 -3
- package/locales/ja_JP/error.json +16 -14
- package/locales/ja_JP/migration.json +4 -4
- package/locales/ja_JP/plugin.json +70 -33
- package/locales/ja_JP/setting.json +1 -1
- package/locales/ko_KR/chat.json +1 -1
- package/locales/ko_KR/common.json +3 -3
- package/locales/ko_KR/error.json +16 -14
- package/locales/ko_KR/migration.json +4 -4
- package/locales/ko_KR/plugin.json +71 -34
- package/locales/ko_KR/setting.json +1 -1
- package/locales/ru_RU/chat.json +1 -1
- package/locales/ru_RU/common.json +3 -3
- package/locales/ru_RU/error.json +16 -14
- package/locales/ru_RU/market.json +2 -2
- package/locales/ru_RU/migration.json +4 -4
- package/locales/ru_RU/plugin.json +71 -34
- package/locales/ru_RU/setting.json +1 -1
- package/locales/zh_CN/chat.json +1 -1
- package/locales/zh_CN/common.json +1 -1
- package/locales/zh_CN/error.json +2 -0
- package/locales/zh_CN/plugin.json +42 -5
- package/locales/zh_CN/setting.json +1 -1
- package/locales/zh_TW/chat.json +1 -1
- package/locales/zh_TW/common.json +3 -3
- package/locales/zh_TW/error.json +16 -14
- package/locales/zh_TW/market.json +2 -2
- package/locales/zh_TW/migration.json +4 -4
- package/locales/zh_TW/plugin.json +98 -61
- package/locales/zh_TW/setting.json +1 -1
- package/package.json +3 -1
- package/scripts/docsWorkflow/index.ts +5 -2
- package/scripts/docsWorkflow/toc.ts +2 -2
- package/scripts/i18nWorkflow/genDiff.ts +7 -8
- package/scripts/i18nWorkflow/utils.ts +0 -14
- package/scripts/readmeWorkflow/syncPluginIndex.ts +9 -11
- package/src/app/api/plugins/route.ts +2 -4
- package/src/app/api/proxy/route.ts +10 -0
- package/src/app/chat/features/ChatInput/ActionBar/Tools/ToolItem.tsx +3 -8
- package/src/app/chat/features/ChatInput/ActionBar/Tools/index.tsx +3 -3
- package/src/app/chat/features/Conversation/ChatList/Error/index.tsx +6 -0
- package/src/app/chat/features/Conversation/ChatList/Messages/Function.tsx +1 -1
- package/src/app/chat/features/Conversation/ChatList/Plugins/Inspector/Settings.tsx +7 -5
- package/src/app/chat/features/Conversation/ChatList/Plugins/Inspector/index.tsx +2 -2
- package/src/app/chat/features/Conversation/ChatList/Plugins/Render/DefaultType/index.tsx +6 -4
- package/src/app/chat/features/Conversation/ChatList/Plugins/Render/index.tsx +2 -2
- package/src/app/chat/features/Migration/ExportConfigButton.tsx +1 -1
- package/src/app/market/features/AgentDetailContent/AgentInfo/index.tsx +1 -0
- package/src/app/market/features/AgentDetailContent/AgentInfo/style.ts +1 -9
- package/src/app/settings/common/Common.tsx +5 -7
- package/src/app/settings/features/UpgradeAlert.tsx +2 -2
- package/src/components/FileList/FileItem.tsx +4 -2
- package/src/components/HotKeys/index.tsx +5 -4
- package/src/const/plugin.ts +1 -0
- package/src/const/url.ts +1 -0
- package/src/const/version.ts +0 -3
- package/src/features/AgentSetting/AgentPlugin/AddPluginButton.tsx +2 -2
- package/src/features/AgentSetting/AgentPlugin/LocalPluginItem.tsx +19 -53
- package/src/features/AgentSetting/AgentPlugin/index.tsx +5 -11
- package/src/features/AgentSetting/store/initialState.ts +1 -1
- package/src/features/PluginDetailModal/APIs.tsx +44 -0
- package/src/features/PluginDetailModal/Meta.tsx +59 -0
- package/src/features/PluginDetailModal/index.tsx +79 -0
- package/src/features/PluginDevModal/PluginPreview.tsx +8 -10
- package/src/features/PluginDevModal/UrlManifestForm.tsx +131 -0
- package/src/features/PluginDevModal/index.tsx +31 -32
- package/src/features/PluginSettings/index.tsx +1 -1
- package/src/features/PluginStore/AddPluginButton.tsx +47 -0
- package/src/features/PluginStore/InstalledPluginList.tsx +54 -0
- package/src/features/PluginStore/OnlineList.tsx +95 -0
- package/src/features/PluginStore/PluginItem/Action.tsx +118 -0
- package/src/features/PluginStore/PluginItem/EditCustomPlugin.tsx +50 -0
- package/src/features/PluginStore/PluginItem/PluginTag.tsx +58 -0
- package/src/features/PluginStore/PluginItem/index.tsx +73 -0
- package/src/features/PluginStore/index.tsx +20 -92
- package/src/hooks/usePlatform.ts +15 -0
- package/src/locales/default/chat.ts +1 -1
- package/src/locales/default/common.ts +1 -1
- package/src/locales/default/error.ts +2 -0
- package/src/locales/default/plugin.ts +41 -4
- package/src/locales/default/setting.ts +1 -1
- package/src/services/__tests__/__snapshots__/plugin.test.ts.snap +154 -0
- package/src/services/__tests__/chat.test.ts +1 -1
- package/src/services/__tests__/global.test.ts +57 -0
- package/src/services/__tests__/openai/OpenAPI_V3.json +245 -0
- package/src/services/__tests__/openai/plugin.json +22 -0
- package/src/services/__tests__/plugin.test.ts +90 -1
- package/src/services/__tests__/share.test.ts +86 -0
- package/src/services/_url.ts +2 -0
- package/src/services/global.ts +15 -0
- package/src/services/plugin.ts +99 -15
- package/src/services/share.ts +34 -0
- package/src/store/chat/actions/enhance.test.ts +15 -15
- package/src/store/chat/actions/message.test.ts +3 -2
- package/src/store/chat/actions/plugin.test.ts +281 -0
- package/src/store/chat/actions/plugin.ts +22 -3
- package/src/store/chat/actions/share.ts +2 -2
- package/src/store/chat/actions/topic.test.ts +12 -4
- package/src/store/global/slices/common.ts +2 -2
- package/src/store/session/selectors.ts +1 -1
- package/src/store/session/slices/agent/selectors.ts +1 -1
- package/src/store/tool/helpers.ts +8 -1
- package/src/store/tool/slices/customPlugin/action.ts +4 -1
- package/src/store/tool/slices/plugin/selectors.test.ts +22 -3
- package/src/store/tool/slices/plugin/selectors.ts +24 -5
- package/src/store/tool/slices/store/selectors.test.ts +18 -1
- package/src/store/tool/slices/store/selectors.ts +14 -3
- package/src/styles/antdOverride.ts +7 -0
- package/src/types/openai/chat.ts +0 -1
- package/src/types/openai/plugin.ts +57 -0
- package/src/types/tool/index.ts +1 -2
- package/src/types/tool/plugin.ts +13 -1
- package/src/types/tool/tool.ts +1 -0
- package/src/types/translate.ts +0 -1
- package/src/utils/platform.ts +8 -5
- package/src/features/PluginDevModal/UrlModeForm.tsx +0 -98
- package/src/features/PluginSettingsModal/index.tsx +0 -59
- package/src/features/PluginStore/PluginItem.tsx +0 -82
- package/src/features/PluginStore/PluginSettings.tsx +0 -40
- package/src/services/latestVersion.ts +0 -11
- package/src/services/shareGPT.ts +0 -28
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { ShareGPTConversation } from '@/types/share';
|
|
2
|
+
import { parseMarkdown } from '@/utils/parseMarkdown';
|
|
3
|
+
|
|
4
|
+
export const SHARE_GPT_URL = 'https://sharegpt.com/api/conversations';
|
|
5
|
+
|
|
6
|
+
class ShareGPTService {
|
|
7
|
+
public async createShareGPTUrl(conversation: ShareGPTConversation) {
|
|
8
|
+
const items = [];
|
|
9
|
+
|
|
10
|
+
for (const item of conversation.items) {
|
|
11
|
+
items.push({
|
|
12
|
+
from: item.from,
|
|
13
|
+
value: item.from === 'gpt' ? await parseMarkdown(item.value) : item.value,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const res = await fetch(SHARE_GPT_URL, {
|
|
18
|
+
body: JSON.stringify({ ...conversation, items }),
|
|
19
|
+
headers: {
|
|
20
|
+
'Content-Type': 'application/json',
|
|
21
|
+
},
|
|
22
|
+
method: 'POST',
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const { id } = await res.json();
|
|
26
|
+
|
|
27
|
+
if (!id) throw new Error('Failed to create ShareGPT URL');
|
|
28
|
+
|
|
29
|
+
// short link to the ShareGPT post
|
|
30
|
+
return `https://shareg.pt/${id}`;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const shareGPTService = new ShareGPTService();
|
|
@@ -71,23 +71,23 @@ describe('ChatEnhanceAction', () => {
|
|
|
71
71
|
const messageContent = 'Hello World';
|
|
72
72
|
const detectedLang = 'en-US';
|
|
73
73
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
74
|
+
act(() => {
|
|
75
|
+
useChatStore.setState({
|
|
76
|
+
messages: [
|
|
77
|
+
{
|
|
78
|
+
id: messageId,
|
|
79
|
+
content: messageContent,
|
|
80
|
+
createdAt: Date.now(),
|
|
81
|
+
updatedAt: Date.now(),
|
|
82
|
+
role: 'user',
|
|
83
|
+
sessionId: 'test',
|
|
84
|
+
topicId: 'test',
|
|
85
|
+
meta: {},
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
});
|
|
88
89
|
});
|
|
89
90
|
|
|
90
|
-
// 模拟语言检测和翻译结果
|
|
91
91
|
(chatService.fetchPresetTaskResult as Mock).mockImplementation(({ params }) => {
|
|
92
92
|
if (params === chainLangDetect(messageContent)) {
|
|
93
93
|
return Promise.resolve(detectedLang);
|
|
@@ -383,8 +383,9 @@ describe('chatMessage actions', () => {
|
|
|
383
383
|
const toggleChatLoadingSpy = vi.spyOn(result.current, 'toggleChatLoading');
|
|
384
384
|
const abortController = new AbortController();
|
|
385
385
|
|
|
386
|
-
|
|
387
|
-
|
|
386
|
+
act(() => {
|
|
387
|
+
useChatStore.setState({ abortController });
|
|
388
|
+
});
|
|
388
389
|
|
|
389
390
|
await act(async () => {
|
|
390
391
|
result.current.stopGenerateMessage();
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { act, renderHook } from '@testing-library/react';
|
|
2
|
+
import { Md5 } from 'ts-md5';
|
|
3
|
+
import { Mock, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import { PLUGIN_SCHEMA_API_MD5_PREFIX, PLUGIN_SCHEMA_SEPARATOR } from '@/const/plugin';
|
|
6
|
+
import { chatService } from '@/services/chat';
|
|
7
|
+
import { messageService } from '@/services/message';
|
|
8
|
+
import { chatSelectors } from '@/store/chat/selectors';
|
|
9
|
+
import { useChatStore } from '@/store/chat/store';
|
|
10
|
+
import { useToolStore } from '@/store/tool';
|
|
11
|
+
import { pluginSelectors } from '@/store/tool/selectors';
|
|
12
|
+
import { LobeTool } from '@/types/tool';
|
|
13
|
+
|
|
14
|
+
// Mock messageService 和 chatSelectors
|
|
15
|
+
vi.mock('@/services/message', () => ({
|
|
16
|
+
messageService: {
|
|
17
|
+
updateMessageContent: vi.fn(),
|
|
18
|
+
updateMessage: vi.fn(),
|
|
19
|
+
updateMessageError: vi.fn(),
|
|
20
|
+
updateMessagePluginState: vi.fn(),
|
|
21
|
+
},
|
|
22
|
+
}));
|
|
23
|
+
vi.mock('@/services/chat', () => ({
|
|
24
|
+
chatService: {
|
|
25
|
+
runPluginApi: vi.fn(),
|
|
26
|
+
},
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
vi.mock('@/store/chat/selectors', () => ({
|
|
30
|
+
chatSelectors: {
|
|
31
|
+
currentChats: vi.fn(),
|
|
32
|
+
getMessageById: vi.fn(),
|
|
33
|
+
},
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
describe('ChatPluginAction', () => {
|
|
37
|
+
describe('fillPluginMessageContent', () => {
|
|
38
|
+
it('should update message content and process the message', async () => {
|
|
39
|
+
// 设置模拟函数的返回值
|
|
40
|
+
const mockCurrentChats: any[] = [];
|
|
41
|
+
(chatSelectors.currentChats as Mock).mockReturnValue(mockCurrentChats);
|
|
42
|
+
|
|
43
|
+
// 设置初始状态
|
|
44
|
+
const initialState = {
|
|
45
|
+
messages: [],
|
|
46
|
+
coreProcessMessage: vi.fn(),
|
|
47
|
+
refreshMessages: vi.fn(),
|
|
48
|
+
};
|
|
49
|
+
useChatStore.setState(initialState);
|
|
50
|
+
|
|
51
|
+
const { result } = renderHook(() => useChatStore());
|
|
52
|
+
|
|
53
|
+
const messageId = 'message-id';
|
|
54
|
+
const newContent = 'Updated content';
|
|
55
|
+
|
|
56
|
+
await act(async () => {
|
|
57
|
+
await result.current.fillPluginMessageContent(messageId, newContent);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// 验证 messageService.updateMessageContent 是否被正确调用
|
|
61
|
+
expect(messageService.updateMessageContent).toHaveBeenCalledWith(messageId, newContent);
|
|
62
|
+
|
|
63
|
+
// 验证 refreshMessages 是否被调用
|
|
64
|
+
expect(result.current.refreshMessages).toHaveBeenCalled();
|
|
65
|
+
|
|
66
|
+
// 验证 coreProcessMessage 是否被正确调用
|
|
67
|
+
expect(result.current.coreProcessMessage).toHaveBeenCalledWith(mockCurrentChats, messageId);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('runPluginDefaultType', () => {
|
|
72
|
+
it('should run the default plugin type and update message content', async () => {
|
|
73
|
+
const pluginPayload = { apiName: 'testApi', arguments: { key: 'value' } };
|
|
74
|
+
const messageId = 'message-id';
|
|
75
|
+
const pluginApiResponse = 'Plugin API response';
|
|
76
|
+
|
|
77
|
+
const initialState = {
|
|
78
|
+
refreshMessages: vi.fn(),
|
|
79
|
+
coreProcessMessage: vi.fn(),
|
|
80
|
+
toggleChatLoading: vi.fn(),
|
|
81
|
+
};
|
|
82
|
+
useChatStore.setState(initialState);
|
|
83
|
+
|
|
84
|
+
(chatService.runPluginApi as Mock).mockResolvedValue(pluginApiResponse);
|
|
85
|
+
|
|
86
|
+
const { result } = renderHook(() => useChatStore());
|
|
87
|
+
|
|
88
|
+
await act(async () => {
|
|
89
|
+
await result.current.runPluginDefaultType(messageId, pluginPayload);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
expect(initialState.toggleChatLoading).toHaveBeenCalledWith(
|
|
93
|
+
true,
|
|
94
|
+
messageId,
|
|
95
|
+
expect.any(String),
|
|
96
|
+
);
|
|
97
|
+
expect(chatService.runPluginApi).toHaveBeenCalledWith(pluginPayload, { signal: undefined });
|
|
98
|
+
expect(messageService.updateMessageContent).toHaveBeenCalledWith(
|
|
99
|
+
messageId,
|
|
100
|
+
pluginApiResponse,
|
|
101
|
+
);
|
|
102
|
+
expect(initialState.refreshMessages).toHaveBeenCalled();
|
|
103
|
+
expect(initialState.coreProcessMessage).toHaveBeenCalled();
|
|
104
|
+
expect(initialState.toggleChatLoading).toHaveBeenCalledWith(false);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should handle errors when the plugin API call fails', async () => {
|
|
108
|
+
const pluginPayload = { apiName: 'testApi', arguments: { key: 'value' } };
|
|
109
|
+
const messageId = 'message-id';
|
|
110
|
+
const error = new Error('API call failed');
|
|
111
|
+
|
|
112
|
+
const initialState = {
|
|
113
|
+
refreshMessages: vi.fn(),
|
|
114
|
+
coreProcessMessage: vi.fn(),
|
|
115
|
+
toggleChatLoading: vi.fn(),
|
|
116
|
+
};
|
|
117
|
+
useChatStore.setState(initialState);
|
|
118
|
+
|
|
119
|
+
(chatService.runPluginApi as Mock).mockRejectedValue(error);
|
|
120
|
+
|
|
121
|
+
const { result } = renderHook(() => useChatStore());
|
|
122
|
+
|
|
123
|
+
await act(async () => {
|
|
124
|
+
await result.current.runPluginDefaultType(messageId, pluginPayload);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
expect(initialState.toggleChatLoading).toHaveBeenCalledWith(
|
|
128
|
+
true,
|
|
129
|
+
messageId,
|
|
130
|
+
expect.any(String),
|
|
131
|
+
);
|
|
132
|
+
expect(chatService.runPluginApi).toHaveBeenCalledWith(pluginPayload, { signal: undefined });
|
|
133
|
+
expect(messageService.updateMessageError).toHaveBeenCalledWith(messageId, error);
|
|
134
|
+
expect(initialState.refreshMessages).toHaveBeenCalled();
|
|
135
|
+
expect(initialState.toggleChatLoading).toHaveBeenCalledWith(false);
|
|
136
|
+
expect(initialState.coreProcessMessage).not.toHaveBeenCalled(); // 确保在错误情况下不调用此方法
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe('triggerFunctionCall', () => {
|
|
141
|
+
it('should trigger a function call and update the plugin message accordingly', async () => {
|
|
142
|
+
const messageId = 'message-id';
|
|
143
|
+
const messageContent = `{"function_call": {"name": "pluginName${PLUGIN_SCHEMA_SEPARATOR}apiName", "arguments": {"key": "value"}}}`;
|
|
144
|
+
const messagePluginPayload = {
|
|
145
|
+
apiName: 'apiName',
|
|
146
|
+
identifier: 'pluginName',
|
|
147
|
+
type: 'default',
|
|
148
|
+
arguments: { key: 'value' },
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const initialState = {
|
|
152
|
+
refreshMessages: vi.fn(),
|
|
153
|
+
runPluginDefaultType: vi.fn(),
|
|
154
|
+
};
|
|
155
|
+
useChatStore.setState(initialState);
|
|
156
|
+
|
|
157
|
+
(chatSelectors.getMessageById as Mock).mockImplementation(() => () => ({
|
|
158
|
+
id: messageId,
|
|
159
|
+
content: messageContent,
|
|
160
|
+
}));
|
|
161
|
+
|
|
162
|
+
const { result } = renderHook(() => useChatStore());
|
|
163
|
+
|
|
164
|
+
await act(async () => {
|
|
165
|
+
await result.current.triggerFunctionCall(messageId);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
expect(chatSelectors.getMessageById).toHaveBeenCalledWith(messageId);
|
|
169
|
+
expect(messageService.updateMessage).toHaveBeenCalledWith(messageId, {
|
|
170
|
+
content: '',
|
|
171
|
+
plugin: messagePluginPayload,
|
|
172
|
+
role: 'function',
|
|
173
|
+
});
|
|
174
|
+
expect(initialState.refreshMessages).toHaveBeenCalled();
|
|
175
|
+
expect(initialState.runPluginDefaultType).toHaveBeenCalledWith(
|
|
176
|
+
messageId,
|
|
177
|
+
messagePluginPayload,
|
|
178
|
+
);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should handle function call with MD5 prefixed API name', async () => {
|
|
182
|
+
const messageId = 'message-id';
|
|
183
|
+
const apiName = 'originalApiName';
|
|
184
|
+
const id = 'pluginIdentifier';
|
|
185
|
+
const md5ApiName = PLUGIN_SCHEMA_API_MD5_PREFIX + Md5.hashStr(apiName).toString();
|
|
186
|
+
const messageContent = JSON.stringify({
|
|
187
|
+
function_call: { name: id + PLUGIN_SCHEMA_SEPARATOR + md5ApiName, arguments: {} },
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const plugin = { identifier: id, manifest: { api: [{ name: apiName }] } } as LobeTool;
|
|
191
|
+
|
|
192
|
+
useToolStore.setState({ installedPlugins: [plugin] });
|
|
193
|
+
|
|
194
|
+
useChatStore.setState({ refreshMessages: vi.fn(), runPluginDefaultType: vi.fn() });
|
|
195
|
+
|
|
196
|
+
(chatSelectors.getMessageById as Mock).mockImplementation(() => () => ({
|
|
197
|
+
id: messageId,
|
|
198
|
+
content: messageContent,
|
|
199
|
+
}));
|
|
200
|
+
|
|
201
|
+
const { result } = renderHook(() => useChatStore());
|
|
202
|
+
|
|
203
|
+
await act(async () => {
|
|
204
|
+
await result.current.triggerFunctionCall(messageId);
|
|
205
|
+
});
|
|
206
|
+
expect(result.current.refreshMessages).toHaveBeenCalled();
|
|
207
|
+
|
|
208
|
+
expect(messageService.updateMessage).toHaveBeenCalledWith(
|
|
209
|
+
messageId,
|
|
210
|
+
expect.objectContaining({
|
|
211
|
+
// 确保正确的 API 名称被设置
|
|
212
|
+
plugin: expect.objectContaining({ apiName }),
|
|
213
|
+
}),
|
|
214
|
+
);
|
|
215
|
+
expect(result.current.runPluginDefaultType).toHaveBeenCalledWith(
|
|
216
|
+
messageId,
|
|
217
|
+
expect.objectContaining({
|
|
218
|
+
apiName: apiName,
|
|
219
|
+
}),
|
|
220
|
+
);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('should handle standalone plugin type', async () => {
|
|
224
|
+
const messageId = 'message-id';
|
|
225
|
+
const messageContent = JSON.stringify({
|
|
226
|
+
function_call: {
|
|
227
|
+
name: `pluginName${PLUGIN_SCHEMA_SEPARATOR}apiName${PLUGIN_SCHEMA_SEPARATOR}standalone`,
|
|
228
|
+
arguments: {},
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
useChatStore.setState({
|
|
233
|
+
refreshMessages: vi.fn(),
|
|
234
|
+
runPluginDefaultType: vi.fn(),
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
(chatSelectors.getMessageById as Mock).mockImplementation(() => () => ({
|
|
238
|
+
id: messageId,
|
|
239
|
+
content: messageContent,
|
|
240
|
+
}));
|
|
241
|
+
|
|
242
|
+
const { result } = renderHook(() => useChatStore());
|
|
243
|
+
|
|
244
|
+
await act(async () => {
|
|
245
|
+
await result.current.triggerFunctionCall(messageId);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// 验证 refreshMessages 是否被调用
|
|
249
|
+
expect(result.current.refreshMessages).toHaveBeenCalled();
|
|
250
|
+
|
|
251
|
+
// 验证 runPluginDefaultType 是否没有被调用,因为类型是 standalone
|
|
252
|
+
expect(result.current.runPluginDefaultType).not.toHaveBeenCalled();
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
describe('updatePluginState', () => {
|
|
257
|
+
it('should update the plugin state for a message', async () => {
|
|
258
|
+
const messageId = 'message-id';
|
|
259
|
+
const pluginStateKey = 'key';
|
|
260
|
+
const pluginStateValue = 'value';
|
|
261
|
+
|
|
262
|
+
const initialState = {
|
|
263
|
+
refreshMessages: vi.fn(),
|
|
264
|
+
};
|
|
265
|
+
useChatStore.setState(initialState);
|
|
266
|
+
|
|
267
|
+
const { result } = renderHook(() => useChatStore());
|
|
268
|
+
|
|
269
|
+
await act(async () => {
|
|
270
|
+
await result.current.updatePluginState(messageId, pluginStateKey, pluginStateValue);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
expect(messageService.updateMessagePluginState).toHaveBeenCalledWith(
|
|
274
|
+
messageId,
|
|
275
|
+
pluginStateKey,
|
|
276
|
+
pluginStateValue,
|
|
277
|
+
);
|
|
278
|
+
expect(initialState.refreshMessages).toHaveBeenCalled();
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
});
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import { Md5 } from 'ts-md5';
|
|
1
2
|
import { StateCreator } from 'zustand/vanilla';
|
|
2
3
|
|
|
3
|
-
import { PLUGIN_SCHEMA_SEPARATOR } from '@/const/plugin';
|
|
4
|
+
import { PLUGIN_SCHEMA_API_MD5_PREFIX, PLUGIN_SCHEMA_SEPARATOR } from '@/const/plugin';
|
|
4
5
|
import { chatService } from '@/services/chat';
|
|
5
6
|
import { messageService } from '@/services/message';
|
|
6
7
|
import { ChatStore } from '@/store/chat/store';
|
|
8
|
+
import { useToolStore } from '@/store/tool';
|
|
9
|
+
import { pluginSelectors } from '@/store/tool/selectors';
|
|
7
10
|
import { ChatPluginPayload } from '@/types/chatMessage';
|
|
8
11
|
import { OpenAIFunctionCall } from '@/types/openai/functionCall';
|
|
9
12
|
import { setNamespace } from '@/utils/storeDebug';
|
|
@@ -37,12 +40,18 @@ export const chatPlugin: StateCreator<
|
|
|
37
40
|
runPluginDefaultType: async (id, payload) => {
|
|
38
41
|
const { refreshMessages, coreProcessMessage, toggleChatLoading } = get();
|
|
39
42
|
let data: string;
|
|
43
|
+
|
|
40
44
|
try {
|
|
41
45
|
const abortController = toggleChatLoading(true, id, n('fetchPlugin') as string);
|
|
42
46
|
data = await chatService.runPluginApi(payload, { signal: abortController?.signal });
|
|
43
47
|
} catch (error) {
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
const err = error as Error;
|
|
49
|
+
|
|
50
|
+
// ignore the aborted request error
|
|
51
|
+
if (!err.message.includes('The user aborted a request.')) {
|
|
52
|
+
await messageService.updateMessageError(id, error as any);
|
|
53
|
+
await refreshMessages();
|
|
54
|
+
}
|
|
46
55
|
|
|
47
56
|
data = '';
|
|
48
57
|
}
|
|
@@ -79,6 +88,16 @@ export const chatPlugin: StateCreator<
|
|
|
79
88
|
identifier,
|
|
80
89
|
type: (type ?? 'default') as any,
|
|
81
90
|
};
|
|
91
|
+
|
|
92
|
+
// if the apiName is md5, try to find the correct apiName in the plugins
|
|
93
|
+
if (apiName.startsWith(PLUGIN_SCHEMA_API_MD5_PREFIX)) {
|
|
94
|
+
const md5 = apiName.replace(PLUGIN_SCHEMA_API_MD5_PREFIX, '');
|
|
95
|
+
const manifest = pluginSelectors.getPluginManifestById(identifier)(useToolStore.getState());
|
|
96
|
+
|
|
97
|
+
const api = manifest?.api.find((api) => Md5.hashStr(api.name).toString() === md5);
|
|
98
|
+
if (!api) return;
|
|
99
|
+
payload.apiName = api.name;
|
|
100
|
+
}
|
|
82
101
|
} else {
|
|
83
102
|
if (message.plugin) payload = message.plugin;
|
|
84
103
|
}
|
|
@@ -3,7 +3,7 @@ import { produce } from 'immer';
|
|
|
3
3
|
import { StateCreator } from 'zustand/vanilla';
|
|
4
4
|
|
|
5
5
|
import { DEFAULT_USER_AVATAR_URL } from '@/const/meta';
|
|
6
|
-
import {
|
|
6
|
+
import { shareGPTService } from '@/services/share';
|
|
7
7
|
import { useSessionStore } from '@/store/session';
|
|
8
8
|
import { agentSelectors } from '@/store/session/selectors';
|
|
9
9
|
import { ShareGPTConversation } from '@/types/share';
|
|
@@ -102,7 +102,7 @@ export const chatShare: StateCreator<ChatStore, [['zustand/devtools', never]], [
|
|
|
102
102
|
|
|
103
103
|
set({ shareLoading: true });
|
|
104
104
|
|
|
105
|
-
const res = await
|
|
105
|
+
const res = await shareGPTService.createShareGPTUrl({
|
|
106
106
|
avatarUrl: avatar || DEFAULT_USER_AVATAR_URL,
|
|
107
107
|
items: shareMsgs,
|
|
108
108
|
});
|
|
@@ -85,7 +85,9 @@ describe('topic action', () => {
|
|
|
85
85
|
describe('saveToTopic', () => {
|
|
86
86
|
it('should not create a topic if there are no messages', async () => {
|
|
87
87
|
const { result } = renderHook(() => useChatStore());
|
|
88
|
-
|
|
88
|
+
act(() => {
|
|
89
|
+
useChatStore.setState({ messages: [] });
|
|
90
|
+
});
|
|
89
91
|
|
|
90
92
|
const createTopicSpy = vi.spyOn(topicService, 'createTopic');
|
|
91
93
|
|
|
@@ -98,7 +100,9 @@ describe('topic action', () => {
|
|
|
98
100
|
it('should create a topic and bind messages to it', async () => {
|
|
99
101
|
const { result } = renderHook(() => useChatStore());
|
|
100
102
|
const messages = [{ id: 'message1' }, { id: 'message2' }] as ChatMessage[];
|
|
101
|
-
|
|
103
|
+
act(() => {
|
|
104
|
+
useChatStore.setState({ messages, activeId: 'session-id' });
|
|
105
|
+
});
|
|
102
106
|
|
|
103
107
|
const createTopicSpy = vi
|
|
104
108
|
.spyOn(topicService, 'createTopic')
|
|
@@ -133,8 +137,10 @@ describe('topic action', () => {
|
|
|
133
137
|
it('should call mutate to refresh topics', async () => {
|
|
134
138
|
const { result } = renderHook(() => useChatStore());
|
|
135
139
|
const activeId = 'test-session-id';
|
|
136
|
-
useChatStore.setState({ activeId });
|
|
137
140
|
|
|
141
|
+
act(() => {
|
|
142
|
+
useChatStore.setState({ activeId });
|
|
143
|
+
});
|
|
138
144
|
// Mock the mutate function to resolve immediately
|
|
139
145
|
|
|
140
146
|
await act(async () => {
|
|
@@ -148,8 +154,10 @@ describe('topic action', () => {
|
|
|
148
154
|
it('should handle errors during refreshing topics', async () => {
|
|
149
155
|
const { result } = renderHook(() => useChatStore());
|
|
150
156
|
const activeId = 'test-session-id';
|
|
151
|
-
useChatStore.setState({ activeId });
|
|
152
157
|
|
|
158
|
+
act(() => {
|
|
159
|
+
useChatStore.setState({ activeId });
|
|
160
|
+
});
|
|
153
161
|
// Mock the mutate function to throw an error
|
|
154
162
|
// 设置模拟错误
|
|
155
163
|
(mutate as Mock).mockImplementation(() => {
|
|
@@ -4,7 +4,7 @@ import useSWR, { SWRResponse } from 'swr';
|
|
|
4
4
|
import type { StateCreator } from 'zustand/vanilla';
|
|
5
5
|
|
|
6
6
|
import { CURRENT_VERSION } from '@/const/version';
|
|
7
|
-
import {
|
|
7
|
+
import { globalService } from '@/services/global';
|
|
8
8
|
import { merge } from '@/utils/merge';
|
|
9
9
|
import { setNamespace } from '@/utils/storeDebug';
|
|
10
10
|
|
|
@@ -72,7 +72,7 @@ export const createCommonSlice: StateCreator<
|
|
|
72
72
|
);
|
|
73
73
|
},
|
|
74
74
|
useCheckLatestVersion: () =>
|
|
75
|
-
useSWR('checkLatestVersion',
|
|
75
|
+
useSWR('checkLatestVersion', globalService.getLatestVersion, {
|
|
76
76
|
onSuccess: (data: string) => {
|
|
77
77
|
if (gt(data, CURRENT_VERSION))
|
|
78
78
|
set({ hasNewVersion: true, latestVersion: data }, false, n('checkLatestVersion'));
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { agentSelectors } from './slices/agent/selectors';
|
|
2
|
-
export { sessionExportSelectors,sessionSelectors } from './slices/session/selectors';
|
|
2
|
+
export { sessionExportSelectors, sessionSelectors } from './slices/session/selectors';
|
|
@@ -78,7 +78,7 @@ const currentAgentMeta = (s: SessionStore): MetaData => {
|
|
|
78
78
|
avatar: isInbox ? DEFAULT_INBOX_AVATAR : DEFAULT_AVATAR,
|
|
79
79
|
backgroundColor: DEFAULT_BACKGROUND_COLOR,
|
|
80
80
|
description: isInbox ? t('inbox.desc') : currentAgentSystemRole(s) || t('noDescription'),
|
|
81
|
-
title: isInbox ? t('inbox') : t('defaultSession'),
|
|
81
|
+
title: isInbox ? t('inbox.title', { ns: 'chat' }) : t('defaultSession'),
|
|
82
82
|
};
|
|
83
83
|
|
|
84
84
|
const session = sessionSelectors.currentSession(s);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
|
|
1
|
+
import { LobeChatPluginManifest, PluginSchema } from '@lobehub/chat-plugin-sdk';
|
|
2
2
|
|
|
3
3
|
import { LobeTool } from '@/types/tool';
|
|
4
4
|
|
|
@@ -6,15 +6,22 @@ const getPluginFormList = (list: LobeTool[], id: string) => list?.find((p) => p.
|
|
|
6
6
|
|
|
7
7
|
const getPluginTitle = (meta?: LobeChatPluginManifest['meta']) => meta?.title;
|
|
8
8
|
const getPluginDesc = (meta?: LobeChatPluginManifest['meta']) => meta?.description;
|
|
9
|
+
|
|
10
|
+
const getPluginTags = (meta?: LobeChatPluginManifest['meta']) => meta?.tags;
|
|
9
11
|
const getPluginAvatar = (meta?: LobeChatPluginManifest['meta']) => meta?.avatar || '🧩';
|
|
10
12
|
|
|
11
13
|
const isCustomPlugin = (id: string, pluginList: LobeTool[]) =>
|
|
12
14
|
pluginList.some((i) => i.identifier === id && i.type === 'customPlugin');
|
|
13
15
|
|
|
16
|
+
const isSettingSchemaNonEmpty = (schema?: PluginSchema) =>
|
|
17
|
+
schema?.properties && Object.keys(schema.properties).length > 0;
|
|
18
|
+
|
|
14
19
|
export const pluginHelpers = {
|
|
15
20
|
getPluginAvatar,
|
|
16
21
|
getPluginDesc,
|
|
17
22
|
getPluginFormList,
|
|
23
|
+
getPluginTags,
|
|
18
24
|
getPluginTitle,
|
|
19
25
|
isCustomPlugin,
|
|
26
|
+
isSettingSchemaNonEmpty,
|
|
20
27
|
};
|
|
@@ -44,7 +44,10 @@ export const createCustomPluginSlice: StateCreator<
|
|
|
44
44
|
const { refreshPlugins, updateInstallLoadingState } = get();
|
|
45
45
|
try {
|
|
46
46
|
updateInstallLoadingState(id, true);
|
|
47
|
-
const manifest = await pluginService.getPluginManifest(
|
|
47
|
+
const manifest = await pluginService.getPluginManifest(
|
|
48
|
+
plugin.customParams?.manifestUrl,
|
|
49
|
+
plugin.customParams?.useProxy,
|
|
50
|
+
);
|
|
48
51
|
updateInstallLoadingState(id, false);
|
|
49
52
|
|
|
50
53
|
await pluginService.updatePluginManifest(id, manifest);
|
|
@@ -27,7 +27,6 @@ const mockState = {
|
|
|
27
27
|
manifest: {
|
|
28
28
|
identifier: 'plugin-2',
|
|
29
29
|
api: [{ name: 'api-2' }],
|
|
30
|
-
type: 'default',
|
|
31
30
|
},
|
|
32
31
|
type: 'plugin',
|
|
33
32
|
},
|
|
@@ -53,9 +52,29 @@ const mockState = {
|
|
|
53
52
|
describe('pluginSelectors', () => {
|
|
54
53
|
describe('enabledSchema', () => {
|
|
55
54
|
it('enabledSchema should return correct ChatCompletionFunctions array', () => {
|
|
56
|
-
const result = pluginSelectors.enabledSchema(['plugin-1'])(mockState);
|
|
57
|
-
expect(result).toEqual([{ name: 'plugin-1____api-
|
|
55
|
+
const result = pluginSelectors.enabledSchema(['plugin-1', 'plugin-2'])(mockState);
|
|
56
|
+
expect(result).toEqual([{ name: 'plugin-1____api-1' }, { name: 'plugin-2____api-2' }]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('enabledSchema should return with standalone plugin', () => {
|
|
60
|
+
const result = pluginSelectors.enabledSchema(['plugin-3'])({
|
|
61
|
+
...mockState,
|
|
62
|
+
installedPlugins: [
|
|
63
|
+
...mockState.installedPlugins,
|
|
64
|
+
{
|
|
65
|
+
identifier: 'plugin-3',
|
|
66
|
+
manifest: {
|
|
67
|
+
identifier: 'plugin-3',
|
|
68
|
+
api: [{ name: 'api-3' }],
|
|
69
|
+
type: 'standalone',
|
|
70
|
+
},
|
|
71
|
+
type: 'plugin',
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
} as ToolStoreState);
|
|
75
|
+
expect(result).toEqual([{ name: 'plugin-3____api-3____standalone' }]);
|
|
58
76
|
});
|
|
77
|
+
|
|
59
78
|
it('enabledSchema should return empty', () => {
|
|
60
79
|
const result = pluginSelectors.enabledSchema([])(mockState);
|
|
61
80
|
expect(result).toEqual([]);
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
|
|
2
2
|
import { uniq, uniqBy } from 'lodash-es';
|
|
3
|
+
import { Md5 } from 'ts-md5';
|
|
3
4
|
|
|
4
|
-
import { PLUGIN_SCHEMA_SEPARATOR } from '@/const/plugin';
|
|
5
|
+
import { PLUGIN_SCHEMA_API_MD5_PREFIX, PLUGIN_SCHEMA_SEPARATOR } from '@/const/plugin';
|
|
5
6
|
import { ChatCompletionFunctions } from '@/types/openai/chat';
|
|
6
|
-
import { LobeToolCustomPlugin } from '@/types/tool/plugin';
|
|
7
|
+
import { InstallPluginMeta, LobeToolCustomPlugin } from '@/types/tool/plugin';
|
|
7
8
|
|
|
8
9
|
import type { ToolStoreState } from '../../initialState';
|
|
9
10
|
|
|
@@ -60,11 +61,16 @@ const installedPluginManifestList = (s: ToolStoreState) =>
|
|
|
60
61
|
.filter((i) => !!i);
|
|
61
62
|
|
|
62
63
|
const installedPluginMetaList = (s: ToolStoreState) =>
|
|
63
|
-
installedPlugins(s).map((p) => ({
|
|
64
|
+
installedPlugins(s).map<InstallPluginMeta>((p) => ({
|
|
65
|
+
author: p.manifest?.author,
|
|
66
|
+
createdAt: p.manifest?.createdAt || (p.manifest as any)?.createAt,
|
|
67
|
+
homepage: p.manifest?.homepage,
|
|
64
68
|
identifier: p.identifier,
|
|
65
69
|
meta: getPluginMetaById(p.identifier)(s),
|
|
66
70
|
type: p.type,
|
|
67
71
|
}));
|
|
72
|
+
const installedCustomPluginMetaList = (s: ToolStoreState) =>
|
|
73
|
+
installedPluginMetaList(s).filter((p) => p.type === 'customPlugin');
|
|
68
74
|
|
|
69
75
|
const isPluginHasUI = (id: string) => (s: ToolStoreState) => {
|
|
70
76
|
const plugin = getPluginManifestById(id)(s);
|
|
@@ -85,10 +91,22 @@ const enabledSchema =
|
|
|
85
91
|
)
|
|
86
92
|
.flatMap((manifest) =>
|
|
87
93
|
manifest.api.map((m) => {
|
|
88
|
-
const pluginType =
|
|
94
|
+
const pluginType =
|
|
95
|
+
manifest.type && manifest.type !== 'default'
|
|
96
|
+
? `${PLUGIN_SCHEMA_SEPARATOR + manifest.type}`
|
|
97
|
+
: '';
|
|
89
98
|
|
|
90
99
|
// 将插件的 identifier 作为前缀,避免重复
|
|
91
|
-
|
|
100
|
+
let apiName = manifest.identifier + PLUGIN_SCHEMA_SEPARATOR + m.name + pluginType;
|
|
101
|
+
|
|
102
|
+
// OpenAI GPT function_call name can't be longer than 64 characters
|
|
103
|
+
// So we need to use md5 to shorten the name
|
|
104
|
+
// and then find the correct apiName in response by md5
|
|
105
|
+
if (apiName.length >= 64) {
|
|
106
|
+
const md5Content = PLUGIN_SCHEMA_API_MD5_PREFIX + Md5.hashStr(m.name).toString();
|
|
107
|
+
|
|
108
|
+
apiName = manifest.identifier + PLUGIN_SCHEMA_SEPARATOR + md5Content + pluginType;
|
|
109
|
+
}
|
|
92
110
|
|
|
93
111
|
return {
|
|
94
112
|
...m,
|
|
@@ -108,6 +126,7 @@ export const pluginSelectors = {
|
|
|
108
126
|
getPluginManifestLoadingStatus,
|
|
109
127
|
getPluginMetaById,
|
|
110
128
|
getPluginSettingsById,
|
|
129
|
+
installedCustomPluginMetaList,
|
|
111
130
|
installedPluginManifestList,
|
|
112
131
|
installedPluginMetaList,
|
|
113
132
|
installedPlugins,
|