@lobehub/lobehub 2.0.0-next.313 → 2.0.0-next.315
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 +50 -0
- package/apps/desktop/src/main/appBrowsers.ts +4 -1
- package/apps/desktop/src/main/controllers/BrowserWindowsCtr.ts +15 -3
- package/apps/desktop/src/main/core/browser/Browser.ts +14 -4
- package/apps/desktop/src/main/core/browser/BrowserManager.ts +7 -2
- package/changelog/v1.json +18 -0
- package/e2e/src/steps/community/detail-pages.steps.ts +2 -2
- package/e2e/src/steps/community/interactions.steps.ts +6 -6
- package/e2e/src/steps/hooks.ts +19 -3
- package/package.json +1 -1
- package/packages/desktop-bridge/src/index.ts +5 -0
- package/packages/electron-client-ipc/src/types/window.ts +3 -2
- package/src/app/[variants]/(desktop)/desktop-onboarding/_layout/index.tsx +6 -3
- package/src/app/[variants]/(desktop)/desktop-onboarding/components/OnboardingFooterActions.tsx +38 -0
- package/src/app/[variants]/(desktop)/desktop-onboarding/features/DataModeStep.tsx +19 -14
- package/src/app/[variants]/(desktop)/desktop-onboarding/features/LoginStep.tsx +53 -19
- package/src/app/[variants]/(desktop)/desktop-onboarding/features/PermissionsStep.tsx +19 -14
- package/src/app/[variants]/(desktop)/desktop-onboarding/index.tsx +8 -7
- package/src/app/manifest.ts +1 -1
- package/src/features/Electron/titlebar/NavigationBar.tsx +1 -2
- package/src/server/manifest.ts +2 -2
- package/src/server/services/aiAgent/index.ts +28 -11
- package/src/server/services/klavis/index.ts +228 -0
- package/src/server/services/market/index.ts +13 -0
- package/src/server/services/sandbox/index.ts +84 -18
- package/src/server/services/toolExecution/builtin.ts +12 -0
- package/src/server/services/toolExecution/index.ts +70 -4
- package/src/server/services/toolExecution/serverRuntimes/cloudSandbox.ts +7 -0
- package/src/services/electron/system.ts +5 -5
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import { APP_WINDOW_MIN_SIZE } from '@lobechat/desktop-bridge';
|
|
3
4
|
import { Flexbox, Skeleton } from '@lobehub/ui';
|
|
4
5
|
import { Suspense, memo, useCallback, useEffect, useState } from 'react';
|
|
5
6
|
import { useSearchParams } from 'react-router-dom';
|
|
@@ -50,12 +51,11 @@ const DesktopOnboardingPage = memo(() => {
|
|
|
50
51
|
|
|
51
52
|
// 设置窗口大小和可调整性
|
|
52
53
|
useEffect(() => {
|
|
53
|
-
const
|
|
54
|
+
const minimumSize = { height: 900, width: 1200 };
|
|
54
55
|
|
|
55
56
|
const applyWindowSettings = async () => {
|
|
56
57
|
try {
|
|
57
|
-
await electronSystemService.
|
|
58
|
-
await electronSystemService.setWindowResizable({ resizable: false });
|
|
58
|
+
await electronSystemService.setWindowMinimumSize(minimumSize);
|
|
59
59
|
} catch (error) {
|
|
60
60
|
console.error('[DesktopOnboarding] Failed to apply window settings:', error);
|
|
61
61
|
}
|
|
@@ -64,7 +64,8 @@ const DesktopOnboardingPage = memo(() => {
|
|
|
64
64
|
applyWindowSettings();
|
|
65
65
|
|
|
66
66
|
return () => {
|
|
67
|
-
|
|
67
|
+
// Restore to app-level default minimum size preset
|
|
68
|
+
electronSystemService.setWindowMinimumSize(APP_WINDOW_MIN_SIZE).catch((error) => {
|
|
68
69
|
console.error('[DesktopOnboarding] Failed to restore window settings:', error);
|
|
69
70
|
});
|
|
70
71
|
};
|
|
@@ -127,9 +128,9 @@ const DesktopOnboardingPage = memo(() => {
|
|
|
127
128
|
// 如果是第4步(LoginStep),完成 onboarding
|
|
128
129
|
setDesktopOnboardingCompleted();
|
|
129
130
|
clearDesktopOnboardingStep(); // Clear persisted step since onboarding is complete
|
|
130
|
-
// Restore window
|
|
131
|
+
// Restore window minimum size before hard reload (cleanup won't run due to hard navigation)
|
|
131
132
|
electronSystemService
|
|
132
|
-
.
|
|
133
|
+
.setWindowMinimumSize(APP_WINDOW_MIN_SIZE)
|
|
133
134
|
.catch(console.error)
|
|
134
135
|
.finally(() => {
|
|
135
136
|
// Use hard reload instead of SPA navigation to ensure the app boots with the new desktop state.
|
|
@@ -196,7 +197,7 @@ const DesktopOnboardingPage = memo(() => {
|
|
|
196
197
|
|
|
197
198
|
return (
|
|
198
199
|
<OnboardingContainer>
|
|
199
|
-
<Flexbox gap={24} style={{ maxWidth: 560, width: '100%' }}>
|
|
200
|
+
<Flexbox gap={24} style={{ maxWidth: 560, minHeight: '100%', width: '100%' }}>
|
|
200
201
|
<Suspense
|
|
201
202
|
fallback={
|
|
202
203
|
<Flexbox gap={8}>
|
package/src/app/manifest.ts
CHANGED
|
@@ -53,13 +53,12 @@ const NavigationBar = memo(() => {
|
|
|
53
53
|
return (
|
|
54
54
|
<Flexbox
|
|
55
55
|
align="center"
|
|
56
|
-
className={electronStylish.nodrag}
|
|
57
56
|
data-width={leftPanelWidth}
|
|
58
57
|
horizontal
|
|
59
58
|
justify="end"
|
|
60
59
|
style={{ width: `${leftPanelWidth - 12}px` }}
|
|
61
60
|
>
|
|
62
|
-
<Flexbox align="center" gap={2} horizontal>
|
|
61
|
+
<Flexbox align="center" className={electronStylish.nodrag} gap={2} horizontal>
|
|
63
62
|
<ActionIcon disabled={!canGoBack} icon={ArrowLeft} onClick={goBack} size="small" />
|
|
64
63
|
<ActionIcon disabled={!canGoForward} icon={ArrowRight} onClick={goForward} size="small" />
|
|
65
64
|
<Popover
|
package/src/server/manifest.ts
CHANGED
|
@@ -63,10 +63,10 @@ export class Manifest {
|
|
|
63
63
|
screenshots: screenshots.map((item) => this._getScreenshot(item)),
|
|
64
64
|
short_name: name,
|
|
65
65
|
splash_pages: null,
|
|
66
|
-
start_url: '/
|
|
66
|
+
start_url: '/',
|
|
67
67
|
tab_strip: {
|
|
68
68
|
new_tab_button: {
|
|
69
|
-
url: '/
|
|
69
|
+
url: '/',
|
|
70
70
|
},
|
|
71
71
|
},
|
|
72
72
|
theme_color: color,
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
import { AgentService } from '@/server/services/agent';
|
|
29
29
|
import { AgentRuntimeService } from '@/server/services/agentRuntime';
|
|
30
30
|
import type { StepLifecycleCallbacks } from '@/server/services/agentRuntime/types';
|
|
31
|
+
import { KlavisService } from '@/server/services/klavis';
|
|
31
32
|
import { MarketService } from '@/server/services/market';
|
|
32
33
|
|
|
33
34
|
const log = debug('lobe-server:ai-agent-service');
|
|
@@ -93,6 +94,7 @@ export class AiAgentService {
|
|
|
93
94
|
private readonly topicModel: TopicModel;
|
|
94
95
|
private readonly agentRuntimeService: AgentRuntimeService;
|
|
95
96
|
private readonly marketService: MarketService;
|
|
97
|
+
private readonly klavisService: KlavisService;
|
|
96
98
|
|
|
97
99
|
constructor(db: LobeChatDatabase, userId: string) {
|
|
98
100
|
this.userId = userId;
|
|
@@ -105,6 +107,7 @@ export class AiAgentService {
|
|
|
105
107
|
this.topicModel = new TopicModel(db, userId);
|
|
106
108
|
this.agentRuntimeService = new AgentRuntimeService(db, userId);
|
|
107
109
|
this.marketService = new MarketService({ userInfo: { userId } });
|
|
110
|
+
this.klavisService = new KlavisService({ db, userId });
|
|
108
111
|
}
|
|
109
112
|
|
|
110
113
|
/**
|
|
@@ -205,7 +208,16 @@ export class AiAgentService {
|
|
|
205
208
|
}
|
|
206
209
|
log('execAgent: got %d lobehub skill manifests', lobehubSkillManifests.length);
|
|
207
210
|
|
|
208
|
-
// 6.
|
|
211
|
+
// 6. Fetch Klavis tool manifests from database
|
|
212
|
+
let klavisManifests: LobeToolManifest[] = [];
|
|
213
|
+
try {
|
|
214
|
+
klavisManifests = await this.klavisService.getKlavisManifests();
|
|
215
|
+
} catch (error) {
|
|
216
|
+
log('execAgent: failed to fetch klavis manifests: %O', error);
|
|
217
|
+
}
|
|
218
|
+
log('execAgent: got %d klavis manifests', klavisManifests.length);
|
|
219
|
+
|
|
220
|
+
// 7. Create tools using Server AgentToolsEngine
|
|
209
221
|
const hasEnabledKnowledgeBases =
|
|
210
222
|
agentConfig.knowledgeBases?.some((kb: { enabled?: boolean | null }) => kb.enabled === true) ??
|
|
211
223
|
false;
|
|
@@ -216,7 +228,7 @@ export class AiAgentService {
|
|
|
216
228
|
};
|
|
217
229
|
|
|
218
230
|
const toolsEngine = createServerAgentToolsEngine(toolsContext, {
|
|
219
|
-
additionalManifests: lobehubSkillManifests,
|
|
231
|
+
additionalManifests: [...lobehubSkillManifests, ...klavisManifests],
|
|
220
232
|
agentConfig: {
|
|
221
233
|
chatConfig: agentConfig.chatConfig ?? undefined,
|
|
222
234
|
plugins: agentConfig.plugins ?? undefined,
|
|
@@ -254,15 +266,20 @@ export class AiAgentService {
|
|
|
254
266
|
for (const manifest of lobehubSkillManifests) {
|
|
255
267
|
toolSourceMap[manifest.identifier] = 'lobehubSkill';
|
|
256
268
|
}
|
|
269
|
+
// Mark klavis tools
|
|
270
|
+
for (const manifest of klavisManifests) {
|
|
271
|
+
toolSourceMap[manifest.identifier] = 'klavis';
|
|
272
|
+
}
|
|
257
273
|
|
|
258
274
|
log(
|
|
259
|
-
'execAgent: generated %d tools from %d configured plugins, %d lobehub skills',
|
|
275
|
+
'execAgent: generated %d tools from %d configured plugins, %d lobehub skills, %d klavis tools',
|
|
260
276
|
tools?.length ?? 0,
|
|
261
277
|
pluginIds.length,
|
|
262
278
|
lobehubSkillManifests.length,
|
|
279
|
+
klavisManifests.length,
|
|
263
280
|
);
|
|
264
281
|
|
|
265
|
-
//
|
|
282
|
+
// 8. Get existing messages if provided
|
|
266
283
|
let historyMessages: any[] = [];
|
|
267
284
|
if (existingMessageIds.length > 0) {
|
|
268
285
|
historyMessages = await this.messageModel.query({
|
|
@@ -275,7 +292,7 @@ export class AiAgentService {
|
|
|
275
292
|
}
|
|
276
293
|
}
|
|
277
294
|
|
|
278
|
-
//
|
|
295
|
+
// 9. Create user message in database
|
|
279
296
|
// Include threadId if provided (for SubAgent task execution in isolated Thread)
|
|
280
297
|
const userMessageRecord = await this.messageModel.create({
|
|
281
298
|
agentId: resolvedAgentId,
|
|
@@ -286,7 +303,7 @@ export class AiAgentService {
|
|
|
286
303
|
});
|
|
287
304
|
log('execAgent: created user message %s', userMessageRecord.id);
|
|
288
305
|
|
|
289
|
-
//
|
|
306
|
+
// 10. Create assistant message placeholder in database
|
|
290
307
|
// Include threadId if provided (for SubAgent task execution in isolated Thread)
|
|
291
308
|
const assistantMessageRecord = await this.messageModel.create({
|
|
292
309
|
agentId: resolvedAgentId,
|
|
@@ -306,7 +323,7 @@ export class AiAgentService {
|
|
|
306
323
|
// Combine history messages with user message
|
|
307
324
|
const allMessages = [...historyMessages, userMessage];
|
|
308
325
|
|
|
309
|
-
//
|
|
326
|
+
// 11. Process messages using Server ContextEngineering
|
|
310
327
|
const processedMessages = await serverMessagesEngine({
|
|
311
328
|
capabilities: {
|
|
312
329
|
isCanUseFC: isModelSupportToolUse,
|
|
@@ -341,11 +358,11 @@ export class AiAgentService {
|
|
|
341
358
|
|
|
342
359
|
log('execAgent: processed %d messages', processedMessages.length);
|
|
343
360
|
|
|
344
|
-
//
|
|
361
|
+
// 12. Generate operation ID: agt_{timestamp}_{agentId}_{topicId}_{random}
|
|
345
362
|
const timestamp = Date.now();
|
|
346
363
|
const operationId = `op_${timestamp}_${resolvedAgentId}_${topicId}_${nanoid(8)}`;
|
|
347
364
|
|
|
348
|
-
//
|
|
365
|
+
// 13. Create initial context
|
|
349
366
|
const initialContext: AgentRuntimeContext = {
|
|
350
367
|
payload: {
|
|
351
368
|
// Pass assistant message ID so agent runtime knows which message to update
|
|
@@ -366,7 +383,7 @@ export class AiAgentService {
|
|
|
366
383
|
},
|
|
367
384
|
};
|
|
368
385
|
|
|
369
|
-
//
|
|
386
|
+
// 14. Log final operation parameters summary
|
|
370
387
|
log(
|
|
371
388
|
'execAgent: creating operation %s with params: model=%s, provider=%s, tools=%d, messages=%d, manifests=%d',
|
|
372
389
|
operationId,
|
|
@@ -377,7 +394,7 @@ export class AiAgentService {
|
|
|
377
394
|
Object.keys(toolManifestMap).length,
|
|
378
395
|
);
|
|
379
396
|
|
|
380
|
-
//
|
|
397
|
+
// 15. Create operation using AgentRuntimeService
|
|
381
398
|
// Wrap in try-catch to handle operation startup failures (e.g., QStash unavailable)
|
|
382
399
|
// If createOperation fails, we still have valid messages that need error info
|
|
383
400
|
try {
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import type { LobeToolManifest } from '@lobechat/context-engine';
|
|
2
|
+
import type { LobeChatDatabase } from '@lobechat/database';
|
|
3
|
+
import debug from 'debug';
|
|
4
|
+
|
|
5
|
+
import { PluginModel } from '@/database/models/plugin';
|
|
6
|
+
import { getKlavisClient, isKlavisClientAvailable } from '@/libs/klavis';
|
|
7
|
+
import { type ToolExecutionResult } from '@/server/services/toolExecution/types';
|
|
8
|
+
|
|
9
|
+
const log = debug('lobe-server:klavis-service');
|
|
10
|
+
|
|
11
|
+
export interface KlavisToolExecuteParams {
|
|
12
|
+
args: Record<string, any>;
|
|
13
|
+
/** Tool identifier (same as Klavis server identifier, e.g., 'google-calendar') */
|
|
14
|
+
identifier: string;
|
|
15
|
+
toolName: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface KlavisServiceOptions {
|
|
19
|
+
db?: LobeChatDatabase;
|
|
20
|
+
userId?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Klavis Service
|
|
25
|
+
*
|
|
26
|
+
* Provides a unified interface to Klavis Client with business logic encapsulation.
|
|
27
|
+
* This service wraps Klavis Client methods to execute tools and fetch manifests.
|
|
28
|
+
*
|
|
29
|
+
* Usage:
|
|
30
|
+
* ```typescript
|
|
31
|
+
* // With database and userId (for manifest fetching)
|
|
32
|
+
* const service = new KlavisService({ db, userId });
|
|
33
|
+
* await service.executeKlavisTool({ identifier, toolName, args });
|
|
34
|
+
*
|
|
35
|
+
* // Without database (for tool execution only if you have serverUrl)
|
|
36
|
+
* const service = new KlavisService();
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export class KlavisService {
|
|
40
|
+
private db?: LobeChatDatabase;
|
|
41
|
+
private userId?: string;
|
|
42
|
+
private pluginModel?: PluginModel;
|
|
43
|
+
|
|
44
|
+
constructor(options: KlavisServiceOptions = {}) {
|
|
45
|
+
const { db, userId } = options;
|
|
46
|
+
|
|
47
|
+
this.db = db;
|
|
48
|
+
this.userId = userId;
|
|
49
|
+
|
|
50
|
+
if (db && userId) {
|
|
51
|
+
this.pluginModel = new PluginModel(db, userId);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
log(
|
|
55
|
+
'KlavisService initialized: hasDB=%s, hasUserId=%s, isClientAvailable=%s',
|
|
56
|
+
!!db,
|
|
57
|
+
!!userId,
|
|
58
|
+
isKlavisClientAvailable(),
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Execute a Klavis tool
|
|
64
|
+
* @param params - Tool execution parameters
|
|
65
|
+
* @returns Tool execution result
|
|
66
|
+
*/
|
|
67
|
+
async executeKlavisTool(params: KlavisToolExecuteParams): Promise<ToolExecutionResult> {
|
|
68
|
+
const { identifier, toolName, args } = params;
|
|
69
|
+
|
|
70
|
+
log('executeKlavisTool: %s/%s with args: %O', identifier, toolName, args);
|
|
71
|
+
|
|
72
|
+
// Check if Klavis client is available
|
|
73
|
+
if (!isKlavisClientAvailable()) {
|
|
74
|
+
return {
|
|
75
|
+
content: 'Klavis service is not configured on server',
|
|
76
|
+
error: { code: 'KLAVIS_NOT_CONFIGURED', message: 'Klavis API key not found' },
|
|
77
|
+
success: false,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Get serverUrl from plugin database
|
|
82
|
+
if (!this.pluginModel) {
|
|
83
|
+
return {
|
|
84
|
+
content: 'Klavis service is not properly initialized',
|
|
85
|
+
error: {
|
|
86
|
+
code: 'KLAVIS_NOT_INITIALIZED',
|
|
87
|
+
message: 'Database and userId are required for Klavis tool execution',
|
|
88
|
+
},
|
|
89
|
+
success: false,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
// Get plugin from database to retrieve serverUrl
|
|
95
|
+
const plugin = await this.pluginModel.findById(identifier);
|
|
96
|
+
if (!plugin) {
|
|
97
|
+
return {
|
|
98
|
+
content: `Klavis server "${identifier}" not found in database`,
|
|
99
|
+
error: { code: 'KLAVIS_SERVER_NOT_FOUND', message: `Server ${identifier} not found` },
|
|
100
|
+
success: false,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const klavisParams = plugin.customParams?.klavis;
|
|
105
|
+
if (!klavisParams || !klavisParams.serverUrl) {
|
|
106
|
+
return {
|
|
107
|
+
content: `Klavis configuration not found for server "${identifier}"`,
|
|
108
|
+
error: {
|
|
109
|
+
code: 'KLAVIS_CONFIG_NOT_FOUND',
|
|
110
|
+
message: `Klavis configuration missing for ${identifier}`,
|
|
111
|
+
},
|
|
112
|
+
success: false,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const { serverUrl } = klavisParams;
|
|
117
|
+
|
|
118
|
+
log('executeKlavisTool: calling Klavis API with serverUrl=%s', serverUrl);
|
|
119
|
+
|
|
120
|
+
// Call Klavis client
|
|
121
|
+
const klavisClient = getKlavisClient();
|
|
122
|
+
const response = await klavisClient.mcpServer.callTools({
|
|
123
|
+
serverUrl,
|
|
124
|
+
toolArgs: args,
|
|
125
|
+
toolName,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
log('executeKlavisTool: response: %O', response);
|
|
129
|
+
|
|
130
|
+
// Handle error case
|
|
131
|
+
if (!response.success || !response.result) {
|
|
132
|
+
return {
|
|
133
|
+
content: response.error || 'Unknown error',
|
|
134
|
+
error: { code: 'KLAVIS_EXECUTION_ERROR', message: response.error || 'Unknown error' },
|
|
135
|
+
success: false,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Process the response
|
|
140
|
+
const content = response.result.content || [];
|
|
141
|
+
const isError = response.result.isError || false;
|
|
142
|
+
|
|
143
|
+
// Convert content array to string
|
|
144
|
+
let resultContent = '';
|
|
145
|
+
if (Array.isArray(content)) {
|
|
146
|
+
resultContent = content
|
|
147
|
+
.map((item: any) => {
|
|
148
|
+
if (typeof item === 'string') return item;
|
|
149
|
+
if (item.type === 'text' && item.text) return item.text;
|
|
150
|
+
return JSON.stringify(item);
|
|
151
|
+
})
|
|
152
|
+
.join('\n');
|
|
153
|
+
} else if (typeof content === 'string') {
|
|
154
|
+
resultContent = content;
|
|
155
|
+
} else {
|
|
156
|
+
resultContent = JSON.stringify(content);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
content: resultContent,
|
|
161
|
+
success: !isError,
|
|
162
|
+
};
|
|
163
|
+
} catch (error) {
|
|
164
|
+
const err = error as Error;
|
|
165
|
+
console.error('KlavisService.executeKlavisTool error %s/%s: %O', identifier, toolName, err);
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
content: err.message,
|
|
169
|
+
error: { code: 'KLAVIS_ERROR', message: err.message },
|
|
170
|
+
success: false,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Fetch Klavis tool manifests from database
|
|
177
|
+
* Gets user's connected Klavis servers and builds tool manifests for agent execution
|
|
178
|
+
*
|
|
179
|
+
* @returns Array of tool manifests for connected Klavis servers
|
|
180
|
+
*/
|
|
181
|
+
async getKlavisManifests(): Promise<LobeToolManifest[]> {
|
|
182
|
+
if (!this.pluginModel) {
|
|
183
|
+
log('getKlavisManifests: pluginModel not available, returning empty array');
|
|
184
|
+
return [];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
// Get all plugins from database
|
|
189
|
+
const allPlugins = await this.pluginModel.query();
|
|
190
|
+
|
|
191
|
+
// Filter plugins that have klavis customParams and are authenticated
|
|
192
|
+
const klavisPlugins = allPlugins.filter(
|
|
193
|
+
(plugin) => plugin.customParams?.klavis?.isAuthenticated === true,
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
log('getKlavisManifests: found %d authenticated Klavis plugins', klavisPlugins.length);
|
|
197
|
+
|
|
198
|
+
// Convert to LobeToolManifest format
|
|
199
|
+
const manifests: LobeToolManifest[] = klavisPlugins
|
|
200
|
+
.map((plugin) => {
|
|
201
|
+
if (!plugin.manifest) return null;
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
api: plugin.manifest.api || [],
|
|
205
|
+
author: 'Klavis',
|
|
206
|
+
homepage: 'https://klavis.ai',
|
|
207
|
+
identifier: plugin.identifier,
|
|
208
|
+
meta: plugin.manifest.meta || {
|
|
209
|
+
avatar: '☁️',
|
|
210
|
+
description: `Klavis MCP Server: ${plugin.customParams?.klavis?.serverName}`,
|
|
211
|
+
tags: ['klavis', 'mcp'],
|
|
212
|
+
title: plugin.customParams?.klavis?.serverName || plugin.identifier,
|
|
213
|
+
},
|
|
214
|
+
type: 'builtin',
|
|
215
|
+
version: '1.0.0',
|
|
216
|
+
};
|
|
217
|
+
})
|
|
218
|
+
.filter(Boolean) as LobeToolManifest[];
|
|
219
|
+
|
|
220
|
+
log('getKlavisManifests: returning %d manifests', manifests.length);
|
|
221
|
+
|
|
222
|
+
return manifests;
|
|
223
|
+
} catch (error) {
|
|
224
|
+
console.error('KlavisService.getKlavisManifests error: %O', error);
|
|
225
|
+
return [];
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
@@ -280,6 +280,19 @@ export class MarketService {
|
|
|
280
280
|
return this.market.plugins.callCloudGateway(params, options);
|
|
281
281
|
}
|
|
282
282
|
|
|
283
|
+
/**
|
|
284
|
+
* Export file from sandbox to upload URL
|
|
285
|
+
*/
|
|
286
|
+
async exportFile(params: { path: string; topicId: string; uploadUrl: string; userId: string }) {
|
|
287
|
+
const { path, uploadUrl, topicId, userId } = params;
|
|
288
|
+
|
|
289
|
+
return this.market.plugins.runBuildInTool(
|
|
290
|
+
'exportFile',
|
|
291
|
+
{ path, uploadUrl },
|
|
292
|
+
{ topicId, userId },
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
283
296
|
/**
|
|
284
297
|
* Get plugin manifest
|
|
285
298
|
*/
|
|
@@ -1,16 +1,20 @@
|
|
|
1
|
-
import { type CodeInterpreterToolName } from '@lobehub/market-sdk';
|
|
2
1
|
import {
|
|
3
2
|
type ISandboxService,
|
|
4
3
|
type SandboxCallToolResult,
|
|
5
4
|
type SandboxExportFileResult,
|
|
6
5
|
} from '@lobechat/builtin-tool-cloud-sandbox';
|
|
6
|
+
import { type CodeInterpreterToolName } from '@lobehub/market-sdk';
|
|
7
7
|
import debug from 'debug';
|
|
8
|
+
import { sha256 } from 'js-sha256';
|
|
8
9
|
|
|
10
|
+
import { FileS3 } from '@/server/modules/S3';
|
|
11
|
+
import { type FileService } from '@/server/services/file';
|
|
9
12
|
import { MarketService } from '@/server/services/market';
|
|
10
13
|
|
|
11
14
|
const log = debug('lobe-server:sandbox-service');
|
|
12
15
|
|
|
13
16
|
export interface ServerSandboxServiceOptions {
|
|
17
|
+
fileService: FileService;
|
|
14
18
|
marketService: MarketService;
|
|
15
19
|
topicId: string;
|
|
16
20
|
userId: string;
|
|
@@ -28,11 +32,13 @@ export interface ServerSandboxServiceOptions {
|
|
|
28
32
|
* - MarketService handles authentication via trustedClientToken
|
|
29
33
|
*/
|
|
30
34
|
export class ServerSandboxService implements ISandboxService {
|
|
35
|
+
private fileService: FileService;
|
|
31
36
|
private marketService: MarketService;
|
|
32
37
|
private topicId: string;
|
|
33
38
|
private userId: string;
|
|
34
39
|
|
|
35
40
|
constructor(options: ServerSandboxServiceOptions) {
|
|
41
|
+
this.fileService = options.fileService;
|
|
36
42
|
this.marketService = options.marketService;
|
|
37
43
|
this.topicId = options.topicId;
|
|
38
44
|
this.userId = options.userId;
|
|
@@ -45,11 +51,12 @@ export class ServerSandboxService implements ISandboxService {
|
|
|
45
51
|
log('Calling sandbox tool: %s with params: %O, topicId: %s', toolName, params, this.topicId);
|
|
46
52
|
|
|
47
53
|
try {
|
|
48
|
-
const response = await this.marketService
|
|
49
|
-
|
|
50
|
-
params as any,
|
|
51
|
-
|
|
52
|
-
|
|
54
|
+
const response = await this.marketService
|
|
55
|
+
.getSDK()
|
|
56
|
+
.plugins.runBuildInTool(toolName as CodeInterpreterToolName, params as any, {
|
|
57
|
+
topicId: this.topicId,
|
|
58
|
+
userId: this.userId,
|
|
59
|
+
});
|
|
53
60
|
|
|
54
61
|
log('Sandbox tool %s response: %O', toolName, response);
|
|
55
62
|
|
|
@@ -86,26 +93,85 @@ export class ServerSandboxService implements ISandboxService {
|
|
|
86
93
|
}
|
|
87
94
|
|
|
88
95
|
/**
|
|
89
|
-
* Export and upload a file from sandbox
|
|
96
|
+
* Export and upload a file from sandbox to S3
|
|
90
97
|
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
98
|
+
* Steps:
|
|
99
|
+
* 1. Generate S3 pre-signed upload URL
|
|
100
|
+
* 2. Call sandbox exportFile tool to upload file
|
|
101
|
+
* 3. Verify upload success and get metadata
|
|
102
|
+
* 4. Create persistent file record
|
|
93
103
|
*/
|
|
94
104
|
async exportAndUploadFile(path: string, filename: string): Promise<SandboxExportFileResult> {
|
|
95
105
|
log('Exporting file: %s from path: %s, topicId: %s', filename, path, this.topicId);
|
|
96
106
|
|
|
97
|
-
// For server-side, we need to call the exportFile tool
|
|
98
|
-
// The full S3 upload logic should be handled separately
|
|
99
|
-
// This is a basic implementation that can be extended
|
|
100
|
-
|
|
101
107
|
try {
|
|
108
|
+
const s3 = new FileS3();
|
|
109
|
+
|
|
110
|
+
// Use date-based sharding for privacy compliance (GDPR, CCPA)
|
|
111
|
+
const today = new Date().toISOString().split('T')[0];
|
|
112
|
+
|
|
113
|
+
// Generate a unique key for the exported file
|
|
114
|
+
const key = `code-interpreter-exports/${today}/${this.topicId}/${filename}`;
|
|
115
|
+
|
|
116
|
+
// Step 1: Generate pre-signed upload URL
|
|
117
|
+
const uploadUrl = await s3.createPreSignedUrl(key);
|
|
118
|
+
log('Generated upload URL for key: %s', key);
|
|
119
|
+
|
|
120
|
+
// Step 2: Call sandbox's exportFile tool with the upload URL
|
|
121
|
+
const response = await this.marketService.exportFile({
|
|
122
|
+
path,
|
|
123
|
+
topicId: this.topicId,
|
|
124
|
+
uploadUrl,
|
|
125
|
+
userId: this.userId,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
log('Sandbox exportFile response: %O', response);
|
|
129
|
+
|
|
130
|
+
if (!response.success) {
|
|
131
|
+
return {
|
|
132
|
+
error: { message: response.error?.message || 'Failed to export file from sandbox' },
|
|
133
|
+
filename,
|
|
134
|
+
success: false,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const result = response.data?.result;
|
|
139
|
+
const uploadSuccess = result?.success !== false;
|
|
140
|
+
|
|
141
|
+
if (!uploadSuccess) {
|
|
142
|
+
return {
|
|
143
|
+
error: { message: result?.error || 'Failed to upload file from sandbox' },
|
|
144
|
+
filename,
|
|
145
|
+
success: false,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Step 3: Get file metadata from S3 to verify upload and get actual size
|
|
150
|
+
const metadata = await s3.getFileMetadata(key);
|
|
151
|
+
const fileSize = metadata.contentLength;
|
|
152
|
+
const mimeType = metadata.contentType || result?.mimeType || 'application/octet-stream';
|
|
153
|
+
|
|
154
|
+
// Step 4: Create persistent file record using FileService
|
|
155
|
+
// Generate a simple hash from the key (since we don't have the actual file content)
|
|
156
|
+
const fileHash = sha256(key + Date.now().toString());
|
|
157
|
+
|
|
158
|
+
const { fileId, url } = await this.fileService.createFileRecord({
|
|
159
|
+
fileHash,
|
|
160
|
+
fileType: mimeType,
|
|
161
|
+
name: filename,
|
|
162
|
+
size: fileSize,
|
|
163
|
+
url: key, // Store S3 key
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
log('Created file record: fileId=%s, url=%s', fileId, url);
|
|
167
|
+
|
|
102
168
|
return {
|
|
103
|
-
|
|
104
|
-
message:
|
|
105
|
-
'Server-side file export not fully implemented. Use tRPC endpoint for file exports.',
|
|
106
|
-
},
|
|
169
|
+
fileId,
|
|
107
170
|
filename,
|
|
108
|
-
|
|
171
|
+
mimeType,
|
|
172
|
+
size: fileSize,
|
|
173
|
+
success: true,
|
|
174
|
+
url, // This is the permanent /f/:id URL
|
|
109
175
|
};
|
|
110
176
|
} catch (error) {
|
|
111
177
|
log('Error exporting file: %O', error);
|
|
@@ -3,6 +3,7 @@ import { type ChatToolPayload } from '@lobechat/types';
|
|
|
3
3
|
import { safeParseJSON } from '@lobechat/utils';
|
|
4
4
|
import debug from 'debug';
|
|
5
5
|
|
|
6
|
+
import { KlavisService } from '@/server/services/klavis';
|
|
6
7
|
import { MarketService } from '@/server/services/market';
|
|
7
8
|
|
|
8
9
|
import { getServerRuntime, hasServerRuntime } from './serverRuntimes';
|
|
@@ -12,9 +13,11 @@ const log = debug('lobe-server:builtin-tools-executor');
|
|
|
12
13
|
|
|
13
14
|
export class BuiltinToolsExecutor implements IToolExecutor {
|
|
14
15
|
private marketService: MarketService;
|
|
16
|
+
private klavisService: KlavisService;
|
|
15
17
|
|
|
16
18
|
constructor(db: LobeChatDatabase, userId: string) {
|
|
17
19
|
this.marketService = new MarketService({ userInfo: { userId } });
|
|
20
|
+
this.klavisService = new KlavisService({ db, userId });
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
async execute(
|
|
@@ -41,6 +44,15 @@ export class BuiltinToolsExecutor implements IToolExecutor {
|
|
|
41
44
|
});
|
|
42
45
|
}
|
|
43
46
|
|
|
47
|
+
// Route Klavis tools to KlavisService
|
|
48
|
+
if (source === 'klavis') {
|
|
49
|
+
return this.klavisService.executeKlavisTool({
|
|
50
|
+
args,
|
|
51
|
+
identifier,
|
|
52
|
+
toolName: apiName,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
44
56
|
// Use server runtime registry (handles both pre-instantiated and per-request runtimes)
|
|
45
57
|
if (!hasServerRuntime(identifier)) {
|
|
46
58
|
throw new Error(`Builtin tool "${identifier}" is not implemented`);
|