@minded-ai/mindedjs 3.1.21 → 3.1.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +0 -4
- package/dist/agent.js.map +1 -1
- package/dist/browserTask/executeBrowserTask.d.ts.map +1 -1
- package/dist/browserTask/executeBrowserTask.js +1 -69
- package/dist/browserTask/executeBrowserTask.js.map +1 -1
- package/dist/nodes/addPromptNode.d.ts.map +1 -1
- package/dist/nodes/addPromptNode.js +1 -0
- package/dist/nodes/addPromptNode.js.map +1 -1
- package/dist/nodes/addToolRunNode.d.ts.map +1 -1
- package/dist/nodes/addToolRunNode.js +1 -0
- package/dist/nodes/addToolRunNode.js.map +1 -1
- package/dist/platform/toolExecutor.d.ts.map +1 -1
- package/dist/platform/toolExecutor.js +1 -0
- package/dist/platform/toolExecutor.js.map +1 -1
- package/dist/toolsLibrary/browserSessionPersistence.d.ts +28 -0
- package/dist/toolsLibrary/browserSessionPersistence.d.ts.map +1 -0
- package/dist/toolsLibrary/browserSessionPersistence.js +159 -0
- package/dist/toolsLibrary/browserSessionPersistence.js.map +1 -0
- package/dist/toolsLibrary/withBrowserSession.d.ts +2 -1
- package/dist/toolsLibrary/withBrowserSession.d.ts.map +1 -1
- package/dist/toolsLibrary/withBrowserSession.js +65 -50
- package/dist/toolsLibrary/withBrowserSession.js.map +1 -1
- package/dist/types/Tools.types.d.ts +1 -0
- package/dist/types/Tools.types.d.ts.map +1 -1
- package/dist/types/Tools.types.js.map +1 -1
- package/docs/low-code-editor/rpa-tools.md +4 -1
- package/docs/low-code-editor/tools.md +86 -1
- package/package.json +2 -2
- package/src/agent.ts +0 -5
- package/src/browserTask/executeBrowserTask.ts +1 -73
- package/src/nodes/addPromptNode.ts +1 -0
- package/src/nodes/addToolRunNode.ts +1 -0
- package/src/platform/toolExecutor.ts +1 -0
- package/src/toolsLibrary/browserSessionPersistence.ts +203 -0
- package/src/toolsLibrary/withBrowserSession.ts +74 -56
- package/src/types/Tools.types.ts +1 -0
|
@@ -17,7 +17,6 @@ import { BrowserTaskMode, InvokeBrowserTaskOptions } from './types';
|
|
|
17
17
|
import { isLocalOperatorSetup, validateLocalOperatorSetup } from '../cli/localOperatorSetup';
|
|
18
18
|
import { getConfig } from '../platform/config';
|
|
19
19
|
import CDP from 'chrome-remote-interface';
|
|
20
|
-
import { CookieStore } from './CookieStore';
|
|
21
20
|
import { ProxyType } from '../types/Tools.types';
|
|
22
21
|
|
|
23
22
|
// Map to track sessionId to instanceId for local browser sessions
|
|
@@ -26,9 +25,6 @@ const localSessionInstances = new Map<string, string>();
|
|
|
26
25
|
// Map to track CDP clients for cookie management
|
|
27
26
|
const cdpClients = new Map<string, CDP.Client>();
|
|
28
27
|
|
|
29
|
-
// Initialize cookie store
|
|
30
|
-
const cookieStore = new CookieStore(path.join(process.cwd(), 'local_storage', 'cookies'));
|
|
31
|
-
|
|
32
28
|
// Helper function to extract port from CDP URL
|
|
33
29
|
const extractPortFromCdpUrl = (cdpUrl: string): number | null => {
|
|
34
30
|
try {
|
|
@@ -39,61 +35,6 @@ const extractPortFromCdpUrl = (cdpUrl: string): number | null => {
|
|
|
39
35
|
}
|
|
40
36
|
};
|
|
41
37
|
|
|
42
|
-
// Helper function to load cookies for a session
|
|
43
|
-
const loadCookiesForSession = async (sessionId: string): Promise<void> => {
|
|
44
|
-
// Check if cookies should be disabled
|
|
45
|
-
const noCookies = process.env.MINDED_NO_COOKIES === 'true';
|
|
46
|
-
if (noCookies) {
|
|
47
|
-
logger.info({ message: '[Browser] Cookies disabled via cookies=false flag', sessionId });
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const stored = await cookieStore.load();
|
|
52
|
-
if (!stored || stored.length === 0) {
|
|
53
|
-
logger.info({ message: '[Browser] No stored cookies to load', sessionId });
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
const cdp = cdpClients.get(sessionId);
|
|
57
|
-
if (!cdp) {
|
|
58
|
-
logger.warn({ message: '[Browser] No CDP client found for session', sessionId });
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
try {
|
|
63
|
-
await cdp.Network.setCookies({ cookies: stored });
|
|
64
|
-
logger.info({ message: '[Browser] Loaded cookies into browser session', sessionId, count: stored.length });
|
|
65
|
-
} catch (error) {
|
|
66
|
-
logger.warn({ message: '[Browser] Failed to set cookies via CDP', sessionId, error });
|
|
67
|
-
}
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
// Helper function to save cookies for a session
|
|
71
|
-
const saveCookiesForSession = async (sessionId: string): Promise<void> => {
|
|
72
|
-
// Check if cookies should be disabled
|
|
73
|
-
const noCookies = process.env.MINDED_NO_COOKIES === 'true';
|
|
74
|
-
if (noCookies) {
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
try {
|
|
79
|
-
const cdp = cdpClients.get(sessionId);
|
|
80
|
-
if (!cdp) {
|
|
81
|
-
logger.warn({ message: '[Browser] No CDP client found for session', sessionId });
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const result = await cdp.Network.getAllCookies();
|
|
86
|
-
if (result) {
|
|
87
|
-
await cookieStore.save(result.cookies);
|
|
88
|
-
logger.info({ message: '[Browser] Saved cookies from browser session', sessionId, count: result.cookies.length });
|
|
89
|
-
} else {
|
|
90
|
-
logger.warn({ message: '[Browser] No cookies found in browser session', sessionId });
|
|
91
|
-
}
|
|
92
|
-
} catch (error) {
|
|
93
|
-
logger.warn({ message: '[Browser] Failed to get/save cookies from session', sessionId, error: JSON.stringify(error) });
|
|
94
|
-
}
|
|
95
|
-
};
|
|
96
|
-
|
|
97
38
|
// Helper function to cleanup CDP client
|
|
98
39
|
const cleanupCdpClient = async (sessionId: string): Promise<void> => {
|
|
99
40
|
const cdp = cdpClients.get(sessionId);
|
|
@@ -144,13 +85,6 @@ export const createBrowserSession = async ({
|
|
|
144
85
|
logger.info({ message: '[Browser] Created CDP client for session', sessionId, port });
|
|
145
86
|
}
|
|
146
87
|
|
|
147
|
-
// Load cookies for this session (best-effort)
|
|
148
|
-
try {
|
|
149
|
-
await loadCookiesForSession(sessionId);
|
|
150
|
-
} catch (error) {
|
|
151
|
-
logger.warn({ message: '[Browser] Failed to load cookies for session', sessionId, error });
|
|
152
|
-
}
|
|
153
|
-
|
|
154
88
|
return {
|
|
155
89
|
sessionId,
|
|
156
90
|
cdpUrl,
|
|
@@ -213,6 +147,7 @@ export const destroyBrowserSession = async ({
|
|
|
213
147
|
};
|
|
214
148
|
}
|
|
215
149
|
|
|
150
|
+
// Destroy browser sessions via socket for both cloud and on-prem
|
|
216
151
|
const response = await mindedConnection.awaitEmit<DestroyBrowserSessionRequest, DestroyBrowserSessionResponse>(
|
|
217
152
|
mindedConnectionSocketMessageType.DESTROY_BROWSER_SESSION,
|
|
218
153
|
{
|
|
@@ -345,13 +280,6 @@ export const invokeBrowserTask = async (options: InvokeBrowserTaskOptions): Prom
|
|
|
345
280
|
}
|
|
346
281
|
}
|
|
347
282
|
|
|
348
|
-
// Save cookies after task completes (best-effort)
|
|
349
|
-
try {
|
|
350
|
-
await saveCookiesForSession(sessionId);
|
|
351
|
-
} catch (error) {
|
|
352
|
-
logger.warn({ message: '[Browser] Failed to save cookies after task completion', sessionId, error });
|
|
353
|
-
}
|
|
354
|
-
|
|
355
283
|
return {
|
|
356
284
|
result,
|
|
357
285
|
steps: [],
|
|
@@ -53,6 +53,7 @@ export const addToolRunNode = async ({ graph, tools, toolNode, attachedToNodeNam
|
|
|
53
53
|
proxyUser: matchedTool.proxyUser,
|
|
54
54
|
proxyPass: matchedTool.proxyPass,
|
|
55
55
|
toolName: matchedTool.name,
|
|
56
|
+
persistSession: matchedTool.persistSession,
|
|
56
57
|
},
|
|
57
58
|
async (page) => {
|
|
58
59
|
// Pass page directly to RPA tool execution
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { BrowserContext, Page } from 'playwright';
|
|
2
|
+
import { logger } from '../utils/logger';
|
|
3
|
+
import { saveRecord, getRecords } from './storage';
|
|
4
|
+
|
|
5
|
+
const BROWSER_SESSION_KEY = 'browser-session';
|
|
6
|
+
|
|
7
|
+
interface Cookie {
|
|
8
|
+
name: string;
|
|
9
|
+
value: string;
|
|
10
|
+
domain?: string;
|
|
11
|
+
path?: string;
|
|
12
|
+
expires?: number;
|
|
13
|
+
httpOnly?: boolean;
|
|
14
|
+
secure?: boolean;
|
|
15
|
+
sameSite?: 'Strict' | 'Lax' | 'None';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface LocalStorageEntry {
|
|
19
|
+
url: string;
|
|
20
|
+
data: Record<string, string>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Extract localStorage from the current page
|
|
25
|
+
*/
|
|
26
|
+
const extractLocalStorageFromPage = async (page: Page): Promise<Record<string, string>> => {
|
|
27
|
+
return await page.evaluate(() => Object.fromEntries(Object.entries(localStorage)));
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Fetch both cookies and localStorage
|
|
32
|
+
*/
|
|
33
|
+
export const fetchSessionDataViaSocket = async (): Promise<{
|
|
34
|
+
cookies: Cookie[] | null;
|
|
35
|
+
localStorage: LocalStorageEntry[] | null;
|
|
36
|
+
}> => {
|
|
37
|
+
try {
|
|
38
|
+
const records = await getRecords({
|
|
39
|
+
filters: { key: BROWSER_SESSION_KEY },
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
if (records.length === 0) {
|
|
43
|
+
return { cookies: null, localStorage: null };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Take the last (most recent) record
|
|
47
|
+
const latestRecord = records[records.length - 1];
|
|
48
|
+
const cookies = latestRecord.data?.cookies || null;
|
|
49
|
+
const localStorage = latestRecord.data?.localStorage || null;
|
|
50
|
+
|
|
51
|
+
return { cookies, localStorage };
|
|
52
|
+
} catch (error) {
|
|
53
|
+
logger.warn({ message: 'Failed to fetch session data', error });
|
|
54
|
+
return { cookies: null, localStorage: null };
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Save both cookies and localStorage for ALL origins
|
|
60
|
+
*/
|
|
61
|
+
export const saveSessionDataViaSocket = async (
|
|
62
|
+
context: BrowserContext,
|
|
63
|
+
page: Page,
|
|
64
|
+
sessionId: string,
|
|
65
|
+
): Promise<void> => {
|
|
66
|
+
try {
|
|
67
|
+
const cookies = await context.cookies();
|
|
68
|
+
const data: Record<string, unknown> = {};
|
|
69
|
+
|
|
70
|
+
if (cookies && cookies.length > 0) {
|
|
71
|
+
data.cookies = cookies;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Extract localStorage for ALL origins using CDP
|
|
75
|
+
let client;
|
|
76
|
+
try {
|
|
77
|
+
client = await context.newCDPSession(page);
|
|
78
|
+
|
|
79
|
+
// Enable Target domain to track all frames and their origins
|
|
80
|
+
await client.send('Target.setDiscoverTargets', { discover: true });
|
|
81
|
+
|
|
82
|
+
// Get all targets (frames, workers, etc.)
|
|
83
|
+
type TargetInfo = {
|
|
84
|
+
targetId: string;
|
|
85
|
+
type: string;
|
|
86
|
+
title: string;
|
|
87
|
+
url: string;
|
|
88
|
+
attached: boolean;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const response = await client.send('Target.getTargets');
|
|
92
|
+
const targetInfos = (response as { targetInfos: TargetInfo[] }).targetInfos;
|
|
93
|
+
|
|
94
|
+
// Extract unique origins from all page-type targets
|
|
95
|
+
const origins = new Set<string>();
|
|
96
|
+
for (const target of targetInfos || []) {
|
|
97
|
+
if (target.type === 'page' || target.type === 'iframe') {
|
|
98
|
+
try {
|
|
99
|
+
const url = new URL(target.url);
|
|
100
|
+
if (url.protocol === 'http:' || url.protocol === 'https:') {
|
|
101
|
+
origins.add(url.origin);
|
|
102
|
+
}
|
|
103
|
+
} catch {
|
|
104
|
+
// Ignore invalid URLs
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Also add the current page origin
|
|
110
|
+
try {
|
|
111
|
+
const currentOrigin = new URL(page.url()).origin;
|
|
112
|
+
origins.add(currentOrigin);
|
|
113
|
+
} catch {
|
|
114
|
+
// Ignore if current URL is invalid
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Extract localStorage from all origins in parallel
|
|
118
|
+
const extractionPromises = Array.from(origins).map(async (origin) => {
|
|
119
|
+
try {
|
|
120
|
+
type DOMStorageResponse = {
|
|
121
|
+
entries: Array<[string, string]>;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const storageResponse = await client!.send('DOMStorage.getDOMStorageItems', {
|
|
125
|
+
storageId: {
|
|
126
|
+
securityOrigin: origin,
|
|
127
|
+
isLocalStorage: true,
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
const { entries } = storageResponse as DOMStorageResponse;
|
|
131
|
+
|
|
132
|
+
if (entries && entries.length > 0) {
|
|
133
|
+
const data = Object.fromEntries(entries);
|
|
134
|
+
|
|
135
|
+
// Check payload size to prevent WebSocket errors (10MB limit as safety margin)
|
|
136
|
+
const dataSize = JSON.stringify(data).length;
|
|
137
|
+
if (dataSize > 10 * 1024 * 1024) {
|
|
138
|
+
logger.warn({
|
|
139
|
+
message: 'localStorage data too large, skipping origin',
|
|
140
|
+
origin,
|
|
141
|
+
sizeMB: (dataSize / 1024 / 1024).toFixed(2)
|
|
142
|
+
});
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
url: origin,
|
|
148
|
+
data,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
} catch (err) {
|
|
152
|
+
logger.warn({ message: 'Failed to extract localStorage for origin', origin, error: err });
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const results = await Promise.all(extractionPromises);
|
|
158
|
+
const localStorageByOrigin = results.filter(Boolean) as LocalStorageEntry[];
|
|
159
|
+
|
|
160
|
+
if (localStorageByOrigin.length > 0) {
|
|
161
|
+
data.localStorage = localStorageByOrigin;
|
|
162
|
+
}
|
|
163
|
+
} catch (err) {
|
|
164
|
+
// Fallback: If CDP approach fails, use current page only
|
|
165
|
+
logger.warn({ message: 'Failed to extract localStorage via CDP, falling back to current page', error: err });
|
|
166
|
+
try {
|
|
167
|
+
const currentUrl = page.url();
|
|
168
|
+
const localStorageData = await extractLocalStorageFromPage(page);
|
|
169
|
+
|
|
170
|
+
if (Object.keys(localStorageData).length > 0) {
|
|
171
|
+
data.localStorage = [{
|
|
172
|
+
url: currentUrl,
|
|
173
|
+
data: localStorageData
|
|
174
|
+
}];
|
|
175
|
+
}
|
|
176
|
+
} catch (fallbackErr) {
|
|
177
|
+
logger.warn({ message: 'Failed to extract localStorage via fallback method', error: fallbackErr });
|
|
178
|
+
}
|
|
179
|
+
} finally {
|
|
180
|
+
if (client) {
|
|
181
|
+
await client.detach().catch(() => {});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (Object.keys(data).length === 0) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Add key to data for filtering
|
|
190
|
+
data.key = BROWSER_SESSION_KEY;
|
|
191
|
+
|
|
192
|
+
// Save with the actual sessionId, not a constant key
|
|
193
|
+
// TTL: 10 days (in seconds)
|
|
194
|
+
const TTL_10_DAYS = 10 * 24 * 60 * 60;
|
|
195
|
+
try {
|
|
196
|
+
await saveRecord(sessionId, data, TTL_10_DAYS);
|
|
197
|
+
} catch (error) {
|
|
198
|
+
logger.warn({ message: 'Failed to save session data', sessionId, error });
|
|
199
|
+
}
|
|
200
|
+
} catch (error) {
|
|
201
|
+
logger.warn({ message: 'Failed to save session data', sessionId, error });
|
|
202
|
+
}
|
|
203
|
+
};
|
|
@@ -4,13 +4,10 @@ import { createBrowserSession, destroyBrowserSession } from '../browserTask/exec
|
|
|
4
4
|
import { logger } from '../utils/logger';
|
|
5
5
|
import * as fs from 'fs';
|
|
6
6
|
import * as path from 'path';
|
|
7
|
-
import { CookieStore } from '../browserTask/CookieStore';
|
|
8
7
|
import { awaitEmit, emit } from '../platform/mindedConnection';
|
|
9
8
|
import { mindedConnectionSocketMessageType } from '../platform/mindedConnectionTypes';
|
|
10
9
|
import { ProxyType } from '../types/Tools.types';
|
|
11
|
-
|
|
12
|
-
// Initialize cookie store
|
|
13
|
-
const cookieStore = new CookieStore(path.join(process.cwd(), 'local_storage', 'cookies'));
|
|
10
|
+
import { fetchSessionDataViaSocket, saveSessionDataViaSocket } from './browserSessionPersistence';
|
|
14
11
|
|
|
15
12
|
/**
|
|
16
13
|
* Strips unnecessary content from HTML to reduce file size
|
|
@@ -85,7 +82,6 @@ class ScreenshotCapture {
|
|
|
85
82
|
error,
|
|
86
83
|
});
|
|
87
84
|
});
|
|
88
|
-
|
|
89
85
|
} catch (error) {
|
|
90
86
|
logger.warn({
|
|
91
87
|
message: 'Failed to capture screenshot',
|
|
@@ -141,7 +137,7 @@ class LogsCapture {
|
|
|
141
137
|
try {
|
|
142
138
|
const timestamp = new Date().toISOString();
|
|
143
139
|
|
|
144
|
-
// Send
|
|
140
|
+
// Send full log history to backend - backend overwrites the file on each upload
|
|
145
141
|
await awaitEmit(mindedConnectionSocketMessageType.RPA_LOGS_UPLOAD, {
|
|
146
142
|
type: mindedConnectionSocketMessageType.RPA_LOGS_UPLOAD,
|
|
147
143
|
sessionId: this.sessionId,
|
|
@@ -216,13 +212,20 @@ const createProxyLocator = (
|
|
|
216
212
|
/**
|
|
217
213
|
* Create a proxy page that intercepts actions and captures screenshots and logs
|
|
218
214
|
*/
|
|
219
|
-
const createProxyPage = (
|
|
220
|
-
page: Page,
|
|
221
|
-
screenshotCapture: ScreenshotCapture,
|
|
222
|
-
logsCapture: LogsCapture,
|
|
223
|
-
): Page => {
|
|
215
|
+
const createProxyPage = (page: Page, screenshotCapture: ScreenshotCapture, logsCapture: LogsCapture): Page => {
|
|
224
216
|
// List of methods to intercept for screenshot capture and logging
|
|
225
|
-
const methodsToIntercept = [
|
|
217
|
+
const methodsToIntercept = [
|
|
218
|
+
'click',
|
|
219
|
+
'fill',
|
|
220
|
+
'type',
|
|
221
|
+
'goto',
|
|
222
|
+
'selectOption',
|
|
223
|
+
'check',
|
|
224
|
+
'uncheck',
|
|
225
|
+
'setInputFiles',
|
|
226
|
+
'waitForTimeout',
|
|
227
|
+
'evaluate',
|
|
228
|
+
];
|
|
226
229
|
// Methods that return locators and need special handling
|
|
227
230
|
const locatorMethods = ['locator'];
|
|
228
231
|
|
|
@@ -323,37 +326,6 @@ const formatLocatorActionLog = (action: string, selector: string | undefined, ar
|
|
|
323
326
|
}
|
|
324
327
|
};
|
|
325
328
|
|
|
326
|
-
// Helper function to load cookies for a session
|
|
327
|
-
const loadCookiesForSession = async (context: BrowserContext, sessionId: string): Promise<void> => {
|
|
328
|
-
const stored = await cookieStore.load();
|
|
329
|
-
if (!stored || stored.length === 0) {
|
|
330
|
-
logger.info({ message: 'No stored cookies to load', sessionId });
|
|
331
|
-
return;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
try {
|
|
335
|
-
await context.addCookies(stored);
|
|
336
|
-
logger.info({ message: 'Loaded cookies into browser session', sessionId, count: stored.length });
|
|
337
|
-
} catch (error) {
|
|
338
|
-
logger.warn({ message: 'Failed to add cookies to context', sessionId, error });
|
|
339
|
-
}
|
|
340
|
-
};
|
|
341
|
-
|
|
342
|
-
// Helper function to save cookies for a session
|
|
343
|
-
const saveCookiesForSession = async (context: BrowserContext, sessionId: string): Promise<void> => {
|
|
344
|
-
try {
|
|
345
|
-
const cookies = await context.cookies();
|
|
346
|
-
if (cookies && cookies.length > 0) {
|
|
347
|
-
await cookieStore.save(cookies);
|
|
348
|
-
logger.info({ message: 'Saved cookies from browser session', sessionId, count: cookies.length });
|
|
349
|
-
} else {
|
|
350
|
-
logger.info({ message: 'No cookies found in browser session', sessionId });
|
|
351
|
-
}
|
|
352
|
-
} catch (error) {
|
|
353
|
-
logger.warn({ message: 'Failed to get/save cookies from session', sessionId, error: JSON.stringify(error) });
|
|
354
|
-
}
|
|
355
|
-
};
|
|
356
|
-
|
|
357
329
|
export const withBrowserSession = async <T>(
|
|
358
330
|
{
|
|
359
331
|
sessionId,
|
|
@@ -365,6 +337,7 @@ export const withBrowserSession = async <T>(
|
|
|
365
337
|
proxyUser,
|
|
366
338
|
proxyPass,
|
|
367
339
|
toolName,
|
|
340
|
+
persistSession = true,
|
|
368
341
|
}: {
|
|
369
342
|
sessionId: string;
|
|
370
343
|
browserTaskMode: BrowserTaskMode;
|
|
@@ -375,10 +348,19 @@ export const withBrowserSession = async <T>(
|
|
|
375
348
|
proxyUser?: string;
|
|
376
349
|
proxyPass?: string;
|
|
377
350
|
toolName: string;
|
|
351
|
+
persistSession?: boolean;
|
|
378
352
|
},
|
|
379
353
|
callback: (page: Page) => Promise<T>,
|
|
380
354
|
): Promise<{ result: T }> => {
|
|
381
|
-
const { cdpUrl } = await createBrowserSession({
|
|
355
|
+
const { cdpUrl } = await createBrowserSession({
|
|
356
|
+
sessionId,
|
|
357
|
+
browserTaskMode,
|
|
358
|
+
proxyCountryCode,
|
|
359
|
+
proxyType,
|
|
360
|
+
proxyServer,
|
|
361
|
+
proxyUser,
|
|
362
|
+
proxyPass,
|
|
363
|
+
});
|
|
382
364
|
if (!cdpUrl) {
|
|
383
365
|
throw new Error('CDP URL was not provided by the browser session.');
|
|
384
366
|
}
|
|
@@ -409,16 +391,52 @@ export const withBrowserSession = async <T>(
|
|
|
409
391
|
const logsCapture = new LogsCapture(sessionId, toolCallId);
|
|
410
392
|
page = createProxyPage(page, screenshotCapture, logsCapture);
|
|
411
393
|
|
|
412
|
-
// Load cookies
|
|
413
|
-
|
|
414
|
-
if (browserTaskMode === BrowserTaskMode.LOCAL && !noCookies) {
|
|
394
|
+
// Load session data (cookies + localStorage) once at startup (if persistence is enabled)
|
|
395
|
+
if (persistSession) {
|
|
415
396
|
try {
|
|
416
|
-
await
|
|
397
|
+
const { cookies, localStorage: localStorageEntries } = await fetchSessionDataViaSocket();
|
|
398
|
+
|
|
399
|
+
// Load cookies into browser context
|
|
400
|
+
if (cookies && cookies.length > 0) {
|
|
401
|
+
await context.addCookies(cookies);
|
|
402
|
+
logger.info({ message: 'Loaded cookies into browser session', sessionId, count: cookies.length });
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Inject localStorage for ALL origins using addInitScript
|
|
406
|
+
// This runs on EVERY page load, but only sets items that don't already exist
|
|
407
|
+
// This prevents overwriting user changes during the session while restoring stored data
|
|
408
|
+
if (localStorageEntries && localStorageEntries.length > 0) {
|
|
409
|
+
const localStorageByOrigin = localStorageEntries.reduce(
|
|
410
|
+
(acc, entry) => {
|
|
411
|
+
try {
|
|
412
|
+
const origin = new URL(entry.url).origin;
|
|
413
|
+
acc[origin] = entry.data;
|
|
414
|
+
} catch (error) {
|
|
415
|
+
logger.warn({ message: 'Failed to parse origin for localStorage', url: entry.url, error });
|
|
416
|
+
}
|
|
417
|
+
return acc;
|
|
418
|
+
},
|
|
419
|
+
{} as Record<string, Record<string, string>>,
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
await context.addInitScript((storageData: Record<string, Record<string, string>>) => {
|
|
423
|
+
const currentOrigin = window.location.origin;
|
|
424
|
+
const dataForOrigin = storageData[currentOrigin];
|
|
425
|
+
|
|
426
|
+
if (dataForOrigin) {
|
|
427
|
+
// Only set items that don't already exist (non-destructive merge)
|
|
428
|
+
// This preserves any changes made by the user during the current session
|
|
429
|
+
for (const [key, value] of Object.entries(dataForOrigin)) {
|
|
430
|
+
if (localStorage.getItem(key) === null) {
|
|
431
|
+
localStorage.setItem(key, value);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}, localStorageByOrigin);
|
|
436
|
+
}
|
|
417
437
|
} catch (error) {
|
|
418
|
-
logger.warn({ message: 'Failed to load
|
|
438
|
+
logger.warn({ message: 'Failed to load session data', sessionId, error });
|
|
419
439
|
}
|
|
420
|
-
} else if (noCookies) {
|
|
421
|
-
logger.info({ message: 'Cookies disabled via cookies=false flag', sessionId });
|
|
422
440
|
}
|
|
423
441
|
|
|
424
442
|
// Configure download behavior via CDP
|
|
@@ -532,12 +550,12 @@ export const withBrowserSession = async <T>(
|
|
|
532
550
|
}
|
|
533
551
|
}
|
|
534
552
|
|
|
535
|
-
// Save cookies before destroying session (
|
|
536
|
-
if (
|
|
553
|
+
// Save cookies and localStorage before destroying session (if persistence is enabled)
|
|
554
|
+
if (persistSession) {
|
|
537
555
|
try {
|
|
538
|
-
await
|
|
556
|
+
await saveSessionDataViaSocket(context, page, sessionId);
|
|
539
557
|
} catch (error) {
|
|
540
|
-
logger.warn({ message: 'Failed to save
|
|
558
|
+
logger.warn({ message: 'Failed to save session data', sessionId, error });
|
|
541
559
|
}
|
|
542
560
|
}
|
|
543
561
|
|
|
@@ -545,7 +563,7 @@ export const withBrowserSession = async <T>(
|
|
|
545
563
|
sessionId,
|
|
546
564
|
localRun: browserTaskMode === BrowserTaskMode.LOCAL,
|
|
547
565
|
onPrem: browserTaskMode === BrowserTaskMode.ON_PREM,
|
|
548
|
-
}).catch(() => {
|
|
566
|
+
}).catch(() => {});
|
|
549
567
|
}
|
|
550
568
|
};
|
|
551
569
|
|
package/src/types/Tools.types.ts
CHANGED
|
@@ -33,6 +33,7 @@ export interface RPATool<Input extends z.ZodSchema, Memory = any, Output extends
|
|
|
33
33
|
proxyUser?: string; // Custom proxy username - used when proxyType is CUSTOM
|
|
34
34
|
proxyPass?: string; // Custom proxy password - used when proxyType is CUSTOM
|
|
35
35
|
browserTaskMode?: BrowserTaskMode; // Optional browser provider (local/cloud/onPrem)
|
|
36
|
+
persistSession?: boolean; // Session persistence: true (global persistence with cloud storage, default), false (no persistence)
|
|
36
37
|
execute: (input: RPAToolExecuteInput<Input, Memory>) => Promise<ToolReturnType>;
|
|
37
38
|
}
|
|
38
39
|
|