@lobehub/chat 0.94.5 → 0.95.1

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.
Files changed (43) hide show
  1. package/.github/workflows/pr-welcome.yml +1 -1
  2. package/CHANGELOG.md +60 -0
  3. package/README.md +4 -4
  4. package/docs/Lighthouse.md +8 -8
  5. package/docs/Lighthouse.zh-CN.md +8 -8
  6. package/package.json +4 -4
  7. package/src/app/chat/(desktop)/features/ChatInput.tsx +0 -1
  8. package/src/app/chat/features/Conversation/ChatList/Error/Plugin/PluginSettings.tsx +1 -1
  9. package/src/app/chat/features/Conversation/ChatList/Messages/Assistant.tsx +12 -15
  10. package/src/app/chat/features/Conversation/ChatList/Messages/Function.tsx +26 -15
  11. package/src/app/chat/features/Conversation/ChatList/Plugins/Inspector/Settings.tsx +36 -0
  12. package/src/app/chat/features/Conversation/ChatList/Plugins/Inspector/index.tsx +126 -0
  13. package/src/app/chat/features/Conversation/ChatList/Plugins/{style.ts → Inspector/style.ts} +1 -0
  14. package/src/app/chat/features/Conversation/ChatList/Plugins/{IFrameRender → Render/DefaultType/IFrameRender}/index.tsx +5 -16
  15. package/src/app/chat/features/Conversation/ChatList/Plugins/Render/DefaultType/index.tsx +117 -0
  16. package/src/app/chat/features/Conversation/ChatList/Plugins/Render/StandaloneType/Iframe.tsx +136 -0
  17. package/src/app/chat/features/Conversation/ChatList/Plugins/Render/StandaloneType/index.tsx +28 -0
  18. package/src/app/chat/features/Conversation/ChatList/Plugins/Render/index.tsx +28 -0
  19. package/src/app/chat/features/Conversation/ChatList/Plugins/Render/utils/iframeOnReady.ts +23 -0
  20. package/src/app/chat/features/Conversation/ChatList/Plugins/Render/utils/listenToPlugin.ts +65 -0
  21. package/src/app/chat/features/Conversation/ChatList/Plugins/Render/utils/pluginSettings.ts +17 -0
  22. package/src/app/chat/features/Conversation/ChatList/Plugins/Render/utils/pluginState.ts +20 -0
  23. package/src/app/chat/features/Conversation/ChatList/Plugins/Render/utils/postMessage.ts +17 -0
  24. package/src/const/plugin.ts +1 -1
  25. package/src/features/PluginSettings/index.tsx +5 -3
  26. package/src/features/PluginSettingsModal/index.tsx +45 -0
  27. package/src/locales/default/plugin.ts +3 -2
  28. package/src/store/plugin/selectors.ts +17 -4
  29. package/src/store/session/slices/chat/actions/index.ts +4 -1
  30. package/src/store/session/slices/chat/actions/message.ts +102 -117
  31. package/src/store/session/slices/chat/actions/plugin.ts +109 -0
  32. package/src/store/session/slices/chat/reducers/message.ts +31 -1
  33. package/src/store/session/slices/chat/selectors/chat.ts +17 -20
  34. package/src/store/session/slices/chat/selectors/index.ts +4 -2
  35. package/src/types/chatMessage.ts +1 -0
  36. package/src/utils/message.ts +8 -1
  37. package/src/app/chat/features/Conversation/ChatList/Plugins/FunctionCall.tsx +0 -90
  38. package/src/app/chat/features/Conversation/ChatList/Plugins/IFrameRender/hooks.ts +0 -29
  39. package/src/app/chat/features/Conversation/ChatList/Plugins/IFrameRender/utils.ts +0 -17
  40. package/src/app/chat/features/Conversation/ChatList/Plugins/PluginMessage.tsx +0 -68
  41. /package/src/app/chat/features/Conversation/ChatList/Plugins/{PluginResultRender.tsx → Inspector/PluginResultJSON.tsx} +0 -0
  42. /package/src/app/chat/features/Conversation/ChatList/Plugins/{SystemJsRender → Render/DefaultType/SystemJsRender}/index.tsx +0 -0
  43. /package/src/app/chat/features/Conversation/ChatList/Plugins/{SystemJsRender → Render/DefaultType/SystemJsRender}/utils.ts +0 -0
