@sparkleideas/browser 3.0.0-alpha.18
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/README.md +730 -0
- package/agents/architect.yaml +11 -0
- package/agents/coder.yaml +11 -0
- package/agents/reviewer.yaml +10 -0
- package/agents/security-architect.yaml +10 -0
- package/agents/tester.yaml +10 -0
- package/docker/Dockerfile +22 -0
- package/docker/docker-compose.yml +52 -0
- package/docker/test-fixtures/index.html +61 -0
- package/package.json +56 -0
- package/skills/browser/SKILL.md +204 -0
- package/src/agent/index.ts +35 -0
- package/src/application/browser-service.ts +570 -0
- package/src/domain/types.ts +324 -0
- package/src/index.ts +156 -0
- package/src/infrastructure/agent-browser-adapter.ts +654 -0
- package/src/infrastructure/hooks-integration.ts +170 -0
- package/src/infrastructure/memory-integration.ts +449 -0
- package/src/infrastructure/reasoningbank-adapter.ts +282 -0
- package/src/infrastructure/security-integration.ts +528 -0
- package/src/infrastructure/workflow-templates.ts +479 -0
- package/src/mcp-tools/browser-tools.ts +1210 -0
- package/src/mcp-tools/index.ts +6 -0
- package/src/skill/index.ts +24 -0
- package/tests/agent-browser-adapter.test.ts +328 -0
- package/tests/browser-service.test.ts +137 -0
- package/tests/e2e/browser-e2e.test.ts +175 -0
- package/tests/memory-integration.test.ts +277 -0
- package/tests/reasoningbank-adapter.test.ts +219 -0
- package/tests/security-integration.test.ts +194 -0
- package/tests/workflow-templates.test.ts +231 -0
- package/tmp.json +0 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +15 -0
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sparkleideas/browser - Browser Service
|
|
3
|
+
* Core application service integrating agent-browser with agentic-flow
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { AgentBrowserAdapter } from '../infrastructure/agent-browser-adapter.js';
|
|
7
|
+
import { createMemoryManager, type BrowserMemoryManager } from '../infrastructure/memory-integration.js';
|
|
8
|
+
import { getSecurityScanner, type BrowserSecurityScanner, type ThreatScanResult } from '../infrastructure/security-integration.js';
|
|
9
|
+
import type {
|
|
10
|
+
Snapshot,
|
|
11
|
+
SnapshotOptions,
|
|
12
|
+
ActionResult,
|
|
13
|
+
BrowserSession,
|
|
14
|
+
BrowserTrajectory,
|
|
15
|
+
BrowserTrajectoryStep,
|
|
16
|
+
BrowserSwarmConfig,
|
|
17
|
+
BrowserAgentConfig,
|
|
18
|
+
} from '../domain/types.js';
|
|
19
|
+
|
|
20
|
+
// ============================================================================
|
|
21
|
+
// Trajectory Tracking for ReasoningBank Integration
|
|
22
|
+
// ============================================================================
|
|
23
|
+
|
|
24
|
+
interface TrajectoryTracker {
|
|
25
|
+
id: string;
|
|
26
|
+
sessionId: string;
|
|
27
|
+
goal: string;
|
|
28
|
+
steps: BrowserTrajectoryStep[];
|
|
29
|
+
startedAt: string;
|
|
30
|
+
lastSnapshot?: Snapshot;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const activeTrajectories = new Map<string, TrajectoryTracker>();
|
|
34
|
+
|
|
35
|
+
// ============================================================================
|
|
36
|
+
// Browser Service Class
|
|
37
|
+
// ============================================================================
|
|
38
|
+
|
|
39
|
+
export interface BrowserServiceConfig extends Partial<BrowserAgentConfig> {
|
|
40
|
+
enableMemory?: boolean;
|
|
41
|
+
enableSecurity?: boolean;
|
|
42
|
+
requireHttps?: boolean;
|
|
43
|
+
blockedDomains?: string[];
|
|
44
|
+
allowedDomains?: string[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class BrowserService {
|
|
48
|
+
private adapter: AgentBrowserAdapter;
|
|
49
|
+
private sessionId: string;
|
|
50
|
+
private currentTrajectory?: string;
|
|
51
|
+
private snapshots: Map<string, Snapshot> = new Map();
|
|
52
|
+
private memoryManager?: BrowserMemoryManager;
|
|
53
|
+
private securityScanner?: BrowserSecurityScanner;
|
|
54
|
+
private config: BrowserServiceConfig;
|
|
55
|
+
|
|
56
|
+
constructor(config: BrowserServiceConfig = {}) {
|
|
57
|
+
this.config = config;
|
|
58
|
+
this.sessionId = config.sessionId || `browser-${Date.now()}`;
|
|
59
|
+
this.adapter = new AgentBrowserAdapter({
|
|
60
|
+
session: this.sessionId,
|
|
61
|
+
timeout: config.defaultTimeout || 30000,
|
|
62
|
+
headless: config.headless !== false,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Initialize memory integration if enabled (default: true)
|
|
66
|
+
if (config.enableMemory !== false) {
|
|
67
|
+
this.memoryManager = createMemoryManager(this.sessionId);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Initialize security scanning if enabled (default: true)
|
|
71
|
+
if (config.enableSecurity !== false) {
|
|
72
|
+
this.securityScanner = getSecurityScanner({
|
|
73
|
+
requireHttps: config.requireHttps,
|
|
74
|
+
blockedDomains: config.blockedDomains,
|
|
75
|
+
allowedDomains: config.allowedDomains,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ===========================================================================
|
|
81
|
+
// Trajectory Management (for ReasoningBank/SONA learning)
|
|
82
|
+
// ===========================================================================
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Start a new trajectory for learning
|
|
86
|
+
*/
|
|
87
|
+
startTrajectory(goal: string): string {
|
|
88
|
+
const id = `traj-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
89
|
+
activeTrajectories.set(id, {
|
|
90
|
+
id,
|
|
91
|
+
sessionId: this.sessionId,
|
|
92
|
+
goal,
|
|
93
|
+
steps: [],
|
|
94
|
+
startedAt: new Date().toISOString(),
|
|
95
|
+
});
|
|
96
|
+
this.currentTrajectory = id;
|
|
97
|
+
return id;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Record a step in the current trajectory
|
|
102
|
+
*/
|
|
103
|
+
private recordStep(action: string, input: Record<string, unknown> | object, result: ActionResult): void {
|
|
104
|
+
if (!this.currentTrajectory) return;
|
|
105
|
+
|
|
106
|
+
const trajectory = activeTrajectories.get(this.currentTrajectory);
|
|
107
|
+
if (!trajectory) return;
|
|
108
|
+
|
|
109
|
+
trajectory.steps.push({
|
|
110
|
+
action,
|
|
111
|
+
input: input as Record<string, unknown>,
|
|
112
|
+
result,
|
|
113
|
+
snapshot: trajectory.lastSnapshot,
|
|
114
|
+
timestamp: new Date().toISOString(),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* End trajectory and return for learning (also stores in memory)
|
|
120
|
+
*/
|
|
121
|
+
async endTrajectory(success: boolean, verdict?: string): Promise<BrowserTrajectory | null> {
|
|
122
|
+
if (!this.currentTrajectory) return null;
|
|
123
|
+
|
|
124
|
+
const trajectory = activeTrajectories.get(this.currentTrajectory);
|
|
125
|
+
if (!trajectory) return null;
|
|
126
|
+
|
|
127
|
+
const completed: BrowserTrajectory = {
|
|
128
|
+
...trajectory,
|
|
129
|
+
completedAt: new Date().toISOString(),
|
|
130
|
+
success,
|
|
131
|
+
verdict,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// Store in memory for learning
|
|
135
|
+
if (this.memoryManager) {
|
|
136
|
+
await this.memoryManager.storeTrajectory(completed);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
activeTrajectories.delete(this.currentTrajectory);
|
|
140
|
+
this.currentTrajectory = undefined;
|
|
141
|
+
|
|
142
|
+
return completed;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Get current trajectory for inspection
|
|
147
|
+
*/
|
|
148
|
+
getCurrentTrajectory(): TrajectoryTracker | null {
|
|
149
|
+
if (!this.currentTrajectory) return null;
|
|
150
|
+
return activeTrajectories.get(this.currentTrajectory) || null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ===========================================================================
|
|
154
|
+
// Core Browser Operations
|
|
155
|
+
// ===========================================================================
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Navigate to URL with trajectory tracking and security scanning
|
|
159
|
+
*/
|
|
160
|
+
async open(url: string, options?: { waitUntil?: 'load' | 'domcontentloaded' | 'networkidle'; headers?: Record<string, string>; skipSecurityCheck?: boolean }): Promise<ActionResult> {
|
|
161
|
+
// Security check before navigation
|
|
162
|
+
if (this.securityScanner && !options?.skipSecurityCheck) {
|
|
163
|
+
const scanResult = await this.securityScanner.scanUrl(url);
|
|
164
|
+
if (!scanResult.safe) {
|
|
165
|
+
const threats = scanResult.threats.map(t => `${t.type}: ${t.description}`).join('; ');
|
|
166
|
+
return {
|
|
167
|
+
success: false,
|
|
168
|
+
error: `Security scan failed: ${threats}`,
|
|
169
|
+
data: { scanResult },
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const result = await this.adapter.open({
|
|
175
|
+
url,
|
|
176
|
+
waitUntil: options?.waitUntil,
|
|
177
|
+
headers: options?.headers,
|
|
178
|
+
});
|
|
179
|
+
this.recordStep('open', { url, ...options }, result);
|
|
180
|
+
return result;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Scan URL for security threats without navigating
|
|
185
|
+
*/
|
|
186
|
+
async scanUrl(url: string): Promise<ThreatScanResult> {
|
|
187
|
+
if (!this.securityScanner) {
|
|
188
|
+
return { safe: true, threats: [], pii: [], score: 1, scanDuration: 0 };
|
|
189
|
+
}
|
|
190
|
+
return this.securityScanner.scanUrl(url);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Get snapshot with automatic caching
|
|
195
|
+
*/
|
|
196
|
+
async snapshot(options: SnapshotOptions = {}): Promise<ActionResult<Snapshot>> {
|
|
197
|
+
const result = await this.adapter.snapshot({
|
|
198
|
+
interactive: options.interactive !== false,
|
|
199
|
+
compact: options.compact !== false,
|
|
200
|
+
...options,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
if (result.success && result.data) {
|
|
204
|
+
// Cache snapshot and update trajectory
|
|
205
|
+
const snapshot = result.data as Snapshot;
|
|
206
|
+
this.snapshots.set('latest', snapshot);
|
|
207
|
+
|
|
208
|
+
if (this.currentTrajectory) {
|
|
209
|
+
const trajectory = activeTrajectories.get(this.currentTrajectory);
|
|
210
|
+
if (trajectory) {
|
|
211
|
+
trajectory.lastSnapshot = snapshot;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
this.recordStep('snapshot', options, result);
|
|
217
|
+
return result as ActionResult<Snapshot>;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Click with trajectory tracking
|
|
222
|
+
*/
|
|
223
|
+
async click(target: string, options?: { button?: 'left' | 'right' | 'middle'; force?: boolean }): Promise<ActionResult> {
|
|
224
|
+
const result = await this.adapter.click({
|
|
225
|
+
target,
|
|
226
|
+
...options,
|
|
227
|
+
});
|
|
228
|
+
this.recordStep('click', { target, ...options }, result);
|
|
229
|
+
return result;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Fill input with trajectory tracking and PII scanning
|
|
234
|
+
*/
|
|
235
|
+
async fill(target: string, value: string, options?: { force?: boolean; skipPIICheck?: boolean }): Promise<ActionResult> {
|
|
236
|
+
// Check for PII in the value
|
|
237
|
+
if (this.securityScanner && !options?.skipPIICheck) {
|
|
238
|
+
const scanResult = this.securityScanner.scanContent(value, target);
|
|
239
|
+
if (scanResult.pii.length > 0) {
|
|
240
|
+
// Log masked values for security
|
|
241
|
+
const maskedPII = scanResult.pii.map(p => `${p.type}: ${p.masked}`).join(', ');
|
|
242
|
+
console.log(`[security] PII detected in form field ${target}: ${maskedPII}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const result = await this.adapter.fill({
|
|
247
|
+
target,
|
|
248
|
+
value,
|
|
249
|
+
...options,
|
|
250
|
+
});
|
|
251
|
+
this.recordStep('fill', { target, value: this.securityScanner ? '[REDACTED]' : value, ...options }, result);
|
|
252
|
+
return result;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Check if content contains PII
|
|
257
|
+
*/
|
|
258
|
+
scanForPII(content: string, context?: string): ThreatScanResult {
|
|
259
|
+
if (!this.securityScanner) {
|
|
260
|
+
return { safe: true, threats: [], pii: [], score: 1, scanDuration: 0 };
|
|
261
|
+
}
|
|
262
|
+
return this.securityScanner.scanContent(content, context);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Type text with trajectory tracking
|
|
267
|
+
*/
|
|
268
|
+
async type(target: string, text: string, options?: { delay?: number }): Promise<ActionResult> {
|
|
269
|
+
const result = await this.adapter.type({
|
|
270
|
+
target,
|
|
271
|
+
text,
|
|
272
|
+
...options,
|
|
273
|
+
});
|
|
274
|
+
this.recordStep('type', { target, text, ...options }, result);
|
|
275
|
+
return result;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Press key with trajectory tracking
|
|
280
|
+
*/
|
|
281
|
+
async press(key: string, delay?: number): Promise<ActionResult> {
|
|
282
|
+
const result = await this.adapter.press(key, delay);
|
|
283
|
+
this.recordStep('press', { key, delay }, result);
|
|
284
|
+
return result;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Wait for condition
|
|
289
|
+
*/
|
|
290
|
+
async wait(options: { selector?: string; timeout?: number; text?: string; url?: string; load?: 'load' | 'domcontentloaded' | 'networkidle'; fn?: string }): Promise<ActionResult> {
|
|
291
|
+
const result = await this.adapter.wait(options);
|
|
292
|
+
this.recordStep('wait', options, result);
|
|
293
|
+
return result;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Get element text
|
|
298
|
+
*/
|
|
299
|
+
async getText(target: string): Promise<ActionResult<string>> {
|
|
300
|
+
const result = await this.adapter.getText(target);
|
|
301
|
+
this.recordStep('getText', { target }, result);
|
|
302
|
+
return result;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Execute JavaScript
|
|
307
|
+
*/
|
|
308
|
+
async eval<T = unknown>(script: string): Promise<ActionResult<T>> {
|
|
309
|
+
const result = await this.adapter.eval<T>({ script });
|
|
310
|
+
this.recordStep('eval', { script }, result);
|
|
311
|
+
return result;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Take screenshot
|
|
316
|
+
*/
|
|
317
|
+
async screenshot(options?: { path?: string; fullPage?: boolean }): Promise<ActionResult<string>> {
|
|
318
|
+
const result = await this.adapter.screenshot(options || {});
|
|
319
|
+
this.recordStep('screenshot', options || {}, result);
|
|
320
|
+
return result;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Close browser
|
|
325
|
+
*/
|
|
326
|
+
async close(): Promise<ActionResult> {
|
|
327
|
+
const result = await this.adapter.close();
|
|
328
|
+
this.recordStep('close', {}, result);
|
|
329
|
+
return result;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// ===========================================================================
|
|
333
|
+
// High-Level Workflow Operations
|
|
334
|
+
// ===========================================================================
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Authenticate using header injection (skips login UI)
|
|
338
|
+
*/
|
|
339
|
+
async authenticateWithHeaders(url: string, headers: Record<string, string>): Promise<ActionResult> {
|
|
340
|
+
return this.open(url, { headers });
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Fill and submit a form
|
|
345
|
+
*/
|
|
346
|
+
async submitForm(fields: Array<{ target: string; value: string }>, submitButton: string): Promise<ActionResult> {
|
|
347
|
+
// Fill all fields
|
|
348
|
+
for (const field of fields) {
|
|
349
|
+
const result = await this.fill(field.target, field.value);
|
|
350
|
+
if (!result.success) return result;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Click submit
|
|
354
|
+
return this.click(submitButton);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Extract data using snapshot refs
|
|
359
|
+
*/
|
|
360
|
+
async extractData(refs: string[]): Promise<Record<string, string>> {
|
|
361
|
+
const data: Record<string, string> = {};
|
|
362
|
+
for (const ref of refs) {
|
|
363
|
+
const result = await this.getText(ref);
|
|
364
|
+
if (result.success && result.data) {
|
|
365
|
+
data[ref] = result.data;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return data;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Navigate and wait for specific element
|
|
373
|
+
*/
|
|
374
|
+
async navigateAndWait(url: string, selector: string, timeout?: number): Promise<ActionResult> {
|
|
375
|
+
const navResult = await this.open(url);
|
|
376
|
+
if (!navResult.success) return navResult;
|
|
377
|
+
|
|
378
|
+
return this.wait({ selector, timeout });
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// ===========================================================================
|
|
382
|
+
// Session State
|
|
383
|
+
// ===========================================================================
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Get cached snapshot
|
|
387
|
+
*/
|
|
388
|
+
getLatestSnapshot(): Snapshot | null {
|
|
389
|
+
return this.snapshots.get('latest') || null;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Get session ID
|
|
394
|
+
*/
|
|
395
|
+
getSessionId(): string {
|
|
396
|
+
return this.sessionId;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Get underlying adapter for advanced operations
|
|
401
|
+
*/
|
|
402
|
+
getAdapter(): AgentBrowserAdapter {
|
|
403
|
+
return this.adapter;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Get memory manager for direct memory operations
|
|
408
|
+
*/
|
|
409
|
+
getMemoryManager(): BrowserMemoryManager | undefined {
|
|
410
|
+
return this.memoryManager;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Get security scanner for direct security operations
|
|
415
|
+
*/
|
|
416
|
+
getSecurityScanner(): BrowserSecurityScanner | undefined {
|
|
417
|
+
return this.securityScanner;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Find similar trajectories for a goal (uses HNSW search)
|
|
422
|
+
*/
|
|
423
|
+
async findSimilarTrajectories(goal: string, topK = 5): Promise<BrowserTrajectory[]> {
|
|
424
|
+
if (!this.memoryManager) return [];
|
|
425
|
+
return this.memoryManager.findSimilarTrajectories(goal, topK);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Get session memory statistics
|
|
430
|
+
*/
|
|
431
|
+
async getMemoryStats(): Promise<{
|
|
432
|
+
trajectories: number;
|
|
433
|
+
patterns: number;
|
|
434
|
+
snapshots: number;
|
|
435
|
+
errors: number;
|
|
436
|
+
successRate: number;
|
|
437
|
+
} | null> {
|
|
438
|
+
if (!this.memoryManager) return null;
|
|
439
|
+
return this.memoryManager.getSessionStats();
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// ============================================================================
|
|
444
|
+
// Browser Swarm Coordinator
|
|
445
|
+
// ============================================================================
|
|
446
|
+
|
|
447
|
+
export class BrowserSwarmCoordinator {
|
|
448
|
+
private config: BrowserSwarmConfig;
|
|
449
|
+
private services: Map<string, BrowserService> = new Map();
|
|
450
|
+
private sharedData: Map<string, unknown> = new Map();
|
|
451
|
+
|
|
452
|
+
constructor(config: BrowserSwarmConfig) {
|
|
453
|
+
this.config = config;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Spawn a new browser agent in the swarm
|
|
458
|
+
*/
|
|
459
|
+
async spawnAgent(role: 'navigator' | 'scraper' | 'validator' | 'tester' | 'monitor'): Promise<BrowserService> {
|
|
460
|
+
if (this.services.size >= this.config.maxSessions) {
|
|
461
|
+
throw new Error(`Max sessions (${this.config.maxSessions}) reached`);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const sessionId = `${this.config.sessionPrefix}-${role}-${Date.now()}`;
|
|
465
|
+
const service = new BrowserService({
|
|
466
|
+
sessionId,
|
|
467
|
+
role,
|
|
468
|
+
capabilities: this.getCapabilitiesForRole(role),
|
|
469
|
+
defaultTimeout: 30000,
|
|
470
|
+
headless: true,
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
this.services.set(sessionId, service);
|
|
474
|
+
return service;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Get capabilities for a role
|
|
479
|
+
*/
|
|
480
|
+
private getCapabilitiesForRole(role: string): string[] {
|
|
481
|
+
switch (role) {
|
|
482
|
+
case 'navigator':
|
|
483
|
+
return ['navigation', 'authentication', 'session-management'];
|
|
484
|
+
case 'scraper':
|
|
485
|
+
return ['snapshot', 'extraction', 'pagination'];
|
|
486
|
+
case 'validator':
|
|
487
|
+
return ['assertions', 'state-checks', 'screenshots'];
|
|
488
|
+
case 'tester':
|
|
489
|
+
return ['forms', 'interactions', 'assertions'];
|
|
490
|
+
case 'monitor':
|
|
491
|
+
return ['network', 'console', 'errors'];
|
|
492
|
+
default:
|
|
493
|
+
return [];
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Share data between agents
|
|
499
|
+
*/
|
|
500
|
+
shareData(key: string, value: unknown): void {
|
|
501
|
+
this.sharedData.set(key, value);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Get shared data
|
|
506
|
+
*/
|
|
507
|
+
getSharedData<T>(key: string): T | undefined {
|
|
508
|
+
return this.sharedData.get(key) as T | undefined;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Get all active sessions
|
|
513
|
+
*/
|
|
514
|
+
getSessions(): string[] {
|
|
515
|
+
return Array.from(this.services.keys());
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Get a specific service
|
|
520
|
+
*/
|
|
521
|
+
getService(sessionId: string): BrowserService | undefined {
|
|
522
|
+
return this.services.get(sessionId);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Close all sessions
|
|
527
|
+
*/
|
|
528
|
+
async closeAll(): Promise<void> {
|
|
529
|
+
const closePromises = Array.from(this.services.values()).map(s => s.close());
|
|
530
|
+
await Promise.all(closePromises);
|
|
531
|
+
this.services.clear();
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Get coordinator stats
|
|
536
|
+
*/
|
|
537
|
+
getStats(): { activeSessions: number; maxSessions: number; topology: string } {
|
|
538
|
+
return {
|
|
539
|
+
activeSessions: this.services.size,
|
|
540
|
+
maxSessions: this.config.maxSessions,
|
|
541
|
+
topology: this.config.topology,
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// ============================================================================
|
|
547
|
+
// Factory Functions
|
|
548
|
+
// ============================================================================
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Create a standalone browser service
|
|
552
|
+
*/
|
|
553
|
+
export function createBrowserService(options?: Partial<BrowserAgentConfig>): BrowserService {
|
|
554
|
+
return new BrowserService(options);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Create a browser swarm coordinator
|
|
559
|
+
*/
|
|
560
|
+
export function createBrowserSwarm(config?: Partial<BrowserSwarmConfig>): BrowserSwarmCoordinator {
|
|
561
|
+
return new BrowserSwarmCoordinator({
|
|
562
|
+
topology: config?.topology || 'hierarchical',
|
|
563
|
+
maxSessions: config?.maxSessions || 5,
|
|
564
|
+
sessionPrefix: config?.sessionPrefix || 'swarm',
|
|
565
|
+
sharedCookies: config?.sharedCookies,
|
|
566
|
+
coordinatorSession: config?.coordinatorSession,
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
export default BrowserService;
|