@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.
- package/CHANGELOG.md +58 -0
- package/changelog/v1.json +21 -0
- package/package.json +2 -2
- package/packages/builtin-tool-cloud-sandbox/src/ExecutionRuntime/index.ts +18 -31
- package/packages/builtin-tool-cloud-sandbox/src/types.ts +3 -3
- package/src/app/[variants]/(main)/chat/_layout/Sidebar/Topic/List/Item/Actions.tsx +1 -1
- package/src/app/[variants]/(main)/chat/_layout/Sidebar/Topic/TopicListContent/ThreadList/ThreadItem/Actions.tsx +1 -1
- package/src/app/[variants]/(main)/chat/profile/features/EditorCanvas/TypoBar.tsx +1 -11
- package/src/app/[variants]/(main)/group/_layout/Sidebar/Topic/List/Item/Actions.tsx +1 -1
- package/src/app/[variants]/(main)/group/_layout/Sidebar/Topic/TopicListContent/ThreadList/ThreadItem/Actions.tsx +1 -1
- package/src/app/[variants]/(main)/group/profile/features/EditorCanvas/TypoBar.tsx +1 -11
- package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/Item/Actions.tsx +1 -1
- package/src/app/[variants]/(main)/home/_layout/Body/Project/List/Actions.tsx +1 -1
- package/src/app/[variants]/(main)/home/_layout/Footer/index.tsx +3 -9
- package/src/app/[variants]/(main)/home/_layout/Header/components/AddButton.tsx +1 -1
- package/src/app/[variants]/(main)/memory/features/EditableModal/index.tsx +8 -101
- package/src/app/[variants]/(main)/page/_layout/Body/List/Item/Actions.tsx +1 -1
- package/src/app/[variants]/(main)/resource/(home)/_layout/Body/LibraryList/List/Item/Actions.tsx +1 -1
- package/src/features/ChatInput/InputEditor/index.tsx +1 -0
- package/src/features/ChatInput/TypoBar/index.tsx +0 -11
- package/src/features/Conversation/ChatItem/components/MessageContent/index.tsx +11 -12
- package/src/features/EditorModal/EditorCanvas.tsx +81 -0
- package/src/features/EditorModal/TextareCanvas.tsx +28 -0
- package/src/{app/[variants]/(main)/memory/features/EditableModal → features/EditorModal}/Typobar.tsx +0 -11
- package/src/features/EditorModal/index.tsx +51 -0
- package/src/features/PageEditor/Copilot/TopicSelector/Actions.tsx +1 -1
- package/src/features/PageEditor/EditorCanvas/InlineToolbar.tsx +1 -17
- package/src/features/ResourceManager/components/Explorer/ItemDropdown/DropdownMenu.tsx +1 -1
- package/src/features/User/UserPanel/ThemeButton.tsx +1 -1
- package/src/server/routers/tools/market.ts +118 -102
- package/src/server/services/discover/index.ts +10 -5
- package/src/services/codeInterpreter.ts +12 -20
- package/src/store/chat/slices/plugin/actions/pluginTypes.ts +13 -86
- package/src/features/Conversation/ChatItem/components/MessageContent/EditableModal.tsx +0 -119
- 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;
|
package/src/{app/[variants]/(main)/memory/features/EditableModal → features/EditorModal}/Typobar.tsx
RENAMED
|
@@ -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
|
|
73
|
-
const
|
|
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
|
|
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
|
|
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
|
-
//
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
283
|
-
.input(
|
|
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('
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
-
|
|
333
|
+
log('Sandbox exportFile response: %O', response);
|
|
333
334
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
-
|
|
339
|
-
const file = await fileModel.findById(fileId);
|
|
340
|
-
if (!file) {
|
|
346
|
+
if (!uploadSuccess) {
|
|
341
347
|
return {
|
|
342
|
-
error: { message: '
|
|
348
|
+
error: { message: result?.error || 'Failed to upload file from sandbox' },
|
|
349
|
+
filename,
|
|
343
350
|
success: false,
|
|
344
|
-
} as
|
|
351
|
+
} as ExportAndUploadFileResult;
|
|
345
352
|
}
|
|
346
353
|
|
|
347
|
-
//
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
|
371
|
+
log('Created file record: fileId=%s, url=%s', fileId, url);
|
|
361
372
|
|
|
362
373
|
return {
|
|
363
|
-
|
|
374
|
+
fileId,
|
|
375
|
+
filename,
|
|
376
|
+
mimeType,
|
|
377
|
+
size: fileSize,
|
|
364
378
|
success: true,
|
|
365
|
-
|
|
379
|
+
url, // This is the permanent /f/:id URL
|
|
380
|
+
} as ExportAndUploadFileResult;
|
|
366
381
|
} catch (error) {
|
|
367
|
-
log('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
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
6
|
-
|
|
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
|
-
*
|
|
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
|
|
52
|
+
async exportAndUploadFile(
|
|
53
|
+
path: string,
|
|
52
54
|
filename: string,
|
|
53
55
|
topicId: string,
|
|
54
|
-
): Promise<
|
|
55
|
-
const input:
|
|
56
|
+
): Promise<ExportAndUploadFileResult> {
|
|
57
|
+
const input: ExportAndUploadFileInput = {
|
|
56
58
|
filename,
|
|
59
|
+
path,
|
|
57
60
|
topicId,
|
|
58
61
|
};
|
|
59
62
|
|
|
60
|
-
return toolsClient.market.
|
|
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
|
|