@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 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.type(selector, text)` | Wait + clear + type |
196
- | `utils.getText(selector)` | Get text content |
197
- | `utils.exists(selector, timeout?)` | Check element exists |
198
- | `utils.waitForElement(selector, timeout?)` | Wait for element |
199
- | `utils.waitForNavigation()` | Wait for page navigation |
200
- | `utils.screenshot(path?)` | Take screenshot |
201
- | `utils.sleep(ms)` | Delay (respects abort signal) |
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 nghĩ tab luôn focused, không bị throttle
622
- * khi user chuyển sang tab khác hoặc click vào trình duyệt.
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. Scrolls the element into view before clicking.
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 an existing ElementHandle
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("#btn", { waitTimeout: 8000 }); // wait longer
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, options?: {
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 nghĩ tab luôn focused, không bị throttle
16832
- * khi user chuyển sang tab khác hoặc click vào trình duyệt.
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. Scrolls the element into view before clicking.
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 an existing ElementHandle
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("#btn", { waitTimeout: 8000 }); // wait longer
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
- (_a = options == null ? void 0 : options.waitTimeout) != null ? _a : 2e3,
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 element.scrollIntoView();
16936
- const clickDelay = (_b = options == null ? void 0 : options.delay) != null ? _b : 1e3;
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.click();
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 element.scrollIntoView();
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
- await element.click();
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
- await element.click({ clickCount: 3 });
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.7",
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
  }