@dyyz1993/agent-browser 0.23.0 → 0.24.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/README.md +108 -0
- package/bin/agent-browser-darwin-arm64 +0 -0
- package/bin/agent-browser-darwin-x64 +0 -0
- package/bin/agent-browser-linux-arm64 +0 -0
- package/bin/agent-browser-linux-x64 +0 -0
- package/bin/agent-browser-win32-x64.exe +0 -0
- package/dist/__tests__/utils/free-port.d.ts +2 -0
- package/dist/__tests__/utils/free-port.d.ts.map +1 -0
- package/dist/__tests__/utils/free-port.js +18 -0
- package/dist/__tests__/utils/free-port.js.map +1 -0
- package/dist/__tests__/utils/parseCli.d.ts.map +1 -1
- package/dist/__tests__/utils/parseCli.js +0 -8
- package/dist/__tests__/utils/parseCli.js.map +1 -1
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +30 -10
- package/dist/actions.js.map +1 -1
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +12 -17
- package/dist/browser.js.map +1 -1
- package/dist/cli/commands.d.ts.map +1 -1
- package/dist/cli/commands.js +7 -14
- package/dist/cli/commands.js.map +1 -1
- package/dist/cli/connection.d.ts.map +1 -1
- package/dist/cli/connection.js +86 -47
- package/dist/cli/connection.js.map +1 -1
- package/dist/cli/flags.d.ts.map +1 -1
- package/dist/cli/flags.js +0 -1
- package/dist/cli/flags.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +24 -22
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/output.d.ts.map +1 -1
- package/dist/cli/output.js +0 -32
- package/dist/cli/output.js.map +1 -1
- package/dist/cli.js +0 -1
- package/dist/cli.js.map +1 -1
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +285 -280
- package/dist/daemon.js.map +1 -1
- package/dist/flow/exporters/cypress.d.ts +9 -0
- package/dist/flow/exporters/cypress.d.ts.map +1 -0
- package/dist/flow/exporters/cypress.js +256 -0
- package/dist/flow/exporters/cypress.js.map +1 -0
- package/dist/flow/exporters/index.d.ts +2 -0
- package/dist/flow/exporters/index.d.ts.map +1 -1
- package/dist/flow/exporters/index.js +2 -0
- package/dist/flow/exporters/index.js.map +1 -1
- package/dist/flow/exporters/selenium.d.ts +9 -0
- package/dist/flow/exporters/selenium.d.ts.map +1 -0
- package/dist/flow/exporters/selenium.js +298 -0
- package/dist/flow/exporters/selenium.js.map +1 -0
- package/dist/flow/flow-executor.d.ts +2 -0
- package/dist/flow/flow-executor.d.ts.map +1 -1
- package/dist/flow/flow-executor.js +143 -49
- package/dist/flow/flow-executor.js.map +1 -1
- package/dist/flow/index.d.ts +1 -1
- package/dist/flow/index.d.ts.map +1 -1
- package/dist/flow/index.js +1 -1
- package/dist/flow/index.js.map +1 -1
- package/dist/flow/output.js.map +1 -1
- package/dist/flow/plugin-system.d.ts.map +1 -1
- package/dist/flow/plugin-system.js.map +1 -1
- package/dist/flow/recorder-to-flow.js.map +1 -1
- package/dist/flow/site-manager.js.map +1 -1
- package/dist/flow/types.d.ts +15 -0
- package/dist/flow/types.d.ts.map +1 -1
- package/dist/flow/yaml-parser.d.ts.map +1 -1
- package/dist/flow/yaml-parser.js +2 -0
- package/dist/flow/yaml-parser.js.map +1 -1
- package/dist/human-mouse.d.ts.map +1 -1
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +0 -11
- package/dist/protocol.js.map +1 -1
- package/dist/rc-config.d.ts.map +1 -1
- package/dist/rc-config.js +1 -2
- package/dist/rc-config.js.map +1 -1
- package/dist/snapshot-store.d.ts +6 -0
- package/dist/snapshot-store.d.ts.map +1 -1
- package/dist/snapshot-store.js +15 -0
- package/dist/snapshot-store.js.map +1 -1
- package/dist/snapshot.d.ts.map +1 -1
- package/dist/snapshot.js +48 -30
- package/dist/snapshot.js.map +1 -1
- package/dist/stream-server-standalone.d.ts.map +1 -1
- package/dist/stream-server-standalone.js.map +1 -1
- package/dist/stream-server.d.ts.map +1 -1
- package/dist/stream-server.js +38 -13
- package/dist/stream-server.js.map +1 -1
- package/dist/test-live.js +5 -5
- package/dist/test-live.js.map +1 -1
- package/dist/types.d.ts +1 -9
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/viewer-script.d.ts.map +1 -1
- package/dist/viewer-script.js +8 -2
- package/dist/viewer-script.js.map +1 -1
- package/package.json +12 -3
- package/skills/agent-browser/SKILL.md +49 -0
- package/dist/ios-actions.d.ts +0 -11
- package/dist/ios-actions.d.ts.map +0 -1
- package/dist/ios-actions.js +0 -228
- package/dist/ios-actions.js.map +0 -1
- package/dist/ios-manager.d.ts +0 -266
- package/dist/ios-manager.d.ts.map +0 -1
- package/dist/ios-manager.js +0 -1076
- package/dist/ios-manager.js.map +0 -1
package/dist/daemon.js
CHANGED
|
@@ -4,10 +4,8 @@ import * as path from 'path';
|
|
|
4
4
|
import * as os from 'os';
|
|
5
5
|
import { randomUUID } from 'crypto';
|
|
6
6
|
import { BrowserManager } from './browser.js';
|
|
7
|
-
import { IOSManager } from './ios-manager.js';
|
|
8
7
|
import { parseCommand, serializeResponse, errorResponse, successResponse } from './protocol.js';
|
|
9
8
|
import { executeCommand } from './actions.js';
|
|
10
|
-
import { executeIOSCommand } from './ios-actions.js';
|
|
11
9
|
import { getExecutablePath } from './rc-config.js';
|
|
12
10
|
import { StreamServerProxy, getStreamServerIpcPath } from './stream-server.js';
|
|
13
11
|
const isWindows = process.platform === 'win32';
|
|
@@ -68,12 +66,11 @@ export function getLastActivityAt() {
|
|
|
68
66
|
* Uses a hash of the session name to get a consistent port
|
|
69
67
|
*/
|
|
70
68
|
function getPortForSession(session) {
|
|
71
|
-
let hash =
|
|
69
|
+
let hash = 2166136261;
|
|
72
70
|
for (let i = 0; i < session.length; i++) {
|
|
73
|
-
hash
|
|
74
|
-
hash
|
|
71
|
+
hash ^= session.charCodeAt(i);
|
|
72
|
+
hash = Math.imul(hash, 16777619);
|
|
75
73
|
}
|
|
76
|
-
// Port range 49152-65535 (dynamic/private ports)
|
|
77
74
|
return 49152 + (Math.abs(hash) % 16383);
|
|
78
75
|
}
|
|
79
76
|
/**
|
|
@@ -82,8 +79,9 @@ function getPortForSession(session) {
|
|
|
82
79
|
*/
|
|
83
80
|
export function getAppDir() {
|
|
84
81
|
// 1. XDG_RUNTIME_DIR (Linux standard)
|
|
85
|
-
|
|
86
|
-
|
|
82
|
+
const runtimeDir = process.env.XDG_RUNTIME_DIR;
|
|
83
|
+
if (runtimeDir) {
|
|
84
|
+
return path.join(runtimeDir, 'agent-browser');
|
|
87
85
|
}
|
|
88
86
|
// 2. Home directory fallback (like Docker Desktop's ~/.docker/run/)
|
|
89
87
|
const homeDir = os.homedir();
|
|
@@ -207,19 +205,36 @@ export function cleanupSocket(session) {
|
|
|
207
205
|
const pidFile = getPidFile(session);
|
|
208
206
|
const streamPortFile = getStreamPortFile(session);
|
|
209
207
|
try {
|
|
210
|
-
|
|
208
|
+
// Remove stale files, ignoring ENOENT (avoid TOCTOU race)
|
|
209
|
+
try {
|
|
211
210
|
fs.unlinkSync(pidFile);
|
|
212
|
-
|
|
211
|
+
}
|
|
212
|
+
catch (_e) {
|
|
213
|
+
/* not found */
|
|
214
|
+
}
|
|
215
|
+
try {
|
|
213
216
|
fs.unlinkSync(streamPortFile);
|
|
217
|
+
}
|
|
218
|
+
catch (_e) {
|
|
219
|
+
/* not found */
|
|
220
|
+
}
|
|
214
221
|
if (isWindows) {
|
|
215
222
|
const portFile = getPortFile(session);
|
|
216
|
-
|
|
223
|
+
try {
|
|
217
224
|
fs.unlinkSync(portFile);
|
|
225
|
+
}
|
|
226
|
+
catch (_e) {
|
|
227
|
+
/* not found */
|
|
228
|
+
}
|
|
218
229
|
}
|
|
219
230
|
else {
|
|
220
231
|
const socketPath = getSocketPath(session);
|
|
221
|
-
|
|
232
|
+
try {
|
|
222
233
|
fs.unlinkSync(socketPath);
|
|
234
|
+
}
|
|
235
|
+
catch (_e) {
|
|
236
|
+
/* not found */
|
|
237
|
+
}
|
|
223
238
|
}
|
|
224
239
|
}
|
|
225
240
|
catch {
|
|
@@ -240,10 +255,9 @@ export async function startDaemon(options) {
|
|
|
240
255
|
}
|
|
241
256
|
cleanupSocket();
|
|
242
257
|
const provider = options?.provider ?? process.env.AGENT_BROWSER_PROVIDER;
|
|
243
|
-
const
|
|
244
|
-
const manager = isIOS ? new IOSManager() : new BrowserManager();
|
|
258
|
+
const manager = new BrowserManager();
|
|
245
259
|
let shuttingDown = false;
|
|
246
|
-
|
|
260
|
+
{
|
|
247
261
|
const ipcPath = getStreamServerIpcPath();
|
|
248
262
|
if (fs.existsSync(ipcPath)) {
|
|
249
263
|
streamServerProxy = new StreamServerProxy(manager);
|
|
@@ -263,305 +277,296 @@ export async function startDaemon(options) {
|
|
|
263
277
|
const server = net.createServer((socket) => {
|
|
264
278
|
let buffer = '';
|
|
265
279
|
let httpChecked = false;
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
}
|
|
305
|
-
catch (err) {
|
|
306
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
307
|
-
socket.write(serializeResponse(errorResponse(quickParse.id, message)) + '\n');
|
|
280
|
+
let processing = false;
|
|
281
|
+
async function processBuffer() {
|
|
282
|
+
if (processing)
|
|
283
|
+
return;
|
|
284
|
+
processing = true;
|
|
285
|
+
try {
|
|
286
|
+
while (buffer.includes('\n')) {
|
|
287
|
+
const newlineIdx = buffer.indexOf('\n');
|
|
288
|
+
const line = buffer.substring(0, newlineIdx);
|
|
289
|
+
buffer = buffer.substring(newlineIdx + 1);
|
|
290
|
+
if (!line.trim())
|
|
291
|
+
continue;
|
|
292
|
+
// Handle custom actions before schema validation (not in standard Zod union)
|
|
293
|
+
// Viewer sends messages with 'type' field, standalone commands use 'action' field.
|
|
294
|
+
// Normalize to support both.
|
|
295
|
+
if (line.trim()) {
|
|
296
|
+
try {
|
|
297
|
+
const quickParse = JSON.parse(line);
|
|
298
|
+
const action = quickParse.action || quickParse.type;
|
|
299
|
+
if (quickParse &&
|
|
300
|
+
action === 'inject_focus_listener' &&
|
|
301
|
+
manager instanceof BrowserManager) {
|
|
302
|
+
try {
|
|
303
|
+
await manager.injectFocusListener((data) => {
|
|
304
|
+
try {
|
|
305
|
+
socket.write(JSON.stringify(data) + '\n');
|
|
306
|
+
}
|
|
307
|
+
catch (_e) {
|
|
308
|
+
// Socket write to disconnected client, non-fatal
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
socket.write(serializeResponse(successResponse(quickParse.id, { injected: true })) + '\n');
|
|
312
|
+
}
|
|
313
|
+
catch (err) {
|
|
314
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
315
|
+
socket.write(serializeResponse(errorResponse(quickParse.id, message)) + '\n');
|
|
316
|
+
}
|
|
317
|
+
continue;
|
|
308
318
|
}
|
|
309
|
-
|
|
310
|
-
}
|
|
311
|
-
if (quickParse && action === 'input_fill' && manager instanceof BrowserManager) {
|
|
312
|
-
const selector = quickParse.selector || '';
|
|
313
|
-
const text = quickParse.text || '';
|
|
314
|
-
debounceInputFill(socket, manager, String(quickParse.id), selector, text);
|
|
315
|
-
continue;
|
|
316
|
-
}
|
|
317
|
-
if (quickParse &&
|
|
318
|
-
(action === 'blur_element' || action === 'input_blur_element') &&
|
|
319
|
-
manager instanceof BrowserManager) {
|
|
320
|
-
try {
|
|
319
|
+
if (quickParse && action === 'input_fill' && manager instanceof BrowserManager) {
|
|
321
320
|
const selector = quickParse.selector || '';
|
|
322
|
-
|
|
323
|
-
socket
|
|
324
|
-
|
|
321
|
+
const text = quickParse.text || '';
|
|
322
|
+
debounceInputFill(socket, manager, String(quickParse.id), selector, text);
|
|
323
|
+
continue;
|
|
325
324
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
325
|
+
if (quickParse &&
|
|
326
|
+
(action === 'blur_element' || action === 'input_blur_element') &&
|
|
327
|
+
manager instanceof BrowserManager) {
|
|
328
|
+
try {
|
|
329
|
+
const selector = quickParse.selector || '';
|
|
330
|
+
await manager.blurElement(selector);
|
|
331
|
+
socket.write(serializeResponse(successResponse(quickParse.id, { blurred: true, selector })) +
|
|
332
|
+
'\n');
|
|
333
|
+
}
|
|
334
|
+
catch (err) {
|
|
335
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
336
|
+
socket.write(serializeResponse(errorResponse(quickParse.id, message)) + '\n');
|
|
337
|
+
}
|
|
338
|
+
continue;
|
|
329
339
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
}
|
|
344
|
-
|
|
340
|
+
if (quickParse && action === 'input_mouse' && manager instanceof BrowserManager) {
|
|
341
|
+
try {
|
|
342
|
+
await manager.injectMouseEvent({
|
|
343
|
+
type: quickParse.eventType,
|
|
344
|
+
x: quickParse.x ?? 0,
|
|
345
|
+
y: quickParse.y ?? 0,
|
|
346
|
+
button: quickParse.button,
|
|
347
|
+
clickCount: quickParse.clickCount,
|
|
348
|
+
deltaX: quickParse.deltaX,
|
|
349
|
+
deltaY: quickParse.deltaY,
|
|
350
|
+
modifiers: quickParse.modifiers,
|
|
351
|
+
});
|
|
352
|
+
socket.write(serializeResponse(successResponse(quickParse.id, { injected: true })) + '\n');
|
|
353
|
+
}
|
|
354
|
+
catch (err) {
|
|
355
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
356
|
+
socket.write(serializeResponse(errorResponse(quickParse.id, message)) + '\n');
|
|
357
|
+
}
|
|
358
|
+
continue;
|
|
345
359
|
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
360
|
+
if (quickParse && action === 'input_keyboard' && manager instanceof BrowserManager) {
|
|
361
|
+
try {
|
|
362
|
+
await manager.injectKeyboardEvent({
|
|
363
|
+
type: quickParse.eventType,
|
|
364
|
+
key: quickParse.key,
|
|
365
|
+
code: quickParse.code,
|
|
366
|
+
text: quickParse.text,
|
|
367
|
+
modifiers: quickParse.modifiers,
|
|
368
|
+
});
|
|
369
|
+
socket.write(serializeResponse(successResponse(quickParse.id, { injected: true })) + '\n');
|
|
370
|
+
}
|
|
371
|
+
catch (err) {
|
|
372
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
373
|
+
socket.write(serializeResponse(errorResponse(quickParse.id, message)) + '\n');
|
|
374
|
+
}
|
|
375
|
+
continue;
|
|
349
376
|
}
|
|
377
|
+
if (quickParse &&
|
|
378
|
+
action === 'keyboard_insert_text' &&
|
|
379
|
+
manager instanceof BrowserManager) {
|
|
380
|
+
try {
|
|
381
|
+
const text = quickParse.text || '';
|
|
382
|
+
await manager.insertText(text);
|
|
383
|
+
socket.write(serializeResponse(successResponse(quickParse.id, { inserted: true })) + '\n');
|
|
384
|
+
}
|
|
385
|
+
catch (err) {
|
|
386
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
387
|
+
socket.write(serializeResponse(errorResponse(quickParse.id, message)) + '\n');
|
|
388
|
+
}
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
if (quickParse && action === '_ping') {
|
|
392
|
+
try {
|
|
393
|
+
const tabList = manager instanceof BrowserManager && manager.isLaunched()
|
|
394
|
+
? await manager.listTabs()
|
|
395
|
+
: [];
|
|
396
|
+
socket.write(serializeResponse(successResponse(quickParse.id, {
|
|
397
|
+
session: currentSession,
|
|
398
|
+
lastActivityAt,
|
|
399
|
+
tabs: tabList,
|
|
400
|
+
})) + '\n');
|
|
401
|
+
}
|
|
402
|
+
catch (err) {
|
|
403
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
404
|
+
socket.write(serializeResponse(errorResponse(quickParse.id, message)) + '\n');
|
|
405
|
+
}
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
catch (_) {
|
|
410
|
+
/* not JSON, fall through to normal parsing */
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
try {
|
|
414
|
+
const parseResult = parseCommand(line);
|
|
415
|
+
if (!parseResult.success) {
|
|
416
|
+
const resp = errorResponse(parseResult.id ?? 'unknown', parseResult.error);
|
|
417
|
+
socket.write(serializeResponse(resp) + '\n');
|
|
350
418
|
continue;
|
|
351
419
|
}
|
|
352
|
-
if
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
420
|
+
// Auto-launch if not already launched and this isn't a launch/close command
|
|
421
|
+
if (!manager.isLaunched() &&
|
|
422
|
+
parseResult.command.action !== 'launch' &&
|
|
423
|
+
parseResult.command.action !== 'close') {
|
|
424
|
+
if (manager instanceof BrowserManager) {
|
|
425
|
+
// Auto-launch desktop browser
|
|
426
|
+
const extensions = process.env.AGENT_BROWSER_EXTENSIONS
|
|
427
|
+
? (process.env.AGENT_BROWSER_EXTENSIONS || '')
|
|
428
|
+
.split(',')
|
|
429
|
+
.map((p) => p.trim())
|
|
430
|
+
.filter(Boolean)
|
|
431
|
+
: undefined;
|
|
432
|
+
// Parse args from env (comma or newline separated)
|
|
433
|
+
const argsEnv = process.env.AGENT_BROWSER_ARGS;
|
|
434
|
+
const args = argsEnv
|
|
435
|
+
? argsEnv
|
|
436
|
+
.split(/[,\n]/)
|
|
437
|
+
.map((a) => a.trim())
|
|
438
|
+
.filter((a) => a.length > 0)
|
|
439
|
+
: undefined;
|
|
440
|
+
// Parse proxy from env
|
|
441
|
+
const proxyServer = process.env.AGENT_BROWSER_PROXY;
|
|
442
|
+
const proxyBypass = process.env.AGENT_BROWSER_PROXY_BYPASS;
|
|
443
|
+
const proxy = proxyServer
|
|
444
|
+
? {
|
|
445
|
+
server: proxyServer,
|
|
446
|
+
...(proxyBypass && { bypass: proxyBypass }),
|
|
447
|
+
}
|
|
448
|
+
: undefined;
|
|
449
|
+
const ignoreHTTPSErrors = process.env.AGENT_BROWSER_IGNORE_HTTPS_ERRORS === '1';
|
|
450
|
+
const allowFileAccess = process.env.AGENT_BROWSER_ALLOW_FILE_ACCESS === '1';
|
|
451
|
+
await manager.launch({
|
|
452
|
+
id: 'auto',
|
|
453
|
+
action: 'launch',
|
|
454
|
+
headless: process.env.AGENT_BROWSER_HEADED !== '1',
|
|
455
|
+
executablePath: process.env.AGENT_BROWSER_EXECUTABLE_PATH || getExecutablePath(),
|
|
456
|
+
extensions: extensions,
|
|
457
|
+
profile: process.env.AGENT_BROWSER_PROFILE,
|
|
458
|
+
storageState: process.env.AGENT_BROWSER_STATE,
|
|
459
|
+
args,
|
|
460
|
+
userAgent: process.env.AGENT_BROWSER_USER_AGENT,
|
|
461
|
+
proxy,
|
|
462
|
+
ignoreHTTPSErrors: ignoreHTTPSErrors,
|
|
463
|
+
allowFileAccess: allowFileAccess,
|
|
360
464
|
});
|
|
361
|
-
socket.write(serializeResponse(successResponse(quickParse.id, { injected: true })) + '\n');
|
|
362
465
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
466
|
+
}
|
|
467
|
+
// Handle close command specially - shuts down daemon
|
|
468
|
+
if (parseResult.command.action === 'close') {
|
|
469
|
+
const response = await executeCommand(parseResult.command, manager);
|
|
470
|
+
socket.write(serializeResponse(response) + '\n');
|
|
471
|
+
if (!shuttingDown) {
|
|
472
|
+
shuttingDown = true;
|
|
473
|
+
// 先断开 StreamServer 连接,发送 unregister 消息
|
|
474
|
+
if (streamServerProxy) {
|
|
475
|
+
await streamServerProxy.disconnect();
|
|
476
|
+
streamServerProxy = null;
|
|
477
|
+
}
|
|
478
|
+
setTimeout(() => {
|
|
479
|
+
server.close();
|
|
480
|
+
cleanupSocket();
|
|
481
|
+
process.exit(0);
|
|
482
|
+
}, 100);
|
|
366
483
|
}
|
|
367
|
-
|
|
484
|
+
return;
|
|
368
485
|
}
|
|
369
|
-
|
|
370
|
-
|
|
486
|
+
// Handle inject_focus_listener: set up focus event bridge to stream-server
|
|
487
|
+
if (parseResult.command.action === 'inject_focus_listener' &&
|
|
371
488
|
manager instanceof BrowserManager) {
|
|
372
489
|
try {
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
490
|
+
await manager.injectFocusListener((data) => {
|
|
491
|
+
try {
|
|
492
|
+
socket.write(JSON.stringify(data) + '\n');
|
|
493
|
+
}
|
|
494
|
+
catch (_e) {
|
|
495
|
+
// Socket write to disconnected client, non-fatal
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
socket.write(serializeResponse(successResponse(parseResult.command.id, { injected: true })) +
|
|
499
|
+
'\n');
|
|
376
500
|
}
|
|
377
501
|
catch (err) {
|
|
378
502
|
const message = err instanceof Error ? err.message : String(err);
|
|
379
|
-
socket.write(serializeResponse(errorResponse(
|
|
503
|
+
socket.write(serializeResponse(errorResponse(parseResult.command.id, message)) + '\n');
|
|
380
504
|
}
|
|
381
505
|
continue;
|
|
382
506
|
}
|
|
383
|
-
|
|
507
|
+
lastActivityAt = Date.now();
|
|
508
|
+
// Execute command with appropriate handler
|
|
509
|
+
let response = await executeCommand(parseResult.command, manager);
|
|
510
|
+
if (response.success && manager instanceof BrowserManager && manager.isLaunched()) {
|
|
384
511
|
try {
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
:
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
512
|
+
const currentUrl = manager.getPage().url();
|
|
513
|
+
if (lastUrl !== null && currentUrl !== lastUrl) {
|
|
514
|
+
const urlTip = `URL changed: ${lastUrl} -> ${currentUrl}`;
|
|
515
|
+
const existingTips = response.tips;
|
|
516
|
+
if (existingTips) {
|
|
517
|
+
const tipsArray = Array.isArray(existingTips) ? existingTips : [existingTips];
|
|
518
|
+
response.tips = [urlTip, ...tipsArray];
|
|
519
|
+
}
|
|
520
|
+
else {
|
|
521
|
+
response.tips = [urlTip];
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
lastUrl = currentUrl;
|
|
393
525
|
}
|
|
394
|
-
catch
|
|
395
|
-
|
|
396
|
-
socket.write(serializeResponse(errorResponse(quickParse.id, message)) + '\n');
|
|
526
|
+
catch {
|
|
527
|
+
// Page may not be available (e.g., after close)
|
|
397
528
|
}
|
|
398
|
-
continue;
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
catch (_) {
|
|
402
|
-
/* not JSON, fall through to normal parsing */
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
try {
|
|
406
|
-
const parseResult = parseCommand(line);
|
|
407
|
-
if (!parseResult.success) {
|
|
408
|
-
const resp = errorResponse(parseResult.id ?? 'unknown', parseResult.error);
|
|
409
|
-
socket.write(serializeResponse(resp) + '\n');
|
|
410
|
-
continue;
|
|
411
|
-
}
|
|
412
|
-
// Handle device_list specially - it works without a session and always uses IOSManager
|
|
413
|
-
if (parseResult.command.action === 'device_list') {
|
|
414
|
-
const iosManager = new IOSManager();
|
|
415
|
-
try {
|
|
416
|
-
const devices = await iosManager.listAllDevices();
|
|
417
|
-
const response = {
|
|
418
|
-
id: parseResult.command.id,
|
|
419
|
-
success: true,
|
|
420
|
-
data: { devices },
|
|
421
|
-
};
|
|
422
|
-
socket.write(serializeResponse(response) + '\n');
|
|
423
529
|
}
|
|
424
|
-
catch (err) {
|
|
425
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
426
|
-
socket.write(serializeResponse(errorResponse(parseResult.command.id, message)) + '\n');
|
|
427
|
-
}
|
|
428
|
-
continue;
|
|
429
|
-
}
|
|
430
|
-
// Auto-launch if not already launched and this isn't a launch/close command
|
|
431
|
-
if (!manager.isLaunched() &&
|
|
432
|
-
parseResult.command.action !== 'launch' &&
|
|
433
|
-
parseResult.command.action !== 'close') {
|
|
434
|
-
if (isIOS && manager instanceof IOSManager) {
|
|
435
|
-
// Auto-launch iOS Safari
|
|
436
|
-
// Check for device in command first (for reused daemons), then fall back to env vars
|
|
437
|
-
const cmd = parseResult.command;
|
|
438
|
-
const iosDevice = cmd.iosDevice || process.env.AGENT_BROWSER_IOS_DEVICE;
|
|
439
|
-
await manager.launch({
|
|
440
|
-
device: iosDevice,
|
|
441
|
-
udid: process.env.AGENT_BROWSER_IOS_UDID,
|
|
442
|
-
});
|
|
443
|
-
}
|
|
444
|
-
else if (manager instanceof BrowserManager) {
|
|
445
|
-
// Auto-launch desktop browser
|
|
446
|
-
const extensions = process.env.AGENT_BROWSER_EXTENSIONS
|
|
447
|
-
? process.env.AGENT_BROWSER_EXTENSIONS.split(',')
|
|
448
|
-
.map((p) => p.trim())
|
|
449
|
-
.filter(Boolean)
|
|
450
|
-
: undefined;
|
|
451
|
-
// Parse args from env (comma or newline separated)
|
|
452
|
-
const argsEnv = process.env.AGENT_BROWSER_ARGS;
|
|
453
|
-
const args = argsEnv
|
|
454
|
-
? argsEnv
|
|
455
|
-
.split(/[,\n]/)
|
|
456
|
-
.map((a) => a.trim())
|
|
457
|
-
.filter((a) => a.length > 0)
|
|
458
|
-
: undefined;
|
|
459
|
-
// Parse proxy from env
|
|
460
|
-
const proxyServer = process.env.AGENT_BROWSER_PROXY;
|
|
461
|
-
const proxyBypass = process.env.AGENT_BROWSER_PROXY_BYPASS;
|
|
462
|
-
const proxy = proxyServer
|
|
463
|
-
? {
|
|
464
|
-
server: proxyServer,
|
|
465
|
-
...(proxyBypass && { bypass: proxyBypass }),
|
|
466
|
-
}
|
|
467
|
-
: undefined;
|
|
468
|
-
const ignoreHTTPSErrors = process.env.AGENT_BROWSER_IGNORE_HTTPS_ERRORS === '1';
|
|
469
|
-
const allowFileAccess = process.env.AGENT_BROWSER_ALLOW_FILE_ACCESS === '1';
|
|
470
|
-
await manager.launch({
|
|
471
|
-
id: 'auto',
|
|
472
|
-
action: 'launch',
|
|
473
|
-
headless: process.env.AGENT_BROWSER_HEADED !== '1',
|
|
474
|
-
executablePath: process.env.AGENT_BROWSER_EXECUTABLE_PATH || getExecutablePath(),
|
|
475
|
-
extensions: extensions,
|
|
476
|
-
profile: process.env.AGENT_BROWSER_PROFILE,
|
|
477
|
-
storageState: process.env.AGENT_BROWSER_STATE,
|
|
478
|
-
args,
|
|
479
|
-
userAgent: process.env.AGENT_BROWSER_USER_AGENT,
|
|
480
|
-
proxy,
|
|
481
|
-
ignoreHTTPSErrors: ignoreHTTPSErrors,
|
|
482
|
-
allowFileAccess: allowFileAccess,
|
|
483
|
-
});
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
// Handle close command specially - shuts down daemon
|
|
487
|
-
if (parseResult.command.action === 'close') {
|
|
488
|
-
const response = isIOS && manager instanceof IOSManager
|
|
489
|
-
? await executeIOSCommand(parseResult.command, manager)
|
|
490
|
-
: await executeCommand(parseResult.command, manager);
|
|
491
530
|
socket.write(serializeResponse(response) + '\n');
|
|
492
|
-
if (!shuttingDown) {
|
|
493
|
-
shuttingDown = true;
|
|
494
|
-
// 先断开 StreamServer 连接,发送 unregister 消息
|
|
495
|
-
if (streamServerProxy) {
|
|
496
|
-
await streamServerProxy.disconnect();
|
|
497
|
-
streamServerProxy = null;
|
|
498
|
-
}
|
|
499
|
-
setTimeout(() => {
|
|
500
|
-
server.close();
|
|
501
|
-
cleanupSocket();
|
|
502
|
-
process.exit(0);
|
|
503
|
-
}, 100);
|
|
504
|
-
}
|
|
505
|
-
return;
|
|
506
|
-
}
|
|
507
|
-
// Handle inject_focus_listener: set up focus event bridge to stream-server
|
|
508
|
-
if (parseResult.command.action === 'inject_focus_listener' &&
|
|
509
|
-
manager instanceof BrowserManager) {
|
|
510
|
-
try {
|
|
511
|
-
await manager.injectFocusListener((data) => {
|
|
512
|
-
try {
|
|
513
|
-
socket.write(JSON.stringify(data) + '\n');
|
|
514
|
-
}
|
|
515
|
-
catch (_) { }
|
|
516
|
-
});
|
|
517
|
-
socket.write(serializeResponse(successResponse(parseResult.command.id, { injected: true })) +
|
|
518
|
-
'\n');
|
|
519
|
-
}
|
|
520
|
-
catch (err) {
|
|
521
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
522
|
-
socket.write(serializeResponse(errorResponse(parseResult.command.id, message)) + '\n');
|
|
523
|
-
}
|
|
524
|
-
continue;
|
|
525
531
|
}
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
? await executeIOSCommand(parseResult.command, manager)
|
|
530
|
-
: await executeCommand(parseResult.command, manager);
|
|
531
|
-
if (response.success &&
|
|
532
|
-
!isIOS &&
|
|
533
|
-
manager instanceof BrowserManager &&
|
|
534
|
-
manager.isLaunched()) {
|
|
535
|
-
try {
|
|
536
|
-
const currentUrl = manager.getPage().url();
|
|
537
|
-
if (lastUrl !== null && currentUrl !== lastUrl) {
|
|
538
|
-
const urlTip = `URL changed: ${lastUrl} -> ${currentUrl}`;
|
|
539
|
-
const existingTips = response.tips;
|
|
540
|
-
if (existingTips) {
|
|
541
|
-
const tipsArray = Array.isArray(existingTips) ? existingTips : [existingTips];
|
|
542
|
-
response.tips = [urlTip, ...tipsArray];
|
|
543
|
-
}
|
|
544
|
-
else {
|
|
545
|
-
response.tips = [urlTip];
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
lastUrl = currentUrl;
|
|
549
|
-
}
|
|
550
|
-
catch {
|
|
551
|
-
// Page may not be available (e.g., after close)
|
|
552
|
-
}
|
|
532
|
+
catch (err) {
|
|
533
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
534
|
+
socket.write(serializeResponse(errorResponse('error', message)) + '\n');
|
|
553
535
|
}
|
|
554
|
-
socket.write(serializeResponse(response) + '\n');
|
|
555
536
|
}
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
537
|
+
}
|
|
538
|
+
finally {
|
|
539
|
+
processing = false;
|
|
540
|
+
if (buffer.includes('\n')) {
|
|
541
|
+
processBuffer();
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
socket.on('data', (data) => {
|
|
546
|
+
buffer += data.toString();
|
|
547
|
+
// Security: Detect and reject HTTP requests to prevent cross-origin attacks.
|
|
548
|
+
// Browsers using fetch() must send HTTP headers (e.g., "POST / HTTP/1.1"),
|
|
549
|
+
// while legitimate clients send raw JSON starting with "{".
|
|
550
|
+
if (!httpChecked) {
|
|
551
|
+
httpChecked = true;
|
|
552
|
+
const trimmed = buffer.trimStart();
|
|
553
|
+
if (/^(GET|POST|PUT|DELETE|HEAD|OPTIONS|PATCH|CONNECT|TRACE)\s/i.test(trimmed)) {
|
|
554
|
+
socket.destroy();
|
|
555
|
+
return;
|
|
559
556
|
}
|
|
560
557
|
}
|
|
558
|
+
processBuffer();
|
|
561
559
|
});
|
|
562
560
|
socket.on('error', () => {
|
|
563
561
|
// Client disconnected, ignore
|
|
564
562
|
});
|
|
563
|
+
socket.on('close', () => {
|
|
564
|
+
// Clear any pending debounce timers to prevent writes to closed socket
|
|
565
|
+
for (const [key, entry] of inputFillDebounceMap.entries()) {
|
|
566
|
+
clearTimeout(entry.timer);
|
|
567
|
+
inputFillDebounceMap.delete(key);
|
|
568
|
+
}
|
|
569
|
+
});
|
|
565
570
|
});
|
|
566
571
|
const pidFile = getPidFile();
|
|
567
572
|
// Write PID file before listening
|