@lobehub/chat 0.63.2 → 0.64.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.
@@ -0,0 +1,96 @@
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';
5
+ import { useTranslation } from 'react-i18next';
6
+
7
+ const ManifestForm = memo<{ form: FormInstance; mode: 'url' | 'local' }>(({ form, mode }) => {
8
+ const { t } = useTranslation('plugin');
9
+
10
+ const isUrl = mode === 'url';
11
+
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;
32
+
33
+ const json = await res.json();
34
+ pluginManifestSchema.parse(json);
35
+ },
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
+ },
49
+
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
+ ];
65
+
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
+ });
95
+
96
+ export default ManifestForm;
@@ -0,0 +1,90 @@
1
+ import { Form, FormItemProps, Input } from '@lobehub/ui';
2
+ import { FormInstance } from 'antd';
3
+ import { memo, useEffect } from 'react';
4
+ import { useTranslation } from 'react-i18next';
5
+
6
+ import EmojiPicker from '@/components/EmojiPicker';
7
+ import { pluginSelectors, usePluginStore } from '@/store/plugin';
8
+
9
+ const MetaForm = memo<{ form: FormInstance }>(({ form }) => {
10
+ const { t } = useTranslation('plugin');
11
+ const [plugins] = usePluginStore((s) => [pluginSelectors.pluginList(s).map((i) => i.identifier)]);
12
+
13
+ useEffect(() => {
14
+ if (usePluginStore.getState().newDevPlugin) {
15
+ form.setFieldsValue(usePluginStore.getState().newDevPlugin);
16
+ }
17
+ }, []);
18
+
19
+ const configItem: FormItemProps[] = [
20
+ {
21
+ children: <Input placeholder={'searchEngine'} />,
22
+ desc: t('dev.meta.identifier.desc'),
23
+ label: t('dev.meta.identifier.label'),
24
+ name: 'identifier',
25
+ rules: [
26
+ { required: true },
27
+ {
28
+ message: t('dev.meta.identifier.pattenErrorMessage'),
29
+ pattern: /^[\w-]+$/,
30
+ },
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
+ },
39
+ ],
40
+ },
41
+ {
42
+ children: <Input placeholder={t('dev.meta.title.placeholder')} />,
43
+ desc: t('dev.meta.title.desc'),
44
+ label: t('dev.meta.title.label'),
45
+ name: ['meta', 'title'],
46
+ rules: [{ required: true }],
47
+ },
48
+ {
49
+ children: <Input placeholder={t('dev.meta.description.placeholder')} />,
50
+ desc: t('dev.meta.description.desc'),
51
+ label: t('dev.meta.description.label'),
52
+ name: ['meta', 'description'],
53
+ },
54
+ {
55
+ children: <Input placeholder={'LobeHub'} />,
56
+ desc: t('dev.meta.author.desc'),
57
+ label: t('dev.meta.author.label'),
58
+ name: 'author',
59
+ },
60
+ {
61
+ children: <Input placeholder={'https://www.lobehub.com'} />,
62
+ desc: t('dev.meta.homepage.desc'),
63
+ label: t('dev.meta.homepage.label'),
64
+ name: 'homepage',
65
+ },
66
+ {
67
+ children: <EmojiPicker defaultAvatar={'🧩'} />,
68
+ desc: t('dev.meta.avatar.desc'),
69
+ label: t('dev.meta.avatar.label'),
70
+ name: ['meta', 'avatar'],
71
+ },
72
+ ];
73
+
74
+ return (
75
+ <Form
76
+ form={form}
77
+ items={[
78
+ {
79
+ children: configItem,
80
+ title: t('dev.metaConfig'),
81
+ },
82
+ ]}
83
+ validateMessages={{
84
+ required: () => t('dev.meta.formFieldRequired'),
85
+ }}
86
+ />
87
+ );
88
+ });
89
+
90
+ export default MetaForm;
@@ -0,0 +1,36 @@
1
+ import { Avatar, Form } from '@lobehub/ui';
2
+ import { Card, Switch, Tag } from 'antd';
3
+ import { memo } from 'react';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { Flexbox } from 'react-layout-kit';
6
+
7
+ import { pluginHelpers, usePluginStore } from '@/store/plugin';
8
+
9
+ const PluginPreview = memo(() => {
10
+ const { t } = useTranslation('plugin');
11
+ const meta = usePluginStore((s) => s.newDevPlugin);
12
+
13
+ const items = {
14
+ avatar: <Avatar avatar={pluginHelpers.getPluginAvatar(meta?.meta) || '🧩'} />,
15
+ children: <Switch disabled />,
16
+ desc: pluginHelpers.getPluginDesc(meta?.meta),
17
+ label: (
18
+ <Flexbox align={'center'} gap={8} horizontal>
19
+ {pluginHelpers.getPluginTitle(meta?.meta) ?? t('dev.preview.title')}
20
+ <Tag bordered={false} color={'gold'}>
21
+ {t('list.item.local.title', { ns: 'plugin' })}
22
+ </Tag>
23
+ </Flexbox>
24
+ ),
25
+ minWidth: undefined,
26
+ tag: !!meta?.identifier ? meta?.identifier : 'id',
27
+ };
28
+
29
+ return (
30
+ <Card size={'small'} title={t('dev.preview.card')}>
31
+ <Form.Item {...items} colon={false} style={{ marginBottom: 0 }} />
32
+ </Card>
33
+ );
34
+ });
35
+
36
+ export default PluginPreview;
@@ -0,0 +1,153 @@
1
+ import { Icon } from '@lobehub/ui';
2
+ import { App, Button, ConfigProvider, Form, Modal, Popconfirm } from 'antd';
3
+ import { createStyles, useTheme } from 'antd-style';
4
+ import { LucideBlocks } from 'lucide-react';
5
+ import { lighten } from 'polished';
6
+ import { memo, useEffect } from 'react';
7
+ import { useTranslation } from 'react-i18next';
8
+ import { Flexbox } from 'react-layout-kit';
9
+
10
+ import { DevPlugin } from '@/store/plugin/initialState';
11
+
12
+ import ManifestForm from './ManifestForm';
13
+ import MetaForm from './MetaForm';
14
+ import PluginPreview from './PluginPreview';
15
+
16
+ const useStyles = createStyles(({ css, token, prefixCls }) => ({
17
+ content: css`
18
+ .${prefixCls}-modal-content {
19
+ border: 1px solid ${token.colorSplit};
20
+ border-radius: 12px;
21
+ }
22
+ `,
23
+ root: css`
24
+ backdrop-filter: blur(5px);
25
+ `,
26
+ }));
27
+
28
+ interface DevModalProps {
29
+ onDelete?: () => void;
30
+ onOpenChange: (open: boolean) => void;
31
+ onSave?: (value: DevPlugin) => void;
32
+ onValueChange?: (value: Partial<DevPlugin>) => void;
33
+ open?: boolean;
34
+ showDelete?: boolean;
35
+ value?: DevPlugin;
36
+ }
37
+
38
+ const DevModal = memo<DevModalProps>(
39
+ ({ open, showDelete, value, onValueChange, onSave, onOpenChange, onDelete }) => {
40
+ const { t } = useTranslation('plugin');
41
+
42
+ const [form] = Form.useForm();
43
+ const theme = useTheme();
44
+
45
+ const { styles } = useStyles();
46
+
47
+ const { message } = App.useApp();
48
+
49
+ useEffect(() => {
50
+ form.setFieldsValue(value);
51
+ }, []);
52
+
53
+ const footer = (
54
+ <Flexbox horizontal justify={'space-between'} style={{ marginTop: 24 }}>
55
+ {showDelete ? (
56
+ <Popconfirm
57
+ okButtonProps={{
58
+ danger: true,
59
+ type: 'primary',
60
+ }}
61
+ onConfirm={() => {
62
+ onDelete?.();
63
+ message.success(t('dev.deleteSuccess'));
64
+ }}
65
+ placement={'topLeft'}
66
+ title={t('dev.confirmDeleteDevPlugin')}
67
+ >
68
+ <Button danger>{t('delete', { ns: 'common' })}</Button>
69
+ </Popconfirm>
70
+ ) : (
71
+ <div />
72
+ )}
73
+
74
+ <Flexbox horizontal>
75
+ <Button
76
+ onClick={() => {
77
+ onOpenChange(false);
78
+ }}
79
+ >
80
+ {t('cancel', { ns: 'common' })}
81
+ </Button>
82
+ <Button
83
+ onClick={() => {
84
+ form.submit();
85
+ }}
86
+ type={'primary'}
87
+ >
88
+ {t('dev.save')}
89
+ </Button>
90
+ </Flexbox>
91
+ </Flexbox>
92
+ );
93
+ return (
94
+ <Form.Provider
95
+ onFormChange={() => {
96
+ onValueChange?.(form.getFieldsValue());
97
+ }}
98
+ onFormFinish={(_, info) => {
99
+ onSave?.(info.values as DevPlugin);
100
+ message.success(t('dev.saveSuccess'));
101
+ onOpenChange(false);
102
+ }}
103
+ >
104
+ <ConfigProvider
105
+ theme={{
106
+ token: {
107
+ colorBgElevated: lighten(0.005, theme.colorBgContainer),
108
+ },
109
+ }}
110
+ >
111
+ <Modal
112
+ centered
113
+ closable
114
+ footer={footer}
115
+ maskClosable
116
+ okText={t('dev.save')}
117
+ onCancel={() => {
118
+ onOpenChange(false);
119
+ }}
120
+ onOk={() => {
121
+ form.submit();
122
+ }}
123
+ open={open}
124
+ style={{ height: 750 }}
125
+ title={
126
+ <Flexbox gap={8} horizontal>
127
+ <Icon icon={LucideBlocks} />
128
+ {t('dev.title')}
129
+ </Flexbox>
130
+ }
131
+ width={700}
132
+ wrapClassName={styles.root}
133
+ >
134
+ <Flexbox gap={12}>
135
+ {t('dev.modalDesc')}
136
+ {/*<Tabs*/}
137
+ {/* items={[*/}
138
+ {/* { children: <MetaForm />, key: 'meta', label: t('dev.tabs.meta') },*/}
139
+ {/* { children: <ManifestForm />, key: 'manifest', label: t('dev.tabs.manifest') },*/}
140
+ {/* ]}*/}
141
+ {/*/>*/}
142
+ <PluginPreview />
143
+ <ManifestForm form={form} mode={'url'} />
144
+ <MetaForm form={form} />
145
+ </Flexbox>
146
+ </Modal>
147
+ </ConfigProvider>
148
+ </Form.Provider>
149
+ );
150
+ },
151
+ );
152
+
153
+ export default DevModal;
@@ -37,6 +37,8 @@ export default {
37
37
  '很抱歉,该插件请求的入参校验未通过,请检查入参与 Api 描述信息是否匹配',
38
38
  [PluginErrorType.PluginSettingsInvalid]:
39
39
  '该插件需要正确配置后才可以使用,请检查你的配置是否正确',
40
+ [PluginErrorType.PluginServerError]:
41
+ '插件服务端请求返回出错,请检查根据下面的报错信息检查你的插件描述文件、插件配置或服务端实现',
40
42
 
41
43
  [ChatErrorType.InvalidAccessCode]:
42
44
  '密码不正确或为空,请输入正确的访问密码,或者添加自定义 OpenAI API Key',
@@ -4,15 +4,91 @@ export default {
4
4
  function_call: '函数调用',
5
5
  response: '返回结果',
6
6
  },
7
+ dev: {
8
+ confirmDeleteDevPlugin: '即将删除该本地插件,删除后将无法找回,是否删除该插件?',
9
+ deleteSuccess: '插件删除成功',
10
+ manifest: {
11
+ identifier: {
12
+ desc: '插件的唯一标识',
13
+ label: '标识符',
14
+ },
15
+ mode: {
16
+ 'local': '可视化配置',
17
+ 'local-tooltip': '暂时不支持可视化配置',
18
+ 'url': '在线链接',
19
+ },
20
+ name: {
21
+ desc: '插件标题',
22
+ label: '标题',
23
+ placeholder: '搜索引擎',
24
+ },
25
+ },
26
+ meta: {
27
+ author: {
28
+ desc: '插件的作者',
29
+ label: '作者',
30
+ },
31
+ avatar: {
32
+ desc: '插件的图标,可以使用 Emoji,也可以使用 URL',
33
+ label: '图标',
34
+ },
35
+ description: {
36
+ desc: '插件描述',
37
+ label: '描述',
38
+ placeholder: '查询搜索引擎获取信息',
39
+ },
40
+ formFieldRequired: '该字段为必填项',
41
+ homepage: {
42
+ desc: '插件的首页',
43
+ label: '首页',
44
+ },
45
+ identifier: {
46
+ desc: '插件的唯一标识,仅支持英文字符、数字、破折号 - 、下划线 _',
47
+ errorDuplicate: '标识符和已有插件重复,请修改标识符',
48
+ label: '标识符',
49
+ pattenErrorMessage: '只能输入英文字符、数字 、- 和_ 这两个符号',
50
+ },
51
+ manifest: {
52
+ desc: 'LobeChat 将会通过该链接安装插件',
53
+ invalid: '输入的 manifest 链接无效,或 manifest 不符合规范',
54
+ label: '插件描述文件 Url 地址',
55
+ urlError: '请输入一个有效的网址',
56
+ },
57
+ title: {
58
+ desc: '插件标题',
59
+ label: '标题',
60
+ placeholder: '搜索引擎',
61
+ },
62
+ },
63
+ metaConfig: '插件元信息配置',
64
+ modalDesc: '添加自定义插件后,可用于插件开发验证,也可直接在会话中使用。插件开发文档请参考',
65
+ preview: {
66
+ card: '预览插件展示效果',
67
+ desc: '预览插件描述',
68
+ title: '插件名称预览',
69
+ },
70
+ save: '保存',
71
+ saveSuccess: '插件设置保存成功',
72
+ tabs: {
73
+ manifest: '功能描述清单 (Manifest)',
74
+ meta: '插件元信息',
75
+ },
76
+
77
+ title: '添加自定义插件',
78
+ },
79
+ list: {
80
+ item: {
81
+ 'local.config': '配置',
82
+ 'local.title': '本地',
83
+ },
84
+ },
85
+
7
86
  loading: {
8
87
  content: '数据获取中...',
9
88
  plugin: '插件运行中...',
10
89
  },
11
90
  pluginList: '插件列表',
12
91
  plugins: {
13
- realtimeWeather: '实时天气预报',
14
- searchEngine: '搜索引擎',
15
92
  unknown: '插件检测中...',
16
- websiteCrawler: '网页内容提取',
17
93
  },
18
94
  };
