@lobehub/lobehub 2.0.0-next.217 → 2.0.0-next.219

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 (35) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/changelog/v1.json +21 -0
  3. package/package.json +2 -2
  4. package/packages/builtin-tool-cloud-sandbox/src/ExecutionRuntime/index.ts +18 -31
  5. package/packages/builtin-tool-cloud-sandbox/src/types.ts +3 -3
  6. package/src/app/[variants]/(main)/chat/_layout/Sidebar/Topic/List/Item/Actions.tsx +1 -1
  7. package/src/app/[variants]/(main)/chat/_layout/Sidebar/Topic/TopicListContent/ThreadList/ThreadItem/Actions.tsx +1 -1
  8. package/src/app/[variants]/(main)/chat/profile/features/EditorCanvas/TypoBar.tsx +1 -11
  9. package/src/app/[variants]/(main)/group/_layout/Sidebar/Topic/List/Item/Actions.tsx +1 -1
  10. package/src/app/[variants]/(main)/group/_layout/Sidebar/Topic/TopicListContent/ThreadList/ThreadItem/Actions.tsx +1 -1
  11. package/src/app/[variants]/(main)/group/profile/features/EditorCanvas/TypoBar.tsx +1 -11
  12. package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/Item/Actions.tsx +1 -1
  13. package/src/app/[variants]/(main)/home/_layout/Body/Project/List/Actions.tsx +1 -1
  14. package/src/app/[variants]/(main)/home/_layout/Footer/index.tsx +3 -9
  15. package/src/app/[variants]/(main)/home/_layout/Header/components/AddButton.tsx +1 -1
  16. package/src/app/[variants]/(main)/memory/features/EditableModal/index.tsx +8 -101
  17. package/src/app/[variants]/(main)/page/_layout/Body/List/Item/Actions.tsx +1 -1
  18. package/src/app/[variants]/(main)/resource/(home)/_layout/Body/LibraryList/List/Item/Actions.tsx +1 -1
  19. package/src/features/ChatInput/InputEditor/index.tsx +1 -0
  20. package/src/features/ChatInput/TypoBar/index.tsx +0 -11
  21. package/src/features/Conversation/ChatItem/components/MessageContent/index.tsx +11 -12
  22. package/src/features/EditorModal/EditorCanvas.tsx +81 -0
  23. package/src/features/EditorModal/TextareCanvas.tsx +28 -0
  24. package/src/{app/[variants]/(main)/memory/features/EditableModal → features/EditorModal}/Typobar.tsx +0 -11
  25. package/src/features/EditorModal/index.tsx +51 -0
  26. package/src/features/PageEditor/Copilot/TopicSelector/Actions.tsx +1 -1
  27. package/src/features/PageEditor/EditorCanvas/InlineToolbar.tsx +1 -17
  28. package/src/features/ResourceManager/components/Explorer/ItemDropdown/DropdownMenu.tsx +1 -1
  29. package/src/features/User/UserPanel/ThemeButton.tsx +1 -1
  30. package/src/server/routers/tools/market.ts +118 -102
  31. package/src/server/services/discover/index.ts +10 -5
  32. package/src/services/codeInterpreter.ts +12 -20
  33. package/src/store/chat/slices/plugin/actions/pluginTypes.ts +13 -86
  34. package/src/features/Conversation/ChatItem/components/MessageContent/EditableModal.tsx +0 -119
  35. package/src/features/Conversation/ChatItem/components/MessageContent/Typobar.tsx +0 -150
@@ -0,0 +1,28 @@
1
+ import { TextArea } from '@lobehub/ui';
2
+ import { FC } from 'react';
3
+
4
+ interface EditorCanvasProps {
5
+ onChange?: (value: string) => void;
6
+ value?: string;
7
+ }
8
+
9
+ const EditorCanvas: FC<EditorCanvasProps> = ({ value, onChange }) => {
10
+ return (
11
+ <TextArea
12
+ onChange={(e) => {
13
+ onChange?.(e.target.value);
14
+ }}
15
+ style={{
16
+ cursor: 'text',
17
+ maxHeight: '80vh',
18
+ minHeight: '50vh',
19
+ overflowY: 'auto',
20
+ padding: 16,
21
+ }}
22
+ value={value}
23
+ variant={'borderless'}
24
+ />
25
+ );
26
+ };
27
+
28
+ export default EditorCanvas;
@@ -4,7 +4,6 @@ import {
4
4
  ChatInputActionBar,
5
5
  ChatInputActions,
6
6
  type ChatInputActionsProps,
7
- CodeLanguageSelect,
8
7
  } from '@lobehub/editor/react';
