Skip to content

Commit 35a3a16

Browse files
authored
✨ feat: plugin default use iframe render (#141)
* 🌐 style: update i18n * 🐛 fix: 修正 functions 可能会重复的问题 * ✨ feat: 支持 iframe 模式的插件加载 * 🐛 fix: 优化默认规则
1 parent 7036f29 commit 35a3a16

File tree

10 files changed

+153
-16
lines changed

10 files changed

+153
-16
lines changed

.eslintrc.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,6 @@ config.rules['unicorn/explicit-length-check'] = 0;
1111
config.rules['unicorn/prefer-code-point'] = 0;
1212
config.rules['no-extra-boolean-cast'] = 0;
1313
config.rules['unicorn/no-useless-undefined'] = 0;
14+
config.rules['react/no-unknown-property'] = 0;
1415

1516
module.exports = config;

locales/en_US/plugin.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,11 @@
5252
"desc": "LobeChat will install the plugin using this link",
5353
"invalid": "The input manifest link is invalid or does not comply with the specification",
5454
"label": "Plugin Manifest URL",
55-
"urlError": "Please enter a valid URL"
55+
"urlError": "Please enter a valid URL",
56+
"jsonInvalid": "The manifest is not valid, validation result: \n\n {{error}}",
57+
"preview": "Preview Manifest",
58+
"refresh": "Refresh",
59+
"requestError": "Failed to request the link, please enter a valid link and check if the link allows cross-origin access"
5660
},
5761
"title": {
5862
"desc": "The title of the plugin",
@@ -73,7 +77,9 @@
7377
"manifest": "Function Description Manifest (Manifest)",
7478
"meta": "Plugin Metadata"
7579
},
76-
"title": "Add Custom Plugin"
80+
"title": "Add Custom Plugin",
81+
"update": "Update",
82+
"updateSuccess": "Plugin settings updated successfully"
7783
},
7884
"list": {
7985
"item": {

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@
6666
"@emoji-mart/data": "^1",
6767
"@emoji-mart/react": "^1",
6868
"@icons-pack/react-simple-icons": "^9",
69-
"@lobehub/chat-plugin-sdk": "^1.13.1",
69+
"@lobehub/chat-plugin-sdk": "^1.15.0",
7070
"@lobehub/chat-plugins-gateway": "^1.5.0",
7171
"@lobehub/ui": "latest",
7272
"@vercel/analytics": "^1",
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { useEffect } from 'react';
2+
3+
import { onPluginFetchMessage, onPluginReady } from './utils';
4+
5+
export const useOnPluginReady = (onReady: () => void) => {
6+
useEffect(() => {
7+
const fn = (e: MessageEvent) => {
8+
onPluginReady(e, onReady);
9+
};
10+
11+
window.addEventListener('message', fn);
12+
return () => {
13+
window.removeEventListener('message', fn);
14+
};
15+
}, []);
16+
};
17+
18+
export const useOnPluginFetchMessage = (onRequest: (data: any) => void, deps: any[] = []) => {
19+
useEffect(() => {
20+
const fn = (e: MessageEvent) => {
21+
onPluginFetchMessage(e, onRequest);
22+
};
23+
24+
window.addEventListener('message', fn);
25+
return () => {
26+
window.removeEventListener('message', fn);
27+
};
28+
}, deps);
29+
};
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { PluginRenderProps } from '@lobehub/chat-plugin-sdk';
2+
import { Skeleton } from 'antd';
3+
import { memo, useEffect, useRef, useState } from 'react';
4+
5+
import { useOnPluginFetchMessage, useOnPluginReady } from './hooks';
6+
import { sendMessageToPlugin } from './utils';
7+
8+
interface IFrameRenderProps extends PluginRenderProps {
9+
height?: number;
10+
url: string;
11+
width?: number;
12+
}
13+
14+
const IFrameRender = memo<IFrameRenderProps>(({ url, width = 800, height = 300, ...props }) => {
15+
const [loading, setLoading] = useState(true);
16+
const [readyForRender, setReady] = useState(false);
17+
const iframeRef = useRef<HTMLIFrameElement>(null);
18+
19+
useOnPluginReady(() => setReady(true));
20+
21+
// 当 props 发生变化时,主动向 iframe 发送数据
22+
useEffect(() => {
23+
const iframeWin = iframeRef.current?.contentWindow;
24+
25+
if (iframeWin && readyForRender) {
26+
sendMessageToPlugin(iframeWin, props);
27+
}
28+
}, [readyForRender, props]);
29+
30+
// 当接收到来自 iframe 的请求时,触发发送数据
31+
useOnPluginFetchMessage(() => {
32+
const iframeWin = iframeRef.current?.contentWindow;
33+
if (iframeWin) {
34+
sendMessageToPlugin(iframeWin, props);
35+
}
36+
}, [props]);
37+
38+
return (
39+
<>
40+
{loading && <Skeleton active style={{ width }} />}
41+
<iframe
42+
// @ts-ignore
43+
allowtransparency="true"
44+
height={height}
45+
hidden={loading}
46+
onLoad={() => {
47+
setLoading(false);
48+
}}
49+
ref={iframeRef}
50+
src={url}
51+
style={{
52+
border: 0,
53+
// iframe 在 color-scheme:dark 模式下无法透明
54+
// refs: https://www.jianshu.com/p/bc5a37bb6a7b
55+
colorScheme: 'light',
56+
}}
57+
width={width}
58+
/>
59+
</>
60+
);
61+
});
62+
export default IFrameRender;
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { PluginChannel } from '@lobehub/chat-plugin-sdk';
2+
3+
export const onPluginReady = (e: MessageEvent, onReady: () => void) => {
4+
if (e.data.type === PluginChannel.pluginReadyForRender) {
5+
onReady();
6+
}
7+
};
8+
9+
export const onPluginFetchMessage = (e: MessageEvent, onRequest: (data: any) => void) => {
10+
if (e.data.type === PluginChannel.fetchPluginMessage) {
11+
onRequest(e.data);
12+
}
13+
};
14+
15+
export const sendMessageToPlugin = (window: Window, props: any) => {
16+
window.postMessage({ props, type: PluginChannel.renderPlugin }, '*');
17+
};

src/pages/chat/features/Conversation/ChatList/Plugins/PluginMessage.tsx

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { Loading3QuartersOutlined } from '@ant-design/icons';
2-
import { memo } from 'react';
2+
import { memo, useMemo } from 'react';
33
import { useTranslation } from 'react-i18next';
44
import { Flexbox } from 'react-layout-kit';
55

66
import { usePluginStore } from '@/store/plugin';
77
import { ChatMessage } from '@/types/chatMessage';
88

9-
import CustomRender from './CustomRender';
9+
import IFrameRender from './IFrameRender';
10+
import SystemJsRender from './SystemJsRender';
1011

1112
export interface FunctionMessageProps extends ChatMessage {
1213
loading?: boolean;
@@ -23,6 +24,8 @@ const PluginMessage = memo<FunctionMessageProps>(({ content, name }) => {
2324
isJSON = false;
2425
}
2526

27+
const contentObj = useMemo(() => (isJSON ? JSON.parse(content) : content), [content]);
28+
2629
// if (!loading)
2730

2831
if (!isJSON) {
@@ -36,10 +39,23 @@ const PluginMessage = memo<FunctionMessageProps>(({ content, name }) => {
3639
);
3740
}
3841

39-
if (!manifest?.ui?.url) return;
42+
if (!manifest?.ui) return;
43+
44+
const ui = manifest.ui;
45+
46+
if (!ui.url) return;
47+
48+
if (ui.mode === 'module')
49+
return <SystemJsRender content={contentObj} name={name || 'unknown'} url={ui.url} />;
4050

4151
return (
42-
<CustomRender content={JSON.parse(content)} name={name || 'unknown'} url={manifest.ui?.url} />
52+
<IFrameRender
53+
content={contentObj}
54+
height={ui.height}
55+
name={name || 'unknown'}
56+
url={ui.url}
57+
width={ui.width}
58+
/>
4359
);
4460
});
4561

src/pages/chat/features/Conversation/ChatList/Plugins/CustomRender.tsx renamed to src/pages/chat/features/Conversation/ChatList/Plugins/SystemJsRender/index.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@ import { PluginRender, PluginRenderProps } from '@lobehub/chat-plugin-sdk';
22
import { Skeleton } from 'antd';
33
import { memo, useEffect, useState } from 'react';
44

5-
import { system } from './dynamticLoader';
5+
import { system } from './utils';
66

7-
interface CustomRenderProps extends PluginRenderProps {
7+
interface SystemJsRenderProps extends PluginRenderProps {
88
url: string;
99
}
10-
const CustomRender = memo<CustomRenderProps>(({ url, ...props }) => {
10+
11+
const SystemJsRender = memo<SystemJsRenderProps>(({ url, ...props }) => {
1112
const [component, setComp] = useState<PluginRender | null>(null);
1213

1314
useEffect(() => {
@@ -31,4 +32,4 @@ const CustomRender = memo<CustomRenderProps>(({ url, ...props }) => {
3132

3233
return <Render {...props} />;
3334
});
34-
export default CustomRender;
35+
export default SystemJsRender;

src/pages/chat/features/Conversation/ChatList/Plugins/dynamticLoader.ts renamed to src/pages/chat/features/Conversation/ChatList/Plugins/SystemJsRender/utils.ts

File renamed without changes.

src/store/plugin/selectors.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
1+
import { uniqBy } from 'lodash-es';
2+
import { ChatCompletionFunctions } from 'openai-edge/types/api';
3+
14
import { PLUGIN_SCHEMA_SEPARATOR } from '@/const/plugin';
25
import { pluginHelpers } from '@/store/plugin/helpers';
36

47
import { PluginStoreState } from './initialState';
58

69
const enabledSchema =
710
(enabledPlugins: string[] = []) =>
8-
(s: PluginStoreState) => {
9-
return Object.values(s.pluginManifestMap)
10-
.filter((p) => {
11-
// 如果不存在 enabledPlugins,那么全部不启用
12-
if (!enabledPlugins) return false;
11+
(s: PluginStoreState): ChatCompletionFunctions[] => {
12+
// 如果不存在 enabledPlugins,那么全部不启用
13+
if (!enabledPlugins) return [];
1314

15+
const list = Object.values(s.pluginManifestMap)
16+
.filter((p) => {
1417
// 如果存在 enabledPlugins,那么只启用 enabledPlugins 中的插件
1518
return enabledPlugins.includes(p.identifier);
1619
})
@@ -21,6 +24,8 @@ const enabledSchema =
2124
name: manifest.identifier + PLUGIN_SCHEMA_SEPARATOR + m.name,
2225
})),
2326
);
27+
28+
return uniqBy(list, 'name');
2429
};
2530

2631
const pluginList = (s: PluginStoreState) => [...s.pluginList, ...s.customPluginList];

0 commit comments

Comments
 (0)