import type { IncomingMessage, ServerResponse } from 'http'; import { dialog, nativeImage } from 'electron'; import crypto from 'node:crypto'; import { extname, join } from 'node:path'; import { homedir } from 'node:os'; import type { HostApiContext } from '../context'; import { parseJsonBody, sendJson } from '../route-utils'; 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', '.ico': 'image/x-icon', '.mp4': 'video/mp4', '.webm': 'video/webm', '.mov': 'video/quicktime', '.avi': 'video/x-msvideo', '.mkv': 'video/x-matroska', '.mp3': 'audio/mpeg', '.wav': 'audio/wav', '.ogg': 'audio/ogg', '.flac': 'audio/flac', '.pdf': 'application/pdf', '.zip': 'application/zip', '.gz': 'application/gzip', '.tar': 'application/x-tar', '.7z': 'application/x-7z-compressed', '.rar': 'application/vnd.rar', '.json': 'application/json', '.xml': 'application/xml', '.csv': 'text/csv', '.txt': 'text/plain', '.md': 'text/markdown', '.html': 'text/html', '.css': 'text/css', '.js': 'text/javascript', '.ts': 'text/typescript', '.py': 'text/x-python', }; 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(homedir(), '.openclaw', 'media', 'outbound'); async function generateImagePreview(filePath: string, mimeType: string): Promise { try { const img = nativeImage.createFromPath(filePath); if (img.isEmpty()) return null; const size = img.getSize(); const maxDim = 512; if (size.width > maxDim || size.height > maxDim) { const resized = size.width >= size.height ? img.resize({ width: maxDim }) : img.resize({ height: maxDim }); return `data:image/png;base64,${resized.toPNG().toString('base64')}`; } const { readFile } = await import('node:fs/promises'); const buf = await readFile(filePath); return `data:${mimeType};base64,${buf.toString('base64')}`; } catch { return null; } } export async function handleFileRoutes( req: IncomingMessage, res: ServerResponse, url: URL, _ctx: HostApiContext, ): Promise { if (url.pathname === '/api/files/stage-paths' && req.method === 'POST') { try { const body = await parseJsonBody<{ filePaths: string[] }>(req); 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 s = 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: s.size, stagedPath, preview }); } sendJson(res, 200, results); } catch (error) { sendJson(res, 500, { success: false, error: String(error) }); } return true; } if (url.pathname === '/api/files/stage-buffer' && req.method === 'POST') { try { const body = await parseJsonBody<{ base64: string; fileName: string; mimeType: string }>(req); 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; sendJson(res, 200, { id, fileName: body.fileName, mimeType, fileSize: buffer.length, stagedPath, preview, }); } catch (error) { sendJson(res, 500, { success: false, error: String(error) }); } return true; } if (url.pathname === '/api/files/thumbnails' && req.method === 'POST') { try { const body = await parseJsonBody<{ paths: Array<{ filePath: string; mimeType: string }> }>(req); const fsP = await import('node:fs/promises'); const results: Record = {}; for (const { filePath, mimeType } of body.paths) { try { const s = await fsP.stat(filePath); const preview = mimeType.startsWith('image/') ? await generateImagePreview(filePath, mimeType) : null; results[filePath] = { preview, fileSize: s.size }; } catch { results[filePath] = { preview: null, fileSize: 0 }; } } sendJson(res, 200, results); } catch (error) { sendJson(res, 500, { success: false, error: String(error) }); } return true; } if (url.pathname === '/api/files/save-image' && req.method === 'POST') { try { const body = await parseJsonBody<{ base64?: string; mimeType?: string; filePath?: string; defaultFileName: string; }>(req); const ext = body.defaultFileName.includes('.') ? body.defaultFileName.split('.').pop()! : (body.mimeType?.split('/')[1] || 'png'); const result = await dialog.showSaveDialog({ defaultPath: join(homedir(), 'Downloads', body.defaultFileName), filters: [ { name: 'Images', extensions: [ext, 'png', 'jpg', 'jpeg', 'webp', 'gif'] }, { name: 'All Files', extensions: ['*'] }, ], }); if (result.canceled || !result.filePath) { sendJson(res, 200, { success: false }); return true; } const fsP = await import('node:fs/promises'); if (body.filePath) { await fsP.copyFile(body.filePath, result.filePath); } else if (body.base64) { await fsP.writeFile(result.filePath, Buffer.from(body.base64, 'base64')); } else { sendJson(res, 400, { success: false, error: 'No image data provided' }); return true; } sendJson(res, 200, { success: true, savedPath: result.filePath }); } catch (error) { sendJson(res, 500, { success: false, error: String(error) }); } return true; } return false; }