@lobehub/lobehub 2.0.0-next.117 → 2.0.0-next.119
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/.github/workflows/{docker.yml → pr-build-docker.yml} +30 -43
- package/.github/workflows/release-docker.yml +133 -0
- package/CHANGELOG.md +50 -0
- package/changelog/v1.json +18 -0
- package/locales/ar/models.json +10 -1
- package/locales/ar/setting.json +1 -1
- package/locales/bg-BG/models.json +10 -1
- package/locales/bg-BG/setting.json +1 -1
- package/locales/de-DE/models.json +10 -1
- package/locales/de-DE/setting.json +1 -1
- package/locales/en-US/models.json +10 -1
- package/locales/en-US/setting.json +1 -1
- package/locales/es-ES/models.json +10 -1
- package/locales/es-ES/setting.json +1 -1
- package/locales/fa-IR/models.json +10 -1
- package/locales/fa-IR/setting.json +1 -1
- package/locales/fr-FR/models.json +10 -1
- package/locales/fr-FR/setting.json +1 -1
- package/locales/it-IT/models.json +10 -1
- package/locales/it-IT/setting.json +1 -1
- package/locales/ja-JP/models.json +10 -1
- package/locales/ja-JP/setting.json +1 -1
- package/locales/ko-KR/models.json +10 -1
- package/locales/ko-KR/setting.json +1 -1
- package/locales/nl-NL/models.json +10 -1
- package/locales/nl-NL/setting.json +1 -1
- package/locales/pl-PL/models.json +10 -1
- package/locales/pl-PL/setting.json +1 -1
- package/locales/pt-BR/models.json +10 -1
- package/locales/pt-BR/setting.json +1 -1
- package/locales/ru-RU/models.json +10 -1
- package/locales/ru-RU/setting.json +1 -1
- package/locales/tr-TR/models.json +10 -1
- package/locales/tr-TR/setting.json +1 -1
- package/locales/vi-VN/models.json +10 -1
- package/locales/vi-VN/setting.json +1 -1
- package/locales/zh-CN/models.json +10 -1
- package/locales/zh-TW/models.json +10 -1
- package/locales/zh-TW/setting.json +1 -1
- package/package.json +1 -1
- package/packages/fetch-sse/src/__tests__/headers.test.ts +367 -0
- package/src/app/[variants]/(main)/discover/(detail)/assistant/features/Details/Capabilities/Plugins.tsx +11 -6
- package/src/libs/mcp/__tests__/__snapshots__/index.test.ts.snap +5 -0
- package/src/server/services/discover/index.ts +42 -2
- package/src/server/services/mcp/contentProcessor.ts +7 -7
- package/src/server/services/mcp/deps/MCPSystemDepsCheckService.ts +3 -3
- package/src/server/services/mcp/index.ts +11 -11
- /package/.github/workflows/{desktop-pr-build.yml → pr-build-desktop.yml} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobehub/lobehub",
|
|
3
|
-
"version": "2.0.0-next.
|
|
3
|
+
"version": "2.0.0-next.119",
|
|
4
4
|
"description": "LobeHub - an open-source,comprehensive AI Agent framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"framework",
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { headersToRecord } from '../headers';
|
|
4
|
+
|
|
5
|
+
describe('headersToRecord', () => {
|
|
6
|
+
describe('undefined or null input', () => {
|
|
7
|
+
it('should return empty object when headersInit is undefined', () => {
|
|
8
|
+
// Arrange & Act
|
|
9
|
+
const result = headersToRecord(undefined);
|
|
10
|
+
|
|
11
|
+
// Assert
|
|
12
|
+
expect(result).toEqual({});
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('should return empty object when headersInit is not provided', () => {
|
|
16
|
+
// Arrange & Act
|
|
17
|
+
const result = headersToRecord();
|
|
18
|
+
|
|
19
|
+
// Assert
|
|
20
|
+
expect(result).toEqual({});
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('Headers instance', () => {
|
|
25
|
+
it('should convert Headers instance to record', () => {
|
|
26
|
+
// Arrange
|
|
27
|
+
const headers = new Headers();
|
|
28
|
+
headers.append('content-type', 'application/json');
|
|
29
|
+
headers.append('authorization', 'Bearer token123');
|
|
30
|
+
headers.append('x-custom-header', 'custom-value');
|
|
31
|
+
|
|
32
|
+
// Act
|
|
33
|
+
const result = headersToRecord(headers);
|
|
34
|
+
|
|
35
|
+
// Assert
|
|
36
|
+
expect(result).toEqual({
|
|
37
|
+
'content-type': 'application/json',
|
|
38
|
+
'authorization': 'Bearer token123',
|
|
39
|
+
'x-custom-header': 'custom-value',
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should handle Headers instance with multiple values for same key', () => {
|
|
44
|
+
// Arrange
|
|
45
|
+
const headers = new Headers();
|
|
46
|
+
headers.append('accept', 'application/json');
|
|
47
|
+
headers.append('accept', 'text/html');
|
|
48
|
+
|
|
49
|
+
// Act
|
|
50
|
+
const result = headersToRecord(headers);
|
|
51
|
+
|
|
52
|
+
// Assert
|
|
53
|
+
expect(result).toHaveProperty('accept');
|
|
54
|
+
expect(typeof result.accept).toBe('string');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should handle empty Headers instance', () => {
|
|
58
|
+
// Arrange
|
|
59
|
+
const headers = new Headers();
|
|
60
|
+
|
|
61
|
+
// Act
|
|
62
|
+
const result = headersToRecord(headers);
|
|
63
|
+
|
|
64
|
+
// Assert
|
|
65
|
+
expect(result).toEqual({});
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('Array format', () => {
|
|
70
|
+
it('should convert array of tuples to record', () => {
|
|
71
|
+
// Arrange
|
|
72
|
+
const headersArray: [string, string][] = [
|
|
73
|
+
['content-type', 'application/json'],
|
|
74
|
+
['authorization', 'Bearer token123'],
|
|
75
|
+
['x-api-key', 'api-key-value'],
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
// Act
|
|
79
|
+
const result = headersToRecord(headersArray);
|
|
80
|
+
|
|
81
|
+
// Assert
|
|
82
|
+
expect(result).toEqual({
|
|
83
|
+
'content-type': 'application/json',
|
|
84
|
+
'authorization': 'Bearer token123',
|
|
85
|
+
'x-api-key': 'api-key-value',
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should handle empty array', () => {
|
|
90
|
+
// Arrange
|
|
91
|
+
const headersArray: [string, string][] = [];
|
|
92
|
+
|
|
93
|
+
// Act
|
|
94
|
+
const result = headersToRecord(headersArray);
|
|
95
|
+
|
|
96
|
+
// Assert
|
|
97
|
+
expect(result).toEqual({});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should handle array with single header', () => {
|
|
101
|
+
// Arrange
|
|
102
|
+
const headersArray: [string, string][] = [['x-single-header', 'single-value']];
|
|
103
|
+
|
|
104
|
+
// Act
|
|
105
|
+
const result = headersToRecord(headersArray);
|
|
106
|
+
|
|
107
|
+
// Assert
|
|
108
|
+
expect(result).toEqual({
|
|
109
|
+
'x-single-header': 'single-value',
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe('Plain object', () => {
|
|
115
|
+
it('should convert plain object to record', () => {
|
|
116
|
+
// Arrange
|
|
117
|
+
const headersObj = {
|
|
118
|
+
'content-type': 'application/json',
|
|
119
|
+
'authorization': 'Bearer token123',
|
|
120
|
+
'x-custom': 'value',
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// Act
|
|
124
|
+
const result = headersToRecord(headersObj);
|
|
125
|
+
|
|
126
|
+
// Assert
|
|
127
|
+
expect(result).toEqual({
|
|
128
|
+
'content-type': 'application/json',
|
|
129
|
+
'authorization': 'Bearer token123',
|
|
130
|
+
'x-custom': 'value',
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should handle empty object', () => {
|
|
135
|
+
// Arrange
|
|
136
|
+
const headersObj = {};
|
|
137
|
+
|
|
138
|
+
// Act
|
|
139
|
+
const result = headersToRecord(headersObj);
|
|
140
|
+
|
|
141
|
+
// Assert
|
|
142
|
+
expect(result).toEqual({});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should handle object with special characters in values', () => {
|
|
146
|
+
// Arrange
|
|
147
|
+
const headersObj = {
|
|
148
|
+
'authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9',
|
|
149
|
+
'x-special': 'value with spaces and symbols: !@#$%',
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// Act
|
|
153
|
+
const result = headersToRecord(headersObj);
|
|
154
|
+
|
|
155
|
+
// Assert
|
|
156
|
+
expect(result).toEqual({
|
|
157
|
+
'authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9',
|
|
158
|
+
'x-special': 'value with spaces and symbols: !@#$%',
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe('Restricted headers filtering', () => {
|
|
164
|
+
it('should remove "host" header from Headers instance', () => {
|
|
165
|
+
// Arrange
|
|
166
|
+
const headers = new Headers();
|
|
167
|
+
headers.append('host', 'example.com');
|
|
168
|
+
headers.append('content-type', 'application/json');
|
|
169
|
+
|
|
170
|
+
// Act
|
|
171
|
+
const result = headersToRecord(headers);
|
|
172
|
+
|
|
173
|
+
// Assert
|
|
174
|
+
expect(result).not.toHaveProperty('host');
|
|
175
|
+
expect(result).toEqual({
|
|
176
|
+
'content-type': 'application/json',
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should remove "connection" header from array', () => {
|
|
181
|
+
// Arrange
|
|
182
|
+
const headersArray: [string, string][] = [
|
|
183
|
+
['connection', 'keep-alive'],
|
|
184
|
+
['authorization', 'Bearer token'],
|
|
185
|
+
];
|
|
186
|
+
|
|
187
|
+
// Act
|
|
188
|
+
const result = headersToRecord(headersArray);
|
|
189
|
+
|
|
190
|
+
// Assert
|
|
191
|
+
expect(result).not.toHaveProperty('connection');
|
|
192
|
+
expect(result).toEqual({
|
|
193
|
+
authorization: 'Bearer token',
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('should remove "content-length" header from plain object', () => {
|
|
198
|
+
// Arrange
|
|
199
|
+
const headersObj = {
|
|
200
|
+
'content-length': '1234',
|
|
201
|
+
'content-type': 'application/json',
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
// Act
|
|
205
|
+
const result = headersToRecord(headersObj);
|
|
206
|
+
|
|
207
|
+
// Assert
|
|
208
|
+
expect(result).not.toHaveProperty('content-length');
|
|
209
|
+
expect(result).toEqual({
|
|
210
|
+
'content-type': 'application/json',
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('should remove all restricted headers (host, connection, content-length)', () => {
|
|
215
|
+
// Arrange
|
|
216
|
+
const headersObj = {
|
|
217
|
+
'host': 'example.com',
|
|
218
|
+
'connection': 'keep-alive',
|
|
219
|
+
'content-length': '1234',
|
|
220
|
+
'authorization': 'Bearer token',
|
|
221
|
+
'content-type': 'application/json',
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
// Act
|
|
225
|
+
const result = headersToRecord(headersObj);
|
|
226
|
+
|
|
227
|
+
// Assert
|
|
228
|
+
expect(result).not.toHaveProperty('host');
|
|
229
|
+
expect(result).not.toHaveProperty('connection');
|
|
230
|
+
expect(result).not.toHaveProperty('content-length');
|
|
231
|
+
expect(result).toEqual({
|
|
232
|
+
'authorization': 'Bearer token',
|
|
233
|
+
'content-type': 'application/json',
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('should handle case when only restricted headers are present', () => {
|
|
238
|
+
// Arrange
|
|
239
|
+
const headersObj = {
|
|
240
|
+
'host': 'example.com',
|
|
241
|
+
'connection': 'keep-alive',
|
|
242
|
+
'content-length': '1234',
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// Act
|
|
246
|
+
const result = headersToRecord(headersObj);
|
|
247
|
+
|
|
248
|
+
// Assert
|
|
249
|
+
expect(result).toEqual({});
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
describe('Edge cases', () => {
|
|
254
|
+
it('should handle headers with empty string values', () => {
|
|
255
|
+
// Arrange
|
|
256
|
+
const headersObj = {
|
|
257
|
+
'x-empty': '',
|
|
258
|
+
'x-normal': 'value',
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
// Act
|
|
262
|
+
const result = headersToRecord(headersObj);
|
|
263
|
+
|
|
264
|
+
// Assert
|
|
265
|
+
expect(result).toEqual({
|
|
266
|
+
'x-empty': '',
|
|
267
|
+
'x-normal': 'value',
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('should handle headers with numeric-like values as strings', () => {
|
|
272
|
+
// Arrange
|
|
273
|
+
const headersArray: [string, string][] = [
|
|
274
|
+
['x-request-id', '12345'],
|
|
275
|
+
['x-retry-count', '3'],
|
|
276
|
+
];
|
|
277
|
+
|
|
278
|
+
// Act
|
|
279
|
+
const result = headersToRecord(headersArray);
|
|
280
|
+
|
|
281
|
+
// Assert
|
|
282
|
+
expect(result).toEqual({
|
|
283
|
+
'x-request-id': '12345',
|
|
284
|
+
'x-retry-count': '3',
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('should handle case-sensitive header names', () => {
|
|
289
|
+
// Arrange
|
|
290
|
+
const headersObj = {
|
|
291
|
+
'Content-Type': 'application/json',
|
|
292
|
+
'Authorization': 'Bearer token',
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
// Act
|
|
296
|
+
const result = headersToRecord(headersObj);
|
|
297
|
+
|
|
298
|
+
// Assert
|
|
299
|
+
expect(result).toEqual({
|
|
300
|
+
'Content-Type': 'application/json',
|
|
301
|
+
'Authorization': 'Bearer token',
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('should preserve header order from array input', () => {
|
|
306
|
+
// Arrange
|
|
307
|
+
const headersArray: [string, string][] = [
|
|
308
|
+
['z-last', 'last'],
|
|
309
|
+
['a-first', 'first'],
|
|
310
|
+
['m-middle', 'middle'],
|
|
311
|
+
];
|
|
312
|
+
|
|
313
|
+
// Act
|
|
314
|
+
const result = headersToRecord(headersArray);
|
|
315
|
+
|
|
316
|
+
// Assert
|
|
317
|
+
expect(Object.keys(result)).toEqual(['z-last', 'a-first', 'm-middle']);
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
describe('Real-world scenarios', () => {
|
|
322
|
+
it('should handle typical SSE request headers', () => {
|
|
323
|
+
// Arrange
|
|
324
|
+
const headers = new Headers();
|
|
325
|
+
headers.append('accept', 'text/event-stream');
|
|
326
|
+
headers.append('content-type', 'application/json');
|
|
327
|
+
headers.append('authorization', 'Bearer abc123');
|
|
328
|
+
headers.append('cache-control', 'no-cache');
|
|
329
|
+
headers.append('connection', 'keep-alive'); // Should be filtered
|
|
330
|
+
|
|
331
|
+
// Act
|
|
332
|
+
const result = headersToRecord(headers);
|
|
333
|
+
|
|
334
|
+
// Assert
|
|
335
|
+
expect(result).toEqual({
|
|
336
|
+
'accept': 'text/event-stream',
|
|
337
|
+
'content-type': 'application/json',
|
|
338
|
+
'authorization': 'Bearer abc123',
|
|
339
|
+
'cache-control': 'no-cache',
|
|
340
|
+
});
|
|
341
|
+
expect(result).not.toHaveProperty('connection');
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it('should handle API request headers with custom fields', () => {
|
|
345
|
+
// Arrange
|
|
346
|
+
const headersObj = {
|
|
347
|
+
'content-type': 'application/json',
|
|
348
|
+
'x-api-key': 'secret-key',
|
|
349
|
+
'x-request-id': 'req-123',
|
|
350
|
+
'user-agent': 'MyApp/1.0',
|
|
351
|
+
'host': 'api.example.com', // Should be filtered
|
|
352
|
+
'content-length': '256', // Should be filtered
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
// Act
|
|
356
|
+
const result = headersToRecord(headersObj);
|
|
357
|
+
|
|
358
|
+
// Assert
|
|
359
|
+
expect(result).toEqual({
|
|
360
|
+
'content-type': 'application/json',
|
|
361
|
+
'x-api-key': 'secret-key',
|
|
362
|
+
'x-request-id': 'req-123',
|
|
363
|
+
'user-agent': 'MyApp/1.0',
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Block } from '@lobehub/ui';
|
|
2
2
|
import { Empty } from 'antd';
|
|
3
|
-
import Link from '
|
|
3
|
+
import { Link } from 'react-router-dom';
|
|
4
4
|
import { memo } from 'react';
|
|
5
5
|
import { Flexbox } from 'react-layout-kit';
|
|
6
6
|
import urlJoin from 'url-join';
|
|
@@ -20,11 +20,16 @@ const Plugin = memo(() => {
|
|
|
20
20
|
|
|
21
21
|
return (
|
|
22
22
|
<Flexbox gap={8}>
|
|
23
|
-
{config?.plugins.map((item) =>
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
{config?.plugins.map((item) => {
|
|
24
|
+
const identifier =
|
|
25
|
+
typeof item === 'string' ? item : (item as { identifier: string }).identifier;
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<Link key={identifier} to={urlJoin('/discover/plugin', identifier)}>
|
|
29
|
+
<PluginItem identifier={identifier} />
|
|
30
|
+
</Link>
|
|
31
|
+
);
|
|
32
|
+
})}
|
|
28
33
|
</Flexbox>
|
|
29
34
|
);
|
|
30
35
|
});
|
|
@@ -5,6 +5,8 @@ exports[`MCPClient > Stdio Transport > should list tools via stdio 1`] = `
|
|
|
5
5
|
{
|
|
6
6
|
"description": "Echoes back a message with 'Hello' prefix",
|
|
7
7
|
"inputSchema": {
|
|
8
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
9
|
+
"additionalProperties": false,
|
|
8
10
|
"properties": {
|
|
9
11
|
"message": {
|
|
10
12
|
"description": "The message to echo",
|
|
@@ -21,6 +23,7 @@ exports[`MCPClient > Stdio Transport > should list tools via stdio 1`] = `
|
|
|
21
23
|
{
|
|
22
24
|
"description": "Lists all available tools and methods",
|
|
23
25
|
"inputSchema": {
|
|
26
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
24
27
|
"properties": {},
|
|
25
28
|
"type": "object",
|
|
26
29
|
},
|
|
@@ -29,6 +32,8 @@ exports[`MCPClient > Stdio Transport > should list tools via stdio 1`] = `
|
|
|
29
32
|
{
|
|
30
33
|
"description": "Adds two numbers",
|
|
31
34
|
"inputSchema": {
|
|
35
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
36
|
+
"additionalProperties": false,
|
|
32
37
|
"properties": {
|
|
33
38
|
"a": {
|
|
34
39
|
"description": "The first number",
|
|
@@ -886,8 +886,48 @@ export class DiscoverService {
|
|
|
886
886
|
const all = await this._getPluginList(locale);
|
|
887
887
|
let raw = all.find((item) => item.identifier === identifier);
|
|
888
888
|
if (!raw) {
|
|
889
|
-
log('getPluginDetail: plugin not found for identifier=%s', identifier);
|
|
890
|
-
|
|
889
|
+
log('getPluginDetail: plugin not found in default store for identifier=%s, trying MCP plugin', identifier);
|
|
890
|
+
try {
|
|
891
|
+
const mcpDetail = await this.getMcpDetail({ identifier, locale });
|
|
892
|
+
const convertedMcp: Partial<DiscoverPluginDetail> = {
|
|
893
|
+
author:
|
|
894
|
+
typeof (mcpDetail as any).author === 'object'
|
|
895
|
+
? (mcpDetail as any).author?.name || ''
|
|
896
|
+
: (mcpDetail as any).author || '',
|
|
897
|
+
avatar: (mcpDetail as any).icon || (mcpDetail as any).avatar || '',
|
|
898
|
+
category: (mcpDetail as any).category as any,
|
|
899
|
+
createdAt: (mcpDetail as any).createdAt || '',
|
|
900
|
+
description: mcpDetail.description || '',
|
|
901
|
+
homepage: mcpDetail.homepage || '',
|
|
902
|
+
identifier: mcpDetail.identifier,
|
|
903
|
+
manifest: undefined,
|
|
904
|
+
related: mcpDetail.related.map((item) => ({
|
|
905
|
+
author:
|
|
906
|
+
typeof (item as any).author === 'object'
|
|
907
|
+
? (item as any).author?.name || ''
|
|
908
|
+
: (item as any).author || '',
|
|
909
|
+
avatar: (item as any).icon || (item as any).avatar || '',
|
|
910
|
+
category: (item as any).category as any,
|
|
911
|
+
createdAt: (item as any).createdAt || '',
|
|
912
|
+
description: (item as any).description || '',
|
|
913
|
+
homepage: (item as any).homepage || '',
|
|
914
|
+
identifier: item.identifier,
|
|
915
|
+
manifest: undefined,
|
|
916
|
+
schemaVersion: 1,
|
|
917
|
+
tags: (item as any).tags || [],
|
|
918
|
+
title: (item as any).name || item.identifier,
|
|
919
|
+
})) as unknown as DiscoverPluginItem[],
|
|
920
|
+
schemaVersion: 1,
|
|
921
|
+
tags: (mcpDetail as any).tags || [],
|
|
922
|
+
title: (mcpDetail as any).name || mcpDetail.identifier,
|
|
923
|
+
};
|
|
924
|
+
const plugin = merge(cloneDeep(DEFAULT_DISCOVER_PLUGIN_ITEM), convertedMcp);
|
|
925
|
+
log('getPluginDetail: returning converted MCP plugin');
|
|
926
|
+
return plugin as DiscoverPluginDetail;
|
|
927
|
+
} catch (error) {
|
|
928
|
+
log('getPluginDetail: MCP plugin not found for identifier=%s, error=%O', identifier, error);
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
891
931
|
}
|
|
892
932
|
|
|
893
933
|
raw = merge(cloneDeep(DEFAULT_DISCOVER_PLUGIN_ITEM), raw);
|
|
@@ -13,9 +13,9 @@ const log = debug('lobe-mcp:content-processor');
|
|
|
13
13
|
export type ProcessContentBlocksFn = (blocks: ToolCallContent[]) => Promise<ToolCallContent[]>;
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
17
|
-
* -
|
|
18
|
-
* -
|
|
16
|
+
* Process content blocks returned by MCP
|
|
17
|
+
* - Upload images/audio to storage and replace data with proxy URL
|
|
18
|
+
* - Keep other types of blocks unchanged
|
|
19
19
|
*/
|
|
20
20
|
export const processContentBlocks = async (
|
|
21
21
|
blocks: ToolCallContent[],
|
|
@@ -64,10 +64,10 @@ export const processContentBlocks = async (
|
|
|
64
64
|
};
|
|
65
65
|
|
|
66
66
|
/**
|
|
67
|
-
*
|
|
68
|
-
* - text:
|
|
69
|
-
* - image/audio:
|
|
70
|
-
* -
|
|
67
|
+
* Convert content blocks to string
|
|
68
|
+
* - text: Extract text field
|
|
69
|
+
* - image/audio: Extract data field (usually the proxy URL after upload)
|
|
70
|
+
* - others: Return empty string
|
|
71
71
|
*/
|
|
72
72
|
export const contentBlocksToString = (blocks: ToolCallContent[] | null | undefined): string => {
|
|
73
73
|
if (!blocks) return '';
|
|
@@ -195,13 +195,13 @@ class MCPSystemDepsCheckService {
|
|
|
195
195
|
// Check if all system dependencies meet requirements
|
|
196
196
|
const allDependenciesMet = systemDependenciesResults.every((dep) => dep.meetRequirement);
|
|
197
197
|
|
|
198
|
-
// Check if configuration is required (
|
|
198
|
+
// Check if configuration is required (has mandatory fields)
|
|
199
199
|
const configSchema = option.connection?.configSchema;
|
|
200
200
|
const needsConfig = Boolean(
|
|
201
201
|
configSchema &&
|
|
202
|
-
//
|
|
202
|
+
// Check if there's a non-empty required array
|
|
203
203
|
((Array.isArray(configSchema.required) && configSchema.required.length > 0) ||
|
|
204
|
-
//
|
|
204
|
+
// Check if any field in properties is marked as required
|
|
205
205
|
(configSchema.properties &&
|
|
206
206
|
Object.values(configSchema.properties).some((prop: any) => prop.required === true))),
|
|
207
207
|
);
|
|
@@ -250,7 +250,7 @@ export class MCPService {
|
|
|
250
250
|
} catch (error) {
|
|
251
251
|
console.error(`Failed to initialize MCP client:`, error);
|
|
252
252
|
|
|
253
|
-
//
|
|
253
|
+
// Preserve complete error information, especially detailed stderr output
|
|
254
254
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
255
255
|
|
|
256
256
|
if (typeof error === 'object' && !!error && 'data' in error) {
|
|
@@ -261,7 +261,7 @@ export class MCPService {
|
|
|
261
261
|
});
|
|
262
262
|
}
|
|
263
263
|
|
|
264
|
-
//
|
|
264
|
+
// Log detailed error information for debugging
|
|
265
265
|
log('Detailed initialization error: %O', {
|
|
266
266
|
error: errorMessage,
|
|
267
267
|
params: this.sanitizeForLogging(params),
|
|
@@ -271,7 +271,7 @@ export class MCPService {
|
|
|
271
271
|
throw new TRPCError({
|
|
272
272
|
cause: error,
|
|
273
273
|
code: 'INTERNAL_SERVER_ERROR',
|
|
274
|
-
message: errorMessage, //
|
|
274
|
+
message: errorMessage, // Use complete error message directly
|
|
275
275
|
});
|
|
276
276
|
}
|
|
277
277
|
}
|
|
@@ -307,12 +307,12 @@ export class MCPService {
|
|
|
307
307
|
): Promise<LobeChatPluginManifest> {
|
|
308
308
|
const mcpParams = { name: identifier, type: 'http' as const, url };
|
|
309
309
|
|
|
310
|
-
//
|
|
310
|
+
// Add authentication info to parameters if available
|
|
311
311
|
if (auth) {
|
|
312
312
|
(mcpParams as any).auth = auth;
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
-
//
|
|
315
|
+
// Add headers info to parameters if available
|
|
316
316
|
if (headers) {
|
|
317
317
|
(mcpParams as any).headers = headers;
|
|
318
318
|
}
|
|
@@ -383,23 +383,23 @@ export class MCPService {
|
|
|
383
383
|
log('Checking MCP plugin installation status: %O', loggableInput);
|
|
384
384
|
const results = [];
|
|
385
385
|
|
|
386
|
-
//
|
|
386
|
+
// Check each deployment option
|
|
387
387
|
for (const option of input.deploymentOptions) {
|
|
388
|
-
//
|
|
388
|
+
// Use system dependency check service to check deployment option
|
|
389
389
|
const result = await mcpSystemDepsCheckService.checkDeployOption(option);
|
|
390
390
|
results.push(result);
|
|
391
391
|
}
|
|
392
392
|
|
|
393
|
-
//
|
|
393
|
+
// Find the recommended or first installable option
|
|
394
394
|
const recommendedResult = results.find((r) => r.isRecommended && r.allDependenciesMet);
|
|
395
395
|
const firstInstallableResult = results.find((r) => r.allDependenciesMet);
|
|
396
396
|
|
|
397
|
-
//
|
|
397
|
+
// Return the recommended result, or the first installable result, or the first result
|
|
398
398
|
const bestResult = recommendedResult || firstInstallableResult || results[0];
|
|
399
399
|
|
|
400
400
|
log('Check completed, best result: %O', bestResult);
|
|
401
401
|
|
|
402
|
-
//
|
|
402
|
+
// Construct return result, ensure configuration check information is included
|
|
403
403
|
const checkResult: CheckMcpInstallResult = {
|
|
404
404
|
...bestResult,
|
|
405
405
|
allOptions: results,
|
|
@@ -407,7 +407,7 @@ export class MCPService {
|
|
|
407
407
|
success: true,
|
|
408
408
|
};
|
|
409
409
|
|
|
410
|
-
//
|
|
410
|
+
// If the best result requires configuration, ensure related fields are set at the top level
|
|
411
411
|
if (bestResult?.needsConfig) {
|
|
412
412
|
checkResult.needsConfig = true;
|
|
413
413
|
checkResult.configSchema = bestResult.configSchema;
|
|
File without changes
|