9
8
  import { cssVar } from 'antd-style';
10
9
  import {
@@ -119,16 +118,6 @@ const TypoBar = memo<{ editor?: IEditor }>(({ editor }) => {
119
118
  label: t('typobar.codeblock'),
120
119
  onClick: editorState.codeblock,
121
120
  },
122
- editorState.isCodeblock && {
123
- children: (
124
- <CodeLanguageSelect
125
- onSelect={(value) => editorState.updateCodeblockLang(value)}
126
- value={editorState.codeblockLang}
127
- />
128
- ),
129
- disabled: !editorState.isCodeblock,
130
- key: 'codeblockLang',
131
- },
132
121
  ].filter(Boolean) as ChatInputActionsProps['items'],
133
122
  [editorState],
134
123
  );
@@ -0,0 +1,51 @@
1
+ import { Modal, ModalProps, createRawModal } from '@lobehub/ui';
2
+ import { memo, useState } from 'react';
3
+ import { useTranslation } from 'react-i18next';
4
+
5
+ import { useUserStore } from '@/store/user';
6
+ import { labPreferSelectors } from '@/store/user/slices/preference/selectors';
7
+
8
+ import EditorCanvas from './EditorCanvas';
9
+ import TextareCanvas from './TextareCanvas';
10
+
11
+ interface EditorModalProps extends ModalProps {
12
+ onConfirm?: (value: string) => Promise<void>;
13
+ value?: string;
14
+ }
15
+
16
+ export const EditorModal = memo<EditorModalProps>(({ value, onConfirm, ...rest }) => {
17
+ const [confirmLoading, setConfirmLoading] = useState(false);
18
+ const { t } = useTranslation('common');
19
+ const [v, setV] = useState(value);
20
+ const enableRichRender = useUserStore(labPreferSelectors.enableInputMarkdown);
21
+
22
+ const Canvas = enableRichRender ? EditorCanvas : TextareCanvas;
23
+
24
+ return (
25
+ <Modal
26
+ cancelText={t('cancel')}
27
+ closable={false}
28
+ confirmLoading={confirmLoading}
29
+ destroyOnHidden
30
+ okText={t('ok')}
31
+ onOk={async () => {
32
+ setConfirmLoading(true);
33
+ await onConfirm?.(v || '');
34
+ setConfirmLoading(false);
35
+ }}
36
+ styles={{
37
+ body: {
38
+ overflow: 'hidden',
39
+ padding: 0,
40
+ },
41
+ }}
42
+ title={null}
43
+ width={'min(90vw, 920px)'}
44
+ {...rest}
45
+ >
46
+ <Canvas onChange={(v) => setV(v)} value={v} />
47
+ </Modal>
48
+ );
49
+ });
50
+
51
+ export const createEditorModal = (props: EditorModalProps) => createRawModal(EditorModal, props);
@@ -11,7 +11,7 @@ const Actions = memo<ActionsProps>(({ dropdownMenu }) => {
11
11
  return null;
12
12
 
13
13
  return (
14
- <DropdownMenu items={dropdownMenu}>
14
+ <DropdownMenu items={dropdownMenu} nativeButton={false}>
15
15
  <ActionIcon icon={MoreHorizontalIcon} size={'small'} />
16
16
  </DropdownMenu>
17
17
  );
@@ -7,12 +7,7 @@ import {
7
7
  INSERT_HEADING_COMMAND,
8
8
  getHotkeyById,
9
9
  } from '@lobehub/editor';
10
- import {
11
- ChatInputActions,
12
- type ChatInputActionsProps,
13
- CodeLanguageSelect,
14
- FloatActions,
15
- } from '@lobehub/editor/react';
10
+ import { ChatInputActions, type ChatInputActionsProps, FloatActions } from '@lobehub/editor/react';
16
11
  import { Block } from '@lobehub/ui';
17
12
  import { createStaticStyles, cssVar } from 'antd-style';
18
13
  import {
@@ -283,17 +278,6 @@ const TypoBar = memo<ToolbarProps>(({ floating, style, className }) => {
283
278
  label: t('typobar.codeblock'),
284
279
  onClick: editorState.codeblock,
285
280
  },
286
- !floating &&
287
- editorState.isCodeblock && {
288
- children: (
289
- <CodeLanguageSelect
290
- onSelect={(value) => editorState.updateCodeblockLang(value)}
291
- value={editorState.codeblockLang}
292
- />
293
- ),
294
- disabled: !editorState.isCodeblock,
295
- key: 'codeblockLang',
296
- },
297
281
  ];
298
282
 
299
283
  return baseItems.filter(Boolean) as ChatInputActionsProps['items'];
@@ -10,7 +10,7 @@ interface DropdownMenuProps {
10
10
 
11
11
  const DropdownMenu = memo<DropdownMenuProps>(({ items, className }) => {
12
12
  return (
13
- <DropdownMenuUI items={items}>
13
+ <DropdownMenuUI items={items} nativeButton={false}>
14
14
  <ActionIcon className={className} icon={MoreHorizontalIcon} size={'small'} />
15
15
  </DropdownMenuUI>
16
16
  );
@@ -43,7 +43,7 @@ const ThemeButton: FC<{ placement?: DropdownMenuProps['placement']; size?: numbe
43
43
  );
44
44
 
45
45
  return (
46
- <DropdownMenu items={items} placement={placement}>
46
+ <DropdownMenu items={items} nativeButton={false} placement={placement}>
47
47
  <ActionIcon
48
48
  icon={themeIcons[(theme as 'dark' | 'light' | 'system') || 'system']}
49
49
  size={size || { blockSize: 32, size: 16 }}
@@ -1,14 +1,13 @@
1
1
  import { type CodeInterpreterToolName, MarketSDK } from '@lobehub/market-sdk';
2
2
  import { TRPCError } from '@trpc/server';
3
3
  import debug from 'debug';
4
+ import { sha256 } from 'js-sha256';
4
5
  import { z } from 'zod';
5
6
 
6
- import { DocumentModel } from '@/database/models/document';
7
- import { FileModel } from '@/database/models/file';
8
7
  import { type ToolCallContent } from '@/libs/mcp';
9
8
  import { authedProcedure, router } from '@/libs/trpc/lambda';
10
9
  import { marketUserInfo, serverDatabase, telemetry } from '@/libs/trpc/lambda/middleware';
11
- import { generateTrustedClientToken } from '@/libs/trusted-client';
10
+ import { generateTrustedClientToken, isTrustedClientEnabled } from '@/libs/trusted-client';
12
11
  import { FileS3 } from '@/server/modules/S3';
13
12
  import { DiscoverService } from '@/server/services/discover';
14
13
  import { FileService } from '@/server/services/file';
@@ -69,21 +68,13 @@ const callCodeInterpreterToolSchema = z.object({
69
68
  userId: z.string(),
70
69
  });
71
70
 
72
- // Schema for getting export file upload URL
73
- const getExportFileUploadUrlSchema = z.object({
71
+ // Schema for export and upload file (combined operation)
72
+ const exportAndUploadFileSchema = z.object({
74
73
  filename: z.string(),
74
+ path: z.string(),
75
75
  topicId: z.string(),
76
76
  });
77
77
 
78
- // Schema for saving exported file content to document
79
- const saveExportedFileContentSchema = z.object({
80
- content: z.string(),
81
- fileId: z.string(),
82
- fileType: z.string(),
83
- filename: z.string(),
84
- url: z.string(),
85
- });
86
-
87
78
  // Schema for cloud MCP endpoint call
88
79
  const callCloudMcpEndpointSchema = z.object({
89
80
  apiParams: z.record(z.any()),
@@ -94,8 +85,7 @@ const callCloudMcpEndpointSchema = z.object({
94
85
 
95
86
  // ============================== Type Exports ==============================
96
87
  export type CallCodeInterpreterToolInput = z.infer<typeof callCodeInterpreterToolSchema>;
97
- export type GetExportFileUploadUrlInput = z.infer<typeof getExportFileUploadUrlSchema>;
98
- export type SaveExportedFileContentInput = z.infer<typeof saveExportedFileContentSchema>;
88
+ export type ExportAndUploadFileInput = z.infer<typeof exportAndUploadFileSchema>;
99
89
 
100
90
  export interface CallToolResult {
101
91
  error?: {
@@ -107,22 +97,16 @@ export interface CallToolResult {
107
97
  success: boolean;
108
98
  }
109
99
 
110
- export interface GetExportFileUploadUrlResult {
111
- downloadUrl: string;
112
- error?: {
113
- message: string;
114
- };
115
- key: string;
116
- success: boolean;
117
- uploadUrl: string;
118
- }
119
-
120
- export interface SaveExportedFileContentResult {
121
- documentId?: string;
100
+ export interface ExportAndUploadFileResult {
122
101
  error?: {
123
102
  message: string;
124
103
  };
104
+ fileId?: string;
105
+ filename: string;
106
+ mimeType?: string;
107
+ size?: number;
125
108
  success: boolean;
109
+ url?: string;
126
110
  }
127
111
 
128
112
  // ============================== Router ==============================
@@ -140,17 +124,26 @@ export const marketRouter = router({
140
124
  let result: { content: string; state: any; success: boolean } | undefined;
141
125
 
142
126
  try {
143
- // Query user_settings to get market.accessToken
144
- const userState = await ctx.userModel.getUserState(async () => ({}));
145
- const userAccessToken = userState.settings?.market?.accessToken;
146
-
147
- log('callCloudMcpEndpoint: userAccessToken exists=%s', !!userAccessToken);
148
-
149
- if (!userAccessToken) {
150
- throw new TRPCError({
151
- code: 'UNAUTHORIZED',
152
- message: 'User access token not found. Please sign in to Market first.',
153
- });
127
+ // Check if trusted client is enabled - if so, we don't need user's accessToken
128
+ const trustedClientEnabled = isTrustedClientEnabled();
129
+
130
+ let userAccessToken: string | undefined;
131
+
132
+ if (!trustedClientEnabled) {
133
+ // Query user_settings to get market.accessToken only if trusted client is not enabled
134
+ const userState = await ctx.userModel.getUserState(async () => ({}));
135
+ userAccessToken = userState.settings?.market?.accessToken;
136
+
137
+ log('callCloudMcpEndpoint: userAccessToken exists=%s', !!userAccessToken);
138
+
139
+ if (!userAccessToken) {
140
+ throw new TRPCError({
141
+ code: 'UNAUTHORIZED',
142
+ message: 'User access token not found. Please sign in to Market first.',
143
+ });
144
+ }
145
+ } else {
146
+ log('callCloudMcpEndpoint: using trusted client authentication');
154
147
  }
155
148
 
156
149
  const cloudResult = await ctx.discoverService.callCloudMcpEndpoint({
@@ -277,99 +270,122 @@ export const marketRouter = router({
277
270
  }),
278
271
 
279
272
  /**
280
- * Generate a pre-signed upload URL for exporting files from sandbox
273
+ * Export a file from sandbox and upload to S3, then create a persistent file record
274
+ * This combines the previous getExportFileUploadUrl + callCodeInterpreterTool + createFileRecord flow
275
+ * Returns a permanent /f/:id URL instead of a temporary pre-signed URL
281
276
  */
282
- getExportFileUploadUrl: marketToolProcedure
283
- .input(getExportFileUploadUrlSchema)
284
- .mutation(async ({ input }) => {
285
- const { filename, topicId } = input;
277
+ exportAndUploadFile: marketToolProcedure
278
+ .input(exportAndUploadFileSchema)
279
+ .mutation(async ({ input, ctx }) => {
280
+ const { path, filename, topicId } = input;
286
281
 
287
- log('Generating export file upload URL for: %s in topic: %s', filename, topicId);
282
+ log('Exporting and uploading file: %s from path: %s in topic: %s', filename, path, topicId);
288
283
 
289
284
  try {
290
285
  const s3 = new FileS3();
291
286
 
287
+ // Use date-based sharding for privacy compliance (GDPR, CCPA)
288
+ const today = new Date().toISOString().split('T')[0];
289
+
292
290
  // Generate a unique key for the exported file
293
- const key = `code-interpreter-exports/${topicId}/${filename}`;
291
+ const key = `code-interpreter-exports/${today}/${topicId}/${filename}`;
294
292
 
295
- // Generate pre-signed upload URL
293
+ // Step 1: Generate pre-signed upload URL
296
294
  const uploadUrl = await s3.createPreSignedUrl(key);
297
-
298
- // Generate download URL (pre-signed for preview)
299
- const downloadUrl = await s3.createPreSignedUrlForPreview(key);
300
-
301
295
  log('Generated upload URL for key: %s', key);
302
296
 
303
- return {
304
- downloadUrl,
305
- key,
306
- success: true,
307
- uploadUrl,
308
- } as GetExportFileUploadUrlResult;
309
- } catch (error) {
310
- log('Error generating export file upload URL: %O', error);
297
+ // Step 2: Generate trusted client token if user info is available
298
+ const trustedClientToken = ctx.marketUserInfo
299
+ ? generateTrustedClientToken(ctx.marketUserInfo)
300
+ : undefined;
301
+
302
+ // Only require user accessToken if trusted client is not available
303
+ let userAccessToken: string | undefined;
304
+ if (!trustedClientToken) {
305
+ const userState = await ctx.userModel.getUserState(async () => ({}));
306
+ userAccessToken = userState.settings?.market?.accessToken;
307
+
308
+ if (!userAccessToken) {
309
+ return {
310
+ error: { message: 'User access token not found. Please sign in to Market first.' },
311
+ filename,
312
+ success: false,
313
+ } as ExportAndUploadFileResult;
314
+ }
315
+ } else {
316
+ log('Using trusted client authentication for exportAndUploadFile');
317
+ }
311
318
 
312
- return {
313
- downloadUrl: '',
314
- error: {
315
- message: (error as Error).message,
316
- },
317
- key: '',
318
- success: false,
319
- uploadUrl: '',
320
- } as GetExportFileUploadUrlResult;
321
- }
322
- }),
319
+ // Initialize MarketSDK
320
+ const market = new MarketSDK({
321
+ accessToken: userAccessToken,
322
+ baseURL: process.env.NEXT_PUBLIC_MARKET_BASE_URL,
323
+ trustedClientToken,
324
+ });
323
325
 
324
- /**
325
- * Save exported file content to documents table
326
- */
327
- saveExportedFileContent: marketToolProcedure
328
- .input(saveExportedFileContentSchema)
329
- .mutation(async ({ ctx, input }) => {
330
- const { content, fileId, fileType, filename, url } = input;
326
+ // Step 3: Call sandbox's exportFile tool with the upload URL
327
+ const response = await market.plugins.runBuildInTool(
328
+ 'exportFile',
329
+ { path, uploadUrl },
330
+ { topicId, userId: ctx.userId },
331
+ );
331
332
 
332
- log('Saving exported file content: fileId=%s, filename=%s', fileId, filename);
333
+ log('Sandbox exportFile response: %O', response);
333
334
 
334
- try {
335
- const documentModel = new DocumentModel(ctx.serverDB, ctx.userId);
336
- const fileModel = new FileModel(ctx.serverDB, ctx.userId);
335
+ if (!response.success) {
336
+ return {
337
+ error: { message: response.error?.message || 'Failed to export file from sandbox' },
338
+ filename,
339
+ success: false,
340
+ } as ExportAndUploadFileResult;
341
+ }
342
+
343
+ const result = response.data?.result;
344
+ const uploadSuccess = result?.success !== false;
337
345
 
338
- // Verify the file exists
339
- const file = await fileModel.findById(fileId);
340
- if (!file) {
346
+ if (!uploadSuccess) {
341
347
  return {
342
- error: { message: 'File not found' },
348
+ error: { message: result?.error || 'Failed to upload file from sandbox' },
349
+ filename,
343
350
  success: false,
344
- } as SaveExportedFileContentResult;
351
+ } as ExportAndUploadFileResult;
345
352
  }
346
353
 
347
- // Create document record with the file content
348
- const document = await documentModel.create({
349
- content,
350
- fileId,
351
- fileType,
352
- filename,
353
- source: url,
354
- sourceType: 'file',
355
- title: filename,
356
- totalCharCount: content.length,
357
- totalLineCount: content.split('\n').length,
354
+ // Step 4: Get file metadata from S3 to verify upload and get actual size
355
+ const metadata = await s3.getFileMetadata(key);
356
+ const fileSize = metadata.contentLength;
357
+ const mimeType = metadata.contentType || result?.mimeType || 'application/octet-stream';
358
+
359
+ // Step 5: Create persistent file record using FileService
360
+ // Generate a simple hash from the key (since we don't have the actual file content)
361
+ const fileHash = sha256(key + Date.now().toString());
362
+
363
+ const { fileId, url } = await ctx.fileService.createFileRecord({
364
+ fileHash,
365
+ fileType: mimeType,
366
+ name: filename,
367
+ size: fileSize,
368
+ url: key, // Store S3 key
358
369
  });
359
370
 
360
- log('Created document for exported file: documentId=%s, fileId=%s', document.id, fileId);
371
+ log('Created file record: fileId=%s, url=%s', fileId, url);
361
372
 
362
373
  return {
363
- documentId: document.id,
374
+ fileId,
375
+ filename,
376
+ mimeType,
377
+ size: fileSize,
364
378
  success: true,
365
- } as SaveExportedFileContentResult;
379
+ url, // This is the permanent /f/:id URL
380
+ } as ExportAndUploadFileResult;
366
381
  } catch (error) {
367
- log('Error saving exported file content: %O', error);
382
+ log('Error in exportAndUploadFile: %O', error);
368
383
 
369
384
  return {
370
385
  error: { message: (error as Error).message },
386
+ filename,
371
387
  success: false,
372
- } as SaveExportedFileContentResult;
388
+ } as ExportAndUploadFileResult;
373
389
  }
374
390
  }),
375
391
  });
@@ -149,7 +149,7 @@ export class DiscoverService {
149
149
  apiParams: Record<string, any>;
150
150
  identifier: string;
151
151
  toolName: string;
152
- userAccessToken: string;
152
+ userAccessToken?: string;
153
153
  }) {
154
154
  log('callCloudMcpEndpoint: params=%O', {
155
155
  apiParams: params.apiParams,
@@ -159,7 +159,14 @@ export class DiscoverService {
159
159
  });
160
160
 
161
161
  try {
162
- // Call cloud gateway with user access token in Authorization header
162
+ // Build headers - only include Authorization if userAccessToken is provided
163
+ // When userAccessToken is not provided, MarketSDK will use trustedClientToken for authentication
164
+ const headers: Record<string, string> = {};
165
+ if (params.userAccessToken) {
166
+ headers.Authorization = `Bearer ${params.userAccessToken}`;
167
+ }
168
+
169
+ // Call cloud gateway with optional user access token in Authorization header
163
170
  const result = await this.market.plugins.callCloudGateway(
164
171
  {
165
172
  apiParams: params.apiParams,
@@ -167,9 +174,7 @@ export class DiscoverService {
167
174
  toolName: params.toolName,
168
175
  },
169
176
  {
170
- headers: {
171
- Authorization: `Bearer ${params.userAccessToken}`,
172
- },
177
+ headers,
173
178
  },
174
179
  );
175
180
 
@@ -2,10 +2,8 @@ import { toolsClient } from '@/libs/trpc/client';
2
2
  import type {
3
3
  CallCodeInterpreterToolInput,
4
4
  CallToolResult,
5
- GetExportFileUploadUrlInput,
6
- GetExportFileUploadUrlResult,
7
- SaveExportedFileContentInput,
8
- SaveExportedFileContentResult,
5
+ ExportAndUploadFileInput,
6
+ ExportAndUploadFileResult,
9
7
  } from '@/server/routers/tools/market';
10
8
  import { useUserStore } from '@/store/user';
11
9
  import { settingsSelectors } from '@/store/user/slices/settings/selectors/settings';
@@ -44,31 +42,25 @@ class CodeInterpreterService {
44
42
  }
45
43
 
46
44
  /**
47
- * Get a pre-signed upload URL for exporting a file from the sandbox
45
+ * Export a file from sandbox and upload to S3, then create a persistent file record
46
+ * This is a single call that combines: getUploadUrl + callTool(exportFile) + createFileRecord
47
+ * Returns a permanent /f/:id URL instead of a temporary pre-signed URL
48
+ * @param path - The file path in the sandbox
48
49
  * @param filename - The name of the file to export
49
50
  * @param topicId - The topic ID for organizing files
50
51
  */
51
- async getExportFileUploadUrl(
52
+ async exportAndUploadFile(
53
+ path: string,
52
54
  filename: string,
53
55
  topicId: string,
54
- ): Promise<GetExportFileUploadUrlResult> {
55
- const input: GetExportFileUploadUrlInput = {
56
+ ): Promise<ExportAndUploadFileResult> {
57
+ const input: ExportAndUploadFileInput = {
56
58
  filename,
59
+ path,
57
60
  topicId,
58
61
  };
59
62
 
60
- return toolsClient.market.getExportFileUploadUrl.mutate(input);
61
- }
62
-
63
- /**
64
- * Save exported file content to documents table
65
- * This creates a document record linked to the file for content retrieval
66
- * @param params - File content and metadata
67
- */
68
- async saveExportedFileContent(
69
- params: SaveExportedFileContentInput,
70
- ): Promise<SaveExportedFileContentResult> {
71
- return toolsClient.market.saveExportedFileContent.mutate(params);
63
+ return toolsClient.market.exportAndUploadFile.mutate(input);
72
64
  }
73
65
  }
74
66