@lobehub/lobehub 2.0.0-next.292 → 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 +22 -11
- package/CHANGELOG.md +52 -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 +10 -0
- package/package.json +1 -1
- package/packages/const/src/cacheControl.ts +1 -0
- package/src/app/(backend)/api/desktop/latest/route.ts +115 -0
- package/src/app/(backend)/middleware/validate/createValidator.test.ts +61 -0
- package/src/app/(backend)/middleware/validate/createValidator.ts +79 -0
- package/src/app/(backend)/middleware/validate/index.ts +3 -0
- 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/server/services/desktopRelease/index.test.ts +65 -0
- package/src/server/services/desktopRelease/index.ts +208 -0
- package/src/store/tool/slices/mcpStore/action.ts +26 -11
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import urlJoin from 'url-join';
|
|
2
|
+
import { parse } from 'yaml';
|
|
3
|
+
|
|
4
|
+
import { FetchCacheTag } from '@/const/cacheControl';
|
|
5
|
+
|
|
6
|
+
export type DesktopDownloadType = 'linux' | 'mac-arm' | 'mac-intel' | 'windows';
|
|
7
|
+
|
|
8
|
+
export interface DesktopDownloadInfo {
|
|
9
|
+
assetName: string;
|
|
10
|
+
publishedAt?: string;
|
|
11
|
+
tag: string;
|
|
12
|
+
type: DesktopDownloadType;
|
|
13
|
+
url: string;
|
|
14
|
+
version: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type GithubReleaseAsset = {
|
|
18
|
+
browser_download_url: string;
|
|
19
|
+
name: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type GithubRelease = {
|
|
23
|
+
assets: GithubReleaseAsset[];
|
|
24
|
+
published_at?: string;
|
|
25
|
+
tag_name: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type UpdateServerManifestFile = {
|
|
29
|
+
url: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
type UpdateServerManifest = {
|
|
33
|
+
files?: UpdateServerManifestFile[];
|
|
34
|
+
path?: string;
|
|
35
|
+
releaseDate?: string;
|
|
36
|
+
version?: string;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const getBasename = (pathname: string) => {
|
|
40
|
+
const cleaned = pathname.split('?')[0] || '';
|
|
41
|
+
const lastSlash = cleaned.lastIndexOf('/');
|
|
42
|
+
return lastSlash >= 0 ? cleaned.slice(lastSlash + 1) : cleaned;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const isAbsoluteUrl = (value: string) => /^https?:\/\//i.test(value);
|
|
46
|
+
|
|
47
|
+
const buildTypeMatchers = (type: DesktopDownloadType) => {
|
|
48
|
+
switch (type) {
|
|
49
|
+
case 'mac-arm': {
|
|
50
|
+
return [/-arm64\.dmg$/i, /-arm64-mac\.zip$/i, /-arm64\.zip$/i, /\.dmg$/i, /\.zip$/i];
|
|
51
|
+
}
|
|
52
|
+
case 'mac-intel': {
|
|
53
|
+
return [/-x64\.dmg$/i, /-x64-mac\.zip$/i, /-x64\.zip$/i, /\.dmg$/i, /\.zip$/i];
|
|
54
|
+
}
|
|
55
|
+
case 'windows': {
|
|
56
|
+
return [/-setup\.exe$/i, /\.exe$/i];
|
|
57
|
+
}
|
|
58
|
+
case 'linux': {
|
|
59
|
+
return [/\.appimage$/i, /\.deb$/i, /\.rpm$/i, /\.snap$/i, /\.tar\.gz$/i];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const resolveDesktopDownloadFromUrls = (options: {
|
|
65
|
+
publishedAt?: string;
|
|
66
|
+
tag: string;
|
|
67
|
+
type: DesktopDownloadType;
|
|
68
|
+
urls: string[];
|
|
69
|
+
version: string;
|
|
70
|
+
}): DesktopDownloadInfo | null => {
|
|
71
|
+
const matchers = buildTypeMatchers(options.type);
|
|
72
|
+
|
|
73
|
+
const matchedUrl = matchers
|
|
74
|
+
.map((matcher) => options.urls.find((url) => matcher.test(getBasename(url))))
|
|
75
|
+
.find(Boolean);
|
|
76
|
+
|
|
77
|
+
if (!matchedUrl) return null;
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
assetName: getBasename(matchedUrl),
|
|
81
|
+
publishedAt: options.publishedAt,
|
|
82
|
+
tag: options.tag,
|
|
83
|
+
type: options.type,
|
|
84
|
+
url: matchedUrl,
|
|
85
|
+
version: options.version,
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export const resolveDesktopDownload = (
|
|
90
|
+
release: GithubRelease,
|
|
91
|
+
type: DesktopDownloadType,
|
|
92
|
+
): DesktopDownloadInfo | null => {
|
|
93
|
+
const tag = release.tag_name;
|
|
94
|
+
const version = tag.replace(/^v/i, '');
|
|
95
|
+
const matchers = buildTypeMatchers(type);
|
|
96
|
+
|
|
97
|
+
const matchedAsset = matchers
|
|
98
|
+
.map((matcher) => release.assets.find((asset) => matcher.test(asset.name)))
|
|
99
|
+
.find(Boolean);
|
|
100
|
+
|
|
101
|
+
if (!matchedAsset) return null;
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
assetName: matchedAsset.name,
|
|
105
|
+
publishedAt: release.published_at,
|
|
106
|
+
tag,
|
|
107
|
+
type,
|
|
108
|
+
url: matchedAsset.browser_download_url,
|
|
109
|
+
version,
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export const getLatestDesktopReleaseFromGithub = async (options?: {
|
|
114
|
+
owner?: string;
|
|
115
|
+
repo?: string;
|
|
116
|
+
token?: string;
|
|
117
|
+
}): Promise<GithubRelease> => {
|
|
118
|
+
const owner = options?.owner || 'lobehub';
|
|
119
|
+
const repo = options?.repo || 'lobe-chat';
|
|
120
|
+
const token = options?.token || process.env.GITHUB_TOKEN;
|
|
121
|
+
|
|
122
|
+
const res = await fetch(`https://api.github.com/repos/${owner}/${repo}/releases/latest`, {
|
|
123
|
+
headers: {
|
|
124
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
125
|
+
'Accept': 'application/vnd.github+json',
|
|
126
|
+
'User-Agent': 'lobehub-server',
|
|
127
|
+
},
|
|
128
|
+
next: { revalidate: 300, tags: [FetchCacheTag.DesktopRelease] },
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
if (!res.ok) {
|
|
132
|
+
const text = await res.text().catch(() => '');
|
|
133
|
+
throw new Error(`GitHub releases/latest request failed: ${res.status} ${text}`.trim());
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return (await res.json()) as GithubRelease;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const fetchUpdateServerManifest = async (
|
|
140
|
+
baseUrl: string,
|
|
141
|
+
manifestName: string,
|
|
142
|
+
): Promise<UpdateServerManifest> => {
|
|
143
|
+
const res = await fetch(urlJoin(baseUrl, manifestName), {
|
|
144
|
+
next: { revalidate: 300, tags: [FetchCacheTag.DesktopRelease] },
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
if (!res.ok) {
|
|
148
|
+
const text = await res.text().catch(() => '');
|
|
149
|
+
throw new Error(`Update server manifest request failed: ${res.status} ${text}`.trim());
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const text = await res.text();
|
|
153
|
+
return (parse(text) || {}) as UpdateServerManifest;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const normalizeManifestUrls = (baseUrl: string, manifest: UpdateServerManifest) => {
|
|
157
|
+
const urls: string[] = [];
|
|
158
|
+
|
|
159
|
+
for (const file of manifest.files || []) {
|
|
160
|
+
if (!file?.url) continue;
|
|
161
|
+
urls.push(isAbsoluteUrl(file.url) ? file.url : urlJoin(baseUrl, file.url));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (manifest.path) {
|
|
165
|
+
urls.push(isAbsoluteUrl(manifest.path) ? manifest.path : urlJoin(baseUrl, manifest.path));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return urls;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
export const getStableDesktopReleaseInfoFromUpdateServer = async (options?: {
|
|
172
|
+
baseUrl?: string;
|
|
173
|
+
}): Promise<{ publishedAt?: string; tag: string; urls: string[]; version: string } | null> => {
|
|
174
|
+
const baseUrl =
|
|
175
|
+
options?.baseUrl || process.env.DESKTOP_UPDATE_SERVER_URL || process.env.UPDATE_SERVER_URL;
|
|
176
|
+
if (!baseUrl) return null;
|
|
177
|
+
|
|
178
|
+
const [mac, win, linux] = await Promise.all([
|
|
179
|
+
fetchUpdateServerManifest(baseUrl, 'stable-mac.yml').catch(() => null),
|
|
180
|
+
fetchUpdateServerManifest(baseUrl, 'stable.yml').catch(() => null),
|
|
181
|
+
fetchUpdateServerManifest(baseUrl, 'stable-linux.yml').catch(() => null),
|
|
182
|
+
]);
|
|
183
|
+
|
|
184
|
+
const manifests = [mac, win, linux].filter(Boolean) as UpdateServerManifest[];
|
|
185
|
+
const version = manifests.map((m) => m.version).find(Boolean) || '';
|
|
186
|
+
if (!version) return null;
|
|
187
|
+
|
|
188
|
+
const tag = `v${version.replace(/^v/i, '')}`;
|
|
189
|
+
const publishedAt = manifests.map((m) => m.releaseDate).find(Boolean);
|
|
190
|
+
|
|
191
|
+
const urls = [
|
|
192
|
+
...(mac ? normalizeManifestUrls(baseUrl, mac) : []),
|
|
193
|
+
...(win ? normalizeManifestUrls(baseUrl, win) : []),
|
|
194
|
+
...(linux ? normalizeManifestUrls(baseUrl, linux) : []),
|
|
195
|
+
];
|
|
196
|
+
|
|
197
|
+
return { publishedAt, tag, urls, version: version.replace(/^v/i, '') };
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
export const resolveDesktopDownloadFromUpdateServer = async (options: {
|
|
201
|
+
baseUrl?: string;
|
|
202
|
+
type: DesktopDownloadType;
|
|
203
|
+
}): Promise<DesktopDownloadInfo | null> => {
|
|
204
|
+
const info = await getStableDesktopReleaseInfoFromUpdateServer({ baseUrl: options.baseUrl });
|
|
205
|
+
if (!info) return null;
|
|
206
|
+
|
|
207
|
+
return resolveDesktopDownloadFromUrls({ ...info, type: options.type });
|
|
208
|
+
};
|
|
@@ -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) => {
|