From 4534598a9a97aa4b92e47ba8a6a23378bd5df019 Mon Sep 17 00:00:00 2001 From: Arvin Xu Date: Thu, 24 Aug 2023 21:43:31 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20use=20new=20plugin=20manife?= =?UTF-8?q?st=20to=20support=20plugin=E2=80=99s=20multi=20api=20(#101)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ feat: support new plugin manifest to support plugin’s multi api * 🐛 fix: 修正导入消息时插件没有初始化的问题 * 💬 style: 修正插件检测中文案 --- package.json | 2 +- src/const/plugin.ts | 1 + .../AgentSetting/AgentPlugin/index.tsx | 14 +- src/hooks/useImportConfig.ts | 7 + src/locales/default/plugin.ts | 2 +- .../ChatList/Plugins/FunctionCall.tsx | 120 +++++++++--------- .../features/Conversation/ChatList/index.tsx | 23 ++-- src/pages/chat/features/Header/PluginTag.tsx | 2 +- src/store/plugin/action.ts | 10 +- src/store/plugin/reducers/manifest.ts | 4 +- src/store/plugin/selectors.ts | 18 ++- src/store/plugin/store.ts | 3 +- .../session/slices/chat/actions/message.ts | 36 +++--- .../slices/chat/selectors/utils.test.ts | 6 +- .../session/slices/chat/selectors/utils.ts | 4 +- src/types/chatMessage.ts | 9 ++ src/types/plugin.ts | 4 +- 17 files changed, 150 insertions(+), 115 deletions(-) create mode 100644 src/const/plugin.ts diff --git a/package.json b/package.json index cbe2b823..a0ba5f88 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "@emoji-mart/react": "^1", "@icons-pack/react-simple-icons": "^8", "@lobehub/chat-plugin-sdk": "latest", - "@lobehub/chat-plugins-gateway": "^1.2.2", + "@lobehub/chat-plugins-gateway": "latest", "@lobehub/ui": "latest", "@vercel/analytics": "^1", "ahooks": "^3", diff --git a/src/const/plugin.ts b/src/const/plugin.ts new file mode 100644 index 00000000..8627ecba --- /dev/null +++ b/src/const/plugin.ts @@ -0,0 +1 @@ +export const PLUGIN_SCHEMA_SEPARATOR = '--__--'; diff --git a/src/features/AgentSetting/AgentPlugin/index.tsx b/src/features/AgentSetting/AgentPlugin/index.tsx index 345722f2..6e8c96ff 100644 --- a/src/features/AgentSetting/AgentPlugin/index.tsx +++ b/src/features/AgentSetting/AgentPlugin/index.tsx @@ -75,23 +75,23 @@ const AgentPlugin = memo(({ config, updateConfig }) => { { - updateConfig(item.name, checked); + updateConfig(item.identifier, checked); if (checked) { - fetchPluginManifest(item.name); + fetchPluginManifest(item.identifier); } }} /> ), desc: item.meta?.description, - label: t(`plugins.${item.name}` as any, { ns: 'plugin' }), + label: t(`plugins.${item.identifier}` as any, { ns: 'plugin' }), minWidth: undefined, - tag: item.name, + tag: item.identifier, })); return { diff --git a/src/hooks/useImportConfig.ts b/src/hooks/useImportConfig.ts index 73353e3e..0318e58c 100644 --- a/src/hooks/useImportConfig.ts +++ b/src/hooks/useImportConfig.ts @@ -1,12 +1,14 @@ import { useMemo } from 'react'; import { useGlobalStore } from '@/store/global'; +import { usePluginStore } from '@/store/plugin'; import { useSessionStore } from '@/store/session'; import { importConfigFile } from '@/utils/config'; export const useImportConfig = () => { const importSessions = useSessionStore((s) => s.importSessions); const importAppSettings = useGlobalStore((s) => s.importAppSettings); + const checkLocalEnabledPlugins = usePluginStore((s) => s.checkLocalEnabledPlugins); const importConfig = (info: any) => { importConfigFile(info, (config) => { @@ -19,6 +21,9 @@ export const useImportConfig = () => { case 'sessions': case 'agents': { importSessions(config.state.sessions); + + // 检查一下插件开启情况 + checkLocalEnabledPlugins(config.state.sessions); break; } @@ -26,6 +31,8 @@ export const useImportConfig = () => { importSessions(config.state.sessions); importAppSettings(config.state.settings); + // 检查一下插件开启情况 + checkLocalEnabledPlugins(config.state.sessions); break; } } diff --git a/src/locales/default/plugin.ts b/src/locales/default/plugin.ts index 28e05f48..7c78054f 100644 --- a/src/locales/default/plugin.ts +++ b/src/locales/default/plugin.ts @@ -12,7 +12,7 @@ export default { plugins: { realtimeWeather: '实时天气预报', searchEngine: '搜索引擎', - undefined: '插件检测中...', + unknown: '插件检测中...', websiteCrawler: '网页内容提取', }, }; diff --git a/src/pages/chat/features/Conversation/ChatList/Plugins/FunctionCall.tsx b/src/pages/chat/features/Conversation/ChatList/Plugins/FunctionCall.tsx index 00fd8a35..f1d4f989 100644 --- a/src/pages/chat/features/Conversation/ChatList/Plugins/FunctionCall.tsx +++ b/src/pages/chat/features/Conversation/ChatList/Plugins/FunctionCall.tsx @@ -7,77 +7,81 @@ import { useTranslation } from 'react-i18next'; import { Flexbox } from 'react-layout-kit'; import { pluginSelectors, usePluginStore } from '@/store/plugin'; -import { OpenAIFunctionCall } from '@/types/chatMessage'; import PluginResult from './PluginResultRender'; import { useStyles } from './style'; export interface FunctionCallProps { + arguments?: string; + command?: any; content: string; - function_call?: OpenAIFunctionCall; + id?: string; loading?: boolean; } -const FunctionCall = memo(({ function_call, loading, content }) => { - const { t } = useTranslation('plugin'); - const { styles } = useStyles(); - const [open, setOpen] = useState(false); - const item = usePluginStore(pluginSelectors.getPluginMetaByName(function_call?.name || '')); +const FunctionCall = memo( + ({ arguments: requestArgs = '{}', command, loading, content, id = 'unknown' }) => { + const { t } = useTranslation('plugin'); + const { styles } = useStyles(); + const [open, setOpen] = useState(false); - const avatar = item?.meta.avatar ? ( - - ) : ( - - ); + const item = usePluginStore(pluginSelectors.getPluginMetaById(id)); - const args = JSON.stringify(function_call, null, 2); - const params = JSON.stringify(JSON.parse(function_call?.arguments || '{}'), null, 2); + const avatar = item?.meta.avatar ? ( + + ) : ( + + ); - return ( - - { - setOpen(!open); - }} - > - {loading ? ( -
- -
- ) : ( - avatar + const args = JSON.stringify(command, null, 2); + const params = JSON.stringify(JSON.parse(requestArgs), null, 2); + + return ( + + { + setOpen(!open); + }} + > + {loading ? ( +
+ +
+ ) : ( + avatar + )} + {t(`plugins.${id}` as any, { ns: 'plugin' })} + +
+ {open && ( + {args}, + key: 'function_call', + label: t('debug.function_call'), + }, + { + children: {params}, + key: 'arguments', + label: t('debug.arguments'), + }, + { + children: , + key: 'response', + label: t('debug.response'), + }, + ]} + style={{ maxWidth: 800 }} + /> )} - {t(`plugins.${function_call?.name}` as any, { ns: 'plugin' })} -
- {open && ( - {args}, - key: 'function_call', - label: t('debug.function_call'), - }, - { - children: {params}, - key: 'arguments', - label: t('debug.arguments'), - }, - { - children: , - key: 'response', - label: t('debug.response'), - }, - ]} - style={{ maxWidth: 800 }} - /> - )} -
- ); -}); + ); + }, +); export default FunctionCall; diff --git a/src/pages/chat/features/Conversation/ChatList/index.tsx b/src/pages/chat/features/Conversation/ChatList/index.tsx index 1f93e4d0..06126ffe 100644 --- a/src/pages/chat/features/Conversation/ChatList/index.tsx +++ b/src/pages/chat/features/Conversation/ChatList/index.tsx @@ -49,25 +49,28 @@ const List = () => { const renderMessage: RenderMessage = useCallback( (content, message: ChatMessage) => { + const id = message.plugin?.identifier || message.function_call?.name; + const command = message.plugin ?? message.function_call; + const args = command?.arguments; + const fcProps = { + arguments: args, + command, + content: message.content, + id, + loading: message.id === chatLoadingId, + }; + if (message.role === 'function') return ( - + ); if (message.role === 'assistant') { return isFunctionMessage(message.content) || !!message.function_call ? ( - + ) : ( content ); diff --git a/src/pages/chat/features/Header/PluginTag.tsx b/src/pages/chat/features/Header/PluginTag.tsx index 01709257..c088b268 100644 --- a/src/pages/chat/features/Header/PluginTag.tsx +++ b/src/pages/chat/features/Header/PluginTag.tsx @@ -19,7 +19,7 @@ const PluginTag = memo(({ plugins }) => { if (plugins.length === 0) return null; const items: MenuProps['items'] = plugins.map((id) => { - const item = list?.find((p) => p.name === id); + const item = list?.find((p) => p.identifier === id); return { icon: ( diff --git a/src/store/plugin/action.ts b/src/store/plugin/action.ts index 1d73e362..69042680 100644 --- a/src/store/plugin/action.ts +++ b/src/store/plugin/action.ts @@ -1,5 +1,5 @@ import { - LobeChatPlugin, + LobeChatPluginManifest, LobeChatPluginsMarketIndex, pluginManifestSchema, } from '@lobehub/chat-plugin-sdk'; @@ -10,6 +10,7 @@ import useSWR, { SWRResponse } from 'swr'; import { StateCreator } from 'zustand/vanilla'; import { getPluginList } from '@/services/plugin'; +import { pluginSelectors } from '@/store/plugin/selectors'; import { LobeSessions } from '@/types/session'; import { setNamespace } from '@/utils/storeDebug'; @@ -51,7 +52,6 @@ export const createPluginSlice: StateCreator< await Promise.all(plugins.map((name) => fetchPluginManifest(name))); - console.log('fetched'); set({ manifestPrepared: true }, false, t('checkLocalEnabledPlugins')); }, dispatchPluginManifest: (payload) => { @@ -62,7 +62,7 @@ export const createPluginSlice: StateCreator< }, fetchPluginManifest: async (name) => { - const plugin = get().pluginList.find((plugin) => plugin.name === name); + const plugin = pluginSelectors.getPluginMetaById(name)(get()); // 1. 校验文件 if (!plugin) return; @@ -74,7 +74,7 @@ export const createPluginSlice: StateCreator< // 2. 发送请求 get().updateManifestLoadingState(name, true); - let data: LobeChatPlugin | null; + let data: LobeChatPluginManifest | null; try { const res = await fetch(plugin.manifest); @@ -100,7 +100,7 @@ export const createPluginSlice: StateCreator< } // 4. 存储 manifest 信息 - get().dispatchPluginManifest({ id: plugin.name, plugin: data, type: 'addManifest' }); + get().dispatchPluginManifest({ id: plugin.identifier, plugin: data, type: 'addManifest' }); }, updateManifestLoadingState: (key, value) => { diff --git a/src/store/plugin/reducers/manifest.ts b/src/store/plugin/reducers/manifest.ts index 509f2e27..4e7531e8 100644 --- a/src/store/plugin/reducers/manifest.ts +++ b/src/store/plugin/reducers/manifest.ts @@ -1,9 +1,9 @@ -import { LobeChatPlugin } from '@lobehub/chat-plugin-sdk'; +import { LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk'; import { produce } from 'immer'; import { PluginManifestMap } from '@/types/plugin'; -type AddManifestDispatch = { id: string; plugin: LobeChatPlugin; type: 'addManifest' }; +type AddManifestDispatch = { id: string; plugin: LobeChatPluginManifest; type: 'addManifest' }; type DeleteManifestDispatch = { id: string; type: 'deleteManifest' }; // type UpdateManifestDispatch = { // id: string; diff --git a/src/store/plugin/selectors.ts b/src/store/plugin/selectors.ts index 4f5e8652..955a2ba1 100644 --- a/src/store/plugin/selectors.ts +++ b/src/store/plugin/selectors.ts @@ -1,3 +1,5 @@ +import { PLUGIN_SCHEMA_SEPARATOR } from '@/const/plugin'; + import { PluginStoreState } from './initialState'; const enabledSchema = @@ -9,15 +11,21 @@ const enabledSchema = if (!enabledPlugins) return false; // 如果存在 enabledPlugins,那么只启用 enabledPlugins 中的插件 - return enabledPlugins.includes(p.name); + return enabledPlugins.includes(p.identifier); }) - .map((i) => i.schema); + .flatMap((manifest) => + manifest.api.map((m) => ({ + ...m, + // 将插件的 identifier 作为前缀,避免重复 + name: manifest.identifier + PLUGIN_SCHEMA_SEPARATOR + m.name, + })), + ); }; -const getPluginMetaByName = (name: string) => (s: PluginStoreState) => - s.pluginList?.find((p) => p.name === name); +const getPluginMetaById = (id: string) => (s: PluginStoreState) => + s.pluginList?.find((p) => p.identifier === id); export const pluginSelectors = { enabledSchema, - getPluginMetaByName, + getPluginMetaById, }; diff --git a/src/store/plugin/store.ts b/src/store/plugin/store.ts index ae6d705b..496503b9 100644 --- a/src/store/plugin/store.ts +++ b/src/store/plugin/store.ts @@ -19,7 +19,7 @@ const createStore: StateCreator = (. // =============== persist 本地缓存中间件配置 ============ // -type SessionPersist = Pick; +type SessionPersist = Pick; const storeName = 'LOBE_PLUGIN'; @@ -28,6 +28,7 @@ const persistOptions: PersistOptions = { partialize: (s) => ({ pluginList: s.pluginList, + pluginManifestMap: s.pluginManifestMap, }), // 手动控制 Hydration ,避免 ssr 报错 diff --git a/src/store/session/slices/chat/actions/message.ts b/src/store/session/slices/chat/actions/message.ts index 7d187496..91e84538 100644 --- a/src/store/session/slices/chat/actions/message.ts +++ b/src/store/session/slices/chat/actions/message.ts @@ -1,7 +1,9 @@ +import { PluginRequestPayload } from '@lobehub/chat-plugin-sdk'; import { template } from 'lodash-es'; import { StateCreator } from 'zustand/vanilla'; import { LOADING_FLAT } from '@/const/message'; +import { PLUGIN_SCHEMA_SEPARATOR } from '@/const/plugin'; import { fetchChatModel } from '@/services/chatModel'; import { fetchPlugin } from '@/services/plugin'; import { SessionStore } from '@/store/session'; @@ -319,32 +321,30 @@ export const chatMessage: StateCreator< const message = session.chats[id]; if (!message) return; - let payload: OpenAIFunctionCall = { name: '' }; + let payload: PluginRequestPayload = { apiName: '', identifier: '' }; + // 识别到内容是 function_call 的情况下 + // 将 function_call 转换为 plugin request payload if (message.content) { - const { function_call } = JSON.parse(message.content); - dispatchMessage({ id, key: 'function_call', type: 'updateMessage', value: function_call }); + const { function_call } = JSON.parse(message.content) as { + function_call: OpenAIFunctionCall; + }; + + const [identifier, apiName] = function_call.name.split(PLUGIN_SCHEMA_SEPARATOR); + payload = { apiName, arguments: function_call.arguments, identifier }; + + dispatchMessage({ id, key: 'plugin', type: 'updateMessage', value: payload }); dispatchMessage({ id, key: 'content', type: 'updateMessage', value: '' }); - payload = function_call; } else { - if (message.function_call) { - payload = message.function_call; + if (message.plugin) { + payload = message.plugin; } } - if (!payload.name) return; + if (!payload.apiName) return; - // const fid = nanoid(); dispatchMessage({ id, key: 'role', type: 'updateMessage', value: 'function' }); - dispatchMessage({ id, key: 'name', type: 'updateMessage', value: payload.name }); - dispatchMessage({ id, key: 'function_call', type: 'updateMessage', value: payload }); - - // dispatchMessage({ - // id: id, - // message: FUNCTION_LOADING, - // parentId: message., - // role: 'function', - // type: 'addMessage', - // }); + dispatchMessage({ id, key: 'name', type: 'updateMessage', value: payload.identifier }); + dispatchMessage({ id, key: 'plugin', type: 'updateMessage', value: payload }); const abortController = toggleChatLoading(true, id); const data = await fetchPlugin(payload, { signal: abortController?.signal }); diff --git a/src/store/session/slices/chat/selectors/utils.test.ts b/src/store/session/slices/chat/selectors/utils.test.ts index 8d4d9a87..94398691 100644 --- a/src/store/session/slices/chat/selectors/utils.test.ts +++ b/src/store/session/slices/chat/selectors/utils.test.ts @@ -342,14 +342,16 @@ describe('organizeChats', () => { ...params, pluginList: [ { - name: 'realtimeWeather', + identifier: 'realtimeWeather', + author: '123', meta: { avatar: '🧩', + title: '天气预报', }, createAt: 'abc', manifest: '', homepage: '', - schemaVersion: 'v1', + schemaVersion: 1, }, ], }); diff --git a/src/store/session/slices/chat/selectors/utils.ts b/src/store/session/slices/chat/selectors/utils.ts index a3f60f7c..a90d4bb1 100644 --- a/src/store/session/slices/chat/selectors/utils.ts +++ b/src/store/session/slices/chat/selectors/utils.ts @@ -38,11 +38,11 @@ export const organizeChats = ( } case 'function': { - const plugin = (pluginList || []).find((m) => m.name === message.name); + const plugin = (pluginList || []).find((m) => m.identifier === message.name); return { avatar: '🧩', - title: plugin?.name || 'plugin-unknown', + title: plugin?.identifier || 'plugin-unknown', }; } } diff --git a/src/types/chatMessage.ts b/src/types/chatMessage.ts index fbde89c5..3cf66b21 100644 --- a/src/types/chatMessage.ts +++ b/src/types/chatMessage.ts @@ -1,3 +1,5 @@ +import { PluginRequestPayload } from '@lobehub/chat-plugin-sdk'; + import { ErrorType } from '@/types/fetch'; import { LLMRoleType } from './llm'; @@ -33,10 +35,17 @@ export interface ChatMessage extends BaseDataModel { }; } & Record; + /** + * replace with plugin + * @deprecated + */ function_call?: OpenAIFunctionCall; name?: string; parentId?: string; + + plugin?: PluginRequestPayload; + // 引用 quotaId?: string; /** diff --git a/src/types/plugin.ts b/src/types/plugin.ts index e104c07f..38e26398 100644 --- a/src/types/plugin.ts +++ b/src/types/plugin.ts @@ -1,3 +1,3 @@ -import { LobeChatPlugin } from '@lobehub/chat-plugin-sdk'; +import { LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk'; -export type PluginManifestMap = Record; +export type PluginManifestMap = Record;