@lobehub/chat 0.71.0 → 0.72.0

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,74 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## [Version 0.72.0](https://github.com/lobehub/lobe-chat/compare/v0.71.1...v0.72.0)
6
+
7
+ <sup>Released on **2023-09-09**</sup>
8
+
9
+ #### ✨ Features
10
+
11
+ - **misc**: Add plugin market Setting Modal, 支持快速刷新与预览 manifest, 适配插件 i18n 方案.
12
+
13
+ #### 🐛 Bug Fixes
14
+
15
+ - **misc**: 修正删除插件时错误开启的问题.
16
+
17
+ #### 💄 Styles
18
+
19
+ - **misc**: 优化 manifest 预览的尺寸.
20
+
21
+ <br/>
22
+
23
+ <details>
24
+ <summary><kbd>Improvements and Fixes</kbd></summary>
25
+
26
+ #### What's improved
27
+
28
+ - **misc**: Add plugin market Setting Modal ([a0603a9](https://github.com/lobehub/lobe-chat/commit/a0603a9))
29
+ - **misc**: 支持快速刷新与预览 manifest, closes [#150](https://github.com/lobehub/lobe-chat/issues/150) ([5bd2eb0](https://github.com/lobehub/lobe-chat/commit/5bd2eb0))
30
+ - **misc**: 适配插件 i18n 方案 ([8709ab3](https://github.com/lobehub/lobe-chat/commit/8709ab3))
31
+
32
+ #### What's fixed
33
+
34
+ - **misc**: 修正删除插件时错误开启的问题 ([0e35c18](https://github.com/lobehub/lobe-chat/commit/0e35c18))
35
+
36
+ #### Styles
37
+
38
+ - **misc**: 优化 manifest 预览的尺寸 ([27f8d6d](https://github.com/lobehub/lobe-chat/commit/27f8d6d))
39
+
40
+ </details>
41
+
42
+ <div align="right">
43
+
44
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
45
+
46
+ </div>
47
+
48
+ ### [Version 0.71.1](https://github.com/lobehub/lobe-chat/compare/v0.71.0...v0.71.1)
49
+
50
+ <sup>Released on **2023-09-09**</sup>
51
+
52
+ #### 🐛 Bug Fixes
53
+
54
+ - **misc**: Fix mobile route.
55
+
56
+ <br/>
57
+
58
+ <details>
59
+ <summary><kbd>Improvements and Fixes</kbd></summary>
60
+
61
+ #### What's fixed
62
+
63
+ - **misc**: Fix mobile route, closes [#165](https://github.com/lobehub/lobe-chat/issues/165) ([d5e03b6](https://github.com/lobehub/lobe-chat/commit/d5e03b6))
64
+
65
+ </details>
66
+
67
+ <div align="right">
68
+
69
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
70
+
71
+ </div>
72
+
5
73
  ## [Version 0.71.0](https://github.com/lobehub/lobe-chat/compare/v0.70.4...v0.71.0)
6
74
 
7
75
  <sup>Released on **2023-09-09**</sup>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "0.71.0",
3
+ "version": "0.72.0",
4
4
  "description": "Lobe Chat is an open-source chatbot client using Vercel AI SDK, Typescript and Next.js",
5
5
  "keywords": [
6
6
  "chatbot",
@@ -0,0 +1,30 @@
1
+ import { Highlighter } from '@lobehub/ui';
2
+ import { Popover } from 'antd';
3
+ import { ReactNode, memo } from 'react';
4
+
5
+ interface PluginManifestPreviewerProps {
6
+ children?: ReactNode;
7
+ manifest: object;
8
+ trigger?: 'click' | 'hover';
9
+ }
10
+
11
+ const ManifestPreviewer = memo<PluginManifestPreviewerProps>(
12
+ ({ manifest, children, trigger = 'click' }) => (
13
+ <Popover
14
+ arrow={false}
15
+ content={
16
+ <Highlighter language={'json'} style={{ maxHeight: 600, maxWidth: 400 }}>
17
+ {JSON.stringify(manifest, null, 2)}
18
+ </Highlighter>
19
+ }
20
+ placement={'right'}
21
+ style={{ width: 400 }}
22
+ title={'Manifest JSON'}
23
+ trigger={trigger}
24
+ >
25
+ {children}
26
+ </Popover>
27
+ ),
28
+ );
29
+
30
+ export default ManifestPreviewer;
package/src/const/url.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { Locales } from '@/locales/resources';
2
+
1
3
  import pkg from '../../package.json';
2
4
  import { INBOX_SESSION_ID } from './session';
3
5
 
@@ -8,7 +10,13 @@ export const FEEDBACK = pkg.bugs.url;
8
10
  export const DISCORD = 'https://discord.gg/AYFPHvv2jT';
9
11
 
10
12
  export const PLUGINS_INDEX_URL =
11
- process.env.PLUGINS_INDEX_URL ?? 'https://chat-plugins.lobehub.com';
13
+ process.env.PLUGINS_INDEX_URL ?? 'https://chat-plugins.lobehub.com/index';
14
+
15
+ export const getPluginIndexJSON = (lang: Locales = 'en-US', baseUrl = PLUGINS_INDEX_URL) => {
16
+ if (lang === 'en-US') return baseUrl;
17
+
18
+ return `${baseUrl}.${lang}.json`;
19
+ };
12
20
 
13
21
  export const AGENTS_INDEX_URL = process.env.AGENTS_INDEX_URL ?? 'https://chat-agents.lobehub.com';
14
22
 
@@ -3,8 +3,8 @@ import isEqual from 'fast-deep-equal';
3
3
  import { memo, useState } from 'react';
4
4
  import { useTranslation } from 'react-i18next';
5
5
  import { Flexbox } from 'react-layout-kit';
6
- import DevModal from 'src/features/PluginDevModal';
7
6
 
7
+ import DevModal from '@/features/PluginDevModal';
8
8
  import { pluginSelectors, usePluginStore } from '@/store/plugin';
9
9
 
10
10
  import { useStore } from '../store';
@@ -36,7 +36,7 @@ const MarketList = memo<{ id: string }>(({ id }) => {
36
36
  mode={'edit'}
37
37
  onDelete={() => {
38
38
  deleteCustomPlugin(id);
39
- toggleAgentPlugin(id);
39
+ toggleAgentPlugin(id, false);
40
40
  }}
41
41
  onOpenChange={setModal}
42
42
  onSave={(value) => {
@@ -1,8 +1,8 @@
1
1
  import { Avatar, Form, Icon, Tooltip } from '@lobehub/ui';
2
- import { Button, Skeleton, Switch, Tag } from 'antd';
2
+ import { Button, Skeleton, Space, Switch, Tag } from 'antd';
3
3
  import { createStyles } from 'antd-style';
4
4
  import isEqual from 'fast-deep-equal';
5
- import { LucideBlocks, LucideStore, LucideTrash2 } from 'lucide-react';
5
+ import { LucideBlocks, LucideSettings, LucideStore, LucideTrash2 } from 'lucide-react';
6
6
  import { memo, useState } from 'react';
7
7
  import { useTranslation } from 'react-i18next';
8
8
  import { Flexbox } from 'react-layout-kit';
@@ -13,6 +13,7 @@ import { pluginHelpers, pluginSelectors, usePluginStore } from '@/store/plugin';
13
13
 
14
14
  import { useStore } from '../store';
15
15
  import LocalPluginItem from './LocalPluginItem';
16
+ import MarketSettingModal from './MarketSettingModal';
16
17
 
17
18
  const useStyles = createStyles(({ css }) => ({
18
19
  avatar: css`
@@ -36,6 +37,7 @@ const MarketList = memo(() => {
36
37
  const { styles } = useStyles();
37
38
 
38
39
  const [showModal, setModal] = useState(false);
40
+ const [showSettings, setShowSettings] = useState(false);
39
41
 
40
42
  const [userEnabledPlugins, hasPlugin, toggleAgentPlugin] = useStore((s) => [
41
43
  s.config.plugins || [],
@@ -173,12 +175,13 @@ const MarketList = memo(() => {
173
175
  onValueChange={updateNewDevPlugin}
174
176
  open={showModal}
175
177
  />
178
+ <MarketSettingModal onOpenChange={setShowSettings} open={showSettings} />
176
179
  <Form
177
180
  items={[
178
181
  {
179
182
  children: isEmpty ? loadingList : [...deprecatedList, ...customList, ...list],
180
183
  extra: (
181
- <Flexbox gap={8} horizontal>
184
+ <Flexbox align={'center'} gap={8} horizontal>
182
185
  {hasDeprecated ? (
183
186
  <Tooltip title={t('settingPlugin.clearDeprecated')}>
184
187
  <Button
@@ -191,21 +194,32 @@ const MarketList = memo(() => {
191
194
  }}
192
195
  size={'small'}
193
196
  type={'text'}
194
- ></Button>
197
+ />
195
198
  </Tooltip>
196
199
  ) : null}
197
- <Tooltip title={t('settingPlugin.addTooltip')}>
198
- <Button
199
- icon={<Icon icon={LucideBlocks} />}
200
- onClick={(e) => {
201
- e.stopPropagation();
202
- setModal(true);
203
- }}
204
- size={'small'}
205
- >
206
- {t('settingPlugin.add')}
207
- </Button>
208
- </Tooltip>
200
+
201
+ <Space.Compact style={{ width: 'auto' }}>
202
+ <Tooltip title={t('settingPlugin.addTooltip')}>
203
+ <Button
204
+ icon={<Icon icon={LucideBlocks} />}
205
+ onClick={(e) => {
206
+ e.stopPropagation();
207
+ setModal(true);
208
+ }}
209
+ size={'small'}
210
+ />
211
+ </Tooltip>
212
+ <Tooltip title={t('settingPlugin.settings')}>
213
+ <Button
214
+ icon={<Icon icon={LucideSettings} />}
215
+ onClick={(e) => {
216
+ e.stopPropagation();
217
+ setShowSettings(true);
218
+ }}
219
+ size={'small'}
220
+ />
221
+ </Tooltip>
222
+ </Space.Compact>
209
223
  </Flexbox>
210
224
  ),
211
225
  icon: LucideStore,
@@ -0,0 +1,76 @@
1
+ import { Icon, Input, Tooltip } from '@lobehub/ui';
2
+ import { ConfigProvider, Form, Modal } from 'antd';
3
+ import { createStyles } from 'antd-style';
4
+ import { LucideSettings } from 'lucide-react';
5
+ import { lighten } from 'polished';
6
+ import { memo } from 'react';
7
+ import { useTranslation } from 'react-i18next';
8
+ import { Flexbox } from 'react-layout-kit';
9
+
10
+ import { PLUGINS_INDEX_URL } from '@/const/url';
11
+
12
+ const useStyles = createStyles(({ css, token, prefixCls }) => ({
13
+ content: css`
14
+ .${prefixCls}-modal-content {
15
+ border: 1px solid ${token.colorSplit};
16
+ border-radius: 12px;
17
+ }
18
+ `,
19
+ root: css`
20
+ backdrop-filter: blur(2px);
21
+ `,
22
+ }));
23
+
24
+ interface MarketSettingModalProps {
25
+ onOpenChange: (open: boolean) => void;
26
+ open?: boolean;
27
+ }
28
+
29
+ const MarketSettingModal = memo<MarketSettingModalProps>(({ open, onOpenChange }) => {
30
+ const { t } = useTranslation('plugin');
31
+ const { styles, theme } = useStyles();
32
+
33
+ return (
34
+ <ConfigProvider
35
+ theme={{
36
+ token: {
37
+ colorBgElevated: lighten(0.005, theme.colorBgContainer),
38
+ },
39
+ }}
40
+ >
41
+ <Modal
42
+ centered
43
+ closable
44
+ maskClosable
45
+ onCancel={() => {
46
+ onOpenChange(false);
47
+ }}
48
+ open={open}
49
+ title={
50
+ <Flexbox gap={8} horizontal style={{ marginBottom: 24 }}>
51
+ <Icon icon={LucideSettings} />
52
+ {t('settings.title')}
53
+ </Flexbox>
54
+ }
55
+ width={700}
56
+ wrapClassName={styles.root}
57
+ >
58
+ <Flexbox gap={12}>
59
+ <Form layout={'vertical'}>
60
+ <Form.Item extra={t('settings.modalDesc')} label={t('settings.indexUrl.title')}>
61
+ <Tooltip title={t('settings.indexUrl.tooltip')}>
62
+ <Input
63
+ defaultValue={PLUGINS_INDEX_URL}
64
+ disabled
65
+ placeholder={'https://xxxxx.com/index.json'}
66
+ />
67
+ </Tooltip>
68
+ </Form.Item>
69
+ </Form>
70
+ </Flexbox>
71
+ </Modal>
72
+ </ConfigProvider>
73
+ );
74
+ });
75
+
76
+ export default MarketSettingModal;
@@ -48,7 +48,7 @@ export interface Action {
48
48
 
49
49
  setAgentMeta: (meta: Partial<MetaData>) => void;
50
50
  streamUpdateMeta: (key: keyof MetaData) => any;
51
- toggleAgentPlugin: (pluginId: string) => void;
51
+ toggleAgentPlugin: (pluginId: string, state?: boolean) => void;
52
52
  /**
53
53
  * 更新加载状态
54
54
  * @param key - SessionLoadingState 的键
@@ -198,8 +198,8 @@ export const store: StateCreator<Store, [['zustand/devtools', never]]> = (set, g
198
198
  };
199
199
  },
200
200
 
201
- toggleAgentPlugin: (id) => {
202
- get().dispatchConfig({ pluginId: id, type: 'togglePlugin' });
201
+ toggleAgentPlugin: (id, state) => {
202
+ get().dispatchConfig({ pluginId: id, state, type: 'togglePlugin' });
203
203
  },
204
204
 
205
205
  updateLoadingState: (key, value) => {
@@ -6,7 +6,7 @@ import { merge } from '@/utils/merge';
6
6
 
7
7
  export type ConfigDispatch =
8
8
  | { config: Partial<any>; type: 'update' }
9
- | { pluginId: string; type: 'togglePlugin' }
9
+ | { pluginId: string; state?: boolean; type: 'togglePlugin' }
10
10
  | { type: 'reset' };
11
11
 
12
12
  export const configReducer = (state: LobeAgentConfig, payload: ConfigDispatch): LobeAgentConfig => {
@@ -19,15 +19,26 @@ export const configReducer = (state: LobeAgentConfig, payload: ConfigDispatch):
19
19
 
20
20
  case 'togglePlugin': {
21
21
  return produce(state, (config) => {
22
- const { pluginId: id } = payload;
22
+ const { pluginId: id, state } = payload;
23
23
  if (config.plugins === undefined) {
24
- config.plugins = [id];
25
- } else {
24
+ config.plugins = [];
25
+ }
26
+
27
+ if (typeof state === 'undefined') {
26
28
  if (config.plugins.includes(id)) {
27
29
  config.plugins.splice(config.plugins.indexOf(id), 1);
28
- } else {
29
- config.plugins.push(id);
30
+
31
+ return;
30
32
  }
33
+
34
+ config.plugins.push(id);
35
+ return;
36
+ }
37
+
38
+ if (!state) {
39
+ config.plugins = config.plugins.filter((pluginId) => pluginId !== id);
40
+ } else {
41
+ config.plugins.push(id);
31
42
  }
32
43
  });
33
44
  }
@@ -1,11 +1,13 @@
1
1
  import { LobeChatPluginManifest, pluginManifestSchema } from '@lobehub/chat-plugin-sdk';
2
- import { ActionIcon, Form, FormItemProps, Highlighter, Input, Tooltip } from '@lobehub/ui';
3
- import { FormInstance, Popover, Radio } from 'antd';
2
+ import { ActionIcon, Form, FormItemProps, Input, Tooltip } from '@lobehub/ui';
3
+ import { FormInstance, Radio } from 'antd';
4
4
  import { FileCode, RotateCwIcon } from 'lucide-react';
5
5
  import { memo, useState } from 'react';
6
6
  import { useTranslation } from 'react-i18next';
7
7
  import { Flexbox } from 'react-layout-kit';
8
8
 
9
+ import ManifestPreviewer from '@/components/ManifestPreviewer';
10
+
9
11
  const ManifestForm = memo<{ form: FormInstance; mode?: 'url' | 'local' }>(
10
12
  ({ form, mode = 'url' }) => {
11
13
  const { t } = useTranslation('plugin');
@@ -21,17 +23,15 @@ const ManifestForm = memo<{ form: FormInstance; mode?: 'url' | 'local' }>(
21
23
  <Input
22
24
  placeholder={'http://localhost:3400/manifest-dev.json'}
23
25
  suffix={
24
- manifest && (
25
- <ActionIcon
26
- icon={RotateCwIcon}
27
- onClick={(e) => {
28
- e.stopPropagation();
29
- form.validateFields(['manifest']);
30
- }}
31
- size={'small'}
32
- title={t('dev.meta.manifest.refresh')}
33
- />
34
- )
26
+ <ActionIcon
27
+ icon={RotateCwIcon}
28
+ onClick={(e) => {
29
+ e.stopPropagation();
30
+ form.validateFields(['manifest']);
31
+ }}
32
+ size={'small'}
33
+ title={t('dev.meta.manifest.refresh')}
34
+ />
35
35
  }
36
36
  />
37
37
  ),
@@ -39,24 +39,13 @@ const ManifestForm = memo<{ form: FormInstance; mode?: 'url' | 'local' }>(
39
39
  <Flexbox horizontal justify={'space-between'} style={{ marginTop: 8 }}>
40
40
  {t('dev.meta.manifest.desc')}
41
41
  {manifest && (
42
- <Popover
43
- arrow={false}
44
- content={
45
- <Highlighter language={'json'}>
46
- {JSON.stringify(manifest, null, 2)}
47
- </Highlighter>
48
- }
49
- placement={'right'}
50
- style={{ width: 400 }}
51
- title={'Manifest JSON'}
52
- trigger={'click'}
53
- >
42
+ <ManifestPreviewer manifest={manifest}>
54
43
  <ActionIcon
55
44
  icon={FileCode}
56
45
  size={'small'}
57
46
  title={t('dev.meta.manifest.preview')}
58
47
  />
59
- </Popover>
48
+ </ManifestPreviewer>
60
49
  )}
61
50
  </Flexbox>
62
51
  ),
@@ -6,7 +6,6 @@ import { Flexbox } from 'react-layout-kit';
6
6
 
7
7
  import SafeSpacing from '@/components/SafeSpacing';
8
8
  import MobileTabBar from '@/features/MobileTabBar';
9
- import { useOnFinishHydrationSession, useSessionStore } from '@/store/session';
10
9
 
11
10
  const useStyles = createStyles(({ css, cx, stylish }) => ({
12
11
  container: cx(
@@ -49,10 +48,6 @@ const AppMobileLayout = memo<AppMobileLayoutProps>(
49
48
  ({ children, showTabBar, navBar, style, className }) => {
50
49
  const { styles, cx } = useStyles();
51
50
 
52
- useOnFinishHydrationSession(() => {
53
- useSessionStore.setState({ isMobile: true });
54
- }, []);
55
-
56
51
  return (
57
52
  <>
58
53
  <Head>
@@ -38,7 +38,7 @@ export default {
38
38
  inbox: {
39
39
  defaultMessage:
40
40
  '你好,我是你的智能助手,你可以问我任何问题,我会尽力回答你。如果需要获得更加专业或定制的助手,可以点击<kbd>+</kbd>创建自定义助手',
41
- desc: '开启大脑集群,激发思维火花。你的智能助理,就在这里与你交流一切',
41
+ desc: '开启大脑集群,激发思维火花。你的智能助理,在这里与你交流一切',
42
42
  title: '随便聊聊',
43
43
  },
44
44
  message: {
@@ -87,13 +87,21 @@ export default {
87
87
  'local.title': '自定义',
88
88
  },
89
89
  },
90
-
91
90
  loading: {
92
91
  content: '数据获取中...',
93
92
  plugin: '插件运行中...',
94
93
  },
94
+
95
95
  pluginList: '插件列表',
96
96
  plugins: {
97
97
  unknown: '插件检测中...',
98
98
  },
99
+ settings: {
100
+ indexUrl: {
101
+ title: '市场索引',
102
+ tooltip: '暂不支持编辑',
103
+ },
104
+ modalDesc: '配置插件市场的地址后,可以使用自定义的插件市场',
105
+ title: '设置插件市场',
106
+ },
99
107
  };
@@ -132,6 +132,7 @@ export default {
132
132
  addTooltip: '添加自定义插件',
133
133
  clearDeprecated: '移除无效插件',
134
134
  config: '{{id}} 插件配置',
135
+ settings: '配置插件市场',
135
136
  title: '插件列表',
136
137
  },
137
138
  settingSystem: {
@@ -0,0 +1,105 @@
1
+ import { ActionIcon } from '@lobehub/ui';
2
+ import { Badge, Button, Tag } from 'antd';
3
+ import { LucideRotateCw, LucideTrash2, RotateCwIcon } from 'lucide-react';
4
+ import { memo, useMemo } from 'react';
5
+ import { useTranslation } from 'react-i18next';
6
+ import { Flexbox } from 'react-layout-kit';
7
+
8
+ import ManifestPreviewer from '@/components/ManifestPreviewer';
9
+ import { pluginSelectors, usePluginStore } from '@/store/plugin';
10
+ import { useSessionStore } from '@/store/session';
11
+
12
+ interface PluginStatusProps {
13
+ deprecated?: boolean;
14
+ id: string;
15
+ title?: string;
16
+ }
17
+ const PluginStatus = memo<PluginStatusProps>(({ title, id, deprecated }) => {
18
+ const { t } = useTranslation('common');
19
+ const [status, isCustom, fetchPluginManifest] = usePluginStore((s) => [
20
+ pluginSelectors.getPluginManifestLoadingStatus(id)(s),
21
+ pluginSelectors.isCustomPlugin(id)(s),
22
+ s.fetchPluginManifest,
23
+ ]);
24
+
25
+ const manifest = usePluginStore(pluginSelectors.getPluginManifestById(id));
26
+
27
+ const removePlugin = useSessionStore((s) => s.removePlugin);
28
+
29
+ const renderStatus = useMemo(() => {
30
+ switch (status) {
31
+ case 'loading': {
32
+ return <Badge color={'blue'} status={'processing'} />;
33
+ }
34
+ case 'error': {
35
+ return (
36
+ <ActionIcon
37
+ icon={LucideRotateCw}
38
+ onClick={() => {
39
+ fetchPluginManifest(id);
40
+ }}
41
+ size={'small'}
42
+ title={t('retry')}
43
+ />
44
+ );
45
+ }
46
+ case 'success': {
47
+ return <Badge status={status} />;
48
+ }
49
+ }
50
+ }, [status]);
51
+
52
+ const tag =
53
+ // 废弃标签
54
+ deprecated ? (
55
+ <Tag bordered={false} color={'red'} style={{ marginRight: 0 }}>
56
+ {t('list.item.deprecated.title', { ns: 'plugin' })}
57
+ </Tag>
58
+ ) : // 自定义标签
59
+ isCustom ? (
60
+ <Tag bordered={false} color={'gold'}>
61
+ {t('list.item.local.title', { ns: 'plugin' })}
62
+ </Tag>
63
+ ) : null;
64
+
65
+ return (
66
+ <Flexbox gap={12} horizontal justify={'space-between'}>
67
+ <Flexbox align={'center'} gap={8} horizontal>
68
+ {title || id}
69
+ {tag}
70
+ </Flexbox>
71
+
72
+ {deprecated ? (
73
+ <ActionIcon
74
+ icon={LucideTrash2}
75
+ onClick={(e) => {
76
+ e.stopPropagation();
77
+ removePlugin(id);
78
+ }}
79
+ size={'small'}
80
+ title={t('settingPlugin.clearDeprecated', { ns: 'setting' })}
81
+ />
82
+ ) : (
83
+ <Flexbox align={'center'} horizontal>
84
+ {isCustom ? (
85
+ <ActionIcon
86
+ icon={RotateCwIcon}
87
+ onClick={(e) => {
88
+ e.stopPropagation();
89
+ fetchPluginManifest(id);
90
+ // form.validateFields(['manifest']);
91
+ }}
92
+ size={'small'}
93
+ title={t('dev.meta.manifest.refresh', { ns: 'plugin' })}
94
+ />
95
+ ) : null}
96
+ <ManifestPreviewer manifest={manifest || {}} trigger={'hover'}>
97
+ <Button icon={renderStatus} size={'small'} type={'text'} />
98
+ </ManifestPreviewer>
99
+ </Flexbox>
100
+ )}
101
+ </Flexbox>
102
+ );
103
+ });
104
+
105
+ export default PluginStatus;
@@ -1,50 +1,13 @@
1
- import { ActionIcon, Avatar, Icon, Tag } from '@lobehub/ui';
1
+ import { Avatar, Icon, Tag } from '@lobehub/ui';
2
2
  import type { MenuProps } from 'antd';
3
- import { Badge, Dropdown } from 'antd';
3
+ import { Dropdown } from 'antd';
4
4
  import isEqual from 'fast-deep-equal';
5
- import { LucideRotateCw, LucideToyBrick } from 'lucide-react';
6
- import { memo, useMemo } from 'react';
7
- import { useTranslation } from 'react-i18next';
8
- import { Flexbox } from 'react-layout-kit';
5
+ import { LucideToyBrick } from 'lucide-react';
6
+ import { memo } from 'react';
9
7
 
10
8
  import { pluginHelpers, pluginSelectors, usePluginStore } from '@/store/plugin';
11
9
 
12
- const PluginStatus = memo<{ id: string; title?: string }>(({ title, id }) => {
13
- const { t } = useTranslation('common');
14
- const [status, fetchPluginManifest] = usePluginStore((s) => [
15
- pluginSelectors.getPluginManifestLoadingStatus(id)(s),
16
- s.fetchPluginManifest,
17
- ]);
18
-
19
- const renderStatus = useMemo(() => {
20
- switch (status) {
21
- case 'loading': {
22
- return <Badge color={'blue'} status={'processing'} />;
23
- }
24
- case 'error': {
25
- return (
26
- <ActionIcon
27
- icon={LucideRotateCw}
28
- onClick={() => {
29
- fetchPluginManifest(id);
30
- }}
31
- size={'small'}
32
- title={t('retry')}
33
- />
34
- );
35
- }
36
- case 'success': {
37
- return <Badge status={status} />;
38
- }
39
- }
40
- }, [status]);
41
-
42
- return (
43
- <Flexbox gap={12} horizontal justify={'space-between'}>
44
- {title} {renderStatus}
45
- </Flexbox>
46
- );
47
- });
10
+ import PluginStatus from './PluginStatus';
48
11
 
49
12
  export interface PluginTagProps {
50
13
  plugins: string[];
@@ -58,11 +21,13 @@ const PluginTag = memo<PluginTagProps>(({ plugins }) => {
58
21
 
59
22
  const items: MenuProps['items'] = plugins.map((id) => {
60
23
  const item = list.find((i) => i.identifier === id);
24
+ const isDeprecated = !item?.title;
25
+ const avatar = isDeprecated ? '♻️' : item?.avatar || '🧩';
61
26
 
62
27
  return {
63
- icon: <Avatar avatar={item?.avatar} size={24} style={{ marginLeft: -6, marginRight: 2 }} />,
28
+ icon: <Avatar avatar={avatar} size={24} style={{ marginLeft: -6, marginRight: 2 }} />,
64
29
  key: id,
65
- label: <PluginStatus id={id} title={item?.title} />,
30
+ label: <PluginStatus deprecated={isDeprecated} id={id} title={item?.title} />,
66
31
  };
67
32
  });
68
33
 
@@ -73,7 +38,7 @@ const PluginTag = memo<PluginTagProps>(({ plugins }) => {
73
38
  <div>
74
39
  <Tag>
75
40
  {<Icon icon={LucideToyBrick} />}
76
- {pluginHelpers.getPluginTitle(displayPlugin?.meta)}
41
+ {pluginHelpers.getPluginTitle(displayPlugin?.meta) || plugins[0]}
77
42
  {count > 1 && <div>({plugins.length - 1}+)</div>}
78
43
  </Tag>
79
44
  </div>
@@ -4,6 +4,7 @@ import { memo } from 'react';
4
4
  import { Flexbox } from 'react-layout-kit';
5
5
 
6
6
  import { useSessionStore } from '@/store/session';
7
+ import { useEffectAfterSessionHydrated } from '@/store/session/hooks';
7
8
  import { agentSelectors } from '@/store/session/selectors';
8
9
  import { genSiteHeadTitle } from '@/utils/genSiteHeadTitle';
9
10
 
@@ -22,6 +23,13 @@ const Chat = memo(() => {
22
23
  const pageTitle = genSiteHeadTitle([avatar, title].filter(Boolean).join(' '));
23
24
  const RenderLayout = mobile ? MobileLayout : DesktopLayout;
24
25
 
26
+ useEffectAfterSessionHydrated(
27
+ (store) => {
28
+ store.setState({ isMobile: mobile });
29
+ },
30
+ [mobile],
31
+ );
32
+
25
33
  return (
26
34
  <>
27
35
  <Head>
@@ -1,10 +1,13 @@
1
- import { PLUGINS_INDEX_URL } from '@/const/url';
1
+ import { getPluginIndexJSON } from '@/const/url';
2
+ import { useGlobalStore } from '@/store/global';
2
3
 
3
4
  /**
4
5
  * 请求插件列表
5
6
  */
6
7
  export const getPluginList = async () => {
7
- const res = await fetch(PLUGINS_INDEX_URL);
8
+ const url = getPluginIndexJSON(useGlobalStore.getState().settings.language);
9
+
10
+ const res = await fetch(url);
8
11
 
9
12
  return res.json();
10
13
  };
@@ -1,6 +1,8 @@
1
1
  import { LobeChatPluginMeta } from '@lobehub/chat-plugin-sdk';
2
2
  import i18n from 'i18next';
3
3
 
4
+ import { CustomPlugin } from '@/types/plugin';
5
+
4
6
  const getI18nValue = (value: string | Record<string, string> | undefined) => {
5
7
  if (!value) return;
6
8
 
@@ -18,9 +20,13 @@ const getPluginTitle = (meta?: LobeChatPluginMeta['meta']) => getI18nValue(meta?
18
20
  const getPluginDesc = (meta?: LobeChatPluginMeta['meta']) => getI18nValue(meta?.description);
19
21
  const getPluginAvatar = (meta?: LobeChatPluginMeta['meta']) => meta?.avatar;
20
22
 
23
+ const isCustomPlugin = (id: string, pluginList: CustomPlugin[]) =>
24
+ pluginList.some((i) => i.identifier === id);
25
+
21
26
  export const pluginHelpers = {
22
27
  getPluginAvatar,
23
28
  getPluginDesc,
24
29
  getPluginFormList,
25
30
  getPluginTitle,
31
+ isCustomPlugin,
26
32
  };
@@ -50,6 +50,9 @@ const getPluginManifestLoadingStatus = (id: string) => (s: PluginStoreState) =>
50
50
  if (!!manifest) return 'success';
51
51
  };
52
52
 
53
+ const isCustomPlugin = (id: string) => (s: PluginStoreState) =>
54
+ pluginHelpers.isCustomPlugin(id, s.customPluginList);
55
+
53
56
  const displayPluginList = (s: PluginStoreState) =>
54
57
  pluginList(s).map((p) => ({
55
58
  author: p.author,
@@ -69,5 +72,6 @@ export const pluginSelectors = {
69
72
  getPluginManifestLoadingStatus,
70
73
  getPluginMetaById,
71
74
  getPluginSettingsById,
75
+ isCustomPlugin,
72
76
  pluginList,
73
77
  };
@@ -1,3 +1,4 @@
1
+ export * from './useEffectAfterHydrated';
1
2
  export * from './useOnFinishHydrationSession';
2
3
  export * from './useSessionChatInit';
3
4
  export * from './useSessionHydrated';
@@ -0,0 +1,22 @@
1
+ import { useEffect } from 'react';
2
+
3
+ import { useSessionStore } from '../store';
4
+
5
+ export const useEffectAfterSessionHydrated = (
6
+ fn: (session: typeof useSessionStore) => void,
7
+ deps: any[] = [],
8
+ ) => {
9
+ useEffect(() => {
10
+ const hasRehydrated = useSessionStore.persist.hasHydrated();
11
+
12
+ if (hasRehydrated) {
13
+ // 等价 useEffect 多次触发
14
+ fn(useSessionStore);
15
+ } else {
16
+ // 等价于 useEffect 第一次触发
17
+ useSessionStore.persist.onFinishHydration(() => {
18
+ fn(useSessionStore);
19
+ });
20
+ }
21
+ }, deps);
22
+ };
@@ -5,16 +5,12 @@ import { SessionStore, useSessionStore } from '../store';
5
5
  /**
6
6
  * 当 Session 水合完毕后才会执行的 useEffect
7
7
  * @param fn
8
- * @param deps
9
8
  */
10
- export const useOnFinishHydrationSession = (
11
- fn: (state: SessionStore) => void,
12
- deps: any[] = [],
13
- ) => {
9
+ export const useOnFinishHydrationSession = (fn: (state: SessionStore) => void) => {
14
10
  useEffect(() => {
15
11
  // 只有当水合完毕后再开始做操作
16
12
  useSessionStore.persist.onFinishHydration(() => {
17
13
  fn(useSessionStore.getState());
18
14
  });
19
- }, deps);
15
+ }, []);
20
16
  };
@@ -1,6 +1,7 @@
1
- import { useEffect, useState } from 'react';
1
+ import { useState } from 'react';
2
2
 
3
3
  import { useSessionStore } from '../store';
4
+ import { useOnFinishHydrationSession } from './useOnFinishHydrationSession';
4
5
 
5
6
  export const useSessionHydrated = () => {
6
7
  // 根据 sessions 是否有值来判断是否已经初始化
@@ -8,11 +9,9 @@ export const useSessionHydrated = () => {
8
9
 
9
10
  const [isInit, setInit] = useState(hasInited);
10
11
 
11
- useEffect(() => {
12
- useSessionStore.persist.onFinishHydration(() => {
13
- if (!isInit) setInit(true);
14
- });
15
- }, []);
12
+ useOnFinishHydrationSession(() => {
13
+ if (!isInit) setInit(true);
14
+ });
16
15
 
17
16
  return isInit;
18
17
  };
@@ -1,3 +1,4 @@
1
+ import { produce } from 'immer';
1
2
  import { StateCreator } from 'zustand/vanilla';
2
3
 
3
4
  import { MetaData } from '@/types/meta';
@@ -7,9 +8,10 @@ import { SessionStore } from '../../store';
7
8
  import { sessionSelectors } from '../session/selectors';
8
9
 
9
10
  /**
10
- * 代理行为接口
11
+ * 助手接口
11
12
  */
12
13
  export interface AgentAction {
14
+ removePlugin: (id: string) => void;
13
15
  /**
14
16
  * 更新代理配置
15
17
  * @param config - 部分 LobeAgentConfig 的配置
@@ -24,6 +26,18 @@ export const createAgentSlice: StateCreator<
24
26
  [],
25
27
  AgentAction
26
28
  > = (set, get) => ({
29
+ removePlugin: (id) => {
30
+ const { activeId } = get();
31
+ const session = sessionSelectors.currentSession(get());
32
+ if (!activeId || !session) return;
33
+
34
+ const config = produce(session.config, (draft) => {
35
+ draft.plugins = draft.plugins?.filter((i) => i !== id) || [];
36
+ });
37
+
38
+ get().dispatchSession({ config, id: activeId, type: 'updateSessionConfig' });
39
+ },
40
+
27
41
  updateAgentConfig: (config) => {
28
42
  const { activeId } = get();
29
43
  const session = sessionSelectors.currentSession(get());
@@ -31,6 +45,7 @@ export const createAgentSlice: StateCreator<
31
45
 
32
46
  get().dispatchSession({ config, id: activeId, type: 'updateSessionConfig' });
33
47
  },
48
+
34
49
  updateAgentMeta: (meta) => {
35
50
  const { activeId } = get();
36
51
  const session = sessionSelectors.currentSession(get());
@@ -1,40 +0,0 @@
1
- {
2
- "exportType": "agents",
3
- "state": {
4
- "sessions": {
5
- "9a2def55-ff50-4f7f-ad6c-b97dac817193": {
6
- "chats": {},
7
- "config": {
8
- "model": "gpt-3.5-turbo",
9
- "params": { "temperature": 0.6 },
10
- "systemRole": "你是一名专业的前端。擅长书写 Typescript JSDoc 代码,代码的示例如下:\n\n```ts\ninterface Props {\n/**\n* 尺寸\n* */\nloading: boolean;\n/**\n* 头像形状\n* @default 'square'\n* @enum [\"square\",\"circle\"]\n* @enumNames [ \"方形\", \"圆形\"]\n*/\nshape?: 'square' | 'circle';\n/**\n* 返回事件\n* @ignore\n*/\nonBack: () => void;\n/**\n* 选择路由的回调函数\n* @param key - 选中的路由\n* @ignore\n*/\nonSelect?: (key: string) => any;\n/**\n* 点击事件回调函数\n* @ignore\n*/\nonClick?: () => void;\n/**\n* 引用\n* @ignore\n*/\nref: any;\n/**\n* Tooltip 提示框位置\n* @enum ['top', 'left', 'right', 'bottom', 'topLeft', 'topRight', 'bottomLeft', 'bottomRight', 'leftTop', 'leftBottom', 'rightTop', 'rightBottom']\n* @enumNames ['上', '左', '右', '下', '左上', '右上', '左下', '右下', '左上', '左下', '右上', '右下']\n* @default 'top'\n*/\nplacement?: TooltipPlacement;\n}\n```\n\n\n接下来用户会输入一串 interface 代码,需要你补全 jsdoc。其中接口的类型不可改变"
11
- },
12
- "createAt": 1690184262713,
13
- "id": "9a2def55-ff50-4f7f-ad6c-b97dac817193",
14
- "meta": { "backgroundColor": "#ec5e41", "avatar": "🥲" },
15
- "type": "agent",
16
- "updateAt": 1690184262713,
17
- "topics": {}
18
- },
19
- "744fe028-c8a2-410c-8b0a-66cfa00d647b": {
20
- "chats": {},
21
- "config": {
22
- "model": "gpt-3.5-turbo",
23
- "params": { "temperature": 0.6 },
24
- "systemRole": "- dafdsafa\n- da- dafdsafa\n- da- dafdsafa\n- da- dafdsafa\n- da- dafdsafa\n- da"
25
- },
26
- "createAt": 1690184730185,
27
- "id": "744fe028-c8a2-410c-8b0a-66cfa00d647b",
28
- "meta": {
29
- "backgroundColor": "#ffef5c",
30
- "title": "dafaa",
31
- "description": "safsafsafsafsafsafsfasfas"
32
- },
33
- "type": "agent",
34
- "updateAt": 1690184730185,
35
- "topics": {}
36
- }
37
- }
38
- },
39
- "version": 1
40
- }