Spaces:
Paused
Paused
| import "dotenv/config"; | |
| import puppeteer from "puppeteer"; | |
| import fs from "fs/promises"; | |
| import path from "path"; | |
| // We can directly import our server actions because this script runs in the same project context | |
| import { saveBattleSetup } from "./app/actions/db.actions"; | |
| import { BattleSetup } from "./types"; | |
| import { generateAndSaveHero } from "./app/actions/ai.actions"; | |
| // --- CONFIGURATION --- | |
| const BASE_URL = "http://localhost:7860"; // The URL of our own Next.js app | |
| const TEMP_DIR = "/tmp"; | |
| const CYCLE_WAIT_TIME_MS = 4 * 60 * 60 * 1000; // 4 hours between cycles | |
| const BATTLE_TIMEOUT_MS = 8 * 60 * 1000; // 8 minutes timeout for battle completion | |
| // --- UTILITY --- | |
| const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); | |
| async function sendErrorToTelegram(screenshotPath: string, errorMessage: string) { | |
| const botToken = process.env.TELEGRAM_BOT_TOKEN; | |
| const chatId = process.env.TELEGRAM_CHAT_ID; | |
| if (!botToken || !chatId) { | |
| console.warn("[Telegram] Bot Token or Chat ID not configured. Skipping notification."); | |
| return; | |
| } | |
| console.log("[Telegram] An error occurred. Sending notification..."); | |
| try { | |
| // 1. Read the screenshot file from the disk (this returns a Buffer) | |
| const imageBuffer = await fs.readFile(screenshotPath); | |
| // --- FIX IS HERE --- | |
| // 2. Convert the Node.js Buffer into a standard Blob | |
| const imageBlob = new Blob([imageBuffer]); | |
| // --- END FIX --- | |
| // 3. Create the multipart form | |
| const form = new FormData(); | |
| form.append('chat_id', chatId); | |
| form.append('caption', `π€ Automation Worker Error π\n\n<pre>${errorMessage}</pre>`); | |
| form.append('parse_mode', 'HTML'); | |
| // 4. Append the Blob to the form data, not the Buffer | |
| // Provide a filename for the API to recognize it as a file upload. | |
| form.append('photo', imageBlob, 'error_screenshot.png'); | |
| // 5. Send the request to the Telegram API | |
| const apiUrl = `https://api.telegram.org/bot${botToken}/sendPhoto`; | |
| const response = await fetch(apiUrl, { | |
| method: 'POST', | |
| body: form, | |
| }); | |
| if (response.ok) { | |
| console.log("[Telegram] β Error notification sent successfully."); | |
| } else { | |
| const errorResponse = await response.json(); | |
| console.error("[Telegram] Failed to send notification:", errorResponse); | |
| } | |
| } catch (error) { | |
| console.error("[Telegram] CRITICAL: Could not send error notification.", error); | |
| } | |
| } | |
| // --- THE MAIN AUTOMATION LOOP --- | |
| async function main() { | |
| console.log("π€ Automation Worker Started"); | |
| console.log("β° Note: The shell script has implemented a 2-3 hour startup delay to avoid triggering HF monitoring"); | |
| console.log("π After initial delay, cycles will run every ~4 hours with random timing variations"); | |
| console.log("π― Current time:", new Date().toLocaleString()); | |
| console.log("π± Starting in organic mode..."); | |
| await sleep(5000); // Initial delay | |
| while (true) { | |
| console.log("π Starting new automation cycle at", new Date().toLocaleString()); | |
| let browser; | |
| try { | |
| // 1. GENERATE HEROES | |
| console.log("[Cycle] Generating two new heroes..."); | |
| const hero1 = await generateAndSaveHero("Marvel"); | |
| await sleep(1000); // Small delay to avoid rate limits | |
| const hero2 = await generateAndSaveHero("DC"); | |
| if (!hero1 || !hero2) { | |
| throw new Error("Failed to generate one or both heroes."); | |
| } | |
| console.log( | |
| `[Cycle] Generated: ${hero1} vs ${hero2}` | |
| ); | |
| // 2. CREATE BATTLE SETUP | |
| const battleSetup: BattleSetup = { | |
| type: "HERO_BATTLE", | |
| config: { | |
| heroIds: [hero1._id as string, hero2._id as string], | |
| gameSpeed: 2.5, // Increased speed for faster battles to reduce overall CPU time | |
| activePowerUps: [ | |
| "SPEED_BOOST", | |
| "HEALTH_PACK", | |
| "GHOST", | |
| "DOMAIN_INVINCIBILITY", | |
| ], | |
| }, | |
| }; | |
| const battleId = await saveBattleSetup(battleSetup); | |
| if (!battleId) { | |
| throw new Error("Failed to save battle setup to database."); | |
| } | |
| console.log(`[Cycle] Created battle setup with ID: ${battleId}`); | |
| // 3. GENERATE ONE-TIME AUTOMATION TOKEN | |
| console.log("[Cycle] Generating one-time automation token..."); | |
| let automationToken: string | null = null; | |
| try { | |
| const tokenResponse = await fetch(`${BASE_URL}/api/youtube/automation-token`, { | |
| method: 'POST', | |
| headers: { | |
| 'Authorization': `Bearer ${process.env.AUTOMATION_SECRET_KEY}`, | |
| 'Content-Type': 'application/json', | |
| }, | |
| }); | |
| if (tokenResponse.ok) { | |
| const tokenData = await tokenResponse.json(); | |
| automationToken = tokenData.token; | |
| console.log(`[Cycle] Automation token generated: ${automationToken?.substring(0, 8)}...`); | |
| } else { | |
| throw new Error(`Token generation failed: ${tokenResponse.status}`); | |
| } | |
| } catch (error) { | |
| console.error("[Cycle] Failed to generate automation token:", error); | |
| continue; // Skip this cycle if token generation fails | |
| } | |
| if (!automationToken) { | |
| console.error("[Cycle] No automation token received, skipping cycle"); | |
| continue; | |
| } | |
| // 4. LAUNCH HEADLESS BROWSER & RECORD | |
| console.log("[Cycle] Launching headless browser..."); | |
| browser = await puppeteer.launch({ | |
| headless: true, // Use the new headless mode | |
| args: [ | |
| "--no-sandbox", | |
| "--disable-setuid-sandbox", | |
| "--disable-dev-shm-usage", // Important for Docker environments | |
| "--disable-web-security", // Reduce security overhead | |
| "--disable-features=VizDisplayCompositor", // Disable compositor | |
| "--disable-background-timer-throttling", // Prevent throttling | |
| "--disable-renderer-backgrounding", | |
| "--disable-backgrounding-occluded-windows", | |
| "--disable-ipc-flooding-protection", // Reduce IPC overhead | |
| "--no-zygote", // Helps in resource-constrained environments | |
| // REMOVED: --single-process to allow multi-core utilization | |
| "--process-per-site", // Use separate processes per site for better CPU distribution | |
| "--max_old_space_size=512", // Increased from 256MB since we have 16GB RAM | |
| "--memory-pressure-off", // Disable memory pressure notifications | |
| "--autoplay-policy=no-user-gesture-required", | |
| "--use-fake-ui-for-media-stream", | |
| // Optimized window size for HF free tier (540x960 instead of 1080x1920) | |
| "--window-size=540,960", | |
| ], | |
| }); | |
| const page = await browser.newPage(); | |
| await page.setViewport({ width: 540, height: 960 }); // Optimized resolution for free tier | |
| // **** SIMPLIFIED: No need for file transfer since we upload directly from browser **** | |
| console.log("[WORKER] Browser automation simplified - no file transfer needed"); | |
| // Set up browser console logging | |
| page.on("console", (msg) => { | |
| const type = msg.type(); | |
| const text = msg.text(); | |
| console.log(`[Browser Console - ${type.toUpperCase()}]: ${text}`); | |
| }); | |
| page.on("pageerror", (error) => { | |
| console.error(`[Browser Page Error]: ${error.message}`); | |
| }); | |
| page.on("requestfailed", (request) => { | |
| console.warn( | |
| `[Browser Request Failed]: ${request.url()} - ${ | |
| request.failure()?.errorText | |
| }` | |
| ); | |
| }); | |
| // 5. Build battle URL with automation token | |
| const automationParams = new URLSearchParams({ | |
| automation: "true", | |
| token: automationToken, | |
| }); | |
| const battleUrl = `${BASE_URL}/battle/${battleId}?${automationParams.toString()}`; | |
| console.log(`[Cycle] Battle URL: ${battleUrl.replace(automationToken, automationToken.substring(0, 8) + '...')}`); | |
| await page.goto(battleUrl, { waitUntil: "networkidle0" }); | |
| // --- SIMPLIFIED: Just wait for battle completion --- | |
| console.log("[Cycle] Battle is running... waiting for completion and direct upload from browser."); | |
| console.log("[Cycle] Expected flow: Battle ends β Recording stops β Upload completes β Window closes"); | |
| // Wait for battle completion (automation will upload directly from browser) | |
| const battleTimeout = Date.now() + BATTLE_TIMEOUT_MS; | |
| let battleComplete = false; | |
| // Look for success indicators in browser console or page closure | |
| while (Date.now() < battleTimeout && !battleComplete) { | |
| try { | |
| // Check if we can detect completion via page evaluation | |
| const isComplete = await page.evaluate(() => { | |
| // Look for toast success messages or other completion indicators | |
| const toastElements = document.querySelectorAll('[data-sonner-toast]'); | |
| for (const toast of toastElements) { | |
| const text = toast.textContent || ''; | |
| if (text.includes('Automation upload complete') || | |
| text.includes('Upload Complete')) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| }); | |
| if (isComplete) { | |
| console.log("[Cycle] β Battle completed successfully! Upload handled by browser."); | |
| battleComplete = true; | |
| break; | |
| } | |
| // Check if page is still responsive (not closed) | |
| const pageTitle = await page.title().catch(() => null); | |
| if (!pageTitle) { | |
| console.log("[Cycle] β Page closed - battle completed!"); | |
| battleComplete = true; | |
| break; | |
| } | |
| await sleep(3000); // Check every 3 seconds (reduced from 5) | |
| } catch (error) { | |
| console.error("[Cycle] Error checking battle status:", error); | |
| // If we can't access the page, it might have closed | |
| console.log("[Cycle] β Page appears to be closed - assuming battle completed"); | |
| battleComplete = true; | |
| break; | |
| } | |
| } | |
| if (!battleComplete) { | |
| // Capture a screenshot for debugging if timeout occurs | |
| const screenshotPath = path.join(TEMP_DIR, "timeout_screenshot.png"); | |
| const timeoutMessage = "Battle timed out. No completion detected within timeout period."; | |
| try { | |
| await page.screenshot({ path: screenshotPath as `${string}.png`, fullPage: true }); | |
| console.log(`[Cycle] Timeout screenshot saved to ${screenshotPath}`); | |
| await sendErrorToTelegram(screenshotPath, timeoutMessage); | |
| } catch (screenshotError) { | |
| console.error(`[Cycle] Failed to take timeout screenshot: ${screenshotError}`); | |
| } | |
| throw new Error(timeoutMessage); | |
| } | |
| console.log("[Cycle] Closing browser."); | |
| await browser.close(); | |
| browser = undefined; | |
| // Force garbage collection for memory optimization on HF free tier | |
| if (global.gc) { | |
| console.log("[Cycle] Running garbage collection..."); | |
| global.gc(); | |
| } | |
| console.log("[Cycle] Battle cycle completed successfully!"); | |
| } catch (error) { | |
| console.error("β AUTOMATION CYCLE FAILED β"); | |
| console.error(error); | |
| if (browser) { | |
| try { | |
| const pages = await browser.pages(); | |
| if (pages.length > 0) { | |
| const currentPage = pages[0]; | |
| const errorScreenshotPath = path.join(TEMP_DIR, 'error_screenshot.png'); | |
| await currentPage.screenshot({ path: errorScreenshotPath as `${string}.png`, fullPage: true }); | |
| console.log(`[Cycle] Error screenshot saved to ${errorScreenshotPath}`); | |
| if (error instanceof Error) { | |
| await sendErrorToTelegram(errorScreenshotPath, error.message); | |
| } | |
| } | |
| } catch (screenshotError) { | |
| console.error(`[Cycle] Failed to take error screenshot: ${screenshotError}`); | |
| } | |
| console.log("[Cycle] Closing browser after error..."); | |
| await browser.close(); | |
| browser = undefined; | |
| } | |
| // Force garbage collection after any error to free memory | |
| if (global.gc) { | |
| console.log("[Cycle] Running post-error garbage collection..."); | |
| global.gc(); | |
| } | |
| } | |
| // Add randomization to cycle timing to appear more organic to HF monitoring | |
| // Base: 4 hours Β± 30 minutes random variation | |
| const randomVariation = (Math.random() - 0.5) * 30 * 60 * 1000; // Β±30 minutes in ms | |
| const actualWaitTime = CYCLE_WAIT_TIME_MS + randomVariation; | |
| const waitMinutes = Math.round(actualWaitTime / 1000 / 60); | |
| console.log( | |
| `[Cycle] Cycle complete. Waiting for ${waitMinutes} minutes (${Math.round(actualWaitTime/1000/60/60 * 10)/10}h) with organic timing variation...` | |
| ); | |
| console.log(`[Cycle] Next cycle expected at: ${new Date(Date.now() + actualWaitTime).toLocaleString()}`); | |
| await sleep(actualWaitTime); | |
| } | |
| } | |
| main().catch(console.error); | |