@dyyz1993/agent-browser 0.9.2 → 0.11.1
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/__tests__/utils/parseCli.d.ts +1 -0
- package/dist/__tests__/utils/parseCli.d.ts.map +1 -1
- package/dist/__tests__/utils/parseCli.js +18 -10
- package/dist/__tests__/utils/parseCli.js.map +1 -1
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +63 -3
- package/dist/actions.js.map +1 -1
- package/dist/browser.d.ts +46 -2
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +343 -13
- package/dist/browser.js.map +1 -1
- package/dist/cli/commands.d.ts.map +1 -1
- package/dist/cli/commands.js +8 -3
- package/dist/cli/commands.js.map +1 -1
- package/dist/cli/connection.d.ts.map +1 -1
- package/dist/cli/connection.js +39 -1
- package/dist/cli/connection.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +27 -20
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/output.d.ts.map +1 -1
- package/dist/cli/output.js +5 -0
- package/dist/cli/output.js.map +1 -1
- package/dist/cli.js +20 -0
- package/dist/cli.js.map +1 -1
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +147 -1
- package/dist/daemon.js.map +1 -1
- package/dist/message-bridge.d.ts.map +1 -1
- package/dist/message-bridge.js +22 -4
- package/dist/message-bridge.js.map +1 -1
- package/dist/openapi.d.ts +22 -0
- package/dist/openapi.d.ts.map +1 -0
- package/dist/openapi.js +382 -0
- package/dist/openapi.js.map +1 -0
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +18 -0
- package/dist/protocol.js.map +1 -1
- package/dist/recorder/inject.js +61 -134
- package/dist/stream-server-standalone.d.ts +10 -0
- package/dist/stream-server-standalone.d.ts.map +1 -1
- package/dist/stream-server-standalone.js +594 -74
- package/dist/stream-server-standalone.js.map +1 -1
- package/dist/stream-server.d.ts +67 -2
- package/dist/stream-server.d.ts.map +1 -1
- package/dist/stream-server.js +371 -51
- package/dist/stream-server.js.map +1 -1
- package/dist/swagger-ui.d.ts +6 -0
- package/dist/swagger-ui.d.ts.map +1 -0
- package/dist/swagger-ui.js +51 -0
- package/dist/swagger-ui.js.map +1 -0
- package/dist/test-live.d.ts +2 -0
- package/dist/test-live.d.ts.map +1 -0
- package/dist/test-live.js +333 -0
- package/dist/test-live.js.map +1 -0
- package/dist/types.d.ts +7 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/viewer-html.d.ts.map +1 -1
- package/dist/viewer-html.js +270 -58
- package/dist/viewer-html.js.map +1 -1
- package/dist/viewer-script.d.ts +20 -2
- package/dist/viewer-script.d.ts.map +1 -1
- package/dist/viewer-script.js +911 -154
- package/dist/viewer-script.js.map +1 -1
- package/package.json +1 -1
- package/scripts/postinstall.js +6 -32
- package/scripts/test-cli-help.sh +51 -0
- package/scripts/verify-form.sh +67 -0
- package/scripts/verify-login.sh +65 -0
- package/scripts/verify-recording.sh +80 -0
- package/scripts/verify-upload.sh +41 -0
- package/skills/agent-browser/SKILL.md +297 -160
- package/skills/agent-browser/references/commands.md +3 -0
- package/skills/agent-browser/references/mobile-viewer.md +188 -0
- package/skills/agent-browser/references/network-monitoring.md +232 -0
- package/skills/agent-browser/references/recorder.md +319 -0
- package/skills/agent-browser/references/viewer-mode.md +148 -0
- package/skills/agent-browser/templates/api-interception.sh +3 -1
- package/skills/agent-browser/templates/data-extraction.sh +8 -4
- package/skills/agent-browser/templates/form-automation.sh +18 -23
- package/skills/agent-browser/templates/network-intercept-crawl.sh +256 -0
- package/skills/agent-browser/templates/recorder-workflow.sh +51 -0
- package/skills/agent-browser/templates/viewer-remote.sh +41 -0
- package/dist/__tests__/test-iframe.d.ts +0 -2
- package/dist/__tests__/test-iframe.d.ts.map +0 -1
- package/dist/__tests__/test-iframe.js +0 -52
- package/dist/__tests__/test-iframe.js.map +0 -1
- package/dist/cli-new.d.ts +0 -3
- package/dist/cli-new.d.ts.map +0 -1
- package/dist/cli-new.js +0 -308
- package/dist/cli-new.js.map +0 -1
- package/dist/cli-old.d.ts +0 -3
- package/dist/cli-old.d.ts.map +0 -1
- package/dist/cli-old.js +0 -1101
- package/dist/cli-old.js.map +0 -1
- package/dist/recorder/binding.d.ts +0 -24
- package/dist/recorder/binding.d.ts.map +0 -1
- package/dist/recorder/binding.js +0 -215
- package/dist/recorder/binding.js.map +0 -1
- package/dist/recorder/index.d.ts +0 -4
- package/dist/recorder/index.d.ts.map +0 -1
- package/dist/recorder/index.js +0 -4
- package/dist/recorder/index.js.map +0 -1
- package/dist/recorder/recorder.d.ts +0 -19
- package/dist/recorder/recorder.d.ts.map +0 -1
- package/dist/recorder/recorder.js +0 -101
- package/dist/recorder/recorder.js.map +0 -1
- package/dist/recorder/store.d.ts +0 -22
- package/dist/recorder/store.d.ts.map +0 -1
- package/dist/recorder/store.js +0 -150
- package/dist/recorder/store.js.map +0 -1
- package/dist/recorder/types.d.ts +0 -73
- package/dist/recorder/types.d.ts.map +0 -1
- package/dist/recorder/types.js +0 -5
- package/dist/recorder/types.js.map +0 -1
package/dist/stream-server.js
CHANGED
|
@@ -8,9 +8,9 @@ import { executeCommand } from './actions.js';
|
|
|
8
8
|
import { errorResponse, serializeResponse } from './protocol.js';
|
|
9
9
|
import { getSocketDir, getSession, getInstanceId } from './daemon.js';
|
|
10
10
|
export const STATE_CONFIGS = {
|
|
11
|
-
user_interacting: { format: 'jpeg', quality: 80, maxFps: 60, scale: 0.
|
|
12
|
-
screen_moving: { format: '
|
|
13
|
-
static: { format: '
|
|
11
|
+
user_interacting: { format: 'jpeg', quality: 80, maxFps: 60, scale: 0.6 },
|
|
12
|
+
screen_moving: { format: 'jpeg', quality: 75, maxFps: 8, scale: 0.8 },
|
|
13
|
+
static: { format: 'jpeg', quality: 80, maxFps: 2, scale: 1 },
|
|
14
14
|
};
|
|
15
15
|
export class StreamStateManager {
|
|
16
16
|
currentState = 'static';
|
|
@@ -119,10 +119,26 @@ export class FrameRateController {
|
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
121
|
export class FrameProcessor {
|
|
122
|
-
|
|
122
|
+
screencastFormat = 'jpeg';
|
|
123
|
+
screencastQuality = 80;
|
|
124
|
+
async process(data, config, viewportWidth, viewportHeight, cropConfig) {
|
|
123
125
|
const buffer = Buffer.from(data, 'base64');
|
|
126
|
+
const needsResize = config.scale < 1 && viewportWidth && viewportHeight;
|
|
127
|
+
const needsCrop = !!cropConfig;
|
|
128
|
+
const needsReencode = config.format !== this.screencastFormat || config.quality !== this.screencastQuality;
|
|
129
|
+
if (!needsResize && !needsCrop && !needsReencode) {
|
|
130
|
+
return buffer;
|
|
131
|
+
}
|
|
124
132
|
let processed = sharp(buffer);
|
|
125
|
-
if (
|
|
133
|
+
if (cropConfig) {
|
|
134
|
+
processed = processed.extract({
|
|
135
|
+
left: Math.round(cropConfig.x),
|
|
136
|
+
top: Math.round(cropConfig.y),
|
|
137
|
+
width: Math.round(cropConfig.width),
|
|
138
|
+
height: Math.round(cropConfig.height),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
if (needsResize) {
|
|
126
142
|
const newWidth = Math.round(viewportWidth * config.scale);
|
|
127
143
|
const newHeight = Math.round(viewportHeight * config.scale);
|
|
128
144
|
processed = processed.resize(newWidth, newHeight);
|
|
@@ -136,6 +152,25 @@ export class FrameProcessor {
|
|
|
136
152
|
return processed.toBuffer();
|
|
137
153
|
}
|
|
138
154
|
}
|
|
155
|
+
function isPrivateIP(hostname) {
|
|
156
|
+
if (hostname === 'localhost' ||
|
|
157
|
+
hostname === '127.0.0.1' ||
|
|
158
|
+
hostname === '::1' ||
|
|
159
|
+
hostname === '[::1]') {
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
const parts = hostname.split('.').map(Number);
|
|
163
|
+
if (parts.length === 4 && parts.every((p) => !isNaN(p))) {
|
|
164
|
+
const [a, b] = parts;
|
|
165
|
+
if (a === 10)
|
|
166
|
+
return true;
|
|
167
|
+
if (a === 172 && b >= 16 && b <= 31)
|
|
168
|
+
return true;
|
|
169
|
+
if (a === 192 && b === 168)
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
139
174
|
export function isAllowedOrigin(origin) {
|
|
140
175
|
if (!origin) {
|
|
141
176
|
return true;
|
|
@@ -145,8 +180,7 @@ export function isAllowedOrigin(origin) {
|
|
|
145
180
|
}
|
|
146
181
|
try {
|
|
147
182
|
const url = new URL(origin);
|
|
148
|
-
|
|
149
|
-
if (host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '[::1]') {
|
|
183
|
+
if (isPrivateIP(url.hostname)) {
|
|
150
184
|
return true;
|
|
151
185
|
}
|
|
152
186
|
}
|
|
@@ -160,7 +194,7 @@ function isCommandMessage(msg) {
|
|
|
160
194
|
}
|
|
161
195
|
export class StreamServer {
|
|
162
196
|
wss = null;
|
|
163
|
-
clients = new
|
|
197
|
+
clients = new Map();
|
|
164
198
|
browser;
|
|
165
199
|
port;
|
|
166
200
|
isScreencasting = false;
|
|
@@ -188,7 +222,7 @@ export class StreamServer {
|
|
|
188
222
|
fps: this.frameRateController.getCurrentFps(),
|
|
189
223
|
state: newState,
|
|
190
224
|
};
|
|
191
|
-
for (const client of this.clients) {
|
|
225
|
+
for (const [client, _state] of this.clients) {
|
|
192
226
|
if (client.readyState === WebSocket.OPEN) {
|
|
193
227
|
client.send(JSON.stringify(headerMessage));
|
|
194
228
|
client.send(processedBuffer);
|
|
@@ -213,8 +247,8 @@ export class StreamServer {
|
|
|
213
247
|
return false;
|
|
214
248
|
},
|
|
215
249
|
});
|
|
216
|
-
this.wss.on('connection', (ws) => {
|
|
217
|
-
this.handleConnection(ws);
|
|
250
|
+
this.wss.on('connection', (ws, req) => {
|
|
251
|
+
this.handleConnection(ws, req);
|
|
218
252
|
});
|
|
219
253
|
this.wss.on('error', (error) => {
|
|
220
254
|
console.error('[StreamServer] WebSocket error:', error);
|
|
@@ -255,7 +289,7 @@ export class StreamServer {
|
|
|
255
289
|
}
|
|
256
290
|
setScreencastFrameCallback(null);
|
|
257
291
|
setEventCallbacks({});
|
|
258
|
-
for (const client of this.clients) {
|
|
292
|
+
for (const [client, _state] of this.clients) {
|
|
259
293
|
client.close();
|
|
260
294
|
}
|
|
261
295
|
this.clients.clear();
|
|
@@ -268,10 +302,60 @@ export class StreamServer {
|
|
|
268
302
|
});
|
|
269
303
|
}
|
|
270
304
|
}
|
|
271
|
-
handleConnection(ws) {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
305
|
+
handleConnection(ws, req) {
|
|
306
|
+
const clientState = {};
|
|
307
|
+
try {
|
|
308
|
+
const url = new URL(req.url || '/', 'http://localhost');
|
|
309
|
+
const rawSelector = url.searchParams.get('selector');
|
|
310
|
+
if (rawSelector) {
|
|
311
|
+
clientState.selector = decodeURIComponent(rawSelector);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
// Invalid URL, ignore
|
|
316
|
+
}
|
|
317
|
+
this.clients.set(ws, clientState);
|
|
318
|
+
if (clientState.selector) {
|
|
319
|
+
clientState.elementCheckTimer = setInterval(() => {
|
|
320
|
+
if (!clientState.selector)
|
|
321
|
+
return;
|
|
322
|
+
this.getElementBox(clientState.selector)
|
|
323
|
+
.then((newBox) => {
|
|
324
|
+
if (!newBox) {
|
|
325
|
+
if (clientState.elementBox || !clientState.degraded) {
|
|
326
|
+
clientState.elementBox = undefined;
|
|
327
|
+
clientState.degraded = true;
|
|
328
|
+
const statusMsg = {
|
|
329
|
+
type: 'status',
|
|
330
|
+
connected: true,
|
|
331
|
+
screencasting: this.isScreencasting,
|
|
332
|
+
degraded: true,
|
|
333
|
+
};
|
|
334
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
335
|
+
ws.send(JSON.stringify(statusMsg));
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
clientState.elementBox = newBox;
|
|
341
|
+
clientState.degraded = false;
|
|
342
|
+
}
|
|
343
|
+
})
|
|
344
|
+
.catch(() => { });
|
|
345
|
+
}, 2500);
|
|
346
|
+
}
|
|
347
|
+
this.sendStatus(ws, clientState);
|
|
348
|
+
this.refreshElementBox(ws, clientState)
|
|
349
|
+
.then(async () => {
|
|
350
|
+
this.sendStatus(ws, clientState);
|
|
351
|
+
if (this.isScreencasting && this.lastFrameData && this.lastFrameMetadata) {
|
|
352
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
353
|
+
this.replayLastFrame(ws, clientState);
|
|
354
|
+
}
|
|
355
|
+
})
|
|
356
|
+
.catch((err) => {
|
|
357
|
+
console.error('[StreamServer] Failed to refresh element box:', err);
|
|
358
|
+
});
|
|
275
359
|
if (this.clients.size === 1 && !this.isScreencasting) {
|
|
276
360
|
this.startScreencast().catch((error) => {
|
|
277
361
|
console.error('[StreamServer] Failed to start screencast:', error);
|
|
@@ -288,7 +372,10 @@ export class StreamServer {
|
|
|
288
372
|
}
|
|
289
373
|
});
|
|
290
374
|
ws.on('close', () => {
|
|
291
|
-
|
|
375
|
+
if (clientState.elementCheckTimer) {
|
|
376
|
+
clearInterval(clientState.elementCheckTimer);
|
|
377
|
+
clientState.elementCheckTimer = undefined;
|
|
378
|
+
}
|
|
292
379
|
this.clients.delete(ws);
|
|
293
380
|
if (this.clients.size === 0 && this.isScreencasting) {
|
|
294
381
|
this.stopScreencast().catch((error) => {
|
|
@@ -361,11 +448,33 @@ export class StreamServer {
|
|
|
361
448
|
this.stateManager.onUserInteraction();
|
|
362
449
|
await this.browser.getPage().keyboard.insertText(message.text);
|
|
363
450
|
break;
|
|
451
|
+
case 'input_focused':
|
|
452
|
+
case 'input_value':
|
|
453
|
+
case 'input_blur':
|
|
454
|
+
for (const [ws] of this.clients.entries()) {
|
|
455
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
456
|
+
try {
|
|
457
|
+
ws.send(JSON.stringify(message));
|
|
458
|
+
}
|
|
459
|
+
catch (_) { }
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
break;
|
|
364
463
|
case 'user_activity':
|
|
365
464
|
this.stateManager.onUserInteraction();
|
|
366
465
|
break;
|
|
367
466
|
case 'status':
|
|
368
|
-
this.
|
|
467
|
+
const cs = this.clients.get(ws);
|
|
468
|
+
if (cs?.selector) {
|
|
469
|
+
this.sendStatus(ws, cs);
|
|
470
|
+
this.refreshElementBox(ws, cs).catch(() => { });
|
|
471
|
+
}
|
|
472
|
+
else if (cs) {
|
|
473
|
+
this.sendStatus(ws, cs);
|
|
474
|
+
}
|
|
475
|
+
else {
|
|
476
|
+
this.sendStatus(ws);
|
|
477
|
+
}
|
|
369
478
|
break;
|
|
370
479
|
}
|
|
371
480
|
}
|
|
@@ -382,36 +491,97 @@ export class StreamServer {
|
|
|
382
491
|
if (!this.frameRateController.shouldSendFrame(config.maxFps)) {
|
|
383
492
|
return;
|
|
384
493
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
494
|
+
for (const [client, clientState] of this.clients) {
|
|
495
|
+
if (client.readyState !== WebSocket.OPEN) {
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
try {
|
|
499
|
+
let processedBuffer;
|
|
500
|
+
let metadata = { ...frame.metadata };
|
|
501
|
+
if (clientState.selector && clientState.elementBox) {
|
|
502
|
+
processedBuffer = await this.frameProcessor.process(frame.data, config, clientState.elementBox.width, clientState.elementBox.height, clientState.elementBox);
|
|
503
|
+
metadata.deviceWidth = clientState.elementBox.width;
|
|
504
|
+
metadata.deviceHeight = clientState.elementBox.height;
|
|
505
|
+
}
|
|
506
|
+
else {
|
|
507
|
+
processedBuffer = await this.frameProcessor.process(frame.data, config, frame.metadata.deviceWidth, frame.metadata.deviceHeight);
|
|
508
|
+
}
|
|
509
|
+
const headerMessage = {
|
|
510
|
+
type: 'frame',
|
|
511
|
+
metadata,
|
|
512
|
+
format: config.format,
|
|
513
|
+
fps: this.frameRateController.getCurrentFps(),
|
|
514
|
+
state: this.stateManager.getState(),
|
|
515
|
+
};
|
|
401
516
|
client.send(JSON.stringify(headerMessage));
|
|
402
517
|
client.send(processedBuffer);
|
|
403
518
|
}
|
|
519
|
+
catch (err) {
|
|
520
|
+
console.error('[StreamServer] Failed to process frame for client:', err);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
async replayLastFrame(ws, clientState) {
|
|
525
|
+
if (!this.lastFrameData || !this.lastFrameMetadata)
|
|
526
|
+
return;
|
|
527
|
+
if (ws.readyState !== WebSocket.OPEN)
|
|
528
|
+
return;
|
|
529
|
+
try {
|
|
530
|
+
const config = this.stateManager.getConfig();
|
|
531
|
+
let processedBuffer;
|
|
532
|
+
let metadata = { ...this.lastFrameMetadata };
|
|
533
|
+
if (clientState.selector && clientState.elementBox) {
|
|
534
|
+
processedBuffer = await this.frameProcessor.process(this.lastFrameData, config, clientState.elementBox.width, clientState.elementBox.height, clientState.elementBox);
|
|
535
|
+
metadata.deviceWidth = clientState.elementBox.width;
|
|
536
|
+
metadata.deviceHeight = clientState.elementBox.height;
|
|
537
|
+
}
|
|
538
|
+
else {
|
|
539
|
+
processedBuffer = await this.frameProcessor.process(this.lastFrameData, config, this.lastFrameMetadata.deviceWidth, this.lastFrameMetadata.deviceHeight);
|
|
540
|
+
}
|
|
541
|
+
const headerMessage = {
|
|
542
|
+
type: 'frame',
|
|
543
|
+
metadata,
|
|
544
|
+
format: config.format,
|
|
545
|
+
fps: this.frameRateController.getCurrentFps(),
|
|
546
|
+
state: this.stateManager.getState(),
|
|
547
|
+
};
|
|
548
|
+
ws.send(JSON.stringify(headerMessage));
|
|
549
|
+
ws.send(processedBuffer);
|
|
550
|
+
}
|
|
551
|
+
catch (err) {
|
|
552
|
+
console.error('[StreamServer] Failed to replay last frame:', err);
|
|
404
553
|
}
|
|
405
554
|
}
|
|
406
555
|
broadcastEvent(message) {
|
|
407
556
|
const payload = JSON.stringify(message);
|
|
408
|
-
for (const client of this.clients) {
|
|
557
|
+
for (const [client, _state] of this.clients) {
|
|
409
558
|
if (client.readyState === WebSocket.OPEN) {
|
|
410
559
|
client.send(payload);
|
|
411
560
|
}
|
|
412
561
|
}
|
|
413
562
|
}
|
|
414
|
-
|
|
563
|
+
async getElementBox(selector) {
|
|
564
|
+
try {
|
|
565
|
+
const page = this.browser.getPage();
|
|
566
|
+
const box = await page.evaluate((sel) => {
|
|
567
|
+
const el = document.querySelector(sel);
|
|
568
|
+
if (!el)
|
|
569
|
+
return null;
|
|
570
|
+
const rect = el.getBoundingClientRect();
|
|
571
|
+
return {
|
|
572
|
+
x: rect.x,
|
|
573
|
+
y: rect.y,
|
|
574
|
+
width: rect.width,
|
|
575
|
+
height: rect.height,
|
|
576
|
+
};
|
|
577
|
+
}, selector);
|
|
578
|
+
return box ?? undefined;
|
|
579
|
+
}
|
|
580
|
+
catch {
|
|
581
|
+
return undefined;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
sendStatus(ws, clientState) {
|
|
415
585
|
let viewportWidth;
|
|
416
586
|
let viewportHeight;
|
|
417
587
|
try {
|
|
@@ -420,9 +590,7 @@ export class StreamServer {
|
|
|
420
590
|
viewportWidth = viewport?.width;
|
|
421
591
|
viewportHeight = viewport?.height;
|
|
422
592
|
}
|
|
423
|
-
catch {
|
|
424
|
-
// Browser not launched yet
|
|
425
|
-
}
|
|
593
|
+
catch { }
|
|
426
594
|
const message = {
|
|
427
595
|
type: 'status',
|
|
428
596
|
connected: true,
|
|
@@ -431,11 +599,48 @@ export class StreamServer {
|
|
|
431
599
|
viewportHeight,
|
|
432
600
|
fps: this.frameRateController.getCurrentFps(),
|
|
433
601
|
state: this.stateManager.getState(),
|
|
602
|
+
version: process.env.npm_package_version || '0.9.5',
|
|
434
603
|
};
|
|
604
|
+
if (clientState?.elementBox) {
|
|
605
|
+
message.element = {
|
|
606
|
+
selector: clientState.selector,
|
|
607
|
+
x: clientState.elementBox.x,
|
|
608
|
+
y: clientState.elementBox.y,
|
|
609
|
+
width: clientState.elementBox.width,
|
|
610
|
+
height: clientState.elementBox.height,
|
|
611
|
+
};
|
|
612
|
+
message.viewportWidth = clientState.elementBox.width;
|
|
613
|
+
message.viewportHeight = clientState.elementBox.height;
|
|
614
|
+
}
|
|
615
|
+
if (clientState?.degraded) {
|
|
616
|
+
message.degraded = true;
|
|
617
|
+
}
|
|
435
618
|
if (ws.readyState === WebSocket.OPEN) {
|
|
436
619
|
ws.send(JSON.stringify(message));
|
|
437
620
|
}
|
|
438
621
|
}
|
|
622
|
+
async refreshElementBox(ws, clientState) {
|
|
623
|
+
if (!clientState.selector)
|
|
624
|
+
return;
|
|
625
|
+
const box = await this.getElementBox(clientState.selector);
|
|
626
|
+
if (box) {
|
|
627
|
+
clientState.elementBox = box;
|
|
628
|
+
clientState.degraded = false;
|
|
629
|
+
}
|
|
630
|
+
else if (!clientState.degraded) {
|
|
631
|
+
clientState.elementBox = undefined;
|
|
632
|
+
clientState.degraded = true;
|
|
633
|
+
const message = {
|
|
634
|
+
type: 'status',
|
|
635
|
+
connected: true,
|
|
636
|
+
screencasting: this.isScreencasting,
|
|
637
|
+
degraded: true,
|
|
638
|
+
};
|
|
639
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
640
|
+
ws.send(JSON.stringify(message));
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
439
644
|
sendError(ws, errorMessage) {
|
|
440
645
|
const message = {
|
|
441
646
|
type: 'error',
|
|
@@ -453,16 +658,58 @@ export class StreamServer {
|
|
|
453
658
|
if (!this.browser.isLaunched()) {
|
|
454
659
|
throw new Error('Browser not launched');
|
|
455
660
|
}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
661
|
+
try {
|
|
662
|
+
await this.browser.startScreencast((frame) => this.broadcastFrame(frame), {
|
|
663
|
+
format: 'jpeg',
|
|
664
|
+
quality: 80,
|
|
665
|
+
maxWidth: 1280,
|
|
666
|
+
maxHeight: 720,
|
|
667
|
+
everyNthFrame: 1,
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
catch (startError) {
|
|
671
|
+
if (startError instanceof Error && startError.message === 'Screencast already active') {
|
|
672
|
+
await this.browser.stopScreencast();
|
|
673
|
+
await this.browser.startScreencast((frame) => this.broadcastFrame(frame), {
|
|
674
|
+
format: 'jpeg',
|
|
675
|
+
quality: 80,
|
|
676
|
+
maxWidth: 1280,
|
|
677
|
+
maxHeight: 720,
|
|
678
|
+
everyNthFrame: 1,
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
else {
|
|
682
|
+
throw startError;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
for (const [client, state] of this.clients) {
|
|
686
|
+
this.sendStatus(client, state);
|
|
465
687
|
}
|
|
688
|
+
try {
|
|
689
|
+
await this.browser.injectFocusListener((data) => {
|
|
690
|
+
for (const [ws] of this.clients.entries()) {
|
|
691
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
692
|
+
try {
|
|
693
|
+
ws.send(JSON.stringify(data));
|
|
694
|
+
}
|
|
695
|
+
catch (_) { }
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
catch (e) {
|
|
701
|
+
console.error('[StreamServer] Failed to inject focus listener:', e);
|
|
702
|
+
}
|
|
703
|
+
try {
|
|
704
|
+
const page = this.browser.getPage();
|
|
705
|
+
await page.evaluate(() => {
|
|
706
|
+
document.body.style.opacity = '0.999';
|
|
707
|
+
requestAnimationFrame(() => {
|
|
708
|
+
document.body.style.opacity = '';
|
|
709
|
+
});
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
catch { }
|
|
466
713
|
}
|
|
467
714
|
catch (error) {
|
|
468
715
|
this.isScreencasting = false;
|
|
@@ -472,10 +719,10 @@ export class StreamServer {
|
|
|
472
719
|
async stopScreencast() {
|
|
473
720
|
if (!this.isScreencasting)
|
|
474
721
|
return;
|
|
475
|
-
await this.browser.stopScreencast();
|
|
476
722
|
this.isScreencasting = false;
|
|
477
|
-
|
|
478
|
-
|
|
723
|
+
await this.browser.stopScreencast();
|
|
724
|
+
for (const [client, state] of this.clients) {
|
|
725
|
+
this.sendStatus(client, state);
|
|
479
726
|
}
|
|
480
727
|
}
|
|
481
728
|
getPort() {
|
|
@@ -507,6 +754,8 @@ export class StreamServerProxy {
|
|
|
507
754
|
reconnectTimer = null;
|
|
508
755
|
lastFrameData = null;
|
|
509
756
|
lastFrameMetadata = null;
|
|
757
|
+
inputFillDebounceMap = new Map();
|
|
758
|
+
static INPUT_FILL_DEBOUNCE_MS = 60;
|
|
510
759
|
constructor(browser) {
|
|
511
760
|
this.browser = browser;
|
|
512
761
|
this.ipcPath = getStreamServerIpcPath();
|
|
@@ -587,6 +836,8 @@ export class StreamServerProxy {
|
|
|
587
836
|
case 'keyboard_down':
|
|
588
837
|
case 'keyboard_up':
|
|
589
838
|
case 'keyboard_insert_text':
|
|
839
|
+
case 'input_fill':
|
|
840
|
+
case 'input_blur_element':
|
|
590
841
|
case 'user_activity':
|
|
591
842
|
this.handleInputMessage(message);
|
|
592
843
|
break;
|
|
@@ -596,6 +847,25 @@ export class StreamServerProxy {
|
|
|
596
847
|
case 'client_disconnected':
|
|
597
848
|
this.handleClientDisconnected(message.session);
|
|
598
849
|
break;
|
|
850
|
+
case 'request_element_box':
|
|
851
|
+
(async () => {
|
|
852
|
+
const box = await this.getElementBox(message.selector);
|
|
853
|
+
const response = {
|
|
854
|
+
type: 'selector_element',
|
|
855
|
+
session: message.session ?? this.session,
|
|
856
|
+
selector: message.selector,
|
|
857
|
+
};
|
|
858
|
+
if (box) {
|
|
859
|
+
response.elementBox = box;
|
|
860
|
+
}
|
|
861
|
+
this.send(response);
|
|
862
|
+
})();
|
|
863
|
+
break;
|
|
864
|
+
case 'input_focused':
|
|
865
|
+
case 'input_value':
|
|
866
|
+
case 'input_blur':
|
|
867
|
+
this.send(message);
|
|
868
|
+
break;
|
|
599
869
|
}
|
|
600
870
|
}
|
|
601
871
|
catch (error) {
|
|
@@ -673,6 +943,23 @@ export class StreamServerProxy {
|
|
|
673
943
|
this.stateManager.onUserInteraction();
|
|
674
944
|
await this.browser.getPage().keyboard.insertText(message.text);
|
|
675
945
|
break;
|
|
946
|
+
case 'input_fill': {
|
|
947
|
+
const sel = message.selector || '';
|
|
948
|
+
const txt = message.text || '';
|
|
949
|
+
const key = sel || '__global__';
|
|
950
|
+
const prev = this.inputFillDebounceMap.get(key);
|
|
951
|
+
if (prev)
|
|
952
|
+
clearTimeout(prev.timer);
|
|
953
|
+
const timer = setTimeout(async () => {
|
|
954
|
+
this.inputFillDebounceMap.delete(key);
|
|
955
|
+
await this.browser.fillValue(sel, txt);
|
|
956
|
+
}, StreamServerProxy.INPUT_FILL_DEBOUNCE_MS);
|
|
957
|
+
this.inputFillDebounceMap.set(key, { timer, text: txt, selector: sel });
|
|
958
|
+
break;
|
|
959
|
+
}
|
|
960
|
+
case 'input_blur_element':
|
|
961
|
+
await this.browser.blurElement(message.selector || '');
|
|
962
|
+
break;
|
|
676
963
|
case 'user_activity':
|
|
677
964
|
this.stateManager.onUserInteraction();
|
|
678
965
|
break;
|
|
@@ -733,6 +1020,39 @@ export class StreamServerProxy {
|
|
|
733
1020
|
this.ipcSocket.write(JSON.stringify(message) + '\n');
|
|
734
1021
|
}
|
|
735
1022
|
}
|
|
1023
|
+
sendSelectorElement(selector, elementBox) {
|
|
1024
|
+
this.send({
|
|
1025
|
+
type: 'selector_element',
|
|
1026
|
+
session: this.session,
|
|
1027
|
+
selector,
|
|
1028
|
+
elementBox,
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
async getElementBox(selector) {
|
|
1032
|
+
try {
|
|
1033
|
+
const page = this.browser.getPage();
|
|
1034
|
+
if (!page) {
|
|
1035
|
+
return undefined;
|
|
1036
|
+
}
|
|
1037
|
+
const box = await page.evaluate((sel) => {
|
|
1038
|
+
const el = document.querySelector(sel);
|
|
1039
|
+
if (!el)
|
|
1040
|
+
return null;
|
|
1041
|
+
const rect = el.getBoundingClientRect();
|
|
1042
|
+
return {
|
|
1043
|
+
x: rect.x,
|
|
1044
|
+
y: rect.y,
|
|
1045
|
+
width: rect.width,
|
|
1046
|
+
height: rect.height,
|
|
1047
|
+
};
|
|
1048
|
+
}, selector);
|
|
1049
|
+
return box ?? undefined;
|
|
1050
|
+
}
|
|
1051
|
+
catch (err) {
|
|
1052
|
+
console.error('[StreamServerProxy] getElementBox error:', err);
|
|
1053
|
+
return undefined;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
736
1056
|
getDaemonSocketPath() {
|
|
737
1057
|
const isWindows = process.platform === 'win32';
|
|
738
1058
|
if (isWindows) {
|