@minded-ai/mindedjs 3.1.21 → 3.1.23
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/cli/validateAgent.d.ts.map +1 -1
- package/dist/cli/validateAgent.js +15 -0
- package/dist/cli/validateAgent.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/Flows.types.d.ts +0 -5
- package/dist/types/Flows.types.d.ts.map +1 -1
- package/dist/types/Flows.types.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 +1 -1
- package/src/agent.ts +0 -5
- package/src/browserTask/executeBrowserTask.ts +1 -73
- package/src/cli/validateAgent.ts +17 -0
- 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/Flows.types.ts +0 -5
- package/src/types/Tools.types.ts +1 -0
|
@@ -7,10 +7,16 @@ Tools are reusable functions that agents can call to perform specific actions li
|
|
|
7
7
|
Every tool must implement the `Tool` interface:
|
|
8
8
|
|
|
9
9
|
```ts
|
|
10
|
-
|
|
10
|
+
// Example: Tool with input and optional output schemas
|
|
11
|
+
interface Tool<
|
|
12
|
+
Input extends z.ZodSchema,
|
|
13
|
+
Memory = any,
|
|
14
|
+
Output extends z.ZodSchema = z.ZodTypeAny
|
|
15
|
+
> {
|
|
11
16
|
name: string; // Unique tool identifier
|
|
12
17
|
description: string; // What the tool does (used by LLM)
|
|
13
18
|
input: Input; // Zod schema for input validation
|
|
19
|
+
output?: Output; // Optional: Zod schema for output validation (e.g., outputSchema)
|
|
14
20
|
isGlobal?: boolean; // Optional: available across all LLM calls
|
|
15
21
|
execute: ({ input, state, agent }) => Promise<{ result? }>;
|
|
16
22
|
}
|
|
@@ -140,6 +146,85 @@ const updateOrderSchema = z.object({
|
|
|
140
146
|
});
|
|
141
147
|
```
|
|
142
148
|
|
|
149
|
+
## Output Schema Configuration
|
|
150
|
+
|
|
151
|
+
Tools can optionally define an output schema to validate and structure their return values. This provides type safety and clear documentation of what the tool returns.
|
|
152
|
+
|
|
153
|
+
### Defining Output Schema
|
|
154
|
+
|
|
155
|
+
**Important**: The output schema variable must be named `outputSchema` for the platform to parse it correctly.
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
import { z } from 'zod';
|
|
159
|
+
import { Tool } from '@minded-ai/mindedjs';
|
|
160
|
+
import memorySchema from '../schema';
|
|
161
|
+
|
|
162
|
+
type Memory = z.infer<typeof memorySchema>;
|
|
163
|
+
|
|
164
|
+
const inputSchema = z.object({
|
|
165
|
+
email: z.string().describe('User email address'),
|
|
166
|
+
password: z.string().describe('User password'),
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Must be named 'outputSchema'
|
|
170
|
+
const outputSchema = z.object({
|
|
171
|
+
success: z.boolean().describe('Status of the login operation'),
|
|
172
|
+
message: z.string().describe('Login status message'),
|
|
173
|
+
url: z.string().describe('Dashboard URL after successful login'),
|
|
174
|
+
usedPersistedSession: z.boolean().describe('Whether a persisted session was used'),
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const loginTool: Tool<typeof inputSchema, Memory, typeof outputSchema> = {
|
|
178
|
+
name: 'login_to_dashboard',
|
|
179
|
+
description: 'Login to dashboard and return access details',
|
|
180
|
+
input: inputSchema,
|
|
181
|
+
output: outputSchema,
|
|
182
|
+
type: 'rpa',
|
|
183
|
+
execute: async ({ input, state, agent, page }): Promise<{ result: z.infer<typeof outputSchema> }> => {
|
|
184
|
+
// Tool logic here
|
|
185
|
+
const loginResult = await performLogin(page, input.email, input.password);
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
result: {
|
|
189
|
+
success: true,
|
|
190
|
+
message: 'Successfully logged in',
|
|
191
|
+
url: page.url(),
|
|
192
|
+
usedPersistedSession: false,
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
export default loginTool;
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Benefits of Output Schema
|
|
202
|
+
|
|
203
|
+
- **Type Safety**: TypeScript will enforce that your return value matches the output schema
|
|
204
|
+
- **Validation**: Zod validates the output at runtime to catch bugs early
|
|
205
|
+
- **Documentation**: Output schema serves as clear documentation of what the tool returns
|
|
206
|
+
- **LLM Context**: The LLM receives structured information about the tool's output format
|
|
207
|
+
|
|
208
|
+
### Output Schema vs Result
|
|
209
|
+
|
|
210
|
+
- **Without output schema**: Return any value in `result`, no validation
|
|
211
|
+
- **With output schema**: Return value must match the schema structure
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
// Without output schema - any result
|
|
215
|
+
return { result: 'Login successful' };
|
|
216
|
+
|
|
217
|
+
// With output schema - typed and validated
|
|
218
|
+
return {
|
|
219
|
+
result: {
|
|
220
|
+
success: true,
|
|
221
|
+
message: 'Login successful',
|
|
222
|
+
url: 'https://dashboard.example.com',
|
|
223
|
+
usedPersistedSession: false,
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
```
|
|
227
|
+
|
|
143
228
|
## Tool Registration
|
|
144
229
|
|
|
145
230
|
### Global Tools
|
package/package.json
CHANGED
package/src/agent.ts
CHANGED
|
@@ -1276,11 +1276,6 @@ export class Agent {
|
|
|
1276
1276
|
|
|
1277
1277
|
const { toolName, params, flags } = parseDirectToolExecutionArgs(toolArgs);
|
|
1278
1278
|
|
|
1279
|
-
// Set environment variable for cookies flag so browser session can read it
|
|
1280
|
-
if (flags.noCookies) {
|
|
1281
|
-
process.env.MINDED_NO_COOKIES = 'true';
|
|
1282
|
-
}
|
|
1283
|
-
|
|
1284
1279
|
logger.info({
|
|
1285
1280
|
message: '[runTool] Executing tool',
|
|
1286
1281
|
toolName,
|
|
@@ -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: [],
|
package/src/cli/validateAgent.ts
CHANGED
|
@@ -480,6 +480,23 @@ export async function runValidateCommand(agentPath?: string): Promise<void> {
|
|
|
480
480
|
process.exit(1);
|
|
481
481
|
}
|
|
482
482
|
|
|
483
|
+
// Check if mainFlow.yaml exists in any of the flows folders
|
|
484
|
+
let mainFlowFound = false;
|
|
485
|
+
for (const folder of flowsFolders) {
|
|
486
|
+
const resolvedFolder = path.isAbsolute(folder) ? folder : path.join(projectDir, folder);
|
|
487
|
+
const mainFlowPath = path.join(resolvedFolder, 'mainFlow.yaml');
|
|
488
|
+
if (fs.existsSync(mainFlowPath)) {
|
|
489
|
+
mainFlowFound = true;
|
|
490
|
+
break;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (!mainFlowFound) {
|
|
495
|
+
console.error('Error: mainFlow.yaml not found in any configured flows folder');
|
|
496
|
+
console.error('The flows directory must contain a mainFlow.yaml file');
|
|
497
|
+
process.exit(1);
|
|
498
|
+
}
|
|
499
|
+
|
|
483
500
|
console.log(`\nValidating ${flowFiles.length} flow(s)...\n`);
|
|
484
501
|
|
|
485
502
|
const graphCompilationErrors: string[] = [];
|
|
@@ -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[0];
|
|
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
|
+
};
|