mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-26 13:19:34 +07:00
✨ feat: plugin default use iframe render (#141)
* 🌐 style: update i18n * 🐛 fix: 修正 functions 可能会重复的问题 * ✨ feat: 支持 iframe 模式的插件加载 * 🐛 fix: 优化默认规则
This commit is contained in:
@@ -11,5 +11,6 @@ config.rules['unicorn/explicit-length-check'] = 0;
|
||||
config.rules['unicorn/prefer-code-point'] = 0;
|
||||
config.rules['no-extra-boolean-cast'] = 0;
|
||||
config.rules['unicorn/no-useless-undefined'] = 0;
|
||||
config.rules['react/no-unknown-property'] = 0;
|
||||
|
||||
module.exports = config;
|
||||
|
||||
@@ -52,7 +52,11 @@
|
||||
"desc": "LobeChat will install the plugin using this link",
|
||||
"invalid": "The input manifest link is invalid or does not comply with the specification",
|
||||
"label": "Plugin Manifest URL",
|
||||
"urlError": "Please enter a valid URL"
|
||||
"urlError": "Please enter a valid URL",
|
||||
"jsonInvalid": "The manifest is not valid, validation result: \n\n {{error}}",
|
||||
"preview": "Preview Manifest",
|
||||
"refresh": "Refresh",
|
||||
"requestError": "Failed to request the link, please enter a valid link and check if the link allows cross-origin access"
|
||||
},
|
||||
"title": {
|
||||
"desc": "The title of the plugin",
|
||||
@@ -73,7 +77,9 @@
|
||||
"manifest": "Function Description Manifest (Manifest)",
|
||||
"meta": "Plugin Metadata"
|
||||
},
|
||||
"title": "Add Custom Plugin"
|
||||
"title": "Add Custom Plugin",
|
||||
"update": "Update",
|
||||
"updateSuccess": "Plugin settings updated successfully"
|
||||
},
|
||||
"list": {
|
||||
"item": {
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
"@emoji-mart/data": "^1",
|
||||
"@emoji-mart/react": "^1",
|
||||
"@icons-pack/react-simple-icons": "^9",
|
||||
"@lobehub/chat-plugin-sdk": "^1.13.1",
|
||||
"@lobehub/chat-plugin-sdk": "^1.15.0",
|
||||
"@lobehub/chat-plugins-gateway": "^1.5.0",
|
||||
"@lobehub/ui": "latest",
|
||||
"@vercel/analytics": "^1",
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { onPluginFetchMessage, onPluginReady } from './utils';
|
||||
|
||||
export const useOnPluginReady = (onReady: () => void) => {
|
||||
useEffect(() => {
|
||||
const fn = (e: MessageEvent) => {
|
||||
onPluginReady(e, onReady);
|
||||
};
|
||||
|
||||
window.addEventListener('message', fn);
|
||||
return () => {
|
||||
window.removeEventListener('message', fn);
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
|
||||
export const useOnPluginFetchMessage = (onRequest: (data: any) => void, deps: any[] = []) => {
|
||||
useEffect(() => {
|
||||
const fn = (e: MessageEvent) => {
|
||||
onPluginFetchMessage(e, onRequest);
|
||||
};
|
||||
|
||||
window.addEventListener('message', fn);
|
||||
return () => {
|
||||
window.removeEventListener('message', fn);
|
||||
};
|
||||
}, deps);
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
import { PluginRenderProps } from '@lobehub/chat-plugin-sdk';
|
||||
import { Skeleton } from 'antd';
|
||||
import { memo, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { useOnPluginFetchMessage, useOnPluginReady } from './hooks';
|
||||
import { sendMessageToPlugin } from './utils';
|
||||
|
||||
interface IFrameRenderProps extends PluginRenderProps {
|
||||
height?: number;
|
||||
url: string;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
const IFrameRender = memo<IFrameRenderProps>(({ url, width = 800, height = 300, ...props }) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [readyForRender, setReady] = useState(false);
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||
|
||||
useOnPluginReady(() => setReady(true));
|
||||
|
||||
// 当 props 发生变化时,主动向 iframe 发送数据
|
||||
useEffect(() => {
|
||||
const iframeWin = iframeRef.current?.contentWindow;
|
||||
|
||||
if (iframeWin && readyForRender) {
|
||||
sendMessageToPlugin(iframeWin, props);
|
||||
}
|
||||
}, [readyForRender, props]);
|
||||
|
||||
// 当接收到来自 iframe 的请求时,触发发送数据
|
||||
useOnPluginFetchMessage(() => {
|
||||
const iframeWin = iframeRef.current?.contentWindow;
|
||||
if (iframeWin) {
|
||||
sendMessageToPlugin(iframeWin, props);
|
||||
}
|
||||
}, [props]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{loading && <Skeleton active style={{ width }} />}
|
||||
<iframe
|
||||
// @ts-ignore
|
||||
allowtransparency="true"
|
||||
height={height}
|
||||
hidden={loading}
|
||||
onLoad={() => {
|
||||
setLoading(false);
|
||||
}}
|
||||
ref={iframeRef}
|
||||
src={url}
|
||||
style={{
|
||||
border: 0,
|
||||
// iframe 在 color-scheme:dark 模式下无法透明
|
||||
// refs: https://www.jianshu.com/p/bc5a37bb6a7b
|
||||
colorScheme: 'light',
|
||||
}}
|
||||
width={width}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
export default IFrameRender;
|
||||
@@ -0,0 +1,17 @@
|
||||
import { PluginChannel } from '@lobehub/chat-plugin-sdk';
|
||||
|
||||
export const onPluginReady = (e: MessageEvent, onReady: () => void) => {
|
||||
if (e.data.type === PluginChannel.pluginReadyForRender) {
|
||||
onReady();
|
||||
}
|
||||
};
|
||||
|
||||
export const onPluginFetchMessage = (e: MessageEvent, onRequest: (data: any) => void) => {
|
||||
if (e.data.type === PluginChannel.fetchPluginMessage) {
|
||||
onRequest(e.data);
|
||||
}
|
||||
};
|
||||
|
||||
export const sendMessageToPlugin = (window: Window, props: any) => {
|
||||
window.postMessage({ props, type: PluginChannel.renderPlugin }, '*');
|
||||
};
|
||||
@@ -1,12 +1,13 @@
|
||||
import { Loading3QuartersOutlined } from '@ant-design/icons';
|
||||
import { memo } from 'react';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import { usePluginStore } from '@/store/plugin';
|
||||
import { ChatMessage } from '@/types/chatMessage';
|
||||
|
||||
import CustomRender from './CustomRender';
|
||||
import IFrameRender from './IFrameRender';
|
||||
import SystemJsRender from './SystemJsRender';
|
||||
|
||||
export interface FunctionMessageProps extends ChatMessage {
|
||||
loading?: boolean;
|
||||
@@ -23,6 +24,8 @@ const PluginMessage = memo<FunctionMessageProps>(({ content, name }) => {
|
||||
isJSON = false;
|
||||
}
|
||||
|
||||
const contentObj = useMemo(() => (isJSON ? JSON.parse(content) : content), [content]);
|
||||
|
||||
// if (!loading)
|
||||
|
||||
if (!isJSON) {
|
||||
@@ -36,10 +39,23 @@ const PluginMessage = memo<FunctionMessageProps>(({ content, name }) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (!manifest?.ui?.url) return;
|
||||
if (!manifest?.ui) return;
|
||||
|
||||
const ui = manifest.ui;
|
||||
|
||||
if (!ui.url) return;
|
||||
|
||||
if (ui.mode === 'module')
|
||||
return <SystemJsRender content={contentObj} name={name || 'unknown'} url={ui.url} />;
|
||||
|
||||
return (
|
||||
<CustomRender content={JSON.parse(content)} name={name || 'unknown'} url={manifest.ui?.url} />
|
||||
<IFrameRender
|
||||
content={contentObj}
|
||||
height={ui.height}
|
||||
name={name || 'unknown'}
|
||||
url={ui.url}
|
||||
width={ui.width}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -2,12 +2,13 @@ import { PluginRender, PluginRenderProps } from '@lobehub/chat-plugin-sdk';
|
||||
import { Skeleton } from 'antd';
|
||||
import { memo, useEffect, useState } from 'react';
|
||||
|
||||
import { system } from './dynamticLoader';
|
||||
import { system } from './utils';
|
||||
|
||||
interface CustomRenderProps extends PluginRenderProps {
|
||||
interface SystemJsRenderProps extends PluginRenderProps {
|
||||
url: string;
|
||||
}
|
||||
const CustomRender = memo<CustomRenderProps>(({ url, ...props }) => {
|
||||
|
||||
const SystemJsRender = memo<SystemJsRenderProps>(({ url, ...props }) => {
|
||||
const [component, setComp] = useState<PluginRender | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -31,4 +32,4 @@ const CustomRender = memo<CustomRenderProps>(({ url, ...props }) => {
|
||||
|
||||
return <Render {...props} />;
|
||||
});
|
||||
export default CustomRender;
|
||||
export default SystemJsRender;
|
||||
@@ -1,3 +1,6 @@
|
||||
import { uniqBy } from 'lodash-es';
|
||||
import { ChatCompletionFunctions } from 'openai-edge/types/api';
|
||||
|
||||
import { PLUGIN_SCHEMA_SEPARATOR } from '@/const/plugin';
|
||||
import { pluginHelpers } from '@/store/plugin/helpers';
|
||||
|
||||
@@ -5,12 +8,12 @@ import { PluginStoreState } from './initialState';
|
||||
|
||||
const enabledSchema =
|
||||
(enabledPlugins: string[] = []) =>
|
||||
(s: PluginStoreState) => {
|
||||
return Object.values(s.pluginManifestMap)
|
||||
.filter((p) => {
|
||||
// 如果不存在 enabledPlugins,那么全部不启用
|
||||
if (!enabledPlugins) return false;
|
||||
(s: PluginStoreState): ChatCompletionFunctions[] => {
|
||||
// 如果不存在 enabledPlugins,那么全部不启用
|
||||
if (!enabledPlugins) return [];
|
||||
|
||||
const list = Object.values(s.pluginManifestMap)
|
||||
.filter((p) => {
|
||||
// 如果存在 enabledPlugins,那么只启用 enabledPlugins 中的插件
|
||||
return enabledPlugins.includes(p.identifier);
|
||||
})
|
||||
@@ -21,6 +24,8 @@ const enabledSchema =
|
||||
name: manifest.identifier + PLUGIN_SCHEMA_SEPARATOR + m.name,
|
||||
})),
|
||||
);
|
||||
|
||||
return uniqBy(list, 'name');
|
||||
};
|
||||
|
||||
const pluginList = (s: PluginStoreState) => [...s.pluginList, ...s.customPluginList];
|
||||
|
||||
Reference in New Issue
Block a user