Skip to content

Commit 64758af

Browse files
committed
improvement(mothership): docs
1 parent 8c09e19 commit 64758af

File tree

7 files changed

+254
-21
lines changed

7 files changed

+254
-21
lines changed

apps/sim/app/api/function/execute/route.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -593,6 +593,7 @@ async function maybeExportSandboxFileToWorkspace(args: {
593593
workspaceId?: string
594594
outputPath?: string
595595
outputFormat?: string
596+
outputMimeType?: string
596597
outputSandboxPath?: string
597598
exportedFileContent?: string
598599
stdout: string
@@ -604,6 +605,7 @@ async function maybeExportSandboxFileToWorkspace(args: {
604605
workspaceId,
605606
outputPath,
606607
outputFormat,
608+
outputMimeType,
607609
outputSandboxPath,
608610
exportedFileContent,
609611
stdout,
@@ -650,14 +652,23 @@ async function maybeExportSandboxFileToWorkspace(args: {
650652
}
651653

652654
const fileName = normalizeOutputWorkspaceFileName(outputPath)
653-
const format = resolveOutputFormat(fileName, outputFormat)
654-
const contentType = FORMAT_TO_CONTENT_TYPE[format]
655+
656+
const TEXT_MIMES = new Set(Object.values(FORMAT_TO_CONTENT_TYPE))
657+
const resolvedMimeType =
658+
outputMimeType ||
659+
FORMAT_TO_CONTENT_TYPE[resolveOutputFormat(fileName, outputFormat)] ||
660+
'application/octet-stream'
661+
const isBinary = !TEXT_MIMES.has(resolvedMimeType)
662+
const fileBuffer = isBinary
663+
? Buffer.from(exportedFileContent, 'base64')
664+
: Buffer.from(exportedFileContent, 'utf-8')
665+
655666
const uploaded = await uploadWorkspaceFile(
656667
resolvedWorkspaceId,
657668
authUserId,
658-
Buffer.from(exportedFileContent, 'utf-8'),
669+
fileBuffer,
659670
fileName,
660-
contentType
671+
resolvedMimeType
661672
)
662673

663674
return NextResponse.json({
@@ -702,6 +713,7 @@ export async function POST(req: NextRequest) {
702713
language = DEFAULT_CODE_LANGUAGE,
703714
outputPath,
704715
outputFormat,
716+
outputMimeType,
705717
outputSandboxPath,
706718
envVars = {},
707719
blockData = {},
@@ -815,6 +827,7 @@ export async function POST(req: NextRequest) {
815827
workspaceId,
816828
outputPath,
817829
outputFormat,
830+
outputMimeType,
818831
outputSandboxPath,
819832
exportedFileContent,
820833
stdout: shellStdout,
@@ -938,6 +951,7 @@ export async function POST(req: NextRequest) {
938951
workspaceId,
939952
outputPath,
940953
outputFormat,
954+
outputMimeType,
941955
outputSandboxPath,
942956
exportedFileContent,
943957
stdout,
@@ -1019,6 +1033,7 @@ export async function POST(req: NextRequest) {
10191033
workspaceId,
10201034
outputPath,
10211035
outputFormat,
1036+
outputMimeType,
10221037
outputSandboxPath,
10231038
exportedFileContent,
10241039
stdout,

apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ export function FileViewer({
141141
}
142142

143143
if (category === 'iframe-previewable') {
144-
return <IframePreview file={file} />
144+
return <IframePreview file={file} workspaceId={workspaceId} />
145145
}
146146

147147
if (category === 'image-previewable') {
@@ -437,13 +437,36 @@ function TextEditor({
437437
)
438438
}
439439

440-
const IframePreview = memo(function IframePreview({ file }: { file: WorkspaceFileRecord }) {
441-
const serveUrl = `/api/files/serve/${encodeURIComponent(file.key)}?context=workspace`
440+
const IframePreview = memo(function IframePreview({
441+
file,
442+
workspaceId,
443+
}: {
444+
file: WorkspaceFileRecord
445+
workspaceId: string
446+
}) {
447+
const { data: fileData, isLoading } = useWorkspaceFileBinary(workspaceId, file.id, file.key)
448+
const [blobUrl, setBlobUrl] = useState<string | null>(null)
449+
450+
useEffect(() => {
451+
if (!fileData) return
452+
const blob = new Blob([fileData], { type: 'application/pdf' })
453+
const url = URL.createObjectURL(blob)
454+
setBlobUrl(url)
455+
return () => URL.revokeObjectURL(url)
456+
}, [fileData])
457+
458+
if (isLoading || !blobUrl) {
459+
return (
460+
<div className='flex h-full items-center justify-center'>
461+
<Skeleton className='h-[200px] w-[80%]' />
462+
</div>
463+
)
464+
}
442465

443466
return (
444467
<div className='flex flex-1 overflow-hidden'>
445468
<iframe
446-
src={serveUrl}
469+
src={blobUrl}
447470
className='h-full w-full border-0'
448471
title={file.name}
449472
onError={() => {

apps/sim/lib/copilot/tools/handlers/function-execute.ts

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,109 @@
1+
import { createLogger } from '@sim/logger'
2+
import { getTableById, queryRows } from '@/lib/table/service'
3+
import {
4+
downloadWorkspaceFile,
5+
findWorkspaceFileRecord,
6+
getSandboxWorkspaceFilePath,
7+
listWorkspaceFiles,
8+
} from '@/lib/uploads/contexts/workspace/workspace-file-manager'
19
import { executeTool as executeAppTool } from '@/tools'
210
import type { ToolExecutionContext, ToolExecutionResult } from '../../tool-executor/types'
311

12+
const logger = createLogger('CopilotFunctionExecute')
13+
14+
const MAX_FILE_SIZE = 10 * 1024 * 1024
15+
const MAX_TOTAL_SIZE = 50 * 1024 * 1024
16+
17+
interface SandboxFile {
18+
path: string
19+
content: string
20+
}
21+
22+
async function resolveInputFiles(
23+
workspaceId: string,
24+
inputFiles?: unknown[],
25+
inputTables?: unknown[]
26+
): Promise<SandboxFile[]> {
27+
const sandboxFiles: SandboxFile[] = []
28+
let totalSize = 0
29+
30+
if (inputFiles?.length && workspaceId) {
31+
const allFiles = await listWorkspaceFiles(workspaceId)
32+
for (const fileRef of inputFiles) {
33+
if (typeof fileRef !== 'string') continue
34+
const record = findWorkspaceFileRecord(allFiles, fileRef)
35+
if (!record) {
36+
logger.warn('Input file not found', { fileRef })
37+
continue
38+
}
39+
if (record.size > MAX_FILE_SIZE) {
40+
logger.warn('Input file exceeds size limit', { fileId: record.id, size: record.size })
41+
continue
42+
}
43+
if (totalSize + record.size > MAX_TOTAL_SIZE) {
44+
logger.warn('Total input size limit reached')
45+
break
46+
}
47+
const buffer = await downloadWorkspaceFile(record)
48+
totalSize += buffer.length
49+
const isText = /^text\/|application\/json|application\/xml|application\/csv/.test(
50+
record.type || ''
51+
)
52+
const content = isText ? buffer.toString('utf-8') : buffer.toString('base64')
53+
sandboxFiles.push({
54+
path: getSandboxWorkspaceFilePath(record),
55+
content,
56+
encoding: isText ? undefined : 'base64',
57+
} as SandboxFile)
58+
}
59+
}
60+
61+
if (inputTables?.length) {
62+
for (const tableId of inputTables) {
63+
if (typeof tableId !== 'string') continue
64+
const table = await getTableById(tableId)
65+
if (!table) {
66+
logger.warn('Input table not found', { tableId })
67+
continue
68+
}
69+
const rows = await queryRows(tableId, workspaceId, {}, 'copilot-fn-exec')
70+
if (!rows.rows?.length) continue
71+
72+
const allKeys = new Set<string>()
73+
for (const row of rows.rows) {
74+
if (row.data && typeof row.data === 'object') {
75+
for (const key of Object.keys(row.data as Record<string, unknown>)) {
76+
allKeys.add(key)
77+
}
78+
}
79+
}
80+
const headers = Array.from(allKeys)
81+
const csvLines = [headers.join(',')]
82+
for (const row of rows.rows) {
83+
const data = (row.data || {}) as Record<string, unknown>
84+
csvLines.push(
85+
headers
86+
.map((h) => {
87+
const val = data[h]
88+
const str = val === null || val === undefined ? '' : String(val)
89+
return str.includes(',') || str.includes('"') || str.includes('\n')
90+
? `"${str.replace(/"/g, '""')}"`
91+
: str
92+
})
93+
.join(',')
94+
)
95+
}
96+
const csvContent = csvLines.join('\n')
97+
sandboxFiles.push({
98+
path: `/home/user/tables/${tableId}.csv`,
99+
content: csvContent,
100+
})
101+
}
102+
}
103+
104+
return sandboxFiles
105+
}
106+
4107
export async function executeFunctionExecute(
5108
params: Record<string, unknown>,
6109
context: ToolExecutionContext
@@ -14,6 +117,19 @@ export async function executeFunctionExecute(
14117
}
15118
}
16119

120+
if (context.workspaceId) {
121+
const inputFiles = enrichedParams.inputFiles as unknown[] | undefined
122+
const inputTables = enrichedParams.inputTables as unknown[] | undefined
123+
124+
if (inputFiles?.length || inputTables?.length) {
125+
const resolved = await resolveInputFiles(context.workspaceId, inputFiles, inputTables)
126+
if (resolved.length > 0) {
127+
const existing = (enrichedParams._sandboxFiles as SandboxFile[]) || []
128+
enrichedParams._sandboxFiles = [...existing, ...resolved]
129+
}
130+
}
131+
}
132+
17133
enrichedParams._context = {
18134
...(typeof enrichedParams._context === 'object' && enrichedParams._context !== null
19135
? (enrichedParams._context as object)

apps/sim/lib/execution/doc-worker.cjs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,16 @@ const FORMATS = {
3939
async setup() {
4040
const PDFLib = require('pdf-lib')
4141
const pdf = await PDFLib.PDFDocument.create()
42-
return { globals: { PDFLib, pdf }, pdf }
42+
43+
async function embedImage(dataUri) {
44+
const base64 = dataUri.split(',')[1]
45+
const bytes = Buffer.from(base64, 'base64')
46+
const mime = dataUri.split(';')[0].split(':')[1] || ''
47+
if (mime.includes('png')) return pdf.embedPng(bytes)
48+
return pdf.embedJpg(bytes)
49+
}
50+
51+
return { globals: { PDFLib, pdf, embedImage }, pdf }
4352
},
4453
async serialize(ctx) {
4554
const pdf = ctx.globals.pdf

apps/sim/lib/execution/e2b.ts

Lines changed: 69 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { CodeLanguage } from '@/lib/execution/languages'
66
export interface SandboxFile {
77
path: string
88
content: string
9+
encoding?: 'base64'
910
}
1011

1112
export interface E2BExecutionRequest {
@@ -49,7 +50,15 @@ export async function executeInE2B(req: E2BExecutionRequest): Promise<E2BExecuti
4950

5051
if (req.sandboxFiles?.length) {
5152
for (const file of req.sandboxFiles) {
52-
await sandbox.files.write(file.path, file.content)
53+
if (file.encoding === 'base64') {
54+
const buf = Buffer.from(file.content, 'base64')
55+
await sandbox.files.write(
56+
file.path,
57+
buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength)
58+
)
59+
} else {
60+
await sandbox.files.write(file.path, file.content)
61+
}
5362
}
5463
logger.info('Wrote sandbox input files', {
5564
sandboxId,
@@ -125,9 +134,30 @@ export async function executeInE2B(req: E2BExecutionRequest): Promise<E2BExecuti
125134
}
126135
}
127136

128-
const exportedFileContent = outputSandboxPath
129-
? await sandbox.files.read(outputSandboxPath)
130-
: undefined
137+
let exportedFileContent: string | undefined
138+
if (outputSandboxPath) {
139+
const ext = outputSandboxPath.slice(outputSandboxPath.lastIndexOf('.')).toLowerCase()
140+
const binaryExts = new Set([
141+
'.png',
142+
'.jpg',
143+
'.jpeg',
144+
'.gif',
145+
'.webp',
146+
'.pdf',
147+
'.zip',
148+
'.mp3',
149+
'.mp4',
150+
'.docx',
151+
'.pptx',
152+
'.xlsx',
153+
])
154+
if (binaryExts.has(ext)) {
155+
const b64Result = await sandbox.commands.run(`base64 -w0 "${outputSandboxPath}"`)
156+
exportedFileContent = b64Result.stdout
157+
} else {
158+
exportedFileContent = await sandbox.files.read(outputSandboxPath)
159+
}
160+
}
131161

132162
return {
133163
result,
@@ -164,7 +194,15 @@ export async function executeShellInE2B(
164194

165195
if (req.sandboxFiles?.length) {
166196
for (const file of req.sandboxFiles) {
167-
await sandbox.files.write(file.path, file.content)
197+
if (file.encoding === 'base64') {
198+
const buf = Buffer.from(file.content, 'base64')
199+
await sandbox.files.write(
200+
file.path,
201+
buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength)
202+
)
203+
} else {
204+
await sandbox.files.write(file.path, file.content)
205+
}
168206
}
169207
logger.info('Wrote sandbox input files', {
170208
sandboxId,
@@ -237,9 +275,32 @@ export async function executeShellInE2B(
237275
cleanedStdout = filteredLines.join('\n')
238276
}
239277

240-
const exportedFileContent = outputSandboxPath
241-
? await sandbox.files.read(outputSandboxPath)
242-
: undefined
278+
let exportedFileContent: string | undefined
279+
if (outputSandboxPath) {
280+
const ext = outputSandboxPath.slice(outputSandboxPath.lastIndexOf('.')).toLowerCase()
281+
const binaryExts = new Set([
282+
'.png',
283+
'.jpg',
284+
'.jpeg',
285+
'.gif',
286+
'.webp',
287+
'.pdf',
288+
'.zip',
289+
'.mp3',
290+
'.mp4',
291+
'.docx',
292+
'.pptx',
293+
'.xlsx',
294+
])
295+
if (binaryExts.has(ext)) {
296+
const b64Result = await sandbox.commands.run(`base64 -w0 "${outputSandboxPath}"`, {
297+
user: 'root',
298+
})
299+
exportedFileContent = b64Result.stdout
300+
} else {
301+
exportedFileContent = await sandbox.files.read(outputSandboxPath)
302+
}
303+
}
243304

244305
return { result: parsed, stdout: cleanedStdout, sandboxId, exportedFileContent }
245306
} finally {

0 commit comments

Comments
 (0)