@@ -0,0 +1,136 @@
1
+ import { PluginRequestPayload } from '@lobehub/chat-plugin-sdk';
2
+ import { Skeleton } from 'antd';
3
+ import { memo, useRef, useState } from 'react';
4
+
5
+ import { useOnPluginSettingsUpdate } from '@/app/chat/features/Conversation/ChatList/Plugins/Render/utils/pluginSettings';
6
+ import { useOnPluginStateUpdate } from '@/app/chat/features/Conversation/ChatList/Plugins/Render/utils/pluginState';
7
+ import { pluginSelectors, usePluginStore } from '@/store/plugin';
8
+ import { useSessionStore } from '@/store/session';
9
+ import { chatSelectors } from '@/store/session/selectors';
10
+
11
+ import { useOnPluginReadyForInteraction } from '../utils/iframeOnReady';
12
+ import {
13
+ useOnPluginFetchMessage,
14
+ useOnPluginFetchPluginSettings,
15
+ useOnPluginFetchPluginState,
16
+ useOnPluginFillContent,
17
+ } from '../utils/listenToPlugin';
18
+ import {
19
+ sendMessageToPlugin,
20
+ sendPayloadToPlugin,
21
+ sendPluginSettingsToPlugin,
22
+ sendPluginStateToPlugin,
23
+ } from '../utils/postMessage';
24
+
25
+ interface IFrameRenderProps {
26
+ height?: number;
27
+ id: string;
28
+ payload?: PluginRequestPayload;
29
+ url: string;
30
+ width?: number;
31
+ }
32
+
33
+ const IFrameRender = memo<IFrameRenderProps>(({ url, id, payload, width = 600, height = 300 }) => {
34
+ const [loading, setLoading] = useState(true);
35
+
36
+ const iframeRef = useRef<HTMLIFrameElement>(null);
37
+
38
+ // when payload change,send content to plugin
39
+ useOnPluginReadyForInteraction(() => {
40
+ const iframeWin = iframeRef.current?.contentWindow;
41
+ if (iframeWin && payload) {
42
+ sendPayloadToPlugin(iframeWin, payload);
43
+ }
44
+ }, [payload]);
45
+
46
+ // when plugin wants to get message content, send it to plugin
47
+ useOnPluginFetchMessage(() => {
48
+ const iframeWin = iframeRef.current?.contentWindow;
49
+
50
+ if (iframeWin) {
51
+ const message = chatSelectors.getMessageById(id)(useSessionStore.getState());
52
+ if (!message) return;
53
+ const props = { content: '' };
54
+
55
+ try {
56
+ props.content = JSON.parse(message.content || '{}');
57
+ } catch {
58
+ props.content = message.content || '';
59
+ }
60
+
61
+ sendMessageToPlugin(iframeWin, props);
62
+ }
63
+ }, []);
64
+
65
+ // when plugin try to send back message, we should fill it to the message content
66
+ const fillPluginContent = useSessionStore((s) => s.fillPluginMessageContent);
67
+ useOnPluginFillContent((content) => {
68
+ fillPluginContent(id, content);
69
+ });
70
+
71
+ // when plugin wants to get plugin state, send it to plugin
72
+ useOnPluginFetchPluginState((key) => {
73
+ const iframeWin = iframeRef.current?.contentWindow;
74
+
75
+ if (iframeWin) {
76
+ const message = chatSelectors.getMessageById(id)(useSessionStore.getState());
77
+ if (!message) return;
78
+
79
+ sendPluginStateToPlugin(iframeWin, key, message.pluginState?.[key]);
80
+ }
81
+ });
82
+
83
+ // when plugin update state, we should update it to the message pluginState key
84
+ const updatePluginState = useSessionStore((s) => s.updatePluginState);
85
+ useOnPluginStateUpdate((key, value) => {
86
+ updatePluginState(id, key, value);
87
+ });
88
+
89
+ // when plugin wants to get plugin settings, send it to plugin
90
+ useOnPluginFetchPluginSettings(() => {
91
+ const iframeWin = iframeRef.current?.contentWindow;
92
+
93
+ if (iframeWin) {
94
+ if (!payload?.identifier) return;
95
+
96
+ const settings = pluginSelectors.getPluginSettingsById(payload?.identifier)(
97
+ usePluginStore.getState(),
98
+ );
99
+
100
+ sendPluginSettingsToPlugin(iframeWin, settings);
101
+ }
102
+ });
103
+
104
+ // when plugin update settings, we should update it to the plugin settings
105
+ const updatePluginSettings = usePluginStore((s) => s.updatePluginSettings);
106
+ useOnPluginSettingsUpdate((value) => {
107
+ if (!payload?.identifier) return;
108
+
109
+ updatePluginSettings(payload?.identifier, value);
110
+ });
111
+
112
+ return (
113
+ <>
114
+ {loading && <Skeleton active style={{ width }} />}
115
+ <iframe
116
+ // @ts-ignore
117
+ allowtransparency="true"
118
+ height={height}
119
+ hidden={loading}
120
+ onLoad={() => {
121
+ setLoading(false);
122
+ }}
123
+ ref={iframeRef}
124
+ src={url}
125
+ style={{
126
+ border: 0,
127
+ // iframe 在 color-scheme:dark 模式下无法透明
128
+ // refs: https://www.jianshu.com/p/bc5a37bb6a7b
129
+ colorScheme: 'light',
130
+ }}
131
+ width={width}
132
+ />
133
+ </>
134
+ );
135
+ });
136
+ export default IFrameRender;
@@ -0,0 +1,28 @@
1
+ import { PluginRequestPayload } from '@lobehub/chat-plugin-sdk';
2
+ import { memo } from 'react';
3
+
4
+ import { usePluginStore } from '@/store/plugin';
5
+
6
+ import IFrameRender from './Iframe';
7
+
8
+ export interface PluginStandaloneTypeProps {
9
+ id: string;
10
+ name?: string;
11
+ payload?: PluginRequestPayload;
12
+ }
13
+
14
+ const PluginDefaultType = memo<PluginStandaloneTypeProps>(({ payload, id, name = 'unknown' }) => {
15
+ const manifest = usePluginStore((s) => s.pluginManifestMap[name]);
16
+
17
+ if (!manifest?.ui) return;
18
+
19
+ const ui = manifest.ui;
20
+
21
+ if (!ui.url) return;
22
+
23
+ return (
24
+ <IFrameRender height={ui.height} id={id} payload={payload} url={ui.url} width={ui.width} />
25
+ );
26
+ });
27
+
28
+ export default PluginDefaultType;
@@ -0,0 +1,28 @@
1
+ import { LobePluginType, PluginRequestPayload } from '@lobehub/chat-plugin-sdk';
2
+ import { memo } from 'react';
3
+
4
+ import DefaultType from './DefaultType';
5
+ import Standalone from './StandaloneType';
6
+
7
+ export interface PluginRenderProps {
8
+ content: string;
9
+ id: string;
10
+ loading?: boolean;
11
+ name?: string;
12
+ payload?: PluginRequestPayload;
13
+ type?: LobePluginType;
14
+ }
15
+
16
+ const PluginRender = memo<PluginRenderProps>(({ content, id, payload, name, type }) => {
17
+ switch (type) {
18
+ case 'standalone': {
19
+ return <Standalone id={id} name={name} payload={payload} />;
20
+ }
21
+
22
+ default: {
23
+ return <DefaultType content={content} name={name} />;
24
+ }
25
+ }
26
+ });
27
+
28
+ export default PluginRender;
@@ -0,0 +1,23 @@
1
+ import { PluginChannel } from '@lobehub/chat-plugin-sdk/client';
2
+ import { useEffect, useState } from 'react';
3
+
4
+ export const useOnPluginReadyForInteraction = (onReady: () => void, deps: any[] = []) => {
5
+ const [readyForRender, setReady] = useState(false);
6
+ useEffect(() => {
7
+ const fn = (e: MessageEvent) => {
8
+ if (e.data.type === PluginChannel.pluginReadyForRender) {
9
+ setReady(true);
10
+ }
11
+ };
12
+
13
+ window.addEventListener('message', fn);
14
+ return () => {
15
+ window.removeEventListener('message', fn);
16
+ };
17
+ }, []);
18
+ useEffect(() => {
19
+ if (readyForRender) {
20
+ onReady();
21
+ }
22
+ }, [readyForRender, ...deps]);
23
+ };
@@ -0,0 +1,65 @@
1
+ import { PluginChannel } from '@lobehub/chat-plugin-sdk/client';
2
+ import { useEffect } from 'react';
3
+
4
+ export const useOnPluginFetchMessage = (onRequest: (data: any) => void, deps: any[] = []) => {
5
+ useEffect(() => {
6
+ const fn = (e: MessageEvent) => {
7
+ if (e.data.type === PluginChannel.fetchPluginMessage) {
8
+ onRequest(e.data);
9
+ }
10
+ };
11
+
12
+ window.addEventListener('message', fn);
13
+ return () => {
14
+ window.removeEventListener('message', fn);
15
+ };
16
+ }, deps);
17
+ };
18
+
19
+ export const useOnPluginFetchPluginState = (onRequest: (key: string) => void) => {
20
+ useEffect(() => {
21
+ const fn = (e: MessageEvent) => {
22
+ if (e.data.type === PluginChannel.fetchPluginState) {
23
+ onRequest(e.data.key);
24
+ }
25
+ };
26
+
27
+ window.addEventListener('message', fn);
28
+ return () => {
29
+ window.removeEventListener('message', fn);
30
+ };
31
+ }, []);
32
+ };
33
+
34
+ export const useOnPluginFillContent = (callback: (content: string) => void) => {
35
+ useEffect(() => {
36
+ const fn = (e: MessageEvent) => {
37
+ if (e.data.type === PluginChannel.fillStandalonePluginContent) {
38
+ const data = e.data.content;
39
+ const content = typeof data !== 'string' ? JSON.stringify(data) : data;
40
+
41
+ callback(content);
42
+ }
43
+ };
44
+
45
+ window.addEventListener('message', fn);
46
+ return () => {
47
+ window.removeEventListener('message', fn);
48
+ };
49
+ }, []);
50
+ };
51
+
52
+ export const useOnPluginFetchPluginSettings = (onRequest: () => void) => {
53
+ useEffect(() => {
54
+ const fn = (e: MessageEvent) => {
55
+ if (e.data.type === PluginChannel.fetchPluginSettings) {
56
+ onRequest();
57
+ }
58
+ };
59
+
60
+ window.addEventListener('message', fn);
61
+ return () => {
62
+ window.removeEventListener('message', fn);
63
+ };
64
+ }, []);
65
+ };
@@ -0,0 +1,17 @@
1
+ import { PluginChannel } from '@lobehub/chat-plugin-sdk/client';
2
+ import { useEffect } from 'react';
3
+
4
+ export const useOnPluginSettingsUpdate = (callback: (settings: any) => void) => {
5
+ useEffect(() => {
6
+ const fn = (e: MessageEvent) => {
7
+ if (e.data.type === PluginChannel.updatePluginSettings) {
8
+ callback(e.data.value);
9
+ }
10
+ };
11
+
12
+ window.addEventListener('message', fn);
13
+ return () => {
14
+ window.removeEventListener('message', fn);
15
+ };
16
+ }, []);
17
+ };
@@ -0,0 +1,20 @@
1
+ import { PluginChannel } from '@lobehub/chat-plugin-sdk/client';
2
+ import { useEffect } from 'react';
3
+
4
+ export const useOnPluginStateUpdate = (callback: (key: string, value: any) => void) => {
5
+ useEffect(() => {
6
+ const fn = (e: MessageEvent) => {
7
+ if (e.data.type === PluginChannel.updatePluginState) {
8
+ const key = e.data.key;
9
+ const value = e.data.value;
10
+
11
+ callback(key, value);
12
+ }
13
+ };
14
+
15
+ window.addEventListener('message', fn);
16
+ return () => {
17
+ window.removeEventListener('message', fn);
18
+ };
19
+ }, []);
20
+ };
@@ -0,0 +1,17 @@
1
+ import { PluginChannel } from '@lobehub/chat-plugin-sdk/client';
2
+
3
+ export const sendMessageToPlugin = (window: Window, props: any) => {
4
+ window.postMessage({ props, type: PluginChannel.renderPlugin }, '*');
5
+ };
6
+
7
+ export const sendPayloadToPlugin = (window: Window, props: any) => {
8
+ window.postMessage({ props, type: PluginChannel.initStandalonePlugin }, '*');
9
+ };
10
+
11
+ export const sendPluginStateToPlugin = (window: Window, key: string, value: any) => {
12
+ window.postMessage({ key, type: PluginChannel.renderPluginState, value }, '*');
13
+ };
14
+
15
+ export const sendPluginSettingsToPlugin = (window: Window, settings: any) => {
16
+ window.postMessage({ type: PluginChannel.renderPluginState, value: settings }, '*');
17
+ };
@@ -1 +1 @@
1
- export const PLUGIN_SCHEMA_SEPARATOR = '--__--';
1
+ export const PLUGIN_SCHEMA_SEPARATOR = '____';
@@ -9,6 +9,8 @@ import { usePluginStore } from '@/store/plugin';
9
9
  import PluginSettingRender from './PluginSettingRender';
