hyper-agent-browser 0.1.0

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.
@@ -0,0 +1,562 @@
1
+ import * as actionCommands from "../commands/actions";
2
+ import * as advancedCommands from "../commands/advanced";
3
+ import * as getterCommands from "../commands/getters";
4
+ import * as infoCommands from "../commands/info";
5
+ import * as navigationCommands from "../commands/navigation";
6
+ import { SessionManager } from "../session/manager";
7
+ import { ReferenceStore } from "../snapshot/reference-store";
8
+ import { formatError } from "../utils/errors";
9
+ import { BrowserPool } from "./browser-pool";
10
+
11
+ export interface DaemonConfig {
12
+ port: number;
13
+ host: string;
14
+ }
15
+
16
+ export interface CommandRequest {
17
+ command: string;
18
+ session: string;
19
+ args: Record<string, any>;
20
+ options: {
21
+ headed?: boolean;
22
+ timeout?: number;
23
+ channel?: "chrome" | "msedge" | "chromium";
24
+ };
25
+ }
26
+
27
+ export interface CommandResponse {
28
+ success: boolean;
29
+ data?: any;
30
+ error?: string;
31
+ }
32
+
33
+ export class DaemonServer {
34
+ private server: any = null;
35
+ private browserPool: BrowserPool;
36
+ private sessionManager: SessionManager;
37
+ private referenceStores: Map<string, ReferenceStore> = new Map();
38
+
39
+ constructor(private config: DaemonConfig) {
40
+ this.browserPool = new BrowserPool();
41
+ this.sessionManager = new SessionManager();
42
+ }
43
+
44
+ async start(): Promise<void> {
45
+ const self = this;
46
+ this.server = Bun.serve({
47
+ port: this.config.port,
48
+ hostname: this.config.host,
49
+
50
+ async fetch(req: Request): Promise<Response> {
51
+ // CORS headers
52
+ const headers = {
53
+ "Access-Control-Allow-Origin": "*",
54
+ "Access-Control-Allow-Methods": "POST, GET, OPTIONS",
55
+ "Access-Control-Allow-Headers": "Content-Type",
56
+ "Content-Type": "application/json",
57
+ };
58
+
59
+ // Handle OPTIONS for CORS
60
+ if (req.method === "OPTIONS") {
61
+ return new Response(null, { headers });
62
+ }
63
+
64
+ const url = new URL(req.url);
65
+
66
+ // Health check
67
+ if (url.pathname === "/health") {
68
+ return Response.json(
69
+ { status: "ok", sessions: self.browserPool.getActiveSessions() },
70
+ { headers },
71
+ );
72
+ }
73
+
74
+ // Execute command
75
+ if (url.pathname === "/execute" && req.method === "POST") {
76
+ try {
77
+ const request: CommandRequest = await req.json();
78
+ const response = await self.executeCommand(request);
79
+ return Response.json(response, { headers });
80
+ } catch (error) {
81
+ return Response.json(
82
+ {
83
+ success: false,
84
+ error: error instanceof Error ? formatError(error) : String(error),
85
+ },
86
+ { status: 500, headers },
87
+ );
88
+ }
89
+ }
90
+
91
+ // Close session
92
+ if (url.pathname === "/close" && req.method === "POST") {
93
+ try {
94
+ const { session } = await req.json();
95
+ const closed = await self.browserPool.close(session);
96
+ await self.sessionManager.markStopped(session);
97
+ self.referenceStores.delete(session);
98
+
99
+ return Response.json({ success: true, closed }, { headers });
100
+ } catch (error) {
101
+ return Response.json(
102
+ {
103
+ success: false,
104
+ error: error instanceof Error ? error.message : String(error),
105
+ },
106
+ { status: 500, headers },
107
+ );
108
+ }
109
+ }
110
+
111
+ // List sessions
112
+ if (url.pathname === "/sessions" && req.method === "GET") {
113
+ const sessions = await self.sessionManager.list();
114
+ return Response.json({ success: true, sessions }, { headers });
115
+ }
116
+
117
+ return Response.json({ success: false, error: "Not found" }, { status: 404, headers });
118
+ },
119
+ });
120
+
121
+ console.log(`Daemon server started on ${this.config.host}:${this.config.port}`);
122
+ }
123
+
124
+ async stop(): Promise<void> {
125
+ if (this.server) {
126
+ this.server.stop();
127
+ this.server = null;
128
+ }
129
+
130
+ await this.browserPool.closeAll();
131
+ console.log("Daemon server stopped");
132
+ }
133
+
134
+ private async executeCommand(request: CommandRequest): Promise<CommandResponse> {
135
+ const { command, session: sessionName, args, options } = request;
136
+
137
+ try {
138
+ // Get or create session
139
+ const session = await this.sessionManager.getOrCreate(
140
+ sessionName,
141
+ options.channel || "chrome",
142
+ );
143
+
144
+ // Get browser from pool
145
+ const browser = await this.browserPool.get(session, {
146
+ headed: options.headed || false,
147
+ timeout: options.timeout || 30000,
148
+ channel: options.channel || session.channel,
149
+ });
150
+
151
+ // Get page
152
+ const page = await browser.getPage();
153
+
154
+ // Get or create reference store for this session
155
+ let referenceStore = this.referenceStores.get(sessionName);
156
+ if (!referenceStore) {
157
+ referenceStore = new ReferenceStore(session);
158
+ await referenceStore.load();
159
+ this.referenceStores.set(sessionName, referenceStore);
160
+
161
+ // Set reference store for actions and getters
162
+ actionCommands.setReferenceStore(referenceStore);
163
+ getterCommands.setReferenceStore(referenceStore);
164
+ }
165
+
166
+ // Show operation indicator if headed
167
+ if (options.headed) {
168
+ await browser.showOperationIndicator(command);
169
+ }
170
+
171
+ let result: any;
172
+
173
+ try {
174
+ // Execute command
175
+ switch (command) {
176
+ // Navigation commands
177
+ case "open":
178
+ await navigationCommands.open(page, args.url, {
179
+ waitUntil: args.waitUntil || "load",
180
+ timeout: options.timeout,
181
+ });
182
+ result = `Opened: ${args.url}`;
183
+ break;
184
+
185
+ case "reload":
186
+ await navigationCommands.reload(page);
187
+ result = "Page reloaded";
188
+ break;
189
+
190
+ case "back":
191
+ await navigationCommands.back(page);
192
+ result = "Navigated back";
193
+ break;
194
+
195
+ case "forward":
196
+ await navigationCommands.forward(page);
197
+ result = "Navigated forward";
198
+ break;
199
+
200
+ // Action commands
201
+ case "click":
202
+ await actionCommands.click(page, args.selector);
203
+ result = `Clicked: ${args.selector}`;
204
+ break;
205
+
206
+ case "fill":
207
+ await actionCommands.fill(page, args.selector, args.value);
208
+ result = `Filled: ${args.selector}`;
209
+ break;
210
+
211
+ case "type":
212
+ await actionCommands.type(page, args.selector, args.text, args.delay || 0);
213
+ result = `Typed into: ${args.selector}`;
214
+ break;
215
+
216
+ case "press":
217
+ await actionCommands.press(page, args.key);
218
+ result = `Pressed: ${args.key}`;
219
+ break;
220
+
221
+ case "scroll":
222
+ await actionCommands.scroll(page, args.direction, args.amount || 500, args.selector);
223
+ result = `Scrolled ${args.direction}`;
224
+ break;
225
+
226
+ case "hover":
227
+ await actionCommands.hover(page, args.selector);
228
+ result = `Hovered: ${args.selector}`;
229
+ break;
230
+
231
+ case "select":
232
+ await actionCommands.select(page, args.selector, args.value);
233
+ result = `Selected: ${args.value} in ${args.selector}`;
234
+ break;
235
+
236
+ case "wait": {
237
+ const conditionValue = /^\d+$/.test(args.condition)
238
+ ? Number.parseInt(args.condition)
239
+ : args.condition;
240
+ await actionCommands.wait(page, conditionValue, {
241
+ timeout: args.timeout ? Number.parseInt(args.timeout) : undefined,
242
+ });
243
+ result = "Wait completed";
244
+ break;
245
+ }
246
+
247
+ // 第一批新增 Action 命令
248
+ case "check":
249
+ await actionCommands.check(page, args.selector);
250
+ result = `Checked: ${args.selector}`;
251
+ break;
252
+
253
+ case "uncheck":
254
+ await actionCommands.uncheck(page, args.selector);
255
+ result = `Unchecked: ${args.selector}`;
256
+ break;
257
+
258
+ case "dblclick":
259
+ await actionCommands.dblclick(page, args.selector);
260
+ result = `Double-clicked: ${args.selector}`;
261
+ break;
262
+
263
+ case "focus":
264
+ await actionCommands.focus(page, args.selector);
265
+ result = `Focused: ${args.selector}`;
266
+ break;
267
+
268
+ case "upload":
269
+ await actionCommands.upload(page, args.selector, args.files);
270
+ result = `Uploaded files to: ${args.selector}`;
271
+ break;
272
+
273
+ case "scrollintoview":
274
+ await actionCommands.scrollIntoView(page, args.selector);
275
+ result = `Scrolled into view: ${args.selector}`;
276
+ break;
277
+
278
+ case "drag":
279
+ await actionCommands.drag(page, args.source, args.target);
280
+ result = `Dragged ${args.source} to ${args.target}`;
281
+ break;
282
+
283
+ // Get 系列命令
284
+ case "get":
285
+ if (!args.subcommand) {
286
+ throw new Error("Get subcommand required (text/value/attr/html/count/box)");
287
+ }
288
+
289
+ switch (args.subcommand) {
290
+ case "text":
291
+ result = await getterCommands.getText(page, args.selector);
292
+ break;
293
+ case "value":
294
+ result = await getterCommands.getValue(page, args.selector);
295
+ break;
296
+ case "attr":
297
+ result = await getterCommands.getAttr(page, args.selector, args.attribute);
298
+ break;
299
+ case "html":
300
+ result = await getterCommands.getHtml(page, args.selector);
301
+ break;
302
+ case "count":
303
+ result = await getterCommands.getCount(page, args.selector);
304
+ break;
305
+ case "box":
306
+ result = await getterCommands.getBox(page, args.selector);
307
+ break;
308
+ default:
309
+ throw new Error(`Unknown get subcommand: ${args.subcommand}`);
310
+ }
311
+ break;
312
+
313
+ // Is 系列命令
314
+ case "is":
315
+ if (!args.subcommand) {
316
+ throw new Error("Is subcommand required (visible/enabled/checked/editable/hidden)");
317
+ }
318
+
319
+ switch (args.subcommand) {
320
+ case "visible":
321
+ result = await getterCommands.isVisible(page, args.selector);
322
+ break;
323
+ case "enabled":
324
+ result = await getterCommands.isEnabled(page, args.selector);
325
+ break;
326
+ case "checked":
327
+ result = await getterCommands.isChecked(page, args.selector);
328
+ break;
329
+ case "editable":
330
+ result = await getterCommands.isEditable(page, args.selector);
331
+ break;
332
+ case "hidden":
333
+ result = await getterCommands.isHidden(page, args.selector);
334
+ break;
335
+ default:
336
+ throw new Error(`Unknown is subcommand: ${args.subcommand}`);
337
+ }
338
+ break;
339
+
340
+ // Info commands
341
+ case "snapshot":
342
+ result = await infoCommands.snapshot(page, {
343
+ interactive: args.interactive,
344
+ full: args.full,
345
+ raw: args.raw,
346
+ referenceStore: referenceStore,
347
+ });
348
+ break;
349
+
350
+ case "screenshot":
351
+ result = await infoCommands.screenshot(page, {
352
+ output: args.output,
353
+ fullPage: args.fullPage,
354
+ selector: args.selector,
355
+ });
356
+ break;
357
+
358
+ case "evaluate":
359
+ result = await infoCommands.evaluate(page, args.script);
360
+ break;
361
+
362
+ case "url":
363
+ result = await page.url();
364
+ break;
365
+
366
+ case "title":
367
+ result = await page.title();
368
+ break;
369
+
370
+ // Dialog 命令
371
+ case "dialog":
372
+ if (!args.action) {
373
+ throw new Error("Dialog action required (accept/dismiss)");
374
+ }
375
+ if (args.action === "accept") {
376
+ await advancedCommands.dialogAccept(page, args.text);
377
+ result = "Dialog handler set to accept";
378
+ } else if (args.action === "dismiss") {
379
+ await advancedCommands.dialogDismiss(page);
380
+ result = "Dialog handler set to dismiss";
381
+ }
382
+ break;
383
+
384
+ // Cookie 命令
385
+ case "cookies":
386
+ if (!args.action) {
387
+ result = await advancedCommands.getCookies(page);
388
+ } else if (args.action === "set") {
389
+ await advancedCommands.setCookie(page, args.name, args.value);
390
+ result = `Cookie set: ${args.name}`;
391
+ } else if (args.action === "clear") {
392
+ await advancedCommands.clearCookies(page);
393
+ result = "Cookies cleared";
394
+ }
395
+ break;
396
+
397
+ // Storage 命令
398
+ case "storage":
399
+ if (!args.storageType) {
400
+ throw new Error("Storage type required (local/session)");
401
+ }
402
+
403
+ if (args.storageType === "local") {
404
+ if (!args.action) {
405
+ result = await advancedCommands.getLocalStorage(page, args.key);
406
+ } else if (args.action === "set") {
407
+ await advancedCommands.setLocalStorage(page, args.key, args.value);
408
+ result = `LocalStorage set: ${args.key}`;
409
+ } else if (args.action === "clear") {
410
+ await advancedCommands.clearLocalStorage(page);
411
+ result = "LocalStorage cleared";
412
+ }
413
+ } else if (args.storageType === "session") {
414
+ if (!args.action) {
415
+ result = await advancedCommands.getSessionStorage(page, args.key);
416
+ } else if (args.action === "set") {
417
+ await advancedCommands.setSessionStorage(page, args.key, args.value);
418
+ result = `SessionStorage set: ${args.key}`;
419
+ } else if (args.action === "clear") {
420
+ await advancedCommands.clearSessionStorage(page);
421
+ result = "SessionStorage cleared";
422
+ }
423
+ }
424
+ break;
425
+
426
+ // PDF 导出
427
+ case "pdf":
428
+ result = await advancedCommands.savePDF(page, {
429
+ path: args.path,
430
+ format: args.format,
431
+ landscape: args.landscape,
432
+ printBackground: args.printBackground,
433
+ margin: args.margin,
434
+ });
435
+ break;
436
+
437
+ // Set 系列命令
438
+ case "set":
439
+ if (!args.subcommand) {
440
+ throw new Error("Set subcommand required (viewport/geo/offline/headers/media)");
441
+ }
442
+
443
+ switch (args.subcommand) {
444
+ case "viewport":
445
+ await advancedCommands.setViewport(page, args.width, args.height);
446
+ result = `Viewport set to ${args.width}x${args.height}`;
447
+ break;
448
+ case "geo":
449
+ await advancedCommands.setGeolocation(page, args.latitude, args.longitude);
450
+ result = `Geolocation set to ${args.latitude}, ${args.longitude}`;
451
+ break;
452
+ case "offline":
453
+ await advancedCommands.setOffline(page, args.enabled);
454
+ result = `Offline mode: ${args.enabled ? "enabled" : "disabled"}`;
455
+ break;
456
+ case "headers":
457
+ await advancedCommands.setExtraHeaders(page, args.headers);
458
+ result = "Extra headers set";
459
+ break;
460
+ case "media":
461
+ await advancedCommands.setMediaColorScheme(page, args.scheme);
462
+ result = `Color scheme set to ${args.scheme}`;
463
+ break;
464
+ default:
465
+ throw new Error(`Unknown set subcommand: ${args.subcommand}`);
466
+ }
467
+ break;
468
+
469
+ // Mouse 命令
470
+ case "mouse":
471
+ if (!args.action) {
472
+ throw new Error("Mouse action required (move/down/up/wheel)");
473
+ }
474
+
475
+ switch (args.action) {
476
+ case "move":
477
+ await advancedCommands.mouseMove(page, args.x, args.y);
478
+ result = `Mouse moved to ${args.x}, ${args.y}`;
479
+ break;
480
+ case "down":
481
+ await advancedCommands.mouseDown(page, args.button || "left");
482
+ result = `Mouse ${args.button || "left"} button down`;
483
+ break;
484
+ case "up":
485
+ await advancedCommands.mouseUp(page, args.button || "left");
486
+ result = `Mouse ${args.button || "left"} button up`;
487
+ break;
488
+ case "wheel":
489
+ await advancedCommands.mouseWheel(page, args.deltaY, args.deltaX || 0);
490
+ result = "Mouse wheel scrolled";
491
+ break;
492
+ default:
493
+ throw new Error(`Unknown mouse action: ${args.action}`);
494
+ }
495
+ break;
496
+
497
+ // Keyboard 命令
498
+ case "keydown":
499
+ await advancedCommands.keyDown(page, args.key);
500
+ result = `Key down: ${args.key}`;
501
+ break;
502
+
503
+ case "keyup":
504
+ await advancedCommands.keyUp(page, args.key);
505
+ result = `Key up: ${args.key}`;
506
+ break;
507
+
508
+ // Debug 命令
509
+ case "console":
510
+ if (args.action === "clear") {
511
+ advancedCommands.clearConsoleLogs();
512
+ result = "Console logs cleared";
513
+ } else {
514
+ result = advancedCommands.getConsoleLogs();
515
+ }
516
+ break;
517
+
518
+ case "errors":
519
+ if (args.action === "clear") {
520
+ advancedCommands.clearPageErrors();
521
+ result = "Page errors cleared";
522
+ } else {
523
+ result = advancedCommands.getPageErrors();
524
+ }
525
+ break;
526
+
527
+ case "highlight":
528
+ await advancedCommands.highlightElement(page, args.selector);
529
+ result = `Highlighted: ${args.selector}`;
530
+ break;
531
+
532
+ default:
533
+ throw new Error(`Unknown command: ${command}`);
534
+ }
535
+ } finally {
536
+ // Hide operation indicator
537
+ if (options.headed) {
538
+ await browser.hideOperationIndicator();
539
+ }
540
+ }
541
+
542
+ // Update session info after command execution
543
+ const url = await page.url();
544
+ const title = await page.title();
545
+ await this.sessionManager.updatePageInfo(sessionName, url, title);
546
+
547
+ // Update session with browser info
548
+ const wsEndpoint = browser.getWsEndpoint();
549
+ const pid = browser.getPid();
550
+ if (wsEndpoint) {
551
+ await this.sessionManager.markRunning(sessionName, wsEndpoint, pid);
552
+ }
553
+
554
+ return { success: true, data: result };
555
+ } catch (error) {
556
+ return {
557
+ success: false,
558
+ error: error instanceof Error ? formatError(error) : String(error),
559
+ };
560
+ }
561
+ }
562
+ }
@@ -0,0 +1,110 @@
1
+ import type { Session } from "./store";
2
+ import { SessionStore } from "./store";
3
+
4
+ export interface SessionManagerOptions {
5
+ baseDir?: string;
6
+ }
7
+
8
+ export class SessionManager {
9
+ private store: SessionStore;
10
+ private activeSessions: Map<string, Session>;
11
+
12
+ constructor(options: SessionManagerOptions = {}) {
13
+ this.store = new SessionStore(options.baseDir);
14
+ this.activeSessions = new Map();
15
+ }
16
+
17
+ async getOrCreate(
18
+ name: string,
19
+ channel: "chrome" | "msedge" | "chromium" = "chrome",
20
+ ): Promise<Session> {
21
+ // Check in-memory cache
22
+ if (this.activeSessions.has(name)) {
23
+ return this.activeSessions.get(name)!;
24
+ }
25
+
26
+ // Try to load from disk
27
+ let session = await this.store.load(name);
28
+
29
+ if (!session) {
30
+ // Create new session
31
+ session = await this.store.create(name, channel);
32
+ }
33
+
34
+ this.activeSessions.set(name, session);
35
+ return session;
36
+ }
37
+
38
+ async update(name: string, updates: Partial<Session>): Promise<Session> {
39
+ const session = await this.store.update(name, updates);
40
+ if (!session) {
41
+ throw new Error(`Session not found: ${name}`);
42
+ }
43
+
44
+ this.activeSessions.set(name, session);
45
+ return session;
46
+ }
47
+
48
+ async markRunning(name: string, wsEndpoint: string, pid?: number): Promise<Session> {
49
+ return this.update(name, {
50
+ status: "running",
51
+ wsEndpoint,
52
+ pid,
53
+ });
54
+ }
55
+
56
+ async markStopped(name: string): Promise<Session> {
57
+ return this.update(name, {
58
+ status: "stopped",
59
+ wsEndpoint: undefined,
60
+ pid: undefined,
61
+ });
62
+ }
63
+
64
+ async updatePageInfo(name: string, url: string, title: string): Promise<Session> {
65
+ return this.update(name, { url, title });
66
+ }
67
+
68
+ async list(): Promise<Session[]> {
69
+ return this.store.list();
70
+ }
71
+
72
+ async delete(name: string): Promise<boolean> {
73
+ this.activeSessions.delete(name);
74
+ return this.store.delete(name);
75
+ }
76
+
77
+ async updateActivity(name: string): Promise<void> {
78
+ await this.store.updateActivity(name);
79
+ const session = await this.store.load(name);
80
+ if (session) {
81
+ this.activeSessions.set(name, session);
82
+ }
83
+ }
84
+
85
+ async close(name: string): Promise<void> {
86
+ await this.markStopped(name);
87
+ this.activeSessions.delete(name);
88
+ }
89
+
90
+ async closeAll(): Promise<void> {
91
+ const sessions = await this.list();
92
+ for (const session of sessions) {
93
+ if (session.status === "running") {
94
+ await this.close(session.name);
95
+ }
96
+ }
97
+ this.activeSessions.clear();
98
+ }
99
+
100
+ isRunning(session: Session): boolean {
101
+ return session.status === "running" && !!session.wsEndpoint;
102
+ }
103
+
104
+ async get(name: string): Promise<Session | null> {
105
+ if (this.activeSessions.has(name)) {
106
+ return this.activeSessions.get(name)!;
107
+ }
108
+ return this.store.load(name);
109
+ }
110
+ }