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.
- package/LICENSE +21 -0
- package/README.md +196 -0
- package/package.json +63 -0
- package/src/browser/context.ts +66 -0
- package/src/browser/manager.ts +414 -0
- package/src/browser/sync-chrome-data.ts +53 -0
- package/src/cli.ts +628 -0
- package/src/cli.ts.backup +529 -0
- package/src/commands/actions.ts +232 -0
- package/src/commands/advanced.ts +252 -0
- package/src/commands/config.ts +44 -0
- package/src/commands/getters.ts +110 -0
- package/src/commands/info.ts +195 -0
- package/src/commands/navigation.ts +50 -0
- package/src/commands/session.ts +83 -0
- package/src/daemon/browser-pool.ts +65 -0
- package/src/daemon/client.ts +128 -0
- package/src/daemon/main.ts +200 -0
- package/src/daemon/server.ts +562 -0
- package/src/session/manager.ts +110 -0
- package/src/session/store.ts +172 -0
- package/src/snapshot/accessibility.ts +182 -0
- package/src/snapshot/dom-extractor.ts +220 -0
- package/src/snapshot/formatter.ts +115 -0
- package/src/snapshot/reference-store.ts +97 -0
- package/src/utils/config.ts +183 -0
- package/src/utils/errors.ts +121 -0
- package/src/utils/logger.ts +12 -0
- package/src/utils/selector.ts +23 -0
|
@@ -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
|
+
}
|