Skip to content

Commit

Permalink
✨ feat: use new plugin manifest to support plugin’s multi api (#101)
Browse files Browse the repository at this point in the history
* ✨ feat: support new plugin manifest to support plugin’s multi api

* 🐛 fix: 修正导入消息时插件没有初始化的问题

* 💬 style: 修正插件检测中文案
  • Loading branch information
arvinxx authored Aug 24, 2023
1 parent 9ecdef4 commit 4534598
Show file tree
Hide file tree
Showing 17 changed files with 150 additions and 115 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions src/const/plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const PLUGIN_SCHEMA_SEPARATOR = '--__--';
14 changes: 7 additions & 7 deletions src/features/AgentSetting/AgentPlugin/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,23 +75,23 @@ const AgentPlugin = memo<AgentPluginProps>(({ config, updateConfig }) => {
<Switch
checked={
// 如果在加载中,说明激活了
pluginManifestLoading[item.name] || !config.plugins
pluginManifestLoading[item.identifier] || !config.plugins
? false
: config.plugins.includes(item.name)
: config.plugins.includes(item.identifier)
}
loading={pluginManifestLoading[item.name]}
loading={pluginManifestLoading[item.identifier]}
onChange={(checked) => {
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 {
Expand Down
7 changes: 7 additions & 0 deletions src/hooks/useImportConfig.ts
Original file line number Diff line number Diff line change
@@ -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) => {
Expand All @@ -19,13 +21,18 @@ export const useImportConfig = () => {
case 'sessions':
case 'agents': {
importSessions(config.state.sessions);

// 检查一下插件开启情况
checkLocalEnabledPlugins(config.state.sessions);
break;
}

case 'all': {
importSessions(config.state.sessions);
importAppSettings(config.state.settings);

// 检查一下插件开启情况
checkLocalEnabledPlugins(config.state.sessions);
break;
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/locales/default/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export default {
plugins: {
realtimeWeather: '实时天气预报',
searchEngine: '搜索引擎',
undefined: '插件检测中...',
unknown: '插件检测中...',
websiteCrawler: '网页内容提取',
},
};
120 changes: 62 additions & 58 deletions src/pages/chat/features/Conversation/ChatList/Plugins/FunctionCall.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<FunctionCallProps>(({ 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<FunctionCallProps>(
({ arguments: requestArgs = '{}', command, loading, content, id = 'unknown' }) => {
const { t } = useTranslation('plugin');
const { styles } = useStyles();
const [open, setOpen] = useState(false);

const avatar = item?.meta.avatar ? (
<Avatar avatar={item?.meta.avatar} size={32} />
) : (
<Icon icon={LucideToyBrick} />
);
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 ? (
<Avatar avatar={item?.meta.avatar} size={32} />
) : (
<Icon icon={LucideToyBrick} />
);

return (
<Flexbox gap={8}>
<Flexbox
align={'center'}
className={styles.container}
gap={8}
horizontal
onClick={() => {
setOpen(!open);
}}
>
{loading ? (
<div>
<LoadingOutlined />
</div>
) : (
avatar
const args = JSON.stringify(command, null, 2);
const params = JSON.stringify(JSON.parse(requestArgs), null, 2);

return (
<Flexbox gap={8}>
<Flexbox
align={'center'}
className={styles.container}
gap={8}
horizontal
onClick={() => {
setOpen(!open);
}}
>
{loading ? (
<div>
<LoadingOutlined />
</div>
) : (
avatar
)}
{t(`plugins.${id}` as any, { ns: 'plugin' })}
<Icon icon={open ? LucideChevronUp : LucideChevronDown} />
</Flexbox>
{open && (
<Tabs
items={[
{
children: <Highlighter language={'json'}>{args}</Highlighter>,
key: 'function_call',
label: t('debug.function_call'),
},
{
children: <Highlighter language={'json'}>{params}</Highlighter>,
key: 'arguments',
label: t('debug.arguments'),
},
{
children: <PluginResult content={content} />,
key: 'response',
label: t('debug.response'),
},
]}
style={{ maxWidth: 800 }}
/>
)}
{t(`plugins.${function_call?.name}` as any, { ns: 'plugin' })}
<Icon icon={open ? LucideChevronUp : LucideChevronDown} />
</Flexbox>
{open && (
<Tabs
items={[
{
children: <Highlighter language={'json'}>{args}</Highlighter>,
key: 'function_call',
label: t('debug.function_call'),
},
{
children: <Highlighter language={'json'}>{params}</Highlighter>,
key: 'arguments',
label: t('debug.arguments'),
},
{
children: <PluginResult content={content} />,
key: 'response',
label: t('debug.response'),
},
]}
style={{ maxWidth: 800 }}
/>
)}
</Flexbox>
);
});
);
},
);

export default FunctionCall;
23 changes: 13 additions & 10 deletions src/pages/chat/features/Conversation/ChatList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<Flexbox gap={12}>
<FunctionCall
content={message.content}
function_call={message.function_call}
loading={message.id === chatLoadingId}
/>
<FunctionCall {...fcProps} />
<PluginMessage loading={message.id === chatLoadingId} {...message} />
</Flexbox>
);

if (message.role === 'assistant') {
return isFunctionMessage(message.content) || !!message.function_call ? (
<FunctionCall
content={message.content}
function_call={message.function_call}
loading={message.id === chatLoadingId}
/>
<FunctionCall {...fcProps} />
) : (
content
);
Expand Down
2 changes: 1 addition & 1 deletion src/pages/chat/features/Header/PluginTag.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const PluginTag = memo<PluginTagProps>(({ 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: (
<Avatar avatar={item?.meta.avatar} size={24} style={{ marginLeft: -6, marginRight: 2 }} />
Expand Down
10 changes: 5 additions & 5 deletions src/store/plugin/action.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {
LobeChatPlugin,
LobeChatPluginManifest,
LobeChatPluginsMarketIndex,
pluginManifestSchema,
} from '@lobehub/chat-plugin-sdk';
Expand All @@ -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';

Expand Down Expand Up @@ -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) => {
Expand All @@ -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;
Expand All @@ -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);
Expand All @@ -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) => {
Expand Down
4 changes: 2 additions & 2 deletions src/store/plugin/reducers/manifest.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
18 changes: 13 additions & 5 deletions src/store/plugin/selectors.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { PLUGIN_SCHEMA_SEPARATOR } from '@/const/plugin';

import { PluginStoreState } from './initialState';

const enabledSchema =
Expand All @@ -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,
};
3 changes: 2 additions & 1 deletion src/store/plugin/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const createStore: StateCreator<PluginStore, [['zustand/devtools', never]]> = (.

// =============== persist 本地缓存中间件配置 ============ //

type SessionPersist = Pick<PluginStore, 'pluginList'>;
type SessionPersist = Pick<PluginStore, 'pluginList' | 'pluginManifestMap'>;

const storeName = 'LOBE_PLUGIN';

Expand All @@ -28,6 +28,7 @@ const persistOptions: PersistOptions<PluginStore, SessionPersist> = {

partialize: (s) => ({
pluginList: s.pluginList,
pluginManifestMap: s.pluginManifestMap,
}),

// 手动控制 Hydration ,避免 ssr 报错
Expand Down
Loading

0 comments on commit 4534598

Please sign in to comment.