@xbrowser/cli 1.10.0 → 1.11.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/dist/{anti-bot-DR56Y63V.js → anti-bot-GTTYNEFB.js} +1 -1
- package/dist/{browser-T3V3JWVH.js → browser-Q5APBNF6.js} +1 -1
- package/dist/{browser-HBL72GPZ.js → browser-V3JIWSTR.js} +2 -2
- package/dist/{browser-WQZ3D6AE.js → browser-XNU7JQYC.js} +3 -2
- package/dist/{cdp-driver-J3YQ4LJV.js → cdp-driver-GD6YBBGE.js} +1 -1
- package/dist/cdp-driver-VEK6BNN6.js +49 -0
- package/dist/{cdp-driver-2T4P4ZE2.js → cdp-driver-XFTTZI5O.js} +436 -56
- package/dist/chunk-3FWLW7FS.js +106 -0
- package/dist/chunk-7OFM755Z.js +3509 -0
- package/dist/{chunk-5UGR6MUK.js → chunk-HBHOKZPN.js} +51 -16
- package/dist/{chunk-BAHSRZIX.js → chunk-HVPUVVSA.js} +13 -10
- package/dist/{chunk-NITFVWWS.js → chunk-JEDP4PJW.js} +448 -65
- package/dist/{chunk-INTQPBYF.js → chunk-JKVUFP3G.js} +6 -2
- package/dist/{chunk-4452SPFI.js → chunk-NODRQGOK.js} +51 -16
- package/dist/{chunk-YMUSHPU4.js → chunk-WHPBNUKB.js} +35 -1
- package/dist/{chunk-WSCP7QCJ.js → chunk-XKQVUFUS.js} +13 -10
- package/dist/{chunk-RRBXV7KE.js → chunk-XQ4HRPDJ.js} +384 -102
- package/dist/cli.js +72 -26
- package/dist/{daemon-client-GKEPT4NY.js → daemon-client-MMDRYCF5.js} +35 -1
- package/dist/{daemon-client-O6BYVRXV.js → daemon-client-Y2YSZCGK.js} +1 -1
- package/dist/daemon-main.js +89 -31
- package/dist/index.d.ts +8 -0
- package/dist/index.js +73 -27
- package/dist/{session-recorder-H3KEYU26.js → session-recorder-3BEVWHOK.js} +1 -1
- package/dist/{session-recorder-QRZMKFVL.js → session-recorder-SLDBENVF.js} +1 -1
- package/dist/{session-replayer-LJUC4TI7.js → session-replayer-WIUYVN5J.js} +90 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
detectAntiBot,
|
|
3
3
|
formatDetectionMessage
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-JKVUFP3G.js";
|
|
5
5
|
import {
|
|
6
6
|
forwardCommandLog,
|
|
7
7
|
forwardNetworkAnalyze,
|
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
forwardSessionList,
|
|
26
26
|
forwardViewerCheckSelector,
|
|
27
27
|
version
|
|
28
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-WHPBNUKB.js";
|
|
29
29
|
import {
|
|
30
30
|
buildViewerUrl
|
|
31
31
|
} from "./chunk-3OD76SUE.js";
|
|
@@ -66,7 +66,7 @@ import {
|
|
|
66
66
|
} from "./chunk-ANVL2ID2.js";
|
|
67
67
|
import {
|
|
68
68
|
SessionRecorder
|
|
69
|
-
} from "./chunk-
|
|
69
|
+
} from "./chunk-NODRQGOK.js";
|
|
70
70
|
import {
|
|
71
71
|
addKnownIssue,
|
|
72
72
|
getKnowledgePath,
|
|
@@ -91,8 +91,8 @@ import {
|
|
|
91
91
|
resolveLaunchOpts,
|
|
92
92
|
saveSessionDiskMeta,
|
|
93
93
|
setActivePage
|
|
94
|
-
} from "./chunk-
|
|
95
|
-
import "./chunk-
|
|
94
|
+
} from "./chunk-XKQVUFUS.js";
|
|
95
|
+
import "./chunk-7OFM755Z.js";
|
|
96
96
|
import "./chunk-TNEN6VQ2.js";
|
|
97
97
|
import {
|
|
98
98
|
errMsg
|
|
@@ -491,14 +491,23 @@ var CAPTCHA_SELECTORS = {
|
|
|
491
491
|
generic: '#captcha, .captcha-container, [class*="captcha"]'
|
|
492
492
|
};
|
|
493
493
|
async function detectCaptcha(page) {
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
494
|
+
const hit = await page.evaluate(`
|
|
495
|
+
(function() {
|
|
496
|
+
var S = ${JSON.stringify(CAPTCHA_SELECTORS)};
|
|
497
|
+
for (var type in S) {
|
|
498
|
+
var sel = S[type];
|
|
499
|
+
var el;
|
|
500
|
+
try { el = document.querySelector(sel); } catch (e) { continue; }
|
|
501
|
+
if (el) {
|
|
502
|
+
var url = el.getAttribute && el.getAttribute('src');
|
|
503
|
+
return { type: type, selector: sel, iframeUrl: url || undefined };
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return null;
|
|
507
|
+
})()
|
|
508
|
+
`).catch(() => null);
|
|
509
|
+
if (!hit) return null;
|
|
510
|
+
return { type: hit.type, selector: hit.selector, iframeUrl: hit.iframeUrl || void 0 };
|
|
502
511
|
}
|
|
503
512
|
async function waitForCaptchaSolved(page, captchaInfo, timeoutMs = 18e4) {
|
|
504
513
|
const startTime = Date.now();
|
|
@@ -549,12 +558,24 @@ var clickCommand = registerCommand({
|
|
|
549
558
|
let detectedNewPage;
|
|
550
559
|
let cleanup;
|
|
551
560
|
if (ctx.browserContext?.on) {
|
|
561
|
+
const targetIdOf = (p2) => p2._targetId;
|
|
562
|
+
let targetIdsBefore;
|
|
563
|
+
try {
|
|
564
|
+
targetIdsBefore = new Set(ctx.browserContext.pages().map(targetIdOf));
|
|
565
|
+
} catch {
|
|
566
|
+
targetIdsBefore = /* @__PURE__ */ new Set();
|
|
567
|
+
}
|
|
552
568
|
const pagePromise = new Promise((resolve10) => {
|
|
553
569
|
const timer = setTimeout(() => {
|
|
554
570
|
ctx.browserContext.off("page", handler);
|
|
555
571
|
resolve10(void 0);
|
|
556
572
|
}, 3e3);
|
|
557
573
|
const handler = (page2) => {
|
|
574
|
+
if (targetIdsBefore.has(targetIdOf(page2))) return;
|
|
575
|
+
try {
|
|
576
|
+
if ((page2.url?.() ?? "") === "about:blank") return;
|
|
577
|
+
} catch {
|
|
578
|
+
}
|
|
558
579
|
clearTimeout(timer);
|
|
559
580
|
ctx.browserContext.off("page", handler);
|
|
560
581
|
resolve10(page2);
|
|
@@ -955,9 +976,11 @@ var mouseCommand = registerCommand({
|
|
|
955
976
|
await ctx.page.mouse.move(p.x, p.y, { steps: p.steps || 1 });
|
|
956
977
|
break;
|
|
957
978
|
case "down":
|
|
979
|
+
await ctx.page.mouse.move(p.x, p.y);
|
|
958
980
|
await ctx.page.mouse.down({ button });
|
|
959
981
|
break;
|
|
960
982
|
case "up":
|
|
983
|
+
await ctx.page.mouse.move(p.x, p.y);
|
|
961
984
|
await ctx.page.mouse.up({ button });
|
|
962
985
|
break;
|
|
963
986
|
case "click":
|
|
@@ -976,10 +999,11 @@ import { z as z7 } from "zod";
|
|
|
976
999
|
import { ok as ok7, normalizeTips as normalizeTips2 } from "@dyyz1993/xcli-core";
|
|
977
1000
|
var evaluateCommand = registerCommand({
|
|
978
1001
|
name: "eval",
|
|
979
|
-
description: "Evaluate JavaScript expression in the browser",
|
|
1002
|
+
description: "Evaluate JavaScript expression in the browser (--frame <url-substring> \u53EF\u5B9A\u5411\u5230 iframe \u4E0A\u4E0B\u6587\uFF0C\u8DE8\u57DF iframe \u4EA6\u53EF)",
|
|
980
1003
|
scope: "page",
|
|
981
1004
|
parameters: z7.object({
|
|
982
|
-
expression: z7.string()
|
|
1005
|
+
expression: z7.string(),
|
|
1006
|
+
frame: z7.string().optional().describe("\u5728 URL \u542B\u6B64\u5B50\u4E32\u7684 iframe \u91CC\u6267\u884C\uFF08\u540C\u8FDB\u7A0B\u7528 contextId\uFF0C\u8DE8\u57DF OOPIF \u7528 Target auto-attach\uFF09")
|
|
983
1007
|
}),
|
|
984
1008
|
handler: async (p, ctx) => {
|
|
985
1009
|
const engine = createRuleEngine();
|
|
@@ -991,7 +1015,7 @@ var evaluateCommand = registerCommand({
|
|
|
991
1015
|
direction: "client\u2192browser"
|
|
992
1016
|
});
|
|
993
1017
|
engine.stop();
|
|
994
|
-
const result = await ctx.page.evaluate(p.expression);
|
|
1018
|
+
const result = p.frame ? await ctx.page.evaluateInFrame(p.frame, p.expression) : await ctx.page.evaluate(p.expression);
|
|
995
1019
|
const response = ok7({ result });
|
|
996
1020
|
if (decision && decision.severity === "danger") {
|
|
997
1021
|
response.tips = normalizeTips2([`${decision.suggestion}`]);
|
|
@@ -5756,6 +5780,11 @@ async function handleSwitch(p, pages, ctx) {
|
|
|
5756
5780
|
const targetPage = pages[p.index];
|
|
5757
5781
|
await targetPage.bringToFront().catch(() => {
|
|
5758
5782
|
});
|
|
5783
|
+
for (let i = 0; i < 5; i++) {
|
|
5784
|
+
const okProbe = await targetPage.evaluate("1").then(() => true, () => false);
|
|
5785
|
+
if (okProbe) break;
|
|
5786
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
5787
|
+
}
|
|
5759
5788
|
const session = ctx.sessionId ? getSessionById(ctx.sessionId) : void 0;
|
|
5760
5789
|
if (session) {
|
|
5761
5790
|
setActivePage(session, targetPage);
|
|
@@ -6210,7 +6239,7 @@ function attachWaitForHuman(ctx, getOrCreateWSServer) {
|
|
|
6210
6239
|
}
|
|
6211
6240
|
function attachDetectAntiBot(ctx) {
|
|
6212
6241
|
ctx.detectAntiBot = async (page, config) => {
|
|
6213
|
-
const { detectAntiBot: detectAntiBot2 } = await import("./anti-bot-
|
|
6242
|
+
const { detectAntiBot: detectAntiBot2 } = await import("./anti-bot-GTTYNEFB.js");
|
|
6214
6243
|
return detectAntiBot2(page, config);
|
|
6215
6244
|
};
|
|
6216
6245
|
}
|
|
@@ -7399,7 +7428,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
7399
7428
|
}
|
|
7400
7429
|
let targetPageOverride = null;
|
|
7401
7430
|
if (_target && extraOpts?.cdpEndpoint) {
|
|
7402
|
-
const { findTargetPage } = await import("./browser-
|
|
7431
|
+
const { findTargetPage } = await import("./browser-V3JIWSTR.js");
|
|
7403
7432
|
targetPageOverride = await findTargetPage(extraOpts.cdpEndpoint, _target);
|
|
7404
7433
|
if (!targetPageOverride) {
|
|
7405
7434
|
return errorResult(`Target "${_target}" not found. Use 'xbrowser targets --cdp ${extraOpts.cdpEndpoint}' to list available pages.`);
|
|
@@ -7416,7 +7445,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
7416
7445
|
params = result.data;
|
|
7417
7446
|
}
|
|
7418
7447
|
if (command.scope !== "cli" && !process.env.XBROWSER_DAEMON_WORKER) {
|
|
7419
|
-
const { forwardExec } = await import("./daemon-client-
|
|
7448
|
+
const { forwardExec } = await import("./daemon-client-Y2YSZCGK.js");
|
|
7420
7449
|
const result = await forwardExec(commandName, params, sessionName, extraOpts?.cdpEndpoint);
|
|
7421
7450
|
if (result) return result;
|
|
7422
7451
|
}
|
|
@@ -10065,10 +10094,12 @@ var HTML_TAGS = /* @__PURE__ */ new Set([
|
|
|
10065
10094
|
"map",
|
|
10066
10095
|
"area"
|
|
10067
10096
|
]);
|
|
10097
|
+
var SELECTOR_DSL_PREFIXES = /^(text=|popup-text=|xpath=|role=|css=|chain=)/;
|
|
10068
10098
|
function normalizeSelector(input) {
|
|
10069
10099
|
if (!input) return input;
|
|
10070
10100
|
if (/^[#\.\[\:\/]/.test(input)) return input;
|
|
10071
10101
|
if (input === "*") return input;
|
|
10102
|
+
if (SELECTOR_DSL_PREFIXES.test(input)) return input;
|
|
10072
10103
|
if (/[>\s+,~]/.test(input)) return input;
|
|
10073
10104
|
const tagName = input.match(/^[a-zA-Z][a-zA-Z0-9]*/)?.[0];
|
|
10074
10105
|
if (tagName && HTML_TAGS.has(tagName.toLowerCase())) return input;
|
|
@@ -10243,11 +10274,23 @@ async function handleBrowserCommand(command, args, options, sessionName, mode, c
|
|
|
10243
10274
|
output: options.output || options.o || args[0]
|
|
10244
10275
|
};
|
|
10245
10276
|
break;
|
|
10246
|
-
case "eval":
|
|
10277
|
+
case "eval": {
|
|
10247
10278
|
cmdName = "eval";
|
|
10248
|
-
|
|
10249
|
-
|
|
10279
|
+
const frameVal = options.frame || void 0;
|
|
10280
|
+
const exprParts = [];
|
|
10281
|
+
for (let i = 0; i < args.length; i++) {
|
|
10282
|
+
if (args[i] === "--frame") {
|
|
10283
|
+
i++;
|
|
10284
|
+
continue;
|
|
10285
|
+
}
|
|
10286
|
+
if (args[i].startsWith("--frame=")) continue;
|
|
10287
|
+
exprParts.push(args[i]);
|
|
10288
|
+
}
|
|
10289
|
+
params = { expression: exprParts.join(" ") || options.expression || "" };
|
|
10290
|
+
if (frameVal) params.frame = frameVal;
|
|
10291
|
+
if (!params.expression) outputError("Usage: xbrowser eval <expression> [--frame <url-substring>]");
|
|
10250
10292
|
break;
|
|
10293
|
+
}
|
|
10251
10294
|
case "scroll": {
|
|
10252
10295
|
let direction = "down";
|
|
10253
10296
|
let distance;
|
|
@@ -10950,7 +10993,7 @@ async function handlePlugin(args, options, mode) {
|
|
|
10950
10993
|
} catch {
|
|
10951
10994
|
}
|
|
10952
10995
|
try {
|
|
10953
|
-
const { daemonPing } = await import("./daemon-client-
|
|
10996
|
+
const { daemonPing } = await import("./daemon-client-Y2YSZCGK.js");
|
|
10954
10997
|
if (await daemonPing()) {
|
|
10955
10998
|
await fetch("http://localhost:9224/rpc", {
|
|
10956
10999
|
method: "POST",
|
|
@@ -11516,7 +11559,7 @@ async function handleFilter(args, _mode, options) {
|
|
|
11516
11559
|
}
|
|
11517
11560
|
}
|
|
11518
11561
|
async function handleGeneratePlugin(sessionName, pluginName, outputDir) {
|
|
11519
|
-
const { SessionRecorder: SessionRecorder2 } = await import("./session-recorder-
|
|
11562
|
+
const { SessionRecorder: SessionRecorder2 } = await import("./session-recorder-SLDBENVF.js");
|
|
11520
11563
|
const { readSiteKnowledge: readSiteKnowledge2, toMarkdown } = await import("./site-knowledge-SYC6VCDB.js");
|
|
11521
11564
|
const { mkdirSync: mkdirSync11, writeFileSync: writeFileSync13 } = await import("fs");
|
|
11522
11565
|
const { join: join14 } = await import("path");
|
|
@@ -12822,6 +12865,9 @@ var HTTPServer = class {
|
|
|
12822
12865
|
this.server = null;
|
|
12823
12866
|
reject(err);
|
|
12824
12867
|
});
|
|
12868
|
+
server.headersTimeout = 20 * 6e4;
|
|
12869
|
+
server.requestTimeout = 20 * 6e4;
|
|
12870
|
+
server.keepAliveTimeout = 20 * 6e4;
|
|
12825
12871
|
server.listen(this.port, this.host, () => {
|
|
12826
12872
|
const addr = server.address();
|
|
12827
12873
|
if (addr && typeof addr === "object") {
|
|
@@ -13457,7 +13503,7 @@ Run "xbrowser ${command} ${subCommand} --help" to see available parameters.`
|
|
|
13457
13503
|
}
|
|
13458
13504
|
const needsBrowser = cmdEntry.scope === "page" || cmdEntry.scope === "browser";
|
|
13459
13505
|
if (needsBrowser && !process.env.XBROWSER_DAEMON_WORKER) {
|
|
13460
|
-
const { forwardPluginExec } = await import("./daemon-client-
|
|
13506
|
+
const { forwardPluginExec } = await import("./daemon-client-Y2YSZCGK.js");
|
|
13461
13507
|
const userTimeout = typeof params.timeout === "number" && params.timeout > 0 ? params.timeout * 1e3 + 3e4 : void 0;
|
|
13462
13508
|
const result = await forwardPluginExec(`${command}.${subCommand}`, params, sessionName, cdpEndpoint, userTimeout);
|
|
13463
13509
|
const resultData = result && typeof result === "object" && "data" in result ? result.data : void 0;
|
|
@@ -13497,7 +13543,7 @@ Run "xbrowser ${command} ${subCommand} --help" to see available parameters.`
|
|
|
13497
13543
|
const targetPage = pages[cmdTabIndex];
|
|
13498
13544
|
await targetPage.bringToFront().catch(() => {
|
|
13499
13545
|
});
|
|
13500
|
-
const { setActivePage: setActivePage2 } = await import("./browser-
|
|
13546
|
+
const { setActivePage: setActivePage2 } = await import("./browser-V3JIWSTR.js");
|
|
13501
13547
|
setActivePage2(session, targetPage);
|
|
13502
13548
|
}
|
|
13503
13549
|
}
|
|
@@ -16670,7 +16716,7 @@ var DataCollector = class {
|
|
|
16670
16716
|
return results;
|
|
16671
16717
|
}
|
|
16672
16718
|
async createBrowserContext() {
|
|
16673
|
-
const { launch } = await import("./cdp-driver-
|
|
16719
|
+
const { launch } = await import("./cdp-driver-GD6YBBGE.js");
|
|
16674
16720
|
const { browser } = await launch({
|
|
16675
16721
|
headless: true,
|
|
16676
16722
|
args: ["--no-sandbox", "--disable-setuid-sandbox"]
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import {
|
|
2
|
+
queryJS
|
|
3
|
+
} from "./chunk-3FWLW7FS.js";
|
|
1
4
|
import "./chunk-KFQGP6VL.js";
|
|
2
5
|
|
|
3
6
|
// src/recorder/session-replayer.ts
|
|
@@ -31,7 +34,7 @@ var SessionReplayer = class {
|
|
|
31
34
|
if (this.opts.page) {
|
|
32
35
|
this.page = this.opts.page;
|
|
33
36
|
} else if (this.opts.cdpUrl) {
|
|
34
|
-
const { launch } = await import("./cdp-driver-
|
|
37
|
+
const { launch } = await import("./cdp-driver-VEK6BNN6.js");
|
|
35
38
|
const { browser } = await launch({ cdpEndpoint: this.opts.cdpUrl });
|
|
36
39
|
let contexts = browser.contexts();
|
|
37
40
|
for (let i = 0; i < 10 && contexts.length === 0; i++) {
|
|
@@ -44,13 +47,14 @@ var SessionReplayer = class {
|
|
|
44
47
|
this.page = pages[0];
|
|
45
48
|
}
|
|
46
49
|
if (!this.page) throw new Error("No page available. Provide cdpUrl or page.");
|
|
47
|
-
const actions = this.recording.actions;
|
|
50
|
+
const actions = this.dedupAdjacentActions(this.recording.actions);
|
|
48
51
|
let success = 0;
|
|
49
52
|
let failed = 0;
|
|
50
53
|
let skipped = 0;
|
|
51
54
|
for (let i = 0; i < actions.length; i++) {
|
|
52
55
|
const action = actions[i];
|
|
53
56
|
this.opts.onStep?.(action, i, actions.length);
|
|
57
|
+
const pagesBefore = this.listContextPages();
|
|
54
58
|
try {
|
|
55
59
|
if (action.trajectory) {
|
|
56
60
|
await this.replayTrajectory(action.trajectory);
|
|
@@ -63,6 +67,15 @@ var SessionReplayer = class {
|
|
|
63
67
|
}
|
|
64
68
|
}
|
|
65
69
|
success++;
|
|
70
|
+
if (action.type === "click" || action.type === "cdp-click") {
|
|
71
|
+
const pagesAfter = this.listContextPages();
|
|
72
|
+
if (pagesAfter && pagesBefore && pagesAfter.length > pagesBefore.length) {
|
|
73
|
+
const newest = pagesAfter[pagesAfter.length - 1];
|
|
74
|
+
await newest.bringToFront().catch(() => {
|
|
75
|
+
});
|
|
76
|
+
this.page = newest;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
66
79
|
} catch (e) {
|
|
67
80
|
const err = e instanceof Error ? e : new Error(String(e));
|
|
68
81
|
this.opts.onError?.(action, err, i);
|
|
@@ -75,6 +88,47 @@ var SessionReplayer = class {
|
|
|
75
88
|
return { success, failed, skipped };
|
|
76
89
|
}
|
|
77
90
|
/** Replay a single action */
|
|
91
|
+
/** Pages of the replayer page's context (best-effort; null if unavailable). */
|
|
92
|
+
listContextPages() {
|
|
93
|
+
try {
|
|
94
|
+
const ctx = this.page.context?.();
|
|
95
|
+
const pages = ctx?.pages?.();
|
|
96
|
+
return Array.isArray(pages) ? pages : null;
|
|
97
|
+
} catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Replay-time adjacent dedup (rec-duel d02/d03/d05).
|
|
103
|
+
*
|
|
104
|
+
* The recorder can emit both the real action signal AND the injected cdp
|
|
105
|
+
* command action for a single interaction when the signal flush lags behind
|
|
106
|
+
* the recorder-side dedup window (heavy snapshot capture slows polling).
|
|
107
|
+
* Replaying both executes the interaction twice. Filter here: an action is
|
|
108
|
+
* skipped when a nearby (≤15s apart) earlier action matches on
|
|
109
|
+
* type-normalized key (selector + text) AND coordinates agree — either side
|
|
110
|
+
* lacks coords (cdp actions carry none) or both are within 30px. Two real
|
|
111
|
+
* clicks on the same element at different spots (canvas buttons) survive.
|
|
112
|
+
*/
|
|
113
|
+
dedupAdjacentActions(actions) {
|
|
114
|
+
const normType = (t) => t === "cdp-click" ? "click" : t === "cdp-fill" ? "input" : t;
|
|
115
|
+
const generic = (a) => (a.type === "click" || a.type === "dblclick" || a.type === "contextmenu") && (a.element?.selector === "html" || a.element?.selector === "body");
|
|
116
|
+
const keyOf = (a) => `${normType(a.type)}|${a.element?.selector || ""}|${a.element?.text || ""}`;
|
|
117
|
+
const coordsOf = (a) => typeof a.x === "number" && typeof a.y === "number" ? { x: a.x, y: a.y } : null;
|
|
118
|
+
const near = (p, q) => Math.abs(p.x - q.x) <= 30 && Math.abs(p.y - q.y) <= 30;
|
|
119
|
+
const lastKept = /* @__PURE__ */ new Map();
|
|
120
|
+
return actions.filter((a) => {
|
|
121
|
+
if (generic(a)) return false;
|
|
122
|
+
const key = keyOf(a);
|
|
123
|
+
const c = coordsOf(a);
|
|
124
|
+
const recent = lastKept.get(key) || [];
|
|
125
|
+
const dup = recent.some((e) => Math.abs(a.timestamp - e.ts) <= 15e3 && (!c || !e.c || near(c, e.c)));
|
|
126
|
+
if (dup) return false;
|
|
127
|
+
recent.push({ ts: a.timestamp, c });
|
|
128
|
+
lastKept.set(key, recent);
|
|
129
|
+
return true;
|
|
130
|
+
});
|
|
131
|
+
}
|
|
78
132
|
async replayAction(action) {
|
|
79
133
|
const page = this.page;
|
|
80
134
|
const timeout = this.opts.stepTimeout;
|
|
@@ -93,9 +147,43 @@ var SessionReplayer = class {
|
|
|
93
147
|
case "click":
|
|
94
148
|
case "cdp-click": {
|
|
95
149
|
const selector = await this.resolveAndWait(action);
|
|
150
|
+
if (typeof action.x === "number" && typeof action.y === "number") {
|
|
151
|
+
const hit = await page.evaluate(`
|
|
152
|
+
(function() {
|
|
153
|
+
const el = ${queryJS(selector)};
|
|
154
|
+
if (!el) return false;
|
|
155
|
+
const r = el.getBoundingClientRect();
|
|
156
|
+
return ${action.x} >= r.x - 2 && ${action.x} <= r.x + r.width + 2
|
|
157
|
+
&& ${action.y} >= r.y - 2 && ${action.y} <= r.y + r.height + 2;
|
|
158
|
+
})()
|
|
159
|
+
`).catch(() => false);
|
|
160
|
+
if (hit) {
|
|
161
|
+
await page.mouse.click(action.x, action.y, { stealth: true });
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
96
165
|
await page.click(selector, { timeout });
|
|
97
166
|
break;
|
|
98
167
|
}
|
|
168
|
+
case "scroll": {
|
|
169
|
+
const [dir, distStr] = (action.value || "down:300").split(":");
|
|
170
|
+
const dist = Number(distStr) || 300;
|
|
171
|
+
const sign = dir === "up" ? -1 : dir === "left" ? 0 : 1;
|
|
172
|
+
const selector = await this.resolveAndWait(action).catch(() => void 0);
|
|
173
|
+
if (selector) {
|
|
174
|
+
await page.evaluate(`
|
|
175
|
+
(function() {
|
|
176
|
+
const el = ${queryJS(selector)};
|
|
177
|
+
if (el) el.scrollTop += ${sign * dist};
|
|
178
|
+
})()
|
|
179
|
+
`).catch(() => {
|
|
180
|
+
});
|
|
181
|
+
} else {
|
|
182
|
+
await page.evaluate(`window.scrollBy(0, ${sign * dist})`).catch(() => {
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
99
187
|
case "input": {
|
|
100
188
|
const selector = await this.resolveAndWait(action);
|
|
101
189
|
await page.fill(selector, action.value ?? "", { timeout });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xbrowser/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.0",
|
|
4
4
|
"description": "Browser automation CLI for web scraping, headless browsing, SEO analysis, and AI agent workflows. A command-line alternative to Playwright, Puppeteer, and Selenium.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|