@lobehub/chat 0.102.1 → 0.102.3
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 +50 -0
- package/README.md +119 -38
- package/README.zh-CN.md +99 -44
- package/docs/Development-Guide/Data-Store/Selectors.zh-CN.md +68 -0
- package/package.json +3 -3
- package/src/app/api/openai/errorResponse.test.ts +55 -0
- package/src/app/chat/features/ChatHeader/PluginTag/PluginStatus.tsx +3 -2
- package/src/app/chat/features/ChatHeader/PluginTag/index.tsx +2 -1
- package/src/app/chat/features/Conversation/ChatList/Error/Plugin/PluginSettings.tsx +2 -1
- package/src/app/chat/features/Conversation/ChatList/Extras/Assistant.test.tsx +86 -0
- package/src/app/chat/features/Conversation/ChatList/Extras/Assistant.tsx +5 -1
- package/src/app/chat/features/Conversation/ChatList/Plugins/Inspector/Settings.tsx +2 -1
- package/src/app/chat/features/Conversation/ChatList/Plugins/Inspector/index.tsx +2 -1
- package/src/app/chat/features/Conversation/ChatList/Plugins/Render/StandaloneType/Iframe.tsx +2 -1
- package/src/app/chat/features/Conversation/index.tsx +3 -3
- package/src/app/chat/features/Conversation/usePluginsInit.ts +14 -0
- package/src/app/settings/llm/LLM/Checker.tsx +2 -2
- package/src/features/AgentSetting/AgentPlugin/LocalPluginItem.tsx +4 -3
- package/src/features/AgentSetting/AgentPlugin/MarketList.tsx +4 -3
- package/src/features/AgentSetting/AgentPlugin/PluginSettings.tsx +3 -2
- package/src/features/AgentSetting/store/action.ts +5 -5
- package/src/features/PluginDevModal/MetaForm.tsx +2 -1
- package/src/features/PluginSettingsModal/index.tsx +2 -1
- package/src/services/chat.ts +84 -0
- package/src/services/plugin.ts +15 -33
- package/src/store/plugin/index.ts +0 -1
- package/src/store/plugin/selectors.test.ts +185 -0
- package/src/store/plugin/selectors.ts +6 -3
- package/src/store/plugin/slices/customPlugin/action.ts +3 -3
- package/src/store/plugin/slices/plugin/action.ts +44 -15
- package/src/store/session/slices/chat/actions/message.ts +2 -2
- package/src/store/session/slices/chat/actions/plugin.ts +2 -2
- package/src/store/session/slices/chat/actions/topic.ts +2 -2
- package/src/store/session/slices/chat/actions/translate.ts +9 -7
- package/src/utils/fetch.ts +0 -3
- package/tests/setup.ts +6 -1
- package/src/app/chat/features/Conversation/ChatList/Extras/AudioPlayer.tsx +0 -139
- package/src/services/chatModel.ts +0 -48
- package/src/services/pluginMarket.ts +0 -13
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { PluginStoreState } from './initialState';
|
|
4
|
+
import { pluginSelectors } from './selectors';
|
|
5
|
+
|
|
6
|
+
const mockState = {
|
|
7
|
+
pluginManifestMap: {
|
|
8
|
+
'plugin-1': {
|
|
9
|
+
identifier: 'plugin-1',
|
|
10
|
+
api: [{ name: 'api-1' }],
|
|
11
|
+
type: 'default',
|
|
12
|
+
},
|
|
13
|
+
'plugin-2': {
|
|
14
|
+
identifier: 'plugin-2',
|
|
15
|
+
api: [{ name: 'api-2' }],
|
|
16
|
+
type: 'default',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
pluginManifestLoading: {
|
|
20
|
+
'plugin-1': false,
|
|
21
|
+
'plugin-2': true,
|
|
22
|
+
},
|
|
23
|
+
pluginList: [
|
|
24
|
+
{
|
|
25
|
+
identifier: 'plugin-1',
|
|
26
|
+
author: 'Author 1',
|
|
27
|
+
createAt: '2021-01-01',
|
|
28
|
+
meta: { avatar: 'avatar-url-1', title: 'Plugin 1' },
|
|
29
|
+
homepage: 'http://homepage-1.com',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
identifier: 'plugin-2',
|
|
33
|
+
author: 'Author 2',
|
|
34
|
+
createAt: '2022-02-02',
|
|
35
|
+
meta: { avatar: 'avatar-url-2', title: 'Plugin 2' },
|
|
36
|
+
homepage: 'http://homepage-2.com',
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
customPluginList: [],
|
|
40
|
+
pluginsSettings: {},
|
|
41
|
+
} as unknown as PluginStoreState;
|
|
42
|
+
|
|
43
|
+
describe('pluginSelectors', () => {
|
|
44
|
+
describe('enabledSchema', () => {
|
|
45
|
+
it('enabledSchema should return correct ChatCompletionFunctions array', () => {
|
|
46
|
+
const result = pluginSelectors.enabledSchema(['plugin-1'])(mockState);
|
|
47
|
+
expect(result).toEqual([{ name: 'plugin-1____api-1____default' }]);
|
|
48
|
+
});
|
|
49
|
+
it('enabledSchema should return empty', () => {
|
|
50
|
+
const result = pluginSelectors.enabledSchema([])(mockState);
|
|
51
|
+
expect(result).toEqual([]);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('getPluginManifestById', () => {
|
|
56
|
+
it('getPluginManifestById should return the correct manifest', () => {
|
|
57
|
+
const result = pluginSelectors.getPluginManifestById('plugin-1')(mockState);
|
|
58
|
+
expect(result).toEqual(mockState.pluginManifestMap['plugin-1']);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
describe('getPluginManifestLoadingStatus', () => {
|
|
62
|
+
it('success', () => {
|
|
63
|
+
const result = pluginSelectors.getPluginManifestLoadingStatus('plugin-1')(mockState);
|
|
64
|
+
expect(result).toBe('success');
|
|
65
|
+
});
|
|
66
|
+
it('loading', () => {
|
|
67
|
+
let result = pluginSelectors.getPluginManifestLoadingStatus('plugin-2')(mockState);
|
|
68
|
+
expect(result).toBe('loading');
|
|
69
|
+
});
|
|
70
|
+
it('error', () => {
|
|
71
|
+
let result = pluginSelectors.getPluginManifestLoadingStatus('plugin-3')(mockState);
|
|
72
|
+
expect(result).toBe('error');
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('onlinePluginStore', () => {
|
|
77
|
+
it('should return the online plugin list', () => {
|
|
78
|
+
const result = pluginSelectors.onlinePluginStore(mockState);
|
|
79
|
+
expect(result).toEqual(mockState.pluginList);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('pluginList', () => {
|
|
84
|
+
it('should return the combined list of pluginList and customPluginList', () => {
|
|
85
|
+
const result = pluginSelectors.pluginList(mockState);
|
|
86
|
+
expect(result).toEqual([...mockState.pluginList, ...mockState.customPluginList]);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('getPluginMetaById', () => {
|
|
91
|
+
it('should return the plugin metadata by id', () => {
|
|
92
|
+
const result = pluginSelectors.getPluginMetaById('plugin-1')(mockState);
|
|
93
|
+
expect(result).toEqual(mockState.pluginList[0]);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('getDevPluginById', () => {
|
|
98
|
+
it('should return undefined for non-existing custom plugin', () => {
|
|
99
|
+
const result = pluginSelectors.getDevPluginById('non-existing')(mockState);
|
|
100
|
+
expect(result).toBeUndefined();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should return custom plugin by id if exists', () => {
|
|
104
|
+
const customPlugin = {
|
|
105
|
+
identifier: 'custom-plugin',
|
|
106
|
+
author: 'Custom Author',
|
|
107
|
+
createAt: '2023-03-03',
|
|
108
|
+
meta: { avatar: 'avatar-url-custom', title: 'Custom Plugin' },
|
|
109
|
+
homepage: 'http://homepage-custom.com',
|
|
110
|
+
};
|
|
111
|
+
const stateWithCustomPlugin = {
|
|
112
|
+
...mockState,
|
|
113
|
+
customPluginList: [customPlugin],
|
|
114
|
+
} as PluginStoreState;
|
|
115
|
+
const result = pluginSelectors.getDevPluginById('custom-plugin')(stateWithCustomPlugin);
|
|
116
|
+
expect(result).toEqual(customPlugin);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe('getPluginSettingsById', () => {
|
|
121
|
+
it('should return the plugin settings by id', () => {
|
|
122
|
+
const mockSettings = { setting1: 'value1' };
|
|
123
|
+
const stateWithSettings = {
|
|
124
|
+
...mockState,
|
|
125
|
+
pluginsSettings: { 'plugin-1': mockSettings },
|
|
126
|
+
};
|
|
127
|
+
const result = pluginSelectors.getPluginSettingsById('plugin-1')(stateWithSettings);
|
|
128
|
+
expect(result).toEqual(mockSettings);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe('isCustomPlugin', () => {
|
|
133
|
+
it('should return false for a non-custom plugin', () => {
|
|
134
|
+
const result = pluginSelectors.isCustomPlugin('plugin-1')(mockState);
|
|
135
|
+
expect(result).toBe(false);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should return true for a custom plugin', () => {
|
|
139
|
+
const stateWithCustomPlugin = {
|
|
140
|
+
...mockState,
|
|
141
|
+
customPluginList: [{ identifier: 'custom-plugin' }],
|
|
142
|
+
} as PluginStoreState;
|
|
143
|
+
const result = pluginSelectors.isCustomPlugin('custom-plugin')(stateWithCustomPlugin);
|
|
144
|
+
expect(result).toBe(true);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe('displayPluginList', () => {
|
|
149
|
+
it('should return a list of plugins with display information', () => {
|
|
150
|
+
const result = pluginSelectors.displayPluginList(mockState);
|
|
151
|
+
const expected = mockState.pluginList.map((p) => ({
|
|
152
|
+
author: p.author,
|
|
153
|
+
avatar: p.meta?.avatar,
|
|
154
|
+
createAt: p.createAt,
|
|
155
|
+
desc: p.meta.description,
|
|
156
|
+
homepage: p.homepage,
|
|
157
|
+
identifier: p.identifier,
|
|
158
|
+
title: p.meta.title,
|
|
159
|
+
}));
|
|
160
|
+
expect(result).toEqual(expected);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe('hasPluginUI', () => {
|
|
165
|
+
it('should return false when the plugin does not have a UI component', () => {
|
|
166
|
+
const result = pluginSelectors.hasPluginUI('non-ui-plugin')(mockState);
|
|
167
|
+
expect(result).toBe(false);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should return true when the plugin has a UI component', () => {
|
|
171
|
+
const stateWithUIPlugin = {
|
|
172
|
+
...mockState,
|
|
173
|
+
pluginManifestMap: {
|
|
174
|
+
...mockState.pluginManifestMap,
|
|
175
|
+
'ui-plugin': {
|
|
176
|
+
...mockState.pluginManifestMap['plugin-1'],
|
|
177
|
+
ui: true,
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
} as unknown as PluginStoreState;
|
|
181
|
+
const result = pluginSelectors.hasPluginUI('ui-plugin')(stateWithUIPlugin);
|
|
182
|
+
expect(result).toBe(true);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
});
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { uniqBy } from 'lodash-es';
|
|
2
2
|
|
|
3
3
|
import { PLUGIN_SCHEMA_SEPARATOR } from '@/const/plugin';
|
|
4
|
-
import { pluginHelpers } from '@/store/plugin/helpers';
|
|
5
4
|
import { ChatCompletionFunctions } from '@/types/openai/chat';
|
|
6
5
|
|
|
7
|
-
import {
|
|
6
|
+
import { pluginHelpers } from './helpers';
|
|
7
|
+
import type { PluginStoreState } from './initialState';
|
|
8
8
|
|
|
9
9
|
const enabledSchema =
|
|
10
10
|
(enabledPlugins: string[] = []) =>
|
|
@@ -34,6 +34,8 @@ const enabledSchema =
|
|
|
34
34
|
return uniqBy(list, 'name');
|
|
35
35
|
};
|
|
36
36
|
|
|
37
|
+
const onlinePluginStore = (s: PluginStoreState) => s.pluginList;
|
|
38
|
+
|
|
37
39
|
const pluginList = (s: PluginStoreState) => [...s.pluginList, ...s.customPluginList];
|
|
38
40
|
|
|
39
41
|
const getPluginMetaById = (id: string) => (s: PluginStoreState) =>
|
|
@@ -47,7 +49,7 @@ const getPluginSettingsById = (id: string) => (s: PluginStoreState) => s.plugins
|
|
|
47
49
|
|
|
48
50
|
// 获取插件 manifest 加载状态
|
|
49
51
|
const getPluginManifestLoadingStatus = (id: string) => (s: PluginStoreState) => {
|
|
50
|
-
const manifest = getPluginManifestById(id);
|
|
52
|
+
const manifest = getPluginManifestById(id)(s);
|
|
51
53
|
|
|
52
54
|
if (s.pluginManifestLoading[id]) return 'loading';
|
|
53
55
|
|
|
@@ -86,5 +88,6 @@ export const pluginSelectors = {
|
|
|
86
88
|
getPluginSettingsById,
|
|
87
89
|
hasPluginUI,
|
|
88
90
|
isCustomPlugin,
|
|
91
|
+
onlinePluginStore,
|
|
89
92
|
pluginList,
|
|
90
93
|
};
|
|
@@ -47,11 +47,11 @@ export const createCustomPluginSlice: StateCreator<
|
|
|
47
47
|
set({ newCustomPlugin: defaultCustomPlugin }, false, t('saveToCustomPluginList'));
|
|
48
48
|
},
|
|
49
49
|
updateCustomPlugin: (id, value) => {
|
|
50
|
-
const { dispatchCustomPluginList,
|
|
50
|
+
const { dispatchCustomPluginList, installPlugin } = get();
|
|
51
51
|
// 1. 更新 list 项信息
|
|
52
52
|
dispatchCustomPluginList({ id, plugin: value, type: 'updateItem' });
|
|
53
|
-
// 2.
|
|
54
|
-
|
|
53
|
+
// 2. 重新安装插件
|
|
54
|
+
installPlugin(id);
|
|
55
55
|
},
|
|
56
56
|
|
|
57
57
|
updateNewCustomPlugin: (newCustomPlugin) => {
|
|
@@ -9,7 +9,7 @@ import { merge, uniq } from 'lodash-es';
|
|
|
9
9
|
import useSWR, { SWRResponse } from 'swr';
|
|
10
10
|
import { StateCreator } from 'zustand/vanilla';
|
|
11
11
|
|
|
12
|
-
import {
|
|
12
|
+
import { pluginService } from '@/services/plugin';
|
|
13
13
|
import { pluginSelectors } from '@/store/plugin/selectors';
|
|
14
14
|
import { LobeSessions } from '@/types/session';
|
|
15
15
|
import { setNamespace } from '@/utils/storeDebug';
|
|
@@ -24,13 +24,17 @@ const t = setNamespace('plugin');
|
|
|
24
24
|
*/
|
|
25
25
|
export interface PluginAction {
|
|
26
26
|
checkLocalEnabledPlugins: (sessions: LobeSessions) => void;
|
|
27
|
+
checkPluginsIsInstalled: (plugins: string[]) => void;
|
|
27
28
|
deletePluginSettings: (id: string) => void;
|
|
28
29
|
dispatchPluginManifest: (payload: PluginDispatch) => void;
|
|
29
|
-
|
|
30
|
+
installPlugin: (identifier: string) => Promise<void>;
|
|
31
|
+
installPlugins: (plugins: string[]) => Promise<void>;
|
|
32
|
+
loadPluginStore: () => Promise<LobeChatPluginsMarketIndex>;
|
|
30
33
|
resetPluginSettings: () => void;
|
|
31
34
|
updateManifestLoadingState: (key: string, value: boolean | undefined) => void;
|
|
32
35
|
updatePluginSettings: <T>(id: string, settings: Partial<T>) => void;
|
|
33
|
-
|
|
36
|
+
useCheckPluginsIsInstalled: (plugins: string[]) => SWRResponse;
|
|
37
|
+
useFetchPluginStore: () => SWRResponse<LobeChatPluginsMarketIndex>;
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
export const createPluginSlice: StateCreator<
|
|
@@ -40,7 +44,7 @@ export const createPluginSlice: StateCreator<
|
|
|
40
44
|
PluginAction
|
|
41
45
|
> = (set, get) => ({
|
|
42
46
|
checkLocalEnabledPlugins: async (sessions) => {
|
|
43
|
-
const {
|
|
47
|
+
const { checkPluginsIsInstalled } = get();
|
|
44
48
|
|
|
45
49
|
let enabledPlugins: string[] = [];
|
|
46
50
|
|
|
@@ -53,9 +57,23 @@ export const createPluginSlice: StateCreator<
|
|
|
53
57
|
|
|
54
58
|
const plugins = uniq(enabledPlugins);
|
|
55
59
|
|
|
56
|
-
await
|
|
60
|
+
await checkPluginsIsInstalled(plugins);
|
|
61
|
+
},
|
|
62
|
+
checkPluginsIsInstalled: async (plugins) => {
|
|
63
|
+
// if there is no plugins, just skip.
|
|
64
|
+
if (plugins.length === 0) return;
|
|
65
|
+
|
|
66
|
+
const { loadPluginStore, installPlugins } = get();
|
|
67
|
+
|
|
68
|
+
// check if the store is empty
|
|
69
|
+
// if it is, we need to load the plugin store
|
|
70
|
+
if (pluginSelectors.onlinePluginStore(get()).length === 0) {
|
|
71
|
+
await loadPluginStore();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
await installPlugins(plugins);
|
|
57
75
|
|
|
58
|
-
set({ manifestPrepared: true }, false, t('
|
|
76
|
+
set({ manifestPrepared: true }, false, t('checkPluginsIsInstalled'));
|
|
59
77
|
},
|
|
60
78
|
deletePluginSettings: (id) => {
|
|
61
79
|
set(
|
|
@@ -72,9 +90,10 @@ export const createPluginSlice: StateCreator<
|
|
|
72
90
|
|
|
73
91
|
set({ pluginManifestMap: nextManifest }, false, t('dispatchPluginManifest', payload));
|
|
74
92
|
},
|
|
75
|
-
|
|
93
|
+
installPlugin: async (name) => {
|
|
76
94
|
const plugin = pluginSelectors.getPluginMetaById(name)(get());
|
|
77
|
-
|
|
95
|
+
|
|
96
|
+
// 1. valid plugin
|
|
78
97
|
|
|
79
98
|
if (!plugin) return;
|
|
80
99
|
|
|
@@ -98,7 +117,7 @@ export const createPluginSlice: StateCreator<
|
|
|
98
117
|
|
|
99
118
|
get().updateManifestLoadingState(name, undefined);
|
|
100
119
|
if (!data) {
|
|
101
|
-
message.error(
|
|
120
|
+
message.error(`插件 ${plugin.meta.title} 描述文件请求失败`);
|
|
102
121
|
return;
|
|
103
122
|
}
|
|
104
123
|
|
|
@@ -113,6 +132,18 @@ export const createPluginSlice: StateCreator<
|
|
|
113
132
|
// 4. 存储 manifest 信息
|
|
114
133
|
get().dispatchPluginManifest({ id: plugin.identifier, plugin: data, type: 'addManifest' });
|
|
115
134
|
},
|
|
135
|
+
installPlugins: async (plugins) => {
|
|
136
|
+
const { installPlugin } = get();
|
|
137
|
+
|
|
138
|
+
await Promise.all(plugins.map((identifier) => installPlugin(identifier)));
|
|
139
|
+
},
|
|
140
|
+
loadPluginStore: async () => {
|
|
141
|
+
const pluginMarketIndex = await pluginService.getPluginList();
|
|
142
|
+
|
|
143
|
+
set({ pluginList: pluginMarketIndex.plugins }, false, t('loadPluginList'));
|
|
144
|
+
|
|
145
|
+
return pluginMarketIndex;
|
|
146
|
+
},
|
|
116
147
|
resetPluginSettings: () => {
|
|
117
148
|
set({ pluginsSettings: {} }, false, t('resetPluginSettings'));
|
|
118
149
|
},
|
|
@@ -135,10 +166,8 @@ export const createPluginSlice: StateCreator<
|
|
|
135
166
|
);
|
|
136
167
|
},
|
|
137
168
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
},
|
|
143
|
-
}),
|
|
169
|
+
useCheckPluginsIsInstalled: (plugins) => useSWR(plugins, get().checkPluginsIsInstalled),
|
|
170
|
+
|
|
171
|
+
useFetchPluginStore: () =>
|
|
172
|
+
useSWR<LobeChatPluginsMarketIndex>('loadPluginStore', get().loadPluginStore),
|
|
144
173
|
});
|
|
@@ -4,7 +4,7 @@ import { StateCreator } from 'zustand/vanilla';
|
|
|
4
4
|
import { VISION_MODEL_WHITE_LIST } from '@/const/llm';
|
|
5
5
|
import { LOADING_FLAT } from '@/const/message';
|
|
6
6
|
import { VISION_MODEL_DEFAULT_MAX_TOKENS } from '@/const/settings';
|
|
7
|
-
import {
|
|
7
|
+
import { chatService } from '@/services/chat';
|
|
8
8
|
import { filesSelectors, useFileStore } from '@/store/files';
|
|
9
9
|
import { SessionStore } from '@/store/session';
|
|
10
10
|
import { ChatMessage } from '@/types/chatMessage';
|
|
@@ -263,7 +263,7 @@ export const chatMessage: StateCreator<
|
|
|
263
263
|
}
|
|
264
264
|
|
|
265
265
|
const fetcher = () =>
|
|
266
|
-
|
|
266
|
+
chatService.getChatCompletion(
|
|
267
267
|
{
|
|
268
268
|
messages: postMessages,
|
|
269
269
|
model: config.model,
|
|
@@ -2,7 +2,7 @@ import { PluginRequestPayload } from '@lobehub/chat-plugin-sdk';
|
|
|
2
2
|
import { StateCreator } from 'zustand/vanilla';
|
|
3
3
|
|
|
4
4
|
import { PLUGIN_SCHEMA_SEPARATOR } from '@/const/plugin';
|
|
5
|
-
import {
|
|
5
|
+
import { chatService } from '@/services/chat';
|
|
6
6
|
import { SessionStore } from '@/store/session';
|
|
7
7
|
import { OpenAIFunctionCall } from '@/types/chatMessage';
|
|
8
8
|
import { setNamespace } from '@/utils/storeDebug';
|
|
@@ -41,7 +41,7 @@ export const chatPlugin: StateCreator<
|
|
|
41
41
|
let data: string;
|
|
42
42
|
try {
|
|
43
43
|
const abortController = toggleChatLoading(true, id, t('fetchPlugin') as string);
|
|
44
|
-
data = await
|
|
44
|
+
data = await chatService.runPluginApi(payload, { signal: abortController?.signal });
|
|
45
45
|
} catch (error) {
|
|
46
46
|
dispatchMessage({ id, key: 'error', type: 'updateMessage', value: error });
|
|
47
47
|
|
|
@@ -2,8 +2,8 @@ import { StateCreator } from 'zustand/vanilla';
|
|
|
2
2
|
|
|
3
3
|
import { chainSummaryTitle } from '@/chains/summaryTitle';
|
|
4
4
|
import { LOADING_FLAT } from '@/const/message';
|
|
5
|
+
import { chatService } from '@/services/chat';
|
|
5
6
|
import { SessionStore } from '@/store/session';
|
|
6
|
-
import { fetchPresetTaskResult } from '@/utils/fetch';
|
|
7
7
|
import { setNamespace } from '@/utils/storeDebug';
|
|
8
8
|
import { nanoid } from '@/utils/uuid';
|
|
9
9
|
|
|
@@ -145,7 +145,7 @@ export const chatTopic: StateCreator<
|
|
|
145
145
|
let output = '';
|
|
146
146
|
|
|
147
147
|
// 自动总结话题标题
|
|
148
|
-
fetchPresetTaskResult({
|
|
148
|
+
chatService.fetchPresetTaskResult({
|
|
149
149
|
onError: () => {
|
|
150
150
|
dispatchTopic({ id: topicId, key: 'title', type: 'updateChatTopic', value: defaultTitle });
|
|
151
151
|
},
|
|
@@ -4,8 +4,8 @@ import { StateCreator } from 'zustand/vanilla';
|
|
|
4
4
|
import { chainLangDetect } from '@/chains/langDetect';
|
|
5
5
|
import { chainTranslate } from '@/chains/translate';
|
|
6
6
|
import { supportLocales } from '@/locales/options';
|
|
7
|
+
import { chatService } from '@/services/chat';
|
|
7
8
|
import { SessionStore } from '@/store/session';
|
|
8
|
-
import { fetchPresetTaskResult } from '@/utils/fetch';
|
|
9
9
|
import { setNamespace } from '@/utils/storeDebug';
|
|
10
10
|
|
|
11
11
|
import { sessionSelectors } from '../../session/selectors';
|
|
@@ -72,14 +72,16 @@ export const chatTranslate: StateCreator<
|
|
|
72
72
|
toggleChatLoading(true, id, t('translateMessage(start)', { id }) as string);
|
|
73
73
|
|
|
74
74
|
// detect from language
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
75
|
+
chatService
|
|
76
|
+
.fetchPresetTaskResult({
|
|
77
|
+
params: chainLangDetect(message.content),
|
|
78
|
+
})
|
|
79
|
+
.then((data) => {
|
|
80
|
+
if (data && supportLocales.includes(data)) from = data;
|
|
81
|
+
});
|
|
80
82
|
|
|
81
83
|
// translate to target language
|
|
82
|
-
await fetchPresetTaskResult({
|
|
84
|
+
await chatService.fetchPresetTaskResult({
|
|
83
85
|
onMessageHandle: (text) => {
|
|
84
86
|
dispatchMessage({
|
|
85
87
|
id,
|
package/src/utils/fetch.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { t } from 'i18next';
|
|
2
2
|
|
|
3
|
-
import { fetchChatModel } from '@/services/chatModel';
|
|
4
3
|
import { ChatMessageError } from '@/types/chatMessage';
|
|
5
4
|
import { ErrorResponse, ErrorType } from '@/types/fetch';
|
|
6
5
|
|
|
@@ -122,5 +121,3 @@ export const fetchAIFactory =
|
|
|
122
121
|
|
|
123
122
|
return await data?.text();
|
|
124
123
|
};
|
|
125
|
-
|
|
126
|
-
export const fetchPresetTaskResult = fetchAIFactory(fetchChatModel);
|
package/tests/setup.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
/* eslint-disable import/newline-after-import,import/first */
|
|
1
2
|
import '@testing-library/jest-dom';
|
|
2
|
-
// remove antd hash on test
|
|
3
3
|
import { theme } from 'antd';
|
|
4
4
|
// mock indexedDB to test with dexie
|
|
5
5
|
// refs: https://github.com/dumbmatter/fakeIndexedDB#dexie-and-other-indexeddb-api-wrappers
|
|
6
6
|
import 'fake-indexeddb/auto';
|
|
7
|
+
import React from 'react';
|
|
7
8
|
|
|
9
|
+
// remove antd hash on test
|
|
8
10
|
theme.defaultConfig.hashed = false;
|
|
11
|
+
|
|
12
|
+
// 将 React 设置为全局变量,这样就不需要在每个测试文件中导入它了
|
|
13
|
+
global.React = React;
|
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
import { ActionIcon, ActionIconProps, Icon, Tag } from '@lobehub/ui';
|
|
2
|
-
import { Dropdown, Slider } from 'antd';
|
|
3
|
-
import { Download, PauseCircle, Play, StopCircle } from 'lucide-react';
|
|
4
|
-
import React, { memo, useCallback, useMemo } from 'react';
|
|
5
|
-
import { Flexbox } from 'react-layout-kit';
|
|
6
|
-
|
|
7
|
-
const secondsToMinutesAndSeconds = (num: number) => Math.floor(num);
|
|
8
|
-
export interface AudioProps {
|
|
9
|
-
currentTime: number;
|
|
10
|
-
download: () => void;
|
|
11
|
-
duration: number;
|
|
12
|
-
isPlaying: boolean;
|
|
13
|
-
pause: () => void;
|
|
14
|
-
play: () => void;
|
|
15
|
-
setTime: (time: number) => void;
|
|
16
|
-
stop: () => void;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface AudioPlayerProps {
|
|
20
|
-
allowPause?: boolean;
|
|
21
|
-
audio: AudioProps;
|
|
22
|
-
buttonSize?: ActionIconProps['size'];
|
|
23
|
-
className?: string;
|
|
24
|
-
isLoading?: boolean;
|
|
25
|
-
onInitPlay?: () => void;
|
|
26
|
-
onPause?: () => void;
|
|
27
|
-
onPlay?: () => void;
|
|
28
|
-
onStop?: () => void;
|
|
29
|
-
showSlider?: boolean;
|
|
30
|
-
style?: React.CSSProperties;
|
|
31
|
-
timeRender?: 'tag' | 'text';
|
|
32
|
-
timeStyle?: React.CSSProperties;
|
|
33
|
-
timeType?: 'left' | 'current' | 'combine';
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const AudioPlayer = memo<AudioPlayerProps>(
|
|
37
|
-
({
|
|
38
|
-
isLoading,
|
|
39
|
-
style,
|
|
40
|
-
timeStyle,
|
|
41
|
-
buttonSize,
|
|
42
|
-
className,
|
|
43
|
-
audio,
|
|
44
|
-
allowPause = true,
|
|
45
|
-
timeType = 'left',
|
|
46
|
-
showSlider = true,
|
|
47
|
-
timeRender = 'text',
|
|
48
|
-
onInitPlay,
|
|
49
|
-
onPause,
|
|
50
|
-
onStop,
|
|
51
|
-
onPlay,
|
|
52
|
-
}) => {
|
|
53
|
-
const { isPlaying, play, stop, pause, duration, setTime, currentTime, download } = audio;
|
|
54
|
-
|
|
55
|
-
const formatedLeftTime = secondsToMinutesAndSeconds(duration - currentTime);
|
|
56
|
-
const formatedCurrentTime = secondsToMinutesAndSeconds(currentTime);
|
|
57
|
-
const formatedDuration = secondsToMinutesAndSeconds(duration);
|
|
58
|
-
|
|
59
|
-
const Time = useMemo(
|
|
60
|
-
() => (timeRender === 'tag' ? Tag : (props: any) => <div {...props} />),
|
|
61
|
-
[timeRender],
|
|
62
|
-
);
|
|
63
|
-
|
|
64
|
-
const handlePlay = useCallback(() => {
|
|
65
|
-
if ((!duration || duration === 0) && !isLoading) {
|
|
66
|
-
onInitPlay?.();
|
|
67
|
-
} else {
|
|
68
|
-
play?.();
|
|
69
|
-
onPlay?.();
|
|
70
|
-
}
|
|
71
|
-
}, [play, duration]);
|
|
72
|
-
|
|
73
|
-
const handlePause = useCallback(() => {
|
|
74
|
-
pause?.();
|
|
75
|
-
onPause?.();
|
|
76
|
-
}, [pause]);
|
|
77
|
-
|
|
78
|
-
const handleStop = useCallback(() => {
|
|
79
|
-
stop?.();
|
|
80
|
-
onStop?.();
|
|
81
|
-
}, [stop]);
|
|
82
|
-
|
|
83
|
-
return (
|
|
84
|
-
<Flexbox
|
|
85
|
-
align={'center'}
|
|
86
|
-
className={className}
|
|
87
|
-
gap={8}
|
|
88
|
-
horizontal
|
|
89
|
-
style={{ paddingRight: 8, width: '100%', ...style }}
|
|
90
|
-
>
|
|
91
|
-
<ActionIcon
|
|
92
|
-
icon={isPlaying ? (allowPause ? PauseCircle : StopCircle) : Play}
|
|
93
|
-
loading={isLoading}
|
|
94
|
-
onClick={isPlaying ? (allowPause ? handlePause : handleStop) : handlePlay}
|
|
95
|
-
size={buttonSize || { blockSize: 32, fontSize: 16 }}
|
|
96
|
-
style={{ flex: 'none' }}
|
|
97
|
-
/>
|
|
98
|
-
{showSlider && (
|
|
99
|
-
<Slider
|
|
100
|
-
disabled={duration === 0}
|
|
101
|
-
max={duration}
|
|
102
|
-
min={0}
|
|
103
|
-
onChange={(e) => setTime(e)}
|
|
104
|
-
step={0.01}
|
|
105
|
-
style={{ flex: 1 }}
|
|
106
|
-
tooltip={{ formatter: secondsToMinutesAndSeconds as any }}
|
|
107
|
-
value={currentTime}
|
|
108
|
-
/>
|
|
109
|
-
)}
|
|
110
|
-
<Dropdown
|
|
111
|
-
disabled={duration === 0}
|
|
112
|
-
menu={{
|
|
113
|
-
items: [
|
|
114
|
-
{
|
|
115
|
-
key: 'download',
|
|
116
|
-
label: <Icon icon={Download} size={{ fontSize: 16 }} />,
|
|
117
|
-
onClick: download,
|
|
118
|
-
},
|
|
119
|
-
],
|
|
120
|
-
}}
|
|
121
|
-
placement="top"
|
|
122
|
-
>
|
|
123
|
-
<Time style={{ cursor: 'pointer', flex: 'none', ...timeStyle }}>
|
|
124
|
-
{timeType === 'left' && formatedLeftTime}
|
|
125
|
-
{timeType === 'current' && formatedCurrentTime}
|
|
126
|
-
{timeType === 'combine' && (
|
|
127
|
-
<span>
|
|
128
|
-
{formatedCurrentTime}
|
|
129
|
-
<span style={{ opacity: 0.66 }}>{` / ${formatedDuration}`}</span>
|
|
130
|
-
</span>
|
|
131
|
-
)}
|
|
132
|
-
</Time>
|
|
133
|
-
</Dropdown>
|
|
134
|
-
</Flexbox>
|
|
135
|
-
);
|
|
136
|
-
},
|
|
137
|
-
);
|
|
138
|
-
|
|
139
|
-
export default AudioPlayer;
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { merge } from 'lodash-es';
|
|
2
|
-
|
|
3
|
-
import { VISION_MODEL_WHITE_LIST } from '@/const/llm';
|
|
4
|
-
import { pluginSelectors, usePluginStore } from '@/store/plugin';
|
|
5
|
-
import { initialLobeAgentConfig } from '@/store/session/initialState';
|
|
6
|
-
import type { OpenAIChatStreamPayload } from '@/types/openai/chat';
|
|
7
|
-
|
|
8
|
-
import { createHeaderWithOpenAI } from './_header';
|
|
9
|
-
import { OPENAI_URLS } from './_url';
|
|
10
|
-
|
|
11
|
-
interface FetchChatModelOptions {
|
|
12
|
-
signal?: AbortSignal | undefined;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* 专门用于对话的 fetch
|
|
17
|
-
*/
|
|
18
|
-
export const fetchChatModel = (
|
|
19
|
-
{ plugins: enabledPlugins, ...params }: Partial<OpenAIChatStreamPayload>,
|
|
20
|
-
options?: FetchChatModelOptions,
|
|
21
|
-
) => {
|
|
22
|
-
const payload = merge(
|
|
23
|
-
{
|
|
24
|
-
model: initialLobeAgentConfig.model,
|
|
25
|
-
stream: true,
|
|
26
|
-
...initialLobeAgentConfig.params,
|
|
27
|
-
},
|
|
28
|
-
params,
|
|
29
|
-
);
|
|
30
|
-
// ============ preprocess tools ============ //
|
|
31
|
-
|
|
32
|
-
const filterTools = pluginSelectors.enabledSchema(enabledPlugins)(usePluginStore.getState());
|
|
33
|
-
|
|
34
|
-
// the rule that model can use tools:
|
|
35
|
-
// 1. tools is not empty
|
|
36
|
-
// 2. model is not in vision white list, because vision model can't use tools
|
|
37
|
-
// TODO: we need to find some method to let vision model use tools
|
|
38
|
-
const shouldUseTools = filterTools.length > 0 && !VISION_MODEL_WHITE_LIST.includes(payload.model);
|
|
39
|
-
|
|
40
|
-
const functions = shouldUseTools ? filterTools : undefined;
|
|
41
|
-
|
|
42
|
-
return fetch(OPENAI_URLS.chat, {
|
|
43
|
-
body: JSON.stringify({ ...payload, functions }),
|
|
44
|
-
headers: createHeaderWithOpenAI({ 'Content-Type': 'application/json' }),
|
|
45
|
-
method: 'POST',
|
|
46
|
-
signal: options?.signal,
|
|
47
|
-
});
|
|
48
|
-
};
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { getPluginIndexJSON } from '@/const/url';
|
|
2
|
-
import { getCurrentLanguage } from '@/store/global/helpers';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* fetch Plugin Market List
|
|
6
|
-
*/
|
|
7
|
-
export const getPluginList = async () => {
|
|
8
|
-
const url = getPluginIndexJSON(getCurrentLanguage());
|
|
9
|
-
|
|
10
|
-
const res = await fetch(url);
|
|
11
|
-
|
|
12
|
-
return res.json();
|
|
13
|
-
};
|