@dyyz1993/agent-browser 0.23.0 → 0.25.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.
Files changed (127) hide show
  1. package/README.md +108 -0
  2. package/bin/agent-browser-darwin-arm64 +0 -0
  3. package/dist/__tests__/utils/free-port.d.ts +2 -0
  4. package/dist/__tests__/utils/free-port.d.ts.map +1 -0
  5. package/dist/__tests__/utils/free-port.js +18 -0
  6. package/dist/__tests__/utils/free-port.js.map +1 -0
  7. package/dist/__tests__/utils/parseCli.d.ts.map +1 -1
  8. package/dist/__tests__/utils/parseCli.js +0 -8
  9. package/dist/__tests__/utils/parseCli.js.map +1 -1
  10. package/dist/actions.d.ts.map +1 -1
  11. package/dist/actions.js +32 -12
  12. package/dist/actions.js.map +1 -1
  13. package/dist/browser.d.ts.map +1 -1
  14. package/dist/browser.js +12 -17
  15. package/dist/browser.js.map +1 -1
  16. package/dist/cli/commands.d.ts.map +1 -1
  17. package/dist/cli/commands.js +11 -13
  18. package/dist/cli/commands.js.map +1 -1
  19. package/dist/cli/connection.d.ts.map +1 -1
  20. package/dist/cli/connection.js +86 -47
  21. package/dist/cli/connection.js.map +1 -1
  22. package/dist/cli/flags.d.ts +1 -0
  23. package/dist/cli/flags.d.ts.map +1 -1
  24. package/dist/cli/flags.js +8 -1
  25. package/dist/cli/flags.js.map +1 -1
  26. package/dist/cli/help.d.ts.map +1 -1
  27. package/dist/cli/help.js +75 -23
  28. package/dist/cli/help.js.map +1 -1
  29. package/dist/cli/output.d.ts.map +1 -1
  30. package/dist/cli/output.js +0 -32
  31. package/dist/cli/output.js.map +1 -1
  32. package/dist/cli.js +150 -15
  33. package/dist/cli.js.map +1 -1
  34. package/dist/daemon.d.ts.map +1 -1
  35. package/dist/daemon.js +285 -280
  36. package/dist/daemon.js.map +1 -1
  37. package/dist/flow/exporters/cypress.d.ts +9 -0
  38. package/dist/flow/exporters/cypress.d.ts.map +1 -0
  39. package/dist/flow/exporters/cypress.js +256 -0
  40. package/dist/flow/exporters/cypress.js.map +1 -0
  41. package/dist/flow/exporters/index.d.ts +2 -0
  42. package/dist/flow/exporters/index.d.ts.map +1 -1
  43. package/dist/flow/exporters/index.js +2 -0
  44. package/dist/flow/exporters/index.js.map +1 -1
  45. package/dist/flow/exporters/selenium.d.ts +9 -0
  46. package/dist/flow/exporters/selenium.d.ts.map +1 -0
  47. package/dist/flow/exporters/selenium.js +298 -0
  48. package/dist/flow/exporters/selenium.js.map +1 -0
  49. package/dist/flow/flow-executor.d.ts +2 -0
  50. package/dist/flow/flow-executor.d.ts.map +1 -1
  51. package/dist/flow/flow-executor.js +143 -49
  52. package/dist/flow/flow-executor.js.map +1 -1
  53. package/dist/flow/index.d.ts +1 -1
  54. package/dist/flow/index.d.ts.map +1 -1
  55. package/dist/flow/index.js +1 -1
  56. package/dist/flow/index.js.map +1 -1
  57. package/dist/flow/output.js.map +1 -1
  58. package/dist/flow/plugin-system.d.ts.map +1 -1
  59. package/dist/flow/plugin-system.js.map +1 -1
  60. package/dist/flow/presets/console-capture.js +31 -0
  61. package/dist/flow/presets/fetch-capture.js +78 -0
  62. package/dist/flow/presets/sse-stream.js +67 -0
  63. package/dist/flow/presets/xhr-only.js +34 -0
  64. package/dist/flow/recorder-to-flow.js.map +1 -1
  65. package/dist/flow/site-manager.js.map +1 -1
  66. package/dist/flow/types.d.ts +15 -0
  67. package/dist/flow/types.d.ts.map +1 -1
  68. package/dist/flow/yaml-parser.d.ts.map +1 -1
  69. package/dist/flow/yaml-parser.js +2 -0
  70. package/dist/flow/yaml-parser.js.map +1 -1
  71. package/dist/human-mouse.d.ts.map +1 -1
  72. package/dist/protocol.d.ts.map +1 -1
  73. package/dist/protocol.js +1 -12
  74. package/dist/protocol.js.map +1 -1
  75. package/dist/rc-config.d.ts.map +1 -1
  76. package/dist/rc-config.js +1 -2
  77. package/dist/rc-config.js.map +1 -1
  78. package/dist/snapshot-store.d.ts +6 -0
  79. package/dist/snapshot-store.d.ts.map +1 -1
  80. package/dist/snapshot-store.js +15 -0
  81. package/dist/snapshot-store.js.map +1 -1
  82. package/dist/snapshot.d.ts.map +1 -1
  83. package/dist/snapshot.js +48 -30
  84. package/dist/snapshot.js.map +1 -1
  85. package/dist/stream-server-standalone.d.ts.map +1 -1
  86. package/dist/stream-server-standalone.js.map +1 -1
  87. package/dist/stream-server.d.ts.map +1 -1
  88. package/dist/stream-server.js +38 -13
  89. package/dist/stream-server.js.map +1 -1
  90. package/dist/test-live.js +5 -5
  91. package/dist/test-live.js.map +1 -1
  92. package/dist/types.d.ts +2 -10
  93. package/dist/types.d.ts.map +1 -1
  94. package/dist/types.js.map +1 -1
  95. package/dist/viewer-script.d.ts.map +1 -1
  96. package/dist/viewer-script.js +8 -2
  97. package/dist/viewer-script.js.map +1 -1
  98. package/package.json +12 -3
  99. package/scripts/check_goods_container.js +35 -0
  100. package/scripts/check_page_content.js +36 -0
  101. package/scripts/click_applause_rate.js +30 -0
  102. package/scripts/explore_jd_page.js +31 -0
  103. package/scripts/extract_all_jd_data.js +80 -0
  104. package/scripts/extract_jd_product_detail.js +62 -0
  105. package/scripts/extract_jd_products_correct_links.js +78 -0
  106. package/scripts/extract_jd_products_final.js +80 -0
  107. package/scripts/extract_jd_reviews.js +48 -0
  108. package/scripts/extract_jd_seafood_final.js +78 -0
  109. package/scripts/extract_multiple_products.js +77 -0
  110. package/scripts/extract_products_no_scroll.js +68 -0
  111. package/scripts/extract_products_simple.js +68 -0
  112. package/scripts/find_applause_rate.js +26 -0
  113. package/scripts/find_jd_links.js +28 -0
  114. package/scripts/find_main_content.js +20 -0
  115. package/scripts/find_product_cards.js +38 -0
  116. package/scripts/find_root_content.js +26 -0
  117. package/scripts/find_unique_products.js +55 -0
  118. package/scripts/get_jd_product_detail.js +16 -0
  119. package/scripts/get_jd_products.js +23 -0
  120. package/scripts/get_jd_seafood_products.js +44 -0
  121. package/scripts/get_product_details_from_images.js +54 -0
  122. package/scripts/verify-form.sh +67 -0
  123. package/scripts/verify-login.sh +65 -0
  124. package/scripts/verify-recording.sh +80 -0
  125. package/scripts/verify-upload.sh +41 -0
  126. package/skills/agent-browser/SKILL.md +49 -0
  127. package/bin/agent-browser-linux-x64 +0 -0
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 = 0;
69
+ let hash = 2166136261;
72
70
  for (let i = 0; i < session.length; i++) {
73
- hash = (hash << 5) - hash + session.charCodeAt(i);
74
- hash |= 0;
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
- if (process.env.XDG_RUNTIME_DIR) {
86
- return path.join(process.env.XDG_RUNTIME_DIR, 'agent-browser');
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
- if (fs.existsSync(pidFile))
208
+ // Remove stale files, ignoring ENOENT (avoid TOCTOU race)
209
+ try {
211
210
  fs.unlinkSync(pidFile);
212
- if (fs.existsSync(streamPortFile))
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
- if (fs.existsSync(portFile))
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
- if (fs.existsSync(socketPath))
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 isIOS = provider === 'ios';
244
- const manager = isIOS ? new IOSManager() : new BrowserManager();
258
+ const manager = new BrowserManager();
245
259
  let shuttingDown = false;
246
- if (!isIOS && manager instanceof BrowserManager) {
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
- socket.on('data', async (data) => {
267
- buffer += data.toString();
268
- // Security: Detect and reject HTTP requests to prevent cross-origin attacks.
269
- // Browsers using fetch() must send HTTP headers (e.g., "POST / HTTP/1.1"),
270
- // while legitimate clients send raw JSON starting with "{".
271
- if (!httpChecked) {
272
- httpChecked = true;
273
- const trimmed = buffer.trimStart();
274
- if (/^(GET|POST|PUT|DELETE|HEAD|OPTIONS|PATCH|CONNECT|TRACE)\s/i.test(trimmed)) {
275
- socket.destroy();
276
- return;
277
- }
278
- }
279
- // Process complete lines
280
- while (buffer.includes('\n')) {
281
- const newlineIdx = buffer.indexOf('\n');
282
- const line = buffer.substring(0, newlineIdx);
283
- buffer = buffer.substring(newlineIdx + 1);
284
- if (!line.trim())
285
- continue;
286
- // Handle custom actions before schema validation (not in standard Zod union)
287
- // Viewer sends messages with 'type' field, standalone commands use 'action' field.
288
- // Normalize to support both.
289
- if (line.trim()) {
290
- try {
291
- const quickParse = JSON.parse(line);
292
- const action = quickParse.action || quickParse.type;
293
- if (quickParse &&
294
- action === 'inject_focus_listener' &&
295
- manager instanceof BrowserManager) {
296
- try {
297
- await manager.injectFocusListener((data) => {
298
- try {
299
- socket.write(JSON.stringify(data) + '\n');
300
- }
301
- catch (_) { }
302
- });
303
- socket.write(serializeResponse(successResponse(quickParse.id, { injected: true })) + '\n');
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
- continue;
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
- await manager.blurElement(selector);
323
- socket.write(serializeResponse(successResponse(quickParse.id, { blurred: true, selector })) +
324
- '\n');
321
+ const text = quickParse.text || '';
322
+ debounceInputFill(socket, manager, String(quickParse.id), selector, text);
323
+ continue;
325
324
  }
326
- catch (err) {
327
- const message = err instanceof Error ? err.message : String(err);
328
- socket.write(serializeResponse(errorResponse(quickParse.id, message)) + '\n');
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
- continue;
331
- }
332
- if (quickParse && action === 'input_mouse' && manager instanceof BrowserManager) {
333
- try {
334
- await manager.injectMouseEvent({
335
- type: quickParse.eventType,
336
- x: quickParse.x ?? 0,
337
- y: quickParse.y ?? 0,
338
- button: quickParse.button,
339
- clickCount: quickParse.clickCount,
340
- deltaX: quickParse.deltaX,
341
- deltaY: quickParse.deltaY,
342
- modifiers: quickParse.modifiers,
343
- });
344
- socket.write(serializeResponse(successResponse(quickParse.id, { injected: true })) + '\n');
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
- catch (err) {
347
- const message = err instanceof Error ? err.message : String(err);
348
- socket.write(serializeResponse(errorResponse(quickParse.id, message)) + '\n');
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 (quickParse && action === 'input_keyboard' && manager instanceof BrowserManager) {
353
- try {
354
- await manager.injectKeyboardEvent({
355
- type: quickParse.eventType,
356
- key: quickParse.key,
357
- code: quickParse.code,
358
- text: quickParse.text,
359
- modifiers: quickParse.modifiers,
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
- catch (err) {
364
- const message = err instanceof Error ? err.message : String(err);
365
- socket.write(serializeResponse(errorResponse(quickParse.id, message)) + '\n');
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
- continue;
484
+ return;
368
485
  }
369
- if (quickParse &&
370
- action === 'keyboard_insert_text' &&
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
- const text = quickParse.text || '';
374
- await manager.insertText(text);
375
- socket.write(serializeResponse(successResponse(quickParse.id, { inserted: true })) + '\n');
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(quickParse.id, message)) + '\n');
503
+ socket.write(serializeResponse(errorResponse(parseResult.command.id, message)) + '\n');
380
504
  }
381
505
  continue;
382
506
  }
383
- if (quickParse && action === '_ping') {
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 tabList = !isIOS && manager instanceof BrowserManager && manager.isLaunched()
386
- ? await manager.listTabs()
387
- : [];
388
- socket.write(serializeResponse(successResponse(quickParse.id, {
389
- session: currentSession,
390
- lastActivityAt,
391
- tabs: tabList,
392
- })) + '\n');
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 (err) {
395
- const message = err instanceof Error ? err.message : String(err);
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
- lastActivityAt = Date.now();
527
- // Execute command with appropriate handler
528
- let response = isIOS && manager instanceof IOSManager
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
- catch (err) {
557
- const message = err instanceof Error ? err.message : String(err);
558
- socket.write(serializeResponse(errorResponse('error', message)) + '\n');
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