10
10
 
11
11
  export const transformPluginSettings = (pluginSettings: PluginSchema) => {
12
+ if (!pluginSettings?.properties) return [];
13
+
12
14
  return Object.entries(pluginSettings.properties).map(([name, i]) => ({
13
15
  desc: i.description,
14
16
  format: i.format,
@@ -21,7 +23,7 @@ export const transformPluginSettings = (pluginSettings: PluginSchema) => {
21
23
 
22
24
  interface PluginSettingsConfigProps {
23
25
  id: string;
24
- settings: PluginSchema;
26
+ schema: PluginSchema;
25
27
  }
26
28
 
27
29
  const useStyles = createStyles(({ css, token }) => ({
@@ -32,8 +34,8 @@ const useStyles = createStyles(({ css, token }) => ({
32
34
  `,
33
35
  }));
34
36
 
35
- const PluginSettingsConfig = memo<PluginSettingsConfigProps>(({ settings, id }) => {
36
- const items = transformPluginSettings(settings);
37
+ const PluginSettingsConfig = memo<PluginSettingsConfigProps>(({ schema, id }) => {
38
+ const items = transformPluginSettings(schema);
37
39
 
38
40
  const { styles } = useStyles();
39
41
  const [updatePluginSettings] = usePluginStore((s) => [s.updatePluginSettings]);
@@ -0,0 +1,45 @@
1
+ import { Avatar, Modal } from '@lobehub/ui';
2
+ import { Divider, Typography } from 'antd';
3
+ import { useTheme } from 'antd-style';
4
+ import isEqual from 'fast-deep-equal';
5
+ import { memo } from 'react';
6
+ import { useTranslation } from 'react-i18next';
7
+ import { Center, Flexbox } from 'react-layout-kit';
8
+
9
+ import PluginSettingsConfig from '@/features/PluginSettings';
10
+ import { pluginHelpers, pluginSelectors, usePluginStore } from '@/store/plugin';
11
+
12
+ interface PluginSettingsModalProps {
13
+ id: string;
14
+ onClose: () => void;
15
+ open?: boolean;
16
+ schema: any;
17
+ }
18
+
19
+ const PluginSettingsModal = memo<PluginSettingsModalProps>(({ schema, onClose, id, open }) => {
20
+ const pluginMeta = usePluginStore(pluginSelectors.getPluginMetaById(id), isEqual);
21
+
22
+ const { t } = useTranslation('plugin');
23
+ const theme = useTheme();
24
+ return (
25
+ <Modal onCancel={onClose} open={open} title={t('setting')} width={600}>
26
+ <Center gap={16}>
27
+ <Avatar
28
+ avatar={pluginHelpers.getPluginAvatar(pluginMeta?.meta) || '⚙️'}
29
+ background={theme.colorFillContent}
30
+ gap={12}
31
+ size={64}
32
+ />
33
+
34
+ <Flexbox style={{ fontSize: 20 }}>{pluginHelpers.getPluginTitle(pluginMeta?.meta)}</Flexbox>
35
+ <Typography.Text type={'secondary'}>
36
+ {pluginHelpers.getPluginDesc(pluginMeta?.meta)}
37
+ </Typography.Text>
38
+ <Divider style={{ marginBottom: 0, marginTop: 8 }} />
39
+ {schema && <PluginSettingsConfig id={id} schema={schema} />}
40
+ </Center>
41
+ </Modal>
42
+ );
43
+ });
44
+
45
+ export default PluginSettingsModal;
@@ -2,6 +2,8 @@ export default {
2
2
  debug: {
3
3
  arguments: '调用参数',
4
4
  function_call: '函数调用',
5
+ off: '关闭调试',
6
+ on: '查看插件调用信息',
5
7
  response: '返回结果',
6
8
  },
7
9
  dev: {
@@ -88,10 +90,9 @@ export default {
88
90
  },
89
91
  },
90
92
  loading: {
91
- content: '数据获取中...',
93
+ content: '调用插件中...',
92
94
  plugin: '插件运行中...',
93
95
  },
94
-
95
96
  pluginList: '插件列表',
96
97
  plugins: {
97
98
  unknown: '插件检测中...',
@@ -18,11 +18,17 @@ const enabledSchema =
18
18
  return enabledPlugins.includes(p.identifier);
19
19
  })
20
20
  .flatMap((manifest) =>
21
- manifest.api.map((m) => ({
22
- ...m,
21
+ manifest.api.map((m) => {
22
+ const pluginType = manifest.type ? `${PLUGIN_SCHEMA_SEPARATOR + manifest.type}` : '';
23
+
23
24
  // 将插件的 identifier 作为前缀,避免重复
24
- name: manifest.identifier + PLUGIN_SCHEMA_SEPARATOR + m.name,
25
- })),
25
+ const apiName = manifest.identifier + PLUGIN_SCHEMA_SEPARATOR + m.name + pluginType;
26
+
27
+ return {
28
+ ...m,
29
+ name: apiName,
30
+ };
31
+ }),
26
32
  );
27
33
 
28
34
  return uniqBy(list, 'name');
@@ -64,6 +70,12 @@ const displayPluginList = (s: PluginStoreState) =>
64
70
  title: pluginHelpers.getPluginTitle(p.meta),
65
71
  }));
66
72
 
73
+ const hasPluginUI = (id: string) => (s: PluginStoreState) => {
74
+ const manifest = getPluginManifestById(id)(s);
75
+
76
+ return !!manifest?.ui;
77
+ };
78
+
67
79
  export const pluginSelectors = {
68
80
  displayPluginList,
69
81
  enabledSchema,
@@ -72,6 +84,7 @@ export const pluginSelectors = {
72
84
  getPluginManifestLoadingStatus,
73
85
  getPluginMetaById,
74
86
  getPluginSettingsById,
87
+ hasPluginUI,
75
88
  isCustomPlugin,
76
89
  pluginList,
77
90
  };
@@ -3,6 +3,7 @@ import { StateCreator } from 'zustand/vanilla';
3
3
  import { SessionStore } from '@/store/session';
4
4
 
5
5
  import { ChatMessageAction, chatMessage } from './message';
6
+ import { ChatPluginAction, chatPlugin } from './plugin';
6
7
  import { ShareAction, chatShare } from './share';
7
8
  import { ChatTopicAction, chatTopic } from './topic';
8
9
  import { ChatTranslateAction, chatTranslate } from './translate';
@@ -14,7 +15,8 @@ export interface ChatAction
14
15
  extends ChatTopicAction,
15
16
  ChatMessageAction,
16
17
  ShareAction,
17
- ChatTranslateAction {}
18
+ ChatTranslateAction,
19
+ ChatPluginAction {}
18
20
 
19
21
  export const createChatSlice: StateCreator<
20
22
  SessionStore,
@@ -26,4 +28,5 @@ export const createChatSlice: StateCreator<
26
28
  ...chatMessage(...params),
27
29
  ...chatShare(...params),
28
30
  ...chatTranslate(...params),
31
+ ...chatPlugin(...params),
29
32
  });