@lobehub/chat 1.82.1 → 1.82.2
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 +26 -0
- package/changelog/v1.json +9 -0
- package/locales/ar/plugin.json +25 -5
- package/locales/bg-BG/plugin.json +25 -5
- package/locales/de-DE/plugin.json +30 -5
- package/locales/en-US/plugin.json +30 -5
- package/locales/es-ES/plugin.json +25 -5
- package/locales/fa-IR/plugin.json +25 -5
- package/locales/fr-FR/plugin.json +25 -5
- package/locales/it-IT/plugin.json +25 -5
- package/locales/ja-JP/plugin.json +25 -5
- package/locales/ko-KR/plugin.json +30 -5
- package/locales/nl-NL/plugin.json +25 -5
- package/locales/pl-PL/plugin.json +30 -5
- package/locales/pt-BR/plugin.json +25 -5
- package/locales/ru-RU/plugin.json +25 -5
- package/locales/tr-TR/plugin.json +25 -5
- package/locales/vi-VN/plugin.json +30 -5
- package/locales/zh-CN/plugin.json +23 -8
- package/locales/zh-TW/plugin.json +25 -5
- package/package.json +1 -1
- package/src/config/aiModels/openrouter.ts +6 -6
- package/src/features/PluginDevModal/MCPManifestForm/ArgsInput.tsx +1 -1
- package/src/features/PluginDevModal/MCPManifestForm/index.tsx +160 -97
- package/src/features/PluginDevModal/MCPManifestForm/utils.test.ts +262 -0
- package/src/features/PluginDevModal/MCPManifestForm/utils.ts +151 -0
- package/src/features/PluginDevModal/index.tsx +31 -22
- package/src/libs/agent-runtime/utils/openaiCompatibleFactory/index.ts +1 -1
- package/src/locales/default/plugin.ts +16 -3
- package/src/server/services/mcp/index.ts +7 -0
@@ -0,0 +1,151 @@
|
|
1
|
+
import { LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
|
2
|
+
|
3
|
+
import { safeParseJSON } from '@/utils/safeParseJSON';
|
4
|
+
|
5
|
+
// (McpConfig, McpServers, ParsedMcpInput 接口定义保持不变)
|
6
|
+
interface McpConfig {
|
7
|
+
args?: string[];
|
8
|
+
command?: string;
|
9
|
+
url?: string;
|
10
|
+
}
|
11
|
+
|
12
|
+
interface McpServers {
|
13
|
+
[key: string]: McpConfig;
|
14
|
+
}
|
15
|
+
|
16
|
+
interface ParsedMcpInput {
|
17
|
+
manifest?: LobeChatPluginManifest;
|
18
|
+
mcpServers?: McpServers;
|
19
|
+
}
|
20
|
+
|
21
|
+
// 移除 DuplicateIdentifier
|
22
|
+
export enum McpParseErrorCode {
|
23
|
+
EmptyMcpServers = 'EmptyMcpServers',
|
24
|
+
InvalidJsonStructure = 'InvalidJsonStructure',
|
25
|
+
InvalidMcpStructure = 'InvalidMcpStructure',
|
26
|
+
ManifestNotSupported = 'ManifestNotSupported',
|
27
|
+
}
|
28
|
+
|
29
|
+
// 移除 isDuplicate
|
30
|
+
interface ParseSuccessResult {
|
31
|
+
identifier: string;
|
32
|
+
mcpConfig: McpConfig & { type: 'stdio' | 'http' };
|
33
|
+
status: 'success';
|
34
|
+
}
|
35
|
+
|
36
|
+
interface ParseErrorResult {
|
37
|
+
errorCode: McpParseErrorCode;
|
38
|
+
// identifier 字段仍然可能有用,用于在结构错误时也能显示用户输入的 ID
|
39
|
+
identifier?: string;
|
40
|
+
status: 'error';
|
41
|
+
}
|
42
|
+
|
43
|
+
interface ParseNoOpResult {
|
44
|
+
status: 'noop';
|
45
|
+
}
|
46
|
+
|
47
|
+
export type ParseResult = ParseSuccessResult | ParseErrorResult | ParseNoOpResult;
|
48
|
+
|
49
|
+
export const parseMcpInput = (value: string): ParseResult => {
|
50
|
+
const parsedJson = safeParseJSON<ParsedMcpInput | McpServers>(value);
|
51
|
+
|
52
|
+
if (parsedJson && typeof parsedJson === 'object' && !Array.isArray(parsedJson)) {
|
53
|
+
// 1. Check for the nested "mcpServers" structure
|
54
|
+
if (
|
55
|
+
'mcpServers' in parsedJson &&
|
56
|
+
typeof parsedJson.mcpServers === 'object' &&
|
57
|
+
parsedJson.mcpServers !== null
|
58
|
+
) {
|
59
|
+
const mcpKeys = Object.keys(parsedJson.mcpServers);
|
60
|
+
|
61
|
+
if (mcpKeys.length > 0) {
|
62
|
+
const identifier = mcpKeys[0];
|
63
|
+
// @ts-expect-error type 不一样
|
64
|
+
const mcpConfig = parsedJson.mcpServers[identifier];
|
65
|
+
|
66
|
+
if (mcpConfig && typeof mcpConfig === 'object' && !Array.isArray(mcpConfig)) {
|
67
|
+
let type: 'stdio' | 'http' | undefined;
|
68
|
+
let resultMcpConfig: McpConfig & { type?: 'stdio' | 'http' } = {};
|
69
|
+
|
70
|
+
if (mcpConfig.command && Array.isArray(mcpConfig.args)) {
|
71
|
+
type = 'stdio';
|
72
|
+
resultMcpConfig = { ...mcpConfig, type };
|
73
|
+
} else if (mcpConfig.url) {
|
74
|
+
type = 'http';
|
75
|
+
resultMcpConfig = { type, url: mcpConfig.url };
|
76
|
+
} else {
|
77
|
+
return {
|
78
|
+
errorCode: McpParseErrorCode.InvalidMcpStructure,
|
79
|
+
identifier,
|
80
|
+
status: 'error',
|
81
|
+
};
|
82
|
+
}
|
83
|
+
|
84
|
+
return {
|
85
|
+
identifier,
|
86
|
+
mcpConfig: resultMcpConfig as McpConfig & { type: 'stdio' | 'http' },
|
87
|
+
status: 'success',
|
88
|
+
};
|
89
|
+
}
|
90
|
+
// mcpConfig is invalid or not an object
|
91
|
+
return {
|
92
|
+
errorCode: McpParseErrorCode.InvalidMcpStructure,
|
93
|
+
identifier: identifier,
|
94
|
+
status: 'error',
|
95
|
+
};
|
96
|
+
} else {
|
97
|
+
// mcpServers object is empty
|
98
|
+
return { errorCode: McpParseErrorCode.EmptyMcpServers, status: 'error' };
|
99
|
+
}
|
100
|
+
}
|
101
|
+
// 3. Check for the flat structure (identifier as top-level key)
|
102
|
+
else {
|
103
|
+
const topLevelKeys = Object.keys(parsedJson);
|
104
|
+
|
105
|
+
// Allow exactly one top-level key which is the identifier
|
106
|
+
if (topLevelKeys.length === 1) {
|
107
|
+
const identifier = topLevelKeys[0];
|
108
|
+
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
109
|
+
const mcpConfig = (parsedJson as any)[identifier];
|
110
|
+
|
111
|
+
if (mcpConfig && typeof mcpConfig === 'object' && !Array.isArray(mcpConfig)) {
|
112
|
+
let type: 'stdio' | 'http' | undefined;
|
113
|
+
let resultMcpConfig: McpConfig & { type?: 'stdio' | 'http' } = {};
|
114
|
+
|
115
|
+
// Explicitly check properties of mcpConfig
|
116
|
+
if (mcpConfig.command && Array.isArray(mcpConfig.args)) {
|
117
|
+
type = 'stdio';
|
118
|
+
resultMcpConfig = { ...mcpConfig, type };
|
119
|
+
} else if (mcpConfig.url) {
|
120
|
+
type = 'http';
|
121
|
+
// For the flat structure, ensure only 'url' is included for http type
|
122
|
+
resultMcpConfig = { type, url: mcpConfig.url };
|
123
|
+
} else {
|
124
|
+
// Invalid structure within the identifier's value
|
125
|
+
return {
|
126
|
+
errorCode: McpParseErrorCode.InvalidMcpStructure,
|
127
|
+
identifier, // We have the identifier here
|
128
|
+
status: 'error',
|
129
|
+
};
|
130
|
+
}
|
131
|
+
|
132
|
+
// Structure parsed successfully
|
133
|
+
return {
|
134
|
+
identifier,
|
135
|
+
mcpConfig: resultMcpConfig as McpConfig & { type: 'stdio' | 'http' },
|
136
|
+
status: 'success',
|
137
|
+
};
|
138
|
+
} else {
|
139
|
+
// The value associated with the single key is not a valid config object
|
140
|
+
return { errorCode: McpParseErrorCode.InvalidMcpStructure, identifier, status: 'error' };
|
141
|
+
}
|
142
|
+
} else {
|
143
|
+
// Neither mcpServers nor manifest, and not a single top-level key structure
|
144
|
+
return { errorCode: McpParseErrorCode.InvalidJsonStructure, status: 'error' };
|
145
|
+
}
|
146
|
+
}
|
147
|
+
}
|
148
|
+
|
149
|
+
// Input is not a valid JSON object or failed safeParseJSON
|
150
|
+
return { status: 'noop' }; // Or potentially InvalidJsonStructure if safeParse failed but wasn't null/undefined?
|
151
|
+
};
|
@@ -36,10 +36,14 @@ const DevModal = memo<DevModalProps>(
|
|
36
36
|
form.setFieldsValue(value);
|
37
37
|
}, []);
|
38
38
|
|
39
|
+
useEffect(() => {
|
40
|
+
if (mode === 'create' && !open) form.resetFields();
|
41
|
+
}, [open]);
|
42
|
+
|
39
43
|
const buttonStyle = mobile ? { flex: 1 } : { margin: 0 };
|
40
44
|
|
41
45
|
const footer = (
|
42
|
-
<Flexbox flex={1} gap={12} horizontal justify={'
|
46
|
+
<Flexbox flex={1} gap={12} horizontal justify={'space-between'}>
|
43
47
|
{isEditMode ? (
|
44
48
|
<Popconfirm
|
45
49
|
arrow={false}
|
@@ -60,25 +64,29 @@ const DevModal = memo<DevModalProps>(
|
|
60
64
|
{t('delete', { ns: 'common' })}
|
61
65
|
</Button>
|
62
66
|
</Popconfirm>
|
63
|
-
) :
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
67
|
+
) : (
|
68
|
+
<div />
|
69
|
+
)}
|
70
|
+
<Flexbox gap={12} horizontal>
|
71
|
+
<Button
|
72
|
+
onClick={() => {
|
73
|
+
onOpenChange(false);
|
74
|
+
}}
|
75
|
+
style={buttonStyle}
|
76
|
+
>
|
77
|
+
{t('cancel', { ns: 'common' })}
|
78
|
+
</Button>
|
79
|
+
<Button
|
80
|
+
loading={submitting}
|
81
|
+
onClick={() => {
|
82
|
+
form.submit();
|
83
|
+
}}
|
84
|
+
style={buttonStyle}
|
85
|
+
type={'primary'}
|
86
|
+
>
|
87
|
+
{t(isEditMode ? 'dev.update' : 'dev.save')}
|
88
|
+
</Button>
|
89
|
+
</Flexbox>
|
82
90
|
</Flexbox>
|
83
91
|
);
|
84
92
|
|
@@ -100,6 +108,7 @@ const DevModal = memo<DevModalProps>(
|
|
100
108
|
>
|
101
109
|
<Modal
|
102
110
|
allowFullscreen
|
111
|
+
destroyOnClose
|
103
112
|
footer={footer}
|
104
113
|
okText={t('dev.save')}
|
105
114
|
onCancel={(e) => {
|
@@ -166,10 +175,10 @@ const DevModal = memo<DevModalProps>(
|
|
166
175
|
showIcon
|
167
176
|
type={'info'}
|
168
177
|
/>
|
169
|
-
<UrlManifestForm form={form} isEditMode={
|
178
|
+
<UrlManifestForm form={form} isEditMode={isEditMode} />
|
170
179
|
</>
|
171
180
|
)}
|
172
|
-
{configMode === 'mcp' && <MCPManifestForm form={form} />}
|
181
|
+
{configMode === 'mcp' && <MCPManifestForm form={form} isEditMode={isEditMode} />}
|
173
182
|
<PluginPreview form={form} />
|
174
183
|
</Flexbox>
|
175
184
|
</Modal>
|
@@ -354,7 +354,7 @@ export const LobeOpenAICompatibleFactory = <T extends Record<string, any> = any>
|
|
354
354
|
async textToImage(payload: TextToImagePayload) {
|
355
355
|
try {
|
356
356
|
const res = await this.client.images.generate(payload);
|
357
|
-
return res.data.map((o) => o.url) as string[];
|
357
|
+
return (res.data || []).map((o) => o.url) as string[];
|
358
358
|
} catch (error) {
|
359
359
|
throw this.handleError(error);
|
360
360
|
}
|
@@ -7,6 +7,7 @@ export default {
|
|
7
7
|
payload: '插件载荷',
|
8
8
|
pluginState: '插件 State',
|
9
9
|
response: '返回结果',
|
10
|
+
title: '插件详情',
|
10
11
|
tool_call: '工具调用请求',
|
11
12
|
},
|
12
13
|
detailModal: {
|
@@ -50,11 +51,13 @@ export default {
|
|
50
51
|
desc: '传递给 STDIO 命令的参数列表,一般在这里输入 MCP 服务器名称',
|
51
52
|
label: '命令参数',
|
52
53
|
placeholder: '例如:mcp-hello-world',
|
54
|
+
required: '请输入启动参数',
|
53
55
|
},
|
54
56
|
command: {
|
55
57
|
desc: '用于启动 MCP STDIO Server 的可执行文件或脚本',
|
56
58
|
label: '命令',
|
57
59
|
placeholder: '例如:npx / uv / docker 等',
|
60
|
+
required: '请输入启动命令',
|
58
61
|
},
|
59
62
|
endpoint: {
|
60
63
|
desc: '输入你的 MCP Streamable HTTP Server 的地址',
|
@@ -62,25 +65,31 @@ export default {
|
|
62
65
|
},
|
63
66
|
identifier: {
|
64
67
|
desc: '为你的 MCP 插件指定一个名称,需要使用英文字符',
|
65
|
-
invalid: '
|
68
|
+
invalid: '标识符只能包含字母、数字、连字符和下划线',
|
66
69
|
label: 'MCP 插件名称',
|
67
70
|
placeholder: '例如:my-mcp-plugin',
|
71
|
+
required: '请输入 MCP 服务标识符',
|
68
72
|
},
|
73
|
+
previewManifest: '预览插件描述文件',
|
74
|
+
testConnection: '测试连接',
|
75
|
+
testConnectionTip: '测试连接成功后 MCP 插件才可以被正常使用',
|
69
76
|
type: {
|
70
77
|
desc: '选择 MCP 插件的通信方式,网页版只支持 Streamable HTTP',
|
71
78
|
httpFeature1: '兼容网页版与桌面端',
|
72
|
-
httpFeature2: '连接远程 MCP
|
79
|
+
httpFeature2: '连接远程 MCP 服务器, 无需额外安装配置',
|
73
80
|
httpShortDesc: '基于流式 HTTP 的通信协议',
|
74
81
|
label: 'MCP 插件类型',
|
75
82
|
stdioFeature1: '更低的通信延迟, 适合本地执行',
|
76
|
-
stdioFeature2: '
|
83
|
+
stdioFeature2: '需在本地安装运行 MCP 服务器',
|
77
84
|
stdioNotAvailable: 'STDIO 模式仅在桌面版可用',
|
78
85
|
stdioShortDesc: '基于标准输入输出的通信协议',
|
79
86
|
title: 'MCP 插件类型',
|
80
87
|
},
|
81
88
|
url: {
|
82
89
|
desc: '输入你的 MCP Server Streamable HTTP 地址,不会以 /sse 结尾',
|
90
|
+
invalid: '请输入有效的 URL 地址',
|
83
91
|
label: 'Streamable HTTP Endpoint URL',
|
92
|
+
required: '请输入 MCP 服务 URL',
|
84
93
|
},
|
85
94
|
},
|
86
95
|
meta: {
|
@@ -108,12 +117,14 @@ export default {
|
|
108
117
|
label: '标识符',
|
109
118
|
pattenErrorMessage: '只能输入英文字符、数字 、- 和_ 这两个符号',
|
110
119
|
},
|
120
|
+
lobe: '{{appName}} 插件',
|
111
121
|
manifest: {
|
112
122
|
desc: '{{appName}}将会通过该链接安装插件',
|
113
123
|
label: '插件描述文件 (Manifest) URL',
|
114
124
|
preview: '预览 Manifest',
|
115
125
|
refresh: '刷新',
|
116
126
|
},
|
127
|
+
openai: 'OpenAI 插件',
|
117
128
|
title: {
|
118
129
|
desc: '插件标题',
|
119
130
|
label: '标题',
|
@@ -156,6 +167,7 @@ export default {
|
|
156
167
|
noManifest: '描述文件不存在',
|
157
168
|
openAPIInvalid: 'OpenAPI 解析失败,错误: \n\n {{error}}',
|
158
169
|
reinstallError: '插件 {{name}} 刷新失败',
|
170
|
+
testConnectionFailed: '获取 Manifest 失败: {{error}}',
|
159
171
|
urlError: '该链接没有返回 JSON 格式的内容, 请确保是有效的链接',
|
160
172
|
},
|
161
173
|
inspector: {
|
@@ -234,5 +246,6 @@ export default {
|
|
234
246
|
},
|
235
247
|
title: '插件商店',
|
236
248
|
},
|
249
|
+
unknownError: '未知错误',
|
237
250
|
unknownPlugin: '未知插件',
|
238
251
|
};
|
@@ -1,4 +1,5 @@
|
|
1
1
|
import { LobeChatPluginApi, LobeChatPluginManifest, PluginSchema } from '@lobehub/chat-plugin-sdk';
|
2
|
+
import { McpError } from '@modelcontextprotocol/sdk/types.js';
|
2
3
|
import { TRPCError } from '@trpc/server';
|
3
4
|
import debug from 'debug';
|
4
5
|
|
@@ -61,6 +62,12 @@ class MCPService {
|
|
61
62
|
|
62
63
|
return result;
|
63
64
|
} catch (error) {
|
65
|
+
if (error instanceof McpError) {
|
66
|
+
const mcpError = error as McpError;
|
67
|
+
|
68
|
+
return mcpError.message;
|
69
|
+
}
|
70
|
+
|
64
71
|
console.error(`Error calling tool "${toolName}" for params %O:`, params, error);
|
65
72
|
// Propagate a TRPCError
|
66
73
|
throw new TRPCError({
|