@alfe.ai/browser 0.0.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/dist/index.cjs +600 -0
- package/dist/index.d.cts +232 -0
- package/dist/index.d.ts +232 -0
- package/dist/index.js +574 -0
- package/package.json +30 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region \0rolldown/runtime.js
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
11
|
+
key = keys[i];
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
13
|
+
get: ((k) => from[k]).bind(null, key),
|
|
14
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
20
|
+
value: mod,
|
|
21
|
+
enumerable: true
|
|
22
|
+
}) : target, mod));
|
|
23
|
+
//#endregion
|
|
24
|
+
let _alfe_ai_remote = require("@alfe.ai/remote");
|
|
25
|
+
let puppeteer_core = require("puppeteer-core");
|
|
26
|
+
puppeteer_core = __toESM(puppeteer_core);
|
|
27
|
+
//#region src/browser-session.ts
|
|
28
|
+
/**
|
|
29
|
+
* BrowserSession — owns the single headless Chrome instance the agent
|
|
30
|
+
* automates and the human takes over. Lazy-launched on first use, idle-shut
|
|
31
|
+
* down when nothing holds it open (cookies/login persist on disk in
|
|
32
|
+
* `userDataDir` across restarts). Tracks the *active* target so OAuth popups /
|
|
33
|
+
* new tabs are what gets screencast, not a stale main window.
|
|
34
|
+
*/
|
|
35
|
+
const DEFAULT_IDLE_MS = 300 * 1e3;
|
|
36
|
+
const noopLogger$2 = {
|
|
37
|
+
info: () => {},
|
|
38
|
+
warn: () => {},
|
|
39
|
+
error: () => {},
|
|
40
|
+
debug: () => {}
|
|
41
|
+
};
|
|
42
|
+
var BrowserSession = class {
|
|
43
|
+
browser = null;
|
|
44
|
+
activePage = null;
|
|
45
|
+
launching = null;
|
|
46
|
+
holds = 0;
|
|
47
|
+
idleTimer = null;
|
|
48
|
+
log;
|
|
49
|
+
idleMs;
|
|
50
|
+
constructor(options) {
|
|
51
|
+
this.options = options;
|
|
52
|
+
this.log = options.logger ?? noopLogger$2;
|
|
53
|
+
this.idleMs = options.idleShutdownMs ?? DEFAULT_IDLE_MS;
|
|
54
|
+
}
|
|
55
|
+
/** Launch Chrome if not already running (idempotent, concurrent-safe). */
|
|
56
|
+
async ensureLaunched() {
|
|
57
|
+
if (this.browser) return;
|
|
58
|
+
if (this.launching) return this.launching;
|
|
59
|
+
this.launching = this.doLaunch().finally(() => {
|
|
60
|
+
this.launching = null;
|
|
61
|
+
});
|
|
62
|
+
return this.launching;
|
|
63
|
+
}
|
|
64
|
+
async doLaunch() {
|
|
65
|
+
const args = [
|
|
66
|
+
"--disable-blink-features=AutomationControlled",
|
|
67
|
+
...this.options.noSandbox ? ["--no-sandbox", "--disable-setuid-sandbox"] : [],
|
|
68
|
+
...this.options.extraArgs ?? []
|
|
69
|
+
];
|
|
70
|
+
this.log.info(`Launching Chrome (${this.options.executablePath})`);
|
|
71
|
+
this.browser = await puppeteer_core.default.launch({
|
|
72
|
+
executablePath: this.options.executablePath,
|
|
73
|
+
headless: this.options.headless ?? true,
|
|
74
|
+
userDataDir: this.options.userDataDir,
|
|
75
|
+
args
|
|
76
|
+
});
|
|
77
|
+
this.activePage = (await this.browser.pages())[0] ?? await this.browser.newPage();
|
|
78
|
+
this.browser.on("targetcreated", (target) => {
|
|
79
|
+
target.page().then((page) => {
|
|
80
|
+
if (page) {
|
|
81
|
+
this.activePage = page;
|
|
82
|
+
this.log.debug("Active page switched to new target");
|
|
83
|
+
}
|
|
84
|
+
}).catch(() => {});
|
|
85
|
+
});
|
|
86
|
+
this.browser.on("disconnected", () => {
|
|
87
|
+
this.browser = null;
|
|
88
|
+
this.activePage = null;
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
/** The current active page, launching Chrome first if needed. */
|
|
92
|
+
async getActivePage() {
|
|
93
|
+
await this.ensureLaunched();
|
|
94
|
+
if (!this.activePage || this.activePage.isClosed()) {
|
|
95
|
+
if (!this.browser) throw new Error("Browser not available");
|
|
96
|
+
this.activePage = (await this.browser.pages()).find((p) => !p.isClosed()) ?? await this.browser.newPage();
|
|
97
|
+
}
|
|
98
|
+
return this.activePage;
|
|
99
|
+
}
|
|
100
|
+
/** Prevent idle shutdown while a viewer or op is active. */
|
|
101
|
+
addHold() {
|
|
102
|
+
this.holds += 1;
|
|
103
|
+
this.clearIdleTimer();
|
|
104
|
+
}
|
|
105
|
+
/** Release a hold; arm idle shutdown when the last one is released. */
|
|
106
|
+
removeHold() {
|
|
107
|
+
this.holds = Math.max(0, this.holds - 1);
|
|
108
|
+
if (this.holds === 0) this.armIdleTimer();
|
|
109
|
+
}
|
|
110
|
+
/** Reset the idle timer on any activity (only relevant when unheld). */
|
|
111
|
+
touch() {
|
|
112
|
+
if (this.holds === 0) this.armIdleTimer();
|
|
113
|
+
}
|
|
114
|
+
armIdleTimer() {
|
|
115
|
+
this.clearIdleTimer();
|
|
116
|
+
const timer = setTimeout(() => {
|
|
117
|
+
if (this.holds === 0) {
|
|
118
|
+
this.log.info("Chrome idle — shutting down (cookies persist on disk)");
|
|
119
|
+
this.shutdown();
|
|
120
|
+
}
|
|
121
|
+
}, this.idleMs);
|
|
122
|
+
timer.unref();
|
|
123
|
+
this.idleTimer = timer;
|
|
124
|
+
}
|
|
125
|
+
clearIdleTimer() {
|
|
126
|
+
if (this.idleTimer) {
|
|
127
|
+
clearTimeout(this.idleTimer);
|
|
128
|
+
this.idleTimer = null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
async shutdown() {
|
|
132
|
+
this.clearIdleTimer();
|
|
133
|
+
const browser = this.browser;
|
|
134
|
+
this.browser = null;
|
|
135
|
+
this.activePage = null;
|
|
136
|
+
if (browser) try {
|
|
137
|
+
await browser.close();
|
|
138
|
+
} catch {}
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
//#endregion
|
|
142
|
+
//#region src/screencast-pump.ts
|
|
143
|
+
const noopLogger$1 = {
|
|
144
|
+
info: () => {},
|
|
145
|
+
warn: () => {},
|
|
146
|
+
error: () => {},
|
|
147
|
+
debug: () => {}
|
|
148
|
+
};
|
|
149
|
+
var ScreencastPump = class {
|
|
150
|
+
cdp = null;
|
|
151
|
+
running = false;
|
|
152
|
+
frameSeq = 0;
|
|
153
|
+
outstanding = null;
|
|
154
|
+
log;
|
|
155
|
+
quality;
|
|
156
|
+
ackTimeoutMs;
|
|
157
|
+
constructor(options) {
|
|
158
|
+
this.options = options;
|
|
159
|
+
this.log = options.logger ?? noopLogger$1;
|
|
160
|
+
this.quality = options.quality ?? 70;
|
|
161
|
+
this.ackTimeoutMs = options.ackTimeoutMs ?? 2e3;
|
|
162
|
+
}
|
|
163
|
+
get isRunning() {
|
|
164
|
+
return this.running;
|
|
165
|
+
}
|
|
166
|
+
/** Start (or restart) the screencast on the given page. */
|
|
167
|
+
async start(page, viewport) {
|
|
168
|
+
await this.stop();
|
|
169
|
+
const cdp = await page.createCDPSession();
|
|
170
|
+
this.cdp = cdp;
|
|
171
|
+
await cdp.send("Emulation.setDeviceMetricsOverride", {
|
|
172
|
+
width: viewport.width,
|
|
173
|
+
height: viewport.height,
|
|
174
|
+
deviceScaleFactor: viewport.dpr,
|
|
175
|
+
mobile: false
|
|
176
|
+
});
|
|
177
|
+
cdp.on("Page.screencastFrame", (event) => {
|
|
178
|
+
this.onCdpFrame(event.data, event.sessionId, event.metadata);
|
|
179
|
+
});
|
|
180
|
+
await cdp.send("Page.startScreencast", {
|
|
181
|
+
format: "jpeg",
|
|
182
|
+
quality: this.quality,
|
|
183
|
+
maxWidth: Math.round(viewport.width * viewport.dpr),
|
|
184
|
+
maxHeight: Math.round(viewport.height * viewport.dpr),
|
|
185
|
+
everyNthFrame: 1
|
|
186
|
+
});
|
|
187
|
+
this.running = true;
|
|
188
|
+
this.log.debug("Screencast started");
|
|
189
|
+
}
|
|
190
|
+
onCdpFrame(dataBase64, cdpSessionId, metadata) {
|
|
191
|
+
if (this.outstanding) this.ackCdp(this.outstanding);
|
|
192
|
+
const frameSeq = ++this.frameSeq;
|
|
193
|
+
const meta = {
|
|
194
|
+
deviceWidth: metadata.deviceWidth ?? 0,
|
|
195
|
+
deviceHeight: metadata.deviceHeight ?? 0,
|
|
196
|
+
frameSeq,
|
|
197
|
+
offsetTop: metadata.offsetTop,
|
|
198
|
+
pageScaleFactor: metadata.pageScaleFactor,
|
|
199
|
+
scrollOffsetX: metadata.scrollOffsetX,
|
|
200
|
+
scrollOffsetY: metadata.scrollOffsetY
|
|
201
|
+
};
|
|
202
|
+
const timer = setTimeout(() => {
|
|
203
|
+
if (this.outstanding?.frameSeq === frameSeq) this.ackCdp(this.outstanding);
|
|
204
|
+
}, this.ackTimeoutMs);
|
|
205
|
+
timer.unref();
|
|
206
|
+
this.outstanding = {
|
|
207
|
+
cdpSessionId,
|
|
208
|
+
frameSeq,
|
|
209
|
+
timer
|
|
210
|
+
};
|
|
211
|
+
this.options.onFrame(Buffer.from(dataBase64, "base64"), meta);
|
|
212
|
+
}
|
|
213
|
+
/** Viewer acked frame `frameSeq` — release Chrome to send the next frame. */
|
|
214
|
+
ackFromViewer(frameSeq) {
|
|
215
|
+
if (this.outstanding?.frameSeq === frameSeq) this.ackCdp(this.outstanding);
|
|
216
|
+
}
|
|
217
|
+
ackCdp(outstanding) {
|
|
218
|
+
clearTimeout(outstanding.timer);
|
|
219
|
+
this.outstanding = null;
|
|
220
|
+
if (this.cdp && this.running) this.cdp.send("Page.screencastFrameAck", { sessionId: outstanding.cdpSessionId }).catch(() => {});
|
|
221
|
+
}
|
|
222
|
+
async stop() {
|
|
223
|
+
this.running = false;
|
|
224
|
+
if (this.outstanding) {
|
|
225
|
+
clearTimeout(this.outstanding.timer);
|
|
226
|
+
this.outstanding = null;
|
|
227
|
+
}
|
|
228
|
+
const cdp = this.cdp;
|
|
229
|
+
this.cdp = null;
|
|
230
|
+
if (cdp) {
|
|
231
|
+
try {
|
|
232
|
+
await cdp.send("Page.stopScreencast");
|
|
233
|
+
} catch {}
|
|
234
|
+
try {
|
|
235
|
+
await cdp.detach();
|
|
236
|
+
} catch {}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
//#endregion
|
|
241
|
+
//#region src/input-injector.ts
|
|
242
|
+
const MOUSE_TYPE = {
|
|
243
|
+
mousemoved: "mouseMoved",
|
|
244
|
+
mousepressed: "mousePressed",
|
|
245
|
+
mousereleased: "mouseReleased"
|
|
246
|
+
};
|
|
247
|
+
var InputInjector = class {
|
|
248
|
+
cdp = null;
|
|
249
|
+
viewport;
|
|
250
|
+
constructor(viewport) {
|
|
251
|
+
this.viewport = viewport;
|
|
252
|
+
}
|
|
253
|
+
async attach(page) {
|
|
254
|
+
await this.detach();
|
|
255
|
+
this.cdp = await page.createCDPSession();
|
|
256
|
+
}
|
|
257
|
+
async detach() {
|
|
258
|
+
const cdp = this.cdp;
|
|
259
|
+
this.cdp = null;
|
|
260
|
+
if (cdp) try {
|
|
261
|
+
await cdp.detach();
|
|
262
|
+
} catch {}
|
|
263
|
+
}
|
|
264
|
+
updateViewport(viewport) {
|
|
265
|
+
this.viewport = viewport;
|
|
266
|
+
}
|
|
267
|
+
toCssPx(nx, ny) {
|
|
268
|
+
return {
|
|
269
|
+
x: Math.round(clamp01(nx) * this.viewport.width),
|
|
270
|
+
y: Math.round(clamp01(ny) * this.viewport.height)
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
mouse(p) {
|
|
274
|
+
if (!this.cdp) return;
|
|
275
|
+
const { x, y } = this.toCssPx(p.nx, p.ny);
|
|
276
|
+
this.cdp.send("Input.dispatchMouseEvent", {
|
|
277
|
+
type: MOUSE_TYPE[p.type],
|
|
278
|
+
x,
|
|
279
|
+
y,
|
|
280
|
+
button: p.button ?? "none",
|
|
281
|
+
buttons: p.buttons ?? 0,
|
|
282
|
+
clickCount: p.clickCount ?? (p.type === "mousepressed" ? 1 : 0),
|
|
283
|
+
modifiers: p.modifiers ?? 0
|
|
284
|
+
}).catch(() => {});
|
|
285
|
+
}
|
|
286
|
+
wheel(p) {
|
|
287
|
+
if (!this.cdp) return;
|
|
288
|
+
const { x, y } = this.toCssPx(p.nx, p.ny);
|
|
289
|
+
this.cdp.send("Input.dispatchMouseEvent", {
|
|
290
|
+
type: "mouseWheel",
|
|
291
|
+
x,
|
|
292
|
+
y,
|
|
293
|
+
deltaX: p.deltaX,
|
|
294
|
+
deltaY: p.deltaY,
|
|
295
|
+
modifiers: p.modifiers ?? 0
|
|
296
|
+
}).catch(() => {});
|
|
297
|
+
}
|
|
298
|
+
key(p) {
|
|
299
|
+
if (!this.cdp) return;
|
|
300
|
+
if (p.type === "char") {
|
|
301
|
+
this.cdp.send("Input.dispatchKeyEvent", {
|
|
302
|
+
type: "char",
|
|
303
|
+
text: p.text ?? "",
|
|
304
|
+
modifiers: p.modifiers ?? 0
|
|
305
|
+
}).catch(() => {});
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
this.cdp.send("Input.dispatchKeyEvent", {
|
|
309
|
+
type: p.type === "keydown" ? "keyDown" : "keyUp",
|
|
310
|
+
key: p.key,
|
|
311
|
+
code: p.code,
|
|
312
|
+
text: p.type === "keydown" ? p.text : void 0,
|
|
313
|
+
modifiers: p.modifiers ?? 0
|
|
314
|
+
}).catch(() => {});
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
function clamp01(n) {
|
|
318
|
+
if (Number.isNaN(n)) return 0;
|
|
319
|
+
return Math.max(0, Math.min(1, n));
|
|
320
|
+
}
|
|
321
|
+
//#endregion
|
|
322
|
+
//#region src/automation.ts
|
|
323
|
+
var BrowserAutomation = class {
|
|
324
|
+
constructor(session, turn, isNavigationAllowed) {
|
|
325
|
+
this.session = session;
|
|
326
|
+
this.turn = turn;
|
|
327
|
+
this.isNavigationAllowed = isNavigationAllowed;
|
|
328
|
+
}
|
|
329
|
+
async navigate(url) {
|
|
330
|
+
await this.turn.acquireAgent();
|
|
331
|
+
if (!this.isNavigationAllowed(url)) throw new Error(`Navigation to ${url} blocked by SSRF policy`);
|
|
332
|
+
const page = await this.session.getActivePage();
|
|
333
|
+
await page.goto(url, { waitUntil: "domcontentloaded" });
|
|
334
|
+
return {
|
|
335
|
+
url: page.url(),
|
|
336
|
+
title: await page.title()
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
async click(selector) {
|
|
340
|
+
await this.turn.acquireAgent();
|
|
341
|
+
await (await this.session.getActivePage()).click(selector);
|
|
342
|
+
}
|
|
343
|
+
async type(selector, text) {
|
|
344
|
+
await this.turn.acquireAgent();
|
|
345
|
+
await (await this.session.getActivePage()).type(selector, text);
|
|
346
|
+
}
|
|
347
|
+
async waitFor(opts) {
|
|
348
|
+
await this.turn.acquireAgent();
|
|
349
|
+
const page = await this.session.getActivePage();
|
|
350
|
+
if (opts.selector) await page.waitForSelector(opts.selector);
|
|
351
|
+
else if (opts.urlPattern) {
|
|
352
|
+
const pattern = opts.urlPattern;
|
|
353
|
+
await page.waitForFunction((p) => window.location.href.includes(p), {}, pattern);
|
|
354
|
+
} else if (typeof opts.ms === "number") await new Promise((resolve) => setTimeout(resolve, opts.ms));
|
|
355
|
+
}
|
|
356
|
+
/** One-shot JPEG screenshot (base64) for the agent's own reasoning — distinct
|
|
357
|
+
* from the continuous screencast stream to viewers. */
|
|
358
|
+
async screenshot() {
|
|
359
|
+
await this.turn.acquireAgent();
|
|
360
|
+
return (await this.session.getActivePage()).screenshot({
|
|
361
|
+
type: "jpeg",
|
|
362
|
+
quality: 70,
|
|
363
|
+
encoding: "base64"
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
/** Evaluate an expression in the page context via CDP (no eval on our side). */
|
|
367
|
+
async evaluate(expression) {
|
|
368
|
+
await this.turn.acquireAgent();
|
|
369
|
+
const cdp = await (await this.session.getActivePage()).createCDPSession();
|
|
370
|
+
try {
|
|
371
|
+
return (await cdp.send("Runtime.evaluate", {
|
|
372
|
+
expression,
|
|
373
|
+
returnByValue: true,
|
|
374
|
+
awaitPromise: true
|
|
375
|
+
})).result.value;
|
|
376
|
+
} finally {
|
|
377
|
+
try {
|
|
378
|
+
await cdp.detach();
|
|
379
|
+
} catch {}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
//#endregion
|
|
384
|
+
//#region src/browser-surface.ts
|
|
385
|
+
/**
|
|
386
|
+
* BrowserSurface — the `SurfaceHandler` for browser co-browsing. Ties together
|
|
387
|
+
* the shared Chrome (BrowserSession), the screencast pump, the input injector,
|
|
388
|
+
* the agent/human turn-mutex, and the agent automation tools. Viewers watch the
|
|
389
|
+
* live page; the turn token gates who can write.
|
|
390
|
+
*
|
|
391
|
+
* Multiple viewers can attach to one browser; screencast frames broadcast to
|
|
392
|
+
* all, and the pump's ack-gating advances on the first viewer ack (a slow
|
|
393
|
+
* second viewer may drop frames — acceptable for v1).
|
|
394
|
+
*/
|
|
395
|
+
const noopLogger = {
|
|
396
|
+
info: () => {},
|
|
397
|
+
warn: () => {},
|
|
398
|
+
error: () => {},
|
|
399
|
+
debug: () => {}
|
|
400
|
+
};
|
|
401
|
+
var BrowserSurface = class {
|
|
402
|
+
surface = "browser";
|
|
403
|
+
session;
|
|
404
|
+
turn;
|
|
405
|
+
pump;
|
|
406
|
+
injector;
|
|
407
|
+
automation;
|
|
408
|
+
viewers = /* @__PURE__ */ new Set();
|
|
409
|
+
viewport = {
|
|
410
|
+
width: 1280,
|
|
411
|
+
height: 720,
|
|
412
|
+
dpr: 1
|
|
413
|
+
};
|
|
414
|
+
handoff = null;
|
|
415
|
+
log;
|
|
416
|
+
constructor(options, sendFrame) {
|
|
417
|
+
this.options = options;
|
|
418
|
+
this.sendFrame = sendFrame;
|
|
419
|
+
this.log = options.logger ?? noopLogger;
|
|
420
|
+
this.session = new BrowserSession(options);
|
|
421
|
+
this.turn = new _alfe_ai_remote.TurnController({ onOwnerChange: () => {
|
|
422
|
+
this.broadcastState();
|
|
423
|
+
} });
|
|
424
|
+
this.pump = new ScreencastPump({
|
|
425
|
+
onFrame: (jpeg, meta) => {
|
|
426
|
+
this.broadcastFrame(jpeg, meta);
|
|
427
|
+
},
|
|
428
|
+
logger: this.log
|
|
429
|
+
});
|
|
430
|
+
this.injector = new InputInjector(this.viewport);
|
|
431
|
+
this.automation = new BrowserAutomation(this.session, this.turn, options.isNavigationAllowed ?? (() => true));
|
|
432
|
+
}
|
|
433
|
+
async openSession(sessionId, open) {
|
|
434
|
+
this.viewers.add(sessionId);
|
|
435
|
+
this.session.addHold();
|
|
436
|
+
if (open.width && open.height) this.viewport = {
|
|
437
|
+
width: open.width,
|
|
438
|
+
height: open.height,
|
|
439
|
+
dpr: open.dpr ?? 1
|
|
440
|
+
};
|
|
441
|
+
await this.ensureStreaming();
|
|
442
|
+
this.sendState(sessionId);
|
|
443
|
+
}
|
|
444
|
+
handleFrame(frame) {
|
|
445
|
+
switch (frame.type) {
|
|
446
|
+
case _alfe_ai_remote.RemoteFrameType.SCREENCAST_ACK: {
|
|
447
|
+
const ack = (0, _alfe_ai_remote.decodeJson)(frame.payload);
|
|
448
|
+
if (ack) this.pump.ackFromViewer(ack.frameSeq);
|
|
449
|
+
this.session.touch();
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
452
|
+
case _alfe_ai_remote.RemoteFrameType.RESIZE: {
|
|
453
|
+
const r = (0, _alfe_ai_remote.decodeJson)(frame.payload);
|
|
454
|
+
if (r) this.applyResize(r.width, r.height, r.dpr ?? 1);
|
|
455
|
+
break;
|
|
456
|
+
}
|
|
457
|
+
case _alfe_ai_remote.RemoteFrameType.INPUT_MOUSE:
|
|
458
|
+
if (this.turn.humanInControl) {
|
|
459
|
+
const p = (0, _alfe_ai_remote.decodeJson)(frame.payload);
|
|
460
|
+
if (p) this.injector.mouse(p);
|
|
461
|
+
}
|
|
462
|
+
break;
|
|
463
|
+
case _alfe_ai_remote.RemoteFrameType.INPUT_WHEEL:
|
|
464
|
+
if (this.turn.humanInControl) {
|
|
465
|
+
const p = (0, _alfe_ai_remote.decodeJson)(frame.payload);
|
|
466
|
+
if (p) this.injector.wheel(p);
|
|
467
|
+
}
|
|
468
|
+
break;
|
|
469
|
+
case _alfe_ai_remote.RemoteFrameType.INPUT_KEY:
|
|
470
|
+
if (this.turn.humanInControl) {
|
|
471
|
+
const p = (0, _alfe_ai_remote.decodeJson)(frame.payload);
|
|
472
|
+
if (p) this.injector.key(p);
|
|
473
|
+
}
|
|
474
|
+
break;
|
|
475
|
+
case _alfe_ai_remote.RemoteFrameType.TAKEOVER_REQUEST:
|
|
476
|
+
if (this.turn.grantHuman()) this.broadcast(_alfe_ai_remote.RemoteFrameType.TAKEOVER_GRANTED);
|
|
477
|
+
else this.sendFrame((0, _alfe_ai_remote.encodeFrame)(_alfe_ai_remote.RemoteFrameType.TAKEOVER_DENIED, frame.sessionId));
|
|
478
|
+
break;
|
|
479
|
+
case _alfe_ai_remote.RemoteFrameType.RELEASE_CONTROL:
|
|
480
|
+
this.releaseToAgent();
|
|
481
|
+
break;
|
|
482
|
+
default: break;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
closeSession(sessionId) {
|
|
486
|
+
if (!this.viewers.delete(sessionId)) return;
|
|
487
|
+
this.session.removeHold();
|
|
488
|
+
if (this.viewers.size === 0) {
|
|
489
|
+
this.pump.stop();
|
|
490
|
+
if (this.turn.humanInControl) this.releaseToAgent();
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Agent tool entry point: hand control to the human and block until they
|
|
495
|
+
* release it (RELEASE_CONTROL) or `timeoutMs` elapses. Resolves with the
|
|
496
|
+
* final page URL/title so the agent resumes on the same live page.
|
|
497
|
+
*/
|
|
498
|
+
async requestHandoff(timeoutMs) {
|
|
499
|
+
this.turn.grantHuman();
|
|
500
|
+
this.broadcast(_alfe_ai_remote.RemoteFrameType.TAKEOVER_GRANTED);
|
|
501
|
+
return new Promise((resolve) => {
|
|
502
|
+
const timer = setTimeout(() => {
|
|
503
|
+
this.handoff = null;
|
|
504
|
+
this.turn.releaseHuman();
|
|
505
|
+
this.broadcast(_alfe_ai_remote.RemoteFrameType.CONTROL_REVOKED);
|
|
506
|
+
this.currentPageInfo().then(({ url, title }) => {
|
|
507
|
+
resolve({
|
|
508
|
+
released: false,
|
|
509
|
+
timedOut: true,
|
|
510
|
+
url,
|
|
511
|
+
title
|
|
512
|
+
});
|
|
513
|
+
});
|
|
514
|
+
}, timeoutMs);
|
|
515
|
+
timer.unref();
|
|
516
|
+
this.handoff = {
|
|
517
|
+
resolve,
|
|
518
|
+
timer
|
|
519
|
+
};
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
async shutdown() {
|
|
523
|
+
await this.pump.stop();
|
|
524
|
+
await this.injector.detach();
|
|
525
|
+
await this.session.shutdown();
|
|
526
|
+
}
|
|
527
|
+
releaseToAgent() {
|
|
528
|
+
this.turn.releaseHuman();
|
|
529
|
+
const waiter = this.handoff;
|
|
530
|
+
this.handoff = null;
|
|
531
|
+
if (waiter) {
|
|
532
|
+
clearTimeout(waiter.timer);
|
|
533
|
+
this.currentPageInfo().then(({ url, title }) => {
|
|
534
|
+
waiter.resolve({
|
|
535
|
+
released: true,
|
|
536
|
+
timedOut: false,
|
|
537
|
+
url,
|
|
538
|
+
title
|
|
539
|
+
});
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
async ensureStreaming() {
|
|
544
|
+
if (this.pump.isRunning || this.viewers.size === 0) return;
|
|
545
|
+
const page = await this.session.getActivePage();
|
|
546
|
+
await this.injector.attach(page);
|
|
547
|
+
this.injector.updateViewport(this.viewport);
|
|
548
|
+
await this.pump.start(page, this.viewport);
|
|
549
|
+
}
|
|
550
|
+
async applyResize(width, height, dpr) {
|
|
551
|
+
this.viewport = {
|
|
552
|
+
width,
|
|
553
|
+
height,
|
|
554
|
+
dpr
|
|
555
|
+
};
|
|
556
|
+
this.injector.updateViewport(this.viewport);
|
|
557
|
+
if (this.pump.isRunning) {
|
|
558
|
+
const page = await this.session.getActivePage();
|
|
559
|
+
await this.pump.start(page, this.viewport);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
broadcastFrame(jpeg, meta) {
|
|
563
|
+
for (const sessionId of this.viewers) this.sendFrame((0, _alfe_ai_remote.encodeScreencastFrame)(sessionId, meta, jpeg));
|
|
564
|
+
}
|
|
565
|
+
broadcast(type) {
|
|
566
|
+
for (const sessionId of this.viewers) this.sendFrame((0, _alfe_ai_remote.encodeFrame)(type, sessionId));
|
|
567
|
+
}
|
|
568
|
+
broadcastState() {
|
|
569
|
+
for (const sessionId of this.viewers) this.sendState(sessionId);
|
|
570
|
+
}
|
|
571
|
+
sendState(sessionId) {
|
|
572
|
+
this.currentPageInfo().then(({ url, title }) => {
|
|
573
|
+
const state = {
|
|
574
|
+
surface: "browser",
|
|
575
|
+
url,
|
|
576
|
+
title,
|
|
577
|
+
controller: this.turn.currentOwner
|
|
578
|
+
};
|
|
579
|
+
this.sendFrame((0, _alfe_ai_remote.encodeJsonFrame)(_alfe_ai_remote.RemoteFrameType.SESSION_STATE, sessionId, state));
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
async currentPageInfo() {
|
|
583
|
+
try {
|
|
584
|
+
const page = await this.session.getActivePage();
|
|
585
|
+
return {
|
|
586
|
+
url: page.url(),
|
|
587
|
+
title: await page.title()
|
|
588
|
+
};
|
|
589
|
+
} catch {
|
|
590
|
+
return {
|
|
591
|
+
url: "",
|
|
592
|
+
title: ""
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
};
|
|
597
|
+
//#endregion
|
|
598
|
+
exports.BrowserAutomation = BrowserAutomation;
|
|
599
|
+
exports.BrowserSession = BrowserSession;
|
|
600
|
+
exports.BrowserSurface = BrowserSurface;
|