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,195 @@
1
+ import type { Page } from "patchright";
2
+ import { AccessibilityExtractor } from "../snapshot/accessibility";
3
+ import { DomSnapshotExtractor } from "../snapshot/dom-extractor";
4
+ import { SnapshotFormatter } from "../snapshot/formatter";
5
+
6
+ export interface SnapshotOptions {
7
+ interactive?: boolean;
8
+ full?: boolean;
9
+ raw?: boolean;
10
+ maxElements?: number;
11
+ referenceStore?: any; // Will be ReferenceStore instance
12
+ }
13
+
14
+ export async function snapshot(page: Page, options: SnapshotOptions = {}): Promise<string> {
15
+ const interactiveOnly = options.interactive ?? true;
16
+
17
+ // Try accessibility API first, fallback to DOM extraction
18
+ let snapshot;
19
+ let domExtractor: DomSnapshotExtractor | null = null;
20
+
21
+ try {
22
+ const accessibilityExtractor = new AccessibilityExtractor();
23
+ snapshot = await accessibilityExtractor.extract(page, interactiveOnly);
24
+
25
+ // If snapshot is empty, try DOM extraction
26
+ if (snapshot.elements.length === 0) {
27
+ throw new Error("Accessibility API returned no elements");
28
+ }
29
+ } catch (error) {
30
+ // Fallback to DOM extraction
31
+ domExtractor = new DomSnapshotExtractor();
32
+ const elements = await domExtractor.extract(page, interactiveOnly);
33
+
34
+ snapshot = {
35
+ url: await page.url(),
36
+ title: await page.title(),
37
+ timestamp: Date.now(),
38
+ elements,
39
+ };
40
+
41
+ // Store element-to-selector mappings if referenceStore is provided
42
+ if (options.referenceStore && domExtractor) {
43
+ const mappings = domExtractor.getAllMappings();
44
+ options.referenceStore.setAll(mappings);
45
+ await options.referenceStore.save();
46
+ }
47
+ }
48
+
49
+ if (options.raw) {
50
+ const formatter = new SnapshotFormatter();
51
+ return formatter.formatJson(snapshot, true);
52
+ }
53
+
54
+ const formatter = new SnapshotFormatter();
55
+ return formatter.format(snapshot, {
56
+ maxElements: options.maxElements,
57
+ includeDisabled: options.full,
58
+ });
59
+ }
60
+
61
+ export interface ScreenshotOptions {
62
+ output?: string;
63
+ fullPage?: boolean;
64
+ selector?: string;
65
+ quality?: number;
66
+ }
67
+
68
+ export async function screenshot(page: Page, options: ScreenshotOptions = {}): Promise<string> {
69
+ const output = options.output || "screenshot.png";
70
+
71
+ if (options.selector) {
72
+ const { parseSelector } = await import("../utils/selector");
73
+ const parsed = parseSelector(options.selector);
74
+
75
+ let locator;
76
+ if (parsed.type === "css") {
77
+ locator = page.locator(parsed.value);
78
+ } else if (parsed.type === "text") {
79
+ locator = page.getByText(parsed.value);
80
+ } else if (parsed.type === "xpath") {
81
+ locator = page.locator(`xpath=${parsed.value}`);
82
+ } else {
83
+ throw new Error(`Unsupported selector type for screenshot: ${parsed.type}`);
84
+ }
85
+
86
+ await locator.screenshot({ path: output });
87
+ } else {
88
+ await page.screenshot({
89
+ path: output,
90
+ fullPage: options.fullPage,
91
+ });
92
+ }
93
+
94
+ return output;
95
+ }
96
+
97
+ // Whitelist of safe operations
98
+ const SAFE_OPERATIONS = [
99
+ /^document\.title$/,
100
+ /^document\.URL$/,
101
+ /^window\.location\.href$/,
102
+ /^window\.innerWidth$/,
103
+ /^window\.innerHeight$/,
104
+ /^document\.body\.scrollHeight$/,
105
+ /^document\.body\.scrollWidth$/,
106
+ /^document\.readyState$/,
107
+ ];
108
+
109
+ // Enhanced blacklist for dangerous patterns
110
+ const FORBIDDEN_PATTERNS = [
111
+ /\b(eval|Function|constructor)\b/i,
112
+ /require\s*\(/,
113
+ /import\s*\(/,
114
+ /process\./,
115
+ /child_process/,
116
+ /fs\./,
117
+ /__dirname/,
118
+ /__filename/,
119
+ /globalThis/,
120
+ /window\s*\[/,
121
+ /\[(['"`])[a-z_]+\1\]/i, // Bracket notation: window['process']
122
+ /Object\.(assign|defineProperty|create)/,
123
+ /\.constructor/,
124
+ /prototype/,
125
+ ];
126
+
127
+ export async function evaluate(page: Page, script: string): Promise<any> {
128
+ const trimmedScript = script.trim();
129
+
130
+ // 1. Whitelist mode - only allow safe operations
131
+ const isSafe = SAFE_OPERATIONS.some((pattern) => pattern.test(trimmedScript));
132
+
133
+ if (!isSafe) {
134
+ // 2. Enhanced blacklist check
135
+ for (const pattern of FORBIDDEN_PATTERNS) {
136
+ if (pattern.test(trimmedScript)) {
137
+ throw new Error(`Unsafe operation blocked: ${pattern.source}`);
138
+ }
139
+ }
140
+ }
141
+
142
+ // 3. Execute and limit result size (防止数据窃取)
143
+ const result = await page.evaluate(trimmedScript);
144
+ const serialized = JSON.stringify(result);
145
+
146
+ if (serialized.length > 100000) {
147
+ throw new Error("Result too large (max 100KB). Use snapshot command instead.");
148
+ }
149
+
150
+ return result;
151
+ }
152
+
153
+ export async function url(page: Page): Promise<string> {
154
+ return page.url();
155
+ }
156
+
157
+ export async function title(page: Page): Promise<string> {
158
+ return page.title();
159
+ }
160
+
161
+ export interface ContentOptions {
162
+ selector?: string;
163
+ maxLength?: number;
164
+ }
165
+
166
+ export async function content(page: Page, options: ContentOptions = {}): Promise<string> {
167
+ const maxLength = options.maxLength || 10000;
168
+
169
+ let text: string;
170
+ if (options.selector) {
171
+ const { parseSelector } = await import("../utils/selector");
172
+ const parsed = parseSelector(options.selector);
173
+
174
+ let locator;
175
+ if (parsed.type === "css") {
176
+ locator = page.locator(parsed.value);
177
+ } else if (parsed.type === "text") {
178
+ locator = page.getByText(parsed.value);
179
+ } else if (parsed.type === "xpath") {
180
+ locator = page.locator(`xpath=${parsed.value}`);
181
+ } else {
182
+ throw new Error(`Unsupported selector type: ${parsed.type}`);
183
+ }
184
+
185
+ text = (await locator.textContent()) || "";
186
+ } else {
187
+ text = (await page.textContent("body")) || "";
188
+ }
189
+
190
+ if (text.length > maxLength) {
191
+ text = `${text.slice(0, maxLength)}... (truncated at ${maxLength} chars)`;
192
+ }
193
+
194
+ return text;
195
+ }
@@ -0,0 +1,50 @@
1
+ import type { Page } from "patchright";
2
+
3
+ export interface NavigationOptions {
4
+ waitUntil?: "load" | "domcontentloaded" | "networkidle";
5
+ timeout?: number;
6
+ }
7
+
8
+ export async function open(
9
+ page: Page,
10
+ url: string,
11
+ options: NavigationOptions = {},
12
+ ): Promise<void> {
13
+ const waitUntil = options.waitUntil || "load";
14
+ const timeout = options.timeout || 30000;
15
+
16
+ await page.goto(url, {
17
+ waitUntil,
18
+ timeout,
19
+ });
20
+ }
21
+
22
+ export async function reload(page: Page, options: NavigationOptions = {}): Promise<void> {
23
+ const waitUntil = options.waitUntil || "load";
24
+ const timeout = options.timeout || 30000;
25
+
26
+ await page.reload({
27
+ waitUntil,
28
+ timeout,
29
+ });
30
+ }
31
+
32
+ export async function back(page: Page, options: NavigationOptions = {}): Promise<void> {
33
+ const waitUntil = options.waitUntil || "load";
34
+ const timeout = options.timeout || 30000;
35
+
36
+ await page.goBack({
37
+ waitUntil,
38
+ timeout,
39
+ });
40
+ }
41
+
42
+ export async function forward(page: Page, options: NavigationOptions = {}): Promise<void> {
43
+ const waitUntil = options.waitUntil || "load";
44
+ const timeout = options.timeout || 30000;
45
+
46
+ await page.goForward({
47
+ waitUntil,
48
+ timeout,
49
+ });
50
+ }
@@ -0,0 +1,83 @@
1
+ import type { SessionManager } from "../session/manager";
2
+
3
+ export interface SessionListOptions {
4
+ json?: boolean;
5
+ }
6
+
7
+ export async function listSessions(
8
+ sessionManager: SessionManager,
9
+ options: SessionListOptions = {},
10
+ ): Promise<string> {
11
+ const sessions = await sessionManager.list();
12
+
13
+ if (options.json) {
14
+ return JSON.stringify(sessions, null, 2);
15
+ }
16
+
17
+ if (sessions.length === 0) {
18
+ return "No sessions found.";
19
+ }
20
+
21
+ const lines: string[] = [];
22
+ lines.push(
23
+ `${
24
+ "SESSION".padEnd(15) + "STATUS".padEnd(10) + "BROWSER".padEnd(12) + "URL".padEnd(40)
25
+ }LAST ACTIVE`,
26
+ );
27
+ lines.push("-".repeat(100));
28
+
29
+ for (const session of sessions) {
30
+ const name = session.name.padEnd(15);
31
+ const status = session.status.padEnd(10);
32
+ const browser = (session.channel || "-").padEnd(12);
33
+ const url = (session.url || "-").slice(0, 38).padEnd(40);
34
+ const lastActive = formatLastActive(session.lastActiveAt);
35
+
36
+ lines.push(`${name}${status}${browser}${url}${lastActive}`);
37
+ }
38
+
39
+ return lines.join("\n");
40
+ }
41
+
42
+ function formatLastActive(timestamp: number): string {
43
+ const now = Date.now();
44
+ const diff = now - timestamp;
45
+
46
+ const minutes = Math.floor(diff / 60000);
47
+ const hours = Math.floor(diff / 3600000);
48
+ const days = Math.floor(diff / 86400000);
49
+
50
+ if (minutes < 1) {
51
+ return "just now";
52
+ }
53
+ if (minutes < 60) {
54
+ return `${minutes} min ago`;
55
+ }
56
+ if (hours < 24) {
57
+ return `${hours} hour${hours > 1 ? "s" : ""} ago`;
58
+ }
59
+ return `${days} day${days > 1 ? "s" : ""} ago`;
60
+ }
61
+
62
+ export async function closeSession(
63
+ sessionManager: SessionManager,
64
+ sessionName?: string,
65
+ closeAll = false,
66
+ ): Promise<string> {
67
+ if (closeAll) {
68
+ await sessionManager.closeAll();
69
+ return "All sessions closed.";
70
+ }
71
+
72
+ if (!sessionName) {
73
+ throw new Error("Session name required");
74
+ }
75
+
76
+ const session = await sessionManager.get(sessionName);
77
+ if (!session) {
78
+ throw new Error(`Session not found: ${sessionName}`);
79
+ }
80
+
81
+ await sessionManager.close(sessionName);
82
+ return `Session closed: ${sessionName}`;
83
+ }
@@ -0,0 +1,65 @@
1
+ import { BrowserManager } from "../browser/manager";
2
+ import type { BrowserManagerOptions } from "../browser/manager";
3
+ import type { Session } from "../session/store";
4
+
5
+ /**
6
+ * BrowserPool 管理多个 Session 的浏览器实例
7
+ */
8
+ export class BrowserPool {
9
+ private browsers: Map<string, BrowserManager> = new Map();
10
+
11
+ async get(session: Session, options: BrowserManagerOptions = {}): Promise<BrowserManager> {
12
+ const key = session.name;
13
+
14
+ if (this.browsers.has(key)) {
15
+ const browser = this.browsers.get(key)!;
16
+
17
+ // 检查浏览器是否还连接
18
+ if (browser.isConnected()) {
19
+ return browser;
20
+ }
21
+
22
+ // 浏览器断开连接,尝试重连
23
+ try {
24
+ await browser.connect();
25
+ return browser;
26
+ } catch (error) {
27
+ console.error(`Failed to reconnect browser for session ${key}:`, error);
28
+ // 重连失败,移除旧实例
29
+ this.browsers.delete(key);
30
+ }
31
+ }
32
+
33
+ // 创建新的浏览器实例
34
+ const browser = new BrowserManager(session, options);
35
+ await browser.connect();
36
+ this.browsers.set(key, browser);
37
+
38
+ return browser;
39
+ }
40
+
41
+ async close(sessionName: string): Promise<boolean> {
42
+ const browser = this.browsers.get(sessionName);
43
+ if (!browser) {
44
+ return false;
45
+ }
46
+
47
+ await browser.close();
48
+ this.browsers.delete(sessionName);
49
+ return true;
50
+ }
51
+
52
+ async closeAll(): Promise<void> {
53
+ const closePromises = Array.from(this.browsers.values()).map((b) => b.close());
54
+ await Promise.all(closePromises);
55
+ this.browsers.clear();
56
+ }
57
+
58
+ getActiveSessions(): string[] {
59
+ return Array.from(this.browsers.keys());
60
+ }
61
+
62
+ size(): number {
63
+ return this.browsers.size;
64
+ }
65
+ }
@@ -0,0 +1,128 @@
1
+ import { existsSync } from "node:fs";
2
+ import { readFile } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+ import type { CommandRequest, CommandResponse } from "./server";
6
+
7
+ const CONFIG_FILE = join(homedir(), ".hab", "daemon.json");
8
+
9
+ export class DaemonClient {
10
+ private host = "127.0.0.1";
11
+ private port = 9527;
12
+
13
+ async ensureDaemonRunning(): Promise<void> {
14
+ // Load daemon config
15
+ if (existsSync(CONFIG_FILE)) {
16
+ try {
17
+ const content = await readFile(CONFIG_FILE, "utf-8");
18
+ const config = JSON.parse(content);
19
+ this.host = config.host;
20
+ this.port = config.port;
21
+ } catch {
22
+ // Use defaults
23
+ }
24
+ }
25
+
26
+ // Check if daemon is running
27
+ const healthy = await this.checkHealth();
28
+ if (healthy) {
29
+ return;
30
+ }
31
+
32
+ // Auto-start daemon
33
+ console.log("Starting daemon...");
34
+ await this.startDaemon();
35
+
36
+ // Wait for daemon to be ready
37
+ for (let i = 0; i < 30; i++) {
38
+ await new Promise((resolve) => setTimeout(resolve, 100));
39
+ if (await this.checkHealth()) {
40
+ console.log("Daemon started successfully");
41
+ return;
42
+ }
43
+ }
44
+
45
+ throw new Error("Failed to start daemon");
46
+ }
47
+
48
+ private async checkHealth(): Promise<boolean> {
49
+ try {
50
+ const response = await fetch(`http://${this.host}:${this.port}/health`, {
51
+ signal: AbortSignal.timeout(1000),
52
+ });
53
+ return response.ok;
54
+ } catch {
55
+ return false;
56
+ }
57
+ }
58
+
59
+ private async startDaemon(): Promise<void> {
60
+ // Start daemon in background
61
+ const projectRoot = join(import.meta.dir, "../..");
62
+ const daemonScript = join(projectRoot, "src/daemon/main.ts");
63
+
64
+ const proc = Bun.spawn({
65
+ cmd: ["bun", daemonScript, "start"],
66
+ cwd: projectRoot,
67
+ stdout: "ignore",
68
+ stderr: "ignore",
69
+ stdin: "ignore",
70
+ });
71
+
72
+ proc.unref();
73
+ }
74
+
75
+ async execute(request: CommandRequest): Promise<CommandResponse> {
76
+ await this.ensureDaemonRunning();
77
+
78
+ try {
79
+ const response = await fetch(`http://${this.host}:${this.port}/execute`, {
80
+ method: "POST",
81
+ headers: { "Content-Type": "application/json" },
82
+ body: JSON.stringify(request),
83
+ signal: AbortSignal.timeout(60000), // 60s timeout
84
+ });
85
+
86
+ if (!response.ok) {
87
+ const error = await response.text();
88
+ throw new Error(`Daemon error: ${error}`);
89
+ }
90
+
91
+ return await response.json();
92
+ } catch (error) {
93
+ if (error instanceof Error && error.name === "TimeoutError") {
94
+ throw new Error("Command timed out");
95
+ }
96
+ throw error;
97
+ }
98
+ }
99
+
100
+ async closeSession(session: string): Promise<boolean> {
101
+ await this.ensureDaemonRunning();
102
+
103
+ try {
104
+ const response = await fetch(`http://${this.host}:${this.port}/close`, {
105
+ method: "POST",
106
+ headers: { "Content-Type": "application/json" },
107
+ body: JSON.stringify({ session }),
108
+ });
109
+
110
+ const result = await response.json();
111
+ return result.closed;
112
+ } catch {
113
+ return false;
114
+ }
115
+ }
116
+
117
+ async listSessions(): Promise<any[]> {
118
+ await this.ensureDaemonRunning();
119
+
120
+ try {
121
+ const response = await fetch(`http://${this.host}:${this.port}/sessions`);
122
+ const result = await response.json();
123
+ return result.sessions || [];
124
+ } catch {
125
+ return [];
126
+ }
127
+ }
128
+ }