@hira-core/sdk 1.0.7 → 1.0.8
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 +32 -11
- package/dist/index.d.ts +149 -6
- package/dist/index.js +840 -14
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -188,17 +188,22 @@ const utils = new BrowserUtils(context);
|
|
|
188
188
|
|
|
189
189
|
#### Navigation & Interaction
|
|
190
190
|
|
|
191
|
-
| Method | Description
|
|
192
|
-
| ------------------------------------------ |
|
|
193
|
-
| `utils.goto(url)` | Navigate to URL
|
|
194
|
-
| `utils.click(selector)` | Wait + scroll + click
|
|
195
|
-
| `utils.
|
|
196
|
-
| `utils.
|
|
197
|
-
| `utils.
|
|
198
|
-
| `utils.
|
|
199
|
-
| `utils.
|
|
200
|
-
| `utils.
|
|
201
|
-
| `utils.
|
|
191
|
+
| Method | Description |
|
|
192
|
+
| ------------------------------------------ | -------------------------------------- |
|
|
193
|
+
| `utils.goto(url)` | Navigate to URL |
|
|
194
|
+
| `utils.click(selector)` | Wait + scroll + click |
|
|
195
|
+
| `utils.click({ x, y })` | Click at specific coordinates |
|
|
196
|
+
| `utils.type(selector, text)` | Wait + clear + type |
|
|
197
|
+
| `utils.select(selector, value)` | Select dropdown option (native `<select>`) |
|
|
198
|
+
| `utils.getText(selector)` | Get text content |
|
|
199
|
+
| `utils.getPosition(selector)` | Get element center coordinates |
|
|
200
|
+
| `utils.exists(selector, timeout?)` | Check element exists |
|
|
201
|
+
| `utils.waitForElement(selector, timeout?)` | Wait for element |
|
|
202
|
+
| `utils.waitForNavigation()` | Wait for page navigation |
|
|
203
|
+
| `utils.screenshot(path?)` | Take screenshot |
|
|
204
|
+
| `utils.sleep(ms)` | Delay (respects abort signal) |
|
|
205
|
+
| `utils.scroll({ deltaY })` | Smooth 60fps scroll (ease-in-out) |
|
|
206
|
+
| `utils.scroll({ deltaY, container })` | Smooth scroll inside overflow container|
|
|
202
207
|
|
|
203
208
|
#### Tab Management
|
|
204
209
|
|
|
@@ -344,6 +349,22 @@ npx @hira-core/cli build
|
|
|
344
349
|
|
|
345
350
|
---
|
|
346
351
|
|
|
352
|
+
## Virtual Cursor
|
|
353
|
+
|
|
354
|
+
The SDK includes a **virtual cursor** — a visible SVG cursor overlay that shows mouse movement, clicks, and scrolling in real-time. This is enabled by default and can be toggled via `IExecutionConfig.virtualCursor`:
|
|
355
|
+
|
|
356
|
+
```typescript
|
|
357
|
+
// In flow runner params
|
|
358
|
+
execution: {
|
|
359
|
+
virtualCursor: true, // default — show cursor overlay
|
|
360
|
+
// virtualCursor: false, // disable cursor overlay
|
|
361
|
+
}
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
When enabled, `BrowserUtils.click()`, `scroll()`, and other interaction methods will show realistic Bézier curve mouse movement powered by [ghost-cursor](https://github.com/nicr9/ghost-cursor).
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
347
368
|
## License
|
|
348
369
|
|
|
349
370
|
ISC
|
package/dist/index.d.ts
CHANGED
|
@@ -237,6 +237,12 @@ interface IExecutionConfig {
|
|
|
237
237
|
* Dev mode: dùng constructor `super(AntidetectProvider.GPM, ...)` trong flow class.
|
|
238
238
|
*/
|
|
239
239
|
antidetectProvider?: 'gpm' | 'hidemium' | 'genlogin' | 'adspower';
|
|
240
|
+
/**
|
|
241
|
+
* Enable virtual cursor — SVG arrow with Bézier curve mouse movement.
|
|
242
|
+
* When true, click() and type() will move cursor naturally before interacting.
|
|
243
|
+
* Default: true
|
|
244
|
+
*/
|
|
245
|
+
virtualCursor?: boolean;
|
|
240
246
|
}
|
|
241
247
|
interface IBrowserWindowConfig {
|
|
242
248
|
width: number;
|
|
@@ -314,6 +320,8 @@ interface IScriptContext<TConfig extends IFlowConfig = IFlowConfig> {
|
|
|
314
320
|
page: Page;
|
|
315
321
|
profile: IAntidetectProfile;
|
|
316
322
|
index: number;
|
|
323
|
+
/** Execution config — includes virtualCursor flag etc. */
|
|
324
|
+
execution: IExecutionConfig;
|
|
317
325
|
globalInput: InferGlobalInput<TConfig>;
|
|
318
326
|
profileInput: InferProfileInput<TConfig>;
|
|
319
327
|
/**
|
|
@@ -616,13 +624,26 @@ declare class BrowserUtils<TConfig extends IFlowConfig = IFlowConfig> {
|
|
|
616
624
|
private activePage;
|
|
617
625
|
/** Currently active iframe — null means main frame. Changed via activeIframe() */
|
|
618
626
|
private activeFrame;
|
|
627
|
+
/** Virtual cursor — Bézier movement + SVG visualization */
|
|
628
|
+
private readonly hiraCursor;
|
|
619
629
|
constructor(context: IScriptContext<TConfig>);
|
|
620
630
|
/**
|
|
621
|
-
* CDP trick — Chromium
|
|
622
|
-
*
|
|
631
|
+
* CDP trick — Chromium thinks tab is always focused, not throttled
|
|
632
|
+
* when user switches to another tab or clicks elsewhere.
|
|
623
633
|
*/
|
|
624
634
|
private enableFocusEmulation;
|
|
635
|
+
/**
|
|
636
|
+
* Apply stealth scripts to remove automation fingerprints.
|
|
637
|
+
* Uses evaluateOnNewDocument — runs BEFORE any page scripts on every navigation.
|
|
638
|
+
* Persists across navigations on the same page instance.
|
|
639
|
+
*/
|
|
640
|
+
private applyStealthScripts;
|
|
625
641
|
private checkAbort;
|
|
642
|
+
/**
|
|
643
|
+
* Scroll element vào viewport mượt mà — segments + delay.
|
|
644
|
+
* Chỉ scroll nếu element nằm ngoài viewport.
|
|
645
|
+
*/
|
|
646
|
+
private smoothScrollToElement;
|
|
626
647
|
/**
|
|
627
648
|
* Pause execution for the given duration.
|
|
628
649
|
*
|
|
@@ -650,10 +671,11 @@ declare class BrowserUtils<TConfig extends IFlowConfig = IFlowConfig> {
|
|
|
650
671
|
*/
|
|
651
672
|
private resolveElement;
|
|
652
673
|
/**
|
|
653
|
-
* Click on an element
|
|
674
|
+
* Click on an element or at specific coordinates.
|
|
675
|
+
* Scrolls the element into view before clicking.
|
|
654
676
|
* Throws if the element is not found.
|
|
655
677
|
*
|
|
656
|
-
* @param target - CSS selector, XPath, or
|
|
678
|
+
* @param target - CSS selector, XPath, ElementHandle, or `{ x, y }` coordinates
|
|
657
679
|
* @param options.delay - Delay in ms before clicking (default: 1000)
|
|
658
680
|
* @param options.waitTimeout - Max wait time for element to appear (default: 2000)
|
|
659
681
|
* @param options.frame - Optional Frame to search within
|
|
@@ -662,9 +684,58 @@ declare class BrowserUtils<TConfig extends IFlowConfig = IFlowConfig> {
|
|
|
662
684
|
* @example
|
|
663
685
|
* await utils.click("#submit-btn");
|
|
664
686
|
* await utils.click("#btn", { delay: 0 }); // click immediately
|
|
665
|
-
* await utils.click(
|
|
687
|
+
* await utils.click({ x: 500, y: 300 }); // click at coordinates
|
|
688
|
+
* const pos = await utils.getPosition("#btn", { randomXY: true });
|
|
689
|
+
* await utils.click(pos!); // click random point inside element
|
|
666
690
|
*/
|
|
667
|
-
click(target: string | ElementHandle
|
|
691
|
+
click(target: string | ElementHandle | {
|
|
692
|
+
x: number;
|
|
693
|
+
y: number;
|
|
694
|
+
}, options?: {
|
|
695
|
+
delay?: number;
|
|
696
|
+
waitTimeout?: number;
|
|
697
|
+
frame?: Frame;
|
|
698
|
+
}): Promise<boolean>;
|
|
699
|
+
/**
|
|
700
|
+
* Get the position (x, y) of an element.
|
|
701
|
+
* Returns center point by default, or a random point within bounds with `randomXY`.
|
|
702
|
+
*
|
|
703
|
+
* @param selector - CSS selector or XPath
|
|
704
|
+
* @param options.randomXY - If true, returns random point within element (10% margin from edges)
|
|
705
|
+
* @param options.waitTimeout - Max wait time for element (default: 2000ms)
|
|
706
|
+
* @param options.frame - Optional Frame to search within
|
|
707
|
+
* @returns `{ x, y }` or null if element not found / no bounding box
|
|
708
|
+
*
|
|
709
|
+
* @example
|
|
710
|
+
* const center = await utils.getPosition("#btn");
|
|
711
|
+
* const rand = await utils.getPosition("#btn", { randomXY: true });
|
|
712
|
+
* if (rand) await utils.click(rand);
|
|
713
|
+
*/
|
|
714
|
+
getPosition(selector: string, options?: {
|
|
715
|
+
randomXY?: boolean;
|
|
716
|
+
waitTimeout?: number;
|
|
717
|
+
frame?: Frame;
|
|
718
|
+
}): Promise<{
|
|
719
|
+
x: number;
|
|
720
|
+
y: number;
|
|
721
|
+
} | null>;
|
|
722
|
+
/**
|
|
723
|
+
* Select a value from a `<select>` dropdown.
|
|
724
|
+
* Human-like flow: cursor moves to select → click to open → page.select() → close.
|
|
725
|
+
*
|
|
726
|
+
* NOTE: Native `<option>` elements CAN NOT be clicked via Puppeteer
|
|
727
|
+
* ("Node is either not clickable or not an Element").
|
|
728
|
+
* `page.select()` is the ONLY reliable method.
|
|
729
|
+
*
|
|
730
|
+
* @param selector - CSS selector or XPath of the `<select>` element
|
|
731
|
+
* @param value - The `value` attribute of the option to select
|
|
732
|
+
* @param options.delay - Delay before clicking (default: 500ms)
|
|
733
|
+
* @param options.waitTimeout - Max wait for element (default: 2000ms)
|
|
734
|
+
*
|
|
735
|
+
* @example
|
|
736
|
+
* await utils.select("#country", "vn");
|
|
737
|
+
*/
|
|
738
|
+
select(selector: string, value: string, options?: {
|
|
668
739
|
delay?: number;
|
|
669
740
|
waitTimeout?: number;
|
|
670
741
|
frame?: Frame;
|
|
@@ -741,6 +812,78 @@ declare class BrowserUtils<TConfig extends IFlowConfig = IFlowConfig> {
|
|
|
741
812
|
goto(url: string, options?: {
|
|
742
813
|
waitUntil?: "load" | "domcontentloaded" | "networkidle0" | "networkidle2";
|
|
743
814
|
}): Promise<boolean>;
|
|
815
|
+
/**
|
|
816
|
+
* Scroll the page or a container by a specific amount.
|
|
817
|
+
* Uses CDP mouseWheel events (human-like, not JS scrollBy).
|
|
818
|
+
*
|
|
819
|
+
* @param direction - "up" or "down"
|
|
820
|
+
* @param amount - Pixels to scroll
|
|
821
|
+
* @param options.container - CSS selector of scroll container
|
|
822
|
+
* @param options.speed - Scroll speed 1-100 (default: 50)
|
|
823
|
+
*
|
|
824
|
+
* @example
|
|
825
|
+
* await utils.scroll("down", 500);
|
|
826
|
+
* await utils.scroll("up", 200, { container: "#chat-messages" });
|
|
827
|
+
*/
|
|
828
|
+
scroll(direction: "up" | "down", amount: number, options?: {
|
|
829
|
+
container?: string;
|
|
830
|
+
speed?: number;
|
|
831
|
+
}): Promise<void>;
|
|
832
|
+
/**
|
|
833
|
+
* Scroll to the top or bottom of the page.
|
|
834
|
+
* Scrolls in segments with pauses — giống người lướt dần đến đích.
|
|
835
|
+
*
|
|
836
|
+
* @param position - "top" or "bottom"
|
|
837
|
+
* @param options.speed - Scroll speed 1-100 (default: 50)
|
|
838
|
+
*
|
|
839
|
+
* @example
|
|
840
|
+
* await utils.scrollTo("bottom");
|
|
841
|
+
* await utils.scrollTo("top");
|
|
842
|
+
*/
|
|
843
|
+
scrollTo(position: "top" | "bottom", options?: {
|
|
844
|
+
speed?: number;
|
|
845
|
+
}): Promise<void>;
|
|
846
|
+
/**
|
|
847
|
+
* Scroll an element into the viewport.
|
|
848
|
+
* Scrolls in segments with pauses — không nhảy tới đích.
|
|
849
|
+
*
|
|
850
|
+
* @param selector - CSS selector or XPath of the element
|
|
851
|
+
* @param options.speed - Scroll speed 1-100 (default: 50)
|
|
852
|
+
* @returns true if element found and scrolled
|
|
853
|
+
*
|
|
854
|
+
* @example
|
|
855
|
+
* await utils.scrollToElement("#footer");
|
|
856
|
+
* await utils.scrollToElement("//button[text()='Load more']");
|
|
857
|
+
*/
|
|
858
|
+
scrollToElement(selector: string, options?: {
|
|
859
|
+
speed?: number;
|
|
860
|
+
}): Promise<boolean>;
|
|
861
|
+
/**
|
|
862
|
+
* Simulate natural browsing scroll — cuộn xuống xen kẽ lướt ngược, delay random.
|
|
863
|
+
* Giống người đang đọc/lướt web tự nhiên.
|
|
864
|
+
*
|
|
865
|
+
* @param options.duration - Thời gian scroll tính bằng giây (default: 5)
|
|
866
|
+
* @param options.direction - Main direction (default: "down")
|
|
867
|
+
* @param options.backChance - Chance to scroll back 0-1 (default: 0.15)
|
|
868
|
+
* @param options.backAmount - [min, max] px to scroll back (default: [50, 150])
|
|
869
|
+
* @param options.stepSize - [min, max] px per step (default: [200, 400])
|
|
870
|
+
* @param options.stepDelay - [min, max] ms delay between steps (default: [300, 800])
|
|
871
|
+
* @param options.container - CSS selector of scroll container
|
|
872
|
+
*
|
|
873
|
+
* @example
|
|
874
|
+
* await utils.randomScroll(); // 5s lướt xuống
|
|
875
|
+
* await utils.randomScroll({ duration: 10 }); // 10s
|
|
876
|
+
* await utils.randomScroll({ backChance: 0.3, container: "#feed" });
|
|
877
|
+
*/
|
|
878
|
+
randomScroll(options?: {
|
|
879
|
+
duration?: number;
|
|
880
|
+
direction?: "down" | "up";
|
|
881
|
+
backChance?: number;
|
|
882
|
+
backAmount?: [number, number];
|
|
883
|
+
stepSize?: [number, number];
|
|
884
|
+
stepDelay?: [number, number];
|
|
885
|
+
container?: string;
|
|
886
|
+
}): Promise<void>;
|
|
744
887
|
/**
|
|
745
888
|
* Wait for the current page to complete a navigation (e.g. after a form submit).
|
|
746
889
|
* Throws on timeout.
|
package/dist/index.js
CHANGED
|
@@ -12453,6 +12453,7 @@ var BaseFlow = class {
|
|
|
12453
12453
|
page,
|
|
12454
12454
|
profile,
|
|
12455
12455
|
index,
|
|
12456
|
+
execution: params.execution,
|
|
12456
12457
|
globalInput: params.globalInput || {},
|
|
12457
12458
|
profileInput: profileData || {},
|
|
12458
12459
|
output: outputObj,
|
|
@@ -16803,6 +16804,9 @@ var AntidetectBaseFlow = class extends BaseFlow {
|
|
|
16803
16804
|
}
|
|
16804
16805
|
};
|
|
16805
16806
|
|
|
16807
|
+
// src/utils/browser.utils.ts
|
|
16808
|
+
var import_puppeteer_core3 = require("puppeteer-core");
|
|
16809
|
+
|
|
16806
16810
|
// src/sdk-config.ts
|
|
16807
16811
|
var SDK_CONFIG = {
|
|
16808
16812
|
actionDelayMs: 1e3,
|
|
@@ -16812,24 +16816,439 @@ function getSdkConfig() {
|
|
|
16812
16816
|
return SDK_CONFIG;
|
|
16813
16817
|
}
|
|
16814
16818
|
|
|
16819
|
+
// src/utils/hira-cursor.ts
|
|
16820
|
+
var import_ghost_cursor = require("ghost-cursor");
|
|
16821
|
+
var CURSOR_SVG_NORMAL = `
|
|
16822
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
|
16823
|
+
<defs>
|
|
16824
|
+
<linearGradient id="hiraGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
16825
|
+
<stop offset="0%" stop-color="#FF3333"/>
|
|
16826
|
+
<stop offset="100%" stop-color="#FF6B35"/>
|
|
16827
|
+
</linearGradient>
|
|
16828
|
+
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
|
|
16829
|
+
<feDropShadow dx="1" dy="1" stdDeviation="0.8" flood-color="#000" flood-opacity="0.3"/>
|
|
16830
|
+
</filter>
|
|
16831
|
+
</defs>
|
|
16832
|
+
<path d="M 4 2 L 4 20 L 9 15 L 14 22 L 16 21 L 11 14 L 18 14 Z"
|
|
16833
|
+
fill="url(#hiraGrad)" stroke="#FFFFFF" stroke-width="1.2" stroke-linejoin="round"
|
|
16834
|
+
filter="url(#shadow)"/>
|
|
16835
|
+
</svg>`;
|
|
16836
|
+
var CURSOR_SVG_CLICK = `
|
|
16837
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
|
16838
|
+
<defs>
|
|
16839
|
+
<linearGradient id="hiraClickGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
16840
|
+
<stop offset="0%" stop-color="#FFD700"/>
|
|
16841
|
+
<stop offset="100%" stop-color="#FFA500"/>
|
|
16842
|
+
</linearGradient>
|
|
16843
|
+
<filter id="shadowClick" x="-20%" y="-20%" width="140%" height="140%">
|
|
16844
|
+
<feDropShadow dx="1" dy="1" stdDeviation="0.8" flood-color="#000" flood-opacity="0.3"/>
|
|
16845
|
+
</filter>
|
|
16846
|
+
</defs>
|
|
16847
|
+
<path d="M 4 2 L 4 20 L 9 15 L 14 22 L 16 21 L 11 14 L 18 14 Z"
|
|
16848
|
+
fill="url(#hiraClickGrad)" stroke="#FFFFFF" stroke-width="1.2" stroke-linejoin="round"
|
|
16849
|
+
filter="url(#shadowClick)"/>
|
|
16850
|
+
</svg>`;
|
|
16851
|
+
var _HiraCursor = class _HiraCursor {
|
|
16852
|
+
constructor(enabled) {
|
|
16853
|
+
/** ghost-cursor instances keyed by page — weak so GC cleans up when page closes */
|
|
16854
|
+
this.cursorMap = /* @__PURE__ */ new WeakMap();
|
|
16855
|
+
/** Track which pages already have SVG injected */
|
|
16856
|
+
this.injectedPages = /* @__PURE__ */ new WeakSet();
|
|
16857
|
+
this.enabled = enabled;
|
|
16858
|
+
}
|
|
16859
|
+
/** Whether virtual cursor is enabled */
|
|
16860
|
+
get isEnabled() {
|
|
16861
|
+
return this.enabled;
|
|
16862
|
+
}
|
|
16863
|
+
/**
|
|
16864
|
+
* Inject SVG cursor + mousemove listener onto the page.
|
|
16865
|
+
* Idempotent — calling multiple times on the same page will skip.
|
|
16866
|
+
* Must be called after each page.goto() since navigation clears the DOM.
|
|
16867
|
+
*/
|
|
16868
|
+
async inject(page) {
|
|
16869
|
+
if (!this.enabled) return;
|
|
16870
|
+
if (this.injectedPages.has(page)) return;
|
|
16871
|
+
try {
|
|
16872
|
+
if (page.isClosed()) return;
|
|
16873
|
+
const svgNormal = `data:image/svg+xml;base64,${Buffer.from(CURSOR_SVG_NORMAL).toString("base64")}`;
|
|
16874
|
+
const svgClick = `data:image/svg+xml;base64,${Buffer.from(CURSOR_SVG_CLICK).toString("base64")}`;
|
|
16875
|
+
await page.evaluate(
|
|
16876
|
+
(normalUri, clickUri) => {
|
|
16877
|
+
const old = document.getElementById("hira-cursor");
|
|
16878
|
+
if (old) old.remove();
|
|
16879
|
+
const el = document.createElement("div");
|
|
16880
|
+
el.id = "hira-cursor";
|
|
16881
|
+
const img = document.createElement("img");
|
|
16882
|
+
img.src = normalUri;
|
|
16883
|
+
img.style.cssText = "width:24px;height:24px;pointer-events:none;";
|
|
16884
|
+
el.appendChild(img);
|
|
16885
|
+
el.style.cssText = `
|
|
16886
|
+
position: fixed;
|
|
16887
|
+
left: -30px; top: -30px;
|
|
16888
|
+
z-index: 2147483647;
|
|
16889
|
+
pointer-events: none;
|
|
16890
|
+
transform: translate(-2px, -2px);
|
|
16891
|
+
`;
|
|
16892
|
+
document.body.appendChild(el);
|
|
16893
|
+
document.documentElement.style.cursor = "none";
|
|
16894
|
+
document.body.style.cursor = "none";
|
|
16895
|
+
document.addEventListener(
|
|
16896
|
+
"mousemove",
|
|
16897
|
+
(e) => {
|
|
16898
|
+
el.style.left = e.clientX + "px";
|
|
16899
|
+
el.style.top = e.clientY + "px";
|
|
16900
|
+
},
|
|
16901
|
+
true
|
|
16902
|
+
);
|
|
16903
|
+
document.addEventListener(
|
|
16904
|
+
"mousedown",
|
|
16905
|
+
() => {
|
|
16906
|
+
img.src = clickUri;
|
|
16907
|
+
el.style.transform = "translate(-2px, -2px) scale(0.88)";
|
|
16908
|
+
},
|
|
16909
|
+
true
|
|
16910
|
+
);
|
|
16911
|
+
document.addEventListener(
|
|
16912
|
+
"mouseup",
|
|
16913
|
+
() => {
|
|
16914
|
+
img.src = normalUri;
|
|
16915
|
+
el.style.transform = "translate(-2px, -2px) scale(1)";
|
|
16916
|
+
},
|
|
16917
|
+
true
|
|
16918
|
+
);
|
|
16919
|
+
},
|
|
16920
|
+
svgNormal,
|
|
16921
|
+
svgClick
|
|
16922
|
+
);
|
|
16923
|
+
this.injectedPages.add(page);
|
|
16924
|
+
this.getOrCreateCursor(page);
|
|
16925
|
+
const viewport = await page.evaluate(() => ({
|
|
16926
|
+
w: window.innerWidth,
|
|
16927
|
+
h: window.innerHeight
|
|
16928
|
+
}));
|
|
16929
|
+
const startX = viewport.w * (0.3 + Math.random() * 0.4);
|
|
16930
|
+
const startY = viewport.h * (0.3 + Math.random() * 0.4);
|
|
16931
|
+
await page.mouse.move(startX, startY);
|
|
16932
|
+
} catch {
|
|
16933
|
+
}
|
|
16934
|
+
}
|
|
16935
|
+
/**
|
|
16936
|
+
* Mark page as needing re-injection (after navigate).
|
|
16937
|
+
* Next inject() call will re-create the SVG.
|
|
16938
|
+
*/
|
|
16939
|
+
markDirty(page) {
|
|
16940
|
+
this.injectedPages.delete(page);
|
|
16941
|
+
this.cursorMap.delete(page);
|
|
16942
|
+
}
|
|
16943
|
+
/**
|
|
16944
|
+
* Bézier move + click element — uses ghost-cursor click() native.
|
|
16945
|
+
*
|
|
16946
|
+
* ghost-cursor click() flow:
|
|
16947
|
+
* 1. move(element) — Bézier curve + overshoot + scrollIntoView
|
|
16948
|
+
* 2. delay(hesitate) — pause before pressing (simulates human thinking)
|
|
16949
|
+
* 3. mouseDown via CDP
|
|
16950
|
+
* 4. delay(waitForClick) — hold mouse button (simulates real press-and-hold)
|
|
16951
|
+
* 5. mouseUp via CDP
|
|
16952
|
+
*
|
|
16953
|
+
* ⚠️ Default ghost-cursor: hesitate=0, waitForClick=0 (instant click = bot-like)
|
|
16954
|
+
* → We override with random hesitate 80-250ms, waitForClick 30-120ms
|
|
16955
|
+
*
|
|
16956
|
+
* For long distances, segmentedMove runs first to create visible cursor travel,
|
|
16957
|
+
* then ghost-cursor click handles the final short-range approach.
|
|
16958
|
+
*
|
|
16959
|
+
* @param options.clickCount - Number of clicks (e.g. 3 = select all text)
|
|
16960
|
+
* @param options.hesitate - Override hesitate (ms). Default: random 80-250ms
|
|
16961
|
+
* @param options.waitForClick - Override mousedown hold duration (ms). Default: random 30-120ms
|
|
16962
|
+
*/
|
|
16963
|
+
async clickElement(page, element, options) {
|
|
16964
|
+
var _a, _b, _c;
|
|
16965
|
+
if (!this.enabled) return false;
|
|
16966
|
+
try {
|
|
16967
|
+
if (page.isClosed()) return false;
|
|
16968
|
+
const cursor = this.getOrCreateCursor(page);
|
|
16969
|
+
const box = await element.boundingBox();
|
|
16970
|
+
if (box) {
|
|
16971
|
+
const targetX = box.x + box.width / 2;
|
|
16972
|
+
const targetY = box.y + box.height / 2;
|
|
16973
|
+
await this.segmentedMove(page, cursor, targetX, targetY);
|
|
16974
|
+
}
|
|
16975
|
+
const hesitate = (_a = options == null ? void 0 : options.hesitate) != null ? _a : 80 + Math.random() * 170;
|
|
16976
|
+
const waitForClick = (_b = options == null ? void 0 : options.waitForClick) != null ? _b : 30 + Math.random() * 90;
|
|
16977
|
+
await cursor.click(element, {
|
|
16978
|
+
hesitate,
|
|
16979
|
+
waitForClick,
|
|
16980
|
+
moveDelay: 0,
|
|
16981
|
+
// SDK handles delay via actionDelay() after click
|
|
16982
|
+
clickCount: (_c = options == null ? void 0 : options.clickCount) != null ? _c : 1
|
|
16983
|
+
});
|
|
16984
|
+
return true;
|
|
16985
|
+
} catch {
|
|
16986
|
+
return false;
|
|
16987
|
+
}
|
|
16988
|
+
}
|
|
16989
|
+
/**
|
|
16990
|
+
* Move cursor to coordinates via Bézier curve.
|
|
16991
|
+
* For long distances (> SEGMENT_THRESHOLD), splits into segments with delays.
|
|
16992
|
+
*/
|
|
16993
|
+
async moveTo(page, x, y) {
|
|
16994
|
+
if (!this.enabled) return;
|
|
16995
|
+
try {
|
|
16996
|
+
if (page.isClosed()) return;
|
|
16997
|
+
const cursor = this.getOrCreateCursor(page);
|
|
16998
|
+
await this.segmentedMove(page, cursor, x, y);
|
|
16999
|
+
} catch {
|
|
17000
|
+
}
|
|
17001
|
+
}
|
|
17002
|
+
/**
|
|
17003
|
+
* Move cursor through multiple small segments for long distances.
|
|
17004
|
+
*
|
|
17005
|
+
* Ghost-cursor dispatches all CDP mouseMoved events in a tight loop
|
|
17006
|
+
* with NO delay between steps → cursor "teleports" on long moves.
|
|
17007
|
+
*
|
|
17008
|
+
* Fix: split long paths into ~SEGMENT_SIZE px segments, each segment
|
|
17009
|
+
* is a single ghost-cursor moveTo (short Bézier curve), with
|
|
17010
|
+
* SEGMENT_DELAY ms pause between each segment.
|
|
17011
|
+
* → Cursor movement is visible and natural.
|
|
17012
|
+
*
|
|
17013
|
+
* The final segment always uses ghost-cursor moveTo directly for precision.
|
|
17014
|
+
*/
|
|
17015
|
+
async segmentedMove(page, cursor, targetX, targetY) {
|
|
17016
|
+
const currentPos = cursor.getLocation();
|
|
17017
|
+
const dx = targetX - currentPos.x;
|
|
17018
|
+
const dy = targetY - currentPos.y;
|
|
17019
|
+
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
17020
|
+
if (distance <= _HiraCursor.SEGMENT_THRESHOLD) {
|
|
17021
|
+
await cursor.moveTo({ x: targetX, y: targetY });
|
|
17022
|
+
return;
|
|
17023
|
+
}
|
|
17024
|
+
const segCount = Math.ceil(distance / _HiraCursor.SEGMENT_SIZE);
|
|
17025
|
+
const stepX = dx / segCount;
|
|
17026
|
+
const stepY = dy / segCount;
|
|
17027
|
+
for (let i = 1; i < segCount; i++) {
|
|
17028
|
+
if (page.isClosed()) return;
|
|
17029
|
+
const wx = currentPos.x + stepX * i + (Math.random() - 0.5) * 10;
|
|
17030
|
+
const wy = currentPos.y + stepY * i + (Math.random() - 0.5) * 10;
|
|
17031
|
+
await cursor.moveTo({ x: Math.max(0, wx), y: Math.max(0, wy) });
|
|
17032
|
+
const [minDelay, maxDelay] = _HiraCursor.SEGMENT_DELAY;
|
|
17033
|
+
const segDelay = minDelay + Math.random() * (maxDelay - minDelay);
|
|
17034
|
+
await new Promise((r) => setTimeout(r, segDelay));
|
|
17035
|
+
}
|
|
17036
|
+
await cursor.moveTo({ x: targetX, y: targetY });
|
|
17037
|
+
}
|
|
17038
|
+
/**
|
|
17039
|
+
* Smooth 60fps scroll — animation runs INSIDE browser via requestAnimationFrame.
|
|
17040
|
+
*
|
|
17041
|
+
* Behaves like native `scrollIntoView({ behavior: 'smooth' })`:
|
|
17042
|
+
* - True 60fps (requestAnimationFrame, no Node.js setTimeout jitter)
|
|
17043
|
+
* - Ease-in-out sine curve (accelerate → decelerate → smooth stop)
|
|
17044
|
+
* - Browser generates natural scroll events
|
|
17045
|
+
*
|
|
17046
|
+
* @param deltaY - Positive = scroll down, Negative = scroll up
|
|
17047
|
+
* @param speed - 1-100 (100 = instant, default: 50)
|
|
17048
|
+
*/
|
|
17049
|
+
async scrollDelta(page, deltaY, speed = 50) {
|
|
17050
|
+
try {
|
|
17051
|
+
if (page.isClosed()) return;
|
|
17052
|
+
const absDelta = Math.abs(deltaY);
|
|
17053
|
+
if (speed >= 100 || absDelta <= 5) {
|
|
17054
|
+
await page.evaluate((dy) => window.scrollBy(0, dy), deltaY);
|
|
17055
|
+
return;
|
|
17056
|
+
}
|
|
17057
|
+
const durationMs = Math.max(150, 1e3 - speed * 10);
|
|
17058
|
+
await page.evaluate((totalDelta, duration) => {
|
|
17059
|
+
return new Promise((resolve) => {
|
|
17060
|
+
let scrolled = 0;
|
|
17061
|
+
const start = performance.now();
|
|
17062
|
+
function frame(now) {
|
|
17063
|
+
const elapsed = now - start;
|
|
17064
|
+
const t = Math.min(elapsed / duration, 1);
|
|
17065
|
+
const eased = -(Math.cos(Math.PI * t) - 1) / 2;
|
|
17066
|
+
const target = eased * totalDelta;
|
|
17067
|
+
const delta = target - scrolled;
|
|
17068
|
+
scrolled = target;
|
|
17069
|
+
if (Math.abs(delta) > 0.5) {
|
|
17070
|
+
window.scrollBy(0, delta);
|
|
17071
|
+
}
|
|
17072
|
+
if (t < 1) {
|
|
17073
|
+
requestAnimationFrame(frame);
|
|
17074
|
+
} else {
|
|
17075
|
+
resolve();
|
|
17076
|
+
}
|
|
17077
|
+
}
|
|
17078
|
+
requestAnimationFrame(frame);
|
|
17079
|
+
});
|
|
17080
|
+
}, deltaY, durationMs);
|
|
17081
|
+
} catch {
|
|
17082
|
+
}
|
|
17083
|
+
}
|
|
17084
|
+
/**
|
|
17085
|
+
* Scroll a container element (with overflow) — smooth 60fps inside browser.
|
|
17086
|
+
* Requires a container selector instead of scrolling the window.
|
|
17087
|
+
*/
|
|
17088
|
+
async scrollContainerDelta(page, containerSelector, deltaY, speed = 50) {
|
|
17089
|
+
try {
|
|
17090
|
+
if (page.isClosed()) return;
|
|
17091
|
+
const durationMs = Math.max(150, 1e3 - speed * 10);
|
|
17092
|
+
await page.evaluate((selector, totalDelta, duration) => {
|
|
17093
|
+
return new Promise((resolve) => {
|
|
17094
|
+
const el = document.querySelector(selector);
|
|
17095
|
+
if (!el) {
|
|
17096
|
+
resolve();
|
|
17097
|
+
return;
|
|
17098
|
+
}
|
|
17099
|
+
let scrolled = 0;
|
|
17100
|
+
const start = performance.now();
|
|
17101
|
+
function frame(now) {
|
|
17102
|
+
const elapsed = now - start;
|
|
17103
|
+
const t = Math.min(elapsed / duration, 1);
|
|
17104
|
+
const eased = -(Math.cos(Math.PI * t) - 1) / 2;
|
|
17105
|
+
const target = eased * totalDelta;
|
|
17106
|
+
const delta = target - scrolled;
|
|
17107
|
+
scrolled = target;
|
|
17108
|
+
if (Math.abs(delta) > 0.5) {
|
|
17109
|
+
el.scrollBy(0, delta);
|
|
17110
|
+
}
|
|
17111
|
+
if (t < 1) {
|
|
17112
|
+
requestAnimationFrame(frame);
|
|
17113
|
+
} else {
|
|
17114
|
+
resolve();
|
|
17115
|
+
}
|
|
17116
|
+
}
|
|
17117
|
+
requestAnimationFrame(frame);
|
|
17118
|
+
});
|
|
17119
|
+
}, containerSelector, deltaY, durationMs);
|
|
17120
|
+
} catch {
|
|
17121
|
+
}
|
|
17122
|
+
}
|
|
17123
|
+
/**
|
|
17124
|
+
* Scroll to top or bottom — calculates distance then uses scrollDelta (smooth).
|
|
17125
|
+
*/
|
|
17126
|
+
async scrollToPosition(page, position, speed = 50) {
|
|
17127
|
+
try {
|
|
17128
|
+
if (page.isClosed()) return;
|
|
17129
|
+
const { scrollY, scrollHeight, innerHeight } = await page.evaluate(() => ({
|
|
17130
|
+
scrollY: window.scrollY,
|
|
17131
|
+
scrollHeight: document.body.scrollHeight,
|
|
17132
|
+
innerHeight: window.innerHeight
|
|
17133
|
+
}));
|
|
17134
|
+
let deltaY;
|
|
17135
|
+
if (position === "top") {
|
|
17136
|
+
deltaY = -scrollY;
|
|
17137
|
+
} else {
|
|
17138
|
+
deltaY = scrollHeight - innerHeight - scrollY;
|
|
17139
|
+
}
|
|
17140
|
+
if (Math.abs(deltaY) < 5) return;
|
|
17141
|
+
await this.scrollDelta(page, deltaY, speed);
|
|
17142
|
+
} catch {
|
|
17143
|
+
}
|
|
17144
|
+
}
|
|
17145
|
+
/**
|
|
17146
|
+
* Scroll element into viewport — calculates offset then uses scrollDelta (smooth).
|
|
17147
|
+
*/
|
|
17148
|
+
async scrollElementIntoView(page, element, speed = 50) {
|
|
17149
|
+
try {
|
|
17150
|
+
if (page.isClosed()) return;
|
|
17151
|
+
const box = await element.boundingBox();
|
|
17152
|
+
if (!box) return;
|
|
17153
|
+
const { innerHeight } = await page.evaluate(() => ({
|
|
17154
|
+
innerHeight: window.innerHeight
|
|
17155
|
+
}));
|
|
17156
|
+
const elementCenter = box.y + box.height / 2;
|
|
17157
|
+
const viewportCenter = innerHeight / 2;
|
|
17158
|
+
const deltaY = elementCenter - viewportCenter;
|
|
17159
|
+
if (Math.abs(deltaY) < 5) return;
|
|
17160
|
+
await this.scrollDelta(page, deltaY, speed);
|
|
17161
|
+
} catch {
|
|
17162
|
+
}
|
|
17163
|
+
}
|
|
17164
|
+
/**
|
|
17165
|
+
* Move cursor to center of a container — needed before scrolling a container.
|
|
17166
|
+
* Cursor ON: Bézier move (visible)
|
|
17167
|
+
* Cursor OFF: CDP mouse.move (hidden)
|
|
17168
|
+
*/
|
|
17169
|
+
async moveToContainer(page, containerSelector) {
|
|
17170
|
+
try {
|
|
17171
|
+
if (page.isClosed()) return;
|
|
17172
|
+
const el = await page.$(containerSelector);
|
|
17173
|
+
if (!el) return;
|
|
17174
|
+
const box = await el.boundingBox();
|
|
17175
|
+
if (!box) return;
|
|
17176
|
+
const x = box.x + box.width / 2 + (Math.random() - 0.5) * box.width * 0.3;
|
|
17177
|
+
const y = box.y + box.height / 2 + (Math.random() - 0.5) * box.height * 0.3;
|
|
17178
|
+
if (this.enabled) {
|
|
17179
|
+
const cursor = this.getOrCreateCursor(page);
|
|
17180
|
+
await cursor.moveTo({ x, y });
|
|
17181
|
+
} else {
|
|
17182
|
+
await page.mouse.move(x, y);
|
|
17183
|
+
}
|
|
17184
|
+
} catch {
|
|
17185
|
+
}
|
|
17186
|
+
}
|
|
17187
|
+
/** Get or create ghost-cursor instance for the given page */
|
|
17188
|
+
getOrCreateCursor(page) {
|
|
17189
|
+
let cursor = this.cursorMap.get(page);
|
|
17190
|
+
if (!cursor) {
|
|
17191
|
+
cursor = new import_ghost_cursor.GhostCursor(page, {
|
|
17192
|
+
defaultOptions: {
|
|
17193
|
+
// Override defaults for all actions — human-like timing
|
|
17194
|
+
click: {
|
|
17195
|
+
hesitate: 120,
|
|
17196
|
+
// default if not overridden per-call
|
|
17197
|
+
waitForClick: 60,
|
|
17198
|
+
// hold mouse button 60ms
|
|
17199
|
+
moveDelay: 0
|
|
17200
|
+
// SDK manages delay separately
|
|
17201
|
+
},
|
|
17202
|
+
move: {
|
|
17203
|
+
moveDelay: 0
|
|
17204
|
+
// SDK manages delay separately
|
|
17205
|
+
},
|
|
17206
|
+
moveTo: {
|
|
17207
|
+
moveDelay: 0
|
|
17208
|
+
}
|
|
17209
|
+
}
|
|
17210
|
+
});
|
|
17211
|
+
this.cursorMap.set(page, cursor);
|
|
17212
|
+
}
|
|
17213
|
+
return cursor;
|
|
17214
|
+
}
|
|
17215
|
+
};
|
|
17216
|
+
/**
|
|
17217
|
+
* Minimum distance (px) to activate segmented move.
|
|
17218
|
+
* Below threshold → ghost-cursor moves normally (fast enough, looks natural).
|
|
17219
|
+
* Above threshold → split into small segments with delay between each.
|
|
17220
|
+
*/
|
|
17221
|
+
_HiraCursor.SEGMENT_THRESHOLD = 500;
|
|
17222
|
+
/** Length of each segment (px) when splitting long-distance moves */
|
|
17223
|
+
_HiraCursor.SEGMENT_SIZE = 650;
|
|
17224
|
+
/** Delay between segments (ms) — randomized within this range */
|
|
17225
|
+
_HiraCursor.SEGMENT_DELAY = [30, 60];
|
|
17226
|
+
var HiraCursor = _HiraCursor;
|
|
17227
|
+
|
|
16815
17228
|
// src/utils/browser.utils.ts
|
|
16816
17229
|
var BrowserUtils = class {
|
|
16817
17230
|
constructor(context) {
|
|
16818
17231
|
/** Currently active iframe — null means main frame. Changed via activeIframe() */
|
|
16819
17232
|
this.activeFrame = null;
|
|
16820
|
-
var _a;
|
|
17233
|
+
var _a, _b;
|
|
16821
17234
|
this.ctx = context;
|
|
16822
17235
|
this.logger = context.logger;
|
|
16823
17236
|
this.outputDefs = (_a = context.outputDefinitions) != null ? _a : [];
|
|
16824
17237
|
this.validOutputKeys = new Set(this.outputDefs.map((d) => d.key));
|
|
16825
17238
|
this.defaultPage = context.page;
|
|
16826
17239
|
this.activePage = context.page;
|
|
17240
|
+
const cursorEnabled = ((_b = context.execution) == null ? void 0 : _b.virtualCursor) !== false;
|
|
17241
|
+
this.hiraCursor = new HiraCursor(cursorEnabled);
|
|
16827
17242
|
this.enableFocusEmulation(this.activePage).catch(() => {
|
|
16828
17243
|
});
|
|
17244
|
+
this.applyStealthScripts(this.activePage).catch(() => {
|
|
17245
|
+
});
|
|
17246
|
+
this.hiraCursor.inject(this.activePage).catch(() => {
|
|
17247
|
+
});
|
|
16829
17248
|
}
|
|
16830
17249
|
/**
|
|
16831
|
-
* CDP trick — Chromium
|
|
16832
|
-
*
|
|
17250
|
+
* CDP trick — Chromium thinks tab is always focused, not throttled
|
|
17251
|
+
* when user switches to another tab or clicks elsewhere.
|
|
16833
17252
|
*/
|
|
16834
17253
|
async enableFocusEmulation(page) {
|
|
16835
17254
|
try {
|
|
@@ -16838,10 +17257,68 @@ var BrowserUtils = class {
|
|
|
16838
17257
|
} catch {
|
|
16839
17258
|
}
|
|
16840
17259
|
}
|
|
17260
|
+
/**
|
|
17261
|
+
* Apply stealth scripts to remove automation fingerprints.
|
|
17262
|
+
* Uses evaluateOnNewDocument — runs BEFORE any page scripts on every navigation.
|
|
17263
|
+
* Persists across navigations on the same page instance.
|
|
17264
|
+
*/
|
|
17265
|
+
async applyStealthScripts(page) {
|
|
17266
|
+
try {
|
|
17267
|
+
const stealthFn = () => {
|
|
17268
|
+
Object.defineProperty(navigator, "webdriver", {
|
|
17269
|
+
get: () => void 0
|
|
17270
|
+
});
|
|
17271
|
+
Object.defineProperty(navigator, "plugins", {
|
|
17272
|
+
get: () => [1, 2, 3, 4, 5]
|
|
17273
|
+
});
|
|
17274
|
+
Object.defineProperty(navigator, "languages", {
|
|
17275
|
+
get: () => ["en-US", "en"]
|
|
17276
|
+
});
|
|
17277
|
+
if (window.cdc_adoQpoasnfa76pfcZLmcfl_Array) {
|
|
17278
|
+
delete window.cdc_adoQpoasnfa76pfcZLmcfl_Array;
|
|
17279
|
+
}
|
|
17280
|
+
if (window.cdc_adoQpoasnfa76pfcZLmcfl_Promise) {
|
|
17281
|
+
delete window.cdc_adoQpoasnfa76pfcZLmcfl_Promise;
|
|
17282
|
+
}
|
|
17283
|
+
if (window.cdc_adoQpoasnfa76pfcZLmcfl_Symbol) {
|
|
17284
|
+
delete window.cdc_adoQpoasnfa76pfcZLmcfl_Symbol;
|
|
17285
|
+
}
|
|
17286
|
+
};
|
|
17287
|
+
await page.evaluate(stealthFn);
|
|
17288
|
+
await page.evaluateOnNewDocument(stealthFn);
|
|
17289
|
+
} catch {
|
|
17290
|
+
}
|
|
17291
|
+
}
|
|
16841
17292
|
checkAbort() {
|
|
16842
17293
|
const signal = global.__HIRA_ABORT_SIGNAL__;
|
|
16843
17294
|
if (signal == null ? void 0 : signal.aborted) throw new Error("cancelled");
|
|
16844
17295
|
}
|
|
17296
|
+
/**
|
|
17297
|
+
* Scroll element vào viewport mượt mà — segments + delay.
|
|
17298
|
+
* Chỉ scroll nếu element nằm ngoài viewport.
|
|
17299
|
+
*/
|
|
17300
|
+
async smoothScrollToElement(element) {
|
|
17301
|
+
try {
|
|
17302
|
+
for (let i = 0; i < 15; i++) {
|
|
17303
|
+
const box = await element.boundingBox();
|
|
17304
|
+
if (!box) return;
|
|
17305
|
+
const vh = await this.activePage.evaluate(() => window.innerHeight);
|
|
17306
|
+
const center = box.y + box.height / 2;
|
|
17307
|
+
const margin = vh * 0.15;
|
|
17308
|
+
if (center >= margin && center <= vh - margin) break;
|
|
17309
|
+
const delta = center - vh / 2;
|
|
17310
|
+
const maxSeg = 300 + Math.random() * 200;
|
|
17311
|
+
const segment = Math.abs(delta) > maxSeg ? delta > 0 ? maxSeg : -maxSeg : delta;
|
|
17312
|
+
await this.hiraCursor.scrollDelta(this.activePage, segment, 50);
|
|
17313
|
+
await this.rawSleep(150 + Math.random() * 200);
|
|
17314
|
+
}
|
|
17315
|
+
} catch {
|
|
17316
|
+
try {
|
|
17317
|
+
await element.scrollIntoView();
|
|
17318
|
+
} catch {
|
|
17319
|
+
}
|
|
17320
|
+
}
|
|
17321
|
+
}
|
|
16845
17322
|
/**
|
|
16846
17323
|
* Pause execution for the given duration.
|
|
16847
17324
|
*
|
|
@@ -16899,10 +17376,11 @@ var BrowserUtils = class {
|
|
|
16899
17376
|
return target.$(resolved);
|
|
16900
17377
|
}
|
|
16901
17378
|
/**
|
|
16902
|
-
* Click on an element
|
|
17379
|
+
* Click on an element or at specific coordinates.
|
|
17380
|
+
* Scrolls the element into view before clicking.
|
|
16903
17381
|
* Throws if the element is not found.
|
|
16904
17382
|
*
|
|
16905
|
-
* @param target - CSS selector, XPath, or
|
|
17383
|
+
* @param target - CSS selector, XPath, ElementHandle, or `{ x, y }` coordinates
|
|
16906
17384
|
* @param options.delay - Delay in ms before clicking (default: 1000)
|
|
16907
17385
|
* @param options.waitTimeout - Max wait time for element to appear (default: 2000)
|
|
16908
17386
|
* @param options.frame - Optional Frame to search within
|
|
@@ -16911,10 +17389,31 @@ var BrowserUtils = class {
|
|
|
16911
17389
|
* @example
|
|
16912
17390
|
* await utils.click("#submit-btn");
|
|
16913
17391
|
* await utils.click("#btn", { delay: 0 }); // click immediately
|
|
16914
|
-
* await utils.click(
|
|
17392
|
+
* await utils.click({ x: 500, y: 300 }); // click at coordinates
|
|
17393
|
+
* const pos = await utils.getPosition("#btn", { randomXY: true });
|
|
17394
|
+
* await utils.click(pos!); // click random point inside element
|
|
16915
17395
|
*/
|
|
16916
17396
|
async click(target, options) {
|
|
16917
|
-
var _a, _b;
|
|
17397
|
+
var _a, _b, _c, _d, _e;
|
|
17398
|
+
if (typeof target === "object" && !(target instanceof import_puppeteer_core3.ElementHandle) && "x" in target && "y" in target) {
|
|
17399
|
+
try {
|
|
17400
|
+
this.checkAbort();
|
|
17401
|
+
await this.log("info", `\u{1F5B1}\uFE0F Click: (${Math.round(target.x)}, ${Math.round(target.y)})`);
|
|
17402
|
+
const clickDelay = (_a = options == null ? void 0 : options.delay) != null ? _a : 1e3;
|
|
17403
|
+
if (clickDelay > 0) await this.sleep(clickDelay);
|
|
17404
|
+
if (this.hiraCursor.isEnabled) {
|
|
17405
|
+
await this.hiraCursor.moveTo(this.activePage, target.x, target.y);
|
|
17406
|
+
await this.rawSleep(50 + Math.random() * 50);
|
|
17407
|
+
}
|
|
17408
|
+
await this.activePage.mouse.click(target.x, target.y);
|
|
17409
|
+
await this.actionDelay();
|
|
17410
|
+
return true;
|
|
17411
|
+
} catch (error) {
|
|
17412
|
+
if (error instanceof Error && error.message === "cancelled") throw error;
|
|
17413
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
17414
|
+
throw new Error(`Click failed: (${target.x}, ${target.y}) \u2014 ${msg}`);
|
|
17415
|
+
}
|
|
17416
|
+
}
|
|
16918
17417
|
const label = typeof target === "string" ? target : "[ElementHandle]";
|
|
16919
17418
|
try {
|
|
16920
17419
|
this.checkAbort();
|
|
@@ -16923,19 +17422,32 @@ var BrowserUtils = class {
|
|
|
16923
17422
|
if (typeof target === "string") {
|
|
16924
17423
|
element = await this.waitForElement(
|
|
16925
17424
|
target,
|
|
16926
|
-
(
|
|
17425
|
+
(_b = options == null ? void 0 : options.waitTimeout) != null ? _b : 2e3,
|
|
16927
17426
|
options == null ? void 0 : options.frame
|
|
16928
17427
|
);
|
|
17428
|
+
if (!element) {
|
|
17429
|
+
const scope = (_d = (_c = options == null ? void 0 : options.frame) != null ? _c : this.activeFrame) != null ? _d : this.activePage;
|
|
17430
|
+
const resolved = target.startsWith("//") || target.startsWith("(") ? `xpath/${target}` : target;
|
|
17431
|
+
element = await scope.$(resolved);
|
|
17432
|
+
}
|
|
16929
17433
|
} else {
|
|
16930
17434
|
element = target;
|
|
16931
17435
|
}
|
|
16932
17436
|
if (!element) {
|
|
16933
17437
|
throw new Error(`Click failed \u2014 element not found: ${this.shortSelector(label)}`);
|
|
16934
17438
|
}
|
|
16935
|
-
await
|
|
16936
|
-
const clickDelay = (
|
|
17439
|
+
await this.smoothScrollToElement(element);
|
|
17440
|
+
const clickDelay = (_e = options == null ? void 0 : options.delay) != null ? _e : 1e3;
|
|
16937
17441
|
if (clickDelay > 0) await this.sleep(clickDelay);
|
|
16938
|
-
await element.
|
|
17442
|
+
const box = await element.boundingBox();
|
|
17443
|
+
if (this.hiraCursor.isEnabled && !this.activeFrame && !(options == null ? void 0 : options.frame) && box) {
|
|
17444
|
+
const clicked = await this.hiraCursor.clickElement(this.activePage, element);
|
|
17445
|
+
if (!clicked) {
|
|
17446
|
+
await element.click();
|
|
17447
|
+
}
|
|
17448
|
+
} else {
|
|
17449
|
+
await element.click();
|
|
17450
|
+
}
|
|
16939
17451
|
await this.actionDelay();
|
|
16940
17452
|
return true;
|
|
16941
17453
|
} catch (error) {
|
|
@@ -16944,6 +17456,95 @@ var BrowserUtils = class {
|
|
|
16944
17456
|
throw new Error(`Click failed: ${this.shortSelector(label)} \u2014 ${msg}`);
|
|
16945
17457
|
}
|
|
16946
17458
|
}
|
|
17459
|
+
/**
|
|
17460
|
+
* Get the position (x, y) of an element.
|
|
17461
|
+
* Returns center point by default, or a random point within bounds with `randomXY`.
|
|
17462
|
+
*
|
|
17463
|
+
* @param selector - CSS selector or XPath
|
|
17464
|
+
* @param options.randomXY - If true, returns random point within element (10% margin from edges)
|
|
17465
|
+
* @param options.waitTimeout - Max wait time for element (default: 2000ms)
|
|
17466
|
+
* @param options.frame - Optional Frame to search within
|
|
17467
|
+
* @returns `{ x, y }` or null if element not found / no bounding box
|
|
17468
|
+
*
|
|
17469
|
+
* @example
|
|
17470
|
+
* const center = await utils.getPosition("#btn");
|
|
17471
|
+
* const rand = await utils.getPosition("#btn", { randomXY: true });
|
|
17472
|
+
* if (rand) await utils.click(rand);
|
|
17473
|
+
*/
|
|
17474
|
+
async getPosition(selector, options) {
|
|
17475
|
+
var _a;
|
|
17476
|
+
try {
|
|
17477
|
+
this.checkAbort();
|
|
17478
|
+
const element = await this.waitForElement(
|
|
17479
|
+
selector,
|
|
17480
|
+
(_a = options == null ? void 0 : options.waitTimeout) != null ? _a : 2e3,
|
|
17481
|
+
options == null ? void 0 : options.frame
|
|
17482
|
+
);
|
|
17483
|
+
if (!element) return null;
|
|
17484
|
+
await this.smoothScrollToElement(element);
|
|
17485
|
+
const box = await element.boundingBox();
|
|
17486
|
+
if (!box) return null;
|
|
17487
|
+
if (options == null ? void 0 : options.randomXY) {
|
|
17488
|
+
const mx = box.width * 0.1;
|
|
17489
|
+
const my = box.height * 0.1;
|
|
17490
|
+
return {
|
|
17491
|
+
x: box.x + mx + Math.random() * (box.width - 2 * mx),
|
|
17492
|
+
y: box.y + my + Math.random() * (box.height - 2 * my)
|
|
17493
|
+
};
|
|
17494
|
+
}
|
|
17495
|
+
return {
|
|
17496
|
+
x: box.x + box.width / 2,
|
|
17497
|
+
y: box.y + box.height / 2
|
|
17498
|
+
};
|
|
17499
|
+
} catch (error) {
|
|
17500
|
+
if (error instanceof Error && error.message === "cancelled") throw error;
|
|
17501
|
+
await this.log("warn", `\u26A0\uFE0F getPosition failed: ${this.shortSelector(selector)}`);
|
|
17502
|
+
return null;
|
|
17503
|
+
}
|
|
17504
|
+
}
|
|
17505
|
+
/**
|
|
17506
|
+
* Select a value from a `<select>` dropdown.
|
|
17507
|
+
* Human-like flow: cursor moves to select → click to open → page.select() → close.
|
|
17508
|
+
*
|
|
17509
|
+
* NOTE: Native `<option>` elements CAN NOT be clicked via Puppeteer
|
|
17510
|
+
* ("Node is either not clickable or not an Element").
|
|
17511
|
+
* `page.select()` is the ONLY reliable method.
|
|
17512
|
+
*
|
|
17513
|
+
* @param selector - CSS selector or XPath of the `<select>` element
|
|
17514
|
+
* @param value - The `value` attribute of the option to select
|
|
17515
|
+
* @param options.delay - Delay before clicking (default: 500ms)
|
|
17516
|
+
* @param options.waitTimeout - Max wait for element (default: 2000ms)
|
|
17517
|
+
*
|
|
17518
|
+
* @example
|
|
17519
|
+
* await utils.select("#country", "vn");
|
|
17520
|
+
*/
|
|
17521
|
+
async select(selector, value, options) {
|
|
17522
|
+
var _a, _b, _c;
|
|
17523
|
+
try {
|
|
17524
|
+
this.checkAbort();
|
|
17525
|
+
await this.log(
|
|
17526
|
+
"info",
|
|
17527
|
+
`\u{1F4CB} Select "${value}" \u2192 ${this.shortSelector(selector)}`
|
|
17528
|
+
);
|
|
17529
|
+
await this.click(selector, {
|
|
17530
|
+
delay: (_a = options == null ? void 0 : options.delay) != null ? _a : 500,
|
|
17531
|
+
waitTimeout: options == null ? void 0 : options.waitTimeout,
|
|
17532
|
+
frame: options == null ? void 0 : options.frame
|
|
17533
|
+
});
|
|
17534
|
+
await this.rawSleep(300 + Math.random() * 200);
|
|
17535
|
+
const target = (_c = (_b = options == null ? void 0 : options.frame) != null ? _b : this.activeFrame) != null ? _c : this.activePage;
|
|
17536
|
+
const resolved = selector.startsWith("//") || selector.startsWith("(") ? `xpath/${selector}` : selector;
|
|
17537
|
+
await target.select(resolved, value);
|
|
17538
|
+
await this.rawSleep(100);
|
|
17539
|
+
await this.activePage.keyboard.press("Escape");
|
|
17540
|
+
await this.actionDelay();
|
|
17541
|
+
return true;
|
|
17542
|
+
} catch (error) {
|
|
17543
|
+
if (error instanceof Error && error.message === "cancelled") throw error;
|
|
17544
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
17545
|
+
throw new Error(`Select failed: ${this.shortSelector(selector)} \u2014 ${msg}`);
|
|
17546
|
+
}
|
|
17547
|
+
}
|
|
16947
17548
|
/**
|
|
16948
17549
|
* Type text into an input element. Throws if the element is not found.
|
|
16949
17550
|
*
|
|
@@ -16981,8 +17582,15 @@ var BrowserUtils = class {
|
|
|
16981
17582
|
if (!element) {
|
|
16982
17583
|
throw new Error(`Type failed \u2014 element not found: ${this.shortSelector(selector)}`);
|
|
16983
17584
|
}
|
|
16984
|
-
await
|
|
17585
|
+
await this.smoothScrollToElement(element);
|
|
16985
17586
|
if (mode === "paste") {
|
|
17587
|
+
if (this.hiraCursor.isEnabled && !this.activeFrame && !(options == null ? void 0 : options.frame)) {
|
|
17588
|
+
const clicked = await this.hiraCursor.clickElement(this.activePage, element);
|
|
17589
|
+
if (!clicked) await element.click();
|
|
17590
|
+
} else {
|
|
17591
|
+
await element.click();
|
|
17592
|
+
}
|
|
17593
|
+
await this.rawSleep(100 + Math.random() * 100);
|
|
16986
17594
|
const target = (_b = this.activeFrame) != null ? _b : this.activePage;
|
|
16987
17595
|
await target.evaluate(
|
|
16988
17596
|
(el, val) => {
|
|
@@ -16994,10 +17602,24 @@ var BrowserUtils = class {
|
|
|
16994
17602
|
text
|
|
16995
17603
|
);
|
|
16996
17604
|
} else if (mode === "append") {
|
|
16997
|
-
|
|
17605
|
+
if (this.hiraCursor.isEnabled && !this.activeFrame && !(options == null ? void 0 : options.frame)) {
|
|
17606
|
+
const clicked = await this.hiraCursor.clickElement(this.activePage, element);
|
|
17607
|
+
if (!clicked) await element.click();
|
|
17608
|
+
} else {
|
|
17609
|
+
await element.click();
|
|
17610
|
+
}
|
|
17611
|
+
await element.press("End");
|
|
16998
17612
|
await element.type(text, { delay: (_c = options == null ? void 0 : options.delay) != null ? _c : 50 });
|
|
16999
17613
|
} else {
|
|
17000
|
-
|
|
17614
|
+
if (this.hiraCursor.isEnabled && !this.activeFrame && !(options == null ? void 0 : options.frame)) {
|
|
17615
|
+
const clicked = await this.hiraCursor.clickElement(this.activePage, element);
|
|
17616
|
+
if (!clicked) await element.click();
|
|
17617
|
+
} else {
|
|
17618
|
+
await element.click();
|
|
17619
|
+
}
|
|
17620
|
+
await this.activePage.keyboard.down("Control");
|
|
17621
|
+
await this.activePage.keyboard.press("a");
|
|
17622
|
+
await this.activePage.keyboard.up("Control");
|
|
17001
17623
|
await element.press("Backspace");
|
|
17002
17624
|
await element.type(text, { delay: (_d = options == null ? void 0 : options.delay) != null ? _d : 50 });
|
|
17003
17625
|
}
|
|
@@ -17081,11 +17703,13 @@ var BrowserUtils = class {
|
|
|
17081
17703
|
try {
|
|
17082
17704
|
this.checkAbort();
|
|
17083
17705
|
await this.log("info", `\u{1F310} Navigate \u2192 ${url2}`);
|
|
17706
|
+
this.hiraCursor.markDirty(this.activePage);
|
|
17084
17707
|
await this.activePage.goto(url2, {
|
|
17085
17708
|
waitUntil: (options == null ? void 0 : options.waitUntil) || "load",
|
|
17086
17709
|
timeout: 6e4
|
|
17087
17710
|
});
|
|
17088
17711
|
await this.actionDelay();
|
|
17712
|
+
await this.hiraCursor.inject(this.activePage);
|
|
17089
17713
|
return true;
|
|
17090
17714
|
} catch (error) {
|
|
17091
17715
|
if (error instanceof Error && error.message === "cancelled") throw error;
|
|
@@ -17093,6 +17717,201 @@ var BrowserUtils = class {
|
|
|
17093
17717
|
throw new Error(`Navigate failed: ${url2} \u2014 ${msg}`);
|
|
17094
17718
|
}
|
|
17095
17719
|
}
|
|
17720
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
17721
|
+
// Scroll methods — CDP mouseWheel (cả cursor ON và OFF đều giống người)
|
|
17722
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
17723
|
+
/**
|
|
17724
|
+
* Scroll the page or a container by a specific amount.
|
|
17725
|
+
* Uses CDP mouseWheel events (human-like, not JS scrollBy).
|
|
17726
|
+
*
|
|
17727
|
+
* @param direction - "up" or "down"
|
|
17728
|
+
* @param amount - Pixels to scroll
|
|
17729
|
+
* @param options.container - CSS selector of scroll container
|
|
17730
|
+
* @param options.speed - Scroll speed 1-100 (default: 50)
|
|
17731
|
+
*
|
|
17732
|
+
* @example
|
|
17733
|
+
* await utils.scroll("down", 500);
|
|
17734
|
+
* await utils.scroll("up", 200, { container: "#chat-messages" });
|
|
17735
|
+
*/
|
|
17736
|
+
async scroll(direction, amount, options) {
|
|
17737
|
+
var _a, _b;
|
|
17738
|
+
try {
|
|
17739
|
+
this.checkAbort();
|
|
17740
|
+
const arrow = direction === "down" ? "\u2193" : "\u2191";
|
|
17741
|
+
const containerLabel = (options == null ? void 0 : options.container) ? ` (${this.shortSelector(options.container)})` : "";
|
|
17742
|
+
await this.log("info", `\u{1F4DC} Scroll ${arrow} ${amount}px${containerLabel}`);
|
|
17743
|
+
if (options == null ? void 0 : options.container) {
|
|
17744
|
+
await this.hiraCursor.moveToContainer(this.activePage, options.container);
|
|
17745
|
+
}
|
|
17746
|
+
const deltaY = direction === "down" ? amount : -amount;
|
|
17747
|
+
if (options == null ? void 0 : options.container) {
|
|
17748
|
+
await this.hiraCursor.scrollContainerDelta(this.activePage, options.container, deltaY, (_a = options == null ? void 0 : options.speed) != null ? _a : 50);
|
|
17749
|
+
} else {
|
|
17750
|
+
await this.hiraCursor.scrollDelta(this.activePage, deltaY, (_b = options == null ? void 0 : options.speed) != null ? _b : 50);
|
|
17751
|
+
}
|
|
17752
|
+
await this.actionDelay();
|
|
17753
|
+
} catch (error) {
|
|
17754
|
+
if (error instanceof Error && error.message === "cancelled") throw error;
|
|
17755
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
17756
|
+
await this.log("warn", `\u{1F4DC} Scroll failed: ${msg}`);
|
|
17757
|
+
}
|
|
17758
|
+
}
|
|
17759
|
+
/**
|
|
17760
|
+
* Scroll to the top or bottom of the page.
|
|
17761
|
+
* Scrolls in segments with pauses — giống người lướt dần đến đích.
|
|
17762
|
+
*
|
|
17763
|
+
* @param position - "top" or "bottom"
|
|
17764
|
+
* @param options.speed - Scroll speed 1-100 (default: 50)
|
|
17765
|
+
*
|
|
17766
|
+
* @example
|
|
17767
|
+
* await utils.scrollTo("bottom");
|
|
17768
|
+
* await utils.scrollTo("top");
|
|
17769
|
+
*/
|
|
17770
|
+
async scrollTo(position, options) {
|
|
17771
|
+
var _a;
|
|
17772
|
+
try {
|
|
17773
|
+
this.checkAbort();
|
|
17774
|
+
await this.log("info", `\u{1F4DC} Scroll to ${position}`);
|
|
17775
|
+
const speed = (_a = options == null ? void 0 : options.speed) != null ? _a : 50;
|
|
17776
|
+
const { scrollY, scrollHeight, innerHeight } = await this.activePage.evaluate(() => ({
|
|
17777
|
+
scrollY: window.scrollY,
|
|
17778
|
+
scrollHeight: document.body.scrollHeight,
|
|
17779
|
+
innerHeight: window.innerHeight
|
|
17780
|
+
}));
|
|
17781
|
+
let remaining = position === "top" ? scrollY : scrollHeight - innerHeight - scrollY;
|
|
17782
|
+
if (remaining < 5) return;
|
|
17783
|
+
const direction = position === "top" ? -1 : 1;
|
|
17784
|
+
while (remaining > 5) {
|
|
17785
|
+
this.checkAbort();
|
|
17786
|
+
const segment = Math.min(300 + Math.random() * 300, remaining);
|
|
17787
|
+
await this.hiraCursor.scrollDelta(this.activePage, direction * segment, speed);
|
|
17788
|
+
remaining -= segment;
|
|
17789
|
+
if (remaining > 5) {
|
|
17790
|
+
await this.sleep(200 + Math.random() * 300);
|
|
17791
|
+
}
|
|
17792
|
+
}
|
|
17793
|
+
await this.actionDelay();
|
|
17794
|
+
} catch (error) {
|
|
17795
|
+
if (error instanceof Error && error.message === "cancelled") throw error;
|
|
17796
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
17797
|
+
await this.log("warn", `\u{1F4DC} Scroll to ${position} failed: ${msg}`);
|
|
17798
|
+
}
|
|
17799
|
+
}
|
|
17800
|
+
/**
|
|
17801
|
+
* Scroll an element into the viewport.
|
|
17802
|
+
* Scrolls in segments with pauses — không nhảy tới đích.
|
|
17803
|
+
*
|
|
17804
|
+
* @param selector - CSS selector or XPath of the element
|
|
17805
|
+
* @param options.speed - Scroll speed 1-100 (default: 50)
|
|
17806
|
+
* @returns true if element found and scrolled
|
|
17807
|
+
*
|
|
17808
|
+
* @example
|
|
17809
|
+
* await utils.scrollToElement("#footer");
|
|
17810
|
+
* await utils.scrollToElement("//button[text()='Load more']");
|
|
17811
|
+
*/
|
|
17812
|
+
async scrollToElement(selector, options) {
|
|
17813
|
+
var _a;
|
|
17814
|
+
try {
|
|
17815
|
+
this.checkAbort();
|
|
17816
|
+
await this.log("info", `\u{1F4DC} Scroll to element: ${this.shortSelector(selector)}`);
|
|
17817
|
+
const element = await this.resolveElement(selector);
|
|
17818
|
+
if (!element) {
|
|
17819
|
+
await this.log("warn", `\u{1F4DC} Element not found: ${this.shortSelector(selector)}`);
|
|
17820
|
+
return false;
|
|
17821
|
+
}
|
|
17822
|
+
const speed = (_a = options == null ? void 0 : options.speed) != null ? _a : 50;
|
|
17823
|
+
for (let attempt = 0; attempt < 20; attempt++) {
|
|
17824
|
+
this.checkAbort();
|
|
17825
|
+
const box = await element.boundingBox();
|
|
17826
|
+
if (!box) return false;
|
|
17827
|
+
const viewportHeight = await this.activePage.evaluate(() => window.innerHeight);
|
|
17828
|
+
const elementCenter = box.y + box.height / 2;
|
|
17829
|
+
const viewportCenter = viewportHeight / 2;
|
|
17830
|
+
const deltaY = elementCenter - viewportCenter;
|
|
17831
|
+
if (Math.abs(deltaY) < 50) break;
|
|
17832
|
+
const maxSegment = 300 + Math.random() * 200;
|
|
17833
|
+
const segment = Math.abs(deltaY) > maxSegment ? deltaY > 0 ? maxSegment : -maxSegment : deltaY;
|
|
17834
|
+
await this.hiraCursor.scrollDelta(this.activePage, segment, speed);
|
|
17835
|
+
await this.sleep(200 + Math.random() * 300);
|
|
17836
|
+
}
|
|
17837
|
+
await this.actionDelay();
|
|
17838
|
+
return true;
|
|
17839
|
+
} catch (error) {
|
|
17840
|
+
if (error instanceof Error && error.message === "cancelled") throw error;
|
|
17841
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
17842
|
+
await this.log("warn", `\u{1F4DC} Scroll to element failed: ${msg}`);
|
|
17843
|
+
return false;
|
|
17844
|
+
}
|
|
17845
|
+
}
|
|
17846
|
+
/**
|
|
17847
|
+
* Simulate natural browsing scroll — cuộn xuống xen kẽ lướt ngược, delay random.
|
|
17848
|
+
* Giống người đang đọc/lướt web tự nhiên.
|
|
17849
|
+
*
|
|
17850
|
+
* @param options.duration - Thời gian scroll tính bằng giây (default: 5)
|
|
17851
|
+
* @param options.direction - Main direction (default: "down")
|
|
17852
|
+
* @param options.backChance - Chance to scroll back 0-1 (default: 0.15)
|
|
17853
|
+
* @param options.backAmount - [min, max] px to scroll back (default: [50, 150])
|
|
17854
|
+
* @param options.stepSize - [min, max] px per step (default: [200, 400])
|
|
17855
|
+
* @param options.stepDelay - [min, max] ms delay between steps (default: [300, 800])
|
|
17856
|
+
* @param options.container - CSS selector of scroll container
|
|
17857
|
+
*
|
|
17858
|
+
* @example
|
|
17859
|
+
* await utils.randomScroll(); // 5s lướt xuống
|
|
17860
|
+
* await utils.randomScroll({ duration: 10 }); // 10s
|
|
17861
|
+
* await utils.randomScroll({ backChance: 0.3, container: "#feed" });
|
|
17862
|
+
*/
|
|
17863
|
+
async randomScroll(options) {
|
|
17864
|
+
var _a, _b, _c, _d, _e, _f;
|
|
17865
|
+
try {
|
|
17866
|
+
this.checkAbort();
|
|
17867
|
+
const duration = ((_a = options == null ? void 0 : options.duration) != null ? _a : 5) * 1e3;
|
|
17868
|
+
const direction = (_b = options == null ? void 0 : options.direction) != null ? _b : "down";
|
|
17869
|
+
const backChance = (_c = options == null ? void 0 : options.backChance) != null ? _c : 0.15;
|
|
17870
|
+
const backAmount = (_d = options == null ? void 0 : options.backAmount) != null ? _d : [50, 150];
|
|
17871
|
+
const stepSize = (_e = options == null ? void 0 : options.stepSize) != null ? _e : [200, 400];
|
|
17872
|
+
const stepDelay = (_f = options == null ? void 0 : options.stepDelay) != null ? _f : [300, 800];
|
|
17873
|
+
const arrow = direction === "down" ? "\u2193" : "\u2191";
|
|
17874
|
+
const containerLabel = (options == null ? void 0 : options.container) ? ` (${this.shortSelector(options.container)})` : "";
|
|
17875
|
+
const startTime = Date.now();
|
|
17876
|
+
const sign = direction === "down" ? 1 : -1;
|
|
17877
|
+
if (options == null ? void 0 : options.container) {
|
|
17878
|
+
await this.hiraCursor.moveToContainer(this.activePage, options.container);
|
|
17879
|
+
}
|
|
17880
|
+
let steps = 0;
|
|
17881
|
+
let totalScrolled = 0;
|
|
17882
|
+
while (Date.now() - startTime < duration) {
|
|
17883
|
+
this.checkAbort();
|
|
17884
|
+
if (steps > 0 && Math.random() < backChance) {
|
|
17885
|
+
const back = backAmount[0] + Math.random() * (backAmount[1] - backAmount[0]);
|
|
17886
|
+
if (options == null ? void 0 : options.container) {
|
|
17887
|
+
await this.hiraCursor.scrollContainerDelta(this.activePage, options.container, -sign * back, 40);
|
|
17888
|
+
} else {
|
|
17889
|
+
await this.hiraCursor.scrollDelta(this.activePage, -sign * back, 40);
|
|
17890
|
+
}
|
|
17891
|
+
} else {
|
|
17892
|
+
const step = stepSize[0] + Math.random() * (stepSize[1] - stepSize[0]);
|
|
17893
|
+
if (options == null ? void 0 : options.container) {
|
|
17894
|
+
await this.hiraCursor.scrollContainerDelta(this.activePage, options.container, sign * step, 40);
|
|
17895
|
+
} else {
|
|
17896
|
+
await this.hiraCursor.scrollDelta(this.activePage, sign * step, 40);
|
|
17897
|
+
}
|
|
17898
|
+
totalScrolled += step;
|
|
17899
|
+
}
|
|
17900
|
+
steps++;
|
|
17901
|
+
if (Date.now() - startTime < duration) {
|
|
17902
|
+
const delay = stepDelay[0] + Math.random() * (stepDelay[1] - stepDelay[0]);
|
|
17903
|
+
await this.sleep(delay);
|
|
17904
|
+
}
|
|
17905
|
+
}
|
|
17906
|
+
const elapsed = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
17907
|
+
await this.log("info", `\u{1F5B1}\uFE0F Random scroll ${arrow} ~${Math.round(totalScrolled)}px${containerLabel} \u2014 ${steps} steps, ${elapsed}s`);
|
|
17908
|
+
await this.actionDelay();
|
|
17909
|
+
} catch (error) {
|
|
17910
|
+
if (error instanceof Error && error.message === "cancelled") throw error;
|
|
17911
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
17912
|
+
await this.log("warn", `\u{1F5B1}\uFE0F Random scroll failed: ${msg}`);
|
|
17913
|
+
}
|
|
17914
|
+
}
|
|
17096
17915
|
/**
|
|
17097
17916
|
* Wait for the current page to complete a navigation (e.g. after a form submit).
|
|
17098
17917
|
* Throws on timeout.
|
|
@@ -17321,8 +18140,10 @@ var BrowserUtils = class {
|
|
|
17321
18140
|
if (page) {
|
|
17322
18141
|
await page.bringToFront();
|
|
17323
18142
|
await this.enableFocusEmulation(page);
|
|
18143
|
+
await this.applyStealthScripts(page);
|
|
17324
18144
|
this.activePage = page;
|
|
17325
18145
|
this.activeFrame = null;
|
|
18146
|
+
await this.hiraCursor.inject(page);
|
|
17326
18147
|
}
|
|
17327
18148
|
return page;
|
|
17328
18149
|
}
|
|
@@ -17362,8 +18183,10 @@ var BrowserUtils = class {
|
|
|
17362
18183
|
if (page) {
|
|
17363
18184
|
await page.bringToFront();
|
|
17364
18185
|
await this.enableFocusEmulation(page);
|
|
18186
|
+
await this.applyStealthScripts(page);
|
|
17365
18187
|
this.activePage = page;
|
|
17366
18188
|
this.activeFrame = null;
|
|
18189
|
+
await this.hiraCursor.inject(page);
|
|
17367
18190
|
}
|
|
17368
18191
|
return page;
|
|
17369
18192
|
}
|
|
@@ -17390,8 +18213,10 @@ var BrowserUtils = class {
|
|
|
17390
18213
|
const page = pages[index];
|
|
17391
18214
|
await page.bringToFront();
|
|
17392
18215
|
await this.enableFocusEmulation(page);
|
|
18216
|
+
await this.applyStealthScripts(page);
|
|
17393
18217
|
this.activePage = page;
|
|
17394
18218
|
this.activeFrame = null;
|
|
18219
|
+
await this.hiraCursor.inject(page);
|
|
17395
18220
|
return page;
|
|
17396
18221
|
}
|
|
17397
18222
|
throw new Error(`Tab[${index}] not found (total: ${pages.length})`);
|
|
@@ -17464,6 +18289,7 @@ var BrowserUtils = class {
|
|
|
17464
18289
|
}
|
|
17465
18290
|
this.activePage = this.defaultPage;
|
|
17466
18291
|
this.activeFrame = null;
|
|
18292
|
+
await this.hiraCursor.inject(this.activePage);
|
|
17467
18293
|
return this.activePage;
|
|
17468
18294
|
}
|
|
17469
18295
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hira-core/sdk",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
4
4
|
"description": "SDK for building Hira automation flows with TypeScript",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"axios": "^1.13.4",
|
|
47
47
|
"exceljs": "^4.4.0",
|
|
48
|
+
"ghost-cursor": "^1.4.2",
|
|
48
49
|
"p-limit": "3.1.0"
|
|
49
50
|
}
|
|
50
51
|
}
|