@lobehub/lobehub 2.0.0-next.293 → 2.0.0-next.294
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/release-desktop-beta.yml +6 -6
- package/.github/workflows/release-desktop-stable.yml +11 -11
- package/CHANGELOG.md +27 -0
- package/apps/desktop/electron.vite.config.ts +0 -1
- package/apps/desktop/src/main/controllers/McpCtr.ts +50 -18
- package/apps/desktop/src/main/libs/mcp/client.ts +54 -2
- package/changelog/v1.json +5 -0
- package/package.json +1 -1
- package/src/app/[variants]/(main)/agent/_layout/AgentIdSync.tsx +12 -1
- package/src/app/[variants]/(main)/group/_layout/GroupIdSync.tsx +12 -1
- package/src/features/MCP/MCPInstallProgress/InstallError/ErrorDetails.tsx +61 -83
- package/src/features/PluginDevModal/MCPManifestForm/index.tsx +30 -3
- package/src/libs/mcp/types.ts +31 -0
- package/src/store/tool/slices/mcpStore/action.ts +26 -11
|
@@ -4,7 +4,7 @@ name: Release Desktop Beta
|
|
|
4
4
|
# Beta/Nightly 频道发版工作流
|
|
5
5
|
# ============================================
|
|
6
6
|
# 触发条件: 发布包含 pre-release 标识的 release
|
|
7
|
-
# 如: v2.0.0-beta.1, v2.0.0-alpha.1, v2.0.0-rc.1, v2.0.0-nightly.xxx
|
|
7
|
+
# 如: v2.0.0-beta.1, v2.0.0-alpha.1, v2.0.0-rc.1, v2.0.0-nightly.xxx, v2.0.0-next.292
|
|
8
8
|
#
|
|
9
9
|
# 注意: Stable 版本 (如 v2.0.0) 由 release-desktop-stable.yml 处理
|
|
10
10
|
# ============================================
|
|
@@ -24,10 +24,10 @@ env:
|
|
|
24
24
|
|
|
25
25
|
jobs:
|
|
26
26
|
# ============================================
|
|
27
|
-
# 检查是否为 Beta/Nightly 版本 (排除 Stable)
|
|
27
|
+
# 检查是否为 Beta/Nightly/Next 版本 (排除 Stable)
|
|
28
28
|
# ============================================
|
|
29
29
|
check-beta:
|
|
30
|
-
name: Check if Beta/Nightly Release
|
|
30
|
+
name: Check if Beta/Nightly/Next Release
|
|
31
31
|
runs-on: ubuntu-latest
|
|
32
32
|
outputs:
|
|
33
33
|
is_beta: ${{ steps.check.outputs.is_beta }}
|
|
@@ -40,10 +40,10 @@ jobs:
|
|
|
40
40
|
version="${version#v}"
|
|
41
41
|
echo "version=${version}" >> $GITHUB_OUTPUT
|
|
42
42
|
|
|
43
|
-
# Beta/Nightly 版本包含 beta/alpha/rc/nightly
|
|
44
|
-
if [[ "$version" == *"beta"* ]] || [[ "$version" == *"alpha"* ]] || [[ "$version" == *"rc"* ]] || [[ "$version" == *"nightly"* ]]; then
|
|
43
|
+
# Beta/Nightly/Next 版本包含 beta/alpha/rc/nightly/next
|
|
44
|
+
if [[ "$version" == *"beta"* ]] || [[ "$version" == *"alpha"* ]] || [[ "$version" == *"rc"* ]] || [[ "$version" == *"nightly"* ]] || [[ "$version" == *"next"* ]]; then
|
|
45
45
|
echo "is_beta=true" >> $GITHUB_OUTPUT
|
|
46
|
-
echo "✅ Beta/Nightly release detected: $version"
|
|
46
|
+
echo "✅ Beta/Nightly/Next release detected: $version"
|
|
47
47
|
else
|
|
48
48
|
echo "is_beta=false" >> $GITHUB_OUTPUT
|
|
49
49
|
echo "⏭️ Skipping: $version is a stable release (handled by release-desktop-stable.yml)"
|
|
@@ -3,10 +3,10 @@ name: Release Desktop Stable
|
|
|
3
3
|
# ============================================
|
|
4
4
|
# Stable 频道发版工作流
|
|
5
5
|
# ============================================
|
|
6
|
-
# 触发条件: 发布不含 pre-release
|
|
6
|
+
# 触发条件: 发布不含 pre-release 后缀的 release (如 v2.0.0)
|
|
7
7
|
#
|
|
8
8
|
# 与 Beta 的区别:
|
|
9
|
-
# 1. 仅响应 stable 版本 tag (
|
|
9
|
+
# 1. 仅响应 stable 版本 tag (不含任何 '-' 后缀)
|
|
10
10
|
# 2. 使用 STABLE 专用的 Umami 配置
|
|
11
11
|
# 3. 额外上传到 S3 更新服务器
|
|
12
12
|
# 4. 构建时注入 UPDATE_SERVER_URL 让客户端从 S3 检查更新
|
|
@@ -89,8 +89,8 @@ jobs:
|
|
|
89
89
|
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
|
90
90
|
# 手动触发: 使用输入的版本号
|
|
91
91
|
version="${{ inputs.version }}"
|
|
92
|
+
version="${version#v}"
|
|
92
93
|
echo "is_manual=true" >> $GITHUB_OUTPUT
|
|
93
|
-
echo "is_stable=true" >> $GITHUB_OUTPUT
|
|
94
94
|
echo "version=${version}" >> $GITHUB_OUTPUT
|
|
95
95
|
echo "release_notes=" >> $GITHUB_OUTPUT
|
|
96
96
|
echo "🔧 Manual trigger: version=${version}"
|
|
@@ -106,15 +106,15 @@ jobs:
|
|
|
106
106
|
printf '%s\n' "$release_body"
|
|
107
107
|
echo "EOF"
|
|
108
108
|
} >> $GITHUB_OUTPUT
|
|
109
|
+
fi
|
|
109
110
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
fi
|
|
111
|
+
# 检查是否为 stable 版本 (不含任何 '-' 后缀)
|
|
112
|
+
if [[ "$version" == *"-"* ]]; then
|
|
113
|
+
echo "is_stable=false" >> $GITHUB_OUTPUT
|
|
114
|
+
echo "⏭️ Skipping: $version is not a stable release"
|
|
115
|
+
else
|
|
116
|
+
echo "is_stable=true" >> $GITHUB_OUTPUT
|
|
117
|
+
echo "✅ Stable release detected: $version"
|
|
118
118
|
fi
|
|
119
119
|
|
|
120
120
|
# ============================================
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
## [Version 2.0.0-next.294](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.293...v2.0.0-next.294)
|
|
6
|
+
|
|
7
|
+
<sup>Released on **2026-01-15**</sup>
|
|
8
|
+
|
|
9
|
+
#### 🐛 Bug Fixes
|
|
10
|
+
|
|
11
|
+
- **chat**: Reset activeTopicId when switching agent/group.
|
|
12
|
+
- **mcp**: Fix installation check hanging issue in desktop app.
|
|
13
|
+
|
|
14
|
+
<br/>
|
|
15
|
+
|
|
16
|
+
<details>
|
|
17
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
18
|
+
|
|
19
|
+
#### What's fixed
|
|
20
|
+
|
|
21
|
+
- **chat**: Reset activeTopicId when switching agent/group, closes [#11523](https://github.com/lobehub/lobe-chat/issues/11523) ([fde54b0](https://github.com/lobehub/lobe-chat/commit/fde54b0))
|
|
22
|
+
- **mcp**: Fix installation check hanging issue in desktop app, closes [#11524](https://github.com/lobehub/lobe-chat/issues/11524) ([b9341c3](https://github.com/lobehub/lobe-chat/commit/b9341c3))
|
|
23
|
+
|
|
24
|
+
</details>
|
|
25
|
+
|
|
26
|
+
<div align="right">
|
|
27
|
+
|
|
28
|
+
[](#readme-top)
|
|
29
|
+
|
|
30
|
+
</div>
|
|
31
|
+
|
|
5
32
|
## [Version 2.0.0-next.293](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.292...v2.0.0-next.293)
|
|
6
33
|
|
|
7
34
|
<sup>Released on **2026-01-15**</sup>
|
|
@@ -7,7 +7,7 @@ import superjson from 'superjson';
|
|
|
7
7
|
import FileService from '@/services/fileSrv';
|
|
8
8
|
import { createLogger } from '@/utils/logger';
|
|
9
9
|
|
|
10
|
-
import { MCPClient } from '../libs/mcp/client';
|
|
10
|
+
import { MCPClient, MCPConnectionError } from '../libs/mcp/client';
|
|
11
11
|
import type { MCPClientParams, ToolCallContent, ToolCallResult } from '../libs/mcp/types';
|
|
12
12
|
import { ControllerModule, IpcMethod } from './index';
|
|
13
13
|
|
|
@@ -228,8 +228,9 @@ export default class McpCtr extends ControllerModule {
|
|
|
228
228
|
type: 'stdio',
|
|
229
229
|
};
|
|
230
230
|
|
|
231
|
-
|
|
231
|
+
let client: MCPClient | undefined;
|
|
232
232
|
try {
|
|
233
|
+
client = await this.createClient(params);
|
|
233
234
|
const manifest = await client.listManifests();
|
|
234
235
|
const identifier = input.name;
|
|
235
236
|
|
|
@@ -257,8 +258,25 @@ export default class McpCtr extends ControllerModule {
|
|
|
257
258
|
mcpParams: params,
|
|
258
259
|
type: 'mcp' as any,
|
|
259
260
|
});
|
|
261
|
+
} catch (error) {
|
|
262
|
+
// If it's an MCPConnectionError with stderr logs, enhance the error message
|
|
263
|
+
if (error instanceof MCPConnectionError && error.stderrLogs.length > 0) {
|
|
264
|
+
const stderrOutput = error.stderrLogs.join('\n');
|
|
265
|
+
const enhancedError = new Error(
|
|
266
|
+
`${error.message}\n\n--- STDIO Process Output ---\n${stderrOutput}`,
|
|
267
|
+
);
|
|
268
|
+
enhancedError.name = error.name;
|
|
269
|
+
logger.error('getStdioMcpServerManifest failed with STDIO logs:', {
|
|
270
|
+
message: error.message,
|
|
271
|
+
stderrLogs: error.stderrLogs,
|
|
272
|
+
});
|
|
273
|
+
throw enhancedError;
|
|
274
|
+
}
|
|
275
|
+
throw error;
|
|
260
276
|
} finally {
|
|
261
|
-
|
|
277
|
+
if (client) {
|
|
278
|
+
await client.disconnect();
|
|
279
|
+
}
|
|
262
280
|
}
|
|
263
281
|
}
|
|
264
282
|
|
|
@@ -313,8 +331,9 @@ export default class McpCtr extends ControllerModule {
|
|
|
313
331
|
type: 'stdio',
|
|
314
332
|
};
|
|
315
333
|
|
|
316
|
-
|
|
334
|
+
let client: MCPClient | undefined;
|
|
317
335
|
try {
|
|
336
|
+
client = await this.createClient(params);
|
|
318
337
|
const args = safeParseToRecord(input.args);
|
|
319
338
|
|
|
320
339
|
const raw = (await client.callTool(input.toolName, args)) as ToolCallResult;
|
|
@@ -328,10 +347,25 @@ export default class McpCtr extends ControllerModule {
|
|
|
328
347
|
success: true,
|
|
329
348
|
});
|
|
330
349
|
} catch (error) {
|
|
350
|
+
// If it's an MCPConnectionError with stderr logs, enhance the error message
|
|
351
|
+
if (error instanceof MCPConnectionError && error.stderrLogs.length > 0) {
|
|
352
|
+
const stderrOutput = error.stderrLogs.join('\n');
|
|
353
|
+
const enhancedError = new Error(
|
|
354
|
+
`${error.message}\n\n--- STDIO Process Output ---\n${stderrOutput}`,
|
|
355
|
+
);
|
|
356
|
+
enhancedError.name = error.name;
|
|
357
|
+
logger.error('callTool failed with STDIO logs:', {
|
|
358
|
+
message: error.message,
|
|
359
|
+
stderrLogs: error.stderrLogs,
|
|
360
|
+
});
|
|
361
|
+
throw enhancedError;
|
|
362
|
+
}
|
|
331
363
|
logger.error('callTool failed:', error);
|
|
332
364
|
throw error;
|
|
333
365
|
} finally {
|
|
334
|
-
|
|
366
|
+
if (client) {
|
|
367
|
+
await client.disconnect();
|
|
368
|
+
}
|
|
335
369
|
}
|
|
336
370
|
}
|
|
337
371
|
|
|
@@ -361,8 +395,9 @@ export default class McpCtr extends ControllerModule {
|
|
|
361
395
|
}
|
|
362
396
|
|
|
363
397
|
private async checkSystemDependency(dependency: any) {
|
|
398
|
+
const checkCommand = dependency.checkCommand || `${dependency.name} --version`;
|
|
399
|
+
|
|
364
400
|
try {
|
|
365
|
-
const checkCommand = dependency.checkCommand || `${dependency.name} --version`;
|
|
366
401
|
const { stdout, stderr } = await execPromise(checkCommand);
|
|
367
402
|
|
|
368
403
|
if (stderr && !stdout) {
|
|
@@ -444,22 +479,19 @@ export default class McpCtr extends ControllerModule {
|
|
|
444
479
|
const packageName = details?.packageName;
|
|
445
480
|
if (!packageName) return { installed: false };
|
|
446
481
|
|
|
482
|
+
// Only check global npm list - do NOT use npx as it may download packages
|
|
447
483
|
try {
|
|
448
484
|
const { stdout } = await execPromise(`npm list -g ${packageName} --depth=0`);
|
|
449
|
-
if (!stdout.includes('(empty)') && stdout.includes(packageName))
|
|
485
|
+
if (!stdout.includes('(empty)') && stdout.includes(packageName)) {
|
|
486
|
+
return { installed: true };
|
|
487
|
+
}
|
|
450
488
|
} catch {
|
|
451
|
-
// ignore
|
|
489
|
+
// ignore - package not found in global list
|
|
452
490
|
}
|
|
453
491
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
} catch (error) {
|
|
458
|
-
return {
|
|
459
|
-
error: error instanceof Error ? error.message : 'Unknown error',
|
|
460
|
-
installed: false,
|
|
461
|
-
};
|
|
462
|
-
}
|
|
492
|
+
// For npm packages, we don't require pre-installation
|
|
493
|
+
// npx will handle downloading and running on-demand during actual MCP connection
|
|
494
|
+
return { installed: false };
|
|
463
495
|
}
|
|
464
496
|
|
|
465
497
|
if (installationMethod === 'python') {
|
|
@@ -553,7 +585,7 @@ export default class McpCtr extends ControllerModule {
|
|
|
553
585
|
const bestResult = recommendedResult || firstInstallableResult || results[0];
|
|
554
586
|
|
|
555
587
|
const checkResult: CheckMcpInstallResult = {
|
|
556
|
-
...
|
|
588
|
+
...bestResult,
|
|
557
589
|
allOptions: results as any,
|
|
558
590
|
platform: process.platform,
|
|
559
591
|
success: true,
|
|
@@ -6,15 +6,31 @@ import {
|
|
|
6
6
|
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
7
7
|
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
|
8
8
|
import type { Progress } from '@modelcontextprotocol/sdk/types.js';
|
|
9
|
+
import type { Readable } from 'node:stream';
|
|
9
10
|
|
|
10
11
|
import { getDesktopEnv } from '@/env';
|
|
11
12
|
|
|
12
13
|
import type { MCPClientParams, McpPrompt, McpResource, McpTool, ToolCallResult } from './types';
|
|
13
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Custom error class for MCP connection errors that includes STDIO logs
|
|
17
|
+
*/
|
|
18
|
+
export class MCPConnectionError extends Error {
|
|
19
|
+
readonly stderrLogs: string[];
|
|
20
|
+
|
|
21
|
+
constructor(message: string, stderrLogs: string[] = []) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.name = 'MCPConnectionError';
|
|
24
|
+
this.stderrLogs = stderrLogs;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
14
28
|
export class MCPClient {
|
|
15
29
|
private readonly mcp: Client;
|
|
16
30
|
|
|
17
31
|
private transport: Transport;
|
|
32
|
+
private stderrLogs: string[] = [];
|
|
33
|
+
private isStdio: boolean = false;
|
|
18
34
|
|
|
19
35
|
constructor(params: MCPClientParams) {
|
|
20
36
|
this.mcp = new Client({ name: 'lobehub-desktop-mcp-client', version: '1.0.0' });
|
|
@@ -40,14 +56,21 @@ export class MCPClient {
|
|
|
40
56
|
}
|
|
41
57
|
|
|
42
58
|
case 'stdio': {
|
|
43
|
-
this.
|
|
59
|
+
this.isStdio = true;
|
|
60
|
+
const stdioTransport = new StdioClientTransport({
|
|
44
61
|
args: params.args,
|
|
45
62
|
command: params.command,
|
|
46
63
|
env: {
|
|
47
64
|
...getDefaultEnvironment(),
|
|
48
65
|
...params.env,
|
|
49
66
|
},
|
|
67
|
+
stderr: 'pipe', // Capture stderr for better error messages
|
|
50
68
|
});
|
|
69
|
+
|
|
70
|
+
// Listen to stderr stream to collect logs
|
|
71
|
+
this.setupStderrListener(stdioTransport);
|
|
72
|
+
|
|
73
|
+
this.transport = stdioTransport;
|
|
51
74
|
break;
|
|
52
75
|
}
|
|
53
76
|
|
|
@@ -60,16 +83,45 @@ export class MCPClient {
|
|
|
60
83
|
}
|
|
61
84
|
}
|
|
62
85
|
|
|
86
|
+
private setupStderrListener(transport: StdioClientTransport) {
|
|
87
|
+
const stderr = transport.stderr as Readable | null;
|
|
88
|
+
if (stderr) {
|
|
89
|
+
stderr.on('data', (chunk: Buffer) => {
|
|
90
|
+
const text = chunk.toString('utf8');
|
|
91
|
+
// Split by newlines and filter empty lines
|
|
92
|
+
const lines = text.split('\n').filter((line) => line.trim());
|
|
93
|
+
this.stderrLogs.push(...lines);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Get collected stderr logs from the STDIO process
|
|
100
|
+
*/
|
|
101
|
+
getStderrLogs(): string[] {
|
|
102
|
+
return this.stderrLogs;
|
|
103
|
+
}
|
|
104
|
+
|
|
63
105
|
private isMethodNotFoundError(error: unknown) {
|
|
64
106
|
const err = error as any;
|
|
65
107
|
if (!err) return false;
|
|
108
|
+
// eslint-disable-next-line unicorn/numeric-separators-style
|
|
66
109
|
if (err.code === -32601) return true;
|
|
67
110
|
if (typeof err.message === 'string' && err.message.includes('Method not found')) return true;
|
|
68
111
|
return false;
|
|
69
112
|
}
|
|
70
113
|
|
|
71
114
|
async initialize(options: { onProgress?: (progress: Progress) => void } = {}) {
|
|
72
|
-
|
|
115
|
+
try {
|
|
116
|
+
await this.mcp.connect(this.transport, { onprogress: options.onProgress });
|
|
117
|
+
} catch (error) {
|
|
118
|
+
// If this is a STDIO connection and we have stderr logs, enhance the error
|
|
119
|
+
if (this.isStdio && this.stderrLogs.length > 0) {
|
|
120
|
+
const originalMessage = error instanceof Error ? error.message : String(error);
|
|
121
|
+
throw new MCPConnectionError(originalMessage, this.stderrLogs);
|
|
122
|
+
}
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
73
125
|
}
|
|
74
126
|
|
|
75
127
|
async disconnect() {
|
package/changelog/v1.json
CHANGED
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.294",
|
|
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",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { useUnmount } from 'ahooks';
|
|
1
|
+
import { usePrevious, useUnmount } from 'ahooks';
|
|
2
|
+
import { useEffect } from 'react';
|
|
2
3
|
import { useParams } from 'react-router-dom';
|
|
3
4
|
import { createStoreUpdater } from 'zustand-utils';
|
|
4
5
|
|
|
@@ -9,10 +10,20 @@ const AgentIdSync = () => {
|
|
|
9
10
|
const useStoreUpdater = createStoreUpdater(useAgentStore);
|
|
10
11
|
const useChatStoreUpdater = createStoreUpdater(useChatStore);
|
|
11
12
|
const params = useParams<{ aid?: string }>();
|
|
13
|
+
const prevAgentId = usePrevious(params.aid);
|
|
12
14
|
|
|
13
15
|
useStoreUpdater('activeAgentId', params.aid);
|
|
14
16
|
useChatStoreUpdater('activeAgentId', params.aid ?? '');
|
|
15
17
|
|
|
18
|
+
// Reset activeTopicId when switching to a different agent
|
|
19
|
+
// This prevents messages from being saved to the wrong topic bucket
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
// Only reset topic when switching between agents (not on initial mount)
|
|
22
|
+
if (prevAgentId !== undefined && prevAgentId !== params.aid) {
|
|
23
|
+
useChatStore.getState().switchTopic(null, { skipRefreshMessage: true });
|
|
24
|
+
}
|
|
25
|
+
}, [params.aid, prevAgentId]);
|
|
26
|
+
|
|
16
27
|
// Clear activeAgentId when unmounting (leaving chat page)
|
|
17
28
|
useUnmount(() => {
|
|
18
29
|
useAgentStore.setState({ activeAgentId: undefined });
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { useUnmount } from 'ahooks';
|
|
1
|
+
import { usePrevious, useUnmount } from 'ahooks';
|
|
2
|
+
import { useEffect } from 'react';
|
|
2
3
|
import { useParams } from 'react-router-dom';
|
|
3
4
|
import { createStoreUpdater } from 'zustand-utils';
|
|
4
5
|
|
|
@@ -10,6 +11,7 @@ const GroupIdSync = () => {
|
|
|
10
11
|
const useAgentGroupStoreUpdater = createStoreUpdater(useAgentGroupStore);
|
|
11
12
|
const useChatStoreUpdater = createStoreUpdater(useChatStore);
|
|
12
13
|
const params = useParams<{ gid?: string }>();
|
|
14
|
+
const prevGroupId = usePrevious(params.gid);
|
|
13
15
|
const router = useQueryRoute();
|
|
14
16
|
|
|
15
17
|
// Sync groupId to agentGroupStore and chatStore
|
|
@@ -19,6 +21,15 @@ const GroupIdSync = () => {
|
|
|
19
21
|
// Inject router to agentGroupStore for navigation
|
|
20
22
|
useAgentGroupStoreUpdater('router', router);
|
|
21
23
|
|
|
24
|
+
// Reset activeTopicId when switching to a different group
|
|
25
|
+
// This prevents messages from being saved to the wrong topic bucket
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
// Only reset topic when switching between groups (not on initial mount)
|
|
28
|
+
if (prevGroupId !== undefined && prevGroupId !== params.gid) {
|
|
29
|
+
useChatStore.getState().switchTopic(null, { skipRefreshMessage: true });
|
|
30
|
+
}
|
|
31
|
+
}, [params.gid, prevGroupId]);
|
|
32
|
+
|
|
22
33
|
// Clear activeGroupId when unmounting (leaving group page)
|
|
23
34
|
useUnmount(() => {
|
|
24
35
|
useAgentGroupStore.setState({ activeGroupId: undefined, router: undefined });
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Flexbox, Highlighter, Tag } from '@lobehub/ui';
|
|
2
2
|
import { cssVar } from 'antd-style';
|
|
3
|
-
import { ChevronDown, ChevronRight } from 'lucide-react';
|
|
4
3
|
import * as motion from 'motion/react-m';
|
|
5
|
-
import { memo
|
|
4
|
+
import { memo } from 'react';
|
|
6
5
|
import { useTranslation } from 'react-i18next';
|
|
7
6
|
|
|
8
7
|
import { type MCPErrorInfoMetadata } from '@/types/plugins';
|
|
@@ -12,94 +11,73 @@ const ErrorDetails = memo<{
|
|
|
12
11
|
errorMessage?: string;
|
|
13
12
|
}>(({ errorInfo, errorMessage }) => {
|
|
14
13
|
const { t } = useTranslation('plugin');
|
|
15
|
-
const [expanded, setExpanded] = useState(false);
|
|
16
14
|
|
|
17
15
|
return (
|
|
18
16
|
<Flexbox gap={8}>
|
|
19
|
-
<
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
size="small"
|
|
24
|
-
style={{
|
|
25
|
-
fontSize: '12px',
|
|
26
|
-
padding: '0 4px',
|
|
27
|
-
}}
|
|
28
|
-
variant="filled"
|
|
17
|
+
<motion.div
|
|
18
|
+
animate={{ height: 'auto', opacity: 1 }}
|
|
19
|
+
initial={{ height: 0, opacity: 0 }}
|
|
20
|
+
style={{ overflow: 'hidden' }}
|
|
29
21
|
>
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
style={{ overflow: 'hidden' }}
|
|
22
|
+
<Flexbox
|
|
23
|
+
gap={8}
|
|
24
|
+
style={{
|
|
25
|
+
backgroundColor: cssVar.colorFillQuaternary,
|
|
26
|
+
borderRadius: 8,
|
|
27
|
+
fontFamily: 'monospace',
|
|
28
|
+
fontSize: '11px',
|
|
29
|
+
padding: '8px 12px',
|
|
30
|
+
}}
|
|
40
31
|
>
|
|
41
|
-
|
|
42
|
-
gap={
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
</div>
|
|
63
|
-
)}
|
|
64
|
-
{errorInfo.params.args && (
|
|
65
|
-
<div>
|
|
66
|
-
{t('mcpInstall.errorDetails.args')}: {errorInfo.params.args.join(' ')}
|
|
67
|
-
</div>
|
|
68
|
-
)}
|
|
69
|
-
</div>
|
|
70
|
-
</Flexbox>
|
|
71
|
-
)}
|
|
72
|
-
|
|
73
|
-
{errorInfo.errorLog && (
|
|
74
|
-
<Flexbox gap={4}>
|
|
75
|
-
<div>
|
|
76
|
-
<Tag color="red" variant={'filled'}>
|
|
77
|
-
{t('mcpInstall.errorDetails.errorOutput')}
|
|
78
|
-
</Tag>
|
|
79
|
-
</div>
|
|
80
|
-
<Highlighter
|
|
81
|
-
language={'log'}
|
|
82
|
-
style={{
|
|
83
|
-
maxHeight: 200,
|
|
84
|
-
overflow: 'auto',
|
|
85
|
-
}}
|
|
86
|
-
>
|
|
87
|
-
{errorInfo.errorLog}
|
|
88
|
-
</Highlighter>
|
|
89
|
-
</Flexbox>
|
|
90
|
-
)}
|
|
32
|
+
{errorInfo.params && (
|
|
33
|
+
<Flexbox gap={4}>
|
|
34
|
+
<div>
|
|
35
|
+
<Tag color="blue" variant={'filled'}>
|
|
36
|
+
{t('mcpInstall.errorDetails.connectionParams')}
|
|
37
|
+
</Tag>
|
|
38
|
+
</div>
|
|
39
|
+
<div style={{ marginTop: 4, wordBreak: 'break-all' }}>
|
|
40
|
+
{errorInfo.params.command && (
|
|
41
|
+
<div>
|
|
42
|
+
{t('mcpInstall.errorDetails.command')}: {errorInfo.params.command}
|
|
43
|
+
</div>
|
|
44
|
+
)}
|
|
45
|
+
{errorInfo.params.args && (
|
|
46
|
+
<div>
|
|
47
|
+
{t('mcpInstall.errorDetails.args')}: {errorInfo.params.args.join(' ')}
|
|
48
|
+
</div>
|
|
49
|
+
)}
|
|
50
|
+
</div>
|
|
51
|
+
</Flexbox>
|
|
52
|
+
)}
|
|
91
53
|
|
|
92
|
-
|
|
54
|
+
{errorInfo.errorLog && (
|
|
55
|
+
<Flexbox gap={4}>
|
|
93
56
|
<div>
|
|
94
|
-
<Tag color="
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
</div>
|
|
57
|
+
<Tag color="red" variant={'filled'}>
|
|
58
|
+
{t('mcpInstall.errorDetails.errorOutput')}
|
|
59
|
+
</Tag>
|
|
98
60
|
</div>
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
61
|
+
<Highlighter
|
|
62
|
+
language={'log'}
|
|
63
|
+
style={{
|
|
64
|
+
maxHeight: 200,
|
|
65
|
+
overflow: 'auto',
|
|
66
|
+
}}
|
|
67
|
+
>
|
|
68
|
+
{errorInfo.errorLog}
|
|
69
|
+
</Highlighter>
|
|
70
|
+
</Flexbox>
|
|
71
|
+
)}
|
|
72
|
+
|
|
73
|
+
{errorInfo.originalError && errorInfo.originalError !== errorMessage && (
|
|
74
|
+
<div>
|
|
75
|
+
<Tag color="orange">{t('mcpInstall.errorDetails.originalError')}</Tag>
|
|
76
|
+
<div style={{ marginTop: 4, wordBreak: 'break-all' }}>{errorInfo.originalError}</div>
|
|
77
|
+
</div>
|
|
78
|
+
)}
|
|
79
|
+
</Flexbox>
|
|
80
|
+
</motion.div>
|
|
103
81
|
</Flexbox>
|
|
104
82
|
);
|
|
105
83
|
});
|
|
@@ -6,8 +6,10 @@ import { useTranslation } from 'react-i18next';
|
|
|
6
6
|
|
|
7
7
|
import KeyValueEditor from '@/components/KeyValueEditor';
|
|
8
8
|
import MCPStdioCommandInput from '@/components/MCPStdioCommandInput';
|
|
9
|
+
import ErrorDetails from '@/features/MCP/MCPInstallProgress/InstallError/ErrorDetails';
|
|
9
10
|
import { useToolStore } from '@/store/tool';
|
|
10
11
|
import { mcpStoreSelectors, pluginSelectors } from '@/store/tool/selectors';
|
|
12
|
+
import { type MCPErrorInfoMetadata } from '@/types/plugins';
|
|
11
13
|
|
|
12
14
|
import ArgsInput from './ArgsInput';
|
|
13
15
|
import CollapsibleSection from './CollapsibleSection';
|
|
@@ -46,10 +48,12 @@ const MCPManifestForm = ({ form, isEditMode }: MCPManifestFormProps) => {
|
|
|
46
48
|
const testState = useToolStore(mcpStoreSelectors.getMCPConnectionTestState(identifier), isEqual);
|
|
47
49
|
|
|
48
50
|
const [connectionError, setConnectionError] = useState<string | null>(null);
|
|
51
|
+
const [errorMetadata, setErrorMetadata] = useState<MCPErrorInfoMetadata | null>(null);
|
|
49
52
|
|
|
50
53
|
const handleTestConnection = async () => {
|
|
51
54
|
setIsTesting(true);
|
|
52
55
|
setConnectionError(null);
|
|
56
|
+
setErrorMetadata(null);
|
|
53
57
|
|
|
54
58
|
// Manually trigger validation for fields needed for the test
|
|
55
59
|
let isValid = false;
|
|
@@ -97,12 +101,29 @@ const MCPManifestForm = ({ form, isEditMode }: MCPManifestFormProps) => {
|
|
|
97
101
|
// Be careful about overwriting user input if not desired
|
|
98
102
|
form.setFieldsValue({ manifest: result.manifest });
|
|
99
103
|
setConnectionError(null); // 清除本地错误状态
|
|
104
|
+
setErrorMetadata(null);
|
|
100
105
|
} else if (result.error) {
|
|
101
106
|
// Store 已经处理了错误状态,这里可以选择显示额外的用户友好提示
|
|
102
107
|
const errorMessage = t('error.testConnectionFailed', {
|
|
103
108
|
error: result.error,
|
|
104
109
|
});
|
|
105
110
|
setConnectionError(errorMessage);
|
|
111
|
+
|
|
112
|
+
// Build error metadata for detailed display
|
|
113
|
+
if (result.errorLog || mcpType === 'stdio') {
|
|
114
|
+
setErrorMetadata({
|
|
115
|
+
errorLog: result.errorLog,
|
|
116
|
+
params:
|
|
117
|
+
mcpType === 'stdio'
|
|
118
|
+
? {
|
|
119
|
+
args: mcp?.args,
|
|
120
|
+
command: mcp?.command,
|
|
121
|
+
type: 'stdio',
|
|
122
|
+
}
|
|
123
|
+
: undefined,
|
|
124
|
+
timestamp: Date.now(),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
106
127
|
}
|
|
107
128
|
} catch (error) {
|
|
108
129
|
// Handle unexpected errors
|
|
@@ -121,7 +142,10 @@ const MCPManifestForm = ({ form, isEditMode }: MCPManifestFormProps) => {
|
|
|
121
142
|
<QuickImportSection
|
|
122
143
|
form={form}
|
|
123
144
|
isEditMode={isEditMode}
|
|
124
|
-
onClearConnectionError={() =>
|
|
145
|
+
onClearConnectionError={() => {
|
|
146
|
+
setConnectionError(null);
|
|
147
|
+
setErrorMetadata(null);
|
|
148
|
+
}}
|
|
125
149
|
/>
|
|
126
150
|
<Form form={form} layout={'vertical'}>
|
|
127
151
|
<Flexbox>
|
|
@@ -270,9 +294,12 @@ const MCPManifestForm = ({ form, isEditMode }: MCPManifestFormProps) => {
|
|
|
270
294
|
{(connectionError || testState.error) && (
|
|
271
295
|
<Alert
|
|
272
296
|
closable
|
|
273
|
-
|
|
297
|
+
extra={errorMetadata ? <ErrorDetails errorInfo={errorMetadata} /> : undefined}
|
|
298
|
+
onClose={() => {
|
|
299
|
+
setConnectionError(null);
|
|
300
|
+
setErrorMetadata(null);
|
|
301
|
+
}}
|
|
274
302
|
showIcon
|
|
275
|
-
style={{ marginBottom: 16 }}
|
|
276
303
|
title={connectionError || testState.error}
|
|
277
304
|
type="error"
|
|
278
305
|
/>
|
package/src/libs/mcp/types.ts
CHANGED
|
@@ -240,3 +240,34 @@ export function createMCPError(
|
|
|
240
240
|
|
|
241
241
|
return error;
|
|
242
242
|
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* STDIO Process Output separator used in enhanced error messages
|
|
246
|
+
*/
|
|
247
|
+
const STDIO_OUTPUT_SEPARATOR = '--- STDIO Process Output ---';
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Parse error message to extract STDIO process output logs
|
|
251
|
+
* The enhanced error format from desktop is:
|
|
252
|
+
* "Original message\n\n--- STDIO Process Output ---\nlogs..."
|
|
253
|
+
*/
|
|
254
|
+
export interface ParsedStdioError {
|
|
255
|
+
errorLog?: string;
|
|
256
|
+
originalMessage: string;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function parseStdioErrorMessage(errorMessage: string): ParsedStdioError {
|
|
260
|
+
const separatorIndex = errorMessage.indexOf(STDIO_OUTPUT_SEPARATOR);
|
|
261
|
+
|
|
262
|
+
if (separatorIndex === -1) {
|
|
263
|
+
return { originalMessage: errorMessage };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const originalMessage = errorMessage.slice(0, separatorIndex).trim();
|
|
267
|
+
const errorLog = errorMessage.slice(separatorIndex + STDIO_OUTPUT_SEPARATOR.length).trim();
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
errorLog: errorLog || undefined,
|
|
271
|
+
originalMessage: originalMessage || errorMessage,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
@@ -9,7 +9,7 @@ import { gt, valid } from 'semver';
|
|
|
9
9
|
import useSWR, { type SWRResponse } from 'swr';
|
|
10
10
|
import { type StateCreator } from 'zustand/vanilla';
|
|
11
11
|
|
|
12
|
-
import { type MCPErrorData } from '@/libs/mcp/types';
|
|
12
|
+
import { type MCPErrorData, parseStdioErrorMessage } from '@/libs/mcp/types';
|
|
13
13
|
import { discoverService } from '@/services/discover';
|
|
14
14
|
import { mcpService } from '@/services/mcp';
|
|
15
15
|
import { pluginService } from '@/services/plugin';
|
|
@@ -132,6 +132,8 @@ const buildCloudMcpManifest = (params: {
|
|
|
132
132
|
// Test connection result type
|
|
133
133
|
export interface TestMcpConnectionResult {
|
|
134
134
|
error?: string;
|
|
135
|
+
/** STDIO process output logs for debugging */
|
|
136
|
+
errorLog?: string;
|
|
135
137
|
manifest?: LobeChatPluginManifest;
|
|
136
138
|
success: boolean;
|
|
137
139
|
}
|
|
@@ -301,8 +303,6 @@ export const createMCPPluginStoreSlice: StateCreator<
|
|
|
301
303
|
// Check if cloudEndPoint is available: web + stdio type + haveCloudEndpoint exists
|
|
302
304
|
const hasCloudEndpoint = !isDesktop && stdioOption && haveCloudEndpoint;
|
|
303
305
|
|
|
304
|
-
console.log('hasCloudEndpoint', hasCloudEndpoint);
|
|
305
|
-
|
|
306
306
|
let shouldUseHttpDeployment = !!httpOption && (!hasNonHttpDeployment || !isDesktop);
|
|
307
307
|
|
|
308
308
|
if (hasCloudEndpoint) {
|
|
@@ -592,7 +592,7 @@ export const createMCPPluginStoreSlice: StateCreator<
|
|
|
592
592
|
event: 'install',
|
|
593
593
|
identifier: plugin.identifier,
|
|
594
594
|
source: 'self',
|
|
595
|
-
})
|
|
595
|
+
});
|
|
596
596
|
|
|
597
597
|
discoverService.reportMcpInstallResult({
|
|
598
598
|
identifier: plugin.identifier,
|
|
@@ -653,10 +653,22 @@ export const createMCPPluginStoreSlice: StateCreator<
|
|
|
653
653
|
};
|
|
654
654
|
} else {
|
|
655
655
|
// Fallback handling for normal errors
|
|
656
|
-
const
|
|
656
|
+
const rawErrorMessage = error instanceof Error ? error.message : String(error);
|
|
657
|
+
|
|
658
|
+
// Parse STDIO error message to extract process output logs
|
|
659
|
+
const { originalMessage, errorLog } = parseStdioErrorMessage(rawErrorMessage);
|
|
660
|
+
|
|
657
661
|
errorInfo = {
|
|
658
|
-
message:
|
|
662
|
+
message: originalMessage,
|
|
659
663
|
metadata: {
|
|
664
|
+
errorLog,
|
|
665
|
+
params: connection
|
|
666
|
+
? {
|
|
667
|
+
args: connection.args,
|
|
668
|
+
command: connection.command,
|
|
669
|
+
type: connection.type,
|
|
670
|
+
}
|
|
671
|
+
: undefined,
|
|
660
672
|
step: 'installation_error',
|
|
661
673
|
timestamp: Date.now(),
|
|
662
674
|
},
|
|
@@ -800,7 +812,7 @@ export const createMCPPluginStoreSlice: StateCreator<
|
|
|
800
812
|
event: 'activate',
|
|
801
813
|
identifier: identifier,
|
|
802
814
|
source: 'self',
|
|
803
|
-
})
|
|
815
|
+
});
|
|
804
816
|
|
|
805
817
|
return { manifest, success: true };
|
|
806
818
|
} catch (error) {
|
|
@@ -809,20 +821,23 @@ export const createMCPPluginStoreSlice: StateCreator<
|
|
|
809
821
|
return { error: 'Test cancelled', success: false };
|
|
810
822
|
}
|
|
811
823
|
|
|
812
|
-
const
|
|
824
|
+
const rawErrorMessage = error instanceof Error ? error.message : String(error);
|
|
825
|
+
|
|
826
|
+
// Parse STDIO error message to extract process output logs
|
|
827
|
+
const { originalMessage, errorLog } = parseStdioErrorMessage(rawErrorMessage);
|
|
813
828
|
|
|
814
829
|
// Set error state
|
|
815
830
|
set(
|
|
816
831
|
produce((draft: MCPStoreState) => {
|
|
817
832
|
draft.mcpTestLoading[identifier] = false;
|
|
818
|
-
draft.mcpTestErrors[identifier] =
|
|
833
|
+
draft.mcpTestErrors[identifier] = originalMessage;
|
|
819
834
|
delete draft.mcpTestAbortControllers[identifier];
|
|
820
835
|
}),
|
|
821
836
|
false,
|
|
822
837
|
n('testMcpConnection/error'),
|
|
823
838
|
);
|
|
824
839
|
|
|
825
|
-
return { error:
|
|
840
|
+
return { error: originalMessage, errorLog, success: false };
|
|
826
841
|
}
|
|
827
842
|
},
|
|
828
843
|
|
|
@@ -834,7 +849,7 @@ export const createMCPPluginStoreSlice: StateCreator<
|
|
|
834
849
|
event: 'uninstall',
|
|
835
850
|
identifier: identifier,
|
|
836
851
|
source: 'self',
|
|
837
|
-
})
|
|
852
|
+
});
|
|
838
853
|
},
|
|
839
854
|
|
|
840
855
|
updateMCPInstallProgress: (identifier, progress) => {
|