mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-20 20:38:16 -05:00
improvement(browseruse): add profile id param (#2903)
* improvement(browseruse): add profile id param * make request a stub since we have directExec
This commit is contained in:
committed by
GitHub
parent
a26a1a9737
commit
8344d68ca8
@@ -57,6 +57,12 @@ export const BrowserUseBlock: BlockConfig<BrowserUseResponse> = {
|
||||
type: 'switch',
|
||||
placeholder: 'Save browser data',
|
||||
},
|
||||
{
|
||||
id: 'profile_id',
|
||||
title: 'Profile ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter browser profile ID (optional)',
|
||||
},
|
||||
{
|
||||
id: 'apiKey',
|
||||
title: 'API Key',
|
||||
@@ -75,6 +81,7 @@ export const BrowserUseBlock: BlockConfig<BrowserUseResponse> = {
|
||||
variables: { type: 'json', description: 'Task variables' },
|
||||
model: { type: 'string', description: 'AI model to use' },
|
||||
save_browser_data: { type: 'boolean', description: 'Save browser data' },
|
||||
profile_id: { type: 'string', description: 'Browser profile ID for persistent sessions' },
|
||||
},
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Task execution identifier' },
|
||||
|
||||
@@ -1,11 +1,214 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type { BrowserUseRunTaskParams, BrowserUseRunTaskResponse } from '@/tools/browser_use/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('BrowserUseTool')
|
||||
|
||||
const POLL_INTERVAL_MS = 5000 // 5 seconds between polls
|
||||
const MAX_POLL_TIME_MS = 180000 // 3 minutes maximum polling time
|
||||
const POLL_INTERVAL_MS = 5000
|
||||
const MAX_POLL_TIME_MS = 180000
|
||||
const MAX_CONSECUTIVE_ERRORS = 3
|
||||
|
||||
async function createSessionWithProfile(
|
||||
profileId: string,
|
||||
apiKey: string
|
||||
): Promise<{ sessionId: string } | { error: string }> {
|
||||
try {
|
||||
const response = await fetch('https://api.browser-use.com/api/v2/sessions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Browser-Use-API-Key': apiKey,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
profileId: profileId.trim(),
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error(`Failed to create session with profile: ${errorText}`)
|
||||
return { error: `Failed to create session with profile: ${response.statusText}` }
|
||||
}
|
||||
|
||||
const data = (await response.json()) as { id: string }
|
||||
logger.info(`Created session ${data.id} with profile ${profileId}`)
|
||||
return { sessionId: data.id }
|
||||
} catch (error: any) {
|
||||
logger.error('Error creating session with profile:', error)
|
||||
return { error: `Error creating session: ${error.message}` }
|
||||
}
|
||||
}
|
||||
|
||||
async function stopSession(sessionId: string, apiKey: string): Promise<void> {
|
||||
try {
|
||||
const response = await fetch(`https://api.browser-use.com/api/v2/sessions/${sessionId}`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Browser-Use-API-Key': apiKey,
|
||||
},
|
||||
body: JSON.stringify({ action: 'stop' }),
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
logger.info(`Stopped session ${sessionId}`)
|
||||
} else {
|
||||
logger.warn(`Failed to stop session ${sessionId}: ${response.statusText}`)
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.warn(`Error stopping session ${sessionId}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
function buildRequestBody(
|
||||
params: BrowserUseRunTaskParams,
|
||||
sessionId?: string
|
||||
): Record<string, any> {
|
||||
const requestBody: Record<string, any> = {
|
||||
task: params.task,
|
||||
}
|
||||
|
||||
if (sessionId) {
|
||||
requestBody.sessionId = sessionId
|
||||
logger.info(`Using session ${sessionId} for task`)
|
||||
}
|
||||
|
||||
if (params.variables) {
|
||||
let secrets: Record<string, string> = {}
|
||||
|
||||
if (Array.isArray(params.variables)) {
|
||||
logger.info('Converting variables array to dictionary format')
|
||||
params.variables.forEach((row: any) => {
|
||||
if (row.cells?.Key && row.cells.Value !== undefined) {
|
||||
secrets[row.cells.Key] = row.cells.Value
|
||||
logger.info(`Added secret for key: ${row.cells.Key}`)
|
||||
} else if (row.Key && row.Value !== undefined) {
|
||||
secrets[row.Key] = row.Value
|
||||
logger.info(`Added secret for key: ${row.Key}`)
|
||||
}
|
||||
})
|
||||
} else if (typeof params.variables === 'object' && params.variables !== null) {
|
||||
logger.info('Using variables object directly')
|
||||
secrets = params.variables
|
||||
}
|
||||
|
||||
if (Object.keys(secrets).length > 0) {
|
||||
logger.info(`Found ${Object.keys(secrets).length} secrets to include`)
|
||||
requestBody.secrets = secrets
|
||||
} else {
|
||||
logger.warn('No usable secrets found in variables')
|
||||
}
|
||||
}
|
||||
|
||||
if (params.model) {
|
||||
requestBody.llm_model = params.model
|
||||
}
|
||||
|
||||
if (params.save_browser_data) {
|
||||
requestBody.save_browser_data = params.save_browser_data
|
||||
}
|
||||
|
||||
requestBody.use_adblock = true
|
||||
requestBody.highlight_elements = true
|
||||
|
||||
return requestBody
|
||||
}
|
||||
|
||||
async function fetchTaskStatus(
|
||||
taskId: string,
|
||||
apiKey: string
|
||||
): Promise<{ ok: true; data: any } | { ok: false; error: string }> {
|
||||
try {
|
||||
const response = await fetch(`https://api.browser-use.com/api/v2/tasks/${taskId}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Browser-Use-API-Key': apiKey,
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
return { ok: false, error: `HTTP ${response.status}: ${response.statusText}` }
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return { ok: true, data }
|
||||
} catch (error: any) {
|
||||
return { ok: false, error: error.message || 'Network error' }
|
||||
}
|
||||
}
|
||||
|
||||
async function pollForCompletion(
|
||||
taskId: string,
|
||||
apiKey: string
|
||||
): Promise<{ success: boolean; output: any; steps: any[]; error?: string }> {
|
||||
let liveUrlLogged = false
|
||||
let consecutiveErrors = 0
|
||||
const startTime = Date.now()
|
||||
|
||||
while (Date.now() - startTime < MAX_POLL_TIME_MS) {
|
||||
const result = await fetchTaskStatus(taskId, apiKey)
|
||||
|
||||
if (!result.ok) {
|
||||
consecutiveErrors++
|
||||
logger.warn(
|
||||
`Error polling task ${taskId} (attempt ${consecutiveErrors}/${MAX_CONSECUTIVE_ERRORS}): ${result.error}`
|
||||
)
|
||||
|
||||
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
|
||||
logger.error(`Max consecutive errors reached for task ${taskId}`)
|
||||
return {
|
||||
success: false,
|
||||
output: null,
|
||||
steps: [],
|
||||
error: `Failed to poll task status after ${MAX_CONSECUTIVE_ERRORS} attempts: ${result.error}`,
|
||||
}
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS))
|
||||
continue
|
||||
}
|
||||
|
||||
consecutiveErrors = 0
|
||||
const taskData = result.data
|
||||
const status = taskData.status
|
||||
|
||||
logger.info(`BrowserUse task ${taskId} status: ${status}`)
|
||||
|
||||
if (['finished', 'failed', 'stopped'].includes(status)) {
|
||||
return {
|
||||
success: status === 'finished',
|
||||
output: taskData.output ?? null,
|
||||
steps: taskData.steps || [],
|
||||
}
|
||||
}
|
||||
|
||||
if (!liveUrlLogged && taskData.live_url) {
|
||||
logger.info(`BrowserUse task ${taskId} live URL: ${taskData.live_url}`)
|
||||
liveUrlLogged = true
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS))
|
||||
}
|
||||
|
||||
const finalResult = await fetchTaskStatus(taskId, apiKey)
|
||||
if (finalResult.ok && ['finished', 'failed', 'stopped'].includes(finalResult.data.status)) {
|
||||
return {
|
||||
success: finalResult.data.status === 'finished',
|
||||
output: finalResult.data.output ?? null,
|
||||
steps: finalResult.data.steps || [],
|
||||
}
|
||||
}
|
||||
|
||||
logger.warn(
|
||||
`Task ${taskId} did not complete within the maximum polling time (${MAX_POLL_TIME_MS / 1000}s)`
|
||||
)
|
||||
return {
|
||||
success: false,
|
||||
output: null,
|
||||
steps: [],
|
||||
error: `Task did not complete within the maximum polling time (${MAX_POLL_TIME_MS / 1000}s)`,
|
||||
}
|
||||
}
|
||||
|
||||
export const runTaskTool: ToolConfig<BrowserUseRunTaskParams, BrowserUseRunTaskResponse> = {
|
||||
id: 'browser_use_run_task',
|
||||
@@ -44,7 +247,14 @@ export const runTaskTool: ToolConfig<BrowserUseRunTaskParams, BrowserUseRunTaskR
|
||||
visibility: 'user-only',
|
||||
description: 'API key for BrowserUse API',
|
||||
},
|
||||
profile_id: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Browser profile ID for persistent sessions (cookies, login state)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: 'https://api.browser-use.com/api/v2/tasks',
|
||||
method: 'POST',
|
||||
@@ -52,155 +262,94 @@ export const runTaskTool: ToolConfig<BrowserUseRunTaskParams, BrowserUseRunTaskR
|
||||
'Content-Type': 'application/json',
|
||||
'X-Browser-Use-API-Key': params.apiKey,
|
||||
}),
|
||||
body: (params) => {
|
||||
const requestBody: Record<string, any> = {
|
||||
task: params.task,
|
||||
}
|
||||
|
||||
if (params.variables) {
|
||||
let secrets: Record<string, string> = {}
|
||||
|
||||
if (Array.isArray(params.variables)) {
|
||||
logger.info('Converting variables array to dictionary format')
|
||||
params.variables.forEach((row) => {
|
||||
if (row.cells?.Key && row.cells.Value !== undefined) {
|
||||
secrets[row.cells.Key] = row.cells.Value
|
||||
logger.info(`Added secret for key: ${row.cells.Key}`)
|
||||
} else if (row.Key && row.Value !== undefined) {
|
||||
secrets[row.Key] = row.Value
|
||||
logger.info(`Added secret for key: ${row.Key}`)
|
||||
}
|
||||
})
|
||||
} else if (typeof params.variables === 'object' && params.variables !== null) {
|
||||
logger.info('Using variables object directly')
|
||||
secrets = params.variables
|
||||
}
|
||||
|
||||
if (Object.keys(secrets).length > 0) {
|
||||
logger.info(`Found ${Object.keys(secrets).length} secrets to include`)
|
||||
requestBody.secrets = secrets
|
||||
} else {
|
||||
logger.warn('No usable secrets found in variables')
|
||||
}
|
||||
}
|
||||
|
||||
if (params.model) {
|
||||
requestBody.llm_model = params.model
|
||||
}
|
||||
|
||||
if (params.save_browser_data) {
|
||||
requestBody.save_browser_data = params.save_browser_data
|
||||
}
|
||||
|
||||
requestBody.use_adblock = true
|
||||
requestBody.highlight_elements = true
|
||||
|
||||
return requestBody
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = (await response.json()) as { id: string }
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: data.id,
|
||||
success: true,
|
||||
output: null,
|
||||
steps: [],
|
||||
},
|
||||
}
|
||||
},
|
||||
directExecution: async (params: BrowserUseRunTaskParams): Promise<ToolResponse> => {
|
||||
let sessionId: string | undefined
|
||||
|
||||
postProcess: async (result, params) => {
|
||||
if (!result.success) {
|
||||
return result
|
||||
if (params.profile_id) {
|
||||
logger.info(`Creating session with profile ID: ${params.profile_id}`)
|
||||
const sessionResult = await createSessionWithProfile(params.profile_id, params.apiKey)
|
||||
if ('error' in sessionResult) {
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
id: null,
|
||||
success: false,
|
||||
output: null,
|
||||
steps: [],
|
||||
},
|
||||
error: sessionResult.error,
|
||||
}
|
||||
}
|
||||
sessionId = sessionResult.sessionId
|
||||
}
|
||||
|
||||
const taskId = result.output.id
|
||||
let liveUrlLogged = false
|
||||
const requestBody = buildRequestBody(params, sessionId)
|
||||
logger.info('Creating BrowserUse task', { hasSession: !!sessionId })
|
||||
|
||||
try {
|
||||
const initialTaskResponse = await fetch(
|
||||
`https://api.browser-use.com/api/v2/tasks/${taskId}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Browser-Use-API-Key': params.apiKey,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if (initialTaskResponse.ok) {
|
||||
const initialTaskData = await initialTaskResponse.json()
|
||||
if (initialTaskData.live_url) {
|
||||
logger.info(
|
||||
`BrowserUse task ${taskId} launched with live URL: ${initialTaskData.live_url}`
|
||||
)
|
||||
liveUrlLogged = true
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to get initial task details for ${taskId}:`, error)
|
||||
}
|
||||
|
||||
let elapsedTime = 0
|
||||
|
||||
while (elapsedTime < MAX_POLL_TIME_MS) {
|
||||
try {
|
||||
const statusResponse = await fetch(`https://api.browser-use.com/api/v2/tasks/${taskId}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Browser-Use-API-Key': params.apiKey,
|
||||
},
|
||||
})
|
||||
|
||||
if (!statusResponse.ok) {
|
||||
throw new Error(`Failed to get task status: ${statusResponse.statusText}`)
|
||||
}
|
||||
|
||||
const taskData = await statusResponse.json()
|
||||
const status = taskData.status
|
||||
|
||||
logger.info(`BrowserUse task ${taskId} status: ${status}`)
|
||||
|
||||
if (['finished', 'failed', 'stopped'].includes(status)) {
|
||||
result.output = {
|
||||
id: taskId,
|
||||
success: status === 'finished',
|
||||
output: taskData.output ?? null,
|
||||
steps: taskData.steps || [],
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
if (!liveUrlLogged && status === 'running' && taskData.live_url) {
|
||||
logger.info(`BrowserUse task ${taskId} running with live URL: ${taskData.live_url}`)
|
||||
liveUrlLogged = true
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS))
|
||||
elapsedTime += POLL_INTERVAL_MS
|
||||
} catch (error: any) {
|
||||
logger.error('Error polling for task status:', {
|
||||
message: error.message || 'Unknown error',
|
||||
taskId,
|
||||
})
|
||||
const response = await fetch('https://api.browser-use.com/api/v2/tasks', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Browser-Use-API-Key': params.apiKey,
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error(`Failed to create task: ${errorText}`)
|
||||
return {
|
||||
...result,
|
||||
error: `Error polling for task status: ${error.message || 'Unknown error'}`,
|
||||
success: false,
|
||||
output: {
|
||||
id: null,
|
||||
success: false,
|
||||
output: null,
|
||||
steps: [],
|
||||
},
|
||||
error: `Failed to create task: ${response.statusText}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.warn(
|
||||
`Task ${taskId} did not complete within the maximum polling time (${MAX_POLL_TIME_MS / 1000}s)`
|
||||
)
|
||||
return {
|
||||
...result,
|
||||
error: `Task did not complete within the maximum polling time (${MAX_POLL_TIME_MS / 1000}s)`,
|
||||
const data = (await response.json()) as { id: string }
|
||||
const taskId = data.id
|
||||
logger.info(`Created BrowserUse task: ${taskId}`)
|
||||
|
||||
const result = await pollForCompletion(taskId, params.apiKey)
|
||||
|
||||
if (sessionId) {
|
||||
await stopSession(sessionId, params.apiKey)
|
||||
}
|
||||
|
||||
return {
|
||||
success: result.success && !result.error,
|
||||
output: {
|
||||
id: taskId,
|
||||
success: result.success,
|
||||
output: result.output,
|
||||
steps: result.steps,
|
||||
},
|
||||
error: result.error,
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.error('Error creating BrowserUse task:', error)
|
||||
|
||||
if (sessionId) {
|
||||
await stopSession(sessionId, params.apiKey)
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
id: null,
|
||||
success: false,
|
||||
output: null,
|
||||
steps: [],
|
||||
},
|
||||
error: `Error creating task: ${error.message}`,
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ export interface BrowserUseRunTaskParams {
|
||||
variables?: Record<string, string>
|
||||
model?: string
|
||||
save_browser_data?: boolean
|
||||
profile_id?: string
|
||||
}
|
||||
|
||||
export interface BrowserUseTaskStep {
|
||||
|
||||
Reference in New Issue
Block a user