@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.
@@ -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 errorMessage = error instanceof Error ? error.message : String(error);
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: errorMessage,
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 errorMessage = error instanceof Error ? error.message : String(error);
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] = errorMessage;
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: errorMessage, success: false };
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) => {