@xbrowser/cli 0.16.0 → 1.0.2
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 +17 -26
- package/dist/{browser-R7B255ML.js → browser-GITRHHFO.js} +4 -1
- package/dist/{browser-GWBH6OJK.js → browser-R56O3CW6.js} +3 -1
- package/dist/{browser-I2HJZ7IP.js → browser-ZJOZB5CR.js} +4 -2
- package/dist/cdp-driver-BE3FOMRN.js +2803 -0
- package/dist/cdp-driver-TOPYJIFL.js +47 -0
- package/dist/chunk-2SVQTI2O.js +2794 -0
- package/dist/{chunk-KDYXFLAC.js → chunk-ACFE6PKF.js} +1015 -121
- package/dist/chunk-BBMRDUYQ.js +260 -0
- package/dist/chunk-CAFNSGYM.js +4834 -0
- package/dist/{chunk-DTJRVA76.js → chunk-ETCO4SNK.js} +2 -2
- package/dist/{chunk-RS6YYWTK.js → chunk-JPA2ZT2R.js} +140 -72
- package/dist/chunk-JPHCY4TC.js +260 -0
- package/dist/chunk-KFQGP6VL.js +33 -0
- package/dist/{chunk-ITKPSIP7.js → chunk-MDAPTB7C.js} +6 -25
- package/dist/chunk-OZKD3W4X.js +417 -0
- package/dist/chunk-PPG4D2EW.js +2796 -0
- package/dist/{chunk-ATFTAKMN.js → chunk-Q4IGYTKR.js} +39 -7
- package/dist/{chunk-F3ZWFCJJ.js → chunk-QIK2I3VQ.js} +141 -72
- package/dist/chunk-WJRE55TN.js +83 -0
- package/dist/cli.js +2358 -1086
- package/dist/{convert-4DUWZIKH.js → convert-LB3GJTLR.js} +4 -2
- package/dist/{convert-EKQVHKB4.js → convert-R3XXYKC6.js} +2 -2
- package/dist/{daemon-client-GX2UYIW4.js → daemon-client-DRCUMNHK.js} +45 -72
- package/dist/{daemon-client-XWSSQBEA.js → daemon-client-UZZEHHIV.js} +8 -1
- package/dist/daemon-main.js +3067 -1688
- package/dist/{extract-JUOQQX4V.js → extract-2ZFW2MX7.js} +1 -1
- package/dist/{extract-EGRXZSSK.js → extract-BSYBM4MR.js} +2 -0
- package/dist/{filter-OLAE26HN.js → filter-KCFO4RSV.js} +2 -0
- package/dist/{filter-VID2GGZ7.js → filter-T7DSZ2X7.js} +1 -1
- package/dist/{human-interaction-W753RVJB.js → human-interaction-UKAS5ZXV.js} +2 -2
- package/dist/index.d.ts +745 -148
- package/dist/index.js +3488 -1719
- package/dist/launcher-QUJ4M2VS.js +19 -0
- package/dist/launcher-YARP45UY.js +19 -0
- package/dist/{network-store-YAF5OIBH.js → network-store-XGZ25FFC.js} +1 -0
- package/dist/{network-store-BN6QEZ7R.js → network-store-YVDNUREI.js} +1 -1
- package/dist/{parse-action-dsl-T3DYC33D.js → parse-action-dsl-UM333TL2.js} +1 -1
- package/dist/{proxy-WKGUCH2C.js → proxy-LV4BJ5RC.js} +1 -1
- package/dist/session-recorder-RTDGURIJ.js +8 -0
- package/dist/session-recorder-YI7YYM36.js +7 -0
- package/dist/session-replayer-GLTUICSD.js +276 -0
- package/dist/site-knowledge-SYC6VCDB.js +23 -0
- package/package.json +6 -6
- package/dist/chunk-2ONMTDLK.js +0 -2050
- package/dist/daemon-client-3IJD6X4B.js +0 -59
- package/dist/network-store-2S5HATEV.js +0 -194
- package/dist/parse-action-dsl-DRSPBALP.js +0 -72
- package/dist/screenshot-CWAWMXVA.js +0 -28
- package/dist/screenshot-MB6R7RSS.js +0 -26
- package/dist/session-recorder-ILSSV2UC.js +0 -6
- package/dist/session-recorder-XET3DNML.js +0 -7
|
@@ -9,12 +9,12 @@ function generateJSScript(recording) {
|
|
|
9
9
|
// Start URL: ${recording.startUrl}
|
|
10
10
|
// Events: ${events.length}
|
|
11
11
|
|
|
12
|
-
import {
|
|
12
|
+
import { launch } from '@xbrowser/cli';
|
|
13
13
|
|
|
14
14
|
const START_URL = '${escapeString(recording.startUrl)}';
|
|
15
15
|
|
|
16
16
|
async function main() {
|
|
17
|
-
const browser = await
|
|
17
|
+
const { browser } = await launch({ headless: true });
|
|
18
18
|
const context = await browser.newContext();
|
|
19
19
|
const page = await context.newPage();
|
|
20
20
|
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import {
|
|
2
|
+
launch
|
|
3
|
+
} from "./chunk-PPG4D2EW.js";
|
|
1
4
|
import {
|
|
2
5
|
CDPInterceptorProxy
|
|
3
6
|
} from "./chunk-ZZ2TFWIV.js";
|
|
@@ -7,7 +10,6 @@ import { randomUUID } from "crypto";
|
|
|
7
10
|
import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "fs";
|
|
8
11
|
import { join } from "path";
|
|
9
12
|
import { homedir } from "os";
|
|
10
|
-
import { chromium } from "playwright";
|
|
11
13
|
|
|
12
14
|
// src/utils/cdp.ts
|
|
13
15
|
async function fetchNoProxy(url) {
|
|
@@ -63,6 +65,7 @@ async function resolveCDPEndpoint(raw) {
|
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
// src/browser.ts
|
|
68
|
+
import { SessionStore } from "@dyyz1993/xcli-core";
|
|
66
69
|
function logSessionEvent(event, details) {
|
|
67
70
|
const ts = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").substring(0, 19);
|
|
68
71
|
const pid = process.pid;
|
|
@@ -75,7 +78,7 @@ function sessionFile(name) {
|
|
|
75
78
|
function ensureSessionDir() {
|
|
76
79
|
mkdirSync(SESSION_DIR, { recursive: true });
|
|
77
80
|
}
|
|
78
|
-
var sessions =
|
|
81
|
+
var sessions = new SessionStore();
|
|
79
82
|
var _sharedBrowser = null;
|
|
80
83
|
var _sharedCdpProxy = null;
|
|
81
84
|
var IDLE_TIMEOUT_MS = (process.env.XBROWSER_IDLE_TIMEOUT ? parseInt(process.env.XBROWSER_IDLE_TIMEOUT, 10) : 30) * 60 * 1e3;
|
|
@@ -86,7 +89,7 @@ function resetIdleTimer() {
|
|
|
86
89
|
const now = Date.now();
|
|
87
90
|
let allIdle = true;
|
|
88
91
|
const idleSessions = [];
|
|
89
|
-
for (const
|
|
92
|
+
for (const s of sessions) {
|
|
90
93
|
if (now - s.lastActivityAt < IDLE_TIMEOUT_MS) {
|
|
91
94
|
allIdle = false;
|
|
92
95
|
} else {
|
|
@@ -109,7 +112,7 @@ function touchSession(id) {
|
|
|
109
112
|
resetIdleTimer();
|
|
110
113
|
}
|
|
111
114
|
process.on("exit", () => {
|
|
112
|
-
for (const session of sessions.
|
|
115
|
+
for (const session of sessions.list()) {
|
|
113
116
|
if (session.isCDP) {
|
|
114
117
|
logSessionEvent("process_exit", `Session "${session.name}": CDP connection (not closing external browser).`);
|
|
115
118
|
} else {
|
|
@@ -192,15 +195,21 @@ async function createBrowser(options) {
|
|
|
192
195
|
const realEndpoint = await resolveCDPEndpoint(options.cdpEndpoint);
|
|
193
196
|
if (options.intercept) {
|
|
194
197
|
const config = typeof options.intercept === "object" ? { ...options.intercept, cdpEndpoint: realEndpoint } : { cdpEndpoint: realEndpoint };
|
|
195
|
-
|
|
196
|
-
const proxyPort = await
|
|
198
|
+
_sharedCdpProxy = new CDPInterceptorProxy(config);
|
|
199
|
+
const proxyPort = await _sharedCdpProxy.start();
|
|
197
200
|
console.error(`[CDP Interceptor] Proxy running on ws://localhost:${proxyPort}, forwarding to ${realEndpoint}`);
|
|
198
|
-
|
|
201
|
+
const { browser: browser3 } = await launch({ cdpEndpoint: `ws://localhost:${proxyPort}` });
|
|
202
|
+
return browser3;
|
|
199
203
|
}
|
|
200
|
-
|
|
204
|
+
const { browser: browser2 } = await launch({ cdpEndpoint: realEndpoint });
|
|
205
|
+
await browser2.discoverContexts().catch((err) => {
|
|
206
|
+
console.error(`[browser] discoverContexts failed: ${err.message}`);
|
|
207
|
+
});
|
|
208
|
+
return browser2;
|
|
201
209
|
}
|
|
202
210
|
const executablePath = options?.executablePath || process.env.XBROWSER_CHROMIUM_PATH || discoverChromiumPath();
|
|
203
|
-
|
|
211
|
+
const { browser } = await launch({ executablePath, headless: options?.headless ?? true });
|
|
212
|
+
return browser;
|
|
204
213
|
}
|
|
205
214
|
async function getBrowser(options) {
|
|
206
215
|
if (_sharedBrowser) return _sharedBrowser;
|
|
@@ -210,10 +219,7 @@ async function getBrowser(options) {
|
|
|
210
219
|
return _sharedBrowser;
|
|
211
220
|
}
|
|
212
221
|
function findSession(name) {
|
|
213
|
-
|
|
214
|
-
if (session.name === name) return session;
|
|
215
|
-
}
|
|
216
|
-
return void 0;
|
|
222
|
+
return sessions.find(name);
|
|
217
223
|
}
|
|
218
224
|
function getSessionById(id) {
|
|
219
225
|
return sessions.get(id);
|
|
@@ -317,9 +323,14 @@ async function findOrRestoreSession(name, cdpEndpoint) {
|
|
|
317
323
|
return void 0;
|
|
318
324
|
}
|
|
319
325
|
const targetUrl = meta.conversationUrl || meta.url;
|
|
320
|
-
if (targetUrl && page.url() !== targetUrl
|
|
321
|
-
|
|
322
|
-
|
|
326
|
+
if (targetUrl && page.url() !== targetUrl) {
|
|
327
|
+
try {
|
|
328
|
+
if (!page.url().includes(new URL(targetUrl).hostname)) {
|
|
329
|
+
await page.goto(targetUrl, { waitUntil: "domcontentloaded", timeout: 3e4 }).catch(() => {
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
} catch {
|
|
333
|
+
}
|
|
323
334
|
}
|
|
324
335
|
const session = {
|
|
325
336
|
id: meta.id || randomUUID(),
|
|
@@ -332,13 +343,13 @@ async function findOrRestoreSession(name, cdpEndpoint) {
|
|
|
332
343
|
isCDP: true,
|
|
333
344
|
cdpEndpoint: ep
|
|
334
345
|
};
|
|
335
|
-
for (const
|
|
346
|
+
for (const existingSession of sessions.list()) {
|
|
336
347
|
if (existingSession.name === name) {
|
|
337
|
-
logSessionEvent("remove_stale", `Removing stale session name="${name}" id="${
|
|
338
|
-
sessions.
|
|
348
|
+
logSessionEvent("remove_stale", `Removing stale session name="${name}" id="${existingSession.id}" during restore`);
|
|
349
|
+
sessions.removeById(existingSession.id);
|
|
339
350
|
}
|
|
340
351
|
}
|
|
341
|
-
sessions.set(session
|
|
352
|
+
sessions.set(session);
|
|
342
353
|
resetIdleTimer();
|
|
343
354
|
await installNetworkCapture(page, name);
|
|
344
355
|
return session;
|
|
@@ -351,10 +362,15 @@ async function findOrRestoreSession(name, cdpEndpoint) {
|
|
|
351
362
|
async function createEphemeralContext(options) {
|
|
352
363
|
if (options?.cdpEndpoint) {
|
|
353
364
|
const endpoint = await resolveCDPEndpoint(options.cdpEndpoint);
|
|
354
|
-
const b2 = await
|
|
365
|
+
const { browser: b2 } = await launch({ cdpEndpoint: endpoint });
|
|
355
366
|
const contexts = b2.contexts();
|
|
356
367
|
const ctx = contexts[0] || await b2.newContext();
|
|
357
|
-
const
|
|
368
|
+
const allPages = ctx.pages();
|
|
369
|
+
const existingPages = allPages.filter((p) => {
|
|
370
|
+
const url = p.url();
|
|
371
|
+
return url !== "about:blank" && !url.startsWith("chrome://");
|
|
372
|
+
});
|
|
373
|
+
const page2 = existingPages.length > 0 ? existingPages[0] : allPages.length > 0 ? allPages[0] : await ctx.newPage();
|
|
358
374
|
resetIdleTimer();
|
|
359
375
|
ephemeralConnections.set(page2, b2);
|
|
360
376
|
return { context: ctx, page: page2 };
|
|
@@ -386,38 +402,59 @@ async function closeEphemeralContext(context) {
|
|
|
386
402
|
}
|
|
387
403
|
}
|
|
388
404
|
function getAllSessions() {
|
|
389
|
-
return
|
|
405
|
+
return sessions.list();
|
|
390
406
|
}
|
|
391
407
|
async function installNetworkCapture(page, sessionName) {
|
|
392
408
|
if (process.env.XBROWSER_DAEMON_WORKER !== "1") return;
|
|
393
|
-
const { networkStore } = await import("./network-store-
|
|
394
|
-
|
|
409
|
+
const { networkStore } = await import("./network-store-YVDNUREI.js");
|
|
410
|
+
const requestData = /* @__PURE__ */ new Map();
|
|
411
|
+
const responseMeta = /* @__PURE__ */ new Map();
|
|
412
|
+
const xbPage = page;
|
|
413
|
+
xbPage.on("request", (params) => {
|
|
395
414
|
try {
|
|
396
|
-
const
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
415
|
+
const p = params;
|
|
416
|
+
requestData.set(p.requestId, {
|
|
417
|
+
method: p.request.method,
|
|
418
|
+
headers: p.request.headers,
|
|
419
|
+
postData: p.request.postData ?? null,
|
|
420
|
+
resourceType: p.type
|
|
421
|
+
});
|
|
422
|
+
} catch {
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
xbPage.on("response", (params) => {
|
|
426
|
+
try {
|
|
427
|
+
const p = params;
|
|
428
|
+
responseMeta.set(p.requestId, {
|
|
429
|
+
status: p.response.status,
|
|
430
|
+
url: p.response.url,
|
|
431
|
+
headers: p.response.headers,
|
|
432
|
+
mimeType: p.response.mimeType,
|
|
433
|
+
type: p.type
|
|
434
|
+
});
|
|
435
|
+
} catch {
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
xbPage.on("requestfinished", async (params) => {
|
|
439
|
+
try {
|
|
440
|
+
const p = params;
|
|
441
|
+
const meta = responseMeta.get(p.requestId);
|
|
442
|
+
if (!meta) return;
|
|
443
|
+
const req = requestData.get(p.requestId);
|
|
444
|
+
const method = req?.method ?? "GET";
|
|
445
|
+
const contentType = meta.headers["content-type"] || meta.headers["Content-Type"] || "";
|
|
446
|
+
const resourceType = req?.resourceType ?? meta.type;
|
|
447
|
+
const requestHeaders = req?.headers ?? {};
|
|
407
448
|
let requestBody = void 0;
|
|
408
|
-
const method = request.method();
|
|
409
449
|
const isPostLike = ["POST", "PATCH", "PUT"].includes(method);
|
|
410
450
|
if (isPostLike && requestHeaders["content-type"]?.includes("application/json")) {
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
requestBody = postData;
|
|
418
|
-
}
|
|
451
|
+
const postData = req?.postData;
|
|
452
|
+
if (postData) {
|
|
453
|
+
try {
|
|
454
|
+
requestBody = JSON.parse(postData);
|
|
455
|
+
} catch {
|
|
456
|
+
requestBody = postData;
|
|
419
457
|
}
|
|
420
|
-
} catch {
|
|
421
458
|
}
|
|
422
459
|
}
|
|
423
460
|
let responseBody = void 0;
|
|
@@ -425,7 +462,11 @@ async function installNetworkCapture(page, sessionName) {
|
|
|
425
462
|
const isJsonish = contentType.includes("json") || contentType.includes("javascript") || contentType.includes("text/");
|
|
426
463
|
if (isJsonish) {
|
|
427
464
|
try {
|
|
428
|
-
const
|
|
465
|
+
const bodyResult = await xbPage._cdpSend(
|
|
466
|
+
"Network.getResponseBody",
|
|
467
|
+
{ requestId: p.requestId }
|
|
468
|
+
);
|
|
469
|
+
const text = bodyResult.body ?? "";
|
|
429
470
|
size = text.length;
|
|
430
471
|
if (size <= 10240) {
|
|
431
472
|
try {
|
|
@@ -438,8 +479,11 @@ async function installNetworkCapture(page, sessionName) {
|
|
|
438
479
|
}
|
|
439
480
|
} else {
|
|
440
481
|
try {
|
|
441
|
-
const
|
|
442
|
-
|
|
482
|
+
const bodyResult = await xbPage._cdpSend(
|
|
483
|
+
"Network.getResponseBody",
|
|
484
|
+
{ requestId: p.requestId }
|
|
485
|
+
);
|
|
486
|
+
size = bodyResult.body?.length ?? 0;
|
|
443
487
|
} catch {
|
|
444
488
|
size = 0;
|
|
445
489
|
}
|
|
@@ -447,17 +491,19 @@ async function installNetworkCapture(page, sessionName) {
|
|
|
447
491
|
networkStore.add(sessionName, {
|
|
448
492
|
timestamp: Date.now(),
|
|
449
493
|
method,
|
|
450
|
-
url,
|
|
451
|
-
path: new URL(url).pathname,
|
|
452
|
-
status:
|
|
494
|
+
url: meta.url,
|
|
495
|
+
path: new URL(meta.url).pathname,
|
|
496
|
+
status: meta.status,
|
|
453
497
|
contentType,
|
|
454
498
|
size,
|
|
455
|
-
headers,
|
|
499
|
+
headers: meta.headers,
|
|
456
500
|
body: responseBody,
|
|
457
501
|
requestHeaders,
|
|
458
502
|
requestBody,
|
|
459
|
-
resourceType
|
|
503
|
+
resourceType
|
|
460
504
|
});
|
|
505
|
+
requestData.delete(p.requestId);
|
|
506
|
+
responseMeta.delete(p.requestId);
|
|
461
507
|
} catch {
|
|
462
508
|
}
|
|
463
509
|
});
|
|
@@ -481,16 +527,38 @@ async function createSession(name, url, options) {
|
|
|
481
527
|
}
|
|
482
528
|
context = contexts[0] || await b.newContext();
|
|
483
529
|
let targetPage = null;
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
530
|
+
const targetHostname = url ? (() => {
|
|
531
|
+
try {
|
|
532
|
+
return new URL(url).hostname;
|
|
533
|
+
} catch {
|
|
534
|
+
return "";
|
|
535
|
+
}
|
|
536
|
+
})() : "";
|
|
537
|
+
if (targetHostname) {
|
|
538
|
+
for (const ctx of contexts) {
|
|
539
|
+
const pages = ctx.pages();
|
|
540
|
+
for (const p of pages) {
|
|
541
|
+
const pUrl = p.url();
|
|
542
|
+
if (pUrl && pUrl !== "about:blank" && !pUrl.startsWith("chrome://") && pUrl.includes(targetHostname)) {
|
|
543
|
+
targetPage = p;
|
|
544
|
+
break;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
if (targetPage) break;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
if (!targetPage) {
|
|
551
|
+
for (const ctx of contexts) {
|
|
552
|
+
const pages = ctx.pages();
|
|
553
|
+
for (const p of pages) {
|
|
554
|
+
const pUrl = p.url();
|
|
555
|
+
if (pUrl && pUrl !== "about:blank" && !pUrl.startsWith("chrome://")) {
|
|
556
|
+
targetPage = p;
|
|
557
|
+
break;
|
|
558
|
+
}
|
|
491
559
|
}
|
|
560
|
+
if (targetPage) break;
|
|
492
561
|
}
|
|
493
|
-
if (targetPage) break;
|
|
494
562
|
}
|
|
495
563
|
if (!targetPage && options?.cdpEndpoint) {
|
|
496
564
|
const targets = await getCDPTargets(options.cdpEndpoint);
|
|
@@ -531,14 +599,14 @@ async function createSession(name, url, options) {
|
|
|
531
599
|
isCDP,
|
|
532
600
|
cdpEndpoint: options?.cdpEndpoint
|
|
533
601
|
};
|
|
534
|
-
sessions.set(session
|
|
602
|
+
sessions.set(session);
|
|
535
603
|
logSessionEvent("create_session", `name="${name}" id="${session.id}" url="${url || "(no url)"}" isCDP=${isCDP} cdpEndpoint=${options?.cdpEndpoint || "(none)"}`);
|
|
536
604
|
resetIdleTimer();
|
|
537
605
|
await installNetworkCapture(page, name);
|
|
538
606
|
return session;
|
|
539
607
|
}
|
|
540
608
|
async function closeSessionByName(name) {
|
|
541
|
-
for (const
|
|
609
|
+
for (const session of sessions) {
|
|
542
610
|
if (session.name === name || session.id === name) {
|
|
543
611
|
logSessionEvent("close_session", `name="${session.name}" id="${session.id}" url="${session.page.url()}"`);
|
|
544
612
|
if (session.isCDP) {
|
|
@@ -557,20 +625,20 @@ async function closeSessionByName(name) {
|
|
|
557
625
|
});
|
|
558
626
|
}
|
|
559
627
|
}
|
|
560
|
-
sessions.
|
|
628
|
+
sessions.removeById(session.id);
|
|
561
629
|
const file2 = sessionFile(session.name);
|
|
562
630
|
try {
|
|
563
631
|
unlinkSync(file2);
|
|
564
632
|
} catch {
|
|
565
633
|
}
|
|
566
634
|
try {
|
|
567
|
-
const { networkStore, commandLogStore } = await import("./network-store-
|
|
635
|
+
const { networkStore, commandLogStore } = await import("./network-store-YVDNUREI.js");
|
|
568
636
|
networkStore.clear(session.name);
|
|
569
637
|
commandLogStore.clear(session.name);
|
|
570
638
|
} catch {
|
|
571
639
|
}
|
|
572
640
|
try {
|
|
573
|
-
const { SessionRecorder } = await import("./session-recorder-
|
|
641
|
+
const { SessionRecorder } = await import("./session-recorder-RTDGURIJ.js");
|
|
574
642
|
SessionRecorder.cleanup(session.name);
|
|
575
643
|
} catch {
|
|
576
644
|
}
|
|
@@ -585,9 +653,9 @@ async function closeSessionByName(name) {
|
|
|
585
653
|
return false;
|
|
586
654
|
}
|
|
587
655
|
async function closeAllSessions() {
|
|
588
|
-
const names =
|
|
656
|
+
const names = sessions.list().map((s) => `${s.name}(${s.page.url()})`).join(", ");
|
|
589
657
|
if (names) logSessionEvent("close_all_sessions", `Closing ${sessions.size} sessions: ${names}`);
|
|
590
|
-
for (const
|
|
658
|
+
for (const session of sessions.list()) {
|
|
591
659
|
try {
|
|
592
660
|
if (!session.isCDP) {
|
|
593
661
|
await session.context.close();
|
|
@@ -596,9 +664,9 @@ async function closeAllSessions() {
|
|
|
596
664
|
await session.browser.close().catch(() => {
|
|
597
665
|
});
|
|
598
666
|
}
|
|
599
|
-
sessions.
|
|
667
|
+
sessions.removeById(session.id);
|
|
600
668
|
} catch {
|
|
601
|
-
sessions.
|
|
669
|
+
sessions.removeById(session.id);
|
|
602
670
|
}
|
|
603
671
|
}
|
|
604
672
|
}
|
|
@@ -636,7 +704,7 @@ async function ensureProcessCanExit() {
|
|
|
636
704
|
clearTimeout(idleTimer);
|
|
637
705
|
idleTimer = null;
|
|
638
706
|
}
|
|
639
|
-
for (const session of sessions.
|
|
707
|
+
for (const session of sessions.list()) {
|
|
640
708
|
if (session.browser) {
|
|
641
709
|
if (session.isCDP) {
|
|
642
710
|
await session.browser.close().catch(() => {
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__require
|
|
3
|
+
} from "./chunk-3RG5ZIWI.js";
|
|
4
|
+
|
|
5
|
+
// src/cdp-driver/launcher.ts
|
|
6
|
+
import { spawn } from "child_process";
|
|
7
|
+
import { existsSync as fsExistsSync } from "fs";
|
|
8
|
+
var DEFAULT_CHROME_PATHS = {
|
|
9
|
+
darwin: [
|
|
10
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
11
|
+
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
12
|
+
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
|
13
|
+
"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"
|
|
14
|
+
],
|
|
15
|
+
linux: [
|
|
16
|
+
"/usr/bin/google-chrome",
|
|
17
|
+
"/usr/bin/google-chrome-stable",
|
|
18
|
+
"/usr/bin/chromium",
|
|
19
|
+
"/usr/bin/chromium-browser",
|
|
20
|
+
"/usr/bin/microsoft-edge"
|
|
21
|
+
],
|
|
22
|
+
win32: [
|
|
23
|
+
"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
|
|
24
|
+
"C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
|
|
25
|
+
"C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe"
|
|
26
|
+
]
|
|
27
|
+
};
|
|
28
|
+
var DEFAULT_ARGS = [
|
|
29
|
+
"--no-first-run",
|
|
30
|
+
"--no-default-browser-check",
|
|
31
|
+
"--no-sandbox",
|
|
32
|
+
"--disable-background-networking",
|
|
33
|
+
"--disable-background-timer-throttling",
|
|
34
|
+
"--disable-backgrounding-occluded-windows",
|
|
35
|
+
"--disable-breakpad",
|
|
36
|
+
"--disable-client-side-phishing-detection",
|
|
37
|
+
"--disable-default-apps",
|
|
38
|
+
"--disable-extensions",
|
|
39
|
+
"--disable-hang-monitor",
|
|
40
|
+
"--disable-ipc-flood-protection",
|
|
41
|
+
"--disable-popup-blocking",
|
|
42
|
+
"--disable-prompt-on-repost",
|
|
43
|
+
"--disable-renderer-backgrounding",
|
|
44
|
+
"--disable-sync",
|
|
45
|
+
"--disable-translate",
|
|
46
|
+
"--metrics-recording-only",
|
|
47
|
+
"--password-store=basic",
|
|
48
|
+
"--use-mock-keychain"
|
|
49
|
+
];
|
|
50
|
+
var ANTI_DETECT_ARGS = [
|
|
51
|
+
"--disable-blink-features=AutomationControlled",
|
|
52
|
+
"--disable-features=IsolateOrigins,site-per-process"
|
|
53
|
+
];
|
|
54
|
+
function findChrome() {
|
|
55
|
+
const platform = process.platform;
|
|
56
|
+
const paths = DEFAULT_CHROME_PATHS[platform] ?? [];
|
|
57
|
+
for (const p of paths) {
|
|
58
|
+
if (fsExistsSync(p)) return p;
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
async function launchChrome(options = {}) {
|
|
63
|
+
const {
|
|
64
|
+
executablePath,
|
|
65
|
+
headless = true,
|
|
66
|
+
args: extraArgs = [],
|
|
67
|
+
userDataDir,
|
|
68
|
+
timeout = 3e4,
|
|
69
|
+
env
|
|
70
|
+
} = options;
|
|
71
|
+
const chromePath = executablePath ?? findChrome();
|
|
72
|
+
if (!chromePath) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
[
|
|
75
|
+
"Chrome/Chromium not found.",
|
|
76
|
+
"",
|
|
77
|
+
"\u63A8\u8350\uFF1A\u7528 cdp-tunnel \u590D\u7528\u4F60\u5DF2\u6709\u7684 Chrome\uFF08\u542B\u767B\u5F55\u6001\u3001\u53CD\u722C\u53CB\u597D\uFF09",
|
|
78
|
+
" npx cdp-tunnel setup # \u96F6\u5B89\u88C5\u4E00\u952E\u542F\u52A8\u4EE3\u7406 + \u52A0\u8F7D Chrome \u6269\u5C55",
|
|
79
|
+
" xbrowser goto https://example.com --cdp http://localhost:9221",
|
|
80
|
+
"",
|
|
81
|
+
"\u6216\u6307\u5B9A Chrome \u8DEF\u5F84\uFF1A",
|
|
82
|
+
' xbrowser config set browser.executablePath "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"'
|
|
83
|
+
].join("\n")
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
const port = await findFreePort();
|
|
87
|
+
const allArgs = [
|
|
88
|
+
`--remote-debugging-port=${port}`,
|
|
89
|
+
"--remote-allow-origins=*",
|
|
90
|
+
"--no-sandbox",
|
|
91
|
+
"--no-first-run",
|
|
92
|
+
"--no-default-browser-check",
|
|
93
|
+
"--disable-background-timer-throttling",
|
|
94
|
+
"--disable-backgrounding-occluded-windows",
|
|
95
|
+
"--disable-renderer-backgrounding",
|
|
96
|
+
"--disable-features=Translate",
|
|
97
|
+
"--disable-popup-blocking"
|
|
98
|
+
];
|
|
99
|
+
if (headless) {
|
|
100
|
+
allArgs.push("--headless", "--hide-scrollbars", "--mute-audio");
|
|
101
|
+
}
|
|
102
|
+
let tmpDir;
|
|
103
|
+
if (userDataDir) {
|
|
104
|
+
allArgs.push(`--user-data-dir=${userDataDir}`);
|
|
105
|
+
} else {
|
|
106
|
+
const { mkdirSync } = await import("fs");
|
|
107
|
+
tmpDir = `/tmp/xbrowser-chrome-${process.pid}-${Date.now()}`;
|
|
108
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
109
|
+
allArgs.push(`--user-data-dir=${tmpDir}`);
|
|
110
|
+
}
|
|
111
|
+
allArgs.push(...extraArgs, "about:blank");
|
|
112
|
+
const childEnv = {
|
|
113
|
+
...process.env,
|
|
114
|
+
...env
|
|
115
|
+
};
|
|
116
|
+
const quotedPath = chromePath.includes(" ") ? `"${chromePath}"` : chromePath;
|
|
117
|
+
const quotedArgs = allArgs.map((a) => {
|
|
118
|
+
if (a.includes(" ")) return `"${a}"`;
|
|
119
|
+
return a;
|
|
120
|
+
}).join(" ");
|
|
121
|
+
const fullCmd = `${quotedPath} ${quotedArgs}`;
|
|
122
|
+
const child = process.platform === "darwin" ? spawn("/bin/sh", ["-c", fullCmd], {
|
|
123
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
124
|
+
env: childEnv
|
|
125
|
+
}) : spawn(chromePath, allArgs, {
|
|
126
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
127
|
+
env: childEnv
|
|
128
|
+
});
|
|
129
|
+
const stderrLines = [];
|
|
130
|
+
child.stderr?.on("data", (data) => {
|
|
131
|
+
const line = data.toString().trim();
|
|
132
|
+
if (line) stderrLines.push(line);
|
|
133
|
+
});
|
|
134
|
+
child.stdout?.on("data", (data) => {
|
|
135
|
+
const line = data.toString().trim();
|
|
136
|
+
if (line) stderrLines.push(`[stdout] ${line}`);
|
|
137
|
+
});
|
|
138
|
+
child.on("error", (err) => {
|
|
139
|
+
if (!child.killed) {
|
|
140
|
+
console.error(`Chrome process error: ${err.message}`);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
try {
|
|
144
|
+
const wsEndpoint = await waitForCDPReady(port, timeout, child);
|
|
145
|
+
return { process: child, wsEndpoint, port, tmpDir };
|
|
146
|
+
} catch (err) {
|
|
147
|
+
const stderr = stderrLines.slice(-20).join("\n");
|
|
148
|
+
const exitInfo = child.exitCode !== null ? ` (exit code: ${child.exitCode})` : " (still running)";
|
|
149
|
+
throw new Error(`${err.message}${exitInfo}
|
|
150
|
+
Chrome stderr:
|
|
151
|
+
${stderr || "(empty)"}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
async function connectToCDP(rawEndpoint) {
|
|
155
|
+
if (rawEndpoint.startsWith("ws://") || rawEndpoint.startsWith("wss://")) {
|
|
156
|
+
return rawEndpoint;
|
|
157
|
+
}
|
|
158
|
+
return resolveEndpointFromHTTP(rawEndpoint);
|
|
159
|
+
}
|
|
160
|
+
async function findFreePort() {
|
|
161
|
+
const { createServer } = await import("net");
|
|
162
|
+
return new Promise((resolve, reject) => {
|
|
163
|
+
const srv = createServer();
|
|
164
|
+
srv.listen(0, "127.0.0.1", () => {
|
|
165
|
+
const addr = srv.address();
|
|
166
|
+
if (addr && typeof addr === "object") {
|
|
167
|
+
const port = addr.port;
|
|
168
|
+
srv.close(() => resolve(port));
|
|
169
|
+
} else {
|
|
170
|
+
srv.close();
|
|
171
|
+
reject(new Error("Failed to find free port"));
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
srv.on("error", reject);
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
async function waitForCDPReady(port, timeoutMs, child) {
|
|
178
|
+
const deadline = Date.now() + timeoutMs;
|
|
179
|
+
while (Date.now() < deadline) {
|
|
180
|
+
if (child.exitCode !== null && child.exitCode !== 0) {
|
|
181
|
+
throw new Error(`Chrome exited with code ${child.exitCode} before CDP became ready`);
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
const wsEndpoint = await resolveEndpointFromHTTP(`http://127.0.0.1:${port}`);
|
|
185
|
+
return wsEndpoint;
|
|
186
|
+
} catch {
|
|
187
|
+
await sleep(200);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
throw new Error(`Chrome CDP not ready after ${timeoutMs}ms (port ${port})`);
|
|
191
|
+
}
|
|
192
|
+
async function resolveEndpointFromHTTP(baseURL) {
|
|
193
|
+
const url = `${baseURL}/json/version`;
|
|
194
|
+
const resp = await fetch(url, { signal: AbortSignal.timeout(3e3) });
|
|
195
|
+
if (!resp.ok) {
|
|
196
|
+
throw new Error(`CDP HTTP ${resp.status}: ${url}`);
|
|
197
|
+
}
|
|
198
|
+
const data = await resp.json();
|
|
199
|
+
if (!data.webSocketDebuggerUrl) {
|
|
200
|
+
throw new Error("No webSocketDebuggerUrl in CDP response");
|
|
201
|
+
}
|
|
202
|
+
return data.webSocketDebuggerUrl;
|
|
203
|
+
}
|
|
204
|
+
async function getCDPTargets(baseURL) {
|
|
205
|
+
const url = `${baseURL}/json/list`;
|
|
206
|
+
const resp = await fetch(url, { signal: AbortSignal.timeout(5e3) });
|
|
207
|
+
if (!resp.ok) {
|
|
208
|
+
throw new Error(`CDP list HTTP ${resp.status}: ${url}`);
|
|
209
|
+
}
|
|
210
|
+
const targets = await resp.json();
|
|
211
|
+
return targets;
|
|
212
|
+
}
|
|
213
|
+
async function killChrome(child, tmpDir) {
|
|
214
|
+
if (child.exitCode !== null) {
|
|
215
|
+
if (tmpDir) cleanupTmpDir(tmpDir);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
return new Promise((resolve) => {
|
|
219
|
+
const timer = setTimeout(() => {
|
|
220
|
+
try {
|
|
221
|
+
child.kill("SIGKILL");
|
|
222
|
+
} catch {
|
|
223
|
+
}
|
|
224
|
+
if (tmpDir) cleanupTmpDir(tmpDir);
|
|
225
|
+
resolve();
|
|
226
|
+
}, 5e3);
|
|
227
|
+
child.once("exit", () => {
|
|
228
|
+
clearTimeout(timer);
|
|
229
|
+
if (tmpDir) cleanupTmpDir(tmpDir);
|
|
230
|
+
resolve();
|
|
231
|
+
});
|
|
232
|
+
try {
|
|
233
|
+
child.kill("SIGTERM");
|
|
234
|
+
} catch {
|
|
235
|
+
clearTimeout(timer);
|
|
236
|
+
if (tmpDir) cleanupTmpDir(tmpDir);
|
|
237
|
+
resolve();
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
function cleanupTmpDir(dir) {
|
|
242
|
+
try {
|
|
243
|
+
const { rmSync } = __require("fs");
|
|
244
|
+
rmSync(dir, { recursive: true, force: true });
|
|
245
|
+
} catch {
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
function sleep(ms) {
|
|
249
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export {
|
|
253
|
+
DEFAULT_ARGS,
|
|
254
|
+
ANTI_DETECT_ARGS,
|
|
255
|
+
findChrome,
|
|
256
|
+
launchChrome,
|
|
257
|
+
connectToCDP,
|
|
258
|
+
getCDPTargets,
|
|
259
|
+
killChrome
|
|
260
|
+
};
|