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 {