@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.
Files changed (29) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/apps/desktop/src/main/appBrowsers.ts +4 -1
  3. package/apps/desktop/src/main/controllers/BrowserWindowsCtr.ts +15 -3
  4. package/apps/desktop/src/main/core/browser/Browser.ts +14 -4
  5. package/apps/desktop/src/main/core/browser/BrowserManager.ts +7 -2
  6. package/changelog/v1.json +18 -0
  7. package/e2e/src/steps/community/detail-pages.steps.ts +2 -2
  8. package/e2e/src/steps/community/interactions.steps.ts +6 -6
  9. package/e2e/src/steps/hooks.ts +19 -3
  10. package/package.json +1 -1
  11. package/packages/desktop-bridge/src/index.ts +5 -0
  12. package/packages/electron-client-ipc/src/types/window.ts +3 -2
  13. package/src/app/[variants]/(desktop)/desktop-onboarding/_layout/index.tsx +6 -3
  14. package/src/app/[variants]/(desktop)/desktop-onboarding/components/OnboardingFooterActions.tsx +38 -0
  15. package/src/app/[variants]/(desktop)/desktop-onboarding/features/DataModeStep.tsx +19 -14
  16. package/src/app/[variants]/(desktop)/desktop-onboarding/features/LoginStep.tsx +53 -19
  17. package/src/app/[variants]/(desktop)/desktop-onboarding/features/PermissionsStep.tsx +19 -14
  18. package/src/app/[variants]/(desktop)/desktop-onboarding/index.tsx +8 -7
  19. package/src/app/manifest.ts +1 -1
  20. package/src/features/Electron/titlebar/NavigationBar.tsx +1 -2
  21. package/src/server/manifest.ts +2 -2
  22. package/src/server/services/aiAgent/index.ts +28 -11
  23. package/src/server/services/klavis/index.ts +228 -0
  24. package/src/server/services/market/index.ts +13 -0
  25. package/src/server/services/sandbox/index.ts +84 -18
  26. package/src/server/services/toolExecution/builtin.ts +12 -0
  27. package/src/server/services/toolExecution/index.ts +70 -4
  28. package/src/server/services/toolExecution/serverRuntimes/cloudSandbox.ts +7 -0
  29. 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 fixedSize = { height: 900, width: 1400 };
54
+ const minimumSize = { height: 900, width: 1200 };
54
55
 
55
56
  const applyWindowSettings = async () => {
56
57
  try {
57
- await electronSystemService.setWindowSize(fixedSize);
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
- electronSystemService.setWindowResizable({ resizable: true }).catch((error) => {
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 resizable before hard reload (cleanup won't run due to hard navigation)
131
+ // Restore window minimum size before hard reload (cleanup won't run due to hard navigation)
131
132
  electronSystemService
132
- .setWindowResizable({ resizable: true })
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}>
@@ -16,7 +16,7 @@ const manifest = async (): Promise<MetadataRoute.Manifest> => {
16
16
  ],
17
17
  name: 'LobeChat',
18
18
  short_name: 'LobeChat',
19
- start_url: '/agent',
19
+ start_url: '/',
20
20
  theme_color: '#000000',
21
21
  };
22
22
  }
@@ -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
@@ -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: '/agent',
66
+ start_url: '/',
67
67
  tab_strip: {
68
68
  new_tab_button: {
69
- url: '/agent',
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. Create tools using Server AgentToolsEngine
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
- // 6. Get existing messages if provided
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
- // 7. Create user message in database
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
- // 8. Create assistant message placeholder in database
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
- // 9. Process messages using Server ContextEngineering
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
- // 10. Generate operation ID: agt_{timestamp}_{agentId}_{topicId}_{random}
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
- // 11. Create initial context
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
- // 12. Log final operation parameters summary
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
- // 13. Create operation using AgentRuntimeService
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.getSDK().plugins.runBuildInTool(
49
- toolName as CodeInterpreterToolName,
50
- params as any,
51
- { topicId: this.topicId, userId: this.userId },
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
- * Note: This is a simplified version for server-side use.
92
- * The full implementation with S3 upload is in the tRPC router.
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
- error: {
104
- message:
105
- 'Server-side file export not fully implemented. Use tRPC endpoint for file exports.',
106
- },
169
+ fileId,
107
170
  filename,
108
- success: false,
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`);