@lobehub/chat 1.81.9 → 1.82.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.
- package/.cursor/rules/desktop-local-tools-implement.mdc +80 -0
- package/.env.desktop +2 -1
- package/.github/scripts/pr-comment.js +4 -9
- package/CHANGELOG.md +50 -0
- package/changelog/v1.json +18 -0
- package/locales/ar/electron.json +38 -2
- package/locales/ar/plugin.json +33 -2
- package/locales/bg-BG/electron.json +38 -2
- package/locales/bg-BG/plugin.json +33 -2
- package/locales/de-DE/electron.json +38 -2
- package/locales/de-DE/plugin.json +28 -2
- package/locales/en-US/electron.json +38 -2
- package/locales/en-US/plugin.json +28 -2
- package/locales/es-ES/electron.json +38 -2
- package/locales/es-ES/plugin.json +33 -2
- package/locales/fa-IR/electron.json +38 -2
- package/locales/fa-IR/plugin.json +33 -2
- package/locales/fr-FR/electron.json +38 -2
- package/locales/fr-FR/plugin.json +33 -2
- package/locales/it-IT/electron.json +38 -2
- package/locales/it-IT/plugin.json +33 -2
- package/locales/ja-JP/electron.json +38 -2
- package/locales/ja-JP/plugin.json +33 -2
- package/locales/ko-KR/electron.json +38 -2
- package/locales/ko-KR/plugin.json +28 -2
- package/locales/nl-NL/electron.json +38 -2
- package/locales/nl-NL/plugin.json +33 -2
- package/locales/pl-PL/electron.json +38 -2
- package/locales/pl-PL/plugin.json +28 -2
- package/locales/pt-BR/electron.json +38 -2
- package/locales/pt-BR/plugin.json +33 -2
- package/locales/ru-RU/electron.json +38 -2
- package/locales/ru-RU/plugin.json +33 -2
- package/locales/tr-TR/electron.json +38 -2
- package/locales/tr-TR/plugin.json +33 -2
- package/locales/vi-VN/electron.json +38 -2
- package/locales/vi-VN/plugin.json +28 -2
- package/locales/zh-CN/electron.json +38 -2
- package/locales/zh-CN/plugin.json +38 -2
- package/locales/zh-TW/electron.json +38 -2
- package/locales/zh-TW/plugin.json +33 -2
- package/package.json +1 -1
- package/packages/electron-client-ipc/src/events/update.ts +3 -3
- package/src/app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/Connection/Mode.tsx +222 -0
- package/src/app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/Connection/Option.tsx +104 -0
- package/src/app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/Connection/Sync.tsx +42 -0
- package/src/app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/Connection/Waiting.tsx +203 -0
- package/src/app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/Connection/index.tsx +57 -0
- package/src/app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/UpdateModal.tsx +242 -0
- package/src/app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/UpdateNotification.tsx +193 -0
- package/src/app/[variants]/(main)/_layout/Desktop/{Titlebar.tsx → ElectronTitlebar/index.tsx} +15 -1
- package/src/app/[variants]/(main)/_layout/Desktop/SideBar/BottomActions.tsx +3 -2
- package/src/app/[variants]/(main)/_layout/Desktop/index.tsx +1 -1
- package/src/app/[variants]/layout.tsx +2 -1
- package/src/components/ManifestPreviewer/index.tsx +4 -1
- package/src/features/ChatInput/ActionBar/Tools/Dropdown.tsx +2 -1
- package/src/features/Conversation/Extras/Usage/index.tsx +7 -1
- package/src/features/Conversation/Messages/Assistant/Tool/Render/CustomRender.tsx +1 -1
- package/src/features/Conversation/components/MarkdownElements/LocalFile/Render/LocalFile.tsx +65 -0
- package/src/features/Conversation/components/MarkdownElements/LocalFile/Render/index.tsx +29 -0
- package/src/features/Conversation/components/MarkdownElements/LocalFile/index.ts +16 -0
- package/src/features/Conversation/components/MarkdownElements/index.ts +7 -1
- package/src/features/Conversation/components/MarkdownElements/remarkPlugins/__snapshots__/createRemarkSelfClosingTagPlugin.test.ts.snap +260 -0
- package/src/features/Conversation/components/MarkdownElements/remarkPlugins/createRemarkSelfClosingTagPlugin.test.ts +204 -0
- package/src/features/Conversation/components/MarkdownElements/remarkPlugins/createRemarkSelfClosingTagPlugin.ts +133 -0
- package/src/features/Conversation/components/MarkdownElements/type.ts +5 -1
- package/src/features/PluginAvatar/index.tsx +2 -1
- package/src/features/PluginDevModal/MCPManifestForm/ArgsInput.tsx +20 -0
- package/src/features/PluginDevModal/MCPManifestForm/MCPTypeSelect.tsx +176 -0
- package/src/features/PluginDevModal/MCPManifestForm/index.tsx +226 -0
- package/src/features/PluginDevModal/PluginPreview.tsx +4 -3
- package/src/features/PluginDevModal/index.tsx +43 -34
- package/src/features/PluginStore/AddPluginButton.tsx +3 -1
- package/src/features/PluginStore/PluginItem/Action.tsx +5 -2
- package/src/features/PluginStore/PluginItem/PluginAvatar.tsx +25 -0
- package/src/features/PluginStore/PluginItem/index.tsx +4 -3
- package/src/features/PluginTag/index.tsx +8 -2
- package/src/{server/modules/MCPClient → libs/mcp}/__tests__/__snapshots__/index.test.ts.snap +0 -56
- package/src/{server/modules/MCPClient → libs/mcp}/__tests__/index.test.ts +2 -2
- package/src/{server/modules/MCPClient/index.ts → libs/mcp/client.ts} +29 -33
- package/src/libs/mcp/index.ts +2 -0
- package/src/libs/mcp/types.ts +27 -0
- package/src/locales/default/electron.ts +38 -2
- package/src/locales/default/plugin.ts +41 -3
- package/src/server/modules/ElectronIPCClient/index.ts +36 -0
- package/src/server/routers/lambda/session.ts +2 -6
- package/src/server/routers/tools/index.ts +2 -0
- package/src/server/routers/tools/mcp.ts +85 -0
- package/src/server/services/file/impls/index.ts +9 -1
- package/src/server/services/file/impls/local.test.ts +299 -0
- package/src/server/services/file/impls/local.ts +183 -0
- package/src/server/services/mcp/index.ts +176 -0
- package/src/services/aiModel/index.ts +5 -1
- package/src/services/aiProvider/index.ts +5 -1
- package/src/services/electron/autoUpdate.ts +4 -0
- package/src/services/file/index.ts +5 -1
- package/src/services/mcp.ts +36 -0
- package/src/services/message/index.ts +5 -1
- package/src/services/plugin/index.ts +5 -1
- package/src/services/session/index.ts +5 -1
- package/src/services/tableViewer/desktop.ts +15 -0
- package/src/services/tableViewer/index.ts +4 -1
- package/src/services/thread/index.ts +5 -1
- package/src/services/topic/index.ts +5 -1
- package/src/services/user/index.ts +5 -1
- package/src/store/chat/slices/plugin/action.ts +46 -2
- package/src/store/electron/actions/app.ts +59 -0
- package/src/store/electron/actions/sync.ts +5 -1
- package/src/store/electron/initialState.ts +3 -1
- package/src/store/electron/store.ts +6 -1
- package/src/store/tool/slices/customPlugin/action.ts +16 -4
- package/src/types/tool/plugin.ts +9 -0
- package/src/utils/client/GlobalAgentContextManager.ts +85 -0
- package/src/utils/promptTemplate.test.ts +78 -0
- package/src/utils/promptTemplate.ts +17 -0
@@ -0,0 +1,226 @@
|
|
1
|
+
import {
|
2
|
+
SiBun,
|
3
|
+
SiDocker,
|
4
|
+
SiNodedotjs,
|
5
|
+
SiNpm,
|
6
|
+
SiPnpm,
|
7
|
+
SiPython,
|
8
|
+
} from '@icons-pack/react-simple-icons';
|
9
|
+
import { LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
|
10
|
+
import { ActionIcon, FormItem } from '@lobehub/ui';
|
11
|
+
import { AutoComplete, Form, FormInstance, Input } from 'antd';
|
12
|
+
import { FileCode, RotateCwIcon } from 'lucide-react';
|
13
|
+
import { FC, useState } from 'react';
|
14
|
+
import { useTranslation } from 'react-i18next';
|
15
|
+
import { Flexbox } from 'react-layout-kit';
|
16
|
+
|
17
|
+
import ManifestPreviewer from '@/components/ManifestPreviewer';
|
18
|
+
import { mcpService } from '@/services/mcp';
|
19
|
+
import { useToolStore } from '@/store/tool';
|
20
|
+
import { pluginSelectors } from '@/store/tool/selectors';
|
21
|
+
import { PluginInstallError } from '@/types/tool/plugin';
|
22
|
+
|
23
|
+
import ArgsInput from './ArgsInput';
|
24
|
+
import MCPTypeSelect from './MCPTypeSelect';
|
25
|
+
|
26
|
+
interface MCPManifestFormProps {
|
27
|
+
form: FormInstance;
|
28
|
+
isEditMode?: boolean;
|
29
|
+
}
|
30
|
+
|
31
|
+
// 定义预设的命令选项
|
32
|
+
const STDIO_COMMAND_OPTIONS: {
|
33
|
+
// 假设图标是 React 函数组件
|
34
|
+
color?: string;
|
35
|
+
icon?: FC<{ color?: string; size?: number }>;
|
36
|
+
value: string;
|
37
|
+
}[] = [
|
38
|
+
{ color: '#CB3837', icon: SiNpm, value: 'npx' },
|
39
|
+
{ color: '#CB3837', icon: SiNpm, value: 'npm' },
|
40
|
+
{ color: '#F69220', icon: SiPnpm, value: 'pnpm' },
|
41
|
+
{ color: '#F69220', icon: SiPnpm, value: 'pnpx' },
|
42
|
+
{ color: '#339933', icon: SiNodedotjs, value: 'node' },
|
43
|
+
{ color: '#efe2d2', icon: SiBun, value: 'bun' },
|
44
|
+
{ color: '#efe2d2', icon: SiBun, value: 'bunx' },
|
45
|
+
{ color: '#DE5FE9', icon: SiPython, value: 'uv' },
|
46
|
+
{ color: '#3776AB', icon: SiPython, value: 'python' },
|
47
|
+
{ color: '#2496ED', icon: SiDocker, value: 'docker' },
|
48
|
+
];
|
49
|
+
|
50
|
+
const MCPManifestForm = ({ form, isEditMode }: MCPManifestFormProps) => {
|
51
|
+
const { t } = useTranslation('plugin');
|
52
|
+
const mcpType = Form.useWatch(['customParams', 'mcp', 'type'], form);
|
53
|
+
const [manifest, setManifest] = useState<LobeChatPluginManifest>();
|
54
|
+
const pluginIds = useToolStore(pluginSelectors.storeAndInstallPluginsIdList);
|
55
|
+
|
56
|
+
const HTTP_URL_KEY = ['customParams', 'mcp', 'url'];
|
57
|
+
const STDIO_COMMAND = ['customParams', 'mcp', 'command'];
|
58
|
+
const STDIO_ARGS = ['customParams', 'mcp', 'args'];
|
59
|
+
return (
|
60
|
+
<Form form={form} layout={'vertical'}>
|
61
|
+
<Flexbox>
|
62
|
+
<Form.Item
|
63
|
+
label={t('dev.mcp.type.title')}
|
64
|
+
name={['customParams', 'mcp', 'type']}
|
65
|
+
rules={[{ required: true }]}
|
66
|
+
>
|
67
|
+
<MCPTypeSelect />
|
68
|
+
</Form.Item>
|
69
|
+
<Form.Item
|
70
|
+
extra={t('dev.mcp.identifier.desc')}
|
71
|
+
label={t('dev.mcp.identifier.label')}
|
72
|
+
name={'identifier'}
|
73
|
+
rules={[
|
74
|
+
{ required: true },
|
75
|
+
{
|
76
|
+
message: t('dev.mcp.identifier.invalid'),
|
77
|
+
pattern: /^[\w-]+$/,
|
78
|
+
},
|
79
|
+
isEditMode
|
80
|
+
? {}
|
81
|
+
: {
|
82
|
+
message: t('dev.meta.identifier.errorDuplicate'),
|
83
|
+
validator: async () => {
|
84
|
+
const id = form.getFieldValue('identifier');
|
85
|
+
if (!id) return true;
|
86
|
+
if (pluginIds.includes(id)) {
|
87
|
+
throw new Error('Duplicate');
|
88
|
+
}
|
89
|
+
},
|
90
|
+
},
|
91
|
+
]}
|
92
|
+
>
|
93
|
+
<Input placeholder={t('dev.mcp.identifier.placeholder')} />
|
94
|
+
</Form.Item>
|
95
|
+
|
96
|
+
{mcpType === 'http' && (
|
97
|
+
<Form.Item
|
98
|
+
extra={
|
99
|
+
<Flexbox horizontal justify={'space-between'} style={{ marginTop: 8 }}>
|
100
|
+
{t('dev.mcp.url.desc')}
|
101
|
+
{manifest && (
|
102
|
+
<ManifestPreviewer manifest={manifest}>
|
103
|
+
<ActionIcon
|
104
|
+
icon={FileCode}
|
105
|
+
size={'small'}
|
106
|
+
title={t('dev.meta.manifest.preview')}
|
107
|
+
/>
|
108
|
+
</ManifestPreviewer>
|
109
|
+
)}
|
110
|
+
</Flexbox>
|
111
|
+
}
|
112
|
+
hasFeedback
|
113
|
+
label={t('dev.mcp.url.label')}
|
114
|
+
name={HTTP_URL_KEY}
|
115
|
+
rules={[
|
116
|
+
{ required: true },
|
117
|
+
{ type: 'url' },
|
118
|
+
{
|
119
|
+
validator: async (_, value) => {
|
120
|
+
if (!value) return true;
|
121
|
+
try {
|
122
|
+
const data = await mcpService.getStreamableMcpServerManifest(
|
123
|
+
form.getFieldValue('identifier'),
|
124
|
+
value,
|
125
|
+
);
|
126
|
+
setManifest(data);
|
127
|
+
form.setFieldsValue({ identifier: data.identifier, manifest: data });
|
128
|
+
} catch (error) {
|
129
|
+
const err = error as PluginInstallError;
|
130
|
+
throw t(`error.${err.message}`, { error: err.cause! });
|
131
|
+
}
|
132
|
+
},
|
133
|
+
},
|
134
|
+
]}
|
135
|
+
>
|
136
|
+
<Input
|
137
|
+
placeholder="https://mcp.higress.ai/mcp-github/xxxxx"
|
138
|
+
suffix={
|
139
|
+
<ActionIcon
|
140
|
+
icon={RotateCwIcon}
|
141
|
+
onClick={(e) => {
|
142
|
+
e.stopPropagation();
|
143
|
+
form.validateFields([HTTP_URL_KEY]);
|
144
|
+
}}
|
145
|
+
size={'small'}
|
146
|
+
title={t('dev.meta.manifest.refresh')}
|
147
|
+
/>
|
148
|
+
}
|
149
|
+
/>
|
150
|
+
</Form.Item>
|
151
|
+
)}
|
152
|
+
|
153
|
+
{mcpType === 'stdio' && (
|
154
|
+
<>
|
155
|
+
<Form.Item
|
156
|
+
extra={t('dev.mcp.command.desc')}
|
157
|
+
label={t('dev.mcp.command.label')}
|
158
|
+
name={STDIO_COMMAND}
|
159
|
+
rules={[{ required: true }]}
|
160
|
+
>
|
161
|
+
<AutoComplete
|
162
|
+
options={STDIO_COMMAND_OPTIONS.map(({ value, icon: Icon, color }) => ({
|
163
|
+
label: (
|
164
|
+
<Flexbox align={'center'} gap={8} horizontal>
|
165
|
+
{Icon && <Icon color={color} size={16} />}
|
166
|
+
{value}
|
167
|
+
</Flexbox>
|
168
|
+
),
|
169
|
+
value: value,
|
170
|
+
}))}
|
171
|
+
placeholder={t('dev.mcp.command.placeholder')}
|
172
|
+
/>
|
173
|
+
</Form.Item>
|
174
|
+
<Form.Item
|
175
|
+
extra={t('dev.mcp.args.desc')}
|
176
|
+
hasFeedback
|
177
|
+
label={t('dev.mcp.args.label')}
|
178
|
+
name={STDIO_ARGS}
|
179
|
+
rules={[
|
180
|
+
{ required: true },
|
181
|
+
{
|
182
|
+
validator: async (_, value) => {
|
183
|
+
if (!value) return true;
|
184
|
+
const name = form.getFieldValue('identifier');
|
185
|
+
|
186
|
+
if (!name) throw new Error('Please input mcp server name');
|
187
|
+
try {
|
188
|
+
const data = await mcpService.getStdioMcpServerManifest(
|
189
|
+
name,
|
190
|
+
form.getFieldValue(STDIO_COMMAND),
|
191
|
+
value,
|
192
|
+
);
|
193
|
+
setManifest(data);
|
194
|
+
form.setFieldsValue({ identifier: data.identifier, manifest: data });
|
195
|
+
} catch (error) {
|
196
|
+
const err = error as PluginInstallError;
|
197
|
+
throw t(`error.${err.message}`, { error: err.cause! });
|
198
|
+
}
|
199
|
+
},
|
200
|
+
},
|
201
|
+
]}
|
202
|
+
>
|
203
|
+
<ArgsInput
|
204
|
+
placeholder={t('dev.mcp.args.placeholder')}
|
205
|
+
suffix={
|
206
|
+
<ActionIcon
|
207
|
+
icon={RotateCwIcon}
|
208
|
+
onClick={(e) => {
|
209
|
+
e.stopPropagation();
|
210
|
+
form.validateFields([STDIO_ARGS]);
|
211
|
+
}}
|
212
|
+
size={'small'}
|
213
|
+
title={t('dev.meta.manifest.refresh')}
|
214
|
+
/>
|
215
|
+
}
|
216
|
+
/>
|
217
|
+
</Form.Item>
|
218
|
+
</>
|
219
|
+
)}
|
220
|
+
<FormItem name={'manifest'} noStyle />
|
221
|
+
</Flexbox>
|
222
|
+
</Form>
|
223
|
+
);
|
224
|
+
};
|
225
|
+
|
226
|
+
export default MCPManifestForm;
|
@@ -1,9 +1,10 @@
|
|
1
|
-
import {
|
1
|
+
import { Form } from '@lobehub/ui';
|
2
2
|
import { Form as AForm, Card, FormInstance } 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 PluginAvatar from '@/features/PluginStore/PluginItem/PluginAvatar';
|
7
8
|
import PluginTag from '@/features/PluginStore/PluginItem/PluginTag';
|
8
9
|
import { pluginHelpers } from '@/store/tool';
|
9
10
|
import { LobeToolCustomPlugin } from '@/types/tool/plugin';
|
@@ -15,7 +16,7 @@ const PluginPreview = memo<{ form: FormInstance }>(({ form }) => {
|
|
15
16
|
const meta = plugin?.manifest?.meta;
|
16
17
|
|
17
18
|
const items = {
|
18
|
-
avatar: <
|
19
|
+
avatar: <PluginAvatar avatar={pluginHelpers.getPluginAvatar(meta)} />,
|
19
20
|
desc: pluginHelpers.getPluginDesc(meta) || 'Plugin Description',
|
20
21
|
label: (
|
21
22
|
<Flexbox align={'center'} gap={8} horizontal>
|
@@ -27,7 +28,7 @@ const PluginPreview = memo<{ form: FormInstance }>(({ form }) => {
|
|
27
28
|
};
|
28
29
|
|
29
30
|
return (
|
30
|
-
<Card
|
31
|
+
<Card size={'small'} styles={{ body: { padding: '0 16px' } }} title={t('dev.preview.card')}>
|
31
32
|
<Form.Item {...items} colon={false} style={{ alignItems: 'center', marginBottom: 0 }} />
|
32
33
|
</Card>
|
33
34
|
);
|
@@ -1,5 +1,5 @@
|
|
1
|
-
import { Alert, Icon, Modal
|
2
|
-
import { App, Button, Form, Popconfirm, Segmented } from 'antd';
|
1
|
+
import { Alert, Icon, Modal } from '@lobehub/ui';
|
2
|
+
import { App, Button, Form, Popconfirm, Segmented, Tag } from 'antd';
|
3
3
|
import { useResponsive } from 'antd-style';
|
4
4
|
import { MoveUpRight } from 'lucide-react';
|
5
5
|
import { memo, useEffect, useState } from 'react';
|
@@ -9,6 +9,7 @@ import { Flexbox } from 'react-layout-kit';
|
|
9
9
|
import { WIKI_PLUGIN_GUIDE } from '@/const/url';
|
10
10
|
import { LobeToolCustomPlugin } from '@/types/tool/plugin';
|
11
11
|
|
12
|
+
import MCPManifestForm from './MCPManifestForm';
|
12
13
|
import PluginPreview from './PluginPreview';
|
13
14
|
import UrlManifestForm from './UrlManifestForm';
|
14
15
|
|
@@ -25,7 +26,7 @@ interface DevModalProps {
|
|
25
26
|
const DevModal = memo<DevModalProps>(
|
26
27
|
({ open, mode = 'create', value, onValueChange, onSave, onOpenChange, onDelete }) => {
|
27
28
|
const isEditMode = mode === 'edit';
|
28
|
-
const [configMode, setConfigMode] = useState<'url' | '
|
29
|
+
const [configMode, setConfigMode] = useState<'url' | 'mcp'>('mcp');
|
29
30
|
const { t } = useTranslation('plugin');
|
30
31
|
const { message } = App.useApp();
|
31
32
|
const [submitting, setSubmitting] = useState(false);
|
@@ -118,49 +119,57 @@ const DevModal = memo<DevModalProps>(
|
|
118
119
|
e.stopPropagation();
|
119
120
|
}}
|
120
121
|
>
|
121
|
-
<Alert
|
122
|
-
message={
|
123
|
-
<Trans i18nKey={'dev.modalDesc'} ns={'plugin'}>
|
124
|
-
添加自定义插件后,可用于插件开发验证,也可直接在会话中使用。插件开发文档请参考:
|
125
|
-
<a
|
126
|
-
href={WIKI_PLUGIN_GUIDE}
|
127
|
-
rel="noreferrer"
|
128
|
-
style={{ paddingInline: 8 }}
|
129
|
-
target={'_blank'}
|
130
|
-
>
|
131
|
-
文档
|
132
|
-
</a>
|
133
|
-
<Icon icon={MoveUpRight} />
|
134
|
-
</Trans>
|
135
|
-
}
|
136
|
-
showIcon
|
137
|
-
type={'info'}
|
138
|
-
/>
|
139
122
|
<Segmented
|
140
123
|
block
|
141
124
|
onChange={(e) => {
|
142
|
-
setConfigMode(e as
|
125
|
+
setConfigMode(e as 'url' | 'mcp');
|
143
126
|
}}
|
144
127
|
options={[
|
145
128
|
{
|
146
|
-
label: t('dev.manifest.mode.url'),
|
147
|
-
value: 'url',
|
148
|
-
},
|
149
|
-
{
|
150
|
-
disabled: true,
|
151
129
|
label: (
|
152
|
-
<
|
153
|
-
{t('dev.manifest.mode.
|
154
|
-
|
130
|
+
<Flexbox align={'center'} gap={4} horizontal justify={'center'}>
|
131
|
+
{t('dev.manifest.mode.mcp')}
|
132
|
+
<div>
|
133
|
+
<Tag bordered={false} color={'warning'}>
|
134
|
+
{t('dev.manifest.mode.mcpExp')}
|
135
|
+
</Tag>
|
136
|
+
</div>
|
137
|
+
</Flexbox>
|
155
138
|
),
|
156
|
-
value: '
|
139
|
+
value: 'mcp',
|
140
|
+
},
|
141
|
+
{
|
142
|
+
label: t('dev.manifest.mode.url'),
|
143
|
+
value: 'url',
|
157
144
|
},
|
158
145
|
]}
|
146
|
+
value={configMode}
|
159
147
|
/>
|
160
148
|
|
161
|
-
{configMode === 'url'
|
162
|
-
|
163
|
-
|
149
|
+
{configMode === 'url' && (
|
150
|
+
<>
|
151
|
+
<Alert
|
152
|
+
message={
|
153
|
+
<Trans i18nKey={'dev.modalDesc'} ns={'plugin'}>
|
154
|
+
添加自定义插件后,可用于插件开发验证,也可直接在会话中使用。插件开发文档请参考:
|
155
|
+
<a
|
156
|
+
href={WIKI_PLUGIN_GUIDE}
|
157
|
+
rel="noreferrer"
|
158
|
+
style={{ paddingInline: 8 }}
|
159
|
+
target={'_blank'}
|
160
|
+
>
|
161
|
+
文档
|
162
|
+
</a>
|
163
|
+
<Icon icon={MoveUpRight} />
|
164
|
+
</Trans>
|
165
|
+
}
|
166
|
+
showIcon
|
167
|
+
type={'info'}
|
168
|
+
/>
|
169
|
+
<UrlManifestForm form={form} isEditMode={mode === 'edit'} />
|
170
|
+
</>
|
171
|
+
)}
|
172
|
+
{configMode === 'mcp' && <MCPManifestForm form={form} />}
|
164
173
|
<PluginPreview form={form} />
|
165
174
|
</Flexbox>
|
166
175
|
</Modal>
|
@@ -5,6 +5,7 @@ import { forwardRef, useState } from 'react';
|
|
5
5
|
import { useTranslation } from 'react-i18next';
|
6
6
|
|
7
7
|
import DevModal from '@/features/PluginDevModal';
|
8
|
+
import { useAgentStore } from '@/store/agent';
|
8
9
|
import { useToolStore } from '@/store/tool';
|
9
10
|
|
10
11
|
const AddPluginButton = forwardRef<HTMLButtonElement>((props, ref) => {
|
@@ -15,6 +16,7 @@ const AddPluginButton = forwardRef<HTMLButtonElement>((props, ref) => {
|
|
15
16
|
s.installCustomPlugin,
|
16
17
|
s.updateNewCustomPlugin,
|
17
18
|
]);
|
19
|
+
const togglePlugin = useAgentStore((s) => s.togglePlugin);
|
18
20
|
|
19
21
|
return (
|
20
22
|
<div
|
@@ -26,7 +28,7 @@ const AddPluginButton = forwardRef<HTMLButtonElement>((props, ref) => {
|
|
26
28
|
onOpenChange={setModal}
|
27
29
|
onSave={async (devPlugin) => {
|
28
30
|
await installCustomPlugin(devPlugin);
|
29
|
-
|
31
|
+
await togglePlugin(devPlugin.identifier);
|
30
32
|
}}
|
31
33
|
onValueChange={updateNewDevPlugin}
|
32
34
|
open={showModal}
|
@@ -6,6 +6,7 @@ import { useTranslation } from 'react-i18next';
|
|
6
6
|
import { Flexbox } from 'react-layout-kit';
|
7
7
|
|
8
8
|
import PluginDetailModal from '@/features/PluginDetailModal';
|
9
|
+
import { useAgentStore } from '@/store/agent';
|
9
10
|
import { useServerConfigStore } from '@/store/serverConfig';
|
10
11
|
import { pluginHelpers, useToolStore } from '@/store/tool';
|
11
12
|
import { pluginSelectors, pluginStoreSelectors } from '@/store/tool/selectors';
|
@@ -31,6 +32,7 @@ const Actions = memo<ActionsProps>(({ identifier, type }) => {
|
|
31
32
|
const { t } = useTranslation('plugin');
|
32
33
|
const [open, setOpen] = useState(false);
|
33
34
|
const plugin = useToolStore(pluginSelectors.getToolManifestById(identifier));
|
35
|
+
const togglePlugin = useAgentStore((s) => s.togglePlugin);
|
34
36
|
const { modal } = App.useApp();
|
35
37
|
const [tab, setTab] = useState('info');
|
36
38
|
const hasSettings = pluginHelpers.isSettingSchemaNonEmpty(plugin?.settings);
|
@@ -89,8 +91,9 @@ const Actions = memo<ActionsProps>(({ identifier, type }) => {
|
|
89
91
|
) : (
|
90
92
|
<Button
|
91
93
|
loading={installing}
|
92
|
-
onClick={() => {
|
93
|
-
installPlugin(identifier);
|
94
|
+
onClick={async () => {
|
95
|
+
await installPlugin(identifier);
|
96
|
+
await togglePlugin(identifier);
|
94
97
|
}}
|
95
98
|
size={mobile ? 'small' : undefined}
|
96
99
|
type={'primary'}
|
@@ -0,0 +1,25 @@
|
|
1
|
+
import { MCP } from '@lobehub/icons';
|
2
|
+
import { Avatar } from '@lobehub/ui';
|
3
|
+
import { CSSProperties, memo } from 'react';
|
4
|
+
|
5
|
+
interface PluginAvatarProps {
|
6
|
+
alt?: string;
|
7
|
+
avatar?: string;
|
8
|
+
size?: number;
|
9
|
+
style?: CSSProperties;
|
10
|
+
}
|
11
|
+
|
12
|
+
const PluginAvatar = memo<PluginAvatarProps>(({ avatar, style, size, alt }) => {
|
13
|
+
return avatar === 'MCP_AVATAR' ? (
|
14
|
+
<MCP.Avatar size={size ? size * 0.8 : 36} />
|
15
|
+
) : (
|
16
|
+
<Avatar
|
17
|
+
alt={alt}
|
18
|
+
avatar={avatar}
|
19
|
+
size={size}
|
20
|
+
style={{ flex: 'none', overflow: 'hidden', ...style }}
|
21
|
+
/>
|
22
|
+
);
|
23
|
+
});
|
24
|
+
|
25
|
+
export default PluginAvatar;
|
@@ -1,14 +1,15 @@
|
|
1
|
-
import {
|
1
|
+
import { Tooltip } from '@lobehub/ui';
|
2
2
|
import { Typography } from 'antd';
|
3
3
|
import { createStyles } from 'antd-style';
|
4
4
|
import Link from 'next/link';
|
5
5
|
import { memo } from 'react';
|
6
6
|
import { Flexbox } from 'react-layout-kit';
|
7
7
|
|
8
|
-
import PluginTag from '@/features/PluginStore/PluginItem/PluginTag';
|
9
8
|
import { InstallPluginMeta } from '@/types/tool/plugin';
|
10
9
|
|
11
10
|
import Actions from './Action';
|
11
|
+
import PluginAvatar from './PluginAvatar';
|
12
|
+
import PluginTag from './PluginTag';
|
12
13
|
|
13
14
|
const { Paragraph } = Typography;
|
14
15
|
|
@@ -51,7 +52,7 @@ const PluginItem = memo<InstallPluginMeta>(({ identifier, homepage, author, type
|
|
51
52
|
horizontal
|
52
53
|
style={{ overflow: 'hidden', position: 'relative' }}
|
53
54
|
>
|
54
|
-
<
|
55
|
+
<PluginAvatar avatar={meta.avatar} />
|
55
56
|
<Flexbox flex={1} gap={4} style={{ overflow: 'hidden', position: 'relative' }}>
|
56
57
|
<Flexbox align={'center'} gap={8} horizontal>
|
57
58
|
<Tooltip title={identifier}>
|
@@ -1,12 +1,14 @@
|
|
1
1
|
'use client';
|
2
2
|
|
3
|
-
import {
|
3
|
+
import { Icon, Tag } from '@lobehub/ui';
|
4
4
|
import type { MenuProps } from 'antd';
|
5
5
|
import { Dropdown } from 'antd';
|
6
6
|
import isEqual from 'fast-deep-equal';
|
7
7
|
import { LucideToyBrick } from 'lucide-react';
|
8
8
|
import { memo } from 'react';
|
9
|
+
import { Center } from 'react-layout-kit';
|
9
10
|
|
11
|
+
import Avatar from '@/features/PluginStore/PluginItem/PluginAvatar';
|
10
12
|
import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig';
|
11
13
|
import { pluginHelpers, useToolStore } from '@/store/tool';
|
12
14
|
import { toolSelectors } from '@/store/tool/selectors';
|
@@ -30,7 +32,11 @@ const PluginTag = memo<PluginTagProps>(({ plugins }) => {
|
|
30
32
|
const avatar = isDeprecated ? '♻️' : pluginHelpers.getPluginAvatar(item?.meta);
|
31
33
|
|
32
34
|
return {
|
33
|
-
icon:
|
35
|
+
icon: (
|
36
|
+
<Center style={{ minWidth: 24 }}>
|
37
|
+
<Avatar avatar={avatar} size={24} />
|
38
|
+
</Center>
|
39
|
+
),
|
34
40
|
key: id,
|
35
41
|
label: (
|
36
42
|
<PluginStatus
|
package/src/{server/modules/MCPClient → libs/mcp}/__tests__/__snapshots__/index.test.ts.snap
RENAMED
@@ -1,61 +1,5 @@
|
|
1
1
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
2
2
|
|
3
|
-
exports[`MCPClient > Stdio Transport (using SDK Mock Server) > should list tools via stdio 1`] = `
|
4
|
-
[
|
5
|
-
{
|
6
|
-
"description": "Echoes back a message with 'Hello' prefix",
|
7
|
-
"inputSchema": {
|
8
|
-
"$schema": "http://json-schema.org/draft-07/schema#",
|
9
|
-
"additionalProperties": false,
|
10
|
-
"properties": {
|
11
|
-
"message": {
|
12
|
-
"description": "The message to echo",
|
13
|
-
"type": "string",
|
14
|
-
},
|
15
|
-
},
|
16
|
-
"required": [
|
17
|
-
"message",
|
18
|
-
],
|
19
|
-
"type": "object",
|
20
|
-
},
|
21
|
-
"name": "echo",
|
22
|
-
},
|
23
|
-
{
|
24
|
-
"description": "Lists all available tools and methods",
|
25
|
-
"inputSchema": {
|
26
|
-
"$schema": "http://json-schema.org/draft-07/schema#",
|
27
|
-
"additionalProperties": false,
|
28
|
-
"properties": {},
|
29
|
-
"type": "object",
|
30
|
-
},
|
31
|
-
"name": "debug",
|
32
|
-
},
|
33
|
-
{
|
34
|
-
"description": "Adds two numbers",
|
35
|
-
"inputSchema": {
|
36
|
-
"$schema": "http://json-schema.org/draft-07/schema#",
|
37
|
-
"additionalProperties": false,
|
38
|
-
"properties": {
|
39
|
-
"a": {
|
40
|
-
"description": "The first number",
|
41
|
-
"type": "number",
|
42
|
-
},
|
43
|
-
"b": {
|
44
|
-
"description": "The second number",
|
45
|
-
"type": "number",
|
46
|
-
},
|
47
|
-
},
|
48
|
-
"required": [
|
49
|
-
"a",
|
50
|
-
"b",
|
51
|
-
],
|
52
|
-
"type": "object",
|
53
|
-
},
|
54
|
-
"name": "add",
|
55
|
-
},
|
56
|
-
]
|
57
|
-
`;
|
58
|
-
|
59
3
|
exports[`MCPClient > Stdio Transport > should list tools via stdio 1`] = `
|
60
4
|
[
|
61
5
|
{
|
@@ -36,10 +36,10 @@ describe('MCPClient', () => {
|
|
36
36
|
const result = await mcpClient.listTools();
|
37
37
|
|
38
38
|
// Check exact length if no other tools are expected
|
39
|
-
expect(result
|
39
|
+
expect(result).toHaveLength(3);
|
40
40
|
|
41
41
|
// Expect the tools defined in mock-sdk-server.ts
|
42
|
-
expect(result
|
42
|
+
expect(result).toMatchSnapshot();
|
43
43
|
});
|
44
44
|
|
45
45
|
it('should call the "echo" tool via stdio', async () => {
|
@@ -4,54 +4,34 @@ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/
|
|
4
4
|
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.d.ts';
|
5
5
|
import debug from 'debug';
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
interface MCPConnectionBase {
|
10
|
-
id: string;
|
11
|
-
name: string;
|
12
|
-
type: 'http' | 'stdio';
|
13
|
-
}
|
7
|
+
import { MCPClientParams, McpTool } from './types';
|
14
8
|
|
15
|
-
|
16
|
-
type: 'http';
|
17
|
-
url: string;
|
18
|
-
}
|
19
|
-
|
20
|
-
interface StdioMCPConnection extends MCPConnectionBase {
|
21
|
-
args: string[];
|
22
|
-
command: string;
|
23
|
-
type: 'stdio';
|
24
|
-
}
|
25
|
-
type MCPConnection = HttpMCPConnection | StdioMCPConnection;
|
9
|
+
const log = debug('lobe-mcp:client');
|
26
10
|
|
27
11
|
export class MCPClient {
|
28
12
|
private mcp: Client;
|
29
13
|
private transport: Transport;
|
30
14
|
|
31
|
-
constructor(
|
32
|
-
log('Creating MCPClient with connection: %O',
|
15
|
+
constructor(params: MCPClientParams) {
|
16
|
+
log('Creating MCPClient with connection: %O', params);
|
33
17
|
this.mcp = new Client({ name: 'lobehub-mcp-client', version: '1.0.0' });
|
34
18
|
|
35
|
-
switch (
|
19
|
+
switch (params.type) {
|
36
20
|
case 'http': {
|
37
|
-
log('Using HTTP transport with url: %s',
|
38
|
-
this.transport = new StreamableHTTPClientTransport(new URL(
|
21
|
+
log('Using HTTP transport with url: %s', params.url);
|
22
|
+
this.transport = new StreamableHTTPClientTransport(new URL(params.url));
|
39
23
|
break;
|
40
24
|
}
|
41
25
|
case 'stdio': {
|
42
|
-
log(
|
43
|
-
'Using Stdio transport with command: %s and args: %O',
|
44
|
-
connection.command,
|
45
|
-
connection.args,
|
46
|
-
);
|
26
|
+
log('Using Stdio transport with command: %s and args: %O', params.command, params.args);
|
47
27
|
this.transport = new StdioClientTransport({
|
48
|
-
args:
|
49
|
-
command:
|
28
|
+
args: params.args,
|
29
|
+
command: params.command,
|
50
30
|
});
|
51
31
|
break;
|
52
32
|
}
|
53
33
|
default: {
|
54
|
-
const err = new Error(`Unsupported MCP connection type: ${(
|
34
|
+
const err = new Error(`Unsupported MCP connection type: ${(params as any).type}`);
|
55
35
|
log('Error creating client: %O', err);
|
56
36
|
throw err;
|
57
37
|
}
|
@@ -64,11 +44,27 @@ export class MCPClient {
|
|
64
44
|
log('MCP connection initialized.');
|
65
45
|
}
|
66
46
|
|
47
|
+
async disconnect() {
|
48
|
+
log('Disconnecting MCP connection...');
|
49
|
+
// Assuming the mcp client has a disconnect method
|
50
|
+
if (this.mcp && typeof (this.mcp as any).disconnect === 'function') {
|
51
|
+
await (this.mcp as any).disconnect();
|
52
|
+
log('MCP connection disconnected.');
|
53
|
+
} else {
|
54
|
+
log('MCP client does not have a disconnect method or is not initialized.');
|
55
|
+
// Depending on the transport, we might need specific cleanup
|
56
|
+
if (this.transport && typeof (this.transport as any).close === 'function') {
|
57
|
+
(this.transport as any).close();
|
58
|
+
log('Transport closed.');
|
59
|
+
}
|
60
|
+
}
|
61
|
+
}
|
62
|
+
|
67
63
|
async listTools() {
|
68
64
|
log('Listing tools...');
|
69
|
-
const tools = await this.mcp.listTools();
|
65
|
+
const { tools } = await this.mcp.listTools();
|
70
66
|
log('Listed tools: %O', tools);
|
71
|
-
return tools;
|
67
|
+
return tools as McpTool[];
|
72
68
|
}
|
73
69
|
|
74
70
|
async callTool(toolName: string, args: any) {
|