diff --git a/apps/rowboat/app/api/generated-images/[...path]/route.ts b/apps/rowboat/app/api/generated-images/[id]/route.ts similarity index 55% rename from apps/rowboat/app/api/generated-images/[...path]/route.ts rename to apps/rowboat/app/api/generated-images/[id]/route.ts index ce946169..e9312498 100644 --- a/apps/rowboat/app/api/generated-images/[...path]/route.ts +++ b/apps/rowboat/app/api/generated-images/[id]/route.ts @@ -1,12 +1,14 @@ import { NextRequest, NextResponse } from 'next/server'; -import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3'; +import { S3Client, GetObjectCommand, HeadObjectCommand } from '@aws-sdk/client-s3'; import { Readable } from 'stream'; -export async function GET(request: NextRequest, props: { params: Promise<{ path: string[] }> }) { +// Serves generated images from S3 by UUID-only path: /api/generated-images/{id} +// Reconstructs the S3 key using the same sharding logic as image creation. +export async function GET(request: NextRequest, props: { params: Promise<{ id: string }> }) { const params = await props.params; - const path = params.path || []; - if (path.length < 3) { - return NextResponse.json({ error: 'Invalid path' }, { status: 400 }); + const id = params.id; + if (!id) { + return NextResponse.json({ error: 'Missing id' }, { status: 400 }); } const bucket = process.env.RAG_UPLOADS_S3_BUCKET || ''; @@ -23,8 +25,31 @@ export async function GET(request: NextRequest, props: { params: Promise<{ path: } as any : undefined, }); - const filename = path[path.length - 1]; - const key = `generated_images/${path.join('/')}`; + // Reconstruct directory sharding from last two characters of UUID + const last2 = id.slice(-2).padStart(2, '0'); + const dirA = last2.charAt(0); + const dirB = last2.charAt(1); + const baseKey = `generated_images/${dirA}/${dirB}/${id}`; + + // Try known extensions in order used by generator + const exts = ['.png', '.jpg', '.webp']; + let foundExt: string | null = null; + for (const ext of exts) { + try { + await s3.send(new HeadObjectCommand({ Bucket: bucket, Key: `${baseKey}${ext}` })); + foundExt = ext; + break; + } catch { + // continue trying next extension + } + } + + if (!foundExt) { + return NextResponse.json({ error: 'Not found' }, { status: 404 }); + } + + const key = `${baseKey}${foundExt}`; + const filename = `${id}${foundExt}`; try { const resp = await s3.send(new GetObjectCommand({ Bucket: bucket, Key: key })); const contentType = resp.ContentType || 'application/octet-stream'; diff --git a/apps/rowboat/src/application/lib/agents-runtime/agent-tools.ts b/apps/rowboat/src/application/lib/agents-runtime/agent-tools.ts index d7c0672b..cb8e8b74 100644 --- a/apps/rowboat/src/application/lib/agents-runtime/agent-tools.ts +++ b/apps/rowboat/src/application/lib/agents-runtime/agent-tools.ts @@ -659,11 +659,11 @@ export function createGenerateImageTool( const images = await Promise.all(result.images.map(async (img) => { const buf = Buffer.from(img.dataBase64, 'base64'); const ext = img.mimeType === 'image/jpeg' ? '.jpg' : img.mimeType === 'image/webp' ? '.webp' : '.png'; - const base = `${projectId}-${Math.floor(Math.random() * 1e12).toString(36)}`; - const last2 = base.slice(-2).padStart(2, '0'); + const imageId = crypto.randomUUID(); + const last2 = imageId.slice(-2).padStart(2, '0'); const dirA = last2.charAt(0); const dirB = last2.charAt(1); - const filename = `${base}${ext}`; + const filename = `${imageId}${ext}`; const key = `generated_images/${dirA}/${dirB}/${filename}`; await s3.send(new PutObjectCommand({ Bucket: s3Bucket, @@ -671,7 +671,7 @@ export function createGenerateImageTool( Body: buf, ContentType: img.mimeType, })); - const url = `/api/generated-images/${dirA}/${dirB}/${filename}`; + const url = `/api/generated-images/${imageId}`; return { mimeType: img.mimeType, bytes: buf.length, url }; })); const payload = {