const express = require('express'); const { chromium } = require('playwright-extra') const StealthPlugin = require('puppeteer-extra-plugin-stealth') const { v4: uuidv4 } = require('uuid'); const yaml = require('js-yaml'); const fs = require('fs').promises; const path = require('path'); function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } const app = express(); const port = parseInt(process.env.LISTEN_PORT) || 3000; app.use(express.json()); let browserPool = {}; const maxBrowsers = parseInt(process.env.MAX_BROWSERS) || 16; let waitingQueue = []; const initializeBrowserPool = (size) => { for (let i = 0; i < size; i++) { browserPool[String(i)] = { browserId: null, status: 'empty', browser: null, // actually context browser0: null, // browser pages: {}, lastActivity: Date.now() }; } }; const v8 = require('v8'); const processNextInQueue = async () => { const availableBrowserslot = Object.keys(browserPool).find( id => browserPool[id].status === 'empty' ); if (waitingQueue.length > 0 && availableBrowserslot) { const nextRequest = waitingQueue.shift(); try { const browserEntry = browserPool[availableBrowserslot]; let browserId = uuidv4() browserEntry.browserId = browserId browserEntry.status = 'not'; nextRequest.res.send({ availableBrowserslot: availableBrowserslot }); } catch (error) { nextRequest.res.status(500).send({ error: 'Failed to allocate browser.' }); } } else if (waitingQueue.length > 0) { } }; const releaseBrowser = async (browserslot) => { const browserEntry = browserPool[browserslot]; if (browserEntry && browserEntry.browser) { await browserEntry.browser.close(); await browserEntry.browser0.close(); browserEntry.browserId = null; browserEntry.status = 'empty'; browserEntry.browser = null; browserEntry.browser0 = null; browserEntry.pages = {}; browserEntry.lastActivity = Date.now(); processNextInQueue(); } }; setInterval(async () => { const now = Date.now(); for (const [browserslot, browserEntry] of Object.entries(browserPool)) { if (browserEntry.status === 'not' && now - browserEntry.lastActivity > 600000) { await releaseBrowser(browserslot); } } }, 60000); function findPageByPageId(browserId, pageId) { const slot = Object.keys(browserPool).find(slot => browserPool[slot].browserId === browserId); const browserEntry = browserPool[slot] if (browserEntry && browserEntry.pages[pageId]) { return browserEntry.pages[pageId]; } return null; } function findPagePrefixesWithCurrentMark(browserId, currentPageId) { const slot = Object.keys(browserPool).find(slot => browserPool[slot].browserId === browserId); const browserEntry = browserPool[slot] let pagePrefixes = []; if (browserEntry) { console.log(`current page id:${currentPageId}`, typeof currentPageId) for (const pageId in browserEntry.pages) { const page = browserEntry.pages[pageId]; const pageTitle = page.pageTitle; console.log(`iter page id:${pageId}`, typeof pageId) const isCurrentPage = pageId === currentPageId; const pagePrefix = `Tab ${pageId}${isCurrentPage ? ' (current)' : ''}: ${pageTitle}`; pagePrefixes.push(pagePrefix); } } return pagePrefixes.length > 0 ? pagePrefixes.join('\n') : null; } const { Mutex } = require("async-mutex"); const mutex = new Mutex(); app.post('/getBrowser', async (req, res) => { const { storageState, geoLocation } = req.body; const tryAllocateBrowser = () => { const availableBrowserslot = Object.keys(browserPool).find( id => browserPool[id].status === 'empty' ); let browserId = null; if (availableBrowserslot) { browserId = uuidv4() browserPool[availableBrowserslot].browserId = browserId } return {availableBrowserslot, browserId}; }; const waitForAvailableBrowser = () => { return new Promise(resolve => { waitingQueue.push(request => resolve(request)); }); }; // Acquire the mutex lock const release = await mutex.acquire(); try { let {availableBrowserslot, browserId} = tryAllocateBrowser(); if (!availableBrowserslot) { await waitForAvailableBrowser().then((id) => { availableBrowserslot = id; }); } console.log(storageState); let browserEntry = browserPool[availableBrowserslot]; if (!browserEntry.browser) { chromium.use(StealthPlugin()) // Configure browser launch options based on environment const isContainer = process.env.DOCKER_CONTAINER === 'true'; const launchOptions = { headless: true, chromiumSandbox: !isContainer, // Disable sandbox only in container }; // Add container-specific arguments if running in Docker if (isContainer) { launchOptions.args = [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', // Overcome limited resource problems '--disable-gpu' // Applicable to docker containers ]; console.log('[INFO] Running in container mode - sandbox disabled for compatibility'); } else { console.log('[INFO] Running in host mode - sandbox enabled for security'); } const new_browser = await chromium.launch(launchOptions); browserEntry.browser = await new_browser.newContext({ viewport: {width: 1024, height: 768}, locale: 'en-US', // Set the locale to English (US) geolocation: { latitude: 40.4415, longitude: -80.0125 }, // Coordinates for Pittsburgh, PA, USA permissions: ['geolocation'], // Grant geolocation permissions userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' // Example user agent }); browserEntry.browser0 = new_browser; } browserEntry.status = 'not'; browserEntry.lastActivity = Date.now(); console.log(`browserId: ${browserId}`) res.send({browserId: browserId}); } catch (error) { console.error(error); res.status(500).send({ error: 'Failed to get browser.' }); } finally { // Release the mutex lock release(); } }); app.post('/closeBrowser', async (req, res) => { const { browserId } = req.body; if (!browserId) { return res.status(400).send({ error: 'Missing required field: browserId.' }); } const slot = Object.keys(browserPool).find(slot => browserPool[slot].browserId === browserId); const browserEntry = browserPool[slot] if (!browserEntry || !browserEntry.browser) { return res.status(404).send({ error: 'Browser not found.' }); } try { await browserEntry.browser.close(); await browserEntry.browser0.close(); browserEntry.browserId = null; browserEntry.pages = {}; browserEntry.browser = null; browserEntry.browser0 = null; browserEntry.status = 'empty'; browserEntry.lastActivity = null; if (waitingQueue.length > 0) { const nextRequest = waitingQueue.shift(); const nextAvailableBrowserId = Object.keys(browserPool).find( id => browserPool[id].status === 'empty' ); if (nextRequest && nextAvailableBrowserId) { browserPool[nextAvailableBrowserId].status = 'not'; nextRequest(nextAvailableBrowserId); } } res.send({ message: 'Browser closed successfully.' }); } catch (error) { console.error(error); res.status(500).send({ error: 'Failed to close browser.' }); } }); app.post('/openPage', async (req, res) => { const { browserId, url } = req.body; if (!browserId || !url) { return res.status(400).send({ error: 'Missing browserId or url.' }); } const slot = Object.keys(browserPool).find(slot => browserPool[slot].browserId === browserId); const browserEntry = browserPool[slot] // const browserEntry = browserPool[browserId]; if (!browserEntry || !browserEntry.browser) { return res.status(404).send({ error: 'Browser not found.' }); } console.log(await browserEntry.browser.storageState()); const setCustomUserAgent = async (page) => { await page.addInitScript(() => { Object.defineProperty(navigator, 'userAgent', { get: () => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', }); }); }; try { console.log(`[WEB_SERVER] OpenPage: Creating new page for browser ${browserId}`); const page = await browserEntry.browser.newPage(); await setCustomUserAgent(page); console.log(`[WEB_SERVER] OpenPage: Navigating to URL: ${url}`); const startTime = Date.now(); await page.goto(url); const endTime = Date.now(); console.log(`[WEB_SERVER] OpenPage: Navigation completed in ${endTime - startTime}ms`); const currentUrl = page.url(); console.log(`[WEB_SERVER] OpenPage: Actual URL after navigation: ${currentUrl}`); if (currentUrl !== url) { console.log(`[WEB_SERVER] OpenPage: URL_MISMATCH - Expected: ${url} | Actual: ${currentUrl}`); } const pageIdint = Object.keys(browserEntry.pages).length; console.log(`current page id:${pageIdint}`) const pageTitle = await page.title(); console.log(`[WEB_SERVER] OpenPage: Page title: ${pageTitle}`); const pageId = String(pageIdint); browserEntry.pages[pageId] = {'pageId': pageId, 'pageTitle': pageTitle, 'page': page, 'downloadedFiles': [], 'downloadSources': []}; browserEntry.lastActivity = Date.now(); // Define your download path const downloadPath = `./DownloadedFiles/${browserId}`; path.resolve(downloadPath); console.log(`Download path: ${downloadPath}`); // Ensure the download directory exists // try { // await fs.access(downloadPath); // } catch (error) { // if (error.code === 'ENOENT') { // await fs.mkdir(downloadPath, { recursive: true }); // } else { // console.error(`Failed to access download directory: ${error}`); // return; // } // } // Listen for the download event page.on('download', async (download) => { try { console.log('Download object properties:', download.url(), download.suggestedFilename(), download.failure()); const tmp_downloadPath = await download.path(); console.log(`Download path: ${tmp_downloadPath}`); // Get the original filename const filename = download.suggestedFilename(); console.log(`Suggested filename: ${filename}`); // Create the full path to save the file try { await fs.access(downloadPath); } catch (error) { if (error.code === 'ENOENT') { await fs.mkdir(downloadPath, { recursive: true }); } else { console.error(`Failed to access download directory: ${error}`); return; } } const filePath = path.join(downloadPath, filename); console.log(`Saving to path: ${filePath}`); // Save the file to the specified path await download.saveAs(filePath); console.log(`Download completed: ${filePath}`); browserEntry.pages[pageId].downloadedFiles.push(filePath); } catch (error) { console.error(`Failed to save download: ${error}`); } }); const userAgent = await page.evaluate(() => navigator.userAgent); console.log('USER AGENT: ', userAgent); res.send({ browserId, pageId }); } catch (error) { console.error(error); res.status(500).send({ error: 'Failed to open new page.' }); } }); function parseAccessibilityTree(nodes) { const IGNORED_ACTREE_PROPERTIES = [ "focusable", "editable", "readonly", "level", "settable", "multiline", "invalid", "hiddenRoot", "hidden", "controls", "labelledby", "describedby", "url" ]; const IGNORED_ACTREE_ROLES = [ "gridcell", ]; let nodeIdToIdx = {}; nodes.forEach((node, idx) => { if (!(node.nodeId in nodeIdToIdx)) { nodeIdToIdx[node.nodeId] = idx; } }); let treeIdxtoElement = {}; function dfs(idx, depth, parent_name) { let treeStr = ""; let node = nodes[idx]; let indent = "\t".repeat(depth); let validNode = true; try { let role = node.role.value; let name = node.name.value; let nodeStr = `${role} '${name}'`; if (!name.trim() || IGNORED_ACTREE_ROLES.includes(role) || (parent_name.trim().includes(name.trim()) && ["StaticText", "heading", "image", "generic"].includes(role))){ validNode = false; } else{ let properties = []; (node.properties || []).forEach(property => { if (!IGNORED_ACTREE_PROPERTIES.includes(property.name)) { properties.push(`${property.name}: ${property.value.value}`); } }); if (properties.length) { nodeStr += " " + properties.join(" "); } } if (validNode) { treeIdxtoElement[Object.keys(treeIdxtoElement).length + 1] = node; treeStr += `${indent}[${Object.keys(treeIdxtoElement).length}] ${nodeStr}`; } } catch (e) { validNode = false; } for (let childNodeId of node.childIds) { if (Object.keys(treeIdxtoElement).length >= 300) { break; } if (!(childNodeId in nodeIdToIdx)) { continue; } let childDepth = validNode ? depth + 1 : depth; let curr_name = validNode ? node.name.value : parent_name; let childStr = dfs(nodeIdToIdx[childNodeId], childDepth, curr_name); if (childStr.trim()) { if (treeStr.trim()) { treeStr += "\n"; } treeStr += childStr; } } return treeStr; } let treeStr = dfs(0, 0, 'root'); return {treeStr, treeIdxtoElement}; } async function getBoundingClientRect(client, backendNodeId) { try { // Resolve the node to get the RemoteObject const remoteObject = await client.send("DOM.resolveNode", {backendNodeId: parseInt(backendNodeId)}); const remoteObjectId = remoteObject.object.objectId; // Call a function on the resolved node to get its bounding client rect const response = await client.send("Runtime.callFunctionOn", { objectId: remoteObjectId, functionDeclaration: ` function() { if (this.nodeType === 3) { // Node.TEXT_NODE var range = document.createRange(); range.selectNode(this); var rect = range.getBoundingClientRect().toJSON(); range.detach(); return rect; } else { return this.getBoundingClientRect().toJSON(); } } `, returnByValue: true }); return response; } catch (e) { return {result: {subtype: "error"}}; } } async function fetchPageAccessibilityTree(accessibilityTree) { let seenIds = new Set(); let filteredAccessibilityTree = []; let backendDOMids = []; for (let i = 0; i < accessibilityTree.length; i++) { if (filteredAccessibilityTree.length >= 20000) { break; } let node = accessibilityTree[i]; if (!seenIds.has(node.nodeId) && 'backendDOMNodeId' in node) { filteredAccessibilityTree.push(node); seenIds.add(node.nodeId); backendDOMids.push(node.backendDOMNodeId); } } accessibilityTree = filteredAccessibilityTree; return [accessibilityTree, backendDOMids]; } async function fetchAllBoundingClientRects(client, backendNodeIds) { const fetchRectPromises = backendNodeIds.map(async (backendNodeId) => { return getBoundingClientRect(client, backendNodeId); }); try { const results = await Promise.all(fetchRectPromises); return results; } catch (error) { console.error("An error occurred:", error); } } function removeNodeInGraph(node, nodeidToCursor, accessibilityTree) { const nodeid = node.nodeId; const nodeCursor = nodeidToCursor[nodeid]; const parentNodeid = node.parentId; const childrenNodeids = node.childIds; const parentCursor = nodeidToCursor[parentNodeid]; // Update the children of the parent node if (accessibilityTree[parentCursor] !== undefined) { // Remove the nodeid from parent's childIds const index = accessibilityTree[parentCursor].childIds.indexOf(nodeid); //console.log('index:', index); accessibilityTree[parentCursor].childIds.splice(index, 1); // Insert childrenNodeids in the same location childrenNodeids.forEach((childNodeid, idx) => { if (childNodeid in nodeidToCursor) { accessibilityTree[parentCursor].childIds.splice(index + idx, 0, childNodeid); } }); // Update children node's parent childrenNodeids.forEach(childNodeid => { if (childNodeid in nodeidToCursor) { const childCursor = nodeidToCursor[childNodeid]; accessibilityTree[childCursor].parentId = parentNodeid; } }); } accessibilityTree[nodeCursor].parentId = "[REMOVED]"; } function processAccessibilityTree(accessibilityTree, minRatio) { const nodeidToCursor = {}; accessibilityTree.forEach((node, index) => { nodeidToCursor[node.nodeId] = index; }); let count = 0; accessibilityTree.forEach(node => { if (node.union_bound === undefined) { removeNodeInGraph(node, nodeidToCursor, accessibilityTree); return; } const x = node.union_bound.x; const y = node.union_bound.y; const width = node.union_bound.width; const height = node.union_bound.height; // Invisible node if (width === 0 || height === 0) { removeNodeInGraph(node, nodeidToCursor, accessibilityTree); return; } const inViewportRatio = getInViewportRatio( parseFloat(x), parseFloat(y), parseFloat(width), parseFloat(height), ); // if (inViewportRatio < 0.5) { if (inViewportRatio < minRatio) { count += 1; removeNodeInGraph(node, nodeidToCursor, accessibilityTree); } }); console.log('number of nodes marked:', count); accessibilityTree = accessibilityTree.filter(node => node.parentId !== "[REMOVED]"); return accessibilityTree; } function getInViewportRatio(elemLeftBound, elemTopBound, width, height, config) { const elemRightBound = elemLeftBound + width; const elemLowerBound = elemTopBound + height; const winLeftBound = 0; const winRightBound = 1024; const winTopBound = 0; const winLowerBound = 768; const overlapWidth = Math.max( 0, Math.min(elemRightBound, winRightBound) - Math.max(elemLeftBound, winLeftBound), ); const overlapHeight = Math.max( 0, Math.min(elemLowerBound, winLowerBound) - Math.max(elemTopBound, winTopBound), ); const ratio = (overlapWidth * overlapHeight) / (width * height); return ratio; } app.post('/getAccessibilityTree', async (req, res) => { const { browserId, pageId, currentRound } = req.body; if (!browserId || !pageId) { return res.status(400).send({ error: 'Missing browserId or pageId.' }); } const pageEntry = findPageByPageId(browserId, pageId); if (!pageEntry) { return res.status(404).send({ error: 'pageEntry not found.' }); } const page = pageEntry.page; if (!page) { return res.status(404).send({ error: 'Page not found.' }); } try { console.time('FullAXTTime'); const client = await page.context().newCDPSession(page); const response = await client.send('Accessibility.getFullAXTree'); const [axtree, backendDOMids] = await fetchPageAccessibilityTree(response.nodes); console.log('finished fetching page accessibility tree') const boundingClientRects = await fetchAllBoundingClientRects(client, backendDOMids);; console.log('finished fetching bounding client rects') console.log('boundingClientRects:', boundingClientRects.length, 'axtree:', axtree.length); for (let i = 0; i < boundingClientRects.length; i++) { if (axtree[i].role.value === 'RootWebArea') { axtree[i].union_bound = [0.0, 0.0, 10.0, 10.0]; } else { axtree[i].union_bound = boundingClientRects[i].result.value; } } const clone_axtree = processAccessibilityTree(JSON.parse(JSON.stringify(axtree)), -1.0); // no space pruning const pruned_axtree = processAccessibilityTree(axtree, 0.5); const fullTreeRes = parseAccessibilityTree(clone_axtree); // full tree const {treeStr, treeIdxtoElement} = parseAccessibilityTree(pruned_axtree); // pruned tree console.timeEnd('FullAXTTime'); console.log(treeStr); pageEntry['treeIdxtoElement'] = treeIdxtoElement; const accessibilitySnapshot = await page.accessibility.snapshot(); const prefix = findPagePrefixesWithCurrentMark(browserId, pageId) || ''; let yamlWithPrefix = `${prefix}\n${treeStr}`; // if (pageEntry['downloadedFiles'].length > 0) { // if (pageEntry['downloadSources'].length < pageEntry['downloadedFiles'].length) { // const source_name = pruned_axtree[0].name.value; // while (pageEntry['downloadSources'].length < pageEntry['downloadedFiles'].length) { // pageEntry['downloadSources'].push(source_name); // } // } // const downloadedFiles = pageEntry['downloadedFiles']; // yamlWithPrefix += `\n\nYou have successfully downloaded the following files:\n`; // downloadedFiles.forEach((file, idx) => { // yamlWithPrefix += `File ${idx + 1} (from ${pageEntry['downloadSources'][idx]}): ${file}\n`; // } // ); // } const screenshotBuffer = await page.screenshot(); const fileName = `${browserId}@@${pageId}@@${currentRound}.png`; const screenshotPath = './screenshots'; const filePath = path.join(screenshotPath, fileName); // Ensure the download directory exists try { await fs.access(screenshotPath); } catch (error) { if (error.code === 'ENOENT') { await fs.mkdir(screenshotPath, { recursive: true }); } else { console.error(`Failed to access download directory: ${error}`); return; } } // await fs.writeFile(filePath, screenshotBuffer); const boxed_screenshotBuffer = await getboxedScreenshot( page, browserId, pageId, currentRound, treeIdxtoElement ); const currentUrl = page.url(); const html = await page.content(); res.send({ yaml: yamlWithPrefix, fulltree: fullTreeRes.treeStr, url: currentUrl, html: html, snapshot: accessibilitySnapshot, nonboxed_screenshot: screenshotBuffer.toString("base64"), boxed_screenshot: boxed_screenshotBuffer.toString("base64"), downloaded_file_path: pageEntry['downloadedFiles']}); } catch (error) { console.error(error); res.status(500).send({ error: 'Failed to get accessibility tree.' }); } }); async function getboxedScreenshot( page, browserId, pageId, currentRound, treeIdxtoElement ) { // filter treeIdxtoElement to only include elements that are interactive // (e.g., buttons, links, form elements, etc.) const interactiveElements = {}; Object.keys(treeIdxtoElement).forEach(function (index) { var elementData = treeIdxtoElement[index]; var role = elementData.role.value; if ( role === "button" || role === "link" || role === "tab" || role.includes("box") ) { interactiveElements[index] = elementData; } }); await page.evaluate((interactiveElements) => { Object.keys(interactiveElements).forEach(function (index) { var elementData = interactiveElements[index]; var unionBound = elementData.union_bound; // Access the union_bound object // Create a new div element to represent the bounding box var newElement = document.createElement("div"); var borderColor = "#000000"; // Use your color function to get the color newElement.style.outline = `2px dashed ${borderColor}`; newElement.style.position = "fixed"; // Use union_bound's x, y, width, and height newElement.style.left = unionBound.x + "px"; newElement.style.top = unionBound.y + "px"; newElement.style.width = unionBound.width + "px"; newElement.style.height = unionBound.height + "px"; newElement.style.pointerEvents = "none"; newElement.style.boxSizing = "border-box"; newElement.style.zIndex = 2147483647; newElement.classList.add("bounding-box"); // Create a floating label to show the index var label = document.createElement("span"); label.textContent = index; label.style.position = "absolute"; // Adjust label position with respect to union_bound label.style.top = Math.max(-19, -unionBound.y) + "px"; label.style.left = Math.min(Math.floor(unionBound.width / 5), 2) + "px"; label.style.background = borderColor; label.style.color = "white"; label.style.padding = "2px 4px"; label.style.fontSize = "12px"; label.style.borderRadius = "2px"; newElement.appendChild(label); // Append the element to the document body document.body.appendChild(newElement); }); }, interactiveElements); // Pass treeIdxtoElement here as a second argument // Optionally wait a bit to ensure the boxes are drawn await page.waitForTimeout(1000); // Take the screenshot const screenshotBuffer = await page.screenshot(); // Define the file name and path const fileName = `${browserId}@@${pageId}@@${currentRound}_with_box.png`; const filePath = path.join("./screenshots", fileName); // Write the screenshot to a file await fs.writeFile(filePath, screenshotBuffer); await page.evaluate(() => { document.querySelectorAll(".bounding-box").forEach((box) => box.remove()); }); return screenshotBuffer; } async function adjustAriaHiddenForSubmenu(menuitemElement) { try { const submenu = await menuitemElement.$('div.submenu'); if (submenu) { await submenu.evaluate(node => { node.setAttribute('aria-hidden', 'false'); }); } } catch (e) { console.log('Failed to adjust aria-hidden for submenu:', e); } } async function clickElement(click_locator, adjust_aria_label, x1, x2, y1, y2) { const elements = adjust_aria_label ? await click_locator.elementHandles() : await click_locator.all(); if (elements.length > 1) { for (const element of elements) { await element.evaluate(el => { if (el.tagName.toLowerCase() === 'a' && el.hasAttribute('target')) { el.setAttribute('target', '_self'); } }); } const targetX = (x1 + x2) / 2; const targetY = (y1 + y2) / 2; let closestElement = null; let closestDistance = Infinity; for (const element of elements) { const boundingBox = await element.boundingBox(); if (boundingBox) { const elementCenterX = boundingBox.x + boundingBox.width / 2; const elementCenterY = boundingBox.y + boundingBox.height / 2; const distance = Math.sqrt( Math.pow(elementCenterX - targetX, 2) + Math.pow(elementCenterY - targetY, 2) ); if (distance < closestDistance) { closestDistance = distance; closestElement = element; } } } await closestElement.click({ timeout: 5000, force: true}); if (adjust_aria_label) { await adjustAriaHiddenForSubmenu(closestElement); } } else if (elements.length === 1) { await elements[0].evaluate(el => { if (el.tagName.toLowerCase() === 'a' && el.hasAttribute('target')) { el.setAttribute('target', '_self'); } }); await elements[0].click({ timeout: 5000, force: true}); if (adjust_aria_label) { await adjustAriaHiddenForSubmenu(elements[0]); } } else { return false; } return true; } app.post('/performAction', async (req, res) => { const { browserId, pageId, actionName, targetId, targetElementType, targetElementName, actionValue, needEnter } = req.body; console.log(`[WEB_SERVER] PerformAction: Received action request`); console.log(`[WEB_SERVER] PerformAction: Browser: ${browserId} | Page: ${pageId} | Action: ${actionName}`); console.log(`[WEB_SERVER] PerformAction: Target: ${targetElementType} | Name: ${targetElementName} | Value: ${actionValue}`); if (['click', 'type'].includes(actionName) && (!browserId || !actionName || !targetElementType || !pageId)) { console.log(`[WEB_SERVER] PerformAction: ERROR - Missing required fields for ${actionName}`); return res.status(400).send({ error: 'Missing required fields.' }); } else if (!browserId || !actionName || !pageId) { console.log(`[WEB_SERVER] PerformAction: ERROR - Missing basic required fields`); return res.status(400).send({ error: 'Missing required fields.' }); } const slot = Object.keys(browserPool).find(slot => browserPool[slot].browserId === browserId); console.log(`[WEB_SERVER] PerformAction: Found browser slot: ${slot}`); const browserEntry = browserPool[slot] if (!browserEntry || !browserEntry.browser) { console.log(`[WEB_SERVER] PerformAction: ERROR - Browser not found for ID: ${browserId}`); return res.status(404).send({ error: 'Browser not found.' }); } const pageEntry = browserEntry.pages[pageId]; console.log(`[WEB_SERVER] PerformAction: Page entry found: ${pageEntry ? 'YES' : 'NO'}`); if (!pageEntry || !pageEntry.page) { console.log(`[WEB_SERVER] PerformAction: ERROR - Page not found for ID: ${pageId}`); console.log(`[WEB_SERVER] PerformAction: Available pages: ${Object.keys(browserEntry.pages)}`); return res.status(404).send({ error: 'Page not found.' }); } try { const page = pageEntry.page; const treeIdxtoElement = pageEntry.treeIdxtoElement; let adjust_aria_label = false; if (targetElementType === 'menuitem' || targetElementType === 'combobox') { adjust_aria_label = true; } switch (actionName) { case 'click': let element = treeIdxtoElement[targetId]; let clicked = false; let click_locator; try{ click_locator = await page.getByRole(targetElementType, { name: targetElementName, exact:true, timeout: 5000}); clicked = await clickElement(click_locator, adjust_aria_label, element.union_bound.x, element.union_bound.x + element.union_bound.width, element.union_bound.y, element.union_bound.y + element.union_bound.height); } catch (e) { console.log(e); clicked = false; } if (!clicked) { const click_locator = await page.getByRole(targetElementType, { name: targetElementName}); clicked = await clickElement(click_locator, adjust_aria_label, element.union_bound.x, element.union_bound.x + element.union_bound.width, element.union_bound.y, element.union_bound.y + element.union_bound.height); if (!clicked) { const targetElementNameStartWords = targetElementName.split(' ').slice(0, 3).join(' '); const click_locator = await page.getByText(targetElementNameStartWords); clicked = await clickElement(click_locator, adjust_aria_label, element.union_bound.x, element.union_bound.x + element.union_bound.width, element.union_bound.y, element.union_bound.y + element.union_bound.height); if (!clicked) { return res.status(400).send({ error: 'No clickable element found.' }); } } } await page.waitForTimeout(5000); break; case 'type': let type_clicked = false; let locator; let node = treeIdxtoElement[targetId]; try{ locator = await page.getByRole(targetElementType, { name: targetElementName, exact:true, timeout: 5000}).first() type_clicked = await clickElement(locator, adjust_aria_label, node.union_bound.x, node.union_bound.x + node.union_bound.width, node.union_bound.y, node.union_bound.y + node.union_bound.height); } catch (e) { console.log(e); type_clicked = false; } if (!type_clicked) { locator = await page.getByRole(targetElementType, { name: targetElementName}).first() type_clicked = await clickElement(locator, adjust_aria_label, node.union_bound.x, node.union_bound.x + node.union_bound.width, node.union_bound.y, node.union_bound.y + node.union_bound.height); if (!type_clicked) { locator = await page.getByPlaceholder(targetElementName).first(); type_clicked = await clickElement(locator, adjust_aria_label, node.union_bound.x, node.union_bound.x + node.union_bound.width, node.union_bound.y, node.union_bound.y + node.union_bound.height); if (!type_clicked) { return res.status(400).send({ error: 'No clickable element found.' }); } } } await page.keyboard.press('Control+A'); await page.keyboard.press('Backspace'); if (needEnter) { const newactionValue = actionValue + '\n'; await page.keyboard.type(newactionValue); } else { await page.keyboard.type(actionValue); } break; case 'select': let menu_locator = await page.getByRole(targetElementType, { name: targetElementName, exact:true, timeout: 5000}); await menu_locator.selectOption({ label: actionValue }) await menu_locator.click(); break; case 'scroll': if (actionValue === 'down') { await page.evaluate(() => window.scrollBy(0, window.innerHeight)); } else if (actionValue === 'up') { await page.evaluate(() => window.scrollBy(0, -window.innerHeight)); } else { return res.status(400).send({ error: 'Unsupported scroll direction.' }); } break; case 'goback': await page.goBack(); break; case 'goto': console.log(`[WEB_SERVER] PerformAction: GOTO - Navigating to: ${actionValue}`); const gotoStartTime = Date.now(); try { await page.goto(actionValue, { timeout: 60000 }); const gotoEndTime = Date.now(); const finalUrl = page.url(); console.log(`[WEB_SERVER] PerformAction: GOTO - Navigation completed in ${gotoEndTime - gotoStartTime}ms`); console.log(`[WEB_SERVER] PerformAction: GOTO - Final URL: ${finalUrl}`); if (finalUrl !== actionValue) { console.log(`[WEB_SERVER] PerformAction: GOTO - URL_MISMATCH - Expected: ${actionValue} | Actual: ${finalUrl}`); } } catch (error) { console.log(`[WEB_SERVER] PerformAction: GOTO - Navigation FAILED: ${error.message}`); throw error; } break; case 'restart': await page.goto("https://www.bing.com"); // await page.goto(actionValue); break; case 'wait': await sleep(3000); break; default: return res.status(400).send({ error: 'Unsupported action.' }); } browserEntry.lastActivity = Date.now(); await sleep(3000); const currentUrl = page.url(); console.log(`current url: ${currentUrl}`); res.send({ message: 'Action performed successfully.' }); } catch (error) { console.error(error); res.status(500).send({ error: 'Failed to perform action.' }); } }); app.post('/gotoUrl', async (req, res) => { const { browserId, pageId, targetUrl } = req.body; if (!targetUrl) { return res.status(400).send({ error: 'Missing required fields.' }); } const slot = Object.keys(browserPool).find(slot => browserPool[slot].browserId === browserId); const browserEntry = browserPool[slot] if (!browserEntry || !browserEntry.browser) { return res.status(404).send({ error: 'Browser not found.' }); } const pageEntry = browserEntry.pages[pageId]; if (!pageEntry || !pageEntry.page) { return res.status(404).send({ error: 'Page not found.' }); } try { const page = pageEntry.page; console.log(`target url: ${targetUrl}`); await page.goto(targetUrl, { timeout: 60000 }); browserEntry.lastActivity = Date.now(); await sleep(3000); const currentUrl = page.url(); console.log(`current url: ${currentUrl}`); res.send({ message: 'Action performed successfully.' }); } catch (error) { console.error(error); res.status(500).send({ error: 'Failed to perform action.' }); } }); app.post('/takeScreenshot', async (req, res) => { const { browserId, pageId } = req.body; if (!browserId || !pageId) { return res.status(400).send({ error: 'Missing required fields: browserId, pageId.' }); } const slot = Object.keys(browserPool).find(slot => browserPool[slot].browserId === browserId); const browserEntry = browserPool[slot] if (!browserEntry || !browserEntry.browser) { return res.status(404).send({ error: 'Browser not found.' }); } const pageEntry = browserEntry.pages[pageId]; if (!pageEntry || !pageEntry.page) { return res.status(404).send({ error: 'Page not found.' }); } try { const page = pageEntry.page; const screenshotBuffer = await page.screenshot({ fullPage: true }); res.setHeader('Content-Type', 'image/png'); res.send(screenshotBuffer); } catch (error) { console.error(error); res.status(500).send({ error: 'Failed to take screenshot.' }); } }); app.post('/loadScreenshot', (req, res) => { const { browserId, pageId, currentRound } = req.body; const fileName = `${browserId}@@${pageId}@@${currentRound}.png`; const filePath = path.join('./screenshots', fileName); res.sendFile(filePath, (err) => { if (err) { console.error(err); if (err.code === 'ENOENT') { res.status(404).send({ error: 'Screenshot not found.' }); } else { res.status(500).send({ error: 'Error sending screenshot file.' }); } } }); }); app.post("/gethtmlcontent", async (req, res) => { const { browserId, pageId, currentRound } = req.body; // if (!browserId || !pageId) { // return res.status(400).send({ error: 'Missing browserId or pageId.' }); // } const pageEntry = findPageByPageId(browserId, pageId); const page = pageEntry.page; // if (!page) { // return res.status(404).send({ error: 'Page not found.' }); // } try { const html = await page.content(); const currentUrl = page.url(); res.send({ html: html, url: currentUrl }); } catch (error) { console.error(error); res.status(500).send({ error: "Failed to get html content." }); } }); app.post('/getFile', async (req, res) => { try { const { filename } = req.body; if (!filename) { return res.status(400).send({ error: 'Filename is required.' }); } const data = await fs.readFile(filename); // simply directly read it! const base64String = data.toString('base64'); res.send({ file: base64String }); } catch (err) { console.error(err); res.status(500).send({ error: 'File not found or cannot be read.' }); } }); // 健康检查端点 app.get('/health', (req, res) => { const healthStatus = { status: 'healthy', timestamp: new Date().toISOString(), uptime: process.uptime(), memory: process.memoryUsage(), browserPool: { total: maxBrowsers, active: Object.values(browserPool).filter(b => b.status !== 'empty').length, empty: Object.values(browserPool).filter(b => b.status === 'empty').length } }; res.json(healthStatus); }); app.listen(port, () => { initializeBrowserPool(maxBrowsers); console.log(`Server listening at http://localhost:${port}`); console.log(`Health check available at http://localhost:${port}/health`); }); process.on('exit', async () => { for (const browserEntry of browserPool) { await browserEntry.browser.close(); await browserEntry.browser0.close(); } });