@@ -1,3 +1,4 @@
1
+
1
2
  export default {
2
3
  danger: {
3
4
  clear: {
@@ -128,6 +129,8 @@ export default {
128
129
  },
129
130
  },
130
131
  settingPlugin: {
132
+ add: '添加',
133
+ addTooltip: '添加自定义插件',
131
134
  config: '{{id}} 插件配置',
132
135
  title: '插件列表',
133
136
  },
@@ -18,7 +18,8 @@ export const renderErrorMessage: RenderErrorMessage = (error, message: ChatMessa
18
18
  case PluginErrorType.PluginManifestInvalid:
19
19
  case PluginErrorType.PluginManifestNotFound:
20
20
  case PluginErrorType.PluginApiNotFound:
21
- case PluginErrorType.PluginApiParamsError: {
21
+ case PluginErrorType.PluginApiParamsError:
22
+ case PluginErrorType.PluginServerError: {
22
23
  return <PluginError content={(error as any).body} id={message.id} />;
23
24
  }
24
25
  case PluginErrorType.PluginSettingsInvalid: {
@@ -6,7 +6,7 @@ import { memo, useState } from 'react';
6
6
  import { useTranslation } from 'react-i18next';
7
7
  import { Flexbox } from 'react-layout-kit';
8
8
 
9
- import { pluginSelectors, usePluginStore } from '@/store/plugin';
9
+ import { pluginHelpers, pluginSelectors, usePluginStore } from '@/store/plugin';
10
10
 
11
11
  import PluginResult from './PluginResultRender';
12
12
  import { useStyles } from './style';
@@ -27,8 +27,11 @@ const FunctionCall = memo<FunctionCallProps>(
27
27
 
28
28
  const item = usePluginStore(pluginSelectors.getPluginMetaById(id));
29
29
 
30
- const avatar = item?.meta.avatar ? (
31
- <Avatar avatar={item?.meta.avatar} size={32} />
30
+ const pluginAvatar = pluginHelpers.getPluginAvatar(item?.meta);
31
+ const pluginTitle = pluginHelpers.getPluginTitle(item?.meta);
32
+
33
+ const avatar = pluginAvatar ? (
34
+ <Avatar avatar={pluginAvatar} size={32} />
32
35
  ) : (
33
36
  <Icon icon={LucideToyBrick} />
34
37
  );
@@ -54,7 +57,7 @@ const FunctionCall = memo<FunctionCallProps>(
54
57
  ) : (
55
58
  avatar
56
59
  )}
57
- {t(`plugins.${id}` as any, { ns: 'plugin' })}
60
+ {pluginTitle ?? t('plugins.unknown')}
58
61
  <Icon icon={open ? LucideChevronUp : LucideChevronDown} />
59
62
  </Flexbox>
60
63
  {open && (
@@ -12,41 +12,35 @@ export interface FunctionMessageProps extends ChatMessage {
12
12
  loading?: boolean;
13
13
  }
14
14
 
15
- const PluginMessage = memo<FunctionMessageProps>(
16
- ({
17
- content,
18
- // loading,
19
- name,
20
- }) => {
21
- const { t } = useTranslation('plugin');
22
-
23
- const manifest = usePluginStore((s) => s.pluginManifestMap[name || '']);
24
- let isJSON = true;
25
- try {
26
- JSON.parse(content);
27
- } catch {
28
- isJSON = false;
29
- }
30
-
31
- // if (!loading)
32
-
33
- if (!isJSON) {
34
- return (
35
- <Flexbox gap={8} horizontal>
36
- <div>
37
- <Loading3QuartersOutlined spin />
38
- </div>
39
- {t('loading.content')}
40
- </Flexbox>
41
- );
42
- }
43
-
44
- if (!manifest?.ui?.url) return;
15
+ const PluginMessage = memo<FunctionMessageProps>(({ content, name }) => {
16
+ const { t } = useTranslation('plugin');
45
17
 
18
+ const manifest = usePluginStore((s) => s.pluginManifestMap[name || '']);
19
+ let isJSON = true;
20
+ try {
21
+ JSON.parse(content);
22
+ } catch {
23
+ isJSON = false;
24
+ }
25
+
26
+ // if (!loading)
27
+
28
+ if (!isJSON) {
46
29
  return (
47
- <CustomRender content={JSON.parse(content)} name={name || 'unknown'} url={manifest.ui?.url} />
30
+ <Flexbox gap={8} horizontal>
31
+ <div>
32
+ <Loading3QuartersOutlined spin />
33
+ </div>
34
+ {t('loading.content')}
35
+ </Flexbox>
48
36
  );
49
- },
50
- );
37
+ }
38
+
39
+ if (!manifest?.ui?.url) return;
40
+
41
+ return (
42
+ <CustomRender content={JSON.parse(content)} name={name || 'unknown'} url={manifest.ui?.url} />
43
+ );
44
+ });
51
45
 
52
46
  export default PluginMessage;
@@ -1,6 +1,6 @@
1
1
  import { PluginRequestPayload, createHeadersWithPluginSettings } from '@lobehub/chat-plugin-sdk';
2
2
 
3
- import { usePluginStore } from '@/store/plugin';
3
+ import { pluginSelectors, usePluginStore } from '@/store/plugin';
4
4
  import { getMessageError } from '@/utils/fetch';
5
5
 
6
6
  import { URLS } from './url';
@@ -17,10 +17,13 @@ export const fetchPlugin = async (
17
17
  params: PluginRequestPayload,
18
18
  options?: FetchChatModelOptions,
19
19
  ) => {
20
- const settings = usePluginStore.getState().pluginsSettings?.[params.identifier];
20
+ const s = usePluginStore.getState();
21
+
22
+ const settings = pluginSelectors.getPluginSettingsById(params.identifier)(s);
23
+ const manifest = pluginSelectors.getPluginManifestById(params.identifier)(s);
21
24
 
22
25
  const res = await fetch(URLS.plugins, {
23
- body: JSON.stringify(params),
26
+ body: JSON.stringify({ ...params, manifest }),
24
27
  headers: createHeadersWithPluginSettings(settings),
25
28
  method: 'POST',
26
29
  signal: options?.signal,