@dyyz1993/agent-browser 0.13.2 → 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__/e2e/utils/test-helpers.d.ts +1 -0
- package/dist/__tests__/e2e/utils/test-helpers.d.ts.map +1 -1
- package/dist/__tests__/e2e/utils/test-helpers.js +14 -1
- package/dist/__tests__/e2e/utils/test-helpers.js.map +1 -1
- 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 +83 -9
- package/dist/__tests__/utils/parseCli.js.map +1 -1
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +298 -9
- package/dist/actions.js.map +1 -1
- package/dist/browser.d.ts +11 -1
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +75 -19
- package/dist/browser.js.map +1 -1
- package/dist/cli/commands.d.ts.map +1 -1
- package/dist/cli/commands.js +172 -15
- package/dist/cli/commands.js.map +1 -1
- package/dist/cli/connection.d.ts +13 -0
- package/dist/cli/connection.d.ts.map +1 -1
- package/dist/cli/connection.js +137 -48
- 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 +63 -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 +20 -2
- package/dist/cli.js.map +1 -1
- package/dist/daemon.d.ts +1 -0
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +291 -264
- package/dist/daemon.js.map +1 -1
- package/dist/diff.d.ts.map +1 -1
- package/dist/diff.js +1 -1
- package/dist/diff.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 +6 -0
- package/dist/flow/exporters/index.d.ts.map +1 -0
- package/dist/flow/exporters/index.js +5 -0
- package/dist/flow/exporters/index.js.map +1 -0
- package/dist/flow/exporters/playwright.d.ts +20 -0
- package/dist/flow/exporters/playwright.d.ts.map +1 -0
- package/dist/flow/exporters/playwright.js +175 -0
- package/dist/flow/exporters/playwright.js.map +1 -0
- package/dist/flow/exporters/python.d.ts +20 -0
- package/dist/flow/exporters/python.d.ts.map +1 -0
- package/dist/flow/exporters/python.js +163 -0
- package/dist/flow/exporters/python.js.map +1 -0
- 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/exporters/types.d.ts +13 -0
- package/dist/flow/exporters/types.d.ts.map +1 -0
- package/dist/flow/exporters/types.js +2 -0
- package/dist/flow/exporters/types.js.map +1 -0
- package/dist/flow/flow-executor.d.ts +57 -0
- package/dist/flow/flow-executor.d.ts.map +1 -0
- package/dist/flow/flow-executor.js +1263 -0
- package/dist/flow/flow-executor.js.map +1 -0
- package/dist/flow/index.d.ts +15 -0
- package/dist/flow/index.d.ts.map +1 -0
- package/dist/flow/index.js +10 -0
- package/dist/flow/index.js.map +1 -0
- package/dist/flow/output.d.ts +11 -0
- package/dist/flow/output.d.ts.map +1 -0
- package/dist/flow/output.js +84 -0
- package/dist/flow/output.js.map +1 -0
- package/dist/flow/plugin-system.d.ts +48 -0
- package/dist/flow/plugin-system.d.ts.map +1 -0
- package/dist/flow/plugin-system.js +132 -0
- package/dist/flow/plugin-system.js.map +1 -0
- package/dist/flow/plugins/file-output-plugin.d.ts +8 -0
- package/dist/flow/plugins/file-output-plugin.d.ts.map +1 -0
- package/dist/flow/plugins/file-output-plugin.js +31 -0
- package/dist/flow/plugins/file-output-plugin.js.map +1 -0
- package/dist/flow/plugins/index.d.ts +4 -0
- package/dist/flow/plugins/index.d.ts.map +1 -0
- package/dist/flow/plugins/index.js +4 -0
- package/dist/flow/plugins/index.js.map +1 -0
- package/dist/flow/plugins/logging-plugin.d.ts +7 -0
- package/dist/flow/plugins/logging-plugin.d.ts.map +1 -0
- package/dist/flow/plugins/logging-plugin.js +40 -0
- package/dist/flow/plugins/logging-plugin.js.map +1 -0
- package/dist/flow/plugins/webhook-plugin.d.ts +7 -0
- package/dist/flow/plugins/webhook-plugin.d.ts.map +1 -0
- package/dist/flow/plugins/webhook-plugin.js +24 -0
- package/dist/flow/plugins/webhook-plugin.js.map +1 -0
- package/dist/flow/presets/index.d.ts +10 -0
- package/dist/flow/presets/index.d.ts.map +1 -0
- package/dist/flow/presets/index.js +29 -0
- package/dist/flow/presets/index.js.map +1 -0
- package/dist/flow/recorder-to-flow.d.ts +70 -0
- package/dist/flow/recorder-to-flow.d.ts.map +1 -0
- package/dist/flow/recorder-to-flow.js +392 -0
- package/dist/flow/recorder-to-flow.js.map +1 -0
- package/dist/flow/site-manager.d.ts +24 -0
- package/dist/flow/site-manager.d.ts.map +1 -0
- package/dist/flow/site-manager.js +125 -0
- package/dist/flow/site-manager.js.map +1 -0
- package/dist/flow/types.d.ts +196 -0
- package/dist/flow/types.d.ts.map +1 -0
- package/dist/flow/types.js +2 -0
- package/dist/flow/types.js.map +1 -0
- package/dist/flow/yaml-parser.d.ts +15 -0
- package/dist/flow/yaml-parser.d.ts.map +1 -0
- package/dist/flow/yaml-parser.js +216 -0
- package/dist/flow/yaml-parser.js.map +1 -0
- package/dist/human-mouse.d.ts.map +1 -1
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +15 -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/recorder/inject.js +730 -332
- package/dist/snapshot-store.d.ts +83 -0
- package/dist/snapshot-store.d.ts.map +1 -0
- package/dist/snapshot-store.js +112 -0
- package/dist/snapshot-store.js.map +1 -0
- package/dist/snapshot.d.ts +6 -7
- package/dist/snapshot.d.ts.map +1 -1
- package/dist/snapshot.js +471 -17
- 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 +13 -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 +12 -6
- package/dist/viewer-script.js.map +1 -1
- package/package.json +18 -5
- package/skills/agent-browser/SKILL.md +151 -3
- 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';
|
|
@@ -15,6 +13,7 @@ let currentSession = process.env.AGENT_BROWSER_SESSION || 'default';
|
|
|
15
13
|
let currentInstanceId = randomUUID().substring(0, 8);
|
|
16
14
|
let streamServerProxy = null;
|
|
17
15
|
let lastUrl = null;
|
|
16
|
+
let lastActivityAt = Date.now();
|
|
18
17
|
const STREAM_SERVER_PID_FILE = 'stream-server.pid';
|
|
19
18
|
const INPUT_FILL_DEBOUNCE_MS = 60;
|
|
20
19
|
const inputFillDebounceMap = new Map();
|
|
@@ -59,17 +58,19 @@ export function getSession() {
|
|
|
59
58
|
export function getInstanceId() {
|
|
60
59
|
return currentInstanceId;
|
|
61
60
|
}
|
|
61
|
+
export function getLastActivityAt() {
|
|
62
|
+
return lastActivityAt;
|
|
63
|
+
}
|
|
62
64
|
/**
|
|
63
65
|
* Get port number for TCP mode (Windows)
|
|
64
66
|
* Uses a hash of the session name to get a consistent port
|
|
65
67
|
*/
|
|
66
68
|
function getPortForSession(session) {
|
|
67
|
-
let hash =
|
|
69
|
+
let hash = 2166136261;
|
|
68
70
|
for (let i = 0; i < session.length; i++) {
|
|
69
|
-
hash
|
|
70
|
-
hash
|
|
71
|
+
hash ^= session.charCodeAt(i);
|
|
72
|
+
hash = Math.imul(hash, 16777619);
|
|
71
73
|
}
|
|
72
|
-
// Port range 49152-65535 (dynamic/private ports)
|
|
73
74
|
return 49152 + (Math.abs(hash) % 16383);
|
|
74
75
|
}
|
|
75
76
|
/**
|
|
@@ -78,8 +79,9 @@ function getPortForSession(session) {
|
|
|
78
79
|
*/
|
|
79
80
|
export function getAppDir() {
|
|
80
81
|
// 1. XDG_RUNTIME_DIR (Linux standard)
|
|
81
|
-
|
|
82
|
-
|
|
82
|
+
const runtimeDir = process.env.XDG_RUNTIME_DIR;
|
|
83
|
+
if (runtimeDir) {
|
|
84
|
+
return path.join(runtimeDir, 'agent-browser');
|
|
83
85
|
}
|
|
84
86
|
// 2. Home directory fallback (like Docker Desktop's ~/.docker/run/)
|
|
85
87
|
const homeDir = os.homedir();
|
|
@@ -203,19 +205,36 @@ export function cleanupSocket(session) {
|
|
|
203
205
|
const pidFile = getPidFile(session);
|
|
204
206
|
const streamPortFile = getStreamPortFile(session);
|
|
205
207
|
try {
|
|
206
|
-
|
|
208
|
+
// Remove stale files, ignoring ENOENT (avoid TOCTOU race)
|
|
209
|
+
try {
|
|
207
210
|
fs.unlinkSync(pidFile);
|
|
208
|
-
|
|
211
|
+
}
|
|
212
|
+
catch (_e) {
|
|
213
|
+
/* not found */
|
|
214
|
+
}
|
|
215
|
+
try {
|
|
209
216
|
fs.unlinkSync(streamPortFile);
|
|
217
|
+
}
|
|
218
|
+
catch (_e) {
|
|
219
|
+
/* not found */
|
|
220
|
+
}
|
|
210
221
|
if (isWindows) {
|
|
211
222
|
const portFile = getPortFile(session);
|
|
212
|
-
|
|
223
|
+
try {
|
|
213
224
|
fs.unlinkSync(portFile);
|
|
225
|
+
}
|
|
226
|
+
catch (_e) {
|
|
227
|
+
/* not found */
|
|
228
|
+
}
|
|
214
229
|
}
|
|
215
230
|
else {
|
|
216
231
|
const socketPath = getSocketPath(session);
|
|
217
|
-
|
|
232
|
+
try {
|
|
218
233
|
fs.unlinkSync(socketPath);
|
|
234
|
+
}
|
|
235
|
+
catch (_e) {
|
|
236
|
+
/* not found */
|
|
237
|
+
}
|
|
219
238
|
}
|
|
220
239
|
}
|
|
221
240
|
catch {
|
|
@@ -236,10 +255,9 @@ export async function startDaemon(options) {
|
|
|
236
255
|
}
|
|
237
256
|
cleanupSocket();
|
|
238
257
|
const provider = options?.provider ?? process.env.AGENT_BROWSER_PROVIDER;
|
|
239
|
-
const
|
|
240
|
-
const manager = isIOS ? new IOSManager() : new BrowserManager();
|
|
258
|
+
const manager = new BrowserManager();
|
|
241
259
|
let shuttingDown = false;
|
|
242
|
-
|
|
260
|
+
{
|
|
243
261
|
const ipcPath = getStreamServerIpcPath();
|
|
244
262
|
if (fs.existsSync(ipcPath)) {
|
|
245
263
|
streamServerProxy = new StreamServerProxy(manager);
|
|
@@ -259,287 +277,296 @@ export async function startDaemon(options) {
|
|
|
259
277
|
const server = net.createServer((socket) => {
|
|
260
278
|
let buffer = '';
|
|
261
279
|
let httpChecked = false;
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
catch (err) {
|
|
302
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
303
|
-
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;
|
|
304
318
|
}
|
|
305
|
-
|
|
306
|
-
}
|
|
307
|
-
if (quickParse && action === 'input_fill' && manager instanceof BrowserManager) {
|
|
308
|
-
const selector = quickParse.selector || '';
|
|
309
|
-
const text = quickParse.text || '';
|
|
310
|
-
debounceInputFill(socket, manager, String(quickParse.id), selector, text);
|
|
311
|
-
continue;
|
|
312
|
-
}
|
|
313
|
-
if (quickParse &&
|
|
314
|
-
(action === 'blur_element' || action === 'input_blur_element') &&
|
|
315
|
-
manager instanceof BrowserManager) {
|
|
316
|
-
try {
|
|
319
|
+
if (quickParse && action === 'input_fill' && manager instanceof BrowserManager) {
|
|
317
320
|
const selector = quickParse.selector || '';
|
|
318
|
-
|
|
319
|
-
socket
|
|
320
|
-
|
|
321
|
+
const text = quickParse.text || '';
|
|
322
|
+
debounceInputFill(socket, manager, String(quickParse.id), selector, text);
|
|
323
|
+
continue;
|
|
321
324
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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;
|
|
325
339
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
}
|
|
340
|
-
|
|
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;
|
|
341
359
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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;
|
|
345
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');
|
|
346
418
|
continue;
|
|
347
419
|
}
|
|
348
|
-
if
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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,
|
|
356
464
|
});
|
|
357
|
-
socket.write(serializeResponse(successResponse(quickParse.id, { injected: true })) + '\n');
|
|
358
465
|
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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);
|
|
362
483
|
}
|
|
363
|
-
|
|
484
|
+
return;
|
|
364
485
|
}
|
|
365
|
-
|
|
366
|
-
|
|
486
|
+
// Handle inject_focus_listener: set up focus event bridge to stream-server
|
|
487
|
+
if (parseResult.command.action === 'inject_focus_listener' &&
|
|
367
488
|
manager instanceof BrowserManager) {
|
|
368
489
|
try {
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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');
|
|
372
500
|
}
|
|
373
501
|
catch (err) {
|
|
374
502
|
const message = err instanceof Error ? err.message : String(err);
|
|
375
|
-
socket.write(serializeResponse(errorResponse(
|
|
503
|
+
socket.write(serializeResponse(errorResponse(parseResult.command.id, message)) + '\n');
|
|
376
504
|
}
|
|
377
505
|
continue;
|
|
378
506
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
const devices = await iosManager.listAllDevices();
|
|
396
|
-
const response = {
|
|
397
|
-
id: parseResult.command.id,
|
|
398
|
-
success: true,
|
|
399
|
-
data: { devices },
|
|
400
|
-
};
|
|
401
|
-
socket.write(serializeResponse(response) + '\n');
|
|
402
|
-
}
|
|
403
|
-
catch (err) {
|
|
404
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
405
|
-
socket.write(serializeResponse(errorResponse(parseResult.command.id, message)) + '\n');
|
|
406
|
-
}
|
|
407
|
-
continue;
|
|
408
|
-
}
|
|
409
|
-
// Auto-launch if not already launched and this isn't a launch/close command
|
|
410
|
-
if (!manager.isLaunched() &&
|
|
411
|
-
parseResult.command.action !== 'launch' &&
|
|
412
|
-
parseResult.command.action !== 'close') {
|
|
413
|
-
if (isIOS && manager instanceof IOSManager) {
|
|
414
|
-
// Auto-launch iOS Safari
|
|
415
|
-
// Check for device in command first (for reused daemons), then fall back to env vars
|
|
416
|
-
const cmd = parseResult.command;
|
|
417
|
-
const iosDevice = cmd.iosDevice || process.env.AGENT_BROWSER_IOS_DEVICE;
|
|
418
|
-
await manager.launch({
|
|
419
|
-
device: iosDevice,
|
|
420
|
-
udid: process.env.AGENT_BROWSER_IOS_UDID,
|
|
421
|
-
});
|
|
422
|
-
}
|
|
423
|
-
else if (manager instanceof BrowserManager) {
|
|
424
|
-
// Auto-launch desktop browser
|
|
425
|
-
const extensions = process.env.AGENT_BROWSER_EXTENSIONS
|
|
426
|
-
? process.env.AGENT_BROWSER_EXTENSIONS.split(',')
|
|
427
|
-
.map((p) => p.trim())
|
|
428
|
-
.filter(Boolean)
|
|
429
|
-
: undefined;
|
|
430
|
-
// Parse args from env (comma or newline separated)
|
|
431
|
-
const argsEnv = process.env.AGENT_BROWSER_ARGS;
|
|
432
|
-
const args = argsEnv
|
|
433
|
-
? argsEnv
|
|
434
|
-
.split(/[,\n]/)
|
|
435
|
-
.map((a) => a.trim())
|
|
436
|
-
.filter((a) => a.length > 0)
|
|
437
|
-
: undefined;
|
|
438
|
-
// Parse proxy from env
|
|
439
|
-
const proxyServer = process.env.AGENT_BROWSER_PROXY;
|
|
440
|
-
const proxyBypass = process.env.AGENT_BROWSER_PROXY_BYPASS;
|
|
441
|
-
const proxy = proxyServer
|
|
442
|
-
? {
|
|
443
|
-
server: proxyServer,
|
|
444
|
-
...(proxyBypass && { bypass: proxyBypass }),
|
|
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()) {
|
|
511
|
+
try {
|
|
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
|
+
}
|
|
445
523
|
}
|
|
446
|
-
|
|
447
|
-
const ignoreHTTPSErrors = process.env.AGENT_BROWSER_IGNORE_HTTPS_ERRORS === '1';
|
|
448
|
-
const allowFileAccess = process.env.AGENT_BROWSER_ALLOW_FILE_ACCESS === '1';
|
|
449
|
-
await manager.launch({
|
|
450
|
-
id: 'auto',
|
|
451
|
-
action: 'launch',
|
|
452
|
-
headless: process.env.AGENT_BROWSER_HEADED !== '1',
|
|
453
|
-
executablePath: process.env.AGENT_BROWSER_EXECUTABLE_PATH || getExecutablePath(),
|
|
454
|
-
extensions: extensions,
|
|
455
|
-
profile: process.env.AGENT_BROWSER_PROFILE,
|
|
456
|
-
storageState: process.env.AGENT_BROWSER_STATE,
|
|
457
|
-
args,
|
|
458
|
-
userAgent: process.env.AGENT_BROWSER_USER_AGENT,
|
|
459
|
-
proxy,
|
|
460
|
-
ignoreHTTPSErrors: ignoreHTTPSErrors,
|
|
461
|
-
allowFileAccess: allowFileAccess,
|
|
462
|
-
});
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
// Handle close command specially - shuts down daemon
|
|
466
|
-
if (parseResult.command.action === 'close') {
|
|
467
|
-
const response = isIOS && manager instanceof IOSManager
|
|
468
|
-
? await executeIOSCommand(parseResult.command, manager)
|
|
469
|
-
: 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;
|
|
524
|
+
lastUrl = currentUrl;
|
|
477
525
|
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
cleanupSocket();
|
|
481
|
-
process.exit(0);
|
|
482
|
-
}, 100);
|
|
483
|
-
}
|
|
484
|
-
return;
|
|
485
|
-
}
|
|
486
|
-
// Handle inject_focus_listener: set up focus event bridge to stream-server
|
|
487
|
-
if (parseResult.command.action === 'inject_focus_listener' &&
|
|
488
|
-
manager instanceof BrowserManager) {
|
|
489
|
-
try {
|
|
490
|
-
await manager.injectFocusListener((data) => {
|
|
491
|
-
try {
|
|
492
|
-
socket.write(JSON.stringify(data) + '\n');
|
|
493
|
-
}
|
|
494
|
-
catch (_) { }
|
|
495
|
-
});
|
|
496
|
-
socket.write(serializeResponse(successResponse(parseResult.command.id, { injected: true })) +
|
|
497
|
-
'\n');
|
|
498
|
-
}
|
|
499
|
-
catch (err) {
|
|
500
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
501
|
-
socket.write(serializeResponse(errorResponse(parseResult.command.id, message)) + '\n');
|
|
502
|
-
}
|
|
503
|
-
continue;
|
|
504
|
-
}
|
|
505
|
-
// Execute command with appropriate handler
|
|
506
|
-
let response = isIOS && manager instanceof IOSManager
|
|
507
|
-
? await executeIOSCommand(parseResult.command, manager)
|
|
508
|
-
: await executeCommand(parseResult.command, manager);
|
|
509
|
-
if (response.success &&
|
|
510
|
-
!isIOS &&
|
|
511
|
-
manager instanceof BrowserManager &&
|
|
512
|
-
manager.isLaunched()) {
|
|
513
|
-
try {
|
|
514
|
-
const currentUrl = manager.getPage().url();
|
|
515
|
-
if (lastUrl !== null && currentUrl !== lastUrl) {
|
|
516
|
-
const urlTip = `URL changed: ${lastUrl} -> ${currentUrl}`;
|
|
517
|
-
const existingTips = response.tips;
|
|
518
|
-
if (existingTips) {
|
|
519
|
-
const tipsArray = Array.isArray(existingTips) ? existingTips : [existingTips];
|
|
520
|
-
response.tips = [urlTip, ...tipsArray];
|
|
521
|
-
}
|
|
522
|
-
else {
|
|
523
|
-
response.tips = [urlTip];
|
|
524
|
-
}
|
|
526
|
+
catch {
|
|
527
|
+
// Page may not be available (e.g., after close)
|
|
525
528
|
}
|
|
526
|
-
lastUrl = currentUrl;
|
|
527
|
-
}
|
|
528
|
-
catch {
|
|
529
|
-
// Page may not be available (e.g., after close)
|
|
530
529
|
}
|
|
530
|
+
socket.write(serializeResponse(response) + '\n');
|
|
531
|
+
}
|
|
532
|
+
catch (err) {
|
|
533
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
534
|
+
socket.write(serializeResponse(errorResponse('error', message)) + '\n');
|
|
531
535
|
}
|
|
532
|
-
socket.write(serializeResponse(response) + '\n');
|
|
533
536
|
}
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
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;
|
|
537
556
|
}
|
|
538
557
|
}
|
|
558
|
+
processBuffer();
|
|
539
559
|
});
|
|
540
560
|
socket.on('error', () => {
|
|
541
561
|
// Client disconnected, ignore
|
|
542
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
|
+
});
|
|
543
570
|
});
|
|
544
571
|
const pidFile = getPidFile();
|
|
545
572
|
// Write PID file before listening
|