@lobehub/chat 0.66.0 → 0.67.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,49 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## [Version 0.67.0](https://github.com/lobehub/lobe-chat/compare/v0.66.0...v0.67.0)
6
+
7
+ <sup>Released on **2023-09-02**</sup>
8
+
9
+ #### ♻ Code Refactoring
10
+
11
+ - **plugin**: 重构 plugin Store 组织结构,便于开发与迭代维护.
12
+
13
+ #### ✨ Features
14
+
15
+ - **plugin-dev**: 优化 manifest 报错原因提示,并支持 id 从 manifest 自动获取.
16
+
17
+ #### 🐛 Bug Fixes
18
+
19
+ - **plugin-dev**: 修正编辑模式下预览展示问题和 id 重复校验问题.
20
+ - **plugin**: 修正开启插件后会话无效的问题.
21
+
22
+ <br/>
23
+
24
+ <details>
25
+ <summary><kbd>Improvements and Fixes</kbd></summary>
26
+
27
+ #### Code refactoring
28
+
29
+ - **plugin**: 重构 plugin Store 组织结构,便于开发与迭代维护 ([ec527cb](https://github.com/lobehub/lobe-chat/commit/ec527cb))
30
+
31
+ #### What's improved
32
+
33
+ - **plugin-dev**: 优化 manifest 报错原因提示,并支持 id 从 manifest 自动获取 ([7f0787d](https://github.com/lobehub/lobe-chat/commit/7f0787d))
34
+
35
+ #### What's fixed
36
+
37
+ - **plugin-dev**: 修正编辑模式下预览展示问题和 id 重复校验问题 ([17c39ef](https://github.com/lobehub/lobe-chat/commit/17c39ef))
38
+ - **plugin**: 修正开启插件后会话无效的问题 ([82e3beb](https://github.com/lobehub/lobe-chat/commit/82e3beb))
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
+
5
48
  ## [Version 0.66.0](https://github.com/lobehub/lobe-chat/compare/v0.65.1...v0.66.0)
6
49
 
7
50
  <sup>Released on **2023-09-02**</sup>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "0.66.0",
3
+ "version": "0.67.0",
4
4
  "description": "Lobe Chat is an open-source chatbot client using LangChain, Typescript and Next.js",
5
5
  "keywords": [
6
6
  "chatbot",
@@ -17,11 +17,13 @@ const MarketList = memo<{ id: string }>(({ id }) => {
17
17
  const updateConfig = useStore((s) => s.toggleAgentPlugin);
18
18
  const [plugins, hasPlugin] = useStore((s) => [s.config.plugins || [], !!s.config.plugins]);
19
19
 
20
- const [useFetchPluginList, fetchPluginManifest, dispatchDevPluginList] = usePluginStore((s) => [
21
- s.useFetchPluginList,
22
- s.fetchPluginManifest,
23
- s.dispatchDevPluginList,
24
- ]);
20
+ const [useFetchPluginList, fetchPluginManifest, deleteCustomPlugin, updateCustomPlugin] =
21
+ usePluginStore((s) => [
22
+ s.useFetchPluginList,
23
+ s.fetchPluginManifest,
24
+ s.deleteCustomPlugin,
25
+ s.updateCustomPlugin,
26
+ ]);
25
27
 
26
28
  const pluginManifestLoading = usePluginStore((s) => s.pluginManifestLoading, isEqual);
27
29
  const devPlugin = usePluginStore(pluginSelectors.getDevPluginById(id), isEqual);
@@ -31,15 +33,15 @@ const MarketList = memo<{ id: string }>(({ id }) => {
31
33
  return (
32
34
  <>
33
35
  <DevModal
36
+ mode={'edit'}
34
37
  onDelete={() => {
35
- dispatchDevPluginList({ id, type: 'deleteItem' });
38
+ deleteCustomPlugin(id);
36
39
  }}
37
40
  onOpenChange={setModal}
38
41
  onSave={(value) => {
39
- dispatchDevPluginList({ id, plugin: value, type: 'updateItem' });
42
+ updateCustomPlugin(id, value);
40
43
  }}
41
44
  open={showModal}
42
- showDelete
43
45
  value={devPlugin}
44
46
  />
45
47
 
@@ -41,22 +41,33 @@ const MarketList = memo(() => {
41
41
  // setModal(true);
42
42
  // }, []);
43
43
 
44
- const updateConfig = useStore((s) => s.toggleAgentPlugin);
45
- const [plugins, hasPlugin] = useStore((s) => [s.config.plugins || [], !!s.config.plugins]);
44
+ const [plugins, hasPlugin, toggleAgentPlugin] = useStore((s) => [
45
+ s.config.plugins || [],
46
+ !!s.config.plugins,
47
+ s.toggleAgentPlugin,
48
+ ]);
46
49
 
47
50
  const [useFetchPluginList, fetchPluginManifest, saveToDevList, updateNewDevPlugin] =
48
51
  usePluginStore((s) => [
49
52
  s.useFetchPluginList,
50
53
  s.fetchPluginManifest,
51
- s.saveToDevList,
52
- s.updateNewDevPlugin,
54
+ s.saveToCustomPluginList,
55
+ s.updateNewCustomPlugin,
53
56
  ]);
54
57
  const pluginManifestLoading = usePluginStore((s) => s.pluginManifestLoading, isEqual);
55
58
  const pluginList = usePluginStore((s) => s.pluginList, isEqual);
56
- const devPluginList = usePluginStore((s) => s.devPluginList, isEqual);
59
+ const customPluginList = usePluginStore((s) => s.customPluginList, isEqual);
57
60
 
58
61
  useFetchPluginList();
59
62
 
63
+ const togglePlugin = async (pluginId: string, fetchManifest?: boolean) => {
64
+ toggleAgentPlugin(pluginId);
65
+ if (fetchManifest) {
66
+ await fetchPluginManifest(pluginId);
67
+ }
68
+ };
69
+
70
+ // =========== Skeleton Loading =========== //
60
71
  const loadingItem = {
61
72
  avatar: (
62
73
  <Skeleton
@@ -77,11 +88,12 @@ const MarketList = memo(() => {
77
88
  />
78
89
  ),
79
90
  };
80
-
81
91
  const loadingList = [loadingItem, loadingItem, loadingItem];
82
92
 
83
93
  const isEmpty = pluginList.length === 0;
84
94
 
95
+ // =========== Plugin List =========== //
96
+
85
97
  const list = pluginList.map(({ identifier, meta }) => ({
86
98
  avatar: <Avatar avatar={meta.avatar} />,
87
99
  children: (
@@ -92,10 +104,7 @@ const MarketList = memo(() => {
92
104
  }
93
105
  loading={pluginManifestLoading[identifier]}
94
106
  onChange={(checked) => {
95
- updateConfig(identifier);
96
- if (checked) {
97
- fetchPluginManifest(identifier);
98
- }
107
+ togglePlugin(identifier, checked);
99
108
  }}
100
109
  />
101
110
  ),
@@ -105,7 +114,9 @@ const MarketList = memo(() => {
105
114
  tag: identifier,
106
115
  }));
107
116
 
108
- const devList = devPluginList.map(({ identifier, meta }) => ({
117
+ // =========== Custom Plugin List =========== //
118
+
119
+ const customList = customPluginList.map(({ identifier, meta }) => ({
109
120
  avatar: <Avatar avatar={pluginHelpers.getPluginAvatar(meta) || '🧩'} />,
110
121
  children: <LocalPluginItem id={identifier} />,
111
122
  desc: pluginHelpers.getPluginDesc(meta),
@@ -125,14 +136,19 @@ const MarketList = memo(() => {
125
136
  <>
126
137
  <DevModal
127
138
  onOpenChange={setModal}
128
- onSave={saveToDevList}
139
+ onSave={async (devPlugin) => {
140
+ // 先保存
141
+ saveToDevList(devPlugin);
142
+ // 再开启插件
143
+ await togglePlugin(devPlugin.identifier, true);
144
+ }}
129
145
  onValueChange={updateNewDevPlugin}
130
146
  open={showModal}
131
147
  />
132
148
  <Form
133
149
  items={[
134
150
  {
135
- children: isEmpty ? loadingList : [...devList, ...list],
151
+ children: isEmpty ? loadingList : [...customList, ...list],
136
152
  extra: (
137
153
  <Tooltip title={t('settingPlugin.addTooltip')}>
138
154
  <Button
@@ -1,96 +1,160 @@
1
- import { pluginManifestSchema } from '@lobehub/chat-plugin-sdk';
2
- import { Form, FormItemProps, Input, Tooltip } from '@lobehub/ui';
3
- import { FormInstance, Radio } from 'antd';
4
- import { memo } from 'react';
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';
4
+ import { FileCode, RotateCwIcon } from 'lucide-react';
5
+ import { memo, useState } from 'react';
5
6
  import { useTranslation } from 'react-i18next';
7
+ import { Flexbox } from 'react-layout-kit';
6
8
 
7
- const ManifestForm = memo<{ form: FormInstance; mode: 'url' | 'local' }>(({ form, mode }) => {
8
- const { t } = useTranslation('plugin');
9
+ const ManifestForm = memo<{ form: FormInstance; mode?: 'url' | 'local' }>(
10
+ ({ form, mode = 'url' }) => {
11
+ const { t } = useTranslation('plugin');
9
12
 
10
- const isUrl = mode === 'url';
13
+ const [manifest, setManifest] = useState<LobeChatPluginManifest>();
11
14
 
12
- const configItem: FormItemProps[] = isUrl
13
- ? [
14
- {
15
- children: <Input placeholder={'http://localhost/manifest.json'} />,
16
- desc: t('dev.meta.manifest.desc'),
17
- hasFeedback: true,
18
- label: t('dev.meta.manifest.label'),
19
- name: 'manifest',
20
- required: true,
21
- rules: [
22
- { required: true },
23
- {
24
- message: t('dev.meta.manifest.urlError'),
25
- pattern: /^https?:\/\/.*/,
26
- },
27
- {
28
- message: t('dev.meta.manifest.invalid'),
29
- validator: async (_, value) => {
30
- const res = await fetch(value);
31
- if (!res.ok) return true;
15
+ const isUrl = mode === 'url';
32
16
 
33
- const json = await res.json();
34
- pluginManifestSchema.parse(json);
17
+ const configItem: FormItemProps[] = isUrl
18
+ ? [
19
+ {
20
+ children: (
21
+ <Input
22
+ placeholder={'http://localhost:3400/manifest-dev.json'}
23
+ 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
+ )
35
+ }
36
+ />
37
+ ),
38
+ extra: (
39
+ <Flexbox horizontal justify={'space-between'} style={{ marginTop: 8 }}>
40
+ {t('dev.meta.manifest.desc')}
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
+ >
54
+ <ActionIcon
55
+ icon={FileCode}
56
+ size={'small'}
57
+ title={t('dev.meta.manifest.preview')}
58
+ />
59
+ </Popover>
60
+ )}
61
+ </Flexbox>
62
+ ),
63
+ // extra: <div>123</div>,
64
+ hasFeedback: true,
65
+ label: t('dev.meta.manifest.label'),
66
+ name: 'manifest',
67
+ required: true,
68
+ rules: [
69
+ { required: true },
70
+ {
71
+ message: t('dev.meta.manifest.urlError'),
72
+ pattern: /^https?:\/\/.*/,
35
73
  },
36
- },
37
- ],
38
- },
39
- ]
40
- : // TODO: 后续做成本地配置模式
41
- [
42
- {
43
- children: <Input placeholder={'searchEngine'} />,
44
- desc: t('dev.meta.identifier.desc'),
45
- label: t('dev.meta.identifier.label'),
46
- name: 'name',
47
- required: true,
48
- },
74
+ {
75
+ // message: t('dev.meta.manifest.invalid'),
76
+ validator: async (_, value) => {
77
+ if (!value) return true;
49
78
 
50
- {
51
- children: <Input placeholder={t('dev.meta.description.placeholder')} />,
52
- desc: t('dev.meta.description.desc'),
53
- label: t('dev.meta.description.label'),
54
- name: 'description',
55
- required: true,
56
- },
57
- {
58
- children: <Input placeholder={'searchEngine'} />,
59
- desc: t('dev.meta.identifier.desc'),
60
- label: t('dev.meta.identifier.label'),
61
- name: 'identifier',
62
- required: true,
63
- },
64
- ];
79
+ let res: Response;
65
80
 
66
- return (
67
- <Form
68
- form={form}
69
- items={[
70
- {
71
- children: configItem,
72
- extra: (
73
- <Radio.Group
74
- onChange={(v) => {
75
- form.setFieldValue('manifestMode', v.target.value);
76
- }}
77
- size={'small'}
78
- value={mode}
79
- >
80
- <Radio.Button value={'url'}>{t('dev.manifest.mode.url')}</Radio.Button>
81
- <Tooltip title={t('dev.manifest.mode.local-tooltip')}>
82
- <Radio.Button disabled value={'local'}>
83
- {t('dev.manifest.mode.local')}
84
- </Radio.Button>
85
- </Tooltip>
86
- </Radio.Group>
87
- ),
88
- title: t('dev.tabs.manifest'),
89
- },
90
- ]}
91
- layout={isUrl ? 'vertical' : undefined}
92
- />
93
- );
94
- });
81
+ try {
82
+ res = await fetch(value);
83
+ } catch {
84
+ throw t('dev.meta.manifest.requestError');
85
+ }
86
+
87
+ const json = await res.json().catch(() => {
88
+ throw t('dev.meta.manifest.urlError');
89
+ });
90
+
91
+ const valid = pluginManifestSchema.safeParse(json);
92
+ if (!valid.success) {
93
+ throw t('dev.meta.manifest.jsonInvalid', { error: valid.error });
94
+ }
95
+
96
+ setManifest(json);
97
+ form.setFieldValue('identifier', valid.data.identifier);
98
+ },
99
+ },
100
+ ],
101
+ },
102
+ ]
103
+ : // TODO: 后续做成本地配置模式
104
+ [
105
+ {
106
+ children: <Input placeholder={'searchEngine'} />,
107
+ desc: t('dev.meta.identifier.desc'),
108
+ label: t('dev.meta.identifier.label'),
109
+ name: 'name',
110
+ required: true,
111
+ },
112
+
113
+ {
114
+ children: <Input placeholder={t('dev.meta.description.placeholder')} />,
115
+ desc: t('dev.meta.description.desc'),
116
+ label: t('dev.meta.description.label'),
117
+ name: 'description',
118
+ required: true,
119
+ },
120
+ {
121
+ children: <Input placeholder={'searchEngine'} />,
122
+ desc: t('dev.meta.identifier.desc'),
123
+ label: t('dev.meta.identifier.label'),
124
+ name: 'identifier',
125
+ required: true,
126
+ },
127
+ ];
128
+
129
+ return (
130
+ <Form
131
+ form={form}
132
+ items={[
133
+ {
134
+ children: configItem,
135
+ extra: (
136
+ <Radio.Group
137
+ onChange={(v) => {
138
+ form.setFieldValue('manifestMode', v.target.value);
139
+ }}
140
+ size={'small'}
141
+ value={mode}
142
+ >
143
+ <Radio.Button value={'url'}>{t('dev.manifest.mode.url')}</Radio.Button>
144
+ <Tooltip title={t('dev.manifest.mode.local-tooltip')}>
145
+ <Radio.Button disabled value={'local'}>
146
+ {t('dev.manifest.mode.local')}
147
+ </Radio.Button>
148
+ </Tooltip>
149
+ </Radio.Group>
150
+ ),
151
+ title: t('dev.tabs.manifest'),
152
+ },
153
+ ]}
154
+ layout={isUrl ? 'vertical' : undefined}
155
+ />
156
+ );
157
+ },
158
+ );
95
159
 
96
160
  export default ManifestForm;
@@ -1,24 +1,20 @@
1
1
  import { Form, FormItemProps, Input } from '@lobehub/ui';
2
2
  import { FormInstance } from 'antd';
3
- import { memo, useEffect } from 'react';
3
+ import { memo } from 'react';
4
4
  import { useTranslation } from 'react-i18next';
5
5
 
6
6
  import EmojiPicker from '@/components/EmojiPicker';
7
7
  import { pluginSelectors, usePluginStore } from '@/store/plugin';
8
8
 
9
- const MetaForm = memo<{ form: FormInstance }>(({ form }) => {
9
+ const MetaForm = memo<{ form: FormInstance; mode?: 'edit' | 'create' }>(({ form, mode }) => {
10
+ const isEditMode = mode === 'edit';
11
+
10
12
  const { t } = useTranslation('plugin');
11
13
  const [plugins] = usePluginStore((s) => [pluginSelectors.pluginList(s).map((i) => i.identifier)]);
12
14
 
13
- useEffect(() => {
14
- if (usePluginStore.getState().newDevPlugin) {
15
- form.setFieldsValue(usePluginStore.getState().newDevPlugin);
16
- }
17
- }, []);
18
-
19
15
  const configItem: FormItemProps[] = [
20
16
  {
21
- children: <Input placeholder={'searchEngine'} />,
17
+ children: <Input disabled placeholder={'searchEngine'} />,
22
18
  desc: t('dev.meta.identifier.desc'),
23
19
  label: t('dev.meta.identifier.label'),
24
20
  name: 'identifier',
@@ -28,14 +24,17 @@ const MetaForm = memo<{ form: FormInstance }>(({ form }) => {
28
24
  message: t('dev.meta.identifier.pattenErrorMessage'),
29
25
  pattern: /^[\w-]+$/,
30
26
  },
31
- {
32
- message: t('dev.meta.identifier.errorDuplicate'),
33
- validator: async (_, value) => {
34
- if (plugins.includes(value)) {
35
- throw new Error('Duplicate');
36
- }
37
- },
38
- },
27
+ // 编辑模式下,不进行重复校验
28
+ isEditMode
29
+ ? {}
30
+ : {
31
+ message: t('dev.meta.identifier.errorDuplicate'),
32
+ validator: async (_, value) => {
33
+ if (plugins.includes(value)) {
34
+ throw new Error('Duplicate');
35
+ }
36
+ },
37
+ },
39
38
  ],
40
39
  },
41
40
  {
@@ -1,29 +1,30 @@
1
1
  import { Avatar, Form } from '@lobehub/ui';
2
- import { Card, Switch, Tag } from 'antd';
2
+ import { Form as AForm, Card, FormInstance, Switch, Tag } from 'antd';
3
3
  import { memo } from 'react';
4
4
  import { useTranslation } from 'react-i18next';
5
5
  import { Flexbox } from 'react-layout-kit';
6
6
 
7
- import { pluginHelpers, usePluginStore } from '@/store/plugin';
7
+ import { pluginHelpers } from '@/store/plugin';
8
+ import { CustomPlugin } from '@/types/plugin';
8
9
 
9
- const PluginPreview = memo(() => {
10
+ const PluginPreview = memo<{ form: FormInstance }>(({ form }) => {
10
11
  const { t } = useTranslation('plugin');
11
- const meta = usePluginStore((s) => s.newDevPlugin);
12
+ const plugin: CustomPlugin = AForm.useWatch([], form);
12
13
 
13
14
  const items = {
14
- avatar: <Avatar avatar={pluginHelpers.getPluginAvatar(meta?.meta) || '🧩'} />,
15
+ avatar: <Avatar avatar={pluginHelpers.getPluginAvatar(plugin?.meta) || '🧩'} />,
15
16
  children: <Switch disabled />,
16
- desc: pluginHelpers.getPluginDesc(meta?.meta),
17
+ desc: pluginHelpers.getPluginDesc(plugin?.meta),
17
18
  label: (
18
19
  <Flexbox align={'center'} gap={8} horizontal>
19
- {pluginHelpers.getPluginTitle(meta?.meta) ?? t('dev.preview.title')}
20
+ {pluginHelpers.getPluginTitle(plugin?.meta) ?? t('dev.preview.title')}
20
21
  <Tag bordered={false} color={'gold'}>
21
22
  {t('list.item.local.title', { ns: 'plugin' })}
22
23
  </Tag>
23
24
  </Flexbox>
24
25
  ),
25
26
  minWidth: undefined,
26
- tag: !!meta?.identifier ? meta?.identifier : 'id',
27
+ tag: !!plugin?.identifier ? plugin?.identifier : 'id',
27
28
  };
28
29
 
29
30
  return (
@@ -1,13 +1,13 @@
1
1
  import { Icon } from '@lobehub/ui';
2
2
  import { App, Button, ConfigProvider, Form, Modal, Popconfirm } from 'antd';
3
- import { createStyles, useTheme } from 'antd-style';
3
+ import { createStyles } from 'antd-style';
4
4
  import { LucideBlocks } from 'lucide-react';
5
5
  import { lighten } from 'polished';
6
- import { memo, useEffect } from 'react';
6
+ import { memo, useEffect, useState } from 'react';
7
7
  import { useTranslation } from 'react-i18next';
8
8
  import { Flexbox } from 'react-layout-kit';
9
9
 
10
- import { DevPlugin } from '@/store/plugin/initialState';
10
+ import { CustomPlugin } from '@/types/plugin';
11
11
 
12
12
  import ManifestForm from './ManifestForm';
13
13
  import MetaForm from './MetaForm';
@@ -26,33 +26,31 @@ const useStyles = createStyles(({ css, token, prefixCls }) => ({
26
26
  }));
27
27
 
28
28
  interface DevModalProps {
29
+ mode?: 'edit' | 'create';
29
30
  onDelete?: () => void;
30
31
  onOpenChange: (open: boolean) => void;
31
- onSave?: (value: DevPlugin) => void;
32
- onValueChange?: (value: Partial<DevPlugin>) => void;
32
+ onSave?: (value: CustomPlugin) => Promise<void> | void;
33
+ onValueChange?: (value: Partial<CustomPlugin>) => void;
33
34
  open?: boolean;
34
- showDelete?: boolean;
35
- value?: DevPlugin;
35
+ value?: CustomPlugin;
36
36
  }
37
37
 
38
38
  const DevModal = memo<DevModalProps>(
39
- ({ open, showDelete, value, onValueChange, onSave, onOpenChange, onDelete }) => {
39
+ ({ open, mode = 'create', value, onValueChange, onSave, onOpenChange, onDelete }) => {
40
+ const isEditMode = mode === 'edit';
40
41
  const { t } = useTranslation('plugin');
41
-
42
- const [form] = Form.useForm();
43
- const theme = useTheme();
44
-
45
- const { styles } = useStyles();
46
-
42
+ const { styles, theme } = useStyles();
47
43
  const { message } = App.useApp();
48
44
 
45
+ const [submitting, setSubmitting] = useState(false);
46
+ const [form] = Form.useForm();
49
47
  useEffect(() => {
50
48
  form.setFieldsValue(value);
51
49
  }, []);
52
50
 
53
51
  const footer = (
54
52
  <Flexbox horizontal justify={'space-between'} style={{ marginTop: 24 }}>
55
- {showDelete ? (
53
+ {isEditMode ? (
56
54
  <Popconfirm
57
55
  okButtonProps={{
58
56
  danger: true,
@@ -80,24 +78,30 @@ const DevModal = memo<DevModalProps>(
80
78
  {t('cancel', { ns: 'common' })}
81
79
  </Button>
82
80
  <Button
81
+ loading={submitting}
83
82
  onClick={() => {
84
83
  form.submit();
85
84
  }}
86
85
  type={'primary'}
87
86
  >
88
- {t('dev.save')}
87
+ {t(isEditMode ? 'dev.update' : 'dev.save')}
89
88
  </Button>
90
89
  </Flexbox>
91
90
  </Flexbox>
92
91
  );
92
+
93
93
  return (
94
94
  <Form.Provider
95
95
  onFormChange={() => {
96
96
  onValueChange?.(form.getFieldsValue());
97
97
  }}
98
- onFormFinish={(_, info) => {
99
- onSave?.(info.values as DevPlugin);
100
- message.success(t('dev.saveSuccess'));
98
+ onFormFinish={async (_, info) => {
99
+ if (onSave) {
100
+ setSubmitting(true);
101
+ await onSave?.(info.values as CustomPlugin);
102
+ setSubmitting(false);
103
+ }
104
+ message.success(t(isEditMode ? 'dev.updateSuccess' : 'dev.saveSuccess'));
101
105
  onOpenChange(false);
102
106
  }}
103
107
  >
@@ -121,7 +125,6 @@ const DevModal = memo<DevModalProps>(
121
125
  form.submit();
122
126
  }}
123
127
  open={open}
124
- style={{ height: 750 }}
125
128
  title={
126
129
  <Flexbox gap={8} horizontal>
127
130
  <Icon icon={LucideBlocks} />
@@ -139,9 +142,9 @@ const DevModal = memo<DevModalProps>(
139
142
  {/* { children: <ManifestForm />, key: 'manifest', label: t('dev.tabs.manifest') },*/}
140
143
  {/* ]}*/}
141
144
  {/*/>*/}
142
- <PluginPreview />
143
- <ManifestForm form={form} mode={'url'} />
144
- <MetaForm form={form} />
145
+ <PluginPreview form={form} />
146
+ <ManifestForm form={form} />
147
+ <MetaForm form={form} mode={mode} />
145
148
  </Flexbox>
146
149
  </Modal>
147
150
  </ConfigProvider>
@@ -43,16 +43,19 @@ export default {
43
43
  label: '首页',
44
44
  },
45
45
  identifier: {
46
- desc: '插件的唯一标识,仅支持英文字符、数字、破折号 - 、下划线 _',
46
+ desc: '插件的唯一标识,将从 manifest 中自动识别',
47
47
  errorDuplicate: '标识符和已有插件重复,请修改标识符',
48
48
  label: '标识符',
49
49
  pattenErrorMessage: '只能输入英文字符、数字 、- 和_ 这两个符号',
50
50
  },
51
51
  manifest: {
52
52
  desc: 'LobeChat 将会通过该链接安装插件',
53
- invalid: '输入的 manifest 链接无效,或 manifest 不符合规范',
53
+ jsonInvalid: ' manifest 不符合规范,校验结果: \n\n {{error}}',
54
54
  label: '插件描述文件 Url 地址',
55
- urlError: '请输入一个有效的网址',
55
+ preview: '预览 Manifest',
56
+ refresh: '刷新',
57
+ requestError: '请求该链接失败,请输入一个有效的链接,并检查链接是否允许跨域访问',
58
+ urlError: '该链接没有返回 JSON 格式的内容, 请输入一个有效的链接',
56
59
  },
57
60
  title: {
58
61
  desc: '插件标题',
@@ -73,13 +76,14 @@ export default {
73
76
  manifest: '功能描述清单 (Manifest)',
74
77
  meta: '插件元信息',
75
78
  },
76
-
77
79
  title: '添加自定义插件',
80
+ update: '更新',
81
+ updateSuccess: '插件设置更新成功',
78
82
  },
79
83
  list: {
80
84
  item: {
81
85
  'local.config': '配置',
82
- 'local.title': '本地',
86
+ 'local.title': '自定义',
83
87
  },
84
88
  },
85
89
 
@@ -1,38 +1,9 @@
1
- import { LobeChatPluginManifest, LobeChatPluginMeta } from '@lobehub/chat-plugin-sdk';
1
+ import { CustomPluginState, initialCustomPluginState } from './slices/customPlugin';
2
+ import { PluginState, initialPluginState } from './slices/plugin';
2
3
 
3
- import { PluginManifestMap } from '@/types/plugin';
4
-
5
- export type PluginManifestLoadingState = Record<string, boolean>;
6
- export type PluginsSettings = Record<string, any>;
7
-
8
- export interface DevPlugin extends LobeChatPluginMeta {
9
- apiMode: 'openapi' | 'simple';
10
- enableSettings: boolean;
11
- manifestConfig?: LobeChatPluginManifest;
12
- manifestMode: 'local' | 'url';
13
- }
14
-
15
- export interface PluginStoreState {
16
- devPluginList: DevPlugin[];
17
- manifestPrepared: boolean;
18
- newDevPlugin: Partial<DevPlugin>;
19
- pluginList: LobeChatPluginMeta[];
20
- pluginManifestLoading: PluginManifestLoadingState;
21
- pluginManifestMap: PluginManifestMap;
22
- pluginsSettings: PluginsSettings;
23
- }
24
- export const defaultDevPlugin: Partial<DevPlugin> = {
25
- apiMode: 'simple',
26
- enableSettings: false,
27
- manifestMode: 'url',
28
- };
4
+ export type PluginStoreState = PluginState & CustomPluginState;
29
5
 
30
6
  export const initialState: PluginStoreState = {
31
- devPluginList: [],
32
- manifestPrepared: false,
33
- newDevPlugin: defaultDevPlugin,
34
- pluginList: [],
35
- pluginManifestLoading: {},
36
- pluginManifestMap: {},
37
- pluginsSettings: {},
7
+ ...initialPluginState,
8
+ ...initialCustomPluginState,
38
9
  };
@@ -23,13 +23,13 @@ const enabledSchema =
23
23
  );
24
24
  };
25
25
 
26
- const pluginList = (s: PluginStoreState) => [...s.pluginList, ...s.devPluginList];
26
+ const pluginList = (s: PluginStoreState) => [...s.pluginList, ...s.customPluginList];
27
27
 
28
28
  const getPluginMetaById = (id: string) => (s: PluginStoreState) =>
29
29
  pluginHelpers.getPluginFormList(pluginList(s), id);
30
30
 
31
31
  const getDevPluginById = (id: string) => (s: PluginStoreState) =>
32
- s.devPluginList.find((i) => i.identifier === id);
32
+ s.customPluginList.find((i) => i.identifier === id);
33
33
 
34
34
  const getPluginManifestById = (id: string) => (s: PluginStoreState) => s.pluginManifestMap[id];
35
35
  const getPluginSettingsById = (id: string) => (s: PluginStoreState) => s.pluginsSettings[id];
@@ -0,0 +1,64 @@
1
+ import { merge } from 'lodash-es';
2
+ import { StateCreator } from 'zustand/vanilla';
3
+
4
+ import { CustomPlugin } from '@/types/plugin';
5
+ import { setNamespace } from '@/utils/storeDebug';
6
+
7
+ import { PluginStore } from '../../store';
8
+ import { defaultCustomPlugin } from './initialState';
9
+ import { CustomPluginListDispatch, devPluginListReducer } from './reducers/customPluginList';
10
+
11
+ const t = setNamespace('customPlugin');
12
+
13
+ /**
14
+ * 代理行为接口
15
+ */
16
+ export interface CustomPluginAction {
17
+ deleteCustomPlugin: (id: string) => void;
18
+ dispatchCustomPluginList: (payload: CustomPluginListDispatch) => void;
19
+ saveToCustomPluginList: (value: CustomPlugin) => void;
20
+ updateCustomPlugin: (id: string, value: CustomPlugin) => void;
21
+ updateNewCustomPlugin: (value: Partial<CustomPlugin>) => void;
22
+ }
23
+
24
+ export const createCustomPluginSlice: StateCreator<
25
+ PluginStore,
26
+ [['zustand/devtools', never]],
27
+ [],
28
+ CustomPluginAction
29
+ > = (set, get) => ({
30
+ deleteCustomPlugin: (id) => {
31
+ const { dispatchCustomPluginList, dispatchPluginManifest, deletePluginSettings } = get();
32
+ // 1.删除插件项
33
+ dispatchCustomPluginList({ id, type: 'deleteItem' });
34
+ // 2.删除本地 manifest 记录
35
+ dispatchPluginManifest({ id, type: 'deleteManifest' });
36
+ // 3.删除插件配置
37
+ deletePluginSettings(id);
38
+ },
39
+ dispatchCustomPluginList: (payload) => {
40
+ const { customPluginList } = get();
41
+
42
+ const nextList = devPluginListReducer(customPluginList, payload);
43
+ set({ customPluginList: nextList }, false, t('dispatchCustomPluginList', payload));
44
+ },
45
+ saveToCustomPluginList: (value) => {
46
+ get().dispatchCustomPluginList({ plugin: value, type: 'addItem' });
47
+ set({ newCustomPlugin: defaultCustomPlugin }, false, t('saveToCustomPluginList'));
48
+ },
49
+ updateCustomPlugin: (id, value) => {
50
+ const { dispatchCustomPluginList, fetchPluginManifest } = get();
51
+ // 1. 更新 list 项信息
52
+ dispatchCustomPluginList({ id, plugin: value, type: 'updateItem' });
53
+ // 2. 更新 重新拉取 manifest
54
+ fetchPluginManifest(id);
55
+ },
56
+
57
+ updateNewCustomPlugin: (newCustomPlugin) => {
58
+ set(
59
+ { newCustomPlugin: merge({}, get().newCustomPlugin, newCustomPlugin) },
60
+ false,
61
+ t('updateNewDevPlugin'),
62
+ );
63
+ },
64
+ });
@@ -0,0 +1,3 @@
1
+ export * from './action';
2
+ export * from './initialState';
3
+ // export * from './selectors';
@@ -0,0 +1,16 @@
1
+ import { CustomPlugin } from '@/types/plugin';
2
+
3
+ export interface CustomPluginState {
4
+ customPluginList: CustomPlugin[];
5
+ newCustomPlugin: Partial<CustomPlugin>;
6
+ }
7
+ export const defaultCustomPlugin: Partial<CustomPlugin> = {
8
+ apiMode: 'simple',
9
+ enableSettings: false,
10
+ manifestMode: 'url',
11
+ };
12
+
13
+ export const initialCustomPluginState: CustomPluginState = {
14
+ customPluginList: [],
15
+ newCustomPlugin: defaultCustomPlugin,
16
+ };
@@ -1,11 +1,11 @@
1
1
  import { produce } from 'immer';
2
2
 
3
- import { DevPlugin } from '../initialState';
3
+ import { CustomPlugin } from '@/types/plugin';
4
4
 
5
- export type DevListState = DevPlugin[];
5
+ export type DevListState = CustomPlugin[];
6
6
 
7
7
  export type AddPluginAction = {
8
- plugin: DevPlugin;
8
+ plugin: CustomPlugin;
9
9
  type: 'addItem';
10
10
  };
11
11
 
@@ -16,15 +16,15 @@ export type DeletePluginAction = {
16
16
 
17
17
  export type UpdatePluginAction = {
18
18
  id: string;
19
- plugin: DevPlugin;
19
+ plugin: CustomPlugin;
20
20
  type: 'updateItem';
21
21
  };
22
22
 
23
- export type DevListDispatch = AddPluginAction | DeletePluginAction | UpdatePluginAction;
23
+ export type CustomPluginListDispatch = AddPluginAction | DeletePluginAction | UpdatePluginAction;
24
24
 
25
25
  export const devPluginListReducer = (
26
26
  state: DevListState,
27
- payload: DevListDispatch,
27
+ payload: CustomPluginListDispatch,
28
28
  ): DevListState => {
29
29
  switch (payload.type) {
30
30
  case 'addItem': {
@@ -14,10 +14,8 @@ import { pluginSelectors } from '@/store/plugin/selectors';
14
14
  import { LobeSessions } from '@/types/session';
15
15
  import { setNamespace } from '@/utils/storeDebug';
16
16
 
17
- import { DevPlugin, defaultDevPlugin } from './initialState';
18
- import { DevListDispatch, devPluginListReducer } from './reducers/devPluginList';
17
+ import { PluginStore } from '../../store';
19
18
  import { PluginDispatch, pluginManifestReducer } from './reducers/manifest';
20
- import { PluginStore } from './store';
21
19
 
22
20
  const t = setNamespace('plugin');
23
21
 
@@ -26,12 +24,10 @@ const t = setNamespace('plugin');
26
24
  */
27
25
  export interface PluginAction {
28
26
  checkLocalEnabledPlugins: (sessions: LobeSessions) => void;
29
- dispatchDevPluginList: (payload: DevListDispatch) => void;
27
+ deletePluginSettings: (id: string) => void;
30
28
  dispatchPluginManifest: (payload: PluginDispatch) => void;
31
- fetchPluginManifest: (name: string) => Promise<void>;
32
- saveToDevList: (value: DevPlugin) => void;
29
+ fetchPluginManifest: (identifier: string) => Promise<void>;
33
30
  updateManifestLoadingState: (key: string, value: boolean | undefined) => void;
34
- updateNewDevPlugin: (value: Partial<DevPlugin>) => void;
35
31
  updatePluginSettings: <T>(id: string, settings: Partial<T>) => void;
36
32
  useFetchPluginList: () => SWRResponse<LobeChatPluginsMarketIndex>;
37
33
  }
@@ -56,15 +52,18 @@ export const createPluginSlice: StateCreator<
56
52
 
57
53
  const plugins = uniq(enabledPlugins);
58
54
 
59
- await Promise.all(plugins.map((name) => fetchPluginManifest(name)));
55
+ await Promise.all(plugins.map((identifier) => fetchPluginManifest(identifier)));
60
56
 
61
57
  set({ manifestPrepared: true }, false, t('checkLocalEnabledPlugins'));
62
58
  },
63
- dispatchDevPluginList: (payload) => {
64
- const { devPluginList } = get();
65
-
66
- const nextList = devPluginListReducer(devPluginList, payload);
67
- set({ devPluginList: nextList }, false, t('dispatchDevList', payload));
59
+ deletePluginSettings: (id) => {
60
+ set(
61
+ produce((draft) => {
62
+ draft.pluginsSettings[id] = undefined;
63
+ }),
64
+ false,
65
+ t('deletePluginSettings'),
66
+ );
68
67
  },
69
68
  dispatchPluginManifest: (payload) => {
70
69
  const { pluginManifestMap } = get();
@@ -72,6 +71,7 @@ export const createPluginSlice: StateCreator<
72
71
 
73
72
  set({ pluginManifestMap: nextManifest }, false, t('dispatchPluginManifest', payload));
74
73
  },
74
+
75
75
  fetchPluginManifest: async (name) => {
76
76
  const plugin = pluginSelectors.getPluginMetaById(name)(get());
77
77
  // 1. 校验文件
@@ -113,11 +113,6 @@ export const createPluginSlice: StateCreator<
113
113
  // 4. 存储 manifest 信息
114
114
  get().dispatchPluginManifest({ id: plugin.identifier, plugin: data, type: 'addManifest' });
115
115
  },
116
- saveToDevList: (value) => {
117
- get().dispatchDevPluginList({ plugin: value, type: 'addItem' });
118
- set({ newDevPlugin: defaultDevPlugin }, false, t('saveToDevList'));
119
- },
120
-
121
116
  updateManifestLoadingState: (key, value) => {
122
117
  set(
123
118
  produce((draft) => {
@@ -127,13 +122,6 @@ export const createPluginSlice: StateCreator<
127
122
  t('updateManifestLoadingState'),
128
123
  );
129
124
  },
130
- updateNewDevPlugin: (newDevPlugin) => {
131
- set(
132
- { newDevPlugin: merge({}, get().newDevPlugin, newDevPlugin) },
133
- false,
134
- t('updateNewDevPlugin'),
135
- );
136
- },
137
125
  updatePluginSettings: (id, settings) => {
138
126
  set(
139
127
  produce((draft) => {
@@ -0,0 +1,3 @@
1
+ export * from './action';
2
+ export * from './initialState';
3
+ // export * from './selectors';
@@ -0,0 +1,22 @@
1
+ import { LobeChatPluginMeta } from '@lobehub/chat-plugin-sdk';
2
+
3
+ import { PluginManifestMap } from '@/types/plugin';
4
+
5
+ export type PluginManifestLoadingState = Record<string, boolean>;
6
+ export type PluginsSettings = Record<string, any>;
7
+
8
+ export interface PluginState {
9
+ manifestPrepared: boolean;
10
+ pluginList: LobeChatPluginMeta[];
11
+ pluginManifestLoading: PluginManifestLoadingState;
12
+ pluginManifestMap: PluginManifestMap;
13
+ pluginsSettings: PluginsSettings;
14
+ }
15
+
16
+ export const initialPluginState: PluginState = {
17
+ manifestPrepared: false,
18
+ pluginList: [],
19
+ pluginManifestLoading: {},
20
+ pluginManifestMap: {},
21
+ pluginsSettings: {},
22
+ };
@@ -5,23 +5,25 @@ import { StateCreator } from 'zustand/vanilla';
5
5
 
6
6
  import { isDev } from '@/utils/env';
7
7
 
8
- import { PluginAction, createPluginSlice } from './action';
9
8
  import { PluginStoreState, initialState } from './initialState';
9
+ import { CustomPluginAction, createCustomPluginSlice } from './slices/customPlugin';
10
+ import { PluginAction, createPluginSlice } from './slices/plugin';
10
11
 
11
12
  // =============== 聚合 createStoreFn ============ //
12
13
 
13
- export type PluginStore = PluginStoreState & PluginAction;
14
+ export type PluginStore = PluginStoreState & CustomPluginAction & PluginAction;
14
15
 
15
16
  const createStore: StateCreator<PluginStore, [['zustand/devtools', never]]> = (...parameters) => ({
16
17
  ...initialState,
17
18
  ...createPluginSlice(...parameters),
19
+ ...createCustomPluginSlice(...parameters),
18
20
  });
19
21
 
20
22
  // =============== persist 本地缓存中间件配置 ============ //
21
23
 
22
24
  type SessionPersist = Pick<
23
25
  PluginStore,
24
- 'pluginList' | 'pluginManifestMap' | 'pluginsSettings' | 'devPluginList'
26
+ 'pluginList' | 'pluginManifestMap' | 'pluginsSettings' | 'customPluginList'
25
27
  >;
26
28
 
27
29
  const storeName = 'LOBE_PLUGIN';
@@ -30,7 +32,7 @@ const persistOptions: PersistOptions<PluginStore, SessionPersist> = {
30
32
  name: storeName,
31
33
 
32
34
  partialize: (s) => ({
33
- devPluginList: s.devPluginList,
35
+ customPluginList: s.customPluginList,
34
36
  pluginList: s.pluginList,
35
37
  pluginManifestMap: s.pluginManifestMap,
36
38
  pluginsSettings: s.pluginsSettings,
@@ -1,3 +1,10 @@
1
- import { LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
1
+ import { LobeChatPluginManifest, LobeChatPluginMeta } from '@lobehub/chat-plugin-sdk';
2
2
 
3
3
  export type PluginManifestMap = Record<string, LobeChatPluginManifest>;
4
+
5
+ export interface CustomPlugin extends LobeChatPluginMeta {
6
+ apiMode: 'openapi' | 'simple';
7
+ enableSettings: boolean;
8
+ manifestConfig?: LobeChatPluginManifest;
9
+ manifestMode: 'local' | 'url';
10
+ }