import crypto from 'node:crypto'; import { nativeImage } from 'electron'; import { extname, join } from 'node:path'; import type { HostApiContext } from '../context'; import type { NormalizedHostApiRequest } from '../route-utils'; import { ok, parseJsonBody } from '../route-utils'; import { getUserDataDir } from '@electron/utils/paths'; const EXT_MIME_MAP: Record = { '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.webp': 'image/webp', '.svg': 'image/svg+xml', '.bmp': 'image/bmp', '.txt': 'text/plain', '.json': 'application/json', '.pdf': 'application/pdf', }; function getMimeType(ext: string): string { return EXT_MIME_MAP[ext.toLowerCase()] || 'application/octet-stream'; } function mimeToExt(mimeType: string): string { for (const [ext, mime] of Object.entries(EXT_MIME_MAP)) { if (mime === mimeType) return ext; } return ''; } const OUTBOUND_DIR = join(getUserDataDir(), 'openclaw-media', 'outbound'); async function generateImagePreview(filePath: string, mimeType: string): Promise { try { const image = nativeImage.createFromPath(filePath); if (image.isEmpty()) return null; const size = image.getSize(); const maxDim = 512; const normalized = size.width > maxDim || size.height > maxDim ? (size.width >= size.height ? image.resize({ width: maxDim }) : image.resize({ height: maxDim })) : image; return `data:${mimeType};base64,${normalized.toPNG().toString('base64')}`; } catch { return null; } } export async function handleFileRoutes( request: NormalizedHostApiRequest, _ctx: HostApiContext, ) { const { pathname, method } = request; if (pathname === '/api/files/stage-buffer' && method === 'POST') { const body = parseJsonBody<{ base64: string; fileName: string; mimeType: string }>(request.body); const fsP = await import('node:fs/promises'); await fsP.mkdir(OUTBOUND_DIR, { recursive: true }); const id = crypto.randomUUID(); const ext = extname(body.fileName) || mimeToExt(body.mimeType); const stagedPath = join(OUTBOUND_DIR, `${id}${ext}`); const buffer = Buffer.from(body.base64 || '', 'base64'); await fsP.writeFile(stagedPath, buffer); const mimeType = body.mimeType || getMimeType(ext); const preview = mimeType.startsWith('image/') ? await generateImagePreview(stagedPath, mimeType) : null; return ok({ id, fileName: body.fileName, mimeType, fileSize: buffer.length, stagedPath, preview, }); } if (pathname === '/api/files/stage-paths' && method === 'POST') { const body = parseJsonBody<{ filePaths: string[] }>(request.body); const fsP = await import('node:fs/promises'); await fsP.mkdir(OUTBOUND_DIR, { recursive: true }); const results = []; for (const filePath of body.filePaths || []) { const id = crypto.randomUUID(); const ext = extname(filePath); const stagedPath = join(OUTBOUND_DIR, `${id}${ext}`); await fsP.copyFile(filePath, stagedPath); const stats = await fsP.stat(stagedPath); const mimeType = getMimeType(ext); const fileName = filePath.split(/[\\/]/).pop() || 'file'; const preview = mimeType.startsWith('image/') ? await generateImagePreview(stagedPath, mimeType) : null; results.push({ id, fileName, mimeType, fileSize: stats.size, stagedPath, preview, }); } return ok(results); } return null; }