diff --git a/agent-server/nodejs/src/api-server.js b/agent-server/nodejs/src/api-server.js index 3f62ae5c0f..afb9acee1e 100644 --- a/agent-server/nodejs/src/api-server.js +++ b/agent-server/nodejs/src/api-server.js @@ -147,6 +147,14 @@ class APIServer { result = await this.getScreenshot(JSON.parse(body)); break; + case '/page/execute': + if (method !== 'POST') { + this.sendError(res, 405, 'Method not allowed'); + return; + } + result = await this.executeJavaScript(JSON.parse(body)); + break; + default: this.sendError(res, 404, 'Not found'); return; @@ -336,6 +344,36 @@ class APIServer { }; } + async executeJavaScript(payload) { + const { clientId, tabId, expression, returnByValue = true, awaitPromise = false } = payload; + + if (!clientId) { + throw new Error('Client ID is required'); + } + + if (!tabId) { + throw new Error('Tab ID is required'); + } + + if (!expression) { + throw new Error('JavaScript expression is required'); + } + + const baseClientId = clientId.split(':')[0]; + + logger.info('Executing JavaScript', { baseClientId, tabId, expression: expression.substring(0, 100) }); + + const result = await this.browserAgentServer.evaluateJavaScript(tabId, expression, { returnByValue, awaitPromise }); + + return { + clientId: baseClientId, + tabId: result.tabId, + result: result.result, + exceptionDetails: result.exceptionDetails, + timestamp: Date.now() + }; + } + /** * Handle OpenAI Responses API compatible requests with nested model format */ diff --git a/agent-server/nodejs/src/lib/BrowserAgentServer.js b/agent-server/nodejs/src/lib/BrowserAgentServer.js index fd5a48de02..736ce8454e 100644 --- a/agent-server/nodejs/src/lib/BrowserAgentServer.js +++ b/agent-server/nodejs/src/lib/BrowserAgentServer.js @@ -1220,6 +1220,74 @@ export class BrowserAgentServer extends EventEmitter { } } + /** + * Execute JavaScript in a browser tab + * @param {string} tabId - Tab ID (target ID) + * @param {string} expression - JavaScript expression to execute + * @param {Object} options - Execution options + * @param {boolean} options.returnByValue - Whether to return by value (default: true) + * @param {boolean} options.awaitPromise - Whether to await promises (default: false) + * @returns {Promise} Result with execution result + */ + async evaluateJavaScript(tabId, expression, options = {}) { + const { returnByValue = true, awaitPromise = false } = options; + + try { + logger.info('Executing JavaScript via CDP', { tabId, expressionLength: expression.length }); + + // Use Runtime.evaluate to execute JavaScript + const result = await this.sendCDPCommandToTarget(tabId, 'Runtime.evaluate', { + expression, + returnByValue, + awaitPromise + }); + + if (result.exceptionDetails) { + logger.warn('JavaScript execution threw exception', { + tabId, + exception: result.exceptionDetails + }); + } else { + logger.info('JavaScript executed successfully', { + tabId, + resultType: result.result?.type + }); + } + + // Extract value from CDP RemoteObject + // CDP returns RemoteObject with structure: {type: 'string', value: 'foo'} + // For undefined/null, CDP returns: {type: 'undefined'} or {type: 'null', value: null} + // We need to check if 'value' property exists, not if it's undefined + let extractedResult; + if (result.result) { + if ('value' in result.result) { + // RemoteObject has a value field - extract it + extractedResult = result.result.value; + } else if (result.result.type === 'undefined') { + // Special case: undefined has no value field + extractedResult = undefined; + } else { + // For objects/functions without returnByValue, return the whole RemoteObject + extractedResult = result.result; + } + } else { + extractedResult = result.result; + } + + return { + tabId, + result: extractedResult, + exceptionDetails: result.exceptionDetails + }; + } catch (error) { + logger.error('Failed to execute JavaScript via CDP', { + tabId, + error: error.message + }); + throw error; + } + } + } /**