diff --git a/apps/sim/app/api/workspaces/[id]/files/[fileId]/compiled-check/route.ts b/apps/sim/app/api/workspaces/[id]/files/[fileId]/compiled-check/route.ts new file mode 100644 index 0000000000..cbc7c7514c --- /dev/null +++ b/apps/sim/app/api/workspaces/[id]/files/[fileId]/compiled-check/route.ts @@ -0,0 +1,90 @@ +import { createLogger } from '@sim/logger' +import { toError } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { getSession } from '@/lib/auth' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { BINARY_DOC_TASKS, MAX_DOCUMENT_PREVIEW_CODE_BYTES } from '@/lib/execution/constants' +import { runSandboxTask, SandboxUserCodeError } from '@/lib/execution/sandbox/run-task' +import { downloadWorkspaceFile, getWorkspaceFile } from '@/lib/uploads/contexts/workspace' +import { verifyWorkspaceMembership } from '@/app/api/workflows/utils' + +export const dynamic = 'force-dynamic' +export const runtime = 'nodejs' + +const logger = createLogger('WorkspaceFileCompiledCheckAPI') + +/** + * GET /api/workspaces/[id]/files/[fileId]/compiled-check + * + * Compiles the saved JavaScript source of a .docx / .pptx / .pdf file and + * returns whether it succeeds. Used by the file agent to self-verify generated + * code before finalising an edit. + * + * Returns: + * 200 { ok: true } + * 200 { ok: false, error: string, errorName: string } — user code error + * 4xx on auth / missing file / unsupported extension + * 500 on system (sandbox infra) failure + */ +export const GET = withRouteHandler( + async (request: NextRequest, { params }: { params: Promise<{ id: string; fileId: string }> }) => { + const { id: workspaceId, fileId } = await params + + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const membership = await verifyWorkspaceMembership(session.user.id, workspaceId) + if (!membership) { + return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 }) + } + + const fileRecord = await getWorkspaceFile(workspaceId, fileId) + if (!fileRecord) { + return NextResponse.json({ error: 'File not found' }, { status: 404 }) + } + + const ext = fileRecord.name.split('.').pop()?.toLowerCase() ?? '' + const taskId = BINARY_DOC_TASKS[ext] + if (!taskId) { + return NextResponse.json( + { error: `Compiled check only supports .docx, .pptx, and .pdf files` }, + { status: 422 } + ) + } + + let buffer: Buffer + try { + buffer = await downloadWorkspaceFile(fileRecord) + } catch (err) { + logger.error('Failed to download file for compiled check', { + fileId, + error: toError(err).message, + }) + return NextResponse.json({ error: 'Failed to read file' }, { status: 500 }) + } + + const code = buffer.toString('utf-8') + + if (Buffer.byteLength(code, 'utf-8') > MAX_DOCUMENT_PREVIEW_CODE_BYTES) { + return NextResponse.json({ error: 'File source exceeds maximum size' }, { status: 413 }) + } + + try { + await runSandboxTask(taskId, { code, workspaceId }, { ownerKey: `user:${session.user.id}` }) + return NextResponse.json({ ok: true }) + } catch (err) { + if (err instanceof SandboxUserCodeError) { + logger.info('Compiled check failed with user code error', { + fileId, + taskId, + error: toError(err).message, + errorName: err.name, + }) + return NextResponse.json({ ok: false, error: toError(err).message, errorName: err.name }) + } + throw err + } + } +) diff --git a/apps/sim/app/api/workspaces/[id]/files/[fileId]/style/route.ts b/apps/sim/app/api/workspaces/[id]/files/[fileId]/style/route.ts new file mode 100644 index 0000000000..a07706da0f --- /dev/null +++ b/apps/sim/app/api/workspaces/[id]/files/[fileId]/style/route.ts @@ -0,0 +1,81 @@ +import { createLogger } from '@sim/logger' +import { toError } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { getSession } from '@/lib/auth' +import { extractDocumentStyle } from '@/lib/copilot/vfs/document-style' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { downloadWorkspaceFile, getWorkspaceFile } from '@/lib/uploads/contexts/workspace' +import { verifyWorkspaceMembership } from '@/app/api/workflows/utils' + +export const dynamic = 'force-dynamic' +export const runtime = 'nodejs' + +const logger = createLogger('WorkspaceFileStyleAPI') + +/** + * GET /api/workspaces/[id]/files/[fileId]/style + * Extract a compact JSON style summary from an uploaded .docx or .pptx file. + * Uses OOXML theme XML to return theme colors, font pair, and named styles. + * Only works on binary OOXML files (ZIP format) — not on JS source files. + */ +export const GET = withRouteHandler( + async (request: NextRequest, { params }: { params: Promise<{ id: string; fileId: string }> }) => { + const { id: workspaceId, fileId } = await params + + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const membership = await verifyWorkspaceMembership(session.user.id, workspaceId) + if (!membership) { + return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 }) + } + + const fileRecord = await getWorkspaceFile(workspaceId, fileId) + if (!fileRecord) { + return NextResponse.json({ error: 'File not found' }, { status: 404 }) + } + + const rawExt = fileRecord.name.split('.').pop()?.toLowerCase() + if (rawExt !== 'docx' && rawExt !== 'pptx') { + return NextResponse.json( + { error: 'Style extraction only supports .docx and .pptx files' }, + { status: 422 } + ) + } + const ext: 'docx' | 'pptx' = rawExt + + let buffer: Buffer + try { + buffer = await downloadWorkspaceFile(fileRecord) + } catch (err) { + logger.error('Failed to download file for style extraction', { + fileId, + error: toError(err).message, + }) + return NextResponse.json({ error: 'Failed to read file' }, { status: 500 }) + } + + const summary = await extractDocumentStyle(buffer, ext) + if (!summary) { + return NextResponse.json( + { + error: + 'File is not a compiled binary document — style extraction requires an uploaded or compiled .docx/.pptx file', + }, + { status: 422 } + ) + } + + logger.info('Extracted style summary via API', { + fileId, + format: ext, + themeName: summary.theme.name, + }) + + return NextResponse.json(summary, { + headers: { 'Cache-Control': 'private, max-age=300' }, + }) + } +) diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/docx-preview.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/docx-preview.tsx index a8228dfc08..45a68d164a 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/docx-preview.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/docx-preview.tsx @@ -6,12 +6,7 @@ import { toError } from '@sim/utils/errors' import { cn } from '@/lib/core/utils/cn' import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace' import { useWorkspaceFileBinary } from '@/hooks/queries/workspace-files' -import { - PDF_PAGE_SKELETON, - PreviewError, - resolvePreviewError, - shouldSuppressStreamingDocumentError, -} from './preview-shared' +import { PDF_PAGE_SKELETON, PreviewError, resolvePreviewError } from './preview-shared' const logger = createLogger('DocxPreview') @@ -94,7 +89,6 @@ export const DocxPreview = memo(function DocxPreview({ try { setRendering(true) - setRenderError(null) const response = await fetch(`/api/workspaces/${workspaceId}/docx/preview`, { method: 'POST', @@ -136,12 +130,7 @@ export const DocxPreview = memo(function DocxPreview({ setHasRenderedPreview(true) } const msg = toError(err).message || 'Failed to render document' - if (previousHtml || shouldSuppressStreamingDocumentError(msg)) { - logger.info('Suppressing transient DOCX streaming preview error', { error: msg }) - } else { - logger.error('DOCX render failed', { error: msg }) - setRenderError(msg) - } + logger.info('Transient DOCX streaming preview error (suppressed)', { error: msg }) } } finally { if (!cancelled) { @@ -157,17 +146,11 @@ export const DocxPreview = memo(function DocxPreview({ } }, [streamingContent, workspaceId]) - const error = - hasRenderedPreview && streamingContent !== undefined - ? null - : streamingContent !== undefined - ? renderError - : resolvePreviewError(fetchError, renderError) + const error = streamingContent !== undefined ? null : resolvePreviewError(fetchError, renderError) if (error) return const showSkeleton = - !hasRenderedPreview && - ((streamingContent !== undefined && rendering) || (streamingContent === undefined && isLoading)) + !hasRenderedPreview && (streamingContent !== undefined || isLoading || rendering) return (
diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx index 6ac5593405..55b00d8e4e 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx @@ -18,12 +18,7 @@ import { ImagePreview } from './image-preview' import type { PdfDocumentSource } from './pdf-viewer' import { PptxPreview } from './pptx-preview' import { resolvePreviewType } from './preview-panel' -import { - PDF_PAGE_SKELETON, - PreviewError, - resolvePreviewError, - shouldSuppressStreamingDocumentError, -} from './preview-shared' +import { PDF_PAGE_SKELETON, PreviewError, resolvePreviewError } from './preview-shared' import { TextEditor } from './text-editor' import { XlsxPreview } from './xlsx-preview' @@ -128,7 +123,14 @@ export function FileViewer({ } if (category === 'pptx-previewable') { - return + return ( + + ) } if (category === 'xlsx-previewable') { @@ -160,7 +162,6 @@ const IframePreview = memo(function IframePreview({ const streamingBufferSeqRef = useRef(0) const [streamingBufferSeq, setStreamingBufferSeq] = useState(0) const [rendering, setRendering] = useState(false) - const [renderError, setRenderError] = useState(null) useEffect(() => { if (streamingContent === undefined) return @@ -173,7 +174,6 @@ const IframePreview = memo(function IframePreview({ try { setRendering(true) - setRenderError(null) const response = await fetch(`/api/workspaces/${workspaceId}/pdf/preview`, { method: 'POST', @@ -196,12 +196,7 @@ const IframePreview = memo(function IframePreview({ } catch (err) { if (!cancelled && !(err instanceof DOMException && err.name === 'AbortError')) { const msg = toError(err).message || 'Failed to render PDF' - if (streamingBufferRef.current || shouldSuppressStreamingDocumentError(msg)) { - logger.info('Suppressing transient PDF streaming preview error', { error: msg }) - } else { - logger.error('PDF render failed', { error: msg }) - setRenderError(msg) - } + logger.info('Transient PDF streaming preview error (suppressed)', { error: msg }) } } finally { if (!cancelled) setRendering(false) @@ -228,8 +223,6 @@ const IframePreview = memo(function IframePreview({ [streamingBuffer] ) - if (renderError) return - if (streamingContent !== undefined) { if (!streamingSource) { return
{PDF_PAGE_SKELETON}
@@ -361,7 +354,7 @@ const UnsupportedPreview = memo(function UnsupportedPreview({ return (
-

+

Preview not available{ext ? ` for .${ext} files` : ' for this file'}

diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/pptx-preview.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/pptx-preview.tsx index 41ac2a1a22..5047cbc966 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/pptx-preview.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/pptx-preview.tsx @@ -1,16 +1,12 @@ 'use client' -import { useEffect, useState } from 'react' +import { memo, useEffect, useState } from 'react' import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import { Skeleton } from '@/components/emcn' import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace' import { useWorkspaceFileBinary } from '@/hooks/queries/workspace-files' -import { - PreviewError, - resolvePreviewError, - shouldSuppressStreamingDocumentError, -} from './preview-shared' +import { PreviewError, resolvePreviewError } from './preview-shared' const logger = createLogger('PptxPreview') @@ -44,15 +40,6 @@ function pptxCacheKey(fileId: string, dataUpdatedAt: number, byteLength: number) return `${fileId}:${dataUpdatedAt}:${byteLength}` } -function shouldSuppressStreamingPptxError(message: string): boolean { - return ( - shouldSuppressStreamingDocumentError(message) || - message.includes('SyntaxError: Invalid or unexpected token') || - message.includes('PPTX generation cancelled') || - message.includes('SyntaxError: Unexpected end of input') - ) -} - function pptxCacheSet(key: string, slides: string[]): void { pptxSlideCache.set(key, slides) if (pptxSlideCache.size > 5) { @@ -136,7 +123,7 @@ async function getPptxRenderSize( } } -export function PptxPreview({ +export const PptxPreview = memo(function PptxPreview({ file, workspaceId, streamingContent, @@ -169,7 +156,6 @@ export function PptxPreview({ if (cancelled) return try { setRendering(true) - setRenderError(null) const response = await fetch(`/api/workspaces/${workspaceId}/pptx/preview`, { method: 'POST', @@ -197,12 +183,7 @@ export function PptxPreview({ } catch (err) { if (!cancelled && !(err instanceof DOMException && err.name === 'AbortError')) { const msg = toError(err).message || 'Failed to render presentation' - if (shouldSuppressStreamingPptxError(msg)) { - logger.info('Suppressing transient PPTX streaming preview error', { error: msg }) - } else { - logger.error('PPTX render failed', { error: msg }) - setRenderError(msg) - } + logger.info('Transient PPTX streaming preview error (suppressed)', { error: msg }) } } finally { if (!cancelled) setRendering(false) @@ -264,12 +245,12 @@ export function PptxPreview({ } }, [fileData, streamingContent, cacheKey]) - const error = resolvePreviewError(fetchError, renderError) + const error = streamingContent !== undefined ? null : resolvePreviewError(fetchError, renderError) const loading = isFetching || rendering if (error) return - if (loading && slides.length === 0) { + if ((loading || streamingContent !== undefined) && slides.length === 0) { return PPTX_SLIDE_SKELETON } @@ -287,4 +268,4 @@ export function PptxPreview({

) -} +}) diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/preview-panel.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/preview-panel.tsx index 641134eddd..bf9794f1ad 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/preview-panel.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/preview-panel.tsx @@ -291,7 +291,7 @@ const MermaidDiagram = memo(function MermaidDiagram({ definition }: { definition if (error) { return (
- Diagram error: + Diagram error: {error}
) diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/preview-shared.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/preview-shared.tsx index de0c59f531..489347c70c 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/preview-shared.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/preview-shared.tsx @@ -5,7 +5,9 @@ import { Skeleton } from '@/components/emcn' export function PreviewError({ label, error }: { label: string; error: string }) { return (
-

Failed to preview {label}

+

+ Failed to preview {label} +

{error}

) @@ -19,20 +21,6 @@ export function resolvePreviewError( return renderError } -export function shouldSuppressStreamingDocumentError(message: string): boolean { - const lower = message.toLowerCase() - return ( - lower.includes('preview failed') || - lower.includes('aborterror') || - lower.includes('unexpected end') || - lower.includes('unexpected eof') || - lower.includes('invalid or unexpected token') || - lower.includes('end of central directory') || - lower.includes('corrupted zip') || - lower.includes('end of data reached') - ) -} - export const PDF_PAGE_SKELETON = (
{[0, 1].map((i) => ( diff --git a/apps/sim/lib/copilot/vfs/document-style.ts b/apps/sim/lib/copilot/vfs/document-style.ts new file mode 100644 index 0000000000..3c1ebac6c5 --- /dev/null +++ b/apps/sim/lib/copilot/vfs/document-style.ts @@ -0,0 +1,171 @@ +import { createLogger } from '@sim/logger' +import { toError } from '@sim/utils/errors' + +const logger = createLogger('DocumentStyle') + +// ZIP magic bytes: PK\x03\x04 +const ZIP_MAGIC = [0x50, 0x4b, 0x03, 0x04] + +interface ThemeColors { + dk1: string + lt1: string + dk2: string + lt2: string + accent1: string + accent2: string + accent3: string + accent4: string + accent5: string + accent6: string + hlink: string + folHlink: string +} + +export interface DocumentStyleSummary { + format: 'docx' | 'pptx' + theme: { + name: string + colors: Partial + fonts: { major: string; minor: string } + } + styles?: Array<{ + id: string + name: string + type: string + fontSize?: number + bold?: boolean + color?: string + font?: string + }> +} + +function attr(xml: string, name: string): string { + const rx = new RegExp(`${name}="([^"]*)"`) + return rx.exec(xml)?.[1] ?? '' +} + +function between(xml: string, open: string, close: string): string { + const start = xml.indexOf(open) + if (start < 0) return '' + const end = xml.indexOf(close, start + open.length) + if (end < 0) return '' + return xml.slice(start + open.length, end) +} + +function parseColorSlot(xml: string, slot: string): string { + const inner = between(xml, ``, ``) + if (!inner) return '' + // srgbClr uses val=; sysClr has val="windowText" but lastClr holds the fallback hex + const srgb = attr(inner, 'val') + if (srgb && inner.includes('', '') + const minor = between(xml, '', '') + return { major: attr(major, 'typeface') || '', minor: attr(minor, 'typeface') || '' } +} + +function parseThemeXml(xml: string): DocumentStyleSummary['theme'] { + const clrSchemeMatch = /]*name="([^"]*)"/.exec(xml) + const slots: Array = [ + 'dk1', + 'lt1', + 'dk2', + 'lt2', + 'accent1', + 'accent2', + 'accent3', + 'accent4', + 'accent5', + 'accent6', + 'hlink', + 'folHlink', + ] + const colors: Partial = {} + for (const slot of slots) { + const hex = parseColorSlot(xml, slot) + if (hex) colors[slot] = hex + } + return { name: clrSchemeMatch?.[1] ?? '', colors, fonts: parseFontScheme(xml) } +} + +function parseDocxStyles(xml: string): DocumentStyleSummary['styles'] { + const targetIds = new Set([ + 'Normal', + 'DefaultParagraphFont', + 'Heading1', + 'Heading2', + 'Heading3', + 'Title', + 'Subtitle', + ]) + const results: DocumentStyleSummary['styles'] = [] + const blocks = xml.split('/.test(block) && !/]*w:ascii="([^"]*)"/.exec(block) + const font = fontMatch?.[1] + results.push({ + id: styleId, + name, + type: styleType, + ...(fontSize !== undefined && { fontSize }), + ...(bold && { bold }), + ...(color && { color }), + ...(font && { font }), + }) + } + return results +} + +/** + * Extract a compact style summary from a binary OOXML (.docx or .pptx) buffer. + * Returns null if the buffer is not a valid ZIP/OOXML file. + */ +export async function extractDocumentStyle( + buffer: Buffer, + ext: 'docx' | 'pptx' +): Promise { + if (buffer.length < 4) return null + for (let i = 0; i < 4; i++) { + if (buffer[i] !== ZIP_MAGIC[i]) return null + } + + try { + const JSZip = (await import('jszip')).default + const zip = await JSZip.loadAsync(buffer) + + const themePath = ext === 'docx' ? 'word/theme/theme1.xml' : 'ppt/theme/theme1.xml' + const themeFile = zip.file(themePath) + if (!themeFile) return null + + const theme = parseThemeXml(await themeFile.async('string')) + const summary: DocumentStyleSummary = { format: ext, theme } + + if (ext === 'docx') { + const stylesFile = zip.file('word/styles.xml') + if (stylesFile) { + const styles = parseDocxStyles(await stylesFile.async('string')) + if (styles && styles.length > 0) summary.styles = styles + } + } + + return summary + } catch (err) { + logger.warn('Failed to extract document style from buffer', { error: toError(err).message }) + return null + } +} diff --git a/apps/sim/lib/copilot/vfs/workspace-vfs.ts b/apps/sim/lib/copilot/vfs/workspace-vfs.ts index e529c836ea..bc0d1af5f1 100644 --- a/apps/sim/lib/copilot/vfs/workspace-vfs.ts +++ b/apps/sim/lib/copilot/vfs/workspace-vfs.ts @@ -20,6 +20,7 @@ import { toError } from '@sim/utils/errors' import { and, desc, eq, isNotNull, isNull, ne } from 'drizzle-orm' import { listApiKeys } from '@/lib/api-key/service' import { buildWorkspaceMd, type WorkspaceMdData } from '@/lib/copilot/chat/workspace-context' +import { extractDocumentStyle } from '@/lib/copilot/vfs/document-style' import { type FileReadResult, readFileRecord } from '@/lib/copilot/vfs/file-reader' import { normalizeVfsSegment } from '@/lib/copilot/vfs/normalize-segment' import type { DirEntry, GrepMatch, GrepOptions, ReadResult } from '@/lib/copilot/vfs/operations' @@ -57,10 +58,14 @@ import { getAccessibleOAuthCredentials, } from '@/lib/credentials/environment' import { getPersonalAndWorkspaceEnv } from '@/lib/environment/utils' +import { BINARY_DOC_TASKS, MAX_DOCUMENT_PREVIEW_CODE_BYTES } from '@/lib/execution/constants' +import { runSandboxTask, SandboxUserCodeError } from '@/lib/execution/sandbox/run-task' import { getKnowledgeBases } from '@/lib/knowledge/service' import { listTables } from '@/lib/table/service' import { + downloadWorkspaceFile, findWorkspaceFileRecord, + getWorkspaceFile, listWorkspaceFiles, } from '@/lib/uploads/contexts/workspace/workspace-file-manager' import { hasWorkflowChanged } from '@/lib/workflows/comparison' @@ -310,6 +315,8 @@ function getStaticComponentFiles(): Map { * tables/{name}/meta.json * files/{name}/meta.json * files/by-id/{id}/meta.json + * files/by-id/{id}/style (dynamic — OOXML theme/font extraction for .docx/.pptx) + * files/by-id/{id}/compiled-check (dynamic — compile JS source via sandbox, returns {ok,error?}) * jobs/{title}/meta.json * jobs/{title}/history.json * jobs/{title}/executions.json @@ -448,9 +455,78 @@ export class WorkspaceVFS { /** * Attempt to read dynamic workspace file content from storage. * Handles images (base64), parseable documents (PDF, etc.), and text files. + * Also handles: + * `files/by-id/{id}/style` — OOXML theme/style extraction (.docx / .pptx only) + * `files/by-id/{id}/compiled-check` — sandbox compile check for JS-source binary files * Returns null if the path doesn't match `files/{name}` / `files/by-id/{id}` or the file isn't found. */ async readFileContent(path: string): Promise { + // Handle compiled-check path: files/by-id/{id}/compiled-check + const compiledCheckMatch = path.match(/^files\/by-id\/([^/]+)\/compiled-check$/) + if (compiledCheckMatch) { + const fileId = compiledCheckMatch[1] + try { + const record = await getWorkspaceFile(this._workspaceId, fileId) + if (!record) return null + const ext = record.name.split('.').pop()?.toLowerCase() ?? '' + const taskId = BINARY_DOC_TASKS[ext] + if (!taskId) return null + const buffer = await downloadWorkspaceFile(record) + const code = buffer.toString('utf-8') + if (Buffer.byteLength(code, 'utf-8') > MAX_DOCUMENT_PREVIEW_CODE_BYTES) { + return { + content: JSON.stringify({ ok: false, error: 'File source exceeds maximum size' }), + totalLines: 1, + } + } + let result: { ok: boolean; error?: string; errorName?: string } + try { + await runSandboxTask(taskId, { code, workspaceId: this._workspaceId }) + result = { ok: true } + } catch (err) { + if (err instanceof SandboxUserCodeError) { + result = { ok: false, error: toError(err).message, errorName: err.name } + } else { + throw err + } + } + const json = JSON.stringify(result) + return { content: json, totalLines: 1 } + } catch (err) { + logger.warn('Compiled check failed via VFS', { + workspaceId: this._workspaceId, + fileId, + error: toError(err).message, + }) + return null + } + } + + // Handle style extraction path: files/by-id/{id}/style + const styleMatch = path.match(/^files\/by-id\/([^/]+)\/style$/) + if (styleMatch) { + const fileId = styleMatch[1] + try { + const record = await getWorkspaceFile(this._workspaceId, fileId) + if (!record) return null + const rawExt = record.name.split('.').pop()?.toLowerCase() + if (rawExt !== 'docx' && rawExt !== 'pptx') return null + const ext: 'docx' | 'pptx' = rawExt + const buffer = await downloadWorkspaceFile(record) + const summary = await extractDocumentStyle(buffer, ext) + if (!summary) return null + const json = JSON.stringify(summary, null, 2) + return { content: json, totalLines: json.split('\n').length } + } catch (err) { + logger.warn('Failed to extract document style via VFS', { + workspaceId: this._workspaceId, + fileId, + error: toError(err).message, + }) + return null + } + } + const deletedMatch = path.match(/^recently-deleted\/files\/(.+?)(?:\/content)?$/) const activeMatch = path.match(/^files\/(.+?)(?:\/content)?$/) const match = deletedMatch || activeMatch diff --git a/apps/sim/lib/execution/constants.ts b/apps/sim/lib/execution/constants.ts index 2950f2b71b..f53d7d28b4 100644 --- a/apps/sim/lib/execution/constants.ts +++ b/apps/sim/lib/execution/constants.ts @@ -1,4 +1,5 @@ import { DEFAULT_EXECUTION_TIMEOUT_MS } from '@/lib/core/execution-limits' +import type { SandboxTaskId } from '@/sandbox-tasks/registry' export { DEFAULT_EXECUTION_TIMEOUT_MS } @@ -11,3 +12,10 @@ export { DEFAULT_EXECUTION_TIMEOUT_MS } * while reducing memory pressure and abuse potential from oversized payloads. */ export const MAX_DOCUMENT_PREVIEW_CODE_BYTES = 1 * 1024 * 1024 + +/** Maps file extension to the sandbox task that compiles/generates that document type. */ +export const BINARY_DOC_TASKS: Record = { + docx: 'docx-generate', + pptx: 'pptx-generate', + pdf: 'pdf-generate', +}