// Process a single job by ID import puppeteer from "@cloudflare/puppeteer"; import fetch from "node-fetch"; import {Env, Job} from "./types"; export async function processJob(jobId: string, env: Env) { console.log(`[processJob] Starting to process job: ${jobId}`); const startTime = Date.now(); const metaKey = `jobs/${jobId}.json`; console.log(`[processJob] Fetching job metadata from: ${metaKey}`); const obj = await env.JOBS_BUCKET.get(metaKey); if (!obj) { console.warn(`[processJob] Job not found: ${jobId}`); return; } const job = JSON.parse(await obj.text()) as Job; console.log(`[processJob] Job status: ${job.status}, prompt: "${job.prompt?.substring(0, 50)}${job.prompt && job.prompt.length > 50 ? '...' : ''}"`); if (job.status !== "queued") { console.log(`[processJob] Skipping job ${jobId} as it's not in queued state`); return; } // Update status to processing console.log(`[processJob] Updating job ${jobId} status to processing`); job.status = "processing"; await env.JOBS_BUCKET.put(metaKey, JSON.stringify(job)); try { console.log(`[processJob] Starting image generation for job: ${jobId}`); await processImageGeneration(jobId, job, env); console.log(`[processJob] Successfully completed job: ${jobId}, took: ${Date.now() - startTime}ms`); } catch (err: any) { // Mark as failed console.error(`[processJob] Failed to process job ${jobId}:`, err); job.status = "failed"; await env.JOBS_BUCKET.put(metaKey, JSON.stringify(job)); console.log(`[processJob] Marked job ${jobId} as failed, took: ${Date.now() - startTime}ms`); } } // Helper function to download an image from a URL export async function downloadImage(url: string): Promise<{ buffer: ArrayBuffer, contentType: string }> { console.log(`[downloadImage] Starting download from URL: ${url}`); const startTime = Date.now(); try { // Use fetch with the new request object console.log(`[downloadImage] Sending fetch request to: ${url}`); const response = await fetch(url, { method: "GET", headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' } }); console.log(`[downloadImage] Received response with status: ${response.status} ${response.statusText}`); if (!response.ok) { throw new Error(`Failed to download image: ${response.status} ${response.statusText}`); } const contentType = response.headers.get('content-type') || 'application/octet-stream'; console.log(`[downloadImage] Content-Type: ${contentType}`); const buffer = await response.arrayBuffer(); console.log(`[downloadImage] Downloaded ${buffer.byteLength} bytes, took: ${Date.now() - startTime}ms`); return { buffer, contentType }; } catch (error) { console.error(`[downloadImage] Error downloading image: ${error instanceof Error ? error.message : String(error)}`); throw error; } } // Process image generation for a specific job async function processImageGeneration(jobId: string, job: Job, env: Env) { console.log(`[processImageGeneration] Starting image generation for job ${jobId}`); try { console.log(`[processImageGeneration] Processing job ${jobId} with prompt: ${job.prompt}`); const metaKey = `jobs/${jobId}.json`; // Check if BROWSER binding is available if (!env.BROWSER) { console.error(`[processImageGeneration] BROWSER binding is not available for job ${jobId}`); throw new Error('BROWSER binding is not available'); } // Production mode - use Cloudflare's puppeteer console.log(`[processImageGeneration] Launching browser for job ${jobId}`); try { const browser = await puppeteer.launch(env.BROWSER as Fetcher); console.log(`[processImageGeneration] Browser launched successfully for job ${jobId}`); console.log(`[processImageGeneration] Creating new page for job ${jobId}`); const page = await browser.newPage(); console.log(`[processImageGeneration] Page created successfully for job ${jobId}`); try { // In production, get cookies from R2 console.log(`[processImageGeneration] Fetching cookies from R2 for job ${jobId}`); const ck = await env.JOBS_BUCKET.get("cookies.json"); if (ck) { console.log(`[processImageGeneration] Found cookies file for job ${jobId}`); try { const cookiesText = await ck.text(); console.log(`[processImageGeneration] Cookies text length: ${cookiesText.length} characters`); const cookies = JSON.parse(cookiesText); console.log(`[processImageGeneration] Parsed ${cookies.length} cookies for job ${jobId}`); await page.setCookie(...cookies); console.log(`[processImageGeneration] Successfully set cookies for job ${jobId}`); } catch (cookieError) { console.error(`[processImageGeneration] Error parsing or setting cookies: ${cookieError instanceof Error ? cookieError.message : String(cookieError)}`); throw new Error(`Failed to parse or set cookies: ${cookieError instanceof Error ? cookieError.message : String(cookieError)}`); } } else { console.error(`[processImageGeneration] No cookies found for job ${jobId}`); throw new Error('No cookies found in R2'); } await page.setRequestInterception(true); page.on("request", (r: any) => { const t = r.resourceType(); if (["image","font","media"].includes(t)) r.abort(); else r.continue(); }); await page.goto("https://chat.openai.com", { waitUntil: "networkidle2" }); console.log(`[processImageGeneration] Navigated to chat.openai.com for job ${jobId}`); await page.waitForSelector("#prompt-textarea", { timeout: 30000 }); console.log(`[processImageGeneration] Found prompt textarea for job ${jobId}`); // Handle image upload differently in local vs production if (job.imageKey) { console.log(`[processImageGeneration] Job has imageKey: ${job.imageKey}, attempting to get from R2`); const obj = await env.IMAGES_BUCKET.get(job.imageKey); const imgUrl = obj ? (obj as any).url() : null; if (imgUrl) { console.log(`[processImageGeneration] Got image URL: ${imgUrl} for job ${jobId}`); // In production, use the Worker-FS shim const input = await page.$('input[type="file"]'); if (input) { console.log(`[processImageGeneration] Found file input element for job ${jobId}`); await input.uploadFile(new URL(imgUrl).pathname); console.log(`[processImageGeneration] Uploaded file for job ${jobId}`); await new Promise(r => setTimeout(r, 3000)); } else { console.warn(`[processImageGeneration] Could not find file input element for job ${jobId}`); } } else { console.warn(`[processImageGeneration] Could not get image URL from R2 for job ${jobId}`); } } console.log(`[processImageGeneration] Typing prompt for job ${jobId}: ${job.prompt.substring(0, 50)}${job.prompt.length > 50 ? '...' : ''}`); await page.type("#prompt-textarea", job.prompt, { delay: 100 }); await page.keyboard.press("Enter"); console.log(`[processImageGeneration] Submitted prompt for job ${jobId}, waiting for response`); await new Promise(r => setTimeout(r, 5000)); // Wait for "Image created" console.log(`[processImageGeneration] Starting to wait for "Image created" text for job ${jobId}`); let tries = 30; while (tries--) { console.log(`[processImageGeneration] Checking for "Image created" text, tries left: ${tries} for job ${jobId}`); const ready = await page.evaluate(() => { return Array.from(document.querySelectorAll("span,div")) .some(el => el.textContent?.trim() === "Image created"); }); if (ready) { console.log(`[processImageGeneration] Found "Image created" text for job ${jobId}`); break; } await new Promise(r => setTimeout(r, 5000)); } if (tries <= 0) { console.warn(`[processImageGeneration] Timed out waiting for "Image created" text for job ${jobId}`); } // Grab the generated image URL console.log(`[processImageGeneration] Attempting to get generated image URL for job ${jobId}`); const imageSrc = await page.$eval('img[alt="Generated image"]', (el: any) => (el as HTMLImageElement).src); console.log(`[processImageGeneration] Got image URL: ${imageSrc} for job ${jobId}`); // Save results job.status = "done"; job.imageUrl = imageSrc; job.finished_at = Date.now(); await env.JOBS_BUCKET.put(metaKey, JSON.stringify(job)); console.log(`[processImageGeneration] Updated job status to done for job ${jobId}`); // Send webhook if configured if (job.webhook_url) { console.log(`[processImageGeneration] Sending webhook for job ${jobId} to ${job.webhook_url}`); try { await fetch(job.webhook_url, { method: "POST", headers: {"Content-Type":"application/json"}, body: JSON.stringify({ request_id: jobId, image: imageSrc, prompt: job.prompt, status: "done", finished_at: job.finished_at }) }); console.log(`[processImageGeneration] Successfully sent webhook for job ${jobId}`); } catch (webhookError) { console.error(`[processImageGeneration] Error sending webhook for job ${jobId}: ${webhookError instanceof Error ? webhookError.message : String(webhookError)}`); // Don't throw here, we still want to consider the job successful } } // Close browser try { await page.close(); console.log(`[processImageGeneration] Closed page for job ${jobId}`); } catch (e) { console.warn(`[processImageGeneration] Error closing page for job ${jobId}: ${e instanceof Error ? e.message : String(e)}`); } try { await browser.close(); console.log(`[processImageGeneration] Closed browser for job ${jobId}`); } catch (e) { console.warn(`[processImageGeneration] Error closing browser for job ${jobId}: ${e instanceof Error ? e.message : String(e)}`); } } catch (err) { // Make sure to close the page on error try { await page.close(); } catch (e) { /* ignore */ } try { await browser.close(); } catch (e) { /* ignore */ } throw err; } } catch (browserError) { console.error(`[processImageGeneration] Error launching browser: ${browserError instanceof Error ? browserError.message : String(browserError)}`); throw new Error(`Failed to launch browser: ${browserError instanceof Error ? browserError.message : String(browserError)}`); } } catch (err: any) { console.error('Error processing job:', err); throw err; // Let the caller handle the error } }