diff --git a/legacy/src/RobotEdit.tsx b/legacy/src/RobotEdit.tsx index 3b110ba17..f468d2333 100644 --- a/legacy/src/RobotEdit.tsx +++ b/legacy/src/RobotEdit.tsx @@ -44,6 +44,7 @@ export interface RobotSettings { google_access_token?: string | null; google_refresh_token?: string | null; schedule?: ScheduleConfig | null; + proxy?: string | null; } interface RobotSettingsProps { @@ -128,20 +129,19 @@ export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettin const extractedCredentials = extractInitialCredentials(robot.recording.workflow); setCredentials(extractedCredentials); setCredentialGroups(groupCredentialsByType(extractedCredentials)); - + findScrapeListLimits(robot.recording.workflow); } }, [robot]); const findScrapeListLimits = (workflow: WhereWhatPair[]) => { const limits: ScrapeListLimit[] = []; - + workflow.forEach((pair, pairIndex) => { if (!pair.what) return; - + pair.what.forEach((action, actionIndex) => { if (action.action === 'scrapeList' && action.args && action.args.length > 0) { - // Check if first argument has a limit property const arg = action.args[0]; if (arg && typeof arg === 'object' && 'limit' in arg) { limits.push({ @@ -154,7 +154,7 @@ export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettin } }); }); - + setScrapeListLimits(limits); }; @@ -183,12 +183,12 @@ export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettin const selector = action.args[0]; - // Handle full word type actions first - if (action.action === 'type' && + if ( + action.action === 'type' && action.args?.length >= 2 && typeof action.args[1] === 'string' && - action.args[1].length > 1) { - + action.args[1].length > 1 + ) { if (!credentials[selector]) { credentials[selector] = { value: action.args[1], @@ -199,11 +199,11 @@ export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettin continue; } - // Handle character-by-character sequences (both type and press) - if ((action.action === 'type' || action.action === 'press') && + if ( + (action.action === 'type' || action.action === 'press') && action.args?.length >= 2 && - typeof action.args[1] === 'string') { - + typeof action.args[1] === 'string' + ) { if (selector !== currentSelector) { if (currentSelector && currentValue) { credentials[currentSelector] = { @@ -231,9 +231,12 @@ export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettin let j = i + 1; while (j < step.what.length) { const nextAction = step.what[j]; - if (!nextAction.action || !nextAction.args?.[0] || + if ( + !nextAction.action || + !nextAction.args?.[0] || nextAction.args[0] !== selector || - (nextAction.action !== 'type' && nextAction.action !== 'press')) { + (nextAction.action !== 'type' && nextAction.action !== 'press') + ) { break; } if (nextAction.args[1] === 'Backspace') { @@ -290,8 +293,11 @@ export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettin const getRobot = async () => { if (recordingId) { - const robot = await getStoredRecording(recordingId); - setRobot(robot); + const robotData = await getStoredRecording(recordingId); + setRobot({ + ...robotData, + proxy: robotData?.proxy ?? null + }); } else { notify('error', t('robot_edit.notifications.update_failed')); } @@ -310,6 +316,12 @@ export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettin ); }; + const handleProxyChange = (newProxy: string) => { + setRobot((prev) => + prev ? { ...prev, proxy: newProxy } : prev + ); + }; + const handleCredentialChange = (selector: string, value: string) => { setCredentials(prev => ({ ...prev, @@ -333,12 +345,14 @@ export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettin updatedWorkflow[pairIndex].what[actionIndex].args.length > argIndex ) { updatedWorkflow[pairIndex].what[actionIndex].args[argIndex].limit = newLimit; - - setScrapeListLimits(prev => { - return prev.map(item => { - if (item.pairIndex === pairIndex && - item.actionIndex === actionIndex && - item.argIndex === argIndex) { + + setScrapeListLimits(prevLimits => { + return prevLimits.map(item => { + if ( + item.pairIndex === pairIndex && + item.actionIndex === actionIndex && + item.argIndex === argIndex + ) { return { ...item, currentLimit: newLimit }; } return item; @@ -437,13 +451,13 @@ export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettin const renderScrapeListLimitFields = () => { if (scrapeListLimits.length === 0) return null; - + return ( <> {t('List Limits')} - + {scrapeListLimits.map((limitInfo, index) => ( + handleProxyChange(e.target.value)} + placeholder="http://username:password@host:port" + style={{ marginBottom: '20px' }} + /> + {renderScrapeListLimitFields()} {(Object.keys(credentials).length > 0) && ( @@ -573,7 +597,8 @@ export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettin color: '#ff00c3 !important', borderColor: '#ff00c3 !important', backgroundColor: 'whitesmoke !important', - }}> + }} + > {t('robot_edit.cancel')} diff --git a/server/src/api/record.ts b/server/src/api/record.ts index 8fc927fb6..5f74146e0 100644 --- a/server/src/api/record.ts +++ b/server/src/api/record.ts @@ -493,20 +493,27 @@ async function createWorkflowAndStoreMetadata(id: string, userId: string, isSDK: }; } - const proxyConfig = await getDecryptedProxyConfig(userId); let proxyOptions: any = {}; - if (proxyConfig.proxy_url) { + if ((recording as any).proxy) { proxyOptions = { - server: proxyConfig.proxy_url, - ...(proxyConfig.proxy_username && proxyConfig.proxy_password && { - username: proxyConfig.proxy_username, - password: proxyConfig.proxy_password, - }), + server: (recording as any).proxy, }; + } else { + const proxyConfig = await getDecryptedProxyConfig(userId); + + if (proxyConfig.proxy_url) { + proxyOptions = { + server: proxyConfig.proxy_url, + ...(proxyConfig.proxy_username && proxyConfig.proxy_password && { + username: proxyConfig.proxy_username, + password: proxyConfig.proxy_password, + }), + }; + } } - const browserId = createRemoteBrowserForRun(userId); + const browserId = createRemoteBrowserForRun(userId, proxyOptions); const runId = uuid(); @@ -543,7 +550,7 @@ async function createWorkflowAndStoreMetadata(id: string, userId: string, isSDK: runByAPI: plainRun.runByAPI || false, browserId: plainRun.browserId }; - + serverIo.of('/queued-run').to(`user-${userId}`).emit('run-started', runStartedData); logger.log('info', `API run started notification sent for run: ${plainRun.runId} to user-${userId}`); } catch (socketError: any) { @@ -1518,31 +1525,32 @@ router.post("/robots/:id/duplicate", requireAPIKey, async (req: Request, res: Re const currentTimestamp = new Date().toLocaleString(); const newRobot = await Robot.create({ - id: uuid(), - userId: originalRobot.userId, - recording_meta: { - ...originalRobot.recording_meta, - id: uuid(), - name: `${originalRobot.recording_meta.name} (${lastWord})`, - url: targetUrl, - createdAt: currentTimestamp, - updatedAt: currentTimestamp, - }, - recording: { ...originalRobot.recording, workflow }, - google_sheet_email: null, - google_sheet_name: null, - google_sheet_id: null, - google_access_token: null, - google_refresh_token: null, - airtable_base_id: null, - airtable_base_name: null, - airtable_table_name: null, - airtable_table_id: null, - airtable_access_token: null, - airtable_refresh_token: null, - webhooks: null, - schedule: null, - }); + id: uuid(), + userId: originalRobot.userId, + recording_meta: { + ...originalRobot.recording_meta, + id: uuid(), + name: `${originalRobot.recording_meta.name} (${lastWord})`, + url: targetUrl, + createdAt: currentTimestamp, + updatedAt: currentTimestamp, + }, + recording: { ...originalRobot.recording, workflow }, + proxy: originalRobot.proxy, + google_sheet_email: null, + google_sheet_name: null, + google_sheet_id: null, + google_access_token: null, + google_refresh_token: null, + airtable_base_id: null, + airtable_base_name: null, + airtable_table_name: null, + airtable_table_id: null, + airtable_access_token: null, + airtable_refresh_token: null, + webhooks: null, + schedule: null, +}); logger.log('info', `Robot with ID ${id} duplicated successfully as ${newRobot.id}.`); diff --git a/server/src/api/sdk.ts b/server/src/api/sdk.ts index c9e6e2a74..c5b7c3130 100644 --- a/server/src/api/sdk.ts +++ b/server/src/api/sdk.ts @@ -1177,6 +1177,7 @@ router.post("/sdk/extract/llm", requireAPIKey, async (req: AuthenticatedRequest, capture("maxun-oss-llm-robot-created", { robot_meta: robot.recording_meta, recording: robot.recording, + proxy: null, prompt: prompt }); diff --git a/server/src/browser-management/classes/RemoteBrowser.ts b/server/src/browser-management/classes/RemoteBrowser.ts index 8f910df0c..84f52c50d 100644 --- a/server/src/browser-management/classes/RemoteBrowser.ts +++ b/server/src/browser-management/classes/RemoteBrowser.ts @@ -12,7 +12,6 @@ import { readFileSync } from "fs"; import { InterpreterSettings } from "../../types"; import { WorkflowGenerator } from "../../workflow-management/classes/Generator"; import { WorkflowInterpreter } from "../../workflow-management/classes/Interpreter"; -import { getDecryptedProxyConfig } from '../../routes/proxy'; import { getInjectableScript } from 'idcac-playwright'; import { FingerprintInjector } from "fingerprint-injector"; import { FingerprintGenerator } from "fingerprint-generator"; @@ -119,58 +118,10 @@ export class RemoteBrowser { this.socket = socket; this.userId = userId; this.interpreter = new WorkflowInterpreter(socket); - this.generator = new WorkflowGenerator(socket, poolId); + this.generator = new WorkflowGenerator(socket, poolId, userId); this.isRecordingMode = isRecordingMode; } - // private initializeMemoryManagement(): void { - // this.memoryManagementInterval = setInterval(() => { - // const memoryUsage = process.memoryUsage(); - // const heapUsageRatio = memoryUsage.heapUsed / MEMORY_CONFIG.maxHeapSize; - - // if (heapUsageRatio > MEMORY_CONFIG.heapUsageThreshold * 1.2) { - // logger.warn( - // "Critical memory pressure detected, triggering emergency cleanup" - // ); - // this.performMemoryCleanup(); - // } else if (heapUsageRatio > MEMORY_CONFIG.heapUsageThreshold) { - // logger.warn("High memory usage detected, triggering cleanup"); - - // if ( - // global.gc && - // heapUsageRatio > MEMORY_CONFIG.heapUsageThreshold * 1.1 - // ) { - // global.gc(); - // } - // } - // }, MEMORY_CONFIG.gcInterval); - // } - - // private async performMemoryCleanup(): Promise { - // if (global.gc) { - // try { - // global.gc(); - // logger.info("Garbage collection requested"); - // } catch (error) { - // logger.error("Error during garbage collection:", error); - // } - // } - - // if (this.currentPage) { - // try { - // await new Promise((resolve) => setTimeout(resolve, 500)); - // logger.info("CDP session reset completed"); - // } catch (error) { - // logger.error("Error resetting CDP session:", error); - // } - // } - - // this.socket.emit("memory-cleanup", { - // userId: this.userId, - // timestamp: Date.now(), - // }); - // } - /** * Normalizes URLs to prevent navigation loops while maintaining consistent format */ @@ -398,26 +349,22 @@ export class RemoteBrowser { return userAgents[Math.floor(Math.random() * userAgents.length)]; } - /** - * Apply modern fingerprint-suite injection - */ - private async applyEnhancedFingerprinting(context: BrowserContext): Promise { - try { - try { - const fingerprintGenerator = new FingerprintGenerator(); - const fingerprint = fingerprintGenerator.getFingerprint(); - const fingerprintInjector = new FingerprintInjector(); + /** + * Apply modern fingerprint-suite injection + */ + private async applyEnhancedFingerprinting(context: BrowserContext): Promise { + try { + const fingerprintGenerator = new FingerprintGenerator(); + const fingerprint = fingerprintGenerator.getFingerprint(); + const fingerprintInjector = new FingerprintInjector(); - await fingerprintInjector.attachFingerprintToPlaywright(context as any, fingerprint); + await fingerprintInjector.attachFingerprintToPlaywright(context as any, fingerprint); - logger.info("Enhanced fingerprinting applied successfully"); - } catch (fingerprintError: any) { - logger.warn(`Modern fingerprint injection failed: ${fingerprintError.message}. Using existing protection.`); - } - } catch (error: any) { - logger.error(`Enhanced fingerprinting failed: ${error.message}`); - } + logger.info("Enhanced fingerprinting applied successfully"); + } catch (fingerprintError: any) { + logger.warn(`Modern fingerprint injection failed: ${fingerprintError.message}. Using existing protection.`); } +} /** * An asynchronous constructor for asynchronously initialized properties. @@ -425,7 +372,10 @@ export class RemoteBrowser { * @param options remote browser options to be used when launching the browser * @returns {Promise} */ - public initialize = async (userId: string): Promise => { + public initialize = async ( + userId: string, + proxyOptions?: { server: string; username?: string; password?: string } + ): Promise => { const MAX_RETRIES = 3; const OVERALL_INIT_TIMEOUT = 120000; let retryCount = 0; @@ -448,40 +398,21 @@ export class RemoteBrowser { this.emitLoadingProgress(20, 0); - const proxyConfig = await getDecryptedProxyConfig(userId); - let proxyOptions: { server: string, username?: string, password?: string } = { server: '' }; - - if (proxyConfig.proxy_url) { - proxyOptions = { - server: proxyConfig.proxy_url, - ...(proxyConfig.proxy_username && proxyConfig.proxy_password && { - username: proxyConfig.proxy_username, - password: proxyConfig.proxy_password, - }), - }; - } - const contextOptions: any = { - // viewport: { height: 400, width: 900 }, - // recordVideo: { dir: 'videos/' } - // Force reduced motion to prevent animation issues reducedMotion: 'reduce', - // Force JavaScript to be enabled javaScriptEnabled: true, - // Set a reasonable timeout timeout: 50000, - // Disable hardware acceleration forcedColors: 'none', isMobile: false, hasTouch: false, userAgent: this.getUserAgent(), }; - if (proxyOptions.server) { + if (proxyOptions?.server) { contextOptions.proxy = { server: proxyOptions.server, - username: proxyOptions.username ? proxyOptions.username : undefined, - password: proxyOptions.password ? proxyOptions.password : undefined, + username: proxyOptions.username, + password: proxyOptions.password, }; } @@ -704,16 +635,6 @@ export class RemoteBrowser { public async switchOff(): Promise { this.isDOMStreamingActive = false; - // if (this.memoryCleanupInterval) { - // clearInterval(this.memoryCleanupInterval); - // this.memoryCleanupInterval = null; - // } - - // if (this.memoryManagementInterval) { - // clearInterval(this.memoryManagementInterval); - // this.memoryManagementInterval = null; - // } - this.removeAllSocketListeners(); try { @@ -834,7 +755,6 @@ export class RemoteBrowser { const workflow = this.generator.AddGeneratedFlags(this.generator.getWorkflowFile()); await this.initializeNewPage(); if (this.currentPage) { - // this.currentPage.setViewportSize({ height: 400, width: 900 }); const params = this.generator.getParams(); if (params) { this.interpreterSettings.params = params.reduce((acc, param) => { @@ -851,7 +771,6 @@ export class RemoteBrowser { (newPage: Page) => this.currentPage = newPage, this.interpreterSettings ); - // clear the active index from generator this.generator.clearLastIndex(); } else { logger.log('error', 'Could not get a new page, returned undefined'); @@ -883,10 +802,8 @@ export class RemoteBrowser { await this.setupPageEventListeners(this.currentPage); - //await this.currentPage.setViewportSize({ height: 400, width: 900 }) this.client = await this.currentPage.context().newCDPSession(this.currentPage); - // Include userId in the URL change event - this.socket.emit('urlChanged', { + this.socket.emit('urlChanged', { url: this.currentPage.url(), userId: this.userId }); @@ -916,4 +833,4 @@ export class RemoteBrowser { logger.log('error', 'Could not get a new page, returned undefined'); } }; -} +} \ No newline at end of file diff --git a/server/src/browser-management/controller.ts b/server/src/browser-management/controller.ts index 79379ee67..a355fbb5b 100644 --- a/server/src/browser-management/controller.ts +++ b/server/src/browser-management/controller.ts @@ -37,7 +37,7 @@ export const initializeRemoteBrowserForRecording = (userId: string, mode: string } else { const browserSession = new RemoteBrowser(socket, userId, id, true); browserSession.interpreter.subscribeToPausing(); - + try { await browserSession.initialize(userId); await browserSession.registerEditorEvents(); @@ -108,15 +108,16 @@ export const initializeRemoteBrowserForRecording = (userId: string, mode: string * Creates a new {@link Socket} connection over a dedicated namespace. * Returns the new remote browser's generated id. * @param userId User ID for browser ownership + * @param proxyOptions Optional proxy options for this run * @returns string Browser ID * @category BrowserManagement-Controller */ -export const createRemoteBrowserForRun = (userId: string): string => { +export const createRemoteBrowserForRun = (userId: string, proxyOptions?: any): string => { if (!userId) { logger.log('error', 'createRemoteBrowserForRun: Missing required parameter userId'); throw new Error('userId is required'); } - + const id = uuid(); const slotReserved = browserPool.reserveBrowserSlotAtomic(id, userId, "run"); @@ -127,12 +128,12 @@ export const createRemoteBrowserForRun = (userId: string): string => { logger.log('info', `createRemoteBrowserForRun: Reserved slot ${id} for user ${userId}`); - initializeBrowserAsync(id, userId) + initializeBrowserAsync(id, userId, proxyOptions) .catch((error: any) => { logger.log('error', `Unhandled error in initializeBrowserAsync for browser ${id}: ${error.message}`); browserPool.failBrowserSlot(id); }); - + return id; }; @@ -330,12 +331,12 @@ export const stopRunningInterpretation = async (userId: string) => { } }; -const initializeBrowserAsync = async (id: string, userId: string) => { +const initializeBrowserAsync = async (id: string, userId: string, proxyOptions?: any) => { try { const namespace = io.of(id); let clientConnected = false; let connectionTimeout: NodeJS.Timeout; - + const waitForConnection = new Promise((resolve) => { namespace.on('connection', (socket: Socket) => { clientConnected = true; @@ -343,7 +344,7 @@ const initializeBrowserAsync = async (id: string, userId: string) => { logger.log('info', `Frontend connected to browser ${id} via socket ${socket.id}`); resolve(socket); }); - + connectionTimeout = setTimeout(() => { if (!clientConnected) { logger.log('warn', `No client connected to browser ${id} within timeout, proceeding with dummy socket`); @@ -360,7 +361,7 @@ const initializeBrowserAsync = async (id: string, userId: string) => { const connectWithRetry = async (maxRetries: number = 3): Promise => { let retryCount = 0; - + while (retryCount < maxRetries) { try { const socket = await waitForConnection; @@ -370,7 +371,7 @@ const initializeBrowserAsync = async (id: string, userId: string) => { } catch (error: any) { logger.log('warn', `Connection attempt ${retryCount + 1} failed for browser ${id}: ${error.message}`); } - + retryCount++; if (retryCount < maxRetries) { const delay = Math.pow(2, retryCount) * 1000; @@ -378,15 +379,15 @@ const initializeBrowserAsync = async (id: string, userId: string) => { await new Promise(resolve => setTimeout(resolve, delay)); } } - + return null; }; const socket = await connectWithRetry(3); - + try { let browserSession: RemoteBrowser; - + if (socket) { logger.log('info', `Using real socket for browser ${id}`); browserSession = new RemoteBrowser(socket, userId, id); @@ -399,7 +400,7 @@ const initializeBrowserAsync = async (id: string, userId: string) => { on: () => {}, id: `dummy-${id}`, } as any; - + browserSession = new RemoteBrowser(dummySocket, userId, id); } @@ -409,7 +410,7 @@ const initializeBrowserAsync = async (id: string, userId: string) => { const BROWSER_INIT_TIMEOUT = 45000; logger.log('info', `Browser initialization starting with ${BROWSER_INIT_TIMEOUT/1000}s timeout`); - const initPromise = browserSession.initialize(userId); + const initPromise = browserSession.initialize(userId, proxyOptions); const timeoutPromise = new Promise((_, reject) => { setTimeout(() => reject(new Error('Browser initialization timeout')), BROWSER_INIT_TIMEOUT); }); @@ -435,9 +436,9 @@ const initializeBrowserAsync = async (id: string, userId: string) => { } throw new Error('Failed to upgrade reserved browser slot'); } - + await new Promise(resolve => setTimeout(resolve, 500)); - + if (socket) { socket.emit('ready-for-run'); } else { @@ -447,11 +448,11 @@ const initializeBrowserAsync = async (id: string, userId: string) => { } catch (error: any) { logger.log('error', `Error with dummy socket browser ${id}: ${error.message}`); } - }, 100); + }, 100); } - + logger.log('info', `Browser ${id} successfully initialized for run with ${socket ? 'real' : 'dummy'} socket`); - + } catch (error: any) { logger.log('error', `Error initializing browser ${id}: ${error.message}`); browserPool.failBrowserSlot(id); @@ -460,7 +461,7 @@ const initializeBrowserAsync = async (id: string, userId: string) => { } throw error; } - + } catch (error: any) { logger.log('error', `Error setting up browser ${id}: ${error.message}`); browserPool.failBrowserSlot(id); diff --git a/server/src/db/migrations/20260312120000-add-proxy-to-robot.js b/server/src/db/migrations/20260312120000-add-proxy-to-robot.js new file mode 100644 index 000000000..ccb5f4dab --- /dev/null +++ b/server/src/db/migrations/20260312120000-add-proxy-to-robot.js @@ -0,0 +1,23 @@ +'use strict'; + +module.exports = { + async up(queryInterface, Sequelize) { + const table = await queryInterface.describeTable('robot'); + + if (!table.proxy) { + await queryInterface.addColumn('robot', 'proxy', { + type: Sequelize.STRING, + allowNull: true, + comment: 'Optional proxy URL for this robot; overrides default proxy when set' + }); + } + }, + + async down(queryInterface, Sequelize) { + const table = await queryInterface.describeTable('robot'); + + if (table.proxy) { + await queryInterface.removeColumn('robot', 'proxy'); + } + } +}; \ No newline at end of file diff --git a/server/src/models/Robot.ts b/server/src/models/Robot.ts index 637bcb9cb..bfdde9788 100644 --- a/server/src/models/Robot.ts +++ b/server/src/models/Robot.ts @@ -29,8 +29,21 @@ interface WebhookConfig { updatedAt: string; lastCalledAt?: string | null; retryAttempts?: number; - retryDelay?: number; - timeout?: number; + retryDelay?: number; + timeout?: number; +} + +interface ScheduleConfig { + runEvery: number; + runEveryUnit: 'MINUTES' | 'HOURS' | 'DAYS' | 'WEEKS' | 'MONTHS'; + startFrom: 'SUNDAY' | 'MONDAY' | 'TUESDAY' | 'WEDNESDAY' | 'THURSDAY' | 'FRIDAY' | 'SATURDAY'; + atTimeStart?: string; + atTimeEnd?: string; + timezone: string; + lastRunAt?: Date; + nextRunAt?: Date; + dayOfMonth?: string; + cronExpression?: string; } interface RobotAttributes { @@ -38,52 +51,53 @@ interface RobotAttributes { userId?: number; recording_meta: RobotMeta; recording: RobotWorkflow; + + /** NEW: proxy for robot */ + proxy?: string | null; + google_sheet_email?: string | null; google_sheet_name?: string | null; google_sheet_id?: string | null; google_access_token?: string | null; google_refresh_token?: string | null; - airtable_base_id?: string | null; - airtable_base_name?: string | null; - airtable_table_name?: string | null; - airtable_access_token?: string | null; - airtable_refresh_token?: string | null; - schedule?: ScheduleConfig | null; + + airtable_base_id?: string | null; + airtable_base_name?: string | null; + airtable_table_name?: string | null; + airtable_access_token?: string | null; + airtable_refresh_token?: string | null; airtable_table_id?: string | null; - webhooks?: WebhookConfig[] | null; -} -interface ScheduleConfig { - runEvery: number; - runEveryUnit: 'MINUTES' | 'HOURS' | 'DAYS' | 'WEEKS' | 'MONTHS'; - startFrom: 'SUNDAY' | 'MONDAY' | 'TUESDAY' | 'WEDNESDAY' | 'THURSDAY' | 'FRIDAY' | 'SATURDAY'; - atTimeStart?: string; - atTimeEnd?: string; - timezone: string; - lastRunAt?: Date; - nextRunAt?: Date; - dayOfMonth?: string; - cronExpression?: string; + schedule?: ScheduleConfig | null; + webhooks?: WebhookConfig[] | null; } -interface RobotCreationAttributes extends Optional { } +interface RobotCreationAttributes extends Optional {} class Robot extends Model implements RobotAttributes { + public id!: string; public userId!: number; + public recording_meta!: RobotMeta; public recording!: RobotWorkflow; + + /** NEW: proxy field */ + public proxy!: string | null; + public google_sheet_email!: string | null; public google_sheet_name!: string | null; public google_sheet_id!: string | null; public google_access_token!: string | null; public google_refresh_token!: string | null; - public airtable_base_id!: string | null; - public airtable_base_name!: string | null; - public airtable_table_name!: string | null; - public airtable_access_token!: string | null; - public airtable_refresh_token!: string | null; - public airtable_table_id!: string | null; + + public airtable_base_id!: string | null; + public airtable_base_name!: string | null; + public airtable_table_name!: string | null; + public airtable_access_token!: string | null; + public airtable_refresh_token!: string | null; + public airtable_table_id!: string | null; + public schedule!: ScheduleConfig | null; public webhooks!: WebhookConfig[] | null; } @@ -95,66 +109,88 @@ Robot.init( defaultValue: DataTypes.UUIDV4, primaryKey: true, }, + userId: { type: DataTypes.INTEGER, allowNull: false, }, + recording_meta: { type: DataTypes.JSONB, allowNull: false, }, + recording: { type: DataTypes.JSONB, allowNull: false, }, + + /** NEW proxy column */ + proxy: { + type: DataTypes.STRING, + allowNull: true, + }, + google_sheet_email: { type: DataTypes.STRING, allowNull: true, }, + google_sheet_name: { type: DataTypes.STRING, allowNull: true, }, + google_sheet_id: { type: DataTypes.STRING, allowNull: true, }, + google_access_token: { type: DataTypes.STRING, allowNull: true, }, + google_refresh_token: { type: DataTypes.STRING, allowNull: true, }, + airtable_base_id: { type: DataTypes.STRING, allowNull: true, }, + airtable_base_name: { type: DataTypes.STRING, allowNull: true, }, + airtable_table_name: { type: DataTypes.STRING, allowNull: true, }, + airtable_table_id: { type: DataTypes.STRING, allowNull: true, }, + airtable_access_token: { type: DataTypes.TEXT, allowNull: true, }, + airtable_refresh_token: { type: DataTypes.TEXT, allowNull: true, }, + schedule: { type: DataTypes.JSONB, allowNull: true, }, + webhooks: { type: DataTypes.JSONB, allowNull: true, diff --git a/server/src/routes/storage.ts b/server/src/routes/storage.ts index ea710bb8b..16dedf559 100644 --- a/server/src/routes/storage.ts +++ b/server/src/routes/storage.ts @@ -521,6 +521,7 @@ router.post('/recordings/scrape', requireSignIn, async (req: AuthenticatedReques formats: finalFormats, }, recording: { workflow: [] }, + proxy: null, google_sheet_email: null, google_sheet_name: null, google_sheet_id: null, @@ -644,6 +645,7 @@ router.post('/recordings/llm', requireSignIn, async (req: AuthenticatedRequest, google_sheet_id: null, google_access_token: null, google_refresh_token: null, + proxy: null, schedule: null, }); @@ -795,6 +797,7 @@ router.post('/recordings/:id/duplicate', requireSignIn, async (req: Authenticate airtable_refresh_token: null, webhooks: null, schedule: null, + proxy: null, }); logger.log('info', `Robot with ID ${id} duplicated successfully as ${newRobot.id}.`); @@ -1605,7 +1608,9 @@ router.post('/recordings/crawl', requireSignIn, async (req: AuthenticatedRequest airtable_access_token: null, airtable_refresh_token: null, schedule: null, - webhooks: null + webhooks: null, + proxy: null, + }); logger.log('info', `Crawl robot created with id: ${newRobot.id}`); @@ -1723,7 +1728,8 @@ router.post('/recordings/search', requireSignIn, async (req: AuthenticatedReques airtable_access_token: null, airtable_refresh_token: null, schedule: null, - webhooks: null + webhooks: null, + proxy: null, }); logger.log('info', `Search robot created with id: ${newRobot.id}`); diff --git a/server/src/workflow-management/classes/Generator.ts b/server/src/workflow-management/classes/Generator.ts index c59c626fb..fe939708c 100644 --- a/server/src/workflow-management/classes/Generator.ts +++ b/server/src/workflow-management/classes/Generator.ts @@ -72,6 +72,8 @@ export class WorkflowGenerator { private poolId: string | null = null; + private userId: string = ''; + private pageCloseListeners: Map void> = new Map(); /** @@ -80,9 +82,10 @@ export class WorkflowGenerator { * @param socket The socket used to communicate with the client. * @constructor */ - public constructor(socket: Socket, poolId: string) { + constructor(socket: Socket, poolId: string, userId: string) { this.socket = socket; this.poolId = poolId; + this.userId = userId; this.registerEventHandlers(socket); this.initializeSocketListeners(); this.initializeDOMListeners(); @@ -1034,6 +1037,7 @@ export class WorkflowGenerator { userId, recording_meta: this.recordingMeta, recording: recording, + proxy: null, }); capture( 'maxun-oss-robot-created', @@ -1153,7 +1157,7 @@ export class WorkflowGenerator { */ public notifyUrlChange = (url: string) => { if (this.socket) { - this.socket.emit('urlChanged', url); + this.socket.emit('urlChanged', { url, userId: this.userId }) } } diff --git a/src/api/storage.ts b/src/api/storage.ts index d07913323..a195f1b96 100644 --- a/src/api/storage.ts +++ b/src/api/storage.ts @@ -107,6 +107,7 @@ export const updateRecording = async (id: string, data: { credentials?: Credentials; targetUrl?: string; workflow?: any[]; + proxy?: string | null; formats?: string[]; }): Promise => { try {